--- /dev/null
+;;; -*- no-byte-compile: t; -*-
+((nil . ((tab-width . 8)
+ (fill-column . 80)
+ ;; (commment-fill-column . 80)
+ (emacs-lisp-docstring-fill-column . 65)
+ (bug-reference-url-format . "https://github.com/djcb/mu/issues/%s")))
+ (c-mode . ((c-file-style . "linux")
+ (indent-tabs-mode . t)
+ (mode . bug-reference-prog)))
+ (c-ts-mode . ((indent-tabs-mode . t)
+ (c-ts-mode-indent-style . linux)
+ (c-ts-mode-indent-offset . 8)
+ (mode . bug-reference-prog)))
+ (c++-mode . ((c-file-style . "linux")
+ (fill-column . 100)
+ ;; (comment-fill-column . 80)
+ (mode . bug-reference-prog)))
+ (c++-ts-mode . ((indent-tabs-mode . t)
+ (c-ts-mode-indent-style . linux)
+ (c-ts-mode-indent-offset . 8)
+ (mode . bug-reference-prog)))
+ (emacs-lisp-mode . ((indent-tabs-mode . nil)
+ (mode . bug-reference-prog)))
+ (lisp-data-mode . ((indent-tabs-mode . nil)))
+ (texinfo-mode . ((mode . bug-reference-prog)))
+ (org-mode . ((mode . bug-reference))))
--- /dev/null
+#-*-mode:conf-*-
+# editorconfig file (see EditorConfig.org), with some
+# lowest-denominator settings that should work for many editors.
+
+
+root = true # this is the top-level
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+trim_trailing_whitespace = true
+
+# The "best" answer is "tabs-for-indentation; spaces for alignment".
+
+[*.{cc,cpp,hh,hpp}]
+indent_style = tab
+indent_size = 8
+max_line_length = 90
+
+[*.{c,h}]
+indent_style = tab
+indent_size = 8
+max_line_length = 80
+
+[configure.ac]
+indent_style = tab
+indent_size = 4
+max_line_length = 100
+
+[Makefile.am]
+indent_style = tab
+indent_size = 8
+max_line_length = 100
--- /dev/null
+---
+name: Mu4e Feature request
+about: Suggest an idea for this project
+title: "[mu4e rfe]"
+labels: rfe, mu4e, new
+assignees: ''
+
+---
+Note, please see the IDEAS.org file in repository root for existing ideas;
+maybe it's already there.
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
--- /dev/null
+---
+name: Guile
+about: mu-guile related item
+title: "[guile]"
+labels: new, guile
+assignees: ''
+
+---
+
+**Describe the item**
+A clear and concise description of what you expected or wished to happen and what actually happened while using mu-guile.
+
+**To Reproduce**
+Steps to reproduce the behavior.
+
+**Environment**
+Please describe the versions of OS, Emacs, mu/mu4e etc. you are using.
+
+**Checklist**
+- [ ] you are running either the latest 1.4.x release, or a 1.5.11+ development release (otherwise, please upgrade).
--- /dev/null
+---
+name: Misc
+about: Miscellaneous items you want to share
+title: "[misc]"
+labels: new
+assignees: ''
+
+---
+
+**Note**: for questions, please use the mailing-list: https://groups.google.com/g/mu-discuss
+
+**Describe the issue**
+A clear and concise description, i.e. what you expected/desired to happen and what actually happened.
+
+**Environment**
+If applicable, please describe the versions of OS, Emacs, mu etc. you are using.
--- /dev/null
+---
+name: Mu Bug Report
+about: Create a report to help us improve
+title: "[mu bug]"
+labels: bug, mu, new
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is, what you expected to happen and what actually happened.
+
+**To Reproduce**
+Detailed steps to reproduce the behavior. If this is about a specific (kind of) message, **always** attach an (anonymized as need) example message.
+
+**Environment**
+Please describe the versions of OS, Emacs, mu etc. you are using.
+
+**Checklist**
+- [ ] you are running either the latest 1.8.x/1.10.x release or `master` (otherwise, please upgrade).
--- /dev/null
+---
+name: Mu4e Bug Report
+about: Create a report to help us improve
+title: "[mu4e bug]"
+labels: bug, mu4e, new
+assignees: ''
+---
+
+**Describe the bug**
+
+Give the bug a good title.
+
+Please provide a clear and concise description of what you expected to happen
+and what actually happened, and follow the steps below.
+
+**How to Reproduce**
+
+Include the exact steps of what you were doing (commands executed etc.). Include
+any relevant logs and outputs:
+
+- Best start from `emacs -Q`, and load a minimal `mu4e` setup; describe the steps
+ that lead up to the bug.
+- Does the problem happen each time? Sometimes?
+- If this is about a specific (kind of) message, attach an example message.
+ (Open the message, press `.` (`mu4e-view-raw-message`), then `C-x C-w` and
+ attach. Anonymize as needed, all that matters is that the issue still
+ reproduces.
+
+**Environment**
+
+Please describe the versions of OS, Emacs, mu/mu4e etc. you are using.
+
+**Checklist**
+
+- [ ] you are running either an 1.10.x/1.12.x release or `master` (otherwise please upgrade)
+- [ ] you can reproduce the problem without 3rd party extensions (including Doom/Evil, various extensions etc.)
+- [ ] you have read all of the above
+
+Please make sure you all items in the checklist are set/met before filing the ticket.
+
+Thank you!
--- /dev/null
+# Important! Before filing an issue, please consider the following:
+
+ * Ensure your mu/mu4e setup is no older than the latest stable release (1.6.x).
+
+ * Disable any third-party mu4e extensions; this includes customizations like the ones in "Doom" /
+ "Evil" etc.
+
+ * If a problem occurs with a certain (type of) message, attach an (anonymized) example of
+ such a message
+
+ * Please provide some minimal steps to reproduce
+
+ * Please follow the below template
+
+ Thanks!
+
+## Expected or desired behavior
+
+Please describe the behavior you expect or want
+
+## Actual behavior
+
+Please describe the behavior you are actually seeing.
+
+For bug-reports, if applicable, include error messages, emacs stack traces, example messages
+etc. Try to be as specific as possible - when do you see this happening? Does it happen always?
+Sometimes? How often?
+
+## Steps to reproduce
+
+For bug-reports, please describe in as much detail as possible how one can reproduce the problem.
+
+If there's a problem with a specific (type of) message, please attach such a message to the report.
+
+## Versions of mu, mu4e/emacs, operating system etc.
+
+## Any other detail
+
+E.g. are you using the gnus-based message view?
--- /dev/null
+name: Build & run tests
+
+on:
+ - push
+ - pull_request
+
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+ - macos-latest
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - if: contains(matrix.os, 'ubuntu')
+ name: ubuntu-deps
+ run: |
+ sudo apt update
+ sudo apt-get install meson ninja-build libglib2.0-dev libxapian-dev libgmime-3.0-dev libcld2-dev pkg-config guile-3.0-dev emacs texinfo
+
+ - if: contains(matrix.os, 'macos')
+ name: macos-deps
+ run: |
+ brew install meson ninja libgpg-error libtool pkg-config glib gmime xapian guile emacs texinfo
+
+ - name: configure
+ run: ./autogen.sh # '-Db_sanitize=address'
+
+ - name: build
+ run: make
+
+ - name: test
+ run: make test-verbose-if-fail
--- /dev/null
+www/mu
+mug
+/mu/mu
+/mu/mu-help-strings.h
+mug2
+.desktop
+*html
+.deps
+.libs
+autom4te*
+Makefile
+Makefile.in
+INSTALL
+aclocal.m4
+config.*
+configure
+install-sh
+depcomp
+libtool
+ltmain.sh
+
+# Added automatically by `autoreconf`
+m4/libtool.m4
+m4/ltoptions.m4
+m4/ltsugar.m4
+m4/ltversion.m4
+m4/lt~obsolete.m4
+
+missing
+nohup.out
+vgdump
+stamp-h1
+GPATH
+GRTAGS
+GSYMS
+GTAGS
+*.lo
+*.o
+*.la
+*.x
+*.go
+*.gz
+*.bz2
+\#*
+*.aux
+*.cp
+*.fn
+*.info
+*.ky
+*.log
+*.pg
+*.toc
+*.tp
+*.vr
+*.elc
+*.gcda
+*.gcno
+*.trs
+*.exe
+*.lib
+aminclude_static.am
+elisp-comp
+elc-stamp
+dummy.cc
+msg2pdf
+gmime-test
+test-mu-cmd
+test-mu-cmd-cfind
+test-mu-contacts
+test-mu-container
+test-mu-date
+test-mu-flags
+test-mu-maildir
+test-mu-msg
+test-mu-msg-fields
+test-mu-query
+test-mu-runtime
+test-mu-store
+test-mu-str
+test-mu-threads
+test-mu-util
+test-parser
+test-tokenizer
+test-utils
+tokenize
+test-command-parser
+test-mu-utils
+test-sexp-parser
+test-scanner
+/guile/tests/test-mu-guile
+
+mu4e-config.el
+mu4e.pdf
+texinfo.tex
+texi.texi
+*.tex
+*.pdf
+/www/auto/*
+configure.lineno
+/test.xml
+/mu4e/mdate-sh
+/mu4e/mu4e-about.el
+/mu4e/stamp-vti
+/mu4e/version.texi
+/lib/doxyfile
+/version.texi
+/compile
+/TAGS
+parse
+
+*_flymake.*
+*_flymake_*
+/perf.data
+perf.data
+perf.data.old
+*vgdump
+/lib/asan.log*
+/man/mu-mfind.1
+/mu/mu-memcheck
+mu-*-coverage
+mu*tar.xz
+compile_commands.json
+/lib/utils/test-sexp
+/lib/utils/test-option
+/lib/test-mu-threader
+/lib/test-mu-tokenizer
+/lib/test-mu-parser
+/lib/test-mu-query-threader
+/lib/test-contacts
+/lib/test-flags
+/lib/test-maildir
+/lib/test-msg
+/lib/test-msg-fields
+/lib/test-query
+/lib/test-store
+/lib/test-threader
+/mu/test-cmd
+/mu/test-cmd-cfind
+/mu/test-query
+/mu/test-threads
+/lib/test-threads
--- /dev/null
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
--- /dev/null
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
--- /dev/null
+ 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>.
--- /dev/null
+#+STARTUP:showall
+* IDEAS
+
+Ideas for future enhancements. We collect those here so they don't clutter up
+the Github issue list, i.e. without any clear plan for adding in the near
+future.
+
+- Ability to _mute_ message threads. This is useful but also requires quite bit of extra infra; we could add some blacklist for "muted" messages, perhaps on the 'mu server' side, but then we'd need some way to manage that (ie., unmute).
+ https://github.com/djcb/mu/issues/636
+
+- Support automatic handling for List-Unsubscribe headers
+ https://github.com/djcb/mu/issues/2623 This seems useful, but probably
+ requires a lot of testing to get right.
+
+- Allow for *muting* messages https://github.com/djcb/mu/issues/636 Useful;
+ probably need to do this by *remembering* the thread-id of muted messages; and
+ management (unmute etc.). Perhaps at the mu side, a list of thread-id to add
+ to each query for what *not* to match.
+
+- Support *creating* calendar invitations.
+ https://github.com/djcb/mu/issues/2308
+ Shouldn't be _too_ hard, for someone that uses the functionality.
+
+- Make sorting stable if there are multiple messages with the same date. We
+ _could_ do this by adding some random millisecs to each messasge's timestamp; _or_
+ complicating the search (i.e., the message hash?). Maybe leave as is?
+ https://github.com/djcb/mu/issues/2527
+
+- Include "message summary" in message information, for display in the headers
+ buffer: https://github.com/djcb/mu/issues/1821 It's not so easy to get a
+ useful one line description... perhaps the first line after the "Dear x,"?
+ Moreover, this requires new functionality on the headers-view side as well.
+
+- Support indexing PDF (and other) attachments. This can be done extending
+ process_message_part in mu-message.cc; instead of using something
+ PDF-specific, we could pipe a PDF through some tool to extract text; and we'd
+ need some way for users to specify a MIME-type => tool mapping (in Config).
+ https://github.com/djcb/mu/issues/2117
+
+- Support "aggregate actions" apply to a set of messages, e.g. apply patch-set
+ in a set of messages. That'll require some advanced scripting, maybe using
+ Guile.
+ https://github.com/djcb/mu/issues/301
+
+* Done
+
+- Support mu4e-mark-handle-when also for when leaving emacs
+ (kill-emacs-query-functions).
+ https://github.com/djcb/mu/issues/2649
--- /dev/null
+## Copyright (C) 2008-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## 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, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+# Makefile with some useful targets for meson/ninja
+V ?= 0
+
+BUILDDIR ?= $(CURDIR)/build
+BUILDDIR_COVERAGE ?= $(CURDIR)/build-coverage
+BUILDDIR_VALGRIND ?= $(CURDIR)/build-valgrind
+BUILDDIR_BENCHMARK ?= $(CURDIR)/build-benchmark
+
+GENHTML ?= genhtml
+LCOV ?= lcov
+MAKEINFO ?= makeinfo
+MESON ?= meson
+NINJA ?= ninja
+VALGRIND ?= valgrind
+
+ifneq ($(V),0)
+ VERBOSE=--verbose
+endif
+
+# when MU_HACKER is set, do a debug build
+# MU_HACKER is for djcb & compatible developers
+# note that mu uses C++17, we only pass C++23 here
+# for the better error messages (esp. for fmt).
+ifneq (${MU_HACKER},)
+MESON_FLAGS:=$(MESON_FLAGS) '-Dbuildtype=debug' \
+ '-Db_sanitize=address' \
+ '-Dreadline=enabled' \
+ '-Dcpp_std=c++23'
+endif
+
+.PHONY: all build-valgrind
+.PHONY: check test test-verbose-if-fail test-valgrind test-helgrind
+.PHONY: benchmark coverage
+.PHONY: dist install uninstall clean distclean
+.PHONY: mu4e-doc-html
+
+# MESON_FLAGS, e.g. "-Dreadline=enabled"
+
+# examples:
+# 1. build with clang, and the thread-sanitizer
+# make clean all MESON_FLAGS="-Db_sanitize=thread" CXX=clang++ CC=clang
+all: $(BUILDDIR)
+ @$(MESON) compile -C $(BUILDDIR) $(VERBOSE)
+ @ln -sf $(BUILDDIR)/compile_commands.json $(CURDIR) || /bin/true
+
+$(BUILDDIR):
+ @$(MESON) setup $(MESON_FLAGS) $(BUILDDIR)
+
+check: test
+
+test: all
+ @$(MESON) test $(VERBOSE) -C $(BUILDDIR)
+
+install: $(BUILDDIR)
+ @$(MESON) install -C $(BUILDDIR) $(VERBOSE)
+
+uninstall: $(BUILDDIR)
+ @$(NINJA) -C $(BUILDDIR) uninstall
+
+clean:
+ @rm -rf $(BUILDDIR) $(BUILDDIR_COVERAGE) $(BUILDDIR_VALGRIND) $(BUILDDIR_BENCHMARK)
+ @rm -rf compile_commands.json
+
+#
+# below targets are just for development/testing/debugging. They may or
+# may not work on your system.
+#
+test-verbose-if-fail: all
+ $(MESON) test -C $(BUILDDIR) || $(MESON) test -C $(BUILDDIR) --verbose
+
+build-valgrind: $(BUILDDIR_VALGRIND)
+ @$(MESON) compile -C $(BUILDDIR_VALGRIND) $(VERBOSE)
+
+$(BUILDDIR_VALGRIND):
+ @$(MESON) setup --buildtype=debug $(BUILDDIR_VALGRIND)
+
+vg_opts:=--enable-debuginfod=no --leak-check=full --error-exitcode=1
+test-valgrind: export G_SLICE=always-malloc
+test-valgrind: export G_DEBUG=gc-friendly
+test-valgrind: build-valgrind
+ @$(MESON) test -C $(BUILDDIR_VALGRIND) \
+ --wrap="$(VALGRIND) $(vg_opts)" \
+ --timeout-multiplier 100
+
+check-valgrind: test-valgrind
+
+# we do _not_ pass helgrind; but this seems to be a false-alarm
+# https://gitlab.gnome.org/GNOME/glib/-/issues/2662
+test-helgrind: $(BUILDDIR_VALGRIND)
+ $(MESON) -C $(BUILDDIR_VALGRIND) test \
+ --wrap="$(VALGRIND) --tool=helgrind --error-exitcode=1" \
+ --timeout-multiplier 100
+
+check-helgrind: test-helgrind
+
+#
+# benchmarking
+#
+
+$(BUILDDIR_BENCHMARK):
+ @$(MESON) setup --buildtype=debugoptimized $(BUILDDIR_BENCHMARK)
+
+build-benchmark-target: $(BUILDDIR_BENCHMARK)
+ @$(MESON) compile -C $(BUILDDIR_BENCHMARK) $(VERBOSE)
+
+benchmark: build-benchmark-target
+ $(NINJA) -C $(BUILDDIR_BENCHMARK) benchmark
+
+#
+# coverage
+#
+
+$(BUILDDIR_COVERAGE):
+ $(MESON) setup -Db_coverage=true --buildtype=debug $(BUILDDIR_COVERAGE)
+
+covfile:=$(BUILDDIR_COVERAGE)/meson-logs/coverage.info
+
+# generate by hand, meson's built-ins are rather inflexible
+coverage: $(BUILDDIR_COVERAGE)
+ @$(MESON) compile -C $(BUILDDIR_COVERAGE)
+ @$(MESON) test -C $(BUILDDIR_COVERAGE) $(VERBOSE)
+ $(LCOV) --capture --directory . --output-file $(covfile)
+ @$(LCOV) --remove $(covfile) '/usr/*' '*guile*' '*thirdparty*' '*/tests/*' '*mime-object*' --output $(covfile)
+ @$(LCOV) --remove $(covfile) '*mu/mu/*' --output $(covfile)
+ @mkdir -p $(BUILDDIR_COVERAGE)/meson-logs/coverage
+ @$(GENHTML) $(covfile) --output-directory $(BUILDDIR_COVERAGE)/meson-logs/coverage/
+ @echo "coverage report at: file://$(BUILDDIR_COVERAGE)/meson-logs/coverage/index.html"
+
+#
+# misc
+#
+
+dist: $(BUILDDIR)
+ $(MESON) compile -C $(BUILDDIR) $(VERBOSE)
+ $(MESON) dist -C $(BUILDDIR) $(VERBOSE)
+
+distclean: clean
+
+HTMLPATH=${BUILDDIR}/mu4e/mu4e
+mu4e-doc-html:
+ @mkdir -p ${HTMLPATH} && cp mu4e/texinfo-klare.css ${HTMLPATH}
+ @cd mu4e; makeinfo -v -I ${BUILDDIR} -I ${BUILDDIR}/mu4e --html --css-ref=texinfo-klare.css -o ${HTMLPATH} mu4e.texi
--- /dev/null
+#+STARTUP:showall
+* NEWS (user visible changes & bigger non-visible ones)
+
+* 1.12 (released on February 24, 2024)
+
+** Some highlights
+
+ - Significant speedups in both ~mu~ and ~mu4e~
+ - Reworked message composition, closer to its Gnus origins which adds many of its features
+ - Overhauled the query parser; squashing a number of bugs/limitations, incl. dealing
+ with CJK messages
+ - Experimental folding of message threads
+ - Better and faster indexing of HTML messages
+ - Experimental search by (human) language wit CLD2
+
+ For details & more, see below. Note a few minor new features were added
+ _after_ the initial 1.12.0.
+
+*** mu
+
+ - new command ~mu move~ to move messages across maildirs and/or change their
+ flags. See the manpage for all the details.
+
+ - ~mu~ commands ~extract~ ~verify~ and ~view~ can now read the message from
+ standard input; see their man-pages for details
+
+ - ~mu init~ gained the ~--ignored-address~ option for email-addresses / regexps
+ that should _not_ be included in the contacts-cache (i.e., for ~mu cfind~ and
+ Mu4e address completion). See the ~mu-init~ manpage for details.
+
+ It's not unusual for ~noreply~-type e-mail addresses to be the majority in
+ an e-mail corpus; to get rid of those, with something like
+ ~--ignored-address=/.*no.*reply.*/~
+
+ - what used to be the ~mu fields~ command has been merged into ~mu info~; i.e.,
+ ~mu fields~ is now ~mu info fields~.
+
+ - ~mu view~ gained ~--format=html~ which compels it to output the HTML body of
+ the message rather than the (default) plain-text body. See its updated
+ manpage for details.
+
+ - when encountering an HTML message part during indexing, previously (i.e.,
+ ~mu 1.10~) we would attempt to process that as-is, with HTML-tags etc.; this
+ is now improved by employing a custom html->text scraper which extracts
+ the human-readable text from the html.
+
+ - mu querying and (esp.) showing results has been made significantly faster;
+ e.g., in one big ~mu find~ query we went from ~47s to only ~7s
+
+ - /experimental/: if you build ~mu~ with [[https://github.com/CLD2Owners/cld2][CLD2]] support (available in many Linux
+ distros), ~mu~ will try to detect the language of the body of e-mail
+ messages; you can then search by their ISO-639-1 code, e.g.:
+ ~$ mu find lang:en~
+
+ the matching is not perfect, and seems to favor non-English if there's a
+ mostly English message with some other language mixed in.
+
+ this does require re-indexing the database.
+
+ - set the default database batch-size (using the ~mu init~ command) to 50000
+ rather than 250000; the latter was too high for systems with limited
+ memory. You can of course change that with ~--batch-size=...~
+
+ - restore expansion for path options such as ~--maildir=~/Maildir~ (to e.g.
+ ~/home/user/Maildir~) for shells that do not do that, such as Bash.
+
+ - overhauled the query-parser; this is (should be) compatible with the older
+ one, apart from a number of fixes. There is a new option ~--analyze~ for the
+ ~mu find~ command, which shows the parsed query in a (hopefully)
+ human-readable s-expression form; this can be used to debug your queries
+ (this replaces the older ~--format=mquery|xquery~)
+
+ Furthermore, there now support for "ngram"-based indexing and querying,
+ which is useful for languages/scripts without explicit word-breaks, such
+ as Chinese/Japanese/Korean. See the *mu-init* manpages, in particular the
+ ~--support-ngrams~ option, and why you may (or may not) want to enable that.
+
+ - the build has been made reproducible
+
+*** mu4e
+
+**** message composer
+
+ - Overhaul of the message composer; it is now closer to the Gnus/Message
+ composer functions (e.g. the whole mu4e-specific draft setup is gone);
+ this reduces code size and offers some new capabilities.
+
+ More of the ~message-~ functionality can be used now in ~mu4e~.
+
+ - Variables ~mu4e-compose-signature~, ~mu4e-compose-cite-function~ are gone
+ (with aliases in place), use ~message-signature~, ~message-cite-function~
+ instead. There's a special ~mu4e-message-cite-nothing~ for the case where
+ you do not want to cite anything.
+
+ - There's a new function ~mu4e-compose-wide-reply~ (bound to =W=) which does a
+ wide-reply, a.k.a., 'reply to all'. So ~mu4e-compose-reply-recipients~ is
+ not needed anymore and has been obsoleted (and doesn't do anything).
+ ~mu4e-compose-reply-ignore-address~ is no longer supported, use
+ ~message-prune-recipient-rules~ instead.
+
+ Same for ~mu4e-compose-dont-reply-to-self~; roughly the same effect can be
+ achieved by setting ~message-dont-reply-to-names~ to
+ ~#'mu4e-personal-or-alternative-address-p~. This only works for
+ [[info:(message) Wide Reply][wide-replies]].
+
+ - Another new function is ~mu4e-compose-supersede~ (not bound to any key by
+ default), with which you can /supersede/ your own messages; that is, send
+ the message as a kind-of reply to the same recipients. This only works if
+ you were the sender.
+
+ - The special mailing list handling is gone; ~mu4e-compose-reply~ and
+ ~mu4e-compose-wide-reply~ should take care of that. There's also
+ ~message-reply-to-function~ for ultimate control; see [[info:(message)
+ Reply][info (message) Reply]] for details.
+
+ - ~mu4e-compose-in-new-frame~ has been generalized (in a backward-compatible
+ way) to ~mu4e-compose-switch~, which lets you decide whether a message
+ should be composed in the current window (default), a new window or a new
+ frame.
+
+ - ~mu4e-compose-context-switch~ is gone; it was a little too fragile. Best
+ change when creating the message (=mu4e= asks you by default, see
+ ~mu4e-compose-context-policy~).
+
+ - there's a new hook ~mu4e-compose-post-hook~ which fires when message
+ composition is complete - either a message has been sent, it is postponed,
+ canceled etc. (1.12.5).
+
+ - iCalendar support is a work-in-progress with the new editor. One change is
+ that support is now _automatically_ available.
+
+**** other
+
+ - New command ~mu4e-search-query~ (bound to =c=) which lets you pick a query
+ (from bookmark / maildir shortcuts) with completion in main / headers /
+ view buffers.
+
+ - improved support for dealing with attachments and other MIME-parts in the
+ message view; they gained completions support with annotations in the
+ minibuffer
+
+ It is possible to save all attachments at once with =C-c C-a=, except with
+ Helm, which uses its own mechanism for this. This same has been extended
+ to the MIME-part actions.
+
+ - experimental: support folding message threads (with =TAB= / =S-TAB=). See the
+ [[info:mu4e:Folding threads][entry in the Mu4e manual]] for further details.
+
+ - mailing list support was modernized a bit; the format changed (see the
+ ~mu4e-mailing-lists~ and ~mu4e-user-mailing-lists~ docstrings. There is
+ ~M-x mu4e-mailing-list-info-refresh~ to update to the new values after
+ changing them.
+
+ - also, there are now actions ('a' in view/header) to get to online archives
+ for some (selected) mailing-list archives.
+
+ - ~mu4e-quit~ now takes a prefix argument which, if provided, causes it to
+ bury the mu main buffer, rather than quitting mu. ~mu4e~ will now just
+ switch the mu4e buffer if it exists (otherwise it starts ~mu4e~).
+
+ - ~mu4e~ queries are much snappier now, due to the mentioned speed-ups in
+ querying; ~mu4e~ also adds a new optimization =mu4e-mu-allow-temp-file=
+ (turned off by default), which speed up things further; e.g., for showing
+ 500 messages (debug build), we went from 642ms to 247ms, given an
+ in-memory temp file.
+
+ If and how much this helps, depends on your setup, see the
+ =mu4e-mu-allow-temp-file= docstring for details on how to determine this.
+
+ - Maildir lists are now generated server-side; so e.g. jumping to the 'jo'
+ /other/ Maildirs used to be quite slow the first time, but is now very fast.
+
+ ~mu4e-cache-maildir-list~ is obsolete / non-functional now.
+
+ - after retrieving mail (~mu4e-update-mail-and-index~), save the output of the
+ retrieval command in a buffer =*mu4e-last-update*=, = which can be useful
+ for diagnosis.
+
+ - links (in text-mode emails) are now clickable through <mouse-2>, to be
+ consistent with eww.
+
+ - support new-mail notifications on MacOS out-of-the-box
+
+ - allow sorting by tag
+
+ - ~mu4e~ now follows Emacs' ~package~ guidelines
+
+*** Contributors
+
+ Thanks to our contributors - code committers belows, but also to everyone
+ who filed tickets, asked questions, answered them etc.
+
+ Babak Farrokhi, Christophe Troestler, Christoph Reichenbach, Daniel Fleischer,
+ David Edmondson, Davide Masserut, Dirk-Jan C. Binnema, Jeremy Sowden,
+ Lin Jian, Martin R. Albrecht, Nacho Barrientos, Nicholas Vollmer,
+ Nicolas P. Rougier, ramon diaz-uriarte (at Phelsuma), reindert, Ruijie Yu,
+ Sean Farley, stardiviner, Tassilo Horn and Thierry Volpiatto
+
+
+* Old news
+ :PROPERTIES:
+ :VISIBILITY: folded
+ :END:
+
+** 1.10 (released on March 26, 2023)
+
+*** mu
+
+ - a new command-line parser, which allows (hopefully!) for a better user
+ interaction; better error checking and more
+
+ - Invalid e-mail addresses are no longer added to the contacts-cache.
+
+ - The ~cfind~ command gained ~--format=json~, which makes it easy to further
+ process contact information, e.g. using ~jq~. See the manpage for more
+ details.
+
+ - The ~init~ command learned ~--reinit~ to reinitialize the database with the
+ settings of an existing one
+
+ - The ~script~ command is gone, and integrated with ~mu~ directly, i.e. the
+ scripts (when enabled) are directly visible in the ~mu~ output. Also see the
+ Guile section.
+
+ - The ~extract~ command gained the ~--uncooked~ option to tell it to _not_ replace
+ spaces with dashes in extracted filenames (and a few other things).
+
+ - Revamped manpages which are now generated from ~org~ descriptions
+
+ - Standardize on PCRE-flavored regular expressions throughout *mu*.
+
+ - ~mu~ no longer attempts to 'expand' the =~= (and some other characters) in
+ command line options that take filenames, since it was a bit unpredictable.
+ So write e.g. ~--option=/home/user/hello~ instead of ~--option=~/hello~
+
+ - Experimental: as bit of a hack, html message bodies are processed as if
+ they were plain text, similar how "old mu" would do it (1.6.x and earlier).
+ A nicer solution would be to convert to text, but this something for the
+ future.
+
+ - the MSYS2 (Windows) builds is _experimental_ now; some things may not work;
+ see e.g. https://github.com/djcb/mu/issues?q=is%3Aissue+label%3Amsys, but
+ we welcome efforts to fix those things.
+
+*** mu4e
+
+ - ~emacs~ 26.3 or higher is now required for ~mu4e~
+
+ - ~mu4e-view-mode-hook~ now fires before the message is rendered. If you have
+ hook-functions that depend on the message contents, you should use
+ the new ~mu4e-view-rendered-hook~.
+
+ - mu4e window management has been completely reworked and cleaned up,
+ affecting the message loading as well as the window-layout. As a
+ user-visible feature, there's now the =z= binding (~mu4e-view-detach~), to
+ 'detach' view and alllow for keV Detaching and reattaching][manual entry]] for further
+ details.
+
+ - As a result of that, ~mu4e-split-view~ can no longer be a function; the new
+ way is to use ~display-buffer-alist~ as explained in the [[info:mu4e:Buffer Display][manual]]
+
+ - ~mu4e~ now keeps track of 'baseline' query results and shows the difference
+ from that in the main view and modeline (you'll might see something like
+ =1(+1)/2= for your bookmarks or in the modeline; that means that there is
+ one more unread message since baseline; see the [[info:mu4e#Bookmarks and Maildirs][manual entry]] for details.
+
+ The idea is that you get a quick overview of where changes happened while
+ you were doing something else. This is a somewhat experimental feature
+ which is under active development
+
+ - Related to that, you can now crown one of your bookmarks in =mu4e-bookmarks=
+ with ~:favorite t~, causing it to be highlighted in the main view and used
+ in the mode-line. See the new [[info:mu4e#Modeline][modeline entry]] in the manual; this uses the
+ new =mu4e-modeline-mode= minor-mode.
+
+ - Expanding on that further, you can also get desktop notifications for new
+ mail (on systems with DBus for now; see [[info:mu4e:#Desktop notifications][Desktop notifications]] in the
+ manual.
+
+ - If your search query matches some bookmark, the modeline now shows the
+ bookmark's name rather than the query; this can be controlled through
+ =mu4e-modeline-prefer-bookmark-name= (default: =t=).
+
+ - You can now tell mu4e to use emacs' completion system rather than the mu4e
+ built-in one; see the variables ~mu4e-read-option-use-builtin~ and
+ ~mu4e-completing-read-function~; e.g. to always emacs completion (which
+ may have been enhanced by various completion frameworks), use:
+ #+begin_src elisp
+ (setq mu4e-read-option-use-builtin nil
+ mu4e-completing-read-function 'completing-read)
+ #+end_src
+
+ - when moving messages (which includes changing flags), file-flags changes
+ are propagated to duplicates of the messages; that is, e.g. the /Seen/ or
+ /Replied/ status is propagated to all duplicates (earlier, this was only
+ done when marking a message as read). Note, /Draft/, /Flagged/ and /Trashed/
+ flags are deliberately *not* propagated.
+
+ - Teach ~mu4e-copy-thing-at-point~ about ~shr~ links
+
+ - The ~mu4e-headers-toggle-setting~ has been renamed
+ ~mu4e-headers-toggle-property~ and has the new default binding ~P~, which
+ works in both the headers-view and message-view. The older functions
+ ~mu4e-headers-toggle-threading~, ~mu4e-headers-toggle-threading~,
+ ~mu4e-headers-toggle-full-search~ ~mu4e-headers-toggle-include-related~,
+ ~full-search~skip-duplicates~ have been removed (with their keybindings) in
+ favor of ~mu4e-headers-toggle-property~.
+
+ - There's also a new property ~mu4e-headers-hide-enabled~, which controls
+ wheter ~mu4e-headers-hide-predicate~ is applied (when non-~nil~). This can be
+ used to temporarily turn the predicate off/on.
+
+ - You can now jump to previous / next threads in headers-view, message view.
+ Default binding is ~{~ and ~}~, respectively.
+
+ - When searching, the number of hidden messages is now shown in the
+ message footer along with the number of Found messages
+
+ - The ~eldoc~ support in header-mode is now optional and disabled by default;
+ set ~mu4e-eldoc-support~ to non-nil to enable it.
+
+ - In the main view, the keybindings shown are a representation of the actual
+ keybindings, rather than just the defaults. This is for the benefit for
+ people who want to use different keybindings.
+
+ - As a side-effect of that, ~mu4e-main-mode~ and ~mu4e-main-mode-hook~ functions
+ are now invoked _before_ the rendering takes place; if you're customizations
+ depend on happening after rendering is completed, use the new
+ ~mu4e-main-rendered-hook~ instead.
+
+ - ~mu4e-cache-maildir-list~ has been promoted to be a =defcustom=, enabled by
+ default. This caches the list of "other" maildirs (i.e., without a
+ shortcut).
+
+ - For testing, a new command ~mu4e-server-repl~ to start a ~mu~ server just as
+ ~mu4e~ does it. Note that this cannot run at the same time when ~mu4e~ runs.
+
+ - all the obsolete function and variable aliases have been moved to
+ ~mu4e-obsolete.el~ so we can unclutter the non-obsolete code a bit.
+
+*** guile
+
+ - in the 1.8 release, the /current/ Guile API was deprecated; that does not
+ mean that Guile support goes way, just that it will look different.
+
+ - Guile script commands are now integrated with the main ~mu~, so without
+ further parameters ~mu~ shows both subcommands and scripts. This is a
+ work-in-progress!
+
+ - The per-(week|day|year|year-month) scripts have been combined into a
+ ~histogram~ script. If you have Guile-support enabled, and have ~gnuplot~
+ installed, you can do e.g.,
+
+#+begin_example
+ mu histogram -- --time-unit=day --query="hello"
+#+end_example
+
+ to get a histogram of such messages. Note, this area is under active
+ development and will likely change.
+
+*** building and installation
+
+ - the autotools build (which was deprecated since 1.8) has now been removed.
+ we thank it for its services since 2008. We continue with ~meson~.
+
+ However, we still have ~autogen.sh~ and a ~Makefile~ which can be helpful for
+ driving ~meson~-based builds. Think of the ~Makefile~ as a convenient place to
+ put common action for which I always forget the ~meson~ incantation.**
+
+ - ~meson~ 56.0 or higher is required for building
+
+ - ~emacs~ 26.3 or higher is needed for ~mu4e~
+
+*** internals
+
+ As usual, there have been a number of internal updates in the ~mu~ codebase:
+
+ - reworked the internal s-expression parser
+
+ - new command-line argument parser (based on CLI11)
+
+ - message-move flag propagation moved from the mu4e-server to mu-store
+
+ - more =mu4e~= internals have been renamed/reworked in to ~mu4e--~.
+
+*** contributor to this release
+
+ Aimé Bertrand, Aleksei Atavin, Al Haji-Ali, Andreas Hindborg, Anton Tetov,
+ Arsen Arsenović, Babak Farrokhi, Ben Cohen, Damon Kwok, Daniel Colascione,
+ Derek Zhou, Dirk-Jan C. Binnema, John Hamelink, Leo Gaskin, Manuel
+ Wiesinger, Marcel van der Boom, Mark Knoop, Mickey Petersen, Nicholas
+ Vollmer, Protesilaos Stavrou, Remco van 't Veer, Sean Allred, Sean Farley,
+ Stephen Eglen, Tassilo Horn
+
+ And of course all the people how filed tickets, asked question, provided
+ suggestions.
+
+
+** 1.8 (released on June 25, 2022)
+
+ (there are some changes in the installation procedure compared to 1.6.x; see
+ Installation below)
+
+**** mu
+
+ - The server protocol (as used my mu4e) has seen a number of updates, to
+ allow for faster rendering. As before, there's no compatibility between
+ minor release numbers (1.4 vs 1.6 vs 1.8) nor within development series
+ (such as 1.7). However, within a stable release (such as all 1.6.x) the
+ protocol won't change (except if required to fix some severe bug; this
+ never happened in practice)
+
+ - The ~processed~ number in the indexing statistics has been renamed into
+ ~checked~ and describes the number of message files considered for updating,
+ which is a bit more useful that the old value, which was more-or-less
+ synonymous with the ~updated~ number (which are the messages that got
+ (re)parsed / (re)added to the database.
+
+ Basically, it counts all the messages for which we checked their timestamp.
+
+ - The internals of the message handling in ~mu~ have been heavily reworked;
+ much of this is not immediately visible but is an enabler for some new
+ features.
+
+ - instead of passing ~--muhome~, you can now also set an environment variable
+ ~MUHOME~.
+
+ - the ~info~ command now includes information about the last indexing
+ operation and the last database change that took place; note that the
+ information may be slightly delayed due to database caching.
+
+ - the ~verify~ command for checking signatures has been updated, and is more
+ informative
+
+ - a new command ~fields~ provides information about the message fields and
+ flags for use in queries. The information is the same information that ~mu~
+ uses and so stays up to date.
+
+ - a new message field ~changed~, which refers to the time/date of the last
+ time a message was changed (the file ~ctime~)
+
+ - new message flags ~personal~ to search for "personal" messages, which are
+ defined as a message with at least one personal contact, and ~calendar~ for
+ messages with calendar-invitations.
+
+ - message sexps are now cached in the store, which makes delivering
+ sexp-based search results (as used by ~mu4e~) much faster.
+
+ - Windows/MSYS support is deprecated; it doesn't work well (if at all) and
+ there's currently not sufficient developer interest/expertise to change
+ this.
+
+**** mu4e
+
+ - the old mu4e-view is *gone*; only the gnus-based one remains. This allowed
+ for removing quite a bit of old code.
+
+ - the mu4e headers rendering is much faster (a factor of 3+), which makes
+ displaying big results snappier. This required some updates in the headers
+ handling and in the server protocol. Separate from that, the cached
+ message sexps (see the ~mu~ section) make getting the results much faster.
+ This becomes esp. clear when there are a lot of query results.
+
+ - "related" messages are now recognizable as such in the headers-view, with
+ their own face, ~mu4e-related-face~; by default with an italic slant.
+
+ - For performance testing, you can set the variable
+ ~mu4e-headers-report-render-time~ to ~t~ and ~mu4e~ will report the
+ search/rendering speed of each query operation.
+
+ - Removed header-fields ~:attachments~, ~:signature~, ~:encryption~ and
+ ~:user-agent~. They're obsolete with the Gnus-based message viewer.
+
+ - The various "toggles" for the headers-view (full-search, include-related,
+ skip-duplicates, threading) were a bit hard to find and with non-obvious
+ key-bindings. For that, there is now ~mu4e-headers-toggle-setting~ (bound
+ to ~M~) to handle all of that. The toggles are also reflected in the
+ mode-line; so e.g. 'RTU' means we're including [R]elated messages, and show
+ [T]hreads, skip duplicates ([U]nique).
+
+ - A new ~defcustom~, ~mu4e-view-open-program~ for starting the appropriate
+ program for a give file (e.g., ~xdg-open~). There are some reasonable
+ defaults for various systems. This can also be set to a function.
+
+ - indexing happens in the background now and mu4e can interact with the
+ server while it is ongoing; this allows for using mu4e during lengthy
+ indexing operations.
+
+ - ~mu4e-index-updated-hook~ now fires after indexing completed, regardless of
+ whether anything changed (before, it fired only if something changed). In
+ your hook-functions (or elsewhere) you can check if anything changed using
+ the new variable ~mu4e-index-update-status~. And note that ~processed~ has
+ been renamed into ~checked~, with a slightly different meaning, see the mu
+ section.
+
+ - ~message-user-organization~ can now be used to set the ~Organization:~
+ header. See its docstring for details.
+
+ - ~mu4e-compose-context-switch~ no longer attempts to update the draft folder
+ (which turned out to be a little fragile). However, it has been updated to
+ automatically change the ~Organization:~ header, and attempts to update the
+ message signature. Also, there's a key-binding now: ~C-c ;~
+
+ - Changed the default for ~mu4e-compose-complete-only-after~ to 2018-01-01,
+ to filter out contacts not seen after that date.
+
+ - As an additional measure to limit the number of contacts that mu4e loads
+ for auto-completions, there's ~mu4e-compose-complete-max~, to set a precise
+ numerical match (*before* any possible filtering). Set to ~nil~ (no maximum
+ by default).
+
+ - Updated the "fancy" characters for some header fields. Added new ones for
+ personal and list messages.
+
+ - Removed ~make-mu4e-bookmark~ which was obsoleted in version 1.3.9.
+
+ - Add command ~mu4e-sexp-at-point~ for showing/hiding the s-expression for
+ the message-at-point. Useful for development / debugging. Bound to ~,~ in
+ headers and view mode.
+
+ - undo is now supported across message-saves
+
+ - a lot of the internals have been changed:
+
+ - =mu4e= is slowly moving from using the '=~'= to the more common '=--'=
+ separator for private functions; i.e., =mu4e-foo= becomes =mu4e--foo=.
+
+ - =mu4e-utils.el= had become a bit of a dumping ground for bits of code;
+ it's gone now, with the functionality move to topic-specific files --
+ =mu4e-folders.el=, =mu4e-bookmarks.el=, =mu4e-update.el=, and included in
+ existing files.
+
+ - the remaining common functionality has ended up in =mu4e-helpers.el=
+
+ - =mu4e-search.el= takes the search-specific code from =mu4e-headers.el=,
+ and adds a minor-mode for the keybindings.
+
+ - =mu4e-context.el= and =mu4e-update.el= also define minor modes with
+ keybindings, which saves a lot of code in the various views, since they
+ don't need explicitly bind all those function.
+
+ - also =mu4e-vars.el= had become very big, we're refactoring the =defvar= /
+ =defcustom= declarations to the topic-specific files.
+
+ - =mu4e-proc.el= has been renamed =mu4e-server.el=.
+
+ - Between =mu= and =mu4e=, contact cells are now represented as a plist ~(:name
+ "Foo Bar" :email "foobar@example.com")~ rather than a cons-cell ~("Foo
+ Bar" . "foobar@example.com").~
+
+ If you have scripts depending on the old format, there's the
+ ~mu4e-contact-cons~ function which takes a news-style contact and yields
+ the old form.
+
+ - Because of all these changes, it is recommended you remove older version
+ of ~mu4e~ before reinstalling.
+
+**** guile
+
+ - the current guile support has been deprecated. It may be revamped at some
+ point, but will be different from the current one, which is to be removed
+ after 1.8
+
+**** toys
+
+ - the ~toys~ (~mug~) has been removed, as they no longer worked with the rest of
+ the code.
+
+*** Installation
+
+ - =mu= switched to the [[https://mesonbuild.com][meson]] build system by default. The existing =autotools=
+ is still available, but is to be removed after the 1.8 release.
+
+ Using =meson= (which you may need to install), you can use something like
+ the following in the mu top source directory:
+
+#+BEGIN_SRC sh
+ $ meson build && ninja -C build
+#+END_SRC
+
+ - However, note that =autogen.sh= has been updated, and there's a
+ convenience =Makefile= with some useful targets, so you can also do:
+#+BEGIN_SRC sh
+ $ ./autogen.sh && make # and optionally, 'sudo make install'
+#+END_SRC
+
+ - After that, either =ninja -C build= or =make= should be enough to rebuild
+
+ - NOTE: development versions 1.7.18 - 17.7.25 had a bug where the mail file
+ names sometimes got misnamed (with some extra ':2,'). This can be restored
+ with something like:
+#+begin_example
+ $ find ~/Maildir -name '*:2,*:*' | \
+ sed "s/\(\([^:]*\)\(:2,\)\{1,\}\(:2,.*$\)\)/mv '\0' '\2\4'/" > rename.sh
+#+end_example
+ (replace 'Maildir' with the path to your maildir)
+
+ once this is done, do check the generated 'rename.sh' and after convincing
+ yourself it does the right thing, do
+#+begin_example
+ $ sh rename.sh
+#+end_example
+ after that, re-index.
+
+ - Before installing, it is recommended that you *remove* any older versions
+ of ~mu~ and especially ~mu4e~, since they may conflict with the newer ones.
+
+ - =mu= now requires C++17 support for building
+
+
+*** Contributor for this release
+
+ - As per ~git~: c0dev0id, Christophe Troestler, Daniel Fleischer, Daniel Nagy,
+ Dirk-Jan C. Binnema, Dr. Rich Cordero, Kai von Fintel, Marcelo Henrique
+ Cerri, Nicholas Vollmer, PRESFIL, Tassilo Horn, Thierry Volpiatto, Yaman
+ Qalieh, Yuri D'Elia, Zero King
+ - And of course all the people filing issues, suggesting features and helping
+ out on the maling list.
+
+
+
+
+** 1.6 (released, as of July 27 2021)
+
+ NOTE: After upgrading, you need to call ~mu init~, with your prefered parameters
+ before you can use ~mu~ / ~mu4e~. This is because the underlying database-schema
+ has changed.
+
+*** mu
+
+ - Where available (and with suitably equiped ~libglib~), log to the ~systemd~
+ journal instead of =~/.cache/mu.log=. Passing the ~--debug~ option to ~mu~
+ increases the amount that is logged.
+
+ - Follow symlinks in maildirs, and support moving messsages across
+ filesystems. Obviously, that is typically quite a bit slower than the
+ single-filesystem case, but can be still be useful.
+
+ - Optionally provide readline support for the ~mu~ server (when in tty-mode)
+
+ - Reworked the way mu generates s-expressions for mu4e; they are created
+ programmatically now instead of through string building.
+
+ - The indexer (the part of mu that scans maildirs and updates the message
+ store) has been rewritten so it can work asynchronously and take advantage
+ of multiple cores. Note that for now, indexing in ~mu4e~ is still a blocking
+ operation.
+
+ - Portability updates for dealing with non-POSIX systems, and in particular
+ VFAT filesystem, and building using Clang/libc++.
+
+ - The personal addresses (as per ~--my-address=~ for ~mu init~) can now also
+ include regular expressions (basic POSIX); wrap the expression in ~/~, e.g.,
+ ~--my-address='/.*@example.*/~'.
+
+ - Modernized the querying/threading machinery; this makes some old code a
+ lot easier to understand and maintain, and even while not an explicit
+ goal, is also faster.
+
+ - Experimental support for the Meson build system.
+
+*** mu4e
+
+ - Use the gnus-based message viewer as the default; the new viewer has quite
+ a few extra features compared to the old, mu4e-specific one, such as
+ faster crypto, support for S/MIME, syntax-highlighting, calendar
+ invitations and more.
+
+ The new view is superior in most ways, but if you still depend on
+ something from the old one, you can use:
+ #+begin_example
+ ;; set *before* loading mu4e; and restart emacs if you want to change it
+ ;; users of use-packag~ should can use the :init section for this.
+ (setq mu4e-view-use-old t)
+ #+end_example
+
+ (The older variable ~mu4e-view-use-gnus~ with the opposite meaning is
+ obsolete now, and no longer in use).
+
+ - Include maildir-shortcuts in the main-view with overall/unread counts,
+ similar to bookmarks, and with the same ~:hide~ and ~:hide-unread~ properties.
+ Note that for the latter, you need to update your maildir-shortcuts to the
+ new format, as explained in the ~mu4e-maildir-shortcuts~ docstring.
+
+ You can set ~mu4e-main-hide-fully-read~ to hide any bookmarks/maildirs that
+ have no unread messages.
+
+ - Add some more properties for use in capturing org-mode links to messages /
+ queries. See [[info:mu4e#Org-mode links][the mu4e manual]] for details.
+
+ - Honor ~truncate-string-ellipsis~ so you can now use 'fancy' ellipses for
+ truncated strings with ~(setq truncate-string-ellipsis "…")~
+
+ - Add a variable ~mu4e-mu-debug~ which, when set to non-~nil,~ makes the ~mu~
+ server log more verbosely (to ~mu.log~ or the journal)
+
+ - Better alignment in headers-buffers; this looks nicer, but is also a bit
+ slower, hence you need to enable ~mu4e-headers-precise-alignment~ for this.
+
+ - Support ~mu~'s new regexp-based personal addresses, and add
+ ~mu4e-personal-address-p~ to check whether a given string matches a personal
+ address.
+
+ - TAB-Completion for writing ~mu~ queries
+
+ - Switch the context for existing draft messages using
+ ~mu4e-compose-context-switch~ or ~C-c C-;~ in ~mu4e-compose-mode~.
+
+
+** 1.4 (released, as of April 18 2020)
+
+*** mu
+
+ - mu now defaults to the [[https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html][XDG Base Directory Specification]] for the default
+ locations for various files. E.g. on Unix the mu database now lives under
+ ~~/.cache/mu/~ rather than ~~/.mu~. You can still use the old location by
+ passing ~--muhome=~/.mu~ to various ~mu~ commands, or setting ~(setq
+ mu4e-mu-home "~/.mu")~ for ~mu4e~.
+
+ If your ~~/.cache~ is volatile (e.g., is cleared on reboot), you may want
+ use ~--muhome~. Some mailing-list dicussion suggest that's fairly rare
+ though.
+
+ After upgrading, you may wish to delete the files in the old location to
+ recover some diskspace.
+
+ - There's a new subcommand ~mu init~ to initialize the mu database, which
+ takes the ~--maildir~ and ~--my-address~ parameters that ~index~ used to take.
+ These parameters are persistent so ~index~ does not need (or accept) them
+ anymore. ~mu4e~ now depends on those parameters.
+
+ ~init~ only needs to be run once or when changing these parameters. That
+ implies that you need to re-index after changing these parameters. The
+ ~.noupdate~ files are ignored when indexing the first time after ~mu init~ (or
+ in general, when the database is empty).
+
+ - There is another new subcommand ~mu info~ to get information about the mu
+ database, the personal addresses etc.
+
+ - The contacts cache (which is used by ~mu cfind~ and ~mu4e~'s
+ contact-completion) is now stored as part of the Xapian database rather
+ than as a separate file.
+
+ - The ~--xbatchsize~ and ~--autoupgrade~ options for indexing are gone; both are
+ determined implicitly now.
+
+*** mu4e
+
+ - ~mu4e~ no longer uses the ~mu4e-maildir~ and ~mu4e-user-mail-address-list~
+ variables; instead it uses the information it gets from ~mu~ (see the ~mu~
+ section above). If you have a non-default ~mu4e-mu-home~, make sure to set
+ it before ~mu4e~ starts.
+
+ It is strongly recommended that you run ~mu init~ with the appropriate
+ parameters to (re)initialize the Xapian database, as mentioned in the
+ mu-section above.
+
+ The main screen shows your address(es), and issues a warning if
+ ~user-email-address~ is not part of that (and refer you to ~mu init~). You can
+ avoid the addresses in the main screen and the warning by setting
+ ~mu4e-main-view-hide-addresses~ to non-nil.
+
+ - In many cases, ~mu4e~ used to receive /all/ contacts after each indexing
+ operation; this was slow for some users, so we have updated this to /only/
+ get the contacts that have changed since the last round.
+
+ We also moved sorting the contacts to the mu-side, which speeds things up
+ further. However, as a side-effect of this, ~mu4e-contact-rewrite-function~
+ and ~mu4e-compose-complete-ignore-address-regexp~ have been obsoleted; users
+ of those should migrate to ~mu4e-contact-process-function~; see its
+ docstring for details.
+
+ - Christophe Troestler contributed support for Gnus' calender-invitation
+ handling in mu4e (i.e., you should be able to accept/reject invitations
+ etc.). It's very fresh code, and likely it'll be tweaked in the future.
+ But it's available now for testing. Note that this requires the gnus-based
+ viewer, as per ~(setq mu4e-view-use-gnus t)~
+
+ - In addition, he added support for custom headers, so the ones for for the
+ non-gnus-view should work just as well.
+
+ - ~org-mode~ support is enabled by default now. ~speedbar~ support is disabled
+ by default. The support org functionality has been moved to ~mu4e-org.el~,
+ with ~org-mu4e.el~ remaining for older things.
+
+ - ~mu4e~ now adds message-ids to messages when saving drafts, so we can find
+ them even with ~mu4e-headers-skip-duplicates~.
+
+ - Bookmarks (as in ~mu4e-bookmarks~) are now simple plists (instead of cl
+ structs). ~make-mu4e-bookmark~ has been updated to produce such plists (for
+ backward compatibility). A bookmark now looks like a list of e.g. ~(:name
+ "My bookmark" :query "banana OR pear" :key ?f)~ this format is a bit easier
+ extensible.
+
+ - ~mu4e~ recognizes an attribute ~:hide t~, which will hide the bookmark item
+ from the main-screen (and speedbar), but keep it available through the
+ completion UI.
+
+ - ~mu4e-maildir-shortcuts~ have also become plists. The older format is still
+ recognized for backward compatibility, but you are encouraged to upgrade.
+
+ - Replying to mailing-lists has been improved, allowing for choosing for
+ replying to all, sender, list-only.
+
+ - A very visible change, ~mu4e~ now shows unread/all counts for bookmarks in
+ the main screen that are strings. This is on by default, but can be
+ disabled by setting ~:hide-unread~ in the bookmark ~plist~ to ~t~. For
+ speed-reasons, these counts do _not_ filter out duplicates nor messages that
+ have been removed from the filesystem.
+
+ - ~mu4e-attachment-dir~ now also applies to composing messages; it determines
+ the default directory for inclusion.
+
+ - The mu4e <-> mu interaction has been rewritten to communicate using
+ s-expressions, with a repl for testing.
+
+*** guile
+
+ - guile 3.0 is now supported; guile 2.2 still works.
+
+*** toys
+
+ - Updated the ~mug~ toy UI to use Webkit2/GTK+. Note that this is just a toy
+ which is not meant for distribution. ~msg2pdf~ is disabled for now.
+
+
+*** How to upgrade mu4e
+
+ - upgrade ~mu~ to the latest stable version (1.4.x)
+
+ - shut down emacs
+
+ - Run ~mu init~ in a terminal
+
+ - Make sure ~mu init~ points to the right Maildir folder and add your email
+ address(es) the following way:
+
+ ~mu init --maildir=~/Maildir --my-address=jim@example.com --my-address=bob@example.com~
+
+ - once this is done, run ~mu index~
+
+ - Don't forget to delete your old mail cache location if necessary (see
+ release notes for more detail).
+
+** 1.2
+
+ After a bit over a year since version 1.0, here is version 1.2. This is
+ mostly a bugfix release, but there are also a number of new features.
+
+*** mu
+
+ - Substantial (algorithmic) speed-up of message-threading; this also (or
+ especially) affects mu4e, since threading is the default. See commit
+ eb9bfbb1ca3c for all the details, and thanks to Nicolas Avrutin.
+
+ - The query-parser now generates better queries for wildcard searches, by
+ using the Xapian machinery for that (when available) rather than
+ transforming into regexp queries.
+
+ - The perl backend is hardly used and will be removed; for now we just
+ disable it in the build.
+
+ - Allow outputting messages in json format, closely following the sexp
+ output. This adds an (optional) dependency on the Json-Glib library.
+
+*** mu4e
+
+ - Bump the minimal required emacs version to 24.4. This was already de-facto
+ true, now it is enforced.
+
+ - In mu4e-bookmarks, allow the `:query` element to take a function (or
+ lambda) to dynamically generate the query string.
+
+ - There is a new message-view for mu4e, based on the Gnus' article-view.
+ This bring a lot of (but not all) of the very rich Gnus article-mode
+ feature-set to mu4e, such as S/MIME-support, syntax-highlighting,
+
+ For now this is experimental ("tech preview"), but might replace the
+ current message-view in a future release. Enable it with:
+ (setq mu4e-view-use-gnus t)
+
+ Thanks to Christophe Troestler for his work on fixing various encoding
+ issues.
+
+ - Many bug fixes
+
+*** guile
+
+ - Now requires guile 2.2.
+
+*** Contributors for this release:
+
+ Ævar Arnfjörð Bjarmason, Albert Krewinkel, Alberto Luaces, Alex Bennée, Alex
+ Branham, Alex Murray, Cheong Yiu Fung, Chris Nixon, Christian Egli,
+ Christophe Troestler, Dirk-Jan C. Binnema, Eric Danan, Evan Klitzke, Ian
+ Kelling, ibizaman, James P. Ascher, John Whitbeck, Junyeong Jeong, Kevin
+ Foley, Marcelo Henrique Cerri, Nicolas Avrutin, Oleh Krehel, Peter W. V.
+ Tran-Jørgensen, Piotr Oleskiewicz, Sebastian Miele, Ulrich Ölmann,
+
+** 1.0
+
+ After a decade of development, *mu 1.0*!
+
+ Note: the new release requires a C++14 capable compiler.
+
+*** mu
+
+ - New, custom query parser which replaces Xapian's 'QueryParser'
+ both in mu and mu4e. Existing queries should still work, but the new
+ engine handles non-alphanumeric queries much better.
+ - Support regular expressions in queries (with the new query engine),
+ e.g. "subject:/foo.*bar/". See the new `mu-query` and updated `mu-easy`
+ manpages for examples.
+ - cfind: ensure nicks are unique
+ - auxiliary programs invoked from mu/mu4e survive terminating the
+ shell / emacs
+
+*** mu4e
+
+ - Allow for rewriting message bodies
+ - Toggle-menus for header settings
+ - electric-quote-(local-)mode work when composing emails
+ - Respect format=flowed and delsp=yes for viewing plain-text
+ messages
+ - Added new mu4e-split-view mode: single-window
+ - Add menu item for `untrash'.
+ - Unbreak abbrevs in mu4e-compose-mode
+ - Allow forwarding messages as attachments
+ (`mu4e-compose-forward-as-attachment')
+ - New defaults: default to 'skip duplicates' and 'include related'
+ in headers-view, which should be good defaults for most users. Can be
+ customized using `mu4e-headers-skip-duplicates' and
+ `mu4e-headers-include-related', respectively.
+ - Many bug fixed (see github for all the details).
+ - Updated documentation
+
+*** Contributors for this release:
+
+ Ævar Arnfjörð Bjarmason, Alex Bennée, Arne Köhn, Christophe Troestler,
+ Damien Garaud, Dirk-Jan C. Binnema, galaunay, Hong Xu, Ian Kelling, John
+ Whitbeck, Josiah Schwab, Jun Hao, Krzysztof Jurewicz, maxime, Mekeor Melire,
+ Nathaniel Nicandro, Ronald Evers, Sean 'Shaleh' Perry, Sébastien Le
+ Callonnec, Stig Brautaset, Thierry Volpiatto, Titus von der Malsburg,
+ Vladimir Sedach, Wataru Ashihara, Yuri D'Elia.
+
+ And all the people on the mailing-list and in github, with bug reports,
+ questions and suggestions.
+
+
+** 0.9.18
+
+ New development series which will lead to 0.9.18.
+
+*** mu
+
+ - Increase the default maximum size for messages to index to 500
+ Mb; you can customize this using the --max-msg-size parameter to mu index.
+ - implement "lazy-checking", which makes mu not descend into
+ subdirectories when the directory-timestamp is up to date; greatly speeds
+ up indexing (see --lazy-check)
+ - prefer gpg2 for crypto
+ - fix a crash when running on OpenBSD
+ - fix --clear-links (broken filenames)
+ - You can now set the MU_HOME environment variable as an
+ alternative way of setting the mu homedir via the --muhome command-line
+ parameter.
+
+*** mu4e
+
+**** reading messages
+
+ - Add `mu4e-action-view-with-xwidget`, and action for viewing
+ e-mails inside a Webkit-widget inside emacs (requires emacs 25.x with
+ xwidget/webkit/gtk3 support)
+ - Explicitly specify utf8 for external html viewing, so browsers
+ can handle it correctly.
+ - Make `shr' the default renderer for rich-text emails (when
+ available)
+ - Add a :user-agent field to the message-sexp (in mu4e-view), which
+ is either the User-Agent or X-Mailer field, when present.
+
+**** composing messages
+
+ - Cleanly handle early exits from message composition as well as while
+ composing.
+ - Allow for resending existing messages, possibly editing them. M-x
+ mu4e-compose-resend, or use the menu; no shortcut.
+ - Better handle the closing of separate compose frames
+ - Improved font-locking for the compose buffers, and more extensive
+ checks for cited parts.
+ - automatically sign/encrypt replies to signed/encrypted messages
+ (subject to `mu4e-compose-crypto-reply-policy')
+
+**** searching & marking
+
+ - Add a hook `mu4e-mark-execute-pre-hook`, which is run just before
+ executing marks.
+ - Just before executing any search, a hook-function
+ `mu4e-headers-search-hook` is invoked, which receives the search
+ expression as its parameter.
+ - In addition, there's a `mu4e-headers-search-bookmark-hook` which
+ gets called when searches get invoked as a bookmark (note that
+ `mu4e-headers-search-hook` will also be called just afterwards). This
+ hook also receives the search expression as its parameter.
+ - Remove the 'z' keybinding for leaving the headers
+ view. Keybindings are precious!
+ - Fix parentheses/precedence in narrowing search terms
+
+**** indexing
+
+ - Allow for indexing in the background; see
+ `mu4e-index-update-in-background`.
+ - Better handle mbsync output in the update buffer
+ - Add variables mu4e-index-cleanup and mu4e-index-lazy to enable
+ lazy checking from mu4e; you can sit from mu4e using something like:
+#+begin_src elisp
+(setq mu4e-index-cleanup nil ;; don't do a full cleanup check
+ mu4e-index-lazy-check t) ;; don't consider up-to-date dirs #+END_SRC
+#+end_src
+**** misc
+
+ - don't overwrite global-mode-string, append to it.
+ - Make org-links (and more general, all users of
+ mu4e-view-message-with-message-id) use a headers buffer, then view the
+ message. This way, those linked message are just like any other, and can
+ be deleted, moved etc.
+ - Support org-mode 9.x
+ - Improve file-name escaping, and make it support non-ascii filenames
+ - Attempt to jump to the same messages after a re-search update operation
+ - Add action for spam-filter options
+ - Let `mu4e~read-char-choice' become case-insensitive if there is
+ no exact match; small convenience that affects most the single-char
+ option-reading in mu4e.
+
+*** Perl
+
+ - an experimental Perl binding ("mup") is available now. See
+ perl/README.md for details.
+
+*** Contributors:
+
+ Aaron LI, Abdo Roig-Maranges, Ævar Arnfjörð Bjarmason, Alex Bennée, Allen,
+ Anders Johansson, Antoine Levitt, Arthur Lee, attila, Charles-H. Schulz,
+ Christophe Troestler, Chunyang Xu, Dirk-Jan C. Binnema, Jakub Sitnicki,
+ Josiah Schwab, jsrjenkins, Jun Hao, Klaus Holst, Lukas Fürmetz, Magnus
+ Therning, Maximilian Matthe, Nicolas Richard, Piotr Trojanek, Prashant
+ Sachdeva, Remco van 't Veer, Stephen Eglen, Stig Brautaset, Thierry
+ Volpiatto, Thomas Moulia, Titus von der Malsburg, Yuri D'Elia, Vladimir
+ Sedach
+
+** 0.9.16
+
+*** Release
+
+ 2016-01-20: Release from the 0.9.15 series
+
+*** Contributors:
+
+ Adam Sampson, Ævar Arnfjörð Bjarmason, Bar Shirtcliff, Charles-H. Schulz,
+ Clément Pit--Claudel, Damien Cassou, Declan Qian, Dima Kogan, Dirk-Jan C.
+ Binnema, Foivos S. Zakkak, Hinrik Örn Sigurðsson, Jeroen Tiebout, JJ Asghar,
+ Jonas Bernoulli, Jun Hao, Martin Yrjölä, Maximilian Matthé, Piotr Trojanek,
+ prsarv, Thierry Volpiatto, Titus von der Malsburg
+
+ (and of course all people who reported issues, provided suggestions etc.)
+
+** 0.9.15
+
+ - bump version to 0.9.15. From now on, odd minor version numbers
+ are for development versions; thus, 0.9.16 is to be the next stable
+ release.
+ - special case text/calendar attachments to get .vcs
+ extensions. This makes it easier to process those with external tools.
+ - change the message file names to better conform to the maildir
+ spec; this was confusing some tools.
+ - fix navigation when not running in split-view mode
+ - add `mu4e-view-body-face', so the body-face for message in the
+ view can be customized; e.g. (set-face-attribute 'mu4e-view-body-face nil
+ :font "Liberation Serif-10")
+ - add `mu4e-action-show-thread`, an action for the headers and view
+ buffers to search for messages in the same thread as the current one.
+ - allow for transforming mailing-list names for display, using
+ `mu4e-mailing-list-patterns'.
+ - some optimizations in indexing (~30% faster in some cases)
+ - new variable mu4e-user-agent-string, to customize the User-Agent:
+ header.
+ - when removing the "In-reply-to" header from replies, mu4e will
+ also remove the (hidden) References header, effectively creating a new
+ message-thread.
+ - implement 'mu4e-context', for defining and switching between
+ various contexts, which are groups of settings. This can be used for
+ instance for switch between e-mail accounts. See the section in the manual
+ for details.
+ - correctly decode mailing-list headers
+ - allow for "fancy" mark-characters; and improve the default set
+ - by default, the maildirs are no longer cached; please see the
+ variable ~mu4e-cache-maildir-list~ if you have a lot of maildirs and it
+ gets slow.
+ - change the default value for
+ ~org-mu4e-link-query-in-headers-mode~ to ~nil~, ie. by default link to the
+ message, not the query, as this is usually more useful behavior.
+ - overwrite target message files that already exist, rather than
+ erroring out.
+ - set mu4e-view-html-plaintext-ratio-heuristic to 5, as 10 was too
+ high to detect some effectively html-only messages
+ - add mu4e-view-toggle-html (keybinding: 'h') to toggle between
+ text and html display. The existing 'mu4e-view-toggle-hide-cited' gets the
+ new binding '#'.
+ - add a customization variable `mu4e-view-auto-mark-as-read'
+ (defaults to t); if set to nil, mu4e won't mark messages as read when you
+ open them. This can be useful on read-only file-systems, since
+ marking-as-read implies a file-move operation.
+ - use smaller chunks for mu server on Cygwin, allowing for better
+ mu4e support there.
+
+** 0.9.13
+
+*** contributors
+
+ Attila, Daniele Pizzolli, Charles-H.Schulz, David C Sterrat, Dirk-Jan C.
+ Binnema, Eike Kettner, Florian Lindner, Foivos S. Zakkak, Gour, KOMURA
+ Takaaki, Pan Jie, Phil Hagelberg, thdox, Tiago Saboga, Titus von der
+ Malsburg
+
+ (and of course all people who reported issues, provided suggestions etc.)
+
+*** mu/mu4e/guile
+
+ - NEWS (this file) is now visible from within mu4e – "N" in the main-menu.
+
+ - make `mu4e-headers-sort-field', `mu4e-headers-sort-direction'
+ public (that, is change the prefix from mu4e~ to mu4e-), so users can
+ manipulate them
+
+ - make it possible the 'fancy' (unicode) characters separately for
+ headers and marks (see the variable `mu4e-use-fancy-chars'.)
+
+ - allow for composing in a separate frame (see
+ `mu4e-compose-in-new-frame')
+
+ - add the `:thread-subject' header field, for showing the subject
+ for a thread only once. So, instead of (from the manual):
+
+#+begin_example
+06:32 Nu To Edmund Dantès GstDev + Re: Gstreamer-V4L...
+15:08 Nu Abbé Busoni GstDev + Re: Gstreamer-V...
+18:20 Nu Pierre Morrel GstDev \ Re: Gstreamer...
+2013-03-18 S Jacopo EmacsUsr + emacs server on win...
+2013-03-18 S Mercédès EmacsUsr \ RE: emacs server ...
+2013-03-18 S Beachamp EmacsUsr + Re: Copying a whole...
+22:07 Nu Albert de Moncerf EmacsUsr \ Re: Copying a who...
+2013-03-18 S Gaspard Caderousse GstDev | Issue with GESSimpl...
+2013-03-18 Ss Baron Danglars GuileUsr | Guile-SDL 0.4.2 ava...
+End of search results
+#+end_example
+
+the headers list would now look something like:
+#+begin_example
+06:32 Nu To Edmund Dantès GstDev + Re: Gstreamer-V4L...
+15:08 Nu Abbé Busoni GstDev +
+18:20 Nu Pierre Morrel GstDev \ Re: Gstreamer...
+2013-03-18 S Jacopo EmacsUsr + emacs server on win...
+2013-03-18 S Mercédès EmacsUsr \
+2013-03-18 S Beachamp EmacsUsr + Re: Copying a whole...
+22:07 Nu Albert de Moncerf EmacsUsr \
+2013-03-18 S Gaspard Caderousse GstDev | Issue with GESSimpl...
+2013-03-18 Ss Baron Danglars GuileUsr | Guile-SDL 0.4.2 ava...
+End of search results
+#+end_example
+
+ This is a feature known from e.g. `mutt' and `gnus` and many other
+ clients, and can be enabled by customizing `mu4e-headers-fields'
+ (replacing `:subject' with `:thread-subject')
+
+ It's not the default yet, but may become so in the future.
+
+ - add some spam-handling actions to mu4e-contrib.el
+
+ - mu4e now targets org 8.x, which support for previous versions
+ relegated to `org-old-mu4e.el`. Some of the new org-features are improved
+ capture templates.
+
+ - updates to the documentation, in particular about using BBDB.
+
+ - improved URL-handling (use emacs built-in functionality)
+
+ - many bug fixes, including some crash fixes on BSD
+
+*** guile
+
+ – add --delete option to the find-dups scripts, to automatically delete
+ them. Use with care!
+
+** Release 0.9.12
+
+*** mu
+
+ - truncate /all/ terms the go beyond xapian's max term length
+ - lowercase the domain-part of email addresses in mu cfind (and mu4e), if
+ the domain is in ascii
+ - give messages without msgids fake-message-ids; this fixes the problem
+ where such messages were not found in --include-related queries
+ - cleanup of the query parser
+ - provide fake message-ids for messages without it; fixes #183
+ - allow showing tags in 'mu find' output
+ - fix CSV quoting
+
+*** mu4e
+
+ - update the emacs <-> backend protocol; documented in the mu-server man page
+ - show 'None' as date for messages without it (Headers View)
+ - add `mu4e-headers-found-hook', `mu4e-update-pre-hook'.
+ - split org support in org-old-mu4e.el (org <= 7.x) and org-mu4e.el
+ - org: improve template keywords
+ - rework URL handling
+
+** Release 0.9.10
+
+*** mu
+
+ - allow 'contact:' as a shortcut in queries for 'from:foo OR to:foo OR
+ cc:foo OR bcc:foo', and 'recip:' as a shortcut for 'to:foo OR cc:foo OR
+ bcc:foo'
+ - support getting related messages (--include-related), which includes
+ messages that may not match the query, but that are in the same threads as
+ messages that were
+ - support "list:"/"v:" for matching mailing list names, and the "v"
+ format-field to show them. E.g 'mu find list:emacs-orgmode.gnu.org'
+
+*** mu4e
+
+ - scroll down in message view takes you to next message (but see
+ `mu4e-view-scroll-to-next')
+ - support 'human dates', that is, show the time for today's messages, and
+ the date for older messages in the headers view
+ - replace `mu4e-user-mail-address-regexp' and `mu4e-my-mail-addresses' with
+ `mu4e-user-mail-address-list'
+ - support tags (i.e.., X-Keywords and friends) in the headers-view, and the
+ message view. Thanks to Abdó Roig-Maranges. New field ":tags".
+ - automatically update the headers buffer when new messages are found during
+ indexing; set `mu4e-headers-auto-update' to nil to disable this.
+ - update mail/index with M-x mu4e-update-mail-and-index; which everywhere in
+ mu4e is available with key C-S-u. Use prefix argument to run in
+ background.
+ - add function `mu4e-update-index' to only update the index
+ - add 'friendly-names' for mailing lists, so they should up nicely in the
+ headers view
+
+*** guile
+
+ - add 'mu script' command to run mu script, for example to do statistics on
+ your message corpus. See the mu-script man-page.
+
+*** mug
+
+ - ported to gtk+ 3; remove gtk+ 2.x code
+
+
+
+** Release 0.9.9 <2012-10-14>
+
+*** mu4e
+ - view: address can be toggled long/short, compose message
+ - sanitize opening urls (mouse-1, and not too eager)
+ - tooltips for header labels, flags
+ - add sort buttons to header-labels
+ - support signing / decryption of messages
+ - improve address-autocompletion (e.g., ensure it's case-insensitive)
+ - much faster when there are many maildirs
+ - improved line wrapping
+ - better handle attached messages
+ - improved URL-matching
+ - improved messages to user (mu4e-(warn|error|message))
+ - add refiling functionality
+ - support fancy non-ascii in the UI
+ - dynamic folders (i.e.., allow mu4e-(sent|draft|trash|refile)-folder) to
+ be a function
+ - dynamic attachment download folder (can be a function now)
+ - much improved manual
+
+*** mu
+ - remove --summary (use --summary-len instead)
+ - add --after for mu find, to limit to messages after T
+ - add new command `mu verify', to verify signatures
+ - fix iso-2022-jp decoding (and other 7-bit clean non-ascii)
+ - add support for X-keywords
+ - performance improvements for threaded display (~ 25% for 23K msgs)
+ - mu improved user-help (and the 'mu help' command)
+ - toys/mug2 replaces toys/mug
+
+*** mu-guile
+ - automated tests
+ - add mu:timestamp, mu:count
+ - handle db reopenings in the background
+
+
+** Release 0.9.8.5 <2012-07-01>
+
+*** mu4e
+
+ - auto-completion of e-mail addresses
+ - inline display of images (see `mu4e-view-show-images'), uses imagemagick
+ if available
+ - interactively change number of headers / columns for showing headers with
+ C-+ and C-- in headers, view mode
+ - support flagging message
+ - navigate to previous/next queries like a web browser (with <M-left>,
+ <M-right>)
+ - narrow search results with '/'
+ - next/previous take a prefix arg now, to move to the nth previous/next message
+ - allow for writing rich-text messages with org-mode
+ - enable marking messages as Flagged
+ - custom marker functions (see manual)
+ - better "dwim" handling of buffer switching / killing
+ - deferred marking of message (i.e.., mark now, decide what to mark for
+ later)
+ - enable changing of sort order, display of threads
+ - clearer marks for marked messages
+ - fix sorting by subject (disregarding Re:, Fwd: etc.)
+ - much faster handling when there are many maildirs (speedbar)
+ - handle mailto: links
+ - improved, extended documentation
+
+*** mu
+
+ - support .noupdate files (parallel to .noindex, dir is ignored unless we're
+ doing a --rebuild).
+ - append all inline text parts, when getting the text body
+ - respect custom maildir flags
+ - correctly handle the case where g_utf8_strdown (str) > len (str)
+ - make gtk, guile, webkit dependency optional, even if they are installed
+
+
+** Release 0.9.8.4 <2012-05-08>
+
+*** mu4e
+
+ - much faster header buffers
+ - split view mode (headers, view); see `mu4e-split-view'.
+ - add search history for queries
+ - ability to open attachments with arbitrary programs, pipe through shell
+ commands or open in the current emacs
+ - quote names in recipient addresses
+ - mu4e-get-maildirs works now for recursive maildirs as well
+ - define arbitrary operations for headers/messages/attachments using the
+ actions system -- see the chapter 'Actions' in the manual
+ - allow mu4e to be uses as the default emacs mailer (`mu4e-user-agent')
+ - mark headers based on a regexp, `mu4e-mark-matches', or '%'
+ - mark threads, sub-threads (mu4e-hdrs-mark-thread,
+ mu4e-hdrs-mark-subthread, or 'T', 't')
+ - add msg2pdf toy
+ - easy logging (using `mu4e-toggle-logging')
+ - improve mu4e-speedbar for use in headers/view
+ - use the message-mode FCC system for saving messages to the sent-messages
+ folder
+ - fix: off-by-one in number of matches shown
+
+*** general
+
+ - fix for opening files with non-ascii names
+ - much improved support for searching non-Latin (Cyrillic etc.) languages
+ we can now match 'Тесла' or 'Аркона' without problems
+ - smarter escaping (fixes issues with finding message ids)
+ - fixes for queries with brackets
+ - allow --summary-len for the length of message summaries
+ - numerous other small fixes
+
+
+** Release 0.9.8.3 <2012-04-06>
+
+ *NOTE*: existing mu/mu4e are recommended to run `mu index --rebuild' after
+ installation.
+
+*** mu4e
+
+ - allow for searching by editing bookmarks
+ (`mu4e-search-bookmark-edit-first') (keybinding 'B')
+ - make it configurable what to do with sent messages (see
+ `mu4e-sent-messages-behavior')
+ - speedbar support (initial patch by Antono V)
+ - better handling of drafts:
+ - don't save too early
+ - more descriptive buffer names (based on Subject, if any)
+ - don't put "--text-follows-this-line--" markers in files
+ - automatically include signatures, if set
+ - add user-settable variables mu4e-view-wrap-lines and mu4e-view-hide-cited,
+ which determine the initial way a message is displayed
+ - improved documentation
+
+*** general
+
+ - much improved searching for GMail folders (i.e. maildir:/ matching);
+ this requires a 'mu index --rebuild'
+ - correctly handle utf-8 messages, even if they don't specify this explicitly
+ - fix compiler warnings for newer/older gcc and clang/clang++
+ - fix unit tests (and some code) for Ubuntu 10.04 and FreeBSD9
+ - fix warnings for compilation with GTK+ 3.2 and recent glib (g_set_error)
+ - fix mu_msg_move_to_maildir for top-level messages
+ - fix in maildir scanning
+ - plug some memleaks
+
+** Release 0.9.8.2 <2012-03-11>
+
+*** mu4e:
+
+ - make mail updating non-blocking
+ - allow for automatic periodic update ('mu4e-update-interval')
+ - allow for external triggering of update
+ - make behavior when leaving the headers buffer customizable, ie.
+ ask/apply/ignore ('mu4e-headers-leave-behaviour')
+
+*** general
+
+ - fix output for some non-UTF8 locales
+ - open ('play') file names with spaces
+ - don't show unnecessary errors for --format=links
+ - make build warning-free for clang/clang++
+ - allow for slightly older autotools
+ - fix unit tests for some hidden assumptions (locale, dir structure etc.)
+ - some documentation updates / clarifications
+
+** Release 0.9.8.1 <2012-02-18 Sat>
+
+*** mu
+ - show only leaf/rfc822 MIME-parts
+
+*** mu4e
+
+ - allow for shell commands with arguments in `mu4e-get-mail-command'.
+ - support marking messages as 'read' and 'unread'
+ - show the current query in the the mode-line (`global-mode-string').
+ - don't repeat 'Re:' / 'Fwd:'
+ - colorize cited message parts
+ - better handling of text-based, embedded message attachments
+ - for text-bodies, concatenate all text/plain parts
+ - make filladapt dep optional
+ - documentation improvements
+
+** Release 0.9.8 <2012-01-31>
+
+ - '--descending' has been renamed into '--reverse'
+ - search for attachment MIME-type using 'mime:' or 'y:'
+ - search for text in text-attachments using 'embed:' or 'e:'
+ - searching for attachment file names now uses 'file:' (was: 'attach:')
+ - experimental emacs-based mail client -- "mu4e"
+ - added more unit tests
+ - improved guile binding - no special binary is needed anymore, it's
+ installable are works with the normal guile system; code has been
+ substantially improved. still 'experimental'
+
+** Release 0.9.7 <2011-09-03 Sat>
+
+ - don't enforce UTF-8 output, use locale (fixes issue #11)
+ - add mail threading to mu-find (using -t/--threads) (sorta fixes issue #13)
+ - add header line to --format=mutt-ab (mu cfind), (fixes issue #42)
+ - terminate mu view results with a form-feed marker (use --terminate) (fixes
+ issue #41)
+ - search X-Label: tags (fixes issue #40)
+ - added toys/muile, the mu guile shells, which allows for message stats etc.
+ - fix date handling (timezones)
+
+** Release 0.9.6 <2011-05-28 Sat>
+
+ - FreeBSD build fix
+ - fix matching for mu cfind to be as expected
+ - fix mu-contacts for broken names/emails
+ - clear the contacts-cache too when doing a --rebuild
+ - wildcard searches ('*') for fields (except for path/maildir)
+ - search for attachment file names (with 'a:'/'attach:') -- also works with
+ wildcards
+ - remove --xquery completely; use --output=xquery instead
+ - fix progress info in 'mu index'
+ - display the references for a message using the 'r' character (xmu find)
+ - remove --summary-len/-k, instead use --summary for mu view and mu find, and
+ - support colorized output for some sub-commands (view, cfind and
+ extract). Disabled by default, use --color to enable, or set env MU_COLORS
+ to non-empty
+ - update documentation, added more examples
+
+** Release 0.9.5 <2011-04-25 Mon>
+
+ - bug fix for infinite loop in Maildir detection
+ - minor fixes in tests, small optimizations
+
+** Release 0.9.4 <2011-04-12 Tue>
+
+ - add the 'cfind' command, to search/export contact information
+ - add 'flag:unread' as a synonym for 'flag:new OR NOT flag:unseen'
+ - updated documentation
+
+** Release 0.9.3 <2011-02-13 Sun>
+
+ - don't warn about missing files with --quiet
+
+** Release 0.9.2 <2011-02-02 Wed>
+
+ - stricter checking of options; and options must now *follow* the sub-command
+ (if any); so, something like: 'mu index --maildir=/foo/bar'
+ - output searches as plain text (default), XML, JSON or s-expressions using
+ --format=plain|xml|json|sexp. For example: 'mu find foobar --output=json'.
+ These format options are experimental (except for 'plain')
+ - the --xquery option should now be used as --format=xquery, for output
+ symlinks, use --format=links. This is a change in the options.
+ - search output can include the message size using the 'z' shortcut
+ - match message size ranges (i.e.. size:500k..2M)
+ - fix: honor the --overwrite (or lack thereof) parameter
+ - support folder names with special characters (@, ' ', '.' and so on)
+ - better check for already-running mu index
+ - when --maildir= is not provided for mu index, default to the last one
+ - add --max-msg-size, to specify a new maximum message size
+ - move the 'mug' UI to toys/mug; no longer installable
+ - better support for Solaris builds, Gentoo.
+
+** Release 0.9.1 <2010-12-05 Sun>
+
+ - Add missing icon for mug
+ - Fix unit tests (Issue #30)
+ - Fix Fedora 14 build (broken GTK+ 3) (Issue #31)
+
+** Release 0.9 <2010-12-04 Sat>
+
+ - you can now search for the message priority ('prio:high', 'prio:low',
+ 'prio:normal')
+ - you can now search for message flags, e.g. 'flag:attach' for messages with
+ attachment, or 'flag:encrypted' for encrypted messages
+ - you can search for time-intervals, e.g. 'date:2010-11-26..2010-11-29' for
+ messages in that range. See the mu-find(1) and mu-easy(1) man-pages for
+ details and examples.
+ - you can store bookmarked queries in ~/.mu/bookmarks
+ - the 'flags' parameter has been renamed in 'flag'
+ - add a simple graphical UI for searching, called 'mug'
+ - fix --clearlinks for file systems without entry->d_type (fixes issue #28)
+ - make matching case-insensitive and accent-insensitive (accent-insensitive
+ for characters in Unicode Blocks 'Latin-1 Supplement' and 'Latin
+ Extended-A')
+ - more extensive pre-processing is done to make searching for email-addresses
+ and message-ids less likely to not work (issue #21)
+ - updated the man-pages
+ - experimental support for Fedora 14, which uses GMime 2.5.x (fixes issue #29)
+
+** Release 0.8 <2010-10-30 Sat>
+
+ - There's now 'mu extract' for getting information about MIME-parts
+ (attachments) and extracting them
+ - Queries are now internally converted to lowercase; this solves some of the
+ false-negative issues
+ - All mu sub-commands now have their own man-page
+ - 'mu find' now takes a --summary-len=<n> argument to print a summary of
+ up-to-n lines of the message
+ - Same for 'mu view'; the summary replaces the full body
+ - Setting the mu home dir now goes with -m, --muhome
+ - --log-stderr, --reindex, --rebuild, --autoupgrade, --nocleanup, --mode,
+ --linksdir, --clearlinks lost their single char version
+
+** Release 0.7 <2010-02-27 Sat>
+
+ - Database format changed
+ - Automatic database scheme version check, notifies users when an upgrade
+ is needed
+ - 'mu view', to view mail message files
+ - Support for >10K matches
+ - Support for unattended upgrades - that is, the database can automatically
+ by upgraded (--autoupgrade). Also, the log file is automatically cleaned
+ when it gets too big (unless you use --nocleanup)
+ - Search for a certain Maildir using the maildir:,m: search prefixes. For
+ example, you can find all messages located in ~/Maildir/foo/bar/cur/msg
+ ~/Maildir/foo/bar/new/msg and with m:/foo/bar this replace the search for
+ path/p in 0.6
+ - Fixes for reported issues ()
+ - A test suite with a growing number of unit tests
+
+
+** Release 0.6 <2010-01-23 Sat>
+
+ - First new release of mu since 2008
+ - No longer depends on sqlite
+
+
+# Local Variables:
+# mode: org; org-startup-folded: nil
+# fill-column:80
+# End:
--- /dev/null
+#+TITLE:mu
+[[https://github.com/djcb/mu/blob/master/COPYING][https://img.shields.io/github/license/djcb/mu?logo=gnu&.svg]]
+[[https://en.cppreference.com][https://img.shields.io/badge/Made%20with-C/CPP-1f425f?logo=c&.svg]]
+[[https://img.shields.io/github/v/release/djcb/mu][https://img.shields.io/github/v/release/djcb/mu.svg]]
+[[https://github.com/djcb/mu/graphs/contributors][https://img.shields.io/github/contributors/djcb/mu.svg]]
+[[https://github.com/djcb/mu/issues][https://img.shields.io/github/issues/djcb/mu.svg]]
+[[https://github.com/djcb/mu/issues?q=is%3Aissue+is%3Aopen+label%3Arfe][https://img.shields.io/github/issues/djcb/mu/rfe?color=008b8b.svg]]
+[[https://github.com/djcb/mu/pull/new][https://img.shields.io/badge/PRs-welcome-brightgreen.svg]]\\
+[[https://www.gnu.org/software/emacs/][https://img.shields.io/badge/Emacs-26.3-922793?logo=gnu-emacs&logoColor=b39ddb&.svg]]
+[[https://www.djcbsoftware.nl/code/mu/mu4e/Installation.html#Dependencies-for-Debian_002fUbuntu][https://img.shields.io/badge/Platform-Linux-2e8b57?logo=linux&.svg]]
+[[https://www.djcbsoftware.nl/code/mu/mu4e/Installation.html#Building-from-a-release-tarball-1][https://img.shields.io/badge/Platform-FreeBSD-8b3a3a?logo=freebsd&logoColor=c32136&.svg]]
+[[https://formulae.brew.sh/formula/mu#default][https://img.shields.io/badge/Platform-macOS-101010?logo=apple&logoColor=ffffff&.svg]]
+
+ [ *Note*: you are looking at the *development* branch, which is where new code is
+ being developed and tested, and which may occasionally break. Distributions and
+ non-adventurous users are instead recommended to use the [[https://github.com/djcb/mu/tree/release/1.10][1.10 Release Branch]] or
+ to pick up one of the [[https://github.com/djcb/mu/releases][1.10 Releases]]. ]
+
+Welcome to ~mu~!
+
+Latest development news: [[NEWS.org]].
+
+With the enormous amounts of e-mail many people gather and the importance of
+e-mail message in our work-flows, it's essential to quickly deal with all that
+mail - in particular, to instantly find that one important e-mail you need right
+now, and quickly file away message for later use.
+
+~mu~ is a tool for dealing with e-mail messages stored in the Maildir-format. ~mu~'s
+purpose in life is to help you to quickly find the messages you need; in
+addition, it allows you to view messages, extract attachments, create new
+maildirs, and so on.
+
+After indexing your messages into a [[http://www.xapian.org][Xapian]]-database, you can search them using a
+custom query language. You can use various message fields or words in the body
+text to find the right messages.
+
+Built on top of ~mu~ are some extensions (included in this package):
+
+- ~mu4e~: a full-featured e-mail client that runs inside emacs
+
+- ~mu-guile~: bindings for the Guile/Scheme programming language (version 3.0 and
+ later)
+
+~mu~ is written in C++; ~mu4e~ is written in ~elisp~ and ~mu-guile~ in a mix of C++ and
+Scheme.
+
+~mu~ is available in Linux distributions (e.g. Debian/Ubuntu and Fedora) under the
+name ~maildir-utils~; apparently because they don't like short names. All of the
+code is distributed under the terms of the [[https://www.gnu.org/licenses/gpl-3.0.en.html][GNU General Public License version 3]]
+(or higher).
+
+* Installation
+
+Note: building from source is an /advanced/ subject, especially if something goes
+wrong. The below simple examples are a start, but all tools involved have many
+options; there are differences between systems, versions etc. So if this is all
+a bit daunting we recommend to wait for someone else to build it for you, such
+as a Linux distribution. Many have packages available.
+
+** Requirements
+
+To be able to build ~mu~, ensure you have:
+
+- a C++17 compiler (~gcc~ or ~clang~ are known to work)
+- development packages for /Xapian/ and /GMime/ and /GLib/ (see ~meson.build~ for thex
+ versions)
+- basic tools such as ~make~, ~sed~, ~grep~
+- ~meson~
+
+For ~mu4e~, you also need ~emacs~.
+
+Note, support for Windows is very much _experimental_, that is, it works for some
+people, but we can't really support it due to lack of the specific expertise.
+Help is welcome!
+
+** Building
+
+#+begin_example
+$ git clone git://github.com/djcb/mu.git
+$ cd mu
+#+end_example
+
+~mu~ uses ~meson~ for building, so you can use that directly, and all the usual
+commands apply. You can also use it _indirectly_ through the provided ~Makefile~,
+which provides a number of useful targets.
+
+For instance, using the ~Makefile~, you could install ~mu~ using:
+
+#+begin_example
+$ ./autogen.sh && make
+$ sudo make install
+#+end_example
+
+Alternatively, you can run ~meson~ directly (see the ~meson~ documentation for
+more details):
+#+begin_example
+$ meson setup -C build
+$ meson compile -C build
+$ meson install -C build
+#+end_example
+
+** Contributing
+
+Contributions are welcome! See the Github issue list and [[IDEAS.org]].
--- /dev/null
+#!/bin/sh
+# Run this to generate all the initial makefiles, etc.
+
+echo "*** meson build setup"
+
+test -f mu/mu.cc || {
+ echo "*** Run this script from the top-level mu source directory"
+ exit 1
+}
+BUILDDIR=build
+
+command -v meson 2> /dev/null
+if [ $? != 0 ]; then
+ echo "*** 'meson' not found, please install it ***"
+ exit 1
+fi
+
+# we could remove build/ but let's avoid rm -rf risks...
+if test -d ${BUILDDIR}; then
+ meson setup --reconfigure ${BUILDDIR} $@ || exit 1
+else
+ meson setup ${BUILDDIR} $@ || exit 1
+fi
+
+echo "*** Now run either 'ninja -C ${BUILDDIR}' or 'make' to build mu"
+echo "*** Check the Makefile for other useful targets"
--- /dev/null
+#!/usr/bin/env python3
+
+"""
+Script to get date strings, since the MacOS 'date' is not quite up to GNU
+standards
+
+E.g..
+ date.py 2023-10-14 "The year-month is %y %m"
+"""
+
+import sys
+from datetime import datetime
+
+date=datetime.strptime(sys.argv[1],'%Y-%m-%d')
+print(date.strftime(sys.argv[2]))
--- /dev/null
+#!/bin/sh
+
+infodir=$1
+infofile=$2
+
+# Meson post-install script to update info metadata
+
+# If DESTDIR is set, do _not_ install-info, since it's only a temporary
+# install
+if test -z "${DESTDIR}"; then
+ install-info --info-dir "${infodir}" "${infodir}/${infofile}"
+ gzip --best --force "${infodir}/${infofile}"
+fi
--- /dev/null
+@set UPDATED @UPDATED@
+@set UPDATED-MONTH @UPDATEDMONTH@
+@set UPDATED-YEAR @UPDATEDYEAR@
+@set EDITION @VERSION@
+@set VERSION @VERSION@
--- /dev/null
+#compdef mu
+
+## Copyright (C) 2011-2012 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## 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, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+# zsh completion for mu. Install this by copying/linking to this file somewhere in
+# your $fpath; the link/copy must have a name starting with an underscore "_"
+
+# main dispatcher function
+_mu() {
+ if (( CURRENT > 2 )) ; then
+ local cmd=${words[2]}
+ curcontext="${curcontext%:*:*}:mu-$cmd"
+ (( CURRENT-- ))
+ shift words
+ _call_function ret _mu_$cmd
+ return ret
+ else
+ _mu_commands
+ fi
+}
+
+
+
+_mu_commands() {
+ local -a mu_commands
+ mu_commands=(
+ 'index:scan your maildirs and import their metadata in the database'
+ 'find:search for messages in the database'
+ 'view:display specific messages'
+ 'cfind:search for contacts (name + email) in the database'
+ 'extract:extract message-parts (attachments) and save or open them'
+ 'mkdir:create maildirs'
+# below are not generally very useful, so let's not auto-complete them
+# 'add: add a message to the database.'
+# 'remove:remove a message from the database.'
+# 'server:sart the mu server'
+)
+
+ _describe -t command 'command' mu_commands
+}
+
+_mu_common_options=(
+ '--debug[output information useful for debugging mu]'
+ '--quiet[do not give any non-critical information]'
+ '--nocolor[do not use colors in some of the output]'
+ '--version[display mu version and copyright information]'
+ '--log-stderr[log to standard error]'
+)
+
+_mu_db_options=(
+ '--muhome[use some non-default location for the mu database]:directory:_files'
+)
+
+_mu_find_options=(
+ '--fields[fields to display in the output]'
+ '--sortfield[field to sort the output by]'
+ '--descending[sort in descending order]'
+ '--summary[include a summary of the message]'
+ '--summary-len[number of lines to use for the summary]'
+ '--bookmark[use a named bookmark]'
+ '--output[set the kind of output for the query]'
+)
+
+_mu_view_options=(
+ '--summary[only show a summary of the message]'
+ '--summary-len[number of lines to use for the summary]'
+)
+
+
+_mu_view() {
+ _arguments -s : \
+ $_mu_common_options \
+ $_mu_view_options
+}
+
+_mu_extract() {
+ _files
+}
+
+_mu_find() {
+ _arguments -s : \
+ $_mu_common_options \
+ $_mu_db_options \
+ $_mu_find_options
+}
+
+_mu_index() {
+ _arguments -s : \
+ $_mu_db_options \
+ $_mu_common_options
+}mu
+
+_mu_cleanup() {
+ _arguments -s : \
+ $_mu_db_options \
+ $_mu_common_options
+}
+
+
+_mu_mkdir() {
+ _arguments -s : \
+ '--mode=[file mode for the new Maildir]:file mode: ' \
+ $_mu_common_options
+}
+
+_mu "$@"
+
+# Local variables:
+# mode: sh
+# End:
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+
+;; Copyright (C) 2012 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; 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, 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, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;;
+;; a little hack to convert the output of
+;; mu find <expr> --format=sexp
+;; and
+;; mu view <expr> --format=sexp
+;; into XML or JSON
+
+(use-modules (ice-9 getopt-long) (ice-9 format) (ice-9 regex))
+(use-modules (sxml simple))
+
+(define (mapconcat func lst sepa)
+ "Apply FUNC to elements of LST, concat the result as strings
+separated by SEPA."
+ (if (null? lst)
+ ""
+ (string-append
+ (func (car lst))
+ (if (null? (cdr lst))
+ ""
+ (string-append sepa (mapconcat func (cdr lst) sepa))))))
+
+(define (property-list? obj)
+ "Is OBJ a elisp-style property list (ie. a list of the
+form (:symbol1 something :symbol2 somethingelse), as in an elisp
+proplilst."
+ (and (list? obj)
+ (not (null? obj))
+ (symbol? (car obj))
+ (string= ":" (substring (symbol->string (car obj)) 0 1))))
+
+(define (plist->pairs plist)
+ "Convert an elisp-style property list; e.g:
+ (:prop1 foo :prop2: bar ...)
+into a list of pairs
+ ((prop1 . foo) (prop2 . bar) ...)."
+ (if (null? plist)
+ '()
+ (cons
+ (cons
+ (substring (symbol->string (car plist)) 1)
+ (cadr plist))
+ (plist->pairs (cddr plist)))))
+
+(define (string->xml str)
+ "XML-encode STR."
+ ;; sneakily re-using sxml->xml
+ (call-with-output-string (lambda (port) (sxml->xml str port))))
+
+(define (string->json str)
+ "Convert string into a JSON-encoded string."
+ (letrec ((convert
+ (lambda (lst)
+ (if (null? lst)
+ ""
+ (string-append
+ (cond
+ ((equal? (car lst) #\") "\\\"")
+ ((equal? (car lst) #\\) "\\\\")
+ ((equal? (car lst) #\/) "\\/")
+ ((equal? (car lst) #\bs) "\\b")
+ ((equal? (car lst) #\ff) "\\f")
+ ((equal? (car lst) #\lf) "\\n")
+ ((equal? (car lst) #\cr) "\\r")
+ ((equal? (car lst) #\ht) "\\t")
+ (#t (string (car lst))))
+ (convert (cdr lst)))))))
+ (convert (string->list str))))
+
+(define (etime->time_t t)
+ "Convert elisp time object T into a time_t value."
+ (logior (ash (car t) 16) (car (cdr t))))
+
+(define (sexp->xml)
+ "Convert string INPUT to XML, return the XML (string)."
+ (letrec ((convert-xml
+ (lambda* (expr #:optional parent)
+ (cond
+ ((property-list? expr)
+ (mapconcat
+ (lambda (pair)
+ (format #f "\t<~a>~a</~a>\n"
+ (car pair) (convert-xml (cdr pair) (car pair)) (car pair)))
+ (plist->pairs expr) " "))
+ ((list? expr)
+ (cond
+ ((member parent '("from" "to" "cc" "bcc"))
+ (mapconcat (lambda (addr)
+ (format #f "<address>~a~a</address>"
+ (if (string? (car addr))
+ (format #f "<name>~a</name>"
+ (string->xml (car addr))) "")
+ (if (string? (cdr addr))
+ (format #f "<email>~a</email>"
+ (string->xml (cdr addr))) "")))
+ expr " "))
+ ((string= parent "parts") "<!-- message parts -->") ;; for now, ignore
+ ;; convert the crazy emacs time thingy to time_t...
+ ((string= parent "date") (format #f "~a" (etime->time_t expr)))
+ (#t
+ (mapconcat
+ (lambda (elm) (format #f "<item>~a</item>" (convert-xml elm))) expr ""))))
+ ((string? expr) (string->xml expr))
+ ((symbol? expr) (format #f "~a" expr))
+ ((number? expr) (number->string expr))
+ (#t "."))))
+ (msg->xml
+ (lambda ()
+ (let ((expr (read)))
+ (if (not (eof-object? expr))
+ (string-append (format #f "<message>\n~a</message>\n" (convert-xml expr)) (msg->xml))
+ "")))))
+ (format #f "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<messages>\n~a</messages>" (msg->xml))))
+
+
+(define (sexp->json)
+ "Convert string INPUT to JSON, return the JSON (string)."
+ (letrec ((convert-json
+ (lambda* (expr #:optional parent)
+ (cond
+ ((property-list? expr)
+ (mapconcat
+ (lambda (pair)
+ (format #f "\n\t\"~a\": ~a"
+ (car pair) (convert-json (cdr pair) (car pair))))
+ (plist->pairs expr) ", "))
+ ((list? expr)
+ (cond
+ ((member parent '("from" "to" "cc" "bcc"))
+ (string-append "["
+ (mapconcat (lambda (addr)
+ (format #f "{~a~a}"
+ (if (string? (car addr))
+ (format #f "\"name\": \"~a\","
+ (string->json (car addr))) "")
+ (if (string? (cdr addr))
+ (format #f "\"email\": \"~a\""
+ (string->json (cdr addr))) "")))
+ expr ", ")
+ "]"))
+ ((string= parent "parts") "[]") ;; todo
+ ;; convert the crazy emacs time thingy to time_t...
+ ((string= parent "date")
+ (format #f "~a" (format #f "~a" (etime->time_t expr))))
+ (#t
+ (string-append "["
+ (mapconcat (lambda (elm) (format #f "~a" (convert-json elm))) expr ",") "]"))))
+ ((string? expr)
+ (format #f "\"~a\"" (string->json expr)))
+ ((symbol? expr)
+ (format #f "\"~a\"" expr))
+ ((number? expr) (number->string expr))
+ (#t "."))))
+ (msg->json
+ (lambda (first)
+ (let ((expr (read)))
+ (if (not (eof-object? expr))
+ (string-append (format #f "~a{~a\n}"
+ (if first "" ",\n")
+ (convert-json expr)) (msg->json #f))
+ "")))))
+ (format #f "[\n~a\n]" (msg->json #t))))
+
+(define (main args)
+ (let* ((optionspec '((format (value #t))))
+ (options (getopt-long args optionspec))
+ (msg (string-append
+ "usage: mu-sexp-convert "
+ "--format=<xml|json>\n"
+ "reads from standard-input and prints to standard output\n"))
+ (outformat (or (option-ref options 'format #f)
+ (begin (display msg) (exit 1)))))
+ (cond
+ ((string= outformat "xml")
+ (format #t "~a\n" (sexp->xml)))
+ ((string= outformat "json")
+ (format #t "~a\n" (sexp->json)))
+ (#t (begin
+ (display msg)
+ (exit 1))))))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
--- /dev/null
+
+# These refer to the release version
+# When 0.9.9.6 gets out, remove the global pre line
+%global pre pre2
+%global rel 1
+
+Summary: A lightweight email search engine for Maildirs
+Name: mu
+Version: 0.9.9.6
+URL: https://github.com/djcb/mu
+# From Packaging:NamingGuidelines for pre-relase versions:
+# Release: 0.%{X}.%{alphatag} where %{X} is the release number
+%if %{pre}
+Release: 0.%{rel}.%{prerelease}%{?dist}
+%else
+Release: %{rel}%{?dist}
+%endif
+
+License: GPLv3
+Group: Applications/Internet
+BuildRoot: %{_tmppath}/%{name}-%{version}-build
+
+# Source is at ssaavedra repo because djcb has not yet this version tag created
+Source0: http://github.com/ssaavedra/%{name}/archive/v%{version}%{?pre}.tar.gz
+BuildRequires: emacs-el
+BuildRequires: emacs
+BuildRequires: gmime-devel
+BuildRequires: guile-devel
+BuildRequires: xapian-core-devel
+BuildRequires: libuuid-devel
+BuildRequires: texinfo
+Requires: gmime
+Requires: guile
+Requires: xapian-core-libs
+Requires: emacs-filesystem >= %{_emacs_version}
+
+
+%description
+E-mail is the 'flow' in the work flow of many people. Consequently, one spends a lot of time searching for old e-mails, to dig up some important piece of information. With people having tens of thousands of e-mails (or more), this is becoming harder and harder. How to find that one e-mail in an ever-growing haystack?
+Enter mu.
+'mu' is a set of command-line tools for Linux/Unix that enable you to quickly find the e-mails you are looking for, assuming that you store your e-mails in Maildirs (if you don't know what 'Maildirs' are, you are probably not using them).
+
+%package gtk
+Group: Applications/Internet
+Summary: GUI for using mu (called mug)
+BuildRequires: gtk3-devel
+BuildRequires: webkitgtk3-devel
+Requires: gtk3
+Requires: gmime
+Requires: webkitgtk3
+Requires: mu = %{version}-%{release}
+
+%description gtk
+Mug is a simple GUI for mu from version 0.9.
+
+%package guile
+Group: Applications/Internet
+Summary: Guile scripting capabilities for mu
+Requires: guile
+Requires: mu = %{version}-%{release}
+Requires(post): info
+Requires(preun): info
+
+%description guile
+Bindings for Guile to interact with mu.
+
+
+%prep
+%setup -n %{name}-%{version}%{?pre} -q
+
+%build
+autoreconf -i
+%configure
+make %{?_smp_mflags}
+
+%install
+rm -rf %{buildroot}
+make install DESTDIR=%{buildroot}
+install -p -c -m 755 %{_builddir}/%{buildsubdir}/toys/mug/mug %{buildroot}%{_bindir}/mug
+cp -p %{_builddir}/%{buildsubdir}/mu4e/*.el %{buildroot}%{_emacs_sitelispdir}/mu4e/
+rm -f %{buildroot}%{_infodir}/dir
+
+%clean
+rm -rf %{buildroot}
+
+%post
+/sbin/install-info \
+ --info-dir=%{_infodir} %{_infodir}/mu4e.info.gz || :
+%preun
+if [ $1 = 0 -a -f %{_infodir}/mu4e.info.gz ]; then
+ /sbin/install-info --delete \
+ --info-dir=%{_infodir} %{_infodir}/mu4e.info.gz || :
+fi
+
+%post guile
+/sbin/install-info \
+ --info-dir=%{_infodir} %{_infodir}/mu-guile.info.gz || :
+
+%preun guile
+if [ $1 = 0 -a -f %{_infodir}/mu-guile.info.gz ]; then
+ /sbin/install-info --delete \
+ --info-dir=%{_infodir} %{_infodir}/mu-guile.info.gz || :
+fi
+
+
+%files
+%defattr(-,root,root)
+%{_bindir}/mu
+%{_mandir}/man1/*
+%{_mandir}/man5/*
+%{_datadir}/mu/*
+
+%{_emacs_sitelispdir}/mu4e
+%{_emacs_sitelispdir}/mu4e/*.elc
+%{_emacs_sitelispdir}/mu4e/*.el
+%{_infodir}/mu4e.info.gz
+
+%files gtk
+%{_bindir}/mug
+
+%files guile
+%{_libdir}/libguile-mu.*
+%{_datadir}/guile/site/2.0/mu/*
+%{_datadir}/guile/site/2.0/mu.scm
+%{_infodir}/mu-guile.info.gz
+
+%changelog
+* Wed Feb 12 2014 Santiago Saavedra <ssaavedra@gpul.org> - 0.9.9.5-1
+- Create first SPEC.
--- /dev/null
+#!/bin/sh
+## Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## 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, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+@abs_builddir@/build-env @guild@ compile "$@"
+
+# Local-Variables:
+# mode: sh
+# End:
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+
+;;
+;; Copyright (C) 2012 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; 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, 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, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+
+(use-modules (ice-9 getopt-long) (ice-9 format))
+(use-modules (srfi srfi-1))
+(use-modules (mu))
+
+(define (sort-by-freq c1 c2)
+ (< (mu:frequency c1) (mu:frequency c2)))
+
+(define (sort-by-newness c1 c2)
+ (< (mu:last-seen c1) (mu:last-seen c2)))
+
+(define (main args)
+ (let* ((optionspec '( (muhome (value #t))
+ (sort-by (value #t))
+ (revert (value #f))
+ (format (value #t))
+ (limit (value #t))
+ (help (single-char #\h) (value #f))))
+ (options (getopt-long args optionspec))
+ (msg (string-append
+ "usage: contacts-export [--help] [--muhome=<muhome>] "
+ "--format=<org-contact|mutt-alias|mutt-ab|wanderlust|quoted|plain(*)> "
+ "--sort-by=<frequency(*)|newness> [--revert] [--limit=<n>]\n"))
+ (help (option-ref options 'help #f))
+ (muhome (option-ref options 'muhome #f))
+ (sort-by (or (option-ref options 'sort-by #f) "frequency"))
+ (revert (option-ref options 'revert #f))
+ (form (or (option-ref options 'format #f) "plain"))
+ (limit (string->number (option-ref options 'limit "1000000"))))
+ (if help
+ (begin
+ (display msg)
+ (exit 0))
+ (begin
+ (setlocale LC_ALL "")
+ (mu:initialize muhome)
+ (let* ((sort-func
+ (cond
+ ((string= sort-by "frequency") sort-by-freq)
+ ((string= sort-by "newness") sort-by-newness)
+ (else (begin (display msg) (exit 1)))))
+ (contacts '()))
+ ;; make a list of all contacts
+ (mu:for-each-contact
+ (lambda (c) (set! contacts (cons c contacts))))
+
+ ;; should we sort it?
+ (if sort-by
+ (set! contacts (sort! contacts
+ (if revert (negate sort-func) sort-func))))
+
+ ;; should we limit the number?
+ (if (and limit (< limit (length contacts)))
+ (set! contacts (take! contacts limit)))
+ ;; export!
+ (for-each
+ (lambda (c)
+ (format #t "~a\n" (mu:contact->string c form)))
+ contacts))))))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;;
+;; Copyright (C) 2011-2012 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; 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, 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, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+(setlocale LC_ALL "")
+
+(use-modules (ice-9 getopt-long) (ice-9 optargs) (ice-9 popen) (ice-9 format))
+(use-modules (mu) (mu stats) (mu plot))
+;;(use-modules (mu) (mu message) (mu stats) (mu plot))
+
+(define (per-hour expr output)
+ "Count the total number of messages for each weekday (0-6 for Sun..Sat) that
+match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set
+terminal'."
+ (mu:plot
+ (sort
+ (mu:tabulate
+ (lambda (msg)
+ (tm:hour (localtime (mu:date msg)))) expr)
+ (lambda (x y) (< (car x) (car y))))
+ (format #f "Messages per hour matching ~a" expr) "Hour" "Messages" output))
+
+(define (per-day expr output)
+ "Count the total number of messages for each weekday (0-6 for Sun..Sat) that
+match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set
+terminal'."
+ (mu:plot
+ (mu:weekday-numbers->names
+ (sort (mu:tabulate
+ (lambda (msg)
+ (tm:wday (localtime (mu:date msg)))) expr)
+ (lambda (x y) (< (car x) (car y)))))
+ (format #f "Messages per weekday matching ~a" expr) "Day" "Messages" output))
+
+(define (per-month expr output)
+ "Count the total number of messages for each weekday (0-6 for Sun..Sat) that
+match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set
+terminal'."
+ (mu:plot
+ (mu:month-numbers->names
+ (sort
+ (mu:tabulate
+ (lambda (msg)
+ (tm:mon (localtime (mu:date msg)))) expr)
+ (lambda (x y) (< (car x) (car y)))))
+ (format #f "Messages per month matching ~a" expr) "Month" "Messages" output))
+
+
+(define (per-year-month expr output)
+ "Count the total number of messages for each weekday (0-6 for Sun..Sat) that
+match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set
+terminal'."
+ (mu:plot
+ (sort (mu:tabulate
+ (lambda (msg)
+ (string->number
+ (format #f "~d~2'0d"
+ (+ 1900 (tm:year (localtime (mu:date msg))))
+ (tm:mon (localtime (mu:date msg))))))
+ expr)
+ (lambda (x y) (< (car x) (car y))))
+ (format #f "Messages per year/month matching ~a" expr)
+ "Year/Month" "Messages" output))
+
+
+
+(define (per-year expr output)
+ "Count the total number of messages for each weekday (0-6 for Sun..Sat) that
+match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set
+terminal'."
+ (mu:plot
+ (sort (mu:tabulate
+ (lambda (msg)
+ (+ 1900 (tm:year (localtime (mu:date msg))))) expr)
+ (lambda (x y) (< (car x) (car y))))
+ (format #f "Messages per year matching ~a" expr) "Year" "Messages" output))
+
+
+
+(define (main args)
+ (let* ((optionspec '( (muhome (value #t))
+ (what (value #t))
+ (text (value #f))
+ (help (single-char #\h) (value #f))))
+ (options (getopt-long args optionspec))
+ (msg (string-append
+ "usage: mu-msg-stats [--help] [--text] "
+ "[--muhome=<muhome>] "
+ "--what=<per-hour|per-day|per-month|per-year-month|"
+ "per-year> [searchexpr]\n"))
+ (help (option-ref options 'help #f))
+ (what (option-ref options 'what #f))
+ (text (option-ref options 'text #f))
+ ;; if `text' is `#f', use a graphical window by setting output to "wxt",
+ ;; else use text-mode plotting ("dumb")
+ (output (if text "dumb" "wxt"))
+ (muhome (option-ref options 'muhome #f))
+ (restargs (option-ref options '() #f))
+ (expr (if restargs (string-join restargs) "")))
+ (if (or help (not what))
+ (begin
+ (display msg)
+ (exit (if help 0 1))))
+ (mu:initialize muhome)
+ (cond
+ ((string= what "per-hour") (per-hour expr output))
+ ((string= what "per-day") (per-day expr output))
+ ((string= what "per-month") (per-month expr output))
+ ((string= what "per-year-month") (per-year-month expr output))
+ ((string= what "per-year") (per-year expr output))
+ (else (begin
+ (display msg)
+ (exit 1))))))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+
+;;
+;; Copyright (C) 2012 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; 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, 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, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;; script to list the message matching <query> which are newer than
+;; <n> minutes
+
+;; use it, eg. like:
+;; $ mu-biff --newer-than=`date +%s --date='5 minutes ago'` "maildir:/inbox"
+
+
+(use-modules (ice-9 getopt-long) (ice-9 format))
+(use-modules (mu))
+
+(define (main args)
+ (let* ((optionspec '((muhome (value #t))
+ (newer-than (value #t))
+ (help (single-char #\h) (value #f))))
+ (options (getopt-long args optionspec))
+ (msg (string-append
+ "usage: mu-biff [--help] [--muhome=<muhome>]"
+ " [--newer-than=<timestamp>] <query>"))
+ (help (option-ref options 'help #f))
+ (newer-than (string->number (option-ref options 'newer-than "0")))
+ (muhome (option-ref options 'muhome #f))
+ (query (string-concatenate (option-ref options '() '()))))
+ (if help
+ (begin (display msg) (newline) (exit 0))
+ (begin
+ (mu:initialize muhome)
+ (mu:for-each-message
+ (lambda (msg)
+ (if (> (mu:timestamp msg) newer-than)
+ (format #t "~a ~a\n"
+ (mu:from msg)
+ (mu:subject msg))))
+ query)))))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+
+;;
+;; Copyright (C) 2011-2012 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; 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, 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, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+(use-modules (ice-9 getopt-long) (ice-9 format))
+(use-modules (mu))
+
+(define (display-org-header query)
+ "Print the header for the org-file for QUERY."
+ (format #t "* Messages matching '~a'\n\n" query))
+
+(define (org-mu4e-link msg)
+ "Create a link for this message understandable by org-mu4e."
+ (let* ((subject ;; cleanup subject
+ (string-map
+ (lambda (kar)
+ (if (member kar '(#\] #\[)) #\space kar))
+ (or (mu:subject msg) "No subject"))))
+ (format #f "[[mu4e:msgid:~a][~s]]"
+ (mu:message-id msg) subject)))
+
+(define (display-org-entry msg tag)
+ "Write an org entry for MSG."
+ (format #t "** ~a ~a\n\t~s\n\t~s\n"
+ (org-mu4e-link msg)
+ (if tag (string-concatenate `(":" ,tag "::")) "")
+ (or (mu:from msg) "?")
+ (let ((body (mu:body-txt msg)))
+ (if (not body) ;; get a 'summary' of the body text
+ "<no plain-text body>"
+ (string-map
+ (lambda (c)
+ (if (or (char=? c #\newline) (char=? c #\return))
+ #\space
+ c))
+ (substring body 0 (min (string-length body) 100)))))))
+
+(define (main args)
+ (let* ((optionspec '( (muhome (value #t))
+ (tag (value #t))
+ (help (single-char #\h) (value #f))))
+ (options (getopt-long args optionspec))
+ (msg (string-append
+ "usage: mu4e-org [--help] [--muhome=<muhome>] [--tag=<tag>] <query>"))
+ (help (option-ref options 'help #f))
+ (tag (option-ref options 'tag #f))
+ (muhome (option-ref options 'muhome #f))
+ (query (string-concatenate (option-ref options '() '()))))
+ (if help
+ (begin (display msg) (exit 0))
+ (begin
+ (mu:initialize muhome)
+ (display-org-header query)
+ (mu:for-each-message
+ (lambda (msg) (display-org-entry msg tag))
+ query)))))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
--- /dev/null
+@c The GNU Free Documentation License.
+@center Version 1.2, November 2002
+
+@c This file is intended to be included within another document,
+@c hence no sectioning command or @node.
+
+@display
+Copyright @copyright{} 2000,2001,2002 Free Software Foundation, Inc.
+51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+Everyone is permitted to copy and distribute verbatim copies
+of this license document, but changing it is not allowed.
+@end display
+
+@enumerate 0
+@item
+PREAMBLE
+
+The purpose of this License is to make a manual, textbook, or other
+functional and useful document @dfn{free} in the sense of freedom: to
+assure everyone the effective freedom to copy and redistribute it,
+with or without modifying it, either commercially or noncommercially.
+Secondarily, this License preserves for the author and publisher a way
+to get credit for their work, while not being considered responsible
+for modifications made by others.
+
+This License is a kind of ``copyleft'', which means that derivative
+works of the document must themselves be free in the same sense. It
+complements the GNU General Public License, which is a copyleft
+license designed for free software.
+
+We have designed this License in order to use it for manuals for free
+software, because free software needs free documentation: a free
+program should come with manuals providing the same freedoms that the
+software does. But this License is not limited to software manuals;
+it can be used for any textual work, regardless of subject matter or
+whether it is published as a printed book. We recommend this License
+principally for works whose purpose is instruction or reference.
+
+@item
+APPLICABILITY AND DEFINITIONS
+
+This License applies to any manual or other work, in any medium, that
+contains a notice placed by the copyright holder saying it can be
+distributed under the terms of this License. Such a notice grants a
+world-wide, royalty-free license, unlimited in duration, to use that
+work under the conditions stated herein. The ``Document'', below,
+refers to any such manual or work. Any member of the public is a
+licensee, and is addressed as ``you''. You accept the license if you
+copy, modify or distribute the work in a way requiring permission
+under copyright law.
+
+A ``Modified Version'' of the Document means any work containing the
+Document or a portion of it, either copied verbatim, or with
+modifications and/or translated into another language.
+
+A ``Secondary Section'' is a named appendix or a front-matter section
+of the Document that deals exclusively with the relationship of the
+publishers or authors of the Document to the Document's overall
+subject (or to related matters) and contains nothing that could fall
+directly within that overall subject. (Thus, if the Document is in
+part a textbook of mathematics, a Secondary Section may not explain
+any mathematics.) The relationship could be a matter of historical
+connection with the subject or with related matters, or of legal,
+commercial, philosophical, ethical or political position regarding
+them.
+
+The ``Invariant Sections'' are certain Secondary Sections whose titles
+are designated, as being those of Invariant Sections, in the notice
+that says that the Document is released under this License. If a
+section does not fit the above definition of Secondary then it is not
+allowed to be designated as Invariant. The Document may contain zero
+Invariant Sections. If the Document does not identify any Invariant
+Sections then there are none.
+
+The ``Cover Texts'' are certain short passages of text that are listed,
+as Front-Cover Texts or Back-Cover Texts, in the notice that says that
+the Document is released under this License. A Front-Cover Text may
+be at most 5 words, and a Back-Cover Text may be at most 25 words.
+
+A ``Transparent'' copy of the Document means a machine-readable copy,
+represented in a format whose specification is available to the
+general public, that is suitable for revising the document
+straightforwardly with generic text editors or (for images composed of
+pixels) generic paint programs or (for drawings) some widely available
+drawing editor, and that is suitable for input to text formatters or
+for automatic translation to a variety of formats suitable for input
+to text formatters. A copy made in an otherwise Transparent file
+format whose markup, or absence of markup, has been arranged to thwart
+or discourage subsequent modification by readers is not Transparent.
+An image format is not Transparent if used for any substantial amount
+of text. A copy that is not ``Transparent'' is called ``Opaque''.
+
+Examples of suitable formats for Transparent copies include plain
+@sc{ascii} without markup, Texinfo input format, La@TeX{} input
+format, @acronym{SGML} or @acronym{XML} using a publicly available
+@acronym{DTD}, and standard-conforming simple @acronym{HTML},
+PostScript or @acronym{PDF} designed for human modification. Examples
+of transparent image formats include @acronym{PNG}, @acronym{XCF} and
+@acronym{JPG}. Opaque formats include proprietary formats that can be
+read and edited only by proprietary word processors, @acronym{SGML} or
+@acronym{XML} for which the @acronym{DTD} and/or processing tools are
+not generally available, and the machine-generated @acronym{HTML},
+PostScript or @acronym{PDF} produced by some word processors for
+output purposes only.
+
+The ``Title Page'' means, for a printed book, the title page itself,
+plus such following pages as are needed to hold, legibly, the material
+this License requires to appear in the title page. For works in
+formats which do not have any title page as such, ``Title Page'' means
+the text near the most prominent appearance of the work's title,
+preceding the beginning of the body of the text.
+
+A section ``Entitled XYZ'' means a named subunit of the Document whose
+title either is precisely XYZ or contains XYZ in parentheses following
+text that translates XYZ in another language. (Here XYZ stands for a
+specific section name mentioned below, such as ``Acknowledgements'',
+``Dedications'', ``Endorsements'', or ``History''.) To ``Preserve the Title''
+of such a section when you modify the Document means that it remains a
+section ``Entitled XYZ'' according to this definition.
+
+The Document may include Warranty Disclaimers next to the notice which
+states that this License applies to the Document. These Warranty
+Disclaimers are considered to be included by reference in this
+License, but only as regards disclaiming warranties: any other
+implication that these Warranty Disclaimers may have is void and has
+no effect on the meaning of this License.
+
+@item
+VERBATIM COPYING
+
+You may copy and distribute the Document in any medium, either
+commercially or noncommercially, provided that this License, the
+copyright notices, and the license notice saying this License applies
+to the Document are reproduced in all copies, and that you add no other
+conditions whatsoever to those of this License. You may not use
+technical measures to obstruct or control the reading or further
+copying of the copies you make or distribute. However, you may accept
+compensation in exchange for copies. If you distribute a large enough
+number of copies you must also follow the conditions in section 3.
+
+You may also lend copies, under the same conditions stated above, and
+you may publicly display copies.
+
+@item
+COPYING IN QUANTITY
+
+If you publish printed copies (or copies in media that commonly have
+printed covers) of the Document, numbering more than 100, and the
+Document's license notice requires Cover Texts, you must enclose the
+copies in covers that carry, clearly and legibly, all these Cover
+Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on
+the back cover. Both covers must also clearly and legibly identify
+you as the publisher of these copies. The front cover must present
+the full title with all words of the title equally prominent and
+visible. You may add other material on the covers in addition.
+Copying with changes limited to the covers, as long as they preserve
+the title of the Document and satisfy these conditions, can be treated
+as verbatim copying in other respects.
+
+If the required texts for either cover are too voluminous to fit
+legibly, you should put the first ones listed (as many as fit
+reasonably) on the actual cover, and continue the rest onto adjacent
+pages.
+
+If you publish or distribute Opaque copies of the Document numbering
+more than 100, you must either include a machine-readable Transparent
+copy along with each Opaque copy, or state in or with each Opaque copy
+a computer-network location from which the general network-using
+public has access to download using public-standard network protocols
+a complete Transparent copy of the Document, free of added material.
+If you use the latter option, you must take reasonably prudent steps,
+when you begin distribution of Opaque copies in quantity, to ensure
+that this Transparent copy will remain thus accessible at the stated
+location until at least one year after the last time you distribute an
+Opaque copy (directly or through your agents or retailers) of that
+edition to the public.
+
+It is requested, but not required, that you contact the authors of the
+Document well before redistributing any large number of copies, to give
+them a chance to provide you with an updated version of the Document.
+
+@item
+MODIFICATIONS
+
+You may copy and distribute a Modified Version of the Document under
+the conditions of sections 2 and 3 above, provided that you release
+the Modified Version under precisely this License, with the Modified
+Version filling the role of the Document, thus licensing distribution
+and modification of the Modified Version to whoever possesses a copy
+of it. In addition, you must do these things in the Modified Version:
+
+@enumerate A
+@item
+Use in the Title Page (and on the covers, if any) a title distinct
+from that of the Document, and from those of previous versions
+(which should, if there were any, be listed in the History section
+of the Document). You may use the same title as a previous version
+if the original publisher of that version gives permission.
+
+@item
+List on the Title Page, as authors, one or more persons or entities
+responsible for authorship of the modifications in the Modified
+Version, together with at least five of the principal authors of the
+Document (all of its principal authors, if it has fewer than five),
+unless they release you from this requirement.
+
+@item
+State on the Title page the name of the publisher of the
+Modified Version, as the publisher.
+
+@item
+Preserve all the copyright notices of the Document.
+
+@item
+Add an appropriate copyright notice for your modifications
+adjacent to the other copyright notices.
+
+@item
+Include, immediately after the copyright notices, a license notice
+giving the public permission to use the Modified Version under the
+terms of this License, in the form shown in the Addendum below.
+
+@item
+Preserve in that license notice the full lists of Invariant Sections
+and required Cover Texts given in the Document's license notice.
+
+@item
+Include an unaltered copy of this License.
+
+@item
+Preserve the section Entitled ``History'', Preserve its Title, and add
+to it an item stating at least the title, year, new authors, and
+publisher of the Modified Version as given on the Title Page. If
+there is no section Entitled ``History'' in the Document, create one
+stating the title, year, authors, and publisher of the Document as
+given on its Title Page, then add an item describing the Modified
+Version as stated in the previous sentence.
+
+@item
+Preserve the network location, if any, given in the Document for
+public access to a Transparent copy of the Document, and likewise
+the network locations given in the Document for previous versions
+it was based on. These may be placed in the ``History'' section.
+You may omit a network location for a work that was published at
+least four years before the Document itself, or if the original
+publisher of the version it refers to gives permission.
+
+@item
+For any section Entitled ``Acknowledgements'' or ``Dedications'', Preserve
+the Title of the section, and preserve in the section all the
+substance and tone of each of the contributor acknowledgements and/or
+dedications given therein.
+
+@item
+Preserve all the Invariant Sections of the Document,
+unaltered in their text and in their titles. Section numbers
+or the equivalent are not considered part of the section titles.
+
+@item
+Delete any section Entitled ``Endorsements''. Such a section
+may not be included in the Modified Version.
+
+@item
+Do not retitle any existing section to be Entitled ``Endorsements'' or
+to conflict in title with any Invariant Section.
+
+@item
+Preserve any Warranty Disclaimers.
+@end enumerate
+
+If the Modified Version includes new front-matter sections or
+appendices that qualify as Secondary Sections and contain no material
+copied from the Document, you may at your option designate some or all
+of these sections as invariant. To do this, add their titles to the
+list of Invariant Sections in the Modified Version's license notice.
+These titles must be distinct from any other section titles.
+
+You may add a section Entitled ``Endorsements'', provided it contains
+nothing but endorsements of your Modified Version by various
+parties---for example, statements of peer review or that the text has
+been approved by an organization as the authoritative definition of a
+standard.
+
+You may add a passage of up to five words as a Front-Cover Text, and a
+passage of up to 25 words as a Back-Cover Text, to the end of the list
+of Cover Texts in the Modified Version. Only one passage of
+Front-Cover Text and one of Back-Cover Text may be added by (or
+through arrangements made by) any one entity. If the Document already
+includes a cover text for the same cover, previously added by you or
+by arrangement made by the same entity you are acting on behalf of,
+you may not add another; but you may replace the old one, on explicit
+permission from the previous publisher that added the old one.
+
+The author(s) and publisher(s) of the Document do not by this License
+give permission to use their names for publicity for or to assert or
+imply endorsement of any Modified Version.
+
+@item
+COMBINING DOCUMENTS
+
+You may combine the Document with other documents released under this
+License, under the terms defined in section 4 above for modified
+versions, provided that you include in the combination all of the
+Invariant Sections of all of the original documents, unmodified, and
+list them all as Invariant Sections of your combined work in its
+license notice, and that you preserve all their Warranty Disclaimers.
+
+The combined work need only contain one copy of this License, and
+multiple identical Invariant Sections may be replaced with a single
+copy. If there are multiple Invariant Sections with the same name but
+different contents, make the title of each such section unique by
+adding at the end of it, in parentheses, the name of the original
+author or publisher of that section if known, or else a unique number.
+Make the same adjustment to the section titles in the list of
+Invariant Sections in the license notice of the combined work.
+
+In the combination, you must combine any sections Entitled ``History''
+in the various original documents, forming one section Entitled
+``History''; likewise combine any sections Entitled ``Acknowledgements'',
+and any sections Entitled ``Dedications''. You must delete all
+sections Entitled ``Endorsements.''
+
+@item
+COLLECTIONS OF DOCUMENTS
+
+You may make a collection consisting of the Document and other documents
+released under this License, and replace the individual copies of this
+License in the various documents with a single copy that is included in
+the collection, provided that you follow the rules of this License for
+verbatim copying of each of the documents in all other respects.
+
+You may extract a single document from such a collection, and distribute
+it individually under this License, provided you insert a copy of this
+License into the extracted document, and follow this License in all
+other respects regarding verbatim copying of that document.
+
+@item
+AGGREGATION WITH INDEPENDENT WORKS
+
+A compilation of the Document or its derivatives with other separate
+and independent documents or works, in or on a volume of a storage or
+distribution medium, is called an ``aggregate'' if the copyright
+resulting from the compilation is not used to limit the legal rights
+of the compilation's users beyond what the individual works permit.
+When the Document is included in an aggregate, this License does not
+apply to the other works in the aggregate which are not themselves
+derivative works of the Document.
+
+If the Cover Text requirement of section 3 is applicable to these
+copies of the Document, then if the Document is less than one half of
+the entire aggregate, the Document's Cover Texts may be placed on
+covers that bracket the Document within the aggregate, or the
+electronic equivalent of covers if the Document is in electronic form.
+Otherwise they must appear on printed covers that bracket the whole
+aggregate.
+
+@item
+TRANSLATION
+
+Translation is considered a kind of modification, so you may
+distribute translations of the Document under the terms of section 4.
+Replacing Invariant Sections with translations requires special
+permission from their copyright holders, but you may include
+translations of some or all Invariant Sections in addition to the
+original versions of these Invariant Sections. You may include a
+translation of this License, and all the license notices in the
+Document, and any Warranty Disclaimers, provided that you also include
+the original English version of this License and the original versions
+of those notices and disclaimers. In case of a disagreement between
+the translation and the original version of this License or a notice
+or disclaimer, the original version will prevail.
+
+If a section in the Document is Entitled ``Acknowledgements'',
+``Dedications'', or ``History'', the requirement (section 4) to Preserve
+its Title (section 1) will typically require changing the actual
+title.
+
+@item
+TERMINATION
+
+You may not copy, modify, sublicense, or distribute the Document except
+as expressly provided for under this License. Any other attempt to
+copy, modify, sublicense or distribute the Document is void, and will
+automatically terminate your rights under this License. However,
+parties who have received copies, or rights, from you under this
+License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+@item
+FUTURE REVISIONS OF THIS LICENSE
+
+The Free Software Foundation may publish new, revised versions
+of the GNU Free Documentation 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. See
+@uref{http://www.gnu.org/copyleft/}.
+
+Each version of the License is given a distinguishing version number.
+If the Document specifies that a particular numbered version of this
+License ``or any later version'' applies to it, you have the option of
+following the terms and conditions either of that specified version or
+of any later version that has been published (not as a draft) by the
+Free Software Foundation. If the Document does not specify a version
+number of this License, you may choose any version ever published (not
+as a draft) by the Free Software Foundation.
+@end enumerate
+
+@page
+@heading ADDENDUM: How to use this License for your documents
+
+To use this License in a document you have written, include a copy of
+the License in the document and put the following copyright and
+license notices just after the title page:
+
+@smallexample
+@group
+ Copyright (C) @var{year} @var{your name}.
+ Permission is granted to copy, distribute and/or modify this document
+ under the terms of the GNU Free Documentation License, Version 1.2
+ or any later version published by the Free Software Foundation;
+ with no Invariant Sections, no Front-Cover Texts, and no Back-Cover
+ Texts. A copy of the license is included in the section entitled ``GNU
+ Free Documentation License''.
+@end group
+@end smallexample
+
+If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts,
+replace the ``with@dots{}Texts.'' line with this:
+
+@smallexample
+@group
+ with the Invariant Sections being @var{list their titles}, with
+ the Front-Cover Texts being @var{list}, and with the Back-Cover Texts
+ being @var{list}.
+@end group
+@end smallexample
+
+If you have Invariant Sections without Cover Texts, or some other
+combination of the three, merge those two alternatives to suit the
+situation.
+
+If your document contains nontrivial examples of program code, we
+recommend releasing these examples in parallel under your choice of
+free software license, such as the GNU General Public License,
+to permit their use in free software.
+
+@c Local Variables:
+@c ispell-local-pdict: "ispell-dict"
+@c End:
+
--- /dev/null
+## Copyright (C) 2022-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## 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, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+#
+# create a shell script for compiling from the source dirs
+compile_scm_conf = configuration_data()
+compile_scm_conf.set('abs_builddir', meson.current_build_dir())
+compile_scm_conf.set('guild', 'guild')
+compile_scm=configure_file(
+ input: 'compile-scm.in',
+ output: 'compile-scm',
+ configuration: compile_scm_conf,
+ install: false
+)
+run_command('chmod', '+x', compile_scm, check: true)
+scm_compiler=join_paths(meson.current_build_dir(), 'compile-scm')
+
+#
+# NOTE: snarfing works but you get:
+# ,----
+# | cc1plus: warning: command-line option ‘-std=gnu11’ is valid for C/ObjC
+# | but not for C++
+# `----
+# this is because the snarf-script hardcodes the '-std=gnu11' but we're
+# building for c++; even worse, e.g. on some MacOS, the warning is a
+# hard error.
+#
+# We can override flag through a env variable CPP; but then we _also_ need to
+# override the compiler, so e.g. CPP="g++ -std=c++17'; but it's a bit
+# hairy/ugly/fragile to derive the raw compiler name in meson; also the
+# generator expression doesn't take an 'env:' parameter, so we'd need
+# to rewrite using custom_target...
+#
+# for now, we avoid all that by simply including the generated files.
+do_snarf=false
+
+if do_snarf
+ snarf = find_program('guile-snarf3.0','guile-snarf')
+ # there must be a better way of feeding the include paths to snarf...
+ snarf_args=['-o', '@OUTPUT@', '@INPUT@', '-I' + meson.current_source_dir() + '/..',
+ '-I' + meson.current_source_dir() + '/../lib',
+ '-I' + meson.current_build_dir() + '/..']
+ snarf_args += '-I' + join_paths(glib_dep.get_pkgconfig_variable('includedir'),
+ 'glib-2.0')
+ snarf_args += '-I' + join_paths(glib_dep.get_pkgconfig_variable('libdir'),
+ 'glib-2.0', 'include')
+ snarf_args += '-I' + join_paths(guile_dep.get_pkgconfig_variable('includedir'),
+ 'guile', '3.0')
+ snarf_gen=generator(snarf,
+ output: '@BASENAME@.x',
+ arguments: snarf_args)
+ snarf_srcs=['mu-guile.cc', 'mu-guile-message.cc']
+ snarf_x=snarf_gen.process(snarf_srcs)
+else
+ snarf_x = [ 'mu-guile-message.x', 'mu-guile.x' ]
+endif
+
+lib_guile_mu = shared_module(
+ 'guile-mu',
+ [ 'mu-guile.cc',
+ 'mu-guile-message.cc' ],
+ dependencies: [guile_dep, glib_dep, lib_mu_dep, config_h_dep, thread_dep ],
+ install: true,
+ install_dir: guile_extension_dir
+)
+
+if makeinfo.found()
+ custom_target('mu_guile_info',
+ input: 'mu-guile.texi',
+ output: 'mu-guile.info',
+ install: true,
+ install_dir: infodir,
+ command: [makeinfo,
+ '-o', join_paths(meson.current_build_dir(), 'mu-guile.info'),
+ join_paths(meson.current_source_dir(), 'mu-guile.texi'),
+ '-I', join_paths(meson.current_build_dir(), '..')])
+ if install_info.found()
+ infodir = join_paths(get_option('prefix') / get_option('infodir'))
+ meson.add_install_script(install_info_script, infodir, 'mu-guile.info')
+ endif
+endif
+
+guile_scm_dir=join_paths(datadir, 'guile', 'site', '3.0')
+install_data(['mu.scm'], install_dir: guile_scm_dir)
+guile_scm_mu_dir=join_paths(guile_scm_dir, 'mu')
+foreach mod : ['script.scm', 'message.scm', 'stats.scm', 'plot.scm']
+ install_data(join_paths('mu', mod), install_dir: guile_scm_mu_dir)
+endforeach
+
+mu_guile_scripts=[
+ join_paths('scripts', 'find-dups.scm'),
+ join_paths('scripts', 'msgs-count.scm'),
+ join_paths('scripts', 'histogram.scm')]
+mu_guile_script_dir=join_paths(datadir, 'mu', 'scripts')
+install_data(mu_guile_scripts, install_dir: mu_guile_script_dir)
+
+guile_builddir=meson.current_build_dir()
+
+if not get_option('tests').disabled()
+ subdir('tests')
+endif
--- /dev/null
+/*
+** Copyright (C) 2011-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include <config.h>
+#include "mu-guile-message.hh"
+
+#include "message/mu-message.hh"
+#include "utils/mu-utils.hh"
+
+#include <glib-object.h>
+#include <memory>
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wredundant-decls"
+#include <libguile.h>
+#pragma GCC diagnostic pop
+
+#include "mu-guile.hh"
+
+#include <mu-store.hh>
+#include <mu-query.hh>
+
+using namespace Mu;
+
+/* pseudo field, not in Xapian */
+constexpr auto MU_GUILE_MSG_FIELD_ID_TIMESTAMP = Field::id_size() + 1;
+
+/* some symbols */
+static SCM SYMB_PRIO_LOW, SYMB_PRIO_NORMAL, SYMB_PRIO_HIGH;
+static std::array<SCM, AllMessageFlagInfos.size()> SYMB_FLAGS;
+static SCM SYMB_CONTACT_TO, SYMB_CONTACT_CC, SYMB_CONTACT_BCC, SYMB_CONTACT_FROM;
+static long MSG_TAG;
+
+
+using MessageSPtr = std::unique_ptr<Message>;
+
+static gboolean
+mu_guile_scm_is_msg(SCM scm)
+{
+ return SCM_NIMP(scm) && (long)SCM_CAR(scm) == MSG_TAG;
+}
+
+static SCM
+message_scm_create(Xapian::Document&& doc)
+{
+ /* placement-new */
+
+ void *scm_mem{scm_gc_malloc(sizeof(Message), "msg")};
+ Message* msgp = new(scm_mem)Message(std::move(doc));
+
+ SCM_RETURN_NEWSMOB(MSG_TAG, msgp);
+}
+
+static const Message*
+message_from_scm(SCM msg_smob)
+{
+ return reinterpret_cast<Message*>(SCM_CDR(msg_smob));
+}
+
+static size_t
+message_scm_free(SCM msg_smob)
+{
+ if (auto msg = message_from_scm(msg_smob); msg)
+ msg->~Message();
+
+ return sizeof(Message);
+}
+
+static int
+message_scm_print(SCM msg_smob, SCM port, scm_print_state* pstate)
+{
+ scm_puts("#<msg ", port);
+
+ if (auto msg = message_from_scm(msg_smob); msg)
+ scm_puts(msg->path().c_str(), port);
+
+ scm_puts(">", port);
+ return 1;
+}
+
+struct FlagData {
+ Flags flags;
+ SCM lst;
+};
+
+#define MU_GUILE_INITIALIZED_OR_ERROR \
+ do { \
+ if (!(mu_guile_initialized())) { \
+ mu_guile_error(FUNC_NAME, \
+ 0, \
+ "mu not initialized; call mu:initialize", \
+ SCM_UNDEFINED); \
+ return SCM_UNSPECIFIED; \
+ } \
+ } while (0)
+
+
+static SCM
+get_flags_scm(const Message& msg)
+{
+ SCM lst{SCM_EOL};
+ const auto flags{msg.flags()};
+
+ for (auto i = 0; i != AllMessageFlagInfos.size(); ++i) {
+ const auto& info{AllMessageFlagInfos.at(i)};
+ if (any_of(info.flag & flags))
+ scm_append_x(scm_list_2(lst, scm_list_1(SYMB_FLAGS.at(i))));
+ }
+
+ return lst;
+}
+
+static SCM
+get_prio_scm(const Message& msg)
+{
+ switch (msg.priority()) {
+ case Priority::Low: return SYMB_PRIO_LOW;
+ case Priority::Normal: return SYMB_PRIO_NORMAL;
+ case Priority::High: return SYMB_PRIO_HIGH;
+
+ default: g_return_val_if_reached(SCM_UNDEFINED);
+ }
+}
+
+static SCM
+msg_string_list_field(const Message& msg, Field::Id field_id)
+{
+ SCM scmlst{SCM_EOL};
+ for (auto&& val: msg.document().string_vec_value(field_id)) {
+ SCM item;
+ item = scm_list_1(mu_guile_scm_from_string(val));
+ scmlst = scm_append_x(scm_list_2(scmlst, item));
+ }
+
+ return scmlst;
+}
+
+static SCM
+msg_contact_list_field(const Message& msg, Field::Id field_id)
+{
+ return scm_from_utf8_string(
+ to_string(msg.document().contacts_value(field_id)).c_str());
+}
+
+static SCM
+get_body(const Message& msg, bool html)
+{
+ if (const auto body = html ? msg.body_html() : msg.body_text(); body)
+ return mu_guile_scm_from_string(*body);
+ else
+ return SCM_BOOL_F;
+}
+
+SCM_DEFINE(get_field,
+ "mu:c:get-field",
+ 2,
+ 0,
+ 0,
+ (SCM MSG, SCM FIELD),
+ "Get the field FIELD from message MSG.\n")
+#define FUNC_NAME s_get_field
+{
+ SCM_ASSERT(mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME);
+ auto msg{message_from_scm(MSG)};
+ SCM_ASSERT(msg, MSG, SCM_ARG1, FUNC_NAME);
+
+ SCM_ASSERT(scm_integer_p(FIELD), FIELD, SCM_ARG2, FUNC_NAME);
+ const auto field_opt{field_from_number(static_cast<size_t>(scm_to_int(FIELD)))};
+ SCM_ASSERT(!!field_opt, FIELD, SCM_ARG2, FUNC_NAME);
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wswitch-enum"
+ switch (field_opt->id) {
+ case Field::Id::Priority:
+ return get_prio_scm(*msg);
+ case Field::Id::Flags:
+ return get_flags_scm(*msg);
+ case Field::Id::BodyText:
+ return get_body(*msg, false);
+ default:
+ break;
+ }
+#pragma GCC diagnostic pop
+
+ switch (field_opt->type) {
+ case Field::Type::String:
+ return mu_guile_scm_from_string(msg->document().string_value(field_opt->id));
+ case Field::Type::ByteSize:
+ case Field::Type::TimeT:
+ case Field::Type::Integer:
+ return scm_from_uint(msg->document().integer_value(field_opt->id));
+ case Field::Type::StringList:
+ return msg_string_list_field(*msg, field_opt->id);
+ case Field::Type::ContactList:
+ return msg_contact_list_field(*msg, field_opt->id);
+ default:
+ SCM_ASSERT(0, FIELD, SCM_ARG2, FUNC_NAME);
+ }
+
+ return SCM_UNSPECIFIED;
+}
+#undef FUNC_NAME
+
+static SCM
+contacts_to_list(const Message& msg, Option<Field::Id> field_id)
+{
+ SCM list{SCM_EOL};
+
+ const auto contacts{field_id ?
+ msg.document().contacts_value(*field_id) :
+ msg.all_contacts()};
+
+ for (auto&& contact: contacts) {
+ SCM item{scm_list_1(
+ scm_cons(mu_guile_scm_from_string(contact.name),
+ mu_guile_scm_from_string(contact.email)))};
+ list = scm_append_x(scm_list_2(list, item));
+ }
+
+ return list;
+}
+
+SCM_DEFINE(get_contacts,
+ "mu:c:get-contacts",
+ 2,
+ 0,
+ 0,
+ (SCM MSG, SCM CONTACT_TYPE),
+ "Get a list of contact information pairs.\n")
+#define FUNC_NAME s_get_contacts
+{
+ SCM list;
+
+ MU_GUILE_INITIALIZED_OR_ERROR;
+
+ SCM_ASSERT(mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME);
+ auto msg{message_from_scm(MSG)};
+ SCM_ASSERT(msg, MSG, SCM_ARG1, FUNC_NAME);
+
+ SCM_ASSERT(scm_symbol_p(CONTACT_TYPE) || scm_is_bool(CONTACT_TYPE),
+ CONTACT_TYPE,
+ SCM_ARG2,
+ FUNC_NAME);
+
+ if (CONTACT_TYPE == SCM_BOOL_F)
+ return SCM_UNSPECIFIED; /* nothing to do */
+
+ Option<Field::Id> field_id;
+ if (CONTACT_TYPE == SCM_BOOL_T)
+ field_id = {}; /* get all */
+ else {
+ if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_TO))
+ field_id = Field::Id::To;
+ else if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_CC))
+ field_id = Field::Id::Cc;
+ else if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_BCC))
+ field_id = Field::Id::Bcc;
+ else if (scm_is_eq(CONTACT_TYPE, SYMB_CONTACT_FROM))
+ field_id = Field::Id::From;
+ else {
+ mu_guile_error(FUNC_NAME, 0, "invalid contact type", SCM_UNDEFINED);
+ return SCM_UNSPECIFIED;
+ }
+ }
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wcast-function-type"
+ list = contacts_to_list(*msg, field_id);
+#pragma GCC diagnostic pop
+
+ /* explicitly close the file backend, so we won't run out of fds */
+
+
+ return list;
+}
+#undef FUNC_NAME
+
+SCM_DEFINE(get_parts,
+ "mu:c:get-parts",
+ 1,
+ 1,
+ 0,
+ (SCM MSG, SCM ATTS_ONLY),
+ "Get the list of mime-parts for MSG. If ATTS_ONLY is #t, only"
+ "get parts that are (look like) attachments. The resulting list has "
+ "elements which are list of the form (index name mime-type size).\n")
+#define FUNC_NAME s_get_parts
+{
+ MU_GUILE_INITIALIZED_OR_ERROR;
+
+ SCM_ASSERT(mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME);
+ auto msg{message_from_scm(MSG)};
+ SCM_ASSERT(msg, MSG, SCM_ARG1, FUNC_NAME);
+ SCM_ASSERT(scm_is_bool(ATTS_ONLY), ATTS_ONLY, SCM_ARG2, FUNC_NAME);
+
+ SCM attlist = SCM_EOL; /* empty list */
+ bool attachments_only = ATTS_ONLY == SCM_BOOL_T ? TRUE : FALSE;
+
+ size_t n{};
+ for (auto&& part: msg->parts()) {
+
+ if (attachments_only && !part.is_attachment())
+ continue;
+
+ const auto mime_type{part.mime_type()};
+ const auto filename{part.cooked_filename()};
+
+ SCM elm = scm_list_5(
+ /* msg */
+ mu_guile_scm_from_string(msg->path().c_str()),
+ /* index */
+ scm_from_uint(n++),
+ /* filename or #f */
+ filename ? mu_guile_scm_from_string(*filename) : SCM_BOOL_F,
+ /* mime-type */
+ mime_type ? mu_guile_scm_from_string(*mime_type) : SCM_BOOL_F,
+ /* size */
+ part.size() > 0 ? scm_from_uint(part.size()) : SCM_BOOL_F);
+
+ attlist = scm_cons(elm, attlist);
+ }
+
+ /* explicitly close the file backend, so we won't run of fds */
+ msg->unload_mime_message();
+
+ return attlist;
+}
+#undef FUNC_NAME
+
+SCM_DEFINE(get_header,
+ "mu:c:get-header",
+ 2,
+ 0,
+ 0,
+ (SCM MSG, SCM HEADER),
+ "Get an arbitrary HEADER from MSG.\n")
+#define FUNC_NAME s_get_header
+{
+ MU_GUILE_INITIALIZED_OR_ERROR;
+
+ SCM_ASSERT(mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME);
+ auto msg{message_from_scm(MSG)};
+ SCM_ASSERT(msg, MSG, SCM_ARG1, FUNC_NAME);
+
+ SCM_ASSERT(scm_is_string(HEADER) || HEADER == SCM_UNDEFINED, HEADER, SCM_ARG2, FUNC_NAME);
+
+ char *header = scm_to_utf8_string(HEADER);
+ SCM val = mu_guile_scm_from_string(msg->header(header).value_or(""));
+ free(header);
+
+ /* explicitly close the file backend, so we won't run of fds */
+ msg->unload_mime_message();
+
+ return val;
+}
+#undef FUNC_NAME
+SCM_DEFINE(for_each_message,
+ "mu:c:for-each-message",
+ 3,
+ 0,
+ 0,
+ (SCM FUNC, SCM EXPR, SCM MAXNUM),
+ "Call FUNC for each msg in the message store matching EXPR. EXPR is"
+ "either a string containing a mu search expression or a boolean; in the former "
+ "case, limit the messages to only those matching the expression, in the "
+ "latter case, match /all/ messages if the EXPR equals #t, and match "
+ "none if EXPR equals #f.")
+#define FUNC_NAME s_for_each_message
+{
+ char* expr{};
+
+ MU_GUILE_INITIALIZED_OR_ERROR;
+
+ SCM_ASSERT(scm_procedure_p(FUNC), FUNC, SCM_ARG1, FUNC_NAME);
+ SCM_ASSERT(scm_is_bool(EXPR) || scm_is_string(EXPR), EXPR, SCM_ARG2, FUNC_NAME);
+ SCM_ASSERT(scm_is_integer(MAXNUM), MAXNUM, SCM_ARG3, FUNC_NAME);
+
+ if (EXPR == SCM_BOOL_F)
+ return SCM_UNSPECIFIED; /* nothing to do */
+
+ if (EXPR == SCM_BOOL_T)
+ expr = strdup("\"\""); /* note, "" matches *all* messages */
+ else
+ expr = scm_to_utf8_string(EXPR);
+
+ const auto res = mu_guile_store().run_query(expr,{}, {}, scm_to_int(MAXNUM));
+ free(expr);
+ if (!res)
+ return SCM_UNSPECIFIED;
+
+ for (auto&& mi : *res) {
+ if (auto xdoc{mi.document()}; xdoc) {
+ scm_call_1(FUNC, message_scm_create(std::move(xdoc.value())));
+ }
+ }
+
+ return SCM_UNSPECIFIED;
+}
+#undef FUNC_NAME
+
+static SCM
+register_symbol(const char* name)
+{
+ SCM scm;
+
+ scm = scm_from_utf8_symbol(name);
+ scm_c_define(name, scm);
+ scm_c_export(name, NULL);
+
+ return scm;
+}
+
+static void
+define_symbols(void)
+{
+ SYMB_CONTACT_TO = register_symbol("mu:contact:to");
+ SYMB_CONTACT_CC = register_symbol("mu:contact:cc");
+ SYMB_CONTACT_FROM = register_symbol("mu:contact:from");
+ SYMB_CONTACT_BCC = register_symbol("mu:contact:bcc");
+
+ SYMB_PRIO_LOW = register_symbol("mu:prio:low");
+ SYMB_PRIO_NORMAL = register_symbol("mu:prio:normal");
+ SYMB_PRIO_HIGH = register_symbol("mu:prio:high");
+
+ for (auto i = 0U; i != AllMessageFlagInfos.size(); ++i) {
+ const auto& info{AllMessageFlagInfos.at(i)};
+ const auto name = "mu:flag:" + std::string{info.name};
+ SYMB_FLAGS[i] = register_symbol(name.c_str());
+ }
+}
+static void
+define_vars(void)
+{
+ field_for_each([](auto&& field){
+
+ auto defvar = [&](auto&& fname, auto&& ffield) {
+ const auto name{"mu:field:" + std::string{fname}};
+ scm_c_define(name.c_str(), scm_from_uint(field.value_no()));
+ scm_c_export(name.c_str(), NULL);
+ };
+
+ // define for both name and (if exists) alias.
+ if (!field.name.empty())
+ defvar(field.name, field);
+ if (!field.alias.empty())
+ defvar(field.alias, field);
+ });
+
+ /* non-Xapian field: timestamp */
+ scm_c_define("mu:field:timestamp",
+ scm_from_uint(MU_GUILE_MSG_FIELD_ID_TIMESTAMP));
+ scm_c_export("mu:field:timestamp", NULL);
+
+}
+
+void*
+mu_guile_message_init(void* data)
+{
+ MSG_TAG = scm_make_smob_type("message", sizeof(Message));
+
+ scm_set_smob_free(MSG_TAG, message_scm_free);
+ scm_set_smob_print(MSG_TAG, message_scm_print);
+
+ define_vars();
+ define_symbols();
+
+#ifndef SCM_MAGIC_SNARFER
+#include "mu-guile-message.x"
+#endif /*SCM_MAGIC_SNARFER*/
+
+ return NULL;
+}
--- /dev/null
+/*
+** Copyright (C) 2011-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_GUILE_MESSAGE_H__
+#define MU_GUILE_MESSAGE_H__
+
+/**
+ * Initialize this mu guile module.
+ *
+ * @param data
+q *
+ * @return
+ */
+extern "C" {
+void* mu_guile_message_init(void* data);
+}
+
+#endif /*MU_GUILE_MESSAGE_HH__*/
--- /dev/null
+/* cpp arguments: mu-guile-message.cc -DHAVE_CONFIG_H -I. -I.. -I../lib -I/usr/local/include/guile/3.0 -pthread -I/usr/include/glib-2.0 -I/usr/lib64/glib-2.0/include -I/usr/include/sysprof-4 -I/usr/include/libmount -I/usr/include/blkid -pthread -fno-strict-aliasing -Wall -Wextra -Wundef -Wwrite-strings -Wpointer-arith -Wmissing-declarations -Wredundant-decls -Wno-unused-parameter -Wno-missing-field-initializers -Wformat=2 -Wcast-align -Wformat-nonliteral -Wformat-security -Wsign-compare -Wstrict-aliasing -Wshadow -Winline -Wpacked -Wmissing-format-attribute -Wmissing-noreturn -Winit-self -Wmissing-include-dirs -Wunused-but-set-variable -Warray-bounds -Wreturn-type -Wno-overloaded-virtual -Wswitch-enum -Wswitch-default -Wno-error=unused-parameter -Wno-error=missing-field-initializers -Wno-error=overloaded-virtual -Wno-redundant-decls -Wno-missing-declarations -Wno-suggest-attribute=noreturn -O2 -Wno-inline */
+scm_c_define_gsubr (s_get_field, 2, 0, 0, (scm_t_subr) get_field);;
+scm_c_define_gsubr (s_get_contacts, 2, 0, 0, (scm_t_subr) get_contacts);;
+scm_c_define_gsubr (s_get_parts, 1, 1, 0, (scm_t_subr) get_parts);;
+scm_c_define_gsubr (s_get_header, 2, 0, 0, (scm_t_subr) get_header);;
+scm_c_define_gsubr (s_for_each_message, 3, 0, 0, (scm_t_subr) for_each_message);;
--- /dev/null
+/*
+** Copyright (C) 2011-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include <config.h>
+
+#include "mu-guile.hh"
+
+#include <locale.h>
+#include <glib-object.h>
+
+#include <mu-store.hh>
+#include <mu-query.hh>
+
+#include <utils/mu-utils-file.hh>
+
+using namespace Mu;
+
+SCM
+mu_guile_scm_from_string(const std::string& str)
+{
+ if (str.empty())
+ return SCM_BOOL_F;
+ else
+ return scm_from_stringn(str.c_str(), str.size(),
+ "UTF-8",
+ SCM_FAILED_CONVERSION_QUESTION_MARK);
+}
+
+SCM
+mu_guile_error(const char* func_name, int status, const char* fmt, SCM args)
+{
+ scm_error_scm(scm_from_locale_symbol("MuError"),
+ scm_from_utf8_string(func_name ? func_name : "<nameless>"),
+ scm_from_utf8_string(fmt),
+ args,
+ scm_list_1(scm_from_int(status)));
+
+ return SCM_UNSPECIFIED;
+}
+
+SCM
+mu_guile_g_error(const char* func_name, GError* err)
+{
+ scm_error_scm(scm_from_locale_symbol("MuError"),
+ scm_from_utf8_string(func_name),
+ scm_from_utf8_string(err ? err->message : "error"),
+ SCM_UNDEFINED,
+ SCM_UNDEFINED);
+
+ return SCM_UNSPECIFIED;
+}
+
+/* there can be only one */
+
+static Option<Mu::Store> StoreSingleton = Nothing;
+
+static bool
+mu_guile_init_instance(const std::string& muhome) try {
+ setlocale(LC_ALL, "");
+
+ const auto path{runtime_path(RuntimePath::XapianDb, muhome)};
+ auto store = Store::make(path);
+ if (!store) {
+ mu_critical("error creating store @ %s: %s", path,
+ store.error().what());
+ throw store.error();
+ } else
+ StoreSingleton.emplace(std::move(store.value()));
+
+ mu_debug("mu-guile: opened store @ {} (n={}); maildir: {}",
+ StoreSingleton->path(),
+ StoreSingleton->size(),
+ StoreSingleton->root_maildir());
+
+ return true;
+
+} catch (const Xapian::Error& xerr) {
+ mu_critical("{}: xapian error '{}'", __func__, xerr.get_msg());
+ return false;
+} catch (const std::runtime_error& re) {
+ mu_critical("{}: error: {}", __func__, re.what());
+ return false;
+} catch (const std::exception& e) {
+ mu_critical("{}: caught exception: {}", __func__, e.what());
+ return false;
+} catch (...) {
+ mu_critical("{}: caught exception", __func__);
+ return false;
+}
+
+static void
+mu_guile_uninit_instance()
+{
+ StoreSingleton.reset();
+}
+
+Mu::Store&
+mu_guile_store()
+{
+ if (!StoreSingleton)
+ mu_error("mu guile not initialized");
+
+ return StoreSingleton.value();
+}
+
+gboolean
+mu_guile_initialized()
+{
+ g_debug("initialized ? %u", !!StoreSingleton);
+
+ return !!StoreSingleton;
+}
+
+SCM_DEFINE_PUBLIC(mu_initialize,
+ "mu:initialize",
+ 0,
+ 1,
+ 0,
+ (SCM MUHOME),
+ "Initialize mu - needed before you call any of the other "
+ "functions. Optionally, you can provide MUHOME which should be an "
+ "absolute path to your mu home directory "
+ "-- typically, the default, ~/.cache/mu, should be just fine.")
+#define FUNC_NAME s_mu_initialize
+{
+ char* muhome;
+
+ SCM_ASSERT(scm_is_string(MUHOME) || MUHOME == SCM_BOOL_F || SCM_UNBNDP(MUHOME),
+ MUHOME,
+ SCM_ARG1,
+ FUNC_NAME);
+
+ if (mu_guile_initialized())
+ return mu_guile_error(FUNC_NAME, 0, "Already initialized", SCM_UNSPECIFIED);
+
+ if (SCM_UNBNDP(MUHOME) || MUHOME == SCM_BOOL_F)
+ muhome = NULL;
+ else
+ muhome = scm_to_utf8_string(MUHOME);
+
+ if (!mu_guile_init_instance(muhome ? muhome : "")) {
+ free(muhome);
+ mu_guile_error(FUNC_NAME, 0, "Failed to initialize mu", SCM_UNSPECIFIED);
+ return SCM_UNSPECIFIED;
+ }
+
+ g_debug("mu-guile: initialized @ %s", muhome ? muhome : "<default>");
+ free(muhome);
+
+ /* cleanup when we're exiting */
+ atexit(mu_guile_uninit_instance);
+
+ return SCM_UNSPECIFIED;
+}
+#undef FUNC_NAME
+
+SCM_DEFINE_PUBLIC(mu_initialized_p,
+ "mu:initialized?",
+ 0,
+ 0,
+ 0,
+ (void),
+ "Whether mu is initialized or not.\n")
+#define FUNC_NAME s_mu_initialized_p
+{
+ return mu_guile_initialized() ? SCM_BOOL_T : SCM_BOOL_F;
+}
+#undef FUNC_NAME
+
+SCM_DEFINE(log_func,
+ "mu:c:log",
+ 1,
+ 0,
+ 1,
+ (SCM LEVEL, SCM FRM, SCM ARGS),
+ "log some message at LEVEL using a list of ARGS applied to FRM"
+ "(in 'simple-format' notation).\n")
+#define FUNC_NAME s_log_func
+{
+ gchar* output;
+ SCM str;
+ int level;
+
+ SCM_ASSERT(scm_integer_p(LEVEL), LEVEL, SCM_ARG1, FUNC_NAME);
+ SCM_ASSERT(scm_is_string(FRM), FRM, SCM_ARG2, "<write_log>");
+ SCM_VALIDATE_REST_ARGUMENT(ARGS);
+
+ level = scm_to_int(LEVEL);
+ if (level != G_LOG_LEVEL_MESSAGE && level != G_LOG_LEVEL_WARNING &&
+ level != G_LOG_LEVEL_CRITICAL)
+ return mu_guile_error(FUNC_NAME, 0, "invalid log level", SCM_UNSPECIFIED);
+
+ str = scm_simple_format(SCM_BOOL_F, FRM, ARGS);
+
+ if (!scm_is_string(str))
+ return SCM_UNSPECIFIED;
+
+ output = scm_to_utf8_string(str);
+ g_log(G_LOG_DOMAIN, (GLogLevelFlags)level, "%s", output);
+ free(output);
+
+ return SCM_UNSPECIFIED;
+}
+#undef FUNC_NAME
+
+static struct {
+ const char* name;
+ unsigned val;
+} VAR_PAIRS[] = {
+
+ {"mu:message", G_LOG_LEVEL_MESSAGE},
+ {"mu:warning", G_LOG_LEVEL_WARNING},
+ {"mu:critical", G_LOG_LEVEL_CRITICAL}};
+
+static void
+define_vars(void)
+{
+ unsigned u;
+ for (u = 0; u != G_N_ELEMENTS(VAR_PAIRS); ++u) {
+ scm_c_define(VAR_PAIRS[u].name, scm_from_uint(VAR_PAIRS[u].val));
+ scm_c_export(VAR_PAIRS[u].name, NULL);
+ }
+}
+
+void*
+mu_guile_init(void* data)
+{
+ define_vars();
+
+#ifndef SCM_MAGIC_SNARFER
+#include "mu-guile.x"
+#endif /*SCM_MAGIC_SNARFER*/
+
+ return NULL;
+}
--- /dev/null
+/*
+** Copyright (C) 2011-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef __MU_GUILE_H__
+#define __MU_GUILE_H__
+
+#include <glib.h>
+#include <mu-query.hh>
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wredundant-decls"
+#include <libguile.h>
+#pragma GCC diagnostic pop
+
+/**
+ * get the singleton Store instance
+ */
+Mu::Store& mu_guile_store();
+
+/**
+ * whether mu-guile is initialized
+ *
+ * @return TRUE if MuGuile is Initialized, FALSE otherwise
+ */
+gboolean mu_guile_initialized();
+
+/**
+ * raise a guile error (based on a GError)
+ *
+ * @param func_name function name
+ * @param err the error
+ *
+ * @return SCM_UNSPECIFIED
+ */
+SCM mu_guile_g_error(const char* func_name, GError* err);
+
+/**
+ * raise a guile error
+ *
+ * @param func_name function
+ * @param status err code
+ * @param fmt format string for error msg
+ * @param args params for format string
+ *
+ * @return SCM_UNSPECIFIED
+ */
+SCM mu_guile_error(const char* func_name, int status, const char* fmt, SCM args);
+
+/**
+ * convert a string into an SCM -- . It assumes str is in UTF8 encoding, and
+ * replace characters with '?' if needed.
+ *
+ * @param str a string
+ *
+ * @return a guile string or #f for empty
+ */
+SCM mu_guile_scm_from_string(const std::string& str);
+
+/**
+ * Initialize this mu guile module.
+ *
+ * @param data
+ *
+ * @return
+ */
+extern "C" {
+void* mu_guile_init(void* data);
+}
+#endif /*__MU_GUILE_H__*/
--- /dev/null
+\input texinfo.tex @c -*-texinfo-*-
+@c %**start of header
+@setfilename mu-guile.info
+@settitle mu-guile user manual
+
+@c Use proper quote and backtick for code sections in PDF output
+@c Cf. Texinfo manual 14.2
+@set txicodequoteundirected
+@set txicodequotebacktick
+
+@documentencoding UTF-8
+@c %**end of header
+
+@include version.texi
+
+@copying
+Copyright @copyright{} 2012-@value{UPDATED-YEAR} Dirk-Jan C. Binnema
+
+@quotation
+Permission is granted to copy, distribute and/or modify this document
+under the terms of the GNU Free Documentation License, Version 1.3 or
+any later version published by the Free Software Foundation; with no
+Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A
+copy of the license is included in the section entitled ``GNU Free
+Documentation License.''
+@end quotation
+@end copying
+
+@titlepage
+@title @t{mu-guile} - extending @t{mu} with Guile Scheme
+@subtitle version @value{VERSION}
+@author Dirk-Jan C. Binnema
+
+@c The following two commands start the copyright page.
+@page
+@vskip 0pt plus 1filll
+@insertcopying
+@end titlepage
+
+@dircategory The Algorithmic Language Scheme
+@direntry
+* Mu-guile: (mu-guile). Guile-bindings for the mu e-mail indexer/searcher
+@end direntry
+
+@contents
+
+@ifnottex
+@node Top
+@top mu-guile manual
+@end ifnottex
+
+@iftex
+@node Welcome to mu-guile
+@unnumbered Welcome to mu-guile
+@end iftex
+
+Welcome to @t{mu-guile}!
+
+@t{mu} is a program for indexing and searching your e-mails. It can search
+your messages in many different ways, but sometimes that may not be
+enough. If you have very specific queries, or want do generate some
+statistics, you need some more power.
+
+@t{mu-guile} is made for those cases. @t{mu-guile} exposes the internals of
+@t{mu} and its database to the @t{guile} programming language. Guile is the
+@emph{GNU Ubiquitous Intelligent Language for Extensions} - a version of the
+@emph{Scheme} programming language and the official GNU extension language.
+
+Guile/Scheme is a member of the @emph{Lisp} family of programming languages --
+like emacs-lisp, @emph{Racket}, Common Lisp. If you're not familiar with
+Scheme, @t{mu-guile} is an excellent opportunity to learn a bit about!
+
+Trust me, it's not very hard -- and it's @emph{fun}!
+
+@menu
+* Getting started::
+* Initializing mu-guile::
+* Messages::
+* Contacts::
+* Attachments and other parts::
+* Statistics::
+* Plotting data::
+* Writing scripts::
+
+Appendices
+
+* Recipes:: Snippets do specific things
+* GNU Free Documentation License:: The license of this manual.
+@end menu
+
+@node Getting started
+@chapter Getting started
+
+@menu
+* Installation::
+* Making sure it works::
+@end menu
+
+This chapter walks you through the installation and the basic setup.
+
+@node Installation
+@section Installation
+
+@t{mu-guile} is part of @t{mu} - by installing the latter, the former is
+necessarily installed as well. At the time of writing, there are no
+distribution-provided packaged versions of @t{mu-guile}; so for now, you need
+to follow the steps below.
+
+@subsection Guile 2.x
+
+@t{mu-guile} is built automatically when @t{mu} is built, if you have
+@t{guile} version 2 or higher. (@t{mu} checks for this during
+@t{configure}). Thus, the first step is to ensure you have @t{guile}
+installed.
+
+On Debian/Ubuntu you can install @t{guile} 2.x using the @t{guile-2.0-dev}
+package (and its dependencies):
+@example
+$ sudo apt-get install guile-2.0-dev
+@end example
+
+At the time of writing, there are no official packages for
+Fedora@footnote{@url{https://bugzilla.redhat.com/show_bug.cgi?id=678238}}. If
+you are using Fedora or any other system that does not have packages, you need
+to compile @t{guile} from
+source@footnote{@url{http://www.gnu.org/software/guile/manual/html_node/Obtaining-and-Installing-Guile.html#Obtaining-and-Installing-Guile}}.
+
+@subsection gnuplot
+
+For creating graphs with @t{mu-guile}, you need the @t{gnuplot} program --
+most likely, there is a package available for your system; for example:
+
+@example
+$ sudo apt-get install gnuplot
+@end example
+
+and in Fedora:
+
+@example
+$ sudo yum install gnuplot
+@end example
+
+@subsection mu
+
+Assuming @t{guile} 2.x is installed correctly, @t{mu} finds it during its
+@t{configure}-stage, and creates @t{mu-guile}. Building @t{mu} follows the
+normal steps -- please see the @t{mu} documentation for the details.
+
+The output of @t{./configure} should end with a little text describing the
+detected versions of various libraries @t{mu} depends on. In particular, it
+should mention the @t{guile} version, e.g.
+
+@example
+Guile version : 2.0.3.82-a2c66
+@end example
+
+If you don't see any line referring to @t{guile}, please install it, and run
+@t{configure} again. After a successful @t{./configure}, we can make and
+install the package:
+
+@example
+$ make && sudo make install
+@end example
+
+@subsection mu-guile
+
+After this, @t{mu} and @t{mu-guile} are installed -- usually somewhere under
+@t{/usr/local}.You may need to update @t{guile}'s @code{%load-path} to find it
+there. You can check the current @code{%load-path} with the following:
+
+@example
+guile -c '(display %load-path)(newline)'
+@end example
+
+If necessary, you can add the @t{%load-path} by adding to your
+@file{~/.guile}:
+
+@lisp
+(set! %load-path (cons "/usr/local/share/guile/site/2.0" %load-path))
+@end lisp
+
+Or, alternatively, you can set @t{GUILE_LOAD_PATH}:
+@example
+export GUILE_LOAD_PATH=/usr/local/share/guile/site/2.0
+@end example
+
+In both cases the directory should be the directory that contains the
+installed @t{mu.scm}; if you installed @t{mu} under a different prefix, you
+must change the @code{%load-path} accordingly. After this, you should be ready
+to go!
+
+Furthermore, you need to ensure that @t{guile} can find the mu-guile
+library; for this we can use @code{LTDL_LIBRARY_PATH}, e.g.
+@example
+export LTDL_LIBRARY_PATH=/usr/local/lib
+@end example
+
+@node Making sure it works
+@section Making sure it works
+
+Assuming @t{mu-guile} has been installed correctly (@ref{Installation}), and
+also assuming that you have already indexed your e-mail messages (if
+necessary, see the @t{mu-index} man-page), we are ready to start @t{mu-guile};
+a session may look something like this:
+
+@cartouche
+@verbatim
+GNU Guile 2.0.5.123-4bd53
+Copyright (C) 1995-2012 Free Software Foundation, Inc.
+
+Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'.
+This program is free software, and you are welcome to redistribute it
+under certain conditions; type `,show c' for details.
+
+Enter `,help' for help.
+scheme@(guile-user)>
+@end verbatim
+@end cartouche
+
+@noindent
+Now, copy-paste the following after the prompt:
+
+@cartouche
+@lisp
+(use-modules (mu))
+(mu:initialize)
+(for-each
+ (lambda(msg)
+ (format #t "Subject: ~a\n" (mu:subject msg)))
+ (mu:message-list "hello"))
+@end lisp
+@end cartouche
+
+@noindent
+After pressing @key{Enter}, you should get a list of all subjects of messages
+that match @t{hello}:
+
+@verbatim
+...
+Subject: RE: The Bird Serpent War Cataclysm
+Subject: Hello!
+Subject: Re: post-run tomorrow
+Subject: When all is lost
+...
+@end verbatim
+
+@noindent
+If all this works, congratulations! @t{mu-guile} is installed now, ready to
+serve your every searching need!
+
+@node Initializing mu-guile
+@chapter Initializing mu-guile
+
+We now have installed @t{mu-guile}, and in @ref{Making sure it works}
+confirmed that things work by trying some simple script. In this and the
+following chapters, we take a closer look at programming with @t{mu-guile}.
+
+It is possible to write separate programs with @t{mu-guile}, but for now we'll
+do things @emph{interactively}, that is, from the Guile-prompt
+(``@abbr{REPL}'').
+
+As we have seen, we start our @t{mu-guile} session by starting @t{guile}:
+
+@verbatim
+$ guile
+@end verbatim
+
+@cartouche
+@verbatim
+GNU Guile 2.0.5.123-4bd53
+Copyright (C) 1995-2012 Free Software Foundation, Inc.
+
+Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'.
+This program is free software, and you are welcome to redistribute it
+under certain conditions; type `,show c' for details.
+
+Enter `,help' for help.
+scheme@(guile-user)>
+@end verbatim
+@end cartouche
+
+The first thing we need to do is loading the modules. All the basics are in
+the @t{(mu)} module, with some statistical extras in @t{(mu stats)}, and some
+graph plotting functionality in @t{(mu plot)}@footnote{@code{(mu plot)}
+requires the @t{gnuplot} program}. Let's load all of them:
+@verbatim
+scheme@(guile-user)> (use-modules (mu) (mu stats) (mu plot))
+@end verbatim
+
+The first time you do this, @t{guile} will probably respond by showing some
+messages about compiling the modules, and then return to you with another
+prompt. Before we can do anything with @t{mu guile}, we need to initialize the
+system:
+
+@verbatim
+scheme@(guile-user)> (mu:initialize)
+@end verbatim
+
+This opens the database for reading, using the default location of
+@file{~/.cache/mu}@footnote{If you keep your @t{mu} database in a non-standard
+place, use @code{(mu:initialize "/path/to/my/mu/")}}
+
+Now, @t{mu-guile} is ready to go. In the next chapter, we go through the
+modules and show what you can do with them.
+
+@node Messages
+@chapter Messages
+
+In this chapter, we discuss searching messages and doing things with them.
+
+@menu
+* Finding messages:: query for messages in the database
+* Message methods:: what methods are available for messages?
+* Example - the longest subject:: find the messages with the longest subject
+@end menu
+
+@node Finding messages
+@section Finding messages
+Now we are ready to retrieve some messages from the system. There are two main
+procedures to do this:
+
+@itemize
+@item @code{(mu:message-list [<search-expression>])}
+@item @code{(mu:for-each-message <procedure> [<search-expression>])}
+@end itemize
+
+@noindent
+The first procedure, @code{mu:message-list} returns a list of all messages
+matching @t{<search-expression>}; if you leave @t{<search-expression>} out, it
+returns @emph{all} messages. For example, to get all messages with @t{coffee}
+in the subject line:
+
+@verbatim
+scheme@(guile-user)> (mu:message-list "subject:coffee")
+$1 = (#<<mu:message> 9040640> #<<mu:message> 9040630>
+ #<<mu:message> 9040570>)
+@end verbatim
+
+@noindent
+Apparently, we have three messages matching @t{subject:coffee}, so we get a
+list of three @code{<mu:message>} objects. Let's just use the
+@code{mu:subject} procedure ('method') provided by @code{<mu:message>} objects
+to retrieve the subject-field (more about methods in the next section).
+
+For your convenience, @t{guile} has saved the result of our last query in a
+variable called @t{$1}, so to get the subject of the first message in the
+list, we can do:
+
+@verbatim
+scheme@(guile-user)> (mu:subject (car $1))
+$2 = "Re: best coffee ever!"
+@end verbatim
+
+@noindent
+The second procedure we mentioned, @code{mu:for-each-message}, executes some
+procedure for each message matched by the search expression (or @emph{all}
+messages if the search expression is omitted):
+
+@verbatim
+scheme@(guile-user)> (mu:for-each-message
+ (lambda(msg)
+ (display (mu:subject msg))
+ (newline))
+ "subject:coffee")
+Re: best coffee ever!
+best coffee ever!
+Coffee beans
+scheme@(guile-user)>
+@end verbatim
+
+@noindent
+Using @code{mu:message-list} and/or
+@code{mu:for-each-message}@footnote{Implementation node:
+@code{mu:message-list} is implemented in terms of @code{mu:for-each-message},
+not the other way around. Due to the way @t{mu} works,
+@code{mu:for-each-message} is rather more efficient than a combination of
+@code{for-each} and @code{mu:message-list}} and a couple of @t{<mu:message>}
+methods, together with what Guile/Scheme provides, should allow for many
+interesting programs.
+
+@node Message methods
+@section Message methods
+
+Now that we've seen how to retrieve lists of message objects
+(@code{<mu:message>}), let's see what we can do with such an object.
+
+@code{<mu:message>} defines the following methods that all take a single
+@code{<mu:message>} object as a parameter. We won't go into the exact meanings
+for all of these procedures here - for the details about various flags /
+properties, please refer to the @t{mu-find} man-page.
+
+@itemize
+@item @code{(mu:bcc msg)}: the @t{Bcc} field of the message, or @t{#f} if there is none
+@item @code{(mu:body-html msg)}: : the html body of the message, or @t{#f} if there is none
+@item @code{(mu:body-txt msg)}: the plain-text body of the message, or @t{#f} if there is none
+@item @code{(mu:cc msg)}: the @t{Bcc} field of the message, or @t{#f} if there is none
+@item @code{(mu:date msg)}: the @t{Date} field of the message, or 0 if there is none
+@item @code{(mu:flags msg)}: list of message-flags for this message
+@item @code{(mu:from msg)}: the @t{From} field of the message, or @t{#f} if there is none
+@item @code{(mu:maildir msg)}: the maildir this message lives in, or @t{#f} if there is none
+@item @code{(mu:message-id msg)}: the @t{Message-Id} field of the message, or @t{#f} if there is none
+@item @code{(mu:path msg)}: the file system path for this message
+@item @code{(mu:priority msg)}: the priority of this message (either @t{mu:prio:low}, @t{mu:prio:normal} or @t{mu:prio:high}
+@item @code{(mu:references msg)}: the list of messages (message-ids) this message
+refers to in(mu: the @t{References:} header
+@item @code{(mu:size msg)}: size of the message in bytes
+@item @code{(mu:subject msg)}: the @t{Subject} field of the message, or @t{#f} if there is none.
+@item @code{(mu:tags msg)}: list of tags for this message
+@item @code{(mu:timestamp msg)}: the timestamp (mtime) of the message file, or
+#f if there is none.
+message file
+@item @code{(mu:to msg)}: the sender of the message, or @t{#f} if there is none
+@end itemize
+
+With these methods, we can query messages for their properties; for example:
+
+@verbatim
+scheme@(guile-user)> (define msg (car (mu:message-list "snow")))
+scheme@(guile-user)> (mu:subject msg)
+$1 = "Re: Running in the snow is beautiful"
+scheme@(guile-user)> (mu:flags msg)
+$2 = (mu:flag:replied mu:flag:seen)
+scheme@(guile-user)> (strftime "%F" (localtime (mu:date msg)))
+$3 = "2011-01-15"
+@end verbatim
+
+There are a couple more methods:
+@itemize
+@item @code{(mu:header msg "<header-name>")} returns an arbitrary message
+header (or @t{#f} if not found) -- e.g. @code{(header msg "User-Agent")}
+@item If you include the @t{mu contact} module, the @code{(mu:contacts
+msg [contact-type])} method (to get a list of contacts) is
+added. @xref{Contacts}.
+@item If you include the @t{mu part} module, the @code{((mu:parts msg)} and
+@code{(mu:attachments msg)} methods are added. @xref{Attachments and other parts}.
+@end itemize
+
+@node Example - the longest subject
+@section Example - the longest subject
+
+Now, let's write a little example -- let's find out what is the @emph{longest
+subject} of any e-mail messages we received in the year 2011. You can try
+this if you put the following in a separate file, make it executable, and run
+it like any program.
+
+@lisp
+#!/bin/sh
+exec guile -s $0 $@
+!#
+
+(use-modules (mu))
+(use-modules (srfi srfi-1))
+
+(mu:initialize)
+
+;; note: (subject msg) => #f if there is no subject
+(define list-of-subjects
+ (map (lambda (msg)
+ (or (mu:subject msg) "")) (mu:message-list "date:2011..2011")))
+;; see the mu-find manpage for the date syntax
+
+(define longest-subject
+ (fold (lambda (subj1 subj2)
+ (if (> (string-length subj1) (string-length subj2))
+ subj1 subj2))
+ "" list-of-subjects))
+
+(format #t "Longest subject: ~s\n" longest-subject)
+@end lisp
+
+There are many other ways to solve the same problem, for example by using an
+iterative approach with @code{mu:for-each-message}, but it should show how one
+can easily write little programs to answer specific questions about your
+e-mail corpus.
+
+@node Contacts
+@chapter Contacts
+
+We can retrieve the sender and recipients of an e-mail message using methods
+like @code{mu:from}, @code{mu:to} etc.; @xref{Message methods}. These
+procedures return the list of recipients as a single string; however, often it
+is more useful to deal with recipients as separate objects.
+
+@menu
+* Contact procedures and objects::
+* All contacts::
+* Utility procedures::
+* Example - mutt export::
+@end menu
+
+
+@node Contact procedures and objects
+@section Contact procedures and objects
+
+Message objects (@pxref{Messages}) have a method @t{mu:contacts}:
+
+ @code{(mu:contacts <message-object> [<contact-type>])}
+
+The @t{<contact-type>} is a symbol, one of @code{mu:to}, @code{mu:from},
+@code{mu:cc} or @code{mu:bcc}. This will then get the contact objects for the
+contacts of the corresponding type. If you leave out the contact-type (or
+specify @t{#t} for it, you will get a list of @emph{all} contact objects for
+the message.
+
+A contact object (@code{<mu:contact>}) has two methods:
+@itemize
+@item @code{mu:name} returns the name of the contact, or #f if there is none
+@item @code{mu:email} returns the e-mail address of the contact, or #f if there is none
+@end itemize
+
+Let's get a list of all names and e-mail addresses in the 'To:' field, of
+messages matching 'book':
+
+@lisp
+(use-modules (mu))
+(mu:initialize)
+(mu:for-each-message
+ (lambda (msg)
+ (for-each
+ (lambda (contact)
+ (format #t "~a => ~a\n"
+ (or (mu:email contact) "") (or (mu:name contact) "no-name")))
+ (mu:contacts msg mu:contact:to)))
+ "book")
+@end lisp
+
+This shows what the methods do, but for many uses, it would be more useful to
+have each of the contacts only show up @emph{once} - for that, please refer to
+@xref{All contacts}.
+
+@node All contacts
+@section All contacts
+
+Sometimes you may want to inspect @emph{all} the different contacts in the
+@t{mu} database. This is useful, for instance, when exporting contacts to some
+external format that can then be important in an e-mail program.
+
+To enable this, there is the procedure @code{mu:for-each-contact}, defined as
+
+ @code{(mu:for-each-contact procedure [search-expression])}.
+
+This will aggregate the unique contacts from @emph{all} messages matching
+@t{<search-expression>} (when it is left empty, it will match all messages in
+the database), and execute @t{procedure} for each of them.
+
+The @t{procedure} receives an object of the type @t{<mu:contact-with-stats>},
+which is a @emph{subclass} of the @t{<mu:contact>} class discussed in
+@xref{Contact procedures and objects}. @t{<mu:contact-with-stats>} objects
+expose the following additional methods:
+
+@itemize
+@item @code{(mu:frequency <contact>)}: returns the @emph{number of times} this contact occurred in
+one of the address fields
+@item @code{(mu:last-seen <contact>)}: returns the @emph{most recent time} the contact was
+seen in one of the address fields, as a @t{time_t} value
+@end itemize
+
+The method assumes an e-mail address is unique for a certain contact; if a
+certain e-mail address occurs with different names, it uses the most recent
+non-empty name.
+
+@node Utility procedures
+@section Utility procedures
+
+To make dealing with contacts even easier, there are a number of utility
+procedures that can save you a bit of typing.
+
+For converting contacts to some textual form, there is @code{(mu:contact->string
+<mu:contact> format)}, which takes a contact and returns a text string with
+the given format. Currently supported formats are @t{"org-contact}, @t{"mutt-alias"},
+@t{"mutt-ab"}, @t{"wanderlust"} and @t{"plain"}.
+
+
+@node Example - mutt export
+@section Example - mutt export
+
+Let's see how we could export the addresses in the @t{mu} database to the
+addressbook format of the venerable
+@t{mutt}@footnote{@url{http://www.mutt.org/}} e-mail client.
+
+The addressbook format that @t{mutt} uses is a sequence of lines that look
+something like:
+@verbatim
+alias <nick> [<name>] "<" <email> ">"
+@end verbatim
+
+@t{mu guile} provides the procedure @code{(mu:contact->string <mu:contact>
+format)} that we can use to do the conversion.
+
+We may want to focus on people with whom we have frequent correspondence; so
+we may want to limit ourselves to people we have seen at least 10 times in the
+last year.
+
+It is a bit hard to @emph{guess} the nick name for e-mail contacts, but
+@code{mu:contact->string} tries something based on the name. You can always
+adjust them later by hand, obviously.
+
+@lisp
+#!/bin/sh
+exec guile -s $0 $@
+!#
+
+(use-modules (mu))
+(mu:initialize)
+
+;; Get a list of contacts that were seen at least 20 times since 2010
+(define (selected-contacts)
+ (let ((addrs '())
+ (start (car (mktime (car (strptime "%F" "2010-01-01")))))
+ (minfreq 20))
+ (mu:for-each-contact
+ (lambda (contact)
+ (if (and (mu:email contact)
+ (>= (mu:frequency contact) minfreq)
+ (>= (mu:last-seen contact) start))
+ (set! addrs (cons contact addrs)))))
+ addrs))
+
+(for-each
+ (lambda (contact)
+ (format #t "~a\n" (mu:contact->string contact "mutt-alias")))
+ (selected-contacts))
+@end lisp
+
+This simple program could be improved in many ways; this is left as an
+exercise to the reader.
+
+@node Attachments and other parts
+@chapter Attachments and other parts
+
+To deal with @emph{attachments}, or, more in general @emph{MIME-parts}, there
+is the @t{mu part} module.
+
+@menu
+* Parts and their methods::
+* Attachment example::
+@end menu
+
+@node Parts and their methods
+@section Parts and their methods
+The module defines the @code{<mu-part>} class, and adds two methods to
+@code{<mu:message>} objects:
+@itemize
+@item @code{(mu:parts msg)} - returns a list @code{<mu-part>} objects, one for
+each MIME-parts in the message.
+@item @code{(mu:attachments msg)} - like @code{parts}, but only list those MIME-parts
+that look like proper attachments.
+@end itemize
+
+A @code{<mu:part>} object exposes a few methods to get information about the
+part:
+@itemize
+@item @code{(mu:name <part>)} - returns the file name of the mime-part, or @code{#f} if
+there is none.
+@item @code{(mu:mime-type <part>)} - returns the mime-type of the mime-part, or @code{#f}
+if there is none.
+@item @code{(mu:size <part>)} - returns the size in bytes of the mime-part
+@end itemize
+
+@c Then, we may want to save the part to a file; this can be done using either:
+@c @itemize
+@c @item @code{(mu:save part <part>)} - save a part to a temporary file, return the file
+@c name@footnote{the temporary filename is a predictable procedure of (user-id,
+@c msg-path, part-index)}
+@c @item @code{(mu:save-as <part> <path>)} - save part to file at path
+@c @end itemize
+
+@node Attachment example
+@section Attachment example
+
+Let's look at some small example. Let's get a list of the biggest attachments
+in messages about Luxemburg:
+
+@lisp
+#!/bin/sh
+exec guile -s $0 $@
+!#
+
+(use-modules (mu))
+(mu:initialize)
+
+(define (all-attachments expr)
+ "Return a list of (name . size) for all attachments in messages
+matching EXPR."
+ (let ((pairs '()))
+ (mu:for-each-message
+ (lambda (msg)
+ (for-each
+ (lambda (att) ;; add (filename . size) to the list
+ (set! pairs (cons (cons (mu:name att) (or (mu:size att) 0)) pairs)))
+ (mu:attachments msg)))
+ expr)
+ pairs))
+
+(for-each
+ (lambda (att)
+ (format #t "~a: ~,1fKb\n"
+ (car att) (exact->inexact (/ (cdr att) 1024))))
+ (sort (all-attachments "Luxemburg")
+ (lambda (att1 att2)
+ (< (cdr att1) (cdr att2)))))
+@end lisp
+
+As an exercise for the reader, you might want to re-rewrite the
+@code{all-attachments} in terms of @code{mu:message-list}, which would
+probably be a bit more elegant.
+
+
+@node Statistics
+@chapter Statistics
+
+@t{mu-guile} offers some convenience procedures to determine various statistics
+about the messages in the database.
+
+@menu
+* Basics:: @code{mu:count}, @code{mu:average}, ...
+* Tabulating values:: @code{mu:tabulate}
+* Most frequent values:: @code{mu:top-n-most-frequent}
+@end menu
+
+@node Basics
+@section Basics
+
+Let's look at some of the basic statistical operations available, in an
+interactive session:
+@example
+GNU Guile 2.0.5.123-4bd53
+Copyright (C) 1995-2012 Free Software Foundation, Inc.
+
+Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'.
+This program is free software, and you are welcome to redistribute it
+under certain conditions; type `,show c' for details.
+
+Enter `,help' for help.
+scheme@@(guile-user)> ;; load modules, initialize mu
+scheme@@(guile-user)> (use-modules (mu) (mu stats))
+scheme@@(guile-user)> (mu:initialize)
+scheme@@(guile-user)>
+scheme@@(guile-user)> ;; count the number of messages with 'hello' in their subject
+scheme@@(guile-user)> (mu:count "subject:hello")
+$1 = 162
+scheme@@(guile-user)> ;; average the size of messages with hello in their subject
+scheme@@(guile-user)> (mu:average mu:size "subject:hello")
+$2 = 34597733/81
+scheme@@(guile-user)> (exact->inexact $2)
+$3 = 427132.506172839
+scheme@@(guile-user)> ;; calculate the correlation between message size and
+scheme@@(guile-user)> ;; subject length
+scheme@@(guile-user)> (mu:correl mu:size (lambda (msg)
+ (string-length (mu:subject msg))) "subject:hello")
+$5 = -0.10804368622292
+scheme@@(guile-user)>
+@end example
+
+@node Tabulating values
+@section Tabulating values
+
+@code{(mu:tabulate <procedure> [<search-expr>])} applies @t{<procedure>} to each
+message matching @t{<search-expr>} (leave empty to match @emph{all} messages),
+and returns a associative list (a list of pairs) with each of the different
+results of @t{<procedure>} and their frequencies. For fields that contain lists
+of values (such as address-fields), each of the values in the list is added
+separately.
+
+@subsection Example: messages per weekday
+
+We demonstrate @code{mu:tabulate} with an example. Suppose we want to know how
+many messages we receive per weekday:
+
+@lisp
+#!/bin/sh
+exec guile -s $0 $@
+!#
+
+(use-modules (mu) (mu stats) (mu plot))
+(mu:initialize)
+
+;; create a list like (("Sun" . 13) ("Mon" . 23) ...)
+(define weekday-table
+ (mu:weekday-numbers->names
+ (sort
+ (mu:tabulate
+ (lambda (msg)
+ (tm:wday (localtime (mu:date msg)))))
+ (lambda (a b) (< (car a) (car b))))))
+
+(for-each
+ (lambda (elm)
+ (format #t "~a: ~a\n" (car elm) (cdr elm)))
+ weekday-table)
+@end lisp
+
+
+The procedure @code{weekday-table} uses @code{mu:tabulate-message} to get the
+frequencies per hour -- this returns a list of pairs:
+@verbatim
+((5 . 2339) (0 . 2278) (4 . 2800) (2 . 3184) (6 . 1856) (3 . 2833) (1 . 2993))
+@end verbatim
+
+We sort these pairs by the day number, and then apply
+@code{mu:weekday-numbers->names}, which takes the list, and returns a list
+where the day numbers are replace by there abbreviated name (in the current
+locale). Note, there is also @code{mu:month-numbers->names}.
+
+The script then outputs these numbers in the following form:
+
+@verbatim
+Sun: 2278
+Mon: 2993
+Tue: 3184
+Wed: 2833
+Thu: 2800
+Fri: 2339
+Sat: 1856
+@end verbatim
+
+Clearly, Saturday is a slow day for e-mail...
+
+@node Most frequent values
+@section Most frequent values
+
+In the above example, the number of values is small (the seven weekdays);
+however, in many cases there can be many different values (for example, all
+different message subjects), many of which may not be very interesting -- all
+we need to know is the top-10 of most frequently seen values.
+
+This is fairly easy to achieve using @code{mu:tabulate} -- to get the top-10
+subjects@footnote{this requires the @code{(srfi srfi-1)}-module}, we can use
+something like this:
+@lisp
+(take
+ (sort
+ (mu:tabulate mu:subject)
+ (lambda (a b) (> (cdr a) (cdr b))))
+ 10)
+@end lisp
+
+If this is not short enough, @t{mu-guile} offers a convenience procedure to do
+this: @code{mu:top-n-most-frequent}. For example, to get the top-10 people we
+sent mail to most often:
+
+@lisp
+(mu:top-n-most-frequent mu:to 10 "maildir:/sent")
+@end lisp
+
+Can't make it much easier than that!
+
+
+@node Plotting data
+@chapter Plotting data
+
+You can plot the results in the format produced by @code{mu:tabulate} with the
+@t{(mu plot)} module, an experimental module that requires the
+@t{gnuplot}@footnote{@url{http://www.gnuplot.info/}} program to be installed
+on your system.
+
+The @code{mu:plot-histogram} procedure takes the following arguments:
+
+@code{(mu:plot-histogram <data> <title> <x-label> <y-label> [<want-ascii>])}
+
+Here, @code{<data>} is a table of data in the format that @code{mu:tabulate}
+produces. @code{<title>}, @code{<x-label>} and @code{<y-lablel>} are,
+respectively, the title of the graph, and the labels for X- and
+Y-axis. Finally, if you pass @t{#t} for the final @code{<want-ascii>}
+parameter, a plain-text rendering of the graph will be produced; otherwise, a
+graphical window will be shown.
+
+An example should clarify how this works in practice; let's plot the number of
+message per hour:
+
+@lisp
+#!/bin/sh
+exec guile -s $0 $@
+!#
+
+(use-modules (mu) (mu stats) (mu plot))
+(mu:initialize)
+
+(define (mail-per-hour-table)
+ (sort
+ (mu:tabulate
+ (lambda (msg)
+ (tm:hour (localtime (mu:date msg)))))
+ (lambda (x y) (< (car x) (car y)))))
+
+(mu:plot-histogram (mail-per-hour-table) "Mail per hour" "Hour" "Frequency")
+@end lisp
+
+@cartouche
+@verbatim
+ Mail per hour
+Frequency
+ 1200 ++--+--+--+--+-+--+--+--+--+-+--+--+--+-+--+--+--+--+-+--+--+--+--++
+ |+ + + + + + + "/tmp/fileHz7D2u" using 2:xticlabels(1) ********
+ 1100 ++ *** +*
+ **** * * *
+ 1000 *+ * **** * +*
+ * * ****** **** * ** * *
+ 900 *+ * * ** **** * **** ** * +*
+ * * * ** * * ********* * ** ** * *
+ 800 *+ * **** ** * * * * ** * * ** ** * +*
+ 700 *+ *** **** * ** * * * * ** **** * ** ** * +*
+ * * * **** * * ** * * * * ** * **** ** ** * *
+ 600 *+ * **** * * * * ** * * * * ** * * * ** ** * +*
+ * * ** * * * * * ** * * * * ** * * * ** ** * *
+ 500 *+ * ** * * * * * ** * * * * ** * * * ** ** * +*
+ * * ** **** *** * * * ** * * * * ** * * * ** ** * *
+ 400 *+ * ** ** **** * * * * * ** * * * * ** * * * ** ** * +*
+ *+ *+**+**+* +*******+* +* +*+ *+**+* +*+ *+ *+**+* +*+ *+**+**+* +*
+ 300 ********************************************************************
+ 0 1 2 3 4 5 6 7 8 910 11 12 1314 15 16 17 1819 20 21 22 23
+ Hour
+@end verbatim
+@end cartouche
+
+@node Writing scripts
+@chapter Writing scripts
+
+The @t{mu} program has built-in support for running guile-scripts, and comes
+with a number of examples.
+
+You can get a list of all scripts with the @t{mu script} command:
+@verbatim
+$ mu script
+Available scripts (use --verbose for details):
+ * find-dups: find duplicate messages
+ * msgs-count: count the number of messages matching some query
+ * msgs-per-day: graph the number of messages per day
+ * msgs-per-hour: graph the number of messages per hour
+ * msgs-per-month: graph the number of messages per month
+ * msgs-per-year: graph the number of messages per year
+ * msgs-per-year-month: graph the number of messages per year-month
+@end verbatim
+
+You can then execute such a script by its name:
+@verbatim
+$ mu msgs-per-month --textonly --query=hello
+
+
+ Messages per month matching hello
+
+ 240 ++-+-----+----+-----+-----+-----+----+-----+-----+-----+----+-----+-++
+ | + + + + "/tmp/filewi9H0N" using 2:xticlabels(1) ****** |
+ 220 ++ * * ******
+ | * * * *
+ 200 ++ * * * +*
+ | * * * *
+ 180 ++ ****** * * * +*
+ | * * * * * *
+ 160 ****** * * * * * +*
+ * * * * * * * *
+ * ******* * * * * ****** * *
+ 140 *+ ** * * * * * * ******** +*
+ * ** ******* * * * * * ** ** *
+ 120 *+ ** ** ******* * * * * ** ** +*
+ * ** ** ** * * * ******* ** ** *
+ 100 *+ ** ** ** * * * * ** ** ** +*
+ * + ** + ** + ** + * + * + + * + * + ** + ** + ** + *
+ 80 **********************************************************************
+ Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
+ Month
+@end verbatim
+
+Please refer to the @t{mu-script} man-page for some details on writing your
+own scripts.
+
+
+@node Recipes
+@appendix Recipes
+
+@itemize
+@item Calculating the average length of subject-lines
+@lisp
+;; the average length of all our
+(let ((len 0) (n 0))
+ (mu:for-each-message
+ (lambda (msg)
+ (set! len (+ len (string-length (or (mu:subject msg) ""))))
+ (set! n (+ n 1))))
+ (if (= n 0) 0 (/ len n)))
+ ;; this gives a rational, exact result;
+ ;; use exact->inexact to get decimals
+
+;; we we can make this short with the mu:average (with (mu stats))
+(mu:average (lambda (msg) (string-length (or (mu:subject msg) ""))))
+
+
+@end lisp
+@end itemize
+
+@node GNU Free Documentation License
+@appendix GNU Free Documentation License
+
+@include fdl.texi
+@bye
--- /dev/null
+/* cpp arguments: mu-guile.cc -DHAVE_CONFIG_H -I. -I.. -I../lib -I/usr/local/include/guile/3.0 -pthread -I/usr/include/glib-2.0 -I/usr/lib64/glib-2.0/include -I/usr/include/sysprof-4 -I/usr/include/libmount -I/usr/include/blkid -pthread -fno-strict-aliasing -Wall -Wextra -Wundef -Wwrite-strings -Wpointer-arith -Wmissing-declarations -Wredundant-decls -Wno-unused-parameter -Wno-missing-field-initializers -Wformat=2 -Wcast-align -Wformat-nonliteral -Wformat-security -Wsign-compare -Wstrict-aliasing -Wshadow -Winline -Wpacked -Wmissing-format-attribute -Wmissing-noreturn -Winit-self -Wmissing-include-dirs -Wunused-but-set-variable -Warray-bounds -Wreturn-type -Wno-overloaded-virtual -Wswitch-enum -Wswitch-default -Wno-error=unused-parameter -Wno-error=missing-field-initializers -Wno-error=overloaded-virtual -Wno-redundant-decls -Wno-missing-declarations -Wno-suggest-attribute=noreturn -O2 -Wno-inline */
+scm_c_define_gsubr (s_mu_initialize, 0, 1, 0, (scm_t_subr) mu_initialize); scm_c_export (s_mu_initialize, __null );;
+scm_c_define_gsubr (s_mu_initialized_p, 0, 0, 0, (scm_t_subr) mu_initialized_p); scm_c_export (s_mu_initialized_p, __null );;
+scm_c_define_gsubr (s_log_func, 1, 0, 1, (scm_t_subr) log_func);;
--- /dev/null
+;; Copyright (C) 2011-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; 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, 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, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+(define-module (mu)
+ :use-module (oop goops)
+ :use-module (ice-9 optargs)
+ :use-module (texinfo string-utils)
+ :export
+ ( ;; classes
+ <mu:message>
+ <mu:contact>
+ <mu:part>
+ ;; general
+;; mu:initialize
+ ;; mu:initialized?
+ mu:log-warning
+ mu:log-message
+ mu:log-critical
+ ;; search funcs
+ mu:for-each-message
+ mu:for-each-msg
+ mu:message-list
+ ;; message funcs
+ mu:header
+ ;; message accessors
+ mu:field:bcc
+ mu:field:body-html
+ mu:field:body-txt
+ mu:field:cc
+ mu:field:date
+ mu:field:flags
+ mu:field:from
+ mu:field:maildir
+ mu:field:message-id
+ mu:field:path
+ mu:field:prio
+ mu:field:refs
+ mu:field:size
+ mu:field:subject
+ mu:field:tags
+ mu:field:timestamp
+ mu:field:to
+ ;; contact funcs
+ mu:name
+ mu:email
+ mu:contact->string
+ ;;
+ mu:for-each-contact
+ ;;
+ mu:contacts
+ ;;
+ ;; <mu:contact-with-stats>
+ mu:frequency
+ mu:last-seen
+ ;; parts
+
+ <mu:part>
+ ;; message function
+ mu:attachments
+ mu:parts
+ ;; <mu:part> methods
+ mu:name
+ mu:mime-type
+ ;; size
+ ;; mu:save
+ ;; mu:save-as
+ ))
+
+;; this is needed for guile < 2.0.4
+(setlocale LC_ALL "")
+
+;; load the binary
+(load-extension "libguile-mu" "mu_guile_init")
+(load-extension "libguile-mu" "mu_guile_message_init")
+
+;; define some dummies so we don't get errors during byte compilation
+(eval-when (compile)
+ (define mu:c:get-field)
+ (define mu:c:get-contacts)
+ (define mu:c:for-each-message)
+ (define mu:c:get-header)
+ (define mu:critical)
+ (define mu:c:log)
+ (define mu:message)
+ (define mu:c:log)
+ (define mu:warning)
+ (define mu:c:log)
+ (define mu:c:get-parts))
+
+(define (mu:log-warning frm . args)
+ "Log FRM with ARGS at warning."
+ (mu:c:log mu:warning frm args))
+
+(define (mu:log-message frm . args)
+ "Log FRM with ARGS at warning."
+ (mu:c:log mu:message frm args))
+
+(define (mu:log-critical frm . args)
+ "Log FRM with ARGS at warning."
+ (mu:c:log mu:critical frm args))
+
+(define-class <mu:message> ()
+ (msg #:init-keyword #:msg)) ;; the MuMsg-smob we're wrapping
+
+(define-syntax define-getter
+ (syntax-rules ()
+ ((define-getter method-name field)
+ (begin
+ (define-method (method-name (msg <mu:message>))
+ (mu:c:get-field (slot-ref msg 'msg) field))
+ (export method-name)))))
+
+(define-getter mu:bcc mu:field:bcc)
+(define-getter mu:body-html mu:field:body-html)
+(define-getter mu:body-txt mu:field:body-txt)
+(define-getter mu:cc mu:field:cc)
+(define-getter mu:date mu:field:date)
+(define-getter mu:flags mu:field:flags)
+(define-getter mu:from mu:field:from)
+(define-getter mu:maildir mu:field:maildir)
+(define-getter mu:message-id mu:field:message-id)
+(define-getter mu:path mu:field:path)
+(define-getter mu:priority mu:field:prio)
+(define-getter mu:references mu:field:refs)
+(define-getter mu:size mu:field:size)
+(define-getter mu:subject mu:field:subject)
+(define-getter mu:tags mu:field:tags)
+(define-getter mu:timestamp mu:field:timestamp)
+(define-getter mu:to mu:field:to)
+
+(define-method (mu:header (msg <mu:message>) (hdr <string>))
+ "Get an arbitrary header HDR from message MSG; return #f if it does
+not exist."
+ (mu:c:get-header (slot-ref msg 'msg) hdr))
+
+(define* (mu:for-each-message func #:optional (expr #t) (maxresults -1))
+ "Execute function FUNC for each message that matches mu search expression EXPR.
+If EXPR is not provided, match /all/ messages in the store. MAXRESULTS
+specifies the maximum of messages to return, or -1 (the default) for
+no limit."
+ (mu:c:for-each-message
+ (lambda (msg)
+ (func (make <mu:message> #:msg msg)))
+ expr
+ maxresults))
+
+;; backward-compatibility alias
+(define mu:for-each-msg mu:for-each-message)
+
+(define* (mu:message-list #:optional (expr #t) (maxresults -1))
+ "Return a list of all messages matching mu search expression
+EXPR. If EXPR is not provided, return a list of /all/ messages in the
+store. MAXRESULTS specifies the maximum of messages to return, or
+-1 (the default) for no limit."
+ (let ((lst '()))
+ (mu:for-each-message
+ (lambda (m)
+ (set! lst (append! lst (list m)))) expr maxresults)
+ lst))
+
+;; contacts
+(define-class <mu:contact> ()
+ (name #:init-value #f #:accessor mu:name #:init-keyword #:name)
+ (email #:init-value #f #:accessor mu:email #:init-keyword #:email))
+
+(define-method (mu:contacts (msg <mu:message>) contact-type)
+ "Get all contacts for MSG of the given CONTACT-TYPE. MSG is of type <mu-message>,
+while contact type is either `mu:contact:to', `mu:contact:cc',
+`mu:contact:from' or `mu:contact:bcc' to get the corresponding type of
+contacts, or #t to get all.
+
+Returns a list of <mu-contact> objects."
+ (map (lambda (pair) ;; a pair (na . addr)
+ (make <mu:contact> #:name (car pair) #:email (cdr pair)))
+ (mu:c:get-contacts (slot-ref msg 'msg) contact-type)))
+
+(define-method (mu:contacts (msg <mu:message>))
+ "Get contacts of all types for message MSG as a list of <mu-contact>
+objects."
+ (mu:contacts msg #t))
+
+(define-class <mu:contact-with-stats> (<mu:contact>)
+ (tstamp #:init-value 0 #:accessor mu:timestamp #:init-keyword #:timestamp)
+ (last-seen #:init-value 0 #:accessor mu:last-seen)
+ (freq #:init-value 1 #:accessor mu:frequency))
+
+(define* (mu:for-each-contact proc #:optional (expr #t))
+ "Execute PROC for each contact. PROC receives a <mu-contact> instance
+as parameter. If EXPR is specified, only consider contacts in messages
+matching EXPR."
+ (let ((c-hash (make-hash-table 4096)))
+ (mu:for-each-message
+ (lambda (msg)
+ (for-each
+ (lambda (ct)
+ (let ((ct-ws (make <mu:contact-with-stats>
+ #:name (mu:name ct)
+ #:email (mu:email ct)
+ #:timestamp (mu:date msg))))
+ (update-contacts-hash c-hash ct-ws)))
+ (mu:contacts msg #t)))
+ expr)
+ (hash-for-each ;; c-hash now contains a map of email->contact
+ (lambda (email ct-ws) (proc ct-ws)) c-hash)))
+
+(define-method (update-contacts-hash c-hash (nc <mu:contact-with-stats>))
+ "Update the contacts hash with a new and/or existing contact."
+ ;; xc: existing-contact, nc: new contact
+ (let ((xc (hash-ref c-hash (mu:email nc))))
+ (if (not xc) ;; no existing contact with this email address?
+ (hash-set! c-hash (mu:email nc) nc) ;; store the new contact.
+ ;; otherwise:
+ (begin
+ ;; 1) update the frequency for the existing contact
+ (set! (mu:frequency xc) (1+ (mu:frequency xc)))
+ ;; 2) update the name if the new one is not empty and its timestamp is newer
+ ;; in that case, also update the timestamp
+ (if (and (mu:name nc) (> (string-length (mu:name nc)))
+ (> (mu:timestamp nc) (mu:timestamp xc)))
+ (set! (mu:name xc) (mu:name nc))
+ (set! (mu:timestamp xc) (mu:timestamp nc)))
+ ;; 3) update last-seen with timestamp, if x's timestamp is newer
+ (if (> (mu:timestamp nc) (mu:last-seen xc))
+ (set! (mu:last-seen xc) (mu:timestamp nc)))
+ ;; okay --> now xc has been updated; but it back in the hash
+ (hash-set! c-hash (mu:email xc) xc)))))
+
+(define-method (mu:contact->string (contact <mu:contact>) (form <string>))
+ "Convert a contact to a string in format FORM, which is a string,
+either \"org-contact\", \"mutt-alias\", \"mutt-ab\",
+\"wanderlust\", \"quoted\" \"plain\"."
+ (let* ((name (mu:name contact)) (email (mu:email contact))
+ (nick ;; simplistic nick guessing...
+ (string-map
+ (lambda(kar)
+ (if (char-alphabetic? kar) kar #\_))
+ (string-downcase (or name email)))))
+ (cond
+ ((string= form "plain")
+ (format #f "~a~a~a" (or name "") (if name " " "") email))
+ ((string= form "org-contact")
+ (format #f "* ~s\n:PROPERTIES:\n:EMAIL:~a\n:NICK:~a\n:END:"
+ (or name email) email nick))
+ ((string= form "wanderlust")
+ (format #f "~a ~s ~s"
+ nick (or name email) email))
+ ((string= form "mutt-alias")
+ (format #f "alias ~a ~a <~a>"
+ nick (or name email) email))
+ ((string= form "mutt-ab")
+ (format #f "~a\t~a\t"
+ email (or name "")))
+ ((string= form "quoted")
+ (string-append
+ "\""
+ (escape-special-chars
+ (string-append
+ (if name
+ (format #f "\"~a\" " name)
+ "")
+ (format #f "<~a>" email))
+ "\"" #\\)
+ "\""))
+ (else (error "Unsupported format")))))
+
+;; message parts
+
+
+(define-class <mu:part> ()
+ (msgpath #:init-value #f #:init-keyword #:msgpath)
+ (index #:init-value #f #:init-keyword #:index)
+ (name #:init-value #f #:getter mu:name #:init-keyword #:name)
+ (mime-type #:init-value #f #:getter mu:mime-type #:init-keyword #:mime-type)
+ (size #:init-value 0 #:getter mu:size #:init-keyword #:size))
+
+(define-method (get-parts (msg <mu:message>) (files-only <boolean>))
+ "Get the part for MSG as a list of <mu:part> objects; if FILES-ONLY is #t,
+only get the part with file names."
+ (map (lambda (part)
+ (make <mu:part>
+ #:msgpath (list-ref part 0)
+ #:index (list-ref part 1)
+ #:name (list-ref part 2)
+ #:mime-type (list-ref part 3)
+ #:size (list-ref part 4)))
+ (mu:c:get-parts (slot-ref msg 'msg) files-only)))
+
+(define-method (mu:attachments (msg <mu:message>))
+ "Get the attachments for MSG as a list of <mu:part> objects."
+ (get-parts msg #t))
+
+(define-method (mu:parts (msg <mu:message>))
+ "Get the MIME-parts for MSG as a list of <mu-part> objects."
+ (get-parts msg #f))
+
+;; (define-method (mu:save (part <mu:part>))
+;; "Save PART to a temporary file, and return the file name. If the
+;; part had a filename, the temporary file's file name will be just that;
+;; otherwise a name is made up."
+;; (mu:save-part (slot-ref part 'msgpath) (slot-ref part 'index)))
+
+;; (define-method (mu:save-as (part <mu:part>) (filepath <string>))
+;; "Save message-part PART to file system path PATH."
+;; (copy-file (save part) filepath))
--- /dev/null
+* OUTDATED *
+
+* README
+
+** What is muile?
+
+ `muile' is a little experiment/toy using the equally experimental mu guile
+ bindings, to be found in libmuguile/ in the top-level source directory.
+
+ `guile'[1] is an interpreter/library for the Scheme programming language[2],
+ specifically meant for extending other programs. It is, in fact, the
+ official GNU language for doing so. 'muile' requires guile 2.x to get the full
+ support.
+
+ Older versions may not support e.g. the 'mu-stats.scm' things discussed below.
+
+ The combination of mu + guile is called `muile', and allows you to write
+ little Scheme-programs to query the mu-database, or inspect individual
+ messages. It is still in an experimental stage, but useful already.
+
+** How do I get it?
+
+ The git-version and the future 0.9.7 version of mu will automatically build
+ muile if you have guile. I've been using guile 2.x from git, but installing
+ the 'guile-1.8-dev' package (Ubuntu/Debian) should do the trick. (I only did
+ very minimal testing with guile 1.8 though).
+
+ Then, configure mu. The configure output should tell you about whether guile
+ was found (and where). If it's found, build mu, and toys/muile should be
+ created, as well.
+
+** What can I do with it?
+
+ Go to toys/muile and start muile. You'll end up with a guile-shell where you
+ can type scheme [1], it looks something like this (for guile 2.x):
+
+ ,----
+ | scheme@(guile-user)>
+ `----
+
+ Now, let's load a message (of course, replace with a message on your system):
+
+ ,----
+ | scheme@(guile-user)> (define msg (mu:msg:make-from-file "/home/djcb/Maildir/cur/12131e7b20a2:2,S"))
+ `----
+
+ This defines a variable 'msg', which holds some message on your file
+ system. It's now easy to inspect this message:
+
+ ,----
+ | scheme@(guile-user)> (define msg (mu:msg:make-from-file "/home/djcb/Maildir/cur/12131e7b20a2:2,S"))
+ `----
+
+ Now, we can inspect this message a bit:
+ ,----
+ | scheme@(guile-user)> (mu:msg:subject msg)
+ | $1 = "See me in bikini :-)"
+ | scheme@(guile-user)> (mu:msg:flags msg)
+ | $2 = (mu:attach mu:unread)
+ `----
+
+ and so on. Note, it's probably easiest to explore the various mu: methods
+ using autocompletion; to enable that make sure you have
+
+
+ ,----
+ | (use-modules (ice-9 readline))
+ | (activate-readline)
+ `----
+
+ in your ~/.guile configuration.
+
+** does this tool have some parameters?
+
+ Yes, there is --muhome to set a non-default place for the message database
+ (see the documentation on --muhome in the mu-find manpage).
+
+ And there is --msg=<path> where you specify some particular message file;
+ it will be available as 'mu:current-msg' in the guile (muile) environment. For
+ example:
+
+ ,----
+ | ./muile --msg=~/Maildir/inbox/cur/1311310172_1234:2,S
+ | [...]
+ | scheme@(guile-user)> mu:current-msg
+ | $1 = #<msg /home/djcb/Maildir/inbox/cur/1311310172_1234:2,S>
+ | scheme@(guile-user)> (mu:msg:size mu:current-msg)
+ | $2 = 7206
+ `----
+
+** What about searching messages in the database?
+
+ That's easy, too - it does require a little more scheme knowledge. For
+ searching messages there is the mu:store:for-each function, which takes two
+ arguments; the first argument is a function that will be called for each
+ message found. The optional second argument is the search expression (following
+ 'mu find' syntax); if don't provide the argument, all messages match.
+
+ So how does this work in practice? Let's see I want to see the subject and
+ sender for messages about milk:
+
+ ,----
+ | (mu:store:for-each (lambda(msg) (format #t "~s ~s\n" (mu:msg:from msg) (mu:msg:subject msg))) "milk")
+ `----
+
+ or slightly more readable:
+
+ ,----
+ | (mu:store:for-each
+ | (lambda(msg)
+ | (format #t "~s ~s\n" (mu:msg:from msg) (mu:msg:subject msg)))
+ | "milk")
+ `----
+
+ As you can see, I provide an anonymous ('lambda') function which will be
+ called for each message matching 'milk'. Admittedly, this requires a bit of
+ Scheme-knowledge... but this time is good as any to learn this nice
+ language.
+
+
+** Can I do some statistics on my messages?
+
+ Yes you can. In fact, it's pretty easy. If you load (in the muile/ directory)
+ the file 'mu-stats.scm':
+
+ ,----
+ | (load "mu-stats.scm")
+ `----
+
+ you'll get a bunch of functions (with names starting with 'mu:stats') to make
+ this very easy. Let's see, suppose I want to see how many messages I get per
+ weekday:
+
+ ,----
+ | scheme@(guile-user)> (mu:stats:per-weekday)
+ | $1 = ((0 . 2255) (1 . 2788) (2 . 2868) (3 . 2599) (4 . 2629) (5 . 2287) (6 . 1851))
+ `----
+
+ Note, Sunday=0, Monday=1 and so on. Apparently, I get/send most of e-mail on
+ Tuesdays, and least on Saturday.
+
+ And note that mu:stats:per-weekdays takes an optional search expression
+ argument, to limit the results to messages matching that, e.g., to only
+ consider messages related to emacs during this year:
+
+ ,----
+ | scheme@(guile-user)> (mu:stats:per-weekday "emacs date:2011..now")
+ | $8 = ((0 . 54) (1 . 22) (2 . 46) (3 . 47) (4 . 39) (5 . 54) (6 . 50))
+ `----
+
+ There's also 'mu:stats:per-month', 'mu:stats:per-year', 'mu:stats:per-hour'.
+ I learnt that during 3-4am I sent/receive only about a third of what I sent
+ during 11-12pm.
+
+** What about getting the top-10 people in the To:-field?
+
+ Easy.
+
+ ,----
+ | scheme@(guile-user)> (mu:stats:top-n-to)
+ | $1 = ((("Abc" "myself@example.com") . 4465) (("Def" "somebodyelse@example.com") . 2114)
+ | (and so on)
+ `----
+
+ I've changed the names a bit to protect the innocent, but what the function
+ does is return a list of pairs of
+
+ (<name> <email>) . <frequency>
+
+ descending in order of frequency. Note, 'mu:stats:top-n-to' takes two
+ optional arguments - first the 'n' in top-n (default is 10), and seconds as
+ search expression to limit the messages considered.
+
+ There are also the functions 'mu:stats:top-n-subject' and
+ 'mu:stats:top-n-from' which do the same, mutatis mutandis, and it's quite
+ easy to add your own (see the mu-stats.scm for examples)
+
+** What about showing the results in a table?
+
+ Even easier. Try:
+
+ ,----
+ | (mu:stats:table (mu:stats:top-n-to))
+ `----
+
+ or
+
+ ,----
+ | (mu:stats:table (mu:stats:per-weekday))
+ `----
+
+ You can also export the table:
+
+ ,----
+ | (mu:stats:export (mu:stats:per-weekday))
+ `----
+
+ which will create a temporary file with the results, for further processing
+ in e.g. 'R' or 'gnuplot'.
+
+
+[1] http://www.gnu.org/s/guile/
+[2] http://en.wikipedia.org/wiki/Scheme_(programming_language)
+
+# Local Variables:
+# mode: org; org-startup-folded: nil
+# End:
--- /dev/null
+(define-module (mu contact) :use-module(mu))
+(display "(mu contact) is deprecated, please remove from (use-modules ...)")
+(newline)
+
--- /dev/null
+(define-module (mu message) :use-module (mu))
+(display "(mu message) is deprecated, please remove from (use-modules ...)")
+(newline)
+
--- /dev/null
+(define-module (mu part) :use-module (mu))
+(display "(mu part) is deprecated, please remove from (use-modules ...)")
+(newline)
+
--- /dev/null
+;;
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; 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, 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, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+(define-module (mu plot)
+ :use-module (mu)
+ :use-module (ice-9 popen)
+ :export ( mu:plot ;; alias for mu:plot-histogram
+ mu:plot-histogram
+ ))
+
+(define (export-pairs pairs)
+ "Write a temporary file with the list of PAIRS in table format, and
+return the file name."
+ (let* ((output (mkstemp "/tmp/mu-guile-XXXXXX" "w"))
+ (datafile (port-filename output)))
+ (for-each
+ (lambda(pair)
+ (display (format #f "~a ~a\n" (car pair) (cdr pair)) output))
+ pairs)
+ (close output)
+ datafile))
+
+(define (find-program-in-path prog)
+ "Find exutable program PROG in PATH; return the full path, or #f if
+not found."
+ (let* ((path (parse-path (getenv "PATH")))
+ (progpath (search-path path prog)))
+ (if (not progpath)
+ #f
+ (if (access? progpath X_OK) ;; is
+ progpath
+ #f))))
+
+(define* (mu:plot-histogram data title x-label y-label
+ #:optional (output "dumb") (extra-gnuplot-opts '()))
+ "Plot DATA with TITLE, X-LABEL and X-LABEL using the gnuplot
+program. DATA is a list of cons-pairs (X . Y).
+
+ OUTPUT is a string
+that determines the type of output that gnuplot produces, depending on
+the system. Which options are available depends on the particulars for
+the gnuplot installation, but typical examples would be \"dumb\" for
+text-only display, \"wxterm\" to write to a graphical window, or
+\"png\" to write a PNG-image to stdout.
+
+EXTRA-GNUPLOT-OPTS is a list
+of any additional options for gnuplot."
+ (if (not (find-program-in-path "gnuplot"))
+ (error "cannot find 'gnuplot' in path"))
+ (when (zero? (length data))
+ (error "No data for plotting"))
+ (let* ((datafile (export-pairs data))
+ (gnuplot (open-pipe "gnuplot -p" OPEN_WRITE))
+ (recipe
+ (string-append
+ "reset\n"
+ "set term " (or output "dumb") "\n"
+ "set title \"" title "\"\n"
+ "set xlabel \"" x-label "\"\n"
+ "set ylabel \"" y-label "\"\n"
+ "set boxwidth 0.9\n"
+ (string-join extra-gnuplot-opts "\n")
+ "plot \"" datafile "\" using 2:xticlabels(1) with boxes fs solid title \"\"\n")))
+ (display recipe gnuplot)
+ (close-pipe gnuplot)))
+
+;; backward compatibility
+(define mu:plot mu:plot-histogram)
--- /dev/null
+;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; 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, 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, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+(define-module (mu script)
+ :export (mu:run-stats))
+
+(use-modules (ice-9 getopt-long) (ice-9 optargs) (ice-9 popen) (ice-9 format))
+(use-modules (mu) (mu stats) (mu plot))
+
+(define (help-and-exit)
+ "Show some help."
+ (display
+ (string-append "usage: script [--help] [--textonly] "
+ "[--muhome=<muhome>] [--query=<query>")
+ (newline))
+ (exit 0))
+
+(define (mu:run-stats args func)
+ "Run some statistics function.
+Interpret argument-list ARGS (like command-line
+arguments). Possible arguments are:
+ --help (show some help and exit)
+ --muhome (path to alternative mu home directory)
+ --output (a string describing the output, e.g. \"dumb\", \"png\" \"wxt\")
+ searchexpr (a search query)
+then call FUNC with args SEARCHEXPR and OUTPUT."
+ (setlocale LC_ALL "")
+ (let* ((optionspec '((muhome (value #t))
+ (query (value #t))
+ (output (value #f))
+ (time-unit (value #t)) ;; Ignore.
+ (help (single-char #\h) (value #f))))
+ (options (getopt-long args optionspec))
+ (query (option-ref options 'query #f))
+ (help (option-ref options 'help #f))
+ (output (option-ref options 'output #f))
+ (muhome (option-ref options 'muhome #f))
+ (restargs (option-ref options '() #f)))
+ (if help (help-and-exit))
+ (mu:initialize muhome)
+ (func (or query "") output)))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
--- /dev/null
+;;
+;; Copyright (C) 2011-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; 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, 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, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+(define-module (mu stats)
+ :use-module (oop goops)
+ :use-module (mu)
+ :use-module (srfi srfi-1)
+ :use-module (ice-9 i18n)
+ :use-module (ice-9 r5rs)
+ :export ( mu:tabulate
+ mu:top-n-most-frequent
+ mu:count
+ mu:average
+ mu:stddev
+ mu:correl
+ mu:max
+ mu:min
+ mu:weekday-numbers->names
+ mu:month-numbers->names))
+
+
+(define* (mu:tabulate func #:optional (expr #t))
+ "Execute FUNC for each message matching EXPR, and return an alist
+with maps each result of FUNC to its frequency. If the result of FUNC
+is a list, add each of its values separately.
+ FUNC is a function takes a <mu-message> instance as its argument. For
+example, to tabulate messages by weekday, one could use:
+ (mu:tabulate (lambda(msg) (tm:wday (localtime (date msg))))), and
+get back a list like
+ ((1 . 2) (2 . 5)(3 . 4)(4 . 4)(5 . 12)(6 . 7)(7. 2))."
+ (let* ((table '())
+ ;; func to add a value to our table
+ (update-table
+ (lambda (val)
+ (let ((old-freq (or (assoc-ref table val) 0)))
+ (set! table (assoc-set! table val (1+ old-freq)))))))
+ (mu:for-each-message
+ (lambda(msg)
+ (let ((val (func msg)))
+ (if (list? val)
+ (for-each update-table val)
+ (update-table val))))
+ expr)
+ table))
+
+(define* (top-n func less n #:optional (expr #t))
+ "Take the results of (mu:tabulate FUNC EXPR), sort using LESS (a
+function taking two arguments A and B (cons cells, (VAL . FREQ)), and
+returns #t if A < B, #f otherwise), and then take the first N."
+ (take (sort (mu:tabulate func expr) less) n))
+
+(define* (mu:top-n-most-frequent func n #:optional (expr #t))
+ "Take the results of (mu:tabulate FUNC EXPR), and return the N items
+with the highest frequency."
+ (top-n func (lambda (a b) (> (cdr a) (cdr b))) n expr))
+
+(define* (mu:count #:optional (expr #t))
+ "Count the number of messages matching EXPR. If EXPR is not
+provided, match /all/ messages."
+ (let ((num 0))
+ (mu:for-each-message
+ (lambda (msg) (set! num (1+ num)))
+ expr)
+ num))
+
+(define (average lst)
+ "Calculate the average of a list LST of numbers, or #f if undefined."
+ (if (null? lst)
+ #f
+ (/ (apply + lst) (length lst))))
+
+(define (stddev lst)
+ "Calculate the standard deviation of a list LST of numbers or #f if
+undefined."
+ (let* ((avg (average lst))
+ (sosq (if avg
+ (apply + (map (lambda (x)(* (- x avg) (- x avg))) lst)))))
+ (if sosq
+ (sqrt (/ sosq (length lst))))))
+
+
+(define* (mu:average func #:optional (expr #t))
+ "Get the average value of FUNC applied to all messages matching
+EXPR (or #t for all). Returns #f if undefined."
+ (average (map func (mu:message-list expr))))
+
+(define* (mu:stddev func #:optional (expr #t))
+ "Get the standard deviation the the values of FUNC applied to all
+messages matching EXPR (or #t for all). This is the 'population' stddev,
+not the 'sample' stddev. Returns #f if undefined."
+ (stddev (map func (mu:message-list expr))))
+
+(define* (mu:max func #:optional (expr #t))
+ "Get the maximum value of FUNC applied to all messages matching
+EXPR (or #t for all). Returns #f if undefined."
+ (apply max (map func (mu:message-list expr))))
+
+(define* (mu:min func #:optional (expr #t))
+ "Get the minimum value of FUNC applied to all messages matching
+EXPR (or #t for all). Returns #f if undefined."
+ (apply min (map func (mu:message-list expr))))
+
+
+(define (correl lst)
+ "Calculate Pearson's correlation coefficient for a list LST of cons
+pair, where the car and cdr of the pairs are values from data set 1
+and 2, respectively."
+ (let ((n (length lst))
+ (sx (apply + (map car lst)))
+ (sy (apply + (map cdr lst)))
+ (sxy (apply + (map (lambda (cell) (* (car cell) (cdr cell))) lst)))
+ (sxx (apply + (map (lambda (cell) (* (car cell) (car cell))) lst)))
+ (syy (apply + (map (lambda (cell) (* (cdr cell) (cdr cell))) lst))))
+ (/ (- (* n sxy) (* sx sy))
+ (sqrt (* (- (* n sxx) (* sx sx)) (- (* n syy) (* sy sy)))))))
+
+(define* (mu:correl func1 func2 #:optional (expr #t))
+ "Determine Pearson's correlation coefficient between the value for
+functions FUNC1 and FUNC2 to all messages matching EXPR (or #t for
+all). Returns #f if undefined."
+ (let ((data
+ (map (lambda (msg)
+ (cons (func1 msg) (func2 msg)))
+ (mu:message-list expr))))
+ (if data (correl data) #f)))
+
+
+;; a list of abbreviated, localized day names
+(define day-names
+ (map locale-day-short (iota 7 1)))
+
+(define (mu:weekday-numbers->names table)
+ "Convert a list of pairs with the car denoting a day number (0-6)
+into a list of pairs with the car replaced by the corresponding day
+name (abbreviated) for the current locale."
+ (map
+ (lambda (pair)
+ (cons (list-ref day-names (car pair)) (cdr pair)))
+ table))
+
+;; a list of abbreviated, localized month names
+(define month-names
+ (map locale-month-short (iota 12 1)))
+
+(define (mu:month-numbers->names table)
+ "Convert a list of pairs with the car denoting a month number (0-11)
+into a list of pairs with the car replaced by the corresponding day
+name (abbreviated)."
+ (map
+ (lambda (pair)
+ (cons (list-ref month-names (car pair)) (cdr pair)))
+ table))
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;;
+;; Copyright (C) 2013-2015 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; 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, 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, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+;; INFO: Find duplicate messages
+;; INFO: options:
+;; INFO: --muhome=<muhome>: path to mu home dir
+;; INFO: --delete: delete all but the first one (experimental, be careful!)
+
+(use-modules (mu) (mu script) (mu stats))
+(use-modules (ice-9 getopt-long) (ice-9 optargs)
+ (ice-9 popen) (ice-9 format) (ice-9 rdelim))
+
+(define (md5sum path)
+ (let* ((port (open-pipe* OPEN_READ "md5sum" path))
+ (md5 (read-delimited " " port)))
+ (close-pipe port)
+ md5))
+
+(define (find-dups delete expr)
+ (let ((id-table (make-hash-table 20000)))
+ ;; fill the hash with <msgid-size> => <list of paths>
+ (mu:for-each-message
+ (lambda (msg)
+ (let* ((id (format #f "~a-~d" (mu:message-id msg)
+ (mu:size msg)))
+ (lst (hash-ref id-table id)))
+ (if lst
+ (set! lst (cons (mu:path msg) lst))
+ (set! lst (list (mu:path msg))))
+ (hash-set! id-table id lst)))
+ expr)
+ ;; list all the paths with multiple elements; check the md5sum to
+ ;; make 100%-minus-ε sure they are really the same file.
+ (hash-for-each
+ (lambda (id paths)
+ (if (> (length paths) 1)
+ (let ((hash (make-hash-table 10)))
+ (for-each
+ (lambda (path)
+ (when (file-exists? path)
+ (let* ((md5 (md5sum path)) (lst (hash-ref hash md5)))
+ (if lst
+ (set! lst (cons path lst))
+ (set! lst (list path)))
+ (hash-set! hash md5 lst))))
+ paths)
+ ;; hash now maps the md5sum to the messages...
+ (hash-for-each
+ (lambda (md5 mpaths)
+ (if (> (length mpaths) 1)
+ (begin
+ ;;(format #t "md5sum: ~a:\n" md5)
+ (let ((num 1))
+ (for-each
+ (lambda (path)
+ (if (equal? num 1)
+ (format #t "~a\n" path)
+ (begin
+ (format #t "~a: ~a\n" (if delete "deleting" "dup") path)
+ (if delete (delete-file path))))
+ (set! num (+ 1 num)))
+ mpaths)))))
+ hash))))
+ id-table)))
+
+
+
+(define (main args)
+ "Find duplicate messages and, potentially, delete the dups.
+ Be careful with that!
+Interpret argument-list ARGS (like command-line
+arguments). Possible arguments are:
+ --muhome (path to alternative mu home directory).
+ --delete (delete all but the first one). Run mu index afterwards.
+ --expr (expression to constrain search)."
+ (setlocale LC_ALL "")
+ (let* ((optionspec '( (muhome (value #t))
+ (delete (value #f))
+ (expr (value #t))
+ (help (single-char #\h) (value #f))))
+ (options (getopt-long args optionspec))
+ (help (option-ref options 'help #f))
+ (delete (option-ref options 'delete #f))
+ (expr (option-ref options 'expr #t))
+ (muhome (option-ref options 'muhome #f)))
+ (mu:initialize muhome)
+ (find-dups delete expr)))
+
+
+;; Local Variables:
+;; mode: scheme
+;; End:
+
+
+
+
+
+
+
+
+
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;; Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; 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, or (at your option) any
+;; later version.
+
+;; INFO: Histogram of the number of messages per time-unit
+;; INFO: Options:
+;; INFO: --query=<query>: limit to messages matching query
+;; INFO: --muhome=<muhome>: path to mu home dir
+;; INFO: --time-unit: hour|day|month|year|month-year
+;; INFO: --output: the output format, such as "png", "wxt"
+;; INFO: (depending on the environment)
+
+(use-modules (mu) (mu stats) (mu plot)
+ (ice-9 getopt-long) (ice-9 format))
+
+(define (per-hour expr output)
+ "Count the total number of messages per hour that match EXPR.
+OUTPUT corresponds to the output format, as per gnuplot's 'set terminal'."
+ (mu:plot-histogram
+ (sort
+ (mu:tabulate
+ (lambda (msg)
+ (tm:hour (localtime (mu:date msg)))) expr)
+ (lambda (x y) (< (car x) (car y))))
+ (format #f "Messages per hour matching ~a" expr)
+ "Hour" "Messages" output))
+
+
+(define (per-day expr output)
+ "Count the total number of messages for each weekday (0-6 for
+Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as
+per gnuplot's 'set terminal'."
+ (mu:plot-histogram
+ (mu:weekday-numbers->names
+ (sort (mu:tabulate
+ (lambda (msg)
+ (tm:wday (localtime (mu:date msg)))) expr)
+ (lambda (x y) (< (car x) (car y)))))
+ (format #f "Messages per weekday matching ~a" expr)
+ "Day" "Messages" output))
+
+(define (per-month expr output)
+ "Count the total number of messages per month that match EXPR.
+OUTPUT corresponds to the output format, as per gnuplot's 'set terminal'."
+ (mu:plot-histogram
+ (mu:month-numbers->names
+ (sort
+ (mu:tabulate
+ (lambda (msg)
+ (tm:mon (localtime (mu:date msg)))) expr)
+ (lambda (x y) (< (car x) (car y)))))
+ (format #f "Messages per month matching ~a" expr)
+ "Month" "Messages" output))
+
+(define (per-year expr output)
+ "Count the total number of messages per year that match EXPR. OUTPUT corresponds
+to the output format, as per gnuplot's 'set terminal'."
+ (mu:plot-histogram
+ (sort (mu:tabulate
+ (lambda (msg)
+ (+ 1900 (tm:year (localtime (mu:date msg))))) expr)
+ (lambda (x y) (< (car x) (car y))))
+ (format #f "Messages per year matching ~a" expr)
+ "Year" "Messages" output))
+
+
+(define (per-year-month expr output)
+ "Count the total number of messages for each year and month that match EXPR.
+OUTPUT corresponds to the output format, as per gnuplot's 'set terminal'."
+ (mu:plot-histogram
+ (sort (mu:tabulate
+ (lambda (msg)
+ (string->number
+ (format #f "~d~2'0d"
+ (+ 1900 (tm:year (localtime (mu:date msg))))
+ (tm:mon (localtime (mu:date msg))))))
+ expr)
+ (lambda (x y) (< (car x) (car y))))
+ (format #f "Messages per year/month matching ~a" expr)
+ "Year/Month" "Messages" output))
+
+(define (main args)
+ (let* ((optionspec
+ '((time-unit (value #t))
+ (query (value #t))
+ (muhome (value #t))
+ (output (value #t))
+ (help (single-char #\h) (value #f))))
+ (options (getopt-long args optionspec))
+ (help (option-ref options 'help #f))
+ (time-unit (option-ref options 'time-unit "year"))
+ (muhome (option-ref options 'muhome #f))
+ (query (option-ref options 'query ""))
+ (output (option-ref options 'output "dumb"))
+ (rest (option-ref options '() #f))
+ (func
+ (cond
+ ((equal? time-unit "hour") per-hour)
+ ((equal? time-unit "day") per-day)
+ ((equal? time-unit "month") per-month)
+ ((equal? time-unit "year") per-year)
+ ((equal? time-unit "year-month") per-year-month)
+ (else #f))))
+ (setlocale LC_ALL "")
+ (unless func
+ (display "error: unknown time-unit\n")
+ (set! help #t))
+ (if help
+ (begin
+ (display
+ (string-append "parameters: [--help] [--output=dumb|png|wxt] "
+ "[--muhome=<muhome>] [--query=<query>]"
+ "[--time-unit=hour|day|month|year|year-month]"))
+ (newline))
+ (begin
+ (mu:initialize muhome)
+ (func query output)))))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+;; Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; 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, 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, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+
+;; INFO: Count the number of messages matching some query
+;; INFO: options:
+;; INFO: --query=<query>: limit to messages matching query
+;; INFO: --muhome=<muhome>: path to mu home dir (optional)
+
+(use-modules (mu) (mu script) (mu stats))
+
+(define (count expr output)
+ "Print the total number of messages matching the query EXPR.
+OUTPUT is ignored."
+ (display (mu:count expr))
+ (newline))
+
+(define (main args)
+ (mu:run-stats args count))
+
+;; Local Variables:
+;; mode: scheme
+;; End:
--- /dev/null
+## Copyright (C) 2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## 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, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+#
+# guile test; they don't work with ASAN.
+#
+if get_option('b_sanitize') == 'none'
+ guile_load_path = join_paths(meson.project_source_root(), 'guile')
+ guile_extensions_path = ':'.join([
+ join_paths(meson.project_build_root(), 'guile'),
+ meson.current_build_dir()])
+
+ test('test-mu-guile',
+ executable('test-mu-guile',
+ 'test-mu-guile.cc',
+ install: false,
+ cpp_args: [
+ '-DABS_SRCDIR="' + meson.current_source_dir() + '"',
+ '-DGUILE_LOAD_PATH="' + guile_load_path + '"',
+ '-DGUILE_EXTENSIONS_PATH="' + guile_extensions_path + '"'
+ ],
+ dependencies: [glib_dep, lib_mu_dep]))
+else
+ message('sanitizer build; skip guile test')
+endif
--- /dev/null
+/*
+** Copyright (C) 2012-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <lib/mu-query.hh>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+
+#include "utils/mu-test-utils.hh"
+#include <lib/mu-store.hh>
+#include <utils/mu-utils.hh>
+
+using namespace Mu;
+
+static std::string test_dir;
+
+static std::string
+fill_database(void)
+{
+ const auto cmdline = mu_format(
+ "/bin/sh -c '"
+ "{} init --muhome={} --maildir={} --quiet; "
+ "{} index --muhome={} --quiet'",
+ MU_PROGRAM,
+ test_dir,
+ MU_TESTMAILDIR2,
+ MU_PROGRAM,
+ test_dir);
+
+ if (g_test_verbose())
+ mu_println("{}", cmdline);
+
+ GError *err{};
+ if (!g_spawn_command_line_sync(cmdline.c_str(), NULL, NULL, NULL, &err)) {
+ mu_printerrln("Error: {}", err ? err->message : "?");
+ g_clear_error(&err);
+ g_assert(0);
+ }
+
+ return test_dir;
+}
+
+static void
+test_something(const char* what)
+{
+ g_setenv("GUILE_AUTO_COMPILE", "0", TRUE);
+ g_setenv("GUILE_LOAD_PATH", GUILE_LOAD_PATH, TRUE);
+ g_setenv("GUILE_EXTENSIONS_PATH",GUILE_EXTENSIONS_PATH, TRUE);
+
+ if (g_test_verbose())
+ g_print("GUILE_LOAD_PATH: %s\n", GUILE_LOAD_PATH);
+
+ const auto dir = fill_database();
+ const auto cmdline = mu_format("{} -q -e main {}/test-mu-guile.scm "
+ "--muhome={} --test={}",
+ GUILE_BINARY, ABS_SRCDIR,
+ dir, what);
+
+ if (g_test_verbose())
+ mu_println("cmdline: {}", cmdline);
+
+ GError *err{};
+ int status{};
+ if (!g_spawn_command_line_sync(cmdline.c_str(), NULL, NULL, &status, &err) ||
+ status != 0) {
+ mu_printerrln("Error: {}", err ? err->message : "something went wrong");
+ g_clear_error(&err);
+ g_assert(0);
+ }
+}
+
+static void
+test_mu_guile_queries(void)
+{
+ test_something("queries");
+}
+
+static void
+test_mu_guile_messages(void)
+{
+ test_something("message");
+}
+
+static void
+test_mu_guile_stats(void)
+{
+ test_something("stats");
+}
+
+int
+main(int argc, char* argv[])
+{
+ int rv;
+ TempDir tempdir;
+ test_dir = tempdir.path();
+
+ mu_test_init(&argc, &argv);
+
+ if (!set_en_us_utf8_locale())
+ return 0; /* don't error out... */
+
+ g_test_add_func("/guile/queries", test_mu_guile_queries);
+ g_test_add_func("/guile/message", test_mu_guile_messages);
+ g_test_add_func("/guile/stats", test_mu_guile_stats);
+
+ rv = g_test_run();
+
+ return rv;
+}
--- /dev/null
+#!/bin/sh
+exec guile -e main -s $0 $@
+!#
+
+;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;;
+;; 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, 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, write to the Free Software Foundation,
+;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+(setlocale LC_ALL "")
+
+(use-modules (srfi srfi-1))
+(use-modules (ice-9 getopt-long) (ice-9 optargs) (ice-9 popen) (ice-9 format))
+(use-modules (mu) (mu stats))
+
+(define (n-results-or-exit query n)
+ "Run QUERY, and exit 1 if the number of results != N."
+ (let ((lst (mu:message-list query)))
+ (if (not (= (length lst) n))
+ (begin
+ (simple-format (current-error-port) "Query: \"~A\"; expected ~A, got ~A\n"
+ query n (length lst))
+ (exit 1)))))
+
+(define (test-queries)
+ "Test a bunch of queries (or die trying)."
+ (n-results-or-exit "hello" 1)
+ (n-results-or-exit "f:john fruit" 1)
+ (n-results-or-exit "f:soc@example.com" 1)
+ (n-results-or-exit "t:alki@example.com" 1)
+ (n-results-or-exit "t:alcibiades" 1)
+ (n-results-or-exit "f:soc@example.com OR f:john" 2)
+ (n-results-or-exit "f:soc@example.com OR f:john OR t:edmond" 3)
+ (n-results-or-exit "t:julius" 1)
+ (n-results-or-exit "s:dude" 1)
+ (n-results-or-exit "t:dantès" 1)
+ (n-results-or-exit "file:sittingbull.jpg" 1)
+ (n-results-or-exit "file:custer.jpg" 1)
+ (n-results-or-exit "file:custer.*" 1)
+ (n-results-or-exit "j:sit*" 1)
+ (n-results-or-exit "mime:image/jpeg" 1)
+ (n-results-or-exit "mime:text/plain" 14)
+ (n-results-or-exit "y:text*" 14)
+ (n-results-or-exit "y:image*" 1)
+ (n-results-or-exit "mime:message/rfc822" 2))
+
+(define (error-exit msg . args)
+ "Print error and exit."
+ (let ((msg (apply format #f msg args)))
+ (simple-format (current-error-port) "*ERROR*: ~A\n" msg)
+ (exit 1)))
+
+(define (str-equal-or-exit got exp)
+ "S1 == S2 or exit 1."
+ ;; (format #t "'~A' <=> '~A'\n" s1 s2)
+ (if (not (string= exp got))
+ (error-exit "Expected \"~A\", got \"~A\"\n" exp got)))
+
+(define (test-message)
+ "Test functions for a particular message."
+
+ (let ((msg (car (mu:message-list "hello"))))
+ (str-equal-or-exit (mu:subject msg) "Fwd: rfc822")
+ (str-equal-or-exit (mu:to msg) "martin")
+ (str-equal-or-exit (mu:from msg) "foobar <foo@example.com>")
+ (str-equal-or-exit (mu:header msg "X-Mailer") "Ximian Evolution 1.4.5")
+
+ (if (not (equal? (mu:priority msg) mu:prio:normal))
+ (error-exit "Expected ~A, got ~A" (mu:priority msg) mu:prio:normal)))
+
+ (let ((msg (car (mu:message-list "atoms"))))
+ (str-equal-or-exit (mu:subject msg) "atoms")
+ (str-equal-or-exit (mu:to msg) "Democritus <demo@example.com>")
+ (str-equal-or-exit (mu:from msg) "Richard P. Feynman <rpf@example.com>")
+ ;;(str-equal-or-exit (mu:header msg "Content-transfer-encoding") "7BIT")
+
+ (if (not (equal? (mu:priority msg) mu:prio:high))
+ (error-exit "Expected ~a, got ~a" (mu:priority msg) mu:prio:high))))
+
+(define (num-equal-or-exit got exp)
+ "S1 == S2 or exit 1."
+ ;; (format #t "'~A' <=> '~A'\n" s1 s2)
+ (if (not (= exp got))
+ (error-exit "Expected \"~S\", got \"~S\"\n" exp got)))
+
+(define (test-stats)
+ "Test statistical functions."
+ ;; average
+ (num-equal-or-exit (mu:average mu:size) 82601/14)
+ (num-equal-or-exit (floor (mu:stddev mu:size)) 12637.0)
+ (num-equal-or-exit (mu:max mu:size) 46308)
+ (num-equal-or-exit (mu:min mu:size) 111))
+
+(define (main args)
+ (let* ((optionspec '((muhome (value #t))
+ (test (value #t))))
+ (options (getopt-long args optionspec))
+ (muhome (option-ref options 'muhome #f))
+ (test (option-ref options 'test #f)))
+
+ (mu:initialize muhome)
+
+ (if test
+ (cond
+ ((string= test "queries") (test-queries))
+ ((string= test "message") (test-message))
+ ((string= test "stats") (test-stats))
+ (#t (exit 1))))))
+
+
+;; Local Variables:
+;; mode: scheme
+;; End:
--- /dev/null
+## Copyright (C) 2021-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## 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, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+
+subdir('utils')
+subdir('message')
+
+lib_mu=static_library(
+ 'mu',
+ [
+ # db
+ 'mu-config.cc',
+ 'mu-contacts-cache.cc',
+ 'mu-store.cc',
+ 'mu-xapian-db.cc',
+ # querying
+ 'mu-query-macros.cc',
+ 'mu-query-match-deciders.cc',
+ 'mu-query-parser.cc',
+ 'mu-query-processor.cc',
+ 'mu-query-threads.cc',
+ 'mu-query-xapianizer.cc',
+ 'mu-query.cc',
+ # indexing
+ 'mu-indexer.cc',
+ 'mu-scanner.cc',
+ # mu4e
+ 'mu-server.cc',
+ # misc
+ 'mu-maildir.cc',
+ 'mu-script.cc',
+ ],
+ dependencies: [
+ glib_dep,
+ gio_dep,
+ gmime_dep,
+ xapian_dep,
+ guile_dep,
+ config_h_dep,
+ lib_mu_utils_dep,
+ lib_mu_message_dep],
+ install: false)
+
+lib_mu_dep = declare_dependency(
+ link_with: lib_mu,
+ dependencies: [ lib_mu_message_dep, thread_dep ],
+ include_directories:
+ include_directories(['.', '..']))
+
+#
+# dev helpers
+#
+
+process_query = executable('process-query', [ 'mu-query-processor.cc'],
+ install: false,
+ cpp_args: ['-DBUILD_PROCESS_QUERY'],
+ dependencies: [glib_dep, lib_mu_dep])
+
+parse_query = executable( 'parse-query', [ 'mu-query-parser.cc' ],
+ install: false,
+ cpp_args: ['-DBUILD_PARSE_QUERY'],
+ dependencies: [glib_dep, lib_mu_dep])
+
+parse_query_expand = executable( 'parse-query-expand', [ 'mu-query-parser.cc' ],
+ install: false,
+ cpp_args: ['-DBUILD_PARSE_QUERY_EXPAND'],
+ dependencies: [glib_dep, lib_mu_dep])
+
+xapian_query = executable('xapianize-query', [ 'mu-query-xapianizer.cc' ],
+ install: false,
+ cpp_args: ['-DBUILD_XAPIANIZE_QUERY'],
+ dependencies: [glib_dep, lib_mu_dep])
+
+list_maildirs = executable('list-maildirs', 'mu-scanner.cc',
+ install: false,
+ cpp_args: ['-DBUILD_LIST_MAILDIRS'],
+ dependencies: [glib_dep, config_h_dep,
+ lib_mu_utils_dep])
+
+if not get_option('tests').disabled()
+ subdir('tests')
+endif
--- /dev/null
+## Copyright (C) 2022-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## 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, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+
+lib_mu_message=static_library(
+ 'mu-message',
+ [
+ 'mu-message.cc',
+ 'mu-message-file.cc',
+ 'mu-message-part.cc',
+ 'mu-contact.cc',
+ 'mu-document.cc',
+ 'mu-fields.cc',
+ 'mu-flags.cc',
+ 'mu-priority.cc',
+ 'mu-mime-object.cc',
+ ],
+ dependencies: [
+ glib_dep,
+ gmime_dep,
+ xapian_dep,
+ config_h_dep,
+ lib_mu_utils_dep],
+ install: false)
+
+lib_mu_message_dep = declare_dependency(
+ link_with: lib_mu_message,
+ dependencies: [ xapian_dep, gmime_dep, lib_mu_utils_dep, config_h_dep ],
+ include_directories:
+ include_directories(['.', '..']))
+
+if not get_option('tests').disabled()
+ subdir('tests')
+endif
--- /dev/null
+/*
+** Copyright (C) 2022-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-contact.hh"
+#include "mu-message.hh"
+#include "utils/mu-utils.hh"
+#include "mu-mime-object.hh"
+
+#include <gmime/gmime.h>
+#include <glib.h>
+#include <string>
+
+using namespace Mu;
+
+std::string
+Contact::display_name() const
+{
+ const auto needs_quoting= [](const std::string& n) {
+ for (auto& c: n)
+ if (c == ',' || c == '"' || c == '@')
+ return true;
+ return false;
+ };
+
+ if (name.empty())
+ return email;
+ else if (!needs_quoting(name))
+ return name + " <" + email + '>';
+ else
+ return Mu::quote(name) + " <" + email + '>';
+}
+
+std::string
+Mu::to_string(const Mu::Contacts& contacts)
+{
+ std::string res;
+
+ seq_for_each(contacts, [&](auto&& contact) {
+ if (res.empty())
+ res = contact.display_name();
+ else
+ res += ", " + contact.display_name();
+ });
+
+ return res;
+}
+
+size_t
+Mu::lowercase_hash(const std::string& s)
+{
+ std::size_t djb = 5381; // djb hash
+ for (const auto c : s)
+ djb = ((djb << 5) + djb) +
+ static_cast<size_t>(g_ascii_tolower(c));
+ return djb;
+}
+
+#ifdef BUILD_TESTS
+/*
+ * Tests.
+ *
+ */
+
+#include "utils/mu-test-utils.hh"
+
+static void
+test_ctor_foo()
+{
+ Contact c{
+ "foo@example.com",
+ "Foo Bar",
+ Contact::Type::Bcc,
+ 1645214647
+ };
+
+ assert_equal(c.email, "foo@example.com");
+ assert_equal(c.name, "Foo Bar");
+ g_assert_true(*c.field_id() == Field::Id::Bcc);
+ g_assert_cmpuint(c.message_date,==,1645214647);
+
+ assert_equal(c.display_name(), "Foo Bar <foo@example.com>");
+}
+
+
+static void
+test_ctor_blinky()
+{
+ Contact c{
+ "bar@example.com",
+ "Blinky",
+ 1645215014,
+ true, /* personal */
+ 13, /*freq*/
+ 12345 /* tstamp */
+ };
+
+ assert_equal(c.email, "bar@example.com");
+ assert_equal(c.name, "Blinky");
+ g_assert_true(c.personal);
+ g_assert_cmpuint(c.frequency,==,13);
+ g_assert_cmpuint(c.tstamp,==,12345);
+ g_assert_cmpuint(c.message_date,==,1645215014);
+
+ assert_equal(c.display_name(), "Blinky <bar@example.com>");
+}
+
+static void
+test_ctor_cleanup()
+{
+ Contact c{
+ "bar@example.com",
+ "Bli\nky",
+ 1645215014,
+ true, /* personal */
+ 13, /*freq*/
+ 12345 /* tstamp */
+ };
+
+ assert_equal(c.email, "bar@example.com");
+ assert_equal(c.name, "Bli ky");
+ g_assert_true(c.personal);
+ g_assert_cmpuint(c.frequency,==,13);
+ g_assert_cmpuint(c.tstamp,==,12345);
+ g_assert_cmpuint(c.message_date,==,1645215014);
+
+ assert_equal(c.display_name(), "Bli ky <bar@example.com>");
+}
+
+static void
+test_encode()
+{
+ Contact c{
+ "cassius@example.com",
+ "Ali, Muhammad \"The Greatest\"",
+ 345,
+ false, /* personal */
+ 333, /*freq*/
+ 768 /* tstamp */
+ };
+
+ assert_equal(c.email, "cassius@example.com");
+ assert_equal(c.name, "Ali, Muhammad \"The Greatest\"");
+ g_assert_false(c.personal);
+ g_assert_cmpuint(c.frequency,==,333);
+ g_assert_cmpuint(c.tstamp,==,768);
+ g_assert_cmpuint(c.message_date,==,345);
+
+ assert_equal(c.display_name(),
+ "\"Ali, Muhammad \\\"The Greatest\\\"\" <cassius@example.com>");
+}
+
+
+static void
+test_sender()
+{
+ Contact c{"aa@example.com", "Anders Ångström",
+ Contact::Type::Sender, 54321};
+
+ assert_equal(c.email, "aa@example.com");
+ assert_equal(c.name, "Anders Ångström");
+ g_assert_false(c.personal);
+ g_assert_cmpuint(c.frequency,==,1);
+ g_assert_cmpuint(c.message_date,==,54321);
+
+ g_assert_false(!!c.field_id());
+}
+
+
+static void
+test_misc()
+{
+ g_assert_false(!!contact_type_from_field_id(Field::Id::Subject));
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+ g_mime_init();
+
+ g_test_add_func("/message/contact/ctor-foo", test_ctor_foo);
+ g_test_add_func("/message/contact/ctor-blinky", test_ctor_blinky);
+ g_test_add_func("/message/contact/ctor-cleanup", test_ctor_cleanup);
+ g_test_add_func("/message/contact/encode", test_encode);
+
+ g_test_add_func("/message/contact/sender", test_sender);
+ g_test_add_func("/message/contact/misc", test_misc);
+
+ return g_test_run();
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_MESSAGE_CONTACT_HH__
+#define MU_MESSAGE_CONTACT_HH__
+
+#include <functional>
+#include <string>
+#include <vector>
+#include <functional>
+#include <cctype>
+#include <cstring>
+#include <cstdlib>
+#include <ctime>
+
+#include <utils/mu-option.hh>
+#include "mu-fields.hh"
+
+struct _InternetAddressList;
+
+namespace Mu {
+
+/**
+ * Get the hash value for a lowercase value of s; useful for email-addresses
+ *
+ * @param s a string
+ *
+ * @return a hash value.
+ */
+size_t lowercase_hash(const std::string& s);
+
+struct Contact {
+ enum struct Type {
+ None, Sender, From, ReplyTo, To, Cc, Bcc
+ };
+
+ /**
+ * Construct a new Contact
+ *
+ * @param email_ email address
+ * @param name_ name or empty
+ * @param type_ contact field type
+ * @param message_date_ data for the message for this contact
+ */
+ Contact(const std::string& email_, const std::string& name_ = "",
+ Type type_ = Type::None, ::time_t message_date_ = 0)
+ : email{email_}, name{name_}, type{type_},
+ message_date{message_date_}, personal{}, frequency{1}, tstamp{}
+ { cleanup_name(); }
+
+ /**
+ * Construct a new Contact
+ *
+ * @param email_ email address
+ * @param name_ name or empty
+ * @param message_date_ date of message this contact originate from
+ * @param personal_ is this a personal contact?
+ * @param freq_ how often was this contact seen?
+ * @param tstamp_ timestamp for last change
+ */
+ Contact(const std::string& email_, const std::string& name_,
+ time_t message_date_, bool personal_, size_t freq_,
+ int64_t tstamp_)
+ : email{email_}, name{name_}, type{Type::None},
+ message_date{message_date_}, personal{personal_}, frequency{freq_},
+ tstamp{tstamp_}
+ { cleanup_name();}
+
+ /**
+ * Get the "display name" for this contact:
+ *
+ * If there's a non-empty name, it's Jane Doe <email@example.com>
+ * otherwise it's just the e-mail address. Names with commas are quoted
+ * (with the quotes escaped).
+ *
+ * @return the display name
+ */
+ std::string display_name() const;
+
+
+ /**
+ * Does the contact contain a valid email address as per
+ * https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
+ * ?
+ *
+ * @return true or false
+ */
+ bool has_valid_email() const;
+
+ /**
+ * Operator==; based on the hash values (ie. lowercase e-mail address)
+ *
+ * @param rhs some other Contact
+ *
+ * @return true orf false.
+ */
+ bool operator== (const Contact& rhs) const noexcept {
+ return hash() == rhs.hash();
+ }
+
+ /**
+ * Get a hash-value for this contact, which gets lazily calculated. This
+ * * is for use with container classes. This uses the _lowercase_ email
+ * address.
+ *
+ * @return the hash
+ */
+ size_t hash() const {
+ static size_t cached_hash;
+ if (cached_hash == 0) {
+ cached_hash = lowercase_hash(email);
+ }
+ return cached_hash;
+ }
+
+ /**
+ * Get the corresponding Field::Id (if any)
+ * for this contact.
+ *
+ * @return the field-id or Nothing.
+ */
+ constexpr Option<Field::Id> field_id() const noexcept {
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wswitch-enum"
+ switch(type) {
+ case Type::Bcc:
+ return Field::Id::Bcc;
+ case Type::Cc:
+ return Field::Id::Cc;
+ case Type::From:
+ return Field::Id::From;
+ case Type::To:
+ return Field::Id::To;
+ default:
+ return Nothing;
+ }
+#pragma GCC diagnostic pop
+ }
+
+
+ /*
+ * data members
+ */
+
+ std::string email; /**< Email address for this contact.Not empty */
+ std::string name; /**< Name for this contact; can be empty. */
+ Type type; /**< Type of contact */
+ int64_t message_date; /**< Date of the contact's message */
+ bool personal; /**< A personal message? */
+ size_t frequency; /**< Frequency of this contact */
+ int64_t tstamp; /**< Timestamp for this contact (internal use) */
+
+private:
+ void cleanup_name() { // replace control characters by spaces.
+ for (auto& c: name)
+ if (iscntrl(c))
+ c = ' ';
+ }
+};
+
+constexpr Option<Contact::Type>
+contact_type_from_field_id(Field::Id id) noexcept {
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wswitch-enum"
+ switch(id) {
+ case Field::Id::Bcc:
+ return Contact::Type::Bcc;
+ case Field::Id::Cc:
+ return Contact::Type::Cc;
+ case Field::Id::From:
+ return Contact::Type::From;
+ case Field::Id::To:
+ return Contact::Type::To;
+ default:
+ return Nothing;
+ }
+#pragma GCC diagnostic pop
+}
+
+using Contacts = std::vector<Contact>;
+
+/**
+ * Get contacts as a comma-separated list.
+ *
+ * @param contacts contacs
+ *
+ * @return string with contacts.
+ */
+std::string to_string(const Contacts& contacts);
+
+} // namespace Mu
+
+/**
+ * Implement our hash int std::
+ */
+template<> struct std::hash<Mu::Contact> {
+ std::size_t operator()(const Mu::Contact& c) const noexcept {
+ return c.hash();
+ }
+};
+
+#endif /* MU_CONTACT_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2022-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include "config.h"
+
+#include "mu-document.hh"
+#include "mu-message.hh"
+#include "utils/mu-sexp.hh"
+
+#include <cstdint>
+#include <glib.h>
+#include <numeric>
+#include <algorithm>
+#include <charconv>
+#include <cinttypes>
+
+#include <string>
+#include <utils/mu-utils.hh>
+
+using namespace Mu;
+
+// backward compat
+#ifndef HAVE_XAPIAN_FLAG_NGRAMS
+#define FLAG_NGRAMS FLAG_CJK_NGRAM
+#endif /*HAVE_XAPIAN_FLAG_NGRAMS*/
+
+
+const Xapian::Document&
+Document::xapian_document() const
+{
+ if (dirty_sexp_) {
+ xdoc_.set_data(sexp().to_string());
+ dirty_sexp_ = false;
+ }
+ return xdoc_;
+}
+
+template<typename SexpType> void
+Document::put_prop(const std::string& pname, SexpType&& val)
+{
+ cached_sexp().put_props(pname, std::forward<SexpType>(val));
+ dirty_sexp_ = true;
+}
+
+template<typename SexpType> void
+Document::put_prop(const Field& field, SexpType&& val)
+{
+ put_prop(std::string(":") + std::string{field.name},
+ std::forward<SexpType>(val));
+}
+
+static Xapian::TermGenerator
+make_term_generator(Xapian::Document& doc, Document::Options opts)
+{
+ Xapian::TermGenerator termgen;
+
+ if (any_of(opts & Document::Options::SupportNgrams))
+ termgen.set_flags(Xapian::TermGenerator::FLAG_NGRAMS);
+
+ termgen.set_document(doc);
+
+ return termgen;
+}
+
+static void
+add_search_term(Xapian::Document& doc, const Field& field, const std::string& val,
+ Document::Options opts)
+{
+ if (field.is_normal_term() || field.is_phrasable_term()) {
+ const auto flat{utf8_flatten(val)};
+ if (field.is_normal_term())
+ doc.add_term(field.xapian_term(flat));
+ if (field.is_phrasable_term()) {
+ auto termgen{make_term_generator(doc, opts)};
+ termgen.index_text(flat, 1, field.xapian_term());
+ }
+ } else if (field.is_boolean_term()) {
+ doc.add_boolean_term(field.xapian_term(val));
+ } else
+ throw std::logic_error("not a search term");
+}
+
+void
+Document::add(Field::Id id, const std::string& val)
+{
+ const auto field{field_from_id(id)};
+
+ if (field.is_value())
+ xdoc_.add_value(field.value_no(), val);
+
+ if (field.is_searchable())
+ add_search_term(xdoc_, field, val, options_);
+
+ if (field.include_in_sexp())
+ put_prop(field, val);
+}
+
+void
+Document::add(Field::Id id, const std::vector<std::string>& vals)
+{
+ if (vals.empty())
+ return;
+
+ const auto field{field_from_id(id)};
+ if (field.is_value())
+ xdoc_.add_value(field.value_no(), Mu::join(vals, SepaChar1));
+
+ if (field.is_searchable())
+ std::for_each(vals.begin(), vals.end(),
+ [&](const auto& val) {
+ add_search_term(xdoc_, field, val, options_); });
+
+ if (field.include_in_sexp()) {
+ Sexp elms{};
+ for(auto&& val: vals)
+ elms.add(val);
+ put_prop(field, std::move(elms));
+ }
+}
+
+
+std::vector<std::string>
+Document::string_vec_value(Field::Id field_id) const noexcept
+{
+ return Mu::split(string_value(field_id), SepaChar1);
+}
+
+static Sexp
+make_contacts_sexp(const Contacts& contacts)
+{
+ Sexp contacts_sexp;
+
+ seq_for_each(contacts, [&](auto&& c) {
+ Sexp contact(":email"_sym, c.email);
+ if (!c.name.empty())
+ contact.add(":name"_sym, c.name);
+ contacts_sexp.add(std::move(contact));
+ });
+
+ return contacts_sexp;
+}
+
+void
+Document::add(Field::Id id, const Contacts& contacts)
+{
+ if (contacts.empty())
+ return;
+
+ const auto field{field_from_id(id)};
+ std::vector<std::string> cvec;
+
+ const std::string sepa2(1, SepaChar2);
+ auto&& termgen{make_term_generator(xdoc_, options_)};
+
+ for (auto&& contact: contacts) {
+
+ const auto cfield_id{contact.field_id()};
+ if (!cfield_id || *cfield_id != id)
+ continue;
+
+ const auto e{contact.email};
+ xdoc_.add_term(field.xapian_term(e));
+
+ /* allow searching for address components, too */
+ const auto atpos = e.find('@');
+ if (atpos != std::string::npos && atpos < e.size() - 1) {
+ xdoc_.add_term(field.xapian_term(e.substr(0, atpos)));
+ xdoc_.add_term(field.xapian_term(e.substr(atpos + 1)));
+ }
+
+ if (!contact.name.empty())
+ termgen.index_text(utf8_flatten(contact.name), 1,
+ field.xapian_term());
+ cvec.emplace_back(contact.email + sepa2 + contact.name);
+ }
+
+ if (!cvec.empty())
+ xdoc_.add_value(field.value_no(), join(cvec, SepaChar1));
+
+ if (field.include_in_sexp())
+ put_prop(field, make_contacts_sexp(contacts));
+}
+
+Contacts
+Document::contacts_value(Field::Id id) const noexcept
+{
+ const auto vals{string_vec_value(id)};
+ Contacts contacts;
+ contacts.reserve(vals.size());
+
+ const auto ctype{contact_type_from_field_id(id)};
+ if (G_UNLIKELY(!ctype)) {
+ mu_critical("invalid field-id for contact-type: <{}>",
+ static_cast<size_t>(id));
+ return {};
+ }
+
+ for (auto&& s: vals) {
+
+ const auto pos = s.find(SepaChar2);
+ if (G_UNLIKELY(pos == std::string::npos)) {
+ mu_critical("invalid contact data '{}'", s);
+ break;
+ }
+
+ contacts.emplace_back(s.substr(0, pos), s.substr(pos + 1), *ctype);
+ }
+
+ return contacts;
+}
+
+void
+Document::add_extra_contacts(const std::string& propname, const Contacts& contacts)
+{
+ if (!contacts.empty()) {
+ put_prop(propname, make_contacts_sexp(contacts));
+ dirty_sexp_ = true;
+ }
+}
+
+
+static Sexp
+make_emacs_time_sexp(::time_t t)
+{
+ return Sexp().add(static_cast<unsigned>(t >> 16),
+ static_cast<unsigned>(t & 0xffff),
+ 0);
+}
+
+void
+Document::add(Field::Id id, int64_t val)
+{
+ /*
+ * Xapian stores everything (incl. numbers) as strings.
+ *
+ * we comply, by storing a number a base-16 and prefixing with 'f' +
+ * length; such that the strings are sorted in the numerical order.
+ */
+ const auto field{field_from_id(id)};
+
+ if (field.is_value())
+ xdoc_.add_value(field.value_no(), to_lexnum(val));
+
+ if (field.include_in_sexp()) {
+ if (field.is_time_t())
+ put_prop(field, make_emacs_time_sexp(val));
+ else
+ put_prop(field, val);
+ }
+}
+
+int64_t
+Document::integer_value(Field::Id field_id) const noexcept
+{
+ if (auto&& v{string_value(field_id)}; v.empty())
+ return 0;
+ else
+ return from_lexnum(v);
+}
+
+void
+Document::add(Priority prio)
+{
+ constexpr auto field{field_from_id(Field::Id::Priority)};
+
+ xdoc_.add_value(field.value_no(), std::string(1, to_char(prio)));
+ xdoc_.add_boolean_term(field.xapian_term(to_char(prio)));
+
+ if (field.include_in_sexp())
+ put_prop(field, Sexp::Symbol(priority_name(prio)));
+}
+
+Priority
+Document::priority_value() const noexcept
+{
+ const auto val{string_value(Field::Id::Priority)};
+ return priority_from_char(val.empty() ? 'n' : val[0]);
+}
+
+void
+Document::add(Flags flags)
+{
+ constexpr auto field{field_from_id(Field::Id::Flags)};
+
+ Sexp flaglist;
+ xdoc_.add_value(field.value_no(), to_lexnum(static_cast<int64_t>(flags)));
+ flag_infos_for_each([&](auto&& flag_info) {
+ auto term=[&](){return field.xapian_term(flag_info.shortcut_lower());};
+ if (any_of(flag_info.flag & flags)) {
+ xdoc_.add_boolean_term(term());
+ flaglist.add(Sexp::Symbol(flag_info.name));
+ }
+ });
+
+ if (field.include_in_sexp())
+ put_prop(field, std::move(flaglist));
+}
+
+
+Flags
+Document::flags_value() const noexcept
+{
+ return static_cast<Flags>(integer_value(Field::Id::Flags));
+}
+
+void
+Document::remove(Field::Id field_id)
+{
+ const auto field{field_from_id(field_id)};
+ const auto pfx{field.xapian_prefix()};
+
+ xapian_try([&]{
+
+ if (auto&& val{xdoc_.get_value(field.value_no())}; !val.empty()) {
+ // g_debug("removing value<%u>: '%s'", field.value_no(),
+ // val.c_str());
+ xdoc_.remove_value(field.value_no());
+ }
+
+ std::vector<std::string> kill_list;
+ for (auto&& it = xdoc_.termlist_begin();
+ it != xdoc_.termlist_end(); ++it) {
+ const auto term{*it};
+ if (!term.empty() && term.at(0) == pfx)
+ kill_list.emplace_back(term);
+ }
+
+ for (auto&& term: kill_list) {
+ // g_debug("removing term '%s'", term.c_str());
+ try {
+ xdoc_.remove_term(term);
+ } catch(const Xapian::InvalidArgumentError& xe) {
+ mu_critical("failed to remove '{}'", term);
+ }
+ }
+ });
+}
+
+
+#ifdef BUILD_TESTS
+
+#include "utils/mu-test-utils.hh"
+
+#define assert_same_contact(C1,C2) do { \
+ g_assert_cmpstr(C1.email.c_str(),==,C2.email.c_str()); \
+ g_assert_cmpstr(C2.name.c_str(),==,C2.name.c_str()); \
+ } while (0)
+
+#define assert_same_contacts(CV1,CV2) do { \
+ g_assert_cmpuint(CV1.size(),==,CV2.size()); \
+ for (auto i = 0U; i != CV1.size(); ++i) \
+ assert_same_contact(CV1[i], CV2[i]); \
+ } while(0)
+
+
+
+static const Contacts test_contacts = {{
+ Contact{"john@example.com", "John", Contact::Type::Bcc},
+ Contact{"ringo@example.com", "Ringo", Contact::Type::Bcc},
+ Contact{"paul@example.com", "Paul", Contact::Type::Cc},
+ Contact{"george@example.com", "George", Contact::Type::Cc},
+ Contact{"james@example.com", "James", Contact::Type::From},
+ Contact{"lars@example.com", "Lars", Contact::Type::To},
+ Contact{"kirk@example.com", "Kirk", Contact::Type::To},
+ Contact{"jason@example.com", "Jason", Contact::Type::To}
+ }};
+
+static void
+test_bcc()
+{
+ {
+ Document doc;
+ doc.add(Field::Id::Bcc, test_contacts);
+
+ Contacts expected_contacts = {{
+ Contact{"john@example.com", "John",
+ Contact::Type::Bcc},
+ Contact{"ringo@example.com", "Ringo",
+ Contact::Type::Bcc},
+ }};
+ const auto actual_contacts = doc.contacts_value(Field::Id::Bcc);
+ assert_same_contacts(expected_contacts, actual_contacts);
+ }
+
+ {
+ Document doc;
+ Contacts contacts = {{
+ Contact{"john@example.com", "John Lennon",
+ Contact::Type::Bcc},
+ Contact{"ringo@example.com", "Ringo",
+ Contact::Type::Bcc},
+ }};
+ doc.add(Field::Id::Bcc, contacts);
+
+ TempDir tempdir;
+ auto db = Xapian::WritableDatabase(tempdir.path());
+ db.add_document(doc.xapian_document());
+
+ auto contacts2 = doc.contacts_value(Field::Id::Bcc);
+ assert_same_contacts(contacts, contacts2);
+ }
+
+}
+
+static void
+test_cc()
+{
+ Document doc;
+ doc.add(Field::Id::Cc, test_contacts);
+
+ Contacts expected_contacts = {{
+ Contact{"paul@example.com", "Paul", Contact::Type::Cc},
+ Contact{"george@example.com", "George", Contact::Type::Cc}
+ }};
+ const auto actual_contacts = doc.contacts_value(Field::Id::Cc);
+
+ assert_same_contacts(expected_contacts, actual_contacts);
+}
+
+
+static void
+test_from()
+{
+ Document doc;
+ doc.add(Field::Id::From, test_contacts);
+
+ Contacts expected_contacts = {{
+ Contact{"james@example.com", "James", Contact::Type::From},
+ }};
+ const auto actual_contacts = doc.contacts_value(Field::Id::From);
+
+ assert_same_contacts(expected_contacts, actual_contacts);
+}
+
+static void
+test_to()
+{
+ Document doc;
+ doc.add(Field::Id::To, test_contacts);
+
+ Contacts expected_contacts = {{
+ Contact{"lars@example.com", "Lars", Contact::Type::To},
+ Contact{"kirk@example.com", "Kirk", Contact::Type::To},
+ Contact{"jason@example.com", "Jason", Contact::Type::To}
+ }};
+ const auto actual_contacts = doc.contacts_value(Field::Id::To);
+
+ assert_same_contacts(expected_contacts, actual_contacts);
+}
+
+
+static void
+test_size()
+{
+ {
+ Document doc;
+ doc.add(Field::Id::Size, 12345);
+ g_assert_cmpuint(doc.integer_value(Field::Id::Size),==,12345);
+ }
+
+ {
+ Document doc;
+ g_assert_cmpuint(doc.integer_value(Field::Id::Size),==,0);
+ }
+}
+
+
+int
+main(int argc, char* argv[])
+{
+ g_test_init(&argc, &argv, NULL);
+
+ g_test_add_func("/message/document/bcc", test_bcc);
+ g_test_add_func("/message/document/cc", test_cc);
+ g_test_add_func("/message/document/from", test_from);
+ g_test_add_func("/message/document/to", test_to);
+
+ g_test_add_func("/message/document/size", test_size);
+
+ return g_test_run();
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/** Copyright (C) 2022-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_DOCUMENT_HH__
+#define MU_DOCUMENT_HH__
+
+#include <utility>
+#include <string>
+#include <vector>
+
+#include "mu-xapian-db.hh"
+#include "mu-fields.hh"
+#include "mu-priority.hh"
+#include "mu-flags.hh"
+#include "mu-contact.hh"
+#include <utils/mu-option.hh>
+#include <utils/mu-sexp.hh>
+
+namespace Mu {
+
+/**
+ * A Document describes the information about a message that is
+ * or can be stored in the database.
+ *
+ */
+class Document {
+public:
+ enum struct Options {
+ None = 0,
+ SupportNgrams = 1 << 0, /**< Support ngrams, as used in
+ * CJK and other languages. */
+ };
+
+ /**
+ * Construct a message for a new Xapian Document
+ *
+ * @param flags behavioral flags
+ */
+ Document(Options opts = Options::None): options_{opts} {}
+
+ /**
+ * Construct a message document based on an existing Xapian document.
+ *
+ * @param doc
+ * @param flags behavioral flags
+ */
+ Document(const Xapian::Document& doc, Options opts = Options::None):
+ xdoc_{doc}, options_{opts} {}
+
+ /**
+ * DTOR
+ */
+ ~Document() {
+ xapian_document(); // for side-effect up updating sexp.
+ }
+
+ /**
+ * Get a reference to the underlying Xapian document.
+ *
+ */
+ const Xapian::Document& xapian_document() const;
+
+ /**
+ * Get the doc-id for this document
+ *
+ * @return the docid
+ */
+ Xapian::docid docid() const { return xdoc_.get_docid(); }
+
+ /*
+ * updating a document with terms & values
+ */
+
+ /**
+ * Add a string value to the document
+ *
+ * @param field_id field id
+ * @param val string value
+ */
+ void add(Field::Id field_id, const std::string& val);
+
+ /**
+ * Add a string-vec value to the document, if non-empty
+ *
+ * @param field_id field id
+ * @param val string-vec value
+ */
+ void add(Field::Id field_id, const std::vector<std::string>& vals);
+
+
+ /**
+ * Add message-contacts to the document, if non-empty
+ *
+ * @param field_id field id
+ * @param contacts message contacts
+ */
+ void add(Field::Id id, const Contacts& contacts);
+
+ /**
+ * Add some extra contacts with the given propname; this is useful for
+ * ":reply-to" and ":list-post" which don't have a Field::Id and are
+ * only present in the sexp, not in the terms/values
+ *
+ * @param propname property name (e.g.,. ":reply-to")
+ * @param contacts contacts for this property.
+ */
+ void add_extra_contacts(const std::string& propname,
+ const Contacts& contacts);
+
+ /**
+ * Add an integer value to the document
+ *
+ * @param field_id field id
+ * @param val integer value
+ */
+ void add(Field::Id field_id, int64_t val);
+
+ /**
+ * Add a message priority to the document
+ *
+ * @param prio priority
+ */
+ void add(Priority prio);
+
+
+ /**
+ * Add message flags to the document
+ *
+ * @param flags mesage flags.
+ */
+ void add(Flags flags);
+
+ /**
+ * Remove values and terms for some field.
+ *
+ * @param field_id
+ */
+ void remove(Field::Id field_id);
+
+ /**
+ * Get the cached s-expression
+ *
+ * @return the cached s-expression
+ */
+ const Sexp& sexp() const { return cached_sexp(); }
+
+ /**
+ * Get the message s-expression as a string
+ *
+ * @return message s-expression string
+ */
+ std::string sexp_str() const { return xdoc_.get_data(); }
+
+ /**
+ * Generically adds an optional value, if set, to the document
+ *
+ * @param id the field 0d
+ * @param an optional value
+ */
+ template<typename T> void add(Field::Id id, const Option<T>& val) {
+ if (val)
+ add(id, val.value());
+ }
+
+ /*
+ * Retrieving values
+ */
+
+ /**
+ * Get a message-field as a string-value
+ *
+ * @param field_id id of the field to get.
+ *
+ * @return a string (empty if not found)
+ */
+ std::string string_value(Field::Id field_id) const noexcept {
+ return xapian_try([&]{
+ return xdoc_.get_value(field_from_id(field_id).value_no());
+ }, std::string{});
+ }
+
+ /**
+ * Get a vec of string values.
+ *
+ * @param field_id id of the field to get
+ *
+ * @return a string list
+ */
+ std::vector<std::string> string_vec_value(Field::Id field_id) const noexcept;
+
+
+ /**
+ * Get an integer value
+ *
+ * @param field_id id of the field to get
+ *
+ * @return an integer or 0 if not found.
+ */
+ int64_t integer_value(Field::Id field_id) const noexcept;
+
+
+ /**
+ * Get contacts
+ *
+ * @param field_id id of the contacts field to get
+ *
+ * @return a contacts list
+ */
+ Contacts contacts_value(Field::Id id) const noexcept;
+
+ /**
+ * Get the priority
+ *
+ * @return the message priority
+ */
+ Priority priority_value() const noexcept;
+
+ /**
+ * Get the message flags
+ *
+ *
+ * @return flags
+ */
+ Flags flags_value() const noexcept;
+
+private:
+ template<typename SexpType> void put_prop(const Field& field, SexpType&& val);
+ template<typename SexpType> void put_prop(const std::string& pname, SexpType&& val);
+
+ Sexp& cached_sexp() const {
+ if (cached_sexp_.empty())
+ if (auto&& s{Sexp::parse(xdoc_.get_data())}; s)
+ cached_sexp_ = std::move(*s);
+ return cached_sexp_;
+ }
+
+ mutable Xapian::Document xdoc_;
+ Options options_;
+ mutable Sexp cached_sexp_;
+ mutable bool dirty_sexp_{}; /* xdoc's sexp is outdated */
+};
+MU_ENABLE_BITOPS(Document::Options);
+
+} // namepace Mu
+
+#endif /* MU_DOCUMENT_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2022-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-fields.hh"
+#include "mu-flags.hh"
+
+#include "utils/mu-test-utils.hh"
+
+using namespace Mu;
+
+std::string
+Field::xapian_term(const std::string& s) const
+{
+ const auto start{std::string(1U, xapian_prefix())};
+ if (const auto& size = s.size(); size == 0)
+ return start;
+
+ std::string res{start};
+ res.reserve(s.size() + 10);
+
+ /* slightly optimized common pure-ascii. */
+ if (G_LIKELY(g_str_is_ascii(s.c_str()))) {
+ res += s;
+ for (auto i = 1; res[i]; ++i)
+ res[i] = g_ascii_tolower(res[i]);
+ } else
+ res += utf8_flatten(s);
+
+ if (G_UNLIKELY(res.size() > MaxTermLength))
+ res.erase(MaxTermLength);
+
+ return res;
+}
+
+/**
+ * compile-time checks
+ */
+constexpr bool
+validate_field_ids()
+{
+ for (auto id = 0U; id != Field::id_size(); ++id) {
+ const auto field_id = static_cast<Field::Id>(id);
+ if (field_from_id(field_id).id != field_id)
+ return false;
+ }
+ return true;
+}
+
+constexpr bool
+validate_field_shortcuts()
+{
+#ifdef BUILD_TESTS
+ std::array<size_t, 26> no_dups = {0};
+#endif /*BUILD_TESTS*/
+ for (auto id = 0U; id != Field::id_size(); ++id) {
+ const auto field_id = static_cast<Field::Id>(id);
+ const auto shortcut = field_from_id(field_id).shortcut;
+ if (shortcut != 0 &&
+ (shortcut < 'a' || shortcut > 'z'))
+ return false;
+#ifdef BUILD_TESTS
+ if (shortcut != 0) {
+ if (++no_dups[static_cast<size_t>(shortcut-'a')] > 1) {
+ mu_critical("shortcut '{}' is duplicated", shortcut);
+ return false;
+ }
+ }
+#endif
+ }
+
+ return true;
+}
+
+
+constexpr /*static*/ bool
+validate_field_flags()
+{
+ for (auto&& field: Fields) {
+ /* - A field has at most one of Phrasable, Boolean */
+ size_t flagnum{};
+
+ if (field.is_phrasable_term())
+ ++flagnum;
+ if (field.is_boolean_term())
+ ++flagnum;
+
+ if (flagnum > 1) {
+ //mu_warning("invalid field {}", field.name);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/*
+ * tests... also build as runtime-tests, so we can get coverage info
+ */
+#ifdef BUILD_TESTS
+#define static_assert g_assert_true
+#endif /*BUILD_TESTS*/
+
+
+[[maybe_unused]]
+static void
+test_ids()
+{
+ static_assert(validate_field_ids());
+}
+
+[[maybe_unused]]
+static void
+test_shortcuts()
+{
+ static_assert(validate_field_shortcuts());
+}
+
+[[maybe_unused]]
+static void
+test_prefix()
+{
+ static_assert(field_from_id(Field::Id::Subject).xapian_prefix() == 'S');
+}
+
+[[maybe_unused]]
+static void
+test_field_flags()
+{
+ static_assert(validate_field_flags());
+}
+
+#ifdef BUILD_TESTS
+
+
+static void
+test_field_from_name()
+{
+ g_assert_true(field_from_name("s")->id == Field::Id::Subject);
+ g_assert_true(field_from_name("subject")->id == Field::Id::Subject);
+ g_assert_false(!!field_from_name("8"));
+ g_assert_false(!!field_from_name(""));
+
+ g_assert_true(field_from_name("").value_or(field_from_id(Field::Id::Bcc)).id ==
+ Field::Id::Bcc);
+}
+
+static void
+test_xapian_term()
+{
+ using namespace std::string_literals;
+ using namespace std::literals;
+
+ assert_equal(field_from_id(Field::Id::Subject).xapian_term(""s), "S");
+ assert_equal(field_from_id(Field::Id::Subject).xapian_term("boo"s), "Sboo");
+
+ assert_equal(field_from_id(Field::Id::From).xapian_term('x'), "Fx");
+ assert_equal(field_from_id(Field::Id::To).xapian_term("boo"sv), "Tboo");
+
+ auto s1 = field_from_id(Field::Id::Subject).xapian_term(std::string(MaxTermLength - 1, 'x'));
+ auto s2 = field_from_id(Field::Id::Subject).xapian_term(std::string(MaxTermLength, 'x'));
+ g_assert_cmpuint(s1.length(), ==, s2.length());
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/message/fields/ids", test_ids);
+ g_test_add_func("/message/fields/shortcuts", test_shortcuts);
+ g_test_add_func("/message/fields/from-name", test_field_from_name);
+ g_test_add_func("/message/fields/prefix", test_prefix);
+ g_test_add_func("/message/fields/xapian-term", test_xapian_term);
+ g_test_add_func("/message/fields/flags", test_field_flags);
+
+ return g_test_run();
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2022-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_FIELDS_HH__
+#define MU_FIELDS_HH__
+
+#include <cstdint>
+#include <string_view>
+#include <algorithm>
+#include <array>
+#include <mu-xapian-db.hh>
+#include <utils/mu-utils.hh>
+#include <utils/mu-option.hh>
+
+namespace Mu {
+
+// Xapian does not like terms much longer than this
+constexpr auto MaxTermLength = 240;
+// http://article.gmane.org/gmane.comp.search.xapian.general/3656 */
+
+struct Field {
+ /**
+ * Field Ids.
+ *
+ * Note, the Ids are also used as indices in the Fields array,
+ * so their numerical values must be 0...Count.
+ *
+ */
+ enum struct Id {
+ Bcc = 0, /**< Blind Carbon-Copy */
+ BodyText, /**< Text body */
+ Cc, /**< Carbon-Copy */
+ Changed, /**< Last change time (think 'ctime') */
+ Date, /**< Message date */
+ EmbeddedText, /**< Embedded text in message */
+ File, /**< Filename */
+ Flags, /**< Message flags */
+ From, /**< Message sender */
+ Language, /**< Body language */
+ Maildir, /**< Maildir path */
+ MailingList, /**< Mailing list */
+ MessageId, /**< Message Id */
+ MimeType, /**< MIME-Type */
+ Path, /**< File-system Path */
+ Priority, /**< Message priority */
+ References, /**< All references (incl. Reply-To:) */
+ Size, /**< Message size (in bytes) */
+ Subject, /**< Message subject */
+ Tags, /**< Message Tags */
+ ThreadId, /**< Thread Id */
+ To, /**< To: recipient */
+ //
+ _count_ /**< Number of Ids */
+ };
+
+ /**
+ * Get the number of Id values.
+ *
+ * @return the number.
+ */
+ static constexpr size_t id_size()
+ {
+ return static_cast<size_t>(Id::_count_);
+ }
+
+ constexpr Xapian::valueno value_no() const {
+ return static_cast<Xapian::valueno>(id);
+ }
+
+ /**
+ * Field types
+ *
+ */
+ enum struct Type {
+ String, /**< String */
+ StringList, /**< List of strings */
+ ContactList, /**< List of contacts */
+ ByteSize, /**< Size in bytes */
+ TimeT, /**< A time_t value */
+ Integer, /**< An integer */
+ };
+
+ constexpr bool is_string() const { return type == Type::String; }
+ constexpr bool is_string_list() const { return type == Type::StringList; }
+ constexpr bool is_byte_size() const { return type == Type::ByteSize; }
+ constexpr bool is_time_t() const { return type == Type::TimeT; }
+ constexpr bool is_integer() const { return type == Type::Integer; }
+ constexpr bool is_numerical() const { return is_byte_size() || is_time_t() || is_integer(); }
+
+ /**
+ * Field flags
+ * note: the differences for our purposes between a xapian field and a
+ * term: - there is only a single value for some item in per document
+ * (msg), ie. one value containing the list of To: addresses - there
+ * can be multiple terms, each containing e.g. one of the To:
+ * addresses - searching uses terms, but to display some field, it
+ * must be in the value
+ *
+ * Rules (build-time enforced):
+ * - A field has at most one of PhrasableTerm, BooleanTerm, ContactTerm.
+ */
+
+ enum struct Flag {
+ /*
+ * Different kind of terms; at most one is true, and cannot be combined with
+ * Contact. Compile-time enforced.
+ */
+ NormalTerm = 1 << 0,
+ /**< Field is a searchable term */
+ BooleanTerm = 1 << 1,
+ /**< Field is a boolean search-term (i.e. at most one per message);
+ * wildcards do not work */
+ PhrasableTerm = 1 << 2,
+ /**< Field has phrasable/indexable text as term */
+ /*
+ * Contact flag cannot be combined with any of the term flags.
+ * This is compile-time enforced.
+ */
+ Contact = 1 << 10,
+ /**< field contains one or more e-mail-addresses */
+ Value = 1 << 11,
+ /**< Field value is stored (so the literal value can be retrieved) */
+
+ Range = 1 << 21,
+
+ IncludeInSexp = 1 << 24,
+ /**< whether to include this field in the cached sexp. */
+
+ /**< whether this is a range field (e.g., date, size)*/
+ Internal = 1 << 26
+ };
+
+ constexpr bool any_of(Flag some_flag) const{
+ return (static_cast<int>(some_flag) & static_cast<int>(flags)) != 0;
+ }
+
+ constexpr bool is_phrasable_term() const { return any_of(Flag::PhrasableTerm); }
+ constexpr bool is_boolean_term() const { return any_of(Flag::BooleanTerm); }
+ constexpr bool is_normal_term() const { return any_of(Flag::NormalTerm); }
+ constexpr bool is_searchable() const { return is_phrasable_term() ||
+ is_boolean_term() ||
+ is_normal_term(); }
+ constexpr bool is_sortable() const { return is_value(); }
+
+
+ constexpr bool is_value() const { return any_of(Flag::Value); }
+ constexpr bool is_internal() const { return any_of(Flag::Internal); }
+
+ constexpr bool is_contact() const { return any_of(Flag::Contact); }
+ constexpr bool is_range() const { return any_of(Flag::Range); }
+
+ constexpr bool include_in_sexp() const { return any_of(Flag::IncludeInSexp);}
+
+ /**
+ * Field members
+ *
+ */
+ Id id; /**< Id of the message field */
+ Type type; /**< Type of the message field */
+ std::string_view name; /**< Name of the message field */
+ std::string_view alias; /**< Alternative name for the message field */
+ std::string_view description; /**< Decription of the message field */
+ std::string_view example_query; /**< Example query */
+ char shortcut; /**< Shortcut for the message field; a..z */
+ Flag flags; /**< Flags */
+
+ /**
+ * Convenience / helpers
+ *
+ */
+
+ constexpr char xapian_prefix() const {
+ /* xapian uses uppercase shortcuts; toupper is not constexpr */
+ return shortcut == 0 ? 0 : shortcut - ('a' - 'A');
+ }
+
+ /**
+ * Get the xapian term; truncated to MaxTermLength and
+ * utf8-flattened.
+ *
+ * @param s
+ *
+ * @return the xapian term
+ */
+ std::string xapian_term(const std::string& s="") const;
+ std::string xapian_term(std::string_view sv) const {
+ return xapian_term(std::string{sv});
+ }
+ std::string xapian_term(char c) const {
+ return xapian_term(std::string(1, c));
+ }
+};
+
+// equality
+static inline constexpr bool operator==(const Field& f1, const Field& f2) { return f1.id == f2.id; }
+static inline constexpr bool operator==(const Field& f1, const Field::Id id) { return f1.id == id; }
+
+
+MU_ENABLE_BITOPS(Field::Flag);
+
+/**
+ * Sequence of _all_ message fields
+ */
+static constexpr std::array<Field, Field::id_size()>
+ Fields = {
+ {
+ {
+ Field::Id::Bcc,
+ Field::Type::ContactList,
+ "bcc", {},
+ "Blind carbon-copy recipient",
+ "bcc:foo@example.com",
+ 'h',
+ Field::Flag::Contact |
+ Field::Flag::Value |
+ Field::Flag::IncludeInSexp |
+ Field::Flag::NormalTerm |
+ Field::Flag::PhrasableTerm,
+ },
+ {
+ Field::Id::BodyText,
+ Field::Type::String,
+ "body", {},
+ "Message plain-text body",
+ "body:capybara",
+ 'b',
+ Field::Flag::PhrasableTerm,
+ },
+ {
+ Field::Id::Cc,
+ Field::Type::ContactList,
+ "cc", {},
+ "Carbon-copy recipient",
+ "cc:quinn@example.com",
+ 'c',
+ Field::Flag::Contact |
+ Field::Flag::Value |
+ Field::Flag::IncludeInSexp |
+ Field::Flag::NormalTerm |
+ Field::Flag::PhrasableTerm,
+ },
+ {
+ Field::Id::Changed,
+ Field::Type::TimeT,
+ "changed", {},
+ "Last change time",
+ "changed:30M..",
+ 'k',
+ Field::Flag::Value |
+ Field::Flag::Range |
+ Field::Flag::IncludeInSexp
+ },
+ {
+ Field::Id::Date,
+ Field::Type::TimeT,
+ "date", {},
+ "Message date",
+ "date:20220101..20220505",
+ 'd',
+ Field::Flag::Value |
+ Field::Flag::Range |
+ Field::Flag::IncludeInSexp
+ },
+ {
+ Field::Id::EmbeddedText,
+ Field::Type::String,
+ "embed", {},
+ "Embedded text",
+ "embed:war OR embed:peace",
+ 'e',
+ Field::Flag::PhrasableTerm
+ },
+ {
+ Field::Id::File,
+ Field::Type::String,
+ "file", {},
+ "Attachment file name",
+ "file:/image\\.*.jpg/",
+ 'j',
+ Field::Flag::BooleanTerm
+ },
+ {
+ Field::Id::Flags,
+ Field::Type::Integer,
+ "flags", "flag",
+ "Message properties",
+ "flag:unread AND flag:personal",
+ 'g',
+ Field::Flag::BooleanTerm |
+ Field::Flag::Value |
+ Field::Flag::IncludeInSexp
+ },
+ {
+ Field::Id::From,
+ Field::Type::ContactList,
+ "from", {},
+ "Message sender",
+ "from:jimbo",
+ 'f',
+ Field::Flag::Contact |
+ Field::Flag::Value |
+ Field::Flag::IncludeInSexp |
+ Field::Flag::NormalTerm |
+ Field::Flag::PhrasableTerm,
+ },
+ {
+ Field::Id::Language,
+ Field::Type::String,
+ "language", "lang",
+ "ISO 639-1 language code for body",
+ "lang:nl",
+ 'a',
+ Field::Flag::BooleanTerm |
+ Field::Flag::Value |
+ Field::Flag::IncludeInSexp
+ },
+ {
+ Field::Id::Maildir,
+ Field::Type::String,
+ "maildir", {},
+ "Maildir path for message",
+ "maildir:/private/archive",
+ 'm',
+ Field::Flag::BooleanTerm |
+ Field::Flag::Value |
+ Field::Flag::IncludeInSexp
+ },
+ {
+ Field::Id::MailingList,
+ Field::Type::String,
+ "list", {},
+ "Mailing list (List-Id:)",
+ "list:mu-discuss.example.com",
+ 'v',
+ Field::Flag::BooleanTerm |
+ Field::Flag::Value |
+ Field::Flag::IncludeInSexp
+ },
+ {
+ Field::Id::MessageId,
+ Field::Type::String,
+ "message-id", "msgid",
+ "Message-Id",
+ "msgid:abc@123",
+ 'i',
+ Field::Flag::BooleanTerm |
+ Field::Flag::Value |
+ Field::Flag::IncludeInSexp
+ },
+ {
+ Field::Id::MimeType,
+ Field::Type::String,
+ "mime", "mime-type",
+ "Attachment MIME-type",
+ "mime:image/jpeg",
+ 'y',
+ Field::Flag::BooleanTerm
+ },
+ {
+ Field::Id::Path,
+ Field::Type::String,
+ "path", {},
+ "File system path to message",
+ "path:/a/b/Maildir/cur/msg:2,S",
+ 'l',
+ Field::Flag::BooleanTerm |
+ Field::Flag::Value |
+ Field::Flag::IncludeInSexp
+ },
+ {
+ Field::Id::Priority,
+ Field::Type::Integer,
+ "priority", "prio",
+ "Priority",
+ "prio:high",
+ 'p',
+ Field::Flag::BooleanTerm |
+ Field::Flag::Value |
+ Field::Flag::IncludeInSexp
+ },
+ {
+ Field::Id::References,
+ Field::Type::StringList,
+ "references", {},
+ "References to related messages",
+ {},
+ 'r',
+ Field::Flag::Value |
+ Field::Flag::IncludeInSexp
+ },
+ {
+ Field::Id::Size,
+ Field::Type::ByteSize,
+ "size", {},
+ "Message size in bytes",
+ "size:1M..5M",
+ 'z',
+ Field::Flag::Value |
+ Field::Flag::Range |
+ Field::Flag::IncludeInSexp
+ },
+ {
+ Field::Id::Subject,
+ Field::Type::String,
+ "subject", {},
+ "Message subject",
+ "subject:wombat",
+ 's',
+ Field::Flag::Value |
+ Field::Flag::IncludeInSexp |
+ Field::Flag::NormalTerm |
+ Field::Flag::PhrasableTerm
+ },
+ {
+ Field::Id::Tags,
+ Field::Type::StringList,
+ "tags", "tag",
+ "Message tags",
+ "tag:projectx",
+ 'x',
+ Field::Flag::BooleanTerm |
+ Field::Flag::Value |
+ Field::Flag::IncludeInSexp
+ },
+ {
+ Field::Id::ThreadId,
+ Field::Type::String,
+ "thread", {},
+ "Thread a message belongs to",
+ {},
+ 'w',
+ Field::Flag::BooleanTerm |
+ Field::Flag::Value
+ },
+ {
+ Field::Id::To,
+ Field::Type::ContactList,
+ "to", {},
+ "Message recipient",
+ "to:flimflam@example.com",
+ 't',
+ Field::Flag::Contact |
+ Field::Flag::Value |
+ Field::Flag::IncludeInSexp |
+ Field::Flag::NormalTerm |
+ Field::Flag::PhrasableTerm,
+ },
+ }};
+
+/*
+ * Convenience
+ */
+
+/**
+ * Get the message field for the given Id.
+ *
+ * @param id of the message field
+ *
+ * @return ref of the message field.
+ */
+constexpr const Field&
+field_from_id(Field::Id id)
+{
+ return Fields.at(static_cast<size_t>(id));
+}
+
+/**
+ * Invoke func for each message-field
+ *
+ * @param func some callable
+ */
+template <typename Func>
+constexpr void field_for_each(Func&& func) {
+ for (const auto& field: Fields)
+ func(field);
+}
+
+/**
+ * Find a message field that satisfies some predicate
+ *
+ * @param pred the predicate (a callable)
+ *
+ * @return a message-field id, or nullopt if not found.
+ */
+template <typename Pred>
+constexpr Option<Field> field_find_if(Pred&& pred) {
+ for (auto&& field: Fields)
+ if (pred(field))
+ return field;
+ return Nothing;
+}
+
+/**
+ * Get the the message-field for the given name or shortcut
+ *
+ * @param name_or_shortcut
+ *
+ * @return the message-field or Nothing
+ */
+static inline
+Option<Field> field_from_shortcut(char shortcut) {
+ return field_find_if([&](auto&& field){
+ return field.shortcut == shortcut;
+ });
+}
+static inline
+Option<Field> field_from_name(const std::string& name) {
+ switch(name.length()) {
+ case 0:
+ return Nothing;
+ case 1:
+ return field_from_shortcut(name[0]);
+ default:
+ return field_find_if([&](auto&& field){
+ return name == field.name || name == field.alias;
+ });
+ }
+}
+
+/**
+ * Return combination-fields such
+ * as "contact", "recip" and "" (empty)
+ *
+ * @param name combination field name
+ *
+ * @return list of matching fields
+ */
+using FieldsVec = std::vector<Field>;
+static inline
+const FieldsVec& fields_from_name(const std::string& name) {
+
+ static const FieldsVec none{};
+ static const FieldsVec recip_fields ={
+ field_from_id(Field::Id::To),
+ field_from_id(Field::Id::Cc),
+ field_from_id(Field::Id::Bcc)};
+
+ static const FieldsVec contact_fields = {
+ field_from_id(Field::Id::To),
+ field_from_id(Field::Id::Cc),
+ field_from_id(Field::Id::Bcc),
+ field_from_id(Field::Id::From),
+ };
+ static const FieldsVec empty_fields= {
+ field_from_id(Field::Id::To),
+ field_from_id(Field::Id::Cc),
+ field_from_id(Field::Id::Bcc),
+ field_from_id(Field::Id::From),
+ field_from_id(Field::Id::Subject),
+ field_from_id(Field::Id::BodyText),
+ field_from_id(Field::Id::EmbeddedText),
+ };
+
+ if (name == "recip")
+ return recip_fields;
+ else if (name == "contact")
+ return contact_fields;
+ else if (name.empty())
+ return empty_fields;
+ else
+ return none;
+}
+
+static inline bool
+field_is_combi (const std::string& name)
+{
+ return name == "recip" || name == "contact";
+}
+
+
+/**
+ * Get the Field::Id for some number, or nullopt if it does not match
+ *
+ * @param id an id number
+ *
+ * @return Field::Id or nullopt
+ */
+static inline
+Option<Field> field_from_number(size_t id)
+{
+ if (id >= static_cast<size_t>(Field::Id::_count_))
+ return Nothing;
+ else
+ return field_from_id(static_cast<Field::Id>(id));
+}
+
+
+} // namespace Mu
+#endif /* MU_FIELDS_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+/*
+ * implementation is almost completely in the header; here we just add some
+ * compile-time tests.
+ */
+
+#include "mu-flags.hh"
+
+using namespace Mu;
+
+std::string
+Mu::to_string(Flags flags)
+{
+ std::string str;
+
+ for (auto&& info: AllMessageFlagInfos)
+ if (any_of(info.flag & flags))
+ str+=info.shortcut;
+
+ return str;
+}
+
+
+/*
+ * flags & flag-info
+ */
+constexpr bool
+validate_message_info_flags()
+{
+ for (auto id = 0U; id != AllMessageFlagInfos.size(); ++id) {
+ const auto flag = static_cast<Flags>(1 << id);
+ if (flag != AllMessageFlagInfos[id].flag)
+ return false;
+ }
+ return true;
+}
+
+
+/*
+ * tests... also build as runtime-tests, so we can get coverage info
+ */
+#ifdef BUILD_TESTS
+#define static_assert g_assert_true
+#endif /*BUILD_TESTS*/
+
+[[maybe_unused]] static void
+test_basic()
+{
+ static_assert(AllMessageFlagInfos.size() ==
+ __builtin_ctz(static_cast<unsigned>(Flags::_final_)));
+ static_assert(validate_message_info_flags());
+
+ static_assert(!!flag_info(Flags::Encrypted));
+ static_assert(!flag_info(Flags::None));
+ static_assert(!flag_info(static_cast<Flags>(0)));
+ static_assert(!flag_info(static_cast<Flags>(1<<AllMessageFlagInfos.size())));
+}
+
+/*
+ * flag_info
+ */
+[[maybe_unused]] static void
+test_flag_info()
+{
+ static_assert(flag_info('D')->flag == Flags::Draft);
+ static_assert(flag_info('l')->flag == Flags::MailingList);
+ static_assert(!flag_info('y'));
+
+ static_assert(flag_info("trashed")->flag == Flags::Trashed);
+ static_assert(flag_info("attach")->flag == Flags::HasAttachment);
+ static_assert(!flag_info("fnorb"));
+
+
+ static_assert(flag_info('D')->shortcut_lower() == 'd');
+ static_assert(flag_info('u')->shortcut_lower() == 'u');
+}
+
+/*
+ * flags_from_expr
+ */
+[[maybe_unused]] static void
+test_flags_from_expr()
+{
+ static_assert(flags_from_absolute_expr("SRP").value() ==
+ (Flags::Seen | Flags::Replied | Flags::Passed));
+ static_assert(flags_from_absolute_expr("Faul").value() ==
+ (Flags::Flagged | Flags::Unread |
+ Flags::HasAttachment | Flags::MailingList));
+
+ /* note: unread is a special flag, _implied_ from "new or not seen" */
+ static_assert(flags_from_absolute_expr("N").value() == (Flags::New|Flags::Unread));
+
+ static_assert(!flags_from_absolute_expr("DRT?"));
+ static_assert(flags_from_absolute_expr("DRT?", true/*ignore invalid*/).value() ==
+ (Flags::Draft | Flags::Replied |
+ Flags::Trashed | Flags::Unread));
+ static_assert(flags_from_absolute_expr("DFPNxulabcdef", true/*ignore invalid*/).value() ==
+ (Flags::Draft|Flags::Flagged|Flags::Passed|
+ Flags::New | Flags::Encrypted |
+ Flags::Unread | Flags::MailingList | Flags::Calendar |
+ Flags::HasAttachment));
+}
+
+
+/*
+ * flags_from_delta_expr
+ */
+[[maybe_unused]] static void
+test_flags_from_delta_expr()
+{
+ static_assert(flags_from_delta_expr(
+ "+S-u-N", Flags::New|Flags::Unread).value() ==
+ Flags::Seen);
+
+ /* note: unread is a special flag, _implied_ from "new or not seen" */
+ static_assert(flags_from_delta_expr(
+ "+S-N", Flags::New|Flags::Unread).value() ==
+ Flags::Seen);
+ static_assert(flags_from_delta_expr(
+ "-S", Flags::Seen).value() ==
+ Flags::Unread);
+
+ static_assert(flags_from_delta_expr("+R+P-F", Flags::Seen).value() ==
+ (Flags::Seen|Flags::Passed|Flags::Replied));
+ /* '-B' is invalid */
+ static_assert(!flags_from_delta_expr("+R+P-B", Flags::Seen));
+ /* '-B' is invalid, but ignore invalid */
+ static_assert(flags_from_delta_expr("+R+P-B", Flags::Seen, true) ==
+ (Flags::Replied|Flags::Passed|Flags::Seen));
+ static_assert(flags_from_delta_expr("+F+T-S", Flags::None, true).value() ==
+ (Flags::Flagged|Flags::Trashed|Flags::Unread));
+}
+
+/*
+ * flags_filter
+ */
+[[maybe_unused]] static void
+test_flags_filter()
+{
+ static_assert(flags_filter(flags_from_absolute_expr(
+ "DFPNxulabcdef", true/*ignore invalid*/).value(),
+ MessageFlagCategory::Mailfile) ==
+ (Flags::Draft|Flags::Flagged|Flags::Passed));
+}
+
+
+
+[[maybe_unused]] static void
+test_flags_keep_unmutable()
+{
+ static_assert(flags_keep_unmutable((Flags::Seen|Flags::Passed),
+ (Flags::Flagged|Flags::Draft),
+ Flags::Replied) ==
+ (Flags::Flagged|Flags::Draft));
+}
+
+
+
+#ifdef BUILD_TESTS
+int
+main(int argc, char* argv[])
+{
+ g_test_init(&argc, &argv, NULL);
+
+ g_test_add_func("/message/flags/basic", test_basic);
+ g_test_add_func("/message/flags/flag-info", test_flag_info);
+ g_test_add_func("/message/flags/flags-from-absolute-expr",
+ test_flags_from_expr);
+ g_test_add_func("/message/flags/flags-from-delta-expr",
+ test_flags_from_delta_expr);
+ g_test_add_func("/message/flags/flags-filter",
+ test_flags_filter);
+ g_test_add_func("/message/flags/flags-keep-unmutable",
+ test_flags_keep_unmutable);
+
+ return g_test_run();
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2022-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_FLAGS_HH__
+#define MU_FLAGS_HH__
+
+#include <algorithm>
+#include <string_view>
+#include <array>
+#include <utils/mu-utils.hh>
+#include <utils/mu-option.hh>
+
+namespace Mu {
+
+enum struct Flags {
+ None = 0, /**< No flags */
+ /**
+ * next 6 are seen in the file-info part of maildir message file
+ * names, ie., in a name like "1234345346:2,<fileinfo>",
+ * <fileinfo> consists of zero or more of the following
+ * characters (in ascii order)
+ */
+ Draft = 1 << 0, /**< A draft message */
+ Flagged = 1 << 1, /**< A flagged message */
+ Passed = 1 << 2, /**< A passed (forwarded) message */
+ Replied = 1 << 3, /**< A replied message */
+ Seen = 1 << 4, /**< A seen (read) message */
+ Trashed = 1 << 5, /**< A trashed message */
+
+ /**
+ * decides on cur/ or new/ in the maildir
+ */
+ New = 1 << 6, /**< A new message */
+
+ /**
+ * content flags -- not visible in the filename, but used for
+ * searching
+ */
+ Signed = 1 << 7, /**< Cryptographically signed */
+ Encrypted = 1 << 8, /**< Encrypted */
+ HasAttachment = 1 << 9, /**< Has an attachment */
+
+ Unread = 1 << 10, /**< Unread; pseudo-flag, only for queries, so we can
+ * search for flag:unread, which is equivalent to
+ * 'flag:new OR NOT flag:seen' */
+ /**
+ * other content flags
+ */
+ MailingList = 1 << 11, /**< A mailing-list message */
+ Personal = 1 << 12, /**< A personal message (i.e., at least one of the
+ * contact fields contains a personal address) */
+ Calendar = 1 << 13, /**< A calendar invitation */
+ /*
+ * <private>
+ */
+ _final_ = 1 << 14
+};
+MU_ENABLE_BITOPS(Flags);
+
+/**
+ * Message flags category
+ *
+ */
+enum struct MessageFlagCategory {
+ None, /**< Nothing */
+ Mailfile, /**< Flag for a message file */
+ Maildir, /**< Flag for message file's location */
+ Content, /**< Message content flag */
+ Pseudo /**< Pseudo flag */
+};
+
+/**
+ * Info about invidual message flags
+ *
+ */
+struct MessageFlagInfo {
+
+ Flags flag; /**< The message flag */
+ char shortcut; /**< Shortcut character;
+ * tolower(shortcut) must be
+ * unique for all flags */
+ std::string_view name; /**< Name of the flag */
+ MessageFlagCategory category; /**< Flag category */
+ std::string_view description; /**< Description */
+
+ /**
+ * Get the lower-case version of shortcut
+ *
+ * @return lower-case shortcut
+ */
+ constexpr char shortcut_lower() const {
+ return shortcut >= 'A' && shortcut <= 'Z' ?
+ shortcut + ('a' - 'A') : shortcut;
+ }
+};
+
+/**
+ * Array of all flag information.
+ */
+constexpr std::array<MessageFlagInfo, 14> AllMessageFlagInfos = {{
+ MessageFlagInfo{Flags::Draft, 'D', "draft", MessageFlagCategory::Mailfile,
+ "Draft (in progress)"
+ },
+ MessageFlagInfo{Flags::Flagged, 'F', "flagged", MessageFlagCategory::Mailfile,
+ "User-flagged"
+ },
+ MessageFlagInfo{Flags::Passed, 'P', "passed", MessageFlagCategory::Mailfile,
+ "Forwarded message"
+ },
+ MessageFlagInfo{Flags::Replied, 'R', "replied", MessageFlagCategory::Mailfile,
+ "Replied-to"
+ },
+ MessageFlagInfo{Flags::Seen, 'S', "seen", MessageFlagCategory::Mailfile,
+ "Viewed at least once"
+ },
+ MessageFlagInfo{Flags::Trashed, 'T', "trashed", MessageFlagCategory::Mailfile,
+ "Marked for deletion"
+ },
+ MessageFlagInfo{Flags::New, 'N', "new", MessageFlagCategory::Maildir,
+ "New message"
+ },
+ MessageFlagInfo{Flags::Signed, 'z', "signed", MessageFlagCategory::Content,
+ "Cryptographically signed"
+ },
+ MessageFlagInfo{Flags::Encrypted, 'x', "encrypted", MessageFlagCategory::Content,
+ "Encrypted"
+ },
+ MessageFlagInfo{Flags::HasAttachment,'a', "attach", MessageFlagCategory::Content,
+ "Has at least one attachment"
+ },
+ MessageFlagInfo{Flags::Unread, 'u', "unread", MessageFlagCategory::Pseudo,
+ "New or not seen message"
+ },
+ MessageFlagInfo{Flags::MailingList, 'l', "list", MessageFlagCategory::Content,
+ "Mailing list message"
+ },
+ MessageFlagInfo{Flags::Personal, 'q', "personal", MessageFlagCategory::Content,
+ "Personal message"
+ },
+ MessageFlagInfo{Flags::Calendar, 'c', "calendar", MessageFlagCategory::Content,
+ "Calendar invitation"
+ },
+}};
+
+
+/**
+ * Invoke some callable Func for each flag info
+ *
+ * @param func some callable
+ */
+template<typename Func>
+constexpr void flag_infos_for_each(Func&& func)
+{
+ for (auto&& info: AllMessageFlagInfos)
+ func(info);
+}
+
+/**
+ * Get flag info for some flag
+ *
+ * @param flag a singular flag
+ *
+ * @return the MessageFlagInfo, or Nothing in case of error.
+ */
+constexpr const Option<MessageFlagInfo>
+flag_info(Flags flag)
+{
+ constexpr auto upper = static_cast<unsigned>(Flags::_final_);
+ const auto val = static_cast<unsigned>(flag);
+
+ if (__builtin_popcount(val) != 1 || val >= upper)
+ return Nothing;
+
+ return AllMessageFlagInfos[static_cast<unsigned>(__builtin_ctz(val))];
+}
+
+/**
+ * Get flag info for some flag
+ *
+ * @param shortcut shortcut character
+ *
+ * @return the MessageFlagInfo
+ */
+constexpr const Option<MessageFlagInfo>
+flag_info(char shortcut)
+{
+ for (auto&& info : AllMessageFlagInfos)
+ if (info.shortcut == shortcut)
+ return info;
+
+ return Nothing;
+}
+
+/**
+ * Get flag info for some flag, either by its name of is shortcut
+ *
+ * @param name the name of the message-flag, or its shortcut
+ *
+ * @return the MessageFlagInfo or Nothing if not found
+ */
+constexpr const Option<MessageFlagInfo>
+flag_info(std::string_view name)
+{
+ if (name.empty())
+ return Nothing;
+
+ for (auto&& info : AllMessageFlagInfos)
+ if (info.name == name)
+ return info;
+
+ return flag_info(name.at(0));
+}
+
+/**
+ * 'unread' is a pseudo-flag that means 'new or not seen'
+ *
+ * @param flags
+ *
+ * @return flags with unread added or removed.
+ */
+constexpr Flags
+imply_unread(Flags flags)
+{
+ /* unread is a pseudo flag equivalent to 'new or not seen' */
+ if (any_of(flags & Flags::New) || none_of(flags & Flags::Seen))
+ return flags | Flags::Unread;
+ else
+ return flags & ~Flags::Unread;
+}
+
+/**
+ * There are two string-based expression types for flags:
+ * 1) 'absolute': replace the existing flags
+ * 2) 'delta' : flags as a delta of existing flags.
+ */
+
+/**
+ * Get the (OR'ed) flags corresponding to an expression.
+ *
+ * @param expr the expression (a sequence of flag shortcut characters)
+ * @param ignore_invalid if @true, ignore invalid flags, otherwise return
+ * nullopt if an invalid flag is encountered
+ *
+ * @return the (OR'ed) flags or Flags::None
+ */
+constexpr Option<Flags>
+flags_from_absolute_expr(std::string_view expr, bool ignore_invalid = false)
+{
+ Flags flags{Flags::None};
+
+ for (auto&& kar : expr) {
+ if (const auto& info{flag_info(kar)}; !info) {
+ if (!ignore_invalid)
+ return Nothing;
+ } else
+ flags |= info->flag;
+ }
+
+ return imply_unread(flags);
+}
+
+/**
+ * Calculate flags from existing flags and a delta expression
+ *
+ * Update @p flags with the flags in @p expr, where @p exprt consists of the the
+ * normal flag shortcut characters, prefixed with either '+' or '-', which means
+ * resp. "add this flag" or "remove this flag".
+ *
+ * So, e.g. "-N+S" would unset the NEW flag and set the SEEN flag, without
+ * affecting other flags.
+ *
+ * @param expr delta expression
+ * @param flags existing flags
+ * @param ignore_invalid if @true, ignore invalid flags, otherwise return
+ * Nothing if an invalid flag is encountered
+ *
+ * @return new flags, or Nothing in case of error
+ */
+constexpr Option<Flags>
+flags_from_delta_expr(std::string_view expr, Flags flags,
+ bool ignore_invalid = false)
+{
+ if (expr.size() % 2 != 0)
+ return Nothing;
+
+ for (auto u = 0U; u != expr.size(); u += 2) {
+ if (const auto& info{flag_info(expr[u + 1])}; !info) {
+ if (!ignore_invalid)
+ return Nothing;
+ } else {
+ switch (expr[u]) {
+ case '+': flags |= info->flag; break;
+ case '-': flags &= ~info->flag; break;
+ default:
+ if (!ignore_invalid)
+ return Nothing;
+ break;
+ }
+ }
+ }
+
+ return imply_unread(flags);
+}
+
+/**
+ * Calculate the flags from either 'absolute' or 'delta' expressions
+ *
+ * @param expr a flag expression, either 'delta' or 'absolute'
+ * @param flags optional: existing flags or none. Required for delta.
+ *
+ * @return either messages flags or Nothing in case of error.
+ */
+constexpr Option<Flags>
+flags_from_expr(std::string_view expr, Option<Flags> flags = Nothing)
+{
+ if (expr.empty())
+ return Nothing;
+
+ if (expr[0] == '+' || expr[0] == '-')
+ return flags_from_delta_expr(
+ expr, flags.value_or(Flags::None), true);
+ else
+ return flags_from_absolute_expr(expr, true);
+}
+
+/**
+ * Filter out flags which are not in the given category
+ *
+ * @param flags flags
+ * @param cat category
+ *
+ * @return filtered flags
+ */
+constexpr Flags
+flags_filter(Flags flags, MessageFlagCategory cat)
+{
+ for (auto&& info : AllMessageFlagInfos)
+ if (info.category != cat)
+ flags &= ~info.flag;
+ return flags;
+}
+
+/**
+ * Filter out any flags which are _not_ Maildir / Mailfile flags
+ *
+ * @param flags flags
+ *
+ * @return filtered flags
+ */
+constexpr Flags
+flags_maildir_file(Flags flags)
+{
+ for (auto&& info : AllMessageFlagInfos)
+ if (info.category != MessageFlagCategory::Maildir &&
+ info.category != MessageFlagCategory::Mailfile)
+ flags &= ~info.flag;
+ return flags;
+}
+
+
+
+
+/**
+ * Return flags, where flags = new_flags but with unmutable_flag in the
+ * result the same as in old_flags
+ *
+ * @param old_flags
+ * @param new_flags
+ * @param immutable_flag
+ *
+ * @return
+ */
+constexpr Flags
+flags_keep_unmutable(Flags old_flags, Flags new_flags, Flags immutable_flag)
+{
+ if (any_of(old_flags & immutable_flag))
+ return new_flags | immutable_flag;
+ else
+ return new_flags & ~immutable_flag;
+}
+
+
+/**
+ * Get a string representation of flags
+ *
+ * @param flags flags
+ *
+ * @return string as a sequence of message-flag shortcuts
+ */
+std::string to_string(Flags flags);
+
+
+/**
+ * Get a string representation of Flags for fmt
+ *
+ * @param flags flags
+ *
+ * @return string as a sequence of message-flag shortcuts
+ */
+static inline auto format_as(const Flags& flags) {
+ return to_string(flags);
+}
+
+} // namespace Mu
+
+#endif /* MU_FLAGS_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2022-2023 Dirk-Jan C. Binnema <djcb.bulk@gmail.com>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-message-file.hh"
+#include "utils/mu-utils-file.hh"
+
+using namespace Mu;
+
+Result<std::string>
+Mu::maildir_from_path(const std::string& path, const std::string& root)
+{
+ const auto pos = path.find(root);
+ if (pos != 0 || path[root.length()] != '/')
+ return Err(Error{Error::Code::InvalidArgument,
+ "root '{}' is not a root for path '{}'", root, path});
+
+ auto mdir{path.substr(root.length())};
+ auto slash{mdir.rfind('/')};
+
+ if (G_UNLIKELY(slash == std::string::npos) || slash < 4)
+ return Err(Error{Error::Code::InvalidArgument,
+ "invalid path: {}", path});
+ mdir.erase(slash);
+ auto subdir = mdir.data() + slash - 4;
+ if (G_UNLIKELY(strncmp(subdir, "/cur", 4) != 0 && strncmp(subdir, "/new", 4)))
+ return Err(Error::Code::InvalidArgument,
+ "cannot find '/new' or '/cur' - invalid path: {}", path);
+
+ if (mdir.length() == 4)
+ return "/";
+
+ mdir.erase(mdir.length() - 4);
+
+ return Ok(std::move(mdir));
+}
+
+Mu::FileParts
+Mu::message_file_parts(const std::string& file)
+{
+ const auto pos{file.find_last_of(":!;")};
+
+ /* no suffix at all? */
+ if (pos == std::string::npos ||
+ pos > file.length() - 3 ||
+ file[pos + 1] != '2' ||
+ file[pos + 2] != ',')
+ return FileParts{ file, ':', {}};
+
+ return FileParts {
+ file.substr(0, pos),
+ file[pos],
+ file.substr(pos + 3)
+ };
+}
+
+Mu::Result<DirFile>
+Mu::base_message_dir_file(const std::string& path)
+{
+ constexpr auto newdir{"/new"};
+
+ const auto dname{dirname(path)};
+ bool is_new{!!g_str_has_suffix(dname.c_str(), newdir)};
+
+ std::string mdir{dname.substr(0, dname.size() - 4)};
+ return Ok(DirFile{std::move(mdir), basename(path), is_new});
+}
+
+Mu::Result<Mu::Flags>
+Mu::flags_from_path(const std::string& path)
+{ /*
+ * this gets us the source maildir filesystem path, the directory
+ * in which new/ & cur/ lives, and the source file
+ */
+ auto dirfile{base_message_dir_file(path)};
+ if (!dirfile)
+ return Err(std::move(dirfile.error()));
+
+ /* a message under new/ is just.. New. Filename is not considered */
+ if (dirfile->is_new)
+ return Ok(Flags::New);
+
+ /* it's cur/ message, so parse the file name */
+ const auto parts{message_file_parts(dirfile->file)};
+ auto flags{flags_from_absolute_expr(parts.flags_suffix,
+ true/*ignore invalid*/)};
+ if (!flags) {
+ /* LCOV_EXCL_START*/
+ return Err(Error{Error::Code::InvalidArgument,
+ "invalid flags ('{}')", parts.flags_suffix});
+ /* LCOV_EXCL_STOP*/
+ }
+
+ /* of course, only _file_ flags are allowed */
+ return Ok(flags_filter(flags.value(), MessageFlagCategory::Mailfile));
+}
+
+
+#ifdef BUILD_TESTS
+
+#include "utils/mu-test-utils.hh"
+
+static void
+test_maildir_from_path()
+{
+ std::array<std::tuple<std::string, std::string, std::string>, 1> test_cases = {{
+ { "/home/foo/Maildir/hello/cur/msg123", "/home/foo/Maildir", "/hello" }
+ }};
+
+ for(auto&& tcase: test_cases) {
+ const auto res{maildir_from_path(std::get<0>(tcase), std::get<1>(tcase))};
+ assert_valid_result(res);
+ assert_equal(*res, std::get<2>(tcase));
+ }
+
+ g_assert_false(!!maildir_from_path("/home/foo/Maildir/cur/test1", "/home/bar"));
+ g_assert_false(!!maildir_from_path("/x", "/x/y"));
+ g_assert_false(!!maildir_from_path("/home/a/Maildir/b/xxx/test", "/home/a/Maildir"));
+}
+
+static void
+test_base_message_dir_file()
+{
+ struct TestCase {
+ const std::string path;
+ DirFile expected;
+ };
+ std::array<TestCase, 1> test_cases = {{
+ { "/home/djcb/Maildir/foo/cur/msg:2,S",
+ { "/home/djcb/Maildir/foo", "msg:2,S", false } }
+ }};
+ for(auto&& tcase: test_cases) {
+ const auto res{base_message_dir_file(tcase.path)};
+ assert_valid_result(res);
+ assert_equal(res->dir, tcase.expected.dir);
+ assert_equal(res->file, tcase.expected.file);
+ g_assert_cmpuint(res->is_new, ==, tcase.expected.is_new);
+ }
+}
+
+static void
+test_flags_from_path()
+{
+ std::array<std::pair<std::string, Flags>, 5> test_cases = {{
+ {"/home/foo/Maildir/test/cur/123456:2,FSR",
+ (Flags::Replied | Flags::Seen | Flags::Flagged)},
+ {"/home/foo/Maildir/test/new/123456", Flags::New},
+ {/* NOTE: when in new/, the :2,.. stuff is ignored */
+ "/home/foo/Maildir/test/new/123456:2,FR",
+ Flags::New},
+ {"/home/foo/Maildir/test/cur/123456:2,DTP",
+ (Flags::Draft | Flags::Trashed | Flags::Passed)},
+ {"/home/foo/Maildir/test/cur/123456:2,S", Flags::Seen}
+ }};
+
+ for (auto&& tcase: test_cases) {
+ auto res{flags_from_path(tcase.first)};
+ assert_valid_result(res);
+ /* LCOV_EXCL_START*/
+ if (g_test_verbose()) {
+ mu_println("{} -> <{}>", tcase.first,
+ to_string(res.value()));
+ g_assert_true(res.value() == tcase.second);
+ }
+ /*LCOV_EXCL_STOP*/
+ }
+}
+
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/message/file/maildir-from-path",
+ test_maildir_from_path);
+ g_test_add_func("/message/file/base-message-dir-file",
+ test_base_message_dir_file);
+ g_test_add_func("/message/file/flags-from-path", test_flags_from_path);
+
+ return g_test_run();
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb.bulk@gmail.com>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_MESSAGE_FILE_HH__
+#define MU_MESSAGE_FILE_HH__
+
+#include "mu-flags.hh"
+#include <utils/mu-result.hh>
+
+namespace Mu {
+
+/*
+ * The file-components, ie.
+ * 1631819685.fb7b279bbb0a7b66.evergrey:2,RS
+ * => {
+ * "1631819685.fb7b279bbb0a7b66.evergrey",
+ * ':',
+ * "2,",
+ * "RS"
+ * }
+ */
+struct FileParts {
+ std::string base; /**< basename */
+ char separator; /**< separator */
+ std::string flags_suffix; /**< suffix (with flags) */
+};
+
+/**
+ * Get the file-parts for some message-file
+ *
+ * @param file path to some message file (does not have to exist)
+ *
+ * @return FileParts for the message file
+ */
+FileParts message_file_parts(const std::string& file);
+
+
+struct DirFile {
+ std::string dir;
+ std::string file;
+ bool is_new;
+};
+
+/**
+ * Get information about the message file componemts
+ *
+ * @param path message path
+ *
+ * @return the components for the message file or an error.
+ */
+Result<DirFile> base_message_dir_file(const std::string& path);
+
+
+
+/**
+ * Get the Maildir flags from the full path of a mailfile. The flags are as
+ * specified in http://cr.yp.to/proto/maildir.html, plus Flag::New for new
+ * messages, ie the ones that live in new/. The flags are logically OR'ed. Note
+ * that the file does not have to exist; the flags are based on the path only.
+ *
+ * @param pathname of a mailfile; it does not have to refer to an
+ * actual message
+ *
+ * @return the message flags or an error
+ */
+Result<Flags> flags_from_path(const std::string& pathname);
+
+/**
+ * get the maildir for a certain message path, ie, the path *before*
+ * cur/ or new/ and *after* the root.
+ *
+ * @param path path for some message
+ * @param root filesystem root for the maildir
+ *
+ * @return the maildir or an Error
+ */
+Result<std::string> maildir_from_path(const std::string& path,
+ const std::string& root);
+} // Mu
+
+
+#endif /* MU_MESSAGE_FILE_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+
+#include "mu-message-part.hh"
+#include "mu-mime-object.hh"
+#include "utils/mu-utils.hh"
+#include "utils/mu-utils-file.hh"
+#include <string>
+
+using namespace Mu;
+
+MessagePart::MessagePart(const Mu::MimeObject& obj):
+ mime_obj{std::make_unique<Mu::MimeObject>(obj)}
+{}
+
+MessagePart::MessagePart(const MessagePart& other):
+ MessagePart(*other.mime_obj)
+{}
+
+MessagePart::~MessagePart() = default;
+
+const MimeObject&
+MessagePart::mime_object() const noexcept
+{
+ return *mime_obj;
+}
+
+static std::string
+cook(const std::string& fname, const std::vector<char>& forbidden)
+{
+ std::string clean;
+ clean.reserve(fname.length());
+
+ for (auto& c: basename(fname))
+ if (seq_some(forbidden,[&](char fc){return ::iscntrl(c) || c == fc;}))
+ clean += '-';
+ else
+ clean += c;
+
+ if (clean[0] == '.' && (clean == "." || clean == ".."))
+ return "-";
+ else
+ return clean;
+}
+
+static std::string
+cook_minimal(const std::string& fname)
+{
+ return cook(fname, { '/' });
+}
+
+static std::string
+cook_full(const std::string& fname)
+{
+ auto cooked = cook(fname, { '/', ' ', '\\', ':' });
+ if (cooked.size() > 1 && cooked[0] == '-')
+ cooked.erase(0, 1);
+
+ return cooked;
+}
+
+Option<std::string>
+MessagePart::cooked_filename(bool minimal) const noexcept
+{
+ auto&& cooker{minimal ? cook_minimal : cook_full};
+
+ // a MimePart... use the name if there is one.
+ if (mime_object().is_part())
+ return MimePart{mime_object()}.filename().map(cooker);
+
+ // MimeMessagepart. Construct a name based on subject.
+ if (mime_object().is_message_part()) {
+ auto msg{MimeMessagePart{mime_object()}.get_message()};
+ if (!msg)
+ return Nothing;
+ else
+ return msg->subject()
+ .map(cooker)
+ .value_or("no-subject") + ".eml";
+ }
+
+ return Nothing;
+}
+
+Option<std::string>
+MessagePart::raw_filename() const noexcept
+{
+ if (!mime_object().is_part())
+ return Nothing;
+ else
+ return MimePart{mime_object()}.filename();
+}
+
+
+
+Option<std::string>
+MessagePart::mime_type() const noexcept
+{
+ if (const auto ctype{mime_object().content_type()}; ctype)
+ return ctype->media_type() + "/" + ctype->media_subtype();
+ else
+ return Nothing;
+}
+
+Option<std::string>
+MessagePart::content_description() const noexcept
+{
+ if (!mime_object().is_part())
+ return Nothing;
+ else
+ return MimePart{mime_object()}.content_description();
+}
+
+size_t
+MessagePart::size() const noexcept
+{
+ if (!mime_object().is_part())
+ return 0;
+ else
+ return MimePart{mime_object()}.size();
+}
+
+bool
+MessagePart::is_attachment() const noexcept
+{
+ if (!mime_object().is_part())
+ return false;
+ else
+ return MimePart{mime_object()}.is_attachment();
+}
+
+
+Option<std::string>
+MessagePart::to_string() const noexcept
+{
+ if (mime_object().is_part())
+ return MimePart{mime_object()}.to_string();
+ else
+ return mime_object().to_string_opt();
+}
+
+Result<size_t>
+MessagePart::to_file(const std::string& path, bool overwrite) const noexcept
+{
+ if (mime_object().is_part())
+ return MimePart{mime_object()}.to_file(path, overwrite);
+ else if (mime_object().is_message_part()) {
+ if (auto&& msg{MimeMessagePart{mime_object()}.get_message()}; !msg)
+ return Err(Error::Code::Message, "failed to get message-part");
+ else
+ return msg->to_file(path, overwrite);
+ } else
+ return mime_object().to_file(path, overwrite);
+}
+
+bool
+MessagePart::is_signed() const noexcept
+{
+ return mime_object().is_multipart_signed();
+}
+
+bool
+MessagePart::is_encrypted() const noexcept
+{
+ return mime_object().is_multipart_encrypted();
+}
+
+bool /* heuristic */
+MessagePart::looks_like_attachment() const noexcept
+{
+ auto matches=[](const MimeContentType& ctype,
+ const std::initializer_list<std::pair<const char*, const char*>>& ctypes) {
+ return std::find_if(ctypes.begin(), ctypes.end(), [&](auto&& item){
+ return ctype.is_type(item.first, item.second); }) != ctypes.end();
+ };
+
+ const auto ctype{mime_object().content_type()};
+ if (!ctype)
+ return false; // no content-type: not an attachment.
+
+ // we consider some parts _not_ to be attachments regardless of disposition
+ if (matches(*ctype,{{"application", "pgp-keys"}}))
+ return false;
+
+ // we consider some parts to be attachments regardless of disposition
+ if (matches(*ctype,{{"image", "*"},
+ {"audio", "*"},
+ {"application", "*"},
+ {"application", "x-patch"}}))
+ return true;
+
+ // otherwise, rely on the disposition
+ return is_attachment();
+}
+
+
+
+#ifdef BUILD_TESTS
+#include "utils/mu-test-utils.hh"
+
+static void
+test_cooked_full()
+{
+ std::array<std::pair<std::string, std::string>, 4> cases = {{
+ { "/hello/world/foo", "foo" },
+ { "foo:/\n/bar", "bar"},
+ { "Aap Noot Mies", "Aap-Noot-Mies"},
+ { "..", "-"}
+ }};
+
+ for (auto&& test: cases)
+ assert_equal(cook_full(test.first), test.second);
+}
+
+static void
+test_cooked_minimal()
+{
+ std::array<std::pair<std::string, std::string>, 4> cases = {{
+ { "/hello/world/foo", "foo" },
+ { "foo:/\n/bar", "bar"},
+ { "Aap Noot Mies.doc", "Aap Noot Mies.doc"},
+ { "..", "-"}
+ }};
+
+ for (auto&& test: cases)
+ assert_equal(cook_minimal(test.first), test.second);
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/message/message-part/cooked-full", test_cooked_full);
+ g_test_add_func("/message/message-part/cooked-minimal", test_cooked_minimal);
+
+ return g_test_run();
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+
+#ifndef MU_MESSAGE_PART_HH__
+#define MU_MESSAGE_PART_HH__
+
+#include <string>
+#include <memory>
+#include <utils/mu-option.hh>
+#include <utils/mu-result.hh>
+
+namespace Mu {
+
+class MimeObject; // forward declaration; don't want to include for build-time
+ // reasons.
+
+class MessagePart {
+public:
+ /**
+ * Construct MessagePart from a MimeObject
+ *
+ * @param obj
+ */
+ MessagePart(const MimeObject& obj);
+
+ /**
+ * Copy CTOR
+ *
+ * @param other
+ */
+ MessagePart(const MessagePart& other);
+
+ /**
+ * DTOR
+ *
+ */
+ ~MessagePart();
+
+ /**
+ * Get the underlying MimeObject; you need to include mu-mime-object.hh
+ * to do anything useful with it.
+ *
+ * @return reference to the mime-object
+ */
+ const MimeObject& mime_object() const noexcept;
+
+ /**
+ * Filename for the mime-part file. This is a "cooked" filename with
+ * unallowed characters removed. If there's no filename specified,
+ * construct one (such as in the case of a MimeMessagePart).
+ *
+ * @param minimal if true, only perform *minimal* cookiing, where we
+ * only remove forward-slashes.
+ *
+ * @see raw_filename()
+ *
+ * @return the name
+ */
+ Option<std::string> cooked_filename(bool minimal=false) const noexcept;
+
+ /**
+ * Name for the mime-part file, i.e., MimePart::filename
+ *
+ * @return the filename or Nothing if there is none
+ */
+ Option<std::string> raw_filename() const noexcept;
+
+ /**
+ * Mime-type for the mime-part (e.g. "text/plain")
+ *
+ * @return the mime-part or Nothing if there is none
+ */
+ Option<std::string> mime_type() const noexcept;
+
+
+ /**
+ * Get the content description for this part, or Nothing
+ *
+ * @return the content description
+ */
+ Option<std::string> content_description() const noexcept;
+
+ /**
+ * Get the length of the (unencoded) MIME-part.
+ *
+ * @return the size
+ */
+ size_t size() const noexcept;
+
+ /**
+ * Does this part have an "attachment" disposition? Otherwise it is
+ * "inline". Note that does *not* map 1:1 to a message's HasAttachment
+ * flag (which uses looks_like_attachment())
+ *
+ * @return true or false.
+ */
+ bool is_attachment() const noexcept;
+
+
+ /**
+ * Does this part appear to be an attachment from an end-users point of
+ * view? This uses some heuristics to guess. Some parts for which
+ * is_attachment() is true may not "really" be attachments, and
+ * vice-versa
+ *
+ * @return true or false.
+ */
+ bool looks_like_attachment() const noexcept;
+
+ /**
+ * Is this part signed?
+ *
+ * @return true or false
+ */
+ bool is_signed() const noexcept;
+
+
+ /**
+ * Is this part encrypted?
+ *
+ * @return true or false
+ */
+ bool is_encrypted() const noexcept;
+
+
+ /**
+ * Write (decoded) mime-part contents to string
+ *
+ * @return a string or nothing if there is no contemt
+ */
+ Option<std::string> to_string() const noexcept;
+
+ /**
+ * Write (decoded) mime part to a file
+ *
+ * @param path path to file
+ * @param overwrite whether to possibly overwrite
+ *
+ * @return size of file or or an error.
+ */
+ Result<size_t> to_file(const std::string& path, bool overwrite) const noexcept;
+
+ struct Private;
+private:
+ const std::unique_ptr<MimeObject> mime_obj;
+};
+
+} // namespace Mu
+
+#endif /* MU_MESSAGE_PART_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2022-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include "config.h"
+
+#include "mu-message.hh"
+#include "gmime/gmime-references.h"
+#include "gmime/gmime-stream-mem.h"
+#include "mu-maildir.hh"
+
+#include <array>
+#include <string>
+#include <regex>
+#include <utils/mu-utils.hh>
+#include <utils/mu-error.hh>
+#include <utils/mu-option.hh>
+#include <utils/mu-lang-detector.hh>
+
+#include <atomic>
+#include <mutex>
+#include <cstdlib>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <gmime/gmime.h>
+
+#include "gmime/gmime-message.h"
+#include "mu-mime-object.hh"
+
+using namespace Mu;
+
+struct Message::Private {
+ Private(Message::Options options):
+ opts{options}, doc{doc_opts(opts)} {}
+ Private(Message::Options options, Xapian::Document&& xdoc):
+ opts{options}, doc{std::move(xdoc), doc_opts(opts)} {}
+
+ Message::Options opts;
+ Document doc;
+ mutable Option<MimeMessage> mime_msg;
+
+ Flags flags{};
+ Option<std::string> mailing_list;
+ std::vector<Part> parts;
+
+ ::time_t ctime{};
+
+ std::string cache_path;
+ /*
+ * we only need to index these, so we don't
+ * really need these copy if we re-arrange things
+ * a bit
+ */
+ Option<std::string> body_txt;
+ Option<std::string> body_html;
+ Option<std::string> embedded;
+
+ Option<std::string> language; /* body ISO language code */
+
+private:
+ Document::Options doc_opts(Message::Options mopts) {
+ return any_of(opts & Message::Options::SupportNgrams) ?
+ Document::Options::SupportNgrams :
+ Document::Options::None;
+ }
+};
+
+
+static void fill_document(Message::Private& priv);
+
+static Result<struct stat>
+get_statbuf(const std::string& path, Message::Options opts = Message::Options::None)
+{
+ if (none_of(opts & Message::Options::AllowRelativePath) &&
+ !g_path_is_absolute(path.c_str()))
+ return Err(Error::Code::File, "path '{}' is not absolute", path);
+
+ if (::access(path.c_str(), R_OK) != 0)
+ return Err(Error::Code::File, "file @ '{}' is not readable", path);
+
+ struct stat statbuf{};
+ if (::stat(path.c_str(), &statbuf) < 0)
+ return Err(Error::Code::File, "cannot stat {}: {}", path,
+ g_strerror(errno));
+
+ if (!S_ISREG(statbuf.st_mode))
+ return Err(Error::Code::File, "not a regular file: {}", path);
+
+ return Ok(std::move(statbuf));
+}
+
+
+Message::Message(const std::string& path, Message::Options opts):
+ priv_{std::make_unique<Private>(opts)}
+{
+ const auto statbuf{get_statbuf(path, opts)};
+ if (!statbuf)
+ throw statbuf.error();
+
+ priv_->ctime = statbuf->st_ctime;
+
+ init_gmime();
+ if (auto msg{MimeMessage::make_from_file(path)}; !msg)
+ throw msg.error();
+ else
+ priv_->mime_msg = std::move(msg.value());
+
+ auto xpath{to_string_opt_gchar(g_canonicalize_filename(path.c_str(), NULL))};
+ if (xpath)
+ priv_->doc.add(Field::Id::Path, std::move(xpath.value()));
+
+ priv_->doc.add(Field::Id::Size, static_cast<int64_t>(statbuf->st_size));
+
+ // rest of the fields
+ fill_document(*priv_);
+}
+
+Message::Message(const std::string& text, const std::string& path,
+ Message::Options opts):
+ priv_{std::make_unique<Private>(opts)}
+{
+ if (text.empty())
+ throw Error{Error::Code::InvalidArgument, "text must not be empty"};
+
+ if (!path.empty()) {
+ auto xpath{to_string_opt_gchar(g_canonicalize_filename(path.c_str(), {}))};
+ if (xpath)
+ priv_->doc.add(Field::Id::Path, std::move(xpath.value()));
+ }
+
+ priv_->ctime = ::time({});
+
+ priv_->doc.add(Field::Id::Size, static_cast<int64_t>(text.size()));
+
+ init_gmime();
+ if (auto msg{MimeMessage::make_from_text(text)}; !msg)
+ throw msg.error();
+ else
+ priv_->mime_msg = std::move(msg.value());
+
+ fill_document(*priv_);
+}
+
+
+Message::Message(Message&& other) noexcept
+{
+ *this = std::move(other);
+}
+
+Message&
+Message::operator=(Message&& other) noexcept
+{
+ if (this != &other)
+ priv_ = std::move(other.priv_);
+
+ return *this;
+}
+
+Message::Message(Xapian::Document&& doc):
+ priv_{std::make_unique<Private>(Message::Options::None, std::move(doc))}
+{}
+
+
+Message::~Message() = default;
+
+const Mu::Document&
+Message::document() const
+{
+ return priv_->doc;
+}
+
+Message::Options
+Message::options() const
+{
+ return priv_->opts;
+}
+
+unsigned
+Message::docid() const
+{
+ return priv_->doc.xapian_document().get_docid();
+}
+
+
+const Mu::Sexp&
+Message::sexp() const
+{
+ return priv_->doc.sexp();
+}
+
+Result<void>
+Message::set_maildir(const std::string& maildir)
+{
+ /* sanity check a little bit */
+ if (maildir.empty() ||
+ maildir.at(0) != '/' ||
+ (maildir.size() > 1 && maildir.at(maildir.length()-1) == '/'))
+ return Err(Error::Code::Message,
+ "'{}' is not a valid maildir", maildir.c_str());
+
+ const auto path{document().string_value(Field::Id::Path)};
+ if (path == maildir || path.find(maildir) == std::string::npos)
+ return Err(Error::Code::Message,
+ "'{}' is not a valid maildir for message @ {}",
+ maildir, path);
+
+ priv_->doc.remove(Field::Id::Maildir);
+ priv_->doc.add(Field::Id::Maildir, maildir);
+
+ return Ok();
+}
+
+void
+Message::set_flags(Flags flags)
+{
+ priv_->doc.remove(Field::Id::Flags);
+ priv_->doc.add(flags);
+}
+
+bool
+Message::load_mime_message(bool reload) const
+{
+ if (priv_->mime_msg && !reload)
+ return true;
+
+ const auto path{document().string_value(Field::Id::Path)};
+ if (auto mime_msg{MimeMessage::make_from_file(path)}; !mime_msg) {
+ mu_warning("failed to load '{}': {}",
+ path, mime_msg.error().what());
+ return false;
+ } else {
+ priv_->mime_msg = std::move(mime_msg.value());
+ fill_document(*priv_);
+ return true;
+ }
+}
+
+void
+Message::unload_mime_message() const
+{
+ priv_->mime_msg = Nothing;
+}
+
+bool
+Message::has_mime_message() const
+{
+ return !!priv_->mime_msg;
+}
+
+
+static Priority
+get_priority(const MimeMessage& mime_msg)
+{
+ constexpr std::array<std::pair<std::string_view, Priority>, 10>
+ prio_alist = {{
+ {"high", Priority::High},
+ {"1", Priority::High},
+ {"2", Priority::High},
+
+ {"normal", Priority::Normal},
+ {"3", Priority::Normal},
+
+ {"low", Priority::Low},
+ {"list", Priority::Low},
+ {"bulk", Priority::Low},
+ {"4", Priority::Low},
+ {"5", Priority::Low}
+ }};
+
+ const auto opt_str = mime_msg.header("Precedence")
+ .disjunction(mime_msg.header("X-Priority"))
+ .disjunction(mime_msg.header("Importance"));
+
+ if (!opt_str)
+ return Priority::Normal;
+
+ const auto it = seq_find_if(prio_alist, [&](auto&& item) {
+ return g_ascii_strncasecmp(item.first.data(), opt_str->c_str(),
+ item.first.size()) == 0; });
+
+ return it == prio_alist.cend() ? Priority::Normal : it->second;
+}
+
+
+/* see: http://does-not-exist.org/mail-archives/mutt-dev/msg08249.html */
+static std::vector<std::string>
+extract_tags(const MimeMessage& mime_msg)
+{
+ constexpr std::array<std::pair<const char*, char>, 3> tag_headers = {{
+ {"X-Label", ' '}, {"X-Keywords", ','}, {"Keywords", ' '}
+ }};
+
+ std::vector<std::string> tags;
+ seq_for_each(tag_headers, [&](auto&& item) {
+ if (auto&& hdr = mime_msg.header(item.first); hdr) {
+ for (auto&& tagval : split(*hdr, item.second)) {
+ tagval.erase(0, tagval.find_first_not_of(' '));
+ tagval.erase(tagval.find_last_not_of(' ')+1);
+ tags.emplace_back(std::move(tagval));
+ }
+ }
+ });
+
+ return tags;
+}
+
+static Option<std::string>
+get_mailing_list(const MimeMessage& mime_msg)
+{
+ char *dechdr, *res;
+ const char *b, *e;
+
+ const auto hdr{mime_msg.header("List-Id")};
+ if (!hdr) {
+ /* some marketing messages don't have a List-Id, but _do_ have a
+ * List-Unsubscribe; if so, return an empty string here, so this
+ * message is still flagged as "MailingList"
+ */
+ if (const auto lu = mime_msg.header("List-Unsubscribe"); !!lu)
+ return "";
+ else
+ return Nothing;
+ }
+
+ dechdr = g_mime_utils_header_decode_phrase(NULL, hdr->c_str());
+ if (!dechdr)
+ return {};
+
+ e = NULL;
+ b = ::strchr(dechdr, '<');
+ if (b)
+ e = strchr(b, '>');
+
+ if (b && e)
+ res = g_strndup(b + 1, e - b - 1);
+ else
+ res = g_strdup(dechdr);
+
+ g_free(dechdr);
+
+ return to_string_opt_gchar(std::move(res));
+}
+
+static void
+append_text(Option<std::string>& str, Option<std::string>&& app)
+{
+ if (!str && app)
+ str = std::move(*app);
+ else if (str && app)
+ str.value() += app.value();
+}
+
+static void
+accumulate_text(const MimePart& part, Message::Private& info,
+ const MimeContentType& ctype)
+{
+ if (!ctype.is_type("text", "*"))
+ return; /* not a text type */
+
+ if (part.is_attachment())
+ append_text(info.embedded, part.to_string());
+ else if (ctype.is_type("text", "plain"))
+ append_text(info.body_txt, part.to_string());
+ else if (ctype.is_type("text", "html"))
+ append_text(info.body_html, part.to_string());
+}
+
+
+static bool /* heuristic */
+looks_like_attachment(const MimeObject& parent, const MessagePart& mpart)
+{
+ if (parent) { /* crypto multipart children are not considered attachments */
+ if (const auto parent_ctype{parent.content_type()}; parent_ctype) {
+ if (parent_ctype->is_type("multipart", "signed") ||
+ parent_ctype->is_type("multipart", "encrypted"))
+ return false;
+ }
+ }
+
+ return mpart.looks_like_attachment();
+}
+
+
+static void
+process_part(const MimeObject& parent, const MimePart& part,
+ Message::Private& info, const MessagePart& mpart)
+{
+ const auto ctype{part.content_type()};
+ if (!ctype)
+ return;
+
+ // flag as calendar, if not already
+ if (none_of(info.flags & Flags::Calendar) &&
+ ctype->is_type("text", "calendar"))
+ info.flags |= Flags::Calendar;
+
+ // flag as attachment, if not already.
+ if (none_of(info.flags & Flags::HasAttachment) &&
+ looks_like_attachment(parent, mpart))
+ info.flags |= Flags::HasAttachment;
+
+ // if there are text parts, gather.
+ accumulate_text(part, info, *ctype);
+}
+
+
+static void
+process_message_part(const MimeMessagePart& msg_part,
+ Message::Private& info)
+{
+ auto submsg{msg_part.get_message()};
+ if (!submsg)
+ return;
+
+ submsg->for_each([&](auto&& parent, auto&& child_obj) {
+ /* NOTE: we only handle one level; ideally, we'd apply the whole
+ parsing machinery recursively; so this a little crude. */
+ if (!child_obj.is_part())
+ return;
+ if (const auto ctype{child_obj.content_type()}; !ctype)
+ return;
+ else if (ctype->is_type("text", "plain"))
+ append_text(info.embedded, MimePart{child_obj}.to_string());
+ else if (ctype->is_type("text", "html")) {
+ if (auto&& str{MimePart{child_obj}.to_string()}; str)
+ append_text(info.embedded, html_to_text(*str));
+ }
+ });
+}
+
+static void
+handle_object(const MimeObject& parent,
+ const MimeObject& obj, Message::Private& info);
+
+
+static void
+handle_encrypted(const MimeMultipartEncrypted& part, Message::Private& info)
+{
+ if (!any_of(info.opts & Message::Options::Decrypt)) {
+ /* just added to the list */
+ info.parts.emplace_back(part);
+ return;
+ }
+
+ const auto proto{part.content_type_parameter("protocol").value_or("unknown")};
+ const auto ctx = MimeCryptoContext::make(proto);
+ if (!ctx) {
+ mu_warning("failed to create context for protocol <{}>", proto);
+ return;
+ }
+
+ auto res{part.decrypt(*ctx)};
+ if (!res) {
+ mu_warning("failed to decrypt: {}", res.error().what());
+ return;
+ }
+
+ if (res->first.is_multipart()) {
+ MimeMultipart{res->first}.for_each(
+ [&](auto&& parent, auto&& child_obj) {
+ handle_object(parent, child_obj, info);
+ });
+
+ } else
+ handle_object(part, res->first, info);
+}
+
+
+static void
+handle_object(const MimeObject& parent,
+ const MimeObject& obj, Message::Private& info)
+{
+ /* if it's an encrypted part we should decrypt, recurse */
+ if (obj.is_multipart_encrypted())
+ handle_encrypted(MimeMultipartEncrypted{obj}, info);
+ else if (obj.is_part() ||
+ obj.is_message_part() ||
+ obj.is_multipart_signed() ||
+ obj.is_multipart_encrypted())
+ info.parts.emplace_back(obj);
+
+ if (obj.is_part())
+ process_part(parent, obj, info, info.parts.back());
+ else if (obj.is_message_part())
+ process_message_part(obj, info);
+ else if (obj.is_multipart_signed())
+ info.flags |= Flags::Signed;
+ else if (obj.is_multipart_encrypted()) {
+ /* FIXME: An encrypted part might be signed at the same time.
+ * In that case the signed flag is lost. */
+ info.flags |= Flags::Encrypted;
+ } else if (obj.is_mime_application_pkcs7_mime()) {
+ MimeApplicationPkcs7Mime smime(obj);
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wswitch-enum"
+ // CompressedData, CertsOnly, Unknown
+ switch (smime.smime_type()) {
+ case Mu::MimeApplicationPkcs7Mime::SecureMimeType::SignedData:
+ info.flags |= Flags::Signed;
+ break;
+ case Mu::MimeApplicationPkcs7Mime::SecureMimeType::EnvelopedData:
+ info.flags |= Flags::Encrypted;
+ break;
+ default:
+ break;
+ }
+#pragma GCC diagnostic pop
+ }
+}
+
+/**
+ * This message -- recursively walk through message, and initialize some
+ * other values that depend on another.
+ *
+ * @param mime_msg
+ * @param path
+ * @param info
+ */
+static void
+process_message(const MimeMessage& mime_msg, const std::string& path,
+ Message::Private& info)
+{
+ /* only have file-flags when there's a path. */
+ if (!path.empty()) {
+ info.flags = flags_from_path(path).value_or(Flags::None);
+ /* pseudo-flag --> unread means either NEW or NOT SEEN, just
+ * for searching convenience */
+ if (any_of(info.flags & Flags::New) || none_of(info.flags & Flags::Seen))
+ info.flags |= Flags::Unread;
+ }
+
+ // parts
+ mime_msg.for_each([&](auto&& parent, auto&& child_obj) {
+ handle_object(parent, child_obj, info);
+ });
+
+ // get the mailing here, and use it do update flags, too.
+ info.mailing_list = get_mailing_list(mime_msg);
+ if (info.mailing_list)
+ info.flags |= Flags::MailingList;
+
+#ifdef HAVE_CLD2
+ /* language detection requires the cld2 lib */
+ if (info.body_txt) { /* attempt to get the body-language */
+ if (const auto lang{detect_language(info.body_txt.value())}; lang) {
+ info.language = lang->code;
+ mu_debug("detected language: {}", lang->code);
+ } else
+ mu_debug("could not detect language");
+ }
+#endif /*HAVE_CLD2*/
+}
+
+static Mu::Result<std::string>
+calculate_sha256(const std::string& path)
+{
+ g_autoptr(GChecksum) checksum{g_checksum_new(G_CHECKSUM_SHA256)};
+
+ FILE *file{::fopen(path.c_str(), "r")};
+ if (!file)
+ return Err(Error{Error::Code::File, "failed to open {}: {}",
+ path, ::strerror(errno)});
+
+ std::array<uint8_t, 4096> buf{};
+ while (true) {
+ const auto n = ::fread(buf.data(), 1, buf.size(), file);
+ if (n == 0)
+ break;
+ g_checksum_update(checksum, buf.data(), n);
+ }
+
+ bool has_err = ::ferror(file) != 0;
+ ::fclose(file);
+
+ if (has_err)
+ return Err(Error{Error::Code::File, "failed to read {}", path});
+
+ return Ok(g_checksum_get_string(checksum));
+}
+
+/**
+ * Get a fake-message-id for a message without one.
+ *
+ * @param path message path
+ *
+ * @return a fake message-id
+ */
+static std::string
+fake_message_id(const std::string& path)
+{
+ constexpr auto mu_suffix{"@mu.id"};
+
+ // not a very good message-id, only for testing.
+ if (path.empty() || ::access(path.c_str(), R_OK) != 0)
+ return mu_format("{:08x}{}", g_str_hash(path.c_str()), mu_suffix);
+ if (const auto sha256_res{calculate_sha256(path)}; !sha256_res)
+ return mu_format("{:08x}{}", g_str_hash(path.c_str()), mu_suffix);
+ else
+ return mu_format("{}{}", sha256_res.value(), mu_suffix);
+}
+
+/* many of the doc.add(fields ....) automatically update the sexp-list as well;
+ * however, there are some _extra_ values in the sexp-list that are not
+ * based on a field. So we add them here.
+ */
+
+
+static void
+doc_add_list_post(Document& doc, const MimeMessage& mime_msg)
+{
+ /* some mailing lists do not set the reply-to; see pull #1278. So for
+ * those cases, check the List-Post address and use that instead */
+
+ GMatchInfo* minfo;
+ GRegex* rx;
+ const auto list_post{mime_msg.header("List-Post")};
+ if (!list_post)
+ return;
+
+ rx = g_regex_new("<?mailto:([a-z0-9!@#$%&'*+-/=?^_`{|}~]+)>?",
+ G_REGEX_CASELESS, (GRegexMatchFlags)0, {});
+ g_return_if_fail(rx);
+
+ Contacts contacts;
+ if (g_regex_match(rx, list_post->c_str(), (GRegexMatchFlags)0, &minfo)) {
+ auto address = (char*)g_match_info_fetch(minfo, 1);
+ contacts.push_back(Contact(address));
+ g_free(address);
+ }
+
+ g_match_info_free(minfo);
+ g_regex_unref(rx);
+
+ doc.add_extra_contacts(":list-post", contacts);
+}
+
+static void
+doc_add_reply_to(Document& doc, const MimeMessage& mime_msg)
+{
+ doc.add_extra_contacts(":reply-to", mime_msg.contacts(Contact::Type::ReplyTo));
+}
+
+static void
+fill_document(Message::Private& priv)
+{
+ /* hunt & gather info from message tree */
+ Document& doc{priv.doc};
+ MimeMessage& mime_msg{priv.mime_msg.value()};
+
+ const auto path{doc.string_value(Field::Id::Path)};
+ const auto refs{mime_msg.references()};
+ const auto& raw_message_id = mime_msg.message_id();
+ const auto message_id = raw_message_id.has_value() && !raw_message_id->empty()
+ ? *raw_message_id
+ : fake_message_id(path);
+
+ process_message(mime_msg, path, priv);
+
+ doc_add_list_post(doc, mime_msg); /* only in sexp */
+ doc_add_reply_to(doc, mime_msg); /* only in sexp */
+
+ field_for_each([&](auto&& field) {
+ /* insist on explicitly handling each */
+#pragma GCC diagnostic push
+#pragma GCC diagnostic error "-Wswitch"
+ switch(field.id) {
+ case Field::Id::Bcc:
+ doc.add(field.id, mime_msg.contacts(Contact::Type::Bcc));
+ break;
+ case Field::Id::BodyText:
+ doc.add(field.id, priv.body_txt);
+ if (priv.body_html)
+ doc.add(field.id, html_to_text(*priv.body_html));
+ break;
+ case Field::Id::Cc:
+ doc.add(field.id, mime_msg.contacts(Contact::Type::Cc));
+ break;
+ case Field::Id::Changed:
+ doc.add(field.id, priv.ctime);
+ break;
+ case Field::Id::Date:
+ doc.add(field.id, mime_msg.date());
+ break;
+ case Field::Id::EmbeddedText:
+ doc.add(field.id, priv.embedded);
+ break;
+ case Field::Id::File:
+ for (auto&& part: priv.parts)
+ doc.add(field.id, part.raw_filename());
+ break;
+ case Field::Id::Flags:
+ doc.add(priv.flags);
+ break;
+ case Field::Id::From:
+ doc.add(field.id, mime_msg.contacts(Contact::Type::From));
+ break;
+ case Field::Id::Language:
+ doc.add(field.id, priv.language);
+ break;
+ case Field::Id::Maildir: /* already */
+ break;
+ case Field::Id::MailingList:
+ doc.add(field.id, priv.mailing_list);
+ break;
+ case Field::Id::MessageId:
+ doc.add(field.id, message_id);
+ break;
+ case Field::Id::MimeType:
+ for (auto&& part: priv.parts)
+ doc.add(field.id, part.mime_type());
+ break;
+ case Field::Id::Path: /* already */
+ break;
+ case Field::Id::Priority:
+ doc.add(get_priority(mime_msg));
+ break;
+ case Field::Id::References:
+ if (!refs.empty())
+ doc.add(field.id, refs);
+ break;
+ case Field::Id::Size: /* already */
+ break;
+ case Field::Id::Subject:
+ doc.add(field.id, mime_msg.subject().map(remove_ctrl));
+ break;
+ case Field::Id::Tags:
+ if (auto&& tags{extract_tags(mime_msg)}; !tags.empty())
+ doc.add(field.id, tags);
+ break;
+ case Field::Id::ThreadId:
+ // either the oldest reference, or otherwise the message id
+ doc.add(field.id, refs.empty() ? message_id : refs.at(0));
+ break;
+ case Field::Id::To:
+ doc.add(field.id, mime_msg.contacts(Contact::Type::To));
+ break;
+ /* LCOV_EXCL_START */
+ case Field::Id::_count_:
+ default:
+ break;
+ /* LCOV_EXCL_STOP */
+ }
+#pragma GCC diagnostic pop
+
+ });
+}
+
+Option<std::string>
+Message::header(const std::string& header_field) const
+{
+ load_mime_message();
+ return priv_->mime_msg->header(header_field);
+}
+
+Option<std::string>
+Message::body_text() const
+{
+ load_mime_message();
+ return priv_->body_txt;
+}
+
+Option<std::string>
+Message::body_html() const
+{
+ load_mime_message();
+ return priv_->body_html;
+}
+
+Contacts
+Message::all_contacts() const
+{
+ Contacts contacts;
+
+ if (!load_mime_message())
+ return contacts; /* empty */
+
+ return priv_->mime_msg->contacts(Contact::Type::None); /* get all types */
+}
+
+const std::vector<Message::Part>&
+Message::parts() const
+{
+ if (!load_mime_message()) {
+ static std::vector<Message::Part> empty;
+ return empty;
+ }
+
+ return priv_->parts;
+}
+
+Result<std::string>
+Message::cache_path(Option<size_t> index) const
+{
+ /* create tmpdir for this message, if needed */
+ if (priv_->cache_path.empty()) {
+ GError *err{};
+ auto tpath{to_string_opt_gchar(g_dir_make_tmp("mu-cache-XXXXXX", &err))};
+ if (!tpath)
+ return Err(Error::Code::File, &err, "failed to create temp dir");
+
+ priv_->cache_path = std::move(tpath.value());
+ }
+
+ if (index) {
+ GError *err{};
+ auto tpath = mu_format("{}/{}", priv_->cache_path, *index);
+ if (g_mkdir(tpath.c_str(), 0700) != 0)
+ return Err(Error::Code::File, &err,
+ "failed to create cache dir '{}'; err={}", tpath, errno);
+ return Ok(std::move(tpath));
+ } else
+
+ return Ok(std::string{priv_->cache_path});
+}
+
+// for now this only remove stray '/' at the end
+std::string
+Message::sanitize_maildir(const std::string& mdir)
+{
+ if (mdir.size() > 1 && mdir.at(mdir.length()-1) == '/')
+ return mdir.substr(0, mdir.length() - 1);
+ else
+ return mdir;
+}
+
+Result<void>
+Message::update_after_move(const std::string& new_path,
+ const std::string& new_maildir,
+ Flags new_flags)
+{
+ if (auto statbuf{get_statbuf(new_path)}; !statbuf)
+ return Err(statbuf.error());
+ else
+ priv_->ctime = statbuf->st_ctime;
+
+ priv_->doc.remove(Field::Id::Path);
+ priv_->doc.remove(Field::Id::Changed);
+
+ priv_->doc.add(Field::Id::Path, new_path);
+ priv_->doc.add(Field::Id::Changed, priv_->ctime);
+
+ set_flags(new_flags);
+
+ if (const auto res = set_maildir(sanitize_maildir(new_maildir)); !res)
+ return res;
+
+ return Ok();
+}
--- /dev/null
+/*
+** Copyright (C) 2022-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_MESSAGE_HH__
+#define MU_MESSAGE_HH__
+
+#include <memory>
+#include <string>
+#include <vector>
+#include <iostream>
+
+#include "mu-xapian-db.hh"
+
+#include "mu-contact.hh"
+#include "mu-priority.hh"
+#include "mu-flags.hh"
+#include "mu-fields.hh"
+#include "mu-document.hh"
+#include "mu-message-part.hh"
+#include "mu-message-file.hh"
+
+#include "utils/mu-utils.hh"
+#include "utils/mu-option.hh"
+#include "utils/mu-result.hh"
+#include "utils/mu-sexp.hh"
+
+namespace Mu {
+
+class Message {
+public:
+ enum struct Options {
+ None = 0, /**< Defaults */
+ Decrypt = 1 << 0, /**< Attempt to decrypt */
+ RetrieveKeys = 1 << 1, /**< Auto-retrieve crypto keys (implies network
+ * access) */
+ AllowRelativePath = 1 << 2, /**< Allow relative paths for filename
+ * in make_from_path */
+ SupportNgrams = 1 << 3, /**< Support ngrams, as used in
+ * CJK and other languages. */
+ };
+
+ /**
+ * Move CTOR
+ *
+ * @param some other message
+ */
+ Message(Message&& other) noexcept;
+
+ /**
+ * operator=
+ *
+ * @param other move some object object
+ *
+ * @return
+ */
+ Message& operator=(Message&& other) noexcept;
+
+ /**
+ * Construct a message based on a path
+ *
+ * @param path path to message
+ * @param opts options
+ *
+ * @return a message or an error
+ */
+ static Result<Message> make_from_path(const std::string& path,
+ Options opts={}) try {
+ return Ok(Message{path,opts});
+ } catch (Error& err) {
+ return Err(err);
+ }
+ /* LCOV_EXCL_START */
+ catch (...) {
+ return Err(Mu::Error(Error::Code::Message,
+ "failed to create message from path"));
+ }
+ /* LCOV_EXCL_STOP */
+
+ /**
+ * Construct a message based on a Xapian::Document
+ *
+ * @param doc a Mu Document
+ *
+ * @return a message or an error
+ */
+ static Result<Message> make_from_document(Xapian::Document&& doc) try {
+ return Ok(Message{std::move(doc)});
+ } catch (Error& err) {
+ return Err(err);
+ }
+ /* LCOV_EXCL_START */
+ catch (...) {
+ return Err(Mu::Error(Error::Code::Message,
+ "failed to create message from document"));
+ }
+ /* LCOV_EXCL_STOP */
+
+ /**
+ * Construct a message from a string. This is mostly useful for testing.
+ *
+ * @param text message text
+ * @param path path to message - optional; path does not have to exist.
+ * @param opts options
+ *
+ * @return a message or an error
+ */
+ static Result<Message> make_from_text(const std::string& text,
+ const std::string& path={},
+ Options opts={}) try {
+ return Ok(Message{text, path, opts});
+ } catch (Error& err) {
+ return Err(err);
+ }
+ /* LCOV_EXCL_START */
+ catch (...) {
+ return Err(Mu::Error(Error::Code::Message,
+ "failed to create message from text"));
+ }
+ /* LCOV_EXCL_STOP */
+
+ /**
+ * DTOR
+ */
+ ~Message();
+
+ /**
+ * Get the document.
+ *
+ *
+ * @return document
+ */
+ const Document& document() const;
+
+
+ /**
+ * The message options for this message
+ *
+ * @return message options
+ */
+ Options options() const;
+
+
+ /**
+ * Get the document-id, or 0 if non-existent.
+ *
+ * @return document id
+ */
+ unsigned docid() const;
+
+ /**
+ * Get the file system path of this message
+ *
+ * @return the path of this Message or NULL in case of error.
+ * the returned string should *not* be modified or freed.
+ */
+ std::string path() const { return document().string_value(Field::Id::Path); }
+
+ /**
+ * Get the sender (From:) of this message
+ *
+ * @return the sender(s) of this Message
+ */
+ Contacts from() const { return document().contacts_value(Field::Id::From); }
+
+ /**
+ * Get the recipient(s) (To:) for this message
+ *
+ * @return recipients
+ */
+ Contacts to() const { return document().contacts_value(Field::Id::To); }
+
+ /**
+ * Get the recipient(s) (Cc:) for this message
+ *
+ * @return recipients
+ */
+ Contacts cc() const { return document().contacts_value(Field::Id::Cc); }
+
+ /**
+ * Get the recipient(s) (Bcc:) for this message
+ *
+ * @return recipients
+ */
+ Contacts bcc() const { return document().contacts_value(Field::Id::Bcc); }
+
+ /**
+ * Get the maildir this message resides in; i.e., if the path is
+ * ~/Maildir/foo/bar/cur/msg, the maildir would typically be foo/bar
+ *
+ * This is determined when _storing_ the message (which uses
+ * set_maildir())
+ *
+ * @return the maildir requested or empty */
+ std::string maildir() const { return document().string_value(Field::Id::Maildir); }
+
+ /**
+ * Set the maildir for this message. This is for use by the _store_ when
+ * it has determined the maildir for this message from the message's path and
+ * the root-maildir known by the store.
+ *
+ * @param maildir the maildir for this message
+ *
+ * @return Ok() or some error if the maildir is invalid
+ */
+ Result<void> set_maildir(const std::string& maildir);
+
+ /**
+ * Clean up the maildir. This is for internal use, but exposed for testing.
+ * For now cleaned-up means "stray trailing / removed".
+ *
+ * @param maildir some maildir
+ *
+ * @return a cleaned-up version
+ */
+ static std::string sanitize_maildir(const std::string& maildir);
+
+ /**
+ * Get the subject of this message
+ *
+ * @return the subject of this Message
+ */
+ std::string subject() const { return document().string_value(Field::Id::Subject); }
+
+ /**
+ * Get the Message-Id of this message
+ *
+ * @return the Message-Id of this message (without the enclosing <>), or
+ * a fake message-id for messages that don't have them.
+ *
+ * For file-backed message, this fake message-id is based on a hash of the
+ * message contents. For non-file-backed (test) messages, some other value
+ * is concocted.
+ */
+ std::string message_id() const { return document().string_value(Field::Id::MessageId);}
+
+ /**
+ * get the mailing list for a message, i.e. the mailing-list
+ * identifier in the List-Id header.
+ *
+ * @return the mailing list id for this message (without the enclosing <>)
+ * or NULL in case of error or if there is none.
+ */
+ std::string mailing_list() const { return document().string_value(Field::Id::MailingList);}
+
+ /**
+ * get the message date/time (the Date: field) as time_t
+ *
+ * @return message date/time or 0 in case of error or if there
+ * is no such header.
+ */
+ ::time_t date() const {
+ return static_cast<::time_t>(document().integer_value(Field::Id::Date));
+ }
+
+ /**
+ * get the last change-time this message. For path/document-based
+ * messages this corresponds with the ctime of the underlying file; for
+ * the text-based ones (as used for testing) it is the creation time.
+ *
+ * @return last-change time or 0 if unknown
+ */
+ ::time_t changed() const {
+ return static_cast<::time_t>(document().integer_value(Field::Id::Changed));
+ }
+
+ /**
+ * get the flags for this message.
+ *
+ * @return the file/content flags
+ */
+ Flags flags() const { return document().flags_value(); }
+
+
+ /**
+ * Update the flags for this message. This is useful for flags
+ * that can only be determined after the message has been created already,
+ * such as the 'personal' flag.
+ *
+ * @param flags new flags.
+ */
+ void set_flags(Flags flags);
+
+ /**
+ * get the message priority for this message. The X-Priority, X-MSMailPriority,
+ * Importance and Precedence header are checked, in that order. if no known or
+ * explicit priority is set, Priority::Id::Normal is assumed
+ *
+ * @return the message priority
+ */
+ Priority priority() const { return document().priority_value(); }
+
+ /**
+ * get the file size in bytes of this message
+ *
+ * @return the filesize
+ */
+ size_t size() const { return static_cast<size_t>(document().integer_value(Field::Id::Size)); }
+
+ /**
+ * Get the (possibly empty) list of references (consisting of both the
+ * References and In-Reply-To fields), with the oldest first and the
+ * direct parent as the last one. Note, any reference (message-id) will
+ * appear at most once, duplicates and fake-message-id (see impls) are
+ * filtered out.
+ *
+ * @return a vec with the references for this msg.
+ */
+ std::vector<std::string> references() const {
+ return document().string_vec_value(Field::Id::References);
+ }
+
+ /**
+ * Get the thread-id for this message. This is the message-id of the
+ * oldest-known (grand) parent, or the message-id of this message if
+ * none.
+ *
+ * @return the thread id.
+ */
+ std::string thread_id() const {
+ return document().string_value(Field::Id::ThreadId);
+ }
+
+ /**
+ * get the list of tags (ie., X-Label)
+ *
+ * @param msg a valid MuMsg
+ *
+ * @return a list with the tags for this msg. Don't modify/free
+ */
+ std::vector<std::string> tags() const {
+ return document()
+ .string_vec_value(Field::Id::Tags);
+ }
+
+ /*
+ * Convert to Sexp
+ */
+
+ /**
+ * Get the s-expression for this message. Stays valid as long as this
+ * message is.
+ *
+ * @return an Sexp representing the message.
+ */
+ const Sexp& sexp() const;
+
+ /*
+ * And some non-const message, for updating an existing
+ * message after a file-system move.
+ *
+ * @return Ok or an error.
+ */
+ Result<void> update_after_move(const std::string& new_path,
+ const std::string& new_maildir,
+ Flags new_flags);
+ /*
+ * Below require a file-backed message, which is a relatively slow
+ * if there isn't one already; see load_mime_message()
+ */
+
+ /**
+ * Get the text body
+ *
+ * @return text body
+ */
+ Option<std::string> body_text() const;
+
+ /**
+ * Get the HTML body
+ *
+ * @return text body
+ */
+ Option<std::string> body_html() const;
+
+ /**
+ * Get some message-header
+ *
+ * @param header_field name of the header
+ *
+ * @return the value (UTF-8), or Nothing.
+ */
+ Option<std::string> header(const std::string& header_field) const;
+
+
+ /**
+ * Get all contacts for this message.
+ *
+ * @return contacts
+ */
+ Contacts all_contacts() const;
+
+ /**
+ * Get information about MIME-parts in this message.
+ *
+ * @return mime-part info.
+ */
+ using Part = MessagePart;
+ const std::vector<Part>& parts() const;
+
+ /**
+ * Get the path to a cache directory for this message, which is useful
+ * for temporarily saving attachments
+ *
+ * @param index optionally, create <cache-path>/<index> instead;
+ * this is useful for having part-specific subdirectories.
+ *
+ * @return path to a (created) cache directory, or an error.
+ */
+ Result<std::string> cache_path(Option<size_t> index={}) const;
+
+
+ /**
+ * Load the GMime (file) message (for a database-backed message),
+ * if not already (but see @param reload).
+ *
+ * Affects cached-state only, so we still mark this as 'const'
+ *
+ * @param reload whether to force reloading (even if already)
+ *
+ * @return true if loading worked; false otherwise.
+ */
+ bool load_mime_message(bool reload=false) const;
+
+ /**
+ * Clear the GMime message.
+ *
+ * Affects cached-state only, so we still mark this as 'const'
+ */
+ void unload_mime_message() const;
+
+ /**
+ * Has a (file-base) GMime message been loaded?
+ *
+ *
+ * @return true or false
+ */
+ bool has_mime_message() const;
+
+ struct Private;
+
+ /*
+ * Usually the make_ builders are better to create a message, but in
+ * some special cases, we need a heap-allocated message... */
+
+ Message(Xapian::Document&& xdoc);
+ Message(const std::string& path, Options opts);
+
+private:
+ Message(const std::string& str, const std::string& path, Options opt);
+
+ std::unique_ptr<Private> priv_;
+
+}; // Message
+MU_ENABLE_BITOPS(Message::Options);
+
+static inline auto
+format_as(const Message& msg) {
+ return msg.path();
+}
+
+} // Mu
+#endif /* MU_MESSAGE_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2022-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+
+#include "mu-mime-object.hh"
+#include "gmime/gmime-message.h"
+#include "utils/mu-utils.hh"
+#include "utils/mu-utils-file.hh"
+#include <mutex>
+#include <regex>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <errno.h>
+
+using namespace Mu;
+
+
+\f
+/* note, we do the gmime initialization here rather than in mu-runtime, because this way
+ * we don't need mu-runtime for simple cases -- such as our unit tests. Also note that we
+ * need gmime init even for the doc backend, as we use the address parsing functions also
+ * there. */
+
+void
+Mu::init_gmime(void)
+{
+ // fast path.
+ static bool gmime_initialized = false;
+ if (gmime_initialized)
+ return;
+
+ static std::mutex gmime_lock;
+ std::lock_guard lock (gmime_lock);
+ if (gmime_initialized)
+ return; // already
+
+ mu_debug("initializing gmime {}.{}.{}",
+ gmime_major_version,
+ gmime_minor_version,
+ gmime_micro_version);
+
+ g_mime_init();
+ gmime_initialized = true;
+
+ std::atexit([] {
+ mu_debug("shutting down gmime");
+ g_mime_shutdown();
+ gmime_initialized = false;
+ });
+}
+
+
+std::string
+Mu::address_rfc2047(const Contact& contact)
+{
+ init_gmime();
+
+ InternetAddress *addr =
+ internet_address_mailbox_new(contact.name.c_str(),
+ contact.email.c_str());
+
+ std::string encoded = to_string_gchar(
+ internet_address_to_string(addr, {}, true));
+
+ g_object_unref(addr);
+
+ return encoded;
+}
+
+\f
+/*
+ * MimeObject
+ */
+
+Option<std::string>
+MimeObject::header(const std::string& hdr) const noexcept
+{
+ if (auto val{g_mime_object_get_header(self(), hdr.c_str())}; !val)
+ return Nothing;
+ else if (!g_utf8_validate(val, -1, {}))
+ return utf8_clean(val);
+ else
+ return std::string{val};
+}
+
+
+std::vector<std::pair<std::string, std::string>>
+MimeObject::headers() const noexcept
+{
+ GMimeHeaderList *lst;
+
+ lst = g_mime_object_get_header_list(self()); /* _not_ owned */
+ if (!lst)
+ return {};
+
+ std::vector<std::pair<std::string, std::string>> hdrs;
+ const auto hdr_num{g_mime_header_list_get_count(lst)};
+
+ for (int i = 0; i != hdr_num; ++i) {
+ GMimeHeader *hdr{g_mime_header_list_get_header_at(lst, i)};
+ if (!hdr) /* ^^^ _not_ owned */
+ continue;
+ const auto name{g_mime_header_get_name(hdr)};
+ const auto val{g_mime_header_get_value(hdr)};
+ if (!name || !val)
+ continue;
+ hdrs.emplace_back(name, val);
+ }
+
+ return hdrs;
+}
+
+Result<size_t>
+MimeObject::write_to_stream(const MimeFormatOptions& f_opts,
+ MimeStream& stream) const
+{
+ auto written = g_mime_object_write_to_stream(self(), f_opts.get(),
+ GMIME_STREAM(stream.object()));
+ if (written < 0)
+ return Err(Error::Code::File, "failed to write mime-object to stream");
+ else
+ return Ok(static_cast<size_t>(written));
+}
+
+Result<size_t>
+MimeObject::to_file(const std::string& path, bool overwrite) const noexcept
+{
+ GError *err{};
+ auto strm{g_mime_stream_fs_open(path.c_str(),
+ O_WRONLY | O_CREAT | O_TRUNC |(overwrite ? 0 : O_EXCL),
+ S_IRUSR|S_IWUSR,
+ &err)};
+ if (!strm)
+ return Err(Error::Code::File, &err, "failed to open '{}'", path);
+
+ MimeStream stream{MimeStream::make_from_stream(strm)};
+ return write_to_stream({}, stream);
+}
+
+
+Option<std::string>
+MimeObject::to_string_opt() const noexcept
+{
+ auto stream{MimeStream::make_mem()};
+ if (!stream) {
+ mu_warning("failed to create mem stream");
+ return Nothing;
+ }
+
+ const auto written = g_mime_object_write_to_stream(
+ self(), {}, GMIME_STREAM(stream.object()));
+ if (written < 0) {
+ mu_warning("failed to write object to stream");
+ return Nothing;
+ }
+
+ std::string buffer;
+ buffer.resize(written + 1);
+ stream.reset();
+
+ auto bytes{g_mime_stream_read(GMIME_STREAM(stream.object()),
+ buffer.data(), written)};
+ if (bytes < 0)
+ return Nothing;
+
+ buffer.data()[written]='\0';
+ buffer.resize(written);
+
+ return buffer;
+}
+
+\f
+/*
+ * MimeCryptoContext
+ */
+
+Result<size_t>
+MimeCryptoContext::import_keys(MimeStream& stream)
+{
+ GError *err{};
+ auto res = g_mime_crypto_context_import_keys(
+ self(), GMIME_STREAM(stream.object()), &err);
+
+ if (res < 0)
+ return Err(Error::Code::File, &err,
+ "error importing keys");
+
+ return Ok(static_cast<size_t>(res));
+}
+
+void
+MimeCryptoContext::set_request_password(PasswordRequestFunc pw_func)
+{
+ static auto request_func = pw_func;
+
+ g_mime_crypto_context_set_request_password(
+ self(),
+ [](GMimeCryptoContext *ctx,
+ const char *user_id,
+ const char *prompt,
+ gboolean reprompt,
+ GMimeStream *response,
+ GError **err) -> gboolean {
+ MimeStream mstream{MimeStream::make_from_stream(response)};
+
+ auto res = request_func(MimeCryptoContext(ctx),
+ std::string{user_id ? user_id : ""},
+ std::string{prompt ? prompt : ""},
+ !!reprompt,
+ mstream);
+ if (res)
+ return TRUE;
+
+ res.error().fill_g_error(err);
+ return FALSE;
+ });
+
+}
+
+Result<void>
+MimeCryptoContext::setup_gpg_test(const std::string& testpath)
+{
+ /* setup clean environment for testing; inspired by gmime */
+
+ g_setenv ("GNUPGHOME", join_paths(testpath, ".gnupg").c_str(), 1);
+
+ /* disable environment variables that gpg-agent uses for pinentry */
+ g_unsetenv ("DBUS_SESSION_BUS_ADDRESS");
+ g_unsetenv ("DISPLAY");
+ g_unsetenv ("GPG_TTY");
+
+ if (g_mkdir_with_parents((testpath + "/.gnupg").c_str(), 0700) != 0)
+ return Err(Error::Code::File,
+ "failed to create gnupg dir; err={}", errno);
+
+ auto write_gpgfile=[&](const std::string& fname, const std::string& data)
+ -> Result<void> {
+
+ GError *err{};
+ std::string path{mu_format("{}/{}", testpath, fname)};
+ if (!g_file_set_contents(path.c_str(), data.c_str(), data.size(), &err))
+ return Err(Error::Code::File, &err, "failed to write {}", path);
+ else
+ return Ok();
+ };
+
+ // some more elegant way?
+ if (auto&& res = write_gpgfile("gpg.conf", "pinentry-mode loopback\n"); !res)
+ return res;
+ if (auto&& res = write_gpgfile("gpgsm.conf", "disable-crl-checks\n"))
+ return res;
+
+ return Ok();
+}
+
+\f
+/*
+ * MimeMessage
+ */
+
+
+
+static Result<MimeMessage>
+make_from_stream(GMimeStream* &&stream/*consume*/)
+{
+ init_gmime();
+ GMimeParser *parser{g_mime_parser_new_with_stream(stream)};
+ g_object_unref(stream);
+ if (!parser)
+ return Err(Error::Code::Message, "cannot create mime parser");
+
+ GMimeMessage *gmime_msg{g_mime_parser_construct_message(parser, NULL)};
+ g_object_unref(parser);
+ if (!gmime_msg)
+ return Err(Error::Code::Message, "message seems invalid");
+
+ auto mime_msg{MimeMessage{std::move(G_OBJECT(gmime_msg))}};
+ g_object_unref(gmime_msg);
+
+ return Ok(std::move(mime_msg));
+}
+
+Result<MimeMessage>
+MimeMessage::make_from_file(const std::string& path)
+{
+ GError* err{};
+ init_gmime();
+ if (auto&& stream{g_mime_stream_file_open(path.c_str(), "r", &err)}; !stream)
+ return Err(Error::Code::Message, &err,
+ "failed to open stream for {}", path);
+ else
+ return make_from_stream(std::move(stream));
+}
+
+Result<MimeMessage>
+MimeMessage::make_from_text(const std::string& text)
+{
+ init_gmime();
+ if (auto&& stream{g_mime_stream_mem_new_with_buffer(
+ text.c_str(), text.length())}; !stream)
+ return Err(Error::Code::Message,
+ "failed to open stream for string");
+ else
+ return make_from_stream(std::move(stream));
+}
+
+Option<int64_t>
+MimeMessage::date() const noexcept
+{
+ GDateTime *dt{g_mime_message_get_date(self())};
+ if (!dt)
+ return Nothing;
+ else
+ return g_date_time_to_unix(dt);
+}
+
+constexpr Option<GMimeAddressType>
+address_type(Contact::Type ctype)
+{
+ switch(ctype) {
+ case Contact::Type::Bcc:
+ return GMIME_ADDRESS_TYPE_BCC;
+ case Contact::Type::Cc:
+ return GMIME_ADDRESS_TYPE_CC;
+ case Contact::Type::From:
+ return GMIME_ADDRESS_TYPE_FROM;
+ case Contact::Type::To:
+ return GMIME_ADDRESS_TYPE_TO;
+ case Contact::Type::ReplyTo:
+ return GMIME_ADDRESS_TYPE_REPLY_TO;
+ case Contact::Type::Sender:
+ return GMIME_ADDRESS_TYPE_SENDER;
+ case Contact::Type::None:
+ default:
+ return Nothing;
+ }
+}
+
+static Mu::Contacts
+all_contacts(const MimeMessage& msg)
+{
+ Contacts contacts;
+
+ for (auto&& cctype: {
+ Contact::Type::Sender,
+ Contact::Type::From,
+ Contact::Type::ReplyTo,
+ Contact::Type::To,
+ Contact::Type::Cc,
+ Contact::Type::Bcc
+ }) {
+ auto addrs{msg.contacts(cctype)};
+ std::move(addrs.begin(), addrs.end(),
+ std::back_inserter(contacts));
+ }
+
+ return contacts;
+}
+
+Mu::Contacts
+MimeMessage::contacts(Contact::Type ctype) const noexcept
+{
+ /* special case: get all */
+ if (ctype == Contact::Type::None)
+ return all_contacts(*this);
+
+ const auto atype{address_type(ctype)};
+ if (!atype)
+ return {};
+
+ auto addrs{g_mime_message_get_addresses(self(), *atype)};
+ if (!addrs)
+ return {};
+
+ const auto msgtime{date().value_or(0)};
+
+ Contacts contacts;
+ auto lst_len{internet_address_list_length(addrs)};
+ contacts.reserve(lst_len);
+ for (auto i = 0; i != lst_len; ++i) {
+
+ auto&& addr{internet_address_list_get_address(addrs, i)};
+ const auto name{internet_address_get_name(addr)};
+
+ if (G_UNLIKELY(!INTERNET_ADDRESS_IS_MAILBOX(addr)))
+ continue;
+
+ const auto email{internet_address_mailbox_get_addr (
+ INTERNET_ADDRESS_MAILBOX(addr))};
+ if (G_UNLIKELY(!email))
+ continue;
+
+ contacts.emplace_back(email, name ? name : "", ctype, msgtime);
+ }
+
+ return contacts;
+}
+
+/*
+ * references() returns the concatenation of the References and In-Reply-To
+ * message-ids (in that order). Duplicates are removed.
+ *
+ * The _first_ one in the list determines the thread-id for the message.
+ */
+std::vector<std::string>
+MimeMessage::references() const noexcept
+{
+ // is ref already in the list? O(n) but with small n.
+ auto is_dup = [](auto&& seq, const std::string& ref) {
+ return seq_some(seq, [&](auto&& str) { return ref == str; });
+ };
+
+ auto is_fake = [](auto&& msgid) {
+ // this is bit ugly; protonmail injects fake References which
+ // can otherwise screw up threading.
+ if (g_str_has_suffix(msgid, "protonmail.internalid"))
+ return true;
+ /* ... */
+ return false;
+ };
+
+ std::vector<std::string> refs;
+ for (auto&& ref_header: { "References", "In-reply-to" }) {
+
+ auto hdr{header(ref_header)};
+ if (!hdr)
+ continue;
+
+ GMimeReferences *mime_refs{g_mime_references_parse({}, hdr->c_str())};
+ refs.reserve(refs.size() + g_mime_references_length(mime_refs));
+
+ for (auto i = 0; i != g_mime_references_length(mime_refs); ++i) {
+ const auto msgid{g_mime_references_get_message_id(mime_refs, i)};
+ if (msgid && !is_dup(refs, msgid) && !is_fake(msgid))
+ refs.emplace_back(msgid);
+ }
+ g_mime_references_free(mime_refs);
+ }
+
+ return refs;
+}
+
+void
+MimeMessage::for_each(const ForEachFunc& func) const noexcept
+{
+ struct CallbackData { const ForEachFunc& func; };
+ CallbackData cbd{func};
+
+ g_mime_message_foreach(
+ self(),
+ [] (GMimeObject *parent, GMimeObject *part, gpointer user_data) {
+ auto cb_data{reinterpret_cast<CallbackData*>(user_data)};
+ cb_data->func(MimeObject{parent}, MimeObject{part});
+ }, &cbd);
+}
+
+
+\f
+/*
+ * MimePart
+ */
+size_t
+MimePart::size() const noexcept
+{
+ auto wrapper{g_mime_part_get_content(self())};
+ if (!wrapper) {
+ mu_warning("failed to get content wrapper");
+ return 0;
+ }
+
+ auto stream{g_mime_data_wrapper_get_stream(wrapper)};
+ if (!stream) {
+ mu_warning("failed to get stream");
+ return 0;
+ }
+
+ return static_cast<size_t>(g_mime_stream_length(stream));
+}
+Option<std::string>
+MimePart::to_string() const noexcept
+{
+ /*
+ * easy case: text. this automatically handles conversion to utf-8.
+ */
+ if (GMIME_IS_TEXT_PART(self())) {
+ if (char* txt{g_mime_text_part_get_text(GMIME_TEXT_PART(self()))}; !txt)
+ return Nothing;
+ else
+ return to_string_gchar(std::move(txt)/*consumes*/);
+ }
+
+ /*
+ * harder case: read from stream manually
+ */
+ GMimeDataWrapper *wrapper{g_mime_part_get_content(self())};
+ if (!wrapper) { /* this happens with invalid mails */
+ mu_warning("failed to create data wrapper");
+ return Nothing;
+ }
+
+ GMimeStream *stream{g_mime_stream_mem_new()};
+ if (!stream) {
+ mu_warning("failed to create mem stream");
+ return Nothing;
+ }
+
+ ssize_t buflen{g_mime_data_wrapper_write_to_stream(wrapper, stream)};
+ if (buflen <= 0) { /* empty buffer, not an error */
+ g_object_unref(stream);
+ return Nothing;
+ }
+
+ std::string buffer;
+ buffer.resize(buflen + 1);
+ g_mime_stream_reset(stream);
+
+ auto bytes{g_mime_stream_read(stream, buffer.data(), buflen)};
+ g_object_unref(stream);
+ if (bytes < 0)
+ return Nothing;
+
+ buffer.resize(bytes + 1);
+
+ return buffer;
+}
+
+Result<size_t>
+MimePart::to_file(const std::string& path, bool overwrite) const noexcept
+{
+ MimeDataWrapper wrapper{g_mime_part_get_content(self())};
+ if (!wrapper) /* this happens with invalid mails */
+ return Err(Error::Code::File, "failed to create data wrapper");
+
+ GError *err{};
+ auto strm{g_mime_stream_fs_open(path.c_str(),
+ O_WRONLY | O_CREAT | O_TRUNC |(overwrite ? 0 : O_EXCL),
+ S_IRUSR|S_IWUSR,
+ &err)};
+ if (!strm)
+ return Err(Error::Code::File, &err, "failed to open '{}'", path);
+
+ MimeStream stream{MimeStream::make_from_stream(strm)};
+ ssize_t written{g_mime_data_wrapper_write_to_stream(
+ GMIME_DATA_WRAPPER(wrapper.object()),
+ GMIME_STREAM(stream.object()))};
+
+ if (written < 0) {
+ return Err(Error::Code::File, &err,
+ "failed to write to '{}'", path);
+ }
+
+ return Ok(static_cast<size_t>(written));
+}
+
+void
+MimeMultipart::for_each(const ForEachFunc& func) const noexcept
+{
+ struct CallbackData { const ForEachFunc& func; };
+ CallbackData cbd{func};
+
+ g_mime_multipart_foreach(
+ self(),
+ [] (GMimeObject *parent, GMimeObject *part, gpointer user_data) {
+ auto cb_data{reinterpret_cast<CallbackData*>(user_data)};
+ cb_data->func(MimeObject{parent}, MimeObject{part});
+ }, &cbd);
+}
+
+
+/*
+ * we need to be able to pass a crypto-context to the verify(), but
+ * g_mime_multipart_signed_verify() doesn't offer that anymore in GMime 3.x.
+ *
+ * So, add that by reimplementing it a bit (follow the upstream impl)
+ */
+
+
+static bool
+mime_types_equal (const std::string& mime_type, const std::string& official_type)
+{
+ if (g_ascii_strcasecmp(mime_type.c_str(), official_type.c_str()) == 0)
+ return true;
+
+ const auto slash_pos = official_type.find("/");
+ if (slash_pos == std::string::npos || slash_pos == 0)
+ return false;
+
+ /* If the official mime-type's subtype already begins with "x-", then there's
+ * nothing else to check. */
+ const auto subtype{official_type.substr(slash_pos + 1)};
+ if (g_ascii_strncasecmp (subtype.c_str(), "x-", 2) == 0)
+ return false;
+ const auto supertype{official_type.substr(0, slash_pos - 1)};
+ const auto xtype{official_type.substr(0, slash_pos - 1) + "x-" + subtype};
+
+ /* Check if the "x-" version of the official mime-type matches the
+ * supplied mime-type. For example, if the official mime-type is
+ * "application/pkcs7-signature", then we also want to match
+ * "application/x-pkcs7-signature". */
+ return g_ascii_strcasecmp(mime_type.c_str(), xtype.c_str()) == 0;
+}
+
+
+/**
+ * A bit of a monster, this impl.
+ *
+ * It's the transliteration of the g_mime_multipart_signed_verify() which
+ * adds the feature of passing in the CryptoContext.
+ *
+ */
+Result<std::vector<MimeSignature>>
+MimeMultipartSigned::verify(const MimeCryptoContext& ctx, VerifyFlags vflags) const noexcept
+{
+ if (g_mime_multipart_get_count(GMIME_MULTIPART(self())) < 2)
+ return Err(Error::Code::Crypto, "cannot verify, not enough subparts");
+
+ const auto proto{content_type_parameter("protocol")};
+ const auto sign_proto{ctx.signature_protocol()};
+
+ if (!proto || !sign_proto || !mime_types_equal(*proto, *sign_proto))
+ return Err(Error::Code::Crypto, "unsupported protocol {}",
+ proto.value_or("<unknown>"));
+
+ const auto sig{signed_signature_part()};
+ const auto content{signed_content_part()};
+ if (!sig || !content)
+ return Err(Error::Code::Crypto, "cannot find part");
+
+ const auto sig_mime_type{sig->mime_type()};
+ if (!sig || !mime_types_equal(sig_mime_type.value_or("<none>"), *sign_proto))
+ return Err(Error::Code::Crypto, "failed to find matching signature part");
+
+ MimeFormatOptions fopts{g_mime_format_options_new()};
+ g_mime_format_options_set_newline_format(fopts.get(), GMIME_NEWLINE_FORMAT_DOS);
+
+ MimeStream stream{MimeStream::make_mem()};
+ if (auto&& res = content->write_to_stream(fopts, stream); !res)
+ return Err(res.error());
+ stream.reset();
+
+ MimeDataWrapper wrapper{g_mime_part_get_content(GMIME_PART(sig->object()))};
+ MimeStream sigstream{MimeStream::make_mem()};
+ if (auto&& res = wrapper.write_to_stream(sigstream); !res)
+ return Err(res.error());
+ sigstream.reset();
+
+ GError *err{};
+ GMimeSignatureList *siglist{g_mime_crypto_context_verify(
+ GMIME_CRYPTO_CONTEXT(ctx.object()),
+ static_cast<GMimeVerifyFlags>(vflags),
+ GMIME_STREAM(stream.object()),
+ GMIME_STREAM(sigstream.object()),
+ {},
+ &err)};
+ if (!siglist)
+ return Err(Error::Code::Crypto, &err, "failed to verify");
+
+ std::vector<MimeSignature> sigs;
+ for (auto i = 0;
+ i != g_mime_signature_list_length(siglist); ++i) {
+ GMimeSignature *msig = g_mime_signature_list_get_signature(siglist, i);
+ sigs.emplace_back(MimeSignature(msig));
+ }
+ g_object_unref(siglist);
+
+ return sigs;
+}
+
+
+std::vector<MimeCertificate>
+MimeDecryptResult::recipients() const noexcept
+{
+ GMimeCertificateList *lst{g_mime_decrypt_result_get_recipients(self())};
+ if (!lst)
+ return {};
+
+ std::vector<MimeCertificate> certs;
+ for (int i = 0; i != g_mime_certificate_list_length(lst); ++i)
+ certs.emplace_back(
+ MimeCertificate(
+ g_mime_certificate_list_get_certificate(lst, i)));
+
+ return certs;
+}
+
+std::vector<MimeSignature>
+MimeDecryptResult::signatures() const noexcept
+{
+ GMimeSignatureList *lst{g_mime_decrypt_result_get_signatures(self())};
+ if (!lst)
+ return {};
+
+ std::vector<MimeSignature> sigs;
+ for (auto i = 0; i != g_mime_signature_list_length(lst); ++i) {
+ GMimeSignature *sig = g_mime_signature_list_get_signature(lst, i);
+ sigs.emplace_back(MimeSignature(sig));
+ }
+
+ return sigs;
+}
+/**
+ * Like verify, a bit of a monster, this impl.
+ *
+ * It's the transliteration of the g_mime_multipart_encrypted_decrypt() which
+ * adds the feature of passing in the CryptoContext.
+ *
+ */
+
+Mu::Result<MimeMultipartEncrypted::Decrypted>
+MimeMultipartEncrypted::decrypt(const MimeCryptoContext& ctx, DecryptFlags dflags,
+ const std::string& session_key) const noexcept
+{
+ if (g_mime_multipart_get_count(GMIME_MULTIPART(self())) < 2)
+ return Err(Error::Code::Crypto, "cannot decrypted, not enough subparts");
+
+ const auto proto{content_type_parameter("protocol")};
+ const auto enc_proto{ctx.encryption_protocol()};
+
+ if (!proto || !enc_proto || !mime_types_equal(*proto, *enc_proto))
+ return Err(Error::Code::Crypto, "unsupported protocol {}",
+ proto.value_or("<unknown>"));
+
+ const auto version{encrypted_version_part()};
+ const auto encrypted{encrypted_content_part()};
+ if (!version || !encrypted)
+ return Err(Error::Code::Crypto, "cannot find part");
+
+ if (!mime_types_equal(version->mime_type().value_or(""), proto.value()))
+ return Err(Error::Code::Crypto,
+ "cannot decrypt; unexpected version content-type '{}' != '{}'",
+ version->mime_type().value_or(""), proto.value());
+
+ if (!mime_types_equal(encrypted->mime_type().value_or(""),
+ "application/octet-stream"))
+ return Err(Error::Code::Crypto,
+ "cannot decrypt; unexpected encrypted content-type '{}'",
+ encrypted->mime_type().value_or(""));
+
+ const auto content{encrypted->content()};
+ auto ciphertext{MimeStream::make_mem()};
+ content.write_to_stream(ciphertext);
+ ciphertext.reset();
+
+ auto stream{MimeStream::make_mem()};
+ auto filtered{MimeStream::make_filtered(stream)};
+ auto filter{g_mime_filter_dos2unix_new(FALSE)};
+ g_mime_stream_filter_add(GMIME_STREAM_FILTER(filtered.object()),
+ filter);
+ g_object_unref(filter);
+
+ GError *err{};
+ GMimeDecryptResult *dres =
+ g_mime_crypto_context_decrypt(GMIME_CRYPTO_CONTEXT(ctx.object()),
+ static_cast<GMimeDecryptFlags>(dflags),
+ session_key.empty() ?
+ NULL : session_key.c_str(),
+ GMIME_STREAM(ciphertext.object()),
+ GMIME_STREAM(filtered.object()),
+ &err);
+ if (!dres)
+ return Err(Error::Code::Crypto, &err, "decryption failed");
+
+ filtered.flush();
+ stream.reset();
+
+ auto parser{g_mime_parser_new()};
+ g_mime_parser_init_with_stream(parser, GMIME_STREAM(stream.object()));
+
+ auto decrypted{g_mime_parser_construct_part(parser, NULL)};
+ g_object_unref(parser);
+ if (!decrypted) {
+ g_object_unref(dres);
+ return Err(Error::Code::Crypto, "failed to parse decrypted part");
+ }
+
+ Decrypted result = { MimeObject{decrypted}, MimeDecryptResult{dres} };
+
+ g_object_unref(decrypted);
+ g_object_unref(dres);
+
+ return Ok(std::move(result));
+}
--- /dev/null
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_MIME_OBJECT_HH__
+#define MU_MIME_OBJECT_HH__
+
+#include <stdexcept>
+#include <string>
+#include <functional>
+#include <array>
+#include <vector>
+#include <gmime/gmime.h>
+#include "gmime/gmime-application-pkcs7-mime.h"
+#include "gmime/gmime-crypto-context.h"
+#include "utils/mu-option.hh"
+#include "utils/mu-result.hh"
+#include "utils/mu-utils.hh"
+#include "mu-contact.hh"
+
+namespace Mu {
+
+/* non-GObject types */
+
+using MimeFormatOptions = deletable_unique_ptr<GMimeFormatOptions, g_mime_format_options_free>;
+
+/**
+ * Initialize gmime (idempotent)
+ *
+ */
+void init_gmime(void);
+
+
+/**
+ * Get a RFC2047-compatible address for the given contact
+ *
+ * @param contact a contact
+ *
+ * @return an address string
+ */
+std::string address_rfc2047(const Contact& contact);
+
+class Object {
+public:
+ /**
+ * Default CTOR
+ *
+ */
+ Object() noexcept: self_{} {}
+
+ /**
+ * Create an object from a GObject
+ *
+ * @param obj a gobject. A ref is added.
+ */
+ Object(GObject* &&obj): self_{G_OBJECT(g_object_ref(obj))} {
+ if (!G_IS_OBJECT(obj))
+ throw std::runtime_error("not a g-object");
+ }
+
+ /**
+ * Copy CTOR
+ *
+ * @param other some other Object
+ */
+ Object(const Object& other) noexcept { *this = other; }
+
+ /**
+ * Move CTOR
+ *
+ * @param other some other Object
+ */
+ Object(Object&& other) noexcept { *this = std::move(other); }
+
+ /**
+ * operator=
+ *
+ * @param other copy some other object
+ *
+ * @return *this
+ */
+ Object& operator=(const Object& other) noexcept {
+
+ if (this != &other) {
+ auto oldself = self_;
+ self_ = other.self_ ?
+ G_OBJECT(g_object_ref(other.self_)) : nullptr;
+ if (oldself)
+ g_object_unref(oldself);
+ }
+ return *this;
+ }
+
+ /**
+ * operator=
+ *
+ * @param other move some object object
+ *
+ * @return
+ */
+ Object& operator=(Object&& other) noexcept {
+
+ if (this != &other) { auto oldself = self_;
+ self_ = other.self_;
+ other.self_ = nullptr;
+ if (oldself)
+ g_object_unref(oldself);
+ }
+ return *this;
+ }
+
+ /**
+ * DTOR
+ */
+ virtual ~Object() {
+ if (self_) {
+ g_object_unref(self_);
+ }
+ }
+
+ /**
+ * operator bool
+ *
+ * @return true if object wraps a GObject, false otherwise
+ */
+ operator bool() const noexcept { return !!self_; }
+
+ /**
+ * Get a ptr to the underlying GObject
+ *
+ * @return GObject or NULL
+ */
+ GObject* object() const { return self_; }
+
+
+ /**
+ * Unref the object
+ *
+ */
+ void unref() noexcept {
+ g_object_unref(self_);
+ }
+
+
+ /**
+ * Ref the object
+ *
+ */
+ void ref() noexcept {
+ g_object_ref(self_);
+ }
+
+
+private:
+ mutable GObject *self_{};
+};
+
+
+
+
+
+\f
+/**
+ * Thin wrapper around a GMimeContentType
+ *
+ */
+struct MimeContentType: public Object {
+
+ MimeContentType(GMimeContentType *ctype) : Object{G_OBJECT(ctype)} {
+ if (!GMIME_IS_CONTENT_TYPE(self()))
+ throw std::runtime_error("not a content-type");
+ }
+ std::string media_type() const noexcept {
+ return g_mime_content_type_get_media_type(self());
+ }
+ std::string media_subtype() const noexcept {
+ return g_mime_content_type_get_media_subtype(self());
+ }
+
+ Option<std::string> mime_type() const noexcept {
+ return to_string_opt_gchar(g_mime_content_type_get_mime_type(self()));
+ }
+
+ bool is_type(const std::string& type, const std::string& subtype) const {
+ return g_mime_content_type_is_type(self(), type.c_str(),
+ subtype.c_str());
+ }
+private:
+ GMimeContentType* self() const {
+ return reinterpret_cast<GMimeContentType*>(object());
+ }
+};
+
+
+
+
+\f
+/**
+ * Thin wrapper around a GMimeStream
+ *
+ */
+struct MimeStream: public Object {
+
+ ssize_t write(const char* buf, ::size_t size) {
+ return g_mime_stream_write(self(), buf, size);
+ }
+
+ bool reset() {
+ return g_mime_stream_reset(self()) < 0 ? false : true;
+ }
+
+ bool flush() {
+ return g_mime_stream_flush(self()) < 0 ? false : true;
+ }
+
+ static MimeStream make_mem() {
+ MimeStream mstream{g_mime_stream_mem_new()};
+ mstream.unref(); /* remove extra ref */
+ return mstream;
+ }
+
+ static MimeStream make_filtered(MimeStream& stream) {
+ MimeStream mstream{g_mime_stream_filter_new(stream.self())};
+ mstream.unref(); /* remove extra refs */
+ return mstream;
+ }
+
+ static MimeStream make_from_stream(GMimeStream *strm) {
+ MimeStream mstream{strm};
+ mstream.unref(); /* remove extra ref */
+ return mstream;
+ }
+
+private:
+ MimeStream(GMimeStream *stream): Object(G_OBJECT(stream)) {
+ if (!GMIME_IS_STREAM(self()))
+ throw std::runtime_error("not a mime-stream");
+ };
+
+ GMimeStream* self() const {
+ return reinterpret_cast<GMimeStream*>(object());
+ }
+};
+
+template<typename S, typename T>
+constexpr Option<std::string_view> to_string_view_opt(const S& seq, T t) {
+ auto&& it = seq_find_if(seq, [&](auto&& item){return item.first == t;});
+ if (it == seq.cend())
+ return Nothing;
+ else
+ return it->second;
+}
+
+\f
+/**
+ * Thin wrapper around a GMimeDataWrapper
+ *
+ */
+struct MimeDataWrapper: public Object {
+ MimeDataWrapper(GMimeDataWrapper *wrapper): Object(G_OBJECT(wrapper)) {
+ if (!GMIME_IS_DATA_WRAPPER(self()))
+ throw std::runtime_error("not a data-wrapper");
+ };
+
+ Result<size_t> write_to_stream(MimeStream& stream) const {
+ if (auto&& res = g_mime_data_wrapper_write_to_stream(
+ self(), GMIME_STREAM(stream.object())) ; res < 0)
+ return Err(Error::Code::Message, "failed to write to stream");
+ else
+ return Ok(static_cast<size_t>(res));
+ }
+
+private:
+ GMimeDataWrapper* self() const {
+ return reinterpret_cast<GMimeDataWrapper*>(object());
+ }
+};
+
+
+\f
+/**
+ * Thin wrapper around a GMimeCertifcate
+ *
+ */
+struct MimeCertificate: public Object {
+ MimeCertificate(GMimeCertificate *cert) : Object{G_OBJECT(cert)} {
+ if (!GMIME_IS_CERTIFICATE(self()))
+ throw std::runtime_error("not a certificate");
+ }
+
+ enum struct PubkeyAlgo {
+ Default = GMIME_PUBKEY_ALGO_DEFAULT,
+ Rsa = GMIME_PUBKEY_ALGO_RSA,
+ RsaE = GMIME_PUBKEY_ALGO_RSA_E,
+ RsaS = GMIME_PUBKEY_ALGO_RSA_S,
+ ElgE = GMIME_PUBKEY_ALGO_ELG_E,
+ Dsa = GMIME_PUBKEY_ALGO_DSA,
+ Ecc = GMIME_PUBKEY_ALGO_ECC,
+ Elg = GMIME_PUBKEY_ALGO_ELG,
+ EcDsa = GMIME_PUBKEY_ALGO_ECDSA,
+ EcDh = GMIME_PUBKEY_ALGO_ECDH,
+ EdDsa = GMIME_PUBKEY_ALGO_EDDSA,
+ };
+
+ enum struct DigestAlgo {
+ Default = GMIME_DIGEST_ALGO_DEFAULT,
+ Md5 = GMIME_DIGEST_ALGO_MD5,
+ Sha1 = GMIME_DIGEST_ALGO_SHA1,
+ RipEmd160 = GMIME_DIGEST_ALGO_RIPEMD160,
+ Md2 = GMIME_DIGEST_ALGO_MD2,
+ Tiger192 = GMIME_DIGEST_ALGO_TIGER192,
+ Haval5160 = GMIME_DIGEST_ALGO_HAVAL5160,
+ Sha256 = GMIME_DIGEST_ALGO_SHA256,
+ Sha384 = GMIME_DIGEST_ALGO_SHA384,
+ Sha512 = GMIME_DIGEST_ALGO_SHA512,
+ Sha224 = GMIME_DIGEST_ALGO_SHA224,
+ Md4 = GMIME_DIGEST_ALGO_MD4,
+ Crc32 = GMIME_DIGEST_ALGO_CRC32,
+ Crc32Rfc1510 = GMIME_DIGEST_ALGO_CRC32_RFC1510,
+ Crc32Rfc2440 = GMIME_DIGEST_ALGO_CRC32_RFC2440,
+ };
+
+ enum struct Trust {
+ Unknown = GMIME_TRUST_UNKNOWN,
+ Undefined = GMIME_TRUST_UNDEFINED,
+ Never = GMIME_TRUST_NEVER,
+ Marginal = GMIME_TRUST_MARGINAL,
+ TrustFull = GMIME_TRUST_FULL,
+ TrustUltimate = GMIME_TRUST_ULTIMATE,
+ };
+
+ enum struct Validity {
+ Unknown = GMIME_VALIDITY_UNKNOWN,
+ Undefined = GMIME_VALIDITY_UNDEFINED,
+ Never = GMIME_VALIDITY_NEVER,
+ Marginal = GMIME_VALIDITY_MARGINAL,
+ Full = GMIME_VALIDITY_FULL,
+ Ultimate = GMIME_VALIDITY_ULTIMATE,
+ };
+
+ PubkeyAlgo pubkey_algo() const {
+ return static_cast<PubkeyAlgo>(
+ g_mime_certificate_get_pubkey_algo(self()));
+ }
+
+ DigestAlgo digest_algo() const {
+ return static_cast<DigestAlgo>(
+ g_mime_certificate_get_digest_algo(self()));
+ }
+
+ Validity id_validity() const {
+ return static_cast<Validity>(
+ g_mime_certificate_get_id_validity(self()));
+ }
+
+ Trust trust() const {
+ return static_cast<Trust>(
+ g_mime_certificate_get_trust(self()));
+ }
+
+ Option<std::string> issuer_serial() const {
+ return to_string_opt(g_mime_certificate_get_issuer_serial(self()));
+ }
+ Option<std::string> issuer_name() const {
+ return to_string_opt(g_mime_certificate_get_issuer_name(self()));
+ }
+
+ Option<std::string> fingerprint() const {
+ return to_string_opt(g_mime_certificate_get_fingerprint(self()));
+ }
+
+ Option<std::string> key_id() const {
+ return to_string_opt(g_mime_certificate_get_key_id(self()));
+ }
+
+
+ Option<std::string> name() const {
+ return to_string_opt(g_mime_certificate_get_name(self()));
+ }
+
+ Option<std::string> user_id() const {
+ return to_string_opt(g_mime_certificate_get_user_id(self()));
+ }
+
+ Option<::time_t> created() const {
+ if (auto t = g_mime_certificate_get_created(self()); t >= 0)
+ return t;
+ else
+ return Nothing;
+ }
+
+ Option<::time_t> expires() const {
+ if (auto t = g_mime_certificate_get_expires(self()); t >= 0)
+ return t;
+ else
+ return Nothing;
+ }
+
+private:
+ GMimeCertificate* self() const {
+ return reinterpret_cast<GMimeCertificate*>(object());
+ }
+};
+
+constexpr std::array<std::pair<MimeCertificate::PubkeyAlgo, std::string_view>, 11>
+AllPubkeyAlgos = {{
+ { MimeCertificate::PubkeyAlgo::Default, "default"},
+ { MimeCertificate::PubkeyAlgo::Rsa, "rsa"},
+ { MimeCertificate::PubkeyAlgo::RsaE, "rsa-encryption-only"},
+ { MimeCertificate::PubkeyAlgo::RsaS, "rsa-signing-only"},
+ { MimeCertificate::PubkeyAlgo::ElgE, "el-gamal-encryption-only"},
+ { MimeCertificate::PubkeyAlgo::Dsa, "dsa"},
+ { MimeCertificate::PubkeyAlgo::Ecc, "elliptic curve"},
+ { MimeCertificate::PubkeyAlgo::Elg, "el-gamal"},
+ { MimeCertificate::PubkeyAlgo::EcDsa, "elliptic-curve+dsa"},
+ { MimeCertificate::PubkeyAlgo::EcDh, "elliptic-curve+diffie-helman"},
+ { MimeCertificate::PubkeyAlgo::EdDsa, "elliptic-curve+dsa-2"}
+ }};
+
+constexpr Option<std::string_view> to_string_view_opt(MimeCertificate::PubkeyAlgo algo) {
+ return to_string_view_opt(AllPubkeyAlgos, algo);
+}
+
+constexpr std::array<std::pair<MimeCertificate::DigestAlgo, std::string_view>, 15>
+AllDigestAlgos = {{
+ { MimeCertificate::DigestAlgo::Default, "default"},
+ { MimeCertificate::DigestAlgo::Md5, "md5"},
+ { MimeCertificate::DigestAlgo::Sha1, "sha1"},
+ { MimeCertificate::DigestAlgo::RipEmd160, "ripemd-160"},
+ { MimeCertificate::DigestAlgo::Md2, "md2"},
+ { MimeCertificate::DigestAlgo::Tiger192, "tiger-192"},
+ { MimeCertificate::DigestAlgo::Haval5160, "haval-5-160"},
+ { MimeCertificate::DigestAlgo::Sha256, "sha-256"},
+ { MimeCertificate::DigestAlgo::Sha384, "sha-384"},
+ { MimeCertificate::DigestAlgo::Sha512, "sha-512"},
+ { MimeCertificate::DigestAlgo::Sha224, "sha-224"},
+ { MimeCertificate::DigestAlgo::Md4, "md4"},
+ { MimeCertificate::DigestAlgo::Crc32, "crc32"},
+ { MimeCertificate::DigestAlgo::Crc32Rfc1510, "crc32-rfc1510"},
+ { MimeCertificate::DigestAlgo::Crc32Rfc2440, "crc32-rfc2440"},
+ }};
+
+constexpr Option<std::string_view> to_string_view_opt(MimeCertificate::DigestAlgo algo) {
+ return to_string_view_opt(AllDigestAlgos, algo);
+}
+
+constexpr std::array<std::pair<MimeCertificate::Trust, std::string_view>, 6>
+AllTrusts = {{
+ { MimeCertificate::Trust::Unknown, "unknown" },
+ { MimeCertificate::Trust::Undefined, "undefined" },
+ { MimeCertificate::Trust::Never, "never" },
+ { MimeCertificate::Trust::Marginal, "marginal" },
+ { MimeCertificate::Trust::TrustFull, "trust-full" },
+ { MimeCertificate::Trust::TrustUltimate,"trust-ultimate" },
+ }};
+
+constexpr Option<std::string_view> to_string_view_opt(MimeCertificate::Trust trust) {
+ return to_string_view_opt(AllTrusts, trust);
+}
+
+constexpr std::array<std::pair<MimeCertificate::Validity, std::string_view>, 6>
+AllValidities = {{
+ { MimeCertificate::Validity::Unknown, "unknown" },
+ { MimeCertificate::Validity::Undefined, "undefined" },
+ { MimeCertificate::Validity::Never, "never" },
+ { MimeCertificate::Validity::Marginal, "marginal" },
+ { MimeCertificate::Validity::Full, "full" },
+ { MimeCertificate::Validity::Ultimate, "ultimate" },
+ }};
+
+constexpr Option<std::string_view> to_string_view_opt(MimeCertificate::Validity val) {
+ return to_string_view_opt(AllValidities, val);
+}
+
+
+\f
+/**
+ * Thin wrapper around a GMimeSignature
+ *
+ */
+struct MimeSignature: public Object {
+ MimeSignature(GMimeSignature *sig) : Object{G_OBJECT(sig)} {
+ if (!GMIME_IS_SIGNATURE(self()))
+ throw std::runtime_error("not a signature");
+ }
+
+ /**
+ * Signature status
+ *
+ */
+ enum struct Status {
+ Valid = GMIME_SIGNATURE_STATUS_VALID,
+ Green = GMIME_SIGNATURE_STATUS_GREEN,
+ Red = GMIME_SIGNATURE_STATUS_RED,
+ KeyRevoked = GMIME_SIGNATURE_STATUS_KEY_REVOKED,
+ KeyExpired = GMIME_SIGNATURE_STATUS_KEY_EXPIRED,
+ SigExpired = GMIME_SIGNATURE_STATUS_SIG_EXPIRED,
+ KeyMissing = GMIME_SIGNATURE_STATUS_KEY_MISSING,
+ CrlMissing = GMIME_SIGNATURE_STATUS_CRL_MISSING,
+ CrlTooOld = GMIME_SIGNATURE_STATUS_CRL_TOO_OLD,
+ BadPolicy = GMIME_SIGNATURE_STATUS_BAD_POLICY,
+ SysError = GMIME_SIGNATURE_STATUS_SYS_ERROR,
+ TofuConflict = GMIME_SIGNATURE_STATUS_TOFU_CONFLICT
+ };
+
+ Status status() const { return static_cast<Status>(
+ g_mime_signature_get_status(self())); }
+
+ ::time_t created() const { return g_mime_signature_get_created(self()); }
+ ::time_t expires() const { return g_mime_signature_get_expires(self()); }
+
+
+ const MimeCertificate certificate() const {
+ return MimeCertificate{g_mime_signature_get_certificate(self())};
+ }
+
+private:
+ GMimeSignature* self() const {
+ return reinterpret_cast<GMimeSignature*>(object());
+ }
+};
+
+constexpr std::array<std::pair<MimeSignature::Status, std::string_view>, 12>
+AllMimeSignatureStatuses= {{
+ { MimeSignature::Status::Valid, "valid" },
+ { MimeSignature::Status::Green, "green" },
+ { MimeSignature::Status::Red, "red" },
+ { MimeSignature::Status::KeyRevoked, "key-revoked" },
+ { MimeSignature::Status::KeyExpired, "key-expired" },
+ { MimeSignature::Status::SigExpired, "sig-expired" },
+ { MimeSignature::Status::KeyMissing, "key-missing" },
+ { MimeSignature::Status::CrlMissing, "crl-missing" },
+ { MimeSignature::Status::CrlTooOld, "crl-too-old" },
+ { MimeSignature::Status::BadPolicy, "bad-policy" },
+ { MimeSignature::Status::SysError, "sys-error" },
+ { MimeSignature::Status::TofuConflict, "tofu-confict" },
+ }};
+MU_ENABLE_BITOPS(MimeSignature::Status);
+
+static inline std::string to_string(MimeSignature::Status status) {
+ std::string str;
+ for (auto&& item: AllMimeSignatureStatuses) {
+ if (none_of(item.first & status))
+ continue;
+ if (!str.empty())
+ str += ", ";
+ str += item.second;
+ }
+ if (str.empty())
+ str = "none";
+
+ return str;
+}
+
+
+
+\f
+/**
+* Thin wrapper around a GMimeDecryptResult
+ *
+ */
+struct MimeDecryptResult: public Object {
+ MimeDecryptResult (GMimeDecryptResult *decres) : Object{G_OBJECT(decres)} {
+ if (!GMIME_IS_DECRYPT_RESULT(self()))
+ throw std::runtime_error("not a decrypt-result");
+ }
+
+ std::vector<MimeCertificate> recipients() const noexcept;
+ std::vector<MimeSignature> signatures() const noexcept;
+
+ enum struct CipherAlgo {
+ Default = GMIME_CIPHER_ALGO_DEFAULT,
+ Idea = GMIME_CIPHER_ALGO_IDEA,
+ Des3 = GMIME_CIPHER_ALGO_3DES,
+ Cast5 = GMIME_CIPHER_ALGO_CAST5,
+ Blowfish = GMIME_CIPHER_ALGO_BLOWFISH,
+ Aes = GMIME_CIPHER_ALGO_AES,
+ Aes192 = GMIME_CIPHER_ALGO_AES192,
+ Aes256 = GMIME_CIPHER_ALGO_AES256,
+ TwoFish = GMIME_CIPHER_ALGO_TWOFISH,
+ Camellia128 = GMIME_CIPHER_ALGO_CAMELLIA128,
+ Camellia192 = GMIME_CIPHER_ALGO_CAMELLIA192,
+ Camellia256 = GMIME_CIPHER_ALGO_CAMELLIA256
+ };
+
+ CipherAlgo cipher() const noexcept {
+ return static_cast<CipherAlgo>(
+ g_mime_decrypt_result_get_cipher(self()));
+ }
+
+ using DigestAlgo = MimeCertificate::DigestAlgo;
+ DigestAlgo mdc() const noexcept {
+ return static_cast<DigestAlgo>(
+ g_mime_decrypt_result_get_mdc(self()));
+ }
+
+ Option<std::string> session_key() const noexcept {
+ return to_string_opt(g_mime_decrypt_result_get_session_key(self()));
+ }
+
+private:
+ GMimeDecryptResult* self() const {
+ return reinterpret_cast<GMimeDecryptResult*>(object());
+ }
+};
+
+constexpr std::array<std::pair<MimeDecryptResult::CipherAlgo, std::string_view>, 12>
+AllCipherAlgos= {{
+ {MimeDecryptResult::CipherAlgo::Default, "default"},
+ {MimeDecryptResult::CipherAlgo::Idea, "idea"},
+ {MimeDecryptResult::CipherAlgo::Des3, "3des"},
+ {MimeDecryptResult::CipherAlgo::Cast5, "cast5"},
+ {MimeDecryptResult::CipherAlgo::Blowfish, "blowfish"},
+ {MimeDecryptResult::CipherAlgo::Aes, "aes"},
+ {MimeDecryptResult::CipherAlgo::Aes192, "aes192"},
+ {MimeDecryptResult::CipherAlgo::Aes256, "aes256"},
+ {MimeDecryptResult::CipherAlgo::TwoFish, "twofish"},
+ {MimeDecryptResult::CipherAlgo::Camellia128, "camellia128"},
+ {MimeDecryptResult::CipherAlgo::Camellia192, "camellia192"},
+ {MimeDecryptResult::CipherAlgo::Camellia256, "camellia256"},
+ }};
+
+constexpr Option<std::string_view> to_string_view_opt(MimeDecryptResult::CipherAlgo algo) {
+ return to_string_view_opt(AllCipherAlgos, algo);
+}
+
+\f
+/**
+ * Thin wrapper around a GMimeCryptoContext
+ *
+ */
+struct MimeCryptoContext : public Object {
+
+ /**
+ * Make a new PGP crypto context.
+ *
+ * For 'test-mode', pass a test-path; in this mode GPG will be setup
+ * in an isolated mode so it does not affect normal usage.
+ *
+ * @param testpath (for unit-tests) pass a path to an existing dir to
+ * create a pgp setup. For normal use, leave empty.
+ *
+ * @return A MimeCryptoContext or an error
+ */
+ static Result<MimeCryptoContext>
+ make_gpg(const std::string& testpath={}) try {
+ if (!testpath.empty()) {
+ if (auto&& res = setup_gpg_test(testpath); !res)
+ return Err(res.error());
+ }
+ MimeCryptoContext ctx(g_mime_gpg_context_new());
+ ctx.unref(); /* remove extra ref */
+ return Ok(std::move(ctx));
+ } catch (...) {
+ return Err(Error::Code::Crypto, "failed to create crypto context");
+ }
+
+ static Result<MimeCryptoContext>
+ make(const std::string& protocol) {
+ auto ctx = g_mime_crypto_context_new(protocol.c_str());
+ if (!ctx)
+ return Err(Error::Code::Crypto,
+ "unsupported protocol {}", protocol);
+ MimeCryptoContext mctx{ctx};
+ mctx.unref(); /* remove extra ref */
+ return Ok(std::move(mctx));
+ }
+
+ Option<std::string> encryption_protocol() const noexcept {
+ return to_string_opt(g_mime_crypto_context_get_encryption_protocol(self()));
+ }
+ Option<std::string> signature_protocol() const noexcept {
+ return to_string_opt(g_mime_crypto_context_get_signature_protocol(self()));
+ }
+ Option<std::string> key_exchange_protocol() const noexcept {
+ return to_string_opt(g_mime_crypto_context_get_key_exchange_protocol(self()));
+ }
+
+ /**
+ * Imports a stream of keys/certificates contained within stream into
+ * the key/certificate database controlled by @this.
+ *
+ * @param stream
+ *
+ * @return number of keys imported, or an error.
+ */
+ Result<size_t> import_keys(MimeStream& stream);
+
+ /**
+ * Prototype for a request-password function.
+ *
+ * @param ctx the MimeCryptoContext making the request
+ * @param user_id the user_id of the password being requested
+ * @param prompt a string containing some helpful context for the prompt
+ * @param reprompt true if this password request is a reprompt due to a
+ * previously bad password response
+ * @param response a stream for the application to write the password to
+ * (followed by a newline '\n' character)
+ *
+ * @return nothing (Ok) or an error,
+ */
+ using PasswordRequestFunc =
+ std::function<Result<void>(
+ const MimeCryptoContext& ctx,
+ const std::string& user_id,
+ const std::string& prompt,
+ bool reprompt,
+ MimeStream& response)>;
+ /**
+ * Set a function to request a password.
+ *
+ * @param pw_func password function.
+ */
+ void set_request_password(PasswordRequestFunc pw_func);
+
+
+private:
+ MimeCryptoContext(GMimeCryptoContext *ctx): Object{G_OBJECT(ctx)} {
+ if (!GMIME_IS_CRYPTO_CONTEXT(self()))
+ throw std::runtime_error("not a crypto-context");
+ }
+
+ static Result<void> setup_gpg_test(const std::string& testpath);
+
+ GMimeCryptoContext* self() const {
+ return reinterpret_cast<GMimeCryptoContext*>(object());
+ }
+};
+
+\f
+/**
+ * Thin wrapper around a GMimeObject
+ *
+ */
+class MimeObject: public Object {
+public:
+ /**
+ * Construct a new MimeObject. Take a ref on the obj
+ *
+ * @param mime_part mime-part pointer
+ */
+ MimeObject(const Object& obj): Object{obj} {
+ if (!GMIME_IS_OBJECT(self()))
+ throw std::runtime_error("not a mime-object");
+ }
+ MimeObject(GMimeObject *mobj): Object{G_OBJECT(mobj)} {
+ if (mobj && !GMIME_IS_OBJECT(self()))
+ throw std::runtime_error("not a mime-object");
+ }
+
+ /**
+ * Get a header from the MimeObject
+ *
+ * @param header the header to retrieve
+ *
+ * @return header value (UTF-8) or Nothing
+ */
+ Option<std::string> header(const std::string& header) const noexcept;
+
+
+ /**
+ * Get all headers as pairs of name, value
+ *
+ * @return all headers
+ */
+ std::vector<std::pair<std::string, std::string>> headers() const noexcept;
+
+
+ /**
+ * Get the content type
+ *
+ * @return the content-type or Nothing
+ */
+ Option<MimeContentType> content_type() const noexcept {
+ auto ct{g_mime_object_get_content_type(self())};
+ if (!ct)
+ return Nothing;
+ else
+ return MimeContentType(ct);
+ }
+
+ Option<std::string> mime_type() const noexcept {
+ if (auto ct = content_type(); !ct)
+ return Nothing;
+ else
+ return ct->mime_type();
+ }
+
+ /**
+ * Get the content-type parameter
+ *
+ * @param param name of parameter
+ *
+ * @return the value of the parameter, or Nothing
+ */
+ Option<std::string> content_type_parameter(const std::string& param) const noexcept {
+ return Mu::to_string_opt(
+ g_mime_object_get_content_type_parameter(self(), param.c_str()));
+ }
+
+ /**
+ * Write this MimeObject to some stream
+ *
+ * @param f_opts formatting options
+ * @param stream the stream
+ *
+ * @return the number or bytes written or an error
+ */
+ Result<size_t> write_to_stream(const MimeFormatOptions& f_opts,
+ MimeStream& stream) const;
+ /**
+ * Write the object to a string.
+ *
+ * @return
+ */
+ Option<std::string> to_string_opt() const noexcept;
+
+ /**
+ * Write object to a file
+ *
+ * @param path path to file
+ * @param overwrite if true, overwrite existing file, if it \18bqexists
+ *
+ * @return size of the wrtten file, or an error.
+ */
+ Result<size_t> to_file(const std::string& path, bool overwrite) const noexcept;
+
+ /*
+ * subtypes.
+ */
+
+ /**
+ * Is this a MimePart?
+ *
+ * @return true or false
+ */
+ bool is_part() const { return GMIME_IS_PART(self()); }
+
+ /**
+ * Is this a MimeMultiPart?
+ *
+ * @return true or false
+ */
+ bool is_multipart() const { return GMIME_IS_MULTIPART(self());}
+
+ /**
+ * Is this a MimeMultiPart?
+ *
+ * @return true or false
+ */
+ bool is_multipart_encrypted() const {
+ return GMIME_IS_MULTIPART_ENCRYPTED(self());
+ }
+
+ /**
+ * Is this a MimeMultiPart?
+ *
+ * @return true or false
+ */
+ bool is_multipart_signed() const {
+ return GMIME_IS_MULTIPART_SIGNED(self());
+ }
+
+ /**
+ * Is this a MimeMessage?
+ *
+ * @return true or false
+ */
+ bool is_message() const { return GMIME_IS_MESSAGE(self());}
+
+ /**
+ * Is this a MimeMessagePart?
+ *
+ * @return true orf alse
+ */
+ bool is_message_part() const { return GMIME_IS_MESSAGE_PART(self());}
+
+ /**
+ * Is this a MimeApplicationpkcs7Mime?
+ *
+ * @return true orf alse
+ */
+ bool is_mime_application_pkcs7_mime() const {
+ return GMIME_IS_APPLICATION_PKCS7_MIME(self());
+ }
+
+ /**
+ * Callback for for_each(). See GMimeObjectForEachFunc.
+ *
+ */
+ using ForEachFunc = std::function<void(const MimeObject& parent,
+ const MimeObject& part)>;
+
+private:
+ GMimeObject* self() const {
+ return reinterpret_cast<GMimeObject*>(object());
+ }
+};
+
+\f
+/**
+ * Thin wrapper around a GMimeMessage
+ *
+ */
+class MimeMessage: public MimeObject {
+public:
+ /**
+ * Construct a MimeMessage
+ *
+ * @param obj an Object of the right type
+ */
+ MimeMessage(const Object& obj): MimeObject(obj) {
+ if (!is_message())
+ throw std::runtime_error("not a mime-message");
+ }
+
+ /**
+ * Make a MimeMessage from a file
+ *
+ * @param path path to the file
+ *
+ * @return a MimeMessage or an error.
+ */
+ static Result<MimeMessage> make_from_file (const std::string& path);
+
+ /**
+ * Make a MimeMessage from a string
+ *
+ * @param path path to the file
+ *
+ * @return a MimeMessage or an error.
+ */
+ static Result<MimeMessage> make_from_text (const std::string& text);
+
+ /**
+ * Get the contacts of a given type, or None for _all_
+ *
+ * @param ctype contact type
+ *
+ * @return contacts
+ */
+ Contacts contacts(Contact::Type ctype) const noexcept;
+
+ /**
+ * Gets the message-id if it exists, or nullopt otherwise.
+ *
+ * @return string or nullopt
+ */
+ Option<std::string> message_id() const noexcept {
+ return Mu::to_string_opt(g_mime_message_get_message_id(self()));
+ }
+
+ /**
+ * Gets the message-id if it exists, or nullopt otherwise.
+ *
+ * @return string or nullopt
+ */
+ Option<std::string> subject() const noexcept {
+ return Mu::to_string_opt(g_mime_message_get_subject(self()));
+ }
+
+ /**
+ * Gets the date if it exists, or nullopt otherwise.
+ *
+ * @return a time_t value (expressed as a 64-bit number) or nullopt
+ */
+ Option<int64_t> date() const noexcept;
+
+
+ /**
+ * Get the references for this message (including in-reply-to), in the
+ * order of older..newer; the first one would the oldest parent, and
+ * in-reply-to would be the last one (if any). These are de-duplicated,
+ * and known-fake references removed (see implementation)
+ *
+ * @return references.
+ */
+ std::vector<std::string> references() const noexcept;
+
+
+ /**
+ * Recursively apply func tol all parts of this message
+ *
+ * @param func a function
+ */
+ void for_each(const ForEachFunc& func) const noexcept;
+
+private:
+ GMimeMessage* self() const {
+ return reinterpret_cast<GMimeMessage*>(object());
+ }
+};
+\f
+/**
+ * Thin wrapper around a GMimePart.
+ *
+ */
+class MimePart: public MimeObject {
+public:
+ /**
+ * Construct a MimePart
+ *
+ * @param obj an Object of the right type
+ */
+ MimePart(const Object& obj): MimeObject(obj) {
+ if (!is_part())
+ throw std::runtime_error("not a mime-part");
+ }
+
+ /**
+ * Determines whether or not the part is an attachment based on the
+ * value of the Content-Disposition header.
+ *
+ * @return true or false
+ */
+ bool is_attachment() const noexcept {
+ return g_mime_part_is_attachment(self());
+ }
+
+ /**
+ * Gets the value of the Content-Description for this mime part
+ * if it exists, or nullopt otherwise.
+ *
+ * @return string or nullopt
+ */
+ Option<std::string> content_description() const noexcept {
+ return Mu::to_string_opt(g_mime_part_get_content_description(self()));
+ }
+
+ /**
+ * Gets the value of the Content-Id for this mime part
+ * if it exists, or nullopt otherwise.
+ *
+ * @return string or nullopt
+ */
+ Option<std::string> content_id() const noexcept {
+ return Mu::to_string_opt(g_mime_part_get_content_id(self()));
+ }
+
+ /**
+ * Gets the value of the Content-Md5 header for this mime part
+ * if it exists, or nullopt otherwise.
+ *
+ * @return string or nullopt
+ */
+ Option<std::string> content_md5() const noexcept {
+ return Mu::to_string_opt(g_mime_part_get_content_md5(self()));
+
+ }
+
+ /**
+ * Verify the content md5 for the specified mime part. Returns false if
+ * the mime part does not contain a Content-MD5.
+ *
+ * @return true or false
+ */
+ bool verify_content_md5() const noexcept {
+ return g_mime_part_verify_content_md5(self());
+ }
+
+ /**
+ * Gets the value of the Content-Location for this mime part if it
+ * exists, or nullopt otherwise.
+ *
+ * @return string or nullopt
+ */
+ Option<std::string> content_location() const noexcept {
+ return Mu::to_string_opt(g_mime_part_get_content_location(self()));
+ }
+
+
+ MimeDataWrapper content() const noexcept {
+ return MimeDataWrapper{g_mime_part_get_content(self())};
+ }
+
+ /**
+ * Gets the filename for this mime part if it exists, or nullopt
+ * otherwise.
+ *
+ * @return string or nullopt
+ */
+ Option<std::string> filename() const noexcept {
+ return Mu::to_string_opt(g_mime_part_get_filename(self()));
+ }
+
+ /**
+ * Size of content, in bytes
+ *
+ * @return size
+ */
+ size_t size() const noexcept;
+
+ /**
+ * Get as UTF-8 string
+ *
+ * @return a string, or NULL.
+ */
+ Option<std::string> to_string() const noexcept;
+
+ /**
+ * Write part to a file
+ *
+ * @param path path to file
+ * @param overwrite if true, overwrite existing file, if it \18bqexists
+ *
+ * @return size of the wrtten file, or an error.
+ */
+ Result<size_t> to_file(const std::string& path, bool overwrite)
+ const noexcept;
+
+ /**
+ * Types of Content Encoding.
+ *
+ */
+ enum struct ContentEncoding {
+ Default = GMIME_CONTENT_ENCODING_DEFAULT,
+ SevenBit = GMIME_CONTENT_ENCODING_7BIT,
+ EightBit = GMIME_CONTENT_ENCODING_8BIT,
+ Binary = GMIME_CONTENT_ENCODING_BINARY,
+ Base64 = GMIME_CONTENT_ENCODING_BASE64,
+ QuotedPrintable = GMIME_CONTENT_ENCODING_QUOTEDPRINTABLE,
+ UuEncode = GMIME_CONTENT_ENCODING_UUENCODE
+ };
+
+ /**
+ * Gets the content encoding of the mime part.
+ *
+ * @return the content encoding
+ */
+ ContentEncoding content_encoding() const noexcept {
+ const auto enc{g_mime_part_get_content_encoding(self())};
+ g_return_val_if_fail(enc <= GMIME_CONTENT_ENCODING_UUENCODE,
+ ContentEncoding::Default);
+ return static_cast<ContentEncoding>(enc);
+ }
+
+
+ /**
+ * Types of OpenPGP data
+ *
+ */
+ enum struct OpenPGPData {
+ None = GMIME_OPENPGP_DATA_NONE,
+ Encrypted = GMIME_OPENPGP_DATA_ENCRYPTED,
+ Signed = GMIME_OPENPGP_DATA_SIGNED,
+ PublicKey = GMIME_OPENPGP_DATA_PUBLIC_KEY,
+ PrivateKey = GMIME_OPENPGP_DATA_PRIVATE_KEY,
+ };
+
+ /**
+ * Gets whether or not (and what type) of OpenPGP data is contained
+ *
+ * @return OpenGPGData
+ */
+ OpenPGPData openpgp_data() const noexcept {
+ const auto data{g_mime_part_get_openpgp_data(self())};
+ g_return_val_if_fail(data <= GMIME_OPENPGP_DATA_PRIVATE_KEY,
+ OpenPGPData::None);
+ return static_cast<OpenPGPData>(data);
+ }
+
+private:
+ GMimePart* self() const {
+ return reinterpret_cast<GMimePart*>(object());
+ }
+};
+
+
+\f
+/**
+ * Thin wrapper around a GMimeMessagePart.
+ *
+ */
+class MimeMessagePart: public MimeObject {
+public:
+ /**
+ * Construct a MimeMessagePart
+ *
+ * @param obj an Object of the right type
+ */
+ MimeMessagePart(const Object& obj): MimeObject(obj) {
+ if (!is_message_part())
+ throw std::runtime_error("not a mime-message-part");
+ }
+
+ /**
+ * Get the MimeMessage for this MimeMessagePart.
+ *
+ * @return the MimeMessage or Nothing
+ */
+ Option<MimeMessage> get_message() const {
+ auto msg{g_mime_message_part_get_message(self())};
+ if (msg)
+ return MimeMessage(Object(G_OBJECT(msg)));
+ else
+ return Nothing;
+ }
+private:
+ GMimeMessagePart* self() const {
+ return reinterpret_cast<GMimeMessagePart*>(object());
+ }
+
+};
+\f/**
+ * Thin wrapper around a GMimeApplicationPkcs7Mime
+ *
+ */
+class MimeApplicationPkcs7Mime: public MimePart {
+public:
+ /**
+ * Construct a MimeApplicationPkcs7Mime
+ *
+ * @param obj an Object of the right type
+ */
+ MimeApplicationPkcs7Mime(const Object& obj): MimePart(obj) {
+ if (!is_mime_application_pkcs7_mime())
+ throw std::runtime_error("not a mime-application-pkcs7-mime");
+ }
+
+ enum struct SecureMimeType {
+ CompressedData = GMIME_SECURE_MIME_TYPE_COMPRESSED_DATA,
+ EnvelopedData = GMIME_SECURE_MIME_TYPE_ENVELOPED_DATA,
+ SignedData = GMIME_SECURE_MIME_TYPE_SIGNED_DATA,
+ CertsOnly = GMIME_SECURE_MIME_TYPE_CERTS_ONLY,
+ Unknown = GMIME_SECURE_MIME_TYPE_UNKNOWN
+ };
+
+ SecureMimeType smime_type() const {
+ return static_cast<SecureMimeType>(
+ g_mime_application_pkcs7_mime_get_smime_type(self()));
+ }
+
+private:
+ GMimeApplicationPkcs7Mime* self() const {
+ return reinterpret_cast<GMimeApplicationPkcs7Mime*>(object());
+ }
+};
+
+\f
+/**
+ * Thin wrapper around a GMimeMultiPart
+ *
+ */
+class MimeMultipart: public MimeObject {
+public:
+ /**
+ * Construct a MimeMultipart
+ *
+ * @param obj an Object of the right type
+ */
+ MimeMultipart(const Object& obj): MimeObject(obj) {
+ if (!is_multipart())
+ throw std::runtime_error("not a mime-multipart");
+ }
+
+ Option<MimePart> signed_content_part() const {
+ return part(GMIME_MULTIPART_SIGNED_CONTENT);
+ }
+
+ Option<MimePart> signed_signature_part() const {
+ return part(GMIME_MULTIPART_SIGNED_SIGNATURE);
+ }
+
+ Option<MimePart> encrypted_version_part() const {
+ return part(GMIME_MULTIPART_ENCRYPTED_VERSION);
+ }
+
+ Option<MimePart> encrypted_content_part() const {
+ return part(GMIME_MULTIPART_ENCRYPTED_CONTENT);
+ }
+
+ /**
+ * Recursively apply func to all parts
+ *
+ * @param func a function
+ */
+ void for_each(const ForEachFunc& func) const noexcept;
+
+private:
+ // Note: the part may not be available if the message was marked as
+ // _signed_ or _encrypted_ because it contained a forwarded signed or
+ // encrypted message.
+ Option<MimePart> part(int index) const {
+ if (auto&& p{g_mime_multipart_get_part(self() ,index)};
+ !GMIME_IS_PART(p))
+ return Nothing;
+ else
+ return Some(MimeObject{p});
+ }
+
+ GMimeMultipart* self() const {
+ return reinterpret_cast<GMimeMultipart*>(object());
+ }
+};
+
+\f
+/**
+ * Thin wrapper around a GMimeMultiPartEncrypted
+ *
+ */
+class MimeMultipartEncrypted: public MimeMultipart {
+public:
+ /**
+ * Construct a MimeMultipartEncrypted
+ *
+ * @param obj an Object of the right type
+ */
+ MimeMultipartEncrypted(const Object& obj): MimeMultipart(obj) {
+ if (!is_multipart_encrypted())
+ throw std::runtime_error("not a mime-multipart-encrypted");
+ }
+
+ enum struct DecryptFlags {
+ None = GMIME_DECRYPT_NONE,
+ ExportSessionKey = GMIME_DECRYPT_EXPORT_SESSION_KEY,
+ NoVerify = GMIME_DECRYPT_NO_VERIFY,
+ EnableKeyserverLookups = GMIME_DECRYPT_ENABLE_KEYSERVER_LOOKUPS,
+ EnableOnlineCertificateChecks = GMIME_DECRYPT_ENABLE_ONLINE_CERTIFICATE_CHECKS
+ };
+
+ using Decrypted = std::pair<MimeObject, MimeDecryptResult>;
+ Result<Decrypted> decrypt(const MimeCryptoContext& ctx,
+ DecryptFlags flags=DecryptFlags::None,
+ const std::string& session_key = {}) const noexcept;
+
+private:
+ GMimeMultipartEncrypted* self() const {
+ return reinterpret_cast<GMimeMultipartEncrypted*>(object());
+ }
+};
+
+MU_ENABLE_BITOPS(MimeMultipartEncrypted::DecryptFlags);
+
+\f
+/**
+ * Thin wrapper around a GMimeMultiPartSigned
+ *
+ */
+class MimeMultipartSigned: public MimeMultipart {
+public:
+ /**
+ * Construct a MimeMultipartSigned
+ *
+ * @param obj an Object of the right type
+ */
+ MimeMultipartSigned(const Object& obj): MimeMultipart(obj) {
+ if (!is_multipart_signed())
+ throw std::runtime_error("not a mime-multipart-signed");
+ }
+
+ enum struct VerifyFlags {
+ None = GMIME_VERIFY_NONE,
+ EnableKeyserverLookups = GMIME_VERIFY_ENABLE_KEYSERVER_LOOKUPS,
+ EnableOnlineCertificateChecks = GMIME_VERIFY_ENABLE_ONLINE_CERTIFICATE_CHECKS
+ };
+
+ // Result<std::vector<MimeSignature>> verify(VerifyFlags vflags=VerifyFlags::None) const noexcept;
+
+ Result<std::vector<MimeSignature>> verify(const MimeCryptoContext& ctx,
+ VerifyFlags vflags=VerifyFlags::None) const noexcept;
+
+private:
+ GMimeMultipartSigned* self() const {
+ return reinterpret_cast<GMimeMultipartSigned*>(object());
+ }
+};
+
+
+MU_ENABLE_BITOPS(MimeMultipartSigned::VerifyFlags);
+
+} // namespace Mu
+
+
+#endif /* MU_MIME_OBJECT_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-priority.hh"
+
+using namespace Mu;
+
+std::string
+Mu::to_string(Priority prio)
+{
+ return std::string{priority_name(prio)};
+}
+
+/*
+ * tests... also build as runtime-tests, so we can get coverage info
+ */
+#ifdef BUILD_TESTS
+#include <glib.h>
+#define static_assert g_assert_true
+#endif /*BUILD_TESTS*/
+
+[[maybe_unused]] static void
+test_priority_to_char()
+{
+ static_assert(to_char(Priority::Low) == 'l');
+ static_assert(to_char(Priority::Normal) == 'n');
+ static_assert(to_char(Priority::High) == 'h');
+}
+
+[[maybe_unused]] static void
+test_priority_from_char()
+{
+ static_assert(priority_from_char('l') == Priority::Low);
+ static_assert(priority_from_char('n') == Priority::Normal);
+ static_assert(priority_from_char('h') == Priority::High);
+ static_assert(priority_from_char('x') == Priority::Normal);
+}
+
+[[maybe_unused]] static void
+test_priority_name()
+{
+ static_assert(priority_name(Priority::Low) == "low");
+ static_assert(priority_name(Priority::Normal) == "normal");
+ static_assert(priority_name(Priority::High) == "high");
+}
+
+
+#ifdef BUILD_TESTS
+int
+main(int argc, char* argv[])
+{
+ g_test_init(&argc, &argv, NULL);
+
+ g_test_add_func("/message/priority/to-char", test_priority_to_char);
+ g_test_add_func("/message/priority/from-char", test_priority_from_char);
+ g_test_add_func("/message/priority/name", test_priority_name);
+
+ return g_test_run();
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_PRIORITY_HH__
+#define MU_PRIORITY_HH__
+
+#include <array>
+#include <string>
+#include <string_view>
+#include "mu-fields.hh"
+
+namespace Mu {
+/**
+ * Message priorities
+ *
+ */
+
+/**
+ * The priority ids
+ *
+ */
+enum struct Priority : char {
+ Low = 'l', /**< Low priority */
+ Normal = 'n', /**< Normal priority */
+ High = 'h', /**< High priority */
+};
+
+/**
+ * Sequence of all message priorities.
+ */
+static constexpr std::array<Priority, 3> AllMessagePriorities = {
+ Priority::Low, Priority::Normal, Priority::High};
+
+/**
+ * Get the char for some priority
+ *
+ * @param id an id
+ *
+ * @return the char
+ */
+constexpr char
+to_char(Priority prio)
+{
+ return static_cast<char>(prio);
+}
+
+/**
+ * Get the priority for some character; unknown ones
+ * become Normal.
+ *
+ * @param c some character
+ */
+constexpr Priority
+priority_from_char(char c)
+{
+ switch (c) {
+ case 'l':
+ return Priority::Low;
+ case 'h':
+ return Priority::High;
+ case 'n':
+ default:
+ return Priority::Normal;
+ }
+}
+
+/**
+ * Get the priority from their (internal) name, i.e., low/normal/high
+ * or shortcut.
+ *
+ * @param pname
+ *
+ * @return the priority or none
+ */
+static inline Option<Priority>
+priority_from_name(std::string_view pname)
+{
+ if (pname == "low" || pname == "l")
+ return Priority::Low;
+ else if (pname == "high" || pname == "h")
+ return Priority::High;
+ else if (pname == "normal" || pname == "n")
+ return Priority::Normal;
+ else
+ return Nothing;
+}
+
+
+/**
+ * Get the name for a given priority
+ *
+ * @return the name
+ */
+constexpr std::string_view
+priority_name(Priority prio)
+{
+ switch (prio) {
+ case Priority::Low:
+ return "low";
+ case Priority::High:
+ return "high";
+ case Priority::Normal:
+ default:
+ return "normal";
+ }
+}
+
+/**
+ * Get the name for a given priority (backward compatibility)
+ *
+ * @return the name
+ */
+constexpr const char*
+priority_name_c_str(Priority prio)
+{
+ switch (prio) {
+ case Priority::Low:
+ return "low";
+ case Priority::High:
+ return "high";
+ case Priority::Normal:
+ default:
+ return "normal";
+ }
+}
+
+/**
+ * Get a the message priority as a string
+ *
+ * @param prio priority
+ *
+ * @return a string
+ */
+std::string to_string(Priority prio);
+
+} // namespace Mu
+
+#endif /*MU_PRIORITY_HH_*/
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include "utils/mu-test-utils.hh"
+#include "mu-message.hh"
+#include "mu-mime-object.hh"
+#include <glib.h>
+#include <regex>
+
+using namespace Mu;
+
+/*
+ * test message 1
+ */
+
+static void
+test_message_mailing_list()
+{
+ constexpr const char *test_message_1 =
+R"(Return-Path: <sqlite-dev-bounces@sqlite.org>
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+ by mail.xxxxsoftware.nl (Postfix) with ESMTP id 32F276963F
+ for <xxxx@localhost>; Mon, 4 Aug 2008 21:49:34 +0300 (EEST)
+Message-Id: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net>
+From: anon@example.com
+To: sqlite-dev@sqlite.org
+Mime-Version: 1.0 (Apple Message framework v926)
+Date: Mon, 4 Aug 2008 11:40:49 +0200
+X-Mailer: Apple Mail (2.926)
+Subject: [sqlite-dev] VM optimization inside sqlite3VdbeExec
+Precedence: list
+Reply-To: sqlite-dev@sqlite.org
+List-Id: <sqlite-dev.sqlite.org>
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Sender: sqlite-dev-bounces@sqlite.org
+Content-Length: 639
+
+Inside sqlite3VdbeExec there is a very big switch statement.
+In order to increase performance with few modifications to the
+original code, why not use this technique ?
+http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html
+
+With a properly defined "instructions" array, instead of the switch
+statement you can use something like:
+goto * instructions[pOp->opcode];
+)";
+ auto message{Message::make_from_text(
+ test_message_1,
+ "/home/test/Maildir/inbox/cur/1649279256.107710_1.evergrey:2,S")};
+ g_assert_true(!!message);
+ assert_equal(message->path(),
+ "/home/test/Maildir/inbox/cur/1649279256.107710_1.evergrey:2,S");
+ g_assert_true(message->maildir().empty());
+
+ g_assert_true(message->bcc().empty());
+
+ g_assert_true(!message->body_html());
+ assert_equal(message->body_text().value_or(""),
+R"(Inside sqlite3VdbeExec there is a very big switch statement.
+In order to increase performance with few modifications to the
+original code, why not use this technique ?
+http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html
+
+With a properly defined "instructions" array, instead of the switch
+statement you can use something like:
+goto * instructions[pOp->opcode];
+)");
+ g_assert_true(message->cc().empty());
+ g_assert_cmpuint(message->date(), ==, 1217842849);
+ g_assert_true(message->flags() == (Flags::MailingList | Flags::Seen));
+
+ const auto from{message->from()};
+ g_assert_cmpuint(from.size(),==,1);
+ assert_equal(from.at(0).name, "");
+ assert_equal(from.at(0).email, "anon@example.com");
+
+ assert_equal(message->mailing_list(), "sqlite-dev.sqlite.org");
+ assert_equal(message->message_id(),
+ "83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net");
+
+ g_assert_true(message->priority() == Priority::Low);
+ g_assert_cmpuint(message->size(),==,::strlen(test_message_1));
+
+ /* text-based message use time({}) as their changed-time */
+ g_assert_cmpuint(::time({}) - message->changed(), >=, 0);
+ g_assert_cmpuint(::time({}) - message->changed(), <=, 2);
+
+ g_assert_true(message->references().empty());
+
+ assert_equal(message->subject(),
+ "[sqlite-dev] VM optimization inside sqlite3VdbeExec");
+
+ const auto to{message->to()};
+ g_assert_cmpuint(to.size(),==,1);
+ assert_equal(to.at(0).name, "");
+ assert_equal(to.at(0).email, "sqlite-dev@sqlite.org");
+
+ assert_equal(message->header("X-Mailer").value_or(""), "Apple Mail (2.926)");
+
+ auto all_contacts{message->all_contacts()};
+ g_assert_cmpuint(all_contacts.size(), ==, 4);
+ seq_sort(all_contacts, [](auto&& c1, auto&& c2){return c1.email < c2.email; });
+ assert_equal(all_contacts[0].email, "anon@example.com");
+ assert_equal(all_contacts[1].email, "sqlite-dev-bounces@sqlite.org");
+ assert_equal(all_contacts[2].email, "sqlite-dev@sqlite.org");
+ assert_equal(all_contacts[3].email, "sqlite-dev@sqlite.org");
+}
+
+
+static void
+test_message_attachments(void)
+{
+ constexpr const char* msg_text =
+R"(Return-Path: <foo@example.com>
+Received: from pop.gmail.com [256.85.129.309]
+ by evergrey with POP3 (fetchmail-6.4.29)
+ for <djcb@localhost> (single-drop); Thu, 24 Mar 2022 20:12:40 +0200 (EET)
+Sender: "Foo, Example" <foo@example.com>
+User-agent: mu4e 1.7.11; emacs 29.0.50
+From: "Foo Example" <foo@example.com>
+To: bar@example.com
+Subject: =?utf-8?B?w6R0dMOkY2htZcOxdHM=?=
+Date: Thu, 24 Mar 2022 20:04:39 +0200
+Organization: ACME Inc.
+Message-Id: <3144HPOJ0VC77.3H1XTAG2AMTLH@"@WILSONB.COM>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="=-=-="
+
+--=-=-=
+Content-Type: text/plain
+
+Hello,
+--=-=-=
+Content-Type: image/jpeg
+Content-Disposition: attachment; filename=file-01.bin
+Content-Transfer-Encoding: base64
+
+AAECAw==
+--=-=-=
+Content-Type: audio/ogg
+Content-Disposition: inline; filename=/tmp/file-02.bin
+Content-Transfer-Encoding: base64
+
+BAUGBw==
+--=-=-=
+Content-Type: message/rfc822
+Content-Disposition: attachment;
+ filename="message.eml"
+
+From: "Fnorb" <fnorb@example.com>
+To: Bob <bob@example.com>
+Subject: news for you
+Date: Mon, 28 Mar 2022 22:53:26 +0300
+
+Attached message!
+
+--=-=-=
+Content-Type: text/plain
+
+World!
+--=-=-=--
+)";
+
+ auto message{Message::make_from_text(msg_text)};
+ g_assert_true(!!message);
+ g_assert_true(message->has_mime_message());
+ g_assert_true(message->path().empty());
+
+ g_assert_true(message->bcc().empty());
+ g_assert_true(!message->body_html());
+ assert_equal(message->body_text().value_or(""), R"(Hello,World!)");
+
+ g_assert_true(message->cc().empty());
+ g_assert_cmpuint(message->date(), ==, 1648145079);
+ /* no Flags::Unread since it's a message without path */
+ g_assert_true(message->flags() == (Flags::HasAttachment));
+
+ const auto from{message->from()};
+ g_assert_cmpuint(from.size(),==,1);
+ assert_equal(from.at(0).name, "Foo Example");
+ assert_equal(from.at(0).email, "foo@example.com");
+
+ // problem case: https://github.com/djcb/mu/issues/2232o
+ assert_equal(message->message_id(),
+ "3144HPOJ0VC77.3H1XTAG2AMTLH@\"@WILSONB.COM");
+
+ g_assert_true(message->path().empty());
+ g_assert_true(message->priority() == Priority::Normal);
+ g_assert_cmpuint(message->size(),==,::strlen(msg_text));
+
+ /* text-based message use time({}) as their changed-time */
+ g_assert_cmpuint(::time({}) - message->changed(), >=, 0);
+ g_assert_cmpuint(::time({}) - message->changed(), <=, 2);
+
+ assert_equal(message->subject(), "ättächmeñts");
+
+ const auto cache_path{message->cache_path()};
+ g_assert_true(!!cache_path);
+
+ g_assert_cmpuint(message->parts().size(),==,5);
+ {
+ auto&& part{message->parts().at(0)};
+ g_assert_false(!!part.raw_filename());
+ assert_equal(part.mime_type().value(), "text/plain");
+ assert_equal(part.to_string().value(), "Hello,");
+ }
+ {
+ auto&& part{message->parts().at(1)};
+ assert_equal(part.raw_filename().value(), "file-01.bin");
+ assert_equal(part.mime_type().value(), "image/jpeg");
+ // file consists of 4 bytes 0...3
+ g_assert_cmpuint(part.to_string()->at(0), ==, 0);
+ g_assert_cmpuint(part.to_string()->at(1), ==, 1);
+ g_assert_cmpuint(part.to_string()->at(2), ==, 2);
+ g_assert_cmpuint(part.to_string()->at(3), ==, 3);
+ }
+ {
+ auto&& part{message->parts().at(2)};
+ assert_equal(part.raw_filename().value(), "/tmp/file-02.bin");
+ assert_equal(part.cooked_filename().value(), "file-02.bin");
+ assert_equal(part.mime_type().value(), "audio/ogg");
+ // file consistso of 4 bytes 4..7
+ assert_equal(part.to_string().value(), "\004\005\006\007");
+ const auto fpath{*cache_path + part.cooked_filename().value()};
+ const auto res = part.to_file(fpath, true);
+
+ g_assert_cmpuint(*res,==,4);
+ g_assert_cmpuint(::access(fpath.c_str(), R_OK), ==, 0);
+ }
+
+ {
+ auto&& part{message->parts().at(3)};
+ g_assert_true(part.mime_type() == "message/rfc822");
+
+ const auto fname{*cache_path + "/msgpart"};
+ g_assert_cmpuint(part.to_file(fname, true).value_or(123), ==, 139);
+ g_assert_true(::access(fname.c_str(), F_OK) == 0);
+ }
+
+ {
+ auto&& part{message->parts().at(4)};
+ g_assert_false(!!part.raw_filename());
+ g_assert_true(!!part.mime_type());
+ assert_equal(part.mime_type().value(), "text/plain");
+ assert_equal(part.to_string().value(), "World!");
+ }
+}
+
+
+/*
+ * some test keys.
+ */
+
+constexpr std::string_view pub_key =
+R"(-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mDMEYlbaNhYJKwYBBAHaRw8BAQdAEgxZnlN3mIwqV89zchjFlEby8OgrbrkT+yRN
+hQhc+A+0LU11IFRlc3QgKG11IHRlc3Rpbmcga2V5KSA8bXVAZGpjYnNvZnR3YXJl
+Lm5sPoiUBBMWCgA8FiEE/HZRT+2bPjARz29Cw7FsU49t3vAFAmJW2jYCGwMFCwkI
+BwIDIgIBBhUKCQgLAgQWAgMBAh4HAheAAAoJEMOxbFOPbd7wJ2kBAIGmUDWYEPtn
+qYTwhZIdZtTa4KJ3UdtTqey9AnxJ9mzAAQDRJOoVppj5wW2xRhgYP+ysN2iBUYGE
+MhahOcNgxodbCLg4BGJW2jYSCisGAQQBl1UBBQEBB0D4Sp+GTVre7Cx5a8D3SwLJ
+/bRAVGDwqI7PL9B/cMmCTwMBCAeIeAQYFgoAIBYhBPx2UU/tmz4wEc9vQsOxbFOP
+bd7wBQJiVto2AhsMAAoJEMOxbFOPbd7w1tYA+wdfYCcwOP0QoNZZz2Yk12YkDk2R
+FsRrZZpb0GKC/a2VAP4qFceeSegcUCBTQaoeFE9vq9XiUVOO98QI8r9C8QwvBw==
+=jM/g
+-----END PGP PUBLIC KEY BLOCK-----
+)";
+
+constexpr std::string_view priv_key = // "test1234"
+R"(-----BEGIN PGP PRIVATE KEY BLOCK-----
+
+lIYEYlbaNhYJKwYBBAHaRw8BAQdAEgxZnlN3mIwqV89zchjFlEby8OgrbrkT+yRN
+hQhc+A/+BwMCz6T2uBpk6a7/rXyE7C1bRbGjP6YSFcyRFz8VRV3Xlm7z6rdbdKZr
+8R15AtLvXA4DOK5GiZRB2VbIxi8B9CtZ9qQx6YbQPkAmRzISGAjECrQtTXUgVGVz
+dCAobXUgdGVzdGluZyBrZXkpIDxtdUBkamNic29mdHdhcmUubmw+iJQEExYKADwW
+IQT8dlFP7Zs+MBHPb0LDsWxTj23e8AUCYlbaNgIbAwULCQgHAgMiAgEGFQoJCAsC
+BBYCAwECHgcCF4AACgkQw7FsU49t3vAnaQEAgaZQNZgQ+2ephPCFkh1m1NrgondR
+21Op7L0CfEn2bMABANEk6hWmmPnBbbFGGBg/7Kw3aIFRgYQyFqE5w2DGh1sInIsE
+YlbaNhIKKwYBBAGXVQEFAQEHQPhKn4ZNWt7sLHlrwPdLAsn9tEBUYPCojs8v0H9w
+yYJPAwEIB/4HAwI9MZDWcsoiJ/9oV5DRiAedeo3Ta/1M+aKfeNV36Ch1VGLwQF3E
+V77qIrJlsT8CwOZHWUksUBENvG3ak3vd84awHHaHoTmoFwtISfvQrFK0iHgEGBYK
+ACAWIQT8dlFP7Zs+MBHPb0LDsWxTj23e8AUCYlbaNgIbDAAKCRDDsWxTj23e8NbW
+APsHX2AnMDj9EKDWWc9mJNdmJA5NkRbEa2WaW9Bigv2tlQD+KhXHnknoHFAgU0Gq
+HhRPb6vV4lFTjvfECPK/QvEMLwc=
+=w1Nc
+-----END PGP PRIVATE KEY BLOCK-----
+)";
+
+
+static void
+test_message_signed(void)
+{
+ constexpr const char *msgtext =
+R"(Return-Path: <diggler@gmail.com>
+From: Mu Test <mu@djcbsoftware.nl>
+To: Mu Test <mu@djcbsoftware.nl>
+Subject: boo
+Date: Wed, 13 Apr 2022 17:19:08 +0300
+Message-ID: <878rs9ysin.fsf@djcbsoftware.nl>
+MIME-Version: 1.0
+Content-Type: multipart/signed; boundary="=-=-=";
+ micalg=pgp-sha512; protocol="application/pgp-signature"
+
+--=-=-=
+Content-Type: text/plain
+
+Sapperdeflap
+
+--=-=-=
+Content-Type: application/pgp-signature; name="signature.asc"
+
+-----BEGIN PGP SIGNATURE-----
+
+iIkEARYKADEWIQT8dlFP7Zs+MBHPb0LDsWxTj23e8AUCYlbcLhMcbXVAZGpjYnNv
+ZnR3YXJlLm5sAAoJEMOxbFOPbd7waIkA/jK1oY7OL8vrDoubNYxamy8HHmwtvO01
+Q46aYjxe0As6AP90bcAZ3dcn5RcTJaM0UhZssguawZ+tnriD3+5DPkMMCg==
+=e32+
+-----END PGP SIGNATURE-----
+--=-=-=--
+)";
+ TempDir tempdir;
+ auto ctx{MimeCryptoContext::make_gpg(tempdir.path())};
+ g_assert_true(!!ctx);
+
+ auto stream{MimeStream::make_mem()};
+ stream.write(pub_key.data(), pub_key.size());
+ stream.reset();
+
+ auto imported = ctx->import_keys(stream);
+ g_assert_cmpuint(*imported, ==, 1);
+
+ auto message{Message::make_from_text(
+ msgtext,
+ "/home/test/Maildir/inbox/cur/1649279777.107710_1.mindcrime:2,RS")};
+ g_assert_true(!!message);
+
+ g_assert_true(message->bcc().empty());
+ assert_equal(message->body_text().value_or(""), "Sapperdeflap\n");
+ g_assert_true(message->flags() == (Flags::Signed|Flags::Seen|Flags::Replied));
+
+ size_t n{};
+ for (auto&& part: message->parts()) {
+ if (!part.is_signed())
+ continue;
+
+ const auto& mobj{part.mime_object()};
+ if (!mobj.is_multipart_signed())
+ continue;
+
+ const auto mpart{MimeMultipartSigned(mobj)};
+ const auto sigs{mpart.verify(*ctx)};
+ if (!sigs)
+ mu_warning("{}", sigs.error().what());
+
+ g_assert_true(!!sigs);
+ g_assert_cmpuint(sigs->size(), ==, 1);
+ ++n;
+ }
+
+ g_assert_cmpuint(n, ==, 1);
+}
+
+
+static void
+test_message_signed_encrypted(void)
+{
+ constexpr const char *msgtext =
+R"(From: "Mu Test" <mu@djcbsoftware.nl>
+To: mu@djcbsoftware.nl
+Subject: encrypted and signed
+Date: Wed, 13 Apr 2022 17:32:30 +0300
+Message-ID: <87lew9xddt.fsf@djcbsoftware.nl>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+ protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+
+hF4DeEerj6WhdZASAQdAKdZwmugAlQA8c06Q5iQw4rwSADgfEWBTWlI6tDw7hEAw
+0qSSeeQbA802qjG5TesaDVbFoPp1gOESt67HkJBABj9niwZLnjbzVRXKFoPTYabu
+1MBWAQkCEO6kS0N73XQeJ9+nDkUacRX6sSgVM0j+nRdCGcrCQ8MOfLd9KUUBxpXy
+r/rIBMpZGOIpKJnoZ2x75VsQIp/ADHLe9zzXVe0tkahXJqvLo26w3gn4NSEIEDp6
+4T/zMZImqGrENaixNmRiRSAnwPkLt95qJGOIqYhuW3X6hMRZyU4zDNwkAvnK+2Fv
+Wjd+EmiFzh5tvCmPOSj556YFMV7UpFWO9VznXX/T5+f4i+95Lsm9Uotv/SiNtNQG
+DPU3wiL347SzmPFXckknjlzSzDL1XbdbHdmoJs0uNnbaZxRwhkuTYbLHdpBZrBgR
+C0bdoCx44QVU8HaZ2x91h3GoM/0q5bqM/rvCauwbokiJgAUrznecNPY=
+=Ado7
+-----END PGP MESSAGE-----
+--=-=-=--
+)";
+ TempDir tempdir;
+ auto ctx{MimeCryptoContext::make_gpg(tempdir.path())};
+ g_assert_true(!!ctx);
+
+ /// test1234
+ // ctx->set_request_password([](const MimeCryptoContext& ctx,
+ // const std::string& user_id,
+ // const std::string& prompt,
+ // bool reprompt,
+ // MimeStream& response)->Result<void> {
+ // return Err(Error::Code::Internal, "boo");
+ // //return Ok();
+ // });
+
+ {
+ auto stream{MimeStream::make_mem()};
+ stream.write(priv_key.data(), priv_key.size());
+ stream.write(pub_key.data(), pub_key.size());
+ stream.reset();
+
+
+ g_assert_cmpint(ctx->import_keys(stream).value_or(-1),==,1);
+ }
+
+ auto message{Message::make_from_text(
+ msgtext,
+ "/home/test/Maildir/inbox/cur/1649279888.107710_1.mindcrime:2,FS")};
+ g_assert_true(!!message);
+ g_assert_true(message->flags() == (Flags::Encrypted|Flags::Seen|Flags::Flagged));
+
+ size_t n{};
+ for (auto&& part: message->parts()) {
+
+ if (!part.is_encrypted())
+ continue;
+
+ g_assert_false(!!part.content_description());
+ g_assert_false(part.is_attachment());
+ g_assert_cmpuint(part.size(),==,0);
+
+ const auto& mobj{part.mime_object()};
+ if (!mobj.is_multipart_encrypted())
+ continue;
+
+ /* FIXME: make this work without user having to
+ * type password */
+
+ // const auto mpart{MimeMultipartEncrypted(mobj)};
+ // const auto decres = mpart.decrypt(*ctx);
+ // assert_valid_result(decres);
+
+ ++n;
+ }
+
+ g_assert_cmpuint(n, ==, 1);
+}
+
+
+static void
+test_message_multipart_mixed_rfc822(void)
+{
+ constexpr const char *msgtext =
+R"(Content-Type: multipart/mixed;
+ boundary="Multipart_Tue_Sep__2_15:42:35_2014-1"
+
+--Multipart_Tue_Sep__2_15:42:35_2014-1
+Content-Type: message/rfc822
+)";
+ auto message{Message::make_from_text(msgtext)};
+ g_assert_true(!!message);
+ //g_assert_true(message->sexp().empty());
+}
+
+
+static void
+test_message_detect_attachment(void)
+{
+ constexpr const char *msgtext =
+R"(From: "DUCK, Donald" <donald@example.com>
+Date: Tue, 3 May 2022 10:26:26 +0300
+Message-ID: <SADKLAJCLKDJLAS-xheQjE__+hS-3tff=pTYpMUyGiJwNGF_DA@mail.gmail.com>
+Subject: =?Windows-1252?Q?Purkuty=F6urakka?=
+To: Hello <moika@example.com>
+Cc: =?iso-8859-1?q?M=FCller=2C?= Mickey <Mickey.Mueller@example.com>
+Content-Type: multipart/mixed; boundary="000000000000e687ed05de166d71"
+
+--000000000000e687ed05de166d71
+Content-Type: multipart/alternative; boundary="000000000000e687eb05de166d6f"
+
+--000000000000e687eb05de166d6f
+Content-Type: text/plain; charset="UTF-8"
+Content-Transfer-Encoding: quoted-printable
+
+fyi
+
+---------- Forwarded message ---------
+From: Fooish Bar <foobar@example.com>
+Date: Tue, 3 May 2022 at 08:59
+Subject: Ty=C3=B6t
+To: "DUCK, Donald" <donald@example.com>
+
+Moi,
+
+--
+
+--000000000000e687eb05de166d6f
+Content-Type: text/html; charset="UTF-8"
+Content-Transfer-Encoding: quoted-printable
+
+abc
+
+--000000000000e687eb05de166d6f--
+--000000000000e687ed05de166d71
+Content-Type: application/pdf;
+ name="test1.pdf"
+Content-Disposition: attachment;
+ filename="test2.pdf"
+Content-Transfer-Encoding: base64
+Content-ID: <18088cfd4bc5517c6321>
+X-Attachment-Id: 18088cfd4bc5517c6321
+
+JVBERi0xLjcKJeLjz9MKNyAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDEgMCBSIC9MYXN0
+TW9kaWZpZWQgKEQ6MjAyMjA1MDMwODU3MzYrMDMnMDAnKSAvUmVzb3VyY2VzIDIgMCBSIC9NZWRp
+cmVmCjM1NjE4CiUlRU9GCg==
+--000000000000e687ed05de166d71--
+)";
+ auto message{Message::make_from_text(msgtext)};
+ g_assert_true(!!message);
+
+ g_assert_true(message->path().empty());
+
+ /* https://groups.google.com/g/mu-discuss/c/kCtrlxMXBjo */
+ g_assert_cmpuint(message->cc().size(),==, 1);
+ assert_equal(message->cc().at(0).email, "Mickey.Mueller@example.com");
+ assert_equal(message->cc().at(0).name, "Müller, Mickey");
+ assert_equal(message->cc().at(0).display_name(), "\"Müller, Mickey\" <Mickey.Mueller@example.com>");
+
+ g_assert_true(message->bcc().empty());
+ assert_equal(message->subject(), "Purkutyöurakka");
+ assert_equal(message->body_html().value_or(""), "abc\n");
+ assert_equal(message->body_text().value_or(""),
+ R"(fyi
+
+---------- Forwarded message ---------
+From: Fooish Bar <foobar@example.com>
+Date: Tue, 3 May 2022 at 08:59
+Subject: Työt
+To: "DUCK, Donald" <donald@example.com>
+
+Moi,
+
+--
+)");
+ g_assert_cmpuint(message->date(), ==, 1651562786);
+ g_assert_true(message->flags() == (Flags::HasAttachment));
+
+ g_assert_cmpuint(message->parts().size(), ==, 3);
+
+ for (auto&& part: message->parts())
+ g_info("%s %s",
+ part.is_attachment() ? "yes" : "no",
+ part.mime_type().value_or("boo").c_str());
+}
+
+
+static void
+test_message_calendar(void)
+{
+ constexpr const char *msgtext =
+R"(MIME-Version: 1.0
+From: William <william@example.com>
+To: Billy <billy@example.com>
+Date: Thu, 9 Jan 2014 11:09:34 +0100
+Subject: Invitation: HELLO, @ Thu 9 Jan 2014 08:30 - 09:30
+ (william@example.com)
+Thread-Topic: Invitation: HELLO, @ Thu 9 Jan 2014 08:30 - 09:30
+ (william@example.com)
+Thread-Index: Ac8NIuske7OtG01VRpukb/bHE7SVHg==
+Message-ID: <001a11c3440066ee0b04ef86cea8@google.com>
+Accept-Language: en-US
+Content-Language: en-US
+X-MS-Exchange-Organization-AuthAs: Anonymous
+X-MS-Has-Attach: yes
+Content-Type: multipart/mixed;
+ boundary="_004_001a11c3440066ee0b04ef86cea8googlecom_"
+
+--_004_001a11c3440066ee0b04ef86cea8googlecom_
+Content-Type: multipart/alternative;
+ boundary="_002_001a11c3440066ee0b04ef86cea8googlecom_"
+
+--_002_001a11c3440066ee0b04ef86cea8googlecom_
+Content-Type: text/html; charset="utf-8"
+Content-Transfer-Encoding: base64
+
+PGh0bWw+DQo8aGVhZD4NCjxtZXRhIGh0dHAtZXF1aXY9IkNvbnRlbnQtVHlwZSIgY29udGVudD0i
+dGV4dC9odG1sOyBjaGFyc2V0PXV0Zi04Ij4NCjxtZXRhIG5hbWU9IkdlbmVyYXRvciIgY29udGVu
+dD0iTWljcm9zb2Z0IEV4Y2hhbmdlIFNlcnZlciI+DQo8IS0tIGNvbnZlcnRlZCBmcm9tIHJ0ZiAt
+LT4NCjxzdHlsZT48IS0tIC5FbWFpbFF1b3RlIHsgbWFyZ2luLWxlZnQ6IDFwdDsgcGFkZGluZy1s
+ZWZ0OiA0cHQ7IGJvcmRlci1sZWZ0OiAjODAwMDAwIDJweCBzb2xpZDsgfSAtLT48L3N0eWxlPg0K
+PC9oZWFkPg0KPGJvZHk+DQo8Zm9udCBmYWNlPSJUaW1lcyBOZXcgUm9tYW4iIHNpemU9IjMiPjxh
+IG5hbWU9IkJNX0JFR0lOIj48L2E+DQo8dGFibGUgYm9yZGVyPSIxIiB3aWR0aD0iNzM0IiBzdHls
+ZT0iYm9yZGVyOjEgc29saWQ7IGJvcmRlci1jb2xsYXBzZTpjb2xsYXBzZTsgbWFyZ2luLWxlZnQ6
+IDJwdDsgIj4NCjx0cj4NCjx0ZD48Zm9udCBzaXplPSIxIj48YSBocmVmPSJodHRwczovL3d3dy5n
+b29nbGUuY29tL2NhbGVuZGFyL2V2ZW50P2FjdGlvbj1WSUVXJmFtcDtlaWQ9YzNOemNXUXhjRGxs
+Ym1VeU0ySnZNbWsyYjNOeU56ZG5jRzhnWkdwallrQmthbU5pYzI5bWRIZGhjbVV1Ym13JmFtcDt0
+b2s9TWpZamQybHNiR2xoYlhOZlpESXdRR2RzYjJKdmJXRnBiQzVqYjIxak16YzJZVGhrWW1Ga016
+QTJaRFV3TldVMlltWXhOamRqTm1ZMVlUVXhObUpqTWpFNU4yWTMmYW1wO2N0ej1BbWVyaWNhL1Nh
+b19QYXVsbyZhbXA7aGw9ZW5fR0IiPjxmb250IGNvbG9yPSIjMjIwMENDIj48dT5tb3JlDQpkZXRh
+aWxzIMK7PC91PjwvZm9udD48L2E+PGJyPg0KDQo8ZGl2IHN0eWxlPSJtYXJnaW4tYm90dG9tOiAx
+NHB0OyAiPjxmb250IGZhY2U9IkFyaWFsLCBzYW5zLXNlcmlmIiBzaXplPSIyIiBjb2xvcj0iIzIy
+MjIyMiI+PGI+SEVMTE8sPC9iPjwvZm9udD48L2Rpdj4NCjxkaXY+PGZvbnQgc2l6ZT0iMSIgY29s
+b3I9IiMyMjIyMjIiPjxicj4NCg0KSSBBTSBERVNNT05EIFdJTExJQU1TIEFORCBNWSBMSVRUTEUg
+U0lTVEVSIElTIEdMT1JJQSwgT1VSIEZBVEhFUiBPV05TIEEgTElNSVRFRCBPRiBDT0NPQSBBTkQg
+R09MRCBCVVNJTkVTUyBJTiBSRVBVQkxJUVVFIERVIENPTkdPLiBBRlRFUiBISVMgVFJJUCBUTyBD
+T1RFIERJVk9JUkUgVE8gTkVHT1RJQVRFIE9OIENPQ09BIEFORCBHT0xEIEJVU0lORVNTIEhFIFdB
+TlRFRCBUTyBJTlZFU1QgSU4gQUJST0FELiA8L2ZvbnQ+PC9kaXY+DQo8ZGl2IHN0eWxlPSJtYXJn
+aW4tdG9wOiAxNHB0OyBtYXJnaW4tYm90dG9tOiAxNHB0OyAiPjxmb250IHNpemU9IjMiPk9ORSBX
+RUVLIEhFIENBTUUgQkFDSyBGUk9NIEhJUyBUUklQIFRPIEFCSURKQU4gSEUgSEFEIEEgTU9UT1Ig
+QUNDSURFTlQgV0lUSCBPVVIgTU9USEVSIFdISUNIIE9VUiBNT1RIRVIgRElFRCBJTlNUQU5UTFkg
+QlVUIE9VUiBGQVRIRVIgRElFRCBBRlRFUiBGSVZFIERBWVMgSU4gQSBQUklWQVRFIEhPU1BJVEFM
+IElOIE9VUiBDT1VOVFJZLg0KSVQgV0FTIExJS0UgT1VSIEZBVEhFUiBLTkVXIEhFIFdBUyBHT0lO
+RyBUTyBESUUgTUFZIEhJUyBHRU5UTEUgU09VTCBSRVNUIElOIFBSRUZFQ1QgUEVBQ0UuIDwvZm9u
+dD48L2Rpdj4NCjxkaXYgc3R5bGU9Im1hcmdpbi10b3A6IDE0cHQ7IG1hcmdpbi1ib3R0b206IDE0
+cHQ7ICI+PGZvbnQgc2l6ZT0iMyI+SEUgRElTQ0xPU0VEIFRPIE1FIEFTIFRIRSBPTkxZIFNPTiBU
+SEFUIEhFIERFUE9TSVRFRCBUSEUgU1VNIE9GIChVU0QgJCAxMCw1MDAsMDAwKSBJTlRPIEEgQkFO
+SyBJTiBBQklESkFOIFRIQVQgVEhFIE1PTkVZIFdBUyBNRUFOVCBGT1IgSElTIENPQ09BIEFORCBH
+T0xEIEJVU0lORVNTIEhFIFdBTlRFRCBUTyBFU1RBQkxJU0ggSU4NCkFCUk9BRC5XRSBBUkUgU09M
+SUNJVElORyBGT1IgWU9VUiBIRUxQIFRPIFRSQU5TRkVSIFRISVMgTU9ORVkgSU5UTyBZT1VSIEFD
+Q09VTlQgSU4gWU9VUiBDT1VOVFJZIEZPUiBPVVIgSU5WRVNUTUVOVC4gPC9mb250PjwvZGl2Pg0K
+PGRpdiBzdHlsZT0ibWFyZ2luLXRvcDogMTRwdDsgbWFyZ2luLWJvdHRvbTogMTRwdDsgIj48Zm9u
+dCBzaXplPSIzIj5QTEVBU0UgRk9SIFNFQ1VSSVRZIFJFQVNPTlMsSSBBRFZJQ0UgWU9VIFJFUExZ
+IFVTIFRIUk9VR0ggT1VSIFBSSVZBVEUgRU1BSUw6IDxhIGhyZWY9Im1haWx0bzp3aWxsaWFtc2Rl
+c21vbmQxMDdAeWFob28uY29tLnZuIj48Zm9udCBjb2xvcj0iIzAwMDBGRiI+PHU+d2lsbGlhbXNk
+ZXNtb25kMTA3QHlhaG9vLmNvbS52bjwvdT48L2ZvbnQ+PC9hPg0KRk9SIE1PUkUgREVUQUlMUy4g
+PC9mb250PjwvZGl2Pg0KPGRpdiBzdHlsZT0ibWFyZ2luLXRvcDogMTRwdDsgbWFyZ2luLWJvdHRv
+bTogMTRwdDsgIj48Zm9udCBzaXplPSIzIj5SRUdBUkRTLiA8L2ZvbnQ+PC9kaXY+DQo8ZGl2IHN0
+eWxlPSJtYXJnaW4tdG9wOiAxNHB0OyBtYXJnaW4tYm90dG9tOiAxNHB0OyAiPjxmb250IHNpemU9
+IjMiPkRFU01PTkQgL0dMT1JJQSBXSUxMSUFNUy48L2ZvbnQ+PC9kaXY+DQo8ZGl2Pjxmb250IHNp
+emU9IjMiIGNvbG9yPSIjMjIyMjIyIj4mbmJzcDs8L2ZvbnQ+PC9kaXY+DQo8ZGl2Pjxmb250IHNp
+emU9IjMiIGNvbG9yPSIjMjIyMjIyIj4mbmJzcDs8L2ZvbnQ+PC9kaXY+DQo8ZGl2Pjxmb250IHNp
+emU9IjMiIGNvbG9yPSIjMjIyMjIyIj4mbmJzcDs8L2ZvbnQ+PC9kaXY+DQo8ZGl2Pjxmb250IHNp
+emU9IjMiIGNvbG9yPSIjMjIyMjIyIj4mbmJzcDs8L2ZvbnQ+PC9kaXY+DQo8dGFibGUgYm9yZGVy
+PSIxIiB3aWR0aD0iNzM0IiBzdHlsZT0iYm9yZGVyOjEgc29saWQ7IGJvcmRlci1jb2xsYXBzZTpj
+b2xsYXBzZTsgbWFyZ2luLWxlZnQ6IDJwdDsgIj4NCjxjb2wgd2lkdGg9IjM2NSI+DQo8Y29sIHdp
+ZHRoPSIzNjkiPg0KPHRyPg0KPHRkPjxmb250IHNpemU9IjMiPjxpPldoZW48L2k+PC9mb250Pjwv
+dGQ+DQo8dGQ+PGZvbnQgZmFjZT0iQXJpYWwsIHNhbnMtc2VyaWYiIHNpemU9IjEiIGNvbG9yPSIj
+MjIyMjIyIj5UaHUgOSBKYW4gMjAxNCAwODozMCDigJMgMDk6MzAgPGZvbnQgY29sb3I9IiM4ODg4
+ODgiPlNhbyBQYXVsbzwvZm9udD48L2ZvbnQ+PC90ZD4NCjwvdHI+DQo8dHI+DQo8dGQ+PGZvbnQg
+c2l6ZT0iMyI+PGk+Q2FsZW5kYXI8L2k+PC9mb250PjwvdGQ+DQo8dGQ+PGZvbnQgZmFjZT0iQXJp
+YWwsIHNhbnMtc2VyaWYiIHNpemU9IjEiIGNvbG9yPSIjMjIyMjIyIj53aWxsaWFtc19kMjBAZ2xv
+Ym9tYWlsLmNvbTwvZm9udD48L3RkPg0KPC90cj4NCjx0cj4NCjx0ZD48Zm9udCBzaXplPSIzIj48
+aT5XaG88L2k+PC9mb250PjwvdGQ+DQo8dGQ+PGZvbnQgZmFjZT0iQXJpYWwsIHNhbnMtc2VyaWYi
+IHNpemU9IjEiIGNvbG9yPSIjMjIyMjIyIj4oR3Vlc3QgbGlzdCBoYXMgYmVlbiBoaWRkZW4gYXQg
+b3JnYW5pc2VyJ3MgcmVxdWVzdCk8L2ZvbnQ+PC90ZD4NCjwvdHI+DQo8L3RhYmxlPg0KPGRpdiBz
+dHlsZT0ibWFyZ2luLWJvdHRvbTogMTRwdDsgIj48Zm9udCBzaXplPSIxIiBjb2xvcj0iIzg4ODg4
+OCI+R29pbmc/Jm5ic3A7Jm5ic3A7IDxhIGhyZWY9Imh0dHBzOi8vd3d3Lmdvb2dsZS5jb20vY2Fs
+ZW5kYXIvZXZlbnQ/YWN0aW9uPVJFU1BPTkQmYW1wO2VpZD1jM056Y1dReGNEbGxibVV5TTJKdk1t
+azJiM055TnpkbmNHOGdaR3BqWWtCa2FtTmljMjltZEhkaGNtVXVibXcmYW1wO3JzdD0xJmFtcDt0
+b2s9TWpZamQybHNiR2xoYlhOZlpESXdRR2RzYjJKdmJXRnBiQzVqYjIxak16YzJZVGhrWW1Ga016
+QTJaRFV3TldVMlltWXhOamRqTm1ZMVlUVXhObUpqTWpFNU4yWTMmYW1wO2N0ej1BbWVyaWNhL1Nh
+b19QYXVsbyZhbXA7aGw9ZW5fR0IiPjxmb250IGNvbG9yPSIjMjIwMENDIj48dT48Yj5ZZXM8L2I+
+PC91PjwvZm9udD48L2E+PGZvbnQgY29sb3I9IiMyMjIyMjIiPjxiPg0KLSA8L2I+PC9mb250Pjxh
+IGhyZWY9Imh0dHBzOi8vd3d3Lmdvb2dsZS5jb20vY2FsZW5kYXIvZXZlbnQ/YWN0aW9uPVJFU1BP
+TkQmYW1wO2VpZD1jM056Y1dReGNEbGxibVV5TTJKdk1tazJiM055TnpkbmNHOGdaR3BqWWtCa2Ft
+TmljMjltZEhkaGNtVXVibXcmYW1wO3JzdD0zJmFtcDt0b2s9TWpZamQybHNiR2xoYlhOZlpESXdR
+R2RzYjJKdmJXRnBiQzVqYjIxak16YzJZVGhrWW1Ga016QTJaRFV3TldVMlltWXhOamRqTm1ZMVlU
+VXhObUpqTWpFNU4yWTMmYW1wO2N0ej1BbWVyaWNhL1Nhb19QYXVsbyZhbXA7aGw9ZW5fR0IiPjxm
+b250IGNvbG9yPSIjMjIwMENDIj48dT48Yj5NYXliZTwvYj48L3U+PC9mb250PjwvYT48Zm9udCBj
+b2xvcj0iIzIyMjIyMiI+PGI+DQotIDwvYj48L2ZvbnQ+PGEgaHJlZj0iaHR0cHM6Ly93d3cuZ29v
+Z2xlLmNvbS9jYWxlbmRhci9ldmVudD9hY3Rpb249UkVTUE9ORCZhbXA7ZWlkPWMzTnpjV1F4Y0Rs
+bGJtVXlNMkp2TW1rMmIzTnlOemRuY0c4Z1pHcGpZa0JrYW1OaWMyOW1kSGRoY21VdWJtdyZhbXA7
+cnN0PTImYW1wO3Rvaz1NallqZDJsc2JHbGhiWE5mWkRJd1FHZHNiMkp2YldGcGJDNWpiMjFqTXpj
+MllUaGtZbUZrTXpBMlpEVXdOV1UyWW1ZeE5qZGpObVkxWVRVeE5tSmpNakU1TjJZMyZhbXA7Y3R6
+PUFtZXJpY2EvU2FvX1BhdWxvJmFtcDtobD1lbl9HQiI+PGZvbnQgY29sb3I9IiMyMjAwQ0MiPjx1
+PjxiPk5vPC9iPjwvdT48L2ZvbnQ+PC9hPjxmb250IGNvbG9yPSIjMjIyMjIyIj4mbmJzcDsmbmJz
+cDsmbmJzcDsNCjwvZm9udD48YSBocmVmPSJodHRwczovL3d3dy5nb29nbGUuY29tL2NhbGVuZGFy
+L2V2ZW50P2FjdGlvbj1WSUVXJmFtcDtlaWQ9YzNOemNXUXhjRGxsYm1VeU0ySnZNbWsyYjNOeU56
+ZG5jRzhnWkdwallrQmthbU5pYzI5bWRIZGhjbVV1Ym13JmFtcDt0b2s9TWpZamQybHNiR2xoYlhO
+ZlpESXdRR2RzYjJKdmJXRnBiQzVqYjIxak16YzJZVGhrWW1Ga016QTJaRFV3TldVMlltWXhOamRq
+Tm1ZMVlUVXhObUpqTWpFNU4yWTMmYW1wO2N0ej1BbWVyaWNhL1Nhb19QYXVsbyZhbXA7aGw9ZW5f
+R0IiPjxmb250IGNvbG9yPSIjMjIwMENDIj48dT5tb3JlDQpvcHRpb25zIMK7PC91PjwvZm9udD48
+L2E+PC9mb250PjwvZGl2Pg0KPC9mb250PjwvdGQ+DQo8L3RyPg0KPHRyPg0KPHRkIHN0eWxlPSJi
+YWNrZ3JvdW5kLWNvbG9yOiAjRjZGNkY2OyAiPjxmb250IHNpemU9IjMiPkludml0YXRpb24gZnJv
+bSA8YSBocmVmPSJodHRwczovL3d3dy5nb29nbGUuY29tL2NhbGVuZGFyLyI+PGZvbnQgY29sb3I9
+IiMwMDAwRkYiPjx1Pkdvb2dsZSBDYWxlbmRhcjwvdT48L2ZvbnQ+PC9hPg0KPGRpdiBzdHlsZT0i
+bWFyZ2luLXRvcDogMTRwdDsgbWFyZ2luLWJvdHRvbTogMTRwdDsgIj48Zm9udCBzaXplPSIzIj5Z
+b3UgYXJlIHJlY2VpdmluZyB0aGlzIGNvdXJ0ZXN5IGVtYWlsIGF0IHRoZSBhY2NvdW50IGRqY2JA
+ZGpjYnNvZnR3YXJlLm5sIGJlY2F1c2UgeW91IGFyZSBhbiBhdHRlbmRlZSBvZiB0aGlzIGV2ZW50
+LjwvZm9udD48L2Rpdj4NCjxkaXYgc3R5bGU9Im1hcmdpbi10b3A6IDE0cHQ7IG1hcmdpbi1ib3R0
+b206IDE0cHQ7ICI+PGZvbnQgc2l6ZT0iMyI+VG8gc3RvcCByZWNlaXZpbmcgZnV0dXJlIG5vdGlm
+aWNhdGlvbnMgZm9yIHRoaXMgZXZlbnQsIGRlY2xpbmUgdGhpcyBldmVudC4gQWx0ZXJuYXRpdmVs
+eSwgeW91IGNhbiBzaWduIHVwIGZvciBhIEdvb2dsZSBhY2NvdW50IGF0DQo8YSBocmVmPSJodHRw
+czovL3d3dy5nb29nbGUuY29tL2NhbGVuZGFyLyI+aHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS9jYWxl
+bmRhci88L2E+IGFuZCBjb250cm9sIHlvdXIgbm90aWZpY2F0aW9uIHNldHRpbmdzIGZvciB5b3Vy
+IGVudGlyZSBjYWxlbmRhci48L2ZvbnQ+PC9kaXY+DQo8L2ZvbnQ+PC90ZD4NCjwvdHI+DQo8L3Rh
+YmxlPg0KPC9mb250Pg0KPC9ib2R5Pg0KPC9odG1sPg0K
+
+--_002_001a11c3440066ee0b04ef86cea8googlecom_
+Content-Type: text/calendar; charset="UTF-8"; method=REQUEST
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+PRODID:-//Google Inc//Google Calendar 70.9054//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTART:20140109T103000Z
+DTEND:20140109T113000Z
+DTSTAMP:20140109T100934Z
+ORGANIZER;CN=William:mailto:william@example.com
+UID:sssqd1p9ene23bo2i6osr77gpo@google.com
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=
+ TRUE;CN=billy@example.com;X-NUM-GUESTS=0:mailto:billy@example.com
+CREATED:20140109T100932Z
+DESCRIPTION:\nI AM DESMOND WILLIAMS AND MY LITTLE SISTER IS GLORIA\, OUR FA
+ THER OWNS A LIMITED OF COCOA AND GOLD BUSINESS IN REPUBLIQUE DU CONGO. AFTE
+ R HIS TRIP TO COTE DIVOIRE TO NEGOTIATE ON COCOA AND GOLD BUSINESS HE WANTE
+ D TO INVEST IN ABROAD. \n\nONE WEEK HE CAME BACK FROM HIS TRIP TO ABIDJAN H
+ E HAD A MOTOR ACCIDENT WITH OUR MOTHER WHICH OUR MOTHER DIED INSTANTLY BUT
+ OUR FATHER DIED AFTER FIVE DAYS IN A PRIVATE HOSPITAL IN OUR COUNTRY. IT WA
+ S LIKE OUR FATHER KNEW HE WAS GOING TO DIE MAY HIS GENTLE SOUL REST IN PREF
+ ECT PEACE. \n\nHE DISCLOSED TO ME AS THE ONLY SON THAT HE DEPOSITED THE SUM
+ OF (USD $ 10\,500\,000) INTO A BANK IN ABIDJAN THAT THE MONEY WAS MEANT FO
+ R HIS COCOA AND GOLD BUSINESS HE WANTED TO ESTABLISH IN ABROAD.WE ARE SOLIC
+ ITING FOR YOUR HELP TO TRANSFER THIS MONEY INTO YOUR ACCOUNT IN YOUR COUNTR
+ Y FOR OUR INVESTMENT. \n\nPLEASE FOR SECURITY REASONS\,I ADVICE YOU REPLY U
+ S THROUGH OUR PRIVATE EMAIL FOR MORE DETAI
+ LS. \n\nREGARDS. \n\nDESMOND /GLORIA WILLIAMS.\nView your event at http://w
+ ww.google.com/calendar/event?action=VIEW&eid=c3NzcWQxcDllbmUyM2JvMmk2b3NyNz
+ dncG8gZGpjYkBkamNic29mdHdhcmUubmw&tok=MjYjd2lsbGlhbXNfZDIwQGdsb2JvbWFpbC5jb
+ 21jMzc2YThkYmFkMzA2ZDUwNWU2YmYxNjdjNmY1YTUxNmJjMjE5N2Y3&ctz=America/Sao_Pau
+ lo&hl=en_GB.
+LAST-MODIFIED:20140109T100932Z
+LOCATION:
+SEQUENCE:0
+STATUS:CONFIRMED
+SUMMARY:HELLO\,
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--_002_001a11c3440066ee0b04ef86cea8googlecom_--
+
+--_004_001a11c3440066ee0b04ef86cea8googlecom_
+Content-Type: application/ics; name="invite.ics"
+Content-Description: invite.ics
+Content-Disposition: attachment; filename="invite.ics"; size=2029;
+ creation-date="Thu, 09 Jan 2014 10:09:44 GMT";
+ modification-date="Thu, 09 Jan 2014 10:09:44 GMT"
+Content-Transfer-Encoding: base64
+
+QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vR29vZ2xlIEluYy8vR29vZ2xlIENhbGVuZGFyIDcw
+LjkwNTQvL0VODQpWRVJTSU9OOjIuMA0KQ0FMU0NBTEU6R1JFR09SSUFODQpNRVRIT0Q6UkVRVUVT
+VA0KQkVHSU46VkVWRU5UDQpEVFNUQVJUOjIwMTQwMTA5VDEwMzAwMFoNCkRURU5EOjIwMTQwMTA5
+VDExMzAwMFoNCkRUU1RBTVA6MjAxNDAxMDlUMTAwOTM0Wg0KT1JHQU5JWkVSO0NOPVdpbGxpYW1z
+IFdpbGxpYW1zOm1haWx0bzp3aWxsaWFtc19kMjBAZ2xvYm9tYWlsLmNvbQ0KVUlEOnNzc3FkMXA5
+ZW5lMjNibzJpNm9zcjc3Z3BvQGdvb2dsZS5jb20NCkFUVEVOREVFO0NVVFlQRT1JTkRJVklEVUFM
+O1JPTEU9UkVRLVBBUlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPQ0KIFRSVUU7
+Q049ZGpjYkBkamNic29mdHdhcmUubmw7WC1OVU0tR1VFU1RTPTA6bWFpbHRvOmRqY2JAZGpjYnNv
+ZnR3YXJlLm5sDQpDUkVBVEVEOjIwMTQwMTA5VDEwMDkzMloNCkRFU0NSSVBUSU9OOlxuSSBBTSBE
+RVNNT05EIFdJTExJQU1TIEFORCBNWSBMSVRUTEUgU0lTVEVSIElTIEdMT1JJQVwsIE9VUiBGQQ0K
+IFRIRVIgT1dOUyBBIExJTUlURUQgT0YgQ09DT0EgQU5EIEdPTEQgQlVTSU5FU1MgSU4gUkVQVUJM
+SVFVRSBEVSBDT05HTy4gQUZURQ0KIFIgSElTIFRSSVAgVE8gQ09URSBESVZPSVJFIFRPIE5FR09U
+SUFURSBPTiBDT0NPQSBBTkQgR09MRCBCVVNJTkVTUyBIRSBXQU5URQ0KIEQgVE8gSU5WRVNUIElO
+IEFCUk9BRC4gXG5cbk9ORSBXRUVLIEhFIENBTUUgQkFDSyBGUk9NIEhJUyBUUklQIFRPIEFCSURK
+QU4gSA0KIEUgSEFEIEEgTU9UT1IgQUNDSURFTlQgV0lUSCBPVVIgTU9USEVSIFdISUNIIE9VUiBN
+T1RIRVIgRElFRCBJTlNUQU5UTFkgQlVUIA0KIE9VUiBGQVRIRVIgRElFRCBBRlRFUiBGSVZFIERB
+WVMgSU4gQSBQUklWQVRFIEhPU1BJVEFMIElOIE9VUiBDT1VOVFJZLiBJVCBXQQ0KIFMgTElLRSBP
+VVIgRkFUSEVSIEtORVcgSEUgV0FTIEdPSU5HIFRPIERJRSBNQVkgSElTIEdFTlRMRSBTT1VMIFJF
+U1QgSU4gUFJFRg0KIEVDVCBQRUFDRS4gXG5cbkhFIERJU0NMT1NFRCBUTyBNRSBBUyBUSEUgT05M
+WSBTT04gVEhBVCBIRSBERVBPU0lURUQgVEhFIFNVTQ0KICBPRiAoVVNEICQgMTBcLDUwMFwsMDAw
+KSBJTlRPIEEgQkFOSyBJTiBBQklESkFOIFRIQVQgVEhFIE1PTkVZIFdBUyBNRUFOVCBGTw0KIFIg
+SElTIENPQ09BIEFORCBHT0xEIEJVU0lORVNTIEhFIFdBTlRFRCBUTyBFU1RBQkxJU0ggSU4gQUJS
+T0FELldFIEFSRSBTT0xJQw0KIElUSU5HIEZPUiBZT1VSIEhFTFAgVE8gVFJBTlNGRVIgVEhJUyBN
+T05FWSBJTlRPIFlPVVIgQUNDT1VOVCBJTiBZT1VSIENPVU5UUg0KIFkgRk9SIE9VUiBJTlZFU1RN
+RU5ULiBcblxuUExFQVNFIEZPUiBTRUNVUklUWSBSRUFTT05TXCxJIEFEVklDRSBZT1UgUkVQTFkg
+VQ0KIFMgVEhST1VHSCBPVVIgUFJJVkFURSBFTUFJTDogd2lsbGlhbXNkZXNtb25kMTA3QHlhaG9v
+LmNvbS52biBGT1IgTU9SRSBERVRBSQ0KIExTLiBcblxuUkVHQVJEUy4gXG5cbkRFU01PTkQgL0dM
+T1JJQSBXSUxMSUFNUy5cblZpZXcgeW91ciBldmVudCBhdCBodHRwOi8vdw0KIHd3Lmdvb2dsZS5j
+b20vY2FsZW5kYXIvZXZlbnQ/YWN0aW9uPVZJRVcmZWlkPWMzTnpjV1F4Y0RsbGJtVXlNMkp2TW1r
+MmIzTnlOeg0KIGRuY0c4Z1pHcGpZa0JrYW1OaWMyOW1kSGRoY21VdWJtdyZ0b2s9TWpZamQybHNi
+R2xoYlhOZlpESXdRR2RzYjJKdmJXRnBiQzVqYg0KIDIxak16YzJZVGhrWW1Ga016QTJaRFV3TldV
+MlltWXhOamRqTm1ZMVlUVXhObUpqTWpFNU4yWTMmY3R6PUFtZXJpY2EvU2FvX1BhdQ0KIGxvJmhs
+PWVuX0dCLg0KTEFTVC1NT0RJRklFRDoyMDE0MDEwOVQxMDA5MzJaDQpMT0NBVElPTjoNClNFUVVF
+TkNFOjANClNUQVRVUzpDT05GSVJNRUQNClNVTU1BUlk6SEVMTE9cLA0KVFJBTlNQOk9QQVFVRQ0K
+RU5EOlZFVkVOVA0KRU5EOlZDQUxFTkRBUg0K
+
+--_004_001a11c3440066ee0b04ef86cea8googlecom_--
+
+)";
+ auto message{Message::make_from_text(
+ msgtext,
+ "/home/test/Maildir/inbox/cur/162342449279256.107710_1.evergrey:2,PSp")};
+ g_assert_true(!!message);
+ assert_equal(message->subject(),
+ "Invitation: HELLO, @ Thu 9 Jan 2014 08:30 - 09:30 (william@example.com)");
+ g_assert_true(message->flags() == (Flags::Passed|Flags::Seen|
+ Flags::HasAttachment|Flags::Calendar));
+ g_assert_cmpuint(message->body_html().value_or("").find("DETAILS"), ==, 2271);
+}
+
+
+static void
+test_message_references()
+{
+ constexpr auto msgtext =
+R"(Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset=utf-8
+References: <YuvYh1JbE3v+abd5@kili>
+ <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com>
+ <T4CDWjUrgtI5n4mh1JEdW6RLYzqbPE9-yDrhEVwDM22WX-198fBwcnLd-4_xR1gvsVSHQps9fp_pZevTF0ZmaA==@protonmail.internalid>
+To: "Robin Murphy" <robin.murphy@arm.com>
+Reply-To: "Dan Carpenter" <dan.carpenter@oracle.com>
+From: "Dan Carpenter" <dan.carpenter@oracle.com>
+Subject: Re: [PATCH] iommu/omap: fix buffer overflow in debugfs
+List-Id: <kernel-janitors.vger.kernel.org>
+Date: Fri, 5 Aug 2022 09:37:02 +0300
+In-Reply-To: <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com>
+Precedence: bulk
+Message-Id: <20220805063702.GH3438@kadam>
+
+On Thu, Aug 04, 2022 at 05:31:39PM +0100, Robin Murphy wrote:
+> On 04/08/2022 3:32 pm, Dan Carpenter wrote:
+> > There are two issues here:
+)";
+ auto message{Message::make_from_text(
+ msgtext,
+ "/home/test/Maildir/inbox/cur/162342449279256.88888_1.evergrey:2,S")};
+ g_assert_true(!!message);
+ assert_equal(message->subject(),
+ "Re: [PATCH] iommu/omap: fix buffer overflow in debugfs");
+ g_assert_true(message->priority() == Priority::Low);
+
+ /*
+ * "90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com" is seen both in
+ * references and in-reply-to; in the de-duplication, the first one wins.
+ */
+ std::vector<std::string> expected_refs = {
+ "YuvYh1JbE3v+abd5@kili",
+ "90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com",
+ /* protonmail.internalid is fake and removed */
+ // "T4CDWjUrgtI5n4mh1JEdW6RLYzqbPE9-yDrhEVwDM22WX-198fBwcnLd-4_"
+ // "xR1gvsVSHQps9fp_pZevTF0ZmaA==@protonmail.internalid"
+ };
+
+ assert_equal_seq_str(expected_refs, message->references());
+}
+
+
+static void
+test_message_outlook_body()
+{
+ constexpr auto msgtext =
+R"x(Received: from vu-ex2.activedir.vu.lt (172.16.159.219) by
+ vu-ex1.activedir.vu.lt (172.16.159.218) with Microsoft SMTP Server
+ (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.2.1118.9
+ via Mailbox Transport; Fri, 27 May 2022 11:40:05 +0300
+Received: from vu-ex2.activedir.vu.lt (172.16.159.219) by
+ vu-ex2.activedir.vu.lt (172.16.159.219) with Microsoft SMTP Server
+ (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id
+ 15.2.1118.9; Fri, 27 May 2022 11:40:05 +0300
+Received: from vu-ex2.activedir.vu.lt ([172.16.159.219]) by
+ vu-ex2.activedir.vu.lt ([172.16.159.219]) with mapi id 15.02.1118.009; Fri,
+ 27 May 2022 11:40:05 +0300
+From: =?windows-1257?Q?XXXXXXXXXX= <XXXXXXXXXX>
+To: <XXXXXXXXXX@XXXXXXXXXX.com>
+Subject: =?windows-1257?Q?Pra=F0ymas?=
+Thread-Topic: =?windows-1257?Q?Pra=F0ymas?=
+Thread-Index: AQHYcaRi3ejPSLxkl0uTFDto7z2OcA==
+Date: Fri, 27 May 2022 11:40:05 +0300
+Message-ID: <5c2cd378af634e929a6cc69da1e66b9d@XX.vu.lt>
+Accept-Language: en-US, lt-LT
+Content-Language: en-US
+X-MS-Has-Attach:
+Content-Type: text/html; charset="windows-1257"
+Content-Transfer-Encoding: quoted-printable
+MIME-Version: 1.0
+X-TUID: 1vFQ9RPwwg/u
+
+<html>
+<head>
+<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dwindows-1=
+257">
+<style type=3D"text/css" style=3D"display:none;"><!-- P {margin-top:0;margi=
+n-bottom:0;} --></style>
+</head>
+<body dir=3D"ltr">
+<div id=3D"divtagdefaultwrapper" style=3D"font-size:12pt;color:#000000;font=
+-family:Calibri,Helvetica,sans-serif;" dir=3D"ltr">
+<p>Laba diena visiems,</p>
+<p>Trumpai.</p>
+<p>D=EBl leidimo ar neleidimo ginti darb=E0: ed=EBstytojo paskyroje spaud=
+=FEiate ikon=E0 "ra=F0to darbai", atidar=E6 susiraskite =E1ra=F0=
+=E0 "tvirtinti / netvirtinti", pa=FEym=EBkite vien=E0 i=F0 j=F8.&=
+nbsp;</p>
+<p><br>
+</p>
+<p>=D0=E1 darb=E0 privalu atlikti, kad paskui nekilt=F8 problem=F8 studentu=
+i =E1vedant =E1vertinim=E0.</p>
+<p><br>
+</p>
+<p>Jei neleid=FEiate ginti darbo, pra=F0au informuoti mane ir komisijos sek=
+retori=F8. </p>
+<p><br>
+</p>
+<p>Vis=E0 tolesn=E6 informacij=E0 atsi=F8siu artimiausiu metu (stengsiuosi =
+=F0iandien vakare).</p>
+<p><br>
+</p>
+<p>Pagarbiai.</p>
+<p><br>
+</p>
+<p><br>
+</p>
+<div id=3D"Signature">
+<div id=3D"divtagdefaultwrapper" dir=3D"ltr" style=3D"font-family: Calibri,=
+ Helvetica, sans-serif, EmojiFont, "Apple Color Emoji", "Seg=
+oe UI Emoji", NotoColorEmoji, "Segoe UI Symbol", "Andro=
+id Emoji", EmojiSymbols;">
+<p style=3D"color:rgb(0,0,0); font-size:12pt"><br>
+</p>
+<p style=3D"color:rgb(0,0,0); font-size:12pt"><br>
+</p>
+<p style=3D"color:rgb(0,0,0); font-size:12pt"><br>
+</p>
+<p style=3D"color:rgb(0,0,0); font-size:12pt"><span style=3D"font-size:10pt=
+; background-color:rgb(255,255,255); color:rgb(0,111,201)"><br>
+</span></p>
+<p style=3D"color:rgb(0,0,0); font-size:12pt"><span style=3D"font-size:10pt=
+; background-color:rgb(255,255,255); color:rgb(0,111,201)">XXXXXXXXXX</span></p>
+<p style=3D""><font color=3D"#006fc9"><span style=3D"font-size:13.3333px"><=
+/span></font></p>
+<span style=3D"font-size:10pt; background-color:rgb(255,255,255); color:rgb=
+(0,111,201); font-size:10pt"></span>
+<p style=3D""><font color=3D"#006fc9"><span style=3D"font-size:13.3333px">XXXXXXXXXX</span></font></p>
+<p style=3D""><font color=3D"#006fc9"><span style=3D"font-size:13.3333px">XXXXXXXXXX</span></font></p>
+<p style=3D""><font color=3D"#006fc9"><span style=3D"font-size:13.3333px">XXXXXXXXXX</span></font></p>
+<p style=3D""><br>
+</p>
+<p style=3D""><br>
+</p>
+</div>
+</div>
+</div>
+</body>
+</html>
+)x";
+ g_test_bug("2349");
+
+ auto message{Message::make_from_text(
+ msgtext,
+ "/home/test/Maildir/inbox/cur/162342449279256.77777_1.evergrey:2,S")};
+ g_assert_true(!!message);
+
+ assert_equal(message->subject(), "Prašymas");
+ g_assert_true(message->priority() == Priority::Normal);
+
+ g_assert_false(!!message->body_text());
+ g_assert_true(!!message->body_html());
+ g_assert_cmpuint(message->body_html()->find("<p>Pagarbiai.</p>"), ==, 935);
+}
+
+
+static void
+test_message_message_id()
+{
+ constexpr const auto msg1 =
+R"(From: "Mu Test" <mu@djcbsoftware.nl>
+To: mu@djcbsoftware.nl
+Message-ID: <87lew9xddt.fsf@djcbsoftware.nl>
+
+abc
+)";
+
+ constexpr const auto msg2 =
+R"(From: "Mu Test" <mu@djcbsoftware.nl>
+To: mu@djcbsoftware.nl
+
+abc
+)";
+
+ constexpr const auto msg3 =
+R"(From: "Mu Test" <mu@djcbsoftware.nl>
+To: mu@djcbsoftware.nl
+Message-ID:
+
+abc
+)";
+ const auto m1{Message::make_from_text(msg1, "/foo/cur/m123:2,S")};
+ assert_valid_result(m1);
+
+ const auto m2{Message::make_from_text(msg2, "/foo/cur/m456:2,S")};
+ assert_valid_result(m2);
+ const auto m3{Message::make_from_text(msg3, "/foo/cur/m789:2,S")};
+ assert_valid_result(m3);
+
+ assert_equal(m1->message_id(), "87lew9xddt.fsf@djcbsoftware.nl");
+
+ /* both with absent and empty message-id, generate "random" fake one,
+ * which must end in @mu.id */
+ const auto id2{m2->message_id()};
+ const auto id3{m3->message_id()};
+
+ g_assert_true(g_str_has_suffix(id2.c_str(), "@mu.id"));
+ g_assert_true(g_str_has_suffix(id3.c_str(), "@mu.id"));
+}
+
+
+static void
+test_message_fail ()
+{
+ {
+ const auto msg = Message::make_from_path("/root/non-existent-path-12345");
+ g_assert_false(!!msg);
+ }
+
+ {
+ const auto msg = Message::make_from_text("", "");
+ g_assert_false(!!msg);
+ }
+}
+
+static void
+test_message_sanitize_maildir()
+{
+ assert_equal(Message::sanitize_maildir("/"), "/");
+ assert_equal(Message::sanitize_maildir("/foo/bar"), "/foo/bar");
+ assert_equal(Message::sanitize_maildir("/foo/bar/cuux/"), "/foo/bar/cuux");
+}
+
+static void
+test_message_subject_with_newline()
+{
+constexpr const auto txt =
+R"(To: foo@example.com
+Subject: =?utf-8?q?Le_poids_=C3=A9conomique_de_la_chasse_:_=0A=0Ala_dette_cach?= =?utf-8?q?=C3=A9e_de_la_chasse_!?=
+Date: Mon, 24 Apr 2023 07:32:43 +0000
+
+Hello!
+)";
+ g_test_bug("2477");
+
+ const auto msg{Message::make_from_text(txt, "/foo/cur/m123:2,S")};
+ assert_valid_result(msg);
+
+ assert_equal(msg->subject(), // newlines are filtered-out
+ "Le poids économique de la chasse : la dette cachée de la chasse !");
+ assert_equal(msg->header("Subject").value_or(""),
+ "Le poids économique de la chasse : \n\nla dette cachée de la chasse !");
+ g_assert_true(none_of(msg->flags() & Flags::MailingList));
+}
+
+static void
+test_message_list_unsubscribe()
+{
+ constexpr const auto txt =
+R"(From: "Mu Test" <mu@djcbsoftware.nl>
+To: mu@djcbsoftware.nl
+Subject: Test
+Message-ID: <87lew9xddt.fsf@djcbsoftware.nl>
+List-Unsubscribe: <mailto:unsubscribe-T7BC8RRQMK-booking-email-9@booking.com>
+
+abcdef
+)";
+ const auto msg{Message::make_from_text(txt, "/xxx/m123:2,S")};
+ assert_valid_result(msg);
+
+ assert_equal(msg->mailing_list(), "");
+ g_assert_true(any_of(msg->flags() & Flags::MailingList));
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/message/message/mailing-list",
+ test_message_mailing_list);
+ g_test_add_func("/message/message/attachments",
+ test_message_attachments);
+ g_test_add_func("/message/message/signed",
+ test_message_signed);
+ g_test_add_func("/message/message/signed-encrypted",
+ test_message_signed_encrypted);
+ g_test_add_func("/message/message/multipart-mixed-rfc822",
+ test_message_multipart_mixed_rfc822);
+ g_test_add_func("/message/message/detect-attachment",
+ test_message_detect_attachment);
+ g_test_add_func("/message/message/calendar",
+ test_message_calendar);
+ g_test_add_func("/message/message/references",
+ test_message_references);
+ g_test_add_func("/message/message/outlook-body",
+ test_message_outlook_body);
+ g_test_add_func("/message/message/message-id",
+ test_message_message_id);
+ g_test_add_func("/message/message/subject-with-newline",
+ test_message_subject_with_newline);
+ g_test_add_func("/message/message/fail",
+ test_message_fail);
+ g_test_add_func("/message/message/sanitize-maildir",
+ test_message_sanitize_maildir);
+ g_test_add_func("/message/message/message-list-unsubscribe",
+ test_message_list_unsubscribe);
+
+ return g_test_run();
+}
--- /dev/null
+## Copyright (C) 2022-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## 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, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+#
+# tests
+#
+
+test('test-contact',
+ executable('test-contact',
+ '../mu-contact.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, gmime_dep, lib_mu_message_dep]))
+
+test('test-document',
+ executable('test-document',
+ '../mu-document.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, gmime_dep, lib_mu_message_dep]))
+
+test('test-fields',
+ executable('test-fields',
+ '../mu-fields.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, gmime_dep, lib_mu_message_dep]))
+
+test('test-flags',
+ executable('test-flags',
+ '../mu-flags.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, gmime_dep, lib_mu_message_dep]))
+
+test('test-message',
+ executable('test-message',
+ '../test-mu-message.cc',
+ install: false,
+ dependencies: [glib_dep, gmime_dep, lib_mu_message_dep]))
+
+test('test-priority',
+ executable('test-priority',
+ '../mu-priority.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, gmime_dep, lib_mu_message_dep]))
+
+test('test-message-file',
+ executable('test-message-file',
+ '../mu-message-file.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_message_dep]))
+
+test('test-message-part',
+ executable('test-message-part',
+ '../mu-message-part.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_message_dep]))
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-config.hh"
+
+using namespace Mu;
+
+constexpr /*static*/ bool
+validate_props()
+{
+ size_t id{0};
+ for (auto&& prop: Config::properties) {
+
+ // ids must match
+ if (static_cast<size_t>(prop.id) != id)
+ return false;
+ ++id;
+ }
+
+ return true;
+}
+
+#ifdef BUILD_TESTS
+#define static_assert g_assert_true
+#endif /*BUILD_TESTS*/
+
+[[maybe_unused]]
+static void
+test_props()
+{
+ static_assert(validate_props());
+}
+
+#ifdef BUILD_TESTS
+/*
+ * Tests.
+ *
+ */
+
+#include "utils/mu-test-utils.hh"
+
+static void
+test_basic()
+{
+ MemDb db;
+ Config conf_db{db};
+
+ g_assert_false(conf_db.read_only());
+
+ using Id = Config::Id;
+
+ {
+ const auto rmd = conf_db.get<Id::RootMaildir>();
+ g_assert_true(rmd.empty());
+ }
+
+ {
+ auto res = conf_db.set<Id::RootMaildir>("/home/djcb/Maildir");
+ assert_valid_result(res);
+
+ const auto rmd = conf_db.get<Id::RootMaildir>();
+ assert_equal(rmd, "/home/djcb/Maildir");
+ }
+
+ {
+ g_assert_true(Config::property<Id::BatchSize>().default_val == "50000");
+ g_assert_cmpuint(conf_db.get<Id::BatchSize>(),==,50000);
+
+ assert_valid_result(conf_db.set<Id::BatchSize>(123456));
+ g_assert_cmpuint(conf_db.get<Id::BatchSize>(),==,123456);
+ }
+
+
+ {
+ MemDb db2;
+ Config conf_db2{db2};
+
+ g_assert_cmpuint(conf_db2.get<Id::BatchSize>(),==,50000);
+ g_assert_true(conf_db2.get<Id::RootMaildir>().empty());
+
+ // BatchSize is configurable; RootMaildir is not.
+ conf_db2.import_configurable(conf_db);
+
+ g_assert_cmpuint(conf_db2.get<Id::BatchSize>(),==,123456);
+ g_assert_true(conf_db2.get<Id::RootMaildir>().empty());
+ }
+}
+
+static void
+test_read_only()
+{
+ MemDb db{true/*read-only*/};
+ Config conf_db{db};
+
+ auto res = conf_db.set<Config::Id::MaxMessageSize>(12345);
+ g_assert_false(!!res);
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/config-db/props", test_props);
+ g_test_add_func("/config-db/basic", test_basic);
+ g_test_add_func("/config-db/read-only", test_read_only);
+
+ return g_test_run();
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_CONFIG_HH__
+#define MU_CONFIG_HH__
+
+#include <cstdint>
+#include <cinttypes>
+#include <string_view>
+#include <string>
+#include <array>
+#include <vector>
+#include <variant>
+#include <unordered_map>
+
+#include "mu-xapian-db.hh"
+
+#include <utils/mu-utils.hh>
+#include <utils/mu-result.hh>
+#include <utils/mu-option.hh>
+
+namespace Mu {
+
+struct Property {
+ enum struct Id {
+ BatchSize, /**< Xapian batch-size */
+ Contacts, /**< Cache of contact information */
+ Created, /**< Time of creation */
+ IgnoredAddresses,/**< Email addresses ignored for the contacts-cache */
+ LastChange, /**< Time of last change */
+ LastIndex, /**< Time of last index */
+ MaxMessageSize, /**< Maximum message size (in bytes) */
+ PersonalAddresses, /**< List of personal e-mail addresses */
+ RootMaildir, /**< Root maildir path */
+ SchemaVersion, /**< Xapian DB schema version */
+ SupportNgrams, /**< Support ngrams for indexing & querying
+ * for e.g. CJK languages */
+ /* <private> */
+ _count_ /* Number of Ids */
+ };
+
+ static constexpr size_t id_size = static_cast<size_t>(Id::_count_);
+ /**< Number of Property::Ids */
+
+ enum struct Flags {
+ None = 0, /**< Nothing in particular */
+ ReadOnly = 1 << 0, /**< Property is read-only for external use
+ * (but can change from within the store) */
+ Configurable = 1 << 1, /**< A user-configurable parameter; name
+ * starts with 'conf-' */
+ Internal = 1 << 2, /**< Mu-internal field */
+ };
+ enum struct Type {
+ Boolean, /**< Some boolean value */
+ Number, /**< Some number */
+ Timestamp, /**< Timestamp number */
+ Path, /**< Path string */
+ String, /**< A string */
+ StringList, /**< A list of strings */
+ };
+
+ using Value = std::variant<int64_t, std::string, std::vector<std::string> >;
+
+ Id id;
+ Type type;
+ Flags flags;
+ std::string_view name;
+ std::string_view default_val;
+ std::string_view description;
+};
+
+MU_ENABLE_BITOPS(Property::Flags);
+
+class Config {
+public:
+ using Id = Property::Id;
+ using Type = Property::Type;
+ using Flags = Property::Flags;
+ using Value = Property::Value;
+
+ static constexpr std::array<Property, Property::id_size>
+ properties = {{
+ {
+ Id::BatchSize,
+ Type::Number,
+ Flags::Configurable,
+ "batch-size",
+ "50000",
+ "Number of changes in a database transaction"
+ },
+ {
+ Id::Contacts,
+ Type::String,
+ Flags::Internal,
+ "contacts",
+ {},
+ "Serialized contact information"
+ },
+ {
+ Id::Created,
+ Type::Timestamp,
+ Flags::ReadOnly,
+ MetadataIface::created_key,
+ {},
+ "Database creation time"
+ },
+ {
+ Id::IgnoredAddresses,
+ Type::StringList,
+ Flags::Configurable,
+ "ignored-addresses",
+ {},
+ "E-mail addresses ignored for the contacts-cache, "
+ "literal or /regexp/"
+ },
+ {
+ Id::LastChange,
+ Type::Timestamp,
+ Flags::ReadOnly,
+ MetadataIface::last_change_key,
+ {},
+ "Time when last change occurred"
+ },
+ {
+ Id::LastIndex,
+ Type::Timestamp,
+ Flags::ReadOnly,
+ "last-index",
+ {},
+ "Time when last indexing operation was completed"
+ },
+ {
+ Id::MaxMessageSize,
+ Type::Number,
+ Flags::Configurable,
+ "max-message-size",
+ "100000000", // default max: 100M bytes
+ "Maximum message size (in bytes); bigger messages are skipped"
+ },
+ {
+ Id::PersonalAddresses,
+ Type::StringList,
+ Flags::Configurable,
+ "personal-addresses",
+ {},
+ "Personal e-mail addresses, literal or /regexp/"
+ },
+ {
+ Id::RootMaildir,
+ Type::Path,
+ Flags::ReadOnly,
+ "root-maildir",
+ {},
+ "Absolute path of the top of the Maildir tree"
+ },
+ {
+ Id::SchemaVersion,
+ Type::Number,
+ Flags::ReadOnly,
+ "schema-version",
+ {},
+ "Version of the Xapian database schema"
+ },
+ {
+ Id::SupportNgrams,
+ Type::Boolean,
+ Flags::Configurable,
+ "support-ngrams",
+ {},
+ "Support n-grams for working with CJK and other languages"
+ },
+ }};
+
+ /**
+ * Construct a new Config object.
+ *
+ * @param db The config-store (database); must stay valid for the
+ * lifetime of this config.
+ */
+ Config(MetadataIface& cstore): cstore_{cstore}{}
+
+ /**
+ * Get the property by its id
+ *
+ * @param id a property id (!= Id::_count_)
+ *
+ * @return the property
+ */
+ template <Id ID>
+ constexpr static const Property& property() {
+ return properties[static_cast<size_t>(ID)];
+ }
+
+ /**
+ * Get a Property by its name.
+ *
+ * @param name The name
+ *
+ * @return the property or Nothing if not found
+ */
+ static Option<const Property&> property(const std::string& name) {
+ const auto pname{std::string_view(name.data(), name.size())};
+ for(auto&& prop: properties)
+ if (prop.name == pname)
+ return prop;
+ return Nothing;
+ }
+
+ /**
+ * Get the property value of the correct type
+ *
+ * @param prop_id a property id
+ *
+ * @return the value or Nothing
+ */
+ template<Id ID>
+ auto get() const {
+ constexpr auto&& prop{property<ID>()};
+ const auto str = std::invoke([&]()->std::string {
+ const auto str = cstore_.metadata(std::string{prop.name});
+ return str.empty() ? std::string{prop.default_val} : str;
+ });
+ if constexpr (prop.type == Type::Number)
+ return static_cast<size_t>(str.empty() ? 0 : std::atoll(str.c_str()));
+ if constexpr (prop.type == Type::Boolean)
+ return static_cast<size_t>(str.empty() ? false :
+ std::atol(str.c_str()) != 0);
+ else if constexpr (prop.type == Type::Timestamp)
+ return static_cast<time_t>(str.empty() ? 0 : std::atoll(str.c_str()));
+ else if constexpr (prop.type == Type::Path || prop.type == Type::String)
+ return str;
+ else if constexpr (prop.type == Type::StringList)
+ return split(str, SepaChar1);
+
+ throw std::logic_error("invalid prop " + std::string{prop.name});
+ }
+
+ /**
+ * Set a new value for some property
+ *
+ * @param prop_id property-id
+ * @param val the new value (of the correct type)
+ *
+ * @return Ok() or some error
+ */
+ template<Id ID, typename T>
+ Result<void> set(const T& val) {
+ constexpr auto&& prop{property<ID>()};
+ if (read_only())
+ return Err(Error::Code::AccessDenied,
+ "cannot write to read-only db");
+
+ const auto strval = std::invoke([&]{
+ if constexpr (prop.type == Type::Number || prop.type == Type::Timestamp)
+ return mu_format("{}", static_cast<int64_t>(val));
+ if constexpr (prop.type == Type::Boolean)
+ return val ? "1" : "0";
+ else if constexpr (prop.type == Type::Path || prop.type == Type::String)
+ return std::string{val};
+ else if constexpr (prop.type == Type::StringList)
+ return join(val, SepaChar1);
+ else
+ throw std::logic_error("invalid prop " + std::string{prop.name});
+ });
+
+ cstore_.set_metadata(std::string{prop.name}, strval);
+ return Ok();
+ }
+
+ /**
+ * Is this a read-only Config?
+ *
+ *
+ * @return true or false
+ */
+ bool read_only() const { return cstore_.read_only();};
+
+ /**
+ * Import configurable settings to some other MetadataIface
+ *
+ * @param target some other metadata interface
+ */
+ void import_configurable(const Config& src) const {
+ for (auto&& prop: properties) {
+ if (any_of(prop.flags & Flags::Configurable)) {
+ const auto&& key{std::string{prop.name}};
+ if (auto&& val{src.cstore_.metadata(key)}; !val.empty())
+ cstore_.set_metadata(key, std::string{val});
+ }
+ }
+ }
+
+private:
+ MetadataIface& cstore_;
+};
+
+
+} // namespace Mu
+
+#endif /* MU_CONFIG_DB_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2019-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-contacts-cache.hh"
+
+#include <mutex>
+#include <unordered_map>
+#include <set>
+#include <sstream>
+#include <functional>
+#include <algorithm>
+#include <ctime>
+
+#include <utils/mu-utils.hh>
+#include <utils/mu-regex.hh>
+#include <glib.h>
+
+using namespace Mu;
+
+struct EmailHash {
+ std::size_t operator()(const std::string& email) const {
+ return lowercase_hash(email);
+ }
+};
+struct EmailEqual {
+ bool operator()(const std::string& email1, const std::string& email2) const {
+ return lowercase_hash(email1) == lowercase_hash(email2);
+ }
+};
+
+using ContactUMap = std::unordered_map<const std::string, Contact, EmailHash, EmailEqual>;
+struct ContactsCache::Private {
+ Private(Config& config_db)
+ :config_db_{config_db},
+ contacts_{deserialize(config_db_.get<Config::Id::Contacts>())},
+ personal_plain_{make_matchers<Config::Id::PersonalAddresses>()},
+ personal_rx_{make_rx_matchers<Config::Id::PersonalAddresses>()},
+ ignored_plain_{make_matchers<Config::Id::IgnoredAddresses>()},
+ ignored_rx_{make_rx_matchers<Config::Id::IgnoredAddresses>()},
+ dirty_{0},
+ email_rx_{unwrap(Regex::make(email_rx_str, G_REGEX_OPTIMIZE))}
+ {}
+
+ ~Private() { serialize(); }
+
+ ContactUMap deserialize(const std::string&) const;
+ void serialize() const;
+
+ bool is_valid_email(const std::string& email) const {
+ return email_rx_.matches(email);
+ }
+
+ Config& config_db_;
+ ContactUMap contacts_;
+ mutable std::mutex mtx_;
+
+ const StringVec personal_plain_;
+ const std::vector<Regex> personal_rx_;
+
+ const StringVec ignored_plain_;
+ const std::vector<Regex> ignored_rx_;
+
+ mutable size_t dirty_;
+ Regex email_rx_;
+
+private:
+ static bool is_rx(const std::string& p) {
+ return p.size() >= 2 && p.at(0) == '/' && p.at(p.length() - 1) == '/';
+ }
+
+ template<Config::Id Id> StringVec make_matchers() const {
+ return seq_remove(config_db_.get<Id>(), is_rx);
+ }
+ template<Config::Id Id> std::vector<Regex> make_rx_matchers() const {
+ std::vector<Regex> rxvec;
+ for (auto&& p: config_db_.get<Id>()) {
+
+ if (!is_rx(p))
+ continue;
+ constexpr auto opts{static_cast<GRegexCompileFlags>(G_REGEX_OPTIMIZE|G_REGEX_CASELESS)};
+
+ const auto rxstr{p.substr(1, p.length() - 2)};
+ try {
+ rxvec.push_back(unwrap(Regex::make(rxstr, opts)));
+ mu_debug("match {}: '{}' {}", Config::property<Id>().name,
+ p, rxvec.back());
+ } catch (const Error& rex) {
+ mu_warning("invalid personal address regexp '{}': {}",
+ p, rex.what());
+ }
+ }
+ return rxvec;
+ }
+
+ /* regexp as per:
+ * https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
+ *
+ * "This requirement is a willful violation of RFC 5322, which defines a
+ * syntax for email addresses that is simultaneously too strict (before
+ * the "@" character), too vague (after the "@" character), and too lax
+ * (allowing comments, whitespace characters, and quoted strings in
+ * manners unfamiliar to most users) to be of practical use here."
+ */
+ static constexpr auto email_rx_str =
+ R"(^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$)";
+
+};
+
+ContactUMap
+ContactsCache::Private::deserialize(const std::string& serialized) const
+{
+ ContactUMap contacts;
+ std::stringstream ss{serialized, std::ios_base::in};
+ std::string line;
+
+ while (getline(ss, line)) {
+ const auto parts = Mu::split(line, SepaChar2);
+ if (G_UNLIKELY(parts.size() != 5)) {
+ mu_warning("error: '{}'", line);
+ continue;
+ }
+ Contact ci(parts[0], // email
+ std::move(parts[1]), // name
+ (time_t)g_ascii_strtoll(parts[3].c_str(), NULL, 10), // message_date
+ parts[2][0] == '1' ? true : false, // personal
+ (std::size_t)g_ascii_strtoll(parts[4].c_str(), NULL, 10), // frequency
+ g_get_monotonic_time()); // tstamp
+ contacts.emplace(std::move(parts[0]), std::move(ci));
+ }
+
+ return contacts;
+}
+
+
+void
+ContactsCache::Private::serialize() const
+{
+ if (config_db_.read_only()) {
+ if (dirty_ > 0)
+ mu_critical("dirty data in read-only ccache!"); // bug
+ return;
+ }
+
+ std::string s;
+ std::unique_lock lock(mtx_);
+
+ if (dirty_ == 0)
+ return; // nothing to do.
+
+ for (auto& item : contacts_) {
+ const auto& ci{item.second};
+ s += mu_format("{}{}{}{}{}{}{}{}{}\n",
+ ci.email, SepaChar2,
+ ci.name, SepaChar2,
+ ci.personal ? 1 : 0, SepaChar2,
+ ci.message_date, SepaChar2,
+ ci.frequency);
+ }
+ config_db_.set<Config::Id::Contacts>(s);
+ dirty_ = 0;
+}
+
+ContactsCache::ContactsCache(Config& config_db)
+ : priv_{std::make_unique<Private>(config_db)}
+{}
+
+ContactsCache::~ContactsCache() = default;
+
+void
+ContactsCache::serialize() const
+{
+ if (priv_->config_db_.read_only())
+ throw std::runtime_error("cannot serialize read-only contacts-cache");
+
+ priv_->serialize();
+}
+
+void
+ContactsCache::add(Contact&& contact)
+{
+ /* we do _not_ cache invalid email addresses, so we won't offer them in completions etc. It
+ * should be _rare_, but we've seen cases ( broken local messages) */
+ if (!is_valid(contact.email)) {
+ mu_warning("not caching invalid e-mail address '{}'", contact.email);
+ return;
+ }
+
+ if (is_ignored(contact.email)) {
+ /* ignored this address, e.g. 'noreply@example.com */
+ return;
+ }
+
+ std::lock_guard<std::mutex> l_{priv_->mtx_};
+
+ ++priv_->dirty_;
+
+ auto it = priv_->contacts_.find(contact.email);
+
+ if (it == priv_->contacts_.end()) { // completely new contact
+
+ contact.name = contact.name;
+ if (!contact.personal)
+ contact.personal = is_personal(contact.email);
+ contact.tstamp = g_get_monotonic_time();
+
+ auto email{contact.email};
+ // return priv_->contacts_.emplace(ContactUMap::value_type(email, std::move(contact)))
+ // .first->second;
+ mu_debug("adding contact {} <{}>", contact.name.c_str(), contact.email.c_str());
+ priv_->contacts_.emplace(ContactUMap::value_type(email, std::move(contact)));
+
+ } else { // existing contact.
+ auto& existing{it->second};
+ ++existing.frequency;
+ if (contact.message_date > existing.message_date) { // update?
+ existing.email = std::move(contact.email);
+ // update name only if new one is not empty.
+ if (!contact.name.empty())
+ existing.name = std::move(contact.name);
+ existing.tstamp = g_get_monotonic_time();
+ existing.message_date = contact.message_date;
+ }
+ mu_debug("updating contact {} <{}> ({})",
+ contact.name, contact.email, existing.frequency);
+ }
+}
+
+
+void
+ContactsCache::add(Contacts&& contacts, bool& personal)
+{
+ personal = seq_find_if(contacts,[&](auto&& c){
+ return is_personal(c.email); }) != contacts.cend();
+
+ for (auto&& contact: contacts) {
+ contact.personal = personal;
+ add(std::move(contact));
+ }
+}
+
+const Contact*
+ContactsCache::_find(const std::string& email) const
+{
+ std::lock_guard<std::mutex> l_{priv_->mtx_};
+
+ const auto it = priv_->contacts_.find(email);
+ if (it == priv_->contacts_.end())
+ return {};
+ else
+ return &it->second;
+}
+
+void
+ContactsCache::clear()
+{
+ std::lock_guard<std::mutex> l_{priv_->mtx_};
+
+ ++priv_->dirty_;
+
+ priv_->contacts_.clear();
+}
+
+std::size_t
+ContactsCache::size() const
+{
+ std::lock_guard<std::mutex> l_{priv_->mtx_};
+
+ return priv_->contacts_.size();
+}
+
+
+/**
+ * This is used for sorting the Contacts in order of relevance. A highly
+ * specific algorithm, but the details don't matter _too_ much.
+ *
+ * This is currently used for the ordering in mu-cfind and auto-completion in
+ * mu4e, if the various completion methods don't override it...
+ */
+constexpr auto RecentOffset{15 * 24 * 3600};
+struct ContactLessThan {
+ ContactLessThan()
+ : recently_{::time({}) - RecentOffset} {}
+
+ bool operator()(const Mu::Contact& ci1, const Mu::Contact& ci2) const {
+ // non-personal is less relevant.
+ if (ci1.personal != ci2.personal)
+ return ci1.personal < ci2.personal;
+
+ // older is less relevant for recent messages
+ if (std::max(ci1.message_date, ci2.message_date) > recently_ &&
+ ci1.message_date != ci2.message_date)
+ return ci1.message_date < ci2.message_date;
+
+ // less frequent is less relevant
+ if (ci1.frequency != ci2.frequency)
+ return ci1.frequency < ci2.frequency;
+
+ // if all else fails, alphabetically
+ return ci1.email < ci2.email;
+ }
+ // only sort recently seen contacts by recency; approx 15 days.
+ // this changes during the lifetime, but that's all fine.
+ const time_t recently_;
+};
+
+using ContactSet = std::set<std::reference_wrapper<const Contact>,
+ ContactLessThan>;
+
+void
+ContactsCache::for_each(const EachContactFunc& each_contact) const
+{
+ std::lock_guard<std::mutex> l_{priv_->mtx_};
+
+ // first sort them for 'rank'
+ ContactSet sorted;
+ for (const auto& item : priv_->contacts_)
+ sorted.emplace(item.second);
+
+ // return in _reverse_ order, so we get the most relevant ones first.
+ for (auto it = sorted.rbegin(); it != sorted.rend(); ++it) {
+ if (!each_contact(*it))
+ break;
+ }
+}
+
+static bool
+address_matches(const std::string& addr, const StringVec& plain, const std::vector<Regex>& regexes)
+{
+ for (auto&& p : plain)
+ if (g_ascii_strcasecmp(addr.c_str(), p.c_str()) == 0)
+ return true;
+
+ for (auto&& rx : regexes) {
+ if (rx.matches(addr))
+ return true;
+ }
+
+ return false;
+}
+
+bool
+ContactsCache::is_personal(const std::string& addr) const
+{
+ return address_matches(addr, priv_->personal_plain_, priv_->personal_rx_);
+}
+
+bool
+ContactsCache::is_ignored(const std::string& addr) const
+{
+ return address_matches(addr, priv_->ignored_plain_, priv_->ignored_rx_);
+}
+
+bool
+ContactsCache::is_valid(const std::string& addr) const
+{
+ return priv_->is_valid_email(addr);
+}
+
+
+#ifdef BUILD_TESTS
+/*
+ * Tests.
+ *
+ */
+
+#include "utils/mu-test-utils.hh"
+
+static void
+test_mu_contacts_cache_base()
+{
+ MemDb xdb{};
+ Config cdb{xdb};
+ ContactsCache contacts(cdb);
+
+ g_assert_true(contacts.empty());
+ g_assert_cmpuint(contacts.size(), ==, 0);
+
+ contacts.add(Mu::Contact("foo.bar@example.com",
+ "Foo", {}, 12345));
+ g_assert_false(contacts.empty());
+ g_assert_cmpuint(contacts.size(), ==, 1);
+
+ contacts.add(Mu::Contact("cuux@example.com", "Cuux", {},
+ 54321));
+
+ g_assert_cmpuint(contacts.size(), ==, 2);
+
+ contacts.add(
+ Mu::Contact("foo.bar@example.com", "Foo", {}, 77777));
+ g_assert_cmpuint(contacts.size(), ==, 2);
+
+ contacts.add(
+ Mu::Contact("Foo.Bar@Example.Com", "Foo", {}, 88888));
+ g_assert_cmpuint(contacts.size(), ==, 2);
+ // note: replaces first.
+
+ {
+ const auto info = contacts._find("bla@example.com");
+ g_assert_false(info);
+ }
+
+ {
+ const auto info = contacts._find("foo.BAR@example.com");
+ g_assert_true(info);
+
+ g_assert_cmpstr(info->email.c_str(), ==, "Foo.Bar@Example.Com");
+ }
+
+ contacts.clear();
+ g_assert_true(contacts.empty());
+ g_assert_cmpuint(contacts.size(), ==, 0);
+}
+
+static void
+test_mu_contacts_cache_personal()
+{
+ MemDb xdb{};
+ Config cdb{xdb};
+ cdb.set<Config::Id::PersonalAddresses>
+ (StringVec{{"foo@example.com", "bar@cuux.org", "/bar-.*@fnorb.f./"}});
+ ContactsCache contacts{cdb};
+
+ g_assert_true(contacts.is_personal("foo@example.com"));
+ g_assert_true(contacts.is_personal("Bar@CuuX.orG"));
+ g_assert_true(contacts.is_personal("bar-123abc@fnorb.fi"));
+ g_assert_true(contacts.is_personal("bar-zzz@fnorb.fr"));
+
+ g_assert_false(contacts.is_personal("foo@bar.com"));
+ g_assert_false(contacts.is_personal("BÂr@CuuX.orG"));
+ g_assert_false(contacts.is_personal("bar@fnorb.fi"));
+ g_assert_false(contacts.is_personal("bar-zzz@fnorb.xr"));
+}
+
+static void
+test_mu_contacts_cache_ignored()
+{
+ MemDb xdb{};
+ Config cdb{xdb};
+ cdb.set<Config::Id::IgnoredAddresses>
+ (StringVec{{"foo@example.com", "bar@cuux.org", "/bar-.*@fnorb.f./"}});
+ ContactsCache contacts{cdb};
+
+ g_assert_true(contacts.is_ignored("foo@example.com"));
+ g_assert_true(contacts.is_ignored("Bar@CuuX.orG"));
+ g_assert_true(contacts.is_ignored("bar-123abc@fnorb.fi"));
+ g_assert_true(contacts.is_ignored("bar-zzz@fnorb.fr"));
+
+ g_assert_false(contacts.is_ignored("foo@bar.com"));
+ g_assert_false(contacts.is_ignored("BÂr@CuuX.orG"));
+ g_assert_false(contacts.is_ignored("bar@fnorb.fi"));
+ g_assert_false(contacts.is_ignored("bar-zzz@fnorb.xr"));
+
+ g_assert_cmpuint(contacts.size(),==,0);
+ contacts.add(Mu::Contact{"a@example.com", "a", 123, true, 1000, 0});
+ g_assert_cmpuint(contacts.size(),==,1);
+ contacts.add(Mu::Contact{"foo@example.com", "b", 123, true, 1000, 0}); // ignored
+ contacts.add(Mu::Contact{"bar-123abc@fnorb.fi", "c", 123, true, 1000, 0}); // ignored
+ g_assert_cmpuint(contacts.size(),==,1);
+ contacts.add(Mu::Contact{"b@example.com", "d", 123, true, 1000, 0});
+ g_assert_cmpuint(contacts.size(),==,2);
+}
+
+
+
+static void
+test_mu_contacts_cache_foreach()
+{
+ MemDb xdb{};
+ Config cdb{xdb};
+ ContactsCache ccache(cdb);
+
+ ccache.add(Mu::Contact{"a@example.com", "a", 123, true, 1000, 0});
+ ccache.add(Mu::Contact{"b@example.com", "b", 456, true, 1000, 0});
+
+ {
+ size_t n{};
+ g_assert_false(ccache.empty());
+ g_assert_cmpuint(ccache.size(),==,2);
+ ccache.for_each([&](auto&& contact) { ++n; return false; });
+ g_assert_cmpuint(n,==,1);
+ }
+
+ {
+ size_t n{};
+ g_assert_false(ccache.empty());
+ g_assert_cmpuint(ccache.size(),==,2);
+ ccache.for_each([&](auto&& contact) { ++n; return true; });
+ g_assert_cmpuint(n,==,2);
+ }
+
+ {
+ size_t n{};
+ ccache.clear();
+ g_assert_true(ccache.empty());
+ g_assert_cmpuint(ccache.size(),==,0);
+ ccache.for_each([&](auto&& contact) { ++n; return true; });
+ g_assert_cmpuint(n,==,0);
+ }
+}
+
+
+
+static void
+test_mu_contacts_cache_sort()
+{
+ auto result_chars = [](const Mu::ContactsCache& ccache)->std::string {
+ std::string str;
+ if (g_test_verbose())
+ fmt::print("contacts-cache:\n");
+
+ ccache.for_each([&](auto&& contact) {
+ if (g_test_verbose())
+ fmt::print("\t- {}\n", contact.display_name());
+ str += contact.name;
+ return true;
+ });
+ return str;
+ };
+
+ const auto now{std::time({})};
+
+ // "first" means more relevant
+
+ { /* recent messages, newer comes first */
+
+ MemDb xdb{};
+ Config cdb{xdb};
+ ContactsCache ccache(cdb);
+
+ ccache.add(Mu::Contact{"a@example.com", "a", now, true, 1000, 0});
+ ccache.add(Mu::Contact{"b@example.com", "b", now-1, true, 1000, 0});
+ assert_equal(result_chars(ccache), "ab");
+ }
+
+ { /* non-recent messages, more frequent comes first */
+
+ MemDb xdb{};
+ Config cdb{xdb};
+ ContactsCache ccache(cdb);
+
+ ccache.add(Mu::Contact{"a@example.com", "a", now-2*RecentOffset, true, 1000, 0});
+ ccache.add(Mu::Contact{"b@example.com", "b", now-3*RecentOffset, true, 2000, 0});
+ assert_equal(result_chars(ccache), "ba");
+ }
+
+ { /* personal comes first */
+ MemDb xdb{};
+ Config cdb{xdb};
+ ContactsCache ccache(cdb);
+
+ ccache.add(Mu::Contact{"a@example.com", "a", now-5*RecentOffset, true, 1000, 0});
+ ccache.add(Mu::Contact{"b@example.com", "b", now, false, 8000, 0});
+ assert_equal(result_chars(ccache), "ab");
+ }
+
+ { /* if all else fails, reverse-alphabetically */
+ MemDb xdb{};
+ Config cdb{xdb};
+ ContactsCache ccache(cdb);
+
+ ccache.add(Mu::Contact{"a@example.com", "a", now, false, 1000, 0});
+ ccache.add(Mu::Contact{"b@example.com", "b", now, false, 1000, 0});
+ g_assert_cmpuint(ccache.size(),==,2);
+ assert_equal(result_chars(ccache), "ba");
+ }
+}
+
+static void
+test_mu_contacts_valid_address()
+{
+ MemDb xdb{};
+ Config cdb{xdb};
+ ContactsCache ccache(cdb);
+
+ g_assert_true(ccache.is_valid("a@example.com"));
+ g_assert_false(ccache.is_valid("a***@@booa@example..com"));
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/contacts-cache/base", test_mu_contacts_cache_base);
+ g_test_add_func("/contacts-cache/personal", test_mu_contacts_cache_personal);
+ g_test_add_func("/contacts-cache/ignored", test_mu_contacts_cache_ignored);
+ g_test_add_func("/contacts-cache/for-each", test_mu_contacts_cache_foreach);
+ g_test_add_func("/contacts-cache/sort", test_mu_contacts_cache_sort);
+ g_test_add_func("/contacts-cache/valid-address", test_mu_contacts_valid_address);
+
+ return g_test_run();
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef __MU_CONTACTS_CACHE_HH__
+#define __MU_CONTACTS_CACHE_HH__
+
+#include <glib.h>
+#include <time.h>
+#include <memory>
+#include <functional>
+#include <chrono>
+#include <string>
+#include <time.h>
+#include <inttypes.h>
+#include <utils/mu-utils.hh>
+
+#include "mu-config.hh"
+#include <message/mu-message.hh>
+
+namespace Mu {
+
+class ContactsCache {
+public:
+ /**
+ * Construct a new ContactsCache object
+ *
+ * @param config db configuration database object
+ */
+ ContactsCache(Config& config);
+
+ /**
+ * DTOR
+ *
+ */
+ ~ContactsCache();
+
+ /**
+ * Add a contact
+ *
+ * Invalid email address are not cached (but we log a warning); neither
+ * are "ignored" addresses (see --ignored-address in mu-init(1))
+ *
+ * @param contact a Contact object
+ */
+ void add(Contact&& contact);
+
+
+ /**
+ * Add a contacts sequence; this should be used for the contacts of a
+ * specific message, and determines if it is a "personal" message:
+ * if any of the contacts matches one of the personal addresses,
+ * any of the senders/recipients are considered "personal"
+ *
+ * Invalid email address are not cached (but we log a warning); neither
+ * are "ignored" addresses (see --ignored-address in mu-init(1))
+ *
+ * @param contacts a Contact object sequence
+ * @param is_personal receives true if any of the contacts was personal;
+ * false otherwise
+ */
+ void add(Contacts&& contacts, bool& is_personal);
+ void add(Contacts&& contacts) {
+ bool _ignore;
+ add(std::move(contacts), _ignore);
+ }
+
+ /**
+ * Clear all contacts
+ *
+ */
+ void clear();
+
+ /**
+ * Get the number of contacts
+ *
+
+ * @return number of contacts
+ */
+ std::size_t size() const;
+
+ /**
+ * Are there no contacts?
+ *
+ * @return true or false
+ */
+ bool empty() const { return size() == 0; }
+
+ /**
+ * Serialize contact information. This all marks the data as
+ * non-dirty
+ */
+ void serialize() const;
+
+ /**
+ * Does this look like a 'personal' address?
+ *
+ * @param addr some e-mail address
+ *
+ * @return true or false
+ */
+ bool is_personal(const std::string& addr) const;
+
+ /**
+ * Does this look like an email-address that should be ignored?
+ *
+ * @param addr some e-mail address
+ *
+ * @return true or false
+ */
+ bool is_ignored(const std::string& addr) const;
+
+ /**
+ * Does this look like a valid email-address?
+ *
+ * @param addr some e-mail address
+ *
+ * @return true or false
+ */
+ bool is_valid(const std::string& addr) const;
+
+ /**
+ * Find a contact based on the email address. This is not safe, since
+ * the returned ptr can be invalidated at any time; only for unit-tests.
+ *
+ * @param email email address
+ *
+ * @return contact info, or {} if not found
+ */
+ const Contact* _find(const std::string& email) const;
+
+ /**
+ * Prototype for a callable that receives a contact
+ *
+ * @param contact some contact
+ *
+ * @return to get more contacts; false otherwise
+ */
+ using EachContactFunc = std::function<bool(const Contact& contact_info)>;
+
+ /**
+ * Invoke some callable for each contact, in _descending_ order of rank (i.e., the
+ * highest ranked contacts come first).
+ *
+ * @param each_contact function invoked for each contact
+ */
+ void for_each(const EachContactFunc& each_contact) const;
+
+private:
+ struct Private;
+ std::unique_ptr<Private> priv_;
+};
+
+} // namespace Mu
+
+#endif /* __MU_CONTACTS_CACHE_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2020-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-indexer.hh"
+
+#include <config.h>
+
+#include <atomic>
+#include <algorithm>
+#include <mutex>
+#include <vector>
+#include <thread>
+#include <condition_variable>
+#include <iostream>
+#include <atomic>
+#include <chrono>
+using namespace std::chrono_literals;
+
+#include "mu-store.hh"
+
+#include "mu-scanner.hh"
+#include "utils/mu-async-queue.hh"
+#include "utils/mu-error.hh"
+
+#include "utils/mu-utils-file.hh"
+
+using namespace Mu;
+
+struct IndexState {
+ enum State { Idle,
+ Scanning,
+ Finishing,
+ Cleaning
+ };
+ static const char* name(State s) {
+ switch (s) {
+ case Idle:
+ return "idle";
+ case Scanning:
+ return "scanning";
+ case Finishing:
+ return "finishing";
+ case Cleaning:
+ return "cleaning";
+ default:
+ return "<error>";
+ }
+ }
+
+ bool operator==(State rhs) const {
+ return state_.load() == rhs;
+ }
+ bool operator!=(State rhs) const {
+ return state_.load() != rhs;
+ }
+ void change_to(State new_state) {
+ mu_debug("changing indexer state {}->{}", name((State)state_),
+ name((State)new_state));
+ state_.store(new_state);
+ }
+
+private:
+ std::atomic<State> state_{Idle};
+};
+
+struct Indexer::Private {
+ Private(Mu::Store& store)
+ : store_{store}, scanner_{store_.root_maildir(),
+ [this](auto&& path,
+ auto&& statbuf, auto&& info) {
+ return handler(path, statbuf, info);
+ }},
+ max_message_size_{store_.config().get<Mu::Config::Id::MaxMessageSize>()},
+ was_empty_{store.empty()} {
+
+ mu_message("created indexer for {} -> "
+ "{} (batch-size: {}; was-empty: {}; ngrams: {})",
+ store.root_maildir(), store.path(),
+ store.config().get<Mu::Config::Id::BatchSize>(),
+ was_empty_,
+ store.config().get<Mu::Config::Id::SupportNgrams>());
+ }
+
+ ~Private() {
+ stop();
+ }
+
+ bool dir_predicate(const std::string& path, const struct dirent* dirent) const;
+ bool handler(const std::string& fullpath, struct stat* statbuf, Scanner::HandleType htype);
+
+ void maybe_start_worker();
+ void item_worker();
+ void scan_worker();
+
+ bool add_message(const std::string& path);
+
+ bool cleanup();
+ bool start(const Indexer::Config& conf, bool block);
+ bool stop();
+
+ bool is_running() const { return state_ != IndexState::Idle; }
+
+ Indexer::Config conf_;
+ Store& store_;
+ Scanner scanner_;
+ const size_t max_message_size_;
+
+ ::time_t dirstamp_{};
+ std::size_t max_workers_;
+ std::vector<std::thread> workers_;
+ std::thread scanner_worker_;
+
+ struct WorkItem {
+ std::string full_path;
+ enum Type {
+ Dir,
+ File
+ };
+ Type type;
+ };
+
+ AsyncQueue<WorkItem> todos_;
+
+ Progress progress_{};
+ IndexState state_{};
+ std::mutex lock_, w_lock_;
+ std::atomic<time_t> completed_{};
+ bool was_empty_{};
+};
+
+bool
+Indexer::Private::handler(const std::string& fullpath, struct stat* statbuf,
+ Scanner::HandleType htype)
+{
+ switch (htype) {
+ case Scanner::HandleType::EnterDir:
+ case Scanner::HandleType::EnterNewCur: {
+ if (fullpath.length() > MaxTermLength) {
+ // currently the db uses the path as a key, and
+ // therefore it cannot be too long. We'd get an error
+ // later anyway but for now it's useful for surviving
+ // circular symlinks
+ mu_warning("'{}' is too long; ignore", fullpath);
+ return false;
+ }
+
+ // in lazy-mode, we ignore this dir if its dirstamp suggest it
+ // is up-to-date (this is _not_ always true; hence we call it
+ // lazy-mode); only for actual message dirs, since the dir
+ // tstamps may not bubble up.U
+ dirstamp_ = store_.dirstamp(fullpath);
+ if (conf_.lazy_check && dirstamp_ >= statbuf->st_ctime &&
+ htype == Scanner::HandleType::EnterNewCur) {
+ mu_debug("skip {} (seems up-to-date: {:%FT%T} >= {:%FT%T})",
+ fullpath, mu_time(dirstamp_), mu_time(statbuf->st_ctime));
+ return false;
+ }
+
+ // don't index dirs with '.noindex'
+ auto noindex = ::access((fullpath + "/.noindex").c_str(), F_OK) == 0;
+ if (noindex) {
+ mu_debug("skip {} (has .noindex)", fullpath);
+ return false; // don't descend into this dir.
+ }
+
+ // don't index dirs with '.noupdate', unless we do a full
+ // (re)index.
+ if (!conf_.ignore_noupdate) {
+ auto noupdate = ::access((fullpath + "/.noupdate").c_str(), F_OK) == 0;
+ if (noupdate) {
+ mu_debug("skip {} (has .noupdate)", fullpath);
+ return false;
+ }
+ }
+
+ mu_debug("checked {}", fullpath);
+ return true;
+ }
+ case Scanner::HandleType::LeaveDir: {
+ todos_.push({fullpath, WorkItem::Type::Dir});
+ return true;
+ }
+
+ case Scanner::HandleType::File: {
+ ++progress_.checked;
+
+ if ((size_t)statbuf->st_size > max_message_size_) {
+ mu_debug("skip {} (too big: {} bytes)", fullpath, statbuf->st_size);
+ return false;
+ }
+
+ // if the message is not in the db yet, or not up-to-date, queue
+ // it for updating/inserting.
+ if (statbuf->st_ctime <= dirstamp_ && store_.contains_message(fullpath))
+ return false;
+
+ // push the remaining messages to our "todo" queue for
+ // (re)parsing and adding/updating to the database.
+ todos_.push({fullpath, WorkItem::Type::File});
+ return true;
+ }
+ default:
+ g_return_val_if_reached(false);
+ return false;
+ }
+}
+
+void
+Indexer::Private::maybe_start_worker()
+{
+ std::lock_guard lock{w_lock_};
+
+ if (todos_.size() > workers_.size() && workers_.size() < max_workers_) {
+ workers_.emplace_back(std::thread([this] { item_worker(); }));
+ mu_debug("added worker {}", workers_.size());
+ }
+}
+
+bool
+Indexer::Private::add_message(const std::string& path)
+{
+ /*
+ * Having the lock here makes things a _lot_ slower.
+ *
+ * The reason for having the lock is some helgrind warnings;
+ * but it believed those are _false alarms_
+ * https://gitlab.gnome.org/GNOME/glib/-/issues/2662
+ *
+ * For now, set the lock as we were seeing some db corruption.
+ */
+ std::unique_lock lock{w_lock_};
+ auto msg{Message::make_from_path(path, store_.message_options())};
+ if (!msg) {
+ mu_warning("failed to create message from {}: {}", path, msg.error().what());
+ return false;
+ }
+ // if the store was empty, we know that the message is completely new
+ // and can use the fast path (Xapians 'add_document' rather tahn
+ // 'replace_document)
+ auto res = store_.add_message(msg.value(), was_empty_);
+ if (!res) {
+ mu_warning("failed to add message @ {}: {}", path, res.error().what());
+ return false;
+ }
+
+ return true;
+}
+
+void
+Indexer::Private::item_worker()
+{
+ WorkItem item;
+
+ mu_debug("started worker");
+
+ while (state_ == IndexState::Scanning) {
+ if (!todos_.pop(item, 250ms))
+ continue;
+ try {
+ switch (item.type) {
+ case WorkItem::Type::File: {
+ if (G_LIKELY(add_message(item.full_path)))
+ ++progress_.updated;
+ } break;
+ case WorkItem::Type::Dir:
+ store_.set_dirstamp(item.full_path, ::time(NULL));
+ break;
+ default:
+ g_warn_if_reached();
+ break;
+ }
+ } catch (const Mu::Error& er) {
+ mu_warning("error adding message @ {}: {}", item.full_path, er.what());
+ }
+
+ maybe_start_worker();
+ std::this_thread::yield();
+ }
+}
+
+bool
+Indexer::Private::cleanup()
+{
+ mu_debug("starting cleanup");
+
+ size_t n{};
+ std::vector<Store::Id> orphans; // store messages without files.
+ store_.for_each_message_path([&](Store::Id id, const std::string& path) {
+ ++n;
+ if (::access(path.c_str(), R_OK) != 0) {
+ mu_debug("cannot read {} (id={}); queuing for removal from store",
+ path, id);
+ orphans.emplace_back(id);
+ }
+
+ return state_ == IndexState::Cleaning;
+ });
+
+ if (orphans.empty())
+ mu_debug("nothing to clean up");
+ else {
+ mu_debug("removing {} stale message(s) from store", orphans.size());
+ store_.remove_messages(orphans);
+ progress_.removed += orphans.size();
+ }
+
+ return true;
+}
+
+void
+Indexer::Private::scan_worker()
+{
+ XapianDb::Transaction tx{store_.xapian_db()}; // RAII
+
+ progress_.reset();
+ if (conf_.scan) {
+ mu_debug("starting scanner");
+ if (!scanner_.start()) { // blocks.
+ mu_warning("failed to start scanner");
+ state_.change_to(IndexState::Idle);
+ return;
+ }
+ mu_debug("scanner finished with {} file(s) in queue", todos_.size());
+ }
+
+ // now there may still be messages in the work queue...
+ // finish those; this is a bit ugly; perhaps we should
+ // handle SIGTERM etc.
+
+ if (!todos_.empty()) {
+ const auto workers_size = std::invoke([this] {
+ std::lock_guard lock{w_lock_};
+ return workers_.size();
+ });
+ mu_debug("process {} remaining message(s) with {} worker(s)",
+ todos_.size(), workers_size);
+ while (!todos_.empty())
+ std::this_thread::sleep_for(100ms);
+ }
+ // and let the worker finish their work.
+ state_.change_to(IndexState::Finishing);
+ for (auto&& w : workers_)
+ if (w.joinable())
+ w.join();
+
+ if (conf_.cleanup) {
+ mu_debug("starting cleanup");
+ state_.change_to(IndexState::Cleaning);
+ cleanup();
+ mu_debug("cleanup finished");
+ }
+
+ completed_ = ::time({});
+ store_.config().set<Mu::Config::Id::LastIndex>(completed_);
+ state_.change_to(IndexState::Idle);
+}
+
+bool
+Indexer::Private::start(const Indexer::Config& conf, bool block)
+{
+ stop();
+
+ conf_ = conf;
+ if (conf_.max_threads == 0) {
+ /* benchmarking suggests that ~4 threads is the fastest (the
+ * real bottleneck is the database, so adding more threads just
+ * slows things down)
+ */
+ max_workers_ = std::min(4U, std::thread::hardware_concurrency());
+ } else
+ max_workers_ = conf.max_threads;
+
+ if (store_.empty() && conf_.lazy_check) {
+ mu_debug("turn off lazy check since we have an empty store");
+ conf_.lazy_check = false;
+ }
+
+ mu_debug("starting indexer with <= {} worker thread(s)", max_workers_);
+ mu_debug("indexing: {}; clean-up: {}", conf_.scan ? "yes" : "no",
+ conf_.cleanup ? "yes" : "no");
+
+ state_.change_to(IndexState::Scanning);
+ /* kick off the first worker, which will spawn more if needed. */
+ workers_.emplace_back(std::thread([this] { item_worker(); }));
+ /* kick the disk-scanner thread */
+ scanner_worker_ = std::thread([this] { scan_worker(); });
+
+ mu_debug("started indexer in {}-mode", block ? "blocking" : "non-blocking");
+ if (block) {
+ while(is_running()) {
+ using namespace std::chrono_literals;
+ std::this_thread::sleep_for(100ms);
+ }
+ }
+
+ return true;
+}
+
+bool
+Indexer::Private::stop()
+{
+ scanner_.stop();
+
+ todos_.clear();
+ if (scanner_worker_.joinable())
+ scanner_worker_.join();
+
+ state_.change_to(IndexState::Idle);
+ for (auto&& w : workers_)
+ if (w.joinable())
+ w.join();
+ workers_.clear();
+
+ return true;
+}
+
+Indexer::Indexer(Store& store)
+ : priv_{std::make_unique<Private>(store)}
+{}
+
+Indexer::~Indexer() = default;
+
+bool
+Indexer::start(const Indexer::Config& conf, bool block)
+{
+ const auto mdir{priv_->store_.root_maildir()};
+ if (G_UNLIKELY(access(mdir.c_str(), R_OK) != 0)) {
+ mu_critical("'{}' is not readable: {}", mdir, g_strerror(errno));
+ return false;
+ }
+
+ std::lock_guard lock(priv_->lock_);
+ if (is_running())
+ return true;
+
+ return priv_->start(conf, block);
+}
+
+bool
+Indexer::stop()
+{
+ std::lock_guard lock{priv_->lock_};
+
+ if (!is_running())
+ return true;
+
+ mu_debug("stopping indexer");
+ return priv_->stop();
+}
+
+bool
+Indexer::is_running() const
+{
+ return priv_->is_running();
+}
+
+const Indexer::Progress&
+Indexer::progress() const
+{
+ priv_->progress_.running = priv_->state_ == IndexState::Idle ? false : true;
+
+ return priv_->progress_;
+}
+
+::time_t
+Indexer::completed() const
+{
+ return priv_->completed_;
+}
+
+
+#if BUILD_TESTS
+#include "mu-test-utils.hh"
+
+static void
+test_index_basic()
+{
+ allow_warnings();
+
+ TempDir tdir;
+ auto store = Store::make_new(tdir.path(), MU_TESTMAILDIR2);
+ assert_valid_result(store);
+ g_assert_true(store->empty());
+
+ Indexer& idx{store->indexer()};
+
+ g_assert_false(idx.is_running());
+ g_assert_true(idx.stop());
+ g_assert_cmpuint(idx.completed(),==, 0);
+
+ const auto& prog{idx.progress()};
+ g_assert_false(prog.running);
+ g_assert_cmpuint(prog.checked,==, 0);
+ g_assert_cmpuint(prog.updated,==, 0);
+ g_assert_cmpuint(prog.removed,==, 0);
+
+ Indexer::Config conf{};
+ conf.ignore_noupdate = true;
+
+ {
+ const auto start{time({})};
+ g_assert_true(idx.start(conf));
+ while (idx.is_running())
+ g_usleep(10000);
+
+ g_assert_false(idx.is_running());
+ g_assert_true(idx.stop());
+
+ g_assert_cmpuint(idx.completed() - start, <, 5);
+
+ g_assert_false(prog.running);
+ g_assert_cmpuint(prog.checked,==, 14);
+ g_assert_cmpuint(prog.updated,==, 14);
+ g_assert_cmpuint(prog.removed,==, 0);
+
+ g_assert_cmpuint(store->size(),==,14);
+ }
+
+ conf.lazy_check = true;
+ conf.max_threads = 1;
+ conf.ignore_noupdate = false;
+
+ {
+ const auto start{time({})};
+ g_assert_true(idx.start(conf));
+ while (idx.is_running())
+ g_usleep(10000);
+
+ g_assert_false(idx.is_running());
+ g_assert_true(idx.stop());
+
+ g_assert_cmpuint(idx.completed() - start, <, 3);
+
+ g_assert_false(prog.running);
+ g_assert_cmpuint(prog.checked,==, 0);
+ g_assert_cmpuint(prog.updated,==, 0);
+ g_assert_cmpuint(prog.removed,==, 0);
+
+ g_assert_cmpuint(store->size(),==, 14);
+ }
+}
+
+
+static void
+test_index_lazy()
+{
+ allow_warnings();
+
+ TempDir tdir;
+ auto store = Store::make_new(tdir.path(), MU_TESTMAILDIR2);
+ assert_valid_result(store);
+ g_assert_true(store->empty());
+ Indexer& idx{store->indexer()};
+
+ Indexer::Config conf{};
+ conf.lazy_check = true;
+ conf.ignore_noupdate = false;
+
+ const auto start{time({})};
+ g_assert_true(idx.start(conf));
+ while (idx.is_running())
+ g_usleep(10000);
+
+ g_assert_false(idx.is_running());
+ g_assert_true(idx.stop());
+
+ g_assert_cmpuint(idx.completed() - start, <, 3);
+
+ const auto& prog{idx.progress()};
+ g_assert_false(prog.running);
+ g_assert_cmpuint(prog.checked,==, 6);
+ g_assert_cmpuint(prog.updated,==, 6);
+ g_assert_cmpuint(prog.removed,==, 0);
+
+ g_assert_cmpuint(store->size(),==, 6);
+}
+
+static void
+test_index_cleanup()
+{
+ allow_warnings();
+
+ TempDir tdir;
+ auto mdir = join_paths(tdir.path(), "Test");
+ {
+ auto res = run_command({"cp", "-r", MU_TESTMAILDIR2, mdir});
+ assert_valid_result(res);
+ g_assert_cmpuint(res->exit_code,==, 0);
+ }
+
+ auto store = Store::make_new(tdir.path(), mdir);
+ assert_valid_result(store);
+ g_assert_true(store->empty());
+ Indexer& idx{store->indexer()};
+
+ Indexer::Config conf{};
+ conf.ignore_noupdate = true;
+
+ g_assert_true(idx.start(conf));
+ while (idx.is_running())
+ g_usleep(10000);
+
+ g_assert_false(idx.is_running());
+ g_assert_true(idx.stop());
+ g_assert_cmpuint(store->size(),==, 14);
+
+ // remove a message
+ {
+ auto mpath = join_paths(mdir, "bar", "cur", "mail6");
+ auto res = run_command({"rm", mpath});
+ assert_valid_result(res);
+ g_assert_cmpuint(res->exit_code,==, 0);
+ }
+
+ // no cleanup, # stays the same
+ conf.cleanup = false;
+ g_assert_true(idx.start(conf));
+ while (idx.is_running())
+ g_usleep(10000);
+ g_assert_false(idx.is_running());
+ g_assert_true(idx.stop());
+ g_assert_cmpuint(store->size(),==, 14);
+
+ // cleanup, message is gone from store.
+ conf.cleanup = true;
+ g_assert_true(idx.start(conf));
+ while (idx.is_running())
+ g_usleep(10000);
+ g_assert_false(idx.is_running());
+ g_assert_true(idx.stop());
+ g_assert_cmpuint(store->size(),==, 13);
+}
+
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/index/basic", test_index_basic);
+ g_test_add_func("/index/lazy", test_index_lazy);
+ g_test_add_func("/index/cleanup", test_index_cleanup);
+
+ return g_test_run();
+
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_INDEXER_HH__
+#define MU_INDEXER_HH__
+
+#include <atomic>
+#include <memory>
+#include <chrono>
+
+namespace Mu {
+
+class Store;
+
+/// An object abstracting the index process.
+class Indexer {
+public:
+ /**
+ * Construct an indexer object
+ *
+ * @param store the message store to use
+ */
+ Indexer(Store& store);
+
+ /**
+ * DTOR
+ */
+ ~Indexer();
+
+ /// A configuration object for the indexer
+ struct Config {
+ bool scan{true};
+ /**< scan for new messages */
+ bool cleanup{true};
+ /**< clean messages no longer in the file system */
+ size_t max_threads{};
+ /**< maximum # of threads to use */
+ bool ignore_noupdate{};
+ /**< ignore .noupdate files */
+ bool lazy_check{};
+ /**< whether to skip directories that don't have a changed
+ * mtime */
+ };
+
+ /**
+ * Start indexing. If already underway, do nothing. This returns
+ * immediately after starting, with the work being done in the
+ * background, unless blocking = true
+ *
+ * @param conf a configuration object
+ *
+ * @return true if starting worked or an indexing process was already
+ * underway; false otherwise.
+ *
+ */
+ bool start(const Config& conf, bool block=false);
+
+ /**
+ * Stop indexing. If not indexing, do nothing.
+ *
+ * @return true if we stopped indexing, or indexing was not underway; false otherwise.
+ */
+ bool stop();
+
+ /**
+ * Is an indexing process running?
+ *
+ * @return true or false.
+ */
+ bool is_running() const;
+
+ // Object describing current progress
+ struct Progress {
+ void reset() {
+ running = false;
+ checked = updated = removed = 0;
+ }
+ std::atomic<bool> running{}; /**< Is an index operation in progress? */
+ std::atomic<size_t> checked{}; /**< Number of messages checked for changes */
+ std::atomic<size_t> updated{}; /**< Number of messages (re)parsed/added/updated */
+ std::atomic<size_t> removed{}; /**< Number of message removed from store */
+ };
+
+ /**
+ * Get an object describing the current progress. The progress object
+ * describes the most recent indexing job, and is reset upon a fresh
+ * start().
+ *
+ * @return a progress object.
+ */
+ const Progress& progress() const;
+
+ /**
+ * Last time indexing was completed.
+ *
+ * @return the time or 0
+ */
+ ::time_t completed() const;
+
+private:
+ struct Private;
+ std::unique_ptr<Private> priv_;
+};
+
+} // namespace Mu
+#endif /* MU_INDEXER_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2008-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to 59the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <string>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <fcntl.h>
+#include <stdlib.h>
+
+#include <string.h>
+#include <errno.h>
+#include <glib/gprintf.h>
+#include <gio/gio.h>
+
+#include "glibconfig.h"
+#include "mu-maildir.hh"
+#include "utils/mu-utils.hh"
+#include "utils/mu-utils-file.hh"
+
+using namespace Mu;
+
+#define MU_MAILDIR_NOINDEX_FILE ".noindex"
+#define MU_MAILDIR_NOUPDATE_FILE ".noupdate"
+
+/* On Linux (and some BSD), we have entry->d_type, but some file
+ * systems (XFS, ReiserFS) do not support it, and set it DT_UNKNOWN.
+ * On other OSs, notably Solaris, entry->d_type is not present at all.
+ * For these cases, we use lstat (in get_dtype) as a slower fallback,
+ * and return it in the d_type parameter
+ */
+static unsigned char
+get_dtype(struct dirent* dentry, const std::string& path, bool use_lstat)
+{
+#ifdef HAVE_STRUCT_DIRENT_D_TYPE
+
+ if (dentry->d_type == DT_UNKNOWN)
+ goto slowpath;
+ if (dentry->d_type == DT_LNK && !use_lstat)
+ goto slowpath;
+
+ return dentry->d_type; /* fastpath */
+
+slowpath:
+#endif /*HAVE_STRUCT_DIRENT_D_TYPE*/
+
+ return determine_dtype(path, use_lstat);
+}
+
+static Mu::Result<void>
+create_maildir(const std::string& path, mode_t mode)
+{
+ if (path.empty())
+ return Err(Error{Error::Code::File, "path must not be empty"});
+
+ std::array<std::string,3> subdirs = {"new", "cur", "tmp"};
+ for (auto&& subdir: subdirs) {
+
+ const auto fullpath{join_paths(path, subdir)};
+
+ /* if subdir already exists, don't try to re-create
+ * it */
+ if (check_dir(fullpath, true/*readable*/, true/*writable*/))
+ continue;
+
+ int rv{g_mkdir_with_parents(fullpath.c_str(), static_cast<int>(mode))};
+
+ /* note, g_mkdir_with_parents won't detect an error if
+ * there's already such a dir, but with the wrong
+ * permissions; so we need to check */
+ if (rv != 0 || !check_dir(fullpath, true/*readable*/, true/*writable*/))
+ return Err(Error{Error::Code::File,
+ "creating dir failed for {}: {}",
+ fullpath, g_strerror(errno)});
+ }
+
+ return Ok();
+}
+
+static Mu::Result<void> /* create a noindex file if requested */
+create_noindex(const std::string& path)
+{
+ const auto noindexpath{join_paths(path, MU_MAILDIR_NOINDEX_FILE)};
+
+ /* note, if the 'close' failed, creation may still have succeeded...*/
+ int fd = ::creat(noindexpath.c_str(), 0644);
+ if (fd < 0 || ::close(fd) != 0)
+ return Err(Error{Error::Code::File,
+ "error creating .noindex: {}", g_strerror(errno)});
+ else
+ return Ok();
+}
+
+Mu::Result<void>
+Mu::maildir_mkdir(const std::string& path, mode_t mode, bool noindex)
+{
+ if (auto&& created{create_maildir(path, mode)}; !created)
+ return created; // fail.
+ else if (!noindex)
+ return Ok();
+
+ if (auto&& created{create_noindex(path)}; !created)
+ return created; //fail
+
+ return Ok();
+}
+
+/* determine whether the source message is in 'new' or in 'cur';
+ * we ignore messages in 'tmp' for obvious reasons */
+static Mu::Result<void>
+check_subdir(const std::string& src, bool& in_cur)
+{
+ char *srcpath{g_path_get_dirname(src.c_str())};
+
+ bool invalid{};
+ if (g_str_has_suffix(srcpath, "cur"))
+ in_cur = true;
+ else if (g_str_has_suffix(srcpath, "new"))
+ in_cur = false;
+ else
+ invalid = true;
+
+ g_free(srcpath);
+
+ if (invalid)
+ return Err(Error{Error::Code::File, "invalid source message '{}'", src});
+ else
+ return Ok();
+}
+
+static Mu::Result<std::string>
+get_target_fullpath(const std::string& src, const std::string& targetpath,
+ bool unique_names)
+{
+ bool in_cur{};
+ if (auto&& res = check_subdir(src, in_cur); !res)
+ return Err(std::move(res.error()));
+
+ const auto srcfile{basename(src)};
+
+ /* create target-path; note: make the filename *cough* unique by
+ * including a hash of the srcname in the targetname. This helps if
+ * there are copies of a message (which all have the same basename)
+ */
+ if (unique_names)
+ return join_paths(targetpath, in_cur ? "cur" : "new",
+ mu_format("{:08x}-{}", g_str_hash(src.c_str()), srcfile));
+ else
+ return join_paths(targetpath, in_cur ? "cur" : "new", srcfile.c_str());
+}
+
+Result<void>
+Mu::maildir_link(const std::string& src, const std::string& targetpath,
+ bool unique_names)
+{
+ auto path_res{get_target_fullpath(src, targetpath, unique_names)};
+ if (!path_res)
+ return Err(std::move(path_res.error()));
+
+ auto rv{::symlink(src.c_str(), path_res->c_str())};
+ if (rv != 0)
+ return Err(Error{Error::Code::File,
+ "error creating link {} => {}: {}",
+ *path_res, src, g_strerror(errno)});
+
+ return Ok();
+}
+
+static bool
+clear_links(const std::string& path, DIR* dir)
+{
+ bool res;
+ struct dirent* dentry;
+
+ res = true;
+ errno = 0;
+
+ while ((dentry = ::readdir(dir))) {
+
+ if (dentry->d_name[0] == '.')
+ continue; /* ignore .,.. other dotdirs */
+
+ const auto fullpath{join_paths(path, dentry->d_name)};
+ const auto d_type = get_dtype(dentry, fullpath.c_str(),
+ true/*lstat*/);
+ switch(d_type) {
+ case DT_LNK:
+ if (::unlink(fullpath.c_str()) != 0) {
+ /* LCOV_EXCL_START*/
+ mu_warning("error unlinking {}: {}", fullpath, g_strerror(errno));
+ res = false;
+ /* LCOV_EXCL_STOP*/
+ } else
+ mu_debug("unlinked linksdir {}", fullpath);
+ break;
+ case DT_DIR: {
+ DIR* subdir{::opendir(fullpath.c_str())};
+ /* LCOV_EXCL_START*/
+ if (!subdir) {
+ mu_warning("error opening dir {}: {}", fullpath, g_strerror(errno));
+ res = false;
+ }
+ if (!clear_links(fullpath, subdir))
+ res = false;
+ /* LCOV_EXCL_STOP*/
+ ::closedir(subdir);
+ } break;
+ default:
+ break;
+ }
+ }
+
+ return res;
+}
+
+Mu::Result<void>
+Mu::maildir_clear_links(const std::string& path)
+{
+ const auto dir{::opendir(path.c_str())};
+ if (!dir)
+ return Err(Error{Error::Code::File, "failed to open {}: {}",
+ path, g_strerror(errno)});
+
+ clear_links(path, dir);
+ ::closedir(dir);
+
+ return Ok();
+}
+
+/* LCOV_EXCL_START*/
+static Mu::Result<void>
+msg_move_verify(const std::string& src, const std::string& dst)
+{
+ /* double check -- is the target really there? */
+ if (::access(dst.c_str(), F_OK) != 0)
+ return Err(Error{Error::Code::File,
+ "can't find target ({}->{})", src, dst});
+
+ if (::access(src.c_str(), F_OK) == 0) {
+ if (src == dst) {
+ mu_warning("moved {} to itself", src);
+ }
+ /* this could happen if some other tool (for mail syncing) is
+ * interfering */
+ mu_debug("source is still there ({}->{})", src, dst);
+ }
+
+ mu_debug("moved {} -> {}", src, dst);
+
+ return Ok();
+}
+/* LCOV_EXCL_STOP*/
+
+/* LCOV_EXCL_START*/
+// don't use this right now, since it gives as (false alarm)
+// valgrind warning in tests
+/* use GIO to move files; this is slower than rename() so only use
+ * this when needed: when moving across filesystems */
+G_GNUC_UNUSED static Mu::Result<void>
+msg_move_g_file(const std::string& src, const std::string& dst)
+{
+ GFile *srcfile{g_file_new_for_path(src.c_str())};
+ GFile *dstfile{g_file_new_for_path(dst.c_str())};
+
+ GError* err{};
+ auto res = g_file_move(srcfile, dstfile,
+ G_FILE_COPY_OVERWRITE,
+ NULL, NULL, NULL, &err);
+ g_clear_object(&srcfile);
+ g_clear_object(&dstfile);
+
+ if (res)
+ return Ok();
+ else
+ return Err(Error::Code::File, &err, "error moving {} -> {}", src, dst);
+}
+/* LCOV_EXCL_STOP*/
+
+/* use mv to move files; this is slower than rename() so only use this when
+ * needed: when moving across filesystems */
+G_GNUC_UNUSED static Mu::Result<void>
+msg_move_mv_file(const std::string& src, const std::string& dst)
+{
+ if (auto res{run_command0({"/bin/mv", src, dst})}; !res)
+ return Err(Error::Code::File, "error moving {}->{}; err={}", src, dst, res.error());
+ else
+ return Ok();
+}
+
+Mu::Result<void>
+Mu::maildir_move_message(const std::string& oldpath,
+ const std::string& newpath,
+ bool assume_remote)
+{
+ mu_debug("moving {} --> {} (assume-remote:{})", oldpath, newpath, assume_remote);
+
+ if (::access(oldpath.c_str(), R_OK) != 0)
+ return Err(Error{Error::Code::File, "cannot read {}", oldpath});
+
+ if (oldpath == newpath)
+ return Ok(); // nothing to do.
+
+ if (!assume_remote) { /* for testing */
+
+ if (::rename(oldpath.c_str(), newpath.c_str()) == 0) /* seems it worked; double-check */
+ return msg_move_verify(oldpath, newpath);
+ /* LCOV_EXCL_START*/
+ if (errno != EXDEV) /* some unrecoverable error occurred */
+ return Err(Error{Error::Code::File, "error moving {} -> {}: {}",
+ oldpath, newpath, strerror(errno)});
+ /* LCOV_EXCL_STOP*/
+ }
+
+ /* the EXDEV / assume-remote case -- source and target live on different
+ * file systems
+ *
+ * we can choose either msg_move_gio_file or msg_move_mv_file;
+ * we use the latter for now, since the former gives some (false)
+ * valgrind alarms.
+ * */
+ if (auto&& res{msg_move_mv_file(oldpath, newpath)}; !res)
+ return res;
+ else
+ return msg_move_verify(oldpath, newpath);
+}
+
+static std::string
+reinvent_filename_base()
+{
+ return mu_format("{}.{:08x}{:08x}.{}", ::time({}),
+ g_random_int(), g_get_monotonic_time(), g_get_host_name());
+}
+
+/**
+ * Determine the destination filename
+ *
+ * @param file a filename
+ * @param flags flags for the destination
+ * @param new_name whether to change the basename
+ *
+ * @return the destion filename.
+ */
+static std::string
+determine_dst_filename(const std::string& file, Flags flags,
+ bool new_name)
+{
+ /* Recalculate a unique new base file name */
+ auto&& parts{message_file_parts(file)};
+ if (new_name)
+ parts.base = reinvent_filename_base();
+
+ /* for a New message, there are no flags etc.; so we only return the
+ * name sans suffix */
+ if (any_of(flags & Flags::New))
+ return std::move(parts.base);
+
+ const auto flagstr{
+ to_string(
+ flags_filter(
+ flags, MessageFlagCategory::Mailfile))};
+
+ return parts.base + parts.separator + "2," + flagstr;
+}
+
+
+/*
+ * sanity checks
+ */
+static Mu::Result<void>
+check_determine_target_params (const std::string& old_path,
+ const std::string& root_maildir_path,
+ const std::string& target_maildir,
+ Flags newflags)
+{
+ if (!g_path_is_absolute(old_path.c_str()))
+ return Err(Error{Error::Code::File,
+ "old_path is not absolute ({})", old_path});
+
+ if (!g_path_is_absolute(root_maildir_path.c_str()))
+ return Err(Error{Error::Code::File,
+ "root maildir path is not absolute ({})",
+ root_maildir_path});
+
+ if (!target_maildir.empty() && target_maildir[0] != '/')
+ return Err(Error{Error::Code::File,
+ "target maildir must be empty or start with / ({})",
+ target_maildir});
+
+ if (old_path.find(root_maildir_path) != 0)
+ return Err(Error{Error::Code::File,
+ "old-path must be below root-maildir ({}) ({})",
+ old_path, root_maildir_path});
+
+ if (any_of(newflags & Flags::New) && newflags != Flags::New)
+ return Err(Error{Error::Code::File,
+ "if the New flag is specified, it must be the only flag"});
+ return Ok();
+}
+
+
+Mu::Result<std::string>
+Mu::maildir_determine_target(const std::string& old_path,
+ const std::string& root_maildir_path,
+ const std::string& target_maildir,
+ Flags newflags,
+ bool new_name)
+{
+ newflags = flags_maildir_file(newflags); // filter out irrelevant flags.
+
+ /* sanity checks */
+ if (const auto checked{check_determine_target_params(
+ old_path, root_maildir_path, target_maildir, newflags)}; !checked)
+ return Err(Error{std::move(checked.error())});
+
+ /*
+ * this gets us the source maildir filesystem path, the directory
+ * in which new/ & cur/ lives, and the source file
+ */
+ const auto src{base_message_dir_file(old_path)};
+ if (!src)
+ return Err(src.error());
+ const auto& [src_mdir, src_file, is_new] = *src;
+
+ /* if target_mdir is empty, the src_dir does not change (though cur/
+ * maybe become new or vice-versa) */
+ const auto dst_mdir = target_maildir.empty() ? src_mdir :
+ join_paths(root_maildir_path, target_maildir);
+
+ /* now calculate the message name (incl. its immediate parent dir) */
+ const auto dst_file{determine_dst_filename(src_file, newflags, new_name)};
+
+ /* and the complete path name. */
+ const std::string subdir{(none_of(newflags & Flags::New)) ? "cur" : "new"};
+
+ return join_paths(dst_mdir, subdir,dst_file);
+}
--- /dev/null
+/*
+** Copyright (C) 2008-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_MAILDIR_HH__
+#define MU_MAILDIR_HH__
+
+#include <string>
+#include <utils/mu-result.hh>
+
+#include <glib.h>
+#include <time.h>
+#include <sys/types.h> /* for mode_t */
+#include <message/mu-message.hh>
+
+namespace Mu {
+
+/**
+ * Create a new maildir. if parts of the maildir already exists, those will
+ * simply be ignored.
+ *
+ * IOW, if you try to create the same maildir twice, the second will simply be a
+ * no-op (without any errors). Note, if the function fails 'halfway', it will
+ * *not* try to remove the parts the were created. it *will* create any parent
+ * dirs that are not yet existent.
+ *
+ * @param path the path (missing components will be created, as in 'mkdir -p').
+ * must be non-empty
+ * @param mode the file mode (e.g., 0755)
+ * @param noindex add a .noindex file to the maildir, so it will be excluded
+ * from indexing by 'mu index'
+ *
+ * @return a valid result (!!result) or an Error
+ */
+Result<void> maildir_mkdir(const std::string& path, mode_t mode=0700,
+ bool noindex=false);
+
+/**
+ * Create a symbolic link to a mail message
+ *
+ * @param src the full path to the source message
+ * @param targetpath the path to the target maildir; ie., *not*
+ * MyMaildir/cur, but just MyMaildir/. The function will figure out
+ * the correct subdir then.
+ * @param unique_names whether to create unique names; should be true unless
+ * for tests.
+ *
+ * @return a valid result (!!result) or an Error
+ */
+Result<void> maildir_link(const std::string& src, const std::string& targetpath,
+ bool unique_names=true);
+/**
+ * Recursively delete all the symbolic links in a directory tree
+ *
+ * @param dir top dir
+ *
+ * @return a valid result (!!result) or an Error
+ */
+Result<void> maildir_clear_links(const std::string& dir);
+
+/**
+ * Move a message file to another maildir. If the target exists, it is overwritten.
+ *
+ * @param oldpath an absolute file system path to an existing message in an
+ * actual maildir
+ * @param newpath the absolute full path to the target file
+ * @param assume_remote assume the target is on a different file-system,
+ * and hence rename() won't work and we need another method
+ *
+ * @return a valid result or an Error
+ */
+Result<void> maildir_move_message(const std::string& oldpath,
+ const std::string& newpath,
+ bool assume_remote = false);
+
+/**
+ * Determine the target path for a to-be-moved message; i.e. this does not
+ * actually move the message, only calculate the path.
+ *
+ * @param old_path an absolute file system path to an existing message in an
+ * actual maildir
+ * @param root_maildir_path the absolute file system path under which
+ * all maildirs live.
+ * @param target_maildir the target maildir; note that this the base-level
+ * Maildir, ie. /home/user/Maildir/archive, and must _not_ include the
+ * 'cur' or 'new' part. Note that the target maildir must be on the
+ * same filesystem. Can be empty if the message should not be moved to
+ * a different maildir; note that this may still involve a
+ * move to another directory (say, from new/ to cur/)
+ * @param flags to set for the target (influences the filename, path).
+ * Any non-Maildir/File flags are ignored.
+ * @param new_name whether to change the basename of the file
+ *
+ * @return Full path name of the target file or an Error
+ */
+Result<std::string>
+maildir_determine_target(const std::string& old_path,
+ const std::string& root_maildir_path,
+ const std::string& target_maildir,
+ Flags newflags,
+ bool new_name);
+
+} // namespace Mu
+
+#endif /*MU_MAILDIR_HH__*/
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-query-macros.hh"
+
+#include <glib.h>
+#include <unordered_map>
+
+#include "utils/mu-utils.hh"
+
+using namespace Mu;
+
+constexpr auto MU_BOOKMARK_GROUP = "mu";
+
+struct QueryMacros::Private {
+ Private(const Config& conf): conf_{conf} {}
+
+
+ Result<void> import_key_file(GKeyFile *kfile);
+
+ const Config& conf_;
+ std::unordered_map<std::string, std::string> macros_{};
+};
+
+Result<void>
+QueryMacros::Private::import_key_file(GKeyFile *kfile)
+{
+ if (!kfile)
+ return Err(Error::Code::InvalidArgument, "invalid key-file");
+
+ GError *err{};
+ size_t num{};
+ gchar **keys{g_key_file_get_keys(kfile, MU_BOOKMARK_GROUP, &num, &err)};
+ if (!keys)
+ return Err(Error::Code::File, &err/*cons*/,"failed to read keys");
+
+ for (auto key = keys; key && *key; ++key) {
+
+ auto rawval{g_key_file_get_string(kfile, MU_BOOKMARK_GROUP, *key, &err)};
+ if (!rawval) {
+ g_strfreev(keys);
+ return Err(Error::Code::File, &err/*cons*/,"failed to read key '{}'", *key);
+ }
+
+ auto val{to_string_gchar(std::move(rawval))};
+ macros_.erase(val); // we want to replace
+ macros_.emplace(std::string(*key), std::move(val));
+ ++num;
+ }
+
+ g_strfreev(keys);
+ mu_debug("imported {} query macro(s); total {}", num, macros_.size());
+ return Ok();
+}
+
+QueryMacros::QueryMacros(const Config& conf):
+ priv_{std::make_unique<Private>(conf)} {}
+
+QueryMacros::~QueryMacros() = default;
+
+Result<void>
+QueryMacros::load_bookmarks(const std::string& path)
+{
+ GError *err{};
+ GKeyFile *kfile{g_key_file_new()};
+ if (!g_key_file_load_from_file(kfile, path.c_str(), G_KEY_FILE_NONE, &err)) {
+ g_key_file_unref(kfile);
+ return Err(Error::Code::File, &err/*cons*/,
+ "failed to read bookmarks from {}", path);
+ }
+
+ auto&& res = priv_->import_key_file(kfile);
+ g_key_file_unref(kfile);
+
+ return res;
+}
+
+Option<std::string>
+QueryMacros::find_macro(const std::string& name) const
+{
+ if (const auto it{priv_->macros_.find(name)}; it != priv_->macros_.end())
+ return it->second;
+ else
+ return Nothing;
+}
+
+
+#ifdef BUILD_TESTS
+/*
+ * Tests.
+ *
+ */
+
+#include "utils/mu-test-utils.hh"
+#include "utils/mu-utils-file.hh"
+
+static void
+test_bookmarks()
+{
+ MemDb db;
+ Config conf_db{db};
+ QueryMacros qm{conf_db};
+
+ TempDir tdir{};
+ const auto bmfile{join_paths(tdir.path(), "bookmarks.ini")};
+ std::ofstream os{bmfile};
+
+ mu_println(os, "# test\n"
+ "[mu]\n"
+ "foo=subject:bar");
+ os.close();
+
+ auto res = qm.load_bookmarks(bmfile);
+ assert_valid_result(res);
+
+ assert_equal(qm.find_macro("foo").value_or(""), "subject:bar");
+ assert_equal(qm.find_macro("bar").value_or("nope"), "nope");
+}
+
+
+static void
+test_bookmarks_fail()
+{
+
+ MemDb db;
+ Config conf_db{db};
+ QueryMacros qm{conf_db};
+
+ auto res = qm.load_bookmarks("/foo/bar/non-existent");
+ g_assert_false(!!res);
+}
+
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/query/macros/bookmarks", test_bookmarks);
+ g_test_add_func("/query/macros/bookmarks-fail", test_bookmarks_fail);
+
+ return g_test_run();
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+
+#ifndef MU_QUERY_MACROS_HH__
+#define MU_QUERY_MACROS_HH__
+
+#include <string>
+#include <memory>
+
+#include <utils/mu-result.hh>
+#include <utils/mu-option.hh>
+
+#include "mu-config.hh"
+
+namespace Mu {
+
+class QueryMacros{
+public:
+ /**
+ * Construct QueryMacros object
+ *
+ * @param conf config object ref
+ */
+ QueryMacros(const Config& conf);
+
+ /**
+ * DTOR
+ */
+ ~QueryMacros();
+
+ /**
+ * Read bookmarks (ie. macros) from a bookmark-file
+ *
+ * @param bookmarks_file path to the bookmarks file
+ *
+ * @return Ok or some error
+ */
+ Result<void> load_bookmarks(const std::string& bookmarks_file);
+
+
+ /**
+ * Find a macro (aka 'bookmark') by its name
+ *
+ * @param name the name of the bookmark
+ *
+ * @return the macro value or Nothing if not found
+ */
+ Option<std::string> find_macro(const std::string& name) const;
+
+private:
+ struct Private;
+ std::unique_ptr<Private> priv_;
+};
+
+
+} // namespace Mu
+
+#endif /* MU_QUERY_MACROS_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-query-match-deciders.hh"
+
+#include "mu-query-results.hh"
+#include "utils/mu-option.hh"
+
+using namespace Mu;
+
+
+// We use a MatchDecider to gather information about the matches, and decide
+// whether to include them in the results.
+//
+// Note that to include the "related" messages, we need _two_ queries; the first
+// one to get the initial matches (called the Leader-Query) and a Related-Query,
+// to get the Leader matches + all messages that have a thread-id seen in the
+// Leader matches.
+//
+// We use the MatchDecider to gather information and use it for both queries.
+
+struct MatchDecider : public Xapian::MatchDecider {
+ MatchDecider(QueryFlags qflags, DeciderInfo& info) : qflags_{qflags}, decider_info_{info} {}
+ /**
+ * Update the match structure with unreadable/duplicate flags
+ *
+ * @param doc a Xapian document.
+ *
+ * @return a new QueryMatch object
+ */
+ QueryMatch make_query_match(const Xapian::Document& doc) const
+ {
+ QueryMatch qm{};
+
+ auto msgid{opt_string(doc, Field::Id::MessageId)
+ .value_or(*opt_string(doc, Field::Id::Path))};
+ if (!decider_info_.message_ids.emplace(std::move(msgid)).second)
+ qm.flags |= QueryMatch::Flags::Duplicate;
+
+ const auto path{opt_string(doc, Field::Id::Path)};
+ if (!path || ::access(path->c_str(), R_OK) != 0)
+ qm.flags |= QueryMatch::Flags::Unreadable;
+
+ return qm;
+ }
+
+ /**
+ * Should this message be included in the results?
+ *
+ * @param qm a query match
+ *
+ * @return true or false
+ */
+ bool should_include(const QueryMatch& qm) const
+ {
+ if (any_of(qflags_ & QueryFlags::SkipDuplicates) &&
+ any_of(qm.flags & QueryMatch::Flags::Duplicate))
+ return false;
+
+ if (any_of(qflags_ & QueryFlags::SkipUnreadable) &&
+ any_of(qm.flags & QueryMatch::Flags::Unreadable))
+ return false;
+
+ return true;
+ }
+ /**
+ * Gather thread ids from this match.
+ *
+ * @param doc the document (message)
+ *
+ */
+ void gather_thread_ids(const Xapian::Document& doc) const
+ {
+ auto thread_id{opt_string(doc, Field::Id::ThreadId)};
+ if (thread_id)
+ decider_info_.thread_ids.emplace(std::move(*thread_id));
+ }
+
+protected:
+ const QueryFlags qflags_;
+ DeciderInfo& decider_info_;
+
+private:
+ Option<std::string> opt_string(const Xapian::Document& doc, Field::Id id) const noexcept {
+ const auto value_no{field_from_id(id).value_no()};
+ std::string val = xapian_try([&] { return doc.get_value(value_no); }, std::string{""});
+ if (val.empty())
+ return Nothing;
+ else
+ return Some(std::move(val));
+ }
+};
+
+struct MatchDeciderLeader final : public MatchDecider {
+ MatchDeciderLeader(QueryFlags qflags, DeciderInfo& info) : MatchDecider(qflags, info) {}
+ /**
+ * operator()
+ *
+ * This receives the documents considered during a Xapian query, and
+ * is to return either true (keep) or false (ignore)
+ *
+ * We use this to potentiallly avoid certain messages (documents):
+ * - with QueryFlags::SkipUnreadable this will return false for message
+ * that are not readable in the file-system
+ * - with QueryFlags::SkipDuplicates this will return false for messages
+ * whose message-id was seen before.
+ *
+ * Even if we do not skip these messages entirely, we remember whether
+ * they were unreadable/duplicate (in the QueryMatch::Flags), so we can
+ * quickly find that info when doing the second 'related' query.
+ *
+ * The "leader" query. Matches here get the Leader flag unless they are
+ * duplicates / unreadable. We check the duplicate/readable status
+ * regardless of whether SkipDuplicates/SkipUnreadable was passed
+ * (to gather that information); however those flags
+ * affect our true/false verdict.
+ *
+ * @param doc xapian document
+ *
+ * @return true or false
+ */
+ bool operator()(const Xapian::Document& doc) const override {
+ // by definition, we haven't seen the docid before,
+ // so no need to search
+ auto it = decider_info_.matches.emplace(doc.get_docid(), make_query_match(doc));
+ it.first->second.flags |= QueryMatch::Flags::Leader;
+
+ return should_include(it.first->second);
+ }
+};
+
+std::unique_ptr<Xapian::MatchDecider>
+Mu::make_leader_decider(QueryFlags qflags, DeciderInfo& info)
+{
+ return std::make_unique<MatchDeciderLeader>(qflags, info);
+}
+
+struct MatchDeciderRelated final : public MatchDecider {
+ MatchDeciderRelated(QueryFlags qflags, DeciderInfo& info) : MatchDecider(qflags, info) {}
+ /**
+ * operator()
+ *
+ * This receives the documents considered during a Xapian query, and
+ * is to return either true (keep) or false (ignore)
+ *
+ * We use this to potentially avoid certain messages (documents):
+ * - with QueryFlags::SkipUnreadable this will return false for message
+ * that are not readable in the file-system
+ * - with QueryFlags::SkipDuplicates this will return false for messages
+ * whose message-id was seen before.
+ *
+ * Unlike in the "leader" decider (scroll up), we don't need to remember
+ * messages we won't include.
+ *
+ * @param doc xapian document
+ *
+ * @return true or false
+ */
+ bool operator()(const Xapian::Document& doc) const override {
+ // we may have seen this match in the "Leader" query.
+ const auto it = decider_info_.matches.find(doc.get_docid());
+ if (it != decider_info_.matches.end())
+ return should_include(it->second);
+
+ auto qm{make_query_match(doc)};
+ if (should_include(qm)) {
+ qm.flags |= QueryMatch::Flags::Related;
+ decider_info_.matches.emplace(doc.get_docid(), std::move(qm));
+ return true;
+ } else
+ return false; // nope.
+ }
+};
+
+std::unique_ptr<Xapian::MatchDecider>
+Mu::make_related_decider(QueryFlags qflags, DeciderInfo& info)
+{
+ return std::make_unique<MatchDeciderRelated>(qflags, info);
+}
+
+struct MatchDeciderThread final : public MatchDecider {
+ MatchDeciderThread(QueryFlags qflags, DeciderInfo& info) : MatchDecider{qflags, info} {}
+ /**
+ * operator()
+ *
+ * This receives the documents considered during a Xapian query, and
+ * is to return either true (keep) or false (ignore)
+ *
+ * Only include documents that earlier checks have decided to include.
+ *
+ * @param doc xapian document
+ *
+ * @return true or false
+ */
+ bool operator()(const Xapian::Document& doc) const override {
+ // we may have seen this match in the "Leader" query,
+ // or in the second (unbuounded) related query;
+ const auto it{decider_info_.matches.find(doc.get_docid())};
+ return it != decider_info_.matches.end() && !it->second.thread_path.empty();
+ }
+};
+
+std::unique_ptr<Xapian::MatchDecider>
+Mu::make_thread_decider(QueryFlags qflags, DeciderInfo& info)
+{
+ return std::make_unique<MatchDeciderThread>(qflags, info);
+}
--- /dev/null
+/*
+** Copyright (C) 2021-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_QUERY_MATCH_DECIDERS_HH__
+#define MU_QUERY_MATCH_DECIDERS_HH__
+
+#include <unordered_set>
+#include <unordered_map>
+#include <memory>
+
+#include "mu-xapian-db.hh"
+
+#include "mu-query-results.hh"
+
+namespace Mu {
+using StringSet = std::unordered_set<std::string>;
+
+struct DeciderInfo {
+ QueryMatches matches;
+ StringSet thread_ids;
+ StringSet message_ids;
+};
+
+/**
+ * Make a "leader" decider, that is, a MatchDecider for either a singular or the
+ * first query in the leader/related pair of queries. Gather information for
+ * threading, and the subsequent "related" query.
+ *
+ * @param qflags query flags
+ * @param match_info receives information about the matches.
+ *
+ * @return a unique_ptr to a match decider.
+ */
+std::unique_ptr<Xapian::MatchDecider> make_leader_decider(QueryFlags qflags, DeciderInfo& info);
+
+/**
+ * Make a "related" decider, that is, a MatchDecider for the second query
+ * in the leader/related pair of queries.
+ *
+ * @param qflags query flags
+ * @param match_info receives information about the matches.
+ *
+ * @return a unique_ptr to a match decider.
+ */
+std::unique_ptr<Xapian::MatchDecider> make_related_decider(QueryFlags qflags, DeciderInfo& info);
+
+/**
+ * Make a "thread" decider, that is, a MatchDecider that removes all but the
+ * document excepts for the ones found during initial/related searches.
+ *
+ * @param qflags query flags
+ * @param match_info receives information about the matches.
+ *
+ * @return a unique_ptr to a match decider.
+ */
+std::unique_ptr<Xapian::MatchDecider> make_thread_decider(QueryFlags qflags, DeciderInfo& info);
+
+} // namespace Mu
+
+#endif /* MU_QUERY_MATCH_DECIDERS_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-query-parser.hh"
+
+#include <string_view>
+#include <variant>
+#include <type_traits>
+#include <iostream>
+
+#include "utils/mu-utils.hh"
+#include "utils/mu-sexp.hh"
+#include "utils/mu-option.hh"
+#include <glib.h>
+#include "utils/mu-utils-file.hh"
+
+using namespace Mu;
+
+// Sexp extensions...
+static Sexp&
+prepend(Sexp& s, Sexp&& e)
+{
+ s.list().insert(s.list().begin(), std::move(e));
+ return s;
+}
+
+static Option<Sexp&>
+second(Sexp& s)
+{
+ if (s.listp() && !s.empty() && s.cbegin() + 1 != s.cend())
+ return *(s.begin()+1);
+ else
+ return Nothing;
+}
+
+
+static bool
+looks_like_matcher(const Sexp& sexp)
+{
+ // all the "terminal values" (from the Mu parser's pov)
+ const std::array<Sexp::Symbol, 5> value_syms = {
+ placeholder_sym, phrase_sym, regex_sym, range_sym, wildcard_sym
+ };
+
+ if (!sexp.listp() || sexp.empty() || !sexp.front().symbolp())
+ return false;
+
+ const auto symbol{sexp.front().symbol()};
+ if (seq_some(value_syms, [&](auto &&sym) { return symbol == sym; }))
+ return true;
+ else if (!!field_from_name(symbol.name) || field_is_combi(symbol.name))
+ return true;
+ else
+ return false;
+}
+
+struct ParseContext {
+ bool expand;
+ std::vector<std::string> warnings;
+};
+
+
+
+
+/**
+ * Indexable fields become _phrase_ fields if they contain
+ * wordbreakable data;
+ *
+ * @param field
+ * @param val
+ *
+ * @return
+ */
+static Option<Sexp>
+phrasify(const Field& field, const Sexp& val)
+{
+ if (!field.is_phrasable_term() || !val.stringp())
+ return Nothing; // nothing to phrasify
+
+ auto words{utf8_wordbreak(val.string())};
+ if (words.find(' ') == std::string::npos)
+ return Nothing; // nothing to phrasify
+
+ auto phrase = Sexp {
+ Sexp::Symbol{field.name},
+ Sexp{phrase_sym, Sexp{std::move(words)}}};
+
+ // if the field both a normal term & phrasable, match both
+ // if they are different
+ if (val.string() != words)
+ return Sexp{or_sym,
+ Sexp {Sexp::Symbol{field.name}, Sexp(val.string())},
+ std::move(phrase)};
+ else
+ return phrase;
+}
+
+
+/*
+ * Grammar
+ *
+ * query -> factor { (<OR> | <XOR>) factor }
+ * factor -> unit { [<AND>] unit }
+ * unit -> matcher | <NOT> query | <(> query <)>
+ * matcher
+ */
+
+static Sexp query(Sexp& tokens, ParseContext& ctx);
+
+
+static Sexp
+matcher(Sexp& tokens, ParseContext& ctx)
+{
+ if (tokens.empty())
+ return {};
+
+ auto val{*tokens.head()};
+ tokens.pop_front();
+ /* special case: if we find some non-matcher type here, we need to second-guess the token */
+ if (!looks_like_matcher(val))
+ val = Sexp{placeholder_sym, val.symbol().name};
+
+ const auto fieldsym{val.front().symbol()};
+
+ // Note the _expand_ case is what we use when processing the query 'for real';
+ // the non-expand case is only to have a bit more human-readable Sexp for use
+ // mu find's '--analyze'
+ //
+ // Re: phrase-fields We map something like 'subject:hello-world'
+ // to
+ // (or (subject "hello-world" (subject (phrase "hello world"))))
+
+ if (ctx.expand) { /* should we expand meta-fields? */
+ auto fields = fields_from_name(fieldsym == placeholder_sym ? "" : fieldsym.name);
+ if (!fields.empty()) {
+ Sexp vals{};
+ vals.add(or_sym);
+ for (auto&& field: fields)
+ if (auto&& phrase{phrasify(field, *second(val))}; phrase)
+ vals.add(std::move(*phrase));
+ else
+ vals.add(Sexp{Sexp::Symbol{field.name}, Sexp{*second(val)}});
+ val = std::move(vals);
+ }
+
+ }
+
+ if (auto&& field{field_from_name(fieldsym.name)}; field) {
+ if (auto&& phrase(phrasify(*field, *second(val))); phrase)
+ val = std::move(*phrase);
+ }
+
+ return val;
+}
+
+static Sexp
+unit(Sexp& tokens, ParseContext& ctx)
+{
+ if (tokens.head_symbolp(not_sym)) { /* NOT */
+ tokens.pop_front();
+ Sexp sub{unit(tokens, ctx)};
+
+ /* special case: interpret "not" as a matcher instead; */
+ if (sub.empty())
+ return matcher(prepend(tokens, Sexp{placeholder_sym, not_sym.name}), ctx);
+
+ /* we try to optimize: double negations are removed */
+ if (sub.head_symbolp(not_sym))
+ return *second(sub);
+ else
+ return Sexp(not_sym, std::move(sub));
+
+ } else if (tokens.head_symbolp(open_sym)) { /* ( sub) */
+ tokens.pop_front();
+ Sexp sub{query(tokens, ctx)};
+ if (tokens.head_symbolp(close_sym))
+ tokens.pop_front();
+ else {
+ //g_warning("expected <)>");
+ }
+ return sub;
+ }
+
+ /* matcher */
+ return matcher(tokens, ctx);
+}
+
+
+static Sexp
+factor(Sexp& tokens, ParseContext& ctx)
+{
+ Sexp un = unit(tokens, ctx);
+
+ /* query 'a b' is to be interpreted as 'a AND b';
+ *
+ * we need an implicit AND if the head symbol is either
+ * a matcher (value) or the start of a sub-expression */
+ auto implicit_and = [&]() {
+ if (tokens.head_symbolp(open_sym))
+ return true;
+ else if (tokens.head_symbolp(not_sym)) // turn a lone 'not' -> 'and not'
+ return true;
+ else if (auto&& head{tokens.head()}; head)
+ return looks_like_matcher(*head);
+ else
+ return false;
+ };
+
+ Sexp uns;
+ while (true) {
+ if (tokens.head_symbolp(and_sym))
+ tokens.pop_front();
+ else if (!implicit_and())
+ break;
+
+ if (auto&& un2 = unit(tokens, ctx); !un2.empty())
+ uns.add(std::move(un2));
+ else
+ break;
+ }
+
+ if (!uns.empty()) {
+ un = Sexp{and_sym, std::move(un)};
+ un.add_list(std::move(uns));
+ }
+
+ return un;
+}
+
+static Sexp
+query(Sexp& tokens, ParseContext& ctx)
+{
+ /* note: we flatten (or (or ( or ...)) etc. here;
+ * for optimization (since Xapian likes flat trees) */
+
+ Sexp fact = factor(tokens, ctx);
+ Sexp or_factors, xor_factors;
+ while (true) {
+ auto factors = std::invoke([&]()->Option<Sexp&> {
+
+ if (tokens.head_symbolp(or_sym))
+ return or_factors;
+ else if (tokens.head_symbolp(xor_sym))
+ return xor_factors;
+ else
+ return Nothing;
+ });
+
+ if (!factors)
+ break;
+
+ tokens.pop_front();
+ factors->add(factor(tokens, ctx));
+ }
+
+ // a bit clumsy...
+
+ if (!or_factors.empty() && xor_factors.empty()) {
+ fact = Sexp{or_sym, std::move(fact)};
+ fact.add_list(std::move(or_factors));
+ } else if (or_factors.empty() && !xor_factors.empty()) {
+ fact = Sexp{xor_sym, std::move(fact)};
+ fact.add_list(std::move(xor_factors));
+ } else if (!or_factors.empty() && !xor_factors.empty()) {
+ fact = Sexp{or_sym, std::move(fact)};
+ fact.add_list(std::move(or_factors));
+ prepend(xor_factors, xor_sym);
+ fact.add(std::move(xor_factors));
+ }
+
+ return fact;
+}
+
+Sexp
+Mu::parse_query(const std::string& expr, bool expand)
+{
+ ParseContext context;
+ context.expand = expand;
+
+ if (auto&& items = process_query(expr); !items.listp())
+ throw std::runtime_error("tokens must be a list-sexp");
+ else
+ return query(items, context);
+}
+
+
+#if defined(BUILD_PARSE_QUERY)||defined(BUILD_PARSE_QUERY_EXPAND)
+int
+main (int argc, char *argv[])
+{
+ if (argc < 2) {
+ mu_printerrln("expected: {} <query>", argv[0]);
+ return 1;
+ }
+
+ std::string expr;
+ for (auto i = 1; i < argc; ++i) {
+ expr += argv[i];
+ expr += " ";
+ }
+
+ auto&& sexp = parse_query(expr,
+#ifdef BUILD_PARSE_QUERY_EXPAND
+ true/*expand*/
+#else
+ false/*don't expand*/
+#endif
+ );
+ mu_println("{}", sexp.to_string());
+ return 0;
+}
+#endif // BUILD_PARSE_QUERY || BUILD_PARSE_QUERY_EXPAND
+
+
+
+#if BUILD_TESTS
+/*
+ * Tests.
+ *
+ */
+
+#include "utils/mu-test-utils.hh"
+
+using TestCase = std::pair<std::string, std::string>;
+
+static void
+test_parser_basic()
+{
+ std::vector<TestCase> cases = {
+ // single term
+ TestCase{R"(a)", R"((_ "a"))"},
+ // a and b
+ TestCase{R"(a and b)", R"((and (_ "a") (_ "b")))"},
+ // a and b and c
+ TestCase{R"(a and b and c)", R"((and (_ "a") (_ "b") (_ "c")))"},
+ // a or b
+ TestCase{R"(a or b)", R"((or (_ "a") (_ "b")))"},
+ // a or b and c
+ TestCase{R"(a or b and c)", R"((or (_ "a") (and (_ "b") (_ "c"))))"},
+ // a and b or c
+ TestCase{R"(a and b or c)", R"((or (and (_ "a") (_ "b")) (_ "c")))"},
+ // not a
+ TestCase{R"(not a)", R"((not (_ "a")))"},
+ // lone not
+ TestCase{R"(not)", R"((_ "not"))"},
+ // a and (b or c)
+ TestCase{R"(a and (b or c))", R"((and (_ "a") (or (_ "b") (_ "c"))))"},
+ // not a and not b
+ TestCase{R"(not a and b)", R"((and (not (_ "a")) (_ "b")))"},
+ // a not b
+ TestCase{R"(a not b)", R"((and (_ "a") (not (_ "b"))))"},
+ };
+
+ for (auto&& test: cases) {
+ auto&& sexp{parse_query(test.first)};
+ //mu_message ("'{}' <=> '{}'", sexp.to_string(), test.second);
+ assert_equal(sexp.to_string(), test.second);
+ }
+}
+
+static void
+test_parser_recover()
+{
+ std::vector<TestCase> cases = {
+ // implicit AND
+ TestCase{R"(a b)", R"((and (_ "a") (_ "b")))"},
+ // a or or (second to be used as value)
+ TestCase{R"(a or and)", R"((or (_ "a") (_ "and")))"},
+ // missing end )
+ TestCase{R"(a and ()", R"((_ "a"))"},
+ // missing end )
+ TestCase{R"(a and (b)", R"((and (_ "a") (_ "b")))"},
+ };
+
+ for (auto&& test: cases) {
+ auto&& sexp{parse_query(test.first)};
+ assert_equal(sexp.to_string(), test.second);
+ }
+}
+
+
+static void
+test_parser_fields()
+{
+ std::vector<TestCase> cases = {
+ // simple field
+ TestCase{R"(s:hello)", R"((subject "hello"))"},
+ // field, wildcard, regexp
+ TestCase{R"(subject:a* recip:/b/)",
+ R"((and (subject (wildcard "a")) (recip (regex "b"))))"},
+ TestCase{R"(from:hello or subject:world)",
+ R"((or (from "hello") (subject "world")))"},
+ };
+
+ for (auto&& test: cases) {
+ auto&& sexp{parse_query(test.first)};
+ assert_equal(sexp.to_string(), test.second);
+ }
+}
+
+static void
+test_parser_expand()
+{
+ std::vector<TestCase> cases = {
+ // simple field
+ TestCase{R"(recip:a)", R"((or (to "a") (cc "a") (bcc "a")))"},
+ // field, wildcard, regexp
+ TestCase{R"(a*)",
+ R"((or (to (wildcard "a")) (cc (wildcard "a")) (bcc (wildcard "a")) (from (wildcard "a")) (subject (wildcard "a")) (body (wildcard "a")) (embed (wildcard "a"))))"},
+ TestCase{R"(a xor contact:b)",
+ R"((xor (or (to "a") (cc "a") (bcc "a") (from "a") (subject "a") (body "a") (embed "a")) (or (to "b") (cc "b") (bcc "b") (from "b"))))"}
+ };
+
+ for (auto&& test: cases) {
+ auto&& sexp{parse_query(test.first, true/*expand*/)};
+ assert_equal(sexp.to_string(), test.second);
+ }
+}
+
+
+static void
+test_parser_range()
+{
+ std::vector<TestCase> cases = {
+ TestCase{R"(size:1)", R"((size (range "1" "1")))"},
+ TestCase{R"(size:2..)", R"((size (range "2" "")))"},
+ TestCase{R"(size:..1k)", R"((size (range "" "1024")))"},
+ TestCase{R"(size:..)", R"((size (range "" "")))"},
+ };
+
+ for (auto&& test: cases) {
+ auto&& sexp{parse_query(test.first, true/*expand*/)};
+ assert_equal(sexp.to_string(), test.second);
+ }
+}
+
+static void
+test_parser_optimize()
+{
+ std::vector<TestCase> cases = {
+ TestCase{R"(not a)", R"((not (_ "a")))"},
+ TestCase{R"(not not a)", R"((_ "a"))"},
+ TestCase{R"(not not not a)", R"((not (_ "a")))"},
+ TestCase{R"(not not not not a)", R"((_ "a"))"},
+ };
+
+
+ for (auto&& test: cases) {
+ auto&& sexp{parse_query(test.first)};
+ assert_equal(sexp.to_string(), test.second);
+ }
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/query-parser/basic", test_parser_basic);
+ g_test_add_func("/query-parser/recover", test_parser_recover);
+ g_test_add_func("/query-parser/fields", test_parser_fields);
+ g_test_add_func("/query-parser/range", test_parser_range);
+ g_test_add_func("/query-parser/expand", test_parser_expand);
+ g_test_add_func("/query-parser/optimize", test_parser_optimize);
+
+ return g_test_run();
+}
+
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2023-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include <string>
+
+#include "mu-xapian-db.hh"
+
+#include "utils/mu-sexp.hh"
+#include "utils/mu-result.hh"
+#include "mu-store.hh"
+
+namespace Mu {
+/*
+ * Some useful symbol-sexps
+ */
+static inline const auto placeholder_sym = "_"_sym;
+static inline const auto phrase_sym = "phrase"_sym;
+static inline const auto regex_sym = "regex"_sym;
+static inline const auto range_sym = "range"_sym;
+static inline const auto wildcard_sym = "wildcard"_sym;
+
+static inline const auto open_sym = "("_sym;
+static inline const auto close_sym = ")"_sym;
+
+static inline const auto and_sym = "and"_sym;
+static inline const auto or_sym = "or"_sym;
+static inline const auto xor_sym = "xor"_sym;
+static inline const auto not_sym = "not"_sym;
+static inline const auto and_not_sym = "and-not"_sym;
+
+
+/*
+ * We take a query, then parse it into a human-readable s-expression and then
+ * turn that s-expression into a Xapian query
+ *
+ * some query:
+ * "from:hello or subject:world"
+ *
+ * 1. tokenize-query
+ * => ((from "hello") or (subject "world"))
+ *
+ * 2. parse-query
+ * => (or (from "hello") (subject "world"))
+ *
+ * 3. xapian-query
+ * => Query((Fhello OR Sworld))
+ * *
+ */
+
+/**
+ * Analyze the query expression and express it as a Sexp-list with the sequence
+ * of elements.
+ *
+ * @param expr a search expression
+ *
+ * @return Sexp with the sequence of elements
+ */
+Sexp process_query(const std::string& expr);
+
+/**
+ * Parse the query expression and create a parse-tree expressed as an Sexp
+ * object (tree).
+ *
+ * Internally, this processes the stream into element (see process_query()) and
+ * processes the tokens into a Sexp. This sexp is meant to be human-readable.
+ *
+ * @param expr a search expression
+ * @param expand whether to expand meta-fields (such as '_', 'recip', 'contacts')
+ *
+ * @return Sexp with the parse tree
+ */
+Sexp parse_query(const std::string& expr, bool expand=false);
+
+/**
+ * Make a Xapian Query for the given string expression.
+ *
+ * This uses parse_query() and turns the S-expression into a Xapian::Query.
+ * Unlike mere parsing, this uses the information in the store to resolve
+ * wildcard / regex queries.
+ *
+ * @param store the message store
+ * @param expr a string expression
+ * @param flavor type of parser to use
+ *
+ * @return a Xapian query result or an error.
+ */
+enum struct ParserFlags {
+ None = 0 << 0,
+ SupportNgrams = 1 << 0, /**< Support Xapian's Ngrams for CJK etc. handling */
+ XapianParser = 1 << 1, /**< For testing only, use Xapian's
+ * built-in QueryParser; this is not
+ * fully compatible with mu, only useful
+ * for debugging. */
+};
+Result<Xapian::Query> make_xapian_query(const Store& store, const std::string& expr,
+ ParserFlags flag=ParserFlags::None) noexcept;
+
+MU_ENABLE_BITOPS(ParserFlags);
+} // namespace Mu
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-query-parser.hh"
+
+#include <string_view>
+#include <variant>
+#include <type_traits>
+#include <iostream>
+
+#include "utils/mu-option.hh"
+#include <glib.h>
+#include "utils/mu-utils-file.hh"
+
+using namespace Mu;
+
+/**
+ * An 'Element' here is a rather rich version of what is traditionally
+ * considered a (lexical) token.
+ *
+ * We try to determine as much as possible during the analysis phase; which is
+ * quite a bit (given the fairly simple query language), and the parsing phase
+ * only has to deal with the putting these elements in a tree.
+ *
+ * During analysis:
+ * 1) separate the query into a sequence strings
+ * 2) for each of these strings
+ * - Does it look like an Op? ('or', 'and' etc.) --> Op
+ * - Otherwise: treat as a Basic field ([field]:value)
+ * - Whitespace in value? -> promote to Phrase
+ * - otherwise:
+ * - Is value a regex (in /<regex>/) -> promote to Regex
+ * - Is value a wildcard (ends in '*') -> promote to Wildcard
+ * - is value a range (a..b) -> promote to Range
+ *
+ * After analysis, we have the sequence of element as a Sexp, which can then be
+ * fed to the parser. We attempt to make the Sexp as human-readable as possible.
+ */
+struct Element {
+ enum struct Bracket { Open, Close} ;
+ enum struct Op { And, Or, Xor, Not, AndNot };
+
+ template<typename ValueType>
+ struct FieldValue {
+ FieldValue(const ValueType& v): field{}, value{v}{}
+
+ template<typename StringType>
+ FieldValue(const StringType& fname, const ValueType& v):
+ field{std::string{fname}}, value{v}{}
+ template<typename StringType>
+ FieldValue(const Option<StringType>& fname, const ValueType& v) {
+ if (fname)
+ field = std::string{*fname};
+ value = v;
+ }
+
+ Option<std::string> field{};
+ ValueType value{};
+ };
+ struct Basic: public FieldValue<std::string> {using FieldValue::FieldValue;};
+ struct Regex: public FieldValue<std::string> {using FieldValue::FieldValue;};
+ struct Wildcard: public FieldValue<std::string> {using FieldValue::FieldValue;};
+ struct Range: public FieldValue<std::pair<std::string, std::string>> {
+ using FieldValue::FieldValue; };
+
+ using ValueType = std::variant<
+ /* */
+ Bracket,
+ /* op */
+ Op,
+ /* string values */
+ std::string,
+ /* value types */
+ Basic,
+ Regex,
+ Wildcard,
+ Range
+ >;
+
+ // helper
+ template <typename T, typename U>
+ struct decay_equiv:
+ std::is_same<typename std::decay<T>::type, U>::type {};
+
+ Element(Bracket b): value{b} {}
+ Element(Op op): value{op} {}
+
+ template<typename T,
+ typename std::enable_if<std::is_base_of<class FieldValue<T>, T>::value>::type = 0>
+ Element(const std::string& field, const T& val): value{T{field, val}} {}
+
+ Element(const std::string& val): value{val} {}
+
+ template<typename T>
+ Option<T&> get_opt() {
+ if (std::holds_alternative<T>(value))
+ return std::get<T>(value);
+ else
+ return Nothing;
+ }
+
+ Sexp sexp() const {
+ return std::visit([](auto&& arg)->Sexp {
+
+ auto field_sym = [](const Option<std::string>& field) {
+ return field ? Sexp::Symbol{*field} : placeholder_sym;
+ };
+
+ using T = std::decay_t<decltype(arg)>;
+
+ if constexpr (std::is_same_v<T, Bracket>) {
+ switch(arg) {
+ case Bracket::Open:
+ return open_sym;
+ case Bracket::Close:
+ return close_sym;
+ default:
+ throw std::logic_error("invalid bracket type");
+ }
+ } else if constexpr (std::is_same_v<T, Op>) {
+ switch(arg) {
+ case Op::And:
+ return and_sym;
+ case Op::Or:
+ return or_sym;
+ case Op::Xor:
+ return xor_sym;
+ case Op::Not:
+ return not_sym;
+ case Op::AndNot:
+ return and_not_sym;
+ default:
+ throw std::logic_error("invalid op type");
+ }
+ } else if constexpr (std::is_same_v<T, Basic>) {
+ return Sexp { field_sym(arg.field), arg.value };
+ } else if constexpr (std::is_same_v<T, Regex>) {
+ return Sexp { field_sym(arg.field), Sexp{ regex_sym, arg.value}};
+ } else if constexpr (std::is_same_v<T, Wildcard>) {
+ return Sexp { field_sym(arg.field), Sexp{ wildcard_sym, arg.value}};
+ } else if constexpr (std::is_same_v<T, Range>) {
+ return Sexp {field_sym(arg.field),
+ Sexp{ range_sym, arg.value.first, arg.value.second }};
+ } else if constexpr (std::is_same_v<T, std::string>) {
+ throw std::logic_error("no bare strings should be here");
+ } else
+ throw std::logic_error("uninvited visitor");
+ }, value);
+ }
+
+ ValueType value;
+};
+
+using Elements = std::vector<Element>;
+
+
+
+/**
+ * Remove first character from string and return it.
+ *
+ * @param[in,out] str a string
+ * @param[in,out] pos position in _original_ string
+ *
+ * @return a char or 0 if there is none.
+ */
+static char
+read_char(std::string& str, size_t& pos)
+{
+ if (str.empty())
+ return {};
+
+ auto kar{str.at(0)};
+ str.erase(0, 1);
+ ++pos;
+
+ return kar;
+}
+
+/**
+ * Restore kar at the beginning of the string
+ *
+ * @param[in,out] str a string
+ * @param[in,out] pos position in _original_ string
+ * @param kar a character
+ */
+static void
+unread_char(std::string& str, size_t& pos, char kar)
+{
+ str = kar + str;
+ --pos;
+}
+
+
+/**
+ * Remove the the next element from the string and return it
+ *
+ * @param[in,out] str a string
+ * @param[in,out] pos position in _original_ string *
+ *
+ * @return an Element or Nothing
+ */
+static Option<Element>
+next_element(std::string& str, size_t& pos)
+{
+ bool quoted{}, escaped{};
+ std::string value{};
+
+ auto is_separator = [](char c) { return c == ' '|| c == '(' || c == ')'; };
+
+ while (!str.empty()) {
+
+ auto kar = read_char(str, pos);
+
+ if (kar == '\\') {
+ escaped = !escaped;
+ if (escaped)
+ continue;
+ }
+
+ if (kar == '"' && !escaped) {
+ if (!escaped && quoted)
+ return Element{value};
+ else {
+ quoted = true;
+ continue;
+ }
+ }
+
+ if (!quoted && !escaped && is_separator(kar)) {
+ if (!value.empty()) {
+ unread_char(str, pos, kar);
+ return Element{value};
+ }
+
+ if (quoted || kar == ' ')
+ continue;
+
+ switch (kar) {
+ case '(':
+ return Element{Element::Bracket::Open};
+ case ')':
+ return Element{Element::Bracket::Close};
+ default:
+ break;
+ }
+ }
+
+ value += kar;
+ escaped = false;
+ }
+
+ if (value.empty())
+ return Nothing;
+ else
+ return Element{value};
+}
+
+
+static Option<Element>
+opify(Element&& element)
+{
+ auto&& str{element.get_opt<std::string>()};
+ if (!str)
+ return element;
+
+ static const std::unordered_map<std::string, Element::Op> ops = {
+ { "and", Element::Op::And },
+ { "or", Element::Op::Or},
+ { "xor", Element::Op::Xor },
+ { "not", Element::Op::Not },
+ // AndNot only appears during parsing.
+ };
+
+ if (auto&& it = ops.find(utf8_flatten(*str)); it != ops.end())
+ element.value = it->second;
+
+ return element;
+}
+
+static Option<Element>
+basify(Element&& element)
+{
+ auto&& str{element.get_opt<std::string>()};
+ if (!str)
+ return element;
+
+ const auto pos = str->find(':');
+ if (pos == std::string::npos) {
+ element.value = Element::Basic{*str};
+ return element;
+ }
+
+ const auto fname{str->substr(0, pos)};
+ if (auto&& field{field_from_name(fname)}; field) {
+ auto val{str->substr(pos + 1)};
+ if (field == Field::Id::Flags) {
+ if (auto&& finfo{flag_info(val)}; finfo)
+ element.value = Element::Basic{field->name,
+ std::string{finfo->name}};
+ else
+ element.value = Element::Basic{*str};
+ } else if (field == Field::Id::Priority) {
+ if (auto&& prio{priority_from_name(val)}; prio)
+ element.value = Element::Basic{field->name,
+ std::string{priority_name(*prio)}};
+ else
+ element.value = Element::Basic{*str};
+ } else
+ element.value = Element::Basic{std::string{field->name},
+ str->substr(pos + 1)};
+ } else if (field_is_combi(fname))
+ element.value = Element::Basic{fname, str->substr(pos +1)};
+ else
+ element.value = Element::Basic{*str};
+
+ return element;
+}
+
+static Option<Element>
+wildcardify(Element&& element)
+{
+ auto&& basic{element.get_opt<Element::Basic>()};
+ if (!basic)
+ return element;
+
+ auto&& val{basic->value};
+ if (val.size() < 2 || val[val.size()-1] != '*')
+ return element;
+
+ val.erase(val.size() - 1);
+ element.value = Element::Wildcard{basic->field, val};
+
+ return element;
+}
+
+static Option<Element>
+regexpify(Element&& element)
+{
+ auto&& str{element.get_opt<Element::Basic>()};
+ if (!str)
+ return element;
+
+ auto&& val{str->value};
+ if (val.size() < 3 || val[0] != '/' || val[val.size()-1] != '/')
+ return element;
+
+ val.erase(val.size() - 1);
+ val.erase(0, 1);
+ element.value = Element::Regex{str->field, std::move(val)};
+
+ return element;
+}
+
+// handle range-fields: Size, Date, Changed
+static Option<Element>
+rangify(Element&& element)
+{
+ auto&& str{element.get_opt<Element::Basic>()};
+ if (!str)
+ return element;
+
+ if (!str->field)
+ return element;
+
+ auto&& field = field_from_name(*str->field);
+ if (!field || !field->is_range())
+ return element;
+
+ /* yes: get the range */
+ auto&& range = std::invoke([&]()->std::pair<std::string, std::string> {
+ const auto val{str->value};
+ const auto pos{val.find("..")};
+
+ if (pos == std::string::npos)
+ return { val, val };
+ else
+ return {val.substr(0, pos), val.substr(pos + 2)};
+ });
+
+ if (field->id == Field::Id::Size) {
+ int64_t s1{range.first.empty() ? -1 :
+ parse_size(range.first, false/*first*/).value_or(-1)};
+ int64_t s2{range.second.empty() ? -1 :
+ parse_size(range.second, true/*last*/).value_or(-1)};
+ if (s2 >= 0 && s1 > s2)
+ std::swap(s1, s2);
+ element.value = Element::Range{str->field,
+ {s1 < 0 ? "" : std::to_string(s1),
+ s2 < 0 ? "" : std::to_string(s2)}};
+
+ } else if (field->id == Field::Id::Date || field->id == Field::Id::Changed) {
+ auto tstamp=[](auto&& str, auto&& first)->int64_t {
+ return str.empty() ? -1 :
+ parse_date_time(str, first ,false/*local*/).value_or(-1);
+ };
+ int64_t lower{tstamp(range.first, true/*lower*/)};
+ int64_t upper{tstamp(range.second, false/*upper*/)};
+ if (lower >= 0 && upper >= 0 && lower > upper) {
+ // can't simply swap due to rounding up/down
+ lower = tstamp(range.second, true/*lower*/);
+ upper = tstamp(range.first, false/*upper*/);
+ }
+ // use "Zulu" time.
+ element.value = Element::Range{
+ str->field,
+ {lower < 0 ? "" :
+ mu_format("{:%FT%TZ}",mu_time(lower, true/*utc*/)),
+ upper < 0 ? "" :
+ mu_format("{:%FT%TZ}", mu_time(upper, true/*utc*/))}};
+ }
+
+ return element;
+}
+
+static Elements
+process(const std::string& expr)
+{
+ Elements elements{};
+ size_t offset{0};
+
+ /* all control chars become SPC */
+ std::string str{expr};
+ for (auto& c: str)
+ c = ::iscntrl(c) ? ' ' : c;
+
+ while(!str.empty()) {
+ auto&& element = next_element(str, offset)
+ .and_then(opify)
+ .and_then(basify)
+ .and_then(regexpify)
+ .and_then(wildcardify)
+ .and_then(rangify);
+ if (element)
+ elements.emplace_back(std::move(element.value()));
+ }
+
+ return elements;
+}
+
+Sexp
+Mu::process_query(const std::string& expr)
+{
+ const auto& elements{::process(expr)};
+
+ Sexp sexp{};
+ for (auto&& elm: elements)
+ sexp.add(elm.sexp());
+
+ return sexp;
+}
+
+#ifdef BUILD_PROCESS_QUERY
+int
+main (int argc, char *argv[])
+{
+ if (argc < 2) {
+ mu_printerrln("expected: process-query <query>");
+ return 1;
+ }
+
+ std::string expr;
+ for (auto i = 1; i < argc; ++i) {
+ expr += argv[i];
+ expr += " ";
+ }
+
+ auto sexp = process_query(expr);
+ mu_println("{}", sexp.to_string());
+
+ return 0;
+}
+#endif /*BUILD_ANALYZE_QUERY*/
+
+#if BUILD_TESTS
+/*
+ *
+ * Tests.
+ *
+ */
+
+#include "utils/mu-test-utils.hh"
+
+using TestCase = std::pair<std::string, std::string>;
+
+static void
+test_processor()
+{
+ std::vector<TestCase> cases = {
+ // basics
+ TestCase{R"(hello world)", R"(((_ "hello") (_ "world")))"},
+ TestCase{R"(maildir:/"hello world")", R"(((maildir "/hello world")))"},
+ TestCase{R"(flag:deleted)", R"(((_ "flag:deleted")))"} // non-existing flags
+ };
+
+ for (auto&& test: cases) {
+ auto&& sexp{process_query(test.first)};
+ assert_equal(sexp.to_string(), test.second);
+ }
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/query-parser/processor", test_processor);
+
+ return g_test_run();
+}
+
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2022-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_QUERY_RESULTS_HH__
+#define MU_QUERY_RESULTS_HH__
+
+#include <algorithm>
+#include <limits>
+#include <stdexcept>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+#include <limits>
+#include <ostream>
+#include <cmath>
+#include <memory>
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <glib.h>
+
+#include <mu-xapian-db.hh>
+#include <utils/mu-utils.hh>
+#include <utils/mu-option.hh>
+
+#include <message/mu-message.hh>
+
+namespace Mu {
+
+/**
+ * This implements a QueryResults structure, which capture the results of a
+ * Xapian query, and a QueryResultsIterator, which gives C++-compliant iterator
+ * to go over the results. and finally QueryThreader (in query-threader.cc) which
+ * calculates the threads, using the JWZ algorithm.
+ */
+
+/// Flags that influence now matches are presented (or skipped)
+enum struct QueryFlags {
+ None = 0, /**< no flags */
+ Descending = 1 << 0, /**< sort z->a */
+ SkipUnreadable = 1 << 1, /**< skip unreadable msgs */
+ SkipDuplicates = 1 << 2, /**< skip duplicate msgs */
+ IncludeRelated = 1 << 3, /**< include related msgs */
+ Threading = 1 << 4, /**< calculate threading info */
+ // internal
+ Leader = 1 << 5, /**< This is the leader query (for internal use
+ * only)*/
+};
+MU_ENABLE_BITOPS(QueryFlags);
+
+/// Stores all the essential information for sorting the results.
+struct QueryMatch {
+ /// Flags for a match (message) found
+ enum struct Flags {
+ None = 0, /**< No Flags */
+ Leader = 1 << 0, /**< Mark direct matches as leader */
+ Related = 1 << 1, /**< A related message */
+ Unreadable = 1 << 2, /**< No readable file */
+ Duplicate = 1 << 3, /**< Message-id seen before */
+
+ Root = 1 << 10, /**< Is this the thread-root? */
+ First = 1 << 11, /**< Is this the first message in a thread? */
+ Last = 1 << 12, /**< Is this the last message in a thread? */
+ Orphan = 1 << 13, /**< Is this message without a parent? */
+ HasChild = 1 << 14, /**< Does this message have a child? */
+
+ ThreadSubject = 1 << 20, /**< Message holds subject for (sub)thread */
+ };
+
+ Flags flags{Flags::None}; /**< Flags */
+ std::string date_key; /**< The date-key (for sorting all sub-root levels) */
+ // the thread subject is the subject of the first message in a thread,
+ // and any message that has a different subject compared to its predecessor
+ // (ignoring prefixes such as Re:)
+ //
+ // otherwise, it is empty.
+ std::string subject; /**< subject for this message */
+ size_t thread_level{}; /**< The thread level */
+ std::string thread_path; /**< The hex-numerial path in the thread, ie. '00:01:0a' */
+ std::string thread_date; /**< date of newest message in thread */
+
+ bool operator<(const QueryMatch& rhs) const { return date_key < rhs.date_key; }
+
+ bool has_flag(Flags flag) const;
+};
+
+MU_ENABLE_BITOPS(QueryMatch::Flags);
+
+inline bool
+QueryMatch::has_flag(QueryMatch::Flags flag) const
+{
+ return any_of(flags & flag);
+}
+
+/* LCOV_EXCL_START */
+static inline std::ostream&
+operator<<(std::ostream& os, QueryMatch::Flags mflags)
+{
+ if (mflags == QueryMatch::Flags::None) {
+ os << "<none>";
+ return os;
+ }
+
+ if (any_of(mflags & QueryMatch::Flags::Leader))
+ os << "leader ";
+ if (any_of(mflags & QueryMatch::Flags::Unreadable))
+ os << "unreadable ";
+ if (any_of(mflags & QueryMatch::Flags::Duplicate))
+ os << "dup ";
+
+ if (any_of(mflags & QueryMatch::Flags::Root))
+ os << "root ";
+ if (any_of(mflags & QueryMatch::Flags::Related))
+ os << "related ";
+ if (any_of(mflags & QueryMatch::Flags::First))
+ os << "first ";
+ if (any_of(mflags & QueryMatch::Flags::Last))
+ os << "last ";
+ if (any_of(mflags & QueryMatch::Flags::Orphan))
+ os << "orphan ";
+ if (any_of(mflags & QueryMatch::Flags::HasChild))
+ os << "has-child ";
+
+ return os;
+}
+
+inline std::ostream&
+operator<<(std::ostream& os, const QueryMatch& qmatch)
+{
+ os << "qm:[" << qmatch.thread_path << "]: " // " (" << qmatch.thread_level << "): "
+ << "> date:<" << qmatch.date_key << "> "
+ << "flags:{" << qmatch.flags << "}";
+
+ return os;
+}
+/* LCOV_EXCL_STOP*/
+
+using QueryMatches = std::unordered_map<Xapian::docid, QueryMatch>;
+
+
+///
+/// This is a view over the Xapian::MSet, which can optionally filter unreadable
+/// / duplicate messages.
+///
+/// Note, we internally skip unreadable/duplicate messages (when asked too); those
+/// skipped ones do _not_ count towards the max_size
+///
+class QueryResultsIterator {
+public:
+ using iterator_category = std::output_iterator_tag;
+ using value_type = Message;
+ using difference_type = void;
+ using pointer = void;
+ using reference = void;
+
+ QueryResultsIterator(Xapian::MSetIterator mset_it, QueryMatches& query_matches)
+ : mset_it_{mset_it}, query_matches_{query_matches} {
+ }
+
+ /**
+ * Increment the iterator (we don't support post-increment)
+ *
+ * @return an updated iterator, or end() if we were already at end()
+ */
+ QueryResultsIterator& operator++() {
+ ++mset_it_;
+ mdoc_ = Nothing;
+ return *this;
+ }
+
+ /**
+ * (Non)Equivalence operators
+ *
+ * @param rhs some other iterator
+ *
+ * @return true or false
+ */
+ bool operator==(const QueryResultsIterator& rhs) const { return mset_it_ == rhs.mset_it_; }
+ bool operator!=(const QueryResultsIterator& rhs) const { return mset_it_ != rhs.mset_it_; }
+
+ QueryResultsIterator& operator*() { return *this; }
+ const QueryResultsIterator& operator*() const { return *this; }
+
+
+ /**
+ * Get the Xapian::Document this iterator is pointing at,
+ * or an empty document when looking at end().
+ *
+ * @return a document
+ */
+ Option<Xapian::Document> document() const {
+ return xapian_try([this]()->Option<Xapian::Document> {
+ auto doc{mset_it_.get_document()};
+ if (doc.get_docid() == 0)
+ return Nothing;
+ else
+ return Some(std::move(doc));
+ }, Nothing);
+ }
+
+
+ /**
+ * get the corresponding Message for this iter, if any
+ *
+ * @return a Message or Nothing
+ */
+ Option<Message> message() const {
+ if (auto&& xdoc{document()}; !xdoc)
+ return Nothing;
+ else if (auto&& doc{Message::make_from_document(std::move(xdoc.value()))};
+ !doc)
+ return Nothing;
+ else
+ return Some(std::move(doc.value()));
+ }
+
+ /**
+ * Get the doc-id for the document this iterator is pointing at, or 0
+ * when looking at end.
+ *
+ * @return a doc-id.
+ */
+ Xapian::docid doc_id() const { return *mset_it_; }
+
+ /**
+ * Get the message-id for the document (message) this iterator is
+ * pointing at, or not when not available
+ *
+ * @return a message-id
+ */
+ Option<std::string> message_id() const noexcept {
+ return opt_string(Field::Id::MessageId);
+ }
+
+ /**
+ * Get the thread-id for the document (message) this iterator is
+ * pointing at, or Nothing.
+ *
+ * @return a message-id
+ */
+ Option<std::string> thread_id() const noexcept {
+ return opt_string(Field::Id::ThreadId);
+ }
+
+ /**
+ * Get the file-system path for the document (message) this iterator is
+ * pointing at, or Nothing.
+ *
+ * @return a filesystem path
+ */
+ Option<std::string> path() const noexcept {
+ return opt_string(Field::Id::Path);
+ }
+
+ /**
+ * Get the a sortable date str for the document (message) the iterator
+ * is pointing at. pointing at, or Nothing. This (encoded) string
+ * has the same sort-order as the corresponding date.
+ *
+ * @return a filesystem path
+ */
+ Option<std::string> date_str() const noexcept {
+ return opt_string(Field::Id::Date);
+ }
+
+ /**
+ * Get the subject for the document (message) this iterator is pointing
+ * at.
+ *
+ * @return the subject
+ */
+ Option<std::string> subject() const noexcept {
+ return opt_string(Field::Id::Subject);
+ }
+
+ /**
+ * Get the references for the document (messages) this is iterator is
+ * pointing at, or empty if pointing at end of if no references are
+ * available.
+ *
+ * @return references
+ */
+ std::vector<std::string> references() const noexcept {
+ return mu_document().string_vec_value(Field::Id::References);
+ }
+
+ /**
+ * Get some value from the document, or Nothing if empty.
+ *
+ * @param id a message field id
+ *
+ * @return the value
+ */
+ Option<std::string> opt_string(Field::Id id) const noexcept {
+ if (auto&& val{mu_document().string_value(id)}; val.empty())
+ return Nothing;
+ else
+ return Some(std::move(val));
+ }
+
+ /**
+ * Get the Query match info for this message.
+ *
+ * @return the match info.
+ */
+ QueryMatch& query_match() {
+ g_assert(query_matches_.find(doc_id()) != query_matches_.end());
+ return query_matches_.find(doc_id())->second;
+ }
+ const QueryMatch& query_match() const {
+ g_assert(query_matches_.find(doc_id()) != query_matches_.end());
+ return query_matches_.find(doc_id())->second;
+ }
+
+private:
+ /**
+ * Get a (cached) reference for the Mu::Document corresponding
+ * to the current iter.
+ *
+ * @return cached mu document,
+ */
+ const Mu::Document& mu_document() const {
+ if (!mdoc_) {
+ if (auto xdoc = document(); !xdoc)
+ std::runtime_error("iter without document");
+ else
+ mdoc_ = Mu::Document{xdoc.value()};
+ }
+ return mdoc_.value();
+ }
+
+ mutable Option<Mu::Document> mdoc_; // cache.
+ Xapian::MSetIterator mset_it_;
+ QueryMatches& query_matches_;
+};
+
+
+static inline auto
+format_as(const QueryResultsIterator& it)
+{
+ return it.path().value_or("<no path>");
+}
+
+constexpr auto MaxQueryResultsSize = std::numeric_limits<size_t>::max();
+
+class QueryResults {
+public:
+ /// Helper types
+ using iterator = QueryResultsIterator;
+ using const_iterator = const iterator;
+
+ /**
+ * Construct a QueryResults object
+ *
+ * @param mset an Xapian::MSet with matches
+ */
+ QueryResults(const Xapian::MSet& mset, QueryMatches&& query_matches)
+ : mset_{mset}, query_matches_{std::move(query_matches)}
+ {
+ }
+ /**
+ * Is this QueryResults object empty (ie., no matches)?
+ *
+ * @return true are false
+ */
+ bool empty() const { return mset_.empty(); }
+
+ /**
+ * Get the number of matches in this QueryResult
+ *
+ * @return number of matches
+ */
+ size_t size() const { return mset_.size(); }
+
+ /**
+ * Get the begin iterator to the results.
+ *
+ * @return iterator
+ */
+ const iterator begin() const { return QueryResultsIterator(mset_.begin(), query_matches_); }
+
+ /**
+ * Get the end iterator to the results.
+ *
+ * @return iterator
+ */
+ const_iterator end() const { return QueryResultsIterator(mset_.end(), query_matches_); }
+
+ /**
+ * Get the query-matches for these QueryResults. The non-const
+ * version can be use to _steal_ the query results, by moving
+ * them.
+ *
+ * @return query-matches
+ */
+ const QueryMatches& query_matches() const { return query_matches_; }
+ QueryMatches& query_matches() { return query_matches_; }
+
+private:
+ const Xapian::MSet mset_;
+ mutable QueryMatches query_matches_;
+};
+
+} // namespace Mu
+
+#endif /* MU_QUERY_RESULTS_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-query-threads.hh"
+#include <message/mu-message.hh>
+
+#include <set>
+#include <unordered_set>
+#include <list>
+#include <cassert>
+#include <cstring>
+#include <iostream>
+#include <iomanip>
+
+#include <utils/mu-option.hh>
+
+using namespace Mu;
+
+struct Container {
+ using Containers = std::vector<Container*>;
+
+ Container() = default;
+ Container(Option<QueryMatch&> msg) : query_match{msg} {}
+ Container(const Container&) = delete;
+ Container(Container&&) = default;
+
+ void add_child(Container& new_child)
+ {
+ new_child.parent = this;
+ children.emplace_back(&new_child);
+ }
+ void remove_child(Container& child)
+ {
+ children.erase(find_child(child));
+ assert(!has_child(child));
+ }
+
+ Containers::iterator find_child(Container& child)
+ {
+ return std::find_if(children.begin(), children.end(), [&](auto&& c) {
+ return c == &child;
+ });
+ }
+ Containers::const_iterator find_child(Container& child) const
+ {
+ return std::find_if(children.begin(), children.end(), [&](auto&& c) {
+ return c == &child;
+ });
+ }
+ bool has_child(Container& child) const { return find_child(child) != children.cend(); }
+
+ bool is_reachable(Container* other) const
+ {
+ auto up{ur_parent()};
+ return up && up == other->ur_parent();
+ }
+ template <typename Func> void for_each_child(Func&& func)
+ {
+ auto it{children.rbegin()};
+ while (it != children.rend()) {
+ auto next = std::next(it);
+ func(*it);
+ it = next;
+ }
+ }
+ // During sorting, this is the cached value for the (recursive) date-key
+ // of this container -- ie.. either the one from the first of its
+ // children, or from its query-match, if it has no children.
+ //
+ // Note that the sub-root-levels of threads are always sorted by date,
+ // in ascending order, regardless of whatever sorting was specified for
+ // the root-level.
+
+ std::string thread_date_key;
+
+ Option<QueryMatch&> query_match;
+ bool is_nuked{};
+ Container* parent{};
+ Containers children;
+
+ using ContainerVec = std::vector<Container*>;
+
+ private:
+ const Container* ur_parent() const
+ {
+ assert(this->parent != this);
+ return parent ? parent->ur_parent() : this;
+ }
+};
+
+using Containers = Container::Containers;
+using ContainerVec = Container::ContainerVec;
+
+/* LCOV_EXCL_START */
+static std::ostream&
+operator<<(std::ostream& os, const Container& container)
+{
+ os << "container: " << std::right << std::setw(10) << &container
+ << ": parent: " << std::right << std::setw(10) << container.parent << " ["
+ << container.thread_date_key << "]"
+ << "\n children: ";
+
+ for (auto&& c : container.children)
+ os << std::right << std::setw(10) << c << " ";
+
+ os << (container.is_nuked ? " nuked" : "");
+
+ if (container.query_match)
+ os << "\n " << container.query_match.value();
+
+ return os;
+}
+/* LCOV_EXCL_STOP */
+
+using IdTable = std::unordered_map<std::string, Container>;
+using DupTable = std::multimap<std::string, Container>;
+
+static void
+handle_duplicates(IdTable& id_table, DupTable& dup_table)
+{
+ size_t n{};
+
+ for (auto&& dup : dup_table) {
+ const auto msgid{dup.first};
+ auto it = id_table.find(msgid);
+ if (it == id_table.end())
+ continue;
+
+ // add duplicates as fake children
+ char buf[32];
+ ::snprintf(buf, sizeof(buf), "dup-%zu", ++n);
+ it->second.add_child(id_table.emplace(buf, std::move(dup.second)).first->second);
+ }
+}
+
+template <typename QueryResultsType>
+static IdTable
+determine_id_table(QueryResultsType& qres)
+{
+ // 1. For each query_match
+ IdTable id_table;
+ DupTable dups;
+ for (auto&& mi : qres) {
+ const auto msgid{mi.message_id().value_or(*mi.path())};
+ // Step 0 (non-JWZ): filter out dups, handle those at the end
+ if (mi.query_match().has_flag(QueryMatch::Flags::Duplicate)) {
+ dups.emplace(msgid, mi.query_match());
+ continue;
+ }
+ // 1.A If id_table contains an empty Container for this ID:
+ // Store this query_match (query_match) in the Container's query_match (value) slot.
+ // Else:
+ // Create a new Container object holding this query_match (query-match);
+ // Index the Container by Query_Match-ID
+ auto c_it = id_table.find(msgid);
+ auto& container = [&]() -> Container& {
+ if (c_it != id_table.end()) {
+ if (!c_it->second.query_match) // hmm, dup?
+ c_it->second.query_match = mi.query_match();
+ return c_it->second;
+ } else {
+ // Else:
+ // Create a new Container object holding this query_match
+ // (query-match); Index the Container by Query_Match-ID
+ return id_table.emplace(msgid, mi.query_match()).first->second;
+ }
+ }();
+
+ // We sort by date (ascending), *except* for the root; we don't
+ // know what query_matchs will be at the root level yet, so remember
+ // both. Moreover, even when sorting the top-level in descending
+ // order, still sort the thread levels below that in ascending
+ // order.
+ container.thread_date_key = container.query_match->date_key =
+ mi.date_str().value_or("");
+ // initial guess for the thread-date; might be updated
+ // later.
+
+ // remember the subject, we use it to determine the (sub)thread subject
+ container.query_match->subject = mi.subject().value_or("");
+
+ // 1.B
+ // For each element in the query_match's References field:
+ Container* parent_ref_container{};
+ for (const auto& ref : mi.references()) {
+ // grand_<n>-parent -> grand_<n-1>-parent -> ... -> parent.
+
+ // Find a Container object for the given Query_Match-ID; If it exists, use
+ // it; otherwise make one with a null Query_Match.
+ auto ref_container = [&]() -> Container* {
+ auto ref_it = id_table.find(ref);
+ if (ref_it == id_table.end())
+ ref_it = id_table.emplace(ref, Nothing).first;
+ return &ref_it->second;
+ }();
+
+ // Link the References field's Containers together in the order implied
+ // by the References header.
+ // * If they are already linked, don't change the existing links.
+ //
+ // * Do not add a link if adding that link would introduce a loop: that is,
+ // before asserting A->B, search down the children of B to see if A is
+ // reachable, and also search down the children of A to see if B is
+ // reachable. If either is already reachable as a child of the other,
+ // don't add the link.
+ if (parent_ref_container && !ref_container->parent) {
+ if (!parent_ref_container->is_reachable(ref_container))
+ parent_ref_container->add_child(*ref_container);
+ // else
+ // g_message ("%u: reachable %s -> %s", __LINE__,
+ // msgid.c_str(), ref.c_str());
+ }
+
+ parent_ref_container = ref_container;
+ }
+
+ // Add the query_match to the chain.
+ if (parent_ref_container && !container.parent) {
+ if (!parent_ref_container->is_reachable(&container))
+ parent_ref_container->add_child(container);
+ // else
+ // g_message ("%u: reachable %s -> parent", __LINE__,
+ // msgid.c_str());
+ }
+ }
+
+ // non-JWZ: add duplicate messages.
+ handle_duplicates(id_table, dups);
+
+ return id_table;
+}
+
+/// Recursively walk all containers under the root set.
+/// For each container:
+///
+/// If it is an empty container with no children, nuke it.
+///
+/// Note: Normally such containers won't occur, but they can show up when two
+/// query_matchs have References lines that disagree. For example, assuming A and
+/// B are query_matchs, and 1, 2, and 3 are references for query_matchs we haven't
+/// seen:
+///
+/// A has references: 1, 2, 3
+/// B has references: 1, 3
+///
+/// There is ambiguity as to whether 3 is a child of 1 or of 2. So,
+/// depending on the processing order, we might end up with either
+///
+/// -- 1
+/// |-- 2
+/// \-- 3
+/// |-- A
+/// \-- B
+///
+/// or
+///
+/// -- 1
+/// |-- 2 <--- non root childless container!
+/// \-- 3
+/// |-- A
+/// \-- B
+///
+/// If the Container has no Query_Match, but does have children, remove this
+/// container but promote its children to this level (that is, splice them in
+/// to the current child list.)
+///
+/// Do not promote the children if doing so would promote them to the root
+/// set -- unless there is only one child, in which case, do.
+
+static void
+prune(Container* child)
+{
+ Container* container{child->parent};
+
+ for (auto& grandchild : child->children) {
+ grandchild->parent = container;
+ if (container)
+ container->children.emplace_back(grandchild);
+ }
+
+ child->children.clear();
+ child->is_nuked = true;
+
+ if (container)
+ container->remove_child(*child);
+}
+
+static bool
+prune_empty_containers(Container& container)
+{
+ Containers to_prune;
+
+ container.for_each_child([&](auto& child) {
+ if (prune_empty_containers(*child))
+ to_prune.emplace_back(child);
+ });
+
+ for (auto& child : to_prune)
+ prune(child);
+
+ // Never nuke these.
+ if (container.query_match)
+ return false;
+
+ // If it is an empty container with no children, nuke it.
+ //
+ // If the Container is empty, but does have children, remove this
+ // container but promote its children to this level (that is, splice them in
+ // to the current child list.)
+ //
+ // Do not promote the children if doing so would promote them to the root
+ // set -- unless there is only one child, in which case, do.
+ // const auto rootset_child{!container.parent->parent};
+ if (container.parent || container.children.size() <= 1)
+ return true; // splice/nuke it.
+
+ return false;
+}
+
+static void
+prune_empty_containers(IdTable& id_table)
+{
+ for (auto&& item : id_table) {
+ auto& child(item.second);
+ if (child.parent)
+ continue; // not a root child.
+
+ if (prune_empty_containers(item.second))
+ prune(&child);
+ }
+}
+
+//
+// Sorting.
+//
+
+/// Register some information about a match (i.e., message) that we can use for
+/// subsequent queries.
+using ThreadPath = std::vector<unsigned>;
+inline std::string
+to_string(const ThreadPath& tpath, size_t digits)
+{
+ std::string str;
+ str.reserve(tpath.size() * digits);
+
+ bool first{true};
+ for (auto&& segm : tpath) {
+ str += mu_format("{}{:0{}x}", first ? "" : ":", segm, digits);
+ first = false;
+ }
+
+ return str;
+}
+
+static bool // compare subjects, ignore anything before the last ':<space>*'
+subject_matches(const std::string& sub1, const std::string& sub2)
+{
+ auto search_str = [](const std::string& s) -> const char* {
+ const auto pos = s.find_last_of(':');
+ if (pos == std::string::npos)
+ return s.c_str();
+ else {
+ const auto pos2 = s.find_first_not_of(' ', pos + 1);
+ return s.c_str() + (pos2 == std::string::npos ? pos : pos2);
+ }
+ };
+
+ return g_strcmp0(search_str(sub1), search_str(sub2)) == 0;
+}
+
+static bool
+update_container(Container& container,
+ bool descending,
+ ThreadPath& tpath,
+ size_t seg_size,
+ const std::string& prev_subject = "")
+{
+ if (!container.children.empty()) {
+ Container* first = container.children.front();
+ if (first->query_match)
+ first->query_match->flags |= QueryMatch::Flags::First;
+ Container* last = container.children.back();
+ if (last->query_match)
+ last->query_match->flags |= QueryMatch::Flags::Last;
+ }
+
+ if (!container.query_match)
+ return false; // nothing else to do.
+
+ auto& qmatch(*container.query_match);
+ if (!container.parent)
+ qmatch.flags |= QueryMatch::Flags::Root;
+ else if (!container.parent->query_match)
+ qmatch.flags |= QueryMatch::Flags::Orphan;
+
+ if (!container.children.empty())
+ qmatch.flags |= QueryMatch::Flags::HasChild;
+
+ if (qmatch.has_flag(QueryMatch::Flags::Root) || prev_subject.empty() ||
+ !subject_matches(prev_subject, qmatch.subject))
+ qmatch.flags |= QueryMatch::Flags::ThreadSubject;
+
+ if (descending && container.parent) {
+ // trick xapian by giving it "inverse" sorting key so our
+ // ascending-date sorted threads stay in that order
+ tpath.back() = ((1U << (4 * seg_size)) - 1) - tpath.back();
+ }
+
+ qmatch.thread_path = to_string(tpath, seg_size);
+ qmatch.thread_level = tpath.size() - 1;
+
+ // ensure thread root comes before its children
+ if (descending)
+ qmatch.thread_path += ":z";
+
+ return true;
+}
+
+static void
+update_containers(Containers& children,
+ bool descending,
+ ThreadPath& tpath,
+ size_t seg_size,
+ std::string& prev_subject)
+{
+ size_t idx{0};
+
+ for (auto&& c : children) {
+ tpath.emplace_back(idx++);
+ if (c->query_match) {
+ update_container(*c, descending, tpath, seg_size, prev_subject);
+ prev_subject = c->query_match->subject;
+ }
+ update_containers(c->children, descending, tpath, seg_size, prev_subject);
+ tpath.pop_back();
+ }
+}
+
+static void
+update_containers(ContainerVec& root_vec, bool descending, size_t n)
+{
+ ThreadPath tpath;
+ tpath.reserve(n);
+
+ const auto seg_size = static_cast<size_t>(std::ceil(std::log2(n) / 4.0));
+ /*note: 4 == std::log2(16)*/
+
+ size_t idx{0};
+ for (auto&& c : root_vec) {
+ tpath.emplace_back(idx++);
+ std::string prev_subject;
+ if (update_container(*c, descending, tpath, seg_size))
+ prev_subject = c->query_match->subject;
+ update_containers(c->children, descending, tpath, seg_size, prev_subject);
+ tpath.pop_back();
+ }
+}
+
+static void
+sort_container(Container& container)
+{
+ // 1. childless container.
+ if (container.children.empty())
+ return; // no children; nothing to sort.
+
+ // 2. container with children.
+ // recurse, depth-first: sort the children
+ for (auto& child : container.children)
+ sort_container(*child);
+
+ // now sort this level.
+ std::sort(container.children.begin(), container.children.end(), [&](auto&& c1, auto&& c2) {
+ return c1->thread_date_key < c2->thread_date_key;
+ });
+
+ // and 'bubble up' the date of the *newest* message with a date. We
+ // reasonably assume that it's later than its parent.
+ const auto& newest_date = container.children.back()->thread_date_key;
+ if (!newest_date.empty())
+ container.thread_date_key = newest_date;
+}
+
+static void
+sort_siblings(IdTable& id_table, bool descending)
+{
+ if (id_table.empty())
+ return;
+
+ // unsorted vec of root containers. We can
+ // only sort these _after_ sorting the children.
+ ContainerVec root_vec;
+ for (auto&& item : id_table) {
+ if (!item.second.parent && !item.second.is_nuked)
+ root_vec.emplace_back(&item.second);
+ }
+
+ // now sort all threads _under_ the root set (by date/ascending)
+ for (auto&& c : root_vec)
+ sort_container(*c);
+
+ // and then sort the root set.
+ //
+ // The difference with the sub-root containers is that at the top-level,
+ // we can sort either in ascending or descending order, while on the
+ // subroot level it's always in ascending order.
+ //
+ // Note that unless we're testing, _xapian_ will handle
+ // the ascending/descending of the top level.
+ std::sort(root_vec.begin(), root_vec.end(), [&](auto&& c1, auto&& c2) {
+#ifdef BUILD_TESTS
+ if (descending)
+ return c2->thread_date_key < c1->thread_date_key;
+ else
+#endif /*BUILD_TESTS*/
+ return c1->thread_date_key < c2->thread_date_key;
+ });
+
+ // now all is sorted... final step is to determine thread paths and
+ // other flags.
+ update_containers(root_vec, descending, id_table.size());
+}
+
+/* LCOV_EXCL_START */
+static std::ostream&
+operator<<(std::ostream& os, const IdTable& id_table)
+{
+ os << "------------------------------------------------\n";
+ for (auto&& item : id_table) {
+ os << item.first << " => " << item.second << "\n";
+ }
+ os << "------------------------------------------------\n";
+
+ std::set<std::string> ids;
+ for (auto&& item : id_table) {
+ if (item.second.query_match)
+ ids.emplace(item.second.query_match->thread_path);
+ }
+
+ for (auto&& id : ids) {
+ auto it = std::find_if(id_table.begin(), id_table.end(), [&](auto&& item) {
+ return item.second.query_match &&
+ item.second.query_match->thread_path == id;
+ });
+ assert(it != id_table.end());
+ os << it->first << ": " << it->second << '\n';
+ }
+ return os;
+}
+/* LCOV_EXCL_STOP */
+
+template <typename Results>
+static void
+calculate_threads_real(Results& qres, bool descending)
+{
+ // Step 1: build the id_table
+ auto id_table{determine_id_table(qres)};
+
+ if (g_test_verbose())
+ std::cout << "*** id-table(1):\n" << id_table << "\n";
+
+ // // Step 2: get the root set
+ // // Step 3: discard id_table
+ // Nope: id-table owns the containers.
+ // Step 4: prune empty containers
+ prune_empty_containers(id_table);
+
+ // Step 5: group root-set by subject.
+ // Not implemented.
+
+ // Step 6: we're done threading
+
+ // Step 7: sort siblings. The segment-size is the number of hex-digits
+ // in the thread-path string (so we can lexically compare them.)
+ sort_siblings(id_table, descending);
+
+ // Step 7a:. update querymatches
+ for (auto&& item : id_table) {
+ Container& c{item.second};
+ if (c.query_match)
+ c.query_match->thread_date = c.thread_date_key;
+ }
+ // if (g_test_verbose())
+ // std::cout << "*** id-table(2):\n" << id_table << "\n";
+}
+
+void
+Mu::calculate_threads(Mu::QueryResults& qres, bool descending)
+{
+ calculate_threads_real(qres, descending);
+}
+
+#ifdef BUILD_TESTS
+
+struct MockQueryResult {
+ MockQueryResult(const std::string& message_id_arg,
+ const std::string& date_arg,
+ const std::vector<std::string>& refs_arg = {})
+ : message_id_{message_id_arg}, date_{date_arg}, refs_{refs_arg}
+ {
+ }
+ MockQueryResult(const std::string& message_id_arg,
+ const std::vector<std::string>& refs_arg = {})
+ : MockQueryResult(message_id_arg, "", refs_arg)
+ {
+ }
+ Option<std::string> message_id() const { return message_id_; }
+ Option<std::string> path() const { return path_; }
+ Option<std::string> date_str() const { return date_; }
+ Option<std::string> subject() const { return subject_; }
+ QueryMatch& query_match() { return query_match_; }
+ const QueryMatch& query_match() const { return query_match_; }
+ const std::vector<std::string>& references() const { return refs_; }
+
+ std::string path_;
+ std::string message_id_;
+ QueryMatch query_match_{};
+ std::string date_;
+ std::string subject_;
+ std::vector<std::string> refs_;
+};
+
+using MockQueryResults = std::vector<MockQueryResult>;
+
+G_GNUC_UNUSED static std::ostream&
+operator<<(std::ostream& os, const MockQueryResults& qrs)
+{
+ for (auto&& mi : qrs)
+ os << mi.query_match().thread_path << " :: " << mi.message_id().value_or("<none>")
+ << std::endl;
+
+ return os;
+}
+
+static void
+calculate_threads(MockQueryResults& qres, bool descending)
+{
+ calculate_threads_real(qres, descending);
+}
+
+using Expected = std::vector<std::pair<std::string, std::string>>;
+
+static void
+assert_thread_paths(const MockQueryResults& qrs, const Expected& expected)
+{
+ for (auto&& exp : expected) {
+ auto it = std::find_if(qrs.begin(), qrs.end(), [&](auto&& qr) {
+ return qr.message_id().value_or("") == exp.first ||
+ qr.path().value_or("") == exp.first;
+ });
+ g_assert_true(it != qrs.end());
+ mu_debug("thread-path ({}@{}): expected: '{}'; got '{}'",
+ it->message_id().value_or("<none>"),
+ it->path().value_or("<none>"),
+ exp.second, it->query_match().thread_path);
+ g_assert_cmpstr(exp.second.c_str(), ==, it->query_match().thread_path.c_str());
+ }
+}
+
+static void
+test_sort_ascending()
+{
+ auto results = MockQueryResults{MockQueryResult{"m1", "1", {"m2"}},
+ MockQueryResult{"m2", "2", {"m3"}},
+ MockQueryResult{"m3", "3", {}},
+ MockQueryResult{"m4", "4", {}}};
+
+ calculate_threads(results, false);
+
+ assert_thread_paths(results, {{"m1", "0:0:0"}, {"m2", "0:0"}, {"m3", "0"}, {"m4", "1"}});
+}
+
+static void
+test_sort_descending()
+{
+ auto results = MockQueryResults{MockQueryResult{"m1", "1", {"m2"}},
+ MockQueryResult{"m2", "2", {"m3"}},
+ MockQueryResult{"m3", "3", {}},
+ MockQueryResult{"m4", "4", {}}};
+
+ calculate_threads(results, true);
+
+ assert_thread_paths(results,
+ {{"m1", "1:f:f:z"}, {"m2", "1:f:z"}, {"m3", "1:z"}, {"m4", "0:z"}});
+}
+
+static void
+test_id_table_inconsistent()
+{
+ auto results = MockQueryResults{
+ MockQueryResult{"m1", "1", {"m2"}}, // 1->2
+ MockQueryResult{"m2", "2", {"m1"}}, // 2->1
+ MockQueryResult{"m3", "3", {"m3"}}, // self ref
+ MockQueryResult{"m4", "4", {"m3", "m5"}},
+ MockQueryResult{"m5", "5", {"m4", "m4"}}, // dup parent
+ };
+
+ calculate_threads(results, false);
+ assert_thread_paths(results,
+ {
+ {"m2", "0"},
+ {"m1", "0:0"},
+ {"m3", "1"},
+ {"m5", "1:0"},
+ {"m4", "1:0:0"},
+ });
+}
+
+static void
+test_dups_dup_last()
+{
+ MockQueryResult r1{"m1", "1", {}};
+ r1.query_match().flags |= QueryMatch::Flags::Leader;
+ r1.path_ = "/path1";
+
+ MockQueryResult r1_dup{"m1", "1", {}};
+ r1_dup.query_match().flags |= QueryMatch::Flags::Duplicate;
+ r1_dup.path_ = "/path2";
+
+ auto results = MockQueryResults{r1, r1_dup};
+
+ calculate_threads(results, false);
+
+ assert_thread_paths(results,
+ {
+ {"/path1", "0"},
+ {"/path2", "0:0"},
+ });
+}
+
+static void
+test_dups_dup_first()
+{
+ // now dup becomes the leader; this will _demote_
+ // r1.
+
+ MockQueryResult r1_dup{"m1", "1", {}};
+ r1_dup.query_match().flags |= QueryMatch::Flags::Duplicate;
+ r1_dup.path_ = "/path1";
+
+ MockQueryResult r1{"m1", "1", {}};
+ r1.query_match().flags |= QueryMatch::Flags::Leader;
+ r1.path_ = "/path2";
+
+ auto results = MockQueryResults{r1_dup, r1};
+
+ calculate_threads(results, false);
+
+ assert_thread_paths(results, {
+ {"/path2", "0"},
+ {"/path1", "0:0"},
+ });
+}
+
+static void
+test_dups_dup_multi()
+{
+ // now dup becomes the leader; this will _demote_
+ // r1.
+
+ MockQueryResult r1_dup1{"m1", "1", {}};
+ r1_dup1.query_match().flags |= QueryMatch::Flags::Duplicate;
+ r1_dup1.path_ = "/path1";
+
+ MockQueryResult r1_dup2{"m1", "1", {}};
+ r1_dup2.query_match().flags |= QueryMatch::Flags::Duplicate;
+ r1_dup2.path_ = "/path2";
+
+ MockQueryResult r1{"m1", "1", {}};
+ r1.query_match().flags |= QueryMatch::Flags::Leader;
+ r1.path_ = "/path3";
+
+ auto results = MockQueryResults{r1_dup1, r1_dup2, r1};
+ calculate_threads(results, false);
+
+ assert_thread_paths(results, {
+ {"/path3", "0"},
+ {"/path1", "0:0"},
+ {"/path2", "0:1"},
+ });
+}
+
+
+
+
+static void
+test_do_not_prune_root_empty_with_children()
+{
+ // m7 should not be nuked
+ auto results = MockQueryResults{
+ MockQueryResult{"x1", "1", {"m7"}},
+ MockQueryResult{"x2", "2", {"m7"}},
+ };
+
+ calculate_threads(results, false);
+
+ assert_thread_paths(results,
+ {
+ {"x1", "0:0"},
+ {"x2", "0:1"},
+ });
+}
+
+static void
+test_prune_root_empty_with_child()
+{
+ // m7 should be nuked
+ auto results = MockQueryResults{
+ MockQueryResult{"m1", "1", {"m7"}},
+ };
+
+ calculate_threads(results, false);
+
+ assert_thread_paths(results,
+ {
+ {"m1", "0"},
+ });
+}
+
+static void
+test_prune_empty_with_children()
+{
+ // m6 should be nuked
+ auto results = MockQueryResults{
+ MockQueryResult{"m1", "1", {"m7", "m6"}},
+ MockQueryResult{"m2", "2", {"m7", "m6"}},
+ };
+
+ calculate_threads(results, false);
+
+ assert_thread_paths(results,
+ {
+ {"m1", "0:0"},
+ {"m2", "0:1"},
+ });
+}
+
+static void
+test_thread_info_ascending()
+{
+ auto results = MockQueryResults{
+ MockQueryResult{"m1", "5", {}},
+ MockQueryResult{"m2", "1", {}},
+ MockQueryResult{"m3", "3", {"m2"}},
+ MockQueryResult{"m4", "2", {"m2"}},
+ // orphan siblings
+ MockQueryResult{"m10", "6", {"m9"}},
+ MockQueryResult{"m11", "7", {"m9"}},
+ };
+ calculate_threads(results, false);
+
+ assert_thread_paths(results,
+ {
+ {"m2", "0"}, // 2
+ {"m4", "0:0"}, // 2
+ {"m3", "0:1"}, // 3
+ {"m1", "1"}, // 5
+
+ {"m10", "2:0"}, // 6
+ {"m11", "2:1"}, // 7
+ });
+
+ g_assert_true(results[0].query_match().has_flag(QueryMatch::Flags::Root));
+ g_assert_true(results[1].query_match().has_flag(QueryMatch::Flags::Root |
+ QueryMatch::Flags::HasChild));
+ g_assert_true(results[2].query_match().has_flag(QueryMatch::Flags::Last));
+ g_assert_true(results[3].query_match().has_flag(QueryMatch::Flags::First));
+ g_assert_true(results[4].query_match().has_flag(QueryMatch::Flags::Orphan |
+ QueryMatch::Flags::First));
+ g_assert_true(
+ results[5].query_match().has_flag(QueryMatch::Flags::Orphan | QueryMatch::Flags::Last));
+}
+
+static void
+test_thread_info_descending()
+{
+ auto results = MockQueryResults{
+ MockQueryResult{"m1", "5", {}},
+ MockQueryResult{"m2", "1", {}},
+ MockQueryResult{"m3", "3", {"m2"}},
+ MockQueryResult{"m4", "2", {"m2"}},
+ // orphan siblings
+ MockQueryResult{"m10", "6", {"m9"}},
+ MockQueryResult{"m11", "7", {"m9"}},
+ };
+ calculate_threads(results, true /*descending*/);
+
+ assert_thread_paths(results,
+ {
+ {"m1", "1:z"}, // 5
+ {"m2", "2:z"}, // 2
+ {"m4", "2:f:z"}, // 2
+ {"m3", "2:e:z"}, // 3
+
+ {"m10", "0:f:z"}, // 6
+ {"m11", "0:e:z"}, // 7
+ });
+ g_assert_true(results[0].query_match().has_flag(QueryMatch::Flags::Root));
+ g_assert_true(results[1].query_match().has_flag(QueryMatch::Flags::Root |
+ QueryMatch::Flags::HasChild));
+ g_assert_true(results[2].query_match().has_flag(QueryMatch::Flags::Last));
+ g_assert_true(results[3].query_match().has_flag(QueryMatch::Flags::First));
+
+ g_assert_true(
+ results[4].query_match().has_flag(QueryMatch::Flags::Orphan | QueryMatch::Flags::Last));
+ g_assert_true(results[5].query_match().has_flag(QueryMatch::Flags::Orphan |
+ QueryMatch::Flags::First));
+}
+
+int
+main(int argc, char* argv[])
+try {
+ g_test_init(&argc, &argv, NULL);
+
+ g_test_add_func("/threader/sort/ascending", test_sort_ascending);
+ g_test_add_func("/threader/sort/decending", test_sort_descending);
+
+ g_test_add_func("/threader/id-table-inconsistent", test_id_table_inconsistent);
+ g_test_add_func("/threader/dups/dup-last", test_dups_dup_last);
+ g_test_add_func("/threader/dups/dup-first", test_dups_dup_first);
+ g_test_add_func("/threader/dups/dup-multi", test_dups_dup_multi);
+
+ g_test_add_func("/threader/prune/do-not-prune-root-empty-with-children",
+ test_do_not_prune_root_empty_with_children);
+ g_test_add_func("/threader/prune/prune-root-empty-with-child",
+ test_prune_root_empty_with_child);
+ g_test_add_func("/threader/prune/prune-empty-with-children",
+ test_prune_empty_with_children);
+
+ g_test_add_func("/threader/thread-info/ascending", test_thread_info_ascending);
+ g_test_add_func("/threader/thread-info/descending", test_thread_info_descending);
+
+ return g_test_run();
+} catch (const std::runtime_error& re) {
+ std::cerr << re.what() << "\n";
+ return 1;
+} catch (...) {
+ std::cerr << "caught exception\n";
+ return 1;
+}
+
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_QUERY_THREADS__
+#define MU_QUERY_THREADS__
+
+#include "mu-query-results.hh"
+
+namespace Mu {
+/**
+ * Calculate the threads for these query results; that is, determine the
+ * thread-paths for each message, so we can let Xapian order them in the correct
+ * order.
+ *
+ * Note - threads are sorted chronologically, and the messages below the top
+ * level are always sorted in ascending orde
+ *
+ * @param qres query results
+ * @param descending whether to sort the top-level in descending order
+ */
+void calculate_threads(QueryResults& qres, bool descending);
+
+} // namespace Mu
+
+#endif /*MU_QUERY_THREADS__*/
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+#include "mu-query-parser.hh"
+
+#include <string_view>
+#include <variant>
+#include <array>
+#include <type_traits>
+
+#include "utils/mu-option.hh"
+#include <glib.h>
+#include "utils/mu-utils-file.hh"
+
+using namespace Mu;
+
+// backward compat
+#ifndef HAVE_XAPIAN_FLAG_NGRAMS
+#define FLAG_NGRAMS FLAG_CJK_NGRAM
+#endif /*HAVE_XAPIAN_FLAG_NGRAMS*/
+
+/**
+ * Expand terms for scripts without explicit word-breaks (e.g.
+ * Chinese/Japanese/Korean) in the way that Xapian expects it -
+ * use Xapian's built-in QueryParser just for that.
+ */
+static Result<Xapian::Query>
+ngram_expand(const Field& field, const std::string& str)
+{
+ Xapian::QueryParser qp;
+ const auto pfx{std::string(1U, field.xapian_prefix())};
+
+ qp.set_default_op(Xapian::Query::OP_OR);
+
+ return qp.parse_query(str, Xapian::QueryParser::FLAG_NGRAMS, pfx);
+}
+
+
+static Option<Sexp>
+tail(Sexp&& s)
+{
+ if (!s.listp() || s.empty())
+ return Nothing;
+
+ s.list().erase(s.list().begin(), s.list().begin() + 1);
+
+ return s;
+}
+
+Option<std::string>
+head_symbol(const Sexp& s)
+{
+ if (!s.listp() || s.empty() || !s.head() || !s.head()->symbolp())
+ return Nothing;
+
+ return s.head()->symbol().name;
+}
+
+
+Option<std::string>
+string_nth(const Sexp& args, size_t n)
+{
+ if (!args.listp() || args.size() < n + 1)
+ return Nothing;
+
+ if (auto&& item{args.list().at(n)}; !item.stringp())
+ return Nothing;
+ else
+ return item.string();
+}
+
+static Result<Xapian::Query>
+phrase(const Field& field, Sexp&& s)
+{
+ if (!field.is_phrasable_term())
+ return Err(Error::Code::InvalidArgument,
+ "field {} does not support phrases", field.name);
+
+ if (s.size() == 1 && s.front().stringp()) {
+ auto&& words{split(s.front().string(), " ")};
+ std::vector<Xapian::Query> phvec;
+ phvec.reserve(words.size());
+ for(auto&& w: words)
+ phvec.emplace_back(Xapian::Query{field.xapian_term(std::move(w))});
+ return Xapian::Query{Xapian::Query::OP_PHRASE,
+ phvec.begin(), phvec.end()};
+ } else
+ return Err(Error::Code::InvalidArgument,
+ "invalid phrase for field {}: '{}'", field.name, s.to_string());
+}
+
+static Result<Xapian::Query>
+regex(const Store& store, const Field& field, const std::string& rx_str)
+{
+ auto&& str{utf8_flatten(rx_str)};
+ auto&& rx{Regex::make(str, G_REGEX_OPTIMIZE)};
+ if (!rx) {
+ mu_warning("invalid regexp: '{}': {}", str, rx.error().what());
+ return Xapian::Query::MatchNothing;
+ }
+
+ std::vector<Xapian::Query> rxvec;
+ store.for_each_term(field.id, [&](auto&& str) {
+ if (auto&& val{str.data() + 1}; rx->matches(val))
+ rxvec.emplace_back(field.xapian_term(std::string_view{val}));
+ return true;
+ });
+
+ return Xapian::Query(Xapian::Query::OP_OR, rxvec.begin(), rxvec.end());
+}
+
+
+
+static Result<Xapian::Query>
+range(const Field& field, Sexp&& s)
+{
+ auto&& r0{string_nth(s, 0)};
+ auto&& r1{string_nth(s, 1)};
+ if (!r0 || !r1)
+ return Err(Error::Code::InvalidArgument, "expected 2 range values");
+
+ // in the sexp, we use iso date/time for human readability; now convert to
+ // time_t
+ auto iso_to_lexnum=[](const std::string& s)->Option<std::string> {
+ if (s.empty())
+ return s;
+ if (auto&& t{parse_date_time(s, true, true/*utc*/)}; !t)
+ return Nothing;
+ else
+ return to_lexnum(*t);
+ };
+
+ if (field == Field::Id::Date || field == Field::Id::Changed) {
+ // iso -> time_t
+ r0 = iso_to_lexnum(*r0);
+ r1 = iso_to_lexnum(*r1);
+ } else if (field == Field::Id::Size) {
+ if (!r0->empty())
+ r0 = to_lexnum(::atoll(r0->c_str()));
+ if (!r1->empty())
+ r1 = to_lexnum(::atoll(r1->c_str()));
+ } else
+ return Err(Error::Code::InvalidArgument,
+ "unsupported range field {}", field.name);
+
+ if (r0->empty() && r1->empty())
+ return Xapian::Query::MatchNothing; // empty range matches nothing.
+ else if (r0->empty() && !r1->empty())
+ return Xapian::Query(Xapian::Query::OP_VALUE_LE,
+ field.value_no(), *r1);
+ else if (!r0->empty() && r1->empty())
+ return Xapian::Query(Xapian::Query::OP_VALUE_GE,
+ field.value_no(), *r0);
+ else
+ return Xapian::Query(Xapian::Query::OP_VALUE_RANGE,
+ field.value_no(), *r0, *r1);
+}
+
+
+
+using OpPair = std::pair<const std::string_view, Xapian::Query::op>;
+static constexpr std::array<OpPair, 4> LogOpPairs = {{
+ { "and", Xapian::Query::OP_AND },
+ { "or", Xapian::Query::OP_OR },
+ { "xor", Xapian::Query::OP_XOR },
+ { "not", Xapian::Query::OP_AND_NOT }
+ }};
+
+static Option<Xapian::Query::op>
+find_log_op(const std::string& opname)
+{
+ for (auto&& p: LogOpPairs)
+ if (p.first == opname)
+ return p.second;
+
+ return Nothing;
+}
+
+static Result<Xapian::Query> parse(const Store& store, Sexp&& s, Mu::ParserFlags flags);
+
+static Result<Xapian::Query>
+parse_logop(const Store& store, Xapian::Query::op op, Sexp&& args, Mu::ParserFlags flags)
+{
+ if (!args.listp() || args.empty())
+ return Err(Error::Code::InvalidArgument,
+ "expected non-empty list but got", args.to_string());
+
+ std::vector<Xapian::Query> qs;
+ for (auto&& elm: args.list()) {
+ if (auto&& q{parse(store, std::move(elm), flags)}; !q)
+ return Err(std::move(q.error()));
+ else
+ qs.emplace_back(std::move(*q));
+ }
+
+ switch(op) {
+ case Xapian::Query::OP_AND_NOT:
+ // TODO: optimize AND_NOT
+ if (qs.size() != 1)
+ return Err(Error::Code::InvalidArgument,
+ "expected single argument for NOT");
+ else
+ return Xapian::Query{op, Xapian::Query::MatchAll, qs.at(0)};
+
+ case Xapian::Query::OP_AND:
+ case Xapian::Query::OP_OR:
+ case Xapian::Query::OP_XOR:
+ return Xapian::Query(op, qs.begin(), qs.end());
+
+ default:
+ return Err(Error::Code::InvalidArgument, "unexpected xapian op");
+ }
+}
+
+
+static Result<Xapian::Query>
+parse_field_matcher(const Store& store, const Field& field,
+ const std::string& match_sym, Sexp&& args)
+{
+ auto&& str0{string_nth(args, 0)};
+
+ if (match_sym == wildcard_sym.name && str0)
+ return Xapian::Query{Xapian::Query::OP_WILDCARD,
+ field.xapian_term(*str0)};
+ else if (match_sym == range_sym.name && !!str0)
+ return range(field, std::move(args));
+ else if (match_sym == regex_sym.name && !!str0)
+ return regex(store, field, *str0);
+ else if (match_sym == phrase_sym.name)
+ return phrase(field, std::move(args));
+
+ return Err(Error::Code::InvalidArgument,
+ "invalid field '{}'/'{}' matcher: {}",
+ field.name, match_sym, args.to_string());
+}
+
+static Result<Xapian::Query>
+parse_basic(const Field &field, Sexp &&vals, Mu::ParserFlags flags)
+{
+ auto ngrams = any_of(flags & ParserFlags::SupportNgrams);
+ if (!vals.stringp())
+ return Err(Error::Code::InvalidArgument, "expected string");
+
+ auto&& val{vals.string()};
+
+ switch (field.id) {
+ case Field::Id::Flags:
+ if (auto&& finfo{flag_info(val)}; finfo)
+ return Xapian::Query{field.xapian_term(finfo->shortcut_lower())};
+ else
+ return Err(Error::Code::InvalidArgument, "invalid flag '{}'", val);
+ case Field::Id::Priority:
+ if (auto&& prio{priority_from_name(val)}; prio)
+ return Xapian::Query{field.xapian_term(to_char(*prio))};
+ else
+ return Err(Error::Code::InvalidArgument, "invalid priority '{}'", val);
+ default: {
+ auto q{Xapian::Query{field.xapian_term(val)}};
+ if (ngrams) { // special case: cjk; see if we can create an expanded query.
+ if (field.is_phrasable_term() && contains_unbroken_script(val))
+ if (auto&& ng{ngram_expand(field, val)}; ng)
+ return ng;
+ }
+ return q;
+ }}
+}
+
+static Result<Xapian::Query>
+parse(const Store& store, Sexp&& s, Mu::ParserFlags flags)
+{
+ auto&& headsym{head_symbol(s)};
+ if (!headsym)
+ return Err(Error::Code::InvalidArgument,
+ "expected (symbol ...) but got {}", s.to_string());
+
+ // ie., something like (or|and| ... ....)
+ if (auto&& logop{find_log_op(*headsym)}; logop) {
+ if (auto&& args{tail(std::move(s))}; !args)
+ return Err(Error::Code::InvalidArgument,
+ "expected (logop ...) but got {}",
+ s.to_string());
+ else
+ return parse_logop(store, *logop, std::move(*args), flags);
+
+ }
+ // something like (field ...)
+ else if (auto&& field{field_from_name(*headsym)}; field) {
+
+ auto&& rest{tail(std::move(s))};
+ if (!rest || rest->empty())
+ return Err(Error::Code::InvalidArgument,
+ "expected field-value or field-matcher");
+
+ auto&& matcher{rest->front()};
+ // field-value: (field "value"); ensure "value" is there
+ if (matcher.stringp())
+ return parse_basic(*field, std::move(matcher), flags);
+
+ // otherwise, we expect a field-matcher, e.g. (field (phrase "a b c"))
+ // ensure the matcher is a list starting with a symbol
+ auto&& match_sym{head_symbol(matcher)};
+ if (!match_sym)
+ return Err(Error::Code::InvalidArgument,
+ "expected field-matcher");
+
+ if (auto&& args{tail(std::move(matcher))}; !args)
+ return Err(Error::Code::InvalidArgument, "expected matcher arguments");
+ else
+ return parse_field_matcher(store, *field,
+ *match_sym, std::move(*args));
+ }
+ return Err(Error::Code::InvalidArgument, "unexpected sexp {}", s.to_string());
+}
+
+/* LCOV_EXCL_START*/
+// parse the way Xapian's internal parser does it; for testing.
+static Xapian::Query
+xapian_query_classic(const std::string& expr, Mu::ParserFlags flags)
+{
+ Xapian::QueryParser xqp;
+
+ // add prefixes
+ field_for_each([&](auto&& field){
+
+ if (!field.is_searchable())
+ return;
+
+ const auto prefix{std::string(1U, field.xapian_prefix())};
+ std::vector<std::string> names = {
+ std::string{field.name},
+ std::string(1U, field.shortcut)
+ };
+ if (!field.alias.empty())
+ names.emplace_back(std::string{field.alias});
+
+ for (auto&& name: names)
+ xqp.add_prefix(name, prefix);
+ });
+
+ auto xflags = Xapian::QueryParser::FLAG_PHRASE |
+ Xapian::QueryParser::FLAG_BOOLEAN |
+ Xapian::QueryParser::FLAG_WILDCARD;
+
+ if (any_of(flags & ParserFlags::SupportNgrams))
+ xflags |= Xapian::QueryParser::FLAG_NGRAMS;
+
+ xqp.set_default_op(Xapian::Query::OP_AND);
+ return xqp.parse_query(expr, xflags);
+}
+/* LCOV_EXCL_STOP*/
+
+Result<Xapian::Query>
+Mu::make_xapian_query(const Store& store, const std::string& expr, Mu::ParserFlags flags) noexcept
+{
+ if (any_of(flags & Mu::ParserFlags::XapianParser))
+ return xapian_query_classic(expr, flags);
+
+ return parse(store, Mu::parse_query(expr, true/*expand*/), flags);
+}
+
+
+#ifdef BUILD_XAPIANIZE_QUERY
+int
+main (int argc, char *argv[])
+{
+ if (argc < 2) {
+ mu_printerrln("expected: parse-query <query>");
+ return 1;
+ }
+
+ auto store = Store::make(runtime_path(Mu::RuntimePath::XapianDb));
+ if (!store) {
+ mu_printerrln("error: {}", store.error());
+ return 2;
+ }
+
+ std::string expr;
+ for (auto i = 1; i < argc; ++i) {
+ expr += argv[i];
+ expr += " ";
+ }
+
+ if (auto&& query{make_xapian_query(*store, expr)}; !query) {
+ mu_printerrln("error: {}", query.error());
+ return 1;
+ } else
+ mu_println("mu: {}", query->get_description());
+
+ if (auto&& query{make_xapian_query(*store, expr, ParserFlags::XapianParser)}; !query) {
+ mu_printerrln("error: {}", query.error());
+ return 2;
+ } else
+ mu_println("xp: {}", query->get_description());
+
+ return 0;
+
+
+}
+#endif /*BUILD_XAPIANIZE_QUERY*/
+
+#if BUILD_TESTS
+/*
+ * Tests.
+ *
+ */
+
+#include "utils/mu-test-utils.hh"
+
+using TestCase = std::pair<std::string, std::string>;
+
+static void
+test_sexp()
+{
+ /* tail */
+ g_assert_false(!!tail(Sexp{}));
+ auto t = tail(Sexp{1,2,3});
+ g_assert_true(!!t && t->listp() && t->size() == 2);
+
+ /* head_symbol */
+ g_assert_false(!!head_symbol(Sexp{}));
+ assert_equal(head_symbol(Sexp{"foo"_sym, 1, 2}).value_or("bar"), "foo");
+
+ /* string_nth */
+ g_assert_false(!!string_nth(Sexp{}, 123));
+ g_assert_false(!!string_nth(Sexp{1, 2, 3}, 1));
+ assert_equal(string_nth(Sexp{"aap", "noot", "mies"}, 2).value_or("wim"), "mies");
+}
+
+
+static void
+test_xapian()
+{
+ allow_warnings();
+
+ auto&& testhome{unwrap(make_temp_dir())};
+ auto&& dbpath{runtime_path(RuntimePath::XapianDb, testhome)};
+ auto&& store{unwrap(Store::make_new(dbpath, join_paths(testhome, "test-maildir")))};
+
+ // Xapian internal format (get_description()) is _not_ guaranteed
+ // to be the same between versions
+ auto&& zz{make_xapian_query(store, R"(subject:"hello world")")};
+ assert_valid_result(zz);
+ /* LCOV_EXCL_START*/
+ if (zz->get_description() != R"(Query((Shello world OR (Shello PHRASE 2 Sworld))))") {
+ mu_println("{}", zz->get_description());
+ if (mu_test_mu_hacker()) {
+ // in the mu hacker case, we want to be warned if Xapian changed.
+ g_critical("xapian version mismatch");
+ g_assert_true(false);
+ } else {
+ g_test_skip("incompatible xapian descriptions");
+ return;
+ }
+ }
+ /* LCOV_EXCL_STOP*/
+
+ std::vector<TestCase> cases = {
+
+ TestCase{R"(i:87h766tzzz.fsf@gnus.org)", R"(Query(I87h766tzzz.fsf@gnus.org))"},
+ TestCase{R"(subject:foo to:bar)", R"(Query((Sfoo AND Tbar)))"},
+ TestCase{R"(subject:"cuux*")", R"(Query(WILDCARD SYNONYM Scuux))"},
+ TestCase{R"(subject:"hello world")",
+ R"(Query((Shello world OR (Shello PHRASE 2 Sworld))))"},
+ TestCase{R"(subject:/boo/")", R"(Query())"},
+
+ // logic
+ TestCase{R"(not)", R"(Query((Tnot OR Cnot OR Hnot OR Fnot OR Snot OR Bnot OR Enot)))"},
+ TestCase{R"(from:a and (from:b or from:c))", R"(Query((Fa AND (Fb OR Fc))))"},
+ // optimize?
+ TestCase{R"(not from:a and to:b)", R"(Query(((<alldocuments> AND_NOT Fa) AND Tb)))"},
+ TestCase{R"(cc:a not bcc:b)", R"(Query((Ca AND (<alldocuments> AND_NOT Hb))))"},
+
+ // ranges.
+ TestCase{R"(size:1..10")", R"(Query(VALUE_RANGE 17 g1 ga))"},
+ TestCase{R"(size:10..1")", R"(Query(VALUE_RANGE 17 g1 ga))"},
+ TestCase{R"(size:10..")", R"(Query(VALUE_GE 17 ga))"},
+ TestCase{R"(size:..10")", R"(Query(VALUE_LE 17 ga))"},
+ TestCase{R"(size:10")", R"(Query(VALUE_RANGE 17 ga ga))"}, // change?
+ TestCase{R"(size:..")", R"(Query())"},
+ };
+
+ for (auto&& test: cases) {
+ auto&& xq{make_xapian_query(store, test.first)};
+ assert_valid_result(xq);
+ assert_equal(xq->get_description(), test.second);
+ }
+
+ remove_directory(testhome);
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ Xapian::QueryParser qp;
+
+ g_test_add_func("/query-parser/sexp", test_sexp);
+ g_test_add_func("/query-parser/xapianizer", test_xapian);
+
+ return g_test_run();
+}
+
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2008-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include <mu-query.hh>
+
+#include <stdexcept>
+#include <string>
+#include <cctype>
+#include <cstring>
+#include <sstream>
+#include <cmath>
+
+#include <stdlib.h>
+#include <glib/gstdio.h>
+
+#include "mu-xapian-db.hh"
+#include "mu-query-results.hh"
+#include "mu-query-match-deciders.hh"
+#include "mu-query-threads.hh"
+
+#include "mu-query-parser.hh"
+
+using namespace Mu;
+
+struct Query::Private {
+ Private(const Store& store) :
+ store_{store},
+ parser_flags_{any_of(store_.message_options() & Message::Options::SupportNgrams) ?
+ ParserFlags::SupportNgrams : ParserFlags::None} {}
+
+ Xapian::Enquire make_enquire(const std::string& expr, Field::Id sortfield_id,
+ QueryFlags qflags) const;
+ Xapian::Enquire make_related_enquire(const StringSet& thread_ids,
+ Field::Id sortfield_id,
+ QueryFlags qflags) const;
+
+ Option<QueryResults> run_threaded(QueryResults&& qres, Xapian::Enquire& enq,
+ QueryFlags qflags, size_t max_size) const;
+ Option<QueryResults> run_singular(const std::string& expr,
+ Field::Id sortfield_id,
+ QueryFlags qflags, size_t maxnum) const;
+ Option<QueryResults> run_related(const std::string& expr,
+ Field::Id sortfield_id,
+ QueryFlags qflags, size_t maxnum) const;
+
+ Option<QueryResults> run(const std::string& expr,
+ Field::Id sortfield_id, QueryFlags qflags,
+ size_t maxnum) const;
+ const Store& store_;
+ const ParserFlags parser_flags_;
+};
+
+Query::Query(const Store& store) : priv_{std::make_unique<Private>(store)} {}
+
+Query::~Query() = default;
+
+static Xapian::Enquire&
+sort_enquire(Xapian::Enquire& enq, Field::Id sortfield_id, QueryFlags qflags)
+{
+ const auto value_no{field_from_id(sortfield_id).value_no()};
+ enq.set_sort_by_value(value_no, any_of(qflags & QueryFlags::Descending));
+
+ return enq;
+}
+
+static Xapian::Query
+make_query(const Store& store, const std::string& expr, ParserFlags parser_flags)
+{
+ if (expr.empty() || expr == R"("")")
+ return Xapian::Query::MatchAll;
+ else {
+ if (auto&& q{make_xapian_query(store, expr, parser_flags)}; !q) {
+ mu_warning("error in query '{}': {}", expr, q.error().what());
+ return Xapian::Query::MatchNothing;
+ } else
+ return q.value();
+ }
+}
+
+Xapian::Enquire
+Query::Private::make_enquire(const std::string& expr,
+ Field::Id sortfield_id,
+ QueryFlags qflags) const
+{
+ auto enq{store_.xapian_db().enquire()};
+ enq.set_query(make_query(store_, expr, parser_flags_));
+ sort_enquire(enq, sortfield_id, qflags);
+
+ return enq;
+}
+
+Xapian::Enquire
+Query::Private::make_related_enquire(const StringSet& thread_ids,
+ Field::Id sortfield_id,
+ QueryFlags qflags) const
+{
+ auto enq{store_.xapian_db().enquire()};
+ std::vector<Xapian::Query> qvec;
+ qvec.reserve(thread_ids.size());
+
+ for (auto&& t : thread_ids)
+ qvec.emplace_back(field_from_id(Field::Id::ThreadId).xapian_term(t));
+
+ Xapian::Query qr{Xapian::Query::OP_OR, qvec.begin(), qvec.end()};
+ enq.set_query(qr);
+
+ sort_enquire(enq, sortfield_id, qflags);
+
+ return enq;
+}
+
+struct ThreadKeyMaker : public Xapian::KeyMaker {
+ ThreadKeyMaker(const QueryMatches& matches) : match_info_(matches) {}
+ std::string operator()(const Xapian::Document& doc) const override {
+ const auto it{match_info_.find(doc.get_docid())};
+ return (it == match_info_.end()) ? "" : it->second.thread_path;
+ }
+ const QueryMatches& match_info_;
+};
+
+Option<QueryResults>
+Query::Private::run_threaded(QueryResults&& qres, Xapian::Enquire& enq, QueryFlags qflags,
+ size_t maxnum) const
+{
+ const auto descending{any_of(qflags & QueryFlags::Descending)};
+
+ calculate_threads(qres, descending);
+
+ ThreadKeyMaker key_maker{qres.query_matches()};
+ enq.set_sort_by_key(&key_maker, descending);
+
+ DeciderInfo minfo;
+ minfo.matches = qres.query_matches();
+ auto mset{enq.get_mset(0, maxnum, {}, make_thread_decider(qflags, minfo).get())};
+ mset.fetch();
+
+ return QueryResults{mset, std::move(qres.query_matches())};
+}
+
+Option<QueryResults>
+Query::Private::run_singular(const std::string& expr,
+ Field::Id sortfield_id,
+ QueryFlags qflags, size_t maxnum) const
+{
+ // i.e. a query _without_ related messages, but still possibly
+ // with threading.
+ //
+ // In the threading case, the sortfield-id is ignored, we always sort by
+ // date (since threading the threading results are always by date.)
+
+ const auto singular_qflags{qflags | QueryFlags::Leader};
+ const auto threading{any_of(qflags & QueryFlags::Threading)};
+
+ DeciderInfo minfo{};
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wextra"
+ auto enq{make_enquire(expr, threading ? Field::Id::Date : sortfield_id, qflags)};
+#pragma GCC diagnostic ignored "-Wswitch-default"
+#pragma GCC diagnostic pop
+ auto mset{enq.get_mset(0, maxnum, {},
+ make_leader_decider(singular_qflags, minfo).get())};
+ mset.fetch();
+
+ auto qres{QueryResults{mset, std::move(minfo.matches)}};
+
+ return threading ? run_threaded(std::move(qres), enq, qflags, maxnum) : qres;
+}
+
+static Option<std::string>
+opt_string(const Xapian::Document& doc, Field::Id id) noexcept
+{
+ const auto value_no{field_from_id(id).value_no()};
+ std::string val =
+ xapian_try([&] { return doc.get_value(value_no); }, std::string{""});
+ if (val.empty())
+ return Nothing;
+ else
+ return Some(std::move(val));
+}
+
+Option<QueryResults>
+Query::Private::run_related(const std::string& expr,
+ Field::Id sortfield_id,
+ QueryFlags qflags, size_t maxnum) const
+{
+ // i.e. a query _with_ related messages and possibly with threading.
+ //
+ // In the threading case, the sortfield-id is ignored, we always sort by
+ // date (since threading the threading results are always by date.);
+ // moreover, in either threaded or non-threaded case, we sort the first
+ // ("leader") query by date, i.e, we prefer the newest or oldest
+ // (descending) messages.
+ const auto leader_qflags{QueryFlags::Leader | qflags};
+ const auto threading{any_of(qflags & QueryFlags::Threading)};
+
+ // Run our first, "leader" query
+ DeciderInfo minfo{};
+ auto enq{make_enquire(expr, Field::Id::Date, leader_qflags)};
+ const auto mset{
+ enq.get_mset(0, maxnum, {}, make_leader_decider(leader_qflags, minfo).get())};
+
+ // Gather the thread-ids we found
+ mset.fetch();
+ minfo.thread_ids.reserve(mset.size());
+ for (auto it = mset.begin(); it != mset.end(); ++it)
+ if (auto thread_id{opt_string(it.get_document(), Field::Id::ThreadId)}; thread_id)
+ minfo.thread_ids.emplace(std::move(*thread_id));
+
+ // Now, determine the "related query".
+ //
+ // In the threaded-case, we search among _all_ messages, since complete
+ // threads are preferred; no need to sort in that case since the search
+ // is unlimited and the sorting happens during threading.
+ auto r_enq = std::invoke([&]{
+ if (threading)
+ return make_related_enquire(minfo.thread_ids, Field::Id::Date,
+ qflags);
+ else
+ return make_related_enquire(minfo.thread_ids, sortfield_id, qflags);
+ });
+
+ const auto r_mset{r_enq.get_mset(0, threading ? store_.size() : maxnum, {},
+ make_related_decider(qflags, minfo).get())};
+ auto qres{QueryResults{r_mset, std::move(minfo.matches)}};
+ return threading ? run_threaded(std::move(qres), r_enq, qflags, maxnum) : qres;
+}
+
+Option<QueryResults>
+Query::Private::run(const std::string& expr, Field::Id sortfield_id, QueryFlags qflags,
+ size_t maxnum) const
+{
+ const auto eff_maxnum{maxnum == 0 ? store_.size() : maxnum};
+
+ if (any_of(qflags & QueryFlags::IncludeRelated))
+ return run_related(expr, sortfield_id, qflags, eff_maxnum);
+ else
+ return run_singular(expr, sortfield_id, qflags, eff_maxnum);
+}
+
+Result<QueryResults>
+Query::run(const std::string& expr, Field::Id sortfield_id,
+ QueryFlags qflags, size_t maxnum) const
+{
+ // some flags are for internal use only.
+ g_return_val_if_fail(none_of(qflags & QueryFlags::Leader),
+ Err(Error::Code::InvalidArgument, "cannot pass Leader flag"));
+
+ StopWatch sw{
+ mu_format("query: '{}'; (related:{}; threads:{}; ngrams:{}; max-size:{})",
+ expr,
+ any_of(qflags & QueryFlags::IncludeRelated) ? "yes" : "no",
+ any_of(qflags & QueryFlags::Threading) ? "yes" : "no",
+ any_of(priv_->parser_flags_ & ParserFlags::SupportNgrams) ? "yes" : "no",
+ maxnum == 0 ? std::string{"∞"} : std::to_string(maxnum))};
+
+ return xapian_try_result([&]{
+ if (auto&& res = priv_->run(expr, sortfield_id, qflags, maxnum); res)
+ return Result<QueryResults>(Ok(std::move(res.value())));
+ else
+ return Result<QueryResults>(Err(Error::Code::Query,
+ "failed to run query"));
+ });
+}
+
+size_t
+Query::count(const std::string& expr) const
+{
+ return xapian_try(
+ [&] {
+ const auto enq{priv_->make_enquire(expr, {}, {})};
+ auto mset{enq.get_mset(0, priv_->store_.size())};
+ mset.fetch();
+ return mset.size();
+ },
+ 0);
+}
+
+/* LCOV_EXCL_START*/
+std::string
+Query::parse(const std::string& expr, bool xapian) const
+{
+ if (xapian)
+ return make_query(priv_->store_, expr,
+ priv_->parser_flags_).get_description();
+ else
+ return parse_query(expr).to_string();
+}
+/* LCOV_EXCL_STOP*/
--- /dev/null
+/*
+** Copyright (C) 2008-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef __MU_QUERY_HH__
+#define __MU_QUERY_HH__
+
+#include <memory>
+
+#include <glib.h>
+#include <mu-store.hh>
+#include <mu-query-results.hh>
+#include <utils/mu-utils.hh>
+#include <utils/mu-option.hh>
+#include <utils/mu-result.hh>
+#include <message/mu-message.hh>
+
+namespace Mu {
+
+class Query {
+public:
+ /**
+ * Run a query on the store
+ *
+ * @param expr the search expression
+ * @param sortfield_id the sortfield-id. Default to Date
+ * @param flags query flags
+ * @param maxnum maximum number of results to return. 0 for 'no limit'
+ *
+ * @return the query-results or an error
+ */
+ Result<QueryResults> run(const std::string& expr,
+ Field::Id sortfield_id = Field::Id::Date,
+ QueryFlags flags = QueryFlags::None,
+ size_t maxnum = 0) const;
+
+ /**
+ * run a Xapian query to count the number of matches; for the syntax, please
+ * refer to the mu-query manpage
+ *
+ * @param expr the search expression; use "" to match all messages
+ *
+ * @return the number of matches
+ */
+ size_t count(const std::string& expr = "") const;
+
+ /**
+ * For debugging, get the internal string representation of the parsed
+ * query
+ *
+ * @param expr a xapian search expression
+ * @param xapian if true, show Xapian's internal representation,
+ * otherwise, mu's.
+
+ * @return the string representation of the query
+ */
+ std::string parse(const std::string& expr, bool xapian) const;
+
+private:
+ friend class Store;
+
+ /**
+ * Construct a new Query instance.
+ *
+ * @param store a MuStore object
+ */
+ Query(const Store& store);
+ /**
+ * DTOR
+ *
+ */
+ ~Query();
+
+ /**
+ * Move CTOR
+ *
+ * @param other
+ */
+
+ struct Private;
+ std::unique_ptr<Private> priv_;
+};
+} // namespace Mu
+
+#endif /*__MU_QUERY_HH__*/
--- /dev/null
+/*
+** Copyright (C) 2020-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include "mu-scanner.hh"
+
+#include "config.h"
+
+#include <chrono>
+#include <mutex>
+#include <atomic>
+#include <thread>
+#include <cstring>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <glib.h>
+
+#include "utils/mu-utils.hh"
+#include "utils/mu-utils-file.hh"
+#include "utils/mu-error.hh"
+
+using namespace Mu;
+
+using Mode = Scanner::Mode;
+
+/*
+ * dentry->d_ino, dentry->d_type may not be available
+ */
+struct dentry_t {
+ dentry_t(const struct dirent *dentry):
+#if HAVE_DIRENT_D_INO
+ d_ino{dentry->d_ino},
+#endif /*HAVE_DIRENT_D_INO*/
+
+#if HAVE_DIRENT_D_TYPE
+ d_type(dentry->d_type),
+#endif /*HAVE_DIRENT_D_TYPE*/
+ d_name{static_cast<const char*>(dentry->d_name)} {}
+#if HAVE_DIRENT_D_INO
+ ino_t d_ino;
+#endif /*HAVE_DIRENT_D_INO*/
+
+#if HAVE_DIRENT_D_TYPE
+ unsigned char d_type;
+#endif /*HAVE_DIRENT_D_TYPE*/
+
+ std::string d_name;
+};
+
+struct Scanner::Private {
+ Private(const std::string& root_dir, Scanner::Handler handler, Mode mode):
+ root_dir_{root_dir}, handler_{handler}, mode_{mode} {
+ if (root_dir_.length() > PATH_MAX)
+ throw Mu::Error{Error::Code::InvalidArgument, "path is too long"};
+ if (!handler_)
+ throw Mu::Error{Error::Code::InvalidArgument, "missing handler"};
+ }
+ ~Private() { stop(); }
+
+ Result<void> start();
+ void stop();
+
+ bool process_dentry(const std::string& path, const dentry_t& dentry,
+ bool is_maildir);
+ bool process_dir(const std::string& path, bool is_maildir);
+
+ int lazy_stat(const char *fullpath, struct stat *stat_buf,
+ const dentry_t& dentry);
+
+ bool maildirs_only_mode() const { return mode_ == Mode::MaildirsOnly; }
+
+ const std::string root_dir_;
+ const Scanner::Handler handler_;
+ Mode mode_;
+ std::atomic<bool> running_{};
+ std::mutex lock_;
+};
+
+static bool
+ignore_dentry(const dentry_t& dentry)
+{
+ const auto d_name{dentry.d_name.c_str()};
+
+ /* dotdir? */
+ if (d_name[0] == '\0' || (d_name[1] == '\0' && d_name[0] == '.') ||
+ (d_name[2] == '\0' && d_name[0] == '.' && d_name[1] == '.'))
+ return true;
+
+ if (d_name[0] != 't' && d_name[0] != 'h' && d_name[0] != '.')
+ return false; /* don't ignore */
+
+ if (::strcmp(d_name, "tmp") == 0 || ::strcmp(d_name, "hcache.db") == 0)
+ return true; // ignore
+
+ if (d_name[0] == '.')
+ for (auto dname : { "nnmaildir", "notmuch", "noindex", "noupdate"})
+ if (::strcmp(d_name + 1, dname) == 0)
+ return true;
+
+ return false; /* don't ignore */
+}
+
+
+/*
+ * stat() if necessary (we'd like to avoid it), which we can if we only need the
+ * file-type and we already have that from the dentry.
+ */
+int
+Scanner::Private::lazy_stat(const char *path, struct stat *stat_buf, const dentry_t& dentry)
+{
+#if HAVE_DIRENT_D_TYPE
+ if (maildirs_only_mode()) {
+ switch (dentry.d_type) {
+ case DT_REG:
+ stat_buf->st_mode = S_IFREG;
+ return 0;
+ case DT_DIR:
+ stat_buf->st_mode = S_IFDIR;
+ return 0;
+ default:
+ /* LNK is inconclusive; we need a stat. */
+ break;
+ }
+ }
+#endif /*HAVE_DIRENT_D_TYPE*/
+
+ int res = ::stat(path, stat_buf);
+ if (res != 0)
+ mu_warning("failed to stat {}: {}", path, g_strerror(errno));
+
+ return res;
+}
+
+
+bool
+Scanner::Private::process_dentry(const std::string& path, const dentry_t& dentry,
+ bool is_maildir)
+{
+ if (ignore_dentry(dentry))
+ return true;
+
+ auto call_handler=[&](auto&& path, auto&& statbuf, auto&& htype)->bool {
+ return maildirs_only_mode() ? true : handler_(path, statbuf, htype);
+ };
+
+ const auto fullpath{join_paths(path, dentry.d_name)};
+ struct stat statbuf{};
+ if (lazy_stat(fullpath.c_str(), &statbuf, dentry) != 0)
+ return false;
+
+ if (maildirs_only_mode() && S_ISDIR(statbuf.st_mode) && dentry.d_name == "cur") {
+ handler_(path/*without cur*/, {}, Scanner::HandleType::Maildir);
+ return true; // found maildir; no need to recurse further.
+ }
+
+ if (S_ISDIR(statbuf.st_mode)) {
+ const auto new_cur = dentry.d_name == "cur" || dentry.d_name == "new";
+ const auto htype =
+ new_cur ?
+ Scanner::HandleType::EnterNewCur :
+ Scanner::HandleType::EnterDir;
+
+ const auto res = call_handler(fullpath, &statbuf, htype);
+ if (!res)
+ return true; // skip
+
+ process_dir(fullpath, new_cur);
+ return call_handler(fullpath, &statbuf, Scanner::HandleType::LeaveDir);
+
+ } else if (S_ISREG(statbuf.st_mode) && is_maildir)
+ return call_handler(fullpath, &statbuf, Scanner::HandleType::File);
+
+ mu_debug("skip {} (neither maildir-file nor directory)", fullpath);
+
+ return true;
+}
+
+bool
+Scanner::Private::process_dir(const std::string& path, bool is_maildir)
+{
+ if (!running_)
+ return true; /* we're done */
+
+ if (G_UNLIKELY(path.length() > PATH_MAX)) {
+ // note: unlikely to hit this, one case would be a self-referential
+ // symlink; that should be caught earlier, so this is just a backstop.
+ mu_warning("path is too long: {}", path);
+ return false;
+ }
+
+ const auto dir{::opendir(path.c_str())};
+ if (G_UNLIKELY(!dir)) {
+ mu_warning("failed to scan dir {}: {}", path, g_strerror(errno));
+ return false;
+ }
+
+ std::vector<dentry_t> dir_entries;
+ while (running_) {
+ errno = 0;
+ if (const auto& dentry{::readdir(dir)}; dentry) {
+#if HAVE_DIRENT_D_TYPE /* optimization: filter out non-dirs early. NB not all file-systems support
+ * returning the file-type in `d_type`, so don't skip `DT_UNKNOWN`.
+ */
+ if (maildirs_only_mode() &&
+ dentry->d_type != DT_DIR &&
+ dentry->d_type != DT_LNK &&
+ dentry->d_type != DT_UNKNOWN)
+ continue;
+#endif /*HAVE_DIRENT_D_TYPE*/
+ dir_entries.emplace_back(dentry);
+ continue;
+ } else if (errno != 0) {
+ mu_warning("failed to read {}: {}", path, g_strerror(errno));
+ continue;
+ }
+
+ break;
+ }
+ ::closedir(dir);
+
+#if HAVE_DIRENT_D_INO
+ // sort by i-node; much faster on rotational (HDDs) devices and on SSDs
+ // sort is quick enough to not matter much
+ std::sort(dir_entries.begin(), dir_entries.end(),
+ [](auto&& d1, auto&& d2){ return d1.d_ino < d2.d_ino; });
+#endif /*HAVEN_DIRENT_D_INO*/
+
+ // now process...
+ for (auto&& dentry: dir_entries)
+ process_dentry(path, dentry, is_maildir);
+
+ return true;
+}
+
+Result<void>
+Scanner::Private::start()
+{
+ const auto mode{F_OK | R_OK};
+ if (G_UNLIKELY(::access(root_dir_.c_str(), mode) != 0))
+ return Err(Error::Code::File, "'{}' is not readable: {}", root_dir_,
+ g_strerror(errno));
+
+ struct stat statbuf {};
+ if (G_UNLIKELY(::stat(root_dir_.c_str(), &statbuf) != 0))
+ return Err(Error::Code::File, "'{}' is not stat'able: {}",
+ root_dir_, g_strerror(errno));
+
+ if (G_UNLIKELY(!S_ISDIR(statbuf.st_mode)))
+ return Err(Error::Code::File,
+ "'{}' is not a directory", root_dir_);
+
+ running_ = true;
+ mu_debug("starting scan @ {}", root_dir_);
+
+ const auto bname{basename(root_dir_)};
+ const auto is_maildir = bname == "cur" || bname == "new";
+
+ const auto start{std::chrono::steady_clock::now()};
+ process_dir(root_dir_, is_maildir);
+ const auto elapsed = std::chrono::steady_clock::now() - start;
+ mu_debug("finished scan of {} in {} ms", root_dir_, to_ms(elapsed));
+ running_ = false;
+
+ return Ok();
+}
+
+void
+Scanner::Private::stop()
+{
+ if (running_) {
+ mu_debug("stopping scan");
+ running_ = false;
+ }
+}
+
+Scanner::Scanner(const std::string& root_dir, Scanner::Handler handler, Mode flavor)
+ : priv_{std::make_unique<Private>(root_dir, handler, flavor)}
+{}
+
+Scanner::~Scanner() = default;
+
+Result<void>
+Scanner::start()
+{
+ if (priv_->running_)
+ return Ok(); // nothing to do
+
+ auto res = priv_->start(); /* blocks */
+ priv_->running_ = false;
+
+ return res;
+}
+
+void
+Scanner::stop()
+{
+ std::lock_guard l(priv_->lock_);
+ priv_->stop();
+}
+
+bool
+Scanner::is_running() const
+{
+ return priv_->running_;
+}
+
+
+#if BUILD_TESTS
+/* LCOV_EXCL_START*/
+#include "mu-test-utils.hh"
+
+static void
+test_scan_maildirs()
+{
+ allow_warnings();
+
+ size_t count{};
+ Scanner scanner{
+ MU_TESTMAILDIR,
+ [&](const std::string& fullpath, const struct stat* statbuf, auto&& htype) -> bool {
+ ++count;
+ g_usleep(10000);
+ return true;
+ }};
+ assert_valid_result(scanner.start());
+ scanner.stop();
+ count = 0;
+ assert_valid_result(scanner.start());
+
+ while (scanner.is_running()) { g_usleep(100000); }
+
+ // very rudimentary test...
+ g_assert_cmpuint(count,==,23);
+}
+
+static void
+test_count_maildirs()
+{
+ allow_warnings();
+
+ std::vector<std::string> dirs;
+ Scanner scanner{
+ MU_TESTMAILDIR2,
+ [&](const std::string& fullpath, const struct stat* statbuf, auto&& htype) -> bool {
+ dirs.emplace_back(basename(fullpath));
+ return true;
+ }, Scanner::Mode::MaildirsOnly};
+ assert_valid_result(scanner.start());
+
+ while (scanner.is_running()) { g_usleep(1000); }
+
+ g_assert_cmpuint(dirs.size(),==,3);
+ g_assert_true(seq_find_if(dirs, [](auto& p){return p == "bar";}) != dirs.end());
+ g_assert_true(seq_find_if(dirs, [](auto& p){return p == "Foo";}) != dirs.end());
+ g_assert_true(seq_find_if(dirs, [](auto& p){return p == "wom_bat";}) != dirs.end());
+}
+
+static void
+test_fail_nonexistent()
+{
+ allow_warnings();
+
+ Scanner scanner{"/foo/bar/non-existent",
+ [&](auto&& a1, auto&& a2, auto&& a3){ return false; }};
+ g_assert_false(scanner.is_running());
+ g_assert_false(!!scanner.start());
+ g_assert_false(scanner.is_running());
+}
+
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/scanner/scan-maildirs", test_scan_maildirs);
+ g_test_add_func("/scanner/count-maildirs", test_count_maildirs);
+ g_test_add_func("/scanner/fail-nonexistent", test_fail_nonexistent);
+
+ return g_test_run();
+}
+#endif /*BUILD_TESTS*/
+
+#if BUILD_LIST_MAILDIRS
+
+static bool
+on_path(const std::string& path, struct stat* statbuf, Scanner::HandleType htype)
+{
+ mu_println("{}", path);
+ return true;
+}
+
+int
+main (int argc, char *argv[])
+{
+ if (argc < 2) {
+ mu_printerrln("expected: path to maildir");
+ return 1;
+ }
+
+ Scanner scanner{argv[1], on_path, Mode::MaildirsOnly};
+
+ scanner.start();
+
+ return 0;
+}
+/* LCOV_EXCL_STOP*/
+#endif /*BUILD_LIST_MAILDIRS*/
--- /dev/null
+/*
+** Copyright (C) 2020-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_SCANNER_HH__
+#define MU_SCANNER_HH__
+
+#include <functional>
+#include <memory>
+#include <utils/mu-result.hh>
+
+#include <dirent.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+namespace Mu {
+
+/**
+ * @brief Maildir scanner
+ *
+ * Scans maildir (trees) recursively, and calls the Handler callback for
+ * directories & files.
+ *
+ * It filters out (i.e., does *not* call the handler for):
+ * - files starting with '.'
+ * - files that do not live in a cur / new leaf maildir
+ * - directories '.' and '..' and 'tmp'
+*/
+class Scanner {
+ public:
+ enum struct HandleType {
+ /*
+ * Mode: All
+ */
+ File,
+ EnterNewCur, /* cur/ or new/ */
+ EnterDir, /* some other directory */
+ LeaveDir,
+ /*
+ * Mode: Maildir
+ */
+ Maildir,
+ };
+
+ /**
+ * Callback handler function
+ *
+ * path: full file-system path
+ * statbuf: stat result or nullptr (for Mode::MaildirsOnly)
+ * htype: HandleType. For Mode::MaildirsOnly only Maildir
+ */
+ using Handler = std::function<
+ bool(const std::string& path, struct stat* statbuf, HandleType htype)>;
+
+ /**
+ * Running mode for this Scanner
+ */
+ enum struct Mode {
+ All, /**< Vanilla */
+ MaildirsOnly /**< Only return maildir to handler */
+ };
+
+ /**
+ * Construct a scanner object for scanning a directory, recursively.
+ *
+ * If handler is a directory
+ *
+ * @param root_dir root dir to start scanning
+ * @param handler handler function for some direntry
+ * @param options options to influence behavior
+ */
+ Scanner(const std::string& root_dir, Handler handler, Mode mode = Mode::All);
+
+ /**
+ * DTOR
+ */
+ ~Scanner();
+
+ /**#
+ * Start the scan; this is a blocking call than runs until
+ * finished or (from another thread) stop() is called.
+ *
+ * @return Ok if starting worked; an Error otherwise
+ */
+ Result<void> start();
+
+ /**
+ * Request stopping the scan if it's running; otherwise do nothing
+ */
+ void stop();
+
+ /**
+ * Is a scan currently running?
+ *
+ * @return true or false
+ */
+ bool is_running() const;
+
+private:
+ struct Private;
+ std::unique_ptr<Private> priv_;
+};
+
+} // namespace Mu
+
+#endif /* MU_SCANNER_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2022-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include "config.h"
+
+#include "mu-script.hh"
+#include "mu/mu-options.hh"
+#include "utils/mu-utils.hh"
+#include "utils/mu-option.hh"
+
+#include <fstream>
+#include <iostream>
+
+#ifdef BUILD_GUILE
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wredundant-decls"
+#include <libguile.h>
+#pragma GCC diagnostic pop
+#endif /*BUILD_GUILE*/
+
+using namespace Mu;
+
+static std::string
+get_name(const std::string& path)
+{
+ auto pos = path.find_last_of("/");
+ if (pos == std::string::npos)
+ return path;
+
+ auto name = path.substr(pos + 1);
+
+ pos = name.find_last_of(".");
+ if (pos == std::string::npos)
+ return name;
+
+ return name.substr(0, pos);
+}
+
+
+static Mu::Option<Mu::ScriptInfo>
+get_info(std::string&& path, const std::string& prefix)
+{
+ std::ifstream file{path};
+ if (!file.is_open()) {
+ mu_warning ("failed to open {}", path);
+ return Nothing;
+ }
+
+ Mu::ScriptInfo info{};
+ info.path = path;
+ info.name = get_name(path);
+
+ std::string line;
+ while (std::getline(file, line)) {
+
+ if (line.find(prefix) != 0)
+ continue;
+
+ line = line.substr(prefix.length());
+
+ if (info.oneline.empty())
+ info.oneline = line;
+ else
+ info.description += line;
+ }
+
+ // std::cerr << "ONELINE: " << info.oneline << '\n';
+ // std::cerr << "DESCR : " << info.description << '\n';
+
+ return info;
+}
+
+
+
+static void
+script_infos_in_dir(const std::string& scriptdir, Mu::ScriptInfos& infos)
+{
+ DIR *dir = opendir(scriptdir.c_str());
+ if (!dir) {
+ mu_debug("failed to open '{}': {}", scriptdir,
+ g_strerror(errno));
+ return;
+ }
+
+ const std::string ext{".scm"};
+
+ struct dirent *dentry;
+ while ((dentry = readdir(dir))) {
+
+ if (!g_str_has_suffix(dentry->d_name, ext.c_str()))
+ continue;
+
+ auto&& info = get_info(scriptdir + "/" + dentry->d_name, ";; INFO: ");
+ if (!info)
+ continue;
+
+ infos.emplace_back(std::move(*info));
+ }
+
+ closedir(dir); /* ignore error checking... */
+}
+
+
+Mu::ScriptInfos
+Mu::script_infos(const Mu::ScriptPaths& paths)
+{
+ /* create a list of names, paths */
+ ScriptInfos infos;
+ for (auto&& dir: paths) {
+ script_infos_in_dir(dir, infos);
+ }
+
+ std::sort(infos.begin(), infos.end(), [](auto&& i1, auto&& i2) {
+ return i1.name < i2.name;
+ });
+
+ return infos;
+}
+
+Result<void>
+Mu::run_script(const std::string& path,
+ const std::vector<std::string>& args)
+{
+#ifndef BUILD_GUILE
+ return Err(Error::Code::Script,
+ "guile script support is not available");
+#else
+ std::string mainargs;
+ for (auto&& arg: args)
+ mainargs += mu_format("{}\"{}\"", mainargs.empty() ? "" : " ", arg);
+ auto expr = mu_format("(main '(\"{}\" {}))", get_name(path), mainargs);
+
+ std::vector<const char*> argv = {
+ GUILE_BINARY,
+ "-l", path.c_str(),
+ "-c", expr.c_str(),
+ };
+
+ /* does not return */
+ scm_boot_guile(argv.size(), const_cast<char**>(argv.data()),
+ [](void *closure, int argc, char **argv) {
+ scm_shell(argc, argv);
+ }, NULL);
+
+ return Ok();
+#endif /*BUILD_GUILE*/
+}
--- /dev/null
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#ifndef MU_SCRIPT_HH__
+#define MU_SCRIPT_HH__
+
+#include <string>
+#include <vector>
+
+#include <utils/mu-result.hh>
+
+namespace Mu {
+
+/**
+ * Information about a script.
+ *
+ */
+struct ScriptInfo {
+ std::string name; /**< Name of script */
+ std::string path; /**< Full path to script */
+ std::string oneline; /**< One-line description */
+ std::string description; /**< More help */
+};
+
+/// Sequence of script infos.
+using ScriptInfos = std::vector<ScriptInfo>;
+
+/**
+ * Get information about the available scripts
+ *
+ * @return infos
+ */
+using ScriptPaths = std::vector<std::string>;
+ScriptInfos script_infos(const ScriptPaths& paths);
+
+
+/**
+ * Run some specific script
+ *
+ * @param path full path to the scripts
+ * @param args argument vector to pass to the script
+ *
+ * @return Ok() or some error; however, note that this does not return after succesfully
+ * starting a script.
+ */
+Result<void> run_script(const std::string& path, const std::vector<std::string>& args);
+
+} // namepace Mu
+
+#endif /* MU_SCRIPT_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2020-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+#include "mu-server.hh"
+
+#include "message/mu-message.hh"
+
+#include <fstream>
+#include <sstream>
+#include <string>
+#include <algorithm>
+#include <atomic>
+#include <thread>
+#include <mutex>
+#include <variant>
+#include <functional>
+
+#include <cstring>
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <unistd.h>
+
+#include "mu-maildir.hh"
+#include "mu-query.hh"
+#include "mu-store.hh"
+
+#include "utils/mu-utils.hh"
+#include "utils/mu-utils-file.hh"
+
+#include "utils/mu-option.hh"
+#include "utils/mu-command-handler.hh"
+#include "utils/mu-readline.hh"
+
+using namespace Mu;
+
+/* LCOV_EXCL_START */
+
+/// output stream to _either_ a file or to a stringstream
+struct OutputStream {
+ /**
+ * Construct an OutputStream for a tempfile
+ *
+ * @param tmp_dir dir for temp files
+ */
+ OutputStream(const std::string& tmp_dir):
+ fname_{join_paths(tmp_dir,
+ mu_format("mu-{}.eld", g_get_monotonic_time()))},
+ out_{std::ofstream{fname_}} {
+ if (!out().good())
+ throw Mu::Error{Error::Code::File, "failed to create temp-file"};
+ }
+ /**
+ * Construct an OutputStream for a stringstream
+ *
+ * @param cdr name of the output (e.g., "contacts")
+ *
+ * @return
+ */
+ OutputStream(): out_{std::ostringstream{}} {}
+
+ /**
+ * Get a writable ostream
+ *
+ * @return an ostream
+ */
+ std::ostream& out() {
+ if (std::holds_alternative<std::ofstream>(out_))
+ return std::get<std::ofstream>(out_);
+ else
+ return std::get<std::ostringstream>(out_);
+ }
+
+ /// conversion
+ operator std::ostream&() { return out(); }
+
+ /**
+ * Get the output as a string, either something like, either a lisp form
+ * or a the full path to a temp file containing the same.
+ *
+ * @return lisp form or path
+ */
+ std::string to_string() const {
+ return std::holds_alternative<std::ostringstream>(out_) ?
+ std::get<std::ostringstream>(out_).str() :
+ quote(fname_);
+ }
+
+ /**
+ * Delete file, if any. Only do this when the OutputStream is no
+ * longer needed.
+ */
+ void unlink () {
+ if (fname_.empty())
+ return;
+ if (auto&&res{::unlink(fname_.c_str())}; res != 0)
+ mu_warning("failed to unlink '{}'", ::strerror(res));
+ else
+ mu_debug("unlinked output-stream {}", fname_);
+ }
+
+private:
+ std::string fname_;
+ using OutType = std::variant<std::ofstream, std::ostringstream>;
+ OutType out_;
+};
+
+
+
+/// @brief object to manage the server-context for all commands.
+struct Server::Private {
+ Private(Store& store, const Server::Options& opts, Output output)
+ : store_{store}, options_{opts}, output_{output},
+ command_handler_{make_command_map()},
+ keep_going_{true},
+ tmp_dir_{unwrap(make_temp_dir())}
+ {}
+
+ ~Private() {
+ indexer().stop();
+ if (index_thread_.joinable())
+ index_thread_.join();
+ if (!tmp_dir_.empty())
+ remove_directory(tmp_dir_);
+ }
+ //
+ // construction helpers
+ //
+ CommandHandler::CommandInfoMap make_command_map();
+
+ //
+ // acccessors
+ Store& store() { return store_; }
+ const Store& store() const { return store_; }
+ Indexer& indexer() { return store().indexer(); }
+ //CommandMap& command_map() const { return command_map_; }
+
+ //
+ // invoke
+ //
+ bool invoke(const std::string& expr) noexcept;
+
+ //
+ // output
+ void output(const std::string& str, Server::OutputFlags flags = {}) const {
+ if (output_)
+ output_(str, flags);
+ }
+ void output_sexp(const Sexp& sexp, Server::OutputFlags flags = {}) const {
+ output(sexp.to_string(), flags);
+ }
+
+ size_t output_results(const QueryResults& qres, size_t batch_size) const;
+
+ //
+ // handlers for various commands.
+ //
+ void add_handler(const Command& cmd);
+ void compose_handler(const Command& cmd);
+ void contacts_handler(const Command& cmd);
+ void data_handler(const Command& cmd);
+ void find_handler(const Command& cmd);
+ void help_handler(const Command& cmd);
+ void index_handler(const Command& cmd);
+ void move_handler(const Command& cmd);
+ void mkdir_handler(const Command& cmd);
+ void ping_handler(const Command& cmd);
+ void queries_handler(const Command& cmd);
+ void quit_handler(const Command& cmd);
+ void remove_handler(const Command& cmd);
+ void view_handler(const Command& cmd);
+
+private:
+ void move_docid(Store::Id docid, Option<std::string> flagstr,
+ bool new_name, bool no_view);
+
+ void perform_move(Store::Id docid,
+ const Message& msg,
+ const std::string& maildirarg,
+ Flags flags,
+ bool new_name,
+ bool no_view);
+
+ void view_mark_as_read(Store::Id docid, Message&& msg, bool rename);
+
+ OutputStream make_output_stream() const {
+ if (options_.allow_temp_file)
+ return OutputStream{tmp_dir_};
+ else
+ return OutputStream{};
+ }
+
+ std::ofstream make_temp_file_stream(std::string& fname) const;
+
+ Store& store_;
+ Server::Options options_;
+ Server::Output output_;
+ const CommandHandler command_handler_;
+ std::atomic<bool> keep_going_{};
+ std::thread index_thread_;
+ std::string tmp_dir_;
+};
+
+static void
+append_metadata(std::string& str, const QueryMatch& qmatch)
+{
+ const auto td{::atoi(qmatch.thread_date.c_str())};
+
+ str += mu_format(" :meta (:path \"{}\" :level {} :date \"{}\" "
+ ":data-tstamp ({} {} 0)",
+ qmatch.thread_path,
+ qmatch.thread_level,
+ qmatch.thread_date,
+ static_cast<unsigned>(td >> 16),
+ static_cast<unsigned>(td & 0xffff));
+
+ if (qmatch.has_flag(QueryMatch::Flags::Root))
+ str += " :root t";
+ if (qmatch.has_flag(QueryMatch::Flags::Related))
+ str += " :related t";
+ if (qmatch.has_flag(QueryMatch::Flags::First))
+ str += " :first-child t";
+ if (qmatch.has_flag(QueryMatch::Flags::Last))
+ str += " :last-child t";
+ if (qmatch.has_flag(QueryMatch::Flags::Orphan))
+ str += " :orphan t";
+ if (qmatch.has_flag(QueryMatch::Flags::Duplicate))
+ str += " :duplicate t";
+ if (qmatch.has_flag(QueryMatch::Flags::HasChild))
+ str += " :has-child t";
+ if (qmatch.has_flag(QueryMatch::Flags::ThreadSubject))
+ str += " :thread-subject t";
+
+ str += ')';
+}
+
+/*
+ * A message here consists of a message s-expression with optionally a :docid
+ * and/or :meta expression added.
+ *
+ * We could parse the sexp and use the Sexp APIs to add some things... but...
+ * it's _much_ faster to directly work on the string representation: remove the
+ * final ')', add a few items, and add the ')' again.
+ */
+static std::string
+msg_sexp_str(const Message& msg, Store::Id docid, const Option<QueryMatch&> qm)
+{
+ auto&& sexpstr{msg.document().sexp_str()};
+
+ if (docid != 0 || qm) {
+ sexpstr.reserve(sexpstr.size () + (docid == 0 ? 0 : 16) + (qm ? 64 : 0));
+
+ // remove the closing ( ... )
+ sexpstr.erase(sexpstr.end() - 1);
+
+ if (docid != 0)
+ sexpstr += " :docid " + to_string(docid);
+ if (qm)
+ append_metadata(sexpstr, *qm);
+
+ sexpstr += ')'; // ... end close it again.
+ }
+
+ return sexpstr;
+}
+
+
+CommandHandler::CommandInfoMap
+Server::Private::make_command_map()
+{
+ CommandHandler::CommandInfoMap cmap;
+
+ using CommandInfo = CommandHandler::CommandInfo;
+ using ArgMap = CommandHandler::ArgMap;
+ using ArgInfo = CommandHandler::ArgInfo;
+ using Type = Sexp::Type;
+ using Type = Sexp::Type;
+
+ cmap.emplace(
+ "add",
+ CommandInfo{
+ ArgMap{{":path", ArgInfo{Type::String, true, "file system path to the message"}}},
+ "add a message to the store",
+ [&](const auto& params) { add_handler(params); }});
+
+ cmap.emplace(
+ "contacts",
+ CommandInfo{
+ ArgMap{{":personal", ArgInfo{Type::Symbol, false, "only personal contacts"}},
+ {":after",
+ ArgInfo{Type::String, false, "only contacts seen after time_t string"}},
+ {":tstamp", ArgInfo{Type::String, false, "return changes since tstamp"}},
+ {":maxnum", ArgInfo{Type::Number, false, "max number of contacts to return"}}
+ },
+ "get contact information",
+ [&](const auto& params) { contacts_handler(params); }});
+
+ cmap.emplace(
+ "data",
+ CommandInfo{
+ ArgMap{{":kind", ArgInfo{Type::Symbol, true, "kind of data (maildirs)"}}},
+ "request data of some kind",
+ [&](const auto& params) { data_handler(params); }});
+
+ cmap.emplace(
+ "find",
+ CommandInfo{
+ ArgMap{{":query", ArgInfo{Type::String, true, "search expression"}},
+ {":threads",
+ ArgInfo{Type::Symbol, false, "whether to include threading information"}},
+ {":sortfield", ArgInfo{Type::Symbol, false, "the field to sort results by"}},
+ {":descending",
+ ArgInfo{Type::Symbol, false, "whether to sort in descending order"}},
+ {":batch-size", ArgInfo{Type::Number, false, "batch size for result"}},
+ {":maxnum", ArgInfo{Type::Number, false, "maximum number of result (hint)"}},
+ {":skip-dups",
+ ArgInfo{Type::Symbol,
+ false,
+ "whether to skip messages with duplicate message-ids"}},
+ {":include-related",
+ ArgInfo{Type::Symbol,
+ false,
+ "whether to include other message related to matching ones"}}},
+ "query the database for messages",
+ [&](const auto& params) { find_handler(params); }});
+
+ cmap.emplace(
+ "help",
+ CommandInfo{
+ ArgMap{{":command", ArgInfo{Type::Symbol, false, "command to get information for"}},
+ {":full", ArgInfo{Type::Symbol, false, "show full descriptions"}}},
+ "get information about one or all commands",
+ [&](const auto& params) { help_handler(params); }});
+ cmap.emplace(
+ "index",
+ CommandInfo{
+ ArgMap{{":my-addresses", ArgInfo{Type::List, false, "list of 'my' addresses"}},
+ {":cleanup",
+ ArgInfo{Type::Symbol,
+ false,
+ "whether to remove stale messages from the store"}},
+ {":lazy-check",
+ ArgInfo{Type::Symbol,
+ false,
+ "whether to avoid indexing up-to-date directories"}}},
+ "scan maildir for new/updated/removed messages",
+ [&](const auto& params) { index_handler(params); }});
+ cmap.emplace(
+ "mkdir",
+ CommandInfo{
+ ArgMap{
+ {":path", ArgInfo{Type::String, true, "location for the new maildir"}},
+ {":update", ArgInfo{Type::Symbol, false,
+ "whether to send an update after creating"}}
+ }, "create a new maildir",
+ [&](const auto& params) { mkdir_handler(params); }});
+ cmap.emplace(
+ "move",
+ CommandInfo{
+ ArgMap{
+ {":docid", ArgInfo{Type::Number, false, "document-id"}},
+ {":msgid", ArgInfo{Type::String, false, "message-id"}},
+ {":flags", ArgInfo{Type::String, false, "new flags for the message"}},
+ {":maildir", ArgInfo{Type::String, false, "the target maildir"}},
+ {":rename", ArgInfo{Type::Symbol, false, "change filename when moving"}},
+ {":no-view",
+ ArgInfo{Type::Symbol, false, "if set, do not hint at updating the view"}},
+ },
+ "move messages and/or change their flags",
+ [&](const auto& params) { move_handler(params); }});
+ cmap.emplace(
+ "ping",
+ CommandInfo{
+ ArgMap{},
+ "ping the mu-server and get server information in the response",
+ [&](const auto& params) { ping_handler(params); }});
+
+ cmap.emplace(
+ "queries",
+ CommandInfo{
+ ArgMap{
+ {":queries",
+ ArgInfo{Type::List, false, "queries for which to get read/unread numbers"}},
+ },
+ "get unread/totals information for a list of queries",
+ [&](const auto& params) { queries_handler(params); }});
+
+ cmap.emplace("quit", CommandInfo{{}, "quit the mu server", [&](const auto& params) {
+ quit_handler(params);
+ }});
+
+ cmap.emplace(
+ "remove",
+ CommandInfo{
+ ArgMap{{":docid",
+ ArgInfo{Type::Number, false, "document-id for the message to remove"}},
+ {":path",
+ ArgInfo{Type::String, false, "document-id for the message to remove"}}
+ },
+ "remove a message from filesystem and database, using either :docid or :path",
+ [&](const auto& params) { remove_handler(params); }});
+
+ cmap.emplace(
+ "view",
+ CommandInfo{ArgMap{
+ {":docid", ArgInfo{Type::Number, false, "document-id"}},
+ {":msgid", ArgInfo{Type::String, false, "message-id"}},
+ {":path", ArgInfo{Type::String, false, "message filesystem path"}},
+ {":mark-as-read",
+ ArgInfo{Type::Symbol, false, "mark message as read (if not already)"}},
+ {":rename", ArgInfo{Type::Symbol, false, "change filename when moving"}},
+ },
+ "view a message. exactly one of docid/msgid/path must be specified",
+ [&](const auto& params) { view_handler(params); }});
+ return cmap;
+}
+
+bool
+Server::Private::invoke(const std::string& expr) noexcept
+{
+ auto make_error=[](auto&& code, auto&& msg) {
+ return Sexp().put_props(
+ ":error", Error::error_number(code),
+ ":message", msg);
+ };
+
+ if (!keep_going_)
+ return false;
+ try {
+ auto cmd{Command::make_parse(std::string{expr})};
+ if (!cmd)
+ throw cmd.error();
+
+ auto res = command_handler_.invoke(*cmd);
+ if (!res)
+ throw res.error();
+
+ } catch (const Mu::Error& me) {
+ output_sexp(make_error(me.code(), mu_format("{}",
+ me.what())));
+ keep_going_ = true;
+ } catch (const Xapian::Error& xerr) {
+ output_sexp(make_error(Error::Code::Internal,
+ mu_format("xapian error: {}: {}",
+ xerr.get_type(), xerr.get_description())));
+ keep_going_ = false;
+ } catch (const std::runtime_error& re) {
+ output_sexp(make_error(Error::Code::Internal,
+ mu_format("caught runtime exception: {}",
+ re.what())));
+ keep_going_ = false;
+ } catch (const std::out_of_range& oore) {
+ output_sexp(make_error(Error::Code::Internal,
+ mu_format("caught out-of-range exception: {}",
+ oore.what())));
+ keep_going_ = false;
+ } catch (const std::exception& e) {
+ output_sexp(make_error(Error::Code::Internal,
+ mu_format(" exception: {}", e.what())));
+ keep_going_ = false;
+ } catch (...) {
+ output_sexp(make_error(Error::Code::Internal,
+ mu_format("something went wrong: quitting")));
+ keep_going_ = false;
+ }
+
+ return keep_going_;
+}
+
+/* 'add' adds a message to the database, and takes two parameters: 'path', which
+ * is the full path to the message, and 'maildir', which is the maildir this
+ * message lives in (e.g. "/inbox").
+ *
+ * responds with an (added . <message sexp>) forr the new message
+ */
+void
+Server::Private::add_handler(const Command& cmd)
+{
+ auto path{cmd.string_arg(":path")};
+ const auto docid_res{store().add_message(*path)};
+
+ if (!docid_res)
+ throw docid_res.error();
+
+ const auto docid{docid_res.value()};
+ output_sexp(Sexp().put_props(":info", "add"_sym,
+ ":path", *path,
+ ":docid", docid));
+
+ auto msg_res{store().find_message(docid)};
+ if (!msg_res)
+ throw Error(Error::Code::Store,
+ "failed to get message at {} (docid={})", *path, docid);
+
+ output(mu_format("(:update {})",
+ msg_sexp_str(msg_res.value(), docid, {})));
+}
+
+void
+Server::Private::contacts_handler(const Command& cmd)
+{
+ const auto personal = cmd.boolean_arg(":personal");
+ const auto afterstr = cmd.string_arg(":after").value_or("");
+ const auto tstampstr = cmd.string_arg(":tstamp").value_or("");
+ const auto maxnum = cmd.number_arg(":maxnum").value_or(0 /*unlimited*/);
+
+ const auto after{afterstr.empty() ? 0 : parse_date_time(afterstr, true).value_or(0)};
+ const auto tstamp = g_ascii_strtoll(tstampstr.c_str(), NULL, 10);
+
+ mu_debug("find {} contacts last seen >= {:%c} (tstamp: {})",
+ personal ? "personal" : "any", mu_time(after), tstamp);
+
+ auto match_contact = [&](const Contact& ci)->bool {
+ if (ci.tstamp < tstamp)
+ return false; /* already seen? */
+ else if (personal && !ci.personal)
+ return false; /* not personal? */
+ else if (ci.message_date < after)
+ return false; /* too old? */
+ else
+ return true;
+ };
+
+ auto n{0};
+ auto&& out{make_output_stream()};
+ mu_print(out, "(");
+ store().contacts_cache().for_each([&](const Contact& ci) {
+ if (!match_contact(ci))
+ return true; // continue
+ mu_println(out.out(), "{}", quote(ci.display_name()));
+ ++n;
+ return maxnum == 0 || n < maxnum;
+ });
+ mu_print(out, ")");
+ output(mu_format("(:contacts {}\n:tstamp \"{}\")",
+ out.to_string(), g_get_monotonic_time()));
+
+ mu_debug("sent {} of {} contact(s)", n, store().contacts_cache().size());
+}
+
+void
+Server::Private::data_handler(const Command& cmd)
+{
+ const auto request_type{unwrap(cmd.symbol_arg(":kind"))};
+
+ if (request_type == "maildirs") {
+ auto&& out{make_output_stream()};
+ mu_print(out, "(");
+ for (auto&& mdir: store().maildirs())
+ mu_println(out, "{}", quote(std::move(mdir)));
+ mu_print(out, ")");
+ output(mu_format("(:maildirs {})", out.to_string()));
+ } else
+ throw Error(Error::Code::InvalidArgument,
+ "invalid request type '{}'", request_type);
+}
+
+
+/*
+ * creating a message object just to get a path seems a bit excessive maybe
+ * mu_store_get_path could be added if this turns out to be a problem
+ */
+static std::string
+path_from_docid(const Store& store, Store::Id docid)
+{
+ auto msg{store.find_message(docid)};
+ if (!msg)
+ throw Error(Error::Code::Store, "could not get message from store");
+
+ if (auto path{msg->path()}; path.empty())
+ throw Error(Error::Code::Store, "could not get path for message {}",
+ docid);
+ else
+ return path;
+}
+
+static std::vector<Store::Id>
+determine_docids(const Store& store, const Command& cmd)
+{
+ auto docid{cmd.number_arg(":docid").value_or(0)};
+ const auto msgid{cmd.string_arg(":msgid").value_or("")};
+
+ if ((docid == 0) == msgid.empty())
+ throw Error(Error::Code::InvalidArgument,
+ "precisely one of docid and msgid must be specified");
+
+ if (docid != 0)
+ return {static_cast<Store::Id>(docid)};
+ else
+ return store.find_duplicates(msgid);
+}
+
+size_t
+Server::Private::output_results(const QueryResults& qres, size_t batch_size) const
+{
+ // create an output stream with a file name
+ size_t n{}, batch_n{};
+ auto&& out{make_output_stream()};
+ // structured bindings / lambda don't work with some clang.
+
+ mu_print(out, "(");
+ for (auto&& mi: qres) {
+
+ auto msg{mi.message()};
+ if (!msg)
+ continue;
+
+ auto qm{mi.query_match()}; // construct sexp for a single header.
+ mu_println(out, "{}", msg_sexp_str(*msg, mi.doc_id(), qm));
+ ++n;
+ ++batch_n;
+
+ if (n % batch_size == 0) {
+ // batch complete
+ mu_print(out, ")");
+ batch_size = 5000;
+ output(mu_format("(:headers {})", out.to_string()));
+ batch_n = 0;
+ // start a new batch
+ out = make_output_stream();
+ mu_print(out, "(");
+ }
+ }
+
+ mu_print(out, ")");
+ if (batch_n > 0)
+ output(mu_format("(:headers {})", out.to_string()));
+ else
+ out.unlink();
+
+ return n;
+}
+
+
+void
+Server::Private::find_handler(const Command& cmd)
+{
+ const auto q{cmd.string_arg(":query").value_or("")};
+ const auto threads{cmd.boolean_arg(":threads")};
+ // perhaps let mu4e set this as frame-lines of the appropriate frame.
+ const auto batch_size{cmd.number_arg(":batch-size").value_or(200)};
+ const auto descending{cmd.boolean_arg(":descending")};
+ const auto maxnum{cmd.number_arg(":maxnum").value_or(-1) /*unlimited*/};
+ const auto skip_dups{cmd.boolean_arg(":skip-dups")};
+ const auto include_related{cmd.boolean_arg(":include-related")};
+
+ // complicated!
+ auto sort_field_id = std::invoke([&]()->Field::Id {
+ if (const auto arg = cmd.symbol_arg(":sortfield"); !arg)
+ return Field::Id::Date;
+ else if (arg->length() < 2)
+ throw Error{Error::Code::InvalidArgument, "invalid sort field '{}'",
+ *arg};
+ else if (const auto field{field_from_name(arg->substr(1))}; !field)
+ throw Error{Error::Code::InvalidArgument, "invalid sort field '{}'",
+ *arg};
+ else
+ return field->id;
+ });
+
+ if (batch_size < 1)
+ throw Error{Error::Code::InvalidArgument, "invalid batch-size {}", batch_size};
+
+ auto qflags{QueryFlags::SkipUnreadable}; // don't show unreadables.
+ if (descending)
+ qflags |= QueryFlags::Descending;
+ if (skip_dups)
+ qflags |= QueryFlags::SkipDuplicates;
+ if (include_related)
+ qflags |= QueryFlags::IncludeRelated;
+ if (threads)
+ qflags |= QueryFlags::Threading;
+
+ StopWatch sw{mu_format("{} (indexing: {})", __func__,
+ indexer().is_running() ? "yes" : "no")};
+
+ // we need to _lock_ the store while querying (which likely consists of
+ // multiple actual queries) + grabbing the results.
+ std::lock_guard l{store_.lock()};
+ auto qres{store_.run_query(q, sort_field_id, qflags, maxnum)};
+ if (!qres)
+ throw Error(Error::Code::Query, "failed to run query: {}", qres.error().what());
+
+ /* before sending new results, send an 'erase' message, so the frontend
+ * knows it should erase the headers buffer. this will ensure that the
+ * output of two finds will not be mixed. */
+ output_sexp(Sexp().put_props(":erase", Sexp::t_sym));
+ const auto bsize{static_cast<size_t>(batch_size)};
+ const auto foundnum = output_results(*qres, bsize);
+ output_sexp(Sexp().put_props(":found", foundnum));
+}
+
+void
+Server::Private::help_handler(const Command& cmd)
+{
+ const auto command{cmd.symbol_arg(":command").value_or("")};
+ const auto full{cmd.bool_arg(":full").value_or(!command.empty())};
+ auto&& info_map{command_handler_.info_map()};
+
+ if (command.empty()) {
+ mu_println(";; Commands are single-line s-expressions of the form\n"
+ ";; (<command-name> :param1 val1 :param2 val2 ...)\n"
+ ";; For instance:\n;; (help :command mkdir)\n"
+ ";; to get more information about the 'mkdir' command\n;;\n"
+ ";; The following commands are available:");
+ }
+
+ std::vector<std::string> names;
+ for (auto&& name_cmd: info_map)
+ names.emplace_back(name_cmd.first);
+
+ std::sort(names.begin(), names.end());
+
+ for (auto&& name : names) {
+ const auto& info{info_map.find(name)->second};
+
+ if (!command.empty() && name != command)
+ continue;
+
+ mu_println(";; {:<12} -- {}", name, info.docstring);
+
+ if (!full)
+ continue;
+
+ for (auto&& argname : info.sorted_argnames()) {
+ const auto& arg{info.args.find(argname)};
+ mu_println(";; {:<17} :: {:<24} -- {}",
+ arg->first, to_string(arg->second),
+ arg->second.docstring);
+ }
+ mu_println(";;");
+ }
+}
+
+static Sexp
+get_stats(const Indexer::Progress& stats, const std::string& state)
+{
+ Sexp sexp;
+ sexp.put_props(
+ ":info", "index"_sym,
+ ":status", Sexp::Symbol(state),
+ ":checked", static_cast<int>(stats.checked),
+ ":updated", static_cast<int>(stats.updated),
+ ":cleaned-up", static_cast<int>(stats.removed));
+
+ return sexp;
+}
+
+void
+Server::Private::index_handler(const Command& cmd)
+{
+ Mu::Indexer::Config conf{};
+ conf.cleanup = cmd.boolean_arg(":cleanup");
+ conf.lazy_check = cmd.boolean_arg(":lazy-check");
+ // ignore .noupdate with an empty store.
+ conf.ignore_noupdate = store().empty();
+
+ indexer().stop();
+ if (index_thread_.joinable())
+ index_thread_.join();
+
+ // start a background track.
+ index_thread_ = std::thread([this, conf = std::move(conf)] {
+ StopWatch sw{"indexing"};
+ indexer().start(conf);
+ while (indexer().is_running()) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(2000));
+ output_sexp(get_stats(indexer().progress(), "running"),
+ Server::OutputFlags::Flush);
+ }
+ output_sexp(get_stats(indexer().progress(), "complete"),
+ Server::OutputFlags::Flush);
+ });
+}
+
+void
+Server::Private::mkdir_handler(const Command& cmd)
+{
+ const auto path{cmd.string_arg(":path").value_or("<error>")};
+ const auto update{cmd.boolean_arg(":update")};
+
+ if (path.find(store().root_maildir()) != 0)
+ throw Error{Error::Code::File, "maildir is not below root-maildir"};
+
+ if (auto&& res = maildir_mkdir(path, 0755, false); !res)
+ throw res.error();
+
+ /* mu4e does a lot of opportunistic 'mkdir', only send it updates when
+ * requested */
+ if (!update)
+ return;
+
+ output_sexp(Sexp().put_props(":info", "mkdir",
+ ":message",
+ mu_format("{} has been created", path)));
+}
+
+void
+Server::Private::perform_move(Store::Id docid,
+ const Message& msg,
+ const std::string& maildirarg,
+ Flags flags,
+ bool new_name,
+ bool no_view)
+{
+ bool different_mdir{};
+ auto maildir{maildirarg};
+ if (maildir.empty()) {
+ maildir = msg.maildir();
+ different_mdir = false;
+ } else /* are we moving to a different mdir, or is it just flags? */
+ different_mdir = maildir != msg.maildir();
+
+ Store::MoveOptions move_opts{Store::MoveOptions::DupFlags};
+ if (new_name)
+ move_opts |= Store::MoveOptions::ChangeName;
+
+ /* note: we get back _all_ the messages that changed; the first is the
+ * primary mover; the rest (if present) are any dups affected */
+ const auto id_paths{unwrap(store().move_message(docid, maildir, flags, move_opts))};
+ for (auto& [id,path]: id_paths) {
+ auto idmsg{store().find_message(id)};
+ if (!idmsg)
+ throw Error{Error::Code::Xapian, "cannot find message for id {}", id};
+
+ auto sexpstr = "(:update " + msg_sexp_str(*idmsg, id, {});
+ /* note, the :move t thing is a hint to the frontend that it
+ * could remove the particular header */
+ if (different_mdir)
+ sexpstr += " :move t";
+ if (!no_view && id == docid)
+ sexpstr += " :maybe-view t";
+ sexpstr += ')';
+ output(std::move(sexpstr));
+ }
+}
+
+static Flags
+calculate_message_flags(const Message& msg, Option<std::string> flagopt)
+{
+ const auto flags = std::invoke([&]()->Option<Flags>{
+ if (!flagopt)
+ return msg.flags();
+ else
+ return flags_from_expr(*flagopt, msg.flags());
+ });
+
+ if (!flags)
+ throw Error{Error::Code::InvalidArgument,
+ "invalid flags '{}'", flagopt.value_or("")};
+ else
+ return flags.value();
+}
+
+void
+Server::Private::move_docid(Store::Id docid,
+ Option<std::string> flagopt,
+ bool new_name,
+ bool no_view)
+{
+ if (docid == Store::InvalidId)
+ throw Error{Error::Code::InvalidArgument, "invalid docid"};
+
+ auto msg{store_.find_message(docid)};
+ if (!msg)
+ throw Error{Error::Code::Store, "failed to get message from store"};
+
+ const auto flags = calculate_message_flags(msg.value(), flagopt);
+ perform_move(docid, *msg, "", flags, new_name, no_view);
+}
+
+/*
+ * 'move' moves a message to a different maildir and/or changes its flags.
+ * parameters are *either* a 'docid:' or 'msgid:' pointing to the message, a
+ * 'maildir:' for the target maildir, and a 'flags:' parameter for the new
+ * flags.
+ *
+ * With :msgid, this is "opportunistic": it's not an error when the given
+ * message-id does not exist. This is e.g. for the case when tagging possible
+ * related messages.
+ */
+void
+Server::Private::move_handler(const Command& cmd)
+{
+ auto maildir{cmd.string_arg(":maildir").value_or("")};
+ const auto flagopt{cmd.string_arg(":flags")};
+ const auto rename{cmd.boolean_arg(":rename")};
+ const auto no_view{cmd.boolean_arg(":noupdate")};
+ const auto docids{determine_docids(store_, cmd)};
+
+ if (docids.empty()) {
+ if (!!cmd.string_arg(":msgid")) {
+ // msgid not found: no problem.
+ mu_debug("no move: '{}' not found",
+ *cmd.string_arg(":msgid"));
+ return;
+ }
+ // however, if we wanted to be move by msgid, it's worth raising
+ // an error.
+ throw Mu::Error{Error::Code::Store,
+ "message not found in store (docid={})",
+ cmd.number_arg(":docid").value_or(0)};
+ } else if (docids.size() > 1) {
+ if (!maildir.empty()) // ie. duplicate message-ids.
+ throw Mu::Error{Error::Code::Store,
+ "cannot move multiple messages at the same time"};
+ // multi.
+ for (auto&& docid : docids)
+ move_docid(docid, flagopt, rename, no_view);
+ return;
+ } else {
+ const auto docid{docids.at(0)};
+ auto msg = store().find_message(docid)
+ .or_else([&]{throw Error{Error::Code::InvalidArgument,
+ "cannot find message {}", docid};}).value();
+
+ /* if maildir was not specified, take the current one */
+ if (maildir.empty())
+ maildir = msg.maildir();
+
+ /* determine the real target flags, which come from the flags-parameter
+ * we received (ie., flagstr), if any, plus the existing message
+ * flags. */
+ const auto flags = calculate_message_flags(msg, flagopt);
+ perform_move(docid, msg, maildir, flags, rename, no_view);
+ }
+}
+
+void
+Server::Private::ping_handler(const Command& cmd)
+{
+ const auto storecount{store().size()};
+ if (storecount == (unsigned)-1)
+ throw Error{Error::Code::Store, "failed to read store"};
+ Sexp addrs;
+ for (auto&& addr : store().config().get<Config::Id::PersonalAddresses>())
+ addrs.add(addr);
+
+ output_sexp(Sexp()
+ .put_props(":pong", "mu")
+ .put_props(":props",
+ Sexp().put_props(
+ ":version", VERSION,
+ ":personal-addresses", std::move(addrs),
+ ":database-path", store().path(),
+ ":root-maildir", store().root_maildir(),
+ ":doccount", storecount)));
+}
+
+void
+Server::Private::queries_handler(const Command& cmd)
+{
+ const auto queries{cmd.string_vec_arg(":queries")
+ .value_or(std::vector<std::string>{})};
+
+ Sexp qresults;
+ for (auto&& q : queries) {
+ const auto count{store_.count_query(q)};
+ const auto unreadq{mu_format("flag:unread AND ({})", q)};
+ const auto unread{store_.count_query(unreadq)};
+ qresults.add(Sexp().put_props(":query", q,
+ ":count", count,
+ ":unread", unread));
+ }
+
+ output_sexp(Sexp(":queries"_sym, std::move(qresults)));
+}
+
+
+void
+Server::Private::quit_handler(const Command& cmd)
+{
+ keep_going_ = false;
+}
+
+void
+Server::Private::remove_handler(const Command& cmd)
+{
+ auto docid_opt{cmd.number_arg(":docid")};
+ auto path_opt{cmd.string_arg(":path")};
+
+ if (!!docid_opt == !!path_opt)
+ throw Error(Error::Code::InvalidArgument,
+ "must pass precisely one of :docid and :path");
+ std::string path;
+ Store::Id docid{};
+ if (docid = docid_opt.value_or(0); docid != 0)
+ path = path_from_docid(store(), docid);
+ else
+ path = path_opt.value();
+
+ if (::unlink(path.c_str()) != 0 && errno != ENOENT)
+ throw Error(Error::Code::File,
+ "could not delete {}: {}", path, g_strerror(errno));
+
+ if (!store().remove_message(path))
+ mu_warning("failed to remove message @ {} ({}) from store", path, docid);
+ else
+ mu_debug("removed message @ {} @ ({})", path, docid);
+
+ output_sexp(Sexp().put_props(":remove", docid)); // act as if it worked.
+}
+
+void
+Server::Private::view_mark_as_read(Store::Id docid, Message&& msg, bool rename)
+{
+ auto new_flags = [](const Message& m)->Option<Flags> {
+ auto nflags = flags_from_delta_expr("+S-u-N", m.flags());
+ if (!nflags || nflags == m.flags())
+ return Nothing; // nothing to do
+ else
+ return nflags;
+ };
+
+ auto&& nflags = new_flags(msg);
+ if (!nflags) { // nothing to move, just send the message for viewing.
+ output(mu_format("(:view {})", msg_sexp_str(msg, docid, {})));
+ return;
+ }
+
+ // move message + dups, present results.
+ Store::MoveOptions move_opts{Store::MoveOptions::DupFlags};
+ if (rename)
+ move_opts |= Store::MoveOptions::ChangeName;
+
+ const auto ids{Store::id_vec(unwrap(store().move_message(docid, {}, nflags, move_opts)))};
+ for (auto&& [id, moved_msg]: store().find_messages(ids))
+ output(mu_format("({} {})", id == docid ? ":view" : ":update",
+ msg_sexp_str(moved_msg, id, {})));
+}
+
+void
+Server::Private::view_handler(const Command& cmd)
+{
+ StopWatch sw{mu_format("{} (indexing: {})", __func__, indexer().is_running())};
+
+ const auto mark_as_read{cmd.boolean_arg(":mark-as-read")};
+ /* for now, do _not_ rename, as it seems to confuse mbsync */
+ const auto rename{false};
+ //const auto rename{get_bool_or(params, ":rename")};
+
+ const auto docids{determine_docids(store(), cmd)};
+
+ if (docids.empty())
+ throw Error{Error::Code::Store, "failed to find message for view"};
+ const auto docid{docids.at(0)};
+ auto msg = store().find_message(docid)
+ .or_else([]{throw Error{Error::Code::Store,
+ "failed to find message for view"};}).value();
+
+ /* if the message should not be marked-as-read, we're done. */
+ if (!mark_as_read)
+ output(mu_format("(:view {})", msg_sexp_str(msg, docid, {})));
+ else
+ view_mark_as_read(docid, std::move(msg), rename);
+ /* otherwise, mark message and and possible dups as read */
+}
+
+Server::Server(Store& store, const Server::Options& opts, Server::Output output)
+ : priv_{std::make_unique<Private>(store, opts, output)}
+{}
+
+Server::~Server() = default;
+
+bool
+Server::invoke(const std::string& expr) noexcept
+{
+ return priv_->invoke(expr);
+}
+
+/* LCOV_EXCL_STOP */
--- /dev/null
+/*
+** Copyright (C) 2020-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_SERVER_HH__
+#define MU_SERVER_HH__
+
+#include <memory>
+#include <functional>
+
+#include <utils/mu-sexp.hh>
+#include <utils/mu-utils.hh>
+#include <mu-store.hh>
+
+/* LCOV_EXCL_START */
+
+namespace Mu {
+
+/**
+ * @brief Implements the mu server, as used by mu4e.
+ */
+class Server {
+public:
+ enum struct OutputFlags {
+ None = 0,
+ Flush = 1 << 0, /**< flush output buffer after */
+ };
+
+ /**
+ * Prototype for output function
+ *
+ * @param str a string
+ * @param flags flags that influence the behavior
+ */
+ using Output = std::function<void(const std::string& str, OutputFlags flags)>;
+
+ struct Options {
+ bool allow_temp_file; /**< temp file optimization allowed? */
+ };
+
+ /**
+ * Construct a new server
+ *
+ * @param store a message store object
+ * @param output callable for the server responses.
+ */
+ Server(Store& store, const Options& opts, Output output);
+
+ /**
+ * DTOR
+ */
+ ~Server();
+
+ /**
+ * Invoke a call on the server.
+ *
+ * @param expr the s-expression to call
+ *
+ * @return true if we the server is still ready for more
+ * calls, false when it should quit.
+ */
+ bool invoke(const std::string& expr) noexcept;
+
+private:
+ struct Private;
+ std::unique_ptr<Private> priv_;
+};
+MU_ENABLE_BITOPS(Server::OutputFlags);
+
+} // namespace Mu
+
+/* LCOV_EXCL_STOP */
+
+#endif /* MU_SERVER_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2021-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+#include "mu-store.hh"
+
+#include <chrono>
+#include <mutex>
+#include <array>
+#include <cstdlib>
+#include <stdexcept>
+#include <unordered_map>
+#include <atomic>
+#include <type_traits>
+#include <iostream>
+#include <cstring>
+
+#include "mu-maildir.hh"
+#include "mu-query.hh"
+#include "mu-xapian-db.hh"
+#include "mu-scanner.hh"
+
+#include "utils/mu-error.hh"
+
+#include "utils/mu-utils.hh"
+#include <utils/mu-utils-file.hh>
+
+using namespace Mu;
+
+static_assert(std::is_same<Store::Id, Xapian::docid>::value, "wrong type for Store::Id");
+
+// Properties
+constexpr auto ExpectedSchemaVersion = MU_STORE_SCHEMA_VERSION;
+
+static std::string
+remove_slash(const std::string& str)
+{
+ auto clean{str};
+ while (!clean.empty() && clean[clean.length() - 1] == '/')
+ clean.pop_back();
+
+ return clean;
+}
+
+struct Store::Private {
+
+ Private(const std::string& path, bool readonly):
+ xapian_db_{XapianDb(path, readonly ? XapianDb::Flavor::ReadOnly
+ : XapianDb::Flavor::Open)},
+ config_{xapian_db_},
+ contacts_cache_{config_},
+ root_maildir_{remove_slash(config_.get<Config::Id::RootMaildir>())},
+ message_opts_{make_message_options(config_)}
+ {}
+
+ Private(const std::string& path, const std::string& root_maildir,
+ Option<const Config&> conf):
+ xapian_db_{XapianDb(path, XapianDb::Flavor::CreateOverwrite)},
+ config_{make_config(xapian_db_, root_maildir, conf)},
+ contacts_cache_{config_},
+ root_maildir_{remove_slash(config_.get<Config::Id::RootMaildir>())},
+ message_opts_{make_message_options(config_)}
+ {}
+
+ ~Private() try {
+ mu_debug("closing store @ {}", xapian_db_.path());
+ if (!xapian_db_.read_only())
+ contacts_cache_.serialize();
+ } catch (...) {
+ mu_critical("caught exception in store dtor");
+ }
+
+ Config make_config(XapianDb& xapian_db, const std::string& root_maildir,
+ Option<const Config&> conf) {
+
+ if (!g_path_is_absolute(root_maildir.c_str()))
+ throw Error{Error::Code::File,
+ "root maildir path is not absolute ({})",
+ root_maildir};
+
+ Config config{xapian_db};
+ if (conf)
+ config.import_configurable(*conf);
+
+ config.set<Config::Id::RootMaildir>(remove_slash(root_maildir));
+ config.set<Config::Id::SchemaVersion>(ExpectedSchemaVersion);
+
+ return config;
+ }
+
+ Message::Options make_message_options(const Config& conf) {
+ if (conf.get<Config::Id::SupportNgrams>())
+ return Message::Options::SupportNgrams;
+ else
+ return Message::Options::None;
+ }
+
+ Option<Message> find_message_unlocked(Store::Id docid) const;
+ Store::IdVec find_duplicates_unlocked(const Store& store,
+ const std::string& message_id) const;
+
+ Result<Store::Id> add_message_unlocked(Message& msg);
+ Result<Store::Id> update_message_unlocked(Message& msg, Store::Id docid);
+ Result<Store::Id> update_message_unlocked(Message& msg, const std::string& old_path);
+
+
+ using PathMessage = std::pair<std::string, Message>;
+ Result<PathMessage> move_message_unlocked(Message&& msg,
+ Option<const std::string&> target_mdir,
+ Option<Flags> new_flags,
+ MoveOptions opts);
+ XapianDb xapian_db_;
+ Config config_;
+ ContactsCache contacts_cache_;
+ std::unique_ptr<Indexer> indexer_;
+
+ const std::string root_maildir_;
+ const Message::Options message_opts_;
+
+ std::mutex lock_;
+};
+
+
+Result<Store::Id>
+Store::Private::add_message_unlocked(Message& msg)
+{
+ auto&& docid{xapian_db_.add_document(msg.document().xapian_document())};
+ if (docid)
+ mu_debug("added message @ {}; docid = {}", msg.path(), *docid);
+
+ return docid;
+}
+
+
+Result<Store::Id>
+Store::Private::update_message_unlocked(Message& msg, Store::Id docid)
+{
+ auto&& res{xapian_db_.replace_document(docid, msg.document().xapian_document())};
+ if (res)
+ mu_debug("updated message @ {}; docid = {}", msg.path(), *res);
+
+ return res;
+}
+
+Result<Store::Id>
+Store::Private::update_message_unlocked(Message& msg, const std::string& path_to_replace)
+{
+ return xapian_db_.replace_document(
+ field_from_id(Field::Id::Path).xapian_term(path_to_replace),
+ msg.document().xapian_document());
+}
+
+Option<Message>
+Store::Private::find_message_unlocked(Store::Id docid) const
+{
+ if (auto&& doc{xapian_db_.document(docid)}; !doc)
+ return Nothing;
+ else if (auto&& msg{Message::make_from_document(std::move(*doc))}; !msg)
+ return Nothing;
+ else
+ return Some(std::move(*msg));
+}
+
+Store::IdVec
+Store::Private::find_duplicates_unlocked(const Store& store,
+ const std::string& message_id) const
+{
+ if (message_id.empty() || message_id.size() > MaxTermLength) {
+ mu_warning("invalid message-id '{}'", message_id);
+ return {};
+ }
+
+ auto expr{mu_format("{}:{}",
+ field_from_id(Field::Id::MessageId).shortcut,
+ message_id)};
+ if (auto&& res{store.run_query(expr)}; !res) {
+ mu_warning("error finding message-ids: {}", res.error().what());
+ return {};
+
+ } else {
+ Store::IdVec ids;
+ ids.reserve(res->size());
+ for (auto&& mi: *res)
+ ids.emplace_back(mi.doc_id());
+ return ids;
+ }
+}
+
+
+Store::Store(const std::string& path, Store::Options opts)
+ : priv_{std::make_unique<Private>(path, none_of(opts & Store::Options::Writable))}
+{
+ if (none_of(opts & Store::Options::Writable) &&
+ any_of(opts & Store::Options::ReInit))
+ throw Mu::Error(Error::Code::InvalidArgument,
+ "Options::ReInit requires Options::Writable");
+
+ const auto s_version{config().get<Config::Id::SchemaVersion>()};
+ if (any_of(opts & Store::Options::ReInit)) {
+ /* don't try to recover from version with an incompatible scheme */
+ if (s_version < 500)
+ throw Mu::Error(Error::Code::CannotReinit,
+ "old schema ({}) is too old to re-initialize from",
+ s_version).add_hint("Invoke 'mu init' without '--reinit'; "
+ "see mu-init(1) for details");
+ const auto old_root_maildir{root_maildir()};
+
+ MemDb mem_db;
+ Config old_config(mem_db);
+ old_config.import_configurable(config());
+
+ this->priv_.reset();
+ /* and create a new one "in place" */
+ Store new_store(path, old_root_maildir, old_config);
+ this->priv_ = std::move(new_store.priv_);
+ }
+
+ /* otherwise, the schema version should match. */
+ if (s_version != ExpectedSchemaVersion)
+ throw Mu::Error(Error::Code::SchemaMismatch,
+ "expected schema-version {}, but got {}",
+ ExpectedSchemaVersion, s_version).
+ add_hint("Please (re)initialize with 'mu init'; see mu-init(1) for details");
+}
+
+Store::Store(const std::string& path,
+ const std::string& root_maildir,
+ Option<const Config&> conf):
+ priv_{std::make_unique<Private>(path, root_maildir, conf)}
+{}
+
+Store::Store(Store&& other)
+{
+ priv_ = std::move(other.priv_);
+ priv_->indexer_.reset();
+}
+
+Store::~Store() = default;
+
+Store::Statistics
+Store::statistics() const
+{
+ Statistics stats{};
+
+ stats.size = size();
+ stats.last_change = config().get<Config::Id::LastChange>();
+ stats.last_index = config().get<Config::Id::LastIndex>();
+
+ return stats;
+}
+
+const XapianDb&
+Store::xapian_db() const
+{
+ return priv_->xapian_db_;
+}
+
+XapianDb&
+Store::xapian_db()
+{
+ return priv_->xapian_db_;
+}
+
+const Config&
+Store::config() const
+{
+ return priv_->config_;
+}
+
+Config&
+Store::config()
+{
+ return priv_->config_;
+}
+
+const std::string&
+Store::root_maildir() const {
+ return priv_->root_maildir_;
+}
+
+const ContactsCache&
+Store::contacts_cache() const
+{
+ return priv_->contacts_cache_;
+}
+
+Indexer&
+Store::indexer()
+{
+ std::lock_guard guard{priv_->lock_};
+
+ if (xapian_db().read_only())
+ throw Error{Error::Code::Store, "no indexer for read-only store"};
+ else if (!priv_->indexer_)
+ priv_->indexer_ = std::make_unique<Indexer>(*this);
+
+ return *priv_->indexer_.get();
+}
+
+Result<Store::Id>
+Store::add_message(Message& msg, bool is_new)
+{
+ const auto mdir{maildir_from_path(msg.path(), root_maildir())};
+ if (!mdir)
+ return Err(mdir.error());
+ if (auto&& res = msg.set_maildir(mdir.value()); !res)
+ return Err(res.error());
+
+ // we shouldn't mix ngrams/non-ngrams messages.
+ if (any_of(msg.options() & Message::Options::SupportNgrams) !=
+ any_of(message_options() & Message::Options::SupportNgrams))
+ return Err(Error::Code::InvalidArgument, "incompatible message options");
+
+ /* add contacts from this message to cache; this cache
+ * also determines whether those contacts are _personal_, i.e. match
+ * our personal addresses.
+ *
+ * if a message has any personal contacts, mark it as personal; do
+ * this by updating the message flags.
+ */
+ bool is_personal{};
+ priv_->contacts_cache_.add(msg.all_contacts(), is_personal);
+ if (is_personal)
+ msg.set_flags(msg.flags() | Flags::Personal);
+
+ std::lock_guard guard{priv_->lock_};
+ auto&& res = is_new ?
+ priv_->add_message_unlocked(msg) :
+ priv_->update_message_unlocked(msg, msg.path());
+ if (!res)
+ return Err(res.error());
+
+ mu_debug("added {}{}message @ {}; docid = {}",
+ is_new ? "new " : "", is_personal ? "personal " : "", msg.path(), *res);
+
+ return res;
+}
+
+Result<Store::Id>
+Store::add_message(const std::string& path, bool is_new)
+{
+ if (auto msg{Message::make_from_path(path, priv_->message_opts_)}; !msg)
+ return Err(msg.error());
+ else
+ return add_message(msg.value(), is_new);
+}
+
+
+bool
+Store::remove_message(const std::string& path)
+{
+ const auto term{field_from_id(Field::Id::Path).xapian_term(path)};
+
+ std::lock_guard guard{priv_->lock_};
+
+ xapian_db().delete_document(term);
+ mu_debug("deleted message @ {} from store", path);
+ return true;
+}
+
+void
+Store::remove_messages(const std::vector<Store::Id>& ids)
+{
+ std::lock_guard guard{priv_->lock_};
+
+ XapianDb::Transaction tx (xapian_db()); // RAII
+
+ for (auto&& id : ids)
+ xapian_db().delete_document(id);
+}
+
+
+Option<Message>
+Store::find_message(Store::Id docid) const
+{
+ std::lock_guard guard{priv_->lock_};
+
+ return priv_->find_message_unlocked(docid);
+}
+
+Option<Store::Id>
+Store::find_message_id(const std::string& path) const
+{
+ constexpr auto path_field{field_from_id(Field::Id::Path)};
+
+ std::lock_guard guard{priv_->lock_};
+
+ auto enq{xapian_db().enquire()};
+ enq.set_query(Xapian::Query{path_field.xapian_term(path)});
+
+ if (auto mset{enq.get_mset(0, 1)}; mset.empty())
+ return Nothing; // message not found
+ else
+ return Some(*mset.begin());
+}
+
+
+Store::IdMessageVec
+Store::find_messages(IdVec ids) const
+{
+ std::lock_guard guard{priv_->lock_};
+
+ IdMessageVec id_msgs;
+ for (auto&& id: ids) {
+ if (auto&& msg{priv_->find_message_unlocked(id)}; msg)
+ id_msgs.emplace_back(std::make_pair(id, std::move(*msg)));
+ }
+
+ return id_msgs;
+}
+
+/**
+ * Move a message in store and filesystem; with DryRun, only calculate the target name.
+ *
+ * Lock is assumed taken already
+ *
+ * @param id message id
+ * @param target_mdir target_mdir (or Nothing for current)
+ * @param new_flags new flags (or Nothing)
+ * @param opts move_options
+ *
+ * @return the Message after the moving, or an Error
+ */
+Result<Store::Private::PathMessage>
+Store::Private::move_message_unlocked(Message&& msg,
+ Option<const std::string&> target_mdir,
+ Option<Flags> new_flags,
+ MoveOptions opts)
+{
+ const auto old_path = msg.path();
+ const auto target_flags = new_flags.value_or(msg.flags());
+ const auto target_maildir = target_mdir.value_or(msg.maildir());
+
+ /* 1. first determine the file system path of the target */
+ const auto target_path =
+ maildir_determine_target(msg.path(), root_maildir_,
+ target_maildir, target_flags,
+ any_of(opts & MoveOptions::ChangeName));
+ if (!target_path)
+ return Err(target_path.error());
+
+ // in dry-run mode, we only determine the target-path
+ if (none_of(opts & MoveOptions::DryRun)) {
+
+ /* 2. let's move it */
+ if (const auto res = maildir_move_message(msg.path(), target_path.value()); !res)
+ return Err(res.error());
+
+ /* 3. file move worked, now update the message with the new info.*/
+ if (auto&& res = msg.update_after_move(
+ target_path.value(), target_maildir, target_flags); !res)
+ return Err(res.error());
+
+ /* 4. update message worked; re-store it */
+ if (auto&& res = update_message_unlocked(msg, old_path); !res)
+ return Err(res.error());
+ }
+
+ /* 6. Profit! */
+ return Ok(PathMessage{std::move(*target_path), std::move(msg)});
+}
+
+Store::IdVec
+Store::find_duplicates(const std::string& message_id) const
+{
+ std::lock_guard guard{priv_->lock_};
+
+ return priv_->find_duplicates_unlocked(*this, message_id);
+}
+
+
+Result<Store::IdPathVec>
+Store::move_message(Store::Id id,
+ Option<const std::string&> target_mdir,
+ Option<Flags> new_flags,
+ MoveOptions opts)
+{
+ auto filter_dup_flags=[](Flags old_flags, Flags new_flags) -> Flags {
+ new_flags = flags_keep_unmutable(old_flags, new_flags, Flags::Draft);
+ new_flags = flags_keep_unmutable(old_flags, new_flags, Flags::Flagged);
+ new_flags = flags_keep_unmutable(old_flags, new_flags, Flags::Trashed);
+ return new_flags;
+ };
+
+ std::lock_guard guard{priv_->lock_};
+
+ auto msg{priv_->find_message_unlocked(id)};
+ if (!msg)
+ return Err(Error::Code::Store, "cannot find message <{}>", id);
+
+ const auto message_id{msg->message_id()};
+ auto res{priv_->move_message_unlocked(std::move(*msg), target_mdir, new_flags, opts)};
+ if (!res)
+ return Err(res.error());
+
+ IdPathVec id_paths{{id, res->first}};
+ if (none_of(opts & Store::MoveOptions::DupFlags) || message_id.empty() || !new_flags)
+ return Ok(std::move(id_paths));
+
+ /* handle the dup-flags case; i.e. apply (a subset of) the flags to
+ * all messages with the same message-id as well */
+ auto dups{priv_->find_duplicates_unlocked(*this, message_id)};
+ for (auto&& dupid: dups) {
+
+ if (dupid == id)
+ continue; // already
+
+ auto dup_msg{priv_->find_message_unlocked(dupid)};
+ if (!dup_msg)
+ continue; // no such message
+
+ /* For now, don't change Draft/Flagged/Trashed */
+ const auto dup_flags{filter_dup_flags(dup_msg->flags(), *new_flags)};
+ /* use the updated new_flags and MoveOptions without DupFlags (so we don't
+ * recurse) */
+ opts = opts & ~MoveOptions::DupFlags;
+ if (auto dup_res = priv_->move_message_unlocked(
+ std::move(*dup_msg), Nothing, dup_flags, opts); !dup_res)
+ mu_warning("failed to move dup: {}", dup_res.error().what());
+ else
+ id_paths.emplace_back(dupid, dup_res->first);
+ }
+
+ // sort the dup paths by name;
+ std::sort(id_paths.begin() + 1, id_paths.end(),
+ [](const auto& idp1, const auto& idp2) { return idp1.second < idp2.second; });
+
+ return Ok(std::move(id_paths));
+}
+
+Store::IdVec
+Store::id_vec(const IdPathVec& ips)
+{
+ IdVec idv;
+ for (auto&& ip: ips)
+ idv.emplace_back(ip.first);
+
+ return idv;
+}
+
+
+time_t
+Store::dirstamp(const std::string& path) const
+{
+ std::string ts;
+
+ {
+ std::unique_lock lock{priv_->lock_};
+ ts = xapian_db().metadata(path);
+ }
+
+ return ts.empty() ? 0 /*epoch*/ : ::strtoll(ts.c_str(), {}, 16);
+}
+
+void
+Store::set_dirstamp(const std::string& path, time_t tstamp)
+{
+ std::unique_lock lock{priv_->lock_};
+
+ xapian_db().set_metadata(path, mu_format("{:x}", tstamp));
+}
+
+bool
+Store::contains_message(const std::string& path) const
+{
+ std::unique_lock lock{priv_->lock_};
+
+ return xapian_db().term_exists(field_from_id(Field::Id::Path).xapian_term(path));
+}
+
+std::size_t
+Store::for_each_message_path(Store::ForEachMessageFunc msg_func) const
+{
+ size_t n{};
+
+ xapian_try([&] {
+ std::lock_guard guard{priv_->lock_};
+ auto enq{xapian_db().enquire()};
+
+ enq.set_query(Xapian::Query::MatchAll);
+ enq.set_cutoff(0, 0);
+
+ Xapian::MSet matches(enq.get_mset(0, xapian_db().size()));
+ constexpr auto path_no{field_from_id(Field::Id::Path).value_no()};
+ for (auto&& it = matches.begin(); it != matches.end(); ++it, ++n)
+ if (!msg_func(*it, it.get_document().get_value(path_no)))
+ break;
+ });
+
+ return n;
+}
+
+std::size_t
+Store::for_each_term(Field::Id field_id, Store::ForEachTermFunc func) const
+{
+ return xapian_db().all_terms(field_from_id(field_id).xapian_term(), func);
+}
+
+std::mutex&
+Store::lock() const
+{
+ return priv_->lock_;
+}
+
+Result<QueryResults>
+Store::run_query(const std::string& expr,
+ Field::Id sortfield_id,
+ QueryFlags flags, size_t maxnum) const
+{
+ return Query{*this}.run(expr, sortfield_id, flags, maxnum);
+}
+
+size_t
+Store::count_query(const std::string& expr) const
+{
+ return xapian_try([&] {
+ std::lock_guard guard{priv_->lock_};
+ Query q{*this};
+ return q.count(expr); }, 0);
+}
+
+std::string
+Store::parse_query(const std::string& expr, bool xapian) const
+{
+ return xapian_try([&] {
+ std::lock_guard guard{priv_->lock_};
+ Query q{*this};
+
+ return q.parse(expr, xapian);
+ }, std::string{});
+}
+
+
+std::vector<std::string>
+Store::maildirs() const
+{
+ std::vector<std::string> mdirs;
+ const auto prefix_size{root_maildir().size()};
+
+ Scanner::Handler handler = [&](const std::string& path, auto&& _1, auto&& _2) {
+ auto md{path.substr(prefix_size)};
+ mdirs.emplace_back(md.empty() ? "/" : std::move(md));
+ return true;
+ };
+
+ Scanner scanner{root_maildir(), handler, Scanner::Mode::MaildirsOnly};
+ scanner.start();
+ std::sort(mdirs.begin(), mdirs.end());
+
+ return mdirs;
+}
+
+Message::Options
+Store::message_options() const
+{
+ return priv_->message_opts_;
+}
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_STORE_HH__
+#define MU_STORE_HH__
+
+#include <string>
+#include <vector>
+#include <mutex>
+#include <ctime>
+#include <memory>
+
+#include "mu-contacts-cache.hh"
+#include "mu-xapian-db.hh"
+#include "mu-config.hh"
+#include "mu-indexer.hh"
+#include "mu-query-results.hh"
+
+#include <utils/mu-utils.hh>
+#include <utils/mu-utils.hh>
+#include <utils/mu-option.hh>
+
+#include <message/mu-message.hh>
+
+namespace Mu {
+
+class Store {
+public:
+ using Id = Xapian::docid; /**< Id for a message in the store */
+ static constexpr Id InvalidId = 0; /**< Invalid store id */
+ using IdVec = std::vector<Id>; /**< Vector of document ids */
+ using IdPathVec = std::vector<std::pair<Id, std::string>>;
+ /**< vector of id, path pairs */
+
+ /**
+ * Configuration options.
+ */
+ enum struct Options {
+ None = 0, /**< No specific options */
+ Writable = 1 << 0, /**< Open in writable mode */
+ ReInit = 1 << 1, /**< Re-initialize based on existing */
+ };
+
+ /**
+ * Make a store for an existing document database
+ *
+ * @param path path to the database
+ * @param options startup options
+ *
+ * @return A store or an error.
+ */
+ static Result<Store> make(const std::string& path,
+ Options opts=Options::None) noexcept {
+ return xapian_try_result(
+ [&]{return Ok(Store{path, opts});});
+ }
+
+ /**
+ * Construct a store for a not-yet-existing document database
+ *
+ * @param path path to the database
+ * @param root_maildir absolute path to maildir to use for this store
+ * @param conf a configuration object
+ *
+ * @return a store or an error
+ */
+ static Result<Store> make_new(const std::string& path,
+ const std::string& root_maildir,
+ Option<const Config&> conf={}) noexcept {
+ return xapian_try_result(
+ [&]{return Ok(Store(path, root_maildir, conf));});
+ }
+
+ /**
+ * Move CTOR
+ *
+ */
+ Store(Store&&);
+
+ /**
+ * DTOR
+ */
+ ~Store();
+
+ /**
+ * Store statistics. Unlike the properties, these can change
+ * during the lifetime of a store.
+ *
+ */
+ struct Statistics {
+ size_t size; /**< number of messages in store */
+ ::time_t last_change; /**< last time any update happened */
+ ::time_t last_index; /**< last time an indexing op was performed */
+ };
+
+ /**
+ * Get store statistics
+ *
+ * @return statistics
+ */
+ Statistics statistics() const;
+
+ /**
+ * Get the underlying xapian db object
+ *
+ * @return the XapianDb for this store
+ */
+ const XapianDb& xapian_db() const;
+ XapianDb& xapian_db();
+
+ /**
+ * Get the Config for this store
+ *
+ * @return the Config
+ */
+ const Config& config() const;
+ Config& config();
+
+ /**
+ * Get the ContactsCache object for this store
+ *
+ * @return the Contacts object
+ */
+ const ContactsCache& contacts_cache() const;
+
+ /**
+ * Get the Indexer associated with this store. It is an error to call
+ * this on a read-only store.
+ *
+ * @return the indexer.
+ */
+ Indexer& indexer();
+
+ /**
+ * Run a query; see the `mu-query` man page for the syntax.
+ *
+ * Multi-threaded callers must acquire the lock and keep it
+ * at least as long as the return value.
+ *
+ * @param expr the search expression
+ * @param sortfieldid the sortfield-id. If the field is NONE, sort by DATE
+ * @param flags query flags
+ * @param maxnum maximum number of results to return. 0 for 'no limit'
+ *
+ * @return the query-results or an error.
+ */
+ std::mutex& lock() const;
+ Result<QueryResults> run_query(const std::string& expr,
+ Field::Id sortfield_id = Field::Id::Date,
+ QueryFlags flags = QueryFlags::None,
+ size_t maxnum = 0) const;
+
+ /**
+ * run a Xapian query merely to count the number of matches; for the
+ * syntax, please refer to the mu-query manpage
+ *
+ * @param expr the search expression; use "" to match all messages
+ *
+ * @return the number of matches
+ */
+ size_t count_query(const std::string& expr = "") const;
+
+ /**
+ * For debugging, get the internal string representation of the parsed
+ * query
+ *
+ * @param expr a xapian search expression
+ * @param xapian if true, show Xapian's internal representation,
+ * otherwise, mu's.
+ *
+ * @return the string representation of the query
+ */
+ std::string parse_query(const std::string& expr, bool xapian) const;
+
+ /**
+ * Add or update a message to the store. When planning to write many
+ * messages, it's much faster to do so in a transaction. If so, set
+ * @param in_transaction to true. When done with adding messages, call
+ * commit().
+ *
+ * Optimization: If you are sure the message (i.e., a message with the
+ * given file-system path) does not yet exist in the database, ie., when
+ * doing the initial indexing, set @p is_new to true since we then don't
+ * have to check for the existing message.
+ *
+ * @param msg a message
+ * @param is_new whether this is a completely new message
+ *
+ * @return the doc id of the added message or an error.
+ */
+ Result<Id> add_message(Message& msg, bool is_new = false);
+ Result<Id> add_message(const std::string& path, bool is_new = false);
+
+ /**
+ * Remove a message from the store. It will _not_ remove the message
+ * from the file system.
+ *
+ * @param path the message path.
+ *
+ * @return true if removing happened; false otherwise.
+ */
+ bool remove_message(const std::string& path);
+
+ /**
+ * Remove a number if messages from the store. It will _not_ remove the
+ * message from the file system.
+ *
+ * @param ids vector with store ids for the message
+ */
+ void remove_messages(const std::vector<Id>& ids);
+
+ /**
+ * Remove a message from the store. It will _not_ remove the message
+ * from the file system.
+ *
+ * @param id the store id for the message
+ */
+ void remove_message(Id id) { remove_messages({id}); }
+
+ /**
+ * Find message in the store.
+ *
+ * @param id doc id for the message to find
+ *
+ * @return a message (if found) or Nothing
+ */
+ Option<Message> find_message(Id id) const;
+
+ /**
+ * Find a message's docid based on its path
+ *
+ * @param path path to the message
+ *
+ * @return the docid or Nothing if not found
+ */
+ Option<Id> find_message_id(const std::string& path) const;
+
+ /**
+ * Find the messages for the given ids
+ *
+ * @param ids document ids for the message
+ *
+ * @return id, message pairs for the messages found
+ * (which not necessarily _all_ of the ids)
+ */
+ using IdMessageVec = std::vector<std::pair<Id, Message>>;
+ IdMessageVec find_messages(IdVec ids) const;
+
+ /**
+ * Find the ids for all messages with a give message-id
+ *
+ * @param message_id a message id
+ *
+ * @return the ids of all messages with the given message-id
+ */
+ IdVec find_duplicates(const std::string& message_id) const;
+
+ /**
+ * does a certain message exist in the store already?
+ *
+ * @param path the message path
+ *
+ * @return true if the message exists in the store, false otherwise
+ */
+ bool contains_message(const std::string& path) const;
+
+ /**
+ * Options for moving
+ *
+ */
+ enum struct MoveOptions {
+ None = 0, /**< Defaults */
+ ChangeName = 1 << 0, /**< Change the name when moving */
+ DupFlags = 1 << 1, /**< Update flags for duplicate messages too */
+ DryRun = 1 << 2, /**< Don't really move, just determine target paths */
+ };
+
+ /**
+ * Move a message both in the filesystem and in the store. After a successful move, the
+ * message is updated.
+ *
+ * @param id the id for some message
+ * @param target_mdir the target maildir (if any)
+ * @param new_flags new flags (if any)
+ * @param opts move options
+ *
+ * @return Result, either an IdPathVec with ids and paths for the moved message(s) or some
+ * error. Note that in case of success at least one message is returned, and only with
+ * MoveOptions::DupFlags can it be more than one.
+ *
+ * The first element of the IdPathVec, is the main message that got move; any subsequent
+ * (if any) are the duplicate paths, sorted by path-name.
+ */
+ Result<IdPathVec> move_message(Store::Id id,
+ Option<const std::string&> target_mdir = Nothing,
+ Option<Flags> new_flags = Nothing,
+ MoveOptions opts = MoveOptions::None);
+ /**
+ * Convert IdPathVec -> IdVec
+ *
+ * @param ips idpath vector
+ *
+ * @return vector of ids
+ */
+ static IdVec id_vec(const IdPathVec& ips);
+
+ /**
+ * Prototype for the ForEachMessageFunc
+ *
+ * @param id :t store Id for the message
+ * @param path: the absolute path to the message
+ *
+ * @return true if for_each should continue; false to quit
+ */
+ using ForEachMessageFunc = std::function<bool(Id, const std::string&)>;
+
+ /**
+ * Call @param func for each document in the store. This takes a lock on
+ * the store, so the func should _not_ call any other Store:: methods.
+ *
+ * @param func a Callable invoked for each message.
+ *
+ * @return the number of times func was invoked
+ */
+ size_t for_each_message_path(ForEachMessageFunc func) const;
+
+ /**
+ * Prototype for the ForEachTermFunc
+ *
+ * @param term:
+ *
+ * @return true if for_each should continue; false to quit
+ */
+ using ForEachTermFunc = std::function<bool(const std::string&)>;
+
+ /**
+ * Call @param func for each term for the given field in the store. This
+ * takes a lock on the store, so the func should _not_ call any other
+ * Store:: methods.
+ *
+ * @param id the field id
+ * @param func a Callable invoked for each message.
+ *
+ * @return the number of times func was invoked
+ */
+ size_t for_each_term(Field::Id id, ForEachTermFunc func) const;
+
+ /**
+ * Get the timestamp for some message, or 0 if not found
+ *
+ * @param path the path
+ *
+ * @return the timestamp, or 0 if not found
+ */
+ time_t message_tstamp(const std::string& path) const;
+
+ /**
+ * Get the timestamp for some directory
+ *
+ * @param path the path
+ *
+ * @return the timestamp, or 0 if not found
+ */
+ time_t dirstamp(const std::string& path) const;
+
+ /**
+ * Set the timestamp for some directory
+ *
+ * @param path a filesystem path
+ * @param tstamp the timestamp for that path
+ */
+ void set_dirstamp(const std::string& path, time_t tstamp);
+
+ /*
+ *
+ * Some convenience
+ *
+ */
+
+ /**
+ * Get the Xapian database-path for this store
+ *
+ * @return the path
+ */
+ const std::string& path() const { return xapian_db().path(); }
+
+ /**
+ * Get the root-maildir for this store
+ *
+ * @return the root-maildir
+ */
+ const std::string& root_maildir() const;
+
+ /**
+ * Get the number of messages in the store
+ *
+ * @return the number
+ */
+ size_t size() const { return xapian_db().size(); }
+
+ /**
+ * Is the store empty?
+ *
+ * @return true or false
+ */
+ bool empty() const { return xapian_db().empty(); }
+
+
+ /**
+ * Get the list of maildirs, that is, the list of maildirs
+ * under root_maildir, without file-system prefix.
+ *
+ * This does a file-system scan.
+ *
+ * @return list of maildirs
+ */
+ std::vector<std::string> maildirs() const;
+
+
+ /**
+ * Compatible message-options for this store
+ *
+ * @return message-options.
+ */
+ Message::Options message_options() const;
+
+
+ /*
+ * _almost_ private
+ */
+
+ /**
+ * Get a reference to the private data. For internal use.
+ *
+ * @return private reference.
+ */
+ struct Private;
+ std::unique_ptr<Private>& priv() { return priv_; }
+ const std::unique_ptr<Private>& priv() const { return priv_; }
+
+private:
+ /**
+ * Construct a store for an existing document database
+ *
+ * @param path path to the database
+ * @param options startup options
+ */
+ Store(const std::string& path, Options opts=Options::None);
+
+ /**
+ * Construct a store for a not-yet-existing document database
+ *
+ * @param path path to the database
+ * @param config a configuration object
+ */
+ Store(const std::string& path, const std::string& root_maildir,
+ Option<const Config&> conf);
+
+ std::unique_ptr<Private> priv_;
+};
+
+MU_ENABLE_BITOPS(Store::Options);
+MU_ENABLE_BITOPS(Store::MoveOptions);
+
+static inline std::string
+format_as(const Store& store)
+{
+ return mu_format("store ({}/{})", format_as(store.xapian_db()),
+ store.root_maildir());
+}
+
+} // namespace Mu
+
+#endif /* MU_STORE_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+
+#include "mu-xapian-db.hh"
+#include "utils/mu-utils.hh"
+#include <inttypes.h>
+#include <mu-config.hh>
+
+#include <mutex>
+
+using namespace Mu;
+
+const Xapian::Database&
+XapianDb::db() const
+{
+ if (std::holds_alternative<Xapian::WritableDatabase>(db_))
+ return std::get<Xapian::WritableDatabase>(db_);
+ else
+ return std::get<Xapian::Database>(db_);
+}
+
+Xapian::WritableDatabase&
+XapianDb::wdb()
+{
+ if (read_only())
+ throw std::runtime_error("database is read-only");
+ return std::get<Xapian::WritableDatabase>(db_);
+}
+
+bool
+XapianDb::read_only() const
+{
+ return !std::holds_alternative<Xapian::WritableDatabase>(db_);
+}
+
+const std::string&
+XapianDb::path() const
+{
+ return path_;
+}
+
+void
+XapianDb::set_timestamp(const std::string_view key)
+{
+ wdb().set_metadata(std::string{key}, mu_format("{}", ::time({})));
+}
+
+using Flavor = XapianDb::Flavor;
+
+static std::string
+make_path(const std::string& db_path, Flavor flavor)
+{
+ if (flavor != Flavor::ReadOnly) {
+ /* we do our own flushing, set Xapian's internal one as
+ * the backstop*/
+ g_setenv("XAPIAN_FLUSH_THRESHOLD", "500000", 1);
+ /* create path if needed */
+ if (g_mkdir_with_parents(db_path.c_str(), 0700) != 0)
+ throw Error(Error::Code::File, "failed to create database dir {}: {}",
+ db_path, ::strerror(errno));
+ }
+
+ return db_path;
+}
+
+static XapianDb::DbType
+make_db(const std::string& db_path, Flavor flavor)
+{
+ switch (flavor) {
+
+ case Flavor::ReadOnly:
+ return Xapian::Database(db_path);
+ case Flavor::Open:
+ return Xapian::WritableDatabase(db_path, Xapian::DB_OPEN);
+ case Flavor::CreateOverwrite:
+ return Xapian::WritableDatabase(db_path, Xapian::DB_CREATE_OR_OVERWRITE);
+ /* LCOV_EXCL_START*/
+ default:
+ throw std::logic_error("unknown flavor");
+ /* LCOV_EXCL_STOP*/
+ }
+}
+
+XapianDb::XapianDb(const std::string& db_path, Flavor flavor):
+ path_(make_path(db_path, flavor)),
+ db_(make_db(path_, flavor)),
+ batch_size_{Config(*this).get<Config::Id::BatchSize>()}
+{
+ if (flavor == Flavor::CreateOverwrite)
+ set_timestamp(MetadataIface::created_key);
+
+ mu_debug("created {} / {} (batch-size: {})", flavor, *this, batch_size_);
+}
+
+
+#ifdef BUILD_TESTS
+/*
+ * Tests.
+ *
+ */
+
+#include "utils/mu-test-utils.hh"
+#include "config.h"
+#include "mu-store.hh"
+
+static void
+test_errors()
+{
+ allow_warnings();
+
+ TempDir tdir;
+ auto store = Store::make_new(tdir.path(), MU_TESTMAILDIR2);
+ assert_valid_result(store);
+ g_assert_true(store->empty());
+
+ XapianDb xdb(tdir.path(), Flavor::ReadOnly);
+ g_assert_true(xdb.read_only());
+
+ g_assert_false(!!xdb.delete_document("Boo"));
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/xapian-db/errors", test_errors);
+
+ return g_test_run();
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_XAPIAN_DB_HH__
+#define MU_XAPIAN_DB_HH__
+
+#include <variant>
+#include <memory>
+#include <string>
+#include <mutex>
+#include <functional>
+#include <unordered_map>
+
+#include <glib.h>
+
+#include <utils/mu-result.hh>
+#include <utils/mu-utils.hh>
+
+/* starting with 1.4.6, Xapian supports C++ move semantics,
+ * but only with XAPIAN_MOVE_SEMANTICS defined
+ */
+#ifndef XAPIAN_MOVE_SEMANTICS
+#define XAPIAN_MOVE_SEMANTICS
+#endif /*XAPIAN_MOVE_SEMANTICS*/
+#include <xapian.h>
+
+namespace Mu {
+
+// LCOV_EXCL_START
+
+// avoid exception-handling boilerplate.
+template <typename Func> void
+xapian_try(Func&& func) noexcept
+try {
+ func();
+} catch (const Xapian::Error& xerr) {
+ mu_critical("{}: xapian error '{}'", __func__, xerr.get_msg());
+} catch (const std::runtime_error& re) {
+ mu_critical("{}: runtime error: {}", __func__, re.what());
+} catch (const std::exception& e) {
+ mu_critical("{}: caught std::exception: {}", __func__, e.what());
+} catch (...) {
+ mu_critical("{}: caught exception", __func__);
+}
+
+template <typename Func, typename Default = std::invoke_result<Func>> auto
+xapian_try(Func&& func, Default&& def) noexcept -> std::decay_t<decltype(func())>
+try {
+ return func();
+} catch (const Xapian::DocNotFoundError& xerr) {
+ return static_cast<Default>(def);
+} catch (const Xapian::Error& xerr) {
+ mu_warning("{}: xapian error '{}'", __func__, xerr.get_msg());
+ return static_cast<Default>(def);
+} catch (const std::runtime_error& re) {
+ mu_critical("{}: runtime error: {}", __func__, re.what());
+ return static_cast<Default>(def);
+} catch (const std::exception& e) {
+ mu_critical("{}: caught std::exception: {}", __func__, e.what());
+ return static_cast<Default>(def);
+} catch (...) {
+ mu_critical("{}: caught exception", __func__);
+ return static_cast<Default>(def);
+}
+
+template <typename Func> auto
+xapian_try_result(Func&& func) noexcept -> std::decay_t<decltype(func())>
+try {
+ return func();
+} catch (const Xapian::DatabaseNotFoundError& nferr) {
+ return Err(Error{Error::Code::Xapian, "failed to open database"}.
+ add_hint("Try (re)creating using `mu init'"));
+} catch (const Xapian::DatabaseLockError& dlerr) {
+ return Err(Error{Error::Code::StoreLock, "database locked"}.
+ add_hint("Perhaps mu is already running?"));
+} catch (const Xapian::DatabaseCorruptError& dcerr) {
+ return Err(Error{Error::Code::Xapian, "failed to read database"}.
+ add_hint("Try (re)creating using `mu init'"));
+} catch (const Xapian::DocNotFoundError& dnferr) {
+ return Err(Error{Error::Code::Xapian, "message not found in database"}.
+ add_hint("Try reopening the database"));
+} catch (const Xapian::Error& xerr) {
+ return Err(Error::Code::Xapian, "{}", xerr.get_msg());
+} catch (const std::runtime_error& re) {
+ return Err(Error::Code::Internal, "runtime error: {}", re.what());
+} catch (const std::exception& e) {
+ return Err(Error::Code::Internal, "caught std::exception: {}", e.what());
+} catch (...) {
+ return Err(Error::Code::Internal, "caught exception");
+}
+
+// LCOV_EXCL_STOP
+
+/// abstract base
+struct MetadataIface {
+ virtual ~MetadataIface(){}
+ virtual void set_metadata(const std::string& name, const std::string& val) = 0;
+ virtual std::string metadata(const std::string& name) const = 0;
+ virtual bool read_only() const = 0;
+
+ using each_func = std::function<void(const std::string&, const std::string&)>;
+ virtual void for_each(each_func&& func) const =0;
+
+ /*
+ * These are special: handled on the Xapian db level
+ * rather than Config
+ */
+ static inline constexpr std::string_view created_key = "created";
+ static inline constexpr std::string_view last_change_key = "last-change";
+};
+
+
+/// In-memory db
+struct MemDb: public MetadataIface {
+ /**
+ * Create a new memdb
+ *
+ * @param readonly read-only? (for testing)
+ */
+ MemDb(bool readonly=false):read_only_{readonly} {}
+
+ /**
+ * Set some metadata
+ *
+ * @param name key name
+ * @param val value
+ */
+ void set_metadata(const std::string& name, const std::string& val) override {
+ map_.erase(name);
+ map_[name] = val;
+ }
+
+ /**
+ * Get metadata for given key, empty if not found
+ *
+ * @param name key name
+ *
+ * @return string
+ */
+ std::string metadata(const std::string& name) const override {
+ if (auto&& it = map_.find(name); it != map_.end())
+ return it->second;
+ else
+ return {};
+ }
+
+ /**
+ * Is this db read-only?
+ *
+ * @return true or false
+ */
+ bool read_only() const override { return read_only_; }
+
+
+ /**
+ * Invoke function for each key/value pair. Do not call
+ * @this from each_func().
+ *
+ * @param func a function
+ */
+ void for_each(MetadataIface::each_func&& func) const override {
+ for (const auto& [key, value] : map_)
+ func(key, value);
+ }
+
+private:
+ std::unordered_map<std::string, std::string> map_;
+ const bool read_only_;
+};
+
+/**
+ * Fairly thin wrapper around Xapian::Database and Xapian::WritableDatabase
+ * with just the things we need + locking + exception handling
+ */
+class XapianDb: public MetadataIface {
+#define DB_LOCKED std::unique_lock lock__{lock_};
+public:
+ /**
+ * Type of database to create.
+ *
+ */
+ enum struct Flavor {
+ ReadOnly, /**< Read-only database */
+ Open, /**< Open existing read-write */
+ CreateOverwrite, /**< Create new or overwrite existing */
+ };
+
+ /**
+ * XapianDb CTOR. This may throw some Xapian exception.
+ *
+ * @param db_path path to the database
+ * @param flavor kind of database
+ */
+ XapianDb(const std::string& db_path, Flavor flavor);
+
+ /**
+ * DTOR
+ */
+ ~XapianDb() {
+ if (tx_level_ > 0)
+ mu_warning("inconsistent transaction level ({})", tx_level_);
+ if (tx_level_ > 0) {
+ mu_debug("closing db after committing {} change(s)", changes_);
+ xapian_try([this]{ DB_LOCKED; wdb().commit_transaction(); });
+ } else
+ mu_debug("closing db");
+ }
+
+ /**
+ * Is the database read-only?
+ *
+ * @return true or false
+ */
+ bool read_only() const override;
+
+ /**
+ * Path to the database; empty for in-memory databases
+ *
+ * @return path to database
+ */
+ const std::string& path() const;
+
+ /**
+ * Get a description of the Xapian database
+ *
+ * @return description
+ */
+ const std::string description() const {
+ return db().get_description();
+ }
+
+ /**
+ * Get the number of documents (messages) in the database
+ *
+ * @return number
+ */
+ size_t size() const noexcept {
+ return xapian_try([this]{
+ DB_LOCKED; return db().get_doccount(); }, 0);
+ }
+
+ /**
+ * Is the the base empty?
+ *
+ * @return true or false
+ */
+ size_t empty() const noexcept { return size() == 0; }
+
+ /**
+ * Get a database enquire object for queries.
+ *
+ * @return an enquire object
+ */
+ Xapian::Enquire enquire() const {
+ DB_LOCKED; return Xapian::Enquire(db());
+ }
+
+ /**
+ * Get a document from the database if there is one
+ *
+ * @param id id of the document
+ *
+ * @return the document or an error
+ */
+ Result<Xapian::Document> document(Xapian::docid id) const {
+ return xapian_try_result([&]{
+ DB_LOCKED; return Ok(db().get_document(id)); });
+ }
+
+ /**
+ * Get metadata for the given key
+ *
+ * @param key key (non-empty)
+ *
+ * @return the value or empty
+ */
+ std::string metadata(const std::string& key) const override {
+ return xapian_try([&]{
+ DB_LOCKED; return db().get_metadata(key);}, "");
+ }
+
+ /**
+ * Set metadata for the given key
+ *
+ * @param key key (non-empty)
+ * @param val new value for key
+ */
+ void set_metadata(const std::string& key, const std::string& val) override {
+ xapian_try([&] { DB_LOCKED; wdb().set_metadata(key, val);
+ maybe_commit(); });
+ }
+
+ /**
+ * Invoke function for each key/value pair. This is called with the lock
+ * held, so do not call functions on @this is each_func().
+ *
+ * @param each_func a function
+ */
+ //using each_func = MetadataIface::each_func;
+ void for_each(MetadataIface::each_func&& func) const override {
+ xapian_try([&]{
+ DB_LOCKED;
+ for (auto&& it = db().metadata_keys_begin();
+ it != db().metadata_keys_end(); ++it)
+ func(*it, db().get_metadata(*it));
+ });
+ }
+
+ /**
+ * Does the given term exist in the database?
+ *
+ * @param term some term
+ *
+ * @return true or false
+ */
+ bool term_exists(const std::string& term) const {
+ return xapian_try([&]{
+ DB_LOCKED; return db().term_exists(term);}, false);
+ }
+
+ /**
+ * Add a new document to the database
+ *
+ * @param doc a document (message)
+ *
+ * @return new docid or 0
+ */
+ Result<Xapian::docid> add_document(const Xapian::Document& doc) {
+ return xapian_try_result([&]{
+ DB_LOCKED;
+ auto&& id{wdb().add_document(doc)};
+ set_timestamp(MetadataIface::last_change_key);
+ maybe_commit();
+ return Ok(std::move(id));
+ });
+ }
+
+ /**
+ * Replace document in database
+ *
+ * @param term unique term
+ * @param id docid
+ * @param doc replacement document
+ *
+ * @return new docid or an error
+ */
+ Result<Xapian::docid>
+ replace_document(const std::string& term, const Xapian::Document& doc) {
+ return xapian_try_result([&]{
+ DB_LOCKED;
+ auto&& id{wdb().replace_document(term, doc)};
+ set_timestamp(MetadataIface::last_change_key);
+ maybe_commit();
+ return Ok(std::move(id));
+ });
+ }
+ Result<Xapian::docid>
+ replace_document(Xapian::docid id, const Xapian::Document& doc) {
+ return xapian_try_result([&]{
+ DB_LOCKED;
+ wdb().replace_document(id, doc);
+ set_timestamp(MetadataIface::last_change_key);
+ maybe_commit();
+ return Ok(std::move(id));
+ });
+ }
+
+ /**
+ * Delete document(s) for the given term or id
+ *
+ * @param term a term
+ *
+ * @return Ok or Error
+ */
+ Result<void> delete_document(const std::string& term) {
+ return xapian_try_result([&]{
+ DB_LOCKED;
+ wdb().delete_document(term);
+ set_timestamp(MetadataIface::last_change_key);
+ maybe_commit();
+ return Ok();
+ });
+ }
+ Result<void> delete_document(Xapian::docid id) {
+ return xapian_try_result([&]{
+ DB_LOCKED;
+ wdb().delete_document(id);
+ set_timestamp(MetadataIface::last_change_key);
+ maybe_commit();
+ return Ok();
+ });
+ }
+
+ template<typename Func>
+ size_t all_terms(const std::string& prefix, Func&& func) const {
+ DB_LOCKED;
+ size_t n{};
+ for (auto it = db().allterms_begin(prefix); it != db().allterms_end(prefix); ++it) {
+ if (!func(*it))
+ break;
+ ++n;
+ }
+ return n;
+ }
+
+ /*
+ * If the "transaction ref count" > 0 (with inc_transactions());, we run
+ * in "transaction mode". That means that the subsequent Xapian mutation
+ * are part of a transactions, which is flushed when the number of
+ * changes reaches the batch size, _or_ the transaction ref count is
+ * decreased to 0 (dec_transactions()). *
+ */
+
+ /**
+ * Increase the transaction level; needs to be balance by dec_transactions()
+ */
+ void inc_transaction_level() {
+ xapian_try([this]{
+ DB_LOCKED;
+ if (tx_level_ == 0) {// need to start the Xapian transaction?
+ mu_debug("begin transaction");
+ wdb().begin_transaction();
+ }
+ ++tx_level_;
+ mu_debug("ind'd tx level to {}", tx_level_);
+ });
+ }
+
+ /**
+ * Decrease the transaction level (to balance inc_transactions())
+ *
+ * If the level reach 0, perform a Xapian commit.
+ */
+ void dec_transaction_level() {
+ xapian_try([this]{
+ DB_LOCKED;
+ if (tx_level_ == 0) {
+ mu_critical("cannot dec transaction-level)");
+ throw std::runtime_error("cannot dec transactions");
+ }
+
+ --tx_level_;
+ if (tx_level_ == 0) {// need to commit the Xapian transaction?
+ mu_debug("committing {} changes", changes_);
+ changes_ = 0;
+ wdb().commit_transaction();
+ }
+
+ mu_debug("dec'd tx level to {}", tx_level_);
+ });
+ }
+
+ /**
+ * Are we inside a transaction?
+ *
+ * @return true or false
+ */
+ bool in_transaction() const { DB_LOCKED; return tx_level_ > 0; }
+
+
+ /**
+ * RAII Transaction object
+ *
+ */
+ struct Transaction {
+ Transaction(XapianDb& db): db_{db} {
+ db_.inc_transaction_level();
+ }
+ ~Transaction() {
+ db_.dec_transaction_level();
+ }
+ private:
+ XapianDb& db_;
+ };
+
+
+ /**
+ * Manually request the Xapian DB to be committed to disk. This won't
+ * do anything while in a transaction.
+ */
+ void commit() {
+ xapian_try([this]{
+ DB_LOCKED;
+ if (tx_level_ == 0) {
+ mu_info("committing xapian-db @ {}", path_);
+ wdb().commit();
+ } else
+ mu_debug("not committing while in transaction");
+ });
+ }
+
+ using DbType = std::variant<Xapian::Database, Xapian::WritableDatabase>;
+private:
+
+ /**
+ * To be called after all changes, with DB_LOCKED held.
+ */
+ void maybe_commit() {
+ // in transaction-mode and enough changes, commit them
+ // and start a new transaction
+ if (tx_level_ > 0 && ++changes_ >= batch_size_) {
+ mu_debug("batch full ({}/{}); committing change", changes_, batch_size_);
+ wdb().commit_transaction();
+ wdb().commit();
+ --tx_level_;
+ changes_ = 0;
+ wdb().begin_transaction();
+ ++tx_level_;
+ }
+ }
+
+ void set_timestamp(const std::string_view key);
+
+ /**
+ * Get a reference to the underlying database
+ *
+ * @return db database reference
+ */
+ const Xapian::Database& db() const;
+ /**
+ * Get a reference to the underlying writable database. It is
+ * an error to call this on a read-only database.
+ *
+ * @return db writable database reference
+ */
+ Xapian::WritableDatabase& wdb();
+
+ mutable std::mutex lock_;
+ std::string path_;
+ DbType db_;
+ size_t tx_level_{};
+ const size_t batch_size_;
+ size_t changes_{};
+};
+
+constexpr std::string_view
+format_as(XapianDb::Flavor flavor)
+{
+ switch(flavor) {
+ case XapianDb::Flavor::CreateOverwrite:
+ return "create-overwrite";
+ case XapianDb::Flavor::Open:
+ return "open";
+ case XapianDb::Flavor::ReadOnly:
+ return "read-only";
+ default:
+ return "??";
+ }
+}
+
+static inline std::string
+format_as(const XapianDb& db)
+{
+ return mu_format("{} @ {}", db.description(), db.path());
+}
+
+} // namespace Mu
+
+#endif /* MU_XAPIAN_DB_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2022-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include <glib.h>
+#include <string>
+#include <thread>
+#include <vector>
+#include <iostream>
+#include <fstream>
+
+#include <utils/mu-utils.hh>
+#include <utils/mu-regex.hh>
+#include <mu-store.hh>
+#include "mu-maildir.hh"
+
+#include "utils/mu-test-utils.hh"
+
+using namespace Mu;
+
+constexpr auto test_msg =
+R"(Return-Path: <htcondor-users-bounces@cs.wisc.edu>
+Received: from pop3.web.de [212.227.17.177]
+ by localhost with POP3 (fetchmail-6.4.6)
+ for <arne@localhost> (single-drop); Fri, 26 Jun 2020 12:56:08 +0200 (CEST)
+Received: from jeeves.cs.wisc.edu ([128.105.6.16]) by mx-ha.web.de (mxweb112
+ [212.227.17.8]) with ESMTPS (Nemesis) id 1MdMYE-1jFXaM2gnA-00ZKvt for
+ <@ID@@web.de>; Fri, 26 Jun 2020 01:28:11 +0200
+Received: from jeeves.cs.wisc.edu (localhost [127.0.0.1])
+ by jeeves.cs.wisc.edu (8.14.4/8.14.4) with ESMTP id 05PNLgek013419;
+ Thu, 25 Jun 2020 18:22:23 -0500
+Received: from shale.cs.wisc.edu (shale.cs.wisc.edu [128.105.6.25])
+ by jeeves.cs.wisc.edu (8.14.4/8.14.4) with ESMTP id 05PNLaf0013414
+ (version=TLSv1/SSLv3 cipher=AES256-GCM-SHA384 bits=256 verify=OK)
+ for <htcondor-users@jeeves.cs.wisc.edu>; Thu, 25 Jun 2020 18:21:36 -0500
+Received: from smtp7.wiscmail.wisc.edu (wmmta4.doit.wisc.edu [144.92.197.245])
+ by shale.cs.wisc.edu (8.14.4/8.14.4) with ESMTP id 05PNLaMK013694
+ (version=TLSv1/SSLv3 cipher=DHE-RSA-AES128-GCM-SHA256 bits=128
+ verify=NO)
+ for <htcondor-users@cs.wisc.edu>; Thu, 25 Jun 2020 18:21:36 -0500
+Received: from USG02-CY1-obe.outbound.protection.office365.us
+ ([23.103.209.108]) by smtp7.wiscmail.wisc.edu
+ (Oracle Communications Messaging Server 8.0.2.4.20190812 64bit (built
+ Aug 12
+ 2019)) with ESMTPS id <0QCI042LC8VUXFC0@smtp7.wiscmail.wisc.edu> for
+ htcondor-users@cs.wisc.edu (ORCPT htcondor-users@cs.wisc.edu); Thu,
+ 25 Jun 2020 18:21:31 -0500 (CDT)
+X-Spam-Report: IsSpam=no, Probability=11%, Hits= RETURN_RECEIPT 0.5,
+ FROM_US_TLD 0.1, HTML_00_01 0.05, HTML_00_10 0.05, SUPERLONG_LINE 0.05,
+ BODYTEXTP_SIZE_3000_LESS 0, BODY_SIZE_10000_PLUS 0, DKIM_SIGNATURE 0,
+ KNOWN_MTA_TFX 0, NO_URI_HTTPS 0, SPF_PASS 0, SXL_IP_TFX_WM 0,
+ WEBMAIL_SOURCE 0, WEBMAIL_XOIP 0, WEBMAIL_X_IP_HDR 0, __ANY_URI 0,
+ __ARCAUTH_DKIM_PASSED 0, __ARCAUTH_DMARC_PASSED 0, __ARCAUTH_PASSED 0,
+ __ATTACHMENT_SIZE_0_10K 0, __ATTACHMENT_SIZE_10_25K 0,
+ __BODY_NO_MAILTO 0,
+ __CT 0, __CTYPE_HAS_BOUNDARY 0, __CTYPE_MULTIPART 0, __HAS_ATTACHMENT 0,
+ __HAS_ATTACHMENT1 0, __HAS_ATTACHMENT2 0, __HAS_FROM 0, __HAS_MSGID 0,
+ __HAS_XOIP 0, __HIGHBITS 0, __MIME_TEXT_P 0, __MIME_TEXT_P1 0,
+ __MIME_TEXT_P2 0, __MIME_VERSION 0, __MULTIPLE_RCPTS_TO_X2 0,
+ __NO_HTML_TAG_RAW 0, __RETURN_RECEIPT_TO 0, __SANE_MSGID 0,
+ __TO_MALFORMED_2 0, __TO_NAME 0, __TO_NAME_DIFF_FROM_ACC 0,
+ __TO_NO_NAME 0,
+ __TO_REAL_NAMES 0, __URI_IN_BODY 0, __URI_MAILTO 0, __URI_NOT_IMG 0,
+ __URI_NO_PATH 0, __URI_NS , __URI_WITHOUT_PATH 0
+X-Wisc-Doma: @ID@X@numerica.us,numerica.us
+X-Wisc-Env-From-B64: d2VzbGV5LnRheWxvckBudW1lcmljYS51cw==
+X-Spam-PmxInfo: Server=avs-13, Version=6.4.7.2805085,
+ Antispam-Engine: 2.7.2.2107409, Antispam-Data: 2020.6.25.231519,
+ AntiVirus-Engine: 5.74.0, AntiVirus-Data: 2020.6.25.5740002,
+ SenderIP=[23.103.209.108]
+X-Wisc-DKIM-Verify: @ID@XXXXXXX@numerica.us,numericaus.onmicrosoft.com!pass
+X-Spam-Score: *
+ARC-Seal: i=1; a=rsa-sha256; s=arcselector5401; d=microsoft.com; cv=none;
+ b=KyXoddJsnsHsBwhdlO5rcljgMRaylJAUAxWTjG4jQL1C8XJAMgeERtH2sRffdjibYUFfSuDUNJmrTrvrbjKGUt2I8J2M2MgUB/upMoroVPNBrP1Fy9wMeZJQuSS4r4KjZZktsl2i8eq667pzOZO6+wX2IA5M7YtxDqglcWOE6btWzbABVjx+9eCXMt0eMd1+UI6ABK8Frd33EFQLKT0h/cxidWR9l+0gCMAcRxsLrQ82+ckU606AIV/DA1E4Tq7ADe/+CRv4QszDN93pWL/1N2/OOh9vFTs9g9ZG6uXjN+Km/IAdylPbfHgKW60ev3/Bvv6N3pA7DjpuiKj6BnW7mQ==
+ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com;
+ s=arcselector5401;
+ h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck;
+ bh=OZrj1we1ZUH0xBMhJ5/F6EQnB0cmitFs2xZW1fLMRNs=;
+ b=Pq07a3u26s2UdpucJuVQ0h68272wx46Wp61x/30TelPPFLCRxVjmlH1U3IBmIsZ1jOEtGXFJRv65L3HmwGxRUdLlMOdPRB64BBfHQ9NGWUBykKQmOrJNGJs635nEdpugpzngzIdcg1PS5vHxPJAnOeqoo71OVPI3JqPrPEn2TJJgb9J6PApexkqIbVl35prGPsyS/t2IlYw3/ihWzORG6wvqJeqedgpJTBXeGaDoMa+MQ1BeUsdvybh8+hau4ASpM5lwyeXlGmJ5mUTZi39jp+dFdDrmCj/VM4ezeuXeH9+HFtDjKLZJaTDWUID0IBcr91BaoQE/4r6y+lpkah6LLQ==
+ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=pass
+ smtp.mailfrom=numerica.us;
+ dmarc=pass action=none header.from=numerica.us;
+ dkim=pass header.d=numerica.us; arc=none
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
+ d=numericaus.onmicrosoft.com; s=selector1-numericaus-onmicrosoft-com;
+ h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck;
+ bh=OZrj1we1ZUH0xBMhJ5/F6EQnB0cmitFs2xZW1fLMRNs=;
+ b=cFn0eL5k2IKry9U8qa8mbVaxRiyicUAWzRc3NUtj+VEbgShfrz8SO6FPX20WTQQJg/Fu/3isqsSEUt+9NSEEbgd5eQ1EVz5E/JVeNjPe9GXR0JEF/g3f6yM7CO+kKTvXSRvQjce683U0j7Aj1pSDEktoVNP4xvOS2Gx9VjdWTmc=
+Received: from DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM (2001:489a:200:413::10)
+ by DM3P110MB0490.NAMP110.PROD.OUTLOOK.COM (2001:489a:200:413::14)
+ with Microsoft SMTP Server
+ (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384)
+ id 15.20.3109.25; Thu, 25 Jun 2020 23:21:07 +0000
+Received: from DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM
+ ([fe80::f548:f084:9867:9375]) by DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM
+ ([fe80::f548:f084:9867:9375%11]) with mapi id 15.20.3131.024; Thu,
+ 25 Jun 2020 23:21:07 +0000
+From: Raul Endymion <XXXXXXXXXXXXXXXXXXXX@numerica.us>
+To: "'htcondor-users@cs.wisc.edu'" <htcondor-users@cs.wisc.edu>
+Thread-topic: OPINIONS WANTED: Are there any blatent downsides I am missing to
+ the following Condor configuration
+Thread-index: AdZLRbEvYoEDBZChS62aOHgPzKD8kw==
+Date: Thu, 25 Jun 2020 23:21:06 +0000
+Message-id: <DM3P110MB04746CDBA55B3E597EFD1877FA920@DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM>
+Accept-Language: en-US
+Content-language: en-US
+X-MS-Has-Attach: yes
+X-MS-TNEF-Correlator:
+X-Originating-IP: [50.233.29.54]
+x-ms-publictraffictype: Email
+x-ms-office365-filtering-correlation-id: f4edecdf-5582-4b2d-226a-08d8195e7007
+x-ms-traffictypediagnostic: DM3P110MB0490:
+x-microsoft-antispam-prvs: <DM3P110MB04907C313C4243B4FE42A20CFA920@DM3P110MB0490.NAMP110.PROD.OUTLOOK.COM>
+x-ms-oob-tlc-oobclassifiers: OLM:10000;
+x-forefront-prvs: 0445A82F82
+x-ms-exchange-senderadcheck: 1
+x-microsoft-antispam: BCL:0;
+X-Forefront-Antispam-Report: CIP:255.255.255.255; CTRY:; LANG:en; SCL:1; SRV:;
+ IPV:NLI; SFV:NSPM; H:DM3P110MB0474.NAMP110.PROD.OUTLOOK.COM;
+ PTR:; CAT:NONE; SFTY:;
+ SFS:(346002)(366004)(6916009)(83380400001)(71200400001)(8936002)(55016002)(5660300002)(8676002)(9686003)(66616009)(64756008)(33656002)(52536014)(66446008)(66476007)(66556008)(66946007)(99936003)(76116006)(186003)(86362001)(26005)(7696005)(6506007)(44832011)(508600001)(2906002)(80162005)(80862006)(491001)(554374003);
+ DIR:OUT; SFP:1102;
+x-ms-exchange-transport-forked: True
+MIME-version: 1.0
+X-OriginatorOrg: numerica.us
+X-MS-Exchange-CrossTenant-Network-Message-Id: f4edecdf-5582-4b2d-226a-08d8195e7007
+X-MS-Exchange-CrossTenant-OriginalArrivalTime: 25 Jun 2020 23:21:06.8341 (UTC)
+X-MS-Exchange-CrossTenant-fromentityheader: Hosted
+X-MS-Exchange-CrossTenant-id: fae7a2ae-df1d-444e-91be-babb0900b9c2
+X-MS-Exchange-CrossTenant-mailboxtype: HOSTED
+X-MS-Exchange-Transport-CrossTenantHeadersStamped: DM3P110MB0490
+Subject: [HTCondor-users] OPINIONS WANTED: Are there any blatent downsides I
+ am missing to the following Condor configuration
+X-BeenThere: htcondor-users@cs.wisc.edu
+X-Mailman-Version: 2.1.19
+Precedence: list
+List-Id: HTCondor-Users Mail List <htcondor-users.cs.wisc.edu>
+List-Unsubscribe: <https://lists.cs.wisc.edu/mailman/options/htcondor-users>,
+ <mailto:htcondor-users-request@cs.wisc.edu?subject=unsubscribe>
+List-Archive: <https://www-auth.cs.wisc.edu/lists/htcondor-users/>
+List-Post: <mailto:htcondor-users@cs.wisc.edu>
+List-Help: <mailto:htcondor-users-request@cs.wisc.edu?subject=help>
+List-Subscribe: <https://lists.cs.wisc.edu/mailman/listinfo/htcondor-users>,
+ <mailto:htcondor-users-request@cs.wisc.edu?subject=subscribe>
+Reply-To: HTCondor-Users Mail List <htcondor-users@cs.wisc.edu>
+Content-Type: multipart/mixed; boundary="===============0678627779074767862=="
+Errors-To: htcondor-users-bounces@cs.wisc.edu
+Sender: "HTCondor-users" <htcondor-users-bounces@cs.wisc.edu>
+Envelope-To: <@ID@XXXXX@web.de>
+X-UI-Filterresults: unknown:2;V03:K0:cdojl5YHfkg=:jhTbQXp38SL2za/LB4M7aUwpyw
+ 5rDHoN1+/ScH/O9/G1fKWbGryQ203thF+1ZrHUOOwq8MVOc5SsoqzSTsaNbEAdthFcDDz3Oui
+ SHxX1hdpV3UOjZEHzWlpjEjRe7t74g2RI/ESELmkPuLg/LZC7SjAsg70cTJBIfDPYxJkJAcUl
+ 9W6OEBsmtTDO0va/EQRYjfkpoF9tjfmfMNw9KSKHuDdqZu2Xfak8mQKnWsoxWeUkD31r60iPC
+ yikbj7KP5AlHaWMzyTTdlvtjYRLfSuUSe1uqjI5NWCnZDDjz7zODoaWPp7p2U/MQenXEjN6+M
+ WnZL6ZC8AGtze/hYgOCXcLf4ydQ7m9YueJiY5nDn7g+cwnhxypVNFTL5NjSpKKXbkzbyu9Tdl
+ ez+92g/9pGW17iOo5NrFtfctLlmCEH0RxjouKI7FBmv3bIvFC4FvfghiNf7OZmRg2/nT5i+1o
+ AICYNAx2y5CezKsKM2f1tm60dkydQIR8pK45dDKZPz3i7NeJm9dknZ2OYFTnucUvdPaT8nR43
+ cK3kk2QUE48Ngo/0NwepSGrV9TkOt+hY3PUYkXWp/mwP2QPSjy4cALyvLyKwG24qZ9CiiRLMV
+ KqPFlCRnoDG5MHJ4d0krFlqmg8rNsWzV3oWfMNKFZmD24lVUmWGb+ExxbCFc0xzIt12o/EqBw
+ nVkXLu+E0apM+cmG6ubYfOymRoUpiKsZI9ivc+mAaEE+v2RBzcAURzlhzQHIn81onvzbQwCge
+ tMtBkSEfqyoa1HjalX4B9WQ90M42K+7xW039ydakQ7JOeYVpkPXYoBF7mbrXRckhMXjQatLQ8
+ MWA8+U031Xfa1ueOIfCCkzJ49wyx1LoLPyqdjCvnzaRd72yNEMJ5zM/itMIPE9reIHBtpom0i
+ RhIYdJFDrL+SKqE68lcJakCcF3R+VLApwLKOr0HChGQjdEk7c/rm5E0dF5f3oYlHf591QoXIJ
+ h16yfcJYe6fMo1YYunkvbEDFPpzttIq7aIk0FzxrOdRvj3yQajDbwOpYI/5T/DaabPn3M8lK7
+ 8pn7LrbmyCaLHhkYMS4h3SDkYWsifza6vkldizrK7IPf6KhS7AhTkbnEonWS6454GLUg1nYGX
+ W5Qp/G9LzvjtEGQMcwnCN5jb5zq7o3f+9FrROKjpwFxL+mL85CEXY/KMOVpf6hDJJfSyu6/X6
+ FpbwlJVLFdGeA0/+xcKcmutpkJACgK2kHqvZ8MZxt+5jBJWVlIDLZKa8/IoGWC+ikLX2/hPNB
+ 4TU89QYG5ygPmwwDXruFG7N7jVURZceHqWNKtqegS6YQ5nirsPJWJR7jzgr+HbntUaQETXNpn
+ QrxpsVHXfqRu2GlP5h28RaIpvBVUcwqrs+eLJELStvBzyAmaVPVoKFjEWFfwrmE89W6Bmz2W3
+ kHExOq3hI3gDsGXKjTjT/kjHkaHmtnVUXr4vqovf8Ht4Vwmtf0S4xsgpYjnYjUIzG9eiwIFAZ
+ hL2gvjwW51qtMvybf01C50xTiS9GSfO0SR7meBPA67skcA+wFo11wmwXsUk1irpKnC+Y9hVZX
+ 2vPkfZ1T2VXNo997cQC59lBpi/TU5gnuM7H/Vcl7tF3Lqtmqut7s6HkPWCegDZ3O2W7shH7aZ
+ 1bOXbO+W/SNC+WcMnj+fhuP+dHcrt0Vw4RD9knJOOzdZTH3OCli/vpjqgTbCKEaWMhCIeM2g0
+ RiLFxTeTEEBCa49bwa8n2r4T/vA3duZd8F/DNKvWTfhRr1Mxtz3n15EOar13fFijtnieEiv4/
+ vO/5uRF+H86Fcoua7B8AswThbiG1vou6M48g0Zo6iGEcrueKEaHMI4XM7wQF77KazMdn5f1BP
+ +KyQX83aHJN/qGniXgF8yu+h0M7Nf0YrTteYQd2C/HZrIA8IaLqqvLoGRl7dRBnbZiP7jRdQm
+ 1YEYtjX4XBoShrXPfIxPnJBUBnnOaePYxOJkS2FaBv19jPkMnyc9xuJYD7JOTFnXKzAnoaBqT
+ OR+dGrLLGZ1MM/0gqclKTv7Hcce+6CJyTWkx5mq42w49HFI/kdHBRxU8xIRv4B9l0ePf9EbWr
+ cDcrssee//6KXiRmF4fm7jq828/uhj8MIJet9sIU5ncKwHEse3I4YmVT5+dB+ZGZh0gbJPFj6
+ xcICpshhYct+euMCdNfy3lkxiRr76RwfBzLAOP5+1U3GAx/hcsL2AgyBHMwWo+Kkeq8pPy4YI
+ pQMxJyylI6JMa/DbBggnDk+xNZpRKo/XA4lAJY57DCOPL2ZcL8kU2aCd5LjtYHK0ZWSFtOjxs
+ oIEr/f2vvg+zibxzaANBzylZn3yPe9pI/IBefu9fL4MVaYY3aboxuncX4fyi0VH0WbFkSYXRi
+ a7LIu3LI2LTU13C/LE7j9hmxP6TApyiXi14f0GSa2sbF6HWp2v2rhYM7h67AAn3SQgvcJLpgb
+ Hz5ABb/OAk6ABVEl+a483zexJ6iT2P0gYc08zmewy8Jf8AD9r846k9pGZuhBaOHREx3bA16Bj
+ uWYh3QzSI6MQoJM3XbBGLVkX36Lfj54T9kk97lLaxfbGPuNoyOV9iTBKxts3m2KD+52iH3EEi
+ glbH6HNIUHyCHdEXsXyGVFwfM9V7OQcVO/g266KIQ74wU16x/Zdsq4p/1PcRXHRnoMxP/pUrj
+ EOLWzFU71qzC/OSkYWRil9HXUyucTFGQ0N08jZNXctI9lElWtgq3iI+Cz2F20rz+LJGhSHSkZ
+ 0G5JgXrtspeJN5yoH6TOE0hblr5sZcAM0wiSP7x/hPBeYHswzTA5/laWMn++9aTPVgpPaJ9/x
+ wyLm55OZr4Jl+StWd3MqLCgiRB3cNGrDX7f8Eqnj4wfCHiGIUHewD4qrfXraZQhIk17W+9JyD
+ osmUiVD9ZRdNCY2eNnu8ZkJ4uzKl44lwLL43sInKBjdAHlnoxrR2FOrYXbnU31ujwxdeUr6Hs
+ xPFy0Git0CpWCWYmaz37KA8GW7PE4ffWzcfCmz6AKBrbHcCreeUnyqnSEDy9ubnz7mcLRnu3W
+ RAWi6diI8gcS9g0+r4z5PtZX9rveXRekHJ4k08VuYVmdiz3gjXmHPlm9IKPEAbygP2EYgjwGE
+ RbReLc8xHJlfLbwdXyGw0HU=
+
+--===============0678627779074767862==
+Content-language: en-US
+Content-type: multipart/signed; protocol="application/x-pkcs7-signature";
+ micalg=2.16.840.1.101.3.4.2.3;
+ boundary="----=_NextPart_000_0018_01D64B14.F58791A0"
+
+------=_NextPart_000_0018_01D64B14.F58791A0
+Content-Type: text/plain;
+ charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+
+Hey!
+
+I am architecting our final HTCondor configuration over here and I have =
+an idea I am unsure about and I would like to ask some experienced users =
+for their opinion.
+
+Background, we have a small, relatively homogenous cluster (with no =
+special universes) and less than 10 users. Since each user has their own =
+workstation separate from our cluster I thought the following =
+configuration would suit our needs, but I want to make sure there isn't =
+a huge disadvantage I am missing:
+
+1. Set the Central Manager to be highly available to the point of =
+tolerating N cluster machine failures
+2. Put a Submit on each of the users' workstations (I am a little =
+worried about the resource usage of the condor_shadow and condor_schedd, =
+my users are already running into RAM consumption issues over time as it =
+is)
+3. Place an Execute on each of the cluster machines, which would lead to =
+the central manager being on a machine that is also executing jobs
+
+Fortunately both my users' and cluster machines all have access to the =
+same network storage, and we have centralized authentication so we can =
+just use our users' credentials to authenticate everywhere.=20
+
+Before I set this in dry mud, does anyone have any retrospective =
+recommendations I could benefit hearing from, since I am still pretty =
+new to the project?
+
+Thank you!
+-Raul
+
+Raul Endymion =E2=80=93 Cluster Manager
+Numerica Corporation (www.numerica.us)
+5042 Technology Parkway #100
+Fort Collins, Colorado 80528
+=E2=98=8E=EF=B8=8F (970) 207 2233
+=F0=9F=93=A7 @ID@XXXXXXXXXXXXXXX@numerica.us
+
+
+
+------=_NextPart_000_0018_01D64B14.F58791A0
+Content-Type: application/pkcs7-signature;
+ name="smime.p7s"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="smime.p7s"
+
+MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgMFADCABgkqhkiG9w0BBwEAAKCCEv4w
+ggWpMIIDkaADAgECAhAV2Tfkh0+gtEu0gskeSMTdMA0GCSqGSIb3DQEBCwUAMFsxEjAQBgoJkiaJ
+k/IsZAEZFgJ1czEYMBYGCgmSJomT8ixkARkWCG51bWVyaWNhMRIwEAYKCZImiZPyLGQBGRYCYWQx
+FzAVBgNVBAMTDmFkLUdJTEdBTEFELUNBMB4XDTE2MDcyNDE5NTcxM1oXDTM2MDcyNDIwMDcxMlow
+WzESMBAGCgmSJomT8ixkARkWAnVzMRgwFgYKCZImiZPyLGQBGRYIbnVtZXJpY2ExEjAQBgoJkiaJ
+k/IsZAEZFgJhZDEXMBUGA1UEAxMOYWQtR0lMR0FMQUQtQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC
+DwAwggIKAoICAQCq+/935KPrc8clxrq76k7GrrUHRbsM4FCfyrWicGPZsOKbJfcoloF2EAfj6AYR
+QyU/l9um/8NqW+cu6/TY6YcY622L+UtT1QWC/Kt0kVL7cTtZN+VK/BkjcDVbUOqdeFY1q0tMzdco
+WFxqjayGRYnX6oEZ7krDsGtJBBET/504Z3vDq/0ZD3lNG2dCWp1y+3VzUcb+OKkOPwMGHpw3gZM5
+lZN/znB7d7qwxFSRoLzZZB3nZKKJHcp2ZuyJR+pCT5VdHGGV4gpVQKuL49/UoJBA0o8Kv0DGPByD
++LVwhlyFMi2jlnCd5lqiWRw9JAE3fqS/Di/cGbMjXMI2CplBj+GmZH8fgy4BQRwmsOUELTaYkJyJ
+otcHGENO1+xYrR/lFEQLhh+8V2IJvBM2G1dgJ3EuEslL4q0xGeYLZJd7Z9xvXkAJaX/eWjHWICFI
+zbsH/6fBqXYow/V8hfZhb20dGGnPESXPqMv/1mLgUIqr++Fjl6zKM5mYZuHlmrtd+eLgg7VsjDvh
+cMxdQnju+jzJflxlmY2KSwt5lsu7viqmQyqVUnHFaEsV116B0uCROc5o1pBdRMdeeLrRoj6xPVlc
+IzmIZz3wZERxCAWeJqBx5d1kXe+cDL4pMNQ/hmah4mshjtyOGv+oEgcdxzUQ72W7JNLhSv8C6gpU
+eQwPq8usFAvUOwIDAQABo2kwZzATBgkrBgEEAYI3FAIEBh4EAEMAQTAOBgNVHQ8BAf8EBAMCAYYw
+DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUF+CLMX/eZk96ElRSeiEHqnsujqEwEAYJKwYBBAGC
+NxUBBAMCAQAwDQYJKoZIhvcNAQELBQADggIBACcwALtn+SFUx+YTrLCFY+Ghh4yubQt3YdEI6hOQ
+JnmNPKsUEzCvoRE5L2ZLkG2VhJNX3KAJmXgkZMCGBPbiA/65r/cbYqZATQEG/g9aVicz/IBHXvg4
+7+YDDN9VpRy8c93AZNNTRf83Pw+CDsdIGG7mg8rc0tiCgt0V3gN0wF8oRSsb/trqd+ujk41bvaPw
+Rl+8JUeRN0Pq9lH4VGGk9GEIQv8JXhr2VKFmJcGKLB+qvMRvWQZ5oPTGDE3pUYI5q8f7/fMiJKU6
+hb9l+tXP7uDLWIawg/MoUc2BwAThyXFk9LZhkYWYpzbaf2Ez2JYieD4ey8RjEKvis9mF6Z/p6+69
+GbYvuf2bRikYenrmboXCUO820totjP2UyHczexZsMP/XznmyDJuN+BDLzLjm7ks8lXDwpF/Kqnjm
+1EyiQI0OB4cn889yM039U7raJeHpuiwju2/YO6krE+plLQhkM7pl6v6Ly/ZKICwDfbcU8k8LE4+K
+3VaXmVYRYbSXx8l2Ke0CWKNfehBGQ024gKjNt8t7gCgInG5s+roumqeKyfCWlhYll1FAxEQmwP/6
+966y7uJrGLra0VUjdppbZpAENSF0pdX08VfsasSZ20hnCaLWO1b3i0ZOBLBAoNzeCm+BdS6DAOhy
+JnHHZ+OBoiaYwCSjSvTDmHyQkNK3wmu+/wyNMIIGnDCCBISgAwIBAgITbwAAAEFhCq43is5OqAAA
+AAAAQTANBgkqhkiG9w0BAQsFADBbMRIwEAYKCZImiZPyLGQBGRYCdXMxGDAWBgoJkiaJk/IsZAEZ
+FghudW1lcmljYTESMBAGCgmSJomT8ixkARkWAmFkMRcwFQYDVQQDEw5hZC1HSUxHQUxBRC1DQTAe
+Fw0xOTA3MjIxNDE4MDFaFw0yMTA3MjIxNDI4MDFaMFwxEjAQBgoJkiaJk/IsZAEZFgJ1czEYMBYG
+CgmSJomT8ixkARkWCG51bWVyaWNhMRIwEAYKCZImiZPyLGQBGRYCYWQxGDAWBgNVBAMTD2FkLUNF
+TEVCUklBTi1DQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKRLgjg0yC0P2jLwTCIA
+V/zEGk/PEc3pZxNAo7m0I/SXdNulUEkjxai5Wq53i0EhWVLpUU8XY3joXax46yCMqh0PUn90QmMD
+BybLyFDX6av8tVS5cQs0HbTZdIuj7A/dsKzKKIrSHd3SQ9MLNPRkSRdhagmf5LCF1Y4xEEiuAA/H
+XdYAxGIcl8n6b2CcLlZzq4W13Ipv8FIZoDsG1u0b9NGfeSOOHidi5kdD6r8lM5PaSPmZsl5PdKK6
++E1Y6rBCvITu0MBo5Tjuwt5cok3Ve0BK5Fg89aIL2/rMicm20qG6nbqxLhHeR0mhPO98KIIzDoeL
+rLpAlWS7GoPvJqbRzxsCAwEAAaOCAlYwggJSMBAGCSsGAQQBgjcVAQQDAgEBMCMGCSsGAQQBgjcV
+AgQWBBSv5TU1Bjnw5n3u1iO2y+BHQXk7MTAdBgNVHQ4EFgQUoeMyqBhiyBcgwJN8zbr7pRbgs+sw
+GQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB
+Af8wHwYDVR0jBBgwFoAUF+CLMX/eZk96ElRSeiEHqnsujqEwgdMGA1UdHwSByzCByDCBxaCBwqCB
+v4aBvGxkYXA6Ly8vQ049YWQtR0lMR0FMQUQtQ0EsQ049R2lsZ2FsYWQsQ049Q0RQLENOPVB1Ymxp
+YyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNvbmZpZ3VyYXRpb24sREM9YWQsREM9
+bnVtZXJpY2EsREM9dXM/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENsYXNz
+PWNSTERpc3RyaWJ1dGlvblBvaW50MIHGBggrBgEFBQcBAQSBuTCBtjCBswYIKwYBBQUHMAKGgaZs
+ZGFwOi8vL0NOPWFkLUdJTEdBTEFELUNBLENOPUFJQSxDTj1QdWJsaWMlMjBLZXklMjBTZXJ2aWNl
+cyxDTj1TZXJ2aWNlcyxDTj1Db25maWd1cmF0aW9uLERDPWFkLERDPW51bWVyaWNhLERDPXVzP2NB
+Q2VydGlmaWNhdGU/YmFzZT9vYmplY3RDbGFzcz1jZXJ0aWZpY2F0aW9uQXV0aG9yaXR5MA0GCSqG
+SIb3DQEBCwUAA4ICAQBmRoSlPe++k7tsAJOvq0+0dNI6yk6gOBmY4g5jL9NTEjSxPWkeYegIwLr2
+UqpiIIZmAh9e9v3z0T2egVyRqNezLPXLkg/2gUfV6D0kRyKtG5mL0yAn/0hkkVyf6jWJpCKmH77x
+0w3UpnfKs79jv5YpQDhC2eRFivN50HhIkigLWScPq4zd81ghmN8VFTHVQmsGua/mm1Oj5/pBFuQF
+B4ljon1N//wX5ZJZaUlJR9eR9tM9m+Gyds2flr5+mZT6Zgm26fKiC5zs91aGnzqGx6s30jfXELP2
+FjFrrR46ooV7ehhnyBlCACxIWqXe5sSZsSh9oEYZ7Ux5Vq0thkfArBWsF7HA+LovKCUyHLcXbVBB
+6/VAwZ3GLYi/bqbVIEFlVRu4nv/JyKWwoGbAhGyzZNWoeHszFrEIQbQMoMsEumVkMZreE6AxP+zb
+6JPPOjlhpymtMo54z1MDYJPyo4HmcpL4xUjHZgqgOxMrbHC4oIVLvKZ/scbVBhPnd0tHHSZqj3ZS
+gfTvG/ut/tLNTXXe48PkLBw4KguhbLm61Elu3wJALT0UL+ENgUWwb7csUGQBqOyPAHXGYnf/ACOc
+UBqQckcrK8Jq3u8rnCloW3uDw86hw7MFM+YjmhVRdYRxpJmhKVPT6Amufp2WsSVId8q3CSqTH33L
+fcxbV1n7hLWHA67MhTCCBq0wggWVoAMCAQICEycAAAsJMaw2RjtHZFUAAQAACwkwDQYJKoZIhvcN
+AQELBQAwXDESMBAGCgmSJomT8ixkARkWAnVzMRgwFgYKCZImiZPyLGQBGRYIbnVtZXJpY2ExEjAQ
+BgoJkiaJk/IsZAEZFgJhZDEYMBYGA1UEAxMPYWQtQ0VMRUJSSUFOLUNBMB4XDTIwMDUxMjE1MDk0
+MloXDTIxMDcyMjE0MjgwMVowgcExEjAQBgoJkiaJk/IsZAEZFgJ1czEYMBYGCgmSJomT8ixkARkW
+CG51bWVyaWNhMRIwEAYKCZImiZPyLGQBGRYCYWQxETAPBgNVBAsTCE51bWVyaWNhMQ4wDAYDVQQL
+EwVVc2VyczEYMBYGA1UECxMPUHJlc2VudCBJbnRlcm5zMRYwFAYDVQQDEw1XZXNsZXkgVGF5bG9y
+MSgwJgYJKoZIhvcNAQkBFhl3ZXNsZXkudGF5bG9yQG51bWVyaWNhLnVzMIIBIjANBgkqhkiG9w0B
+AQEFAAOCAQ8AMIIBCgKCAQEA5clDLapXkiLVXhAFP9GJv+JJkt+cacyvWaX9xEvqMQXOXb7MqO5E
+DJE8XPMfxaX84WhuMMePOc9SNUKpDtTa2SHz+AOom+JH38ce2gfrdOPwez/e6RrUb3o8ZvMr3hJl
+Yy+6vEFEADIICfHSlIjkLJbGNFTRDccvkOPjD2W+fmzFAtWyNb/eqM+mwdTuXjOxTvP6V34zJsvc
+YKJUzhhD8jI7GdqOoNoirTlaMVTH5udK0P2KvzD6F0LfwcOlc3bTvY9uI585xhdniK4yAIka8OMq
+5zmyEQLYOadcVSscjAlkC1sQ0gbwL3AdwS+bntryq+2Ds380OJ+Z1Uy7TRkeBQIDAQABo4IDADCC
+AvwwPAYJKwYBBAGCNxUHBC8wLQYlKwYBBAGCNxUI9/Bss4wDhbmBGISeqheH4YBfgSWC6qJEgcjE
+IgIBZQIBKDATBgNVHSUEDDAKBggrBgEFBQcDBDAOBgNVHQ8BAf8EBAMCBaAwGwYJKwYBBAGCNxUK
+BA4wDDAKBggrBgEFBQcDBDBEBgkqhkiG9w0BCQ8ENzA1MA4GCCqGSIb3DQMCAgIAgDAOBggqhkiG
+9w0DBAICAIAwBwYFKw4DAgcwCgYIKoZIhvcNAwcwHQYDVR0OBBYEFDZHoDwoOKD5uzpF/2CcZSeg
+XWLmMB8GA1UdIwQYMBaAFKHjMqgYYsgXIMCTfM26+6UW4LPrMIHVBgNVHR8Egc0wgcowgceggcSg
+gcGGgb5sZGFwOi8vL0NOPWFkLUNFTEVCUklBTi1DQSxDTj1DZWxlYnJpYW4sQ049Q0RQLENOPVB1
+YmxpYyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNvbmZpZ3VyYXRpb24sREM9YWQs
+REM9bnVtZXJpY2EsREM9dXM/Y2VydGlmaWNhdGVSZXZvY2F0aW9uTGlzdD9iYXNlP29iamVjdENs
+YXNzPWNSTERpc3RyaWJ1dGlvblBvaW50MIHHBggrBgEFBQcBAQSBujCBtzCBtAYIKwYBBQUHMAKG
+gadsZGFwOi8vL0NOPWFkLUNFTEVCUklBTi1DQSxDTj1BSUEsQ049UHVibGljJTIwS2V5JTIwU2Vy
+dmljZXMsQ049U2VydmljZXMsQ049Q29uZmlndXJhdGlvbixEQz1hZCxEQz1udW1lcmljYSxEQz11
+cz9jQUNlcnRpZmljYXRlP2Jhc2U/b2JqZWN0Q2xhc3M9Y2VydGlmaWNhdGlvbkF1dGhvcml0eTBS
+BgNVHREESzBJoCwGCisGAQQBgjcUAgOgHgwcd2VzbGV5LnRheWxvckBhZC5udW1lcmljYS51c4EZ
+d2VzbGV5LnRheWxvckBudW1lcmljYS51czANBgkqhkiG9w0BAQsFAAOCAQEAX3zFhiDYU+vQap2J
+hiysyC9L7nkL7VI2OQWg4Z/JnNJTFiA6BwtoDYAT4qq1Jix4hZc+g78Gj99OnkhlBQDe9Hq12yI9
+muboQSDAYO6iDK76wQv3Rt8Fl4SUD4Ygwy52QrkTDrj/HZxTNask5p/2ilGBJnG9KT2VbEgGJkP9
+kXn1vAgOl3BCxgjdWekWCvxpmffr+Z3UtmQIiZAB3OsKcgdsSy9pveTMjxtKJemaH3kpXQiTgCev
+CMuWZb3YnqXI8Fd+uUw6HwA4c+ZH62G9Q8KGkwXyhOPizmm3UeSlMo27yUCE+cF5EIHBxpGJ6z83
+7MbxMVKnS1Wz1n8MtW2ezDGCBCEwggQdAgEBMHMwXDESMBAGCgmSJomT8ixkARkWAnVzMRgwFgYK
+CZImiZPyLGQBGRYIbnVtZXJpY2ExEjAQBgoJkiaJk/IsZAEZFgJhZDEYMBYGA1UEAxMPYWQtQ0VM
+RUJSSUFOLUNBAhMnAAALCTGsNkY7R2RVAAEAAAsJMA0GCWCGSAFlAwQCAwUAoIICfzAYBgkqhkiG
+9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yMDA2MjUyMzIwNDRaME8GCSqGSIb3
+DQEJBDFCBEBaj66vdgjAhEO0p7lO6X44h+LpUlAcROa5Hi4Jp5aWS4hU8CuqOrH12y2GRNmNhKLa
+0YieL4fCL3YqDRfop79NMFIGCyqGSIb3DQEJEAIBMUMwQQQdAAAAABAAAACgLzslsB99TKIYKeHy
+Wh5cAQAAAACAAQAwHTAbgRl3ZXNsZXkudGF5bG9yQG51bWVyaWNhLnVzMIGCBgkrBgEEAYI3EAQx
+dTBzMFwxEjAQBgoJkiaJk/IsZAEZFgJ1czEYMBYGCgmSJomT8ixkARkWCG51bWVyaWNhMRIwEAYK
+CZImiZPyLGQBGRYCYWQxGDAWBgNVBAMTD2FkLUNFTEVCUklBTi1DQQITJwAACwkxrDZGO0dkVQAB
+AAALCTCBhAYLKoZIhvcNAQkQAgsxdaBzMFwxEjAQBgoJkiaJk/IsZAEZFgJ1czEYMBYGCgmSJomT
+8ixkARkWCG51bWVyaWNhMRIwEAYKCZImiZPyLGQBGRYCYWQxGDAWBgNVBAMTD2FkLUNFTEVCUklB
+Ti1DQQITJwAACwkxrDZGO0dkVQABAAALCTCBkwYJKoZIhvcNAQkPMYGFMIGCMAsGCWCGSAFlAwQB
+KjALBglghkgBZQMEARYwCgYIKoZIhvcNAwcwCwYJYIZIAWUDBAECMA4GCCqGSIb3DQMCAgIAgDAN
+BggqhkiG9w0DAgIBQDALBglghkgBZQMEAgMwCwYJYIZIAWUDBAICMAsGCWCGSAFlAwQCATAHBgUr
+DgMCGjANBgkqhkiG9w0BAQEFAASCAQBNFxhcbK6Rmw0Xyu+79cH5kUsXENcdUaJPKlegcY/gl2BZ
+0CPpGcRnwz6z8OPYjvw3jrkiAE8nBbuCKu1CPtuk1h4Cybk7exyMybYvK5xge+N+dz2mFipRfGSY
+rl/ztX1jyvcDruxaSJwb8WMhAGs505yfaCJfwgFOI3QGi+wUunbOIKy3QQZTXDv89yslZqi0wmeI
+8sVRqSAYZRIPEylwS9CU2ReK9BJlfVLZnNP1At4gHE6S2hk8T0eVeLT8uhQiUXXJe4644UoPhoA4
+Fxgm7Q62KT6yP9O7c4eZzmQ4A9hdlWM6CtZ5pgMAzLOrVFdypzSc+S1j8DqcFkALCw83AAAAAAAA
+
+------=_NextPart_000_0018_01D64B14.F58791A0--
+
+--===============0678627779074767862==
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+HTCondor-users mailing list
+To unsubscribe, send a message to htcondor-users-request@cs.wisc.edu with a
+subject: Unsubscribe
+You can also unsubscribe by visiting
+https://lists.cs.wisc.edu/mailman/listinfo/htcondor-users
+
+The archives can be found at:
+https://lists.cs.wisc.edu/archive/htcondor-users/
+--===============0678627779074767862==--)";
+
+static std::string
+message(const Regex& rx, size_t id)
+{
+ char buf[16];
+ ::snprintf(buf, sizeof(buf), "%zu", id);
+
+ return to_string_gchar(
+ g_regex_replace(rx, test_msg, -1, 0, buf,
+ G_REGEX_MATCH_DEFAULT, {}));
+}
+
+struct TestData {
+ size_t num_maildirs;
+ size_t num_messages;
+ size_t num_threads;
+};
+
+
+static void
+setup(const TestData& tdata)
+{
+ /* create toplevel */
+ auto top_maildir = std::string{BENCH_MAILDIRS};
+ int res = g_mkdir_with_parents(top_maildir.c_str(), 0700);
+ g_assert_cmpuint(res,==, 0);
+
+ /* create maildirs */
+ for (size_t i = 0; i != tdata.num_maildirs; ++i) {
+ const auto mdir = mu_format("{}/maildir-{}", top_maildir, i);
+ auto res = maildir_mkdir(mdir);
+ g_assert(!!res);
+ }
+ const auto rx = Regex::make("@ID@");
+ /* create messages */
+ for (size_t n = 0; n != tdata.num_messages; ++n) {
+ auto mpath = mu_format("{}/maildir-{}/cur/msg-{}:2,S",
+ top_maildir, n % tdata.num_maildirs,
+ n);
+ std::ofstream stream(mpath);
+ auto msg = message(*rx, n);
+ stream.write(msg.c_str(), msg.size());
+ g_assert_true(stream.good());
+ }
+}
+
+static void
+tear_down()
+{
+ /* ugly */
+ GError *err{};
+ const auto cmd{mu_format("/bin/rm -rf '{}' '{}'", BENCH_MAILDIRS, BENCH_STORE)};
+ if (!g_spawn_command_line_sync(cmd.c_str(), NULL, NULL, NULL, &err)) {
+ mu_warning("error: {}", err ? err->message : "?");
+ g_clear_error(&err);
+ }
+}
+
+void
+black_hole(void)
+{
+ return; /* do nothing */
+}
+
+static void
+benchmark_indexer(gconstpointer testdata)
+{
+ using namespace std::chrono_literals;
+ using Clock = std::chrono::steady_clock;
+ const auto tdata = reinterpret_cast<const TestData*>(testdata);
+
+ setup(*tdata);
+ auto start = Clock::now();
+
+ {
+ auto store{Store::make_new(BENCH_STORE, BENCH_MAILDIRS)};
+ g_assert_true(!!store);
+ Indexer::Config conf{};
+ conf.max_threads = tdata->num_threads;
+
+ auto res = store->indexer().start(conf);
+ g_assert_true(res);
+ while(store->indexer().is_running()) {
+ std::this_thread::sleep_for(100ms);
+ }
+ g_assert_cmpuint(store->size(),==, tdata->num_messages);
+ }
+
+ const auto elapsed = Clock::now() - start;
+ std::cout << "indexed " << tdata->num_messages << " messages in "
+ << tdata->num_maildirs << " maildirs in "
+ << to_ms(elapsed) << "ms; "
+ << to_us(elapsed) / tdata->num_messages << " μs/message; "
+ << static_cast<size_t>(1000*tdata->num_messages / to_ms(elapsed))
+ << " messages/s"
+ << " (" << tdata->num_threads << " thread(s))\n";
+
+ tear_down();
+}
+
+int
+main(int argc, char *argv[])
+{
+ size_t num_maildirs{}, num_messages{};
+
+ mu_test_init(&argc, &argv);
+
+ if (g_test_perf()) {
+ num_maildirs = 20;
+ num_messages = 5000;
+ } else {
+ num_maildirs = 10;
+ num_messages = 1000;
+ }
+
+ g_log_set_handler(
+ NULL,
+ (GLogLevelFlags)(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION),
+ (GLogFunc)black_hole,
+ NULL);
+
+
+ size_t thread_num{};
+ const auto tnum = g_getenv("THREAD_NUM");
+ if (tnum)
+ thread_num = ::strtol(tnum, NULL, 10);
+
+ if (thread_num != 0) {
+ /* THREAD_NUM specified */
+ static TestData tdata{num_maildirs, num_messages, thread_num};
+ char *name = g_strdup_printf("/bench/indexer/%zu-cores", thread_num);
+ g_test_add_data_func(name, &tdata, benchmark_indexer);
+ g_free(name);
+ } else {
+ /* no THREAD_NUM specified */
+
+ const size_t hw_threads = std::thread::hardware_concurrency();
+
+ {
+ static TestData tdata{num_maildirs, num_messages, 1};
+ g_test_add_data_func("/bench/indexer/1-core", &tdata, benchmark_indexer);
+ }
+
+ if (hw_threads > 2) {
+ static TestData tdata{num_maildirs, num_messages, hw_threads/2};
+ char *name = g_strdup_printf("/bench/indexer/%zu-cores", hw_threads/2);
+ g_test_add_data_func(name, &tdata, benchmark_indexer);
+ g_free(name);
+ }
+
+ if (hw_threads > 1) {
+ static TestData tdata{num_maildirs, num_messages, hw_threads};
+ char *name = g_strdup_printf("/bench/indexer/%zu-cores", hw_threads);
+ g_test_add_data_func(name, &tdata, benchmark_indexer);
+ g_free(name);
+ }
+ }
+
+ tear_down();
+
+ return g_test_run();
+}
--- /dev/null
+## Copyright (C) 2021-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## 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, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+#
+# tests
+#
+
+
+#
+# unit tests
+#
+
+test('test-threads',
+ executable('test-threads',
+ '../mu-query-threads.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_dep]))
+test('test-contacts-cache',
+ executable('test-contacts-cache',
+ '../mu-contacts-cache.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_dep]))
+
+test('test-config',
+ executable('test-config',
+ '../mu-config.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_dep]))
+
+test('test-query-macros',
+ executable('test-query-macros',
+ '../mu-query-macros.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [lib_mu_dep]))
+
+test('test-query-processor',
+ executable('test-query-processor',
+ '../mu-query-processor.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [lib_mu_dep]))
+
+test('test-query-parser',
+ executable('test-query-parser',
+ '../mu-query-parser.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [lib_mu_dep]))
+
+test('test-query-xapianizer',
+ executable('test-query-xapianizer',
+ '../mu-query-xapianizer.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [lib_mu_dep]))
+
+
+test('test-indexer',
+ executable('test-indexer',
+ '../mu-indexer.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, config_h_dep,
+ lib_mu_dep]))
+
+test('test-scanner',
+ executable('test-scanner',
+ '../mu-scanner.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, config_h_dep,
+ lib_mu_utils_dep]))
+
+test('test-xapian-db',
+ executable('test-xapian-db',
+ '../mu-xapian-db.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [lib_mu_dep, config_h_dep]))
+
+test('test-maildir',
+ executable('test-maildir',
+ 'test-mu-maildir.cc',
+ install: false,
+ dependencies: [glib_dep, lib_mu_dep]))
+test('test-msg',
+ executable('test-msg',
+ 'test-mu-msg.cc',
+ install: false,
+ dependencies: [glib_dep, lib_mu_dep]))
+test('test-store',
+ executable('test-store',
+ 'test-mu-store.cc',
+ install: false,
+ dependencies: [glib_dep, lib_mu_dep]))
+test('test-query',
+ executable('test-query',
+ 'test-query.cc',
+ install: false,
+ dependencies: [glib_dep, gmime_dep, lib_mu_dep]))
+
+test('test-store-query',
+ executable('test-store-query',
+ 'test-mu-store-query.cc',
+ install: false,
+ dependencies: [glib_dep, gmime_dep, lib_mu_dep]))
+#
+# benchmarks
+#
+bench_maildirs=join_paths(meson.current_build_dir(), 'maildirs')
+bench_store=join_paths(meson.current_build_dir(), 'store')
+bench_indexer_exe = executable(
+ 'bench-indexer',
+ 'bench-indexer.cc',
+ install:false,
+ cpp_args:['-DBENCH_MAILDIRS="' + bench_maildirs + '"',
+ '-DBENCH_STORE="' + bench_store + '"',
+ ],
+ dependencies: [lib_mu_dep, glib_dep])
+
+benchmark('bench-indexer', bench_indexer_exe, args: ['-m', 'perf'])
+
+#
+# below does _not_ pass; it is believed that it's a false alarm.
+# https://gitlab.gnome.org/GNOME/glib/-/issues/2662
+
+# also register benchmark as a normal test so it gets included for
+# valgrind/helgrind etc.
+# test('test-bench-indexer', bench_indexer_exe,
+# args : ['-m', 'quick'], env: ['THREADNUM=16'])
--- /dev/null
+/*
+** Copyright (C) 2014 Jakub Sitnicki <jsitnicki@gmail.com>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+#include <glib.h>
+
+#include "utils/mu-test-utils.hh"
+#include "mu-container.hh"
+
+static gboolean
+container_has_children(const MuContainer* c)
+{
+ return c && c->child;
+}
+
+static gboolean
+container_is_sibling_of(const MuContainer* c, const MuContainer* sibling)
+{
+ const MuContainer* cur;
+
+ for (cur = c; cur; cur = cur->next) {
+ if (cur == sibling)
+ return TRUE;
+ }
+
+ return container_is_sibling_of(sibling, c);
+}
+
+static void
+test_mu_container_splice_children_when_parent_has_no_siblings(void)
+{
+ MuContainer *child, *parent, *root_set;
+
+ child = mu_container_new(NULL, 0, "child");
+ parent = mu_container_new(NULL, 0, "parent");
+ parent = mu_container_append_children(parent, child);
+
+ root_set = parent;
+ root_set = mu_container_splice_children(root_set, parent);
+
+ g_assert(root_set != NULL);
+ g_assert(!container_has_children(parent));
+ g_assert(container_is_sibling_of(root_set, child));
+
+ mu_container_destroy(parent);
+ mu_container_destroy(child);
+}
+
+int
+main(int argc, char* argv[])
+{
+ g_test_init(&argc, &argv, NULL);
+
+ g_test_add_func("/mu-container/mu-container-splice-children-when-parent-has-no-siblings",
+ test_mu_container_splice_children_when_parent_has_no_siblings);
+
+ g_log_set_handler(
+ NULL,
+ (GLogLevelFlags)(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION),
+ (GLogFunc)black_hole,
+ NULL);
+
+ return g_test_run();
+}
--- /dev/null
+/*
+** Copyright (C) 2008-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <vector>
+#include <fstream>
+
+#include "utils/mu-test-utils.hh"
+#include "mu-maildir.hh"
+#include "utils/mu-utils.hh"
+#include "utils/mu-utils-file.hh"
+#include "utils/mu-result.hh"
+
+using namespace Mu;
+
+static void
+test_maildir_mkdir_01()
+{
+ TempDir temp_dir;
+ auto mdir = join_paths(temp_dir.path(), "cuux");
+ auto res{maildir_mkdir(mdir, 0755, false/*!noindex*/)};
+ assert_valid_result(res);
+
+ for (auto sub : {"tmp", "cur", "new"}) {
+ auto subpath = join_paths(mdir, sub);
+ g_assert_cmpuint(g_access(subpath.c_str(), R_OK), ==, 0);
+ g_assert_cmpuint(g_access(subpath.c_str(), W_OK), ==, 0);
+ }
+
+ auto noindex = join_paths(mdir, ".noindex");
+ g_assert_cmpuint(g_access(noindex.c_str(), F_OK), !=, 0);
+}
+
+static void
+test_maildir_mkdir_02()
+{
+ TempDir temp_dir;
+ auto mdir = join_paths(temp_dir.path(), "cuux");
+ auto res{maildir_mkdir(mdir, 0755, true/*noindex*/)};
+ assert_valid_result(res);
+
+ for (auto sub : {"tmp", "cur", "new"}) {
+ auto subpath = join_paths(mdir, sub);
+ g_assert_cmpuint(g_access(subpath.c_str(), R_OK), ==, 0);
+ g_assert_cmpuint(g_access(subpath.c_str(), W_OK), ==, 0);
+ }
+
+ auto noindex = join_paths(mdir, ".noindex");
+ g_assert_cmpuint(g_access(noindex.c_str(), F_OK), ==, 0);
+}
+
+static void
+test_maildir_mkdir_03()
+{
+ TempDir temp_dir;
+ auto mdir = join_paths(temp_dir.path(), "cuux");
+
+ // create part already
+ auto curdir = join_paths(mdir, "cur");
+ g_assert_cmpuint(g_mkdir_with_parents(curdir.c_str(), 0755), ==, 0);
+
+ auto res{maildir_mkdir(mdir, 0755, false/*!noindex*/)};
+ assert_valid_result(res);
+
+ // should still work.
+ for (auto sub : {"tmp", "cur", "new"}) {
+ auto subpath = join_paths(mdir, sub);
+ g_assert_cmpuint(g_access(subpath.c_str(), R_OK), ==, 0);
+ g_assert_cmpuint(g_access(subpath.c_str(), W_OK), ==, 0);
+ }
+
+ auto noindex = join_paths(mdir, ".noindex");
+ g_assert_cmpuint(g_access(noindex.c_str(), F_OK), !=, 0);
+}
+
+
+static void
+test_maildir_mkdir_04()
+{
+ allow_warnings();
+
+ if (geteuid() == 0) {
+ g_test_skip("not useful when run as root");
+ return;
+ }
+
+ TempDir temp_dir;
+ auto mdir = join_paths(temp_dir.path(), "cuux");
+ g_assert_cmpuint(g_mkdir_with_parents(mdir.c_str(), 0755), ==, 0);
+
+ auto curdir = join_paths(mdir, "cur");
+ g_assert_cmpuint(g_mkdir_with_parents(curdir.c_str(), 0000), ==, 0);
+
+ /* this should fail now, because cur is not read/writable */
+ auto res = maildir_mkdir(mdir, 0755, false);
+ g_assert_false(!!res);
+}
+
+static gboolean
+ignore_error(const char* log_domain, GLogLevelFlags log_level, const gchar* msg, gpointer user_data)
+{
+ return FALSE; /* don't abort */
+}
+
+static void
+test_maildir_mkdir_05(void)
+{
+ /* this must fail */
+ g_test_log_set_fatal_handler((GTestLogFatalFunc)ignore_error, NULL);
+
+ g_assert_false(!!maildir_mkdir({}, 0755, true));
+}
+
+[[maybe_unused]] static void
+assert_matches_regexp(const char* str, const char* rx)
+{
+ if (!g_regex_match_simple(rx, str, (GRegexCompileFlags)0, (GRegexMatchFlags)0)) {
+ if (g_test_verbose())
+ g_print("%s does not match %s", str, rx);
+ g_assert(0);
+ }
+}
+
+
+static void
+test_determine_target_ok(void)
+{
+ struct TestCase {
+ std::string old_path;
+ std::string root_maildir;
+ std::string target_maildir;
+ Flags new_flags;
+ bool new_name;
+ std::string expected;
+ };
+ const std::vector<TestCase> testcases = {
+ TestCase{ /* change some flags */
+ "/home/foo/Maildir/test/cur/123456:2,FR",
+ "/home/foo/Maildir",
+ {},
+ Flags::Seen | Flags::Passed,
+ false,
+ "/home/foo/Maildir/test/cur/123456:2,PS"
+ },
+
+ TestCase{ /* from cur -> new */
+ "/home/foo/Maildir/test/cur/123456:2,FR",
+ "/home/foo/Maildir",
+ {},
+ Flags::New,
+ false,
+ "/home/foo/Maildir/test/new/123456"
+ },
+
+ TestCase{ /* from new->cur */
+ "/home/foo/Maildir/test/cur/123456",
+ "/home/foo/Maildir",
+ {},
+ Flags::Seen | Flags::Flagged,
+ false,
+ "/home/foo/Maildir/test/cur/123456:2,FS"
+ },
+
+ TestCase{ /* change maildir */
+ "/home/foo/Maildir/test/cur/123456:2,FR",
+ "/home/foo/Maildir",
+ "/test2",
+ Flags::Flagged | Flags::Replied,
+ false,
+ "/home/foo/Maildir/test2/cur/123456:2,FR"
+ },
+ TestCase{ /* remove all flags */
+ "/home/foo/Maildir/test/new/123456",
+ "/home/foo/Maildir",
+ {},
+ Flags::None,
+ false,
+ "/home/foo/Maildir/test/cur/123456:2,"
+ },
+ };
+
+ for (auto&& testcase: testcases) {
+ const auto res = maildir_determine_target(
+ testcase.old_path,
+ testcase.root_maildir,
+ testcase.target_maildir,
+ testcase.new_flags,
+ testcase.new_name);
+ g_assert_true(!!res);
+ g_assert_cmpstr(testcase.expected.c_str(), ==,
+ res.value().c_str());
+ }
+}
+
+
+
+static void
+test_determine_target_fail(void)
+{
+ struct TestCase {
+ std::string old_path;
+ std::string root_maildir;
+ std::string target_maildir;
+ Flags new_flags;
+ bool new_name;
+ std::string expected;
+ };
+ const std::vector<TestCase> testcases = {
+ TestCase{ /* fail: no absolute path */
+ "../foo/Maildir/test/cur/123456:2,FR-not-absolute",
+ "/home/foo/Maildir",
+ {},
+ Flags::Seen | Flags::Passed,
+ false,
+ "/home/foo/Maildir/test/cur/123456:2,PS"
+ },
+
+ TestCase{ /* fail: no absolute root */
+ "/home/foo/Maildir/test/cur/123456:2,FR",
+ "../foo/Maildir-not-absolute",
+ {},
+ Flags::New,
+ false,
+ "/home/foo/Maildir/test/new/123456"
+ },
+
+ TestCase{ /* fail: maildir must start with '/' */
+ "/home/foo/Maildir/test/cur/123456",
+ "/home/foo/Maildir",
+ "mymaildirwithoutslash",
+ Flags::Seen | Flags::Flagged,
+ false,
+ "/home/foo/Maildir/test/cur/123456:2,FS"
+ },
+
+ TestCase{ /* fail: path must be below maildir */
+ "/home/foo/Maildir/test/cur/123456:2,FR",
+ "/home/bar/Maildir",
+ "/test2",
+ Flags::Flagged | Flags::Replied,
+ false,
+ "/home/foo/Maildir/test2/cur/123456:2,FR"
+ },
+ TestCase{ /* fail: New cannot be combined */
+ "/home/foo/Maildir/test/new/123456",
+ "/home/foo/Maildir",
+ {},
+ Flags::New | Flags::Replied,
+ false,
+ "/home/foo/Maildir/test/cur/123456:2,"
+ },
+ };
+
+ for (auto&& testcase: testcases) {
+ const auto res = maildir_determine_target(
+ testcase.old_path,
+ testcase.root_maildir,
+ testcase.target_maildir,
+ testcase.new_flags,
+ testcase.new_name);
+ g_assert_false(!!res);
+ }
+}
+
+
+
+static void
+test_maildir_get_new_path_01(void)
+{
+ struct {
+ std::string oldpath;
+ Flags flags;
+ std::string newpath;
+ } paths[] = {{"/home/foo/Maildir/test/cur/123456:2,FR",
+ Flags::Replied,
+ "/home/foo/Maildir/test/cur/123456:2,R"},
+ {"/home/foo/Maildir/test/cur/123456:2,FR",
+ Flags::New,
+ "/home/foo/Maildir/test/new/123456"},
+ {"/home/foo/Maildir/test/new/123456:2,FR",
+ (Flags::Seen | Flags::Replied),
+ "/home/foo/Maildir/test/cur/123456:2,RS"},
+ {"/home/foo/Maildir/test/new/1313038887_0.697",
+ (Flags::Seen | Flags::Flagged | Flags::Passed),
+ "/home/foo/Maildir/test/cur/1313038887_0.697:2,FPS"},
+ {"/home/foo/Maildir/test/new/1313038887_0.697:2,",
+ (Flags::Seen | Flags::Flagged | Flags::Passed),
+ "/home/foo/Maildir/test/cur/1313038887_0.697:2,FPS"},
+ /* note the ':2,' suffix on the new message is
+ * removed */
+
+ {"/home/foo/Maildir/trash/new/1312920597.2206_16.cthulhu",
+ Flags::Seen,
+ "/home/foo/Maildir/trash/cur/1312920597.2206_16.cthulhu:2,S"}};
+
+ for (int i = 0; i != G_N_ELEMENTS(paths); ++i) {
+ const auto newpath{maildir_determine_target(paths[i].oldpath,
+ "/home/foo/Maildir",
+ {}, paths[i].flags, false)};
+ assert_valid_result(newpath);
+ assert_equal(*newpath, paths[i].newpath);
+ }
+}
+
+static void
+test_maildir_get_new_path_02(void)
+{
+ struct {
+ std::string oldpath;
+ Flags flags;
+ std::string targetdir;
+ std::string newpath;
+ std::string root_maildir;
+ } paths[] = {{"/home/foo/Maildir/test/cur/123456:2,FR",
+ Flags::Replied,
+ "/blabla",
+ "/home/foo/Maildir/blabla/cur/123456:2,R",
+ "/home/foo/Maildir"},
+ {"/home/bar/Maildir/test/cur/123456:2,FR",
+ Flags::New,
+ "/coffee",
+ "/home/bar/Maildir/coffee/new/123456",
+ "/home/bar/Maildir"
+ },
+ {"/home/cuux/Maildir/test/new/123456",
+ (Flags::Seen | Flags::Replied),
+ "/tea",
+ "/home/cuux/Maildir/tea/cur/123456:2,RS",
+ "/home/cuux/Maildir"},
+ {"/home/boy/Maildir/test/new/1313038887_0.697:2,",
+ (Flags::Seen | Flags::Flagged | Flags::Passed),
+ "/stuff",
+ "/home/boy/Maildir/stuff/cur/1313038887_0.697:2,FPS",
+ "/home/boy/Maildir"}};
+
+ for (int i = 0; i != G_N_ELEMENTS(paths); ++i) {
+ auto newpath{maildir_determine_target(paths[i].oldpath,
+ paths[i].root_maildir,
+ paths[i].targetdir,
+ paths[i].flags,
+ false)};
+ assert_valid_result(newpath);
+ assert_equal(*newpath, paths[i].newpath);
+ }
+}
+
+
+static void
+test_maildir_get_new_path_custom_real(bool change_name)
+{
+ struct {
+ std::string oldpath;
+ Flags flags;
+ std::string targetdir;
+ std::string newpath;
+ std::string root_maildir;
+ } paths[] = {{"/home/foo/Maildir/test/cur/123456:2,FR",
+ Flags::Replied,
+ "/blabla",
+ "/home/foo/Maildir/blabla/cur/123456:2,R",
+ "/home/foo/Maildir"},
+ {"/home/foo/Maildir/test/cur/123456:2,hFeRllo123",
+ Flags::Flagged,
+ "/blabla",
+ "/home/foo/Maildir/blabla/cur/123456:2,F",
+ "/home/foo/Maildir"},
+ {"/home/foo/Maildir/test/cur/123456:2,abc",
+ Flags::Passed,
+ "/blabla",
+ "/home/foo/Maildir/blabla/cur/123456:2,P",
+ "/home/foo/Maildir"}};
+
+ for (int i = 0; i != G_N_ELEMENTS(paths); ++i) {
+ auto newpath{maildir_determine_target(paths[i].oldpath,
+ paths[1].root_maildir,
+ paths[i].targetdir,
+ paths[i].flags,
+ change_name)};
+ assert_valid_result(newpath);
+ if (change_name)
+ g_assert_true(*newpath != paths[i].newpath); // weak test
+ else
+ assert_equal(*newpath, paths[i].newpath);
+ }
+}
+
+
+static void
+test_maildir_get_new_path_custom(void)
+{
+ return test_maildir_get_new_path_custom_real(false);
+}
+
+
+static void
+test_maildir_get_new_path_custom_change_name(void)
+{
+ return test_maildir_get_new_path_custom_real(true);
+}
+
+
+static void
+test_maildir_from_path(void)
+{
+ unsigned u;
+
+ struct {
+ std::string path, exp;
+ } cases[] = {{"/home/foo/Maildir/test/cur/123456:2,FR", "/test"},
+ {"/home/foo/Maildir/lala/new/1313038887_0.697:2,", "/lala"}};
+
+ for (u = 0; u != G_N_ELEMENTS(cases); ++u) {
+ auto mdir{maildir_from_path(cases[u].path, "/home/foo/Maildir")};
+ assert_valid_result(mdir);
+ assert_equal(*mdir, cases[u].exp);
+ }
+}
+
+static void
+test_maildir_link()
+{
+ TempDir tmpdir;
+
+ assert_valid_result(maildir_mkdir(tmpdir.path() + "/foo"));
+ assert_valid_result(maildir_mkdir(tmpdir.path() + "/bar"));
+
+ const auto srcpath1 = tmpdir.path() + "/foo/cur/msg1";
+ const auto srcpath2 = tmpdir.path() + "/foo/new/msg2";
+
+ {
+ std::ofstream stream(srcpath1);
+ stream.write("cur", 3);
+ g_assert_true(stream.good());
+ stream.close();
+ }
+
+ {
+ std::ofstream stream(srcpath2);
+ stream.write("new", 3);
+ g_assert_true(stream.good());
+ stream.close();
+ }
+
+ assert_valid_result(maildir_link(srcpath1, tmpdir.path() + "/bar", false));
+ assert_valid_result(maildir_link(srcpath2, tmpdir.path() + "/bar", false));
+
+ const auto dstpath1 = tmpdir.path() + "/bar/cur/msg1";
+ const auto dstpath2 = tmpdir.path() + "/bar/new/msg2";
+
+ g_assert_true(g_access(dstpath1.c_str(), F_OK) == 0);
+ g_assert_true(g_access(dstpath2.c_str(), F_OK) == 0);
+
+ g_assert_false(!!maildir_clear_links("/nonexistent/bla/foo/xuux"));
+
+ assert_valid_result(maildir_clear_links(tmpdir.path() + "/bar"));
+ g_assert_false(g_access(dstpath1.c_str(), F_OK) == 0);
+ g_assert_false(g_access(dstpath2.c_str(), F_OK) == 0);
+}
+
+
+static void
+test_maildir_move(bool assume_remote)
+{
+ TempDir tmpdir;
+
+ assert_valid_result(maildir_mkdir(tmpdir.path() + "/foo"));
+ assert_valid_result(maildir_mkdir(tmpdir.path() + "/bar"));
+
+ const auto srcpath1{join_paths(tmpdir.path(), "/foo/cur/msg1")};
+ const auto srcpath2{join_paths(tmpdir.path(), "/foo/new/msg2")};
+
+ {
+ std::ofstream stream(srcpath1);
+ stream.write("cur", 3);
+ g_assert_true(stream.good());
+ stream.close();
+ }
+
+ {
+ std::ofstream stream(srcpath2);
+ stream.write("new", 3);
+ g_assert_true(stream.good());
+ stream.close();
+ }
+
+ const auto dstpath = tmpdir.path() + "/test1";
+
+ assert_valid_result(maildir_move_message(srcpath1, dstpath, assume_remote));
+ assert_valid_result(maildir_move_message(srcpath2, dstpath, assume_remote));
+
+ assert_valid_result(maildir_move_message(dstpath, dstpath)); // self-move is okay.
+}
+
+static void
+test_maildir_move_vanilla()
+{
+ test_maildir_move(false/*!assume_remote*/);
+}
+
+static void
+test_maildir_move_remote()
+{
+ test_maildir_move(true/*assume_remote*/);
+}
+
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ /* mu_util_maildir_mkmdir */
+ g_test_add_func("/maildir/mkdir-01", test_maildir_mkdir_01);
+ g_test_add_func("/maildir/mkdir-02", test_maildir_mkdir_02);
+ g_test_add_func("/maildir/mkdir-03", test_maildir_mkdir_03);
+ g_test_add_func("/maildir/mkdir-04", test_maildir_mkdir_04);
+ g_test_add_func("/maildir/mkdir-05", test_maildir_mkdir_05);
+
+ g_test_add_func("/maildir/determine-target-ok", test_determine_target_ok);
+ g_test_add_func("/maildir/determine-target-fail", test_determine_target_fail);
+
+ // /* get/set flags */
+ g_test_add_func("/maildir/get-new-path-01", test_maildir_get_new_path_01);
+ g_test_add_func("/maildir/get-new-path-02", test_maildir_get_new_path_02);
+ g_test_add_func("/maildir/get-new-path-custom", test_maildir_get_new_path_custom);
+ g_test_add_func("/maildir/get-new-path-custom-change-name",
+ test_maildir_get_new_path_custom_change_name);
+
+ g_test_add_func("/maildir/from-path", test_maildir_from_path);
+
+ g_test_add_func("/maildir/link", test_maildir_link);
+ g_test_add_func("/maildir/move-vanilla", test_maildir_move_vanilla);
+ g_test_add_func("/maildir/move-remote", test_maildir_move_remote);
+
+ return g_test_run();
+}
--- /dev/null
+/*
+** Copyright (C) 2008-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#if HAVE_CONFIG_H
+#include "config.h"
+#endif /*HAVE_CONFIG_H*/
+
+#include <glib.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <time.h>
+
+#include <locale.h>
+
+#include "utils/mu-test-utils.hh"
+#include "mu-message-fields.hh"
+
+static void
+test_mu_msg_field_body(void)
+{
+ Field::Id field;
+
+ field = Field::Id::BodyText;
+
+ g_assert_cmpstr(mu_msg_field_name(field), ==, "body");
+ g_assert_cmpuint(mu_msg_field_shortcut(field), ==, 'b');
+ g_assert_cmpuint(mu_msg_field_xapian_prefix(field), ==, 'B');
+
+ g_assert_cmpuint(mu_msg_field_is_numeric(field), ==, FALSE);
+}
+
+static void
+test_mu_msg_field_subject(void)
+{
+ Field::Id field;
+
+ field = Field::Id::Subject;
+
+ g_assert_cmpstr(mu_msg_field_name(field), ==, "subject");
+ g_assert_cmpuint(mu_msg_field_shortcut(field), ==, 's');
+ g_assert_cmpuint(mu_msg_field_xapian_prefix(field), ==, 'S');
+
+ g_assert_cmpuint(mu_msg_field_is_numeric(field), ==, FALSE);
+}
+
+static void
+test_mu_msg_field_to(void)
+{
+ Field::Id field;
+
+ field = Field::Id::To;
+
+ g_assert_cmpstr(mu_msg_field_name(field), ==, "to");
+ g_assert_cmpuint(mu_msg_field_shortcut(field), ==, 't');
+ g_assert_cmpuint(mu_msg_field_xapian_prefix(field), ==, 'T');
+
+ g_assert_cmpuint(mu_msg_field_is_numeric(field), ==, FALSE);
+}
+
+static void
+test_mu_msg_field_prio(void)
+{
+ Field::Id field;
+
+ field = Field::Id::Priority;
+
+ g_assert_cmpstr(mu_msg_field_name(field), ==, "prio");
+ g_assert_cmpuint(mu_msg_field_shortcut(field), ==, 'p');
+ g_assert_cmpuint(mu_msg_field_xapian_prefix(field), ==, 'P');
+
+ g_assert_cmpuint(mu_msg_field_is_numeric(field), ==, TRUE);
+}
+
+static void
+test_mu_msg_field_flags(void)
+{
+ Field::Id field;
+
+ field = Field::Id::Flags;
+
+ g_assert_cmpstr(mu_msg_field_name(field), ==, "flag");
+ g_assert_cmpuint(mu_msg_field_shortcut(field), ==, 'g');
+ g_assert_cmpuint(mu_msg_field_xapian_prefix(field), ==, 'G');
+
+ g_assert_cmpuint(mu_msg_field_is_numeric(field), ==, TRUE);
+}
+
+int
+main(int argc, char* argv[])
+{
+ g_test_init(&argc, &argv, NULL);
+
+ /* mu_msg_str_date */
+ g_test_add_func("/mu-msg-fields/mu-msg-field-body", test_mu_msg_field_body);
+ g_test_add_func("/mu-msg-fields/mu-msg-field-subject", test_mu_msg_field_subject);
+ g_test_add_func("/mu-msg-fields/mu-msg-field-to", test_mu_msg_field_to);
+ g_test_add_func("/mu-msg-fields/mu-msg-field-prio", test_mu_msg_field_prio);
+ g_test_add_func("/mu-msg-fields/mu-msg-field-flags", test_mu_msg_field_flags);
+
+ /* FIXME: add tests for mu_msg_str_flags; but note the
+ * function simply calls mu_msg_field_str */
+
+ g_log_set_handler(
+ NULL,
+ (GLogLevelFlags)(G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION),
+ (GLogFunc)black_hole,
+ NULL);
+
+ return g_test_run();
+}
--- /dev/null
+/*
+** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <glib.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <time.h>
+#include <array>
+#include <string>
+
+#include <locale.h>
+
+#include "utils/mu-test-utils.hh"
+#include "utils/mu-result.hh"
+#include "utils/mu-utils.hh"
+
+#include <message/mu-message.hh>
+
+using namespace Mu;
+
+using ExpectedContacts = const std::vector<std::pair<std::string, std::string>>;
+
+static void
+assert_contacts_equal(const Contacts& contacts,
+ const ExpectedContacts& expected)
+{
+ g_assert_cmpuint(contacts.size(), ==, expected.size());
+
+ size_t n{};
+ for (auto&& contact: contacts) {
+ if (g_test_verbose())
+ mu_message("{{ \"{}\", \"{}\"}},\n",
+ contact.name, contact.email);
+ assert_equal(contact.name, expected.at(n).first);
+ assert_equal(contact.email, expected.at(n).second);
+ ++n;
+ }
+ mu_print("\n");
+}
+
+
+static void
+test_mu_msg_01(void)
+{
+ auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1220863042.12663_1.mindcrime!2,S")
+ .value()};
+
+ assert_contacts_equal(msg.to(), {{ "Donald Duck", "gcc-help@gcc.gnu.org" }});
+ assert_contacts_equal(msg.from(), {{ "Mickey Mouse", "anon@example.com" }});
+
+ assert_equal(msg.subject(), "gcc include search order");
+ assert_equal(msg.message_id(),
+ "3BE9E6535E3029448670913581E7A1A20D852173@"
+ "emss35m06.us.lmco.com");
+ assert_equal(msg.header("Mailing-List").value_or(""),
+ "contact gcc-help-help@gcc.gnu.org; run by ezmlm");
+ g_assert_true(msg.priority() == Priority::Normal);
+ g_assert_cmpuint(msg.date(), ==, 1217530645);
+
+ assert_contacts_equal(msg.all_contacts(), {
+ { "", "gcc-help-owner@gcc.gnu.org"},
+ { "Mickey Mouse", "anon@example.com" },
+ { "Donald Duck", "gcc-help@gcc.gnu.org" }
+ });
+
+}
+
+static void
+test_mu_msg_02(void)
+{
+ auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1220863087.12663_19.mindcrime!2,S")
+ .value()};
+
+ assert_equal(msg.to().at(0).email, "help-gnu-emacs@gnu.org");
+ assert_equal(msg.subject(), "Re: Learning LISP; Scheme vs elisp.");
+ assert_equal(msg.from().at(0).email, "anon@example.com");
+ assert_equal(msg.message_id(), "r6bpm5-6n6.ln1@news.ducksburg.com");
+ assert_equal(msg.header("Errors-To").value_or(""),
+ "help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org");
+ g_assert_true(msg.priority() /* 'low' */
+ == Priority::Low);
+ g_assert_cmpuint(msg.date(), ==, 1218051515);
+ mu_println("flags: {}", Mu::to_string(msg.flags()));
+ g_assert_true(msg.flags() == (Flags::Seen|Flags::MailingList));
+
+ assert_contacts_equal(msg.all_contacts(), {
+ { "", "help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org"},
+ { "", "anon@example.com"},
+ { "", "help-gnu-emacs@gnu.org"},
+ });
+
+}
+
+static void
+test_mu_msg_03(void)
+{
+ //const GSList* params;
+
+ auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1283599333.1840_11.cthulhu!2,")
+ .value()};
+
+ assert_equal(msg.to().at(0).display_name(), "Bilbo Baggins <bilbo@anotherexample.com>");
+ assert_equal(msg.subject(), "Greetings from Lothlórien");
+ assert_equal(msg.from().at(0).display_name(), "Frodo Baggins <frodo@example.com>");
+ g_assert_true(msg.priority() == Priority::Normal);
+ g_assert_cmpuint(msg.date(), ==, 0);
+ assert_equal(msg.body_text().value_or(""),
+ "\nLet's write some fünkÿ text\nusing umlauts.\n\nFoo.\n");
+
+ // params = mu_msg_get_body_text_content_type_parameters(msg, MU_MSG_OPTION_NONE);
+ // g_assert_cmpuint(g_slist_length((GSList*)params), ==, 2);
+
+ // assert_equal((char*)params->data, "charset");
+ // params = g_slist_next(params);
+ // assert_equal((char*)params->data, "UTF-8");
+ g_assert_true(msg.flags() == (Flags::Unread));
+}
+
+static void
+test_mu_msg_04(void)
+{
+ auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/mail5").value()};
+
+ assert_equal(msg.to().at(0).display_name(), "George Custer <gac@example.com>");
+ assert_equal(msg.subject(), "pics for you");
+ assert_equal(msg.from().at(0).display_name(), "Sitting Bull <sb@example.com>");
+ g_assert_true(msg.priority() /* 'low' */
+ == Priority::Normal);
+ g_assert_cmpuint(msg.date(), ==, 0);
+ g_assert_true(msg.flags() ==
+ (Flags::HasAttachment|Flags::Unread));
+ g_assert_true(msg.flags() ==
+ (Flags::HasAttachment|Flags::Unread));
+}
+
+static void
+test_mu_msg_multimime(void)
+{
+ auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/multimime!2,FS").value()};
+
+ /* ie., are text parts properly concatenated? */
+ assert_equal(msg.subject(), "multimime");
+ assert_equal(msg.body_text().value_or(""), "abcdef");
+ g_assert_true(msg.flags() == (Flags::HasAttachment|Flags::Flagged|Flags::Seen));
+}
+
+static void
+test_mu_msg_flags(void)
+{
+ std::array<std::pair<std::string, Flags>, 2> tests= {{
+ {MU_TESTMAILDIR4 "/multimime!2,FS",
+ (Flags::Flagged | Flags::Seen |
+ Flags::HasAttachment)},
+ {MU_TESTMAILDIR4 "/special!2,Sabc",
+ (Flags::Seen)}
+ }};
+
+ for (auto&& test: tests) {
+ auto msg = Message::make_from_path(test.first);
+ assert_valid_result(msg);
+ g_assert_true(msg->flags() == test.second);
+ }
+}
+
+static void
+test_mu_msg_umlaut(void)
+{
+ auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1305664394.2171_402.cthulhu!2,")
+ .value()};
+
+ assert_contacts_equal(msg.to(), { { "Helmut Kröger", "hk@testmu.xxx"}});
+ assert_contacts_equal(msg.from(), { { "Mü", "testmu@testmu.xx"}});
+
+ assert_equal(msg.subject(), "Motörhead");
+ assert_equal(msg.from().at(0).display_name(), "Mü <testmu@testmu.xx>");
+ g_assert_true(msg.priority() == Priority::Normal);
+ g_assert_cmpuint(msg.date(), ==, 0);
+}
+
+static void
+test_mu_msg_references(void)
+{
+ auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1305664394.2171_402.cthulhu!2,")
+ .value()};
+
+ std::array<std::string, 4> expected_refs = {
+ "non-exist-01@msg.id",
+ "non-exist-02@msg.id",
+ "non-exist-03@msg.id",
+ "non-exist-04@msg.id"
+ };
+
+ assert_equal_seq_str(msg.references(), expected_refs);
+ assert_equal(msg.thread_id(), expected_refs[0]);
+}
+
+static void
+test_mu_msg_references_dups(void)
+{
+ auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/1252168370_3.14675.cthulhu!2,S")
+ .value()};
+
+ std::array<std::string, 6> expected_refs = {
+ "439C1136.90504@euler.org",
+ "4399DD94.5070309@euler.org",
+ "20051209233303.GA13812@gauss.org",
+ "439B41ED.2080402@euler.org",
+ "439A1E03.3090604@euler.org",
+ "20051211184308.GB13513@gauss.org"
+ };
+
+ assert_equal_seq_str(msg.references(), expected_refs);
+ assert_equal(msg.thread_id(), expected_refs[0]);
+}
+
+static void
+test_mu_msg_references_many(void)
+{
+ auto msg{Message::make_from_path(MU_TESTMAILDIR2 "/bar/cur/181736.eml")
+ .value()};
+
+ std::array<std::string, 11> expected_refs = {
+ "e9065dac-13c1-4103-9e31-6974ca232a89@t15g2000prt.googlegroups.com",
+ "87hbblwelr.fsf@sapphire.mobileactivedefense.com",
+ "pql248-4va.ln1@wilbur.25thandClement.com",
+ "ikns6r$li3$1@Iltempo.Update.UU.SE",
+ "8762s0jreh.fsf@sapphire.mobileactivedefense.com",
+ "ikqqp1$jv0$1@Iltempo.Update.UU.SE",
+ "87hbbjc5jt.fsf@sapphire.mobileactivedefense.com",
+ "ikr0na$lru$1@Iltempo.Update.UU.SE",
+ "tO8cp.1228$GE6.370@news.usenetserver.com",
+ "ikr6ks$nlf$1@Iltempo.Update.UU.SE",
+ "8ioh48-8mu.ln1@leafnode-msgid.gclare.org.uk"
+ };
+
+ assert_equal_seq_str(msg.references(), expected_refs);
+ assert_equal(msg.thread_id(), expected_refs[0]);
+}
+
+static void
+test_mu_msg_tags(void)
+{
+ auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/mail1").value()};
+
+ assert_contacts_equal(msg.to(), {{ "Julius Caesar", "jc@example.com" }});
+ assert_contacts_equal(msg.from(), {{ "John Milton", "jm@example.com" }});
+
+ assert_equal(msg.subject(),"Fere libenter homines id quod volunt credunt");
+
+ g_assert_true(msg.priority() == Priority::High);
+ g_assert_cmpuint(msg.date(), ==, 1217530645);
+
+ std::array<std::string, 4> expected_tags = {
+ "Paradise",
+ "losT",
+ "john",
+ "milton"
+ };
+ assert_equal_seq_str(msg.tags(), expected_tags);
+}
+
+static void
+test_mu_msg_comp_unix_programmer(void)
+{
+ auto msg{Message::make_from_path(MU_TESTMAILDIR4 "/181736.eml").value()};
+
+ g_assert_true(msg.to().empty());
+ assert_equal(msg.subject(),
+ "Re: Are writes \"atomic\" to readers of the file?");
+ assert_equal(msg.from().at(0).display_name(), "Jimbo Foobarcuux <jimbo@slp53.sl.home>");
+ assert_equal(msg.message_id(), "oktdp.42997$Te.22361@news.usenetserver.com");
+
+ auto refs = join(msg.references(), ',');
+ assert_equal(refs,
+ "e9065dac-13c1-4103-9e31-6974ca232a89@t15g2000prt"
+ ".googlegroups.com,"
+ "87hbblwelr.fsf@sapphire.mobileactivedefense.com,"
+ "pql248-4va.ln1@wilbur.25thandClement.com,"
+ "ikns6r$li3$1@Iltempo.Update.UU.SE,"
+ "8762s0jreh.fsf@sapphire.mobileactivedefense.com,"
+ "ikqqp1$jv0$1@Iltempo.Update.UU.SE,"
+ "87hbbjc5jt.fsf@sapphire.mobileactivedefense.com,"
+ "ikr0na$lru$1@Iltempo.Update.UU.SE,"
+ "tO8cp.1228$GE6.370@news.usenetserver.com,"
+ "ikr6ks$nlf$1@Iltempo.Update.UU.SE,"
+ "8ioh48-8mu.ln1@leafnode-msgid.gclare.org.uk");
+
+ //"jimbo@slp53.sl.home (Jimbo Foobarcuux)";
+ g_assert_true(msg.priority() == Priority::Normal);
+ g_assert_cmpuint(msg.date(), ==, 1299603860);
+}
+
+static void
+test_mu_str_prio_01(void)
+{
+ g_assert_true(priority_name(Priority::Low) == "low");
+ g_assert_true(priority_name(Priority::Normal) == "normal");
+ g_assert_true(priority_name(Priority::High) == "high");
+}
+
+G_GNUC_UNUSED static gboolean
+ignore_error(const char* log_domain, GLogLevelFlags log_level, const gchar* msg, gpointer user_data)
+{
+ return FALSE; /* don't abort */
+}
+
+
+int
+main(int argc, char* argv[])
+{
+ int rv;
+
+ g_test_init(&argc, &argv, NULL);
+
+ /* mu_msg_str_date */
+ g_test_add_func("/mu-msg/mu-msg-01", test_mu_msg_01);
+ g_test_add_func("/mu-msg/mu-msg-02", test_mu_msg_02);
+ g_test_add_func("/mu-msg/mu-msg-03", test_mu_msg_03);
+ g_test_add_func("/mu-msg/mu-msg-04", test_mu_msg_04);
+ g_test_add_func("/mu-msg/mu-msg-multimime", test_mu_msg_multimime);
+
+ g_test_add_func("/mu-msg/mu-msg-flags", test_mu_msg_flags);
+
+ g_test_add_func("/mu-msg/mu-msg-tags", test_mu_msg_tags);
+ g_test_add_func("/mu-msg/mu-msg-references", test_mu_msg_references);
+ g_test_add_func("/mu-msg/mu-msg-references_dups", test_mu_msg_references_dups);
+ g_test_add_func("/mu-msg/mu-msg-references_many", test_mu_msg_references_many);
+
+ g_test_add_func("/mu-msg/mu-msg-umlaut", test_mu_msg_umlaut);
+ g_test_add_func("/mu-msg/mu-msg-comp-unix-programmer", test_mu_msg_comp_unix_programmer);
+
+ g_test_add_func("/mu-str/mu-str-prio-01", test_mu_str_prio_01);
+
+ rv = g_test_run();
+
+ return rv;
+}
--- /dev/null
+/*
+** Copyright (C) 2022-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include "config.h"
+
+#include "utils/mu-result.hh"
+#include <array>
+#include <thread>
+#include <string>
+#include <string_view>
+#include <fstream>
+#include <unordered_map>
+
+#include <mu-store.hh>
+#include <mu-maildir.hh>
+#include <utils/mu-utils.hh>
+#include <utils/mu-utils-file.hh>
+#include <utils/mu-test-utils.hh>
+#include <message/mu-message.hh>
+
+#include "mu-query-parser.hh"
+
+using namespace Mu;
+
+
+/// map of some (unique) path-tail to the message-text
+using TestMap = std::unordered_map<std::string, std::string>;
+
+static Store
+make_test_store(const std::string& test_path, const TestMap& test_map,
+ Option<const Config&> conf={})
+{
+ const auto maildir{join_paths(test_path, "/Maildir/")};
+ // note the trailing '/'
+ g_test_bug("2513");
+
+ /* write messages to disk */
+ for (auto&& item: test_map) {
+
+ /* create the directory for the message */
+ const auto msgpath{join_paths(maildir, item.first)};
+ auto dir = to_string_gchar(g_path_get_dirname(msgpath.c_str()));
+ if (g_test_verbose())
+ mu_message("create maildir {}", dir.c_str());
+
+ g_assert_cmpuint(g_mkdir_with_parents(dir.c_str(), 0700), ==, 0);
+
+ /* write the file */
+ std::ofstream stream(msgpath);
+ stream.write(item.second.data(), item.second.size());
+ g_assert_true(stream.good());
+ stream.close();
+ }
+
+ auto store = Store::make_new(test_path, maildir, conf);
+ assert_valid_result(store);
+
+ /* index the messages */
+ g_assert_true(store->indexer().start({},true/*block*/));
+ if (test_map.size() > 0)
+ g_assert_false(store->empty());
+
+ g_assert_cmpuint(store->size(),==,test_map.size());
+
+ /* and we have a fully-ready store */
+ return std::move(store.value());
+}
+
+static void
+test_simple()
+{
+ const TestMap test_msgs = {{
+
+// "sqlite-msg" "Simple mailing list message.
+{
+"basic/cur/sqlite-msg:2,S",
+R"(Return-Path: <sqlite-dev-bounces@sqlite.org>
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+ by mail.xxxxsoftware.nl (Postfix) with ESMTP id 32F276963F
+ for <xxxx@localhost>; Mon, 4 Aug 2008 21:49:34 +0300 (EEST)
+Message-Id: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net>
+From: "Foo Example" <foo@example.com>
+To: sqlite-dev@sqlite.org
+Cc: "Bank of America" <bank@example.com>
+Bcc: Aku Ankka <donald.duck@duckstad.nl>
+Mime-Version: 1.0 (Apple Message framework v926)
+Date: Mon, 4 Aug 2008 11:40:49 +0200
+X-Mailer: Apple Mail (2.926)
+Subject: [sqlite-dev] VM optimization inside sqlite3VdbeExec
+Precedence: list
+Reply-To: sqlite-dev@sqlite.org
+List-Id: <sqlite-dev.sqlite.org>
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Sender: sqlite-dev-bounces@sqlite.org
+
+Inside sqlite3VdbeExec there is a very big switch statement.
+In order to increase performance with few modifications to the
+original code, why not use this technique ?
+http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html
+
+With a properly defined "instructions" array, instead of the switch
+statement you can use something like:
+goto * instructions[pOp->opcode];
+
+I said: "Aujourd'hui!"
+)"},
+}};
+ TempDir tdir;
+ auto store{make_test_store(tdir.path(), test_msgs, {})};
+
+ // matches
+ for (auto&& expr: {
+ "Inside",
+ "from:foo@example.com",
+ "from:Foo",
+ "from:\"Foo Example\"",
+ "from:/Foo.*Example/",
+ "recip:\"Bank Of America\"",
+ "cc:bank@example.com",
+ "cc:bank",
+ "cc:america",
+ "bcc:donald.duck@duckstad.nl",
+ "bcc:donald.duck",
+ "bcc:duckstad.nl",
+ "bcc:aku",
+ "bcc:ankka",
+ "bcc:\"aku ankka\"",
+ "date:2008-08-01..2008-09-01",
+ "prio:low",
+ "to:sqlite-dev@sqlite.org",
+ "list:sqlite-dev.sqlite.org",
+ "aujourd'hui",
+#ifdef HAVE_CLD2
+ "lang:en",
+#endif /*HAVE_CLD2*/
+ }) {
+
+ if (g_test_verbose())
+ mu_message("query: '{}'\n", expr,
+ make_xapian_query(store, expr)->get_description());
+
+ auto qr = store.run_query(expr);
+ assert_valid_result(qr);
+ g_assert_false(qr->empty());
+ g_assert_cmpuint(qr->size(), ==, 1);
+ }
+
+ auto qr = store.run_query("statement");
+ assert_valid_result(qr);
+ g_assert_false(qr->empty());
+ g_assert_cmpuint(qr->size(), ==, 1);
+
+ assert_equal(qr->begin().subject().value_or(""),
+ "[sqlite-dev] VM optimization inside sqlite3VdbeExec");
+ g_assert_true(qr->begin().references().empty());
+ //g_assert_cmpuint(qr->begin().date().value_or(0), ==, 123454);
+}
+
+static void
+test_spam_address_components()
+{
+ const TestMap test_msgs = {{
+
+// "sqlite-msg" "Simple mailing list message.
+{
+"spam/cur/spam-msg:2,S",
+R"(Message-Id: <abcde@foo.bar>
+From: "Foo Example" <bar@example.com>
+To: example@example.com
+Subject: ***SPAM*** this is a test
+
+Boo!
+)"},
+}};
+ TempDir tdir;
+ auto store{make_test_store(tdir.path(), test_msgs, {})};
+
+ g_test_bug("2278");
+ g_test_bug("2281");
+
+ // matches both
+ for (auto&& expr: {
+ "SPAM",
+ "spam",
+ "/.*SPAM.*/",
+ "subject:SPAM",
+ "from:bar@example.com",
+ "subject:\\*\\*\\*SPAM\\*\\*\\*",
+ "bar",
+ "example.com"
+ }) {
+
+ if (g_test_verbose())
+ g_message("query: '%s'", expr);
+ auto qr = store.run_query(expr);
+ assert_valid_result(qr);
+ g_assert_false(qr->empty());
+ g_assert_cmpuint(qr->size(), ==, 1);
+ }
+}
+
+
+static void
+test_dups_related()
+{
+ const TestMap test_msgs = {{
+/* parent */
+{
+"inbox/cur/msg1:2,S",
+R"(Message-Id: <abcde@foo.bar>
+From: "Foo Example" <bar@example.com>
+Date: Sat, 06 Aug 2022 11:01:54 -0700
+To: example@example.com
+Subject: test1
+
+Parent
+)"},
+/* child (dup vv) */
+{
+"boo/cur/msg2:1,S",
+R"(Message-Id: <edcba@foo.bar>
+In-Reply-To: <abcde@foo.bar>
+From: "Foo Example" <bar@example.com>
+Date: Sat, 06 Aug 2022 13:01:54 -0700
+To: example@example.com
+Subject: Re: test1
+
+Child
+)"},
+/* child (dup ^^) */
+{
+"inbox/cur/msg2:1,S",
+R"(Message-Id: <edcba@foo.bar>
+In-Reply-To: <abcde@foo.bar>
+From: "Foo Example" <bar@example.com>
+Date: Sat, 06 Aug 2022 14:01:54 -0700
+To: example@example.com
+Subject: Re: test1
+
+Child
+)"},
+}};
+ TempDir tdir;
+ auto store{make_test_store(tdir.path(), test_msgs, {})};
+ {
+ // direct matches
+ auto qr = store.run_query("test1", Field::Id::Date,
+ QueryFlags::None);
+ g_assert_true(!!qr);
+ g_assert_false(qr->empty());
+ g_assert_cmpuint(qr->size(), ==, 3);
+ }
+
+ {
+ // skip duplicate messages; which one is skipped is arbitrary.
+ auto qr = store.run_query("test1", Field::Id::Date,
+ QueryFlags::SkipDuplicates);
+ g_assert_true(!!qr);
+ g_assert_false(qr->empty());
+ g_assert_cmpuint(qr->size(), ==, 2);
+ }
+
+ {
+ // no related
+ auto qr = store.run_query("Parent", Field::Id::Date);
+ g_assert_true(!!qr);
+ g_assert_false(qr->empty());
+ g_assert_cmpuint(qr->size(), ==, 1);
+ }
+
+ {
+ // find related messages
+ auto qr = store.run_query("Parent", Field::Id::Date,
+ QueryFlags::IncludeRelated);
+ g_assert_true(!!qr);
+ g_assert_false(qr->empty());
+ g_assert_cmpuint(qr->size(), ==, 3);
+ }
+
+ {
+ // find related messages, skip dups. the leader message
+ // should _not_ be skipped.
+ auto qr = store.run_query("test1 AND maildir:/inbox",
+ Field::Id::Date,
+ QueryFlags::IncludeRelated|
+ QueryFlags::SkipDuplicates);
+ g_assert_true(!!qr);
+ g_assert_false(qr->empty());
+ g_assert_cmpuint(qr->size(), ==, 2);
+
+ // ie the /boo is to be skipped, since it's not in the leader
+ // set.
+ for (auto&& m: *qr)
+ assert_equal(m.message()->maildir(), "/inbox");
+ }
+
+ {
+ // find related messages, find parent from child.
+ auto qr = store.run_query("Child and maildir:/inbox",
+ Field::Id::Date,
+ QueryFlags::IncludeRelated);
+ g_assert_true(!!qr);
+ g_assert_false(qr->empty());
+ g_assert_cmpuint(qr->size(), ==, 3);
+
+ }
+
+ {
+ // find related messages, find parent from child.
+ // leader message wins
+ auto qr = store.run_query("Child and maildir:/inbox",
+ Field::Id::Date,
+ QueryFlags::IncludeRelated|
+ QueryFlags::SkipDuplicates|
+ QueryFlags::Descending);
+ g_assert_true(!!qr);
+ g_assert_false(qr->empty());
+ g_assert_cmpuint(qr->size(), ==, 2);
+
+ // ie the /boo is to be skipped, since it's not in the leader
+ // set.
+ for (auto&& m: *qr)
+ assert_equal(m.message()->maildir(), "/inbox");
+ }
+}
+
+
+static void
+test_related_missing_root()
+{
+ const TestMap test_msgs = {{
+{
+"inbox/cur/msg1:2,S",
+R"(Content-Type: text/plain; charset=utf-8
+References: <EZrZOnVCsYfFcX3Ls0VFoRnJdCGV4GM5YtO739l-iOB2ADNH7cIJWb0DaO5Of3BWDUEKq18Rz3a7rNoI96bNwQ==@protonmail.internalid>
+To: "Joerg Roedel" <joro@8bytes.org>, "Suman Anna" <s-anna@ti.com>
+Reply-To: "Dan Carpenter" <dan.carpenter@oracle.com>
+From: "Dan Carpenter" <dan.carpenter@oracle.com>
+Subject: [PATCH] iommu/omap: fix buffer overflow in debugfs
+Date: Thu, 4 Aug 2022 17:32:39 +0300
+Message-Id: <YuvYh1JbE3v+abd5@kili>
+List-Id: <kernel-janitors.vger.kernel.org>
+Precedence: bulk
+
+There are two issues here:
+)"},
+{
+"inbox/cur/msg2:2,S",
+R"(Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset=utf-8
+References: <YuvYh1JbE3v+abd5@kili>
+ <9pEUi_xoxa7NskF7EK_qfrlgjXzGsyw9K7cMfYbo-KI6fnyVMKTpc8E2Fu94V8xedd7cMpn0LlBrr9klBMflpw==@protonmail.internalid>
+Reply-To: "Laurent Pinchart" <laurent.pinchart@ideasonboard.com>
+From: "Laurent Pinchart" <laurent.pinchart@ideasonboard.com>
+Subject: Re: [PATCH] iommu/omap: fix buffer overflow in debugfs
+List-Id: <kernel-janitors.vger.kernel.org>
+Message-Id: <YuvzKJM66k+ZPD9c@pendragon.ideasonboard.com>
+Precedence: bulk
+In-Reply-To: <YuvYh1JbE3v+abd5@kili>
+
+Hi Dan,
+
+Thank you for the patch.
+)"},
+{
+"inbox/cur/msg3:2,S",
+R"(Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset=utf-8
+References: <YuvYh1JbE3v+abd5@kili>
+ <G6TStg8J52Q-uSMTR7wRQdPeloxpZMiEQT_F8_JIDYM25eEPeHGgrNKO0fuO78MiQgD9Mz4BDtsZlZgmPKFe4Q==@protonmail.internalid>
+To: "Dan Carpenter" <dan.carpenter@oracle.com>, "Joerg Roedel"
+ <joro@8bytes.org>, "Suman Anna" <s-anna@ti.com>
+Reply-To: "Robin Murphy" <robin.murphy@arm.com>
+From: "Robin Murphy" <robin.murphy@arm.com>
+Subject: Re: [PATCH] iommu/omap: fix buffer overflow in debugfs
+List-Id: <kernel-janitors.vger.kernel.org>
+Message-Id: <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com>
+Precedence: bulk
+In-Reply-To: <YuvYh1JbE3v+abd5@kili>
+Date: Thu, 4 Aug 2022 17:31:39 +0100
+
+On 04/08/2022 3:32 pm, Dan Carpenter wrote:
+> There are two issues here:
+)"},
+{
+"inbox/new/msg4",
+R"(Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset=utf-8
+References: <YuvYh1JbE3v+abd5@kili>
+ <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com>
+ <T4CDWjUrgtI5n4mh1JEdW6RLYzqbPE9-yDrhEVwDM22WX-198fBwcnLd-4_xR1gvsVSHQps9fp_pZevTF0ZmaA==@protonmail.internalid>
+To: "Robin Murphy" <robin.murphy@arm.com>
+Reply-To: "Dan Carpenter" <dan.carpenter@oracle.com>
+From: "Dan Carpenter" <dan.carpenter@oracle.com>
+Subject: Re: [PATCH] iommu/omap: fix buffer overflow in debugfs
+List-Id: <kernel-janitors.vger.kernel.org>
+Date: Fri, 5 Aug 2022 09:37:02 +0300
+In-Reply-To: <90a760c4-6e88-07b4-1f20-8b10414e49aa@arm.com>
+Precedence: bulk
+Message-Id: <20220805063702.GH3438@kadam>
+
+On Thu, Aug 04, 2022 at 05:31:39PM +0100, Robin Murphy wrote:
+> On 04/08/2022 3:32 pm, Dan Carpenter wrote:
+> > There are two issues here:
+)"},
+}};
+ TempDir tdir;
+ auto store{make_test_store(tdir.path(), test_msgs, {})};
+ {
+ auto qr = store.run_query("fix buffer overflow in debugfs",
+ Field::Id::Date, QueryFlags::IncludeRelated);
+ g_assert_true(!!qr);
+ g_assert_cmpuint(qr->size(), ==, 4);
+ }
+
+ {
+ auto qr = store.run_query("fix buffer overflow in debugfs and flag:unread",
+ Field::Id::Date, QueryFlags::None);
+ g_assert_true(!!qr);
+ g_assert_cmpuint(qr->size(), ==, 1);
+ assert_equal(qr->begin().message_id().value_or(""), "20220805063702.GH3438@kadam");
+ assert_equal(qr->begin().thread_id().value_or(""), "YuvYh1JbE3v+abd5@kili");
+ }
+
+ {
+ /* this one failed earlier, because the 'protonmail' id is the
+ * first reference, which means it does _not_ have the same
+ * thread-id as the rest; however, we filter these
+ * fake-message-ids now.*/
+ g_test_bug("2312");
+
+ auto qr = store.run_query("fix buffer overflow in debugfs and flag:unread",
+ Field::Id::Date, QueryFlags::IncludeRelated);
+ g_assert_true(!!qr);
+ g_assert_cmpuint(qr->size(), ==, 4);
+ }
+}
+
+
+static void
+test_body_matricula()
+{
+ const TestMap test_msgs = {{
+{
+"basic/cur/matricula-msg:2,S",
+R"(From: XXX <XX@XX.com>
+Subject:
+ =?iso-8859-1?Q?EF_-_Pago_matr=EDcula_de_la_matr=EDcula_de_inscripci=F3n_a?=
+Date: Thu, 4 Aug 2022 14:29:41 +0000
+Message-ID:
+ <VE1PR03MB5471882920DE08CFE44D97A0FE9F9@VE1PR03MB5471.eurprd03.prod.outlook.com>
+Accept-Language: es-AR, es-ES, en-US
+Content-Language: es-AR
+X-MS-Has-Attach: yes
+Content-Type: multipart/mixed;
+ boundary="_004_VE1PR03MB5471882920DE08CFE44D97A0FE9F9VE1PR03MB5471eurp_"
+MIME-Version: 1.0
+X-OriginatorOrg: ef.com
+X-MS-Exchange-CrossTenant-AuthAs: Internal
+X-MS-Exchange-CrossTenant-AuthSource: VE1PR03MB5471.eurprd03.prod.outlook.com
+
+--_004_VE1PR03MB5471882920DE08CFE44D97A0FE9F9VE1PR03MB5471eurp_
+Content-Type: multipart/alternative;
+ boundary="_000_VE1PR03MB5471882920DE08CFE44D97A0FE9F9VE1PR03MB5471eurp_"
+
+--_000_VE1PR03MB5471882920DE08CFE44D97A0FE9F9VE1PR03MB5471eurp_
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+Buenas tardes Familia,
+
+
+Espero que est=E9n muy bien.
+
+
+
+Ya cargamos en sistema su pre inscripci=F3n para el curso
+
+
+Quedamos atentos ante cualquier consulta que surja.
+
+Saludos,
+)"},
+}};
+ TempDir tdir;
+ auto store{make_test_store(tdir.path(), test_msgs, {})};
+
+ /* i.e., non-utf8 text parts were not converted */
+ g_test_bug("2333");
+
+ // matches
+ for (auto&& expr: {
+ "subject:matrícula",
+ "subject:matricula",
+ "body:atentos",
+ "body:inscripción"
+ }) {
+
+ if (g_test_verbose())
+ g_message("query: '%s'", expr);
+ auto qr = store.run_query(expr);
+ assert_valid_result(qr);
+ g_assert_false(qr->empty());
+ g_assert_cmpuint(qr->size(), ==, 1);
+ }
+}
+
+
+
+static void
+test_duplicate_refresh_real(bool rename)
+{
+ g_test_bug("2327");
+
+ const TestMap test_msgs = {{
+ "inbox/new/msg",
+ { R"(Message-Id: <abcde@foo.bar>
+From: "Foo Example" <bar@example.com>
+Date: Wed, 26 Oct 2022 11:01:54 -0700
+To: example@example.com
+Subject: Rainy night in Helsinki
+
+Boo!
+)"},
+ }};
+
+ /* create maildir with message */
+ TempDir tdir;
+ auto store{make_test_store(tdir.path(), test_msgs, {})};
+ g_debug("%s", store.root_maildir().c_str());
+ /* ensure we have a proper maildir, with new/, cur/ */
+ auto mres = maildir_mkdir(store.root_maildir() + "/inbox");
+ assert_valid_result(mres);
+ g_assert_cmpuint(store.size(), ==, 1U);
+
+ /*
+ * find the one msg with a query
+ */
+ auto qr = store.run_query("Helsinki", Field::Id::Date, QueryFlags::None);
+ g_assert_true(!!qr);
+ g_assert_cmpuint(qr->size(), ==, 1);
+ const auto old_path = qr->begin().path().value();
+ const auto old_docid = qr->begin().doc_id();
+ assert_equal(qr->begin().message()->path(), old_path);
+ g_assert_true(::access(old_path.c_str(), F_OK) == 0);
+
+
+ /*
+ * mark as read, i.e. move to cur/; ensure it really moved.
+ */
+ auto move_opts{rename ? Store::MoveOptions::ChangeName : Store::MoveOptions::None};
+ auto moved_msgs = store.move_message(old_docid, Nothing, Flags::Seen, move_opts);
+ assert_valid_result(moved_msgs);
+
+ g_assert_true(moved_msgs->size() == 1);
+ auto&& moved_msg_opt = store.find_message(moved_msgs->at(0).first);
+ g_assert_true(!!moved_msg_opt);
+ const auto&moved_msg = std::move(*moved_msg_opt);
+ const auto new_path = moved_msg.path();
+ if (!rename)
+ assert_equal(new_path, store.root_maildir() + "/inbox/cur/msg:2,S");
+ g_assert_cmpuint(store.size(), ==, 1);
+ g_assert_false(::access(old_path.c_str(), F_OK) == 0);
+ g_assert_true(::access(new_path.c_str(), F_OK) == 0);
+
+ /* also ensure that the cached sexp for the message has been updated;
+ * that's what mu4e uses */
+ const auto moved_sexp{moved_msg.sexp()};
+ g_assert_true(moved_sexp.plistp());
+ g_assert_true(!!moved_sexp.get_prop(":path"));
+ assert_equal(moved_sexp.get_prop(":path").value().string(), new_path);
+
+ /*
+ * find new message with query, ensure it's really that new one.
+ */
+ auto qr2 = store.run_query("Helsinki", Field::Id::Date, QueryFlags::None);
+ g_assert_true(!!qr2);
+ g_assert_cmpuint(qr2->size(), ==, 1);
+ assert_equal(qr2->begin().path().value(), new_path);
+
+ /* index the messages */
+ auto res = store.indexer().start({});
+ g_assert_true(res);
+ while(store.indexer().is_running()) {
+ using namespace std::chrono_literals;
+ std::this_thread::sleep_for(100ms);
+ }
+ g_assert_cmpuint(store.size(), ==, 1);
+
+ /*
+ * ensure query still has the right results
+ */
+ auto qr3 = store.run_query("Helsinki", Field::Id::Date, QueryFlags::None);
+ g_assert_true(!!qr3);
+ g_assert_cmpuint(qr3->size(), ==, 1);
+ const auto path3{qr3->begin().path().value()};
+ assert_equal(path3, new_path);
+ assert_equal(qr3->begin().message()->path(), new_path);
+ g_assert_true(::access(path3.c_str(), F_OK) == 0);
+}
+
+
+static void
+test_duplicate_refresh()
+{
+ test_duplicate_refresh_real(false/*no rename*/);
+}
+
+
+static void
+test_duplicate_refresh_rename()
+{
+ test_duplicate_refresh_real(true/*rename*/);
+}
+
+static void
+test_term_split()
+{
+ g_test_bug("2365");
+
+ // Note the fancy quote in "foo’s bar"
+ const TestMap test_msgs = {{
+ "inbox/new/msg",
+ {
+R"(Message-Id: <abcde@foo.bar>
+From: "Foo Example" <bar@example.com>
+Date: Wed, 26 Oct 2022 11:01:54 -0700
+To: example@example.com
+Subject: foo’s bar
+
+Boo!
+)"},
+ }};
+
+ TempDir tdir;
+ auto store{make_test_store(tdir.path(), test_msgs, {})};
+ /* true: match; false: no match */
+ const auto cases = std::array<std::pair<const char*, bool>, 8>{{
+ {"subject:foo's", true},
+ {"subject:foo*", true},
+ {"subject:/foo/", true},
+ {"subject:/foo’s/", true}, /* <-- breaks before PR #2365 */
+ {"subject:/foo.*bar/", true}, /* <-- breaks before PR #2365 */
+ {"subject:/foo’s bar/", false}, /* <-- no matching, needs quoting */
+ {"subject:\"/foo’s bar/\"", true}, /* <-- this works, quote the regex */
+ {R"(subject:"/foo’s bar/")", true}, /* <-- this works, quote the regex */
+ }};
+
+ for (auto&& test: cases) {
+ mu_debug("query: '{}'", test.first);
+ auto qr = store.run_query(test.first);
+ assert_valid_result(qr);
+ if (test.second)
+ g_assert_cmpuint(qr->size(), ==, 1);
+ else
+ g_assert_true(qr->empty());
+ }
+}
+
+static void
+test_subject_kata_containers()
+{
+ g_test_bug("2167");
+
+ // Note the fancy quote in "foo’s bar"
+ const TestMap test_msgs = {{
+ "inbox/new/msg",
+ {
+R"(Message-Id: <abcde@foo.bar>
+From: "Foo Example" <bar@example.com>
+Date: Wed, 26 Oct 2022 11:01:54 -0700
+To: example@example.com
+Subject: kata-containers
+
+voodoo-containers
+
+Boo!
+)"},
+ }};
+
+ TempDir tdir;
+ auto store{make_test_store(tdir.path(), test_msgs, {})};
+ /* true: match; false: no match */
+ const auto cases = std::vector<std::pair<const char*, bool>>{{
+ {"subject:kata", true},
+ {"subject:containers", true},
+ {"subject:kata-containers", true},
+ {"subject:\"kata containers\"", true},
+ {"voodoo-containers", true},
+ {"voodoo containers", true}
+ }};
+
+ for (auto&& test: cases) {
+ mu_debug("query: '{}'", test.first);
+ auto qr = store.run_query(test.first);
+ assert_valid_result(qr);
+ if (test.second)
+ g_assert_cmpuint(qr->size(), ==, 1);
+ else
+ g_assert_true(qr->empty());
+ }
+}
+
+static void
+test_related_dup_threaded()
+{
+ // test message sent to self, and copy of received msg.
+
+ const auto test_msg = R"(From: "Edward Mallory" <ed@leviathan.gb>
+To: "Laurence Oliphant <oli@hotmail.com>
+Subject: Boo
+Date: Wed, 07 Dec 2022 18:38:06 +0200
+Message-ID: <875yentbhg.fsf@djcbsoftware.nl>
+MIME-Version: 1.0
+Content-Type: text/plain
+
+Boo!
+)";
+ const TestMap test_msgs = {
+ {"sent/cur/msg1", test_msg },
+ {"inbox/cur/msg1", test_msg },
+ {"inbox/cur/msg2", test_msg }};
+
+ TempDir tdir;
+ auto store{make_test_store(tdir.path(), test_msgs, {})};
+
+ g_assert_cmpuint(store.size(), ==, 3);
+
+
+ // normal query should give 2
+ {
+ auto qr = store.run_query("maildir:/inbox", Field::Id::Date,
+ QueryFlags::None);
+ assert_valid_result(qr);
+ g_assert_cmpuint(qr->size(), ==, 2);
+ }
+
+ // a related query should give 3
+ {
+ auto qr = store.run_query("maildir:/inbox", Field::Id::Date,
+ QueryFlags::IncludeRelated);
+ assert_valid_result(qr);
+ g_assert_cmpuint(qr->size(), ==, 3);
+ }
+
+ // a related/threading query should give 3.
+ {
+ auto qr = store.run_query("maildir:/inbox", Field::Id::Date,
+ QueryFlags::IncludeRelated | QueryFlags::Threading);
+ assert_valid_result(qr);
+ g_assert_cmpuint(qr->size(), ==, 3);
+ }
+}
+
+
+static void
+test_html()
+{
+ // test message sent to self, and copy of received msg.
+
+ const auto test_msg = R"(From: Test <test@example.com>
+To: abc@example.com
+Date: Mon, 23 May 2011 10:53:45 +0200
+Subject: vla
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d"
+Message-ID: <10374608.109906.11909.20115aabbccdd.MSGID@mailinglijst.nl>
+
+--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d
+Content-Type: text/plain; charset="iso-8859-15"
+Content-Transfer-Encoding: quoted-printable
+
+text
+
+--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d
+Content-Type: text/html; charset="iso-8859-15"
+Content-Transfer-Encoding: quoted-printable
+
+html
+
+--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d--
+)";
+ const TestMap test_msgs = {{"inbox/cur/msg1", test_msg }};
+
+ TempDir tdir;
+ auto store{make_test_store(tdir.path(), test_msgs, {})};
+ g_assert_cmpuint(store.size(), ==, 1);
+
+ {
+ auto qr = store.run_query("body:text", Field::Id::Date,
+ QueryFlags::None);
+ assert_valid_result(qr);
+ g_assert_cmpuint(qr->size(), ==, 1);
+ }
+
+ {
+ auto qr = store.run_query("body:html", Field::Id::Date,
+ QueryFlags::None);
+ assert_valid_result(qr);
+ g_assert_cmpuint(qr->size(), ==, 1);
+ }
+}
+
+
+static void
+test_ngrams()
+{
+ g_test_bug("2167");
+
+ // Note the fancy quote in "foo’s bar"
+ const TestMap test_msgs = {{
+ "inbox/new/msg",
+ {
+R"(From: "Bob" <bob@builder.com>
+Subject: スポンサーシップ募集
+To: "Chase" <chase@ppatrol.org>
+Message-Id: 112342343e9dfo.fsf@builder.com
+
+ 中文
+
+https://trac.xapian.org/ticket/719
+
+ サーバがダウンしました
+)"}}};
+
+ MemDb mdb;
+ Config conf{mdb};
+ conf.set<Config::Id::SupportNgrams>(true);
+
+ TempDir tdir;
+ auto store{make_test_store(tdir.path(), test_msgs, conf)};
+
+ /* true: match; false: no match */
+ const auto cases = std::vector<std::pair<std::string_view, bool>>{{
+ {"body:中文", true},
+ {"body:中", true},
+ {"body:文", true},
+ {"body:し", true},
+ {"body:サー", true},
+ {"body:サーバがダウンしました", true}, // fail
+ {"中文", true},
+ {"中", true},
+ {"文", true},
+ {"subject:スポン", true },
+ {"subject:スポンサーシップ募集", true },
+ {"subject:シップ", true }, // XXX should match
+ {"サーバがダウンしました", true}, // okay
+ {"body:サーバがダウンしました", true}, // okay
+ {"subject:スポンサーシップ募集", true}, // okay
+ {"subject:シップx", true }, // XXX should match
+ }};
+
+ for (auto&& test: cases) {
+ auto qr = store.run_query(std::string{test.first});
+ assert_valid_result(qr);
+ if (test.second)
+ g_assert_cmpuint(qr->size(), ==, 1);
+ else
+ g_assert_true(qr->empty());
+ }
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/store/query/simple",
+ test_simple);
+ g_test_add_func("/store/query/spam-address-components",
+ test_spam_address_components);
+ g_test_add_func("/store/query/dups-related",
+ test_dups_related);
+ g_test_add_func("/store/query/related-missing-root",
+ test_related_missing_root);
+ g_test_add_func("/store/query/body-matricula",
+ test_body_matricula);
+ g_test_add_func("/store/query/duplicate-refresh",
+ test_duplicate_refresh);
+ g_test_add_func("/store/query/duplicate-refresh-rename",
+ test_duplicate_refresh_rename);
+ g_test_add_func("/store/query/term-split",
+ test_term_split);
+ g_test_add_func("/store/query/kata_containers",
+ test_subject_kata_containers);
+ g_test_add_func("/store/query/related-dup-threaded",
+ test_related_dup_threaded);
+ g_test_add_func("/store/query/html",
+ test_html);
+ g_test_add_func("/store/query/ngrams",
+ test_ngrams);
+
+ return g_test_run();
+}
--- /dev/null
+/*
+** Copyright (C) 2008-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <glib.h>
+#include <stdlib.h>
+#include <thread>
+#include <array>
+#include <unistd.h>
+#include <time.h>
+#include <fstream>
+
+#include <locale.h>
+
+#include "utils/mu-test-utils.hh"
+#include "mu-store.hh"
+#include "utils/mu-result.hh"
+#include <utils/mu-utils.hh>
+#include <utils/mu-utils-file.hh>
+#include "mu-maildir.hh"
+
+using namespace Mu;
+
+using namespace std::chrono_literals;
+
+static std::string MuTestMaildir = Mu::canonicalize_filename(MU_TESTMAILDIR, "/");
+static std::string MuTestMaildir2 = Mu::canonicalize_filename(MU_TESTMAILDIR2, "/");
+
+static void
+test_store_ctor_dtor()
+{
+ TempDir tempdir;
+ auto store{Store::make_new(tempdir.path(), "/tmp")};
+ assert_valid_result(store);
+
+ g_assert_true(store->empty());
+ g_assert_cmpuint(0, ==, store->size());
+
+ g_assert_cmpuint(MU_STORE_SCHEMA_VERSION, ==,
+ store->config().get<Config::Id::SchemaVersion>());
+}
+
+static void
+test_store_reinit()
+{
+ TempDir tempdir;
+ {
+ MemDb mdb;
+ Config conf{mdb};
+ conf.set<Config::Id::MaxMessageSize>(1234567);
+ conf.set<Config::Id::BatchSize>(7654321);
+ conf.set<Config::Id::PersonalAddresses>(
+ StringVec{ "foo@example.com", "bar@example.com" });
+
+ auto store{Store::make_new(tempdir.path(), MuTestMaildir, conf)};
+ assert_valid_result(store);
+
+ g_assert_true(store->empty());
+ g_assert_cmpuint(0, ==, store->size());
+
+ g_assert_cmpuint(MU_STORE_SCHEMA_VERSION, ==,
+ store->config().get<Config::Id::SchemaVersion>());
+
+ const auto msgpath{MuTestMaildir + "/cur/1283599333.1840_11.cthulhu!2,"};
+ const auto id = store->add_message(msgpath);
+ assert_valid_result(id);
+ g_assert_true(store->contains_message(msgpath));
+ g_assert_cmpuint(store->size(), ==, 1);
+ }
+
+ //now let's reinitialize it.
+ {
+ auto store{Store::make(tempdir.path(),
+ Store::Options::Writable|Store::Options::ReInit)};
+
+ assert_valid_result(store);
+ g_assert_true(store->empty());
+
+ assert_equal(store->path(), tempdir.path());
+ assert_equal(store->root_maildir(), MuTestMaildir);
+
+ g_assert_cmpuint(store->config().get<Config::Id::BatchSize>(),==,7654321);
+ g_assert_cmpuint(store->config().get<Config::Id::MaxMessageSize>(),==,1234567);
+
+ const auto addrs{store->config().get<Config::Id::PersonalAddresses>()};
+ g_assert_cmpuint(addrs.size(),==,2);
+ g_assert_true(seq_some(addrs, [](auto&& a){return a=="foo@example.com";}));
+ g_assert_true(seq_some(addrs, [](auto&& a){return a=="bar@example.com";}));
+
+ const auto msgpath{MuTestMaildir + "/cur/1283599333.1840_11.cthulhu!2,"};
+ const auto id = store->add_message(msgpath);
+ assert_valid_result(id);
+ g_assert_true(store->contains_message(msgpath));
+ g_assert_cmpuint(store->size(), ==, 1);
+ }
+}
+
+
+
+static void
+test_store_add_count_remove()
+{
+ TempDir tempdir{false};
+
+ auto store{Store::make_new(tempdir.path() + "/xapian", MuTestMaildir)};
+ assert_valid_result(store);
+
+ assert_equal(store->path(), tempdir.path() + "/xapian");
+ assert_equal(store->root_maildir(), MuTestMaildir);
+
+ const auto msgpath{MuTestMaildir + "/cur/1283599333.1840_11.cthulhu!2,"};
+ const auto id1 = store->add_message(msgpath);
+ assert_valid_result(id1);
+
+ g_assert_cmpuint(store->size(), ==, 1);
+ g_assert_true(store->contains_message(msgpath));
+
+ const auto id2 = store->add_message(MuTestMaildir2 + "/bar/cur/mail3");
+ g_assert_false(!!id2); // wrong maildir.
+
+ const auto msg3path{MuTestMaildir + "/cur/1252168370_3.14675.cthulhu!2,S"};
+ const auto id3 = store->add_message(msg3path);
+ assert_valid_result(id3);
+
+ g_assert_cmpuint(store->size(), ==, 2);
+ g_assert_true(store->contains_message(msg3path));
+
+ store->remove_message(id1.value());
+ g_assert_cmpuint(store->size(), ==, 1);
+ g_assert_false(
+ store->contains_message(MuTestMaildir + "/cur/1283599333.1840_11.cthulhu!2,"));
+
+ store->remove_message(msg3path);
+ g_assert_true(store->empty());
+ g_assert_false(store->contains_message(msg3path));
+}
+
+
+static void
+test_message_mailing_list()
+{
+ constexpr const char *test_message_1 =
+R"(Return-Path: <sqlite-dev-bounces@sqlite.org>
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+ by mail.xxxxsoftware.nl (Postfix) with ESMTP id 32F276963F
+ for <xxxx@localhost>; Mon, 4 Aug 2008 21:49:34 +0300 (EEST)
+Message-Id: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net>
+From: anon@example.com
+To: sqlite-dev@sqlite.org
+Mime-Version: 1.0 (Apple Message framework v926)
+Date: Mon, 4 Aug 2008 11:40:49 +0200
+X-Mailer: Apple Mail (2.926)
+Subject: Capybaras United
+Precedence: list
+Reply-To: sqlite-dev@sqlite.org
+List-Id: <sqlite-dev.sqlite.org>
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Sender: sqlite-dev-bounces@sqlite.org
+Content-Length: 639
+
+Inside sqlite3VdbeExec there is a very big switch statement.
+In order to increase performance with few modifications to the
+original code, why not use this technique ?
+http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html
+
+With a properly defined "instructions" array, instead of the switch
+statement you can use something like:
+goto * instructions[pOp->opcode];
+)";
+ TempDir tempdir;
+ auto store{Store::make_new(tempdir.path(), "/home/test/Maildir")};
+ assert_valid_result(store);
+
+ const auto msgpath{"/home/test/Maildir/inbox/cur/1649279256.107710_1.evergrey:2,S"};
+ auto message{Message::make_from_text(test_message_1, msgpath)};
+ assert_valid_result(message);
+
+ const auto docid = store->add_message(*message);
+ assert_valid_result(docid);
+ g_assert_cmpuint(store->size(),==, 1);
+
+ /* ensure 'update' dtrt, i.e., nothing. */
+ const auto docid2 = store->add_message(*message);
+ assert_valid_result(docid2);
+ g_assert_cmpuint(store->size(),==, 1);
+ g_assert_cmpuint(*docid,==,*docid2);
+
+ auto msg2{store->find_message(*docid)};
+ g_assert_true(!!msg2);
+ assert_equal(message->path(), msg2->path());
+
+ g_assert_true(store->contains_message(message->path()));
+
+ const auto qr = store->run_query("to:sqlite-dev@sqlite.org");
+ g_assert_true(!!qr);
+ g_assert_cmpuint(qr->size(), ==, 1);
+}
+
+
+static void
+test_message_attachments(void)
+{
+ constexpr const char* msg_text =
+R"(Return-Path: <foo@example.com>
+Received: from pop.gmail.com [256.85.129.309]
+ by evergrey with POP3 (fetchmail-6.4.29)
+ for <djcb@localhost> (single-drop); Thu, 24 Mar 2022 20:12:40 +0200 (EET)
+Sender: "Foo, Example" <foo@example.com>
+User-agent: mu4e 1.7.11; emacs 29.0.50
+From: "Foo Example" <foo@example.com>
+To: bar@example.com
+Subject: =?utf-8?B?w6R0dMOkY2htZcOxdHM=?=
+Date: Thu, 24 Mar 2022 20:04:39 +0200
+Organization: ACME Inc.
+Message-Id: <3144HPOJ0VC77.3H1XTAG2AMTLH@"@WILSONB.COM>
+MIME-Version: 1.0
+X-label: @NextActions operation:mindcrime Queensrÿche
+Content-Type: multipart/mixed; boundary="=-=-="
+
+--=-=-=
+Content-Type: text/plain
+
+Hello,
+--=-=-=
+Content-Type: image/jpeg
+Content-Disposition: attachment; filename=file-01.bin
+Content-Transfer-Encoding: base64
+
+AAECAw==
+--=-=-=
+Content-Type: audio/ogg
+Content-Disposition: inline; filename=/tmp/file-02.bin
+Content-Transfer-Encoding: base64
+
+BAUGBw==
+--=-=-=
+Content-Type: message/rfc822
+Content-Disposition: attachment;
+ filename="message.eml"
+
+From: "Fnorb" <fnorb@example.com>
+To: Bob <bob@example.com>
+Subject: news for you
+Date: Mon, 28 Mar 2022 22:53:26 +0300
+
+Attached message!
+
+--=-=-=
+Content-Type: text/plain
+
+World!
+--=-=-=--
+)";
+
+ TempDir tempdir;
+ auto store{Store::make_new(tempdir.path(), "/home/test/Maildir")};
+ assert_valid_result(store);
+
+ auto message{Message::make_from_text(
+ msg_text,
+ "/home/test/Maildir/inbox/cur/1649279256.abcde_1.evergrey:2,S")};
+ assert_valid_result(message);
+
+ const auto docid = store->add_message(*message);
+ assert_valid_result(docid);
+
+ auto msg2{store->find_message(*docid)};
+ g_assert_true(!!msg2);
+ assert_equal(message->path(), msg2->path());
+
+ g_assert_true(store->contains_message(message->path()));
+
+ // for (auto&& term = msg2->document().xapian_document().termlist_begin();
+ // term != msg2->document().xapian_document().termlist_end(); ++term)
+ // g_message(">>> %s", (*term).c_str());
+
+ const auto stats{store->statistics()};
+ g_assert_cmpuint(stats.size,==,store->size());
+ g_assert_cmpuint(stats.last_index,==,0);
+ g_assert_cmpuint(stats.last_change,>=,::time({}));
+}
+
+
+static void
+test_index_move()
+{
+ const std::string msg_text =
+R"(From: Valentine Michael Smith <mike@example.com>
+To: Raul Endymion <raul@example.com>
+Cc: emacs-devel@gnu.org
+Subject: Re: multi-eq hash tables
+Date: Tue, 03 May 2022 20:58:02 +0200
+Message-ID: <87h766tzzz.fsf@gnus.org>
+MIME-Version: 1.0
+Content-Type: text/plain
+Precedence: list
+List-Id: "Emacs development discussions." <emacs-devel.gnu.org>
+List-Post: <mailto:emacs-devel@gnu.org>
+
+Raul Endymion <raul@example.com> writes:
+
+> Maybe we should introduce something like:
+>
+> (define-hash-table-test shallow-equal
+> (lambda (x1 x2) (while (and (consp x1) (consp x2) (eql (car x1) (car x2)))
+> (setq x1 (cdr x1)) (setq x2 (cdr x2)))
+> (equal x1 x2)))
+> ...)
+
+Yes, that would be excellent.
+)";
+
+ TempDir tempdir2;
+
+ { // create a message file.
+ const auto res1 = maildir_mkdir(tempdir2.path() + "/Maildir/a");
+ assert_valid_result(res1);
+
+ std::ofstream output{tempdir2.path() + "/Maildir/a/new/msg"};
+ output.write(msg_text.c_str(), msg_text.size());
+ output.close();
+ g_assert_true(output.good());
+ }
+
+ // Index it into a store.
+ TempDir tempdir;
+ auto store{Store::make_new(tempdir.path(), tempdir2.path() + "/Maildir")};
+ assert_valid_result(store);
+
+ store->indexer().start({});
+ size_t n{};
+ while (store->indexer().is_running()) {
+ std::this_thread::sleep_for(100ms);
+ g_assert_cmpuint(n++,<=,25);
+ }
+ g_assert_true(!store->indexer().is_running());
+ const auto& prog{store->indexer().progress()};
+ g_assert_cmpuint(prog.updated,==,1);
+ g_assert_cmpuint(store->size(), ==, 1);
+ g_assert_false(store->empty());
+
+ // Find the message
+ auto qr = store->run_query("path:" + tempdir2.path() + "/Maildir/a/new/msg");
+ assert_valid_result(qr);
+ g_assert_cmpuint(qr->size(),==,1);
+
+ const auto msg = qr->begin().message();
+ g_assert_true(!!msg);
+
+ // Check the message
+ const auto oldpath{msg->path()};
+ assert_equal(msg->subject(), "Re: multi-eq hash tables");
+ g_assert_true(msg->docid() != 0);
+ g_debug("%s", msg->sexp().to_string().c_str());
+
+ // Move the message from new->cur
+ std::this_thread::sleep_for(1s); /* ctime should change */
+ const auto msgs3 = store->move_message(msg->docid(), {}, Flags::Seen);
+ assert_valid_result(msgs3);
+ g_assert_true(msgs3->size() == 1);
+ auto&& msg3_opt{store->find_message(msgs3->at(0).first/*id*/)};
+ g_assert_true(!!msg3_opt);
+ auto&& msg3{std::move(*msg3_opt)};
+
+ assert_equal(msg3.maildir(), "/a");
+ assert_equal(msg3.path(), tempdir2.path() + "/Maildir/a/cur/msg:2,S");
+ g_assert_true(::access(msg3.path().c_str(), R_OK)==0);
+ g_assert_false(::access(oldpath.c_str(), R_OK)==0);
+
+ g_debug("%s", msg3.sexp().to_string().c_str()); g_assert_cmpuint(store->size(), ==, 1);
+}
+
+
+
+static void
+test_store_move_dups()
+{
+ const std::string msg_text =
+R"(From: Valentine Michael Smith <mike@example.com>
+To: Raul Endymion <raul@example.com>
+Subject: Re: multi-eq hash tables
+Date: Tue, 03 May 2022 20:58:02 +0200
+Message-ID: <87h766tzzz.fsf@gnus.org>
+
+Yes, that would be excellent.
+)";
+ TempDir tempdir2;
+
+ // create a message file + dups
+ const auto res1 = maildir_mkdir(tempdir2.path() + "/Maildir/a");
+ assert_valid_result(res1);
+ const auto res2 = maildir_mkdir(tempdir2.path() + "/Maildir/b");
+ assert_valid_result(res2);
+
+ auto msg1_path = join_paths(tempdir2.path(), "Maildir/a/new/msg123");
+ auto msg2_path = join_paths(tempdir2.path(), "Maildir/a/cur/msgabc:2,S");
+ auto msg3_path = join_paths(tempdir2.path(),"Maildir/b/cur/msgdef:2,RS");
+
+ TempDir tempdir;
+ auto store{Store::make_new(tempdir.path(),
+ join_paths(tempdir2.path() , "Maildir"))};
+ assert_valid_result(store);
+
+ std::vector<Store::Id> ids;
+ for (auto&& p: {msg1_path, msg2_path, msg3_path}) {
+ std::ofstream output{p};
+ output.write(msg_text.c_str(), msg_text.size());
+ output.close();
+ auto res = store->add_message(p);
+ assert_valid_result(res);
+ ids.emplace_back(*res);
+ }
+ g_assert_cmpuint(store->size(), ==, 3);
+
+ // mark main message (+ dups) as seen
+ auto mres = store->move_message(ids.at(0), {},
+ Flags::Seen | Flags::Flagged | Flags::Passed,
+ Store::MoveOptions::DupFlags);
+ assert_valid_result(mres);
+ mu_info("found {} matches", mres->size());
+ for (auto&& m: *mres)
+ mu_info("id: {}: {}", m.first, m.second);
+
+ // al three dups should have been updated
+ g_assert_cmpuint(mres->size(), ==, 3);
+ auto&& id_msgs{store->find_messages(Store::id_vec(*mres))};
+
+ // first should be the original
+ g_assert_cmpuint(id_msgs.at(0).first, ==, ids.at(0));
+ { // Message 1
+ const Message& msg = id_msgs.at(0).second;
+ assert_equal(msg.path(), tempdir2.path() + "/Maildir/a/cur/msg123:2,FPS");
+ g_assert_true(msg.flags() == (Flags::Seen|Flags::Flagged|Flags::Passed));
+ }
+ // note: Seen and Passed should be added to msg2/3, but Flagged shouldn't
+ // msg3 should loose its R flag.
+
+ auto check_msg2 = [&](const Message& msg) {
+ assert_equal(msg.path(), join_paths(tempdir2.path(), "/Maildir/a/cur/msgabc:2,PS"));
+ };
+ auto check_msg3 = [&](const Message& msg) {
+ assert_equal(msg.path(), join_paths(tempdir2.path(), "/Maildir/b/cur/msgdef:2,PS"));
+ };
+
+ if (id_msgs.at(1).first == ids.at(1)) {
+ check_msg2(id_msgs.at(1).second);
+ check_msg3(id_msgs.at(2).second);
+ } else {
+ check_msg2(id_msgs.at(2).second);
+ check_msg3(id_msgs.at(1).second);
+ }
+}
+
+static void
+test_store_circular_symlink(void)
+{
+ allow_warnings();
+
+ g_test_bug("2517");
+
+ auto testhome{unwrap(make_temp_dir())};
+ auto dbpath{runtime_path(RuntimePath::XapianDb, testhome)};
+
+ /* create a writable copy */
+ const auto testmdir = join_paths(testhome, "test-maildir");
+ auto cres1 = run_command({CP_PROGRAM, "-r", MU_TESTMAILDIR, testmdir});
+ assert_valid_command(cres1);
+ // create a symink
+ auto cres2 = run_command({LN_PROGRAM, "-s", testmdir, join_paths(testmdir, "testlink")});
+ assert_valid_command(cres2);
+
+ auto&& store = unwrap(Store::make_new(dbpath, testmdir));
+ store.indexer().start({});
+ size_t n{};
+ while (store.indexer().is_running()) {
+ std::this_thread::sleep_for(100ms);
+ g_assert_cmpuint(n++,<=,25);
+ }
+ // there will be a lot of dups....
+ g_assert_false(store.empty());
+
+ remove_directory(testhome);
+}
+
+static void
+test_store_maildirs()
+{
+ allow_warnings();
+
+ TempDir tdir;
+ auto store = Store::make_new(tdir.path(), MU_TESTMAILDIR2);
+ assert_valid_result(store);
+ g_assert_true(store->empty());
+
+ const auto mdirs = store->maildirs();
+
+ g_assert_cmpuint(mdirs.size(), ==, 3);
+ g_assert(seq_some(mdirs, [](auto&& m){return m == "/Foo";}));
+ g_assert(seq_some(mdirs, [](auto&& m){return m == "/bar";}));
+ g_assert(seq_some(mdirs, [](auto&& m){return m == "/wom_bat";}));
+}
+
+
+static void
+test_store_parse()
+{
+ allow_warnings();
+
+ TempDir tdir;
+ auto store = Store::make_new(tdir.path(), MU_TESTMAILDIR2);
+ assert_valid_result(store);
+ g_assert_true(store->empty());
+
+ // Xapian internal format (get_description()) is _not_ guaranteed
+ // to be the same between versions
+ const auto&& pq1{store->parse_query("subject:\"hello world\"", false)};
+ const auto&& pq2{store->parse_query("subject:\"hello world\"", true)};
+
+ assert_equal(pq1, "(or (subject \"hello world\") (subject (phrase \"hello world\")))");
+
+ /* LCOV_EXCL_START*/
+ if (pq2 != "Query((Shello world OR (Shello PHRASE 2 Sworld)))") {
+ g_test_skip("incompatible xapian descriptions");
+ return;
+ }
+ /* LCOV_EXCL_STOP*/
+
+ assert_equal(pq2, "Query((Shello world OR (Shello PHRASE 2 Sworld)))");
+}
+
+static void
+test_store_fail()
+{
+ {
+ const auto store = Store::make("/root/non-existent-path/12345");
+ g_assert_false(!!store);
+ }
+
+ {
+ const auto store = Store::make_new("/../../root/non-existent-path/12345",
+ "/../../root/non-existent-path/54321");
+ g_assert_false(!!store);
+ }
+}
+
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/store/ctor-dtor", test_store_ctor_dtor);
+ g_test_add_func("/store/reinit", test_store_reinit);
+ g_test_add_func("/store/add-count-remove", test_store_add_count_remove);
+ g_test_add_func("/store/message/mailing-list",
+ test_message_mailing_list);
+ g_test_add_func("/store/message/attachments",
+ test_message_attachments);
+ g_test_add_func("/store/move-dups", test_store_move_dups);
+
+ g_test_add_func("/store/maildirs", test_store_maildirs);
+ g_test_add_func("/store/parse", test_store_parse);
+
+ g_test_add_func("/store/index/index-move", test_index_move);
+ g_test_add_func("/store/index/circular-symlink", test_store_circular_symlink);
+
+ g_test_add_func("/store/index/fail", test_store_fail);
+
+ return g_test_run();
+}
--- /dev/null
+/*
+** Copyright (C) 2022-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include <config.h>
+
+#include <vector>
+#include <glib.h>
+
+#include <iostream>
+#include <sstream>
+#include <unistd.h>
+
+#include "mu-store.hh"
+#include "mu-query.hh"
+#include "utils/mu-result.hh"
+#include "utils/mu-utils.hh"
+#include "utils/mu-test-utils.hh"
+
+using namespace Mu;
+
+static void
+test_query()
+{
+ allow_warnings();
+ TempDir temp_dir;
+
+ auto store = Store::make_new(temp_dir.path(), std::string{MU_TESTMAILDIR});
+ assert_valid_result(store);
+
+ auto&& idx{store->indexer()};
+ g_assert_true(idx.start(Indexer::Config{}));
+ while (idx.is_running()) {
+ g_usleep(1000);
+ }
+
+ auto dump_matches = [](const QueryResults& res) {
+ size_t n{};
+ for (auto&& item : res) {
+ if (g_test_verbose()) {
+ std::cout << item.query_match() << '\n';
+ mu_debug("{:02d} {} {}",
+ ++n,
+ item.path().value_or("<none>"),
+ item.message_id().value_or("<none>"));
+ }
+ }
+ };
+
+ g_assert_cmpuint(store->size(), ==, 19);
+
+ {
+ const auto res = store->run_query("", {}, QueryFlags::None);
+ g_assert_true(!!res);
+ g_assert_cmpuint(res->size(), ==, 19);
+ dump_matches(*res);
+
+ g_assert_cmpuint(store->count_query(""), ==, 19);
+
+ }
+
+ {
+ const auto res = store->run_query("", Field::Id::Path, QueryFlags::None, 11);
+ g_assert_true(!!res);
+ g_assert_cmpuint(res->size(), ==, 11);
+ dump_matches(*res);
+ }
+}
+
+int
+main(int argc, char* argv[]) try {
+
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/query", test_query);
+
+ return g_test_run();
+
+} catch (const std::runtime_error& re) {
+ std::cerr << re.what() << "\n";
+ return 1;
+} catch (...) {
+ std::cerr << "caught exception\n";
+ return 1;
+}
--- /dev/null
+## Copyright (C) 2022-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## 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, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+thirdparty=join_paths('..', '..', 'thirdparty')
+
+srcs = [
+ 'mu-command-handler.cc',
+ 'mu-html-to-text.cc',
+ 'mu-lang-detector.cc',
+ 'mu-logger.cc',
+ 'mu-option.cc',
+ 'mu-readline.cc',
+ 'mu-sexp.cc',
+ 'mu-utils-file.cc',
+ 'mu-utils.cc',
+]
+
+if not get_option('tests').disabled()
+ test_srcs = [ 'mu-test-utils.cc' ]
+else
+ test_srcs = []
+endif
+
+lib_mu_utils=static_library('mu-utils',
+ [ srcs, test_srcs ], dependencies: [
+ glib_dep,
+ gio_dep,
+ gio_unix_dep,
+ config_h_dep,
+ readline_dep,
+ cld2_dep
+], include_directories:
+ include_directories(['.', '..', thirdparty]),
+install: false)
+
+lib_mu_utils_dep = declare_dependency(
+ link_with: lib_mu_utils,
+ compile_args: '-DFMT_HEADER_ONLY',
+ include_directories:
+ include_directories(['.', '..', thirdparty]))
+
+#
+# tools
+#
+html2text = executable('mu-html2text',
+ 'mu-html-to-text.cc',
+ dependencies: [ lib_mu_utils_dep, glib_dep ],
+ cpp_args: ['-DBUILD_HTML_TO_TEXT'],
+ install: false)
+
+if not get_option('tests').disabled()
+ subdir('tests')
+endif
--- /dev/null
+/*
+** Copyright (C) 2020-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef __MU_ASYNC_QUEUE_HH__
+#define __MU_ASYNC_QUEUE_HH__
+
+#include <deque>
+#include <mutex>
+#include <chrono>
+#include <condition_variable>
+
+namespace Mu {
+
+constexpr std::size_t UnlimitedAsyncQueueSize{0};
+
+template <typename ItemType, /**< the type of Item to queue */
+ std::size_t MaxSize = UnlimitedAsyncQueueSize, /**< maximum size for the queue */
+ typename Allocator = std::allocator<ItemType>> /**< allocator for the items */
+
+class AsyncQueue {
+ public:
+ using value_type = ItemType;
+ using allocator_type = Allocator;
+ using size_type = std::size_t;
+ using reference = value_type&;
+ using const_reference = const value_type&;
+ using pointer = typename std::allocator_traits<allocator_type>::pointer;
+ using const_pointer = typename std::allocator_traits<allocator_type>::const_pointer;
+
+ using Timeout = std::chrono::steady_clock::duration;
+
+ /**
+ * Push an item to the end of the queue by moving it
+ *
+ * @param item the item to move to the end of the queue
+ * @param timeout and optional timeout
+ *
+ * @return true if the item was pushed; false otherwise.
+ */
+ bool push(const value_type& item, Timeout timeout = {}) {
+ return push(std::move(value_type(item)), timeout);
+ }
+
+ /**
+ * Push an item to the end of the queue by moving it
+ *
+ * @param item the item to move to the end of the queue
+ * @param timeout and optional timeout
+ *
+ * @return true if the item was pushed; false otherwise.
+ */
+ bool push(value_type&& item, Timeout timeout = {}) {
+ std::unique_lock lock{m_};
+
+ if (!unlimited()) {
+ const auto rv = cv_full_.wait_for(lock, timeout, [&]() {
+ return !full_unlocked();
+ }) && !full_unlocked();
+ if (!rv)
+ return false;
+ }
+
+ q_.emplace_back(std::move(item));
+ cv_empty_.notify_one();
+
+ return true;
+ }
+
+ /**
+ * Pop an item from the queue
+ *
+ * @param receives the value if the function returns true
+ * @param timeout optional time to wait for an item to become available
+ *
+ * @return true if an item was popped (into val), false otherwise.
+ */
+ bool pop(value_type& val, Timeout timeout = {}) {
+ std::unique_lock lock{m_};
+
+ if (timeout != Timeout{}) {
+ const auto rv = cv_empty_.wait_for(lock, timeout, [&]() {
+ return !q_.empty();
+ }) && !q_.empty();
+ if (!rv)
+ return false;
+
+ } else if (q_.empty())
+ return false;
+
+ val = std::move(q_.front());
+ q_.pop_front();
+ cv_full_.notify_one();
+
+ return true;
+ }
+
+ /**
+ * Clear the queue
+ *
+ */
+ void clear() {
+ std::unique_lock lock{m_};
+ q_.clear();
+ cv_full_.notify_one();
+ }
+
+ /**
+ * Size of the queue
+ *
+ *
+ * @return the size
+ */
+ size_type size() const {
+ std::unique_lock lock{m_};
+ return q_.size();
+ }
+
+ /**
+ * Maximum size of the queue if specified through the template
+ * parameter; otherwise the (theoretical) max_size of the inner
+ * container.
+ *
+ * @return the maximum size
+ */
+ size_type max_size() const { return unlimited() ? q_.max_size() : MaxSize; }
+
+ /**
+ * Is the queue empty?
+ *
+ * @return true or false
+ */
+ bool empty() const {
+ std::unique_lock lock{m_};
+ return q_.empty();
+ }
+
+ /**
+ * Is the queue full? Returns false unless a maximum size was specified
+ * (as a template argument)
+ *
+ * @return true or false.
+ */
+ bool full() const {
+ if (unlimited())
+ return false;
+
+ std::unique_lock lock{m_};
+ return full_unlocked();
+ }
+
+ /**
+ * Is this queue (theoretically) unlimited in size?
+ *
+ * @return true or false
+ */
+ constexpr static bool unlimited() { return MaxSize == UnlimitedAsyncQueueSize; }
+
+private:
+ bool full_unlocked() const { return q_.size() >= max_size(); }
+
+ std::deque<ItemType, Allocator> q_;
+ mutable std::mutex m_;
+ std::condition_variable cv_full_, cv_empty_;
+};
+
+} // namespace Mu
+
+#endif /* __MU_ASYNC_QUEUE_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2020-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-command-handler.hh"
+#include "mu-error.hh"
+#include "mu-utils.hh"
+
+#include <iostream>
+#include <algorithm>
+
+using namespace Mu;
+
+Option<std::vector<std::string>>
+Command::string_vec_arg(const std::string& name) const
+{
+ auto&& val{arg_val(name, Sexp::Type::List)};
+ if (!val)
+ return Nothing;
+
+ std::vector<std::string> vec;
+ for (const auto& item : val->list()) {
+ if (!item.stringp()) {
+ // mu_warning("command: non-string in string-list for {}: {}",
+ // name, to_string());
+ return Nothing;
+ } else
+ vec.emplace_back(item.string());
+ }
+
+ return vec;
+}
+
+static Result<void>
+validate(const CommandHandler::CommandInfoMap& cmap,
+ const CommandHandler::CommandInfo& cmd_info,
+ const Command& cmd)
+{
+ // all required parameters must be present
+ for (auto&& arg : cmd_info.args) {
+
+ const auto& argname{arg.first};
+ const auto& arginfo{arg.second};
+
+ // calls use keyword-parameters, e.g.
+ //
+ // (my-function :bar 1 :cuux "fnorb")
+ //
+ // so, we're looking for the odd-numbered parameters.
+ const auto param_it = cmd.find_arg(argname);
+ const auto&& param_val = std::next(param_it);
+ // it's an error when a required parameter is missing.
+ if (param_it == cmd.cend()) {
+ if (arginfo.required)
+ return Err(Error::Code::Command,
+ "missing required parameter {} in command '{}'",
+ argname, cmd.to_string());
+ continue; // not required
+ }
+
+ // the types must match, but the 'nil' symbol is acceptable as "no value"
+ if (param_val->type() != arginfo.type && !(param_val->nilp()))
+ return Err(Error::Code::Command,
+ "parameter {} expects type {}, but got {} in command '{}'",
+ argname, to_string(arginfo.type),
+ to_string(param_val->type()), cmd.to_string());
+ }
+
+ // all parameters must be known
+ for (auto it = cmd.cbegin() + 1; it != cmd.cend() && it + 1 != cmd.cend(); it += 2) {
+ const auto& cmdargname{it->symbol()};
+ if (std::none_of(cmd_info.args.cbegin(), cmd_info.args.cend(),
+ [&](auto&& arg) { return cmdargname == arg.first; }))
+ return Err(Error::Code::Command,
+ "unknown parameter '{} 'in command '{}'",
+ cmdargname.name.c_str(), cmd.to_string().c_str());
+ }
+
+ return Ok();
+
+}
+
+Result<void>
+CommandHandler::invoke(const Command& cmd, bool do_validate) const
+{
+ const auto cmit{cmap_.find(cmd.name())};
+ if (cmit == cmap_.cend())
+ return Err(Error::Code::Command,
+ "unknown command '{}'", cmd.to_string().c_str());
+
+ const auto& cmd_info{cmit->second};
+ if (do_validate) {
+ if (auto&& res = validate(cmap_, cmd_info, cmd); !res)
+ return Err(res.error());
+ }
+
+ if (cmd_info.handler)
+ cmd_info.handler(cmd);
+
+ return Ok();
+}
+
+
+// LCOV_EXCL_START
+#ifdef BUILD_TESTS
+
+#include "mu-test-utils.hh"
+
+
+static void
+test_args()
+{
+ const auto cmd = Command::make_parse(R"((foo :bar 123 :cuux "456" :boo nil :bah true))");
+ assert_valid_result(cmd);
+
+ assert_equal(cmd->name(), "foo");
+ g_assert_true(cmd->find_arg(":bar") != cmd->cend());
+ g_assert_true(cmd->find_arg(":bxr") == cmd->cend());
+
+ g_assert_cmpint(cmd->number_arg(":bar").value_or(-1), ==, 123);
+ g_assert_cmpint(cmd->number_arg(":bor").value_or(-1), ==, -1);
+
+ assert_equal(cmd->string_arg(":cuux").value_or(""), "456");
+ assert_equal(cmd->string_arg(":caax").value_or(""), ""); // not present
+ assert_equal(cmd->string_arg(":bar").value_or("abc"), "abc"); // wrong type
+
+ g_assert_false(cmd->boolean_arg(":boo"));
+ g_assert_true(cmd->boolean_arg(":bah"));
+}
+
+using CommandInfoMap = CommandHandler::CommandInfoMap;
+using ArgMap = CommandHandler::ArgMap;
+using ArgInfo = CommandHandler::ArgInfo;
+using CommandInfo = CommandHandler::CommandInfo;
+
+static Result<void>
+call(const CommandInfoMap& cmap, const std::string& str) try {
+
+ if (const auto cmd{Command::make_parse(str)}; !cmd)
+ return Err(Error::Code::Internal, "invalid s-expression '{}'", str);
+ else
+ return CommandHandler(cmap).invoke(*cmd);
+
+} catch (const Error& err) {
+ return Err(Error{err});
+}
+
+static void
+test_command()
+{
+ allow_warnings();
+
+ CommandInfoMap ci_map;
+ ci_map.emplace(
+ "my-command",
+ CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}},
+ {":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}},
+ "My command,",
+ {}});
+ ci_map.emplace(
+ "another-command",
+ CommandInfo{
+ ArgMap{
+ {":queries", ArgInfo{Sexp::Type::List, false,
+ "queries for which to get read/unread numbers"}},
+ {":symbol", ArgInfo{Sexp::Type::Symbol, true,
+ "some boring symbol"}},
+ {":bool", ArgInfo{Sexp::Type::Symbol, true,
+ "some even more boring boolean symbol"}},
+ {":symbol2", ArgInfo{Sexp::Type::Symbol, false,
+ "some even more boring symbol"}},
+ {":bool2", ArgInfo{Sexp::Type::Symbol, false,
+ "some boring boolean symbol"}},
+ },
+ "get unread/totals information for a list of queries",
+ [&](const auto& params) {
+ const auto queries{params.string_vec_arg(":queries")
+ .value_or(std::vector<std::string>{})};
+ g_assert_cmpuint(queries.size(),==,3);
+ g_assert_true(params.bool_arg(":bool").value_or(false) == true);
+ assert_equal(params.symbol_arg(":symbol").value_or("boo"), "sym");
+
+ g_assert_false(!!params.bool_arg(":bool2"));
+ g_assert_false(!!params.bool_arg(":symbol2"));
+
+ }});
+
+ CommandHandler handler(std::move(ci_map));
+ const auto cmap{handler.info_map()};
+
+ assert_valid_result(call(cmap, "(my-command :param1 \"hello\")"));
+ assert_valid_result(call(cmap, "(my-command :param1 \"hello\" :param2 123)"));
+ g_assert_false(!!call(cmap, "(my-command :param1 \"hello\" :param2 123 :param3 xxx)"));
+ assert_valid_result(call(cmap, "(another-command :queries (\"foo\" \"bar\" \"cuux\") "
+ ":symbol sym :bool true)"));
+}
+
+static void
+test_command2()
+{
+ allow_warnings();
+
+ CommandInfoMap cmap;
+ cmap.emplace("bla",
+ CommandInfo{ArgMap{
+ {":foo", ArgInfo{Sexp::Type::Number, false, "foo"}},
+ {":bar", ArgInfo{Sexp::Type::String, false, "bar"}},
+ }, "yeah",
+ [&](const auto& params) {}});
+
+ g_assert_true(call(cmap, "(bla :foo nil)"));
+ g_assert_false(call(cmap, "(bla :foo nil :bla nil)"));
+}
+
+static void
+test_command_fail()
+{
+ allow_warnings();
+
+ CommandInfoMap cmap;
+
+ cmap.emplace(
+ "my-command",
+ CommandInfo{ArgMap{{":param1", ArgInfo{Sexp::Type::String, true, "some string"}},
+ {":param2", ArgInfo{Sexp::Type::Number, false, "some integer"}}},
+ "My command,",
+ {}});
+
+ g_assert_false(call(cmap, "(my-command)"));
+ g_assert_false(call(cmap, "(my-command2)"));
+ g_assert_false(call(cmap, "(my-command :param1 123 :param2 123)"));
+ g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 \"123\")"));
+
+ g_assert_false(call(cmap, "(my-command"));
+
+ g_assert_false(!!Command::make_parse(R"((foo :bar 123 :cuux "456" :boo nil :bah))"));
+}
+
+
+int
+main(int argc, char* argv[]) try {
+
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/utils/command-parser/args", test_args);
+ g_test_add_func("/utils/command-parser/command", test_command);
+ g_test_add_func("/utils/command-parser/command2", test_command2);
+ g_test_add_func("/utils/command-parser/command-fail", test_command_fail);
+
+ return g_test_run();
+
+} catch (const std::runtime_error& re) {
+ std::cerr << re.what() << "\n";
+ return 1;
+}
+
+#endif /*BUILD_TESTS*/
+// LCOV_EXCL_STOP
--- /dev/null
+/*
+** Copyright (C) 2020-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#ifndef MU_COMMAND_HANDLER_HH__
+#define MU_COMMAND_HANDLER_HH__
+
+#include <vector>
+#include <string>
+#include <ostream>
+#include <stdexcept>
+#include <unordered_map>
+#include <functional>
+#include <algorithm>
+
+#include "utils/mu-error.hh"
+#include "utils/mu-sexp.hh"
+#include "utils/mu-option.hh"
+
+namespace Mu {
+
+///
+/// Commands are s-expressions with the follow properties:
+
+/// 1) a command is a list with a command-name as its first argument
+/// 2) the rest of the parameters are pairs of colon-prefixed symbol and a value of some
+/// type (ie. 'keyword arguments')
+/// 3) each command is described by its CommandInfo structure, which defines the type
+/// 4) calls to the command must include all required parameters
+/// 5) all parameters must be of the specified type; however the symbol 'nil' is allowed
+/// for specify a non-required parameter to be absent; this is for convenience on the
+/// call side.
+
+struct Command: public Sexp {
+
+ static Result<Command> make(Sexp&& sexp) try {
+ return Ok(Command{std::move(sexp)});
+ } catch (const Error& e) {
+ return Err(e);
+ }
+
+ static Result<Command> make_parse(const std::string& cmdstr) try {
+ if (auto&& sexp{Sexp::parse(cmdstr)}; !sexp)
+ return Err(sexp.error());
+ else
+ return Ok(Command(std::move(*sexp)));
+ } catch (const Error& e) {
+ return Err(e);
+ }
+
+ /**
+ * Get name of the command (first element) in a command exp
+ *
+ * @return name
+ */
+ const std::string& name() const {
+ return cbegin()->symbol().name;
+ }
+
+ /**
+ * Find the argument with the given name.
+ *
+ * @param arg name
+ *
+ * @return iterator point at the argument, or cend
+ */
+ const_iterator find_arg(const std::string& arg) const {
+ return find_prop(arg, cbegin() + 1, cend());
+ }
+
+ /**
+ * Get a string argument
+ *
+ * @param name of the argument
+ *
+ * @return ref to string, or Nothing if not found
+ */
+ Option<const std::string&> string_arg(const std::string& name) const {
+ if (auto&& val{arg_val(name, Sexp::Type::String)}; !val)
+ return Nothing;
+ else
+ return val->string();
+ }
+
+ /**
+ * Get a string-vec argument
+ *
+ * @param name of the argument
+ *
+ * @return ref to string-vec, or Nothing if not found or some error.
+ */
+ Option<std::vector<std::string>> string_vec_arg(const std::string& name) const;
+
+ /**
+ * Get a symbol argument
+ *
+ * @param name of the argument
+ *
+ * @return ref to symbol name, or Nothing if not found
+ */
+ Option<const std::string&> symbol_arg(const std::string& name) const {
+ if (auto&& val{arg_val(name, Sexp::Type::Symbol)}; !val)
+ return Nothing;
+ else
+ return val->symbol().name;
+ }
+
+ /**
+ * Get a number argument
+ *
+ * @param name of the argument
+ *
+ * @return number or Nothing if not found
+ */
+ Option<int> number_arg(const std::string& name) const {
+ if (auto&& val{arg_val(name, Sexp::Type::Number)}; !val)
+ return Nothing;
+ else
+ return static_cast<int>(val->number());
+ }
+
+ /*
+ * helpers
+ */
+
+ /**
+ * Get a boolean argument
+ *
+ * @param name of the argument
+ *
+ * @return true if there's a non-nil symbol value for the given
+ * name; false otherwise.
+ */
+ Option<bool> bool_arg(const std::string& name) const {
+ if (auto&& symb{symbol_arg(name)}; !symb)
+ return Nothing;
+ else
+ return symb.value() == "nil" ? false : true;
+ }
+
+ /**
+ * Treat any argument as a boolean
+ *
+ * @param name name of the argument
+ *
+ * @return false if the the argument is absent or the symbol false;
+ * otherwise true.
+ */
+ bool boolean_arg(const std::string& name) const {
+ auto&& it{find_arg(name)};
+ return (it == cend() || std::next(it)->nilp()) ? false : true;
+ }
+
+private:
+ explicit Command(Sexp&& s){
+ *this = std::move(static_cast<Command&&>(s));
+ if (!listp() || empty() || !cbegin()->symbolp() ||
+ !plistp(cbegin() + 1, cend()))
+ throw Error(Error::Code::Command,
+ "expected command, got '{}'", to_string());
+ }
+
+
+ Option<const Sexp&> arg_val(const std::string& name, Sexp::Type type) const {
+ if (auto&& it{find_arg(name)}; it == cend()) {
+ //std::cerr << "--> %s name found " << name << '\n';
+ return Nothing;
+ } else if (auto&& val{it + 1}; val->type() != type) {
+ //std::cerr << "--> type " << Sexp::type_name(it->type()) << '\n';
+ return Nothing;
+ } else
+ return *val;
+ }
+};
+
+struct CommandHandler {
+
+ /// Information about a function argument
+ struct ArgInfo {
+ ArgInfo(Sexp::Type typearg, bool requiredarg, std::string&& docarg)
+ : type{typearg}, required{requiredarg}, docstring{std::move(docarg)} {}
+ const Sexp::Type type; /**< Sexp::Type of the argument */
+ const bool required; /**< Is this argument required? */
+ const std::string docstring; /**< Documentation */
+ };
+
+ /// The arguments for a function, which maps their names to the information.
+ using ArgMap = std::unordered_map<std::string, ArgInfo>;
+
+ // A handler function
+ using Handler = std::function<void(const Command&)>;
+
+ /// Information about some command
+ struct CommandInfo {
+ CommandInfo(ArgMap&& argmaparg, std::string&& docarg, Handler&& handlerarg)
+ : args{std::move(argmaparg)}, docstring{std::move(docarg)},
+ handler{std::move(handlerarg)} {}
+ const ArgMap args;
+ const std::string docstring;
+ const Handler handler;
+
+ /**
+ * Get a sorted list of argument names, for display. Required args come
+ * first, then alphabetical.
+ *
+ * @return vec with the sorted names.
+ */ /* LCOV_EXCL_START */
+ std::vector<std::string> sorted_argnames() const {
+ // sort args -- by required, then alphabetical.
+ std::vector<std::string> names;
+ for (auto&& arg : args)
+ names.emplace_back(arg.first);
+ std::sort(names.begin(), names.end(), [&](const auto& name1, const auto& name2) {
+ const auto& arg1{args.find(name1)->second};
+ const auto& arg2{args.find(name2)->second};
+ if (arg1.required != arg2.required)
+ return arg1.required;
+ else
+ return name1 < name2;
+ });
+ return names;
+ }
+ /* LCOV_EXCL_STOP */
+
+ };
+
+ /// All commands, mapping their name to information about them.
+ using CommandInfoMap = std::unordered_map<std::string, CommandInfo>;
+
+ CommandHandler(const CommandInfoMap& cmap): cmap_{cmap} {}
+ CommandHandler(CommandInfoMap&& cmap): cmap_{std::move(cmap)} {}
+
+ const CommandInfoMap& info_map() const { return cmap_; }
+
+ /**
+ * Invoke some command
+ *
+ * A command uses keyword arguments, e.g. something like: (foo :bar 1
+ * :cuux "fnorb")
+ *
+ * @param cmd a Sexp describing a command call
+ * @param validate whether to validate before invoking. Useful during
+ * development.
+ *
+ * Return Ok() or some Error
+ */
+ Result<void> invoke(const Command& cmd, bool validate=true) const;
+
+private:
+ const CommandInfoMap cmap_;
+};
+
+/* LCOV_EXCL_START */
+static inline std::ostream&
+operator<<(std::ostream& os, const CommandHandler::ArgInfo& info)
+{
+ os << info.type << " (" << (info.required ? "required" : "optional") << ")";
+
+ return os;
+}
+/* LCOV_EXCL_STOP */
+
+static inline std::ostream&
+operator<<(std::ostream& os, const CommandHandler::CommandInfo& info)
+{
+ for (auto&& arg : info.args)
+ os << " " << arg.first << " " << arg.second << '\n'
+ << " " << arg.second.docstring << "\n";
+
+ return os;
+}
+
+static inline std::ostream&
+operator<<(std::ostream& os, const CommandHandler::CommandInfoMap& map)
+{
+ for (auto&& c : map)
+ os << c.first << '\n' << c.second;
+
+ return os;
+}
+
+} // namespace Mu
+
+#endif /* MU_COMMAND_HANDLER_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+
+#if BUILD_TESTS
+
+#include "mu-error.hh"
+#include "mu-test-utils.hh"
+
+using namespace Mu;
+
+static void
+test_fill_error()
+{
+ const Error err{Error::Code::Internal, "boo!"};
+ GError *gerr{};
+
+ err.fill_g_error(&gerr);
+
+ assert_equal(gerr->message, "boo!");
+ g_assert_cmpint(gerr->code, ==, static_cast<int>(err.code()));
+
+ g_clear_error(&gerr);
+}
+
+static void
+test_add_hint()
+{
+ Error err(Error::Code::Internal, "baa!");
+ err.add_hint("hello");
+
+ assert_equal(err.hint(), "hello");
+}
+
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/error/fill-error", test_fill_error);
+ g_test_add_func("/error/add-hint", test_add_hint);
+
+ return g_test_run();
+
+}
+
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2019-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_ERROR_HH__
+#define MU_ERROR_HH__
+
+#include <stdexcept>
+#include <string>
+#include <errno.h>
+#include <cstdint>
+
+#include "mu-utils.hh"
+#include <glib.h>
+
+#ifndef FMT_HEADER_ONLY
+#define FMT_HEADER_ONLY
+#endif
+
+#include <fmt/format.h>
+#include <fmt/core.h>
+
+namespace Mu {
+
+// calculate an error enum value.
+constexpr uint32_t err_enum(uint8_t code, uint8_t rv, uint8_t cat) {
+ return static_cast<uint32_t>(code|(rv << 16)|cat<<24);
+}
+
+struct Error final : public std::exception {
+
+ // 16 lower bits are for the error code;the next 8 bits are for the return code; the upper
+ // byte is for flags
+ static constexpr uint8_t SoftError = 1;
+
+ enum struct Code: uint32_t {
+ Ok = err_enum(0,0,0),
+
+ // used by mu4e.
+ NoMatches = err_enum(4,2,SoftError),
+ SchemaMismatch = err_enum(110,11,0),
+
+ // other
+ AccessDenied = err_enum(100,1,0),
+ AssertionFailure = err_enum(101,1,0),
+ Command = err_enum(102,1,0),
+ Crypto = err_enum(103,1,0),
+ File = err_enum(104,1,0),
+ Index = err_enum(105,1,0),
+ Internal = err_enum(106,1,0),
+ InvalidArgument = err_enum(107,1,0),
+ Message = err_enum(108,1,0),
+ NotFound = err_enum(109,1,0),
+ Parsing = err_enum(111,1,0),
+ Play = err_enum(112,1,0),
+ Query = err_enum(113,1,0),
+ Script = err_enum(115,1,0),
+ ScriptNotFound = err_enum(116,1,0),
+ Store = err_enum(117,1,0),
+ StoreLock = err_enum(118,19,0),
+ UnverifiedSignature = err_enum(119,1,0),
+ User = err_enum(120,1,0),
+ Xapian = err_enum(121,1,0),
+
+ CannotReinit = err_enum(122,1,0),
+ };
+
+ /**
+ * Construct an error
+ *
+ * @param code the error-code
+ * @param args... libfmt-style format string and parameters
+ */
+ template<typename...T>
+ Error(Code code, fmt::format_string<T...> frm, T&&... args):
+ code_{code},
+ what_{fmt::format(frm, std::forward<T>(args)...)} {}
+
+ /**
+ * Construct an error
+ *
+ * @param code the error-code
+ * @param gerr a GError (or {}); the error is _consumed_ by this function
+ * @param args... libfmt-style format string and parameters
+ */
+ template<typename...T>
+ Error(Code code, GError **gerr, fmt::format_string<T...> frm, T&&... args):
+ code_{code},
+ what_{fmt::format(frm, std::forward<T>(args)...) +
+ fmt::format(": {}", (gerr && *gerr) ? (*gerr)->message :
+ "something went wrong")}
+ { g_clear_error(gerr); }
+
+ /**
+ * Get the descriptive message for this error.
+ *
+ * @return
+ */
+ virtual const char* what() const noexcept override { return what_.c_str(); }
+
+ /**
+ * Get the error-code for this error
+ *
+ * @return the error-code
+ */
+ Code code() const noexcept { return code_; }
+
+ /**
+ * Get the error number (e.g. for reporting to mu4e) for some error.
+ *
+ * @param c error code
+ *
+ * @return the error number
+ */
+ static constexpr uint32_t error_number(Code c) noexcept {
+ return static_cast<uint32_t>(c) & 0xffff;
+ }
+
+ /**
+ * Is this is a 'soft error'?
+ *
+ * @return true or false
+ */
+ constexpr bool is_soft_error() const {
+ return !!((static_cast<uint32_t>(code_)>>24) & SoftError);
+ }
+
+ constexpr uint8_t exit_code() const {
+ return ((static_cast<uint32_t>(code_) >> 16) & 0xff);
+ }
+
+ /**
+ * Fill a GError with the error information
+ *
+ * @param err GError** (or NULL)
+ */
+ void fill_g_error(GError **err) const noexcept{
+ g_set_error(err, error_quark(), static_cast<int>(code_),
+ "%s", what_.c_str());
+ }
+
+ /**
+ * Add an end-user hint
+ *
+ * @param args... libfmt-style format string and parameters
+ *
+ * @return the error
+ */
+ template<typename...T>
+ Error& add_hint(fmt::format_string<T...> frm, T&&... args) {
+ hint_ = fmt::format(frm, std::forward<T>(args)...);
+ return *this;
+ }
+
+ /**
+ * Get the hint
+ *
+ * @return the hint, empty for no hint.
+ */
+ const std::string& hint() const { return hint_; }
+
+private:
+ static inline GQuark error_quark (void) {
+ static GQuark error_domain = 0;
+ if (G_UNLIKELY(error_domain == 0))
+ error_domain = g_quark_from_static_string("mu-error-quark");
+ return error_domain;
+ }
+
+ const Code code_;
+ const std::string what_;
+ std::string hint_;
+};
+
+static inline auto
+format_as(const Error& err) {
+ return mu_format("<{} ({}:{})>",
+ err.what(),
+ Error::error_number(err.code()),
+ err.exit_code());
+}
+
+} // namespace Mu
+
+#endif /* MU_ERROR_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-utils.hh"
+#include "mu-option.hh"
+#include "mu-regex.hh"
+
+#include <string>
+#include <array>
+#include <string_view>
+#include <algorithm>
+
+using namespace Mu;
+
+
+static bool
+starts_with(std::string_view haystack, std::string_view needle)
+{
+ if (needle.size() > haystack.size())
+ return false;
+
+ for (auto&& c = 0U; c != needle.size(); ++c)
+ if (::tolower(haystack[c]) != ::tolower(needle[c]))
+ return false;
+
+ return true;
+}
+
+static bool
+matches(std::string_view haystack, std::string_view needle)
+{
+ if (needle.size() != haystack.size())
+ return false;
+ else
+ return starts_with(haystack, needle);
+}
+
+
+
+/**
+ * HTML parsing context
+ *
+ */
+class Context {
+public:
+ /**
+ * Construct a parsing context
+ *
+ * @param html some html to parse
+ */
+ Context(const std::string& html): html_{html}, pos_{} {}
+
+ /**
+ * Are we done with the html blob, i.e, has it been fully scraped?
+ *
+ * @return true or false
+ */
+ bool done() const {
+ return pos_ >= html_.size();
+ }
+
+ /**
+ * Get the current position
+ *
+ * @return position
+ */
+ size_t position() const {
+ return pos_;
+ }
+
+ /**
+ * Get the size of the HTML
+ *
+ * @return size
+ */
+ size_t size() const {
+ return html_.size();
+ }
+
+ /**
+ * Advance the position by _n_ characters.
+ *
+ * @param n number by which to advance.
+ */
+ void advance(size_t n=1) {
+ if (pos_ + n > html_.size())
+ throw std::range_error("out of range");
+ pos_ += n;
+ }
+
+ /**
+ * Are we looking at the given string?
+ *
+ * @param str string to match (case-insensitive)
+ *
+ * @return true or false
+ */
+ bool looking_at(std::string_view str) const {
+ if (pos_ >= html_.size() || pos_ + str.size() >= html_.size())
+ return false;
+ else
+ return matches({html_.data()+pos_, str.size()}, str);
+ }
+
+ /**
+ * Grab a substring-view from the html
+ *
+ * @param fpos starting position
+ * @param len length
+ *
+ * @return string view
+ */
+ std::string_view substr(size_t fpos, size_t len) const {
+ if (fpos + len > html_.size())
+ throw std::range_error(mu_format("{} + {} > {}",
+ fpos, len, html_.size()));
+ else
+ return { html_.data() + fpos, len };
+ }
+
+ /**
+ * Grab the string of alphabetic characters at the
+ * head (pos) of the context, and advance over it.
+ *
+ * @return the head-word or empty
+ */
+ std::string_view eat_head_word() {
+ size_t start_pos{pos_};
+ while (!done()) {
+ if (!::isalpha(html_.at(pos_)))
+ break;
+ ++pos_;
+ }
+ return {html_.data() + start_pos, pos_ - start_pos};
+ }
+
+
+ /**
+ * Get the scraped data; only available when done()
+
+ * @return scraped data
+ */
+ std::string scraped() {
+ return cleanup(raw_scraped_);
+ }
+
+ /**
+ * Get the raw scrape buffer, where we can append
+ * scraped data.
+ *
+ * @return the buffer
+ */
+ std::string& raw_scraped() {
+ return raw_scraped_;
+ }
+
+
+ /**
+ * Get a reference to the HTML
+ *
+ * @return html
+ */
+ const std::string& html() const { return html_; }
+
+private:
+
+ /**
+ * Cleanup some raw scraped html: remove superfluous
+ * whitespace, avoid too long lines.
+ *
+ * @param unclean
+ *
+ * @return cleaned up string.
+ */
+ std::string cleanup(const std::string unclean) const {
+ // reduce whitespace and avoid too long lines;
+ // makes it easier to debug.
+ bool was_wspace{};
+ size_t col{};
+ std::string clean;
+ clean.reserve(unclean.size()/2);
+ for(auto&& c: unclean) {
+ auto wspace = c == ' ' || c == '\t' || c == '\n';
+ if (wspace) {
+ was_wspace = true;
+ continue;
+ }
+ ++col;
+ if (was_wspace) {
+ if (col > 80) {
+ clean += '\n';
+ col = 0;
+ } else if (!clean.empty())
+ clean += ' ';
+ was_wspace = false;
+ }
+ clean += c;
+ }
+ return clean;
+ }
+
+
+ const std::string& html_; // no copy!
+ size_t pos_{};
+ std::string raw_scraped_;
+};
+
+
+G_GNUC_UNUSED static auto
+format_as(const Context& ctx)
+{
+ return mu_format("<{}:{}: '{}'>",
+ ctx.position(), ctx.size(),
+ ctx.substr(ctx.position(),
+ std::min(static_cast<size_t>(8),
+ ctx.size() - ctx.position())));
+}
+
+
+static void
+skip_quoted(Context& ctx, std::string_view quote)
+{
+ while(!ctx.done()) {
+ if (ctx.looking_at(quote)) // closing quote
+ return;
+ ctx.advance();
+ }
+}
+
+
+// attempt to skip over <script> / <style> blocks
+static void
+skip_script_style(Context& ctx, std::string_view tag)
+{
+ // <script> or <style> must be ignored
+
+ bool escaped{};
+ bool quoted{}, squoted{};
+ bool inl_comment{};
+ bool endl_comment{};
+
+ auto end_tag_str = mu_format("</{}>", tag);
+ auto end_tag = std::string_view(end_tag_str.data());
+
+ while (!ctx.done()) {
+
+ if (inl_comment) {
+ if (ctx.looking_at("*/")) {
+ inl_comment = false;
+ ctx.advance(2);
+ } else
+ ctx.advance();
+ continue;
+ }
+
+ if (endl_comment) {
+ endl_comment = ctx.looking_at("\n");
+ ctx.advance();
+ continue;
+ }
+
+ if (ctx.looking_at("\\")) {
+ escaped = !escaped;
+ ctx.advance();
+ continue;
+ }
+
+ if (ctx.looking_at("\"") && !escaped && squoted) {
+ quoted = !quoted;
+ ctx.advance();
+ continue;
+ }
+
+ if (ctx.looking_at("'") && !escaped && !quoted) {
+ squoted = !squoted;
+ ctx.advance();
+ continue;
+ }
+
+
+ if (ctx.looking_at("/*")) {
+ inl_comment = true;
+ ctx.advance(2);
+ continue;
+ }
+
+ if (ctx.looking_at("//")) {
+ endl_comment = true;
+ ctx.advance(2);
+ continue;
+ }
+
+ if (!quoted && !squoted && ctx.looking_at(end_tag)) {
+ ctx.advance(end_tag.size());
+ break; /* we're done, finally! */
+ }
+
+ ctx.advance();
+ }
+}
+
+// comment block; ignore completely
+// pos will be immediately after the '<!--
+static void
+comment(Context& ctx)
+{
+ constexpr std::string_view comment_endtag{"-->"};
+ while (!ctx.done()) {
+
+ if (ctx.looking_at(comment_endtag)) {
+ ctx.advance(comment_endtag.size());
+ ctx.raw_scraped() += ' ';
+ return;
+ }
+ ctx.advance();
+ }
+}
+
+static bool // do we need a SPC separator for this tag?
+needs_separator(std::string_view tagname)
+{
+ constexpr std::array<const char*, 7> nosep_tags = {
+ "b", "em", "i", "s", "strike", "tt", "u"
+ };
+ return !seq_some(nosep_tags, [&](auto&& t){return matches(tagname, t);});
+}
+
+static bool // do we need to skip the element completely?
+is_skip_element(std::string_view tagname)
+{
+ constexpr std::array<const char*, 4> skip_tags = {
+ "script", "style", "head", "meta"
+ };
+ return seq_some(skip_tags, [&](auto&& t){return matches(tagname, t);});
+}
+
+// skip the end-tag
+static void
+end_tag(Context& ctx)
+{
+ while (!ctx.done()) {
+ if (ctx.looking_at(">")) {
+ ctx.advance();
+ return;
+ }
+ ctx.advance();
+ }
+}
+
+// skip the whole element
+static void
+skip_element(Context& ctx, std::string_view tagname)
+{
+ // do something special?
+}
+
+
+// the start of a tag, i.e., pos will be just after the '<'
+static void
+tag(Context& ctx)
+{
+ // some elements we want to skip completely,
+ // for others just the tags.
+ constexpr std::string_view comment_start {"!--"};
+ if (ctx.looking_at(comment_start)) {
+ ctx.advance(comment_start.size());
+ comment(ctx);
+ return;
+ }
+
+ if (ctx.looking_at("/")) {
+ ctx.advance();
+ end_tag(ctx);
+ return;
+ }
+
+ auto tagname = ctx.eat_head_word();
+ if (tagname == "script" ||tagname == "style") {
+ skip_script_style(ctx, tagname);
+ return;
+ }
+ else if (is_skip_element(tagname))
+ skip_element(ctx, tagname);
+
+ const auto needs_sepa = needs_separator(tagname);
+ while (!ctx.done()) {
+
+ if (ctx.looking_at("\""))
+ skip_quoted(ctx, "\"");
+
+ if (ctx.looking_at("'"))
+ skip_quoted(ctx, "'");
+
+ if (ctx.looking_at(">")) {
+ ctx.advance();
+ if (needs_sepa)
+ ctx.raw_scraped() += ' ';
+ return;
+ }
+ ctx.advance();
+ }
+}
+
+
+static void
+html_escape_char(Context& ctx)
+{
+ // we only care about a few accented chars, and add them unaccented, lowercase, since that's
+ // we do for indexing anyway.
+ constexpr std::array<const char*, 11> escs = {
+ "breve",
+ "caron",
+ "circ",
+ "cute",
+ "grave",
+ "horn"/*thorn*/,
+ "macr",
+ "slash",
+ "strok",
+ "tilde",
+ "uml",
+ };
+
+ auto unescape=[escs](std::string_view esc)->char {
+ if (esc.empty())
+ return ' ';
+ auto first{static_cast<char>(::tolower(esc.at(0)))};
+ auto rest=esc.substr(1);
+ if (seq_some(escs, [&](auto&& e){return starts_with(rest, e);}))
+ return first;
+ else
+ return ' ';
+ };
+
+ size_t start_pos{ctx.position()};
+ while (!ctx.done()) {
+ if (ctx.looking_at(";")) {
+ auto esc = ctx.substr(start_pos, ctx.position() - start_pos);
+ ctx.raw_scraped() += unescape(esc);
+ ctx.advance();
+ return;
+ }
+ ctx.advance();
+ }
+}
+
+
+// a block of text to be scraped
+static void
+text(Context& ctx)
+{
+ size_t start_pos{ctx.position()};
+ while (!ctx.done()) {
+
+ if (ctx.looking_at("&")) {
+
+ ctx.raw_scraped() += ctx.substr(start_pos,
+ ctx.position() - start_pos);
+ ctx.advance();
+ html_escape_char(ctx);
+ start_pos = ctx.position();
+
+ } else if (ctx.looking_at("<")) {
+ ctx.raw_scraped() += ctx.substr(start_pos,
+ ctx.position() - start_pos);
+ ctx.advance();
+ tag(ctx);
+ start_pos = ctx.position();
+
+ } else
+ ctx.advance();
+ }
+
+ ctx.raw_scraped() += ctx.substr(start_pos, ctx.size() - start_pos);
+}
+
+static Context *CTX{};
+
+std::string
+Mu::html_to_text(const std::string& html)
+{
+ Context ctx{html};
+ CTX = &ctx;
+
+ text(ctx);
+
+ CTX = {};
+ return ctx.scraped();
+}
+
+#ifdef BUILD_TESTS
+#include "mu-test-utils.hh"
+
+static void
+test_1()
+{
+ static std::vector<std::pair<std::string, std::string>>
+ tests = {
+ { "<!-- Hello -->A", "A" },
+ { "A<!-- Test -->B", "A B" },
+ { "A<i>a</i><b>p</b>", "Aap"},
+ { "N&ocute;Ôt", "Noot"},
+ {
+ "foo<!-- bar --><i>c</i>uu<bla>x</bla>"
+ "<!--hello -->world<!--",
+ "foo cuu x world"
+ }
+ };
+
+ for (auto&& test: tests)
+ assert_equal(html_to_text(test.first), test.second);
+}
+
+static void
+test_2()
+{
+ static std::vector<std::pair<std::string, std::string>>
+ tests = {
+ { R"(<i>hello, <b bar="/b">world!</b>)",
+ "hello, world!"},
+ };
+
+ for (auto&& test: tests)
+ assert_equal(html_to_text(test.first), test.second);
+}
+
+
+static void
+test_3()
+{
+ static std::vector<std::pair<std::string, std::string>>
+ tests = {
+ {R"(<i>hello, </i><script language="javascript">
+ function foo() {
+ alert("Stroopwafel!"); // test
+ }
+ </script>world!)",
+ "hello, world!"},
+ };
+
+ for (auto&& test: tests)
+ assert_equal(html_to_text(test.first), test.second);
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/html-to-text/test-1", test_1);
+ g_test_add_func("/html-to-text/test-2", test_2);
+ g_test_add_func("/html-to-text/test-3", test_3);
+
+ return g_test_run();
+}
+
+
+#endif /*BUILD_TESTS*/
+
+
+#ifdef BUILD_HTML_TO_TEXT
+
+#include "mu-utils-file.hh"
+
+// simple tool that reads html on stdin and outputs text on stdout
+// e.g. curl --silent https://www.example.com | build/lib/utils/mu-html2text
+
+int
+main (int argc, char *argv[])
+{
+ auto res = read_from_stdin();
+ if (!res) {
+ mu_printerrln("error reading from stdin: {}", res.error().what());
+ return 1;
+ }
+
+ mu_println("{}", html_to_text(*res));
+
+ return 0;
+}
+
+#endif /*BUILD_HTML_TO_TEXT*/
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include "config.h"
+#include "mu-lang-detector.hh"
+
+using namespace Mu;
+
+#ifndef HAVE_CLD2
+// Dummy implementation
+Option<Language> Mu::detect_language(const std::string& txt) { return Nothing; }
+#else
+#include <cld2/public/compact_lang_det.h>
+#include <cld2/public/encodings.h>
+
+Option<Language>
+Mu::detect_language(const std::string& txt)
+{
+ bool is_reliable;
+ const auto lang = CLD2::DetectLanguage(
+ txt.c_str(), txt.length(),
+ true/*plain-text*/,
+ &is_reliable);
+
+ if (lang == CLD2::UNKNOWN_LANGUAGE || !is_reliable)
+ return {};
+
+ Mu::Language res = {
+ CLD2::LanguageName(lang),
+ CLD2::LanguageCode(lang)
+ };
+ if (!res.name || !res.code)
+ return {};
+ else
+ return Some(std::move(res));
+}
+#endif /*HAVE_CLD2*/
+
+#ifdef BUILD_TESTS
+#include <vector>
+#include "mu-test-utils.hh"
+
+static void
+test_lang_detector()
+{
+ using Case = std::tuple<std::string,std::string, std::string>;
+ using Cases = std::vector<Case>;
+
+ const Cases tests = {{
+ { "hello world, this is a bit of English",
+ "ENGLISH", "en" },
+ { "En nu een paar Nederlandse woorden",
+ "DUTCH", "nl" },
+ { "Hyvää huomenta! Puhun vähän suomea",
+ "FINNISH", "fi" },
+ { "So eine Arbeit wird eigentlich nie fertig, man muß sie für "
+ "fertig erklären, wenn man nach Zeit und Umständen das "
+ "möglichste getan hat.",
+ "GERMAN", "de"}
+ }};
+
+ for (auto&& test: tests) {
+ const auto res = detect_language(std::get<0>(test));
+#ifndef HAVE_CLD2
+ g_assert_false(!!res);
+#else
+ g_assert_true(!!res);
+ assert_equal(std::get<1>(test), res->name);
+ assert_equal(std::get<2>(test), res->code);
+#endif
+
+ }
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/utils/lang-detector", test_lang_detector);
+
+ return g_test_run();
+}
+
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_LANG_DETECTOR_HH__
+#define MU_LANG_DETECTOR_HH__
+
+#include <string>
+#include "mu-option.hh"
+
+namespace Mu {
+
+struct Language {
+ const char *name; /**< Language name, e.g. "Dutch" */
+ const char *code; /**< Language code, e.g. "nl" */
+};
+
+/**
+ * Detect the language of text
+ *
+ * @param txt some text (UTF-8)
+ *
+ * @return either a Language or nothing; the latter
+ * also if we cannot not reliably determine a single language
+ */
+Option<Language> detect_language(const std::string& txt);
+
+} // namespace Mu
+
+
+#endif /* MU_LANG_DETECTOR_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2020-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#define G_LOG_USE_STRUCTURED
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <iostream>
+#include <fstream>
+#include <cstring>
+
+#include <thread>
+#include <mutex>
+
+#include "mu-logger.hh"
+
+using namespace Mu;
+
+static bool MuLogInitialized = false;
+static Mu::Logger::Options MuLogOptions;
+static std::ofstream MuStream;
+static auto MaxLogFileSize = 1000 * 1024;
+static std::mutex logger_mtx;
+
+static std::string MuLogPath;
+
+static bool
+maybe_open_logfile()
+{
+ if (MuStream.is_open())
+ return true;
+
+ const auto logdir{to_string_gchar(g_path_get_dirname(MuLogPath.c_str()))};
+ if (g_mkdir_with_parents(logdir.c_str(), 0700) != 0) {
+ mu_printerrln("creating {} failed: {}", logdir, g_strerror(errno));
+ return false;
+ }
+
+ MuStream.open(MuLogPath, std::ios::out | std::ios::app);
+ if (!MuStream.is_open()) {
+ mu_printerrln("opening {} failed: {}", MuLogPath, g_strerror(errno));
+ return false;
+ }
+
+ MuStream.sync_with_stdio(false);
+ return true;
+}
+
+static bool
+maybe_rotate_logfile()
+{
+ static unsigned n = 0;
+
+ if (n++ % 1000 != 0)
+ return true;
+
+ GStatBuf statbuf;
+ if (g_stat(MuLogPath.c_str(), &statbuf) == -1 || statbuf.st_size <= MaxLogFileSize)
+ return true;
+
+ const auto old = MuLogPath + ".old";
+ g_unlink(old.c_str()); // opportunistic
+
+ if (MuStream.is_open())
+ MuStream.close();
+
+ if (g_rename(MuLogPath.c_str(), old.c_str()) != 0)
+ mu_printerrln("failed to rename {} -> {}: {}", MuLogPath, old, g_strerror(errno));
+
+ return maybe_open_logfile();
+}
+
+static GLogWriterOutput
+log_file(GLogLevelFlags level, const GLogField* fields, gsize n_fields, gpointer user_data)
+{
+ std::lock_guard lock{logger_mtx};
+
+ if (!maybe_open_logfile())
+ return G_LOG_WRITER_UNHANDLED;
+
+ char timebuf[22];
+ time_t now{::time(NULL)};
+ ::strftime(timebuf, sizeof(timebuf), "%F %T", ::localtime(&now));
+
+ char* msg = g_log_writer_format_fields(level, fields, n_fields, FALSE);
+ if (msg && msg[0] == '\n') // hmm... seems lines start with '\n'r
+ msg[0] = ' ';
+
+ MuStream << timebuf << ' ' << msg << std::endl;
+
+ g_free(msg);
+
+ return maybe_rotate_logfile() ? G_LOG_WRITER_HANDLED : G_LOG_WRITER_UNHANDLED;
+}
+
+static GLogWriterOutput
+log_stdouterr(GLogLevelFlags level, const GLogField* fields, gsize n_fields, gpointer user_data)
+{
+ return g_log_writer_standard_streams(level, fields, n_fields, user_data);
+}
+
+static GLogWriterOutput
+log_journal(GLogLevelFlags level, const GLogField* fields, gsize n_fields, gpointer user_data)
+{
+ return g_log_writer_journald(level, fields, n_fields, user_data);
+}
+
+
+Result<Logger>
+Mu::Logger::make(const std::string& path, Mu::Logger::Options opts)
+{
+ if (MuLogInitialized)
+ return Err(Error::Code::Internal, "logging already initialized");
+
+ return Ok(Logger(path, opts));
+}
+
+Mu::Logger::Logger(const std::string& path, Mu::Logger::Options opts)
+{
+ if (g_getenv("MU_LOG_STDOUTERR"))
+ opts |= Logger::Options::StdOutErr;
+
+ MuLogOptions = opts;
+ MuLogPath = path;
+
+ g_log_set_writer_func(
+ [](GLogLevelFlags level, const GLogField* fields, gsize n_fields, gpointer user_data) {
+ // filter out debug-level messages?
+ if (level == G_LOG_LEVEL_DEBUG &&
+ (none_of(MuLogOptions & Options::Debug)))
+ return G_LOG_WRITER_HANDLED;
+
+ // log criticals to stdout / err or if asked
+ if (level == G_LOG_LEVEL_CRITICAL ||
+ any_of(MuLogOptions & Options::StdOutErr)) {
+ log_stdouterr(level, fields, n_fields, user_data);
+ }
+
+ // log to the journal, or, if not available to a file.
+ if (any_of(MuLogOptions & Options::File) ||
+ log_journal(level, fields, n_fields, user_data) != G_LOG_WRITER_HANDLED)
+ return log_file(level, fields, n_fields, user_data);
+ else
+ return G_LOG_WRITER_HANDLED;
+ },
+ NULL,
+ NULL);
+
+ g_message("logging initialized; debug: %s, stdout/stderr: %s",
+ any_of(opts & Options::Debug) ? "yes" : "no",
+ any_of(opts & Options::StdOutErr) ? "yes" : "no");
+
+ MuLogInitialized = true;
+}
+
+Logger::~Logger()
+{
+ if (!MuLogInitialized)
+ return;
+
+ if (MuStream.is_open())
+ MuStream.close();
+
+ MuLogInitialized = false;
+}
+
+
+#ifdef BUILD_TESTS
+#include <vector>
+#include <atomic>
+
+#include "mu-test-utils.hh"
+#include "mu-utils-file.hh"
+
+static void
+test_logger_threads(void)
+{
+ TempDir temp_dir;
+ const auto testpath{join_paths(temp_dir.path(), "test.log")};
+ mu_message("log-file: {}", testpath);
+
+ auto logger = Logger::make(testpath, Logger::Options::File | Logger::Options::Debug);
+ assert_valid_result(logger);
+
+ const auto thread_num = 16;
+ std::atomic<bool> running = true;
+
+ std::vector<std::thread> threads;
+
+ /* log to the logger file from many threass */
+ for (auto n = 0; n != thread_num; ++n)
+ threads.emplace_back(
+ std::thread([&running]{
+ while (running) {
+ //mu_debug("log message from thread <{}>", n);
+ std::this_thread::yield();
+ }
+ }));
+
+ using namespace std::chrono_literals;
+ std::this_thread::sleep_for(1s);
+ running = false;
+
+ for (auto n = 0; n != 16; ++n)
+ if (threads[n].joinable())
+ threads[n].join();
+}
+
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/utils/logger", test_logger_threads);
+
+ return g_test_run();
+}
+
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2020-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_LOGGER_HH__
+#define MU_LOGGER_HH__
+
+#include <string>
+#include <utils/mu-utils.hh>
+#include <utils/mu-result.hh>
+
+namespace Mu {
+
+/**
+ * RAII object for handling logging (through g_(debug|warning|...))
+ *
+ */
+struct Logger {
+
+ /**
+ * Logging options
+ *
+ */
+ enum struct Options {
+ None = 0, /**< Nothing specific */
+ StdOutErr = 1 << 1, /**< Log to stdout/stderr */
+ File = 1 << 2, /**< Force logging to file, even if journal available */
+ Debug = 1 << 3, /**< Include debug-level logs */
+ };
+
+ /**
+ * Initialize the logging sub-system.
+ *
+ * Note that the path is only used if structured logging fails --
+ * practically, it goes to the file if there's no systemd/journald.
+ *
+ * if the environment variable MU_LOG_STDOUTERR is set,
+ * LogOptions::StdoutErr is implied.
+ *
+ * @param path path to the log file
+ * @param opts logging options
+ */
+ static Result<Logger> make(const std::string& path, Options opts=Options::None);
+
+ /**
+ * DTOR
+ *
+ */
+ ~Logger();
+
+private:
+ Logger(const std::string& path, Options opts);
+};
+
+MU_ENABLE_BITOPS(Logger::Options);
+
+} // namespace Mu
+
+#endif /* MU_LOGGER_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-option.hh"
+#include <glib.h>
+
+using namespace Mu;
+
+Mu::Option<std::string>
+Mu::to_string_opt_gchar(gchar*&& str)
+{
+ auto res = to_string_opt(str);
+ g_free(str);
+
+ return res;
+}
+
+#if BUILD_TESTS
+#include "mu-test-utils.hh"
+
+static Option<int>
+get_opt_int(bool b)
+{
+ if (b)
+ return Some(123);
+ else
+ return Nothing;
+}
+
+static void
+test_option()
+{
+ {
+ const auto oi{get_opt_int(true)};
+ g_assert_true(!!oi);
+ g_assert_cmpint(oi.value(), ==, 123);
+ }
+
+ {
+ const auto oi{get_opt_int(false)};
+ g_assert_false(!!oi);
+ g_assert_false(oi.has_value());
+ g_assert_cmpint(oi.value_or(456), ==, 456);
+ }
+}
+
+static void
+test_unwrap()
+{
+ {
+ auto&& oi{get_opt_int(true)};
+ g_assert_cmpint(unwrap(std::move(oi)), ==, 123);
+ }
+
+ auto ex{0};
+ try {
+ auto&& oi{get_opt_int(false)};
+ unwrap(std::move(oi));
+ } catch(...) {
+ ex = 1;
+ }
+
+ g_assert_cmpuint(ex, ==, 1);
+}
+
+static void
+test_opt_gchar()
+{
+ auto o1{to_string_opt_gchar(g_strdup("boo!"))};
+ auto o2{to_string_opt_gchar(nullptr)};
+
+ g_assert_false(!!o2);
+ g_assert_true(o1.value() == "boo!");
+}
+
+
+
+int
+main(int argc, char* argv[])
+{
+ g_test_init(&argc, &argv, NULL);
+
+ g_test_add_func("/option/option", test_option);
+ g_test_add_func("/option/unwrap", test_unwrap);
+ g_test_add_func("/option/opt-gchar", test_opt_gchar);
+
+ return g_test_run();
+}
+
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_OPTION__
+#define MU_OPTION__
+
+#include <tl/optional.hpp>
+#include <stdexcept>
+#include <string>
+
+namespace Mu {
+
+/// Either a value of type T, or None
+template <typename T> using Option = tl::optional<T>;
+
+template <typename T>
+Option<T>
+Some(T&& t)
+{
+ return std::move(t);
+}
+constexpr auto Nothing = tl::nullopt; // 'None' is already taken.
+
+template<typename T> T
+unwrap(Option<T>&& res)
+{
+ if (!!res)
+ return std::move(res.value());
+ else
+ throw std::runtime_error("failure is not an option");
+}
+
+
+/**
+ * Maybe create a string from a const char pointer.
+ *
+ * @param str a char pointer or NULL
+ *
+ * @return option with either the string or nothing if str was NULL.
+ */
+Option<std::string>
+static inline to_string_opt(const char* str) {
+ if (str)
+ return std::string{str};
+ else
+ return Nothing;
+}
+
+/**
+ * Like maybe_string that takes a const char*, but additionally,
+ * g_free() the string.
+ *
+ * @param str char pointer or NULL (consumed)
+ *
+ * @return option with either the string or nothing if str was NULL.
+ */
+Option<std::string> to_string_opt_gchar(char*&& str);
+
+
+} // namespace Mu
+#endif /*MU_OPTION__*/
--- /dev/null
+/*
+** Copyright (C) 2020-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include "config.h"
+
+#include "mu-utils.hh"
+#include "mu-readline.hh"
+
+#include <string>
+#include <unistd.h>
+
+#ifdef HAVE_LIBREADLINE
+#if defined(HAVE_READLINE_READLINE_H)
+#include <readline/readline.h>
+#elif defined(HAVE_READLINE_H)
+#include <readline.h>
+#else /* !defined(HAVE_READLINE_H) */
+extern char* readline();
+#endif /* !defined(HAVE_READLINE_H) */
+char* cmdline = NULL;
+#else /* !defined(HAVE_READLINE_READLINE_H) */
+/* no readline */
+#endif /* HAVE_LIBREADLINE */
+
+#ifdef HAVE_READLINE_HISTORY
+#if defined(HAVE_READLINE_HISTORY_H)
+#include <readline/history.h>
+#elif defined(HAVE_HISTORY_H)
+#include <history.h>
+#else /* !defined(HAVE_HISTORY_H) */
+extern void add_history();
+extern int write_history();
+extern int read_history();
+#endif /* defined(HAVE_READLINE_HISTORY_H) */
+/* no history */
+#endif /* HAVE_READLINE_HISTORY */
+
+#if defined(HAVE_LIBREADLINE) && defined(HAVE_READLINE_HISTORY)
+#define HAVE_READLINE (1)
+#else
+#define HAVE_READLINE (0)
+#endif
+
+using namespace Mu;
+
+static bool is_a_tty{};
+static std::string hist_path;
+static size_t max_lines{};
+
+// LCOV_EXCL_START
+
+bool
+Mu::have_readline()
+{
+ return HAVE_READLINE != 0;
+}
+
+void
+Mu::setup_readline(const std::string& histpath, size_t maxlines)
+{
+ is_a_tty = !!::isatty(::fileno(stdout));
+ hist_path = histpath;
+ max_lines = maxlines;
+
+#if HAVE_READLINE
+ rl_bind_key('\t', rl_insert); // default (filenames) is not useful
+ using_history();
+ read_history(hist_path.c_str());
+
+ if (max_lines > 0)
+ stifle_history(max_lines);
+#endif /*HAVE_READLINE*/
+}
+
+void
+Mu::shutdown_readline()
+{
+#if HAVE_READLINE
+ if (!is_a_tty)
+ return;
+
+ write_history(hist_path.c_str());
+ if (max_lines > 0)
+ history_truncate_file(hist_path.c_str(), max_lines);
+#endif /*HAVE_READLINE*/
+}
+
+std::string
+Mu::read_line(bool& do_quit)
+{
+#if HAVE_READLINE
+ if (is_a_tty) {
+ auto buf = readline(";; mu% ");
+ if (!buf) {
+ do_quit = true;
+ return {};
+ }
+ std::string line{buf};
+ ::free(buf);
+ return line;
+ }
+#endif /*HAVE_READLINE*/
+
+ std::string line;
+ mu_print(";; mu> ");
+ if (!std::getline(std::cin, line))
+ do_quit = true;
+
+ return line;
+}
+
+void
+Mu::save_line(const std::string& line)
+{
+#if HAVE_READLINE
+ if (is_a_tty)
+ add_history(line.c_str());
+#endif /*HAVE_READLINE*/
+}
+
+// LCOV_EXCL_STOP
--- /dev/null
+/*
+** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include <string>
+
+namespace Mu {
+
+/**
+ * Setup readline when available and on tty.
+ *
+ * @param histpath path to the history file
+ * @param max_lines maximum number of history to save
+ */
+void setup_readline(const std::string& histpath, size_t max_lines);
+
+/**
+ * Shutdown readline
+ *
+ */
+void shutdown_readline();
+
+/**
+ * Read a command line
+ *
+ * @param do_quit recceives whether we should quit.
+ *
+ * @return the string read or empty
+ */
+std::string read_line(bool& do_quit);
+
+/**
+ * Save a line to history (or do nothing when readline is not active)
+ *
+ * @param line a line.
+ */
+void save_line(const std::string& line);
+
+
+/**
+ * Do we have the non-shim readline?
+ *
+ * @return true or failse
+ */
+bool have_readline();
+
+} // namespace Mu
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "mu-regex.hh"
+#include <iostream>
+
+using namespace Mu;
+
+#if BUILD_TESTS
+#include "mu-test-utils.hh"
+
+// No need for extensive regex test, we just rely on GRegex.
+
+static void
+test_regex_match()
+{
+ auto rx = Regex::make("a.*b.c");
+ assert_valid_result(rx);
+
+ assert_equal(mu_format("{}", *rx), "/a.*b.c/");
+
+ g_assert_true(rx->matches("axxxxxbqc"));
+ g_assert_false(rx->matches("axxxxxbqqc"));
+
+ { // unset matches nothing.
+ Regex rx2;
+ g_assert_false(rx2.matches(""));
+ }
+}
+
+
+static void
+test_regex_match2()
+{
+ Regex rx;
+ {
+ std::string foo = "h.llo";
+ rx = unwrap(Regex::make(foo.c_str()));
+ }
+
+ std::string hei = "hei";
+
+ g_assert_true(rx.matches("hallo"));
+ g_assert_false(rx.matches(hei));
+}
+
+
+static void
+test_regex_replace()
+{
+ {
+ auto rx = Regex::make("f.o");
+ assert_valid_result(rx);
+ assert_equal(rx->replace("foobar", "cuux").value_or("error"), "cuuxbar");
+ }
+
+ {
+ auto rx = Regex::make("f.o", G_REGEX_MULTILINE);
+ assert_valid_result(rx);
+ assert_equal(rx->replace("foobar\nfoobar", "cuux").value_or("error"),
+ "cuuxbar\ncuuxbar");
+ }
+}
+
+
+static void
+test_regex_fail()
+{
+ allow_warnings();
+
+ { // unset rx can't replace / error.
+ Regex rx;
+ assert_equal(mu_format("{}", rx), "//");
+ g_assert_false(!!rx.replace("foo", "bar"));
+ }
+
+ {
+ auto rx = Regex::make("(");
+ g_assert_false(!!rx);
+
+ }
+
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/regex/match", test_regex_match);
+ g_test_add_func("/regex/match2", test_regex_match2);
+ g_test_add_func("/regex/replace", test_regex_replace);
+ g_test_add_func("/regex/fail", test_regex_fail);
+
+ return g_test_run();
+}
+
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2022-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#ifndef MU_REGEX_HH__
+#define MU_REGEX_HH__
+
+#include <glib.h>
+#
+#include <utils/mu-result.hh>
+#include <utils/mu-utils.hh>
+
+namespace Mu {
+/**
+ * RAII wrapper around a GRegex which in itself is a wrapper around PCRE. We use
+ * PCRE rather than std::regex because it is much faster.
+ */
+struct Regex {
+#if !GLIB_CHECK_VERSION(2,74,0) /* backward compat */
+#define G_REGEX_DEFAULT (static_cast<GRegexCompileFlags>(0))
+#define G_REGEX_MATCH_DEFAULT (static_cast<GRegexMatchFlags>(0))
+#endif
+ /**
+ * Trivial constructor
+ *
+ * @return
+ */
+ Regex() noexcept: rx_{} {}
+
+ /**
+ * Construct a new Regex object
+ *
+ * @param ptrn PRRE regular expression pattern
+ * @param cflags compile flags
+ * @param mflags match flags
+ *
+ * @return a Regex object or an error.
+ */
+ static Result<Regex> make(const std::string& ptrn,
+ GRegexCompileFlags cflags = G_REGEX_DEFAULT,
+ GRegexMatchFlags mflags = G_REGEX_MATCH_DEFAULT) noexcept try {
+ return Regex(ptrn.c_str(), cflags, mflags);
+ } catch (const Error& err) {
+ return Err(err);
+ }
+
+
+ /**
+ * Copy CTOR
+ *
+ * @param other some other Regex
+ */
+ Regex(const Regex& other) noexcept: rx_{} { *this = other; }
+
+ /**
+ * Move CTOR
+ *
+ * @param other some other Regex
+ */
+ Regex(Regex&& other) noexcept: rx_{} { *this = std::move(other); }
+
+
+ /**
+ * DTOR
+ */
+ ~Regex() noexcept { g_clear_pointer(&rx_, g_regex_unref); }
+
+ /**
+ * Cast to the the underlying GRegex*
+ *
+ * @return a GRegex*
+ */
+ operator const GRegex*() const noexcept { return rx_; }
+
+ /**
+ * Doe this object contain a valid GRegex*?
+ *
+ * @return true or false
+ */
+ operator bool() const noexcept { return !!rx_; }
+
+ /**
+ * operator=
+ *
+ * @param other copy some other object to this one
+ *
+ * @return *this
+ */
+ Regex& operator=(const Regex& other) noexcept {
+ if (this != &other) {
+ g_clear_pointer(&rx_, g_regex_unref);
+ if (other.rx_)
+ rx_ = g_regex_ref(other.rx_);
+ }
+ return *this;
+ }
+
+ /**
+ * operator=
+ *
+ * @param other move some other object to this one
+ *
+ * @return *this
+ */
+ Regex& operator=(Regex&& other) noexcept {
+ if (this != &other) {
+ g_clear_pointer(&rx_, g_regex_unref);
+ rx_ = other.rx_;
+ other.rx_ = nullptr;
+ }
+ return *this;
+ }
+
+ /**
+ * Does this regexp match the given string? An unset Regex matches
+ * nothing.
+ *
+ * @param str string to test
+ * @param mflags match flags
+ *
+ * @return true or false
+ */
+ bool matches(const std::string& str,
+ GRegexMatchFlags mflags=G_REGEX_MATCH_DEFAULT) const noexcept {
+ if (!rx_)
+ return false;
+ else
+ return g_regex_match(rx_, str.c_str(), mflags, nullptr);
+ // strangely, valgrind reports some memory error related to
+ // the str.c_str(). It *seems* like a false alarm.
+ }
+
+ /**
+ * Replace all occurrences of @this regexp in some string with a
+ * replacement string
+ *
+ * @param str some string
+ * @param repl replacement string
+ *
+ * @return string or error
+ */
+ Result<std::string> replace(const std::string& str, const std::string& repl) const {
+ GError *gerr{};
+
+ if (!rx_)
+ return Err(Error::Code::InvalidArgument, "missing regexp");
+ else if (auto&& s{g_regex_replace(rx_, str.c_str(), str.length(), 0,
+ repl.c_str(), G_REGEX_MATCH_DEFAULT, &gerr)}; !s)
+ return Err(Error::Code::InvalidArgument, &gerr, "error in Regex::replace");
+ else
+ return Ok(to_string_gchar(std::move(s)));
+ }
+
+ const GRegex* g_regex() const { return rx_; }
+
+private:
+ Regex(const char *ptrn, GRegexCompileFlags cflags, GRegexMatchFlags mflags) {
+ GError *err{};
+ if (rx_ = g_regex_new(ptrn, cflags, mflags, &err); !rx_)
+ throw Error{Error::Code::InvalidArgument, &err,
+ "invalid regexp: '{}'", ptrn};
+ }
+
+ GRegex *rx_{};
+};
+
+static inline std::string format_as(const Regex& rx) {
+ if (auto&& grx{rx.g_regex()}; !grx)
+ return "//";
+ else
+ return mu_format("/{}/", g_regex_get_pattern(grx));
+
+}
+
+
+} // namespace Mu
+
+
+#endif /* MU_REGEX_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2019-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_RESULT_HH__
+#define MU_RESULT_HH__
+
+#include <tl/expected.hpp>
+#include "utils/mu-error.hh"
+
+namespace Mu {
+/**
+ * A little Rust-envy...a Result is _either_ some value of type T, _or_ a Mu::Error
+ */
+template <typename T> using Result = tl::expected<T, Error>;
+
+/**
+ * Ok() is not typically strictly needed (unlike Err), but imitates Rust's Ok
+ * and it helps the reader.
+ *
+ * @param t the value to return
+ *
+ * @return a success Result<T>
+ */
+template <typename T> Result<T>
+Ok(T&& t)
+{
+ return std::move(t);
+}
+
+/**
+ * Implementation of Ok() for void results.
+ *
+ * @return a success Result<void>
+ */
+static inline Result<void>
+Ok()
+{
+ return {};
+}
+
+/**
+ * Return an error
+ *
+ * @param err the error
+ *
+ * @return error
+ */
+template<typename T> Result<T>
+Err(Error&& err)
+{
+ return tl::unexpected(std::move(err));
+}
+template<typename T> Result<T>
+Err(const Error& err)
+{
+ return tl::unexpected(err);
+}
+
+static inline tl::unexpected<Error>
+Err(Error&& err)
+{
+ return tl::unexpected(std::move(err));
+}
+
+static inline tl::unexpected<Error>
+Err(const Error& err)
+{
+ return tl::unexpected(err);
+}
+
+template<typename T>
+static inline tl::unexpected<Error>
+Err(const Result<T>& res)
+{
+ return res.error();
+}
+
+template<typename T>
+static inline tl::unexpected<Error>
+Err(Result<T>&& res)
+{
+ return std::move(res.error());
+}
+
+/*
+ * convenience
+ */
+template <typename ...T>
+tl::unexpected<Error>
+Err(Error::Code code, fmt::format_string<T...> frm, T&&... args)
+{
+ return Err(Error{code, frm, std::forward<T>(args)...});
+}
+
+template <typename ...T>
+tl::unexpected<Error>
+Err(Error::Code code, GError **err, fmt::format_string<T...> frm, T&&... args)
+{
+ return Err(Error{code, err, frm, std::forward<T>(args)...});
+}
+
+
+template<typename T> T
+unwrap(Result<T>&& res)
+{
+ if (!!res)
+ return std::move(res.value());
+ else
+ throw res.error();
+}
+
+/**
+ * Assert that some result has a value (for unit tests)
+ *
+ * @param R some result
+ */
+#define assert_valid_result(R) do { \
+ auto&& res__ = R; \
+ if(!res__) { \
+ mu_printerrln("{}:{}: error-result: {}", \
+ __FILE__, __LINE__, \
+ (res__).error().what()); \
+ g_assert_true(!!res__); \
+ } \
+} while(0)
+
+}// namespace Mu
+
+#endif /* MU_RESULT_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2022-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+
+#include "mu-sexp.hh"
+#include "mu-utils.hh"
+
+#include <atomic>
+#include <sstream>
+#include <array>
+
+using namespace Mu;
+
+template<typename...T> static Mu::Error
+parsing_error(size_t pos, fmt::format_string<T...> frm, T&&... args)
+{
+ const auto&& msg{fmt::format(frm, std::forward<T>(args)...)};
+ if (pos == 0)
+ return Mu::Error(Error::Code::Parsing, "{}", msg);
+ else
+ return Mu::Error(Error::Code::Parsing, "{}: {}", pos, msg);
+}
+
+static size_t
+skip_whitespace(const std::string& s, size_t pos)
+{
+ while (pos != s.size()) {
+ if (s[pos] == ' ' || s[pos] == '\t' || s[pos] == '\n')
+ ++pos;
+ else
+ break;
+ }
+ return pos;
+}
+
+static Result<Sexp> parse(const std::string& expr, size_t& pos);
+
+static Result<Sexp>
+parse_list(const std::string& expr, size_t& pos)
+{
+ if (expr[pos] != '(') // sanity check.
+ return Err(parsing_error(pos, "expected: '(' but got '{}", expr[pos]));
+
+ Sexp lst{};
+
+ ++pos;
+ while (expr[pos] != ')' && pos != expr.size()) {
+ if (auto&& item = parse(expr, pos); item)
+ lst.add(std::move(*item));
+ else
+ return Err(item.error());
+ }
+
+ if (expr[pos] != ')')
+ return Err(parsing_error(pos, "expected: ')' but got '{}'", expr[pos]));
+ ++pos;
+ return Ok(std::move(lst));
+}
+
+static Result<Sexp>
+parse_string(const std::string& expr, size_t& pos)
+{
+ if (expr[pos] != '"') // sanity check.
+ return Err(parsing_error(pos, "expected: '\"'' but got '{}", expr[pos]));
+
+ bool escape{};
+ std::string str;
+ for (++pos; pos != expr.size(); ++pos) {
+ auto kar = expr[pos];
+ if (escape && (kar == '"' || kar == '\\')) {
+ str += kar;
+ escape = false;
+ continue;
+ }
+
+ if (kar == '"')
+ break;
+ else if (kar == '\\')
+ escape = true;
+ else
+ str += kar;
+ }
+
+ if (escape || expr[pos] != '"')
+ return Err(parsing_error(pos, "unterminated string '{}'", str));
+
+ ++pos;
+ return Ok(Sexp{std::move(str)});
+}
+
+
+static Result<Sexp>
+parse_integer(const std::string& expr, size_t& pos)
+{
+ if (!isdigit(expr[pos]) && expr[pos] != '-') // sanity check.
+ return Err(parsing_error(pos, "expected: <digit> but got '{}", expr[pos]));
+
+ std::string num; // negative number?
+ if (expr[pos] == '-') {
+ num = "-";
+ ++pos;
+ }
+
+ for (; isdigit(expr[pos]); ++pos)
+ num += expr[pos];
+
+ return Ok(Sexp{::atoi(num.c_str())});
+}
+
+static Result<Sexp>
+parse_symbol(const std::string& expr, size_t& pos)
+{
+ if (!isalpha(expr[pos]) && expr[pos] != ':') // sanity check.
+ return Err(parsing_error(pos, "expected: <alpha>|: but got '{}", expr[pos]));
+
+ std::string symb(1, expr[pos]);
+ for (++pos; isalnum(expr[pos]) || expr[pos] == '-'; ++pos)
+ symb += expr[pos];
+
+ return Ok(Sexp{Sexp::Symbol{symb}});
+}
+
+static Result<Sexp>
+parse(const std::string& expr, size_t& pos)
+{
+ pos = skip_whitespace(expr, pos);
+
+ if (pos == expr.size())
+ return Err(parsing_error(pos, "expected: character '{}", expr[pos]));
+
+ const auto kar = expr[pos];
+ const auto sexp = std::invoke([&]() -> Result<Sexp> {
+ if (kar == '(')
+ return parse_list(expr, pos);
+ else if (kar == '"')
+ return parse_string(expr, pos);
+ else if (isdigit(kar) || kar == '-')
+ return parse_integer(expr, pos);
+ else if (isalpha(kar) || kar == ':')
+ return parse_symbol(expr, pos);
+ else
+ return Err(parsing_error(pos, "unexpected character '{}", kar));
+ });
+
+ if (sexp)
+ pos = skip_whitespace(expr, pos);
+
+ return sexp;
+}
+
+Result<Sexp>
+Sexp::parse(const std::string& expr)
+{
+ size_t pos{};
+ auto res = ::parse(expr, pos);
+ if (!res)
+ return res;
+ else if (pos != expr.size())
+ return Err(parsing_error(pos, "trailing data starting with '{}'", expr[pos]));
+ else
+ return res;
+}
+
+std::string
+Sexp::to_string(Format fopts) const
+{
+ std::stringstream sstrm;
+ const auto splitp{any_of(fopts & Format::SplitList)};
+ const auto typeinfop{any_of(fopts & Format::TypeInfo)};
+
+ if (listp()) {
+ sstrm << '(';
+ bool first{true};
+ for(auto&& elm: list()) {
+ sstrm << (first ? "" : " ") << elm.to_string(fopts);
+ first = false;
+ }
+ sstrm << ')';
+ if (splitp)
+ sstrm << '\n';
+ } else if (stringp())
+ sstrm << quote(string());
+ else if (numberp())
+ sstrm << number();
+ else if (symbolp())
+ sstrm << symbol().name;
+
+ if (typeinfop)
+ sstrm << '<' << Sexp::type_name(type()) << '>';
+
+ return sstrm.str();
+}
+
+// LCOV_EXCL_START
+
+std::string
+Sexp::to_json_string(Format fopts) const
+{
+ std::stringstream sstrm;
+
+ switch (type()) {
+ case Type::List: {
+ // property-lists become JSON objects
+ if (plistp()) {
+ sstrm << "{";
+ auto it{list().begin()};
+ bool first{true};
+ while (it != list().end()) {
+ sstrm << (first ? "" : ",") << quote(it->symbol().name) << ":";
+ ++it;
+ sstrm << it->to_json_string();
+ ++it;
+ first = false;
+ }
+ sstrm << "}";
+ if (any_of(fopts & Format::SplitList))
+ sstrm << '\n';
+ } else { // other lists become arrays.
+ sstrm << '[';
+ bool first{true};
+ for (auto&& child : list()) {
+ sstrm << (first ? "" : ", ") << child.to_json_string();
+ first = false;
+ }
+ sstrm << ']';
+ if (any_of(fopts & Format::SplitList))
+ sstrm << '\n';
+ }
+ break;
+ }
+ case Type::String:
+ sstrm << quote(string());
+ break;
+ case Type::Symbol:
+ if (nilp())
+ sstrm << "false";
+ else if (symbol() == "t")
+ sstrm << "true";
+ else
+ sstrm << quote(symbol().name);
+ break;
+ case Type::Number:
+ sstrm << number();
+ break;
+ default:
+ break;
+ }
+
+ return sstrm.str();
+}
+
+
+
+Sexp&
+Sexp::del_prop(const std::string& pname)
+{
+ if (auto kill_it = find_prop(pname, begin(), end()); kill_it != cend())
+ list().erase(kill_it, kill_it + 2);
+ return *this;
+}
+
+
+Sexp::const_iterator
+Sexp::find_prop(const std::string& s,
+ Sexp::const_iterator b, Sexp::const_iterator e) const
+{
+ for (auto&& it = b; it != e && it+1 != e; it += 2)
+ if (it->symbolp() && it->symbol() == s)
+ return it;
+ return e;
+}
+
+Sexp::iterator
+Sexp::find_prop(const std::string& s,
+ Sexp::iterator b, Sexp::iterator e)
+{
+ for (auto&& it = b; it != e && it+1 != e; it += 2)
+ if (it->symbolp() && it->symbol() == s)
+ return it;
+ return e;
+}
+
+
+bool
+Sexp::plistp(Sexp::const_iterator b, Sexp::const_iterator e) const
+{
+ if (b == e)
+ return true;
+ else if (b + 1 == e)
+ return false;
+ else
+ return b->symbolp() && plistp(b + 2, e);
+}
+
+
+// LCOV_EXCL_STOP
+
+#if BUILD_TESTS
+
+#include "mu-test-utils.hh"
+
+static void
+test_list()
+{
+ {
+ Sexp s;
+ g_assert_true(s.listp());
+ g_assert_true(s.to_string() == "()");
+ g_assert_true(Sexp::type_name(s.type()) == "list");
+ g_assert_true(s.empty());
+ }
+
+ {
+ Sexp::List items = {
+ Sexp("hello"),
+ Sexp(123),
+ Sexp::Symbol("world")
+ };
+ const Sexp s{std::move(items)};
+ g_assert_false(s.empty());
+ g_assert_cmpuint(s.size(),==,3);
+ g_assert_true(s.to_string() == "(\"hello\" 123 world)");
+
+
+ /* copy */
+ Sexp s2 = s;
+ g_assert_true(s2.to_string() == "(\"hello\" 123 world)");
+
+ /* move */
+ Sexp s3 = std::move(s2);
+ g_assert_true(s3.to_string() == "(\"hello\" 123 world)");
+
+ s3.clear();
+ g_assert_true(s3.empty());
+ }
+
+}
+
+static void
+test_string()
+{
+ {
+ Sexp s("hello");
+ g_assert_true(s.stringp());
+ g_assert_true(s.string()=="hello");
+ g_assert_true(s.to_string()=="\"hello\"");
+ g_assert_true(Sexp::type_name(s.type()) == "string");
+ }
+
+ {
+ // Sexp s(std::string_view("hel\"lo"));
+ // g_assert_true(s.is_string());
+ // g_assert_cmpstr(s.string().c_str(),==,"hel\"lo");
+ // g_assert_cmpstr(s.to_string().c_str(),==,"\"hel\\\"lo\"");
+ }
+}
+
+static void
+test_number()
+{
+ {
+ Sexp s(123);
+ g_assert_true(s.numberp());
+ g_assert_cmpint(s.number(),==,123);
+ g_assert_true(s.to_string() == "123");
+ g_assert_true(Sexp::type_name(s.type()) == "number");
+ }
+
+ {
+ Sexp s(true);
+ g_assert_true(s.numberp());
+ g_assert_cmpint(s.number(),==,1);
+ g_assert_true(s.to_string()=="1");
+ }
+}
+
+static void
+test_symbol()
+{
+ {
+ Sexp s{Sexp::Symbol("hello")};
+ g_assert_true(s.symbolp());
+ g_assert_true(s.symbol()=="hello");
+ g_assert_true (s.to_string()=="hello");
+ g_assert_true(Sexp::type_name(s.type()) == "symbol");
+ }
+
+ {
+ Sexp s{"hello"_sym};
+ g_assert_true(s.symbolp());
+ g_assert_true(s.symbol()=="hello");
+ g_assert_true (s.to_string()=="hello");
+ }
+
+}
+
+static void
+test_multi()
+{
+ Sexp s{"abc", 123, Sexp::Symbol{"def"}};
+ g_assert_true(s.to_string() == "(\"abc\" 123 def)");
+}
+
+
+static void
+test_add()
+{
+ {
+ Sexp s{"abc", 123};
+ s.add("def"_sym);
+ g_assert_true(s.to_string() == "(\"abc\" 123 def)");
+ }
+}
+
+static void
+test_add_multi()
+{
+ {
+ Sexp s{"abc", 123};
+ s.add("def"_sym, 456, Sexp{"boo", 2});
+ g_assert_true(s.to_string() == "(\"abc\" 123 def 456 (\"boo\" 2))");
+ }
+
+ {
+ Sexp s{"abc", 123};
+ Sexp t{"boo", 2};
+ s.add("def"_sym, 456, t);
+ g_assert_true(s.to_string() == "(\"abc\" 123 def 456 (\"boo\" 2))");
+ }
+
+}
+
+static void
+test_plist()
+{
+ Sexp s;
+ s.put_props("hello", "world"_sym, "foo", 123, "bar"_sym, "cuux");
+ g_assert_true(s.to_string() == R"((hello world foo 123 bar "cuux"))");
+
+ s.put_props("hello", 12345);
+ g_assert_true(s.to_string() == R"((foo 123 bar "cuux" hello 12345))");
+}
+
+
+static void
+check_parse(const std::string& expr, const std::string& expected)
+{
+ auto sexp = Sexp::parse(expr);
+ assert_valid_result(sexp);
+ assert_equal(to_string(*sexp), expected);
+}
+
+static void
+test_parser()
+{
+ check_parse(":foo-123", ":foo-123");
+ check_parse("foo", "foo");
+ check_parse(R"(12345)", "12345");
+ check_parse(R"(-12345)", "-12345");
+ check_parse(R"((123 bar "cuux"))", "(123 bar \"cuux\")");
+
+ check_parse(R"("foo\"bar\"cuux")", "\"foo\\\"bar\\\"cuux\"");
+
+ check_parse(R"("foo
+bar")",
+ "\"foo\nbar\"");
+}
+
+static void
+test_parser_fail()
+{
+ g_assert_false(!!Sexp::parse("\""));
+ g_assert_false(!!Sexp::parse("123abc"));
+ g_assert_false(!!Sexp::parse("("));
+ g_assert_false(!!Sexp::parse(")"));
+ g_assert_false(!!Sexp::parse("(hello (boo))))"));
+
+ g_assert_true(Sexp::type_name(static_cast<Sexp::Type>(-1)) == "<error>");
+}
+
+
+int
+main(int argc, char* argv[])
+try {
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/sexp/list", test_list);
+ g_test_add_func("/sexp/string", test_string);
+ g_test_add_func("/sexp/number", test_number);
+ g_test_add_func("/sexp/symbol", test_symbol);
+ g_test_add_func("/sexp/multi", test_multi);
+ g_test_add_func("/sexp/add", test_add);
+ g_test_add_func("/sexp/add-multi", test_add_multi);
+ g_test_add_func("/sexp/plist", test_plist);
+ g_test_add_func("/sexp/parser", test_parser);
+ g_test_add_func("/sexp/parser-fail", test_parser_fail);
+
+ return g_test_run();
+
+} catch (const std::runtime_error& re) {
+ mu_printerrln("{}", re.what());
+ return 1;
+}
+
+
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2020-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_SEXP_HH__
+#define MU_SEXP_HH__
+
+#include "mu-utils.hh"
+
+#include <stdexcept>
+#include <vector>
+#include <string>
+#include <string_view>
+#include <iostream>
+#include <variant>
+#include <cinttypes>
+#include <ostream>
+#include <cassert>
+
+#include <utils/mu-result.hh>
+#include <utils/mu-option.hh>
+
+namespace Mu {
+
+/**
+ * A structure somewhat similar to a Lisp s-expression and which can be
+ * constructed from/to an s-expressing string representation.
+ *
+ * A sexp is either an atom (String, Number, Symbol) or a List.
+ */
+struct Sexp {
+ /**
+ * Types
+ *
+ */
+ using List = std::vector<Sexp>;
+ using String = std::string;
+ using Number = int64_t;
+ struct Symbol { // distinguish from String.
+ Symbol(const std::string& s): name{s} {}
+ Symbol(std::string&& s): name(std::move(s)) {}
+ Symbol(const char* str): Symbol(std::string{str}) {}
+ Symbol(std::string_view sv): Symbol(std::string{sv}) {}
+ operator const std::string&() const {return name; }
+ std::string name;
+
+ bool operator==(const Symbol& rhs) const {
+ return this == &rhs ? true : rhs.name == name;
+ }
+ bool operator!=(const Symbol& rhs) const { return *this == rhs ? false : true; }
+ };
+ enum struct Type { List, String, Number, Symbol };
+ using ValueType = std::variant<List, String, Number, Symbol>;
+
+ /**
+ * Is some Sexp of the given type?
+ *
+ * @return true or false
+ */
+ constexpr bool stringp() const { return std::holds_alternative<String>(value); }
+ constexpr bool numberp() const { return std::holds_alternative<Number>(value); }
+ constexpr bool listp() const { return std::holds_alternative<List>(value); }
+ constexpr bool symbolp() const { return std::holds_alternative<Symbol>(value); }
+ constexpr bool symbolp(const Sexp::Symbol& sym) const {return symbolp() && symbol() == sym; }
+ constexpr bool nilp() const { return symbolp(nil_sym); }
+
+ // Get the specific variant type.
+ const List& list() const { return std::get<List>(value); }
+ List& list() { return std::get<List>(value); }
+ const String& string() const { return std::get<String>(value); }
+ String& string() { return std::get<String>(value); }
+ const Number& number() const { return std::get<Number>(value); }
+ Number& number() { return std::get<Number>(value); }
+ const Symbol& symbol() const { return std::get<Symbol>(value); }
+ Symbol& symbol() { return std::get<Symbol>(value); }
+
+ /**
+ * Constructors
+ */
+ Sexp():value{List{}} {} // default: an empty list.
+ // Copy & move ctors
+ Sexp(const Sexp& other):value{other.value}{}
+ Sexp(Sexp&& other):value{std::move(other.value)}{}
+ // From various types
+ Sexp(const List& lst): value{lst} {}
+ Sexp(List&& lst): value{std::move(lst)} {}
+ Sexp(const String& str): value{str} {}
+ Sexp(String&& str): value{std::move(str)} {}
+ Sexp(const char *str): Sexp{std::string{str}} {}
+ Sexp(std::string_view sv): Sexp{std::string{sv}} {}
+
+ template<typename N, typename = std::enable_if_t<std::is_integral_v<N>> >
+ Sexp(N n):value{static_cast<Number>(n)} {}
+
+ Sexp(const Symbol& sym): value{sym} {}
+ Sexp(Symbol&& sym): value{std::move(sym)} {}
+
+ template<typename S, typename T, typename... Args>
+ Sexp(S&& s, T&& t, Args&&... args): value{List()} {
+ auto& l{std::get<List>(value)};
+ l.emplace_back(Sexp(std::forward<S>(s)));
+ l.emplace_back(Sexp(std::forward<T>(t)));
+ (l.emplace_back(Sexp(std::forward<Args>(args))), ...);
+ }
+
+ /**
+ * Copy-assignment
+ *
+ * @param rhs another sexp
+ *
+ * @return the sexp
+ */
+ Sexp& operator=(const Sexp& rhs) {
+ if (this != &rhs)
+ value = rhs.value;
+ return *this;
+ }
+
+ /**
+ * Move-assignment
+ *
+ * @param rhs another sexp
+ *
+ * @return the sexp
+ */
+ Sexp& operator=(Sexp&& rhs) {
+ if (this != &rhs)
+ value = std::move(rhs.value);
+ return *this;
+ }
+
+ /**
+ * Get the type of value
+ *
+ * @return type
+ */
+ constexpr Type type() const { return static_cast<Type>(value.index()); }
+ /**
+ * Get the name for some type
+ *
+ * @param t type
+ *
+ * @return name
+ */
+ static constexpr std::string_view type_name(Type t) {
+ switch(t) {
+ case Type::String:
+ return "string";
+ case Type::Number:
+ return "number";
+ case Type::Symbol:
+ return "symbol";
+ case Type::List:
+ return "list";
+ default:
+ return "<error>";
+ }
+ }
+
+ /**
+ * Parse sexp from string
+ *
+ * @param str a string
+ *
+ * @return either an Sexp or an error
+ */
+ static Result<Sexp> parse(const std::string& str);
+
+
+ /**
+ * List specific functionality
+ *
+ */
+ using iterator = List::iterator;
+ using const_iterator = List::const_iterator;
+
+ iterator begin() { return list().begin(); }
+ const_iterator begin() const { return list().begin(); }
+ const_iterator cbegin() const { return list().cbegin(); }
+
+ iterator end() { return list().end(); }
+ const_iterator end() const { return list().end(); }
+ const_iterator cend() const { return list().cend(); }
+
+ bool empty() const { return list().empty(); }
+ size_t size() const { return list().size(); }
+ void clear() { list().clear(); }
+
+ /// Adding to lists
+ Sexp& add(const Sexp& s) { list().emplace_back(s); return *this; }
+ Sexp& add(Sexp&& s) { list().emplace_back(std::move(s)); return *this; }
+ Sexp& add() { return *this; }
+
+ template <typename V1, typename V2, typename... Args>
+ Sexp& add(V1&& v1, V2&& v2, Args... args) {
+ return add(std::forward<V1>(v1))
+ .add(std::forward<V2>(v2))
+ .add(std::forward<Args>(args)...);
+ }
+
+ /// Adding list elements
+ Sexp& add_list(Sexp&& l) { for (auto&& e: l) add(std::move(e)); return *this;};
+
+ /// Some convenience for the query parser
+ Sexp& front() { return list().front(); }
+ const Sexp& front() const { return list().front(); }
+ void pop_front() { list().erase(list().begin()); }
+
+ Option<Sexp&> head() { if (listp()&&!empty()) return front(); else return Nothing; }
+ Option<const Sexp&> head() const { if (listp()&&!empty()) return front(); else return Nothing; }
+
+ bool head_symbolp() const {
+ if (auto&& h{head()}; h) return h->symbolp(); else return false;
+ }
+ bool head_symbolp(const Symbol& sym) const {
+ if (head_symbolp()) return head()->symbolp(sym); else return false;
+ }
+
+ /**
+ * Property lists (aka plists)
+ */
+
+ bool plistp() const { return listp() && plistp(cbegin(), cend()); }
+ Sexp& put_props() { return *this; } // Final case for template pack.
+ template <class PropType, class SexpType, typename... Args>
+ Sexp& put_props(PropType&& prop, SexpType&& sexp, Args... args) {
+ auto&& propname{std::string(prop)};
+ return del_prop(propname)
+ .add(Symbol(std::move(propname)),
+ std::forward<SexpType>(sexp))
+ .put_props(std::forward<Args>(args)...);
+ }
+
+ /**
+ * Find the property value for some property by name
+ *
+ * @param p property name
+ *
+ * @return the property if found, or nothing
+ */
+ const Option<const Sexp&> get_prop(const std::string& p) const {
+ if (auto&& it = find_prop(p, cbegin(), cend()); it != cend())
+ return *(std::next(it));
+ else
+ return Nothing;
+ }
+ /// Output to string
+ enum struct Format {
+ Default = 0, /**< Nothing in particular */
+ SplitList = 1 << 0, /**< Insert newline after list item */
+ TypeInfo = 1 << 1, /**< Show type-info */
+ };
+
+ /**
+ * Get a string representation of the sexp
+ *
+ * @return str
+ */
+ std::string to_string(Format fopts=Format::Default) const;
+ std::string to_json_string(Format fopts=Format::Default) const;
+
+ Sexp& del_prop(const std::string& pname);
+
+ /**
+ * Some useful constants
+ *
+ */
+ static inline const auto nil_sym = Sexp::Symbol{"nil"};
+ static inline const auto t_sym = Sexp::Symbol{"t"};
+
+protected:
+ const_iterator find_prop(const std::string& s, const_iterator b,
+ const_iterator e) const;
+ bool plistp(const_iterator b, const_iterator e) const;
+private:
+ iterator find_prop(const std::string& s,iterator b,
+ iterator e);
+ ValueType value;
+
+
+};
+
+MU_ENABLE_BITOPS(Sexp::Format);
+
+/**
+ * String-literal; allow for ":foo"_sym to be a symbol
+ */
+static inline Sexp::Symbol
+operator"" _sym(const char* str, std::size_t n)
+{
+ return Sexp::Symbol{str};
+}
+
+static inline std::ostream&
+operator<<(std::ostream& os, const Sexp::Type& stype)
+{
+ os << Sexp::type_name(stype);
+ return os;
+}
+
+
+static inline std::ostream&
+operator<<(std::ostream& os, const Sexp& sexp)
+{
+ os << sexp.to_string();
+ return os;
+}
+
+} // namespace Mu
+
+#endif /* MU_SEXP_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2008-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+
+#include <langinfo.h>
+#include <locale.h>
+
+#include "utils/mu-utils.hh"
+#include "utils/mu-test-utils.hh"
+#include "utils/mu-utils-file.hh"
+#include "utils/mu-error.hh"
+
+using namespace Mu;
+
+/* LCOV_EXCL_START*/
+bool
+Mu::mu_test_mu_hacker()
+{
+ return !!g_getenv("MU_HACKER");
+}
+/* LCOV_EXCL_STOP*/
+
+
+const char*
+Mu::set_tz(const char* tz)
+{
+ static const char* oldtz;
+
+ oldtz = getenv("TZ");
+ if (tz)
+ setenv("TZ", tz, 1);
+ else
+ unsetenv("TZ");
+
+ tzset();
+ return oldtz;
+}
+
+bool
+Mu::set_en_us_utf8_locale()
+{
+ setenv("LC_ALL", "en_US.UTF-8", 1);
+
+ if (auto str = setlocale(LC_ALL, "en_US.UTF-8"); !str)
+ return false;
+
+ if (strcmp(nl_langinfo(CODESET), "UTF-8") != 0)
+ return false;
+
+ return true;
+}
+
+static void
+black_hole(void)
+{
+ return; /* do nothing */
+}
+
+void
+Mu::mu_test_init(int *argc, char ***argv)
+{
+ TempDir temp_dir;
+
+ g_unsetenv("XAPIAN_CJK_NGRAM");
+ g_setenv("MU_TEST", "yes", TRUE);
+ g_setenv("XDG_CACHE_HOME", temp_dir.path().c_str(), TRUE);
+
+ setlocale(LC_ALL, "");
+
+ g_test_init(argc, argv, NULL);
+
+ g_test_bug_base("https://github.com/djcb/mu/issues/");
+
+ if (!g_test_verbose())
+ g_log_set_handler(
+ NULL,
+ (GLogLevelFlags)(G_LOG_LEVEL_MASK |
+ G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION),
+ (GLogFunc)black_hole, NULL);
+}
+
+void
+Mu::allow_warnings()
+{
+ g_test_log_set_fatal_handler(
+ [](const char*, GLogLevelFlags, const char*, gpointer) { return FALSE; },
+ {});
+}
+
+Mu::TempDir::TempDir(bool autodelete): autodelete_{autodelete} {
+
+ if (auto res{make_temp_dir()}; !res)
+ throw res.error();
+ else
+ path_ = std::move(*res);
+
+ mu_debug("created '{}'", path_);
+}
+
+Mu::TempDir::~TempDir()
+{
+ if (::access(path_.c_str(), F_OK) != 0)
+ return; /* nothing to do */
+
+ if (!autodelete_) {
+ mu_debug("_not_ deleting {}", path_);
+ return;
+ }
+
+ if (auto&& res{run_command0({RM_PROGRAM, "-fr", path_})}; !res) {
+ /* LCOV_EXCL_START*/
+ mu_warning("error removing {}: {}", path_, format_as(res.error()));
+ /* LCOV_EXCL_STOP*/
+ } else
+ mu_debug("removed '{}'", path_);
+}
--- /dev/null
+/*
+** Copyright (C) 2008-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_TEST_UTILS_HH__
+#define MU_TEST_UTILS_HH__
+
+#include <initializer_list>
+#include <string>
+#include <utils/mu-utils.hh>
+#include <utils/mu-result.hh>
+
+namespace Mu {
+
+/**
+ * mu wrapper for g_test_init. Sets environment variable MU_TEST to 1.
+ *
+ * @param argc
+ * @param argv
+ */
+void mu_test_init(int *argc, char ***argv);
+
+
+/**
+ * Are we running in a MU_HACKER environment?
+ *
+ * @return true or false
+ */
+bool mu_test_mu_hacker();
+
+/**
+ * set the timezone
+ *
+ * @param tz timezone
+ *
+ * @return the old timezone
+ */
+const char* set_tz(const char* tz);
+
+/**
+ * switch the locale to en_US.utf8, return TRUE if it succeeds
+ *
+ * @return true if the switch succeeds, false otherwise
+ */
+bool set_en_us_utf8_locale();
+
+
+/**
+ * For unit tests, assert two std::string's are equal.
+ *
+ * @param s1 string1
+ * @param s2 string2
+ */
+#define assert_equal(s1__,s2__) do { \
+ std::string s1s__(s1__), s2s__(s2__); \
+ g_assert_cmpstr(s1s__.c_str(), ==, s2s__.c_str()); \
+ } while(0)
+
+
+#define assert_equal_seq(seq1__, seq2__) do { \
+ g_assert_cmpuint(seq1__.size(), ==, seq2__.size()); \
+ size_t n__{}; \
+ for (auto&& item__: seq1__) { \
+ g_assert_true(item__ == seq2__.at(n__)); \
+ ++n__; \
+ } \
+ } while(0)
+
+#define assert_equal_seq_str(seq1__, seq2__) do { \
+ g_assert_cmpuint(seq1__.size(), ==, seq2__.size()); \
+ size_t n__{}; \
+ for (auto&& item__: seq1__) { \
+ assert_equal(item__, seq2__.at(n__)); \
+ ++n__; \
+ } \
+ } while(0)
+
+#define assert_valid_command(RCO) do { \
+ assert_valid_result(RCO); \
+ if ((RCO)->exit_code != 0 && !(RCO)->standard_err.empty()) \
+ mu_printerrln("{}:{}: {}", \
+ __FILE__, __LINE__, (RCO)->standard_err); \
+ g_assert_cmpuint((RCO)->exit_code, ==, 0); \
+} while (0)
+
+/**
+ * For unit-tests, allow warnings in the current function.
+ *
+ */
+void allow_warnings();
+
+
+/**
+ * For unit-tests, a RAII tempdir.
+ *
+ */
+struct TempDir {
+ /**
+ * Construct a temporary directory
+ */
+ TempDir(bool autodelete=true);
+
+ /**
+ * DTOR; removes the temporary directory
+ *
+ *
+ * @return
+ */
+ ~TempDir();
+
+ /**
+ * Path to the temporary directory
+ *
+ * @return the path.
+ */
+ const std::string& path() const { return path_; }
+private:
+ std::string path_;
+ const bool autodelete_;
+};
+
+static inline auto format_as(const TempDir& td) {
+ return td.path();
+}
+
+
+/**
+ * Temporary (RAII) timezone
+ */
+struct TempTz {
+ TempTz(const char* tz) {
+ if (timezone_available(tz))
+ old_tz_ = set_tz(tz);
+ else
+ old_tz_ = {};
+ mu_debug("timezone '{}' {}available", tz, old_tz_ ? "": "not ");
+ }
+ ~TempTz() {
+ if (old_tz_) {
+ mu_debug("reset timezone to '{}'", old_tz_);
+ set_tz(old_tz_);
+ }
+ }
+ bool available() const { return !!old_tz_; }
+private:
+ const char *old_tz_{};
+};
+
+} // namepace Mu
+
+
+#endif /* MU_TEST_UTILS_HH__ */
--- /dev/null
+// borrowed from Xapian; slightly adapted
+
+/* Copyright (c) 2007, 2008 Yung-chung Lin (henearkrxern@gmail.com)
+ * Copyright (c) 2011 Richard Boulton (richard@tartarus.org)
+ * Copyright (c) 2011 Brandon Schaefer (brandontschaefer@gmail.com)
+ * Copyright (c) 2011,2018,2019,2023 Olly Betts
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+#ifndef MU_UNBROKEN_HH__
+#define MU_UNBROKEN_HH__
+
+#include <algorithm>
+#include <iterator>
+
+/**
+ * Does unichar p belong to a script without explicit word separators?
+ *
+ * @param p
+ *
+ * @return true or false
+ */
+static inline bool
+is_unbroken_script(unsigned p)
+{
+ // Array containing the last value in each range of codepoints which
+ // are either all in scripts which are written without explicit word
+ // breaks, or all not in such scripts.
+ //
+ // We only include scripts here which ICU has dictionaries for. The
+ // same list is currently also used to decide which languages to do
+ // ngrams for, though perhaps that should use a separate list.
+ constexpr unsigned splits[] = {
+ // 0E00..0E7F; Thai, Lanna Tai, Pali
+ // 0E80..0EFF; Lao
+ 0x0E00 - 1, 0x0EFF,
+ // 1000..109F; Myanmar (Burmese)
+ 0x1000 - 1, 0x109F,
+ // 1100..11FF; Hangul Jamo
+ 0x1100 - 1, 0x11FF,
+ // 1780..17FF; Khmer
+ 0x1780 - 1, 0x17FF,
+ // 19E0..19FF; Khmer Symbols
+ 0x19E0 - 1, 0x19FF,
+ // 2E80..2EFF; CJK Radicals Supplement
+ // 2F00..2FDF; Kangxi Radicals
+ // 2FE0..2FFF; Ideographic Description Characters
+ // 3000..303F; CJK Symbols and Punctuation
+ // 3040..309F; Hiragana
+ // 30A0..30FF; Katakana
+ // 3100..312F; Bopomofo
+ // 3130..318F; Hangul Compatibility Jamo
+ // 3190..319F; Kanbun
+ // 31A0..31BF; Bopomofo Extended
+ // 31C0..31EF; CJK Strokes
+ // 31F0..31FF; Katakana Phonetic Extensions
+ // 3200..32FF; Enclosed CJK Letters and Months
+ // 3300..33FF; CJK Compatibility
+ // 3400..4DBF; CJK Unified Ideographs Extension A
+ // 4DC0..4DFF; Yijing Hexagram Symbols
+ // 4E00..9FFF; CJK Unified Ideographs
+ 0x2E80 - 1, 0x9FFF,
+ // A700..A71F; Modifier Tone Letters
+ 0xA700 - 1, 0xA71F,
+ // A960..A97F; Hangul Jamo Extended-A
+ 0xA960 - 1, 0xA97F,
+ // A9E0..A9FF; Myanmar Extended-B (Burmese)
+ 0xA9E0 - 1, 0xA9FF,
+ // AA60..AA7F; Myanmar Extended-A (Burmese)
+ 0xAA60 - 1, 0xAA7F,
+ // AC00..D7AF; Hangul Syllables
+ // D7B0..D7FF; Hangul Jamo Extended-B
+ 0xAC00 - 1, 0xD7FF,
+ // F900..FAFF; CJK Compatibility Ideographs
+ 0xF900 - 1, 0xFAFF,
+ // FE30..FE4F; CJK Compatibility Forms
+ 0xFE30 - 1, 0xFE4F,
+ // FF00..FFEF; Halfwidth and Fullwidth Forms
+ 0xFF00 - 1, 0xFFEF,
+ // 1AFF0..1AFFF; Kana Extended-B
+ // 1B000..1B0FF; Kana Supplement
+ // 1B100..1B12F; Kana Extended-A
+ // 1B130..1B16F; Small Kana Extension
+ 0x1AFF0 - 1, 0x1B16F,
+ // 1F200..1F2FF; Enclosed Ideographic Supplement
+ 0x1F200 - 1, 0x1F2FF,
+ // 20000..2A6DF; CJK Unified Ideographs Extension B
+ 0x20000 - 1, 0x2A6DF,
+ // 2A700..2B73F; CJK Unified Ideographs Extension C
+ // 2B740..2B81F; CJK Unified Ideographs Extension D
+ // 2B820..2CEAF; CJK Unified Ideographs Extension E
+ // 2CEB0..2EBEF; CJK Unified Ideographs Extension F
+ 0x2A700 - 1, 0x2EBEF,
+ // 2F800..2FA1F; CJK Compatibility Ideographs Supplement
+ 0x2F800 - 1, 0x2FA1F,
+ // 30000..3134F; CJK Unified Ideographs Extension G
+ // 31350..323AF; CJK Unified Ideographs Extension H
+ 0x30000 - 1, 0x323AF
+ };
+ // Binary chop to find the first entry which is >= p. If it's an odd
+ // offset then the codepoint is in a script which needs splitting; if it's
+ // an even offset then it's not.
+ auto it = std::lower_bound(std::begin(splits),
+ std::end(splits), p);
+
+ return ((it - splits) & 1);
+}
+
+
+#endif /* MU_UNBROKEN_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2023-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include "mu-utils.hh"
+#include "mu-utils-file.hh"
+
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include <glib.h>
+#include <gio/gio.h>
+#include <gio/gunixinputstream.h>
+
+#ifdef HAVE_WORDEXP_H
+#include <wordexp.h>
+#endif /*HAVE_WORDEXP_H*/
+
+using namespace Mu;
+
+
+bool
+Mu::check_dir (const std::string& path, bool readable, bool writeable)
+{
+ const auto mode = F_OK | (readable ? R_OK : 0) | (writeable ? W_OK : 0);
+
+ if (::access (path.c_str(), mode) != 0)
+ return false;
+
+ struct stat statbuf{};
+ if (::stat (path.c_str(), &statbuf) != 0)
+ return false;
+
+ return S_ISDIR(statbuf.st_mode) ? true : false;
+}
+
+uint8_t
+Mu::determine_dtype (const std::string& path, bool use_lstat)
+{
+ int res;
+ struct stat statbuf{};
+
+ if (use_lstat)
+ res = ::lstat(path.c_str(), &statbuf);
+ else
+ res = ::stat(path.c_str(), &statbuf);
+
+ if (res != 0) {
+ mu_warning ("{}stat failed on {}: {}",
+ use_lstat ? "l" : "", path, g_strerror(errno));
+ return DT_UNKNOWN;
+ }
+
+ /* we only care about dirs, regular files and links */
+ if (S_ISREG (statbuf.st_mode))
+ return DT_REG;
+ else if (S_ISDIR (statbuf.st_mode))
+ return DT_DIR;
+ else if (S_ISLNK (statbuf.st_mode))
+ return DT_LNK;
+
+ return DT_UNKNOWN;
+}
+
+std::string
+Mu::canonicalize_filename(const std::string& path, const std::string& relative_to)
+{
+ auto str{to_string_opt_gchar(
+ g_canonicalize_filename(
+ path.c_str(),
+ relative_to.empty() ? nullptr : relative_to.c_str())).value()};
+
+ // remove trailing '/'... is this needed?
+ if (str[str.length()-1] == G_DIR_SEPARATOR)
+ str.erase(str.length() - 1);
+
+ return str;
+}
+
+std::string
+Mu::basename(const std::string& path)
+{
+ return to_string_gchar(g_path_get_basename(path.c_str()));
+}
+
+std::string
+Mu::dirname(const std::string& path)
+{
+ return to_string_gchar(g_path_get_dirname(path.c_str()));
+}
+
+Result<std::string>
+Mu::make_temp_dir()
+{
+ GError *err{};
+ if (auto tmpdir{g_dir_make_tmp("mu-tmp-XXXXXX", &err)}; !tmpdir)
+ return Err(Error::Code::File, &err,
+ "failed to create temporary directory");
+ else
+ return Ok(to_string_gchar(std::move(tmpdir)));
+}
+
+
+Result<void>
+Mu::remove_directory(const std::string& path)
+{
+ /* ugly */
+ GError *err{};
+ const auto cmd{mu_format("/bin/rm -rf '{}'", path)};
+ if (!g_spawn_command_line_sync(cmd.c_str(), NULL,
+ NULL, NULL, &err))
+ return Err(Error::Code::File, &err, "failed to remove {}", path);
+ else
+ return Ok();
+}
+
+
+
+
+std::string
+Mu::runtime_path(Mu::RuntimePath path, const std::string& muhome)
+{
+ auto [mu_cache, mu_config] =
+ std::invoke([&]()->std::pair<std::string, std::string> {
+ if (muhome.empty())
+ return { join_paths(g_get_user_cache_dir(), "mu"),
+ join_paths(g_get_user_config_dir(), "mu")};
+ else
+ return { muhome, muhome };
+ });
+
+ switch (path) {
+ case Mu::RuntimePath::Cache:
+ return mu_cache;
+ case Mu::RuntimePath::XapianDb:
+ return join_paths(mu_cache, "xapian");
+ case Mu::RuntimePath::LogFile:
+ return join_paths(mu_cache, "mu.log");
+ case Mu::RuntimePath::Bookmarks:
+ return join_paths(mu_config, "bookmarks");
+ case Mu::RuntimePath::Config:
+ return mu_config;
+ case Mu::RuntimePath::Scripts:
+ return join_paths(mu_config, "scripts");
+ /*LCOV_EXCL_START*/
+ default:
+ throw std::logic_error("unknown path");
+ /*LCOV_EXCL_STOP*/
+ }
+}
+
+/* LCOV_EXCL_START*/
+static gpointer
+cancel_wait(gpointer data)
+{
+ guint timeout, deadline;
+ GCancellable *cancel;
+
+ cancel = (GCancellable*)data;
+ timeout = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(cancel), "timeout"));
+ deadline = g_get_monotonic_time() + 1000 * timeout;
+
+ while (g_get_monotonic_time() < deadline && !g_cancellable_is_cancelled(cancel)) {
+ g_usleep(50 * 1000); /* 50 ms */
+ g_thread_yield();
+ }
+
+ g_cancellable_cancel(cancel);
+
+ return NULL;
+}
+
+static void
+cancel_wait_free(gpointer data)
+{
+ GThread *thread;
+ GCancellable *cancel;
+
+ cancel = (GCancellable*)data;
+ thread = (GThread*)g_object_get_data(G_OBJECT(cancel), "thread");
+
+ g_cancellable_cancel(cancel);
+ g_thread_join(thread);
+}
+
+GCancellable*
+Mu::g_cancellable_new_with_timeout(guint timeout)
+{
+ GCancellable *cancel;
+
+ cancel = g_cancellable_new();
+
+ g_object_set_data(G_OBJECT(cancel), "timeout", GUINT_TO_POINTER(timeout));
+ g_object_set_data(G_OBJECT(cancel), "thread",
+ g_thread_new("cancel-wait", cancel_wait, cancel));
+ g_object_set_data_full(G_OBJECT(cancel), "cancel", cancel, cancel_wait_free);
+
+ return cancel;
+}
+/* LCOV_EXCL_STOP*/
+
+/* LCOV_EXCL_START*/
+Result<std::string>
+Mu::read_from_stdin()
+{
+ g_autoptr(GOutputStream) outmem = g_memory_output_stream_new_resizable();
+ g_autoptr(GInputStream) input = g_unix_input_stream_new(STDIN_FILENO, TRUE);
+ //g_autoptr(GCancellable) cancel{maybe_cancellable_timeout(timeout)};
+
+ GError *err{};
+ auto bytes = g_output_stream_splice(outmem, input,
+ static_cast<GOutputStreamSpliceFlags>
+ (G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE |
+ G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET),
+ {}, &err);
+
+ if (bytes < 0)
+ return Err(Error::Code::File, &err, "error reading from pipe");
+
+ return Ok(std::string{
+ static_cast<const char*>(g_memory_output_stream_get_data(
+ G_MEMORY_OUTPUT_STREAM(outmem))),
+ g_memory_output_stream_get_size(G_MEMORY_OUTPUT_STREAM(outmem))});
+}
+/* LCOV_EXCL_STOP*/
+
+
+/*
+ * Set the child to a group leader to avoid being killed when the
+ * parent group is killed.
+ */
+/*LCOV_EXCL_START*/
+static void
+maybe_setsid (G_GNUC_UNUSED gpointer user_data)
+{
+#if HAVE_SETSID
+ setsid();
+#endif /*HAVE_SETSID*/
+}
+/*LCOV_EXCL_STOP*/
+
+Result<Mu::CommandOutput>
+Mu::run_command(std::initializer_list<std::string> args, bool try_setsid)
+{
+ std::vector<char*> argvec{};
+ for (auto&& arg: args)
+ argvec.push_back(g_strdup(arg.c_str()));
+ argvec.push_back({});
+
+ {
+ std::vector<std::string> qargs{};
+ for(auto&& arg: args)
+ qargs.emplace_back("'" + arg + "'");
+ mu_debug("run-command: {}", fmt::join(qargs, " "));
+ }
+
+ GError *err{};
+ int wait_status{};
+ gchar *std_out{}, *std_err{};
+ auto res = g_spawn_sync({},
+ static_cast<char**>(argvec.data()),
+ {},
+ (GSpawnFlags)(G_SPAWN_SEARCH_PATH),
+ try_setsid ? maybe_setsid : nullptr, {},
+ &std_out, &std_err, &wait_status, &err);
+
+ for (auto& a: argvec)
+ g_free(a);
+
+ if (!res)
+ return Err(Error::Code::File, &err, "failed to execute command");
+ else
+ return Ok(Mu::CommandOutput{
+ WEXITSTATUS(wait_status),
+ to_string_gchar(std::move(std_out/*consumed*/)),
+ to_string_gchar(std::move(std_err/*consumed*/))});
+}
+
+Result<Mu::CommandOutput>
+Mu::run_command0(std::initializer_list<std::string> args, bool try_setsid)
+{
+ if (auto&& res{run_command(args, try_setsid)}; !res)
+ return res;
+ else if (res->exit_code != 0)
+ return Err(Error::Code::File, "command returned {}: {}",
+ res->exit_code,
+ res->standard_err.empty() ?
+ std::string{"something went wrong"}:
+ res->standard_err);
+ else
+ return Ok(std::move(*res));
+}
+
+
+Mu::Option<std::string>
+Mu::program_in_path(const std::string& name)
+{
+ if (char *path = g_find_program_in_path(name.c_str()); path)
+ return to_string_gchar(std::move(path)/*consumes*/);
+ else
+ return Nothing;
+}
+
+
+/* LCOV_EXCL_START*/
+constexpr auto default_open_program =
+#ifdef __APPLE__
+ "open"
+#else
+ "xdg-open"
+#endif /*!__APPLE__*/
+ ;
+
+Mu::Result<void>
+Mu::play (const std::string& path)
+{
+ /* check nativity */
+ GFile *gf = g_file_new_for_path(path.c_str());
+ auto is_native = g_file_is_native(gf);
+ g_object_unref(gf);
+ if (!is_native)
+ return Err(Error::Code::File, "'{}' is not a native file", path);
+
+ auto mpp{g_getenv ("MU_PLAY_PROGRAM")};
+ const std::string prog{mpp ? mpp : default_open_program};
+
+ const auto program_path{program_in_path(prog)};
+ if (!program_path)
+ return Err(Error::Code::File, "cannot find '{}' in path", prog);
+ else if (auto&& res{run_command({*program_path, path}, true/*try-setsid*/)}; !res)
+ return Err(std::move(res.error()));
+ else
+ return Ok();
+}
+/* LCOV_EXCL_STOP*/
+
+
+Result<std::string>
+expand_path_real(const std::string& str)
+{
+#ifndef HAVE_WORDEXP_H
+ return Ok(std::string{str});
+#else
+ int res;
+ wordexp_t result{};
+
+ res = wordexp(str.c_str(), &result, 0);
+ if (res != 0)
+ return Err(Error::Code::File, "cannot expand {}; err={}", str, res);
+ else if (auto&n = result.we_wordc; n != 1) {
+ wordfree(&result);
+ return Err(Error::Code::File, "expected 1 expansions, but got {} for {}", n, str);
+ }
+
+ std::string expanded{result.we_wordv[0]};
+ wordfree(&result);
+
+ return Ok(std::move(expanded));
+
+#endif /*HAVE_WORDEXP_H*/
+}
+
+
+Result<std::string>
+Mu::expand_path(const std::string& str)
+{
+ if (auto&& res{expand_path_real(str)}; res)
+ return res;
+
+ // failed... try quoting.
+ auto qstr{to_string_gchar(g_shell_quote(str.c_str()))};
+ return expand_path_real(qstr);
+}
+
+
+
+#ifdef BUILD_TESTS
+
+/*
+ * Tests.
+ *
+ */
+
+#include <glib/gstdio.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include "utils/mu-test-utils.hh"
+
+static void
+test_check_dir_01(void)
+{
+ if (g_access("/usr/bin", F_OK) == 0) {
+ g_assert_cmpuint(
+ check_dir("/usr/bin", true, false) == true,
+ ==,
+ g_access("/usr/bin", R_OK) == 0);
+ }
+}
+
+static void
+test_check_dir_02(void)
+{
+ if (g_access("/tmp", F_OK) == 0) {
+ g_assert_cmpuint(
+ check_dir("/tmp", false, true) == true,
+ ==,
+ g_access("/tmp", W_OK) == 0);
+ }
+}
+
+static void
+test_check_dir_03(void)
+{
+ if (g_access(".", F_OK) == 0) {
+ g_assert_cmpuint(
+ check_dir(".", true, true) == true,
+ ==,
+ g_access(".", W_OK | R_OK) == 0);
+ }
+}
+
+static void
+test_check_dir_04(void)
+{
+ /* not a dir, so it must be false */
+ g_assert_cmpuint(
+ check_dir("test-util.c", true, true),
+ ==,
+ false);
+}
+
+static void
+test_determine_dtype_with_lstat(void)
+{
+ g_assert_cmpuint(
+ determine_dtype(MU_TESTMAILDIR, true), ==, DT_DIR);
+ g_assert_cmpuint(
+ determine_dtype(MU_TESTMAILDIR2, true), ==, DT_DIR);
+ g_assert_cmpuint(
+ determine_dtype(MU_TESTMAILDIR2 "/Foo/cur/mail5", true),
+ ==, DT_REG);
+}
+
+
+static void
+test_program_in_path(void)
+{
+ g_assert_true(!!program_in_path("ls"));
+}
+
+static void
+test_join_paths()
+{
+
+ assert_equal(join_paths(), "");
+ assert_equal(join_paths("a"), "a");
+ assert_equal(join_paths("a", "b"), "a/b");
+ assert_equal(join_paths("/a/b///c/d//", "e"), "/a/b/c/d/e");
+}
+
+static void
+test_runtime_paths()
+{
+ TempDir tdir;
+
+ assert_equal(runtime_path(RuntimePath::Cache, tdir.path()), tdir.path());
+ assert_equal(runtime_path(RuntimePath::XapianDb, tdir.path()),
+ join_paths(tdir.path(), "xapian"));
+ assert_equal(runtime_path(RuntimePath::Bookmarks, tdir.path()),
+ join_paths(tdir.path(), "bookmarks"));
+ assert_equal(runtime_path(RuntimePath::Config, tdir.path()), tdir.path());
+ assert_equal(runtime_path(RuntimePath::Scripts, tdir.path()),
+ join_paths(tdir.path(), "scripts"));
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ /* check_dir */
+ g_test_add_func("/utils/check-dir-01",
+ test_check_dir_01);
+ g_test_add_func("/utils/check-dir-02",
+ test_check_dir_02);
+ g_test_add_func("/utils/check-dir-03",
+ test_check_dir_03);
+ g_test_add_func("/utils/check-dir-04",
+ test_check_dir_04);
+ g_test_add_func("/utils/determine-dtype-with-lstat",
+ test_determine_dtype_with_lstat);
+ g_test_add_func("/utils/program-in-path",
+ test_program_in_path);
+ g_test_add_func("/utils/join-paths",
+ test_join_paths);
+ g_test_add_func("/utils/runtime-paths",
+ test_runtime_paths);
+
+ return g_test_run();
+}
+
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_UTILS_FILE_HH__
+#define MU_UTILS_FILE_HH__
+
+#include <string>
+#include <cinttypes>
+#include <sys/stat.h>
+
+#include <gio/gio.h>
+#include <utils/mu-option.hh>
+#include <utils/mu-result.hh>
+#include <utils/mu-regex.hh>
+
+namespace Mu {
+
+/**
+ * Check if the directory has the given attributes
+ *
+ * @param path path to dir
+ * @param readable is it readable? false means "don't care"
+ * @param writeable is it writable? false means "don't care"
+ *
+ * @return true if is is a directory with given attributes; false otherwise.
+ */
+bool check_dir(const std::string& path, bool readable=false, bool writeable=false);
+
+/**
+ * See g_canonicalize_filename
+ *
+ * @param filename
+ * @param relative_to
+ *
+ * @return
+ */
+std::string canonicalize_filename(const std::string& path, const std::string& relative_to="");
+
+/**
+ * Expand the filesystem path (as per wordexp(3))
+ *
+ * @param str a filesystem path string
+ *
+ * @return the expanded string or some error
+ */
+Result<std::string> expand_path(const std::string& str);
+
+
+/**
+ * Get the basename for path, i.e. without leading directory component,
+ * @see g_path_get_basename
+ *
+ * @param path
+ *
+ * @return the basename
+ */
+std::string basename(const std::string& path);
+
+
+/**
+ * Get the dirname for path, i.e. without leading directory component,
+ * @see g_path_get_dirname
+ *
+ * @param path
+ *
+ * @return the dirname
+ */
+std::string dirname(const std::string& path);
+
+
+/*
+ * for OSs without support for direntry->d_type, like Solaris
+ */
+#ifndef DT_UNKNOWN
+enum {
+ DT_UNKNOWN = 0,
+#define DT_UNKNOWN DT_UNKNOWN
+ DT_FIFO = 1,
+#define DT_FIFO DT_FIFO
+ DT_CHR = 2,
+#define DT_CHR DT_CHR
+ DT_DIR = 4,
+#define DT_DIR DT_DIR
+ DT_BLK = 6,
+#define DT_BLK DT_BLK
+ DT_REG = 8,
+#define DT_REG DT_REG
+ DT_LNK = 10,
+#define DT_LNK DT_LNK
+ DT_SOCK = 12,
+#define DT_SOCK DT_SOCK
+ DT_WHT = 14
+#define DT_WHT DT_WHT
+};
+#endif /*DT_UNKNOWN*/
+
+ /**
+ * get the d_type (as in direntry->d_type) for the file at path, using either
+ * stat(3) or lstat(3)
+ *
+ * @param path full path
+ * @param use_lstat whether to use lstat (otherwise use stat)
+ *
+ * @return DT_REG, DT_DIR, DT_LNK, or DT_UNKNOWN (other values are not supported
+ * currently)
+ */
+uint8_t determine_dtype(const std::string& path, bool use_lstat=false);
+
+
+/**
+ * Well-known runtime paths
+ *
+ */
+enum struct RuntimePath {
+ XapianDb,
+ Cache,
+ LogFile,
+ Config,
+ Scripts,
+ Bookmarks
+};
+
+/**
+ * Get some well-known Path for internal use when don't have
+ * access to the command-line
+ *
+ * @param path the RuntimePath to find
+ * @param muhome path to muhome directory, or empty for the default.
+ *
+ * @return the path name
+ */
+std::string runtime_path(RuntimePath path, const std::string& muhome="");
+
+/**
+ * Join path components into a path (with '/')
+ *
+ * @param s a string-convertible value
+ * @param args 0 or more string-convertible values
+ *
+ * @return the path
+ */
+static inline std::string join_paths() { return {}; }
+template<typename S> std::string join_paths_(S&& s) { return std::string{s}; }
+template<typename S, typename...Args>
+std::string join_paths_(S&& s, Args...args) {
+
+ static std::string sepa{"/"};
+ auto&& str{std::string{std::forward<S>(s)}};
+ if (auto&& rest{join_paths_(std::forward<Args>(args)...)}; !rest.empty())
+ str += (sepa + rest);
+ return str;
+}
+
+template<typename S, typename...Args>
+std::string join_paths(S&& s, Args...args) {
+
+ constexpr auto sepa = '/';
+ auto path = join_paths_(std::forward<S>(s), std::forward<Args>(args)...);
+
+ auto c{0U};
+ while (c < path.size()) {
+
+ if (path[c] != sepa) {
+ ++c;
+ continue;
+ }
+
+ while (path[++c] == '/') {
+ path.erase(c, 1);
+ --c;
+ }
+ }
+
+ return path;
+}
+
+
+/**
+ * Like g_cancellable_new(), but automatically cancels itself
+ * after timeout
+ *
+ * @param timeout timeout in millisecs
+ *
+ * @return A GCancellable* instances; free with g_object_unref() when
+ * no longer needed.
+ */
+GCancellable* g_cancellable_new_with_timeout(guint timeout);
+
+/**
+ * Read for standard input
+ *
+ * @return data from standard input or an error.
+ */
+Result<std::string> read_from_stdin();
+
+/**
+ * Create a randomly-named temporary directory
+ *
+ * @return name of the temporary directory or an error.
+ */
+Result<std::string> make_temp_dir();
+
+
+/**
+ * Remove a directory, recursively. Does not have to be empty.
+ *
+ * @param path path to directory
+ *
+ * @return Ok() or an error.
+ */
+Result<void> remove_directory(const std::string& path);
+
+/**
+ * Run some system command.
+ *
+ * @param args a list of commmand line arguments (like argv)
+ * @param try_setsid whether to try setsid(2) (see its manpage for details) if this
+ * system supports it.
+ *
+ * @return Ok(exit code) or an error. Note that exit-code != 0 is _not_
+ * considered an error from the perspective of run_command, but is for
+ * run_command0
+ */
+struct CommandOutput {
+ int exit_code;
+ std::string standard_out;
+ std::string standard_err;
+};
+Result<CommandOutput> run_command(std::initializer_list<std::string> args,
+ bool try_setsid=false);
+Result<CommandOutput> run_command0(std::initializer_list<std::string> args,
+ bool try_setsid=false);
+
+/**
+ * Try to 'play' (ie., open with it's associated program) a file. On MacOS, the
+ * the program 'open' is used for this; on other platforms 'xdg-open' to do the
+ * actual opening. In addition you can set it to another program by setting thep
+ * MU_PLAY_PROGRAM environment variable
+ *
+ * This requires a 'native' file, see g_file_is_native()
+ *
+ * @param path full path of the file to open
+ *
+ * @return Ok() if succeeded, some error otherwise.
+ */
+Result<void> play(const std::string& path);
+
+/**
+ * Find program in PATH
+ *
+ * @param name the name of the program
+ *
+ * @return either the full path to program, or Nothing if not found.
+ */
+Option<std::string> program_in_path(const std::string& name);
+
+} // namespace Mu
+
+#endif /* MU_UTILS_FILE_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2017-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This library is free software; you can redistribute it and/or
+** modify it under the terms of the GNU Lesser General Public License
+** as published by the Free Software Foundation; either version 2.1
+** of the License, or (at your option) any later version.
+**
+** This library 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
+** Lesser General Public License for more details.
+**
+** You should have received a copy of the GNU Lesser General Public
+** License along with this library; if not, write to the Free
+** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+** 02110-1301, USA.
+*/
+
+#ifndef _XOPEN_SOURCE
+#define _XOPEN_SOURCE
+#include <stdexcept>
+#endif /*_XOPEN_SOURCE*/
+
+#include <array>
+
+#include <time.h>
+
+#define GNU_SOURCE
+#include <stdio.h>
+#include <stdint.h>
+#include <unistd.h>
+
+#include <string.h>
+#include <iostream>
+#include <algorithm>
+#include <numeric>
+#include <functional>
+#include <cinttypes>
+#include <charconv>
+#include <limits>
+
+#include <glib.h>
+#include <glib/gprintf.h>
+
+#include "mu-utils.hh"
+#include "mu-unbroken.hh"
+
+#include "mu-error.hh"
+#include "mu-option.hh"
+
+using namespace Mu;
+
+namespace {
+
+static gunichar
+unichar_tolower(gunichar uc)
+{
+ if (!g_unichar_isalpha(uc))
+ return uc;
+
+ if (g_unichar_get_script(uc) != G_UNICODE_SCRIPT_LATIN)
+ return g_unichar_tolower(uc);
+
+ switch (uc) {
+ case 0x00e6:
+ case 0x00c6: return 'e'; /* æ */
+ case 0x00f8: return 'o'; /* ø */
+ case 0x0110:
+ case 0x0111:
+ return 'd'; /* đ */
+ /* todo: many more */
+ default: return g_unichar_tolower(uc);
+ }
+}
+
+/**
+ * gx_utf8_flatten:
+ * @str: a UTF-8 string
+ * @len: the length of @str, or -1 if it is %NULL-terminated
+ *
+ * Flatten some UTF-8 string; that is, downcase it and remove any diacritics.
+ *
+ * Returns: (transfer full): a flattened string, free with g_free().
+ */
+static char*
+gx_utf8_flatten(const gchar* str, gssize len)
+{
+ GString* gstr;
+ char * norm, *cur;
+
+ g_return_val_if_fail(str, NULL);
+
+ norm = g_utf8_normalize(str, len, G_NORMALIZE_ALL);
+ if (!norm)
+ return NULL;
+
+ gstr = g_string_sized_new(strlen(norm));
+
+ for (cur = norm; cur && *cur; cur = g_utf8_next_char(cur)) {
+ gunichar uc;
+
+ uc = g_utf8_get_char(cur);
+ if (g_unichar_combining_class(uc) != 0)
+ continue;
+
+ g_string_append_unichar(gstr, unichar_tolower(uc));
+ }
+
+ g_free(norm);
+
+ return g_string_free(gstr, FALSE);
+}
+
+} // namespace
+
+bool
+Mu::contains_unbroken_script(const char *str)
+{
+ while (str && *str) {
+ auto uc = g_utf8_get_char(str);
+ if (is_unbroken_script(uc))
+ return true;
+ str = g_utf8_next_char(str);
+ }
+
+ return false;
+}
+
+std::string // gx_utf8_flatten
+Mu::utf8_flatten(const char* str)
+{
+ if (!str)
+ return {};
+
+ if (contains_unbroken_script(str))
+ return std::string{str};
+
+ // the pure-ascii case
+ if (g_str_is_ascii(str)) {
+ auto l = g_ascii_strdown(str, -1);
+ std::string s{l};
+ g_free(l);
+ return s;
+ }
+
+ // seems we need the big guns
+ char* flat = gx_utf8_flatten(str, -1);
+ if (!flat)
+ return {};
+
+ std::string s{flat};
+ g_free(flat);
+
+ return s;
+}
+
+
+/* turn \0-terminated buf into ascii (which is a utf8 subset); convert
+ * any non-ascii into '.'
+ */
+static char*
+asciify_in_place (char *buf)
+{
+ char *c;
+
+ g_return_val_if_fail (buf, NULL);
+
+ for (c = buf; c && *c; ++c) {
+ if ((!isprint(*c) && !isspace (*c)) || !isascii(*c))
+ *c = '.';
+ }
+
+ return buf;
+}
+
+static char*
+utf8ify (const char *buf)
+{
+ char *utf8;
+
+ g_return_val_if_fail (buf, NULL);
+
+ utf8 = g_strdup (buf);
+
+ if (!g_utf8_validate (buf, -1, NULL))
+ asciify_in_place (utf8);
+
+ return utf8;
+}
+
+
+std::string
+Mu::utf8_clean(const std::string& dirty)
+{
+ g_autoptr(GString) gstr = g_string_sized_new(dirty.length());
+ g_autofree char *cstr = utf8ify(dirty.c_str());
+
+ for (auto cur = cstr; cur && *cur; cur = g_utf8_next_char(cur)) {
+ const gunichar uc = g_utf8_get_char(cur);
+ if (g_unichar_iscntrl(uc))
+ g_string_append_c(gstr, ' ');
+ else
+ g_string_append_unichar(gstr, uc);
+ }
+
+ return std::string{g_strstrip(gstr->str)};
+}
+
+
+std::string
+Mu::utf8_wordbreak(const std::string& txt)
+{
+ g_autoptr(GString) gstr = g_string_sized_new(txt.length());
+
+ bool spc{};
+ for (auto cur = txt.c_str(); cur && *cur; cur = g_utf8_next_char(cur)) {
+ const gunichar uc = g_utf8_get_char(cur);
+
+ if (g_unichar_iscntrl(uc)) {
+ g_string_append_c(gstr, ' ');
+ continue;
+ }
+ // inspired by Xapian's termgenerator.
+
+ switch(uc) {
+ case '\'':
+ case '&':
+ case 0xb7:
+ case 0x5f4:
+ case 0x2019:
+ case 0x201b:
+ case 0x2027:
+ case ',':
+ case '.':
+ case ';':
+ case '+':
+ case '#':
+ case '-':
+ case 0x037e: // GREEK QUESTION MARK
+ case 0x0589: // ARMENIAN FULL STOP
+ case 0x060D: // ARABIC DATE SEPARATOR
+ case 0x07F8: // NKO COMMA
+ case 0x2044: // FRACTION SLASH
+ case 0xFE10: // PRESENTATION FORM FOR VERTICAL COMMA
+ case 0xFE13: // PRESENTATION FORM FOR VERTICAL COLON
+ case 0xFE14: // PRESENTATION FORM FOR VERTICAL SEMICOLON
+ if (spc)
+ break;
+ spc = true;
+ g_string_append_c(gstr, ' ');
+ break;
+ default:
+ spc = false;
+ g_string_append_unichar(gstr, uc);
+ break;
+ }
+ }
+
+ return std::string{g_strstrip(gstr->str)};
+}
+
+
+std::string
+Mu::remove_ctrl(const std::string& str)
+{
+ char prev{'\0'};
+ std::string result;
+ result.reserve(str.length());
+
+ for (auto&& c : str) {
+ if (::iscntrl(c) || c == ' ') {
+ if (prev != ' ')
+ result += prev = ' ';
+ } else
+ result += prev = c;
+ }
+
+ return result;
+}
+
+std::vector<std::string>
+Mu::split(const std::string& str, const std::string& sepa)
+{
+ std::vector<std::string> vec;
+ size_t b = 0, e = 0;
+
+ /* special cases */
+ if (str.empty())
+ return vec;
+ else if (sepa.empty()) {
+ for (auto&& c: str)
+ vec.emplace_back(1, c);
+ return vec;
+ }
+
+ while (true) {
+ if (e = str.find(sepa, b); e != std::string::npos) {
+ vec.emplace_back(str.substr(b, e - b));
+ b = e + sepa.length();
+ } else {
+ vec.emplace_back(str.substr(b));
+ break;
+ }
+ }
+
+ return vec;
+}
+
+std::vector<std::string>
+Mu::split(const std::string& str, char sepa)
+{
+ std::vector<std::string> vec;
+ size_t b = 0, e = 0;
+
+ /* special case */
+ if (str.empty())
+ return vec;
+
+ while (true) {
+ if (e = str.find(sepa, b); e != std::string::npos) {
+ vec.emplace_back(str.substr(b, e - b));
+ b = e + sizeof(sepa);
+ } else {
+ vec.emplace_back(str.substr(b));
+ break;
+ }
+ }
+
+ return vec;
+}
+
+std::string
+Mu::join(const std::vector<std::string>& svec, const std::string& sepa)
+{
+ if (svec.empty())
+ return {};
+
+
+ /* calculate the overall size beforehand, to avoid re-allocations. */
+ size_t value_len =
+ std::accumulate(svec.cbegin(), svec.cend(), 0,
+ [](size_t size, const std::string& s) {
+ return size + s.size();
+ }) + (svec.size() - 1) * sepa.length();
+
+ std::string value;
+ value.reserve(value_len);
+
+ std::accumulate(svec.cbegin(), svec.cend(), std::ref(value),
+ [&](std::string& s1, const std::string& s2)->std::string& {
+ if (s1.empty())
+ s1 = s2;
+ else {
+ s1.append(sepa);
+ s1.append(s2);
+ }
+ return s1;
+ });
+
+ return value;
+}
+
+std::string
+Mu::quote(const std::string& str)
+{
+ std::string res{"\""};
+
+ for (auto&& k : str) {
+ switch (k) {
+ case '"': res += "\\\""; break;
+ case '\\': res += "\\\\"; break;
+ default: res += k;
+ }
+ }
+
+ return res + "\"";
+}
+
+static Option<::time_t>
+delta_ymwdhMs(const std::string& expr)
+{
+ char* endptr;
+ auto num = strtol(expr.c_str(), &endptr, 10);
+ if (num <= 0 || num > 9999 || !endptr || !*endptr)
+ return Nothing;
+
+ int years, months, weeks, days, hours, minutes, seconds;
+ years = months = weeks = days = hours = minutes = seconds = 0;
+
+ switch (endptr[0]) {
+ case 's': seconds = num; break;
+ case 'M': minutes = num; break;
+ case 'h': hours = num; break;
+ case 'd': days = num; break;
+ case 'w': weeks = num; break;
+ case 'm': months = num; break;
+ case 'y': years = num; break;
+ default:
+ return Nothing;
+ }
+
+ GDateTime *then, *now = g_date_time_new_now_local();
+ if (weeks != 0)
+ then = g_date_time_add_weeks(now, -weeks);
+ else
+ then =
+ g_date_time_add_full(now, -years, -months, -days, -hours, -minutes, -seconds);
+
+ auto t = std::max<::time_t>(0, g_date_time_to_unix(then));
+
+ g_date_time_unref(then);
+ g_date_time_unref(now);
+
+ return t;
+}
+
+static Option<::time_t>
+special_date_time(const std::string& d, bool is_first)
+{
+ if (d == "now")
+ return ::time({});
+
+ if (d == "today") {
+ GDateTime *dt, *midnight;
+ dt = g_date_time_new_now_local();
+
+ if (!is_first) {
+ GDateTime* tmp = dt;
+ dt = g_date_time_add_days(dt, 1);
+ g_date_time_unref(tmp);
+ }
+
+ midnight = g_date_time_add_full(dt,
+ 0,
+ 0,
+ 0,
+ -g_date_time_get_hour(dt),
+ -g_date_time_get_minute(dt),
+ -g_date_time_get_second(dt));
+ time_t t = MAX(0, (gint64)g_date_time_to_unix(midnight));
+ g_date_time_unref(dt);
+ g_date_time_unref(midnight);
+
+ return t;
+ }
+
+ return Nothing;
+}
+
+// if a date has a month day greater than the number of days in that month,
+// change it to a valid date point to the last second in that month
+static void
+fixup_month(struct tm* tbuf)
+{
+ decltype(tbuf->tm_mday) max_days;
+ const auto month = tbuf->tm_mon + 1;
+ const auto year = tbuf->tm_year + 1900;
+
+ switch (month) {
+ case 2:
+ if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0))
+ max_days = 29;
+ else
+ max_days = 28;
+ break;
+ case 4:
+ case 6:
+ case 9:
+ case 11:
+ max_days = 30;
+ break;
+ default:
+ max_days = 31;
+ break;
+ }
+
+ if (tbuf->tm_mday > max_days) {
+ tbuf->tm_mday = max_days;
+ tbuf->tm_hour = 23;
+ tbuf->tm_min = 59;
+ tbuf->tm_sec = 59;
+ }
+ }
+
+
+Option<::time_t>
+Mu::parse_date_time(const std::string& dstr, bool is_first, bool utc)
+{
+ struct tm tbuf{};
+ GDateTime *dtime{};
+ gint64 t;
+
+ /* one-sided dates */
+ if (dstr.empty())
+ return is_first ? 0 : G_MAXINT64;
+ else if (dstr == "today" || dstr == "now")
+ return special_date_time(dstr, is_first);
+ else if (dstr.find_first_of("ymdwhMs") != std::string::npos)
+ return delta_ymwdhMs(dstr);
+
+ constexpr char UserDateMin[] = "19700101000000";
+ constexpr char UserDateMax[] = "29991231235959";
+
+ std::string date(is_first ? UserDateMin : UserDateMax);
+ std::copy_if(dstr.begin(), dstr.end(), date.begin(), [](auto c) { return isdigit(c); });
+
+ if (!::strptime(date.c_str(), "%Y%m%d%H%M%S", &tbuf) &&
+ !::strptime(date.c_str(), "%Y%m%d%H%M", &tbuf) &&
+ !::strptime(date.c_str(), "%Y%m%d%H", &tbuf) &&
+ !::strptime(date.c_str(), "%Y%m%d", &tbuf) &&
+ !::strptime(date.c_str(), "%Y%m", &tbuf) &&
+ !::strptime(date.c_str(), "%Y", &tbuf))
+ return Nothing;
+
+ fixup_month(&tbuf);
+ dtime = utc ?
+ g_date_time_new_utc(tbuf.tm_year + 1900,
+ tbuf.tm_mon + 1,
+ tbuf.tm_mday,
+ tbuf.tm_hour,
+ tbuf.tm_min,
+ tbuf.tm_sec) :
+ g_date_time_new_local(tbuf.tm_year + 1900,
+ tbuf.tm_mon + 1,
+ tbuf.tm_mday,
+ tbuf.tm_hour,
+ tbuf.tm_min,
+ tbuf.tm_sec);
+
+ t = g_date_time_to_unix(dtime);
+ g_date_time_unref(dtime);
+
+ return to_time_t(t);
+}
+
+
+Option<int64_t>
+Mu::parse_size(const std::string& val, bool is_first)
+{
+ int64_t size{-1};
+ std::string str;
+ GRegex* rx;
+ GMatchInfo* minfo;
+
+ /* one-sided ranges */
+ if (val.empty())
+ return is_first ? 0 : std::numeric_limits<int64_t>::max();
+
+ rx = g_regex_new("^(\\d+)(b|k|kb|m|mb|g|gb)?$",
+ G_REGEX_CASELESS, (GRegexMatchFlags)0, NULL);
+ minfo = NULL;
+ if (g_regex_match(rx, val.c_str(), (GRegexMatchFlags)0, &minfo)) {
+
+ char* s;
+ s = g_match_info_fetch(minfo, 1);
+ size = atoll(s);
+ g_free(s);
+
+ s = g_match_info_fetch(minfo, 2);
+ switch (s ? g_ascii_tolower(s[0]) : 0) {
+ case 'k': size *= 1024; break;
+ case 'm': size *= (1024 * 1024); break;
+ case 'g': size *= (1024 * 1024 * 1024); break;
+ default: break;
+ }
+
+ g_free(s);
+ }
+
+ g_regex_unref(rx);
+ g_match_info_unref(minfo);
+
+ if (size < 0)
+ return Nothing;
+ else
+ return size;
+
+}
+
+std::string
+Mu::to_lexnum(int64_t val)
+{
+ char buf[18]; /* 1 byte prefix + hex + \0 */
+ buf[0] = 'f' + ::snprintf(buf + 1, sizeof(buf) - 1, "%" PRIx64, val);
+ return buf;
+}
+
+int64_t
+Mu::from_lexnum(const std::string& str)
+{
+ int64_t val{};
+ std::from_chars(str.c_str() + 1, str.c_str() + str.size(), val, 16);
+
+ return val;
+}
+
+bool
+Mu::locale_workaround() try
+{
+ // quite horrible... but some systems break otherwise with
+ // https://github.com/djcb/mu/issues/2252
+
+ try {
+ std::locale::global(std::locale(""));
+ } catch (const std::runtime_error& re) {
+ g_setenv("LC_ALL", "C", 1);
+ std::locale::global(std::locale(""));
+ }
+
+ return true;
+
+} catch (...) {
+ return false;
+}
+
+bool
+Mu::timezone_available(const std::string& tz)
+{
+ const auto old_tz = g_getenv("TZ");
+
+ g_setenv("TZ", tz.c_str(), TRUE);
+
+ auto tzone = g_time_zone_new_local ();
+ bool have_tz = g_strcmp0(g_time_zone_get_identifier(tzone), tz.c_str()) == 0;
+ g_time_zone_unref (tzone);
+
+ if (old_tz)
+ g_setenv("TZ", old_tz, TRUE);
+ else
+ g_unsetenv("TZ");
+
+ return have_tz;
+}
+
+std::string
+Mu::summarize(const std::string& str, size_t max_lines)
+{
+ size_t nl_seen;
+ unsigned i,j;
+ gboolean last_was_blank;
+
+ if (str.empty())
+ return {};
+
+ /* len for summary <= original len */
+ char *summary = g_new (gchar, str.length() + 1);
+
+ /* copy the string up to max_lines lines, replace CR/LF/tab with
+ * single space */
+ for (i = j = 0, nl_seen = 0, last_was_blank = TRUE;
+ nl_seen < max_lines && i < str.length(); ++i) {
+
+ if (str[i] == '\n' || str[i] == '\r' ||
+ str[i] == '\t' || str[i] == ' ' ) {
+
+ if (str[i] == '\n')
+ ++nl_seen;
+
+ /* no double-blanks or blank at end of str */
+ if (!last_was_blank && str[i+1] != '\0')
+ summary[j++] = ' ';
+
+ last_was_blank = TRUE;
+ } else {
+
+ summary[j++] = str[i];
+ last_was_blank = FALSE;
+ }
+ }
+
+ summary[j] = '\0';
+
+ return to_string_gchar(std::move(summary)/*consumes*/);
+}
+
+
+
+
+static bool
+locale_is_utf8 (void)
+{
+ const gchar *dummy;
+ static int is_utf8 = -1;
+ if (G_UNLIKELY(is_utf8 == -1))
+ is_utf8 = g_get_charset(&dummy) ? 1 : 0;
+
+ return !!is_utf8;
+}
+
+bool
+Mu::fputs_encoded (const std::string& str, FILE *stream)
+{
+ g_return_val_if_fail (stream, false);
+
+ /* g_get_charset return TRUE when the locale is UTF8 */
+ if (locale_is_utf8())
+ return ::fputs (str.c_str(), stream) == EOF ? false: true;
+
+ /* charset is _not_ utf8, so we need to convert it */
+ char *conv{};
+ if (g_utf8_validate (str.c_str(), -1, NULL))
+ conv = g_locale_from_utf8 (str.c_str(), -1, {}, {}, {});
+
+ /* conversion failed; this happens because is some cases GMime may gives
+ * us non-UTF-8 strings from e.g. wrongly encoded message-subjects; if
+ * so, we escape the string */
+ conv = conv ? conv : g_strescape (str.c_str(), "\n\t");
+ int rv = conv ? ::fputs (conv, stream) : EOF;
+ g_free (conv);
+
+ return (rv == EOF) ? false: true;
+}
--- /dev/null
+/*
+** Copyright (C) 2020-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This library is free software; you can redistribute it and/or
+** modify it under the terms of the GNU Lesser General Public License
+** as published by the Free Software Foundation; either version 2.1
+** of the License, or (at your option) any later version.
+**
+** This library 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
+** Lesser General Public License for more details.
+**
+** You should have received a copy of the GNU Lesser General Public
+** License along with this library; if not, write to the Free
+** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+** 02110-1301, USA.
+*/
+
+#ifndef MU_UTILS_HH__
+#define MU_UTILS_HH__
+
+#include <string>
+#include <string_view>
+#include <sstream>
+#include <vector>
+#include <chrono>
+#include <memory>
+#include <cstdarg>
+#include <glib.h>
+#include <ostream>
+#include <iostream>
+#include <type_traits>
+#include <algorithm>
+#include <numeric>
+
+#include "mu-option.hh"
+
+#ifndef FMT_HEADER_ONLY
+#define FMT_HEADER_ONLY
+#endif /*FMT_HEADER_ONLY*/
+#include <fmt/format.h>
+#include <fmt/core.h>
+#include <fmt/chrono.h>
+#include <fmt/ostream.h>
+
+namespace Mu {
+
+/*
+ * Separator characters used in various places; importantly,
+ * they are not used in UTF-8
+ */
+constexpr const auto SepaChar1 = '\xfe';
+constexpr const auto SepaChar2 = '\xff';
+
+/*
+ * Logging/printing/formatting functions connect libfmt with the Glib logging
+ * system. We wrap so perhaps at some point (C++23?) we can use std:: instead.
+ */
+
+/*
+ * Debug/error/warning logging
+ *
+ * The 'noexcept' means that they _wilL_ terminate the program
+ * when the formatting fails (ie. a bug)
+ */
+
+template<typename...T>
+void mu_debug(fmt::format_string<T...> frm, T&&... args) noexcept {
+ g_log("mu", G_LOG_LEVEL_DEBUG, "%s",
+ fmt::format(frm, std::forward<T>(args)...).c_str());
+}
+template<typename...T>
+void mu_info(fmt::format_string<T...> frm, T&&... args) noexcept {
+ g_log("mu", G_LOG_LEVEL_INFO, "%s",
+ fmt::format(frm, std::forward<T>(args)...).c_str());
+}
+template<typename...T>
+void mu_message(fmt::format_string<T...> frm, T&&... args) noexcept {
+ g_log("mu", G_LOG_LEVEL_MESSAGE, "%s",
+ fmt::format(frm, std::forward<T>(args)...).c_str());
+}
+template<typename...T>
+void mu_warning(fmt::format_string<T...> frm, T&&... args) noexcept {
+ g_log("mu", G_LOG_LEVEL_WARNING, "%s",
+ fmt::format(frm, std::forward<T>(args)...).c_str());
+}
+/* LCOV_EXCL_START*/
+template<typename...T>
+void mu_critical(fmt::format_string<T...> frm, T&&... args) noexcept {
+ g_log("mu", G_LOG_LEVEL_CRITICAL, "%s",
+ fmt::format(frm, std::forward<T>(args)...).c_str());
+}
+template<typename...T>
+void mu_error(fmt::format_string<T...> frm, T&&... args) noexcept {
+ g_log("mu", G_LOG_LEVEL_ERROR, "%s",
+ fmt::format(frm, std::forward<T>(args)...).c_str());
+}
+/* LCOV_EXCL_STOP*/
+
+/*
+ * Printing; add our wrapper functions, one day we might be able to use std::
+ */
+
+template<typename...T>
+void mu_print(fmt::format_string<T...> frm, T&&... args) noexcept {
+ fmt::print(frm, std::forward<T>(args)...);
+}
+template<typename...T>
+void mu_println(fmt::format_string<T...> frm, T&&... args) noexcept {
+ fmt::println(frm, std::forward<T>(args)...);
+}
+
+template<typename...T>
+void mu_printerr(fmt::format_string<T...> frm, T&&... args) noexcept {
+ fmt::print(stderr, frm, std::forward<T>(args)...);
+}
+template<typename...T>
+void mu_printerrln(fmt::format_string<T...> frm, T&&... args) noexcept {
+ fmt::println(stderr, frm, std::forward<T>(args)...);
+}
+
+
+/* stream */
+template<typename...T>
+void mu_print(std::ostream& os, fmt::format_string<T...> frm, T&&... args) noexcept {
+ fmt::print(os, frm, std::forward<T>(args)...);
+}
+template<typename...T>
+void mu_println(std::ostream& os, fmt::format_string<T...> frm, T&&... args) noexcept {
+ fmt::println(os, frm, std::forward<T>(args)...);
+}
+
+/*
+ * Fprmatting
+ */
+template<typename...T>
+std::string mu_format(fmt::format_string<T...> frm, T&&... args) noexcept {
+ return fmt::format(frm, std::forward<T>(args)...);
+}
+
+template<typename Range>
+auto mu_join(Range&& range, std::string_view sepa) {
+ return fmt::join(std::forward<Range>(range), sepa);
+}
+
+template <typename T=::time_t>
+std::tm mu_time(T t={}, bool use_utc=false) {
+ ::time_t tt{static_cast<::time_t>(t)};
+ return use_utc ? fmt::gmtime(tt) : fmt::localtime(tt);
+}
+
+using StringVec = std::vector<std::string>;
+
+/**
+ * Does the string contain script without explicit word separators?
+ *
+ * @param str a string
+ *
+ * @return true or false
+ */
+bool contains_unbroken_script(const char* str);
+static inline bool contains_unbroken_script(const std::string& str) {
+ return contains_unbroken_script(str.c_str());
+}
+
+/**
+ * Flatten a string -- down-case and fold diacritics.
+ *
+ * @param str a string
+ *
+ * @return a flattened string
+ */
+std::string utf8_flatten(const char* str);
+inline std::string
+utf8_flatten(const std::string& s) {
+ return utf8_flatten(s.c_str());
+}
+
+/**
+ * Replace all control characters with spaces, and remove leading and trailing space.
+ *
+ * @param dirty an unclean string
+ *
+ * @return a cleaned-up string.
+ */
+std::string utf8_clean(const std::string& dirty);
+
+
+/**
+ * Replace all wordbreak chars (as recognized by Xapian by single SPC)
+ *
+ * @param txt text
+ *
+ * @return string
+ */
+std::string utf8_wordbreak(const std::string& txt);
+
+
+/**
+ * Remove ctrl characters, replacing them with ' '; subsequent
+ * ctrl characters are replaced by a single ' '
+ *
+ * @param str a string
+ *
+ * @return the string without control characters
+ */
+std::string remove_ctrl(const std::string& str);
+
+/**
+ * Split a string in parts. As a special case, splitting an empty string
+ * yields an empty vector (not a vector with a single empty element)
+ *
+ * @param str a string
+ * @param sepa the separator
+ *
+ * @return the parts.
+ */
+std::vector<std::string> split(const std::string& str, const std::string& sepa);
+
+/**
+ * Split a string in parts. As a special case, splitting an empty string
+ * yields an empty vector (not a vector with a single empty element)
+ *
+ * @param str a string
+ * @param sepa the separator
+ *
+ * @return the parts.
+ */
+std::vector<std::string> split(const std::string& str, char sepa);
+
+/**
+ * Join the strings in svec into a string, separated by sepa
+ *
+ * @param svec a string vector
+ * @param sepa separator
+ *
+ * @return string
+ */
+std::string join(const std::vector<std::string>& svec, const std::string& sepa);
+static inline std::string join(const std::vector<std::string>& svec, char sepa) {
+ return join(svec, std::string(1, sepa));
+}
+
+/**
+ * write a string (assumed to be in utf8-format) to a stream,
+ * converted to the current locale
+ *
+ * @param str a string
+ * @param stream a stream
+ *
+ * @return true if printing worked, false otherwise
+ */
+bool fputs_encoded (const std::string& str, FILE *stream);
+
+/**
+ * print a fmt-style formatted string (assumed to be in utf8-format) to stdout,
+ * converted to the current locale
+ *
+ * @param a standard fmt-style format string, followed by a parameter list
+ *
+ * @return true if printing worked, false otherwise
+ */
+template<typename...T>
+static inline bool mu_print_encoded(fmt::format_string<T...> frm, T&&... args) noexcept {
+ return fputs_encoded(fmt::format(frm, std::forward<T>(args)...),
+ stdout);
+}
+
+/**
+ * Convert an int64_t to a time_t, clamping it within the range.
+ *
+ * This is only doing anything when using a 32-bit time_t value. This doesn't
+ * solve the 3038 problem, but at least allows for clearly marking where we
+ * convert
+ *
+ * @param t some 64-bit value that encodes a Unix time.
+ *
+ * @return a time_t value
+ */
+constexpr ::time_t time_t_min = 0;
+constexpr ::time_t time_t_max = std::numeric_limits<::time_t>::max();
+constexpr ::time_t to_time_t(int64_t t) {
+ return std::clamp(t,
+ static_cast<int64_t>(time_t_min),
+ static_cast<int64_t>(time_t_max));
+}
+
+
+/**
+ * Parse a date string to the corresponding time_t
+ * *
+ * @param date the date expressed a YYYYMMDDHHMMSS or any n... of the first
+ * characters, using the local timezone. Non-digits are ignored,
+ * so 2018-05-05 is equivalent to 20180505.
+ * @param first whether to fill out incomplete dates to the start (@true) or the
+ * end (@false); ie. either 1972 -> 197201010000 or 1972 -> 197212312359
+ * @param use_utc interpret @param date as UTC
+ *
+ * @return the corresponding time_t or Nothing if parsing failed.
+ */
+Option<::time_t> parse_date_time(const std::string& date, bool first, bool use_utc=false);
+
+/**
+ * Crudely convert HTML to plain text. This attempts to scrape the
+ * human-readable text from html-email so we can use it for indexing.
+ *
+ * @param html html
+ *
+ * @return plain text
+ */
+std::string html_to_text(const std::string& html);
+
+/**
+ * Hack to avoid locale crashes
+ *
+ * @return true if setting locale worked; false otherwise
+ */
+bool locale_workaround();
+
+
+/**
+ * Is the given timezone available? For tests
+ *
+ * @param tz a timezone, such as Europe/Helsinki
+ *
+ * @return true or false
+ */
+bool timezone_available(const std::string& tz);
+
+
+// https://stackoverflow.com/questions/19053351/how-do-i-use-a-custom-deleter-with-a-stdunique-ptr-member
+template <auto fn>
+struct deleter_from_fn {
+ template <typename T>
+ constexpr void operator()(T* arg) const {
+ fn(arg);
+ }
+};
+template <typename T, auto fn>
+using deletable_unique_ptr = std::unique_ptr<T, deleter_from_fn<fn>>;
+
+
+
+using Clock = std::chrono::steady_clock;
+using Duration = Clock::duration;
+
+template <typename Unit>
+constexpr int64_t
+to_unit(Duration d)
+{
+ using namespace std::chrono;
+ return duration_cast<Unit>(d).count();
+}
+
+constexpr int64_t
+to_s(Duration d)
+{
+ return to_unit<std::chrono::seconds>(d);
+}
+constexpr int64_t
+to_ms(Duration d)
+{
+ return to_unit<std::chrono::milliseconds>(d);
+}
+constexpr int64_t
+to_us(Duration d)
+{
+ return to_unit<std::chrono::microseconds>(d);
+}
+
+struct StopWatch {
+ using Clock = std::chrono::steady_clock;
+ StopWatch(const std::string name) : start_{Clock::now()}, name_{name} {}
+ ~StopWatch() {
+ const auto us{static_cast<double>(to_us(Clock::now() - start_))};
+ /* LCOV_EXCL_START*/
+ if (us > 2000000)
+ mu_debug("sw: {}: finished after {:.1f} s", name_, us / 1000000);
+ /* LCOV_EXCL_STOP*/
+ else if (us > 2000)
+ mu_debug("sw: {}: finished after {:.1f} ms", name_, us / 1000);
+ else
+ mu_debug("sw: {}: finished after {} us", name_, us);
+ }
+private:
+ Clock::time_point start_;
+ std::string name_;
+};
+
+/**
+ * Convert a size string to a size in bytes
+ *
+ * @param sizestr the size string
+ * @param first
+ *
+ * @return the size or Nothing if parsing failed
+ */
+Option<int64_t> parse_size(const std::string& sizestr, bool first);
+
+/**
+ * Convert a size into a size in bytes string
+ *
+ * @param size the size
+ * @param first
+ *
+ * @return the size expressed as a string with the decimal number of bytes
+ */
+std::string size_to_string(int64_t size);
+
+/**
+ * get a crude 'summary' of the string, ie. the first /n/ lines of the strings,
+ * with all newlines removed, replaced by single spaces
+ *
+ * @param str the source string
+ * @param max_lines the maximum number of lines to include in the summary
+ *
+ * @return a newly allocated string with the summary. use g_free to free it.
+ */
+std::string summarize(const std::string& str, size_t max_lines);
+
+
+/**
+ * Quote & escape a string for " and \
+ *
+ * @param str a string
+ *
+ * @return quoted string
+ */
+std::string quote(const std::string& str);
+
+
+/**
+ * Convert any ostreamable<< value to a string
+ *
+ * @param t the value
+ *
+ * @return a std::string
+ */
+template <typename T>
+static inline std::string
+to_string(const T& val)
+{
+ std::stringstream sstr;
+ sstr << val;
+
+ return sstr.str();
+}
+/**
+ * Convert to std::string to a std::string_view
+ * Careful with the lifetimes!
+ *
+ * @param s a string
+ *
+ * @return a string_view
+ */
+static inline std::string_view
+to_string_view(const std::string& s)
+{
+ return std::string_view{s.data(), s.size()};
+}
+
+/**
+ * Consume a gchar and return a std::string
+ *
+ * @param str a gchar* (consumed/freed)
+ *
+ * @return a std::string, empty if gchar was {}
+ */
+static inline std::string
+to_string_gchar(gchar*&& str)
+{
+ std::string s(str?str:"");
+ g_free(str);
+ return s;
+}
+
+
+/*
+ * Lexnums are lexicographically sortable string representations of non-negative
+ * integers. Start with 'f' + length of hex-representation number, followed by
+ * the hex representation itself. So,
+ *
+ * 0 -> 'g0'
+ * 1 -> 'g1'
+ * 10 -> 'ga'
+ * 16 -> 'h10'
+ *
+ * etc.
+ */
+std::string to_lexnum(int64_t val);
+int64_t from_lexnum(const std::string& str);
+
+/**
+ * Like std::find_if, but using sequence instead of a range.
+ *
+ * @param seq some std::find_if compatible sequence
+ * @param pred a predicate
+ *
+ * @return an iterator
+ */
+template<typename Sequence, typename UnaryPredicate>
+typename Sequence::const_iterator seq_find_if(const Sequence& seq, UnaryPredicate pred) {
+ return std::find_if(seq.cbegin(), seq.cend(), pred);
+}
+
+/**
+ * Is pred(element) true for at least one element of sequence?
+ *
+ * @param seq sequence
+ * @param pred a predicate
+ *
+ * @return true or false
+ */
+template<typename Sequence, typename UnaryPredicate>
+bool seq_some(const Sequence& seq, UnaryPredicate pred) {
+ return seq_find_if(seq, pred) != seq.cend();
+}
+
+/**
+ * Create a sequence that has all element of seq for which pred is true
+ *
+ * @param seq sequence
+ * @param pred false
+ *
+ * @return sequence
+ */
+template<typename Sequence, typename UnaryPredicate>
+Sequence seq_filter(const Sequence& seq, UnaryPredicate pred) {
+ Sequence res;
+ std::copy_if(seq.begin(), seq.end(), std::back_inserter(res), pred);
+ return res;
+}
+
+/**
+ * Create a sequence that has all element of seq for which pred is false
+ *
+ * @param seq sequence
+ * @param pred false
+ *
+ * @return sequence
+ */
+template<typename Sequence, typename UnaryPredicate>
+Sequence seq_remove(const Sequence& seq, UnaryPredicate pred) {
+ Sequence res;
+ std::remove_copy_if(seq.begin(), seq.end(), std::back_inserter(res), pred);
+ return res;
+}
+
+template<typename Sequence, typename Compare>
+void seq_sort(Sequence& seq, Compare cmp) { std::sort(seq.begin(), seq.end(), cmp); }
+
+
+/**
+ * Like std::accumulate, but using a sequence instead of a range.
+ *
+ * @param seq some std::accumulate compatible sequence
+ * @param init the initial value
+ * @param op binary operation to calculate the next element
+ *
+ * @return the result value.
+ */
+template<typename Sequence, typename ResultType, typename BinaryOp>
+ResultType seq_fold(const Sequence& seq, ResultType init, BinaryOp op) {
+ return std::accumulate(seq.cbegin(), seq.cend(), init, op);
+}
+
+template<typename Sequence, typename UnaryOp>
+void seq_for_each(const Sequence& seq, UnaryOp op) {
+ std::for_each(seq.cbegin(), seq.cend(), op);
+}
+
+struct MaybeAnsi {
+ explicit MaybeAnsi(bool use_color) : color_{use_color} {}
+
+ enum struct Color {
+ Black = 30,
+ Red = 31,
+ Green = 32,
+ Yellow = 33,
+ Blue = 34,
+ Magenta = 35,
+ Cyan = 36,
+ White = 37,
+
+ BrightBlack = 90,
+ BrightRed = 91,
+ BrightGreen = 92,
+ BrightYellow = 93,
+ BrightBlue = 94,
+ BrightMagenta = 95,
+ BrightCyan = 96,
+ BrightWhite = 97,
+ };
+
+ std::string fg(Color c) const { return ansi(c, true); }
+ std::string bg(Color c) const { return ansi(c, false); }
+
+ std::string reset() const { return color_ ? "\x1b[0m" : ""; }
+
+private:
+ std::string ansi(Color c, bool fg = true) const
+ {
+ return color_ ? mu_format("\x1b[{}m",
+ static_cast<int>(c) + (fg ? 0 : 10)) : "";
+ }
+
+ const bool color_;
+};
+
+#define MU_COLOR_RED "\x1b[31m"
+#define MU_COLOR_GREEN "\x1b[32m"
+#define MU_COLOR_YELLOW "\x1b[33m"
+#define MU_COLOR_BLUE "\x1b[34m"
+#define MU_COLOR_MAGENTA "\x1b[35m"
+#define MU_COLOR_CYAN "\x1b[36m"
+#define MU_COLOR_DEFAULT "\x1b[0m"
+
+
+/// Allow using enum structs as bitflags
+#define MU_TO_NUM(ET, ELM) std::underlying_type_t<ET>(ELM)
+#define MU_TO_ENUM(ET, NUM) static_cast<ET>(NUM)
+#define MU_ENABLE_BITOPS(ET) \
+ constexpr ET operator&(ET e1, ET e2) { \
+ return MU_TO_ENUM(ET, MU_TO_NUM(ET, e1) & MU_TO_NUM(ET, e2)); \
+ } \
+ constexpr ET operator|(ET e1, ET e2) { \
+ return MU_TO_ENUM(ET, MU_TO_NUM(ET, e1) | MU_TO_NUM(ET, e2)); \
+ } \
+ constexpr ET operator~(ET e) { return MU_TO_ENUM(ET, ~(MU_TO_NUM(ET, e))); } \
+ constexpr bool any_of(ET e) { return MU_TO_NUM(ET, e) != 0; } \
+ constexpr bool none_of(ET e) { return MU_TO_NUM(ET, e) == 0; } \
+ constexpr bool one_of(ET e1, ET e2) { return (e1 & e2) == e2; } \
+ constexpr ET& operator&=(ET& e1, ET e2) { return e1 = e1 & e2; } \
+ constexpr ET& operator|=(ET& e1, ET e2) { return e1 = e1 | e2; } \
+ static_assert(1==1) // require a semicolon
+
+} // namespace Mu
+
+#endif /* MU_UTILS_HH__ */
--- /dev/null
+## Copyright (C) 2021-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## 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, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+
+################################################################################
+# tests
+
+
+#
+# tests
+#
+test('test-sexp',
+ executable('test-sexp', '../mu-sexp.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_utils_dep]))
+
+test('test-regex',
+ executable('test-regex', '../mu-regex.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_utils_dep]))
+
+test('test-command-handler',
+ executable('test-command-handler', '../mu-command-handler.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_utils_dep]))
+
+test('test-utils-file',
+ executable('test-utils-file', '../mu-utils-file.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, gio_unix_dep,config_h_dep, lib_mu_utils_dep]))
+
+test('test-logger',
+ executable('test-logger', '../mu-logger.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_utils_dep, thread_dep ]))
+
+test('test-option',
+ executable('test-option', '../mu-option.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_utils_dep ]))
+
+test('test-lang-detector',
+ executable('test-lang-detector', '../mu-lang-detector.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [ config_h_dep, glib_dep, lib_mu_utils_dep ]))
+
+test('test-html-to-text',
+ executable('test-html-to-text', '../mu-html-to-text.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_utils_dep]))
+
+test('test-error',
+ executable('test-error', '../mu-error.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_utils_dep]))
+
+test('test-mu-utils',
+ executable('test-mu-utils',
+ 'test-utils.cc',
+ install: false,
+ dependencies: [glib_dep, lib_mu_utils_dep]))
--- /dev/null
+/*
+** Copyright (C) 2017-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** This library is free software; you can redistribute it and/or
+** modify it under the terms of the GNU Lesser General Public License
+** as published by the Free Software Foundation; either version 2.1
+** of the License, or (at your option) any later version.
+**
+** This library 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
+** Lesser General Public License for more details.
+**
+** You should have received a copy of the GNU Lesser General Public
+** License along with this library; if not, write to the Free
+** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
+** 02110-1301, USA.
+*/
+
+#include <vector>
+#include <glib.h>
+
+#include <iostream>
+#include <sstream>
+#include <functional>
+#include <array>
+
+#include "mu-utils.hh"
+#include "mu-test-utils.hh"
+#include "mu-error.hh"
+
+using namespace Mu;
+
+
+struct Case {
+ const std::string expr;
+ bool is_first{};
+ const std::string expected;
+};
+using CaseVec = std::vector<Case>;
+using ProcFunc = std::function<std::string(std::string, bool)>;
+
+static void
+test_cases(const CaseVec& cases, ProcFunc proc)
+{
+ for (const auto& casus : cases) {
+ const auto res = proc(casus.expr, casus.is_first);
+ //mu_println("'{}'\n'{}'", casus.expected, res);
+ assert_equal(casus.expected, res);
+ }
+}
+
+static void
+test_date_basic()
+{
+ const auto hki = "Europe/Helsinki";
+
+ // ensure we have the needed TZ or skip the test.
+ if (!timezone_available(hki)) {
+ g_test_skip("timezone Europe/Helsinki not available");
+ return;
+ }
+
+ g_setenv("TZ", hki, TRUE);
+ std::vector<std::tuple<const char*, bool/*is_first*/, ::time_t>> cases = {{
+ {"2015-09-18T09:10:23", true, 1442556623},
+ {"1972-12-14T09:10:23", true, 93165023},
+ {"1972-12-14T09:10", true, 93165000},
+ {"1854-11-18T17:10:23", true, 0},
+
+ {"2000-02-31T09:10:23", true, 951861599},
+ {"2000-02-29T23:59:59", true, 951861599},
+
+ {"20220602", true, 1654117200},
+ {"20220605", false, 1654462799},
+
+ {"202206", true, 1654030800},
+ {"202206", false, 1656622799},
+
+ {"2016", true, 1451599200},
+ {"2016", false, 1483221599},
+
+ // {"fnorb", true, -1},
+ // {"fnorb", false, -1},
+ {"", false, time_t_max},
+ {"", true, time_t_min}
+ }};
+
+ for (auto& test: cases) {
+ if (g_test_verbose())
+ g_debug("checking %s", std::get<0>(test));
+ g_assert_cmpuint(parse_date_time(std::get<0>(test),
+ std::get<1>(test)).value_or(-1),==,
+ std::get<2>(test));
+ }
+}
+
+static void
+test_date_ymwdhMs(void)
+{
+ struct testcase {
+ std::string expr;
+ int64_t diff;
+ int tolerance;
+ };
+
+ std::array<testcase, 7> cases = {{
+ {"7s", 7, 1},
+ {"3M", 3 * 60, 1},
+ {"3h", 3 * 60 * 60, 1},
+ {"21d", 21 * 24 * 60 * 60, 3600 + 1},
+ {"2w", 2 * 7 * 24 * 60 * 60, 3600 + 1},
+ {"2y", 2 * 365 * 24 * 60 * 60, 24 * 3600 + 1},
+ {"3m", 3 * 30 * 24 * 60 * 60, 3 * 24 * 3600 + 1}
+ }};
+
+ for (auto&& tcase: cases) {
+ const auto date = parse_date_time(tcase.expr, true);
+ g_assert_true(date);
+ const auto diff = ::time({}) - *date;
+ if (g_test_verbose())
+ std::cerr << tcase.expr << ' ' << diff << ' ' << tcase.diff << '\n';
+
+ g_assert_true(tcase.diff - diff <= tcase.tolerance);
+ }
+
+ // note: perhaps it'd be nice if we'd detect this error;
+ // currently we're being rather tolerant
+ // g_assert_false(!!parse_date_time("25q", false));
+}
+
+static void
+test_parse_size()
+{
+ constexpr std::array<std::tuple<const char*, bool, int64_t>, 6> cases = {{
+ { "456", false, 456 },
+ { "", false, G_MAXINT64 },
+ { "", true, 0 },
+ { "2K", false, 2048 },
+ { "2M", true, 2097152 },
+ { "5G", true, 5368709120 }
+ }};
+ for(auto&& test: cases) {
+ g_assert_cmpint(parse_size(std::get<0>(test), std::get<1>(test))
+ .value_or(-1), ==, std::get<2>(test));
+ }
+
+ g_assert_false(!!parse_size("-1", true));
+ g_assert_false(!!parse_size("scoobydoobydoo", false));
+}
+
+static void
+test_flatten()
+{
+ CaseVec cases = {
+ {"Менделе́ев", true, "менделеев"},
+ {"", true, ""},
+ {"Ångström", true, "angstrom"},
+ {"đodø", true, "dodo"},
+
+ // don't touch combining characters in CJK etc.
+ {"スポンサーシップ募集",true, "スポンサーシップ募集"}
+ };
+
+ test_cases(cases, [](auto s, auto f) { return utf8_flatten(s); });
+}
+
+static void
+test_remove_ctrl()
+{
+ CaseVec cases = {
+ {"Foo\n\nbar", true, "Foo bar"},
+ {"", false, ""},
+ {" ", false, " "},
+ {"Hello World ", false, "Hello World "},
+ {"Ångström", false, "Ångström"},
+ };
+
+ test_cases(cases, [](auto s, auto f) { return remove_ctrl(s); });
+}
+
+static void
+test_clean()
+{
+ CaseVec cases = {
+ {"\t a\t\nb ", true, "a b"},
+ {"", true, ""},
+ {"Ångström", true, "Ångström"},
+ {"\345\245", true, ".."},
+ };
+
+ test_cases(cases, [](auto s, auto f) { return utf8_clean(s); });
+}
+
+
+static void
+test_word_break()
+{
+ CaseVec cases = {
+ {"aap+noot&mies", true, "aap noot mies"},
+ {"hallo", true, "hallo"},
+ {" foo-bar###cuux,fnorb ", true, "foo bar cuux fnorb"},
+ {"eyes\nof\tMedusa", true, "eyes of Medusa"},
+ };
+
+ test_cases(cases, [](auto s, auto f) { return utf8_wordbreak(s); });
+}
+
+
+static void
+test_format()
+{
+ g_assert_true(mu_format("hello {}", "world") == "hello world");
+ g_assert_true(mu_format("hello {}, {}", "world", 123) == "hello world, 123");
+}
+
+static void
+test_split()
+{
+ using svec = std::vector<std::string>;
+ auto assert_equal_svec=[](const svec& sv1, const svec& sv2) {
+ g_assert_cmpuint(sv1.size(),==,sv2.size());
+ for (auto i = 0U; i != sv1.size(); ++i)
+ g_assert_cmpstr(sv1[i].c_str(),==,sv2[i].c_str());
+ };
+
+ // string sepa
+ assert_equal_svec(split("axbxc", "x"), {"a", "b", "c"});
+ assert_equal_svec(split("axbxcx", "x"), {"a", "b", "c", ""});
+ assert_equal_svec(split("", "boo"), {});
+ assert_equal_svec(split("ayybyyc", "yy"), {"a", "b", "c"});
+ assert_equal_svec(split("abc", ""), {"a", "b", "c"});
+ assert_equal_svec(split("", "boo"), {});
+
+ // char sepa
+ assert_equal_svec(split("axbxc", 'x'), {"a", "b", "c"});
+ assert_equal_svec(split("axbxcx", 'x'), {"a", "b", "c", ""});
+}
+
+static void
+test_join()
+{
+ assert_equal(join({"a", "b", "c"}, "x"), "axbxc");
+ assert_equal(join({"a", "b", "c"}, ""), "abc");
+ assert_equal(join({},"foo"), "");
+ assert_equal(join({"d", "e", "f"}, "foo"), "dfooefoof");
+}
+
+
+enum struct Bits { None = 0, Bit1 = 1 << 0, Bit2 = 1 << 1 };
+MU_ENABLE_BITOPS(Bits);
+
+static void
+test_define_bitmap()
+{
+ g_assert_cmpuint((guint)Bits::None, ==, (guint)0);
+ g_assert_cmpuint((guint)Bits::Bit1, ==, (guint)1);
+ g_assert_cmpuint((guint)Bits::Bit2, ==, (guint)2);
+
+ g_assert_cmpuint((guint)(Bits::Bit1 | Bits::Bit2), ==, (guint)3);
+ g_assert_cmpuint((guint)(Bits::Bit1 & Bits::Bit2), ==, (guint)0);
+
+ g_assert_cmpuint((guint)(Bits::Bit1 & (~Bits::Bit2)), ==, (guint)1);
+
+ {
+ Bits b{Bits::Bit1};
+ b |= Bits::Bit2;
+ g_assert_cmpuint((guint)b, ==, (guint)3);
+ }
+
+ {
+ Bits b{Bits::Bit1};
+ b &= Bits::Bit1;
+ g_assert_cmpuint((guint)b, ==, (guint)1);
+ }
+}
+
+static void
+test_to_from_lexnum()
+{
+ assert_equal(to_lexnum(0), "g0");
+ assert_equal(to_lexnum(100), "h64");
+ assert_equal(to_lexnum(12345), "j3039");
+
+ g_assert_cmpuint(from_lexnum(to_lexnum(0)), ==, 0);
+ g_assert_cmpuint(from_lexnum(to_lexnum(7777)), ==, 7777);
+ g_assert_cmpuint(from_lexnum(to_lexnum(9876543)), ==, 9876543);
+}
+
+static void
+test_locale_workaround()
+{
+ g_assert_true(locale_workaround());
+
+ g_setenv("LC_ALL", "BOO", 1);
+
+ g_assert_true(locale_workaround());
+}
+
+
+static void
+test_summarize(void)
+{
+ const char *txt =
+ "Khiron was fortified and made the seat of a pargana during "
+ "the reign of Asaf-ud-Daula.\n\the headquarters had previously "
+ "been at Satanpur since its foundation and fortification by "
+ "the Bais raja Sathna.\n\nKhiron was also historically the seat "
+ "of a taluqdari estate belonging to a Janwar dynasty.\n"
+ "There were also several Kayasth qanungo families, "
+ "including many descended from Rai Sahib Rai, who had been "
+ "a chakladar under the Nawabs of Awadh.";
+
+ const auto summ = summarize(txt, 3);
+ g_assert_cmpstr(summ.c_str(), ==,
+ "Khiron was fortified and made the seat of a pargana "
+ "during the reign of Asaf-ud-Daula. he headquarters had "
+ "previously been at Satanpur since its foundation and "
+ "fortification by the Bais raja Sathna. ");
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/utils/date-basic", test_date_basic);
+ g_test_add_func("/utils/date-ymwdhMs", test_date_ymwdhMs);
+ g_test_add_func("/utils/parse-size", test_parse_size);
+ g_test_add_func("/utils/flatten", test_flatten);
+ g_test_add_func("/utils/remove-ctrl", test_remove_ctrl);
+ g_test_add_func("/utils/clean", test_clean);
+ g_test_add_func("/utils/word-break", test_word_break);
+ g_test_add_func("/utils/format", test_format);
+ g_test_add_func("/utils/summarize", test_summarize);
+ g_test_add_func("/utils/split", test_split);
+ g_test_add_func("/utils/join", test_join);
+ g_test_add_func("/utils/define-bitmap", test_define_bitmap);
+ g_test_add_func("/utils/to-from-lexnum", test_to_from_lexnum);
+ g_test_add_func("/utils/locale-workaround", test_locale_workaround);
+
+ return g_test_run();
+}
--- /dev/null
+* AUTHOR
+
+Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+# Local Variables:
+# mode: org
+# End:
--- /dev/null
+* REPORTING BUGS
+
+Please report bugs at <https://github.com/djcb/mu/issues>.
+
+# Local Variables:
+# mode: org
+# End:
--- /dev/null
+* COMMON OPTIONS
+
+** -d, --debug
+makes mu generate extra debug information, useful for debugging the program
+itself. By default, debug information goes to the log file, ~/.cache/mu/mu.log.
+It can safely be deleted when mu is not running. When running with --debug
+option, the log file can grow rather quickly. See the note on logging below.
+
+** -q, --quiet
+causes mu not to output informational messages and progress information to
+standard output, but only to the log file. Error messages will still be sent to
+standard error. Note that mu index is much faster with --quiet, so it is
+recommended you use this option when using mu from scripts etc.
+
+** --log-stderr
+causes mu to not output log messages to standard error, in addition to sending
+them to the log file.
+
+** --nocolor
+do not use ANSI colors. The environment variable ~NO_COLOR~ can be used as an
+alternative to ~--nocolor~.
+
+** -V, --version
+prints mu version and copyright information.
+
+** -h, --help
+lists the various command line options.
+
+# Local Variables:
+# mode: org
+# End:
--- /dev/null
+* COPYRIGHT
+
+This manpage is part of ~mu~ @VERSION@.
+
+Copyright © 2008-@YEAR@ Dirk-Jan C. Binnema. License GPLv3+: GNU GPL version 3
+or later <https://gnu.org/licenses/gpl.html>. This is free software: you are
+free to change and redistribute it. There is NO WARRANTY, to the extent
+permitted by law.
+
+# Local Variables:
+# mode: org
+# End:
--- /dev/null
+* EXIT CODE
+
+This command returns 0 upon successful completion, or a non-zero exit code
+otherwise.
+
+ 0. success
+ 2. no matches found. Try a different query
+ 11. database schema mismatch. You need to re-initialize ~mu~, see *mu-init(1)*
+ 19. failed to acquire lock. Some other program has exclusive access to the mu database
+ 99. caught an exception
+
+# Local Variables:
+# mode: org
+# End:
--- /dev/null
+## Copyright (C) 2021-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## 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, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+#
+# generate org include files
+#
+man_data=configuration_data()
+man_data.set('VERSION', meson.project_version())
+man_data.set('YEAR', mu_year)
+incs=[
+ 'author.inc',
+ 'bugs.inc',
+ 'common-options.inc',
+ 'copyright.inc.in',
+ 'exit-code.inc',
+ 'muhome.inc',
+ 'prefooter.inc',
+]
+foreach inc: incs
+ # configure the .in ones
+ if inc.substring(-3) == '.in'
+ configure_file(input: inc,
+ output: '@BASENAME@',
+ configuration: man_data)
+ else # and copy the rest
+ configure_file(input: inc, output:'@BASENAME@.inc',
+ copy:true)
+ endif
+endforeach
+
+# man-pages is org-format.
+man_orgs=[
+ 'mu.1.org',
+ 'mu-add.1.org',
+ 'mu-bookmarks.5.org',
+ 'mu-cfind.1.org',
+ 'mu-easy.7.org',
+ 'mu-extract.1.org',
+ 'mu-find.1.org',
+ 'mu-help.1.org',
+ 'mu-index.1.org',
+ 'mu-info.1.org',
+ 'mu-init.1.org',
+ 'mu-mkdir.1.org',
+ 'mu-move.1.org',
+ 'mu-query.7.org',
+ 'mu-remove.1.org',
+ 'mu-server.1.org',
+ 'mu-verify.1.org',
+ 'mu-view.1.org'
+]
+
+foreach src : man_orgs
+ # meson makes in tricky to use the results of e.g. configure_file
+ # in custom_commands..., so this is admittedly a little hacky.
+ org = join_paths(meson.current_build_dir(), src)
+ man = '@BASENAME@'
+ section = src.substring(-5, -4)
+
+ # we fill in some man-page details:
+ # @SECTION_ID@: the man-page section
+ # @MAN_DATE@: date of the generation (not yet supported by ox-man)
+ conf_data = configuration_data()
+ conf_data.set('SECTION_ID', section)
+ conf_data.set('MAN_DATE', mu_month_year)
+ configure_file(input: src, output:'@BASENAME@.org',
+ configuration: conf_data)
+
+ expr_tmpl = ''.join([
+ '(progn',
+ ' (require \'ox-man)',
+ ' (setq org-export-with-sub-superscripts \'{})',
+ ' (org-export-to-file \'man "@0@"))'])
+ expr = expr_tmpl.format(org.substring(0,-4))
+ sectiondir = join_paths(mandir, 'man' + section)
+
+ custom_target(src + '-to-man',
+ build_by_default: true,
+ input: src,
+ output: '@BASENAME@',
+ install: true,
+ install_dir: sectiondir,
+ depend_files: incs,
+ command: [emacs,
+ '--no-init-file',
+ '--batch',
+ org,
+ '--eval', expr])
+endforeach
--- /dev/null
+#+TITLE: MU ADD
+#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
+
+* NAME
+
+mu-add - add one or more messages to the database
+
+* SYNOPSIS
+
+*mu [common-options] add [options] <file> [<files>]*
+
+* DESCRIPTION
+
+~mu add~ is the command to add specific message files to the database. Each file
+must be specified with an absolute path.
+
+* ADD OPTIONS
+
+#+include: "muhome.inc" :minlevel 2
+
+#+include: "common-options.inc" :minlevel 1
+
+#+include: "exit-code.inc" :minlevel 1
+
+#+include: "prefooter.inc" :minlevel 1
+
+* SEE ALSO
+
+*mu(1)*, *mu-index(1)*, *mu-remove(1)*
--- /dev/null
+#+TITLE: MU BOOKMARKS
+#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
+
+* NAME
+
+mu-bookmarks - file with bookmarks (shortcuts) for mu search expressions
+
+* DESCRIPTION
+
+Bookmarks are named shortcuts for search queries. They allow using a convenient
+name for often-used queries. The bookmarks are also visible as shortcuts in the
+mu experimental user interfaces, =mug= and =mug2=.
+
+The bookmarks file is read from =<muhome>/bookmarks=. On Unix this would typically
+be w be =~/.config/mu/bookmarks=, but this can be influenced using the ~--muhome~
+parameter for *mu-find(1)*.
+
+The bookmarks file is a typical key=value *.ini*-file, which is best shown by
+means of an example:
+
+#+begin_example
+[mu]
+inbox=maildir:/inbox # inbox
+oldhat=maildir:/archive subject:hat # archived with subject containing 'hat'
+#+end_example
+
+The *[mu]* group header is required. For practical uses of bookmarks, see
+*mu-find(1)*.
+
+#+include: "author.inc" :minlevel 1
+
+#+include: "copyright.inc" :minlevel 1
+
+* SEE ALSO
+
+*mu(1)*, *mu-find(1)*
--- /dev/null
+#+TITLE: MU CFIND
+#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
+
+* NAME
+
+mu-cfind - find contacts in the *mu* database and export them
+for use in other programs.
+
+* SYNOPSIS
+
+*mu [common-options] cfind [options] [<pattern>]*
+
+* DESCRIPTION
+
+*mu cfind* is the *mu* command for finding =contacts= (name and e-mail address of
+people who were either an e-mail's sender or receiver). There are different
+output formats available, for importing the contacts into other programs.
+
+* SEARCHING CONTACTS
+
+When you index your messages (see *mu index*), *mu* creates a list of unique e-mail
+addresses found and the accompanying name, and caches this list. In case the
+same e-mail address is used with different names, the most recent non-empty name
+is used.
+
+*mu cfind* starts a search for contacts that match a =regular expression=. For
+example:
+
+#+begin_example
+$ mu cfind '@gmail\.com'
+#+end_example
+
+would find all contacts with a gmail-address, while
+
+#+begin_example
+$ mu cfind Mary
+#+end_example
+
+lists all contacts with Mary in either name or e-mail address.
+
+If you do not specify a search expression, *mu cfind* returns the full list of
+contacts. Note, *mu cfind* uses a cache with the e-mail information, which is
+populated during the indexing process.
+
+The regular expressions are basic case-insensitive PCRE, see *pcre(3)*.
+
+* CFIND OPTIONS
+
+** --format=plain|mutt-alias|mutt-ab|wl|org-contact|bbdb|csv
+sets the output format to the given value. The following are available:
+
+#+ATTR_MAN: :disable-caption t
+| --format= | description |
+|-------------+-----------------------------------|
+| plain | default, simple list |
+| mutt-alias | mutt alias-format |
+| mutt-ab | mutt external address book format |
+| wl | wanderlust addressbook format |
+| org-contact | org-mode org-contact format |
+| bbdb | BBDB format |
+| csv | comma-separated values [1] |
+| json | JSON format |
+
+
+[1] *CSV* is not fully standardized, but *mu cfind* follows some common practices:
+any double-quote is replaced by a double-double quote (thus, "hello" become
+""hello"", and fields with commas are put in double-quotes. Normally, this
+should only apply to name fields.
+
+** --personal,-p only show addresses seen in messages where one of `my' e-mail
+addresses was seen in one of the address fields; this is to exclude addresses
+only seen in mailing-list messages. See the ~--my-address~ parameter to *mu init*.
+
+** --after=<timestamp> only show addresses last seen after
+=<timestamp>=. =<timestamp>= is a UNIX *time_t* value, the number of
+seconds since 1970-01-01 (in UTC).
+
+From the command line, you can use the *date* command to get this value. For
+example, only consider addresses last seen after 2020-06-01, you could specify
+#+begin_example
+ --after=`date +%s --date='2020-06-01'`
+#+end_example
+
+#+include: "muhome.inc" :minlevel 2
+
+#+include: "common-options.inc" :minlevel 1
+
+* JSON FORMAT
+
+With ~--format=json~, the matching contacts come out as a JSON array, e.g.,
+#+begin_example
+[
+ {
+ "email" : "syb@example.com",
+ "name" : "Sybil Gerard",
+ "display" : "Sybil Gerard <syb@example.com>",
+ "last-seen" : 1075982687,
+ "last-seen-iso" : "2004-02-05T14:04:47Z",
+ "personal" : false,
+ "frequency" : 14
+ },
+ {
+ "email" : "ed@example.com",
+ "name" : "Mallory, Edward",
+ "display" : "\"Mallory, Edward\" <ed@example.com>",
+ "last-seen" : 1425991805,
+ "last-seen-iso" : "2015-03-10T14:50:05Z",
+ "personal" : true,
+ "frequency" : 2
+ }
+]
+#+end_example
+
+Each contact has the following fields:
+
+#+ATTR_MAN: :disable-caption t
+| property | description |
+|---------------+--------------------------------------------------------------------------|
+| ~email~ | the email-address |
+| ~name~ | the name (or ~none~) |
+| ~display~ | the combination name and e-mail address for display purposes |
+| ~last-seen~ | date of most recent message with this contact (Unix time) |
+| ~last-seen-iso~ | ~last-seen~ represented as an ISO-8601 timestamp |
+| ~personal~ | whether the email was seen in a message together with a personal address |
+| ~frequency~ | approximation of the number of times this contact was seen in messages |
+
+The JSON format is useful for further processing, e.g. using the *jq(1)* tool:
+
+List display names, sorted by their last-seen date:
+#+begin_example
+$ mu cfind --format=json --personal | jq -r '.[] | ."last-seen-iso" + " " + .display' | sort
+#+end_example
+
+* INTEGRATION WITH MUTT
+
+You can use *mu cfind* as an external address book server for *mutt*.
+For this to work, add the following to your =muttrc=:
+
+#+begin_example
+set query_command = "mu cfind --format=mutt-ab '%s'"
+#+end_example
+
+Now, in mutt, you can search for e-mail addresses using the *query*-command,
+which is (by default) accessible by pressing *Q*.
+
+* ENCODING
+
+*mu cfind* output is encoded according to the current locale except for
+=--format=bbdb=. This is hard-coded to UTF-8, and as such specified in the
+output-file, so emacs/bbdb can handle things correctly, without guessing.
+
+#+include: "exit-code.inc" :minlevel 1
+
+#+include: "bugs.inc" :minlevel 1
+
+#+include: "author.inc" :minlevel 1
+
+#+include: "copyright.inc" :minlevel 1
+
+* SEE ALSO
+*mu(1)*, *mu-index(1)*, *mu-find(1)*, *pcre(3)*, *jq(1)*
--- /dev/null
+#+TITLE: MU EASY
+#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
+
+* NAME
+
+mu-easy - a quick introduction to mu
+
+* DESCRIPTION
+
+*mu* is a set of tools for dealing with e-mail messages in Maildirs. There are
+many options, which are all described in the man pages for the various
+sub-commands. This man pages jumps over all of the details and gives examples of
+some common use cases. If the use cases described here do not precisely do what
+you want, please check the more extensive information in the man page about the
+sub-command you are using -- for example, the *mu-index(1)* or *mu-find(1)* man
+pages.
+
+*NOTE*: the *index* command (and therefore, the ones that depend on that, such as
+*find*), require that you store your mail in the Maildir-format. If you don't do
+so, you can still use the other commands, but you won't be able to index/search
+your mail.
+
+By default, *mu* uses colorized output when it thinks your terminal is capable of
+doing so. If you don't like color, you can use the *--nocolor* command-line
+option, or set either the *MU_NOCOLOR* or the *NO_COLOR* environment variable to
+non-empty.
+
+* SETTING THINGS UP
+
+The first time you run the mu commands, you need to initialize it. This is done
+with the *init* command.
+
+#+begin_example
+$ mu init
+#+end_example
+
+This uses the defaults (see *mu-init(1)* for details on how to change that).
+
+
+* INDEXING YOUR E-MAIL
+
+Before you can search e-mails, you'll first need to index them:
+
+#+begin_example
+$ mu index
+#+end_example
+
+The process can take a few minutes, depending on the amount of mail you have,
+the speed of your computer, hard drive etc. Usually, indexing should be able to
+reach a speed of a few hundred messages per second.
+
+*mu index* guesses the top-level Maildir to do its job; if it guesses wrong, you
+can use the =--maildir= option to specify the top-level directory that should be
+processed. See the *mu-index(1)* man page for more details.
+
+Normally, *mu index* visits all the directories under the top-level Maildir;
+however, you can exclude certain directories (say, the `trash' or `spam'
+folders) by creating a file called =.noindex= in the directory. When *mu* sees such
+a file, it will exclude this directory and its sub-directories from indexing.
+Also see *.noupdate* in the *mu-index(1)* manpage.
+
+* SEARCHING YOUR E-MAIL
+
+After you have indexed your mail, you can start searching it. By default, the
+search results are printed on standard output. Alternatively, the output can
+take the form of Maildir with symbolic links to the found messages. This enables
+integration with e-mail clients; see the *mu-find(1)* man page for details, the
+syntax of the search parameters and so on. Here, we just give some examples for
+common cases.
+
+You can use the *mu fields* command to get information about all possible fields
+and flags.
+
+First, let's search for all messages sent to Julius (Caesar) regarding fruit:
+
+#+begin_example
+$ mu find t:julius fruit
+#+end_example
+
+This should return something like:
+
+#+begin_example
+2008-07-31T21:57:25 EEST John Milton <jm@example.com> Fere libenter homines id quod volunt credunt
+#+end_example
+
+This means there is a message to `julius' with `fruit' somewhere in the message.
+In this case, it's a message from John Milton. Note that the date format depends
+on your the language/locale you are using.
+
+How do we know that the message was sent to Julius Caesar? Well, it's not
+visible from the results above, because the default fields that are shown are
+date/sender/subject. However, we can change this using the =--fields= parameter
+(try *mu fields* to see all the details):
+
+#+begin_example
+$ mu find --fields="t s" t:julius fruit
+#+end_example
+
+In other words, display the `To:'-field (t) and the subject (s). This should
+return something like:
+#+begin_example
+Julius Caesar <jc@example.com> Fere libenter homines id quod volunt credunt
+#+end_example
+
+This is the same message found before, only with some different fields
+displayed.
+
+By default, *mu* uses the logical ~AND~ for the search parameters -- that is, it
+displays messages that match all the parameters. However, we can use logical ~OR~
+as well:
+
+#+begin_example
+$ mu find t:julius OR f:socrates
+#+end_example
+
+In other words, display messages that are either sent to Julius Caesar *or* are
+from Socrates. This could return something like:
+
+#+begin_example
+2008-07-31T21:57:25 EEST Socrates <soc@example.com> cool stuff
+2008-07-31T21:57:25 EEST John Milton <jm@example.com> Fere libenter homines id quod volunt credunt
+#+end_example
+
+What if we want to see some of the body of the message? You can get a `summary'
+of the first lines of the message using the =--summary-len= option, which will
+`summarize' the first =n= lines of the message:
+
+#+begin_example
+$ mu find --summary-len=3 napoleon m:/archive
+#+end_example
+
+#+begin_example
+1970-01-01T02:00:00 EET Napoleon Bonaparte <nb@example.com> rock on dude
+Summary: Le 24 février 1815, la vigie de Notre-Dame de la Garde signala le
+trois-mâts le Pharaon, venant de Smyrne, Trieste et Naples. Comme
+d'habitude, un pilote côtier partit aussitôt du port, rasa le château
+#+end_example
+
+The summary consists of the first /n/ lines of the message with all superfluous
+whitespace removed.
+
+Also note the *m:/archive* parameter in the query. This means that we only match
+messages in a maildir called ~'/archive'~.
+
+* MORE QUERIES
+
+Let's list a few more queries that may be interesting; please note that
+searches for message flags, priority and date ranges are only available in mu
+version 0.9 or later.
+
+Get all important messages which are signed:
+#+begin_example
+ *$ mu find flag:signed prio:high *
+#+end_example
+
+Get all messages from Jim without an attachment:
+#+begin_example
+ *$ mu find from:jim AND NOT flag:attach*
+#+end_example
+
+Get all messages where Jack is in one of the contact fields:
+#+begin_example
+ *$ mu find contact:jack*
+#+end_example
+This uses the special contact: pseudo-field which matches (*from*,
+*to*, *cc* and *bcc*).
+
+Get all messages in the Sent Items folder about yoghurt:
+#+begin_example
+ *$mu find maildir:'/Sent Items' yoghurt*
+#+end_example
+Note how we need to quote search terms that include spaces.
+
+
+Get all unread messages where the subject mentions Ångström:
+#+begin_example
+ *$ mu find subject:Ångström flag:unread*
+#+end_example
+which is equivalent to:
+#+begin_example
+ *$ mu find subject:angstrom flag:unread*
+#+end_example
+because does mu is case-insensitive and accent-insensitive.
+
+Get all unread messages between March 2002 and August 2003 about some bird (or
+a Swedish rock band):
+#+begin_example
+ *$ mu find date:20020301..20030831 nightingale flag:unread*
+#+end_example
+
+Get all messages received today:
+#+begin_example
+ *$ mu find date:today..now*
+#+end_example
+
+Get all messages we got in the last two weeks about emacs:
+#+begin_example
+ *$ mu find date:2w..now emacs*
+#+end_example
+
+Another powerful feature (since 0.9.6) are wildcard searches, where you can
+search for the last =n= characters in a word. For example, you can search
+for:
+#+begin_example
+ *$ mu find 'subject:soc*'*
+#+end_example
+and get mails about soccer, Socrates, society, and so on. Note, it's important
+to quote the search query, otherwise the shell will interpret
+the `*'.
+
+You can also search for messages with a certain attachment using their
+filename, for example:
+
+#+begin_example
+ *$ mu find 'file:pic*'*
+#+end_example
+will get you all messages with an attachment starting with `pic'.
+
+If you want to find attachments with a certain MIME-type, you can use the
+following:
+
+Get all messages with PDF attachments:
+#+begin_example
+ *$ mu find mime:application/pdf*
+#+end_example
+
+or even:
+
+Get all messages with image attachments:
+#+begin_example
+ *$ mu find 'mime:image/*'*
+#+end_example
+
+
+Note that (1) the `*' wildcard can only be used as the rightmost thing in a
+search query, and (2) that you need to quote the search term, because
+otherwise your shell will interpret the `*' (expanding it to all files in the
+current directory -- probably not what you want).
+
+* DISPLAYING MESSAGES
+
+We might also want to display the complete messages instead of the header
+information. This can be done using *mu view* command. Note that this
+command does not use the database; you simply provide it the path to a
+message.
+
+Therefore, if you want to display some message from a search query, you'll
+need its path. To get the path (think *l*ocation) for our first example we
+can use:
+
+#+begin_example
+$ mu find --fields="l" t:julius fruit
+#+end_example
+
+And we'll get something like:
+#+begin_example
+/home/someuser/Maildir/archive/cur/1266188485_0.6850.cthulhu:2,
+#+end_example
+
+We can now display this message:
+
+#+begin_example
+$ mu view /home/someuser/Maildir/archive/cur/1266188485_0.6850.cthulhu:2,
+From: John Milton <jm@example.com>
+To: Julius Caesar <jc@example.com>
+Subject: Fere libenter homines id quod volunt credunt
+Date: 2008-07-31T21:57:25 EEST
+
+OF Mans First Disobedience, and the Fruit
+Of that Forbidden Tree, whose mortal taste
+Brought Death into the World, and all our woe,
+[...]
+#+end_example
+
+* FINDING CONTACTS
+
+While *mu find* searches for messages, there is also *mu cfind* to find =contacts=,
+that is, names + addresses. Without any search expression, *mu cfind* lists all of
+your contacts.
+
+#+begin_example
+$ mu cfind julius
+#+end_example
+
+will find all contacts with `julius' in either name or e-mail address. Note that
+*mu cfind* accepts a =regular expression= (as per *pcre(3)*)
+
+*mu cfind* also supports a =--format==-parameter, which sets the output to some
+specific format, so the results can be imported into another program. For
+example, to export your contact information to a *mutt* address book file, you can
+use something like:
+
+#+begin_example
+$ mu cfind --format=mutt-alias > ~/mutt-aliases
+#+end_example
+
+Then, you can use them in *mutt* if you add something like *source ~/mutt-aliases*
+to your =muttrc=.
+
+#+include: "prefooter.inc" :minlevel 1
+
+* SEE ALSO
+*mu(1)*, *mu-init(1)*, *mu-index(1)*, *mu-find(1)*, *mu-mfind(1)*, *mu-mkdir(1)*, *mu-view(1)*, *mu-extract(1)*
--- /dev/null
+#+TITLE: MU EXTRACT
+#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
+
+* NAME
+
+mu-extract - display and save message parts
+(attachments), and open them with other tools.
+
+* SYNOPSIS
+
+*mu [common-options] extract [options] [<file>]*
+
+*mu [common-options] extract [options] <file> <pattern>*
+
+* DESCRIPTION
+
+*mu extract* is the *mu* sub-command for extracting MIME-parts (e.g., attachments)
+from mail messages. The sub-command works on message files, and does not require
+the message to be indexed in the database.
+
+For attachments, the file name used when saving it is the name of the attachment
+in the message. If there is no such name, or when saving non-attachment
+MIME-parts, a name is derived from the message-id of the message.
+
+If you specify a regular express pattern as the second argument, all attachments
+with filenames matching that pattern will be extracted. The regular expressions
+are basic PCRE, and are case-sensitive by default; see *pcre(3)* for more details.
+
+Without any options, *mu extract* simply outputs the list of leaf MIME-parts in
+the message. Only `leaf' MIME-parts (including RFC822 attachments) are
+considered, *multipart/** etc. are ignored.
+
+Without a filename parameter, ~mu extract~ reads a message from standard-input. In
+that case, you cannot use the second, ~<pattern>~ parameter as this would be
+ambiguous; instead, use the ~--matches~ option.
+
+* EXTRACT OPTIONS
+
+** -a, --save-attachments
+save all MIME-parts that look like attachments.
+
+** --save-all
+save all non-multipart MIME-parts.
+
+** --parts=<parts>
+only consider the following numbered parts (comma-separated list). The numbers
+for the parts can be seen from running *mu extract* without any options but only
+the message file.
+
+** --target-dir=<dir>
+save the parts in the target directory rather than the current working
+directory.
+
+** --overwrite
+overwrite existing files with the same name; by default overwriting is not
+allowed.
+
+** -u,--uncooked
+by default, ~mu~ transforms the attachment filenames a bit (such as by replacing
+spaces by dashes); with this option, leave that to the minimum for creating
+a legal filename in the target directory.
+
+** --matches=<pattern>
+Attachments with filenames matching the pattern will be extracted. The regular
+expressions are basic PCRE, and are case-sensitive by default; see *pcre(3)* for
+more details.
+
+** --play
+Try to `play' (open) the attachment with the default application for the
+particular file type. On MacOS, this uses the *open* program, on other platforms
+it uses *xdg-open*. You can choose a different program by setting the
+*MU_PLAY_PROGRAM* environment variable.
+
+#+include: "common-options.inc" :minlevel 1
+
+* EXAMPLES
+
+To display information about all the MIME-parts in a message file:
+#+begin_example
+$ mu extract msgfile
+#+end_example
+
+To extract MIME-part 3 and 4 from this message, overwriting existing files with
+the same name:
+#+begin_example
+$ mu extract --parts=3,4 --overwrite msgfile
+#+end_example
+
+To extract all files ending in `.jpg' (case-insensitive):
+#+begin_example
+$ mu extract msgfile '.*\.jpg'
+#+end_example
+
+To extract an mp3-file, and play it in the default mp3-playing application:
+#+begin_example
+$ mu extract --play msgfile 'whoopsididitagain.mp3'
+#+end_example
+
+when reading from standard-input, you need ~--matches~, so:
+#+begin_example
+$ cat msgfile | mu extract --play --matches 'whoopsididitagain.mp3'
+#+end_example
+
+#+include: "prefooter.inc" :minlevel 1
+
+* SEE ALSO
+
+*mu(1)*
--- /dev/null
+#+TITLE: MU FIND
+#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
+
+* NAME
+
+mu-find - find e-mail messages in the *mu* database.
+
+* SYNOPSIS
+
+*mu [common-options] find [options] <search expression>*
+
+* DESCRIPTION
+
+*mu find* is the *mu* command for searching e-mail message that were stored earlier
+using *mu index(1)*.
+
+* SEARCHING MAIL
+
+*mu find* starts a search for messages in the database that match some search
+pattern. The search patterns are described in detail in *mu-query(7)*.
+
+For example:
+
+#+begin_example
+$ mu find subject:snow and date:2009..
+#+end_example
+
+would find all messages in 2009 with `snow' in the subject field, e.g:
+
+#+begin_example
+2009-03-05 17:57:33 EET Lucia <lucia@example.com> running in the snow
+2009-03-05 18:38:24 EET Marius <marius@foobar.com> Re: running in the snow
+#+end_example
+
+Note, this the default, plain-text output, which is the default, so you don't
+have to use *--format=plain*. For other types of output (such as symlinks, XML or
+s-expressions), see the discussion in the *OPTIONS*-section below about *--format*.
+
+The search pattern is taken as a command-line parameter. If the search
+parameter consists of multiple parts (as in the example) they are
+treated as if there were a logical *and* between them.
+
+For details on the possible queries, see *mu-query(7)*.
+
+* FIND OPTIONS
+
+Note, some of the important options are described in the *mu*(1) man-page
+and not here, as they apply to multiple mu-commands.
+
+The *find*-command has various options that influence the way *mu* displays the
+results. If you don't specify anything, the defaults are ~fields="d f s"~,
+~--sortfield=date~ and ~--reverse~.
+
+** -f, --fields=<fields>
+specifies a string that determines which fields are shown in the output. This
+string consists of a number of characters (such as 's' for subject or 'f' for
+from), which will replace with the actual field in the output. Fields that are
+not known will be output as-is, allowing for some simple formatting.
+
+For example:
+
+#+begin_example
+$ mu find subject:snow --fields "d f s"
+#+end_example
+
+lists the date, subject and sender of all messages with `snow' in the their
+subject.
+
+The table of replacement characters is superset of the list mentions for search
+parameters, such as:
+#+begin_example
+ t *t*o: recipient
+ d Sent *d*ate of the message
+ f Message sender (*f*rom:)
+ g Message flags (fla*g*s)
+ l Full path to the message (*l*ocation)
+ s Message *s*ubject
+ i Message-*i*d
+ m *m*aildir
+#+end_example
+
+For the complete list, try the command: ~mu info fields~.
+
+The message flags are described in *mu-query(7)*. As an example, a message which
+is `seen', has an attachment and is signed would have `asz' as its corresponding
+output string, while an encrypted new message would have `nx'.
+
+** -s, --sortfield=<field> and -z,--reverse
+specify the field to sort the search results by and the direction (i.e.,
+`reverse' means that the sort should be reverted - Z-A). Examples include:
+
+#+begin_example
+ cc,c Cc (carbon-copy) recipient(s)
+ date,d Message sent date
+ from,f Message sender
+ maildir,m Maildir
+ msgid,i Message id
+ prio,p Nessage priority
+ subject,s Message subject
+ to,t To:-recipient(s)
+#+end_example
+
+For the complete list, try the command: ~mu info fields~.
+
+Thus, for example, to sort messages by date, you could specify:
+
+#+begin_example
+$ mu find fahrrad --fields "d f s" --sortfield=date --reverse
+#+end_example
+
+Note, if you specify a sortfield, by default, messages are sorted in reverse
+(descending) order (e.g., from lowest to highest). This is usually a good
+choice, but for dates it may be more useful to sort in the opposite direction.
+
+** -n, --maxnum=<number>
+If > 0, display maximally that number of entries. If not specified, all matching
+entries are displayed.
+
+** --summary-len=<number>
+If > 0, use that number of lines of the message to provide a summary.
+
+** --format=<plain|links|xml|sexp>
+
+output results in the specified format:
+
+- The default is *plain*, i.e normal output with one line per message.
+- *links* outputs the results as a maildir with symbolic links to the found
+ messages. This enables easy integration with mail-clients (see below for more
+ information).
+- *xml* formats the search results as XML.
+- *sexp* formats the search results as an s-expression as used in Lisp programming
+ environments
+
+** --linksdir=<dir> and -c, --clearlinks
+when using ~-format=links~, output the results as a maildir with symbolic links to
+the found messages. This enables easy integration with mail-clients (see below
+for more information). *mu* will create the maildir if it does not exist yet.
+
+If you specify ~--clearlinks~, existing symlinks will be cleared from the target
+directories; this allows for re-use of the same maildir. However, this option
+will delete any symlink it finds, so be careful.
+
+#+begin_example
+$ mu find grolsch --format=links --linksdir=~/Maildir/search --clearlinks
+#+end_example
+
+stores links to found messages in =~/Maildir/search=. If the directory does not
+exist yet, it will be created. Note: when *mu* creates a Maildir for these links,
+it automatically inserts a =.noindex= file, to exclude the directory from *mu
+index*.
+
+** --after=<timestamp>
+only show messages whose message files were last modified (*mtime*) after
+=<timestamp>=. =<timestamp>= is a UNIX *time_t* value, the number of seconds since
+1970-01-01 (in UTC).
+
+From the command line, you can use the *date* command to get this value. For
+example, only consider messages modified (or created) in the last 5 minutes, you
+could specify
+#+begin_example
+ --after=`date +%s --date='5 min ago'`
+#+end_example
+This is assuming the GNU *date* command.
+
+** --exec=<command>
+the ~--exec~ coption causes the =command= to be executed on each matched message;
+for example, to see the raw text of all messages matching `milkshake', you could
+use:
+#+begin_example
+$ mu find milkshake --exec='less'
+#+end_example
+which is roughly equivalent to:
+#+begin_example
+$ mu find milkshake --fields="l" | xargs less
+#+end_example
+
+** -b, --bookmark=<bookmark>
+use a bookmarked search query. Using this option, a query from your bookmark
+file will be prepended to other search queries. See *mu-bookmarks(5)* for the
+details of the bookmarks file.
+
+
+** -u, --skip-dups
+whenever there are multiple messages with the same message-id field, only show
+the first one. This is useful if you have copies of the same message, which is a
+common occurrence when using e.g. Gmail together with *offlineimap*.
+
+** -r, --include-related
+include messages being referred to by the matched messages -- i.e.. include
+messages that are part of the same message thread as some matched messages. This
+is useful if you want Gmail-style `conversations'.
+
+** -t, --threads
+show messages in a `threaded' format -- that is, with indentation and arrows
+showing the conversation threads in the list of matching messages. When using
+this, sorting is chronological (by date), based on the newest message in a
+thread.
+
+Messages in the threaded list are indented based on the depth in the discussion,
+and are prefix with a kind of arrow with thread-related information about the
+message, as in the following table:
+#+begin_example
+| | normal | orphan | duplicate |
+|-------------+--------+--------+-----------|
+| first child | `-> | `*> | `=> |
+| other | |-> | |*> | |=> |
+#+end_example
+
+Here, an `orphan' is a message without a parent message (in the list of
+matches), and a duplicate is a message whose message-id was already seen before;
+not this may not really be the same message, if the message-id was copied.
+
+The algorithm used for determining the threads is based on Jamie Zawinksi's
+description: http://www.jwz.org/doc/threading.html
+
+** -a,--analyze
+instead of executing the query, analyze it by show the parse-tree s-expression
+and a stringified version of the Xapian query. This can help users to determine
+how ~mu~ interprets some query.
+
+The output of this command are differ between versions, but should be helpful
+nevertheless.
+
+#+include: "muhome.inc" :minlevel 2
+
+#+include: "common-options.inc" :minlevel 1
+
+* INTEGRATION
+
+It is possible to integrate *mu find* with some mail clients
+
+** *mutt*
+
+For *mutt* you can use the following in your =muttrc=; pressing the F8 key will
+start a search, and F9 will take you to the results.
+
+#+begin_example
+# mutt macros for mu
+macro index <F8> "<shell-escape>mu find --clearlinks --format=links --linksdir=~/Maildir/search " \\
+ "mu find"
+macro index <F9> "<change-folder-readonly>~/Maildir/search" \\
+ "mu find results"
+#+end_example
+
+
+** *Wanderlust*
+
+*Sam B* suggested the following on the *mu*-mailing list. First add the following to
+your Wanderlust configuration file:
+
+#+begin_example
+(require 'elmo-search)
+(elmo-search-register-engine
+ 'mu 'local-file
+ :prog "/usr/local/bin/mu" ;; or wherever you've installed it
+ :args '("find" pattern "--fields" "l") :charset 'utf-8)
+
+(setq elmo-search-default-engine 'mu)
+;; for when you type "g" in folder or summary.
+(setq wl-default-spec "[")
+#+end_example
+
+Now, you can search using the *g* key binding; you can also create permanent
+virtual folders when the messages matching some expression by adding something
+like the following to your =folders= file.
+
+#+begin_example
+VFolders {
+ [date:today..now]!mu "Today"
+ [size:1m..100m]!mu "Big"
+ [flag:unread]!mu "Unread"
+}
+#+end_example
+
+After restarting Wanderlust, the virtual folders should appear.
+
+* ENCODING
+
+*mu find* output is encoded according to the locale for =--format=plain= (the
+default format), and UTF-8 for all other formats (=sexp=, =xml=).
+
+
+* PERFORMANCE
+
+Some notes on performance, comparing the timings between some recent releases;
+taking the total number for 10 test runs.
+
+1. time (repeat 10 mu find "" -n 50000 > /dev/null)
+2. time (repeat 10 mu find "" -n 50000 --include-related --threads > /dev/null)
+
+
+#+ATTR_MAN: :disable-caption t
+| release | time 1 (sec) | time 2 (sec) |
+|---------------+--------------+--------------|
+| 1.4 | 8.9s | 59.3s |
+| 1.6 | 8.3s | 27.5s |
+| 1.8 | 8.7s | 29.3s |
+| 1.10 | 9.8s | 30.6s |
+| 1.11 (master) | 10.1s | 29.5s |
+
+
+
+
+#+include: "exit-code.inc" :minlevel 1
+
+#+include: "bugs.inc" :minlevel 1
+
+#+include: "author.inc" :minlevel 1
+
+#+include: "copyright.inc" :minlevel 1
+
+* SEE ALSO
+
+*mu(1)*, *mu-index(1)*, *mu-query(7)*, *mu-info(1)*
--- /dev/null
+#+TITLE: MU HELP
+#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
+
+* NAME
+
+mu-help - show help information about mu commands.
+
+* SYNOPSIS
+
+*mu [common-options] help [<command>]*
+
+* DESCRIPTION
+
+*mu help* provides help information about mu commands.
+
+#+include: "common-options.inc" :minlevel 1
+
+#+include: "exit-code.inc" :minlevel 1
+
+#+include: "prefooter.inc" :minlevel 1
--- /dev/null
+#+TITLE: MU INDEX
+#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
+
+* NAME
+
+mu-index - index e-mail messages stored in Maildirs
+
+* SYNOPSIS
+
+*mu [common-options] index*
+
+* DESCRIPTION
+
+*mu index* is the *mu* command for scanning the contents of Maildir directories and
+storing the results in a Xapian database. The data can then be queried using
+*mu-find(1)*.
+
+Before the first time you run *mu index*, you must run *mu init* to initialize the
+database.
+
+*index* understands Maildirs as defined by Daniel Bernstein for *qmail(7)*. In
+addition, it understands recursive Maildirs (Maildirs within Maildirs),
+Maildir++. It also supports VFAT-based Maildirs which use =!= or =;= as the
+separators instead of =:=.
+
+E-mail messages which are not stored in something resembling a maildir
+leaf-directory (=cur= and =new=) are ignored, as are the cache directories for
+=notmuch= and =gnus=, and any dot-directory.
+
+Symlinks are followed, and the directories can be spread over multiple
+filesystems; however note that moving files around is much faster when multiple
+filesystems are not involved. Be careful to avoid self-referential symlinks!
+
+If there is a file called =.noindex= in a directory, the contents of that
+directory and all of its subdirectories will be ignored. This can be useful to
+exclude certain directories from the indexing process, for example directories
+with spam-messages.
+
+If there is a file called =.noupdate= in a directory, the contents of that
+directory and all of its subdirectories will be ignored. This can be useful to
+speed up things you have some maildirs that never change.
+
+=.noupdate= does not affect already-indexed message: you can still search for
+them. =.noupdate= is ignored when you start indexing with an empty database (such
+as directly after =mu init=).
+
+There also the option *--lazy-check* which can greatly speed up indexing; see
+below for details.
+
+The first run of *mu index* may take a few minutes if you have a lot of mail (tens
+of thousands of messages). Fortunately, such a full scan needs to be done only
+once; after that it suffices to index the changes, which goes much faster. See
+the `PERFORMANCE (i,ii,iii)' below for more information.
+
+The optional `phase two' of the indexing-process is the removal of messages from
+the database for which there is no longer a corresponding file in the Maildir.
+If you do not want this, you can use ~-n~, ~--nocleanup~.
+
+When *mu index* catches one of the signals *SIGINT*, *SIGHUP* or *SIGTERM* (e.g., when
+you press Ctrl-C during the indexing process), it attempts to shutdown
+gracefully; it tries to save and commit data, and close the database etc. If it
+receives another signal (e.g., when pressing Ctrl-C once more), *mu index* will
+terminate immediately.
+
+* INDEX OPTIONS
+
+** --lazy-check
+
+in lazy-check mode, *mu* does not consider messages for which the time-stamp
+(ctime) of the directory they reside in has not changed since the previous
+indexing run. This is much faster than the non-lazy check, but won't update
+messages that have change (rather than having been added or removed), since
+merely editing a message does not update the directory time-stamp. Of course,
+you can run *mu-index* occasionally without ~--lazy-check~, to pick up such
+messages.
+
+** --nocleanup
+
+disable the database cleanup that *mu* does by default after indexing.
+
+** --reindex
+
+perform a complete reindexing of all the messages in the maildir.
+
+#+include: "muhome.inc" :minlevel 2
+
+#+include: "common-options.inc" :minlevel 1
+
+* ENCRYPTION
+
+*mu index* does _not_ decrypt messages, and only the metadata (such as headers) of
+encrypted messages makes it to the database. *mu view* and *mu4e* can decrypt
+messages, but those work with the message directly and the information is not
+added to the database.
+
+* PERFORMANCE
+
+** indexing in ancient times (2009?)
+
+As a non-scientific benchmark, a simple test on the author's machine (a Thinkpad
+X61s laptop using Linux 2.6.35 and an ext3 file system) with no existing
+database, and a maildir with 27273 messages:
+
+#+begin_example
+$ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
+$ time mu index --quiet
+66,65s user 6,05s system 27% cpu 4:24,20 total
+#+end_example
+(about 103 messages per second)
+
+A second run, which is the more typical use case when there is a database
+already, goes much faster:
+
+#+begin_example
+$ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
+$ time mu index --quiet
+0,48s user 0,76s system 10% cpu 11,796 total
+#+end_example
+(more than 56818 messages per second)
+
+Note that each test flushes the caches first; a more common use case might be to
+run *mu index* when new mail has arrived; the cache may stay quite `warm' in that
+case:
+
+#+begin_example
+ $ time mu index --quiet
+ 0,33s user 0,40s system 80% cpu 0,905 total
+#+end_example
+which is more than 30000 messages per second.
+
+** indexing in 2012
+
+As per June 2012, we did the same non-scientific benchmark, this time with an
+Intel i5-2500 CPU @ 3.30GHz, an ext4 file system and a maildir with 22589
+messages. We start without an existing database.
+
+#+begin_example
+ $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
+ $ time mu index --quiet
+ 27,79s user 2,17s system 48% cpu 1:01,47 total
+#+end_example
+(about 813 messages per second)
+
+A second run, which is the more typical use case when there is a database
+already, goes much faster:
+
+#+begin_example
+$ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
+$ time mu index --quiet
+0,13s user 0,30s system 19% cpu 2,162 total
+#+end_example
+(more than 173000 messages per second)
+
+** indexing in 2016
+
+As per July 2016, we did the same non-scientific benchmark, again with the Intel
+i5-2500 CPU @ 3.30GHz, an ext4 file system. This time, the maildir contains
+72525 messages.
+
+#+begin_example
+$ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
+$ time mu index --quiet
+40,34s user 2,56s system 64% cpu 1:06,17 total
+#+end_example
+(about 1099 messages per second).
+
+** indexing in 2022
+
+A few years later and it is June 2022. There's a lot more happening during
+indexing, but indexing became multi-threaded and machines are faster; e.g. this
+is with an AMD Ryzen Threadripper 1950X (16 cores) @ 3.399GHz.
+
+The instructions are a little different since we have a proper repeatable
+benchmark now. After building,
+
+#+begin_example
+ $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches'
+% THREAD_NUM=4 build/lib/tests/bench-indexer -m perf
+# random seed: R02Sf5c50e4851ec51adaf301e0e054bd52b
+1..1
+# Start of bench tests
+# Start of indexer tests
+indexed 5000 messages in 20 maildirs in 3763ms; 752 μs/message; 1328 messages/s (4 thread(s))
+ok 1 /bench/indexer/4-cores
+# End of indexer tests
+# End of bench tests
+#+end_example
+
+Things are again a little faster, even though the index does a lot more now
+(text-normalizatian, and pre-generating message-sexps). A faster machine helps,
+too!
+
+** recent releases
+
+Indexing the the same 93000-message mail corpus with the last few releases:
+
+#+ATTR_MAN: :disable-caption t
+| release | time (sec) | notes |
+|---------------+------------+------------------------------------------|
+| 1.4 | 160s | |
+| 1.6 | 178s | |
+| 1.8 | 97s | |
+| 1.10 | 120s | adds html indexing, sexp-caching |
+| 1.11 (master) | 96s | adds language-guessing, batch-size=50000 |
+| | | |
+
+Quite some variation!
+
+Over time new features / refactoring can change the timings quite a bit. At
+least for now, the latest code is both the fastest and the most featureful!
+
+#+include: "exit-code.inc" :minlevel 1
+
+#+include: "prefooter.inc"
+
+* SEE ALSO
+
+*maildir(5)*, *mu(1)*, *mu-init(1)*, *mu-find(1)*, *mu-cfind(1)*
--- /dev/null
+#+TITLE: MU INFO
+#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
+
+* NAME
+
+mu-info - show information
+
+* SYNOPSIS
+
+*mu [common options] info [<topic>]*
+
+* DESCRIPTION
+
+~mu info~ is the ~mu~ command for getting information about various topics:
+
+- *mu*: general mu build information (default)
+- *store*: information about the message store
+- *fields*: table with all the query fields and flags
+- *maildirs*: list all maildirs under the store's root-maildir
+
+Note that while running (e.g. ~mu4e~), some of the ~store~ information can be
+delayed due to database caching.
+
+#+include: "common-options.inc" :minlevel 1
+
+#+include: "exit-code.inc" :minlevel 1
+
+#+include: "prefooter.inc" :minlevel 1
+
+* SEE ALSO
+
+*mu(1)*
--- /dev/null
+#+TITLE: MU INIT
+#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
+
+* NAME
+
+mu-init - initialize the mu message database
+
+* SYNOPSIS
+
+*mu [common-options] init [options]*
+
+* DESCRIPTION
+
+*mu init* is the subcommand for setting up the mu message database. After *mu init*
+has completed, you can run *mu index*
+
+* INIT OPTIONS
+
+** -m, --maildir=<maildir>
+
+use =<maildir>= as the root-maildir.
+
+By default, *mu* uses the *MAILDIR* environment; if it is not set, it uses =~/Maildir=
+if it is an existing directory. If neither of those can be used, the ~--maildir~
+option is required; it must be an absolute path (but ~~/~ expansion is
+performed).
+
+** --my-address=<email-address-or-regex>
+
+specifies that some e-mail address is `my-address' (the option can be used
+multiple times). Any message in which at least one of the contact fields
+contains such an address is considered a `personal' messages; this can then be
+used for filtering in *mu-find(1)*, *mu-cfind(1)* and *mu4e*, e.g. to filter-out
+mailing list messages.
+
+=<email-address-or-regex>= can be either a plain e-mail address (such as
+*foo@example.com*), or a basic PCRE regular-expression (see *pcre(3)* for details),
+wrapped in */* (such as =/foo-.*@example\\.com/=). Depending on your shell, the
+argument may need to be quoted.
+
+** --ignored-address=<email-address-or-regex>
+
+specifies that some e-mail address is to be ignored from the contacts-cache (the
+option can be used multiple times). Such addresses then cannot be found with
+*mu-cfind(1)* or in the Mu4e contacts cache.
+
+=<my-email-address>= can be either a plain e-mail address or a regexp, just like
+for the =--my-address= option.
+
+** --max-message-size=<size>
+
+specifies the maximum size for an e-mail message. Usually, the default of
+100000000 bytes should be fine.
+
+** --batch-size=<size>
+
+the number of changes after which they are committed to the database; decreasing
+the value reduces the memory requirements, at the cost of make indexing
+substantially slower. Usually, the default of 250000 should be fine.
+
+Batch-size 0 is interpreted as `use the default'.
+
+** --support-ngrams
+
+whether to enable support for using ngrams in indexing and query parsing; this
+can be useful for languages without explicit word breaks, such as
+Chinese/Japanese/Korean. See *NGRAM SUPPORT* below for details.
+
+** --reinit
+
+reinitialize the database from an earlier version; that is, create a new empty
+database with the existing settings. This cannot be combined with the other ~init~
+options.
+
+#+include: "muhome.inc" :minlevel 2
+
+* NGRAM SUPPORT
+
+*mu*'s underlying Xapian database supports `ngrams', which improve searching for
+languages/scripts that do not have explicit word breaks, such as Chinese,
+Japanese and Korean. It is fairly intrusive, and influences both indexing and
+query-parsing; it is not enabled by default, and is recommended only if you need
+to search for messages written in such languages.
+
+When enabled, *mu* automatically uses ngrams automatically. Xapian environment
+variables such as ~XAPIAN_CJK_NGRAM~ are ignored.
+
+#+include: "exit-code.inc" :minlevel 1
+
+
+* EXAMPLE
+#+begin_example
+$ mu init --maildir=~/Maildir --my-address=alice@example.com --my-address=bob@example.com --ignored-address='/.*reply.*/'
+#+end_example
+
+#+include: "prefooter.inc" :minlevel 1
+
+* SEE ALSO
+
+*mu-index(1)*, *mu-find(1)*, *mu-cfind(1)*, *pcre(3)*
--- /dev/null
+#+TITLE: MU MKDIR
+#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
+
+* NAME
+
+mu-mkdir - create a new Maildir
+
+* SYNOPSIS
+
+*mu [common-options] mkdir [options] <dir> [<dirs>]*
+
+* DESCRIPTION
+
+*mu mkdir* is the command for creating Maildirs as per *maildir(5)*. A maildir is a
+a directory with subdirectories ~new~, ~cur~ and ~tmp~.
+
+The command does not use the mu database.
+
+If creation fails for any reason, *no* attempt is made to remove any parts that
+were created. This is for safety reasons.
+
+* MKDIR OPTIONS
+
+** --mode=<mode>
+set the file access mode for the new maildir(s) as in *chmod(1)*. The default
+is 0755.
+
+#+include: "common-options.inc" :minlevel 1
+
+* EXAMPLE
+
+#+begin_example
+$ mu mkdir tom dick harry
+#+end_example
+
+creates three maildirs, =tom=, =dick= and =harry=.
+
+#+include: "prefooter.inc" :minlevel 1
+
+* SEE ALSO
+
+*maildir(5)*, *chmod(1)*
--- /dev/null
+#+TITLE: MU MOVE
+#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
+
+* NAME
+
+mu-move - move a message file or change its flags
+
+* SYNOPSIS
+
+*mu [common-options] move [options] <src> [--flags=<flags>] [<target>]*
+
+* DESCRIPTION
+
+*mu move* is the command for moving messages in a Maildir or changing their flags.
+
+For any change, both the message file in the file system as well as its
+representation in the database are updated accordingly.
+
+The source message file and target-maildir must reside under the root-maildir
+for mu's database (see *mu info store*).
+
+* MOVE OPTIONS
+
+** --flags=<flags>
+
+specify the new message flags. See *FLAGS* for details.
+
+** --change-name
+
+change the basename of the message file when moving; this can be useful when
+using some external tools such as *mbsync(1)* which otherwise get confused
+
+** --update-dups
+
+update the flags of duplicate messages too, where "duplicate messages" are
+defined as all message that share the same message-id. Note that the
+Draft/Flagged/Trashed flags are deliberately _not_ changed if you change those on
+the source message.
+
+** --dry-run,-n
+
+print the target filename(s), but don't change anything.
+
+Note that with the ~--change-name~, the target name is not constant, so you cannot
+use a dry-run to predict the exact name when doing a `real' run.
+
+#+include: "common-options.inc" :minlevel 1
+
+* FLAGS
+
+(Note: if you are not familiar with Maildirs, please refer to the *maildir(5)*
+man-page, or see http://cr.yp.to/proto/maildir.html)
+
+The message flags specify the Maildir-metadata for a message and are represented
+by uppercase letters at the end of the message file name for all `non-new'
+messages, i.e. messages that live in the ~cur/~ sub-directory of a Maildir.
+
+#+ATTR_MAN: :disable-caption t
+| Flag | Meaning |
+|------+------------------------------------|
+| D | Draft message |
+| F | Flagged message |
+| P | Passed message (i.e., `forwarded') |
+| R | Replied message |
+| S | Seen message |
+| T | Trashed; to be deleted later |
+
+New messages (in the ~new/~ sub-directory) do not have flags encoded in their
+file-name; but we *mu* uses `N' in the ~--flags~ to represent that:
+
+#+ATTR_MAN: :disable-caption t
+| Flag | Meaning |
+|------+---------|
+| N | New |
+
+Thus, changing flags means changing the letters at the end of the message
+file-name, except when setting or removing the `N' (new) flag. Setting or
+un-setting the New flag causes the message is to be moved from ~cur/~ to ~new/~ or
+vice-versa, respectively. When marking a message as New, it looses the other
+flags.
+
+* ABSOLUTE AND RELATIVE FLAGS
+
+You can specify the flags with the ~--flags~ parameter, and do either with either
+*absolute* or *relative* flags.
+
+Absolute flags just specify the new flags by their letters; e.g. to specify a
+/Trashed/, /Seen/, /Replied/ message, you'd use ~--flags STR~.
+#+end_example
+
+Relative flags are relative to the current flags for some message, and each of
+the flags is prefixed with either ~+~ ("add this flag") or ~-~ ("remove this flag").
+
+So to add the /Seen/ flag and remove the /Draft/ flag from whatever the message
+already has, ~--flags +S-D~.
+
+You cannot combine relative and relative flags.
+
+* EXAMPLES
+
+** change some flags
+#+begin_example
+$ mu move /home/user/Maildir/inbox/cur/1695559560.a73985881f4611ac2.hostname!2,S --flags +F
+/home/user/Maildir/inbox/cur/1695559560.a73985881f4611ac2.hostname!2,FS
+#+end_example
+
+** move to a different maildir
+#+begin_example
+$ mu move /home/user/Maildir/project1/cur/1695559560.a73985881f4611ac2.hostname!2,S /project2
+/home/user/Maildir/project2/cur/1695559560.a73985881f4611ac2.hostname!2,S
+#+end_example
+
+#+include: "prefooter.inc" :minlevel 1
+
+* SEE ALSO
+
+*maildir(5)*
--- /dev/null
+#+TITLE: MU QUERY
+#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
+
+* NAME
+
+mu-query - a language for finding messages in *mu* databases.
+
+* DESCRIPTION
+
+The mu query language is the language used by *mu find* and *mu4e* to find messages
+in *mu*'s Xapian database. The language is quite similar to Xapian's default
+query-parser, but is an independent implementation that is customized for the
+mu/mu4e use-case.
+
+Here, we give a structured but informal overview of the query language and
+provide examples. As a companion to this, we recommend the *mu fields* and *mu
+flags* commands to get an up-to-date list of the available fields and flags.
+
+Furthermore, *mu find* provides the ~--analyze~ option, which shows how *mu*
+interprets your query; see the *ANALYZING QUERIES* section below.
+
+*NOTE:* if you use queries on the command-line (say, for *mu find*), you need to
+quote any characters that would otherwise be interpreted by the shell, such as
+*""*, *(* and *)* and whitespace.
+
+* TERMS
+
+The basic building blocks of a query are *terms*; these are just normal words like
+`banana' or `hello', or words prefixed with a field-name which makes them apply
+to just that field. See *mu info fields* for all the available fields.
+
+Some example queries:
+#+begin_example
+vacation
+subject:capybara
+maildir:/inbox
+#+end_example
+
+Terms without an explicit field-prefix, (like `vacation' above) are interpreted
+like:
+#+begin_example
+to:vacation or subject:vacation or body:vacation or ...
+#+end_example
+
+The language is case-insensitive for terms and attempts to `flatten' diacritics,
+so =angtrom= matches =Ångström=.
+
+If terms contain whitespace, they need to be quoted:
+#+begin_example
+subject:"hi there"
+#+end_example
+This is a so-called =phrase query=, which means that we match against subjects
+that contain the literal phrase "hi there". Phrase queries only work for fields
+that are /indexed/, i.e., fields with *index* in the *mu info fields* search column.
+
+Remember that you need to escape those quotes when using this from the
+command-line:
+#+begin_example
+mu find subject:\\"hi there\\"
+#+end_example
+
+* LOGICAL OPERATORS
+
+We can combine terms with logical operators -- binary ones: *and*, *or*, *xor* and the
+unary *not*, with the conventional rules for precedence and association. The
+operators are case-insensitive.
+
+You can also group things with *(* and *)*, so you can write:
+#+begin_example
+(subject:beethoven or subject:bach) and not body:elvis
+#+end_example
+
+If you do not explicitly specify an operator between terms, *and* is implied, so
+the queries
+#+begin_example
+subject:chip subject:dale
+#+end_example
+#+begin_example
+subject:chip AND subject:dale
+#+end_example
+are equivalent. For readability, we recommend the second version.
+
+Note that a =pure not= - e.g. searching for *not apples* is quite a `heavy' query.
+
+* REGULAR EXPRESSIONS AND WILDCARDS
+
+The language supports matching basic PCRE regular expressions, see *pcre(3)*.
+
+Regular expressions are enclosed in *//*. Some examples:
+
+#+begin_example
+subject:/h.llo/ # match hallo, hello, ...
+subject:/
+#+end_example
+
+Note the difference between `maildir:/foo' and `maildir:/foo/'; the former
+matches messages in the `/foo' maildir, while the latter matches all messages in
+all maildirs that match `foo', such as `/foo', `/bar/cuux/foo', `/fooishbar'
+etc.
+
+Wildcards are another mechanism for matching where a term with a rightmost ***
+(and =only= in that position) matches any term that starts with the part before
+the ***; they are therefore less powerful than regular expressions, but also much
+faster:
+#+begin_example
+foo*
+#+end_example
+is equivalent to
+#+begin_example
+/foo.*/
+#+end_example
+
+Regular expressions can be useful, but are relatively slow.
+
+* FIELDS
+
+We already saw a number of search fields, such as *subject:* and *body:*. For the
+full table with all details, including single-char shortcuts, try the command:
+~mu info fields~.
+
+#+ATTR_MAN: :disable-caption t
+#+begin_example
++-----------+----------+----------+-----------------------------+
+| flag | shortcut | category | description |
++-----------+----------+----------+-----------------------------+
+| draft | D | file | Draft (in progress) |
++-----------+----------+----------+-----------------------------+
+| flagged | F | file | User-flagged |
++-----------+----------+----------+-----------------------------+
+| passed | P | file | Forwarded message |
++-----------+----------+----------+-----------------------------+
+| replied | R | file | Replied-to |
++-----------+----------+----------+-----------------------------+
+| seen | S | file | Viewed at least once |
++-----------+----------+----------+-----------------------------+
+| trashed | T | file | Marked for deletion |
++-----------+----------+----------+-----------------------------+
+| new | N | maildir | New message |
++-----------+----------+----------+-----------------------------+
+| signed | z | content | Cryptographically signed |
++-----------+----------+----------+-----------------------------+
+| encrypted | x | content | Encrypted |
++-----------+----------+----------+-----------------------------+
+| attach | a | content | Has at least one attachment |
++-----------+----------+----------+-----------------------------+
+| unread | u | pseudo | New or not seen message |
++-----------+----------+----------+-----------------------------+
+| list | l | content | Mailing list message |
++-----------+----------+----------+-----------------------------+
+| personal | q | content | Personal message |
++-----------+----------+----------+-----------------------------+
+| calendar | c | content | Calendar invitation |
++-----------+----------+----------+-----------------------------+
+#+end_example
+
+(*) The language code for the text-body if found. This works only if ~mu~ was
+built with CLD2 support.
+
+There are also the special fields *contact:*, which matches all contact-fields
+(=from=, =to=, =cc= and =bcc=), and *recip*, which matches all recipient-fields (=to=, =cc=
+and =bcc=).
+
+Hence, for instance,
+#+begin_example
+contact:fnorb@example.com
+#+end_example
+is equivalent to
+#+begin_example
+(from:fnorb@example.com or to:fnorb@example.com or
+ cc:from:fnorb@example.com or bcc:fnorb@example.com)
+#+end_example
+
+* DATE RANGES
+
+The *date:* field takes a date-range, expressed as the lower and upper bound,
+separated by *..*. Either lower or upper (but not both) can be omitted to create
+an open range.
+
+Dates are expressed in local time and using ISO-8601 format (YYYY-MM-DD
+HH:MM:SS); you can leave out the right part and *mu* adds the rest, depending on
+whether this is the beginning or end of the range (e.g., as a lower bound,
+`2015' would be interpreted as the start of that year; as an upper bound as the
+end of the year).
+
+You can use `/' , `.', `-', `:' and `T' to make dates more human-readable.
+
+Some examples:
+#+begin_example
+date:20170505..20170602
+date:2017-05-05..2017-06-02
+date:..2017-10-01T12:00
+date:2015-06-01..
+date:2016..2016
+#+end_example
+
+You can also use the special `dates' *now* and *today*:
+#+begin_example
+date:20170505..now
+date:today..
+#+end_example
+
+Finally, you can use relative `ago' times which express some time before now and
+consist of a number followed by a unit, with units *s* for seconds, *M* for minutes,
+*h* for hours, *d* for days, *w* for week, *m* for months and *y* for years. Some
+examples:
+
+#+begin_example
+date:3m..
+date:2017.01.01..5w
+#+end_example
+
+* SIZE RANGES
+
+The *size* or *z* field allows you to match =size ranges= -- that is, match messages
+that have a byte-size within a certain range. Units (b (for bytes), K (for 1000
+bytes) and M (for 1000 * 1000 bytes) are supported). Some examples:
+
+#+begin_example
+size:10k..2m
+size:10m..
+#+end_example
+
+* FLAG FIELD
+
+The *flag/g* field allows you to match message flags. The following fields are
+available:
+#+begin_example
++-----------+----------+----------+-----------------------------+
+| flag | shortcut | category | description |
++-----------+----------+----------+-----------------------------+
+| draft | D | file | Draft (in progress) |
++-----------+----------+----------+-----------------------------+
+| flagged | F | file | User-flagged |
++-----------+----------+----------+-----------------------------+
+| passed | P | file | Forwarded message |
++-----------+----------+----------+-----------------------------+
+| replied | R | file | Replied-to |
++-----------+----------+----------+-----------------------------+
+| seen | S | file | Viewed at least once |
++-----------+----------+----------+-----------------------------+
+| trashed | T | file | Marked for deletion |
++-----------+----------+----------+-----------------------------+
+| new | N | maildir | New message |
++-----------+----------+----------+-----------------------------+
+| signed | z | content | Cryptographically signed |
++-----------+----------+----------+-----------------------------+
+| encrypted | x | content | Encrypted |
++-----------+----------+----------+-----------------------------+
+| attach | a | content | Has at least one attachment |
++-----------+----------+----------+-----------------------------+
+| unread | u | pseudo | New or not seen message |
++-----------+----------+----------+-----------------------------+
+| list | l | content | Mailing list message |
++-----------+----------+----------+-----------------------------+
+| personal | q | content | Personal message |
++-----------+----------+----------+-----------------------------+
+| calendar | c | content | Calendar invitation |
++-----------+----------+----------+-----------------------------+
+#+end_example
+
+Some examples:
+#+begin_example
+flag:attach
+flag:replied
+g:x
+#+end_example
+
+Encrypted messages may be signed as well, but this is only visible after
+decrypting and thus invisible to *mu*.
+
+* PRIORITY FIELD
+
+The message priority field (*prio:*) has three possible values: *low*, *normal* or
+*high*. For instance, to match high-priority messages:
+#+begin_example
+prio:high
+#+end_example
+
+* MAILDIR
+
+The Maildir field describes the directory path starting *after* the Maildir root
+directory, and before the =/cur/= or =/new/= part. So, for example, if there's a
+message with the file name =~/Maildir/lists/running/cur/1234.213:2,=, you could
+find it (and all the other messages in that same maildir) with:
+#+begin_example
+maildir:/lists/running
+#+end_example
+
+Note the starting `/'. If you want to match mails in the `root' maildir, you can
+do with a single `/':
+#+begin_example
+maildir:/
+#+end_example
+
+If you have maildirs (or any fields) that include spaces, you need to quote
+them, ie.
+#+begin_example
+maildir:"/Sent Items"
+#+end_example
+
+And once again, note that when using the command-line, such queries must be
+quoted:
+#+begin_example
+mu find 'maildir:"/Sent Items"'
+#+end_example
+
+Also note that you should *not* end the maildir with a ~/~, or it can be
+misinterpreted as a regular expression term; see aforementioned.
+
+* MORE EXAMPLES
+
+Here are some simple examples of *mu* queries; you can make many more complicated
+queries using various logical operators, parentheses and so on, but in the
+author's experience, it's usually faster to find a message with a simple query
+just searching for some words.
+
+Find all messages with both `bee' and `bird' (in any field)
+#+begin_example
+bee AND bird
+#+end_example
+
+Find all messages with either Frodo or Sam:
+#+begin_example
+Frodo OR Sam
+#+end_example
+
+Find all messages with the `wombat' as subject, and `capybara' anywhere:
+#+begin_example
+subject:wombat and capybara
+#+end_example
+
+Find all messages in the `Archive' folder from Fred:
+#+begin_example
+from:fred and maildir:/Archive
+#+end_example
+
+Find all unread messages with attachments:
+#+begin_example
+flag:attach and flag:unread
+#+end_example
+
+Find all messages with PDF-attachments:
+#+begin_example
+mime:application/pdf
+#+end_example
+
+Find all messages with attached images:
+#+begin_example
+mime:image/*
+#+end_example
+
+Find all messages written in Dutch or German with the word `hallo':
+#+begin_example
+hallo and (lang:nl or lang:de)
+#+end_example
+
+This is only available if your *mu* has support for this; see *mu info* and check
+for "cld2-support*.
+
+* ANALZYING QUERIES
+
+Despite all the excellent documentation, in some cases it can be non-obvious how
+~mu~ interprets your query. For that, you can ask ~mu~ to analyze the query -- that
+is, show how ~mu~ interprets the query.
+
+This uses the the ~--analyze~ option to *mu find*.
+#+begin_example
+$ mu find subject:wombat AND date:3m.. size:..2000 --analyze
+,* query:
+ subject:wombat AND date:3m.. size:..2000
+,* parsed query:
+ (and (subject "wombat") (date (range "2023-05-30T06:10:09Z" "")) (size (range "" "2000")))
+,* Xapian query:
+ Query((Swombat AND VALUE_GE 4 n64759341 AND VALUE_LE 17 i7d0))
+#+end_example
+
+The ~parsed query~ is usually the most useful one for understanding how *mu*
+interprets your query.
+
+#+include: "prefooter.inc" :minlevel 1
+
+* SEE ALSO
+
+*mu-find(1)*, *mu-info(1), *pcre(3)*
--- /dev/null
+#+TITLE: MU REMOVE
+#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
+
+* NAME
+
+mu-remove - remove messages from the database.
+
+* SYNOPSIS
+
+*mu [common-options] remove [options] <file> [<files>]*
+
+* DESCRIPTION
+
+*mu remove* removes specific messages from the database, each of them specified by
+their filename. The files do not have to exist in the file system.
+
+* REMOVE OPTIONS
+
+#+include: "muhome.inc" :minlevel 2
+
+#+include: "common-options.inc" :minlevel 1
+
+#+include: "prefooter.inc" :minlevel 1
+
+* SEE ALSO
+
+*mu(1)*, *mu-index(1)*, *mu-add(1)*
--- /dev/null
+#+TITLE: MU-SERVER
+#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
+
+* NAME
+
+mu-server - the mu backend for the mu4e e-mail client
+
+* SYNOPSIS
+
+mu [common-options] server
+
+* DESCRIPTION
+
+*mu server* starts a simple shell in which one can query and manipulate the mu
+database. The output uses s-expressions. *mu server* is not meant for use by
+humans, except for debugging purposes. Instead, it is designed specifically for
+the *mu4e* e-mail client.
+
+#+begin_example
+ (<command-name> :param1 value1 :param2 value2)
+#+end_example
+
+For example, to view a certain message, the command would be:
+
+#+begin_example
+ (view :docid 12345)
+#+end_example
+
+Parameters can be sent in any order; they must be of the correct type though.
+See *lib/utils/mu-sexp-parser.hh* and *lib/utils/mu-sexp-parser.cc* in source-tree
+for the details.
+
+* OUTPUT FORMAT
+
+*mu server* accepts a number of commands, and delivers its results in the form:
+
+#+begin_example
+ \\376<length>\\377<s-expr>
+#+end_example
+
+\\376 (one byte 0xfe), followed by the length of the s-expression expressed as
+an hexadecimal number, followed by another \\377 (one byte 0xff), followed by
+the actual s-expression.
+
+By prefixing the expression with its length, it can be processed more
+efficiently. The \\376 and \\377 were chosen since they never occur in valid
+UTF-8 (in which the s-expressions are encoded).
+
+* SERVER OPTIONS
+
+** --commands
+
+List available commands (and try with ~--verbose~)
+
+** --eval <expression>
+
+Evaluate a mu4e server s-expression
+
+** --allow-temp-file
+
+If set, allow for the output of some commands to use temp-files rather than
+directly through the emacs process input/output. This is noticeably faster for
+commands with a lot of output, esp. when the the temp-file uses a in-memory
+file-system.
+
+* PERFORMANCE
+
+As an indication for the relative performance, we can simulate something ~mu4e~
+does; we take overall time of 50 such requests:
+
+#+begin_src sh
+time build/mu/mu server --allow-temp-file --eval '(find :query "\"\"" :include-related t :threads t :maxnum 50000)' >/dev/null
+#+end_src
+(and ~--allow-temp-file~ for 1.11)
+
+#+ATTR_MAN: :disable-caption t
+| release | time (sec) |
+|---------------+------------|
+| 1.8 | 8.6s |
+| 1.10 | 5.7s |
+| 1.11 (master) | 2.8s |
+
+
+#+include: "muhome.inc" :minlevel 2
+
+#+include: "common-options.inc" :minlevel 1
+
+#+include: "prefooter.inc" :minlevel 1
+
+* SEE ALSO
+*mu(1)*
--- /dev/null
+#+TITLE: MU VERIFY
+#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
+
+* NAME
+
+mu-verify - verify message signatures and display information about them
+
+* SYNOPSIS
+
+*mu [common-options] verify [options] [<file> ... ]*
+
+* DESCRIPTION
+
+*mu verify* is the *mu* command for verifying message signatures (such as PGP/GPG
+signatures) and displaying information about them. The sub-command works on
+message files, and does not require the message to be indexed in the database.
+
+If no message file is provided, the command expects the message on
+standard-input.
+
+* VERIFY OPTIONS
+
+** -r, --auto-retrieve
+attempt to find keys online (see the *auto-key-retrieve* option in the *gnupg(1)*
+documentation).
+
+** decrypt
+attempt to decrypt the message
+
+#+include: "common-options.inc" :minlevel 1
+
+* EXAMPLES
+
+To display aggregated (one-line) information about the verification status in a
+message:
+#+begin_example
+$ mu verify msgfile
+#+end_example
+
+To display information about all the signatures:
+#+begin_example
+$ mu verify --verbose msgfile
+#+end_example
+
+If you only want to use the exit code, you can use:
+#+begin_example
+$ mu verify --quiet msgfile
+#+end_example
+which does not give any output unless there is an error.
+
+#+include: "prefooter.inc" :minlevel 1
+
+* SEE ALSO
+
+*mu(1)*
--- /dev/null
+#+TITLE: MU VIEW
+#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
+
+* NAME
+
+mu-view - display an e-mail message file
+
+* SYNOPSIS
+
+mu [common options] view [options] [<file> ...]
+
+* DESCRIPTION
+
+*mu view* is the *mu* command for displaying e-mail message files. It works on
+message files and does =not= require the message to be indexed in the database.
+
+The command shows some common headers (From:, To:, Cc:, Bcc:, Subject: and
+Date:), the list of attachments and either the plain-text or html body of the
+message (if any), or its s-expression representation.
+
+If no message file is provided, the command reads the message from
+standard-input.
+
+* VIEW OPTIONS
+
+** --format,-o = <format>
+use the given output format, one of:
+
+- ~plain~ - use the plain-text body; this is the default
+- ~html~ - use the HTML body
+- ~sexp~ - show the S-expression representation of the message
+
+** --summary-len=<number>
+instead of displaying the full message, output a summary based upon the first
+=<number>= lines of the message.
+
+** --terminate
+terminate messages with \\f (=form-feed=) characters when displaying them. This is
+useful when you want to further process them.
+
+** --decrypt
+attempt to decrypt encrypted message bodies. This is only possible if *mu*
+was built with crypto-support.
+
+** --auto-retrieve
+attempt to retrieve crypto-keys automatically from the network, when needed.
+
+#+include: "common-options.inc" :minlevel 1
+
+* BUGS
+
+* SEE ALSO
+
+*mu(1)*
--- /dev/null
+#+TITLE: MU
+#+MAN_CLASS_OPTIONS: :section-id "@SECTION_ID@" :date "@MAN_DATE@"
+
+* NAME
+
+mu - a set of tools to deal with Maildirs and message files, in particular to
+index and search e-mail messages.
+
+* SYNOPSIS
+
+~mu~ [COMMON-OPTIONS] [[COMMAND] [COMMAND-OPTIONS]]
+
+For information about the common options, see *COMMON OPTIONS*.
+
+* DESCRIPTION
+
+~mu~ is the general command shows help about the specific commands:
+
+- ~add~: add specific messages to the database.
+- ~cfind~: find contacts
+- ~extract~: extract attachments and other MIME-parts
+- ~find~: find messages in the database
+- ~help~: get help for some command
+- ~index~: (re)index the messages in a Maildir
+- ~info~: show information about the mu database
+- ~init~: initialize the mu database
+- ~mkdir~: create a new Maildir
+- ~remove~: remove specific messages from the database
+- ~server~: start a server process (for ~mu4e~-internal use)
+- ~view~: view a specific message
+
+Each of the commands have their own manpage ~mu-<command~>~.
+
+~mu~ is a set of tools for dealing with Maildirs and the e-mail messages
+in them.
+
+~mu~'s main purpose is to enable searching of e-mail messages. It
+does so by periodically scanning a Maildir directory tree and
+analyzing the e-mail messages found (this is called `indexing'). The
+results of this analysis are stored in a database, which can then be
+queried.
+
+In addition to indexing and searching, ~mu~ also offers
+functionality for viewing messages, extracting attachments and
+creating maildirs, and searching and exporting contact information.
+
+~mu~ can be used from the command line or can be integrated with various
+e-mail clients.
+
+This manpage gives a general overview of the available commands
+(~index~, ~find~, etc.); each ~mu~ command has its own
+man-page as well.
+
+* COLORS
+
+Some ~mu~ commands support colorized output, and do so by default. If you don't
+want colors, you can use ~--nocolor~.
+
+* ENCODING
+
+~mu~'s output is in the current locale, with the exceptions of the output
+specifically meant for output to UTF8-encoded files. In practice, this means
+that the output of commands ~index~, ~view~, ~extract~ is always encoded according to
+the current locale.
+
+The same is true for ~find~ and ~cfind~, with some exceptions, where
+the output is always UTF-8, regardless of the locale:
+
+- For ~cfind~ the exception is ~--format=bbdb~. This is hard-coded to UTF-8, and as
+ such specified in the output-file, so emacs/bbdb can handle it correctly
+ without guessing.
+- For ~find~ the output is encoded according the locale for ~--format=plain~ (the
+ default), and UTF-8 for all other formats.
+
+* DATABASE AND FILE
+
+Commands ~mu index~ and ~find~ and ~cfind~ work with the database, while the other
+ones work on individual mail files. Hence, running ~view~, ~mkdir~ and ~extract~ does
+not require the mu database.
+
+#+include: "common-options.inc" :minlevel 1
+
+#+include: "exit-code.inc" :minlevel 1
+
+#+include: "prefooter.inc" :minlevel 1
+
+* SEE ALSO
+~mu-add(1)~, ~mu-cfind(1)~, ~mu-extract(1)~, ~mu-find(1)~, ~mu-help(1)~, ~mu-index(1)~,
+~mu-info(1)~, ~mu-init(1)~, ~mu-mkdir(1)~, ~mu-remove(1)~, ~mu-server(1)~, ~mu-view(1)~,
+~mu-query(7)~, ~mu-easy(1)~
--- /dev/null
+** --muhome
+use a non-default directory to store and read the database, write the logs, etc.
+By default, ~mu~ uses the XDG Base Directory Specification (e.g. on GNU/Linux this
+defaults to =~/.cache/mu= and =~/.config/mu=). Earlier versions of ~mu~ defaulted to
+=~/.mu=, which now requires =--muhome=~/.mu=.
+
+The environment variable ~MUHOME~ can be used as an alternative to ~--muhome~. The
+latter has precedence.
+
+# Local Variables:
+# mode: org
+# End:
--- /dev/null
+#+include: "bugs.inc" :minlevel 1
+
+#+include: "author.inc" :minlevel 1
+
+#+include: "copyright.inc" :minlevel 1
+
+# Local Variables:
+# mode: org
+# End:
--- /dev/null
+## Copyright (C) 2022-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## 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, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+################################################################################
+# project setup
+project('mu', ['c', 'cpp'],
+ version: '1.12.5',
+ meson_version: '>= 0.56.0',
+ license: 'GPL-3.0-or-later',
+ default_options : [
+ 'buildtype=debugoptimized',
+ 'warning_level=3',
+ 'c_std=c11',
+ 'cpp_std=c++17'])
+
+# hard-code the date here (for reproduciblity); we derive the dates used in e.g.
+# documentation from this.
+mu_date='2024-04-15'
+
+# installation paths
+prefixdir = get_option('prefix')
+bindir = prefixdir / get_option('bindir')
+datadir = prefixdir / get_option('datadir')
+mandir = prefixdir / get_option('mandir')
+infodir = prefixdir / get_option('infodir')
+
+# allow for configuring lispdir, as with autotools.
+if get_option('lispdir') == ''
+ mu4e_lispdir= datadir / join_paths('emacs', 'site-lisp', 'mu4e')
+else
+ mu4e_lispdir= get_option('lispdir') / 'mu4e'
+endif
+
+################################################################################
+# compilers / flags
+#
+
+# compilers
+cc = meson.get_compiler('c')
+cxx= meson.get_compiler('cpp')
+
+extra_flags = [
+ '-Wno-unused-parameter',
+ '-Wno-cast-function-type',
+ '-Wformat-security',
+ '-Wformat=2',
+ '-Wstack-protector',
+ '-fstack-protector-strong',
+ '-Wno-switch-enum',
+ # assuming these are false alarm... (in fmt, with gcc13):
+ '-Wno-array-bounds',
+ '-Wno-stringop-overflow',]
+
+if (cxx.get_id() == 'clang')
+ extra_flags += [
+ '-Wc11-extensions',
+ '-Wno-keyword-macro',
+ '-Wno-deprecated-volatile',
+ '-Wno-#warnings']
+endif
+
+extra_cpp_flags= [
+ '-Wno-volatile'
+]
+
+if get_option('buildtype') == 'debug'
+ extra_flags += [
+ '-D_GLIBCXX_ASSERTIONS',
+ '-ggdb',
+ '-g3']
+endif
+
+# extra arguments, if available
+foreach extra_arg : extra_flags
+ if cc.has_argument (extra_arg)
+ add_project_arguments([extra_arg], language: 'c')
+ endif
+endforeach
+
+foreach extra_arg : extra_flags + extra_cpp_flags
+ if cxx.has_argument (extra_arg)
+ add_project_arguments([extra_arg], language: 'cpp')
+ endif
+endforeach
+
+# some clang don't have charconv, but we need it.
+# https://github.com/djcb/mu/issues/2347
+cxx.check_header('charconv', required:true)
+
+
+build_aux = join_paths(meson.current_source_dir(), 'build-aux')
+################################################################################
+# derived date values (based on 'mu-date'); used in docs
+# we can't use the 'date' because MacOS 'date' is incompatible with GNU's.
+pdate=find_program(join_paths(build_aux, 'date.py'))
+env = environment()
+env.set('LANG', 'C')
+mu_day_month_year = run_command(pdate, mu_date, '%d %B %Y',
+ check:true, capture:true,
+ env: env).stdout().strip()
+mu_month_year = run_command(pdate, mu_date, '%B %Y',
+ check:true, capture:true,
+ env: env).stdout().strip()
+mu_year = run_command(pdate, mu_date, '%Y',
+ check:true, capture:true, env: env).stdout().strip()
+
+################################################################################
+# config.h setup
+#
+config_h_data=configuration_data()
+config_h_data.set('MU_STORE_SCHEMA_VERSION', 500)
+config_h_data.set_quoted('PACKAGE_VERSION', meson.project_version())
+config_h_data.set_quoted('PACKAGE_STRING', meson.project_name() + ' ' +
+ meson.project_version())
+config_h_data.set_quoted('VERSION', meson.project_version())
+config_h_data.set_quoted('PACKAGE_NAME', meson.project_name())
+
+add_project_arguments(['-DHAVE_CONFIG_H'], language: 'c')
+add_project_arguments(['-DHAVE_CONFIG_H'], language: 'cpp')
+config_h_dep=declare_dependency(
+ include_directories: include_directories(['.']))
+
+
+#
+# d_type, d_ino are not available universally, so let's check
+# (we use them for optimizations in mu-scanner
+#
+if cxx.has_member('struct dirent', 'd_ino', prefix : '#include<dirent.h>')
+ config_h_data.set('HAVE_DIRENT_D_INO', 1)
+endif
+
+if cxx.has_member('struct dirent', 'd_type', prefix : '#include<dirent.h>')
+ config_h_data.set('HAVE_DIRENT_D_TYPE', 1)
+endif
+
+
+functions=[
+ 'setsid'
+]
+foreach f : functions
+ if cc.has_function(f)
+ define = 'HAVE_' + f.underscorify().to_upper()
+ config_h_data.set(define, 1)
+ endif
+endforeach
+
+if cc.has_function('wordexp')
+ config_h_data.set('HAVE_WORDEXP_H',1)
+else
+ message('no wordexp, no command-line option expansion')
+endif
+
+if not get_option('tests').disabled()
+ # only needed for tests
+ cp=find_program('cp')
+ ln=find_program('ln')
+ rm=find_program('rm')
+
+ config_h_data.set_quoted('CP_PROGRAM', cp.full_path())
+ config_h_data.set_quoted('RM_PROGRAM', rm.full_path())
+ config_h_data.set_quoted('LN_PROGRAM', ln.full_path())
+
+ testmaildir=join_paths(meson.current_source_dir(), 'testdata')
+ config_h_data.set_quoted('MU_TESTMAILDIR', join_paths(testmaildir, 'testdir'))
+ config_h_data.set_quoted('MU_TESTMAILDIR2', join_paths(testmaildir, 'testdir2'))
+ config_h_data.set_quoted('MU_TESTMAILDIR4', join_paths(testmaildir, 'testdir4'))
+ config_h_data.set_quoted('MU_TESTMAILDIR_CJK', join_paths(testmaildir, 'cjk'))
+endif
+
+
+################################################################################
+# hard dependencies
+#
+glib_dep = dependency('glib-2.0', version: '>= 2.60')
+gobject_dep = dependency('gobject-2.0', version: '>= 2.60')
+gio_dep = dependency('gio-2.0', version: '>= 2.60')
+gio_unix_dep = dependency('gio-unix-2.0', version: '>= 2.60')
+gmime_dep = dependency('gmime-3.0', version: '>= 3.2')
+thread_dep = dependency('threads')
+
+# we need Xapian 1.4
+xapian_dep = dependency('xapian-core', version:'>= 1.4', required:true)
+xapver = xapian_dep.version()
+if xapver.version_compare('>= 1.4.6')
+ message('xapian ' + xapver + ' supports c++ move-semantics')
+ config_h_data.set('HAVE_XAPIAN_MOVE_SEMANTICS', 1)
+endif
+if xapver.version_compare('>= 1.4.23')
+ message('xapian ' + xapver + ' supports ngrams')
+ config_h_data.set('HAVE_XAPIAN_FLAG_NGRAMS', 1)
+endif
+
+# optionally, use Compact Language Detector2 if we can find it.
+cld2_dep = meson.get_compiler('cpp').find_library('cld2', required: get_option('cld2'))
+if not get_option('cld2').disabled() and cld2_dep.found()
+ config_h_data.set('HAVE_CLD2', 1)
+else
+ message('CLD2 not found or disabled; no support for language detection')
+endif
+
+# soft dependencies
+guile_dep = dependency('guile-3.0', required: get_option('guile'))
+# allow for a custom guile-extension-dir
+if guile_dep.found()
+ custom_guile_xd=get_option('guile-extension-dir')
+ if custom_guile_xd == ''
+ guile_extension_dir = guile_dep.get_variable(pkgconfig: 'extensiondir')
+ else
+ guile_extension_dir = custom_guile_xd
+ endif
+ config_h_data.set_quoted('MU_GUILE_EXTENSION_DIR', guile_extension_dir)
+ message('Using guile-extension-dir: ' + guile_extension_dir)
+endif
+
+makeinfo=find_program(['makeinfo'], required:false)
+if not makeinfo.found()
+ message('makeinfo (texinfo) not found; not building info documentation')
+else
+ install_info=find_program(['install-info'], required:false)
+ if not install_info.found()
+ message('install-info not found')
+ else
+ install_info_script=join_paths(build_aux, 'meson-install-info.sh')
+ endif
+endif
+
+# readline. annoyingly, macos has an incompatible libedit claiming to be
+# readline. this is only a dev/debug convenience for the mu4e repl.
+readline_dep=[]
+if get_option('readline').enabled()
+ readline_dep = dependency('readline', version:'>= 8.0')
+ config_h_data.set('HAVE_LIBREADLINE', 1)
+ config_h_data.set('HAVE_READLINE_READLINE_H', 1)
+ config_h_data.set('HAVE_READLINE_HISTORY', 1)
+ config_h_data.set('HAVE_READLINE_HISTORY_H', 1)
+endif
+
+
+################################################################################
+# write out version.texi (for texinfo builds in mu4e, guile)
+version_texi_data=configuration_data()
+version_texi_data.set('VERSION', meson.project_version())
+version_texi_data.set('EDITION', meson.project_version())
+
+# derived date values
+version_texi_data.set('UPDATED', mu_day_month_year)
+version_texi_data.set('UPDATEDMONTH', mu_month_year)
+version_texi_data.set('UPDATEDYEAR', mu_year)
+
+configure_file(input: join_paths(build_aux, 'version.texi.in'),
+ output: 'version.texi',
+ configuration: version_texi_data)
+
+################################################################################
+# install some data files
+install_data('NEWS.org', install_dir : join_paths(datadir,'doc', 'mu'))
+
+################################################################################
+# subdirs
+subdir('lib')
+subdir('mu')
+
+
+# emacs -- needed for mu4e compilation
+emacs_name=get_option('emacs')
+emacs_min_version='26.3'
+emacs=find_program([emacs_name], version: '>='+emacs_min_version, required:false)
+if emacs.found()
+ subdir('man')
+ subdir('mu4e')
+else
+ message('emacs not found; not pre-compiling mu4e / generating manpages')
+endif
+
+if not get_option('guile').disabled() and guile_dep.found()
+ config_h_data.set('BUILD_GUILE', 1)
+ config_h_data.set_quoted('GUILE_BINARY',
+ guile_dep.get_variable(pkgconfig: 'guile'))
+ #message('guile is disabled for now')
+ subdir('guile')
+endif
+
+config_h_data.set_quoted('MU_PROGRAM', mu.full_path())
+################################################################################
+
+################################################################################
+# write-out config.h
+configure_file(output : 'config.h', configuration : config_h_data)
+
+if gmime_dep.version() == '3.2.13'
+ warning('gmime version 3.2.13 detected, which as a decoding bug')
+ warning('See: https://github.com/jstedfast/gmime/issues/133')
+endif
--- /dev/null
+## Copyright (C) 2022-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## 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, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+option('tests',
+ type : 'feature',
+ value: 'auto',
+ description: 'build unit tests')
+
+option('guile',
+ type : 'feature',
+ value: 'auto',
+ description: 'build the guile scripting support (requires guile-3.x)')
+
+option('cld2',
+ type : 'feature',
+ value: 'auto',
+ description: 'Compact Language Detector2')
+
+# by default, this uses guile_dep.get_variable(pkgconfig: 'extensiondir')
+option('guile-extension-dir',
+ type: 'string',
+ description: 'custom install path for the guile extension module')
+
+option('readline',
+ type: 'feature',
+ value: 'auto',
+ description: 'enable readline support for the mu4e repl')
+
+option('emacs',
+ type: 'string',
+ value: 'emacs',
+ description: 'name/path of the emacs executable')
+
+option('lispdir',
+ type: 'string',
+ description: 'path under which to install emacs-lisp files')
--- /dev/null
+## Copyright (C) 2021-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## 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, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+mu = executable(
+ 'mu', [
+ 'mu.cc',
+ 'mu-options.cc',
+ 'mu-cmd-add.cc',
+ 'mu-cmd-cfind.cc',
+ 'mu-cmd-extract.cc',
+ 'mu-cmd-find.cc',
+ 'mu-cmd-info.cc',
+ 'mu-cmd-init.cc',
+ 'mu-cmd-index.cc',
+ 'mu-cmd-mkdir.cc',
+ 'mu-cmd-move.cc',
+ 'mu-cmd-remove.cc',
+ 'mu-cmd-script.cc',
+ 'mu-cmd-server.cc',
+ 'mu-cmd-verify.cc',
+ 'mu-cmd-view.cc',
+ 'mu-cmd.cc'
+],
+ dependencies: [ glib_dep, gmime_dep, lib_mu_dep, thread_dep, config_h_dep ],
+ cpp_args: ['-DMU_SCRIPTS_DIR="'+ join_paths(datadir, 'mu', 'scripts') + '"'],
+ install: true)
+#
+if not get_option('tests').disabled()
+ subdir('tests')
+endif
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+#include "mu-cmd.hh"
+
+using namespace Mu;
+
+Result<void>
+Mu::mu_cmd_add(Mu::Store& store, const Options& opts)
+{
+ for (auto&& file: opts.add.files) {
+ const auto docid{store.add_message(file)};
+ if (!docid)
+ return Err(docid.error());
+ else
+ mu_debug("added message @ {}, docid={}", file, *docid);
+ }
+
+ return Ok();
+}
+
+
+#ifdef BUILD_TESTS
+/*
+ * Tests.
+ *
+ */
+
+#include "utils/mu-test-utils.hh"
+
+static void
+test_add_ok()
+{
+ auto testhome{unwrap(make_temp_dir())};
+ auto dbpath{runtime_path(RuntimePath::XapianDb, testhome)};
+
+ {
+ unwrap(Store::make_new(dbpath, MU_TESTMAILDIR));
+ }
+
+ {
+ auto res = run_command({MU_PROGRAM, "add", mu_format("--muhome={}", testhome),
+ MU_TESTMAILDIR "/cur/1220863042.12663_1.mindcrime!2,S"});
+ assert_valid_command(res);
+ }
+
+ {
+ auto&& store = Store::make(dbpath);
+ assert_valid_result(store);
+ g_assert_cmpuint(store->size(),==,1);
+ }
+
+ { // re-add the same
+ auto res = run_command({MU_PROGRAM, "add", mu_format("--muhome={}",testhome),
+ MU_TESTMAILDIR "/cur/1220863042.12663_1.mindcrime!2,S"});
+ assert_valid_command(res);
+ }
+
+ {
+ auto&& store = Store::make(dbpath);
+ assert_valid_result(store);
+ g_assert_cmpuint(store->size(),==,1);
+ }
+
+
+ remove_directory(testhome);
+}
+
+static void
+test_add_fail()
+{
+ auto testhome{unwrap(make_temp_dir())};
+ auto dbpath{runtime_path(RuntimePath::XapianDb, testhome)};
+
+ {
+ unwrap(Store::make_new(dbpath, MU_TESTMAILDIR2));
+ }
+
+ { // wrong maildir
+ auto res = run_command({MU_PROGRAM, "add", mu_format("--muhome={}", testhome),
+ MU_TESTMAILDIR "/cur/1220863042.12663_1.mindcrime!2,S"});
+ assert_valid_result(res);
+ g_assert_cmpuint(res->exit_code,!=,0);
+ }
+
+
+ { // non-existent
+ auto res = run_command({MU_PROGRAM, "add", mu_format("--muhome={}", testhome),
+ "/foo/bar/non-existent"});
+ assert_valid_result(res);
+ g_assert_cmpuint(res->exit_code,!=,0);
+ }
+
+ remove_directory(testhome);
+}
+
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/cmd/add/ok", test_add_ok);
+ g_test_add_func("/cmd/add/fail", test_add_fail);
+
+ return g_test_run();
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2022-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+#include "mu-cmd.hh"
+
+#include <cstdint>
+#include <string>
+#include <functional>
+#include <unordered_map>
+
+#include <utils/mu-utils.hh>
+#include <utils/mu-regex.hh>
+#include <utils/mu-option.hh>
+
+using namespace Mu;
+
+enum struct ItemType { Header, Footer, Normal };
+using OutputFunc = std::function<void(ItemType itype, Option<const Contact&>, const Options&)>;
+using OptContact = Option<const Contact&>;
+using Format = Options::Cfind::Format;
+
+// simplistic guess of first & last names, for setting
+// some initial value.
+static std::pair<std::string, std::string>
+guess_first_last_name(const std::string& name)
+{
+ if (name.empty())
+ return {};
+
+ const auto lastspc = name.find_last_of(' ');
+ if (lastspc == name.npos)
+ return { name, "" }; // no last name
+ else
+ return { name.substr(0, lastspc), name.substr(lastspc + 1)};
+}
+
+
+// candidate nick and a _count_ for that given nick, to uniquify them.
+static std::unordered_map<std::string, size_t> nicks;
+static std::string
+guess_nick(const Contact& contact)
+{
+ auto cleanup = [](const std::string& str) {
+ std::string clean;
+ for (auto& c: str) // XXX: support non-ascii
+ if (!::ispunct(c) && !::isspace(c))
+ clean += c;
+ return clean;
+ };
+
+ auto nick = cleanup(std::invoke([&]()->std::string {
+
+ // no name? use the user part from the addr
+ if (contact.name.empty()) {
+ const auto pos{contact.email.find('@')};
+ if (pos == std::string::npos)
+ return contact.email; // no '@'
+ else
+ return contact.email.substr(0, pos);
+ }
+
+ const auto names{guess_first_last_name(contact.name)};
+ /* if there's no last name, use first name as the nick */
+ if (names.second.empty())
+ return names.first;
+
+ char initial[7] = {};
+ if (g_unichar_to_utf8(g_utf8_get_char(names.second.c_str()), initial) == 0) {
+ /* couldn't we get an initial for the last name?
+ * just use the first name*/
+ return names.first;
+ } else // prepend the initial
+ return names.first + initial;
+ }));
+
+ // uniquify.
+ if (auto it = nicks.find(nick); it == nicks.cend())
+ nicks.emplace(nick, 0);
+ else {
+ ++it->second;
+ nick = mu_format("{}{}", nick, ++it->second);
+ }
+
+ return nick;
+}
+
+
+static void
+output_plain(ItemType itype, OptContact contact, const Options& opts)
+{
+ if (!contact)
+ return;
+
+ const auto col1{opts.nocolor ? "" : MU_COLOR_MAGENTA};
+ const auto col2{opts.nocolor ? "" : MU_COLOR_GREEN};
+ const auto coldef{opts.nocolor ? "" : MU_COLOR_DEFAULT};
+
+ mu_print_encoded("{}{}{}{}{}{}{}\n",
+ col1, contact->name, coldef,
+ contact->name.empty() ? "" : " ",
+ col2, contact->email, coldef);
+}
+
+static void
+output_mutt_alias(ItemType itype, OptContact contact, const Options& opts)
+{
+ if (!contact)
+ return;
+
+ const auto nick{guess_nick(*contact)};
+ mu_print_encoded("alias {} {} <{}>\n", nick, contact->name, contact->email);
+
+}
+
+static void
+output_mutt_address_book(ItemType itype, OptContact contact, const Options& opts)
+{
+ if (itype == ItemType::Header)
+ mu_print ("Matching addresses in the mu database:\n");
+
+ if (contact)
+ mu_print_encoded("{}\t{}\t\n", contact->email, contact->name);
+}
+
+static void
+output_wanderlust(ItemType itype, OptContact contact, const Options& opts)
+{
+ if (!contact || contact->name.empty())
+ return;
+
+ auto nick=guess_nick(*contact);
+
+ mu_print_encoded("{} \"{}\" \"{}\"\n", contact->email, nick, contact->name);
+
+}
+
+static void
+output_org_contact(ItemType itype, OptContact contact, const Options& opts)
+{
+ if (!contact || contact->name.empty())
+ return;
+
+ mu_print_encoded("* {}\n:PROPERTIES:\n:EMAIL: {}\n:END:\n\n",
+ contact->name, contact->email);
+}
+
+static void
+output_bbdb(ItemType itype, OptContact contact, const Options& opts)
+{
+ if (itype == ItemType::Header)
+ mu_println (";; -*-coding: utf-8-emacs;-*-\n"
+ ";;; file-version: 6");
+ if (!contact)
+ return;
+
+ const auto names{guess_first_last_name(contact->name)};
+ const auto now{mu_format("{:%Y-%m-%d}", mu_time(::time({})))};
+ const auto timestamp{mu_format("{:%Y-%m-%d}", mu_time(contact->message_date))};
+
+ mu_println("[\"{}\" \"{}\" nil nil nil nil (\"{}\") "
+ "((creation-date . \"{}\") (time-stamp . \"{}\")) nil]",
+ names.first, names.second, contact->email, now, timestamp);
+}
+
+static void
+output_csv(ItemType itype, OptContact contact, const Options& opts)
+{
+ if (!contact)
+ return;
+
+ mu_print_encoded("{},{}\n",
+ contact->name.empty() ? "" : Mu::quote(contact->name),
+ Mu::quote(contact->email));
+}
+
+static void
+output_json(ItemType itype, OptContact contact, const Options& opts)
+{
+ if (itype == ItemType::Header)
+ mu_println("[");
+ if (contact) {
+ mu_print("{}", itype == ItemType::Header ? "" : ",\n");
+ mu_println (" {{");
+
+ const std::string name = contact->name.empty() ? "null" : Mu::quote(contact->name);
+ mu_print_encoded(
+ " \"email\" : \"{}\",\n"
+ " \"name\" : {},\n"
+ " \"display\" : {},\n"
+ " \"last-seen\" : {},\n"
+ " \"last-seen-iso\" : \"{}\",\n"
+ " \"personal\" : {},\n"
+ " \"frequency\" : {}\n",
+ contact->email,
+ name,
+ Mu::quote(contact->display_name()),
+ contact->message_date,
+ mu_format("{:%FT%TZ}", mu_time(contact->message_date, true/*utc*/)),
+ contact->personal ? "true" : "false",
+ contact->frequency);
+ mu_print(" }}");
+ }
+
+ if (itype == ItemType::Footer)
+ mu_println("\n]");
+}
+
+static OutputFunc
+find_output_func(Format format)
+{
+#pragma GCC diagnostic push
+#pragma GCC diagnostic error "-Wswitch"
+ switch(format) {
+ case Format::Plain:
+ return output_plain;
+ case Format::MuttAlias:
+ return output_mutt_alias;
+ case Format::MuttAddressBook:
+ return output_mutt_address_book;
+ case Format::Wanderlust:
+ return output_wanderlust;
+ case Format::OrgContact:
+ return output_org_contact;
+ case Format::Bbdb:
+ return output_bbdb;
+ case Format::Csv:
+ return output_csv;
+ case Format::Json:
+ return output_json;
+ default:
+ mu_warning("unsupported format");
+ return {};
+ }
+#pragma GCC diagnostic pop
+}
+
+
+Result<void>
+Mu::mu_cmd_cfind(const Mu::Store& store, const Mu::Options& opts)
+{
+ size_t num{};
+ OutputFunc output = find_output_func(opts.cfind.format);
+ if (!output)
+ return Err(Error::Code::Internal,
+ "missing output function");
+
+ // get the pattern regex, if any.
+ Regex rx{};
+ if (!opts.cfind.rx_pattern.empty()) {
+ if (auto&& res = Regex::make(opts.cfind.rx_pattern,
+ static_cast<GRegexCompileFlags>
+ (G_REGEX_OPTIMIZE|G_REGEX_CASELESS)); !res)
+ return Err(std::move(res.error()));
+ else
+ rx = res.value();
+ }
+
+ nicks.clear();
+ store.contacts_cache().for_each([&](const Contact& contact)->bool {
+
+ if (opts.cfind.maxnum && num > *opts.cfind.maxnum)
+ return false; /* stop the loop */
+
+ if (!store.contacts_cache().is_valid(contact.email))
+ return true; /* next */
+
+ // filter for maxnum, personal & "after"
+ if ((opts.cfind.personal && !contact.personal) ||
+ (opts.cfind.after.value_or(0) > contact.message_date))
+ return true; /* next */
+
+ // filter for regex, if any.
+ if (rx) {
+ if (!rx.matches(contact.name) && !rx.matches(contact.email))
+ return true; /* next */
+ }
+
+ /* seems we have a match! display it. */
+ const auto itype{num == 0 ? ItemType::Header : ItemType::Normal};
+ output(itype, contact, opts);
+ ++num;
+ return true;
+ });
+
+ if (num == 0)
+ return Err(Error::Code::NoMatches, "no matching contacts found");
+
+ output(ItemType::Footer, Nothing, opts);
+ return Ok();
+}
+
+
+
+
+#ifdef BUILD_TESTS
+/*
+ * Tests.
+ *
+ */
+
+#include "utils/mu-test-utils.hh"
+
+
+static std::string test_mu_home;
+
+static void
+test_mu_cfind_plain(void)
+{
+ auto res{run_command({MU_PROGRAM, "--nocolor", "cfind", "--muhome", test_mu_home,
+ "--format", "plain", "testmu\\.xxx?"})};
+ assert_valid_result(res);
+
+ /* note, output order is unspecified */
+ if (res->standard_out[0] == 'H')
+ assert_equal(res->standard_out,
+ "Helmut Kröger hk@testmu.xxx\n"
+ "Mü testmu@testmu.xx\n");
+ else
+ assert_equal(res->standard_out,
+ "Mü testmu@testmu.xx\n"
+ "Helmut Kröger hk@testmu.xxx\n");
+}
+
+static void
+test_mu_cfind_bbdb(void)
+{
+ const auto old_tz{set_tz("Europe/Helsinki")};
+ auto res{run_command({MU_PROGRAM, "--nocolor", "cfind", "--muhome", test_mu_home,
+ "--format", "bbdb", "testmu\\.xxx?"})};
+ assert_valid_result(res);
+ g_assert_cmpuint(res->standard_out.size(), >, 52);
+
+#define frm1 \
+ ";; -*-coding: utf-8-emacs;-*-\n" \
+ ";;; file-version: 6\n" \
+ "[\"Helmut\" \"Kröger\" nil nil nil nil (\"hk@testmu.xxx\") " \
+ "((creation-date . \"{}\") " \
+ "(time-stamp . \"1970-01-01\")) nil]\n" \
+ "[\"Mü\" \"\" nil nil nil nil (\"testmu@testmu.xx\") " \
+ "((creation-date . \"{}\") " \
+ "(time-stamp . \"1970-01-01\")) nil]\n"
+
+#define frm2 \
+ ";; -*-coding: utf-8-emacs;-*-\n" \
+ ";;; file-version: 6\n" \
+ "[\"Mü\" \"\" nil nil nil nil (\"testmu@testmu.xx\") " \
+ "((creation-date . \"{}\") " \
+ "(time-stamp . \"1970-01-01\")) nil]\n" \
+ "[\"Helmut\" \"Kröger\" nil nil nil nil (\"hk@testmu.xxx\") " \
+ "((creation-date . \"{}\") " \
+ "(time-stamp . \"1970-01-01\")) nil]\n"
+
+ auto&& today{mu_format("{:%F}", mu_time(::time({})))};
+ std::string expected;
+ if (res->standard_out.at(52) == 'H')
+ expected = mu_format(frm1, today, today);
+ else
+ expected = mu_format(frm2, today, today);
+
+ assert_equal(res->standard_out, expected);
+ set_tz(old_tz);
+}
+
+static void
+test_mu_cfind_wl(void)
+{
+ auto res{run_command({MU_PROGRAM, "--nocolor", "cfind", "--muhome", test_mu_home,
+ "--format", "wl", "testmu\\.xxx?"})};
+ assert_valid_result(res);
+
+ if (res->standard_out.at(0) == 'h')
+ assert_equal(res->standard_out,
+ "hk@testmu.xxx \"HelmutK\" \"Helmut Kröger\"\n"
+ "testmu@testmu.xx \"Mü\" \"Mü\"\n");
+ else
+ assert_equal(res->standard_out,
+ "testmu@testmu.xx \"Mü\" \"Mü\"\n"
+ "hk@testmu.xxx \"HelmutK\" \"Helmut Kröger\"\n");
+}
+
+static void
+test_mu_cfind_mutt_alias(void)
+{
+ auto res{run_command({MU_PROGRAM, "--nocolor", "cfind", "--muhome", test_mu_home,
+ "--format", "mutt-alias", "testmu\\.xxx?"})};
+ assert_valid_result(res);
+
+ if (res->standard_out.at(6) == 'H')
+ assert_equal(res->standard_out,
+ "alias HelmutK Helmut Kröger <hk@testmu.xxx>\n"
+ "alias Mü Mü <testmu@testmu.xx>\n");
+ else
+ assert_equal(res->standard_out,
+ "alias Mü Mü <testmu@testmu.xx>\n"
+ "alias HelmutK Helmut Kröger <hk@testmu.xxx>\n");
+}
+
+static void
+test_mu_cfind_mutt_ab(void)
+{
+ auto res{run_command({MU_PROGRAM, "--nocolor", "cfind", "--muhome", test_mu_home,
+ "--format", "mutt-ab", "testmu\\.xxx?"})};
+ assert_valid_result(res);
+
+ if (res->standard_out.at(39) == 'h')
+ assert_equal(res->standard_out,
+ "Matching addresses in the mu database:\n"
+ "hk@testmu.xxx\tHelmut Kröger\t\n"
+ "testmu@testmu.xx\tMü\t\n");
+ else
+ assert_equal(res->standard_out,
+ "Matching addresses in the mu database:\n"
+ "testmu@testmu.xx\tMü\t\n"
+ "hk@testmu.xxx\tHelmut Kröger\t\n");
+}
+
+static void
+test_mu_cfind_org_contact(void)
+{
+ auto res{run_command({MU_PROGRAM, "--nocolor", "cfind", "--muhome", test_mu_home,
+ "--format", "org-contact", "testmu\\.xxx?"})};
+ assert_valid_result(res);
+
+ if (res->standard_out.at(2) == 'H')
+ assert_equal(res->standard_out,
+ "* Helmut Kröger\n"
+ ":PROPERTIES:\n"
+ ":EMAIL: hk@testmu.xxx\n"
+ ":END:\n\n"
+ "* Mü\n"
+ ":PROPERTIES:\n"
+ ":EMAIL: testmu@testmu.xx\n"
+ ":END:\n\n");
+ else
+ assert_equal(res->standard_out,
+ "* Mü\n"
+ ":PROPERTIES:\n"
+ ":EMAIL: testmu@testmu.xx\n"
+ ":END:\n\n"
+ "* Helmut Kröger\n"
+ ":PROPERTIES:\n"
+ ":EMAIL: hk@testmu.xxx\n"
+ ":END:\n\n");
+}
+
+static void
+test_mu_cfind_csv(void)
+{
+ auto res{run_command({MU_PROGRAM, "--nocolor", "cfind", "--muhome", test_mu_home,
+ "--format", "csv", "testmu\\.xxx?"})};
+ assert_valid_result(res);
+
+ if (res->standard_out.at(1) == 'H')
+ assert_equal(res->standard_out,
+ "\"Helmut Kröger\",\"hk@testmu.xxx\"\n"
+ "\"Mü\",\"testmu@testmu.xx\"\n");
+ else
+ assert_equal(res->standard_out,
+ "\"Mü\",\"testmu@testmu.xx\"\n"
+ "\"Helmut Kröger\",\"hk@testmu.xxx\"\n");
+}
+
+
+static void
+test_mu_cfind_json()
+{
+ auto res{run_command({MU_PROGRAM, "--nocolor", "cfind", "--muhome", test_mu_home,
+ "--format", "json", "^a@example\\.com"})};
+ assert_valid_result(res);
+
+ const auto expected = R"([
+ {
+ "email" : "a@example.com",
+ "name" : null,
+ "display" : "a@example.com",
+ "last-seen" : 1463331445,
+ "last-seen-iso" : "2016-05-15T16:57:25Z",
+ "personal" : false,
+ "frequency" : 1
+ }
+]
+)";
+ assert_equal(res->standard_out, expected);
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ if (!set_en_us_utf8_locale())
+ return 0; /* don't error out... */
+
+ TempDir temp_dir{};
+ {
+ test_mu_home = temp_dir.path();
+
+ auto res1 = run_command({MU_PROGRAM, "--quiet", "init",
+ "--muhome", test_mu_home, "--maildir" , MU_TESTMAILDIR});
+ assert_valid_result(res1);
+
+ auto res2 = run_command({MU_PROGRAM, "--quiet", "index",
+ "--muhome", test_mu_home});
+ assert_valid_result(res2);
+ }
+
+ g_test_add_func("/cmd/find/plain", test_mu_cfind_plain);
+ g_test_add_func("/cmd/find/bbdb", test_mu_cfind_bbdb);
+ g_test_add_func("/cmd/find/wl", test_mu_cfind_wl);
+ g_test_add_func("/cmd/find/mutt-alias", test_mu_cfind_mutt_alias);
+ g_test_add_func("/cmd/find/mutt-ab", test_mu_cfind_mutt_ab);
+ g_test_add_func("/cmd/find/org-contact", test_mu_cfind_org_contact);
+ g_test_add_func("/cmd/find/csv", test_mu_cfind_csv);
+ g_test_add_func("/cmd/find/json", test_mu_cfind_json);
+
+ return g_test_run();
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2010-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+#include "mu-cmd.hh"
+#include "utils/mu-utils.hh"
+#include "utils/mu-utils-file.hh"
+#include "utils/mu-regex.hh"
+#include <message/mu-message.hh>
+
+using namespace Mu;
+
+static Result<void>
+save_part(const Message::Part& part, size_t idx, const Options& opts)
+{
+ const auto targetdir = std::invoke([&]{
+ const auto tdir{opts.extract.targetdir};
+ return tdir.empty() ? tdir : tdir + G_DIR_SEPARATOR_S;
+ });
+
+ /* 'uncooked' isn't really _raw_; it means only doing some _minimal_
+ * cooking */
+ const auto path{targetdir +
+ part.cooked_filename(opts.extract.uncooked)
+ .value_or(mu_format("part-{}", idx))};
+
+ if (auto&& res{part.to_file(path, opts.extract.overwrite)}; !res)
+ return Err(res.error());
+ else if (opts.extract.play)
+ return play(path);
+ else
+ return Ok();
+}
+
+static Result<void>
+save_parts(const Message& message, const std::string& filename_rx,
+ const Options& opts)
+{
+ size_t partnum{}, saved_num{};
+ for (auto&& part: message.parts()) {
+ ++partnum;
+ // should we extract this part?
+ const auto do_extract = std::invoke([&]() {
+
+ if (opts.extract.save_all)
+ return true;
+ else if (opts.extract.save_attachments &&
+ part.looks_like_attachment())
+ return true;
+ else if (seq_some(opts.extract.parts,
+ [&](auto&& num){return num==partnum;}))
+ return true;
+ else if (!filename_rx.empty() && part.raw_filename()) {
+ if (auto rx = Regex::make(filename_rx); !rx)
+ throw rx.error();
+ else if (rx->matches(*part.raw_filename()))
+ return true;
+ }
+ return false;
+ });
+
+ if (!do_extract)
+ continue;
+
+ if (auto res = save_part(part, partnum, opts); !res)
+ return res;
+
+ ++saved_num;
+ }
+
+ if (saved_num == 0)
+ return Err(Error::Code::File,
+ "no {} extracted from this message",
+ opts.extract.save_attachments ? "attachments" : "parts");
+ else
+ return Ok();
+}
+
+#define color_maybe(C) \
+ do { \
+ if (color) \
+ fputs((C), stdout); \
+ } while (0)
+
+static void
+show_part(const MessagePart& part, size_t index, bool color)
+{
+ /* index */
+ mu_print(" {} ", index);
+
+ /* filename */
+ color_maybe(MU_COLOR_GREEN);
+ const auto fname{part.raw_filename()};
+ fputs_encoded(fname.value_or("<none>"), stdout);
+ fputs_encoded(" ", stdout);
+
+ /* content-type */
+ color_maybe(MU_COLOR_BLUE);
+ const auto ctype{part.mime_type()};
+ fputs_encoded(ctype.value_or("<none>"), stdout);
+
+ /* /\* disposition *\/ */
+ color_maybe(MU_COLOR_MAGENTA);
+ mu_print_encoded(" [{}]", part.is_attachment() ? "attachment" : "inline");
+ /* size */
+ if (part.size() > 0) {
+ color_maybe(MU_COLOR_CYAN);
+ mu_print(" ({} bytes)", part.size());
+ }
+
+ color_maybe(MU_COLOR_DEFAULT);
+ fputs("\n", stdout);
+}
+
+static Mu::Result<void>
+show_parts(const Message& message, const Options& opts)
+{
+ size_t index{};
+ mu_println("MIME-parts in this message:");
+ for (auto&& part: message.parts())
+ show_part(part, ++index, !opts.nocolor);
+
+ return Ok();
+}
+
+Mu::Result<void>
+Mu::mu_cmd_extract(const Options& opts)
+{
+ auto message = std::invoke([&]()->Result<Message>{
+ const auto mopts{message_options(opts.extract)};
+ if (!opts.extract.message.empty())
+ return Message::make_from_path(opts.extract.message, mopts);
+
+ const auto msgtxt = read_from_stdin();
+ if (!msgtxt)
+ return Err(msgtxt.error());
+ else
+ return Message::make_from_text(*msgtxt, {}, mopts);
+ });
+
+ if (!message)
+ return Err(message.error());
+ else if (opts.extract.parts.empty() &&
+ !opts.extract.save_attachments && !opts.extract.save_all &&
+ opts.extract.filename_rx.empty())
+ return show_parts(*message, opts); /* show, don't save */
+
+ if (!check_dir(opts.extract.targetdir, false/*!readable*/, true/*writeable*/))
+ return Err(Error::Code::File,
+ "target '{}' is not a writable directory",
+ opts.extract.targetdir);
+
+ return save_parts(*message, opts.extract.filename_rx, opts);
+}
+
+
+
+
+#ifdef BUILD_TESTS
+/*
+ * Tests.
+ *
+ */
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <utils/mu-regex.hh>
+#include "utils/mu-test-utils.hh"
+
+
+static gint64
+get_file_size(const std::string& path)
+{
+ int rv;
+ struct stat statbuf;
+
+ mu_info("ppatj {}", path);
+
+ rv = stat(path.c_str(), &statbuf);
+ if (rv != 0) {
+ mu_debug ("error: {}", g_strerror (errno));
+ return -1;
+ }
+
+ mu_debug("{} -> {} bytes", path, statbuf.st_size);
+
+ return statbuf.st_size;
+}
+
+static void
+test_mu_extract_02(void)
+{
+ TempDir temp_dir{};
+ auto res= run_command({
+ MU_PROGRAM, "extract", "--save-attachments",
+ mu_format("--target-dir='{}'", temp_dir.path()),
+ join_paths(MU_TESTMAILDIR2, "Foo", "cur", "mail5")});
+ assert_valid_result(res);
+ g_assert_true(res->standard_err.empty());
+
+ g_assert_cmpuint(get_file_size(join_paths(temp_dir.path(), "custer.jpg")), >=, 15955);
+ g_assert_cmpuint(get_file_size(join_paths(temp_dir.path(), "custer.jpg")), <=, 15960);
+ g_assert_cmpuint(get_file_size(join_paths(temp_dir.path(), "sittingbull.jpg")), ==, 17674);
+}
+
+static void
+test_mu_extract_03(void)
+{
+ TempDir temp_dir{};
+ auto res= run_command({
+ MU_PROGRAM, "extract", "--parts=3",
+ mu_format("--target-dir='{}'", temp_dir.path()),
+ join_paths(MU_TESTMAILDIR2, "Foo", "cur", "mail5")});
+ assert_valid_result(res);
+ g_assert_true(res->standard_err.empty());
+
+ g_assert_true(g_access(join_paths(temp_dir.path(), "custer.jpg").c_str(), F_OK) == 0);
+ g_assert_false(g_access(join_paths(temp_dir.path(), "sittingbull.jpg").c_str(), F_OK) == 0);
+}
+
+static void
+test_mu_extract_overwrite(void)
+{
+ TempDir temp_dir{};
+ auto res= run_command({
+ MU_PROGRAM, "extract", "-a",
+ mu_format("--target-dir='{}'", temp_dir.path()),
+ join_paths(MU_TESTMAILDIR2, "Foo", "cur", "mail5")});
+ assert_valid_result(res);
+ g_assert_true(res->standard_err.empty());
+
+ g_assert_true(g_access(join_paths(temp_dir.path(), "custer.jpg").c_str(), F_OK) == 0);
+ g_assert_true(g_access(join_paths(temp_dir.path(), "sittingbull.jpg").c_str(), F_OK) == 0);
+
+
+ /* now, it should fail, because we don't allow overwrites
+ * without --overwrite */
+ auto res2 = run_command({
+ MU_PROGRAM, "extract", "-a",
+ mu_format("--target-dir='{}'", temp_dir.path()),
+ join_paths(MU_TESTMAILDIR2, "Foo", "cur", "mail5")});
+
+ assert_valid_result(res2);
+ g_assert_false(res2->standard_err.empty());
+
+
+ auto res3 = run_command({
+ MU_PROGRAM, "extract", "-a", "--overwrite",
+ mu_format("--target-dir='{}'", temp_dir.path()),
+ join_paths(MU_TESTMAILDIR2, "Foo", "cur", "mail5")});
+
+ assert_valid_result(res3);
+ g_assert_true(res3->standard_err.empty());
+}
+
+static void
+test_mu_extract_by_name(void)
+{
+ TempDir temp_dir{};
+ auto res= run_command({
+ MU_PROGRAM, "extract",
+ mu_format("--target-dir='{}'", temp_dir.path()),
+ join_paths(MU_TESTMAILDIR2, "Foo", "cur", "mail5"),
+ "sittingbull.jpg"});
+ assert_valid_result(res);
+ g_assert_true(res->standard_err.empty());
+
+ g_assert_true(g_access(join_paths(temp_dir.path(), "sittingbull.jpg").c_str(), F_OK) == 0);
+ g_assert_false(g_access(join_paths(temp_dir.path(), "custer.jpg").c_str(), F_OK) == 0);
+}
+
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/cmd/extract/02", test_mu_extract_02);
+ g_test_add_func("/cmd/extract/03", test_mu_extract_03);
+ g_test_add_func("/cmd/extract/overwrite", test_mu_extract_overwrite);
+ g_test_add_func("/cmd/extract/by-name", test_mu_extract_by_name);
+
+ return g_test_run();
+}
+
+#endif
--- /dev/null
+ /*
+** Copyright (C) 2008-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <array>
+
+#include <unistd.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <sys/wait.h>
+
+#include "message/mu-message.hh"
+#include "mu-maildir.hh"
+#include "mu-query-match-deciders.hh"
+#include "mu-query.hh"
+#include "mu-query-macros.hh"
+#include "mu-query-parser.hh"
+#include "message/mu-message.hh"
+
+#include "utils/mu-option.hh"
+
+#include "mu-cmd.hh"
+#include "utils/mu-utils.hh"
+
+using namespace Mu;
+
+using Format = Options::Find::Format;
+
+struct OutputInfo {
+ Xapian::docid docid{};
+ bool header{};
+ bool footer{};
+ bool last{};
+ Option<QueryMatch&> match_info;
+};
+
+constexpr auto FirstOutput{OutputInfo{0, true, false, {}, {}}};
+constexpr auto LastOutput{OutputInfo{0, false, true, {}, {}}};
+
+using OutputFunc = std::function<Result<void>(const Option<Message>& msg, const OutputInfo&,
+ const Options&)>;
+
+using Format = Options::Find::Format;
+
+static Result<void>
+analyze_query_expr(const Store& store, const std::string& expr, const Options& opts)
+{
+ auto print_item=[&](auto&&title, auto&&val) {
+ const auto blue{opts.nocolor ? "" : MU_COLOR_BLUE};
+ const auto green{opts.nocolor ? "" : MU_COLOR_GREEN};
+ const auto reset{opts.nocolor ? "" : MU_COLOR_DEFAULT};
+ mu_println("* {}{}{}:\n {}{}{}", blue, title, reset, green, val, reset);
+ };
+
+ print_item("query", expr);
+
+ const auto pq{parse_query(expr, false/*don't expand*/).to_string()};
+ const auto pqx{parse_query(expr, true/*do expand*/).to_string()};
+
+ print_item("parsed query", pq);
+ if (pq != pqx)
+ print_item("parsed query (expanded)", pqx);
+
+ auto xq{make_xapian_query(store, expr)};
+ if (!xq)
+ return Err(std::move(xq.error()));
+
+ print_item("Xapian query", xq->get_description());
+
+ return Ok();
+}
+
+static Result<QueryResults>
+run_query(const Store& store, const std::string& expr, const Options& opts)
+{
+ Mu::QueryFlags qflags{QueryFlags::SkipUnreadable};
+ if (opts.find.reverse)
+ qflags |= QueryFlags::Descending;
+ if (opts.find.skip_dups)
+ qflags |= QueryFlags::SkipDuplicates;
+ if (opts.find.include_related)
+ qflags |= QueryFlags::IncludeRelated;
+ if (opts.find.threads)
+ qflags |= QueryFlags::Threading;
+
+ return store.run_query(expr,
+ opts.find.sortfield,
+ qflags, opts.find.maxnum.value_or(0));
+}
+
+static Result<void>
+exec_cmd(const Option<Message>& msg, const OutputInfo& info, const Options& opts)
+{
+ if (!msg)
+ return Ok();
+
+ int wait_status{};
+ GError *err{};
+ auto cmdline{mu_format("{} {}", opts.find.exec,
+ to_string_gchar(g_shell_quote(msg->path().c_str())))};
+
+ if (!g_spawn_command_line_sync(cmdline.c_str(), {}, {}, &wait_status, &err))
+ return Err(Error::Code::File, &err/*consumed*/,
+ "failed to execute shell command");
+ else if (WEXITSTATUS(wait_status) != 0)
+ return Err(Error::Code::File,
+ "shell command exited with exit-code {}",
+ WEXITSTATUS(wait_status));
+ return Ok();
+}
+
+static Result<std::string>
+resolve_bookmark(const Store& store, const Options& opts)
+{
+ QueryMacros macros{store.config()};
+ if (auto&& res{macros.load_bookmarks(opts.runtime_path(RuntimePath::Bookmarks))}; !res)
+ return Err(res.error());
+ else if (auto&& bm{macros.find_macro(opts.find.bookmark)}; !bm)
+ return Err(Error::Code::InvalidArgument, "bookmark '{}' not found",
+ opts.find.bookmark);
+ else
+ return Ok(std::move(*bm));
+}
+
+static Result<std::string>
+get_query(const Store& store, const Options& opts)
+{
+ if (opts.find.bookmark.empty() && opts.find.query.empty())
+ return Err(Error::Code::InvalidArgument,
+ "neither bookmark nor query");
+
+ std::string bookmark;
+ if (!opts.find.bookmark.empty()) {
+ const auto res = resolve_bookmark(store, opts);
+ if (!res)
+ return Err(std::move(res.error()));
+ bookmark = res.value() + " ";
+ }
+
+ auto&& query{join(opts.find.query, " ")};
+ return Ok(bookmark + query);
+}
+
+static Result<void>
+prepare_links(const Options& opts)
+{
+ /* note, mu_maildir_mkdir simply ignores whatever part of the
+ * mail dir already exists */
+ if (auto&& res = maildir_mkdir(opts.find.linksdir, 0700, true); !res)
+ return Err(std::move(res.error()));
+
+ if (!opts.find.clearlinks)
+ return Ok();
+
+ if (auto&& res = maildir_clear_links(opts.find.linksdir); !res)
+ return Err(std::move(res.error()));
+
+ return Ok();
+}
+
+static Result<void>
+output_link(const Option<Message>& msg, const OutputInfo& info, const Options& opts)
+{
+ if (info.header)
+ return prepare_links(opts);
+ else if (info.footer)
+ return Ok();
+
+ /* during test, do not create "unique names" (i.e., names with path
+ * hashes), so we get a predictable result */
+ const auto unique_names{!g_getenv("MU_TEST")&&!g_test_initialized()};
+
+ if (auto&& res = maildir_link(msg->path(), opts.find.linksdir, unique_names); !res)
+ return Err(std::move(res.error()));
+
+ return Ok();
+}
+
+static void
+ansi_color_maybe(Field::Id field_id, bool color)
+{
+ const char* ansi;
+
+ if (!color)
+ return; /* nothing to do */
+
+ switch (field_id) {
+ case Field::Id::From: ansi = MU_COLOR_CYAN; break;
+
+ case Field::Id::To:
+ case Field::Id::Cc:
+ case Field::Id::Bcc: ansi = MU_COLOR_BLUE; break;
+ case Field::Id::Subject: ansi = MU_COLOR_GREEN; break;
+ case Field::Id::Date: ansi = MU_COLOR_MAGENTA; break;
+
+ default:
+ if (field_from_id(field_id).type != Field::Type::String)
+ ansi = MU_COLOR_YELLOW;
+ else
+ ansi = MU_COLOR_RED;
+ }
+
+ fputs(ansi, stdout);
+}
+
+static void
+ansi_reset_maybe(Field::Id field_id, bool color)
+{
+ if (!color)
+ return; /* nothing to do */
+
+ fputs(MU_COLOR_DEFAULT, stdout);
+}
+
+static std::string
+display_field(const Message& msg, Field::Id field_id)
+{
+ switch (field_from_id(field_id).type) {
+ case Field::Type::String:
+ return msg.document().string_value(field_id);
+ case Field::Type::Integer:
+ if (field_id == Field::Id::Priority) {
+ return to_string(msg.priority());
+ } else if (field_id == Field::Id::Flags) {
+ return to_string(msg.flags());
+ } else /* as string */
+ return msg.document().string_value(field_id);
+ case Field::Type::TimeT:
+ return mu_format("{:%c}",
+ mu_time(msg.document().integer_value(field_id)));
+ case Field::Type::ByteSize:
+ return to_string(msg.document().integer_value(field_id));
+ case Field::Type::StringList:
+ return join(msg.document().string_vec_value(field_id), ',');
+ case Field::Type::ContactList:
+ return to_string(msg.document().contacts_value(field_id));
+ default:
+ g_return_val_if_reached("");
+ return "";
+ }
+}
+
+static void
+print_summary(const Message& msg, const Options& opts)
+{
+ const auto body{msg.body_text()};
+ if (!body)
+ return;
+
+ const auto summ{summarize(body->c_str(), opts.find.summary_len.value_or(0))};
+
+ mu_print("Summary: ");
+ fputs_encoded(summ, stdout);
+ mu_println("");
+}
+
+static void
+thread_indent(const QueryMatch& info, const Options& opts)
+{
+ const auto is_root{any_of(info.flags & QueryMatch::Flags::Root)};
+ const auto first_child{any_of(info.flags & QueryMatch::Flags::First)};
+ const auto last_child{any_of(info.flags & QueryMatch::Flags::Last)};
+ const auto empty_parent{any_of(info.flags & QueryMatch::Flags::Orphan)};
+ const auto is_dup{any_of(info.flags & QueryMatch::Flags::Duplicate)};
+ // const auto is_related{any_of(info.flags & QueryMatch::Flags::Related)};
+
+ /* indent */
+ if (opts.debug) {
+ ::fputs(info.thread_path.c_str(), stdout);
+ ::fputs(" ", stdout);
+ } else
+ for (auto i = info.thread_level; i > 1; --i)
+ ::fputs(" ", stdout);
+
+ if (!is_root) {
+ if (first_child)
+ ::fputs("\\", stdout);
+ else if (last_child)
+ ::fputs("/", stdout);
+ else
+ ::fputs(" ", stdout);
+ ::fputs(empty_parent ? "*> " : is_dup ? "=> "
+ : "-> ",
+ stdout);
+ }
+}
+
+static void
+output_plain_fields(const Message& msg, const std::string& fields,
+ bool color, bool threads)
+{
+ size_t nonempty{};
+
+ for (auto&& k: fields) {
+ const auto field_opt{field_from_shortcut(k)};
+ if (!field_opt || (!field_opt->is_value() && !field_opt->is_contact()))
+ nonempty += printf("%c", k);
+
+ else {
+ ansi_color_maybe(field_opt->id, color);
+ nonempty += fputs_encoded(
+ display_field(msg, field_opt->id), stdout);
+ ansi_reset_maybe(field_opt->id, color);
+ }
+ }
+
+ if (nonempty)
+ fputs("\n", stdout);
+}
+
+static Result<void>
+output_plain(const Option<Message>& msg, const OutputInfo& info,
+ const Options& opts)
+{
+ if (!msg)
+ return Ok();
+
+ /* we reuse the color (whatever that may be)
+ * for message-priority for threads, too */
+ ansi_color_maybe(Field::Id::Priority, !opts.nocolor);
+ if (opts.find.threads && info.match_info)
+ thread_indent(*info.match_info, opts);
+
+ output_plain_fields(*msg, opts.find.fields, !opts.nocolor, opts.find.threads);
+
+ if (opts.view.summary_len)
+ print_summary(*msg, opts);
+
+ return Ok();
+}
+
+static Result<void>
+output_sexp(const Option<Message>& msg, const OutputInfo& info, const Options& opts)
+{
+ if (msg) {
+ if (const auto sexp{msg->sexp()}; !sexp.empty())
+ fputs(sexp.to_string().c_str(), stdout);
+ else
+ fputs(msg->sexp().to_string().c_str(), stdout);
+ fputs("\n", stdout);
+ }
+
+ return Ok();
+}
+
+static Result<void>
+output_json(const Option<Message>& msg, const OutputInfo& info, const Options& opts)
+{
+ if (info.header) {
+ mu_println("[");
+ return Ok();
+ }
+
+ if (info.footer) {
+ mu_println("]");
+ return Ok();
+ }
+
+ if (!msg)
+ return Ok();
+
+ mu_println("{}{}", msg->sexp().to_json_string(), info.last ? "" : ",");
+
+ return Ok();
+}
+
+static void
+print_attr_xml(const std::string& elm, const std::string& str)
+{
+ if (str.empty())
+ return; /* empty: don't include */
+
+ auto&& esc{to_string_opt_gchar(g_markup_escape_text(str.c_str(), -1))};
+ mu_println("\t\t<{}>{}</{}>", elm, esc.value_or(""), elm);
+}
+
+static Result<void>
+output_xml(const Option<Message>& msg, const OutputInfo& info, const Options& opts)
+{
+ if (info.header) {
+ mu_println("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
+ mu_println("<messages>");
+ return Ok();
+ }
+
+ if (info.footer) {
+ mu_println("</messages>");
+ return Ok();
+ }
+
+ mu_println("\t<message>");
+ print_attr_xml("from", to_string(msg->from()));
+ print_attr_xml("to", to_string(msg->to()));
+ print_attr_xml("cc", to_string(msg->cc()));
+ print_attr_xml("subject", msg->subject());
+ mu_println("\t\t<date>{}</date>", (unsigned)msg->date());
+ mu_println("\t\t<size>{}</size>", (unsigned)msg->size());
+ print_attr_xml("msgid", msg->message_id());
+ print_attr_xml("path", msg->path());
+ print_attr_xml("maildir", msg->maildir());
+ mu_println("\t</message>");
+
+ return Ok();
+}
+
+static OutputFunc
+get_output_func(const Options& opts)
+{
+ if (!opts.find.exec.empty())
+ return exec_cmd;
+
+ switch (opts.find.format) {
+ case Format::Links:
+ return output_link;
+ case Format::Plain:
+ return output_plain;
+ case Format::Xml:
+ return output_xml;
+ case Format::Sexp:
+ return output_sexp;
+ case Format::Json:
+ return output_json;
+ default:
+ throw Error(Error::Code::Internal,
+ "invalid format {}",
+ static_cast<size_t>(opts.find.format));
+ }
+}
+
+static Result<void>
+output_query_results(const QueryResults& qres, const Options& opts)
+{
+ GError* err{};
+ const auto output_func{get_output_func(opts)};
+ if (!output_func)
+ return Err(Error::Code::Query, &err, "failed to find output function");
+
+ if (auto&& res = output_func(Nothing, FirstOutput, opts); !res)
+ return Err(std::move(res.error()));
+
+ size_t n{0};
+ for (auto&& item : qres) {
+ n++;
+ auto msg{item.message()};
+ if (!msg)
+ continue;
+
+ if (msg->changed() < opts.find.after.value_or(0))
+ continue;
+
+ if (auto&& res = output_func(msg,
+ {item.doc_id(),
+ false,
+ false,
+ n == qres.size(), /* last? */
+ item.query_match()},
+ opts); !res)
+ return Err(std::move(res.error()));
+ }
+
+ if (auto&& res{output_func(Nothing, LastOutput, opts)}; !res)
+ return Err(std::move(res.error()));
+ else
+ return Ok();
+}
+
+static Result<void>
+process_store_query(const Store& store, const std::string& expr, const Options& opts)
+{
+ auto qres{run_query(store, expr, opts)};
+ if (!qres)
+ return Err(qres.error());
+
+ if (qres->empty())
+ return Err(Error::Code::NoMatches, "no matches for search expression");
+
+ return output_query_results(*qres, opts);
+}
+
+Result<void>
+Mu::mu_cmd_find(const Store& store, const Options& opts)
+{
+ auto expr{get_query(store, opts)};
+ if (!expr)
+ return Err(expr.error());
+
+ if (opts.find.analyze)
+ return analyze_query_expr(store, *expr, opts);
+ else
+ return process_store_query(store, *expr, opts);
+}
+
+
+
+#ifdef BUILD_TESTS
+/*
+ * Tests.
+ *
+ */
+
+#include "utils/mu-test-utils.hh"
+
+
+/* tests for the command line interface, uses testdir2 */
+
+static std::string test_mu_home;
+
+auto count_nl(const std::string& s)->size_t {
+ size_t n{};
+ for (auto&& c: s)
+ if (c == '\n')
+ ++n;
+ return n;
+}
+
+static size_t
+search_func(const std::string& expr, size_t expected)
+{
+ auto res = run_command({MU_PROGRAM, "find", "--muhome", test_mu_home, expr});
+ assert_valid_result(res);
+
+ /* we expect zero lines of error output if there is a match; otherwise
+ * there should be one line 'No matches found' */
+ if (res->exit_code != 0) {
+ g_assert_cmpuint(res->exit_code, ==, 2); // no match
+ g_assert_true(res->standard_out.empty());
+ g_assert_cmpuint(count_nl(res->standard_err), ==, 1);
+ return 0;
+ }
+
+ return count_nl(res->standard_out);
+}
+
+#define search(Q,EXP) do { \
+ g_assert_cmpuint(search_func(Q, EXP), ==, EXP); \
+} while(0)
+
+
+static void
+test_mu_find_empty_query(void)
+{
+ search("\"\"", 14);
+}
+
+static void
+test_mu_find_01(void)
+{
+ search("f:john fruit", 1);
+ search("f:soc@example.com", 1);
+ search("t:alki@example.com", 1);
+ search("t:alcibiades", 1);
+ search("http emacs", 1);
+ search("f:soc@example.com OR f:john", 2);
+ search("f:soc@example.com OR f:john OR t:edmond", 3);
+ search("t:julius", 1);
+ search("s:dude", 1);
+ search("t:dantès", 1);
+}
+
+/* index testdir2, and make sure it adds two documents */
+static void
+test_mu_find_02(void)
+{
+ search("bull", 1);
+ search("g:x", 0);
+ search("flag:encrypted", 0);
+ search("flag:attach", 1);
+
+ search("i:3BE9E6535E0D852173@emss35m06.us.lmco.com", 1);
+}
+
+static void
+test_mu_find_file(void)
+{
+ search("file:sittingbull.jpg", 1);
+ search("file:custer.jpg", 1);
+ search("file:custer.*", 1);
+ search("j:sit*", 1);
+}
+
+static void
+test_mu_find_mime(void)
+{
+ search("mime:image/jpeg", 1);
+ search("mime:text/plain", 14);
+ search("y:text*", 14);
+ search("y:image*", 1);
+ search("mime:message/rfc822", 2);
+}
+
+static void
+test_mu_find_text_in_rfc822(void)
+{
+ search("embed:dancing", 1);
+ search("e:curious", 1);
+ search("embed:with", 2);
+ search("e:karjala", 0);
+ search("embed:navigation", 1);
+}
+
+static void
+test_mu_find_maildir_special(void)
+{
+ search("\"maildir:/wOm_bàT\"", 3);
+ search("\"maildir:/wOm*\"", 3);
+ search("\"maildir:/wOm_*\"", 3);
+ search("\"maildir:wom_bat\"", 0);
+ search("\"maildir:/wombat\"", 0);
+ search("subject:atoms", 1);
+ search("\"maildir:/wom_bat\" subject:atoms", 1);
+}
+
+
+/* some more tests */
+
+static void
+test_mu_find_wrong_muhome()
+{
+ auto res = run_command({MU_PROGRAM, "find", "--muhome",
+ join_paths("/foo", "bar", "nonexistent"), "f:socrates"});
+ assert_valid_result(res);
+ g_assert_cmpuint(res->exit_code,==,1); // general error
+ g_assert_cmpuint(count_nl(res->standard_err), >, 1);
+}
+
+static void
+test_mu_find_links(void)
+{
+ TempDir temp_dir;
+
+ {
+ auto res = run_command({MU_PROGRAM, "find", "--muhome", test_mu_home,
+ "--format", "links", "--linksdir", temp_dir.path(),
+ "mime:message/rfc822"});
+ assert_valid_result(res);
+ g_assert_cmpuint(res->exit_code,==,0);
+ g_assert_cmpuint(count_nl(res->standard_out),==,0);
+ g_assert_cmpuint(count_nl(res->standard_err),==,0);
+ }
+
+
+ /* furthermore, two symlinks should be there */
+ const auto f1{mu_format("{}/cur/rfc822.1", temp_dir)};
+ const auto f2{mu_format("{}/cur/rfc822.2", temp_dir)};
+
+ g_assert_cmpuint(determine_dtype(f1.c_str(), true), ==, DT_LNK);
+ g_assert_cmpuint(determine_dtype(f2.c_str(), true), ==, DT_LNK);
+
+ /* now we try again, we should get a line of error output,
+ * when we find the first target file already exists */
+ {
+ auto res = run_command({MU_PROGRAM, "find", "--muhome", test_mu_home,
+ "--format", "links", "--linksdir", temp_dir.path(),
+ "mime:message/rfc822"});
+ assert_valid_result(res);
+ g_assert_cmpuint(res->exit_code,==,1);
+ g_assert_cmpuint(count_nl(res->standard_out),==,0);
+ g_assert_cmpuint(count_nl(res->standard_err),==,1);
+ }
+
+ /* now we try again with --clearlinks, and the we should be
+ * back to 0 errors */
+ {
+ auto res = run_command({MU_PROGRAM, "find", "--muhome", test_mu_home,
+ "--format", "links", "--clearlinks", "--linksdir", temp_dir.path(),
+ "mime:message/rfc822"});
+ assert_valid_result(res);
+ g_assert_cmpuint(res->exit_code,==,0);
+ g_assert_cmpuint(count_nl(res->standard_out),==,0);
+ g_assert_cmpuint(count_nl(res->standard_err),==,0);
+ }
+
+ g_assert_cmpuint(determine_dtype(f1.c_str(), true), ==, DT_LNK);
+ g_assert_cmpuint(determine_dtype(f2.c_str(), true), ==, DT_LNK);
+}
+
+/* some more tests */
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ if (!set_en_us_utf8_locale())
+ return 0; /* don't error out... */
+
+ TempDir temp_dir{};
+ {
+ test_mu_home = temp_dir.path();
+
+ auto res1 = run_command({MU_PROGRAM, "--quiet", "init",
+ "--muhome", test_mu_home, "--maildir" , MU_TESTMAILDIR2});
+ assert_valid_result(res1);
+
+ auto res2 = run_command({MU_PROGRAM, "--quiet", "index",
+ "--muhome", test_mu_home});
+ assert_valid_result(res2);
+ }
+
+ g_test_add_func("/cmd/find/empty-query", test_mu_find_empty_query);
+ g_test_add_func("/cmd/find/01", test_mu_find_01);
+ g_test_add_func("/cmd/find/02", test_mu_find_02);
+ g_test_add_func("/cmd/find/file", test_mu_find_file);
+ g_test_add_func("/cmd/find/mime", test_mu_find_mime);
+ g_test_add_func("/cmd/find/links", test_mu_find_links);
+ g_test_add_func("/cmd/find/text-in-rfc822", test_mu_find_text_in_rfc822);
+ g_test_add_func("/cmd/find/wrong-muhome", test_mu_find_wrong_muhome);
+ g_test_add_func("/cmd/find/maildir-special", test_mu_find_maildir_special);
+
+ return g_test_run();
+}
+
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2008-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+#include "mu-cmd.hh"
+
+#include <chrono>
+#include <thread>
+#include <atomic>
+
+#include <errno.h>
+#include <string.h>
+#include <cstdio>
+#include <signal.h>
+#include <unistd.h>
+
+#include "mu-store.hh"
+
+using namespace Mu;
+
+static std::atomic<bool> caught_signal;
+
+static void
+sig_handler(int _sig)
+{
+ caught_signal = true;
+}
+
+static void
+install_sig_handler(void)
+{
+ struct sigaction action;
+ int i, sigs[] = {SIGINT, SIGHUP, SIGTERM};
+
+ sigemptyset(&action.sa_mask);
+ action.sa_flags = SA_RESETHAND;
+ action.sa_handler = sig_handler;
+
+ for (i = 0; i != G_N_ELEMENTS(sigs); ++i)
+ if (sigaction(sigs[i], &action, NULL) != 0)
+ mu_critical("set sigaction for {} failed: {}",
+ sigs[i], g_strerror(errno));
+}
+
+static void
+print_stats(const Indexer::Progress& stats, bool color)
+{
+ const char* kars = "-\\|/";
+ static auto i = 0U;
+
+ MaybeAnsi col{color};
+ using Color = MaybeAnsi::Color;
+
+ mu_print("{}{}{} indexing messages; "
+ "checked: {}{}{}; "
+ "updated/new: {}{}{}; "
+ "cleaned-up: {}{}{}",
+ col.fg(Color::Yellow), kars[++i % 4], col.reset(),
+ col.fg(Color::Green), static_cast<size_t>(stats.checked), col.reset(),
+ col.fg(Color::Green), static_cast<size_t>(stats.updated), col.reset(),
+ col.fg(Color::Green), static_cast<size_t>(stats.removed), col.reset());
+}
+
+Result<void>
+Mu::mu_cmd_index(const Options& opts)
+{
+ auto store = std::invoke([&]{
+ if (opts.index.reindex)
+ return Store::make(opts.runtime_path(RuntimePath::XapianDb),
+ Store::Options::ReInit|Store::Options::Writable);
+ else
+ return Store::make(opts.runtime_path(RuntimePath::XapianDb),
+ Store::Options::Writable);
+ });
+
+ if (!store)
+ return Err(store.error());
+
+ const auto mdir{store->root_maildir()};
+ if (G_UNLIKELY(::access(mdir.c_str(), R_OK) != 0))
+ return Err(Error::Code::File, "'{}' is not readable: {}",
+ mdir, g_strerror(errno));
+
+ MaybeAnsi col{!opts.nocolor};
+ using Color = MaybeAnsi::Color;
+ if (!opts.quiet) {
+ if (opts.index.lazycheck)
+ mu_print("lazily ");
+
+ mu_println("indexing maildir {}{}{} -> "
+ "store {}{}{}",
+ col.fg(Color::Green), store->root_maildir(), col.reset(),
+ col.fg(Color::Blue), store->path(), col.reset());
+ }
+
+ Mu::Indexer::Config conf{};
+ conf.cleanup = !opts.index.nocleanup;
+ conf.lazy_check = opts.index.lazycheck;
+ // ignore .noupdate with an empty store.
+ conf.ignore_noupdate = store->empty();
+
+ install_sig_handler();
+
+ auto& indexer{store->indexer()};
+ indexer.start(conf);
+ while (!caught_signal && indexer.is_running()) {
+ if (!opts.quiet)
+ print_stats(indexer.progress(), !opts.nocolor);
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+
+ if (!opts.quiet) {
+ mu_print("\r");
+ ::fflush({});
+ }
+ }
+
+ indexer.stop();
+
+ if (!opts.quiet) {
+ print_stats(indexer.progress(), !opts.nocolor);
+ mu_print("\n");
+ ::fflush({});
+ }
+
+ return Ok();
+}
+
+
+#ifdef BUILD_TESTS
+
+/*
+ * Tests.
+ *
+ */
+#include <config.h>
+#include <mu-store.hh>
+#include "utils/mu-test-utils.hh"
+
+
+static void
+test_mu_index(size_t batch_size=0)
+{
+ TempDir temp_dir{};
+
+ const auto mu_home{temp_dir.path()};
+
+ auto res1 = run_command({MU_PROGRAM, "--quiet", "init", "--batch-size",
+ mu_format("{}", batch_size == 0 ? 10000 : batch_size),
+ "--muhome", mu_home, "--maildir" , MU_TESTMAILDIR2});
+ assert_valid_command(res1);
+
+ auto res2 = run_command({MU_PROGRAM, "--quiet", "index",
+ "--muhome", mu_home});
+ assert_valid_command(res2);
+
+ auto&& store = unwrap(Store::make(join_paths(temp_dir.path(), "xapian")));
+ g_assert_cmpuint(store.size(),==,14);
+}
+
+
+static void
+test_mu_index_basic()
+{
+ test_mu_index();
+}
+
+static void
+test_mu_index_batch()
+{
+ test_mu_index(2);
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/cmd/index/basic", test_mu_index_basic);
+ g_test_add_func("/cmd/index/batch", test_mu_index_batch);
+
+ return g_test_run();
+}
+
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include "mu-cmd.hh"
+#include <message/mu-message.hh>
+#include "utils/mu-utils.hh"
+
+#include <glib.h>
+#include <gmime/gmime.h>
+
+#include <fmt/ostream.h>
+
+#include <thirdparty/tabulate.hpp>
+
+using namespace Mu;
+using namespace tabulate;
+
+template <> struct fmt::formatter<Table> : ostream_formatter {};
+
+static void
+colorify(Table& table, const Options& opts)
+{
+ if (opts.nocolor || table.size() == 0)
+ return;
+
+ for (auto&& c = 0U; c != table.row(0).size(); ++c) {
+ switch (c) {
+ case 0:
+ table.column(c).format()
+ .font_color(Color::green)
+ .font_style({FontStyle::bold});
+ break;
+ case 1:
+ table.column(c).format()
+ .font_color(Color::blue);
+ break;
+ case 2:
+ table.column(c).format()
+ .font_color(Color::magenta);
+ break;
+
+ case 3:
+ table.column(c).format()
+ .font_color(Color::yellow);
+ break;
+ case 4:
+ table.column(c).format()
+ .font_color(Color::green);
+ break;
+ case 5:
+ table.column(c).format()
+ .font_color(Color::blue);
+ break;
+ case 6:
+ table.column(c).format()
+ .font_color(Color::magenta);
+ break;
+
+ case 7:
+ table.column(c).format()
+ .font_color(Color::yellow);
+ break;
+ default:
+ table.column(c).format()
+ .font_color(Color::grey);
+ break;
+ }
+ }
+
+ for (auto&& c = 0U; c != table.row(0).size(); ++c)
+ table[0][c].format()
+ .font_color(Color::white)
+ .font_style({FontStyle::bold});
+}
+
+
+static Result<void>
+topic_fields(const Options& opts)
+{
+ using namespace std::string_literals;
+
+ Table fields;
+ fields.add_row({"field-name", "alias", "short", "search",
+ "value", "sexp", "example query", "description"});
+
+ auto searchable=[&](const Field& field)->std::string {
+ if (field.is_boolean_term())
+ return "boolean";
+ if (field.is_phrasable_term())
+ return "phrase";
+ if (field.is_value())
+ return "yes";
+ if (field.is_contact())
+ return "contact";
+ if (field.is_range())
+ return "range";
+ return "no";
+ };
+
+ size_t row{};
+ field_for_each([&](auto&& field){
+ if (field.is_internal())
+ return; // skip.
+
+ fields.add_row({mu_format("{}", field.name),
+ field.alias.empty() ? "" : mu_format("{}", field.alias),
+ field.shortcut ? mu_format("{}", field.shortcut) : ""s,
+ searchable(field),
+ field.is_value() ? "yes" : "no",
+ field.include_in_sexp() ? "yes" : "no",
+ field.example_query,
+ field.description});
+ ++row;
+ });
+
+ colorify(fields, opts);
+
+ std::cout << "# Message fields\n" << fields << '\n';
+
+ return Ok();
+}
+
+static Result<void>
+topic_flags(const Options& opts)
+{
+ using namespace tabulate;
+ using namespace std::string_literals;
+
+ Table flags;
+ flags.add_row({"flag", "shortcut", "category", "description"});
+
+ flag_infos_for_each([&](const MessageFlagInfo& info) {
+
+ const auto catname = std::invoke(
+ [](MessageFlagCategory cat)->std::string {
+ switch(cat){
+ case MessageFlagCategory::Mailfile:
+ return "file";
+ case MessageFlagCategory::Maildir:
+ return "maildir";
+ case MessageFlagCategory::Content:
+ return "content";
+ case MessageFlagCategory::Pseudo:
+ return "pseudo";
+ default:
+ return {};
+ }
+ }, info.category);
+
+ flags.add_row({mu_format("{}", info.name),
+ mu_format("{}", info.shortcut),
+ catname,
+ std::string{info.description}});
+ });
+
+ colorify(flags, opts);
+
+ std::cout << "# Message flags\n" << flags << '\n';
+
+ return Ok();
+}
+
+static Result<void>
+topic_store(const Mu::Store& store, const Options& opts)
+{
+ auto tstamp = [](::time_t t)->std::string {
+ if (t == 0)
+ return "never";
+ else
+ return mu_format("{:%c}", mu_time(t));
+ };
+
+ Table info;
+ const auto conf{store.config()};
+ info.add_row({"property", "value"});
+ info.add_row({"maildir", store.root_maildir()});
+ info.add_row({"database-path", store.path()});
+ info.add_row({"schema-version",
+ mu_format("{}", conf.get<Config::Id::SchemaVersion>())});
+ info.add_row({"max-message-size", mu_format("{}", conf.get<Config::Id::MaxMessageSize>())});
+ info.add_row({"batch-size", mu_format("{}", conf.get<Config::Id::BatchSize>())});
+ info.add_row({"created", tstamp(conf.get<Config::Id::Created>())});
+
+ for (auto&& c : conf.get<Config::Id::PersonalAddresses>())
+ info.add_row({"personal-address", c});
+ for (auto&& c : conf.get<Config::Id::IgnoredAddresses>())
+ info.add_row({"ignored-address", c});
+
+ info.add_row({"messages in store", mu_format("{}", store.size())});
+ info.add_row({"support-ngrams", conf.get<Config::Id::SupportNgrams>() ? "yes" : "no"});
+
+ info.add_row({"last-change", tstamp(store.statistics().last_change)});
+ info.add_row({"last-index", tstamp(store.statistics().last_index)});
+
+ if (!opts.nocolor)
+ colorify(info, opts);
+
+ std::cout << info << '\n';
+
+ return Ok();
+}
+
+static Result<void>
+topic_maildirs(const Mu::Store& store, const Options& opts)
+{
+ for (auto&& mdir: store.maildirs())
+ mu_println("{}", mdir);
+
+ return Ok();
+}
+
+static Result<void>
+topic_mu(const Options& opts)
+{
+ Table info;
+
+ using namespace tabulate;
+
+ info.add_row({"property", "value", "description"});
+ info.add_row({"mu-version", std::string{VERSION}, "Mu runtime version"});
+ info.add_row({"xapian-version", Xapian::version_string(), "Xapian runtime version"});
+ info.add_row({"gmime-version",
+ mu_format("{}.{}.{}", gmime_major_version, gmime_minor_version,
+ gmime_micro_version), "GMime runtime version"});
+ info.add_row({"glib-version",
+ mu_format("{}.{}.{}", glib_major_version, glib_minor_version,
+ glib_micro_version), "GLib runtime version"});
+ info.add_row({"schema-version", mu_format("{}", MU_STORE_SCHEMA_VERSION),
+ "Version of mu's database schema"});
+
+ info.add_row({"cld2-support",
+#if HAVE_CLD2
+ "yes"
+#else
+ "no"
+#endif
+ , "Support searching by language-code?"});
+
+ info.add_row({"guile-support",
+#if BUILD_GUILE
+ "yes"
+#else
+ "no"
+#endif
+ , "GNU Guile 3.x scripting support?"});
+ info.add_row({"readline-support",
+#if HAVE_LIBREADLINE
+ "yes"
+#else
+ "no"
+#endif
+ , "Better 'm server' REPL for debugging?"});
+
+ if (!opts.nocolor)
+ colorify(info, opts);
+
+ std::cout << info << '\n';
+
+ return Ok();
+}
+
+
+Result<void>
+Mu::mu_cmd_info(const Mu::Store& store, const Options& opts)
+{
+ if (!locale_workaround())
+ return Err(Error::Code::User, "failed to find a working locale");
+
+ const auto topic{opts.info.topic};
+ if (topic == "store")
+ return topic_store(store, opts);
+ else if (topic == "maildirs")
+ return topic_maildirs(store, opts);
+ else if (topic == "fields") {
+ topic_fields(opts);
+ std::cout << std::endl;
+ return topic_flags(opts);
+ } else if (topic == "mu") {
+ return topic_mu(opts);
+ } else {
+ topic_mu(opts);
+
+ MaybeAnsi col{!opts.nocolor};
+ using Color = MaybeAnsi::Color;
+
+ auto topic = [&](auto&& t, auto&& d)->std::string {
+ return mu_format("{}{:<10}{} - {:>12}",
+ col.fg(Color::Green), t, col.reset(), d);
+ };
+
+ mu_println("\nother info topics ('mu info <topic>'):\n{}\n{}\n{}",
+ topic("store", "information about the message store (database)"),
+ topic("maildirs", "list the maildirs under the store's root-maildir"),
+ topic("fields", "information about message fields"));
+ }
+
+ return Ok();
+}
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include "mu-cmd.hh"
+
+using namespace Mu;
+
+#ifndef BUILD_TESTS
+
+Result<void>
+Mu::mu_cmd_init(const Options& opts)
+{
+ auto store = std::invoke([&]()->Result<Store> {
+
+ /*
+ * reinit
+ */
+ if (opts.init.reinit)
+ return Store::make(opts.runtime_path(RuntimePath::XapianDb),
+ Store::Options::ReInit|Store::Options::Writable);
+ /*
+ * full init
+ */
+
+ /* not provided, nor could we find a good default */
+ if (opts.init.maildir.empty())
+ return Err(Error::Code::InvalidArgument,
+ "missing --maildir parameter and could "
+ "not determine default");
+ else if (!g_path_is_absolute(opts.init.maildir.c_str()))
+ return Err(Error{Error::Code::File,
+ "--maildir is not absolute"});
+
+ MemDb mdb;
+ Config conf{mdb};
+
+ if (opts.init.max_msg_size)
+ conf.set<Config::Id::MaxMessageSize>(*opts.init.max_msg_size);
+ if (opts.init.batch_size && *opts.init.batch_size != 0)
+ conf.set<Config::Id::BatchSize>(*opts.init.batch_size);
+ if (!opts.init.my_addresses.empty())
+ conf.set<Config::Id::PersonalAddresses>(opts.init.my_addresses);
+ if (!opts.init.ignored_addresses.empty())
+ conf.set<Config::Id::IgnoredAddresses>(opts.init.ignored_addresses);
+ if (opts.init.support_ngrams)
+ conf.set<Config::Id::SupportNgrams>(true);
+
+ return Store::make_new(opts.runtime_path(RuntimePath::XapianDb),
+ opts.init.maildir, conf);
+ });
+
+ if (!store)
+ return Err(store.error());
+
+ if (!opts.quiet) {
+
+ mu_println("mu has been {} with the following properties:",
+ opts.init.reinit ? "reinitialized" : "created");
+ // mildly hacky
+ Options opts_copy{opts};
+ opts_copy.info.topic = "store";
+ mu_cmd_info(*store, opts_copy);
+
+ mu_println("Database is empty. You can use 'mu index' to fill it.");
+ }
+
+ return Ok();
+}
+
+
+
+#else /* BUILD_TESTS */
+
+/*
+ * Tests.
+ *
+ */
+#include <config.h>
+#include <mu-store.hh>
+#include "utils/mu-test-utils.hh"
+
+
+static void
+test_mu_init_basic()
+{
+ TempDir temp_dir{};
+
+ const auto mu_home{temp_dir.path()};
+
+ auto res1 = run_command({MU_PROGRAM, "--quiet", "init",
+ "--muhome", mu_home, "--maildir" , MU_TESTMAILDIR2});
+ assert_valid_command(res1);
+
+ auto&& store = unwrap(Store::make(join_paths(temp_dir.path(), "xapian")));
+ g_assert_true(store.empty());
+}
+
+static void
+test_mu_init_maildir()
+{
+ TempDir temp_dir{};
+
+ const auto mu_home{temp_dir.path()};
+
+ g_setenv("MAILDIR", MU_TESTMAILDIR2, 1);
+ auto res1 = run_command({MU_PROGRAM, "--quiet", "init",
+ "--muhome", mu_home});
+ assert_valid_command(res1);
+
+ auto&& store = unwrap(Store::make(join_paths(temp_dir.path(), "xapian")));
+ g_assert_true(store.empty());
+ assert_equal(store.root_maildir(), MU_TESTMAILDIR2);
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/cmd/init/basic", test_mu_init_basic);
+ g_test_add_func("/cmd/init/maildir", test_mu_init_maildir);
+
+ return g_test_run();
+}
+
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+#include "mu-cmd.hh"
+
+#include "mu-maildir.hh"
+
+using namespace Mu;
+
+Mu::Result<void>
+Mu::mu_cmd_mkdir(const Options& opts)
+{
+ for (auto&& dir: opts.mkdir.dirs) {
+ if (auto&& res =
+ maildir_mkdir(dir, opts.mkdir.mode); !res)
+ return res;
+ }
+
+ return Ok();
+}
+
+
+
+#ifdef BUILD_TESTS
+/*
+ * Tests.
+ *
+ */
+
+#include "utils/mu-test-utils.hh"
+
+static void
+test_mkdir_single()
+{
+ auto testroot{unwrap(make_temp_dir())};
+ auto testdir1{join_paths(testroot, "testdir1")};
+
+ auto res = run_command({MU_PROGRAM, "mkdir", testdir1});
+ assert_valid_command(res);
+
+ g_assert_true(check_dir(join_paths(testdir1, "cur"), true, true));
+ g_assert_true(check_dir(join_paths(testdir1, "new"), true, true));
+ g_assert_true(check_dir(join_paths(testdir1, "tmp"), true, true));
+}
+
+static void
+test_mkdir_multi()
+{
+ auto testroot{unwrap(make_temp_dir())};
+ auto testdir2{join_paths(testroot, "testdir2")};
+ auto testdir3{join_paths(testroot, "testdir3")};
+
+ auto res = run_command({MU_PROGRAM, "mkdir", testdir2, testdir3});
+ assert_valid_command(res);
+
+ g_assert_true(check_dir(join_paths(testdir2, "cur"), true, true));
+ g_assert_true(check_dir(join_paths(testdir2, "new"), true, true));
+ g_assert_true(check_dir(join_paths(testdir3, "tmp"), true, true));
+
+ g_assert_true(check_dir(join_paths(testdir3, "cur"), true, true));
+ g_assert_true(check_dir(join_paths(testdir3, "new"), true, true));
+ g_assert_true(check_dir(join_paths(testdir3, "tmp"), true, true));
+}
+
+int
+main(int argc, char* argv[]) try {
+
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/cmd/mkdir/single", test_mkdir_single);
+ g_test_add_func("/cmd/mkdir/multi", test_mkdir_multi);
+
+ return g_test_run();
+
+} catch (const Error& e) {
+ mu_printerrln("{}", e.what());
+ return 1;
+} catch (...) {
+ mu_printerrln("caught exception");
+ return 1;
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+#include "mu-cmd.hh"
+
+#include "mu-store.hh"
+#include "mu-maildir.hh"
+#include "message/mu-message-file.hh"
+
+ #include <unistd.h>
+
+using namespace Mu;
+
+
+Result<void>
+Mu::mu_cmd_move(Mu::Store& store, const Options& opts)
+{
+ const auto& src{opts.move.src};
+ if (::access(src.c_str(), R_OK) != 0 || determine_dtype(src) != DT_REG)
+ return Err(Error::Code::InvalidArgument,
+ "Source is not a readable file");
+
+ auto id{store.find_message_id(src)};
+ if (!id)
+ return Err(Error{Error::Code::InvalidArgument,
+ "Source file is not present in database"}
+ .add_hint("Perhaps run mu index?"));
+
+ std::string dest{opts.move.dest};
+ Option<const std::string&> dest_path;
+ if (dest.empty() && opts.move.flags.empty())
+ return Err(Error::Code::InvalidArgument,
+ "Must have at least one of destination and flags");
+ else if (!dest.empty()) {
+ const auto mdirs{store.maildirs()};
+
+ if (!seq_some(mdirs, [&](auto &&d){ return d == dest;}))
+ return Err(Error{Error::Code::InvalidArgument,
+ "No maildir '{}' in store", dest}
+ .add_hint("Try 'mu mkdir'"));
+ else
+ dest_path = dest;
+ }
+
+ auto old_flags{flags_from_path(src)};
+ if (!old_flags)
+ return Err(Error::Code::InvalidArgument, "failed to determine old flags");
+
+ Flags new_flags{};
+ if (!opts.move.flags.empty()) {
+ if (auto&& nflags{flags_from_expr(to_string_view(opts.move.flags),
+ *old_flags)}; !nflags)
+ return Err(Error::Code::InvalidArgument, "Invalid flags");
+ else
+ new_flags = flags_maildir_file(*nflags);
+
+ if (any_of(new_flags & Flags::New) && new_flags != Flags::New)
+ return Err(Error{Error::Code::File,
+ "the New flag cannot be combined with others"}
+ .add_hint("See the mu-move manpage"));
+ }
+
+ Store::MoveOptions move_opts{};
+ if (opts.move.change_name)
+ move_opts |= Store::MoveOptions::ChangeName;
+ if (opts.move.update_dups)
+ move_opts |= Store::MoveOptions::DupFlags;
+ if (opts.move.dry_run)
+ move_opts |= Store::MoveOptions::DryRun;
+
+ auto id_paths = store.move_message(*id, dest_path, new_flags, move_opts);
+ if (!id_paths)
+ return Err(std::move(id_paths.error()));
+
+ for (const auto&[_id, path]: *id_paths)
+ mu_println("{}", path);
+
+ return Ok();
+}
+
+
+
+#ifdef BUILD_TESTS
+/*
+ * Tests.
+ *
+ */
+
+#include "utils/mu-test-utils.hh"
+
+static void
+test_move_dry_run()
+{
+ allow_warnings();
+
+ TempDir tdir;
+ const auto dbpath{runtime_path(RuntimePath::XapianDb, tdir.path())};
+
+ auto res = run_command0({CP_PROGRAM, "-r", MU_TESTMAILDIR, tdir.path()});
+ assert_valid_command(res);
+
+ const auto testpath{join_paths(tdir.path(), "testdir")};
+ const auto src{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,S")};
+ {
+ auto store = Store::make_new(dbpath, testpath, {});
+ assert_valid_result(store);
+ g_assert_true(store->indexer().start({}, true/*block*/));
+ }
+
+ // make a message 'New'
+ {
+ auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
+ "--flags", "N", "--dry-run"});
+ assert_valid_command(res);
+
+ auto dst{join_paths(testpath, "new", "1220863042.12663_1.mindcrime")};
+ assert_equal(res->standard_out, dst + '\n');
+
+ g_assert_true(::access(dst.c_str(), F_OK) != 0);
+ g_assert_true(::access(src.c_str(), F_OK) == 0);
+ }
+
+ // change some flags
+ {
+ auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
+ "--flags", "FP", "--dry-run"});
+ assert_valid_command(res);
+
+ auto dst{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,FP")};
+ assert_equal(res->standard_out, dst + '\n');
+ }
+
+ // change some relative flag
+ {
+ auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
+ "--flags", "+F", "--dry-run"});
+ assert_valid_command(res);
+
+ auto dst{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,FS")};
+ assert_equal(res->standard_out, dst + '\n');
+ }
+
+ {
+ auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
+ "--flags", "-S+P+T", "--dry-run"});
+ assert_valid_command(res);
+
+ auto dst{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,PT")};
+ assert_equal(res->standard_out, dst + '\n');
+ }
+
+ // change maildir
+ for (auto& o : {"o1", "o2"})
+ assert_valid_result(maildir_mkdir(join_paths(tdir.path(), "testdir", o)));
+
+ {
+ auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
+ "/o1", "--flags", "-S+F", "--dry-run"});
+ assert_valid_command(res);
+ assert_equal(res->standard_out,
+ join_paths(testpath,
+ "o1/cur", "1220863042.12663_1.mindcrime!2,F") + "\n");
+ }
+
+ // change-dups; first create some dups and index them.
+ assert_valid_result(run_command0({CP_PROGRAM, src, join_paths(testpath, "o1/cur")}));
+ assert_valid_result(run_command0({CP_PROGRAM, src, join_paths(testpath, "o2/cur")}));
+ {
+ auto store = Store::make(dbpath, Store::Options::Writable);
+ assert_valid_result(store);
+ g_assert_true(store->indexer().start({}, true/*block*/));
+ }
+
+ // change some flags + update dups
+ {
+ auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
+ "--flags", "-S+S+T+R", "--update-dups", "--dry-run"});
+ assert_valid_command(res);
+
+ auto p{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,RST")};
+ auto p1{join_paths(testpath, "o1", "cur", "1220863042.12663_1.mindcrime!2,RS")};
+ auto p2{join_paths(testpath, "o2", "cur", "1220863042.12663_1.mindcrime!2,RS")};
+
+ assert_equal(res->standard_out, mu_format("{}\n{}\n{}\n", p, p1, p2));
+ }
+}
+
+
+static void
+test_move_real()
+{
+ allow_warnings();
+
+ TempDir tdir;
+ const auto dbpath{runtime_path(RuntimePath::XapianDb, tdir.path())};
+
+ auto res = run_command0({CP_PROGRAM, "-r", MU_TESTMAILDIR, tdir.path()});
+ assert_valid_command(res);
+
+ const auto testpath{join_paths(tdir.path(), "testdir")};
+ const auto src{join_paths(testpath, "cur", "1220863042.12663_1.mindcrime!2,S")};
+ {
+ auto store = Store::make_new(dbpath, testpath, {});
+ assert_valid_result(res);
+ g_assert_true(store->indexer().start({}, true/*block*/));
+ }
+
+ {
+ auto res = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src,
+ "--flags", "N"});
+ assert_valid_command(res);
+ auto dst{join_paths(testpath, "new", "1220863042.12663_1.mindcrime")};
+ g_assert_true(::access(dst.c_str(), F_OK) == 0);
+ g_assert_true(::access(src.c_str(), F_OK) != 0);
+ }
+
+ // change flags, maildir, update-dups
+ // change-dups; first create some dups and index them.
+ const auto src2{join_paths(testpath, "cur", "1305664394.2171_402.cthulhu!2,")};
+ for (auto& o : {"o1", "o2", "o3"})
+ assert_valid_result(maildir_mkdir(join_paths(tdir.path(), "testdir", o)));
+ assert_valid_result(run_command0({CP_PROGRAM, src2, join_paths(testpath, "o1/cur")}));
+ assert_valid_result(run_command0({CP_PROGRAM, src2, join_paths(testpath, "o2/new")}));
+ {
+ auto store = Store::make(dbpath, Store::Options::Writable);
+ assert_valid_result(store);
+ g_assert_true(store->indexer().start({}, true/*block*/));
+ }
+
+ auto res2 = run_command0({MU_PROGRAM, "move", "--muhome", tdir.path(), src2, "/o3",
+ "--flags", "-S+S+T+R", "--update-dups", "--change-name"});
+ assert_valid_command(res2);
+
+ auto store = Store::make(dbpath, Store::Options::Writable);
+ assert_valid_result(store);
+ g_assert_true(store->indexer().start({}, true/*block*/));
+
+ for (auto&& f: split(res2->standard_out, "\n")) {
+ //mu_println(">> {}", f);
+ if (f.length() > 2)
+ g_assert_true(::access(f.c_str(), F_OK) == 0);
+ }
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/cmd/move/dry-run", test_move_dry_run);
+ g_test_add_func("/cmd/move/real", test_move_real);
+
+ return g_test_run();
+
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+#include "mu-cmd.hh"
+
+using namespace Mu;
+
+Result<void>
+Mu::mu_cmd_remove(Mu::Store& store, const Options& opts)
+{
+ for (auto&& file: opts.remove.files) {
+ const auto res = store.remove_message(file);
+ if (!res)
+ return Err(Error::Code::File, "failed to remove {}", file.c_str());
+ else
+ mu_debug("removed message @ {}", file);
+ }
+
+ return Ok();
+}
+
+
+#ifdef BUILD_TESTS
+/*
+ * Tests.
+ *
+ */
+
+#include "utils/mu-test-utils.hh"
+
+static void
+test_remove_ok()
+{
+ auto testhome{unwrap(make_temp_dir())};
+ auto dbpath{runtime_path(RuntimePath::XapianDb, testhome)};
+
+ /* create a writable copy */
+ const auto testmdir = join_paths(testhome, "test-maildir");
+ const auto testmsg = join_paths(testmdir, "/cur/1220863042.12663_1.mindcrime!2,S");
+ auto cres = run_command({CP_PROGRAM, "-r", MU_TESTMAILDIR, testmdir});
+ assert_valid_command(cres);
+
+ {
+ auto&& store = unwrap(Store::make_new(dbpath, testmdir));
+ auto res = store.add_message(testmsg);
+ assert_valid_result(res);
+ g_assert_true(store.contains_message(testmsg));
+ }
+
+ { // remove the same
+ auto res = run_command({MU_PROGRAM, "remove",
+ mu_format("--muhome={}", testhome),
+ testmsg});
+ assert_valid_command(res);
+ }
+
+ {
+ auto&& store = unwrap(Store::make(dbpath));
+ g_assert_false(!!store.contains_message(testmsg));
+ g_assert_cmpuint(::access(testmsg.c_str(), F_OK), ==, 0);
+ }
+
+ remove_directory(testhome);
+}
+
+
+int
+main(int argc, char* argv[]) try {
+
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/cmd/remove/ok", test_remove_ok);
+
+ return g_test_run();
+
+} catch (const Error& e) {
+ mu_printerrln("{}", e.what());
+ return 1;
+} catch (...) {
+ mu_printerrln("caught exception");
+ return 1;
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2012-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include "mu-cmd.hh"
+#include "mu-script.hh"
+#include "utils/mu-utils.hh"
+
+using namespace Mu;
+
+Result<void>
+Mu::mu_cmd_script(const Options& opts)
+{
+ ScriptPaths paths = { MU_SCRIPTS_DIR };
+ const auto&& scriptinfos{script_infos(paths)};
+ auto script_it = Mu::seq_find_if(scriptinfos, [&](auto&& item) {
+ return item.name == opts.script.name;
+ });
+
+ if (script_it == scriptinfos.cend())
+ return Err(Error::Code::InvalidArgument,
+ "cannot find script '{}'", opts.script.name);
+
+ std::vector<std::string> params{opts.script.params};
+ if (!opts.muhome.empty()) {
+ params.emplace_back("--muhome");
+ params.emplace_back(opts.muhome);
+ }
+
+ // won't return unless there's an error.
+ return run_script(script_it->path, opts.script.params);
+}
--- /dev/null
+/*
+** Copyright (C) 2020-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <string>
+#include <algorithm>
+#include <atomic>
+#include <cstdio>
+
+#include <unistd.h>
+
+#include "mu-cmd.hh"
+#include "mu-server.hh"
+
+#include "utils/mu-utils.hh"
+#include "utils/mu-readline.hh"
+
+using namespace Mu;
+
+static std::atomic<int> MuTerminate{0};
+static bool tty;
+
+static void
+sig_handler(int sig)
+{
+ MuTerminate = sig;
+}
+
+static void
+install_sig_handler()
+{
+ MuTerminate = 0;
+
+ struct sigaction action{};
+ action.sa_handler = sig_handler;
+ sigemptyset(&action.sa_mask);
+ action.sa_flags = SA_RESETHAND;
+
+ for (auto sig: {SIGINT, SIGHUP, SIGTERM, SIGPIPE})
+ if (sigaction(sig, &action, NULL) != 0)
+ mu_critical("set sigaction for {} failed: {}",
+ sig, g_strerror(errno));
+}
+
+/*
+ * Markers for/after the length cookie that precedes the expression we write to
+ * output. We use octal 376, 377 (ie, 0xfe, 0xff) as they will never occur in
+ * utf8
+ */
+
+#define COOKIE_PRE "\376"
+#define COOKIE_POST "\377"
+
+static void
+cookie(size_t n)
+{
+ const auto num{static_cast<unsigned>(n)};
+
+ if (tty) // for testing.
+ ::printf("[%x]", num);
+ else
+ ::printf(COOKIE_PRE "%x" COOKIE_POST, num);
+}
+
+
+
+static void
+output_stdout(const std::string& str, Server::OutputFlags flags)
+{
+ cookie(str.size() + 1);
+ if (G_UNLIKELY(::puts(str.c_str()) < 0)) {
+ mu_critical("failed to write output '{}'", str);
+ ::raise(SIGTERM); /* terminate ourselves */
+ }
+ if (any_of(flags & Server::OutputFlags::Flush))
+ std::fflush(stdout);
+}
+
+
+static void
+report_error(const Mu::Error& err) noexcept
+{
+ output_stdout(Sexp(":error"_sym, Error::error_number(err.code()),
+ ":message"_sym, err.what()).to_string(),
+ Server::OutputFlags::Flush);
+}
+
+Result<void>
+Mu::mu_cmd_server(const Mu::Options& opts) try {
+
+ auto store = Store::make(opts.runtime_path(RuntimePath::XapianDb),
+ Store::Options::Writable);
+ if (!store)
+ return Err(store.error());
+
+ Server::Options sopts{};
+ sopts.allow_temp_file = opts.server.allow_temp_file;
+
+ Server server{*store, sopts, output_stdout};
+ mu_message("created server with store @ {}; maildir @ {}; debug-mode {};"
+ "readline: {}",
+ store->path(), store->root_maildir(),
+ opts.debug ? "yes" : "no",
+ have_readline() ? "yes" : "no");
+
+ tty = ::isatty(::fileno(stdout));
+ const auto eval = std::string{opts.server.commands ? "(help :full t)" : opts.server.eval};
+ if (!eval.empty()) {
+ server.invoke(eval);
+ return Ok();
+ }
+
+ // Note, the readline stuff is inactive unless on a tty.
+ const auto histpath{opts.runtime_path(RuntimePath::Cache) + "/history"};
+ setup_readline(histpath, 50);
+
+ install_sig_handler();
+ mu_println(";; Welcome to the " PACKAGE_STRING " command-server{}\n"
+ ";; Use (help) to get a list of commands, (quit) to quit.",
+ opts.debug ? " (debug-mode)" : "");
+
+ bool do_quit{};
+ while (!MuTerminate && !do_quit) {
+ std::fflush(stdout); // Needed for Windows, see issue #1827.
+ const auto line{read_line(do_quit)};
+ if (line.find_first_not_of(" \t") == std::string::npos)
+ continue; // skip whitespace-only lines
+
+ do_quit = server.invoke(line) ? false : true;
+ save_line(line);
+ }
+
+ if (MuTerminate != 0)
+ mu_message ("shutting down due to signal {}", MuTerminate.load());
+
+ shutdown_readline();
+
+ return Ok();
+
+} catch (const Error& er) { /* note: user-level error, "OK" for mu */
+ report_error(er);
+ mu_warning("server caught exception: {}", er.what());
+ return Ok();
+} catch (...) {
+ mu_critical("server caught exception");
+ return Err(Error::Code::Internal, "caught exception");
+}
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+#include "mu-cmd.hh"
+
+#include "message/mu-message.hh"
+#include "message/mu-mime-object.hh"
+
+#include <iostream>
+#include <iomanip>
+
+using namespace Mu;
+
+template <typename T>
+static void
+key_val(const Mu::MaybeAnsi& col, const std::string& key, T val)
+{
+ using Color = Mu::MaybeAnsi::Color;
+
+ mu_println("{}{:<18}{}: {}{}{}",
+ col.fg(Color::BrightBlue), key, col.reset(),
+ col.fg(Color::Green), val, col.reset());
+}
+
+static void
+print_signature(const Mu::MimeSignature& sig, const Options& opts)
+{
+ Mu::MaybeAnsi col{!opts.nocolor};
+
+ const auto created{sig.created()};
+ key_val(col, "created",
+ created == 0 ? std::string{"unknown"} :
+ mu_format("{:%c}", mu_time(sig.created())));
+
+ const auto expires{sig.expires()};
+ key_val(col, "expires", expires==0 ? std::string{"never"} :
+ mu_format("{:%c}", mu_time(sig.expires())));
+
+ const auto cert{sig.certificate()};
+ key_val(col, "public-key algo",
+ to_string_view_opt(cert.pubkey_algo()).value_or("unknown"));
+ key_val(col, "digest algo",
+ to_string_view_opt(cert.digest_algo()).value_or("unknown"));
+ key_val(col, "id-validity",
+ to_string_view_opt(cert.id_validity()).value_or("unknown"));
+ key_val(col, "trust",
+ to_string_view_opt(cert.trust()).value_or("unknown"));
+ key_val(col, "issuer-serial", cert.issuer_serial().value_or("unknown"));
+ key_val(col, "issuer-name", cert.issuer_name().value_or("unknown"));
+ key_val(col, "finger-print", cert.fingerprint().value_or("unknown"));
+ key_val(col, "key-id", cert.key_id().value_or("unknown"));
+ key_val(col, "name", cert.name().value_or("unknown"));
+ key_val(col, "user-id", cert.user_id().value_or("unknown"));
+}
+
+
+static bool
+verify(const MimeMultipartSigned& sigpart, const Options& opts)
+{
+ using VFlags = MimeMultipartSigned::VerifyFlags;
+ const auto vflags{opts.verify.auto_retrieve ?
+ VFlags::EnableKeyserverLookups: VFlags::None};
+
+ auto ctx{MimeCryptoContext::make_gpg()};
+ if (!ctx)
+ return false;
+
+ const auto sigs{sigpart.verify(*ctx, vflags)};
+ Mu::MaybeAnsi col{!opts.nocolor};
+
+ if (!sigs || sigs->empty()) {
+
+ if (!opts.quiet)
+ mu_println("cannot find signatures in part");
+
+ return true;
+ }
+
+ bool valid{true};
+ for (auto&& sig: *sigs) {
+
+ const auto status{sig.status()};
+
+ if (!opts.quiet)
+ key_val(col, "status", to_string(status));
+
+ if (opts.verbose)
+ print_signature(sig, opts);
+
+ if (none_of(sig.status() & MimeSignature::Status::Green))
+ valid = false;
+ }
+
+ return valid;
+}
+
+
+static bool
+verify_message(const Message& message, const Options& opts, const std::string& name)
+{
+ if (none_of(message.flags() & Flags::Signed)) {
+ if (!opts.quiet)
+ mu_println("{}: no signed parts found", name);
+ return false;
+ }
+
+ bool verified{true}; /* innocent until proven guilty */
+ for(auto&& part: message.parts()) {
+
+ if (!part.is_signed())
+ continue;
+
+ const auto& mobj{part.mime_object()};
+ if (!mobj.is_multipart_signed())
+ continue;
+
+ if (!verify(MimeMultipartSigned(mobj), opts))
+ verified = false;
+ }
+
+ return verified;
+}
+
+
+
+Mu::Result<void>
+Mu::mu_cmd_verify(const Options& opts)
+{
+ bool all_ok{true};
+ const auto mopts = message_options(opts.verify);
+
+ for (auto&& file: opts.verify.files) {
+
+ auto message{Message::make_from_path(file, mopts)};
+ if (!message)
+ return Err(message.error());
+
+ if (!opts.quiet && opts.verify.files.size() > 1)
+ mu_println("verifying {}", file);
+
+ if (!verify_message(*message, opts, file))
+ all_ok = false;
+ }
+
+ // when no messages provided, read from stdin
+ if (opts.verify.files.empty()) {
+ const auto msgtxt = read_from_stdin();
+ if (!msgtxt)
+ return Err(msgtxt.error());
+ auto message{Message::make_from_text(*msgtxt, {}, mopts)};
+ if (!message)
+ return Err(message.error());
+
+ all_ok = verify_message(*message, opts, "<stdin>");
+ }
+
+ if (all_ok)
+ return Ok();
+ else
+ return Err(Error::Code::UnverifiedSignature,
+ "failed to verify one or more signatures");
+}
+
+
+
+#ifdef BUILD_TESTS
+/*
+ * Tests.
+ *
+ */
+
+#include "utils/mu-test-utils.hh"
+
+/* we can only test 'verify' if gpg is installed, and has djcb@djcbsoftware's key in the keyring */
+static bool
+verify_is_testable(void)
+{
+ auto gpg{program_in_path("gpg2")};
+ if (!gpg) {
+ mu_message("cannot find gpg2 in path");
+ return false;
+ }
+
+ auto res{run_command({*gpg, "--list-keys", "DCC4A036"})}; /* djcb@djcbsoftware.nl's key */
+ if (!res || res->exit_code != 0) {
+ mu_message("key DCC4A036 not found");
+ return false;
+ }
+
+ return true;
+}
+
+static void
+test_mu_verify_good(void)
+{
+ if (!verify_is_testable()) {
+ g_test_skip("cannot test verify");
+ return;
+ }
+
+ auto res = run_command({MU_PROGRAM, "verify",
+ join_paths(MU_TESTMAILDIR4, "signed!2,S")});
+ assert_valid_result(res);
+ g_assert_cmpuint(res->exit_code ,==, 0);
+}
+
+static void
+test_mu_verify_bad(void)
+{
+ if (!verify_is_testable()) {
+ g_test_skip("cannot test verify");
+ return;
+ }
+
+ auto res = run_command({MU_PROGRAM, "verify",
+ join_paths(MU_TESTMAILDIR4, "signed-bad!2,S")});
+ assert_valid_result(res);
+ g_assert_cmpuint(res->exit_code,==, 1);
+}
+
+int
+main(int argc, char* argv[]) try {
+
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/cmd/verify/good", test_mu_verify_good);
+ g_test_add_func("/cmd/verify/bad", test_mu_verify_bad);
+
+ return g_test_run();
+
+} catch (const Error& e) {
+ mu_printerrln("{}", e.what());
+ return 1;
+} catch (...) {
+ mu_printerrln("caught exception");
+ return 1;
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+#include <config.h>
+
+#include "mu-cmd.hh"
+
+#include "message/mu-message.hh"
+
+#include <iostream>
+#include <iomanip>
+
+using namespace Mu;
+
+
+#define VIEW_TERMINATOR '\f' /* form-feed */
+
+using namespace Mu;
+
+static Mu::Result<void>
+view_msg_sexp(const Message& message, const Options& opts)
+{
+ ::fputs(message.sexp().to_string().c_str(), stdout);
+ ::fputs("\n", stdout);
+
+ return Ok();
+}
+
+
+static std::string /* return comma-sep'd list of attachments */
+get_attach_str(const Message& message, const Options& opts)
+{
+ std::string str;
+ seq_for_each(message.parts(), [&](auto&& part) {
+ if (auto fname = part.raw_filename(); fname) {
+ if (str.empty())
+ str = fname.value();
+ else
+ str += ", " + fname.value();
+ }
+ });
+
+ return str;
+}
+
+#define color_maybe(C) \
+ do { \
+ if (color) \
+ fputs((C), stdout); \
+ } while (0)
+
+static void
+print_field(const std::string& field, const std::string& val, bool color)
+{
+ if (val.empty())
+ return;
+
+ color_maybe(MU_COLOR_MAGENTA);
+ fputs_encoded(field, stdout);
+ color_maybe(MU_COLOR_DEFAULT);
+ fputs(": ", stdout);
+
+ color_maybe(MU_COLOR_GREEN);
+ fputs_encoded(val, stdout);
+
+ color_maybe(MU_COLOR_DEFAULT);
+ fputs("\n", stdout);
+}
+
+/* a summary_len of 0 mean 'don't show summary, show body */
+static void
+body_or_summary(const Message& message, const Options& opts)
+{
+ const auto color{!opts.nocolor};
+ using Format = Options::View::Format;
+
+ std::string body, btype;
+ switch (opts.view.format) {
+ case Format::Plain:
+ btype = "plain text";
+ body = message.body_text().value_or("");
+ break;
+ case Format::Html:
+ btype = "html";
+ body = message.body_html().value_or("");
+ break;
+ default:
+ throw std::range_error("unsupported format"); // bug
+ }
+
+ if (body.empty()) {
+ if (any_of(message.flags() & Flags::Encrypted)) {
+ color_maybe(MU_COLOR_CYAN);
+ mu_println("[No {} body found; message does have encrypted parts]",
+ btype);
+ } else {
+ color_maybe(MU_COLOR_MAGENTA);
+ mu_println("[No {} body found]", btype);
+ }
+ color_maybe(MU_COLOR_DEFAULT);
+ return;
+ }
+
+ if (opts.view.summary_len) {
+ const auto summ{summarize(body, *opts.view.summary_len)};
+ print_field("Summary", summ, color);
+ } else {
+ mu_print_encoded("{}", body);
+ if (!g_str_has_suffix(body.c_str(), "\n"))
+ mu_println("");
+ }
+}
+
+/* we ignore fields for now */
+/* summary_len == 0 means "no summary */
+static Mu::Result<void>
+view_msg_plain(const Message& message, const Options& opts)
+{
+ const auto color{!opts.nocolor};
+
+ print_field("From", to_string(message.from()), color);
+ print_field("To", to_string(message.to()), color);
+ print_field("Cc", to_string(message.cc()), color);
+ print_field("Bcc", to_string(message.bcc()), color);
+ print_field("Subject", message.subject(), color);
+
+ if (auto&& date = message.date(); date != 0)
+ print_field("Date", mu_format("{:%c}", mu_time(date)), color);
+
+ print_field("Tags", join(message.tags(), ", "), color);
+
+ print_field("Attachments",get_attach_str(message, opts), color);
+
+ mu_println("");
+ body_or_summary(message, opts);
+
+ return Ok();
+}
+
+static Mu::Result<void>
+handle_msg(const Message& message, const Options& opts)
+{
+ using Format = Options::View::Format;
+
+ switch (opts.view.format) {
+ case Format::Plain:
+ case Format::Html:
+ return view_msg_plain(message, opts);
+
+ case Format::Sexp:
+ return view_msg_sexp(message, opts);
+ default:
+ mu_critical("bug: should not be reached");
+ return Err(Error::Code::Internal, "error");
+ }
+}
+
+Mu::Result<void>
+Mu::mu_cmd_view(const Options& opts)
+{
+ for (auto&& file: opts.view.files) {
+ auto message{Message::make_from_path(
+ file, message_options(opts.view))};
+ if (!message)
+ return Err(message.error());
+
+ if (auto res = handle_msg(*message, opts); !res)
+ return res;
+ /* add a separator between two messages? */
+ if (opts.view.terminate)
+ mu_print("{}", VIEW_TERMINATOR);
+ }
+
+ // no files? read from stding
+ if (opts.view.files.empty()) {
+ const auto msgtxt = read_from_stdin();
+ if (!msgtxt)
+ return Err(msgtxt.error());
+ auto message = Message::make_from_text(*msgtxt,{},
+ message_options(opts.view));
+ if (!message)
+ return Err(message.error());
+ else
+ return handle_msg(*message, opts);
+ }
+ return Ok();
+}
+
+
+#ifdef BUILD_TESTS
+/*
+ * Tests.
+ *
+ */
+
+#include <fcntl.h> /* Definition of AT_* constants */
+#include <sys/stat.h>
+#include <fstream>
+#include <utils/mu-regex.hh>
+#include "utils/mu-test-utils.hh"
+
+static constexpr std::string_view test_msg =
+R"(From: Test <test@example.com>
+To: abc@example.com
+Date: Mon, 23 May 2011 10:53:45 +0200
+Subject: vla
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d"
+Message-ID: <10374608.109906.11909.20115aabbccdd.MSGID@mailinglijst.nl>
+
+--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d
+Content-Type: text/plain; charset="iso-8859-15"
+Content-Transfer-Encoding: quoted-printable
+
+text
+
+--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d
+Content-Type: text/html; charset="iso-8859-15"
+Content-Transfer-Encoding: quoted-printable
+
+html
+
+--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d--
+)";
+
+
+static std::string msgpath;
+
+static void
+test_view_plain()
+{
+ auto res = run_command({MU_PROGRAM, "view", msgpath});
+ assert_valid_command(res);
+ auto output{*res};
+
+ // silly hack to avoid locale diffs
+ auto rx = unwrap(Regex::make("^Date:.*", G_REGEX_MULTILINE));
+ output.standard_out = unwrap(rx.replace(output.standard_out, "Date: xxx"));
+
+ g_assert_true(output.standard_err.empty());
+ assert_equal(output.standard_out,
+R"(From: Test <test@example.com>
+To: abc@example.com
+Subject: vla
+Date: xxx
+
+text
+)");
+
+}
+
+static void
+test_view_html()
+{
+ auto res = run_command({MU_PROGRAM, "view", "--format=html", msgpath});
+ assert_valid_command(res);
+ auto output{*res};
+
+ auto rx = unwrap(Regex::make("^Date:.*", G_REGEX_MULTILINE));
+ output.standard_out = unwrap(rx.replace(output.standard_out, "Date: xxx"));
+
+ g_assert_true(output.standard_err.empty());
+ assert_equal(output.standard_out,
+R"(From: Test <test@example.com>
+To: abc@example.com
+Subject: vla
+Date: xxx
+
+html
+)");
+
+}
+
+static void
+test_view_sexp()
+{
+ TempTz tz("Europe/Amsterdam");
+ if (!tz.available()) {
+ g_test_skip("timezone not available");
+ return;
+ }
+
+ auto res = run_command({MU_PROGRAM, "view", "--format=sexp", msgpath});
+ assert_valid_command(res);
+ auto output{*res};
+
+ g_assert_true(output.standard_err.empty());
+
+ // Note: :path, :changed (file ctime) change per run.
+ struct stat statbuf{};
+ g_assert_true(::stat(msgpath.c_str(), &statbuf) == 0);
+
+ const auto expected = mu_format(
+ R"((:path "{}" :size 638 :changed ({} {} 0) :date (19930 8345 0) :flags (unread) :from ((:email "test@example.com" :name "Test")) :message-id "10374608.109906.11909.20115aabbccdd.MSGID@mailinglijst.nl" :priority normal :subject "vla" :to ((:email "abc@example.com")))
+)",
+ msgpath,
+ statbuf.st_ctime >> 16,
+ statbuf.st_ctime & 0xffff);
+
+ assert_equal(output.standard_out, expected);
+}
+
+static void
+test_mu_view_01(void)
+{
+ TempDir temp_dir{};
+
+ if (!set_en_us_utf8_locale()) {
+ g_test_skip("failed to switch to en_US/utf8");
+ return;
+ }
+
+ auto res = run_command({MU_PROGRAM, "view",
+ join_paths(MU_TESTMAILDIR2, "bar", "cur", "mail4")});
+ assert_valid_result(res);
+ g_assert_true(res->standard_err.empty());
+
+ g_assert_cmpuint(res->standard_out.size(), ==, 364);
+}
+
+static void
+test_mu_view_multi(void)
+{
+ TempDir temp_dir{};
+
+ if (!set_en_us_utf8_locale()) {
+ g_test_skip("failed to switch to en_US/utf8");
+ return;
+ }
+
+ auto res = run_command({MU_PROGRAM, "view",
+ join_paths(MU_TESTMAILDIR2, "bar", "cur", "mail5"),
+ join_paths(MU_TESTMAILDIR2, "bar", "cur", "mail5")});
+ assert_valid_result(res);
+ g_assert_true(res->standard_err.empty());
+
+ g_assert_cmpuint(res->standard_out.size(), ==, 162);
+}
+
+static void
+test_mu_view_multi_separate(void)
+{
+ TempDir temp_dir{};
+
+ if (!set_en_us_utf8_locale()) {
+ g_test_skip("failed to switch to en_US/utf8");
+ return;
+ }
+
+ auto res = run_command({MU_PROGRAM, "view", "--terminate",
+ join_paths(MU_TESTMAILDIR2, "bar", "cur", "mail5"),
+ join_paths(MU_TESTMAILDIR2, "bar", "cur", "mail5")});
+ assert_valid_result(res);
+ g_assert_true(res->standard_err.empty());
+
+ g_assert_cmpuint(res->standard_out.size(), ==, 164);
+}
+
+static void
+test_mu_view_attach(void)
+{
+ TempDir temp_dir{};
+
+ if (!set_en_us_utf8_locale()) {
+ g_test_skip("failed to switch to en_US/utf8");
+ return;
+ }
+
+ auto res = run_command({MU_PROGRAM, "view", "--terminate",
+ join_paths(MU_TESTMAILDIR2, "Foo", "cur", "mail5")});
+ assert_valid_result(res);
+ g_assert_true(res->standard_err.empty());
+
+ g_assert_cmpuint(res->standard_out.size(), ==, 164);
+}
+
+
+
+int
+main(int argc, char* argv[]) try {
+
+ TempDir tmpdir{};
+ msgpath = join_paths(tmpdir.path(), "test-message.txt");
+ std::ofstream strm{msgpath};
+ strm.write(test_msg.data(), test_msg.size());
+ strm.close();
+ g_assert_true(strm.good());
+
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/cmd/view/01", test_mu_view_01);
+ g_test_add_func("/cmd/view/multi", test_mu_view_multi);
+ g_test_add_func("/cmd/view/multi-separate", test_mu_view_multi_separate);
+ g_test_add_func("/cmd/view/attach", test_mu_view_attach);
+ g_test_add_func("/cmd/view/plain", test_view_plain);
+ g_test_add_func("/cmd/view/html", test_view_html);
+ g_test_add_func("/cmd/view/sexp", test_view_sexp);
+
+ return g_test_run();
+
+} catch (const Error& e) {
+ mu_printerrln("{}", e.what());
+ return 1;
+} catch (...) {
+ mu_printerrln("caught exception");
+ return 1;
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2010-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <iostream>
+#include <iomanip>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "mu-options.hh"
+#include "mu-cmd.hh"
+#include "mu-maildir.hh"
+#include "mu-contacts-cache.hh"
+#include "message/mu-message.hh"
+#include "message/mu-mime-object.hh"
+
+#include "utils/mu-error.hh"
+#include "utils/mu-utils-file.hh"
+#include "utils/mu-utils.hh"
+
+#include <thirdparty/tabulate.hpp>
+
+using namespace Mu;
+
+
+static Result<void>
+cmd_fields(const Options& opts)
+{
+ mu_printerrln("the 'mu fields' command has been superseded by 'mu info'; try:\n"
+ " mu info fields\n");
+ return Ok();
+}
+
+
+static Result<void>
+cmd_find(const Options& opts)
+{
+ auto store{Store::make(opts.runtime_path(RuntimePath::XapianDb))};
+ if (!store)
+ return Err(store.error());
+ else
+ return mu_cmd_find(*store, opts);
+}
+
+
+static void
+show_usage(void)
+{
+ mu_println("usage: mu command [options] [parameters]");
+ mu_println("where command is one of index, find, cfind, view, mkdir, "
+ "extract, add, remove, script, verify or server");
+ mu_println("see the mu, mu-<command> or mu-easy manpages for "
+ "more information");
+}
+
+
+using ReadOnlyStoreFunc = std::function<Result<void>(const Store&, const Options&)>;
+using WritableStoreFunc = std::function<Result<void>(Store&, const Options&)>;
+
+static Result<void>
+with_readonly_store(const ReadOnlyStoreFunc& func, const Options& opts)
+{
+ auto store{Store::make(opts.runtime_path(RuntimePath::XapianDb))};
+ if (!store)
+ return Err(store.error());
+
+ return func(store.value(), opts);
+}
+
+static Result<void>
+with_writable_store(const WritableStoreFunc func, const Options& opts)
+{
+ auto store{Store::make(opts.runtime_path(RuntimePath::XapianDb),
+ Store::Options::Writable)};
+ if (!store)
+ return Err(store.error());
+
+ return func(store.value(), opts);
+}
+
+Result<void>
+Mu::mu_cmd_execute(const Options& opts) try {
+
+ if (!opts.sub_command)
+ return Err(Error::Code::Internal, "missing subcommand");
+
+ switch (*opts.sub_command) {
+ case Options::SubCommand::Help:
+ return Ok(); /* already handled in mu-options.cc */
+ /*
+ * no store needed
+ */
+ case Options::SubCommand::Fields:
+ return cmd_fields(opts);
+ case Options::SubCommand::Mkdir:
+ return mu_cmd_mkdir(opts);
+ case Options::SubCommand::Script:
+ return mu_cmd_script(opts);
+ case Options::SubCommand::View:
+ return mu_cmd_view(opts);
+ case Options::SubCommand::Verify:
+ return mu_cmd_verify(opts);
+ case Options::SubCommand::Extract:
+ return mu_cmd_extract(opts);
+ /*
+ * read-only store
+ */
+
+ case Options::SubCommand::Cfind:
+ return with_readonly_store(mu_cmd_cfind, opts);
+ case Options::SubCommand::Find:
+ return cmd_find(opts);
+ case Options::SubCommand::Info:
+ return with_readonly_store(mu_cmd_info, opts);
+
+ /* writable store */
+
+ case Options::SubCommand::Add:
+ return with_writable_store(mu_cmd_add, opts);
+ case Options::SubCommand::Remove:
+ return with_writable_store(mu_cmd_remove, opts);
+ case Options::SubCommand::Move:
+ return with_writable_store(mu_cmd_move, opts);
+
+ /*
+ * commands instantiate store themselves
+ */
+ case Options::SubCommand::Index:
+ return mu_cmd_index(opts);
+ case Options::SubCommand::Init:
+ return mu_cmd_init(opts);
+ case Options::SubCommand::Server:
+ return mu_cmd_server(opts);
+
+ default:
+ show_usage();
+ return Ok();
+ }
+
+} catch (const Mu::Error& er) {
+ return Err(er);
+} catch (const std::runtime_error& re) {
+ return Err(Error::Code::Internal, "runtime-error: {}", re.what());
+} catch (const std::exception& ex) {
+ return Err(Error::Code::Internal, "error: {}", ex.what());
+} catch (...) {
+ return Err(Error::Code::Internal, "caught exception");
+}
--- /dev/null
+/*
+** Copyright (C) 2008-2022-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_CMD_HH__
+#define MU_CMD_HH__
+
+#include <glib.h>
+#include <mu-store.hh>
+#include <utils/mu-result.hh>
+
+#include "mu-options.hh"
+
+namespace Mu {
+
+
+/**
+ * Get message options from (sub)command options
+ *
+ * @param cmdopts (sub) command options
+ *
+ * @return message options
+ */
+template<typename CmdOpts>
+constexpr Message::Options
+message_options(const CmdOpts& cmdopts)
+{
+ Message::Options mopts{Message::Options::AllowRelativePath};
+
+ if (cmdopts.decrypt)
+ mopts |= Message::Options::Decrypt;
+ if (cmdopts.auto_retrieve)
+ mopts |= Message::Options::RetrieveKeys;
+
+ return mopts;
+}
+
+/**
+ * execute the 'add' command
+ *
+ * @param store store object to use
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_add(Store& store, const Options& opts);
+
+/**
+ * execute the 'cfind' command
+ *
+ * @param store store object to use
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_cfind(const Store& store, const Options& opts);
+
+/**
+ * execute the 'extract' command
+ *
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_extract(const Options& opts);
+
+/**
+ * execute the 'find' command
+ *
+ * @param store store object to use
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_find(const Store& store, const Options& opts);
+
+/**
+ * execute the 'index' command
+ *
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_index(const Options& opt);
+
+/**
+ * execute the 'info' command
+ *
+ * @param store message store object.
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_info(const Mu::Store& store, const Options& opts);
+
+/**
+ * execute the 'init' command
+ *
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_init(const Options& opts);
+
+/**
+ * execute the 'mkdir' command
+ *
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_mkdir(const Options& opts);
+
+/**
+ * execute the 'move' command
+ *
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_move(Store& store, const Options& opts);
+
+/**
+ * execute the 'remove' command
+ *
+ * @param store store object to use
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_remove(Store& store, const Options& opt);
+
+/**
+ * execute the 'script' command
+ *
+ * @param opts configuration options
+ * @param err receives error information, or NULL
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_script(const Options& opts);
+
+
+/**
+ * execute the server command
+ * @param opts configuration options
+ * @param err receives error information, or NULL
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_server(const Options& opts);
+
+/**
+ * execute the 'verify' command
+ *
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Mu::Result<void> mu_cmd_verify(const Options& opts);
+
+/**
+ * execute the 'view' command
+ *
+ * @param opts configuration options
+ *
+ * @return Ok() or some error
+ */
+Mu::Result<void> mu_cmd_view(const Options& opts);
+
+/**
+ * execute some mu command, based on 'opts'
+ *
+ * @param opts configuration option
+ * @param err receives error information, or NULL
+ *
+ * @return Ok() or some error
+ */
+Result<void> mu_cmd_execute(const Options& opts);
+
+} // namespace Mu
+
+#endif /*__MU_CMD_H__*/
--- /dev/null
+#!/bin/sh
+
+export G_SLICE=always-malloc
+export G_DEBUG=gc-friendly
+
+libtool --mode=execute valgrind --tool=memcheck --leak-check=full --show-possibly-lost=no --leak-resolution=med --track-origins=yes --num-callers=20 --log-file='@abs_top_builddir@/mu-%p.vgdump' @abs_top_builddir@/mu/mu $@
--- /dev/null
+/*
+** Copyright (C) 2022-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+/**
+ * @brief Command-line handling
+ *
+ * Here we implement mu's command-line parsing based on the CLI11 library. At
+ * the time of writing, that library seems to be the best based on the criteria
+ * that it supports the features we need and is available as a header-only
+ * include.
+ *
+ * CLI11 can do quite a bit, and we're only scratching the surface here,
+ * plan is to slowly improve things.
+ *
+ * - we do quite a bit of sanity-checking, but the errors are a rather terse
+ * - the docs could be improved, e.g., `mu find --help` and --format/--sortfield
+ *
+ */
+
+
+#include <config.h>
+#include <stdexcept>
+#include <array>
+#include <unordered_map>
+#include <iostream>
+#include <string_view>
+#include <unistd.h>
+
+#include <utils/mu-utils.hh>
+#include <utils/mu-utils-file.hh>
+#include <utils/mu-error.hh>
+#include "utils/mu-test-utils.hh"
+#include "mu-options.hh"
+#include "mu-script.hh"
+
+#include <thirdparty/CLI11.hpp>
+
+using namespace Mu;
+
+
+/*
+ * helpers
+ */
+
+
+
+/**
+ * array of associated pair elements -- like an alist
+ * but based on std::array and thus can be constexpr
+ */
+template<typename T1, typename T2, std::size_t N>
+ using AssocPairs = std::array<std::pair<T1, T2>, N>;
+
+
+/**
+ * Get the first value of the pair where the second element is @param s.
+ *
+ * @param p AssocPairs
+ * @param s some second pair value
+ *
+ * @return the matching first pair value, or Nothing if not found.
+ */
+template<typename P>
+constexpr Option<typename P::value_type::first_type>
+to_first(const P& p, typename P::value_type::second_type s)
+{
+ for (const auto& item: p)
+ if (item.second == s)
+ return item.first;
+ return Nothing;
+}
+
+/**
+ * Get the second value of the pair where the first element is @param f.
+ *
+ * @param p AssocPairs
+ * @param f some first pair value
+ *
+ * @return the matching second pair value, or Nothing if not found.
+ */
+template<typename P>
+constexpr Option<typename P::value_type::second_type>
+to_second(const P& p, typename P::value_type::first_type f)
+{
+ for (const auto& item: p)
+ if (item.first == f)
+ return item.second;
+ return Nothing;
+}
+
+
+/**
+ * Options-specific array-bases type that maps some enum to a <name, description> pair
+ */
+template<typename T, std::size_t N>
+using InfoEnum = AssocPairs<T, std::pair<std::string_view, std::string_view>, N>;
+
+/**
+ * Get the name (shortname) for some InfoEnum, based on the enum
+ *
+ * @param ie an InfoEnum
+ * @param e an enum value
+ *
+ * @return the name if found, or Nothing
+ */
+template<typename IE>
+static constexpr Option<std::string_view>
+to_name(const IE& ie, typename IE::value_type::first_type e) {
+ if (auto&& s{to_second(ie, e)}; s)
+ return s->first;
+ else
+ return Nothing;
+}
+
+/**
+ * Get the enum value for some InfoEnum, based on the name
+ *
+ * @param ie an InfoEnum
+ * @param name some name (shortname)
+ *
+ * @return the name if found, or Nothing
+ */
+template<typename IE>
+static constexpr Option<typename IE::value_type::first_type>
+to_enum(const IE& ie, std::string_view name) {
+ for(auto&& item: ie)
+ if (item.second.first == name)
+ return item.first;
+ else
+ return Nothing;
+}
+
+/**
+ * List help options for as a string, with the default marked with '(*)'
+ *
+ * @param ie infoenum
+ * @param default_opt default option
+ *
+ * @return a help string
+ */
+template<typename IE>
+static std::string
+options_help(const IE& ie, typename IE::value_type::first_type default_opt)
+{
+ std::string s;
+ for(auto&& item: ie) {
+ if (!s.empty())
+ s += ", ";
+ s += std::string{item.second.first};
+ if (item.first == default_opt)
+ s += "(*)"; /* default option */
+ }
+ return s;
+}
+
+
+/**
+ * Get map from string->type
+ */
+template<typename IE>
+static std::unordered_map<std::string, typename IE::value_type::first_type>
+options_map(const IE& ie)
+{
+ std::unordered_map<std::string, typename IE::value_type::first_type> map;
+ for (auto&& item : ie)
+ map.emplace(std::string{item.second.first}, item.first);
+
+ return map;
+}
+
+// transformers
+
+// Expand the path using wordexp
+static const std::function ExpandPath = [](std::string filepath)->std::string {
+ if (auto&& res{expand_path(filepath)}; !res)
+ throw CLI::ValidationError{res.error().what()};
+ else
+ return res.value();
+};
+
+// Canonicalize path
+static const std::function CanonicalizePath = [](std::string filepath)->std::string {
+ return filepath = canonicalize_filename(filepath);
+};
+
+/*
+ * common
+ */
+
+template<typename T>
+static void
+sub_crypto(CLI::App& sub, T& opts)
+{
+ sub.add_flag("--auto-retrieve,-r", opts.auto_retrieve,
+ "Attempt to automatically retrieve online keys");
+ sub.add_flag("--decrypt", opts.decrypt,
+ "Attempt to decrypt");
+}
+
+/*
+ * subcommands
+ */
+
+static void
+sub_add(CLI::App& sub, Options& opts)
+{
+ sub.add_option("files", opts.add.files,
+ "Path(s) to message files(s)")
+ ->required();
+}
+
+static void
+sub_cfind(CLI::App& sub, Options& opts)
+{
+ using Format = Options::Cfind::Format;
+ static constexpr InfoEnum<Format, 8> FormatInfos = {{
+ { Format::Plain, {"plain", "Plain output"} },
+ { Format::MuttAlias, {"mutt-alias", "Mutt alias"} },
+ { Format::MuttAddressBook, {"mutt-ab", "Mutt address book"}},
+ { Format::Wanderlust, {"wl", "Wanderlust"}},
+ { Format::OrgContact, {"org-contact", "org-contact"}},
+ { Format::Bbdb, {"bbdb", "Emacs BBDB"}},
+ { Format::Csv, {"csv", "comma-separated values"}},
+ { Format::Json, {"json", "format as json array"}},
+ }};
+
+ const auto fhelp = options_help(FormatInfos, Format::Plain);
+ const auto fmap = options_map(FormatInfos);
+
+ sub.add_option("--format,-o", opts.cfind.format,
+ "Output format; one of " + fhelp)
+ ->type_name("<format>")
+ ->default_str("plain")
+ ->default_val(Format::Plain)
+ ->transform(CLI::CheckedTransformer(fmap));
+
+ sub.add_option("pattern", opts.cfind.rx_pattern,
+ "Regular expression pattern to match");
+ sub.add_flag("--personal,-p", opts.cfind.personal,
+ "Only show 'personal' contacts");
+ sub.add_option("--after", opts.cfind.after,
+ "Only show results after some timestamps")
+ ->type_name("<time_t>")
+ ->check(CLI::PositiveNumber);
+ sub.add_option("--maxnum,-n", opts.cfind.maxnum,
+ "Maximum number of results")
+ ->type_name("<number>")
+ ->check(CLI::PositiveNumber);
+}
+
+
+
+static void
+sub_extract(CLI::App& sub, Options& opts)
+{
+ sub_crypto(sub, opts.extract);
+
+ sub.add_flag("--save-attachments,-a", opts.extract.save_attachments,
+ "Save all attachments");
+ sub.add_flag("--save-all", opts.extract.save_all, "Save all MIME parts")
+ ->excludes("--save-attachments");
+ sub.add_flag("--overwrite", opts.extract.overwrite,
+ "Overwrite existing files");
+ sub.add_flag("--play", opts.extract.play,
+ "Attempt to open the extracted parts");
+ sub.add_option("--parts", opts.extract.parts,
+ "Save specific parts (comma-sep'd list)")
+ ->type_name("<parts>")->delimiter(',');
+ sub.add_option("--target-dir", opts.extract.targetdir,
+ "Target directory for saving")
+ ->type_name("<dir>")
+ ->transform(ExpandPath, "expand target path")
+ ->default_str("<current>")
+ ->default_val(".");
+ sub.add_flag("--uncooked,-u", opts.extract.uncooked,
+ "Avoid massaging extracted file-names");
+ // optional; otherwise use standard-input
+ sub.add_option("message-path", opts.extract.message,
+ "Path to message file")
+ ->type_name("<message-path>");
+
+ sub.add_option("--matches", opts.extract.filename_rx,
+ "Regular expression for files to save")
+ ->type_name("<filename-rx>")
+ ->excludes("--parts")
+ ->excludes("--save-attachments")
+ ->excludes("--save-all");
+
+ // backward compat: filename-rx as non-option
+ sub.add_option("filename-rx", opts.extract.filename_rx,
+ "Regular expression for files to save")
+ ->type_name("<filename-rx>")
+ ->excludes("--parts")
+ ->excludes("--save-attachments")
+ ->excludes("--matches")
+ ->excludes("--save-all");
+}
+
+static void
+sub_fields(CLI::App& sub, Options& opts)
+{
+ // nothing to do.
+}
+
+
+static void
+sub_find(CLI::App& sub, Options& opts)
+{
+ using Format = Options::Find::Format;
+ static constexpr InfoEnum<Format, 7> FormatInfos = {{
+ { Format::Plain,
+ {"plain", "Plain output"}
+ },
+ { Format::Links,
+ {"links", "Maildir with symbolic links"}
+ },
+ { Format::Xml,
+ {"xml", "XML"}
+ },
+ { Format::Sexp,
+ {"sexp", "S-expressions"}
+ },
+ { Format::Json,
+ {"json", "JSON"}
+ },
+ }};
+
+ sub.add_flag("--threads,-t", opts.find.threads,
+ "Show message threads");
+ sub.add_flag("--skip-dups,-u", opts.find.skip_dups,
+ "Show only one of messages with same message-id");
+ sub.add_flag("--include-related,-r", opts.find.include_related,
+ "Include related messages in results");
+ sub.add_flag("--analyze,-a", opts.find.analyze,
+ "Analyze the query");
+
+ const auto fhelp = options_help(FormatInfos, Format::Plain);
+ const auto fmap = options_map(FormatInfos);
+
+ sub.add_option("--format,-o", opts.find.format,
+ "Output format; one of " + fhelp)
+ ->type_name("<format>")
+ ->default_str("plain")
+ ->default_val(Format::Plain)
+ ->transform(CLI::CheckedTransformer(fmap));
+
+ sub.add_option("--maxnum,-n", opts.find.maxnum,
+ "Maximum number of results")
+ ->type_name("<number>")
+ ->check(CLI::PositiveNumber);
+
+ sub.add_option("--fields,-f", opts.find.fields,
+ "Fields to display")
+ ->default_val("d f s");
+
+ std::unordered_map<std::string, Field::Id> smap;
+ std::string sopts;
+ field_for_each([&](auto&& field){
+ if (field.is_sortable()) {
+ smap.emplace(std::string(field.name), field.id);
+ smap.emplace(std::string(1, field.shortcut), field.id);
+ if (!sopts.empty())
+ sopts += ", ";
+ sopts += mu_format("{}|{}", field.name, field.shortcut);
+ }
+ });
+ sub.add_option("--sortfield,-s", opts.find.sortfield,
+ "Field to sort the results by; one of " + sopts)
+ ->type_name("<field>")
+ ->default_str("date")
+ ->default_val(Field::Id::Date)
+ ->transform(CLI::CheckedTransformer(smap));
+
+ sub.add_flag("--reverse,-z", opts.find.reverse,
+ "Sort in descending order");
+
+ sub.add_option("--bookmark,-b", opts.find.bookmark,
+ "Use bookmarked query")
+ ->type_name("<bookmark>");
+
+ sub.add_flag("--clearlinks", opts.find.clearlinks,
+ "Clear old links first");
+ sub.add_option("--linksdir", opts.find.linksdir,
+ "Use bookmarked query")
+ ->type_name("<dir>")
+ ->transform(ExpandPath, "expand linksdir path");
+
+ sub.add_option("--summary-len", opts.find.summary_len,
+ "Use up to so many lines for the summary")
+ ->type_name("<lines>")
+ ->check(CLI::PositiveNumber);
+
+ sub.add_option("--exec", opts.find.exec,
+ "Command to execute on message file")
+ ->type_name("<command>");
+
+ sub.add_option("query", opts.find.query,
+ "Search query pattern(s)")
+ ->type_name("<query>");
+}
+
+static void
+sub_help(CLI::App& sub, Options& opts)
+{
+ sub.add_option("command", opts.help.command,
+ "Command to request help for")
+ ->type_name("<command>");
+}
+
+static void
+sub_index(CLI::App& sub, Options& opts)
+{
+ sub.add_flag("--lazy-check", opts.index.lazycheck,
+ "Skip based on dir-timestamps");
+ sub.add_flag("--nocleanup", opts.index.nocleanup,
+ "Don't clean up database after indexing");
+ sub.add_flag("--reindex", opts.index.reindex,
+ "Perform a complete reindexing");
+}
+
+
+static void
+sub_info(CLI::App& sub, Options& opts)
+{
+ sub.add_option("topic", opts.info.topic,
+ "Information topic")
+ ->type_name("<topic>") ;
+}
+
+static void
+sub_init(CLI::App& sub, Options& opts)
+{
+ const auto default_mdir = std::invoke([]()->std::string {
+ if (const auto mdir_env{::getenv("MAILDIR")}; mdir_env)
+ return mdir_env;
+ else if (const auto mdir_home = ::join_paths(g_get_home_dir(), "Maildir");
+ check_dir(mdir_home))
+ return mdir_home;
+ else
+ return {};
+ });
+
+ sub.add_option("--maildir,-m", opts.init.maildir, "Top of the maildir")
+ ->type_name("<maildir>")
+ ->default_val(default_mdir)
+ ->transform(ExpandPath, "expand maildir path");
+ sub.add_option("--my-address", opts.init.my_addresses,
+ "Personal e-mail address or regexp")
+ ->type_name("<address>");
+ sub.add_option("--ignored-address", opts.init.ignored_addresses,
+ "Ignored e-mail address or regexp")
+ ->type_name("<address>");
+
+ sub.add_option("--max-message-size", opts.init.max_msg_size,
+ "Maximum allowed message size in bytes");
+ sub.add_option("--batch-size", opts.init.batch_size,
+ "Maximum size of database transaction");
+ sub.add_option("--support-ngrams", opts.init.support_ngrams,
+ "Support CJK n-grams if for querying/indexing");
+ sub.add_flag("--reinit", opts.init.reinit,
+ "Re-initialize database with current settings")
+ ->excludes("--maildir")
+ ->excludes("--my-address")
+ ->excludes("--ignored-address")
+ ->excludes("--max-message-size")
+ ->excludes("--batch-size")
+ ->excludes("--support-ngrams");
+}
+
+static void
+sub_mkdir(CLI::App& sub, Options& opts)
+{
+ sub.add_option("--mode", opts.mkdir.mode, "Set the access mode (octal)")
+ ->default_val(0755)
+ ->type_name("<mode>");
+
+ sub.add_option("dirs", opts.mkdir.dirs, "Path to directory/ies")
+ ->type_name("<dir>")
+ ->required();
+}
+
+
+static void
+sub_move(CLI::App& sub, Options& opts)
+{
+ sub.add_flag("--change-name", opts.move.change_name,
+ "Change name of target file");
+ sub.add_flag("--update-dups", opts.move.update_dups,
+ "Update duplicate messages too");
+ sub.add_flag("--dry-run,-n", opts.move.dry_run,
+ "Print target name, but do not change anything");
+
+ sub.add_option("--flags", opts.move.flags, "Target flags")
+ ->type_name("<flags>");
+
+ sub.add_option("source", opts.move.src, "Message file to move")
+ ->type_name("<message-path>")
+ ->transform(ExpandPath, "expand source path")
+ ->transform(CanonicalizePath, "canonicalize source path")
+ ->required();
+ sub.add_option("destination", opts.move.dest,
+ "Destination maildir")
+ ->type_name("<maildir>");
+}
+
+
+static void
+sub_remove(CLI::App& sub, Options& opts)
+{
+ sub.add_option("files", opts.remove.files,
+ "Paths to message files to remove")
+ ->type_name("<files>");
+}
+
+static void
+sub_server(CLI::App& sub, Options& opts)
+{
+ sub.add_flag("--commands", opts.server.commands,
+ "List available commands");
+ sub.add_option("--eval", opts.server.eval,
+ "Evaluate mu server expression")
+ ->excludes("--commands");
+ sub.add_flag("--allow-temp-file", opts.server.allow_temp_file,
+ "Allow for the temp-file optimization")
+ ->excludes("--commands");
+
+}
+
+static void
+sub_verify(CLI::App& sub, Options& opts)
+{
+ sub_crypto(sub, opts.verify);
+
+ // optional; otherwise use standard-input
+ sub.add_option("message-paths", opts.verify.files,
+ "Message files to verify")
+ ->type_name("<message-path>");
+}
+
+static void
+sub_view(CLI::App& sub, Options& opts)
+{
+ using Format = Options::View::Format;
+ static constexpr InfoEnum<Format, 3> FormatInfos = {{
+ { Format::Plain,
+ {"plain", "Plain output"}
+ },
+ { Format::Html,
+ {"html", "Plain output with HTML body"}
+ },
+ { Format::Sexp,
+ {"sexp", "S-expressions"}
+ },
+ }};
+
+ const auto fhelp = options_help(FormatInfos, Format::Plain);
+ const auto fmap = options_map(FormatInfos);
+
+ sub.add_option("--format,-o", opts.view.format,
+ "Output format; one of " + fhelp)
+ ->type_name("<format>")
+ ->default_str("plain")
+ ->default_val(Format::Plain)
+ ->transform(CLI::CheckedTransformer(fmap));
+
+ sub_crypto(sub, opts.view);
+
+ sub.add_option("--summary-len", opts.view.summary_len,
+ "Use up to so many lines for the summary")
+ ->type_name("<lines>")
+ ->check(CLI::PositiveNumber);
+
+ sub.add_flag("--terminate", opts.view.terminate,
+ "Insert form-feed after each message");
+
+ // optional; otherwise use standard-input
+ sub.add_option("message-paths", opts.view.files,
+ "Message files to view")
+ ->type_name("<message-path>");
+}
+
+
+using SubCommand = Options::SubCommand;
+using Category = Options::Category;
+
+struct CommandInfo {
+ Category category;
+ std::string_view name;
+ std::string_view help;
+
+ // std::function is not constexp-friendly
+ typedef void(*setup_func_t)(CLI::App&, Options&);
+ setup_func_t setup_func{};
+};
+
+static constexpr
+AssocPairs<SubCommand, CommandInfo, Options::SubCommandNum> SubCommandInfos= {{
+ { SubCommand::Add,
+ { Category::NeedsWritableStore,
+ "add", "Add message(s) to the database", sub_add}
+ },
+ { SubCommand::Cfind,
+ { Category::NeedsReadOnlyStore,
+ "cfind", "Find contacts matching pattern", sub_cfind}
+ },
+ { SubCommand::Extract,
+ {Category::None,
+ "extract", "Extract MIME-parts from messages", sub_extract}
+ },
+ { SubCommand::Fields,
+ {Category::None,
+ "fields", "Superseded by 'mu info'", sub_fields}
+ },
+ { SubCommand::Find,
+ {Category::NeedsReadOnlyStore,
+ "find", "Find messages matching query", sub_find }
+ },
+ { SubCommand::Help,
+ {Category::None,
+ "help", "Show help information", sub_help }
+ },
+ { SubCommand::Index,
+ {Category::NeedsWritableStore,
+ "index", "Store message information in the database", sub_index }
+ },
+ { SubCommand::Info,
+ {Category::NeedsReadOnlyStore,
+ "info", "Show information", sub_info }
+ },
+ { SubCommand::Init,
+ {Category::NeedsWritableStore,
+ "init", "Initialize the database", sub_init }
+ },
+ { SubCommand::Mkdir,
+ {Category::None,
+ "mkdir", "Create a new Maildir", sub_mkdir }
+ },
+ { SubCommand::Move,
+ {Category::NeedsWritableStore,
+ "move", "Move a message or change flags", sub_move }
+ },
+ { SubCommand::Remove,
+ {Category::NeedsWritableStore,
+ "remove", "Remove message from file-system and database", sub_remove }
+ },
+ { SubCommand::Script,
+ // Note: SubCommand::Script is special; there's no literal
+ // "script" subcommand, there subcommands for all the scripts.
+ {Category::None,
+ "script", "Invoke a script", {}}
+ },
+ { SubCommand::Server,
+ {Category::NeedsWritableStore,
+ "server", "Start a mu server (for mu4e)", sub_server}
+ },
+ { SubCommand::Verify,
+ {Category::None,
+ "verify", "Verify cryptographic signatures", sub_verify}
+ },
+ { SubCommand::View,
+ {Category::None,
+ "view", "View specific messages", sub_view}
+ },
+ }};
+
+
+
+static ScriptInfos
+add_scripts(CLI::App& app, Options& opts)
+{
+#ifndef BUILD_GUILE
+ return {};
+#else
+ ScriptPaths paths = { MU_SCRIPTS_DIR };
+ auto scriptinfos{script_infos(paths)};
+ for (auto&& script: scriptinfos) {
+ auto&& sub = app.add_subcommand(script.name)->group("Scripts")
+ ->description(script.oneline);
+ sub->add_option("params", opts.script.params,
+ "Parameter to script")
+ ->type_name("<params>");
+ }
+
+ return scriptinfos;
+#endif /*BUILD_GUILE*/
+}
+
+
+static Result<Options>
+show_manpage(Options& opts, const std::string& name)
+{
+ char *path = g_find_program_in_path("man");
+ if (!path)
+ return Err(Error::Code::Command,
+ "cannot find 'man' program");
+
+ GError* err{};
+ auto cmd{to_string_gchar(std::move(path)) + " " + name};
+ auto res = g_spawn_command_line_sync(cmd.c_str(), {}, {}, {}, &err);
+ if (!res)
+ return Err(Error::Code::Command, &err,
+ "error running man command");
+
+ return Ok(std::move(opts));
+}
+
+
+static Result<Options>
+cmd_help(const CLI::App& app, Options& opts)
+{
+ if (opts.help.command.empty()) {
+ mu_println("{}", app.help());
+ return Ok(std::move(opts));
+ }
+
+ for (auto&& item: SubCommandInfos) {
+ if (item.second.name == opts.help.command)
+ return show_manpage(opts, "mu-" + opts.help.command);
+ }
+
+ for (auto&& item: {"query", "easy"})
+ if (item == opts.help.command)
+ return show_manpage(opts, "mu-" + opts.help.command);
+
+ return Err(Error::Code::Command,
+ "no help available for '{}'", opts.help.command);
+}
+
+bool
+Options::default_no_color()
+{
+ static const auto no_color =
+ !::isatty(::fileno(stdout)) ||
+ !::isatty(::fileno(stderr)) ||
+ ::getenv("NO_COLOR") != NULL;
+
+ return no_color;
+}
+
+static void
+add_global_options(CLI::App& cli, Options& opts)
+{
+ opts.nocolor = Options::default_no_color();
+ errno = 0;
+
+ cli.add_flag("-q,--quiet", opts.quiet, "Hide non-essential output");
+ cli.add_flag("-v,--verbose", opts.verbose, "Show verbose output");
+ cli.add_flag("--log-stderr", opts.log_stderr, "Log to stderr")
+ ->group(""/*always hide*/);
+ cli.add_flag("--nocolor", opts.nocolor, "Don't show ANSI colors")
+ ->default_val(Options::default_no_color())
+ ->default_str(Options::default_no_color() ? "<true>" : "<false>");
+ cli.add_flag("-d,--debug", opts.debug, "Run in debug mode")
+ ->group(""/*always hide*/);
+}
+
+Result<Options>
+Options::make(int argc, char *argv[])
+{
+ Options opts{};
+ CLI::App app{"mu mail indexer/searcher", "mu"};
+
+ app.description(R"(mu mail indexer/searcher
+Copyright (C) 2008-2023 Dirk-Jan C. Binnema
+
+License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
+This is free software: you are free to change and redistribute it.
+There is NO WARRANTY, to the extent permitted by law.
+)");
+ app.set_version_flag("-V,--version", PACKAGE_VERSION);
+ app.set_help_flag("-h,--help", "Show help informmation");
+ app.set_help_all_flag("--help-all");
+ app.require_subcommand(0, 1);
+
+ add_global_options(app, opts);
+
+ /*
+ * subcommands
+ *
+ * we keep around a map of the subcommand pointers, so we can
+ * easily find the chosen one (if any) later.
+ */
+ for (auto&& cmdinfo: SubCommandInfos) {
+ //const auto cmdtype = cmdinfo.first;
+ const auto name{std::string{cmdinfo.second.name}};
+ const auto help{std::string{cmdinfo.second.help}};
+ const auto setup{cmdinfo.second.setup_func};
+ const auto cat{category(cmdinfo.first)};
+
+ if (!setup)
+ continue;
+
+ auto sub = app.add_subcommand(name, help);
+ setup(*sub, opts);
+
+ /* allow global options _after_ subcommand as well;
+ * this is for backward compat with the older
+ * command-line parsing */
+ sub->fallthrough(true);
+
+ /* store commands get the '--muhome' parameter as well */
+ if (cat == Category::NeedsReadOnlyStore ||
+ cat == Category::NeedsWritableStore)
+ sub->add_option("--muhome",
+ opts.muhome, "Specify alternative mu directory")
+ ->envname("MUHOME")
+ ->type_name("<dir>")
+ ->transform(ExpandPath, "expand muhome path");
+ }
+
+ /* add scripts (if supported) as semi-subcommands as well */
+ const auto scripts = add_scripts(app, opts);
+
+ try {
+ app.parse(argc, argv);
+
+ // find the chosen sub command, if any.
+ for (auto&& cmdinfo: SubCommandInfos) {
+ if (cmdinfo.first == SubCommand::Script)
+ continue; // not a _real_ subcommand.
+ const auto name{std::string{cmdinfo.second.name}};
+ if (app.got_subcommand(name)) {
+ opts.sub_command = cmdinfo.first;
+ }
+ }
+
+ // otherwise, perhaps it's a script?
+ if (!opts.sub_command) {
+ for (auto&& info: scripts) { // find the chosen script, if any.
+ if (app.got_subcommand(info.name)) {
+ opts.sub_command = SubCommand::Script;
+ opts.script.name = info.name;
+ }
+ }
+ }
+
+ // if nothing else, try "help"
+ if (opts.sub_command.value_or(SubCommand::Help) == SubCommand::Help)
+ return cmd_help(app, opts);
+
+ } catch (const CLI::CallForHelp& cfh) {
+ mu_println("{}", app.help());
+ } catch (const CLI::CallForAllHelp& cfah) {
+ mu_println("{}", app.help("", CLI::AppFormatMode::All));
+ } catch (const CLI::CallForVersion&) {
+ mu_println("version {}", PACKAGE_VERSION);
+ } catch (const CLI::ParseError& pe) {
+ return Err(Error::Code::InvalidArgument, "{}", pe.what());
+ } catch (...) {
+ return Err(Error::Code::Internal, "error parsing arguments");
+ }
+
+ return Ok(std::move(opts));
+}
+
+Category
+Options::category(Options::SubCommand sub)
+{
+ for (auto&& item: SubCommandInfos)
+ if (item.first == sub)
+ return item.second.category;
+
+ return Category::None;
+}
+
+/*
+ * trust but verify
+ */
+
+static constexpr bool
+validate_subcommand_ids()
+{
+ size_t val{};
+ for (auto& cmd: Options::SubCommands)
+ if (static_cast<size_t>(cmd) != val++)
+ return false;
+
+ for (auto u = 0U; u != SubCommandInfos.size(); ++u)
+ if (static_cast<size_t>(SubCommandInfos.at(u).first) != u)
+ return false;
+ return true;
+}
+
+
+/*
+ * tests... also build as runtime-tests, so we can get coverage info
+ */
+#ifdef BUILD_TESTS
+#define static_assert g_assert_true
+#endif /*BUILD_TESTS*/
+
+
+[[maybe_unused]]
+static void
+test_ids()
+{
+ static_assert(validate_subcommand_ids());
+}
+
+#ifdef BUILD_TESTS
+
+enum struct TestEnum { A, B, C };
+constexpr AssocPairs<TestEnum, std::string_view, 3>
+test_epairs = {{
+ {TestEnum::A, "a"},
+ {TestEnum::B, "b"},
+ {TestEnum::C, "c"},
+}};
+
+static constexpr Option<std::string_view>
+to_name(TestEnum te)
+{
+ return to_second(test_epairs, te);
+}
+
+static constexpr Option<TestEnum>
+to_type(std::string_view name)
+{
+ return to_first(test_epairs, name);
+
+}
+
+static void
+test_enum_pairs(void)
+{
+ assert_equal(to_name(TestEnum::A).value(), "a");
+ g_assert_true(to_type("c").value() == TestEnum::C);
+}
+
+int
+main(int argc, char* argv[])
+{
+ mu_test_init(&argc, &argv);
+
+ g_test_add_func("/options/ids", test_ids);
+ g_test_add_func("/option/enum-pairs", test_enum_pairs);
+
+ return g_test_run();
+}
+#endif /*BUILD_TESTS*/
--- /dev/null
+/*
+** Copyright (C) 2022-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#ifndef MU_OPTIONS_HH__
+#define MU_OPTIONS_HH__
+
+#include <sstream>
+#include <string>
+#include <vector>
+#include <utils/mu-option.hh>
+#include <utils/mu-result.hh>
+#include <utils/mu-utils.hh>
+#include <utils/mu-utils-file.hh>
+
+#include <message/mu-fields.hh>
+#include <mu-script.hh>
+#include <ctime>
+#include <sys/stat.h>
+
+/* command-line options for Mu */
+namespace Mu {
+struct Options {
+ using OptSize = Option<std::size_t>;
+ using SizeVec = std::vector<std::size_t>;
+ using OptTStamp = Option<std::time_t>;
+ using OptFieldId = Option<Field::Id>;
+ using StringVec = std::vector<std::string>;
+
+ /*
+ * general options
+ */
+ bool quiet; /**< don't give any output */
+ bool debug; /**< log debug-level info */
+ bool version; /**< request mu version */
+ bool log_stderr; /**< log to stderr */
+ bool nocolor; /**< don't use use ansi-colors */
+ bool verbose; /**< verbose output */
+ std::string muhome; /**< alternative mu dir */
+
+ /**
+ * Whether by default, we should show color
+ *
+ * @return true or false
+ */
+ static bool default_no_color();
+
+ enum struct SubCommand {
+ Add, Cfind, Extract, Fields, Find, Help, Index,Info, Init, Mkdir,
+ Move, Remove, Script, Server, Verify, View,
+ // <private>
+ __count__
+ };
+ static constexpr auto SubCommandNum = static_cast<size_t>(SubCommand::__count__);
+ static constexpr std::array<SubCommand, SubCommandNum> SubCommands = {{
+ SubCommand::Add,
+ SubCommand::Cfind,
+ SubCommand::Extract,
+ SubCommand::Fields,
+ SubCommand::Find,
+ SubCommand::Help,
+ SubCommand::Index,
+ SubCommand::Info,
+ SubCommand::Init,
+ SubCommand::Mkdir,
+ SubCommand::Move,
+ SubCommand::Remove,
+ SubCommand::Script,
+ SubCommand::Server,
+ SubCommand::Verify,
+ SubCommand::View
+ }};
+
+ Option<SubCommand> sub_command; /**< The chosen sub-command, if any. */
+
+ /*
+ * Add
+ */
+ struct Add {
+ StringVec files; /**< field to add */
+ } add;
+
+ /*
+ * Cfind
+ */
+ struct Cfind {
+ enum struct Format { Plain, MuttAlias, MuttAddressBook,
+ Wanderlust, OrgContact, Bbdb, Csv, Json };
+ Format format; /**< Output format */
+ bool personal; /**< only show personal contacts */
+ OptTStamp after; /**< only last seen after tstamp */
+ OptSize maxnum; /**< maximum number of results */
+ std::string rx_pattern; /**< contact regexp to match */
+ } cfind;
+
+
+ struct Crypto {
+ bool auto_retrieve; /**< auto-retrieve keys */
+ bool decrypt; /**< decrypt */
+ };
+
+ /*
+ * Extract
+ */
+ struct Extract: public Crypto {
+ std::string message; /**< path to message file */
+ bool save_all; /**< extract all parts */
+ bool save_attachments; /**< extract all attachment parts */
+ SizeVec parts; /**< parts to save / open */
+ std::string targetdir{}; /**< where to save attachments */
+ bool overwrite; /**< overwrite same-named files */
+ bool play; /**< try to 'play' attachment */
+ std::string filename_rx; /**< Filename rx to save */
+ bool uncooked{}; /**< Whether to avoid massaging
+ * the output filename */
+ } extract;
+
+ /*
+ * Fields
+ */
+
+ /*
+ * Find
+ */
+ struct Find {
+ std::string fields; /**< fields to show in output */
+ Field::Id sortfield; /**< field to sort by */
+ OptSize maxnum; /**< max # of entries to print */
+ bool reverse; /**< sort in revers order (z->a) */
+ bool threads; /**< show message threads */
+ bool clearlinks; /**< clear linksdir first */
+ std::string linksdir; /**< directory for links */
+ OptSize summary_len; /**< max # of lines for summary */
+ std::string bookmark; /**< use bookmark */
+ bool analyze; /**< analyze query */
+
+ enum struct Format { Plain, Links, Xml, Json, Sexp, Exec };
+ Format format; /**< Output format */
+ std::string exec; /**< cmd to execute on matches */
+ bool skip_dups; /**< show only first with msg id */
+ bool include_related; /**< included related messages */
+ /**< for find and cind */
+ OptTStamp after; /**< only last seen after T */
+ bool auto_retrieve; /**< assume we're online */
+ bool decrypt; /**< try to decrypt the body */
+
+ StringVec query; /**< search query */
+ } find;
+
+ struct Help {
+ std::string command; /**< Help parameter */
+ } help;
+
+ /*
+ * Index
+ */
+ struct Index {
+ bool nocleanup; /**< don't cleanup del'd mails */
+ bool lazycheck; /**< don't check uptodate dirs */
+ bool reindex; /**< do a full re-index */
+ } index;
+
+
+ /*
+ * Info
+ */
+ struct Info {
+ std::string topic; /**< what to get info about? */
+ } info;
+
+ /*
+ * Init
+ */
+ struct Init {
+ std::string maildir; /**< where the mails are */
+ StringVec my_addresses; /**< personal e-mail addresses */
+ StringVec ignored_addresses; /**< addresses to be ignored for
+ * the contacts-cache */
+ OptSize max_msg_size; /**< max size for message files */
+ OptSize batch_size; /**< db transaction batch size */
+ bool reinit; /**< re-initialize */
+ bool support_ngrams; /**< support CJK etc. ngrams */
+
+ } init;
+
+ /*
+ * Mkdir
+ */
+ struct Mkdir {
+ StringVec dirs; /**< Dir(s) to create */
+ mode_t mode; /**< Mode for the maildir */
+ } mkdir;
+
+ /*
+ * Move
+ */
+ struct Move {
+ std::string src; /**< Source file */
+ std::string dest; /**< Destination dir */
+ std::string flags; /**< Flags for destination */
+ bool change_name; /**< Change basename for destination */
+ bool update_dups; /**< Update duplicate messages too */
+ bool dry_run; /**< Just print the result path,
+ but do not change anything */
+ } move;
+
+ /*
+ * Remove
+ */
+ struct Remove {
+ StringVec files; /**< Files to remove */
+ } remove;
+
+ /*
+ * Scripts (i.e., finding scriot)
+ */
+ struct Script {
+ std::string name; /**< name of script */
+ StringVec params; /**< script params */
+ } script;
+
+ /*
+ * Server
+ */
+ struct Server {
+ bool commands; /**< dump docs for commands */
+ std::string eval; /**< command to evaluate */
+ bool allow_temp_file; /**< temp-file optimization allowed? */
+ } server;
+
+ /*
+ * Verify
+ */
+ struct Verify: public Crypto {
+ StringVec files; /**< message files to verify */
+ } verify;
+ /*
+ * View
+ */
+ struct View: public Crypto {
+ bool terminate; /**< add \f between msgs in view */
+ OptSize summary_len; /**< max # of lines for summary */
+
+ enum struct Format { Plain, Sexp, Html };
+ Format format; /**< output format*/
+
+ StringVec files; /**< Message file(s) */
+ } view;
+
+
+ /**
+ * Create an Options structure fo the given command-line arguments.
+ *
+ * @param argc argc
+ * @param argv argc
+ *
+ * @return Options, or an Error
+ */
+ static Result<Options> make(int argc, char *argv[]);
+
+
+ /**
+ * Different commands need different things
+ *
+ */
+ enum struct Category {
+ None,
+ NeedsReadOnlyStore,
+ NeedsWritableStore,
+ };
+
+ /**
+ * Get the category for some subcommand
+ *
+ * @param sub subcommand
+ *
+ * @return the category
+ */
+ static Category category(SubCommand sub);
+
+ /**
+ * Get some well-known Path
+ *
+ * @param path the Path to find
+ *
+ * @return the path name
+ */
+ std::string runtime_path(RuntimePath path) const {
+ return Mu::runtime_path(path, muhome);
+ }
+};
+
+} // namepace Mu
+
+#endif /* MU_OPTIONS_HH__ */
--- /dev/null
+/*
+** Copyright (C) 2008-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include <config.h>
+#include <functional>
+
+#include <glib.h>
+#include <glib-object.h>
+#include <locale.h>
+
+#include "mu-cmd.hh"
+#include "mu-options.hh"
+#include "utils/mu-utils.hh"
+#include "utils/mu-logger.hh"
+
+#include "mu-cmd.hh"
+
+using namespace Mu;
+
+
+static void
+output_error(const std::string& what, bool use_color)
+{
+ using Color = MaybeAnsi::Color;
+ MaybeAnsi col{use_color};
+
+ mu_printerrln("{}error{}: {}{}{}",
+ col.fg(Color::Red), col.reset(),
+ col.fg(Color::BrightYellow), what, col.reset());
+}
+
+static int
+handle_result(const Result<void>& res, const Mu::Options& opts)
+{
+ if (res)
+ return 0;
+
+ using Color = MaybeAnsi::Color;
+ MaybeAnsi col{!opts.nocolor};
+
+ // show the error and some help, but not if it's only a softerror.
+ if (!res.error().is_soft_error())
+ output_error(res.error().what(), !opts.nocolor);
+ else
+ mu_printerrln("{}{}{}",
+ col.fg(Color::BrightBlue), res.error().what(), col.reset());
+
+ // perhaps give some useful hint on how to solve it.
+ if (!res.error().hint().empty())
+ mu_printerrln("{}hint{}: {}{}{}",
+ col.fg(Color::Blue), col.reset(),
+ col.fg(Color::Green), res.error().hint(), col.reset());
+
+ if (res.error().exit_code() != 0 && !res.error().is_soft_error()) {
+ mu_warning("mu finishing with error: {}",
+ format_as(res.error()));
+ if (const auto& hint = res.error().hint(); !hint.empty())
+ mu_info("hint: {}", hint);
+ }
+
+ return res.error().exit_code();
+}
+
+int
+main(int argc, char* argv[]) try
+{
+ /*
+ * We handle this through explicit options
+ */
+ g_unsetenv("XAPIAN_CJK_NGRAM");
+
+ /*
+ * set up locale
+ */
+ ::setlocale(LC_ALL, "");
+
+ /*
+ * read command-line options
+ */
+ const auto opts{Options::make(argc, argv)};
+ if (!opts) {
+ output_error(opts.error().what(), !Options::default_no_color());
+ return opts.error().exit_code();
+ } else if (!opts->sub_command) {
+ // nothing more to do.
+ return 0;
+ }
+
+ // setup logging
+ Logger::Options lopts{Logger::Options::None};
+ if (opts->log_stderr)
+ lopts |= Logger::Options::StdOutErr;
+ if (opts->debug)
+ lopts |= Logger::Options::Debug;
+ if (!!g_getenv("MU_TEST"))
+ lopts |= Logger::Options::File;
+
+ const auto logger{Logger::make(opts->runtime_path(RuntimePath::LogFile), lopts)};
+ if (!logger) {
+ output_error(logger.error().what(), !opts->nocolor);
+ return logger.error().exit_code();
+ }
+
+ /*
+ * handle sub command
+ */
+ return handle_result(mu_cmd_execute(*opts), *opts);
+
+ // exceptions should have been handled earlier, but catch them here,
+ // just in case...
+} catch (const std::logic_error& le) {
+ mu_printerrln("caught logic-error: {}", le.what());
+ return 97;
+} catch (const std::runtime_error& re) {
+ mu_printerrln("caught runtime-error: {}", re.what());
+ return 98;
+} catch (...) {
+ mu_printerrln("caught exception");
+ return 99;
+}
--- /dev/null
+/*
+** Copyright (C) 2011-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#define _POSIX_C_SOURCE 1
+
+#include <gmime/gmime.h>
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <locale.h>
+
+static gchar*
+get_recip(GMimeMessage* msg, GMimeAddressType atype)
+{
+ char* recep;
+ InternetAddressList* receps;
+
+ receps = g_mime_message_get_addresses(msg, atype);
+ recep = (char*)internet_address_list_to_string(receps, NULL, FALSE);
+
+ if (!recep || !*recep) {
+ g_free(recep);
+ return NULL;
+ }
+
+ return recep;
+}
+
+static gchar*
+get_refs_str(GMimeMessage* msg)
+{
+ const gchar* str;
+ GMimeReferences* mime_refs;
+ int i, refs_len;
+ gchar* rv;
+
+ str = g_mime_object_get_header(GMIME_OBJECT(msg), "References");
+ if (!str)
+ return NULL;
+
+ mime_refs = g_mime_references_parse(NULL, str);
+ refs_len = g_mime_references_length(mime_refs);
+ for (rv = NULL, i = 0; i < refs_len; ++i) {
+ const char* msgid;
+ char *tmp;
+
+ msgid = g_mime_references_get_message_id(mime_refs, i);
+ tmp = rv;
+ rv = g_strdup_printf("%s%s%s", rv ? rv : "", rv ? "," : "", msgid);
+ g_free(tmp);
+ }
+ g_mime_references_free(mime_refs);
+
+ return rv;
+}
+
+static void
+print_date(GMimeMessage* msg)
+{
+ GDateTime* dt;
+ gchar* buf;
+
+ dt = g_mime_message_get_date(msg);
+ if (!dt)
+ return;
+
+ dt = g_date_time_to_local(dt);
+ buf = g_date_time_format(dt, "%c");
+ g_date_time_unref(dt);
+
+ if (buf) {
+ g_print("Date : %s\n", buf);
+ g_free(buf);
+ }
+}
+
+static void
+print_body(GMimeMessage* msg)
+{
+ GMimeObject* body;
+ GMimeDataWrapper* wrapper;
+ GMimeStream* stream;
+
+ body = g_mime_message_get_body(msg);
+
+ if (GMIME_IS_MULTIPART(body))
+ body = g_mime_multipart_get_part(GMIME_MULTIPART(body), 0);
+ if (!GMIME_IS_PART(body))
+ return;
+
+ wrapper = g_mime_part_get_content(GMIME_PART(body));
+ if (!GMIME_IS_DATA_WRAPPER(wrapper))
+ return;
+
+ stream = g_mime_data_wrapper_get_stream(wrapper);
+ if (!GMIME_IS_STREAM(stream))
+ return;
+
+ do {
+ char buf[512];
+ ssize_t len;
+
+ len = g_mime_stream_read(stream, buf, sizeof(buf));
+ if (len == -1)
+ break;
+
+ if (write(fileno(stdout), buf, len) == -1)
+ break;
+
+ if (len < (int)sizeof(buf))
+ break;
+
+ } while (1);
+}
+
+static gboolean
+test_message(GMimeMessage* msg)
+{
+ gchar* val;
+ const gchar* str;
+
+ val = get_recip(msg, GMIME_ADDRESS_TYPE_FROM);
+ g_print("From : %s\n", val ? val : "<none>");
+ g_free(val);
+
+ val = get_recip(msg, GMIME_ADDRESS_TYPE_TO);
+ g_print("To : %s\n", val ? val : "<none>");
+ g_free(val);
+
+ val = get_recip(msg, GMIME_ADDRESS_TYPE_CC);
+ g_print("Cc : %s\n", val ? val : "<none>");
+ g_free(val);
+
+ val = get_recip(msg, GMIME_ADDRESS_TYPE_BCC);
+ g_print("Bcc : %s\n", val ? val : "<none>");
+ g_free(val);
+
+ str = g_mime_message_get_subject(msg);
+ g_print("Subject: %s\n", str ? str : "<none>");
+
+ print_date(msg);
+
+ str = g_mime_message_get_message_id(msg);
+ g_print("Msg-id : %s\n", str ? str : "<none>");
+
+ {
+ gchar* refsstr;
+ refsstr = get_refs_str(msg);
+ g_print("Refs : %s\n", refsstr ? refsstr : "<none>");
+ g_free(refsstr);
+ }
+
+ print_body(msg);
+
+ return TRUE;
+}
+
+static gboolean
+test_stream(GMimeStream* stream)
+{
+ GMimeParser* parser;
+ GMimeMessage* msg;
+ gboolean rv;
+
+ parser = NULL;
+ msg = NULL;
+
+ parser = g_mime_parser_new_with_stream(stream);
+ if (!parser) {
+ g_warning("failed to create parser");
+ rv = FALSE;
+ goto leave;
+ }
+
+ msg = g_mime_parser_construct_message(parser, NULL);
+ if (!msg) {
+ g_warning("failed to construct message");
+ rv = FALSE;
+ goto leave;
+ }
+
+ rv = test_message(msg);
+
+leave:
+ if (parser)
+ g_object_unref(parser);
+
+ if (msg)
+ g_object_unref(msg);
+
+ return rv;
+}
+
+static gboolean
+test_file(const char* path)
+{
+ FILE* file;
+ GMimeStream* stream;
+ gboolean rv;
+
+ stream = NULL;
+ file = NULL;
+
+ file = fopen(path, "r");
+ if (!file) {
+ g_warning("cannot open file '%s': %s", path, g_strerror(errno));
+ rv = FALSE;
+ goto leave;
+ }
+
+ stream = g_mime_stream_file_new(file);
+ if (!stream) {
+ g_warning("cannot open stream for '%s'", path);
+ rv = FALSE;
+ goto leave;
+ }
+
+ rv = test_stream(stream);
+ g_object_unref(stream);
+ return rv;
+
+leave:
+ if (file)
+ fclose(file);
+
+ return rv;
+}
+
+int
+main(int argc, char* argv[])
+{
+ gboolean rv;
+
+ if (argc != 2) {
+ g_printerr("usage: %s <msg-file>\n", argv[0]);
+ return 1;
+ }
+
+ setlocale(LC_ALL, "");
+
+ g_mime_init();
+
+ rv = test_file(argv[1]);
+
+ g_mime_shutdown();
+
+ return rv ? 0 : 1;
+}
--- /dev/null
+## Copyright (C) 2022-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## 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, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+#
+# tests
+#
+
+
+test('test-cmd-add',
+ executable('test-cmd-add',
+ '../mu-cmd-add.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_dep]))
+
+test('test-cmd-cfind',
+ executable('test-cmd-cfind',
+ '../mu-cmd-cfind.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_dep]))
+
+test('test-cmd-extract',
+ executable('test-cmd-extract',
+ '../mu-cmd-extract.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_dep]))
+
+test('test-cmd-find',
+ executable('test-cmd-find',
+ '../mu-cmd-find.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_dep]))
+
+test('test-cmd-index',
+ executable('test-cmd-index',
+ '../mu-cmd-index.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_dep]))
+
+test('test-cmd-init',
+ executable('test-cmd-init',
+ '../mu-cmd-init.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_dep]))
+
+test('test-cmd-mkdir',
+ executable('test-cmd-mkdir',
+ '../mu-cmd-mkdir.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_dep]))
+
+test('test-cmd-move',
+ executable('test-cmd-move',
+ '../mu-cmd-move.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_dep]))
+
+test('test-cmd-remove',
+ executable('test-cmd-remove',
+ '../mu-cmd-remove.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_dep]))
+
+test('test-cmd-verify',
+ executable('test-cmd-verify',
+ '../mu-cmd-verify.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_dep]))
+
+test('test-cmd-view',
+ executable('test-cmd-view',
+ '../mu-cmd-view.cc',
+ install: false,
+ cpp_args: ['-DBUILD_TESTS'],
+ dependencies: [glib_dep, lib_mu_dep]))
+
+test('test-cmd-query',
+ executable('test-cmd-query',
+ 'test-mu-query.cc',
+ install: false,
+ dependencies: [glib_dep, config_h_dep, lib_mu_dep]))
+
+gmime_test = executable(
+ 'gmime-test', [
+ 'gmime-test.c'
+],
+ dependencies: [ glib_dep, gmime_dep ],
+ install: false)
--- /dev/null
+/*
+** Copyright (C) 2008-2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+**
+** 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, 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, write to the Free Software Foundation,
+** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+**
+*/
+
+#include "config.h"
+
+#include <unordered_set>
+#include <string>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <locale.h>
+
+#include "utils/mu-test-utils.hh"
+#include "mu-query.hh"
+#include "utils/mu-result.hh"
+#include "utils/mu-utils.hh"
+#include "utils/mu-utils-file.hh"
+#include "mu-store.hh"
+
+using namespace Mu;
+
+static std::string DB_PATH1;
+static std::string DB_PATH2;
+
+static std::string
+make_database(const std::string& dbdir, const std::string& testdir)
+{
+ /* use the env var rather than `--muhome` */
+ g_setenv("MUHOME", dbdir.c_str(), 1);
+ const auto cmdline{mu_format(
+ "/bin/sh -c '"
+ "{} --quiet init --maildir={} ; "
+ "{} --quiet index'",
+ MU_PROGRAM, testdir, MU_PROGRAM)};
+
+ if (g_test_verbose())
+ mu_printerrln("\n{}", cmdline);
+
+ g_assert(g_spawn_command_line_sync(cmdline.c_str(), NULL, NULL, NULL, NULL));
+ auto xpath = join_paths(dbdir, "xapian");
+ /* ensure MUHOME worked */
+ g_assert_cmpuint(::access(xpath.c_str(), F_OK), ==, 0);
+
+ return xpath;
+}
+
+static void
+assert_no_dups(const QueryResults& qres)
+{
+ std::unordered_set<std::string> msgid_set, path_set;
+
+ for (auto&& mi : qres) {
+ g_assert_true(msgid_set.find(mi.message_id().value()) == msgid_set.end());
+ g_assert_true(path_set.find(mi.path().value()) == path_set.end());
+
+ path_set.emplace(*mi.path());
+ msgid_set.emplace(*mi.message_id());
+
+ g_assert_false(msgid_set.find(mi.message_id().value()) == msgid_set.end());
+ g_assert_false(path_set.find(mi.path().value()) == path_set.end());
+ }
+}
+
+/* note: this also *moves the iter* */
+static size_t
+run_and_count_matches(const std::string& xpath,
+ const std::string& expr,
+ Mu::QueryFlags flags = Mu::QueryFlags::None)
+{
+ auto store{Store::make(xpath)};
+ assert_valid_result(store);
+
+ // if (g_test_verbose()) {
+ // std::cout << "==> mquery: " << store.parse_query(expr, false) << "\n";
+ // std::cout << "==> xquery: " << store.parse_query(expr, true) << "\n";
+ // }
+
+ Mu::allow_warnings();
+
+ auto qres{store->run_query(expr, {}, flags)};
+ g_assert_true(!!qres);
+ assert_no_dups(*qres);
+
+ if (g_test_verbose())
+ mu_println("'{}' => {}\n", expr, qres->size());
+
+ return qres->size();
+}
+
+typedef struct {
+ const char* query;
+ size_t count; /* expected number of matches */
+} QResults;
+
+static void
+test_mu_query_01(void)
+{
+ int i;
+ QResults queries[] = {
+ {"basic", 3},
+ {"question", 5},
+ {"thanks", 2},
+ {"html", 4},
+ {"subject:exception", 1},
+ {"exception", 1},
+ {"subject:A&B", 1},
+ {"A&B", 1},
+ {"subject:elisp", 1},
+ {"html AND contains", 1},
+ {"html and contains", 1},
+ {"from:pepernoot", 0},
+ {"foo:pepernoot", 0},
+ {"funky", 1},
+ {"fünkÿ", 1},
+ { "", 19 },
+ {"msgid:abcd$efgh@example.com", 1},
+ {"i:abcd$efgh@example.com", 1},
+#ifdef HAVE_CLD2
+{ "lang:en", 14},
+#endif /*HAVE_CLD2*/
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+ ==, queries[i].count);
+}
+
+static void
+test_mu_query_02(void)
+{
+ const char* q;
+ q = "i:f7ccd24b0808061357t453f5962w8b61f9a453b684d0@mail.gmail.com";
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, q), ==, 1);
+}
+
+static void
+test_mu_query_03(void)
+{
+ int i;
+ QResults queries[] = {{"ploughed", 1},
+ {"i:3BE9E6535E3029448670913581E7A1A20D852173@"
+ "emss35m06.us.lmco.com",
+ 1},
+ {"i:!&!AAAAAAAAAYAAAAAAAAAOH1+8mkk+lLn7Gg5fke7"
+ "FbCgAAAEAAAAJ7eBDgcactKhXL6r8cEnJ8BAAAAAA==@"
+ "example.com",
+ 1},
+
+ /* subsets of the words in the subject should match */
+ {"s:gcc include search order", 1},
+ {"s:gcc include search", 1},
+ {"s:search order", 1},
+ {"s:include", 1},
+
+ {"s:lisp", 1},
+ {"s:LISP", 1},
+
+ // { "s:\"Re: Learning LISP; Scheme vs elisp.\"", 1},
+ // { "subject:Re: Learning LISP; Scheme vs elisp.", 1},
+ // { "subject:\"Re: Learning LISP; Scheme vs elisp.\"", 1},
+ {"to:help-gnu-emacs@gnu.org", 4},
+ //{"t:help-gnu-emacs", 4},
+ {"flag:flagged", 1}};
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+ ==, queries[i].count);
+}
+
+static void
+test_mu_query_04(void)
+{
+ int i;
+
+ QResults queries[] = {
+ {"frodo@example.com", 1},
+ {"f:frodo@example.com", 1},
+ {"f:Frodo Baggins", 1},
+ {"bilbo@anotherexample.com", 1},
+ {"t:bilbo@anotherexample.com", 1},
+ {"t:bilbo", 1},
+ {"f:bilbo", 0},
+ {"baggins", 1},
+ {"prio:h", 1},
+ {"prio:high", 1},
+ {"prio:normal", 11},
+ {"prio:l", 7},
+ {"not prio:l", 12},
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+ ==, queries[i].count);
+}
+
+static void
+test_mu_query_logic(void)
+{
+ int i;
+ QResults queries[] = {{"subject:gcc", 1},
+ {"subject:lisp", 1},
+ {"subject:gcc OR subject:lisp", 2},
+ {"subject:gcc or subject:lisp", 2},
+ {"subject:gcc AND subject:lisp", 0},
+ {"subject:gcc OR (subject:scheme AND subject:elisp)", 2},
+ {"(subject:gcc OR subject:scheme) AND subject:elisp", 1}};
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+ ==, queries[i].count);
+}
+
+static void
+test_mu_query_accented_chars_01(void)
+{
+ auto store = Store::make(DB_PATH1);
+ assert_valid_result(store);
+
+ auto qres{store->run_query("fünkÿ")};
+ g_assert_true(!!qres);
+ g_assert_false(qres->empty());
+
+ const auto msg{qres->begin().message()};
+ if (!msg) {
+ mu_warning("error getting message");
+ g_assert_not_reached();
+ }
+
+ assert_equal(msg->subject(), "Greetings from Lothlórien");
+}
+
+static void
+test_mu_query_accented_chars_02(void)
+{
+ int i;
+
+ QResults queries[] = {{"f:mü", 1},
+ { "s:motörhead", 1},
+ {"t:Helmut", 1},
+ {"t:Kröger", 1},
+ {"s:MotorHeäD", 1},
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i) {
+ auto count = run_and_count_matches(DB_PATH1, queries[i].query);
+ if (count != queries[i].count)
+ mu_warning("query '{}'; expected {} but got {}",
+ queries[i].query, queries[i].count, count);
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+ ==, queries[i].count);
+ }
+}
+
+static void
+test_mu_query_accented_chars_fraiche(void)
+{
+ int i;
+
+ QResults queries[] = {{"crème fraîche", 1},
+ {"creme fraiche", 1},
+ {"fraîche crème", 1},
+ {"будланула", 1},
+ {"БУДЛАНУЛА", 1},
+ {"CRÈME FRAÎCHE", 1},
+ {"CREME FRAICHE", 1}};
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i) {
+ if (g_test_verbose())
+ mu_println("{}", queries[i].query);
+
+ g_assert_cmpuint(run_and_count_matches(DB_PATH2, queries[i].query),
+ ==, queries[i].count);
+ }
+}
+
+static void
+test_mu_query_wildcards(void)
+{
+ int i;
+
+ QResults queries[] = {
+ {"f:mü", 1},
+ {"s:mo*", 1},
+ {"t:Helm*", 1},
+ {"queensryche", 1},
+ {"Queen*", 1},
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+ ==, queries[i].count);
+}
+
+static void
+test_mu_query_dates_helsinki(void)
+{
+ const auto hki = "Europe/Helsinki";
+ if (!timezone_available(hki)) {
+ g_test_skip("timezone not available");
+ return;
+ }
+
+ int i;
+ const char* old_tz;
+
+ QResults queries[] = {{"date:20080731..20080804", 5},
+ {"date:20080731..20080804 s:gcc", 1},
+ {"date:200808110803..now", 7},
+ {"date:200808110803..today", 7},
+ {"date:200808110801..now", 7}};
+
+ old_tz = set_tz(hki);
+ TempDir tdir;
+ const auto xpath{make_database(tdir.path(), MU_TESTMAILDIR)};
+ g_assert_false(xpath.empty());
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(xpath, queries[i].query),
+ ==, queries[i].count);
+
+ set_tz(old_tz);
+}
+
+static void
+test_mu_query_dates_sydney(void)
+{
+ const auto syd = "Australia/Sydney";
+ if (!timezone_available(syd)) {
+ g_test_skip("timezone not available");
+ return;
+ }
+
+ int i;
+ const char* old_tz;
+ QResults queries[] = {{"date:20080731..20080804", 5},
+ {"date:20080731..20080804 s:gcc", 1},
+ {"date:200808110803..now", 7},
+ {"date:200808110803..today", 7},
+ {"date:200808110801..now", 7}};
+ old_tz = set_tz(syd);
+
+ TempDir tdir;
+ const auto xpath{make_database(tdir.path(), MU_TESTMAILDIR)};
+ g_assert_false(xpath.empty());
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(xpath, queries[i].query),
+ ==, queries[i].count);
+ set_tz(old_tz);
+}
+
+static void
+test_mu_query_dates_la(void)
+{
+ const auto la = "America/Los_Angeles";
+ if (!timezone_available(la)) {
+ g_test_skip("timezone not available");
+ return;
+ }
+
+ int i;
+ const char* old_tz;
+
+ QResults queries[] = {{"date:20080731..20080804", 5},
+ {"date:2008-07-31..2008-08-04", 5},
+ {"date:20080804..20080731", 5},
+ {"date:20080731..20080804 s:gcc", 1},
+ {"date:200808110803..now", 6},
+ {"date:200808110803..today", 6},
+ {"date:200808110801..now", 6}};
+ old_tz = set_tz(la);
+
+ TempDir tdir;
+ const auto xpath{make_database(tdir.path(), MU_TESTMAILDIR)};
+ g_assert_false(xpath.empty());
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i) {
+ /* g_print ("%s\n", queries[i].query); */
+ g_assert_cmpuint(run_and_count_matches(xpath, queries[i].query),
+ ==, queries[i].count);
+ }
+
+ set_tz(old_tz);
+}
+
+static void
+test_mu_query_sizes(void)
+{
+ int i;
+ QResults queries[] = {
+ {"size:0b..2m", 19},
+ {"size:3b..2m", 19},
+ {"size:2k..4k", 4},
+
+ {"size:0b..2m", 19},
+ {"size:2m..0b", 19},
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+ ==, queries[i].count);
+}
+
+static void
+test_mu_query_attach(void)
+{
+ int i;
+ QResults queries[] = {{"j:sittingbull.jpg", 1}, {"file:custer", 0}, {"file:custer.jpg", 1}};
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i) {
+ if (g_test_verbose())
+ mu_println("query: {}", queries[i].query);
+ g_assert_cmpuint(run_and_count_matches(DB_PATH2, queries[i].query),
+ ==, queries[i].count);
+ }
+}
+
+static void
+test_mu_query_msgid(void)
+{
+ int i;
+ QResults queries[] = {
+ {"i:CAHSaMxZ9rk5ASjqsbXizjTQuSk583=M6TORHz"
+ "=bfogtmbGGs5A@mail.gmail.com",
+ 1},
+ {"msgid:CAHSaMxZ9rk5ASjqsbXizjTQuSk583=M6TORHz="
+ "bfogtmbGGs5A@mail.gmail.com",
+ 1},
+
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i) {
+ if (g_test_verbose())
+ mu_println("query: {}", queries[i].query);
+ g_assert_cmpuint(run_and_count_matches(DB_PATH2, queries[i].query),
+ ==, queries[i].count);
+ }
+}
+
+static void
+test_mu_query_tags(void)
+{
+ int i;
+ QResults queries[] = {
+ {"x:paradise", 1},
+ {"tag:lost", 1},
+ {"tag:lost tag:paradise", 1},
+ {"tag:lost tag:horizon", 0},
+ {"tag:lost OR tag:horizon", 1},
+ {"tag:queensryche", 1},
+ {"tag:Queensrÿche", 1},
+ {"x:paradise,lost", 0},
+ {"x:paradise AND x:lost", 1},
+ {"x:\\\\backslash", 1},
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH2, queries[i].query),
+ ==, queries[i].count);
+}
+
+static void
+test_mu_query_wom_bat(void)
+{
+ int i;
+ QResults queries[] = {
+ {"maildir:/wom_bat", 3},
+ //{ "\"maildir:/wom bat\"", 3},
+ // as expected, no longer works with new parser
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH2, queries[i].query),
+ ==, queries[i].count);
+}
+
+static void
+test_mu_query_signed_encrypted(void)
+{
+ int i;
+ QResults queries[] = {
+ {"flag:encrypted", 2},
+ {"flag:signed", 2},
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+ ==,
+ queries[i].count);
+}
+
+static void
+test_mu_query_multi_to_cc(void)
+{
+ int i;
+ QResults queries[] = {
+ {"to:a@example.com", 1},
+ {"cc:d@example.com", 1},
+ {"to:b@example.com", 1},
+ {"cc:e@example.com", 1},
+ {"cc:e@example.com AND cc:d@example.com", 1},
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i)
+ g_assert_cmpuint(run_and_count_matches(DB_PATH1, queries[i].query),
+ ==, queries[i].count);
+}
+
+static void
+test_mu_query_tags_02(void)
+{
+ int i;
+ QResults queries[] = {
+ {"x:paradise", 1},
+ {"tag:@NextActions", 1},
+ {"x:queensrÿche", 1},
+ {"tag:lost OR tag:operation*", 2},
+ };
+
+ for (i = 0; i != G_N_ELEMENTS(queries); ++i) {
+ g_assert_cmpuint(run_and_count_matches(DB_PATH2, queries[i].query),
+ ==, queries[i].count);
+ }
+}
+
+/* Tests for https://github.com/djcb/mu/issues/380
+
+ On certain platforms, something goes wrong during compilation and the
+ --related option doesn't work.
+*/
+static void
+test_mu_query_threads_compilation_error(void)
+{
+ TempDir tdir;
+ const auto xpath = make_database(tdir.path(), MU_TESTMAILDIR);
+
+ g_assert_cmpuint(run_and_count_matches(xpath, "msgid:uwsireh25.fsf@one.dot.net"), ==, 1);
+
+ g_assert_cmpuint(run_and_count_matches(xpath,
+ "msgid:uwsireh25.fsf@one.dot.net",
+ QueryFlags::IncludeRelated), ==, 3);
+}
+
+int
+main(int argc, char* argv[])
+{
+ TempDir td1;
+ TempDir td2;
+
+ mu_test_init(&argc, &argv);
+ DB_PATH1 = make_database(td1.path(), MU_TESTMAILDIR);
+ g_assert_false(DB_PATH1.empty());
+
+ DB_PATH2 = make_database(td2.path(), MU_TESTMAILDIR2);
+ g_assert_false(DB_PATH2.empty());
+
+ g_test_add_func("/mu-query/test-mu-query-01", test_mu_query_01);
+ g_test_add_func("/mu-query/test-mu-query-02", test_mu_query_02);
+ g_test_add_func("/mu-query/test-mu-query-03", test_mu_query_03);
+ g_test_add_func("/mu-query/test-mu-query-04", test_mu_query_04);
+
+ g_test_add_func("/mu-query/test-mu-query-signed-encrypted", test_mu_query_signed_encrypted);
+ g_test_add_func("/mu-query/test-mu-query-multi-to-cc", test_mu_query_multi_to_cc);
+ g_test_add_func("/mu-query/test-mu-query-logic", test_mu_query_logic);
+
+ g_test_add_func("/mu-query/test-mu-query-accented-chars-1",
+ test_mu_query_accented_chars_01);
+ g_test_add_func("/mu-query/test-mu-query-accented-chars-2",
+ test_mu_query_accented_chars_02);
+ g_test_add_func("/mu-query/test-mu-query-accented-chars-fraiche",
+ test_mu_query_accented_chars_fraiche);
+
+ g_test_add_func("/mu-query/test-mu-query-msgid", test_mu_query_msgid);
+
+ g_test_add_func("/mu-query/test-mu-query-wom-bat", test_mu_query_wom_bat);
+
+ g_test_add_func("/mu-query/test-mu-query-wildcards", test_mu_query_wildcards);
+ g_test_add_func("/mu-query/test-mu-query-sizes", test_mu_query_sizes);
+
+ g_test_add_func("/mu-query/test-mu-query-dates-helsinki", test_mu_query_dates_helsinki);
+ g_test_add_func("/mu-query/test-mu-query-dates-sydney", test_mu_query_dates_sydney);
+ g_test_add_func("/mu-query/test-mu-query-dates-la", test_mu_query_dates_la);
+
+ g_test_add_func("/mu-query/test-mu-query-attach", test_mu_query_attach);
+ g_test_add_func("/mu-query/test-mu-query-tags", test_mu_query_tags);
+ g_test_add_func("/mu-query/test-mu-query-tags_02", test_mu_query_tags_02);
+
+ g_test_add_func("/mu-query/test-mu-query-threads-compilation-error",
+ test_mu_query_threads_compilation_error);
+
+ return g_test_run();
+}
--- /dev/null
+@c The GNU Free Documentation License.
+@center Version 1.2, November 2002
+
+@c This file is intended to be included within another document,
+@c hence no sectioning command or @node.
+
+@display
+Copyright @copyright{} 2000,2001,2002 Free Software Foundation, Inc.
+51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+
+Everyone is permitted to copy and distribute verbatim copies
+of this license document, but changing it is not allowed.
+@end display
+
+@enumerate 0
+@item
+PREAMBLE
+
+The purpose of this License is to make a manual, textbook, or other
+functional and useful document @dfn{free} in the sense of freedom: to
+assure everyone the effective freedom to copy and redistribute it,
+with or without modifying it, either commercially or noncommercially.
+Secondarily, this License preserves for the author and publisher a way
+to get credit for their work, while not being considered responsible
+for modifications made by others.
+
+This License is a kind of ``copyleft'', which means that derivative
+works of the document must themselves be free in the same sense. It
+complements the GNU General Public License, which is a copyleft
+license designed for free software.
+
+We have designed this License in order to use it for manuals for free
+software, because free software needs free documentation: a free
+program should come with manuals providing the same freedoms that the
+software does. But this License is not limited to software manuals;
+it can be used for any textual work, regardless of subject matter or
+whether it is published as a printed book. We recommend this License
+principally for works whose purpose is instruction or reference.
+
+@item
+APPLICABILITY AND DEFINITIONS
+
+This License applies to any manual or other work, in any medium, that
+contains a notice placed by the copyright holder saying it can be
+distributed under the terms of this License. Such a notice grants a
+world-wide, royalty-free license, unlimited in duration, to use that
+work under the conditions stated herein. The ``Document'', below,
+refers to any such manual or work. Any member of the public is a
+licensee, and is addressed as ``you''. You accept the license if you
+copy, modify or distribute the work in a way requiring permission
+under copyright law.
+
+A ``Modified Version'' of the Document means any work containing the
+Document or a portion of it, either copied verbatim, or with
+modifications and/or translated into another language.
+
+A ``Secondary Section'' is a named appendix or a front-matter section
+of the Document that deals exclusively with the relationship of the
+publishers or authors of the Document to the Document's overall
+subject (or to related matters) and contains nothing that could fall
+directly within that overall subject. (Thus, if the Document is in
+part a textbook of mathematics, a Secondary Section may not explain
+any mathematics.) The relationship could be a matter of historical
+connection with the subject or with related matters, or of legal,
+commercial, philosophical, ethical or political position regarding
+them.
+
+The ``Invariant Sections'' are certain Secondary Sections whose titles
+are designated, as being those of Invariant Sections, in the notice
+that says that the Document is released under this License. If a
+section does not fit the above definition of Secondary then it is not
+allowed to be designated as Invariant. The Document may contain zero
+Invariant Sections. If the Document does not identify any Invariant
+Sections then there are none.
+
+The ``Cover Texts'' are certain short passages of text that are listed,
+as Front-Cover Texts or Back-Cover Texts, in the notice that says that
+the Document is released under this License. A Front-Cover Text may
+be at most 5 words, and a Back-Cover Text may be at most 25 words.
+
+A ``Transparent'' copy of the Document means a machine-readable copy,
+represented in a format whose specification is available to the
+general public, that is suitable for revising the document
+straightforwardly with generic text editors or (for images composed of
+pixels) generic paint programs or (for drawings) some widely available
+drawing editor, and that is suitable for input to text formatters or
+for automatic translation to a variety of formats suitable for input
+to text formatters. A copy made in an otherwise Transparent file
+format whose markup, or absence of markup, has been arranged to thwart
+or discourage subsequent modification by readers is not Transparent.
+An image format is not Transparent if used for any substantial amount
+of text. A copy that is not ``Transparent'' is called ``Opaque''.
+
+Examples of suitable formats for Transparent copies include plain
+@sc{ascii} without markup, Texinfo input format, La@TeX{} input
+format, @acronym{SGML} or @acronym{XML} using a publicly available
+@acronym{DTD}, and standard-conforming simple @acronym{HTML},
+PostScript or @acronym{PDF} designed for human modification. Examples
+of transparent image formats include @acronym{PNG}, @acronym{XCF} and
+@acronym{JPG}. Opaque formats include proprietary formats that can be
+read and edited only by proprietary word processors, @acronym{SGML} or
+@acronym{XML} for which the @acronym{DTD} and/or processing tools are
+not generally available, and the machine-generated @acronym{HTML},
+PostScript or @acronym{PDF} produced by some word processors for
+output purposes only.
+
+The ``Title Page'' means, for a printed book, the title page itself,
+plus such following pages as are needed to hold, legibly, the material
+this License requires to appear in the title page. For works in
+formats which do not have any title page as such, ``Title Page'' means
+the text near the most prominent appearance of the work's title,
+preceding the beginning of the body of the text.
+
+A section ``Entitled XYZ'' means a named subunit of the Document whose
+title either is precisely XYZ or contains XYZ in parentheses following
+text that translates XYZ in another language. (Here XYZ stands for a
+specific section name mentioned below, such as ``Acknowledgements'',
+``Dedications'', ``Endorsements'', or ``History''.) To ``Preserve the Title''
+of such a section when you modify the Document means that it remains a
+section ``Entitled XYZ'' according to this definition.
+
+The Document may include Warranty Disclaimers next to the notice which
+states that this License applies to the Document. These Warranty
+Disclaimers are considered to be included by reference in this
+License, but only as regards disclaiming warranties: any other
+implication that these Warranty Disclaimers may have is void and has
+no effect on the meaning of this License.
+
+@item
+VERBATIM COPYING
+
+You may copy and distribute the Document in any medium, either
+commercially or noncommercially, provided that this License, the
+copyright notices, and the license notice saying this License applies
+to the Document are reproduced in all copies, and that you add no other
+conditions whatsoever to those of this License. You may not use
+technical measures to obstruct or control the reading or further
+copying of the copies you make or distribute. However, you may accept
+compensation in exchange for copies. If you distribute a large enough
+number of copies you must also follow the conditions in section 3.
+
+You may also lend copies, under the same conditions stated above, and
+you may publicly display copies.
+
+@item
+COPYING IN QUANTITY
+
+If you publish printed copies (or copies in media that commonly have
+printed covers) of the Document, numbering more than 100, and the
+Document's license notice requires Cover Texts, you must enclose the
+copies in covers that carry, clearly and legibly, all these Cover
+Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on
+the back cover. Both covers must also clearly and legibly identify
+you as the publisher of these copies. The front cover must present
+the full title with all words of the title equally prominent and
+visible. You may add other material on the covers in addition.
+Copying with changes limited to the covers, as long as they preserve
+the title of the Document and satisfy these conditions, can be treated
+as verbatim copying in other respects.
+
+If the required texts for either cover are too voluminous to fit
+legibly, you should put the first ones listed (as many as fit
+reasonably) on the actual cover, and continue the rest onto adjacent
+pages.
+
+If you publish or distribute Opaque copies of the Document numbering
+more than 100, you must either include a machine-readable Transparent
+copy along with each Opaque copy, or state in or with each Opaque copy
+a computer-network location from which the general network-using
+public has access to download using public-standard network protocols
+a complete Transparent copy of the Document, free of added material.
+If you use the latter option, you must take reasonably prudent steps,
+when you begin distribution of Opaque copies in quantity, to ensure
+that this Transparent copy will remain thus accessible at the stated
+location until at least one year after the last time you distribute an
+Opaque copy (directly or through your agents or retailers) of that
+edition to the public.
+
+It is requested, but not required, that you contact the authors of the
+Document well before redistributing any large number of copies, to give
+them a chance to provide you with an updated version of the Document.
+
+@item
+MODIFICATIONS
+
+You may copy and distribute a Modified Version of the Document under
+the conditions of sections 2 and 3 above, provided that you release
+the Modified Version under precisely this License, with the Modified
+Version filling the role of the Document, thus licensing distribution
+and modification of the Modified Version to whoever possesses a copy
+of it. In addition, you must do these things in the Modified Version:
+
+@enumerate A
+@item
+Use in the Title Page (and on the covers, if any) a title distinct
+from that of the Document, and from those of previous versions
+(which should, if there were any, be listed in the History section
+of the Document). You may use the same title as a previous version
+if the original publisher of that version gives permission.
+
+@item
+List on the Title Page, as authors, one or more persons or entities
+responsible for authorship of the modifications in the Modified
+Version, together with at least five of the principal authors of the
+Document (all of its principal authors, if it has fewer than five),
+unless they release you from this requirement.
+
+@item
+State on the Title page the name of the publisher of the
+Modified Version, as the publisher.
+
+@item
+Preserve all the copyright notices of the Document.
+
+@item
+Add an appropriate copyright notice for your modifications
+adjacent to the other copyright notices.
+
+@item
+Include, immediately after the copyright notices, a license notice
+giving the public permission to use the Modified Version under the
+terms of this License, in the form shown in the Addendum below.
+
+@item
+Preserve in that license notice the full lists of Invariant Sections
+and required Cover Texts given in the Document's license notice.
+
+@item
+Include an unaltered copy of this License.
+
+@item
+Preserve the section Entitled ``History'', Preserve its Title, and add
+to it an item stating at least the title, year, new authors, and
+publisher of the Modified Version as given on the Title Page. If
+there is no section Entitled ``History'' in the Document, create one
+stating the title, year, authors, and publisher of the Document as
+given on its Title Page, then add an item describing the Modified
+Version as stated in the previous sentence.
+
+@item
+Preserve the network location, if any, given in the Document for
+public access to a Transparent copy of the Document, and likewise
+the network locations given in the Document for previous versions
+it was based on. These may be placed in the ``History'' section.
+You may omit a network location for a work that was published at
+least four years before the Document itself, or if the original
+publisher of the version it refers to gives permission.
+
+@item
+For any section Entitled ``Acknowledgements'' or ``Dedications'', Preserve
+the Title of the section, and preserve in the section all the
+substance and tone of each of the contributor acknowledgements and/or
+dedications given therein.
+
+@item
+Preserve all the Invariant Sections of the Document,
+unaltered in their text and in their titles. Section numbers
+or the equivalent are not considered part of the section titles.
+
+@item
+Delete any section Entitled ``Endorsements''. Such a section
+may not be included in the Modified Version.
+
+@item
+Do not retitle any existing section to be Entitled ``Endorsements'' or
+to conflict in title with any Invariant Section.
+
+@item
+Preserve any Warranty Disclaimers.
+@end enumerate
+
+If the Modified Version includes new front-matter sections or
+appendices that qualify as Secondary Sections and contain no material
+copied from the Document, you may at your option designate some or all
+of these sections as invariant. To do this, add their titles to the
+list of Invariant Sections in the Modified Version's license notice.
+These titles must be distinct from any other section titles.
+
+You may add a section Entitled ``Endorsements'', provided it contains
+nothing but endorsements of your Modified Version by various
+parties---for example, statements of peer review or that the text has
+been approved by an organization as the authoritative definition of a
+standard.
+
+You may add a passage of up to five words as a Front-Cover Text, and a
+passage of up to 25 words as a Back-Cover Text, to the end of the list
+of Cover Texts in the Modified Version. Only one passage of
+Front-Cover Text and one of Back-Cover Text may be added by (or
+through arrangements made by) any one entity. If the Document already
+includes a cover text for the same cover, previously added by you or
+by arrangement made by the same entity you are acting on behalf of,
+you may not add another; but you may replace the old one, on explicit
+permission from the previous publisher that added the old one.
+
+The author(s) and publisher(s) of the Document do not by this License
+give permission to use their names for publicity for or to assert or
+imply endorsement of any Modified Version.
+
+@item
+COMBINING DOCUMENTS
+
+You may combine the Document with other documents released under this
+License, under the terms defined in section 4 above for modified
+versions, provided that you include in the combination all of the
+Invariant Sections of all of the original documents, unmodified, and
+list them all as Invariant Sections of your combined work in its
+license notice, and that you preserve all their Warranty Disclaimers.
+
+The combined work need only contain one copy of this License, and
+multiple identical Invariant Sections may be replaced with a single
+copy. If there are multiple Invariant Sections with the same name but
+different contents, make the title of each such section unique by
+adding at the end of it, in parentheses, the name of the original
+author or publisher of that section if known, or else a unique number.
+Make the same adjustment to the section titles in the list of
+Invariant Sections in the license notice of the combined work.
+
+In the combination, you must combine any sections Entitled ``History''
+in the various original documents, forming one section Entitled
+``History''; likewise combine any sections Entitled ``Acknowledgements'',
+and any sections Entitled ``Dedications''. You must delete all
+sections Entitled ``Endorsements.''
+
+@item
+COLLECTIONS OF DOCUMENTS
+
+You may make a collection consisting of the Document and other documents
+released under this License, and replace the individual copies of this
+License in the various documents with a single copy that is included in
+the collection, provided that you follow the rules of this License for
+verbatim copying of each of the documents in all other respects.
+
+You may extract a single document from such a collection, and distribute
+it individually under this License, provided you insert a copy of this
+License into the extracted document, and follow this License in all
+other respects regarding verbatim copying of that document.
+
+@item
+AGGREGATION WITH INDEPENDENT WORKS
+
+A compilation of the Document or its derivatives with other separate
+and independent documents or works, in or on a volume of a storage or
+distribution medium, is called an ``aggregate'' if the copyright
+resulting from the compilation is not used to limit the legal rights
+of the compilation's users beyond what the individual works permit.
+When the Document is included in an aggregate, this License does not
+apply to the other works in the aggregate which are not themselves
+derivative works of the Document.
+
+If the Cover Text requirement of section 3 is applicable to these
+copies of the Document, then if the Document is less than one half of
+the entire aggregate, the Document's Cover Texts may be placed on
+covers that bracket the Document within the aggregate, or the
+electronic equivalent of covers if the Document is in electronic form.
+Otherwise they must appear on printed covers that bracket the whole
+aggregate.
+
+@item
+TRANSLATION
+
+Translation is considered a kind of modification, so you may
+distribute translations of the Document under the terms of section 4.
+Replacing Invariant Sections with translations requires special
+permission from their copyright holders, but you may include
+translations of some or all Invariant Sections in addition to the
+original versions of these Invariant Sections. You may include a
+translation of this License, and all the license notices in the
+Document, and any Warranty Disclaimers, provided that you also include
+the original English version of this License and the original versions
+of those notices and disclaimers. In case of a disagreement between
+the translation and the original version of this License or a notice
+or disclaimer, the original version will prevail.
+
+If a section in the Document is Entitled ``Acknowledgements'',
+``Dedications'', or ``History'', the requirement (section 4) to Preserve
+its Title (section 1) will typically require changing the actual
+title.
+
+@item
+TERMINATION
+
+You may not copy, modify, sublicense, or distribute the Document except
+as expressly provided for under this License. Any other attempt to
+copy, modify, sublicense or distribute the Document is void, and will
+automatically terminate your rights under this License. However,
+parties who have received copies, or rights, from you under this
+License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+@item
+FUTURE REVISIONS OF THIS LICENSE
+
+The Free Software Foundation may publish new, revised versions
+of the GNU Free Documentation 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. See
+@uref{http://www.gnu.org/copyleft/}.
+
+Each version of the License is given a distinguishing version number.
+If the Document specifies that a particular numbered version of this
+License ``or any later version'' applies to it, you have the option of
+following the terms and conditions either of that specified version or
+of any later version that has been published (not as a draft) by the
+Free Software Foundation. If the Document does not specify a version
+number of this License, you may choose any version ever published (not
+as a draft) by the Free Software Foundation.
+@end enumerate
+
+@page
+@heading ADDENDUM: How to use this License for your documents
+
+To use this License in a document you have written, include a copy of
+the License in the document and put the following copyright and
+license notices just after the title page:
+
+@smallexample
+@group
+ Copyright (C) @var{year} @var{your name}.
+ Permission is granted to copy, distribute and/or modify this document
+ under the terms of the GNU Free Documentation License, Version 1.2
+ or any later version published by the Free Software Foundation;
+ with no Invariant Sections, no Front-Cover Texts, and no Back-Cover
+ Texts. A copy of the license is included in the section entitled ``GNU
+ Free Documentation License''.
+@end group
+@end smallexample
+
+If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts,
+replace the ``with@dots{}Texts.'' line with this:
+
+@smallexample
+@group
+ with the Invariant Sections being @var{list their titles}, with
+ the Front-Cover Texts being @var{list}, and with the Back-Cover Texts
+ being @var{list}.
+@end group
+@end smallexample
+
+If you have Invariant Sections without Cover Texts, or some other
+combination of the three, merge those two alternatives to suit the
+situation.
+
+If your document contains nontrivial examples of program code, we
+recommend releasing these examples in parallel under your choice of
+free software license, such as the GNU General Public License,
+to permit their use in free software.
+
+@c Local Variables:
+@c ispell-local-pdict: "ispell-dict"
+@c End:
+
--- /dev/null
+# htmlxref.cnf - reference file for free Texinfo manuals on the web.
+
+htmlxrefversion=2023-04-02.12; # UTC
+
+# Copyright 2010-2023 Free Software Foundation, Inc.
+#
+# Copying and distribution of this file, with or without modification,
+# are permitted in any medium without royalty provided the copyright
+# notice and this notice are preserved.
+#
+# The latest version of this file is available at
+# http://ftpmirror.gnu.org/texinfo/htmlxref.cnf.
+# Email corrections or additions to bug-texinfo@gnu.org.
+# The primary goal is to list all relevant GNU manuals;
+# other free manuals are also welcome.
+#
+# To be included in this list, a manual must:
+#
+# - have a generic url, e.g., no version numbers;
+# - have a unique file name (e.g., manual identifier), i.e., be related to the
+# package name. Things like "refman" or "tutorial" don't work.
+# - follow the naming convention for nodes described at
+# http://www.gnu.org/software/texinfo/manual/texinfo/html_node/HTML-Xref.html
+# This is what makeinfo and texi2html implement.
+#
+# Unless the above criteria are met, it's not possible to generate
+# reliable cross-manual references.
+#
+# For information on automatically generating all the useful formats for
+# a manual to put on the web, see
+# http://www.gnu.org/prep/maintain/html_node/Manuals-on-Web-Pages.html.
+
+# For people editing this file: when a manual named foo is related to a
+# package named bar, the url should contain a variable reference ${BAR}.
+# Otherwise, the gnumaint scripts have no way of knowing they are
+# associated, and thus gnu.org/manual can't include them.
+
+# shorten references to manuals on www.gnu.org.
+G = https://www.gnu.org
+GS = ${G}/software
+
+3dldf mono ${GS}/3dldf/manual/user_ref/3DLDF.html
+3dldf node ${GS}/3dldf/manual/user_ref/
+
+alive mono ${GS}/alive/manual/alive.html
+alive node ${GS}/alive/manual/html_node/
+
+anubis mono ${GS}/anubis/manual/anubis.html
+anubis chapter ${GS}/anubis/manual/html_chapter/
+anubis section ${GS}/anubis/manual/html_section/
+anubis node ${GS}/anubis/manual/html_node/
+
+artanis mono ${GS}/artanis/manual/artanis.html
+artanis node ${GS}/artanis/manual/html_node/
+
+aspell section http://aspell.net/man-html/index.html
+
+auctex mono ${GS}/auctex/manual/auctex.html
+auctex node ${GS}/auctex/manual/auctex/
+
+autoconf mono ${GS}/autoconf/manual/autoconf.html
+autoconf node ${GS}/autoconf/manual/html_node/
+
+autogen mono ${GS}/autogen/manual/autogen.html
+autogen chapter ${GS}/autogen/manual/html_chapter/
+autogen node ${GS}/autoconf/manual/html_node/
+
+automake mono ${GS}/automake/manual/automake.html
+automake node ${GS}/automake/manual/html_node/
+
+avl node http://adtinfo.org/libavl.html/
+
+bash mono ${GS}/bash/manual/bash.html
+bash node ${GS}/bash/manual/html_node/
+
+BINUTILS = https://sourceware.org/binutils/docs
+binutils mono ${BINUTILS}/binutils.html
+binutils node ${BINUTILS}/binutils/
+ #
+ as mono ${BINUTILS}/as.html
+ as node ${BINUTILS}/as/
+ #
+ bfd mono ${BINUTILS}/bfd.html
+ bfd node ${BINUTILS}/bfd/
+ #
+ gprof mono ${BINUTILS}/gprof.html
+ gprof node ${BINUTILS}/gprof/
+ #
+ ld mono ${BINUTILS}/ld.html
+ ld node ${BINUTILS}/ld/
+
+bison mono ${GS}/bison/manual/bison.html
+bison node ${GS}/bison/manual/html_node/
+
+bpel2owfn mono ${GS}/bpel2owfn/manual/2.0.x/bpel2owfn.html
+
+ccd2cue mono ${GS}/ccd2cue/manual/ccd2cue.html
+ccd2cue node ${GS}/ccd2cue/manual/html_node/
+
+cflow mono ${GS}/cflow/manual/cflow.html
+cflow node ${GS}/cflow/manual/html_node/
+
+chess mono ${GS}/chess/manual/gnuchess.html
+chess node ${GS}/chess/manual/html_node/
+
+combine mono ${GS}/combine/manual/combine.html
+combine chapter ${GS}/combine/manual/html_chapter/
+combine section ${GS}/combine/manual/html_section/
+combine node ${GS}/combine/manual/html_node/
+
+complexity mono ${GS}/complexity/manual/complexity.html
+complexity node ${GS}/complexity/manual/html_node/
+
+coreutils mono ${GS}/coreutils/manual/coreutils.html
+coreutils node ${GS}/coreutils/manual/html_node/
+
+cpio mono ${GS}/cpio/manual/cpio.html
+cpio node ${GS}/cpio/manual/html_node/
+
+cssc node ${GS}/cssc/manual/
+
+CVS = ${GS}/trans-coord/manual
+cvs mono ${CVS}/cvs/cvs.html
+cvs node ${CVS}/cvs/html_node/
+
+ddd mono ${GS}/ddd/manual/html_mono/ddd.html
+
+ddrescue mono ${GS}/ddrescue/manual/ddrescue_manual.html
+
+dejagnu node ${GS}/dejagnu/manual/
+
+DICO = https://www.gnu.org.ua/software/dico/manual
+dico mono ${DICO}/dico.html
+dico chapter ${DICO}/html_chapter/
+dico section ${DICO}/html_section/
+dico node ${DICO}/html_node/
+
+diffutils mono ${GS}/diffutils/manual/diffutils.html
+diffutils node ${GS}/diffutils/manual/html_node/
+
+ed mono ${GS}/ed/manual/ed_manual.html
+
+EMACS = ${GS}/emacs/manual
+emacs mono ${EMACS}/html_mono/emacs.html
+emacs node ${EMACS}/html_node/emacs/
+ #
+ auth mono ${EMACS}/html_mono/auth.html
+ auth node ${EMACS}/html_node/auth/
+ #
+ autotype mono ${EMACS}/html_mono/autotype.html
+ autotype node ${EMACS}/html_node/autotype/
+ #
+ calc mono ${EMACS}/html_mono/calc.html
+ calc node ${EMACS}/html_node/calc/
+ #
+ ccmode mono ${EMACS}/html_mono/ccmode.html
+ ccmode node ${EMACS}/html_node/ccmode/
+ #
+ cl mono ${EMACS}/html_mono/cl.html
+ cl node ${EMACS}/html_node/cl/
+ #
+ dbus mono ${EMACS}/html_mono/dbus.html
+ dbus node ${EMACS}/html_node/dbus/
+ #
+ ebrowse mono ${EMACS}/html_mono/ebrowse.html
+ ebrowse node ${EMACS}/html_node/ebrowse/
+ #
+ ede mono ${EMACS}/html_mono/ede.html
+ ede node ${EMACS}/html_node/ede/
+ #
+ edt mono ${EMACS}/html_mono/edt.html
+ edt node ${EMACS}/html_node/edt/
+ #
+ ediff mono ${EMACS}/html_mono/ediff.html
+ ediff node ${EMACS}/html_node/ediff/
+ #
+ eieio mono ${EMACS}/html_mono/eieio.html
+ eieio node ${EMACS}/html_node/eieio/
+ #
+ elisp mono ${EMACS}/html_mono/elisp.html
+ elisp node ${EMACS}/html_node/elisp/
+ #
+ emacs-gnutls mono ${EMACS}/html_mono/emacs-gnutls.html
+ emacs-gnutls node ${EMACS}/html_node/emacs-gnutls/
+ #
+ emacs-mime mono ${EMACS}/html_mono/emacs-mime.html
+ emacs-mime node ${EMACS}/html_node/emacs-mime/
+ #
+ epa mono ${EMACS}/html_mono/epa.html
+ epa node ${EMACS}/html_node/epa/
+ #
+ erc mono ${EMACS}/html_mono/erc.html
+ erc node ${EMACS}/html_node/erc/
+ #
+ dired-x mono ${EMACS}/html_mono/dired-x.html
+ dired-x node ${EMACS}/html_node/dired-x/
+ #
+ ert mono ${EMACS}/html_mono/ert.html
+ ert node ${EMACS}/html_node/ert/
+ #
+ eshell mono ${EMACS}/html_mono/eshell.html
+ eshell node ${EMACS}/html_node/eshell/
+ #
+ eudc mono ${EMACS}/html_mono/eudc.html
+ eudc node ${EMACS}/html_node/eudc/
+ #
+ eww mono ${EMACS}/html_mono/eww.html
+ eww node ${EMACS}/html_node/eww/
+ #
+ forms mono ${EMACS}/html_mono/forms.html
+ forms node ${EMACS}/html_node/forms/
+ #
+ flymake mono ${EMACS}/html_mono/flymake.html
+ flymake node ${EMACS}/html_node/flymake/
+ #
+ gnus mono ${EMACS}/html_mono/gnus.html
+ gnus node ${EMACS}/html_node/gnus/
+ #
+ htmlfontify mono ${EMACS}/html_mono/htmlfontify.html
+ htmlfontify node ${EMACS}/html_node/htmlfontify/
+ #
+ idlwave mono ${EMACS}/html_mono/idlwave.html
+ idlwave node ${EMACS}/html_node/idlwave/
+ #
+ ido mono ${EMACS}/html_mono/ido.html
+ ido node ${EMACS}/html_node/ido/
+ #
+ info mono ${EMACS}/html_mono/info.html
+ info node ${EMACS}/html_node/info/
+ #
+ mairix-el mono ${EMACS}/html_mono/mairix-el.html
+ mairix-el node ${EMACS}/html_node/mairix-el/
+ #
+ message mono ${EMACS}/html_mono/message.html
+ message node ${EMACS}/html_node/message/
+ #
+ mh-e mono ${EMACS}/html_mono/mh-e.html
+ mh-e node ${EMACS}/html_node/mh-e/
+ #
+ newsticker mono ${EMACS}/html_mono/newsticker.html
+ newsticker node ${EMACS}/html_node/newsticker/
+ #
+ nxml-mode mono ${EMACS}/html_mono/nxml-mode.html
+ nxml-mode node ${EMACS}/html_node/nxml-mode/
+ #
+ octave-mode mono ${EMACS}/html_mono/octave-mode.html
+ octave-mode node ${EMACS}/html_node/octave-mode/
+ #
+ org mono ${EMACS}/html_mono/org.html
+ org node ${EMACS}/html_node/org/
+ #
+ pcl-cvs mono ${EMACS}/html_mono/pcl-cvs.html
+ pcl-cvs node ${EMACS}/html_node/pcl-cvs/
+ #
+ pgg mono ${EMACS}/html_mono/pgg.html
+ pgg node ${EMACS}/html_node/pgg/
+ #
+ rcirc mono ${EMACS}/html_mono/rcirc.html
+ rcirc node ${EMACS}/html_node/rcirc/
+ #
+ reftex mono ${EMACS}/html_mono/reftex.html
+ reftex node ${EMACS}/html_node/reftex/
+ #
+ remember mono ${EMACS}/html_mono/remember.html
+ remember node ${EMACS}/html_node/remember/
+ #
+ sasl mono ${EMACS}/html_mono/sasl.html
+ sasl node ${EMACS}/html_node/sasl/
+ #
+ semantic mono ${EMACS}/html_mono/semantic.html
+ semantic node ${EMACS}/html_node/semantic/
+ #
+ bovine mono ${EMACS}/html_mono/bovine.html
+ bovine node ${EMACS}/html_node/bovine/
+ #
+ srecode mono ${EMACS}/html_mono/srecode.html
+ srecode node ${EMACS}/html_node/srecode/
+ #
+ ses mono ${EMACS}/html_mono/ses.html
+ ses node ${EMACS}/html_node/ses/
+ #
+ sieve mono ${EMACS}/html_mono/sieve.html
+ sieve node ${EMACS}/html_node/sieve/
+ #
+ smtp mono ${EMACS}/html_mono/smtpmail.html
+ smtp node ${EMACS}/html_node/smtpmail/
+ #
+ speedbar mono ${EMACS}/html_mono/speedbar.html
+ speedbar node ${EMACS}/html_node/speedbar/
+ #
+ sc mono ${EMACS}/html_mono/sc.html
+ sc node ${EMACS}/html_node/sc/
+ #
+ todo-mode mono ${EMACS}/html_mono/todo-mode.html
+ todo-mode node ${EMACS}/html_node/todo-mode/
+ #
+ tramp mono ${EMACS}/html_mono/tramp.html
+ tramp node ${EMACS}/html_node/tramp/
+ #
+ url mono ${EMACS}/html_mono/url.html
+ url node ${EMACS}/html_node/url/
+ #
+ vhdl-mode mono ${EMACS}/html_mono/vhdl-mode.html
+ vhdl-mode node ${EMACS}/html_node/vhdl-mode/
+ #
+ vip mono ${EMACS}/html_mono/vip.html
+ vip node ${EMACS}/html_node/vip/
+ #
+ viper mono ${EMACS}/html_mono/viper.html
+ viper node ${EMACS}/html_node/viper/
+ #
+ widget mono ${EMACS}/html_mono/widget.html
+ widget node ${EMACS}/html_node/widget/
+ #
+ wisent mono ${EMACS}/html_mono/wisent.html
+ wisent node ${EMACS}/html_node/wisent/
+ #
+ woman mono ${EMACS}/html_mono/woman.html
+ woman node ${EMACS}/html_node/woman/
+ # (end emacs manuals in EMACS)
+
+easejs mono ${GS}/easejs/manual/easejs.html
+easejs node ${GS}/easejs/manual/
+
+emacs-muse mono ${GS}/emacs-muse/manual/muse.html
+emacs-muse node ${GS}/emacs-muse/manual/html_node/
+
+emms node ${GS}/emms/manual/
+
+ada-mode mono https://elpa.gnu.org/packages/ada-mode.html
+
+gpr-mode mono https://elpa.gnu.org/packages/doc/gpr-mode.html
+
+findutils mono ${GS}/findutils/manual/html_mono/find.html
+findutils node ${GS}/findutils/manual/html_node/find_html
+
+flex node https://westes.github.io/flex/manual/
+
+gama mono ${GS}/gama/manual/gama.html
+gama node ${GS}/gama/manual/html_node/
+
+GAWK = ${GS}/gawk/manual
+gawk mono ${GAWK}/gawk.html
+gawk node ${GAWK}/html_node/
+ gawkinet mono ${GAWK}/gawkinet/gawkinet.html
+ gawkinet node ${GAWK}/gawkinet/html_node/
+
+gcal mono ${GS}/gcal/manual/gcal.html
+gcal node ${GS}/gcal/manual/html_node/
+
+GCC = https://gcc.gnu.org/onlinedocs
+gcc node ${GCC}/gcc/
+ cpp node ${GCC}/cpp/
+ gfortran node ${GCC}/gfortran/
+ gnat_rm node ${GCC}/gnat_rm/
+ gnat_ugn node ${GCC}/gnat_ugn/
+ libgomp node ${GCC}/libgomp/
+ libstdc++ node ${GCC}/libstdc++/
+ #
+ gccint node ${GCC}/gccint/
+ cppinternals node ${GCC}/cppinternals/
+ gfc-internals node ${GCC}/gfc-internals/
+ gnat-style node ${GCC}/gnat-style/
+ libiberty node ${GCC}/libiberty/
+
+GDB = https://sourceware.org/gdb/current/onlinedocs
+gdb node ${GDB}/gdb.html/
+ stabs node ${GDB}/stabs.html/
+
+GDBM = http://www.gnu.org.ua/software/gdbm/manual
+gdbm node ${GDBM}/
+
+gettext mono ${GS}/gettext/manual/gettext.html
+gettext node ${GS}/gettext/manual/html_node/
+
+gforth node https://www.complang.tuwien.ac.at/forth/gforth/Docs-html/
+
+global mono ${GS}/global/manual/global.html
+
+gmediaserver node ${GS}/gmediaserver/manual/
+
+gmp node https://www.gmplib.org/manual/
+
+gnu-arch node ${GS}/gnu-arch/tutorial/
+
+gnu-c-manual mono ${GS}/gnu-c-manual/gnu-c-manual.html
+
+gnu-crypto node ${GS}/gnu-crypto/manual/
+
+gnubg mono ${GS}/gnubg/manual/gnubg.html
+gnubg node ${GS}/gnubg/manual/html_node/
+
+GNUCOBOL = https://gnucobol.sourceforge.io/HTML
+gnucobpg mono ${GNUCOBOL}/gnucobpg.html
+ gnucobqr mono ${GNUCOBOL}/gnucobqr.html
+ gnucobsp mono ${GNUCOBOL}/gnucobsp.html
+
+gnubik mono ${GS}/gnubik/manual/gnubik.html
+gnubik node ${GS}/gnubik/manual/html_node/
+
+gnulib mono ${GS}/gnulib/manual/gnulib.html
+gnulib node ${GS}/gnulib/manual/html_node/
+
+GNUN = ${GS}/trans-coord/manual
+gnun mono ${GNUN}/gnun/gnun.html
+gnun node ${GNUN}/gnun/html_node/
+ web-trans mono ${GNUN}/web-trans/web-trans.html
+ web-trans node ${GNUN}/web-trans/html_node/
+
+GNUPG = https://www.gnupg.org/documentation/manuals
+gnupg node ${GNUPG}/gnupg/
+ dirmngr node ${GNUPG}/dirmngr/
+ gcrypt node ${GNUPG}/gcrypt/
+ libgcrypt node ${GNUPG}/gcrypt/
+ ksba node ${GNUPG}/ksba/
+ assuan node ${GNUPG}/assuan/
+ gpgme node ${GNUPG}/gpgme/
+
+gnuprologjava node ${GS}/gnuprologjava/manual/
+
+gnuschool mono ${GS}/gnuschool/gnuschool.html
+
+GNUSTANDARDS = ${G}/prep
+ maintain mono ${GNUSTANDARDS}/maintain/maintain.html
+ maintain node ${GNUSTANDARDS}/maintain/html_node/
+ #
+ standards mono ${GNUSTANDARDS}/standards/standards.html
+ standards node ${GNUSTANDARDS}/standards/html_node/
+
+# following url is a redirect, which cannot be used for links within the manual
+#gnutls mono ${GS}/gnutls/manual/gnutls.html
+# empty directory
+#gnutls node ${GS}/gnutls/manual/html_node/
+GNUTLS = http://www.gnutls.org/manual
+gnutls mono ${GNUTLS}/gnutls.html
+gnutls node ${GNUTLS}/html_node/
+
+gperf mono ${GS}/gperf/manual/gperf.html
+gperf node ${GS}/gperf/manual/html_node/
+
+grep mono ${GS}/grep/manual/grep.html
+grep node ${GS}/grep/manual/html_node/
+
+groff node ${GS}/groff/manual/html_node/
+
+GRUB = ${GS}/grub/manual/
+ grub mono ${GRUB}/grub/grub.html
+ grub node ${GRUB}/grub/html_node/
+ #
+ multiboot mono ${GRUB}/multiboot/multiboot.html
+ multiboot node ${GRUB}/multiboot/html_node/
+ #
+ grub-dev mono ${GRUB}/grub-dev/grub-dev.html
+ grub-dev node ${GRUB}/grub-dev/html_node/
+
+gsasl mono ${GS}/gsasl/manual/gsasl.html
+gsasl node ${GS}/gsasl/manual/html_node/
+
+gsl node ${GS}/gsl/manual/html_node/
+
+gsrc mono ${GS}/gsrc/manual/gsrc.html
+gsrc node ${GS}/gsrc/manual/html_node/
+
+gss mono ${GS}/gss/manual/gss.html
+gss node ${GS}/gss/manual/html_node/
+
+gtypist mono ${GS}/gtypist/doc/gtypist.html
+
+guile mono ${GS}/guile/manual/guile.html
+guile node ${GS}/guile/manual/html_node/
+
+GUILE_GNOME = ${GS}/guile-gnome/docs
+ gobject node ${GUILE_GNOME}/gobject/html/
+ glib node ${GUILE_GNOME}/glib/html/
+ atk node ${GUILE_GNOME}/atk/html/
+ pango node ${GUILE_GNOME}/pango/html/
+ pangocairo node ${GUILE_GNOME}/pangocairo/html/
+ gdk node ${GUILE_GNOME}/gdk/html/
+ gtk node ${GUILE_GNOME}/gtk/html/
+ libglade node ${GUILE_GNOME}/libglade/html/
+ gnome-vfs node ${GUILE_GNOME}/gnome-vfs/html/
+ libgnomecanvas node ${GUILE_GNOME}/libgnomecanvas/html/
+ gconf node ${GUILE_GNOME}/gconf/html/
+ libgnome node ${GUILE_GNOME}/libgnome/html/
+ libgnomeui node ${GUILE_GNOME}/libgnomeui/html/
+ corba node ${GUILE_GNOME}/corba/html/
+ clutter node ${GUILE_GNOME}/clutter/html/
+ clutter-glx node ${GUILE_GNOME}/clutter-glx/html/
+
+guile-gtk node ${GS}/guile-gtk/docs/guile-gtk/
+
+guile-rpc mono ${GS}/guile-rpc/manual/guile-rpc.html
+guile-rpc node ${GS}/guile-rpc/manual/html_node/
+
+guix mono ${GS}/guix/manual/guix.html
+guix node ${GS}/guix/manual/html_node/
+
+gv mono ${GS}/gv/manual/gv.html
+gv node ${GS}/gv/manual/html_node/
+
+gzip mono ${GS}/gzip/manual/gzip.html
+gzip node ${GS}/gzip/manual/html_node/
+
+hello mono ${GS}/hello/manual/hello.html
+hello node ${GS}/hello/manual/html_node/
+
+help2man mono ${GS}/help2man/help2man.html
+
+idutils mono ${GS}/idutils/manual/idutils.html
+idutils node ${GS}/idutils/manual/html_node/
+
+inetutils mono ${GS}/inetutils/manual/inetutils.html
+inetutils node ${GS}/inetutils/manual/html_node/
+
+# No manual, redirects to git sources
+#jwhois mono ${GS}/jwhois/manual/jwhois.html
+# 404 Not Found
+#jwhois node ${GS}/jwhois/manual/html_node/
+
+libc mono ${GS}/libc/manual/html_mono/libc.html
+libc node ${GS}/libc/manual/html_node/
+
+LIBCDIO = ${GS}/libcdio
+ libcdio mono ${LIBCDIO}/libcdio.html
+ cd-text mono ${LIBCDIO}/cd-text-format.html
+
+libextractor mono ${GS}/libextractor/manual/libextractor.html
+libextractor node ${GS}/libextractor/manual/html_node/
+
+libidn mono ${GS}/libidn/manual/libidn.html
+libidn node ${GS}/libidn/manual/html_node/
+
+libidn2 mono ${GS}/libidn/libidn2/manual/libidn2.html
+libidn2 node ${GS}/libidn/libidn2/manual/html_node/
+
+librejs mono ${GS}/librejs/manual/librejs.html
+librejs node ${GS}/librejs/manual/html_node/
+
+libmatheval mono ${GS}/libmatheval/manual/libmatheval.html
+
+LIBMICROHTTPD = ${GS}/libmicrohttpd
+libmicrohttpd mono ${LIBMICROHTTPD}/manual/libmicrohttpd.html
+libmicrohttpd node ${LIBMICROHTTPD}/manual/html_node/
+ # The manual name is based on the Texinfo file name in the code,
+ # not on the file name for the tutorial which is too generic.
+ microhttpd-tutorial mono ${LIBMICROHTTPD}/tutorial.html
+
+libtasn1 mono ${GS}/libtasn1/manual/libtasn1.html
+libtasn1 node ${GS}/libtasn1/manual/html_node/
+
+libtool mono ${GS}/libtool/manual/libtool.html
+libtool node ${GS}/libtool/manual/html_node/
+
+lightning mono ${GS}/lightning/manual/lightning.html
+lightning node ${GS}/lightning/manual/html_node/
+
+# The stable/ url redirects immediately, but that's ok.
+# The .html extension is omitted on their web site, but it works if given.
+LILYPOND = http://lilypond.org/doc/stable/Documentation
+ lilypond-internals node ${LILYPOND}/internals/
+ lilypond-learning node ${LILYPOND}/learning/
+ lilypond-notation node ${LILYPOND}/notation/
+ lilypond-snippets node ${LILYPOND}/snippets/
+ lilypond-usage node ${LILYPOND}/usage/
+ lilypond-web node ${LILYPOND}/web/
+ music-glossary node ${LILYPOND}/music-glossary/
+
+liquidwar6 mono ${GS}/liquidwar6/manual/liquidwar6.html
+liquidwar6 node ${GS}/liquidwar6/manual/html_node/
+
+lispintro mono ${GS}/emacs/emacs-lisp-intro/html_mono/emacs-lisp-intro.html
+lispintro node ${GS}/emacs/emacs-lisp-intro/html_node/index.html
+
+LSH = http://www.lysator.liu.se/~nisse/lsh
+ lsh mono ${LSH}/lsh.html
+
+m4 mono ${GS}/m4/manual/m4.html
+m4 node ${GS}/m4/manual/html_node/
+
+MITGNUSCHEME = ${GS}/mit-scheme/documentation/stable
+mit-scheme-user mono ${MITGNUSCHEME}/mit-scheme-user.html
+mit-scheme-user node ${MITGNUSCHEME}/mit-scheme-user/
+ #
+ mit-scheme-ref mono ${MITGNUSCHEME}/mit-scheme-ref.html
+ mit-scheme-ref node ${MITGNUSCHEME}/mit-scheme-ref/
+ #
+ mit-scheme-ffi mono ${MITGNUSCHEME}/mit-scheme-ffi.html
+ mit-scheme-ffi node ${MITGNUSCHEME}/mit-scheme-ffi/
+ #
+ mit-scheme-sos mono ${MITGNUSCHEME}/mit-scheme-sos.html
+ mit-scheme-sos node ${MITGNUSCHEME}/mit-scheme-sos/
+ #
+ mit-scheme-imail mono ${MITGNUSCHEME}/mit-scheme-imail.html
+ #
+ mit-scheme-blowfish mono ${MITGNUSCHEME}/mit-scheme-blowfish.html
+ #
+ mit-scheme-gdbm mono ${MITGNUSCHEME}/mit-scheme-gdbm.html
+
+mailutils mono ${GS}/mailutils/manual/mailutils.html
+mailutils chapter ${GS}/mailutils/manual/html_chapter/
+mailutils section ${GS}/mailutils/manual/html_section/
+mailutils node ${GS}/mailutils/manual/html_node/
+
+make mono ${GS}/make/manual/make.html
+make node ${GS}/make/manual/html_node/
+
+mdk mono ${GS}/mdk/manual/mdk.html
+mdk node ${GS}/mdk/manual/html_node/
+
+METAEXCHANGE = https://ftp.gwdg.de/pub/gnu2/iwfmdh/doc/texinfo
+ iwf_mh node ${METAEXCHANGE}/iwf_mh.html
+ scantest node ${METAEXCHANGE}/scantest.html
+
+MIT_SCHEME = ${GS}/mit-scheme/documentation/stable
+ mit-scheme-ref node ${MIT_SCHEME}/mit-scheme-ref/
+ mit-scheme-user node ${MIT_SCHEME}/mit-scheme-user/
+ sos node ${MIT_SCHEME}/mit-scheme-sos/
+ mit-scheme-imail mono ${MIT_SCHEME}/mit-scheme-imail.html
+
+moe mono ${GS}/moe/manual/moe_manual.html
+
+motti node ${GS}/motti/manual/
+
+# only PDF is available as documentation to download
+#mpc node http://www.multiprecision.org/index.php?prog=mpc&page=html
+
+mpfr mono https://www.mpfr.org/mpfr-current/mpfr.html
+
+mtools mono ${GS}/mtools/manual/mtools.html
+
+nano mono https://www.nano-editor.org/dist/latest/nano.html
+
+nettle mono https://www.lysator.liu.se/~nisse/nettle/nettle.html
+
+ocrad mono ${GS}/ocrad/manual/ocrad_manual.html
+
+parted mono ${GS}/parted/manual/parted.html
+parted node ${GS}/parted/manual/html_node/
+
+pascal node https://www.gnu-pascal.de/gpc/
+
+# can't use pcb since url's contain dates --30nov10
+
+PIES = http://www.gnu.org.ua/software/pies/manual
+pies node ${PIES}/
+
+plotutils mono ${GS}/plotutils/manual/en/plotutils.html
+plotutils node ${GS}/plotutils/manual/en/html_node/
+
+proxyknife mono ${GS}/proxyknife/manual/proxyknife.html
+proxyknife node ${GS}/proxyknife/manual/html_node/
+
+pspp mono ${GS}/pspp/manual/pspp.html
+pspp node ${GS}/pspp/manual/html_node/
+
+pyconfigure mono ${GS}/pyconfigure/manual/pyconfigure.html
+pyconfigure node ${GS}/pyconfigure/manual/html_node/
+
+R = https://cran.r-project.org/doc/manuals
+ R-intro mono ${R}/R-intro.html
+ R-lang mono ${R}/R-lang.html
+ R-exts mono ${R}/R-exts.html
+ R-data mono ${R}/R-data.html
+ R-admin mono ${R}/R-admin.html
+ R-ints mono ${R}/R-ints.html
+
+rcs mono ${GS}/rcs/manual/rcs.html
+rcs node ${GS}/rcs/manual/html_node/
+
+READLINE = https://tiswww.cwru.edu/php/chet/readline
+readline mono ${READLINE}/readline.html
+ rluserman mono ${READLINE}/rluserman.html
+ history mono ${READLINE}/history.html
+
+# no manual for Recode found. Most recent fork seems to be at
+# https://github.com/rrthomas/recode/
+
+recutils mono ${GS}/recutils/manual/recutils.html
+recutils node ${GS}/recutils/manual/html_node/
+
+remotecontrol mono ${GS}/remotecontrol/manual/remotecontrol.html
+remotecontrol node ${GS}/remotecontrol/manual/html_node/
+
+rottlog mono ${GS}/rottlog/manual/rottlog.html
+rottlog node ${GS}/rottlog/manual/html_node/
+
+RUSH = http://www.gnu.org.ua/software/rush/manual
+rush mono ${RUSH}/rush.html
+rush chapter ${RUSH}/html_chapter/
+rush section ${RUSH}/html_section/
+rush node ${RUSH}/html_node/
+
+screen mono ${GS}/screen/manual/screen.html
+screen node ${GS}/screen/manual/html_node/
+
+sed mono ${GS}/sed/manual/sed.html
+sed node ${GS}/sed/manual/html_node/
+
+sharutils mono ${GS}/sharutils/manual/sharutils.html
+sharutils chapter ${GS}/sharutils/manual/html_chapter/
+sharutils node ${GS}/sharutils/manual/html_node/
+
+# replaces dmd
+shepherd mono ${GS}/shepherd/manual/shepherd.html
+shepherd node ${GS}/shepherd/manual/html_node/
+
+SMALLTALK = ${GS}/smalltalk
+gst mono ${SMALLTALK}/manual/gst.html
+gst node ${SMALLTALK}/manual/html_node/
+ #
+ gst-base mono ${SMALLTALK}/manual-base/gst-base.html
+ gst-base node ${SMALLTALK}/manual-base/html_node/
+ #
+ gst-libs mono ${SMALLTALK}/manual-libs/gst-libs.html
+ gst-libs node ${SMALLTALK}/manual-libs/html_node/
+
+sourceinstall mono ${GS}/sourceinstall/manual/sourceinstall.html
+sourceinstall node ${GS}/sourceinstall/manual/html_node/
+
+sqltutor mono ${GS}/sqltutor/manual/sqltutor.html
+sqltutor node ${GS}/sqltutor/manual/html_node/
+
+src-highlite mono ${GS}/src-highlite/source-highlight.html
+
+swbis mono ${GS}/swbis/manual.html
+
+tar mono ${GS}/tar/manual/tar.html
+tar chapter ${GS}/tar/manual/html_chapter/
+tar section ${GS}/tar/manual/html_section/
+tar node ${GS}/tar/manual/html_node/
+
+teseq mono ${GS}/teseq/manual/teseq.html
+teseq node ${GS}/teseq/manual/html_node/
+
+TEXINFO = ${GS}/texinfo/manual
+texinfo mono ${TEXINFO}/texinfo/texinfo.html
+texinfo node ${TEXINFO}/texinfo/html_node/
+ #
+ info-stnd mono ${TEXINFO}/info-stnd/info-stnd.html
+ info-stnd node ${TEXINFO}/info-stnd/html_node/
+ #
+ texi2any_api mono ${TEXINFO}/texi2any_api/texi2any_api.html
+ texi2any_api node ${TEXINFO}/texi2any_api/html_node/
+ #
+ texi2any_internals mono ${TEXINFO}/texi2any_internals/texi2any_internals.html
+ texi2any_internals chapter ${TEXINFO}/texi2any_internals/html_chapter/
+
+thales node ${GS}/thales/manual/
+
+units mono ${GS}/units/manual/units.html
+units node ${GS}/units/manual/html_node/
+
+vc-dwim mono ${GS}/vc-dwim/manual/vc-dwim.html
+vc-dwim node ${GS}/vc-dwim/manual/html_node/
+
+wdiff mono ${GS}/wdiff/manual/wdiff.html
+wdiff node ${GS}/wdiff/manual/html_node/
+
+websocket4j mono ${GS}/websocket4j/manual/websocket4j.html
+websocket4j node ${GS}/websocket4j/manual/html_node/
+
+wget mono ${GS}/wget/manual/wget.html
+wget node ${GS}/wget/manual/html_node/
+
+xboard mono ${GS}/xboard/manual/xboard.html
+xboard node ${GS}/xboard/manual/html_node/
+
+# emacs-page
+# Free TeX-related Texinfo manuals on tug.org.
+
+T = https://tug.org/texinfohtml
+
+dvipng mono ${T}/dvipng.html
+dvips mono ${T}/dvips.html
+eplain mono ${T}/eplain.html
+kpathsea mono ${T}/kpathsea.html
+latex2e mono ${T}/latex2e.html
+tlbuild mono ${T}/tlbuild.html
+web2c mono ${T}/web2c.html
+
+
+# Local Variables:
+# eval: (add-hook 'write-file-hooks 'time-stamp)
+# time-stamp-start: "htmlxrefversion="
+# time-stamp-format: "%:y-%02m-%02d.%02H"
+# time-stamp-time-zone: "UTC"
+# time-stamp-end: "; # UTC"
+# End:
--- /dev/null
+## Copyright (C) 2022 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+##
+## 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, write to the Free Software Foundation,
+## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+
+# generate some build data for use in mu4e
+mu4e_meta = configure_file(
+ input: 'mu4e-config.el.in',
+ output: 'mu4e-config.el',
+ install: true,
+ install_dir: mu4e_lispdir,
+ configuration: {
+ 'VERSION' : meson.project_version(),
+ 'MU_DOC_DIR' : join_paths(datadir, 'doc', 'mu'),
+ })
+
+mu4e_pkg_desc = configure_file(
+ input: 'mu4e-pkg.el.in',
+ output: 'mu4e-pkg.el',
+ install: true,
+ install_dir: mu4e_lispdir,
+ configuration: {
+ 'VERSION' : meson.project_version(),
+ 'EMACS_MIN_VERSION' : emacs_min_version,
+ })
+
+mu4e_srcs=[
+ 'mu4e-actions.el',
+ 'mu4e-bookmarks.el',
+ 'mu4e-compose.el',
+ 'mu4e-contacts.el',
+ 'mu4e-context.el',
+ 'mu4e-contrib.el',
+ 'mu4e-draft.el',
+ 'mu4e-folders.el',
+ 'mu4e.el',
+ 'mu4e-headers.el',
+ 'mu4e-helpers.el',
+ 'mu4e-icalendar.el',
+ 'mu4e-lists.el',
+ 'mu4e-main.el',
+ 'mu4e-mark.el',
+ 'mu4e-message.el',
+ 'mu4e-mime-parts.el',
+ 'mu4e-modeline.el',
+ 'mu4e-notification.el',
+ 'mu4e-obsolete.el',
+ 'mu4e-org.el',
+ 'mu4e-query-items.el',
+ 'mu4e-search.el',
+ 'mu4e-server.el',
+ 'mu4e-speedbar.el',
+ 'mu4e-thread.el',
+ 'mu4e-update.el',
+ 'mu4e-vars.el',
+ 'mu4e-view.el',
+ 'mu4e-window.el'
+]
+
+# note, we cannot compile mu4e-config.el without incurring
+# WARNING: Source item
+# '[...]/build/mu4e/mu4e-meta.el' cannot be converted to File object, because
+# it is a generated file. This will become a hard error in the future.
+#
+#... so let's not do that!
+
+foreach src : mu4e_srcs
+ target_name= '@BASENAME@.elc'
+ target_path = join_paths(meson.current_build_dir(), target_name)
+ target_func = '(setq byte-compile-dest-file-function(lambda(_) "' + target_path + '"))'
+
+ # hack-around for native compile issue: copy sources to builddir.
+ # see: https://debbugs.gnu.org/db/47/47987.html
+ configure_file(input: src, output:'@BASENAME@.el', copy:true,
+ install_mode: 'r--r--r--')
+
+ custom_target(src.underscorify() + '_el',
+ build_by_default: true,
+ input: src,
+ output: target_name,
+ install_dir: mu4e_lispdir,
+ install: true,
+ # rebuild all if any changed.
+ depend_files: mu4e_srcs,
+ command: [emacs,
+ '--no-init-file',
+ '--batch',
+ '--directory', meson.current_source_dir(),
+ '--directory', meson.current_build_dir(),
+ # we don't need warnings for items that have become
+ # obsolete _after_ our last supported emacs release.
+ '--eval', '(setq byte-compile-warnings \'(not obsolete))',
+ '--eval', target_func,
+ '--funcall', 'batch-byte-compile', '@INPUT@'])
+
+endforeach
+
+# this depends on the above hack: all mu4e elisp files needs to be in builddir
+mu4e_autoloads = configure_file(
+ output: 'mu4e-autoloads.el',
+ install: true,
+ install_dir: mu4e_lispdir,
+ command: [emacs,
+ '--no-init-file',
+ '--batch',
+ '--load', 'package',
+ '--eval', '(package-generate-autoloads "mu4e" "' +
+ meson.current_build_dir() + '" )'])
+
+# also install the sources and the config
+install_data(mu4e_srcs, install_dir: mu4e_lispdir)
+
+# install mu4e-about.org
+install_data('mu4e-about.org', install_dir : join_paths(datadir, 'doc', 'mu'))
+
+if makeinfo.found()
+ custom_target('mu4e_info',
+ input: 'mu4e.texi',
+ output: 'mu4e.info',
+ install_dir: infodir,
+ install: true,
+ command: [makeinfo,
+ '-o', join_paths(meson.current_build_dir(), 'mu4e.info'),
+ join_paths(meson.current_source_dir(), 'mu4e.texi'),
+ '-I', join_paths(meson.current_build_dir(), '..')])
+ if install_info.found()
+ infodir = join_paths(get_option('prefix') / get_option('infodir'))
+ meson.add_install_script(install_info_script, infodir, 'mu4e.info')
+ endif
+endif
--- /dev/null
+#+STARTUP:showall
+* About mu4e
+
+ *mu4e* is an emacs e-mail client based on the [[http://djcbsoftware.nl/code/mu][mu]] email search engine. It was
+ written & designed by /Dirk-Jan C. Binnema/, with contributions from others.
+
+ *mu4e* and *mu* are free software, licensed under the terms of the [[http://www.gnu.org/licenses/gpl-3.0.html][GNU GPLv3]].
+
+ You can get the code from [[https://github.com/djcb/mu][the git repository]]; there, you can also
+ [[https://github.com/djcb/mu/issues][file bugs and feature requests]].
+
+ *mu4e* has its own [[info:mu4e][manual]], which includes an [[info:mu4e#FAQ%20-%20Frequently%20Anticipated%20Questions][FAQ]]. If that is not enough,
+ there's also the [[http://groups.google.com/group/mu-discuss][mu mailing list]].
+
+ [Press *q* to quit this buffer]
--- /dev/null
+;;; mu4e-actions.el --- Actions for messages/attachments -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2023 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Example actions for messages, attachments (see chapter 'Actions' in the
+;; manual)
+
+;;; Code:
+
+(require 'ido)
+(require 'browse-url)
+
+(require 'mu4e-helpers)
+(require 'mu4e-message)
+(require 'mu4e-search)
+(require 'mu4e-contacts)
+(require 'mu4e-lists)
+\f
+;;; Count lines
+
+(defun mu4e-action-count-lines (msg)
+ "Count the number of lines in the e-mail MSG.
+Works for headers view and message-view."
+ (message "Number of lines: %s"
+ (shell-command-to-string
+ (concat "wc -l < "
+ (shell-quote-argument (mu4e-message-field msg :path))))))
+
+;;; Org Helpers
+
+(defvar mu4e-captured-message nil
+ "The most recently captured message.")
+
+(defun mu4e-action-capture-message (msg)
+ "Remember MSG.
+Later, we can create an attachment based on this message with
+`mu4e-compose-attach-captured-message'."
+ (setq mu4e-captured-message msg)
+ (message "Message has been captured"))
+
+
+(defun mu4e-action-copy-message-file-path (msg)
+ "Save the full path for the current MSG to the kill ring."
+ (kill-new (mu4e-message-field msg :path)))
+
+(defvar mu4e-org-contacts-file nil
+ "File to store contact information for org-contacts.
+Needed by `mu4e-action-add-org-contact'.")
+
+(eval-when-compile ;; silence compiler warning about free variable
+ (unless (require 'org-capture nil 'noerror)
+ (defvar org-capture-templates nil)))
+
+(defun mu4e-action-add-org-contact (msg)
+ "Add an org-contact based on the sender ddress of the current MSG.
+You need to set `mu4e-org-contacts-file' to the full path to the
+file where you store your org-contacts."
+ (unless (require 'org-capture nil 'noerror)
+ (mu4e-error "Feature org-capture is not available"))
+ (unless mu4e-org-contacts-file
+ (mu4e-error "Variable `mu4e-org-contacts-file' is nil"))
+ (let* ((sender (car-safe (mu4e-message-field msg :from)))
+ (name (mu4e-contact-name sender))
+ (email (mu4e-contact-email sender))
+ (blurb
+ (format
+ (concat
+ "* %%?%s\n"
+ ":PROPERTIES:\n"
+ ":EMAIL: %s\n"
+ ":NICK:\n"
+ ":BIRTHDAY:\n"
+ ":END:\n\n")
+ (or name email "")
+ (or email "")))
+ (key "mu4e-add-org-contact-key")
+ (org-capture-templates
+ (append org-capture-templates
+ (list (list key "contacts" 'entry
+ (list 'file mu4e-org-contacts-file) blurb)))))
+ (when (fboundp 'org-capture)
+ (org-capture nil key))))
+
+;;; Patches
+
+(defvar mu4e--patch-directory-history nil
+ "History of directories we have applied patches to.")
+
+;; This essentially works around the fact that read-directory-name
+;; can't have custom history.
+(defun mu4e--read-patch-directory (&optional prompt)
+ "Read a `PROMPT'ed directory name via `completing-read' with history."
+ (unless prompt
+ (setq prompt "Target directory:"))
+ (file-truename
+ (completing-read prompt 'read-file-name-internal #'file-directory-p
+ nil nil 'mu4e--patch-directory-history)))
+
+(defun mu4e-action-git-apply-patch (msg)
+ "Apply `MSG' as a git patch."
+ (let ((path (mu4e--read-patch-directory "Target directory: ")))
+ (let ((default-directory path))
+ (shell-command
+ (format "git apply %s"
+ (shell-quote-argument (mu4e-message-field msg :path)))))))
+
+(defun mu4e-action-git-apply-mbox (msg &optional signoff)
+ "Apply `MSG' a git patch with optional `SIGNOFF'.
+
+If the `default-directory' matches the most recent history entry don't
+bother asking for the git tree again (useful for bulk actions)."
+
+ (let ((cwd (substring-no-properties
+ (or (car mu4e--patch-directory-history)
+ "not-a-dir"))))
+ (unless (and (stringp cwd) (string= default-directory cwd))
+ (setq cwd (mu4e--read-patch-directory "Target directory: ")))
+ (let ((default-directory cwd))
+ (shell-command
+ (format "git am %s %s"
+ (if signoff "--signoff" "")
+ (shell-quote-argument (mu4e-message-field msg :path)))))))
+
+;;; Tagging
+
+(defvar mu4e-action-tags-header "X-Keywords"
+ "Header where tags are stored.
+Used by `mu4e-action-retag-message'. Make sure it is one of the
+headers mu recognizes for storing tags: X-Keywords, X-Label,
+Keywords. Also note that changing this setting on already tagged
+messages can lead to messages with multiple tags headers.")
+
+(defvar mu4e-action-tags-completion-list '()
+ "List of tags for completion in `mu4e-action-retag-message'.")
+
+(defun mu4e--contains-line-matching (regexp path)
+ "Return non-nil if the file at PATH contain a line matching REGEXP.
+Otherwise return nil."
+ (with-temp-buffer
+ (insert-file-contents path)
+ (save-excursion
+ (goto-char (point-min))
+ (re-search-forward regexp nil t))))
+
+(defun mu4e--replace-first-line-matching (regexp to-string path)
+ "Replace first line matching REGEXP in PATH with TO-STRING."
+ (with-temp-file path
+ (insert-file-contents path)
+ (save-excursion
+ (goto-char (point-min))
+ (if (re-search-forward regexp nil t)
+ (replace-match to-string t nil)))))
+
+(declare-function mu4e--server-add "mu4e-server")
+(defun mu4e--refresh-message (path)
+ "Re-parse message at PATH.
+if this works, we will
+receive (:info add :path <path> :docid <docid>) as well as (:update
+<msg-sexp>)."
+ (mu4e--server-add path))
+
+(defun mu4e-action-retag-message (msg &optional retag-arg)
+ "Change tags of MSG with RETAG-ARG.
+
+RETAG-ARG is a comma-separated list of additions and removals.
+
+Example: +tag,+long tag,-oldtag
+would add \"tag\" and \"long tag\", and remove \"oldtag\"."
+ (let* (
+ (path (mu4e-message-field msg :path))
+ (oldtags (mu4e-message-field msg :tags))
+ (tags-completion
+ (append
+ mu4e-action-tags-completion-list
+ (mapcar (lambda (tag) (format "+%s" tag))
+ mu4e-action-tags-completion-list)
+ (mapcar (lambda (tag) (format "-%s" tag))
+ oldtags)))
+ (retag (if retag-arg
+ (split-string retag-arg ",")
+ (completing-read-multiple "Tags: " tags-completion)))
+ (header mu4e-action-tags-header)
+ (sep (cond ((string= header "Keywords") ", ")
+ ((string= header "X-Label") " ")
+ ((string= header "X-Keywords") ", ")
+ (t ", ")))
+ (taglist (if oldtags (copy-sequence oldtags) '()))
+ tagstr)
+ (dolist (tag retag taglist)
+ (cond
+ ((string-match "^\\+\\(.+\\)" tag)
+ (setq taglist (push (match-string 1 tag) taglist)))
+ ((string-match "^\\-\\(.+\\)" tag)
+ (setq taglist (delete (match-string 1 tag) taglist)))
+ (t
+ (setq taglist (push tag taglist)))))
+
+ (setq taglist (sort (delete-dups taglist) 'string<))
+ (setq tagstr (mapconcat 'identity taglist sep))
+
+ (setq tagstr (replace-regexp-in-string "[\\&]" "\\\\\\&" tagstr))
+ (setq tagstr (replace-regexp-in-string "[/]" "\\&" tagstr))
+
+ (if (not (mu4e--contains-line-matching (concat header ":.*") path))
+ ;; Add tags header just before the content
+ (mu4e--replace-first-line-matching
+ "^$" (concat header ": " tagstr "\n") path)
+
+ ;; replaces keywords, restricted to the header
+ (mu4e--replace-first-line-matching
+ (concat header ":.*")
+ (concat header ": " tagstr)
+ path))
+
+ (mu4e-message (concat "tagging: " (mapconcat 'identity taglist ", ")))
+ (mu4e--refresh-message path)))
+
+(defun mu4e-action-show-thread (msg)
+ "Show thread for message at point with point remaining on MSG.
+I.e., point remains on the message with the message-id where the
+action was invoked. If invoked in view mode, continue to display
+the message."
+ (let ((msgid (mu4e-message-field msg :message-id)))
+ (when msgid
+ (let ((mu4e-search-threads t)
+ (mu4e-search-include-related t))
+ (mu4e-search
+ (format "msgid:%s" msgid)
+ nil nil nil
+ msgid (and (eq major-mode 'mu4e-view-mode)
+ (not (eq mu4e-split-view 'single-window))))))))
+
+\f
+;;; Mailing list URLS
+
+(defun mu4e-action-browse-list-archive (msg)
+ "Browse the archive for a mailing list message MSG.
+See `mu4e-mailing-list-archive-url'."
+ (interactive (list (mu4e-message-at-point)))
+ (if-let ((url (mu4e-mailing-list-archive-url msg)))
+ (browse-url url)
+ (mu4e-warn "No archive available for this message")))
+
+(defun mu4e-action-copy-list-archive-url (msg)
+ "Copy the archive url for a mailing list message MSG.
+See `mu4e-mailing-list-archive-url'."
+ (interactive (list (mu4e-message-at-point)))
+ (let ((url (mu4e-mailing-list-archive-url msg)))
+ (if (stringp url)
+ (kill-new url)
+ (mu4e-warn "Cannot get archive URL for this message"))))
+
+;;;
+(provide 'mu4e-actions)
+;;; mu4e-actions.el ends here
--- /dev/null
+;;; mu4e-bookmarks.el --- Bookmarks handling -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2023 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+(require 'mu4e-helpers)
+(require 'mu4e-modeline)
+(require 'mu4e-folders)
+(require 'mu4e-query-items)
+
+\f
+;;; Configuration
+
+(defgroup mu4e-bookmarks nil
+ "Settings for bookmarks."
+ :group 'mu4e)
+
+(defcustom mu4e-bookmarks
+ '(( :name "Unread messages"
+ :query "flag:unread AND NOT flag:trashed"
+ :key ?u)
+ ( :name "Today's messages"
+ :query "date:today..now"
+ :key ?t)
+ ( :name "Last 7 days"
+ :query "date:7d..now"
+ :hide-unread t
+ :key ?w)
+ ( :name "Messages with images"
+ :query "mime:image/*"
+ :key ?p))
+ "List of pre-defined queries that are shown on the main screen.
+
+Each of the list elements is a plist with at least:
+`:name' - the name of the query
+`:query' - the query expression string or function
+`:key' - the shortcut key (single character)
+
+Optionally, you can add the following:
+
+- `:favorite' - if t, monitor the results of this query, and make
+it eligible for showing its status in the modeline. At most
+one bookmark should have this set to t (otherwise the _first_
+bookmark is the implicit favorite). The query for the `:favorite'
+item must be unique among `mu4e-bookmarks' and
+`mu4e-maildir-shortcuts'.
+- `:hide' - if t, the bookmark is hidden from the main-view and
+speedbar.
+- `:hide-unread' - do not show the counts of
+unread/total number of matches for the query in the main-view.
+This can be useful if a bookmark uses a very slow query.
+
+`:hide-unread' is implied from `:hide'.
+
+Note: for efficiency, queries used to determine the unread/all
+counts do not discard duplicate or unreadable messages. Thus, the
+numbers shown may differ from the number you get from a normal
+query."
+ :type '(repeat (plist))
+ :group 'mu4e-bookmarks)
+
+
+(defun mu4e-ask-bookmark (prompt)
+ "Ask user for bookmark using PROMPT.
+Return the corresponding query. The bookmark are as defined in
+`mu4e-bookmarks'."
+ (unless (mu4e-bookmarks) (mu4e-error "No bookmarks defined"))
+ (let* ((bmarks (seq-map (lambda (bm)
+ (cons (format "%c%s"
+ (plist-get bm :key)
+ (plist-get bm :name))
+ (plist-get bm :query)))
+ (mu4e-filter-single-key (mu4e-bookmarks)))))
+ (mu4e-read-option prompt bmarks)))
+
+(defun mu4e-get-bookmark-query (kar)
+ "Get the corresponding bookmarked query for shortcut KAR.
+Raise an error if none is found."
+ (let ((chosen-bm
+ (or (seq-find
+ (lambda (bm)
+ (= kar (plist-get bm :key)))
+ (mu4e-bookmarks))
+ (mu4e-warn "Unknown shortcut '%c'" kar))))
+ (mu4e--bookmark-query chosen-bm)))
+
+(defun mu4e-bookmark-define (query name key)
+ "Define a bookmark for QUERY with NAME and shortcut KEY.
+Append it to `mu4e-bookmarks'. Replaces any existing bookmark
+with KEY."
+ (setq mu4e-bookmarks
+ (seq-remove
+ (lambda (bm)
+ (= (plist-get bm :key) key))
+ (mu4e-bookmarks)))
+ (cl-pushnew `(:name ,name
+ :query ,query
+ :key ,key)
+ mu4e-bookmarks :test 'equal))
+
+(defun mu4e-bookmarks ()
+ "Get `mu4e-bookmarks' in the (new) format.
+Convert from the old format if needed."
+ (seq-map (lambda (item)
+ (if (and (listp item) (= (length item) 3))
+ `(:name ,(nth 1 item) :query ,(nth 0 item)
+ :key ,(nth 2 item))
+ item))
+ mu4e-bookmarks))
+
+(defun mu4e-bookmark-favorite ()
+ "Find the favorite bookmark."
+ ;; note, use query-items, which will have picked a favorite
+ ;; even if user did not provide one explictly
+ (seq-find
+ (lambda (item)
+ (plist-get item :favorite))
+ (mu4e-query-items 'bookmarks)))
+
+;; for Zero-Inbox afficionados
+(defvar mu4e-modeline-all-clear '("C:" . "🌀")
+ "No more messages at all for this query.")
+(defvar mu4e-modeline-all-read '("R:" . "✅")
+ "No unread messages left.")
+(defvar mu4e-modeline-unread-items '("U:" . "📫")
+ "There are some unread items.")
+(defvar mu4e-modeline-new-items '("N:" . "🔥")
+ "There are some new items after the baseline.
+I.e., very new messages.")
+
+(declare-function mu4e-search-bookmark "mu4e-search")
+(defun mu4e-jump-to-favorite ()
+ "Jump to to the favorite bookmark, if any."
+ (interactive)
+ (when-let ((fav (mu4e--bookmark-query (mu4e-bookmark-favorite))))
+ (mu4e-search-bookmark fav)))
+
+(defun mu4e--bookmarks-modeline-item ()
+ "Modeline item showing message counts for the favorite bookmark.
+
+This uses the one special ':favorite' bookmark, and if there is
+one, creates a propertized string for display in the modeline."
+ (when-let ((fav ;; any results for the favorite bookmark item?
+ (seq-find (lambda (bm) (plist-get bm :favorite))
+ (mu4e-query-items 'bookmarks))))
+ (cl-destructuring-bind (&key unread count delta-unread
+ &allow-other-keys) fav
+ (propertize
+ (format "%s%s "
+ (funcall (if mu4e-use-fancy-chars 'cdr 'car)
+ (cond
+ ((> delta-unread 0) mu4e-modeline-new-items)
+ ((> unread 0) mu4e-modeline-unread-items)
+ ((> count 0) mu4e-modeline-all-read)
+ (t mu4e-modeline-all-clear)))
+ (mu4e--query-item-display-counts fav))
+ 'help-echo
+ (format
+ (concat
+ "mu4e favorite bookmark '%s':\n"
+ "\t%s\n\n"
+ "number of matches: %d\n"
+ "unread messages: %d\n"
+ "changes since baseline: %+d\n")
+ (plist-get fav :name)
+ (mu4e--bookmark-query fav)
+ count unread delta-unread)
+ 'mouse-face 'mode-line-highlight
+ 'keymap '(mode-line keymap
+ (mouse-1 . mu4e-jump-to-favorite)
+ (mouse-2 . mu4e-jump-to-favorite)
+ (mouse-3 . mu4e-jump-to-favorite))))))
+\f
+(provide 'mu4e-bookmarks)
+;;; mu4e-bookmarks.el ends here
--- /dev/null
+;;; mu4e-compose.el --- Compose and send messages -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2024 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Implements mu4e-compose-mode, which is a `message-mode' derivative. There's
+;; quite a bit of trickery involved to make the message-mode functions work in
+;; this context; see mu4e-draft for details.
+
+\f
+;;; Code:
+(require 'message)
+(require 'sendmail)
+(require 'gnus-msg)
+(require 'nnheader) ;; for make-full-mail-header
+
+(require 'mu4e-obsolete)
+(require 'mu4e-server)
+(require 'mu4e-message)
+(require 'mu4e-context)
+(require 'mu4e-folders)
+
+(require 'mu4e-draft)
+
+\f
+;;; User configuration for compose-mode
+(defgroup mu4e-compose nil
+ "Customization for composing/sending messages."
+ :group 'mu4e)
+
+(defcustom mu4e-compose-format-flowed nil
+ "Whether to compose messages to be sent as format=flowed.
+\(Or with long lines if variable `use-hard-newlines' is set to
+nil). The variable `fill-flowed-encode-column' lets you customize
+the width beyond which format=flowed lines are wrapped."
+ :type 'boolean
+ :safe 'booleanp
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-pre-hook nil
+ "Hook run just *before* message composition starts.
+
+If the compose-type is a symbol, either `reply' or `forward', the
+variable `mu4e-compose-parent-message' is the message replied to
+/ being forwarded / edited, and `mu4e-compose-type' contains the
+type of message to be composed.
+
+Note that there is no draft message yet when this hook runs, it
+is meant for influencing the how mu4e constructs the draft
+message. If you want to do something with the draft messages
+after it has been constructed, `mu4e-compose-mode-hook' would be
+the place to do that."
+ :type 'hook
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-post-hook
+ (list
+ ;; kill compose frames
+ #'mu4e-compose-post-kill-frame
+ ;; attempt to restore the old configuration.
+ #'mu4e-compose-post-restore-window-configuration)
+ "Hook run *after* message composition is over.
+
+This is hook is run when closing the composition buffer, either
+by sending, postponing, exiting or killing it.
+
+This multiplexes the `message-mode' hooks `message-send-actions',
+`message-postpone-actions', `message-exit-actions' and
+`message-kill-actions', and the hook is run with a variable
+`mu4e-compose-post-trigger' set correspondingly to a symbol,
+`send', `postpone', `exit' or `kill'."
+ :type 'hook
+ :group 'mu4e-compose)
+
+\f
+
+(defvar mu4e-captured-message)
+(defun mu4e-compose-attach-captured-message ()
+ "Insert the last captured message file as an attachment.
+Messages are captured with `mu4e-action-capture-message'."
+ (interactive)
+ (if-let* ((msg mu4e-captured-message)
+ (path (plist-get msg :path))
+ (path (and (file-exists-p path) path)))
+ (mml-attach-file
+ path
+ "message/rfc822"
+ (or (plist-get msg :subject) "No subject")
+ "attachment")
+ (mu4e-warn "No valid message has been captured")))
+
+;; Go to bottom / top
+
+(defun mu4e-compose-goto-top (&optional arg)
+ "Go to the beginning of the message or buffer.
+Go to the beginning of the message or, if already there, go to
+the beginning of the buffer.
+
+Push mark at previous position, unless either a
+\\[universal-argument] prefix ARG is supplied, or Transient Mark mode
+is enabled and the mark is active."
+ (interactive "P")
+ (or arg
+ (region-active-p)
+ (push-mark))
+ (let ((old-position (point)))
+ (message-goto-body)
+ (when (equal (point) old-position)
+ (goto-char (point-min)))))
+
+(defun mu4e-compose-goto-bottom (&optional arg)
+ "Go to the end of the message or buffer.
+Go to the end of the message (before signature) or, if already
+there, go to the end of the buffer.
+
+Push mark at previous position, unless either a
+\\[universal-argument] prefix ARG is supplied, or Transient Mark mode
+is enabled and the mark is active."
+ (interactive "P")
+ (or arg
+ (region-active-p)
+ (push-mark))
+ (let ((old-position (point))
+ (message-position (save-excursion (message-goto-body) (point))))
+ (goto-char (point-max))
+ (when (re-search-backward message-signature-separator message-position t)
+ (forward-line -1))
+ (when (equal (point) old-position)
+ (goto-char (point-max)))))
+
+(defun mu4e-compose-context-switch (&optional force name)
+ "Change the context for the current draft message.
+
+With NAME, switch to the context with NAME, and with FORCE non-nil,
+switch even if the switch is to the same context.
+
+Like `mu4e-context-switch' but with some changes after switching:
+1. Update the From and Organization headers as per the new context
+2. Update the `message-signature' as per the new context.
+
+Unlike some earlier version of this function, does _not_ update
+the draft folder for the messages, as that would require changing
+the file under our feet, which is a bit fragile."
+ (interactive "P")
+
+ (unless (derived-mode-p 'mu4e-compose-mode)
+ (mu4e-error "Only available in mu4e compose buffers"))
+
+ (let ((old-context (mu4e-context-current)))
+ (unless (and name (not force) (eq old-context name))
+ (unless (and (not force)
+ (eq old-context (mu4e-context-switch nil name)))
+ (save-excursion
+ ;; Change From / Organization if needed.
+ (message-replace-header "Organization"
+ (or (message-make-organization) "")
+ '("Subject")) ;; keep in same place
+ (message-replace-header "From"
+ (or (message-make-from) ""))
+ ;; Update signature.
+ (when (message-goto-signature) ;; delete old signature.
+ (if message-signature-insert-empty-line
+ (forward-line -2) (forward-line -1))
+ (delete-region (point) (point-max)))
+ (when message-signature
+ (save-excursion (message-insert-signature))))))))
+
+\f
+;;; address completion
+
+;; inspired by org-contacts.el and
+;; https://github.com/nordlow/elisp/blob/master/mine/completion-styles-cycle.el
+
+(defun mu4e--compose-complete-handler (str pred action)
+ "Complete address STR with predication PRED for ACTION."
+ (cond
+ ((eq action nil)
+ (try-completion str mu4e--contacts-set pred))
+ ((eq action t)
+ (all-completions str mu4e--contacts-set pred))
+ ((eq action 'metadata)
+ ;; our contacts are already sorted - just need to tell the completion
+ ;; machinery not to try to undo that...
+ '(metadata
+ (display-sort-function . identity)
+ (cycle-sort-function . identity)))))
+
+(defun mu4e-complete-contact ()
+ "Attempt to complete the text at point with a contact.
+I.e., either \"name <email>\" or \"email\". Return nil if not found.
+
+This function can be used for `completion-at-point-functions', to
+complete addresses. This can be used from outside mu4e, but mu4e
+must be active (running) for this to work."
+ (let* ((end (point))
+ (start (save-excursion
+ (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")
+ (goto-char (match-end 0))
+ (point))))
+ (list start end #'mu4e--compose-complete-handler)))
+
+(defun mu4e--compose-complete-contact-field ()
+ "Attempt to complete a contact when in a contact field.
+
+This is like `mu4e-compose-complete-contact', but limited to the
+contact fields."
+ (let ((mail-abbrev-mode-regexp
+ "^\\(To\\|B?Cc\\|Reply-To\\|From\\|Sender\\):")
+ (mail-header-separator mu4e--header-separator))
+ (when (mail-abbrev-in-expansion-header-p)
+ (mu4e-complete-contact))))
+
+(defun mu4e--compose-setup-completion ()
+ "Maybe enable auto-completion of addresses.
+Do this when `mu4e-compose-complete-addresses' is non-nil.
+
+When enabled, this attempts to put mu4e's completions at the
+start of the buffer-local `completion-at-point-functions'. Other
+completion functions still apply."
+ (when mu4e-compose-complete-addresses
+ (set (make-local-variable 'completion-ignore-case) t)
+ (set (make-local-variable 'completion-cycle-threshold) 7)
+ (add-to-list (make-local-variable 'completion-styles) 'substring)
+ (add-hook 'completion-at-point-functions
+ #'mu4e--compose-complete-contact-field -10 t)))
+
+\f ;;; mu4e-compose-mode
+(defun mu4e--compose-remap-faces ()
+ "Remap `message-mode' faces to mu4e ones.
+
+Our parent `message-mode' uses font-locking for the compose
+buffers; lets remap its faces so it uses the ones for mu4e."
+ ;; normal headers
+ (face-remap-add-relative 'message-header-name 'mu4e-header-field-face)
+ (face-remap-add-relative 'message-header-other 'mu4e-header-value-face)
+ ;; special headers
+ (face-remap-add-relative 'message-header-from 'mu4e-contact-face)
+ (face-remap-add-relative 'message-header-to 'mu4e-contact-face)
+ (face-remap-add-relative 'message-header-cc 'mu4e-contact-face)
+ (face-remap-add-relative 'message-header-bcc 'mu4e-contact-face)
+ (face-remap-add-relative 'message-header-subject
+ 'mu4e-special-header-value-face))
+
+(defvar mu4e-compose-mode-map
+ (let ((map (make-sparse-keymap)))
+ (set-keymap-parent map message-mode-map)
+ (define-key map (kbd "C-S-u") #'mu4e-update-mail-and-index)
+ (define-key map (kbd "C-c C-u") #'mu4e-update-mail-and-index)
+ (define-key map (kbd "C-c ;") #'mu4e-compose-context-switch)
+
+ ;; emacs 29
+ ;;(keymap-set map "<remap> <beginning-of-buffer>" #'mu4e-compose-goto-top)
+ ;;(keymap-set map "<remap> <end-of-buffer>" #'mu4e-compose-goto-bottom)
+ (define-key map (vector 'remap #'beginning-of-buffer)
+ #'mu4e-compose-goto-top)
+ (define-key map (vector 'remap #'end-of-buffer)
+ #'mu4e-compose-goto-bottom)
+
+ ;; remove some unsupported commands... [remap ..] does not work here
+ ;; XXX remove from menu, too.
+ (define-key map (kbd "C-c C-f C-n") nil) ;; message-goto-newsgroups
+ (define-key map (kbd "C-c C-n") nil) ;; message-insert-newsgroups
+ (define-key map (kbd "C-c C-j") nil) ;; gnus-delay-article
+ map)
+ "The keymap for mu4e-compose buffers.")
+
+(defun mu4e--compose-unsupported (&rest _args)
+ "Advise wrapper for Gnus unsupported functions in mu4e."
+ (when (eq major-mode 'mu4e-compose-mode)
+ (mu4e-warn "Not available in mu4e")))
+
+(defun mu4e--neutralize-undesirables ()
+ "Beware Gnus commands that do not work with mu4e."
+ ;; the Field menu contains many items that don't apply.
+ (advice-add 'gnus-delay-article
+ :before #'mu4e--compose-unsupported) ;; # XXX does not work?!
+ (advice-add 'message-goto-newsgroups :before #'mu4e--compose-unsupported)
+ (advice-add 'message-insert-newsgroups :before #'mu4e--compose-unsupported))
+
+(define-derived-mode mu4e-compose-mode message-mode "mu4e:compose"
+ "Major mode for the mu4e message composition, derived from `message-mode'.
+\\{mu4e-compose-mode-map}."
+ (progn
+ (use-local-map mu4e-compose-mode-map)
+ (mu4e-context-minor-mode)
+ (mu4e--neutralize-undesirables)
+ (mu4e--compose-remap-faces)
+ (setq-local nobreak-char-display nil)
+ ;; set this to allow mu4e to work when gnus-agent is unplugged in gnus
+ (set (make-local-variable 'message-send-mail-real-function) nil)
+ ;; Set to nil to enable `electric-quote-local-mode' to work:
+ (set (make-local-variable 'comment-use-syntax) nil)
+ (mu4e--compose-setup-completion) ;; maybe offer address completion
+ (if mu4e-compose-format-flowed ;; format-flowed
+ (progn
+ (turn-off-auto-fill)
+ (setq truncate-lines nil
+ word-wrap t
+ mml-enable-flowed t
+ use-hard-newlines t)
+ (visual-line-mode t))
+ (setq mml-enable-flowed nil))))
+
+(declare-function mu4e-view-message-text "mu4e-view")
+
+(defun mu4e-message-cite-nothing ()
+ "Function for `message-cite-function' that cites _nothing_."
+ (save-excursion
+ (message-cite-original-without-signature)
+ (delete-region (point-min) (point-max))))
+
+(defun mu4e--compose-cite (msg)
+ "Return a cited version of the ORIG message MSG (a string).
+This function uses `message-cite-function', and its settings apply."
+ (with-temp-buffer
+ (insert (mu4e-view-message-text msg))
+ (goto-char (point-min))
+ (push-mark (point-max))
+ (let ((message-signature-separator "^-- *$")
+ (message-signature-insert-empty-line t))
+ (funcall message-cite-function))
+ (pop-mark)
+ (goto-char (point-min))
+ (buffer-string)))
+\f
+
+;;;###autoload
+(defalias 'mu4e-compose-mail #'mu4e-compose-new)
+
+;;;###autoload
+(defun mu4e-compose-new (&optional to subject other-headers continue
+ _switch-function yank-action send-actions
+ return-action &rest _)
+ "Mu4e's implementation of `compose-mail'.
+TO, SUBJECT, OTHER-HEADERS, CONTINUE, YANK-ACTION SEND-ACTIONS
+RETURN-ACTION are as described in `compose-mail', and to the
+extend that they do not conflict with mu4e's inner workings.
+SWITCH-FUNCTION is ignored."
+ (interactive)
+ (mu4e--draft
+ 'new
+ (lambda () (mu4e--message-call
+ #'message-mail to subject other-headers continue
+ nil ;; switch-function -> we handle it ourselves.
+ yank-action send-actions return-action))))
+
+;;;###autoload
+(defun mu4e-compose-reply-to (&optional to wide)
+ "Reply to the message at point.
+Optional TO can be the To: address for the message. If WIDE is
+non-nil, make it a \"wide\" reply (a.k.a. \"reply-to-all\")."
+ (interactive)
+ (let ((parent (mu4e-message-at-point)))
+ (mu4e--draft-with-parent
+ 'reply parent
+ (lambda ()
+ (with-current-buffer (mu4e--message-call #'message-reply to wide)
+ (message-goto-body)
+ (insert (mu4e--compose-cite parent))
+ (current-buffer))))))
+
+;;;###autoload
+(defun mu4e-compose-reply (&optional wide)
+ "Reply to the message at point. If WIDE is
+non-nil, make it a \"wide\" reply (a.k.a. \"reply-to-all\")."
+ (interactive "P")
+ (mu4e-compose-reply-to nil wide))
+
+;;;###autoload
+(defun mu4e-compose-wide-reply ()
+ "Wide reply to the message at point.
+(a.k.a. \"reply-to-all\")."
+ (interactive)
+ (mu4e-compose-reply-to nil t))1
+
+;;;###autoload
+(defun mu4e-compose-supersede ()
+ "Supersede the message at point.
+
+That is, send the message again, with all the same recipients;
+this can be useful to follow-up on a sent message. The message
+must originate from the current user, as determined through
+`mu4e-personal-or-alternative-address-p'."
+ (interactive)
+ (let ((parent (mu4e-message-at-point)))
+ (mu4e--draft-with-parent
+ 'reply ;; it's a special kind of reply.
+ parent
+ (lambda ()
+ (with-current-buffer (mu4e--message-call #'message-supersede))))))
+
+(defun mu4e-compose-forward ()
+ "Forward the message at point.
+To influence the way a message is forwarded, you can use the
+variables ‘message-forward-as-mime’ and
+‘message-forward-show-mml’."
+ (interactive)
+ (let ((parent (mu4e-message-at-point)))
+ (mu4e--draft-with-parent
+ 'forward parent
+ (lambda ()
+ (setq
+ message-reply-headers (make-full-mail-header
+ 0
+ (or (message-field-value "Subject") "none")
+ (or (message-field-value "From") "nobody")
+ (message-field-value "Date")
+ (message-field-value "Message-Id" t)
+ (message-field-value "References")
+ 0 0 ""))
+ ;; a bit of a hack; mu4e--draft-with-parent will insert the decoded
+ ;; version of the message, but that's not good enough for
+ ;; message-forward, which needs the raw message instead; see #2662.
+ (erase-buffer)
+ (insert-file-contents-literally (mu4e-message-readable-path parent))
+ (with-current-buffer (mu4e--message-call #'message-forward)
+ (current-buffer))))))
+
+;;;###autoload
+(defun mu4e-compose-edit()
+ "Edit an existing draft message."
+ (interactive)
+ (let* ((msg (mu4e-message-at-point)))
+ (unless (member 'draft (mu4e-message-field msg :flags))
+ (mu4e-warn "Cannot edit non-draft messages"))
+ (mu4e--draft
+ 'edit
+ (lambda ()
+ (with-current-buffer
+ (find-file-noselect (mu4e-message-readable-path msg))
+ (mu4e--delimit-headers)
+ (current-buffer))))))
+
+;;;###autoload
+(defun mu4e-compose-resend (address)
+ "Re-send the message at point to ADDRESS.
+The message is resent as-is, without any editing. See
+`message-resend' for details."
+ (interactive
+ (list (completing-read
+ "Resend message to address: " mu4e--contacts-set)))
+ (let ((msg (mu4e-message-at-point)))
+ (with-temp-buffer
+ (mu4e--prepare-draft msg)
+ (insert-file-contents (mu4e-message-readable-path msg))
+ (message-resend address))))
+
+;;; Compose Mail
+
+(declare-function mu4e "mu4e")
+
+;;;###autoload
+(define-mail-user-agent 'mu4e-user-agent
+ #'mu4e-compose-mail
+ #'message-send-and-exit
+ #'message-kill-buffer
+ 'message-send-hook)
+
+;; Without this, `mail-user-agent' cannot be set to `mu4e-user-agent'
+;; through customize, as the custom type expects a function. Not
+;; sure whether this function is actually ever used; if it is then
+;; returning the symbol is probably the correct thing to do, as other
+;; such functions suggest.
+(defun mu4e-user-agent ()
+ "Return the `mu4e-user-agent' symbol."
+ 'mu4e-user-agent)
+\f
+;;; minor mode for use in other modes.
+(defvar mu4e-compose-minor-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map "R" #'mu4e-compose-reply)
+ (define-key map "W" #'mu4e-compose-wide-reply)
+ (define-key map "F" #'mu4e-compose-forward)
+ (define-key map "E" #'mu4e-compose-edit)
+ (define-key map "C" #'mu4e-compose-new)
+ map)
+ "Keymap for compose minor-mode.")
+
+(define-minor-mode mu4e-compose-minor-mode
+ "Mode for searching for messages."
+ :global nil
+ :init-value nil ;; disabled by default
+ :group 'mu4e
+ :lighter ""
+ :keymap mu4e-compose-minor-mode-map)
+
+(defvar mu4e--compose-menu-items
+ '("--"
+ ["Compose new" mu4e-compose-new
+ :help "Compose new message"]
+ ["Reply" mu4e-compose-reply
+ :help "Reply to message"]
+ ["Reply to all" mu4e-compose-wide-reply
+ :help "Reply to all-recipients"]
+ ["Forward" mu4e-compose-forward
+ :help "Forward message"]
+ ["Resend" mu4e-compose-resend
+ :help "Re-send message"])
+ "Easy menu items for message composition.")
+\f ;;;
+(provide 'mu4e-compose)
+;;; mu4e-compose.el ends here
--- /dev/null
+;; auto-generated
+
+(defconst mu4e-mu-version "@VERSION@"
+ "Required mu binary version; mu4e's version must agree with this.")
+
+(defconst mu4e-doc-dir "@MU_DOC_DIR@"
+ "Mu4e's data-dir.")
+
+(provide 'mu4e-config)
--- /dev/null
+;;; mu4e-contacts.el --- Dealing with contacts -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022-2023 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Utility functions used in the mu4e
+
+;;; Code:
+(require 'cl-lib)
+(require 'message)
+(require 'mu4e-helpers)
+(require 'mu4e-update)
+\f
+
+;;; Configuration
+(defcustom mu4e-compose-complete-addresses t
+ "Whether to do auto-completion of e-mail addresses."
+ :type 'boolean
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-complete-only-personal nil
+ "Whether to consider only \"personal\" e-mail addresses for completion.
+That is, addresses from messages where user was explicitly in one
+of the address fields (this excludes mailing list messages).
+These addresses are the ones specified with \"mu init\"."
+ :type 'boolean
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-complete-only-after "2018-01-01"
+ "Consider only contacts last seen after this date.
+
+Date must be a string of the form YYYY-MM-DD.
+
+This is useful for limiting a potentially enormous set of
+contacts for auto-completion to just those that are present in
+the e-mail corpus in recent times. Set to nil to not have any
+time-based restriction."
+ :type 'string
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-complete-max nil
+ "Limit the amount of contacts for completion, nil for no limits.
+After considering the other constraints
+\(`mu4e-compose-complete-addresses' and
+`mu4e-compose-complete-only-after'), pick only the highest-ranked
+<n>.
+
+Lowering this variable reduces start-up time and memory usage."
+ :type '(choice natnum (const :tag "No limits" nil))
+ :group 'mu4e-compose)
+
+;; names and mail-addresses can be mapped onto their canonical
+;; counterpart. use the customizeable function
+;; mu4e-canonical-contact-function to do that. below the identity
+;; function for mapping a contact onto the canonical one.
+(defun mu4e-contact-identity (contact)
+ "Return the name and the mail-address of a CONTACT.
+It is used as the identity function for converting contacts to
+their canonical counterpart; useful as an example."
+ (let ((name (plist-get contact :name))
+ (mail (plist-get contact :mail)))
+ (list :name name :mail mail)))
+
+(defcustom mu4e-contact-process-function
+ (lambda(addr)
+ (cond
+ ((string-match-p "reply" addr)
+ ;; no-reply adresses are not useful of course, but neither are are
+ ;; reply-xxxx addresses since they're autogenerated only useful for direct
+ ;; replies.
+ nil)
+ (t addr)))
+ "Function for processing contact information for use in auto-completion.
+
+The function receives the contact as a string, e.g \"Foo Bar
+ <foo.bar@example.com>\" \"cuux@example.com\"
+
+The function should return either:
+- nil: do not use this contact for completion
+- the (possibly rewritten) address, which must be
+an RFC-2822-compatible e-mail address."
+ :type 'function
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-reply-ignore-address
+ '("no-?reply")
+ "Addresses to prune when doing wide replies.
+
+This can be a regexp matching the address, a list of regexps or a
+predicate function. A value of nil keeps all the addresses."
+ :type '(choice
+ (const nil)
+ function
+ string
+ (repeat string))
+ :group 'mu4e-compose)
+
+\f
+;;; Internal variables
+(defvar mu4e--contacts-tstamp "0"
+ "Timestamp for the most recent contacts update." )
+
+(defvar mu4e--contacts-set nil
+ "Set with the full contact addresses for autocompletion.")
+\f
+;;; user mail address
+(defun mu4e-personal-addresses (&optional no-regexp)
+ "Get the list user's personal addresses, as passed to \"mu init\".
+
+The address are either plain e-mail addresses or regexps (strings
+ wrapped / /). When NO-REGEXP is non-nil, do not include regexp
+ address patterns (if any)."
+ (seq-remove
+ (lambda (addr) (and no-regexp (string-match-p "^/.*/" addr)))
+ (when-let ((props (mu4e-server-properties)))
+ (plist-get props :personal-addresses))))
+
+(defun mu4e-personal-address-p (addr)
+ "Is ADDR a personal address?
+Evaluate to nil if ADDR does not match any of the personal
+addresses. Uses \\=(mu4e-personal-addresses) for the addresses
+with both the plain addresses and /regular expressions/."
+ (when addr
+ (seq-find
+ (lambda (m)
+ (if (string-match "/\\(.*\\)/" m)
+ (let ((rx (match-string 1 m))
+ (case-fold-search t))
+ (string-match rx addr))
+ (eq t (compare-strings addr nil nil m nil nil 'case-insensitive))))
+ (mu4e-personal-addresses))))
+
+
+(defun mu4e-personal-or-alternative-address-p (addr)
+ "Is ADDR either a personal or an alternative address?
+
+That is, does it match either `mu4e-personal-address-p' or
+`message-alternative-emails'.
+
+Note that this expanded definition of user-addresses is only used
+in emacs, not in `mu' (e.g. when indexing).
+
+Also see `mu4e-personal-or-alternative-address-or-empty-p'."
+ (let ((alts message-alternative-emails))
+ (or (mu4e-personal-address-p addr)
+ (cond
+ ((functionp alts) (funcall alts addr))
+ ((stringp alts) (string-match alts addr))
+ (t nil)))))
+
+(defun mu4e-personal-or-alternative-address-or-empty-p (addr)
+ "Is ADDR either a personal, alternative address or nil?
+
+This is like `mu4e-personal-or-alternative-address-p' but also
+return t for _empty_ ADDR. This can be useful for use with
+`message-dont-reply-to-names' since it can receive empty strings;
+those can be filtered-out by returning t here.
+
+See #2680 for further details. "
+ (or (and addr (string= addr ""))
+ (mu4e-personal-or-alternative-address-p addr)))
+
+\f
+;; Helpers
+
+;;; RFC2822 handling of phrases in mail-addresses
+;;
+;; The optional display-name contains a phrase, it sits before the
+;; angle-addr as specified in RFC2822 for email-addresses in header
+;; fields. Contributed by jhelberg.
+
+(defun mu4e--rfc822-phrase-type (ph)
+ "Return an atom or quoted-string for the phrase PH.
+This checks for empty string first. Then quotes around the phrase
+\(returning symbol `rfc822-quoted-string'). Then whether there is
+a quote inside the phrase (returning symbol
+`rfc822-containing-quote').
+
+The reverse of the RFC atext definition is then tested. If it
+matches, nil is returned, if not, it returns a symbol
+`rfc822-atom'."
+ (cond
+ ((= (length ph) 0) 'rfc822-empty)
+ ((= (aref ph 0) ?\")
+ (if (string-match "\"\\([^\"\\\n]\\|\\\\.\\|\\\\\n\\)*\"" ph)
+ 'rfc822-quoted-string
+ 'rfc822-containing-quote)) ; starts with quote, but doesn't end with one
+ ((string-match-p "[\"]" ph) 'rfc822-containing-quote)
+ ((string-match-p "[\000-\037()\*<>@,;:\\\.]+" ph) nil)
+ (t 'rfc822-atom)))
+
+(defun mu4e--rfc822-quote-phrase (ph)
+ "Quote an RFC822 phrase PH only if necessary.
+Atoms and quoted strings don't need quotes. The rest do. In
+case a phrase contains a quote, it will be escaped."
+ (let ((type (mu4e--rfc822-phrase-type ph)))
+ (cond
+ ((eq type 'rfc822-atom) ph)
+ ((eq type 'rfc822-quoted-string) ph)
+ ((eq type 'rfc822-containing-quote)
+ (format "\"%s\""
+ (replace-regexp-in-string "\"" "\\\\\"" ph)))
+ (t (format "\"%s\"" ph)))))
+
+(defsubst mu4e-contact-name (contact)
+ "Get the name of this CONTACT, or nil."
+ (plist-get contact :name))
+
+(defsubst mu4e-contact-email (contact)
+ "Get the name of this CONTACT, or nil."
+ (plist-get contact :email))
+
+(defsubst mu4e-contact-cons (contact)
+ "Convert a CONTACT plist into a old-style (name . email)."
+ (cons
+ (mu4e-contact-name contact)
+ (mu4e-contact-email contact)))
+
+(defsubst mu4e-contact-make (name email)
+ "Create a contact plist from NAME and EMAIL."
+ `(:name ,name :email ,email))
+
+(defun mu4e-contact-full (contact)
+ "Get the full combination of name and email address from CONTACT."
+ (let* ((email (mu4e-contact-email contact))
+ (name (mu4e-contact-name contact)))
+ (if (and name (> (length name) 0))
+ (format "%s <%s>" (mu4e--rfc822-quote-phrase name) email)
+ email)))
+
+\f
+(defun mu4e--update-contacts (contacts &optional tstamp)
+ "Receive a sorted list of CONTACTS newer than TSTAMP.
+Update an internal set with it.
+
+This is used by the completion function in mu4e-compose."
+ (let ((n 0))
+ (unless mu4e--contacts-set
+ (setq mu4e--contacts-set (make-hash-table :test 'equal :weakness nil
+ :size (length contacts))))
+ (dolist (contact contacts)
+ (cl-incf n)
+ (when (functionp mu4e-contact-process-function)
+ (setq contact (funcall mu4e-contact-process-function contact)))
+ (when contact ;; note the explicit deccode; the strings we get are
+ ;; utf-8, but emacs doesn't know yet.
+ (puthash (decode-coding-string contact 'utf-8) t mu4e--contacts-set)))
+ (setq mu4e--contacts-tstamp (or tstamp "0"))
+ (unless (zerop n)
+ (mu4e-index-message "Contacts updated: %d; total %d"
+ n (hash-table-count mu4e--contacts-set)))))
+
+(defun mu4e-contacts-info ()
+ "Display information about the contacts-cache.
+For testing/debugging."
+ (interactive)
+ (with-current-buffer (get-buffer-create "*mu4e-contacts-info*")
+ (erase-buffer)
+ (insert (format "complete addresses: %s\n"
+ (if mu4e-compose-complete-addresses "yes" "no")))
+ (insert (format "only personal addresses: %s\n"
+ (if mu4e-compose-complete-only-personal "yes" "no")))
+ (insert (format "only addresses seen after: %s\n"
+ (or mu4e-compose-complete-only-after "no restrictions")))
+
+ (when mu4e--contacts-set
+ (insert (format "number of contacts cached: %d\n\n"
+ (hash-table-count mu4e--contacts-set)))
+ (maphash (lambda (contact _)
+ (insert (format "%s\n" contact))) mu4e--contacts-set))
+ (pop-to-buffer "*mu4e-contacts-info*")))
+
+(declare-function mu4e--server-contacts "mu4e-server")
+
+(defun mu4e--request-contacts-maybe ()
+ "Maybe update the set of contacts for autocompletion.
+
+If `mu4e-compose-complete-addresses' is non-nil, get/update the
+list of contacts we use for autocompletion; otherwise, do
+nothing."
+ (when mu4e-compose-complete-addresses
+ (mu4e--server-contacts
+ mu4e-compose-complete-only-personal
+ mu4e-compose-complete-only-after
+ mu4e-compose-complete-max
+ mu4e--contacts-tstamp)))
+
+(provide 'mu4e-contacts)
+;;; mu4e-contacts.el ends here
--- /dev/null
+;;; mu4e-context.el --- Switching between settings -*- lexical-binding: t -*-
+
+;; Copyright (C) 2015-2023 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; A mu4e 'context' is a set of variable-settings and functions, which can be
+;; used e.g. to switch between accounts.
+
+;;; Code:
+
+(require 'mu4e-helpers)
+(require 'mu4e-modeline)
+(require 'mu4e-query-items)
+
+\f
+;;; Configuration
+(defcustom mu4e-context-policy 'ask-if-none
+ "The policy to determine the context when entering the mu4e main view.
+
+If the value is `always-ask', ask the user unconditionally.
+
+In all other cases, if any context matches (using its match
+function), this context is used. Otherwise, if none of the
+contexts match, we have the following choices:
+
+- `pick-first': pick the first of the contexts available (ie. the default)
+- `ask': ask the user `ask-if-none': ask if there is no context yet,
+ otherwise leave it as it is
+- nil: return nil; eaves the current context as is.
+
+Also see `mu4e-compose-context-policy'."
+ :type '(choice
+ (const :tag "Always ask what context to use, even if one matches"
+ always-ask)
+ (const :tag "Ask if none of the contexts match" ask)
+ (const :tag "Ask when there's no context yet" ask-if-none)
+ (const :tag "Pick the first context if none match" pick-first)
+ (const :tag "Don't change the context when none match" nil))
+ :group 'mu4e)
+
+
+(defvar mu4e-contexts nil
+ "The list of `mu4e-context' objects describing mu4e's contexts.")
+
+(defvar mu4e-context-changed-hook nil
+ "Hook run just *after* the context changed.")
+
+(defface mu4e-context-face
+ '((t :inherit mu4e-title-face :weight bold))
+ "Face for displaying the context in the modeline."
+ :group 'mu4e-faces)
+
+(defvar mu4e--context-current nil
+ "The current context.
+Internal; use `mu4e-context-switch' to change it.")
+\f
+(defun mu4e-context-current (&optional output)
+ "Get the currently active context, or nil if there is none.
+When OUTPUT is non-nil, echo the name of the current context or
+none."
+ (interactive "p")
+ (let ((ctx mu4e--context-current))
+ (when output
+ (mu4e-message "Current context: %s"
+ (if ctx (mu4e-context-name ctx) "<none>")))
+ ctx))
+
+(cl-defstruct mu4e-context
+ "A mu4e context object with the following members:
+- `name': the name of the context, eg. \"Work\" or \"Private\".
+- `enter-func': a parameterless function invoked when entering
+ this context, or nil
+- `leave-func':a parameterless function invoked when leaving this
+ context, or nil
+- `match-func': a function called when composing a new message,
+ that takes a message plist for the message replied to or
+ forwarded, and nil otherwise. Before composing a new message,
+ `mu4e' switches to the first context for which `match-func'
+ returns t.
+- `vars': variables to set when entering context."
+ name ;; name of the context, e.g. "work"
+ (enter-func nil) ;; function invoked when entering the context
+ (leave-func nil) ;; function invoked when leaving the context
+ (match-func nil) ;; function that takes a msg-proplist, and return t
+ ;; if it matches, nil otherwise
+ vars) ;; alist of variables.
+
+
+(defun mu4e--context-ask-user (prompt)
+ "Let user choose some context based on its name with PROMPT."
+ (when mu4e-contexts
+ (let* ((names (seq-map (lambda (context)
+ (cons (mu4e-context-name context) context))
+ mu4e-contexts))
+ (context (mu4e-read-option prompt names)))
+ (or context (mu4e-error "No such context")))))
+
+(defun mu4e-context-switch (&optional force name)
+ "Switch to a context with NAME.
+Context must be part of `mu4e-contexts'; if NAME is nil, query user.
+
+If the new context is the same as the current context, only
+switch (run associated functions) when prefix argument FORCE is
+non-nil."
+ (interactive "P")
+ (unless mu4e-contexts
+ (mu4e-error "No contexts defined"))
+ (let* ((names (seq-map (lambda (context)
+ (cons (mu4e-context-name context) context))
+ mu4e-contexts))
+ (old-context mu4e--context-current) ; i.e., context before switch
+ (context
+ (if name
+ (cdr-safe (assoc name names))
+ (mu4e--context-ask-user "Switch to context: "))))
+ (unless context (mu4e-error "No such context"))
+ ;; if new context is same as old one, only switch with FORCE
+ (when (or force (not (eq context (mu4e-context-current))))
+ (when (and (mu4e-context-current)
+ (mu4e-context-leave-func mu4e--context-current))
+ (funcall (mu4e-context-leave-func mu4e--context-current)))
+ ;; enter the new context
+ (when (mu4e-context-enter-func context)
+ (funcall (mu4e-context-enter-func context)))
+ (when (mu4e-context-vars context)
+ (mapc (lambda (cell)
+ (set (car cell) (cdr cell)))
+ (mu4e-context-vars context)))
+ (setq mu4e--context-current context)
+ (run-hooks 'mu4e-context-changed-hook)
+ ;; refresh the cached query items if there was a context before; we have
+ ;; have different bookmarks/maildirs now.
+ (when old-context
+ (mu4e--query-items-refresh 'reset-baseline))
+ (mu4e-message "Switched context to %s"
+ (mu4e-context-name context)))
+ context))
+
+(defun mu4e--context-autoswitch (&optional msg policy)
+ "Automatically switch to some context.
+
+When contexts are defined but there is no context yet, switch to
+the first whose :match-func return non-nil. If none of them
+match, return the first. For MSG and POLICY, see
+`mu4e-context-determine'."
+ (when mu4e-contexts
+ (let ((context (mu4e-context-determine msg policy)))
+ (when context (mu4e-context-switch
+ nil (mu4e-context-name context))))))
+
+(defun mu4e-context-determine (msg &optional policy)
+ "Return the first context where match-func evaluate to non-nil.
+
+MSG points to the plist for the message replied to or forwarded,
+or nil if there is no such MSG; similar to what
+`mu4e-compose-pre-hook' does.
+
+POLICY specifies how to do the determination. If POLICY is
+`always-ask', we ask the user unconditionally.
+
+In all other cases, if any context matches (using its match
+function), this context is returned. If none of the contexts
+match, POLICY determines what to do:
+
+- `pick-first': pick the first of the contexts available
+- `ask': ask the user
+- `ask-if-none': ask if there is no context yet
+- otherwise, return nil. Effectively, this leaves the current context
+as it is."
+ (when mu4e-contexts
+ (if (eq policy 'always-ask)
+ (mu4e--context-ask-user "Select context: ")
+ (or ;; is there a matching one?
+ (seq-find (lambda (context)
+ (when (mu4e-context-match-func context)
+ (funcall (mu4e-context-match-func context) msg)))
+ mu4e-contexts)
+ ;; no context found yet; consult policy
+ (pcase policy
+ ('pick-first (car mu4e-contexts))
+ ('ask (mu4e--context-ask-user "Select context: "))
+ ('ask-if-none (or (mu4e-context-current)
+ (mu4e--context-ask-user "Select context: ")))
+ (_ nil))))))
+
+(defmacro with-mu4e-context-vars (context &rest body)
+ "Evaluate BODY, with variables let-bound for CONTEXT (if any).
+`funcall'."
+ (declare (indent 2))
+ `(let* ((vars (and ,context (mu4e-context-vars ,context))))
+ (cl-progv ;; XXX: perhaps use eval's lexical environment instead of progv?
+ (mapcar (lambda(cell) (car cell)) vars)
+ (mapcar (lambda(cell) (cdr cell)) vars)
+ (eval ,@body))))
+
+(defun mu4e--context-modeline-item ()
+ "Propertized string with the current context or nil."
+ (when-let* ((ctx (mu4e-context-current))
+ (name (and ctx (mu4e-context-name ctx))))
+ (concat
+ "<"
+ (propertize
+ name
+ 'face 'mu4e-context-face
+ 'help-echo (format "mu4e context: %s" name))
+ ">")))
+
+(define-minor-mode mu4e-context-minor-mode
+ "Mode for switching the mu4e context."
+ :global nil
+ :init-value nil ;; disabled by default
+ :group 'mu4e
+ :lighter ""
+ (mu4e--modeline-register #'mu4e--context-modeline-item))
+
+(defvar mu4e--context-menu-items
+ '("--"
+ ["Switch-context" mu4e-context-switch
+ :help "Switch the mu4e context"])
+ "Easy menu items for mu4e-context.")
+
+;;;
+(provide 'mu4e-context)
+;;; mu4e-context.el ends here
--- /dev/null
+;;; mu4e-contrib.el --- User-contributed functions -*- lexical-binding: t -*-
+
+;; Copyright (C) 2013-2023 Dirk-Jan C. Binnema
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Some user-contributed functions for mu4e
+
+;;; Code:
+
+(require 'mu4e-headers)
+(require 'mu4e-view)
+(require 'bookmark)
+(require 'eshell)
+
+\f
+;;; Various simple commands
+(defun mu4e-headers-mark-all-unread-read ()
+ "Put a ! \(read) mark on all visible unread messages."
+ (interactive)
+ (mu4e-headers-mark-for-each-if
+ (cons 'read nil)
+ (lambda (msg _param)
+ (memq 'unread (mu4e-msg-field msg :flags)))))
+
+(defun mu4e-headers-flag-all-read ()
+ "Flag all visible messages as \"read\"."
+ (interactive)
+ (mu4e-headers-mark-all-unread-read)
+ (mu4e-mark-execute-all t))
+
+(defun mu4e-headers-mark-all ()
+ "Mark all headers for some action.
+Ask user what action to execute."
+ (interactive)
+ (mu4e-headers-mark-for-each-if
+ (cons 'something nil)
+ (lambda (_msg _param) t))
+ (mu4e-mark-execute-all))
+
+
+\f
+;;; Bogofilter/SpamAssassin
+;;
+;; Support for handling spam with Bogofilter with the possibility
+;; to define it for SpamAssassin, contributed by Gour.
+;;
+;; To add the actions to the menu, you can use something like:
+;;
+;; (add-to-list 'mu4e-headers-actions
+;; '("sMark as spam" . mu4e-register-msg-as-spam) t)
+;; (add-to-list 'mu4e-headers-actions
+;; '("hMark as ham" . mu4e-register-msg-as-ham) t)
+
+(defvar mu4e-register-as-spam-cmd nil
+ "Command for invoking spam processor to register message as spam.
+For example for bogofilter, use \"/usr/bin/bogofilter -Ns < %s\"")
+
+(defvar mu4e-register-as-ham-cmd nil
+ "Command for invoking spam processor to register message as ham.
+For example for bogofile, use \"/usr/bin/bogofilter -Sn < %s\"")
+
+(defun mu4e-register-msg-as-spam (msg)
+ "Register MSG as spam."
+ (interactive)
+ (let* ((path (shell-quote-argument (mu4e-message-field msg :path)))
+ (command (format mu4e-register-as-spam-cmd path)))
+ (shell-command command))
+ (mu4e-mark-at-point 'delete nil))
+
+(defun mu4e-register-msg-as-ham (msg)
+ "Register MSG as ham."
+ (interactive)
+ (let* ((path (shell-quote-argument(mu4e-message-field msg :path)))
+ (command (format mu4e-register-as-ham-cmd path)))
+ (shell-command command))
+ (mu4e-mark-at-point 'something nil))
+
+;; (add-to-list 'mu4e-view-actions
+;; '("sMark as spam" . mu4e-view-register-msg-as-spam) t)
+;; (add-to-list 'mu4e-view-actions
+;; '("hMark as ham" . mu4e-view-register-msg-as-ham) t)
+
+(defun mu4e-view-register-msg-as-spam (msg)
+ "Register MSG as spam (view mode)."
+ (interactive)
+ (let* ((path (shell-quote-argument (mu4e-message-field msg :path)))
+ (command (format mu4e-register-as-spam-cmd path)))
+ (shell-command command))
+ (mu4e-view-mark-for-delete))
+
+(defun mu4e-view-register-msg-as-ham (msg)
+ "Mark MSG as ham (view mode)."
+ (interactive)
+ (let* ((path (shell-quote-argument(mu4e-message-field msg :path)))
+ (command (format mu4e-register-as-ham-cmd path)))
+ (shell-command command))
+ (mu4e-view-mark-for-something))
+
+\f
+;;; Eshell functions
+;;
+;; Code for `gnus-dired-attached' modified to run from eshell,
+;; allowing files to be attached to an email via mu4e using the
+;; eshell. Does not depend on gnus.
+
+
+(defun mu4e--active-composition-buffers ()
+ "Return all active mu4e composition buffers."
+ (let (buffers)
+ (save-excursion
+ (dolist (buffer (buffer-list t))
+ (set-buffer buffer)
+ (when (eq major-mode 'mu4e-compose-mode)
+ (push (buffer-name buffer) buffers))))
+ (nreverse buffers)))
+
+
+
+;; backward compat until 27.1 is univeral.
+(defalias 'mu4e--flatten-list
+ (if (fboundp 'flatten-list)
+ #'flatten-list
+ (with-no-warnings
+ #'eshell-flatten-list)))
+
+;; backward compat ntil 28.1 is universal.
+(defalias 'mu4e--mm-default-file-type
+ (if (fboundp 'mm-default-file-type)
+ #'mm-default-file-type
+ (with-no-warnings
+ #'mm-default-file-encoding)))
+
+(defun eshell/mu4e-attach (&rest args)
+ "Attach files to a mu4e message using eshell with ARGS.
+If no mu4e buffers found, compose a new message and then attach
+the file."
+ (let ((destination nil)
+ (files-str nil)
+ (bufs nil)
+ ;; Remove directories from the list
+ (files-to-attach
+ (delq nil (mapcar
+ (lambda (f) (if (or (not (file-exists-p f))
+ (file-directory-p f))
+ nil
+ (expand-file-name f)))
+ (mu4e--flatten-list (reverse args))))))
+ ;; warn if user tries to attach without any files marked
+ (if (null files-to-attach)
+ (error "No files to attach")
+ (setq files-str
+ (mapconcat
+ (lambda (f) (file-name-nondirectory f))
+ files-to-attach ", "))
+ (setq bufs (mu4e--active-composition-buffers))
+ ;; set up destination mail composition buffer
+ (if (and bufs
+ (y-or-n-p "Attach files to existing mail composition buffer? "))
+ (setq destination
+ (if (= (length bufs) 1)
+ (get-buffer (car bufs))
+ (let ((prompt (mu4e-format "%s" "Attach to buffer")))
+ (substring-no-properties
+ (funcall mu4e-completing-read-function prompt
+ bufs)))))
+ ;; setup a new mail composition buffer
+ (if (y-or-n-p "Compose new mail and attach this file? ")
+ (progn (mu4e-compose-new)
+ (setq destination (current-buffer)))))
+ ;; if buffer was found, set buffer to destination buffer, and attach files
+ (if (not (eq destination 'nil))
+ (progn (set-buffer destination)
+ (goto-char (point-max)) ; attach at end of buffer
+ (while files-to-attach
+ (mml-attach-file (car files-to-attach)
+ (or (mu4e--mm-default-file-type
+ (car files-to-attach))
+ "application/octet-stream") nil)
+ (setq files-to-attach (cdr files-to-attach)))
+ (message "Attached file(s) %s" files-str))
+ (message "No buffer to attach file to.")))))
+
+;;; _
+(provide 'mu4e-contrib)
+;;; mu4e-contrib.el ends here
--- /dev/null
+;;; mu4e-draft.el --- Helpers for m4e-compose -*- lexical-binding: t -*-
+
+;; Copyright (C) 2024 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Implements various helper functions for mu4e-compose. This all
+;; look a little convoluted since we need to subvert the gnus/message
+;; functions a bit to work with mu4e.
+
+(require 'message)
+(require 'mu4e-config)
+(require 'mu4e-helpers)
+(require 'mu4e-contacts)
+(require 'mu4e-folders)
+(require 'mu4e-message)
+(require 'mu4e-context)
+(require 'mu4e-window)
+
+;;; Code:
+
+(declare-function mu4e-compose-mode "mu4e-compose")
+(declare-function mu4e "mu4e")
+
+(defcustom mu4e-compose-crypto-policy
+ '(encrypt-encrypted-replies sign-encrypted-replies)
+ "Policy to control when messages will be signed/encrypted.
+
+The value is a list which influence the way draft messages are
+created. Specifically, it might contain:
+
+- `sign-all-messages': Always add a signature.
+- `sign-new-messages': Add a signature to new message, ie.
+ messages that aren't responses to another message.
+- `sign-forwarded-messages': Add a signature when forwarding
+ a message
+- `sign-edited-messages': Add a signature to drafts
+- `sign-all-replies': Add a signature when responding to
+ another message.
+- `sign-plain-replies': Add a signature when responding to
+ non-encrypted messages.
+- `sign-encrypted-replies': Add a signature when responding
+ to encrypted messages.
+
+It should be noted that certain symbols have priorities over one
+another. So `sign-all-messages' implies `sign-all-replies', which
+in turn implies `sign-plain-replies'. Adding both to the set, is
+not a contradiction, but a redundant configuration.
+
+All `sign-*' options have a `encrypt-*' analogue."
+ :type '(set :greedy t
+ (const :tag "Sign all messages" sign-all-messages)
+ (const :tag "Encrypt all messages" encrypt-all-messages)
+ (const :tag "Sign new messages" sign-new-messages)
+ (const :tag "Encrypt new messages" encrypt-new-messages)
+ (const :tag "Sign forwarded messages" sign-forwarded-messages)
+ (const :tag "Encrypt forwarded messages"
+ encrypt-forwarded-messages)
+ (const :tag "Sign edited messages" sign-edited-messages)
+ (const :tag "Encrypt edited messages" edited-forwarded-messages)
+ (const :tag "Sign all replies" sign-all-replies)
+ (const :tag "Encrypt all replies" encrypt-all-replies)
+ (const :tag "Sign replies to plain messages" sign-plain-replies)
+ (const :tag "Encrypt replies to plain messages"
+ encrypt-plain-replies)
+ (const :tag "Sign replies to encrypted messages"
+ sign-encrypted-replies)
+ (const :tag "Encrypt replies to encrypted messages"
+ encrypt-encrypted-replies))
+ :group 'mu4e-compose)
+
+;;; Crypto
+(defun mu4e--prepare-crypto (parent compose-type)
+ "Possibly encrypt or sign a message based on PARENT and COMPOSE-TYPE.
+See `mu4e-compose-crypto-policy' for more details."
+ (let* ((encrypted-p
+ (and parent (memq 'encrypted (mu4e-message-field parent :flags))))
+ (encrypt
+ (or (memq 'encrypt-all-messages mu4e-compose-crypto-policy)
+ (and (memq 'encrypt-new-messages mu4e-compose-crypto-policy)
+ (eq compose-type 'new)) ;; new messages
+ (and (eq compose-type 'forward) ;; forwarded
+ (memq 'encrypt-forwarded-messages mu4e-compose-crypto-policy))
+ (and (eq compose-type 'edit) ;; edit
+ (memq 'encrypt-edited-messages mu4e-compose-crypto-policy))
+ (and (eq compose-type 'reply) ;; all replies
+ (memq 'encrypt-all-replies mu4e-compose-crypto-policy))
+ (and (eq compose-type 'reply) (not encrypted-p) ;; plain replies
+ (memq 'encrypt-plain-replies mu4e-compose-crypto-policy))
+ (and (eq compose-type 'reply) encrypted-p
+ (memq 'encrypt-encrypted-replies
+ mu4e-compose-crypto-policy)))) ;; encrypted replies
+ (sign
+ (or (memq 'sign-all-messages mu4e-compose-crypto-policy)
+ (and (eq compose-type 'new) ;; new messages
+ (memq 'sign-new-messages mu4e-compose-crypto-policy))
+ (and (eq compose-type 'forward) ;; forwarded messages
+ (memq 'sign-forwarded-messages mu4e-compose-crypto-policy))
+ (and (eq compose-type 'edit) ;; edited messages
+ (memq 'sign-edited-messages mu4e-compose-crypto-policy))
+ (and (eq compose-type 'reply) ;; all replies
+ (memq 'sign-all-replies mu4e-compose-crypto-policy))
+ (and (eq compose-type 'reply) (not encrypted-p) ;; plain replies
+ (memq 'sign-plain-replies mu4e-compose-crypto-policy))
+ (and (eq compose-type 'reply) encrypted-p ;; encrypted replies
+ (memq 'sign-encrypted-replies mu4e-compose-crypto-policy)))))
+ (cond ((and sign encrypt) (mml-secure-message-sign-encrypt))
+ (sign (mml-secure-message-sign))
+ (encrypt (mml-secure-message-encrypt)))))
+
+(defcustom mu4e-sent-messages-behavior 'sent
+ "Determines what mu4e does with sent messages.
+
+This is one of the symbols:
+* `sent' move the sent message to the Sent-folder (`mu4e-sent-folder')
+* `trash' move the sent message to the Trash-folder (`mu4e-trash-folder')
+* `delete' delete the sent message.
+
+Note, when using GMail/IMAP, you should set this to either
+`trash' or `delete', since GMail already takes care of keeping
+copies in the sent folder.
+
+Alternatively, `mu4e-sent-messages-behavior' can be a function
+which takes no arguments, and which should return one of the mentioned
+symbols, for example:
+
+ (setq mu4e-sent-messages-behavior (lambda ()
+ (if (string= (message-sendmail-envelope-from) \"foo@example.com\")
+ \\='delete \\='sent)))
+
+The various `message-' functions from `message-mode' are available
+for querying the message information."
+ :type '(choice (const :tag "move message to mu4e-sent-folder" sent)
+ (const :tag "move message to mu4e-trash-folder" trash)
+ (const :tag "delete message" delete))
+ :group 'mu4e-compose)
+
+(defcustom mu4e-compose-context-policy 'ask
+ "Policy for determining the context when composing a new message.
+
+If the value is `always-ask', ask the user unconditionally.
+
+In all other cases, if any context matches (using its match
+function), this context is used. Otherwise, if none of the
+contexts match, we have the following choices:
+
+- `pick-first': pick the first of the contexts available (ie. the default)
+- `ask': ask the user
+- `ask-if-none': ask if there is no context yet, otherwise leave it as it is
+- nil: return nil; leaves the current context as is.
+
+Also see `mu4e-context-policy'."
+ :type '(choice
+ (const :tag "Always ask what context to use" always-ask)
+ (const :tag "Ask if none of the contexts match" ask)
+ (const :tag "Ask when there's no context yet" ask-if-none)
+ (const :tag "Pick the first context if none match" pick-first)
+ (const :tag "Don't change the context when none match" nil))
+ :safe 'symbolp
+ :group 'mu4e-compose)
+
+;;
+;; display the ready-to-go display buffer in the desired way.
+;;
+(defun mu4e--display-draft-buffer (cbuf)
+ "Display the message composition buffer CBUF.
+Display is influenced by `mu4e-compose-switch'."
+ (let ((func
+ (pcase mu4e-compose-switch
+ ('nil #'switch-to-buffer)
+ ('window #'switch-to-buffer-other-window)
+ ((or 'frame 't) #'switch-to-buffer-other-frame)
+ ('display-buffer #'display-buffer)
+ (_ (mu4e-error "Invalid mu4e-compose-switch")))))
+ (funcall func cbuf)))
+
+(defvar mu4e-user-agent-string
+ (format "mu4e %s; emacs %s" mu4e-mu-version emacs-version)
+ "The User-Agent string for mu4e, or nil.")
+
+;;; Runtime variables; useful for user-hooks etc.
+;; mu4e-compose-parent-message & mu4e-compose-type are buffer-local and
+;; permanent-local so they'll survive the mode change to mu4e-compose-mode and
+;; we can use them in the corresponding mode-hook.
+(defvar-local mu4e-compose-parent-message nil
+ "The parent message plist.
+This is the message being replied to, forwarded or edited; used
+in `mu4e-compose-pre-hook'. For new (non-reply, forward etc.)
+messages, it is nil.")
+(put 'mu4e-compose-parent-message 'permanent-local t)
+
+(defvar-local mu4e-compose-type nil
+ "The compose-type for the current message.")
+(put 'mu4e-compose-type 'permanent-local t)
+
+;;; Filenames
+(defun mu4e--draft-basename()
+ "Construct a randomized filename for a message with flags FLAGSTR.
+It looks something like
+ <time>-<random>.<hostname>
+
+This filename is used for the draft message and the sent message,
+depending on `mu4e-sent-messages-behavior'."
+ (let* ((sysname (if (fboundp 'system-name)
+ (system-name) (with-no-warnings system-name)))
+ (sysname (if (string= sysname "") "localhost" sysname))
+ (hostname (downcase
+ (save-match-data
+ (substring sysname
+ (string-match "^[^.]+" sysname)
+ (match-end 0))))))
+ (format "%s.%04x%04x%04x%04x.%s"
+ (format-time-string "%s" (current-time))
+ (random 65535) (random 65535) (random 65535) (random 65535)
+ hostname)))
+
+(defun mu4e--draft-message-path (base-name &optional parent)
+ "Construct a draft message path, based on PARENT if provided.
+
+PARENT is either nil or the original message (being replied
+ to/forwarded etc.), and is used to determine the draft folder.
+BASE-NAME is the base filename without any Maildir decoration."
+ (let ((draft-dir (mu4e-get-drafts-folder parent)))
+ (mu4e-join-paths
+ (mu4e-root-maildir) draft-dir "cur"
+ (format "%s%s2,DS" base-name mu4e-maildir-info-delimiter))))
+
+(defun mu4e--fcc-path (base-name &optional parent)
+ "Construct a Fcc: path, based on PARENT and `mu4e-sent-messages-behavior'.
+
+PARENT is either nil or the original message (being replied
+to/forwarded etc.), and is used to determine the sent folder,
+together with `mu4e-sent-messages-behavior'. BASE-NAME is the
+base filename without any Maildir decoration.
+
+Returns the path for the sent message, either in the sent or
+trash folder, or nil if the message should be removed after
+sending."
+ (let* ((behavior
+ (if (and (functionp mu4e-sent-messages-behavior)
+ ;; don't interpret 'delete as a function...
+ (not (eq mu4e-sent-messages-behavior 'delete)))
+ (funcall mu4e-sent-messages-behavior)
+ mu4e-sent-messages-behavior))
+ (sent-dir
+ (pcase behavior
+ ('delete nil)
+ ('trash (mu4e-get-trash-folder parent))
+ ('sent (mu4e-get-sent-folder parent))
+ (_ (mu4e-error "Error in `mu4e-sent-messages-behavior'")))))
+ (when sent-dir
+ (mu4e-join-paths
+ (mu4e-root-maildir) sent-dir "cur"
+ (format "%s%s2,S" base-name mu4e-maildir-info-delimiter)))))
+
+
+(defconst mu4e--header-separator
+ ;; XX properties don't show... why not?
+ (propertize "--text follows this line--" 'read-only t 'intangible t)
+ "Line used to separate headers from text in messages being composed.")
+
+(defun mu4e--delimit-headers (&optional undelimit)
+ "Delimit or undelimit (with UNDELIMIT) headers."
+ (let ((mail-header-separator (substring-no-properties mu4e--header-separator))
+ (inhibit-read-only t))
+ (save-excursion (mail-sendmail-undelimit-header)) ;; clear first
+ (unless undelimit (save-excursion (mail-sendmail-delimit-header)))))
+
+(defun mu4e--decoded-message (msg &optional headers-only)
+ "Get the message MSG, decoded as a string.
+With HEADERS-ONLY non-nil, only include the headers part."
+ (with-temp-buffer
+ (setq-local gnus-article-decode-hook
+ '(article-decode-charset
+ article-decode-encoded-words
+ article-decode-idna-rhs
+ article-treat-non-ascii
+ article-remove-cr
+ article-de-base64-unreadable
+ article-de-quoted-unreadable)
+ gnus-inhibit-mime-unbuttonizing nil
+ gnus-unbuttonized-mime-types '(".*/.*")
+ gnus-original-article-buffer (current-buffer))
+ (insert-file-contents-literally
+ (mu4e-message-readable-path msg) nil nil nil t)
+ ;; remove the body / attachments and what not.
+ (when headers-only
+ (rfc822-goto-eoh)
+ (delete-region (point) (point-max)))
+ ;; in rare (broken) case, if a message-id is missing use the generated one
+ ;; from mu.
+ (mu4e--delimit-headers)
+ (unless (message-field-value "Message-Id")
+ (goto-char (point-min))
+ (insert (format "Message-Id: <%s>\n" (plist-get msg :message-id))))
+ (mu4e--delimit-headers 'undelimit)
+ (ignore-errors (run-hooks 'gnus-article-decode-hook))
+ (buffer-substring-no-properties (point-min) (point-max))))
+
+(defvar mu4e--draft-buffer-max-name-length 48)
+(defun mu4e--draft-set-friendly-buffer-name ()
+ "Use some friendly name for this draft buffer."
+ (let* ((subj (message-field-value "subject"))
+ (subj (if (or (not subj) (string-match "^[:blank:]*$" subj))
+ "No subject" subj)))
+ (rename-buffer (generate-new-buffer-name
+ (format "\"%s\""
+ (truncate-string-to-width subj
+ mu4e--draft-buffer-max-name-length
+ 0 nil t)))
+ (buffer-name))))
+
+;; hook impls
+
+(defun mu4e--fcc-handler (msgpath)
+ "Handle Fcc: for MSGPATH.
+This ensures that a copy of a sent messages ends up in the
+appropriate sent-messages folder. If MSGPATH is nil, do nothing."
+ (when msgpath
+ (let* ((target-dir (file-name-directory msgpath))
+ (target-mdir (file-name-directory target-dir)))
+ ;; create maildir if needed
+ (unless (file-exists-p target-mdir)
+ (make-directory
+ (mu4e-join-paths target-mdir "cur" 'parents))
+ (make-directory
+ (mu4e-join-paths target-mdir "new" 'parents)))
+ (write-file msgpath)
+ (mu4e--server-add msgpath))))
+\f
+;; save / send hooks
+
+(defvar-local mu4e--compose-undo nil
+ "Remember the undo-state.")
+
+(defun mu4e--compose-before-save ()
+ "Function called just before the draft buffer is saved."
+ ;; This does 3 things:
+ ;; - set the Message-Id if not already
+ ;; - set the Date if not already
+ ;; - (temporarily) remove the mail-header separator
+ (setq mu4e--compose-undo buffer-undo-list)
+ (save-excursion
+ (unless (message-field-value "Message-ID")
+ (message-generate-headers '(Message-ID)))
+ ;; older Emacsen (<= 28 perhaps?) won't update the Date
+ ;; if there already is one; so make sure it's gone.
+ (message-remove-header "Date")
+ (message-generate-headers '(Date Subject From))
+ (mu4e--delimit-headers 'undelimit))) ;; remove separator
+
+(defun mu4e--set-parent-flags (path)
+ "Set flags for replied-to and forwarded for the message at PATH.
+That is, set the `replied' \"R\" flag on messages we replied to,
+and the `passed' \"F\" flag on message we have forwarded.
+
+If a message has an \"In-Reply-To\" header, it is considered a
+reply to the message with the corresponding message id.
+Otherwise, if it does not have an \"In-Reply-To\" header, but
+does have a \"References:\" header, it is considered to be a
+forward message for the message corresponding with the /last/
+message-id in the references header.
+
+If the message has been determined to be either a forwarded
+message or a reply, we instruct the server to update that message
+with resp. the \"P\" (passed) flag for a forwarded message, or
+the \"R\" flag for a replied message. The original messages are
+also marked as Seen.
+
+Function assumes that it is executed in the context of the
+message buffer."
+ (when-let ((buf (find-file-noselect path)))
+ (with-current-buffer buf
+ (let ((in-reply-to (message-field-value "in-reply-to"))
+ (forwarded-from)
+ (references (message-field-value "references")))
+ (unless in-reply-to
+ (when references
+ (with-temp-buffer ;; inspired by `message-shorten-references'.
+ (insert references)
+ (goto-char (point-min))
+ (let ((refs))
+ (while (re-search-forward "<[^ <]+@[^ <]+>" nil t)
+ (push (match-string 0) refs))
+ ;; the last will be the first
+ (setq forwarded-from (car refs))))))
+ ;; remove the <> and update the flags on the server-side.
+ (when (and in-reply-to (string-match "<\\(.*\\)>" in-reply-to))
+ (mu4e--server-move (match-string 1 in-reply-to) nil "+R-N"))
+ (when (and forwarded-from (string-match "<\\(.*\\)>" forwarded-from))
+ (mu4e--server-move (match-string 1 forwarded-from) nil "+P-N"))))))
+
+(defun mu4e--compose-after-save()
+ "Function called immediately after the draft buffer is saved."
+ ;; This does 3 things:
+ ;; - restore the mail-header-separator (see mu4e--compose-before-save)
+ ;; - update the buffer name (based on the message subject
+ ;; - tell the mu server about the updated draft message
+ (mu4e--delimit-headers)
+ (mu4e--draft-set-friendly-buffer-name)
+ ;; tell the server
+ (mu4e--server-add (buffer-file-name))
+ ;; restore history.
+ (set-buffer-modified-p nil)
+ (setq buffer-undo-list mu4e--compose-undo))
+
+(defun mu4e-sent-handler (docid path)
+ "Handler called with DOCID and PATH for the just-sent message.
+For Forwarded ('Passed') and Replied messages, try to set the
+appropriate flag at the message forwarded or replied-to."
+ ;; XXX we don't need this function anymore here, but we have an external
+ ;; caller in mu4e-icalendar... we should update that.
+ (mu4e--set-parent-flags path)
+ ;; if the draft file exists, remove it now.
+ (when (file-exists-p path)
+ (mu4e--server-remove docid)))
+
+(defun mu4e--send-harden-newlines ()
+ "Set the hard property to all newlines."
+ (save-excursion
+ (goto-char (point-min))
+ (while (search-forward "\n" nil t)
+ (put-text-property (1- (point)) (point) 'hard t))))
+
+(defun mu4e--compose-before-send ()
+ "Function called just before sending a message."
+ ;; Remove References: if In-Reply-To: is missing.
+ ;; This allows the user to effectively start a new message-thread by
+ ;; removing the In-Reply-To header.
+ (when (eq mu4e-compose-type 'reply)
+ (unless (message-field-value "In-Reply-To")
+ (message-remove-header "References")))
+ (when use-hard-newlines
+ (mu4e--send-harden-newlines))
+ ;; now handle what happens _after_ sending; typically, draft is gone and
+ ;; the sent message appears in sent. Update flags for related messages,
+ ;; i.e. for Forwarded ('Passed') and Replied messages, try to set the
+ ;; appropriate flag at the message forwarded or replied-to.
+ (add-hook 'message-sent-hook
+ (lambda ()
+ (when-let ((fcc-path (message-field-value "Fcc")))
+ (mu4e--set-parent-flags fcc-path)
+ ;; we end up with a ((buried) buffer here, visiting the
+ ;; fcc-path; not quite sure why. But let's get rid of it (#2681)
+ (when-let ((buf (find-buffer-visiting fcc-path)))
+ (kill-buffer buf))
+ ;; remove draft
+ (when-let ((draft (buffer-file-name)))
+ (mu4e--server-remove draft))))
+ nil t))
+
+;; overrides for message-* functions
+;;
+;; mostly some magic because the message-reply/-forward/... functions want to
+;; create and switch to buffer by themselves; but mu4e wants to control
+;; when/where the buffers are shown so we subvert the message-functions and get
+;; the buffer without display it.
+
+(defvar mu4e--message-buf nil
+ "The message buffer created by (overridden) message-* functions.")
+
+(defun mu4e--message-pop-to-buffer (name &optional _switch)
+ "Mu4e override for `message-pop-to-buffer'.
+Creates a buffer NAME and returns it."
+ (set-buffer (get-buffer-create name))
+ (erase-buffer)
+ (setq mu4e--message-buf (current-buffer)))
+
+(defun mu4e--message-is-yours-p ()
+ "Mu4e's override for `message-is-yours-p'."
+ (seq-some (lambda (field)
+ (if-let ((recip (message-field-value field)))
+ (mu4e-personal-or-alternative-address-p
+ (car (mail-header-parse-address recip)))))
+ '("From" "Sender")))
+
+(defmacro mu4e--validate-hidden-buffer (&rest body)
+ "Macro to evaluate BODY and asserts that it yields a valid buffer.
+Where valid means that it is a live an non-active buffer.
+Returns said buffer."
+ `(let ((buf (progn ,@body)))
+ (cl-assert (buffer-live-p buf))
+ (cl-assert (not (eq buf (window-buffer (selected-window)))))
+ buf))
+
+(defun mu4e--message-call (func &rest params)
+ "Call message/gnus functions from a mu4e-context.
+E.g., functions such as `message-reply' or `message-forward', but
+manipulate such that they do *not* switch to the created buffer,
+but merely return it.
+
+FUNC is the function to call and PARAMS are its parameters.
+
+For replying/forwarding, this functions expects to be called
+while in a buffer with the to-be-forwarded/replied-to message."
+ (let* ((message-this-is-mail t)
+ (message-generate-headers-first nil)
+ (message-newsreader mu4e-user-agent-string)
+ (message-mail-user-agent nil))
+ (cl-letf
+ ;; `message-pop-to-buffer' attempts switching the visible buffer;
+ ;; instead, we manipulate it to _return_ the buffer.
+ (((symbol-function #'message-pop-to-buffer)
+ #'mu4e--message-pop-to-buffer)
+ ;; teach `message-is-yours-p' about how mu4e defines that
+ ((symbol-function #'message-is-yours-p)
+ #'mu4e--message-is-yours-p))
+ ;; also turn off all the gnus crypto handling, we do that ourselves..
+ (setq-local gnus-message-replysign nil
+ gnus-message-replyencrypt nil
+ gnus-message-replysignencrypted nil)
+ (setq mu4e--message-buf nil)
+ (apply func params))
+ (mu4e--validate-hidden-buffer mu4e--message-buf)))
+;;
+;; make the draft buffer ready for use.
+;;
+
+(defun mu4e--jump-to-a-reasonable-place ()
+ "Jump to a reasonable place for writing an email."
+ (if (not (message-field-value "To"))
+ (message-goto-to)
+ (if (not (message-field-value "Subject"))
+ (message-goto-subject)
+ (pcase message-cite-reply-position
+ ((or 'above 'traditional) (message-goto-body))
+ (_ (when (message-goto-signature) (forward-line -2)))))))
+
+(defvar mu4e-draft-hidden-headers
+ (append message-hidden-headers '("^User-agent:" "^Fcc:"))
+ "Message headers to hide when composing.
+This is mu4e's version of `message-hidden-headers'.")
+
+(defun mu4e--prepare-draft (&optional parent)
+ "Get ready for message composition.
+PARENT is the parent message, if any."
+ (unless (mu4e-running-p) (mu4e 'background)) ;; start if needed
+ (mu4e--context-autoswitch parent mu4e-compose-context-policy))
+
+(defun mu4e--prepare-draft-headers (compose-type)
+ "Add extra headers for message based on COMPOSE-TYPE."
+ (message-generate-headers
+ (seq-filter #'identity ;; ensure needed headers are generated.
+ `(From Subject Date Message-ID
+ ,(when (memq compose-type '(reply forward)) 'References)
+ ,(when (eq compose-type 'reply) 'In-Reply-To)
+ ,(when message-newsreader 'User-Agent)
+ ,(when message-user-organization 'Organization)))))
+
+(defun mu4e--prepare-draft-buffer (compose-type parent)
+ "Prepare the current buffer as a draft-buffer.
+COMPOSE-TYPE and PARENT are as in `mu4e--draft'."
+ (cl-assert (member compose-type '(reply forward edit new)))
+ (cl-assert (eq (if parent t nil)
+ (if (member compose-type '(reply forward)) t nil)))
+ ;; remember some variables, e.g for user hooks. These are permanent-local
+ ;; hence survive the mode-switch below (we do this so these useful vars are
+ ;; available in mode-hooks.
+ (setq-local
+ mu4e-compose-parent-message parent
+ mu4e-compose-type compose-type)
+
+ ;; draft path
+ (unless (eq compose-type 'edit)
+ (set-visited-file-name ;; make it a draft file
+ (mu4e--draft-message-path (mu4e--draft-basename) parent)))
+ ;; fcc
+ (when-let ((fcc-path (mu4e--fcc-path (mu4e--draft-basename) parent)))
+ (message-add-header (concat "Fcc: " fcc-path "\n")))
+
+ (mu4e--prepare-draft-headers compose-type)
+ (mu4e--prepare-crypto parent compose-type)
+ ;; set the attachment dir to something more reasonable than the draft
+ ;; directory.
+ (setq default-directory (mu4e-determine-attachment-dir))
+ (mu4e--draft-set-friendly-buffer-name)
+
+ ;; now, switch to compose mode
+ (mu4e-compose-mode)
+
+ ;; hide some internal headers
+ (let ((message-hidden-headers mu4e-draft-hidden-headers))
+ (message-hide-headers))
+
+ ;; hooks
+ (add-hook 'before-save-hook #'mu4e--compose-before-save nil t)
+ (add-hook 'after-save-hook #'mu4e--compose-after-save nil t)
+ (add-hook 'message-send-hook #'mu4e--compose-before-send nil t)
+ (setq-local message-fcc-handler-function #'mu4e--fcc-handler)
+
+ (mu4e--jump-to-a-reasonable-place)
+
+ (set-buffer-modified-p nil)
+ (undo-boundary))
+
+;;
+;; mu4e-compose-pos-hook helpers
+
+(defvar mu4e--before-draft-window-config nil
+ "The window configuration just before creating the draft.")
+
+(defun mu4e-compose-post-restore-window-configuration()
+ "Function that perhaps restores the window configuration.
+I.e. the configuration just before the draft buffer appeared.
+This is for use in `mu4e-compose-post-hook'.
+See `set-window-configuration' for further details."
+ (when mu4e--before-draft-window-config
+ ;;(message "RESTORE to %s" mu4e--before-draft-window-config)
+ (set-window-configuration mu4e--before-draft-window-config)
+ (setq mu4e--before-draft-window-config nil)))
+
+(defvar mu4e--draft-activation-frame nil
+ "Frame from which composition was activated.
+Used internally for mu4e-compose-post-kill-frame.")
+
+(defun mu4e-compose-post-kill-frame ()
+ "Function that perhaps kills the composition frame.
+This is for use in `mu4e-compose-post-hook'."
+ (let ((msgframe (selected-frame)))
+ ;;(message "kill frame? %s %s" mu4e--draft-activation-frame msgframe)
+ (when (and (frame-live-p msgframe)
+ (not (eq mu4e--draft-activation-frame msgframe)))
+ (delete-frame msgframe))))
+
+(defvar mu4e-message-post-action nil
+ "Runtime variable for use with `mu4e-compose-post-hook'.
+It contains a symbol denoting the action that triggered the hook,
+either `send', `exit', `kill' or `postpone'.")
+
+(defvar mu4e-compose-post-hook)
+(defun mu4e--message-post-actions (trigger)
+ "Invoked after we're done with a message.
+
+I.e. this multiplexes the `message-(send|exit|kill|postpone)-actions';
+with the mu4e-message-post-action set accordingly."
+ (setq mu4e-message-post-action trigger)
+ (run-hooks 'mu4e-compose-post-hook))
+
+(defun mu4e--prepare-post (&optional oldframe oldwindconf)
+ "Prepare the `mu4e-compose-post-hook` handling.
+
+Set up some message actions. In particular, handle closing frames
+when we created it. OLDFRAME is the frame from which the
+message-composition was triggered. OLDWINDCONF is the current
+window configuration."
+ ;; remember current frame & window conf
+ (setq mu4e--draft-activation-frame oldframe
+ mu4e--before-draft-window-config oldwindconf)
+
+ ;; make message's "post" hooks local, and multiplex them
+ (make-local-variable 'message-send-actions)
+ (make-local-variable 'message-postpone-actions)
+ (make-local-variable 'message-exit-actions)
+ (make-local-variable 'message-kill-actions)
+
+ (push (lambda () (mu4e--message-post-actions 'send))
+ message-send-actions)
+ (push (lambda () (mu4e--message-post-actions 'postpone))
+ message-postpone-actions)
+ (push (lambda () (mu4e--message-post-actions 'exit))
+ message-exit-actions)
+ (push (lambda () (mu4e--message-post-actions 'kill))
+ message-kill-actions))
+
+;;
+;; creating drafts
+;;
+
+(defun mu4e--draft (compose-type compose-func &optional parent)
+ "Create a new message draft.
+
+This is the central access point for creating new mail buffers;
+when there's a parent message, use `mu4e--compose-with-parent'.
+
+COMPOSE-TYPE is the type of message to create. COMPOSE-FUNC is a
+function that must return a buffer that satisfies
+`mu4e--validate-hidden-buffer'.
+
+Optionally, PARENT is the message parent or nil. For compose-type
+`reply' and `forward' we require a PARENT; for the other compose
+it must be nil.
+
+After this, user is presented with a message composition buffer.
+
+Returns the new buffer."
+ (mu4e--prepare-draft parent)
+ ;; evaluate BODY; this must yield a hidden, live buffer. This is evaluated in
+ ;; a temp buffer with contains the parent-message, if any. if there's a
+ ;; PARENT, load the corresponding message into a temp-buffer before calling
+ ;; compose-func
+ (let ((draft-buffer)
+ (oldframe (selected-frame))
+ (oldwinconf (current-window-configuration)))
+ (with-temp-buffer
+ ;; provide a temp buffer so the compose-func can do its thing
+ (setq draft-buffer (mu4e--validate-hidden-buffer (funcall compose-func)))
+ (with-current-buffer draft-buffer
+ ;; we have our basic buffer; turn it into a full mu4e composition
+ ;; buffer.
+ (mu4e--prepare-draft-buffer compose-type parent)))
+ ;; we're ready for composition; let's display it in the way user configured
+ ;; things: directly through display buffer (via pop-t or otherwise through
+ ;; mu4e-window.
+ (if (eq mu4e-compose-switch 'display-buffer)
+ (pop-to-buffer draft-buffer)
+ (mu4e-display-buffer draft-buffer 'do-select))
+ ;; prepare possible message actions (such as cleaning-up)
+ (mu4e--prepare-post oldframe oldwinconf)
+ draft-buffer))
+
+(defun mu4e--draft-with-parent (compose-type parent compose-func)
+ "Draft a message based on some parent message.
+COMPOSE-TYPE, COMPOSE-FUNC and PARENT are as in `mu4e--draft',
+but note the different order."
+ (mu4e--draft
+ compose-type
+ (lambda ()
+ (let ( ;; only needed for Fwd. Gnus has a bad default.
+ (message-make-forward-subject-function
+ (list #'message-forward-subject-fwd)))
+ (insert (mu4e--decoded-message parent))
+ ;; let's make sure we don't use message-reply-headers from
+ ;; some unrelated message.
+ (setq message-reply-headers nil)
+ (funcall compose-func)))
+ parent))
+
+(provide 'mu4e-draft)
--- /dev/null
+;;; mu4e-folders.el --- Dealing with maildirs & folders -*- lexical-binding: t -*-
+
+;; Copyright (C) 2021-2023 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Dealing with maildirs & folders
+
+;;; Code:
+(require 'mu4e-helpers)
+(require 'mu4e-context)
+(require 'mu4e-server)
+\f
+;;; Customization
+(defgroup mu4e-folders nil
+ "Special folders."
+ :group 'mu4e)
+
+(defcustom mu4e-drafts-folder "/drafts"
+ "Folder for draft messages, relative to the root maildir.
+For instance, \"/drafts\". Instead of a string, may also be a
+function that takes a message (a msg plist, see
+`mu4e-message-field'), and returns a folder. Note, the message
+parameter refers to the original message being replied to / being
+forwarded / re-edited and is nil otherwise. `mu4e-drafts-folder'
+is only evaluated once."
+ :type '(choice
+ (string :tag "Folder name")
+ (function :tag "Function return folder name"))
+ :group 'mu4e-folders)
+
+(defcustom mu4e-refile-folder "/archive"
+ "Folder for refiling messages, relative to the root maildir.
+For instance \"/Archive\". Instead of a string, may also be a
+function that takes a message (a msg plist, see
+`mu4e-message-field'), and returns a folder. Note that the
+message parameter refers to the message-at-point."
+ :type '(choice
+ (string :tag "Folder name")
+ (function :tag "Function return folder name"))
+ :group 'mu4e-folders)
+
+(defcustom mu4e-sent-folder "/sent"
+ "Folder for sent messages, relative to the root maildir.
+For instance, \"/Sent Items\". Instead of a string, may also be a
+function that takes a message (a msg plist, see
+`mu4e-message-field'), and returns a folder. Note that the
+message parameter refers to the original message being replied to
+/ being forwarded / re-edited, and is nil otherwise."
+ :type '(choice
+ (string :tag "Folder name")
+ (function :tag "Function return folder name"))
+ :group 'mu4e-folders)
+
+(defcustom mu4e-trash-folder "/trash"
+ "Folder for trashed messages, relative to the root maildir.
+For instance, \"/trash\". Instead of a string, may also be a
+function that takes a message (a msg plist, see
+`mu4e-message-field'), and returns a folder. When using
+`mu4e-trash-folder' in the headers view (when marking messages
+for trash). Note that the message parameter refers to the
+message-at-point. When using it when composing a message (see
+`mu4e-sent-messages-behavior'), this refers to the original
+message being replied to / being forwarded / re-edited, and is
+nil otherwise."
+ :type '(choice
+ (string :tag "Folder name")
+ (function :tag "Function return folder name"))
+ :group 'mu4e-folders)
+
+(defcustom mu4e-maildir-shortcuts nil
+ "A list of maildir shortcuts.
+This makes it possible to quickly go to a particular
+maildir (folder), or quickly moving messages to them (e.g., for
+archiving or refiling).
+
+Each of the list elements is a plist with at least:
+`:maildir' - the maildir for the shortcut (e.g. \"/archive\")
+`:key' - the shortcut key.
+
+Optionally, you can add the following:
+`:name' - name of the maildir to be displayed in main-view.
+`:hide' - if t, the shortcut is hidden from the main-view and
+speedbar.
+`:hide-unread' - do not show the counts of unread/total number
+ of matches for the maildir in the main-view, and is implied
+from `:hide'.
+
+For backward compatibility, an older form is recognized as well:
+
+ (maildir . key), where MAILDIR is a maildir (such as
+\"/archive/\"), and key is a single character.
+
+You can use these shortcuts in the headers and view buffers, for
+example with `mu4e-mark-for-move-quick' (or \"m\", by default) or
+`mu4e-jump-to-maildir' (or \"j\", by default), followed by the
+designated shortcut character for the maildir.
+
+Unlike in search queries, folder names with spaces in them must
+NOT be quoted, since mu4e does this for you."
+ :type '(choice
+ (alist :key-type (string :tag "Maildir")
+ :value-type character
+ :tag "Alist (old format)")
+ (repeat (plist
+ :key-type (choice (const :tag "Maildir" :maildir)
+ (const :tag "Shortcut" :key)
+ (const :tag "Name of maildir" :name)
+ (const :tag "Hide from main view" :hide)
+ (const :tag "Do not count" :hide-unread))
+ :tag "Plist (new format)")))
+ :version "1.3.9"
+ :group 'mu4e-folders)
+
+(defcustom mu4e-maildir-initial-input "/"
+ "Initial input for `mu4e-completing-completing-read' function."
+ :type 'string
+ :group 'mu4e-folders)
+
+(defcustom mu4e-maildir-info-delimiter
+ (if (member system-type '(ms-dos windows-nt cygwin))
+ ";" ":")
+ "Separator character between message identifier and flags.
+It defaults to ':' on most platforms, except on Windows, where it
+is not allowed and we use ';' for compatibility with mbsync,
+offlineimap and other programs."
+ :type 'string
+ :group 'mu4e-folders)
+
+(defcustom mu4e-attachment-dir (expand-file-name "~/")
+ "Default directory for attaching and saving attachments.
+
+This can be either a string (a file system path), or a function
+that takes a filename and the mime-type as arguments, and returns
+the attachment dir. See Info node `(mu4e) Attachments' for
+details.
+
+When this called for composing a message, both filename and
+mime-type are nil."
+ :type 'directory
+ :group 'mu4e-folders
+ :safe 'stringp)
+
+(defvar mu4e-maildir-list nil
+ "Cached list of maildirs.")
+
+\f
+(defun mu4e-maildir-shortcuts ()
+ "Get `mu4e-maildir-shortcuts' in the (new) format.
+Converts from the old format if needed."
+ (seq-map (lambda (item) ;; convert from old format?
+ (if (and (consp item) (not (consp (cdr item))))
+ `(:maildir ,(car item) :key ,(cdr item))
+ item))
+ mu4e-maildir-shortcuts))
+
+;; the standard folders can be functions too
+(defun mu4e--get-folder (foldervar msg)
+ "Within the mu-context of MSG, get message folder FOLDERVAR.
+If FOLDER is a string, return it, if it is a function, evaluate
+this function with MSG as parameter which may be nil, and return
+the result."
+ (unless (member foldervar
+ '(mu4e-sent-folder mu4e-drafts-folder
+ mu4e-trash-folder mu4e-refile-folder))
+ (mu4e-error "Folder must be one of mu4e-(sent|drafts|trash|refile)-folder"))
+ ;; get the value with the vars for the relevants context let-bound
+ (with-mu4e-context-vars (mu4e-context-determine msg nil)
+ (let* ((folder (symbol-value foldervar))
+ (val
+ (cond
+ ((stringp folder) folder)
+ ((functionp folder) (funcall folder msg))
+ (t (mu4e-error "Unsupported type for %S" folder)))))
+ (or val (mu4e-error "%S evaluates to nil" foldervar)))))
+
+(defun mu4e-get-drafts-folder (&optional msg)
+ "Get the drafts folder, optionally based on MSG.
+See `mu4e-drafts-folder'." (mu4e--get-folder 'mu4e-drafts-folder msg))
+
+(defun mu4e-get-refile-folder (&optional msg)
+ "Get the folder for refiling, optionally based on MSG.
+See `mu4e-refile-folder'." (mu4e--get-folder 'mu4e-refile-folder msg))
+
+(defun mu4e-get-sent-folder (&optional msg)
+ "Get the sent folder, optionally based on MSG.
+See `mu4e-sent-folder'." (mu4e--get-folder 'mu4e-sent-folder msg))
+
+(defun mu4e-get-trash-folder (&optional msg)
+ "Get the trash folder, optionally based on MSG.
+See `mu4e-trash-folder'." (mu4e--get-folder 'mu4e-trash-folder msg))
+
+;;; Maildirs
+(defun mu4e--guess-maildir (path)
+ "Guess the maildir for PATH, or nil if cannot find it."
+ (let ((idx (string-match (mu4e-root-maildir) path)))
+ (when (and idx (zerop idx))
+ (replace-regexp-in-string
+ (mu4e-root-maildir)
+ ""
+ (expand-file-name
+ (mu4e-join-paths path ".." ".."))))))
+
+(defun mu4e-create-maildir-maybe (dir)
+ "Offer to create maildir DIR if it does not exist yet.
+Return t if it already exists or (after asking) an attempt has been
+to create it; otherwise return nil."
+ (let ((seems-to-exist (file-directory-p dir)))
+ (when (or seems-to-exist
+ (yes-or-no-p (mu4e-format "%s does not exist yet. Create now?" dir)))
+ ;; even when the maildir already seems to exist, call mkdir for a deeper
+ ;; check. However only get an update when the maildir is totally new.
+ (mu4e--server-mkdir dir (not seems-to-exist))
+ t)))
+
+(defun mu4e-get-maildirs ()
+ "Get maildirs under `mu4e-maildir'."
+ mu4e-maildir-list)
+
+(defun mu4e-ask-maildir (prompt)
+ "Ask the user for a maildir (using PROMPT).
+
+If the special shortcut \"o\" (for _o_ther) is used, or
+if (mu4e-maildir-shortcuts) evaluates to nil, let user choose
+from all maildirs under `mu4e-maildir'."
+ (let* ((options
+ (seq-map (lambda (md)
+ (cons
+ (format "%c%s" (plist-get md :key)
+ (or (plist-get md :name)
+ (plist-get md :maildir)))
+ (plist-get md :maildir)))
+ (mu4e-filter-single-key (mu4e-maildir-shortcuts))))
+ (response
+ (if (not options)
+ 'other
+ (mu4e-read-option prompt
+ (append options
+ '(("oOther..." . other)))))))
+ (substring-no-properties
+ (if (eq response 'other)
+ (progn
+ (funcall mu4e-completing-read-function prompt
+ (mu4e-get-maildirs) nil nil
+ mu4e-maildir-initial-input))
+ response))))
+
+(defun mu4e-ask-maildir-check-exists (prompt)
+ "Like `mu4e-ask-maildir', PROMPT for existence of the maildir.
+Offer to create it if it does not exist yet."
+ (let* ((mdir (mu4e-ask-maildir prompt))
+ (fullpath (mu4e-join-paths (mu4e-root-maildir) mdir)))
+ (unless (file-directory-p fullpath)
+ (and (yes-or-no-p
+ (mu4e-format "%s does not exist. Create now?" fullpath))
+ (mu4e--server-mkdir fullpath)))
+ mdir))
+
+;; mu4e-attachment-dir is either a string or a function that takes a
+;; filename and the mime-type as argument, either (or both) which can
+;; be nil
+
+(defun mu4e-determine-attachment-dir (&optional fname mimetype)
+ "Get the target-directory for attachments.
+
+This is based on the variable `mu4e-attachment-dir', which is either:
+- if is a string, used it as-is
+- a function taking two string parameters, both of which can be nil:
+ (1) a filename or a URL
+ (2) a mime-type (such as \"text/plain\"."
+ (let ((dir
+ (cond
+ ((stringp mu4e-attachment-dir)
+ mu4e-attachment-dir)
+ ((functionp mu4e-attachment-dir)
+ (funcall mu4e-attachment-dir fname mimetype))
+ (t
+ (mu4e-error "Unsupported type for mu4e-attachment-dir" )))))
+ (if dir
+ (expand-file-name dir)
+ (mu4e-error "Mu4e-attachment-dir evaluates to nil"))))
+
+(provide 'mu4e-folders)
+;;; mu4e-folders.el ends here
--- /dev/null
+;;; mu4e-headers.el --- Message headers -*- lexical-binding: t; coding:utf-8 -*-
+
+;; Copyright (C) 2011-2023 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; In this file are function related mu4e-headers-mode, to creating the list of
+;; one-line descriptions of emails, aka 'headers' (not to be confused with
+;; headers like 'To:' or 'Subject:')
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'fringe)
+(require 'hl-line)
+(require 'mailcap)
+(require 'mule-util) ;; seems _some_ people need this for
+ ;; truncate-string-ellipsis
+
+(require 'mu4e-update)
+
+ ;; utility functions
+(require 'mu4e-server)
+(require 'mu4e-vars)
+(require 'mu4e-mark)
+(require 'mu4e-context)
+(require 'mu4e-contacts)
+(require 'mu4e-search)
+(require 'mu4e-compose)
+(require 'mu4e-actions)
+(require 'mu4e-message)
+(require 'mu4e-lists)
+(require 'mu4e-update)
+(require 'mu4e-folders)
+(require 'mu4e-thread)
+
+(declare-function mu4e-view "mu4e-view")
+(declare-function mu4e--main-view "mu4e-main")
+
+\f
+;;; Configuration
+
+(defgroup mu4e-headers nil
+ "Settings for the headers view."
+ :group 'mu4e)
+
+(defcustom mu4e-headers-fields
+ '( (:human-date . 12)
+ (:flags . 6)
+ (:mailing-list . 10)
+ (:from . 22)
+ (:subject . nil))
+ "A list of header fields to show in the headers buffer.
+Each element has the form (HEADER . WIDTH), where HEADER is one of
+the available headers (see `mu4e-header-info') and WIDTH is the
+respective width in characters.
+
+A width of nil means \"unrestricted\", and this is best reserved
+for the rightmost (last) field. Note that emacs may become very
+slow with excessively long lines (1000s of characters), so if you
+regularly get such messages, you want to avoid fields with nil
+altogether."
+ :type `(repeat (cons (choice
+ ,@(mapcar (lambda (h)
+ (list 'const :tag
+ (plist-get (cdr h) :help)
+ (car h)))
+ mu4e-header-info)
+ (restricted-sexp
+ :tag "User-specified header"
+ :match-alternatives (mu4e--headers-header-p)))
+ (choice (integer :tag "width")
+ (const :tag "unrestricted width" nil))))
+ :group 'mu4e-headers)
+
+(defun mu4e--headers-header-p (symbol)
+ "Is symbol a valid mu4e header?
+This means its either one of the build-in or user-specified headers."
+ (assoc symbol (append mu4e-header-info mu4e-header-info-custom)))
+
+(defcustom mu4e-headers-date-format "%x"
+ "Date format to use in the headers view.
+In the format of `format-time-string'."
+ :type 'string
+ :group 'mu4e-headers)
+
+(defcustom mu4e-headers-time-format "%X"
+ "Time format to use in the headers view.
+In the format of `format-time-string'."
+ :type 'string
+ :group 'mu4e-headers)
+
+(defcustom mu4e-headers-long-date-format "%c"
+ "Date format to use in the headers view tooltip.
+In the format of `format-time-string'."
+ :type 'string
+ :group 'mu4e-headers)
+
+(defcustom mu4e-headers-precise-alignment nil
+ "When set, use precise (but relatively slow) alignment for columns.
+By default, do it in a slightly inaccurate but faster way. To get
+an idea about the difference, In some tests, the rendering time
+was around 5.8 ms per messages for precise alignment, versus 3.3
+for non-precise aligment (for 445 messages)."
+ :type 'boolean
+ :group 'mu4e-headers)
+
+(defcustom mu4e-headers-auto-update t
+ "Whether to automatically update the current headers buffer if an
+indexing operation showed changes."
+ :type 'boolean
+ :group 'mu4e-headers)
+
+(defcustom mu4e-headers-advance-after-mark t
+ "With this option set to non-nil, automatically advance to the
+next mail after marking a message in header view."
+ :type 'boolean
+ :group 'mu4e-headers)
+
+
+(defcustom mu4e-headers-visible-flags
+ '(draft flagged new passed replied trashed attach encrypted signed
+ list personal)
+ "An ordered list of flags to show in the headers buffer.
+Each element is a symbol in the list.
+
+By default, we leave out `unread' and `seen', since those are
+mostly covered by `new', and the display gets cluttered otherwise."
+ :type '(set
+ (const :tag "Draft" draft)
+ (const :tag "Flagged" flagged)
+ (const :tag "New" new)
+ (const :tag "Passed" passed)
+ (const :tag "Replied" replied)
+ (const :tag "Seen" seen)
+ (const :tag "Trashed" trashed)
+ (const :tag "Attach" attach)
+ (const :tag "Encrypted" encrypted)
+ (const :tag "Signed" signed)
+ (const :tag "List" list)
+ (const :tag "Personal" personal)
+ (const :tag "Calendar" calendar))
+ :group 'mu4e-headers)
+
+(defcustom mu4e-headers-found-hook nil
+ "Hook run just *after* all of the headers for the last search
+query have been received and are displayed."
+ :type 'hook
+ :group 'mu4e-headers)
+
+;;; Public variables
+(defcustom mu4e-headers-from-or-to-prefix '("" . "To ")
+ "Prefix for the :from-or-to field when it is showing,
+ respectively, From: or To:. It is a cons cell with the car
+ element being the From: prefix, the cdr element the To: prefix."
+ :type '(cons string string)
+ :group 'mu4e-headers)
+
+;;;; Fancy marks
+
+;; marks for headers of the form; each is a cons-cell (basic . fancy)
+;; each of which is basic ascii char and something fancy, respectively
+;; by default, we some conservative marks, even when 'fancy'
+;; so they're less likely to break if people don't have certain fonts.
+;; However, if you want to be really 'fancy', you could use something like
+;; the following; esp. with a newer Emacs with color-icon support.
+;; (setq
+;; mu4e-headers-draft-mark '("D" . "💈")
+;; mu4e-headers-flagged-mark '("F" . "📍")
+;; mu4e-headers-new-mark '("N" . "🔥")
+;; mu4e-headers-passed-mark '("P" . "❯")
+;; mu4e-headers-replied-mark '("R" . "❮")
+;; mu4e-headers-seen-mark '("S" . "☑")
+;; mu4e-headers-trashed-mark '("T" . "💀")
+;; mu4e-headers-attach-mark '("a" . "📎")
+;; mu4e-headers-encrypted-mark '("x" . "🔒")
+;; mu4e-headers-signed-mark '("s" . "🔑")
+;; mu4e-headers-unread-mark '("u" . "⎕")
+;; mu4e-headers-list-mark '("l" . "🔈")
+;; mu4e-headers-personal-mark '("p" . "👨")
+;; mu4e-headers-calendar-mark '("c" . "📅"))
+
+
+(defvar mu4e-headers-draft-mark '("D" . "⚒") "Draft.")
+(defvar mu4e-headers-flagged-mark '("F" . "✚") "Flagged.")
+(defvar mu4e-headers-new-mark '("N" . "✱") "New.")
+(defvar mu4e-headers-passed-mark '("P" . "❯") "Passed (fwd).")
+(defvar mu4e-headers-replied-mark '("R" . "❮") "Replied.")
+(defvar mu4e-headers-seen-mark '("S" . "✔") "Seen.")
+(defvar mu4e-headers-trashed-mark '("T" . "⏚") "Trashed.")
+(defvar mu4e-headers-attach-mark '("a" . "⚓") "W/ attachments.")
+(defvar mu4e-headers-encrypted-mark '("x" . "⚴") "Encrypted.")
+(defvar mu4e-headers-signed-mark '("s" . "☡") "Signed.")
+(defvar mu4e-headers-unread-mark '("u" . "⎕") "Unread.")
+(defvar mu4e-headers-list-mark '("l" . "Ⓛ") "Mailing list.")
+(defvar mu4e-headers-personal-mark '("p" . "Ⓟ") "Personal.")
+(defvar mu4e-headers-calendar-mark '("c" . "Ⓒ") "Calendar invitation.")
+
+
+;;;; Graph drawing
+
+(defvar mu4e-headers-thread-mark-as-orphan 'first
+ "Define which messages should be prefixed with the orphan mark.
+`all' marks all the messages without a parent as orphan, `first'
+only marks the first message in the thread.")
+
+(defvar mu4e-headers-thread-root-prefix '("* " . "□ ")
+ "Prefix for root messages.")
+(defvar mu4e-headers-thread-child-prefix '("|>" . "│ ")
+ "Prefix for messages in sub threads that do have a following sibling.")
+(defvar mu4e-headers-thread-first-child-prefix '("o " . "⚬ ")
+ "Prefix for the first child messages in a sub thread.")
+(defvar mu4e-headers-thread-last-child-prefix '("L" . "└ ")
+ "Prefix for messages in sub threads that do not have a following sibling.")
+(defvar mu4e-headers-thread-connection-prefix '("|" . "│ ")
+ "Prefix to connect sibling messages that do not follow each other.
+Must have the same length as `mu4e-headers-thread-blank-prefix'.")
+(defvar mu4e-headers-thread-blank-prefix '(" " . " ")
+ "Prefix to separate non connected messages.
+Must have the same length as `mu4e-headers-thread-connection-prefix'.")
+(defvar mu4e-headers-thread-orphan-prefix '("<>" . "♢ ")
+ "Prefix for orphan messages with siblings.")
+(defvar mu4e-headers-thread-single-orphan-prefix '("<>" . "♢ ")
+ "Prefix for orphan messages with no siblings.")
+(defvar mu4e-headers-thread-duplicate-prefix '("=" . "≡ ")
+ "Prefix for duplicate messages.")
+
+;;;; Various
+
+(defcustom mu4e-headers-actions
+ '( ("capture message" . mu4e-action-capture-message)
+ ("browse online archive" . mu4e-action-browse-list-archive)
+ ("show this thread" . mu4e-action-show-thread))
+ "List of actions to perform on messages in the headers list.
+The actions are cons-cells of the form (NAME . FUNC) where:
+* NAME is the name of the action (e.g. \"Count lines\")
+* FUNC is a function which receives a message plist as an argument.
+
+The first character of NAME is used as the shortcut."
+ :group 'mu4e-headers
+ :type '(alist :key-type string :value-type function))
+
+(defvar mu4e-headers-custom-markers
+ '(("Older than"
+ (lambda (msg date) (time-less-p (mu4e-msg-field msg :date) date))
+ (lambda () (mu4e-get-time-date "Match messages before: ")))
+ ("Newer than"
+ (lambda (msg date) (time-less-p date (mu4e-msg-field msg :date)))
+ (lambda () (mu4e-get-time-date "Match messages after: ")))
+ ("Bigger than"
+ (lambda (msg bytes) (> (mu4e-msg-field msg :size) (* 1024 bytes)))
+ (lambda () (read-number "Match messages bigger than (Kbytes): "))))
+ "List of custom markers -- functions to mark message that match
+some custom function. Each of the list members has the following
+format:
+ (NAME PREDICATE-FUNC PARAM-FUNC)
+* NAME is the name of the predicate function, and the first
+character is the shortcut (so keep those unique).
+* PREDICATE-FUNC is a function that takes two parameters, MSG
+and (optionally) PARAM, and should return non-nil when there's a
+match.
+* PARAM-FUNC is function that is evaluated once, and its value is
+then passed to PREDICATE-FUNC as PARAM. This is useful for
+getting user-input.")
+;;; Internal variables/constants
+
+;; docid cookies
+(defconst mu4e~headers-docid-pre "\376"
+ "Each header starts (invisibly) with the `mu4e~headers-docid-pre',
+followed by the docid, followed by `mu4e~headers-docid-post'.")
+(defconst mu4e~headers-docid-post "\377"
+ "Each header starts (invisibly) with the `mu4e~headers-docid-pre',
+followed by the docid, followed by `mu4e~headers-docid-post'.")
+
+
+(defvar mu4e~headers-search-start nil)
+(defvar mu4e~headers-render-start nil)
+(defvar mu4e~headers-render-time nil)
+
+(defvar mu4e-headers-report-render-time nil
+ "If non-nil, report on the time it took to render the messages.
+This is mostly useful for profiling.")
+
+(defvar mu4e~headers-hidden 0
+ "Number of headers hidden due to `mu4e-headers-hide-predicate'.")
+
+\f
+;;; Clear
+
+(defun mu4e~headers-clear (&optional text)
+ "Clear the headers buffer and related data structures.
+Optionally, show TEXT."
+ (when (buffer-live-p (mu4e-get-headers-buffer))
+ (setq mu4e~headers-render-start (float-time)
+ mu4e~headers-hidden 0)
+ (let ((inhibit-read-only t))
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (mu4e--mark-clear)
+ (remove-overlays)
+ (erase-buffer)
+ (when text
+ (goto-char (point-min))
+ (insert (propertize text 'face 'mu4e-system-face 'intangible t)))))))
+\f
+;;; Misc
+
+(defun mu4e~headers-contact-str (contacts)
+ "Turn the list of contacts CONTACTS (with elements (NAME . EMAIL)
+into a string."
+ (mapconcat
+ (lambda (contact)
+ (let ((name (mu4e-contact-name contact))
+ (email (mu4e-contact-email contact)))
+ (or name email "?"))) contacts ", "))
+
+(defun mu4e~headers-thread-prefix-map (type)
+ "Return the thread prefix based on the symbol TYPE."
+ (let ((get-prefix
+ (lambda (cell)
+ (if mu4e-use-fancy-chars (cdr cell) (car cell)))))
+ (propertize
+ (cl-case type
+ (child (funcall get-prefix mu4e-headers-thread-child-prefix))
+ (first-child (funcall get-prefix mu4e-headers-thread-first-child-prefix))
+ (last-child (funcall get-prefix mu4e-headers-thread-last-child-prefix))
+ (connection (funcall get-prefix mu4e-headers-thread-connection-prefix))
+ (blank (funcall get-prefix mu4e-headers-thread-blank-prefix))
+ (orphan (funcall get-prefix mu4e-headers-thread-orphan-prefix))
+ (single-orphan (funcall get-prefix
+ mu4e-headers-thread-single-orphan-prefix))
+ (duplicate (funcall get-prefix mu4e-headers-thread-duplicate-prefix))
+ (t "?"))
+ 'face 'mu4e-thread-fold-face)))
+
+
+;; headers in the buffer are prefixed by an invisible string with the docid
+;; followed by an EOT ('end-of-transmission', \004, ^D) non-printable ascii
+;; character. this string also has a text-property with the docid. the former
+;; is used for quickly finding a certain header, the latter for retrieving the
+;; docid at point without string matching etc.
+
+(defun mu4e~headers-docid-pos (docid)
+ "Return the pos of the beginning of the line with the header with
+docid DOCID, or nil if it cannot be found."
+ (let ((pos))
+ (save-excursion
+ (setq pos (mu4e~headers-goto-docid docid)))
+ pos))
+
+(defun mu4e~headers-docid-cookie (docid)
+ "Create an invisible string containing DOCID; this is to be used
+at the beginning of lines to identify headers."
+ (propertize (format "%s%d%s"
+ mu4e~headers-docid-pre docid mu4e~headers-docid-post)
+ 'docid docid 'invisible t));;
+
+(defun mu4e~headers-docid-at-point (&optional point)
+ "Get the docid for the header at POINT, or at current (point) if
+nil. Returns the docid, or nil if there is none."
+ (save-excursion
+ (when point
+ (goto-char point))
+ (get-text-property (line-beginning-position) 'docid)))
+
+
+
+(defun mu4e~headers-goto-docid (docid &optional to-mark)
+ "Go to the beginning of the line with the header with docid
+DOCID, or nil if it cannot be found. If the optional TO-MARK is
+non-nil, go to the point directly *after* the docid-cookie instead
+of the beginning of the line."
+ (let ((oldpoint (point)) (newpoint))
+ (goto-char (point-min))
+ (setq newpoint
+ (search-forward (mu4e~headers-docid-cookie docid) nil t))
+ (unless to-mark
+ (if (null newpoint)
+ (goto-char oldpoint) ;; not found; restore old pos
+ (progn
+ (beginning-of-line) ;; found, move to beginning of line
+ (setq newpoint (point)))))
+ newpoint)) ;; return the point, or nil if not found
+
+(defun mu4e~headers-field-for-docid (docid field)
+ "Get FIELD (a symbol, see `mu4e-headers-names') for the message
+with DOCID which must be present in the headers buffer."
+ (save-excursion
+ (when (mu4e~headers-goto-docid docid)
+ (mu4e-message-field (mu4e-message-at-point) field))))
+
+
+;; In order to print a thread tree with all the message connections,
+;; it's necessary to keep track of all sub levels that still have
+;; following messages. For each level, mu4e~headers-thread-state keeps
+;; the value t for a connection or nil otherwise.
+(defvar-local mu4e~headers-thread-state '())
+
+(defun mu4e~headers-thread-prefix (thread)
+ "Calculate the thread prefix based on thread info THREAD."
+ (when thread
+ (let* ((prefix "")
+ (level (plist-get thread :level))
+ (has-child (plist-get thread :has-child))
+ (first-child (plist-get thread :first-child))
+ (last-child (plist-get thread :last-child))
+ (orphan (plist-get thread :orphan))
+ (single-orphan(and orphan first-child last-child))
+ (duplicate (plist-get thread :duplicate)))
+ ;; Do not prefix root messages.
+ (if (= level 0)
+ (setq mu4e~headers-thread-state '()))
+ (if (> level 0)
+ (let* ((length (length mu4e~headers-thread-state))
+ (padding (make-list (max 0 (- level length)) nil)))
+ ;; Trim and pad the state to ensure a message will
+ ;; always be shown with the correct indentation, even if
+ ;; a broken thread is returned. It's trimmed to level-1
+ ;; because the current level has always an connection
+ ;; and it used a special formatting.
+ (setq mu4e~headers-thread-state
+ (cl-subseq (append mu4e~headers-thread-state padding)
+ 0 (- level 1)))
+ ;; Prepare the thread prefix.
+ (setq prefix
+ (concat
+ ;; Current mu4e~headers-thread-state, composed by
+ ;; connections or blanks.
+ (mapconcat
+ (lambda (s)
+ (mu4e~headers-thread-prefix-map
+ (if s 'connection 'blank)))
+ mu4e~headers-thread-state "")
+ ;; Current entry.
+ (mu4e~headers-thread-prefix-map
+ (if single-orphan 'single-orphan
+ (if (and orphan
+ (or first-child
+ (not (eq mu4e-headers-thread-mark-as-orphan
+ 'first))))
+ 'orphan
+ (if last-child 'last-child
+ (if first-child 'first-child
+ 'child)))))))))
+ ;; If a new sub-thread will follow (has-child) and the current
+ ;; one is still not done (not last-child), then a new
+ ;; connection needs to be added to the tree-state. It's not
+ ;; necessary to a blank (nil), because padding will handle
+ ;; that.
+ (if (and has-child (not last-child))
+ (setq mu4e~headers-thread-state
+ (append mu4e~headers-thread-state '(t))))
+ ;; Return the thread prefix.
+ (format "%s%s"
+ prefix
+ (if duplicate
+ (mu4e~headers-thread-prefix-map 'duplicate) "")))))
+
+(defun mu4e~headers-flags-str (flags)
+ "Get a display string for FLAGS.
+Note that `mu4e-flags-to-string' is for internal use only; this
+function is for display. (This difference is significant, since
+internally, the Maildir spec determines what the flags look like,
+while our display may be different)."
+ (or (mapconcat
+ (lambda (flag)
+ (when (member flag mu4e-headers-visible-flags)
+ (if-let* ((mark (intern-soft
+ (format "mu4e-headers-%s-mark" (symbol-name flag))))
+ (cell (symbol-value mark)))
+ (if mu4e-use-fancy-chars (cdr cell) (car cell))
+ "")))
+ flags "")
+ ""))
+
+;;; Special headers
+
+(defun mu4e~headers-from-or-to (msg)
+ "Get the From: address from MSG if not one of user's; otherwise get To:.
+When the from address for message MSG is one of the the user's
+addresses, (as per `mu4e-personal-address-p'), show the To
+address. Otherwise, show the From address, prefixed with the
+appropriate `mu4e-headers-from-or-to-prefix'."
+ (let* ((from1 (car-safe (mu4e-message-field msg :from)))
+ (from1-addr (and from1 (mu4e-contact-email from1)))
+ (is-user (and from1-addr (mu4e-personal-address-p from1-addr))))
+ (if is-user
+ (concat (cdr mu4e-headers-from-or-to-prefix)
+ (mu4e~headers-contact-str (mu4e-message-field msg :to)))
+ (concat (car mu4e-headers-from-or-to-prefix)
+ (mu4e~headers-contact-str (mu4e-message-field msg :from))))))
+
+(defun mu4e~headers-human-date (msg)
+ "Show a \"human\" date for MSG.
+If the date is today, show the time, otherwise, show the date.
+The formats used for date and time are `mu4e-headers-date-format'
+and `mu4e-headers-time-format'."
+ (let ((date (mu4e-msg-field msg :date)))
+ (if (equal date '(0 0 0))
+ "None"
+ (let ((day1 (decode-time date))
+ (day2 (decode-time (current-time))))
+ (if (and
+ (eq (nth 3 day1) (nth 3 day2)) ;; day
+ (eq (nth 4 day1) (nth 4 day2)) ;; month
+ (eq (nth 5 day1) (nth 5 day2))) ;; year
+ (format-time-string mu4e-headers-time-format date)
+ (format-time-string mu4e-headers-date-format date))))))
+
+(defun mu4e~headers-thread-subject (msg)
+ "Get the subject for MSG if it is the first one in a thread.
+Otherwise, return the thread-prefix without the subject-text. In
+other words, show the subject of a thread only once, similar to
+e.g. \"mutt\"."
+ (let* ((tinfo (mu4e-message-field msg :meta))
+ (subj (mu4e-msg-field msg :subject)))
+ (concat ;; prefix subject with a thread indicator
+ (mu4e~headers-thread-prefix tinfo)
+ (if (plist-get tinfo :thread-subject)
+ (truncate-string-to-width subj 600) ""))))
+
+(defun mu4e~headers-mailing-list (list)
+ "Get some identifier for the mailing list."
+ (if list
+ (propertize (mu4e-get-mailing-list-shortname list) 'help-echo list)
+ ""))
+
+(defsubst mu4e~headers-custom-field-value (msg field)
+ "Show some custom header field, or raise an error if it is not
+found."
+ (let* ((item (or (assoc field mu4e-header-info-custom)
+ (mu4e-error "field %S not found" field)))
+ (func (or (plist-get (cdr-safe item) :function)
+ (mu4e-error "no :function defined for field %S %S"
+ field (cdr item)))))
+ (funcall func msg)))
+
+(defun mu4e~headers-field-value (msg field)
+ (let ((val (mu4e-message-field msg field)))
+ (cl-case field
+ (:subject
+ (concat ;; prefix subject with a thread indicator
+ (mu4e~headers-thread-prefix (mu4e-message-field msg :meta))
+ ;; "["(plist-get (mu4e-message-field msg :meta) :path) "] "
+ ;; work-around: emacs' display gets really slow when lines are too long;
+ ;; so limit subject length to 600
+ (truncate-string-to-width val 600)))
+ (:thread-subject ;; if not searching threads, fall back to :subject
+ (if mu4e-search-threads
+ (mu4e~headers-thread-subject msg)
+ (mu4e~headers-field-value msg :subject)))
+ ((:maildir :path :message-id) val)
+ ((:to :from :cc :bcc) (mu4e~headers-contact-str val))
+ ;; if we (ie. `user-mail-address' is the 'From', show
+ ;; 'To', otherwise show From
+ (:from-or-to (mu4e~headers-from-or-to msg))
+ (:date (format-time-string mu4e-headers-date-format val))
+ (:list (or val ""))
+ (:mailing-list (mu4e~headers-mailing-list (mu4e-msg-field msg :list)))
+ (:human-date (propertize (mu4e~headers-human-date msg)
+ 'help-echo (format-time-string
+ mu4e-headers-long-date-format
+ (mu4e-msg-field msg :date))))
+ (:flags (propertize (mu4e~headers-flags-str val)
+ 'help-echo (format "%S" val)))
+ (:tags (propertize (mapconcat 'identity val ", ")))
+ (:size (mu4e-display-size val))
+ (t (mu4e~headers-custom-field-value msg field)))))
+
+(defsubst mu4e~headers-truncate-field-fast (val width)
+ "Truncate VAL to WIDTH. Fast and somewhat inaccurate."
+ (if width
+ (truncate-string-to-width val width 0 ?\s truncate-string-ellipsis)
+ val))
+
+(defun mu4e~headers-truncate-field-precise (field val width)
+ "Return VAL truncated to one less than WIDTH, with a trailing
+space propertized with a `display' text property which expands to
+ the correct column for display."
+ (when width
+ (let ((end-col (cl-loop for (f . w) in mu4e-headers-fields
+ sum w
+ until (equal f field))))
+ (setq val (string-trim-right val))
+ (if (> width (length val))
+ (setq val (concat val " "))
+ (setq val
+ (concat
+ (truncate-string-to-width val (1- width) 0 ?\s t)
+ " ")))
+ (put-text-property (1- (length val))
+ (length val)
+ 'display
+ `(space . (:align-to ,end-col))
+ val)))
+ val)
+
+(defsubst mu4e~headers-truncate-field (field val width)
+ "Truncate VAL to WIDTH."
+ (if mu4e-headers-precise-alignment
+ (mu4e~headers-truncate-field-precise field val width)
+ (mu4e~headers-truncate-field-fast val width)))
+
+(defsubst mu4e~headers-field-handler (f-w msg)
+ "Create a description of the field of MSG described by F-W."
+ (let* ((field (car f-w))
+ (width (cdr f-w))
+ (val (mu4e~headers-field-value msg field))
+ (val (and val
+ (if width
+ (mu4e~headers-truncate-field field val width)
+ val))))
+ val))
+
+(defsubst mu4e~headers-apply-flags (msg fieldval)
+ "Adjust FIELDVAL's face property based on flags in MSG."
+ (let* ((flags (plist-get msg :flags))
+ (meta (plist-get msg :meta))
+ (face (cond
+ ((memq 'trashed flags) 'mu4e-trashed-face)
+ ((memq 'draft flags) 'mu4e-draft-face)
+ ((or (memq 'unread flags) (memq 'new flags))
+ 'mu4e-unread-face)
+ ((memq 'flagged flags) 'mu4e-flagged-face)
+ ((plist-get meta :related) 'mu4e-related-face)
+ ((memq 'replied flags) 'mu4e-replied-face)
+ ((memq 'passed flags) 'mu4e-forwarded-face)
+ (t 'mu4e-header-face))))
+ (add-face-text-property 0 (length fieldval) face t fieldval)
+ fieldval))
+
+(defsubst mu4e~message-header-line (msg)
+ "Return a propertized description of message MSG suitable for
+displaying in the header view."
+ (if (and mu4e-search-hide-enabled mu4e-search-hide-predicate
+ (funcall mu4e-search-hide-predicate msg))
+ (progn
+ (cl-incf mu4e~headers-hidden)
+ nil)
+ (progn
+ (mu4e~headers-apply-flags
+ msg
+ (mapconcat (lambda (f-w) (mu4e~headers-field-handler f-w msg))
+ mu4e-headers-fields " ")))))
+
+(defsubst mu4e~headers-insert-header (msg pos)
+ "Insert a header for MSG at point POS."
+ (when-let ((line (mu4e~message-header-line msg))
+ (docid (plist-get msg :docid)))
+ (goto-char pos)
+ (insert
+ (propertize
+ (concat
+ (mu4e~headers-docid-cookie docid)
+ mu4e--mark-fringe line "\n")
+ 'docid docid 'msg msg))))
+
+(defun mu4e~headers-remove-header (docid &optional ignore-missing)
+ "Remove header with DOCID at point.
+When IGNORE-MISSING is non-nill, don't raise an error when the
+docid is not found."
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (if (mu4e~headers-goto-docid docid)
+ (let ((inhibit-read-only t))
+ (delete-region (line-beginning-position) (line-beginning-position 2)))
+ (unless ignore-missing
+ (mu4e-error "Cannot find message with docid %S" docid)))))
+
+\f
+;;; Handler functions
+
+;; next are a bunch of handler functions; those will be called from mu4e~proc in
+;; response to output from the server process
+
+(defun mu4e~headers-view-handler (msg)
+ "Handler function for displaying a message."
+ (mu4e-view msg))
+
+(defun mu4e~headers-view-this-message-p (docid)
+ "Is DOCID currently being viewed?"
+ (mu4e-get-view-buffers
+ (lambda (_) (eq docid (plist-get mu4e--view-message :docid)))))
+
+;; note: this function is very performance-sensitive
+(defun mu4e~headers-append-handler (msglst)
+ "Append one-line descriptions of messages in MSGLIST.
+Do this at the end of the headers-buffer."
+ (when (buffer-live-p (mu4e-get-headers-buffer))
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (save-excursion
+ (let ((inhibit-read-only t))
+ (seq-do
+ (lambda (msg)
+ (mu4e~headers-insert-header msg (point-max)))
+ msglst))))))
+
+
+(defun mu4e~headers-update-handler (msg is-move maybe-view)
+ "Update handler, will be called when a message has been updated
+in the database. This function will update the current list of
+headers."
+ (when (buffer-live-p (mu4e-get-headers-buffer))
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (let* ((docid (mu4e-message-field msg :docid))
+ (initial-message-at-point (mu4e~headers-docid-at-point))
+ (initial-column (current-column))
+ (inhibit-read-only t)
+ (point (mu4e~headers-docid-pos docid))
+ (markinfo (gethash docid mu4e--mark-map)))
+ (when point ;; is the message present in this list?
+
+ ;; if it's marked, unmark it now
+ (when (mu4e-mark-docid-marked-p docid)
+ (mu4e-mark-set 'unmark))
+
+ ;; re-use the thread info from the old one; this is needed because
+ ;; *update* messages don't have thread info by themselves (unlike
+ ;; search results)
+ ;; since we still have the search results, re-use
+ ;; those
+ (plist-put msg :meta
+ (mu4e~headers-field-for-docid docid :meta))
+
+ ;; first, remove the old one (otherwise, we'd have two headers with
+ ;; the same docid...
+ (mu4e~headers-remove-header docid t)
+
+ ;; if we're actually viewing this message (in mu4e-view mode), we
+ ;; update it; that way, the flags can be updated, as well as the path
+ ;; (which is useful for viewing the raw message)
+ (when (and maybe-view (mu4e~headers-view-this-message-p docid))
+ (save-excursion (mu4e-view msg)))
+ ;; now, if this update was about *moving* a message, we don't show it
+ ;; anymore (of course, we cannot be sure if the message really no
+ ;; longer matches the query, but this seem a good heuristic. if it
+ ;; was only a flag-change, show the message with its updated flags.
+ (unless is-move
+ (save-excursion
+ (mu4e~headers-insert-header msg point)))
+
+ ;; restore the mark, if any. See #2076.
+ (when (and markinfo (mu4e~headers-goto-docid docid))
+ (mu4e-mark-at-point (car markinfo) (cdr markinfo)))
+
+ (if (and initial-message-at-point
+ (mu4e~headers-goto-docid initial-message-at-point))
+ (progn
+ (move-to-column initial-column)
+ (mu4e~headers-highlight initial-message-at-point))
+ ;; attempt to highlight the corresponding line and make it visible
+ (mu4e~headers-highlight docid))
+ (run-hooks 'mu4e-message-changed-hook))))))
+
+(defun mu4e~headers-remove-handler (docid)
+ "Remove handler, will be called when a message with DOCID has
+been removed from the database. This function will hide the removed
+message from the current list of headers. If the message is not
+present, don't do anything."
+ (when (buffer-live-p (mu4e-get-headers-buffer))
+ (mu4e~headers-remove-header docid t))
+ ;; if we were viewing this message, close it now.
+ (when (and (mu4e~headers-view-this-message-p docid)
+ (buffer-live-p (mu4e-get-view-buffer)))
+ (let ((buf (mu4e-get-view-buffer)))
+ (mapc #'delete-window (get-buffer-window-list
+ buf nil t))
+ (kill-buffer buf))))
+
+
+\f
+;;; Performing queries (internal)
+(defconst mu4e~search-message "Searching...")
+(defconst mu4e~no-matches "No matching messages found")
+(defconst mu4e~end-of-results "End of search results")
+
+(defvar mu4e--search-background nil
+ "Is this a background search?
+ If so, do not attempt to switch buffers. This variable is to be let-bound
+to t before \"automatic\" searches.")
+
+(defun mu4e--search-execute (expr ignore-history)
+ "Search for query EXPR.
+
+Switch to the output buffer for the results. If IGNORE-HISTORY is
+true, do *not* update the query history stack."
+ (let* ((buf (mu4e-get-headers-buffer nil t))
+ (view-window mu4e~headers-view-win)
+ (inhibit-read-only t)
+ (rewritten-expr (funcall mu4e-query-rewrite-function expr))
+ (maxnum (unless mu4e-search-full mu4e-search-results-limit)))
+ (with-current-buffer buf
+ ;; NOTE: this resets all buffer-local variables, including
+ ;; `mu4e~headers-view-win', which may have a live window if the
+ ;; headers buffer already exists when `mu4e-get-headers-buffer'
+ ;; is called.
+ (mu4e-headers-mode)
+ (setq mu4e~headers-view-win view-window)
+ (unless ignore-history
+ ;; save the old present query to the history list
+ (when mu4e--search-last-query
+ (mu4e--search-push-query mu4e--search-last-query 'past)))
+ (setq mu4e--search-last-query rewritten-expr)
+ (setq list-buffers-directory rewritten-expr)
+ (mu4e--modeline-update))
+
+ ;; when the buffer is already visible, select it; otherwise,
+ ;; switch to it.
+ (unless (get-buffer-window buf (if mu4e--search-background 0 nil))
+ (mu4e-display-buffer buf t))
+ (run-hook-with-args 'mu4e-search-hook expr)
+ (mu4e~headers-clear mu4e~search-message)
+ (setq mu4e~headers-search-start (float-time))
+ (mu4e--server-find
+ rewritten-expr
+ mu4e-search-threads
+ mu4e-search-sort-field
+ mu4e-search-sort-direction
+ maxnum
+ mu4e-search-skip-duplicates
+ mu4e-search-include-related)))
+
+(defun mu4e~headers-benchmark-message (count)
+ "Get some report message for messaging search and rendering speed."
+ (if (and mu4e-headers-report-render-time
+ mu4e~headers-search-start
+ mu4e~headers-render-start
+ (> count 0))
+ (let ((render-time-ms (* 1000(- (float-time) mu4e~headers-render-start)))
+ (search-time-ms (* 1000(- (float-time) mu4e~headers-search-start))))
+ (format (concat
+ "; search: %0.1f ms (%0.2f ms/msg)"
+ "; render: %0.1f ms (%0.2f ms/msg)")
+ search-time-ms (/ search-time-ms count)
+ render-time-ms (/ render-time-ms count)))
+ ""))
+
+(defun mu4e~headers-found-handler (count)
+ "Create a one line description of the number of headers found
+after the end of the search results."
+ (when (buffer-live-p (mu4e-get-headers-buffer))
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (save-excursion
+ (goto-char (point-max))
+ (let ((inhibit-read-only t)
+ (str (if (zerop count) mu4e~no-matches mu4e~end-of-results))
+ (msg (format "Found %d matching message%s; %d hidden%s"
+ count (if (= 1 count) "" "s")
+ mu4e~headers-hidden
+ (mu4e~headers-benchmark-message count))))
+
+ (insert (propertize str 'face 'mu4e-system-face 'intangible t))
+ (unless (zerop count)
+ (mu4e-message "%s" msg))))
+
+ ;; if we need to jump to some specific message, do so now
+ (goto-char (point-min))
+ (when mu4e--search-msgid-target
+ (if (eq (current-buffer) (window-buffer))
+ (mu4e-headers-goto-message-id mu4e--search-msgid-target)
+ (let* ((pos (mu4e-headers-goto-message-id
+ mu4e--search-msgid-target)))
+ (when pos
+ (set-window-point (get-buffer-window nil t) pos)))))
+ (when (and mu4e--search-view-target (mu4e-message-at-point 'noerror))
+ ;; view the message at point when there is one.
+ (mu4e-headers-view-message))
+ (setq mu4e--search-view-target nil
+ mu4e--search-msgid-target nil)
+ (when (mu4e~headers-docid-at-point)
+ (mu4e~headers-highlight (mu4e~headers-docid-at-point)))
+ ;; maybe enable thread folding
+ (when mu4e-search-threads
+ (mu4e-thread-mode))))
+ ;; run-hooks
+ (run-hooks 'mu4e-headers-found-hook))
+
+\f
+;;; Marking
+
+(defmacro mu4e~headers-defun-mark-for (mark)
+ "Define a function mu4e~headers-mark-MARK."
+ (let ((funcname (intern (format "mu4e-headers-mark-for-%s" mark)))
+ (docstring (format "Mark header at point with %s." mark)))
+ `(progn
+ (defun ,funcname () ,docstring
+ (interactive)
+ (mu4e-headers-mark-and-next ',mark))
+ (put ',funcname 'definition-name ',mark))))
+
+(mu4e~headers-defun-mark-for refile)
+(mu4e~headers-defun-mark-for something)
+(mu4e~headers-defun-mark-for delete)
+(mu4e~headers-defun-mark-for trash)
+(mu4e~headers-defun-mark-for flag)
+(mu4e~headers-defun-mark-for move)
+(mu4e~headers-defun-mark-for read)
+(mu4e~headers-defun-mark-for unflag)
+(mu4e~headers-defun-mark-for untrash)
+(mu4e~headers-defun-mark-for unmark)
+(mu4e~headers-defun-mark-for unread)
+(mu4e~headers-defun-mark-for action)
+
+(declare-function mu4e-view-pipe "mu4e-view")
+
+(defvar mu4e-headers-mode-map
+ (let ((map (make-sparse-keymap)))
+
+ (define-key map "q" #'mu4e~headers-quit-buffer)
+ (define-key map "g" #'mu4e-search-rerun) ;; for compatibility
+
+
+ (define-key map "%" #'mu4e-headers-mark-pattern)
+ (define-key map "t" #'mu4e-headers-mark-subthread)
+ (define-key map "T" #'mu4e-headers-mark-thread)
+
+ (define-key map "," #'mu4e-sexp-at-point)
+ (define-key map ";" #'mu4e-context-switch)
+
+ ;; navigation between messages
+ (define-key map "p" #'mu4e-headers-prev)
+ (define-key map "n" #'mu4e-headers-next)
+ (define-key map (kbd "<M-up>") #'mu4e-headers-prev)
+ (define-key map (kbd "<M-down>") #'mu4e-headers-next)
+
+ (define-key map (kbd "[") #'mu4e-headers-prev-unread)
+ (define-key map (kbd "]") #'mu4e-headers-next-unread)
+
+ (define-key map (kbd "{") #'mu4e-headers-prev-thread)
+ (define-key map (kbd "}") #'mu4e-headers-next-thread)
+
+ ;; change the number of headers
+ (define-key map (kbd "C-+") #'mu4e-headers-split-view-grow)
+ (define-key map (kbd "C--") #'mu4e-headers-split-view-shrink)
+ (define-key map (kbd "<C-kp-add>") 'mu4e-headers-split-view-grow)
+ (define-key map (kbd "<C-kp-subtract>")
+ #'mu4e-headers-split-view-shrink)
+
+ ;; switching to view mode (if it's visible)
+ (define-key map "y" #'mu4e-select-other-view)
+
+ ;; marking/unmarking ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+ (define-key map (kbd "<backspace>") #'mu4e-headers-mark-for-trash)
+ (define-key map (kbd "d") #'mu4e-headers-mark-for-trash)
+ (define-key map (kbd "<delete>") #'mu4e-headers-mark-for-delete)
+ (define-key map (kbd "<deletechar>") #'mu4e-headers-mark-for-delete)
+ (define-key map (kbd "D") #'mu4e-headers-mark-for-delete)
+ (define-key map (kbd "m") #'mu4e-headers-mark-for-move)
+ (define-key map (kbd "r") #'mu4e-headers-mark-for-refile)
+
+ (define-key map (kbd "?") #'mu4e-headers-mark-for-unread)
+ (define-key map (kbd "!") #'mu4e-headers-mark-for-read)
+ (define-key map (kbd "A") #'mu4e-headers-mark-for-action)
+
+ (define-key map (kbd "u") #'mu4e-headers-mark-for-unmark)
+ (define-key map (kbd "+") #'mu4e-headers-mark-for-flag)
+ (define-key map (kbd "-") #'mu4e-headers-mark-for-unflag)
+ (define-key map (kbd "=") #'mu4e-headers-mark-for-untrash)
+ (define-key map (kbd "&") #'mu4e-headers-mark-custom)
+
+ (define-key map (kbd "*")
+ #'mu4e-headers-mark-for-something)
+ (define-key map (kbd "<kp-multiply>")
+ #'mu4e-headers-mark-for-something)
+ (define-key map (kbd "<insertchar>")
+ #'mu4e-headers-mark-for-something)
+ (define-key map (kbd "<insert>")
+ #'mu4e-headers-mark-for-something)
+
+ (define-key map (kbd "#") #'mu4e-mark-resolve-deferred-marks)
+
+ (define-key map "U" #'mu4e-mark-unmark-all)
+ (define-key map "x" #'mu4e-mark-execute-all)
+
+ (define-key map "a" #'mu4e-headers-action)
+
+ ;; message composition
+
+ (define-key map (kbd "RET") #'mu4e-headers-view-message)
+ (define-key map [mouse-2] #'mu4e-headers-view-message)
+
+ (define-key map "$" #'mu4e-show-log)
+ (define-key map "H" #'mu4e-display-manual)
+
+ (define-key map "|" #'mu4e-view-pipe)
+ map)
+ "Keymap for mu4e's headers mode.")
+
+(easy-menu-define mu4e-headers-mode-menu
+ mu4e-headers-mode-map "Menu for mu4e's headers-mode."
+ (append
+ '("Headers" ;;:visible mu4e-headers-mode
+ "--"
+ ["Previous" mu4e-headers-prev
+ :help "Move to previous header"]
+ ["Next" mu4e-headers-prev
+ :help "Move to next header"]
+ "--"
+ ["Mark for move" mu4e-headers-mark-for-move
+ :help "Mark message for move"
+ ])
+ mu4e--compose-menu-items
+ mu4e--search-menu-items
+ mu4e--context-menu-items
+ '(
+ "--"
+ ["Quit" mu4e~headers-quit-buffer
+ :help "Quit the headers"]
+ )))
+
+;;; Headers-mode and mode-map
+
+(defun mu4e~header-line-format ()
+ "Get the format for the header line."
+ (let ((uparrow (if mu4e-use-fancy-chars " ▲" " ^"))
+ (downarrow (if mu4e-use-fancy-chars " ▼" " V")))
+ (cons
+ (make-string
+ (+ mu4e--mark-fringe-len (floor (fringe-columns 'left t))) ?\s)
+ (mapcar
+ (lambda (item)
+ (let* (;; with threading enabled, we're necessarily sorting by date.
+ (sort-field (if mu4e-search-threads
+ :date mu4e-search-sort-field))
+ (field (car item)) (width (cdr item))
+ (info (cdr (assoc field
+ (append mu4e-header-info
+ mu4e-header-info-custom))))
+ (sortable (plist-get info :sortable))
+ ;; if sortable, it is either t (when field is sortable itself)
+ ;; or a symbol (if another field is used for sorting)
+ (this-field (when sortable (if (booleanp sortable)
+ field
+ sortable)))
+ (help (plist-get info :help))
+ ;; triangle to mark the sorted-by column
+ (arrow
+ (when (and sortable (eq this-field sort-field))
+ (if (eq mu4e-search-sort-direction 'descending)
+ downarrow
+ uparrow)))
+ (name (concat (plist-get info :shortname) arrow))
+ (map (make-sparse-keymap)))
+ (when sortable
+ (define-key map [header-line mouse-1]
+ (lambda (&optional e)
+ ;; getting the field, inspired by
+ ;; `tabulated-list-col-sort'
+ (interactive "e")
+ (let* ((obj (posn-object (event-start e)))
+ (field
+ (and obj
+ (get-text-property 0 'field (car obj)))))
+ ;; "t": if we're already sorted by field, the
+ ;; sort-order is changed
+ (mu4e-search-change-sorting field t)))))
+ (concat
+ (propertize
+ (if width
+ (truncate-string-to-width
+ name width 0 ?\s truncate-string-ellipsis)
+ name)
+ 'face (when arrow 'bold)
+ 'help-echo help
+ 'mouse-face (when sortable 'highlight)
+ 'keymap (when sortable map)
+ 'field field) " ")))
+ mu4e-headers-fields))))
+
+(defun mu4e~headers-maybe-auto-update ()
+ "Update the current headers buffer after indexing has brought
+some changes, `mu4e-headers-auto-update' is non-nil and there is
+no user-interaction ongoing."
+ (when (and mu4e-headers-auto-update ;; must be set
+ mu4e-index-update-status
+ (not (mu4e-get-view-buffer)) ;; not when viewing a message
+ (not (zerop (plist-get mu4e-index-update-status :updated)))
+ ;; NOTE: `mu4e-mark-marks-num' can return nil. Is that intended?
+ (zerop (or (mu4e-mark-marks-num) 0)) ;; non active marks
+ (not (active-minibuffer-window))) ;; no user input only
+ ;; rerun search if there's a live window with search results;
+ ;; otherwise we'd trigger a headers view from out of nowhere.
+ (when (and (buffer-live-p (mu4e-get-headers-buffer))
+ (window-live-p (get-buffer-window (mu4e-get-headers-buffer) t)))
+ (let ((mu4e--search-background t))
+ (mu4e-search-rerun)))))
+
+(defcustom mu4e-headers-eldoc-format "“%s” from %f on %d"
+ "Format for the `eldoc' string for the current message in the headers buffer.
+The following specs are supported:
+- %s: the message Subject
+- %f: the message From
+- %t: the message To
+- %c: the message Cc
+- %d: the message Date
+- %p: the message priority
+- %m: the maildir containing the message
+- %F: the message’s flags
+- %M: the Message-Id"
+ :type 'string
+ :group 'mu4e-headers)
+
+(defun mu4e-headers-eldoc-function (&rest _args)
+ (let ((msg (get-text-property (point) 'msg)))
+ (when msg
+ (format-spec
+ mu4e-headers-eldoc-format
+ `((?s . ,(mu4e-message-field msg :subject))
+ (?f . ,(mu4e~headers-contact-str (mu4e-message-field msg :from)))
+ (?t . ,(mu4e~headers-contact-str (mu4e-message-field msg :to)))
+ (?c . ,(mu4e~headers-contact-str (mu4e-message-field msg :cc)))
+ (?d . ,(mu4e~headers-human-date msg))
+ (?p . ,(mu4e-message-field msg :priority))
+ (?m . ,(mu4e-message-field msg :maildir))
+ (?F . ,(mu4e-message-field msg :flags))
+ (?M . ,(mu4e-message-field msg :message-id)))))))
+
+(define-derived-mode mu4e-headers-mode special-mode
+ "mu4e:headers"
+ "Major mode for displaying mu4e search results.
+\\{mu4e-headers-mode-map}."
+ (use-local-map mu4e-headers-mode-map)
+ (make-local-variable 'mu4e~headers-proc)
+ (make-local-variable 'mu4e~highlighted-docid)
+ (set (make-local-variable 'hl-line-face) 'mu4e-header-highlight-face)
+
+ ;; Eldoc support
+ (when (and (featurep 'eldoc) mu4e-eldoc-support)
+ (if (boundp 'eldoc-documentation-functions)
+ ;; Emacs 28 or newer
+ (add-hook 'eldoc-documentation-functions
+ #'mu4e-headers-eldoc-function nil t)
+ ;; Emacs 27 or older
+ (add-function :before-until (local 'eldoc-documentation-function)
+ #'mu4e-headers-eldoc-function)))
+
+ ;; support bookmarks.
+ (set (make-local-variable 'bookmark-make-record-function)
+ 'mu4e--make-bookmark-record)
+ ;; maybe update the current headers upon indexing changes
+ (add-hook 'mu4e-index-updated-hook #'mu4e~headers-maybe-auto-update)
+ (setq
+ truncate-lines t
+ buffer-undo-list t ;; don't record undo information
+ overwrite-mode nil
+ header-line-format (mu4e~header-line-format))
+
+ (mu4e--mark-initialize) ;; initialize the marking subsystem
+ (mu4e-context-minor-mode)
+ (mu4e-update-minor-mode)
+ (mu4e-search-minor-mode)
+ (mu4e-compose-minor-mode)
+ (hl-line-mode 1)
+
+ (mu4e--modeline-register #'mu4e--search-modeline-item)
+ (mu4e--modeline-update))
+
+;;; Highlighting
+
+(defvar mu4e~highlighted-docid nil
+ "The highlighted docid")
+
+(defun mu4e~headers-highlight (docid)
+ "Highlight the header with DOCID, or do nothing if it's not found.
+Also, unhighlight any previously highlighted headers."
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (save-excursion
+ ;; first, unhighlight the previously highlighted docid, if any
+ (when (and docid mu4e~highlighted-docid
+ (mu4e~headers-goto-docid mu4e~highlighted-docid))
+ (hl-line-unhighlight))
+ ;; now, highlight the new one
+ (when (mu4e~headers-goto-docid docid)
+ (hl-line-highlight)))
+ (setq mu4e~highlighted-docid docid)))
+
+;;; Misc 2
+
+(defun mu4e~headers-select-window ()
+ "When there is a visible window for the headers buffer, make sure
+to select it. This is needed when adding new headers, otherwise
+adding a lot of new headers looks really choppy."
+ (let ((win (get-buffer-window (mu4e-get-headers-buffer))))
+ (when win (select-window win))))
+
+(defun mu4e-headers-goto-message-id (msgid)
+ "Go to the next message with message-id MSGID. Return the
+message plist, or nil if not found."
+ (mu4e-headers-find-if
+ (lambda (msg)
+ (let ((this-msgid (mu4e-message-field msg :message-id)))
+ (when (and this-msgid (string= msgid this-msgid))
+ msg)))))
+
+;;; Marking 2
+
+(defun mu4e~headers-mark (docid mark)
+ "(Visually) mark the header for DOCID with character MARK."
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (let ((inhibit-read-only t) (oldpoint (point)))
+ (unless (mu4e~headers-goto-docid docid)
+ (mu4e-error "Cannot find message with docid %S" docid))
+ ;; now, we're at the beginning of the header, looking at
+ ;; <docid>\004
+ ;; (which is invisible). jump past that…
+ (unless (re-search-forward mu4e~headers-docid-post nil t)
+ (mu4e-error "Cannot find the `mu4e~headers-docid-post' separator"))
+
+ ;; clear old marks, and add the new ones.
+ (let ((msg (get-text-property (point) 'msg)))
+ (delete-char mu4e--mark-fringe-len)
+ (insert (propertize
+ (format mu4e--mark-fringe-format mark)
+ 'face 'mu4e-header-marks-face
+ 'docid docid
+ 'msg msg)))
+ (goto-char oldpoint))))
+
+
+;;; Queries & searching
+
+;;; Search-based marking
+
+(defun mu4e-headers-for-each (func)
+ "Call FUNC for each header, moving point to the header.
+FUNC receives one argument, the message s-expression for the
+corresponding header."
+ (save-excursion
+ (goto-char (point-min))
+ (while (search-forward mu4e~headers-docid-pre nil t)
+ ;; not really sure why we need to jump to bol; we do need to, otherwise we
+ ;; miss lines sometimes...
+ (let ((msg (get-text-property (line-beginning-position) 'msg)))
+ (when msg
+ (funcall func msg))))))
+
+
+(defun mu4e-headers-find-if (func &optional backward)
+ "Move to the header for which FUNC returns non-`nil'.
+if BACKWARD is non-nil, search backwards.
+
+ FUNC receives one argument, the message s-expression for the
+corresponding header. If BACKWARD is non-`nil', search backwards.
+Returns the new position, or `nil' if nothing was found. If you
+want to exclude matches for the current message, you can use
+`mu4e-headers-find-if-next'.
+
+Return the found position or nil if not found."
+ (let ((pos)
+ (search-func (if backward 'search-backward 'search-forward)))
+ (save-excursion
+ (while (and (null pos)
+ (funcall search-func mu4e~headers-docid-pre nil t))
+ ;; not really sure why we need to jump to bol; we do need to, otherwise
+ ;; we miss lines sometimes...
+ (let ((msg (get-text-property (line-beginning-position) 'msg)))
+ (when (and msg (funcall func msg))
+ (setq pos (point))))))
+ (when pos
+ (goto-char pos))
+ pos))
+
+(defun mu4e-headers-find-if-next (func &optional backwards)
+ "Like `mu4e-headers-find-if', but do not match the current header.
+Move to the next or (if BACKWARDS is non-`nil') header for which FUNC
+returns non-`nil', starting from the current position."
+ (let ((pos))
+ (save-excursion
+ (if backwards (beginning-of-line) (end-of-line))
+ (setq pos (mu4e-headers-find-if func backwards)))
+ (when pos (goto-char pos))))
+
+(defvar mu4e~headers-regexp-hist nil
+ "History list of regexps used.")
+
+(defun mu4e-headers-mark-for-each-if (markpair mark-pred &optional param)
+ "Mark all headers for which predicate function MARK-PRED returns
+non-nil with MARKPAIR. MARK-PRED is function that receives two
+arguments, MSG (the message at point) and PARAM (a user-specified
+parameter). MARKPAIR is a cell (MARK . TARGET); see
+`mu4e-mark-at-point' for details about marks."
+ (mu4e-headers-for-each
+ (lambda (msg)
+ (when (funcall mark-pred msg param)
+ (mu4e-mark-at-point (car markpair) (cdr markpair))))))
+
+(defun mu4e-headers-mark-pattern ()
+ "Ask user for a kind of mark (move, delete etc.), a field to
+match and a regular expression to match with. Then, mark all
+matching messages with that mark."
+ (interactive)
+ (let ((markpair (mu4e--mark-get-markpair "Mark matched messages with: " t))
+ (field (mu4e-read-option "Field to match: "
+ '( ("subject" . :subject)
+ ("from" . :from)
+ ("to" . :to)
+ ("cc" . :cc)
+ ("bcc" . :bcc)
+ ("list" . :list))))
+ (pattern (read-string
+ (mu4e-format "Regexp:")
+ nil 'mu4e~headers-regexp-hist)))
+ (mu4e-headers-mark-for-each-if
+ markpair
+ (lambda (msg _param)
+ (let* ((value (mu4e-msg-field msg field)))
+ (if (member field '(:to :from :cc :bcc :reply-to))
+ (cl-find-if (lambda (contact)
+ (let ((name (mu4e-contact-name contact))
+ (email (mu4e-contact-email contact)))
+ (or (and name (string-match pattern name))
+ (and email (string-match pattern email)))))
+ value)
+ (string-match pattern (or value ""))))))))
+
+(defun mu4e-headers-mark-custom ()
+ "Mark messages based on a user-provided predicate function."
+ (interactive)
+ (let* ((pred (mu4e-read-option "Match function: "
+ mu4e-headers-custom-markers))
+ (param (when (cdr pred) (eval (cdr pred))))
+ (markpair (mu4e--mark-get-markpair "Mark matched messages with: " t)))
+ (mu4e-headers-mark-for-each-if markpair (car pred) param)))
+
+(defun mu4e~headers-get-thread-info (msg what)
+ "Get WHAT (a symbol, either path or thread-id) for MSG."
+ (let* ((meta (or (mu4e-message-field msg :meta)
+ (mu4e-error "No thread info found")))
+ (path (or (plist-get meta :path)
+ (mu4e-error "No threadpath found"))))
+ (cl-case what
+ (path path)
+ (thread-id
+ (save-match-data
+ ;; the thread id is the first segment of the thread path
+ (when (string-match "^\\([[:xdigit:]]+\\):?" path)
+ (match-string 1 path))))
+ (otherwise (mu4e-error "Not supported")))))
+
+(defun mu4e-headers-mark-thread-using-markpair (markpair &optional subthread)
+ "Mark the thread at point using the given markpair. If SUBTHREAD is
+non-nil, marking is limited to the message at point and its
+descendants."
+ (let* ((mark (car markpair))
+ (allowed-marks (mapcar 'car mu4e-marks)))
+ (unless (memq mark allowed-marks)
+ (mu4e-error "The mark (%s) has to be one of: %s"
+ mark allowed-marks)))
+ ;; note: the thread id is shared by all messages in a thread
+ (let* ((msg (mu4e-message-at-point))
+ (thread-id (mu4e~headers-get-thread-info msg 'thread-id))
+ (path (mu4e~headers-get-thread-info msg 'path))
+ ;; the thread path may have a ':z' suffix for sorting;
+ ;; remove it for subthread matching.
+ (match-path (replace-regexp-in-string ":z$" "" path))
+ (last-marked-point))
+ (mu4e-headers-for-each
+ (lambda (cur-msg)
+ (let ((cur-thread-id (mu4e~headers-get-thread-info cur-msg 'thread-id))
+ (cur-thread-path (mu4e~headers-get-thread-info cur-msg 'path)))
+ (if subthread
+ ;; subthread matching; mymsg's thread path should have path as its
+ ;; prefix
+ (when (string-match (concat "^" match-path) cur-thread-path)
+ (mu4e-mark-at-point (car markpair) (cdr markpair))
+ (setq last-marked-point (point)))
+ ;; nope; not looking for the subthread; looking for the whole thread
+ (when (string= thread-id cur-thread-id)
+ (mu4e-mark-at-point (car markpair) (cdr markpair))
+ (setq last-marked-point (point)))))))
+ (when last-marked-point
+ (goto-char last-marked-point)
+ (mu4e-headers-next))))
+
+(defun mu4e-headers-mark-thread (&optional subthread markpair)
+ "Like `mu4e-headers-mark-thread-using-markpair' but prompt for the markpair."
+ (interactive
+ (let* ((subthread current-prefix-arg))
+ (list current-prefix-arg
+ ;; FIXME: e.g., for refiling we should evaluate this
+ ;; for each line separately
+ (mu4e--mark-get-markpair
+ (if subthread "Mark subthread with: " "Mark whole thread with: ")
+ t))))
+ (mu4e-headers-mark-thread-using-markpair markpair subthread))
+
+(defun mu4e-headers-mark-subthread (&optional markpair)
+ "Like `mu4e-mark-thread', but only for a sub-thread."
+ (interactive)
+ (if markpair (mu4e-headers-mark-thread t markpair)
+ (let ((current-prefix-arg t))
+ (call-interactively 'mu4e-headers-mark-thread))))
+
+\f
+
+(defun mu4e-headers-view-message ()
+ "View message at point."
+ (interactive)
+ (unless (eq major-mode 'mu4e-headers-mode)
+ (mu4e-error "Must be in mu4e-headers-mode (%S)" major-mode))
+ (let* ((msg (mu4e-message-at-point))
+ (path (mu4e-message-field msg :path))
+ (_exists (or (file-readable-p path)
+ (mu4e-warn "No message at %s" path)))
+ (docid (or (mu4e-message-field msg :docid)
+ (mu4e-warn "No message at point")))
+ (mark-as-read
+ (if (functionp mu4e-view-auto-mark-as-read)
+ (funcall mu4e-view-auto-mark-as-read msg)
+ mu4e-view-auto-mark-as-read)))
+ (when-let ((buf (mu4e-get-view-buffer (current-buffer) nil)))
+ (with-current-buffer buf
+ (mu4e-loading-mode 1)))
+ (mu4e--server-view docid mark-as-read)))
+
+(defvar-local mu4e-headers-open-after-move t
+ "If set to non-nil, open message after `mu4e-headers-next' and
+`mu4e-headers-prev' if pointing at a message after the move
+and there is a live message view.
+
+This variable is for let-binding when scripting.")
+
+(defun mu4e~headers-move (lines)
+ "Move point LINES lines.
+Move forward if LINES is positive or backwards if LINES is
+negative. If this succeeds, return the new docid. Otherwise,
+return nil.
+
+If pointing at a message after the move and there is a
+view-window, open the message unless
+`mu4e-headers-open-after-move' is non-nil."
+ (cl-assert (eq major-mode 'mu4e-headers-mode))
+ (when (ignore-errors
+ (let (line-move-visual)
+ (line-move lines)
+ t))
+ (let* ((docid (mu4e~headers-docid-at-point))
+ (folded (and docid (mu4e-thread-message-folded-p))))
+ (if folded
+ (mu4e~headers-move (if (< lines 0) -1 1)) ;; skip folded
+ (when docid
+ ;; Skip invisible text at BOL possibly hidden by
+ ;; the end of another invisible overlay covering
+ ;; previous EOL.
+ (move-to-column 2)
+ ;; update all windows showing the headers buffer
+ (walk-windows
+ (lambda (win)
+ (when (eq (window-buffer win)
+ (mu4e-get-headers-buffer (buffer-name)))
+ (set-window-point win (point))))
+ nil t)
+ ;; If the assigned (and buffer-local) `mu4e~headers-view-win'
+ ;; is not live then that is indicates the user does not want
+ ;; to pop up the view when they navigate in the headers
+ ;; buffer.
+ (when (and mu4e-headers-open-after-move
+ (window-live-p mu4e~headers-view-win))
+ (mu4e-headers-view-message))
+ ;; attempt to highlight the new line, display the message
+ (mu4e~headers-highlight docid)
+ docid)))))
+
+(defun mu4e-headers-next (&optional n)
+ "Move point to the next message header.
+If this succeeds, return the new docid. Otherwise, return nil.
+Optionally, takes an integer N (prefix argument), to the Nth next
+header.
+
+If pointing at a message after the move and there is a
+view-window, open the message unless
+`mu4e-headers-open-after-move' is non-nil."
+ (interactive "P")
+ (mu4e~headers-move (or n 1)))
+
+(defun mu4e-headers-prev (&optional n)
+ "Move point to the previous message header.
+If this succeeds, return the new docid. Otherwise, return nil.
+Optionally, takes an integer N (prefix argument), to the Nth
+previous header.
+
+If pointing at a message after the move and there is a
+view-window, open the message unless
+`mu4e-headers-open-after-move' is non-nil."
+ (interactive "P")
+ (mu4e~headers-move (- (or n 1))))
+
+(defun mu4e~headers-prev-or-next-unread (backwards)
+ "Move point to the next message that is unread (and
+untrashed). If BACKWARDS is non-`nil', move backwards."
+ (interactive "P")
+ (or (mu4e-headers-find-if-next
+ (lambda (msg)
+ (let ((flags (mu4e-message-field msg :flags)))
+ (and (member 'unread flags) (not (member 'trashed flags)))))
+ backwards)
+ (mu4e-message (format "No %s unread message found"
+ (if backwards "previous" "next")))))
+
+(defun mu4e-headers-prev-unread ()
+ "Move point to the previous message that is unread (and
+untrashed)."
+ (interactive)
+ (mu4e~headers-prev-or-next-unread t))
+
+(defun mu4e-headers-next-unread ()
+ "Move point to the next message that is unread (and
+untrashed)."
+ (interactive)
+ (mu4e~headers-prev-or-next-unread nil))
+
+(defun mu4e~headers-thread-root-p (&optional msg)
+ "Is MSG at the root of a thread?
+If MSG is nil, use message at point."
+ (when-let* ((msg (or msg (get-text-property (point) 'msg)))
+ (meta (mu4e-message-field msg :meta)))
+ (let* ((orphan (plist-get meta :orphan))
+ (first-child (plist-get meta :first-child))
+ (root (plist-get meta :root)))
+ (or root (and orphan first-child)))))
+
+(defun mu4e~headers-prev-or-next-thread (backwards)
+ "Move point to the top of the next thread.
+If BACKWARDS is non-`nil', move backwards."
+ (when (mu4e-headers-find-if-next #'mu4e~headers-thread-root-p backwards)
+ (point)))
+
+(defun mu4e-headers-prev-thread ()
+ "Move point to the previous thread."
+ (interactive) (mu4e~headers-prev-or-next-thread t))
+
+(defun mu4e-headers-next-thread ()
+ "Move point to the previous thread."
+ (interactive) (mu4e~headers-prev-or-next-thread nil))
+
+(defun mu4e-headers-split-view-grow (&optional n)
+ "In split-view, grow the headers window.
+In horizontal split-view, increase the number of lines shown by N.
+In vertical split-view, increase the number of columns shown by N.
+If N is negative shrink the headers window. When not in split-view
+do nothing."
+ (interactive "P")
+ (let ((n (or n 1))
+ (hwin (get-buffer-window (mu4e-get-headers-buffer))))
+ (when (and (buffer-live-p (mu4e-get-view-buffer)) (window-live-p hwin))
+ (let ((n (or n 1)))
+ (cl-case mu4e-split-view
+ ;; emacs has weird ideas about what horizontal, vertical means...
+ (horizontal
+ (window-resize hwin n nil)
+ (cl-incf mu4e-headers-visible-lines n))
+ (vertical
+ (window-resize hwin n t)
+ (cl-incf mu4e-headers-visible-columns n)))))))
+
+(defun mu4e-headers-split-view-shrink (&optional n)
+ "In split-view, shrink the headers window.
+In horizontal split-view, decrease the number of lines shown by N.
+In vertical split-view, decrease the number of columns shown by N.
+If N is negative grow the headers window. When not in split-view
+do nothing."
+ (interactive "P")
+ (mu4e-headers-split-view-grow (- (or n 1))))
+
+(defun mu4e-headers-action (&optional actionfunc)
+ "Ask user what to do with message-at-point, then do it.
+The actions are specified in `mu4e-headers-actions'. Optionally,
+pass ACTIONFUNC, which is a function that takes a msg-plist
+argument."
+ (interactive)
+ (let ((msg (mu4e-message-at-point))
+ (afunc (or actionfunc
+ (mu4e-read-option "Action: " mu4e-headers-actions))))
+ (funcall afunc msg)))
+
+(defun mu4e-headers-mark-and-next (mark)
+ "Set mark MARK on the message at point or on all messages in the
+region if there is a region, then move to the next message."
+ (interactive)
+ (when (mu4e-thread-message-folded-p)
+ (mu4e-warn "Cannot mark folded messages"))
+ (mu4e-mark-set mark)
+ (when mu4e-headers-advance-after-mark (mu4e-headers-next)))
+
+(defun mu4e~headers-quit-buffer ()
+ "Quit the mu4e-headers buffer and go back to the main view."
+ (interactive)
+ (mu4e-mark-handle-when-leaving)
+ (quit-window t)
+ ;; clear the decks before going to the main-view
+ (mu4e--query-items-refresh 'reset-baseline)
+ (mu4e--main-view))
+
+\f
+;;; Loading messages
+;;
+
+
+(defvar-local mu4e--loading-overlay-bg nil
+ "Internal variable that holds the loading overlay for the background.")
+
+(defvar-local mu4e--loading-overlay-text nil
+ "Internal variable that holds the loading overlay for the text.")
+
+(define-minor-mode mu4e-loading-mode
+ "Minor mode for buffers awaiting data from mu"
+ :init-value nil :lighter " Loading" :keymap nil
+ (if mu4e-loading-mode
+ (progn
+ (when mu4e-dim-when-loading
+ (setq mu4e--loading-overlay-bg
+ (let ((overlay (make-overlay (point-min) (point-max))))
+ (overlay-put overlay 'face
+ `(:foreground "gray22" :background
+ ,(face-attribute 'default
+ :background)))
+ (overlay-put overlay 'priority 9998)
+ overlay))
+ (setq mu4e--loading-overlay-text
+ (let ((overlay (make-overlay (point-min) (point-min))))
+ (overlay-put overlay 'priority 9999)
+ (overlay-put overlay 'before-string
+ (propertize "Loading…\n"
+ 'face 'mu4e-header-title-face))
+ overlay))))
+ (when mu4e--loading-overlay-bg
+ (delete-overlay mu4e--loading-overlay-bg))
+ (when mu4e--loading-overlay-text
+ (delete-overlay mu4e--loading-overlay-text))))
+
+(provide 'mu4e-headers)
+;;; mu4e-headers.el ends here
--- /dev/null
+;;; mu4e-helpers.el --- Helper functions -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022-2024 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Helper functions used in the mu4e. This is slowly usurp all the code from
+;; mu4e-utils.el that does not depend on other parts of mu4e.
+
+;;; Code:
+
+(require 'seq)
+(require 'ido)
+(require 'cl-lib)
+(require 'bookmark)
+
+(require 'mu4e-window)
+(require 'mu4e-config)
+\f
+;;; Customization
+
+(defcustom mu4e-debug nil
+ "When set to non-nil, log debug information to the mu4e log buffer."
+ :type 'boolean
+ :group 'mu4e)
+
+(defcustom mu4e-completing-read-function #'ido-completing-read
+ "Function to be used to receive user-input during completion.
+
+Suggested possible values are:
+ * `completing-read': emacs built-in completion method
+ * `ido-completing-read': dynamic completion within the minibuffer.
+
+The function is used in two contexts -
+1) directly - for instance in when listing _other_ maildirs
+ in `mu4e-ask-maildir'
+2) if `mu4e-read-option-use-builtin' is nil, it is used
+ as part of `mu4e-read-option' in many places.
+
+Set it to `completing-read' when you want to use completion
+frameworks such as Helm, Ivy or Vertico. In that case, you
+might want to add something like the following in your configuration.
+
+ (setq mu4e-read-option-use-builtin nil
+ mu4e-completing-read-function \\='completing-read)
+."
+ :type 'function
+ :options '(completing-read ido-completing-read)
+ :group 'mu4e)
+
+(defcustom mu4e-read-option-use-builtin t
+ "Whether to use mu4e's traditional completion for
+`mu4e-read-option'.
+
+If nil, use the value of `mu4e-completing-read-function', integrated
+into mu4e.
+
+Many of the third-party completion frameworks - such as Helm, Ivy
+and Vertico - influence `completion-read', so to have mu4e follow
+your overall settings, try the equivalent of
+
+ (setq mu4e-read-option-use-builtin nil
+ mu4e-completing-read-function \\='completing-read)
+
+Tastes differ, but without any such frameworks, the unaugmented
+Emacs `completing-read' is rather Spartan."
+ :type 'boolean
+ :group 'mu4e)
+
+(defcustom mu4e-use-fancy-chars nil
+ "When set, allow fancy (Unicode) characters for marks/threads.
+You can customize the exact fancy characters used with
+`mu4e-marks' and various `mu4e-headers-..-mark' and
+`mu4e-headers..-prefix' variables."
+ :type 'boolean
+ :group 'mu4e)
+
+;; maybe move the next ones... but they're convenient
+;; here because they're needed in multiple buffers.
+
+(defcustom mu4e-view-auto-mark-as-read t
+ "Automatically mark messages as read when you read them.
+This is the default behavior, but can be turned off, for example
+when using a read-only file-system.
+
+This can also be set to a function; if so, receives a message
+plist which should evaluate to nil if the message should *not* be
+marked as read-only, or non-nil otherwise."
+ :type '(choice
+ boolean
+ function)
+ :group 'mu4e-view)
+
+\f
+
+(defun mu4e-select-other-view ()
+ "Switch between headers view and message view."
+ (interactive)
+ (let* ((other-buf
+ (cond
+ ((mu4e-current-buffer-type-p 'view)
+ (mu4e-get-headers-buffer))
+ ((mu4e-current-buffer-type-p 'headers)
+ (mu4e-get-view-buffer))
+ (t (mu4e-error
+ "This window is neither the headers nor the view window."))))
+ (other-win (and other-buf (get-buffer-window other-buf))))
+ (if (window-live-p other-win)
+ (select-window other-win)
+ (mu4e-message "No window to switch to"))))
+
+\f
+
+;;; Messages, warnings and errors
+(defun mu4e-format (frm &rest args)
+ "Create [mu4e]-prefixed string based on format FRM and ARGS."
+ (concat
+ "[" (propertize "mu4e" 'face 'mu4e-title-face) "] "
+ (apply 'format frm
+ (mapcar (lambda (x)
+ (if (stringp x)
+ (decode-coding-string x 'utf-8)
+ x))
+ args))))
+
+(defun mu4e-message (frm &rest args)
+ "Display FRM with ARGS like `message' in mu4e style.
+If we're waiting for user-input or if there's some message in the
+echo area, don't show anything."
+ (unless (or (active-minibuffer-window))
+ (message "%s" (apply 'mu4e-format frm args))))
+
+(declare-function mu4e~loading-close "mu4e-headers")
+
+(defun mu4e-error (frm &rest args)
+ "Display an error with FRM and ARGS like `mu4e-message'.
+
+Create [mu4e]-prefixed error based on format FRM and ARGS. Does a
+local-exit and does not return, and raises a
+debuggable (backtrace) error."
+ (mu4e-log 'error (apply 'mu4e-format frm args))
+ (error "%s" (apply 'mu4e-format frm args)))
+
+(defun mu4e-warn (frm &rest args)
+ "Create [mu4e]-prefixed warning based on format FRM and ARGS.
+Does a local-exit and does not return."
+ (mu4e-log 'error (apply 'mu4e-format frm args))
+ (user-error "%s" (apply 'mu4e-format frm args)))
+
+;;; Reading user input
+
+(defun mu4e--plist-get (lst prop)
+ "Get PROP from plist LST and raise an error if not present."
+ (or (plist-get lst prop)
+ (if (plist-member lst prop)
+ nil
+ (mu4e-error "Missing property %s in %s" prop lst))))
+
+(defun mu4e--matching-choice (choices kar)
+ "Does KAR match any of the CHOICES?
+
+KAR is a character and CHOICES is an alist as described in
+`mu4e--read-choice-builtin'.
+
+First try an exact match, but if there isn't, try
+case-insensitive.
+
+Return the cdr (value) of the matching cell, if any."
+ (let* ((match) (match-ci))
+ (catch 'found
+ (seq-do
+ (lambda (choice)
+ ;; first try an exact match
+ (let ((case-fold-search nil))
+ (if (char-equal kar (caadr choice))
+ (progn
+ (setq match choice)
+ (throw 'found choice)) ;; found it - quit.
+ ;; perhaps case-insensitive?
+ (let ((case-fold-search t))
+ (when (and (not match-ci) (char-equal kar (caadr choice)))
+ (setq match-ci choice))))))
+ choices))
+ (if match (cdadr match)
+ (when match-ci (cdadr match-ci)))))
+
+(defun mu4e--read-choice-completing-read (prompt choices)
+ "Read and return one of CHOICES, prompting for PROMPT.
+
+PROMPT describes a multiple-choice question to the user. CHOICES
+is an alist of the form
+ ( ( <display-string> ( <shortcut> . <value> ))
+ ... )
+Any input that is not one of CHOICES is ignored. This is mu4e's
+version of `read-char-choice' which becomes case-insensitive
+after trying an exact match.
+
+Return the matching choice value (cdr of the cell)."
+ (let* ((metadata `(metadata
+ (display-sort-function . ,#'identity)
+ (cycle-sort-function . ,#'identity)))
+ (quick-result)
+ (result
+ (minibuffer-with-setup-hook
+ (lambda ()
+ (add-hook 'post-command-hook
+ (lambda ()
+ ;; Exit directly if a quick key is pressed
+ (let ((prefix (minibuffer-contents-no-properties)))
+ (unless (string-empty-p prefix)
+ (setq quick-result
+ (mu4e--matching-choice
+ choices (string-to-char prefix)))
+ (when quick-result
+ (exit-minibuffer)))))
+ -1 'local))
+ (funcall mu4e-completing-read-function
+ prompt
+ ;; Use function with metadata to disable sorting.
+ (lambda (input predicate action)
+ (if (eq action 'metadata)
+ metadata
+ (complete-with-action action choices input predicate)))
+ ;; Require confirmation, if the input does not match a suggestion
+ nil t nil nil nil))))
+ (or quick-result
+ (cdadr (assoc result choices)))))
+
+(defun mu4e--read-choice-builtin (prompt choices)
+ "Read and return one of CHOICES, prompting for PROMPT.
+
+PROMPT describes a multiple-choice question to the user. CHOICES
+is an alist of the fiorm
+ ( ( <display-string> ( <shortcut> . <value> ))
+ ... )
+Any input that is not one of CHOICES is ignored. This is mu4e's
+version of `read-char-choice' which becomes case-insensitive
+after trying an exact match.
+
+Return the matching choice value (cdr of the cell)."
+ (let ((chosen) (inhibit-quit nil)
+ (prompt (format "%s%s"
+ (mu4e-format prompt)
+ (mapconcat #'car choices ", "))))
+ (while (not chosen)
+ (message nil) ;; this seems needed...
+ (when-let ((kar (read-char-exclusive prompt)))
+ (when (eq kar ?\e) (keyboard-quit)) ;; `read-char-exclusive' is a C
+ ;; function and doesn't check for
+ ;; `keyboard-quit', there we need to
+ ;; check if ESC is pressed
+ (setq chosen (mu4e--matching-choice choices kar))))
+ chosen))
+
+(defun mu4e-read-option (prompt options)
+ "Ask user for an option from a list on the input area.
+
+PROMPT describes a multiple-choice question to the user. OPTIONS
+describe the options, and is a list of cells describing
+particular options. Cells have the following structure:
+
+ (OPTION . RESULT)
+
+where OPTIONS is a non-empty string describing the option. The
+first character of OPTION is used as the shortcut, and obviously
+all shortcuts must be different, so you can prefix the string
+with an uniquifying character.
+
+The options are provided as a list for the user to choose from;
+user can then choose by typing CHAR. Example:
+ (mu4e-read-option \"Choose an animal: \"
+ \\='((\"Monkey\" . monkey) (\"Gnu\" . gnu) (\"xMoose\" . moose)))
+
+User now will be presented with a list: \"Choose an animal:
+ [M]onkey, [G]nu, [x]Moose\".
+
+If optional character KEY is provied, use that instead of asking
+the user.
+
+Function returns the value (cdr) of the matching cell."
+ (let* ((choices ;; ((<display> ( <key> . <value> ) ...)
+ (seq-map
+ (lambda (option)
+ (list
+ (concat ;; <display>
+ "[" (propertize (substring (car option) 0 1)
+ 'face 'mu4e-highlight-face)
+ "]"
+ (substring (car option) 1))
+ (cons
+ (string-to-char (car option)) ;; <key>
+ (cdr option)))) ;; <value>
+ options))
+ (response (funcall
+ (if mu4e-read-option-use-builtin
+ #'mu4e--read-choice-builtin
+ #'mu4e--read-choice-completing-read)
+ prompt choices)))
+ (or response
+ (mu4e-warn "invalid input"))))
+
+(defun mu4e-filter-single-key (lst)
+ "Return a list consisting of LST items with a `characterp' :key prop."
+ ;; This works for bookmarks and maildirs.
+ (seq-filter (lambda (item)
+ (characterp (plist-get item :key)))
+ lst))
+
+\f
+;;; Logging / debugging
+
+(defconst mu4e--log-max-size 1000000
+ "Max number of characters to keep around in the log buffer.")
+(defconst mu4e--log-buffer-name "*mu4e-log*"
+ "Name of the logging buffer.")
+
+(defun mu4e--get-log-buffer ()
+ "Fetch (and maybe create) the log buffer."
+ (unless (get-buffer mu4e--log-buffer-name)
+ (with-current-buffer (get-buffer-create mu4e--log-buffer-name)
+ (view-mode)
+ (when (fboundp 'so-long-mode)
+ (unless (eq major-mode 'so-long-mode)
+ (eval '(so-long-mode))))
+ (setq buffer-undo-list t)))
+ mu4e--log-buffer-name)
+
+(defun mu4e-log (type frm &rest args)
+ "Log a message of TYPE with format-string FRM and ARGS.
+Use the mu4e log buffer for this. If the variable mu4e-debug is
+non-nil. Type is a symbol, either `to-server', `from-server' or
+`misc'.
+
+This function is meant for debugging."
+ (when mu4e-debug
+ (with-current-buffer (mu4e--get-log-buffer)
+ (let* ((inhibit-read-only t)
+ (tstamp (propertize (format-time-string "%Y-%m-%d %T.%3N"
+ (current-time))
+ 'face 'font-lock-string-face))
+ (msg-face
+ (pcase type
+ ('from-server 'font-lock-type-face)
+ ('to-server 'font-lock-function-name-face)
+ ('misc 'font-lock-variable-name-face)
+ ('error 'font-lock-warning-face)
+ (_ (mu4e-error "Unsupported log type"))))
+ (msg (propertize (apply 'format frm args) 'face msg-face)))
+ (save-excursion
+ (goto-char (point-max))
+ (insert tstamp
+ (pcase type
+ ('from-server " <- ")
+ ('to-server " -> ")
+ ('error " !! ")
+ (_ " "))
+ msg "\n")
+ ;; if `mu4e-log-max-lines is specified and exceeded, clearest the
+ ;; oldest lines
+ (when (> (buffer-size) mu4e--log-max-size)
+ (goto-char (- (buffer-size) mu4e--log-max-size))
+ (beginning-of-line)
+ (delete-region (point-min) (point))))))))
+
+(defun mu4e-toggle-logging ()
+ "Toggle `mu4e-debug'.
+In debug-mode, mu4e logs some of its internal workings to a
+log-buffer. See `mu4e-show-log'."
+ (interactive)
+ (mu4e-log 'misc "logging disabled")
+ (setq mu4e-debug (not mu4e-debug))
+ (mu4e-message "debug logging has been %s"
+ (if mu4e-debug "enabled" "disabled"))
+ (mu4e-log 'misc "logging enabled"))
+
+(defun mu4e-show-log ()
+ "Visit the mu4e debug log."
+ (interactive)
+ (unless mu4e-debug (mu4e-toggle-logging))
+ (let ((buf (get-buffer mu4e--log-buffer-name)))
+ (unless (buffer-live-p buf)
+ (mu4e-warn "No debug log available"))
+ (display-buffer buf)))
+
+
+\f
+;;; Flags
+;; Converting flags->string and vice-versa
+
+(defun mu4e-flags-to-string (flags)
+ "Convert a list of Maildir[1] FLAGS into a string.
+
+See `mu4e-string-to-flags'. \[1\]:
+http://cr.yp.to/proto/maildir.html."
+ (seq-sort
+ '<
+ (seq-mapcat
+ (lambda (flag)
+ (pcase flag
+ (`draft "D")
+ (`flagged "F")
+ (`new "N")
+ (`passed "P")
+ (`replied "R")
+ (`seen "S")
+ (`trashed "T")
+ (`attach "a")
+ (`encrypted "x")
+ (`signed "s")
+ (`unread "u")
+ (_ "")))
+ (seq-uniq flags) 'string)))
+
+(defun mu4e-string-to-flags (str)
+ "Convert a STR with Maildir[1] flags into a list of flags.
+
+See `mu4e-string-to-flags'. \[1\]:
+http://cr.yp.to/proto/maildir.html."
+ (seq-uniq
+ (seq-filter
+ 'identity
+ (seq-mapcat
+ (lambda (kar)
+ (list
+ (pcase kar
+ ('?D 'draft)
+ ('?F 'flagged)
+ ('?P 'passed)
+ ('?R 'replied)
+ ('?S 'seen)
+ ('?T 'trashed)
+ (_ nil))))
+ str))))
+
+\f
+;;; Misc
+(defun mu4e-copy-thing-at-point ()
+ "Copy e-mail address or URL at point to the kill ring.
+If there is not e-mail address at point, do nothing."
+ (interactive)
+ (let* ((thing (and (thing-at-point 'email)
+ (string-trim (thing-at-point 'email 'no-props) "<" ">")))
+ (thing (or thing (get-text-property (point) 'shr-url)))
+ (thing (or thing (thing-at-point 'url 'no-props))))
+ (when thing
+ (kill-new thing)
+ (mu4e-message "Copied '%s' to kill-ring" thing))))
+
+(defun mu4e-display-size (size)
+ "Get a human-friendly string representation of SIZE (in bytes)."
+ (cond
+ ((>= size 1000000)
+ (format "%2.1fM" (/ size 1000000.0)))
+ ((and (>= size 1000) (< size 1000000))
+ (format "%2.1fK" (/ size 1000.0)))
+ ((< size 1000)
+ (format "%d" size))
+ (t "?")))
+
+(defun mu4e-split-ranges-to-numbers (str n)
+ "Convert STR containing attachment numbers into a list of numbers.
+
+STR is a string; N is the highest possible number in the list.
+This includes expanding e.g. 3-5 into 3,4,5. If the letter
+\"a\" ('all')) is given, that is expanded to a list with numbers
+[1..n]."
+ (let ((str-split (split-string str))
+ beg end list)
+ (dolist (elem str-split list)
+ ;; special number "a" converts into all attachments 1-N.
+ (when (equal elem "a")
+ (setq elem (concat "1-" (int-to-string n))))
+ (if (string-match "\\([0-9]+\\)-\\([0-9]+\\)" elem)
+ ;; we have found a range A-B, which needs converting
+ ;; into the numbers A, A+1, A+2, ... B.
+ (progn
+ (setq beg (string-to-number (match-string 1 elem))
+ end (string-to-number (match-string 2 elem)))
+ (while (<= beg end)
+ (cl-pushnew beg list :test 'equal)
+ (setq beg (1+ beg))))
+ ;; else just a number
+ (cl-pushnew (string-to-number elem) list :test 'equal)))
+ ;; Check that all numbers are valid.
+ (mapc
+ (lambda (x)
+ (cond
+ ((> x n)
+ (mu4e-warn "Attachment %d bigger than maximum (%d)" x n))
+ ((< x 1)
+ (mu4e-warn "Attachment number must be greater than 0 (%d)" x))))
+ list)))
+
+(defun mu4e-make-temp-file (ext)
+ "Create a self-destructing temporary file with extension EXT.
+The file will self-destruct in a short while, enough to open it
+in an external program."
+ (let ((tmpfile (make-temp-file "mu4e-" nil (concat "." ext))))
+ (run-at-time "30 sec" nil
+ (lambda () (ignore-errors (delete-file tmpfile))))
+ tmpfile))
+
+(defun mu4e-display-manual ()
+ "Display the mu4e manual page for the current mode.
+Or go to the top level if there is none."
+ (interactive)
+ (info (pcase major-mode
+ ('mu4e-main-mode "(mu4e)Main view")
+ ('mu4e-headers-mode "(mu4e)Headers view")
+ ('mu4e-view-mode "(mu4e)Message view")
+ (_ "mu4e"))))
+
+\f
+;;; bookmarks
+(defun mu4e--make-bookmark-record ()
+ "Create a bookmark for the message at point."
+ (let* ((msg (mu4e-message-at-point))
+ (subject (or (plist-get msg :subject) "No subject"))
+ (date (plist-get msg :date))
+ (date (if date (format-time-string "%F: " date) ""))
+ (title (format "%s%s" date subject))
+ (msgid (or (plist-get msg :message-id)
+ (mu4e-error
+ "Cannot bookmark message without message-id"))))
+ `(,title
+ ,@(bookmark-make-record-default 'no-file 'no-context)
+ (message-id . ,msgid)
+ (handler . mu4e--jump-to-bookmark))))
+
+(declare-function mu4e-view-message-with-message-id "mu4e-view")
+(declare-function mu4e-message-at-point "mu4e-message")
+
+(defun mu4e--jump-to-bookmark (bookmark)
+ "View the message referred to by BOOKMARK."
+ (when-let ((msgid (bookmark-prop-get bookmark 'message-id)))
+ (mu4e-view-message-with-message-id msgid)))
+
+\f;;; Macros
+
+(defmacro mu4e-setq-if-nil (var val)
+ "Set VAR to VAL if VAR is nil."
+ `(unless ,var (setq ,var ,val)))
+
+
+\f
+;;; Misc
+(defun mu4e-join-paths (directory &rest components)
+ "Append COMPONENTS to DIRECTORY and return the resulting string.
+
+This is mu4e's version of Emacs 28's `file-name-concat' with the
+difference it also handles slashes at the beginning of
+COMPONENTS."
+ (replace-regexp-in-string
+ "//+" "/"
+ (mapconcat (lambda (part) (if (stringp part) part ""))
+ (cons directory components) "/")))
+
+(defun mu4e-string-replace (from-string to-string in-string)
+ "Replace FROM-STRING with TO-STRING in IN-STRING each time it occurs.
+Mu4e version of emacs 28's string-replace."
+ (replace-regexp-in-string (regexp-quote from-string)
+ to-string in-string nil 'literal))
+
+(defun mu4e-plistp (object)
+ "Non-nil if and only if OBJECT is a valid plist.
+
+This is mu4e's version of Emacs 29's `plistp'."
+ (let ((len (proper-list-p object)))
+ (and len (zerop (% len 2)))))
+
+(defun mu4e-key-description (cmd)
+ "Get the textual form of current binding to interactive function CMD.
+If it is unbound, return nil. If there are multiple bindings,
+return the shortest.
+
+Roughly does what `substitute-command-keys' does, but picks
+shorter keys in some cases where there are multiple bindings."
+ ;; not a perfect heuristic: e.g. '<up>' is longer that 'C-p'
+ (car-safe
+ (seq-sort (lambda (b1 b2)
+ (< (length b1) (length b2)))
+ (seq-map #'key-description
+ (where-is-internal cmd)))))
+
+(provide 'mu4e-helpers)
+;;; mu4e-helpers.el ends here
--- /dev/null
+;;; mu4e-icalendar.el --- iCalendar & diary integration -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2019-2023 Christophe Troestler
+
+;; Author: Christophe Troestler <Christophe.Troestler@umons.ac.be>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Keywords: email icalendar
+;; Version: 0.0
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; To install:
+;; (require 'mu4e-icalendar)
+;; (gnus-icalendar-setup)
+;; Optional:
+;; (setq mu4e-icalendar-trash-after-reply t)
+
+;; By default, the original message is not cited. However, if you
+;; would like to reply to it, the citation is in the kill-ring (paste
+;; it with `yank').
+
+;; To add the event to a diary file of your choice:
+;; (setq mu4e-icalendar-diary-file "/path/to/your/diary")
+;; If the file specified is not your main diary file, add
+;; #include "/path/to/your/diary"
+;; to you main diary file to display the events.
+
+;; To enable optional iCalendar->Org sync functionality
+;; NOTE: both the capture file and the headline(s) inside must already exist
+;; (require 'org-agenda)
+;; (setq gnus-icalendar-org-capture-file "~/org/notes.org")
+;; (setq gnus-icalendar-org-capture-headline '("Calendar"))
+;; (gnus-icalendar-org-setup)
+
+;;; Code:
+
+(require 'gnus-icalendar)
+(require 'cl-lib)
+
+(require 'mu4e-mark)
+(require 'mu4e-helpers)
+(require 'mu4e-contacts)
+(require 'mu4e-headers)
+(require 'mu4e-obsolete)
+
+\f
+;;; Configuration
+;;;; Calendar
+
+(defgroup mu4e-icalendar nil
+ "Icalendar related settings."
+ :group 'mu4e)
+
+(defcustom mu4e-icalendar-trash-after-reply nil
+ "If non-nil, trash the icalendar invitation after replying."
+ :type 'boolean
+ :group 'mu4e-icalendar)
+
+(defcustom mu4e-icalendar-diary-file nil
+ "If non-nil, the file in which to add events upon reply."
+ :type '(choice (const :tag "Do not insert a diary entry" nil)
+ (string :tag "Insert a diary entry in this file"))
+ :group 'mu4e-icalendar)
+
+\f
+(defun mu4e--icalendar-has-email (email list)
+ "Check that EMAIL is in LIST."
+ (let ((email (downcase email)))
+ (cl-find-if (lambda (c) (let ((e (mu4e-contact-email c)))
+ (and (stringp e) (string= email (downcase e)))))
+ list)))
+
+(declare-function mu4e--view-mode-p "mu4e-view")
+(defun mu4e--icalendar-reply (orig data)
+ "Wrapper for using either `mu4e-icalender-reply' or the ORIG function."
+ (funcall (if (mu4e--view-mode-p) #'mu4e-icalendar-reply orig) data))
+
+(advice-add #'gnus-icalendar-reply :around #'mu4e--icalendar-reply)
+;;(advice-remove #'gnus-icalendar-reply #'mu4e--icalendar-reply)
+
+(defun mu4e-icalendar-reply (data)
+ "Reply to the text/calendar event present in DATA."
+ ;; Based on `gnus-icalendar-reply'.
+ (let* ((handle (car data))
+ (status (cadr data))
+ (event (caddr data))
+ (gnus-icalendar-additional-identities
+ (mu4e-personal-addresses 'no-regexp))
+ (reply (gnus-icalendar-with-decoded-handle
+ handle
+ (gnus-icalendar-event-reply-from-buffer
+ (current-buffer) status (gnus-icalendar-identities))))
+ (msg (mu4e-message-at-point 'noerror))
+ (charset (cdr (assoc 'charset (mm-handle-type handle)))))
+ (when reply
+ (cl-labels
+ ((fold-icalendar-buffer
+ ()
+ (goto-char (point-min))
+ (while (re-search-forward "^\\(.\\{72\\}\\)\\(.+\\)$" nil t)
+ (replace-match "\\1\n \\2")
+ (goto-char (line-beginning-position)))))
+
+ (let ((ical-name gnus-icalendar-reply-bufname))
+ (with-current-buffer (get-buffer-create ical-name)
+ (delete-region (point-min) (point-max))
+ (insert reply)
+ (fold-icalendar-buffer)
+ (when (and charset (string= (downcase charset) "utf-8"))
+ (decode-coding-region (point-min) (point-max) 'utf-8)))
+
+ (save-excursion ;; Compose the reply message.
+ (let* ((message-signature nil)
+ (organizer (gnus-icalendar-event:organizer event))
+ (organizer (when (and organizer
+ (not (string-empty-p organizer)))
+ organizer))
+ (organizer
+ (or organizer
+ (plist-get (car (plist-get msg :reply-to)) :email)
+ (plist-get (car (plist-get msg :from)) :email)
+ (mu4e-warn "Cannot find organizer")))
+ (message-cite-function #'mu4e-message-cite-nothing))
+ (mu4e-compose-reply-to organizer)
+ (message-goto-body)
+ (mml-insert-multipart "alternative")
+ (mml-insert-empty-tag 'part 'type "text/plain")
+ (mml-attach-buffer ical-name
+ "text/calendar; method=REPLY; charset=UTF-8")
+ (when mu4e-icalendar-trash-after-reply
+ ;; Override `mu4e-sent-handler' set by `mu4e-compose-mode' to
+ ;; also trash the message (thus must be appended to hooks).
+ (add-hook 'message-sent-hook
+ (mu4e--icalendar-trash-message-hook msg) 90 t))
+
+ (when gnus-icalendar-org-enabled-p
+ (if (gnus-icalendar-find-org-event-file event)
+ (gnus-icalendar--update-org-event event status)
+ (gnus-icalendar:org-event-save event status)))
+ (when mu4e-icalendar-diary-file
+ (mu4e--icalendar-insert-diary event status
+ mu4e-icalendar-diary-file)))))))))
+
+(declare-function mu4e-view-headers-next "mu4e-view")
+(defun mu4e--icalendar-trash-message (original-msg)
+ "Trash the message ORIGINAL-MSG and move to the next one."
+ (lambda (docid path)
+ "See `mu4e-sent-handler' for DOCID and PATH."
+ (mu4e-sent-handler docid path)
+ (let* ((docid (mu4e-message-field original-msg :docid))
+ (markdescr (assq 'trash mu4e-marks))
+ (action (plist-get (cdr markdescr) :action))
+ (target (mu4e-get-trash-folder original-msg)))
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (run-hook-with-args 'mu4e-mark-execute-pre-hook 'trash original-msg)
+ (funcall action docid original-msg target))
+ (when (and (mu4e~headers-view-this-message-p docid)
+ (buffer-live-p (mu4e-get-view-buffer)))
+ (mu4e-display-buffer (mu4e-get-view-buffer))
+ (or (mu4e-view-headers-next)
+ (kill-buffer-and-window))))))
+
+(defun mu4e--icalendar-trash-message-hook (original-msg)
+ "Trash the iCalendar message ORIGINAL-MSG."
+ (lambda ()
+ (setq mu4e-sent-func
+ (mu4e--icalendar-trash-message original-msg))))
+
+(defun mu4e--icalendar-insert-diary (event reply-status filename)
+ "Insert a diary entry for the EVENT in file named FILENAME.
+REPLY-STATUS is the status of the reply. The possible values are
+given in the doc of `gnus-icalendar-event-reply-from-buffer'."
+ ;; FIXME: handle recurring events
+ (let* ((beg (gnus-icalendar-event:start-time event))
+ (beg-date (format-time-string "%d/%m/%Y" beg))
+ (beg-time (format-time-string "%H:%M" beg))
+ (end (gnus-icalendar-event:end-time event))
+ (end-date (format-time-string "%d/%m/%Y" end))
+ (end-time (format-time-string "%H:%M" end))
+ (summary (gnus-icalendar-event:summary event))
+ (location (gnus-icalendar-event:location event))
+ (status (capitalize (symbol-name reply-status)))
+ (txt (if location
+ (format "%s (%s)\n %s " summary status location)
+ (format "%s (%s)" summary status))))
+ (with-temp-buffer
+ (if (string= beg-date end-date)
+ (insert beg-date " " beg-time "-" end-time " " txt "\n")
+ (insert beg-date " " beg-time " Start of: " txt "\n")
+ (insert beg-date " " end-time " End of: " txt "\n"))
+ (write-region (point-min) (point-max) filename t))))
+
+;;;
+(provide 'mu4e-icalendar)
+;;; mu4e-icalendar.el ends here
--- /dev/null
+;;; mu4e-lists.el --- Get names for mailing lists -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2023 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; In this file, we create a table of list-id -> shortname for mailing lists.
+;; The shortname (friendly) should a at most 8 characters, camel-case
+
+;;; Code:
+(require 'mu4e-message)
+(require 'mu4e-helpers)
+\f
+
+\f ;;; Helpers
+(defmacro mu4e-message-id-url(base-url)
+ "Construct lambda to get an archive URL for message.
+This is based on some BASE-URL to which the message-id is concatenated;
+e.g. public-inbox-based archives."
+ `(lambda (msg) (concat ,base-url "/" (plist-get msg :message-id))))
+
+(defmacro mu4e-x-seq-url (base-url)
+ "Construct x-seq archive URL for MSG or nil if not found."
+ `(lambda (msg)
+ (when-let ((xseq (mu4e-fetch-field msg "X-Seq")))
+ (concat ,base-url "/" xseq))))
+\f
+;;; Configuration
+(defvar mu4e-mailing-lists
+ `( (:list-id "bbdb-info.lists.sourceforge.net" :name "BBDB")
+ (:list-id "boost-announce.lists.boost.org" :name "Boost")
+ (:list-id "boost-interest.lists.boost.org" :name "Boost")
+ (:list-id "curl-library.cool.haxx.se" :name "Curl")
+ (:list-id "dbus.lists.freedesktop.org" :name "DBus")
+ (:list-id "desktop-devel-list.gnome.org" :name "Gnome")
+ (:list-id "discuss-webrtc.googlegroups.com" :name "WebRTC")
+ (:list-id "emacs-devel.gnu.org" :name "EmacsDev"
+ :archive ,(mu4e-message-id-url "https://yhetil.org/emacs-devel"))
+ (:list-id "emacs-orgmode.gnu.org" :name "Orgmode"
+ :archive ,(mu4e-message-id-url "https://list.orgmode.org"))
+ (:list-id "emms-help.gnu.org" :name "Emms")
+ (:list-id "gcc-help.gcc.gnu.org" :name "Gcc")
+ (:list-id "gmime-devel-list.gnome.org" :name "GMime")
+ (:list-id "gnome-shell-list.gnome.org" :name "Gnome")
+ (:list-id "gnu-emacs-sources.gnu.org" :name "Emacs")
+ (:list-id "gnupg-users.gnupg.org" :name "Gnupg")
+ (:list-id "gstreamer-devel.lists.freedesktop.org" :name "GstDev")
+ (:list-id "gtk-devel-list.gnome.org" :name "GtkDev")
+ (:list-id "guile-devel.gnu.org" :name "Guile"
+ :archive ,(mu4e-message-id-url "https://yhetil.org/guile-devel"))
+ (:list-id "guile-user.gnu.org" :name "Guile"
+ :archive ,(mu4e-message-id-url "https://yhetil.org/guile-user"))
+ (:list-id "help-gnu-emacs.gnu.org" :name "EmacsUsr"
+ :archive ,(mu4e-message-id-url "https://yhetil.org/emacs-user"))
+ (:list-id "mu-discuss.googlegroups.com" :name "Mu")
+ (:list-id "nautilus-list.gnome.org" :name "Nautilus")
+ (:list-id "notmuch.notmuchmail.org" :name "Notmuch"
+ :archive ,(mu4e-message-id-url "https://yhetil.org/notmuch"))
+ (:list-id "sqlite-announce.sqlite.org" :name "SQlite")
+ (:list-id "sqlite-dev.sqlite.org" :name "SQLite")
+ (:list-id "xapian-discuss.lists.xapian.org" :name "Xapian")
+ (:list-id "xdg.lists.freedesktop.org" :name "XDG")
+ (:list-id "wl-en.lists.airs.net" :name "WdrLust")
+ (:list-id "wl-en.ml.gentei.org" :name "WdrLust")
+ (:list-id "xapian-devel.lists.xapian.org" :name "Xapian")
+ (:list-id "zsh-users.zsh.org" :name "Zsh"
+ :archive ,(mu4e-x-seq-url "https://www.zsh.org/users")))
+ "List of plists with keys:
+- `:list-id' - the mailing list id
+- `:name' - the display name
+- `:archive' - (optional) a function taking a MSG and
+ returning an URL to to the online-location of
+ the message.
+After changes, use `mu4e-mailing-list-info-refresh' to update the
+corresponding data-structures.")
+
+(defgroup mu4e-lists nil "Configuration for mailing lists."
+ :group 'mu4e)
+
+(defcustom mu4e-user-mailing-lists nil
+ "A list with plists like `mu4e-mailing-lists'.
+These are used in addition to the built-in list
+`mu4e-mailing-lists'.
+
+The older format, a list of cons cells,
+ (LIST-ID . NAME)
+is still supported for backward compatibility.
+
+After changing, use `mu4e-mailing-list-info-refresh' to make mu4e
+use the new values."
+ :group 'mu4e-headers
+ :type '(repeat (plist)))
+
+(defcustom mu4e-mailing-list-patterns '("\\([^.]*\\)\\.")
+ "A list of regexps to capture a shortname out of a list-id.
+For the first regex that matches, its first match-group will be
+used as the shortname."
+ :group 'mu4e-headers
+ :type '(repeat (regexp)))
+
+(defvar mu4e--lists-hash nil
+ "Hash-table of list-id => plist.
+Based on `mu4e-mailing-lists' and `mu4e-user-mailing-lists'.")
+
+(defun mu4e-mailing-list-info-refresh ()
+ "Refresh the mailing list info.
+Based on the current value of `mu4e-mailing-lists' and
+`mu4e-user-mailing-lists'."
+ (interactive)
+ (setq mu4e--lists-hash (make-hash-table :test 'equal))
+ (seq-do (lambda (item)
+ (if (mu4e-plistp item)
+ ;; the new format
+ (puthash (plist-get item :list-id) item mu4e--lists-hash)
+ ;; backward compatibility
+ (puthash (car item) (cdr item) mu4e--lists-hash)))
+ (append mu4e-mailing-lists
+ mu4e-user-mailing-lists))
+ mu4e--lists-hash)
+
+(defun mu4e-mailing-list-info (list-id)
+ "Get mailing list info for LIST-ID.
+Return nil if not found."
+ (unless mu4e--lists-hash (mu4e-mailing-list-info-refresh))
+ (gethash list-id mu4e--lists-hash))
+\f
+
+(defun mu4e-get-mailing-list-shortname (list-id)
+ "Get the shortname for a mailing-list with list-id LIST-ID.
+Either we know about this mailing list, or otherwise
+we guess one."
+ (or ;; 1. perhaps we have it in one of our lists?
+ (plist-get (mu4e-mailing-list-info list-id) :name)
+ ;; 2. see if it matches some pattern
+ (if (seq-find (lambda (p) (string-match p list-id))
+ mu4e-mailing-list-patterns)
+ (match-string 1 list-id)
+ ;; 3. otherwise, just return the whole thing
+ list-id)))
+
+(defun mu4e-mailing-list-archive-url (&optional msg)
+ "Get the mailing-list archive URL for MSG.
+If MSG is nil, use the message at point."
+ (when-let* ((msg (or msg (mu4e-message-at-point)))
+ (list-id (plist-get msg :list))
+ (list-info (and list-id (mu4e-mailing-list-info list-id)))
+ (func (plist-get list-info :archive)))
+ (when func
+ (funcall func msg))))
+
+(provide 'mu4e-lists)
+;;; mu4e-lists.el ends here
--- /dev/null
+;;; mu4e-main.el --- The Main interface for mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2023 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'smtpmail)
+(require 'mu4e-helpers)
+(require 'mu4e-context)
+(require 'mu4e-compose)
+(require 'mu4e-bookmarks)
+(require 'mu4e-folders)
+(require 'mu4e-update)
+(require 'mu4e-contacts)
+(require 'mu4e-search)
+(require 'mu4e-vars) ;; mu-wide variables
+(require 'mu4e-window)
+(require 'mu4e-query-items)
+
+(declare-function mu4e-compose-new "mu4e-compose")
+(declare-function mu4e-quit "mu4e")
+
+(require 'cl-lib)
+
+\f
+;; Configuration
+
+(defcustom mu4e-main-hide-personal-addresses nil
+ "Whether to hide the personal address in the main view.
+
+ This can be useful to avoid the noise when there are many, and
+also hides the warning if your `user-mail-address' is not part of
+the personal addresses."
+ :type 'boolean
+ :group 'mu4e-main)
+
+(defcustom mu4e-main-hide-fully-read nil
+ "Whether to hide bookmarks or maildirs without unread messages."
+ :type 'boolean
+ :group 'mu4e-main)
+
+(defcustom mu4e-main-rendered-hook nil
+ "Hook run after the main-view has been rendered."
+ :type 'hook
+ :group 'mu4e-main)
+
+\f
+;;; Mode
+(define-derived-mode mu4e-org-mode org-mode "mu4e:org"
+ "Major mode for mu4e documents.")
+
+(defun mu4e-info (path)
+ "Show a buffer with the information (an org-file) at PATH."
+ (unless (file-exists-p path)
+ (mu4e-error "Cannot find %s" path))
+ (let ((curbuf (current-buffer)))
+ (find-file path)
+ (mu4e-org-mode)
+ (setq buffer-read-only t)
+ (define-key mu4e-org-mode-map (kbd "q")
+ `(lambda ()
+ (interactive)
+ (bury-buffer)
+ (switch-to-buffer ,curbuf)))))
+
+(defun mu4e-about ()
+ "Show the mu4e \"About\" page."
+ (interactive)
+ (mu4e-info (mu4e-join-paths mu4e-doc-dir "mu4e-about.org")))
+
+(defun mu4e-news ()
+ "Show page with news for the current version of mu4e."
+ (interactive)
+ (mu4e-info (mu4e-join-paths mu4e-doc-dir "NEWS.org")))
+
+(defun mu4e-baseline-time ()
+ "Show the baseline time."
+ (interactive)
+ (mu4e-message "Baseline time: %s" (mu4e--baseline-time-string)))
+
+(defun mu4e--baseline-time-string ()
+ "Calculate the baseline time string."
+ (let* ((baseline-t mu4e--query-items-baseline-tstamp)
+ (updated-t (plist-get mu4e-index-update-status :tstamp))
+ (delta-t (and baseline-t updated-t
+ (float-time (time-subtract updated-t baseline-t)))))
+ (if (and delta-t (> delta-t 0))
+ (format-seconds "%Y %D %H %M %z%S since latest" delta-t)
+ (if baseline-t
+ (current-time-string baseline-t)
+ "Never"))))
+
+(defvar mu4e-main-mode-map
+ (let ((map (make-sparse-keymap)))
+
+ (define-key map "q" #'mu4e-quit)
+ (define-key map "C" #'mu4e-compose-new)
+
+ (define-key map "m" #'mu4e--main-toggle-mail-sending-mode)
+ (define-key map "f" #'smtpmail-send-queued-mail)
+ ;;
+ (define-key map (kbd "C-S-u") #'mu4e-update-mail-and-index)
+ ;; for terminal users
+ (define-key map (kbd "C-c C-u") #'mu4e-update-mail-and-index)
+ (define-key map "U" #'mu4e-update-mail-and-index)
+ (define-key map "S" #'mu4e-kill-update-mail)
+ (define-key map ";" #'mu4e-context-switch)
+ (define-key map "$" #'mu4e-show-log)
+ (define-key map "A" #'mu4e-about)
+ (define-key map "N" #'mu4e-news)
+ (define-key map "H" #'mu4e-display-manual)
+ map)
+ "Keymap for the *mu4e-main* buffer.")
+
+(easy-menu-define mu4e-main-mode-menu
+ mu4e-main-mode-map "Menu for mu4e's main view."
+ (append
+ '("Mu4e" ;;:visible mu4e-headers-mode
+ "--"
+ ["Update mail and index" mu4e-update-mail-and-index]
+ ["Flush queued mail" smtpmail-send-queued-mail]
+ "--"
+ ["Show debug log" mu4e-show-log]
+ )
+ mu4e--compose-menu-items
+ mu4e--search-menu-items
+ '(
+ "--"
+ ["Quit" mu4e-quit :help "Quit mu4e"])))
+
+(declare-function mu4e--server-bookmarks-queries "mu4e")
+
+(define-derived-mode mu4e-main-mode special-mode "mu4e:main"
+ "Major mode for the mu4e main screen.
+
+This mode is a bit special when it comes to keybinding, since it
+shows those keybindings.
+
+For the rebinding the mu4e functions (such as
+`mu4e-search-bookmark' and `mu4e-search-maildir') to different
+keys, note that mu4e determines the bindings when drawing the
+screen, which is *after* we enable the mode. Thus, the
+keybindings must be known when this happens.
+
+Binding the existing bindings (such as \='s') to different
+functions, is *not* really supported, and we still display the
+default binding for the original function; which should still do
+the reasonable thing in most cases.
+
+Still, such a rebinding *only* affects the key, and not e.g. the
+mouse-bindings."
+ (setq truncate-lines t
+ overwrite-mode 'overwrite-mode-binary)
+ (mu4e-context-minor-mode)
+ (mu4e-search-minor-mode)
+ (mu4e-update-minor-mode)
+ (setq-local revert-buffer-function
+ (lambda (_ignore-auto _noconfirm)
+ ;; reset the baseline and get updated results.
+ (mu4e--query-items-refresh 'reset-baseline))))
+
+
+(defun mu4e--main-action (title cmd &optional bindstr alt)
+ "Produce main view action string with TITLE.
+
+When activated, invoke interactive function CMD.
+
+In the result, used the TITLE string, with the first occurrence
+of [@] replaced by a textual replacement of a binding to CMD as
+per `mu4e-key-description', or, if specified, BINDSTR.
+
+If a string ALT is specified, and BINDSTR is longer than a single
+character, use ALT as a substitute. ALT should be a string of
+length 1.
+
+If the first letter after the [@] is equal to the last letter of the
+binding representation, remove that first letter."
+ (let* ((bindstr (or bindstr (mu4e-key-description cmd) alt
+ (mu4e-error "No binding for %s" cmd)))
+ (bindstr
+ (if (and alt (> (length bindstr) 1)) alt bindstr))
+ (title ;; remove first letter afrer [] if it equal last of binding
+ (mu4e-string-replace
+ (concat "[@]" (substring bindstr -1)) "[@]" title))
+ (title ;; insert binding in [@]
+ (mu4e-string-replace
+ "[@]" (format "[%s]" (propertize bindstr 'face 'mu4e-highlight-face))
+ title))
+ (map (make-sparse-keymap)))
+ (define-key map [mouse-2] cmd)
+ (define-key map (kbd "RET") cmd)
+ (propertize title 'keymap map)))
+
+(defun mu4e--main-items (item-type max-length)
+ "Produce the string with menu-items for ITEM-TYPE.
+ITEM-TYPE is a symbol, either `bookmarks' or `maildirs'.
+
+MAX-LENGTH is the maximum length of the item titles; this is used
+for aligning them."
+ (mapconcat
+ (lambda (item)
+ (cl-destructuring-bind
+ (&key hide name key favorite query &allow-other-keys) item
+ ;; hide items explicitly hidden, without key or wrong category.
+ (if hide
+ ""
+ (let ((item-info
+ ;; note, we have a function for the binding,
+ ;; and perhaps a different one for the lambda.
+ (cond
+ ((eq item-type 'maildirs)
+ (list #'mu4e-search-maildir #'mu4e-search
+ query))
+ ((eq item-type 'bookmarks)
+ (list #'mu4e-search-bookmark #'mu4e-search-bookmark
+ (mu4e-get-bookmark-query key)))
+ (t
+ (mu4e-error "Invalid item-type %s" item-type)))))
+ (concat
+ (mu4e--main-action
+ ;; main title
+ (format "\t* [@] %s "
+ (propertize
+ name
+ 'face (if favorite 'mu4e-header-key-face nil)
+ 'help-echo query))
+ ;; function to call when activated
+ (lambda () (interactive)
+ (funcall (nth 1 item-info)
+ (nth 2 item-info)))
+ ;; custom key binding string
+ (concat (mu4e-key-description (nth 0 item-info)) (string key)))
+ ;; counts
+ (format "%s%s\n"
+ (make-string (- max-length (string-width name)) ?\s)
+ (mu4e--query-item-display-counts item)))))))
+ ;; only items which have a single-character :key
+ (mu4e-filter-single-key (mu4e-query-items item-type)) ""))
+
+(defun mu4e--key-val (key val &optional unit)
+ "Show a KEY / VAL pair, with optional UNIT."
+ (concat
+ "\t* "
+ (propertize (format "%-20s" key) 'face 'mu4e-header-title-face)
+ ": "
+ (propertize val 'face 'mu4e-header-key-face)
+ (if unit
+ (propertize (concat " " unit) 'face 'mu4e-header-title-face)
+ "")
+ "\n"))
+
+(defun mu4e--main-baseline-time-string ()
+ "Calculate the baseline time string for use in the main-"
+ (let* ((baseline-t mu4e--query-items-baseline-tstamp)
+ (updated-t (plist-get mu4e-index-update-status :tstamp))
+ (delta-t (and baseline-t updated-t
+ (float-time (time-subtract updated-t baseline-t)))))
+ (if (and delta-t (> delta-t 0))
+ (format-seconds "%Y %D %H %M %z%S ago" delta-t)
+ (if baseline-t
+ (current-time-string baseline-t)
+ "Never"))))
+
+(defun mu4e--main-redraw ()
+ "Redraw the main buffer if there is one.
+Otherwise, do nothing."
+ (when-let* ((buffer (get-buffer mu4e-main-buffer-name))
+ (buffer (and (buffer-live-p buffer) buffer)))
+ (with-current-buffer buffer
+ (let* ((inhibit-read-only t)
+ (pos (point))
+ (addrs (mu4e-personal-addresses))
+ (max-length (seq-reduce (lambda (a b)
+ (max a (length (plist-get b :name))))
+ (mu4e-query-items) 0)))
+ (mu4e-main-mode)
+ (erase-buffer)
+ (insert
+ "* "
+ (propertize "mu4e" 'face 'mu4e-header-key-face)
+ (propertize " - mu for emacs version " 'face 'mu4e-title-face)
+ (propertize mu4e-mu-version 'face 'mu4e-header-key-face)
+ "\n\n"
+ (propertize " Basics\n\n" 'face 'mu4e-title-face)
+ (mu4e--main-action
+ "\t* [@]jump to some maildir\n" #'mu4e-search-maildir nil "j")
+ (mu4e--main-action
+ "\t* enter a [@]search query\n" #'mu4e-search nil "s")
+ (mu4e--main-action
+ "\t* [@]Compose a new message\n" #'mu4e-compose-new nil "C")
+ "\n"
+ (propertize " Bookmarks\n\n" 'face 'mu4e-title-face)
+ (mu4e--main-items 'bookmarks max-length)
+ "\n"
+ (propertize " Maildirs\n\n" 'face 'mu4e-title-face)
+ (mu4e--main-items 'maildirs max-length)
+ "\n"
+ (propertize " Misc\n\n" 'face 'mu4e-title-face)
+ (mu4e--main-action "\t* [@]Choose query\n"
+ #'mu4e-search-query nil "c")
+ (mu4e--main-action "\t* [@]Switch context\n"
+ #'mu4e-context-switch nil ";")
+ (mu4e--main-action "\t* [@]Update email & database\n"
+ #'mu4e-update-mail-and-index nil "U")
+ ;; show the queue functions if `smtpmail-queue-dir' is defined
+ (if (file-directory-p smtpmail-queue-dir)
+ (mu4e--main-view-queue)
+ "")
+ "\n"
+ (mu4e--main-action "\t* [@]News\n" #'mu4e-news nil "N")
+ (mu4e--main-action "\t* [@]About mu4e\n" #'mu4e-about nil "A")
+ (mu4e--main-action "\t* [@]Help\n" #'mu4e-display-manual nil "H")
+ (mu4e--main-action "\t* [@]quit\n" #'mu4e-quit nil "q")
+ "\n"
+ (propertize " Info\n\n" 'face 'mu4e-title-face)
+ (mu4e--key-val "last updated"
+ (current-time-string
+ (plist-get mu4e-index-update-status :tstamp)))
+ (mu4e--key-val "database-path" (mu4e-database-path))
+ (mu4e--key-val "maildir" (mu4e-root-maildir))
+ (mu4e--key-val "in store"
+ (format "%d" (plist-get mu4e--server-props :doccount))
+ "messages")
+ (if mu4e-main-hide-personal-addresses ""
+ (mu4e--key-val "personal addresses"
+ (if addrs (mapconcat #'identity addrs ", " ) "none"))))
+
+ (if mu4e-main-hide-personal-addresses ""
+ (unless (mu4e-personal-address-p user-mail-address)
+ (mu4e-message (concat
+ "Tip: `user-mail-address' ('%s') is not part "
+ "of mu's addresses; add it with 'mu init
+ --my-address='") user-mail-address)))
+ (goto-char pos)))))
+
+(defun mu4e--main-view-queue ()
+ "Display queue-related actions in the main view."
+ (concat
+ (mu4e--main-action "\t* toggle [@]mail sending mode "
+ #'mu4e--main-toggle-mail-sending-mode)
+ "(currently "
+ (propertize (if smtpmail-queue-mail "queued" "direct")
+ 'face 'mu4e-header-key-face)
+ ")\n"
+ (let ((queue-size (mu4e--main-queue-size)))
+ (if (zerop queue-size)
+ ""
+ (mu4e--main-action
+ (format "\t* [@]flush %s queued %s\n"
+ (propertize (int-to-string queue-size)
+ 'face 'mu4e-header-key-face)
+ (if (> queue-size 1) "mails" "mail"))
+ 'smtpmail-send-queued-mail)))))
+
+(defun mu4e--main-queue-size ()
+ "Return, as an int, the number of emails in the queue."
+ (condition-case nil
+ (with-temp-buffer
+ (insert-file-contents (expand-file-name smtpmail-queue-index-file
+ smtpmail-queue-dir))
+ (count-lines (point-min) (point-max)))
+ (error 0)))
+
+(declare-function mu4e--start "mu4e")
+
+(defun mu4e--main-view ()
+ "(Re)create the mu4e main-view, and switch to it.
+
+If `mu4e-split-view' equals \\='single-window, show a mu4e menu
+instead."
+ (if (eq mu4e-split-view 'single-window)
+ (mu4e--main-menu)
+ (let ((buf (get-buffer-create mu4e-main-buffer-name))
+ (inhibit-read-only t))
+ (with-current-buffer buf
+ (mu4e--main-redraw))
+ (mu4e-display-buffer buf t)
+ (run-hooks 'mu4e-main-rendered-hook)))
+ (goto-char (point-min)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Interactive functions
+;; Toggle mail sending mode without switching
+(defun mu4e--main-toggle-mail-sending-mode ()
+ "Toggle sending mail mode, either queued or direct."
+ (interactive)
+ (unless (file-directory-p smtpmail-queue-dir)
+ (mu4e-error "`smtpmail-queue-dir' does not exist"))
+ (setq smtpmail-queue-mail (not smtpmail-queue-mail))
+ (message (concat "Outgoing mail will now be "
+ (if smtpmail-queue-mail "queued" "sent directly")))
+ (unless (or (eq mu4e-split-view 'single-window)
+ (not (buffer-live-p (get-buffer mu4e-main-buffer-name))))
+ (mu4e--main-redraw)))
+
+(defun mu4e--main-menu ()
+ "The mu4e main menu in the mini-buffer."
+ (let ((func (mu4e-read-option
+ "Do: "
+ '(("jump" . mu4e~headers-jump-to-maildir)
+ ("search" . mu4e-search)
+ ("Compose" . mu4e-compose-new)
+ ("bookmarks" . mu4e-search-bookmark)
+ (";Switch context" . mu4e-context-switch)
+ ("Update" . mu4e-update-mail-and-index)
+ ("News" . mu4e-news)
+ ("About" . mu4e-about)
+ ("Help " . mu4e-display-manual)))))
+ (call-interactively func)
+ (when (eq func 'mu4e-context-switch)
+ (sit-for 1)
+ (mu4e--main-menu))))
+
+(provide 'mu4e-main)
+;;; mu4e-main.el ends here
--- /dev/null
+;;; mu4e-mark.el --- Marking messages -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2024 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; In this file are function related to marking messages; they assume we are
+;; currently in the headers buffer.
+
+;;; Code:
+
+(require 'mu4e-server)
+(require 'mu4e-message)
+(require 'mu4e-folders)
+
+;; keep byte-compiler happy
+(declare-function mu4e~headers-mark "mu4e-headers")
+(declare-function mu4e~headers-goto-docid "mu4e-headers")
+(declare-function mu4e-headers-next "mu4e-headers")
+
+;;; Variables & constants
+
+(defcustom mu4e-headers-leave-behavior 'ask
+ "What to do when user leaves the current headers view.
+
+\"Leaving\" here means quitting the headers views, refreshing it
+or even quitting mu4e or Emacs.
+
+Value is one of the following symbols:
+- `ask' ask user whether to ignore the marks
+- `apply' automatically apply the marks before doing anything else
+- `ignore' automatically ignore the marks without asking"
+ :type '(choice (const :tag "ask user whether to ignore marks" ask)
+ (const :tag "apply marks without asking" apply)
+ (const :tag "ignore marks without asking" ignore))
+ :group 'mu4e-headers)
+
+(defcustom mu4e-mark-execute-pre-hook nil
+ "Hook run just *before* a mark is applied to a message.
+The hook function is called with two arguments, the mark being
+executed and the message itself."
+ :type 'hook
+ :group 'mu4e-headers)
+
+(defvar mu4e-headers-show-target t
+ "Whether to show targets (such as \"-> delete\", \"-> /archive\")
+when marking message. Normally, this is useful information for
+the user, however, when you often mark large numbers (thousands)
+of message, showing the target makes this quite a bit
+slower (showing the target uses Emacs overlays, which can be slow
+when overused).")
+
+;;; Insert stuff
+
+(defvar mu4e--mark-map nil
+ "Contains a mapping of docid->markinfo.
+When a message is marked, the information is added here. markinfo
+is a cons cell consisting of the following: (mark . target) where
+MARK is the type of mark (move, trash, delete) TARGET (optional)
+is the target directory (for \"move\")")
+
+;; the mark-map is specific for the current header buffer
+;; currently, there can't be more than one, but we never know what will
+;; happen in the future
+
+;; the fringe is the space on the left of headers, where we put marks below some
+;; handy definitions; only `mu4e-mark-fringe-len' should be change (if ever),
+;; the others follow from that.
+(defconst mu4e--mark-fringe-len 2
+ "Width of the fringe for marks on the left.")
+(defconst mu4e--mark-fringe (make-string mu4e--mark-fringe-len ?\s)
+ "The space on the left of message headers to put marks.")
+(defconst mu4e--mark-fringe-format (format "%%-%ds" mu4e--mark-fringe-len)
+ "Format string to set a mark and leave remaining space.")
+
+(defun mu4e--mark-initialize ()
+ "Initialize the marks-subsystem."
+ (set (make-local-variable 'mu4e--mark-map) (make-hash-table))
+ ;; ask user when kill buffer / emacs with live marks.
+ ;; (subject to mu4e-headers-leave-behavior)
+ (add-hook 'kill-buffer-query-functions
+ #'mu4e-mark-handle-when-leaving nil t)
+ (add-hook 'kill-emacs-query-functions
+ #'mu4e-mark-handle-when-leaving nil t))
+
+(defun mu4e--mark-clear ()
+ "Clear the marks-subsystem."
+ (clrhash mu4e--mark-map))
+
+(defun mu4e--mark-find-headers-buffer ()
+ "Find the headers buffer, if any."
+ (seq-find (lambda (_)
+ (mu4e-current-buffer-type-p 'headers))
+ (buffer-list)))
+
+(defmacro mu4e--mark-in-context (&rest body)
+ "Evaluate BODY in the context of the headers buffer.
+The current buffer must be either a headers or view buffer."
+ `(cond
+ ((mu4e-current-buffer-type-p 'headers) ,@body)
+ ((mu4e-current-buffer-type-p 'view)
+ (when (buffer-live-p (mu4e-get-headers-buffer))
+ (let* ((msg (mu4e-message-at-point))
+ (docid (mu4e-message-field msg :docid)))
+ (with-current-buffer (mu4e-get-headers-buffer)
+ (when (mu4e~headers-goto-docid docid)
+ ,@body
+ )))))))
+
+(defconst mu4e-marks
+ '((refile
+ :char ("r" . "▶")
+ :prompt "refile"
+ :dyn-target (lambda (target msg) (mu4e-get-refile-folder msg))
+ :action (lambda (docid msg target)
+ (mu4e--server-move docid (mu4e--mark-check-target target) "-N")))
+ (delete
+ :char ("D" . "x")
+ :prompt "Delete"
+ :show-target (lambda (target) "delete")
+ :action (lambda (docid msg target) (mu4e--server-remove docid)))
+ (flag
+ :char ("+" . "✚")
+ :prompt "+flag"
+ :show-target (lambda (target) "flag")
+ :action (lambda (docid msg target)
+ (mu4e--server-move docid nil "+F-u-N")))
+ (move
+ :char ("m" . "▷")
+ :prompt "move"
+ :ask-target mu4e--mark-get-move-target
+ :action (lambda (docid msg target)
+ (mu4e--server-move docid (mu4e--mark-check-target target) "-N")))
+ (read
+ :char ("!" . "◼")
+ :prompt "!read"
+ :show-target (lambda (target) "read")
+ :action (lambda (docid msg target) (mu4e--server-move docid nil "+S-u-N")))
+ (trash
+ :char ("d" . "▼")
+ :prompt "dtrash"
+ :dyn-target (lambda (target msg) (mu4e-get-trash-folder msg))
+ :action (lambda (docid msg target)
+ (mu4e--server-move docid
+ (mu4e--mark-check-target target) "+T-N")))
+ (unflag
+ :char ("-" . "➖")
+ :prompt "-unflag"
+ :show-target (lambda (target) "unflag")
+ :action (lambda (docid msg target) (mu4e--server-move docid nil "-F-N")))
+ (untrash
+ :char ("=" . "▲")
+ :prompt "=untrash"
+ :show-target (lambda (target) "untrash")
+ :action (lambda (docid msg target) (mu4e--server-move docid nil "-T")))
+ (unread
+ :char ("?" . "◻")
+ :prompt "?unread"
+ :show-target (lambda (target) "unread")
+ :action (lambda (docid msg target) (mu4e--server-move docid nil "-S+u-N")))
+ (unmark
+ :char " "
+ :prompt "unmark"
+ :action (mu4e-error "No action for unmarking"))
+ (action
+ :char ( "a" . "◯")
+ :prompt "action"
+ :ask-target (lambda () (mu4e-read-option "Action: " mu4e-headers-actions))
+ :action (lambda (docid msg actionfunc)
+ (save-excursion
+ (when (mu4e~headers-goto-docid docid)
+ (mu4e-headers-action actionfunc)))))
+ (something
+ :char ("*" . "✱")
+ :prompt "*something"
+ :action (mu4e-error "No action for deferred mark")))
+
+ "The list of all the possible marks.
+This is an alist mapping mark symbols to their properties. The
+properties are:
+ :char (string) or (basic . fancy) The character to display in
+ the headers view. Either a single-character string, or a
+ dotted-pair cons cell where the second item will be used if
+ `mu4e-use-fancy-chars' is t, otherwise we'll use
+ the first one. It can also be a plain string for backwards
+ compatibility since we didn't always support
+ `mu4e-use-fancy-chars' here.
+ :prompt (string) The prompt to use when asking for marks (used for
+ example when marking a whole thread)
+ :ask-target (function returning a string) Get the target. This
+ function run once per bulk-operation, and thus is suitable
+ for user-interaction. If nil, the target is nil.
+ :dyn-target (function from (TARGET MSG) to string). Compute
+ the dynamic target. This is run once per message, which is
+ passed as MSG. The default is to just return the target.
+ :show-target (function from TARGET to string) How to display
+ the target.
+ :action (function taking (DOCID MSG TARGET)). The action to
+ apply on the message.")
+
+(defun mu4e-mark-at-point (mark target)
+ "Mark (or unmark) message at point.
+MARK specifies the mark-type. For `move'-marks and `trash'-marks
+the TARGET argument is non-nil and specifies to which maildir the
+message is to be moved/trashed. The function works in both
+headers buffers and message buffers.
+
+The following marks are available, and the corresponding props:
+
+ MARK TARGET description
+ ----------------------------------------------------------
+ `refile' y mark this message for archiving
+ `something' n mark this message for *something* (decided later)
+ `delete' n remove the message
+ `flag' n mark this message for flagging
+ `move' y move the message to some folder
+ `read' n mark the message as read
+ `trash' y trash the message to some folder
+ `unflag' n mark this message for unflagging
+ `untrash' n remove the `trashed' flag from a message
+ `unmark' n unmark this message
+ `unread' n mark the message as unread
+ `action' y mark the message for some action."
+ (interactive)
+ (let* ((msg (mu4e-message-at-point))
+ (docid (mu4e-message-field msg :docid))
+ ;; get a cell with the mark char and the "move" already has a target
+ ;; (the target folder) the other ones get a pseudo "target", as info
+ ;; for the user.
+ (markdesc (cdr (or (assq mark mu4e-marks)
+ (mu4e-error "Invalid mark %S" mark))))
+ (get-markkar
+ (lambda (char)
+ (if (listp char)
+ (if mu4e-use-fancy-chars (cdr char) (car char))
+ char)))
+ (markkar (funcall get-markkar (plist-get markdesc :char)))
+ (target (mu4e--mark-get-dyn-target mark target))
+ (show-fct (plist-get markdesc :show-target))
+ (shown-target (if show-fct
+ (funcall show-fct target)
+ (if target (format "%S" target)))))
+ (unless docid (mu4e-warn "No message on this line"))
+ (unless (eq major-mode 'mu4e-headers-mode)
+ (mu4e-error "Not in headers-mode"))
+ (save-excursion
+ (when (mu4e~headers-mark docid markkar)
+ ;; update the hash -- remove everything current, and if add the new
+ ;; stuff, unless we're unmarking
+ (remhash docid mu4e--mark-map)
+ ;; remove possible mark overlays
+ (remove-overlays (line-beginning-position) (line-end-position)
+ 'mu4e-mark t)
+ ;; now, let's set a mark (unless we were unmarking)
+ (unless (eql mark 'unmark)
+ (puthash docid (cons mark target) mu4e--mark-map)
+ ;; when we have a target (ie., when moving), show the target folder in
+ ;; an overlay
+ (when (and shown-target mu4e-headers-show-target)
+ (let* ((targetstr (propertize (concat "-> " shown-target " ")
+ 'face 'mu4e-system-face))
+ ;; mu4e~headers-goto-docid docid t \will take us just after
+ ;; the docid cookie and then we skip the mu4e--mark-fringe
+ (start (+ (length mu4e--mark-fringe)
+ (mu4e~headers-goto-docid docid t)))
+ (overlay (make-overlay start (+ start (length targetstr)))))
+ (overlay-put overlay 'display targetstr)
+ (overlay-put overlay 'mu4e-mark t)
+ (overlay-put overlay 'evaporate t)
+ docid)))))))
+
+(defun mu4e--mark-get-move-target ()
+ "Ask for a move target, and propose to create it if it does not exist."
+ (let* ((target (mu4e-ask-maildir "Move message to: "))
+ (target (if (string= (substring target 0 1) "/")
+ target
+ (concat "/" target)))
+ (fulltarget (mu4e-join-paths (mu4e-root-maildir) target)))
+ (when (mu4e-create-maildir-maybe fulltarget)
+ target)))
+
+(defun mu4e--mark-ask-target (mark)
+ "Ask the target for MARK, if the user should be asked the target."
+ (let ((getter (plist-get (cdr (assq mark mu4e-marks)) :ask-target)))
+ (and getter (funcall getter))))
+
+(defun mu4e--mark-get-dyn-target (mark target)
+ "Get the dynamic TARGET for MARK.
+The result may depend on the message at point."
+ (let ((getter (plist-get (cdr (assq mark mu4e-marks)) :dyn-target)))
+ (if getter
+ (funcall getter target (mu4e-message-at-point))
+ target)))
+
+(defun mu4e-mark-set (mark &optional target)
+ "Mark the header at point with MARK or all in the region.
+Optionally, provide TARGET (for moves)."
+ (unless target
+ (setq target (mu4e--mark-ask-target mark)))
+ (if (not (use-region-p))
+ ;; single message
+ (mu4e-mark-at-point mark target)
+ ;; mark all messages in the region.
+ (save-excursion
+ (let ((cant-go-further) (eor (region-end)))
+ (goto-char (region-beginning))
+ (while (and (< (point) eor) (not cant-go-further))
+ (mu4e-mark-at-point mark target)
+ (setq cant-go-further (not (mu4e-headers-next))))))))
+
+(defun mu4e-mark-restore (docid)
+ "Restore the visual mark for the message with DOCID."
+ (let ((markcell (gethash docid mu4e--mark-map)))
+ (when markcell
+ (save-excursion
+ (when (mu4e~headers-goto-docid docid)
+ (mu4e-mark-at-point (car markcell) (cdr markcell)))))))
+
+(defun mu4e--mark-get-markpair (prompt &optional allow-something)
+ "Ask user with PROMPT for a mark and return (MARK . TARGET).
+If ALLOW-SOMETHING is non-nil, allow the `something' pseudo mark
+as well."
+ (let* ((marks (mapcar (lambda (markdescr)
+ (cons (plist-get (cdr markdescr) :prompt)
+ (car markdescr)))
+ mu4e-marks))
+ (marks
+ (if allow-something
+ marks (seq-remove (lambda (m) (eq 'something (cdr m))) marks)))
+ (mark (mu4e-read-option prompt marks))
+ (target (mu4e--mark-ask-target mark)))
+ (cons mark target)))
+
+(defun mu4e-mark-resolve-deferred-marks ()
+ "Check if there are any deferred ('something') mark-instances.
+If there are such marks, replace them with a _real_ mark (ask the
+user which one)."
+ (interactive)
+ (mu4e--mark-in-context
+ (let ((markpair))
+ (maphash
+ (lambda (docid val)
+ (let ((mark (car val)))
+ (when (eql mark 'something)
+ (unless markpair
+ (setq markpair
+ (mu4e--mark-get-markpair "Set deferred mark(s) to: " nil)))
+ (save-excursion
+ (when (mu4e~headers-goto-docid docid)
+ (mu4e-mark-set (car markpair) (cdr markpair)))))))
+ mu4e--mark-map))))
+
+(defun mu4e--mark-check-target (target)
+ "Check if TARGET exists; if not, offer to create it."
+ (let ((fulltarget (mu4e-join-paths (mu4e-root-maildir) target)))
+ (if (not (mu4e-create-maildir-maybe fulltarget))
+ (mu4e-error "Target dir %s does not exist " fulltarget)
+ target)))
+
+(defun mu4e-mark-execute-all (&optional no-confirmation)
+ "Execute the actions for all marked messages in this buffer.
+After the actions have been executed successfully, the affected
+messages are *hidden* from the current header list. Since the
+headers are the result of a search, we cannot be certain that the
+messages no longer match the current one - to get that
+certainty, we need to rerun the search, but we don't want to do
+that automatically, as it may be too slow and/or break the user's
+flow. Therefore, we hide the message, which in practice seems to
+work well.
+
+If NO-CONFIRMATION is non-nil, don't ask user for confirmation."
+ (interactive "P")
+ (mu4e--mark-in-context
+ (let* ((marknum (mu4e-mark-marks-num))
+ (prompt (format "Are you sure you want to execute %d mark%s?"
+ marknum (if (> marknum 1) "s" ""))))
+ (if (zerop marknum)
+ (mu4e-warn "Nothing is marked")
+ (mu4e-mark-resolve-deferred-marks)
+ (when (or no-confirmation (y-or-n-p prompt))
+ (maphash
+ (lambda (docid val)
+ (let* ((mark (car val)) (target (cdr val))
+ (markdescr (assq mark mu4e-marks))
+ (msg (save-excursion
+ (mu4e~headers-goto-docid docid)
+ (mu4e-message-at-point))))
+ ;; note: whenever you do something with the message,
+ ;; it looses its N (new) flag
+ (if markdescr
+ (progn
+ (run-hook-with-args
+ 'mu4e-mark-execute-pre-hook mark msg)
+ (funcall (plist-get (cdr markdescr) :action)
+ docid msg target))
+ (mu4e-error "Unrecognized mark %S" mark))))
+ mu4e--mark-map))
+ (mu4e-mark-unmark-all 'no-confirm)
+ (message nil)))))
+
+(defun mu4e-mark-unmark-all (&optional no-confirmation)
+ "Unmark all marked messages."
+ (interactive)
+ (mu4e--mark-in-context
+ (when (zerop (mu4e-mark-marks-num))
+ (mu4e-warn "Nothing is marked"))
+ (let* ((marknum (hash-table-count mu4e--mark-map))
+ (prompt (format "Are you sure you want to unmark %d message%s?"
+ marknum (if (> marknum 1) "s" ""))))
+ (when (or no-confirmation (y-or-n-p prompt))
+ (maphash
+ (lambda (docid _val)
+ (save-excursion
+ (when (mu4e~headers-goto-docid docid)
+ (mu4e-mark-set 'unmark))))
+ mu4e--mark-map)
+ ;; in any case, clear the marks map
+ (mu4e--mark-clear)))))
+
+(defun mu4e-mark-docid-marked-p (docid)
+ "Is the given DOCID marked?"
+ (when (gethash docid mu4e--mark-map) t))
+
+(defun mu4e-mark-marks-num ()
+ "Return the number of mark-instances in the current buffer."
+ (mu4e--mark-in-context
+ (if mu4e--mark-map (hash-table-count mu4e--mark-map) 0)))
+
+(defun mu4e-mark-handle-when-leaving ()
+ "Handle any mark-instances in the current buffer when leaving.
+This is done according to the value of
+`mu4e-headers-leave-behavior'. This function is to be called
+before any further action (like searching, quitting the buffer)
+is taken; returning t means \"take the following action\", return
+nil means \"don't do anything\"."
+ (mu4e--mark-in-context
+ (let ((marknum (mu4e-mark-marks-num))
+ (what mu4e-headers-leave-behavior))
+ (unless (zerop marknum) ;; nothing to do?
+ (when (eq what 'ask)
+ (setq what (mu4e-read-option
+ (format "There are %d existing mark(s); should we: "
+ marknum)
+ '( ("apply marks" . apply)
+ ("ignore marks?" . ignore)))))
+ ;; we determined what to do... now do it
+ (when (eq what 'apply)
+ (mu4e-mark-execute-all t)))))
+ t) ;; return t for compat with `kill-buffer-query-functions
+
+;;; _
+(provide 'mu4e-mark)
+;;; mu4e-mark.el ends here
--- /dev/null
+;;; mu4e-message.el --- Working with mu4e-message plists -*- lexical-binding: t -*-
+
+;; Copyright (C) 2012-2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Functions to get data from mu4e-message plist structure
+
+;;; Code:
+
+(require 'mu4e-vars)
+(require 'mu4e-contacts)
+(require 'mu4e-window)
+(require 'flow-fill)
+(require 'shr)
+(require 'pp)
+
+(declare-function mu4e-error "mu4e-helpers")
+(declare-function mu4e-warn "mu4e-helpers")
+(declare-function mu4e-personal-address-p "mu4e-contacts")
+(declare-function mu4e-make-temp-file "mu4e-helpers")
+
+;;; Message fields
+
+(defsubst mu4e-message-field-raw (msg field)
+ "Retrieve FIELD from message plist MSG.
+
+See \"mu fields\" for the full list of field, in particular the
+\"sexp\" column.
+
+Returns nil if the field does not exist.
+
+A message plist looks something like:
+\(:docid 32461
+ :from ((:name \"Nikola Tesla\" :email \"niko@example.com\"))
+ :to ((:name \"Thomas Edison\" :email \"tom@example.com\"))
+ :cc ((:name \"Rupert The Monkey\" :email \"rupert@example.com\"))
+ :subject \"RE: what about the 50K?\"
+ :date (20369 17624 0)
+ :size 4337
+ :message-id \"238C8233AB82D81EE81AF0114E4E74@123213.mail.example.com\"
+ :path \"/home/tom/Maildir/INBOX/cur/133443243973_1.10027.atlas:2,S\"
+ :maildir \"/INBOX\"
+ :priority normal
+ :flags (seen)
+\)).
+Some notes on the format:
+- The address fields are lists of plist (:name NAME :email EMAIL),
+ where the :name part can be absent. The `mu4e-contact-name' and
+ `mu4e-contact-email' accessors can be useful for this.
+- The date is in format emacs uses in `current-time'
+- Attachments are a list of elements with fields :index (the number of
+ the MIME-part), :name (the file name, if any), :mime-type (the
+ MIME-type, if any) and :size (the size in bytes, if any).
+- Messages in the Headers view come from the database and do not have
+ :attachments, :body-txt or :body-html fields. Message in the
+ Message view use the actual message file, and do include these fields."
+ ;; after all this documentation, the spectacular implementation
+ (if msg
+ (plist-get msg field)
+ (mu4e-error "Message must be non-nil")))
+
+(defsubst mu4e-message-field (msg field)
+ "Retrieve FIELD from message plist MSG.
+Like `mu4e-message-field-nil', but will sanitize nil values:
+- all string field except body-txt/body-html: nil -> \"\"
+- numeric fields + dates : nil -> 0
+- all others : return the value
+Thus, function will return nil for empty lists, non-existing body-txt
+or body-html."
+ (let ((val (mu4e-message-field-raw msg field)))
+ (cond
+ (val
+ val) ;; non-nil -> just return it
+ ((member field '(:subject :message-id :path :maildir :in-reply-to))
+ "") ;; string fields except body-txt, body-html: nil -> ""
+ ((member field '(:body-html :body-txt))
+ val)
+ ((member field '(:docid :size))
+ 0) ;; numeric type: nil -> 0
+ (t
+ val)))) ;; otherwise, just return nil
+
+(defsubst mu4e-message-has-field (msg field)
+ "If MSG has a FIELD return t, nil otherwise."
+ (plist-member msg field))
+
+(defsubst mu4e-message-at-point (&optional noerror)
+ "Get the message s-expression for the message at point.
+Either the headers buffer or the view buffer, or nil if there is
+no such message. If optional NOERROR is non-nil, do not raise an
+error when there is no message at point."
+ (or (cond
+ ((eq major-mode 'mu4e-headers-mode) (get-text-property (point) 'msg))
+ ((eq major-mode 'mu4e-view-mode) mu4e--view-message))
+ (unless noerror (mu4e-warn "No message at point"))))
+
+(defsubst mu4e-message-field-at-point (field)
+ "Get the field FIELD from the message at point.
+This is equivalent to:
+ (mu4e-message-field (mu4e-message-at-point) FIELD)."
+ (mu4e-message-field (mu4e-message-at-point) field))
+
+(defun mu4e-message-contact-field-matches (msg cfield rx)
+ "Does MSG's contact-field CFIELD match regexp RX?
+Check if any of the of the CFIELD in MSG matches RX. I.e.
+anything in field CFIELD (either :to, :from, :cc or :bcc, or a
+list of those) of msg MSG matches (with their name or e-mail
+address) regular expressions RX. If there is a match, return
+non-nil; otherwise return nil. RX can also be a list of regular
+expressions, in which case any of those are tried for a match."
+ (cond
+ ((null cfield))
+ ((listp cfield)
+ (seq-find (lambda (cf) (mu4e-message-contact-field-matches msg cf rx))
+ cfield))
+ ((listp rx)
+ ;; if rx is a list, try each one of them for a match
+ (seq-find
+ (lambda (a-rx) (mu4e-message-contact-field-matches msg cfield a-rx))
+ rx))
+ (t
+ ;; not a list, check the rx
+ (seq-find
+ (lambda (ct)
+ (let ((name (mu4e-contact-name ct))
+ (email (mu4e-contact-email ct))
+ ;; the 'rx' may be some `/rx/` from mu4e-personal-addresses;
+ ;; so let's detect and extract in that case.
+ (rx (if (string-match-p "^\\(.*\\)/$" rx)
+ (substring rx 1 -1) rx)))
+ (or
+ (and name (string-match rx name))
+ (and email (string-match rx email)))))
+ (mu4e-message-field msg cfield)))))
+
+(defun mu4e-message-contact-field-matches-me (msg cfield)
+ "Does contact-field CFIELD in MSG match me?
+Checks whether any
+of the of the contacts in field CFIELD (either :to, :from, :cc or
+:bcc) of msg MSG matches *me*, that is, any of the addresses for
+which `mu4e-personal-address-p' return t. Returns the contact
+cell that matched, or nil."
+ (seq-find (lambda (cell)
+ (mu4e-personal-address-p (mu4e-contact-email cell)))
+ (mu4e-message-field msg cfield)))
+
+(defun mu4e-message-sent-by-me (msg)
+ "Is this MSG (to be) sent by me?
+Checks if the from field matches user's personal addresses."
+ (mu4e-message-contact-field-matches-me msg :from))
+
+(defun mu4e-message-personal-p (msg)
+ "Does MSG have user's personal address?
+In any of the contact
+ fields?"
+ (seq-some
+ (lambda (field)
+ (mu4e-message-contact-field-matches-me msg field))
+ '(:from :to :cc :bcc)))
+
+(defsubst mu4e-message-part-field (msgpart field)
+ "Get some FIELD from MSGPART.
+A part would look something like:
+ (:index 2 :name \"photo.jpg\" :mime-type \"image/jpeg\" :size 147331)."
+ (plist-get msgpart field))
+
+;; backward compatibility ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+(defalias 'mu4e-msg-field 'mu4e-message-field)
+
+(defun mu4e-field-at-point (field)
+ "Get FIELD for the message at point.
+Either in the headers buffer or the view buffer. Field is a
+symbol, see `mu4e-header-info'."
+ (plist-get (mu4e-message-at-point) field))
+
+(defun mu4e-message-readable-path (&optional msg)
+ "Get a readable path to MSG or raise an error.
+If MSG is nil, use `mu4e-message-at-point'."
+ (let ((path (plist-get (or msg (mu4e-message-at-point)) :path)))
+ (unless (file-readable-p path)
+ (mu4e-error "No readable message at %s; database outdated?" path))
+ path))
+
+(defun mu4e-copy-message-path ()
+ "Copy the message-path of message at point to the kill ring."
+ (interactive)
+ (let ((path (mu4e-message-field-at-point :path)))
+ (kill-new path)
+ (mu4e-message "Saved '%s' to kill-ring" path)))
+
+(defun mu4e-sexp-at-point ()
+ "Show or hide the s-expression for the message-at-point, if any."
+ (interactive)
+ (if-let ((win (get-buffer-window mu4e--sexp-buffer-name)))
+ (delete-window win)
+ (when-let ((msg (mu4e-message-at-point 'noerror)))
+ (when (buffer-live-p mu4e--sexp-buffer-name)
+ (kill-buffer mu4e--sexp-buffer-name))
+ (with-current-buffer-window
+ (get-buffer-create mu4e--sexp-buffer-name) nil nil
+ (if (fboundp 'lisp-data-mode)
+ (lisp-data-mode)
+ (lisp-mode))
+ (insert (pp-to-string msg))
+ (font-lock-ensure)
+ ;; add basic `quit-window' bindings
+ (view-mode 1)))))
+
+(declare-function mu4e--decoded-message "mu4e-compose")
+
+(defun mu4e-fetch-field (msg hdr &optional first)
+ "Find the value for an arbitrary header field HDR from MSG.
+
+If the header appears multiple times, the field values are
+concatenated, unless FIRST is non-nil, in which case only the
+first value is returned. See `message-field-value' and
+`nessage-fetch-field' for details.
+
+Note: this loads the full message file such that any available
+message header can be used. If the header is part of the MSG
+plist, it is much more efficient to get the information from that
+plist."
+ (with-temp-buffer
+ (insert (mu4e--decoded-message msg 'headers-only))
+ (message-field-value hdr first)))
+;;;
+(provide 'mu4e-message)
+;;; mu4e-message.el ends here
--- /dev/null
+;;; mu4e-mime-parts.el --- Dealing with MIME-parts & URLs -*- lexical-binding: t -*-
+
+;; Copyright (C) 2023 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Implements functions and variables for dealing with MIME-parts and URLs.
+
+
+;;; TODO:
+;; [~] mime part candidate sorting -> is his even possible generally?
+;; [ ] URL support
+
+;;; Code:
+
+
+(require 'mu4e-vars)
+(require 'mu4e-folders)
+(require 'gnus-art)
+
+\f
+
+(defcustom mu4e-view-open-program
+ (pcase system-type
+ ('darwin "open")
+ ('cygwin "cygstart")
+ (_ "xdg-open"))
+ "Tool to open the correct program for a given file or MIME-type.
+May also be a function of a single argument, the file to be
+opened.
+
+In the function-valued case a likely candidate is
+`mailcap-view-file' although note that there was an Emacs bug up
+to Emacs 29 which prevented opening a file if `mailcap-mime-data'
+specified a function as viewer."
+ :type '(choice string function)
+ :group 'mu4e-view)
+
+\f
+;; remember the mime-handles, so we can clean them up when
+;; we quit this buffer.
+(defvar-local mu4e~gnus-article-mime-handles nil)
+(put 'mu4e~gnus-article-mime-handles 'permanent-local t)
+
+(defun mu4e--view-kill-mime-handles ()
+ "Kill cached MIME-handles, if any."
+ (when mu4e~gnus-article-mime-handles
+ (mm-destroy-parts mu4e~gnus-article-mime-handles)
+ (setq mu4e~gnus-article-mime-handles nil)))
+
+
+;;; MIME-parts
+(defvar-local mu4e--view-mime-parts nil
+ "Cached MIME parts for this message.")
+\f
+
+(defun mu4e-view-mime-parts()
+ "Get the list of MIME parts for this message.
+The list is a list of plists, one for each MIME-part.
+
+The plists have the properties:
+
+ :part-index : Gnus index number
+ :mime-type : MIME-type (string) or nil
+ :encoding : Content encoding (string) or nil
+ :disposition : Content disposition (attachment\" or inline\") or nil
+ :filename : The file name if it has one, or an invented one
+ otherwise
+
+There are some internal fields as well, e.g. ; subject to change:
+
+ :target-dir : Target directory for saving
+ :attachment-like : When it has a filename, we can save it
+ :handle : Gnus handle."
+ (or mu4e--view-mime-parts
+ (setq
+ mu4e--view-mime-parts
+ (let ((parts) (indices))
+ (save-excursion
+ (goto-char (point-min))
+ (while (not (eobp))
+ (when-let ((part (get-text-property (point) 'gnus-data))
+ (index (get-text-property (point) 'gnus-part)))
+ (when (and part (numberp index) (not (member index indices)))
+ (let* ((disp (mm-handle-disposition part))
+ (fname (mm-handle-filename part))
+ (mime-type (mm-handle-media-type part))
+ (info
+ `(:part-index ,index
+ :mime-type ,mime-type
+ :encoding ,(mm-handle-encoding part)
+ :disposition ,(car-safe disp)
+
+ ;; if there's no file-name, invent one
+ ;; XXX perhaps guess extension based on mime-type
+ :filename ,(or fname
+ (format "mime-part-%02d" index))
+
+ ;; below are internal
+
+ :target-dir ,(mu4e-determine-attachment-dir
+ fname mime-type)
+ ;; 'attachment-like' just means it has its own
+ ;; filename an we thus we can save it through
+ ;; `mu4e-view-save-attachments', even if it has an
+ ;; 'inline' disposition.
+ :attachment-like ,(if fname t nil)
+ :handle ,part)))
+ (push index indices)
+ (push info parts))))
+ (goto-char (or (next-single-property-change (point) 'gnus-part)
+ (point-max)))))
+ ;; sort by the GNU's part-index, so the order is the same as
+ ;; in the message on screen
+ (seq-sort (lambda (p1 p2) (< (plist-get p1 :part-index)
+ (plist-get p2 :part-index))) parts)))))
+
+;; https://emacs.stackexchange.com/questions/74547/completing-read-search-also-in-annotationsxc
+
+(defun mu4e--uniqify-file-name (fname)
+ "Return a non-yet-existing filename based on FNAME.
+If FNAME does not yet exist, return it unchanged.
+Otherwise, return a file with a unique number appended to the base-name."
+ (let ((num 1) (orig-name fname))
+ (while (file-exists-p fname)
+ (setq fname (format "%s(%d)%s%s"
+ (file-name-sans-extension orig-name)
+ num
+ (if (file-name-extension orig-name) "." "")
+ (file-name-extension orig-name)))
+ (cl-incf num)))
+ fname)
+
+(defvar mu4e--completions-table nil)
+
+(defun mu4e-view-complete-all ()
+ "Pick all current candidates."
+ (interactive)
+ (if (bound-and-true-p helm-mode)
+ (mu4e-warn "Not supported with helm")
+ (when mu4e--completions-table
+ (insert (string-join
+ (seq-map #'car mu4e--completions-table) ", ")))))
+
+(defvar mu4e-view-completion-minor-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "C-c C-a") #'mu4e-view-complete-all)
+ ;; XXX perhaps a binding for clearing all?
+ map)
+ "Keybindings for mu4e-view completion.")
+
+(define-minor-mode mu4e-view-completion-minor-mode
+ "Minor-mode for completing mu4e mime parts."
+ :global nil
+ :init-value nil ;; disabled by default
+ :group 'mu4e
+ :lighter ""
+ :keymap mu4e-view-completion-minor-mode-map)
+
+(defun mu4e--part-annotation (candidate part type longest-filename)
+ "Calculate the annotation candidates as per
+`:annotation-function' (see `completion-extra-properties')
+
+CANDIDATE is the value to annotate.
+
+PART is the matching MIME-part for the annotation, (as per
+`mu4e-view-mime-part').
+
+TYPE is the of what to annotate, a symbol, either ATTACHMENT or
+MIME-PART.
+
+LONGEST-FILENAME is the length of the longest filename; this
+information' is used for alignment."
+ (let* ((filename (propertize (or (plist-get part :filename) "")
+ 'face 'mu4e-header-key-face))
+ (mimetype (propertize (or (plist-get part :mime-type) "")
+ 'face 'mu4e-header-value-face))
+ (target (propertize (or (plist-get part :target-dir) "")
+ 'face 'mu4e-system-face)))
+
+ ;; Sadly, we need too align by hand; this makes some assumptions
+ ;; such a mono-type font and enough space in the minibuffer; and
+ ;; mixing values and representation; ideally Emacs would allow
+ ;; just take some columns and align them (since it knows the display
+ ;; details).
+
+ (pcase type
+ ('attachment
+ ;; in case we're annotating an attachment, the filename is
+ ;; the candidate (completion), so we don't need it in the
+ ;; the annotation. We just need to but some space at beginning
+ ;; for alignment
+ (concat
+ (make-string (- (+ longest-filename 2)
+ (length (format "%s" candidate))) ?\s)
+ (format "%20s" mimetype)
+ " "
+ (format "%s" (concat "-> " target))))
+ ('mime-part
+ ;; when we're annotating a mime-part, the candidate is just a number,
+ ;; and the filename is part of the annotation.
+ (concat
+ " "
+ filename
+ (make-string (- (+ longest-filename 2)
+ (length filename)) ?\s)
+ (format "%20s" mimetype)
+ " "
+ (format "%s" (concat "-> " target))))
+ (_ (mu4e-error "Unsupported annotation type %s" type)))))
+
+
+(defvar helm-comp-read-use-marked)
+(defun mu4e--completing-read-real (prompt candidates multi)
+ "Call the appropriate completion-read function.
+- PROMPT is a string informing the user what to complete
+- CANDIDATES is an alist of candidates of the form
+ (id . part)
+- MULTI if t, allow for completing _multiple_ candidates."
+ (cond
+ ((bound-and-true-p helm-mode)
+ ;; tweaks for "helm"; it's not nice to have to special-case for
+ ;; completion frameworks, but this has been supported for while.
+ ;; basically, with helm, helm-comp-read-use-marked + completing-read
+ ;; is preferred over completing-read-multiple
+ (let ((helm-comp-read-use-marked t))
+ (completing-read prompt candidates)))
+ (multi
+ (completing-read-multiple prompt candidates))
+ (t
+ (completing-read prompt candidates))))
+
+(defun mu4e--completing-read (prompt candidates type &optional multi)
+ "Read the part-id of some MIME-type in this message.
+
+Presents the user with completions for the MIME-parts in
+the current message.
+
+- PROMPT is a string informing the user what to complete
+- CANDIDATES is an alist of candidates of the form
+ (id . part)
+- TYPE is the annotation type to uses as per `mu4e--part-annotation'.
+Optionally,
+- MULTI if t, allow for completing _multiple_ candidates."
+ (cl-assert candidates)
+ (let* ((longest-filename (seq-max
+ (seq-map (lambda (c)
+ (length (plist-get (cdr c) :filename)))
+ candidates)))
+ (annotation-func (lambda (candidate)
+ (mu4e--part-annotation candidate
+ (cdr-safe
+ (assoc candidate candidates))
+ type longest-filename)))
+ (completion-extra-properties
+ `(;; :affixation-function requires emacs 28
+ :annotation-function ,annotation-func
+ :exit-function (lambda (_a _b) (setq mu4e--completions-table nil)))))
+ (setq mu4e--completions-table candidates)
+ (minibuffer-with-setup-hook
+ (lambda ()
+ (mu4e-view-completion-minor-mode))
+ (mu4e--completing-read-real prompt candidates multi))))
+
+(defun mu4e-view-save-attachments (&optional ask-dir)
+ "Save files from the current view buffer.
+This applies to all MIME-parts that are \"attachment-like\" (have a filename),
+regardless of their disposition.
+
+With ASK-DIR is non-nil, user can specify the target-directory; otherwise
+one is determined using `mu4e-attachment-dir'."
+ (interactive "P")
+ (let* ((parts (mu4e-view-mime-parts))
+ (candidates (seq-map
+ (lambda (fpart)
+ (cons ;; (filename . annotation)
+ (plist-get fpart :filename)
+ fpart))
+ (seq-filter
+ (lambda (part) (plist-get part :attachment-like))
+ parts)))
+ (candidates (or candidates
+ (mu4e-warn "No attachments for this message")))
+ (files (mu4e--completing-read "Save file(s): " candidates
+ 'attachment 'multi))
+ (custom-dir (when ask-dir (read-directory-name
+ "Save to directory: "))))
+ ;; we have determined what files to save, and where.
+ (seq-do (lambda (fname)
+ (let* ((part (cdr (assoc fname candidates)))
+ (path (mu4e--uniqify-file-name
+ (mu4e-join-paths
+ (or custom-dir (plist-get part :target-dir))
+ (plist-get part :filename)))))
+ (mm-save-part-to-file (plist-get part :handle) path)))
+ files)))
+
+(defvar mu4e-view-mime-part-actions
+ '(
+ ;;
+ ;; some basic ones
+ ;;
+
+ ;; save MIME-part to a file
+ (:name "save" :handler gnus-article-save-part :receives index)
+ ;; pipe MIME-part to some arbitrary shell command
+ (:name "|pipe" :handler gnus-article-pipe-part :receives index)
+ ;; open with the default handler, if any
+ (:name "open" :handler mu4e--view-open-file :receives temp)
+ ;; open with some custom file.
+ (:name "wopen-with" :handler (lambda (file)(mu4e--view-open-file file t))
+ :receives temp)
+
+ ;;
+ ;; some more examples
+ ;;
+
+ ;; import GPG key
+ (:name "gpg" :handler epa-import-keys :receives temp)
+ ;; open in this emacs instance; tries to use the attachment name,
+ ;; so emacs can use specific modes etc.
+ (:name "emacs" :handler find-file-read-only :receives temp)
+ ;; open in this emacs instance, "raw"
+ (:name "raw" :handler (lambda (str)
+ (let ((tmpbuf
+ (get-buffer-create " *mu4e-raw-mime*")))
+ (with-current-buffer tmpbuf
+ (insert str)
+ (view-mode)
+ (goto-char (point-min)))
+ (display-buffer tmpbuf))) :receives pipe))
+
+ "Specifies actions for MIME-parts.
+
+Each of the actions is a plist with keys
+`(:name <name> ;; name of the action; shortcut is first letter of name
+
+ :handler ;; one of:
+ ;; - a function receiving the index/temp/pipe
+ ;; - a string, which is taken as a shell command
+
+ :receives ;; a symbol specifying what the handler receives
+ ;; - index: the index number of the mime part (default)
+ ;; - temp: the full path to the mime part in a
+ ;; temporary file, which is deleted immediately
+ ;; after the handler returns
+ ;; - pipe: the attachment is piped to some shell command
+ ;; or as a string parameter to a function
+).")
+
+
+(defun mu4e--view-mime-part-to-temp-file (handle)
+ "Write MIME-part HANDLE to a temporary file and return the file name.
+The filename is deduced from the MIME-part's filename, or
+otherwise random; the result is placed in a temporary directory
+with a unique name. Returns the full path for the file created.
+The directory and file are self-destructed."
+ (let* ((tmpdir (make-temp-file "mu4e-temp-" t))
+ (fname (mm-handle-filename handle))
+ (fname (and fname
+ (gnus-map-function mm-file-name-rewrite-functions
+ (file-name-nondirectory fname))))
+ (fname (if fname
+ (concat tmpdir "/" (replace-regexp-in-string "/" "-" fname))
+ (let ((temporary-file-directory tmpdir))
+ (make-temp-file "mimepart")))))
+ (mm-save-part-to-file handle fname)
+ (run-at-time "30 sec" nil
+ (lambda () (ignore-errors (delete-directory tmpdir t))))
+ fname))
+
+(defun mu4e--view-open-file (file &optional force-ask)
+ "Open FILE with default handler, if any.
+Otherwise, or if FORCE-ASK is set, ask user for the program to
+open with."
+ (if (and (not force-ask)
+ (functionp mu4e-view-open-program))
+ (funcall mu4e-view-open-program file)
+ (let ((opener
+ (or (and (not force-ask) mu4e-view-open-program
+ (executable-find mu4e-view-open-program))
+ (read-shell-command "Open MIME-part with: "))))
+ (call-process opener nil 0 nil file))))
+
+(defun mu4e-view-mime-part-action (&optional n)
+ "Apply some action to MIME-part N in the current message.
+If N is not specified, ask for it. For instance, '3 A o' opens
+the third MIME-part."
+ ;; (interactive
+ ;; (list (read-number "Number of MIME-part: ")))
+ (interactive)
+ (let* ((parts (mu4e-view-mime-parts))
+ (candidates (seq-map
+ (lambda (part)
+ (cons (number-to-string
+ (plist-get part :part-index)) part))
+ parts))
+ (candidates (or candidates
+ (mu4e-warn "No MIME-parts for this message")))
+ (ids (seq-map #'string-to-number
+ (if n (list (number-to-string n))
+ (mu4e--completing-read "MIME-part(s) to operate on: "
+ candidates
+ 'mime-part 'multi))))
+ (options
+ (mapcar (lambda (action) `(,(plist-get action :name) . ,action))
+ mu4e-view-mime-part-actions))
+ (action
+ (or (and options (mu4e-read-option "Action: " options))
+ (mu4e-error "No such action")))
+ (handler
+ (or (plist-get action :handler)
+ (mu4e-error "No :handler item found for action %S" action)))
+ (receives
+ (or (plist-get action :receives)
+ (mu4e-error "No :receives item found for action %S" action))))
+
+ ;; Apply the action to all selected MIME-parts
+ (seq-do (lambda (id)
+ (cl-assert (numberp id))
+ (let* ((part (or (cdr-safe (assoc (number-to-string id) candidates))
+ (mu4e-error "No part found for id %s" id)))
+ (handle (plist-get part :handle)))
+ (save-excursion
+ (cond
+ ((functionp handler)
+ (cond
+ ((eq receives 'index) (funcall handler id))
+ ((eq receives 'pipe)
+ (funcall handler (mm-with-unibyte-buffer
+ (mm-insert-part handle)
+ (buffer-string))))
+ ((eq receives 'temp)
+ (funcall handler
+ (mu4e--view-mime-part-to-temp-file handle)))
+ (t (mu4e-error "Invalid :receive for %S" action))))
+ ((stringp handler)
+ (cond
+ ((eq receives 'index)
+ (shell-command
+ (concat handler " " (shell-quote-argument id))))
+ ((eq receives 'pipe)
+ (progn
+ (mm-pipe-part handle handler)))
+ ((eq receives 'temp)
+ (shell-command
+ (shell-command
+ (concat
+ handler " "
+ (shell-quote-argument
+ (mu4e--view-mime-part-to-temp-file handle))))))
+ (t (mu4e-error "Invalid action %S" action))))))))
+ ids)))
+
+(defun mu4e-process-file-through-pipe (path pipecmd)
+ "Process file at PATH through a pipe with PIPECMD."
+ (let ((buf (get-buffer-create "*mu4e-output")))
+ (with-current-buffer buf
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (call-process-shell-command pipecmd path t t)
+ (view-mode)))
+ (display-buffer buf)))
+
+
+\f
+(provide 'mu4e-mime-parts)
+;;; mu4e-mime-parts.el ends here
--- /dev/null
+;;; mu4e-modeline.el --- Modeline for mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2023 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file contains functionality for putting mu4e-related information in the
+;; Emacs modeline, both buffer-specific and globally.
+
+;;; Code:
+
+(require 'cl-lib)
+
+
+(defcustom mu4e-modeline-max-width 42
+ "Determines the maximum length of the local modeline string.
+If the string exceeds this limit, it will be truncated to fit.
+
+Note: this only affects the local modeline items (such as the context,
+the search properties and the last query), not the global items
+(such as the favorite bookmark results)."
+ :type 'integer
+ :group 'mu4e-modeline)
+
+(defcustom mu4e-modeline-prefer-bookmark-name t
+ "Show bookmark name rather than query in modeline.
+
+If non-nil, if the current search query matches some bookmark,
+display the bookmark name rather than the query."
+ :type 'boolean
+ :group 'mu4e-modeline)
+
+(defcustom mu4e-modeline-show-global t
+ "Whether to populate global modeline segments.
+
+If non-nil, show both buffer-specific and global modeline items,
+otherwise only present buffer-specific information."
+ :type 'boolean
+ :group 'mu4e-modeline)
+
+(defvar-local mu4e--modeline-buffer-items nil
+ "List of buffer-local items for the mu4e modeline.
+Each element is function that evaluates to a string.")
+
+(defvar mu4e--modeline-global-items nil
+ "List of items for the global modeline.
+Each element is function that evaluates to a string.")
+
+(defun mu4e--modeline-register (func &optional global)
+ "Register FUNC for calculating some mu4e modeline part.
+If GLOBAL is non-nil, add to the global-modeline; otherwise use
+the buffer-local one."
+ (add-to-list
+ (if global
+ 'mu4e--modeline-global-items
+ 'mu4e--modeline-buffer-items)
+ func 'append))
+
+(defun mu4e--modeline-quote-and-truncate (str)
+ "Quote STR to be used literally in the modeline.
+The string is truncaed to fit if its length exceeds
+`mu4e-modeline-max-width'."
+ (replace-regexp-in-string
+ "%" "%%"
+ (truncate-string-to-width str mu4e-modeline-max-width 0 nil t)))
+
+(defvar mu4e--modeline-item nil
+ "Mu4e item for the global-mode-line.")
+
+(defvar mu4e--modeline-global-string-cached nil
+ "Cached version of the _global_ modeline string.
+Note that we don't cache the local parts, so that the modeline
+gets updated when we leave the buffer from which the local parts
+originate.")
+
+(defun mu4e--modeline-string ()
+ "Get the current mu4e modeline string."
+ (let* ((collect
+ (lambda (lst)
+ (mapconcat
+ (lambda (func) (or (funcall func) "")) lst " ")))
+ (global-string ;; global string is _cached_ as it may be expensive.
+ (and
+ mu4e-modeline-show-global
+ (or mu4e--modeline-global-string-cached
+ (setq mu4e--modeline-global-string-cached
+ (funcall collect mu4e--modeline-global-items))))))
+ (concat
+ ;; (local) buffer items are _not_ cached, so they'll get update
+ ;; automatically when leaving the buffer.
+ (mu4e--modeline-quote-and-truncate
+ (funcall collect mu4e--modeline-buffer-items))
+ (and global-string " ")
+ global-string)))
+
+(define-minor-mode mu4e-modeline-mode
+ "Minor mode for showing mu4e information on the modeline."
+ ;; This is a bit special 'global' mode, since it consists of both
+ ;; buffer-specific parts (mu4e--modeline-buffer-items) and global items
+ ;; (mu4e--modeline-global-items).
+ :global t
+ :group 'mu4e
+ :lighter nil
+ (if mu4e-modeline-mode
+ (progn
+ (setq mu4e--modeline-item '(:eval (mu4e--modeline-string)))
+ (add-to-list 'global-mode-string mu4e--modeline-item)
+ (mu4e--modeline-update))
+ (progn
+ (setq global-mode-string
+ (seq-remove (lambda (item) (equal item mu4e--modeline-item))
+ global-mode-string)))
+ (force-mode-line-update)))
+
+(defun mu4e--modeline-update ()
+ "Recalculate and force-update the modeline."
+ (when mu4e-modeline-mode
+ (setq mu4e--modeline-global-string-cached nil)
+ (force-mode-line-update)))
+
+(provide 'mu4e-modeline)
+
+;;; mu4e-modeline.el ends here
--- /dev/null
+;;; mu4e-notification.el --- Showing mail notifications -*- lexical-binding: t-*-
+;;
+;; Copyright (C) 1996-2023 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;;; Commentary:
+;;; Generic support for showing new-mail notifications.
+
+;;; Code:
+
+(require 'mu4e-query-items)
+(require 'mu4e-bookmarks)
+
+;; for emacs' built-in desktop notifications to work, we need
+;; dbus
+(when (featurep 'dbus)
+ (require 'notifications))
+
+(defcustom mu4e-notification-filter #'mu4e--default-notification-filter
+ "Function for determining if a notification is to be emitted.
+
+If this is the case, the function should return non-nil.
+The function must accept an optional single parameter, unused for
+now."
+ :type 'function
+ :group 'mu4e-notification)
+
+(defcustom mu4e-notification-function
+ #'mu4e--default-notification-function
+ "Function to emit a notification.
+
+The function is invoked when we need to emit a new-mail
+notification in some system-specific way. The function is invoked
+when the query-items have been updated and
+`mu4e-notification-filter' returns non-nil.
+
+The function must accept an optional single parameter, unused for
+now."
+ :type 'function
+ :group 'mu4e-notification)
+
+(defvar mu4e--notification-id nil
+ "The last notification id, so we can replace it.")
+
+(defun mu4e--default-notification-filter (&optional _)
+ "Return t if a notification should be shown.
+
+This default implementation does so when the number of unread
+messages changed since the last notification and it is greater
+than zero."
+ (when-let* ((fav (mu4e-bookmark-favorite))
+ (delta-unread (plist-get fav :delta-unread)))
+ (when (and (> delta-unread 0)
+ (not (= delta-unread mu4e--last-delta-unread)))
+ (setq mu4e--last-delta-unread delta-unread) ;; update
+ t ;; do show notification
+ )))
+
+(defun mu4e--default-notification-function (&optional _)
+ "Default function for handling notifications.
+The default implementation uses emacs' built-in dbus-notification
+support."
+ (when-let* ((fav (mu4e-bookmark-favorite))
+ (title "mu4e found new mail")
+ (delta-unread (or (plist-get fav :delta-unread) 0))
+ (body (format "%d new message%s in %s"
+ delta-unread
+ (if (= delta-unread 1) "" "s")
+ (plist-get fav :name))))
+ (cond
+ ((fboundp 'do-applescript)
+ (do-applescript
+ (format "display notification %S with title %S" body title)))
+ ((fboundp 'notifications-notify)
+ ;; notifications available
+ (setq mu4e--notification-id
+ (notifications-notify
+ :title title
+ :body body
+ :app-name "mu4e@emacs"
+ :replaces-id mu4e--notification-id
+ ;; a custom mu4e icon would be nice...
+ ;; :app-icon (ignore-errors
+ ;; (image-search-load-path
+ ;; "gnus/gnus.png"))
+ :actions '("Show" "Favorite bookmark"
+ "default" "Favorite bookmark")
+ :on-action (lambda (_1 _2) (mu4e-jump-to-favorite)))))
+ ;; ... TBI: other notifications ...
+ (t ;; last resort
+ (mu4e-message "%s: %s" title body)))))
+
+(defun mu4e--notification ()
+ "Function called when the query items have been updated."
+ (when (and (funcall mu4e-notification-filter)
+ (functionp mu4e-notification-function))
+ (funcall mu4e-notification-function)))
+
+(provide 'mu4e-notification)
+;;; mu4e-notification.el ends here
--- /dev/null
+;;; mu4e-obsolete.el --- Obsolete things -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022-2024 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Obsolete variable & function aliases go here, so we don't clutter up the
+;; code.
+
+;;; Code:
+
+\f
+;; mu4e-draft/compose
+
+(make-obsolete-variable 'mu4e-reply-to-address
+ 'mu4e-compose-reply-to-address
+ "v0.9.9")
+
+(make-obsolete-variable 'mu4e-auto-retrieve-keys "no longer used." "1.3.1")
+
+(make-obsolete-variable 'mu4e-compose-func "no longer used" "1.11.26")
+
+(make-obsolete-variable 'mu4e-compose-crypto-reply-encrypted-policy "The use of the
+ 'mu4e-compose-crypto-reply-encrypted-policy' variable is deprecated.
+ 'mu4e-compose-crypto-policy' should be used instead" "2020-03-06")
+
+(make-obsolete-variable 'mu4e-compose-crypto-reply-plain-policy "The use of the
+ 'mu4e-compose-crypto-reply-plain-policy' variable is deprecated.
+ 'mu4e-compose-crypto-policy' should be used instead"
+ "2020-03-06")
+
+(make-obsolete-variable 'mu4e-compose-crypto-reply-policy "The use of the
+ 'mu4e-compose-crypto-reply-policy' variable is deprecated.
+ 'mu4e-compose-crypto-reply-plain-policy' and
+ 'mu4e-compose-crypto-reply-encrypted-policy' should be used instead"
+ "2017-09-02")
+
+(make-obsolete-variable 'mu4e-compose-auto-include-date
+ "This is done unconditionally now" "1.3.5")
+
+(make-obsolete-variable 'mu4e-compose-signature-auto-include
+ "Usage message-signature directly" "1.11.22")
+
+(define-obsolete-variable-alias
+ 'mu4e-compose-signature 'message-signature "1.11.22")
+(define-obsolete-variable-alias
+ 'mu4e-compose-cite-function 'message-cite-function "1.11.22")
+(define-obsolete-variable-alias
+ 'mu4e-compose-in-new-frame 'mu4e-compose-switch "1.11.22")
+
+(define-obsolete-variable-alias 'mu4e-compose-hidden-headers
+ 'mu4e-draft-hidden-headers "1.12.5")
+
+\f
+;; mu4e-message
+
+(make-obsolete-variable 'mu4e-html2text-command "No longer in use" "1.7.0")
+(make-obsolete-variable 'mu4e-view-prefer-html "No longer in use" "1.7.0")
+(make-obsolete-variable 'mu4e-view-html-plaintext-ratio-heuristic
+ "No longer in use" "1.7.0")
+(make-obsolete-variable 'mu4e-message-body-rewrite-functions
+ "No longer in use" "1.7.0")
+;;; Html2Text
+(make-obsolete 'mu4e-shr2text "No longer in use" "1.7.0")
+
+
+\f
+;; old message view
+(make-obsolete-variable 'mu4e-view-show-addresses
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-wrap-lines nil "0.9.9-dev7")
+(make-obsolete-variable 'mu4e-view-hide-cited nil "0.9.9-dev7")
+(make-obsolete-variable 'mu4e-view-date-format
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-image-max-width
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-image-max-height
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-save-multiple-attachments-without-asking
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-attachment-assoc
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-attachment-actions
+ "See mu4e-view-mime-part-actions" "1.7.0")
+(make-obsolete-variable 'mu4e-view-header-field-keymap
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-header-field-keymap
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-contacts-header-keymap
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-view-attachments-header-keymap
+ "Unused with the new message view" "1.7.0")
+(make-obsolete-variable 'mu4e-imagemagick-identify nil "1.7.0")
+(make-obsolete-variable 'mu4e-view-show-images
+ "No longer used" "1.7.0")
+(make-obsolete-variable 'mu4e-view-gnus "Old view is gone" "1.7.0")
+(make-obsolete-variable 'mu4e-view-use-gnus "Gnus view is the default" "1.5.10")
+
+(make-obsolete-variable 'mu4e-cited-regexp "No longer used" "1.7.0")
+
+(define-obsolete-variable-alias 'mu4e-view-blocked-images 'gnus-blocked-images
+ "1.5.12")
+(define-obsolete-variable-alias 'mu4e-view-inhibit-images 'gnus-inhibit-images
+ "1.5.12")
+
+(define-obsolete-variable-alias 'mu4e-after-view-message-hook
+ 'mu4e-view-rendered-hook "1.9.7")
+
+\f
+;; mu4e-org
+(define-obsolete-function-alias 'org-mu4e-open 'mu4e-org-open "1.3.6")
+(define-obsolete-function-alias 'org-mu4e-store-and-capture
+ 'mu4e-org-store-and-capture "1.3.6")
+
+\f
+;; mu4e-search
+(define-obsolete-variable-alias 'mu4e-headers-results-limit
+ 'mu4e-search-results-limit "1.7.0")
+(define-obsolete-variable-alias 'mu4e-headers-full-search
+ 'mu4e-search-full "1.7.0")
+(define-obsolete-variable-alias 'mu4e-headers-show-threads
+ 'mu4e-search-threads "1.7.0")
+(define-obsolete-variable-alias
+ 'mu4e-headers-search-bookmark-hook
+ 'mu4e-search-bookmark-hook "1.7.0")
+(define-obsolete-variable-alias 'mu4e-headers-search-hook
+ 'mu4e-search-hook "1.7.0")
+(define-obsolete-function-alias 'mu4e-headers-search 'mu4e-search "1.7.0")
+(define-obsolete-function-alias 'mu4e-headers-search-edit
+ 'mu4e-search-edit "1.7.0")
+(define-obsolete-function-alias 'mu4e-headers-search-bookmark
+ 'mu4e-search-bookmark "1.7.0")
+(define-obsolete-function-alias 'mu4e-headers-search-bookmark-edit
+ 'mu4e-search-bookmark-edit "1.7.0")
+(define-obsolete-function-alias 'mu4e-headers-search-narrow
+ 'mu4e-search-narrow "1.7.0")
+(define-obsolete-function-alias 'mu4e-headers-rerun-search
+ 'mu4e-search-rerun "1.7.0")
+(define-obsolete-function-alias 'mu4e-headers-query-next
+ 'mu4e-search-next "1.7.0")
+(define-obsolete-function-alias 'mu4e-headers-query-prev
+ 'mu4e-search-prev "1.7.0")
+(define-obsolete-function-alias 'mu4e-headers-forget-queries
+ 'mu4e-search-forget "1.7.0")
+(define-obsolete-function-alias 'mu4e-read-query
+ 'mu4e-search-read-query "1.7.0")
+
+(make-obsolete-variable 'mu4e-display-update-status-in-modeline
+ "No longer used" "1.9.11")
+\f
+;; mu4e-headers
+(make-obsolete-variable 'mu4e-headers-field-properties-function
+ "not used" "1.6.1")
+
+(define-obsolete-function-alias 'mu4e-headers-toggle-setting
+ 'mu4e-headers-toggle-property "1.9.5")
+(define-obsolete-function-alias 'mu4e-headers-toggle-threading
+ 'mu4e-headers-toggle-property "1.9.5")
+(define-obsolete-function-alias 'mu4e-headers-toggle-full-search
+ 'mu4e-headers-toggle-property "1.9.5")
+(define-obsolete-function-alias 'mu4e-headers-toggle-include-related
+ 'mu4e-headers-toggle-property "1.9.5")
+(define-obsolete-function-alias 'mu4e-headers-toggle-skip-duplicates
+ 'mu4e-headers-toggle-property "1.9.5")
+
+(define-obsolete-function-alias 'mu4e-headers-change-sorting
+ 'mu4e-search-change-sorting "1.9.11")
+(define-obsolete-function-alias 'mu4e-headers-toggle-property
+ 'mu4e-search-toggle-property "1.9.11")
+
+(define-obsolete-variable-alias 'mu4e-headers-include-related
+ 'mu4e-search-include-related "1.9.11")
+(define-obsolete-variable-alias 'mu4e-headers-skip-duplicates
+ 'mu4e-search-skip-duplicates "1.9.11")
+(define-obsolete-variable-alias 'mu4e-headers-sort-field
+ 'mu4e-search-sort-field "1.9.11")
+(define-obsolete-variable-alias 'mu4e-headers-sort-direction
+ 'mu4e-search-sort-direction "1.9.11")
+
+(define-obsolete-variable-alias 'mu4e-headers-hide-predicate
+ 'mu4e-search-hide-predicate "1.9.11")
+(define-obsolete-variable-alias 'mu4e-headers-hide-enabled
+ 'mu4e-search-hide-enabled "1.9.11")
+
+(define-obsolete-variable-alias 'mu4e-headers-threaded-label
+ 'mu4e-search-threaded-label "1.9.12")
+(define-obsolete-variable-alias 'mu4e-headers-full-label
+ 'mu4e-search-full-label "1.9.12")
+(define-obsolete-variable-alias 'mu4e-headers-related-label
+ 'mu4e-search-related-label "1.9.12")
+(define-obsolete-variable-alias 'mu4e-headers-skip-duplicates-label
+ 'mu4e-search-skip-duplicates-label "1.9.12")
+(define-obsolete-variable-alias 'mu4e-headers-hide-label
+ 'mu4e-search-hide-label "1.9.12")
+;; by exception, add alias for internal func
+(define-obsolete-function-alias 'mu4e~headers-jump-to-maildir
+ 'mu4e-search-maildir "1.9.13")
+
+\f
+;; mu4e-main
+(define-obsolete-variable-alias
+ 'mu4e-main-buffer-hide-personal-addresses
+ 'mu4e-main-hide-personal-addresses "1.5.7")
+
+\f
+;; mu4e-server
+
+(make-obsolete-variable
+ 'mu4e-maildir
+ "determined by server; see `mu4e-root-maildir'." "1.3.8")
+
+(make-obsolete-variable 'mu4e-header-func "mu4e-headers-append-func" "1.7.4")
+(make-obsolete-variable 'mu4e-temp-func "No longer used" "1.7.0")
+
+\f
+;; mu4e-update
+(define-obsolete-function-alias 'mu4e-interrupt-update-mail
+ 'mu4e-kill-update-mail "1.0-alpha0")
+
+;; mu4e-helpers
+(define-obsolete-function-alias 'mu4e-quote-for-modeline
+ 'mu4e--modeline-quote-and-truncate "1.9.16")
+
+;; mu4e-folder
+(make-obsolete-variable 'mu4e-cache-maildir-list "No longer used" "1.11.15")
+
+;; mu4e-contacts
+
+(define-obsolete-function-alias 'mu4e-user-mail-address-p
+ 'mu4e-personal-address-p "1.5.5")
+
+;; don't use the older vars anymore
+(make-obsolete-variable 'mu4e-user-mail-address-regexp
+ 'mu4e-user-mail-address-list "0.9.9.x")
+(make-obsolete-variable 'mu4e-my-email-addresses
+ 'mu4e-user-mail-address-list "0.9.9.x")
+(make-obsolete-variable 'mu4e-user-mail-address-list
+ "determined by server; see `mu4e-personal-addresses'."
+ "1.3.8")
+(make-obsolete-variable 'mu4e-contact-rewrite-function
+ "mu4e-contact-process-function (see docstring)"
+ "1.3.2")
+(make-obsolete-variable 'mu4e-compose-complete-ignore-address-regexp
+ "mu4e-contact-process-function (see docstring)"
+ "1.3.2")
+
+(make-obsolete-variable 'mu4e-compose-reply-recipients
+ "use mu4e-compose-reply / mu4e-compose-wide-reply"
+ "1.11.23")
+(make-obsolete-variable 'mu4e-compose-reply-ignore-address
+ "see: message-prune-recipient-rules" "1.11.23")
+
+;; this is only a _rough_
+(make-obsolete-variable 'mu4e-compose-dont-reply-to-self
+ "message-dont-reply-to-names"
+ "1.11.24")
+;; calendar
+(define-obsolete-function-alias 'mu4e-icalendar-setup
+ 'gnus-icalendar-setup '"1.11.22")
+
+;; mu4e.
+(define-obsolete-function-alias 'mu4e-clear-caches #'ignore "1.11.15")
+
+(provide 'mu4e-obsolete)
+;;; mu4e-obsolete.el ends here
--- /dev/null
+;;; mu4e-org --- Org-links to mu4e messages/queries -*- lexical-binding: t -*-
+
+;; Copyright (C) 2012-2024 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Keywords: outlines, hypermedia, calendar, mail
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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 1the License, or
+;; (at your option) any later version.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; The expect version here is org 9.x.
+
+;;; Code:
+
+(require 'org)
+(require 'mu4e-view)
+(require 'mu4e-contacts)
+
+
+(defgroup mu4e-org nil
+ "Settings for the Org mode related functionality in mu4e."
+ :group 'mu4e
+ :group 'org)
+
+(defcustom mu4e-org-link-desc-func
+ (lambda (msg) (or (plist-get msg :subject) "No subject"))
+ "Function that takes a msg and returns a description.
+This can be used in org capture templates and storing links.
+
+Example usage:
+
+ (defun my-link-descr (msg)
+ (let ((subject (or (plist-get msg :subject)
+ \"No subject\"))
+ (date (or (format-time-string mu4e-headers-date-format
+ (mu4e-msg-field msg :date))
+ \"No date\")))
+ (concat subject \" \" date)))
+
+ (setq mu4e-org-link-desc-func \\='my-link-descr)"
+ :type '(function)
+ :group 'mu4e-org)
+
+(defvar mu4e-org-link-query-in-headers-mode nil
+ "Prefer linking to the query rather than to the message.
+If non-nil, `org-store-link' in `mu4e-headers-mode' links to the
+the current query; otherwise, it links to the message at point.")
+
+;; backward compat until org >= 9.3 is univeral.
+(defalias 'mu4e--org-link-store-props
+ (if (fboundp 'org-link-store-props)
+ #'org-link-store-props
+ (with-no-warnings
+ #'org-store-link-props)))
+
+(defun mu4e--org-store-link-query ()
+ "Store a link to a mu4e query."
+ (setq org-store-link-plist nil) ; reset
+ (mu4e--org-link-store-props
+ :type "mu4e"
+ :query (mu4e-last-query)
+ :date (format-time-string "%FT%T") ;; avoid error
+ :link (concat "mu4e:query:" (mu4e-last-query))
+ :description (format "[%s]" (mu4e-last-query))))
+
+(defun mu4e--org-store-link-message (&optional msg)
+ "Store a link to a mu4e message.
+If MSG is non-nil, store a link to MSG, otherwise use `mu4e-message-at-point'."
+ (setq org-store-link-plist nil)
+ (let* ((msg (or msg (mu4e-message-at-point)))
+ (from (car-safe (plist-get msg :from)))
+ (to (car-safe (plist-get msg :to)))
+ (date (format-time-string "%FT%T" (plist-get msg :date)))
+ (msgid (or (plist-get msg :message-id)
+ (mu4e-error "Cannot link message without message-id")))
+ (props `(:type "mu4e"
+ :date ,date
+ :from ,(mu4e-contact-full from)
+ :fromname ,(mu4e-contact-name from)
+ :fromnameoraddress ,(or (mu4e-contact-name from)
+ (mu4e-contact-email from)) ;; mu4e-specific
+ :maildir ,(plist-get msg :maildir)
+ :message-id ,msgid
+ :path ,(plist-get msg :path)
+ :subject ,(plist-get msg :subject)
+ :to ,(mu4e-contact-full to)
+ :tonameoraddress ,(or (mu4e-contact-name to)
+ (mu4e-contact-email to)) ;; mu4e-specific
+ :link ,(concat "mu4e:msgid:" msgid)
+ :description ,(funcall mu4e-org-link-desc-func msg))))
+ (apply #'mu4e--org-link-store-props props)))
+
+(defun mu4e-org-store-link ()
+ "Store a link to a mu4e message or query.
+It links to the last known query when in `mu4e-headers-mode' with
+`mu4e-org-link-query-in-headers-mode' set; otherwise it links to
+a specific message, based on its message-id, so that links stay
+valid even after moving the message around."
+ (cond
+ ((derived-mode-p 'mu4e-view-mode) (mu4e--org-store-link-message))
+ ((derived-mode-p 'mu4e-headers-mode)
+ (if mu4e-org-link-query-in-headers-mode
+ (mu4e--org-store-link-query)
+ (mu4e--org-store-link-message)))))
+
+(defun mu4e-org-open (link)
+ "Open the org LINK.
+Open the mu4e message (for links starting with \"msgid:\") or run
+the query (for links starting with \"query:\")."
+ (require 'mu4e)
+ (cond
+ ((string-match "^msgid:\\(.+\\)" link)
+ (mu4e-view-message-with-message-id (match-string 1 link)))
+ ((string-match "^query:\\(.+\\)" link)
+ (mu4e-search (match-string 1 link) current-prefix-arg))
+ (t (mu4e-error "Unrecognized link type '%s'" link))))
+
+(defun mu4e-org-store-and-capture ()
+ "Store a link to the current message or query.
+\(depending on `mu4e-org-link-query-in-headers-mode', and capture
+it with org)."
+ (interactive)
+ (call-interactively 'org-store-link)
+ (org-capture))
+
+;; install mu4e-link support.
+(org-link-set-parameters "mu4e"
+ :follow #'mu4e-org-open
+ :store #'mu4e-org-store-link)
+(provide 'mu4e-org)
+;;; mu4e-org.el ends here
--- /dev/null
+;; -*- no-byte-compile: t; -*-
+(define-package "mu4e" "@VERSION@"
+ "part of mu4e, the mu mail user agent"
+ '((emacs "@EMACS_MIN_VERSION@"))
+ :authors '(("Dirk-Jan C. Binnema" . "djcb@djcbsoftware.nl"))
+ :maintainer '("Dirk-Jan C. Binnema" . "djcb@djcbsoftware.nl")
+ :keywords '("email"))
--- /dev/null
+;;; mu4e-query-items.el --- Manage query results -*- lexical-binding: t -*-
+
+;; Copyright (C) 2023 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Managing the last query results / baseline, which we use to get the
+;; unread-counts, i.e., query items. `mu4e-query-items` delivers these items,
+;; aggregated from various sources.
+
+
+;;; Code:
+
+;;; Last & baseline query results for bookmarks.
+(require 'cl-lib)
+(require 'mu4e-helpers)
+(require 'mu4e-server)
+
+(defcustom mu4e-query-rewrite-function 'identity
+ "Function to rewrite a query.
+
+It takes a search expression string, and returns a possibly
+ changed search expression string.
+
+This function is applied on the search expression just before
+searching, and allows users to modify the query.
+
+For instance, we could change any instance of \"workmail\" into
+\"maildir:/long-path-to-work-related-emails\", by setting the function
+
+\\=(setq mu4e-query-rewrite-function
+ (lambda(expr)
+ (replace-regexp-in-string \"workmail\"
+ \"maildir:/long-path-to-work-related-emails\" expr)))
+
+It is good to remember that the replacement does not understand
+anything about the query, it just does text replacement.
+
+A word of caution: the function should be deterministic and
+always return the same result for a given query (at least within
+some \"context\" (see `mu4e-context'). If not, you may get incorrect results
+for the various unread counts."
+ :type 'function
+ :group 'mu4e-search)
+
+(defvar mu4e--query-items-baseline nil
+ "Some previous version of the query-items.
+This is used as the baseline to track updates by comparing it to
+the latest query-items.")
+(defvar mu4e--query-items-baseline-tstamp nil
+ "Timestamp for when the query-items baseline was updated.")
+(defvar mu4e--last-delta-unread 0 "Last notified number.")
+
+(defun mu4e--bookmark-query (bm)
+ "Get the query string for some bookmark BM."
+ (when bm
+ (let* ((query (or (plist-get bm :query)
+ (mu4e-warn "No query in %S" bm)))
+ ;; queries being functions is deprecated, but for now we
+ ;; still support it.
+ (query (if (functionp query) (funcall query) query)))
+ (unless (stringp query)
+ (mu4e-warn "Could not get query string from %s" bm))
+ ;; apparently, non-UTF8 queries exist, i.e.,
+ ;; with maildir names.
+ (decode-coding-string query 'utf-8 t))))
+
+(defun mu4e--query-items-pick-favorite (items)
+ "Pick the :favorite querty item.
+If ITEMS does not yet have a favorite item, pick the first."
+ (unless (seq-find
+ (lambda (item) (plist-get item :favorite)) items)
+ (plist-put (car items) :favorite t))
+ items)
+
+(defvar mu4e--bookmark-items-cached nil "Cached bookmarks query items.")
+(defvar mu4e--maildir-items-cached nil "Cached maildirs query items.")
+
+(declare-function mu4e-bookmarks "mu4e-bookmarks")
+(declare-function mu4e-maildir-shortcuts "mu4e-folders")
+
+(defun mu4e--query-item-display-counts (item)
+ "Get the count display string for some query-data ITEM."
+ ;; purely for display, but we need it in the main menu, modeline
+ ;; so let's keep it consistent.
+ (cl-destructuring-bind (&key unread hide-unread delta-unread count
+ &allow-other-keys) item
+ (if hide-unread
+ ""
+ (concat
+ (propertize (number-to-string unread)
+ 'face 'mu4e-header-key-face
+ 'help-echo "Number of unread")
+ (if (<= delta-unread 0) ""
+ (propertize (format "(%+d)" delta-unread) 'face
+ 'mu4e-unread-face))
+ "/"
+ (propertize (number-to-string count)
+ 'help-echo "Total number")))))
+
+(defun mu4e--query-items-refresh (&optional reset-baseline)
+ "Get the latest query data from the mu4e server.
+With RESET-BASELINE, reset the baseline first."
+ (when reset-baseline
+ (setq mu4e--query-items-baseline nil
+ mu4e--query-items-baseline-tstamp nil
+ mu4e--bookmark-items-cached nil
+ mu4e--maildir-items-cached nil
+ mu4e--last-delta-unread 0))
+ (mu4e--server-queries
+ ;; note: we must apply the rewrite function here, since the query does not go
+ ;; through mu4e-search.
+ (mapcar (lambda (bm)
+ (funcall mu4e-query-rewrite-function
+ (mu4e--bookmark-query bm)))
+ (seq-filter (lambda (item)
+ (and (not (or (plist-get item :hide)
+ (plist-get item :hide-unread)))))
+ (mu4e-query-items)))))
+
+(defun mu4e--query-items-queries-handler (_sexp)
+ "Handler for queries responses from the mu4e-server.
+I.e. what we get in response to mu4e--query-items-refresh."
+ ;; if we cleared the baseline (in mu4e--query-items-refresh)
+ ;; set it to the latest now.
+ (unless mu4e--query-items-baseline
+ (setq mu4e--query-items-baseline (mu4e-server-query-items)
+ mu4e--query-items-baseline-tstamp (current-time)))
+
+ (setq mu4e--bookmark-items-cached nil
+ mu4e--maildir-items-cached nil)
+ (mu4e-query-items) ;; for side-effects
+ ;; tell the world.
+ (run-hooks 'mu4e-query-items-updated-hook))
+
+;; this makes for O(n*m)... but with typically small(ish) n,m. Perhaps use a
+;; hash for last-query-items and baseline-results?
+(defun mu4e--query-find-item (query data)
+ "Find the item in DATA for the given QUERY."
+ (seq-find (lambda (item)
+ (equal query (mu4e--bookmark-query item)))
+ data))
+
+(defun mu4e--make-query-items (data type)
+ "Map the items in DATA to plists with aggregated query information.
+
+DATA is either the bookmarks or maildirs (user-defined).
+
+LAST-RESULTS-DATA contains unread/counts we received from the
+server, while BASELINE-DATA contains the same but taken at some
+earier time.
+
+The TYPE denotes the category for the query item, a symbol
+bookmark or maildir."
+ (seq-map
+ (lambda (item)
+ (let* ((maildir (plist-get item :maildir))
+ ;; for maildirs, construct the query
+ (query (if (equal type 'maildirs)
+ (format "maildir:\"%s\"" maildir)
+ (plist-get item :query)))
+ (query (if (functionp query) (funcall query) query))
+ (name (plist-get item :name))
+ ;; it is possible that the user has a rewrite function
+ (effective-query (funcall mu4e-query-rewrite-function query))
+ ;; maildir items may have an implicit name
+ ;; which is the maildir value.
+ (name (or name (and (equal type 'maildirs) maildir)))
+ (last-results (mu4e-server-query-items))
+ (baseline mu4e--query-items-baseline)
+ ;; we use the _effective_ query to find the results,
+ ;; since that's what the server will give to us.
+ (baseline-item
+ (mu4e--query-find-item effective-query baseline))
+ (last-results-item
+ (mu4e--query-find-item effective-query last-results))
+ (count (or (plist-get last-results-item :count) 0))
+ (unread (or (plist-get last-results-item :unread) 0))
+ (baseline-count (or (plist-get baseline-item :count) count))
+ (baseline-unread (or (plist-get baseline-item :unread) unread))
+ (delta-unread (- unread baseline-unread))
+ (value
+ (list
+ :name name
+ :query query
+ :key (plist-get item :key)
+ :count count
+ :unread unread
+ :delta-count (- count baseline-count)
+ :delta-unread delta-unread)))
+ ;; remember the *effective* query too; we don't really need it, but
+ ;; useful for debugging.
+ (unless (string= query effective-query)
+ (plist-put value :effective-query effective-query))
+
+ ;; nil props bring me discomfort
+ (when (plist-get item :favorite)
+ (plist-put value :favorite t))
+ (when (plist-get item :hide)
+ (plist-put value :hide t))
+ (when (plist-get item :hide-unread)
+ (plist-put value :hide-unread t))
+ value))
+ data))
+
+(defun mu4e-query-items (&optional type)
+ "Grab query items of TYPE.
+
+TYPE is symbol; either bookmarks or maildirs, or nil for both.
+
+This combines:
+ - the latest queries data (i.e., `(mu4e-server-query-items)')
+ - baseline queries data (i.e. `mu4e-baseline')
+ with the combined queries for `(mu4e-bookmarks)' and
+ `(mu4e-maildir-shortcuts)' in bookmarks-compatible plists.
+
+This packages the aggregated information in a format that is convenient
+for use in various places."
+ (cond
+ ((equal type 'bookmarks)
+ (or mu4e--bookmark-items-cached
+ (setq mu4e--bookmark-items-cached
+ (mu4e--query-items-pick-favorite
+ (mu4e--make-query-items (mu4e-bookmarks) 'bookmarks)))))
+ ((equal type 'maildirs)
+ (or mu4e--maildir-items-cached
+ (setq mu4e--maildir-items-cached
+ (mu4e--make-query-items (mu4e-maildir-shortcuts) 'maildirs))))
+ ((not type)
+ (append (mu4e-query-items 'bookmarks)
+ (mu4e-query-items 'maildirs)))
+ (t
+ (mu4e-error "No such type %s" type))))
+
+(provide 'mu4e-query-items)
+;;; mu4e-query-items.el ends here
--- /dev/null
+;;; mu4e-search.el --- Search-related functions -*- lexical-binding: t -*-
+
+;; Copyright (C) 2021,2022 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Search-related functions and a minor-mode.
+
+;;; Code:
+
+(require 'seq)
+(require 'mu4e-helpers)
+(require 'mu4e-message)
+(require 'mu4e-bookmarks)
+(require 'mu4e-contacts)
+(require 'mu4e-lists)
+(require 'mu4e-mark)
+(require 'mu4e-query-items)
+
+\f
+;;; Configuration
+(defgroup mu4e-search nil
+ "Search-related settings."
+ :group 'mu4e)
+
+(defcustom mu4e-search-results-limit 500
+ "Maximum number of results to show.
+This affects performance, especially when
+`mu4e-summary-include-related' is non-nil.
+Set to -1 for no limits."
+ :type '(choice (const :tag "Unlimited" -1)
+ (integer :tag "Limit"))
+ :group 'mu4e-search)
+
+(defcustom mu4e-search-full nil
+ "Whether to search for all results.
+If this is nil, search for up to `mu4e-search-results-limit')"
+ :type 'boolean
+ :group 'mu4e-search)
+
+(defcustom mu4e-search-threads t
+ "Whether to calculate threads for the search results."
+ :type 'boolean
+ :group 'mu4e-search)
+
+(defcustom mu4e-search-include-related t
+ "Whether to include \"related\" messages in queries.
+With this option set to non-nil, not just return the matches for
+a searches, but also messages that are related (through their
+references) to these messages. This can be useful e.g. to include
+sent messages into message threads."
+ :type 'boolean
+ :group 'mu4e-search)
+
+(defcustom mu4e-search-skip-duplicates t
+ "Whether to skip duplicate messages.
+With this option set to non-nil, show only one of duplicate
+messages. This is useful when you have multiple copies of the same
+message, which is a common occurrence for example when using Gmail
+and offlineimap."
+ :type 'boolean
+ :group 'mu4e-search)
+
+(defvar mu4e-search-hide-predicate nil
+ "Predicate function to hide matching headers.
+Either nil or a function taking one message plist parameter and
+which which return non-nil for messages that should be hidden from
+the search results. Also see `mu4e-search-hide-enabled'.
+
+Example that hides all trashed messages:
+
+ (setq mu4e-search-hide-predicate
+ (lambda (msg)
+ (member \\='trashed (mu4e-message-field msg :flags)))).")
+
+(defvar mu4e-search-hide-enabled t
+ "Whether `mu4e-search-hide-predicate' should be active.
+This can be used to toggle use of the predicate through
+ `mu4e-search-toggle-property'.")
+
+
+(defcustom mu4e-search-sort-field :date
+ "Field to sort the headers by. A symbol:
+one of: `:date', `:subject', `:size', `:prio', `:from', `:to.',
+`:list'.
+
+Note that when threading is enabled (through
+`mu4e-search-threads'), the headers are exclusively sorted
+chronologically (`:date') by the newest message in the thread."
+ :type '(radio (const :date)
+ (const :subject)
+ (const :size)
+ (const :prio)
+ (const :from)
+ (const :to)
+ (const :list))
+ :group 'mu4e-search)
+
+(defcustom mu4e-search-sort-direction 'descending
+ "Direction to sort by; a symbol either `descending' (sorting
+ Z->A) or `ascending' (sorting A->Z)."
+ :type '(radio (const ascending)
+ (const descending))
+ :group 'mu4e-search)
+
+;; mu4e-query-rewrite-function lives in mu4e-query-items.el
+;; to avoid circular deps.
+
+(defcustom mu4e-search-bookmark-hook nil
+ "Hook run just after invoking a bookmarked search.
+
+This function receives the query as its parameter, before any
+rewriting as per `mu4e-query-rewrite-function' has taken place.
+
+The reason to use this instead of `mu4e-search-hook' is
+if you only want to execute a hook when a search is entered via a
+bookmark, e.g. if you'd like to treat the bookmarks as a custom
+folder and change the options for the search."
+ :type 'hook
+ :group 'mu4e-search)
+
+(defcustom mu4e-search-hook nil
+ "Hook run just before executing a new search operation.
+This function receives the query as its parameter, before any
+rewriting as per `mu4e-query-rewrite-function' has taken place
+
+This is a more general hook facility than the
+`mu4e-search-bookmark-hook'. It gets called on every
+executed search, not just those that are invoked via bookmarks,
+but also manually invoked searches."
+ :type 'hook
+ :group 'mu4e-search)
+\f
+;; Internals
+
+;;; History
+(defvar mu4e--search-query-past nil
+ "Stack of queries before the present one.")
+(defvar mu4e--search-query-future nil "Stack of queries after the present one.")
+(defvar mu4e--search-query-stack-size 20
+ "Maximum size for the query stacks.")
+(defvar mu4e--search-last-query nil
+ "The present (most recent) query.")
+
+
+\f
+;;; Interactive functions
+(declare-function mu4e--search-execute "mu4e-headers")
+
+(defvar mu4e--search-view-target nil
+ "Whether to automatically view (open) the target message.")
+(defvar mu4e--search-msgid-target nil
+ "Message-id to jump to after the search has finished.")
+
+
+(defun mu4e-search (&optional expr prompt edit ignore-history msgid show)
+ "Search for query EXPR.
+
+Switch to the output buffer for the results. This is an
+interactive function which ask user for EXPR. PROMPT, if non-nil,
+is the prompt used by this function (default is \"Search for:\").
+If EDIT is non-nil, instead of executing the query for EXPR, let
+the user edit the query before executing it.
+
+If IGNORE-HISTORY is true, do *not* update the query history
+stack. If MSGID is non-nil, attempt to move point to the first
+message with that message-id after searching. If SHOW is non-nil,
+show the message with MSGID."
+ (interactive)
+ (let* ((prompt (mu4e-format (or prompt "Search for: ")))
+ (expr
+ (if (or (null expr) edit)
+ (mu4e-search-read-query prompt expr)
+ expr)))
+ (mu4e-mark-handle-when-leaving)
+ (mu4e--search-execute expr ignore-history)
+ (setq mu4e--search-msgid-target msgid
+ mu4e--search-view-target show)
+ (mu4e--modeline-update)))
+
+(defun mu4e-search-edit ()
+ "Edit the last search expression."
+ (interactive)
+ (mu4e-search mu4e--search-last-query nil t))
+
+(defun mu4e-search-bookmark (&optional expr edit)
+ "Search using some bookmarked query EXPR.
+If EDIT is non-nil, let the user edit the bookmark before starting
+the search."
+ (interactive)
+ (let* ((expr
+ (or expr
+ (mu4e-ask-bookmark
+ (if edit "Select bookmark: " "Bookmark: "))))
+ (expr (if (functionp expr) (funcall expr) expr))
+ (fav (mu4e--bookmark-query (mu4e-bookmark-favorite))))
+ ;; reset baseline when searching for the favorite bookmark query
+ (when (and fav (string= fav expr))
+ (mu4e--query-items-refresh 'reset-baseline))
+
+ (run-hook-with-args 'mu4e-search-bookmark-hook expr)
+ (mu4e-search expr (when edit "Edit query: ") edit)))
+
+(defun mu4e-search-bookmark-edit ()
+ "Edit an existing bookmark before executing it."
+ (interactive)
+ (mu4e-search-bookmark nil t))
+
+
+(defun mu4e-search-maildir (maildir &optional edit)
+ "Search the messages in MAILDIR.
+The user is prompted to ask what maildir. If prefix-argument EDIT
+is given, offer to edit the search query before executing it."
+ (interactive
+ (let ((maildir (mu4e-ask-maildir "Jump to maildir: ")))
+ (list maildir current-prefix-arg)))
+ (when maildir
+ (let* ((query (format "maildir:\"%s\"" maildir))
+ (query (if edit
+ (mu4e-search-read-query "Refine query: " query) query)))
+ (mu4e-mark-handle-when-leaving)
+ (mu4e-search query))))
+
+(defun mu4e-search-narrow(&optional filter)
+ "Narrow the last search.
+Do so by appending search expression FILTER to the last search
+expression. Note that you can go back to the previous
+query (effectively, \"widen\" it), with `mu4e-search-prev'."
+ (interactive
+ (let ((filter
+ (read-string (mu4e-format "Narrow down to: ")
+ nil 'mu4e~headers-search-hist nil t)))
+ (list filter)))
+ (unless mu4e--search-last-query
+ (mu4e-warn "There's nothing to filter"))
+ (mu4e-search (format "(%s) AND (%s)" mu4e--search-last-query filter)))
+
+(defun mu4e--search-push-query (query where)
+ "Push QUERY to one of the query stacks.
+WHERE is a symbol telling us where to push; it's a symbol, either
+`future' or `past'. Also removes duplicates and truncates to
+limit the stack size."
+ (let ((stack
+ (pcase where
+ ('past mu4e--search-query-past)
+ ('future mu4e--search-query-future))))
+ ;; only add if not the same item
+ (unless (and stack (string= (car stack) query))
+ (push query stack)
+ ;; limit the stack to `mu4e--search-query-stack-size' elements
+ (when (> (length stack) mu4e--search-query-stack-size)
+ (setq stack (cl-subseq stack 0 mu4e--search-query-stack-size)))
+ ;; remove all duplicates of the new element
+ (seq-remove (lambda (elm) (string= elm (car stack))) (cdr stack))
+ ;; update the stacks
+ (pcase where
+ ('past (setq mu4e--search-query-past stack))
+ ('future (setq mu4e--search-query-future stack))))))
+
+(defun mu4e--search-pop-query (whence)
+ "Pop a query from the stack.
+WHENCE is a symbol telling us where to get it from, either `future'
+or `past'."
+ (pcase whence
+ ('past
+ (unless mu4e--search-query-past
+ (mu4e-warn "No more previous queries"))
+ (pop mu4e--search-query-past))
+ ('future
+ (unless mu4e--search-query-future
+ (mu4e-warn "No more next queries"))
+ (pop mu4e--search-query-future))))
+
+(defun mu4e-search-rerun ()
+ "Re-run the search for the last search expression."
+ (interactive)
+ ;; if possible, try to return to the same message
+ (let* ((msg (mu4e-message-at-point t))
+ (msgid (and msg (mu4e-message-field msg :message-id))))
+ (mu4e-search mu4e--search-last-query nil nil t msgid)))
+
+(defun mu4e--search-query-navigate (whence)
+ "Execute the previous query from the query stacks.
+WHENCE determines where the query is taken from and is a symbol,
+either `future' or `past'."
+ (let ((query (mu4e--search-pop-query whence))
+ (where (if (eq whence 'future) 'past 'future)))
+ (when query
+ (mu4e--search-push-query mu4e--search-last-query where)
+ (mu4e-search query nil nil t))))
+
+(defun mu4e-search-next ()
+ "Execute the next query from the query stack."
+ (interactive)
+ (mu4e--search-query-navigate 'future))
+
+(defun mu4e-search-prev ()
+ "Execute the previous query from the query stacks."
+ (interactive)
+ (mu4e--search-query-navigate 'past))
+
+;; forget the past so we don't repeat it :/
+(defun mu4e-search-forget ()
+ "Forget the search history."
+ (interactive)
+ (setq mu4e--search-query-past nil
+ mu4e--search-query-future nil)
+ (mu4e-message "Query history cleared"))
+
+(defun mu4e-last-query ()
+ "Get the most recent query or nil if there is none."
+ mu4e--search-last-query)
+\f
+;;; Completion for queries
+
+(defvar mu4e--search-hist nil "History list of searches.")
+(defvar mu4e-minibuffer-search-query-map
+ (let ((map (copy-keymap minibuffer-local-map)))
+ (define-key map (kbd "TAB") #'completion-at-point)
+ map)
+ "The keymap for reading a search query.")
+
+(defun mu4e-search-read-query (prompt &optional initial-input)
+ "Read a query with completion using PROMPT and INITIAL-INPUT."
+ (minibuffer-with-setup-hook
+ (lambda ()
+ (setq-local completion-at-point-functions
+ #'mu4e--search-query-completion-at-point)
+ (use-local-map mu4e-minibuffer-search-query-map))
+ (read-string prompt initial-input 'mu4e--search-hist)))
+
+(defconst mu4e--search-query-keywords
+ '("and" "or" "not"
+ "from:" "to:" "cc:" "bcc:" "contact:" "recip:" "date:" "subject:" "body:"
+ "list:" "maildir:" "flag:" "mime:" "file:" "prio:" "tag:" "msgid:"
+ "size:" "embed:"))
+
+(defun mu4e--search-completion-contacts-action (match _status)
+ "Delete contact alias from contact autocompletion, leaving just email address.
+Implements the `completion-extra-properties' :exit-function' which
+requires a function with arguments string MATCH and completion
+status, STATUS."
+ (let ((contact-email (replace-regexp-in-string "^.*<\\|>$" "" match)))
+ (delete-char (- (length match)))
+ (insert contact-email)))
+
+(defun mu4e--search-query-completion-at-point ()
+ "Provide completion when entering search expressions."
+ (cond
+ ((not (looking-back "[:\"][^ \t]*" nil))
+ (let ((bounds (bounds-of-thing-at-point 'word)))
+ (list (or (car bounds) (point))
+ (or (cdr bounds) (point))
+ mu4e--search-query-keywords)))
+ ((looking-back "flag:\\(\\w*\\)" nil)
+ (list (match-beginning 1)
+ (match-end 1)
+ '("attach" "draft" "flagged" "list" "new" "passed" "replied"
+ "seen" "trashed" "unread" "encrypted" "signed" "personal")))
+ ((looking-back "maildir:\\([a-zA-Z0-9/.]*\\)" nil)
+ (list (match-beginning 1)
+ (match-end 1)
+ (mapcar (lambda (dir)
+ ;; Quote maildirs with whitespace in their name, e.g.,
+ ;; maildir:"Foobar/Junk Mail".
+ (if (string-match-p "[[:space:]]" dir)
+ (concat "\"" dir "\"")
+ dir))
+ (mu4e-get-maildirs))))
+ ((looking-back "prio:\\(\\w*\\)" nil)
+ (list (match-beginning 1)
+ (match-end 1)
+ (list "high" "normal" "low")))
+ ((looking-back "mime:\\([a-zA-Z0-9/-]*\\)" nil)
+ (list (match-beginning 1)
+ (match-end 1)
+ (when (fboundp 'mailcap-mime-types) (mailcap-mime-types))))
+ ((looking-back "\\(from\\|to\\|cc\\|bcc\\|contact\\|recip\\):\\([a-zA-Z0-9/.@]*\\)" nil)
+ (list (match-beginning 2)
+ (match-end 2)
+ mu4e--contacts-set
+ :exit-function
+ #'mu4e--search-completion-contacts-action))
+ ((looking-back "list:\\([a-zA-Z0-9/.@]*\\)" nil)
+ (list (match-beginning 1)
+ (match-end 1)
+ mu4e--lists-hash))))
+
+;;; Interactive functions
+(defun mu4e-search-change-sorting (&optional field dir)
+ "Change the sorting/threading parameters.
+FIELD is the field to sort by; DIR is a symbol: either
+`ascending', `descending', t (meaning: if FIELD is the same as
+the current sortfield, change the sort-order) or nil (ask the
+user).
+
+When threads are enabled (`mu4e-search-threads'), you can only sort
+by the `:date' field."
+ (interactive)
+ (let* ((choices ;; with threads enabled, you can only sort by *date*
+ (if mu4e-search-threads
+ '(("date" . :date))
+ '(("date" . :date)
+ ("from" . :from)
+ ("list" . :list)
+ ("maildir" . :maildir)
+ ("prio" . :prio)
+ ("zsize" . :size)
+ ("subject" . :subject)
+ ("to" . :to))))
+ (field
+ (or field
+ (mu4e-read-option "Sortfield: " choices)))
+ ;; note: 'sortable' is either a boolean (meaning: if non-nil, this is
+ ;; sortable field), _or_ another field (meaning: sort by this other
+ ;; field).
+ (sortable (plist-get (cdr (assoc field mu4e-header-info)) :sortable))
+ ;; error check
+ (sortable
+ (if sortable
+ sortable
+ (mu4e-error "Not a sortable field")))
+ (sortfield (if (booleanp sortable) field sortable))
+ (dir
+ (cl-case dir
+ ((ascending descending) dir)
+ ;; change the sort order if field = curfield
+ (t
+ (if (eq sortfield mu4e-search-sort-field)
+ (if (eq mu4e-search-sort-direction 'ascending)
+ 'descending 'ascending)
+ 'descending)))))
+ (setq
+ mu4e-search-sort-field sortfield
+ mu4e-search-sort-direction dir)
+ (mu4e-message "Sorting by %s (%s)"
+ (symbol-name sortfield)
+ (symbol-name mu4e-search-sort-direction))
+ (mu4e-search-rerun)))
+
+(defun mu4e-search-toggle-property (&optional dont-refresh)
+ "Toggle some aspect of search.
+When prefix-argument DONT-REFRESH is non-nil, do not refresh the
+last search with the new setting."
+ (interactive "P")
+ (let* ((toggles '(("fFull-search" . mu4e-search-full)
+ ("rInclude-related" . mu4e-headers-include-related)
+ ("tShow threads" . mu4e-search-threads)
+ ("uSkip duplicates" . mu4e-search-skip-duplicates)
+ ("pHide-predicate" . mu4e-search-hide-enabled)))
+ (toggles (seq-map
+ (lambda (cell)
+ (cons
+ (concat (car cell)
+ (format" (%s)"
+ (if (symbol-value (cdr cell)) "on" "off")))
+ (cdr cell)))
+ toggles))
+ (choice (mu4e-read-option "Toggle property " toggles)))
+ (when choice
+ (set choice (not (symbol-value choice)))
+ (mu4e-message "Set `%s' to %s" (symbol-name choice) (symbol-value choice))
+ (mu4e--modeline-update)
+ (unless dont-refresh
+ (mu4e-search-rerun)))))
+
+(defvar mu4e-search-threaded-label '("T" . "Ⓣ")
+ "Non-fancy and fancy labels to indicate threaded search in the mode-line.")
+(defvar mu4e-search-full-label '("F" . "Ⓕ")
+ "Non-fancy and fancy labels to indicate full search in the mode-line.")
+(defvar mu4e-search-related-label '("R" . "Ⓡ")
+ "Non-fancy and fancy labels to indicate related search in the mode-line.")
+(defvar mu4e-search-skip-duplicates-label '("U" . "Ⓤ") ;; 'U' for 'unique'
+ "Non-fancy and fancy labels for include-related search in the mode-line.")
+(defvar mu4e-search-hide-label '("H" . "Ⓗ")
+ "Non-fancy and fancy labels to indicate header-hiding is active in
+the mode-line.")
+
+(defun mu4e--search-modeline-item ()
+ "Get mu4e-search modeline item."
+ (let* ((label (lambda (label-cons)
+ (if mu4e-use-fancy-chars
+ (cdr label-cons) (car label-cons))))
+ (props
+ `((,mu4e-search-full ,mu4e-search-full-label
+ "Full search")
+ (,mu4e-search-include-related
+ ,mu4e-search-related-label
+ "Include related messages")
+ (,mu4e-search-threads
+ ,mu4e-search-threaded-label
+ "Show message threads")
+ (,mu4e-search-skip-duplicates
+ ,mu4e-search-skip-duplicates-label
+ "Skip duplicate messages")
+ (,mu4e-search-hide-enabled
+ ,mu4e-search-hide-label
+ "Enable message hide predicate")))
+ ;; can we fin find a bookmark corresponding
+ ;; with this query?
+ (bookmark
+ (and mu4e-modeline-prefer-bookmark-name
+ (seq-find (lambda (item)
+ (string=
+ mu4e--search-last-query
+ (or (plist-get item :effective-query)
+ (plist-get item :query))))
+ (mu4e-query-items 'bookmarks)))))
+ (concat
+ (propertize
+ (mapconcat
+ (lambda (cell)
+ (when (nth 0 cell) (funcall label (nth 1 cell))))
+ props "")
+ 'help-echo (concat "mu4e search properties legend\n\n"
+ (mapconcat
+ (lambda (cell)
+ (format "%s %s (%s)"
+ (funcall label (nth 1 cell))
+ (nth 2 cell)
+ (if (nth 0 cell) "yes" : "no")))
+ props "\n")))
+ " ["
+ (propertize
+ (if bookmark ;; show the bookmark name instead of the query?
+ (plist-get bookmark :name)
+ mu4e--search-last-query)
+ 'face 'mu4e-title-face
+ 'help-echo (format "mu4e query:\n\t%s" mu4e--search-last-query))
+ "]")))
+
+(defun mu4e-search-query (&optional edit)
+ "Select a search query through `completing-read'.
+
+If prefix-argument EDIT is non-nil, allow for editing the chosen
+query before submitting it."
+ (interactive "P")
+ (let* ((candidates (seq-map (lambda (item)
+ (cons (plist-get item :name) item))
+ (mu4e-query-items)))
+ (longest-name
+ (seq-max (seq-map (lambda (c) (length (car c))) candidates)))
+ (longest-query
+ (seq-max (seq-map (lambda (c) (length (plist-get (cdr c) :query)))
+ candidates)))
+
+ (annotation-func
+ (lambda (candidate)
+ (let* ((item (cdr-safe (assoc candidate candidates)))
+ (name (propertize (or (plist-get item :name) "")
+ 'face 'mu4e-header-key-face))
+ (query (propertize (or (plist-get item :query) "")
+ 'face 'mu4e-header-value-face)))
+ (concat
+ " "
+ (make-string (- longest-name (length name)) ?\s)
+ query
+ (make-string (- longest-query (length query)) ?\s)
+ " "
+ (mu4e--query-item-display-counts item)))))
+ (completion-extra-properties
+ `(:annotation-function ,annotation-func))
+ (chosen (completing-read "Query: " candidates))
+ (query (or (plist-get (cdr-safe (assoc chosen candidates)) :query)
+ (mu4e-warn "No query for %s" chosen))))
+ (mu4e-search-bookmark query edit)))
+
+(defvar mu4e-search-minor-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map "s" #'mu4e-search)
+ (define-key map "S" #'mu4e-search-edit)
+ (define-key map "/" #'mu4e-search-narrow)
+
+ (define-key map (kbd "<M-left>") #'mu4e-search-prev)
+ (define-key map (kbd "\\") #'mu4e-search-prev)
+ (define-key map (kbd "<M-right>") #'mu4e-search-next)
+
+ (define-key map "O" #'mu4e-search-change-sorting)
+ (define-key map "P" #'mu4e-search-toggle-property)
+
+ (define-key map "b" #'mu4e-search-bookmark)
+ (define-key map "B" #'mu4e-search-bookmark-edit)
+
+ (define-key map "c" #'mu4e-search-query)
+
+ (define-key map "j" #'mu4e-search-maildir)
+ map)
+ "Keymap for mu4e-search-minor-mode.")
+
+(define-minor-mode mu4e-search-minor-mode
+ "Mode for searching for messages."
+ :global nil
+ :init-value nil ;; disabled by default
+ :group 'mu4e
+ :lighter ""
+ :keymap mu4e-search-minor-mode-map)
+
+(defvar mu4e--search-menu-items
+ '("--"
+ ["Search" mu4e-search
+ :help "Search using expression"]
+ ["Search bookmark" mu4e-search-bookmark
+ :help "Show messages matching some bookmark query"]
+ ["Search maildir" mu4e-search-maildir
+ :help "Show messages in some maildir"]
+ ["Choose query" mu4e-search-query
+ :help "Show messages for some query"]
+ ["Previous query" mu4e-search-prev
+ :help "Run previous query"]
+ ["Next query" mu4e-search-next
+ :help "Run next query"]
+ ["Narrow search" mu4e-search-narrow
+ :help "Narrow the search query"])
+ "Easy menu items for search.")
+
+(provide 'mu4e-search)
+;;; mu4e-search.el ends here
--- /dev/null
+;;; mu4e-server.el --- Control mu server from mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2023 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'mu4e-helpers)
+
+\f
+;;; Configuration
+(defcustom mu4e-mu-home nil
+ "Location of an alternate mu home dir.
+If not set, use the defaults, based on the XDG Base Directory
+Specification.
+
+Changes to this value only take effect after (re)starting the mu
+session."
+ :group 'mu4e
+ :type '(choice (const :tag "Default location" nil)
+ (directory :tag "Specify location"))
+ :safe 'stringp)
+
+(defcustom mu4e-mu-binary (executable-find "mu")
+ "Path to the mu-binary to use.
+
+Changes to this value only take effect after (re)starting the mu
+session."
+ :type '(file :must-match t)
+ :group 'mu4e
+ :safe 'stringp)
+
+(defcustom mu4e-mu-debug nil
+ "Whether to run the mu binary in debug-mode.
+Setting this to t increases the amount of information in the log.
+
+Changes to this value only take effect after (re)starting the mu
+session."
+ :type 'boolean
+ :group 'mu4e)
+
+(defcustom mu4e-change-filenames-when-moving nil
+ "Change message file names when moving them.
+
+When moving messages to different folders, normally mu/mu4e keep
+the base filename the same (the flags-part of the filename may
+change still). With this option set to non-nil, mu4e instead
+changes the filename.
+
+This latter behavior works better with some
+IMAP-synchronization programs such as mbsync; the default works
+better with e.g. offlineimap."
+ :type 'boolean
+ :group 'mu4e
+ :safe 'booleanp)
+
+(defcustom mu4e-mu-allow-temp-file nil
+ "Allow using temp-files for optimizing mu <-> mu4e communication.
+
+Some commands - in particular \"find\" and \"contacts\" - return
+big s-expressions; and it turns out that reading those is faster
+by passing them through a temp file rather than through normal
+stdin/stdout channel - esp. on the (common case) where the
+file-system for temp-files is in-memory.
+
+To see if the helps, you can benchmark the rendering with
+ (setq mu4e-headers-report-render-time t)
+
+and compare the results with `mu4e-mu-allow-temp' set and unset.
+
+Note: for a change to this variable to take effect, you need to
+stop/start mu4e."
+ :type 'boolean
+ :group 'mu4e
+ :safe 'booleanp)
+
+\f
+;; Cached data
+(defvar mu4e-maildir-list)
+
+\f
+;; Handlers are not strictly internal, but are not meant
+;; for overriding outside mu4e. The are mainly for breaking
+;; dependency cycles.
+
+(defvar mu4e-error-func nil
+ "Function called for each error received.
+The function is passed an error plist as argument. See
+`mu4e--server-filter' for the format.")
+
+(defvar mu4e-update-func nil
+ "Function called for each :update sexp returned.
+The function is passed a msg sexp as argument.
+See `mu4e--server-filter' for the format.")
+
+(defvar mu4e-remove-func nil
+ "Function called for each :remove sexp returned.
+This happens when some message has been deleted. The function is
+passed the docid of the removed message.")
+
+(defvar mu4e-sent-func nil
+ "Function called for each :sent sexp received.
+This happens when some message has been sent. The function is
+passed the docid and the draft-path of the sent message.")
+
+(defvar mu4e-view-func nil
+ "Function called for each single-message sexp.
+The function is passed a message sexp as argument. See
+`mu4e--server-filter' for the format.")
+
+(defvar mu4e-headers-append-func nil
+ "Function called with a list of headers to append.
+The function is passed a list of message plists as argument. See
+See `mu4e--server-filter' for the details.")
+
+(defvar mu4e-found-func nil
+ "Function called for when we received a :found sexp.
+This happens after the headers have been returned, to report on
+the number of matches. See `mu4e--server-filter' for the format.")
+
+(defvar mu4e-erase-func nil
+ "Function called we receive an :erase sexp.
+This before new headers are displayed, to clear the current
+headers buffer. See `mu4e--server-filter' for the format.")
+
+(defvar mu4e-info-func nil
+ "Function called for each (:info type ....) sexp received.
+from the server process.")
+
+(defvar mu4e-pong-func nil
+ "Function called for each (:pong type ....) sexp received.")
+
+(defvar mu4e-queries-func nil
+ "Function called for each (:queries type ....) sexp received.")
+
+(defvar mu4e-contacts-func nil
+ "A function called for each (:contacts (<list-of-contacts>))
+sexp received from the server process.")
+
+\f
+;;; Dealing with Server properties
+(defvar mu4e--server-props nil
+ "Metadata we receive from the mu4e server.")
+
+(defun mu4e-server-properties ()
+ "Get the server metadata plist."
+ mu4e--server-props)
+
+(defun mu4e-root-maildir()
+ "Get the root maildir."
+ (or (and mu4e--server-props
+ (plist-get mu4e--server-props :root-maildir))
+ (mu4e-error "Root maildir unknown; did you start mu4e?")))
+
+(defun mu4e-database-path()
+ "Get the root maildir."
+ (or (and mu4e--server-props
+ (plist-get mu4e--server-props :database-path))
+ (mu4e-error "Root maildir unknown; did you start mu4e?")))
+
+(defun mu4e-server-version()
+ "Get the root maildir."
+ (or (and mu4e--server-props
+ (plist-get mu4e--server-props :version))
+ (mu4e-error "Version unknown; did you start mu4e?")))
+
+;;; remember queries result.
+(defvar mu4e--server-query-items nil
+ "Query items results we receive from the mu4e server.
+Those are the results from the counting-queries
+for bookmarks and maildirs.")
+
+(defun mu4e-server-query-items ()
+ "Get the latest server query items."
+ mu4e--server-query-items)
+
+\f
+;;; Handling raw server data
+
+(defvar mu4e--server-buf nil
+ "Buffer (string) for data received from the backend.")
+(defconst mu4e--server-name " *mu4e-server*"
+ "Name of the server process, buffer.")
+(defvar mu4e--server-process nil
+ "The mu-server process.")
+
+;; dealing with the length cookie that precedes expressions
+(defconst mu4e--server-cookie-pre "\376"
+ "Each expression starts with a length cookie:
+<`mu4e--server-cookie-pre'><length-in-hex><`mu4e--server-cookie-post'>.")
+(defconst mu4e--server-cookie-post "\377"
+ "Each expression starts with a length cookie:
+<`mu4e--server-cookie-pre'><length-in-hex><`mu4e--server-cookie-post'>.")
+(defconst mu4e--server-cookie-matcher-rx
+ (concat mu4e--server-cookie-pre "\\([[:xdigit:]]+\\)"
+ mu4e--server-cookie-post)
+ "Regular expression matching the length cookie.
+Match 1 will be the length (in hex).")
+\f
+(defun mu4e-running-p ()
+ "Whether mu4e is running.
+Checks whether the server process is live."
+ (and mu4e--server-process
+ (memq (process-status mu4e--server-process)
+ '(run open listen connect stop)) t))
+
+(defsubst mu4e--server-eat-sexp-from-buf ()
+ "Eat the next s-expression from `mu4e--server-buf'.
+Note: this is a string, not an emacs-buffer. `mu4e--server-buf gets
+its contents from the mu-servers in the following form:
+ <`mu4e--server-cookie-pre'><length-in-hex><`mu4e--server-cookie-post'>
+Function returns this sexp, or nil if there was none.
+`mu4e--server-buf' is updated as well, with all processed sexp data
+removed."
+ (ignore-errors ;; the server may die in the middle...
+ (let ((b (string-match mu4e--server-cookie-matcher-rx mu4e--server-buf))
+ (sexp-len) (objcons))
+ (when b
+ (setq sexp-len (string-to-number (match-string 1 mu4e--server-buf) 16))
+ ;; does mu4e--server-buf contain the full sexp?
+ (when (>= (length mu4e--server-buf) (+ sexp-len (match-end 0)))
+ ;; clear-up start
+ (setq mu4e--server-buf (substring mu4e--server-buf (match-end 0)))
+ ;; note: we read the input in binary mode -- here, we take the part
+ ;; that is the sexp, and convert that to utf-8, before we interpret
+ ;; it.
+ (setq objcons (read-from-string
+ (decode-coding-string
+ (substring mu4e--server-buf 0 sexp-len)
+ 'utf-8 t)))
+ (when objcons
+ (setq mu4e--server-buf (substring mu4e--server-buf sexp-len))
+ (car objcons)))))))
+
+(defun mu4e--server-plist-get (plist key)
+ "Like `plist-get' but load data from file if it is a string.
+
+I.e. (mu4e--server-plist-get (:foo bar) :foo)
+ => bar
+but
+ (mu4e--server-plist-get (:foo \"/tmp/data.eld\") :foo)
+ => evaluates the contents of /tmp/data.eld
+ (and deletes the file afterward).
+
+This for the few sexps we get from the mu server that support this
+(headers, contacts, maildirs)."
+ ;; XXX: perhaps re-use the same buffer?
+ (let ((val (plist-get plist key)))
+ (if (stringp val)
+ (with-temp-buffer
+ (insert-file-contents val)
+ (goto-char (point-min))
+ (delete-file val)
+ (read (current-buffer)))
+ val)))
+
+
+(defun mu4e--server-filter (_proc str)
+ "Filter string STR from PROC.
+This processes the \"mu server\" output. It accumulates the
+strings into valid sexpsv and evaluating those.
+
+The server output is as follows:
+
+ 1. an error
+ (:error 2 :message \"unknown command\")
+ ;; eox
+ => passed to `mu4e-error-func'.
+
+ 2a. a header exp looks something like:
+ (:headers
+ ( ;; message 1
+ :docid 1585
+ :from ((\"Donald Duck\" . \"donald@example.com\"))
+ :to ((\"Mickey Mouse\" . \"mickey@example.com\"))
+ :subject \"Wicked stuff\"
+ :date (20023 26572 0)
+ :size 15165
+ :references (\"200208121222.g7CCMdb80690@msg.id\")
+ :in-reply-to \"200208121222.g7CCMdb80690@msg.id\"
+ :message-id \"foobar32423847ef23@pluto.net\"
+ :maildir: \"/archive\"
+ :path \"/home/mickey/Maildir/inbox/cur/1312_3.32282.pluto,4cd5bd4e9:2,\"
+ :priority high
+ :flags (new unread)
+ :meta <meta-data>
+ )
+ ( .... more messages )
+)
+;; eox
+ => this will be passed to `mu4e-headers-append-func'.
+
+ 2b. After the list of headers has been returned (see 2a.),
+ we'll receive a sexp that looks like
+ (:found <n>) with n the number of messages found. The <n> will be
+ passed to `mu4e-found-func'.
+
+ 3. a view looks like:
+ (:view <msg-sexp>)
+ => the <msg-sexp> (see 2.) will be passed to `mu4e-view-func'.
+ the <msg-sexp> also contains :body-txt and/or :body-html
+
+ 4. a database update looks like:
+ (:update <msg-sexp> :move <nil-or-t>)
+ like :header
+
+ => the <msg-sexp> (see 2.) will be passed to
+ `mu4e-update-func', :move tells us whether this is a move to
+ another maildir, or merely a flag change.
+
+ 5. a remove looks like:
+ (:remove <docid>)
+ => the docid will be passed to `mu4e-remove-func'
+
+ 6. a compose looks like:
+ (:compose <reply|forward|edit|new> [:original<msg-sexp>] [:include <attach>])
+ `mu4e-compose-func'. :original looks like :view."
+ (mu4e-log 'misc "* Received %d byte(s)" (length str))
+ (setq mu4e--server-buf (concat mu4e--server-buf str)) ;; update our buffer
+ (let ((sexp (mu4e--server-eat-sexp-from-buf)))
+ (with-local-quit
+ (while sexp
+ (mu4e-log 'from-server "%s" sexp)
+ (cond
+ ;; a list of messages (after a find command)
+ ((plist-get sexp :headers)
+ (funcall mu4e-headers-append-func
+ (mu4e--server-plist-get sexp :headers)))
+
+ ;; the found sexp, we receive after getting all the headers
+ ((plist-get sexp :found)
+ (funcall mu4e-found-func (plist-get sexp :found)))
+
+ ;; viewing a specific message
+ ((plist-get sexp :view)
+ (funcall mu4e-view-func (plist-get sexp :view)))
+
+ ;; receive an erase message
+ ((plist-get sexp :erase)
+ (funcall mu4e-erase-func))
+
+ ;; receive a :sent message
+ ((plist-get sexp :sent)
+ (funcall mu4e-sent-func
+ (plist-get sexp :docid)
+ (plist-get sexp :path)))
+
+ ;; received a pong message
+ ((plist-get sexp :pong)
+ (setq mu4e--server-props (plist-get sexp :props))
+ (funcall mu4e-pong-func sexp))
+
+ ;; receive queries info
+ ((plist-get sexp :queries)
+ (setq mu4e--server-query-items (plist-get sexp :queries))
+ (funcall mu4e-queries-func sexp))
+
+ ;; received a contacts message
+ ;; note: we use 'member', to match (:contacts nil)
+ ((plist-member sexp :contacts)
+ (funcall mu4e-contacts-func
+ (mu4e--server-plist-get sexp :contacts)
+ (plist-get sexp :tstamp)))
+
+ ;; something got moved/flags changed
+ ((plist-get sexp :update)
+ (funcall mu4e-update-func
+ (plist-get sexp :update)
+ (plist-get sexp :move)
+ (plist-get sexp :maybe-view)))
+
+ ;; a message got removed
+ ((plist-get sexp :remove)
+ (funcall mu4e-remove-func (plist-get sexp :remove)))
+
+ ;; get some info
+ ((plist-get sexp :info)
+ (funcall mu4e-info-func sexp))
+
+ ;; get some data
+ ((plist-get sexp :maildirs)
+ (setq mu4e-maildir-list (mu4e--server-plist-get sexp :maildirs)))
+
+ ;; receive an error
+ ((plist-get sexp :error)
+ (funcall mu4e-error-func
+ (plist-get sexp :error)
+ (plist-get sexp :message)))
+
+ (t (mu4e-message "Unexpected data from server [%S]" sexp)))
+
+ (setq sexp (mu4e--server-eat-sexp-from-buf))))))
+
+(defun mu4e--kill-stale ()
+ "Kill stale mu4e server process.
+As per issue #2198."
+ (seq-each
+ (lambda(proc)
+ (when (and (process-live-p proc)
+ (string-prefix-p mu4e--server-name (process-name proc)))
+ (mu4e-message "killing stale mu4e server")
+ (ignore-errors
+ (signal-process proc 'SIGINT) ;; nicely
+ (sit-for 1.0)
+ (signal-process proc 'SIGKILL)))) ;; forcefully
+ (process-list)))
+
+(defun mu4e--server-args()
+ "Return the command line args for the command to start the mu4e-server."
+ ;; [--debug] server [--muhome=..]
+ (seq-filter #'identity ;; filter out nil
+ `(,(when mu4e-mu-debug "--debug")
+ "server"
+ ,(when mu4e-mu-allow-temp-file "--allow-temp-file")
+ ,(when mu4e-mu-home (format "--muhome=%s" mu4e-mu-home)))))
+
+(defun mu4e--version-check ()
+ ;; sanity-check 1
+ (let ((default-directory temporary-file-directory)) ;;ensure it's local.
+ (unless (and mu4e-mu-binary (file-executable-p mu4e-mu-binary))
+ (mu4e-error
+ "Cannot find mu, please set `mu4e-mu-binary' to the mu executable path"))
+ ;; sanity-check 2
+ (let ((version (let ((s (shell-command-to-string
+ (concat mu4e-mu-binary " --version"))))
+ (and (string-match "version \\([.0-9]+\\)" s)
+ (match-string 1 s)))))
+ (if (not (string= version mu4e-mu-version))
+ (mu4e-error
+ (concat
+ "Found mu version %s, but mu4e needs version %s"
+ "; please set `mu4e-mu-binary' "
+ "accordingly")
+ version mu4e-mu-version)
+ (mu4e-message "Found mu version %s" version)))))
+
+(defun mu4e-server-repl ()
+ "Start a mu4e-server repl.
+
+This is meant for debugging/testing - the repl is designed for
+machines, not for humans.
+
+You cannot run the repl when mu4e is running (or vice-versa)."
+ (interactive)
+ (if (mu4e-running-p)
+ (mu4e-error "Cannot run repl when mu4e is running")
+ (progn
+ (mu4e--version-check)
+ (let ((cmd (string-join (cons mu4e-mu-binary (mu4e--server-args)) " ")))
+ (term cmd)
+ (rename-buffer "*mu4e-repl*" 'unique)
+ (message "invoked: '%s'" cmd)))))
+
+(defun mu4e--server-start ()
+ "Start the mu server process."
+ (mu4e--version-check)
+ ;; kill old/stale servers, if any.
+ (mu4e--kill-stale)
+ (let* ((process-connection-type nil) ;; use a pipe
+ (args (mu4e--server-args)))
+ (setq mu4e--server-buf "")
+ (mu4e-log 'misc "* invoking '%s' with parameters %s" mu4e-mu-binary
+ (mapconcat (lambda (arg) (format "'%s'" arg)) args " "))
+ (setq mu4e--server-process (apply 'start-process
+ mu4e--server-name mu4e--server-name
+ mu4e-mu-binary args))
+ ;; register a function for (:info ...) sexps
+ (unless mu4e--server-process
+ (mu4e-error "Failed to start the mu4e backend"))
+ (set-process-query-on-exit-flag mu4e--server-process nil)
+ (set-process-coding-system mu4e--server-process 'binary 'utf-8-unix)
+ (set-process-filter mu4e--server-process 'mu4e--server-filter)
+ (set-process-sentinel mu4e--server-process 'mu4e--server-sentinel)))
+
+(defun mu4e--server-kill ()
+ "Kill the mu server process."
+ (let* ((buf (get-buffer mu4e--server-name))
+ (proc (and (buffer-live-p buf) (get-buffer-process buf))))
+ (when proc
+ (mu4e-message "shutting down")
+ (set-process-filter mu4e--server-process nil)
+ (set-process-sentinel mu4e--server-process nil)
+ (let ((delete-exited-processes t))
+ (mu4e--server-call-mu '(quit)))
+ ;; try sending SIGINT (C-c) to process, so it can exit gracefully
+ (ignore-errors
+ (signal-process proc 'SIGINT))))
+ (setq
+ mu4e--server-process nil
+ mu4e--server-buf nil))
+
+;; error codes are defined in src/mu-util
+;;(defconst mu4e-xapian-empty 19 "Error code: xapian is empty/non-existent")
+
+(defun mu4e--server-sentinel (proc _msg)
+ "Function called when the server process PROC terminates with MSG."
+ (let ((status (process-status proc)) (code (process-exit-status proc)))
+ (mu4e-log 'misc "* famous last words from server: '%s'" mu4e--server-buf)
+ (setq mu4e--server-process nil)
+ (setq mu4e--server-buf "") ;; clear any half-received sexps
+ (cond
+ ((eq status 'signal)
+ (cond
+ ((or(eq code 9) (eq code 2)) (message nil))
+ ;;(message "the mu server process has been stopped"))
+ (t (mu4e-error (format "server process received signal %d" code)))))
+ ((eq status 'exit)
+ (cond
+ ((eq code 0)
+ (message nil)) ;; don't do anything
+ ((eq code 11)
+ (error "schema mismatch; please re-init mu from command-line"))
+ ((eq code 19)
+ (error "mu database is locked by another process"))
+ (t (error "mu server process ended with exit code %d" code))))
+ (t
+ (error "something bad happened to the mu server process")))))
+
+(defun mu4e--server-call-mu (form)
+ "Call the mu server with some command FORM."
+ (unless (mu4e-running-p) (mu4e--server-start))
+ (let* ((print-length nil) (print-level nil)
+ (cmd (format "%S" form)))
+ (mu4e-log 'to-server "%s" cmd)
+ (process-send-string mu4e--server-process (concat cmd "\n"))))
+
+(defun mu4e--server-add (path)
+ "Add the message at PATH to the database.
+On success, we receive `'(:info add :path <path> :docid <docid>)'
+as well as `'(:update <msg-sexp>)`'; otherwise, we receive an error."
+ (mu4e--server-call-mu `(add :path ,path)))
+
+(defun mu4e--server-contacts (personal after maxnum tstamp)
+ "Ask for contacts with PERSONAL AFTER MAXNUM TSTAMP.
+
+S-expression (:contacts (<list>) :tstamp \"<tstamp>\")
+is expected in response.
+
+If PERSONAL is non-nil, only get personal contacts, if AFTER is
+non-nil, get only contacts seen AFTER (the time_t value). If MAX is non-nil,
+get at most MAX contacts."
+ (mu4e--server-call-mu
+ `(contacts
+ :personal ,(and personal t)
+ :after ,(or after nil)
+ :tstamp ,(or tstamp nil)
+ :maxnum ,(or maxnum nil))))
+
+(defun mu4e--server-data (kind)
+ "Request data of some KIND.
+KIND is a symbol. Currently supported kinds: maildirs."
+ (mu4e--server-call-mu
+ `(data :kind ,kind)))
+
+(defun mu4e--server-find (query threads sortfield sortdir maxnum skip-dups
+ include-related)
+ "Run QUERY with THREADS SORTFIELD SORTDIR MAXNUM SKIP-DUPS INCLUDE-RELATED.
+
+If THREADS is non-nil, show results in threaded fashion,
+SORTFIELD is a symbol describing the field to sort by (or nil);
+see `mu4e~headers-sortfield-choices'. If SORT is `descending',
+sort Z->A, if it's `ascending', sort A->Z. MAXNUM determines the
+maximum number of results to return, or nil for unlimited. If
+SKIP-DUPS is non-nil, show only one of duplicate messages (see
+`mu4e-headers-skip-duplicates'). If INCLUDE-RELATED is non-nil,
+include messages related to the messages matching the search
+query (see `mu4e-headers-include-related').
+
+For each result found, a function is called, depending on the
+kind of result. The variables `mu4e-error-func' contain the
+function that to be be called for, resp., a message (header)
+or an error."
+ (mu4e--server-call-mu
+ `(find
+ :query ,query
+ :threads ,(and threads t)
+ :sortfield ,sortfield
+ :descending ,(if (eq sortdir 'descending) t nil)
+ :maxnum ,maxnum
+ :skip-dups ,(and skip-dups t)
+ :include-related ,(and include-related t))))
+
+(defun mu4e--server-index (&optional cleanup lazy-check)
+ "Index messages.
+If CLEANUP is non-nil, remove messages which are in the database
+but no longer in the filesystem. If LAZY-CHECK is non-nil, only
+consider messages for which the time stamp (ctime) of the
+directory they reside in has not changed since the previous
+indexing run. This is much faster than the non-lazy check, but
+won't update messages that have change (rather than having been
+added or removed), since merely editing a message does not update
+the directory time stamp."
+ (mu4e--server-call-mu
+ `(index :cleanup ,(and cleanup t)
+ :lazy-check ,(and lazy-check t))))
+
+(defun mu4e--server-mkdir (path &optional update)
+ "Create a new maildir-directory at filesystem PATH.
+When UPDATE is non-nil, send a update when completed.
+PATH must be below the root-maildir."
+ ;; handle maildir cache
+ (if (not (string-prefix-p (mu4e-root-maildir) path))
+ (mu4e-error "Cannot create maildir outside root-maildir")
+ (add-to-list 'mu4e-maildir-list ;; update cache
+ (substring path (length (mu4e-root-maildir)))))
+ (mu4e--server-call-mu `(mkdir
+ :path ,path
+ :update ,(or update nil))))
+
+(defun mu4e--server-move (docid-or-msgid &optional maildir flags no-view)
+ "Move message identified by DOCID-OR-MSGID.
+Optionally to MAILDIR and optionally setting FLAGS. If MAILDIR is
+nil, message will be moved within the same maildir.
+
+At least one of MAILDIR and FLAGS must be specified. Note that
+even when MAILDIR is nil, this is still a filesystem move, since
+a change in flags implies a change in message filename.
+
+MAILDIR must be a maildir, that is, the part _without_ cur/ or new/
+or the root-maildir-prefix. E.g. \"/archive\". This directory must
+already exist.
+
+The FLAGS parameter can have the following forms:
+ 1. a list of flags such as `(passed replied seen)'
+ 2. a string containing the one-char versions of the flags, e.g. \"PRS\"
+ 3. a delta-string specifying the changes with +/- and the one-char flags,
+ e.g. \"+S-N\" to set Seen and remove New.
+
+The flags are any of `deleted', `flagged', `new', `passed', `replied' `seen' or
+`trashed', or the corresponding \"DFNPRST\" as defined in [1]. See
+`mu4e-string-to-flags' and `mu4e-flags-to-string'.
+The server reports the results for the operation through
+`mu4e-update-func'.
+
+If the variable `mu4e-change-filenames-when-moving' is
+non-nil, moving to a different maildir generates new names forq
+the target files; this helps certain tools (such as mbsync).
+
+If NO-VIEW is non-nil, do not update the view.
+
+Returns either (:update ... ) or (:error ) sexp, which are handled my
+`mu4e-update-func' and `mu4e-error-func', respectively."
+ (unless (or maildir flags)
+ (mu4e-error "At least one of maildir and flags must be specified"))
+ (unless (or (not maildir)
+ (file-exists-p
+ (mu4e-join-paths (mu4e-root-maildir) maildir)))
+ (mu4e-error "Target directory does not exist"))
+ (mu4e--server-call-mu
+ `(move
+ :docid ,(if (stringp docid-or-msgid) nil docid-or-msgid)
+ :msgid ,(if (stringp docid-or-msgid) docid-or-msgid nil)
+ :flags ,(or flags nil)
+ :maildir ,(or maildir nil)
+ :rename ,(and maildir mu4e-change-filenames-when-moving t)
+ :no-view ,(and no-view t))))
+
+(defun mu4e--server-ping ()
+ "Sends a ping to the mu server, expecting a (:pong ...) in response."
+ (mu4e--server-call-mu `(ping)))
+
+(defun mu4e--server-queries (queries)
+ "Sends queries to the mu server, expecting a (:queries ...) sexp in response.
+QUERIES is a list of queries for the number of results with
+read/unread status are returned in the pong-response."
+ (mu4e--server-call-mu `(queries :queries ,queries)))
+
+(defun mu4e--server-remove (docid-or-path)
+ "Remove message with either DOCID or PATH.
+The results are reported through either (:update ... )
+or (:error) sexps."
+ (if (stringp docid-or-path)
+ (mu4e--server-call-mu `(remove :path ,docid-or-path))
+ (mu4e--server-call-mu `(remove :docid ,docid-or-path))))
+
+(defun mu4e--server-view (docid-or-msgid &optional mark-as-read)
+ "View a message referred to by DOCID-OR-MSGID.
+Optionally, if MARK-AS-READ is non-nil, the backend marks the
+message as \"read\" before returning, if not already. The result
+will be delivered to the function registered as `mu4e-view-func'."
+ (mu4e--server-call-mu
+ `(view
+ :docid ,(if (stringp docid-or-msgid) nil docid-or-msgid)
+ :msgid ,(if (stringp docid-or-msgid) docid-or-msgid nil)
+ :mark-as-read ,(and mark-as-read t)
+ ;; when moving (due to mark-as-read), change filenames
+ ;; if so configured. Note: currently this *ignored*
+ ;; because mbsync seems to get confused.
+ :rename ,(and mu4e-change-filenames-when-moving t))))
+
+\f
+(provide 'mu4e-server)
+;;; mu4e-server.el ends here
--- /dev/null
+;;; mu4e-speedbar --- Speedbar support for mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2012-2021 Antono Vasiljev, Dirk-Jan C. Binnema
+
+;; Author: Antono Vasiljev <self@antono.info>
+;; Version: 0.1
+;; Keywords: file, tags, tools
+
+;; This file is not part of GNU Emacs.
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, 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/>.
+
+;;; Commentary:
+;;
+;; Speedbar provides a frame in which files, and locations in files
+;; are displayed. These functions provide mu4e specific support,
+;; showing maildir list in the side-bar.
+;;
+;; This file requires speedbar.
+
+;;; Code:
+
+(require 'speedbar)
+(require 'mu4e-vars)
+(require 'mu4e-headers)
+(require 'mu4e-context)
+(require 'mu4e-bookmarks)
+
+(defvar mu4e-main-speedbar-key-map nil
+ "Keymap used when in mu4e display mode.")
+(defvar mu4e-headers-speedbar-key-map nil
+ "Keymap used when in mu4e display mode.")
+(defvar mu4e-view-speedbar-key-map nil
+ "Keymap used when in mu4e display mode.")
+
+(defvar mu4e-main-speedbar-menu-items nil
+ "Additional menu-items to add to speedbar frame.")
+(defvar mu4e-headers-speedbar-menu-items nil
+ "Additional menu-items to add to speedbar frame.")
+(defvar mu4e-view-speedbar-menu-items nil
+ "Additional menu-items to add to speedbar frame.")
+
+
+(defun mu4e-speedbar-install-variables ()
+ "Install those variables used by speedbar to enhance mu4e."
+ (add-hook 'mu4e-context-changed-hook
+ #'mu4e~speedbar-context-changed-hook-fn)
+ (dolist (keymap
+ '( mu4e-main-speedbar-key-map
+ mu4e-headers-speedbar-key-map
+ mu4e-view-speedbar-key-map))
+ (unless keymap
+ (setq keymap (speedbar-make-specialized-keymap))
+ (define-key keymap "RET" 'speedbar-edit-line)
+ (define-key keymap "e" 'speedbar-edit-line))))
+
+(defun mu4e~speedbar-context-changed-hook-fn ()
+ (when (buffer-live-p speedbar-buffer)
+ (with-current-buffer speedbar-buffer
+ (let ((inhibit-read-only t))
+ (mu4e-speedbar-buttons)))))
+
+(with-eval-after-load 'speedbar
+ (mu4e-speedbar-install-variables))
+
+(defun mu4e~speedbar-render-maildir-list ()
+ "Insert the list of maildirs in the speedbar."
+ (interactive)
+ (when (buffer-live-p speedbar-buffer)
+ (with-current-buffer speedbar-buffer
+ (mapcar (lambda (maildir-name)
+ (speedbar-insert-button
+ (concat " " maildir-name)
+ 'mu4e-highlight-face
+ 'highlight
+ 'mu4e~speedbar-maildir
+ maildir-name))
+ (mu4e-get-maildirs)))))
+
+(defun mu4e~speedbar-maildir (&optional _text token _ident)
+ "Jump to maildir TOKEN. TEXT and INDENT are not used."
+ (dframe-with-attached-buffer
+ (mu4e-search (concat "\"maildir:" token "\"") current-prefix-arg)))
+
+(defun mu4e~speedbar-render-bookmark-list ()
+ "Insert the list of bookmarks in the speedbar"
+ (interactive)
+ (mapcar (lambda (bookmark)
+ (unless (plist-get bookmark :hide)
+ (speedbar-insert-button
+ (concat " " (plist-get bookmark :name))
+ 'mu4e-highlight-face
+ 'highlight
+ 'mu4e~speedbar-bookmark
+ (plist-get bookmark :query))))
+ (mu4e-bookmarks)))
+
+(defun mu4e~speedbar-bookmark (&optional _text token _ident)
+ "Run bookmarked query TOKEN. TEXT and INDENT are not used."
+ (dframe-with-attached-buffer
+ (mu4e-search token current-prefix-arg)))
+
+;;;###autoload
+(defun mu4e-speedbar-buttons (&optional _buffer)
+ "Create buttons for any mu4e BUFFER."
+ (interactive)
+ (erase-buffer)
+ (insert (propertize "* mu4e\n\n" 'face 'mu4e-title-face))
+
+ (insert (propertize " Bookmarks\n" 'face 'mu4e-title-face))
+ (mu4e~speedbar-render-bookmark-list)
+ (insert "\n")
+ (insert (propertize " Maildirs\n" 'face 'mu4e-title-face))
+ (mu4e~speedbar-render-maildir-list))
+
+(defun mu4e-main-speedbar-buttons (buffer) (mu4e-speedbar-buttons buffer))
+(defun mu4e-headers-speedbar-buttons (buffer) (mu4e-speedbar-buttons buffer))
+(defun mu4e-view-speedbar-buttons (buffer) (mu4e-speedbar-buttons buffer))
+
+;;; _
+(provide 'mu4e-speedbar)
+;;; mu4e-speedbar.el ends here
--- /dev/null
+;;; mu4e-thread.el --- Thread folding support -*- lexical-binding: t -*-
+
+;; Copyright (C) 2023 Nicolas P. Rougier
+
+;; Author: Nicolas P. Rougier <Nicolas.Rougier@inria.fr>
+;; Keywords: mail
+
+;; 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/>.
+
+;;; Commentary:
+
+;; mu4e-thread.el is a library that allows to fold and unfold threads in mu4e
+;; headers mode. Folding works by creating an overlay over thread children that
+;; display a summary (number of hidden messages and possibly number of unread
+;; messages).
+
+;; Folding is performed just-in-time such that it is quite fast to
+;; fold/unfold threads. When a thread has unread messages, the folding stops at
+;; the first unread message unless `mu4e-thread-fold-unread` has been set to t.
+
+;; Similarly, when a thread has marked messages, the folding stops at the first
+;; marked message.
+
+;; Note, you can only use these functions when threads are available, roughly
+;; when `mu4e-search-threads' in non-nil.
+
+;;; Usage example:
+;;
+;; After a search, mu4e-thread-mode will be enable when threads
+;; are available; so, to automatically sort them:
+;; (add-hook 'mu4e-thread-mode-hook #'mu4e-thread-fold-apply-all)
+
+;;; Code:
+
+(require 'mu4e-vars)
+(require 'mu4e-message)
+(require 'mu4e-mark)
+
+(defcustom mu4e-thread-fold-unread nil
+ "Whether to fold unread messages in a thread."
+ :type 'boolean
+ :group 'mu4e-headers)
+
+(defcustom mu4e-thread-fold-single-children nil
+ "When non-nil, fold a thread even if there is only a single child.
+Otherwise, do not not fold single children since would simply
+hide the single child."
+ :type 'boolean
+ :group 'mu4e-headers)
+
+(defface mu4e-thread-fold-face
+ `((t :inherit mu4e-highlight-face))
+ "Face for the information line of a folded thread."
+ :group 'mu4e-faces)
+
+(defvar-local mu4e-thread--fold-status nil
+ "Global folding status.")
+
+(defvar-local mu4e-thread--docids nil
+ "Thread list whose folding has been set individually.")
+
+(defvar mu4e-headers-fields) ;; defined in mu4e-headers.el
+(defun mu4e-thread-fold-info (count unread)
+ "Text to be displayed for a folded thread.
+There are COUNT hidden and UNREAD messages overall."
+ (let ((size (+ 2 (apply #'+ (mapcar (lambda (item) (or (cdr item) 0))
+ mu4e-headers-fields))))
+ (msg (concat (format"[%d hidden messages%s]\n" count
+ (if (> unread 0)
+ (format ", %d unread" unread)
+ "")))))
+ (propertize (concat " " (make-string size ?•) " " msg))))
+
+(defun mu4e-thread-message-folded-p ()
+ "Is point in a folded area?"
+ (when-let* ((overlay (mu4e-thread-is-folded))
+ (beg (overlay-start overlay))
+ (end (overlay-end overlay)))
+ (and (>= (point) beg) (< (point) end))))
+
+(declare-function 'mu4e~headers-thread-root-p "mu4e-headers")
+(defalias 'mu4e-thread-is-root 'mu4e~headers-thread-root-p)
+
+(defun mu4e-thread-goto-root ()
+ "Go to the root of the current thread."
+ (interactive)
+ (goto-char (mu4e-thread-root))
+ (beginning-of-line))
+
+(defun mu4e-thread-root ()
+ "Get the root of the current thread."
+ (interactive)
+ (let ((point))
+ (save-excursion
+ (while (and (not (bobp))
+ (not (mu4e-thread-is-root)))
+ (forward-line -1))
+ (setq point (point)))
+ point))
+
+(declare-function 'mu4e-headers-prev-thread "mu4e-headers")
+(declare-function 'mu4e-headers-next-thread "mu4e-headers")
+
+(defalias 'mu4e-thread-goto-prev 'mu4e-headers-prev-thread)
+(defalias 'mu4e-thread-goto-next 'mu4e-headers-next-thread)
+
+(defun mu4e-thread-prev ()
+ "Get the root of the previous thread (if any)."
+ (save-excursion
+ (when (mu4e-thread-goto-prev)
+ (mu4e-thread-root))))
+
+(defun mu4e-thread-next()
+ "Get the root of the next thread (if any)."
+ (save-excursion
+ (when (mu4e-thread-goto-next)
+ (mu4e-thread-root))))
+
+(defun mu4e-thread-is-folded ()
+ "Test if thread at point is folded."
+ (interactive)
+ (let* ((thread-beg (mu4e-thread-root))
+ (thread-end (or (mu4e-thread-next) (point-max)))
+ (overlays (overlays-in thread-beg thread-end)))
+ (catch 'folded
+ (dolist (overlay overlays)
+ (when (overlay-get overlay 'mu4e-thread-folded)
+ (throw 'folded overlay))))))
+
+(defun mu4e-thread-fold-toggle-all ()
+ "Toggle all threads folding unconditionally.
+Reset individual folding states."
+ (interactive)
+ (setq mu4e-thread--docids nil)
+ (if mu4e-thread--fold-status
+ (mu4e-thread-unfold-all)
+ (mu4e-thread-fold-all)))
+
+(defun mu4e-thread-fold-apply-all ()
+ "Apply global folding status to all threads not set individually."
+ (interactive)
+ ;; Global fold status
+ (if mu4e-thread--fold-status
+ (mu4e-thread-fold-all)
+ (mu4e-thread-unfold-all))
+ ;; Individual fold status
+ (save-excursion
+ (goto-char (point-min))
+ (catch 'end-search
+ (while (not (eobp))
+ (when-let* ((msg (get-text-property (point) 'msg))
+ (docid (mu4e-message-field msg :docid))
+ (state (cdr (assoc docid mu4e-thread--docids))))
+ (if (eq state 'folded)
+ (mu4e-thread-fold)
+ (mu4e-thread-unfold)))
+ (unless (mu4e-thread-next)
+ (throw 'end-search t))
+ (mu4e-thread-goto-next)))))
+
+(defun mu4e-thread-fold-all ()
+ "Fold all threads unconditionally."
+ (interactive)
+ (setq mu4e-thread--fold-status t)
+
+ (save-excursion
+ (goto-char (point-min))
+ (catch 'done
+ (while (not (eobp))
+ (mu4e-thread-fold t)
+ (unless (mu4e-thread-goto-next)
+ (throw 'done t))))))
+
+(defun mu4e-thread-unfold-all ()
+ "Unfold all threads unconditionally."
+ (interactive)
+ (setq mu4e-thread--fold-status nil)
+ (remove-overlays (point-min) (point-max) 'mu4e-thread-folded t))
+
+(defun mu4e-thread-fold-toggle ()
+ "Toggle folding for thread at point."
+ (interactive)
+ (if (mu4e-thread-is-folded)
+ (mu4e-thread-unfold)
+ (mu4e-thread-fold)))
+
+(defun mu4e-thread-fold-toggle-goto-next ()
+ "Toggle folding for thread at point and go to next thread."
+ (interactive)
+ (if (mu4e-thread-is-folded)
+ (mu4e-thread-unfold-goto-next)
+ (mu4e-thread-fold-goto-next)))
+
+(defun mu4e-thread-unfold (&optional no-save)
+ "Unfold thread at point and store state unless NO-SAVE is t."
+ (interactive)
+ (unless (eq (line-end-position) (point-max))
+ (when-let ((overlay (mu4e-thread-is-folded)))
+ (unless no-save
+ (mu4e-thread--save-state 'unfolded))
+ (delete-overlay overlay))))
+
+(defun mu4e-thread--save-state (state)
+ "Save the folding STATE of thread at point."
+ (save-excursion
+ (mu4e-thread-goto-root)
+ (when-let* ((msg (get-text-property (point) 'msg))
+ (docid (mu4e-message-field msg :docid)))
+ (setf (alist-get docid mu4e-thread--docids) state))))
+
+(defun mu4e-thread-fold (&optional no-save)
+ "Fold thread at point and store state unless NO-SAVE is t."
+ (interactive)
+ (unless (eq (line-end-position) (point-max))
+ (let* ((thread-beg (mu4e-thread-root))
+ (thread-end (mu4e-thread-next))
+ (thread-end (if thread-end (1- thread-end) (point-max)))
+ (unread-count 0)
+ (fold-beg (save-excursion
+ (goto-char thread-beg)
+ (forward-line)
+ (point)))
+ (fold-end (save-excursion
+ (goto-char thread-beg)
+ (forward-line)
+ (catch 'fold-end
+ (while (and (not (eobp))
+ (get-text-property (point) 'msg)
+ (and thread-end (< (point) thread-end)))
+ (let* ((msg (get-text-property (point) 'msg))
+ (docid (mu4e-message-field msg :docid))
+ (flags (mu4e-message-field msg :flags))
+ (unread (memq 'unread flags)))
+ (when (mu4e-mark-docid-marked-p docid)
+ (throw 'fold-end (point)))
+ (when unread
+ (unless mu4e-thread-fold-unread
+ (throw 'fold-end (point)))
+ (setq unread-count (+ 1 unread-count))))
+ (forward-line)))
+ (point))))
+ (unless no-save
+ (mu4e-thread--save-state 'folded))
+ (let ((child-count (count-lines fold-beg fold-end))
+ (unread-count (if mu4e-thread-fold-unread unread-count 0)))
+ (when (> child-count (if mu4e-thread-fold-single-children 0 1))
+ (let ((inhibit-read-only t)
+ (overlay (make-overlay fold-beg fold-end))
+ (info (mu4e-thread-fold-info child-count unread-count)))
+ (add-text-properties fold-beg (+ fold-beg 1)
+ '(face mu4e-thread-fold-face))
+ (overlay-put overlay 'mu4e-thread-folded t)
+ (overlay-put overlay 'display info)))))))
+
+(defun mu4e-thread-fold-goto-next ()
+ "Fold the thread at point and go to next thread."
+ (interactive)
+ (unless (eq (line-end-position) (point-max))
+ (mu4e-thread-fold)
+ (mu4e-thread-goto-next)))
+
+(defun mu4e-thread-unfold-goto-next ()
+ "Unfold the thread at point and go to next thread."
+ (interactive)
+ (unless (eq (line-end-position) (point-max))
+ (mu4e-thread-unfold)
+ (mu4e-thread-goto-next)))
+
+(define-minor-mode mu4e-thread-mode
+ "Mode for thread-support."
+ :global nil
+ :init-value nil ;; disabled by default
+ :group 'mu4e
+ :lighter ""
+ :keymap
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "<S-left>") #'mu4e-thread-goto-root)
+ (define-key map (kbd "<tab>") #'mu4e-thread-fold-toggle-goto-next)
+ (define-key map (kbd "<C-tab>") #'mu4e-thread-fold-toggle-goto-next)
+ (define-key map (kbd "<backtab>") #'mu4e-thread-fold-toggle-all)
+ map))
+
+(provide 'mu4e-thread)
+;;; mu4e-thread.el ends here
--- /dev/null
+;;; mu4e-update.el --- Update the mu4e message store -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2023 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Updating the mu4e message store: calling a mail retrieval program and
+;; re-running the index.
+
+;;; Code:
+
+(require 'mu4e-helpers)
+(require 'mu4e-server)
+\f
+;;; Customization
+
+(defcustom mu4e-get-mail-command "true"
+ "Shell command for retrieving new mail.
+Common values are \"offlineimap\", \"fetchmail\" or \"mbsync\", but
+arbitrary shell-commands can be used.
+
+When set to the literal string \"true\" (the default), the
+command simply finishes successfully (running the \"true\"
+command) without retrieving any mail. This can be useful when
+mail is already retrieved in another way, such as a local MDA."
+ :type 'string
+ :group 'mu4e
+ :safe 'stringp)
+
+(defcustom mu4e-index-update-error-warning t
+ "Whether to display warnings during the retrieval process.
+This depends on the `mu4e-get-mail-command' exit code."
+ :type 'boolean
+ :group 'mu4e
+ :safe 'booleanp)
+
+(defcustom mu4e-index-update-error-continue t
+ "Whether to continue with indexing after an error during retrieval."
+ :type 'boolean
+ :group 'mu4e
+ :safe 'booleanp)
+
+(defcustom mu4e-index-update-in-background t
+ "Whether to retrieve mail in the background."
+ :type 'boolean
+ :group 'mu4e
+ :safe 'booleanp)
+
+(defcustom mu4e-index-cleanup t
+ "Whether to run a cleanup phase after indexing.
+
+That is, validate that each message in the message store has a
+corresponding message file in the filesystem.
+
+Having this option as t ensures that no non-existing messages are
+shown but can slow with large message stores on slow file-systems."
+ :type 'boolean
+ :group 'mu4e
+ :safe 'booleanp)
+
+(defcustom mu4e-index-lazy-check nil
+ "Whether to only use a \"lazy\" check during reindexing.
+This influences how we decide whether a message
+needs (re)indexing or not.
+
+When this is set to non-nil, mu only uses the directory
+timestamps to decide whether it needs to check the messages
+beneath it. This makes indexing much faster, but might miss some
+changes. For this, you might want to occasionally call
+`mu4e-update-index-nonlazy'; `mu4e-update-pre-hook' can be used
+to automate this."
+ :type 'boolean
+ :group 'mu4e
+ :safe 'booleanp)
+
+(defcustom mu4e-update-interval nil
+ "Number of seconds between mail retrieval/indexing.
+If nil, don't update automatically. Note, changes in
+`mu4e-update-interval' only take effect after restarting mu4e.
+
+Important, the automatic update *only* works when `mu4e' is
+running."
+ :type '(choice (const :tag "No automatic update" nil)
+ (integer :tag "Seconds"))
+ :group 'mu4e
+ :safe 'integerp)
+
+(defvar mu4e-update-pre-hook nil
+ "Hook run just *before* the mail-retrieval / database updating process starts.
+You can use this hook for example to `mu4e-get-mail-command' with
+some specific setting.")
+
+(defcustom mu4e-hide-index-messages nil
+ "Whether to hide the \"Indexing...\" and contacts messages."
+ :type 'boolean
+ :group 'mu4e)
+
+(defvar mu4e-index-updated-hook nil
+ "Hook run when the indexing process has completed.
+The variable `mu4e-index-update-status' can be used to get
+information about what changed.")
+
+(defvar mu4e-message-changed-hook nil
+ "Hook run when there is a message changed in the data store.
+For new messages, it depends on `mu4e-index-updated-hook'. This
+can be used as a simple way to invoke some action when a message
+changed")
+
+(defvar mu4e-index-update-status nil
+ "Last-seen completed update status, based on server status messages.
+
+If non-nil, this is a plist of the form:
+\(
+:checked <number of messages processed> (checked whether up-to-date)
+:updated <number of messages updated/added
+:cleaned-up <number of stale messages removed from store
+:stamp <emacs (current-time) timestamp for the status)")
+
+(defconst mu4e-last-update-buffer "*mu4e-last-update*"
+ "Name of buffer with cloned from the last update buffer.
+Useful for diagnosing update problems.")
+
+\f
+;;; Internal variables / const
+(defconst mu4e--update-name " *mu4e-update*"
+ "Name of the process and buffer to update mail.")
+(defvar mu4e--progress-reporter nil
+ "Internal, the progress reporter object.")
+(defvar mu4e--update-timer nil
+ "The mu4e update timer.")
+(defconst mu4e--update-buffer-height 8
+ "Height of the mu4e message retrieval/update buffer.")
+(defvar mu4e--get-mail-ask-password "mu4e get-mail: Enter password: "
+ "Query string for `mu4e-get-mail-command' password.")
+(defvar mu4e--get-mail-password-regexp "^Remote: Enter password: $"
+ "Regexp for a `mu4e-get-mail-command' password query.")
+\f
+
+(defun mu4e--get-mail-process-filter (proc msg)
+ "Filter the MSG output of the `mu4e-get-mail-command' PROC.
+
+Currently the filter only checks if the command asks for a
+password by matching the output against
+`mu4e~get-mail-password-regexp'. The messages are inserted into
+the process buffer.
+
+Also scrolls to the final line, and update the progress
+throbber."
+ (when mu4e--progress-reporter
+ (progress-reporter-update mu4e--progress-reporter))
+
+ (when (string-match mu4e--get-mail-password-regexp msg)
+ (if (process-get proc 'x-interactive)
+ (process-send-string proc
+ (concat (read-passwd mu4e--get-mail-ask-password)
+ "\n"))
+ ;; TODO kill process?
+ (mu4e-error "Unrecognized password request")))
+ (when (process-buffer proc)
+ (let ((inhibit-read-only t)
+ (procwin (get-buffer-window (process-buffer proc))))
+ ;; Insert at end of buffer. Leave point alone.
+ (with-current-buffer (process-buffer proc)
+ (goto-char (point-max))
+ (if (string-match ".*\r\\(.*\\)" msg)
+ (progn
+ ;; kill even with \r
+ (end-of-line)
+ (let ((end (point)))
+ (beginning-of-line)
+ (delete-region (point) end))
+ (insert (match-string 1 msg)))
+ (insert msg)))
+ ;; Auto-scroll unless user is interacting with the window.
+ (when (and (window-live-p procwin)
+ (not (eq (selected-window) procwin)))
+ (with-selected-window procwin
+ (goto-char (point-max)))))))
+
+(defun mu4e-index-message (frm &rest args)
+ "Display FRM with ARGS like `mu4e-message' for index messages.
+However, if `mu4e-hide-index-messages' is non-nil, do not display anything."
+ (unless mu4e-hide-index-messages
+ (apply 'mu4e-message frm args)))
+
+(defun mu4e-update-index ()
+ "Update the mu4e index."
+ (interactive)
+ (mu4e--server-index mu4e-index-cleanup mu4e-index-lazy-check))
+
+(defun mu4e-update-index-nonlazy ()
+ "Update the mu4e index non-lazily.
+This is just a convenience wrapper for indexing the non-lazy way
+if you otherwise want to use `mu4e-index-lazy-check'."
+ (interactive)
+ (let ((mu4e-index-cleanup t) (mu4e-index-lazy-check nil))
+ (mu4e-update-index)))
+
+(defvar mu4e--update-buffer nil
+ "The buffer of the update process when updating.")
+
+(define-derived-mode mu4e--update-mail-mode special-mode "mu4e:update"
+ "Major mode used for retrieving new e-mail messages in `mu4e'.")
+
+(define-key mu4e--update-mail-mode-map (kbd "q") 'mu4e-kill-update-mail)
+
+(defun mu4e--temp-window (buf height)
+ "Create a temporary window with HEIGHT at the bottom BUF.
+
+This function uses `display-buffer' with a default preset.
+
+To override this behavior, customize `display-buffer-alist'."
+ (display-buffer buf `(display-buffer-at-bottom
+ (preserve-size . (nil . t))
+ (height . ,height)
+ (inhibit-same-window . t)
+ (window-height . fit-window-to-buffer)))
+ (set-window-buffer (get-buffer-window buf) buf))
+
+(defun mu4e--update-sentinel-func (proc _msg)
+ "Sentinel function for the update process PROC."
+ (when mu4e--progress-reporter
+ (progress-reporter-done mu4e--progress-reporter)
+ (setq mu4e--progress-reporter nil))
+ (unless mu4e-hide-index-messages
+ (message nil))
+ (if (or (not (eq (process-status proc) 'exit))
+ (/= (process-exit-status proc) 0))
+ (progn
+ (when mu4e-index-update-error-warning
+ (mu4e-message "Update process returned with non-zero exit code")
+ (sit-for 5))
+ (when mu4e-index-update-error-continue
+ (mu4e-update-index)))
+ (mu4e-update-index))
+ (when (buffer-live-p mu4e--update-buffer)
+ (delete-windows-on mu4e--update-buffer)
+ ;; clone the update buffer for diagnosis
+ (when (get-buffer mu4e-last-update-buffer)
+ (kill-buffer mu4e-last-update-buffer))
+ (with-current-buffer mu4e--update-buffer
+ (special-mode)
+ (clone-buffer mu4e-last-update-buffer))
+ ;; and kill the buffer itself; the cloning is needed
+ ;; so the temp window handling works as expected.
+ (kill-buffer mu4e--update-buffer)))
+
+;; complicated function, as it:
+;; - needs to check for errors
+;; - (optionally) pop-up a window
+;; - (optionally) check password requests
+(defun mu4e--update-mail-and-index-real (run-in-background)
+ "Get a new mail by running `mu4e-get-mail-command'.
+If
+RUN-IN-BACKGROUND is non-nil (or called with prefix-argument),
+run in the background; otherwise, pop up a window."
+ (let* ((process-connection-type t)
+ (proc (start-process-shell-command
+ mu4e--update-name mu4e--update-name
+ mu4e-get-mail-command))
+ (buf (process-buffer proc))
+ (win (or run-in-background
+ (mu4e--temp-window buf mu4e--update-buffer-height))))
+ (set-process-query-on-exit-flag proc nil)
+ (setq mu4e--update-buffer buf)
+ (when (window-live-p win)
+ (with-selected-window win
+ (erase-buffer)
+ (insert "\n") ;; FIXME -- needed so output starts
+ (mu4e--update-mail-mode)))
+ (setq mu4e--progress-reporter
+ (unless mu4e-hide-index-messages
+ (make-progress-reporter
+ (mu4e-format "Retrieving mail..."))))
+ (set-process-sentinel proc 'mu4e--update-sentinel-func)
+ ;; if we're running in the foreground, handle password requests
+ (unless run-in-background
+ (process-put proc 'x-interactive (not run-in-background))
+ (set-process-filter proc 'mu4e--get-mail-process-filter))))
+
+(defun mu4e-update-mail-and-index (run-in-background)
+ "Retrieve new mail by running `mu4e-get-mail-command'.
+If RUN-IN-BACKGROUND is non-nil (or called with prefix-argument),
+run in the background; otherwise, pop up a window."
+ (interactive "P")
+ (unless mu4e-get-mail-command
+ (mu4e-error "`mu4e-get-mail-command' is not defined"))
+ (if (and (buffer-live-p mu4e--update-buffer)
+ (process-live-p (get-buffer-process mu4e--update-buffer)))
+ (mu4e-message "Update process is already running")
+ (progn
+ (run-hooks 'mu4e-update-pre-hook)
+ (mu4e--update-mail-and-index-real run-in-background))))
+
+(defun mu4e-kill-update-mail ()
+ "Stop the update process by killing it."
+ (interactive)
+ (let* ((proc (and (buffer-live-p mu4e--update-buffer)
+ (get-buffer-process mu4e--update-buffer))))
+ (when (process-live-p proc)
+ (kill-process proc t))))
+
+(define-minor-mode mu4e-update-minor-mode
+ "Mode for triggering mu4e updates."
+ :global nil
+ :init-value nil ;; disabled by default
+ :group 'mu4e
+ :lighter ""
+ :keymap
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "C-S-u") #'mu4e-update-mail-and-index)
+ ;; for terminal users
+ (define-key map (kbd "C-c C-u") #'mu4e-update-mail-and-index)
+ map))
+
+(provide 'mu4e-update)
+;;; mu4e-update.el ends here
--- /dev/null
+;;; mu4e-vars.el --- Variables and faces for mu4e -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2023 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'message)
+(require 'mu4e-helpers)
+\f
+;;; Configuration
+(defgroup mu4e nil
+ "Mu4e - an email-client for Emacs."
+ :group 'mail)
+
+(defcustom mu4e-confirm-quit t
+ "Whether to confirm to quit mu4e."
+ :type 'boolean
+ :group 'mu4e)
+
+(defcustom mu4e-modeline-support t
+ "Support for showing information in the modeline."
+ :type 'boolean
+ :group 'mu4e)
+
+(defcustom mu4e-notification-support nil
+ "Support for new-message notifications."
+ :type 'boolean
+ :group 'mu4e)
+
+(defcustom mu4e-org-support t
+ "Support Org-mode links."
+ :type 'boolean
+ :group 'mu4e)
+
+(defcustom mu4e-speedbar-support nil
+ "Support having a speedbar to navigate folders/bookmarks."
+ :type 'boolean
+ :group 'mu4e)
+
+(defcustom mu4e-eldoc-support nil
+ "Support eldoc help in the headers-view."
+ :type 'boolean
+ :group 'mu4e)
+
+(defcustom mu4e-date-format-long "%c"
+ "Date format to use in the message view.
+Follows the format of `format-time-string'."
+ :type 'string
+ :group 'mu4e)
+
+(defcustom mu4e-dim-when-loading t
+ "Dim buffer text when loading new data.
+If non-nil, dim some buffers during data retrieval and rendering,
+and show some \"Loading\" banner."
+ :type 'boolean
+ :group 'mu4e)
+
+\f
+;;; Faces
+
+(defgroup mu4e-faces nil
+ "Type faces (fonts) used in mu4e."
+ :group 'mu4e
+ :group 'faces)
+
+(defface mu4e-unread-face
+ '((t :inherit font-lock-keyword-face :weight bold))
+ "Face for an unread message header."
+ :group 'mu4e-faces)
+
+(defface mu4e-trashed-face
+ '((t :inherit font-lock-comment-face :strike-through t))
+ "Face for an message header in the trash folder."
+ :group 'mu4e-faces)
+
+(defface mu4e-draft-face
+ '((t :inherit font-lock-string-face))
+ "Face for a draft message header.
+I.e. a message with the draft flag set."
+ :group 'mu4e-faces)
+
+(defface mu4e-flagged-face
+ '((t :inherit font-lock-constant-face :weight bold))
+ "Face for a flagged message header."
+ :group 'mu4e-faces)
+
+(defface mu4e-replied-face
+ '((t :inherit font-lock-builtin-face :weight normal :slant normal))
+ "Face for a replied message header."
+ :group 'mu4e-faces)
+
+(defface mu4e-forwarded-face
+ '((t :inherit font-lock-builtin-face :weight normal :slant normal))
+ "Face for a passed (forwarded) message header."
+ :group 'mu4e-faces)
+
+(defface mu4e-header-face
+ '((t :inherit default))
+ "Face for a header without any special flags."
+ :group 'mu4e-faces)
+
+(defface mu4e-related-face
+ '((t :inherit default :slant italic))
+ "Face for a \='related' header." :group 'mu4e-faces)
+
+(defface mu4e-header-title-face
+ '((t :inherit font-lock-type-face))
+ "Face for a header title in the headers view."
+ :group 'mu4e-faces)
+
+(defface mu4e-header-highlight-face
+ `((t :inherit hl-line :weight bold :underline t
+ ,@(and (>= emacs-major-version 27) '(:extend t))))
+ "Face for the header at point."
+ :group 'mu4e-faces)
+
+(defface mu4e-header-marks-face
+ '((t :inherit font-lock-preprocessor-face))
+ "Face for the mark in the headers list."
+ :group 'mu4e-faces)
+
+(defface mu4e-header-key-face
+ '((t :inherit message-header-name :weight bold))
+ "Face used to highlight items in various places."
+ :group 'mu4e-faces)
+
+(defface mu4e-header-field-face
+ '((t :weight bold))
+ "Face for a header field name (such as \"Subject:\" in \"Subject:\
+Foo\")."
+ :group 'mu4e-faces)
+
+(defface mu4e-header-value-face
+ '((t :inherit font-lock-type-face))
+ "Face for a header value (such as \"Re: Hello!\")."
+ :group 'mu4e-faces)
+
+(defface mu4e-special-header-value-face
+ '((t :inherit font-lock-builtin-face))
+ "Face for special header values."
+ :group 'mu4e-faces)
+
+(defface mu4e-link-face
+ '((t :inherit link))
+ "Face for showing URLs and attachments in the message view."
+ :group 'mu4e-faces)
+
+(defface mu4e-contact-face
+ '((t :inherit font-lock-variable-name-face))
+ "Face for showing URLs and attachments in the message view."
+ :group 'mu4e-faces)
+
+(defface mu4e-highlight-face
+ '((t :inherit highlight))
+ "Face for highlighting things."
+ :group 'mu4e-faces)
+
+(defface mu4e-title-face
+ '((t :inherit font-lock-type-face :weight bold))
+ "Face for a header title in the headers view."
+ :group 'mu4e-faces)
+
+(defface mu4e-modeline-face
+ '((t :inherit font-lock-string-face :weight bold))
+ "Face for the query in the mode-line."
+ :group 'mu4e-faces)
+
+(defface mu4e-footer-face
+ '((t :inherit font-lock-comment-face))
+ "Face for message footers (signatures)."
+ :group 'mu4e-faces)
+
+(defface mu4e-url-number-face
+ '((t :inherit font-lock-constant-face :weight bold))
+ "Face for the number tags for URLs."
+ :group 'mu4e-faces)
+
+(defface mu4e-system-face
+ '((t :inherit font-lock-comment-face :slant italic))
+ "Face for system message (such as the footers for message headers)."
+ :group 'mu4e-faces)
+
+(defface mu4e-ok-face
+ '((t :inherit font-lock-comment-face :weight bold :slant normal))
+ "Face for things that are okay."
+ :group 'mu4e-faces)
+
+(defface mu4e-warning-face
+ '((t :inherit font-lock-warning-face :weight bold :slant normal))
+ "Face for warnings / error."
+ :group 'mu4e-faces)
+
+(defface mu4e-compose-separator-face
+ '((t :inherit message-separator :slant italic))
+ "Face for the headers/message separator in mu4e-compose-mode."
+ :group 'mu4e-faces)
+
+(defface mu4e-region-code
+ '((t (:background "DarkSlateGray")))
+ "Face for highlighting marked region in mu4e-view buffer."
+ :group 'mu4e-faces)
+
+;;; Header information
+
+(defconst mu4e-header-info
+ '((:bcc
+ . (:name "Bcc"
+ :shortname "Bcc"
+ :help "Blind Carbon-Copy recipients for the message"
+ :sortable t))
+ (:cc
+ . (:name "Cc"
+ :shortname "Cc"
+ :help "Carbon-Copy recipients for the message"
+ :sortable t))
+ (:changed
+ . (:name "Changed"
+ :shortname "Chg"
+ :help "Date/time when the message was changed most recently"
+ :sortable t))
+ (:date
+ . (:name "Date"
+ :shortname "Date"
+ :help "Date/time when the message was sent"
+ :sortable t))
+ (:human-date
+ . (:name "Date"
+ :shortname "Date"
+ :help "Date/time when the message was sent"
+ :sortable :date))
+ (:flags
+ . (:name "Flags"
+ :shortname "Flgs"
+ :help "Flags for the message"
+ :sortable nil))
+ (:from
+ . (:name "From"
+ :shortname "From"
+ :help "The sender of the message"
+ :sortable t))
+ (:from-or-to
+ . (:name "From/To"
+ :shortname "From/To"
+ :help "Sender of the message if it's not me; otherwise the recipient"
+ :sortable nil))
+ (:maildir
+ . (:name "Maildir"
+ :shortname "Maildir"
+ :help "Maildir for this message"
+ :sortable t))
+ (:list
+ . (:name "List-Id"
+ :shortname "List"
+ :help "Mailing list id for this message"
+ :sortable t))
+ (:mailing-list
+ . (:name "List"
+ :shortname "List"
+ :help "Mailing list friendly name for this message"
+ :sortable :list))
+ (:message-id
+ . (:name "Message-Id"
+ :shortname "MsgID"
+ :help "Message-Id for this message"
+ :sortable nil))
+ (:path
+ . (:name "Path"
+ :shortname "Path"
+ :help "Full filesystem path to the message"
+ :sortable t))
+ (:size
+ . (:name "Size"
+ :shortname "Size"
+ :help "Size of the message"
+ :sortable t))
+ (:subject
+ . (:name "Subject"
+ :shortname "Subject"
+ :help "Subject of the message"
+ :sortable t))
+ (:tags
+ . (:name "Tags"
+ :shortname "Tags"
+ :help "Tags for the message"
+ ;; sort by _first_ tag.
+ :sortable t))
+ (:thread-subject
+ . (:name "Subject"
+ :shortname "Subject"
+ :help "Subject of the thread"
+ :sortable :subject))
+ (:to
+ . (:name "To"
+ :shortname "To"
+ :help "Recipient of the message"
+ :sortable t)))
+
+ "An alist of all possible header fields and information about them.
+
+This is used in the user-interface (the column headers in the
+header list, and the fields the message view).
+
+Most fields should be self-explanatory. A special one is
+`:from-or-to', which is equal to `:from' unless `:from' matches
+one of the addresses in `(mu4e-personal-addresses)', in which
+case it will be equal to `:to'.
+
+Furthermore, the property `:sortable' determines whether we can
+sort by this field. This can be either a boolean (nil or t), or a
+symbol for /another/ field. For example, the `:human-date' field
+uses `:date' for that.
+
+Note, `:sortable' is not supported for custom header fields.")
+
+(defvar mu4e-header-info-custom
+ '(
+ ;; some examples & debug helpers.
+
+ (:thread-path
+ . ;; Shows the internal thread-path
+ ( :name "Thread-path"
+ :shortname "Thp"
+ :help "The thread-path"
+ :function (lambda (msg)
+ (let ((thread (mu4e-message-field msg :thread)))
+ (or (and thread (plist-get thread :path)) "")))))
+
+ (:thread-date
+ . ;; Shows the internal thread-date
+ ( :name "Thread-date"
+ :shortname "Thd"
+ :help "The thread-date"
+ :function (lambda (msg)
+ (let* ((thread (mu4e-message-field msg :thread))
+ (tdate (and thread (plist-get thread :date-tstamp))))
+ (format-time-string "%F %T " (or tdate 0))))))
+ (:recipnum
+ .
+ ( :name "Number of recipients"
+ :shortname "Recip#"
+ :help "Number of recipients for this message"
+ :function
+ (lambda (msg)
+ (format "%d"
+ (+ (length (mu4e-message-field msg :to))
+ (length (mu4e-message-field msg :cc))))))))
+
+ "An alist of custom (user-defined) headers.
+The format is similar to `mu4e-header-info', but adds a :function
+property, which should point to a function that takes a message
+plist as argument, and returns a string. See the default value of
+`mu4e-header-info-custom for an example.
+
+Note that when using the gnus-based view, you only have access to
+a limited set of message fields: only the ones used in the
+header-view, not including, for instance, the message body.")
+
+;;; Internals
+
+(defvar-local mu4e~headers-view-win nil
+ "The view window connected to this headers view.")
+
+;; It's useful to have the current view message available to
+;; `mu4e-view-mode-hooks' functions, and we set up this variable
+;; before calling `mu4e-view-mode'. However, changing the major mode
+;; clobbers any local variables. Work around that by declaring the
+;; variable permanent-local.
+(defvar-local mu4e--view-message nil "The message being viewed in view mode.")
+(put 'mu4e--view-message 'permanent-local t)
+;;; _
+(provide 'mu4e-vars)
+;;; mu4e-vars.el ends here
--- /dev/null
+;;; mu4e-view.el --- Mode for viewing e-mail messages -*- lexical-binding: t -*-
+
+;; Copyright (C) 2021-2024 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; In this file we define mu4e-view-mode (+ helper functions), which is used for
+;; viewing e-mail messages
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'calendar)
+(require 'gnus-art)
+(require 'comint)
+(require 'browse-url)
+(require 'button)
+(require 'epa)
+(require 'epg)
+(require 'thingatpt)
+
+(require 'mu4e-actions)
+(require 'mu4e-compose)
+(require 'mu4e-context)
+(require 'mu4e-headers)
+(require 'mu4e-mark)
+(require 'mu4e-message)
+(require 'mu4e-server)
+(require 'mu4e-search)
+(require 'mu4e-mime-parts)
+
+;; utility functions
+(require 'mu4e-contacts)
+(require 'mu4e-vars)
+
+;;; Options
+
+(defcustom mu4e-view-scroll-to-next t
+ "Move to the next message with `mu4e-view-scroll-up-or-next'.
+When at the end of a message, move to the next one, if any.
+Otherwise, don't move to the next message."
+ :type 'boolean
+ :group 'mu4e-view)
+
+(defcustom mu4e-view-fields
+ '(:from :to :cc :subject :flags :date :maildir :mailing-list :tags)
+ "Header fields to display in the message view buffer.
+
+For the complete list of available headers, see
+`mu4e-header-info'.
+
+Note, you can use this to add fields that are not otherwise
+shown; you can further tweak the other fields using e.g.,
+`gnus-visible-headers' and `gnus-ignored-headers' - see the gnus
+documentation for details."
+ :type '(repeat symbol)
+ :group 'mu4e-view)
+
+(defcustom mu4e-view-actions
+ (delq nil `(("capture message" . mu4e-action-capture-message)
+ ("view in browser" . mu4e-action-view-in-browser)
+ ("browse online archive" . mu4e-action-browse-list-archive)
+ ,(when (fboundp 'xwidget-webkit-browse-url)
+ '("xview in xwidget" . mu4e-action-view-in-xwidget))
+ ("show this thread" . mu4e-action-show-thread)))
+ "List of actions to perform on messages in view mode.
+The actions are cons-cells of the form:
+ (NAME . FUNC)
+where:
+* NAME is the name of the action (e.g. \"Count lines\")
+* FUNC is a function which receives a message plist as an argument.
+
+The first letter of NAME is used as a shortcut character."
+ :group 'mu4e-view
+ :type '(alist :key-type string :value-type function))
+
+(defcustom mu4e-view-max-specpdl-size 4096
+ "The value of `max-specpdl-size' for displaying messages with Gnus."
+ :type 'integer
+ :group 'mu4e-view)
+
+\f
+
+(defconst mu4e--view-raw-buffer-name " *mu4e-raw-view*"
+ "Name for the raw message view buffer.")
+
+(defun mu4e-view-raw-message ()
+ "Display the raw contents of message at point in a new buffer."
+ (interactive)
+ (let ((path (mu4e-message-readable-path))
+ (buf (get-buffer-create mu4e--view-raw-buffer-name)))
+ (with-current-buffer buf
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (mu4e-raw-view-mode)
+ (insert-file-contents path)
+ (goto-char (point-min))))
+ (mu4e-display-buffer buf t)))
+
+(defun mu4e-view-pipe (cmd)
+ "Pipe the message at point through shell command CMD.
+Then, display the results."
+ (interactive "sShell command: ")
+ (let ((path (mu4e-message-readable-path)))
+ (mu4e-process-file-through-pipe path cmd)))
+
+(defmacro mu4e--view-in-headers-context (&rest body)
+ "Evaluate BODY in the context of the headers buffer."
+ `(progn
+ (let* ((msg (mu4e-message-at-point))
+ (buffer (cond
+ ;; are we already inside a headers buffer?
+ ((mu4e-current-buffer-type-p 'headers) (current-buffer))
+ ;; if not, are we inside a view buffer, and does
+ ;; it have linked headers buffer?
+ ((mu4e-current-buffer-type-p 'view)
+ (when (mu4e--view-detached-p (current-buffer))
+ (mu4e-error
+ "Cannot navigate in a detached view buffer."))
+ (mu4e-get-headers-buffer))
+ ;; fallback; but what would trigger this?
+ (t (mu4e-get-headers-buffer))))
+ (docid (mu4e-message-field msg :docid)))
+ (unless docid
+ (mu4e-error "Message without docid: action is not possible"))
+
+ ;; make sure to select the window if possible, or jumping won't be
+ ;; reflected.
+ (with-selected-window (or (get-buffer-window buffer)
+ (get-buffer-window))
+ (with-current-buffer buffer
+ (mu4e-thread-unfold-all)
+ (if (or (mu4e~headers-goto-docid docid)
+ ;; TODO: Is this the best way to find another
+ ;; relevant docid for a view buffer?
+ ;;
+ ;; If you attach a view buffer to another headers
+ ;; buffer that does not contain the current docid
+ ;; then `mu4e~headers-goto-docid' returns nil and we
+ ;; get an error. This "hack" instead gets its
+ ;; now-changed headers buffer's current message as a
+ ;; docid
+ (mu4e~headers-goto-docid
+ (with-current-buffer buffer
+ (mu4e-message-field (mu4e-message-at-point) :docid))))
+ ,@body
+ (mu4e-error "Cannot find message in headers buffer")))))))
+
+(defun mu4e-view-headers-next (&optional n)
+ "Move point to the next message header.
+If this succeeds, return the new docid. Otherwise, return nil.
+Optionally, takes an integer N (prefix argument), to the Nth next
+header."
+ (interactive "P")
+ (mu4e--view-in-headers-context
+ (mu4e~headers-move (or n 1))))
+
+(defun mu4e-view-headers-prev (&optional n)
+ "Move point to the previous message header.
+If this succeeds, return the new docid. Otherwise, return nil.
+Optionally, takes an integer N (prefix argument), to the Nth
+previous header."
+ (interactive "P")
+ (mu4e--view-in-headers-context
+ (mu4e~headers-move (- (or n 1)))))
+
+(defun mu4e--view-prev-or-next (func backwards)
+ "Move point to the next or previous message.
+Go to the previous message if BACKWARDS is non-nil.
+unread message header in the headers buffer connected with this
+message view. If this succeeds, return the new docid. Otherwise,
+return nil."
+ (mu4e--view-in-headers-context (funcall func backwards))
+ (mu4e-select-other-view)
+ (mu4e-headers-view-message))
+
+(defun mu4e-view-headers-prev-unread ()
+ "Move point to the previous unread message header.
+If this succeeds, return the new docid. Otherwise, return nil."
+ (interactive)
+ (mu4e--view-prev-or-next #'mu4e~headers-prev-or-next-unread t))
+
+(defun mu4e-view-headers-next-unread ()
+ "Move point to the next unread message header.
+If this succeeds, return the new docid. Otherwise, return nil."
+ (interactive)
+ (mu4e--view-prev-or-next #'mu4e~headers-prev-or-next-unread nil))
+
+(defun mu4e-view-headers-prev-thread()
+ "Move point to the previous thread.
+If this succeeds, return the new docid. Otherwise, return nil."
+ (interactive)
+ (mu4e--view-prev-or-next #'mu4e~headers-prev-or-next-thread t))
+
+(defun mu4e-view-headers-next-thread()
+ "Move point to the previous thread.
+If this succeeds, return the new docid. Otherwise, return nil."
+ (interactive)
+ (mu4e--view-prev-or-next #'mu4e~headers-prev-or-next-thread nil))
+
+(defun mu4e-view-thread-goto-root ()
+ "Move to thread root."
+ (interactive)
+ (mu4e--view-in-headers-context (mu4e-thread-goto-root)))
+
+(defun mu4e-view-thread-fold-toggle-goto-next ()
+ "Toggle threading or go to next."
+ (interactive)
+ (mu4e--view-in-headers-context (mu4e-thread-fold-toggle-goto-next)))
+
+(defun mu4e-view-thread-fold-toggle-all ()
+ "Toggle all threads."
+ (interactive)
+ (mu4e--view-in-headers-context (mu4e-thread-fold-toggle-all)))
+
+\f
+;;; Interactive functions
+(defun mu4e-view-action (&optional msg)
+ "Ask user for some action to apply on MSG, then do it.
+If MSG is nil apply action to message returned
+bymessage-at-point. The actions are specified in
+`mu4e-view-actions'."
+ (interactive)
+ (let* ((msg (or msg (mu4e-message-at-point)))
+ (actionfunc (mu4e-read-option "Action: " mu4e-view-actions)))
+ (funcall actionfunc msg)))
+
+(defun mu4e-view-mark-pattern ()
+ "Mark messages that match a certain pattern.
+Ask user for a kind of mark, (move, delete etc.), a field to
+match and a regular expression to match with. Then, mark all
+matching messages with that mark."
+ (interactive)
+ (mu4e--view-in-headers-context (mu4e-headers-mark-pattern)))
+
+(defun mu4e-view-mark-thread (&optional markpair)
+ "Mark whole thread with a certain mark.
+Ask user for a kind of mark (move, delete etc.), and apply it
+to all messages in the thread at point in the headers view. The
+optional MARKPAIR can also be used to provide the mark
+selection."
+ (interactive)
+ (mu4e--view-in-headers-context
+ (if markpair (mu4e-headers-mark-thread nil markpair)
+ (call-interactively 'mu4e-headers-mark-thread))))
+
+(defun mu4e-view-mark-subthread (&optional markpair)
+ "Mark subthread with a certain mark.
+Ask user for a kind of mark (move, delete etc.), and apply it
+to all messages in the subthread at point in the headers view.
+The optional MARKPAIR can also be used to provide the mark
+selection."
+ (interactive)
+ (mu4e--view-in-headers-context
+ (if markpair (mu4e-headers-mark-subthread markpair)
+ (mu4e-headers-mark-subthread))))
+
+(defun mu4e-view-search-narrow ()
+ "Run `mu4e-headers-search-narrow' in the headers buffer."
+ (interactive)
+ (mu4e--view-in-headers-context (mu4e-search-narrow)))
+
+(defun mu4e-view-search-edit ()
+ "Run `mu4e-search-edit' in the headers buffer."
+ (interactive)
+ (mu4e--view-in-headers-context (mu4e-search-edit)))
+
+(defun mu4e-mark-region-code ()
+ "Highlight region marked with `message-mark-inserted-region'.
+Add this function to `mu4e-view-mode-hook' to enable this feature."
+ (require 'message)
+ (let (beg end ov-beg ov-end ov-inv)
+ (save-excursion
+ (goto-char (point-min))
+ (while (re-search-forward
+ (concat "^" message-mark-insert-begin) nil t)
+ (setq ov-beg (match-beginning 0)
+ ov-end (match-end 0)
+ ov-inv (make-overlay ov-beg ov-end)
+ beg ov-end)
+ (overlay-put ov-inv 'invisible t)
+ (overlay-put ov-inv 'mu4e-overlay t)
+ (when (re-search-forward
+ (concat "^" message-mark-insert-end) nil t)
+ (setq ov-beg (match-beginning 0)
+ ov-end (match-end 0)
+ ov-inv (make-overlay ov-beg ov-end)
+ end ov-beg)
+ (overlay-put ov-inv 'invisible t))
+ (when (and beg end)
+ (let ((ov (make-overlay beg end)))
+ (overlay-put ov 'mu4e-overlay t)
+ (overlay-put ov 'face 'mu4e-region-code))
+ (setq beg nil end nil))))))
+
+;;; View Utilities
+
+(defun mu4e-view-mark-custom ()
+ "Run some custom mark function."
+ (mu4e--view-in-headers-context
+ (mu4e-headers-mark-custom)))
+
+(defun mu4e--view-split-view-p ()
+ "Return t if we're in split-view, nil otherwise."
+ (member mu4e-split-view '(horizontal vertical)))
+
+
+(defun mu4e-view-detach ()
+ "Detach the view buffer from its headers buffer."
+ (interactive)
+ (unless mu4e-linked-headers-buffer
+ (mu4e-error "This view buffer is already detached."))
+ (mu4e-message "Detached view buffer from %s"
+ (progn mu4e-linked-headers-buffer
+ (with-current-buffer mu4e-linked-headers-buffer
+ (when (eq (selected-window) mu4e~headers-view-win)
+ (setq mu4e~headers-view-win nil)))
+ (setq mu4e-linked-headers-buffer nil)
+ ;; automatically rename mu4e-view-article buffer when
+ ;; detaching; will get renamed back when reattaching
+ (rename-buffer (make-temp-name (buffer-name)) t))))
+
+(defun mu4e-view-attach (headers-buffer)
+ "Attaches a view buffer to a headers buffer."
+ (interactive
+ (list (get-buffer (read-buffer
+ "Select a headers buffer to attach to: " nil t
+ (lambda (buf) (with-current-buffer (car buf)
+ (mu4e-current-buffer-type-p 'headers)))))))
+ (mu4e-message "Attached view buffer to %s" headers-buffer)
+ (setq mu4e-linked-headers-buffer headers-buffer)
+ (with-current-buffer headers-buffer
+ (setq mu4e~headers-view-win (selected-window))))
+
+;;; Scroll commands
+
+(defun mu4e-view-scroll-up-or-next ()
+ "Scroll-up the current message.
+If `mu4e-view-scroll-to-next' is non-nil, and we cannot scroll up
+any further, go the next message."
+ (interactive)
+ (condition-case nil
+ (scroll-up)
+ (error
+ (when mu4e-view-scroll-to-next
+ (mu4e-view-headers-next)))))
+
+(defun mu4e-scroll-up ()
+ "Scroll text of selected window up one line."
+ (interactive)
+ (scroll-up 1))
+
+(defun mu4e-scroll-down ()
+ "Scroll text of selected window down one line."
+ (interactive)
+ (scroll-down 1))
+
+;;; Mark commands
+
+(defun mu4e-view-unmark-all ()
+ "If we're in split-view, unmark all messages.
+Otherwise, warn user that unmarking only works in the header
+list."
+ (interactive)
+ (if (mu4e--view-split-view-p)
+ (mu4e--view-in-headers-context (mu4e-mark-unmark-all))
+ (mu4e-message "Unmarking needs to be done in the header list view")))
+
+(defun mu4e-view-unmark ()
+ "If we're in split-view, unmark message at point.
+Otherwise, warn user that unmarking only works in the header
+list."
+ (interactive)
+ (if (mu4e--view-split-view-p)
+ (mu4e-view-mark-for-unmark)
+ (mu4e-message "Unmarking needs to be done in the header list view")))
+
+(defmacro mu4e--view-defun-mark-for (mark)
+ "Define a function mu4e-view-mark-for- MARK."
+ (let ((funcname (intern (format "mu4e-view-mark-for-%s" mark)))
+ (docstring (format "Mark the current message for %s." mark)))
+ `(progn
+ (defun ,funcname () ,docstring
+ (interactive)
+ (mu4e--view-in-headers-context
+ (mu4e-headers-mark-and-next ',mark)))
+ (put ',funcname 'definition-name ',mark))))
+
+(mu4e--view-defun-mark-for move)
+(mu4e--view-defun-mark-for refile)
+(mu4e--view-defun-mark-for delete)
+(mu4e--view-defun-mark-for flag)
+(mu4e--view-defun-mark-for unflag)
+(mu4e--view-defun-mark-for unmark)
+(mu4e--view-defun-mark-for something)
+(mu4e--view-defun-mark-for read)
+(mu4e--view-defun-mark-for unread)
+(mu4e--view-defun-mark-for trash)
+(mu4e--view-defun-mark-for untrash)
+
+(defun mu4e-view-marked-execute ()
+ "Execute the marked actions."
+ (interactive)
+ (mu4e--view-in-headers-context
+ (mu4e-mark-execute-all)))
+
+
+;;; URL handling
+
+(defvar mu4e--view-link-map nil
+ "A map of some number->url so we can jump to url by number.")
+(put 'mu4e--view-link-map 'permanent-local t)
+
+(defvar mu4e-view-active-urls-keymap
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "<mouse-2>") #'mu4e--view-browse-url-from-binding)
+ (define-key map (kbd "M-<return>") #'mu4e--view-browse-url-from-binding)
+ map)
+ "Keymap used for the URLs inside the body.")
+
+(defvar mu4e--view-beginning-of-url-regexp
+ "https?\\://\\|mailto:"
+ "Regexp that matches the beginning of certain URLs.
+Match-string 1 will contain the matched URL, if any.")
+
+
+(defun mu4e--view-browse-url-from-binding (&optional url)
+ "View in browser the url at point, or click location.
+If the optional argument URL is provided, browse that instead.
+If the url is mailto link, start writing an email to that address."
+ (interactive)
+ (let* (( url (or url (mu4e--view-get-property-from-event 'mu4e-url))))
+ (when url
+ (if (string-match-p "^mailto:" url)
+ (browse-url-mail url)
+ (browse-url url)))))
+
+(defun mu4e--view-get-property-from-event (prop)
+ "Get the property PROP at point, or the location of the mouse.
+The action is chosen based on the `last-command-event'.
+Meant to be evoked from interactive commands."
+ (if (and (eventp last-command-event)
+ (mouse-event-p last-command-event))
+ (let ((posn (event-end last-command-event)))
+ (when (numberp (posn-point posn))
+ (get-text-property
+ (posn-point posn)
+ prop
+ (window-buffer (posn-window posn)))))
+ (get-text-property (point) prop)))
+
+;; this is fairly simplistic...
+(defun mu4e--view-activate-urls ()
+ "Turn things that look like URLs into clickable things.
+Also number them so they can be opened using `mu4e-view-go-to-url'."
+ (let ((num 0))
+ (save-excursion
+ (setq mu4e--view-link-map ;; buffer local
+ (make-hash-table :size 32 :weakness nil))
+ (goto-char (point-min))
+ (while (re-search-forward mu4e--view-beginning-of-url-regexp nil t)
+ (let ((bounds (thing-at-point-bounds-of-url-at-point)))
+ (when bounds
+ (let* ((url (thing-at-point-url-at-point))
+ (ov (make-overlay (car bounds) (cdr bounds))))
+ (puthash (cl-incf num) url mu4e--view-link-map)
+ (add-text-properties
+ (car bounds)
+ (cdr bounds)
+ `(face mu4e-link-face
+ mouse-face highlight
+ mu4e-url ,url
+ keymap ,mu4e-view-active-urls-keymap
+ help-echo
+ "[mouse-1] or [M-RET] to open the link"))
+ (overlay-put ov 'mu4e-overlay t)
+ (overlay-put ov 'after-string
+ (propertize (format "\u200B[%d]" num)
+ 'face 'mu4e-url-number-face)))))))))
+
+
+(defun mu4e--view-get-urls-num (prompt &optional multi)
+ "Ask the user with PROMPT for an URL number for MSG.
+The number is [1..n] for URLs \[0..(n-1)] in the message. If
+MULTI is nil, return the number for the URL; otherwise (MULTI is
+non-nil), accept ranges of URL numbers, as per
+`mu4e-split-ranges-to-numbers', and return the corresponding
+string."
+ (let* ((count (hash-table-count mu4e--view-link-map)) (def))
+ (when (zerop count) (mu4e-error "No links for this message"))
+ (if (not multi)
+ (if (= count 1)
+ (read-number (mu4e-format "%s: " prompt) 1)
+ (read-number (mu4e-format "%s (1-%d): " prompt count)))
+ (progn
+ (setq def (if (= count 1) "1" (format "1-%d" count)))
+ (read-string (mu4e-format "%s (default %s): " prompt def)
+ nil nil def)))))
+
+(defun mu4e-view-go-to-url (&optional multi)
+ "Offer to go visit one or more URLs.
+If MULTI (prefix-argument) is non-nil, offer to go to a range of URLs."
+ (interactive "P")
+ (mu4e--view-handle-urls "URL to visit"
+ multi
+ (lambda (url) (mu4e--view-browse-url-from-binding url))))
+
+(defun mu4e-view-save-url (&optional multi)
+ "Offer to save URLs to the kill ring.
+If MULTI (prefix-argument) is nil, save a single one, otherwise, offer
+to save a range of URLs."
+ (interactive "P")
+ (mu4e--view-handle-urls "URL to save" multi
+ (lambda (url)
+ (kill-new url)
+ (mu4e-message "Saved %s to the kill-ring" url))))
+
+(defun mu4e-view-fetch-url (&optional multi)
+ "Offer to fetch (download) URLs.
+If MULTI (prefix-argument) is nil,
+download a single one, otherwise, offer to fetch a range of
+URLs. The urls are fetched to `mu4e-attachment-dir'."
+ (interactive "P")
+ (mu4e--view-handle-urls
+ "URL to fetch" multi
+ (lambda (url)
+ (let ((target (concat (mu4e-determine-attachment-dir url) "/"
+ (file-name-nondirectory url))))
+ (url-copy-file url target)
+ (mu4e-message "Fetched %s -> %s" url target)))))
+
+(defun mu4e--view-handle-urls (prompt multi urlfunc)
+ "Handle URLs.
+If MULTI is nil, apply URLFUNC to a single uri, otherwise, apply
+it to a range of uris. PROMPT is the query to present to the user."
+ (if multi
+ (mu4e--view-handle-multi-urls prompt urlfunc)
+ (mu4e--view-handle-single-url prompt urlfunc)))
+
+(defun mu4e--view-handle-single-url (prompt urlfunc &optional num)
+ "Apply URLFUNC to some URL with NUM in the current message.
+Prompting the user with PROMPT for the number."
+ (let* ((num (or num (mu4e--view-get-urls-num prompt)))
+ (url (gethash num mu4e--view-link-map)))
+ (unless url (mu4e-warn "Invalid number for URL"))
+ (funcall urlfunc url)))
+
+(defun mu4e--view-handle-multi-urls (prompt urlfunc)
+ "Apply URLFUNC to a a range of URLs in the current message.
+
+Prompting the user with PROMPT for the numbers.
+
+Default is to apply it to all URLs, [1..n], where n is the number
+of urls. You can type multiple values separated by space, e.g. 1
+3-6 8 will visit urls 1,3,4,5,6 and 8.
+
+Furthermore, there is a shortcut \"a\" which means all urls, but as
+this is the default, you may not need it."
+ (let* ((linkstr (mu4e--view-get-urls-num
+ "URL number range (or 'a' for 'all')" t))
+ (count (hash-table-count mu4e--view-link-map))
+ (linknums (mu4e-split-ranges-to-numbers linkstr count)))
+ (dolist (num linknums)
+ (mu4e--view-handle-single-url prompt urlfunc num))))
+
+(defun mu4e-view-for-each-uri (func)
+ "Evaluate FUNC(uri) for each uri in the current message."
+ (maphash (lambda (_num uri) (funcall func uri)) mu4e--view-link-map))
+
+(defun mu4e-view-message-with-message-id (msgid)
+ "View message with message-id MSGID.
+This (re)creates a
+headers-buffer with a search for MSGID, then open a view for that
+message."
+ (mu4e-search (concat "msgid:" msgid) nil nil t msgid t))
+
+;;; Variables
+
+(defvar gnus-icalendar-additional-identities)
+(defvar-local mu4e--view-rendering nil)
+
+(defun mu4e-view (msg)
+ "Display the message MSG in a new buffer, and keep in sync with HDRSBUF.
+\"In sync\" here means that moving to the next/previous message
+in the the message view affects HDRSBUF, as does marking etc.
+
+As a side-effect, a message that is being viewed loses its
+`unread' marking if it still had that."
+ ;; update headers, if necessary.
+ (mu4e~headers-update-handler msg nil nil)
+ ;; Create a new view buffer (if needed) as it is not
+ ;; feasible to recycle an existing buffer due to buffer-specific
+ ;; state (buttons, etc.) that can interfere with message rendering
+ ;; in gnus.
+ ;;
+ ;; Unfortunately that does create its own issues: namely ensuring
+ ;; buffer-local state that *must* survive is correctly copied
+ ;; across.
+ (let ((linked-headers-buffer))
+ (when-let ((existing-buffer (mu4e-get-view-buffer nil nil)))
+ ;; required; this state must carry over from the killed buffer
+ ;; to the new one.
+ (setq linked-headers-buffer mu4e-linked-headers-buffer)
+ (if (memq mu4e-split-view '(horizontal vertical))
+ (delete-windows-on existing-buffer t))
+ (kill-buffer existing-buffer))
+ (setq gnus-article-buffer (mu4e-get-view-buffer nil t))
+ (with-current-buffer gnus-article-buffer
+ (when linked-headers-buffer
+ (setq mu4e-linked-headers-buffer linked-headers-buffer))
+ (let ((inhibit-read-only t)
+ (gnus-unbuttonized-mime-types '(".*/.*"))
+ (gnus-buttonized-mime-types
+ (append (list "multipart/signed" "multipart/encrypted")
+ gnus-buttonized-mime-types))
+ (gnus-inhibit-mime-unbuttonizing t))
+ (remove-overlays (point-min)(point-max) 'mu4e-overlay t)
+ (erase-buffer)
+ (insert-file-contents-literally
+ (mu4e-message-readable-path msg) nil nil nil t)
+ ;; some messages have ^M which causes various rendering
+ ;; problems later (#2260, #2508), so let's remove those
+ (article-remove-cr)
+ (setq-local mu4e--view-message msg)
+ (mu4e--view-render-buffer msg))
+ (mu4e-loading-mode 0)))
+ (unless (mu4e--view-detached-p gnus-article-buffer)
+ (with-current-buffer mu4e-linked-headers-buffer
+ ;; We need this here as we want to avoid displaying the buffer until
+ ;; the last possible moment --- after the message is rendered in the
+ ;; view buffer.
+ ;;
+ ;; Otherwise, `mu4e-display-buffer' may adjust the view buffer's
+ ;; window height based on a buffer that has no text in it yet!
+ (setq-local mu4e~headers-view-win
+ (mu4e-display-buffer gnus-article-buffer nil))
+ (unless (window-live-p mu4e~headers-view-win)
+ (mu4e-error "Cannot get a message view"))
+ (select-window mu4e~headers-view-win)))
+ (with-current-buffer gnus-article-buffer
+ (let ((inhibit-read-only t))
+ (run-hooks 'mu4e-view-rendered-hook))
+ ;; only needed on some setups; #2683
+ (goto-char (point-min))))
+
+(defun mu4e-view-message-text (msg)
+ "Return the rendered MSG as a string."
+ (with-temp-buffer
+ (insert-file-contents-literally
+ (mu4e-message-readable-path msg) nil nil nil t)
+ (let ((gnus-inhibit-mime-unbuttonizing nil)
+ (gnus-unbuttonized-mime-types '(".*/.*"))
+ (mu4e-view-fields '(:from :to :cc :subject :date)))
+ (mu4e--view-render-buffer msg)
+ (buffer-substring-no-properties (point-min) (point-max)))))
+
+(defun mu4e-action-view-in-browser (msg &optional skip-headers)
+ "Show current MSG in browser if it includes an HTML-part.
+If SKIP-HEADERS is set, do not show include message headers.
+The variables `browse-url-browser-function',
+`browse-url-handlers', and `browse-url-default-handlers'
+determine which browser function to use."
+ (with-temp-buffer
+ (insert-file-contents-literally
+ (mu4e-message-readable-path msg) nil nil nil t)
+ ;; just continue if some of the decoding fails.
+ (ignore-errors (run-hooks 'gnus-article-decode-hook))
+ (let ((header (unless skip-headers
+ (cl-loop for field in '("from" "to" "cc" "date" "subject")
+ when (message-field-value field)
+ concat (format "%s: %s\n" (capitalize field) it))))
+ (parts (mm-dissect-buffer t t)))
+ ;; If singlepart, enforce a list.
+ (when (and (bufferp (car parts))
+ (stringp (car (mm-handle-type parts))))
+ (setq parts (list parts)))
+ ;; Process the list
+ (unless (gnus-article-browse-html-parts parts header)
+ (mu4e-warn "Message does not contain a \"text/html\" part"))
+ (mm-destroy-parts parts))))
+
+(defun mu4e-action-view-in-xwidget (msg)
+ "Show current MSG in an embedded xwidget, if available."
+ (unless (fboundp 'xwidget-webkit-browse-url)
+ (mu4e-error "No xwidget support available"))
+ (let ((browse-url-handlers nil)
+ (browse-url-browser-function
+ (lambda (url &optional _rest)
+ (xwidget-webkit-browse-url url))))
+ (mu4e-action-view-in-browser msg)))
+
+(defun mu4e--view-render-buffer (msg)
+ "Render current buffer with MSG using Gnus' article mode."
+ (setq gnus-summary-buffer (get-buffer-create " *appease-gnus*"))
+ (let* ((inhibit-read-only t)
+ (max-specpdl-size mu4e-view-max-specpdl-size)
+ (mm-decrypt-option 'known)
+ (ct (mail-fetch-field "Content-Type"))
+ (ct (and ct (mail-header-parse-content-type ct)))
+ (charset (mail-content-type-get ct 'charset))
+ (charset (and charset (intern charset)))
+ (mu4e--view-rendering t); Needed if e.g. an ics file is buttonized
+ (gnus-article-emulate-mime nil) ;; avoid perf problems
+ (gnus-newsgroup-charset
+ (if (and charset (coding-system-p charset)) charset
+ (detect-coding-region (point-min) (point-max) t)))
+ ;; Possibly add headers (before "Attachments")
+ (gnus-display-mime-function (mu4e--view-gnus-display-mime msg)))
+ (condition-case err
+ (progn
+ (mm-enable-multibyte)
+ ;; just continue if some of the decoding fails.
+ (ignore-errors (run-hooks 'gnus-article-decode-hook))
+ (gnus-article-prepare-display)
+ (mu4e--view-activate-urls)
+ ;; `gnus-summary-bookmark-make-record' does not work properly when "appeased."
+ (kill-local-variable 'bookmark-make-record-function)
+ (setq mu4e~gnus-article-mime-handles gnus-article-mime-handles
+ gnus-article-decoded-p gnus-article-decode-hook)
+ (set-buffer-modified-p nil)
+ (add-hook 'kill-buffer-hook #'mu4e--view-kill-mime-handles))
+ (epg-error
+ (mu4e-warn "EPG error: %s; fall back to raw view"
+ (error-message-string err))))))
+
+(defun mu4e-view-refresh ()
+ "Refresh the message view."
+ ;;; XXX: sometimes, side-effect: increase the header-buffers size
+ (interactive)
+ (when-let ((msg (and (derived-mode-p 'mu4e-view-mode)
+ mu4e--view-message)))
+ (mu4e-view-quit)
+ (mu4e-view msg)))
+
+(defun mu4e-view-toggle-show-mime-parts()
+ "Toggle whether to show all MIME-parts."
+ (interactive)
+ (setq gnus-inhibit-mime-unbuttonizing
+ (not gnus-inhibit-mime-unbuttonizing))
+ (mu4e-view-refresh))
+
+(defun mu4e-view-toggle-fill-flowed()
+ "Toggle flowed-message text filling."
+ (interactive)
+ (setq mm-fill-flowed (not mm-fill-flowed))
+ (mu4e-view-refresh))
+
+(defun mu4e-view-toggle-emulate-mime()
+ "Toggle GNUs MIME-emulation.
+Note that for some messages, this can trigger high CPU load."
+ (interactive)
+ (setq gnus-article-emulate-mime (not gnus-article-emulate-mime))
+ (mu4e-view-refresh))
+
+(defun mu4e--view-gnus-display-mime (msg)
+ "Like `gnus-display-mime', but include mu4e headers to MSG."
+ (lambda (&optional ihandles)
+ (gnus-display-mime ihandles)
+ (unless ihandles
+ (save-restriction
+ (article-goto-body)
+ (forward-line -1)
+ (narrow-to-region (point) (point))
+ (dolist (field mu4e-view-fields)
+ (let ((fieldval (mu4e-message-field msg field)))
+ (pcase field
+ ((or ':path ':maildir ':list)
+ (mu4e--view-gnus-insert-header field fieldval))
+ (':message-id
+ (when-let ((msgid (plist-get msg :message-id)))
+ (mu4e--view-gnus-insert-header field (format "<%s>" msgid))))
+ (':mailing-list
+ (let ((list (plist-get msg :list)))
+ (if list (mu4e-get-mailing-list-shortname list) "")))
+ ((or ':flags ':tags)
+ (let ((flags (mapconcat (lambda (flag)
+ (if (symbolp flag)
+ (symbol-name flag)
+ flag)) fieldval ", ")))
+ (mu4e--view-gnus-insert-header field flags)))
+ (':size (mu4e--view-gnus-insert-header
+ field (mu4e-display-size fieldval)))
+ ((or ':subject ':to ':from ':cc ':bcc ':from-or-to
+ ':user-agent ':date ':attachments
+ ':signature ':decryption)) ;; handled by Gnus
+ (_
+ (mu4e--view-gnus-insert-header-custom msg field)))))
+ (let ((gnus-treatment-function-alist
+ '((gnus-treat-highlight-headers
+ gnus-article-highlight-headers))))
+ (gnus-treat-article 'head))))))
+
+(defun mu4e--view-gnus-insert-header (field val)
+ "Insert a header FIELD with value VAL."
+ (let* ((info (cdr (assoc field mu4e-header-info)))
+ (key (plist-get info :name))
+ (help (plist-get info :help)))
+ (if (and val (> (length val) 0))
+ (insert (propertize (concat key ":") 'help-echo help)
+ " " val "\n"))))
+
+(defun mu4e--view-gnus-insert-header-custom (msg field)
+ "Insert MSG's custom FIELD."
+ (let* ((info (cdr-safe (or (assoc field mu4e-header-info-custom)
+ (mu4e-error "Custom field %S not found" field))))
+ (key (plist-get info :name))
+ (func (or (plist-get info :function)
+ (mu4e-error "No :function defined for custom field %S %S"
+ field info)))
+ (val (funcall func msg))
+ (help (plist-get info :help)))
+ (when (and val (> (length val) 0))
+ (insert (propertize (concat key ":") 'help-echo help) " " val "\n"))))
+
+(define-advice gnus-icalendar-event-from-handle
+ (:filter-args (handle-attendee) mu4e--view-fix-missing-charset)
+ "Avoid error when displaying an ical attachment without a charset."
+ (if (and (boundp 'mu4e--view-rendering) mu4e--view-rendering)
+ (let* ((handle (car handle-attendee))
+ (attendee (cadr handle-attendee))
+ (buf (mm-handle-buffer handle))
+ (ty (mm-handle-type handle))
+ (rest (cddr handle)))
+ ;; Put the fallback at the end:
+ (setq ty (append ty '((charset . "utf-8"))))
+ (setq handle (cons buf (cons ty rest)))
+ (list handle attendee))
+ handle-attendee))
+
+(defun mu4e--view-mode-p ()
+ "Is the buffer in mu4e-view-mode or one of its descendants?"
+ (or (eq major-mode 'mu4e-view-mode)
+ (derived-mode-p '(mu4e-view-mode))))
+
+(defun mu4e--view-nop (func &rest args)
+ "Do not invoke FUNC with ARGS when in mu4e-view-mode.
+This is useful for advising some Gnus-functionality that does not work in mu4e."
+ (unless (mu4e--view-mode-p)
+ (apply func args)))
+
+(defun mu4e--view-button-reply (func &rest args)
+ "Advise FUNC with ARGS to make `gnus-button-reply' links work in mu4e."
+ (if (mu4e--view-mode-p)
+ (mu4e-compose-reply)
+ (apply func args)))
+
+(defun mu4e--view-button-message-id (func &rest args)
+ "Advise FUNC with ARGS to make `gnus-button-message-id' links work in mu4e."
+ (if (and (mu4e--view-mode-p) (stringp (car-safe args)))
+ (mu4e-view-message-with-message-id (car args))
+ (apply func args)))
+
+(defun mu4e--view-msg-mail (func &rest args)
+ "Advise FUNC with ARGS to make `gnus-msg-mail' links compose with mu4e."
+ (if (mu4e--view-mode-p)
+ (apply 'mu4e-compose-mail args)
+ (apply func args)))
+
+(defun mu4e-view-quit ()
+ "Quit the mu4e-view buffer."
+ (interactive)
+ (if (memq mu4e-split-view '(horizontal vertical))
+ (ignore-errors ;; try, don't error out.
+ (kill-buffer-and-window))
+ ;; single-window case
+ (let ((docid (mu4e-field-at-point :docid)))
+ (when mu4e-linked-headers-buffer ;; re-use mu4e-view-detach?
+ (with-current-buffer mu4e-linked-headers-buffer
+ (when (eq (selected-window) mu4e~headers-view-win)
+ (setq mu4e~headers-view-win nil)))
+ (setq mu4e-linked-headers-buffer nil)
+ (kill-buffer)
+ ;; attempt to move point to just-viewed message.
+ (when docid
+ (ignore-errors
+ (mu4e~headers-goto-docid docid)))))))
+
+(defvar mu4e-view-mode-map
+ (let ((map (make-keymap)))
+ (define-key map (kbd "C-S-u") #'mu4e-update-mail-and-index)
+ (define-key map (kbd "C-c C-u") #'mu4e-update-mail-and-index)
+
+ (define-key map "q" #'mu4e-view-quit)
+
+ (define-key map "z" #'mu4e-view-detach)
+ (define-key map "Z" #'mu4e-view-attach)
+
+ (define-key map "%" #'mu4e-view-mark-pattern)
+ (define-key map "t" #'mu4e-view-mark-subthread)
+ (define-key map "T" #'mu4e-view-mark-thread)
+
+ (define-key map "g" #'mu4e-view-go-to-url)
+ (define-key map "k" #'mu4e-view-save-url)
+ (define-key map "f" #'mu4e-view-fetch-url)
+
+ (define-key map "." #'mu4e-view-raw-message)
+ (define-key map "," #'mu4e-sexp-at-point)
+ (define-key map "|" #'mu4e-view-pipe)
+ (define-key map "a" #'mu4e-view-action)
+ (define-key map "A" #'mu4e-view-mime-part-action)
+ (define-key map "e" #'mu4e-view-save-attachments)
+
+ ;; change the number of headers
+ (define-key map (kbd "C-+") #'mu4e-headers-split-view-grow)
+ (define-key map (kbd "C--") #'mu4e-headers-split-view-shrink)
+ (define-key map (kbd "<C-kp-add>") #'mu4e-headers-split-view-grow)
+ (define-key map (kbd "<C-kp-subtract>") #'mu4e-headers-split-view-shrink)
+
+ ;; intra-message navigation
+ (define-key map (kbd "S-SPC") #'scroll-down)
+ (define-key map (kbd "SPC") #'mu4e-view-scroll-up-or-next)
+ (define-key map (kbd "RET") #'mu4e-scroll-up)
+ (define-key map (kbd "<backspace>") #'mu4e-scroll-down)
+
+ ;; navigation between messages
+ (define-key map "p" #'mu4e-view-headers-prev)
+ (define-key map "n" #'mu4e-view-headers-next)
+ ;; the same
+ (define-key map (kbd "<M-down>") #'mu4e-view-headers-next)
+ (define-key map (kbd "<M-up>") #'mu4e-view-headers-prev)
+
+ (define-key map (kbd "[") #'mu4e-view-headers-prev-unread)
+ (define-key map (kbd "]") #'mu4e-view-headers-next-unread)
+ (define-key map (kbd "{") #'mu4e-view-headers-prev-thread)
+ (define-key map (kbd "}") #'mu4e-view-headers-next-thread)
+
+ ;; ;; threads
+ ;; TODO: find some binding that don't conflict
+ ;; (define-key map (kbd "<S-left>") #'mu4e-view-thread-goto-root)
+ ;; ;; <tab> is taken already
+ ;; (define-key map (kbd "<C-S-tab>") #'mu4e-view-thread-fold-toggle-goto-next)
+ ;; (define-key map (kbd "<backtab>") #'mu4e-view-thread-fold-toggle-all)
+
+
+ ;; switching from view <-> headers (when visible)
+ (define-key map "y" #'mu4e-select-other-view)
+
+ ;; marking/unmarking
+ (define-key map "d" #'mu4e-view-mark-for-trash)
+ (define-key map (kbd "<delete>") #'mu4e-view-mark-for-delete)
+ (define-key map (kbd "<deletechar>") #'mu4e-view-mark-for-delete)
+ (define-key map (kbd "D") #'mu4e-view-mark-for-delete)
+ (define-key map (kbd "m") #'mu4e-view-mark-for-move)
+ (define-key map (kbd "r") #'mu4e-view-mark-for-refile)
+
+ (define-key map (kbd "?") #'mu4e-view-mark-for-unread)
+ (define-key map (kbd "!") #'mu4e-view-mark-for-read)
+
+ (define-key map (kbd "+") #'mu4e-view-mark-for-flag)
+ (define-key map (kbd "-") #'mu4e-view-mark-for-unflag)
+ (define-key map (kbd "=") #'mu4e-view-mark-for-untrash)
+ (define-key map (kbd "&") #'mu4e-view-mark-custom)
+
+ (define-key map (kbd "*") #'mu4e-view-mark-for-something)
+ (define-key map (kbd "<kp-multiply>") #'mu4e-view-mark-for-something)
+ (define-key map (kbd "<insert>") #'mu4e-view-mark-for-something)
+ (define-key map (kbd "<insertchar>") #'mu4e-view-mark-for-something)
+
+ (define-key map ";" #'mu4e-context-switch)
+
+ (define-key map (kbd "#") #'mu4e-mark-resolve-deferred-marks)
+ ;; misc
+ (define-key map "M" #'mu4e-view-massage)
+
+ (define-key map "w" #'visual-line-mode)
+ (define-key map "h" #'mu4e-view-toggle-html)
+ (define-key map (kbd "M-q") #'article-fill-long-lines)
+
+ (define-key map "c" #'mu4e-copy-thing-at-point)
+
+ ;; next 3 only warn user when attempt in the message view
+ (define-key map "u" #'mu4e-view-unmark)
+ (define-key map "U" #'mu4e-view-unmark-all)
+ (define-key map "x" #'mu4e-view-marked-execute)
+
+ (define-key map "$" #'mu4e-show-log)
+ (define-key map "H" #'mu4e-display-manual)
+
+ ;; Make 0..9 shortcuts for digit-argument. Actually, none of the bound
+ ;; functions seem to use a prefix arg but those bindings existed because we
+ ;; used to use `suppress-keymap'. And possibly users added their own
+ ;; prefix arg consuming commands.
+ (dotimes (i 10)
+ (define-key map (kbd (format "%d" i)) #'digit-argument))
+
+ (set-keymap-parent map special-mode-map)
+ (set-keymap-parent map button-buffer-map)
+ map)
+ "Keymap for mu4e-view mode.")
+
+(easy-menu-define mu4e-view-mode-menu
+ mu4e-view-mode-map "Menu for mu4e's view-mode."
+ (append
+ '("View"
+ "--"
+ ["Toggle wrap lines" visual-line-mode]
+ ["View raw" mu4e-view-raw-message]
+ ["Pipe through shell" mu4e-view-pipe]
+ "--"
+ ["Mark for deletion" mu4e-view-mark-for-delete]
+ ["Mark for untrash" mu4e-view-mark-for-untrash]
+ ["Mark for trash" mu4e-view-mark-for-trash]
+ ["Mark for move" mu4e-view-mark-for-move]
+ )
+ mu4e--compose-menu-items
+ mu4e--search-menu-items
+ mu4e--context-menu-items
+ '(
+ "--"
+ ["Quit" mu4e-view-quit
+ :help "Quit the view"]
+ )))
+
+(defcustom mu4e-raw-view-mode-hook nil
+ "Hook run when entering \\[mu4e-raw-view] mode."
+ :options '()
+ :type 'hook
+ :group 'mu4e-view)
+
+(defcustom mu4e-view-mode-hook nil
+ "Hook run when entering \\[mu4e-view] mode."
+ :options '(turn-on-visual-line-mode)
+ :type 'hook
+ :group 'mu4e-view)
+
+(defcustom mu4e-view-rendered-hook '(mu4e-resize-linked-headers-window)
+ "Hook run by `mu4e-view' after a message is rendered."
+ :type 'hook
+ :group 'mu4e-view)
+
+(define-derived-mode mu4e-raw-view-mode fundamental-mode "mu4e:raw-view"
+ (view-mode))
+
+;; "Define the major-mode for the mu4e-view."
+(define-derived-mode mu4e-view-mode gnus-article-mode "mu4e:view"
+ "Major mode for viewing an e-mail message in mu4e.
+Based on Gnus' article-mode."
+ ;; some external tools (bbdb) depend on this
+ (setq gnus-article-buffer (current-buffer))
+
+ ;; ;; turn off gnus modeline changes and menu items
+ (advice-add 'gnus-set-mode-line :around #'mu4e--view-nop)
+ (advice-add 'gnus-button-reply :around #'mu4e--view-button-reply)
+ (advice-add 'gnus-button-message-id :around #'mu4e--view-button-message-id)
+ (advice-add 'gnus-msg-mail :around #'mu4e--view-msg-mail)
+
+ ;; advice gnus-block-private-groups to always return "."
+ ;; so that by default we block images.
+ (advice-add 'gnus-block-private-groups :around
+ (lambda(func &rest args)
+ (if (mu4e--view-mode-p)
+ "." (apply func args))))
+ (use-local-map mu4e-view-mode-map)
+ (mu4e-context-minor-mode)
+ (mu4e-search-minor-mode)
+ (mu4e-compose-minor-mode)
+ (setq buffer-undo-list t) ;; don't record undo info
+
+ ;; support bookmarks.
+ (set (make-local-variable 'bookmark-make-record-function)
+ 'mu4e--make-bookmark-record)
+
+ ;; autopair mode gives error when pressing RET
+ ;; turn it off
+ (when (boundp 'autopair-dont-activate)
+ (setq autopair-dont-activate t)))
+
+;;; Massaging the message view
+
+(defcustom mu4e-view-massage-options
+ '( ("ctoggle citations" . gnus-article-hide-citation)
+ ("htoggle headers" . gnus-article-hide-headers)
+ ("ytoggle crypto" . gnus-article-hide-pem)
+ ("ftoggle fill-flowed" . mu4e-view-toggle-fill-flowed)
+ ("mtoggle show all MIME parts" . mu4e-view-toggle-show-mime-parts)
+ ("Mtoggle show emulate MIME" . mu4e-view-toggle-emulate-mime))
+"Various options for \"massaging\" the message view. See `(gnus)
+Article Treatment' for more options."
+ :group 'mu4e-view
+ :type '(alist :key-type string :value-type function))
+
+(defun mu4e-view-massage()
+ "Massage current message view as per `mu4e-view-massage-options'."
+ (interactive)
+ (funcall (mu4e-read-option "Massage: " mu4e-view-massage-options)))
+
+\f
+(defun mu4e-view-toggle-html ()
+ "Toggle html-display of the first html-part found."
+ (interactive)
+ ;; This function assumes `gnus-article-mime-handle-alist' is sorted by
+ ;; pertinence, i.e. the first HTML part found in it is the most important one.
+ (save-excursion
+ (if-let ((html-part
+ (seq-find (lambda (handle)
+ (equal (mm-handle-media-type (cdr handle))
+ "text/html"))
+ gnus-article-mime-handle-alist))
+ (text-part
+ (seq-find (lambda (handle)
+ (equal (mm-handle-media-type (cdr handle))
+ "text/plain"))
+ gnus-article-mime-handle-alist)))
+ (gnus-article-inline-part (car html-part))
+ (mu4e-warn "Cannot switch; no html and/or text part in this message"))))
+\f
+;;; Bug Reference mode support
+
+;; Due to mu4e's view buffer handling (mu4e-view-mode is called long before the
+;; actual mail text is inserted into the buffer), one should activate
+;; bug-reference-mode in mu4e-after-view-message-hook, not mu4e-view-mode-hook.
+
+;; This is Emacs 28 stuff but there is no need to guard it with some (f)boundp
+;; checks (which would return nil if bug-reference.el is not loaded before
+;; mu4e) since the function definition doesn't hurt and `add-hook' works fine
+;; for not yet defined variables (by creating them).
+(declare-function bug-reference-maybe-setup-from-mail "ext:bug-reference")
+
+(defvar mu4e--view-bug-reference-checked-headers
+ '("list" "list-id" "to" "from" "cc" "subject" "reply-to")
+ "List of mail headers whose values are passed to bug-reference's auto-setup.")
+
+(defun mu4e--view-try-setup-bug-reference-mode ()
+ "Try to guess bug-reference setup from the current mu4e mail.
+Looks at the maildir and the mail headers in
+`mu4e--view-bug-reference-checked-headers' and tries to guess suitable
+values for `bug-reference-bug-regexp' and
+`bug-reference-url-format' by matching the maildir name against
+GROUP-REGEXP and each header value against HEADER-REGEXP in
+`bug-reference-setup-from-mail-alist'."
+ (when (derived-mode-p 'mu4e-view-mode)
+ (let (header-values)
+ (save-excursion
+ (goto-char (point-min))
+ (dolist (field mu4e--view-bug-reference-checked-headers)
+ (let ((val (mail-fetch-field field)))
+ (when val
+ (push val header-values)))))
+ (bug-reference-maybe-setup-from-mail
+ (mail-fetch-field "maildir")
+ header-values))))
+
+(with-eval-after-load 'bug-reference
+ (add-hook 'bug-reference-auto-setup-functions
+ #'mu4e--view-try-setup-bug-reference-mode))
+
+\f
+(provide 'mu4e-view)
+;;; mu4e-view.el ends here
--- /dev/null
+;;; mu4e-window.el --- Window management -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2022 Mickey Petersen
+;; Copyright (C) 2023-2024 Dirk-Jan C. Binnema
+
+;; Author: Mickey Petersen <mickey@masteringemacs.org>
+;; Keywords: mail
+
+;; 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:
+
+;;; Buffer names for internal use
+
+(defconst mu4e--sexp-buffer-name "*mu4e-sexp-at-point*"
+ "Buffer name for sexp buffers.")
+
+(defvar mu4e-main-buffer-name "*mu4e-main*"
+ "Name of the mu4e main buffer.")
+
+(defvar mu4e-embedded-buffer-name " *mu4e-embedded*"
+ "Name for the embedded message view buffer.")
+
+;; Buffer names for public use
+
+(defvar mu4e-headers-buffer-name "*mu4e-headers*"
+ "Name of the buffer for message headers.")
+
+(defvar mu4e-view-buffer-name "*mu4e-article*"
+ "Name of the view buffer.")
+
+(defvar mu4e-headers-buffer-name-func nil
+ "Function used to name the headers buffers.")
+
+(defvar mu4e-view-buffer-name-func nil
+ "Function used to name the view buffers.
+
+The function is given one argument, the headers buffer it is
+linked to.")
+
+(defvar-local mu4e-linked-headers-buffer nil
+ "Holds the headers buffer object that ties it to a view.")
+
+(defcustom mu4e-split-view 'horizontal
+ "How to show messages / headers.
+A symbol which is either:
+ * `horizontal': split horizontally (headers on top)
+ * `vertical': split vertically (headers on the left).
+ * `single-window': view and headers in one window (mu4e will try not to
+ touch your window layout), main view in minibuffer
+ * anything else: don't split (show either headers or messages,
+ not both).
+
+Also see `mu4e-headers-visible-lines' and
+`mu4e-headers-visible-columns'.
+
+Note that in older mu4e version, the value could also be
+function; this is no longer supported; instead you can use
+`display-buffer-alist'."
+ :type '(choice (const :tag "Split horizontally" horizontal)
+ (const :tag "Split vertically" vertical)
+ (const :tag "Single window" single-window)
+ (const :tag "Don't split" nil))
+ :group 'mu4e-headers)
+
+(defcustom mu4e-headers-visible-lines 10
+ "Number of lines to display in the header view when using the
+horizontal split-view. This includes the header-line at the top,
+and the mode-line."
+ :type 'integer
+ :group 'mu4e-headers)
+
+(defcustom mu4e-headers-visible-columns 30
+ "Number of columns to display for the header view when using the
+vertical split-view."
+ :type 'integer
+ :group 'mu4e-headers)
+
+(defcustom mu4e-compose-switch nil
+ "Where to display the new message?
+A symbol:
+- nil : default (new buffer)
+- window : compose in new window
+- frame or t : compose in new frame
+- display-buffer: use `display-buffer' / `display-buffer-alist'
+ (for fine-tuning).
+
+For backward compatibility with `mu4e-compose-in-new-frame', t is
+treated as =\\'frame."
+ :type 'symbol
+ :group 'mu4e-compose)
+
+(declare-function mu4e-view-mode "mu4e-view")
+(declare-function mu4e-error "mu4e-helpers")
+(declare-function mu4e-warn "mu4e-helpers")
+(declare-function mu4e-message "mu4e-helpers")
+
+(defun mu4e-get-headers-buffer (&optional buffer-name create)
+ "Return a related headers buffer optionally named BUFFER-NAME.
+
+If CREATE is non-nil, the headers buffer is created if the
+generated name does not already exist."
+ (let* ((buffer-name
+ (or
+ ;; buffer name generator func. If a user wants
+ ;; to supply its own naming scheme, we use that
+ ;; in lieu of our own heuristic.
+ (and mu4e-headers-buffer-name-func
+ (funcall mu4e-headers-buffer-name-func))
+ ;; if we're supplied a buffer name for a
+ ;; headers buffer then try to use that one.
+ buffer-name
+ ;; if we're asking for a headers buffer from a
+ ;; view, then we get our linked buffer. If
+ ;; there is no such linked buffer -- it is
+ ;; detached -- raise an error.
+ (and (mu4e-current-buffer-type-p 'view)
+ mu4e-linked-headers-buffer)
+ ;; if we're already in a headers buffer then
+ ;; that is the one we use.
+ (and (mu4e-current-buffer-type-p 'headers)
+ (current-buffer))
+ ;; default name to use if all other checks fail.
+ mu4e-headers-buffer-name))
+ (buffer (get-buffer buffer-name)))
+ (when (and (not (buffer-live-p buffer)) create)
+ (setq buffer (get-buffer-create buffer-name)))
+ ;; This may conceivably return a non-existent buffer if `create'
+ ;; and `buffer-live-p' are nil.
+ ;;
+ ;; This is seemingly "OK" as various parts of the code check for
+ ;; buffer liveness themselves.
+ buffer))
+
+(defun mu4e-get-view-buffers (pred)
+ "Filter all known view buffers and keep those where PRED return non-nil.
+
+The PRED function is called from inside the buffer that is being
+tested."
+ (seq-filter
+ (lambda (buf)
+ (with-current-buffer buf
+ (and (mu4e-current-buffer-type-p 'view)
+ (and pred (funcall pred buf)))))
+ (buffer-list)))
+
+(defun mu4e--view-detached-p (buffer)
+ "Return non-nil if BUFFER is a detached view buffer."
+ (with-current-buffer buffer
+ (unless (mu4e-current-buffer-type-p 'view)
+ (mu4e-error "Buffer `%s' is not a valid mu4e view buffer" buffer))
+ (null mu4e-linked-headers-buffer)))
+
+(defun mu4e--get-current-buffer-type ()
+ "Return an internal symbol that corresponds to each mu4e major mode."
+ (cond ((or (derived-mode-p 'mu4e-view-mode)
+ (derived-mode-p 'mu4e-raw-view-mode)) 'view)
+ ((derived-mode-p 'mu4e-headers-mode) 'headers)
+ ((derived-mode-p 'mu4e-compose-mode) 'compose)
+ ((derived-mode-p 'mu4e-main-mode) 'main)
+ (t 'unknown)))
+
+(defun mu4e-current-buffer-type-p (type)
+ "Return non-nil if the current buffer is a mu4e buffer of TYPE.
+
+Where TYPE is `view', `headers', `compose', `main' or `unknown'.
+
+Checks are performed using `derived-mode-p' and the current
+buffer's major mode."
+ (eq (mu4e--get-current-buffer-type) type))
+
+
+;; backward-compat; buffer-local-boundp was introduced in emacs 28.
+(defun mu4e--buffer-local-boundp (symbol buffer)
+ "Return non-nil if SYMBOL is bound in BUFFER.
+Also see `local-variable-p'."
+ (condition-case nil
+ (buffer-local-value symbol buffer)
+ (:success t)
+ (void-variable nil)))
+
+
+(defun mu4e-get-view-buffer (&optional headers-buffer create)
+ "Return a view buffer belonging optionally to HEADERS-BUFFER.
+
+If HEADERS-BUFFER is nil, the most likely (and available) headers
+buffer is used.
+
+Detached view buffers are ignored; that may result in a new view buffer
+being created if CREATE is non-nil."
+ ;; If `headers-buffer' is nil, then the caller does not have a
+ ;; headers buffer preference.
+ ;;
+ ;; In that case, we request the most plausible headers buffer from
+ ;; `mu4e-get-headers-buffer'.
+ (when (setq headers-buffer (or headers-buffer (mu4e-get-headers-buffer)))
+ (let ((buffer)
+ ;; If `mu4e-view-buffer-name-func' is non-nil, then use that
+ ;; to source the name of the view buffer to create or re-use.
+ (buffer-name
+ (or (and mu4e-view-buffer-name-func
+ (funcall mu4e-view-buffer-name-func headers-buffer))
+ ;; If the variable is nil, use the default
+ ;; name
+ mu4e-view-buffer-name))
+ ;; Search all view buffers and return those that are linked to
+ ;; `headers-buffer'.
+ (linked-buffer
+ (mu4e-get-view-buffers
+ (lambda (buf)
+ (and (mu4e--buffer-local-boundp 'mu4e-linked-headers-buffer buf)
+ (eq mu4e-linked-headers-buffer headers-buffer))))))
+ ;; If such a linked buffer exists and its buffer is live, we use that
+ ;; buffer.
+ (if (and linked-buffer (buffer-live-p (car linked-buffer)))
+ ;; NOTE: It's possible for there to be more than one linked view
+ ;; buffer.
+ ;;
+ ;; What, if anything, should the heuristic be to pick the
+ ;; one to use? Presently `car' is used, but there are better
+ ;; ways, no doubt. Perhaps preferring those with live windows?
+ (setq buffer (car linked-buffer))
+ (setq buffer (get-buffer buffer-name))
+ ;; check if `buffer' is already live *and* detached. If it is,
+ ;; we'll generate a new, unique name.
+ (when (and (buffer-live-p buffer) (mu4e--view-detached-p buffer))
+ (setq buffer (generate-new-buffer-name buffer-name)))
+ (when (and (not (buffer-live-p buffer)) create)
+ (setq buffer (get-buffer-create (or buffer buffer-name)))
+ (with-current-buffer buffer
+ (mu4e-view-mode))))
+ (when (and buffer (buffer-live-p buffer))
+ ;; Required. Callers expect the view buffer to be set.
+ (set-buffer buffer)
+ ;; Required. The call chain of `mu4e-view-mode' ends up
+ ;; calling `kill-all-local-variables', which destroys the
+ ;; local binding.
+ (set (make-local-variable 'mu4e-linked-headers-buffer) headers-buffer))
+ buffer)))
+
+;; backward compat: `display-buffer-full-frame' only appears in emacs 29.
+(unless (fboundp 'display-buffer-full-frame)
+ (defun display-buffer-full-frame (buffer alist)
+ "Display BUFFER in the current frame, taking the entire frame.
+ALIST is an association list of action symbols and values. See
+Info node `(elisp) Buffer Display Action Alists' for details of
+such alists.
+
+This is an action function for buffer display, see Info
+node `(elisp) Buffer Display Action Functions'. It should be
+called only by `display-buffer' or a function directly or
+indirectly called by the latter."
+ (when-let ((window (or (display-buffer-reuse-window buffer alist)
+ (display-buffer-same-window buffer alist)
+ (display-buffer-pop-up-window buffer alist)
+ (display-buffer-use-some-window buffer alist))))
+ (delete-other-windows window)
+ window)))
+
+
+(defun mu4e-display-buffer (buffer-or-name &optional select)
+ "Display BUFFER-OR-NAME as per `mu4e-split-view'.
+
+If SELECT is non-nil, the final window (and thus BUFFER-OR-NAME)
+is selected.
+
+This function internally uses `display-buffer' (or
+`pop-to-buffer' if SELECT is non-nil).
+
+It is therefore possible to change the display behavior by
+modifying `display-buffer-alist'.
+
+If `mu4e-split-view' is a function, then it must return a live window
+for BUFFER-OR-NAME to be displayed in."
+ ;; For now, using a function for mu4e-split-view is not behaving well
+ ;; Turn off.
+ (when (functionp mu4e-split-view)
+ (mu4e-message "Function for `mu4e-split-view' not supported; fallback")
+ (setq mu4e-split-view 'horizontal))
+
+ (let* ((buffer-name (or (get-buffer buffer-or-name)
+ (mu4e-error "Buffer `%s' does not exist"
+ buffer-or-name)))
+ (buffer-type
+ (with-current-buffer buffer-name (mu4e--get-current-buffer-type)))
+ (direction (cons 'direction
+ (pcase (cons buffer-type mu4e-split-view)
+ ;; views or headers can display
+ ;; horz/vert depending on the value of
+ ;; `mu4e-split-view'
+ (`(,(or 'view 'headers) . horizontal) 'below)
+ (`(,(or 'view 'headers) . vertical) 'right)
+ (`(,_ . t) nil))))
+ (window-size
+ (pcase (cons buffer-type mu4e-split-view)
+ ;; views or headers can display
+ ;; horz/vert depending on the value of
+ ;; `mu4e-split-view'
+ ('(view . horizontal)
+ '((window-height . shrink-window-if-larger-than-buffer)))
+ ('(view . vertical)
+ '((window-min-width . fit-window-to-buffer)))
+ (`(,_ . t) nil)))
+ (window-action (cond
+ ;; main-buffer
+ ((eq buffer-type 'main)
+ '(display-buffer-reuse-window
+ display-buffer-reuse-mode-window
+ display-buffer-full-frame))
+ ;; compose-buffer
+ ((eq buffer-type 'compose)
+ (pcase mu4e-compose-switch
+ ('window #'display-buffer-pop-up-window)
+ ((or 'frame 't) #'display-buffer-pop-up-frame)
+ (_ '(display-buffer-reuse-window
+ display-buffer-reuse-mode-window
+ display-buffer-same-window))))
+ ;; headers buffer
+ ((memq buffer-type '(headers))
+ '(display-buffer-reuse-window
+ display-buffer-reuse-mode-window
+ display-buffer-same-window))
+
+ ((memq mu4e-split-view '(horizontal vertical))
+ '(display-buffer-in-direction))
+
+ ((memq mu4e-split-view '(single-window))
+ '(display-buffer-reuse-window
+ display-buffer-reuse-mode-window
+ display-buffer-same-window))
+ ;; I cannot discern a difference between
+ ;; `single-window' and "anything else" in
+ ;; `mu4e-split-view'.
+ (t '(display-buffer-reuse-window
+ display-buffer-reuse-mode-window
+ display-buffer-same-window))))
+ (arg `((,@window-action)
+ ,@window-size
+ ,direction)))
+ (funcall (if select #'pop-to-buffer #'display-buffer)
+ buffer-name
+ arg)))
+
+(defun mu4e-resize-linked-headers-window ()
+ "Resizes the linked headers window belonging to a view.
+
+Resizes the current headers view according to `mu4e-split-view'
+and `mu4e-headers-visible-lines' or
+`mu4e-headers-visible-columns'.
+
+This function is best called from the hook
+`mu4e-view-rendered-hook'."
+ (unless (mu4e-current-buffer-type-p 'view)
+ (mu4e-error "Cannot resize as this is not a valid view buffer."))
+ (when-let (win (and mu4e-linked-headers-buffer
+ (get-buffer-window mu4e-linked-headers-buffer)))
+ ;; This can fail for any number of reasons. If it does, we do
+ ;; nothing. If the user has customized the window display we may
+ ;; find it impossible to resize the window, and that should not be
+ ;; cause for error.
+ (ignore-errors
+ (cond ((eq mu4e-split-view 'vertical)
+ (window-resize win (- mu4e-headers-visible-columns
+ (window-width win nil))
+ t t nil))
+ ((eq mu4e-split-view 'horizontal)
+ (set-window-text-height win mu4e-headers-visible-lines))))))
+
+(provide 'mu4e-window)
+;;; mu4e-window.el ends here
--- /dev/null
+;;; mu4e.el --- Mu4e, the mu mail user agent -*- lexical-binding: t -*-
+
+;; Copyright (C) 2011-2023 Dirk-Jan C. Binnema
+
+;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
+;; Keywords: email
+
+;; This file is not part of GNU Emacs.
+
+;; mu4e 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.
+
+;; mu4e 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 mu4e. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+(require 'mu4e-obsolete)
+
+(require 'mu4e-vars)
+(require 'mu4e-window)
+(require 'mu4e-helpers)
+(require 'mu4e-folders)
+(require 'mu4e-context)
+(require 'mu4e-contacts)
+(require 'mu4e-headers)
+(require 'mu4e-search)
+(require 'mu4e-view)
+(require 'mu4e-compose)
+(require 'mu4e-bookmarks)
+(require 'mu4e-update)
+(require 'mu4e-main)
+(require 'mu4e-notification)
+(require 'mu4e-server) ;; communication with backend
+
+\f
+
+(when mu4e-speedbar-support
+ (require 'mu4e-speedbar)) ;; support for speedbar
+(when mu4e-org-support
+ (require 'mu4e-org)) ;; support for org-mode links
+
+;; We can't properly use compose buffers that are revived using
+;; desktop-save-mode; so let's turn that off.
+(with-eval-after-load 'desktop
+ (eval '(add-to-list 'desktop-modes-not-to-save 'mu4e-compose-mode)))
+\f
+
+;;;###autoload
+(defun mu4e (&optional background)
+ "If mu4e is not running yet, start it.
+Then, show the main window, unless BACKGROUND (prefix-argument)
+is non-nil."
+ (interactive "P")
+ (if (not (mu4e-running-p))
+ (progn
+ (mu4e--init-handlers)
+ (mu4e--start (unless background #'mu4e--main-view)))
+ ;; mu4e already running; show unless BACKGROUND
+ (unless background
+ (if (buffer-live-p (get-buffer mu4e-main-buffer-name))
+ (switch-to-buffer mu4e-main-buffer-name)
+ (mu4e--main-view)))))
+
+(defun mu4e-quit(&optional bury)
+ "Quit the mu4e session or bury the buffer.
+
+If prefix-argument BURY is non-nil, merely bury the buffer.
+Otherwise, completely quit mu4e, including automatic updating."
+ (interactive "P")
+ (if bury
+ (bury-buffer)
+ (if mu4e-confirm-quit
+ (when (y-or-n-p (mu4e-format "Are you sure you want to quit?"))
+ (mu4e--stop))
+ (mu4e--stop))))
+\f
+;;; Internals
+
+(defun mu4e--check-requirements ()
+ "Check for the settings required for running mu4e."
+ (unless (>= emacs-major-version 25)
+ (mu4e-error "Emacs >= 25.x is required for mu4e"))
+ (when (mu4e-server-properties)
+ (unless (string= (mu4e-server-version) mu4e-mu-version)
+ (mu4e-error "The mu server has version %s, but we need %s"
+ (mu4e-server-version) mu4e-mu-version)))
+ (unless (and mu4e-mu-binary (file-executable-p mu4e-mu-binary))
+ (mu4e-error "Please set `mu4e-mu-binary' to the full path to the mu
+ binary"))
+ (dolist (var '(mu4e-sent-folder mu4e-drafts-folder
+ mu4e-trash-folder))
+ (unless (and (boundp var) (symbol-value var))
+ (mu4e-error "Please set %S" var))
+ (unless (functionp (symbol-value var)) ;; functions are okay, too
+ (let* ((dir (symbol-value var))
+ (path (mu4e-join-paths (mu4e-root-maildir) dir)))
+ (unless (string= (substring dir 0 1) "/")
+ (mu4e-error "%S must start with a '/'" dir))
+ (unless (mu4e-create-maildir-maybe path)
+ (mu4e-error "%s (%S) does not exist" path var))))))
+
+;;; Starting / getting mail / updating the index
+
+(defun mu4e--pong-handler (_data func)
+ "Handle \"pong\" responses from the mu server.
+Invoke FUNC if non-nil."
+ (let ((doccount (plist-get (mu4e-server-properties) :doccount)))
+ (mu4e--check-requirements)
+ (when func (funcall func))
+ (when (zerop doccount)
+ (mu4e-message "Store is empty; try indexing (M-x mu4e-update-index)."))
+ (when (and mu4e-update-interval (null mu4e--update-timer))
+ (setq mu4e--update-timer
+ (run-at-time 0 mu4e-update-interval
+ (lambda () (mu4e-update-mail-and-index
+ mu4e-index-update-in-background)))))))
+
+(defun mu4e--start (&optional func)
+ "Start mu4e.
+If `mu4e-contexts' have been defined, but we don't have a context
+yet, switch to the matching one, or none matches, the first. If
+mu4e is already running, invoke FUNC (if non-nil).
+
+Otherwise, check requirements, then start mu4e. When successful, invoke
+ FUNC (if non-nil) afterwards."
+ (unless (mu4e-context-current)
+ (mu4e--context-autoswitch nil mu4e-context-policy))
+ (setq mu4e-pong-func
+ (lambda (info) (mu4e--pong-handler info func)))
+ ;; show some notification?
+ (when mu4e-notification-support
+ (add-hook 'mu4e-query-items-updated-hook #'mu4e--notification))
+ ;; modeline support
+ (when mu4e-modeline-support
+ (mu4e--modeline-register #'mu4e--bookmarks-modeline-item 'global)
+ (mu4e-modeline-mode)
+ (add-hook 'mu4e-query-items-updated-hook #'mu4e--modeline-update))
+ (mu4e-modeline-mode (if mu4e-modeline-support 1 -1))
+ ;; redraw main buffer if there is one.
+ (add-hook 'mu4e-query-items-updated-hook #'mu4e--main-redraw)
+ (mu4e--query-items-refresh 'reset-baseline)
+ (mu4e--server-ping)
+ ;; ask for the maildir-list
+ (mu4e--server-data 'maildirs)
+ ;; maybe request the list of contacts, automatically refreshed after
+ ;; re-indexing
+ (unless mu4e--contacts-set
+ (mu4e--request-contacts-maybe)))
+
+(defun mu4e--stop ()
+ "Stop mu4e."
+ (when mu4e--update-timer
+ (cancel-timer mu4e--update-timer)
+ (setq mu4e--update-timer nil))
+
+ (setq ;; clear some caches
+ mu4e-maildir-list nil
+ mu4e--contacts-set nil
+ mu4e--contacts-tstamp "0")
+
+ (remove-hook 'mu4e-query-items-updated-hook #'mu4e--main-redraw)
+ (remove-hook 'mu4e-query-items-updated-hook #'mu4e--modeline-update)
+ (remove-hook 'mu4e-query-items-updated-hook #'mu4e--notification)
+ (mu4e-kill-update-mail)
+ (mu4e-modeline-mode -1)
+ (mu4e--server-kill)
+ ;; kill all mu4e buffers
+ (mapc
+ (lambda (buf)
+ ;; the view buffer has the kill-buffer-hook function
+ ;; mu4e--view-kill-mime-handles which kills the mm-* buffers created by
+ ;; Gnus' article mode. Those have been returned by `buffer-list' but might
+ ;; already be deleted in case the view buffer has been killed first. So we
+ ;; need a `buffer-live-p' check here.
+ (when (buffer-live-p buf)
+ (with-current-buffer buf
+ (when (member major-mode
+ '(mu4e-headers-mode mu4e-view-mode mu4e-main-mode))
+ (kill-buffer)))))
+ (buffer-list)))
+\f
+;;; Handlers
+(defun mu4e--default-handler (&rest args)
+ "Dummy handler function with arbitrary ARGS."
+ (mu4e-error "Not handled: %s" args))
+
+(defun mu4e--error-handler (errcode errmsg)
+ "Handler function for showing an error with ERRCODE and ERRMSG."
+ ;; don't use mu4e-error here; it's running in the process filter context
+ (pcase errcode
+ ('4 (mu4e-warn "No matches for this search query."))
+ ('110 (display-warning 'mu4e errmsg :error)) ;; schema version.
+ (_ (mu4e-error "Error %d: %s" errcode errmsg))))
+
+(defun mu4e--update-status (info)
+ "Update the status message with INFO."
+ (setq mu4e-index-update-status
+ `(:tstamp ,(current-time)
+ :checked ,(plist-get info :checked)
+ :updated ,(plist-get info :updated)
+ :cleaned-up ,(plist-get info :cleaned-up))))
+
+(defun mu4e--info-handler (info)
+ "Handler function for (:INFO ...) sexps received from server."
+ (let* ((type (plist-get info :info))
+ (checked (plist-get info :checked))
+ (updated (plist-get info :updated))
+ (cleaned-up (plist-get info :cleaned-up)))
+ (cond
+ ((eq type 'add) t) ;; do nothing
+ ((eq type 'index)
+ (if (eq (plist-get info :status) 'running)
+ (mu4e-index-message
+ "Indexing... checked %d, updated %d" checked updated)
+ (progn ;; i.e. 'complete
+ (mu4e--update-status info)
+ (mu4e-index-message
+ "%s completed; checked %d, updated %d, cleaned-up %d"
+ (if mu4e-index-lazy-check "Lazy indexing" "Indexing")
+ checked updated cleaned-up)
+ ;; index done; grab updated queries
+ (mu4e--query-items-refresh)
+ (run-hooks 'mu4e-index-updated-hook)
+ ;; backward compatibility...
+ (unless (zerop (+ updated cleaned-up))
+ mu4e-message-changed-hook)
+ (unless (and (not (string= mu4e--contacts-tstamp "0"))
+ (zerop (plist-get info :updated)))
+ (mu4e--request-contacts-maybe)
+ (mu4e--server-data 'maildirs)) ;; update maildir list
+ (mu4e--main-redraw))))
+ ((plist-get info :message)
+ (mu4e-index-message "%s" (plist-get info :message))))))
+
+(defun mu4e--init-handlers()
+ "Initialize the server message handlers.
+Only set set them if they were nil before, so overriding has a
+chance."
+ (mu4e-setq-if-nil mu4e-error-func #'mu4e--error-handler)
+ (mu4e-setq-if-nil mu4e-update-func #'mu4e~headers-update-handler)
+ (mu4e-setq-if-nil mu4e-remove-func #'mu4e~headers-remove-handler)
+ (mu4e-setq-if-nil mu4e-view-func #'mu4e~headers-view-handler)
+ (mu4e-setq-if-nil mu4e-headers-append-func #'mu4e~headers-append-handler)
+ (mu4e-setq-if-nil mu4e-found-func #'mu4e~headers-found-handler)
+ (mu4e-setq-if-nil mu4e-erase-func #'mu4e~headers-clear)
+
+ (mu4e-setq-if-nil mu4e-sent-func #'mu4e--default-handler)
+ (mu4e-setq-if-nil mu4e-contacts-func #'mu4e--update-contacts)
+ (mu4e-setq-if-nil mu4e-info-func #'mu4e--info-handler)
+ (mu4e-setq-if-nil mu4e-pong-func #'mu4e--default-handler)
+
+ (mu4e-setq-if-nil mu4e-queries-func #'mu4e--query-items-queries-handler))
+
+;;;
+(provide 'mu4e)
+;;; mu4e.el ends here
--- /dev/null
+\input texinfo.tex @c -*-texinfo-*-
+@documentencoding UTF-8
+@include version.texi
+@c %**start of header
+@setfilename mu4e.info
+@settitle Mu4e @value{VERSION} user manual
+
+@c Use proper quote and backtick for code sections in PDF output
+@c Cf. Texinfo manual 14.2
+@set txicodequoteundirected
+@set txicodequotebacktick
+@c %**end of header
+
+@copying
+Copyright @copyright{} 2012-@value{UPDATED-YEAR} Dirk-Jan C. Binnema
+
+@quotation
+Permission is granted to copy, distribute and/or modify this document
+under the terms of the GNU Free Documentation License, Version 1.3 or
+any later version published by the Free Software Foundation; with no
+Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A
+copy of the license is included in the section entitled ``GNU Free
+Documentation License.''
+@end quotation
+@end copying
+
+@titlepage
+@title @t{Mu4e} --- an e-mail client for GNU Emacs
+@subtitle version @value{VERSION}, @value{UPDATED}
+@author Dirk-Jan C. Binnema
+
+@c The following two commands start the copyright page.
+@page
+@vskip 0pt plus 1filll
+@insertcopying
+@end titlepage
+
+@dircategory Emacs
+@direntry
+* Mu4e: (Mu4e). An email client for GNU Emacs.
+@end direntry
+
+@contents
+
+@ifnottex
+@node Top
+@top mu4e manual for version @value{VERSION}
+@end ifnottex
+
+@iftex
+@node Welcome to mu4e
+@unnumbered Welcome to mu4e
+@end iftex
+
+Welcome to @t{mu4e}!
+
+@t{mu4e} (@t{mu}-for-emacs) is an e-mail client for GNU Emacs version 26.3 or
+newer, built on top of the @uref{https://www.djcbsoftware.nl/code/mu,mu} e-mail
+search engine. @t{mu4e} is optimized for quickly processing large amounts of
+e-mail.
+
+Some of its highlights:
+@itemize
+@item Fully search-based: there are no folders@footnote{that is, instead of
+folders, you use queries that match messages in a particular folder},
+only queries.
+@item Fully documented, with example configurations
+@item User-interface optimized for speed, with quick key strokes for common actions
+@item Support for non-English languages (so ``angstrom'' matches ``Ångström'')
+@item Asynchronous: heavy actions don't block @t{emacs}@footnote{currently,
+the only exception to this is @emph{sending mail}; there are solutions
+for that though --- see the @ref{FAQ}}
+@item Support for cryptography --- signing, encrypting and decrypting
+@item Address auto-completion based on the contacts in your messages
+@item Extendable with your own snippets of elisp
+@end itemize
+
+In this manual, we go through the installation of @t{mu4e}, do some
+basic configuration and explain its daily use. We also show you how you
+can customize @t{mu4e} for your special needs.
+
+At the end of the manual, there are some example configurations, to get
+you up to speed quickly: @ref{Example configurations}. There's also a section
+with answers to frequently asked questions, @ref{FAQ}.
+
+@menu
+* Introduction:: Where to begin
+* Getting started:: Setting things up
+* Main view:: The @t{mu4e} overview
+* Headers view:: Lists of message headers
+* Message view:: Viewing specific messages
+* Composer:: Creating and editing messages
+* Searching:: Some more background on searching/queries`
+* Marking:: Marking messages and performing actions
+* Contexts:: Defining contexts and switching between them
+* Dynamic folders:: Folders that change based on circumstances
+* Actions:: Defining and using custom actions
+* Extending mu4e:: Writing code for @t{mu4e}
+* Integration:: Integrating @t{mu4e} with Emacs facilities
+
+Appendices
+* Other tools:: mu4e and the rest of the world
+* Example configurations:: Some examples to set you up quickly
+* FAQ:: Common questions and answers
+* Tips and Tricks:: Useful tips
+* How it works:: Some notes about the implementation of @t{mu4e}
+* Debugging:: How to debug problems in @t{mu4e}
+* GNU Free Documentation License:: The license of this manual
+
+Indices
+@c * Command Index:: An item for each standard command name.
+@c * Variable Index:: An item for each variable documented in this manual.
+* Concept Index:: Index of @t{mu4e} concepts and other general subjects.
+
+@end menu
+
+@node Introduction
+@chapter Introduction
+
+Let's get started
+@menu
+* Why another e-mail client::Aren't there enough already
+* Other mail clients::Where @t{mu4e} takes its inspiration from
+* What mu4e does not do::Focus on the core-business, delegate the rest
+* Becoming a mu4e user::Joining the club
+@end menu
+
+@node Why another e-mail client
+@section Why another e-mail client?
+
+I (@t{mu4e}'s author) spend a @emph{lot} of time dealing with e-mail,
+both professionally and privately. Having an efficient e-mail client
+is essential. Since none of the existing ones worked the way I wanted,
+I thought about creating my own.
+
+Emacs is an integral part of my workflow, so it made a lot of
+sense to use it for e-mail as well. And as I had already written an
+e-mail search engine (@t{mu}), it seemed only logical to use that as a
+basis.
+
+@node Other mail clients
+@section Other mail clients
+
+Under the hood, @t{mu4e} is fully search-based, similar to programs like
+@uref{https://notmuchmail.org/,notmuch} and
+@uref{https://sup-heliotrope.github.io/,sup}.
+
+However, @t{mu4e}'s user-interface is quite different. @t{mu4e}'s mail handling
+(deleting, moving, etc.)@: is inspired by
+@uref{http://www.gohome.org/wl/,Wanderlust} (another Emacs-based e-mail client),
+@uref{http://www.mutt.org/,mutt} and the @t{dired} file-manager for emacs.
+
+@t{mu4e} keeps all the `state' in your maildirs, so you can easily
+switch between clients, synchronize over @abbr{IMAP}, backup with
+@t{rsync} and so on. The Xapian-database that @t{mu} maintains is
+merely a @emph{cache}; if you delete it, you won't lose any
+information.
+
+@node What mu4e does not do
+@section What @t{mu4e} does not do
+
+There are a number of things that @t{mu4e} does @b{not} do, by design:
+@itemize
+@item @t{mu}/@t{mu4e} do @emph{not} get your e-mail messages from
+a mail server. Nor does it sync-back any changes. Those tasks are delegated to
+other tools, such as @uref{https://www.offlineimap.org/,offlineimap},
+@uref{http://isync.sourceforge.net/,mbsync} or
+@uref{http://www.fetchmail.info/,fetchmail}; As long as the messages end up in a
+maildir, @t{mu4e} and @t{mu} are happy to deal with them.
+@item @t{mu4e} also does @emph{not} implement sending of messages; instead, it depends on
+@ref{(smtpmail) Top}, which is part of Emacs. In addition, @t{mu4e} piggybacks on
+Gnus' message editor.
+@end itemize
+
+Thus, many of the things an e-mail client traditionally needs to do, are
+delegated to other tools. This leaves @t{mu4e} to concentrate on what it does
+best: quickly finding the mails you are looking for, and handle them as
+efficiently as possible.
+
+@node Becoming a mu4e user
+@section Becoming a @t{mu4e} user
+
+If @t{mu4e} sounds like something for you, give it a shot! We're trying
+hard to make it as easy as possible to set up and use; and while you can
+use elisp in various places to augment @t{mu4e}, a lot of knowledge
+about programming or elisp shouldn't be required. The idea is to provide
+sensible defaults, and allow for customization.
+
+When you take @t{mu4e} into use, it's a good idea to subscribe to the
+@uref{https://groups.google.com/group/mu-discuss,mu/mu4e mailing list}.
+
+Sometimes, you might encounter some unexpected behavior while using @t{mu4e}, or
+have some idea on how it could work better. To report this, you can use the
+@uref{https://github.com/djcb/mu/issues,bug-tracker}. Please always include the
+following information:
+
+@itemize
+@item what did you expect or wish to happen? what actually happened?
+@item can you provide some exact steps to reproduce?
+@item what version of @t{mu4e} and @t{emacs} were you using? What operating system?
+@item can you reproduce it with @command{emacs -q} and only loading @t{mu4e}?
+@item if the problem is related to some specific message, please include the raw message file (appropriately anonymized, of course)
+@end itemize
+
+@node Getting started
+@chapter Getting started
+
+In this chapter, we go through the installation of @t{mu4e} and its basic setup.
+After we have succeeded in @ref{Getting mail}, and @pxref{Indexing your
+messages}, we discuss the @ref{Basic configuration}.
+
+After these steps, @t{mu4e} should be ready to go!
+
+@menu
+* Requirements:: What is needed
+* Versions:: Available stable and development versions
+* Installation:: How to install @t{mu} and @t{mu4e}
+* Getting mail:: Getting mail from a server
+* Initializing the message store:: Settings things up
+* Indexing your messages:: Creating and maintaining the index
+* Basic configuration:: Settings for @t{mu4e}
+* Folders:: Setting up standard folders
+* Retrieval and indexing:: Doing it from @t{mu4e}
+* Sending mail:: How to send mail
+* Running mu4e:: Overview of the @t{mu4e} views
+
+@end menu
+
+@node Requirements
+@section Requirements
+
+@t{mu}/@t{mu4e} are known to work on a wide variety of Unix- and Unix-like
+systems, including many Linux distributions, OS X and FreeBSD. Emacs 26.3 or
+higher is required, as well as @uref{https://xapian.org/,Xapian} and
+@uref{http://spruce.sourceforge.net/gmime/,GMime}.
+
+@t{mu} has optional support for the Guile (Scheme) programming language (version
+3.0 or higher). There are also some GUI-toys, which require GTK+ 3.x and Webkit.
+
+If you intend to compile @t{mu} yourself, you need to have the typical
+development tools, such as C and C++17 compilers (both @command{gcc} and
+@command{clang} work), @command{meson} and @command{make}, and the development
+packages for GMime 3.x, GLib and Xapian. Optionally, you also need the
+development packages for GTK+, Webkit and Guile.
+
+@node Versions
+@section Versions
+
+The stable (release) versions have even minor version numbers, while the
+development versions have odd ones. So, for example, 1.10.5 is a stable version,
+while the 1.11.9 is the development version.
+
+The stable versions only receive bug fixes after being released, while the
+development versions get new features, fixes, and, perhaps, bugs, and are meant
+for people with a tolerance for that.
+
+There is support for one release branch; so, when the 1.10 release is available
+(and a new 1.11 development series start), no more changes are expected for the
+1.8 releases.
+
+@node Installation
+@section Installation
+
+@t{mu4e} is part of @t{mu} --- by installing the latter, the former is
+installed as well. Some Linux distributions provide packaged versions of
+@t{mu}/@t{mu4e}; if you can use those, there is no need to compile
+anything yourself. However, if there are no packages for your
+distribution, if they are outdated, or if you want to use the latest
+development versions, you can follow the steps below.
+
+@subsection Dependencies
+
+The first step is to get some build dependencies. The details depend a
+bit on your system's setup / distribution.
+@itemize
+@item On Debian/Ubuntu and derivatives:
+@example
+$ sudo apt-get install git meson libgmime-3.0-dev libxapian-dev emacs
+@end example
+@item On Fedora and related:
+@example
+$ sudo dnf install git meson gmime30-devel xapian-core-devel emacs
+@end example
+@item Otherwise, install the equivalent of the above on your system
+@end itemize
+
+
+@subsection Getting mu
+
+The next step is to get the @t{mu} sources. There are two alternatives:
+@itemize
+@item @emph{Use a stable release} -- download a release from
+@url{https://github.com/djcb/mu/releases}
+@item @emph{Use an experimental development version} -- get it from the repository,
+and @t{git clone https://github.com/djcb/mu.git}
+@end itemize
+
+@subsection Building mu
+
+What all that in place, let's build and install @t{mu} and @t{mu4e}.
+Enter the directory where you unpacked or cloned @t{mu}. Then:
+
+@example
+$ ./configure && make
+$ sudo make install
+@end example
+
+Note: if you are familiar with @t{meson}, you can of course use its
+commands directly; the @t{make} commands are just a thin wrapper around
+that.
+
+@subsection Installation
+
+After this, @t{mu} and @t{mu4e} should be installed @footnote{there's a
+hard dependency between versions of @t{mu4e} and @t{mu} --- you cannot
+combine different versions} on your system, and be available from the
+command line and in Emacs.
+
+You may need to restart Emacs, so it can find @t{mu4e} in its
+@code{load-path}. If, even after restarting, Emacs cannot find @t{mu4e},
+you may need to add it to your @code{load-path} explicitly; check where
+@t{mu4e} is installed, and add something like the following to your
+configuration before trying again:
+@lisp
+;; the exact path may differ --- check it
+(add-to-list 'load-path "/usr/local/share/emacs/site-lisp/mu4e")
+@end lisp
+
+@subsection mu4e and emacs customization
+
+There is some support for using the Emacs customization system in
+@t{mu4e}, but for now, we recommend setting the values manually. Please
+refer to @ref{Example configurations} for a couple of examples of this; here we
+go through things step-by-step.
+
+@node Getting mail
+@section Getting mail
+
+In order for @t{mu} (and, by extension, @t{mu4e}) to work, you need to have your
+e-mail messages stored in a
+@uref{https://en.wikipedia.org/wiki/Maildir, Maildir}; in this manual we use the
+term `maildir' for both the standard and the hierarchy of maildirs that store
+your messages --- a specific directory structure with one-file-per-message.
+
+If you are already using a maildir, you are lucky. If not, some setup
+is required:
+@itemize
+@item @emph{Using an external IMAP or POP server} --- if you are using an
+@abbr{IMAP} or @abbr{POP} server, you can use tools like @t{getmail},
+@t{fetchmail}, @t{offlineimap} or @t{isync} to download your messages
+into a maildir (@file{~/Maildir}, often). Because it is such a common
+case, there is a full example of setting @t{mu4e} up with
+@t{offlineimap} and Gmail; @pxref{Gmail configuration}.
+@item @emph{Using a local mail server} --- if you are using a local mail- server
+(such as @t{postfix} or @t{qmail}), you can teach them to deliver into
+a maildir as well, maybe in combination with @t{procmail}. A bit of
+googling should be able to provide you with the details.
+@end itemize
+
+While a @t{mu} only supports a single Maildir, it can be spread across
+different file-systems; and symbolic links are supported.
+
+@node Initializing the message store
+@section Initializing the message store
+
+The first time you run @t{mu}, you need to initialize its store
+(database). The default location for that is @t{~/.cache/mu/xapian}, but
+you can change this using the @t{--muhome} option, and remember to pass
+that to the other commands as well. Alternatively, you can use an
+environment variable @t{MUHOME}.
+
+Assuming that your maildir is at @file{~/Maildir}, we issue the
+following command:
+@example
+ $ mu init --maildir=~/Maildir
+@end example
+
+You can add some e-mail addresses, so @t{mu} recognizes them as yours:
+
+@example
+ $ mu init --maildir=~/Maildir --my-address=jim@@example.com \
+ --my-address=bob@@example.com
+@end example
+
+@t{mu} remembers the maildir and your addresses and uses them when
+indexing messages. If you want to change them, you need to @t{init}
+once again.
+
+The addresses may also be basic PCRE regular expressions, wrapped in
+slashes, for example:
+
+@example
+ $ mu init --maildir=~/Maildir '--my-address=/foo-.*@@example\.com/'
+@end example
+
+If you want to see the values for your message-store, you can use
+@command{mu info}.
+
+@node Indexing your messages
+@section Indexing your messages
+
+After you have succeeded in @ref{Getting mail} and initialized the
+message database, we need to @emph{index} the messages. That is --- we
+need to scan the messages in the maildir and store the information
+about them in a special database.
+
+We can do that from @t{mu4e} --- @ref{Main view}, but the first time,
+it is a good idea to run it from the command line, which makes it
+easier to verify that everything works correctly.
+
+Assuming that your maildir is at @file{~/Maildir}, we issue the
+following command:
+@example
+ $ mu index
+@end example
+
+This should scan your messages and fill the database, and give
+progress information while doing so.
+
+The indexing process may take a few minutes the first time you do it
+(for thousands of e-mails); afterwards it is much faster, since @t{mu}
+only scans messages that are new or have changed. Indexing is discussed
+in full detail in the @t{mu-index} man-page.
+
+After the indexing process has finished, you can quickly test if
+everything worked, by trying some command-line searches, for example
+@example
+ $ mu find hello
+@end example
+
+which lists all messages that match @t{hello}. For more examples of
+searches, see @ref{Queries}, or check the @t{mu-find} and @t{mu-easy}
+man pages. If all of this worked well, we are well on our way setting
+things up; the next step is to do some basic configuration for @t{mu4e}.
+
+@node Basic configuration
+@section Basic configuration
+
+Before we can start using @t{mu4e}, we need to tell Emacs to load
+it. So, add to your @file{~/.emacs} (or its moral equivalent, such as
+@file{~/.emacs.d/init.el}) something like:
+
+@lisp
+(require 'mu4e)
+@end lisp
+
+If Emacs complains that it cannot find @t{mu4e}, check your
+@code{load-path} and make sure that @t{mu4e}'s installation directory is
+part of it. If not, you can add it:
+
+@lisp
+(add-to-list 'load-path MU4E-PATH)
+@end lisp
+
+with @t{MU4E-PATH} replaced with the actual path.
+
+@node Folders
+@section Folders
+
+The next step is to tell @t{mu4e} where it can find your Maildir, and
+some special folders.
+
+So, for example@footnote{Note that the folders (@t{mu4e-sent-folder},
+@t{mu4e-drafts-folder}, @t{mu4e-trash-folder} and
+@t{mu4e-refile-folder}) can also be @emph{functions} that are evaluated
+at runtime. This allows for dynamically changing them depending on the
+situation. See @ref{Dynamic folders} for details.}:
+@lisp
+;; these are actually the defaults
+(setq
+ mu4e-sent-folder "/sent" ;; folder for sent messages
+ mu4e-drafts-folder "/drafts" ;; unfinished messages
+ mu4e-trash-folder "/trash" ;; trashed messages
+ mu4e-refile-folder "/archive") ;; saved messages
+@end lisp
+
+The folder (maildir) names are all relative to the root-maildir (see the
+output of @command{mu info}). If you use @t{mu4e-context}, see
+@ref{Contexts and special folders} for what that means for these special
+folders.
+
+@node Retrieval and indexing
+@section Retrieval and indexing with mu4e
+@cindex mail retrieval
+@cindex indexing
+As we have seen, we can do all of the mail retrieval @emph{outside} of
+Emacs/@t{mu4e}. However, you can also do it from within
+@t{mu4e}.
+
+@subsection Basics
+
+To set up mail-retrieval from within @t{mu4e}, set the variable
+@code{mu4e-get-mail-command} to the program or shell command you want to
+use for retrieving mail. You can then get your e-mail using @kbd{M-x
+mu4e-update-mail-and-index}, or @kbd{C-S-u} in all @t{mu4e}-views;
+alternatively, you can use @kbd{C-c C-u}, which may be more convenient
+if you use emacs in a terminal.
+
+You can kill the (foreground) update process with @kbd{q}.
+
+It is possible to update your mail and index periodically in the
+background or foreground, by setting the variable
+@code{mu4e-update-interval} to the number of seconds between these
+updates. If set to @code{nil}, it won't update at all. After you make
+changes to @code{mu4e-update-interval}, @t{mu4e} must be restarted
+before the changes take effect. By default, this will run in
+background and to change it to run in foreground, set
+@code{mu4e-index-update-in-background} to @code{nil}.
+
+After updating has completed, @t{mu4e} keeps the output in a buffer
+@t{*mu4e-last-update*}, which you can use for diagnosis if needed.
+
+@subsection Handling errors during mail retrieval
+
+If the mail-retrieval process returns with a non-zero exit code,
+@t{mu4e} shows a warning (unless @code{mu4e-index-update-error-warning}
+is set to @code{nil}), but then try to index your maildirs anyway
+(unless @code{mu4e-index-update-error-continue} is set to @code{nil}).
+
+Reason for these defaults is that some of the mail-retrieval programs
+may return non-zero, even when the updating process succeeded; however,
+it is hard to tell such pseudo-errors from real ones like `login
+failed'.
+
+If you need more refinement, it may be useful to wrap the mail-retrieval
+program in a shell-script, for example @t{fetchmail} returns 1 to
+indicate `no mail'; we can handle that with:
+@lisp
+(setq mu4e-get-mail-command "fetchmail -v || [ $? -eq 1 ]")
+@end lisp
+A similar approach can be used with other mail retrieval programs,
+although not all of them have their exit codes documented.
+
+@subsection Implicit mail retrieval
+
+If you don't have a specific command for getting mail, for example
+because you are running your own mail-server, you can leave
+@code{mu4e-get-mail-command} at @t{"true"} (the default), in which case
+@t{mu4e} won't try to get new mail, but still re-index your messages.
+
+@subsection Speeding up indexing
+
+If you have a large number of e-mail messages in your store,
+(re)indexing might take a while. The defaults for indexing are to
+ensure that we always have correct, up-to-date information about your
+messages, even if other programs have modified the Maildir.
+
+The downside of this thoroughness (which is the default) is that it is
+relatively slow, something that can be noticeable with large e-mail
+corpora on slow file-systems. For a faster approach, you can use the
+following:
+
+@lisp
+(setq
+ mu4e-index-cleanup nil ;; don't do a full cleanup check
+ mu4e-index-lazy-check t) ;; don't consider up-to-date dirs
+@end lisp
+
+In many cases, the mentioned thoroughness might not be needed, and
+these settings give a very significant speed-up. If it does not work
+for you (e.g., @t{mu4e} fails to find some new messages), simply leave
+at the default.
+
+Note that you can occasionally run a thorough indexing round using
+@code{mu4e-update-index-nonlazy}.
+
+For further details, please refer to the @t{mu-index} manpage; in
+particular, see @t{.noindex} and @t{.noupdate} which can help reducing
+the indexing time.
+
+@subsection Example setup
+
+A simple setup could look something like:
+
+@lisp
+(setq
+ mu4e-get-mail-command "offlineimap" ;; or fetchmail, or ...
+ mu4e-update-interval 300) ;; update every 5 minutes
+@end lisp
+
+A hook @code{mu4e-update-pre-hook} is available which is run right
+before starting the process. That can be useful, for example, to
+influence, @code{mu4e-get-mail-command} based on the the current
+situation (location, time of day, ...).
+
+It is possible to get notifications when the indexing process does any
+updates --- for example when receiving new mail. See
+@code{mu4e-index-updated-hook} and some tips on its usage in the
+@ref{FAQ}.
+
+@node Sending mail
+@section Sending mail
+
+@t{mu4e} uses Emacs's @ref{(message) Top,,message-mode} for writing mail.
+
+For sending mail using @abbr{SMTP}, @t{mu4e} uses @ref{(smtpmail)
+Top,,smtpmail}. This package supports many different ways to send mail; please
+refer to its documentation for the details.
+
+Here, we only provide some simple examples --- for more, see @ref{Example
+configurations}.
+
+A very minimal setup:
+
+@lisp
+;; tell message-mode how to send mail
+(setq message-send-mail-function 'smtpmail-send-it)
+;; if our mail server lives at smtp.example.org; if you have a local
+;; mail-server, simply use 'localhost' here.
+(setq smtpmail-smtp-server "smtp.example.org")
+@end lisp
+
+Since @t{mu4e} (re)uses the same @t{message mode} and @t{smtpmail} that
+Gnus uses, many settings for those also apply to @t{mu4e}.
+
+@subsection Dealing with sent messages
+
+By default, @t{mu4e} puts a copy of messages you sent in the folder
+determined by @code{mu4e-sent-folder}. In some cases, this may not be
+what you want - for example, when using Gmail-over-@abbr{IMAP}, this
+interferes with Gmail's handling of the sent messages folder, and you
+may end up with duplicate messages.
+
+You can use the variable @code{mu4e-sent-messages-behavior} to customize
+what happens with sent messages. The default is the symbol @code{sent}
+which, as mentioned, causes the message to be copied to your
+sent-messages folder. Other possible values are the symbols @code{trash}
+(the sent message is moved to the trash-folder
+(@code{mu4e-trash-folder}), and @code{delete} to simply discard the sent
+message altogether (so Gmail can deal with it).
+
+For Gmail-over-@abbr{IMAP}, you could add the following to your
+settings:
+@verbatim
+;; don't save messages to Sent Messages, Gmail/IMAP takes care of this
+(setq mu4e-sent-messages-behavior 'delete)
+@end verbatim
+And that's it! We should now be ready to go.
+
+For more complex needs, @code{mu4e-sent-messages-behavior} can also be
+a parameter-less function that returns one of the mentioned symbols;
+see the built-in documentation for the variable.
+
+@node Running mu4e
+@section Running mu4e
+
+After following the steps in this chapter, we now (hopefully!) have a
+working @t{mu4e} setup. Great! In the next chapters, we walk you
+through the various views in @t{mu4e}.
+
+For your orientation, the diagram below shows how the views relate to each
+other, and the default key-bindings to navigate between them.
+
+@cartouche
+@verbatim
+
+ [C] +--------+ [RFCE]
+ --------> | editor | <--------
+ / +--------+ \
+ / [RFCE]^ \
+/ | \
++-------+ [sjbB]+---------+ [RET] +---------+
+| main | <---> | headers | <----> | message |
++-------+ [q] +---------+ [qbBjs] +---------+
+ [sjbB] ^
+[.] | [q]
+ V
+ +-----+
+ | raw |
+ +-----+
+
+Default bindings
+----------------
+R: Reply s: search .: raw view (toggle)
+F: Forward j: jump-to-maildir q: quit
+C: Compose b: bookmark-search
+E: Edit B: edit bookmark-search
+
+@end verbatim
+@end cartouche
+
+@node Main view
+@chapter The main view
+
+After you have installed @t{mu4e} (@pxref{Getting started}), you can start it
+with @kbd{M-x mu4e}. @t{mu4e} does some checks to ensure everything is set up
+correctly, and then shows you the @t{mu4e} main view. Its major mode is
+@code{mu4e-main-mode}.
+
+@menu
+* Overview: MV Overview. What is the main view
+* Basic actions::What can we do
+* Bookmarks and Maildirs: Bookmarks and Maildirs. Jumping to other places
+* Miscellaneous::Notes
+@end menu
+
+@node MV Overview
+@section Overview
+
+The main view looks something like the following:
+
+@cartouche
+@verbatim
+* mu4e - mu for emacs version x.y.z
+
+ Basics
+
+ * [j]ump to some maildir
+ * enter a [s]earch query
+ * [C]ompose a new message
+
+ Bookmarks
+
+ * [bu] Unread messages 13085(+3)/13085
+ * [bt] Today's messages
+ * [bw] Last 7 days 53(+3)/128
+ * [bp] Messages with images 75/2441
+
+ Maildirs
+
+ * [ja] /archive 2101/18837
+ * [ji] /inbox 8(+2)/10
+ * [jb] /bulk 33/35
+ * [jB] /bulkarchive 179/2090
+ * [jm] /mu 694(+1)/17687
+ * [jn] /sauron
+ * [js] /sent
+
+ Misc
+
+ * [;]Switch context
+ * [U]pdate email & database
+ * toggle [m]ail sending mode (currently direct)
+ * [f]lush 1 queued mail
+
+ * [N]ews
+ * [A]bout mu4e
+ * [H]elp
+ * [q]uit
+
+ Info
+
+ * last-updated : Sat Dec 31 16:43:56 2022
+ * database-path : /home/pam/.cache/mu/xapian
+ * maildir : /home/pam/Maildir
+ * in store : 86179 messages
+ * personal addresses : /.*example.com/, pam@@example.com
+@end verbatim
+@end cartouche
+
+Let's walk through the menu.
+
+@node Basic actions
+@section Basic actions
+
+First, the @emph{Basics}:
+@itemize
+@item @t{[j]ump to some maildir}: after pressing @key{j} (``jump''),
+@t{mu4e} asks you for a maildir to visit. These are the maildirs you
+set in @ref{Basic configuration} and any of your own. If you choose
+@key{o} (``other'') or @key{/}, you can choose from all maildirs under
+the root-maildir. After choosing a maildir, the messages in that
+maildir are listed, in the @ref{Headers view}.
+@item @t{enter a [s]earch query}: after pressing @key{s}, @t{mu4e} asks
+you for a search query, and after entering one, shows the results in the
+@ref{Headers view}.
+@item @t{[C]ompose a new message}: after pressing @key{C}, you are dropped in
+the @ref{Composer} to write a new message.
+@end itemize
+
+@node Bookmarks and Maildirs
+@section Bookmarks and Maildirs
+
+The next two items in the Main view are @emph{Bookmarks} and @emph{Maildirs}.
+
+Bookmarks are predefined queries with a descriptive name and a shortcut. In the
+example above, we see the default bookmarks. You can pick a bookmark by pressing
+@key{b} followed by the specific bookmark's shortcut. If you want to edit the
+bookmarked query before invoking it, use @key{B}.
+
+@cindex baseline
+Next to each bookmark are some numbers that indicate the unread(delta)/all
+matching messages for the given query, with the delta being the difference in
+unread count since some ``baseline'', and only shown when this delta > 0.
+
+Note that the ``delta'' has its limitations: if you, for instance, deleted 5
+messages and received 5 new one, the ``delta'' would be 0, although there were
+changes indeed. So it is mostly useful for tracking changes while you are
+@emph{not} using @t{mu4e}. For this reason, you can reset the baseline manually,
+e.g. by visiting the main view .
+
+By comparing current results with the baseline, you can quickly what new
+messages have arrived since the last time you looked.
+
+The baseline@footnote{For debugging, it can be useful to see the time for the
+baseline - for that, there is the @code{mu4e-baseline-time} command} . is reset
+automatically when switching to the main view, or invoking @code{buffer-revert}
+(@kbd{g}) while in the main-view. Visiting the ``favorite'' bookmark does the
+same(explained below).
+
+Bookmarks are stored in the variable @code{mu4e-bookmarks}; you can add
+your own and/or replace the default ones; @xref{Bookmarks}. For
+instance:
+@lisp
+(add-to-list 'mu4e-bookmarks
+ ;; add bookmark for recent messages on the Mu mailing list.
+ '( :name "Mu7Days"
+ :key ?m
+ :query "list:mu-discuss.googlegroups.com AND date:7d..now"))
+@end lisp
+
+There are optional keys @t{:hide} to hide the bookmark from the main menu, but
+still have it available (using @key{b})) and @t{:hide-unread} to avoid
+generating the unread-number; that can be useful if you have bookmarks for slow
+queries. Note that @t{:hide-unread} is implied when the query is not a string;
+this for the common case where the query function involves some user input,
+which would be disruptive in this case.
+
+There is also the optional @code{:favorite} property, which at most one bookmark
+should have; this bookmark is highlighted in the main view, and its
+unread-status is shown in the modeline; @xref{Modeline}, and you can enable
+desktop notifications; @xref{Desktop notifications}. We'd recommend creating
+such a ``favorite'', which should match message that require your quick
+attention:
+
+@lisp
+(add-to-list 'mu4e-bookmarks
+ ;; bookmark for message that require quick attention
+ '( :name "Urgent"
+ :key ?u
+ :query "maildir:/inbox AND from:boss@@exmaple.com"))
+@end lisp
+
+Note that @t{mu4e} resets the baseline when you are interacting with it (for
+instance, when you visit the urgent bookmark, or when you go to the main view);
+in such cases, there won't be any further notifications.
+
+The @emph{Maildirs} item is very similar to Bookmarks -- consider maildirs here
+as being a special kind of bookmark query that matches a Maildir. You can
+configure this using the variable @code{mu4e-maildir-shortcuts}; see its
+docstring and @ref{Maildir searches} for more details.
+
+@node Miscellaneous
+@section Miscellaneous
+
+Finally, there are some @emph{Misc} (miscellaneous) actions:
+@itemize
+@item @t{[U]pdate email & database} executes the shell-command in the variable
+@code{mu4e-get-mail-command}, and afterwards updates the @t{mu}
+database; see @ref{Indexing your messages} and @ref{Getting mail} for
+details.
+@item @t{[R]eset query-results baseline} this reset the current 'baseline'
+for query and updates the screen; see @ref{Bookmarks and Maildirs}.
+@item @t{toggle [m]ail sending mode (direct)} toggles between sending
+mail directly, and queuing it first (for example, when you are offline),
+and @t{[f]lush queued mail} flushes any queued mail. This item is
+visible only if you have actually set up mail-queuing. @ref{Queuing
+mail}
+@item @t{[A]bout mu4e} provides general information about the program
+@item @t{[H]elp} shows help information for this view
+@item Finally, @t{[q]uit mu4e} quits your @t{mu4e}-session@footnote{@t{mu4e-quit}; or with a @t{C-u}
+prefix argument, it merely buries the buffer}
+@end itemize
+
+@node Headers view
+@chapter The headers view
+
+The headers view shows the results of a query. The header-line shows the names
+of the fields. Below that, there is a line with those fields, for each matching
+message, followed by a footer line. The major-mode for the headers view is
+@code{mu4e-headers-mode}.
+
+@menu
+* Overview: HV Overview. What is the Header View
+* Keybindings::Do things with your keyboard
+* Marking: HV Marking. Selecting messages for doing things
+* Sorting and threading::Influencing how headers are shown
+* Folding threads:: Showing and hiding thread contents
+* Custom headers: HV Custom headers. Adding your own headers
+* Actions: HV Actions. Defining and using actions
+* Buffer display:: How and where the buffers are displayed
+@end menu
+
+@node HV Overview
+@section Overview
+
+An example headers view:
+@cartouche
+@verbatim
+Date V Flgs From/To List Subject
+06:32 Nu To Edmund Dantès GstDev Gstreamer-V4L2SINK ...
+15:08 Nu Abbé Busoni GstDev ├> ...
+18:20 Nu Pierre Morrel GstDev │└> ...
+07:48 Nu To Edmund Dantès GstDev └> ...
+2013-03-18 S Jacopo EmacsUsr emacs server on win...
+2013-03-18 S Mercédès EmacsUsr └> ...
+2013-03-18 S Beachamp EmacsUsr Re: Copying a whole...
+22:07 Nu Albert de Moncerf EmacsUsr └> ...
+2013-03-18 S Gaspard Caderousse GstDev Issue with GESSimpl...
+2013-03-18 Ss Baron Danglars GuileUsr Guile-SDL 0.4.2 ava...
+End of search results
+@end verbatim
+@end cartouche
+
+Some notes to explain what you see in the example:
+
+@itemize
+@item The fields shown in the headers view can be influenced by customizing
+the variable @code{mu4e-headers-fields}; see @code{mu4e-header-info} for
+the list of built-in fields. Apart from the built-in fields, you can
+also create custom fields using @code{mu4e-header-info-custom}; see
+@ref{HV Custom headers} for details.
+@item By default, the date is shown with the @t{:human-date} field, which
+shows the @emph{time} for today's messages, and the @emph{date} for
+older messages. If you do not want to distinguish between `today' and
+`older', you can use the @t{:date} field instead.
+@item You can customize the date and time formats with the variable
+@code{mu4e-headers-date-format} and @code{mu4e-headers-time-format},
+respectively. In the example, we use @code{:human-date}, which shows the
+time when the message was sent today, and the date otherwise.
+@item By default, the subject is shown using the @t{:subject} field;
+however, it is also possible to use @t{:thread-subject}, which shows
+the subject of a thread only once, similar to the display of the
+@t{mutt} e-mail client.
+@item The header field used for sorting is indicated by ``@t{V}'' or
+``@t{^}''@footnote{or you can use little graphical triangles; see
+variable @code{mu4e-use-fancy-chars}}, corresponding to the sort order
+(descending or ascending, respectively). You can influence this by a
+mouse click, or @key{O}. Not all fields allow sorting.
+@item Instead of showing the @t{From:} and @t{To:} fields separately, you
+can use From/To (@t{:from-or-to} in @code{mu4e-headers-fields} as a more
+compact way to convey the most important information: it shows @t{From:}
+@emph{except} when the e-mail was sent by the user (i.e., you) --- in
+that case it shows @t{To:} (prefixed by @t{To}@footnote{You can
+customize this by changing the variable
+@code{mu4e-headers-from-or-to-prefix} (a cons cell)}, as in the example
+above).
+@item The `List' field shows the mailing-list a message is sent to;
+@code{mu4e} tries to create a convenient shortcut for the mailing-list
+name; the variable @code{mu4e-user-mailing-lists} can be used to add
+your own shortcuts. You can use @code{mu4e-mailing-list-patterns} to
+specify generic shortcuts. For instance, to shorten list names to the
+part before @t{-list}, you could use:
+@lisp
+(setq mu4e-mailing-list-patterns '("\\`\\([-_a-z0-9.]+\\)-list"))
+@end lisp
+@item The letters in the `Flags' field correspond to the following: D=@emph{draft},
+F=@emph{flagged} (i.e., `starred'), N=@emph{new}, P=@emph{passed} (i.e.,
+forwarded), R=@emph{replied}, S=@emph{seen}, T=@emph{trashed},
+a=@emph{has-attachment}, x=@emph{encrypted}, s=@emph{signed},
+u=@emph{unread}. The tooltip for this field also contains this information.
+@item The subject field also indicates the discussion threads, following
+@uref{https://www.jwz.org/doc/threading.html,Jamie Zawinski's mail threading
+algorithm}.
+@item The headers view is @emph{automatically updated} if any changes are
+found during the indexing process, and if there is no current
+user-interaction. If you do not want such automatic updates, set
+@code{mu4e-headers-auto-update} to @code{nil}.
+@item Just before executing a search, a hook-function
+@code{mu4e-search-hook} is invoked, which receives the search
+expression as its parameter.
+@item Also, there is a hook-function @code{mu4e-headers-found-hook} available which
+is invoked just after @t{mu4e} has completed showing the messages in the
+headers-view.
+@end itemize
+
+@node Keybindings
+@section Keybindings
+
+Using the below key bindings, you can do various things with these
+messages; these actions are also listed in the @t{Headers} menu in the
+Emacs menu bar.
+
+@verbatim
+key description
+===========================================================
+n,p view the next, previous message
+],[ move to the next, previous unread message
+},{ move to the next, previous thread
+y select the message view (if visible)
+RET open the message at point in the message view
+
+searching
+---------
+s search
+S edit last query
+/ narrow the search
+b search bookmark
+B edit bookmark before search
+c search query with completion
+j jump to maildir
+M-left,\ previous query
+M-right next query
+
+O change sort order
+P toggle search property
+
+marking
+-------
+d mark for moving to the trash folder
+= mark for removing trash flag ('untrash')
+DEL,D mark for complete deletion
+m mark for moving to another maildir folder
+r mark for refiling
++,- mark for flagging/unflagging
+?,! mark message as unread, read
+
+u unmark message at point
+U unmark *all* messages
+
+% mark based on a regular expression
+T,t mark whole thread, subthread
+
+<insert>,* mark for 'something' (decide later)
+# resolve deferred 'something' marks
+
+x execute actions for the marked messages
+
+threads
+-------
+S-left goto root
+TAB toggle threading at current level
+S-TAB toggle all threading
+
+composition
+-----------
+R,W,F,C reply/reply-to-all/forward/compose
+E edit (only allowed for draft messages)
+
+misc
+----
+a execute some custom action on a header
+| pipe message through shell command
+C-+,C-- increase / decrease the number of headers shown
+H get help
+C-S-u update mail & reindex
+C-c C-u update mail & reindex
+q leave the headers buffer
+@end verbatim
+
+Some keybindings are available through minor modes:
+@itemize
+@item Context; see @pxref{Contexts}.
+@item Composition; see @pxref{Composer} and @t{mu4e-compose-minor-mode}
+@end itemize
+
+@node HV Marking
+@section Marking
+
+You can @emph{mark} messages for a certain action, such as deletion or
+move. After one or more messages are marked, you can then execute
+(@code{mu4e-mark-execute-all}, @key{x}) these actions. This two-step
+mark-execute sequence is similar to what e.g. @t{dired} does. It is how
+@t{mu4e} tries to be as quick as possible, while avoiding accidents.
+
+The mark/unmark commands support the @emph{region} (i.e., ``selection'')
+--- so, for example, if you select some messages and press @key{DEL},
+all messages in the region are marked for deletion.
+
+You can mark all messages that match a certain pattern with @key{%}. In
+addition, you can mark all messages in the current thread (@key{T}) or
+sub-thread (@key{t}).
+
+When you do a new search or refresh the headers buffer while you still
+have marked messages, you are asked what to do with those marks ---
+whether to @emph{apply} them before leaving, or @emph{ignore} them. This
+behavior can be influenced with the variable
+@code{mu4e-headers-leave-behavior}.
+
+For more information about marking, see @ref{Marking}.
+
+@node Sorting and threading
+@section Sorting and threading
+
+By default, @t{mu4e} sorts messages by date, in descending order: the
+most recent messages are shown at the top. In addition, be default
+@t{mu4e} shows the message @emph{threads}, i.e., the tree structure
+representing a discussion thread; this also affects the sort order:
+the top-level messages are sorted by the date of the @emph{newest}
+message in the thread.
+
+The header field used for sorting is indicated by ``@t{V}'' or
+``@t{^}''@footnote{or you can use little graphical triangles; see
+variable @code{mu4e-use-fancy-chars}}, indicating the sort order
+(descending or ascending, respectively).
+
+You can change the sort order by clicking the corresponding column with the
+mouse, or with @kbd{M-x mu4e-headers-change-sorting} (@key{O}); note that not
+all fields can be used for sorting. You can toggle threading on/off through
+@kbd{M-x mu4e-headers-toggle-property} or @key{Pt}. For both of these functions,
+unless you provide a prefix argument (@key{C-u}), the current search is updated
+immediately using the new parameters. You can toggle full-search
+(@ref{Searching}) through @kbd{M-x mu4e-headers-toggle-property} as well; or
+@key{Pf}.
+
+Note that with threading enabled, the sorting is exclusively by date,
+regardless of the column clicked.
+
+If you want to change the defaults for these settings, you can use the variables
+@code{mu4e-search-sort-field} and @code{mu4e-search-show-threads}, as well as
+@code{mu4e-search-change-sorting} to change the sorting of the current search
+results.
+
+@node Folding threads
+@section Folding threads
+
+It is possible to fold threads - that is, visually collapse threads into a
+single line (and the reverse), by default using the @key{TAB} and @key{S-TAB}
+bindings. Note that the collapsing is always for threads as a whole, not for
+sub-threads.
+
+Folding stops at the @emph{first unread message}, unless you set
+@code{mu4e-thread-fold-unread}. Similarly, when a thread has marked messages,
+the folding stops at the first marked message. Marking folded messages is not
+allowed as it is too error-prone.
+
+Thread-mode functionality is only available with @code{mu4e-search-threads}
+enabled; this triggers a minor mode @code{mu4e-thread-mode} in the headers-view.
+For now, this functionality is not available in the message view, due to the
+conflicting key bindings.
+
+If you want to automatically fold all threads after a query, you can use a hook:
+@lisp
+ (add-hook 'mu4e-thread-mode-hook #'mu4e-thread-fold-apply-all)
+@end lisp
+
+By default, single-child threads are @emph{not} collapsed, since it would result
+in replacing a single line with the collapsed one. However, if, for consistency,
+you also want to fold those, you can use @t{mu4e-thread-fold-single-children}.
+
+@node HV Custom headers
+@section Custom headers
+
+Sometimes the normal headers that @t{mu4e} offers (Date, From, To,
+Subject, etc.)@: may not be enough. For these cases, @t{mu4e} offers
+@emph{custom headers} in both the headers-view and the message-view.
+
+You can do so by adding a description of your custom header to
+@code{mu4e-header-info-custom}, which is a list of custom headers.
+
+Let's look at an example --- suppose we want to add a custom header that
+shows the number of recipients for a message, i.e., the sum of the
+number of recipients in the @t{To:} and @t{Cc:} fields. Let's further
+suppose that our function takes a message-plist as its argument
+(@ref{Message functions}).
+
+@lisp
+(add-to-list 'mu4e-header-info-custom
+ '(:recipnum .
+ ( :name "Number of recipients" ;; long name, as seen in the message-view
+ :shortname "Recip#" ;; short name, as seen in the headers view
+ :help "Number of recipients for this message" ;; tooltip
+ :function (lambda (msg)
+ (format "%d"
+ (+ (length (mu4e-message-field msg :to))
+ (length (mu4e-message-field msg :cc))))))))
+@end lisp
+
+Or, let's get the contents of the Jabber-ID header.
+
+@lisp
+(add-to-list 'mu4e-header-info-custom
+ '(:jabber-id .
+ ( :name "Jabber-ID" ;; long name, as seen in the message-view
+ :shortname "JID" ;; short name, as seen in the headers view
+ :help "The Jabber ID" ;; tooltip
+ ;; uses mu4e-fetch-field which is rel. slow, so only appropriate
+ ;; for mu4e-view-fields, and _not_ mu4e-headers-fields
+ :function (lambda (msg)
+ (or (mu4e-fetch-field msg "Jabber-ID") "")))))
+@end lisp
+
+You can then add the custom header to your @code{mu4e-headers-fields} or
+@code{mu4e-view-fields}, just like the built-in headers. However, there is an
+important caveat: when your custom header in @code{mu4e-headers-fields}, the
+function is invoked for each of your message headers in search results, and if
+it is slow, would dramatically slow down @t{mu4e}.
+
+@node HV Actions
+@section Actions
+
+@code{mu4e-headers-action} (@key{a}) lets you pick custom actions to perform
+on the message at point. You can specify these actions using the variable
+@code{mu4e-headers-actions}. See @ref{Actions} for the details.
+
+@t{mu4e} defines some default actions. One of those is for @emph{capturing} a
+message: @key{a c} `captures' the current message. Next, when you're editing
+some message, you can include the previously captured message as an
+attachment, using @code{mu4e-compose-attach-captured-message}. See
+@file{mu4e-actions.el} in the @t{mu4e} source distribution for more example
+actions.
+
+@node Buffer display
+@section Buffer display
+
+By default, @t{mu4e} will attempt to manage the display of its own buffers. For
+headers and message views, the variable @code{mu4e-split-view} is @t{mu4e's}
+built-in way to decide how and where they are shown.
+
+@subsection Split view
+You can control how @t{mu4e} displays its buffers, including the @ref{Headers
+view} and the @ref{Message view}, by customizing @code{mu4e-split-view}. There
+are several options available:
+
+@itemize
+@item @t{horizontal} (this is the default): display the message view below the
+header view. Use @code{mu4e-headers-visible-lines} the set the number of
+lines shown (default: 8).
+@item @t{vertical}: display the message view on the
+right side of the header view. Use @code{mu4e-headers-visible-columns} to set
+the number of visible columns (default: 30).
+@item @t{single-window}: single window mode. Single-window mode tries to
+minimize mu4e window operations (opening, killing, resizing, etc) and buffer
+changes, while still retaining the view and headers buffers. In addition, it
+replaces @t{mu4e}'s main view with a minibuffer-prompt containing the same
+information.
+@item anything else: prefer reusing the same window, where possible.
+@end itemize
+
+Note that using a window-returning @emph{function} for @code{mu4e-split-view} is
+no longer supported, instead you can use @code{display-buffer-alist}, see
+the section on further display customization.
+
+@noindent
+Some useful key bindings in the split view:
+@itemize
+@item @key{C-+} and @key{C--}: interactively change the number of columns or
+headers shown
+@item You can change the selected window from the
+headers-view to the message-view and vice-versa with
+@code{mu4e-select-other-view}, bound to @key{y}
+@end itemize
+
+@subsection Further customization
+
+However, @t{mu4e}'s display rules are provisional; you can override them
+easily by customizing @code{display-buffer-alist}, which governs how Emacs --
+and thus @t{mu4e} -- must display your buffers.
+
+Let's look at some examples.
+
+@subsection Fine-tuning the main buffer display
+
+By default @t{mu4e}'s main buffer occupies the complete frame, but this can be
+changed to use the current window:
+
+@lisp
+(add-to-list 'display-buffer-alist
+ `(,(regexp-quote mu4e-main-buffer-name)
+ display-buffer-same-window))
+@end lisp
+
+@subsection Fine-tuning headers buffer display
+
+You do not need to configure @code{mu4e-split-view} for this to work. In the
+absence of explicit rules to the contrary, @t{mu4e} will fall back on the value
+you have set in @code{mu4e-split-view}.
+
+Here is an example that displays the headers buffer in a side window to the
+right. It occupies half of the width of the frame.
+
+@lisp
+(add-to-list 'display-buffer-alist
+ `(,(regexp-quote mu4e-headers-buffer-name)
+ display-buffer-in-side-window
+ (side . right)
+ (window-width . 0.5)))
+@end lisp
+
+You can type @key{C-x w s} to toggle the side windows to hide or show them at
+will.
+
+Note that you may need to customize @code{mu4e-view-rendered-hook} as well; by
+default it contains @code{mu4e-resize-linked-headers-window} but you can set it
+to @code{nil} if you want to handle manually (through
+@code{display-buffer-alist}.
+
+@node Message view
+@chapter The message view
+
+This chapter discusses the message view, the view for reading e-mail messages.
+
+After selecting a message in the @ref{Headers view}, it appears in a
+message view window, which shows the message headers, followed by the
+message body. Its major mode is @code{mu4e-view-mode}, which derives
+from @t{gnus-article-mode}.
+
+@menu
+* Overview: MSGV Overview. What is the Message View
+* Keybindings: MSGV Keybindings. Do things with your keyboard
+* Rich-text and images: MSGV Rich-text and images. Reading rich-text messages
+* Attachments and MIME-parts: MSGV Attachments and MIME-parts. Working with attachments and other MIME parts
+* Custom headers: MSGV Custom headers. Your very own headers
+* Actions: MSGV Actions. Defining and using actions
+* Detaching & reattaching: MSGV Detaching and reattaching. Multiple message views.
+@end menu
+
+@node MSGV Overview
+@section Overview
+
+An example message view:
+
+@cartouche
+@verbatim
+ From: randy@epiphyte.com
+ To: julia@eruditorum.org
+ Subject: Re: some pics
+ Flags: seen, attach
+ Date: Thu, 11 Feb 2021 12:59:30 +0200 (4 weeks, 3 days, 21 hours ago)
+ Maildir: /inbox
+ Attachments: [2. image/jpeg; DSCN4961.JPG]... [3. image/jpeg; DSCN4962.JPG]...
+
+ Hi Julia,
+
+ Some pics from our trip to Cerin Amroth. Enjoy!
+
+ All the best,
+ Randy.
+
+ On Sun 21 Dec 2003 09:06:34 PM EET, Julia wrote:
+
+ [....]
+@end verbatim
+@end cartouche
+
+Some notes:
+@itemize
+@item The variable @code{mu4e-view-fields} determines the header fields to be
+shown; see @code{mu4e-header-info} for a list of built-in fields. Apart
+from the built-in fields, you can also create custom fields using
+@code{mu4e-header-info-custom}; see @ref{MSGV Custom headers}.
+@item For search-related operations, see @ref{Searching}.
+@item You can scroll down the message using @key{SPC}; if you do this at the
+end of a message,it automatically takes you to the next one. If you want
+to prevent this behavior, set @code{mu4e-view-scroll-to-next} to
+@code{nil}.
+@end itemize
+
+@node MSGV Keybindings
+@section Keybindings
+
+You can find most things you can do with this message in the @emph{View} menu,
+or by using the keyboard; the default bindings are:
+
+@verbatim
+key description
+==============================================================
+n,p view the next, previous message
+],[ move to the next, previous unread message
+},{ move to the next, previous thread
+y select the headers view (if visible)
+
+RET scroll down
+M-RET open URL at point / attachment at point
+
+SPC scroll down, if at end, move to next message
+S-SPC scroll up
+
+searching
+---------
+s search
+S edit last query
+/ narrow the search
+b search bookmark
+B edit bookmark before search
+c search query with completion
+j jump to maildir
+
+O change sort order
+P toggle search property
+
+M-left previous query
+M-right next query
+
+marking messages
+----------------
+d mark for moving to the trash folder
+= mark for removing trash flag ('untrash')
+DEL,D mark for complete deletion
+m mark for moving to another maildir folder
+r mark for refiling
++,- mark for flagging/unflagging
+
+u unmark message at point
+U unmark *all* messages
+
+% mark based on a regular expression
+T,t mark whole thread, subthread
+
+<insert>,* mark for 'something' (decide later)
+# resolve deferred 'something' marks
+
+x execute actions for the marked messages
+
+composition
+-----------
+R,W,F,C reply/reply-to-all/forward/compose
+E edit (only allowed for draft messages)
+
+actions
+-------
+g go to (visit) numbered URL (using `browse-url')
+(or: <mouse-2> or M-RET with point on URL)
+C-u g visits multiple URLs
+f fetch (download )the numbered URL.
+C-u f fetches multiple URLs
+k save the numbered URL in the kill-ring.
+C-u k saves multiple URLs
+
+e extract (save) one or more attachments (asks for numbers)
+(or: <mouse-2> or S-RET with point on attachment)
+a execute some custom action on the message
+A execute some custom action on the message's MIME-parts
+
+misc
+----
+z, Z detach (or reattach) a message view to a headers buffer
+. show the raw message view. 'q' takes you back.
+C-+,C-- increase / decrease the number of headers shown
+H get help
+C-S-u update mail & reindex
+q leave the message view
+@end verbatim
+
+Some keybindings are available through minor modes:
+@itemize
+@item Context; see @pxref{Contexts}
+@item Composition; see @pxref{Composer} and @t{mu4e-compose-minor-mode}
+@end itemize
+
+For the marking commands, please refer to @ref{Marking messages}.
+
+@node MSGV Rich-text and images
+@section Reading rich-text messages
+@cindex rich-text
+
+These days, many e-mail messages contain rich-text (typically, HTML);
+either as an alternative to a text-only version, or even as the only
+option.
+
+By default, mu4e tries to display the 'richest' option, which is the
+last MIME-part of the alternatives. You can customize this to prefer
+the text version, if available, with something like the following in
+your configuration (and see the docstring for
+@t{mm-discouraged-alternatives} for details):
+
+@lisp
+(with-eval-after-load "mm-decode"
+ (add-to-list 'mm-discouraged-alternatives "text/html")
+ (add-to-list 'mm-discouraged-alternatives "text/richtext"))
+@end lisp
+
+When displaying rich-text messages inline, @t{mu4e} (through @t{gnus})
+uses the @t{shr} built-in HTML-renderer. If you're using a dark color
+theme, and the messages are hard to read, it can help to change the
+luminosity, e.g.:
+@lisp
+(setq shr-color-visible-luminance-min 80)
+@end lisp
+
+Note that you can switch between the HTML and text versions by
+clicking on the relevant part in the messages headers; you can make it
+even clearer by indicating them in the message itself, using:
+
+@lisp
+(setq gnus-unbuttonized-mime-types nil)
+@end lisp
+
+@subsection Inline images
+When you run Emacs in graphical mode, by default images attached to
+messages are shown inline in the message view buffer.
+
+To disable this, set @code{gnus-inhibit-images} to @t{t}. By default,
+external images in HTML are not retrieved from external URLs because
+they can be used to track you.
+
+Apart from that, you can also control whether to load remote images;
+since loading remote images is often used for privacy violations, by
+default this is not allowed.
+
+You can specify what URLs to block by setting
+@code{gnus-blocked-images} to a regular expression or to a function
+that will receive a single parameter which is not meaningful for
+@t{mu4e}.
+
+For example, to enable images in Github notifications, you could use
+the following:
+
+@lisp
+(setq gnus-blocked-images
+ (lambda(&optional _ignore)
+ (if (mu4e-message-contact-field-matches
+ (mu4e-message-at-point) :from "notifications@@github.com")
+ nil ".")))
+@end lisp
+
+@code{mu4e} inherits the default @t{gnus-blocked-images} from Gnus and
+ensures that it works with @t{mu4e} too. However, mu4e is not Gnus, so
+if you have Gnus-specific settings for @t{gnus-blocked-images}, you
+should verify that they have the desired effect in @code{mu4e} as
+well.
+
+@node MSGV Attachments and MIME-parts
+@section Attachments and MIME-parts
+@cindex attachments
+@cindex mime-parts
+
+E-mail messages can be though as a series of ``MIME-parts'', which are sections
+of the message. The most prominent is the 'body', that is the main message your
+are reading. Many e-mail messages also contains @emph{attachments}, which
+MIME-parts that contain files@footnote{Attachments come in two flavors:
+@c{inline} and @c{attachment}. @t{mu4e} does not distinguish between them when
+operating on them; everything that specifies a filename is considered an
+attachment}.
+
+To save such attachments as files on your file systems, the @t{mu4e}
+message-view offers the command @code{mu4e-view-save-attachments}; default
+keybinding is @key{e} (think @emph{extract}). After invoking the command, you
+can enter the file names to save, comma-separated, and using the completion
+support. Press @key{RET} to save the chosen files to your file-system.
+
+With a prefix argument, you get to choose the target-directory, otherwise,
+@t{mu4e} determines it following the variable @t{mu4e-attachment-dir} (which can
+be file-system path or a function; see its docstring for details.
+
+While completing, @code{mu4e-view-completion-minor-mode} is active, which offers
+@code{mu4e-view-complete-all} (bound to @key{C-c C-a} to complete @emph{all}
+files@footnote{Except when using 'Helm'; in that case, use the Helm-mechanism
+for selecting multiple}.
+
+@subsection MIME-parts
+
+Not all MIME-parts are message bodies or attachments, and it can be useful to
+operate on those other parts as well. For that, there is the function
+@code{mu4e-view-mime-part-action} (default key-binding @key{A}). You can pass
+the number of the MIME-pars (as seen in the message view) as a prefix argument,
+otherwise you get to get to choose from a completion menu.
+
+After choosing one or more MIME-parts, you are asked for an action to apply to
+them; see the variable @code{mu4e-view-mime-part-actions} for the possibilities;
+and you can add your own actions as well, see @ref{MIME-part actions} for some
+example.
+
+@node MSGV Custom headers
+@section Custom headers
+@cindex custom headers
+
+Sometimes the normal headers (Date, From, To, Subject, etc.)@: may not be
+enough. For these cases, @t{mu4e} offers @emph{custom headers} in both the
+headers-view and the message-view.
+
+See @ref{HV Custom headers} for an example of this; the difference for
+the message-view is that you should add your custom header to
+@code{mu4e-view-fields} rather than @code{mu4e-headers-fields}.
+
+@node MSGV Actions
+@section Actions
+
+You can perform custom functions (``actions'') on messages and their
+attachments. For a general discussion on how to define your own, see
+@ref{Actions}.
+
+@subsection Message actions
+@code{mu4e-view-action} (@key{a}) lets you pick some custom action to perform
+on the current message. You can specify these actions using the variable
+@code{mu4e-view-actions}; @t{mu4e} defines a number of example actions.
+
+@subsection MIME-part actions
+MIME-part actions allow you to act upon MIME-parts in a message - such
+as attachments. For now, these actions are defined and documented in
+@code{mu4e-view-mime-part-actions} and see
+
+@node MSGV Detaching and reattaching
+@section Detaching and reattaching messages
+
+You can have multiple message views, but you must rename the view
+buffer and detach it to stop @t{mu4e} from reusing it when you
+navigate up or down in the headers buffer. If you have several view
+buffers attached to a headers view, then @t{mu4e} may pick one at
+random when it has to choose which one to display a message in.
+
+To detach the message view from its linked headers buffer, type
+@key{z}. A message will appear saying it is detached (or warn you if
+it is already detached.)
+
+Detached buffers are static; they cannot change the displayed message,
+and no headers buffer will use a detached buffer to display its
+messages. You can reattach a buffer to an live headers buffer by
+typing @key{Z}.
+
+You can freely rename a message view buffer -- such as with @key{C-x x
+r} -- if you want a custom, non-randomized name.
+
+Detached messages are often useful for workflows involving lots of
+simultaneous messages.
+
+You can @emph{tear off} the window a message is in and place it in a
+new frame by typing @key{C-x w ^ f}. You can also detach a window and
+put it in its own tab with @key{C-x w ^ t}.
+
+@node Composer
+@chapter Composer
+
+Writing e-mail messages takes place in the Composer. @t{mu4e}'s re-uses much of
+Gnus' @t{message-mode}.
+
+Much of the @t{message-mode} functionality is available, as well some
+@t{mu4e}-specifics. See @ref{(message) Top} for details; not every setting is
+necessarily also supported in @t{mu4e}.
+
+The major mode for the composer is @code{mu4e-compose-mode}.
+
+@menu
+* Composer overview: Composer overview. What is the composer good for
+* Entering the composer:: How to start writing messages
+* Keybindings: Composer Keybindings. Doing things with your keyboard
+* Address autocompletion:: Quickly entering known addresses
+* Compose hooks::Calling functions when composing
+* Signing and encrypting:: Support for cryptography
+* Queuing mail:: Sending mail when the time is ripe
+* Message signatures:: Adding your personal footer to messages
+* Other settings::Miscellaneous
+@end menu
+
+@node Composer overview
+@section Overview
+
+@cartouche
+@verbatim
+ From: Rupert the Monkey <rupert@example.com>
+ To: Wally the Walrus <wally@example.com>
+ Subject: Re: Eau-qui d'eau qui?
+ --text follows this line--
+
+ On Mon 16 Jan 2012 10:18:47 AM EET, Wally the Walrus wrote:
+
+ > Hi Rupert,
+ >
+ > Dude - how are things?
+ >
+ > Later -- Wally.
+@end verbatim
+@end cartouche
+
+@node Entering the composer
+@section Entering the composer
+
+There are a view different ways to @emph{enter} the composer; i.e., from other
+@t{mu4e} views or even completely outside.
+
+If you want the composer to start in a new frame or window, you can configure
+the variable @t{mu4e-compose-switch}; see its docstring for details.
+
+@subsection New message
+
+You can start composing a completely new message with @t{mu4e-compose-new} (with
+@kbd{N} from within @t{mu4e}.
+
+@subsection Reply
+
+You can compose a reply to an existing message with @t{mu4e-compose-reply} (with
+@kbd{R} from within the headers view or when looking at some specific message.
+
+When you want to reply to @emph{all} recipients of a message, you can use
+@t{mu4e-compose-wide-reply}, bound to @kbd{W}. This is often called
+``reply-to-all'', while Gnus uses the term ``wide reply''.
+
+By default, the reply will cite the message being replied to. If you do not want
+that, you can set (or @t{let}-bind) @t{message-cite-function} to
+@t{mu4e-message-cite-nothing}.
+
+See @ref{(message) Reply} and @ref{(message) Wide Reply} for further
+information.
+
+Note: in older versions, @t{mu4e-compose-reply} would @emph{ask} whether you
+want to reply-to-all or not; if you are nostalgic for that old behavior, you
+could add something like the following to your configuration:
+@lisp
+(defun compose-reply-wide-or-not-please-ask ()
+ "Ask whether to reply-to-all or not."
+ (interactive)
+ (mu4e-compose-reply (yes-or-no-p "Reply to all?")))
+
+(define-key mu4e-compose-minor-mode-map (kbd "R")
+ #'compose-reply-wide-or-not-please-ask)
+@end lisp
+
+@subsection Forward
+
+You can forward some existing message with @t{mu4e-compose-forward} (with
+@kbd{F} from within the headers view or when looking at some specific message.
+
+For more information, see @ref{(message) Forwarding}.
+
+To influence the way a message is forwarded, you can use the variables
+@code{message-forward-as-mime} and @code{message-forward-show-mml}.
+
+@subsection Supersede
+
+Occasionally, it can be useful to ``supersede'' a message you sent; this drops
+you into a new message that is just like the old message (and a @t{Supersedes:}
+message header). You can then edit this message and send it.
+
+This is only possible for messages @emph{you} sent, as determined by
+@code{mu4e-personal-or-alternative-address-p}.
+
+This wraps @code{message-supersede}.
+
+@subsection Resend
+
+You can re-send some existing message with @t{mu4e-compose-resend} from within
+the headers view or when looking at some specific message.
+
+This re-sends the message without letting you edit it, as per @ref{(message)
+Resending}.
+
+
+@node Composer Keybindings
+@section Keybindings
+
+@t{mu4e}'s composer derives from Gnus' message editor and shares most of
+its keybindings. Here are some of the more useful ones (you can use the menu
+to find more):
+
+@verbatim
+key description
+--- -----------
+C-c C-c send message
+C-c C-d save to drafts and leave
+C-c C-k kill the message buffer (the message remains in the draft folder)
+C-c C-a attach a file (pro-tip: drag & drop works as well in graphical context)
+C-c C-; switch the context
+
+(mu4e-specific)
+C-S-u update mail & re-index
+@end verbatim
+
+@node Address autocompletion
+@section Address autocompletion
+
+@t{mu4e} supports autocompleting addresses when composing e-mail messages.
+@t{mu4e} uses the e-mail addresses from the messages you sent or received as the
+source for this. Address auto-completion is enabled by default; if you want to
+disable it for some reason, set @t{mu4e-compose-complete-addresses} to @t{nil}.
+
+This uses the Emacs machinery for showing and cycling through the candidate
+addresses; it is active when looking at one of the contact fields in the message
+header area.
+
+It is also possible to use @t{mu4e}'s completion elsewhere in @t{emacs}. To
+enable that, a function @t{mu4e-complete-contact} exists, which you can add to
+@t{completion-at-point-functions}, see @ref{(elisp) Completion in Buffers}.
+@t{mu4e} must be running for any completions to be available.
+
+@subsection Limiting the number of addresses
+
+If you have a lot of mail, especially from mailing lists and the like, there
+can be a @emph{lot} of e-mail addresses, many of which may not be very useful
+when auto-completing. For this reason, @t{mu4e} attempts to limit the number
+of e-mail addresses in the completion pool by filtering out the ones that are
+not likely to be relevant. The following variables are available for tuning
+this:
+
+@itemize
+@item @code{mu4e-compose-complete-only-personal} --- when set to @t{t},
+only consider addresses that were seen in @emph{personal} messages ---
+that is, messages in which one of my e-mail addresses was seen in one
+of the address fields. This is to exclude mailing list posts. You can
+define what is considered `my e-mail address' using the
+@t{--my-address} parameter to @t{mu init}.
+
+@item @code{mu4e-compose-complete-only-after} --- only consider e-mail
+addresses last seen after some date. Parameter is a string, parseable by
+@code{org-parse-time-string}. This excludes old e-mail addresses. The
+default is @t{"2010-01-01"}, i.e., only consider e-mail addresses seen
+since the start of 2010.
+@item @code{mu4e-compose-complete-max} -- the maximum number of contacts to use.
+This adds a hard limit to the 2000 (default) contacts; those are sorted by
+recency / frequency etc. so should include the ones you most likely need.
+@item @code{mu4e-contact-process-function} --- a function to rewrite or
+exclude certain addresses.
+@end itemize
+
+@node Compose hooks
+@section Compose hooks
+
+If you want to change some setting, or execute some custom action before
+message composition starts, you can define a @emph{hook function}. @t{mu4e}
+offers two hooks:
+@itemize
+@item @code{mu4e-compose-pre-hook}: this hook is run @emph{before} composition
+starts; if you are composing a @emph{reply}, @emph{forward} a message, or
+@emph{edit} an existing message, the variable
+@code{mu4e-compose-parent-message} points to the message being replied to,
+forwarded or edited, and you can use @code{mu4e-message-field} to get the
+value of various properties (and see @ref{Message functions}).
+@item @code{mu4e-compose-mode-hook}: this hook is run just before composition
+starts, when the whole buffer has already been set up. This is a good place
+for editing-related settings. @code{mu4e-compose-parent-message} (see above)
+is also at your disposal.
+@item @code{mu4e-compose-post-hook}: this hook is run when we're done with
+message compositions. See the docstring for details.
+@end itemize
+
+@noindent
+As mentioned, @code{mu4e-compose-mode-hook} is especially useful for
+editing-related settings:
+
+Let's look at an example:
+@lisp
+(add-hook 'mu4e-compose-mode-hook
+ (defun my-do-compose-stuff ()
+ "My settings for message composition."
+ (set-fill-column 72)
+ (flyspell-mode)))
+@end lisp
+
+The hook is also useful for adding headers or changing headers, since the
+message is fully formed when this hook runs. For example, to add a
+@t{Bcc:}-header, you could add something like the following, using
+@code{message-add-header} from @code{message-mode}.
+
+@lisp
+(add-hook 'mu4e-compose-mode-hook
+ (defun my-add-bcc ()
+ "Add a Bcc: header."
+ (save-excursion (message-add-header "Bcc: me@@example.com\n"))))
+@end lisp
+
+Or to something context-specific:
+
+@lisp
+(add-hook 'mu4e-compose-mode-hook
+ (lambda()
+ (let* ((ctx (mu4e-context-current))
+ (name (if ctx (mu4e-context-name ctx))))
+ (when name
+ (cond
+ ((string= name "account1")
+ (save-excursion (message-add-header "Bcc: account1@@example.com\n")))
+ ((string= name "account2")
+ (save-excursion (message-add-header "Bcc: account2@@example.com\n"))))))))
+@end lisp
+
+@noindent
+For a more general discussion about extending @t{mu4e}, see @ref{Extending
+mu4e}.
+
+@node Signing and encrypting
+@section Signing and encrypting
+
+Signing and encrypting of messages is possible using @ref{(emacs-mime) Top,
+emacs-mime}, most easily accessed through the @t{Attachments}-menu while
+composing a message, or with @kbd{M-x mml-secure-message-encrypt-pgp}, @kbd{M-x
+mml-secure-message-sign-pgp}.
+
+Important note: the messages are encrypted when they are @emph{sent}: this means
+that draft messages are @emph{not} encrypted. So if you are using e.g.
+@t{offlineimap} or @t{mbsync} to synchronize with some remote IMAP-service, make
+sure the drafts folder is @emph{not} in the set of synchronized folders, for
+obvious reasons.
+
+@node Queuing mail
+@section Queuing mail
+
+If you cannot send mail right now, for example because you are
+currently offline, you can @emph{queue} the mail, and send it when you
+have restored your internet connection. You can control this from the
+@ref{Main view}.
+
+To allow for queuing, you need to tell @t{smtpmail} where you want to store
+the queued messages. For example:
+
+@lisp
+(setq smtpmail-queue-mail t ;; start in queuing mode
+ smtpmail-queue-dir "~/Maildir/queue/cur")
+@end lisp
+
+For convenience, we put the queue directory somewhere in our normal
+maildir. If you want to use queued mail, you should create this directory
+before starting @t{mu4e}. The @command{mu mkdir} command may be useful here,
+so for example:
+
+@verbatim
+ $ mu mkdir ~/Maildir/queue
+ $ touch ~/Maildir/queue/.noindex
+@end verbatim
+
+The file created by the @command{touch} command tells @t{mu} to ignore this
+directory for indexing, which makes sense since it contains @t{smtpmail}
+meta-data rather than normal messages; see the @t{mu-mkdir} and @t{mu-index}
+man-pages for details.
+
+@emph{Warning}: when you switch on queued-mode, your messages @emph{won't}
+reach their destination until you switch it off again; so, be careful not to
+do this accidentally!
+
+@node Message signatures
+@section Message signatures
+
+Message signatures are the standard footer blobs in e-mail messages where you
+can put in information you want to include in every message. The text to include
+is set with @code{message-signature} (older @t{mu4e} used
+@code{mu4e-compose-signature}, but that has been obsoleted).
+
+@node Other settings
+@section Other settings
+
+@itemize
+@item If you want use @t{mu4e} as Emacs' default program for sending mail,
+see @ref{Default email client}.
+@item Normally, @t{mu4e} @emph{buries} the message buffer after sending; if you want
+to kill the buffer instead, add something like the following to your
+configuration:
+@lisp
+(setq message-kill-buffer-on-exit t)
+@end lisp
+@item If you want to exclude your own e-mail addresses when ``replying to
+all'', set @code{message-dont-reply-to-names} to
+@code{mu4e-personal-or-alternative-address-p}. In order for this to work
+properly you need to pass your address to @command{mu init --my-address=} at
+database initialization time, and/or use @t{message-alternative-emails}.
+@end itemize
+
+@node Searching
+@chapter Searching
+
+@t{mu4e} is fully search-based: even if you `jump to a folder', you are
+executing a query for messages that happen to have the property of being in a
+certain folder (maildir).
+
+Normally, queries return up to @code{mu4e-headers-results-limit} (default: 500)
+results. That is usually more than enough, and makes things significantly
+faster. Sometimes, however, you may want to show @emph{all} results; you can
+enable this with @kbd{M-x mu4e-headers-toggle-property}, or by customizing the
+variable @code{mu4e-headers-full-search}. This applies to all search commands.
+
+You can also influence the sort order and whether threads are shown or not;
+see @ref{Sorting and threading}.
+
+@menu
+* Queries:: Searching for messages.
+* Bookmarks:: Remembering queries.
+* Maildir searches:: Queries for maildirs.
+* Other search functionality:: Some more tricks.
+@end menu
+
+@node Queries
+@section Queries
+
+@t{mu4e} queries are the same as the ones that @t{mu find}
+understands@footnote{with the caveat that command-line queries are
+subject to the shell's interpretation before @t{mu} sees them}. You can
+consult the @code{mu-query} man page for the details.
+
+Additionally, @t{mu4e} supports @kbd{TAB}-completion for queries. There
+there is completion for all search keywords such as @code{and},
+@code{from:}, or @code{date:} and also for certain values, i.e., the
+possible values for @code{flag:}, @code{prio:}, @code{mime:}, and
+@code{maildir:}.
+
+Let's look at some examples here.
+
+@itemize
+
+@item Get all messages regarding @emph{bananas}:
+@verbatim
+bananas
+@end verbatim
+
+@item Get all messages regarding @emph{bananas} from @emph{John} with an attachment:
+@verbatim
+from:john and flag:attach and bananas
+@end verbatim
+
+@item Get all messages with subject @emph{wombat} in June 2017
+@verbatim
+subject:wombat and date:20170601..20170630
+@end verbatim
+
+@item Get all messages with PDF attachments in the @t{/projects} folder
+@verbatim
+maildir:/projects and mime:application/pdf
+@end verbatim
+
+@item Get all messages about @emph{Rupert} in the @t{/Sent Items} folder. Note that
+maildirs with spaces must be quoted.
+@verbatim
+"maildir:/Sent Items" and rupert
+@end verbatim
+
+@item Get all important messages which are signed:
+@verbatim
+flag:signed and prio:high
+@end verbatim
+
+@item Get all messages from @emph{Jim} without an attachment:
+@verbatim
+from:jim and not flag:attach
+@end verbatim
+
+@item Get all messages with Alice in one of the contacts-fields (@t{to}, @t{from},
+@t{cc}, @t{bcc}):
+@verbatim
+contact:alice
+@end verbatim
+
+@item Get all unread messages where the subject mentions Ångström: (search is
+case-insensitive and accent-insensitive, so this matches Ångström, angstrom,
+aNGstrøM, ...)
+@verbatim
+subject:Ångström and flag:unread
+@end verbatim
+
+@item Get all unread messages between Mar-2012 and Aug-2013 about some bird:
+@verbatim
+date:20120301..20130831 and nightingale and flag:unread
+@end verbatim
+
+@item Get today's messages:
+@verbatim
+date:today..now
+@end verbatim
+
+@item Get all messages we got in the last two weeks regarding @emph{emacs}:
+@verbatim
+date:2w.. and emacs
+@end verbatim
+
+@item Get messages from the @emph{Mu} mailing list:
+@verbatim
+list:mu-discuss.googlegroups.com
+@end verbatim
+
+Note --- in the @ref{Headers view} you may see the `friendly name' for a
+list; however, when searching you need the real name. You can see the
+real name for a mailing list from the friendly name's tool-tip.
+
+@item Get messages with a subject soccer, Socrates, society, ...; note that
+the `*'-wildcard can only appear as a term's rightmost character:
+@verbatim
+subject:soc*
+@end verbatim
+
+@item Get all messages @emph{not} sent to a mailing-list:
+@verbatim
+NOT flag:list
+@end verbatim
+
+@item Get all mails with attachments with filenames starting with @emph{pic}; note
+that the `*' wildcard can only appear as the term's rightmost character:
+@verbatim
+file:pic*
+@end verbatim
+
+@item Get all messages with PDF-attachments:
+@verbatim
+mime:application/pdf
+@end verbatim
+
+Get all messages with image attachments, and note that the `*' wildcard can
+only appear as the term's rightmost character:
+@verbatim
+mime:image/*
+@end verbatim
+
+Get all messages with files that end in @t{.ppt}; this uses the
+regular-expression support, which is powerful but relatively slow:
+@verbatim
+file:/\.ppt$/
+@end verbatim
+
+@end itemize
+
+@node Bookmarks
+@section Bookmarks
+
+If you have queries that you use often, you may want to store them as
+@emph{bookmarks}. Bookmark searches are available in the main view
+(@pxref{Main view}), header view (@pxref{Headers view}), and message
+view (@pxref{Message view}), using (by default) the key @key{b}
+(@kbd{M-x mu4e-search-bookmark}), or @key{B} (@kbd{M-x
+mu4e-search-bookmark-edit}) which lets you edit the bookmark first.
+
+@subsection Setting up bookmarks
+
+@t{mu4e} provides a number of default bookmarks. Their definition may
+be instructive:
+
+@lisp
+(defcustom mu4e-bookmarks
+ '(( :name "Unread messages"
+ :query "flag:unread AND NOT flag:trashed"
+ :key ?u)
+ ( :name "Today's messages"
+ :query "date:today..now"
+ :key ?t)
+ ( :name "Last 7 days"
+ :query "date:7d..now"
+ :hide-unread t
+ :key ?w)
+ ( :name "Messages with images"
+ :query "mime:image/*"
+ :key ?p))
+ "List of pre-defined queries that are shown on the main screen.
+
+Each of the list elements is a plist with at least:
+:name - the name of the query
+:query - the query expression
+:key - the shortcut key.
+
+Optionally, you add the following:
+:hide - if t, bookmark is hidden from the main-view and speedbar.
+:hide-unread - do not show the counts of unread/total number
+ of matches for the query. This can be useful if a bookmark uses
+ a very slow query. :hide-unread is implied from :hide.
+"
+ :type '(repeat (plist))
+ :group 'mu4e)
+@end lisp
+
+You can replace these or add your own items, by putting in your
+configuration (@file{~/.emacs}) something like:
+@lisp
+(add-to-list 'mu4e-bookmarks
+ '( :name "Big messages"
+ :query "size:5M..500M"
+ :key ?b))
+ @end lisp
+
+This prepends your bookmark to the list, and assigns the key @key{b} to it. If
+you want to @emph{append} your bookmark, you can use @code{t} as the third
+argument to @code{add-to-list}.
+
+In the various @t{mu4e} views, pressing @key{b} lists all the bookmarks
+defined in the echo area, with the shortcut key highlighted. So, to invoke the
+bookmark we just defined (to get the list of "Big Messages"), all you need to
+type is @kbd{bb}.
+
+@subsection Lisp expressions or functions as bookmarks
+
+Instead of using strings, it is also possible to use Lisp expressions as
+bookmarks. Either the expression evaluates to a query string or the expression
+is a function taking no argument that returns a query string.
+
+For example, to get all the messages that are at most a week old in your
+inbox:
+
+@lisp
+(add-to-list 'mu4e-bookmarks
+ '( :name "Inbox messages in the last 7 days"
+ :query (lambda () (concat "maildir:/inbox AND date:"
+ (format-time-string "%Y%m%d"
+ (subtract-time (current-time) (days-to-time 7)))))
+ :key ?w) t)
+@end lisp
+
+Another example where the user is prompted how many days old messages should be
+shown:
+
+@lisp
+(defun my/mu4e-bookmark-num-days-old-query (days-old)
+ (interactive (list (read-number "Show days old messages: " 7)))
+ (let ((start-date (subtract-time (current-time) (days-to-time days-old))))
+ (concat "maildir:/inbox AND date:"
+ (format-time-string "%Y%m%d" start-date))))
+
+(add-to-list 'mu4e-bookmarks
+ `(:name "Inbox messages in the last 7 days"
+ :query ,(lambda () (call-interactively 'my/mu4e-bookmark-num-days-old-query))
+ :key ?o) t)
+@end lisp
+
+It is defining a function to make the code more readable.
+
+@subsection Editing bookmarks before searching
+
+There is also @kbd{M-x mu4e-search-bookmark-edit} (key @key{B}), which
+lets you edit the bookmarked query before invoking it. This can be useful if
+you have many similar queries, but need to change some parameter. For example,
+you could have a bookmark @samp{"date:today..now AND "}@footnote{Not a valid
+search query by itself}, which limits any result to today's messages.
+
+@node Maildir searches
+@section Maildir searches
+
+Maildir searches are quite similar to bookmark searches (see @ref{Bookmarks}),
+with the difference being that the target is always a maildir --- maildir
+queries provide a `traditional' folder-like interface to a search-based e-mail
+client. By default, maildir searches are available in the @ref{Main view},
+@ref{Headers view}, and @ref{Message view}, with the key @key{j}
+(@code{mu4e-jump-to-maildir}). If a prefix argument is given, the maildir
+query can be refined before execution.
+
+@subsection Setting up maildir shortcuts
+
+You can search for maildirs like any other message property
+(e.g. with a query like @t{maildir:/myfolder}), but since it is so common,
+@t{mu4e} offers a shortcut for this.
+
+For this to work, you need to set the variable
+@code{mu4e-maildir-shortcuts} to the list of maildirs you want to have
+quick access to, for example:
+
+@lisp
+(setq mu4e-maildir-shortcuts
+ '( (:maildir "/inbox" :key ?i)
+ (:maildir "/archive" :key ?a)
+ (:maildir "/lists" :key ?l)
+ (:maildir "/work" :key ?w)
+ (:maildir "/sent" :key ?s)
+ (:maildir "/lists/project/project_X" :key ?x :name "Project X")))
+@end lisp
+
+This sets @key{i} as a shortcut for the @t{/inbox} folder --- effectively a
+query @t{maildir:/inbox}. There is a special shortcut @key{o} or @key{/} for
+@emph{other} (so don't use those for your own shortcuts!), which allows you to
+choose from @emph{all} maildirs that you have. There is support for
+autocompletion; note that the list of maildirs is determined when @t{mu4e}
+starts; if there are changes in the maildirs while @t{mu4e} is running, you
+need to restart @t{mu4e}. Optionally, you can specify a name to be displayed
+in the main view.
+
+Each of the folder names is relative to your top-level maildir directory; so
+if you keep your mail in @file{~/Maildir}, @file{/inbox} would refer to
+@file{~/Maildir/inbox}. With these shortcuts, you can jump around your
+maildirs (folders) very quickly --- for example, getting to the @t{/lists}
+folder only requires you to type @kbd{jl}, then change to @t{/work} with
+@kbd{jw}.
+
+While in queries you need to quote folder names (maildirs) with spaces in
+them, you should @emph{not} quote them when used in
+@code{mu4e-maildir-shortcuts}, since @t{mu4e} does that automatically for you.
+
+The very same shortcuts are used by @kbd{M-x mu4e-mark-for-move} (default
+shortcut @key{m}); so, for example, if you want to move a message to the
+@t{/archive} folder, you can do so by typing @kbd{ma}.
+
+@node Other search functionality
+@section Other search functionality
+
+@subsection Navigating through search queries
+You can navigate through previous/next queries using
+@code{mu4e-headers-query-prev} and @code{mu4e-headers-query-next}, which are
+bound to @key{M-left} and @key{M-right}, similar to what some web browsers do.
+
+@t{mu4e} tries to be smart and not record duplicate queries. Also, the number
+of queries remembered has a fixed limit, so @t{mu4e} won't use too much
+memory, even if used for a long time. However, if you want to forget
+previous/next queries, you can use @kbd{M-x mu4e-headers-forget-queries}.
+
+@subsection Narrowing search results
+
+It can be useful to narrow existing search results, that is, to add some
+clauses to the current query to match fewer messages.
+
+For example, suppose you're looking at some mailing list, perhaps by
+jumping to a maildir (@kbd{M-x mu4e-headers-jump-to-maildir}, @key{j}) or
+because you followed some bookmark (@kbd{M-x mu4e-search-bookmark},
+@key{b}). Now, you want to narrow things down to only those messages that have
+attachments.
+
+This is when @kbd{M-x mu4e-search-narrow} (@key{/}) comes in handy. It
+asks for an additional search pattern, which is appended to the current search
+query, in effect getting you the subset of the currently shown headers that
+also match this extra search pattern. @key{\} takes you back to the previous
+query, so, effectively `widens' the search. Technically, narrowing the results
+of query @t{x} with expression @t{y} implies doing a search @t{(x) AND (y)}.
+
+Note that messages that were not in your original search results because of
+@code{mu4e-search-results-limit} may show up in the narrowed query.
+
+@subsection Including related messages
+@anchor{Including related messages}
+
+It can be useful to not only show the messages that directly match a certain
+query, but also include messages that are related to these messages. That is,
+messages that belong to the same discussion threads are included in the results,
+just like e.g. Gmail does it. You can enable this behavior by setting
+@code{mu4e-search-include-related} to @code{t}, and you can toggle between
+including/not-including using @key{P} (@code{mu4e-search-toggle-property}).
+
+Be careful though when e.g. deleting ranges of messages from a certain
+folder --- the list may now also include messages from @emph{other}
+folders.
+
+@subsection Skipping duplicates
+@anchor{Skipping duplicates}
+
+Another useful feature is skipping of @emph{duplicate messages}. When you have
+copies of messages, there's usually little value in including more than one in
+search results. A common reason for having multiple copies of messages is the
+combination of Gmail and @t{offlineimap}, since that is the way the labels /
+virtual folders in Gmail are represented. You can enable skipping duplicates by
+setting @code{mu4e-search-skip-duplicates} to @code{t}, and you can toggle
+the value using @key{P} (@code{mu4e-search-toggle-property}).
+
+Note, messages are considered duplicates when they have the same
+@t{Message-Id}.
+
+@node Marking
+@chapter Marking
+
+In @t{mu4e}, the common way to do things with messages is a two-step process -
+first you @emph{mark} them for a certain action, then you @emph{execute}
+(@key{x}) those marks. This is similar to the way @t{dired} operates. Marking
+can happen in both the @ref{Headers view} and the @ref{Message view}.
+
+@menu
+* Marking messages::Selecting message do something with them
+* What to mark for::What can we do with them
+* Executing the marks::Do it
+* Trashing messages::Exceptions for mailboxes like Gmail
+* Leaving the headers buffer::Handling marks automatically when leaving
+* Built-in marking functions::Helper functions for dealing with them
+* Custom mark functions::Define your own mark function
+* Adding a new kind of mark::Adding your own marks
+@end menu
+
+@node Marking messages
+@section Marking messages
+
+There are multiple ways to mark messages:
+@itemize
+@item @emph{message at point}: you can put a mark on the message-at-point in
+either the @ref{Headers view} or @ref{Message view}
+@item @emph{region}: you can put a mark on all messages in the current region
+(selection) in the @ref{Headers view}
+@item @emph{pattern}: you can put a mark on all messages in the @ref{Headers
+view} matching a certain pattern with @kbd{M-x mu4e-headers-mark-pattern}
+(@key{%})
+@item @emph{thread/subthread}: You can put a mark on all the messages in the
+thread/subthread at point with @kbd{M-x mu4e-headers-mark-thread} and @kbd{M-x
+mu4e-headers-mark-subthread}, respectively
+@end itemize
+
+@node What to mark for
+@section What to mark for
+
+@t{mu4e} supports a number of marks:
+
+@cartouche
+@verbatim
+mark for/as | keybinding | description
+-------------+-------------+------------------------------
+'something' | *, <insert> | mark now, decide later
+delete | D, <delete> | delete
+flag | + | mark as 'flagged' ('starred')
+move | m | move to some maildir
+read | ! | mark as read
+refile | r | mark for refiling
+trash | d | move to the trash folder
+untrash | = | remove 'trash' flag
+unflag | - | remove 'flagged' mark
+unmark | u | remove mark at point
+unmark all | U | remove all marks
+unread | ? | marks as unread
+action | a | apply some action
+@end verbatim
+@end cartouche
+
+After marking a message, the left-most columns in the headers view indicate
+the kind of mark. This is informative, but if you mark many (say, thousands)
+messages, this slows things down significantly@footnote{this uses an
+Emacs feature called @emph{overlays}, which are slow when used a lot
+in a buffer}. For this reason, you can disable this by setting
+@code{mu4e-headers-show-target} to @code{nil}.
+
+@t{something} is a special kind of mark; you can use it to mark messages
+for `something', and then decide later what the `something' should
+be@footnote{This kind of `deferred marking' is similar to the facility
+in @t{dired}, @t{midnight commander}
+(@url{https://www.midnight-commander.org/}) and the like, and uses the
+same key binding (@key{insert}).} Later, you can set the actual mark
+using @kbd{M-x mu4e-mark-resolve-deferred-marks}
+(@key{#}). Alternatively, @t{mu4e} will ask you when you try to execute
+the marks (@key{x}).
+
+@node Executing the marks
+@section Executing the marks
+
+After you have marked some messages, you can execute them with @key{x}
+(@kbd{M-x mu4e-mark-execute-all}).
+
+A hook, @code{mu4e-mark-execute-pre-hook}, is available which is run
+right before execution of each mark. The hook is called with two
+arguments, the mark and the message itself.
+
+@node Trashing messages
+@section Trashing messages
+
+For regular mailboxes, trashing works like other marks: when executed,
+the message is flagged as trashed. Depending on your mailbox provider,
+the trash flag is used to automatically move the message to the trash
+folder (@code{mu4e-trash-folder}) for instance.
+
+Some mailboxes behave differently however and they don't interpret the
+trash flag. In cases like Gmail, the message must be @emph{moved} to
+the trash folder and the trash flag must not be used.
+
+@node Leaving the headers buffer
+@section Leaving the headers buffer
+
+When you quit or update a headers buffer that has marked messages (for
+example, by doing a new search), @t{mu4e} asks you what to do with them,
+depending on the value of the variable @code{mu4e-headers-leave-behavior} ---
+see its documentation.
+
+@node Built-in marking functions
+@section Built-in marking functions
+
+Some examples of @t{mu4e}'s built-in marking functions.
+
+@itemize
+@item @emph{Mark the message at point for trashing}: press @key{d}
+@item @emph{Mark all messages in the buffer as unread}: press @kbd{C-x h o}
+@item @emph{Delete the messages in the current thread}: press @kbd{T D}
+@item @emph{Mark messages with a subject matching ``hello'' for flagging}:
+press @kbd{% s hello RET}.
+@end itemize
+
+@node Custom mark functions
+@section Custom mark functions
+
+Sometimes, the built-in functions to mark messages may not be sufficient for
+your needs. For this, @t{mu4e} offers an easy way to define your own custom
+mark functions. You can choose one of the custom marker functions by pressing
+@key{&} in the @ref{Headers view} and @ref{Message view}.
+
+Custom mark functions are to be appended to the list
+@code{mu4e-headers-custom-markers}. Each of the elements of this list
+('markers') is a list with two or three elements:
+@enumerate
+@item The name of the marker --- a short string describing this marker. The
+first character of this string determines its shortcut, so these should be
+unique. If necessary, simply prefix the name with a unique character.
+@item a predicate function, taking two arguments @code{msg} and @code{param}.
+@code{msg} is the message plist (see @ref{Message functions}) and @code{param} is
+a parameter provided by the third of the marker elements (see the next
+item). The predicate function should return non-@t{nil} if the message
+matches.
+@item (optionally) a function that is evaluated once, and the result is passed as a
+parameter to the predicate function. This is useful when user-input is needed.
+@end enumerate
+
+Let's look at an example: suppose we want to match all messages that have more
+than @emph{n} recipients --- we could do this with the following recipe:
+
+@lisp
+(add-to-list 'mu4e-headers-custom-markers
+ '("More than n recipients"
+ (lambda (msg n)
+ (> (+ (length (mu4e-message-field msg :to))
+ (length (mu4e-message-field msg :cc))) n))
+ (lambda ()
+ (read-number "Match messages with more recipients than: "))) t)
+@end lisp
+
+After evaluating this expression, you can use it by pressing @key{&} in
+the headers buffer to select a custom marker function, and then @key{M}
+to choose this particular one (@t{M} because it is the first character
+of the description).
+
+As you can see, it's not very hard to define simple functions to match
+messages. There are more examples in the defaults for
+@code{mu4e-headers-custom-markers}; see @file{mu4e-headers.el} and see
+@ref{Extending mu4e} for general information about writing your own functions.
+
+
+@node Adding a new kind of mark
+@section Adding a new kind of mark
+
+It is possible to configure new marks, by adding elements to the list
+@code{mu4e-marks}. Such an element must have the following form:
+
+@lisp
+(SYMBOL
+ :char STRING
+ :prompt STRING
+ :ask-target (lambda () TARGET)
+ :dyn-target (lambda (TARGET MSG) DYN-TARGET)
+ :show-target (lambda (DYN-TARGET) STRING)
+ :action (lambda (DOCID MSG DYN-TARGET) nil))
+@end lisp
+
+The symbol can be any symbol, except for the symbols @code{unmark} and
+@code{something}, which are reserved. The rest is a plist with the following
+elements:
+
+@itemize
+@item @code{:char} --- the character to display in the headers view.
+@item @code{:prompt} --- the prompt to use when asking for marks
+(used for example when marking a whole thread).
+@item @code{:ask-target} --- a function run once per bulk-operation, and thus suitable for
+querying the user about a target for move-like marks. If @t{nil}, the
+@t{TARGET} passed to @code{:dyn-target} is @t{nil}.
+@item @code{:dyn-target} --- a function run once per message
+(The message is passed as @t{MSG} to the function). This function allows
+to compute a per-message target, for refile-like marks. If @t{nil}, the
+@t{DYN-TARGET} passed to the @code{:action} is the @t{TARGET} obtained as above.
+@item @code{:show-target} --- how to display the target in the headers view.
+If @code{:show-target} is @t{nil} the @t{DYN-TARGET} is shown (and
+@t{DYN-TARGET} must be a string).
+@item @code{:action} --- the action to apply on the message when the mark is executed.
+@end itemize
+
+As an example, suppose we would like to add a mark for tagging messages
+(GMail-style). We can use the following code (after loading @t{mu4e}):
+
+@lisp
+(add-to-list 'mu4e-marks
+ '(tag
+ :char "g"
+ :prompt "gtag"
+ :ask-target (lambda () (read-string "What tag do you want to add? "))
+ :action (lambda (docid msg target)
+ (mu4e-action-retag-message msg (concat "+" target)))))
+@end lisp
+
+Adding elements to @code{mu4e-marks} (as in the example) allows you to use the
+mark in bulk operations (for example when tagging a whole thread); if you also
+want to add a key-binding for the headers view, you can use something like:
+
+@lisp
+(defun my-mu4e-mark-add-tag()
+ "Add a tag to the message at point."
+ (interactive)
+ (mu4e-headers-mark-and-next 'tag))
+
+(define-key mu4e-headers-mode-map (kbd "g") #'my-mu4e-mark-add-tag)
+@end lisp
+
+@node Contexts
+@chapter Contexts
+
+@menu
+* What are contexts::Defining the concept
+* Context policies::How to determine the current context
+* Contexts and special folders::Using context variables to determine them
+* Contexts example::How to define contexts
+@end menu
+
+It can be useful to switch between different sets of settings in
+@t{mu4e}; a typical example is the case where you have different e-mail
+accounts for private and work email, each with their own values for
+folders, e-mail addresses, mailservers and so on.
+
+The @code{mu4e-context} system is a @t{mu4e}-specific mechanism to allow
+for that; users can define different @i{contexts} corresponding with
+groups of setting and either manually switch between them, or let
+@t{mu4e} determine the right context based on some user-provided
+function.
+
+Note that there are a number of existing ways to switch accounts in
+@t{mu4e}, for example using the method described in the @ref{Tips and
+Tricks} section of this manual. Those still work --- but the new mechanism
+has the benefit of being a core part of @code{mu4e}, thus allowing for
+deeper integration.
+
+@node What are contexts
+@section What are contexts
+
+Let's see what's contained in a context. Most of it is optional.
+
+A @code{mu4e-context} is Lisp object with the following members:
+@itemize
+@item @t{name}: the name of the context, e.g. @t{work} or @t{private}
+@item @t{vars}:
+an association-list (alist) of variable settings for this account.
+@item @t{enter-func}:
+an (optional) function that takes no parameter and is invoked when entering
+the context. You can use this for extra setup etc.
+@item @t{leave-func}:
+an (optional) function that takes no parameter and is invoked when leaving
+the context. You can use this for clearing things up.
+@item @t{match-func}:
+an (optional) function that takes an @t{MSG} message plist as argument,
+and returns non-@t{nil} if this context matches the situation. @t{mu4e}
+uses the first context that matches, in a couple of situations:
+@itemize
+@item when starting @t{mu4e} to determine the
+starting context; in this case, @t{MSG} is nil. You can use e.g. the
+host you're running or the time of day to determine which context
+matches.
+@item before replying to or forwarding a
+message with the given message plist as parameter, or @t{nil} when
+composing a brand new message. The function should return @t{t} when
+this context is the right one for this message, or @t{nil} otherwise.
+@item when determining the target folders for deleting, refiling etc;
+see @ref{Contexts and special folders}.
+@end itemize
+@end itemize
+
+@t{mu4e} uses a variable @code{mu4e-contexts}, which is a list of such
+objects.
+
+@node Context policies
+@section Context policies
+
+When you have defined contexts and you start @t{mu4e} it decides which
+context to use based on the variable @code{mu4e-context-policy};
+similarly, when you compose a new message, the context is determined
+using @code{mu4e-compose-context-policy}.
+
+For both of these, you can choose one of the following policies:
+@itemize
+@item a symbol @code{always-ask}: unconditionally ask the user what context to pick.
+@end itemize
+
+The other choices @b{only apply if none of the contexts match} (i.e.,
+none of the contexts' match-functions returns @code{t}). We have the
+following options:
+
+@itemize
+@item a symbol @code{ask}: ask the user if @t{mu4e} can't figure
+things out the context by itself (through the match-function). This is a
+good policy if there are no match functions, or if the match functions
+don't cover all cases.
+@item a symbol @code{ask-if-none}: if there's already a context, don't change it;
+otherwise, ask the user.
+@item a symbol @code{pick-first}: pick the first (default) context. This is a
+good choice if
+you want to specify context for special case, and fall back to the first
+one if none match.
+@item @code{nil}: don't change the context; this is useful if you don't change
+contexts very often, and e.g. manually changes contexts with @kbd{M-x
+mu4e-context-switch}.
+@end itemize
+
+You can easily switch contexts manually using the @kbd{;} key from
+the main screen.
+
+@node Contexts and special folders
+@section Contexts and special folders
+
+As we discussed in @ref{Folders} and @ref{Dynamic folders}, @t{mu4e}
+recognizes a number of special folders: @code{mu4e-sent-folder},
+@code{mu4e-drafts-folder}, @code{mu4e-trash-folder} and
+@code{mu4e-refile-folder}.
+
+When you have a headers-buffer with messages that belong to different
+contexts (say, a few different accounts), it is desirable for each of
+them to use the specific folders for their own context --- so, for
+instance, if you trash a message, it needs to go to the trash-folder for
+the account it belongs to, which is not necessarily the current context.
+
+To make this easy to do, whenever @t{mu4e} needs to know the value for
+such a special folder for a given message, it tries to determine the
+appropriate context using @code{mu4e-context-determine} (and policy
+@t{nil}; see @ref{Context policies}). If it finds a matching context, it
+let-binds the @code{vars} for that account, and then determines the
+value for the folder. It does not, however, call the @code{enter-func}
+or @code{leave-func}, since we are not really switching contexts.
+
+In practice, this means that as long as each of the accounts has a good
+@t{match-func}, all message operations automatically find the
+appropriate folders.
+
+@node Contexts example
+@section Example
+
+Let's explain how contexts work by looking at an example. We define two
+contexts, `Private' and `Work' for a fictional user @emph{Alice
+Derleth}.
+
+Note that in this case, we automatically switch to the first context
+when starting; see the discussion in the previous section.
+
+@lisp
+
+ (setq mu4e-contexts
+ `( ,(make-mu4e-context
+ :name "Private"
+ :enter-func (lambda () (mu4e-message "Entering Private context"))
+ :leave-func (lambda () (mu4e-message "Leaving Private context"))
+ ;; we match based on the contact-fields of the message
+ :match-func (lambda (msg)
+ (when msg
+ (mu4e-message-contact-field-matches msg
+ :to "aliced@@home.example.com")))
+ :vars '( ( user-mail-address . "aliced@@home.example.com" )
+ ( user-full-name . "Alice Derleth" )
+ ( message-user-organization . "Homebase" )
+ ( message-signature .
+ (concat
+ "Alice Derleth\n"
+ "Lauttasaari, Finland\n"))))
+ ,(make-mu4e-context
+ :name "Work"
+ :enter-func (lambda () (mu4e-message "Switch to the Work context"))
+ ;; no leave-func
+ ;; we match based on the maildir of the message
+ ;; this matches maildir /Arkham and its sub-directories
+ :match-func (lambda (msg)
+ (when msg
+ (string-match-p "^/Arkham" (mu4e-message-field msg :maildir))))
+ :vars '( ( user-mail-address . "aderleth@@miskatonic.example.com" )
+ ( user-full-name . "Alice Derleth" )
+ ( message-user-organization . "Miskatonic University" )
+ ( message-signature .
+ (concat
+ "Prof. Alice Derleth\n"
+ "Miskatonic University, Dept. of Occult Sciences\n"))))
+
+ ,(make-mu4e-context
+ :name "Cycling"
+ :enter-func (lambda () (mu4e-message "Switch to the Cycling context"))
+ ;; no leave-func
+ ;; we match based on the maildir of the message; assume all
+ ;; cycling-related messages go into the /cycling maildir
+ :match-func (lambda (msg)
+ (when msg
+ (string= (mu4e-message-field msg :maildir) "/cycling")))
+ :vars '( ( user-mail-address . "aderleth@@example.com" )
+ ( user-full-name . "AliceD" )
+ ( message-signature . nil)))))
+
+ ;; set `mu4e-context-policy` and `mu4e-compose-policy` to tweak when mu4e should
+ ;; guess or ask the correct context, e.g.
+
+ ;; start with the first (default) context;
+ ;; default is to ask-if-none (ask when there's no context yet, and none match)
+ ;; (setq mu4e-context-policy 'pick-first)
+
+ ;; compose with the current context is no context matches;
+ ;; default is to ask
+ ;; (setq mu4e-compose-context-policy nil)
+@end lisp
+
+A couple of notes about this example:
+@itemize
+@item You can manually switch the context use @code{M-x mu4e-context-switch},
+by default bound to @kbd{;} in headers, view and main mode. The current context
+appears in the modeline by default; see @ref{Modeline} for details.
+@item Normally, @code{M-x mu4e-context-switch} does not call the enter or
+leave functions if the 'new' context is the same as the old one.
+However, with a prefix-argument (@kbd{C-u}), you can force @t{mu4e} to
+invoke those function even in that case.
+@item The function @code{mu4e-context-current} returns the current-context;
+the current context is also visible in the mode-line when in
+headers, view or main mode.
+@item You can set any kind of variable; including settings for mail servers etc.
+However, settings such as @code{mu4e-mu-home} are not changeable after
+they have been set without quitting @t{mu4e} first.
+@item @code{leave-func} (if defined) for the context we are leaving, is invoked
+before the @code{enter-func} (if defined) of the
+context we are entering.
+@item @code{enter-func} (if defined) is invoked before setting the variables.
+@item @code{match-func} (if defined) is invoked just before @code{mu4e-compose-pre-hook}.
+@item See the variables @code{mu4e-context-policy} and
+@code{mu4e-compose-context-policy} to tweak what @t{mu4e} should do when
+no context matches (or if you always want to be asked).
+@item Finally, be careful to get the quotations right --- backticks, single quotes
+and commas and note the '.' between variable name and its value.
+@end itemize
+
+@node Dynamic folders
+@chapter Dynamic folders
+
+In @ref{Folders}, we explained how you can set up @t{mu4e}'s special
+folders:
+@lisp
+(setq
+ mu4e-sent-folder "/sent" ;; sent messages
+ mu4e-drafts-folder "/drafts" ;; unfinished messages
+ mu4e-trash-folder "/trash" ;; trashed messages
+ mu4e-refile-folder "/archive") ;; saved messages
+@end lisp
+
+In some cases, having such static folders may not suffice --- perhaps you want
+to change the folders depending on the context. For example, the folder for
+refiling could vary, based on the sender of the message.
+
+To make this possible, instead of setting the standard folders to a string,
+you can set them to be a @emph{function} that takes a message as its
+parameter, and returns the desired folder name. This chapter shows you how to
+do that. For a more general discussion of how to extend @t{mu4e} and writing
+your own functions, see @ref{Extending mu4e}.
+
+If you use @t{mu4e-context}, see @ref{Contexts and special folders} for
+what that means for these special folders.
+
+@menu
+* Smart refiling:: Automatically choose the target folder
+* Other dynamic folders:: Flexible folders for sent, trash, drafts
+@end menu
+
+
+@node Smart refiling
+@section Smart refiling
+
+When refiling messages, perhaps to archive them, it can be useful to have
+different target folders for different messages, based on some property of
+those message --- smart refiling.
+
+To accomplish this, we can set the refiling folder (@code{mu4e-refile-folder})
+to a function that returns the actual refiling folder for the particular
+message. An example should clarify this:
+
+@lisp
+(setq mu4e-refile-folder
+ (lambda (msg)
+ (cond
+ ;; messages to the mu mailing list go to the /mu folder
+ ((mu4e-message-contact-field-matches msg :to
+ "mu-discuss@@googlegroups.com")
+ "/mu")
+ ;; messages sent directly to some specific address me go to /private
+ ((mu4e-message-contact-field-matches msg :to "me@@example.com")
+ "/private")
+ ;; messages with football or soccer in the subject go to /football
+ ((string-match "football\\|soccer"
+ (mu4e-message-field msg :subject))
+ "/football")
+ ;; messages sent by me go to the sent folder
+ ((mu4e-message-sent-by-me msg
+ (mu4e-personal-addresses))
+ mu4e-sent-folder)
+ ;; everything else goes to /archive
+ ;; important to have a catch-all at the end!
+ (t "/archive"))))
+@end lisp
+
+@noindent
+This can be very powerful; you can select some messages in the headers view,
+then press @key{r}, and have them all marked for refiling to their particular
+folders.
+
+Some notes:
+@itemize
+@item We set @code{mu4e-refile-folder} to an anonymous (@t{lambda}) function. This
+function takes one argument, a message plist@footnote{a property list
+describing a message}. The plist corresponds to the message at point. See
+@ref{Message functions} for a discussion on how to deal with them.
+@item In our function, we use a @t{cond} control structure; the function
+returns the first of the clauses that matches. It's important to make the last
+clause a catch-all, so we always return @emph{some} folder.
+@item We use
+the convenience function @code{mu4e-message-contact-field-matches},
+which evaluates to @code{t} if any of the names or e-mail addresses in a
+contact field (in this case, the @t{To:}-field) matches the regular
+expression. With @t{mu4e} version 0.9.16 or newer, the contact field can
+in fact be a list instead of a single value, such as @code{'(:to :cc)'}.
+@end itemize
+
+@node Other dynamic folders
+@section Other dynamic folders
+
+Using the same mechanism, you can create dynamic sent-, trash-, and
+drafts-folders. The message-parameter you receive for the sent and drafts
+folder is the @emph{original} message, that is, the message you reply to, or
+forward, or edit. If there is no such message (for example when composing a
+brand new message) the message parameter is @t{nil}.
+
+Let's look at an example. Suppose you want a different trash folder for
+work-email. You can achieve this with something like:
+
+@lisp
+(setq mu4e-trash-folder
+(lambda (msg)
+;; the 'and msg' is to handle the case where msg is nil
+(if (and msg
+(mu4e-message-contact-field-matches msg :to "me@@work.example.com"))
+"/trash-work"
+"/trash")))
+@end lisp
+
+@noindent
+Good to remember:
+@itemize
+@item The @code{msg} parameter you receive in the function refers to the
+@emph{original message}, that is, the message being replied to or forwarded.
+When re-editing a message, it refers to the message being edited. When you
+compose a totally new message, the @code{msg} parameter is @code{nil}.
+@item When re-editing messages, the value of @code{mu4e-drafts-folder} is ignored.
+@end itemize
+
+
+@node Actions
+@chapter Actions
+
+@t{mu4e} lets you define custom actions for messages in @ref{Headers view}
+and for both messages and attachments in @ref{Message view}. Custom
+actions allow you to easily extend @t{mu4e} for specific needs --- for example,
+marking messages as spam in a spam filter or applying an attachment with a
+source code patch.
+
+You can invoke the actions with key @key{a} for actions on messages, and key
+@key{A} for actions on attachments.
+
+For general information extending @t{mu4e} and writing your own functions, see
+@ref{Extending mu4e}.
+
+@menu
+* Defining actions::How to create an action
+* Headers view actions::Doing things with message headers
+* Message view actions::Doing things with messages
+* MIME-part actions::Doing things with MIME-parts such as attachments
+* Example actions::Some more examples
+@end menu
+
+@node Defining actions
+@section Defining actions
+
+Defining a new custom action comes down to writing an elisp-function to do the
+work. Functions that operate on messages receive a @var{msg} parameter, which
+corresponds to the message at point. Something like:
+@lisp
+(defun my-action-func (msg)
+ "Describe my message function."
+ ;; do stuff
+ )
+@end lisp
+
+@noindent
+Functions that operate on attachments receive a @var{msg} parameter, which
+corresponds to the message at point, and an @var{attachment-num}, which is the
+number of the attachment as seen in the message view. An attachment function
+looks like:
+@lisp
+(defun my-attachment-action-func (msg attachment-num)
+ "Describe my attachment function."
+ ;; do stuff
+ )
+@end lisp
+
+@noindent
+After you have defined your function, you can add it to the list of
+actions@footnote{Instead of defining the functions separately, you can
+obviously also add a @code{lambda}-function directly to the list; however,
+separate functions are easier to change}, either @code{mu4e-headers-actions},
+@code{mu4e-view-actions} or @code{mu4e-view-mime-part-actions}. The
+format@footnote{Note, the format of the actions has changed since version
+0.9.8.4, and you must change your configuration to use the new format;
+@t{mu4e} warns you when you are using the old format.} of each action is a
+cons-cell, @code{(DESCRIPTION . VALUE)}; see below for some examples. If your
+shortcut is not also the first character of the description, simply prefix the
+description with that character.
+
+Let's look at some examples.
+
+@node Headers view actions
+@section Headers view actions
+
+Suppose we want to inspect the number of recipients for a message in the
+@ref{Headers view}. We add the following to our configuration:
+
+@lisp
+(defun show-number-of-recipients (msg)
+ "Display the number of recipients for the message at point."
+ (message "Number of recipients: %d"
+ (+ (length (mu4e-message-field msg :to))
+ (length (mu4e-message-field msg :cc)))))
+
+;; define 'N' (the first letter of the description) as the shortcut
+;; the 't' argument to add-to-list puts it at the end of the list
+(add-to-list 'mu4e-headers-actions
+ '("Number of recipients" . show-number-of-recipients) t)
+@end lisp
+
+After evaluating this, @kbd{a N} in the headers view shows the number of
+recipients for the message at point.
+
+@node Message view actions
+@section Message view actions
+
+As another example, suppose we would like to search for messages by the
+sender of the message at point:
+
+@lisp
+(defun search-for-sender (msg)
+ "Search for messages sent by the sender of the message at point."
+ (mu4e-search
+ (concat "from:"
+ (mu4e-contact-email (car (mu4e-message-field msg :from))))))
+
+;; define 'x' as the shortcut
+(add-to-list 'mu4e-view-actions
+ '("xsearch for sender" . search-for-sender) t)
+@end lisp
+
+@indent
+If you wonder why we use @code{car}, remember that the @t{From:}-field
+is a list of @code{(:name NAME :email EMAIL)} plists; so this code gets
+us the e-mail address of the first in the list. @t{From:}-fields rarely
+have more that one address.
+
+@node MIME-part actions
+@section MIME-part actions
+
+Finally, let's define a MIME-part action.
+
+The following example action counts the number of lines in an attachment, and
+defines @key{n} as its shortcut key (the @key{n} is prefixed to the
+description). See the the @code{mu4e-view-mime-part-actions} for the details of
+the format.
+
+@lisp
+(add-to-list 'mu4e-view-mime-part-actions
+ ;; count the number of lines in a MIME-part
+ '(:name "line-count" :handler "wc -l" :receives pipe))
+@end lisp
+
+Or another one, to import a calendar invitation into the venerable emacs diary:
+@lisp
+(add-to-list 'mu4e-view-mime-part-actions
+ ;; import into calendar;
+ '(:name "dimport-in-diary" :handler (lambda(file) (icalendar-import-file file diary-file))
+ :receives temp))
+@end lisp
+
+@node Example actions
+@section Example actions
+
+@t{mu4e} includes a number of example actions in the file
+@file{mu4e-actions.el} in the source distribution (see @kbd{C-h f
+mu4e-action-TAB}). For example, for viewing messages in an external web
+browser.
+
+@node Extending mu4e
+@chapter Extending mu4e
+
+@t{mu4e} is designed to be easily extensible --- that is, write your own
+emacs-lisp to make @t{mu4e} behave exactly as you want. Here, we provide some
+guidelines for doing so.
+
+@menu
+* Extension points::Where to hook into @t{mu4e}
+* Available functions::General helper functions
+* Message functions::Working with messages
+* Contact functions::Working with contacts
+* Utility functions::Miscellaneous helpers
+@end menu
+
+@node Extension points
+@section Extension points
+
+There are a number of places where @t{mu4e} lets you plug in your own
+functions:
+@itemize
+@item Custom functions for message header --- see @ref{HV Custom headers}
+@item Using message-specific folders for drafts, trash, sent messages and
+refiling, based on a function --- see @ref{Dynamic folders}
+@item Using an attachment-specific download-directory --- see the
+variable @code{mu4e-attachment-dir}.
+@item Apply a function to a message in the headers view -
+see @ref{Headers view actions}
+@item Apply a function to a message in the message view ---
+see @ref{Message view actions}
+@item Add a new kind of mark for use in the headers view
+- see @ref{Adding a new kind of mark}
+@item Apply a function to a MIME-part --- see @ref{MIME-part actions}
+@item Custom function to mark certain messages ---
+see @ref{Custom mark functions}
+@item Using various @emph{mode}-hooks, @code{mu4e-compose-pre-hook} (see
+@ref{Compose hooks}), @code{mu4e-index-updated-hook} (see @ref{FAQ})
+@end itemize
+
+@noindent
+You can also write your own functions without using the above. If you
+want to do so, key useful functions are @code{mu4e-message-at-point}
+(see below), @code{mu4e-headers-for-each} (to iterate over all
+headers, see its docstring) and @code{mu4e-view-for-each-part} (to
+iterate over all parts/attachments, see its docstring). There is also
+@code{mu4e-view-for-each-uri} to iterate of all the URIs in the
+current message.
+
+Another useful function is
+@code{mu4e-headers-find-if} which searches for a message matching a
+certain pattern; again, see its docstring.
+
+@node Available functions
+@section Available functions
+
+The whole of @t{mu4e} consists of hundreds of elisp functions. However,
+the majority of those are for @emph{internal} use only; you can
+recognize them easily, because they all start with @code{mu4e~} or
+@code{mu4e--}. These functions make all kinds of assumptions, and they
+are subject to change, and should therefore @emph{not} be used. The same
+is true for @emph{variables} with the same prefix; don't touch them. Let
+me repeat that:
+@verbatim
+Do not use mu4e~... or mu4e-- functions or variables!
+@end verbatim
+
+@noindent
+In addition, you should use functions in the right context; functions
+that start with @t{mu4e-view-} are only applicable to the message view,
+while functions starting with @t{mu4e-headers-} are only applicable to
+the headers view. Functions without such prefixes are applicable
+everywhere.
+
+@node Message functions
+@section Message functions
+
+Many functions in @t{mu4e} deal with message plists (property
+lists). They contain information about messages, such as sender and
+recipient, subject, date and so on. To deal with these plists, there are
+a number of @code{mu4e-message-} functions (in @file{mu4e-message.el}),
+such as @code{mu4e-message-field} and @code{mu4e-message-at-point}, and
+a shortcut to combine the two, @code{mu4e-message-field-at-point}.
+
+For example, to get the subject of the message at point, in either the headers
+view or the message view, you could write:
+@lisp
+(mu4e-message-field (mu4e-message-at-point) :subject)
+@end lisp
+@noindent
+Note that:
+@itemize
+@item The contact fields (To, From, Cc, Bcc) are lists of cons-pairs
+@code{(name . email)}; @code{name} may be @code{nil}. So, for example:
+@lisp
+ (mu4e-message-field some-msg :to)
+ ;; => (("Jack" . "jack@@example.com") (nil . "foo@@example.com"))
+@end lisp
+
+If you are only looking for a match in this list (e.g., ``Is Jack one of the
+recipients of the message?''), there is a convenience function
+@code{mu4e-message-contact-field-matches} to make this easy.
+@item The message body is only available in the message view, not in the
+headers view.
+@end itemize
+
+Note that in headers-mode, you only have access to a reduced message
+plist, without the information about the message-body or mime-parts;
+@t{mu4e} does this for performance reasons. And even in view-mode, you
+do not have access to arbitrary message-headers.
+
+However, it is possible to get the information indirectly, using the
+raw-message and some third-party tool like @t{procmail}'s @t{formail}:
+
+@lisp
+(defun my-mu4e-any-message-field-at-point (hdr)
+ "Quick & dirty way to get an arbitrary header HDR at
+point. Requires the 'formail' tool from procmail."
+ (replace-regexp-in-string "\n$" ""
+ (shell-command-to-string
+ (concat "formail -x " hdr " -c < "
+ (shell-quote-argument (mu4e-message-field-at-point :path))))))
+@end lisp
+
+@node Contact functions
+@section Contact functions
+
+It can sometimes be useful to discard or rewrite the contact information
+that @t{mu4e} provides, for example to fix spelling errors, or omit
+unwanted contacts.
+
+To handle this, @t{mu4e} provides @code{mu4e-contact-process-function},
+which, if defined, is applied to each contact. If the result is @t{nil},
+the contact is discarded, otherwise the (modified or not) contact
+information is used.
+
+Each contact is a full e-mail address as you would see in a
+contact-field of an e-mail message, e.g.,
+@verbatim
+"Foo Bar" <foo.bar@example.com>
+@end verbatim
+or
+@verbatim
+cuux@example.com
+@end verbatim
+
+An example @code{mu4e-contact-process-function} might look like:
+
+@lisp
+(defun my-contact-processor (contact)
+ (cond
+ ;; remove unwanted
+ ((string-match-p "evilspammer@@example.com" contact) nil)
+ ((string-match-p "noreply" contact) nil)
+ ;;
+ ;; jonh smiht --> John Smith
+ ((string-match "jonh smiht" contact)
+ (replace-regexp-in-string "jonh smiht" "John Smith" contact))
+ (t contact)))
+
+(setq mu4e-contact-process-function 'my-contact-processor)
+@end lisp
+
+
+@node Utility functions
+@section Utility functions
+
+@file{mu4e-utils} contains a number of utility functions; we list a few
+here. See their docstrings for details:
+@itemize
+@item @code{mu4e-read-option}: read one option from a list. For example:
+@lisp
+(mu4e-read-option "Choose an animal: "
+'(("Monkey" . monkey) ("Gnu" . gnu) ("xMoose" . moose)))
+@end lisp
+The user is presented with:
+@example
+Choose an animal: [M]onkey, [G]nu, [x]Moose
+@end example
+@item @code{mu4e-ask-maildir}: ask for a maildir; try one of the
+shortcuts (@code{mu4e-maildir-shortcuts}), or the full set of available
+maildirs.
+@item @code{mu4e-running-p}: return @code{t} if the @t{mu4e} process is
+running, @code{nil} otherwise.
+@item @code{(mu4e-user-mail-address-p addr)}: return @code{t} if @var{addr} is
+one of the user's e-mail addresses (as per @code{(mu4e-personal-addresses)}).
+@item @code{mu4e-log} logs to the @t{mu4e} debugging log if it is enabled;
+see @code{mu4e-toggle-logging}.
+@item @code{mu4e-message}, @code{mu4e-warning}, @code{mu4e-error} are the
+@t{mu4e} equivalents of the normal elisp @code{message},
+@code{user-error} and @code{error} functions.
+@end itemize
+
+@node Integration
+@chapter Integrating @t{mu4e} with Emacs facilities
+
+In this chapter, we discuss how you can integrate @t{mu4e} with Emacs in various
+ways. Here we focus on Emacs built-ins; for dealing with external tools,
+@xref{Other tools}.
+
+@menu
+* Default email client::Making mu4e the default emacs e-mail program
+* Modeline::Showing mu4e's status in the modeline
+* Desktop notifications::Get desktop notifications for new mail
+* Emacs bookmarks::Using Emacs' bookmark system
+* Eldoc::Information about the current header in the echo area
+* Org-mode links::Adding mu4e to your organized life
+* iCalendar::Enabling iCalendar invite processing
+* Speedbar::A special frame with your folders
+* Dired:: Attaching files using @t{dired}
+@end menu
+
+
+@node Default email client
+@section Default email client
+
+Emacs allows you to select an e-mail program as the default program it uses when
+you press @key{C-x m} (@code{compose-mail}), call @code{report-emacs-bug} and so
+on; see @ref{(emacs) Mail Methods}.
+
+If you want to use @t{mu4e} for this, you can do so by adding the following
+to your configuration:
+
+@lisp
+(setq mail-user-agent 'mu4e-user-agent)
+@end lisp
+
+Similarly, to specify @t{mu4e} as your preferred method for reading
+mail, customize the variable @code{read-mail-command}.
+
+@lisp
+(set-variable 'read-mail-command 'mu4e)
+@end lisp
+
+@node Modeline
+@section Modeline
+@cindex modeline
+
+One of the most visible ways in which @t{mu4e} integrates with Emacs is through
+the @emph{modeline} @xref{Mode Line,,,emacs}. The @t{mu4e} support for that is
+handled through a minor-mode @code{mu4e-modeline-mode}, which is enabled by
+default when @t{mu4e} is running.
+
+To completely turn off the modeline support, set @code{mu4e-modeline-support} to
+@t{nil} before starting @t{mu4e}.
+
+@t{mu4e} shares information on the modeline in two ways:
+@itemize
+@item buffer-specific
+@itemize
+@item current context (as per @ref{Contexts})
+@item current query parameters (headers-mode only)
+@end itemize
+@item global: information about the results for the ``favorite query''
+@end itemize
+
+The global indicators can be disabled by setting @code{mu4e-modeline-show-global}
+to @t{nil}.
+
+All of the bookmark items provide more details in their @code{help-echo},
+i.e., their tooltip.
+
+@subsection Query parameters bookmark item
+The query parameters in the modeline start with the various query flags (such as
+some representation of @code{mu4e-search-threads}, @code{mu4e-search-full}; the
+@t{help-echo} (tool-tip) has the details.
+
+The query parameters are followed by the query-string use for the headers-view.
+By default, if the query string matches some bookmark, the name of that bookmark
+is shown instead of the query it specifies. This can be changed by setting
+@code{mu4e-modeline-prefer-bookmark-name} to @t{nil}.
+
+@cindex favorite bookmark
+@subsection Favorite bookmark modeline item
+The global modeline contains the results of some specific ``favorite'' bookmark
+query from @code{mu4e-bookmarks}. By default, the @emph{first} one in chosen,
+but you may want to change that by using the @code{:favorite} property for a
+particular query, e.g., as part of your @var{mu4e-bookmarks}:
+@example
+ ;; Monitor the inbox folder in the modeline
+ (:query "maildir:/inbox" :name "Inbox" :key ?i :favorite t)
+@end example
+
+The results of this query (the last time it was updated) is shown as some
+character or emoji (depending on @var{mu4e-use-fancy-chars}) and 2 or 3 numbers,
+just like what we saw in @xref{Bookmarks and Maildirs}, e.g.,
+@example
+ N:10(+5)/15
+@end example
+
+@cindex baseline query results
+this means there are @emph{10 unread messages}, with @emph{5 new messages since
+the baseline}, and @emph{15 messages in total} matching the query.
+
+You can customize the icon; see @var{mu4e-modeline-all-clear},
+@var{mu4e-modeline-all-read}, @var{mu4e-modeline-unread-items} and
+@var{mu4e-modeline-new-items}.
+
+Due to the way queries work, the modeline is @emph{not} immediately updated when
+you read messages; but going back to the main view (with @kbd{M-x mu4e} resets
+the counts to latest known ones. When in the main-view, you can use
+@code{revert-buffer} (@kbd{g}) to reset the counters explicitly.
+
+@node Desktop notifications
+@section Desktop notifications
+@cindex desktop notifications
+
+Depending on your desktop environment, it is possible to get notification when
+there is new mail.
+
+The default implementation (which you can override) depends on the same system
+used for the @xref{Bookmarks and Maildirs}, in the main view and the
+@xref{Modeline}, and thus gives updates when there new messages compared to some
+``baseline'', as discussed earlier.
+
+For now, notifications are implemented for desktop environments that support
+DBus-based notifications, as per Emacs' notification sub-system @xref{(elisp)
+Desktop Notifications}.
+
+You can enable mu4e's desktop notifications (provided that you are on a
+supported system) by setting @code{mu4e-notification-support} to @t{t}. If you
+want tweak the details, have a look at @code{mu4e-notification-filter} and
+@code{mu4e-notification-function}.
+
+@node Emacs bookmarks
+@section Emacs bookmarks
+@cindex Emacs bookmarks
+
+Note, Emacs bookmarks are not to be confused with mu4e's bookmarks; the former
+are a generic linking system across Emacs, while the latter are stored queries
+within @t{mu4e}.
+
+@t{mu4e} supports linking to the message-at-point through the normal Emacs
+built-in bookmark system. The links are based on the message's message-id, and
+thus the bookmarks stay valid even if you move the message around.
+
+@node Eldoc
+@section Eldoc
+@cindex eldoc
+
+It is possible to get information about the current header in the echo-area.
+You can enable this by setting @t{mu4e-eldoc-support} to non-@t{nil}.
+
+@node Org-mode links
+@section Org-mode links
+
+It can be useful to include links to e-mail messages or search queries
+in your org-mode files. @t{mu4e} supports this by default, unless you
+set @t{mu4e-support-org} to @code{nil}.
+
+You can use the normal @t{org-mode} mechanisms to store links:
+@kbd{M-x org-store-link} stores a link to a particular message when
+you are in @ref{Message view}. When you are in @ref{Headers view},
+@kbd{M-x org-store-link} links to the @emph{query} if
+@code{mu4e-org-link-query-in-headers-mode} is non-@code{nil}, and to
+the particular message otherwise (which is the default). You can
+customize the link description using @code{mu4e-org-link-desc-func}.
+
+You can insert this link later with @kbd{M-x org-insert-link}. From
+@t{org-mode}, you can go to the query or message the link points to
+with either @kbd{M-x org-agenda-open-link} in agenda buffers, or
+@kbd{M-x org-open-at-point} elsewhere --- both typically bound to
+@kbd{C-c C-o}.
+
+You can also directly @emph{capture} such links --- for example, to
+add e-mail messages to your todo-list. For that, @t{mu4e-org} has a
+function @code{mu4e-org-store-and-capture}. This captures the
+message-at-point (or header --- see the discussion on
+@code{mu4e-org-link-query-in-headers-mode} above), then calls
+@t{org-mode}'s capture functionality.
+
+You can add some specific capture-template for this. In your capture
+templates, the following mu4e-specific values are available:
+
+@cartouche
+@verbatim
+item | description
+-----------------------------------------------------+------------------------
+%:date, %:date-timestamp, %:date-timestamp-inactive | date, org timestamps
+%:from, %:fromname, %:fromaddress | sender, name/address
+%:to, %:toname, %:toaddress | recipient, name/address
+%:maildir | maildir for the message
+%:message-id | message-id
+%:path | file system path
+%:subject | message subject
+@end verbatim
+@end cartouche
+
+For example, to add a message to your todo-list, and set a deadline
+for processing it within two days, you could add this to
+@code{org-capture-templates}:
+
+@lisp
+ ("P" "process-soon" entry (file+headline "todo.org" "Todo")
+ "* TODO %:fromname: %a %?\nDEADLINE: %(org-insert-time-stamp (org-read-date nil t \"+2d\"))")
+@end lisp
+
+If you use the functionality a lot, you may want to define
+key-bindings for that in headers and view mode:
+
+@lisp
+ (define-key mu4e-headers-mode-map (kbd "C-c c") 'mu4e-org-store-and-capture)
+ (define-key mu4e-view-mode-map (kbd "C-c c") 'mu4e-org-store-and-capture)
+@end lisp
+
+@node iCalendar
+@section iCalendar
+
+When Gnus' article-mode is chosen (@ref{Message view}), it is possible
+to view and reply to iCalendar events. To enable this feature, add
+
+@lisp
+(require 'mu4e-icalendar)
+(mu4e-icalendar-setup)
+@end lisp
+
+to your configuration. If you want that the original invitation message
+be automatically trashed after sending the message created by clicking
+on the buttons “Accept”, “Tentative”, or “Decline”, also add:
+
+@lisp
+(setq mu4e-icalendar-trash-after-reply t)
+@end lisp
+
+When you reply to an iCal event, a line may be automatically added to
+the diary file of your choice. You can specify that file with
+
+@lisp
+(setq mu4e-icalendar-diary-file "/path/to/your/diary")
+@end lisp
+
+Note that, if the specified file is not your main diary file, add
+@t{#include "/path/to/your/diary"} to you main diary file to display
+the events.
+
+To enable optional iCalendar→Org sync functionality, add the following:
+
+@lisp
+(setq gnus-icalendar-org-capture-file "~/org/notes.org")
+(setq gnus-icalendar-org-capture-headline '("Calendar"))
+(gnus-icalendar-org-setup)
+@end lisp
+
+Both the capture file and the headline(s) inside it must already exist.
+
+By default, @code{gnus-icalendar-org-setup} adds a temporary capture
+template to the variable @code{org-capture-templates}, with the
+description ``used by gnus-icalendar-org'', and the shortcut key ``#''.
+If you want to use your own template, create it using the same key and
+description. This will prevent the temporary one from being installed
+next time you @code{gnus-icalendar-org-setup} is called.
+
+The full default capture template is:
+
+@lisp
+("#" "used by gnus-icalendar-org" entry
+ (file+olp ,gnus-icalendar-org-capture-file
+ ,gnus-icalendar-org-capture-headline)
+ "%i" :immediate-finish t)
+@end lisp
+
+where the values of the variables @code{gnus-icalendar-org-capture-file}
+and @code{gnus-icalendar-org-capture-headline} are inserted via macro
+expansion.
+
+If, for example, you wanted to store ical events in a date tree,
+prompting for the date, you could use the following:
+
+@lisp
+("#" "used by gnus-icalendar-org" entry
+ (file+olp+datetree path-to-capture-file)
+ "%i" :immediate-finish t :time-prompt t)
+@end lisp
+
+Note that the default behaviour for @code{datetree} targets in this
+situation is to store the event at the date that you capture it, not at
+the date that it is scheduled. That's why I've suggested using the
+@code{:timeprompt t} argument. This gives you an opportunity to set the
+time to the correct value yourself.
+
+You can extract the event time directly, and have the @code{org-capture}
+functions use that to set the @code{datetree} location:
+
+@lisp
+(defun my-catch-event-time (orig-fun &rest args)
+ "Set org-overriding-default-time to the start time of the capture event"
+ (let ((org-overriding-default-time (date-to-time
+ (gnus-icalendar-event:start (car args)))))
+ (apply orig-fun args)))
+
+(advice-add 'gnus-icalendar:org-event-save :around #'my-catch-event-time)
+@end lisp
+
+If you do this, you'll want to omit the @code{:timeprompt t} setting
+from your capture template.
+
+@node Speedbar
+@section Speedbar
+@cindex speedbar
+
+@code{speedbar} is an Emacs-extension that shows navigational
+information for an Emacs buffer in a separate frame. Using
+@code{mu4e-speedbar}, @t{mu4e} lists your bookmarks and maildir
+folders and allows for one-click access to them.
+
+To enable this, add @t{(require 'mu4e-speedbar)} to your configuration;
+then, all you need to do to activate it is @kbd{M-x speedbar}. Then,
+when then switching to the @ref{Main view}, the speedbar-frame is
+updated with your bookmarks and maildirs.
+
+For speed reasons, the list of maildirs is determined when @t{mu4e}
+starts; if the list of maildirs changes while @t{mu4e} is running, you
+need to restart @t{mu4e} to have those changes reflected in the speedbar
+and in other places that use this list, such as auto-completion when
+jumping to a maildir.
+
+@node Dired
+@section Dired
+@cindex dired
+
+It is possible to attach files to @t{mu4e} messages using @t{dired}
+(@ref{Dired,,emacs}), using the following steps (based on a post on
+the @t{mu-discuss} mailing list by @emph{Stephen Eglen}).
+
+@lisp
+(add-hook 'dired-mode-hook 'turn-on-gnus-dired-mode)
+@end lisp
+
+Then, mark the file(s) in @t{dired} you would like to attach and press
+@t{C-c RET C-a}, and you'll be asked whether to attach them to an
+existing message, or create a new one.
+
+@node Other tools
+@appendix Other tools
+
+In this chapter, we discuss some ways in which @t{mu4e} can cooperate
+with other tools.
+
+@menu
+* Org-contacts::Hooking up with org-contacts
+* BBDB::Hooking up with the Insidious Big Brother Database
+* Sauron::Getting new mail notifications with Sauron
+* Hydra:: Custom shortcut menus
+@end menu
+
+@node Org-contacts
+@section Org-contacts
+
+Note, @t{mu4e} supports built-in address autocompletion; @ref{Address
+autocompletion}, and that is the recommended way to do this. However, it is also
+possible to manage your addresses with @t{org-mode}, using
+@uref{https://julien.danjou.info/projects/emacs-packages#org-contacts,org-contacts}.
+
+@t{mu4e-actions} defines a useful action (@ref{Actions}) for adding a
+contact based on the @t{From:}-address in the message at point. To
+enable this, add to your configuration something like:
+
+@lisp
+ (setq mu4e-org-contacts-file <full-path-to-your-org-contacts-file>)
+ (add-to-list 'mu4e-headers-actions
+ '("org-contact-add" . mu4e-action-add-org-contact) t)
+ (add-to-list 'mu4e-view-actions
+ '("org-contact-add" . mu4e-action-add-org-contact) t)
+@end lisp
+
+@noindent
+After this, you should be able to add contacts using @key{a o} in the
+headers view and the message view, using the @t{org-capture} mechanism.
+Note, the shortcut character @key{o} is due to the first character of
+@t{org-contact-add}.
+
+@node BBDB
+@section BBDB
+
+Note, @t{mu4e} supports built-in address autocompletion; @ref{Address
+autocompletion}, and that is the recommended way to do this. However, it is also
+possible to manage your addresses with
+@uref{https://savannah.nongnu.org/projects/bbdb/,BBDB}.
+
+To enable BBDB, add to your @file{~/.emacs} (or its moral equivalent,
+such as @file{~/.emacs.d/init.el}) the following @emph{after} the
+@code{(require 'mu4e)} line:
+
+@lisp
+ ;; Load BBDB (Method 1)
+ (require 'bbdb-loaddefs)
+ ;; OR (Method 2)
+ ;; (require 'bbdb-loaddefs "/path/to/bbdb/lisp/bbdb-loaddefs.el")
+ ;; OR (Method 3)
+ ;; (autoload 'bbdb-insinuate-mu4e "bbdb-mu4e")
+ ;; (bbdb-initialize 'message 'mu4e)
+
+ (setq bbdb-mail-user-agent 'mu4e-user-agent)
+ (setq mu4e-view-rendered-hook 'bbdb-mua-auto-update)
+ (setq mu4e-compose-complete-addresses nil)
+ (setq bbdb-mua-pop-up t)
+ (setq bbdb-mua-pop-up-window-size 5)
+ (setq mu4e-view-show-addresses t)
+@end lisp
+
+For recent emacs (29 and later), address-completion may need some extra setup:
+@lisp
+(add-hook 'message-mode-hook
+ (lambda ()
+ (add-to-list 'completion-at-point-functions
+ #'eudc-capf-complete)))
+@end lisp
+or, if that does not work:
+@lisp
+(add-hook 'message-mode-hook
+ (lambda ()
+ (add-to-list 'completion-at-point-functions
+ #'message-expand-name)))
+@end lisp
+
+@noindent
+After this, you should be able to:
+@itemize
+@item In mu4e-view mode, add the sender of the email to BBDB with @key{C-u :}
+@item Tab-complete addresses from BBDB when composing emails
+@item View the BBDB contact while viewing a message
+@end itemize
+
+
+
+@node Sauron
+@section Sauron
+
+The Emacs package @uref{https://github.com/djcb/sauron,sauron} (by the same
+author) can be used to get notifications about new mails. If you run something
+like the below script from your @t{crontab} (or have some other way of having it
+execute every @emph{n} minutes), you receive notifications in the
+@t{sauron}-buffer when new messages arrive.
+
+@verbatim
+#!/bin/sh
+
+# the mu binary
+MU=mu
+
+# put the path to your Inbox folder here
+CHECKDIR="/home/$LOGNAME/Maildir/Inbox"
+
+sauron_msg () {
+DBUS_COOKIE="/home/$LOGNAME/.sauron-dbus"
+if test "x$DBUS_SESSION_BUS_ADDRESS" = "x"; then
+ if test -e $DBUS_COOKIE; then
+ export DBUS_SESSION_BUS_ADDRESS="`cat $DBUS_COOKIE`"
+ fi
+fi
+if test -n "x$DBUS_SESSION_BUS_ADDRESS"; then
+ dbus-send --session \
+ --dest="org.gnu.Emacs" \
+ --type=method_call \
+ "/org/gnu/Emacs/Sauron" \
+ "org.gnu.Emacs.Sauron.AddMsgEvent" \
+ string:shell uint32:3 string:"$1"
+fi
+}
+
+#
+# -mmin -5: consider only messages that were created / changed in the
+# the last 5 minutes
+#
+for f in `find $CHECKDIR -mmin -5 -a -type f -not -iname '.uidvalidity'`; do
+ subject=`$MU view $f | grep '^Subject:' | sed 's/^Subject://'`
+ sauron_msg "mail: $subject"
+done
+@end verbatim
+
+@noindent
+You might want to put:
+@lisp
+(setq sauron-dbus-cookie t)
+@end lisp
+@noindent
+in your setup, to allow the script to find the D-Bus session bus, even when
+running outside its session.
+
+
+@node Hydra
+@section Hydra
+
+People sometimes ask about having multi-character shortcuts for bookmarks; an
+easy way to achieve this, is by using an emacs package
+@uref{https://github.com/abo-abo/hydra,Hydra}.
+
+With Hydra installed, we can add multi-character shortcuts, for instance:
+@lisp
+(defhydra my-mu4e-bookmarks-work (:color blue)
+ "work bookmarks"
+ ("b" (mu4e-search "banana AND maildir:/work") "banana")
+ ("u" (mu4e-search "flag:unread AND maildir:/work") "unread"))
+
+(defhydra my-mu4e-bookmarks-personal (:color blue)
+ "personal bookmarks"
+ ("c" (mu4e-search "capybara AND maildir:/personal") "capybara")
+ ("u" (mu4e-search "flag:unread AND maildir:/personal") "unread"))
+
+(defhydra my-mu4e-bookmarks (:color blue)
+ "mu4e bookmarks"
+ ("p" (my-mu4e-bookmarks-personal/body) "Personal")
+ ("w" (my-mu4e-bookmarks-work/body) "Work"))
+
+Now, you can bind a convenient key to my-mu4e-bookmarks/body.
+@end lisp
+
+@node Example configurations
+@appendix Example configurations
+
+In this chapter, we show some example configurations. While it is very useful
+to see some working settings, we'd like to warn against blindly copying such
+things.
+
+@menu
+* Minimal configuration::Simplest configuration to get you going
+* Longer configuration::A more extensive setup
+* Gmail configuration::GMail-specific setup
+* Other settings:CONF Other settings. Some other useful configuration
+
+@end menu
+
+@node Minimal configuration
+@section Minimal configuration
+
+An (almost) minimal configuration for @t{mu4e} might look like this --- as you
+see, most of it is commented-out.
+
+@lisp
+;; example configuration for mu4e
+
+;; make sure mu4e is in your load-path
+(require 'mu4e)
+
+;; use mu4e for e-mail in emacs
+(setq mail-user-agent 'mu4e-user-agent)
+
+;; these must start with a "/", and must exist
+;; (i.e.. /home/user/Maildir/sent must exist)
+;; you use e.g. 'mu mkdir' to make the Maildirs if they don't
+;; already exist
+
+;; below are the defaults; if they do not exist yet, mu4e offers to
+;; create them. they can also functions; see their docstrings.
+;; (setq mu4e-sent-folder "/sent")
+;; (setq mu4e-drafts-folder "/drafts")
+;; (setq mu4e-trash-folder "/trash")
+
+;; smtp mail setting; these are the same that `gnus' uses.
+(setq
+ message-send-mail-function 'smtpmail-send-it
+ smtpmail-default-smtp-server "smtp.example.com"
+ smtpmail-smtp-server "smtp.example.com"
+ smtpmail-local-domain "example.com")
+@end lisp
+
+
+@node Longer configuration
+@section Longer configuration
+
+A somewhat longer configuration, showing some more things that you can
+customize.
+
+@lisp
+;; example configuration for mu4e
+(require 'mu4e)
+
+;; use mu4e for e-mail in emacs
+(setq mail-user-agent 'mu4e-user-agent)
+
+;; the next are relative to the root maildir
+;; (see `mu info`).
+;; instead of strings, they can be functions too, see
+;; their docstring or the chapter 'Dynamic folders'
+(setq mu4e-sent-folder "/sent"
+ mu4e-drafts-folder "/drafts"
+ mu4e-trash-folder "/trash")
+
+;; the maildirs you use frequently; access them with 'j' ('jump')
+(setq mu4e-maildir-shortcuts
+ '((:maildir "/archive" :key ?a)
+ (:maildir "/inbox" :key ?i)
+ (:maildir "/work" :key ?w)
+ (:maildir "/sent" :key ?s)))
+
+;; the headers to show in the headers list -- a pair of a field
+;; and its width, with `nil' meaning 'unlimited'
+;; (better only use that for the last field.
+;; These are the defaults:
+(setq mu4e-headers-fields
+ '( (:date . 25) ;; alternatively, use :human-date
+ (:flags . 6)
+ (:from . 22)
+ (:subject . nil))) ;; alternatively, use :thread-subject
+
+(add-to-list 'mu4e-bookmarks
+ ;; ':favorite t' i.e, use this one for the modeline
+ '(:query "maildir:/inbox" :name "Inbox" :key ?i :favorite t))
+
+;; program to get mail; alternatives are 'fetchmail', 'getmail'
+;; isync or your own shellscript. called when 'U' is pressed in
+;; main view.
+
+;; If you get your mail without an explicit command,
+;; use "true" for the command (this is the default)
+(setq mu4e-get-mail-command "offlineimap")
+
+;; general emacs mail settings; used when composing e-mail
+;; the non-mu4e-* stuff is inherited from emacs/message-mode
+(setq mu4e-compose-reply-to-address "foo@@bar.example.com"
+ user-mail-address "foo@@bar.example.com"
+ user-full-name "Foo X. Bar")
+(setq message-signature "Foo X. Bar\nhttp://www.example.com\n")
+
+;; smtp mail setting
+(setq
+ message-send-mail-function 'smtpmail-send-it
+ smtpmail-default-smtp-server "smtp.example.com"
+ smtpmail-smtp-server "smtp.example.com"
+ smtpmail-local-domain "example.com"
+
+ ;; if you need offline mode, set these -- and create the queue dir
+ ;; with 'mu mkdir', i.e.. mu mkdir /home/user/Maildir/queue
+ smtpmail-queue-mail nil
+ smtpmail-queue-dir "/home/user/Maildir/queue/cur")
+
+;; don't keep message buffers around
+(setq message-kill-buffer-on-exit t)
+@end lisp
+
+
+@node Gmail configuration
+@section Gmail configuration
+
+@emph{Gmail} is a popular e-mail provider; let's see how we can make it
+work with @t{mu4e}. Since we are using @abbr{IMAP}, you must enable that
+in the Gmail web interface (in the settings, under the ``Forwarding and
+POP/IMAP''-tab).
+
+Gmail users may also be interested in @ref{Including related messages},
+and in @ref{Skipping duplicates}.
+
+@subsection Setting up offlineimap
+
+First of all, we need a program to get the e-mail from Gmail to our
+local machine; for this we use @t{offlineimap}; on Debian (and
+derivatives like Ubuntu), this is as easy as:
+
+@verbatim
+$ sudo apt-get install offlineimap
+@end verbatim
+
+while on Fedora (and similar) you need:
+@verbatim
+$ sudo yum install offlineimap
+@end verbatim
+
+Then, we can configure @t{offlineimap} by editing @file{~/.offlineimaprc}:
+
+@verbatim
+[general]
+accounts = Gmail
+maxsyncaccounts = 3
+
+[Account Gmail]
+localrepository = Local
+remoterepository = Remote
+
+[Repository Local]
+type = Maildir
+localfolders = ~/Maildir
+
+[Repository Remote]
+type = IMAP
+remotehost = imap.gmail.com
+remoteuser = USERNAME@gmail.com
+remotepass = PASSWORD
+ssl = yes
+maxconnections = 1
+@end verbatim
+
+Obviously, you need to replace @t{USERNAME} and @t{PASSWORD} with your actual
+Gmail username and password. After this, you should be able to download your
+mail:
+
+@verbatim
+$ offlineimap
+ OfflineIMAP 6.3.4
+Copyright 2002-2011 John Goerzen & contributors.
+Licensed under the GNU GPL v2+ (v2 or any later version).
+
+Account sync Gmail:
+ ***** Processing account Gmail
+ Copying folder structure from IMAP to Maildir
+ Establishing connection to imap.gmail.com:993.
+Folder sync [Gmail]:
+ Syncing INBOX: IMAP -> Maildir
+ Syncing [Gmail]/All Mail: IMAP -> Maildir
+ Syncing [Gmail]/Drafts: IMAP -> Maildir
+ Syncing [Gmail]/Sent Mail: IMAP -> Maildir
+ Syncing [Gmail]/Spam: IMAP -> Maildir
+ Syncing [Gmail]/Starred: IMAP -> Maildir
+ Syncing [Gmail]/Trash: IMAP -> Maildir
+Account sync Gmail:
+ ***** Finished processing account Gmail
+@end verbatim
+
+We can now run @command{mu} to make sure things work:
+
+@verbatim
+$ mu index
+mu: indexing messages under /home/foo/Maildir [/home/foo/.cache/mu/xapian]
+| processing mail; checked: 520; updated/new: 520, cleaned-up: 0
+mu: elapsed: 3 second(s), ~ 173 msg/s
+mu: cleaning up messages [/home/foo/.cache/mu/xapian]
+/ processing mail; checked: 520; updated/new: 0, cleaned-up: 0
+mu: elapsed: 0 second(s)
+@end verbatim
+
+We can run both the @t{offlineimap} and the @t{mu index} from within
+@t{mu4e}, but running it from the command line makes it a bit easier to
+troubleshoot as we are setting things up.
+
+Note: when using encryption, you probably do @emph{not} want to
+synchronize your Drafts-folder, since it contains the unencrypted
+messages. You can use OfflineIMAP's @t{folderfilter} for that.
+
+@subsection Settings
+
+Next step: let's make a @t{mu4e} configuration for this:
+
+@lisp
+(require 'mu4e)
+
+;; use mu4e for e-mail in emacs
+(setq mail-user-agent 'mu4e-user-agent)
+
+(setq mu4e-drafts-folder "/[Gmail].Drafts")
+(setq mu4e-sent-folder "/[Gmail].Sent Mail")
+(setq mu4e-trash-folder "/[Gmail].Trash")
+
+;; don't save message to Sent Messages, Gmail/IMAP takes care of this
+(setq mu4e-sent-messages-behavior 'delete)
+
+;; (See the documentation for `mu4e-sent-messages-behavior' if you have
+;; additional non-Gmail addresses and want assign them different
+;; behavior.)
+
+;; setup some handy shortcuts
+;; you can quickly switch to your Inbox -- press ``ji''
+;; then, when you want archive some messages, move them to
+;; the 'All Mail' folder by pressing ``ma''.
+
+(setq mu4e-maildir-shortcuts
+ '( (:maildir "/INBOX" :key ?i)
+ (:maildir "/[Gmail].Sent Mail" :key ?s)
+ (:maildir "/[Gmail].Trash" :key ?t)
+ (:maildir "/[Gmail].All Mail" :key ?a)))
+
+(add-to-list 'mu4e-bookmarks
+ ;; ':favorite t' i.e, use this one for the modeline
+ '(:query "maildir:/inbox" :name "Inbox" :key ?i :favorite t))
+
+;; allow for updating mail using 'U' in the main view:
+(setq mu4e-get-mail-command "offlineimap")
+
+;; something about ourselves
+(setq
+ user-mail-address "USERNAME@@gmail.com"
+ user-full-name "Foo X. Bar"
+ message-signature
+ (concat
+ "Foo X. Bar\n"
+ "http://www.example.com\n"))
+
+;; sending mail -- replace USERNAME with your gmail username
+;; also, make sure the gnutls command line utils are installed
+;; package 'gnutls-bin' in Debian/Ubuntu
+
+(require 'smtpmail)
+(setq message-send-mail-function 'smtpmail-send-it
+ starttls-use-gnutls t
+ smtpmail-starttls-credentials '(("smtp.gmail.com" 587 nil nil))
+ smtpmail-auth-credentials
+ '(("smtp.gmail.com" 587 "USERNAME@@gmail.com" nil))
+ smtpmail-default-smtp-server "smtp.gmail.com"
+ smtpmail-smtp-server "smtp.gmail.com"
+ smtpmail-smtp-service 587)
+
+;; alternatively, for emacs-24 you can use:
+;;(setq message-send-mail-function 'smtpmail-send-it
+;; smtpmail-stream-type 'starttls
+;; smtpmail-default-smtp-server "smtp.gmail.com"
+;; smtpmail-smtp-server "smtp.gmail.com"
+;; smtpmail-smtp-service 587)
+
+;; don't keep message buffers around
+(setq message-kill-buffer-on-exit t)
+@end lisp
+
+And that's it --- put the above in your emacs initialization file, change
+@t{USERNAME} etc. to your own, restart Emacs, and run @kbd{M-x mu4e}.
+
+@node CONF Other settings
+@section Other settings
+
+Finally, here are some more settings that are useful, but not enabled by
+default for various reasons.
+
+@lisp
+;; use 'fancy' non-ascii characters in various places in mu4e
+(setq mu4e-use-fancy-chars t)
+
+;; save attachment to my desktop (this can also be a function)
+(setq mu4e-attachment-dir "~/Desktop")
+
+;; attempt to show images when viewing messages
+(setq mu4e-view-show-images t)
+@end lisp
+
+@node FAQ
+@appendix FAQ --- Frequently Asked Questions
+
+In this chapter we list a number of actual and anticipated questions and their
+answers.
+
+@menu
+* General::General questions and answers about @t{mu4e}
+* Retrieving mail::Getting mail and indexing
+* Reading messages::Dealing with incoming messages
+* Writing messages::Dealing with outgoing messages
+* Known issues::Limitations we know about
+@end menu
+
+@node General
+@section General
+
+@subsection Results from @t{mu} and @t{mu4e} differ - why?
+@anchor{mu-mu4e-differ} In general, the same queries for @command{mu}
+and @t{mu4e} should yield the same results. If they differ, this is
+usually because one of the following reasons:
+@itemize
+@item different options:
+@t{mu4e} defaults to having @t{mu4e-headers-include-related}, and
+@t{mu4e-headers-results-limit} set to 500. However, the command-line
+@command{mu find}'s corresponding @t{--include-related} is false, and
+there's no limit (@t{--maxnum}).
+@item reverse sorting:
+The results may be different when @t{mu4e} and @command{mu find} do
+not both sort their results in the same direction.
+@item shell quoting issues:
+Depending on the shell, various shell metacharacters in search query
+(such as @t{*}) may be expanded by the shell before @command{mu} ever
+sees them, and the query may not be what you think it is. Quoting is
+necessary.
+@end itemize
+
+@subsection The unread/all counts in the main-screen differ from the 'real' numbers - what's going on?
+For speed reasons, the counts do not exclude messages that no longer exist in
+the file-system, nor does it exclude duplicate messages; @xref{mu-mu4e-differ}.
+
+@subsection How can I quickly delete/move/trash a lot of messages?
+You can select ('mark' in Emacs-speak) messages, just like you would select text
+in a buffer; the actions you then take (e.g., @key{DEL} for delete, @key{m} for
+move and @key{t} for trash) apply to all selected messages. You can also use
+functions like @code{mu4e-headers-mark-thread} (@key{T}),
+@code{mu4e-headers-mark-subthread} (@key{t}) to mark whole threads at the same
+time, and @code{mu4e-headers-mark-pattern} (@key{%}) to mark all messages
+matching a certain regular expression.
+
+@subsection Can I automatically apply the marks on messages when leaving the headers buffer?
+Yes you can --- see the documentation for the variable
+@t{mu4e-headers-leave-behavior}.
+
+@subsection How can I set @t{mu4e} as the default e-mail client in Emacs?
+See @ref{Default email client}.
+
+@subsection Can @t{mu4e} use some fancy Unicode instead of these boring plain-ASCII ones?
+Glad you asked! Yes, if you set @code{mu4e-use-fancy-chars} to @t{t}, @t{mu4e}
+uses such fancy characters in a number of places. Since not all fonts include
+all characters, you may want to install the @t{unifont} and/or @t{symbola} fonts
+on your system.
+
+@subsection Can I start @t{mu4e} in the background?
+Yes --- if you provide a prefix-argument (@key{C-u}), @t{mu4e} starts, but does
+not show the main-window.
+
+@subsection Does @t{mu4e} support searching for CJK (Chinese-Japanese-Korean) characters?
+Only partially. If you have @t{Xapian} 1.2.8 or newer, and set the environment
+variable @t{XAPIAN_CJK_NGRAM} to non-empty before indexing, both when using
+@t{mu} from the command-line and from @t{mu4e}.
+
+@subsection How can I customize the function to select a folder?
+The @t{mu4e-completing-read-function} variable can be customized to select a
+folder in any way. The variable can be set to a function that receives five
+arguments, following @t{completing-read}. The default value is
+@code{ido-completing-read}; to use emacs's default behavior, set the variable to
+@code{completing-read}. Helm users can use the same value, and by enabling
+@code{helm-mode} use helm-style completion.
+
+@subsection With a lot of Maildir folders, jumping to them can get slow. What can I do?
+Set @code{mu4e-cache-maildir-list} to @code{t} (make sure to read its
+docstring).
+
+@subsection How can I hide certain messages from the search results?
+See the variables @code{mu4e-headers-hide-predicate} and
+@code{mu4e-headers-hide-enabled}. The latter can be toggled through
+@code{mu4e-headers-toggle-property}.
+
+For example, to filter out GMail's spam folder, set it to:
+@lisp
+(setq mu4e-headers-hide-predicate
+ (lambda (msg)
+ (string-suffix-p "Spam" (mu4e-message-field msg :maildir))))
+@end lisp
+
+@subsection I'm getting an error 'Variable binding depth exceeds max-specpdl-size' when using mu4e -- what can I do about it?
+The error occurs because @t{mu4e} is binding more variables than
+@t{emacs} allows for, by default. You can avoid this by setting a
+higher value, e.g. by adding the following to your configuration:
+@lisp
+(setq max-specpdl-size 5000)
+@end lisp
+Note that Emacs 29 obsoletes this variable.
+
+@node Retrieving mail
+@section Retrieving mail
+
+@subsection How can I get notifications when receiving mail?
+There is @code{mu4e-index-updated-hook}, which gets triggered when the
+indexing process triggered sees an update (not just new mail though). To
+use this hook, put something like the following in your setup (assuming
+you have @t{aplay} and some soundfile, change as needed):
+@lisp
+(add-hook 'mu4e-index-updated-hook
+ (defun new-mail-sound ()
+ (shell-command "aplay ~/Sounds/boing.wav&")))
+@end lisp
+
+@subsection I'm getting mail through a local mailserver. What should I use for @code{mu4e-get-mail-command}?
+Use the literal string @t{"true"} (or don't do anything, it's the
+default) which then uses @t{/bin/true} (a command that does nothing and
+always succeeds). This makes getting mail a no-op, but the messages are
+still re-indexed.
+
+@subsection How can I re-index my messages without getting new mail?
+Use @kbd{M-x mu4e-update-index}
+
+@subsection When I try to run @t{mu index} while @t{mu4e} is running I get errors
+For instance:
+@verbatim
+mu: mu_store_new_writable: xapian error
+ 'Unable to get write lock on ~/.cache/mu/xapian: already locked
+@end verbatim
+What to do about this? You get this error because the underlying Xapian
+database is locked by some other process; it can be opened only once in
+read-write mode. There is not much @t{mu4e} can do about this, but if is
+another @command{mu} instance that is holding the lock, you can ask it
+to (gracefully) terminate:
+@verbatim
+ pkill -2 -u $UID mu # send SIGINT
+ sleep 1
+ mu index
+@end verbatim
+@t{mu4e} automatically restarts @t{mu} when it needs it. In practice, this
+seems to work quite well.
+
+@subsection How can I disable the @t{Indexing...} messages?
+Set the variable @code{mu4e-hide-index-messages} to non-@t{nil}.
+
+@subsection IMAP-synchronization and file-name changes
+Some IMAP-synchronization programs such as @t{mbsync} (but not
+@t{offlineimap}) don't like it when message files do not change their
+names when they are moved to different folders. @t{mu4e} can attempt to
+help with this - you can set the variable
+@code{mu4e-change-filenames-when-moving} to non-@t{nil}.
+
+@subsection @command{offlineimap} and UTF-7
+@command{offlineimap} uses IMAP's UTF-7 for encoding non-ascii folder
+names, while @command{mu} expects UTF-8 (so, e.g. @t{/まりもえ
+お}@footnote{some Japanese characters} becomes @t{/&MH4wijCCMEgwSg-}).
+
+This is best solved by telling @command{offlineimap} to use UTF-8 instead ---
+see @uref{https://github.com/djcb/mu/issues/68#issuecomment-8598652,this
+ticket}.
+
+@subsection @command{mbsync} or @command{offlineimap} do not sync properly
+Unfortunately, @command{mbsync} and/or @command{offlineimap} do not
+always agree with @t{mu} about the meaning of various Maildir-flags. If
+you encounter unexpected behavior, it is recommended you check before
+and after a sync-operation. If the problem only shows up @emph{after}
+sync'ing, the problem is with the sync-program, and it's most productive
+to complain there.
+
+Also, you may want to ensure that @t{mu4e-index-lazy-check} is kept at
+its default (@t{nil}) value, since it seems @command{mbsync} can make
+changes that escape a 'lazy' check.
+
+Furthermore, there have been quite a few related queries on the
+mailing-list; worthwhile to check out.
+
+@node Reading messages
+@section Reading messages
+
+@subsection Opening messages is slower than expected - why?
+@t{mu4e} is designed to be very fast, even with large amounts of mail.
+However, if you experience slowdowns, here are some things to consider:
+@itemize
+@item opening messages while indexing:
+@t{mu4e} communicates with the @t{mu} server mostly synchronously; this means
+that you can do only one thing at a time. The one operation that potentially
+does take a bit of time is indexing of mail. Indexing does happen
+asynchronously, but still can slow down @t{mu} enough that users may notice.
+
+For some strategies to reduce that time, see the next question.
+@item getting contact information can take some time:
+especially when opening @t{mu4e} the first time and you have a
+@emph{lot} of contacts, it can take a few seconds to process those. Note
+that @t{mu4e} 1.3 and higher only get @emph{changed} contacts in
+subsequent updates (after and indexing operation), so this should be
+less of a concern. And you can tweak what contacts you get using
+@var{mu4e-compose-complete-only-personal},
+@var{mu4e-compose-complete-only-after} and
+@var{mu4e-compose-complete-max}.
+@item decryption / sign verification:
+encrypted / signed messages sometimes require network access, and this
+may take a while; certainly if the needed servers cannot be found.
+Part of this may be that influential environment variables are not set
+in the emacs environment.
+@end itemize
+
+If you still experience unexpected slowness, you can of course file a
+ticket, but please be sure to mention the following:
+
+@itemize
+@item are all messages slow or only some messages?
+@item if it's only some messages, is there something specific about them?
+@item in addition, please a (sufficiently censored version of) a message that is slow
+@item is opening @emph{always} slow or only sometimes? When?
+@end itemize
+
+@subsection How can I word-wrap long lines in when viewing a message?
+You can toggle between wrapped and non-wrapped states using @key{w}. If you want
+to do this automatically, invoke @code{visual-line-mode} in your
+@code{mu4e-view-rendered-hook} (@code{mu4e-view-mode-hook} fires too early).
+@subsection How can I perform custom actions on messages and attachments?
+See @ref{Actions}.
+@subsection How can I prevent @t{mu4e} from automatically marking messages as `read' when I read them?
+Set @code{mu4e-view-auto-mark-as-read} to @code{nil}.
+@subsection Does @t{mu4e} support including all related messages in a thread, like Gmail does?
+Yes --- see @ref{Including related messages}.
+@subsection There seems to be a lot of duplicate messages --- how can I get rid of them?
+See @ref{Skipping duplicates}.
+@subsection Some messages are almost unreadable in emacs --- can I view them in an external web browser?
+Indeed, airlines often send messages that heavily depend on html and
+are hard to digest inside emacs. Fortunately, there's an @emph{action}
+(@ref{Message view actions}) defined for this. Simply add to your
+configuration:
+@lisp
+(add-to-list 'mu4e-view-actions
+ '("ViewInBrowser" . mu4e-action-view-in-browser) t)
+@end lisp
+Now, when viewing such a difficult message, type @kbd{aV}, and the
+message opens inside a web browser. You can influence the browser to
+use with @code{browse-url-generic-program}.
+@subsection How can I read encrypted messages that I sent?
+Since you do not own the recipient's key you typically cannot read
+those mails --- so the trick is to encrypt outgoing mails with your
+key, too. This can be automated by adding the following snippet to
+your configuration (courtesy of user @t{kpachnis}):
+@lisp
+(require 'epg-config)
+(setq mml2015-use 'epg
+ epg-user-id "gpg_key_id"
+ mml2015-encrypt-to-self t
+ mml2015-sign-with-sender t)
+@end lisp
+
+@node Writing messages
+@section Writing messages
+
+@subsection How can I automatically set the @t{From:}-address for a reply-message?
+See @ref{Compose hooks}.
+
+@subsection How can I dynamically determine the folders for draft/sent/trashed messages?
+See @ref{Dynamic folders}.
+
+@subsection How can I define aliases for (groups of) e-mail addresses?
+See @ref{(emacs) Mail Aliases}.
+
+@subsection How can I automatically add some header to an outgoing message?
+See @ref{Compose hooks}.
+
+@subsection How can I influence the way the original message looks when replying/forwarding?
+Since @code{mu4e-compose-mode} derives from @xref{(message) Top}, you can re-use
+many (though not @emph{all} of its facilities.
+
+@subsection How can I easily include attachments in the messages I write?
+You can drag-and-drop from your desktop; alternatively, you can use @ref{(emacs)
+Dired}.
+
+@subsection How can I start a new message-thread from a reply?
+Remove the @t{In-Reply-To} header, and @t{mu4e} automatically removes
+the (hidden) @t{References} header as well when sending it. This makes
+the message show up as a top-level message rather than as a response.
+
+@subsection How can I attach an existing message?
+Use @code{mu4e-action-capture-message} (i.e., @kbd{a c} in the headers
+ view) to `capture' the to-be-attached message, then when editing the
+ message, use @kbd{M-x mu4e-compose-attach-captured-message}.
+
+@subsection How can I sign or encrypt messages?
+You can do so using Emacs' MIME-support --- check the
+@t{Attachments}-menu while composing a message. Also see @ref{Signing
+and encrypting}.
+
+@subsection Address auto-completion misses some addresses
+If you have set @code{mu4e-compose-complete-only-personal} to non-nil, @t{mu4e}
+only completes 'personal' addresses - so you tell it about your e-mail addresses
+when setting up the database (@t{mu init}); @ref{Initializing the message
+store}.
+
+If you cannot find specific addresses you'd expect to find, inspect the
+values of @var{mu4e-compose-complete-only-personal},
+@var{mu4e-compose-complete-only-after} and
+@var{mu4e-compose-complete-max}.
+
+@subsection How can I get rid of the message buffer after sending?
+@lisp
+(setq message-kill-buffer-on-exit t)
+@end lisp
+
+@subsection Sending big messages is slow and blocks emacs --- what can I do about it?
+
+For this, there's @uref{https://github.com/jwiegley/emacs-async,emacs-async}
+(also available from the Emacs package repository); add the following snippet to
+your configuration:
+@lisp
+(require 'smtpmail-async)
+(setq
+ send-mail-function 'async-smtpmail-send-it
+ message-send-mail-function 'async-smtpmail-send-it)
+@end lisp
+With this, messages are sent using a background Emacs instance.
+
+A word of warning though, this tends to not be as reliable as sending the
+message in the normal, synchronous fashion, and people have reported silent
+failures, where mail sending fails for some reason without any indication of
+that.
+
+You can check the progress of the background delivery by checking the
+@t{*Messages*}-buffer, which should show something like:
+@verbatim
+Delivering message to "William Shakespeare" <will@example.com>...
+Mark set
+Saving file /home/djcb/Maildir/sent/cur/20130706-044350-darklady:2,S...
+Wrote /home/djcb/Maildir/sent/cur/20130706-044350-darklady:2,S
+Sending...done
+@end verbatim
+The first and final messages are the most important, and there may be
+considerable time between them, depending on the size of the message.
+
+@subsection Is it possible to view headers and messages, or compose new ones, in a separate frame or window?
+Yes. There is built-in support for composing messages in a new frame or window.
+Either use Emacs' standard @t{compose-mail-other-frame} (@kbd{C-x 5 m}) and
+@t{compose-mail-other-window} (@kbd{C-x 4 m}) if you have set up @t{mu4e} as your Emacs
+e-mailer.
+
+Additionally, there's the variable @code{mu4e-compose-switch} (see its
+docstring) which you can customize to influence how @t{mu4e} creates new
+messages.
+
+@subsection How can I apply format=flowed to my outgoing messages?
+This enables receiving clients that support this feature to reflow
+paragraphs. Plain text emails with @t{Content-Type: text/plain;
+format=flowed} can be reflowed (i.e. line endings removed, paragraphs
+refilled) by receiving clients that support this standard. Clients
+that don't support this, show them as is, which means this feature is
+truly non-invasive.
+
+Here's an explanatory blog post which also shows why this is a desirable
+feature: @url{https://mathiasbynens.be/notes/gmail-plain-text} (if you don't
+have it, your mails mostly look quite bad especially on mobile devices) and
+here's the @uref{https://www.ietf.org/rfc/rfc2646.txt,RFC with all the details}.
+
+Since version 0.9.17, @t{mu4e} sends emails with @t{format=flowed} by setting
+@lisp
+(setq mu4e-compose-format-flowed t)
+@end lisp
+
+@noindent
+in your Emacs init file (@file{~/.emacs} or @file{~/.emacs.d/init.el}). The
+transformation of your message into the proper format is done at the time of
+sending. For this to happen properly, you should write each paragraph of your
+message of as a long line (i.e. without carriage return). If you introduce
+unwanted newlines in your paragraph, use @kbd{M-q} to reformat it as a single
+line.
+
+If you want to send the message with paragraphs on single lines but
+without @t{format=flowed} (because, say, the receiver does not
+understand the latter as it is the case for Google or Github), use
+@kbd{M-x use-hard-newlines} (to turn @code{use-hard-newlines} off) or
+uncheck the box @t{format=flowed} in the @t{Text} menu when composing a
+message.
+
+@subsection How can I force images to be shown at the end of my messages, regardless of where I insert them?
+User Marcin Borkowski has a solution:
+@lisp
+(defun mml-attach-file--go-to-eob (orig-fun &rest args)
+ "Go to the end of buffer before attaching files."
+ (save-excursion
+ (save-restriction
+ (widen)
+ (goto-char (point-max))
+ (apply orig-fun args))))
+
+(advice-add 'mml-attach-file :around #'mml-attach-file--go-to-eob)
+@end lisp
+
+@subsection How can I avoid Outlook display issues?
+
+Limited testing shows that certain Outlook clients do not work well with inline
+replies, and the entire message including-and-below the first quoted section is
+collapsed. This means recipients may not even notice important inline text,
+especially if there is some top-posted content. This has been observed on OS X,
+Windows, and Web-based Outlook clients accessing Office 365.
+
+It appears the bug is triggered by the standard reply regex "On ...
+wrote:". Changing "On", or removing the trailing ":" appears to fix the
+bug (in limited testing). Therefore, a simple work-around is to set
+`message-citation-line-format` to something slightly non-standard, such
+as:
+@lisp
+(setq message-citation-line-format "On %Y-%m-%d at %R %Z, %f wrote...")
+@end lisp
+
+@node Known issues
+@section Known issues
+
+Although they are not really @emph{questions}, we end this chapter with a list
+of known issues and/or missing features in @t{mu4e}. Thus, users won't have to
+search in vain for things that are not there (yet), and the author can use it as
+a todo-list.
+
+@subsection UTF-8 language environment is required
+@t{mu4e} does not work well if the Emacs language environment is not UTF-8; so,
+if you encounter problems with encodings, be sure to have
+@code{(set-language-environment "UTF-8")} in your @file{~/.emacs} (or its moral
+equivalents in other places).
+
+@subsection Headers-buffer can get mis-aligned
+Due to the way the headers buffer works, it can get misaligned.
+
+For the particular case where the header values are misaligned with the column
+headings, you can try something like the following:
+@lisp
+(add-hook 'mu4e-headers-mode-hook #'my-mu4e-headers-mode-hook)
+(defun my-mu4e-headers-mode-hook ()
+ ;; Account for the fringe and other spacing in the header line.
+ (header-line-indent-mode 1)
+ (push (propertize " " 'display '(space :align-to header-line-indent-width))
+ header-line-format)
+ ;; Ensure `text-scale-adjust' scales the header line with the headers themselves
+ ;; by ensuring the `default' face is in the inheritance hierarchy.
+ (face-remap-add-relative 'header-line '(:inherit (mu4e-header-face default)))
+@end lisp
+
+This does not solve all possible issues; that would require a thorough rework of
+the headers-view, which may happen at some time.
+
+@node Tips and Tricks
+@appendix Tips and Tricks
+
+@menu
+* Fancy characters:: Non-ascii characters in the UI
+* Refiling messages:: Moving message to some archive folder
+* Saving outgoing messages:: Automatically save sent messages
+* Confirmation before sending:: Check messages before sending
+@end menu
+
+@node Fancy characters
+@section Fancy characters
+
+When using `fancy characters' (@code{mu4e-use-fancy-chars}) with the
+@emph{Inconsolata}-font (and likely others as well), the display may be
+slightly off; the reason for this issue is that Inconsolata does not
+contain the glyphs for the `fancy' arrows and the glyphs that are used
+as replacements are too high.
+
+To fix this, you can use something like the following workaround (in
+your @t{.emacs}-file):
+@lisp
+(when (equal window-system 'x)
+ (set-fontset-font "fontset-default" 'unicode "Dejavu Sans Mono")
+ (set-face-font 'default "Inconsolata-10"))
+@end lisp
+
+Other fonts with good support for Unicode are @t{unifont} and
+@t{symbola}.
+
+For a more complete solution, but with greater overhead, you can also
+try the @emph{unicode-fonts} package:
+@lisp
+(require 'unicode-fonts)
+(require 'persistent-soft) ; To cache the fonts and reduce load time
+(unicode-fonts-setup)
+@end lisp
+
+It's possible to customize various header marks as well, with a ``fancy'' and
+``non-fancy'' version (if you cannot see some the ``fancy'' characters, that is
+an indication that the font you are using does not support those characters.
+
+@lisp
+ (setq
+ mu4e-headers-draft-mark '("D" . "💈")
+ mu4e-headers-flagged-mark '("F" . "📍")
+ mu4e-headers-new-mark '("N" . "🔥")
+ mu4e-headers-passed-mark '("P" . "❯")
+ mu4e-headers-replied-mark '("R" . "❮")
+ mu4e-headers-seen-mark '("S" . "☑")
+ mu4e-headers-trashed-mark '("T" . "💀")
+ mu4e-headers-attach-mark '("a" . "📎")
+ mu4e-headers-encrypted-mark '("x" . "🔒")
+ mu4e-headers-signed-mark '("s" . "🔑")
+ mu4e-headers-unread-mark '("u" . "⎕")
+ mu4e-headers-list-mark '("l" . "🔈")
+ mu4e-headers-personal-mark '("p" . "👨")
+ mu4e-headers-calendar-mark '("c" . "📅"))
+@end lisp
+
+@node Refiling messages
+@section Refiling messages
+
+By setting @code{mu4e-refile-folder} to a function, you can dynamically
+determine where messages are to be refiled. If you want to do this based
+on the subject of a message, you can use a function that matches the
+subject against a list of regexes in the following way. First, set up a
+variable @code{my-mu4e-subject-alist} containing regexes plus associated
+mail folders:
+
+@lisp
+(defvar my-mu4e-subject-alist '(("kolloqui\\(um\\|a\\)" . "/Kolloquium")
+ ("Calls" . "/Calls")
+ ("Lehr" . "/Lehre")
+ ("webseite\\|homepage\\|website" . "/Webseite"))
+ "List of subjects and their respective refile folders.")
+@end lisp
+
+Now you can use the following function to automatically refile messages
+based on their subject line:
+
+@lisp
+(defun my-mu4e-refile-folder-function (msg)
+ "Set the refile folder for MSG."
+ (let ((subject (mu4e-message-field msg :subject))
+ (folder (or (cdar (member* subject my-mu4e-subject-alist
+ :test #'(lambda (x y)
+ (string-match (car y) x))))
+ "/General")))
+ folder))
+@end lisp
+
+Note the @t{"/General"} folder: it is the default folder in case the
+subject does not match any of the regexes in
+@code{my-mu4e-subject-alist}.
+
+In order to make this work, you'll of course need to set
+@code{mu4e-refile-folder} to this function:
+
+@lisp
+(setq mu4e-refile-folder 'my-mu4e-refile-folder-function)
+@end lisp
+
+If you have multiple accounts, you can accommodate them as well:
+
+@lisp
+(defun my-mu4e-refile-folder-function (msg)
+ "Set the refile folder for MSG."
+ (let ((maildir (mu4e-message-field msg :maildir))
+ (subject (mu4e-message-field msg :subject))
+ folder)
+ (cond
+ ((string-match "Account1" maildir)
+ (setq folder (or (catch 'found
+ (dolist (mailing-list my-mu4e-mailing-lists)
+ (if (mu4e-message-contact-field-matches
+ msg :to (car mailing-list))
+ (throw 'found (cdr mailing-list)))))
+ "/Account1/General")))
+ ((string-match "Gmail" maildir)
+ (setq folder "/Gmail/All Mail"))
+ ((string-match "Account2" maildir)
+ (setq folder (or (cdar (member* subject my-mu4e-subject-alist
+ :test #'(lambda (x y)
+ (string-match
+ (car y) x))))
+ "/Account2/General"))))
+ folder))
+@end lisp
+
+This function actually uses different methods to determine the refile
+folder, depending on the account: for @emph{Account2}, it uses
+@code{my-mu4e-subject-alist}, for the @emph{Gmail} account it simply uses the
+folder ``All Mail''. For Account1, it uses another method: it files the
+message based on the mailing list to which it was sent. This requires
+another variable:
+
+@lisp
+(defvar my-mu4e-mailing-lists
+ '(("mu-discuss@@googlegroups.com" . "/Account1/mu4e")
+ ("pandoc-discuss@@googlegroups.com" . "/Account1/Pandoc")
+ ("auctex@@gnu.org" . "/Account1/AUCTeX"))
+ "List of mailing list addresses and folders where
+ their messages are saved.")
+@end lisp
+
+@node Saving outgoing messages
+@section Saving outgoing messages
+
+Like @code{mu4e-refile-folder}, the variable @code{mu4e-sent-folder} can
+also be set to a function, in order to dynamically determine the save
+folder. One might, for example, wish to automatically put messages going
+to mailing lists into the trash (because you'll receive them back from
+the list anyway). If you have set up the variable
+@code{my-mu4e-mailing-lists} as mentioned, you can use the following
+function to determine a 'sent'-folder:
+
+@lisp
+(defun my-mu4e-sent-folder-function (msg)
+ "Set the sent folder for the current message."
+ (let ((from-address (message-field-value "From"))
+ (to-address (message-field-value "To")))
+ (cond
+ ((string-match "my.address@@account1.example.com" from-address)
+ (if (member* to-address my-mu4e-mailing-lists
+ :test #'(lambda (x y)
+ (string-match (car y) x)))
+ "/Trash"
+ "/Account1/Sent"))
+ ((string-match "my.address@@gmail.com" from-address)
+ "/Gmail/Sent Mail")
+ (t (mu4e-ask-maildir-check-exists "Save message to maildir: ")))))
+@end lisp
+
+Note that this function doesn't use @code{(mu4e-message-field msg
+:maildir)} to determine which account the message is being sent from.
+The reason is that the function in @code{mu4e-sent-folder} is
+called when you send the message, but before @t{mu4e} has created the
+message struct from the compose buffer, so that
+@code{mu4e-message-field} cannot be used. Instead, the function uses
+@code{message-field-value}, which extracts the values of the headers in
+the compose buffer. This means that it is not possible to extract the
+account name from the message's maildir, so instead the from address is
+used to determine the account.
+
+Again, the function shows three different possibilities: for the first
+account (@t{my.address@@account1.example.com}) it uses
+@code{my-mu4e-mailing-lists} again to determine if the message goes to a
+mailing list. If so, the message is put in the trash folder, if not, it
+is saved in @t{/Account1/Sent}. For the second (Gmail) account, sent
+mail is simply saved in the Sent Mail folder.
+
+If the from address is not associated with Account1 or with the Gmail
+account, the function uses @code{mu4e-ask-maildir-check-exists} to ask
+the user for a maildir to save the message in.
+
+@node Confirmation before sending
+@section Confirmation before sending
+
+To protect yourself from sending messages too hastily, you can add a
+final confirmation, which you can of course make as elaborate as you
+wish.
+
+@lisp
+(defun confirm-empty-subject ()
+ "Require confirmation before sending without subject."
+ (let ((sub (message-field-value "Subject")))
+ (or (and sub (not (string-match "\\`[ \t]*\\'" sub)))
+ (yes-or-no-p "Really send without Subject? ")
+ (keyboard-quit))))
+
+(add-hook 'message-send-hook #'confirm-empty-subject)
+@end lisp
+
+If you @emph{always} want to be asked for for confirmation, set
+@code{message-confirm-send} to non-@t{nil} so the question ``Send message?'' is
+asked for confirmation.
+
+@node How it works
+@appendix How it works
+
+While perhaps not interesting for all users of @t{mu4e}, some curious
+souls may want to know how @t{mu4e} does its job.
+
+@menu
+* High-level overview::How the pieces fit together
+* mu server::The mu process running in the background
+* Reading from the server::Processing responses from the server
+* The message s-expression::What messages look like from the inside
+@end menu
+
+@node High-level overview
+@section High-level overview
+
+At a high level, we can summarize the structure of the @t{mu4e} system using
+some ascii-art:
+
+@cartouche
+@example
+ +---------+
+ | emacs |
+ | +------+
+ +----| mu4e | --> send mail (smtpmail)
+ +------+
+ | A
+ V | ---/ search, view, move mail
+ +---------+ \
+ | mu |
+ +---------+
+ | A
+ V |
+ +---------+
+ | Maildir | <--- receive mail (fetchmail,
+ +---------+ offlineimap, ...)
+@end example
+@end cartouche
+
+In words:
+@itemize
+@item Your e-mail messages are stored in a Maildir-directory
+(typically, @file{~/Maildir} and its subdirectories), and new mail comes in
+using tools like @t{fetchmail}, @t{offlineimap}, or through a local mail
+server.
+@item @t{mu} indexes these messages periodically, so you can quickly search for
+them. @t{mu} can run in a special @t{server}-mode, where it provides services
+ to client software.
+@item @t{mu4e}, which runs inside Emacs is
+ such a client; it communicates with @command{mu} (in its @t{server}-mode) to
+ search for messages, and manipulate them.
+@item @t{mu4e} uses the facilities
+ offered by Emacs (the Gnus message editor and @t{smtpmail}) to send
+ messages.
+@end itemize
+
+@node mu server
+@section @t{mu server}
+
+@t{mu4e} is based on the @t{mu} e-mail searching/indexer. The latter
+is a C++-program; there are different ways to communicate with a
+client that is emacs-based.
+
+One way to implement this, would be to call the @t{mu} command-line
+tool with some parameters and then parse the output. In fact, that was
+the first approach --- @t{mu4e} would invoke e.g., @t{mu find} and
+process the output in Emacs.
+
+However, with this approach, we need to load the entire e-mail
+@emph{Xapian} database (in which the message is stored) for each
+invocation. Wouldn't it be nicer to keep a running @t{mu} instance
+around? Indeed, it would --- and thus, the @t{mu server} sub-command
+was born. Running @t{mu server} starts a simple shell, in which you
+can give commands to @command{mu}, which then spits out the
+results/errors. @command{mu server} is not meant for humans, but it
+can be used manually, which is great for debugging.
+
+@node Reading from the server
+@section Reading from the server
+
+In the design, the next question was what format @t{mu} should use for its
+output for @t{mu4e} (Emacs) to process. Some other programs use
+@abbr{JSON} here, but it seemed easier (and possibly, more efficient) just to
+talk to Emacs in its native language: @emph{s-expressions}, and
+interpret those using the Emacs-function
+@code{read-from-string}. See @ref{The message s-expression} for details on the
+format.
+
+So, now let's look at how we process the data from @t{mu server} in
+Emacs. We'll leave out a lot of details, @t{mu4e}-specifics, and look
+at a bit more generic approach.
+
+The first thing to do is to create a process (for example, with
+@code{start-process}), and then register a filter function for it, which is
+invoked whenever the process has some data for us. Something like:
+
+@lisp
+ (let ((proc (start-process <arguments>)))
+ (set-process-filter proc 'my-process-filter)
+ (set-process-sentinel proc 'my-process-sentinel))
+@end lisp
+
+Note, the process sentinel is invoked when the process is terminated
+--- so there you can clean things up. The function
+@code{my-process-filter} is a user-defined function that takes the
+process and the chunk of output as arguments; in @t{mu4e} it looks
+something like (pseudo-lisp):
+
+@lisp
+(defun my-process-filter (proc str)
+ ;; mu4e-buf: a global string variable to which data gets appended
+ ;; as we receive it
+ (setq mu4e-buf (concat mu4e-buf str))
+ (when <we-have-received-a-full-expression>
+ <eat-expression-from mu4e-buf>
+ <evaluate-expression>))
+@end lisp
+
+@code{<evaluate-expression>} de-multiplexes the s-expression we got.
+For example, if the s-expression looks like an e-mail message header,
+it is processed by the header-handling function, which appends it to
+the header list. If the s-expression looks like an error message, it
+is reported to the user. And so on.
+
+The language between frontend and backend is documented partly in the
+@t{mu-server} man-page and more completely in the output of @t{mu
+server --commands}.
+
+@t{mu4e} can log these communications; you can use @kbd{M-x
+mu4e-toggle-logging} to turn logging on and off, and you can view the
+log using @kbd{M-x mu4e-show-log} (@key{$}).
+
+@node The message s-expression
+@section The message s-expression
+
+As a word of warning, the details of the s-expression are internal to the mu4e -
+mu communications, and are subject to change between versions.
+
+A typical message s-expression looks something like the following:
+
+@lisp
+(:docid 32461
+ :from ((:name "Nikola Tesla" :email "niko@@example.com"))
+ :to ((:name "Thomas Edison" :email "tom@@example.com"))
+ :cc ((:name "Rupert The Monkey" :email "rupert@@example.com"))
+ :subject "RE: what about the 50K?"
+ :date (20369 17624 0)
+ :size 4337
+ :message-id "C8233AB82D81EE81AF0114E4E74@@123213.mail.example.com"
+ :path "/home/tom/Maildir/INBOX/cur/133443243973_1.10027.atlas:2,S"
+ :maildir "/INBOX"
+ :priority normal
+ :flags (seen attach)
+ ....
+")
+@end lisp
+
+This s-expression forms a property list (@t{plist}), and we can get
+values from it using @t{plist-get}; for example @code{(plist-get msg
+:subject)} would get you the message subject. However, it's better to
+use the function @code{mu4e-message-field} to shield you from some of
+the implementation details that are subject to change; and see the other
+convenience functions in @file{mu4e-message.el}.
+
+Some notes on the format:
+@itemize
+@item The address fields are @emph{lists} of @t{plists} of the form
+@code{(:name <name> :email <email>)}, where @t{name} can be @t{nil}.
+@item The date is in format Emacs uses (for example in
+@code{current-time}).@footnote{Emacs 32-bit integers have only 29 bits
+available for the actual number; the other bits are use by Emacs for
+internal purposes. Therefore, we need to split @t{time_t} in two
+numbers.}
+@end itemize
+
+@subsection Example: ping-pong
+
+As an example of the communication between @t{mu4e} and @command{mu},
+let's look at the @t{ping-pong}-sequence. When @t{mu4e} starts, it
+sends a command @t{ping} to the @t{mu server} backend, to learn about
+its version. @t{mu server} then responds with a @t{pong} s-expression
+to provide this information (this is implemented in
+@file{mu-cmd-server.c}).
+
+We start this sequence when @t{mu4e} is invoked (when the program is
+started). It calls @t{mu4e--server-ping}, and registers a (lambda)
+function for @t{mu4e-server-pong-func}, to handle the response.
+
+@verbatim
+-> (ping)
+<-<prefix>(:pong "mu" :props (:version "x.x.x" :doccount 78545))
+@end verbatim
+
+When we receive such a @t{pong} (in @file{mu4e-server.el}), the lambda
+function we registered is called, and it compares the version we got
+from the @t{pong} with the version we expected, and raises an error if
+they differ.
+
+@node Debugging
+@appendix Debugging
+
+As explained in @ref{How it works}, @t{mu4e} communicates with its
+backend (@t{mu server}) by sending commands and receiving responses
+(s-expressions).
+
+For debugging purposes, it can be very useful to see this data. For
+this reason, @t{mu4e} can log all these messages. Note that the
+`protocol' is documented to some extent in the @t{mu-server} manpage.
+
+You can enable (and disable) logging with @kbd{M-x
+mu4e-toggle-logging}. The log-buffer is called @t{*mu4e-log*}, and in
+the @ref{Main view}, @ref{Headers view} and @ref{Message view},
+there's a keybinding @key{$} that takes you there. You can quit it by
+pressing @key{q}.
+
+Logging can be a bit resource-intensive, so you may not want to leave
+it on all the time. By default, the log only maintains the most recent
+1200 lines. @t{mu} itself keeps a log as well, you can find it in
+@t{<MUHOME>/mu.log}, on Unix typically @t{~/.cache/mu/mu.log}.
+
+@node GNU Free Documentation License
+@appendix GNU Free Documentation License
+
+@include fdl.texi
+
+@c @node Command Index
+@c @unnumbered Command and Function Index
+@c @printindex fn
+
+@c @node Variable Index
+@c @unnumbered Variable Index
+@c @printindex vr
+
+@node Concept Index
+@unnumbered Concept Index
+@printindex cp
+
+@bye
+
+@c Local Variables:
+@c coding: utf-8
+@c End:
--- /dev/null
+/*
+ Custom CSS for HTML documents generated with Texinfo's makeinfo.
+ Public domain 2016 sirgazil. All rights waived.
+*/
+
+
+
+/* NATIVE ELEMENTS */
+a:link,
+a:visited {
+ color: #245C8A;
+ text-decoration: none;
+}
+
+a:active,
+a:focus,
+a:hover {
+ text-decoration: underline;
+}
+
+abbr,
+acronym {
+ cursor: help;
+}
+
+blockquote {
+ color: #555753;
+ font-style: oblique;
+ margin: 30px 0px;
+ padding-left: 3em;
+}
+
+body {
+ background-color: white;
+ box-shadow: 0 0 2px gray;
+ box-sizing: border-box;
+ color: #333;
+ font-family: sans-serif;
+ font-size: 16px;
+ margin: 50px auto;
+ max-width: 960px;
+ padding: 50px;
+}
+
+code,
+samp,
+tt,
+var {
+ color: purple;
+ font-size: 0.8em;
+}
+
+div.example,
+div.lisp {
+ margin: 0px;
+}
+
+dl {
+ margin: 3em 0em;
+}
+
+dl dl {
+ margin: 0em;
+}
+
+dt {
+ background-color: #F5F5F5;
+ padding: 0.5em;
+}
+
+h1,
+h2,
+h2.contents-heading,
+h3,
+h4 {
+ padding: 20px 0px 0px 0px;
+ font-weight: normal;
+}
+
+h1 {
+ font-size: 2.4em;
+}
+
+h2 {
+ font-size: 2.2em;
+ font-weight: bold;
+}
+
+h3 {
+ font-size: 1.8em;
+}
+
+h4 {
+ font-size: 1.4em;
+}
+
+hr {
+ background-color: silver;
+ border-style: none;
+ height: 1px;
+ margin: 0px;
+}
+
+html {
+ background-color: #F5F5F5;
+}
+
+img {
+ max-width: 100%;
+}
+
+li {
+ padding: 5px;
+}
+
+pre.display,
+pre.example,
+pre.format,
+pre.lisp,
+pre.verbatim{
+ overflow: auto;
+}
+
+pre.example,
+pre.lisp,
+pre.verbatim {
+ background-color: #2D3743;
+ border-color: #000;
+ border-style: solid;
+ border-width: thin;
+ color: #E1E1E1;
+ font-size: smaller;
+ padding: 1em;
+}
+
+pre.menu-comment {
+ border-color: #E4E4E4;
+ border-bottom-style: solid;
+ border-width: thin;
+ font-family: sans;
+}
+
+table {
+ border-collapse: collapse;
+ margin: 40px 0px;
+}
+
+table.index-cp *,
+table.index-fn *,
+table.index-ky *,
+table.index-pg *,
+table.index-tp *,
+table.index-vr * {
+ background-color: inherit;
+ border-style: none;
+}
+
+td,
+th {
+ border-color: silver;
+ border-style: solid;
+ border-width: thin;
+ padding: 10px;
+}
+
+th {
+ background-color: #F5F5F5;
+}
+/* END NATIVE ELEMENTS */
+
+
+
+/* CLASSES */
+.contents {
+ margin-bottom: 4em;
+}
+
+.float {
+ margin: 3em 0em;
+}
+
+.float-caption {
+ font-size: smaller;
+ text-align: center;
+}
+
+.float > img {
+ display: block;
+ margin: auto;
+}
+
+.footnote {
+ font-size: smaller;
+ margin: 5em 0em;
+}
+
+.footnote h3 {
+ display: inline;
+ font-size: small;
+}
+
+.header {
+ background-color: #F2F2F2;
+ font-size: small;
+ padding: 0.2em 1em;
+}
+
+.key {
+ color: purple;
+ font-size: 0.8em;
+}
+
+.menu * {
+ border-style: none;
+}
+
+.menu td {
+ padding: 0.5em 0em;
+}
+
+.menu td:last-child {
+ width: 60%;
+}
+
+.menu th {
+ background-color: inherit;
+}
+/* END CLASSES */
--- /dev/null
+From: "Bob" <bob@builder.com>
+Subject: CJK 1
+To: "Chase" <chase@ppatrol.org>
+Date: Thu, 18 Nov 2021 08:35:34 +0200
+Message-Id: 112342343e9dfo.fsf@builder.com
+User-Agent: mu4e 1.7.5; emacs 29.0.50
+
+ サーバがダウンしました
+
+https://github.com/djcb/mu/issues/1428
--- /dev/null
+From: "Bob" <bob@builder.com>
+Subject: CJK 2
+To: "Chase" <chase@ppatrol.org>
+Date: Thu, 18 Nov 2021 08:35:34 +0200
+Message-Id: 271r2342343e9dfo.fsf@builder.com
+User-Agent: mu4e 1.7.5; emacs 29.0.50
+
+ スポンサーシップ募集
+
+https://github.com/djcb/mu/issues/1428
--- /dev/null
+From: "Bob" <bob@builder.com>
+Subject: CJK 3
+To: "Chase" <chase@ppatrol.org>
+Date: Thu, 18 Nov 2021 08:35:34 +0200
+Message-Id: 3871r2342343e9dfo.fsf@builder.com
+User-Agent: mu4e 1.7.5; emacs 29.0.50
+
+ サービス開始について
+
+https://github.com/djcb/mu/issues/1428
--- /dev/null
+From: "Bob" <bob@builder.com>
+Subject: CJK 4
+To: "Chase" <chase@ppatrol.org>
+Date: Thu, 18 Nov 2021 08:35:34 +0200
+Message-Id: 4871r2342343e9dfo.fsf@builder.com
+User-Agent: mu4e 1.7.5; emacs 29.0.50
+
+ ショルダーバック
+
+https://github.com/djcb/mu/issues/1428
--- /dev/null
+Return-Path: <gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level:
+X-Spam-Status: No, score=-4.9 required=3.0 tests=BAYES_00,DATE_IN_PAST_96_XX,
+ RCVD_IN_DNSWL_MED autolearn=ham version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+ by mail.xxxxsoftware.nl (Postfix) with ESMTP id 5123469CB3
+ for <xxxx@localhost>; Thu, 7 Aug 2008 08:10:19 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [66.249.91.109]
+ by mindcrime with IMAP (fetchmail-6.3.8)
+ for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:19 +0300 (EEST)
+Received: by 10.142.237.21 with SMTP id k21cs39272wfh; Wed, 6 Aug 2008
+ 20:15:17 -0700 (PDT)
+Received: by 10.65.133.8 with SMTP id k8mr2071878qbn.7.1218078916289; Wed, 06
+ Aug 2008 20:15:16 -0700 (PDT)
+Received: from sourceware.org (sourceware.org [209.132.176.174]) by
+ mx.google.com with SMTP id 28si7904461qbw.0.2008.08.06.20.15.15; Wed, 06 Aug
+ 2008 20:15:16 -0700 (PDT)
+Received-SPF: neutral (google.com: 209.132.176.174 is neither permitted nor
+ denied by domain of gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org)
+ client-ip=209.132.176.174;
+Authentication-Results: mx.google.com; spf=neutral (google.com:
+ 209.132.176.174 is neither permitted nor denied by domain of
+ gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org)
+ smtp.mail=gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org
+Received: (qmail 13493 invoked by alias); 7 Aug 2008 03:15:13 -0000
+Received: (qmail 13485 invoked by uid 22791); 7 Aug 2008 03:15:12 -0000
+Received: from mailgw1a.lmco.com (HELO mailgw1a.lmco.com) (192.31.106.7)
+ by sourceware.org (qpsmtpd/0.31) with ESMTP; Thu, 07 Aug 2008 03:14:27 +0000
+Received: from emss07g01.ems.lmco.com (relay5.ems.lmco.com [166.29.2.16])by
+ mailgw1a.lmco.com (LM-6) with ESMTP id m773EPZH014730for
+ <gcc-help@gcc.gnu.org>; Wed, 6 Aug 2008 21:14:25 -0600 (MDT)
+Received: from CONVERSION2-DAEMON.lmco.com by lmco.com (PMDF V6.3-x14 #31428)
+ id <0K5700601NO18J@lmco.com> for gcc-help@gcc.gnu.org; Wed, 06 Aug 2008
+ 21:14:25 -0600 (MDT)
+Received: from EMSS04I00.us.lmco.com ([166.17.13.135]) by lmco.com (PMDF
+ V6.3-x14 #31428) with ESMTP id <0K5700H5MNNWGX@lmco.com> for
+ gcc-help@gcc.gnu.org; Wed, 06 Aug 2008 21:14:20 -0600 (MDT)
+Received: from EMSS35M06.us.lmco.com ([158.187.107.143]) by
+ EMSS04I00.us.lmco.com with Microsoft SMTPSVC(5.0.2195.6713); Wed, 06 Aug
+ 2008 23:14:20 -0400
+Date: Thu, 31 Jul 2008 14:57:25 -0400
+From: "Mickey Mouse" <anon@example.com>
+Subject: gcc include search order
+To: "Donald Duck" <gcc-help@gcc.gnu.org>
+Message-id: <3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com>
+MIME-version: 1.0
+Content-type: text/plain; charset=us-ascii
+Content-transfer-encoding: 7BIT
+Content-class: urn:content-classes:message
+Mailing-List: contact gcc-help-help@gcc.gnu.org; run by ezmlm
+Precedence: klub
+List-Id: <gcc-help.gcc.gnu.org>
+List-Unsubscribe: <mailto:gcc-help-unsubscribe-xxxx.klub=gmail.com@gcc.gnu.org>
+List-Archive: <http://gcc.gnu.org/ml/gcc-help/>
+List-Post: <mailto:gcc-help@gcc.gnu.org>
+List-Help: <mailto:gcc-help-help@gcc.gnu.org>
+Sender: gcc-help-owner@gcc.gnu.org
+Delivered-To: mailing list gcc-help@gcc.gnu.org
+Content-Length: 3024
+
+
+Hi.
+In my unit testing I need to change some header files (target is
+vxWorks, which supports some things that the sun does not).
+So, what I do is fetch the development tree, and then in a new unit test
+directory I attempt to compile the unit under test. Since this is NOT
+vxworks, I use sed to change some of the .h files and put them in a
+./changed directory.
+
+When I try to compile the file, it is still using the .h file from the
+original location, even though I have listed the include path for
+./changed before the include path for the development tree.
+
+Here is a partial output from gcc using the -v option
+
+GNU CPP version 3.1 (cpplib) (sparc ELF)
+GNU C++ version 3.1 (sparc-sun-solaris2.8)
+ compiled by GNU C version 3.1.
+ignoring nonexistent directory "NONE/include"
+#include "..." search starts here:
+#include <...> search starts here:
+ .
+ changed
+ /export/home4/xxx/yyyy/builds/int_rel5_latest/src/mp/interface
+ /export/home4/xxx/yyyy/builds/int_rel5_latest/src/ap/app
+ /export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common
+ /export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/interface
+ /usr/local/include/g++-v3
+ /usr/local/include/g++-v3/sparc-sun-solaris2.8
+ /usr/local/include/g++-v3/backward
+ /usr/local/include
+ /usr/local/lib/gcc-lib/sparc-sun-solaris2.8/3.1/include
+ /usr/local/sparc-sun-solaris2.8/include
+ /usr/include
+End of search list.
+
+I know the changed file is correct and that the include is not working
+as expected, because when I copy the file from ./changed, back into the
+development tree, the compilation works as expected.
+
+One more bit of information. The source that I cam compiling is in
+/export/home4/xxx/yyyy/builds/int_rel5_latest/src/ap/app
+And it is including files from
+/export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common
+These include files should be including the files from ./changed (when
+they exist) but they are ignoring the .h files in the ./changed
+directory and are instead using other, unchanged files in the
+/export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common
+directory.
+
+The gcc command line is something like
+
+ TEST_DIR="."
+
+ CHANGED_DIR_NAME=changed
+ CHANGED_FILES_DIR=${TEST_DIR}/${CHANGED_DIR_NAME}
+
+ CICU_HEADER_FILES="-I ${AP_INTERFACE_FILES} -I ${AP_APP_FILES} -I
+${SHARED_COMMON_FILES} -I ${SHARED_INTERFACE_FILES}"
+
+ HEADERS="-I ./ -I ${CHANGED_FILES_DIR} ${CICU_HEADER_FILES}"
+ DEFINES="-DSUNRUN -DA10_DEBUG -DJOETEST"
+
+ CFLAGS="-v -c -g -O1 -pipe -Wformat -Wunused -Wuninitialized -Wshadow
+-Wmissing-prototypes -Wmissing-declarations"
+
+ printf "Compiling the UUT File\n"
+ gcc -fprofile-arcs -ftest-coverage ${CFLAGS} ${HEADERS} ${DEFINES}
+${AP_APP_FILES}/unitUnderTest.cpp
+
+
+I hope this explanation is clear. If anyone knows how to fix the command
+line so that it gets the .h files in the "changed" directory are used
+instead of files in the other include directories.
+
+Thanks
+Joe
+
+----------------------------------------------------
+Time Flies like an Arrow. Fruit Flies like a Banana
+
+
--- /dev/null
+Return-Path: <sqlite-dev-bounces@sqlite.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level:
+X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00,HTML_MESSAGE
+ autolearn=ham version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+ by mail.xxxxsoftware.nl (Postfix) with ESMTP id D724F6963B
+ for <xxxx@localhost>; Mon, 4 Aug 2008 21:49:27 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [72.14.221.111]
+ by mindcrime with IMAP (fetchmail-6.3.8)
+ for <xxxx@localhost> (single-drop); Mon, 04 Aug 2008 21:49:27 +0300 (EEST)
+Received: by 10.142.51.12 with SMTP id y12cs86537wfy; Mon, 4 Aug 2008 00:38:51
+ -0700 (PDT)
+Received: by 10.151.113.5 with SMTP id q5mr272266ybm.37.1217835529913; Mon, 04
+ Aug 2008 00:38:49 -0700 (PDT)
+Received: from sqlite.org (sqlite.org [67.18.92.124]) by mx.google.com with
+ ESMTP id 5si5754915ywd.8.2008.08.04.00.38.30; Mon, 04 Aug 2008 00:38:50 -0700
+ (PDT)
+Received-SPF: pass (google.com: best guess record for domain of
+ sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as permitted sender)
+ client-ip=67.18.92.124;
+Authentication-Results: mx.google.com; spf=pass (google.com: best guess record
+ for domain of sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as
+ permitted sender) smtp.mail=sqlite-dev-bounces@sqlite.org
+Received: from sqlite.org (localhost [127.0.0.1]) by sqlite.org (Postfix) with
+ ESMTP id 765A511C46; Mon, 4 Aug 2008 03:38:27 -0400 (EDT)
+X-Original-To: sqlite-dev@sqlite.org
+Delivered-To: sqlite-dev@sqlite.org
+Received: from ik-out-1112.google.com (ik-out-1112.google.com [66.249.90.176])
+ by sqlite.org (Postfix) with ESMTP id 4C59511C41 for <sqlite-dev@sqlite.org>;
+ Mon, 4 Aug 2008 03:38:23 -0400 (EDT)
+Received: by ik-out-1112.google.com with SMTP id b32so2163423ika.0 for
+ <sqlite-dev@sqlite.org>; Mon, 04 Aug 2008 00:38:23 -0700 (PDT)
+Received: by 10.210.54.19 with SMTP id c19mr14589042eba.107.1217835502549;
+ Mon, 04 Aug 2008 00:38:22 -0700 (PDT)
+Received: by 10.210.115.10 with HTTP; Mon, 4 Aug 2008 00:38:22 -0700 (PDT)
+Message-ID: <477821040808040038s381bf382p7411451e3c1a2e4e@mail.gmail.com>
+Date: Mon, 4 Aug 2008 10:38:22 +0300
+From: anon@example.com
+To: sqlite-dev@sqlite.org
+In-Reply-To: <73d4fc50808030747g303a170ieac567723c2d4f24@mail.gmail.com>
+MIME-Version: 1.0
+References: <477821040808030533y41f1501dq32447b568b6e6ca5@mail.gmail.com>
+ <73d4fc50808030747g303a170ieac567723c2d4f24@mail.gmail.com>
+Subject: Re: [sqlite-dev] SQLite exception A&B
+X-BeenThere: sqlite-dev@sqlite.org
+X-Mailman-Version: 2.1.9
+Priority: normal
+Reply-To: sqlite-dev@sqlite.org
+List-Id: <sqlite-dev.sqlite.org>
+List-Unsubscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>,
+ <mailto:sqlite-dev-request@sqlite.org?subject=unsubscribe>
+List-Archive: <http://sqlite.org:8080/cgi-bin/mailman/private/sqlite-dev>
+List-Post: <mailto:sqlite-dev@sqlite.org>
+List-Help: <mailto:sqlite-dev-request@sqlite.org?subject=help>
+List-Subscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>,
+ <mailto:sqlite-dev-request@sqlite.org?subject=subscribe>
+Content-Type: multipart/mixed; boundary="===============2123623832=="
+Mime-version: 1.0
+Sender: sqlite-dev-bounces@sqlite.org
+Errors-To: sqlite-dev-bounces@sqlite.org
+Content-Length: 8475
+
+--===============2123623832==
+Content-Type: multipart/alternative;
+ boundary="----=_Part_29556_25702991.1217835502493"
+
+------=_Part_29556_25702991.1217835502493
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+Hi Grant,
+
+Thanks for your reply.
+I am using a different session for each thread, whenever a thread wishes to
+access the database it gets a session from the session pool and works with
+that session until its work is done.
+
+Most of the actions the threads are doing on the database are quite
+complicated and are required to be fully committed or completely ignored, so
+yes, I am (most of the time) explicitly beginning and committing my
+transactions.
+
+Regarding the SQLiteStatementImpl, I believe the Poco manual explains that
+sessions and statements for that matter cannot be shared between threads,
+therefore if you are using a session via one thread only it should work
+fine.
+
+My first impression was that the problem was in the Poco infrastructure (I
+have found several Poco related bugs in the past), but the problem ALWAYS
+occurs when I perform the "BEGIN IMMEDIATE" action, if it were a Poco
+related bug, I would expect to see it here and there without any relation to
+this specific statement, but that is not the case.
+
+None the less, I will also post my question on the Poco forums.
+
+Nadav.
+
+On Sun, Aug 3, 2008 at 5:47 PM, Grant Gatchel <grant.gatchel@gmail.com>wrote:
+
+> Are you using the same Poco::Session for every thread or does each call
+> create a new session/handle to the database?
+>
+> Are you explicitly BEGINning and COMMITting your transactions?
+>
+> In looking at the 1.3.2 branch of Poco::Data::SQLite, there appears to be a
+> race condition in the SQLiteStatementImpl::next() method in which the member
+> _nextResponse is being accessed before the SQLiteStatementImpl::hasNext()
+> method has a chance to interpret that value and throw an exception.
+>
+> This question might be more suitable in the Poco forums or mailinglist.
+>
+> - Grant
+>
+> On Sun, Aug 3, 2008 at 8:33 AM, nadav g <nadav.gr@gmail.com> wrote:
+>
+>> Hi All,
+>>
+>> I have been using SQLite with Poco (www.appinf.com) as my infrastructure.
+>> The program is running several threads that access this database very
+>> often and are synchronized by SQLite itself.
+>> Everything seems to work just fine most of time (usually days - weeks) but
+>> I do get an occasional exception:
+>>
+>> Exception: SQL error or missing database: Iterator Error: trying to check
+>> if there is a next value
+>>
+>> The backtrace leads to this statement:
+>> *"BEGIN IMMEDIATE"*
+>>
+>> This specific code runs numerous times before an exception occurs (if
+>> occurs at all) and I cannot think of any reason for it to fail later rather
+>> than sooner.
+>> It is pretty obvious that this situation occurs due to some rare thread
+>> state, but I could not find any information that gives me any hint as to
+>> what this state might be.
+>>
+>> So what I am asking is:
+>> 1) Does anyone know why this sort of exception occurs?
+>> 2) Can anyone think of a reason for such an exception to occur in the
+>> situation I have described?
+>>
+>> Thanks in advance,
+>> Nadav.
+>>
+>>
+>> _______________________________________________
+>> sqlite-dev mailing list
+>> sqlite-dev@sqlite.org
+>> http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev
+>>
+>>
+>
+> _______________________________________________
+> sqlite-dev mailing list
+> sqlite-dev@sqlite.org
+> http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev
+>
+>
+
+------=_Part_29556_25702991.1217835502493
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+<div dir="ltr">Hi Grant,<br><br>Thanks for your reply.<br>I am using a different session for each thread, whenever a thread wishes to access the database it gets a session from the session pool and works with that session until its work is done.<br>
+<br>Most of the actions the threads are doing on the database are quite complicated and are required to be fully committed or completely ignored, so yes, I am (most of the time) explicitly beginning and committing my transactions.<br>
+<br>Regarding the SQLiteStatementImpl, I believe the Poco manual explains that sessions and statements for that matter cannot be shared between threads, therefore if you are using a session via one thread only it should work fine.<br>
+<br>My first impression was that the problem was in the Poco infrastructure (I have found several Poco related bugs in the past), but the problem ALWAYS occurs when I perform the "BEGIN IMMEDIATE" action, if it were a Poco related bug, I would expect to see it here and there without any relation to this specific statement, but that is not the case.<br>
+<br>None the less, I will also post my question on the Poco forums.<br><br>Nadav.<br><br><div class="gmail_quote">On Sun, Aug 3, 2008 at 5:47 PM, Grant Gatchel <span dir="ltr"><<a href="mailto:grant.gatchel@gmail.com">grant.gatchel@gmail.com</a>></span> wrote:<br>
+<blockquote class="gmail_quote" style="border-left: 1px solid rgb(204, 204, 204); margin: 0pt 0pt 0pt 0.8ex; padding-left: 1ex;"><div dir="ltr">Are you using the same Poco::Session for every thread or does each call create a new session/handle to the database?<br>
+<br>Are you explicitly BEGINning and COMMITting your transactions?<br><br>In looking at the 1.3.2 branch of Poco::Data::SQLite, there appears to be a race condition in the SQLiteStatementImpl::next() method in which the member _nextResponse is being accessed before the SQLiteStatementImpl::hasNext() method has a chance to interpret that value and throw an exception.<br>
+
+<br>This question might be more suitable in the Poco forums or mailinglist.<br><br>- Grant<br>
+<br><div class="gmail_quote"><div><div></div><div class="Wj3C7c">
+On Sun, Aug 3, 2008 at 8:33 AM, nadav g <span dir="ltr"><<a href="http://nadav.gr" target="_blank">nadav.gr</a>@<a href="http://gmail.com" target="_blank">gmail.com</a>></span> wrote:<br></div></div><blockquote class="gmail_quote" style="border-left: 1px solid rgb(204, 204, 204); margin: 0pt 0pt 0pt 0.8ex; padding-left: 1ex;">
+<div><div></div><div class="Wj3C7c">
+
+
+<div dir="ltr">Hi All,<br><br>I have been using SQLite with Poco (<a href="http://www.appinf.com" target="_blank">www.appinf.com</a>) as my infrastructure.<br>The program is running several threads that access this database very often and are synchronized by SQLite itself.<br>
+
+
+
+
+Everything seems to work just fine most of time (usually days - weeks) but I do get an occasional exception:<br><br>Exception: SQL error or missing database: Iterator Error: trying to check if there is a next value<br><br>
+
+
+
+
+The backtrace leads to this statement:<br><b>"BEGIN IMMEDIATE"</b><br><br>This specific code runs numerous times before an exception occurs (if occurs at all) and I cannot think of any reason for it to fail later rather than sooner.<br>
+
+
+
+
+It is pretty obvious that this situation occurs due to some rare thread state, but I could not find any information that gives me any hint as to what this state might be.<br><br>So what I am asking is:<br>1) Does anyone know why this sort of exception occurs?<br>
+
+
+
+
+2) Can anyone think of a reason for such an exception to occur in the situation I have described?<br><br>Thanks in advance,<br>Nadav.<br><br></div>
+<br></div></div>_______________________________________________<br>
+sqlite-dev mailing list<br>
+<a href="mailto:sqlite-dev@sqlite.org" target="_blank">sqlite-dev@sqlite.org</a><br>
+<a href="http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev" target="_blank">http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev</a><br>
+<br></blockquote></div><br></div>
+<br>_______________________________________________<br>
+sqlite-dev mailing list<br>
+<a href="mailto:sqlite-dev@sqlite.org">sqlite-dev@sqlite.org</a><br>
+<a href="http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev" target="_blank">http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev</a><br>
+<br></blockquote></div><br></div>
+
+------=_Part_29556_25702991.1217835502493--
+
+--===============2123623832==
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+sqlite-dev mailing list
+sqlite-dev@sqlite.org
+http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev
+
+--===============2123623832==--
+
--- /dev/null
+Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level:
+X-Spam-Status: No, score=-3.6 required=3.0 tests=BAYES_00,RCVD_IN_DNSWL_LOW,
+ SPF_PASS,WHOIS_NETSOLPR autolearn=ham version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+ by mail.xxxxsoftware.nl (Postfix) with ESMTP id 1A6CD69CB6
+ for <xxxx@localhost>; Tue, 12 Aug 2008 21:42:38 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [72.14.221.109]
+ by mindcrime with IMAP (fetchmail-6.3.8)
+ for <xxxx@localhost> (single-drop); Tue, 12 Aug 2008 21:42:38 +0300 (EEST)
+Received: by 10.142.237.21 with SMTP id k21cs123119wfh; Sun, 10 Aug 2008
+ 22:06:31 -0700 (PDT)
+Received: by 10.100.166.10 with SMTP id o10mr9327844ane.0.1218431190107; Sun,
+ 10 Aug 2008 22:06:30 -0700 (PDT)
+Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com
+ with ESMTP id c29si10110392anc.13.2008.08.10.22.06.29; Sun, 10 Aug 2008
+ 22:06:30 -0700 (PDT)
+Received-SPF: pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender) client-ip=199.232.76.165;
+Authentication-Results: mx.google.com; spf=pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender)
+ smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Received: from localhost ([127.0.0.1]:45637 helo=lists.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KSPbx-0006dj-96 for
+ xxxx.klub@gmail.com; Mon, 11 Aug 2008 01:06:29 -0400
+Received: from mailman by lists.gnu.org with tmda-scanned (Exim 4.43) id
+ 1KSPbE-0006cQ-Nd for help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 01:05:44 -0400
+Received: from exim by lists.gnu.org with spam-scanned (Exim 4.43) id
+ 1KSPbD-0006bs-Px for help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 01:05:44 -0400
+Received: from [199.232.76.173] (port=37426 helo=monty-python.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KSPbD-0006bk-HT for
+ help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 01:05:43 -0400
+Received: from main.gmane.org ([80.91.229.2]:46446 helo=ciao.gmane.org) by
+ monty-python.gnu.org with esmtps (TLS-1.0:RSA_AES_256_CBC_SHA1:32) (Exim
+ 4.60) (envelope-from <geh-help-gnu-emacs@m.gmane.org>) id 1KSPbD-0003Kl-CA
+ for help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 01:05:43 -0400
+Received: from list by ciao.gmane.org with local (Exim 4.43) id
+ 1KSPb9-00080r-CX for help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 05:05:39 +0000
+Received: from bas2-toronto63-1088792724.dsl.bell.ca ([64.229.168.148]) by
+ main.gmane.org with esmtp (Gmexim 0.1 (Debian)) id 1AlnuQ-0007hv-00 for
+ <help-gnu-emacs@gnu.org>; Mon, 11 Aug 2008 05:05:39 +0000
+Received: from cpchan by bas2-toronto63-1088792724.dsl.bell.ca with local
+ (Gmexim 0.1 (Debian)) id 1AlnuQ-0007hv-00 for <help-gnu-emacs@gnu.org>; Mon,
+ 11 Aug 2008 05:05:39 +0000
+X-Injected-Via-Gmane: http://gmane.org/
+To: help-gnu-emacs@gnu.org
+From: anon@example.com
+Date: Mon, 11 Aug 2008 01:03:22 -0400
+Organization: Linux Private Site
+Message-ID: <87bq00nnxh.fsf@MagnumOpus.Mercurius>
+References: <877iav5s49.fsf@163.com> <86hc9yc5sj.fsf@timbral.net>
+ <877iat7udd.fsf@163.com> <87fxphcsxi.fsf@lion.rapttech.com.au>
+ <8504ddd4-5e3b-4ed5-bf77-aa9cce81b59a@1g2000pre.googlegroups.com>
+ <87k5es59we.fsf@lion.rapttech.com.au>
+ <63c824e3-62b1-4a93-8fa8-2813e1f9397f@v13g2000pro.googlegroups.com>
+ <874p5vsgg8.fsf@nonospaz.fatphil.org>
+ <8250972e-1886-4021-80bc-376e34881c80@v39g2000pro.googlegroups.com>
+ <87zlnnqvvs.fsf@nonospaz.fatphil.org>
+ <57add0e0-b39d-4c71-8d2c-d3b9ddfaa1a9@1g2000pre.googlegroups.com>
+ <87sktfnz5p.fsf@atthis.clsnet.nl>
+ <562e1111-d9e7-4b6a-b661-3f9af13fea17@b30g2000prf.googlegroups.com>
+ <87d4khoq97.fsf@atthis.clsnet.nl>
+ <0fe404c5-cab8-4692-8a27-532e737a7813@i24g2000prf.googlegroups.com>
+Mime-Version: 1.0
+Content-Type: multipart/signed; boundary="=-=-="; micalg=pgp-sha1;
+ protocol="application/pgp-signature"
+X-Complaints-To: usenet@ger.gmane.org
+X-Gmane-NNTP-Posting-Host: bas2-toronto63-1088792724.dsl.bell.ca
+X-Face: G;
+ Z,`sm>)4t4LB/GUrgH$W`!AmfHMj,LG)Z}X0ax@s9:0>0)B&@vcm{v-le)wng)?|o]D<V6&ay<F=H{M5?$T%p!dPdJeF,au\E@TA"v22K!Zl\\mzpU4]6$ZnAI3_L)h;
+ fpd}mn2py/7gv^|*85-D_f:07cT>\Z}0:6X
+User-Agent: Gnus/5.110011 (No Gnus v0.11) Emacs/23.0.60 (gnu/linux)
+Cancel-Lock: sha1:IKyfrl5drOw6HllHFSmWHAKEeC8=
+X-detected-kernel: by monty-python.gnu.org: Linux 2.6, seldom 2.4 (older, 4)
+Subject: Re: Can anybody tell me how to send HTML-format mail in gnus
+X-BeenThere: help-gnu-emacs@gnu.org
+X-Mailman-Version: 2.1.5
+Precedence: list
+List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org>
+List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe>
+List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs>
+List-Post: <mailto:help-gnu-emacs@gnu.org>
+List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help>
+List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe>
+Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Content-Length: 1229
+Lines: 36
+
+--=-=-=
+Content-Type: text/plain
+
+Xah <xahlee@gmail.com> writes:
+
+> So, i was reading about it in Wikipedia. Although i don't have a TV,
+> and haven't had since 2000, but i still enjoyed the festive spirits
+> anyhow. After all, i'm Chinese by blood. So, in my wandering, i ran
+> into this welcome song on youtube:
+>
+> http://www.youtube.com/watch?v=1HEndNYVhZo
+
+What is your point? Your email is in plain text and I can click on the
+link just fine- it is not exactly rocket science to implement parsing of
+URL's to workable links in an Email program (a lot of programs does
+that, including Gnus). Images can be included inline if you want. Also
+mail markups such as *this*, **this** and _this_ have been around since
+the Usenet days and displayed appropriately by a number of mailers. Like
+others have said, most html messages that I have seen either contains
+useless information, or are plain spam and can introduce a host of
+security problems in some mailers.
+
+Charles
+
+
+--=-=-=
+Content-Type: application/pgp-signature
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v2.0.4-svn0 (GNU/Linux)
+
+iD8DBQFIn8gm3epPyyKbwPYRApbvAKDRirXwzMzI+NHV77+QcP3EgTPaCgCfb/6m
+GtNVKdYAeftaYm1nwRVoCDA=
+=ULo3
+-----END PGP SIGNATURE-----
+--=-=-=--
+
+
+
--- /dev/null
+Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level:
+X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham
+ version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+ by mail.xxxxsoftware.nl (Postfix) with ESMTP id C4D6569CB3
+ for <xxxx@localhost>; Thu, 7 Aug 2008 08:10:08 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [66.249.91.109]
+ by mindcrime with IMAP (fetchmail-6.3.8)
+ for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:08 +0300 (EEST)
+Received: by 10.142.237.21 with SMTP id k21cs34794wfh; Wed, 6 Aug 2008
+ 13:40:29 -0700 (PDT)
+Received: by 10.100.33.13 with SMTP id g13mr1093301ang.79.1218055228418; Wed,
+ 06 Aug 2008 13:40:28 -0700 (PDT)
+Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com
+ with ESMTP id d19si15908789and.17.2008.08.06.13.40.27; Wed, 06 Aug 2008
+ 13:40:28 -0700 (PDT)
+Received-SPF: pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender) client-ip=199.232.76.165;
+Authentication-Results: mx.google.com; spf=pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender)
+ smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Received: from localhost ([127.0.0.1]:56316 helo=lists.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KQpo3-0007Pc-Qk for
+ xxxx.klub@gmail.com; Wed, 06 Aug 2008 16:40:27 -0400
+From: anon@example.com
+Newsgroups: gnu.emacs.help
+Date: Wed, 6 Aug 2008 20:38:35 +0100
+Message-ID: <r6bpm5-6n6.ln1@news.ducksburg.com>
+References: <55dbm5-qcl.ln1@news.ducksburg.com>
+ <mailman.15710.1217599959.18990.help-gnu-emacs@gnu.org>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+X-Trace: individual.net bABVU1hcJwWAuRwe/097AAoOXnGGeYR8G1In635iFGIyfDLPUv
+X-Orig-Path: news.ducksburg.com!news
+Cancel-Lock: sha1:wK7dsPRpNiVxpL/SfvmNzlvUR94=
+ sha1:oepBoM0tJBLN52DotWmBBvW5wbg=
+User-Agent: slrn/pre0.9.9-120/mm/ao (Ubuntu Hardy)
+Path: news.stanford.edu!headwall.stanford.edu!newshub.sdsu.edu!feeder.erje.net!proxad.net!feeder1-2.proxad.net!feed.ac-versailles.fr!fu-berlin.de!uni-berlin.de!individual.net!not-for-mail
+Xref: news.stanford.edu gnu.emacs.help:160868
+To: help-gnu-emacs@gnu.org
+Subject: Re: Learning LISP; Scheme vs elisp.
+X-BeenThere: help-gnu-emacs@gnu.org
+X-Mailman-Version: 2.1.5
+Precedence: list
+List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org>
+List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe>
+List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs>
+List-Post: <mailto:help-gnu-emacs@gnu.org>
+List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help>
+List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe>
+Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Content-Length: 417
+Lines: 11
+
+On 2008-08-01, Thien-Thi Nguyen wrote:
+
+> warriors attack, felling foe after foe,
+> few growing old til they realize: to know
+> what deceit is worth deflection;
+> such receipt reversed rejection!
+> then their heavy arms, e'er transformed to shields:
+> balanced hooked charms, ploughed deep, rich yields.
+
+Aha: the exercise for the reader is to place the parens correctly.
+Might take me a while to solve this puzzle.
+
--- /dev/null
+Return-Path: <sqlite-dev-bounces@sqlite.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level:
+X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham
+ version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+ by mail.xxxxsoftware.nl (Postfix) with ESMTP id 32F276963F
+ for <xxxx@localhost>; Mon, 4 Aug 2008 21:49:34 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [72.14.221.111]
+ by mindcrime with IMAP (fetchmail-6.3.8)
+ for <xxxx@localhost> (single-drop); Mon, 04 Aug 2008 21:49:34 +0300 (EEST)
+Received: by 10.142.51.12 with SMTP id y12cs89397wfy; Mon, 4 Aug 2008 02:41:16
+ -0700 (PDT)
+Received: by 10.150.156.20 with SMTP id d20mr963580ybe.104.1217842875596; Mon,
+ 04 Aug 2008 02:41:15 -0700 (PDT)
+Received: from sqlite.org (sqlite.org [67.18.92.124]) by mx.google.com with
+ ESMTP id 6si3605185ywi.1.2008.08.04.02.40.57; Mon, 04 Aug 2008 02:41:15 -0700
+ (PDT)
+Received-SPF: pass (google.com: best guess record for domain of
+ sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as permitted sender)
+ client-ip=67.18.92.124;
+Authentication-Results: mx.google.com; spf=pass (google.com: best guess record
+ for domain of sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as
+ permitted sender) smtp.mail=sqlite-dev-bounces@sqlite.org
+Received: from sqlite.org (localhost [127.0.0.1]) by sqlite.org (Postfix) with
+ ESMTP id 7147F11C45; Mon, 4 Aug 2008 05:40:55 -0400 (EDT)
+X-Original-To: sqlite-dev@sqlite.org
+Delivered-To: sqlite-dev@sqlite.org
+Received: from relay00.pair.com (relay00.pair.com [209.68.5.9]) by sqlite.org
+ (Postfix) with SMTP id B5F901192C for <sqlite-dev@sqlite.org>; Mon, 4 Aug
+ 2008 05:40:52 -0400 (EDT)
+Received: (qmail 59961 invoked from network); 4 Aug 2008 09:40:50 -0000
+Received: from unknown (HELO ?192.168.0.17?) (unknown) by unknown with SMTP; 4
+ Aug 2008 09:40:50 -0000
+X-pair-Authenticated: 87.13.75.164
+Message-Id: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net>
+From: anon@example.com
+To: sqlite-dev@sqlite.org
+Mime-Version: 1.0 (Apple Message framework v926)
+Date: Mon, 4 Aug 2008 11:40:49 +0200
+X-Mailer: Apple Mail (2.926)
+Subject: [sqlite-dev] VM optimization inside sqlite3VdbeExec
+X-BeenThere: sqlite-dev@sqlite.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+Reply-To: sqlite-dev@sqlite.org
+List-Id: <sqlite-dev.sqlite.org>
+List-Unsubscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>,
+ <mailto:sqlite-dev-request@sqlite.org?subject=unsubscribe>
+List-Archive: <http://sqlite.org:8080/cgi-bin/mailman/private/sqlite-dev>
+List-Post: <mailto:sqlite-dev@sqlite.org>
+List-Help: <mailto:sqlite-dev-request@sqlite.org?subject=help>
+List-Subscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>,
+ <mailto:sqlite-dev-request@sqlite.org?subject=subscribe>
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Sender: sqlite-dev-bounces@sqlite.org
+Errors-To: sqlite-dev-bounces@sqlite.org
+Content-Length: 639
+
+Inside sqlite3VdbeExec there is a very big switch statement.
+In order to increase performance with few modifications to the
+original code, why not use this technique ?
+http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html
+
+With a properly defined "instructions" array, instead of the switch
+statement you can use something like:
+goto * instructions[pOp->opcode];
+---
+Marco Bambini
+http://www.sqlabs.net
+http://www.sqlabs.net/blog/
+http://www.sqlabs.net/realsqlserver/
+
+
+
+_______________________________________________
+sqlite-dev mailing list
+sqlite-dev@sqlite.org
+http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev
+
--- /dev/null
+Return-Path: <sqlite-dev-bounces@sqlite.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level:
+X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham
+ version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+ by mail.xxxxsoftware.nl (Postfix) with ESMTP id 3EBAB6963B
+ for <xxxx@localhost>; Mon, 4 Aug 2008 21:49:35 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [72.14.221.111]
+ by mindcrime with IMAP (fetchmail-6.3.8)
+ for <xxxx@localhost> (single-drop); Mon, 04 Aug 2008 21:49:35 +0300 (EEST)
+Received: by 10.142.51.12 with SMTP id y12cs89536wfy; Mon, 4 Aug 2008 02:48:56
+ -0700 (PDT)
+Received: by 10.150.134.21 with SMTP id h21mr7950048ybd.181.1217843335665;
+ Mon, 04 Aug 2008 02:48:55 -0700 (PDT)
+Received: from sqlite.org (sqlite.org [67.18.92.124]) by mx.google.com with
+ ESMTP id 6si5897081ywi.1.2008.08.04.02.48.35; Mon, 04 Aug 2008 02:48:55 -0700
+ (PDT)
+Received-SPF: pass (google.com: best guess record for domain of
+ sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as permitted sender)
+ client-ip=67.18.92.124;
+Authentication-Results: mx.google.com; spf=pass (google.com: best guess record
+ for domain of sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as
+ permitted sender) smtp.mail=sqlite-dev-bounces@sqlite.org
+Received: from sqlite.org (localhost [127.0.0.1]) by sqlite.org (Postfix) with
+ ESMTP id ED01611C4E; Mon, 4 Aug 2008 05:48:31 -0400 (EDT)
+X-Original-To: sqlite-dev@sqlite.org
+Delivered-To: sqlite-dev@sqlite.org
+Received: from mx0.security.ro (mx0.security.ro [80.96.72.194]) by sqlite.org
+ (Postfix) with ESMTP id EB3F51192C for <sqlite-dev@sqlite.org>; Mon, 4 Aug
+ 2008 05:48:28 -0400 (EDT)
+Received: (qmail 348 invoked from network); 4 Aug 2008 12:48:03 +0300
+Received: from dev.security.ro (HELO ?192.168.1.70?) (192.168.1.70) by
+ mx0.security.ro with SMTP; 4 Aug 2008 12:48:03 +0300
+Message-ID: <4896D06A.8000901@security.ro>
+Date: Mon, 04 Aug 2008 12:48:26 +0300
+From: anon@example.com
+User-Agent: Thunderbird 2.0.0.16 (Windows/20080708)
+MIME-Version: 1.0
+To: sqlite-dev@sqlite.org
+References: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net>
+In-Reply-To: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net>
+Content-Type: multipart/mixed; boundary="------------000207070200050102060301"
+X-BitDefender-Scanner: Clean, Agent: BitDefender qmail 2.0.0 on
+ mx0.security.ro
+X-BitDefender-Spam: No (0)
+X-BitDefender-SpamStamp: v1, whitelisted, total: 0
+Subject: Re: [sqlite-dev] VM optimization inside sqlite3VdbeExec
+X-BeenThere: sqlite-dev@sqlite.org
+X-Mailman-Version: 2.1.9
+Precedence: high
+Reply-To: sqlite-dev@sqlite.org
+List-Id: <sqlite-dev.sqlite.org>
+List-Unsubscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>,
+ <mailto:sqlite-dev-request@sqlite.org?subject=unsubscribe>
+List-Archive: <http://sqlite.org:8080/cgi-bin/mailman/private/sqlite-dev>
+List-Post: <mailto:sqlite-dev@sqlite.org>
+List-Help: <mailto:sqlite-dev-request@sqlite.org?subject=help>
+List-Subscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>,
+ <mailto:sqlite-dev-request@sqlite.org?subject=subscribe>
+Sender: sqlite-dev-bounces@sqlite.org
+Errors-To: sqlite-dev-bounces@sqlite.org
+Content-Length: 2212
+
+This is a multi-part message in MIME format.
+--------------000207070200050102060301
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+
+Marco Bambini wrote:
+> Inside sqlite3VdbeExec there is a very big switch statement.
+> In order to increase performance with few modifications to the
+> original code, why not use this technique ?
+> http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html
+>
+> With a properly defined "instructions" array, instead of the switch
+> statement you can use something like:
+> goto * instructions[pOp->opcode];
+> ---
+> Marco Bambini
+> http://www.sqlabs.net
+> http://www.sqlabs.net/blog/
+> http://www.sqlabs.net/realsqlserver/
+>
+>
+>
+> _______________________________________________
+> sqlite-dev mailing list
+> sqlite-dev@sqlite.org
+> http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev
+>
+All the world's not a VAX. This technique is GCC-specific. The SQLite
+source must be as portable as possible thus tying it to a specific
+compiler is out of the question. While one could conceivably use some
+preprocessor magic to provide alternate implementations, that would be
+impractical considering the sheer size of the code affected.
+On the other hand - perhaps you could benchmark the change and provide
+some data on whether this actually improves performance?
+
+
+--------------000207070200050102060301
+Content-Type: text/x-vcard; charset=utf-8;
+ name="mihailim.vcf"
+Content-Transfer-Encoding: 7bit
+Content-Disposition: attachment;
+ filename="mihailim.vcf"
+
+begin:vcard
+fn:Mihai Limbasan
+n:Limbasan;Mihai
+org:SC SECPRAL COM SRL
+adr:;;str. Actorului nr. 9;Cluj-Napoca;Cluj;400441;Romania
+email;internet:mihailim@security.ro
+title:SoftwareDeveloper
+tel;work:+40 264 449579
+tel;fax:+40 264 418594
+tel;cell:+40 729 038302
+url:http://secpral.ro/
+version:2.1
+end:vcard
+
+
+--------------000207070200050102060301
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+sqlite-dev mailing list
+sqlite-dev@sqlite.org
+http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev
+
+--------------000207070200050102060301--
+
--- /dev/null
+Return-Path: <dfgh@floppydisk.nl>
+X-Spam-Checker-Version: SpamAssassin 3.1.0 (2005-09-13) on mindcrime
+X-Spam-Level:
+Delivered-To: dfgh@floppydisk.nl
+Message-ID: <43A09C49.9040902@euler.org>
+Date: Wed, 14 Dec 2005 23:27:21 +0100
+From: Fred Flintstone <fred@euler.org>
+User-Agent: Mozilla Thunderbird 1.0.7 (X11/20051010)
+X-Accept-Language: nl-NL, nl, en
+MIME-Version: 1.0
+To: dfgh@floppydisk.nl
+Subject: Re: xyz
+References: <439C1136.90504@euler.org> <4399DD94.5070309@euler.org> <20051209233303.GA13812@gauss.org> <439B41ED.2080402@euler.org> <4399DD94.5070309@euler.org> <20051209233303.GA13812@gauss.org> <439A1E03.3090604@euler.org> <20051211184308.GB13513@gauss.org>
+In-Reply-To: <20051211184308.GB13513@gauss.org>
+X-Enigmail-Version: 0.92.0.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+X-UIDL: T<?"!%LG"!cAK"!_j(#!
+Content-Length: 1879
+
+Test 123.
--- /dev/null
+From: Frodo Baggins <frodo@example.com>
+To: Bilbo Baggins <bilbo@anotherexample.com>
+Subject: Greetings from =?UTF-8?B?TG90aGzDs3JpZW4=?=
+User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO)
+Fcc: .sent
+Organization: The Fellowship of the Ring
+MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka")
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+Message-Id: <abcd$efgh@example.com>
+
+
+Let's write some fünkÿ text
+using umlauts.
+
+Foo.
--- /dev/null
+From: =?UTF-8?B?TcO8?= <testmu@testmu.xx>
+To: Helmut =?UTF-8?B?S3LDtmdlcg==?= <hk@testmu.xxx>
+Subject: =?UTF-8?B?TW90w7ZyaGVhZA==?=
+User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO)
+References: <non-exist-01@msg.id> <non-exist-02@msg.id> <non-exist-03@msg.id> <non-exist-04@msg.id>
+1n-Reply-To: <non-exist-04@msg.id>
+MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka")
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+
+Test for issue #38, where apparently searching for accented words in subject,
+to etc. fails.
+
+What about here? Queensrÿche. Mötley Crüe.
+
+
--- /dev/null
+Return-path: <>
+Envelope-to: peter@example.com
+Delivery-date: Fri, 11 May 2012 16:22:03 +0300
+Received: from localhost.example.com ([127.0.0.1] helo=borealis)
+ by borealis with esmtp (Exim 4.77)
+ id 1SSpnB-00038a-Ux
+ for djcb@localhost; Fri, 11 May 2012 16:21:58 +0300
+Delivered-To: peter@example.com
+From: Brian <brian@example.com>
+To: Peter <peter@example.com>
+Subject: encrypted
+User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8
+Date: Fri, 11 May 2012 16:21:42 +0300
+Message-ID: <!&!AAAAAAAAAYAAAAAAAAAOH1+8mkk+lLn7Gg5fke7FbCgAAAEAAAAJ7eBDgcactKhXL6r8cEnJ8BAAAAAA==@example.com>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+ protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v1.4.12 (GNU/Linux)
+
+hQQOA1T38TPQrHD6EA//YXkUB4Dy09ngCRyHWbXmV3XBjuKTr8xrak5ML1kwurav
+gyagOHKLMU+5CKvObChiKtXhtgU0od7IC8o+ALlHevQ0XXcqNYA2KUfX8R7akq7d
+Xx9mA6D8P7Y/P8juUCLBpfrCi2GC42DtvPZSUu3bL/ctUJ3InPHIfHibKF2HMm7/
+gUHAKY8VPJF39dLP8GLcfki6qFdeWbxgtzmuyzHfCBCLnDL0J9vpEQBpGDFMcc4v
+cCbmMJaiPOmRb6U4WOuRVnuXuTztLiIn0jMslzOSFDcLTVBAsrC01r71O+XZKfN4
+mIfcpcWJYKM2NQW8Jwf+8Hr84uznBqs8uTTlrmppjkAHZGqGMjiQDxLhDVaCQzMy
+O8PSV4xT6HPlKXOwV1OLc+vm0A0RAdSBctgZg40oFn4XdB1ur8edwAkLvc0hJKaz
+gyTQiPaXm2Uh2cDeEx4xNgXmwCKasqc9jAlnDC2QwA33+pw3OqgZT5h1obn0fAeR
+mgB+iW1503DIi/96p8HLZcr2EswLEH9ViHIEaFj/vlR5BaOncsLB0SsNV/MHRvym
+Xg5GUjzPIiyBZ3KaR9OIBiZ5eXw+bSrPAo/CAs0Zwxag7W3CH//oK39Qo1GnkYpc
+4IQxhx4IwkzqtCnripltV/kfpGu0yA/OdK8lOjkUqCwvL97o73utXIxm21Zd3mEP
+/iLNrduZjMCq+goz1pDAQa9Dez6VjwRuRPTqeAac8Fx/nzrVzIoIEAt36hpuaH1l
+KpbmHpKgsUWcrE5iYT0RRlRRtRF4PfJg8PUmP1hvw8TaEmNfT+0HgzcJB/gRsVdy
+gTzkzUDzGZLhRcpmM5eW4BkuUmIO7625pM6Jd3HOGyfCGSXyEZGYYeVKzv8xbzYf
+QM6YYKooRN9Ya2jdcWguW0sCSJO/RZ9eaORpTeOba2+Fp6w5L7lga+XM9GLfgref
+Cf39XX1RsmRBsrJTw0z5COf4bT8G3/IfQP0QyKWIFITiFjGmpZhLsKQ3KT4vSe/d
+gTY1xViVhkjvMFn3cgSOSrvktQpAhsXx0IRazN0T7pTU33a5K0SrZajY9ynFDIw9
+we7XYyVwZzYEXjGih5mTH1PhWYK5fZZEKKqaz5TyYv9SeWJ+8FrHeXUKD38SQEHM
+qkpl9Iv17RF4Qy9uASWwRoobhKO+GykTaBSTyw8R8ctG/hfAlnaZxQ3TwNyHWyvU
+9SVJsp27ulv/W9MLZtGpEMK0ckAR164Vyou1KOn200BqxbC2tJpegNeD2TP5ZtdY
+HIcxkgKr0haYcDnVEf1ulSxv23pZWIexbgvVCG7dRL0eB+6O28f9CWehle10MDyM
+0AYyw8Da2cu7PONMovqt4nayScyGTacFBp7c2KXR9DGZ0mcBwOjL/mGRKcVWN3MG
+2auCrwn2KVWmKZI3Jp0T8KhfGBnFs9lUElpDTOiED1/2bKz6Yoc385QtWx99DFMZ
+IWiH5wMxkWFpzjE+GHiJ09vSbTTL4JY9eu2n5nxQmtjYMBVxQm7S7qwH
+=0Paa
+-----END PGP MESSAGE-----
+--=-=-=--
--- /dev/null
+Return-path: <>
+Envelope-to: djcb@localhost
+Delivery-date: Sun, 20 May 2012 09:59:51 +0300
+From: Steve Jobs <jobs@example.com>
+To: Bill Gates <bg@example.com>
+Subject: multimime
+User-agent: mu4e 0.9.8.4; emacs 23.3.1
+Date: Sat, 19 May 2012 20:57:56 +0100
+Message-ID: <m2fwaw2baz.fsf@example.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="=-=-="
+
+--=-=-=
+Content-Type: text/plain
+
+abc
+--=-=-=
+Content-Type: application/octet-stream
+Content-Disposition: attachment; filename="test1.C"
+Content-Transfer-Encoding: base64
+
+aGVyZSBpcyBhIHNpbXBsZSB0ZXN0IGZpbGUuCg==
+--=-=-=
+Content-Type: text/plain
+
+def
+--=-=-=--
--- /dev/null
+Date: Thu, 15 May 2016 14:57:25 -0200
+From:
+To: a@example.com,b@example.com,c@example.com
+Cc: d@example.com,e@example.com
+Subject: test with multi to and cc
+Message-id: <3BE9E652343245@emss35m06.us.lmco.com>
+
+Message with multi cc and to.
+
+
+
--- /dev/null
+Return-path: <>
+Envelope-to: skipio@localhost
+Delivery-date: Fri, 11 May 2012 16:21:57 +0300
+Received: from localhost.roma.net([127.0.0.1] helo=borealis)
+ by borealis with esmtp (Exim 4.77)
+ id 1SSpnB-00038a-55
+ for djcb@localhost; Fri, 11 May 2012 16:21:57 +0300
+Delivered-To: diggler@gmail.com
+From: Skipio <skipio@roma.net>
+To: Hannibal <hanni@carthago.net>
+Subject: signed
+User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8
+Date: Fri, 11 May 2012 16:20:45 +0300
+Message-ID: <878vgy97ma.fsf@roma.net>
+MIME-Version: 1.0
+Content-Type: multipart/signed; boundary="=-=-="; micalg=pgp-sha1;
+ protocol="application/pgp-signature"
+
+--=-=-=
+Content-Type: text/plain
+
+
+I am signed!
+
+--=-=-=
+Content-Type: application/pgp-signature
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.12 (GNU/Linux)
+
+iEYEARECAAYFAk+tEi0ACgkQ6WrHoQF92jxTzACeKd/XxY+P7bpymWL3JBRHaW9p
+DpwAoKw7PDW4z/lNTkWjndVTjoO9jGhs
+=blXz
+-----END PGP SIGNATURE-----
+--=-=-=--
+
--- /dev/null
+Return-path: <>
+Envelope-to: karjala@localhost
+Delivery-date: Fri, 11 May 2012 16:37:57 +0300
+From: karjala@example.com
+To: lapinkulta@example.com
+Subject: signed + encrypted
+User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8
+Date: Fri, 11 May 2012 16:36:08 +0300
+Message-ID: <874nrm96wn.fsf@example.com>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+ protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v1.4.12 (GNU/Linux)
+
+hQQOA1T38TPQrHD6EA/+K4kSpMa7zk+qihUkQnHSq28xYxisNQx6X5DVNjA/Qx16
+uZj/40ae+PoSMTVfklP+B2S/IomuTW6dwVqS7aQ3u4MTzi+YOi11k1lEMD7hR0Wb
+L0i48o3/iCPuCTpnOsaLZvRL06g+oTi0BF2pgz/YdsgsBTGrTb3pkDGSlLIhvh/J
+P8eE3OuzkXS6d8ymJKx2S2wQJrc1AFf1BgJfgc5T0iAvcV+zIMG+PIYcVd04zVpj
+cORFEfvGgfxWkeX+Ks3tu/l5PA1EesnoqFdNFZm+RKBg3RFsOm8tBlJ46xJjfeHg
+zLgifeSLy3tOX7CvWYs9torrx7s7UOI2gV8kzBqz+a7diyCMezceeQ9l0nIRybwW
+C9Egp8Bpfb02iXTOGdE/vRiNItQH14GKmXf4nCSwdtQUm3yzaqY9yL3xBxAlW53e
+YOFfPMESt+E7IlPn0c7llWGrcdrhJbUEoGOIPezES7kdeNPzi8G1lLtvT04/SSZJ
+QxPH5FNzSFaYFAQSdI7TR69P7L7vtLL8ndkjY49HfLFXochQQzsqrzVxzRCruHxA
+zbZSRptNf9SuXEaX9buO1vlFHheGvrCKzEWa6O7JD/DiyrE/zqy4jdlh9abMCouQ
+GWGSbn8jk6SMTQQ2Yv/VOyFqifHZp0UJD59tyIdenpxoYu5M0lwHLNVDlRjLEwUQ
+AIDz1tbLoM7lxs2FOKGr8QqbKIeMfL+NUmbvVIDc4mJrOlRnHh+cZYm4Z49iTl1v
+bYNMYgR5nY7W6rqh0ae7ZOW0h2NzpkAwTzuf1YrSjNavd9KBwOCFtAoZhRwfwFVx
+ju+ByHFNnf7g/R6DekHS0pSiatM0cPDJT05atEZb+13CRHHznonmLHi+VahXjrpg
+cIUA8Lhjdfm6Fsabo7gNZnTTRxNBqUXKK2vJF/XLbNrH5K2BH2dCCmUNtm3yFWiM
+DOzaw3665Y3S6MvZdyKpatbNrVoJdBpRgPxJ1YCSEituFUqHJBStay+aRb5fVkQR
+w3+9hWw+Ob0+2EumKbgfQ7iMwTZBCZP4VOxkoqdHvs9aWm4N7wHtXsyCew3icbJx
+lyUWsDx/FI+HlQRfOqeAMxmp8kKybmHNw8oGiw+uPPUHSD1NFYVm2DtwhYll3Fvs
+YY7r5s3yP1ZnwxMqWI3OsExVUXs8MS4UTAgO+cggO7YidPcANbBDihBFP8mTXtni
+Oo5n5v+/eRoLfHmnsGcaK8EkKsfFHpbqn4gxXGcBuHaTTJ/ZhbW6bi1WWZA9ExaJ
+IeTDtp5Bks1pJvTjCDacvgwl3rEBM6yaeIvB7575Y/GPMTOZhawhfOxV1smMmTKI
+JOWYb3+PuN2cvWetkjFgH8re4sRXq22DKBZHJEWYU8sH0sACAePnIr+pkrOtGeJB
+t1zBqZUnrupH6ptk9n/AjbQ+XSMTEKu55gSjYLAYx1EHApx52QLkdh+ej5xCIVeY
+6wS1Iipkoc6/r6F7CKctupXurNY2AlD4uQIOfD6kQgkqK4PY3hsRHQA+Zqj6oRfr
+kxysFJZvhgt26IeBVapFs10WuYt9iHfpbPUBQUIZCLyPAh08UdVW64Uc2DvUPy+I
+C+3RrmTHQPP/YNKgDQaZ3ySVEDkqjaDPmXr5K0Ibaib2dtPCLcA=
+=pv03
+-----END PGP MESSAGE-----
+--=-=-=--
+
--- /dev/null
+Date: Thu, 1 Jun 2012 14:57:25 -0200
+From: "Rocky Balboa" <rocky@example.com>
+To: "Ivan Drago" <ivan@example.com>
+Subject: currying and tail optimization
+Message-id: <3BE9E653ef345@emss35m06.us.lmco.com>
+MIME-version: 1.0
+Content-type: text/plain; charset=us-ascii
+Content-transfer-encoding: 7BIT
+
+Test 123. I'm a special message with special flags.
--- /dev/null
+Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level:
+X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham
+ version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+ by mail.xxxxsoftware.nl (Postfix) with ESMTP id 6389969CB2
+ for <xxxx@localhost>; Thu, 7 Aug 2008 08:10:07 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [66.249.91.109]
+ by mindcrime with IMAP (fetchmail-6.3.8)
+ for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:07 +0300 (EEST)
+Received: by 10.142.237.21 with SMTP id k21cs34769wfh; Wed, 6 Aug 2008
+ 13:38:53 -0700 (PDT)
+Received: by 10.100.6.13 with SMTP id 13mr4103508anf.83.1218055131215; Wed, 06
+ Aug 2008 13:38:51 -0700 (PDT)
+Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com
+ with ESMTP id b32si10199298ana.34.2008.08.06.13.38.49; Wed, 06 Aug 2008
+ 13:38:51 -0700 (PDT)
+Received-SPF: pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender) client-ip=199.232.76.165;
+DomainKey-Status: good (test mode)
+Authentication-Results: mx.google.com; spf=pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender)
+ smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org; domainkeys=pass
+ (test mode) header.From=juanma_bellon@yahoo.es
+Received: from localhost ([127.0.0.1]:55648 helo=lists.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KQpmT-0005W9-AQ for
+ xxxx.klub@gmail.com; Wed, 06 Aug 2008 16:38:49 -0400
+Received: from mailman by lists.gnu.org with tmda-scanned (Exim 4.43) id
+ 1KQplz-0005U5-Pk for help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:38:19 -0400
+Received: from exim by lists.gnu.org with spam-scanned (Exim 4.43) id
+ 1KQplw-0005Nw-OG for help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:38:19 -0400
+Received: from [199.232.76.173] (port=45465 helo=monty-python.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KQplw-0005NX-I6 for
+ help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:38:16 -0400
+Received: from n74a.bullet.mail.sp1.yahoo.com ([98.136.45.21]:29868) by
+ monty-python.gnu.org with smtp (Exim 4.60) (envelope-from
+ <juanma_bellon@yahoo.es>) id 1KQplw-0007EF-7Z for help-gnu-emacs@gnu.org;
+ Wed, 06 Aug 2008 16:38:16 -0400
+Received: from [216.252.122.216] by n74.bullet.mail.sp1.yahoo.com with NNFMP;
+ 06 Aug 2008 20:38:14 -0000
+Received: from [68.142.237.89] by t1.bullet.sp1.yahoo.com with NNFMP; 06 Aug
+ 2008 20:38:14 -0000
+Received: from [69.147.75.180] by t5.bullet.re3.yahoo.com with NNFMP; 06 Aug
+ 2008 20:38:14 -0000
+Received: from [127.0.0.1] by omp101.mail.re1.yahoo.com with NNFMP; 06 Aug
+ 2008 20:38:14 -0000
+X-Yahoo-Newman-Id: 778995.62909.bm@omp101.mail.re1.yahoo.com
+Received: (qmail 43643 invoked from network); 6 Aug 2008 20:38:14 -0000
+DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws; s=s1024; d=yahoo.es;
+ h=Received:X-YMail-OSG:X-Yahoo-Newman-Property:From:To:Subject:Date:User-Agent:References:In-Reply-To:MIME-Version:Content-Type:Content-Transfer-Encoding:Content-Disposition:Message-Id;
+ b=ThdHlND5CNUsLPGuk+XhCWkdUA9w7lg4hiAgx8F8egsmQteMpwUlV/Y5tfe6K3O2jzHjtsklkzWqm7WY3VAcxxD/QgxLnianK5ZQHoelDAiGaFRqu8Y42XMZso2ccCBFWUQaKo9C+KIfa3e3ci73qehVxTtmr7bxLjurcSYEBPo=
+ ;
+Received: from unknown (HELO 212251170160.customer.cdi.no)
+ (juanma_bellon@212.251.170.160 with plain) by smtp109.plus.mail.re1.yahoo.com
+ with SMTP; 6 Aug 2008 20:38:14 -0000
+X-YMail-OSG: k86L54kVM1kiZbUlYx7gayoBrCLYMFIRDL.KJLBKetNucAbwU4RjeeE1vhjw33hREaUig0CCjG7BTwIfbeZZpRmUcHbxl6gR0z6Sd3lYqA--
+X-Yahoo-Newman-Property: ymail-3
+From: anon@example.com
+To: help-gnu-emacs@gnu.org
+Date: Wed, 6 Aug 2008 22:38:15 +0200
+User-Agent: KMail/1.9.6 (enterprise 0.20070907.709405)
+References: <mailman.15123.1216681940.18990.help-gnu-emacs@gnu.org>
+ <mailman.15143.1216715014.18990.help-gnu-emacs@gnu.org>
+ <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com>
+In-Reply-To: <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com>
+MIME-Version: 1.0
+Content-Type: text/plain; charset="utf-8"
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+Message-Id: <200808062238.15634.juanma_bellon@yahoo.es>
+X-detected-kernel: by monty-python.gnu.org: FreeBSD 6.x (1)
+Subject: Re: basic question: going back to dired
+X-BeenThere: help-gnu-emacs@gnu.org
+X-Mailman-Version: 2.1.5
+Precedence: list
+List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org>
+List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe>
+List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs>
+List-Post: <mailto:help-gnu-emacs@gnu.org>
+List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help>
+List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe>
+Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Content-Length: 361
+
+On Thursday 31 July 2008, Xah wrote:
+> what's the logic of =E2=80=9COK=E2=80=9D?
+
+=46or all I know, it comes from "0 Knock-outs" (from USA civil war times,
+IIRC), i.e., all went really well.
+
+But this is really off-topic.
+=2D-=20
+Juanma
+
+"Having a smoking section in a restaurant is like
+ having a peeing section in a swimming pool."
+ -- Edward Burr
+
+
+
+
+
--- /dev/null
+Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level:
+X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham
+ version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+ by mail.xxxxsoftware.nl (Postfix) with ESMTP id C3EF069CB3
+ for <xxxx@localhost>; Thu, 7 Aug 2008 08:10:10 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [66.249.91.109]
+ by mindcrime with IMAP (fetchmail-6.3.8)
+ for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:10 +0300 (EEST)
+Received: by 10.142.237.21 with SMTP id k21cs35153wfh; Wed, 6 Aug 2008
+ 13:58:17 -0700 (PDT)
+Received: by 10.100.166.10 with SMTP id o10mr4182182ane.0.1218056296101; Wed,
+ 06 Aug 2008 13:58:16 -0700 (PDT)
+Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com
+ with ESMTP id d34si13875743and.3.2008.08.06.13.58.14; Wed, 06 Aug 2008
+ 13:58:16 -0700 (PDT)
+Received-SPF: pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender) client-ip=199.232.76.165;
+Authentication-Results: mx.google.com; spf=pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender)
+ smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org; dkim=pass (test
+ mode) header.i=@gmail.com
+Received: from localhost ([127.0.0.1]:33418 helo=lists.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KQq5G-0001aY-Cr for
+ xxxx.klub@gmail.com; Wed, 06 Aug 2008 16:58:14 -0400
+Received: from mailman by lists.gnu.org with tmda-scanned (Exim 4.43) id
+ 1KQq4n-0001Z9-06 for help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:57:45 -0400
+Received: from exim by lists.gnu.org with spam-scanned (Exim 4.43) id
+ 1KQq4l-0001V8-6c for help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:57:44 -0400
+Received: from [199.232.76.173] (port=46438 helo=monty-python.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KQq4k-0001Un-V2 for
+ help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:57:42 -0400
+Received: from ik-out-1112.google.com ([66.249.90.180]:17562) by
+ monty-python.gnu.org with esmtp (Exim 4.60) (envelope-from
+ <lekktu@gmail.com>) id 1KQq4k-0001fk-OW for help-gnu-emacs@gnu.org; Wed, 06
+ Aug 2008 16:57:42 -0400
+Received: by ik-out-1112.google.com with SMTP id c21so94956ika.2 for
+ <help-gnu-emacs@gnu.org>; Wed, 06 Aug 2008 13:57:41 -0700 (PDT)
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=gamma;
+ h=domainkey-signature:received:received:message-id:date:from:to
+ :subject:cc:in-reply-to:mime-version:content-type
+ :content-transfer-encoding:content-disposition:references;
+ bh=TTNY9749hpg1+TXOwdaCr+zbQGhBUt3IvsjLWp+pxp0=;
+ b=BOfudUT/SiW9V4e9+k3dXDzwm+ogdrq4m5OlO+f1H+oE6OAYGIm8dbdqDAOwUewBoS
+ jRpfZo07YamP9rkko79SeFdQnf7UAPFAw9x7DFCm3x6muSlCcJBR7vYs1rgHOSINAn2B
+ vQx2//lKR4fXfKNURNu+B30KrvoEmw6m2C8dI=
+DomainKey-Signature: a=rsa-sha1; c=nofws; d=gmail.com; s=gamma;
+ h=message-id:date:from:to:subject:cc:in-reply-to:mime-version
+ :content-type:content-transfer-encoding:content-disposition :references;
+ b=UMDBulH/LwxDywEH0pfK3DbJ4u2kIZCVDLIM++PqrdcR82HjcS/O3Jhf5OFrf7Fnyj
+ GH76xmc7zkTG/3aQy2WY6DeWCJaFarEItmhxy3h/xS+kUKeDARzNox0OzK6lIv/u9bdy
+ f2LnFlYRJ7Q5vy3lxpxAWB4v0qCwtF9LjWFg4=
+Received: by 10.210.47.7 with SMTP id u7mr3100239ebu.30.1218056261587; Wed, 06
+ Aug 2008 13:57:41 -0700 (PDT)
+Received: by 10.210.71.14 with HTTP; Wed, 6 Aug 2008 13:57:41 -0700 (PDT)
+Message-ID: <f7ccd24b0808061357t453f5962w8b61f9a453b684d0@mail.gmail.com>
+Date: Wed, 6 Aug 2008 22:57:41 +0200
+From: anon@example.com
+To: Juanma <juanma_bellon@yahoo.es>
+In-Reply-To: <200808062238.15634.juanma_bellon@yahoo.es>
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+References: <mailman.15123.1216681940.18990.help-gnu-emacs@gnu.org>
+ <mailman.15143.1216715014.18990.help-gnu-emacs@gnu.org>
+ <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com>
+ <200808062238.15634.juanma_bellon@yahoo.es>
+X-detected-kernel: by monty-python.gnu.org: Linux 2.6 (newer, 2)
+Cc: help-gnu-emacs@gnu.org
+Subject: Re: basic question: going back to dired
+X-BeenThere: help-gnu-emacs@gnu.org
+X-Mailman-Version: 2.1.5
+Precedence: list
+List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org>
+List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe>
+List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs>
+List-Post: <mailto:help-gnu-emacs@gnu.org>
+List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help>
+List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe>
+Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Content-Length: 309
+
+On Wed, Aug 6, 2008 at 22:38, Juanma <juanma_bellon@yahoo.es> wrote:
+
+> For all I know, it comes from "0 Knock-outs" (from USA civil war times,
+> IIRC), i.e., all went really well.
+
+See http://en.wikipedia.org/wiki/Okay#Etymology
+
+"0 knock-outs" is among the "Improbable or refuted etymologies".
+
+ Juanma
+
+
--- /dev/null
+Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level:
+X-Spam-Status: No, score=-3.6 required=3.0 tests=BAYES_00,RCVD_IN_DNSWL_LOW,
+ SPF_PASS autolearn=ham version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+ by mail.xxxxsoftware.nl (Postfix) with ESMTP id D68E769CB5
+ for <xxxx@localhost>; Fri, 8 Aug 2008 20:56:25 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [72.14.221.111]
+ by mindcrime with IMAP (fetchmail-6.3.8)
+ for <xxxx@localhost> (single-drop); Fri, 08 Aug 2008 20:56:25 +0300 (EEST)
+Received: by 10.142.237.21 with SMTP id k21cs71287wfh; Fri, 8 Aug 2008
+ 07:40:46 -0700 (PDT)
+Received: by 10.100.122.8 with SMTP id u8mr3824321anc.77.1218206446062; Fri,
+ 08 Aug 2008 07:40:46 -0700 (PDT)
+Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com
+ with ESMTP id d35si2718351and.38.2008.08.08.07.40.45; Fri, 08 Aug 2008
+ 07:40:46 -0700 (PDT)
+Received-SPF: pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender) client-ip=199.232.76.165;
+Authentication-Results: mx.google.com; spf=pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender)
+ smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Received: from localhost ([127.0.0.1]:47349 helo=lists.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KRT93-0006Po-A3 for
+ xxxx.klub@gmail.com; Fri, 08 Aug 2008 10:40:45 -0400
+Path: news.stanford.edu!headwall.stanford.edu!newshub.sdsu.edu!news-out.readnews.com!news-xxxfer.readnews.com!panix!not-for-mail
+From: anon@example.com
+Newsgroups: gnu.emacs.help
+Date: Fri, 08 Aug 2008 10:07:30 -0400
+Organization: PANIX Public Access Internet and UNIX, NYC
+Message-ID: <uwsireh25.fsf@one.dot.net>
+References: <mailman.15123.1216681940.18990.help-gnu-emacs@gnu.org>
+ <mailman.15143.1216715014.18990.help-gnu-emacs@gnu.org>
+ <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com>
+ <200808062238.15634.juanma_bellon@yahoo.es>
+ <mailman.15958.1218056266.18990.help-gnu-emacs@gnu.org>
+NNTP-Posting-Host: panix5.panix.com
+Mime-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+X-Trace: reader1.panix.com 1218204439 22850 166.84.1.5 (8 Aug 2008 14:07:19
+ GMT)
+X-Complaints-To: abuse@panix.com
+NNTP-Posting-Date: Fri, 8 Aug 2008 14:07:19 +0000 (UTC)
+User-Agent: Gnus/5.11 (Gnus v5.11) Emacs/22.2 (windows-nt)
+Cancel-Lock: sha1:Ckkp5oJPIMuAVgEHGnS/9MkZsEs=
+Xref: news.stanford.edu gnu.emacs.help:160963
+To: help-gnu-emacs@gnu.org
+Subject: Re: basic question: going back to dired
+X-BeenThere: help-gnu-emacs@gnu.org
+X-Mailman-Version: 2.1.5
+Precedence: list
+List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org>
+List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe>
+List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs>
+List-Post: <mailto:help-gnu-emacs@gnu.org>
+List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help>
+List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe>
+Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Content-Length: 710
+Lines: 27
+
+I seem to remember from my early school days it was a campaign slogan
+for someone nick-named Kinderhook that went something like
+
+Old Kinderhook is OK
+
+- Chris
+
+"Juanma Barranquero" <lekktu@gmail.com> writes:
+
+> On Wed, Aug 6, 2008 at 22:38, Juanma <juanma_bellon@yahoo.es> wrote:
+>
+>> For all I know, it comes from "0 Knock-outs" (from USA civil war times,
+>> IIRC), i.e., all went really well.
+>
+> See http://en.wikipedia.org/wiki/Okay#Etymology
+>
+> "0 knock-outs" is among the "Improbable or refuted etymologies".
+>
+> Juanma
+>
+>
+
+--
+ (. .)
+ =ooO=(_)=Ooo=====================================
+ Chris McMahan | first_initiallastname@one.dot.net
+ =================================================
+
--- /dev/null
+Return-Path: <sqlite-dev-bounces@sqlite.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level:
+X-Spam-Status: No, score=-1.2 required=3.0 tests=BAYES_00,HTML_MESSAGE,
+ MIME_QP_LONG_LINE autolearn=no version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+ by mail.xxxxsoftware.nl (Postfix) with ESMTP id 4E3CF6963B
+ for <xxxx@localhost>; Mon, 4 Aug 2008 21:49:37 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [72.14.221.111]
+ by mindcrime with IMAP (fetchmail-6.3.8)
+ for <xxxx@localhost> (single-drop); Mon, 04 Aug 2008 21:49:37 +0300 (EEST)
+Received: by 10.142.51.12 with SMTP id y12cs94317wfy; Mon, 4 Aug 2008 05:48:28
+ -0700 (PDT)
+Received: by 10.150.152.17 with SMTP id z17mr1245909ybd.194.1217854107583;
+ Mon, 04 Aug 2008 05:48:27 -0700 (PDT)
+Received: from sqlite.org (sqlite.org [67.18.92.124]) by mx.google.com with
+ ESMTP id 9si6334793yws.5.2008.08.04.05.47.57; Mon, 04 Aug 2008 05:48:27 -0700
+ (PDT)
+Received-SPF: pass (google.com: best guess record for domain of
+ sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as permitted sender)
+ client-ip=67.18.92.124;
+Authentication-Results: mx.google.com; spf=pass (google.com: best guess record
+ for domain of sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as
+ permitted sender) smtp.mail=sqlite-dev-bounces@sqlite.org
+Received: from sqlite.org (localhost [127.0.0.1]) by sqlite.org (Postfix) with
+ ESMTP id 4FBC111C6F; Mon, 4 Aug 2008 08:47:54 -0400 (EDT)
+X-Original-To: sqlite-dev@sqlite.org
+Delivered-To: sqlite-dev@sqlite.org
+Received: from cpsmtpo-eml02.kpnxchange.com (cpsmtpo-eml02.kpnxchange.com
+ [213.75.38.151]) by sqlite.org (Postfix) with ESMTP id AA4F111C10 for
+ <sqlite-dev@sqlite.org>; Mon, 4 Aug 2008 08:47:51 -0400 (EDT)
+Received: from hpsmtp-eml21.kpnxchange.com ([213.75.38.121]) by
+ cpsmtpo-eml02.kpnxchange.com with Microsoft SMTPSVC(6.0.3790.1830); Mon, 4
+ Aug 2008 14:47:50 +0200
+Received: from cpbrm-eml13.kpnsp.local ([195.121.247.250]) by
+ hpsmtp-eml21.kpnxchange.com with Microsoft SMTPSVC(6.0.3790.1830); Mon, 4
+ Aug 2008 14:47:50 +0200
+Received: from hpsmtp-eml30.kpnxchange.com ([10.94.53.250]) by
+ cpbrm-eml13.kpnsp.local with Microsoft SMTPSVC(6.0.3790.1830); Mon, 4 Aug
+ 2008 14:47:50 +0200
+Received: from localhost ([10.94.53.250]) by hpsmtp-eml30.kpnxchange.com with
+ Microsoft SMTPSVC(6.0.3790.1830); Mon, 4 Aug 2008 14:47:49 +0200
+Content-class: urn:content-classes:message
+MIME-Version: 1.0
+X-MimeOLE: Produced By Microsoft Exchange V6.5
+Date: Mon, 4 Aug 2008 14:46:06 +0200
+Message-ID: <F687EC042917A94E8BB4B0902946453AE17D6C@CPEXBE-EML18.kpnsp.local>
+X-MS-Has-Attach:
+X-MS-TNEF-Correlator:
+Thread-Topic: [sqlite-dev] VM optimization inside sqlite3VdbeExec
+Thread-Index: Acj2FjkWvteFtLHTTYeVz4ES7E2ggAAGRxeI
+References: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net>
+From: anon@example.com
+To: <sqlite-dev@sqlite.org>
+X-OriginalArrivalTime: 04 Aug 2008 12:47:49.0650 (UTC)
+ FILETIME=[4D577720:01C8F630]
+Subject: Re: [sqlite-dev] VM optimization inside sqlite3VdbeExec
+X-BeenThere: sqlite-dev@sqlite.org
+X-Mailman-Version: 2.1.9
+Precedence: list
+Reply-To: sqlite-dev@sqlite.org
+List-Id: <sqlite-dev.sqlite.org>
+List-Unsubscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>,
+ <mailto:sqlite-dev-request@sqlite.org?subject=unsubscribe>
+List-Archive: <http://sqlite.org:8080/cgi-bin/mailman/private/sqlite-dev>
+List-Post: <mailto:sqlite-dev@sqlite.org>
+List-Help: <mailto:sqlite-dev-request@sqlite.org?subject=help>
+List-Subscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>,
+ <mailto:sqlite-dev-request@sqlite.org?subject=subscribe>
+Content-Type: multipart/mixed; boundary="===============1911358387=="
+Mime-version: 1.0
+Sender: sqlite-dev-bounces@sqlite.org
+Errors-To: sqlite-dev-bounces@sqlite.org
+Content-Length: 5318
+
+This is a multi-part message in MIME format.
+
+--===============1911358387==
+Content-class: urn:content-classes:message
+Content-Type: multipart/alternative;
+ boundary="----_=_NextPart_001_01C8F630.0FC2EC1E"
+
+This is a multi-part message in MIME format.
+
+------_=_NextPart_001_01C8F630.0FC2EC1E
+Content-Type: text/plain;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+Actually, almost every C compiler will already do what you suggest: if =
+the range of case labels is compact, the switch will be compiled using a =
+jump table. Only if the range is limited and/or sparse other techniques =
+will be used, such as linear search and binary search.
+=20
+I'm pretty sure, if you perform the tests suggested by Mihai, that you =
+will find zero performance difference, neither better, nor worse.
+=20
+Paul
+=20
+________________________________
+
+From: anon@example.com
+Sent: Mon 8/4/2008 11:40 AM
+To: sqlite-dev@sqlite.org
+Subject: [sqlite-dev] VM optimization inside sqlite3VdbeExec
+
+
+
+Inside sqlite3VdbeExec there is a very big switch statement.
+In order to increase performance with few modifications to the=20
+original code, why not use this technique ?
+http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html =
+<http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html>=20
+
+With a properly defined "instructions" array, instead of the switch=20
+statement you can use something like:
+goto * instructions[pOp->opcode];
+---
+Marco Bambini
+http://www.sqlabs.net <http://www.sqlabs.net/>=20
+http://www.sqlabs.net/blog/ <http://www.sqlabs.net/blog/>=20
+http://www.sqlabs.net/realsqlserver/ =
+<http://www.sqlabs.net/realsqlserver/>=20
+
+
+
+_______________________________________________
+sqlite-dev mailing list
+sqlite-dev@sqlite.org
+http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev =
+<http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>=20
+
+
+
+------_=_NextPart_001_01C8F630.0FC2EC1E
+Content-Type: text/html;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+<HTML dir=3Dltr><HEAD><TITLE>[sqlite-dev] VM optimization inside =
+sqlite3VdbeExec</TITLE>=0A=
+<META http-equiv=3DContent-Type content=3D"text/html; charset=3Dunicode">=0A=
+<META content=3D"MSHTML 6.00.2715.400" name=3DGENERATOR></HEAD>=0A=
+<BODY>=0A=
+<DIV id=3DidOWAReplyText54900 dir=3Dltr>=0A=
+<DIV dir=3Dltr><FONT face=3DArial color=3D#000000 size=3D2>Actually, =
+almost every C compiler will already do what you suggest: if the range =
+of case labels is compact, the switch will be compiled using a jump =
+table. Only if the range is limited and/or sparse other techniques will =
+be used, such as linear search and binary search.</FONT></DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial size=3D2></FONT> </DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial size=3D2>I'm pretty sure, if you =
+perform the tests suggested by Mihai, that you will find zero =
+performance difference, neither better, nor worse.</FONT></DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial size=3D2></FONT> </DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial size=3D2>Paul</FONT></DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial size=3D2></FONT> </DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial size=3D2>=0A=
+<HR tabIndex=3D-1>=0A=
+</FONT></DIV>=0A=
+<DIV dir=3Dltr><FONT face=3DArial><FONT size=3D2><B>From:</B> =
+sqlite-dev-bounces@sqlite.org on behalf of Marco Bambini<BR><B>Sent:</B> =
+Mon 8/4/2008 11:40 AM<BR><B>To:</B> =
+sqlite-dev@sqlite.org<BR><B>Subject:</B> [sqlite-dev] VM optimization =
+inside sqlite3VdbeExec<BR><BR></FONT></FONT></DIV></DIV>=0A=
+<DIV>=0A=
+<P><FONT face=3DArial size=3D2>Inside sqlite3VdbeExec there is a very =
+big switch statement.<BR>In order to increase performance with few =
+modifications to the <BR>original code, why not use this technique =
+?<BR></FONT><A =
+href=3D"http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html">=
+<FONT face=3DArial =
+size=3D2>http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html<=
+/FONT></A><BR><BR><FONT face=3DArial size=3D2>With a properly defined =
+"instructions" array, instead of the switch <BR>statement you can =
+use something like:<BR>goto * =
+instructions[pOp->opcode];<BR>---<BR>Marco Bambini<BR></FONT><A =
+href=3D"http://www.sqlabs.net/"><FONT face=3DArial =
+size=3D2>http://www.sqlabs.net</FONT></A><BR><A =
+href=3D"http://www.sqlabs.net/blog/"><FONT face=3DArial =
+size=3D2>http://www.sqlabs.net/blog/</FONT></A><BR><A =
+href=3D"http://www.sqlabs.net/realsqlserver/"><FONT face=3DArial =
+size=3D2>http://www.sqlabs.net/realsqlserver/</FONT></A><BR><BR><BR><BR><=
+FONT face=3DArial =
+size=3D2>_______________________________________________<BR>sqlite-dev =
+mailing list<BR>sqlite-dev@sqlite.org<BR></FONT><A =
+href=3D"http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev"><FONT=
+ face=3DArial =
+size=3D2>http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev</FONT=
+></A><BR></P></DIV></BODY></HTML>
+------_=_NextPart_001_01C8F630.0FC2EC1E--
+
+
+--===============1911358387==
+Content-Type: text/plain; charset="us-ascii"
+MIME-Version: 1.0
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+_______________________________________________
+sqlite-dev mailing list
+sqlite-dev@sqlite.org
+http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev
+
+--===============1911358387==--
+
--- /dev/null
+Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level:
+X-Spam-Status: No, score=-3.6 required=3.0 tests=BAYES_00,RCVD_IN_DNSWL_LOW,
+ SPF_PASS autolearn=ham version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+ by mail.xxxxsoftware.nl (Postfix) with ESMTP id D68E769CB5
+ for <xxxx@localhost>; Fri, 8 Aug 2008 20:56:25 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [72.14.221.111]
+ by mindcrime with IMAP (fetchmail-6.3.8)
+ for <xxxx@localhost> (single-drop); Fri, 08 Aug 2008 20:56:25 +0300 (EEST)
+Received: by 10.142.237.21 with SMTP id k21cs71287wfh; Fri, 8 Aug 2008
+ 07:40:46 -0700 (PDT)
+Received: by 10.100.122.8 with SMTP id u8mr3824321anc.77.1218206446062; Fri,
+ 08 Aug 2008 07:40:46 -0700 (PDT)
+Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com
+ with ESMTP id d35si2718351and.38.2008.08.08.07.40.45; Fri, 08 Aug 2008
+ 07:40:46 -0700 (PDT)
+Received-SPF: pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender) client-ip=199.232.76.165;
+Authentication-Results: mx.google.com; spf=pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender)
+ smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Received: from localhost ([127.0.0.1]:47349 helo=lists.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KRT93-0006Po-A3 for
+ xxxx.klub@gmail.com; Fri, 08 Aug 2008 10:40:45 -0400
+Path: news.stanford.edu!headwall.stanford.edu!newshub.sdsu.edu!news-out.readnews.com!news-xxxfer.readnews.com!panix!not-for-mail
+From: anon@example.com
+Newsgroups: gnu.emacs.help
+Date: Fri, 08 Aug 2008 10:07:30 -0400
+Organization: PANIX Public Access Internet and UNIX, NYC
+Message-ID: <uwsireh25.fsf@one.dot.net>
+References: <mailman.15123.1216681940.18990.help-gnu-emacs@gnu.org>
+ <mailman.15143.1216715014.18990.help-gnu-emacs@gnu.org>
+ <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com>
+ <200808062238.15634.juanma_bellon@yahoo.es>
+ <mailman.15958.1218056266.18990.help-gnu-emacs@gnu.org>
+NNTP-Posting-Host: panix5.panix.com
+Mime-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+X-Trace: reader1.panix.com 1218204439 22850 166.84.1.5 (8 Aug 2008 14:07:19
+ GMT)
+X-Complaints-To: abuse@panix.com
+NNTP-Posting-Date: Fri, 8 Aug 2008 14:07:19 +0000 (UTC)
+User-Agent: Gnus/5.11 (Gnus v5.11) Emacs/22.2 (windows-nt)
+Cancel-Lock: sha1:Ckkp5oJPIMuAVgEHGnS/9MkZsEs=
+Xref: news.stanford.edu gnu.emacs.help:160963
+To: help-gnu-emacs@gnu.org
+Subject: Re: basic question: going back to dired
+X-BeenThere: help-gnu-emacs@gnu.org
+X-Mailman-Version: 2.1.5
+Precedence: list
+List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org>
+List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe>
+List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs>
+List-Post: <mailto:help-gnu-emacs@gnu.org>
+List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help>
+List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe>
+Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Content-Length: 710
+Lines: 27
+
+I seem to remember from my early school days it was a campaign slogan
+for someone nick-named Kinderhook that went something like
+
+Old Kinderhook is OK
+
+- Chris
+
+"Juanma Barranquero" <lekktu@gmail.com> writes:
+
+> On Wed, Aug 6, 2008 at 22:38, Juanma <juanma_bellon@yahoo.es> wrote:
+>
+>> For all I know, it comes from "0 Knock-outs" (from USA civil war times,
+>> IIRC), i.e., all went really well.
+>
+> See http://en.wikipedia.org/wiki/Okay#Etymology
+>
+> "0 knock-outs" is among the "Improbable or refuted etymologies".
+>
+> Juanma
+>
+>
+
+--
+ (. .)
+ =ooO=(_)=Ooo=====================================
+ Chris McMahan | first_initiallastname@one.dot.net
+ =================================================
+
--- /dev/null
+Return-Path: <>
+X-Original-To: f00f@localhost
+Delivered-To: f00f@localhost
+Received: from puppet (puppet [127.0.0.1])
+ by f00fmachines.nl (Postfix) with ESMTP id A534D39C7F1
+ for <f00f@localhost>; Mon, 23 May 2011 20:30:05 +0300 (EEST)
+Delivered-To: diggler@gmail.com
+Received: from ew-in-f109.1e100.net [174.15.27.101]
+ by puppet with POP3 (fetchmail-6.3.18)
+ for <f00f@localhost> (single-drop); Mon, 23 May 2011 20:30:05 +0300 (EEST)
+Received: by 10.142.147.13 with SMTP id u13cs87252wfd;
+ Mon, 23 May 2011 01:54:10 -0700 (PDT)
+Received: by 10.204.7.74 with SMTP id c10mr1984197bkc.104.1306140849326;
+ Mon, 23 May 2011 01:54:09 -0700 (PDT)
+Received: from MTX4.mbn1.net (mtx4.mbn1.net [213.188.129.252])
+ by mx.google.com with ESMTP id e6si18117551bkw.39.2011.05.23.01.54.07;
+ Mon, 23 May 2011 01:54:08 -0700 (PDT)
+Received-SPF: pass (google.com: best guess record for domain of MTX4.mbn1.net designates 213.188.129.252 as permitted sender) client-ip=213.188.129.252;
+Authentication-Results: mx.google.com; spf=pass (google.com: best guess record for domain of MTX4.mbn1.net designates 213.188.129.252 as permitted sender) smtp.mail=
+Resent-From: <fwdgrp_11163824_f00f@f00fmachines.nl>
+X-Default-Received-SPF: pass (skip=forwardok (res=PASS)) x-ip-name=192.168.10.123;
+From: ArtOlive <artolive@mailinglijst.nl>
+To: "f00f@f00fmachines.nl" <f00f@f00fmachines.nl>
+Reply-To: <artolive@mailinglijst.nl>
+Date: Mon, 23 May 2011 10:53:45 +0200
+Subject: NIEUWSBRIEF ART OLIVE | juni exposite in galerie ArtOlive
+MIME-Version: 1.0
+Content-Type: multipart/alternative;
+ boundary="_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d"
+X-Mailer: aspNetEmail ver 3.5.2.10
+X-Sender: 87.93.13.24
+X-RemoteIP: 87.93.13.24
+Originating-IP: 87.93.13.24
+X-MAILINGLIJST-ID: <10374608.109906.11909.2011523105345.MSGID@mailinglijst.nl>
+Message-ID: <10374608.109906.11909.2011523105345.MSGID@mailinglijst.nl>
+X-Authenticated-User: guest@mailinglijst.eu
+X-STA-Metric: 0 (engine=030)
+X-STA-NotSpam: geinformeerd vormen spec:usig:3.8.2 twee samen
+X-STA-Spam: 2011 &bull • e-mailing subject:juni
+X-BTI-AntiSpam: score:0,sta:0/030,dnsbl:passed,sw:passed,bsn:10/passed,spf:off,bsctr:passed/1,dk:off,pbmf:none,ipr:0/3,trusted:no,ts:no,bs:no,ubl:passed
+X-Auto-Response-Suppress: DR, RN, NRN, OOF, AutoReply
+Resent-Message-Id: <19740414233016.EB6835A132F5FCF4@MTX4.mbn1.net>
+Resent-Date: Mon, 23 May 2011 10:54:07 +0200 (CEST)
+
+--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d
+Content-Type: text/plain; charset="iso-8859-15"
+Content-Transfer-Encoding: quoted-printable
+
+ART-O-NEWS; juni 2011 Westergasfabriekterrein Polonceaukade 17 10=
+14 DA Amsterdam tel: 020-6758504 info@artolive.nl www.artolive.nlJuni=
+ expositie bij ArtOlive: Peter van den Akker en Marinel Vieleers
+
+
+Zondag 5 juni
+ Elke maand vindt er in de galerie van ArtOlive een expositie plaats. We =
+lichten enkele kunstenaars uit (die je misschien al kent van onze website=
+), waarbij we een spannende mix van materiaal en techniek presenteren. Ti=
+jdens de expositie staan we klaar om elke vraag over ons kunstaanbod te b=
+eantwoorden.=20
+
+De exposities zijn te bezoeken van maandag t/m vrijdag, tussen 10:00 en 1=
+7:00 uur. De opening is altijd op de eerste zondag van de maand. Dit valt=
+ samen met de Sunday Market die elke maand op het Cultuurpark Westergasfa=
+briek georganiseerd wordt. De Sunday Market is gratis te bezoeken en staa=
+t vol met kunst, design, mode en heerlijke hapjes, en er hangt altijd een=
+ vrolijke sfeer. Een ideaal moment dus om in te haken en deze maand twee =
+kunstenaars te presenteren: Peter van den Akker en Marinel Vieleers.
+
+We verwelkomen je graag op zondag 5 juni 2011, van 12:00 t/m 17:00 uur op=
+ de Polonceaukade 17 van het Cultuurpark Westergasfabriek in Amsterdam!=20=
+
+
+
+ bekijk meer werk op www.artolive.nl... Peter van den Akker
+
+
+"In mijn beelden en schilderijen staat het mensbeeld centraal; niet als i=
+ndividu maar als universele gestalte, waarbij ik op transparante wijze ti=
+jdsbeelden en gelaagdheid in het menselijke handelen naar voren breng. Ve=
+rhoudingen tussen mensen, verschuivingen in wereldculturen en verandering=
+en in techniek, architectuur, natuur en mensbeeld vormen mijn inspiratieb=
+ronnen. Het zijn allemaal beelden en sferen die naast en met elkaar besta=
+an. Mijn werkwijze omvat vele technieken in verschillende materialen: sch=
+ilderijen, gemengde technieken op papier/collages, zeefdrukken, beelden i=
+n cortenstaal, keramische objecten."
+
+Peter van den Akker exposeert regelmatig in binnen- en buitenland bij gal=
+erie=EBn en musea en is in verschillende kunstinstellingen en bedrijfscol=
+lecties opgenomen.
+
+
+ lees meer over Peter... Marinel Vieleers
+
+
+Marinel Vieleers probeert het menselijke in de bouwwerken - en ook vaak i=
+ets van het karakter van de bouwer of bewoner - te laten zien. Het zijn m=
+aar subtiele details die dat alles weergeven.
+
+De 'tand des tijds' of invloed van mensen op de gebouwen spelen vaak mee =
+in het werk. Koper, cement, lood en andere materialen worden in haar nieu=
+we werk gebruikt. Op deze manier kan ze gemakkelijker improviseren en nog=
+ directer op haar gevoel afgaan.
+
+Marinel is gefascineerd door de schoonheid van het imperfecte. De gelaagd=
+heid van ouderdom, het verval. De imperfectie die ontstaat door toevallig=
+e omstandigheden maakt een huis, een muur, een schutting, hout of steen b=
+oeiend. Het is doorleefd en het toont een stukje van zijn geschiedenis.
+
+
+ lees meer over Marinel... =20
+ZONDAG 5 MEI - Juni expositie in de galerie van ArtOlive met Marinel Viel=
+eers en Peter van den Akker
+
+Opening op zondag 5 mei, tijdens de Sunday Market op het Cultuurpark West=
+ergasfabriek in Amsterdam. Je bent van harte uitgenodigd om tussen 12:00 =
+en 17:00 uur langs te komen!
+
+Daarna is de expositie te zien op werkdagen (ma - vrij) tussen 10:00 en 1=
+7:00. De expositie duurt tot 24 juni 2011.
+ wil je niet langer door artolive ge=EFnformeerd worden? Klik dan hier om=
+ je af te melden.=20
+ kreeg je dit mailtje doorgestuurd en wil je voortaan zelf ook graag de n=
+ieuwsbrief ontvangen?=20
+ klik dan hier om je aan te melden.=20
+
+Deze e-mailing is verzorgd met MailingLijst
+
+--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d
+Content-Type: text/html; charset="iso-8859-15"
+Content-Transfer-Encoding: quoted-printable
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://ww=
+w.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns=3D"http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3D=
+ISO-8859-15">
+ <title>Artolive</title>
+ </head>
+ <body style=3D"line-height: 13px; font-family: Verdana; color: rgb(11=
+9, 119, 119); font-size: 10px;" vlink=3D"#666666" alink=3D"#666666" link=3D=
+"#666666">
+ <style type=3D"text/css">
+ A {
+ COLOR: #666666; TEXT-DECORATION: none
+ }
+ TD {
+ FONT-FAMILY: Verdana; COLOR: #777777; FONT-SIZE: 10px; VERTIC=
+AL-ALIGN: top
+ }
+ </style>
+ <table style=3D"width: 631px;" width=3D"631" align=3D"center" cel=
+lpadding=3D"0" cellspacing=3D"10">
+ <tbody>
+ <tr>
+ <td><a href=3D"http://www.mailinglijst.eu/redirect.as=
+px?l=3D154041&a=3D10374608&t=3DH" target=3D"_blank"><img style=3D"height:=
+ 42px; width: 188px;" alt=3D"artolive" src=3D"http://mailinglijst.eu/klan=
+ten/11909/Sjabloon/logo.jpg" width=3D"188" border=3D"0" height=3D"42"></a=
+> </td>
+ </tr>
+ <tr>
+ <td style=3D"text-align: right; padding-right: 5px; c=
+olor: rgb(0, 0, 0); font-size: 10px;">ART-O-NEWS • juni 2011 </=
+td>
+ </tr>
+ <tr>
+ <td style=3D"border-width: 1px; border-style: solid; =
+border-color: rgb(167, 169, 172); padding: 5px 10px 5px 15px; color: rgb(=
+167, 169, 172); font-size: 9px;">Westergasfabriekterrein Polonceau=
+kade 17 1014 DA Amsterdam tel: 020-6758504 <a style=3D"color=
+: rgb(167, 169, 172);" href=3D"mailto:info@artolive.nl">info@artolive.nl<=
+/a> <a style=3D"color: rgb(167, 169, 172);" href=3D"http://www.mail=
+inglijst.eu/redirect.aspx?l=3D154041&a=3D10374608&t=3DH" target=3D"_blank=
+">www.artolive.nl</a> </td>
+ </tr>
+ <tr>
+ <td style=3D"border-width: 1px; border-style: solid; =
+border-color: rgb(81, 81, 81);">
+ <table width=3D"100%" cellpadding=3D"10" cellspacing=3D=
+"0">
+ <tbody style=3D"color: rgb(119, 119, 119); font-s=
+ize: 10px; vertical-align: top;">
+ <tr>
+ <td style=3D"vertical-align: top;"><img s=
+tyle=3D"height: 338px; width: 252px;" src=3D"http://mailinglijst.eu/klant=
+en/11909/juni2011/IMG_1698_%28Medium%29.JPG" width=3D"252" height=3D"338"=
+> </td>
+ <td style=3D"vertical-align: top;"><span =
+style=3D"line-height: normal; font-size: 24px; color: rgb(0, 0, 0);">Juni=
+ expositie bij ArtOlive: Peter van den Akker en Marinel Vieleers</span><b=
+r>
+ <p><strong>Zondag 5 juni</strong><br>
+ Elke maand vindt er in de galerie van Art=
+Olive een expositie plaats. We lichten enkele kunstenaars uit (die je mis=
+schien al kent van onze website), waarbij we een spannende mix van materi=
+aal en techniek presenteren. Tijdens de expositie staan we klaar om elke =
+vraag over ons kunstaanbod te beantwoorden. </p>
+ <p>De exposities zijn te bezoeken van maa=
+ndag t/m vrijdag, tussen 10:00 en 17:00 uur. De opening is altijd op de e=
+erste zondag van de maand. Dit valt samen met de Sunday Market die elke m=
+aand op het Cultuurpark Westergasfabriek georganiseerd wordt. De Sunday M=
+arket is gratis te bezoeken en staat vol met kunst, design, mode en heerl=
+ijke hapjes, en er hangt altijd een vrolijke sfeer. Een ideaal moment dus=
+ om in te haken en deze maand twee kunstenaars te presenteren: Peter van =
+den Akker en Marinel Vieleers.</p>
+ <p>We verwelkomen je graag op zondag 5 ju=
+ni 2011, van 12:00 t/m 17:00 uur op de Polonceaukade 17 van het Cultuurpa=
+rk Westergasfabriek in Amsterdam! </p>
+ <br>
+ <p align=3D"right"><img style=3D"height: =
+4px; width: 4px;" alt=3D"" src=3D"http://mailinglijst.eu/klanten/11909/Sj=
+abloon/green_point.jpg" width=3D"4" height=3D"4"> <strong></strong>=
+<a target=3D"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D=
+154041&a=3D10374608&t=3DH"></a><a target=3D"_blank" href=3D"http://www.ma=
+ilinglijst.eu/redirect.aspx?l=3D154041&a=3D10374608&t=3DH"><strong>bekijk=
+ meer werk op www.artolive.nl...</strong></a> </p>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td style=3D"border-width: 1px; border-style: solid; =
+border-color: rgb(167, 169, 172);">
+ <table width=3D"100%" cellpadding=3D"0" cellspacing=3D=
+"0">
+ <tbody>
+ <tr>
+ <td><img style=3D"display: block; height:=
+ 24px; width: 629px;" alt=3D"" src=3D"http://mailinglijst.eu/klanten/1190=
+9/Sjabloon/header_uitgelicht_fade.jpg" width=3D"629" height=3D"24"> </td>=
+
+ </tr>
+ <tr>
+ <td>
+ <table width=3D"100%" cellpadding=3D"10" =
+cellspacing=3D"0">
+ <tbody style=3D"color: rgb(119, 119, =
+119); font-size: 10px; vertical-align: top;">
+ <tr>
+ <td style=3D"width: 150px;"><=
+a target=3D"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D=
+154043&a=3D10374608&t=3DH"><img style=3D"height: 214px; width: 156px; bor=
+der-width: 0px; border-style: solid;" src=3D"http://mailinglijst.eu/klant=
+en/11909/juni2011/akker-adam-eva.jpg" width=3D"156" height=3D"214"></a> <=
+/td>
+ <td><span style=3D"color: rgb=
+(0, 0, 0);">Peter van den Akker</span><br>
+ <p>"In mijn beelden en schild=
+erijen staat het mensbeeld centraal; niet als individu maar als universel=
+e gestalte, waarbij ik op transparante wijze tijdsbeelden en gelaagdheid =
+in het menselijke handelen naar voren breng. Verhoudingen tussen mensen, =
+verschuivingen in wereldculturen en veranderingen in techniek, architectu=
+ur, natuur en mensbeeld vormen mijn inspiratiebronnen. Het zijn allemaal =
+beelden en sferen die naast en met elkaar bestaan. Mijn werkwijze omvat v=
+ele technieken in verschillende materialen: schilderijen, gemengde techni=
+eken op papier/collages, zeefdrukken, beelden in cortenstaal, keramische =
+objecten.”</p>
+ <p>Peter van den Akker expose=
+ert regelmatig in binnen- en buitenland bij galerieën en musea en is=
+ in verschillende kunstinstellingen en bedrijfscollecties opgenomen.</p>
+ <br>
+ <p align=3D"right"><img style=
+=3D"height: 4px; width: 4px;" alt=3D"" src=3D"http://mailinglijst.eu/klan=
+ten/11909/Sjabloon/green_point.jpg" width=3D"4" height=3D"4"> =
+<a target=3D"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D=
+154044&a=3D10374608&t=3DH"></a><a target=3D"_blank" href=3D"http://www.ma=
+ilinglijst.eu/redirect.aspx?l=3D154043&a=3D10374608&t=3DH"></a><a target=3D=
+"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154044&a=3D=
+10374608&t=3DH"><strong>lees meer over Peter...</strong></a><strong></str=
+ong> </p>
+ </td>
+ </tr>
+ <tr>
+ <td align=3D"center"><a targe=
+t=3D"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154045&=
+a=3D10374608&t=3DH"><img style=3D"border-width: 0px; border-style: solid;=
+ height: 279px; width: 44px;" src=3D"http://mailinglijst.eu/klanten/11909=
+/juni2011/vieleer-thesky032_bijgesneden.jpg" width=3D"44" height=3D"279">=
+</a> </td>
+ <td><span style=3D"color: rgb=
+(0, 0, 0);">Marinel Vieleers</span><br>
+ <p>Marinel Vieleers probeert =
+het menselijke in de bouwwerken - en ook vaak iets van het karakter van d=
+e bouwer of bewoner - te laten zien. Het zijn maar subtiele details die d=
+at alles weergeven.</p>
+ <p>De ‘tand des tijds&r=
+squo; of invloed van mensen op de gebouwen spelen vaak mee in het werk. K=
+oper, cement, lood en andere materialen worden in haar nieuwe werk gebrui=
+kt. Op deze manier kan ze gemakkelijker improviseren en nog directer op h=
+aar gevoel afgaan.</p>
+ <p>Marinel is gefascineerd do=
+or de schoonheid van het imperfecte. De gelaagdheid van ouderdom, het ver=
+val. De imperfectie die ontstaat door toevallige omstandigheden maakt een=
+ huis, een muur, een schutting, hout of steen boeiend. Het is doorleefd e=
+n het toont een stukje van zijn geschiedenis.</p>
+ <br>
+ <p align=3D"right"><img style=
+=3D"height: 4px; width: 4px;" alt=3D"" src=3D"http://mailinglijst.eu/klan=
+ten/11909/Sjabloon/green_point.jpg" width=3D"4" height=3D"4"> =
+<a target=3D"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D=
+154045&a=3D10374608&t=3DH"></a><a target=3D"_blank" href=3D"http://www.ma=
+ilinglijst.eu/redirect.aspx?l=3D154046&a=3D10374608&t=3DH"></a><a target=3D=
+"_blank" href=3D"http://www.artolive.nl/work/165738"><strong>lees meer ov=
+er Marinel...</strong></a> </p>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td style=3D"border-width: 1px; border-style: solid; =
+border-color: rgb(167, 169, 172);">
+ <table width=3D"100%" cellpadding=3D"0" cellspacing=3D=
+"0">
+ <tbody>
+ <tr>
+ <td><img style=3D"display: block; height:=
+ 24px; width: 629px;" alt=3D"" src=3D"http://mailinglijst.eu/klanten/1190=
+9/Sjabloon/header_selection_fade.jpg" width=3D"629" height=3D"24"> </td>
+ </tr>
+ <tr>
+ <td style=3D"padding: 5px 5px 2px;">
+ <table width=3D"100%" cellpadding=3D"5" c=
+ellspacing=3D"0">
+ <tbody>
+ <tr>
+ <td><a target=3D"_blank" href=
+=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154048&a=3D10374608&t=3D=
+H"><img style=3D"border-width: 0px; border-style: solid; height: 92px; wi=
+dth: 92px;" src=3D"http://mailinglijst.eu/klanten/11909/juni2011/nw_17372=
+0.jpg" width=3D"92" height=3D"92"></a> </td>
+ <td><a target=3D"_blank" href=
+=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154049&a=3D10374608&t=3D=
+H"><img style=3D"border-width: 0px; border-style: solid; height: 92px; wi=
+dth: 92px;" src=3D"http://mailinglijst.eu/klanten/11909/juni2011/nw_17386=
+9.jpg" width=3D"92" height=3D"92"></a> </td>
+ <td><a target=3D"_blank" href=
+=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154050&a=3D10374608&t=3D=
+H"><img style=3D"border-width: 0px; border-style: solid; height: 92px; wi=
+dth: 92px;" src=3D"http://mailinglijst.eu/klanten/11909/juni2011/nw_17398=
+0.jpg" width=3D"92" height=3D"92"></a> </td>
+ <td><a target=3D"_blank" href=
+=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154051&a=3D10374608&t=3D=
+H"><img style=3D"border-width: 0px; border-style: solid; height: 92px; wi=
+dth: 92px;" src=3D"http://mailinglijst.eu/klanten/11909/juni2011/nw_17390=
+5.jpg" width=3D"92" height=3D"92"></a> </td>
+ <td><a target=3D"_blank" href=
+=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154052&a=3D10374608&t=3D=
+H"><img style=3D"border-width: 0px; border-style: solid; height: 92px; wi=
+dth: 92px;" src=3D"http://mailinglijst.eu/klanten/11909/juni2011/nw_17390=
+4.jpg" width=3D"92" height=3D"92"></a> </td>
+ <td><a target=3D"_blank" href=
+=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D154053&a=3D10374608&t=3D=
+H"><img style=3D"border-width: 0px; border-style: solid; height: 92px; wi=
+dth: 92px;" src=3D"http://mailinglijst.eu/klanten/11909/juni2011/nw_17398=
+4.jpg" width=3D"92" height=3D"92"></a> </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td style=3D"border-width: 1px; border-style: solid; =
+border-color: rgb(167, 169, 172);">
+ <table width=3D"100%" cellpadding=3D"0" cellspacing=3D=
+"0">
+ <tbody>
+ <tr>
+ <td><img style=3D"display: block; height:=
+ 24px; width: 629px;" alt=3D"" src=3D"http://mailinglijst.eu/klanten/1190=
+9/Sjabloon/header_agenda_fade.jpg" width=3D"629" height=3D"24"> </td>
+ </tr>
+ <tr>
+ <td>
+ <table width=3D"100%" cellpadding=3D"10" =
+cellspacing=3D"0">
+ <tbody style=3D"color: rgb(119, 119, =
+119); font-size: 10px; vertical-align: top;" valign=3D"top">
+ <tr>
+ <td valign=3D"top"><br>
+ </td>
+ <td><span style=3D"color: rgb=
+(0, 0, 0);">ZONDAG 5 MEI - Juni expositie in de galerie van ArtOlive met =
+Marinel Vieleers en Peter van den Akker</span><br>
+ </td>
+ </tr>
+ <tr>
+ <td valign=3D"top"><br>
+ </td>
+ <td>Opening op zondag 5 mei, =
+tijdens de Sunday Market op het Cultuurpark Westergasfabriek in Amsterdam=
+. Je bent van harte uitgenodigd om tussen 12:00 en 17:00 uur langs te kom=
+en!<br>
+ </td>
+ </tr>
+ <tr>
+ <td valign=3D"top"><br>
+ </td>
+ <td><span style=3D"color: rgb=
+(0, 0, 0);"></span>Daarna is de expositie te zien op werkdagen (ma - vrij=
+) tussen 10:00 en 17:00. De expositie duurt tot 24 juni 2011.<br>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td style=3D"padding: 30px 15px 15px; text-transform:=
+ uppercase; color: rgb(119, 119, 119); font-size: 8px;"><img style=3D"hei=
+ght: 58px; width: 59px;" alt=3D"Kunst Koop" src=3D"http://mailinglijst.eu=
+/klanten/11909/Sjabloon/kunstkoop.jpg" width=3D"59" align=3D"right" heigh=
+t=3D"58"> wil je niet langer door artolive geïnformeerd worden? Klik=
+ dan <a href=3D'http://www.mailinglijst.eu/nieuwsbrief/edit/?e=3Df00f@djc=
+bmachines.nl&c=3D9856&l=3D100549'>hier</a> om je af te melden. <br>
+ kreeg je dit mailtje doorgestuurd en wil je voortaan =
+zelf ook graag de nieuwsbrief ontvangen? <br>
+ klik dan <a href=3D"http://www.mailinglijst.eu/r=
+edirect.aspx?l=3D154054&a=3D10374608&t=3DH" target=3D"_blank">hier</a> om=
+ je aan te melden. </td>
+ </tr>
+ </tbody>
+ </table>
+ <!-- MailingLijst_code --><img src=3D"http://www.mailinglijst.eu/imag=
+es/10374608.109906.aspx" border=3D0><!-- einde MailingLijst_code --><p><C=
+ENTER><SPAN STYLE=3D"COLOR:#d3d3d3;FONT-FAMILY:verdana;FONT-SIZE: 10px"><=
+HR SIZE=3D1 STYLE=3D"COLOR:#d3d3d3" SIZE=3D1>Deze e-mailing is verzorgd m=
+et <a href=3D"http://www.mailinglijst.com" target=3D_blank class=3D"ml_li=
+nk">MailingLijst</a></SPAN></CENTER></p></BODY>
+</html>
+
+--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d--
--- /dev/null
+From: Sender <test@example.com>
+To: Recip <recip@example.com>
+Subject: search accents
+Date: 2012-12-08 00:48
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: 8bit
+
+line 1: Глокая куздра штеко будланула бокра и курдячит бокрёнка
+line 2: crème fraîche
--- /dev/null
+From: Sitting Bull <sb@example.com>
+To: George Custer <gac@example.com>
+Subject: pics for you
+Mail-Reply-To: djcb@djcbsoftware.nl
+User-Agent: Hunkpapa/2.15.9 (Almost Unreal)
+Message-Id: CAHSaMxZ9rk5ASjqsbXizjTQuSk583=M6TORHz=bfogtmbGGs5A@mail.gmail.com
+Fcc: .sent
+MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka")
+Content-Type: multipart/mixed;
+ boundary="Multipart_Sun_Oct_17_10:37:40_2010-1"
+
+--Multipart_Sun_Oct_17_10:37:40_2010-1
+Content-Type: text/plain; charset=US-ASCII
+
+Dude! Here are some pics!
+
+
+--Multipart_Sun_Oct_17_10:37:40_2010-1
+Content-Type: image/jpeg
+Content-Disposition: inline; filename="sittingbull.jpg"
+Content-Transfer-Encoding: base64
+
+/9j/4AAQSkZJRgABAQAAAQABAAD/4QvoRXhpZgAASUkqAAgAAAAIABIBCQABAAAAAQAAABoBCQAB
+AAAASAAAABsBCQABAAAASAAAACgBCQABAAAAAgAAADEBAgAOAAAAbgAAADIBAgAUAAAAfAAAABMC
+CQABAAAAAQAAAGmHBAABAAAAkAAAAN4AAABndGh1bWIgMi4xMS4zADIwMTA6MTA6MTcgMTA6MzM6
+MzcABgAAkAcABAAAADAyMjEBkQcABAAAAAECAwAAoAcABAAAADAxMDABoAkAAQAAAAEAAAACoAkA
+AQAAAMgAAAADoAkAAQAAAGsBAAAAAAAABgADAQMAAQAAAAYAAAAaAQkAAQAAAEgAAAAbAQkAAQAA
+AEgAAAAoAQkAAQAAAAIAAAABAgQAAQAAACwBAAACAgQAAQAAALMKAAAAAAAA/9j/4AAQSkZJRgAB
+AQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwc
+KDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIy
+MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCACAAEcDASIAAhEBAxEB/8QAHwAA
+AQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIh
+MUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpT
+VFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5
+usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAA
+AAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEI
+FEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVm
+Z2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK
+0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDq77xdrX/CQ6laRXjRxQTF
+ECovA/EUg8Sa6W/5CUuP9xP8K5yWQnxjrw9Lwj9BWjkgZHFAG6mu6yV51OXP+4n/AMTUq61rBB/4
+mU2f9xP/AImsJJTuAJFW0YDnfmgCTUPFGqWFq882p3G1eyqmT/47VfRfGGpawkgGp3CyIeg2cj1+
+7XK+O7zybCGMNjzHyR6gD/69ZvgG8zqU67vvRZH4EUAesJe6m/XVLv8ANf8A4mpf7Qvl/wCX+6b6
+uP8ACs+ObKdaeh3Hg9aANTw/4gurjxTLpU7tIv2cTKzHpgkH+n5UVheHGI+KWzJwdNP/AKFRQBzD
+7f8AhMfEDEHH24j/AMdWrs0oCkDrVKJs+NfEsZ+79u/9kWrd5GqKTmgCstwwkyT0p5uzu61mOzbj
+zSFn3DmgDB8ePLPe2MEQZyykhRzk9/5Va8D6Vd2Mz3d3CYxJHiPd16+la0hhMybkUvxhj1HWr5uM
+uB0wMYoA3YJARjvV+DBPasC2lYsOuK3LVunWgCLQRj4sIPXTGP8A4/RSaF/yV2P30tv/AEOigDmY
+QD408UE9ftw/9AFXpv3iFT9Kgs4t3jXxV6C+H/oAq5cxkMcCgDFltXVyMVVv7iGwtzNcNsQfiT+F
+a8jbAxdgFAzmuTZZfEV81vG+xTyX/uIPT3P9aAIBr9vdNHcQI/lxk5DDBrfsLuK+jE0MqupPOOx9
+KzNY8L6fbaaYrdGXb3BOcnuT+FcpodzN4c8RRRyylrW4IDE9MdM/UUAes2wbOAK27PhRms6CJlwc
+VrWowRkUAV9CP/F3YffSm/8AQ6Kfo+P+FuWp9dLf/wBDooAxrH/kd/Ff/X8P/Ra1evUOcgVW01Qf
+G/izIz/py/8Aota2LqPK4xQBxniWc2mi3MxBGFA/Mgf1rmtEF/Z6HNqMNuzvPnY+7G1V6Hoe+T0r
+qfGmnT3Xhm8WNWJVQ+B/skH+lUPBt3d3PhuzXyBM6xBY0YfKDnALewxmgDE1BfEDaPaXNzMRJPIQ
++TjCgDHb69u9ZGt2Us2lrdNDtMLAgq27Kng84Fd74qnaMwWB8qWRTnzUcfePGSOx4ziuf1kzT6S9
+tuRHlVUG5sDJOMA+lAHofh5/tvh3T7k4ZnhXcfcDB/UVuRQEdqzvDelPo/hywsJGDSRRjeR0yeTj
+2ya3I8/3aAMXSU2/FmzJ/wCgbJ/6FRUunf8AJV7H/sGy/wDoQooAyNJXf448XYPS+X/0Wtb8ynyj
+0rm/DIll8W+KDKQ0pvF3FehPlr0rvINMzbfN8rsc7upH0oA5ie3mktZSI1ICn5W43e1ec6ZrDwax
+facIj9liUNtUcgE8j0IzXrHiqS20rQJbiadoyBsWQjc2T2HvXnvhbREuzeXTbvMlfILcsF6D6jFA
+GJr+pWE1ymFkwFzhlwo+i1xevazLd3Fva2+UiQhh7kdPyr0jVfA8t0BeXNybe35UK2EJAJwST/QG
+uS1Pw7HYalbKHUIxYxyDd8wHUnNAHsnhXVBrGhWkrBlmEYVww6sAATXQInA5rn/AOZtIa3mQHZI+
+xwfvAnJ6d8n9a6yazEKhlzgUAc1YAr8WbH302X/0IUU6xBPxYsSe2my/+hUUAV/Bdj5fi3xWJJDJ
+JHeopY8bj5a5OK9AUArwARXEeFjjxh4xbub5f/RYrsIZgJhGTjcuQMGgDnfHiwnw1KJoVkUuB8yg
+hfeuZ+HemTLpjx3OCZNzKUbPy54/Sut8Z263OlJE1wYgzkkjvgH86yfBb+XYWuIGiEithWzn9aAN
+loTcO0ctuGjV9oMg5JGCSOOnp9K8/wDH1qH1iERrukRAqqB3Jzj9BXpsk6F+oyCuRjJ54rhNcg+3
+Ge5XiUSL5ZGc87sdPagDQ+HlvJHoAdo9h85mUY7dK7WSRCoB6HiuV8IiW10JYs7yszDJ7fN/k1tG
+Rpb4xj7qpnj3Iwfx5oAwLMgfF+1UHI/suTH/AH3RTLJNnxltx2Olvj/vuigB3hgf8Vp4vH/T8v8A
+6AK6aRWFk2CA2CPSua8M4/4T3xcp/wCftD/45XR6q32e1JjUySCRdqA4J3HH9aAKHiJTceH4mliK
+r5e5lDfMpx2Iqp4eQR6Zp75Y4jX7xyfTn8q29djjbS/LMqxYGFdugNZWlskOh2pKgYj2AqO4OB/M
+0AW7+NLQ3Fwi/O6hsk5yRwOO3WuS1qGJtNuvN3iNJkX5e+EIxn8f1re1e4ubq8jSOMiBArZJ/wBY
+xOcfQcZ+tVNTsYh4dnjmG9PMJIP8XYUAQ20z2Hg6OeJGTYQzd+N3Le+RzXQ6TGwtjLLkuxAy3XAH
+f8Saw9Mlt7vwsI4yZI9m07xtyM/y5rqodqxIFAIx1oA5iDj4w2ZHfS3/APQjRSw8/GGzx20x/wD0
+I0UAee+I/GV/4S+IXiAWlvFKJ7gM28njC+1Ubn4v6xclC1hbAq6vwzdjn+lXviB4X1O88b6lPBYX
+EkUkgZXWJiDwPQVzH/CH61zjSbwj1EDf4UAbV78YdZvYPJbT7UA+7HP61HbfFXXLW3SFdOtSqZ67
+v8fesg+Ddbzn+yL3P/XBv8Kcvg3Xc5Oj3x/7YP8A4UAaY+KuvIQP7PtM5JXKt/jUF78Udcu7F7WS
+ytEVv4grZHPB61VPg/Ws/wDIGvs9v3T/AOFMPg7XcHOk32P+uD/4UAWLb4l6vb2zQJZ2m1gP4WGC
+FAz19q17f4va0sSobS04GB8rf41z3/CIayOuk3g/7d2/wqRfCWr8f8S27/78P/hQB33w78Q3fib4
+jR3l3HHG6WTxgR5xjOe/1oq78JvCmo6dq8+qXUBhhETQqJAVYsSDkA8496KAP//ZAP/bAEMABQME
+BAQDBQQEBAUFBQYHDAgHBwcHDwsLCQwRDxISEQ8RERMWHBcTFBoVEREYIRgaHR0fHx8TFyIkIh4k
+HB4fHv/bAEMBBQUFBwYHDggIDh4UERQeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4e
+Hh4eHh4eHh4eHh4eHh4eHv/AABEIAWsAyAMBIgACEQEDEQH/xAAdAAABBAMBAQAAAAAAAAAAAAAH
+AwQFBgACCAEJ/8QAVhAAAQMCBAIHAwYHCwoGAgMAAQIDEQAEBQYSITFBBwgTIlFhcRSBkRUjMqGx
+0RYXQoKSssElJjNDRFJicpOi8CQ0NmNzo7PC0uEnNVNUVYMJRWR08f/EABQBAQAAAAAAAAAAAAAA
+AAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDqjNudMEyw6lvE3HEqUnUS
+kDSmeEkkCTB241Aq6X8mgSL5J/PT99DPrX3S232GwohPbp5/6v8A70PMsrSuwYKgkgtjiONB0Ovp
+lykFQLhJ8+0Fefjoyh/6/wDfFBctsJWkpt0wRzSK9ft7cwVMNmf6IoDOOmjJ5/lB/SFejpmyeT/n
+I/TFBdFqwACGGR6IFOWbRhZ3Ya349wUBgT0y5QP8qT+n/wBq3T0x5OPG8A/OFCy3tbZCRpt2j+YK
+cotLMjvWjM/1BQEs9MWTR/LUn0VWfjiyb/72hki1sw5q9kaHL6ApZDFspQHsze3DuCgI6umLJqUg
++2Eg+AJ/ZSR6aMmj+UOfon7qozjTACUhlsCP5opo402AQWkeumgIK+mrKAA0uuK8e6R+ykj03ZU5
+JePx+6h6gaTGlPHmkUwzDjlrhDKVvtIWXVaUIjj4+6gKKOm7KZUQrtU+YCj/AMteHpuyvvpbeV7l
+f9NAHEc3Ls8ed9iYZLTZIAI4+vpRAy1i7WKYS3dhtIDghQHIjiKC9npwy3O1rcH3K/6a1V034Ef4
+OyfPqlf/AE1WG1KKxAAHhFO0LgTCfhQTf47cGPCwuJ/qr/6a9R004as7YXcn0bX/ANNRLbqhsAOF
+OWnVgEnh4UEm30wWbhhvBL5e3Jpz/ppwOlMLSC3lzElT4suD/kqHN642YT9tIu3T7h2UYHnQWW36
+S9YleXcQA8Qhf7U05b6SMPXiFpYGwfZfu19myl9XZ6leEkR9dVBy7XEFXLcUN+mPEn7VnCrplZQ6
+zclaFeCgAQfqoOqbO4L6VBxvsnURrRqConcbisqo9FOYE5mw1zGENdkLllpZQSCUkApO/wCbWUAY
+63i9N0x3v5SkHy+aqj5QBXhFqRzbFWrrjrWm5a1ER7Unh/saqOSXP3Bst/4pNBbWhLY1nvCt1NyZ
+mRWtuoKESCOdKjSOB99BiEjYHYU4ZQpKpTwpArTxpVt8g7RQPmlQkpP2UoFE8VkU0buEkK4A+NbN
+rCzHDagfQCmN69TtsPjTRLoBA1b0oHFaZPwoH6IPODFJPwOG9asklJJ228a8egp9aBspcHhvQf6T
+b55eZHWVu6m2kgISDsnaT76Kt46hlpx1WyUJKj7q59xzEFXV9dXC1krccK9/M0CybtYUTrMnj50R
+uiPGLl69OHBTaWG2isgjdRkffQgFyoqO441eOiW4WM1W8D6aVBXpFAfGCVGQmnDZO07jmaYW74G0
+yKfNuJUBB50D0ISAIAiNyK3UUkQIpDtgUhAAIApNToAKQACedAotaZgCfOvQrUCAIpsVAma31kEE
+bbUG60Dwn1oW9O6S3h2HqnYvK291E1SlEkzvQr6f1qRg1gVHc3B4+lAaeq2rXkFhcz3NPwcXWU06
+pzqnchtQ4CkBQjT/AKxVZQDXrpOEXLJO0XYj+xFVDI6lHAbEeLSfsq1dd2UPWyxMG7HHhPZCqtkX
+/wAhw9Svo9gj7KC4Wx2MCKdt7ncTTaxbGmBz3p4hISNzHhQJrQSYnjXiUkQOYpZzupJJpFK5g7n3
+0ChQSkbxSluSFDfam5WACokj1NetPAqA29KCXb0HdQ9TSwQlSduVR6XBpJnetkXWgCTQSTQCRpMk
+Uk8djzpsm9ExNaOP6pOqBQRWcblNtly/eI3SwoTPiIrnbEXAl7j4UaulrEHLbJ9z2ZEuKS2fQneg
+Fevy62ASQYoHCHkqXsqrl0U3aWs52mpelLgU3v5jah20/C9zO/hVmyc8Bj+HjfvXCB9dB09bq0mD
+zqQZUkAVDtriOdO7ZzSYO8UEoHBw3r0KB3mm6XUxERWdpO0UDhJBVvSulKh9kU2bUdzvvSzKu9tI
+mgXShOjfaTQk6xZ/cOw4GLk7/m0XlqlGxEcCDzoP9Y1KhgNhI/lR/VNAWuqGpJyG3pBHdVPn84qs
+rTqgqJyKyCI+bXHp2qqygH/XoQEW9i5M6r0CJ/1VVnJKP3Aw8QP4BvYf1RVh69K1lm0BMgXyQPL5
+moPJ50YDY+TCP1RQW+yRoaC44HhSygNXe99NbW7lCUKA48K3cdgbk8KDa5IAMcKj3XiklIMTSrj0
+zCppi8STwNAo5cEo4RXtq53pn1pke8NIUR5UoydWoA+lBMdrCdjSLr5G8yfCmJdWE6ZpNx2NiZoH
+4uZjlThKitJ35TUOl4DntUlZOgoiaAe9Nl+tnDbS1QAULdlzxHgftoMLdm4VzAmKKvTsvsrywUqS
+y8y42seBBBB9xoSWTa7hVwocGmyrh7qDZpUKFXzoptG77NdoFkwiXD7uFUvCMOu7+5SxbNLccO8A
+TRd6Nsq3eB5mbVezq9l7ThsCTEUBcQvhtuKdWy0qVJ40wS4NqcNOTvA+NBKpMokRSiFKnx8opo07
+KAAYNOGwrko0DlB/JO005Y0gTNNW9pkzTm3SDJJoHIQhIlKZ50IesgFDLdgYgC74/mmjC03KaEXW
+WBTlmykH/Ox+qqgJnVAV+8S3hMbOA78fnVVlZ1Oio5BbnkHI/tVVlAOOvSIbtlSN8QSD/Y0yyayh
+3Ldg4BBNs37+6KedfERaWqo2+UE/8E1FdGdwF5Yw0kgzatmPzRQWdhnRClCTWPAEHeKkENodbSpO
+xpBy3IJJjegjFwjh6U1eWSoxw50+uGQHPE86j7qEqiY9KBstYSe6B61gfLapB5QaaPOQs77V4twR
+wNA8TcFyPKsdd25UyS6RBAivXXJSDQOm1jyinlq/CoSY22qDDh1AVI22mUqmNqCpdI+GXOZb9Ng1
+I9jZL+qNiVSAPiKiMhZQFixcHGGgkvJPaJJ4JBolsKaTdO9zdTcKV4CdqalLSrtevcQEkeVA7ytl
+/CcOm7sLVKFugb+VSt06k3WgfSSO9HKss30NsagAkAd0GminCXSr8onegfNKSDO/nJp22tCSDUYF
+xEJpVDkkwDPrQTTCoGxBmnjDp4KG9QVu4rVxjepS3WY1KVPpQSbSiT508tBvBImo9hJICt96f2yS
+TwIigkWht6UHes8ojK9mJE+2Dj/UVRhaEpn7aDnWhH71LP8A/uj9VVATupmZyCiePzn/ABVVledT
+OfxfszG6XYj/AGyqygHfX3A+TrPmTiKZ3/1JqvdFX+h+EqPO2TU91+NrK1Mf/sER/YmoLopSpWR8
+II/9sPtNARbNSdAKdzHCtnY0Eq4zypnbFaEAqImnRUFt6p93OgjbyEEn6qhrxQUo+FTGIp7m23rU
+HeGKBi6E6orRcRtWyiSoxWp4ERQeoAI3NeLSnnW7Q2341s5skcOMUCSEpMk08to0gA/Gmu4HCvW1
+qoPMQWtOJIDR/I3HiKyycacfU4o86Y4qpxBcutiUphIpLLy1LGtSN1RsTwoLS0SQpx3uoG4HlSHa
+gq1JMSa9ullVulJAA5+dN0wYiKCQbcJ+lNLoVzmmjKoEmndudSoVAHpQOrbVq3mIqWtYME7Co9oE
+xHAeFPWVqCQY250EsyuBsSPWn7C1CI4+tRdq4F7gjzFSduoCBHoKCRYUYAG1CHrQJjKFqqB/nqf1
+FUWbck96NO3jNCXrPmMm23Ha9R+oqgJPUvM9H6PLtR/vTWVt1MkoT0eMaQAVpdUYPPtSPurKAb9f
+ZQNkwnf/AMwR/wAA1GdD7QOQMHUeduPtNSfX3H7n2y4mcSSPT5g1G9D69XR9gxCeFvHHzNBczGkR
+tWqXIBjlW4HdO0U1XqQsGOe9ApcAKTHEGoe7tyJqWWSBv61roQ6nvCgrD7cEyPqpNCYn0qavrMhW
+w5U1NsYAAoGLAEmaUcAinItSZ8R4VjjJ0Qo+tBHK+NeoI0kVXs3ZptMIS42wk3FwB9EGAPU0KMWz
+zma6uCtm67BoHZLSQEx6nc0BdzRfBq0UhKu8BwFI4BchttK1EyrlPCqjhmJOXlrbquHUPOrbBJJ5
+8+FaXC32NTguLi2MmCYU2fLjPxoCel0vDUFFQPCnSAQaH+Vc625fRhuKpbtX+CHAruL+6iC0QtKV
+JUFA8CKByyFE8KeI2CTzpG2E8h8acpQSRCRH20DxlRMGPqp2zOkJB2prZtlRIg/Gn7bSUwNW9A+t
+BAO4B8Kk7UeJmo20SUkmeHlUnbK2G1A+aiZjfxoS9Z8k5Ltwrleo4f1VUV0d6IG/OhR1mkg5HZkz
+/lqP1VUBI6l5H4vmRJOzvHl86aykOpOf3iEb/Sdj+0rKCgdfUj5NtxzGJI9/zBqP6Gkj8XuDDn2H
+/Malev6lHybakQD8oNg7f6hVMehVIV0dYMqJhj/mNBcm29Q24U3u2gDtUohvS2dtzTO6H+IoI93d
+PmBTcq0xBmnDoM7b7U1KZUQCBvQLKcQtuFRSCwAmBFIvBxJ2MGaVAUUhQIngaBNUg7kVV81YoUhV
+rbuweCiOPpUxmC9Rh9it5Su+dkDmTQ9Nz2q1PrO5JCd5JNAxxyztBaOOXyktsNp1rUefqaH7dpc5
+hvlDCLJabVJgOLG5H2D0q7YhhV5nDFrfBLYrTbA9reOAwAkHYep/YKL+X8qWGHYe1aWjCUBtMDQn
+c+cmgDtvlq7scEQ2grTdtnVqKdhVXx3EscYTpumoIkdogbKHmDXRWJ2NotlxpbjRUOYVJ9N6G+bc
+Mt3EdghaFx9IAUAXfxB25bIe0haOHd+qiR0QZ1dRdIwXFXdbayEsOqP0T/NNU/MuXH7Qret0KKUn
+dIE/CoCxUW3UnfcyCOINB2BaiRI8Nop+0gxP21R+iPHF45gCUPq1XVsezd347bGr8wlQERQLWp0r
+Gw41IoAKp2k0xQkg/Qnwp9bSYJSZFA7YSfjUhbN6uMAimTBIUCakbZW44UD1hkJAoUdZ9pKMgNqH
+H25H6qqLTR7oPOhZ1ngD0dSOIvGz9SqC4dSZJGQyeRU7H6YrK36lBH4vk7flvQfzxWUFE6/oBwm1
+Vz+UWh7+wVTboPSR0cYKkCf8nk/pGpHr/o/e9aLjf5SbH+4XTPoOUk9G2CyCT7PwA/pGgvaR3eAN
+M71E8qkkJBRsDvTa7QNBFBCuoHGKaqbGuZEU/udIERvTbUmSNO9A0ukjSTG9Nm1BC5PDnUg8pIHC
+ozEn0tMLcCeCSfqoB9nrElXOLi2bJ0IOkeE86rVzcBDKnAoBLRgCNyfL/HjXmO4koPaoPaOEmf5s
+86aYHbPY7mmywdk6mmyHbjwKZkz60Bl6NcGRhWX0XDrWm6vPnnIHeAPAe4VZx2iEFSVrKYgpI0/X
+UJeYuzhrae0Qt9SANmwdIH7ajh0j4WrW204dYEEE8NqCXW6+7dP2yUMNnTKdZJ24cao2ZGCVO9ol
+tzcmUnh/j7qXTm3tbC8xdcKQnsWRJ7pIUoq+qKqeMZ3YUp9xxxsJcUCQngOJ2+NAmptDxKHEqSCN
+vGhnnCxOHY8sISEpe76YECecfb76J2GXDV8wLj2a4DDhlDikEA+h8Kr/AEk4St7Chdtd4sHUI4xz
+Hw391At0JY38mZntmnHdLV38y4nlP5J/x4102ywVAEbiuLMDfcYeauGjpUlQUDHMGu1MpXPyll+x
+vkbh5hKifOKBZtuCRoiKWbQoGYpx2KkmYO/lSiGjI2EeYoMaQSSDTplJA2rxtJG1OmUgD/tQKM6h
+50MOsv3ujlwgzF02ftopJMAAxt5ULeslKujq5I/9w2frNBb+pM6Dkfsuep8n9NH31lJ9SUfvOBgb
+F/cf10VlBVuv7q/B61g7fKTW3/0Lpn0IjT0d4IN/82HPzNO+v8Vfg/apkR8osn/crpr0HEno5wUE
+cLeJnzNARGSCmCD8ab3IBkAcqctjbhwrV1oqTMAUEBdpABnemRAHvqYu7WASQDNMOw7xAKfHjQMn
+YjvDaq/m64RaYFevcNLSj9VWp+3IEyKo3S8U2+RcSXIkthI9SQKAKXV4u5ue21BLY3JngImrb0S2
+TrmCXuYVuKb9pfUkr0nZpA4Ty3maHDSXsQLOHW/8PdOJZbHLvH7AAfjXUOW8rWWGZbssMTZtutMt
+BGpbQMniVRE7mTQDzMfSJctYXcpwbLq30MoSFP3KinXO3cQBJ9SRQpu/lvE8WS+jCQXXVAyylaUu
+T68TXQubsNNpZK1YwLVsDV3mSSPSFCoPJeTH3rv5dvO2UiYtu1RpKxH0o4xvzNBHXuX0WnRpcGzQ
+supWlSyqZMjccfGN6FGG2+Iv4u5cW+ALxNLSoaZUfmwoc1Abq9Jrq3GsPtrbI12gcFNHaI3oJW9q
+7Z9qbEpSpapg+NA2ezHnJ4rZxLCrZLVshHYWzdutBXPECCoAjzqRtlKxNpxp22U0tSTLS4kCPXen
++XLW5xR2EN3iXE/SCmwUj3wD9tTSsHLPdUtK1cYiaDnq1bXa4jcWLuy23FAe7b7q616v1yb/ACFa
+JMyySg78INc19IViLHNAuG0hPa94jzTxo79Vq/DtjiWHDcJWHk+iuXxFAXrls7xO1etJkCCaevNy
+NXOvGGxG6QDQaIbM8AactohMRWyWyD605aSInwoGwTIgCD40MOsa3/4cXh8Hm/1qLSkhMqNDHrHN
+T0Y38Dg43+sKCd6k4H4EHxDj36yKyvOpTIyZpJPF79dFZQVLr/T+Dtt4DEmf+Cuk+g1uejfAzEk2
+wn4ml+v8EjK9sYknE2Y8vmXKT6BQ4OjTAyT/ACfh+caAiBoDYCaxxOlAGnjSralQJg7V4+sdmBtQ
+Rd2gaZM7nwqLdShCzE1NXS2wmJmot5CSdSQregY3KthE+kUNOnjtF5Hf7PVHatlfoFCim+hOkwDI
+86p3SRhyr/Kt/apaKipklIHGRuKDmjKV81ZZuwi6cWSlNzpE8BtE/EiuqLbMPtFi2WVDVA2rjXEy
+5bX1skApW2AQPA6ia6awJQNvavpSZWgKI5EEUBLwmwbxFSHMQYZd098FSQQD761GMWuK45eYPhYT
+cKsUJNw4ncIKphI84BJqs5hzKcPy4+q0lVypISNIkk8gPEkmKnsuZCvMHyEi3sMQFnmC6V7Re3Rb
+16nFcUnyTwHp50Epmxm3ssnrYeUe0uEHSIoIYg0cKS9cOW5Uw1ClqHEJ5n1q59JWKYhh7tthN0+u
+7et2NetKY1pHExyoft4lfYnir1u6sPWL+mUqT9HxE+HxoLhaXS2sND9k5rbdROpJ2INa212OzK1L
+nn3qhMtMPYU67hThKmEElon+YeHw4e6nFxPblQBKPyY/x5UAt6VLwPZoba3DYbn3knf6qLXVGbcV
+e4u8QezS02kEjmSTFDHNuXcRxnNDKrG3U4SnRJMSZ/711B0I5OcyhktuxvA37a4suPqRuJ5CecCg
+ujoEQfCtmOI2rZ0d2vEpEbcKBy2AVcvfS6UgbDnTNJPnSrRK5UmRQKqC9ESDvyoY9Y4q/FlfiQfn
+G/1hRPWogaYihh1joPRhfEj+Mb/WFBMdSon8DgDyL8emtFZWvUnCRlFYSRIL0+utP7IrKCr9f3/R
+q2PH90mR/uV170EKKejLAxAP+TbfpGvOv+CMsWpPPEmY/sV1r0Bk/izwPcx7Od/zjQEhlRUNk+6l
+XEymY5UmyDp4x50uggpHemgj3mZSSRA8aadmEqAVFS9yfmzHhUU8UzvyNB4tpBG4NMLy2YeZWFN8
+QeNPA4QdMkT9VIvOHSUhQ3oORulXJl7a5tvbq2tyq3U4FIQBvBAO310bMBsQcKw+4ZIIS2kKSR5V
+KZ9sPaWm1WqW37i2lSxH0gBBFUTLeamLW+Tg97cBt9StSEcAB/N9aAws4LgLCbTEbrSi3aWLo61d
+1Kk7ifQ7+6vXeke2uLdCsvYLf46tThSk2zKuzBH85wjSBVeTeu3rCMPNom/tnFauyUdKfzvETyqb
+fubpFs2wthtCUCENsSAPACKCk50xPMvt72L4nkt4Xa2SygtuIUkIIjeFHkfKh5huMWdk+W73Bb2y
+UDu6WSpER4iYq45rwS9duV3Nz7Ssap+ceJ5cAJquM9o2QEIJ7wkKPCgnsHxCzxa1F0w6haSkplJ5
+UjdH5tdwe4gCRJj/ABzpo0lq3X2rbKGVLBBKe7J9OdUnpQzULWx+R7R3/KHk6VkH6CefvNBMdEmc
+13PSMm1u3W12C3VBgFIlJ8Z84+uuvLIzaoKtyRXzpy7fXFjcpWypTa5ACk7Eedd99HmJt4zlHDbx
+D6XlKYSFrTwKgN/roJ1xHdFY2J3mt3EmIrUCO7FAqhAVsRtThIAiCD5UigGCZO1bpSqNiaBR3SpH
+CCKFvWNI/Fne7TLjY/vUTl6tJoXdY0KPRnfGP41sf3qCb6lP+h7h0gDW8J/PTWVp1Ilzk59E8HXf
+tRWUFX6/5P4NWoKhHyizt/8AS5SnV+bDnRpgaU7xb8PzjSH/AOQNUZfsk+OINH/dOUr1eFhHRlgg
+3nsJHl3lUBONvGxTBitUthPGZpVLx3kztz51p2xJ2igTuWu6ajLhvjvUs8XltEttqWI3IrZGFJWl
+K7l4hSk6uz578KCtLQrXEz4RUknAHT2a1uaFkayI29KnWcIsrZ/tN1rSJgqmo1u5vLpt1p1pTUPE
+srk7jjvQBvH04hhuarxzELlaGn0Q2hHBJ3gTXOefrwt5oadUl1p9h4HQvYxqkGuoOmF+1U0bJ25Z
+aeWsKMqEq8QJrm7F8PcxfPTLKy12LSzD6mtaFJBkBQHHwoDBaYld4c004SpbRAKVDiAeVWFrPNi1
+aAugrlOgEHcbc6jsv263sBZtbhse0sICXE6eGw5elRSsJtA4/rag8Y+6gaZqzheXlyvsSWrdHdSh
+KREcKgm8bt0IlxYTGxkQaZZvtW14u0D80yGwVRtJBqCxRy2ClOaU7bJkzvQSmI5mff2YgNjYeJNC
+/MTy38auHnFd7VzNWW9uDDVtbgLed2QmOE8SagMesfZr0NKJU5pBVvxJoNMLt3HvyoI70zw3/wC9
+dP8AQRnVzD7K3wm5uGlWSTpSeaT4ek0CMm4GL68asw65LpCNCB4kcffRz6JMAbX0iXOEBxsJsWUO
+IhI0qBH+PfQdDsrQ4RpcSraYml9AUkq2EVDW2GO2OKe0hsr1JIUUq2I9KmEPs6ezSpJWRtNAow2C
+jma2TqA7vCvGFOCQpPpW5PmRHGg8UCUgn30LusgI6Mr8CZ7Rs/3hRWOyBznwoXdZRMdGV/wMuNfr
+UDvqQz+CNwTw7Z0Ae9FZXvUiH70LiOAedjb/AGdZQC/riY9cZgybaXdx2QWnEGUKDaFIAUGV6tlC
+eYq1dX1BPRrgRHO3/wCY1WuubhNnhOUrC1sX3LhtF61LjiYUo9k5JMVaur4kDoxwLf8Ak5P95VAT
+A0rmB8a2w+1L9ylomATvHhSrTetMJBJPKp7B8P7BsFMdqrdRPKg8xK0Qzh6bdlYbkiSBvE71AnCc
+TxHGRcqAtrdteyie84ANtuQq6KZSmCoaz4mtVpII22oIe2wq2t2tMOOL3lajJ3qDzc37Jhb2lSkF
+YgL/AJm3GrhpUkn3VSOljMWDZawBy5xd4gOgpbQlGpSj5Cg59zRaP67w35beeZAUwp8gl1JO59QK
+p/RVgS2XL/GL5am+3LyWg2jVI4QBB47/AAqbxvNb2a1vMYQ+i11KSEQ0QVeEq4/DzqwdGjLlvlkL
+unHEPquH9bkAqRGqVAcyDvHlQXHEMETbu219aJKA+yhtxB5EDYn7KrGYLY21yEOoLayCFBSYNF+8
+w4sXFu+EqubNTRVp4qVpbBA8NzJk1TukZeMu4fdN4ZgaMTcaLIDIZDim9X0zMgwPf6UHP2e03Tt/
+bN2rS3HFpACW06lKPkBxppYdGmasQR7RiCRhtvpK4cV84QP6PL311ecm2jFs0xgybSxu1sIcDxZk
+lJ4jYgmD9tVvFsoWiQ45mDHVvW8QvfsEQDChtufpJI35Gg57Tl/C7ZDjdtcsWz7SE9s65K3EhXPh
+z4eFVvNtrg15b9ph63W32nEMtoKCS8Oa1K4DyFdE57s8k4060zqQbi2SG0sNgw547jYgcffQr6RL
+Sywu6ZFtbN2zCXO0LaR/CECdz/jlQVnLWH3RxRoWoLEaVLUtUwf5225H30XeifGlYJnO0tn0ds3f
+/MG6UgJgkyD6HhVUyW3b3l+wEpSxcusjSDJC522+HD0qy4Thq8VzrY2QcKmRqaeCO4oECAoBQmAR
+QdTWTYCAVLCieFe3WFWV4pKnWh2iFSlSTBFQfR85eHD3MOxLvXlkoNqWf4xP5KvfVwZbIIOxmgi1
+4elgSkrIPDypqtlQE1ZnW0qbg7yKjLxlTKVAJGmOJoItepKR3gDFC/rJKJ6Mr2AdJcb3P9aig4VE
+bDj4UMOsekp6ML2SILrUfpUEj1JmyjJrytiFOOnj5oH7KyvepPIyfcTzccP95IrKAb9dN9p7J1gb
+a+urtkXjPefIKpDTgIMbE93iKtHV1LKujjL5fC+z9mIMeSlVS+tZh2L2HRrZt47Zi1vTiTQKO11w
+OzcgzJnh4mip1YcNb/FTgDrqQrVbE7jh31UBOsLcdhrbZ0JiEDnUvbM9miIk86SsdK3VqaVqbTtE
+bTzp4J5UGq0kxArRxsngDTkkgbcBWizwmRQNXGjvFBHrLZYxXHUYabJhbzDetC0gx3jEb8uHOjq4
+ElMzNUHpcfScEFmq5RbofVC1qXpgDfY+M0HI/YYtl1a3bmyt7C3LnZ26zHarIMaoEmOfhRKyM8nE
+7Fm4tihLaFKWHNMBKtCiVkb89yKHXSo7a2+IlouqKWkKTDjsjURsB5wZnzq+9ByG2cCtmlKI1rB4
+mSSOBI5GfhQG51SMLwhd9d3wWtNqhaU6YTAG+mPEmn2AN2tvZm9cCW37sh1ZPiYAHwgU6Yw9rEcv
+27ToAOgBJT4D9hpZGHNuhDK4T2YiOQoEsVtnlLtvZ7QOuIUdLmqNCSNxHP0oZ490dXl5jryPlJ9F
+m6kOOslZWFKPE7kxwG1EZnGGnMSvLNKoatEEvOzEE8BNKNOYa7iSk2zrbtwWwpxSSVGOUnnQU+wy
+Rg+HIQpNqhSkc1JGxoE9OeErv82Iw9huO9Ko2CUwkn7a6rv7VTiDpMeBP21zn0j26lZuvLy3e1Ia
+CVSonhI24eCaCjWtk0xcW5xC8RbtWoCmm0nvEJMgGOG9WHo4dxRzOthdXGHutoQ+oF7V3QF8IJ3P
+ED31EvM/InbEqZccvHNRURqCWwIjf3mpfJ79xdZywtCriXC+lxSNR7yAdyYMAbbCg6WbZXa4na4m
+2SEqhm4HLSfon3GPjVub2G1QVtbC6wxbau6FoInwqTwl0uWqAsy433HPUUEnJg7CKbvoC0qB4U4k
+cfqpFwlSjHKgg7pstrIKNPhAoSdZhSm+jW4kDvXLSfrP3Uar5jtGpmKB3WkXp6O3GuZu2p+ugn+p
+aQrJTp0gaXHUkxx7yT+2sr3qWQMjL8S69z/pJrKCqdfhR/B21bIgG8ZIMcfm3Pvq79WMA9DeXdWw
+9nV8A4qqf19Qn8F7NYBKxetD3FDn3Vaeq68X+iDA0L/ikKSNuI1E/toDCNKW+7tXjbknTMKB3pNR
+BRusSRwpG1bQlZdTqle5M0D8pIRIMzWi0k7kVs26VARuOHpWy9kydtqBvyAoR9PDdy6W0NBOlLJV
+KuAMnf3UXlQTx40JOmrHLe1vfYLllLluGPnSeRPn8KAA4sxhGY0IwXEbdAcaKSxfJRpVpJAjVzTx
+MHwFGPLGXLXA+yw6zh1lgobSswZKYG9B3GUWzCEXdmXHGFPoShKR/Bq8FetHnJTbjlip1/6Yuz9L
+YjvJP7aC9ZYYPyUyoOiQkApSQUp8hG23Davc2JxC3wi7fwlkvXvZHs0AgFR8idp8JpbKaAjCGwmS
+EqKQeaoP0jtxPH31LPJ7VlSRsY29aClZTwFdnkq3axBhab19IuL8FYUpxziQo8Dvy4VL4Szh7XaN
+WduGlIjVCCBvy4fZUi2T2imjsJmDSVuy6h59bjoUCrupH5I8KBtjTybTCLl8qCSlohJP84jb6656
+zk0FX+IS0pbhW2lKUnc7EzRw6QHpwL2clZ7d1KShIOpQG8SOA2Ek+nOg3iuFrxBV+Ge0S4HHXAAd
++4Dtz5GgodlhTuLOXV9blt+3t1qTcIKtK2FA8DOxnl76neizALh3NTd4ppRJCVIIJ+jJ+HCvcq4h
+8nZdRamwLpecU4tLg1BaCd9W3Ab8TPCp/oVxC3s80Iw95d2lVwSlhLoEJgcoHODQdA2Dei1QmDJG
+9LJZ7N4vN7H8pP8AOrGNSedOAQU7+NB6l1KyIO8SKb3L4QstEwTBn1P214NFvreIJK+fgPCoyzWi
+/eXfPhaQh5SG0qVACdoMefH30Ek+4lCi2dwQTx50Detesfi6J4TeND6lUYbgqf7VsK0KSe799BDr
+VOLPR8pCjwvWgY9FUFr6k8fgSuP/AFHj/eR91ZXvUm/0GV/Xe/XTWUFc695P4NtJO6ResHj/AKty
+rb1YRo6IMvAAd5lZ9/aKqqdexKjgCIE/5Vbn+45Vs6ti9PRLl0Dj7Or/AIi6AqO6QtC1qjkBHE1o
+HCEKCdlQSJ4DwmtL3UGwtKgkpM1s22OzCn1pJMcOFBtgq1rtRJKlT3jBAJ8p4ipIokAz7qq+FYnj
+D+OutrtbVOFto2eQ4dQVJATEQdoPvqypdA3kEcaDCxJ5jehL0k4JbX+aF2D4Kn7tslsaZ2jaD4yf
+qovJeBA9aGGbcYUxmS4XcLW2GwopMDToHL12nagDeMWrOQ8Ut38Te+bfeShTDYSrWUwZUOW870UM
+MxBD2EX17bgFKrp4twdjCEkfZQn6VMHxDMuZrB/CXbd3tnA0u2dX88gkzqG26efjtRiwWy7DLhY0
+gr9ofQTp3nSdx+jQXTLLxUxdJVGpL6jtMQrcRPkR75qQQ+kKUkq3mq1ktSoeSlBQkttqEj6RKASf
+MST7wamXwWX+2EQdlDwoFApS7wmBoB2ptbG6TdvOOvsG3OzTSEFOnfz4+tN7fEW3seXZNmS0NawP
+yR51o9d3Vzj6mBZhq3aSfnCoFThkbwOAoIHON2HsXtbALWgBQVISd1KJgTwiEmR6VV8rgqexG9AB
+0WjzgJG0mpa5Wm4zViN8hQcRaoXJ0gAdmgAAEHeFFcnjO3KkujlTSbK+uX/4MMLCpH5Ox4e80Alv
+7O89rtLK0tlXK1EgISqEK27yp2HAbDyqU6MsLXhucEqWyHHGnNTbhSJIVPCCY9Kl8CtsYvbxzFuz
+bu7dLmi3t1PFGpO41pjhyEceNPMpl+zzkwzeKLr2IK+ilOlCAlJIgevPnQGFNwrsCofSArXA7x29
+Sokd1KiJ9K2QzpSNUwRxpLB22MPs3xqVHaqWeZMkmBQa4u+3fONWdqVrLdylL+gwWiBqTPiOHuNM
+ct3D14L9zswi1RcuIb1cVadp9Nqe4241h6HMQbMLQytao4KhOxPjwpnktl1rKNqHge2cRrcP9JW5
++2gmrRsItgTBJ8KA3WzRoyUFJ4KvG5+CqP6GyGUgAcKBnWzQT0epIH8ub+xVBYepbIyQ2AQR8/Pl
+84Kyk+pPq/A10HcBbo9O8jasoILr1ScBRBgh63j4O1YerOo/ilwHURs0sbf7VdV/r0j9wkqn8u3+
+1yrB1Z5X0TYFwjslgf2q6Aq3X+bSPCeNIYchNywQ5JTBETtTi4SVWy4HLamuEhSElJM78qB1CGG2
+7VpCUpnvnkkePrUQ/cXjOYmbFAUbd4FSf6McR9dSmINLUJCCptB1KTzX4D0mqhmPH3MvZgZvbxtR
+sy+1bKITMLcCt/iAKAjhsJQIiYoW9Ilp+6V44yg3DhQIQ2qTJ2I08440TWHw/bJdSkiUzvQ2xxdy
+xib7rDalvF8nYctXCaAMYfjWJp6Q8Js8SY1D2xBCwkoIIOwE8eKZo34OV+zW0pK+1vS4QFcAttdD
+XGMYwu66SLG4xKzadvWLhDbLikFKgeB9QJPwotYez2YwxI4FDK9/zx+2gTynCb20WVhRfsQAmBKA
+hRmY33Kvqqw4mlCLRxxwEpSJgbk1XsHhl7D5b0Bu5fZU6Y4EylBnfcmdvDzq4ONpXG21AN8i4ZmH
+D845gfxLsBZ3LiV2znaa3FbniD9EAbRw22qw2bTFniV865dXLj4BUpK1Hs+E90UviDqm8UJgCFAH
+zBH3iorPVwm1wdy5ceLZdSGUKClJAUo7GR/jhQVxntG8v4ze3LTSHnWtB0DYFaiYnme9x58aQwNt
+bWT8XdSrQPZwkq8CZn6op1i/zOT20JVPbuBSTxkAEj7BS+VOweyhiDlwvTbuFxKlEcEgQf20AJuF
+Y1eW9vb2jtxpYK3FqZdG0ERPDxiiV0WYWu8xNGP3ilpdb7gSsGCqIkVBXgwDCMFQ40t5Srx5QZd1
+hSVRAkDkB4bVa+iS+YcuHMMR2kBHtACySdzvx5cKAj3t8pi1WUtqWoJkAVC/L9naWKbh1Sbi8WUM
+9i2QopWQYkDcDjvUvi94zZYY/dOphttsqJAnYChDlN22ds14xiS/ZXrm+cuWme00rUClPZpUBvJA
+n86gvrz1zieBui4T2NzeOIY7IHZvUdwPQA1dLVkW7DbKYAQkCqlhlstzMOCWOgDskru3gTMHTpAJ
+9VH4VeFp+cMAbeNBouNEBRG24oJdbFH/AIcpM8b5refJVG90d0bbmgp1sEk9Hze8RfN7fmqoJTqU
+6fwKfE97tndp80VlJdSrbLFyJnvu/aisoIrrzpP4OoVy7S3+1ypjqvuT0SYIkk7B0f71dRnXlSDl
+hKo4Lt/f3nKfdV0a+inByPyQ6D69qugM6BLShHEUyw7a4dQDMHantuklBHKmjbfY3ijBAO9BviT7
+bPdeMJcQQY/Z9dQV6S4n2e+CXgp5tbIUmSUpgyfMGneeGFuYCt5DikKZUlyQJ2Bk+u01st1tWYbS
+zCNZuGlKkD+DSIG/hMigstuEhlO0gjaqRmppJvXvY3GW7pSiYdMJkDjPjV8Q2EtgDkIqi52tmlG4
+L3dE90zxJHDyoAnjDdwzm21uMX7uKXVwA2yIKUJTxWFDZRO3186NzjqWbXCVE8EIT8Ck/fQlunm3
+834azcWVtqt3VJQtLkqEjf7d/Si9i7KU4HaPJ4MupmPAgj7qBtdtqRbX51hPs16h8o2GuTATJ9QR
+5gVamHSS2VRCk+FRl9bj2m7HY9qLi2JCAY1KA2G+wM86c2F12+CW7y161pQNStpkcZjnQNcetwhZ
+dA7y1JHHjVTz5cLdcw2wYQHpd1OTuECISvbeZkDx34xVhz1iAscKNyYKUd4jVHI0PsOujfZgaxC5
+uVIbZSFFtPNOkOBRj1IA8vSgks4OMpNtZtAFthWohJHArA4egVTjCbFf4txag6XHWCtfA7rOoj6z
+UFfF66XcXUavablQSTP0Q2qOJ/pD3ir4y2G8Hbti1rBbCVDgIAoOfsaxFOG481hC3LZ21QrtWm1t
+hayrgrSeRIMRwgUR+ibCWkdpj3cC32g2hCTPZpHI+e31VRs2ZYtHLxePKJUwpxTSWUcdYPDUOCSI
+38iKu/RG4ub+xDehhhSVISeI1DfhtxFARnG27izW28gKQRBBHEUEsFwkXnTJcYeo6rSwWl9CI4bC
+J8d/so3gAtQKpOG4a3adIuL4i0nvOWaJ24qkxQWzKzYdxbEsVIOnULdonwTx+s/VU8rvKK5nwApD
+CrX2TDmrfYECVkCJJ3P106aSJ8qBEqIAEDhQZ61xB6PmduF83+qujY62nwoJ9a8j8XzQif8ALkfq
+roH3UsP72LoT/GO7e9FZWvUtIGWn0wZK3if0kVlAy68IJyxO8Tb/AK6/vpfqquBPRfhQJjvuiDz+
+cVSXXg3yuE+HYH++uvOqopA6M7DWB3XHY/TNAemBKCBSdy2lDqSY350lZ3KdxIn7a0xJ0ltCkmCn
+eg0x1CXMJfbIiUECee1UToGzLe461jFtibRVcWF4WUPkfwiOQnxEVaMRxGbRYWZEGo7oktLS0yym
+5YA1XTzjyz4kqNBeytU+NVvMFlbYip5t5TqVEBIUhUaTyPhU4t4p4c6qGbHL9DinLI/OIlQSTHLj
+QDfOeCsYNmBDlpZEvKR2i7kypSjqg78Ad55UUFLL2WlIUkmWwRtPDehPjeM3t04W7nuBtxsFA2JK
+l/ZANF7C1JVh7bSu8kpjc0Gl1iLbVrh12tQRKkoJJ2hXdH1kVtgbgFvdWyne1LbihqkHUPHbn4+d
+QePYbcYng7Fqw5p7J4FSvNCpA+IFLNXTeFIxS+dSUKTal5QJ2UUpMke/9njQD7NmNX+dM7KyvhyU
+2+HWw7R+4c37VKFaVCBwEggTE0/dbfQWbewabsy6pWs9nJDadjII2Jnlt4VZzY2eH4cvFbawtkYl
+fNN+0PBAG8TJ99Nra0Q7jTSyhLa3GktrK3ZEDcpTzJkn4cqDW8swi5wu23AbaU85pHFS1AD6pq2L
+HzACRAAqKdQh7FiscilA9E//AO1KuL1IMcAKATZrFthgdwnEb1NjhhJdU+133DqV3U6dJj135VNZ
+CxrK7PZ4Xg945erMBb3YmSTJ75gR68KpvSFdtXV3ibz60e0W1ou5LSRsEAqS2k+oMnwIqMyLauYM
+3heI3lk9bKfulWt20pWkgLIgn3EEUHQEd07kCq1lwPO5qxRTgGhCkBJ8QBP2mpe2eXZdnaP6ltaQ
+lp8qmT/NV4Hz5+tQVveLtMcu9AkuOoB9KAgNqUobEA1oHFa9JFJ27mod0+tK7SFzvzoN1lWmZ4UE
++taf3gsTzvkAforo0uujSQN6B/WqcKskWyRzvk/qroJfqYT+DTuw2U9+sisrfqZD97D3kp2fXUis
+oGHXc3y3Eb6WD/vF1TOr7mvCMJyFbWt3i1nbPJdc1IceCSJVI2NXTrrtzgIUebbH1OL++uNnEkHa
+RvQd42PSFlkAFWP4bI//AJKPvp09n/La298cw0+l0j764C7yTxPrXhKualUHcGN51wJVo6pvGcOJ
+0HhcI++k+jDOGCYZlJi1uscw8LC1qANygEBSiY4+dcSHUocTvXg17gK4UH0Jts/ZXUolzMOGJA8b
+pG/11Vcx59y6rGJbx2xcbKIMXSAkHj41xBqXEaj8a1UTO5NB1FjucsuqddGH3tj3HWe0V24AUe/M
+c1RIPGN6I+HdImVm7VtCswYYCEj+Up++uFIJ8ayFfzj4bUHdFp0j5TQ++hWYcNSgr1g+0p58ai84
+dIeUVWi1s47h90lbKmXbcXAhaVbevjw8fSuLAFR9I153juSZoO2nek3J5Nuk5isAlpIIT2oIGxHv
+2Net9KOUWFBacdwwlcqWQ6JnhwiuJAFExJisg8N6Dta16UMn9sFuZhsATqO7nifup4elTJn5OZLD
++1rhzSrzrIJTtsaDoPpMz1gb+NG7sL21uBd4Y7au6FA7laon0mansdz9lrEsh2S14tZ+2pQytbYW
+NWtIAPv2rl7SRsTNbBJ8aDtW36UMn3Fg2HcespU2nUC5wMVBPdIWWk4s1+7to4nWklwrHAHnXJAC
+toJrfccKDuWz6UsohO+YLEf/AGU5b6UMnkE/hDYE/wC0rhi1WG7hC3UFxCFAlMxqHhNSPttq46Sq
+1S2kuqXAPAEyEjyHCg7Vd6T8omNOYLDf/WULOsDmzBMeyqzbYdidtdOi7SvQ2qSBpUJ+uufFLK3V
+rQAlKjIA5DwpdqeZJoOu+pjtlh8Rtqd/WTWUr1N2+zyo54rLiuP9NI/ZWUDLrop1YAgaTu21B5fw
+iv8AtXHrzMmNJ25iuzuuIytzLgUB3UtN/Hta5DW1J+jQRBZAMwTWBid441KlqTFZ2IkkpE0EZ7OJ
+3FeKY7xhMVK9l4CvOy24cPKgiCx5D41qWoMQRUuWSREDjNeFkapIoEEi0Fro1Q4EJAIRzBnf69/S
+lXrlpSrwtdnp1ksgtjgVSeXhtXpYT/NE16GeGw28qDxm4bLlmXez0hep8dn4K25eG1aBVsm00KIL
+obUmQjiZOx90QeVO2WmFFKFMjUTx1QPftTj2FggkBgDURBe3+ygi7z2Zxp/sVJSlTiS03o3QnfaY
+9PXjSeG+zNsuJfSky82RIJOkTq5c5G3OpP2JorCAlkd0GS5sd/t+6tnbW3aX2iW0KSFboS7Mj4UE
+Y8m2DKezUndrTu3BCtczw8K2U1a+03L6XG9Dpc7NGg93+by293hSzrLanCW29KTwSTMV4liOVBHP
+WiUL0ocS5wkpBiffSaWNztvUr2G86a9DMDYR40EX2BE6RNbJYPGNvGpMMA8q9LJ8B8KBghnfhSoa
+QdwmDTzsiTukVuGjEQAaBu22EwIpw0gTsK3S0rbu07tmVTPCg606nrShk5bihA1OJHn3xWVKdUtC
+R0bJUEgEPLB24nWr9kfCsoNOtVaOXeUEttNqWsplISkknStBP1Sa5NXhS9KdVncSAdXcO/hX0PxG
+wssRt/Z7+0ZuWpnS6gKE+O9RRyblgmfka2HpI/bQcDDDGAynVZ3Xac1Rtz/7Ui9hqVbtMOpE7yJr
+6ADKGWxwwlj4q++tGcmZYZWtbWEMoKzKgFqgnxiYoPn/APJrv/oufCvBhywf4Je3ka+hP4L4BEfJ
+jUep++vBlfAAZGGNT6q++g+e3yavj2S/gaw4a4Y+Zc/RNfQo5XwAmThjU+p++vPwWy/M/JjXxV99
+B89Pkp4meycj+qa2GEvT/Aun8019ChlfAASRhrUnzV99bHLeBnjhrJ+P30Hz2ThL+/zDv6Ne/JFw
+Zlh39E19Ck5dwVPDDmR8aw5dwX/49r6/voPnl8kvD+Idn+qa1+S3Z/gnI80mvoactYESScOaJPmf
+vrwZZwEGRhjIPv8AvoPnmMLdH8W5+ia9+TF7y0ufSvoYct4EeOGMH1BNe/g7gn/xzP1/fQfPA4cs
+H6Cx5RXhw9QH0VV9DXcsYAv6WGMn4/fSasqZdJk4Wz8VffQfPgWPPQfSsNlvOk719Al5Qy2RvhLP
+xV99aLydlk8cIY+KvvoOChZ4fKtSbgbbcONYLO030h7yMV3j+BuWNR/ce34+f316jKGWwSBhLIB2
+O6vvoOE7e0swiXRcSBtoAiYP7Yq69FHRxdZ9xt2zs1qtLW3QHLi5WmQkEwEj+keXoTXXyMpZcSNK
+cJYA4wCfvp5Z4DhNqoqtrQNEiDoWoftoGeRMrYdlDBk4PhfaezoMhThBUSSSSSAPGsqfSAlISOAr
+KD//2Q==
+
+
+
+--Multipart_Sun_Oct_17_10:37:40_2010-1
+Content-Type: image/jpeg
+Content-Disposition: inline; filename="custer.jpg"
+Content-Transfer-Encoding: base64
+
+/9j/4AAQSkZJRgABAQAAAQABAAD/4Q1kRXhpZgAASUkqAAgAAAAIABIBCQABAAAAAQAAABoBCQAB
+AAAAyAAAABsBBQABAAAAbgAAACgBAwABAAAAAgAAADEBAgAOAAAAdgAAADIBAgAUAAAAhAAAABMC
+CQABAAAAAQAAAGmHBAABAAAAmAAAAOYAAADIAAAAAQAAAGd0aHVtYiAyLjExLjMAMjAwNTowMTox
+MCAwMDo1NzowMwAGAACQBwAEAAAAMDIyMQGRBwAEAAAAAQIDAACgBwAEAAAAMDEwMAGgAwABAAAA
+//8AAAKgCQABAAAAyAAAAAOgCQABAAAA9gAAAAAAAAAGAAMBAwABAAAABgAAABoBCQABAAAASAAA
+ABsBCQABAAAASAAAACgBCQABAAAAAgAAAAECBAABAAAANAEAAAICBAABAAAAJwwAAAAAAAD/2P/g
+ABBKRklGAAEBAAABAAEAAP/bAEMACAYGBwYFCAcHBwkJCAoMFA0MCwsMGRITDxQdGh8eHRocHCAk
+LicgIiwjHBwoNyksMDE0NDQfJzk9ODI8LjM0Mv/bAEMBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIy
+MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAIAAaAMBIgACEQED
+EQH/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0B
+AgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpD
+REVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmq
+srO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEB
+AQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFR
+B2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVW
+V1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrC
+w8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/ANNRzUiIfSlR
+DmrUUfrXOajEiz2qZYPap0jqlrOr2+i2RkkZDMR+7jLYJPqfanYCd4OOlQGe2ibbLcQo/ozgGvPL
+vxRqE6kSXsj+yfu1/JeT+dYsl55m7JUZ6kD+vWiwrnsAlgfG2eI/RxUhjrxlLhgMLKcdua0YPE2q
+2mDHfSMB2c7h+tFgueqeVimmPiuO034hKWEepW4APHmw9vqK7G3ube9tlntZUliboymiwXECCgpU
+oSnGPNIZEqgUVKIz3opAVUXDVajFV4/vVT8R6yNC0SW6THnsfLhB/vHv+AyaaApeKPF0WjK1nZ7Z
+L0jk9RF9ff2rzC71Ge7naaeZpZWOSzHNVZriSaRndyzsdzMTkk1GFLHABP0qhEhnc5xWjpmg32q5
+8oY9M96hs7DzHG4MBjJJGK9j8HW1ta6ckqqHVunHQ0BY8wl8H6hDGWOcjttNYtxbzWxIcHivoXWN
+X0nS7YzXh5I+WMDJNeY+KTb3umHUotOlhgkfYjkfqaLhY4ESZrb8Oa/caLqCMrFrZyBLH2I9fqKx
+Y4mkcIilmJwABkk16R4V8BmDy9Q1dfnGGjtj292/woCx2qjIB9aftp+MUvFSMYFopxopDKCDDdK5
+L4i2V9dWdlJb28kkEJcylBnaTjBP68116ctWjanFUhHzpit7SLa0t1W7u5lCk4AxmvZbzwvoGoOZ
+rnS7dpDyXUbCfrjFeZ3WkW0OuXEcMMbQK52RljtA9jTESWNvDqExntAJDu2YEf3R6/413Xhq7itb
+IW88MgKMct2zmvN47h9C1WCS1uzGGbbIqnPynsexFelLMloUMjF4pVBEh75oAvX50q7+RIXknkwG
+Pt6c9q0bu00e/wBEk0m/kht4pE2KrOAwPYjPcVmWlza2TSXTbWwMgep9K5zWdavNSmISzS4bOMjA
+x6AUhm3pHhfRNFVZLG3EkmOLiQ72P0PQfhWnITUVjJNLYQPOmyUoN6+hxUjZzQAwr7UgWpKCMUgG
+7RRRzmikxlGNavwDiqiDB6VegGQOKpCJygkhaM9GBBrzHxFp50+5la8tnO4DbIg6LnqP0r0S/v5L
+K1MtvaS3bZKbYcHDDqD7+3WvONT1u+1W/jbUSVtY3yYEGAB396YjCtLQXt1mOBhAjZDPyT7V3GkX
+bJbtp92pe1I+V8ZMf19qzta8T6dounx6dplhbySkbi/U4PIJP9K4y88Q6reYMk2yPOQiKFU/h3oA
+73VLC6trFrmCVJ7dTn5X6fhWLo3ih31i2N3Bi1iYqWjHTPc+uKg0zxCb+2NrNaHj75hOFI9wKs6j
+FF9hf7CotmjAZWTj659aAPUQVdA6EMpGQR0IpjCuJ8GeKVNtJYXshd4xuiKLyw7jH9K7SK4guGdI
+nBePG9Dwy5GRkHkUDFA5HWnMBikIwaKkBo64FFAPNFIZVQVi+IvEk+mSJa2OVnxueQKDt9AM1uov
+NedeIJkufENyySEAEIMHjgY/mKpCZaOri/hjg1G3jnhRtylB5LofVWXHP1zWpqF1pt1pg8+4jmlS
+PbA6o32lj/02ydpGOMjr+lcsXliHOJF7jHNQSyAYmiPyA5I9DTEQnTVimMwywHOD6VoafJHp13Hc
+pZW92gJIhnGUORiljcSJnqDUMTBQ0fdf5UAaFhANDj1STUbP7PJN+9EQHHlnJUKPTJNSW4G4jqCB
+1qKKzu0EeoXV0k0N0CqiSXc4C5HI9OaSNsXCIvCg4oAzNbj1O01iDVXhS3km+dPKUKpHTOB61b0j
+xNLYayl3JGBE6COWNOAQOhFMv7QGCa7N5E489kFuZCXTHOcdhWMGBJJ6CgD3BJEliSVTlXAZT7Gn
+AA1xvgnXJrxX0+5fd5SAxEjnb0wf0rshjFSMTABopwxRSGVgDn615TNCTNMrctHIwP516xzuGK8z
+1FBa+I7yJgNrynH48/1qkJleOZo0GMstMlDy5MKxsx6g8GpGUxSnb9084p5RZUyuFb06UxGfazPE
+WicFWB4B9Kk3fv8Ad68VDOri4BaNt443D+tEchKsfegDQsVQy3DjJcR/d7dR+Va9lDGLR/NkLOzE
+gAenTn6iudsJtmoMeoaMg+3IrbtpJVjXa+O/NAGK4Q3M5bG7ewz+NZqAZcdlY5q3bxXOrasba0QN
+LNIxUFgo7nqa1PCPhttZ1a6S7Vha2rnz9p5Yj+EGgDLsZ7i0nS6t5WikU5BHp6V61o2pxatp8c6M
+vmYxIgP3W9KI/Bukyu0stmgZkwkSsVWMepx1NcOss/h3Wp47K4WRUfa+4cSD6UmM9IGM0VQ0zVrf
+VLbzImxIB88Z6r/9aipAn3ZNcB41tGj1gzKMGVFcH3HH9K7pDzXJeNCrTRuzHem1AueMNuOfzX9K
+pAznLa7juUCSEJMvr3qZwydRx6isi4ty3zJw1TWksjrs3kOOoJpiL0twwXlCwHoKxri6WFj8pG/k
+A1fe6vIODtKmu1+Gc9vcazNHeRxyZh+RXjVuc8nJ5oA4zwxp91rmpSW1nGGnZMZbgAZ5JP5V6Cvw
+213Jb7TYgbcKPMbr/wB816pGsESARpGi+iqAKT7VCPlMi5+tAHisPwc8SLJk39hH/tpI+R/47XWe
+FvBeqeGtHnhlENxO8hlYRSH5vQDIHP1xXoSzow4ZT9DSvIO1AHiepeMNUWaWCBPsgDFWDrl8jsc9
+PpXMajqdxe3BuLt90zAAsABkDp0rq/ixpTWeuwanbDC3kZDr/trwT+IIrziS6uUcJcqGWgDpPDOo
+pb+ILctJxITGQffp+uKKyNFtjc67YxqdyNMrfgDk/wAqKljR64vB5rK8Q6DaapCtxI0qXC7I0Kth
+eXAGR3+8fzrTDYNQavKRol465DpEZFI6gryP1FJMbPMEcqxRhytNkwrLInDDriqST/ay3z4lB3A+
+tSrIWQq3DjqDVEl9h9oiDIC2e1V4H1KxvEuIGe2KHPmZxtqkbuazYtC5FVJ767vG/eyEr6dKAPQr
+P4k6tZRHcy3KKeGlXJI98VuQ+OdM1EB3nubN3IASWM4Y+gIzXksN4YcB49yjsank1L7Q4ZvlVfuj
+3oC57To2vQX8jJBdJujO0hnCnP0ODXWxC6dBuLL6ZFeP2/izwp4hsIodejmtNREflvdRICjnszY5
+OepGOtcvc6vqeh3Ij0jXbnyifl8mZth+maLDPU/i9Cf+EOt53YB4rtcHvypBx+leILdgDbICwrqL
+7TvHPiVo7fUTdTxp8y+dIAg9+uM/rXS6D8PbDTQs+pst5cjkJ/yzU/T+L8fyp3AyfAmiXHn/ANrz
+I0duqkQhurk8Z+mM0V6BI+FCqAFHAA6CiobA/9kA/+EMRWh0dHA6Ly9ucy5hZG9iZS5jb20veGFw
+LzEuMC8APD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQi
+Pz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUg
+NC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5
+LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgog
+ICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6dGlm
+Zj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczpleGlmPSJodHRwOi8v
+bnMuYWRvYmUuY29tL2V4aWYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUu
+Y29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50
+cy8xLjEvIgogICB4bXA6Q3JlYXRlRGF0ZT0iMjAwNS0wMS0xMFQwMDowNzoyNyswMTowMCIKICAg
+eG1wOk1vZGlmeURhdGU9IjIwMDUtMDEtMTBUMDA6NTc6MDMrMDE6MDAiCiAgIHhtcDpNZXRhZGF0
+YURhdGU9IjIwMDUtMDEtMTBUMDA6NTc6MDMrMDE6MDAiCiAgIHhtcDpDcmVhdG9yVG9vbD0iQWRv
+YmUgUGhvdG9zaG9wIENTIFdpbmRvd3MiCiAgIHRpZmY6T3JpZW50YXRpb249IjEiCiAgIHRpZmY6
+WFJlc29sdXRpb249IjIwMC8xIgogICB0aWZmOllSZXNvbHV0aW9uPSIyMDAvMSIKICAgdGlmZjpS
+ZXNvbHV0aW9uVW5pdD0iMiIKICAgZXhpZjpDb2xvclNwYWNlPSI0Mjk0OTY3Mjk1IgogICBleGlm
+OlBpeGVsWERpbWVuc2lvbj0iNzU1IgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iOTMwIgogICB4
+bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6Zjg2ZTcwZTQtNjI5OC0xMWQ5
+LTllM2YtZDQyZjM0NjM5ZGJiIgogICB4bXBNTTpJbnN0YW5jZUlEPSJ1dWlkOmY4NmU3MGU1LTYy
+OTgtMTFkOS05ZTNmLWQ0MmYzNDYzOWRiYiIKICAgZGM6Zm9ybWF0PSJpbWFnZS9qcGVnIi8+CiA8
+L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+IAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAog
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz7/2wBDAAUDBAQEAwUEBAQFBQUG
+BwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUF
+BQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4e
+Hh4eHh7/wAARCAD2AMgDASIAAhEBAxEB/8QAHQAAAAcBAQEAAAAAAAAAAAAAAAIDBAUGBwEICf/E
+AEIQAAIBAwIEBAQDBgUBCAMBAAECAwAEEQUhBhIxQQcTUWEicYGRFDKhI0JSscHRCBVicoLwFiQl
+M5KissJDU9Lh/8QAGQEBAQEBAQEAAAAAAAAAAAAAAAECAwQF/8QAGhEBAQEBAQEBAAAAAAAAAAAA
+AAERAjEhEv/aAAwDAQACEQMRAD8Am8Hl60dEGN+tAbnalEBIr5+PUIoGcmllXPSgkW9LxxntVBET
+2pVY/Y0tFEe+KcpFRDYRDHSjrD/pp4sdHWIUwMHhwOlJcpXr0qVeEEd6aXCxxRtLK6xou7MxwB8z
+TDSSpjelAtQFxxvwjaSmGXW7dnH/AOtWkH3UEUeLjjhGXATV0z7wyD/61cNTTDbG1AKcdKjBxXww
+RznWbUA9Mkj+lSem3+n6lE0mnXtvdopwzQyBgp9DjpTDXGQ0mY6fFD6Vzy8DpTDTMR7UOT3p0U9q
+KF3oaasm1F5PSnTr3pMAb1FJrGwGRRuX1o64xvRtqYG5X2oyp7UpgV1agCKAQSKFKZHLQozUEoNL
+Rj3oBd+lLxJsKrTsaEmnccW+d65Ehz0pzGp9KuI5HHv0pdU7Yrsa+1LqmSBitSAsabdKrXiLxfb8
+IabG6xR3V/O4EVsXweXuxx2/maX4w410PhmJknlWe7A2gRhkf7j2/nWCaxxK9/qs+ohea6mbJuJd
+2A7BR0UDoKuJaud54gcYX0Jk/wC46Lbno5T4sf8ALJ/Sqlq+sx3j/wDiOoahq7A5HmSFIwfYf2qA
+uLxpn55ZGkf1Y5ptLPynAxVxEsdQXcwWNrCvuvM33phM8jS+Z5rZBz1pn+JxuWxRGvxzbDI9aYbE
+rHfXgGFuX/T+1OrPXdZs3LWt/NCx6lTjP2xUGl3EO9HN3Gds/ah8XnTvEjiiywZLsXKj92UBs/ff
+9at+heL9jNiPV7FoG7vCdvsf71izzAocGkWkyD1phuPVuh63o+uRc+mX8NwQMlAcOPmp3p8UA7V5
+Gs725sp0mtLiSGRDlWRiCD7EVq3Afi5KrR2HFA8yPoLxB8S/7x+8Pcb/ADqYNfkA3pLlpWKWC6t4
+7m1lSaGReZJEbKsD3BoKtYWEwvSjhQe1GA9aOoFA2K4JoAb9Kdcq5zQEagnYVKpALkHrQpyAB0Az
+6UKioTlIpxCMikyMiloU2qwLRfMU6iGe4puin0p3Ah6kYHf2rUQsgVFLuyqoGSScACsj8TfFURPJ
+pXDEo2+GW8Hf2T29/t61D+MHiM+pTy6BoMpFhGeWedT/AOeR2B/h/n8qyiSQjuc961ImndzdzTyN
+NPK0sjHJLHNNZLk5601klY96SZqrOnRuipJApKWdn70iuTTqztWnkUAEg+gqp9JRrJIQEVmPsM0s
+LG9dsC2lyenwmtO4D4PZ3jmKeYp64GQPnWoDw8WeBZIrZUbqMj+dTVx5l/yvUACfw0gx1yKQaOaM
+kNGy/MV6UuuALhIyGRDj2qj8W8E3do3mCDIJ3wOgppjICzD2owmPepnWdKaAtlSCO2KgGBBwe1VM
+OBJnYUA2BTdTR1zQaD4UceXHDd+thfyNJpE74dTv5JP76+3qO/zr0PHiQB42DKwyrA5BHrXjtc16
+l8Lo75OBNIF+GE34cYDdeTJ5P/bisdRqLGq980cLjpXQpB60dQc1lonymuBd8UsFocveoogXahSm
+2+1CsqglI6E05twu1NU69KdW/XFaiHcYXOKznx44xOi6QvD2ny8t7fITOyneOE7Y+bbj5A+orRlK
+Rq0kjBUQFmJ6ADqa8l8ba5LxDxTf6vITi4lJjH8KDZR9ABW+WUY03KMCkC5cnJorHJoA4rQ6elBI
+2kOFGTjNGUcx32qZsdAubiESoC+eyjcUQwgspVTmZod+ilxk1a+EtC8/UIeZ+WOVSSmNw3/WKjG0
+60tpbeCZJTLIQVKHIO/Tb5Vb7Vbhbxr+1jES82Vjkckuo/d6bHHr7VBtXhrp8ttaxhreGKFfzcw/
+X1rTbdomQBQCpGxA61ROFW/EaBDcsQrcgyo3GcdKnLNTHmWV0VTjdTjb5f8A+UaieFjHK/mHBQZJ
+J6Yqr8XXPD8UDie9tkC5BzKPsKrfGnFGu6jM2k8OlxynlaY/EgPYbd/sKyjTOFeK+JeJmtdUSRgk
+gDM/wDrjI9fuaiadcY22k3EL3MbRgcpCuB136VjOoIou2ZNlJyK9NeKfhUR4fwS6aZDLYEtcIDu4
+I3b6YrzJdczTNzdtvtViEQoNKwxs7qiKWZjgADJJqS4b4e1fiC/Wy0ewmu5m68i/Co9WPQD3NejP
+C7wtseEuTU9UaK+1gj4SBmO3/wBuerf6vtVXFO8K/CR08nXOKoimCHgsGG59DJ//AD9/Sti5eu1O
+5zzHem/b3rPqyCgHNGUUXau5qK6a4a4DXcZqApOxwDQoxWhWK0gsdDinEGAelJMgxS0CgVqMmHHM
+7wcDa3LHs4sJsEdvgIryU29ez0tobmNre4iSWGVSkiMMhlIwQazXjHwGtLsyXfCt8LWQ7i0uCSny
+V+o+ufnXSMvPGN66BVl4m4H4p4clZdW0W7hQHaUJzxn5OuR+tV4Ieb8pG/pVE5w/odxcSiaWNhGm
+CB6g9/lWn6XprWmnk5jifPQ9MHPX5AVQtK1trTTrlDkyOnlg+m2/9BTF77W7iN3Ms5UAFiW6elRF
+6gsNQZ31aOS3gnjCrIzDnJU9cdh9KXKZto52hYWkcgAxj2OGx03qF4D1qOyv4oNSvVCznq+8cfpz
+fP8AStj/AOxtla6VJDaXLXMDp580gwU5zvzAg7/agsXBN7pqWqWYYK4QOQTsexq4wjTrpArMvKw+
+1ZXw1oyxW8byOfPyc4bOKmJWvLRSPMbH5gcYxmoq5axacP2ulSAzx28e55om5Wyfl3qJ4Jt9Gtbt
+ruCGeaRsBGlkJyM5O9VaOG8v7oJzFhnY9Rg1oGh6BIlp5WQVC9em+KKstmq3isvLEI5MgqNwfXtW
+O6j4EcH2/Fd7qF811PBNKZorQNyRIDuVyNzv7jtU5xBxbxhoDfh7Hh6wEEPWV7gF3/2ov9a5oPE2
+rcUomoXUFvBaRgoAHLSM/cEY+ED077VUSem2NhpNmLLS7K3srdOkcKBR+nU+9CQk96OxBpN8YqVT
+dzSecjpSjgE7UFUBTtvQJMoAz61xRntSpAoBRisqIF36Cjcu3SukUBt1oOcu9CjKAaFYq6r2AcZI
+pxEvTem4SnMKYrUZPrUbipa2bG9RNqN6koelbglElBXBGR6Go/VLezNlPKmnW0kgQkc0S7n7UvET
+iofjLVrjS9PDWyKZHzueigdT+o+9VHmziHSiuu3d6UjJmdZ1B/KBJvnHrnI9iDUqAlxZLplxp6lF
+AdnXClhk7j164ovF/Pa6v5v4ZppJCWl5JCNic8uOg3Pak5+JntoRaS2DRyQrkCQbgEk9vcn70ZV/
+XtFktLCS4ihcRscDKbjpt/Or34IW8eraeLS4vrkJbyeYsAlbkLDcZXoaoGtahf3/ADT80nk8u4J+
+EbdKmPCrU7jh/VI71gTaSEeZj933+VFbglytnfGKdVUMxOMGpaSS3uYEBQZGQDmm2o2FvxBpy3tn
+MAWXmSRNwfqKqY1HUdKumt7wFcbFlG30qYrQOHohBdBlwA24wOm+f+vrSfGfF8nOdN0+8MKQjMrq
+uSx9B/aqmeJkS2llRsyKhKgnHNjNQcU97qzNGzRKZy3MScg/P5bUDbiHjp7GZFt9OlupWBBeQkkD
+tnHqMnFO/Crij8fxBLbRwGOKaIyEZ3LZzzH7kU2nsI9Jljt5ktnuZRhZMZ39fUbZH2rQ+H+GtN0x
+zeW9nDFdSKFkdR1wP0+lBNk5FEkyRSpGBik2HzqBHFGA964QAcZoZHTNWtDADHWuMpxkGuLgnFK4
+GOlZoR5cbmisTncUs4xtSRHM1AF+VCjqu2TQrNVAxg9KdQj1pJBvTmMHpWoyc2y5bYU+jUimtuu1
+PIx9a0HMewFR/E1g2oaXJChxJjCt6Zp+uwBpxFvjeqjzxfF9O1r8XeIwYebHKh/ccMcbf7Sp981A
+Xer8P3Mt3eXrPJMIisUUSgZboC3etw8W4OFE0sXWs31tYXiDML8vNJJ/p5Ruw/lXn6/01dXuGvtO
+tMO8uJQgPIpYZH3wftREUbt7pvwFkk0dtLgSK+Pi9/b9auOkaekdqEIxt27Va+A/CfWbuwTU4tPE
+kL5BmdwoGOu27Y+QNPf8hcSyxxhAiNy87HkUn5tj+9Ax4M1274duxDcRmfT2OSq9Y/cf2q7cQaVb
+a1apcWEsTLIAUYbA59ao2qXXDehoX1PU43cDaKE4P1JGfsPrTXhjiXUtUMknDn+W21ssn/kTO3O5
+9cbkfeinXEXDuraYhafSJJ0XOJIGz+lUK/4ku9PZ1tw6GPJIdcEe1bXJxWLfTHg1WExl15SVBIBP
+p9axbibSdT4g1aX/ACyyldJGCmRl5VC+pJoENB41upOIrK/1W1W8tYHBeHJGR6/Mdd69P6Ve2Wp6
+bBfafKJLaZOZGH9fevMs3DtnoFsJNTW6ZCcGeJQVQ+46/fFX3wr4ls9Im/Cxail1pc7ZYdHgb+Ir
+6euD70I2Vl+HNJkUvsyBlIKkZBBpJhRSLYwaKuD1FKkY61zAA6Vm0EQKW2pZeXuKSU4pQnbaoorh
+TSZ5RR96IwqDqnOcChRosAdaFZqocYB6UrG2/Sk1BzS8Y3rcQ7tj7U9i3ppboaF1r3D+l+bFf6xY
+296q/s4ZSTgnozKu+PatodalfWmm2Zu76dIYV/ebv7AdSfYVmPFnirO4ktOHoDD+7+JlALf8V7fX
+7VfOEtW1K0sNSng4j0Xi7VrlwbeGS9NmkSY/IkfKwXt8+5rJ9S401nT9TOleLPAyzo7HlvIIBDOo
+/iR1+GQD2OKuMqNftPqd1JPqM81xNJ+aSVizH61evCS+s40l0q5jhWYDy/2oGCMgxsc9gwAPtUzx
+F4ZTDQouJeGmm1TRp4xMvNHyzRoRnJHRh7j7Vmmr+ZYTW+q2p/aQNhwD+de6mojWPETxMvdE0KG1
+1LmFxFzJDaKRGWYE/E6rsFHQD296wHiDjbiTXJna71KZUb/8UR5Fx6bdfrTXiC9u9b1iW/uA3NId
+lySFHTApyugO9mLiHJI3xVVXzzN8TMSfc0406/u9OnE9pO8TjuO9WCLR47q3DACGdR3GVb2P96tX
+iN4K8Q8G8LR8QXV5YzxYX8RBEx5oS3pnZgDtQJaD4j3z2fk6laC5jGzSR/mX3q26JxClzah47kTI
+35SVwe+29UfwY0NNQur2/uA4ihVY0IPVzv8AI7Dv61aNStX03U5YT5fKeZ4+VcdOQ7j1xn7VApqL
+/wCZWNzbSr+zmjZcHsazC202WwnVortlukJ/J0BHbPetK06QvEzEHIkI+hFI3fh1+O4e1Pia31uO
+G5t5ARZsm7DAyQc5/Sg0Dwd4ztdU4Y/C6jMyX1rJycnLn4NsfYnGPf2q6aVq2m6tC02n3ccyI5jf
+BwUYdQwO4Psa8kcLa3LpGssWeTyXfEgU4Jwa2vWLxdQ4e1oWGnabc6Zcwfio721YJcQuF5v2i7MQ
+G2BGwzvRZWsOuKTIIqgeC/FjaxpB0q+maS+tF+FnOWkj9fmOn2rQGO9ZsUl0augkVwjfOKAB9qiu
+52pNmzttRiDRGU5xQGQ9xQrqKcbihWaIxF70snWklB74paJcmtQRXHHEq8NaA90ih7uU8lup9cbs
+fYf2rDLvWJ9QvGur6WWWd2y7Mc5q9eOV60Op6XbqhYLA8jHtu2P6VQYruAkeZFsepxW2KfQTI35G
+Gau/DvG99aWo0zW401vR2xz211livujHdSO3p7VSLdrGX9/lPTI9KXaCXk/ZOjemaDW9M02NpbTi
+LT/FjVrPhaxbzf8AL7mTnmgYDaH4iQVxtuDt69aoviLrXD+u67LcaLpE1pBJnzWcgLMf4gmAVPr2
+/nVMnlBcRXCBJ13QkbA06065FxCWcYdG5WT0IoGI0eISMgUe2RR9OJt5Ws3xjtt1qUB518zG6HtU
+frScskVygIwd8UHZ7copUCjaxe8bcR6Yuj3V7NeW5OUiCc0jY3+ZG2aXibmUSb4YdRU3wbxqvA2q
+zak1i9z5kBjDkcxjOf5f2FTQXwqnWz4eOmvGY5orhxICuG5tuvv2ol9rcOs69d+UcpZOsRYfvFld
+f54qNtOJ5+Jtf1XVpMwySzI/L3/Ly/8A1FHg0aLS9aklslKw3flM8Z3AbzR09tzVEjYcoeULsGAc
+CmvEOnarqt1aWelreSySK3PBbgnnUYO4HYUdJkSWAKd+XlOPepJ+J9S4VhttZ01Od1k5JEJ/OpBy
+PuB9hQZLxHpk+n6sySQvE2fiR1wVYdQQad2F5eWkb/h5XiJjaNgDsVbZhj3qa4o1W64nu5tVvIDF
+M7c2CcmohlBJ2xlaB9wzrl1w/r8Go2ZUyxDHK35WB6g16R4T1634i0SHUYF8st8MkZOeRh1FeW/J
+WRw2enU1qvgnxTp+nR3Gj6lMtuZ5Q8EjbKSRjlJ7dBUpK2QkD0rg3zRGIJ2O1GSstlFwAc0VsA7V
+35UXBzUo6p2oV0KaFZVGKm9LxKM9M0T0NdklENvLLjPIhYD5CukRjfi1qCXHF88eQY7ZFgH2yf1J
++1VyGOORMYGKbNK97JPLcMWkkcyOT3J60haieGQqmeXO2a0wkJ7FGHwkKexBpBLqeycJOTy9m7U/
+tJo5sebGMinM8VpLGedMqN996COvF/Hw+ZFyyOo6A9fl71H2NwEvg2cGQcrjpuOmf+u1PxZRK7SW
+NyFx+5UfqyK3/eEBW4jPM4/jH96Im7d/jOGG/auXo57RlO/ptTHS7xJ4lIOT0p60mQ4GxopppdwG
+QwOfy7UtIshlFuInmD4AULnOegqOGI7wMo2JwfSn8mXQgNysR8LZ70Fp4h8ML/hWK01C51Czb8eq
+q0ETEtGx3HsR7im96wjWJFlVmjKq3fmxlz/8ahtK1TXdc4jjt9cvGljit2EQXbmIAAz9KXnYWVq8
+apjCSSDfJGwTr/yNAiMlFy/7oORvjepDihUk0CGFZFjDToDI/wCUZOMn7060vT7OXQVvpLadCwZY
+wzBEkPL8JJPQBs5OcHp61E8Xs3+TwW867mQcy47gGgl+P+CtL4d0qyv9L1w6glwgLq6gEZHUY/lv
+Wb3MmG5c4PKR9akYbaRinPczyRpuiO2QufTNRuqxNHdKvYsD/wBfagPFtEAegFFkbzcKq7A9aDDK
+igx8qHb8zHCiiPQfhJrX+bcJQiWQyT2rGFyeu3Q/aropX0rzp4e8YS8JXqxPCJ7O5I89R+ZcfvCv
+QOnXttqFlFeWkgkglUMjDuKzY1DvPegGoLRWqNFFbI7UK4g2O1CshgAcelGRFkUxt0YEH610jAo0
+LKD71qDzaI1g1Ke1JB5XZM/I4o1xGY2Db4FLcWRfgOML9OgS8kH0LH+4p75AmgDcwwa2wYYLplNi
+BtiixzzRH4gWA7EVyM/h5miz8NPIRHMOUAADvnFENpYrO7X87wSnoU/tTC80bUoR5kEgvUG+xww+
+h/pUxJawr8cec02FxJE27HlHrQQOkTG2lliZCuGyA2xHtUxFKzfER9qYaxBLPci9tD5g5cSRj8w9
+x6ijW8+YsZHTegPLvuMDBp2JAIlbbcY6U2tpPMV8/LOKAceXjB2oJ3hCeN9Z3UMywvjbp0rt9bmS
+OeeQZeQQrGN8AkFjt26ioLhed4uKIU5iOdXBH/GrLqDfEkaDc3BJ/wCKqv8ASirgbmxtLENZ6W7x
+ae37cMQFYc+Qq8wOSAASfXvvVP8AEdndhLJzeZ+LfmBbJyc7VJPdW0ziW6hilkAzzFFBHyqJ8RLt
+brTLec8gkacFiFwT8JoIK2lXlBYbVE8RMFaKT19/ejG5wAq0x1uVnhiB3IbagdwqHVc9MUSP9vdF
+v3E+FaPptvdX7Q2NhDJPdzEJHGgySTWq2/hOukcMvd6teSC/EZZUhwY1OM4JxknttjrQZqYlDGRx
+sOgNaX4M8Sw2Usmi6hOscUp5rcucAN3X60Xh3wf4k1e1ju7ua105GHMsUuWk/wCQH5flnNUnUrFo
+NVms5MB7VzGeX+JTg4+1QemVwRkYxRGJzWd+H3HtrJDBo+sytDdKOSOdz8MnoCexrRRg4IIIPTFZ
+aHRtutCiEkChWappJkLjJrkaEnJo8rCgjYxvW4MN8XrNYeNr3GR5ipJ09VFRWg3XnQlMnmTYg1dP
+G+z/APGbO8HSa25D81J/oRWXQztYXvm78j7PVYqw6hFGSJOXp1OKbpzKOZExipGIxXduGT4sjam3
+lmJzG+du4NULW84YBZCPfaiXtosykxt13xXFCEEDOR39aNDKUk/KceuelEVq/gu4JcRrICD13okr
+XBhE8x+IHDEdSOxNW55oHT4lXPqKjr+2je0kjHLlxjtRURZNiPYjfvXWbAIYCkbZuVeQnp1HvSU8
+yDIBG+2KIW055DxDp7W7AP5uCcA/Dj4v0zVnhdpZIWOxMZkP/Ni230IqrcMRo2tyTqCTDbyMBnbJ
+HKP/AJVaLGRBcXDkhkiYRqADsFGB/KosSyxQOg50JPuKqvH7CJrG2RsjDyN+gFS9veTT3W4+HPrV
+X48uObWljBwI4gMDsTRTbhvTbjXtfstHtZooprqURh5Gwq+5qa8ZeCLjgi6s7d9VttRSUkiSIcpV
+h1BXP61VrW4ltikttIYZo2DI69Qw70e9utS1/VrSPULlp5ZZVXJwOpAJrSN7/wAO3B6Wul/9qL5R
++JuU5bcMu6Reo92/lWo2GmS6hqBv78AJGSLe3JyFH8Te/f2+dNOHEkt+FtOtRcRmNY1AePBGw2pT
+U9RFoHSW9htI+txcu3KIl74z1JOwHr8qjUTkY/Fo0Vs7RW6bM46k98e9Y54+No6alYWtjbRw3MMZ
+82RVwFQ9Ax9Sd9/ep3WvFzQ7G3FhollcXap8IkJ8tSPXfcn3xVP408QdP4k4Yk0M6GLbmkEvmGfn
+JcHOTsPehqhpb2kjiTl53H72dqv/AADxs9hyaZquTaDaObmyY/Y+1Z000iAsRGF6DkJOPnSE1xME
+ODzKR2rNiPTqyxTQrLDIskbDKspyCKFZ14IX7XGgXFm0vP5MpIBOcA9PpQrNaXlmwBtXRkkGkQwz
+SqZyKSireMVh+I4TivAPitJ1JP8ApbY/ry1h+pW3OTgnHbevQPHsN3qOjwaDYcguNTnEILdAoBcn
+/wBtYMRzKFIww2atxmmGl6nPps3lOeaPO4qz2t5aXyghwD71WLy18zJC71FwzSW8uCSKrK+XFpyn
+nifJPXemp8xdgjZ+RqOtLucqG5+YEbUs2pyoCWJoHEqyn8sbkkdlNRV4uoKwPkuvtykVIwcQRK2G
+yKPNrEEzpySFGXpk/Cw9DRVT1qeW1lWQoyCUZx03HWo1LtpG3JrdvBySz1fxAtbDUeG7PUomVjI8
+8SukAxs45ts5GPXrXp+z4b4chAaHQ9MjI6FbVB/SkMeG/CzTdS1jiRNOtdPu5TduiGZISyRKrcxZ
+j6bV6l4a8FuDLSxWO7hvb+U7vJNM0ZLHr8KYx+taqkcUIVI41VewUYArsigSZGKuKoKeEHAMJDpo
+rhvX8VKf5tTW78DfDW+na5udDleV8Zb8ZMOnyatLyCu/pQQgDqKDNbPwJ8M7W6juU4e8xozkLLcy
+un1Utg/WlX8EfDsasurW+hm3u0bmUxXEiqp9lzj9K0gkAda7nK7VRVZeDdIeJwqzwzH8s6vzPGfU
+Bsr+lecPGPh/ifh/iBbLW9Sl1GxkzJY3LfCGGdwR0DDv8/evWb7HOdqpvjLwsvGHh9qGnxIDf26G
+4sX7iVRnl/5br9amFeQiOQ4yCPbrTeSLLl4zv1371DQX8yA+YCGHX2NBuJCshR0UgbdKiHdxd8jl
+dwfSm348xnm5Oai/ibe/bflRj0P96ZXHNBLyMpA96GtG8G9cS34rWDIWO9Qxuv8AqG6n+n1oVV/D
+S3a7490qNCwVZfNbHooJ/pQrnZjUejBjPSl12IG1Nhkt0pxGD1NSVcQ3F17HpeqcOalNJyRRaj5b
+tnAAdGWsIlYJql7CSMpcOvX0Y1vvG3DZ4q4Yn0qO4W3nLLJDKwyEZTnt7ZFYxxfwJq3DV9LNNfR6
+j+xWeeRFK8nM3IOvXfG9bjNRbJzHAxnGMUwurJJVbb608t5gFywJJ9KcZXBBGRVREabM1rP5E26n
+cGp0Q288PMNjTC/tkki5kwCB2613SblsCF2APT50AvNMj5TIuB3z3qNSMBvi7Hap65TbP9dqirhV
+QFsYOelNMW7wn1ltC4uspjPy28kgSTmOwB6H23r2Fpl+rwhieY8ucj+9fP1794mJBAxWhcEeJHF1
+vYCCDU3Fra/xgNk9lyRnHtRXtA3S8iuCppvd38ceDg7HtWD6B40W7Wwe9tQZlHxRBsBv9p7VcND4
+80ni+ynbTGkt7222ntJtnXPQjsw9xV1V8XX7U8o5XO5H8v70dNcsOflZ+VvQmszvG1Yc7RJzxk5y
+N+tIQX8sbjmkhBO551II+9T9GNfTUbaTHJMjfI04S4UjGay+zvp5CpJBP+kbCp2y1GRByFmPfc9K
+foxb2lDE7/ShDIomUZ2OxHzquG8nZQyinNpdyNgEYYGmmPH3jLoFpo3iVrulEGBBcmaEqNuST4wP
+pzY+lUu60i2EBImVpPbvWwf4z7Q23iBpeoRLgXenAE+pR2/owrCo7iUOAT9KrIvJJbyfDkYNSkFx
+Hd24jlxzr+U0QeVdDHMFIG5NMJk8p/gl+1X0aj4Eaf5vFNzeFdrW2IB92IA/QGhVn8BdNntOF59U
+uYyn46UeVnqyLtn5Ek/ahXLq/Wo0LlAbY0dAc9TSJOD70ojjY1iNU7iyM4yKqfF8IvNa1KyfcT8N
+3DKD3aN1dftirVDKp9Kz/wAbLq60pdO1ywmWN+SazfftIvp8s1uJWNluQq/NsRnrTtJkbBUjGKj7
+OSOeE2rN8QGVNNj51jNljzRnvW2U7JKrKQoyR1phM/lSK64z1NKQOsq8/MObrmkJjzty8h9M1BOW
+E63UIQMOalZdBkul+J8DtvVTE01pLzwyMuPQU4bi3UUQJkEjbJFMNSl1wzbwjnurxQvcLTC61OGO
+FdO08IkS+nf5moDUtbvr5irynHtSNoxiIbO9WRFnVWEBfm3A9abadJxE16l3pN7cWk8eyyxyFDj0
+yO3tSNpqSD4ZcYPWp231e0t7blgYA43FBYLbxG474Zt+eW/ttULYDrOhJP1BGftVguOK+MNV4cte
+I7LheG4sp8q01rO+Y5FPxKy9j36dMVlVzfSX1zsMhdzWm/4fePLPhniSTh7XJUj0bWCFLufggnH5
+XPoD+Un5HtRZVq4J8Q+GDpbza/FrMFzAc3Kxwq/kL/ER1K57gbbVpPDHFPAuuMo0jjbT5XbcQzv5
+T/8Apf8AtTTirwj0niC9N7a6oulXirmKSABiT/qGd1x2715q8SOAZ9D4gm0y4jWw1INlMHFtdL2a
+Mn8pP8JqZFe1raxkKjlaGZT3RhinSWLKdosZr532mu8UcPXLQ2uralp8sZwViuHT+Rqaj8XPEeKE
+xJxfqnKRjeXJ+53rWJrYv8cd9YC94ask5Wv44pXch90jJUAEe5B+1eaDKSMgkGlNV1HUNVvZL3Ur
+ye7uZDl5ZnLMfqaluDODeIeLLvydHsHkjU4kuH+GKP5sf5DJ9qvgghI4YkM2/vWreEXhhfcQSxaz
+r8Ulto6kMkbfC917DuF9+/b1GkcB+EPDvDix3mphdX1Jfi5pF/Yxn/Snf5tn5Cr9PP22A6Vi9Lhp
+NHFEkcEEaxRRqFRFGAoHQAUKTnkHPmhXOqTcnNBGNChWI0VjbvgVnX+IiMtwbZzKQDHeA/dWoUK6
+c+s1g0N2/mrMuzKas0YS/tQ7LguvN8qFCt1kwV2tJ/KU5XNO2cqSQBkj0oUKBheFmzvudyah51Oe
+tChViVyOFQfenCxKRvQoVUJSRgHrSTMQ2ATQoVYsOIZ3gBAJpve3Ek5BJ70KFIJXh/i/ibQZEl0r
+Wbu2KkEKJCV/9J2q73Pi3xPxRZ/5JqENhNPe8tsLqSLLICcbD60KFLCM+4gDw63cWUkjTpaSNApb
+qQpIrWPD/wAMeG+IuDLW/vWvYbqUNl4ZRjrtsQaFCs9XIs9XHhvwa4M04CS9hudVl5sg3EnKg/4r
+jP1zWhW8dvY2kdpZW8NtbxjCRRIFVR7AUKFc9tawSSViDTSRsnehQqKbyjLbUKFCs0j/2Q==
+
+Cheerio!
+
+--Multipart_Sun_Oct_17_10:37:40_2010-1--
--- /dev/null
+Path: uutiset.elisa.fi!feeder2.news.elisa.fi!feeder.erje.net!newsfeed.kamp.net!newsfeed0.kamp.net!nx02.iad01.newshosting.com!newshosting.com!post01.iad!not-for-mail
+X-newsreader: xrn 9.03-beta-14-64bit
+Sender: jimbo@lews (Jimbo Foobarcuux)
+From: jimbo@slp53.sl.home (Jimbo Foobarcuux)
+Reply-To: slp53@pacbell.net
+Subject: Re: Are writes "atomic" to readers of the file?
+Newsgroups: comp.unix.programmer
+References: <e9065dac-13c1-4103-9e31-6974ca232a89@t15g2000prt.googlegroups.com> <87hbblwelr.fsf@sapphire.mobileactivedefense.com> <pql248-4va.ln1@wilbur.25thandClement.com> <ikns6r$li3$1@Iltempo.Update.UU.SE> <8762s0jreh.fsf@sapphire.mobileactivedefense.com> <ikqqp1$jv0$1@Iltempo.Update.UU.SE> <87hbbjc5jt.fsf@sapphire.mobileactivedefense.com> <ikr0na$lru$1@Iltempo.Update.UU.SE> <tO8cp.1228$GE6.370@news.usenetserver.com> <ikr6ks$nlf$1@Iltempo.Update.UU.SE> <8ioh48-8mu.ln1@leafnode-msgid.gclare.org.uk>
+Organization: UseNetServer - www.usenetserver.com
+X-Complaints-To: abuse@usenetserver.com
+Message-ID: <oktdp.42997$Te.22361@news.usenetserver.com>
+Date: 08 Mar 2011 17:04:20 GMT
+Lines: 27
+Xref: uutiset.elisa.fi comp.unix.programmer:181736
+
+John Denver <jd@clare.See-My-Signature.invalid> writes:
+>Eric the Red wrote:
+>
+>>> There _IS_ a requirement that all reads and writes to regular files
+>>> be atomic. There is also an ordering guarantee. Any implementation
+>>> that doesn't provide both atomicity and ordering guarantees is broken.
+>>
+>> But where is it specified?
+>
+>The place where it is stated most explicitly is in XSH7 2.9.7
+>Thread Interactions with Regular File Operations:
+>
+> All of the following functions shall be atomic with respect to each
+> other in the effects specified in POSIX.1-2008 when they operate on
+> regular files or symbolic links:
+>
+> [List of functions that includes read() and write()]
+>
+> If two threads each call one of these functions, each call shall
+> either see all of the specified effects of the other call, or none
+> of them.
+>
+
+And, for the purposes of this paragraph, the two threads need not be
+part of the same process.
+
+jimbo
--- /dev/null
+Date: Thu, 31 Jul 2008 14:57:25 -0400
+From: "John Milton" <jm@example.com>
+Subject: Fere libenter homines id quod volunt credunt
+To: "Julius Caesar" <jc@example.com>
+Message-id: <3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com>
+MIME-version: 1.0
+x-label: Paradise losT
+X-Keywords: milton,john
+Content-type: text/plain; charset=us-ascii
+Content-transfer-encoding: 7BIT
+Precedence: high
+
+OF Mans First Disobedience, and the Fruit
+Of that Forbidden Tree, whose mortal tast
+Brought Death into the World, and all our woe,
+With loss of Eden, till one greater Man
+Restore us, and regain the blissful Seat, [ 5 ]
+Sing Heav'nly Muse,that on the secret top
+Of Oreb, or of Sinai, didst inspire
+That Shepherd, who first taught the chosen Seed,
+In the Beginning how the Heav'ns and Earth
+Rose out of Chaos: Or if Sion Hill [ 10 ]
+Delight thee more, and Siloa's Brook that flow'd
+Fast by the Oracle of God; I thence
+Invoke thy aid to my adventrous Song,
+That with no middle flight intends to soar
+Above th' Aonian Mount, while it pursues [ 15 ]
+Things unattempted yet in Prose or Rhime.
+And chiefly Thou O Spirit, that dost prefer
+Before all Temples th' upright heart and pure,
+Instruct me, for Thou know'st; Thou from the first
+Wast present, and with mighty wings outspread [ 20 ]
+Dove-like satst brooding on the vast Abyss
+And mad'st it pregnant: What in me is dark
+Illumin, what is low raise and support;
+That to the highth of this great Argument
+I may assert Eternal Providence, [ 25 ]
+And justifie the wayes of God to men.
--- /dev/null
+Date: Thu, 31 Jul 2008 14:57:25 -0400
+From: "Socrates" <soc@example.com>
+Subject: cool stuff
+To: "Alcibiades" <alki@example.com>
+Message-id: <3BE9E6535E0D852173@emss35m06.us.lmco.com>
+MIME-version: 1.0
+Content-type: text/plain; charset=us-ascii
+Content-transfer-encoding: 7BIT
+Precedence: high
+
+The hour of departure has arrived, and we go our ways—I to die, and you to
+live. Which is better God only knows.
+
+http-emacs
--- /dev/null
+From: Napoleon Bonaparte <nb@example.com>
+To: Edmond =?UTF-8?B?RGFudMOocw==?= <ed@example.com>
+Subject: rock on dude
+User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO)
+Fcc: .sent
+MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka")
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Le 24 février 1815, la vigie de Notre-Dame de la Garde signala le trois-mâts
+le Pharaon, venant de Smyrne, Trieste et Naples.
+
+Comme d'habitude, un pilote côtier partit aussitôt du port, rasa le château
+d'If, et alla aborder le navire entre le cap de Morgion et l'île de Rion.
+
+Aussitôt, comme d'habitude encore, la plate-forme du fort Saint-Jean s'était
+couverte de curieux; car c'est toujours une grande affaire à Marseille que
+l'arrivée d'un bâtiment, surtout quand ce bâtiment, comme le Pharaon, a été
+construit, gréé, arrimé sur les chantiers de la vieille Phocée, et appartient
+à un armateur de la ville.
+
+Cependant ce bâtiment s'avançait; il avait heureusement franchi le détroit que
+quelque secousse volcanique a creusé entre l'île de Calasareigne et l'île de
+Jaros; il avait doublé Pomègue, et il s'avançait sous ses trois huniers, son
+grand foc et sa brigantine, mais si lentement et d'une allure si triste, que
+les curieux, avec cet instinct qui pressent un malheur, se demandaient quel
+accident pouvait être arrivé à bord. Néanmoins les experts en navigation
+reconnaissaient que si un accident était arrivé, ce ne pouvait être au
+bâtiment lui-même; car il s'avançait dans toutes les conditions d'un navire
+parfaitement gouverné: son ancre était en mouillage, ses haubans de beaupré
+décrochés; et près du pilote, qui s'apprêtait à diriger le Pharaon par
+l'étroite entrée du port de Marseille, était un jeune homme au geste rapide et
+à l'œil actif, qui surveillait chaque mouvement du navire et répétait chaque
+ordre du pilote.
--- /dev/null
+Return-Path: <foo@example.com>
+Delivered-To: foo@example.com
+Received: from [128.88.204.56] by freemailng0304.web.de with HTTP;
+ Mon, 07 May 2005 00:27:52 +0200
+Date: Mon, 07 May 2005 00:27:52 +0200
+Message-Id: <293847329847@web.de>
+MIME-Version: 1.0
+From: =?iso-8859-1?Q? "=F6tzi" ?= <oetzi@web.de>
+To: foo@example.com
+Subject: =?iso-8859-1?Q?Re:=20der=20b=E4r=20und=20das=20m=E4dchen?=
+Precedence: fm-user
+Organization: http://freemail.web.de/
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Transfer-Encoding: 8bit
+X-MIME-Autoconverted: from quoted-printable to 8bit by mailhost6.ladot.com id j48MScQ30791
+X-Label: \backslash
+X-UIDL: 93h!!\i<!!L)l!!%_I!!
+X-Spam-Checker-Version: SpamAssassin 3.0.2 (2004-11-16) on mindcrime
+X-Spam-Level:
+X-Spam-Status: No, score=-2.3 required=3.0 tests=AWL,BAYES_00 autolearn=ham
+ version=3.0.2
+
+Viele liebe Gruesse aus der Stadt der Städte..
+
+__________________________________________________________
+Mit WEB.DE FreePhone mit hoechster Qualitaet ab 0 Ct./Min.
+weltweit telefonieren! http://freephone.web.de/?mc=021201
+
+
--- /dev/null
+Date: Mon, 13 Jun 2011 14:57:25 -0400
+From: xyz@123.xx
+Subject: abc
+To: foo@bar.cx
+Message-id: <abc@def>
+
+123
--- /dev/null
+Date: Thu, 31 Jul 2008 14:57:25 -0400
+From: "Geoff Tate" <jeff@example.com>
+Subject: eyes of a stranger
+To: "Enrico Fermi" <enrico@example.com>
+Message-id: <3BE9E6535E302944823E7A1A20D852173@msg.id>
+MIME-version: 1.0
+X-label: @NextActions operation:mindcrime Queensrÿche
+Content-type: text/plain; charset=us-ascii
+Content-transfer-encoding: 7BIT
+Precedence: high
+
+And I raise my head and stare
+Into the eyes of a stranger
+I've always known that the mirror never lies
+People always turn away
+From the eyes of a stranger
+Afraid to know what
+Lies behind the stare
--- /dev/null
+Date: Mon, 11 Sep 2023 19:57:25 -0400
+From: "Tommy" <tommy@example.com>
+Subject: Hide and seek
+To: "Andreas" <andy@example.com>
+Message-id: <3BE9E65sdfklsajdfl3E7A1A20D852173@msg.id>
+MIME-version: 1.0
+
+Behind the polished barrier
+The pending storm draws near
+Although it's an inferno
+You can not step back for your fears
+Hurry son I need to rest
+finish the puzzle you do it best
+I'll do what I can
+But I am telling you
+It can't be done without you!
--- /dev/null
+Date: Sat, 12 Nov 2011 12:06:23 -0400
+From: "Richard P. Feynman" <rpf@example.com>
+Subject: atoms
+To: "Democritus" <demo@example.com>
+Message-id: <3BE9E6535E302944823E7A1A20D852173@msg.id>
+MIME-version: 1.0
+Content-type: text/plain; charset=us-ascii
+Content-transfer-encoding: 7BIT
+Precedence: high
+
+If, in some cataclysm, all scientific knowledge were to be destroyed,
+and only one sentence passed on to the next generation of creatures,
+what statement would contain the most information in the fewest words?
+I believe it is the atomic hypothesis (or atomic fact, or whatever you
+wish to call it) that all things are made of atoms — little particles
+that move around in perpetual motion, attracting each other when they
+are a little distance apart, but repelling upon being squeezed into
+one another. In that one sentence you will see an enormous amount of
+information about the world, if just a little imagination and thinking
+are applied.
--- /dev/null
+Return-Path: <foo@example.com>
+Subject: Fwd: rfc822
+From: foobar <foo@example.com>
+To: martin
+Content-Type: multipart/mixed; boundary="=-XHhVx/BCC6tJB87HLPqF"
+Message-Id: <1077300332.871.27.camel@example.com>
+Mime-Version: 1.0
+X-Mailer: Ximian Evolution 1.4.5
+Date: Fri, 20 Feb 2004 19:05:33 +0100
+
+--=-XHhVx/BCC6tJB87HLPqF
+Content-Type: text/plain
+Content-Transfer-Encoding: 7bit
+
+Hello world, forwarding some RFC822 message
+
+--=-XHhVx/BCC6tJB87HLPqF
+Content-Disposition: inline
+Content-Type: message/rfc822
+
+Return-Path: <cuux@example.com>
+Message-ID: <9A01B19D0D605D478E8B72E1367C66340141B9C5@example.com>
+From: frob@example.com
+To: foo@example.com
+Subject: hopjesvla
+Date: Sat, 13 Dec 2003 19:35:56 +0100
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-1
+Content-Transfer-Encoding: 7bit
+
+The ship drew on and had safely passed the strait, which some volcanic shock
+has made between the Calasareigne and Jaros islands; had doubled Pomegue, and
+approached the harbor under topsails, jib, and spanker, but so slowly and
+sedately that the idlers, with that instinct which is the forerunner of evil,
+asked one another what misfortune could have happened on board. However, those
+experienced in navigation saw plainly that if any accident had occurred, it was
+not to the vessel herself, for she bore down with all the evidence of being
+skilfully handled, the anchor a-cockbill, the jib-boom guys already eased off,
+and standing by the side of the pilot, who was steering the Pharaon towards the
+narrow entrance of the inner port, was a young man, who, with activity and
+vigilant eye, watched every motion of the ship, and repeated each direction of
+the pilot.
+
+--=-XHhVx/BCC6tJB87HLPqF--
--- /dev/null
+From: dwarf@siblings.net
+To: root@eruditorum.org
+Subject: Fwd: test abc
+References: <8639ddr9wu.fsf@cthulhu.djcbsoftware>
+User-agent: mu 0.98pre; emacs 24.0.91.9
+Date: Thu, 24 Nov 2011 14:24:00 +0200
+Message-ID: <861usxr9nj.fsf@cthulhu.djcbsoftware>
+Content-Type: multipart/mixed; boundary="=-=-="
+MIME-Version: 1.0
+
+--=-=-=
+Content-Type: text/plain
+
+Saw the website. Am willing to stipulate that you are not RIST 9E03. Suspect
+that you are the Dentist, who yearns for honest exchange of views. Anonymous,
+digitally signed e-mail is the only safe vehicle for same.
+
+If you want me to believe you are not the Dentist, provide plausible
+explanation for your question regarding why we are building the Crypt.
+
+Yours truly,
+
+--=-=-=
+Content-Type: message/rfc822
+Content-Disposition: inline; filename=
+ "1322137188_3.11919.foo:2,S"
+Content-Description: rfc822
+
+From: dwarf@siblings.net
+To: root@eruditorum.org
+Subject: test abc
+User-agent: mu 0.98pre; emacs 24.0.91.9
+Date: Thu, 24 Nov 2011 14:18:25 +0200
+Message-ID: <8639ddr9wu.fsf@cthulhu.djcbsoftware>
+Content-Type: text/plain
+MIME-Version: 1.0
+
+As I stepped on this unknown middle-aged Filipina's feet during an ill-advised
+ballroom dancing foray, she leaned close to me and uttered some latitude and
+longitude figures with a conspicuously large number of significant digits of
+precision, implying a maximum positional error on the order of the size of a
+dinner plate. Gosh, was I ever curious!
+
+--=-=-=--
--- /dev/null
+Return-Path: <gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level:
+X-Spam-Status: No, score=-4.9 required=3.0 tests=BAYES_00,DATE_IN_PAST_96_XX,
+ RCVD_IN_DNSWL_MED autolearn=ham version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+ by mail.xxxxsoftware.nl (Postfix) with ESMTP id 5123469CB3
+ for <xxxx@localhost>; Thu, 7 Aug 2008 08:10:19 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [66.249.91.109]
+ by mindcrime with IMAP (fetchmail-6.3.8)
+ for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:19 +0300 (EEST)
+Received: by 10.142.237.21 with SMTP id k21cs39272wfh; Wed, 6 Aug 2008
+ 20:15:17 -0700 (PDT)
+Received: by 10.65.133.8 with SMTP id k8mr2071878qbn.7.1218078916289; Wed, 06
+ Aug 2008 20:15:16 -0700 (PDT)
+Received: from sourceware.org (sourceware.org [209.132.176.174]) by
+ mx.google.com with SMTP id 28si7904461qbw.0.2008.08.06.20.15.15; Wed, 06 Aug
+ 2008 20:15:16 -0700 (PDT)
+Received-SPF: neutral (google.com: 209.132.176.174 is neither permitted nor
+ denied by domain of gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org)
+ client-ip=209.132.176.174;
+Authentication-Results: mx.google.com; spf=neutral (google.com:
+ 209.132.176.174 is neither permitted nor denied by domain of
+ gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org)
+ smtp.mail=gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org
+Received: (qmail 13493 invoked by alias); 7 Aug 2008 03:15:13 -0000
+Received: (qmail 13485 invoked by uid 22791); 7 Aug 2008 03:15:12 -0000
+Received: from mailgw1a.lmco.com (HELO mailgw1a.lmco.com) (192.31.106.7)
+ by sourceware.org (qpsmtpd/0.31) with ESMTP; Thu, 07 Aug 2008 03:14:27 +0000
+Received: from emss07g01.ems.lmco.com (relay5.ems.lmco.com [166.29.2.16])by
+ mailgw1a.lmco.com (LM-6) with ESMTP id m773EPZH014730for
+ <gcc-help@gcc.gnu.org>; Wed, 6 Aug 2008 21:14:25 -0600 (MDT)
+Received: from CONVERSION2-DAEMON.lmco.com by lmco.com (PMDF V6.3-x14 #31428)
+ id <0K5700601NO18J@lmco.com> for gcc-help@gcc.gnu.org; Wed, 06 Aug 2008
+ 21:14:25 -0600 (MDT)
+Received: from EMSS04I00.us.lmco.com ([166.17.13.135]) by lmco.com (PMDF
+ V6.3-x14 #31428) with ESMTP id <0K5700H5MNNWGX@lmco.com> for
+ gcc-help@gcc.gnu.org; Wed, 06 Aug 2008 21:14:20 -0600 (MDT)
+Received: from EMSS35M06.us.lmco.com ([158.187.107.143]) by
+ EMSS04I00.us.lmco.com with Microsoft SMTPSVC(5.0.2195.6713); Wed, 06 Aug
+ 2008 23:14:20 -0400
+Date: Thu, 31 Jul 2008 14:57:25 -0400
+From: "Mickey Mouse" <anon@example.com>
+Subject: gcc include search order
+To: "Donald Duck" <gcc-help@gcc.gnu.org>
+Message-id: <3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com>
+MIME-version: 1.0
+Content-type: text/plain; charset=us-ascii
+Content-transfer-encoding: 7BIT
+Content-class: urn:content-classes:message
+Mailing-List: contact gcc-help-help@gcc.gnu.org; run by ezmlm
+Precedence: klub
+List-Id: <gcc-help.gcc.gnu.org>
+List-Unsubscribe: <mailto:gcc-help-unsubscribe-xxxx.klub=gmail.com@gcc.gnu.org>
+List-Archive: <http://gcc.gnu.org/ml/gcc-help/>
+List-Post: <mailto:gcc-help@gcc.gnu.org>
+List-Help: <mailto:gcc-help-help@gcc.gnu.org>
+Sender: gcc-help-owner@gcc.gnu.org
+Delivered-To: mailing list gcc-help@gcc.gnu.org
+Content-Length: 3024
+
+
+Hi.
+In my unit testing I need to change some header files (target is
+vxWorks, which supports some things that the sun does not).
+So, what I do is fetch the development tree, and then in a new unit test
+directory I attempt to compile the unit under test. Since this is NOT
+vxworks, I use sed to change some of the .h files and put them in a
+./changed directory.
+
+When I try to compile the file, it is still using the .h file from the
+original location, even though I have listed the include path for
+./changed before the include path for the development tree.
+
+Here is a partial output from gcc using the -v option
+
+GNU CPP version 3.1 (cpplib) (sparc ELF)
+GNU C++ version 3.1 (sparc-sun-solaris2.8)
+ compiled by GNU C version 3.1.
+ignoring nonexistent directory "NONE/include"
+#include "..." search starts here:
+#include <...> search starts here:
+ .
+ changed
+ /export/home4/xxx/yyyy/builds/int_rel5_latest/src/mp/interface
+ /export/home4/xxx/yyyy/builds/int_rel5_latest/src/ap/app
+ /export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common
+ /export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/interface
+ /usr/local/include/g++-v3
+ /usr/local/include/g++-v3/sparc-sun-solaris2.8
+ /usr/local/include/g++-v3/backward
+ /usr/local/include
+ /usr/local/lib/gcc-lib/sparc-sun-solaris2.8/3.1/include
+ /usr/local/sparc-sun-solaris2.8/include
+ /usr/include
+End of search list.
+
+I know the changed file is correct and that the include is not working
+as expected, because when I copy the file from ./changed, back into the
+development tree, the compilation works as expected.
+
+One more bit of information. The source that I cam compiling is in
+/export/home4/xxx/yyyy/builds/int_rel5_latest/src/ap/app
+And it is including files from
+/export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common
+These include files should be including the files from ./changed (when
+they exist) but they are ignoring the .h files in the ./changed
+directory and are instead using other, unchanged files in the
+/export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common
+directory.
+
+The gcc command line is something like
+
+ TEST_DIR="."
+
+ CHANGED_DIR_NAME=changed
+ CHANGED_FILES_DIR=${TEST_DIR}/${CHANGED_DIR_NAME}
+
+ CICU_HEADER_FILES="-I ${AP_INTERFACE_FILES} -I ${AP_APP_FILES} -I
+${SHARED_COMMON_FILES} -I ${SHARED_INTERFACE_FILES}"
+
+ HEADERS="-I ./ -I ${CHANGED_FILES_DIR} ${CICU_HEADER_FILES}"
+ DEFINES="-DSUNRUN -DA10_DEBUG -DJOETEST"
+
+ CFLAGS="-v -c -g -O1 -pipe -Wformat -Wunused -Wuninitialized -Wshadow
+-Wmissing-prototypes -Wmissing-declarations"
+
+ printf "Compiling the UUT File\n"
+ gcc -fprofile-arcs -ftest-coverage ${CFLAGS} ${HEADERS} ${DEFINES}
+${AP_APP_FILES}/unitUnderTest.cpp
+
+
+I hope this explanation is clear. If anyone knows how to fix the command
+line so that it gets the .h files in the "changed" directory are used
+instead of files in the other include directories.
+
+Thanks
+Joe
+
+----------------------------------------------------
+Time Flies like an Arrow. Fruit Flies like a Banana
+
+
--- /dev/null
+Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org>
+X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime
+X-Spam-Level:
+X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham
+ version=3.2.5
+X-Original-To: xxxx@localhost
+Delivered-To: xxxx@localhost
+Received: from mindcrime (localhost [127.0.0.1])
+ by mail.xxxxsoftware.nl (Postfix) with ESMTP id C4D6569CB3
+ for <xxxx@localhost>; Thu, 7 Aug 2008 08:10:08 +0300 (EEST)
+Delivered-To: xxxx.klub@gmail.com
+Received: from gmail-imap.l.google.com [66.249.91.109]
+ by mindcrime with IMAP (fetchmail-6.3.8)
+ for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:08 +0300 (EEST)
+Received: by 10.142.237.21 with SMTP id k21cs34794wfh; Wed, 6 Aug 2008
+ 13:40:29 -0700 (PDT)
+Received: by 10.100.33.13 with SMTP id g13mr1093301ang.79.1218055228418; Wed,
+ 06 Aug 2008 13:40:28 -0700 (PDT)
+Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com
+ with ESMTP id d19si15908789and.17.2008.08.06.13.40.27; Wed, 06 Aug 2008
+ 13:40:28 -0700 (PDT)
+Received-SPF: pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender) client-ip=199.232.76.165;
+Authentication-Results: mx.google.com; spf=pass (google.com: domain of
+ help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165
+ as permitted sender)
+ smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Received: from localhost ([127.0.0.1]:56316 helo=lists.gnu.org) by
+ lists.gnu.org with esmtp (Exim 4.43) id 1KQpo3-0007Pc-Qk for
+ xxxx.klub@gmail.com; Wed, 06 Aug 2008 16:40:27 -0400
+From: anon@example.com
+Newsgroups: gnu.emacs.help
+Date: Wed, 6 Aug 2008 20:38:35 +0100
+Message-ID: <r6bpm5-6n6.ln1@news.ducksburg.com>
+References: <55dbm5-qcl.ln1@news.ducksburg.com>
+ <mailman.15710.1217599959.18990.help-gnu-emacs@gnu.org>
+Mime-Version: 1.0
+Content-Type: text/plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+X-Trace: individual.net bABVU1hcJwWAuRwe/097AAoOXnGGeYR8G1In635iFGIyfDLPUv
+X-Orig-Path: news.ducksburg.com!news
+Cancel-Lock: sha1:wK7dsPRpNiVxpL/SfvmNzlvUR94=
+ sha1:oepBoM0tJBLN52DotWmBBvW5wbg=
+User-Agent: slrn/pre0.9.9-120/mm/ao (Ubuntu Hardy)
+Path: news.stanford.edu!headwall.stanford.edu!newshub.sdsu.edu!feeder.erje.net!proxad.net!feeder1-2.proxad.net!feed.ac-versailles.fr!fu-berlin.de!uni-berlin.de!individual.net!not-for-mail
+Xref: news.stanford.edu gnu.emacs.help:160868
+To: help-gnu-emacs@gnu.org
+Subject: Re: Learning LISP; Scheme vs elisp.
+X-BeenThere: help-gnu-emacs@gnu.org
+X-Mailman-Version: 2.1.5
+Precedence: list
+List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org>
+List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe>
+List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs>
+List-Post: <mailto:help-gnu-emacs@gnu.org>
+List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help>
+List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>,
+ <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe>
+Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org
+Content-Length: 417
+Lines: 11
+
+On 2008-08-01, Thien-Thi Nguyen wrote:
+
+> warriors attack, felling foe after foe,
+> few growing old til they realize: to know
+> what deceit is worth deflection;
+> such receipt reversed rejection!
+> then their heavy arms, e'er transformed to shields:
+> balanced hooked charms, ploughed deep, rich yields.
+
+Aha: the exercise for the reader is to place the parens correctly.
+Might take me a while to solve this puzzle.
+
--- /dev/null
+Return-Path: <dfgh@floppydisk.nl>
+X-Spam-Checker-Version: SpamAssassin 3.1.0 (2005-09-13) on mindcrime
+X-Spam-Level:
+Delivered-To: dfgh@floppydisk.nl
+Message-ID: <43A09C49.9040902@euler.org>
+Date: Wed, 14 Dec 2005 23:27:21 +0100
+From: Fred Flintstone <fred@euler.org>
+User-Agent: Mozilla Thunderbird 1.0.7 (X11/20051010)
+X-Accept-Language: nl-NL, nl, en
+MIME-Version: 1.0
+To: dfgh@floppydisk.nl
+List-Id: =?utf-8?q?Example_of_List_Id?=
+Subject: Re: xyz
+References: <439C1136.90504@euler.org> <4399DD94.5070309@euler.org> <20051209233303.GA13812@gauss.org> <439B41ED.2080402@euler.org> <4399DD94.5070309@euler.org> <20051209233303.GA13812@gauss.org> <439A1E03.3090604@euler.org> <20051211184308.GB13513@gauss.org>
+In-Reply-To: <20051211184308.GB13513@gauss.org>
+X-Enigmail-Version: 0.92.0.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+X-UIDL: T<?"!%LG"!cAK"!_j(#!
+Content-Length: 1879
+
+Test 123.
--- /dev/null
+From: Frodo Baggins <frodo@example.com>
+To: Bilbo Baggins <bilbo@anotherexample.com>
+Subject: Greetings from =?UTF-8?B?TG90aGzDs3JpZW4=?=
+User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO)
+Fcc: .sent
+Organization: The Fellowship of the Ring
+MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka")
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+
+Let's write some fünkÿ text
+using umlauts.
+
+Foo.
--- /dev/null
+From: =?UTF-8?B?TcO8?= <testmu@testmu.xx>
+To: Helmut =?UTF-8?B?S3LDtmdlcg==?= <hk@testmu.xxx>
+Subject: =?UTF-8?B?TW90w7ZyaGVhZA==?=
+User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO)
+References: <non-exist-01@msg.id> <non-exist-02@msg.id> <non-exist-03@msg.id> <non-exist-04@msg.id>
+1n-Reply-To: <non-exist-04@msg.id>
+MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka")
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+
+Test for issue #38, where apparently searching for accented words in subject,
+to etc. fails.
+
+What about here? Queensrÿche. Mötley Crüe.
+
+
--- /dev/null
+Path: uutiset.elisa.fi!feeder2.news.elisa.fi!feeder.erje.net!newsfeed.kamp.net!newsfeed0.kamp.net!nx02.iad01.newshosting.com!newshosting.com!post01.iad!not-for-mail
+X-newsreader: xrn 9.03-beta-14-64bit
+Sender: jimbo@lews (Jimbo Foobarcuux)
+From: jimbo@slp53.sl.home (Jimbo Foobarcuux)
+Reply-To: slp53@pacbell.net
+Subject: Re: Are writes "atomic" to readers of the file?
+Newsgroups: comp.unix.programmer
+References: <e9065dac-13c1-4103-9e31-6974ca232a89@t15g2000prt.googlegroups.com> <87hbblwelr.fsf@sapphire.mobileactivedefense.com> <pql248-4va.ln1@wilbur.25thandClement.com> <ikns6r$li3$1@Iltempo.Update.UU.SE> <8762s0jreh.fsf@sapphire.mobileactivedefense.com> <ikqqp1$jv0$1@Iltempo.Update.UU.SE> <87hbbjc5jt.fsf@sapphire.mobileactivedefense.com> <ikr0na$lru$1@Iltempo.Update.UU.SE> <tO8cp.1228$GE6.370@news.usenetserver.com> <ikr6ks$nlf$1@Iltempo.Update.UU.SE> <8ioh48-8mu.ln1@leafnode-msgid.gclare.org.uk>
+Organization: UseNetServer - www.usenetserver.com
+X-Complaints-To: abuse@usenetserver.com
+Message-ID: <oktdp.42997$Te.22361@news.usenetserver.com>
+Date: 08 Mar 2011 17:04:20 GMT
+Lines: 27
+Xref: uutiset.elisa.fi comp.unix.programmer:181736
+
+John Denver <jd@clare.See-My-Signature.invalid> writes:
+>Eric the Red wrote:
+>
+>>> There _IS_ a requirement that all reads and writes to regular files
+>>> be atomic. There is also an ordering guarantee. Any implementation
+>>> that doesn't provide both atomicity and ordering guarantees is broken.
+>>
+>> But where is it specified?
+>
+>The place where it is stated most explicitly is in XSH7 2.9.7
+>Thread Interactions with Regular File Operations:
+>
+> All of the following functions shall be atomic with respect to each
+> other in the effects specified in POSIX.1-2008 when they operate on
+> regular files or symbolic links:
+>
+> [List of functions that includes read() and write()]
+>
+> If two threads each call one of these functions, each call shall
+> either see all of the specified effects of the other call, or none
+> of them.
+>
+
+And, for the purposes of this paragraph, the two threads need not be
+part of the same process.
+
+jimbo
--- /dev/null
+Return-path: <>
+Envelope-to: peter@example.com
+Delivery-date: Fri, 11 May 2012 16:22:03 +0300
+Received: from localhost.example.com ([127.0.0.1] helo=borealis)
+ by borealis with esmtp (Exim 4.77)
+ id 1SSpnB-00038a-Ux
+ for djcb@localhost; Fri, 11 May 2012 16:21:58 +0300
+Delivered-To: peter@example.com
+From: Brian <brian@example.com>
+To: Peter <peter@example.com>
+Subject: encrypted
+User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8
+Date: Fri, 11 May 2012 16:21:42 +0300
+Message-ID: <877gwi97kp.fsf@example.com>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+ protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v1.4.12 (GNU/Linux)
+
+hQQOA1T38TPQrHD6EA//YXkUB4Dy09ngCRyHWbXmV3XBjuKTr8xrak5ML1kwurav
+gyagOHKLMU+5CKvObChiKtXhtgU0od7IC8o+ALlHevQ0XXcqNYA2KUfX8R7akq7d
+Xx9mA6D8P7Y/P8juUCLBpfrCi2GC42DtvPZSUu3bL/ctUJ3InPHIfHibKF2HMm7/
+gUHAKY8VPJF39dLP8GLcfki6qFdeWbxgtzmuyzHfCBCLnDL0J9vpEQBpGDFMcc4v
+cCbmMJaiPOmRb6U4WOuRVnuXuTztLiIn0jMslzOSFDcLTVBAsrC01r71O+XZKfN4
+mIfcpcWJYKM2NQW8Jwf+8Hr84uznBqs8uTTlrmppjkAHZGqGMjiQDxLhDVaCQzMy
+O8PSV4xT6HPlKXOwV1OLc+vm0A0RAdSBctgZg40oFn4XdB1ur8edwAkLvc0hJKaz
+gyTQiPaXm2Uh2cDeEx4xNgXmwCKasqc9jAlnDC2QwA33+pw3OqgZT5h1obn0fAeR
+mgB+iW1503DIi/96p8HLZcr2EswLEH9ViHIEaFj/vlR5BaOncsLB0SsNV/MHRvym
+Xg5GUjzPIiyBZ3KaR9OIBiZ5eXw+bSrPAo/CAs0Zwxag7W3CH//oK39Qo1GnkYpc
+4IQxhx4IwkzqtCnripltV/kfpGu0yA/OdK8lOjkUqCwvL97o73utXIxm21Zd3mEP
+/iLNrduZjMCq+goz1pDAQa9Dez6VjwRuRPTqeAac8Fx/nzrVzIoIEAt36hpuaH1l
+KpbmHpKgsUWcrE5iYT0RRlRRtRF4PfJg8PUmP1hvw8TaEmNfT+0HgzcJB/gRsVdy
+gTzkzUDzGZLhRcpmM5eW4BkuUmIO7625pM6Jd3HOGyfCGSXyEZGYYeVKzv8xbzYf
+QM6YYKooRN9Ya2jdcWguW0sCSJO/RZ9eaORpTeOba2+Fp6w5L7lga+XM9GLfgref
+Cf39XX1RsmRBsrJTw0z5COf4bT8G3/IfQP0QyKWIFITiFjGmpZhLsKQ3KT4vSe/d
+gTY1xViVhkjvMFn3cgSOSrvktQpAhsXx0IRazN0T7pTU33a5K0SrZajY9ynFDIw9
+we7XYyVwZzYEXjGih5mTH1PhWYK5fZZEKKqaz5TyYv9SeWJ+8FrHeXUKD38SQEHM
+qkpl9Iv17RF4Qy9uASWwRoobhKO+GykTaBSTyw8R8ctG/hfAlnaZxQ3TwNyHWyvU
+9SVJsp27ulv/W9MLZtGpEMK0ckAR164Vyou1KOn200BqxbC2tJpegNeD2TP5ZtdY
+HIcxkgKr0haYcDnVEf1ulSxv23pZWIexbgvVCG7dRL0eB+6O28f9CWehle10MDyM
+0AYyw8Da2cu7PONMovqt4nayScyGTacFBp7c2KXR9DGZ0mcBwOjL/mGRKcVWN3MG
+2auCrwn2KVWmKZI3Jp0T8KhfGBnFs9lUElpDTOiED1/2bKz6Yoc385QtWx99DFMZ
+IWiH5wMxkWFpzjE+GHiJ09vSbTTL4JY9eu2n5nxQmtjYMBVxQm7S7qwH
+=0Paa
+-----END PGP MESSAGE-----
+--=-=-=--
+
--- /dev/null
+Date: Thu, 31 Jul 2008 14:57:25 -0400
+From: "John Milton" <jm@example.com>
+Subject: Fere libenter homines id quod volunt credunt
+To: "Julius Caesar" <jc@example.com>
+Message-id: <3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com>
+MIME-version: 1.0
+x-label: Paradise losT
+X-keywords: john, milton
+Content-type: text/plain; charset=us-ascii
+Content-transfer-encoding: 7BIT
+Precedence: high
+
+OF Mans First Disobedience, and the Fruit
+Of that Forbidden Tree, whose mortal tast
+Brought Death into the World, and all our woe,
+With loss of Eden, till one greater Man
+Restore us, and regain the blissful Seat, [ 5 ]
+Sing Heav'nly Muse,that on the secret top
+Of Oreb, or of Sinai, didst inspire
+That Shepherd, who first taught the chosen Seed,
+In the Beginning how the Heav'ns and Earth
+Rose out of Chaos: Or if Sion Hill [ 10 ]
+Delight thee more, and Siloa's Brook that flow'd
+Fast by the Oracle of God; I thence
+Invoke thy aid to my adventrous Song,
+That with no middle flight intends to soar
+Above th' Aonian Mount, while it pursues [ 15 ]
+Things unattempted yet in Prose or Rhime.
+And chiefly Thou O Spirit, that dost prefer
+Before all Temples th' upright heart and pure,
+Instruct me, for Thou know'st; Thou from the first
+Wast present, and with mighty wings outspread [ 20 ]
+Dove-like satst brooding on the vast Abyss
+And mad'st it pregnant: What in me is dark
+Illumin, what is low raise and support;
+That to the highth of this great Argument
+I may assert Eternal Providence, [ 25 ]
+And justifie the wayes of God to men.
--- /dev/null
+From: Sitting Bull <sb@example.com>
+To: George Custer <gac@example.com>
+Subject: pics for you
+Mail-Reply-To: djcb@djcbsoftware.nl
+User-Agent: Hunkpapa/2.15.9 (Almost Unreal)
+Fcc: .sent
+MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka")
+Content-Type: multipart/mixed;
+ boundary="Multipart_Sun_Oct_17_10:37:40_2010-1"
+
+--Multipart_Sun_Oct_17_10:37:40_2010-1
+Content-Type: text/plain; charset=US-ASCII
+
+Dude! Here are some pics!
+
+
+--Multipart_Sun_Oct_17_10:37:40_2010-1
+Content-Type: image/jpeg
+Content-Disposition: inline; filename="sittingbull.jpg"
+Content-Transfer-Encoding: base64
+
+/9j/4AAQSkZJRgABAQAAAQABAAD/4QvoRXhpZgAASUkqAAgAAAAIABIBCQABAAAAAQAAABoBCQAB
+AAAASAAAABsBCQABAAAASAAAACgBCQABAAAAAgAAADEBAgAOAAAAbgAAADIBAgAUAAAAfAAAABMC
+CQABAAAAAQAAAGmHBAABAAAAkAAAAN4AAABndGh1bWIgMi4xMS4zADIwMTA6MTA6MTcgMTA6MzM6
+MzcABgAAkAcABAAAADAyMjEBkQcABAAAAAECAwAAoAcABAAAADAxMDABoAkAAQAAAAEAAAACoAkA
+AQAAAMgAAAADoAkAAQAAAGsBAAAAAAAABgADAQMAAQAAAAYAAAAaAQkAAQAAAEgAAAAbAQkAAQAA
+AEgAAAAoAQkAAQAAAAIAAAABAgQAAQAAACwBAAACAgQAAQAAALMKAAAAAAAA/9j/4AAQSkZJRgAB
+AQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwc
+KDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIy
+MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCACAAEcDASIAAhEBAxEB/8QAHwAA
+AQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIh
+MUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpT
+VFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5
+usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAA
+AAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEI
+FEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVm
+Z2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK
+0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDq77xdrX/CQ6laRXjRxQTF
+ECovA/EUg8Sa6W/5CUuP9xP8K5yWQnxjrw9Lwj9BWjkgZHFAG6mu6yV51OXP+4n/AMTUq61rBB/4
+mU2f9xP/AImsJJTuAJFW0YDnfmgCTUPFGqWFq882p3G1eyqmT/47VfRfGGpawkgGp3CyIeg2cj1+
+7XK+O7zybCGMNjzHyR6gD/69ZvgG8zqU67vvRZH4EUAesJe6m/XVLv8ANf8A4mpf7Qvl/wCX+6b6
+uP8ACs+ObKdaeh3Hg9aANTw/4gurjxTLpU7tIv2cTKzHpgkH+n5UVheHGI+KWzJwdNP/AKFRQBzD
+7f8AhMfEDEHH24j/AMdWrs0oCkDrVKJs+NfEsZ+79u/9kWrd5GqKTmgCstwwkyT0p5uzu61mOzbj
+zSFn3DmgDB8ePLPe2MEQZyykhRzk9/5Va8D6Vd2Mz3d3CYxJHiPd16+la0hhMybkUvxhj1HWr5uM
+uB0wMYoA3YJARjvV+DBPasC2lYsOuK3LVunWgCLQRj4sIPXTGP8A4/RSaF/yV2P30tv/AEOigDmY
+QD408UE9ftw/9AFXpv3iFT9Kgs4t3jXxV6C+H/oAq5cxkMcCgDFltXVyMVVv7iGwtzNcNsQfiT+F
+a8jbAxdgFAzmuTZZfEV81vG+xTyX/uIPT3P9aAIBr9vdNHcQI/lxk5DDBrfsLuK+jE0MqupPOOx9
+KzNY8L6fbaaYrdGXb3BOcnuT+FcpodzN4c8RRRyylrW4IDE9MdM/UUAes2wbOAK27PhRms6CJlwc
+VrWowRkUAV9CP/F3YffSm/8AQ6Kfo+P+FuWp9dLf/wBDooAxrH/kd/Ff/X8P/Ra1evUOcgVW01Qf
+G/izIz/py/8Aota2LqPK4xQBxniWc2mi3MxBGFA/Mgf1rmtEF/Z6HNqMNuzvPnY+7G1V6Hoe+T0r
+qfGmnT3Xhm8WNWJVQ+B/skH+lUPBt3d3PhuzXyBM6xBY0YfKDnALewxmgDE1BfEDaPaXNzMRJPIQ
++TjCgDHb69u9ZGt2Us2lrdNDtMLAgq27Kng84Fd74qnaMwWB8qWRTnzUcfePGSOx4ziuf1kzT6S9
+tuRHlVUG5sDJOMA+lAHofh5/tvh3T7k4ZnhXcfcDB/UVuRQEdqzvDelPo/hywsJGDSRRjeR0yeTj
+2ya3I8/3aAMXSU2/FmzJ/wCgbJ/6FRUunf8AJV7H/sGy/wDoQooAyNJXf448XYPS+X/0Wtb8ynyj
+0rm/DIll8W+KDKQ0pvF3FehPlr0rvINMzbfN8rsc7upH0oA5ie3mktZSI1ICn5W43e1ec6ZrDwax
+facIj9liUNtUcgE8j0IzXrHiqS20rQJbiadoyBsWQjc2T2HvXnvhbREuzeXTbvMlfILcsF6D6jFA
+GJr+pWE1ymFkwFzhlwo+i1xevazLd3Fva2+UiQhh7kdPyr0jVfA8t0BeXNybe35UK2EJAJwST/QG
+uS1Pw7HYalbKHUIxYxyDd8wHUnNAHsnhXVBrGhWkrBlmEYVww6sAATXQInA5rn/AOZtIa3mQHZI+
+xwfvAnJ6d8n9a6yazEKhlzgUAc1YAr8WbH302X/0IUU6xBPxYsSe2my/+hUUAV/Bdj5fi3xWJJDJ
+JHeopY8bj5a5OK9AUArwARXEeFjjxh4xbub5f/RYrsIZgJhGTjcuQMGgDnfHiwnw1KJoVkUuB8yg
+hfeuZ+HemTLpjx3OCZNzKUbPy54/Sut8Z263OlJE1wYgzkkjvgH86yfBb+XYWuIGiEithWzn9aAN
+loTcO0ctuGjV9oMg5JGCSOOnp9K8/wDH1qH1iERrukRAqqB3Jzj9BXpsk6F+oyCuRjJ54rhNcg+3
+Ge5XiUSL5ZGc87sdPagDQ+HlvJHoAdo9h85mUY7dK7WSRCoB6HiuV8IiW10JYs7yszDJ7fN/k1tG
+Rpb4xj7qpnj3Iwfx5oAwLMgfF+1UHI/suTH/AH3RTLJNnxltx2Olvj/vuigB3hgf8Vp4vH/T8v8A
+6AK6aRWFk2CA2CPSua8M4/4T3xcp/wCftD/45XR6q32e1JjUySCRdqA4J3HH9aAKHiJTceH4mliK
+r5e5lDfMpx2Iqp4eQR6Zp75Y4jX7xyfTn8q29djjbS/LMqxYGFdugNZWlskOh2pKgYj2AqO4OB/M
+0AW7+NLQ3Fwi/O6hsk5yRwOO3WuS1qGJtNuvN3iNJkX5e+EIxn8f1re1e4ubq8jSOMiBArZJ/wBY
+xOcfQcZ+tVNTsYh4dnjmG9PMJIP8XYUAQ20z2Hg6OeJGTYQzd+N3Le+RzXQ6TGwtjLLkuxAy3XAH
+f8Saw9Mlt7vwsI4yZI9m07xtyM/y5rqodqxIFAIx1oA5iDj4w2ZHfS3/APQjRSw8/GGzx20x/wD0
+I0UAee+I/GV/4S+IXiAWlvFKJ7gM28njC+1Ubn4v6xclC1hbAq6vwzdjn+lXviB4X1O88b6lPBYX
+EkUkgZXWJiDwPQVzH/CH61zjSbwj1EDf4UAbV78YdZvYPJbT7UA+7HP61HbfFXXLW3SFdOtSqZ67
+v8fesg+Ddbzn+yL3P/XBv8Kcvg3Xc5Oj3x/7YP8A4UAaY+KuvIQP7PtM5JXKt/jUF78Udcu7F7WS
+ytEVv4grZHPB61VPg/Ws/wDIGvs9v3T/AOFMPg7XcHOk32P+uD/4UAWLb4l6vb2zQJZ2m1gP4WGC
+FAz19q17f4va0sSobS04GB8rf41z3/CIayOuk3g/7d2/wqRfCWr8f8S27/78P/hQB33w78Q3fib4
+jR3l3HHG6WTxgR5xjOe/1oq78JvCmo6dq8+qXUBhhETQqJAVYsSDkA8496KAP//ZAP/bAEMABQME
+BAQDBQQEBAUFBQYHDAgHBwcHDwsLCQwRDxISEQ8RERMWHBcTFBoVEREYIRgaHR0fHx8TFyIkIh4k
+HB4fHv/bAEMBBQUFBwYHDggIDh4UERQeHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4e
+Hh4eHh4eHh4eHh4eHh4eHv/AABEIAWsAyAMBIgACEQEDEQH/xAAdAAABBAMBAQAAAAAAAAAAAAAH
+AwQFBgACCAEJ/8QAVhAAAQMCBAIHAwYHCwoGAgMAAQIDEQAEBQYSITFBBwgTIlFhcRSBkRUjMqGx
+0RYXQoKSssElJjNDRFJicpOi8CQ0NmNzo7PC0uEnNVNUVYMJRWR08f/EABQBAQAAAAAAAAAAAAAA
+AAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDqjNudMEyw6lvE3HEqUnUS
+kDSmeEkkCTB241Aq6X8mgSL5J/PT99DPrX3S232GwohPbp5/6v8A70PMsrSuwYKgkgtjiONB0Ovp
+lykFQLhJ8+0Fefjoyh/6/wDfFBctsJWkpt0wRzSK9ft7cwVMNmf6IoDOOmjJ5/lB/SFejpmyeT/n
+I/TFBdFqwACGGR6IFOWbRhZ3Ya349wUBgT0y5QP8qT+n/wBq3T0x5OPG8A/OFCy3tbZCRpt2j+YK
+cotLMjvWjM/1BQEs9MWTR/LUn0VWfjiyb/72hki1sw5q9kaHL6ApZDFspQHsze3DuCgI6umLJqUg
++2Eg+AJ/ZSR6aMmj+UOfon7qozjTACUhlsCP5opo402AQWkeumgIK+mrKAA0uuK8e6R+ykj03ZU5
+JePx+6h6gaTGlPHmkUwzDjlrhDKVvtIWXVaUIjj4+6gKKOm7KZUQrtU+YCj/AMteHpuyvvpbeV7l
+f9NAHEc3Ls8ed9iYZLTZIAI4+vpRAy1i7WKYS3dhtIDghQHIjiKC9npwy3O1rcH3K/6a1V034Ef4
+OyfPqlf/AE1WG1KKxAAHhFO0LgTCfhQTf47cGPCwuJ/qr/6a9R004as7YXcn0bX/ANNRLbqhsAOF
+OWnVgEnh4UEm30wWbhhvBL5e3Jpz/ppwOlMLSC3lzElT4suD/kqHN642YT9tIu3T7h2UYHnQWW36
+S9YleXcQA8Qhf7U05b6SMPXiFpYGwfZfu19myl9XZ6leEkR9dVBy7XEFXLcUN+mPEn7VnCrplZQ6
+zclaFeCgAQfqoOqbO4L6VBxvsnURrRqConcbisqo9FOYE5mw1zGENdkLllpZQSCUkApO/wCbWUAY
+63i9N0x3v5SkHy+aqj5QBXhFqRzbFWrrjrWm5a1ER7Unh/saqOSXP3Bst/4pNBbWhLY1nvCt1NyZ
+mRWtuoKESCOdKjSOB99BiEjYHYU4ZQpKpTwpArTxpVt8g7RQPmlQkpP2UoFE8VkU0buEkK4A+NbN
+rCzHDagfQCmN69TtsPjTRLoBA1b0oHFaZPwoH6IPODFJPwOG9asklJJ228a8egp9aBspcHhvQf6T
+b55eZHWVu6m2kgISDsnaT76Kt46hlpx1WyUJKj7q59xzEFXV9dXC1krccK9/M0CybtYUTrMnj50R
+uiPGLl69OHBTaWG2isgjdRkffQgFyoqO441eOiW4WM1W8D6aVBXpFAfGCVGQmnDZO07jmaYW74G0
+yKfNuJUBB50D0ISAIAiNyK3UUkQIpDtgUhAAIApNToAKQACedAotaZgCfOvQrUCAIpsVAma31kEE
+bbUG60Dwn1oW9O6S3h2HqnYvK291E1SlEkzvQr6f1qRg1gVHc3B4+lAaeq2rXkFhcz3NPwcXWU06
+pzqnchtQ4CkBQjT/AKxVZQDXrpOEXLJO0XYj+xFVDI6lHAbEeLSfsq1dd2UPWyxMG7HHhPZCqtkX
+/wAhw9Svo9gj7KC4Wx2MCKdt7ncTTaxbGmBz3p4hISNzHhQJrQSYnjXiUkQOYpZzupJJpFK5g7n3
+0ChQSkbxSluSFDfam5WACokj1NetPAqA29KCXb0HdQ9TSwQlSduVR6XBpJnetkXWgCTQSTQCRpMk
+Uk8djzpsm9ExNaOP6pOqBQRWcblNtly/eI3SwoTPiIrnbEXAl7j4UaulrEHLbJ9z2ZEuKS2fQneg
+Fevy62ASQYoHCHkqXsqrl0U3aWs52mpelLgU3v5jah20/C9zO/hVmyc8Bj+HjfvXCB9dB09bq0mD
+zqQZUkAVDtriOdO7ZzSYO8UEoHBw3r0KB3mm6XUxERWdpO0UDhJBVvSulKh9kU2bUdzvvSzKu9tI
+mgXShOjfaTQk6xZ/cOw4GLk7/m0XlqlGxEcCDzoP9Y1KhgNhI/lR/VNAWuqGpJyG3pBHdVPn84qs
+rTqgqJyKyCI+bXHp2qqygH/XoQEW9i5M6r0CJ/1VVnJKP3Aw8QP4BvYf1RVh69K1lm0BMgXyQPL5
+moPJ50YDY+TCP1RQW+yRoaC44HhSygNXe99NbW7lCUKA48K3cdgbk8KDa5IAMcKj3XiklIMTSrj0
+zCppi8STwNAo5cEo4RXtq53pn1pke8NIUR5UoydWoA+lBMdrCdjSLr5G8yfCmJdWE6ZpNx2NiZoH
+4uZjlThKitJ35TUOl4DntUlZOgoiaAe9Nl+tnDbS1QAULdlzxHgftoMLdm4VzAmKKvTsvsrywUqS
+y8y42seBBBB9xoSWTa7hVwocGmyrh7qDZpUKFXzoptG77NdoFkwiXD7uFUvCMOu7+5SxbNLccO8A
+TRd6Nsq3eB5mbVezq9l7ThsCTEUBcQvhtuKdWy0qVJ40wS4NqcNOTvA+NBKpMokRSiFKnx8opo07
+KAAYNOGwrko0DlB/JO005Y0gTNNW9pkzTm3SDJJoHIQhIlKZ50IesgFDLdgYgC74/mmjC03KaEXW
+WBTlmykH/Ox+qqgJnVAV+8S3hMbOA78fnVVlZ1Oio5BbnkHI/tVVlAOOvSIbtlSN8QSD/Y0yyayh
+3Ldg4BBNs37+6KedfERaWqo2+UE/8E1FdGdwF5Yw0kgzatmPzRQWdhnRClCTWPAEHeKkENodbSpO
+xpBy3IJJjegjFwjh6U1eWSoxw50+uGQHPE86j7qEqiY9KBstYSe6B61gfLapB5QaaPOQs77V4twR
+wNA8TcFyPKsdd25UyS6RBAivXXJSDQOm1jyinlq/CoSY22qDDh1AVI22mUqmNqCpdI+GXOZb9Ng1
+I9jZL+qNiVSAPiKiMhZQFixcHGGgkvJPaJJ4JBolsKaTdO9zdTcKV4CdqalLSrtevcQEkeVA7ytl
+/CcOm7sLVKFugb+VSt06k3WgfSSO9HKss30NsagAkAd0GminCXSr8onegfNKSDO/nJp22tCSDUYF
+xEJpVDkkwDPrQTTCoGxBmnjDp4KG9QVu4rVxjepS3WY1KVPpQSbSiT508tBvBImo9hJICt96f2yS
+TwIigkWht6UHes8ojK9mJE+2Dj/UVRhaEpn7aDnWhH71LP8A/uj9VVATupmZyCiePzn/ABVVledT
+OfxfszG6XYj/AGyqygHfX3A+TrPmTiKZ3/1JqvdFX+h+EqPO2TU91+NrK1Mf/sER/YmoLopSpWR8
+II/9sPtNARbNSdAKdzHCtnY0Eq4zypnbFaEAqImnRUFt6p93OgjbyEEn6qhrxQUo+FTGIp7m23rU
+HeGKBi6E6orRcRtWyiSoxWp4ERQeoAI3NeLSnnW7Q2341s5skcOMUCSEpMk08to0gA/Gmu4HCvW1
+qoPMQWtOJIDR/I3HiKyycacfU4o86Y4qpxBcutiUphIpLLy1LGtSN1RsTwoLS0SQpx3uoG4HlSHa
+gq1JMSa9ullVulJAA5+dN0wYiKCQbcJ+lNLoVzmmjKoEmndudSoVAHpQOrbVq3mIqWtYME7Co9oE
+xHAeFPWVqCQY250EsyuBsSPWn7C1CI4+tRdq4F7gjzFSduoCBHoKCRYUYAG1CHrQJjKFqqB/nqf1
+FUWbck96NO3jNCXrPmMm23Ha9R+oqgJPUvM9H6PLtR/vTWVt1MkoT0eMaQAVpdUYPPtSPurKAb9f
+ZQNkwnf/AMwR/wAA1GdD7QOQMHUeduPtNSfX3H7n2y4mcSSPT5g1G9D69XR9gxCeFvHHzNBczGkR
+tWqXIBjlW4HdO0U1XqQsGOe9ApcAKTHEGoe7tyJqWWSBv61roQ6nvCgrD7cEyPqpNCYn0qavrMhW
+w5U1NsYAAoGLAEmaUcAinItSZ8R4VjjJ0Qo+tBHK+NeoI0kVXs3ZptMIS42wk3FwB9EGAPU0KMWz
+zma6uCtm67BoHZLSQEx6nc0BdzRfBq0UhKu8BwFI4BchttK1EyrlPCqjhmJOXlrbquHUPOrbBJJ5
+8+FaXC32NTguLi2MmCYU2fLjPxoCel0vDUFFQPCnSAQaH+Vc625fRhuKpbtX+CHAruL+6iC0QtKV
+JUFA8CKByyFE8KeI2CTzpG2E8h8acpQSRCRH20DxlRMGPqp2zOkJB2prZtlRIg/Gn7bSUwNW9A+t
+BAO4B8Kk7UeJmo20SUkmeHlUnbK2G1A+aiZjfxoS9Z8k5Ltwrleo4f1VUV0d6IG/OhR1mkg5HZkz
+/lqP1VUBI6l5H4vmRJOzvHl86aykOpOf3iEb/Sdj+0rKCgdfUj5NtxzGJI9/zBqP6Gkj8XuDDn2H
+/Malev6lHybakQD8oNg7f6hVMehVIV0dYMqJhj/mNBcm29Q24U3u2gDtUohvS2dtzTO6H+IoI93d
+PmBTcq0xBmnDoM7b7U1KZUQCBvQLKcQtuFRSCwAmBFIvBxJ2MGaVAUUhQIngaBNUg7kVV81YoUhV
+rbuweCiOPpUxmC9Rh9it5Su+dkDmTQ9Nz2q1PrO5JCd5JNAxxyztBaOOXyktsNp1rUefqaH7dpc5
+hvlDCLJabVJgOLG5H2D0q7YhhV5nDFrfBLYrTbA9reOAwAkHYep/YKL+X8qWGHYe1aWjCUBtMDQn
+c+cmgDtvlq7scEQ2grTdtnVqKdhVXx3EscYTpumoIkdogbKHmDXRWJ2NotlxpbjRUOYVJ9N6G+bc
+Mt3EdghaFx9IAUAXfxB25bIe0haOHd+qiR0QZ1dRdIwXFXdbayEsOqP0T/NNU/MuXH7Qret0KKUn
+dIE/CoCxUW3UnfcyCOINB2BaiRI8Nop+0gxP21R+iPHF45gCUPq1XVsezd347bGr8wlQERQLWp0r
+Gw41IoAKp2k0xQkg/Qnwp9bSYJSZFA7YSfjUhbN6uMAimTBIUCakbZW44UD1hkJAoUdZ9pKMgNqH
+H25H6qqLTR7oPOhZ1ngD0dSOIvGz9SqC4dSZJGQyeRU7H6YrK36lBH4vk7flvQfzxWUFE6/oBwm1
+Vz+UWh7+wVTboPSR0cYKkCf8nk/pGpHr/o/e9aLjf5SbH+4XTPoOUk9G2CyCT7PwA/pGgvaR3eAN
+M71E8qkkJBRsDvTa7QNBFBCuoHGKaqbGuZEU/udIERvTbUmSNO9A0ukjSTG9Nm1BC5PDnUg8pIHC
+ozEn0tMLcCeCSfqoB9nrElXOLi2bJ0IOkeE86rVzcBDKnAoBLRgCNyfL/HjXmO4koPaoPaOEmf5s
+86aYHbPY7mmywdk6mmyHbjwKZkz60Bl6NcGRhWX0XDrWm6vPnnIHeAPAe4VZx2iEFSVrKYgpI0/X
+UJeYuzhrae0Qt9SANmwdIH7ajh0j4WrW204dYEEE8NqCXW6+7dP2yUMNnTKdZJ24cao2ZGCVO9ol
+tzcmUnh/j7qXTm3tbC8xdcKQnsWRJ7pIUoq+qKqeMZ3YUp9xxxsJcUCQngOJ2+NAmptDxKHEqSCN
+vGhnnCxOHY8sISEpe76YECecfb76J2GXDV8wLj2a4DDhlDikEA+h8Kr/AEk4St7Chdtd4sHUI4xz
+Hw391At0JY38mZntmnHdLV38y4nlP5J/x4102ywVAEbiuLMDfcYeauGjpUlQUDHMGu1MpXPyll+x
+vkbh5hKifOKBZtuCRoiKWbQoGYpx2KkmYO/lSiGjI2EeYoMaQSSDTplJA2rxtJG1OmUgD/tQKM6h
+50MOsv3ujlwgzF02ftopJMAAxt5ULeslKujq5I/9w2frNBb+pM6Dkfsuep8n9NH31lJ9SUfvOBgb
+F/cf10VlBVuv7q/B61g7fKTW3/0Lpn0IjT0d4IN/82HPzNO+v8Vfg/apkR8osn/crpr0HEno5wUE
+cLeJnzNARGSCmCD8ab3IBkAcqctjbhwrV1oqTMAUEBdpABnemRAHvqYu7WASQDNMOw7xAKfHjQMn
+YjvDaq/m64RaYFevcNLSj9VWp+3IEyKo3S8U2+RcSXIkthI9SQKAKXV4u5ue21BLY3JngImrb0S2
+TrmCXuYVuKb9pfUkr0nZpA4Ty3maHDSXsQLOHW/8PdOJZbHLvH7AAfjXUOW8rWWGZbssMTZtutMt
+BGpbQMniVRE7mTQDzMfSJctYXcpwbLq30MoSFP3KinXO3cQBJ9SRQpu/lvE8WS+jCQXXVAyylaUu
+T68TXQubsNNpZK1YwLVsDV3mSSPSFCoPJeTH3rv5dvO2UiYtu1RpKxH0o4xvzNBHXuX0WnRpcGzQ
+supWlSyqZMjccfGN6FGG2+Iv4u5cW+ALxNLSoaZUfmwoc1Abq9Jrq3GsPtrbI12gcFNHaI3oJW9q
+7Z9qbEpSpapg+NA2ezHnJ4rZxLCrZLVshHYWzdutBXPECCoAjzqRtlKxNpxp22U0tSTLS4kCPXen
++XLW5xR2EN3iXE/SCmwUj3wD9tTSsHLPdUtK1cYiaDnq1bXa4jcWLuy23FAe7b7q616v1yb/ACFa
+JMyySg78INc19IViLHNAuG0hPa94jzTxo79Vq/DtjiWHDcJWHk+iuXxFAXrls7xO1etJkCCaevNy
+NXOvGGxG6QDQaIbM8AactohMRWyWyD605aSInwoGwTIgCD40MOsa3/4cXh8Hm/1qLSkhMqNDHrHN
+T0Y38Dg43+sKCd6k4H4EHxDj36yKyvOpTIyZpJPF79dFZQVLr/T+Dtt4DEmf+Cuk+g1uejfAzEk2
+wn4ml+v8EjK9sYknE2Y8vmXKT6BQ4OjTAyT/ACfh+caAiBoDYCaxxOlAGnjSralQJg7V4+sdmBtQ
+Rd2gaZM7nwqLdShCzE1NXS2wmJmot5CSdSQregY3KthE+kUNOnjtF5Hf7PVHatlfoFCim+hOkwDI
+86p3SRhyr/Kt/apaKipklIHGRuKDmjKV81ZZuwi6cWSlNzpE8BtE/EiuqLbMPtFi2WVDVA2rjXEy
+5bX1skApW2AQPA6ia6awJQNvavpSZWgKI5EEUBLwmwbxFSHMQYZd098FSQQD761GMWuK45eYPhYT
+cKsUJNw4ncIKphI84BJqs5hzKcPy4+q0lVypISNIkk8gPEkmKnsuZCvMHyEi3sMQFnmC6V7Re3Rb
+16nFcUnyTwHp50Epmxm3ssnrYeUe0uEHSIoIYg0cKS9cOW5Uw1ClqHEJ5n1q59JWKYhh7tthN0+u
+7et2NetKY1pHExyoft4lfYnir1u6sPWL+mUqT9HxE+HxoLhaXS2sND9k5rbdROpJ2INa212OzK1L
+nn3qhMtMPYU67hThKmEElon+YeHw4e6nFxPblQBKPyY/x5UAt6VLwPZoba3DYbn3knf6qLXVGbcV
+e4u8QezS02kEjmSTFDHNuXcRxnNDKrG3U4SnRJMSZ/711B0I5OcyhktuxvA37a4suPqRuJ5CecCg
+ujoEQfCtmOI2rZ0d2vEpEbcKBy2AVcvfS6UgbDnTNJPnSrRK5UmRQKqC9ESDvyoY9Y4q/FlfiQfn
+G/1hRPWogaYihh1joPRhfEj+Mb/WFBMdSon8DgDyL8emtFZWvUnCRlFYSRIL0+utP7IrKCr9f3/R
+q2PH90mR/uV170EKKejLAxAP+TbfpGvOv+CMsWpPPEmY/sV1r0Bk/izwPcx7Od/zjQEhlRUNk+6l
+XEymY5UmyDp4x50uggpHemgj3mZSSRA8aadmEqAVFS9yfmzHhUU8UzvyNB4tpBG4NMLy2YeZWFN8
+QeNPA4QdMkT9VIvOHSUhQ3oORulXJl7a5tvbq2tyq3U4FIQBvBAO310bMBsQcKw+4ZIIS2kKSR5V
+KZ9sPaWm1WqW37i2lSxH0gBBFUTLeamLW+Tg97cBt9StSEcAB/N9aAws4LgLCbTEbrSi3aWLo61d
+1Kk7ifQ7+6vXeke2uLdCsvYLf46tThSk2zKuzBH85wjSBVeTeu3rCMPNom/tnFauyUdKfzvETyqb
+fubpFs2wthtCUCENsSAPACKCk50xPMvt72L4nkt4Xa2SygtuIUkIIjeFHkfKh5huMWdk+W73Bb2y
+UDu6WSpER4iYq45rwS9duV3Nz7Ssap+ceJ5cAJquM9o2QEIJ7wkKPCgnsHxCzxa1F0w6haSkplJ5
+UjdH5tdwe4gCRJj/ABzpo0lq3X2rbKGVLBBKe7J9OdUnpQzULWx+R7R3/KHk6VkH6CefvNBMdEmc
+13PSMm1u3W12C3VBgFIlJ8Z84+uuvLIzaoKtyRXzpy7fXFjcpWypTa5ACk7Eedd99HmJt4zlHDbx
+D6XlKYSFrTwKgN/roJ1xHdFY2J3mt3EmIrUCO7FAqhAVsRtThIAiCD5UigGCZO1bpSqNiaBR3SpH
+CCKFvWNI/Fne7TLjY/vUTl6tJoXdY0KPRnfGP41sf3qCb6lP+h7h0gDW8J/PTWVp1Ilzk59E8HXf
+tRWUFX6/5P4NWoKhHyizt/8AS5SnV+bDnRpgaU7xb8PzjSH/AOQNUZfsk+OINH/dOUr1eFhHRlgg
+3nsJHl3lUBONvGxTBitUthPGZpVLx3kztz51p2xJ2igTuWu6ajLhvjvUs8XltEttqWI3IrZGFJWl
+K7l4hSk6uz578KCtLQrXEz4RUknAHT2a1uaFkayI29KnWcIsrZ/tN1rSJgqmo1u5vLpt1p1pTUPE
+srk7jjvQBvH04hhuarxzELlaGn0Q2hHBJ3gTXOefrwt5oadUl1p9h4HQvYxqkGuoOmF+1U0bJ25Z
+aeWsKMqEq8QJrm7F8PcxfPTLKy12LSzD6mtaFJBkBQHHwoDBaYld4c004SpbRAKVDiAeVWFrPNi1
+aAugrlOgEHcbc6jsv263sBZtbhse0sICXE6eGw5elRSsJtA4/rag8Y+6gaZqzheXlyvsSWrdHdSh
+KREcKgm8bt0IlxYTGxkQaZZvtW14u0D80yGwVRtJBqCxRy2ClOaU7bJkzvQSmI5mff2YgNjYeJNC
+/MTy38auHnFd7VzNWW9uDDVtbgLed2QmOE8SagMesfZr0NKJU5pBVvxJoNMLt3HvyoI70zw3/wC9
+dP8AQRnVzD7K3wm5uGlWSTpSeaT4ek0CMm4GL68asw65LpCNCB4kcffRz6JMAbX0iXOEBxsJsWUO
+IhI0qBH+PfQdDsrQ4RpcSraYml9AUkq2EVDW2GO2OKe0hsr1JIUUq2I9KmEPs6ezSpJWRtNAow2C
+jma2TqA7vCvGFOCQpPpW5PmRHGg8UCUgn30LusgI6Mr8CZ7Rs/3hRWOyBznwoXdZRMdGV/wMuNfr
+UDvqQz+CNwTw7Z0Ae9FZXvUiH70LiOAedjb/AGdZQC/riY9cZgybaXdx2QWnEGUKDaFIAUGV6tlC
+eYq1dX1BPRrgRHO3/wCY1WuubhNnhOUrC1sX3LhtF61LjiYUo9k5JMVaur4kDoxwLf8Ak5P95VAT
+A0rmB8a2w+1L9ylomATvHhSrTetMJBJPKp7B8P7BsFMdqrdRPKg8xK0Qzh6bdlYbkiSBvE71AnCc
+TxHGRcqAtrdteyie84ANtuQq6KZSmCoaz4mtVpII22oIe2wq2t2tMOOL3lajJ3qDzc37Jhb2lSkF
+YgL/AJm3GrhpUkn3VSOljMWDZawBy5xd4gOgpbQlGpSj5Cg59zRaP67w35beeZAUwp8gl1JO59QK
+p/RVgS2XL/GL5am+3LyWg2jVI4QBB47/AAqbxvNb2a1vMYQ+i11KSEQ0QVeEq4/DzqwdGjLlvlkL
+unHEPquH9bkAqRGqVAcyDvHlQXHEMETbu219aJKA+yhtxB5EDYn7KrGYLY21yEOoLayCFBSYNF+8
+w4sXFu+EqubNTRVp4qVpbBA8NzJk1TukZeMu4fdN4ZgaMTcaLIDIZDim9X0zMgwPf6UHP2e03Tt/
+bN2rS3HFpACW06lKPkBxppYdGmasQR7RiCRhtvpK4cV84QP6PL311ecm2jFs0xgybSxu1sIcDxZk
+lJ4jYgmD9tVvFsoWiQ45mDHVvW8QvfsEQDChtufpJI35Gg57Tl/C7ZDjdtcsWz7SE9s65K3EhXPh
+z4eFVvNtrg15b9ph63W32nEMtoKCS8Oa1K4DyFdE57s8k4060zqQbi2SG0sNgw547jYgcffQr6RL
+Sywu6ZFtbN2zCXO0LaR/CECdz/jlQVnLWH3RxRoWoLEaVLUtUwf5225H30XeifGlYJnO0tn0ds3f
+/MG6UgJgkyD6HhVUyW3b3l+wEpSxcusjSDJC522+HD0qy4Thq8VzrY2QcKmRqaeCO4oECAoBQmAR
+QdTWTYCAVLCieFe3WFWV4pKnWh2iFSlSTBFQfR85eHD3MOxLvXlkoNqWf4xP5KvfVwZbIIOxmgi1
+4elgSkrIPDypqtlQE1ZnW0qbg7yKjLxlTKVAJGmOJoItepKR3gDFC/rJKJ6Mr2AdJcb3P9aig4VE
+bDj4UMOsekp6ML2SILrUfpUEj1JmyjJrytiFOOnj5oH7KyvepPIyfcTzccP95IrKAb9dN9p7J1gb
+a+urtkXjPefIKpDTgIMbE93iKtHV1LKujjL5fC+z9mIMeSlVS+tZh2L2HRrZt47Zi1vTiTQKO11w
+OzcgzJnh4mip1YcNb/FTgDrqQrVbE7jh31UBOsLcdhrbZ0JiEDnUvbM9miIk86SsdK3VqaVqbTtE
+bTzp4J5UGq0kxArRxsngDTkkgbcBWizwmRQNXGjvFBHrLZYxXHUYabJhbzDetC0gx3jEb8uHOjq4
+ElMzNUHpcfScEFmq5RbofVC1qXpgDfY+M0HI/YYtl1a3bmyt7C3LnZ26zHarIMaoEmOfhRKyM8nE
+7Fm4tihLaFKWHNMBKtCiVkb89yKHXSo7a2+IlouqKWkKTDjsjURsB5wZnzq+9ByG2cCtmlKI1rB4
+mSSOBI5GfhQG51SMLwhd9d3wWtNqhaU6YTAG+mPEmn2AN2tvZm9cCW37sh1ZPiYAHwgU6Yw9rEcv
+27ToAOgBJT4D9hpZGHNuhDK4T2YiOQoEsVtnlLtvZ7QOuIUdLmqNCSNxHP0oZ490dXl5jryPlJ9F
+m6kOOslZWFKPE7kxwG1EZnGGnMSvLNKoatEEvOzEE8BNKNOYa7iSk2zrbtwWwpxSSVGOUnnQU+wy
+Rg+HIQpNqhSkc1JGxoE9OeErv82Iw9huO9Ko2CUwkn7a6rv7VTiDpMeBP21zn0j26lZuvLy3e1Ia
+CVSonhI24eCaCjWtk0xcW5xC8RbtWoCmm0nvEJMgGOG9WHo4dxRzOthdXGHutoQ+oF7V3QF8IJ3P
+ED31EvM/InbEqZccvHNRURqCWwIjf3mpfJ79xdZywtCriXC+lxSNR7yAdyYMAbbCg6WbZXa4na4m
+2SEqhm4HLSfon3GPjVub2G1QVtbC6wxbau6FoInwqTwl0uWqAsy433HPUUEnJg7CKbvoC0qB4U4k
+cfqpFwlSjHKgg7pstrIKNPhAoSdZhSm+jW4kDvXLSfrP3Uar5jtGpmKB3WkXp6O3GuZu2p+ugn+p
+aQrJTp0gaXHUkxx7yT+2sr3qWQMjL8S69z/pJrKCqdfhR/B21bIgG8ZIMcfm3Pvq79WMA9DeXdWw
+9nV8A4qqf19Qn8F7NYBKxetD3FDn3Vaeq68X+iDA0L/ikKSNuI1E/toDCNKW+7tXjbknTMKB3pNR
+BRusSRwpG1bQlZdTqle5M0D8pIRIMzWi0k7kVs26VARuOHpWy9kydtqBvyAoR9PDdy6W0NBOlLJV
+KuAMnf3UXlQTx40JOmrHLe1vfYLllLluGPnSeRPn8KAA4sxhGY0IwXEbdAcaKSxfJRpVpJAjVzTx
+MHwFGPLGXLXA+yw6zh1lgobSswZKYG9B3GUWzCEXdmXHGFPoShKR/Bq8FetHnJTbjlip1/6Yuz9L
+YjvJP7aC9ZYYPyUyoOiQkApSQUp8hG23Davc2JxC3wi7fwlkvXvZHs0AgFR8idp8JpbKaAjCGwmS
+EqKQeaoP0jtxPH31LPJ7VlSRsY29aClZTwFdnkq3axBhab19IuL8FYUpxziQo8Dvy4VL4Szh7XaN
+WduGlIjVCCBvy4fZUi2T2imjsJmDSVuy6h59bjoUCrupH5I8KBtjTybTCLl8qCSlohJP84jb6656
+zk0FX+IS0pbhW2lKUnc7EzRw6QHpwL2clZ7d1KShIOpQG8SOA2Ek+nOg3iuFrxBV+Ge0S4HHXAAd
++4Dtz5GgodlhTuLOXV9blt+3t1qTcIKtK2FA8DOxnl76neizALh3NTd4ppRJCVIIJ+jJ+HCvcq4h
+8nZdRamwLpecU4tLg1BaCd9W3Ab8TPCp/oVxC3s80Iw95d2lVwSlhLoEJgcoHODQdA2Dei1QmDJG
+9LJZ7N4vN7H8pP8AOrGNSedOAQU7+NB6l1KyIO8SKb3L4QstEwTBn1P214NFvreIJK+fgPCoyzWi
+/eXfPhaQh5SG0qVACdoMefH30Ek+4lCi2dwQTx50Detesfi6J4TeND6lUYbgqf7VsK0KSe799BDr
+VOLPR8pCjwvWgY9FUFr6k8fgSuP/AFHj/eR91ZXvUm/0GV/Xe/XTWUFc695P4NtJO6ResHj/AKty
+rb1YRo6IMvAAd5lZ9/aKqqdexKjgCIE/5Vbn+45Vs6ti9PRLl0Dj7Or/AIi6AqO6QtC1qjkBHE1o
+HCEKCdlQSJ4DwmtL3UGwtKgkpM1s22OzCn1pJMcOFBtgq1rtRJKlT3jBAJ8p4ipIokAz7qq+FYnj
+D+OutrtbVOFto2eQ4dQVJATEQdoPvqypdA3kEcaDCxJ5jehL0k4JbX+aF2D4Kn7tslsaZ2jaD4yf
+qovJeBA9aGGbcYUxmS4XcLW2GwopMDToHL12nagDeMWrOQ8Ut38Te+bfeShTDYSrWUwZUOW870UM
+MxBD2EX17bgFKrp4twdjCEkfZQn6VMHxDMuZrB/CXbd3tnA0u2dX88gkzqG26efjtRiwWy7DLhY0
+gr9ofQTp3nSdx+jQXTLLxUxdJVGpL6jtMQrcRPkR75qQQ+kKUkq3mq1ktSoeSlBQkttqEj6RKASf
+MST7wamXwWX+2EQdlDwoFApS7wmBoB2ptbG6TdvOOvsG3OzTSEFOnfz4+tN7fEW3seXZNmS0NawP
+yR51o9d3Vzj6mBZhq3aSfnCoFThkbwOAoIHON2HsXtbALWgBQVISd1KJgTwiEmR6VV8rgqexG9AB
+0WjzgJG0mpa5Wm4zViN8hQcRaoXJ0gAdmgAAEHeFFcnjO3KkujlTSbK+uX/4MMLCpH5Ox4e80Alv
+7O89rtLK0tlXK1EgISqEK27yp2HAbDyqU6MsLXhucEqWyHHGnNTbhSJIVPCCY9Kl8CtsYvbxzFuz
+bu7dLmi3t1PFGpO41pjhyEceNPMpl+zzkwzeKLr2IK+ilOlCAlJIgevPnQGFNwrsCofSArXA7x29
+Sokd1KiJ9K2QzpSNUwRxpLB22MPs3xqVHaqWeZMkmBQa4u+3fONWdqVrLdylL+gwWiBqTPiOHuNM
+ct3D14L9zswi1RcuIb1cVadp9Nqe4241h6HMQbMLQytao4KhOxPjwpnktl1rKNqHge2cRrcP9JW5
++2gmrRsItgTBJ8KA3WzRoyUFJ4KvG5+CqP6GyGUgAcKBnWzQT0epIH8ub+xVBYepbIyQ2AQR8/Pl
+84Kyk+pPq/A10HcBbo9O8jasoILr1ScBRBgh63j4O1YerOo/ilwHURs0sbf7VdV/r0j9wkqn8u3+
+1yrB1Z5X0TYFwjslgf2q6Aq3X+bSPCeNIYchNywQ5JTBETtTi4SVWy4HLamuEhSElJM78qB1CGG2
+7VpCUpnvnkkePrUQ/cXjOYmbFAUbd4FSf6McR9dSmINLUJCCptB1KTzX4D0mqhmPH3MvZgZvbxtR
+sy+1bKITMLcCt/iAKAjhsJQIiYoW9Ilp+6V44yg3DhQIQ2qTJ2I08440TWHw/bJdSkiUzvQ2xxdy
+xib7rDalvF8nYctXCaAMYfjWJp6Q8Js8SY1D2xBCwkoIIOwE8eKZo34OV+zW0pK+1vS4QFcAttdD
+XGMYwu66SLG4xKzadvWLhDbLikFKgeB9QJPwotYez2YwxI4FDK9/zx+2gTynCb20WVhRfsQAmBKA
+hRmY33Kvqqw4mlCLRxxwEpSJgbk1XsHhl7D5b0Bu5fZU6Y4EylBnfcmdvDzq4ONpXG21AN8i4ZmH
+D845gfxLsBZ3LiV2znaa3FbniD9EAbRw22qw2bTFniV865dXLj4BUpK1Hs+E90UviDqm8UJgCFAH
+zBH3iorPVwm1wdy5ceLZdSGUKClJAUo7GR/jhQVxntG8v4ze3LTSHnWtB0DYFaiYnme9x58aQwNt
+bWT8XdSrQPZwkq8CZn6op1i/zOT20JVPbuBSTxkAEj7BS+VOweyhiDlwvTbuFxKlEcEgQf20AJuF
+Y1eW9vb2jtxpYK3FqZdG0ERPDxiiV0WYWu8xNGP3ilpdb7gSsGCqIkVBXgwDCMFQ40t5Srx5QZd1
+hSVRAkDkB4bVa+iS+YcuHMMR2kBHtACySdzvx5cKAj3t8pi1WUtqWoJkAVC/L9naWKbh1Sbi8WUM
+9i2QopWQYkDcDjvUvi94zZYY/dOphttsqJAnYChDlN22ds14xiS/ZXrm+cuWme00rUClPZpUBvJA
+n86gvrz1zieBui4T2NzeOIY7IHZvUdwPQA1dLVkW7DbKYAQkCqlhlstzMOCWOgDskru3gTMHTpAJ
+9VH4VeFp+cMAbeNBouNEBRG24oJdbFH/AIcpM8b5refJVG90d0bbmgp1sEk9Hze8RfN7fmqoJTqU
+6fwKfE97tndp80VlJdSrbLFyJnvu/aisoIrrzpP4OoVy7S3+1ypjqvuT0SYIkk7B0f71dRnXlSDl
+hKo4Lt/f3nKfdV0a+inByPyQ6D69qugM6BLShHEUyw7a4dQDMHantuklBHKmjbfY3ijBAO9BviT7
+bPdeMJcQQY/Z9dQV6S4n2e+CXgp5tbIUmSUpgyfMGneeGFuYCt5DikKZUlyQJ2Bk+u01st1tWYbS
+zCNZuGlKkD+DSIG/hMigstuEhlO0gjaqRmppJvXvY3GW7pSiYdMJkDjPjV8Q2EtgDkIqi52tmlG4
+L3dE90zxJHDyoAnjDdwzm21uMX7uKXVwA2yIKUJTxWFDZRO3186NzjqWbXCVE8EIT8Ck/fQlunm3
+834azcWVtqt3VJQtLkqEjf7d/Si9i7KU4HaPJ4MupmPAgj7qBtdtqRbX51hPs16h8o2GuTATJ9QR
+5gVamHSS2VRCk+FRl9bj2m7HY9qLi2JCAY1KA2G+wM86c2F12+CW7y161pQNStpkcZjnQNcetwhZ
+dA7y1JHHjVTz5cLdcw2wYQHpd1OTuECISvbeZkDx34xVhz1iAscKNyYKUd4jVHI0PsOujfZgaxC5
+uVIbZSFFtPNOkOBRj1IA8vSgks4OMpNtZtAFthWohJHArA4egVTjCbFf4txag6XHWCtfA7rOoj6z
+UFfF66XcXUavablQSTP0Q2qOJ/pD3ir4y2G8Hbti1rBbCVDgIAoOfsaxFOG481hC3LZ21QrtWm1t
+hayrgrSeRIMRwgUR+ibCWkdpj3cC32g2hCTPZpHI+e31VRs2ZYtHLxePKJUwpxTSWUcdYPDUOCSI
+38iKu/RG4ub+xDehhhSVISeI1DfhtxFARnG27izW28gKQRBBHEUEsFwkXnTJcYeo6rSwWl9CI4bC
+J8d/so3gAtQKpOG4a3adIuL4i0nvOWaJ24qkxQWzKzYdxbEsVIOnULdonwTx+s/VU8rvKK5nwApD
+CrX2TDmrfYECVkCJJ3P106aSJ8qBEqIAEDhQZ61xB6PmduF83+qujY62nwoJ9a8j8XzQif8ALkfq
+roH3UsP72LoT/GO7e9FZWvUtIGWn0wZK3if0kVlAy68IJyxO8Tb/AK6/vpfqquBPRfhQJjvuiDz+
+cVSXXg3yuE+HYH++uvOqopA6M7DWB3XHY/TNAemBKCBSdy2lDqSY350lZ3KdxIn7a0xJ0ltCkmCn
+eg0x1CXMJfbIiUECee1UToGzLe461jFtibRVcWF4WUPkfwiOQnxEVaMRxGbRYWZEGo7oktLS0yym
+5YA1XTzjyz4kqNBeytU+NVvMFlbYip5t5TqVEBIUhUaTyPhU4t4p4c6qGbHL9DinLI/OIlQSTHLj
+QDfOeCsYNmBDlpZEvKR2i7kypSjqg78Ad55UUFLL2WlIUkmWwRtPDehPjeM3t04W7nuBtxsFA2JK
+l/ZANF7C1JVh7bSu8kpjc0Gl1iLbVrh12tQRKkoJJ2hXdH1kVtgbgFvdWyne1LbihqkHUPHbn4+d
+QePYbcYng7Fqw5p7J4FSvNCpA+IFLNXTeFIxS+dSUKTal5QJ2UUpMke/9njQD7NmNX+dM7KyvhyU
+2+HWw7R+4c37VKFaVCBwEggTE0/dbfQWbewabsy6pWs9nJDadjII2Jnlt4VZzY2eH4cvFbawtkYl
+fNN+0PBAG8TJ99Nra0Q7jTSyhLa3GktrK3ZEDcpTzJkn4cqDW8swi5wu23AbaU85pHFS1AD6pq2L
+HzACRAAqKdQh7FiscilA9E//AO1KuL1IMcAKATZrFthgdwnEb1NjhhJdU+133DqV3U6dJj135VNZ
+CxrK7PZ4Xg945erMBb3YmSTJ75gR68KpvSFdtXV3ibz60e0W1ou5LSRsEAqS2k+oMnwIqMyLauYM
+3heI3lk9bKfulWt20pWkgLIgn3EEUHQEd07kCq1lwPO5qxRTgGhCkBJ8QBP2mpe2eXZdnaP6ltaQ
+lp8qmT/NV4Hz5+tQVveLtMcu9AkuOoB9KAgNqUobEA1oHFa9JFJ27mod0+tK7SFzvzoN1lWmZ4UE
++taf3gsTzvkAforo0uujSQN6B/WqcKskWyRzvk/qroJfqYT+DTuw2U9+sisrfqZD97D3kp2fXUis
+oGHXc3y3Eb6WD/vF1TOr7mvCMJyFbWt3i1nbPJdc1IceCSJVI2NXTrrtzgIUebbH1OL++uNnEkHa
+RvQd42PSFlkAFWP4bI//AJKPvp09n/La298cw0+l0j764C7yTxPrXhKualUHcGN51wJVo6pvGcOJ
+0HhcI++k+jDOGCYZlJi1uscw8LC1qANygEBSiY4+dcSHUocTvXg17gK4UH0Jts/ZXUolzMOGJA8b
+pG/11Vcx59y6rGJbx2xcbKIMXSAkHj41xBqXEaj8a1UTO5NB1FjucsuqddGH3tj3HWe0V24AUe/M
+c1RIPGN6I+HdImVm7VtCswYYCEj+Up++uFIJ8ayFfzj4bUHdFp0j5TQ++hWYcNSgr1g+0p58ai84
+dIeUVWi1s47h90lbKmXbcXAhaVbevjw8fSuLAFR9I153juSZoO2nek3J5Nuk5isAlpIIT2oIGxHv
+2Net9KOUWFBacdwwlcqWQ6JnhwiuJAFExJisg8N6Dta16UMn9sFuZhsATqO7nifup4elTJn5OZLD
++1rhzSrzrIJTtsaDoPpMz1gb+NG7sL21uBd4Y7au6FA7laon0mansdz9lrEsh2S14tZ+2pQytbYW
+NWtIAPv2rl7SRsTNbBJ8aDtW36UMn3Fg2HcespU2nUC5wMVBPdIWWk4s1+7to4nWklwrHAHnXJAC
+toJrfccKDuWz6UsohO+YLEf/AGU5b6UMnkE/hDYE/wC0rhi1WG7hC3UFxCFAlMxqHhNSPttq46Sq
+1S2kuqXAPAEyEjyHCg7Vd6T8omNOYLDf/WULOsDmzBMeyqzbYdidtdOi7SvQ2qSBpUJ+uufFLK3V
+rQAlKjIA5DwpdqeZJoOu+pjtlh8Rtqd/WTWUr1N2+zyo54rLiuP9NI/ZWUDLrop1YAgaTu21B5fw
+iv8AtXHrzMmNJ25iuzuuIytzLgUB3UtN/Hta5DW1J+jQRBZAMwTWBid441KlqTFZ2IkkpE0EZ7OJ
+3FeKY7xhMVK9l4CvOy24cPKgiCx5D41qWoMQRUuWSREDjNeFkapIoEEi0Fro1Q4EJAIRzBnf69/S
+lXrlpSrwtdnp1ksgtjgVSeXhtXpYT/NE16GeGw28qDxm4bLlmXez0hep8dn4K25eG1aBVsm00KIL
+obUmQjiZOx90QeVO2WmFFKFMjUTx1QPftTj2FggkBgDURBe3+ygi7z2Zxp/sVJSlTiS03o3QnfaY
+9PXjSeG+zNsuJfSky82RIJOkTq5c5G3OpP2JorCAlkd0GS5sd/t+6tnbW3aX2iW0KSFboS7Mj4UE
+Y8m2DKezUndrTu3BCtczw8K2U1a+03L6XG9Dpc7NGg93+by293hSzrLanCW29KTwSTMV4liOVBHP
+WiUL0ocS5wkpBiffSaWNztvUr2G86a9DMDYR40EX2BE6RNbJYPGNvGpMMA8q9LJ8B8KBghnfhSoa
+QdwmDTzsiTukVuGjEQAaBu22EwIpw0gTsK3S0rbu07tmVTPCg606nrShk5bihA1OJHn3xWVKdUtC
+R0bJUEgEPLB24nWr9kfCsoNOtVaOXeUEttNqWsplISkknStBP1Sa5NXhS9KdVncSAdXcO/hX0PxG
+wssRt/Z7+0ZuWpnS6gKE+O9RRyblgmfka2HpI/bQcDDDGAynVZ3Xac1Rtz/7Ui9hqVbtMOpE7yJr
+6ADKGWxwwlj4q++tGcmZYZWtbWEMoKzKgFqgnxiYoPn/APJrv/oufCvBhywf4Je3ka+hP4L4BEfJ
+jUep++vBlfAAZGGNT6q++g+e3yavj2S/gaw4a4Y+Zc/RNfQo5XwAmThjU+p++vPwWy/M/JjXxV99
+B89Pkp4meycj+qa2GEvT/Aun8019ChlfAASRhrUnzV99bHLeBnjhrJ+P30Hz2ThL+/zDv6Ne/JFw
+Zlh39E19Ck5dwVPDDmR8aw5dwX/49r6/voPnl8kvD+Idn+qa1+S3Z/gnI80mvoactYESScOaJPmf
+vrwZZwEGRhjIPv8AvoPnmMLdH8W5+ia9+TF7y0ufSvoYct4EeOGMH1BNe/g7gn/xzP1/fQfPA4cs
+H6Cx5RXhw9QH0VV9DXcsYAv6WGMn4/fSasqZdJk4Wz8VffQfPgWPPQfSsNlvOk719Al5Qy2RvhLP
+xV99aLydlk8cIY+KvvoOChZ4fKtSbgbbcONYLO030h7yMV3j+BuWNR/ce34+f316jKGWwSBhLIB2
+O6vvoOE7e0swiXRcSBtoAiYP7Yq69FHRxdZ9xt2zs1qtLW3QHLi5WmQkEwEj+keXoTXXyMpZcSNK
+cJYA4wCfvp5Z4DhNqoqtrQNEiDoWoftoGeRMrYdlDBk4PhfaezoMhThBUSSSSSAPGsqfSAlISOAr
+KD//2Q==
+
+
+
+--Multipart_Sun_Oct_17_10:37:40_2010-1
+Content-Type: image/jpeg
+Content-Disposition: inline; filename="custer.jpg"
+Content-Transfer-Encoding: base64
+
+/9j/4AAQSkZJRgABAQAAAQABAAD/4Q1kRXhpZgAASUkqAAgAAAAIABIBCQABAAAAAQAAABoBCQAB
+AAAAyAAAABsBBQABAAAAbgAAACgBAwABAAAAAgAAADEBAgAOAAAAdgAAADIBAgAUAAAAhAAAABMC
+CQABAAAAAQAAAGmHBAABAAAAmAAAAOYAAADIAAAAAQAAAGd0aHVtYiAyLjExLjMAMjAwNTowMTox
+MCAwMDo1NzowMwAGAACQBwAEAAAAMDIyMQGRBwAEAAAAAQIDAACgBwAEAAAAMDEwMAGgAwABAAAA
+//8AAAKgCQABAAAAyAAAAAOgCQABAAAA9gAAAAAAAAAGAAMBAwABAAAABgAAABoBCQABAAAASAAA
+ABsBCQABAAAASAAAACgBCQABAAAAAgAAAAECBAABAAAANAEAAAICBAABAAAAJwwAAAAAAAD/2P/g
+ABBKRklGAAEBAAABAAEAAP/bAEMACAYGBwYFCAcHBwkJCAoMFA0MCwsMGRITDxQdGh8eHRocHCAk
+LicgIiwjHBwoNyksMDE0NDQfJzk9ODI8LjM0Mv/bAEMBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIy
+MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAIAAaAMBIgACEQED
+EQH/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0B
+AgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpD
+REVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmq
+srO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEB
+AQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFR
+B2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVW
+V1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrC
+w8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/ANNRzUiIfSlR
+DmrUUfrXOajEiz2qZYPap0jqlrOr2+i2RkkZDMR+7jLYJPqfanYCd4OOlQGe2ibbLcQo/ozgGvPL
+vxRqE6kSXsj+yfu1/JeT+dYsl55m7JUZ6kD+vWiwrnsAlgfG2eI/RxUhjrxlLhgMLKcdua0YPE2q
+2mDHfSMB2c7h+tFgueqeVimmPiuO034hKWEepW4APHmw9vqK7G3ube9tlntZUliboymiwXECCgpU
+oSnGPNIZEqgUVKIz3opAVUXDVajFV4/vVT8R6yNC0SW6THnsfLhB/vHv+AyaaApeKPF0WjK1nZ7Z
+L0jk9RF9ff2rzC71Ge7naaeZpZWOSzHNVZriSaRndyzsdzMTkk1GFLHABP0qhEhnc5xWjpmg32q5
+8oY9M96hs7DzHG4MBjJJGK9j8HW1ta6ckqqHVunHQ0BY8wl8H6hDGWOcjttNYtxbzWxIcHivoXWN
+X0nS7YzXh5I+WMDJNeY+KTb3umHUotOlhgkfYjkfqaLhY4ESZrb8Oa/caLqCMrFrZyBLH2I9fqKx
+Y4mkcIilmJwABkk16R4V8BmDy9Q1dfnGGjtj292/woCx2qjIB9aftp+MUvFSMYFopxopDKCDDdK5
+L4i2V9dWdlJb28kkEJcylBnaTjBP68116ctWjanFUhHzpit7SLa0t1W7u5lCk4AxmvZbzwvoGoOZ
+rnS7dpDyXUbCfrjFeZ3WkW0OuXEcMMbQK52RljtA9jTESWNvDqExntAJDu2YEf3R6/413Xhq7itb
+IW88MgKMct2zmvN47h9C1WCS1uzGGbbIqnPynsexFelLMloUMjF4pVBEh75oAvX50q7+RIXknkwG
+Pt6c9q0bu00e/wBEk0m/kht4pE2KrOAwPYjPcVmWlza2TSXTbWwMgep9K5zWdavNSmISzS4bOMjA
+x6AUhm3pHhfRNFVZLG3EkmOLiQ72P0PQfhWnITUVjJNLYQPOmyUoN6+hxUjZzQAwr7UgWpKCMUgG
+7RRRzmikxlGNavwDiqiDB6VegGQOKpCJygkhaM9GBBrzHxFp50+5la8tnO4DbIg6LnqP0r0S/v5L
+K1MtvaS3bZKbYcHDDqD7+3WvONT1u+1W/jbUSVtY3yYEGAB396YjCtLQXt1mOBhAjZDPyT7V3GkX
+bJbtp92pe1I+V8ZMf19qzta8T6dounx6dplhbySkbi/U4PIJP9K4y88Q6reYMk2yPOQiKFU/h3oA
+73VLC6trFrmCVJ7dTn5X6fhWLo3ih31i2N3Bi1iYqWjHTPc+uKg0zxCb+2NrNaHj75hOFI9wKs6j
+FF9hf7CotmjAZWTj659aAPUQVdA6EMpGQR0IpjCuJ8GeKVNtJYXshd4xuiKLyw7jH9K7SK4guGdI
+nBePG9Dwy5GRkHkUDFA5HWnMBikIwaKkBo64FFAPNFIZVQVi+IvEk+mSJa2OVnxueQKDt9AM1uov
+NedeIJkufENyySEAEIMHjgY/mKpCZaOri/hjg1G3jnhRtylB5LofVWXHP1zWpqF1pt1pg8+4jmlS
+PbA6o32lj/02ydpGOMjr+lcsXliHOJF7jHNQSyAYmiPyA5I9DTEQnTVimMwywHOD6VoafJHp13Hc
+pZW92gJIhnGUORiljcSJnqDUMTBQ0fdf5UAaFhANDj1STUbP7PJN+9EQHHlnJUKPTJNSW4G4jqCB
+1qKKzu0EeoXV0k0N0CqiSXc4C5HI9OaSNsXCIvCg4oAzNbj1O01iDVXhS3km+dPKUKpHTOB61b0j
+xNLYayl3JGBE6COWNOAQOhFMv7QGCa7N5E489kFuZCXTHOcdhWMGBJJ6CgD3BJEliSVTlXAZT7Gn
+AA1xvgnXJrxX0+5fd5SAxEjnb0wf0rshjFSMTABopwxRSGVgDn615TNCTNMrctHIwP516xzuGK8z
+1FBa+I7yJgNrynH48/1qkJleOZo0GMstMlDy5MKxsx6g8GpGUxSnb9084p5RZUyuFb06UxGfazPE
+WicFWB4B9Kk3fv8Ad68VDOri4BaNt443D+tEchKsfegDQsVQy3DjJcR/d7dR+Va9lDGLR/NkLOzE
+gAenTn6iudsJtmoMeoaMg+3IrbtpJVjXa+O/NAGK4Q3M5bG7ewz+NZqAZcdlY5q3bxXOrasba0QN
+LNIxUFgo7nqa1PCPhttZ1a6S7Vha2rnz9p5Yj+EGgDLsZ7i0nS6t5WikU5BHp6V61o2pxatp8c6M
+vmYxIgP3W9KI/Bukyu0stmgZkwkSsVWMepx1NcOss/h3Wp47K4WRUfa+4cSD6UmM9IGM0VQ0zVrf
+VLbzImxIB88Z6r/9aipAn3ZNcB41tGj1gzKMGVFcH3HH9K7pDzXJeNCrTRuzHem1AueMNuOfzX9K
+pAznLa7juUCSEJMvr3qZwydRx6isi4ty3zJw1TWksjrs3kOOoJpiL0twwXlCwHoKxri6WFj8pG/k
+A1fe6vIODtKmu1+Gc9vcazNHeRxyZh+RXjVuc8nJ5oA4zwxp91rmpSW1nGGnZMZbgAZ5JP5V6Cvw
+213Jb7TYgbcKPMbr/wB816pGsESARpGi+iqAKT7VCPlMi5+tAHisPwc8SLJk39hH/tpI+R/47XWe
+FvBeqeGtHnhlENxO8hlYRSH5vQDIHP1xXoSzow4ZT9DSvIO1AHiepeMNUWaWCBPsgDFWDrl8jsc9
+PpXMajqdxe3BuLt90zAAsABkDp0rq/ixpTWeuwanbDC3kZDr/trwT+IIrziS6uUcJcqGWgDpPDOo
+pb+ILctJxITGQffp+uKKyNFtjc67YxqdyNMrfgDk/wAqKljR64vB5rK8Q6DaapCtxI0qXC7I0Kth
+eXAGR3+8fzrTDYNQavKRol465DpEZFI6gryP1FJMbPMEcqxRhytNkwrLInDDriqST/ay3z4lB3A+
+tSrIWQq3DjqDVEl9h9oiDIC2e1V4H1KxvEuIGe2KHPmZxtqkbuazYtC5FVJ767vG/eyEr6dKAPQr
+P4k6tZRHcy3KKeGlXJI98VuQ+OdM1EB3nubN3IASWM4Y+gIzXksN4YcB49yjsank1L7Q4ZvlVfuj
+3oC57To2vQX8jJBdJujO0hnCnP0ODXWxC6dBuLL6ZFeP2/izwp4hsIodejmtNREflvdRICjnszY5
+OepGOtcvc6vqeh3Ij0jXbnyifl8mZth+maLDPU/i9Cf+EOt53YB4rtcHvypBx+leILdgDbICwrqL
+7TvHPiVo7fUTdTxp8y+dIAg9+uM/rXS6D8PbDTQs+pst5cjkJ/yzU/T+L8fyp3AyfAmiXHn/ANrz
+I0duqkQhurk8Z+mM0V6BI+FCqAFHAA6CiobA/9kA/+EMRWh0dHA6Ly9ucy5hZG9iZS5jb20veGFw
+LzEuMC8APD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQi
+Pz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUg
+NC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5
+LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgog
+ICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6dGlm
+Zj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczpleGlmPSJodHRwOi8v
+bnMuYWRvYmUuY29tL2V4aWYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUu
+Y29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50
+cy8xLjEvIgogICB4bXA6Q3JlYXRlRGF0ZT0iMjAwNS0wMS0xMFQwMDowNzoyNyswMTowMCIKICAg
+eG1wOk1vZGlmeURhdGU9IjIwMDUtMDEtMTBUMDA6NTc6MDMrMDE6MDAiCiAgIHhtcDpNZXRhZGF0
+YURhdGU9IjIwMDUtMDEtMTBUMDA6NTc6MDMrMDE6MDAiCiAgIHhtcDpDcmVhdG9yVG9vbD0iQWRv
+YmUgUGhvdG9zaG9wIENTIFdpbmRvd3MiCiAgIHRpZmY6T3JpZW50YXRpb249IjEiCiAgIHRpZmY6
+WFJlc29sdXRpb249IjIwMC8xIgogICB0aWZmOllSZXNvbHV0aW9uPSIyMDAvMSIKICAgdGlmZjpS
+ZXNvbHV0aW9uVW5pdD0iMiIKICAgZXhpZjpDb2xvclNwYWNlPSI0Mjk0OTY3Mjk1IgogICBleGlm
+OlBpeGVsWERpbWVuc2lvbj0iNzU1IgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iOTMwIgogICB4
+bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6Zjg2ZTcwZTQtNjI5OC0xMWQ5
+LTllM2YtZDQyZjM0NjM5ZGJiIgogICB4bXBNTTpJbnN0YW5jZUlEPSJ1dWlkOmY4NmU3MGU1LTYy
+OTgtMTFkOS05ZTNmLWQ0MmYzNDYzOWRiYiIKICAgZGM6Zm9ybWF0PSJpbWFnZS9qcGVnIi8+CiA8
+L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+IAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAog
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz7/2wBDAAUDBAQEAwUEBAQFBQUG
+BwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUF
+BQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4e
+Hh4eHh7/wAARCAD2AMgDASIAAhEBAxEB/8QAHQAAAAcBAQEAAAAAAAAAAAAAAAIDBAUGBwEICf/E
+AEIQAAIBAwIEBAQDBgUBCAMBAAECAwAEEQUhBhIxQQcTUWEicYGRFDKhI0JSscHRCBVicoLwFiQl
+M5KissJDU9Lh/8QAGQEBAQEBAQEAAAAAAAAAAAAAAAECAwQF/8QAGhEBAQEBAQEBAAAAAAAAAAAA
+AAERAjEhEv/aAAwDAQACEQMRAD8Am8Hl60dEGN+tAbnalEBIr5+PUIoGcmllXPSgkW9LxxntVBET
+2pVY/Y0tFEe+KcpFRDYRDHSjrD/pp4sdHWIUwMHhwOlJcpXr0qVeEEd6aXCxxRtLK6xou7MxwB8z
+TDSSpjelAtQFxxvwjaSmGXW7dnH/AOtWkH3UEUeLjjhGXATV0z7wyD/61cNTTDbG1AKcdKjBxXww
+RznWbUA9Mkj+lSem3+n6lE0mnXtvdopwzQyBgp9DjpTDXGQ0mY6fFD6Vzy8DpTDTMR7UOT3p0U9q
+KF3oaasm1F5PSnTr3pMAb1FJrGwGRRuX1o64xvRtqYG5X2oyp7UpgV1agCKAQSKFKZHLQozUEoNL
+Rj3oBd+lLxJsKrTsaEmnccW+d65Ehz0pzGp9KuI5HHv0pdU7Yrsa+1LqmSBitSAsabdKrXiLxfb8
+IabG6xR3V/O4EVsXweXuxx2/maX4w410PhmJknlWe7A2gRhkf7j2/nWCaxxK9/qs+ohea6mbJuJd
+2A7BR0UDoKuJaud54gcYX0Jk/wC46Lbno5T4sf8ALJ/Sqlq+sx3j/wDiOoahq7A5HmSFIwfYf2qA
+uLxpn55ZGkf1Y5ptLPynAxVxEsdQXcwWNrCvuvM33phM8jS+Z5rZBz1pn+JxuWxRGvxzbDI9aYbE
+rHfXgGFuX/T+1OrPXdZs3LWt/NCx6lTjP2xUGl3EO9HN3Gds/ah8XnTvEjiiywZLsXKj92UBs/ff
+9at+heL9jNiPV7FoG7vCdvsf71izzAocGkWkyD1phuPVuh63o+uRc+mX8NwQMlAcOPmp3p8UA7V5
+Gs725sp0mtLiSGRDlWRiCD7EVq3Afi5KrR2HFA8yPoLxB8S/7x+8Pcb/ADqYNfkA3pLlpWKWC6t4
+7m1lSaGReZJEbKsD3BoKtYWEwvSjhQe1GA9aOoFA2K4JoAb9Kdcq5zQEagnYVKpALkHrQpyAB0Az
+6UKioTlIpxCMikyMiloU2qwLRfMU6iGe4puin0p3Ah6kYHf2rUQsgVFLuyqoGSScACsj8TfFURPJ
+pXDEo2+GW8Hf2T29/t61D+MHiM+pTy6BoMpFhGeWedT/AOeR2B/h/n8qyiSQjuc961ImndzdzTyN
+NPK0sjHJLHNNZLk5601klY96SZqrOnRuipJApKWdn70iuTTqztWnkUAEg+gqp9JRrJIQEVmPsM0s
+LG9dsC2lyenwmtO4D4PZ3jmKeYp64GQPnWoDw8WeBZIrZUbqMj+dTVx5l/yvUACfw0gx1yKQaOaM
+kNGy/MV6UuuALhIyGRDj2qj8W8E3do3mCDIJ3wOgppjICzD2owmPepnWdKaAtlSCO2KgGBBwe1VM
+OBJnYUA2BTdTR1zQaD4UceXHDd+thfyNJpE74dTv5JP76+3qO/zr0PHiQB42DKwyrA5BHrXjtc16
+l8Lo75OBNIF+GE34cYDdeTJ5P/bisdRqLGq980cLjpXQpB60dQc1lonymuBd8UsFocveoogXahSm
+2+1CsqglI6E05twu1NU69KdW/XFaiHcYXOKznx44xOi6QvD2ny8t7fITOyneOE7Y+bbj5A+orRlK
+Rq0kjBUQFmJ6ADqa8l8ba5LxDxTf6vITi4lJjH8KDZR9ABW+WUY03KMCkC5cnJorHJoA4rQ6elBI
+2kOFGTjNGUcx32qZsdAubiESoC+eyjcUQwgspVTmZod+ilxk1a+EtC8/UIeZ+WOVSSmNw3/WKjG0
+60tpbeCZJTLIQVKHIO/Tb5Vb7Vbhbxr+1jES82Vjkckuo/d6bHHr7VBtXhrp8ttaxhreGKFfzcw/
+X1rTbdomQBQCpGxA61ROFW/EaBDcsQrcgyo3GcdKnLNTHmWV0VTjdTjb5f8A+UaieFjHK/mHBQZJ
+J6Yqr8XXPD8UDie9tkC5BzKPsKrfGnFGu6jM2k8OlxynlaY/EgPYbd/sKyjTOFeK+JeJmtdUSRgk
+gDM/wDrjI9fuaiadcY22k3EL3MbRgcpCuB136VjOoIou2ZNlJyK9NeKfhUR4fwS6aZDLYEtcIDu4
+I3b6YrzJdczTNzdtvtViEQoNKwxs7qiKWZjgADJJqS4b4e1fiC/Wy0ewmu5m68i/Co9WPQD3NejP
+C7wtseEuTU9UaK+1gj4SBmO3/wBuerf6vtVXFO8K/CR08nXOKoimCHgsGG59DJ//AD9/Sti5eu1O
+5zzHem/b3rPqyCgHNGUUXau5qK6a4a4DXcZqApOxwDQoxWhWK0gsdDinEGAelJMgxS0CgVqMmHHM
+7wcDa3LHs4sJsEdvgIryU29ez0tobmNre4iSWGVSkiMMhlIwQazXjHwGtLsyXfCt8LWQ7i0uCSny
+V+o+ufnXSMvPGN66BVl4m4H4p4clZdW0W7hQHaUJzxn5OuR+tV4Ieb8pG/pVE5w/odxcSiaWNhGm
+CB6g9/lWn6XprWmnk5jifPQ9MHPX5AVQtK1trTTrlDkyOnlg+m2/9BTF77W7iN3Ms5UAFiW6elRF
+6gsNQZ31aOS3gnjCrIzDnJU9cdh9KXKZto52hYWkcgAxj2OGx03qF4D1qOyv4oNSvVCznq+8cfpz
+fP8AStj/AOxtla6VJDaXLXMDp580gwU5zvzAg7/agsXBN7pqWqWYYK4QOQTsexq4wjTrpArMvKw+
+1ZXw1oyxW8byOfPyc4bOKmJWvLRSPMbH5gcYxmoq5axacP2ulSAzx28e55om5Wyfl3qJ4Jt9Gtbt
+ruCGeaRsBGlkJyM5O9VaOG8v7oJzFhnY9Rg1oGh6BIlp5WQVC9em+KKstmq3isvLEI5MgqNwfXtW
+O6j4EcH2/Fd7qF811PBNKZorQNyRIDuVyNzv7jtU5xBxbxhoDfh7Hh6wEEPWV7gF3/2ov9a5oPE2
+rcUomoXUFvBaRgoAHLSM/cEY+ED077VUSem2NhpNmLLS7K3srdOkcKBR+nU+9CQk96OxBpN8YqVT
+dzSecjpSjgE7UFUBTtvQJMoAz61xRntSpAoBRisqIF36Cjcu3SukUBt1oOcu9CjKAaFYq6r2AcZI
+pxEvTem4SnMKYrUZPrUbipa2bG9RNqN6koelbglElBXBGR6Go/VLezNlPKmnW0kgQkc0S7n7UvET
+iofjLVrjS9PDWyKZHzueigdT+o+9VHmziHSiuu3d6UjJmdZ1B/KBJvnHrnI9iDUqAlxZLplxp6lF
+AdnXClhk7j164ovF/Pa6v5v4ZppJCWl5JCNic8uOg3Pak5+JntoRaS2DRyQrkCQbgEk9vcn70ZV/
+XtFktLCS4ihcRscDKbjpt/Or34IW8eraeLS4vrkJbyeYsAlbkLDcZXoaoGtahf3/ADT80nk8u4J+
+EbdKmPCrU7jh/VI71gTaSEeZj933+VFbglytnfGKdVUMxOMGpaSS3uYEBQZGQDmm2o2FvxBpy3tn
+MAWXmSRNwfqKqY1HUdKumt7wFcbFlG30qYrQOHohBdBlwA24wOm+f+vrSfGfF8nOdN0+8MKQjMrq
+uSx9B/aqmeJkS2llRsyKhKgnHNjNQcU97qzNGzRKZy3MScg/P5bUDbiHjp7GZFt9OlupWBBeQkkD
+tnHqMnFO/Crij8fxBLbRwGOKaIyEZ3LZzzH7kU2nsI9Jljt5ktnuZRhZMZ39fUbZH2rQ+H+GtN0x
+zeW9nDFdSKFkdR1wP0+lBNk5FEkyRSpGBik2HzqBHFGA964QAcZoZHTNWtDADHWuMpxkGuLgnFK4
+GOlZoR5cbmisTncUs4xtSRHM1AF+VCjqu2TQrNVAxg9KdQj1pJBvTmMHpWoyc2y5bYU+jUimtuu1
+PIx9a0HMewFR/E1g2oaXJChxJjCt6Zp+uwBpxFvjeqjzxfF9O1r8XeIwYebHKh/ccMcbf7Sp981A
+Xer8P3Mt3eXrPJMIisUUSgZboC3etw8W4OFE0sXWs31tYXiDML8vNJJ/p5Ruw/lXn6/01dXuGvtO
+tMO8uJQgPIpYZH3wftREUbt7pvwFkk0dtLgSK+Pi9/b9auOkaekdqEIxt27Va+A/CfWbuwTU4tPE
+kL5BmdwoGOu27Y+QNPf8hcSyxxhAiNy87HkUn5tj+9Ax4M1274duxDcRmfT2OSq9Y/cf2q7cQaVb
+a1apcWEsTLIAUYbA59ao2qXXDehoX1PU43cDaKE4P1JGfsPrTXhjiXUtUMknDn+W21ssn/kTO3O5
+9cbkfeinXEXDuraYhafSJJ0XOJIGz+lUK/4ku9PZ1tw6GPJIdcEe1bXJxWLfTHg1WExl15SVBIBP
+p9axbibSdT4g1aX/ACyyldJGCmRl5VC+pJoENB41upOIrK/1W1W8tYHBeHJGR6/Mdd69P6Ve2Wp6
+bBfafKJLaZOZGH9fevMs3DtnoFsJNTW6ZCcGeJQVQ+46/fFX3wr4ls9Im/Cxail1pc7ZYdHgb+Ir
+6euD70I2Vl+HNJkUvsyBlIKkZBBpJhRSLYwaKuD1FKkY61zAA6Vm0EQKW2pZeXuKSU4pQnbaoorh
+TSZ5RR96IwqDqnOcChRosAdaFZqocYB6UrG2/Sk1BzS8Y3rcQ7tj7U9i3ppboaF1r3D+l+bFf6xY
+296q/s4ZSTgnozKu+PatodalfWmm2Zu76dIYV/ebv7AdSfYVmPFnirO4ktOHoDD+7+JlALf8V7fX
+7VfOEtW1K0sNSng4j0Xi7VrlwbeGS9NmkSY/IkfKwXt8+5rJ9S401nT9TOleLPAyzo7HlvIIBDOo
+/iR1+GQD2OKuMqNftPqd1JPqM81xNJ+aSVizH61evCS+s40l0q5jhWYDy/2oGCMgxsc9gwAPtUzx
+F4ZTDQouJeGmm1TRp4xMvNHyzRoRnJHRh7j7Vmmr+ZYTW+q2p/aQNhwD+de6mojWPETxMvdE0KG1
+1LmFxFzJDaKRGWYE/E6rsFHQD296wHiDjbiTXJna71KZUb/8UR5Fx6bdfrTXiC9u9b1iW/uA3NId
+lySFHTApyugO9mLiHJI3xVVXzzN8TMSfc0406/u9OnE9pO8TjuO9WCLR47q3DACGdR3GVb2P96tX
+iN4K8Q8G8LR8QXV5YzxYX8RBEx5oS3pnZgDtQJaD4j3z2fk6laC5jGzSR/mX3q26JxClzah47kTI
+35SVwe+29UfwY0NNQur2/uA4ihVY0IPVzv8AI7Dv61aNStX03U5YT5fKeZ4+VcdOQ7j1xn7VApqL
+/wCZWNzbSr+zmjZcHsazC202WwnVortlukJ/J0BHbPetK06QvEzEHIkI+hFI3fh1+O4e1Pia31uO
+G5t5ARZsm7DAyQc5/Sg0Dwd4ztdU4Y/C6jMyX1rJycnLn4NsfYnGPf2q6aVq2m6tC02n3ccyI5jf
+BwUYdQwO4Psa8kcLa3LpGssWeTyXfEgU4Jwa2vWLxdQ4e1oWGnabc6Zcwfio721YJcQuF5v2i7MQ
+G2BGwzvRZWsOuKTIIqgeC/FjaxpB0q+maS+tF+FnOWkj9fmOn2rQGO9ZsUl0augkVwjfOKAB9qiu
+52pNmzttRiDRGU5xQGQ9xQrqKcbihWaIxF70snWklB74paJcmtQRXHHEq8NaA90ih7uU8lup9cbs
+fYf2rDLvWJ9QvGur6WWWd2y7Mc5q9eOV60Op6XbqhYLA8jHtu2P6VQYruAkeZFsepxW2KfQTI35G
+Gau/DvG99aWo0zW401vR2xz211livujHdSO3p7VSLdrGX9/lPTI9KXaCXk/ZOjemaDW9M02NpbTi
+LT/FjVrPhaxbzf8AL7mTnmgYDaH4iQVxtuDt69aoviLrXD+u67LcaLpE1pBJnzWcgLMf4gmAVPr2
+/nVMnlBcRXCBJ13QkbA06065FxCWcYdG5WT0IoGI0eISMgUe2RR9OJt5Ws3xjtt1qUB518zG6HtU
+frScskVygIwd8UHZ7copUCjaxe8bcR6Yuj3V7NeW5OUiCc0jY3+ZG2aXibmUSb4YdRU3wbxqvA2q
+zak1i9z5kBjDkcxjOf5f2FTQXwqnWz4eOmvGY5orhxICuG5tuvv2ol9rcOs69d+UcpZOsRYfvFld
+f54qNtOJ5+Jtf1XVpMwySzI/L3/Ly/8A1FHg0aLS9aklslKw3flM8Z3AbzR09tzVEjYcoeULsGAc
+CmvEOnarqt1aWelreSySK3PBbgnnUYO4HYUdJkSWAKd+XlOPepJ+J9S4VhttZ01Od1k5JEJ/OpBy
+PuB9hQZLxHpk+n6sySQvE2fiR1wVYdQQad2F5eWkb/h5XiJjaNgDsVbZhj3qa4o1W64nu5tVvIDF
+M7c2CcmohlBJ2xlaB9wzrl1w/r8Go2ZUyxDHK35WB6g16R4T1634i0SHUYF8st8MkZOeRh1FeW/J
+WRw2enU1qvgnxTp+nR3Gj6lMtuZ5Q8EjbKSRjlJ7dBUpK2QkD0rg3zRGIJ2O1GSstlFwAc0VsA7V
+35UXBzUo6p2oV0KaFZVGKm9LxKM9M0T0NdklENvLLjPIhYD5CukRjfi1qCXHF88eQY7ZFgH2yf1J
++1VyGOORMYGKbNK97JPLcMWkkcyOT3J60haieGQqmeXO2a0wkJ7FGHwkKexBpBLqeycJOTy9m7U/
+tJo5sebGMinM8VpLGedMqN996COvF/Hw+ZFyyOo6A9fl71H2NwEvg2cGQcrjpuOmf+u1PxZRK7SW
+NyFx+5UfqyK3/eEBW4jPM4/jH96Im7d/jOGG/auXo57RlO/ptTHS7xJ4lIOT0p60mQ4GxopppdwG
+QwOfy7UtIshlFuInmD4AULnOegqOGI7wMo2JwfSn8mXQgNysR8LZ70Fp4h8ML/hWK01C51Czb8eq
+q0ETEtGx3HsR7im96wjWJFlVmjKq3fmxlz/8ahtK1TXdc4jjt9cvGljit2EQXbmIAAz9KXnYWVq8
+apjCSSDfJGwTr/yNAiMlFy/7oORvjepDihUk0CGFZFjDToDI/wCUZOMn7060vT7OXQVvpLadCwZY
+wzBEkPL8JJPQBs5OcHp61E8Xs3+TwW867mQcy47gGgl+P+CtL4d0qyv9L1w6glwgLq6gEZHUY/lv
+Wb3MmG5c4PKR9akYbaRinPczyRpuiO2QufTNRuqxNHdKvYsD/wBfagPFtEAegFFkbzcKq7A9aDDK
+igx8qHb8zHCiiPQfhJrX+bcJQiWQyT2rGFyeu3Q/aropX0rzp4e8YS8JXqxPCJ7O5I89R+ZcfvCv
+QOnXttqFlFeWkgkglUMjDuKzY1DvPegGoLRWqNFFbI7UK4g2O1CshgAcelGRFkUxt0YEH610jAo0
+LKD71qDzaI1g1Ke1JB5XZM/I4o1xGY2Db4FLcWRfgOML9OgS8kH0LH+4p75AmgDcwwa2wYYLplNi
+BtiixzzRH4gWA7EVyM/h5miz8NPIRHMOUAADvnFENpYrO7X87wSnoU/tTC80bUoR5kEgvUG+xww+
+h/pUxJawr8cec02FxJE27HlHrQQOkTG2lliZCuGyA2xHtUxFKzfER9qYaxBLPci9tD5g5cSRj8w9
+x6ijW8+YsZHTegPLvuMDBp2JAIlbbcY6U2tpPMV8/LOKAceXjB2oJ3hCeN9Z3UMywvjbp0rt9bmS
+OeeQZeQQrGN8AkFjt26ioLhed4uKIU5iOdXBH/GrLqDfEkaDc3BJ/wCKqv8ASirgbmxtLENZ6W7x
+ae37cMQFYc+Qq8wOSAASfXvvVP8AEdndhLJzeZ+LfmBbJyc7VJPdW0ziW6hilkAzzFFBHyqJ8RLt
+brTLec8gkacFiFwT8JoIK2lXlBYbVE8RMFaKT19/ejG5wAq0x1uVnhiB3IbagdwqHVc9MUSP9vdF
+v3E+FaPptvdX7Q2NhDJPdzEJHGgySTWq2/hOukcMvd6teSC/EZZUhwY1OM4JxknttjrQZqYlDGRx
+sOgNaX4M8Sw2Usmi6hOscUp5rcucAN3X60Xh3wf4k1e1ju7ua105GHMsUuWk/wCQH5flnNUnUrFo
+NVms5MB7VzGeX+JTg4+1QemVwRkYxRGJzWd+H3HtrJDBo+sytDdKOSOdz8MnoCexrRRg4IIIPTFZ
+aHRtutCiEkChWappJkLjJrkaEnJo8rCgjYxvW4MN8XrNYeNr3GR5ipJ09VFRWg3XnQlMnmTYg1dP
+G+z/APGbO8HSa25D81J/oRWXQztYXvm78j7PVYqw6hFGSJOXp1OKbpzKOZExipGIxXduGT4sjam3
+lmJzG+du4NULW84YBZCPfaiXtosykxt13xXFCEEDOR39aNDKUk/KceuelEVq/gu4JcRrICD13okr
+XBhE8x+IHDEdSOxNW55oHT4lXPqKjr+2je0kjHLlxjtRURZNiPYjfvXWbAIYCkbZuVeQnp1HvSU8
+yDIBG+2KIW055DxDp7W7AP5uCcA/Dj4v0zVnhdpZIWOxMZkP/Ni230IqrcMRo2tyTqCTDbyMBnbJ
+HKP/AJVaLGRBcXDkhkiYRqADsFGB/KosSyxQOg50JPuKqvH7CJrG2RsjDyN+gFS9veTT3W4+HPrV
+X48uObWljBwI4gMDsTRTbhvTbjXtfstHtZooprqURh5Gwq+5qa8ZeCLjgi6s7d9VttRSUkiSIcpV
+h1BXP61VrW4ltikttIYZo2DI69Qw70e9utS1/VrSPULlp5ZZVXJwOpAJrSN7/wAO3B6Wul/9qL5R
++JuU5bcMu6Reo92/lWo2GmS6hqBv78AJGSLe3JyFH8Te/f2+dNOHEkt+FtOtRcRmNY1AePBGw2pT
+U9RFoHSW9htI+txcu3KIl74z1JOwHr8qjUTkY/Fo0Vs7RW6bM46k98e9Y54+No6alYWtjbRw3MMZ
+82RVwFQ9Ax9Sd9/ep3WvFzQ7G3FhollcXap8IkJ8tSPXfcn3xVP408QdP4k4Yk0M6GLbmkEvmGfn
+JcHOTsPehqhpb2kjiTl53H72dqv/AADxs9hyaZquTaDaObmyY/Y+1Z000iAsRGF6DkJOPnSE1xME
+ODzKR2rNiPTqyxTQrLDIskbDKspyCKFZ14IX7XGgXFm0vP5MpIBOcA9PpQrNaXlmwBtXRkkGkQwz
+SqZyKSireMVh+I4TivAPitJ1JP8ApbY/ry1h+pW3OTgnHbevQPHsN3qOjwaDYcguNTnEILdAoBcn
+/wBtYMRzKFIww2atxmmGl6nPps3lOeaPO4qz2t5aXyghwD71WLy18zJC71FwzSW8uCSKrK+XFpyn
+nifJPXemp8xdgjZ+RqOtLucqG5+YEbUs2pyoCWJoHEqyn8sbkkdlNRV4uoKwPkuvtykVIwcQRK2G
+yKPNrEEzpySFGXpk/Cw9DRVT1qeW1lWQoyCUZx03HWo1LtpG3JrdvBySz1fxAtbDUeG7PUomVjI8
+8SukAxs45ts5GPXrXp+z4b4chAaHQ9MjI6FbVB/SkMeG/CzTdS1jiRNOtdPu5TduiGZISyRKrcxZ
+j6bV6l4a8FuDLSxWO7hvb+U7vJNM0ZLHr8KYx+taqkcUIVI41VewUYArsigSZGKuKoKeEHAMJDpo
+rhvX8VKf5tTW78DfDW+na5udDleV8Zb8ZMOnyatLyCu/pQQgDqKDNbPwJ8M7W6juU4e8xozkLLcy
+un1Utg/WlX8EfDsasurW+hm3u0bmUxXEiqp9lzj9K0gkAda7nK7VRVZeDdIeJwqzwzH8s6vzPGfU
+Bsr+lecPGPh/ifh/iBbLW9Sl1GxkzJY3LfCGGdwR0DDv8/evWb7HOdqpvjLwsvGHh9qGnxIDf26G
+4sX7iVRnl/5br9amFeQiOQ4yCPbrTeSLLl4zv1371DQX8yA+YCGHX2NBuJCshR0UgbdKiHdxd8jl
+dwfSm348xnm5Oai/ibe/bflRj0P96ZXHNBLyMpA96GtG8G9cS34rWDIWO9Qxuv8AqG6n+n1oVV/D
+S3a7490qNCwVZfNbHooJ/pQrnZjUejBjPSl12IG1Nhkt0pxGD1NSVcQ3F17HpeqcOalNJyRRaj5b
+tnAAdGWsIlYJql7CSMpcOvX0Y1vvG3DZ4q4Yn0qO4W3nLLJDKwyEZTnt7ZFYxxfwJq3DV9LNNfR6
+j+xWeeRFK8nM3IOvXfG9bjNRbJzHAxnGMUwurJJVbb608t5gFywJJ9KcZXBBGRVREabM1rP5E26n
+cGp0Q288PMNjTC/tkki5kwCB2613SblsCF2APT50AvNMj5TIuB3z3qNSMBvi7Hap65TbP9dqirhV
+QFsYOelNMW7wn1ltC4uspjPy28kgSTmOwB6H23r2Fpl+rwhieY8ucj+9fP1794mJBAxWhcEeJHF1
+vYCCDU3Fra/xgNk9lyRnHtRXtA3S8iuCppvd38ceDg7HtWD6B40W7Wwe9tQZlHxRBsBv9p7VcND4
+80ni+ynbTGkt7222ntJtnXPQjsw9xV1V8XX7U8o5XO5H8v70dNcsOflZ+VvQmszvG1Yc7RJzxk5y
+N+tIQX8sbjmkhBO551II+9T9GNfTUbaTHJMjfI04S4UjGay+zvp5CpJBP+kbCp2y1GRByFmPfc9K
+foxb2lDE7/ShDIomUZ2OxHzquG8nZQyinNpdyNgEYYGmmPH3jLoFpo3iVrulEGBBcmaEqNuST4wP
+pzY+lUu60i2EBImVpPbvWwf4z7Q23iBpeoRLgXenAE+pR2/owrCo7iUOAT9KrIvJJbyfDkYNSkFx
+Hd24jlxzr+U0QeVdDHMFIG5NMJk8p/gl+1X0aj4Eaf5vFNzeFdrW2IB92IA/QGhVn8BdNntOF59U
+uYyn46UeVnqyLtn5Ek/ahXLq/Wo0LlAbY0dAc9TSJOD70ojjY1iNU7iyM4yKqfF8IvNa1KyfcT8N
+3DKD3aN1dftirVDKp9Kz/wAbLq60pdO1ywmWN+SazfftIvp8s1uJWNluQq/NsRnrTtJkbBUjGKj7
+OSOeE2rN8QGVNNj51jNljzRnvW2U7JKrKQoyR1phM/lSK64z1NKQOsq8/MObrmkJjzty8h9M1BOW
+E63UIQMOalZdBkul+J8DtvVTE01pLzwyMuPQU4bi3UUQJkEjbJFMNSl1wzbwjnurxQvcLTC61OGO
+FdO08IkS+nf5moDUtbvr5irynHtSNoxiIbO9WRFnVWEBfm3A9abadJxE16l3pN7cWk8eyyxyFDj0
+yO3tSNpqSD4ZcYPWp231e0t7blgYA43FBYLbxG474Zt+eW/ttULYDrOhJP1BGftVguOK+MNV4cte
+I7LheG4sp8q01rO+Y5FPxKy9j36dMVlVzfSX1zsMhdzWm/4fePLPhniSTh7XJUj0bWCFLufggnH5
+XPoD+Un5HtRZVq4J8Q+GDpbza/FrMFzAc3Kxwq/kL/ER1K57gbbVpPDHFPAuuMo0jjbT5XbcQzv5
+T/8Apf8AtTTirwj0niC9N7a6oulXirmKSABiT/qGd1x2715q8SOAZ9D4gm0y4jWw1INlMHFtdL2a
+Mn8pP8JqZFe1raxkKjlaGZT3RhinSWLKdosZr532mu8UcPXLQ2uralp8sZwViuHT+Rqaj8XPEeKE
+xJxfqnKRjeXJ+53rWJrYv8cd9YC94ask5Wv44pXch90jJUAEe5B+1eaDKSMgkGlNV1HUNVvZL3Ur
+ye7uZDl5ZnLMfqaluDODeIeLLvydHsHkjU4kuH+GKP5sf5DJ9qvgghI4YkM2/vWreEXhhfcQSxaz
+r8Ulto6kMkbfC917DuF9+/b1GkcB+EPDvDix3mphdX1Jfi5pF/Yxn/Snf5tn5Cr9PP22A6Vi9Lhp
+NHFEkcEEaxRRqFRFGAoHQAUKTnkHPmhXOqTcnNBGNChWI0VjbvgVnX+IiMtwbZzKQDHeA/dWoUK6
+c+s1g0N2/mrMuzKas0YS/tQ7LguvN8qFCt1kwV2tJ/KU5XNO2cqSQBkj0oUKBheFmzvudyah51Oe
+tChViVyOFQfenCxKRvQoVUJSRgHrSTMQ2ATQoVYsOIZ3gBAJpve3Ek5BJ70KFIJXh/i/ibQZEl0r
+Wbu2KkEKJCV/9J2q73Pi3xPxRZ/5JqENhNPe8tsLqSLLICcbD60KFLCM+4gDw63cWUkjTpaSNApb
+qQpIrWPD/wAMeG+IuDLW/vWvYbqUNl4ZRjrtsQaFCs9XIs9XHhvwa4M04CS9hudVl5sg3EnKg/4r
+jP1zWhW8dvY2kdpZW8NtbxjCRRIFVR7AUKFc9tawSSViDTSRsnehQqKbyjLbUKFCs0j/2Q==
+
+Cheerio!
+
+--Multipart_Sun_Oct_17_10:37:40_2010-1--
--- /dev/null
+Return-path: <>
+Envelope-to: djcb@localhost
+Delivery-date: Sun, 20 May 2012 09:59:51 +0300
+From: Steve Jobs <jobs@example.com>
+To: Bill Gates <bg@example.com>
+Subject: multimime
+User-agent: mu4e 0.9.8.4; emacs 23.3.1
+Date: Sat, 19 May 2012 20:57:56 +0100
+Message-ID: <m2fwaw2baz.fsf@example.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="=-=-="
+
+--=-=-=
+Content-Type: text/plain
+
+abc
+--=-=-=
+Content-Type: application/octet-stream
+Content-Disposition: attachment; filename="test1.C"
+Content-Transfer-Encoding: base64
+
+aGVyZSBpcyBhIHNpbXBsZSB0ZXN0IGZpbGUuCg==
+--=-=-=
+Content-Type: text/plain
+
+def
+--=-=-=--
--- /dev/null
+User-agent: mu4e 1.1.0; emacs 27.0.50
+From: Skipio <skipio@roma.net>
+To: Hannibal <hanni@carthago.net>
+Subject: test 123
+Date: Sun, 24 Mar 2019 11:50:42 +0200
+Message-ID: <87zhpky51p.fsf@djcbsoftware.nl>
+MIME-Version: 1.0
+Content-Type: multipart/signed; boundary="=-=-=";
+ micalg=pgp-sha256; protocol="application/pgp-signature"
+
+--=-=-=
+Content-Type: text/plain
+
+
+I am signed!
+
+--=-=-=
+Content-Type: application/pgp-signature; name="signature.asc"
+
+-----BEGIN PGP SIGNATURE-----
+
+iQIzBAEBCAAdFiEEaYec7RdFk3UPFNqYEd3+qdzEoDYFAlyXUwAACgkQEd3+qdzE
+oDbjdw//dAosaEyqSfyUMXjS++iJEeDIwKwO6AjEI0xCbJjHmxq93PA61ApE/BS3
+d/sKa1dsfN+plRS+Fh3NNGSA7evar9dXtMBUr6hwL0VTmm5NDwedaPeuW6mgyVcB
+VNUn5x1e/QdnSClapnGd156sryfcM1pg/667fTHT6WC01Xe0sezpkV9l0j4pslYt
+y6ud/Hejszax+NcwQY7vkCcVWfB9K4zbiapdoCjHi78S4YAcsbd//KmePOqn04Sa
+Tg1XsmMzIh7L/3njkJdIOd9XctTwYEcN5geY1QKrHQ/3+gBeaEYvwsvrnqnVKqMY
+WCg/aYibuXl+xNkPMcKHIj1dXA3m5MkL77RrxODiAYz0YkiQx1/DLZs8PV3IVoB4
+f0GGDqyiOwSmSDa4iuCottwO4yG1WM1i7r6pir22qAekIt43wSdwakOrT1IkS8q2
+o0VGiQtEPy27D+ufiw06t02Ryf20Q7i2YcueZxYeRBq41m11M41DJ4wH7LQcJsww
+qG5iBOdwQFCTWpi1UrbbFjlxXXWvKMuIU+4k7nsamrEL4SDXmq1v13vtlcgJ6vnn
+v7c9+MF7laqdfI+BYnlD1v/9LosPbFTm0hPdvK4yIOORp8Iwj/1PGzTOz6SCUxzA
+kDu+Y+NN9/SM1ppStg1OikYPcfEXF8igWhuORwqcmpgHxVkIQ9I=
+=wnkU
+-----END PGP SIGNATURE-----
+--=-=-=--
--- /dev/null
+Return-path: <>
+Envelope-to: skipio@localhost
+Delivery-date: Fri, 11 May 2012 16:21:57 +0300
+Received: from localhost.roma.net([127.0.0.1] helo=borealis)
+ by borealis with esmtp (Exim 4.77)
+ id 1SSpnB-00038a-55
+ for djcb@localhost; Fri, 11 May 2012 16:21:57 +0300
+From: Skipio <skipio@roma.net>
+To: Hannibal <hanni@carthago.net>
+Subject: signed
+User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8
+Date: Fri, 11 May 2012 16:20:45 +0300
+Message-ID: <878vgy97ma.fsf@roma.net>
+MIME-Version: 1.0
+Content-Type: multipart/signed; boundary="=-=-="; micalg=pgp-sha1;
+ protocol="application/pgp-signature"
+
+--=-=-=
+Content-Type: text/plain
+
+
+I am signed! But it's not good because I added this later
+
+--=-=-=
+Content-Type: application/pgp-signature
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.12 (GNU/Linux)
+
+iEYEARECAAYFAk+tEi0ACgkQ6WrHoQF92jxTzACeKd/XxY+P7bpymWL3JBRHaW9p
+DpwAoKw7PDW4z/lNTkWjndVTjoO9jGhs
+=blXz
+-----END PGP SIGNATURE-----
+--=-=-=--
+
--- /dev/null
+Return-path: <>
+Envelope-to: karjala@localhost
+Delivery-date: Fri, 11 May 2012 16:37:57 +0300
+From: karjala@example.com
+To: lapinkulta@example.com
+Subject: signed + encrypted
+User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8
+Date: Fri, 11 May 2012 16:36:08 +0300
+Message-ID: <874nrm96wn.fsf@example.com>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; boundary="=-=-=";
+ protocol="application/pgp-encrypted"
+
+--=-=-=
+Content-Type: application/pgp-encrypted
+
+Version: 1
+
+--=-=-=
+Content-Type: application/octet-stream
+
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v1.4.12 (GNU/Linux)
+
+hQQOA1T38TPQrHD6EA/+K4kSpMa7zk+qihUkQnHSq28xYxisNQx6X5DVNjA/Qx16
+uZj/40ae+PoSMTVfklP+B2S/IomuTW6dwVqS7aQ3u4MTzi+YOi11k1lEMD7hR0Wb
+L0i48o3/iCPuCTpnOsaLZvRL06g+oTi0BF2pgz/YdsgsBTGrTb3pkDGSlLIhvh/J
+P8eE3OuzkXS6d8ymJKx2S2wQJrc1AFf1BgJfgc5T0iAvcV+zIMG+PIYcVd04zVpj
+cORFEfvGgfxWkeX+Ks3tu/l5PA1EesnoqFdNFZm+RKBg3RFsOm8tBlJ46xJjfeHg
+zLgifeSLy3tOX7CvWYs9torrx7s7UOI2gV8kzBqz+a7diyCMezceeQ9l0nIRybwW
+C9Egp8Bpfb02iXTOGdE/vRiNItQH14GKmXf4nCSwdtQUm3yzaqY9yL3xBxAlW53e
+YOFfPMESt+E7IlPn0c7llWGrcdrhJbUEoGOIPezES7kdeNPzi8G1lLtvT04/SSZJ
+QxPH5FNzSFaYFAQSdI7TR69P7L7vtLL8ndkjY49HfLFXochQQzsqrzVxzRCruHxA
+zbZSRptNf9SuXEaX9buO1vlFHheGvrCKzEWa6O7JD/DiyrE/zqy4jdlh9abMCouQ
+GWGSbn8jk6SMTQQ2Yv/VOyFqifHZp0UJD59tyIdenpxoYu5M0lwHLNVDlRjLEwUQ
+AIDz1tbLoM7lxs2FOKGr8QqbKIeMfL+NUmbvVIDc4mJrOlRnHh+cZYm4Z49iTl1v
+bYNMYgR5nY7W6rqh0ae7ZOW0h2NzpkAwTzuf1YrSjNavd9KBwOCFtAoZhRwfwFVx
+ju+ByHFNnf7g/R6DekHS0pSiatM0cPDJT05atEZb+13CRHHznonmLHi+VahXjrpg
+cIUA8Lhjdfm6Fsabo7gNZnTTRxNBqUXKK2vJF/XLbNrH5K2BH2dCCmUNtm3yFWiM
+DOzaw3665Y3S6MvZdyKpatbNrVoJdBpRgPxJ1YCSEituFUqHJBStay+aRb5fVkQR
+w3+9hWw+Ob0+2EumKbgfQ7iMwTZBCZP4VOxkoqdHvs9aWm4N7wHtXsyCew3icbJx
+lyUWsDx/FI+HlQRfOqeAMxmp8kKybmHNw8oGiw+uPPUHSD1NFYVm2DtwhYll3Fvs
+YY7r5s3yP1ZnwxMqWI3OsExVUXs8MS4UTAgO+cggO7YidPcANbBDihBFP8mTXtni
+Oo5n5v+/eRoLfHmnsGcaK8EkKsfFHpbqn4gxXGcBuHaTTJ/ZhbW6bi1WWZA9ExaJ
+IeTDtp5Bks1pJvTjCDacvgwl3rEBM6yaeIvB7575Y/GPMTOZhawhfOxV1smMmTKI
+JOWYb3+PuN2cvWetkjFgH8re4sRXq22DKBZHJEWYU8sH0sACAePnIr+pkrOtGeJB
+t1zBqZUnrupH6ptk9n/AjbQ+XSMTEKu55gSjYLAYx1EHApx52QLkdh+ej5xCIVeY
+6wS1Iipkoc6/r6F7CKctupXurNY2AlD4uQIOfD6kQgkqK4PY3hsRHQA+Zqj6oRfr
+kxysFJZvhgt26IeBVapFs10WuYt9iHfpbPUBQUIZCLyPAh08UdVW64Uc2DvUPy+I
+C+3RrmTHQPP/YNKgDQaZ3ySVEDkqjaDPmXr5K0Ibaib2dtPCLcA=
+=pv03
+-----END PGP MESSAGE-----
+--=-=-=--
+
--- /dev/null
+Date: Thu, 1 Jun 2012 14:57:25 -0200
+From: "Rocky Balboa" <rocky@example.com>
+To: "Ivan Drago" <ivan@example.com>
+Subject: currying and tail optimization
+Message-id: <3BE9E653ef345@emss35m06.us.lmco.com>
+MIME-version: 1.0
+Content-type: text/plain; charset=us-ascii
+Content-transfer-encoding: 7BIT
+
+Test 123. I'm a special message with special flags.
--- /dev/null
+// CLI11: Version 2.4.1
+// Originally designed by Henry Schreiner
+// https://github.com/CLIUtils/CLI11
+//
+// This is a standalone header file generated by MakeSingleHeader.py in CLI11/scripts
+// from: v2.4.1
+//
+// CLI11 2.4.1 Copyright (c) 2017-2024 University of Cincinnati, developed by Henry
+// Schreiner under NSF AWARD 1414736. All rights reserved.
+//
+// Redistribution and use in source and binary forms of CLI11, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+// 3. Neither the name of the copyright holder nor the names of its contributors
+// may be used to endorse or promote products derived from this software without
+// specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#pragma once
+
+// Standard combined includes:
+#include <algorithm>
+#include <array>
+#include <cctype>
+#include <clocale>
+#include <cmath>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <cwchar>
+#include <exception>
+#include <fstream>
+#include <functional>
+#include <iomanip>
+#include <iostream>
+#include <iterator>
+#include <limits>
+#include <locale>
+#include <map>
+#include <memory>
+#include <numeric>
+#include <set>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+#include <tuple>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+
+#define CLI11_VERSION_MAJOR 2
+#define CLI11_VERSION_MINOR 4
+#define CLI11_VERSION_PATCH 1
+#define CLI11_VERSION "2.4.1"
+
+
+
+
+// The following version macro is very similar to the one in pybind11
+#if !(defined(_MSC_VER) && __cplusplus == 199711L) && !defined(__INTEL_COMPILER)
+#if __cplusplus >= 201402L
+#define CLI11_CPP14
+#if __cplusplus >= 201703L
+#define CLI11_CPP17
+#if __cplusplus > 201703L
+#define CLI11_CPP20
+#endif
+#endif
+#endif
+#elif defined(_MSC_VER) && __cplusplus == 199711L
+// MSVC sets _MSVC_LANG rather than __cplusplus (supposedly until the standard is fully implemented)
+// Unless you use the /Zc:__cplusplus flag on Visual Studio 2017 15.7 Preview 3 or newer
+#if _MSVC_LANG >= 201402L
+#define CLI11_CPP14
+#if _MSVC_LANG > 201402L && _MSC_VER >= 1910
+#define CLI11_CPP17
+#if _MSVC_LANG > 201703L && _MSC_VER >= 1910
+#define CLI11_CPP20
+#endif
+#endif
+#endif
+#endif
+
+#if defined(CLI11_CPP14)
+#define CLI11_DEPRECATED(reason) [[deprecated(reason)]]
+#elif defined(_MSC_VER)
+#define CLI11_DEPRECATED(reason) __declspec(deprecated(reason))
+#else
+#define CLI11_DEPRECATED(reason) __attribute__((deprecated(reason)))
+#endif
+
+// GCC < 10 doesn't ignore this in unevaluated contexts
+#if !defined(CLI11_CPP17) || \
+ (defined(__GNUC__) && !defined(__llvm__) && !defined(__INTEL_COMPILER) && __GNUC__ < 10 && __GNUC__ > 4)
+#define CLI11_NODISCARD
+#else
+#define CLI11_NODISCARD [[nodiscard]]
+#endif
+
+/** detection of rtti */
+#ifndef CLI11_USE_STATIC_RTTI
+#if(defined(_HAS_STATIC_RTTI) && _HAS_STATIC_RTTI)
+#define CLI11_USE_STATIC_RTTI 1
+#elif defined(__cpp_rtti)
+#if(defined(_CPPRTTI) && _CPPRTTI == 0)
+#define CLI11_USE_STATIC_RTTI 1
+#else
+#define CLI11_USE_STATIC_RTTI 0
+#endif
+#elif(defined(__GCC_RTTI) && __GXX_RTTI)
+#define CLI11_USE_STATIC_RTTI 0
+#else
+#define CLI11_USE_STATIC_RTTI 1
+#endif
+#endif
+
+/** <filesystem> availability */
+#if defined CLI11_CPP17 && defined __has_include && !defined CLI11_HAS_FILESYSTEM
+#if __has_include(<filesystem>)
+// Filesystem cannot be used if targeting macOS < 10.15
+#if defined __MAC_OS_X_VERSION_MIN_REQUIRED && __MAC_OS_X_VERSION_MIN_REQUIRED < 101500
+#define CLI11_HAS_FILESYSTEM 0
+#elif defined(__wasi__)
+// As of wasi-sdk-14, filesystem is not implemented
+#define CLI11_HAS_FILESYSTEM 0
+#else
+#include <filesystem>
+#if defined __cpp_lib_filesystem && __cpp_lib_filesystem >= 201703
+#if defined _GLIBCXX_RELEASE && _GLIBCXX_RELEASE >= 9
+#define CLI11_HAS_FILESYSTEM 1
+#elif defined(__GLIBCXX__)
+// if we are using gcc and Version <9 default to no filesystem
+#define CLI11_HAS_FILESYSTEM 0
+#else
+#define CLI11_HAS_FILESYSTEM 1
+#endif
+#else
+#define CLI11_HAS_FILESYSTEM 0
+#endif
+#endif
+#endif
+#endif
+
+/** <codecvt> availability */
+#if defined(__GNUC__) && !defined(__llvm__) && !defined(__INTEL_COMPILER) && __GNUC__ < 5
+#define CLI11_HAS_CODECVT 0
+#else
+#define CLI11_HAS_CODECVT 1
+#include <codecvt>
+#endif
+
+/** disable deprecations */
+#if defined(__GNUC__) // GCC or clang
+#define CLI11_DIAGNOSTIC_PUSH _Pragma("GCC diagnostic push")
+#define CLI11_DIAGNOSTIC_POP _Pragma("GCC diagnostic pop")
+
+#define CLI11_DIAGNOSTIC_IGNORE_DEPRECATED _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"")
+
+#elif defined(_MSC_VER)
+#define CLI11_DIAGNOSTIC_PUSH __pragma(warning(push))
+#define CLI11_DIAGNOSTIC_POP __pragma(warning(pop))
+
+#define CLI11_DIAGNOSTIC_IGNORE_DEPRECATED __pragma(warning(disable : 4996))
+
+#else
+#define CLI11_DIAGNOSTIC_PUSH
+#define CLI11_DIAGNOSTIC_POP
+
+#define CLI11_DIAGNOSTIC_IGNORE_DEPRECATED
+
+#endif
+
+/** Inline macro **/
+#ifdef CLI11_COMPILE
+#define CLI11_INLINE
+#else
+#define CLI11_INLINE inline
+#endif
+
+
+
+#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0
+#include <filesystem> // NOLINT(build/include)
+#else
+#include <sys/stat.h>
+#include <sys/types.h>
+#endif
+
+
+
+
+#ifdef CLI11_CPP17
+#include <string_view>
+#endif // CLI11_CPP17
+
+#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0
+#include <filesystem>
+#include <string_view> // NOLINT(build/include)
+#endif // CLI11_HAS_FILESYSTEM
+
+
+
+#if defined(_WIN32)
+#if !(defined(_AMD64_) || defined(_X86_) || defined(_ARM_))
+#if defined(__amd64__) || defined(__amd64) || defined(__x86_64__) || defined(__x86_64) || defined(_M_X64) || \
+ defined(_M_AMD64)
+#define _AMD64_
+#elif defined(i386) || defined(__i386) || defined(__i386__) || defined(__i386__) || defined(_M_IX86)
+#define _X86_
+#elif defined(__arm__) || defined(_M_ARM) || defined(_M_ARMT)
+#define _ARM_
+#elif defined(__aarch64__) || defined(_M_ARM64)
+#define _ARM64_
+#elif defined(_M_ARM64EC)
+#define _ARM64EC_
+#endif
+#endif
+
+// first
+#ifndef NOMINMAX
+// if NOMINMAX is already defined we don't want to mess with that either way
+#define NOMINMAX
+#include <windef.h>
+#undef NOMINMAX
+#else
+#include <windef.h>
+#endif
+
+// second
+#include <winbase.h>
+// third
+#include <processthreadsapi.h>
+#include <shellapi.h>
+#endif
+
+
+namespace CLI {
+
+
+/// Convert a wide string to a narrow string.
+CLI11_INLINE std::string narrow(const std::wstring &str);
+CLI11_INLINE std::string narrow(const wchar_t *str);
+CLI11_INLINE std::string narrow(const wchar_t *str, std::size_t size);
+
+/// Convert a narrow string to a wide string.
+CLI11_INLINE std::wstring widen(const std::string &str);
+CLI11_INLINE std::wstring widen(const char *str);
+CLI11_INLINE std::wstring widen(const char *str, std::size_t size);
+
+#ifdef CLI11_CPP17
+CLI11_INLINE std::string narrow(std::wstring_view str);
+CLI11_INLINE std::wstring widen(std::string_view str);
+#endif // CLI11_CPP17
+
+#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0
+/// Convert a char-string to a native path correctly.
+CLI11_INLINE std::filesystem::path to_path(std::string_view str);
+#endif // CLI11_HAS_FILESYSTEM
+
+
+
+
+namespace detail {
+
+#if !CLI11_HAS_CODECVT
+/// Attempt to set one of the acceptable unicode locales for conversion
+CLI11_INLINE void set_unicode_locale() {
+ static const std::array<const char *, 3> unicode_locales{{"C.UTF-8", "en_US.UTF-8", ".UTF-8"}};
+
+ for(const auto &locale_name : unicode_locales) {
+ if(std::setlocale(LC_ALL, locale_name) != nullptr) {
+ return;
+ }
+ }
+ throw std::runtime_error("CLI::narrow: could not set locale to C.UTF-8");
+}
+
+template <typename F> struct scope_guard_t {
+ F closure;
+
+ explicit scope_guard_t(F closure_) : closure(closure_) {}
+ ~scope_guard_t() { closure(); }
+};
+
+template <typename F> CLI11_NODISCARD CLI11_INLINE scope_guard_t<F> scope_guard(F &&closure) {
+ return scope_guard_t<F>{std::forward<F>(closure)};
+}
+
+#endif // !CLI11_HAS_CODECVT
+
+CLI11_DIAGNOSTIC_PUSH
+CLI11_DIAGNOSTIC_IGNORE_DEPRECATED
+
+CLI11_INLINE std::string narrow_impl(const wchar_t *str, std::size_t str_size) {
+#if CLI11_HAS_CODECVT
+#ifdef _WIN32
+ return std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>>().to_bytes(str, str + str_size);
+
+#else
+ return std::wstring_convert<std::codecvt_utf8<wchar_t>>().to_bytes(str, str + str_size);
+
+#endif // _WIN32
+#else // CLI11_HAS_CODECVT
+ (void)str_size;
+ std::mbstate_t state = std::mbstate_t();
+ const wchar_t *it = str;
+
+ std::string old_locale = std::setlocale(LC_ALL, nullptr);
+ auto sg = scope_guard([&] { std::setlocale(LC_ALL, old_locale.c_str()); });
+ set_unicode_locale();
+
+ std::size_t new_size = std::wcsrtombs(nullptr, &it, 0, &state);
+ if(new_size == static_cast<std::size_t>(-1)) {
+ throw std::runtime_error("CLI::narrow: conversion error in std::wcsrtombs at offset " +
+ std::to_string(it - str));
+ }
+ std::string result(new_size, '\0');
+ std::wcsrtombs(const_cast<char *>(result.data()), &str, new_size, &state);
+
+ return result;
+
+#endif // CLI11_HAS_CODECVT
+}
+
+CLI11_INLINE std::wstring widen_impl(const char *str, std::size_t str_size) {
+#if CLI11_HAS_CODECVT
+#ifdef _WIN32
+ return std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>>().from_bytes(str, str + str_size);
+
+#else
+ return std::wstring_convert<std::codecvt_utf8<wchar_t>>().from_bytes(str, str + str_size);
+
+#endif // _WIN32
+#else // CLI11_HAS_CODECVT
+ (void)str_size;
+ std::mbstate_t state = std::mbstate_t();
+ const char *it = str;
+
+ std::string old_locale = std::setlocale(LC_ALL, nullptr);
+ auto sg = scope_guard([&] { std::setlocale(LC_ALL, old_locale.c_str()); });
+ set_unicode_locale();
+
+ std::size_t new_size = std::mbsrtowcs(nullptr, &it, 0, &state);
+ if(new_size == static_cast<std::size_t>(-1)) {
+ throw std::runtime_error("CLI::widen: conversion error in std::mbsrtowcs at offset " +
+ std::to_string(it - str));
+ }
+ std::wstring result(new_size, L'\0');
+ std::mbsrtowcs(const_cast<wchar_t *>(result.data()), &str, new_size, &state);
+
+ return result;
+
+#endif // CLI11_HAS_CODECVT
+}
+
+CLI11_DIAGNOSTIC_POP
+
+} // namespace detail
+
+CLI11_INLINE std::string narrow(const wchar_t *str, std::size_t str_size) { return detail::narrow_impl(str, str_size); }
+CLI11_INLINE std::string narrow(const std::wstring &str) { return detail::narrow_impl(str.data(), str.size()); }
+// Flawfinder: ignore
+CLI11_INLINE std::string narrow(const wchar_t *str) { return detail::narrow_impl(str, std::wcslen(str)); }
+
+CLI11_INLINE std::wstring widen(const char *str, std::size_t str_size) { return detail::widen_impl(str, str_size); }
+CLI11_INLINE std::wstring widen(const std::string &str) { return detail::widen_impl(str.data(), str.size()); }
+// Flawfinder: ignore
+CLI11_INLINE std::wstring widen(const char *str) { return detail::widen_impl(str, std::strlen(str)); }
+
+#ifdef CLI11_CPP17
+CLI11_INLINE std::string narrow(std::wstring_view str) { return detail::narrow_impl(str.data(), str.size()); }
+CLI11_INLINE std::wstring widen(std::string_view str) { return detail::widen_impl(str.data(), str.size()); }
+#endif // CLI11_CPP17
+
+#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0
+CLI11_INLINE std::filesystem::path to_path(std::string_view str) {
+ return std::filesystem::path{
+#ifdef _WIN32
+ widen(str)
+#else
+ str
+#endif // _WIN32
+ };
+}
+#endif // CLI11_HAS_FILESYSTEM
+
+
+
+
+namespace detail {
+#ifdef _WIN32
+/// Decode and return UTF-8 argv from GetCommandLineW.
+CLI11_INLINE std::vector<std::string> compute_win32_argv();
+#endif
+} // namespace detail
+
+
+
+namespace detail {
+
+#ifdef _WIN32
+CLI11_INLINE std::vector<std::string> compute_win32_argv() {
+ std::vector<std::string> result;
+ int argc = 0;
+
+ auto deleter = [](wchar_t **ptr) { LocalFree(ptr); };
+ // NOLINTBEGIN(*-avoid-c-arrays)
+ auto wargv = std::unique_ptr<wchar_t *[], decltype(deleter)>(CommandLineToArgvW(GetCommandLineW(), &argc), deleter);
+ // NOLINTEND(*-avoid-c-arrays)
+
+ if(wargv == nullptr) {
+ throw std::runtime_error("CommandLineToArgvW failed with code " + std::to_string(GetLastError()));
+ }
+
+ result.reserve(static_cast<size_t>(argc));
+ for(size_t i = 0; i < static_cast<size_t>(argc); ++i) {
+ result.push_back(narrow(wargv[i]));
+ }
+
+ return result;
+}
+#endif
+
+} // namespace detail
+
+
+
+
+/// Include the items in this namespace to get free conversion of enums to/from streams.
+/// (This is available inside CLI as well, so CLI11 will use this without a using statement).
+namespace enums {
+
+/// output streaming for enumerations
+template <typename T, typename = typename std::enable_if<std::is_enum<T>::value>::type>
+std::ostream &operator<<(std::ostream &in, const T &item) {
+ // make sure this is out of the detail namespace otherwise it won't be found when needed
+ return in << static_cast<typename std::underlying_type<T>::type>(item);
+}
+
+} // namespace enums
+
+/// Export to CLI namespace
+using enums::operator<<;
+
+namespace detail {
+/// a constant defining an expected max vector size defined to be a big number that could be multiplied by 4 and not
+/// produce overflow for some expected uses
+constexpr int expected_max_vector_size{1 << 29};
+// Based on http://stackoverflow.com/questions/236129/split-a-string-in-c
+/// Split a string by a delim
+CLI11_INLINE std::vector<std::string> split(const std::string &s, char delim);
+
+/// Simple function to join a string
+template <typename T> std::string join(const T &v, std::string delim = ",") {
+ std::ostringstream s;
+ auto beg = std::begin(v);
+ auto end = std::end(v);
+ if(beg != end)
+ s << *beg++;
+ while(beg != end) {
+ s << delim << *beg++;
+ }
+ return s.str();
+}
+
+/// Simple function to join a string from processed elements
+template <typename T,
+ typename Callable,
+ typename = typename std::enable_if<!std::is_constructible<std::string, Callable>::value>::type>
+std::string join(const T &v, Callable func, std::string delim = ",") {
+ std::ostringstream s;
+ auto beg = std::begin(v);
+ auto end = std::end(v);
+ auto loc = s.tellp();
+ while(beg != end) {
+ auto nloc = s.tellp();
+ if(nloc > loc) {
+ s << delim;
+ loc = nloc;
+ }
+ s << func(*beg++);
+ }
+ return s.str();
+}
+
+/// Join a string in reverse order
+template <typename T> std::string rjoin(const T &v, std::string delim = ",") {
+ std::ostringstream s;
+ for(std::size_t start = 0; start < v.size(); start++) {
+ if(start > 0)
+ s << delim;
+ s << v[v.size() - start - 1];
+ }
+ return s.str();
+}
+
+// Based roughly on http://stackoverflow.com/questions/25829143/c-trim-whitespace-from-a-string
+
+/// Trim whitespace from left of string
+CLI11_INLINE std::string <rim(std::string &str);
+
+/// Trim anything from left of string
+CLI11_INLINE std::string <rim(std::string &str, const std::string &filter);
+
+/// Trim whitespace from right of string
+CLI11_INLINE std::string &rtrim(std::string &str);
+
+/// Trim anything from right of string
+CLI11_INLINE std::string &rtrim(std::string &str, const std::string &filter);
+
+/// Trim whitespace from string
+inline std::string &trim(std::string &str) { return ltrim(rtrim(str)); }
+
+/// Trim anything from string
+inline std::string &trim(std::string &str, const std::string filter) { return ltrim(rtrim(str, filter), filter); }
+
+/// Make a copy of the string and then trim it
+inline std::string trim_copy(const std::string &str) {
+ std::string s = str;
+ return trim(s);
+}
+
+/// remove quotes at the front and back of a string either '"' or '\''
+CLI11_INLINE std::string &remove_quotes(std::string &str);
+
+/// remove quotes from all elements of a string vector and process escaped components
+CLI11_INLINE void remove_quotes(std::vector<std::string> &args);
+
+/// Add a leader to the beginning of all new lines (nothing is added
+/// at the start of the first line). `"; "` would be for ini files
+///
+/// Can't use Regex, or this would be a subs.
+CLI11_INLINE std::string fix_newlines(const std::string &leader, std::string input);
+
+/// Make a copy of the string and then trim it, any filter string can be used (any char in string is filtered)
+inline std::string trim_copy(const std::string &str, const std::string &filter) {
+ std::string s = str;
+ return trim(s, filter);
+}
+/// Print a two part "help" string
+CLI11_INLINE std::ostream &
+format_help(std::ostream &out, std::string name, const std::string &description, std::size_t wid);
+
+/// Print subcommand aliases
+CLI11_INLINE std::ostream &format_aliases(std::ostream &out, const std::vector<std::string> &aliases, std::size_t wid);
+
+/// Verify the first character of an option
+/// - is a trigger character, ! has special meaning and new lines would just be annoying to deal with
+template <typename T> bool valid_first_char(T c) {
+ return ((c != '-') && (static_cast<unsigned char>(c) > 33)); // space and '!' not allowed
+}
+
+/// Verify following characters of an option
+template <typename T> bool valid_later_char(T c) {
+ // = and : are value separators, { has special meaning for option defaults,
+ // and control codes other than tab would just be annoying to deal with in many places allowing space here has too
+ // much potential for inadvertent entry errors and bugs
+ return ((c != '=') && (c != ':') && (c != '{') && ((static_cast<unsigned char>(c) > 32) || c == '\t'));
+}
+
+/// Verify an option/subcommand name
+CLI11_INLINE bool valid_name_string(const std::string &str);
+
+/// Verify an app name
+inline bool valid_alias_name_string(const std::string &str) {
+ static const std::string badChars(std::string("\n") + '\0');
+ return (str.find_first_of(badChars) == std::string::npos);
+}
+
+/// check if a string is a container segment separator (empty or "%%")
+inline bool is_separator(const std::string &str) {
+ static const std::string sep("%%");
+ return (str.empty() || str == sep);
+}
+
+/// Verify that str consists of letters only
+inline bool isalpha(const std::string &str) {
+ return std::all_of(str.begin(), str.end(), [](char c) { return std::isalpha(c, std::locale()); });
+}
+
+/// Return a lower case version of a string
+inline std::string to_lower(std::string str) {
+ std::transform(std::begin(str), std::end(str), std::begin(str), [](const std::string::value_type &x) {
+ return std::tolower(x, std::locale());
+ });
+ return str;
+}
+
+/// remove underscores from a string
+inline std::string remove_underscore(std::string str) {
+ str.erase(std::remove(std::begin(str), std::end(str), '_'), std::end(str));
+ return str;
+}
+
+/// Find and replace a substring with another substring
+CLI11_INLINE std::string find_and_replace(std::string str, std::string from, std::string to);
+
+/// check if the flag definitions has possible false flags
+inline bool has_default_flag_values(const std::string &flags) {
+ return (flags.find_first_of("{!") != std::string::npos);
+}
+
+CLI11_INLINE void remove_default_flag_values(std::string &flags);
+
+/// Check if a string is a member of a list of strings and optionally ignore case or ignore underscores
+CLI11_INLINE std::ptrdiff_t find_member(std::string name,
+ const std::vector<std::string> names,
+ bool ignore_case = false,
+ bool ignore_underscore = false);
+
+/// Find a trigger string and call a modify callable function that takes the current string and starting position of the
+/// trigger and returns the position in the string to search for the next trigger string
+template <typename Callable> inline std::string find_and_modify(std::string str, std::string trigger, Callable modify) {
+ std::size_t start_pos = 0;
+ while((start_pos = str.find(trigger, start_pos)) != std::string::npos) {
+ start_pos = modify(str, start_pos);
+ }
+ return str;
+}
+
+/// close a sequence of characters indicated by a closure character. Brackets allows sub sequences
+/// recognized bracket sequences include "'`[(<{ other closure characters are assumed to be literal strings
+CLI11_INLINE std::size_t close_sequence(const std::string &str, std::size_t start, char closure_char);
+
+/// Split a string '"one two" "three"' into 'one two', 'three'
+/// Quote characters can be ` ' or " or bracket characters [{(< with matching to the matching bracket
+CLI11_INLINE std::vector<std::string> split_up(std::string str, char delimiter = '\0');
+
+/// get the value of an environmental variable or empty string if empty
+CLI11_INLINE std::string get_environment_value(const std::string &env_name);
+
+/// This function detects an equal or colon followed by an escaped quote after an argument
+/// then modifies the string to replace the equality with a space. This is needed
+/// to allow the split up function to work properly and is intended to be used with the find_and_modify function
+/// the return value is the offset+1 which is required by the find_and_modify function.
+CLI11_INLINE std::size_t escape_detect(std::string &str, std::size_t offset);
+
+/// @brief detect if a string has escapable characters
+/// @param str the string to do the detection on
+/// @return true if the string has escapable characters
+CLI11_INLINE bool has_escapable_character(const std::string &str);
+
+/// @brief escape all escapable characters
+/// @param str the string to escape
+/// @return a string with the escapble characters escaped with '\'
+CLI11_INLINE std::string add_escaped_characters(const std::string &str);
+
+/// @brief replace the escaped characters with their equivalent
+CLI11_INLINE std::string remove_escaped_characters(const std::string &str);
+
+/// generate a string with all non printable characters escaped to hex codes
+CLI11_INLINE std::string binary_escape_string(const std::string &string_to_escape);
+
+CLI11_INLINE bool is_binary_escaped_string(const std::string &escaped_string);
+
+/// extract an escaped binary_string
+CLI11_INLINE std::string extract_binary_string(const std::string &escaped_string);
+
+/// process a quoted string, remove the quotes and if appropriate handle escaped characters
+CLI11_INLINE bool process_quoted_string(std::string &str, char string_char = '\"', char literal_char = '\'');
+
+} // namespace detail
+
+
+
+
+namespace detail {
+CLI11_INLINE std::vector<std::string> split(const std::string &s, char delim) {
+ std::vector<std::string> elems;
+ // Check to see if empty string, give consistent result
+ if(s.empty()) {
+ elems.emplace_back();
+ } else {
+ std::stringstream ss;
+ ss.str(s);
+ std::string item;
+ while(std::getline(ss, item, delim)) {
+ elems.push_back(item);
+ }
+ }
+ return elems;
+}
+
+CLI11_INLINE std::string <rim(std::string &str) {
+ auto it = std::find_if(str.begin(), str.end(), [](char ch) { return !std::isspace<char>(ch, std::locale()); });
+ str.erase(str.begin(), it);
+ return str;
+}
+
+CLI11_INLINE std::string <rim(std::string &str, const std::string &filter) {
+ auto it = std::find_if(str.begin(), str.end(), [&filter](char ch) { return filter.find(ch) == std::string::npos; });
+ str.erase(str.begin(), it);
+ return str;
+}
+
+CLI11_INLINE std::string &rtrim(std::string &str) {
+ auto it = std::find_if(str.rbegin(), str.rend(), [](char ch) { return !std::isspace<char>(ch, std::locale()); });
+ str.erase(it.base(), str.end());
+ return str;
+}
+
+CLI11_INLINE std::string &rtrim(std::string &str, const std::string &filter) {
+ auto it =
+ std::find_if(str.rbegin(), str.rend(), [&filter](char ch) { return filter.find(ch) == std::string::npos; });
+ str.erase(it.base(), str.end());
+ return str;
+}
+
+CLI11_INLINE std::string &remove_quotes(std::string &str) {
+ if(str.length() > 1 && (str.front() == '"' || str.front() == '\'' || str.front() == '`')) {
+ if(str.front() == str.back()) {
+ str.pop_back();
+ str.erase(str.begin(), str.begin() + 1);
+ }
+ }
+ return str;
+}
+
+CLI11_INLINE std::string &remove_outer(std::string &str, char key) {
+ if(str.length() > 1 && (str.front() == key)) {
+ if(str.front() == str.back()) {
+ str.pop_back();
+ str.erase(str.begin(), str.begin() + 1);
+ }
+ }
+ return str;
+}
+
+CLI11_INLINE std::string fix_newlines(const std::string &leader, std::string input) {
+ std::string::size_type n = 0;
+ while(n != std::string::npos && n < input.size()) {
+ n = input.find('\n', n);
+ if(n != std::string::npos) {
+ input = input.substr(0, n + 1) + leader + input.substr(n + 1);
+ n += leader.size();
+ }
+ }
+ return input;
+}
+
+CLI11_INLINE std::ostream &
+format_help(std::ostream &out, std::string name, const std::string &description, std::size_t wid) {
+ name = " " + name;
+ out << std::setw(static_cast<int>(wid)) << std::left << name;
+ if(!description.empty()) {
+ if(name.length() >= wid)
+ out << "\n" << std::setw(static_cast<int>(wid)) << "";
+ for(const char c : description) {
+ out.put(c);
+ if(c == '\n') {
+ out << std::setw(static_cast<int>(wid)) << "";
+ }
+ }
+ }
+ out << "\n";
+ return out;
+}
+
+CLI11_INLINE std::ostream &format_aliases(std::ostream &out, const std::vector<std::string> &aliases, std::size_t wid) {
+ if(!aliases.empty()) {
+ out << std::setw(static_cast<int>(wid)) << " aliases: ";
+ bool front = true;
+ for(const auto &alias : aliases) {
+ if(!front) {
+ out << ", ";
+ } else {
+ front = false;
+ }
+ out << detail::fix_newlines(" ", alias);
+ }
+ out << "\n";
+ }
+ return out;
+}
+
+CLI11_INLINE bool valid_name_string(const std::string &str) {
+ if(str.empty() || !valid_first_char(str[0])) {
+ return false;
+ }
+ auto e = str.end();
+ for(auto c = str.begin() + 1; c != e; ++c)
+ if(!valid_later_char(*c))
+ return false;
+ return true;
+}
+
+CLI11_INLINE std::string find_and_replace(std::string str, std::string from, std::string to) {
+
+ std::size_t start_pos = 0;
+
+ while((start_pos = str.find(from, start_pos)) != std::string::npos) {
+ str.replace(start_pos, from.length(), to);
+ start_pos += to.length();
+ }
+
+ return str;
+}
+
+CLI11_INLINE void remove_default_flag_values(std::string &flags) {
+ auto loc = flags.find_first_of('{', 2);
+ while(loc != std::string::npos) {
+ auto finish = flags.find_first_of("},", loc + 1);
+ if((finish != std::string::npos) && (flags[finish] == '}')) {
+ flags.erase(flags.begin() + static_cast<std::ptrdiff_t>(loc),
+ flags.begin() + static_cast<std::ptrdiff_t>(finish) + 1);
+ }
+ loc = flags.find_first_of('{', loc + 1);
+ }
+ flags.erase(std::remove(flags.begin(), flags.end(), '!'), flags.end());
+}
+
+CLI11_INLINE std::ptrdiff_t
+find_member(std::string name, const std::vector<std::string> names, bool ignore_case, bool ignore_underscore) {
+ auto it = std::end(names);
+ if(ignore_case) {
+ if(ignore_underscore) {
+ name = detail::to_lower(detail::remove_underscore(name));
+ it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) {
+ return detail::to_lower(detail::remove_underscore(local_name)) == name;
+ });
+ } else {
+ name = detail::to_lower(name);
+ it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) {
+ return detail::to_lower(local_name) == name;
+ });
+ }
+
+ } else if(ignore_underscore) {
+ name = detail::remove_underscore(name);
+ it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) {
+ return detail::remove_underscore(local_name) == name;
+ });
+ } else {
+ it = std::find(std::begin(names), std::end(names), name);
+ }
+
+ return (it != std::end(names)) ? (it - std::begin(names)) : (-1);
+}
+
+static const std::string escapedChars("\b\t\n\f\r\"\\");
+static const std::string escapedCharsCode("btnfr\"\\");
+static const std::string bracketChars{"\"'`[(<{"};
+static const std::string matchBracketChars("\"'`])>}");
+
+CLI11_INLINE bool has_escapable_character(const std::string &str) {
+ return (str.find_first_of(escapedChars) != std::string::npos);
+}
+
+CLI11_INLINE std::string add_escaped_characters(const std::string &str) {
+ std::string out;
+ out.reserve(str.size() + 4);
+ for(char s : str) {
+ auto sloc = escapedChars.find_first_of(s);
+ if(sloc != std::string::npos) {
+ out.push_back('\\');
+ out.push_back(escapedCharsCode[sloc]);
+ } else {
+ out.push_back(s);
+ }
+ }
+ return out;
+}
+
+CLI11_INLINE std::uint32_t hexConvert(char hc) {
+ int hcode{0};
+ if(hc >= '0' && hc <= '9') {
+ hcode = (hc - '0');
+ } else if(hc >= 'A' && hc <= 'F') {
+ hcode = (hc - 'A' + 10);
+ } else if(hc >= 'a' && hc <= 'f') {
+ hcode = (hc - 'a' + 10);
+ } else {
+ hcode = -1;
+ }
+ return static_cast<uint32_t>(hcode);
+}
+
+CLI11_INLINE char make_char(std::uint32_t code) { return static_cast<char>(static_cast<unsigned char>(code)); }
+
+CLI11_INLINE void append_codepoint(std::string &str, std::uint32_t code) {
+ if(code < 0x80) { // ascii code equivalent
+ str.push_back(static_cast<char>(code));
+ } else if(code < 0x800) { // \u0080 to \u07FF
+ // 110yyyyx 10xxxxxx; 0x3f == 0b0011'1111
+ str.push_back(make_char(0xC0 | code >> 6));
+ str.push_back(make_char(0x80 | (code & 0x3F)));
+ } else if(code < 0x10000) { // U+0800...U+FFFF
+ if(0xD800 <= code && code <= 0xDFFF) {
+ throw std::invalid_argument("[0xD800, 0xDFFF] are not valid UTF-8.");
+ }
+ // 1110yyyy 10yxxxxx 10xxxxxx
+ str.push_back(make_char(0xE0 | code >> 12));
+ str.push_back(make_char(0x80 | (code >> 6 & 0x3F)));
+ str.push_back(make_char(0x80 | (code & 0x3F)));
+ } else if(code < 0x110000) { // U+010000 ... U+10FFFF
+ // 11110yyy 10yyxxxx 10xxxxxx 10xxxxxx
+ str.push_back(make_char(0xF0 | code >> 18));
+ str.push_back(make_char(0x80 | (code >> 12 & 0x3F)));
+ str.push_back(make_char(0x80 | (code >> 6 & 0x3F)));
+ str.push_back(make_char(0x80 | (code & 0x3F)));
+ }
+}
+
+CLI11_INLINE std::string remove_escaped_characters(const std::string &str) {
+
+ std::string out;
+ out.reserve(str.size());
+ for(auto loc = str.begin(); loc < str.end(); ++loc) {
+ if(*loc == '\\') {
+ if(str.end() - loc < 2) {
+ throw std::invalid_argument("invalid escape sequence " + str);
+ }
+ auto ecloc = escapedCharsCode.find_first_of(*(loc + 1));
+ if(ecloc != std::string::npos) {
+ out.push_back(escapedChars[ecloc]);
+ ++loc;
+ } else if(*(loc + 1) == 'u') {
+ // must have 4 hex characters
+ if(str.end() - loc < 6) {
+ throw std::invalid_argument("unicode sequence must have 4 hex codes " + str);
+ }
+ std::uint32_t code{0};
+ std::uint32_t mplier{16 * 16 * 16};
+ for(int ii = 2; ii < 6; ++ii) {
+ std::uint32_t res = hexConvert(*(loc + ii));
+ if(res > 0x0F) {
+ throw std::invalid_argument("unicode sequence must have 4 hex codes " + str);
+ }
+ code += res * mplier;
+ mplier = mplier / 16;
+ }
+ append_codepoint(out, code);
+ loc += 5;
+ } else if(*(loc + 1) == 'U') {
+ // must have 8 hex characters
+ if(str.end() - loc < 10) {
+ throw std::invalid_argument("unicode sequence must have 8 hex codes " + str);
+ }
+ std::uint32_t code{0};
+ std::uint32_t mplier{16 * 16 * 16 * 16 * 16 * 16 * 16};
+ for(int ii = 2; ii < 10; ++ii) {
+ std::uint32_t res = hexConvert(*(loc + ii));
+ if(res > 0x0F) {
+ throw std::invalid_argument("unicode sequence must have 8 hex codes " + str);
+ }
+ code += res * mplier;
+ mplier = mplier / 16;
+ }
+ append_codepoint(out, code);
+ loc += 9;
+ } else if(*(loc + 1) == '0') {
+ out.push_back('\0');
+ ++loc;
+ } else {
+ throw std::invalid_argument(std::string("unrecognized escape sequence \\") + *(loc + 1) + " in " + str);
+ }
+ } else {
+ out.push_back(*loc);
+ }
+ }
+ return out;
+}
+
+CLI11_INLINE std::size_t close_string_quote(const std::string &str, std::size_t start, char closure_char) {
+ std::size_t loc{0};
+ for(loc = start + 1; loc < str.size(); ++loc) {
+ if(str[loc] == closure_char) {
+ break;
+ }
+ if(str[loc] == '\\') {
+ // skip the next character for escaped sequences
+ ++loc;
+ }
+ }
+ return loc;
+}
+
+CLI11_INLINE std::size_t close_literal_quote(const std::string &str, std::size_t start, char closure_char) {
+ auto loc = str.find_first_of(closure_char, start + 1);
+ return (loc != std::string::npos ? loc : str.size());
+}
+
+CLI11_INLINE std::size_t close_sequence(const std::string &str, std::size_t start, char closure_char) {
+
+ auto bracket_loc = matchBracketChars.find(closure_char);
+ switch(bracket_loc) {
+ case 0:
+ return close_string_quote(str, start, closure_char);
+ case 1:
+ case 2:
+ case std::string::npos:
+ return close_literal_quote(str, start, closure_char);
+ default:
+ break;
+ }
+
+ std::string closures(1, closure_char);
+ auto loc = start + 1;
+
+ while(loc < str.size()) {
+ if(str[loc] == closures.back()) {
+ closures.pop_back();
+ if(closures.empty()) {
+ return loc;
+ }
+ }
+ bracket_loc = bracketChars.find(str[loc]);
+ if(bracket_loc != std::string::npos) {
+ switch(bracket_loc) {
+ case 0:
+ loc = close_string_quote(str, loc, str[loc]);
+ break;
+ case 1:
+ case 2:
+ loc = close_literal_quote(str, loc, str[loc]);
+ break;
+ default:
+ closures.push_back(matchBracketChars[bracket_loc]);
+ break;
+ }
+ }
+ ++loc;
+ }
+ if(loc > str.size()) {
+ loc = str.size();
+ }
+ return loc;
+}
+
+CLI11_INLINE std::vector<std::string> split_up(std::string str, char delimiter) {
+
+ auto find_ws = [delimiter](char ch) {
+ return (delimiter == '\0') ? std::isspace<char>(ch, std::locale()) : (ch == delimiter);
+ };
+ trim(str);
+
+ std::vector<std::string> output;
+ while(!str.empty()) {
+ if(bracketChars.find_first_of(str[0]) != std::string::npos) {
+ auto bracketLoc = bracketChars.find_first_of(str[0]);
+ auto end = close_sequence(str, 0, matchBracketChars[bracketLoc]);
+ if(end >= str.size()) {
+ output.push_back(std::move(str));
+ str.clear();
+ } else {
+ output.push_back(str.substr(0, end + 1));
+ if(end + 2 < str.size()) {
+ str = str.substr(end + 2);
+ } else {
+ str.clear();
+ }
+ }
+
+ } else {
+ auto it = std::find_if(std::begin(str), std::end(str), find_ws);
+ if(it != std::end(str)) {
+ std::string value = std::string(str.begin(), it);
+ output.push_back(value);
+ str = std::string(it + 1, str.end());
+ } else {
+ output.push_back(str);
+ str.clear();
+ }
+ }
+ trim(str);
+ }
+ return output;
+}
+
+CLI11_INLINE std::size_t escape_detect(std::string &str, std::size_t offset) {
+ auto next = str[offset + 1];
+ if((next == '\"') || (next == '\'') || (next == '`')) {
+ auto astart = str.find_last_of("-/ \"\'`", offset - 1);
+ if(astart != std::string::npos) {
+ if(str[astart] == ((str[offset] == '=') ? '-' : '/'))
+ str[offset] = ' '; // interpret this as a space so the split_up works properly
+ }
+ }
+ return offset + 1;
+}
+
+CLI11_INLINE std::string binary_escape_string(const std::string &string_to_escape) {
+ // s is our escaped output string
+ std::string escaped_string{};
+ // loop through all characters
+ for(char c : string_to_escape) {
+ // check if a given character is printable
+ // the cast is necessary to avoid undefined behaviour
+ if(isprint(static_cast<unsigned char>(c)) == 0) {
+ std::stringstream stream;
+ // if the character is not printable
+ // we'll convert it to a hex string using a stringstream
+ // note that since char is signed we have to cast it to unsigned first
+ stream << std::hex << static_cast<unsigned int>(static_cast<unsigned char>(c));
+ std::string code = stream.str();
+ escaped_string += std::string("\\x") + (code.size() < 2 ? "0" : "") + code;
+
+ } else {
+ escaped_string.push_back(c);
+ }
+ }
+ if(escaped_string != string_to_escape) {
+ auto sqLoc = escaped_string.find('\'');
+ while(sqLoc != std::string::npos) {
+ escaped_string.replace(sqLoc, sqLoc + 1, "\\x27");
+ sqLoc = escaped_string.find('\'');
+ }
+ escaped_string.insert(0, "'B\"(");
+ escaped_string.push_back(')');
+ escaped_string.push_back('"');
+ escaped_string.push_back('\'');
+ }
+ return escaped_string;
+}
+
+CLI11_INLINE bool is_binary_escaped_string(const std::string &escaped_string) {
+ size_t ssize = escaped_string.size();
+ if(escaped_string.compare(0, 3, "B\"(") == 0 && escaped_string.compare(ssize - 2, 2, ")\"") == 0) {
+ return true;
+ }
+ return (escaped_string.compare(0, 4, "'B\"(") == 0 && escaped_string.compare(ssize - 3, 3, ")\"'") == 0);
+}
+
+CLI11_INLINE std::string extract_binary_string(const std::string &escaped_string) {
+ std::size_t start{0};
+ std::size_t tail{0};
+ size_t ssize = escaped_string.size();
+ if(escaped_string.compare(0, 3, "B\"(") == 0 && escaped_string.compare(ssize - 2, 2, ")\"") == 0) {
+ start = 3;
+ tail = 2;
+ } else if(escaped_string.compare(0, 4, "'B\"(") == 0 && escaped_string.compare(ssize - 3, 3, ")\"'") == 0) {
+ start = 4;
+ tail = 3;
+ }
+
+ if(start == 0) {
+ return escaped_string;
+ }
+ std::string outstring;
+
+ outstring.reserve(ssize - start - tail);
+ std::size_t loc = start;
+ while(loc < ssize - tail) {
+ // ssize-2 to skip )" at the end
+ if(escaped_string[loc] == '\\' && (escaped_string[loc + 1] == 'x' || escaped_string[loc + 1] == 'X')) {
+ auto c1 = escaped_string[loc + 2];
+ auto c2 = escaped_string[loc + 3];
+
+ std::uint32_t res1 = hexConvert(c1);
+ std::uint32_t res2 = hexConvert(c2);
+ if(res1 <= 0x0F && res2 <= 0x0F) {
+ loc += 4;
+ outstring.push_back(static_cast<char>(res1 * 16 + res2));
+ continue;
+ }
+ }
+ outstring.push_back(escaped_string[loc]);
+ ++loc;
+ }
+ return outstring;
+}
+
+CLI11_INLINE void remove_quotes(std::vector<std::string> &args) {
+ for(auto &arg : args) {
+ if(arg.front() == '\"' && arg.back() == '\"') {
+ remove_quotes(arg);
+ // only remove escaped for string arguments not literal strings
+ arg = remove_escaped_characters(arg);
+ } else {
+ remove_quotes(arg);
+ }
+ }
+}
+
+CLI11_INLINE bool process_quoted_string(std::string &str, char string_char, char literal_char) {
+ if(str.size() <= 1) {
+ return false;
+ }
+ if(detail::is_binary_escaped_string(str)) {
+ str = detail::extract_binary_string(str);
+ return true;
+ }
+ if(str.front() == string_char && str.back() == string_char) {
+ detail::remove_outer(str, string_char);
+ if(str.find_first_of('\\') != std::string::npos) {
+ str = detail::remove_escaped_characters(str);
+ }
+ return true;
+ }
+ if((str.front() == literal_char || str.front() == '`') && str.back() == str.front()) {
+ detail::remove_outer(str, str.front());
+ return true;
+ }
+ return false;
+}
+
+std::string get_environment_value(const std::string &env_name) {
+ char *buffer = nullptr;
+ std::string ename_string;
+
+#ifdef _MSC_VER
+ // Windows version
+ std::size_t sz = 0;
+ if(_dupenv_s(&buffer, &sz, env_name.c_str()) == 0 && buffer != nullptr) {
+ ename_string = std::string(buffer);
+ free(buffer);
+ }
+#else
+ // This also works on Windows, but gives a warning
+ buffer = std::getenv(env_name.c_str());
+ if(buffer != nullptr) {
+ ename_string = std::string(buffer);
+ }
+#endif
+ return ename_string;
+}
+
+} // namespace detail
+
+
+
+// Use one of these on all error classes.
+// These are temporary and are undef'd at the end of this file.
+#define CLI11_ERROR_DEF(parent, name) \
+ protected: \
+ name(std::string ename, std::string msg, int exit_code) : parent(std::move(ename), std::move(msg), exit_code) {} \
+ name(std::string ename, std::string msg, ExitCodes exit_code) \
+ : parent(std::move(ename), std::move(msg), exit_code) {} \
+ \
+ public: \
+ name(std::string msg, ExitCodes exit_code) : parent(#name, std::move(msg), exit_code) {} \
+ name(std::string msg, int exit_code) : parent(#name, std::move(msg), exit_code) {}
+
+// This is added after the one above if a class is used directly and builds its own message
+#define CLI11_ERROR_SIMPLE(name) \
+ explicit name(std::string msg) : name(#name, msg, ExitCodes::name) {}
+
+/// These codes are part of every error in CLI. They can be obtained from e using e.exit_code or as a quick shortcut,
+/// int values from e.get_error_code().
+enum class ExitCodes {
+ Success = 0,
+ IncorrectConstruction = 100,
+ BadNameString,
+ OptionAlreadyAdded,
+ FileError,
+ ConversionError,
+ ValidationError,
+ RequiredError,
+ RequiresError,
+ ExcludesError,
+ ExtrasError,
+ ConfigError,
+ InvalidError,
+ HorribleError,
+ OptionNotFound,
+ ArgumentMismatch,
+ BaseClass = 127
+};
+
+// Error definitions
+
+/// @defgroup error_group Errors
+/// @brief Errors thrown by CLI11
+///
+/// These are the errors that can be thrown. Some of them, like CLI::Success, are not really errors.
+/// @{
+
+/// All errors derive from this one
+class Error : public std::runtime_error {
+ int actual_exit_code;
+ std::string error_name{"Error"};
+
+ public:
+ CLI11_NODISCARD int get_exit_code() const { return actual_exit_code; }
+
+ CLI11_NODISCARD std::string get_name() const { return error_name; }
+
+ Error(std::string name, std::string msg, int exit_code = static_cast<int>(ExitCodes::BaseClass))
+ : runtime_error(msg), actual_exit_code(exit_code), error_name(std::move(name)) {}
+
+ Error(std::string name, std::string msg, ExitCodes exit_code) : Error(name, msg, static_cast<int>(exit_code)) {}
+};
+
+// Note: Using Error::Error constructors does not work on GCC 4.7
+
+/// Construction errors (not in parsing)
+class ConstructionError : public Error {
+ CLI11_ERROR_DEF(Error, ConstructionError)
+};
+
+/// Thrown when an option is set to conflicting values (non-vector and multi args, for example)
+class IncorrectConstruction : public ConstructionError {
+ CLI11_ERROR_DEF(ConstructionError, IncorrectConstruction)
+ CLI11_ERROR_SIMPLE(IncorrectConstruction)
+ static IncorrectConstruction PositionalFlag(std::string name) {
+ return IncorrectConstruction(name + ": Flags cannot be positional");
+ }
+ static IncorrectConstruction Set0Opt(std::string name) {
+ return IncorrectConstruction(name + ": Cannot set 0 expected, use a flag instead");
+ }
+ static IncorrectConstruction SetFlag(std::string name) {
+ return IncorrectConstruction(name + ": Cannot set an expected number for flags");
+ }
+ static IncorrectConstruction ChangeNotVector(std::string name) {
+ return IncorrectConstruction(name + ": You can only change the expected arguments for vectors");
+ }
+ static IncorrectConstruction AfterMultiOpt(std::string name) {
+ return IncorrectConstruction(
+ name + ": You can't change expected arguments after you've changed the multi option policy!");
+ }
+ static IncorrectConstruction MissingOption(std::string name) {
+ return IncorrectConstruction("Option " + name + " is not defined");
+ }
+ static IncorrectConstruction MultiOptionPolicy(std::string name) {
+ return IncorrectConstruction(name + ": multi_option_policy only works for flags and exact value options");
+ }
+};
+
+/// Thrown on construction of a bad name
+class BadNameString : public ConstructionError {
+ CLI11_ERROR_DEF(ConstructionError, BadNameString)
+ CLI11_ERROR_SIMPLE(BadNameString)
+ static BadNameString OneCharName(std::string name) { return BadNameString("Invalid one char name: " + name); }
+ static BadNameString MissingDash(std::string name) {
+ return BadNameString("Long names strings require 2 dashes " + name);
+ }
+ static BadNameString BadLongName(std::string name) { return BadNameString("Bad long name: " + name); }
+ static BadNameString BadPositionalName(std::string name) {
+ return BadNameString("Invalid positional Name: " + name);
+ }
+ static BadNameString DashesOnly(std::string name) {
+ return BadNameString("Must have a name, not just dashes: " + name);
+ }
+ static BadNameString MultiPositionalNames(std::string name) {
+ return BadNameString("Only one positional name allowed, remove: " + name);
+ }
+};
+
+/// Thrown when an option already exists
+class OptionAlreadyAdded : public ConstructionError {
+ CLI11_ERROR_DEF(ConstructionError, OptionAlreadyAdded)
+ explicit OptionAlreadyAdded(std::string name)
+ : OptionAlreadyAdded(name + " is already added", ExitCodes::OptionAlreadyAdded) {}
+ static OptionAlreadyAdded Requires(std::string name, std::string other) {
+ return {name + " requires " + other, ExitCodes::OptionAlreadyAdded};
+ }
+ static OptionAlreadyAdded Excludes(std::string name, std::string other) {
+ return {name + " excludes " + other, ExitCodes::OptionAlreadyAdded};
+ }
+};
+
+// Parsing errors
+
+/// Anything that can error in Parse
+class ParseError : public Error {
+ CLI11_ERROR_DEF(Error, ParseError)
+};
+
+// Not really "errors"
+
+/// This is a successful completion on parsing, supposed to exit
+class Success : public ParseError {
+ CLI11_ERROR_DEF(ParseError, Success)
+ Success() : Success("Successfully completed, should be caught and quit", ExitCodes::Success) {}
+};
+
+/// -h or --help on command line
+class CallForHelp : public Success {
+ CLI11_ERROR_DEF(Success, CallForHelp)
+ CallForHelp() : CallForHelp("This should be caught in your main function, see examples", ExitCodes::Success) {}
+};
+
+/// Usually something like --help-all on command line
+class CallForAllHelp : public Success {
+ CLI11_ERROR_DEF(Success, CallForAllHelp)
+ CallForAllHelp()
+ : CallForAllHelp("This should be caught in your main function, see examples", ExitCodes::Success) {}
+};
+
+/// -v or --version on command line
+class CallForVersion : public Success {
+ CLI11_ERROR_DEF(Success, CallForVersion)
+ CallForVersion()
+ : CallForVersion("This should be caught in your main function, see examples", ExitCodes::Success) {}
+};
+
+/// Does not output a diagnostic in CLI11_PARSE, but allows main() to return with a specific error code.
+class RuntimeError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, RuntimeError)
+ explicit RuntimeError(int exit_code = 1) : RuntimeError("Runtime error", exit_code) {}
+};
+
+/// Thrown when parsing an INI file and it is missing
+class FileError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, FileError)
+ CLI11_ERROR_SIMPLE(FileError)
+ static FileError Missing(std::string name) { return FileError(name + " was not readable (missing?)"); }
+};
+
+/// Thrown when conversion call back fails, such as when an int fails to coerce to a string
+class ConversionError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, ConversionError)
+ CLI11_ERROR_SIMPLE(ConversionError)
+ ConversionError(std::string member, std::string name)
+ : ConversionError("The value " + member + " is not an allowed value for " + name) {}
+ ConversionError(std::string name, std::vector<std::string> results)
+ : ConversionError("Could not convert: " + name + " = " + detail::join(results)) {}
+ static ConversionError TooManyInputsFlag(std::string name) {
+ return ConversionError(name + ": too many inputs for a flag");
+ }
+ static ConversionError TrueFalse(std::string name) {
+ return ConversionError(name + ": Should be true/false or a number");
+ }
+};
+
+/// Thrown when validation of results fails
+class ValidationError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, ValidationError)
+ CLI11_ERROR_SIMPLE(ValidationError)
+ explicit ValidationError(std::string name, std::string msg) : ValidationError(name + ": " + msg) {}
+};
+
+/// Thrown when a required option is missing
+class RequiredError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, RequiredError)
+ explicit RequiredError(std::string name) : RequiredError(name + " is required", ExitCodes::RequiredError) {}
+ static RequiredError Subcommand(std::size_t min_subcom) {
+ if(min_subcom == 1) {
+ return RequiredError("A subcommand");
+ }
+ return {"Requires at least " + std::to_string(min_subcom) + " subcommands", ExitCodes::RequiredError};
+ }
+ static RequiredError
+ Option(std::size_t min_option, std::size_t max_option, std::size_t used, const std::string &option_list) {
+ if((min_option == 1) && (max_option == 1) && (used == 0))
+ return RequiredError("Exactly 1 option from [" + option_list + "]");
+ if((min_option == 1) && (max_option == 1) && (used > 1)) {
+ return {"Exactly 1 option from [" + option_list + "] is required and " + std::to_string(used) +
+ " were given",
+ ExitCodes::RequiredError};
+ }
+ if((min_option == 1) && (used == 0))
+ return RequiredError("At least 1 option from [" + option_list + "]");
+ if(used < min_option) {
+ return {"Requires at least " + std::to_string(min_option) + " options used and only " +
+ std::to_string(used) + "were given from [" + option_list + "]",
+ ExitCodes::RequiredError};
+ }
+ if(max_option == 1)
+ return {"Requires at most 1 options be given from [" + option_list + "]", ExitCodes::RequiredError};
+
+ return {"Requires at most " + std::to_string(max_option) + " options be used and " + std::to_string(used) +
+ "were given from [" + option_list + "]",
+ ExitCodes::RequiredError};
+ }
+};
+
+/// Thrown when the wrong number of arguments has been received
+class ArgumentMismatch : public ParseError {
+ CLI11_ERROR_DEF(ParseError, ArgumentMismatch)
+ CLI11_ERROR_SIMPLE(ArgumentMismatch)
+ ArgumentMismatch(std::string name, int expected, std::size_t received)
+ : ArgumentMismatch(expected > 0 ? ("Expected exactly " + std::to_string(expected) + " arguments to " + name +
+ ", got " + std::to_string(received))
+ : ("Expected at least " + std::to_string(-expected) + " arguments to " + name +
+ ", got " + std::to_string(received)),
+ ExitCodes::ArgumentMismatch) {}
+
+ static ArgumentMismatch AtLeast(std::string name, int num, std::size_t received) {
+ return ArgumentMismatch(name + ": At least " + std::to_string(num) + " required but received " +
+ std::to_string(received));
+ }
+ static ArgumentMismatch AtMost(std::string name, int num, std::size_t received) {
+ return ArgumentMismatch(name + ": At Most " + std::to_string(num) + " required but received " +
+ std::to_string(received));
+ }
+ static ArgumentMismatch TypedAtLeast(std::string name, int num, std::string type) {
+ return ArgumentMismatch(name + ": " + std::to_string(num) + " required " + type + " missing");
+ }
+ static ArgumentMismatch FlagOverride(std::string name) {
+ return ArgumentMismatch(name + " was given a disallowed flag override");
+ }
+ static ArgumentMismatch PartialType(std::string name, int num, std::string type) {
+ return ArgumentMismatch(name + ": " + type + " only partially specified: " + std::to_string(num) +
+ " required for each element");
+ }
+};
+
+/// Thrown when a requires option is missing
+class RequiresError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, RequiresError)
+ RequiresError(std::string curname, std::string subname)
+ : RequiresError(curname + " requires " + subname, ExitCodes::RequiresError) {}
+};
+
+/// Thrown when an excludes option is present
+class ExcludesError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, ExcludesError)
+ ExcludesError(std::string curname, std::string subname)
+ : ExcludesError(curname + " excludes " + subname, ExitCodes::ExcludesError) {}
+};
+
+/// Thrown when too many positionals or options are found
+class ExtrasError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, ExtrasError)
+ explicit ExtrasError(std::vector<std::string> args)
+ : ExtrasError((args.size() > 1 ? "The following arguments were not expected: "
+ : "The following argument was not expected: ") +
+ detail::rjoin(args, " "),
+ ExitCodes::ExtrasError) {}
+ ExtrasError(const std::string &name, std::vector<std::string> args)
+ : ExtrasError(name,
+ (args.size() > 1 ? "The following arguments were not expected: "
+ : "The following argument was not expected: ") +
+ detail::rjoin(args, " "),
+ ExitCodes::ExtrasError) {}
+};
+
+/// Thrown when extra values are found in an INI file
+class ConfigError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, ConfigError)
+ CLI11_ERROR_SIMPLE(ConfigError)
+ static ConfigError Extras(std::string item) { return ConfigError("INI was not able to parse " + item); }
+ static ConfigError NotConfigurable(std::string item) {
+ return ConfigError(item + ": This option is not allowed in a configuration file");
+ }
+};
+
+/// Thrown when validation fails before parsing
+class InvalidError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, InvalidError)
+ explicit InvalidError(std::string name)
+ : InvalidError(name + ": Too many positional arguments with unlimited expected args", ExitCodes::InvalidError) {
+ }
+};
+
+/// This is just a safety check to verify selection and parsing match - you should not ever see it
+/// Strings are directly added to this error, but again, it should never be seen.
+class HorribleError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, HorribleError)
+ CLI11_ERROR_SIMPLE(HorribleError)
+};
+
+// After parsing
+
+/// Thrown when counting a non-existent option
+class OptionNotFound : public Error {
+ CLI11_ERROR_DEF(Error, OptionNotFound)
+ explicit OptionNotFound(std::string name) : OptionNotFound(name + " not found", ExitCodes::OptionNotFound) {}
+};
+
+#undef CLI11_ERROR_DEF
+#undef CLI11_ERROR_SIMPLE
+
+/// @}
+
+
+
+
+// Type tools
+
+// Utilities for type enabling
+namespace detail {
+// Based generally on https://rmf.io/cxx11/almost-static-if
+/// Simple empty scoped class
+enum class enabler {};
+
+/// An instance to use in EnableIf
+constexpr enabler dummy = {};
+} // namespace detail
+
+/// A copy of enable_if_t from C++14, compatible with C++11.
+///
+/// We could check to see if C++14 is being used, but it does not hurt to redefine this
+/// (even Google does this: https://github.com/google/skia/blob/main/include/private/SkTLogic.h)
+/// It is not in the std namespace anyway, so no harm done.
+template <bool B, class T = void> using enable_if_t = typename std::enable_if<B, T>::type;
+
+/// A copy of std::void_t from C++17 (helper for C++11 and C++14)
+template <typename... Ts> struct make_void {
+ using type = void;
+};
+
+/// A copy of std::void_t from C++17 - same reasoning as enable_if_t, it does not hurt to redefine
+template <typename... Ts> using void_t = typename make_void<Ts...>::type;
+
+/// A copy of std::conditional_t from C++14 - same reasoning as enable_if_t, it does not hurt to redefine
+template <bool B, class T, class F> using conditional_t = typename std::conditional<B, T, F>::type;
+
+/// Check to see if something is bool (fail check by default)
+template <typename T> struct is_bool : std::false_type {};
+
+/// Check to see if something is bool (true if actually a bool)
+template <> struct is_bool<bool> : std::true_type {};
+
+/// Check to see if something is a shared pointer
+template <typename T> struct is_shared_ptr : std::false_type {};
+
+/// Check to see if something is a shared pointer (True if really a shared pointer)
+template <typename T> struct is_shared_ptr<std::shared_ptr<T>> : std::true_type {};
+
+/// Check to see if something is a shared pointer (True if really a shared pointer)
+template <typename T> struct is_shared_ptr<const std::shared_ptr<T>> : std::true_type {};
+
+/// Check to see if something is copyable pointer
+template <typename T> struct is_copyable_ptr {
+ static bool const value = is_shared_ptr<T>::value || std::is_pointer<T>::value;
+};
+
+/// This can be specialized to override the type deduction for IsMember.
+template <typename T> struct IsMemberType {
+ using type = T;
+};
+
+/// The main custom type needed here is const char * should be a string.
+template <> struct IsMemberType<const char *> {
+ using type = std::string;
+};
+
+namespace detail {
+
+// These are utilities for IsMember and other transforming objects
+
+/// Handy helper to access the element_type generically. This is not part of is_copyable_ptr because it requires that
+/// pointer_traits<T> be valid.
+
+/// not a pointer
+template <typename T, typename Enable = void> struct element_type {
+ using type = T;
+};
+
+template <typename T> struct element_type<T, typename std::enable_if<is_copyable_ptr<T>::value>::type> {
+ using type = typename std::pointer_traits<T>::element_type;
+};
+
+/// Combination of the element type and value type - remove pointer (including smart pointers) and get the value_type of
+/// the container
+template <typename T> struct element_value_type {
+ using type = typename element_type<T>::type::value_type;
+};
+
+/// Adaptor for set-like structure: This just wraps a normal container in a few utilities that do almost nothing.
+template <typename T, typename _ = void> struct pair_adaptor : std::false_type {
+ using value_type = typename T::value_type;
+ using first_type = typename std::remove_const<value_type>::type;
+ using second_type = typename std::remove_const<value_type>::type;
+
+ /// Get the first value (really just the underlying value)
+ template <typename Q> static auto first(Q &&pair_value) -> decltype(std::forward<Q>(pair_value)) {
+ return std::forward<Q>(pair_value);
+ }
+ /// Get the second value (really just the underlying value)
+ template <typename Q> static auto second(Q &&pair_value) -> decltype(std::forward<Q>(pair_value)) {
+ return std::forward<Q>(pair_value);
+ }
+};
+
+/// Adaptor for map-like structure (true version, must have key_type and mapped_type).
+/// This wraps a mapped container in a few utilities access it in a general way.
+template <typename T>
+struct pair_adaptor<
+ T,
+ conditional_t<false, void_t<typename T::value_type::first_type, typename T::value_type::second_type>, void>>
+ : std::true_type {
+ using value_type = typename T::value_type;
+ using first_type = typename std::remove_const<typename value_type::first_type>::type;
+ using second_type = typename std::remove_const<typename value_type::second_type>::type;
+
+ /// Get the first value (really just the underlying value)
+ template <typename Q> static auto first(Q &&pair_value) -> decltype(std::get<0>(std::forward<Q>(pair_value))) {
+ return std::get<0>(std::forward<Q>(pair_value));
+ }
+ /// Get the second value (really just the underlying value)
+ template <typename Q> static auto second(Q &&pair_value) -> decltype(std::get<1>(std::forward<Q>(pair_value))) {
+ return std::get<1>(std::forward<Q>(pair_value));
+ }
+};
+
+// Warning is suppressed due to "bug" in gcc<5.0 and gcc 7.0 with c++17 enabled that generates a Wnarrowing warning
+// in the unevaluated context even if the function that was using this wasn't used. The standard says narrowing in
+// brace initialization shouldn't be allowed but for backwards compatibility gcc allows it in some contexts. It is a
+// little fuzzy what happens in template constructs and I think that was something GCC took a little while to work out.
+// But regardless some versions of gcc generate a warning when they shouldn't from the following code so that should be
+// suppressed
+#ifdef __GNUC__
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wnarrowing"
+#endif
+// check for constructibility from a specific type and copy assignable used in the parse detection
+template <typename T, typename C> class is_direct_constructible {
+ template <typename TT, typename CC>
+ static auto test(int, std::true_type) -> decltype(
+// NVCC warns about narrowing conversions here
+#ifdef __CUDACC__
+#ifdef __NVCC_DIAG_PRAGMA_SUPPORT__
+#pragma nv_diag_suppress 2361
+#else
+#pragma diag_suppress 2361
+#endif
+#endif
+ TT{std::declval<CC>()}
+#ifdef __CUDACC__
+#ifdef __NVCC_DIAG_PRAGMA_SUPPORT__
+#pragma nv_diag_default 2361
+#else
+#pragma diag_default 2361
+#endif
+#endif
+ ,
+ std::is_move_assignable<TT>());
+
+ template <typename TT, typename CC> static auto test(int, std::false_type) -> std::false_type;
+
+ template <typename, typename> static auto test(...) -> std::false_type;
+
+ public:
+ static constexpr bool value = decltype(test<T, C>(0, typename std::is_constructible<T, C>::type()))::value;
+};
+#ifdef __GNUC__
+#pragma GCC diagnostic pop
+#endif
+
+// Check for output streamability
+// Based on https://stackoverflow.com/questions/22758291/how-can-i-detect-if-a-type-can-be-streamed-to-an-stdostream
+
+template <typename T, typename S = std::ostringstream> class is_ostreamable {
+ template <typename TT, typename SS>
+ static auto test(int) -> decltype(std::declval<SS &>() << std::declval<TT>(), std::true_type());
+
+ template <typename, typename> static auto test(...) -> std::false_type;
+
+ public:
+ static constexpr bool value = decltype(test<T, S>(0))::value;
+};
+
+/// Check for input streamability
+template <typename T, typename S = std::istringstream> class is_istreamable {
+ template <typename TT, typename SS>
+ static auto test(int) -> decltype(std::declval<SS &>() >> std::declval<TT &>(), std::true_type());
+
+ template <typename, typename> static auto test(...) -> std::false_type;
+
+ public:
+ static constexpr bool value = decltype(test<T, S>(0))::value;
+};
+
+/// Check for complex
+template <typename T> class is_complex {
+ template <typename TT>
+ static auto test(int) -> decltype(std::declval<TT>().real(), std::declval<TT>().imag(), std::true_type());
+
+ template <typename> static auto test(...) -> std::false_type;
+
+ public:
+ static constexpr bool value = decltype(test<T>(0))::value;
+};
+
+/// Templated operation to get a value from a stream
+template <typename T, enable_if_t<is_istreamable<T>::value, detail::enabler> = detail::dummy>
+bool from_stream(const std::string &istring, T &obj) {
+ std::istringstream is;
+ is.str(istring);
+ is >> obj;
+ return !is.fail() && !is.rdbuf()->in_avail();
+}
+
+template <typename T, enable_if_t<!is_istreamable<T>::value, detail::enabler> = detail::dummy>
+bool from_stream(const std::string & /*istring*/, T & /*obj*/) {
+ return false;
+}
+
+// check to see if an object is a mutable container (fail by default)
+template <typename T, typename _ = void> struct is_mutable_container : std::false_type {};
+
+/// type trait to test if a type is a mutable container meaning it has a value_type, it has an iterator, a clear, and
+/// end methods and an insert function. And for our purposes we exclude std::string and types that can be constructed
+/// from a std::string
+template <typename T>
+struct is_mutable_container<
+ T,
+ conditional_t<false,
+ void_t<typename T::value_type,
+ decltype(std::declval<T>().end()),
+ decltype(std::declval<T>().clear()),
+ decltype(std::declval<T>().insert(std::declval<decltype(std::declval<T>().end())>(),
+ std::declval<const typename T::value_type &>()))>,
+ void>> : public conditional_t<std::is_constructible<T, std::string>::value ||
+ std::is_constructible<T, std::wstring>::value,
+ std::false_type,
+ std::true_type> {};
+
+// check to see if an object is a mutable container (fail by default)
+template <typename T, typename _ = void> struct is_readable_container : std::false_type {};
+
+/// type trait to test if a type is a container meaning it has a value_type, it has an iterator, a clear, and an end
+/// methods and an insert function. And for our purposes we exclude std::string and types that can be constructed from
+/// a std::string
+template <typename T>
+struct is_readable_container<
+ T,
+ conditional_t<false, void_t<decltype(std::declval<T>().end()), decltype(std::declval<T>().begin())>, void>>
+ : public std::true_type {};
+
+// check to see if an object is a wrapper (fail by default)
+template <typename T, typename _ = void> struct is_wrapper : std::false_type {};
+
+// check if an object is a wrapper (it has a value_type defined)
+template <typename T>
+struct is_wrapper<T, conditional_t<false, void_t<typename T::value_type>, void>> : public std::true_type {};
+
+// Check for tuple like types, as in classes with a tuple_size type trait
+template <typename S> class is_tuple_like {
+ template <typename SS>
+ // static auto test(int)
+ // -> decltype(std::conditional<(std::tuple_size<SS>::value > 0), std::true_type, std::false_type>::type());
+ static auto test(int) -> decltype(std::tuple_size<typename std::decay<SS>::type>::value, std::true_type{});
+ template <typename> static auto test(...) -> std::false_type;
+
+ public:
+ static constexpr bool value = decltype(test<S>(0))::value;
+};
+
+/// Convert an object to a string (directly forward if this can become a string)
+template <typename T, enable_if_t<std::is_convertible<T, std::string>::value, detail::enabler> = detail::dummy>
+auto to_string(T &&value) -> decltype(std::forward<T>(value)) {
+ return std::forward<T>(value);
+}
+
+/// Construct a string from the object
+template <typename T,
+ enable_if_t<std::is_constructible<std::string, T>::value && !std::is_convertible<T, std::string>::value,
+ detail::enabler> = detail::dummy>
+std::string to_string(const T &value) {
+ return std::string(value); // NOLINT(google-readability-casting)
+}
+
+/// Convert an object to a string (streaming must be supported for that type)
+template <typename T,
+ enable_if_t<!std::is_convertible<std::string, T>::value && !std::is_constructible<std::string, T>::value &&
+ is_ostreamable<T>::value,
+ detail::enabler> = detail::dummy>
+std::string to_string(T &&value) {
+ std::stringstream stream;
+ stream << value;
+ return stream.str();
+}
+
+/// If conversion is not supported, return an empty string (streaming is not supported for that type)
+template <typename T,
+ enable_if_t<!std::is_constructible<std::string, T>::value && !is_ostreamable<T>::value &&
+ !is_readable_container<typename std::remove_const<T>::type>::value,
+ detail::enabler> = detail::dummy>
+std::string to_string(T &&) {
+ return {};
+}
+
+/// convert a readable container to a string
+template <typename T,
+ enable_if_t<!std::is_constructible<std::string, T>::value && !is_ostreamable<T>::value &&
+ is_readable_container<T>::value,
+ detail::enabler> = detail::dummy>
+std::string to_string(T &&variable) {
+ auto cval = variable.begin();
+ auto end = variable.end();
+ if(cval == end) {
+ return {"{}"};
+ }
+ std::vector<std::string> defaults;
+ while(cval != end) {
+ defaults.emplace_back(CLI::detail::to_string(*cval));
+ ++cval;
+ }
+ return {"[" + detail::join(defaults) + "]"};
+}
+
+/// special template overload
+template <typename T1,
+ typename T2,
+ typename T,
+ enable_if_t<std::is_same<T1, T2>::value, detail::enabler> = detail::dummy>
+auto checked_to_string(T &&value) -> decltype(to_string(std::forward<T>(value))) {
+ return to_string(std::forward<T>(value));
+}
+
+/// special template overload
+template <typename T1,
+ typename T2,
+ typename T,
+ enable_if_t<!std::is_same<T1, T2>::value, detail::enabler> = detail::dummy>
+std::string checked_to_string(T &&) {
+ return std::string{};
+}
+/// get a string as a convertible value for arithmetic types
+template <typename T, enable_if_t<std::is_arithmetic<T>::value, detail::enabler> = detail::dummy>
+std::string value_string(const T &value) {
+ return std::to_string(value);
+}
+/// get a string as a convertible value for enumerations
+template <typename T, enable_if_t<std::is_enum<T>::value, detail::enabler> = detail::dummy>
+std::string value_string(const T &value) {
+ return std::to_string(static_cast<typename std::underlying_type<T>::type>(value));
+}
+/// for other types just use the regular to_string function
+template <typename T,
+ enable_if_t<!std::is_enum<T>::value && !std::is_arithmetic<T>::value, detail::enabler> = detail::dummy>
+auto value_string(const T &value) -> decltype(to_string(value)) {
+ return to_string(value);
+}
+
+/// template to get the underlying value type if it exists or use a default
+template <typename T, typename def, typename Enable = void> struct wrapped_type {
+ using type = def;
+};
+
+/// Type size for regular object types that do not look like a tuple
+template <typename T, typename def> struct wrapped_type<T, def, typename std::enable_if<is_wrapper<T>::value>::type> {
+ using type = typename T::value_type;
+};
+
+/// This will only trigger for actual void type
+template <typename T, typename Enable = void> struct type_count_base {
+ static const int value{0};
+};
+
+/// Type size for regular object types that do not look like a tuple
+template <typename T>
+struct type_count_base<T,
+ typename std::enable_if<!is_tuple_like<T>::value && !is_mutable_container<T>::value &&
+ !std::is_void<T>::value>::type> {
+ static constexpr int value{1};
+};
+
+/// the base tuple size
+template <typename T>
+struct type_count_base<T, typename std::enable_if<is_tuple_like<T>::value && !is_mutable_container<T>::value>::type> {
+ static constexpr int value{std::tuple_size<T>::value};
+};
+
+/// Type count base for containers is the type_count_base of the individual element
+template <typename T> struct type_count_base<T, typename std::enable_if<is_mutable_container<T>::value>::type> {
+ static constexpr int value{type_count_base<typename T::value_type>::value};
+};
+
+/// Set of overloads to get the type size of an object
+
+/// forward declare the subtype_count structure
+template <typename T> struct subtype_count;
+
+/// forward declare the subtype_count_min structure
+template <typename T> struct subtype_count_min;
+
+/// This will only trigger for actual void type
+template <typename T, typename Enable = void> struct type_count {
+ static const int value{0};
+};
+
+/// Type size for regular object types that do not look like a tuple
+template <typename T>
+struct type_count<T,
+ typename std::enable_if<!is_wrapper<T>::value && !is_tuple_like<T>::value && !is_complex<T>::value &&
+ !std::is_void<T>::value>::type> {
+ static constexpr int value{1};
+};
+
+/// Type size for complex since it sometimes looks like a wrapper
+template <typename T> struct type_count<T, typename std::enable_if<is_complex<T>::value>::type> {
+ static constexpr int value{2};
+};
+
+/// Type size of types that are wrappers,except complex and tuples(which can also be wrappers sometimes)
+template <typename T> struct type_count<T, typename std::enable_if<is_mutable_container<T>::value>::type> {
+ static constexpr int value{subtype_count<typename T::value_type>::value};
+};
+
+/// Type size of types that are wrappers,except containers complex and tuples(which can also be wrappers sometimes)
+template <typename T>
+struct type_count<T,
+ typename std::enable_if<is_wrapper<T>::value && !is_complex<T>::value && !is_tuple_like<T>::value &&
+ !is_mutable_container<T>::value>::type> {
+ static constexpr int value{type_count<typename T::value_type>::value};
+};
+
+/// 0 if the index > tuple size
+template <typename T, std::size_t I>
+constexpr typename std::enable_if<I == type_count_base<T>::value, int>::type tuple_type_size() {
+ return 0;
+}
+
+/// Recursively generate the tuple type name
+template <typename T, std::size_t I>
+ constexpr typename std::enable_if < I<type_count_base<T>::value, int>::type tuple_type_size() {
+ return subtype_count<typename std::tuple_element<I, T>::type>::value + tuple_type_size<T, I + 1>();
+}
+
+/// Get the type size of the sum of type sizes for all the individual tuple types
+template <typename T> struct type_count<T, typename std::enable_if<is_tuple_like<T>::value>::type> {
+ static constexpr int value{tuple_type_size<T, 0>()};
+};
+
+/// definition of subtype count
+template <typename T> struct subtype_count {
+ static constexpr int value{is_mutable_container<T>::value ? expected_max_vector_size : type_count<T>::value};
+};
+
+/// This will only trigger for actual void type
+template <typename T, typename Enable = void> struct type_count_min {
+ static const int value{0};
+};
+
+/// Type size for regular object types that do not look like a tuple
+template <typename T>
+struct type_count_min<
+ T,
+ typename std::enable_if<!is_mutable_container<T>::value && !is_tuple_like<T>::value && !is_wrapper<T>::value &&
+ !is_complex<T>::value && !std::is_void<T>::value>::type> {
+ static constexpr int value{type_count<T>::value};
+};
+
+/// Type size for complex since it sometimes looks like a wrapper
+template <typename T> struct type_count_min<T, typename std::enable_if<is_complex<T>::value>::type> {
+ static constexpr int value{1};
+};
+
+/// Type size min of types that are wrappers,except complex and tuples(which can also be wrappers sometimes)
+template <typename T>
+struct type_count_min<
+ T,
+ typename std::enable_if<is_wrapper<T>::value && !is_complex<T>::value && !is_tuple_like<T>::value>::type> {
+ static constexpr int value{subtype_count_min<typename T::value_type>::value};
+};
+
+/// 0 if the index > tuple size
+template <typename T, std::size_t I>
+constexpr typename std::enable_if<I == type_count_base<T>::value, int>::type tuple_type_size_min() {
+ return 0;
+}
+
+/// Recursively generate the tuple type name
+template <typename T, std::size_t I>
+ constexpr typename std::enable_if < I<type_count_base<T>::value, int>::type tuple_type_size_min() {
+ return subtype_count_min<typename std::tuple_element<I, T>::type>::value + tuple_type_size_min<T, I + 1>();
+}
+
+/// Get the type size of the sum of type sizes for all the individual tuple types
+template <typename T> struct type_count_min<T, typename std::enable_if<is_tuple_like<T>::value>::type> {
+ static constexpr int value{tuple_type_size_min<T, 0>()};
+};
+
+/// definition of subtype count
+template <typename T> struct subtype_count_min {
+ static constexpr int value{is_mutable_container<T>::value
+ ? ((type_count<T>::value < expected_max_vector_size) ? type_count<T>::value : 0)
+ : type_count_min<T>::value};
+};
+
+/// This will only trigger for actual void type
+template <typename T, typename Enable = void> struct expected_count {
+ static const int value{0};
+};
+
+/// For most types the number of expected items is 1
+template <typename T>
+struct expected_count<T,
+ typename std::enable_if<!is_mutable_container<T>::value && !is_wrapper<T>::value &&
+ !std::is_void<T>::value>::type> {
+ static constexpr int value{1};
+};
+/// number of expected items in a vector
+template <typename T> struct expected_count<T, typename std::enable_if<is_mutable_container<T>::value>::type> {
+ static constexpr int value{expected_max_vector_size};
+};
+
+/// number of expected items in a vector
+template <typename T>
+struct expected_count<T, typename std::enable_if<!is_mutable_container<T>::value && is_wrapper<T>::value>::type> {
+ static constexpr int value{expected_count<typename T::value_type>::value};
+};
+
+// Enumeration of the different supported categorizations of objects
+enum class object_category : int {
+ char_value = 1,
+ integral_value = 2,
+ unsigned_integral = 4,
+ enumeration = 6,
+ boolean_value = 8,
+ floating_point = 10,
+ number_constructible = 12,
+ double_constructible = 14,
+ integer_constructible = 16,
+ // string like types
+ string_assignable = 23,
+ string_constructible = 24,
+ wstring_assignable = 25,
+ wstring_constructible = 26,
+ other = 45,
+ // special wrapper or container types
+ wrapper_value = 50,
+ complex_number = 60,
+ tuple_value = 70,
+ container_value = 80,
+
+};
+
+/// Set of overloads to classify an object according to type
+
+/// some type that is not otherwise recognized
+template <typename T, typename Enable = void> struct classify_object {
+ static constexpr object_category value{object_category::other};
+};
+
+/// Signed integers
+template <typename T>
+struct classify_object<
+ T,
+ typename std::enable_if<std::is_integral<T>::value && !std::is_same<T, char>::value && std::is_signed<T>::value &&
+ !is_bool<T>::value && !std::is_enum<T>::value>::type> {
+ static constexpr object_category value{object_category::integral_value};
+};
+
+/// Unsigned integers
+template <typename T>
+struct classify_object<T,
+ typename std::enable_if<std::is_integral<T>::value && std::is_unsigned<T>::value &&
+ !std::is_same<T, char>::value && !is_bool<T>::value>::type> {
+ static constexpr object_category value{object_category::unsigned_integral};
+};
+
+/// single character values
+template <typename T>
+struct classify_object<T, typename std::enable_if<std::is_same<T, char>::value && !std::is_enum<T>::value>::type> {
+ static constexpr object_category value{object_category::char_value};
+};
+
+/// Boolean values
+template <typename T> struct classify_object<T, typename std::enable_if<is_bool<T>::value>::type> {
+ static constexpr object_category value{object_category::boolean_value};
+};
+
+/// Floats
+template <typename T> struct classify_object<T, typename std::enable_if<std::is_floating_point<T>::value>::type> {
+ static constexpr object_category value{object_category::floating_point};
+};
+#if defined _MSC_VER
+// in MSVC wstring should take precedence if available this isn't as useful on other compilers due to the broader use of
+// utf-8 encoding
+#define WIDE_STRING_CHECK \
+ !std::is_assignable<T &, std::wstring>::value && !std::is_constructible<T, std::wstring>::value
+#define STRING_CHECK true
+#else
+#define WIDE_STRING_CHECK true
+#define STRING_CHECK !std::is_assignable<T &, std::string>::value && !std::is_constructible<T, std::string>::value
+#endif
+
+/// String and similar direct assignment
+template <typename T>
+struct classify_object<
+ T,
+ typename std::enable_if<!std::is_floating_point<T>::value && !std::is_integral<T>::value && WIDE_STRING_CHECK &&
+ std::is_assignable<T &, std::string>::value>::type> {
+ static constexpr object_category value{object_category::string_assignable};
+};
+
+/// String and similar constructible and copy assignment
+template <typename T>
+struct classify_object<
+ T,
+ typename std::enable_if<!std::is_floating_point<T>::value && !std::is_integral<T>::value &&
+ !std::is_assignable<T &, std::string>::value && (type_count<T>::value == 1) &&
+ WIDE_STRING_CHECK && std::is_constructible<T, std::string>::value>::type> {
+ static constexpr object_category value{object_category::string_constructible};
+};
+
+/// Wide strings
+template <typename T>
+struct classify_object<T,
+ typename std::enable_if<!std::is_floating_point<T>::value && !std::is_integral<T>::value &&
+ STRING_CHECK && std::is_assignable<T &, std::wstring>::value>::type> {
+ static constexpr object_category value{object_category::wstring_assignable};
+};
+
+template <typename T>
+struct classify_object<
+ T,
+ typename std::enable_if<!std::is_floating_point<T>::value && !std::is_integral<T>::value &&
+ !std::is_assignable<T &, std::wstring>::value && (type_count<T>::value == 1) &&
+ STRING_CHECK && std::is_constructible<T, std::wstring>::value>::type> {
+ static constexpr object_category value{object_category::wstring_constructible};
+};
+
+/// Enumerations
+template <typename T> struct classify_object<T, typename std::enable_if<std::is_enum<T>::value>::type> {
+ static constexpr object_category value{object_category::enumeration};
+};
+
+template <typename T> struct classify_object<T, typename std::enable_if<is_complex<T>::value>::type> {
+ static constexpr object_category value{object_category::complex_number};
+};
+
+/// Handy helper to contain a bunch of checks that rule out many common types (integers, string like, floating point,
+/// vectors, and enumerations
+template <typename T> struct uncommon_type {
+ using type = typename std::conditional<
+ !std::is_floating_point<T>::value && !std::is_integral<T>::value &&
+ !std::is_assignable<T &, std::string>::value && !std::is_constructible<T, std::string>::value &&
+ !std::is_assignable<T &, std::wstring>::value && !std::is_constructible<T, std::wstring>::value &&
+ !is_complex<T>::value && !is_mutable_container<T>::value && !std::is_enum<T>::value,
+ std::true_type,
+ std::false_type>::type;
+ static constexpr bool value = type::value;
+};
+
+/// wrapper type
+template <typename T>
+struct classify_object<T,
+ typename std::enable_if<(!is_mutable_container<T>::value && is_wrapper<T>::value &&
+ !is_tuple_like<T>::value && uncommon_type<T>::value)>::type> {
+ static constexpr object_category value{object_category::wrapper_value};
+};
+
+/// Assignable from double or int
+template <typename T>
+struct classify_object<T,
+ typename std::enable_if<uncommon_type<T>::value && type_count<T>::value == 1 &&
+ !is_wrapper<T>::value && is_direct_constructible<T, double>::value &&
+ is_direct_constructible<T, int>::value>::type> {
+ static constexpr object_category value{object_category::number_constructible};
+};
+
+/// Assignable from int
+template <typename T>
+struct classify_object<T,
+ typename std::enable_if<uncommon_type<T>::value && type_count<T>::value == 1 &&
+ !is_wrapper<T>::value && !is_direct_constructible<T, double>::value &&
+ is_direct_constructible<T, int>::value>::type> {
+ static constexpr object_category value{object_category::integer_constructible};
+};
+
+/// Assignable from double
+template <typename T>
+struct classify_object<T,
+ typename std::enable_if<uncommon_type<T>::value && type_count<T>::value == 1 &&
+ !is_wrapper<T>::value && is_direct_constructible<T, double>::value &&
+ !is_direct_constructible<T, int>::value>::type> {
+ static constexpr object_category value{object_category::double_constructible};
+};
+
+/// Tuple type
+template <typename T>
+struct classify_object<
+ T,
+ typename std::enable_if<is_tuple_like<T>::value &&
+ ((type_count<T>::value >= 2 && !is_wrapper<T>::value) ||
+ (uncommon_type<T>::value && !is_direct_constructible<T, double>::value &&
+ !is_direct_constructible<T, int>::value) ||
+ (uncommon_type<T>::value && type_count<T>::value >= 2))>::type> {
+ static constexpr object_category value{object_category::tuple_value};
+ // the condition on this class requires it be like a tuple, but on some compilers (like Xcode) tuples can be
+ // constructed from just the first element so tuples of <string, int,int> can be constructed from a string, which
+ // could lead to issues so there are two variants of the condition, the first isolates things with a type size >=2
+ // mainly to get tuples on Xcode with the exception of wrappers, the second is the main one and just separating out
+ // those cases that are caught by other object classifications
+};
+
+/// container type
+template <typename T> struct classify_object<T, typename std::enable_if<is_mutable_container<T>::value>::type> {
+ static constexpr object_category value{object_category::container_value};
+};
+
+// Type name print
+
+/// Was going to be based on
+/// http://stackoverflow.com/questions/1055452/c-get-name-of-type-in-template
+/// But this is cleaner and works better in this case
+
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::char_value, detail::enabler> = detail::dummy>
+constexpr const char *type_name() {
+ return "CHAR";
+}
+
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::integral_value ||
+ classify_object<T>::value == object_category::integer_constructible,
+ detail::enabler> = detail::dummy>
+constexpr const char *type_name() {
+ return "INT";
+}
+
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::unsigned_integral, detail::enabler> = detail::dummy>
+constexpr const char *type_name() {
+ return "UINT";
+}
+
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::floating_point ||
+ classify_object<T>::value == object_category::number_constructible ||
+ classify_object<T>::value == object_category::double_constructible,
+ detail::enabler> = detail::dummy>
+constexpr const char *type_name() {
+ return "FLOAT";
+}
+
+/// Print name for enumeration types
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::enumeration, detail::enabler> = detail::dummy>
+constexpr const char *type_name() {
+ return "ENUM";
+}
+
+/// Print name for enumeration types
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::boolean_value, detail::enabler> = detail::dummy>
+constexpr const char *type_name() {
+ return "BOOLEAN";
+}
+
+/// Print name for enumeration types
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::complex_number, detail::enabler> = detail::dummy>
+constexpr const char *type_name() {
+ return "COMPLEX";
+}
+
+/// Print for all other types
+template <typename T,
+ enable_if_t<classify_object<T>::value >= object_category::string_assignable &&
+ classify_object<T>::value <= object_category::other,
+ detail::enabler> = detail::dummy>
+constexpr const char *type_name() {
+ return "TEXT";
+}
+/// typename for tuple value
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::tuple_value && type_count_base<T>::value >= 2,
+ detail::enabler> = detail::dummy>
+std::string type_name(); // forward declaration
+
+/// Generate type name for a wrapper or container value
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::container_value ||
+ classify_object<T>::value == object_category::wrapper_value,
+ detail::enabler> = detail::dummy>
+std::string type_name(); // forward declaration
+
+/// Print name for single element tuple types
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::tuple_value && type_count_base<T>::value == 1,
+ detail::enabler> = detail::dummy>
+inline std::string type_name() {
+ return type_name<typename std::decay<typename std::tuple_element<0, T>::type>::type>();
+}
+
+/// Empty string if the index > tuple size
+template <typename T, std::size_t I>
+inline typename std::enable_if<I == type_count_base<T>::value, std::string>::type tuple_name() {
+ return std::string{};
+}
+
+/// Recursively generate the tuple type name
+template <typename T, std::size_t I>
+inline typename std::enable_if<(I < type_count_base<T>::value), std::string>::type tuple_name() {
+ auto str = std::string{type_name<typename std::decay<typename std::tuple_element<I, T>::type>::type>()} + ',' +
+ tuple_name<T, I + 1>();
+ if(str.back() == ',')
+ str.pop_back();
+ return str;
+}
+
+/// Print type name for tuples with 2 or more elements
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::tuple_value && type_count_base<T>::value >= 2,
+ detail::enabler>>
+inline std::string type_name() {
+ auto tname = std::string(1, '[') + tuple_name<T, 0>();
+ tname.push_back(']');
+ return tname;
+}
+
+/// get the type name for a type that has a value_type member
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::container_value ||
+ classify_object<T>::value == object_category::wrapper_value,
+ detail::enabler>>
+inline std::string type_name() {
+ return type_name<typename T::value_type>();
+}
+
+// Lexical cast
+
+/// Convert to an unsigned integral
+template <typename T, enable_if_t<std::is_unsigned<T>::value, detail::enabler> = detail::dummy>
+bool integral_conversion(const std::string &input, T &output) noexcept {
+ if(input.empty() || input.front() == '-') {
+ return false;
+ }
+ char *val{nullptr};
+ errno = 0;
+ std::uint64_t output_ll = std::strtoull(input.c_str(), &val, 0);
+ if(errno == ERANGE) {
+ return false;
+ }
+ output = static_cast<T>(output_ll);
+ if(val == (input.c_str() + input.size()) && static_cast<std::uint64_t>(output) == output_ll) {
+ return true;
+ }
+ val = nullptr;
+ std::int64_t output_sll = std::strtoll(input.c_str(), &val, 0);
+ if(val == (input.c_str() + input.size())) {
+ output = (output_sll < 0) ? static_cast<T>(0) : static_cast<T>(output_sll);
+ return (static_cast<std::int64_t>(output) == output_sll);
+ }
+ // remove separators
+ if(input.find_first_of("_'") != std::string::npos) {
+ std::string nstring = input;
+ nstring.erase(std::remove(nstring.begin(), nstring.end(), '_'), nstring.end());
+ nstring.erase(std::remove(nstring.begin(), nstring.end(), '\''), nstring.end());
+ return integral_conversion(nstring, output);
+ }
+ if(input.compare(0, 2, "0o") == 0) {
+ val = nullptr;
+ errno = 0;
+ output_ll = std::strtoull(input.c_str() + 2, &val, 8);
+ if(errno == ERANGE) {
+ return false;
+ }
+ output = static_cast<T>(output_ll);
+ return (val == (input.c_str() + input.size()) && static_cast<std::uint64_t>(output) == output_ll);
+ }
+ if(input.compare(0, 2, "0b") == 0) {
+ val = nullptr;
+ errno = 0;
+ output_ll = std::strtoull(input.c_str() + 2, &val, 2);
+ if(errno == ERANGE) {
+ return false;
+ }
+ output = static_cast<T>(output_ll);
+ return (val == (input.c_str() + input.size()) && static_cast<std::uint64_t>(output) == output_ll);
+ }
+ return false;
+}
+
+/// Convert to a signed integral
+template <typename T, enable_if_t<std::is_signed<T>::value, detail::enabler> = detail::dummy>
+bool integral_conversion(const std::string &input, T &output) noexcept {
+ if(input.empty()) {
+ return false;
+ }
+ char *val = nullptr;
+ errno = 0;
+ std::int64_t output_ll = std::strtoll(input.c_str(), &val, 0);
+ if(errno == ERANGE) {
+ return false;
+ }
+ output = static_cast<T>(output_ll);
+ if(val == (input.c_str() + input.size()) && static_cast<std::int64_t>(output) == output_ll) {
+ return true;
+ }
+ if(input == "true") {
+ // this is to deal with a few oddities with flags and wrapper int types
+ output = static_cast<T>(1);
+ return true;
+ }
+ // remove separators
+ if(input.find_first_of("_'") != std::string::npos) {
+ std::string nstring = input;
+ nstring.erase(std::remove(nstring.begin(), nstring.end(), '_'), nstring.end());
+ nstring.erase(std::remove(nstring.begin(), nstring.end(), '\''), nstring.end());
+ return integral_conversion(nstring, output);
+ }
+ if(input.compare(0, 2, "0o") == 0) {
+ val = nullptr;
+ errno = 0;
+ output_ll = std::strtoll(input.c_str() + 2, &val, 8);
+ if(errno == ERANGE) {
+ return false;
+ }
+ output = static_cast<T>(output_ll);
+ return (val == (input.c_str() + input.size()) && static_cast<std::int64_t>(output) == output_ll);
+ }
+ if(input.compare(0, 2, "0b") == 0) {
+ val = nullptr;
+ errno = 0;
+ output_ll = std::strtoll(input.c_str() + 2, &val, 2);
+ if(errno == ERANGE) {
+ return false;
+ }
+ output = static_cast<T>(output_ll);
+ return (val == (input.c_str() + input.size()) && static_cast<std::int64_t>(output) == output_ll);
+ }
+ return false;
+}
+
+/// Convert a flag into an integer value typically binary flags sets errno to nonzero if conversion failed
+inline std::int64_t to_flag_value(std::string val) noexcept {
+ static const std::string trueString("true");
+ static const std::string falseString("false");
+ if(val == trueString) {
+ return 1;
+ }
+ if(val == falseString) {
+ return -1;
+ }
+ val = detail::to_lower(val);
+ std::int64_t ret = 0;
+ if(val.size() == 1) {
+ if(val[0] >= '1' && val[0] <= '9') {
+ return (static_cast<std::int64_t>(val[0]) - '0');
+ }
+ switch(val[0]) {
+ case '0':
+ case 'f':
+ case 'n':
+ case '-':
+ ret = -1;
+ break;
+ case 't':
+ case 'y':
+ case '+':
+ ret = 1;
+ break;
+ default:
+ errno = EINVAL;
+ return -1;
+ }
+ return ret;
+ }
+ if(val == trueString || val == "on" || val == "yes" || val == "enable") {
+ ret = 1;
+ } else if(val == falseString || val == "off" || val == "no" || val == "disable") {
+ ret = -1;
+ } else {
+ char *loc_ptr{nullptr};
+ ret = std::strtoll(val.c_str(), &loc_ptr, 0);
+ if(loc_ptr != (val.c_str() + val.size()) && errno == 0) {
+ errno = EINVAL;
+ }
+ }
+ return ret;
+}
+
+/// Integer conversion
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::integral_value ||
+ classify_object<T>::value == object_category::unsigned_integral,
+ detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ return integral_conversion(input, output);
+}
+
+/// char values
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::char_value, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ if(input.size() == 1) {
+ output = static_cast<T>(input[0]);
+ return true;
+ }
+ return integral_conversion(input, output);
+}
+
+/// Boolean values
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::boolean_value, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ errno = 0;
+ auto out = to_flag_value(input);
+ if(errno == 0) {
+ output = (out > 0);
+ } else if(errno == ERANGE) {
+ output = (input[0] != '-');
+ } else {
+ return false;
+ }
+ return true;
+}
+
+/// Floats
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::floating_point, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ if(input.empty()) {
+ return false;
+ }
+ char *val = nullptr;
+ auto output_ld = std::strtold(input.c_str(), &val);
+ output = static_cast<T>(output_ld);
+ if(val == (input.c_str() + input.size())) {
+ return true;
+ }
+ // remove separators
+ if(input.find_first_of("_'") != std::string::npos) {
+ std::string nstring = input;
+ nstring.erase(std::remove(nstring.begin(), nstring.end(), '_'), nstring.end());
+ nstring.erase(std::remove(nstring.begin(), nstring.end(), '\''), nstring.end());
+ return lexical_cast(nstring, output);
+ }
+ return false;
+}
+
+/// complex
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::complex_number, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ using XC = typename wrapped_type<T, double>::type;
+ XC x{0.0}, y{0.0};
+ auto str1 = input;
+ bool worked = false;
+ auto nloc = str1.find_last_of("+-");
+ if(nloc != std::string::npos && nloc > 0) {
+ worked = lexical_cast(str1.substr(0, nloc), x);
+ str1 = str1.substr(nloc);
+ if(str1.back() == 'i' || str1.back() == 'j')
+ str1.pop_back();
+ worked = worked && lexical_cast(str1, y);
+ } else {
+ if(str1.back() == 'i' || str1.back() == 'j') {
+ str1.pop_back();
+ worked = lexical_cast(str1, y);
+ x = XC{0};
+ } else {
+ worked = lexical_cast(str1, x);
+ y = XC{0};
+ }
+ }
+ if(worked) {
+ output = T{x, y};
+ return worked;
+ }
+ return from_stream(input, output);
+}
+
+/// String and similar direct assignment
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::string_assignable, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ output = input;
+ return true;
+}
+
+/// String and similar constructible and copy assignment
+template <
+ typename T,
+ enable_if_t<classify_object<T>::value == object_category::string_constructible, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ output = T(input);
+ return true;
+}
+
+/// Wide strings
+template <
+ typename T,
+ enable_if_t<classify_object<T>::value == object_category::wstring_assignable, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ output = widen(input);
+ return true;
+}
+
+template <
+ typename T,
+ enable_if_t<classify_object<T>::value == object_category::wstring_constructible, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ output = T{widen(input)};
+ return true;
+}
+
+/// Enumerations
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::enumeration, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ typename std::underlying_type<T>::type val;
+ if(!integral_conversion(input, val)) {
+ return false;
+ }
+ output = static_cast<T>(val);
+ return true;
+}
+
+/// wrapper types
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::wrapper_value &&
+ std::is_assignable<T &, typename T::value_type>::value,
+ detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ typename T::value_type val;
+ if(lexical_cast(input, val)) {
+ output = val;
+ return true;
+ }
+ return from_stream(input, output);
+}
+
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::wrapper_value &&
+ !std::is_assignable<T &, typename T::value_type>::value && std::is_assignable<T &, T>::value,
+ detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ typename T::value_type val;
+ if(lexical_cast(input, val)) {
+ output = T{val};
+ return true;
+ }
+ return from_stream(input, output);
+}
+
+/// Assignable from double or int
+template <
+ typename T,
+ enable_if_t<classify_object<T>::value == object_category::number_constructible, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ int val = 0;
+ if(integral_conversion(input, val)) {
+ output = T(val);
+ return true;
+ }
+
+ double dval = 0.0;
+ if(lexical_cast(input, dval)) {
+ output = T{dval};
+ return true;
+ }
+
+ return from_stream(input, output);
+}
+
+/// Assignable from int
+template <
+ typename T,
+ enable_if_t<classify_object<T>::value == object_category::integer_constructible, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ int val = 0;
+ if(integral_conversion(input, val)) {
+ output = T(val);
+ return true;
+ }
+ return from_stream(input, output);
+}
+
+/// Assignable from double
+template <
+ typename T,
+ enable_if_t<classify_object<T>::value == object_category::double_constructible, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ double val = 0.0;
+ if(lexical_cast(input, val)) {
+ output = T{val};
+ return true;
+ }
+ return from_stream(input, output);
+}
+
+/// Non-string convertible from an int
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::other && std::is_assignable<T &, int>::value,
+ detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ int val = 0;
+ if(integral_conversion(input, val)) {
+#ifdef _MSC_VER
+#pragma warning(push)
+#pragma warning(disable : 4800)
+#endif
+ // with Atomic<XX> this could produce a warning due to the conversion but if atomic gets here it is an old style
+ // so will most likely still work
+ output = val;
+#ifdef _MSC_VER
+#pragma warning(pop)
+#endif
+ return true;
+ }
+ // LCOV_EXCL_START
+ // This version of cast is only used for odd cases in an older compilers the fail over
+ // from_stream is tested elsewhere an not relevant for coverage here
+ return from_stream(input, output);
+ // LCOV_EXCL_STOP
+}
+
+/// Non-string parsable by a stream
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::other && !std::is_assignable<T &, int>::value,
+ detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ static_assert(is_istreamable<T>::value,
+ "option object type must have a lexical cast overload or streaming input operator(>>) defined, if it "
+ "is convertible from another type use the add_option<T, XC>(...) with XC being the known type");
+ return from_stream(input, output);
+}
+
+/// Assign a value through lexical cast operations
+/// Strings can be empty so we need to do a little different
+template <typename AssignTo,
+ typename ConvertTo,
+ enable_if_t<std::is_same<AssignTo, ConvertTo>::value &&
+ (classify_object<AssignTo>::value == object_category::string_assignable ||
+ classify_object<AssignTo>::value == object_category::string_constructible ||
+ classify_object<AssignTo>::value == object_category::wstring_assignable ||
+ classify_object<AssignTo>::value == object_category::wstring_constructible),
+ detail::enabler> = detail::dummy>
+bool lexical_assign(const std::string &input, AssignTo &output) {
+ return lexical_cast(input, output);
+}
+
+/// Assign a value through lexical cast operations
+template <typename AssignTo,
+ typename ConvertTo,
+ enable_if_t<std::is_same<AssignTo, ConvertTo>::value && std::is_assignable<AssignTo &, AssignTo>::value &&
+ classify_object<AssignTo>::value != object_category::string_assignable &&
+ classify_object<AssignTo>::value != object_category::string_constructible &&
+ classify_object<AssignTo>::value != object_category::wstring_assignable &&
+ classify_object<AssignTo>::value != object_category::wstring_constructible,
+ detail::enabler> = detail::dummy>
+bool lexical_assign(const std::string &input, AssignTo &output) {
+ if(input.empty()) {
+ output = AssignTo{};
+ return true;
+ }
+
+ return lexical_cast(input, output);
+}
+
+/// Assign a value through lexical cast operations
+template <typename AssignTo,
+ typename ConvertTo,
+ enable_if_t<std::is_same<AssignTo, ConvertTo>::value && !std::is_assignable<AssignTo &, AssignTo>::value &&
+ classify_object<AssignTo>::value == object_category::wrapper_value,
+ detail::enabler> = detail::dummy>
+bool lexical_assign(const std::string &input, AssignTo &output) {
+ if(input.empty()) {
+ typename AssignTo::value_type emptyVal{};
+ output = emptyVal;
+ return true;
+ }
+ return lexical_cast(input, output);
+}
+
+/// Assign a value through lexical cast operations for int compatible values
+/// mainly for atomic operations on some compilers
+template <typename AssignTo,
+ typename ConvertTo,
+ enable_if_t<std::is_same<AssignTo, ConvertTo>::value && !std::is_assignable<AssignTo &, AssignTo>::value &&
+ classify_object<AssignTo>::value != object_category::wrapper_value &&
+ std::is_assignable<AssignTo &, int>::value,
+ detail::enabler> = detail::dummy>
+bool lexical_assign(const std::string &input, AssignTo &output) {
+ if(input.empty()) {
+ output = 0;
+ return true;
+ }
+ int val{0};
+ if(lexical_cast(input, val)) {
+#if defined(__clang__)
+/* on some older clang compilers */
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wsign-conversion"
+#endif
+ output = val;
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#endif
+ return true;
+ }
+ return false;
+}
+
+/// Assign a value converted from a string in lexical cast to the output value directly
+template <typename AssignTo,
+ typename ConvertTo,
+ enable_if_t<!std::is_same<AssignTo, ConvertTo>::value && std::is_assignable<AssignTo &, ConvertTo &>::value,
+ detail::enabler> = detail::dummy>
+bool lexical_assign(const std::string &input, AssignTo &output) {
+ ConvertTo val{};
+ bool parse_result = (!input.empty()) ? lexical_cast(input, val) : true;
+ if(parse_result) {
+ output = val;
+ }
+ return parse_result;
+}
+
+/// Assign a value from a lexical cast through constructing a value and move assigning it
+template <
+ typename AssignTo,
+ typename ConvertTo,
+ enable_if_t<!std::is_same<AssignTo, ConvertTo>::value && !std::is_assignable<AssignTo &, ConvertTo &>::value &&
+ std::is_move_assignable<AssignTo>::value,
+ detail::enabler> = detail::dummy>
+bool lexical_assign(const std::string &input, AssignTo &output) {
+ ConvertTo val{};
+ bool parse_result = input.empty() ? true : lexical_cast(input, val);
+ if(parse_result) {
+ output = AssignTo(val); // use () form of constructor to allow some implicit conversions
+ }
+ return parse_result;
+}
+
+/// primary lexical conversion operation, 1 string to 1 type of some kind
+template <typename AssignTo,
+ typename ConvertTo,
+ enable_if_t<classify_object<ConvertTo>::value <= object_category::other &&
+ classify_object<AssignTo>::value <= object_category::wrapper_value,
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
+ return lexical_assign<AssignTo, ConvertTo>(strings[0], output);
+}
+
+/// Lexical conversion if there is only one element but the conversion type is for two, then call a two element
+/// constructor
+template <typename AssignTo,
+ typename ConvertTo,
+ enable_if_t<(type_count<AssignTo>::value <= 2) && expected_count<AssignTo>::value == 1 &&
+ is_tuple_like<ConvertTo>::value && type_count_base<ConvertTo>::value == 2,
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
+ // the remove const is to handle pair types coming from a container
+ using FirstType = typename std::remove_const<typename std::tuple_element<0, ConvertTo>::type>::type;
+ using SecondType = typename std::tuple_element<1, ConvertTo>::type;
+ FirstType v1;
+ SecondType v2;
+ bool retval = lexical_assign<FirstType, FirstType>(strings[0], v1);
+ retval = retval && lexical_assign<SecondType, SecondType>((strings.size() > 1) ? strings[1] : std::string{}, v2);
+ if(retval) {
+ output = AssignTo{v1, v2};
+ }
+ return retval;
+}
+
+/// Lexical conversion of a container types of single elements
+template <class AssignTo,
+ class ConvertTo,
+ enable_if_t<is_mutable_container<AssignTo>::value && is_mutable_container<ConvertTo>::value &&
+ type_count<ConvertTo>::value == 1,
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
+ output.erase(output.begin(), output.end());
+ if(strings.empty()) {
+ return true;
+ }
+ if(strings.size() == 1 && strings[0] == "{}") {
+ return true;
+ }
+ bool skip_remaining = false;
+ if(strings.size() == 2 && strings[0] == "{}" && is_separator(strings[1])) {
+ skip_remaining = true;
+ }
+ for(const auto &elem : strings) {
+ typename AssignTo::value_type out;
+ bool retval = lexical_assign<typename AssignTo::value_type, typename ConvertTo::value_type>(elem, out);
+ if(!retval) {
+ return false;
+ }
+ output.insert(output.end(), std::move(out));
+ if(skip_remaining) {
+ break;
+ }
+ }
+ return (!output.empty());
+}
+
+/// Lexical conversion for complex types
+template <class AssignTo, class ConvertTo, enable_if_t<is_complex<ConvertTo>::value, detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std::string> &strings, AssignTo &output) {
+
+ if(strings.size() >= 2 && !strings[1].empty()) {
+ using XC2 = typename wrapped_type<ConvertTo, double>::type;
+ XC2 x{0.0}, y{0.0};
+ auto str1 = strings[1];
+ if(str1.back() == 'i' || str1.back() == 'j') {
+ str1.pop_back();
+ }
+ auto worked = lexical_cast(strings[0], x) && lexical_cast(str1, y);
+ if(worked) {
+ output = ConvertTo{x, y};
+ }
+ return worked;
+ }
+ return lexical_assign<AssignTo, ConvertTo>(strings[0], output);
+}
+
+/// Conversion to a vector type using a particular single type as the conversion type
+template <class AssignTo,
+ class ConvertTo,
+ enable_if_t<is_mutable_container<AssignTo>::value && (expected_count<ConvertTo>::value == 1) &&
+ (type_count<ConvertTo>::value == 1),
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
+ bool retval = true;
+ output.clear();
+ output.reserve(strings.size());
+ for(const auto &elem : strings) {
+
+ output.emplace_back();
+ retval = retval && lexical_assign<typename AssignTo::value_type, ConvertTo>(elem, output.back());
+ }
+ return (!output.empty()) && retval;
+}
+
+// forward declaration
+
+/// Lexical conversion of a container types with conversion type of two elements
+template <class AssignTo,
+ class ConvertTo,
+ enable_if_t<is_mutable_container<AssignTo>::value && is_mutable_container<ConvertTo>::value &&
+ type_count_base<ConvertTo>::value == 2,
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(std::vector<std::string> strings, AssignTo &output);
+
+/// Lexical conversion of a vector types with type_size >2 forward declaration
+template <class AssignTo,
+ class ConvertTo,
+ enable_if_t<is_mutable_container<AssignTo>::value && is_mutable_container<ConvertTo>::value &&
+ type_count_base<ConvertTo>::value != 2 &&
+ ((type_count<ConvertTo>::value > 2) ||
+ (type_count<ConvertTo>::value > type_count_base<ConvertTo>::value)),
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std::string> &strings, AssignTo &output);
+
+/// Conversion for tuples
+template <class AssignTo,
+ class ConvertTo,
+ enable_if_t<is_tuple_like<AssignTo>::value && is_tuple_like<ConvertTo>::value &&
+ (type_count_base<ConvertTo>::value != type_count<ConvertTo>::value ||
+ type_count<ConvertTo>::value > 2),
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std::string> &strings, AssignTo &output); // forward declaration
+
+/// Conversion for operations where the assigned type is some class but the conversion is a mutable container or large
+/// tuple
+template <typename AssignTo,
+ typename ConvertTo,
+ enable_if_t<!is_tuple_like<AssignTo>::value && !is_mutable_container<AssignTo>::value &&
+ classify_object<ConvertTo>::value != object_category::wrapper_value &&
+ (is_mutable_container<ConvertTo>::value || type_count<ConvertTo>::value > 2),
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
+
+ if(strings.size() > 1 || (!strings.empty() && !(strings.front().empty()))) {
+ ConvertTo val;
+ auto retval = lexical_conversion<ConvertTo, ConvertTo>(strings, val);
+ output = AssignTo{val};
+ return retval;
+ }
+ output = AssignTo{};
+ return true;
+}
+
+/// function template for converting tuples if the static Index is greater than the tuple size
+template <class AssignTo, class ConvertTo, std::size_t I>
+inline typename std::enable_if<(I >= type_count_base<AssignTo>::value), bool>::type
+tuple_conversion(const std::vector<std::string> &, AssignTo &) {
+ return true;
+}
+
+/// Conversion of a tuple element where the type size ==1 and not a mutable container
+template <class AssignTo, class ConvertTo>
+inline typename std::enable_if<!is_mutable_container<ConvertTo>::value && type_count<ConvertTo>::value == 1, bool>::type
+tuple_type_conversion(std::vector<std::string> &strings, AssignTo &output) {
+ auto retval = lexical_assign<AssignTo, ConvertTo>(strings[0], output);
+ strings.erase(strings.begin());
+ return retval;
+}
+
+/// Conversion of a tuple element where the type size !=1 but the size is fixed and not a mutable container
+template <class AssignTo, class ConvertTo>
+inline typename std::enable_if<!is_mutable_container<ConvertTo>::value && (type_count<ConvertTo>::value > 1) &&
+ type_count<ConvertTo>::value == type_count_min<ConvertTo>::value,
+ bool>::type
+tuple_type_conversion(std::vector<std::string> &strings, AssignTo &output) {
+ auto retval = lexical_conversion<AssignTo, ConvertTo>(strings, output);
+ strings.erase(strings.begin(), strings.begin() + type_count<ConvertTo>::value);
+ return retval;
+}
+
+/// Conversion of a tuple element where the type is a mutable container or a type with different min and max type sizes
+template <class AssignTo, class ConvertTo>
+inline typename std::enable_if<is_mutable_container<ConvertTo>::value ||
+ type_count<ConvertTo>::value != type_count_min<ConvertTo>::value,
+ bool>::type
+tuple_type_conversion(std::vector<std::string> &strings, AssignTo &output) {
+
+ std::size_t index{subtype_count_min<ConvertTo>::value};
+ const std::size_t mx_count{subtype_count<ConvertTo>::value};
+ const std::size_t mx{(std::min)(mx_count, strings.size() - 1)};
+
+ while(index < mx) {
+ if(is_separator(strings[index])) {
+ break;
+ }
+ ++index;
+ }
+ bool retval = lexical_conversion<AssignTo, ConvertTo>(
+ std::vector<std::string>(strings.begin(), strings.begin() + static_cast<std::ptrdiff_t>(index)), output);
+ if(strings.size() > index) {
+ strings.erase(strings.begin(), strings.begin() + static_cast<std::ptrdiff_t>(index) + 1);
+ } else {
+ strings.clear();
+ }
+ return retval;
+}
+
+/// Tuple conversion operation
+template <class AssignTo, class ConvertTo, std::size_t I>
+inline typename std::enable_if<(I < type_count_base<AssignTo>::value), bool>::type
+tuple_conversion(std::vector<std::string> strings, AssignTo &output) {
+ bool retval = true;
+ using ConvertToElement = typename std::
+ conditional<is_tuple_like<ConvertTo>::value, typename std::tuple_element<I, ConvertTo>::type, ConvertTo>::type;
+ if(!strings.empty()) {
+ retval = retval && tuple_type_conversion<typename std::tuple_element<I, AssignTo>::type, ConvertToElement>(
+ strings, std::get<I>(output));
+ }
+ retval = retval && tuple_conversion<AssignTo, ConvertTo, I + 1>(std::move(strings), output);
+ return retval;
+}
+
+/// Lexical conversion of a container types with tuple elements of size 2
+template <class AssignTo,
+ class ConvertTo,
+ enable_if_t<is_mutable_container<AssignTo>::value && is_mutable_container<ConvertTo>::value &&
+ type_count_base<ConvertTo>::value == 2,
+ detail::enabler>>
+bool lexical_conversion(std::vector<std::string> strings, AssignTo &output) {
+ output.clear();
+ while(!strings.empty()) {
+
+ typename std::remove_const<typename std::tuple_element<0, typename ConvertTo::value_type>::type>::type v1;
+ typename std::tuple_element<1, typename ConvertTo::value_type>::type v2;
+ bool retval = tuple_type_conversion<decltype(v1), decltype(v1)>(strings, v1);
+ if(!strings.empty()) {
+ retval = retval && tuple_type_conversion<decltype(v2), decltype(v2)>(strings, v2);
+ }
+ if(retval) {
+ output.insert(output.end(), typename AssignTo::value_type{v1, v2});
+ } else {
+ return false;
+ }
+ }
+ return (!output.empty());
+}
+
+/// lexical conversion of tuples with type count>2 or tuples of types of some element with a type size>=2
+template <class AssignTo,
+ class ConvertTo,
+ enable_if_t<is_tuple_like<AssignTo>::value && is_tuple_like<ConvertTo>::value &&
+ (type_count_base<ConvertTo>::value != type_count<ConvertTo>::value ||
+ type_count<ConvertTo>::value > 2),
+ detail::enabler>>
+bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
+ static_assert(
+ !is_tuple_like<ConvertTo>::value || type_count_base<AssignTo>::value == type_count_base<ConvertTo>::value,
+ "if the conversion type is defined as a tuple it must be the same size as the type you are converting to");
+ return tuple_conversion<AssignTo, ConvertTo, 0>(strings, output);
+}
+
+/// Lexical conversion of a vector types for everything but tuples of two elements and types of size 1
+template <class AssignTo,
+ class ConvertTo,
+ enable_if_t<is_mutable_container<AssignTo>::value && is_mutable_container<ConvertTo>::value &&
+ type_count_base<ConvertTo>::value != 2 &&
+ ((type_count<ConvertTo>::value > 2) ||
+ (type_count<ConvertTo>::value > type_count_base<ConvertTo>::value)),
+ detail::enabler>>
+bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
+ bool retval = true;
+ output.clear();
+ std::vector<std::string> temp;
+ std::size_t ii{0};
+ std::size_t icount{0};
+ std::size_t xcm{type_count<ConvertTo>::value};
+ auto ii_max = strings.size();
+ while(ii < ii_max) {
+ temp.push_back(strings[ii]);
+ ++ii;
+ ++icount;
+ if(icount == xcm || is_separator(temp.back()) || ii == ii_max) {
+ if(static_cast<int>(xcm) > type_count_min<ConvertTo>::value && is_separator(temp.back())) {
+ temp.pop_back();
+ }
+ typename AssignTo::value_type temp_out;
+ retval = retval &&
+ lexical_conversion<typename AssignTo::value_type, typename ConvertTo::value_type>(temp, temp_out);
+ temp.clear();
+ if(!retval) {
+ return false;
+ }
+ output.insert(output.end(), std::move(temp_out));
+ icount = 0;
+ }
+ }
+ return retval;
+}
+
+/// conversion for wrapper types
+template <typename AssignTo,
+ class ConvertTo,
+ enable_if_t<classify_object<ConvertTo>::value == object_category::wrapper_value &&
+ std::is_assignable<ConvertTo &, ConvertTo>::value,
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std::string> &strings, AssignTo &output) {
+ if(strings.empty() || strings.front().empty()) {
+ output = ConvertTo{};
+ return true;
+ }
+ typename ConvertTo::value_type val;
+ if(lexical_conversion<typename ConvertTo::value_type, typename ConvertTo::value_type>(strings, val)) {
+ output = ConvertTo{val};
+ return true;
+ }
+ return false;
+}
+
+/// conversion for wrapper types
+template <typename AssignTo,
+ class ConvertTo,
+ enable_if_t<classify_object<ConvertTo>::value == object_category::wrapper_value &&
+ !std::is_assignable<AssignTo &, ConvertTo>::value,
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std::string> &strings, AssignTo &output) {
+ using ConvertType = typename ConvertTo::value_type;
+ if(strings.empty() || strings.front().empty()) {
+ output = ConvertType{};
+ return true;
+ }
+ ConvertType val;
+ if(lexical_conversion<typename ConvertTo::value_type, typename ConvertTo::value_type>(strings, val)) {
+ output = val;
+ return true;
+ }
+ return false;
+}
+
+/// Sum a vector of strings
+inline std::string sum_string_vector(const std::vector<std::string> &values) {
+ double val{0.0};
+ bool fail{false};
+ std::string output;
+ for(const auto &arg : values) {
+ double tv{0.0};
+ auto comp = lexical_cast(arg, tv);
+ if(!comp) {
+ errno = 0;
+ auto fv = detail::to_flag_value(arg);
+ fail = (errno != 0);
+ if(fail) {
+ break;
+ }
+ tv = static_cast<double>(fv);
+ }
+ val += tv;
+ }
+ if(fail) {
+ for(const auto &arg : values) {
+ output.append(arg);
+ }
+ } else {
+ std::ostringstream out;
+ out.precision(16);
+ out << val;
+ output = out.str();
+ }
+ return output;
+}
+
+} // namespace detail
+
+
+
+namespace detail {
+
+// Returns false if not a short option. Otherwise, sets opt name and rest and returns true
+CLI11_INLINE bool split_short(const std::string ¤t, std::string &name, std::string &rest);
+
+// Returns false if not a long option. Otherwise, sets opt name and other side of = and returns true
+CLI11_INLINE bool split_long(const std::string ¤t, std::string &name, std::string &value);
+
+// Returns false if not a windows style option. Otherwise, sets opt name and value and returns true
+CLI11_INLINE bool split_windows_style(const std::string ¤t, std::string &name, std::string &value);
+
+// Splits a string into multiple long and short names
+CLI11_INLINE std::vector<std::string> split_names(std::string current);
+
+/// extract default flag values either {def} or starting with a !
+CLI11_INLINE std::vector<std::pair<std::string, std::string>> get_default_flag_values(const std::string &str);
+
+/// Get a vector of short names, one of long names, and a single name
+CLI11_INLINE std::tuple<std::vector<std::string>, std::vector<std::string>, std::string>
+get_names(const std::vector<std::string> &input);
+
+} // namespace detail
+
+
+
+namespace detail {
+
+CLI11_INLINE bool split_short(const std::string ¤t, std::string &name, std::string &rest) {
+ if(current.size() > 1 && current[0] == '-' && valid_first_char(current[1])) {
+ name = current.substr(1, 1);
+ rest = current.substr(2);
+ return true;
+ }
+ return false;
+}
+
+CLI11_INLINE bool split_long(const std::string ¤t, std::string &name, std::string &value) {
+ if(current.size() > 2 && current.compare(0, 2, "--") == 0 && valid_first_char(current[2])) {
+ auto loc = current.find_first_of('=');
+ if(loc != std::string::npos) {
+ name = current.substr(2, loc - 2);
+ value = current.substr(loc + 1);
+ } else {
+ name = current.substr(2);
+ value = "";
+ }
+ return true;
+ }
+ return false;
+}
+
+CLI11_INLINE bool split_windows_style(const std::string ¤t, std::string &name, std::string &value) {
+ if(current.size() > 1 && current[0] == '/' && valid_first_char(current[1])) {
+ auto loc = current.find_first_of(':');
+ if(loc != std::string::npos) {
+ name = current.substr(1, loc - 1);
+ value = current.substr(loc + 1);
+ } else {
+ name = current.substr(1);
+ value = "";
+ }
+ return true;
+ }
+ return false;
+}
+
+CLI11_INLINE std::vector<std::string> split_names(std::string current) {
+ std::vector<std::string> output;
+ std::size_t val = 0;
+ while((val = current.find(',')) != std::string::npos) {
+ output.push_back(trim_copy(current.substr(0, val)));
+ current = current.substr(val + 1);
+ }
+ output.push_back(trim_copy(current));
+ return output;
+}
+
+CLI11_INLINE std::vector<std::pair<std::string, std::string>> get_default_flag_values(const std::string &str) {
+ std::vector<std::string> flags = split_names(str);
+ flags.erase(std::remove_if(flags.begin(),
+ flags.end(),
+ [](const std::string &name) {
+ return ((name.empty()) || (!(((name.find_first_of('{') != std::string::npos) &&
+ (name.back() == '}')) ||
+ (name[0] == '!'))));
+ }),
+ flags.end());
+ std::vector<std::pair<std::string, std::string>> output;
+ output.reserve(flags.size());
+ for(auto &flag : flags) {
+ auto def_start = flag.find_first_of('{');
+ std::string defval = "false";
+ if((def_start != std::string::npos) && (flag.back() == '}')) {
+ defval = flag.substr(def_start + 1);
+ defval.pop_back();
+ flag.erase(def_start, std::string::npos); // NOLINT(readability-suspicious-call-argument)
+ }
+ flag.erase(0, flag.find_first_not_of("-!"));
+ output.emplace_back(flag, defval);
+ }
+ return output;
+}
+
+CLI11_INLINE std::tuple<std::vector<std::string>, std::vector<std::string>, std::string>
+get_names(const std::vector<std::string> &input) {
+
+ std::vector<std::string> short_names;
+ std::vector<std::string> long_names;
+ std::string pos_name;
+ for(std::string name : input) {
+ if(name.length() == 0) {
+ continue;
+ }
+ if(name.length() > 1 && name[0] == '-' && name[1] != '-') {
+ if(name.length() == 2 && valid_first_char(name[1]))
+ short_names.emplace_back(1, name[1]);
+ else if(name.length() > 2)
+ throw BadNameString::MissingDash(name);
+ else
+ throw BadNameString::OneCharName(name);
+ } else if(name.length() > 2 && name.substr(0, 2) == "--") {
+ name = name.substr(2);
+ if(valid_name_string(name))
+ long_names.push_back(name);
+ else
+ throw BadNameString::BadLongName(name);
+ } else if(name == "-" || name == "--") {
+ throw BadNameString::DashesOnly(name);
+ } else {
+ if(!pos_name.empty())
+ throw BadNameString::MultiPositionalNames(name);
+ if(valid_name_string(name)) {
+ pos_name = name;
+ } else {
+ throw BadNameString::BadPositionalName(name);
+ }
+ }
+ }
+ return std::make_tuple(short_names, long_names, pos_name);
+}
+
+} // namespace detail
+
+
+
+class App;
+
+/// Holds values to load into Options
+struct ConfigItem {
+ /// This is the list of parents
+ std::vector<std::string> parents{};
+
+ /// This is the name
+ std::string name{};
+ /// Listing of inputs
+ std::vector<std::string> inputs{};
+
+ /// The list of parents and name joined by "."
+ CLI11_NODISCARD std::string fullname() const {
+ std::vector<std::string> tmp = parents;
+ tmp.emplace_back(name);
+ return detail::join(tmp, ".");
+ }
+};
+
+/// This class provides a converter for configuration files.
+class Config {
+ protected:
+ std::vector<ConfigItem> items{};
+
+ public:
+ /// Convert an app into a configuration
+ virtual std::string to_config(const App *, bool, bool, std::string) const = 0;
+
+ /// Convert a configuration into an app
+ virtual std::vector<ConfigItem> from_config(std::istream &) const = 0;
+
+ /// Get a flag value
+ CLI11_NODISCARD virtual std::string to_flag(const ConfigItem &item) const {
+ if(item.inputs.size() == 1) {
+ return item.inputs.at(0);
+ }
+ if(item.inputs.empty()) {
+ return "{}";
+ }
+ throw ConversionError::TooManyInputsFlag(item.fullname()); // LCOV_EXCL_LINE
+ }
+
+ /// Parse a config file, throw an error (ParseError:ConfigParseError or FileError) on failure
+ CLI11_NODISCARD std::vector<ConfigItem> from_file(const std::string &name) const {
+ std::ifstream input{name};
+ if(!input.good())
+ throw FileError::Missing(name);
+
+ return from_config(input);
+ }
+
+ /// Virtual destructor
+ virtual ~Config() = default;
+};
+
+/// This converter works with INI/TOML files; to write INI files use ConfigINI
+class ConfigBase : public Config {
+ protected:
+ /// the character used for comments
+ char commentChar = '#';
+ /// the character used to start an array '\0' is a default to not use
+ char arrayStart = '[';
+ /// the character used to end an array '\0' is a default to not use
+ char arrayEnd = ']';
+ /// the character used to separate elements in an array
+ char arraySeparator = ',';
+ /// the character used separate the name from the value
+ char valueDelimiter = '=';
+ /// the character to use around strings
+ char stringQuote = '"';
+ /// the character to use around single characters and literal strings
+ char literalQuote = '\'';
+ /// the maximum number of layers to allow
+ uint8_t maximumLayers{255};
+ /// the separator used to separator parent layers
+ char parentSeparatorChar{'.'};
+ /// Specify the configuration index to use for arrayed sections
+ int16_t configIndex{-1};
+ /// Specify the configuration section that should be used
+ std::string configSection{};
+
+ public:
+ std::string
+ to_config(const App * /*app*/, bool default_also, bool write_description, std::string prefix) const override;
+
+ std::vector<ConfigItem> from_config(std::istream &input) const override;
+ /// Specify the configuration for comment characters
+ ConfigBase *comment(char cchar) {
+ commentChar = cchar;
+ return this;
+ }
+ /// Specify the start and end characters for an array
+ ConfigBase *arrayBounds(char aStart, char aEnd) {
+ arrayStart = aStart;
+ arrayEnd = aEnd;
+ return this;
+ }
+ /// Specify the delimiter character for an array
+ ConfigBase *arrayDelimiter(char aSep) {
+ arraySeparator = aSep;
+ return this;
+ }
+ /// Specify the delimiter between a name and value
+ ConfigBase *valueSeparator(char vSep) {
+ valueDelimiter = vSep;
+ return this;
+ }
+ /// Specify the quote characters used around strings and literal strings
+ ConfigBase *quoteCharacter(char qString, char literalChar) {
+ stringQuote = qString;
+ literalQuote = literalChar;
+ return this;
+ }
+ /// Specify the maximum number of parents
+ ConfigBase *maxLayers(uint8_t layers) {
+ maximumLayers = layers;
+ return this;
+ }
+ /// Specify the separator to use for parent layers
+ ConfigBase *parentSeparator(char sep) {
+ parentSeparatorChar = sep;
+ return this;
+ }
+ /// get a reference to the configuration section
+ std::string §ionRef() { return configSection; }
+ /// get the section
+ CLI11_NODISCARD const std::string §ion() const { return configSection; }
+ /// specify a particular section of the configuration file to use
+ ConfigBase *section(const std::string §ionName) {
+ configSection = sectionName;
+ return this;
+ }
+
+ /// get a reference to the configuration index
+ int16_t &indexRef() { return configIndex; }
+ /// get the section index
+ CLI11_NODISCARD int16_t index() const { return configIndex; }
+ /// specify a particular index in the section to use (-1) for all sections to use
+ ConfigBase *index(int16_t sectionIndex) {
+ configIndex = sectionIndex;
+ return this;
+ }
+};
+
+/// the default Config is the TOML file format
+using ConfigTOML = ConfigBase;
+
+/// ConfigINI generates a "standard" INI compliant output
+class ConfigINI : public ConfigTOML {
+
+ public:
+ ConfigINI() {
+ commentChar = ';';
+ arrayStart = '\0';
+ arrayEnd = '\0';
+ arraySeparator = ' ';
+ valueDelimiter = '=';
+ }
+};
+
+
+
+class Option;
+
+/// @defgroup validator_group Validators
+
+/// @brief Some validators that are provided
+///
+/// These are simple `std::string(const std::string&)` validators that are useful. They return
+/// a string if the validation fails. A custom struct is provided, as well, with the same user
+/// semantics, but with the ability to provide a new type name.
+/// @{
+
+///
+class Validator {
+ protected:
+ /// This is the description function, if empty the description_ will be used
+ std::function<std::string()> desc_function_{[]() { return std::string{}; }};
+
+ /// This is the base function that is to be called.
+ /// Returns a string error message if validation fails.
+ std::function<std::string(std::string &)> func_{[](std::string &) { return std::string{}; }};
+ /// The name for search purposes of the Validator
+ std::string name_{};
+ /// A Validator will only apply to an indexed value (-1 is all elements)
+ int application_index_ = -1;
+ /// Enable for Validator to allow it to be disabled if need be
+ bool active_{true};
+ /// specify that a validator should not modify the input
+ bool non_modifying_{false};
+
+ Validator(std::string validator_desc, std::function<std::string(std::string &)> func)
+ : desc_function_([validator_desc]() { return validator_desc; }), func_(std::move(func)) {}
+
+ public:
+ Validator() = default;
+ /// Construct a Validator with just the description string
+ explicit Validator(std::string validator_desc) : desc_function_([validator_desc]() { return validator_desc; }) {}
+ /// Construct Validator from basic information
+ Validator(std::function<std::string(std::string &)> op, std::string validator_desc, std::string validator_name = "")
+ : desc_function_([validator_desc]() { return validator_desc; }), func_(std::move(op)),
+ name_(std::move(validator_name)) {}
+ /// Set the Validator operation function
+ Validator &operation(std::function<std::string(std::string &)> op) {
+ func_ = std::move(op);
+ return *this;
+ }
+ /// This is the required operator for a Validator - provided to help
+ /// users (CLI11 uses the member `func` directly)
+ std::string operator()(std::string &str) const;
+
+ /// This is the required operator for a Validator - provided to help
+ /// users (CLI11 uses the member `func` directly)
+ std::string operator()(const std::string &str) const {
+ std::string value = str;
+ return (active_) ? func_(value) : std::string{};
+ }
+
+ /// Specify the type string
+ Validator &description(std::string validator_desc) {
+ desc_function_ = [validator_desc]() { return validator_desc; };
+ return *this;
+ }
+ /// Specify the type string
+ CLI11_NODISCARD Validator description(std::string validator_desc) const;
+
+ /// Generate type description information for the Validator
+ CLI11_NODISCARD std::string get_description() const {
+ if(active_) {
+ return desc_function_();
+ }
+ return std::string{};
+ }
+ /// Specify the type string
+ Validator &name(std::string validator_name) {
+ name_ = std::move(validator_name);
+ return *this;
+ }
+ /// Specify the type string
+ CLI11_NODISCARD Validator name(std::string validator_name) const {
+ Validator newval(*this);
+ newval.name_ = std::move(validator_name);
+ return newval;
+ }
+ /// Get the name of the Validator
+ CLI11_NODISCARD const std::string &get_name() const { return name_; }
+ /// Specify whether the Validator is active or not
+ Validator &active(bool active_val = true) {
+ active_ = active_val;
+ return *this;
+ }
+ /// Specify whether the Validator is active or not
+ CLI11_NODISCARD Validator active(bool active_val = true) const {
+ Validator newval(*this);
+ newval.active_ = active_val;
+ return newval;
+ }
+
+ /// Specify whether the Validator can be modifying or not
+ Validator &non_modifying(bool no_modify = true) {
+ non_modifying_ = no_modify;
+ return *this;
+ }
+ /// Specify the application index of a validator
+ Validator &application_index(int app_index) {
+ application_index_ = app_index;
+ return *this;
+ }
+ /// Specify the application index of a validator
+ CLI11_NODISCARD Validator application_index(int app_index) const {
+ Validator newval(*this);
+ newval.application_index_ = app_index;
+ return newval;
+ }
+ /// Get the current value of the application index
+ CLI11_NODISCARD int get_application_index() const { return application_index_; }
+ /// Get a boolean if the validator is active
+ CLI11_NODISCARD bool get_active() const { return active_; }
+
+ /// Get a boolean if the validator is allowed to modify the input returns true if it can modify the input
+ CLI11_NODISCARD bool get_modifying() const { return !non_modifying_; }
+
+ /// Combining validators is a new validator. Type comes from left validator if function, otherwise only set if the
+ /// same.
+ Validator operator&(const Validator &other) const;
+
+ /// Combining validators is a new validator. Type comes from left validator if function, otherwise only set if the
+ /// same.
+ Validator operator|(const Validator &other) const;
+
+ /// Create a validator that fails when a given validator succeeds
+ Validator operator!() const;
+
+ private:
+ void _merge_description(const Validator &val1, const Validator &val2, const std::string &merger);
+};
+
+/// Class wrapping some of the accessors of Validator
+class CustomValidator : public Validator {
+ public:
+};
+// The implementation of the built in validators is using the Validator class;
+// the user is only expected to use the const (static) versions (since there's no setup).
+// Therefore, this is in detail.
+namespace detail {
+
+/// CLI enumeration of different file types
+enum class path_type { nonexistent, file, directory };
+
+/// get the type of the path from a file name
+CLI11_INLINE path_type check_path(const char *file) noexcept;
+
+/// Check for an existing file (returns error message if check fails)
+class ExistingFileValidator : public Validator {
+ public:
+ ExistingFileValidator();
+};
+
+/// Check for an existing directory (returns error message if check fails)
+class ExistingDirectoryValidator : public Validator {
+ public:
+ ExistingDirectoryValidator();
+};
+
+/// Check for an existing path
+class ExistingPathValidator : public Validator {
+ public:
+ ExistingPathValidator();
+};
+
+/// Check for an non-existing path
+class NonexistentPathValidator : public Validator {
+ public:
+ NonexistentPathValidator();
+};
+
+/// Validate the given string is a legal ipv4 address
+class IPV4Validator : public Validator {
+ public:
+ IPV4Validator();
+};
+
+class EscapedStringTransformer : public Validator {
+ public:
+ EscapedStringTransformer();
+};
+
+} // namespace detail
+
+// Static is not needed here, because global const implies static.
+
+/// Check for existing file (returns error message if check fails)
+const detail::ExistingFileValidator ExistingFile;
+
+/// Check for an existing directory (returns error message if check fails)
+const detail::ExistingDirectoryValidator ExistingDirectory;
+
+/// Check for an existing path
+const detail::ExistingPathValidator ExistingPath;
+
+/// Check for an non-existing path
+const detail::NonexistentPathValidator NonexistentPath;
+
+/// Check for an IP4 address
+const detail::IPV4Validator ValidIPV4;
+
+/// convert escaped characters into their associated values
+const detail::EscapedStringTransformer EscapedString;
+
+/// Validate the input as a particular type
+template <typename DesiredType> class TypeValidator : public Validator {
+ public:
+ explicit TypeValidator(const std::string &validator_name)
+ : Validator(validator_name, [](std::string &input_string) {
+ using CLI::detail::lexical_cast;
+ auto val = DesiredType();
+ if(!lexical_cast(input_string, val)) {
+ return std::string("Failed parsing ") + input_string + " as a " + detail::type_name<DesiredType>();
+ }
+ return std::string();
+ }) {}
+ TypeValidator() : TypeValidator(detail::type_name<DesiredType>()) {}
+};
+
+/// Check for a number
+const TypeValidator<double> Number("NUMBER");
+
+/// Modify a path if the file is a particular default location, can be used as Check or transform
+/// with the error return optionally disabled
+class FileOnDefaultPath : public Validator {
+ public:
+ explicit FileOnDefaultPath(std::string default_path, bool enableErrorReturn = true);
+};
+
+/// Produce a range (factory). Min and max are inclusive.
+class Range : public Validator {
+ public:
+ /// This produces a range with min and max inclusive.
+ ///
+ /// Note that the constructor is templated, but the struct is not, so C++17 is not
+ /// needed to provide nice syntax for Range(a,b).
+ template <typename T>
+ Range(T min_val, T max_val, const std::string &validator_name = std::string{}) : Validator(validator_name) {
+ if(validator_name.empty()) {
+ std::stringstream out;
+ out << detail::type_name<T>() << " in [" << min_val << " - " << max_val << "]";
+ description(out.str());
+ }
+
+ func_ = [min_val, max_val](std::string &input) {
+ using CLI::detail::lexical_cast;
+ T val;
+ bool converted = lexical_cast(input, val);
+ if((!converted) || (val < min_val || val > max_val)) {
+ std::stringstream out;
+ out << "Value " << input << " not in range [";
+ out << min_val << " - " << max_val << "]";
+ return out.str();
+ }
+ return std::string{};
+ };
+ }
+
+ /// Range of one value is 0 to value
+ template <typename T>
+ explicit Range(T max_val, const std::string &validator_name = std::string{})
+ : Range(static_cast<T>(0), max_val, validator_name) {}
+};
+
+/// Check for a non negative number
+const Range NonNegativeNumber((std::numeric_limits<double>::max)(), "NONNEGATIVE");
+
+/// Check for a positive valued number (val>0.0), <double>::min here is the smallest positive number
+const Range PositiveNumber((std::numeric_limits<double>::min)(), (std::numeric_limits<double>::max)(), "POSITIVE");
+
+/// Produce a bounded range (factory). Min and max are inclusive.
+class Bound : public Validator {
+ public:
+ /// This bounds a value with min and max inclusive.
+ ///
+ /// Note that the constructor is templated, but the struct is not, so C++17 is not
+ /// needed to provide nice syntax for Range(a,b).
+ template <typename T> Bound(T min_val, T max_val) {
+ std::stringstream out;
+ out << detail::type_name<T>() << " bounded to [" << min_val << " - " << max_val << "]";
+ description(out.str());
+
+ func_ = [min_val, max_val](std::string &input) {
+ using CLI::detail::lexical_cast;
+ T val;
+ bool converted = lexical_cast(input, val);
+ if(!converted) {
+ return std::string("Value ") + input + " could not be converted";
+ }
+ if(val < min_val)
+ input = detail::to_string(min_val);
+ else if(val > max_val)
+ input = detail::to_string(max_val);
+
+ return std::string{};
+ };
+ }
+
+ /// Range of one value is 0 to value
+ template <typename T> explicit Bound(T max_val) : Bound(static_cast<T>(0), max_val) {}
+};
+
+namespace detail {
+template <typename T,
+ enable_if_t<is_copyable_ptr<typename std::remove_reference<T>::type>::value, detail::enabler> = detail::dummy>
+auto smart_deref(T value) -> decltype(*value) {
+ return *value;
+}
+
+template <
+ typename T,
+ enable_if_t<!is_copyable_ptr<typename std::remove_reference<T>::type>::value, detail::enabler> = detail::dummy>
+typename std::remove_reference<T>::type &smart_deref(T &value) {
+ return value;
+}
+/// Generate a string representation of a set
+template <typename T> std::string generate_set(const T &set) {
+ using element_t = typename detail::element_type<T>::type;
+ using iteration_type_t = typename detail::pair_adaptor<element_t>::value_type; // the type of the object pair
+ std::string out(1, '{');
+ out.append(detail::join(
+ detail::smart_deref(set),
+ [](const iteration_type_t &v) { return detail::pair_adaptor<element_t>::first(v); },
+ ","));
+ out.push_back('}');
+ return out;
+}
+
+/// Generate a string representation of a map
+template <typename T> std::string generate_map(const T &map, bool key_only = false) {
+ using element_t = typename detail::element_type<T>::type;
+ using iteration_type_t = typename detail::pair_adaptor<element_t>::value_type; // the type of the object pair
+ std::string out(1, '{');
+ out.append(detail::join(
+ detail::smart_deref(map),
+ [key_only](const iteration_type_t &v) {
+ std::string res{detail::to_string(detail::pair_adaptor<element_t>::first(v))};
+
+ if(!key_only) {
+ res.append("->");
+ res += detail::to_string(detail::pair_adaptor<element_t>::second(v));
+ }
+ return res;
+ },
+ ","));
+ out.push_back('}');
+ return out;
+}
+
+template <typename C, typename V> struct has_find {
+ template <typename CC, typename VV>
+ static auto test(int) -> decltype(std::declval<CC>().find(std::declval<VV>()), std::true_type());
+ template <typename, typename> static auto test(...) -> decltype(std::false_type());
+
+ static const auto value = decltype(test<C, V>(0))::value;
+ using type = std::integral_constant<bool, value>;
+};
+
+/// A search function
+template <typename T, typename V, enable_if_t<!has_find<T, V>::value, detail::enabler> = detail::dummy>
+auto search(const T &set, const V &val) -> std::pair<bool, decltype(std::begin(detail::smart_deref(set)))> {
+ using element_t = typename detail::element_type<T>::type;
+ auto &setref = detail::smart_deref(set);
+ auto it = std::find_if(std::begin(setref), std::end(setref), [&val](decltype(*std::begin(setref)) v) {
+ return (detail::pair_adaptor<element_t>::first(v) == val);
+ });
+ return {(it != std::end(setref)), it};
+}
+
+/// A search function that uses the built in find function
+template <typename T, typename V, enable_if_t<has_find<T, V>::value, detail::enabler> = detail::dummy>
+auto search(const T &set, const V &val) -> std::pair<bool, decltype(std::begin(detail::smart_deref(set)))> {
+ auto &setref = detail::smart_deref(set);
+ auto it = setref.find(val);
+ return {(it != std::end(setref)), it};
+}
+
+/// A search function with a filter function
+template <typename T, typename V>
+auto search(const T &set, const V &val, const std::function<V(V)> &filter_function)
+ -> std::pair<bool, decltype(std::begin(detail::smart_deref(set)))> {
+ using element_t = typename detail::element_type<T>::type;
+ // do the potentially faster first search
+ auto res = search(set, val);
+ if((res.first) || (!(filter_function))) {
+ return res;
+ }
+ // if we haven't found it do the longer linear search with all the element translations
+ auto &setref = detail::smart_deref(set);
+ auto it = std::find_if(std::begin(setref), std::end(setref), [&](decltype(*std::begin(setref)) v) {
+ V a{detail::pair_adaptor<element_t>::first(v)};
+ a = filter_function(a);
+ return (a == val);
+ });
+ return {(it != std::end(setref)), it};
+}
+
+// the following suggestion was made by Nikita Ofitserov(@himikof)
+// done in templates to prevent compiler warnings on negation of unsigned numbers
+
+/// Do a check for overflow on signed numbers
+template <typename T>
+inline typename std::enable_if<std::is_signed<T>::value, T>::type overflowCheck(const T &a, const T &b) {
+ if((a > 0) == (b > 0)) {
+ return ((std::numeric_limits<T>::max)() / (std::abs)(a) < (std::abs)(b));
+ }
+ return ((std::numeric_limits<T>::min)() / (std::abs)(a) > -(std::abs)(b));
+}
+/// Do a check for overflow on unsigned numbers
+template <typename T>
+inline typename std::enable_if<!std::is_signed<T>::value, T>::type overflowCheck(const T &a, const T &b) {
+ return ((std::numeric_limits<T>::max)() / a < b);
+}
+
+/// Performs a *= b; if it doesn't cause integer overflow. Returns false otherwise.
+template <typename T> typename std::enable_if<std::is_integral<T>::value, bool>::type checked_multiply(T &a, T b) {
+ if(a == 0 || b == 0 || a == 1 || b == 1) {
+ a *= b;
+ return true;
+ }
+ if(a == (std::numeric_limits<T>::min)() || b == (std::numeric_limits<T>::min)()) {
+ return false;
+ }
+ if(overflowCheck(a, b)) {
+ return false;
+ }
+ a *= b;
+ return true;
+}
+
+/// Performs a *= b; if it doesn't equal infinity. Returns false otherwise.
+template <typename T>
+typename std::enable_if<std::is_floating_point<T>::value, bool>::type checked_multiply(T &a, T b) {
+ T c = a * b;
+ if(std::isinf(c) && !std::isinf(a) && !std::isinf(b)) {
+ return false;
+ }
+ a = c;
+ return true;
+}
+
+} // namespace detail
+/// Verify items are in a set
+class IsMember : public Validator {
+ public:
+ using filter_fn_t = std::function<std::string(std::string)>;
+
+ /// This allows in-place construction using an initializer list
+ template <typename T, typename... Args>
+ IsMember(std::initializer_list<T> values, Args &&...args)
+ : IsMember(std::vector<T>(values), std::forward<Args>(args)...) {}
+
+ /// This checks to see if an item is in a set (empty function)
+ template <typename T> explicit IsMember(T &&set) : IsMember(std::forward<T>(set), nullptr) {}
+
+ /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter
+ /// both sides of the comparison before computing the comparison.
+ template <typename T, typename F> explicit IsMember(T set, F filter_function) {
+
+ // Get the type of the contained item - requires a container have ::value_type
+ // if the type does not have first_type and second_type, these are both value_type
+ using element_t = typename detail::element_type<T>::type; // Removes (smart) pointers if needed
+ using item_t = typename detail::pair_adaptor<element_t>::first_type; // Is value_type if not a map
+
+ using local_item_t = typename IsMemberType<item_t>::type; // This will convert bad types to good ones
+ // (const char * to std::string)
+
+ // Make a local copy of the filter function, using a std::function if not one already
+ std::function<local_item_t(local_item_t)> filter_fn = filter_function;
+
+ // This is the type name for help, it will take the current version of the set contents
+ desc_function_ = [set]() { return detail::generate_set(detail::smart_deref(set)); };
+
+ // This is the function that validates
+ // It stores a copy of the set pointer-like, so shared_ptr will stay alive
+ func_ = [set, filter_fn](std::string &input) {
+ using CLI::detail::lexical_cast;
+ local_item_t b;
+ if(!lexical_cast(input, b)) {
+ throw ValidationError(input); // name is added later
+ }
+ if(filter_fn) {
+ b = filter_fn(b);
+ }
+ auto res = detail::search(set, b, filter_fn);
+ if(res.first) {
+ // Make sure the version in the input string is identical to the one in the set
+ if(filter_fn) {
+ input = detail::value_string(detail::pair_adaptor<element_t>::first(*(res.second)));
+ }
+
+ // Return empty error string (success)
+ return std::string{};
+ }
+
+ // If you reach this point, the result was not found
+ return input + " not in " + detail::generate_set(detail::smart_deref(set));
+ };
+ }
+
+ /// You can pass in as many filter functions as you like, they nest (string only currently)
+ template <typename T, typename... Args>
+ IsMember(T &&set, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&...other)
+ : IsMember(
+ std::forward<T>(set),
+ [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); },
+ other...) {}
+};
+
+/// definition of the default transformation object
+template <typename T> using TransformPairs = std::vector<std::pair<std::string, T>>;
+
+/// Translate named items to other or a value set
+class Transformer : public Validator {
+ public:
+ using filter_fn_t = std::function<std::string(std::string)>;
+
+ /// This allows in-place construction
+ template <typename... Args>
+ Transformer(std::initializer_list<std::pair<std::string, std::string>> values, Args &&...args)
+ : Transformer(TransformPairs<std::string>(values), std::forward<Args>(args)...) {}
+
+ /// direct map of std::string to std::string
+ template <typename T> explicit Transformer(T &&mapping) : Transformer(std::forward<T>(mapping), nullptr) {}
+
+ /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter
+ /// both sides of the comparison before computing the comparison.
+ template <typename T, typename F> explicit Transformer(T mapping, F filter_function) {
+
+ static_assert(detail::pair_adaptor<typename detail::element_type<T>::type>::value,
+ "mapping must produce value pairs");
+ // Get the type of the contained item - requires a container have ::value_type
+ // if the type does not have first_type and second_type, these are both value_type
+ using element_t = typename detail::element_type<T>::type; // Removes (smart) pointers if needed
+ using item_t = typename detail::pair_adaptor<element_t>::first_type; // Is value_type if not a map
+ using local_item_t = typename IsMemberType<item_t>::type; // Will convert bad types to good ones
+ // (const char * to std::string)
+
+ // Make a local copy of the filter function, using a std::function if not one already
+ std::function<local_item_t(local_item_t)> filter_fn = filter_function;
+
+ // This is the type name for help, it will take the current version of the set contents
+ desc_function_ = [mapping]() { return detail::generate_map(detail::smart_deref(mapping)); };
+
+ func_ = [mapping, filter_fn](std::string &input) {
+ using CLI::detail::lexical_cast;
+ local_item_t b;
+ if(!lexical_cast(input, b)) {
+ return std::string();
+ // there is no possible way we can match anything in the mapping if we can't convert so just return
+ }
+ if(filter_fn) {
+ b = filter_fn(b);
+ }
+ auto res = detail::search(mapping, b, filter_fn);
+ if(res.first) {
+ input = detail::value_string(detail::pair_adaptor<element_t>::second(*res.second));
+ }
+ return std::string{};
+ };
+ }
+
+ /// You can pass in as many filter functions as you like, they nest
+ template <typename T, typename... Args>
+ Transformer(T &&mapping, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&...other)
+ : Transformer(
+ std::forward<T>(mapping),
+ [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); },
+ other...) {}
+};
+
+/// translate named items to other or a value set
+class CheckedTransformer : public Validator {
+ public:
+ using filter_fn_t = std::function<std::string(std::string)>;
+
+ /// This allows in-place construction
+ template <typename... Args>
+ CheckedTransformer(std::initializer_list<std::pair<std::string, std::string>> values, Args &&...args)
+ : CheckedTransformer(TransformPairs<std::string>(values), std::forward<Args>(args)...) {}
+
+ /// direct map of std::string to std::string
+ template <typename T> explicit CheckedTransformer(T mapping) : CheckedTransformer(std::move(mapping), nullptr) {}
+
+ /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter
+ /// both sides of the comparison before computing the comparison.
+ template <typename T, typename F> explicit CheckedTransformer(T mapping, F filter_function) {
+
+ static_assert(detail::pair_adaptor<typename detail::element_type<T>::type>::value,
+ "mapping must produce value pairs");
+ // Get the type of the contained item - requires a container have ::value_type
+ // if the type does not have first_type and second_type, these are both value_type
+ using element_t = typename detail::element_type<T>::type; // Removes (smart) pointers if needed
+ using item_t = typename detail::pair_adaptor<element_t>::first_type; // Is value_type if not a map
+ using local_item_t = typename IsMemberType<item_t>::type; // Will convert bad types to good ones
+ // (const char * to std::string)
+ using iteration_type_t = typename detail::pair_adaptor<element_t>::value_type; // the type of the object pair
+
+ // Make a local copy of the filter function, using a std::function if not one already
+ std::function<local_item_t(local_item_t)> filter_fn = filter_function;
+
+ auto tfunc = [mapping]() {
+ std::string out("value in ");
+ out += detail::generate_map(detail::smart_deref(mapping)) + " OR {";
+ out += detail::join(
+ detail::smart_deref(mapping),
+ [](const iteration_type_t &v) { return detail::to_string(detail::pair_adaptor<element_t>::second(v)); },
+ ",");
+ out.push_back('}');
+ return out;
+ };
+
+ desc_function_ = tfunc;
+
+ func_ = [mapping, tfunc, filter_fn](std::string &input) {
+ using CLI::detail::lexical_cast;
+ local_item_t b;
+ bool converted = lexical_cast(input, b);
+ if(converted) {
+ if(filter_fn) {
+ b = filter_fn(b);
+ }
+ auto res = detail::search(mapping, b, filter_fn);
+ if(res.first) {
+ input = detail::value_string(detail::pair_adaptor<element_t>::second(*res.second));
+ return std::string{};
+ }
+ }
+ for(const auto &v : detail::smart_deref(mapping)) {
+ auto output_string = detail::value_string(detail::pair_adaptor<element_t>::second(v));
+ if(output_string == input) {
+ return std::string();
+ }
+ }
+
+ return "Check " + input + " " + tfunc() + " FAILED";
+ };
+ }
+
+ /// You can pass in as many filter functions as you like, they nest
+ template <typename T, typename... Args>
+ CheckedTransformer(T &&mapping, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&...other)
+ : CheckedTransformer(
+ std::forward<T>(mapping),
+ [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); },
+ other...) {}
+};
+
+/// Helper function to allow ignore_case to be passed to IsMember or Transform
+inline std::string ignore_case(std::string item) { return detail::to_lower(item); }
+
+/// Helper function to allow ignore_underscore to be passed to IsMember or Transform
+inline std::string ignore_underscore(std::string item) { return detail::remove_underscore(item); }
+
+/// Helper function to allow checks to ignore spaces to be passed to IsMember or Transform
+inline std::string ignore_space(std::string item) {
+ item.erase(std::remove(std::begin(item), std::end(item), ' '), std::end(item));
+ item.erase(std::remove(std::begin(item), std::end(item), '\t'), std::end(item));
+ return item;
+}
+
+/// Multiply a number by a factor using given mapping.
+/// Can be used to write transforms for SIZE or DURATION inputs.
+///
+/// Example:
+/// With mapping = `{"b"->1, "kb"->1024, "mb"->1024*1024}`
+/// one can recognize inputs like "100", "12kb", "100 MB",
+/// that will be automatically transformed to 100, 14448, 104857600.
+///
+/// Output number type matches the type in the provided mapping.
+/// Therefore, if it is required to interpret real inputs like "0.42 s",
+/// the mapping should be of a type <string, float> or <string, double>.
+class AsNumberWithUnit : public Validator {
+ public:
+ /// Adjust AsNumberWithUnit behavior.
+ /// CASE_SENSITIVE/CASE_INSENSITIVE controls how units are matched.
+ /// UNIT_OPTIONAL/UNIT_REQUIRED throws ValidationError
+ /// if UNIT_REQUIRED is set and unit literal is not found.
+ enum Options {
+ CASE_SENSITIVE = 0,
+ CASE_INSENSITIVE = 1,
+ UNIT_OPTIONAL = 0,
+ UNIT_REQUIRED = 2,
+ DEFAULT = CASE_INSENSITIVE | UNIT_OPTIONAL
+ };
+
+ template <typename Number>
+ explicit AsNumberWithUnit(std::map<std::string, Number> mapping,
+ Options opts = DEFAULT,
+ const std::string &unit_name = "UNIT") {
+ description(generate_description<Number>(unit_name, opts));
+ validate_mapping(mapping, opts);
+
+ // transform function
+ func_ = [mapping, opts](std::string &input) -> std::string {
+ Number num{};
+
+ detail::rtrim(input);
+ if(input.empty()) {
+ throw ValidationError("Input is empty");
+ }
+
+ // Find split position between number and prefix
+ auto unit_begin = input.end();
+ while(unit_begin > input.begin() && std::isalpha(*(unit_begin - 1), std::locale())) {
+ --unit_begin;
+ }
+
+ std::string unit{unit_begin, input.end()};
+ input.resize(static_cast<std::size_t>(std::distance(input.begin(), unit_begin)));
+ detail::trim(input);
+
+ if(opts & UNIT_REQUIRED && unit.empty()) {
+ throw ValidationError("Missing mandatory unit");
+ }
+ if(opts & CASE_INSENSITIVE) {
+ unit = detail::to_lower(unit);
+ }
+ if(unit.empty()) {
+ using CLI::detail::lexical_cast;
+ if(!lexical_cast(input, num)) {
+ throw ValidationError(std::string("Value ") + input + " could not be converted to " +
+ detail::type_name<Number>());
+ }
+ // No need to modify input if no unit passed
+ return {};
+ }
+
+ // find corresponding factor
+ auto it = mapping.find(unit);
+ if(it == mapping.end()) {
+ throw ValidationError(unit +
+ " unit not recognized. "
+ "Allowed values: " +
+ detail::generate_map(mapping, true));
+ }
+
+ if(!input.empty()) {
+ using CLI::detail::lexical_cast;
+ bool converted = lexical_cast(input, num);
+ if(!converted) {
+ throw ValidationError(std::string("Value ") + input + " could not be converted to " +
+ detail::type_name<Number>());
+ }
+ // perform safe multiplication
+ bool ok = detail::checked_multiply(num, it->second);
+ if(!ok) {
+ throw ValidationError(detail::to_string(num) + " multiplied by " + unit +
+ " factor would cause number overflow. Use smaller value.");
+ }
+ } else {
+ num = static_cast<Number>(it->second);
+ }
+
+ input = detail::to_string(num);
+
+ return {};
+ };
+ }
+
+ private:
+ /// Check that mapping contains valid units.
+ /// Update mapping for CASE_INSENSITIVE mode.
+ template <typename Number> static void validate_mapping(std::map<std::string, Number> &mapping, Options opts) {
+ for(auto &kv : mapping) {
+ if(kv.first.empty()) {
+ throw ValidationError("Unit must not be empty.");
+ }
+ if(!detail::isalpha(kv.first)) {
+ throw ValidationError("Unit must contain only letters.");
+ }
+ }
+
+ // make all units lowercase if CASE_INSENSITIVE
+ if(opts & CASE_INSENSITIVE) {
+ std::map<std::string, Number> lower_mapping;
+ for(auto &kv : mapping) {
+ auto s = detail::to_lower(kv.first);
+ if(lower_mapping.count(s)) {
+ throw ValidationError(std::string("Several matching lowercase unit representations are found: ") +
+ s);
+ }
+ lower_mapping[detail::to_lower(kv.first)] = kv.second;
+ }
+ mapping = std::move(lower_mapping);
+ }
+ }
+
+ /// Generate description like this: NUMBER [UNIT]
+ template <typename Number> static std::string generate_description(const std::string &name, Options opts) {
+ std::stringstream out;
+ out << detail::type_name<Number>() << ' ';
+ if(opts & UNIT_REQUIRED) {
+ out << name;
+ } else {
+ out << '[' << name << ']';
+ }
+ return out.str();
+ }
+};
+
+inline AsNumberWithUnit::Options operator|(const AsNumberWithUnit::Options &a, const AsNumberWithUnit::Options &b) {
+ return static_cast<AsNumberWithUnit::Options>(static_cast<int>(a) | static_cast<int>(b));
+}
+
+/// Converts a human-readable size string (with unit literal) to uin64_t size.
+/// Example:
+/// "100" => 100
+/// "1 b" => 100
+/// "10Kb" => 10240 // you can configure this to be interpreted as kilobyte (*1000) or kibibyte (*1024)
+/// "10 KB" => 10240
+/// "10 kb" => 10240
+/// "10 kib" => 10240 // *i, *ib are always interpreted as *bibyte (*1024)
+/// "10kb" => 10240
+/// "2 MB" => 2097152
+/// "2 EiB" => 2^61 // Units up to exibyte are supported
+class AsSizeValue : public AsNumberWithUnit {
+ public:
+ using result_t = std::uint64_t;
+
+ /// If kb_is_1000 is true,
+ /// interpret 'kb', 'k' as 1000 and 'kib', 'ki' as 1024
+ /// (same applies to higher order units as well).
+ /// Otherwise, interpret all literals as factors of 1024.
+ /// The first option is formally correct, but
+ /// the second interpretation is more wide-spread
+ /// (see https://en.wikipedia.org/wiki/Binary_prefix).
+ explicit AsSizeValue(bool kb_is_1000);
+
+ private:
+ /// Get <size unit, factor> mapping
+ static std::map<std::string, result_t> init_mapping(bool kb_is_1000);
+
+ /// Cache calculated mapping
+ static std::map<std::string, result_t> get_mapping(bool kb_is_1000);
+};
+
+namespace detail {
+/// Split a string into a program name and command line arguments
+/// the string is assumed to contain a file name followed by other arguments
+/// the return value contains is a pair with the first argument containing the program name and the second
+/// everything else.
+CLI11_INLINE std::pair<std::string, std::string> split_program_name(std::string commandline);
+
+} // namespace detail
+/// @}
+
+
+
+
+CLI11_INLINE std::string Validator::operator()(std::string &str) const {
+ std::string retstring;
+ if(active_) {
+ if(non_modifying_) {
+ std::string value = str;
+ retstring = func_(value);
+ } else {
+ retstring = func_(str);
+ }
+ }
+ return retstring;
+}
+
+CLI11_NODISCARD CLI11_INLINE Validator Validator::description(std::string validator_desc) const {
+ Validator newval(*this);
+ newval.desc_function_ = [validator_desc]() { return validator_desc; };
+ return newval;
+}
+
+CLI11_INLINE Validator Validator::operator&(const Validator &other) const {
+ Validator newval;
+
+ newval._merge_description(*this, other, " AND ");
+
+ // Give references (will make a copy in lambda function)
+ const std::function<std::string(std::string & filename)> &f1 = func_;
+ const std::function<std::string(std::string & filename)> &f2 = other.func_;
+
+ newval.func_ = [f1, f2](std::string &input) {
+ std::string s1 = f1(input);
+ std::string s2 = f2(input);
+ if(!s1.empty() && !s2.empty())
+ return std::string("(") + s1 + ") AND (" + s2 + ")";
+ return s1 + s2;
+ };
+
+ newval.active_ = active_ && other.active_;
+ newval.application_index_ = application_index_;
+ return newval;
+}
+
+CLI11_INLINE Validator Validator::operator|(const Validator &other) const {
+ Validator newval;
+
+ newval._merge_description(*this, other, " OR ");
+
+ // Give references (will make a copy in lambda function)
+ const std::function<std::string(std::string &)> &f1 = func_;
+ const std::function<std::string(std::string &)> &f2 = other.func_;
+
+ newval.func_ = [f1, f2](std::string &input) {
+ std::string s1 = f1(input);
+ std::string s2 = f2(input);
+ if(s1.empty() || s2.empty())
+ return std::string();
+
+ return std::string("(") + s1 + ") OR (" + s2 + ")";
+ };
+ newval.active_ = active_ && other.active_;
+ newval.application_index_ = application_index_;
+ return newval;
+}
+
+CLI11_INLINE Validator Validator::operator!() const {
+ Validator newval;
+ const std::function<std::string()> &dfunc1 = desc_function_;
+ newval.desc_function_ = [dfunc1]() {
+ auto str = dfunc1();
+ return (!str.empty()) ? std::string("NOT ") + str : std::string{};
+ };
+ // Give references (will make a copy in lambda function)
+ const std::function<std::string(std::string & res)> &f1 = func_;
+
+ newval.func_ = [f1, dfunc1](std::string &test) -> std::string {
+ std::string s1 = f1(test);
+ if(s1.empty()) {
+ return std::string("check ") + dfunc1() + " succeeded improperly";
+ }
+ return std::string{};
+ };
+ newval.active_ = active_;
+ newval.application_index_ = application_index_;
+ return newval;
+}
+
+CLI11_INLINE void
+Validator::_merge_description(const Validator &val1, const Validator &val2, const std::string &merger) {
+
+ const std::function<std::string()> &dfunc1 = val1.desc_function_;
+ const std::function<std::string()> &dfunc2 = val2.desc_function_;
+
+ desc_function_ = [=]() {
+ std::string f1 = dfunc1();
+ std::string f2 = dfunc2();
+ if((f1.empty()) || (f2.empty())) {
+ return f1 + f2;
+ }
+ return std::string(1, '(') + f1 + ')' + merger + '(' + f2 + ')';
+ };
+}
+
+namespace detail {
+
+#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0
+CLI11_INLINE path_type check_path(const char *file) noexcept {
+ std::error_code ec;
+ auto stat = std::filesystem::status(to_path(file), ec);
+ if(ec) {
+ return path_type::nonexistent;
+ }
+ switch(stat.type()) {
+ case std::filesystem::file_type::none: // LCOV_EXCL_LINE
+ case std::filesystem::file_type::not_found:
+ return path_type::nonexistent; // LCOV_EXCL_LINE
+ case std::filesystem::file_type::directory:
+ return path_type::directory;
+ case std::filesystem::file_type::symlink:
+ case std::filesystem::file_type::block:
+ case std::filesystem::file_type::character:
+ case std::filesystem::file_type::fifo:
+ case std::filesystem::file_type::socket:
+ case std::filesystem::file_type::regular:
+ case std::filesystem::file_type::unknown:
+ default:
+ return path_type::file;
+ }
+}
+#else
+CLI11_INLINE path_type check_path(const char *file) noexcept {
+#if defined(_MSC_VER)
+ struct __stat64 buffer;
+ if(_stat64(file, &buffer) == 0) {
+ return ((buffer.st_mode & S_IFDIR) != 0) ? path_type::directory : path_type::file;
+ }
+#else
+ struct stat buffer;
+ if(stat(file, &buffer) == 0) {
+ return ((buffer.st_mode & S_IFDIR) != 0) ? path_type::directory : path_type::file;
+ }
+#endif
+ return path_type::nonexistent;
+}
+#endif
+
+CLI11_INLINE ExistingFileValidator::ExistingFileValidator() : Validator("FILE") {
+ func_ = [](std::string &filename) {
+ auto path_result = check_path(filename.c_str());
+ if(path_result == path_type::nonexistent) {
+ return "File does not exist: " + filename;
+ }
+ if(path_result == path_type::directory) {
+ return "File is actually a directory: " + filename;
+ }
+ return std::string();
+ };
+}
+
+CLI11_INLINE ExistingDirectoryValidator::ExistingDirectoryValidator() : Validator("DIR") {
+ func_ = [](std::string &filename) {
+ auto path_result = check_path(filename.c_str());
+ if(path_result == path_type::nonexistent) {
+ return "Directory does not exist: " + filename;
+ }
+ if(path_result == path_type::file) {
+ return "Directory is actually a file: " + filename;
+ }
+ return std::string();
+ };
+}
+
+CLI11_INLINE ExistingPathValidator::ExistingPathValidator() : Validator("PATH(existing)") {
+ func_ = [](std::string &filename) {
+ auto path_result = check_path(filename.c_str());
+ if(path_result == path_type::nonexistent) {
+ return "Path does not exist: " + filename;
+ }
+ return std::string();
+ };
+}
+
+CLI11_INLINE NonexistentPathValidator::NonexistentPathValidator() : Validator("PATH(non-existing)") {
+ func_ = [](std::string &filename) {
+ auto path_result = check_path(filename.c_str());
+ if(path_result != path_type::nonexistent) {
+ return "Path already exists: " + filename;
+ }
+ return std::string();
+ };
+}
+
+CLI11_INLINE IPV4Validator::IPV4Validator() : Validator("IPV4") {
+ func_ = [](std::string &ip_addr) {
+ auto result = CLI::detail::split(ip_addr, '.');
+ if(result.size() != 4) {
+ return std::string("Invalid IPV4 address must have four parts (") + ip_addr + ')';
+ }
+ int num = 0;
+ for(const auto &var : result) {
+ using CLI::detail::lexical_cast;
+ bool retval = lexical_cast(var, num);
+ if(!retval) {
+ return std::string("Failed parsing number (") + var + ')';
+ }
+ if(num < 0 || num > 255) {
+ return std::string("Each IP number must be between 0 and 255 ") + var;
+ }
+ }
+ return std::string{};
+ };
+}
+
+CLI11_INLINE EscapedStringTransformer::EscapedStringTransformer() {
+ func_ = [](std::string &str) {
+ try {
+ if(str.size() > 1 && (str.front() == '\"' || str.front() == '\'' || str.front() == '`') &&
+ str.front() == str.back()) {
+ process_quoted_string(str);
+ } else if(str.find_first_of('\\') != std::string::npos) {
+ if(detail::is_binary_escaped_string(str)) {
+ str = detail::extract_binary_string(str);
+ } else {
+ str = remove_escaped_characters(str);
+ }
+ }
+ return std::string{};
+ } catch(const std::invalid_argument &ia) {
+ return std::string(ia.what());
+ }
+ };
+}
+} // namespace detail
+
+CLI11_INLINE FileOnDefaultPath::FileOnDefaultPath(std::string default_path, bool enableErrorReturn)
+ : Validator("FILE") {
+ func_ = [default_path, enableErrorReturn](std::string &filename) {
+ auto path_result = detail::check_path(filename.c_str());
+ if(path_result == detail::path_type::nonexistent) {
+ std::string test_file_path = default_path;
+ if(default_path.back() != '/' && default_path.back() != '\\') {
+ // Add folder separator
+ test_file_path += '/';
+ }
+ test_file_path.append(filename);
+ path_result = detail::check_path(test_file_path.c_str());
+ if(path_result == detail::path_type::file) {
+ filename = test_file_path;
+ } else {
+ if(enableErrorReturn) {
+ return "File does not exist: " + filename;
+ }
+ }
+ }
+ return std::string{};
+ };
+}
+
+CLI11_INLINE AsSizeValue::AsSizeValue(bool kb_is_1000) : AsNumberWithUnit(get_mapping(kb_is_1000)) {
+ if(kb_is_1000) {
+ description("SIZE [b, kb(=1000b), kib(=1024b), ...]");
+ } else {
+ description("SIZE [b, kb(=1024b), ...]");
+ }
+}
+
+CLI11_INLINE std::map<std::string, AsSizeValue::result_t> AsSizeValue::init_mapping(bool kb_is_1000) {
+ std::map<std::string, result_t> m;
+ result_t k_factor = kb_is_1000 ? 1000 : 1024;
+ result_t ki_factor = 1024;
+ result_t k = 1;
+ result_t ki = 1;
+ m["b"] = 1;
+ for(std::string p : {"k", "m", "g", "t", "p", "e"}) {
+ k *= k_factor;
+ ki *= ki_factor;
+ m[p] = k;
+ m[p + "b"] = k;
+ m[p + "i"] = ki;
+ m[p + "ib"] = ki;
+ }
+ return m;
+}
+
+CLI11_INLINE std::map<std::string, AsSizeValue::result_t> AsSizeValue::get_mapping(bool kb_is_1000) {
+ if(kb_is_1000) {
+ static auto m = init_mapping(true);
+ return m;
+ }
+ static auto m = init_mapping(false);
+ return m;
+}
+
+namespace detail {
+
+CLI11_INLINE std::pair<std::string, std::string> split_program_name(std::string commandline) {
+ // try to determine the programName
+ std::pair<std::string, std::string> vals;
+ trim(commandline);
+ auto esp = commandline.find_first_of(' ', 1);
+ while(detail::check_path(commandline.substr(0, esp).c_str()) != path_type::file) {
+ esp = commandline.find_first_of(' ', esp + 1);
+ if(esp == std::string::npos) {
+ // if we have reached the end and haven't found a valid file just assume the first argument is the
+ // program name
+ if(commandline[0] == '"' || commandline[0] == '\'' || commandline[0] == '`') {
+ bool embeddedQuote = false;
+ auto keyChar = commandline[0];
+ auto end = commandline.find_first_of(keyChar, 1);
+ while((end != std::string::npos) && (commandline[end - 1] == '\\')) { // deal with escaped quotes
+ end = commandline.find_first_of(keyChar, end + 1);
+ embeddedQuote = true;
+ }
+ if(end != std::string::npos) {
+ vals.first = commandline.substr(1, end - 1);
+ esp = end + 1;
+ if(embeddedQuote) {
+ vals.first = find_and_replace(vals.first, std::string("\\") + keyChar, std::string(1, keyChar));
+ }
+ } else {
+ esp = commandline.find_first_of(' ', 1);
+ }
+ } else {
+ esp = commandline.find_first_of(' ', 1);
+ }
+
+ break;
+ }
+ }
+ if(vals.first.empty()) {
+ vals.first = commandline.substr(0, esp);
+ rtrim(vals.first);
+ }
+
+ // strip the program name
+ vals.second = (esp < commandline.length() - 1) ? commandline.substr(esp + 1) : std::string{};
+ ltrim(vals.second);
+ return vals;
+}
+
+} // namespace detail
+/// @}
+
+
+
+
+class Option;
+class App;
+
+/// This enum signifies the type of help requested
+///
+/// This is passed in by App; all user classes must accept this as
+/// the second argument.
+
+enum class AppFormatMode {
+ Normal, ///< The normal, detailed help
+ All, ///< A fully expanded help
+ Sub, ///< Used when printed as part of expanded subcommand
+};
+
+/// This is the minimum requirements to run a formatter.
+///
+/// A user can subclass this is if they do not care at all
+/// about the structure in CLI::Formatter.
+class FormatterBase {
+ protected:
+ /// @name Options
+ ///@{
+
+ /// The width of the first column
+ std::size_t column_width_{30};
+
+ /// @brief The required help printout labels (user changeable)
+ /// Values are Needs, Excludes, etc.
+ std::map<std::string, std::string> labels_{};
+
+ ///@}
+ /// @name Basic
+ ///@{
+
+ public:
+ FormatterBase() = default;
+ FormatterBase(const FormatterBase &) = default;
+ FormatterBase(FormatterBase &&) = default;
+ FormatterBase &operator=(const FormatterBase &) = default;
+ FormatterBase &operator=(FormatterBase &&) = default;
+
+ /// Adding a destructor in this form to work around bug in GCC 4.7
+ virtual ~FormatterBase() noexcept {} // NOLINT(modernize-use-equals-default)
+
+ /// This is the key method that puts together help
+ virtual std::string make_help(const App *, std::string, AppFormatMode) const = 0;
+
+ ///@}
+ /// @name Setters
+ ///@{
+
+ /// Set the "REQUIRED" label
+ void label(std::string key, std::string val) { labels_[key] = val; }
+
+ /// Set the column width
+ void column_width(std::size_t val) { column_width_ = val; }
+
+ ///@}
+ /// @name Getters
+ ///@{
+
+ /// Get the current value of a name (REQUIRED, etc.)
+ CLI11_NODISCARD std::string get_label(std::string key) const {
+ if(labels_.find(key) == labels_.end())
+ return key;
+ return labels_.at(key);
+ }
+
+ /// Get the current column width
+ CLI11_NODISCARD std::size_t get_column_width() const { return column_width_; }
+
+ ///@}
+};
+
+/// This is a specialty override for lambda functions
+class FormatterLambda final : public FormatterBase {
+ using funct_t = std::function<std::string(const App *, std::string, AppFormatMode)>;
+
+ /// The lambda to hold and run
+ funct_t lambda_;
+
+ public:
+ /// Create a FormatterLambda with a lambda function
+ explicit FormatterLambda(funct_t funct) : lambda_(std::move(funct)) {}
+
+ /// Adding a destructor (mostly to make GCC 4.7 happy)
+ ~FormatterLambda() noexcept override {} // NOLINT(modernize-use-equals-default)
+
+ /// This will simply call the lambda function
+ std::string make_help(const App *app, std::string name, AppFormatMode mode) const override {
+ return lambda_(app, name, mode);
+ }
+};
+
+/// This is the default Formatter for CLI11. It pretty prints help output, and is broken into quite a few
+/// overridable methods, to be highly customizable with minimal effort.
+class Formatter : public FormatterBase {
+ public:
+ Formatter() = default;
+ Formatter(const Formatter &) = default;
+ Formatter(Formatter &&) = default;
+ Formatter &operator=(const Formatter &) = default;
+ Formatter &operator=(Formatter &&) = default;
+
+ /// @name Overridables
+ ///@{
+
+ /// This prints out a group of options with title
+ ///
+ CLI11_NODISCARD virtual std::string
+ make_group(std::string group, bool is_positional, std::vector<const Option *> opts) const;
+
+ /// This prints out just the positionals "group"
+ virtual std::string make_positionals(const App *app) const;
+
+ /// This prints out all the groups of options
+ std::string make_groups(const App *app, AppFormatMode mode) const;
+
+ /// This prints out all the subcommands
+ virtual std::string make_subcommands(const App *app, AppFormatMode mode) const;
+
+ /// This prints out a subcommand
+ virtual std::string make_subcommand(const App *sub) const;
+
+ /// This prints out a subcommand in help-all
+ virtual std::string make_expanded(const App *sub) const;
+
+ /// This prints out all the groups of options
+ virtual std::string make_footer(const App *app) const;
+
+ /// This displays the description line
+ virtual std::string make_description(const App *app) const;
+
+ /// This displays the usage line
+ virtual std::string make_usage(const App *app, std::string name) const;
+
+ /// This puts everything together
+ std::string make_help(const App * /*app*/, std::string, AppFormatMode) const override;
+
+ ///@}
+ /// @name Options
+ ///@{
+
+ /// This prints out an option help line, either positional or optional form
+ virtual std::string make_option(const Option *opt, bool is_positional) const {
+ std::stringstream out;
+ detail::format_help(
+ out, make_option_name(opt, is_positional) + make_option_opts(opt), make_option_desc(opt), column_width_);
+ return out.str();
+ }
+
+ /// @brief This is the name part of an option, Default: left column
+ virtual std::string make_option_name(const Option *, bool) const;
+
+ /// @brief This is the options part of the name, Default: combined into left column
+ virtual std::string make_option_opts(const Option *) const;
+
+ /// @brief This is the description. Default: Right column, on new line if left column too large
+ virtual std::string make_option_desc(const Option *) const;
+
+ /// @brief This is used to print the name on the USAGE line
+ virtual std::string make_option_usage(const Option *opt) const;
+
+ ///@}
+};
+
+
+
+
+using results_t = std::vector<std::string>;
+/// callback function definition
+using callback_t = std::function<bool(const results_t &)>;
+
+class Option;
+class App;
+
+using Option_p = std::unique_ptr<Option>;
+/// Enumeration of the multiOption Policy selection
+enum class MultiOptionPolicy : char {
+ Throw, //!< Throw an error if any extra arguments were given
+ TakeLast, //!< take only the last Expected number of arguments
+ TakeFirst, //!< take only the first Expected number of arguments
+ Join, //!< merge all the arguments together into a single string via the delimiter character default('\n')
+ TakeAll, //!< just get all the passed argument regardless
+ Sum, //!< sum all the arguments together if numerical or concatenate directly without delimiter
+ Reverse, //!< take only the last Expected number of arguments in reverse order
+};
+
+/// This is the CRTP base class for Option and OptionDefaults. It was designed this way
+/// to share parts of the class; an OptionDefaults can copy to an Option.
+template <typename CRTP> class OptionBase {
+ friend App;
+
+ protected:
+ /// The group membership
+ std::string group_ = std::string("Options");
+
+ /// True if this is a required option
+ bool required_{false};
+
+ /// Ignore the case when matching (option, not value)
+ bool ignore_case_{false};
+
+ /// Ignore underscores when matching (option, not value)
+ bool ignore_underscore_{false};
+
+ /// Allow this option to be given in a configuration file
+ bool configurable_{true};
+
+ /// Disable overriding flag values with '=value'
+ bool disable_flag_override_{false};
+
+ /// Specify a delimiter character for vector arguments
+ char delimiter_{'\0'};
+
+ /// Automatically capture default value
+ bool always_capture_default_{false};
+
+ /// Policy for handling multiple arguments beyond the expected Max
+ MultiOptionPolicy multi_option_policy_{MultiOptionPolicy::Throw};
+
+ /// Copy the contents to another similar class (one based on OptionBase)
+ template <typename T> void copy_to(T *other) const;
+
+ public:
+ // setters
+
+ /// Changes the group membership
+ CRTP *group(const std::string &name) {
+ if(!detail::valid_alias_name_string(name)) {
+ throw IncorrectConstruction("Group names may not contain newlines or null characters");
+ }
+ group_ = name;
+ return static_cast<CRTP *>(this);
+ }
+
+ /// Set the option as required
+ CRTP *required(bool value = true) {
+ required_ = value;
+ return static_cast<CRTP *>(this);
+ }
+
+ /// Support Plumbum term
+ CRTP *mandatory(bool value = true) { return required(value); }
+
+ CRTP *always_capture_default(bool value = true) {
+ always_capture_default_ = value;
+ return static_cast<CRTP *>(this);
+ }
+
+ // Getters
+
+ /// Get the group of this option
+ CLI11_NODISCARD const std::string &get_group() const { return group_; }
+
+ /// True if this is a required option
+ CLI11_NODISCARD bool get_required() const { return required_; }
+
+ /// The status of ignore case
+ CLI11_NODISCARD bool get_ignore_case() const { return ignore_case_; }
+
+ /// The status of ignore_underscore
+ CLI11_NODISCARD bool get_ignore_underscore() const { return ignore_underscore_; }
+
+ /// The status of configurable
+ CLI11_NODISCARD bool get_configurable() const { return configurable_; }
+
+ /// The status of configurable
+ CLI11_NODISCARD bool get_disable_flag_override() const { return disable_flag_override_; }
+
+ /// Get the current delimiter char
+ CLI11_NODISCARD char get_delimiter() const { return delimiter_; }
+
+ /// Return true if this will automatically capture the default value for help printing
+ CLI11_NODISCARD bool get_always_capture_default() const { return always_capture_default_; }
+
+ /// The status of the multi option policy
+ CLI11_NODISCARD MultiOptionPolicy get_multi_option_policy() const { return multi_option_policy_; }
+
+ // Shortcuts for multi option policy
+
+ /// Set the multi option policy to take last
+ CRTP *take_last() {
+ auto *self = static_cast<CRTP *>(this);
+ self->multi_option_policy(MultiOptionPolicy::TakeLast);
+ return self;
+ }
+
+ /// Set the multi option policy to take last
+ CRTP *take_first() {
+ auto *self = static_cast<CRTP *>(this);
+ self->multi_option_policy(MultiOptionPolicy::TakeFirst);
+ return self;
+ }
+
+ /// Set the multi option policy to take all arguments
+ CRTP *take_all() {
+ auto self = static_cast<CRTP *>(this);
+ self->multi_option_policy(MultiOptionPolicy::TakeAll);
+ return self;
+ }
+
+ /// Set the multi option policy to join
+ CRTP *join() {
+ auto *self = static_cast<CRTP *>(this);
+ self->multi_option_policy(MultiOptionPolicy::Join);
+ return self;
+ }
+
+ /// Set the multi option policy to join with a specific delimiter
+ CRTP *join(char delim) {
+ auto self = static_cast<CRTP *>(this);
+ self->delimiter_ = delim;
+ self->multi_option_policy(MultiOptionPolicy::Join);
+ return self;
+ }
+
+ /// Allow in a configuration file
+ CRTP *configurable(bool value = true) {
+ configurable_ = value;
+ return static_cast<CRTP *>(this);
+ }
+
+ /// Allow in a configuration file
+ CRTP *delimiter(char value = '\0') {
+ delimiter_ = value;
+ return static_cast<CRTP *>(this);
+ }
+};
+
+/// This is a version of OptionBase that only supports setting values,
+/// for defaults. It is stored as the default option in an App.
+class OptionDefaults : public OptionBase<OptionDefaults> {
+ public:
+ OptionDefaults() = default;
+
+ // Methods here need a different implementation if they are Option vs. OptionDefault
+
+ /// Take the last argument if given multiple times
+ OptionDefaults *multi_option_policy(MultiOptionPolicy value = MultiOptionPolicy::Throw) {
+ multi_option_policy_ = value;
+ return this;
+ }
+
+ /// Ignore the case of the option name
+ OptionDefaults *ignore_case(bool value = true) {
+ ignore_case_ = value;
+ return this;
+ }
+
+ /// Ignore underscores in the option name
+ OptionDefaults *ignore_underscore(bool value = true) {
+ ignore_underscore_ = value;
+ return this;
+ }
+
+ /// Disable overriding flag values with an '=<value>' segment
+ OptionDefaults *disable_flag_override(bool value = true) {
+ disable_flag_override_ = value;
+ return this;
+ }
+
+ /// set a delimiter character to split up single arguments to treat as multiple inputs
+ OptionDefaults *delimiter(char value = '\0') {
+ delimiter_ = value;
+ return this;
+ }
+};
+
+class Option : public OptionBase<Option> {
+ friend App;
+
+ protected:
+ /// @name Names
+ ///@{
+
+ /// A list of the short names (`-a`) without the leading dashes
+ std::vector<std::string> snames_{};
+
+ /// A list of the long names (`--long`) without the leading dashes
+ std::vector<std::string> lnames_{};
+
+ /// A list of the flag names with the appropriate default value, the first part of the pair should be duplicates of
+ /// what is in snames or lnames but will trigger a particular response on a flag
+ std::vector<std::pair<std::string, std::string>> default_flag_values_{};
+
+ /// a list of flag names with specified default values;
+ std::vector<std::string> fnames_{};
+
+ /// A positional name
+ std::string pname_{};
+
+ /// If given, check the environment for this option
+ std::string envname_{};
+
+ ///@}
+ /// @name Help
+ ///@{
+
+ /// The description for help strings
+ std::string description_{};
+
+ /// A human readable default value, either manually set, captured, or captured by default
+ std::string default_str_{};
+
+ /// If given, replace the text that describes the option type and usage in the help text
+ std::string option_text_{};
+
+ /// A human readable type value, set when App creates this
+ ///
+ /// This is a lambda function so "types" can be dynamic, such as when a set prints its contents.
+ std::function<std::string()> type_name_{[]() { return std::string(); }};
+
+ /// Run this function to capture a default (ignore if empty)
+ std::function<std::string()> default_function_{};
+
+ ///@}
+ /// @name Configuration
+ ///@{
+
+ /// The number of arguments that make up one option. max is the nominal type size, min is the minimum number of
+ /// strings
+ int type_size_max_{1};
+ /// The minimum number of arguments an option should be expecting
+ int type_size_min_{1};
+
+ /// The minimum number of expected values
+ int expected_min_{1};
+ /// The maximum number of expected values
+ int expected_max_{1};
+
+ /// A list of Validators to run on each value parsed
+ std::vector<Validator> validators_{};
+
+ /// A list of options that are required with this option
+ std::set<Option *> needs_{};
+
+ /// A list of options that are excluded with this option
+ std::set<Option *> excludes_{};
+
+ ///@}
+ /// @name Other
+ ///@{
+
+ /// link back up to the parent App for fallthrough
+ App *parent_{nullptr};
+
+ /// Options store a callback to do all the work
+ callback_t callback_{};
+
+ ///@}
+ /// @name Parsing results
+ ///@{
+
+ /// complete Results of parsing
+ results_t results_{};
+ /// results after reduction
+ results_t proc_results_{};
+ /// enumeration for the option state machine
+ enum class option_state : char {
+ parsing = 0, //!< The option is currently collecting parsed results
+ validated = 2, //!< the results have been validated
+ reduced = 4, //!< a subset of results has been generated
+ callback_run = 6, //!< the callback has been executed
+ };
+ /// Whether the callback has run (needed for INI parsing)
+ option_state current_option_state_{option_state::parsing};
+ /// Specify that extra args beyond type_size_max should be allowed
+ bool allow_extra_args_{false};
+ /// Specify that the option should act like a flag vs regular option
+ bool flag_like_{false};
+ /// Control option to run the callback to set the default
+ bool run_callback_for_default_{false};
+ /// flag indicating a separator needs to be injected after each argument call
+ bool inject_separator_{false};
+ /// flag indicating that the option should trigger the validation and callback chain on each result when loaded
+ bool trigger_on_result_{false};
+ /// flag indicating that the option should force the callback regardless if any results present
+ bool force_callback_{false};
+ ///@}
+
+ /// Making an option by hand is not defined, it must be made by the App class
+ Option(std::string option_name, std::string option_description, callback_t callback, App *parent)
+ : description_(std::move(option_description)), parent_(parent), callback_(std::move(callback)) {
+ std::tie(snames_, lnames_, pname_) = detail::get_names(detail::split_names(option_name));
+ }
+
+ public:
+ /// @name Basic
+ ///@{
+
+ Option(const Option &) = delete;
+ Option &operator=(const Option &) = delete;
+
+ /// Count the total number of times an option was passed
+ CLI11_NODISCARD std::size_t count() const { return results_.size(); }
+
+ /// True if the option was not passed
+ CLI11_NODISCARD bool empty() const { return results_.empty(); }
+
+ /// This bool operator returns true if any arguments were passed or the option callback is forced
+ explicit operator bool() const { return !empty() || force_callback_; }
+
+ /// Clear the parsed results (mostly for testing)
+ void clear() {
+ results_.clear();
+ current_option_state_ = option_state::parsing;
+ }
+
+ ///@}
+ /// @name Setting options
+ ///@{
+
+ /// Set the number of expected arguments
+ Option *expected(int value);
+
+ /// Set the range of expected arguments
+ Option *expected(int value_min, int value_max);
+
+ /// Set the value of allow_extra_args which allows extra value arguments on the flag or option to be included
+ /// with each instance
+ Option *allow_extra_args(bool value = true) {
+ allow_extra_args_ = value;
+ return this;
+ }
+ /// Get the current value of allow extra args
+ CLI11_NODISCARD bool get_allow_extra_args() const { return allow_extra_args_; }
+ /// Set the value of trigger_on_parse which specifies that the option callback should be triggered on every parse
+ Option *trigger_on_parse(bool value = true) {
+ trigger_on_result_ = value;
+ return this;
+ }
+ /// The status of trigger on parse
+ CLI11_NODISCARD bool get_trigger_on_parse() const { return trigger_on_result_; }
+
+ /// Set the value of force_callback
+ Option *force_callback(bool value = true) {
+ force_callback_ = value;
+ return this;
+ }
+ /// The status of force_callback
+ CLI11_NODISCARD bool get_force_callback() const { return force_callback_; }
+
+ /// Set the value of run_callback_for_default which controls whether the callback function should be called to set
+ /// the default This is controlled automatically but could be manipulated by the user.
+ Option *run_callback_for_default(bool value = true) {
+ run_callback_for_default_ = value;
+ return this;
+ }
+ /// Get the current value of run_callback_for_default
+ CLI11_NODISCARD bool get_run_callback_for_default() const { return run_callback_for_default_; }
+
+ /// Adds a Validator with a built in type name
+ Option *check(Validator validator, const std::string &validator_name = "");
+
+ /// Adds a Validator. Takes a const string& and returns an error message (empty if conversion/check is okay).
+ Option *check(std::function<std::string(const std::string &)> Validator,
+ std::string Validator_description = "",
+ std::string Validator_name = "");
+
+ /// Adds a transforming Validator with a built in type name
+ Option *transform(Validator Validator, const std::string &Validator_name = "");
+
+ /// Adds a Validator-like function that can change result
+ Option *transform(const std::function<std::string(std::string)> &func,
+ std::string transform_description = "",
+ std::string transform_name = "");
+
+ /// Adds a user supplied function to run on each item passed in (communicate though lambda capture)
+ Option *each(const std::function<void(std::string)> &func);
+
+ /// Get a named Validator
+ Validator *get_validator(const std::string &Validator_name = "");
+
+ /// Get a Validator by index NOTE: this may not be the order of definition
+ Validator *get_validator(int index);
+
+ /// Sets required options
+ Option *needs(Option *opt) {
+ if(opt != this) {
+ needs_.insert(opt);
+ }
+ return this;
+ }
+
+ /// Can find a string if needed
+ template <typename T = App> Option *needs(std::string opt_name) {
+ auto opt = static_cast<T *>(parent_)->get_option_no_throw(opt_name);
+ if(opt == nullptr) {
+ throw IncorrectConstruction::MissingOption(opt_name);
+ }
+ return needs(opt);
+ }
+
+ /// Any number supported, any mix of string and Opt
+ template <typename A, typename B, typename... ARG> Option *needs(A opt, B opt1, ARG... args) {
+ needs(opt);
+ return needs(opt1, args...); // NOLINT(readability-suspicious-call-argument)
+ }
+
+ /// Remove needs link from an option. Returns true if the option really was in the needs list.
+ bool remove_needs(Option *opt);
+
+ /// Sets excluded options
+ Option *excludes(Option *opt);
+
+ /// Can find a string if needed
+ template <typename T = App> Option *excludes(std::string opt_name) {
+ auto opt = static_cast<T *>(parent_)->get_option_no_throw(opt_name);
+ if(opt == nullptr) {
+ throw IncorrectConstruction::MissingOption(opt_name);
+ }
+ return excludes(opt);
+ }
+
+ /// Any number supported, any mix of string and Opt
+ template <typename A, typename B, typename... ARG> Option *excludes(A opt, B opt1, ARG... args) {
+ excludes(opt);
+ return excludes(opt1, args...);
+ }
+
+ /// Remove needs link from an option. Returns true if the option really was in the needs list.
+ bool remove_excludes(Option *opt);
+
+ /// Sets environment variable to read if no option given
+ Option *envname(std::string name) {
+ envname_ = std::move(name);
+ return this;
+ }
+
+ /// Ignore case
+ ///
+ /// The template hides the fact that we don't have the definition of App yet.
+ /// You are never expected to add an argument to the template here.
+ template <typename T = App> Option *ignore_case(bool value = true);
+
+ /// Ignore underscores in the option names
+ ///
+ /// The template hides the fact that we don't have the definition of App yet.
+ /// You are never expected to add an argument to the template here.
+ template <typename T = App> Option *ignore_underscore(bool value = true);
+
+ /// Take the last argument if given multiple times (or another policy)
+ Option *multi_option_policy(MultiOptionPolicy value = MultiOptionPolicy::Throw);
+
+ /// Disable flag overrides values, e.g. --flag=<value> is not allowed
+ Option *disable_flag_override(bool value = true) {
+ disable_flag_override_ = value;
+ return this;
+ }
+ ///@}
+ /// @name Accessors
+ ///@{
+
+ /// The number of arguments the option expects
+ CLI11_NODISCARD int get_type_size() const { return type_size_min_; }
+
+ /// The minimum number of arguments the option expects
+ CLI11_NODISCARD int get_type_size_min() const { return type_size_min_; }
+ /// The maximum number of arguments the option expects
+ CLI11_NODISCARD int get_type_size_max() const { return type_size_max_; }
+
+ /// Return the inject_separator flag
+ CLI11_NODISCARD bool get_inject_separator() const { return inject_separator_; }
+
+ /// The environment variable associated to this value
+ CLI11_NODISCARD std::string get_envname() const { return envname_; }
+
+ /// The set of options needed
+ CLI11_NODISCARD std::set<Option *> get_needs() const { return needs_; }
+
+ /// The set of options excluded
+ CLI11_NODISCARD std::set<Option *> get_excludes() const { return excludes_; }
+
+ /// The default value (for help printing)
+ CLI11_NODISCARD std::string get_default_str() const { return default_str_; }
+
+ /// Get the callback function
+ CLI11_NODISCARD callback_t get_callback() const { return callback_; }
+
+ /// Get the long names
+ CLI11_NODISCARD const std::vector<std::string> &get_lnames() const { return lnames_; }
+
+ /// Get the short names
+ CLI11_NODISCARD const std::vector<std::string> &get_snames() const { return snames_; }
+
+ /// Get the flag names with specified default values
+ CLI11_NODISCARD const std::vector<std::string> &get_fnames() const { return fnames_; }
+ /// Get a single name for the option, first of lname, pname, sname, envname
+ CLI11_NODISCARD const std::string &get_single_name() const {
+ if(!lnames_.empty()) {
+ return lnames_[0];
+ }
+ if(!snames_.empty()) {
+ return snames_[0];
+ }
+ if(!pname_.empty()) {
+ return pname_;
+ }
+ return envname_;
+ }
+ /// The number of times the option expects to be included
+ CLI11_NODISCARD int get_expected() const { return expected_min_; }
+
+ /// The number of times the option expects to be included
+ CLI11_NODISCARD int get_expected_min() const { return expected_min_; }
+ /// The max number of times the option expects to be included
+ CLI11_NODISCARD int get_expected_max() const { return expected_max_; }
+
+ /// The total min number of expected string values to be used
+ CLI11_NODISCARD int get_items_expected_min() const { return type_size_min_ * expected_min_; }
+
+ /// Get the maximum number of items expected to be returned and used for the callback
+ CLI11_NODISCARD int get_items_expected_max() const {
+ int t = type_size_max_;
+ return detail::checked_multiply(t, expected_max_) ? t : detail::expected_max_vector_size;
+ }
+ /// The total min number of expected string values to be used
+ CLI11_NODISCARD int get_items_expected() const { return get_items_expected_min(); }
+
+ /// True if the argument can be given directly
+ CLI11_NODISCARD bool get_positional() const { return !pname_.empty(); }
+
+ /// True if option has at least one non-positional name
+ CLI11_NODISCARD bool nonpositional() const { return (!lnames_.empty() || !snames_.empty()); }
+
+ /// True if option has description
+ CLI11_NODISCARD bool has_description() const { return !description_.empty(); }
+
+ /// Get the description
+ CLI11_NODISCARD const std::string &get_description() const { return description_; }
+
+ /// Set the description
+ Option *description(std::string option_description) {
+ description_ = std::move(option_description);
+ return this;
+ }
+
+ Option *option_text(std::string text) {
+ option_text_ = std::move(text);
+ return this;
+ }
+
+ CLI11_NODISCARD const std::string &get_option_text() const { return option_text_; }
+
+ ///@}
+ /// @name Help tools
+ ///@{
+
+ /// \brief Gets a comma separated list of names.
+ /// Will include / prefer the positional name if positional is true.
+ /// If all_options is false, pick just the most descriptive name to show.
+ /// Use `get_name(true)` to get the positional name (replaces `get_pname`)
+ CLI11_NODISCARD std::string get_name(bool positional = false, ///< Show the positional name
+ bool all_options = false ///< Show every option
+ ) const;
+
+ ///@}
+ /// @name Parser tools
+ ///@{
+
+ /// Process the callback
+ void run_callback();
+
+ /// If options share any of the same names, find it
+ CLI11_NODISCARD const std::string &matching_name(const Option &other) const;
+
+ /// If options share any of the same names, they are equal (not counting positional)
+ bool operator==(const Option &other) const { return !matching_name(other).empty(); }
+
+ /// Check a name. Requires "-" or "--" for short / long, supports positional name
+ CLI11_NODISCARD bool check_name(const std::string &name) const;
+
+ /// Requires "-" to be removed from string
+ CLI11_NODISCARD bool check_sname(std::string name) const {
+ return (detail::find_member(std::move(name), snames_, ignore_case_) >= 0);
+ }
+
+ /// Requires "--" to be removed from string
+ CLI11_NODISCARD bool check_lname(std::string name) const {
+ return (detail::find_member(std::move(name), lnames_, ignore_case_, ignore_underscore_) >= 0);
+ }
+
+ /// Requires "--" to be removed from string
+ CLI11_NODISCARD bool check_fname(std::string name) const {
+ if(fnames_.empty()) {
+ return false;
+ }
+ return (detail::find_member(std::move(name), fnames_, ignore_case_, ignore_underscore_) >= 0);
+ }
+
+ /// Get the value that goes for a flag, nominally gets the default value but allows for overrides if not
+ /// disabled
+ CLI11_NODISCARD std::string get_flag_value(const std::string &name, std::string input_value) const;
+
+ /// Puts a result at the end
+ Option *add_result(std::string s);
+
+ /// Puts a result at the end and get a count of the number of arguments actually added
+ Option *add_result(std::string s, int &results_added);
+
+ /// Puts a result at the end
+ Option *add_result(std::vector<std::string> s);
+
+ /// Get the current complete results set
+ CLI11_NODISCARD const results_t &results() const { return results_; }
+
+ /// Get a copy of the results
+ CLI11_NODISCARD results_t reduced_results() const;
+
+ /// Get the results as a specified type
+ template <typename T> void results(T &output) const {
+ bool retval = false;
+ if(current_option_state_ >= option_state::reduced || (results_.size() == 1 && validators_.empty())) {
+ const results_t &res = (proc_results_.empty()) ? results_ : proc_results_;
+ retval = detail::lexical_conversion<T, T>(res, output);
+ } else {
+ results_t res;
+ if(results_.empty()) {
+ if(!default_str_.empty()) {
+ // _add_results takes an rvalue only
+ _add_result(std::string(default_str_), res);
+ _validate_results(res);
+ results_t extra;
+ _reduce_results(extra, res);
+ if(!extra.empty()) {
+ res = std::move(extra);
+ }
+ } else {
+ res.emplace_back();
+ }
+ } else {
+ res = reduced_results();
+ }
+ retval = detail::lexical_conversion<T, T>(res, output);
+ }
+ if(!retval) {
+ throw ConversionError(get_name(), results_);
+ }
+ }
+
+ /// Return the results as the specified type
+ template <typename T> CLI11_NODISCARD T as() const {
+ T output;
+ results(output);
+ return output;
+ }
+
+ /// See if the callback has been run already
+ CLI11_NODISCARD bool get_callback_run() const { return (current_option_state_ == option_state::callback_run); }
+
+ ///@}
+ /// @name Custom options
+ ///@{
+
+ /// Set the type function to run when displayed on this option
+ Option *type_name_fn(std::function<std::string()> typefun) {
+ type_name_ = std::move(typefun);
+ return this;
+ }
+
+ /// Set a custom option typestring
+ Option *type_name(std::string typeval) {
+ type_name_fn([typeval]() { return typeval; });
+ return this;
+ }
+
+ /// Set a custom option size
+ Option *type_size(int option_type_size);
+
+ /// Set a custom option type size range
+ Option *type_size(int option_type_size_min, int option_type_size_max);
+
+ /// Set the value of the separator injection flag
+ void inject_separator(bool value = true) { inject_separator_ = value; }
+
+ /// Set a capture function for the default. Mostly used by App.
+ Option *default_function(const std::function<std::string()> &func) {
+ default_function_ = func;
+ return this;
+ }
+
+ /// Capture the default value from the original value (if it can be captured)
+ Option *capture_default_str() {
+ if(default_function_) {
+ default_str_ = default_function_();
+ }
+ return this;
+ }
+
+ /// Set the default value string representation (does not change the contained value)
+ Option *default_str(std::string val) {
+ default_str_ = std::move(val);
+ return this;
+ }
+
+ /// Set the default value and validate the results and run the callback if appropriate to set the value into the
+ /// bound value only available for types that can be converted to a string
+ template <typename X> Option *default_val(const X &val) {
+ std::string val_str = detail::to_string(val);
+ auto old_option_state = current_option_state_;
+ results_t old_results{std::move(results_)};
+ results_.clear();
+ try {
+ add_result(val_str);
+ // if trigger_on_result_ is set the callback already ran
+ if(run_callback_for_default_ && !trigger_on_result_) {
+ run_callback(); // run callback sets the state, we need to reset it again
+ current_option_state_ = option_state::parsing;
+ } else {
+ _validate_results(results_);
+ current_option_state_ = old_option_state;
+ }
+ } catch(const CLI::Error &) {
+ // this should be done
+ results_ = std::move(old_results);
+ current_option_state_ = old_option_state;
+ throw;
+ }
+ results_ = std::move(old_results);
+ default_str_ = std::move(val_str);
+ return this;
+ }
+
+ /// Get the full typename for this option
+ CLI11_NODISCARD std::string get_type_name() const;
+
+ private:
+ /// Run the results through the Validators
+ void _validate_results(results_t &res) const;
+
+ /** reduce the results in accordance with the MultiOptionPolicy
+ @param[out] out results are assigned to res if there if they are different
+ */
+ void _reduce_results(results_t &out, const results_t &original) const;
+
+ // Run a result through the Validators
+ std::string _validate(std::string &result, int index) const;
+
+ /// Add a single result to the result set, taking into account delimiters
+ int _add_result(std::string &&result, std::vector<std::string> &res) const;
+};
+
+
+
+
+template <typename CRTP> template <typename T> void OptionBase<CRTP>::copy_to(T *other) const {
+ other->group(group_);
+ other->required(required_);
+ other->ignore_case(ignore_case_);
+ other->ignore_underscore(ignore_underscore_);
+ other->configurable(configurable_);
+ other->disable_flag_override(disable_flag_override_);
+ other->delimiter(delimiter_);
+ other->always_capture_default(always_capture_default_);
+ other->multi_option_policy(multi_option_policy_);
+}
+
+CLI11_INLINE Option *Option::expected(int value) {
+ if(value < 0) {
+ expected_min_ = -value;
+ if(expected_max_ < expected_min_) {
+ expected_max_ = expected_min_;
+ }
+ allow_extra_args_ = true;
+ flag_like_ = false;
+ } else if(value == detail::expected_max_vector_size) {
+ expected_min_ = 1;
+ expected_max_ = detail::expected_max_vector_size;
+ allow_extra_args_ = true;
+ flag_like_ = false;
+ } else {
+ expected_min_ = value;
+ expected_max_ = value;
+ flag_like_ = (expected_min_ == 0);
+ }
+ return this;
+}
+
+CLI11_INLINE Option *Option::expected(int value_min, int value_max) {
+ if(value_min < 0) {
+ value_min = -value_min;
+ }
+
+ if(value_max < 0) {
+ value_max = detail::expected_max_vector_size;
+ }
+ if(value_max < value_min) {
+ expected_min_ = value_max;
+ expected_max_ = value_min;
+ } else {
+ expected_max_ = value_max;
+ expected_min_ = value_min;
+ }
+
+ return this;
+}
+
+CLI11_INLINE Option *Option::check(Validator validator, const std::string &validator_name) {
+ validator.non_modifying();
+ validators_.push_back(std::move(validator));
+ if(!validator_name.empty())
+ validators_.back().name(validator_name);
+ return this;
+}
+
+CLI11_INLINE Option *Option::check(std::function<std::string(const std::string &)> Validator,
+ std::string Validator_description,
+ std::string Validator_name) {
+ validators_.emplace_back(Validator, std::move(Validator_description), std::move(Validator_name));
+ validators_.back().non_modifying();
+ return this;
+}
+
+CLI11_INLINE Option *Option::transform(Validator Validator, const std::string &Validator_name) {
+ validators_.insert(validators_.begin(), std::move(Validator));
+ if(!Validator_name.empty())
+ validators_.front().name(Validator_name);
+ return this;
+}
+
+CLI11_INLINE Option *Option::transform(const std::function<std::string(std::string)> &func,
+ std::string transform_description,
+ std::string transform_name) {
+ validators_.insert(validators_.begin(),
+ Validator(
+ [func](std::string &val) {
+ val = func(val);
+ return std::string{};
+ },
+ std::move(transform_description),
+ std::move(transform_name)));
+
+ return this;
+}
+
+CLI11_INLINE Option *Option::each(const std::function<void(std::string)> &func) {
+ validators_.emplace_back(
+ [func](std::string &inout) {
+ func(inout);
+ return std::string{};
+ },
+ std::string{});
+ return this;
+}
+
+CLI11_INLINE Validator *Option::get_validator(const std::string &Validator_name) {
+ for(auto &Validator : validators_) {
+ if(Validator_name == Validator.get_name()) {
+ return &Validator;
+ }
+ }
+ if((Validator_name.empty()) && (!validators_.empty())) {
+ return &(validators_.front());
+ }
+ throw OptionNotFound(std::string{"Validator "} + Validator_name + " Not Found");
+}
+
+CLI11_INLINE Validator *Option::get_validator(int index) {
+ // This is an signed int so that it is not equivalent to a pointer.
+ if(index >= 0 && index < static_cast<int>(validators_.size())) {
+ return &(validators_[static_cast<decltype(validators_)::size_type>(index)]);
+ }
+ throw OptionNotFound("Validator index is not valid");
+}
+
+CLI11_INLINE bool Option::remove_needs(Option *opt) {
+ auto iterator = std::find(std::begin(needs_), std::end(needs_), opt);
+
+ if(iterator == std::end(needs_)) {
+ return false;
+ }
+ needs_.erase(iterator);
+ return true;
+}
+
+CLI11_INLINE Option *Option::excludes(Option *opt) {
+ if(opt == this) {
+ throw(IncorrectConstruction("and option cannot exclude itself"));
+ }
+ excludes_.insert(opt);
+
+ // Help text should be symmetric - excluding a should exclude b
+ opt->excludes_.insert(this);
+
+ // Ignoring the insert return value, excluding twice is now allowed.
+ // (Mostly to allow both directions to be excluded by user, even though the library does it for you.)
+
+ return this;
+}
+
+CLI11_INLINE bool Option::remove_excludes(Option *opt) {
+ auto iterator = std::find(std::begin(excludes_), std::end(excludes_), opt);
+
+ if(iterator == std::end(excludes_)) {
+ return false;
+ }
+ excludes_.erase(iterator);
+ return true;
+}
+
+template <typename T> Option *Option::ignore_case(bool value) {
+ if(!ignore_case_ && value) {
+ ignore_case_ = value;
+ auto *parent = static_cast<T *>(parent_);
+ for(const Option_p &opt : parent->options_) {
+ if(opt.get() == this) {
+ continue;
+ }
+ const auto &omatch = opt->matching_name(*this);
+ if(!omatch.empty()) {
+ ignore_case_ = false;
+ throw OptionAlreadyAdded("adding ignore case caused a name conflict with " + omatch);
+ }
+ }
+ } else {
+ ignore_case_ = value;
+ }
+ return this;
+}
+
+template <typename T> Option *Option::ignore_underscore(bool value) {
+
+ if(!ignore_underscore_ && value) {
+ ignore_underscore_ = value;
+ auto *parent = static_cast<T *>(parent_);
+ for(const Option_p &opt : parent->options_) {
+ if(opt.get() == this) {
+ continue;
+ }
+ const auto &omatch = opt->matching_name(*this);
+ if(!omatch.empty()) {
+ ignore_underscore_ = false;
+ throw OptionAlreadyAdded("adding ignore underscore caused a name conflict with " + omatch);
+ }
+ }
+ } else {
+ ignore_underscore_ = value;
+ }
+ return this;
+}
+
+CLI11_INLINE Option *Option::multi_option_policy(MultiOptionPolicy value) {
+ if(value != multi_option_policy_) {
+ if(multi_option_policy_ == MultiOptionPolicy::Throw && expected_max_ == detail::expected_max_vector_size &&
+ expected_min_ > 1) { // this bizarre condition is to maintain backwards compatibility
+ // with the previous behavior of expected_ with vectors
+ expected_max_ = expected_min_;
+ }
+ multi_option_policy_ = value;
+ current_option_state_ = option_state::parsing;
+ }
+ return this;
+}
+
+CLI11_NODISCARD CLI11_INLINE std::string Option::get_name(bool positional, bool all_options) const {
+ if(get_group().empty())
+ return {}; // Hidden
+
+ if(all_options) {
+
+ std::vector<std::string> name_list;
+
+ /// The all list will never include a positional unless asked or that's the only name.
+ if((positional && (!pname_.empty())) || (snames_.empty() && lnames_.empty())) {
+ name_list.push_back(pname_);
+ }
+ if((get_items_expected() == 0) && (!fnames_.empty())) {
+ for(const std::string &sname : snames_) {
+ name_list.push_back("-" + sname);
+ if(check_fname(sname)) {
+ name_list.back() += "{" + get_flag_value(sname, "") + "}";
+ }
+ }
+
+ for(const std::string &lname : lnames_) {
+ name_list.push_back("--" + lname);
+ if(check_fname(lname)) {
+ name_list.back() += "{" + get_flag_value(lname, "") + "}";
+ }
+ }
+ } else {
+ for(const std::string &sname : snames_)
+ name_list.push_back("-" + sname);
+
+ for(const std::string &lname : lnames_)
+ name_list.push_back("--" + lname);
+ }
+
+ return detail::join(name_list);
+ }
+
+ // This returns the positional name no matter what
+ if(positional)
+ return pname_;
+
+ // Prefer long name
+ if(!lnames_.empty())
+ return std::string(2, '-') + lnames_[0];
+
+ // Or short name if no long name
+ if(!snames_.empty())
+ return std::string(1, '-') + snames_[0];
+
+ // If positional is the only name, it's okay to use that
+ return pname_;
+}
+
+CLI11_INLINE void Option::run_callback() {
+ if(force_callback_ && results_.empty()) {
+ add_result(default_str_);
+ }
+ if(current_option_state_ == option_state::parsing) {
+ _validate_results(results_);
+ current_option_state_ = option_state::validated;
+ }
+
+ if(current_option_state_ < option_state::reduced) {
+ _reduce_results(proc_results_, results_);
+ current_option_state_ = option_state::reduced;
+ }
+ if(current_option_state_ >= option_state::reduced) {
+ current_option_state_ = option_state::callback_run;
+ if(!(callback_)) {
+ return;
+ }
+ const results_t &send_results = proc_results_.empty() ? results_ : proc_results_;
+ bool local_result = callback_(send_results);
+
+ if(!local_result)
+ throw ConversionError(get_name(), results_);
+ }
+}
+
+CLI11_NODISCARD CLI11_INLINE const std::string &Option::matching_name(const Option &other) const {
+ static const std::string estring;
+ for(const std::string &sname : snames_) {
+ if(other.check_sname(sname))
+ return sname;
+ if(other.check_lname(sname))
+ return sname;
+ }
+ for(const std::string &lname : lnames_) {
+ if(other.check_lname(lname))
+ return lname;
+ if(lname.size() == 1) {
+ if(other.check_sname(lname)) {
+ return lname;
+ }
+ }
+ }
+ if(snames_.empty() && lnames_.empty() && !pname_.empty()) {
+ if(other.check_sname(pname_) || other.check_lname(pname_) || pname_ == other.pname_)
+ return pname_;
+ }
+ if(other.snames_.empty() && other.fnames_.empty() && !other.pname_.empty()) {
+ if(check_sname(other.pname_) || check_lname(other.pname_) || (pname_ == other.pname_))
+ return other.pname_;
+ }
+ if(ignore_case_ ||
+ ignore_underscore_) { // We need to do the inverse, in case we are ignore_case or ignore underscore
+ for(const std::string &sname : other.snames_)
+ if(check_sname(sname))
+ return sname;
+ for(const std::string &lname : other.lnames_)
+ if(check_lname(lname))
+ return lname;
+ }
+ return estring;
+}
+
+CLI11_NODISCARD CLI11_INLINE bool Option::check_name(const std::string &name) const {
+
+ if(name.length() > 2 && name[0] == '-' && name[1] == '-')
+ return check_lname(name.substr(2));
+ if(name.length() > 1 && name.front() == '-')
+ return check_sname(name.substr(1));
+ if(!pname_.empty()) {
+ std::string local_pname = pname_;
+ std::string local_name = name;
+ if(ignore_underscore_) {
+ local_pname = detail::remove_underscore(local_pname);
+ local_name = detail::remove_underscore(local_name);
+ }
+ if(ignore_case_) {
+ local_pname = detail::to_lower(local_pname);
+ local_name = detail::to_lower(local_name);
+ }
+ if(local_name == local_pname) {
+ return true;
+ }
+ }
+
+ if(!envname_.empty()) {
+ // this needs to be the original since envname_ shouldn't match on case insensitivity
+ return (name == envname_);
+ }
+ return false;
+}
+
+CLI11_NODISCARD CLI11_INLINE std::string Option::get_flag_value(const std::string &name,
+ std::string input_value) const {
+ static const std::string trueString{"true"};
+ static const std::string falseString{"false"};
+ static const std::string emptyString{"{}"};
+ // check for disable flag override_
+ if(disable_flag_override_) {
+ if(!((input_value.empty()) || (input_value == emptyString))) {
+ auto default_ind = detail::find_member(name, fnames_, ignore_case_, ignore_underscore_);
+ if(default_ind >= 0) {
+ // We can static cast this to std::size_t because it is more than 0 in this block
+ if(default_flag_values_[static_cast<std::size_t>(default_ind)].second != input_value) {
+ if(input_value == default_str_ && force_callback_) {
+ return input_value;
+ }
+ throw(ArgumentMismatch::FlagOverride(name));
+ }
+ } else {
+ if(input_value != trueString) {
+ throw(ArgumentMismatch::FlagOverride(name));
+ }
+ }
+ }
+ }
+ auto ind = detail::find_member(name, fnames_, ignore_case_, ignore_underscore_);
+ if((input_value.empty()) || (input_value == emptyString)) {
+ if(flag_like_) {
+ return (ind < 0) ? trueString : default_flag_values_[static_cast<std::size_t>(ind)].second;
+ }
+ return (ind < 0) ? default_str_ : default_flag_values_[static_cast<std::size_t>(ind)].second;
+ }
+ if(ind < 0) {
+ return input_value;
+ }
+ if(default_flag_values_[static_cast<std::size_t>(ind)].second == falseString) {
+ errno = 0;
+ auto val = detail::to_flag_value(input_value);
+ if(errno != 0) {
+ errno = 0;
+ return input_value;
+ }
+ return (val == 1) ? falseString : (val == (-1) ? trueString : std::to_string(-val));
+ }
+ return input_value;
+}
+
+CLI11_INLINE Option *Option::add_result(std::string s) {
+ _add_result(std::move(s), results_);
+ current_option_state_ = option_state::parsing;
+ return this;
+}
+
+CLI11_INLINE Option *Option::add_result(std::string s, int &results_added) {
+ results_added = _add_result(std::move(s), results_);
+ current_option_state_ = option_state::parsing;
+ return this;
+}
+
+CLI11_INLINE Option *Option::add_result(std::vector<std::string> s) {
+ current_option_state_ = option_state::parsing;
+ for(auto &str : s) {
+ _add_result(std::move(str), results_);
+ }
+ return this;
+}
+
+CLI11_NODISCARD CLI11_INLINE results_t Option::reduced_results() const {
+ results_t res = proc_results_.empty() ? results_ : proc_results_;
+ if(current_option_state_ < option_state::reduced) {
+ if(current_option_state_ == option_state::parsing) {
+ res = results_;
+ _validate_results(res);
+ }
+ if(!res.empty()) {
+ results_t extra;
+ _reduce_results(extra, res);
+ if(!extra.empty()) {
+ res = std::move(extra);
+ }
+ }
+ }
+ return res;
+}
+
+CLI11_INLINE Option *Option::type_size(int option_type_size) {
+ if(option_type_size < 0) {
+ // this section is included for backwards compatibility
+ type_size_max_ = -option_type_size;
+ type_size_min_ = -option_type_size;
+ expected_max_ = detail::expected_max_vector_size;
+ } else {
+ type_size_max_ = option_type_size;
+ if(type_size_max_ < detail::expected_max_vector_size) {
+ type_size_min_ = option_type_size;
+ } else {
+ inject_separator_ = true;
+ }
+ if(type_size_max_ == 0)
+ required_ = false;
+ }
+ return this;
+}
+
+CLI11_INLINE Option *Option::type_size(int option_type_size_min, int option_type_size_max) {
+ if(option_type_size_min < 0 || option_type_size_max < 0) {
+ // this section is included for backwards compatibility
+ expected_max_ = detail::expected_max_vector_size;
+ option_type_size_min = (std::abs)(option_type_size_min);
+ option_type_size_max = (std::abs)(option_type_size_max);
+ }
+
+ if(option_type_size_min > option_type_size_max) {
+ type_size_max_ = option_type_size_min;
+ type_size_min_ = option_type_size_max;
+ } else {
+ type_size_min_ = option_type_size_min;
+ type_size_max_ = option_type_size_max;
+ }
+ if(type_size_max_ == 0) {
+ required_ = false;
+ }
+ if(type_size_max_ >= detail::expected_max_vector_size) {
+ inject_separator_ = true;
+ }
+ return this;
+}
+
+CLI11_NODISCARD CLI11_INLINE std::string Option::get_type_name() const {
+ std::string full_type_name = type_name_();
+ if(!validators_.empty()) {
+ for(const auto &Validator : validators_) {
+ std::string vtype = Validator.get_description();
+ if(!vtype.empty()) {
+ full_type_name += ":" + vtype;
+ }
+ }
+ }
+ return full_type_name;
+}
+
+CLI11_INLINE void Option::_validate_results(results_t &res) const {
+ // Run the Validators (can change the string)
+ if(!validators_.empty()) {
+ if(type_size_max_ > 1) { // in this context index refers to the index in the type
+ int index = 0;
+ if(get_items_expected_max() < static_cast<int>(res.size()) &&
+ (multi_option_policy_ == CLI::MultiOptionPolicy::TakeLast ||
+ multi_option_policy_ == CLI::MultiOptionPolicy::Reverse)) {
+ // create a negative index for the earliest ones
+ index = get_items_expected_max() - static_cast<int>(res.size());
+ }
+
+ for(std::string &result : res) {
+ if(detail::is_separator(result) && type_size_max_ != type_size_min_ && index >= 0) {
+ index = 0; // reset index for variable size chunks
+ continue;
+ }
+ auto err_msg = _validate(result, (index >= 0) ? (index % type_size_max_) : index);
+ if(!err_msg.empty())
+ throw ValidationError(get_name(), err_msg);
+ ++index;
+ }
+ } else {
+ int index = 0;
+ if(expected_max_ < static_cast<int>(res.size()) &&
+ (multi_option_policy_ == CLI::MultiOptionPolicy::TakeLast ||
+ multi_option_policy_ == CLI::MultiOptionPolicy::Reverse)) {
+ // create a negative index for the earliest ones
+ index = expected_max_ - static_cast<int>(res.size());
+ }
+ for(std::string &result : res) {
+ auto err_msg = _validate(result, index);
+ ++index;
+ if(!err_msg.empty())
+ throw ValidationError(get_name(), err_msg);
+ }
+ }
+ }
+}
+
+CLI11_INLINE void Option::_reduce_results(results_t &out, const results_t &original) const {
+
+ // max num items expected or length of vector, always at least 1
+ // Only valid for a trimming policy
+
+ out.clear();
+ // Operation depends on the policy setting
+ switch(multi_option_policy_) {
+ case MultiOptionPolicy::TakeAll:
+ break;
+ case MultiOptionPolicy::TakeLast: {
+ // Allow multi-option sizes (including 0)
+ std::size_t trim_size = std::min<std::size_t>(
+ static_cast<std::size_t>(std::max<int>(get_items_expected_max(), 1)), original.size());
+ if(original.size() != trim_size) {
+ out.assign(original.end() - static_cast<results_t::difference_type>(trim_size), original.end());
+ }
+ } break;
+ case MultiOptionPolicy::Reverse: {
+ // Allow multi-option sizes (including 0)
+ std::size_t trim_size = std::min<std::size_t>(
+ static_cast<std::size_t>(std::max<int>(get_items_expected_max(), 1)), original.size());
+ if(original.size() != trim_size || trim_size > 1) {
+ out.assign(original.end() - static_cast<results_t::difference_type>(trim_size), original.end());
+ }
+ std::reverse(out.begin(), out.end());
+ } break;
+ case MultiOptionPolicy::TakeFirst: {
+ std::size_t trim_size = std::min<std::size_t>(
+ static_cast<std::size_t>(std::max<int>(get_items_expected_max(), 1)), original.size());
+ if(original.size() != trim_size) {
+ out.assign(original.begin(), original.begin() + static_cast<results_t::difference_type>(trim_size));
+ }
+ } break;
+ case MultiOptionPolicy::Join:
+ if(results_.size() > 1) {
+ out.push_back(detail::join(original, std::string(1, (delimiter_ == '\0') ? '\n' : delimiter_)));
+ }
+ break;
+ case MultiOptionPolicy::Sum:
+ out.push_back(detail::sum_string_vector(original));
+ break;
+ case MultiOptionPolicy::Throw:
+ default: {
+ auto num_min = static_cast<std::size_t>(get_items_expected_min());
+ auto num_max = static_cast<std::size_t>(get_items_expected_max());
+ if(num_min == 0) {
+ num_min = 1;
+ }
+ if(num_max == 0) {
+ num_max = 1;
+ }
+ if(original.size() < num_min) {
+ throw ArgumentMismatch::AtLeast(get_name(), static_cast<int>(num_min), original.size());
+ }
+ if(original.size() > num_max) {
+ if(original.size() == 2 && num_max == 1 && original[1] == "%%" && original[0] == "{}") {
+ // this condition is a trap for the following empty indicator check on config files
+ out = original;
+ } else {
+ throw ArgumentMismatch::AtMost(get_name(), static_cast<int>(num_max), original.size());
+ }
+ }
+ break;
+ }
+ }
+ // this check is to allow an empty vector in certain circumstances but not if expected is not zero.
+ // {} is the indicator for an empty container
+ if(out.empty()) {
+ if(original.size() == 1 && original[0] == "{}" && get_items_expected_min() > 0) {
+ out.emplace_back("{}");
+ out.emplace_back("%%");
+ }
+ } else if(out.size() == 1 && out[0] == "{}" && get_items_expected_min() > 0) {
+ out.emplace_back("%%");
+ }
+}
+
+CLI11_INLINE std::string Option::_validate(std::string &result, int index) const {
+ std::string err_msg;
+ if(result.empty() && expected_min_ == 0) {
+ // an empty with nothing expected is allowed
+ return err_msg;
+ }
+ for(const auto &vali : validators_) {
+ auto v = vali.get_application_index();
+ if(v == -1 || v == index) {
+ try {
+ err_msg = vali(result);
+ } catch(const ValidationError &err) {
+ err_msg = err.what();
+ }
+ if(!err_msg.empty())
+ break;
+ }
+ }
+
+ return err_msg;
+}
+
+CLI11_INLINE int Option::_add_result(std::string &&result, std::vector<std::string> &res) const {
+ int result_count = 0;
+ if(allow_extra_args_ && !result.empty() && result.front() == '[' &&
+ result.back() == ']') { // this is now a vector string likely from the default or user entry
+ result.pop_back();
+
+ for(auto &var : CLI::detail::split(result.substr(1), ',')) {
+ if(!var.empty()) {
+ result_count += _add_result(std::move(var), res);
+ }
+ }
+ return result_count;
+ }
+ if(delimiter_ == '\0') {
+ res.push_back(std::move(result));
+ ++result_count;
+ } else {
+ if((result.find_first_of(delimiter_) != std::string::npos)) {
+ for(const auto &var : CLI::detail::split(result, delimiter_)) {
+ if(!var.empty()) {
+ res.push_back(var);
+ ++result_count;
+ }
+ }
+ } else {
+ res.push_back(std::move(result));
+ ++result_count;
+ }
+ }
+ return result_count;
+}
+
+
+
+#ifndef CLI11_PARSE
+#define CLI11_PARSE(app, ...) \
+ try { \
+ (app).parse(__VA_ARGS__); \
+ } catch(const CLI::ParseError &e) { \
+ return (app).exit(e); \
+ }
+#endif
+
+namespace detail {
+enum class Classifier { NONE, POSITIONAL_MARK, SHORT, LONG, WINDOWS_STYLE, SUBCOMMAND, SUBCOMMAND_TERMINATOR };
+struct AppFriend;
+} // namespace detail
+
+namespace FailureMessage {
+/// Printout a clean, simple message on error (the default in CLI11 1.5+)
+CLI11_INLINE std::string simple(const App *app, const Error &e);
+
+/// Printout the full help string on error (if this fn is set, the old default for CLI11)
+CLI11_INLINE std::string help(const App *app, const Error &e);
+} // namespace FailureMessage
+
+/// enumeration of modes of how to deal with extras in config files
+
+enum class config_extras_mode : char { error = 0, ignore, ignore_all, capture };
+
+class App;
+
+using App_p = std::shared_ptr<App>;
+
+namespace detail {
+/// helper functions for adding in appropriate flag modifiers for add_flag
+
+template <typename T, enable_if_t<!std::is_integral<T>::value || (sizeof(T) <= 1U), detail::enabler> = detail::dummy>
+Option *default_flag_modifiers(Option *opt) {
+ return opt->always_capture_default();
+}
+
+/// summing modifiers
+template <typename T, enable_if_t<std::is_integral<T>::value && (sizeof(T) > 1U), detail::enabler> = detail::dummy>
+Option *default_flag_modifiers(Option *opt) {
+ return opt->multi_option_policy(MultiOptionPolicy::Sum)->default_str("0")->force_callback();
+}
+
+} // namespace detail
+
+class Option_group;
+/// Creates a command line program, with very few defaults.
+/** To use, create a new `Program()` instance with `argc`, `argv`, and a help description. The templated
+ * add_option methods make it easy to prepare options. Remember to call `.start` before starting your
+ * program, so that the options can be evaluated and the help option doesn't accidentally run your program. */
+class App {
+ friend Option;
+ friend detail::AppFriend;
+
+ protected:
+ // This library follows the Google style guide for member names ending in underscores
+
+ /// @name Basics
+ ///@{
+
+ /// Subcommand name or program name (from parser if name is empty)
+ std::string name_{};
+
+ /// Description of the current program/subcommand
+ std::string description_{};
+
+ /// If true, allow extra arguments (ie, don't throw an error). INHERITABLE
+ bool allow_extras_{false};
+
+ /// If ignore, allow extra arguments in the ini file (ie, don't throw an error). INHERITABLE
+ /// if error error on an extra argument, and if capture feed it to the app
+ config_extras_mode allow_config_extras_{config_extras_mode::ignore};
+
+ /// If true, return immediately on an unrecognized option (implies allow_extras) INHERITABLE
+ bool prefix_command_{false};
+
+ /// If set to true the name was automatically generated from the command line vs a user set name
+ bool has_automatic_name_{false};
+
+ /// If set to true the subcommand is required to be processed and used, ignored for main app
+ bool required_{false};
+
+ /// If set to true the subcommand is disabled and cannot be used, ignored for main app
+ bool disabled_{false};
+
+ /// Flag indicating that the pre_parse_callback has been triggered
+ bool pre_parse_called_{false};
+
+ /// Flag indicating that the callback for the subcommand should be executed immediately on parse completion which is
+ /// before help or ini files are processed. INHERITABLE
+ bool immediate_callback_{false};
+
+ /// This is a function that runs prior to the start of parsing
+ std::function<void(std::size_t)> pre_parse_callback_{};
+
+ /// This is a function that runs when parsing has finished.
+ std::function<void()> parse_complete_callback_{};
+
+ /// This is a function that runs when all processing has completed
+ std::function<void()> final_callback_{};
+
+ ///@}
+ /// @name Options
+ ///@{
+
+ /// The default values for options, customizable and changeable INHERITABLE
+ OptionDefaults option_defaults_{};
+
+ /// The list of options, stored locally
+ std::vector<Option_p> options_{};
+
+ ///@}
+ /// @name Help
+ ///@{
+
+ /// Usage to put after program/subcommand description in the help output INHERITABLE
+ std::string usage_{};
+
+ /// This is a function that generates a usage to put after program/subcommand description in help output
+ std::function<std::string()> usage_callback_{};
+
+ /// Footer to put after all options in the help output INHERITABLE
+ std::string footer_{};
+
+ /// This is a function that generates a footer to put after all other options in help output
+ std::function<std::string()> footer_callback_{};
+
+ /// A pointer to the help flag if there is one INHERITABLE
+ Option *help_ptr_{nullptr};
+
+ /// A pointer to the help all flag if there is one INHERITABLE
+ Option *help_all_ptr_{nullptr};
+
+ /// A pointer to a version flag if there is one
+ Option *version_ptr_{nullptr};
+
+ /// This is the formatter for help printing. Default provided. INHERITABLE (same pointer)
+ std::shared_ptr<FormatterBase> formatter_{new Formatter()};
+
+ /// The error message printing function INHERITABLE
+ std::function<std::string(const App *, const Error &e)> failure_message_{FailureMessage::simple};
+
+ ///@}
+ /// @name Parsing
+ ///@{
+
+ using missing_t = std::vector<std::pair<detail::Classifier, std::string>>;
+
+ /// Pair of classifier, string for missing options. (extra detail is removed on returning from parse)
+ ///
+ /// This is faster and cleaner than storing just a list of strings and reparsing. This may contain the -- separator.
+ missing_t missing_{};
+
+ /// This is a list of pointers to options with the original parse order
+ std::vector<Option *> parse_order_{};
+
+ /// This is a list of the subcommands collected, in order
+ std::vector<App *> parsed_subcommands_{};
+
+ /// this is a list of subcommands that are exclusionary to this one
+ std::set<App *> exclude_subcommands_{};
+
+ /// This is a list of options which are exclusionary to this App, if the options were used this subcommand should
+ /// not be
+ std::set<Option *> exclude_options_{};
+
+ /// this is a list of subcommands or option groups that are required by this one, the list is not mutual, the
+ /// listed subcommands do not require this one
+ std::set<App *> need_subcommands_{};
+
+ /// This is a list of options which are required by this app, the list is not mutual, listed options do not need the
+ /// subcommand not be
+ std::set<Option *> need_options_{};
+
+ ///@}
+ /// @name Subcommands
+ ///@{
+
+ /// Storage for subcommand list
+ std::vector<App_p> subcommands_{};
+
+ /// If true, the program name is not case sensitive INHERITABLE
+ bool ignore_case_{false};
+
+ /// If true, the program should ignore underscores INHERITABLE
+ bool ignore_underscore_{false};
+
+ /// Allow subcommand fallthrough, so that parent commands can collect commands after subcommand. INHERITABLE
+ bool fallthrough_{false};
+
+ /// Allow '/' for options for Windows like options. Defaults to true on Windows, false otherwise. INHERITABLE
+ bool allow_windows_style_options_{
+#ifdef _WIN32
+ true
+#else
+ false
+#endif
+ };
+ /// specify that positional arguments come at the end of the argument sequence not inheritable
+ bool positionals_at_end_{false};
+
+ enum class startup_mode : char { stable, enabled, disabled };
+ /// specify the startup mode for the app
+ /// stable=no change, enabled= startup enabled, disabled=startup disabled
+ startup_mode default_startup{startup_mode::stable};
+
+ /// if set to true the subcommand can be triggered via configuration files INHERITABLE
+ bool configurable_{false};
+
+ /// If set to true positional options are validated before assigning INHERITABLE
+ bool validate_positionals_{false};
+
+ /// If set to true optional vector arguments are validated before assigning INHERITABLE
+ bool validate_optional_arguments_{false};
+
+ /// indicator that the subcommand is silent and won't show up in subcommands list
+ /// This is potentially useful as a modifier subcommand
+ bool silent_{false};
+
+ /// Counts the number of times this command/subcommand was parsed
+ std::uint32_t parsed_{0U};
+
+ /// Minimum required subcommands (not inheritable!)
+ std::size_t require_subcommand_min_{0};
+
+ /// Max number of subcommands allowed (parsing stops after this number). 0 is unlimited INHERITABLE
+ std::size_t require_subcommand_max_{0};
+
+ /// Minimum required options (not inheritable!)
+ std::size_t require_option_min_{0};
+
+ /// Max number of options allowed. 0 is unlimited (not inheritable)
+ std::size_t require_option_max_{0};
+
+ /// A pointer to the parent if this is a subcommand
+ App *parent_{nullptr};
+
+ /// The group membership INHERITABLE
+ std::string group_{"Subcommands"};
+
+ /// Alias names for the subcommand
+ std::vector<std::string> aliases_{};
+
+ ///@}
+ /// @name Config
+ ///@{
+
+ /// Pointer to the config option
+ Option *config_ptr_{nullptr};
+
+ /// This is the formatter for help printing. Default provided. INHERITABLE (same pointer)
+ std::shared_ptr<Config> config_formatter_{new ConfigTOML()};
+
+ ///@}
+
+#ifdef _WIN32
+ /// When normalizing argv to UTF-8 on Windows, this is the storage for normalized args.
+ std::vector<std::string> normalized_argv_{};
+
+ /// When normalizing argv to UTF-8 on Windows, this is the `char**` value returned to the user.
+ std::vector<char *> normalized_argv_view_{};
+#endif
+
+ /// Special private constructor for subcommand
+ App(std::string app_description, std::string app_name, App *parent);
+
+ public:
+ /// @name Basic
+ ///@{
+
+ /// Create a new program. Pass in the same arguments as main(), along with a help string.
+ explicit App(std::string app_description = "", std::string app_name = "")
+ : App(app_description, app_name, nullptr) {
+ set_help_flag("-h,--help", "Print this help message and exit");
+ }
+
+ App(const App &) = delete;
+ App &operator=(const App &) = delete;
+
+ /// virtual destructor
+ virtual ~App() = default;
+
+ /// Convert the contents of argv to UTF-8. Only does something on Windows, does nothing elsewhere.
+ CLI11_NODISCARD char **ensure_utf8(char **argv);
+
+ /// Set a callback for execution when all parsing and processing has completed
+ ///
+ /// Due to a bug in c++11,
+ /// it is not possible to overload on std::function (fixed in c++14
+ /// and backported to c++11 on newer compilers). Use capture by reference
+ /// to get a pointer to App if needed.
+ App *callback(std::function<void()> app_callback) {
+ if(immediate_callback_) {
+ parse_complete_callback_ = std::move(app_callback);
+ } else {
+ final_callback_ = std::move(app_callback);
+ }
+ return this;
+ }
+
+ /// Set a callback for execution when all parsing and processing has completed
+ /// aliased as callback
+ App *final_callback(std::function<void()> app_callback) {
+ final_callback_ = std::move(app_callback);
+ return this;
+ }
+
+ /// Set a callback to execute when parsing has completed for the app
+ ///
+ App *parse_complete_callback(std::function<void()> pc_callback) {
+ parse_complete_callback_ = std::move(pc_callback);
+ return this;
+ }
+
+ /// Set a callback to execute prior to parsing.
+ ///
+ App *preparse_callback(std::function<void(std::size_t)> pp_callback) {
+ pre_parse_callback_ = std::move(pp_callback);
+ return this;
+ }
+
+ /// Set a name for the app (empty will use parser to set the name)
+ App *name(std::string app_name = "");
+
+ /// Set an alias for the app
+ App *alias(std::string app_name);
+
+ /// Remove the error when extras are left over on the command line.
+ App *allow_extras(bool allow = true) {
+ allow_extras_ = allow;
+ return this;
+ }
+
+ /// Remove the error when extras are left over on the command line.
+ App *required(bool require = true) {
+ required_ = require;
+ return this;
+ }
+
+ /// Disable the subcommand or option group
+ App *disabled(bool disable = true) {
+ disabled_ = disable;
+ return this;
+ }
+
+ /// silence the subcommand from showing up in the processed list
+ App *silent(bool silence = true) {
+ silent_ = silence;
+ return this;
+ }
+
+ /// Set the subcommand to be disabled by default, so on clear(), at the start of each parse it is disabled
+ App *disabled_by_default(bool disable = true) {
+ if(disable) {
+ default_startup = startup_mode::disabled;
+ } else {
+ default_startup = (default_startup == startup_mode::enabled) ? startup_mode::enabled : startup_mode::stable;
+ }
+ return this;
+ }
+
+ /// Set the subcommand to be enabled by default, so on clear(), at the start of each parse it is enabled (not
+ /// disabled)
+ App *enabled_by_default(bool enable = true) {
+ if(enable) {
+ default_startup = startup_mode::enabled;
+ } else {
+ default_startup =
+ (default_startup == startup_mode::disabled) ? startup_mode::disabled : startup_mode::stable;
+ }
+ return this;
+ }
+
+ /// Set the subcommand callback to be executed immediately on subcommand completion
+ App *immediate_callback(bool immediate = true);
+
+ /// Set the subcommand to validate positional arguments before assigning
+ App *validate_positionals(bool validate = true) {
+ validate_positionals_ = validate;
+ return this;
+ }
+
+ /// Set the subcommand to validate optional vector arguments before assigning
+ App *validate_optional_arguments(bool validate = true) {
+ validate_optional_arguments_ = validate;
+ return this;
+ }
+
+ /// ignore extras in config files
+ App *allow_config_extras(bool allow = true) {
+ if(allow) {
+ allow_config_extras_ = config_extras_mode::capture;
+ allow_extras_ = true;
+ } else {
+ allow_config_extras_ = config_extras_mode::error;
+ }
+ return this;
+ }
+
+ /// ignore extras in config files
+ App *allow_config_extras(config_extras_mode mode) {
+ allow_config_extras_ = mode;
+ return this;
+ }
+
+ /// Do not parse anything after the first unrecognized option and return
+ App *prefix_command(bool allow = true) {
+ prefix_command_ = allow;
+ return this;
+ }
+
+ /// Ignore case. Subcommands inherit value.
+ App *ignore_case(bool value = true);
+
+ /// Allow windows style options, such as `/opt`. First matching short or long name used. Subcommands inherit
+ /// value.
+ App *allow_windows_style_options(bool value = true) {
+ allow_windows_style_options_ = value;
+ return this;
+ }
+
+ /// Specify that the positional arguments are only at the end of the sequence
+ App *positionals_at_end(bool value = true) {
+ positionals_at_end_ = value;
+ return this;
+ }
+
+ /// Specify that the subcommand can be triggered by a config file
+ App *configurable(bool value = true) {
+ configurable_ = value;
+ return this;
+ }
+
+ /// Ignore underscore. Subcommands inherit value.
+ App *ignore_underscore(bool value = true);
+
+ /// Set the help formatter
+ App *formatter(std::shared_ptr<FormatterBase> fmt) {
+ formatter_ = fmt;
+ return this;
+ }
+
+ /// Set the help formatter
+ App *formatter_fn(std::function<std::string(const App *, std::string, AppFormatMode)> fmt) {
+ formatter_ = std::make_shared<FormatterLambda>(fmt);
+ return this;
+ }
+
+ /// Set the config formatter
+ App *config_formatter(std::shared_ptr<Config> fmt) {
+ config_formatter_ = fmt;
+ return this;
+ }
+
+ /// Check to see if this subcommand was parsed, true only if received on command line.
+ CLI11_NODISCARD bool parsed() const { return parsed_ > 0; }
+
+ /// Get the OptionDefault object, to set option defaults
+ OptionDefaults *option_defaults() { return &option_defaults_; }
+
+ ///@}
+ /// @name Adding options
+ ///@{
+
+ /// Add an option, will automatically understand the type for common types.
+ ///
+ /// To use, create a variable with the expected type, and pass it in after the name.
+ /// After start is called, you can use count to see if the value was passed, and
+ /// the value will be initialized properly. Numbers, vectors, and strings are supported.
+ ///
+ /// ->required(), ->default, and the validators are options,
+ /// The positional options take an optional number of arguments.
+ ///
+ /// For example,
+ ///
+ /// std::string filename;
+ /// program.add_option("filename", filename, "description of filename");
+ ///
+ Option *add_option(std::string option_name,
+ callback_t option_callback,
+ std::string option_description = "",
+ bool defaulted = false,
+ std::function<std::string()> func = {});
+
+ /// Add option for assigning to a variable
+ template <typename AssignTo,
+ typename ConvertTo = AssignTo,
+ enable_if_t<!std::is_const<ConvertTo>::value, detail::enabler> = detail::dummy>
+ Option *add_option(std::string option_name,
+ AssignTo &variable, ///< The variable to set
+ std::string option_description = "") {
+
+ auto fun = [&variable](const CLI::results_t &res) { // comment for spacing
+ return detail::lexical_conversion<AssignTo, ConvertTo>(res, variable);
+ };
+
+ Option *opt = add_option(option_name, fun, option_description, false, [&variable]() {
+ return CLI::detail::checked_to_string<AssignTo, ConvertTo>(variable);
+ });
+ opt->type_name(detail::type_name<ConvertTo>());
+ // these must be actual lvalues since (std::max) sometimes is defined in terms of references and references
+ // to structs used in the evaluation can be temporary so that would cause issues.
+ auto Tcount = detail::type_count<AssignTo>::value;
+ auto XCcount = detail::type_count<ConvertTo>::value;
+ opt->type_size(detail::type_count_min<ConvertTo>::value, (std::max)(Tcount, XCcount));
+ opt->expected(detail::expected_count<ConvertTo>::value);
+ opt->run_callback_for_default();
+ return opt;
+ }
+
+ /// Add option for assigning to a variable
+ template <typename AssignTo, enable_if_t<!std::is_const<AssignTo>::value, detail::enabler> = detail::dummy>
+ Option *add_option_no_stream(std::string option_name,
+ AssignTo &variable, ///< The variable to set
+ std::string option_description = "") {
+
+ auto fun = [&variable](const CLI::results_t &res) { // comment for spacing
+ return detail::lexical_conversion<AssignTo, AssignTo>(res, variable);
+ };
+
+ Option *opt = add_option(option_name, fun, option_description, false, []() { return std::string{}; });
+ opt->type_name(detail::type_name<AssignTo>());
+ opt->type_size(detail::type_count_min<AssignTo>::value, detail::type_count<AssignTo>::value);
+ opt->expected(detail::expected_count<AssignTo>::value);
+ opt->run_callback_for_default();
+ return opt;
+ }
+
+ /// Add option for a callback of a specific type
+ template <typename ArgType>
+ Option *add_option_function(std::string option_name,
+ const std::function<void(const ArgType &)> &func, ///< the callback to execute
+ std::string option_description = "") {
+
+ auto fun = [func](const CLI::results_t &res) {
+ ArgType variable;
+ bool result = detail::lexical_conversion<ArgType, ArgType>(res, variable);
+ if(result) {
+ func(variable);
+ }
+ return result;
+ };
+
+ Option *opt = add_option(option_name, std::move(fun), option_description, false);
+ opt->type_name(detail::type_name<ArgType>());
+ opt->type_size(detail::type_count_min<ArgType>::value, detail::type_count<ArgType>::value);
+ opt->expected(detail::expected_count<ArgType>::value);
+ return opt;
+ }
+
+ /// Add option with no description or variable assignment
+ Option *add_option(std::string option_name) {
+ return add_option(option_name, CLI::callback_t{}, std::string{}, false);
+ }
+
+ /// Add option with description but with no variable assignment or callback
+ template <typename T,
+ enable_if_t<std::is_const<T>::value && std::is_constructible<std::string, T>::value, detail::enabler> =
+ detail::dummy>
+ Option *add_option(std::string option_name, T &option_description) {
+ return add_option(option_name, CLI::callback_t(), option_description, false);
+ }
+
+ /// Set a help flag, replace the existing one if present
+ Option *set_help_flag(std::string flag_name = "", const std::string &help_description = "");
+
+ /// Set a help all flag, replaced the existing one if present
+ Option *set_help_all_flag(std::string help_name = "", const std::string &help_description = "");
+
+ /// Set a version flag and version display string, replace the existing one if present
+ Option *set_version_flag(std::string flag_name = "",
+ const std::string &versionString = "",
+ const std::string &version_help = "Display program version information and exit");
+
+ /// Generate the version string through a callback function
+ Option *set_version_flag(std::string flag_name,
+ std::function<std::string()> vfunc,
+ const std::string &version_help = "Display program version information and exit");
+
+ private:
+ /// Internal function for adding a flag
+ Option *_add_flag_internal(std::string flag_name, CLI::callback_t fun, std::string flag_description);
+
+ public:
+ /// Add a flag with no description or variable assignment
+ Option *add_flag(std::string flag_name) { return _add_flag_internal(flag_name, CLI::callback_t(), std::string{}); }
+
+ /// Add flag with description but with no variable assignment or callback
+ /// takes a constant string, if a variable string is passed that variable will be assigned the results from the
+ /// flag
+ template <typename T,
+ enable_if_t<std::is_const<T>::value && std::is_constructible<std::string, T>::value, detail::enabler> =
+ detail::dummy>
+ Option *add_flag(std::string flag_name, T &flag_description) {
+ return _add_flag_internal(flag_name, CLI::callback_t(), flag_description);
+ }
+
+ /// Other type version accepts all other types that are not vectors such as bool, enum, string or other classes
+ /// that can be converted from a string
+ template <typename T,
+ enable_if_t<!detail::is_mutable_container<T>::value && !std::is_const<T>::value &&
+ !std::is_constructible<std::function<void(int)>, T>::value,
+ detail::enabler> = detail::dummy>
+ Option *add_flag(std::string flag_name,
+ T &flag_result, ///< A variable holding the flag result
+ std::string flag_description = "") {
+
+ CLI::callback_t fun = [&flag_result](const CLI::results_t &res) {
+ using CLI::detail::lexical_cast;
+ return lexical_cast(res[0], flag_result);
+ };
+ auto *opt = _add_flag_internal(flag_name, std::move(fun), std::move(flag_description));
+ return detail::default_flag_modifiers<T>(opt);
+ }
+
+ /// Vector version to capture multiple flags.
+ template <typename T,
+ enable_if_t<!std::is_assignable<std::function<void(std::int64_t)> &, T>::value, detail::enabler> =
+ detail::dummy>
+ Option *add_flag(std::string flag_name,
+ std::vector<T> &flag_results, ///< A vector of values with the flag results
+ std::string flag_description = "") {
+ CLI::callback_t fun = [&flag_results](const CLI::results_t &res) {
+ bool retval = true;
+ for(const auto &elem : res) {
+ using CLI::detail::lexical_cast;
+ flag_results.emplace_back();
+ retval &= lexical_cast(elem, flag_results.back());
+ }
+ return retval;
+ };
+ return _add_flag_internal(flag_name, std::move(fun), std::move(flag_description))
+ ->multi_option_policy(MultiOptionPolicy::TakeAll)
+ ->run_callback_for_default();
+ }
+
+ /// Add option for callback that is triggered with a true flag and takes no arguments
+ Option *add_flag_callback(std::string flag_name,
+ std::function<void(void)> function, ///< A function to call, void(void)
+ std::string flag_description = "");
+
+ /// Add option for callback with an integer value
+ Option *add_flag_function(std::string flag_name,
+ std::function<void(std::int64_t)> function, ///< A function to call, void(int)
+ std::string flag_description = "");
+
+#ifdef CLI11_CPP14
+ /// Add option for callback (C++14 or better only)
+ Option *add_flag(std::string flag_name,
+ std::function<void(std::int64_t)> function, ///< A function to call, void(std::int64_t)
+ std::string flag_description = "") {
+ return add_flag_function(std::move(flag_name), std::move(function), std::move(flag_description));
+ }
+#endif
+
+ /// Set a configuration ini file option, or clear it if no name passed
+ Option *set_config(std::string option_name = "",
+ std::string default_filename = "",
+ const std::string &help_message = "Read an ini file",
+ bool config_required = false);
+
+ /// Removes an option from the App. Takes an option pointer. Returns true if found and removed.
+ bool remove_option(Option *opt);
+
+ /// creates an option group as part of the given app
+ template <typename T = Option_group>
+ T *add_option_group(std::string group_name, std::string group_description = "") {
+ if(!detail::valid_alias_name_string(group_name)) {
+ throw IncorrectConstruction("option group names may not contain newlines or null characters");
+ }
+ auto option_group = std::make_shared<T>(std::move(group_description), group_name, this);
+ auto *ptr = option_group.get();
+ // move to App_p for overload resolution on older gcc versions
+ App_p app_ptr = std::dynamic_pointer_cast<App>(option_group);
+ add_subcommand(std::move(app_ptr));
+ return ptr;
+ }
+
+ ///@}
+ /// @name Subcommands
+ ///@{
+
+ /// Add a subcommand. Inherits INHERITABLE and OptionDefaults, and help flag
+ App *add_subcommand(std::string subcommand_name = "", std::string subcommand_description = "");
+
+ /// Add a previously created app as a subcommand
+ App *add_subcommand(CLI::App_p subcom);
+
+ /// Removes a subcommand from the App. Takes a subcommand pointer. Returns true if found and removed.
+ bool remove_subcommand(App *subcom);
+
+ /// Check to see if a subcommand is part of this command (doesn't have to be in command line)
+ /// returns the first subcommand if passed a nullptr
+ App *get_subcommand(const App *subcom) const;
+
+ /// Check to see if a subcommand is part of this command (text version)
+ CLI11_NODISCARD App *get_subcommand(std::string subcom) const;
+
+ /// Get a pointer to subcommand by index
+ CLI11_NODISCARD App *get_subcommand(int index = 0) const;
+
+ /// Check to see if a subcommand is part of this command and get a shared_ptr to it
+ CLI::App_p get_subcommand_ptr(App *subcom) const;
+
+ /// Check to see if a subcommand is part of this command (text version)
+ CLI11_NODISCARD CLI::App_p get_subcommand_ptr(std::string subcom) const;
+
+ /// Get an owning pointer to subcommand by index
+ CLI11_NODISCARD CLI::App_p get_subcommand_ptr(int index = 0) const;
+
+ /// Check to see if an option group is part of this App
+ CLI11_NODISCARD App *get_option_group(std::string group_name) const;
+
+ /// No argument version of count counts the number of times this subcommand was
+ /// passed in. The main app will return 1. Unnamed subcommands will also return 1 unless
+ /// otherwise modified in a callback
+ CLI11_NODISCARD std::size_t count() const { return parsed_; }
+
+ /// Get a count of all the arguments processed in options and subcommands, this excludes arguments which were
+ /// treated as extras.
+ CLI11_NODISCARD std::size_t count_all() const;
+
+ /// Changes the group membership
+ App *group(std::string group_name) {
+ group_ = group_name;
+ return this;
+ }
+
+ /// The argumentless form of require subcommand requires 1 or more subcommands
+ App *require_subcommand() {
+ require_subcommand_min_ = 1;
+ require_subcommand_max_ = 0;
+ return this;
+ }
+
+ /// Require a subcommand to be given (does not affect help call)
+ /// The number required can be given. Negative values indicate maximum
+ /// number allowed (0 for any number). Max number inheritable.
+ App *require_subcommand(int value) {
+ if(value < 0) {
+ require_subcommand_min_ = 0;
+ require_subcommand_max_ = static_cast<std::size_t>(-value);
+ } else {
+ require_subcommand_min_ = static_cast<std::size_t>(value);
+ require_subcommand_max_ = static_cast<std::size_t>(value);
+ }
+ return this;
+ }
+
+ /// Explicitly control the number of subcommands required. Setting 0
+ /// for the max means unlimited number allowed. Max number inheritable.
+ App *require_subcommand(std::size_t min, std::size_t max) {
+ require_subcommand_min_ = min;
+ require_subcommand_max_ = max;
+ return this;
+ }
+
+ /// The argumentless form of require option requires 1 or more options be used
+ App *require_option() {
+ require_option_min_ = 1;
+ require_option_max_ = 0;
+ return this;
+ }
+
+ /// Require an option to be given (does not affect help call)
+ /// The number required can be given. Negative values indicate maximum
+ /// number allowed (0 for any number).
+ App *require_option(int value) {
+ if(value < 0) {
+ require_option_min_ = 0;
+ require_option_max_ = static_cast<std::size_t>(-value);
+ } else {
+ require_option_min_ = static_cast<std::size_t>(value);
+ require_option_max_ = static_cast<std::size_t>(value);
+ }
+ return this;
+ }
+
+ /// Explicitly control the number of options required. Setting 0
+ /// for the max means unlimited number allowed. Max number inheritable.
+ App *require_option(std::size_t min, std::size_t max) {
+ require_option_min_ = min;
+ require_option_max_ = max;
+ return this;
+ }
+
+ /// Stop subcommand fallthrough, so that parent commands cannot collect commands after subcommand.
+ /// Default from parent, usually set on parent.
+ App *fallthrough(bool value = true) {
+ fallthrough_ = value;
+ return this;
+ }
+
+ /// Check to see if this subcommand was parsed, true only if received on command line.
+ /// This allows the subcommand to be directly checked.
+ explicit operator bool() const { return parsed_ > 0; }
+
+ ///@}
+ /// @name Extras for subclassing
+ ///@{
+
+ /// This allows subclasses to inject code before callbacks but after parse.
+ ///
+ /// This does not run if any errors or help is thrown.
+ virtual void pre_callback() {}
+
+ ///@}
+ /// @name Parsing
+ ///@{
+ //
+ /// Reset the parsed data
+ void clear();
+
+ /// Parses the command line - throws errors.
+ /// This must be called after the options are in but before the rest of the program.
+ void parse(int argc, const char *const *argv);
+ void parse(int argc, const wchar_t *const *argv);
+
+ private:
+ template <class CharT> void parse_char_t(int argc, const CharT *const *argv);
+
+ public:
+ /// Parse a single string as if it contained command line arguments.
+ /// This function splits the string into arguments then calls parse(std::vector<std::string> &)
+ /// the function takes an optional boolean argument specifying if the programName is included in the string to
+ /// process
+ void parse(std::string commandline, bool program_name_included = false);
+ void parse(std::wstring commandline, bool program_name_included = false);
+
+ /// The real work is done here. Expects a reversed vector.
+ /// Changes the vector to the remaining options.
+ void parse(std::vector<std::string> &args);
+
+ /// The real work is done here. Expects a reversed vector.
+ void parse(std::vector<std::string> &&args);
+
+ void parse_from_stream(std::istream &input);
+
+ /// Provide a function to print a help message. The function gets access to the App pointer and error.
+ void failure_message(std::function<std::string(const App *, const Error &e)> function) {
+ failure_message_ = function;
+ }
+
+ /// Print a nice error message and return the exit code
+ int exit(const Error &e, std::ostream &out = std::cout, std::ostream &err = std::cerr) const;
+
+ ///@}
+ /// @name Post parsing
+ ///@{
+
+ /// Counts the number of times the given option was passed.
+ CLI11_NODISCARD std::size_t count(std::string option_name) const { return get_option(option_name)->count(); }
+
+ /// Get a subcommand pointer list to the currently selected subcommands (after parsing by default, in command
+ /// line order; use parsed = false to get the original definition list.)
+ CLI11_NODISCARD std::vector<App *> get_subcommands() const { return parsed_subcommands_; }
+
+ /// Get a filtered subcommand pointer list from the original definition list. An empty function will provide all
+ /// subcommands (const)
+ std::vector<const App *> get_subcommands(const std::function<bool(const App *)> &filter) const;
+
+ /// Get a filtered subcommand pointer list from the original definition list. An empty function will provide all
+ /// subcommands
+ std::vector<App *> get_subcommands(const std::function<bool(App *)> &filter);
+
+ /// Check to see if given subcommand was selected
+ bool got_subcommand(const App *subcom) const {
+ // get subcom needed to verify that this was a real subcommand
+ return get_subcommand(subcom)->parsed_ > 0;
+ }
+
+ /// Check with name instead of pointer to see if subcommand was selected
+ CLI11_NODISCARD bool got_subcommand(std::string subcommand_name) const {
+ return get_subcommand(subcommand_name)->parsed_ > 0;
+ }
+
+ /// Sets excluded options for the subcommand
+ App *excludes(Option *opt) {
+ if(opt == nullptr) {
+ throw OptionNotFound("nullptr passed");
+ }
+ exclude_options_.insert(opt);
+ return this;
+ }
+
+ /// Sets excluded subcommands for the subcommand
+ App *excludes(App *app) {
+ if(app == nullptr) {
+ throw OptionNotFound("nullptr passed");
+ }
+ if(app == this) {
+ throw OptionNotFound("cannot self reference in needs");
+ }
+ auto res = exclude_subcommands_.insert(app);
+ // subcommand exclusion should be symmetric
+ if(res.second) {
+ app->exclude_subcommands_.insert(this);
+ }
+ return this;
+ }
+
+ App *needs(Option *opt) {
+ if(opt == nullptr) {
+ throw OptionNotFound("nullptr passed");
+ }
+ need_options_.insert(opt);
+ return this;
+ }
+
+ App *needs(App *app) {
+ if(app == nullptr) {
+ throw OptionNotFound("nullptr passed");
+ }
+ if(app == this) {
+ throw OptionNotFound("cannot self reference in needs");
+ }
+ need_subcommands_.insert(app);
+ return this;
+ }
+
+ /// Removes an option from the excludes list of this subcommand
+ bool remove_excludes(Option *opt);
+
+ /// Removes a subcommand from the excludes list of this subcommand
+ bool remove_excludes(App *app);
+
+ /// Removes an option from the needs list of this subcommand
+ bool remove_needs(Option *opt);
+
+ /// Removes a subcommand from the needs list of this subcommand
+ bool remove_needs(App *app);
+ ///@}
+ /// @name Help
+ ///@{
+
+ /// Set usage.
+ App *usage(std::string usage_string) {
+ usage_ = std::move(usage_string);
+ return this;
+ }
+ /// Set usage.
+ App *usage(std::function<std::string()> usage_function) {
+ usage_callback_ = std::move(usage_function);
+ return this;
+ }
+ /// Set footer.
+ App *footer(std::string footer_string) {
+ footer_ = std::move(footer_string);
+ return this;
+ }
+ /// Set footer.
+ App *footer(std::function<std::string()> footer_function) {
+ footer_callback_ = std::move(footer_function);
+ return this;
+ }
+ /// Produce a string that could be read in as a config of the current values of the App. Set default_also to
+ /// include default arguments. write_descriptions will print a description for the App and for each option.
+ CLI11_NODISCARD std::string config_to_str(bool default_also = false, bool write_description = false) const {
+ return config_formatter_->to_config(this, default_also, write_description, "");
+ }
+
+ /// Makes a help message, using the currently configured formatter
+ /// Will only do one subcommand at a time
+ CLI11_NODISCARD std::string help(std::string prev = "", AppFormatMode mode = AppFormatMode::Normal) const;
+
+ /// Displays a version string
+ CLI11_NODISCARD std::string version() const;
+ ///@}
+ /// @name Getters
+ ///@{
+
+ /// Access the formatter
+ CLI11_NODISCARD std::shared_ptr<FormatterBase> get_formatter() const { return formatter_; }
+
+ /// Access the config formatter
+ CLI11_NODISCARD std::shared_ptr<Config> get_config_formatter() const { return config_formatter_; }
+
+ /// Access the config formatter as a configBase pointer
+ CLI11_NODISCARD std::shared_ptr<ConfigBase> get_config_formatter_base() const {
+ // This is safer as a dynamic_cast if we have RTTI, as Config -> ConfigBase
+#if CLI11_USE_STATIC_RTTI == 0
+ return std::dynamic_pointer_cast<ConfigBase>(config_formatter_);
+#else
+ return std::static_pointer_cast<ConfigBase>(config_formatter_);
+#endif
+ }
+
+ /// Get the app or subcommand description
+ CLI11_NODISCARD std::string get_description() const { return description_; }
+
+ /// Set the description of the app
+ App *description(std::string app_description) {
+ description_ = std::move(app_description);
+ return this;
+ }
+
+ /// Get the list of options (user facing function, so returns raw pointers), has optional filter function
+ std::vector<const Option *> get_options(const std::function<bool(const Option *)> filter = {}) const;
+
+ /// Non-const version of the above
+ std::vector<Option *> get_options(const std::function<bool(Option *)> filter = {});
+
+ /// Get an option by name (noexcept non-const version)
+ Option *get_option_no_throw(std::string option_name) noexcept;
+
+ /// Get an option by name (noexcept const version)
+ CLI11_NODISCARD const Option *get_option_no_throw(std::string option_name) const noexcept;
+
+ /// Get an option by name
+ CLI11_NODISCARD const Option *get_option(std::string option_name) const {
+ const auto *opt = get_option_no_throw(option_name);
+ if(opt == nullptr) {
+ throw OptionNotFound(option_name);
+ }
+ return opt;
+ }
+
+ /// Get an option by name (non-const version)
+ Option *get_option(std::string option_name) {
+ auto *opt = get_option_no_throw(option_name);
+ if(opt == nullptr) {
+ throw OptionNotFound(option_name);
+ }
+ return opt;
+ }
+
+ /// Shortcut bracket operator for getting a pointer to an option
+ const Option *operator[](const std::string &option_name) const { return get_option(option_name); }
+
+ /// Shortcut bracket operator for getting a pointer to an option
+ const Option *operator[](const char *option_name) const { return get_option(option_name); }
+
+ /// Check the status of ignore_case
+ CLI11_NODISCARD bool get_ignore_case() const { return ignore_case_; }
+
+ /// Check the status of ignore_underscore
+ CLI11_NODISCARD bool get_ignore_underscore() const { return ignore_underscore_; }
+
+ /// Check the status of fallthrough
+ CLI11_NODISCARD bool get_fallthrough() const { return fallthrough_; }
+
+ /// Check the status of the allow windows style options
+ CLI11_NODISCARD bool get_allow_windows_style_options() const { return allow_windows_style_options_; }
+
+ /// Check the status of the allow windows style options
+ CLI11_NODISCARD bool get_positionals_at_end() const { return positionals_at_end_; }
+
+ /// Check the status of the allow windows style options
+ CLI11_NODISCARD bool get_configurable() const { return configurable_; }
+
+ /// Get the group of this subcommand
+ CLI11_NODISCARD const std::string &get_group() const { return group_; }
+
+ /// Generate and return the usage.
+ CLI11_NODISCARD std::string get_usage() const {
+ return (usage_callback_) ? usage_callback_() + '\n' + usage_ : usage_;
+ }
+
+ /// Generate and return the footer.
+ CLI11_NODISCARD std::string get_footer() const {
+ return (footer_callback_) ? footer_callback_() + '\n' + footer_ : footer_;
+ }
+
+ /// Get the required min subcommand value
+ CLI11_NODISCARD std::size_t get_require_subcommand_min() const { return require_subcommand_min_; }
+
+ /// Get the required max subcommand value
+ CLI11_NODISCARD std::size_t get_require_subcommand_max() const { return require_subcommand_max_; }
+
+ /// Get the required min option value
+ CLI11_NODISCARD std::size_t get_require_option_min() const { return require_option_min_; }
+
+ /// Get the required max option value
+ CLI11_NODISCARD std::size_t get_require_option_max() const { return require_option_max_; }
+
+ /// Get the prefix command status
+ CLI11_NODISCARD bool get_prefix_command() const { return prefix_command_; }
+
+ /// Get the status of allow extras
+ CLI11_NODISCARD bool get_allow_extras() const { return allow_extras_; }
+
+ /// Get the status of required
+ CLI11_NODISCARD bool get_required() const { return required_; }
+
+ /// Get the status of disabled
+ CLI11_NODISCARD bool get_disabled() const { return disabled_; }
+
+ /// Get the status of silence
+ CLI11_NODISCARD bool get_silent() const { return silent_; }
+
+ /// Get the status of disabled
+ CLI11_NODISCARD bool get_immediate_callback() const { return immediate_callback_; }
+
+ /// Get the status of disabled by default
+ CLI11_NODISCARD bool get_disabled_by_default() const { return (default_startup == startup_mode::disabled); }
+
+ /// Get the status of disabled by default
+ CLI11_NODISCARD bool get_enabled_by_default() const { return (default_startup == startup_mode::enabled); }
+ /// Get the status of validating positionals
+ CLI11_NODISCARD bool get_validate_positionals() const { return validate_positionals_; }
+ /// Get the status of validating optional vector arguments
+ CLI11_NODISCARD bool get_validate_optional_arguments() const { return validate_optional_arguments_; }
+
+ /// Get the status of allow extras
+ CLI11_NODISCARD config_extras_mode get_allow_config_extras() const { return allow_config_extras_; }
+
+ /// Get a pointer to the help flag.
+ Option *get_help_ptr() { return help_ptr_; }
+
+ /// Get a pointer to the help flag. (const)
+ CLI11_NODISCARD const Option *get_help_ptr() const { return help_ptr_; }
+
+ /// Get a pointer to the help all flag. (const)
+ CLI11_NODISCARD const Option *get_help_all_ptr() const { return help_all_ptr_; }
+
+ /// Get a pointer to the config option.
+ Option *get_config_ptr() { return config_ptr_; }
+
+ /// Get a pointer to the config option. (const)
+ CLI11_NODISCARD const Option *get_config_ptr() const { return config_ptr_; }
+
+ /// Get a pointer to the version option.
+ Option *get_version_ptr() { return version_ptr_; }
+
+ /// Get a pointer to the version option. (const)
+ CLI11_NODISCARD const Option *get_version_ptr() const { return version_ptr_; }
+
+ /// Get the parent of this subcommand (or nullptr if main app)
+ App *get_parent() { return parent_; }
+
+ /// Get the parent of this subcommand (or nullptr if main app) (const version)
+ CLI11_NODISCARD const App *get_parent() const { return parent_; }
+
+ /// Get the name of the current app
+ CLI11_NODISCARD const std::string &get_name() const { return name_; }
+
+ /// Get the aliases of the current app
+ CLI11_NODISCARD const std::vector<std::string> &get_aliases() const { return aliases_; }
+
+ /// clear all the aliases of the current App
+ App *clear_aliases() {
+ aliases_.clear();
+ return this;
+ }
+
+ /// Get a display name for an app
+ CLI11_NODISCARD std::string get_display_name(bool with_aliases = false) const;
+
+ /// Check the name, case insensitive and underscore insensitive if set
+ CLI11_NODISCARD bool check_name(std::string name_to_check) const;
+
+ /// Get the groups available directly from this option (in order)
+ CLI11_NODISCARD std::vector<std::string> get_groups() const;
+
+ /// This gets a vector of pointers with the original parse order
+ CLI11_NODISCARD const std::vector<Option *> &parse_order() const { return parse_order_; }
+
+ /// This returns the missing options from the current subcommand
+ CLI11_NODISCARD std::vector<std::string> remaining(bool recurse = false) const;
+
+ /// This returns the missing options in a form ready for processing by another command line program
+ CLI11_NODISCARD std::vector<std::string> remaining_for_passthrough(bool recurse = false) const;
+
+ /// This returns the number of remaining options, minus the -- separator
+ CLI11_NODISCARD std::size_t remaining_size(bool recurse = false) const;
+
+ ///@}
+
+ protected:
+ /// Check the options to make sure there are no conflicts.
+ ///
+ /// Currently checks to see if multiple positionals exist with unlimited args and checks if the min and max options
+ /// are feasible
+ void _validate() const;
+
+ /// configure subcommands to enable parsing through the current object
+ /// set the correct fallthrough and prefix for nameless subcommands and manage the automatic enable or disable
+ /// makes sure parent is set correctly
+ void _configure();
+
+ /// Internal function to run (App) callback, bottom up
+ void run_callback(bool final_mode = false, bool suppress_final_callback = false);
+
+ /// Check to see if a subcommand is valid. Give up immediately if subcommand max has been reached.
+ CLI11_NODISCARD bool _valid_subcommand(const std::string ¤t, bool ignore_used = true) const;
+
+ /// Selects a Classifier enum based on the type of the current argument
+ CLI11_NODISCARD detail::Classifier _recognize(const std::string ¤t,
+ bool ignore_used_subcommands = true) const;
+
+ // The parse function is now broken into several parts, and part of process
+
+ /// Read and process a configuration file (main app only)
+ void _process_config_file();
+
+ /// Read and process a particular configuration file
+ bool _process_config_file(const std::string &config_file, bool throw_error);
+
+ /// Get envname options if not yet passed. Runs on *all* subcommands.
+ void _process_env();
+
+ /// Process callbacks. Runs on *all* subcommands.
+ void _process_callbacks();
+
+ /// Run help flag processing if any are found.
+ ///
+ /// The flags allow recursive calls to remember if there was a help flag on a parent.
+ void _process_help_flags(bool trigger_help = false, bool trigger_all_help = false) const;
+
+ /// Verify required options and cross requirements. Subcommands too (only if selected).
+ void _process_requirements();
+
+ /// Process callbacks and such.
+ void _process();
+
+ /// Throw an error if anything is left over and should not be.
+ void _process_extras();
+
+ /// Throw an error if anything is left over and should not be.
+ /// Modifies the args to fill in the missing items before throwing.
+ void _process_extras(std::vector<std::string> &args);
+
+ /// Internal function to recursively increment the parsed counter on the current app as well unnamed subcommands
+ void increment_parsed();
+
+ /// Internal parse function
+ void _parse(std::vector<std::string> &args);
+
+ /// Internal parse function
+ void _parse(std::vector<std::string> &&args);
+
+ /// Internal function to parse a stream
+ void _parse_stream(std::istream &input);
+
+ /// Parse one config param, return false if not found in any subcommand, remove if it is
+ ///
+ /// If this has more than one dot.separated.name, go into the subcommand matching it
+ /// Returns true if it managed to find the option, if false you'll need to remove the arg manually.
+ void _parse_config(const std::vector<ConfigItem> &args);
+
+ /// Fill in a single config option
+ bool _parse_single_config(const ConfigItem &item, std::size_t level = 0);
+
+ /// Parse "one" argument (some may eat more than one), delegate to parent if fails, add to missing if missing
+ /// from main return false if the parse has failed and needs to return to parent
+ bool _parse_single(std::vector<std::string> &args, bool &positional_only);
+
+ /// Count the required remaining positional arguments
+ CLI11_NODISCARD std::size_t _count_remaining_positionals(bool required_only = false) const;
+
+ /// Count the required remaining positional arguments
+ CLI11_NODISCARD bool _has_remaining_positionals() const;
+
+ /// Parse a positional, go up the tree to check
+ /// @param haltOnSubcommand if set to true the operation will not process subcommands merely return false
+ /// Return true if the positional was used false otherwise
+ bool _parse_positional(std::vector<std::string> &args, bool haltOnSubcommand);
+
+ /// Locate a subcommand by name with two conditions, should disabled subcommands be ignored, and should used
+ /// subcommands be ignored
+ CLI11_NODISCARD App *
+ _find_subcommand(const std::string &subc_name, bool ignore_disabled, bool ignore_used) const noexcept;
+
+ /// Parse a subcommand, modify args and continue
+ ///
+ /// Unlike the others, this one will always allow fallthrough
+ /// return true if the subcommand was processed false otherwise
+ bool _parse_subcommand(std::vector<std::string> &args);
+
+ /// Parse a short (false) or long (true) argument, must be at the top of the list
+ /// if local_processing_only is set to true then fallthrough is disabled will return false if not found
+ /// return true if the argument was processed or false if nothing was done
+ bool _parse_arg(std::vector<std::string> &args, detail::Classifier current_type, bool local_processing_only);
+
+ /// Trigger the pre_parse callback if needed
+ void _trigger_pre_parse(std::size_t remaining_args);
+
+ /// Get the appropriate parent to fallthrough to which is the first one that has a name or the main app
+ App *_get_fallthrough_parent();
+
+ /// Helper function to run through all possible comparisons of subcommand names to check there is no overlap
+ CLI11_NODISCARD const std::string &_compare_subcommand_names(const App &subcom, const App &base) const;
+
+ /// Helper function to place extra values in the most appropriate position
+ void _move_to_missing(detail::Classifier val_type, const std::string &val);
+
+ public:
+ /// function that could be used by subclasses of App to shift options around into subcommands
+ void _move_option(Option *opt, App *app);
+}; // namespace CLI
+
+/// Extension of App to better manage groups of options
+class Option_group : public App {
+ public:
+ Option_group(std::string group_description, std::string group_name, App *parent)
+ : App(std::move(group_description), "", parent) {
+ group(group_name);
+ // option groups should have automatic fallthrough
+ }
+ using App::add_option;
+ /// Add an existing option to the Option_group
+ Option *add_option(Option *opt) {
+ if(get_parent() == nullptr) {
+ throw OptionNotFound("Unable to locate the specified option");
+ }
+ get_parent()->_move_option(opt, this);
+ return opt;
+ }
+ /// Add an existing option to the Option_group
+ void add_options(Option *opt) { add_option(opt); }
+ /// Add a bunch of options to the group
+ template <typename... Args> void add_options(Option *opt, Args... args) {
+ add_option(opt);
+ add_options(args...);
+ }
+ using App::add_subcommand;
+ /// Add an existing subcommand to be a member of an option_group
+ App *add_subcommand(App *subcom) {
+ App_p subc = subcom->get_parent()->get_subcommand_ptr(subcom);
+ subc->get_parent()->remove_subcommand(subcom);
+ add_subcommand(std::move(subc));
+ return subcom;
+ }
+};
+
+/// Helper function to enable one option group/subcommand when another is used
+CLI11_INLINE void TriggerOn(App *trigger_app, App *app_to_enable);
+
+/// Helper function to enable one option group/subcommand when another is used
+CLI11_INLINE void TriggerOn(App *trigger_app, std::vector<App *> apps_to_enable);
+
+/// Helper function to disable one option group/subcommand when another is used
+CLI11_INLINE void TriggerOff(App *trigger_app, App *app_to_enable);
+
+/// Helper function to disable one option group/subcommand when another is used
+CLI11_INLINE void TriggerOff(App *trigger_app, std::vector<App *> apps_to_enable);
+
+/// Helper function to mark an option as deprecated
+CLI11_INLINE void deprecate_option(Option *opt, const std::string &replacement = "");
+
+/// Helper function to mark an option as deprecated
+inline void deprecate_option(App *app, const std::string &option_name, const std::string &replacement = "") {
+ auto *opt = app->get_option(option_name);
+ deprecate_option(opt, replacement);
+}
+
+/// Helper function to mark an option as deprecated
+inline void deprecate_option(App &app, const std::string &option_name, const std::string &replacement = "") {
+ auto *opt = app.get_option(option_name);
+ deprecate_option(opt, replacement);
+}
+
+/// Helper function to mark an option as retired
+CLI11_INLINE void retire_option(App *app, Option *opt);
+
+/// Helper function to mark an option as retired
+CLI11_INLINE void retire_option(App &app, Option *opt);
+
+/// Helper function to mark an option as retired
+CLI11_INLINE void retire_option(App *app, const std::string &option_name);
+
+/// Helper function to mark an option as retired
+CLI11_INLINE void retire_option(App &app, const std::string &option_name);
+
+namespace detail {
+/// This class is simply to allow tests access to App's protected functions
+struct AppFriend {
+#ifdef CLI11_CPP14
+
+ /// Wrap _parse_short, perfectly forward arguments and return
+ template <typename... Args> static decltype(auto) parse_arg(App *app, Args &&...args) {
+ return app->_parse_arg(std::forward<Args>(args)...);
+ }
+
+ /// Wrap _parse_subcommand, perfectly forward arguments and return
+ template <typename... Args> static decltype(auto) parse_subcommand(App *app, Args &&...args) {
+ return app->_parse_subcommand(std::forward<Args>(args)...);
+ }
+#else
+ /// Wrap _parse_short, perfectly forward arguments and return
+ template <typename... Args>
+ static auto parse_arg(App *app, Args &&...args) ->
+ typename std::result_of<decltype (&App::_parse_arg)(App, Args...)>::type {
+ return app->_parse_arg(std::forward<Args>(args)...);
+ }
+
+ /// Wrap _parse_subcommand, perfectly forward arguments and return
+ template <typename... Args>
+ static auto parse_subcommand(App *app, Args &&...args) ->
+ typename std::result_of<decltype (&App::_parse_subcommand)(App, Args...)>::type {
+ return app->_parse_subcommand(std::forward<Args>(args)...);
+ }
+#endif
+ /// Wrap the fallthrough parent function to make sure that is working correctly
+ static App *get_fallthrough_parent(App *app) { return app->_get_fallthrough_parent(); }
+};
+} // namespace detail
+
+
+
+
+CLI11_INLINE App::App(std::string app_description, std::string app_name, App *parent)
+ : name_(std::move(app_name)), description_(std::move(app_description)), parent_(parent) {
+ // Inherit if not from a nullptr
+ if(parent_ != nullptr) {
+ if(parent_->help_ptr_ != nullptr)
+ set_help_flag(parent_->help_ptr_->get_name(false, true), parent_->help_ptr_->get_description());
+ if(parent_->help_all_ptr_ != nullptr)
+ set_help_all_flag(parent_->help_all_ptr_->get_name(false, true), parent_->help_all_ptr_->get_description());
+
+ /// OptionDefaults
+ option_defaults_ = parent_->option_defaults_;
+
+ // INHERITABLE
+ failure_message_ = parent_->failure_message_;
+ allow_extras_ = parent_->allow_extras_;
+ allow_config_extras_ = parent_->allow_config_extras_;
+ prefix_command_ = parent_->prefix_command_;
+ immediate_callback_ = parent_->immediate_callback_;
+ ignore_case_ = parent_->ignore_case_;
+ ignore_underscore_ = parent_->ignore_underscore_;
+ fallthrough_ = parent_->fallthrough_;
+ validate_positionals_ = parent_->validate_positionals_;
+ validate_optional_arguments_ = parent_->validate_optional_arguments_;
+ configurable_ = parent_->configurable_;
+ allow_windows_style_options_ = parent_->allow_windows_style_options_;
+ group_ = parent_->group_;
+ usage_ = parent_->usage_;
+ footer_ = parent_->footer_;
+ formatter_ = parent_->formatter_;
+ config_formatter_ = parent_->config_formatter_;
+ require_subcommand_max_ = parent_->require_subcommand_max_;
+ }
+}
+
+CLI11_NODISCARD CLI11_INLINE char **App::ensure_utf8(char **argv) {
+#ifdef _WIN32
+ (void)argv;
+
+ normalized_argv_ = detail::compute_win32_argv();
+
+ if(!normalized_argv_view_.empty()) {
+ normalized_argv_view_.clear();
+ }
+
+ normalized_argv_view_.reserve(normalized_argv_.size());
+ for(auto &arg : normalized_argv_) {
+ // using const_cast is well-defined, string is known to not be const.
+ normalized_argv_view_.push_back(const_cast<char *>(arg.data()));
+ }
+
+ return normalized_argv_view_.data();
+#else
+ return argv;
+#endif
+}
+
+CLI11_INLINE App *App::name(std::string app_name) {
+
+ if(parent_ != nullptr) {
+ std::string oname = name_;
+ name_ = app_name;
+ const auto &res = _compare_subcommand_names(*this, *_get_fallthrough_parent());
+ if(!res.empty()) {
+ name_ = oname;
+ throw(OptionAlreadyAdded(app_name + " conflicts with existing subcommand names"));
+ }
+ } else {
+ name_ = app_name;
+ }
+ has_automatic_name_ = false;
+ return this;
+}
+
+CLI11_INLINE App *App::alias(std::string app_name) {
+ if(app_name.empty() || !detail::valid_alias_name_string(app_name)) {
+ throw IncorrectConstruction("Aliases may not be empty or contain newlines or null characters");
+ }
+ if(parent_ != nullptr) {
+ aliases_.push_back(app_name);
+ const auto &res = _compare_subcommand_names(*this, *_get_fallthrough_parent());
+ if(!res.empty()) {
+ aliases_.pop_back();
+ throw(OptionAlreadyAdded("alias already matches an existing subcommand: " + app_name));
+ }
+ } else {
+ aliases_.push_back(app_name);
+ }
+
+ return this;
+}
+
+CLI11_INLINE App *App::immediate_callback(bool immediate) {
+ immediate_callback_ = immediate;
+ if(immediate_callback_) {
+ if(final_callback_ && !(parse_complete_callback_)) {
+ std::swap(final_callback_, parse_complete_callback_);
+ }
+ } else if(!(final_callback_) && parse_complete_callback_) {
+ std::swap(final_callback_, parse_complete_callback_);
+ }
+ return this;
+}
+
+CLI11_INLINE App *App::ignore_case(bool value) {
+ if(value && !ignore_case_) {
+ ignore_case_ = true;
+ auto *p = (parent_ != nullptr) ? _get_fallthrough_parent() : this;
+ const auto &match = _compare_subcommand_names(*this, *p);
+ if(!match.empty()) {
+ ignore_case_ = false; // we are throwing so need to be exception invariant
+ throw OptionAlreadyAdded("ignore case would cause subcommand name conflicts: " + match);
+ }
+ }
+ ignore_case_ = value;
+ return this;
+}
+
+CLI11_INLINE App *App::ignore_underscore(bool value) {
+ if(value && !ignore_underscore_) {
+ ignore_underscore_ = true;
+ auto *p = (parent_ != nullptr) ? _get_fallthrough_parent() : this;
+ const auto &match = _compare_subcommand_names(*this, *p);
+ if(!match.empty()) {
+ ignore_underscore_ = false;
+ throw OptionAlreadyAdded("ignore underscore would cause subcommand name conflicts: " + match);
+ }
+ }
+ ignore_underscore_ = value;
+ return this;
+}
+
+CLI11_INLINE Option *App::add_option(std::string option_name,
+ callback_t option_callback,
+ std::string option_description,
+ bool defaulted,
+ std::function<std::string()> func) {
+ Option myopt{option_name, option_description, option_callback, this};
+
+ if(std::find_if(std::begin(options_), std::end(options_), [&myopt](const Option_p &v) { return *v == myopt; }) ==
+ std::end(options_)) {
+ if(myopt.lnames_.empty() && myopt.snames_.empty()) {
+ // if the option is positional only there is additional potential for ambiguities in config files and needs
+ // to be checked
+ std::string test_name = "--" + myopt.get_single_name();
+ if(test_name.size() == 3) {
+ test_name.erase(0, 1);
+ }
+
+ auto *op = get_option_no_throw(test_name);
+ if(op != nullptr) {
+ throw(OptionAlreadyAdded("added option positional name matches existing option: " + test_name));
+ }
+ } else if(parent_ != nullptr) {
+ for(auto &ln : myopt.lnames_) {
+ auto *op = parent_->get_option_no_throw(ln);
+ if(op != nullptr) {
+ throw(OptionAlreadyAdded("added option matches existing positional option: " + ln));
+ }
+ }
+ for(auto &sn : myopt.snames_) {
+ auto *op = parent_->get_option_no_throw(sn);
+ if(op != nullptr) {
+ throw(OptionAlreadyAdded("added option matches existing positional option: " + sn));
+ }
+ }
+ }
+ options_.emplace_back();
+ Option_p &option = options_.back();
+ option.reset(new Option(option_name, option_description, option_callback, this));
+
+ // Set the default string capture function
+ option->default_function(func);
+
+ // For compatibility with CLI11 1.7 and before, capture the default string here
+ if(defaulted)
+ option->capture_default_str();
+
+ // Transfer defaults to the new option
+ option_defaults_.copy_to(option.get());
+
+ // Don't bother to capture if we already did
+ if(!defaulted && option->get_always_capture_default())
+ option->capture_default_str();
+
+ return option.get();
+ }
+ // we know something matches now find what it is so we can produce more error information
+ for(auto &opt : options_) {
+ const auto &matchname = opt->matching_name(myopt);
+ if(!matchname.empty()) {
+ throw(OptionAlreadyAdded("added option matched existing option name: " + matchname));
+ }
+ }
+ // this line should not be reached the above loop should trigger the throw
+ throw(OptionAlreadyAdded("added option matched existing option name")); // LCOV_EXCL_LINE
+}
+
+CLI11_INLINE Option *App::set_help_flag(std::string flag_name, const std::string &help_description) {
+ // take flag_description by const reference otherwise add_flag tries to assign to help_description
+ if(help_ptr_ != nullptr) {
+ remove_option(help_ptr_);
+ help_ptr_ = nullptr;
+ }
+
+ // Empty name will simply remove the help flag
+ if(!flag_name.empty()) {
+ help_ptr_ = add_flag(flag_name, help_description);
+ help_ptr_->configurable(false);
+ }
+
+ return help_ptr_;
+}
+
+CLI11_INLINE Option *App::set_help_all_flag(std::string help_name, const std::string &help_description) {
+ // take flag_description by const reference otherwise add_flag tries to assign to flag_description
+ if(help_all_ptr_ != nullptr) {
+ remove_option(help_all_ptr_);
+ help_all_ptr_ = nullptr;
+ }
+
+ // Empty name will simply remove the help all flag
+ if(!help_name.empty()) {
+ help_all_ptr_ = add_flag(help_name, help_description);
+ help_all_ptr_->configurable(false);
+ }
+
+ return help_all_ptr_;
+}
+
+CLI11_INLINE Option *
+App::set_version_flag(std::string flag_name, const std::string &versionString, const std::string &version_help) {
+ // take flag_description by const reference otherwise add_flag tries to assign to version_description
+ if(version_ptr_ != nullptr) {
+ remove_option(version_ptr_);
+ version_ptr_ = nullptr;
+ }
+
+ // Empty name will simply remove the version flag
+ if(!flag_name.empty()) {
+ version_ptr_ = add_flag_callback(
+ flag_name, [versionString]() { throw(CLI::CallForVersion(versionString, 0)); }, version_help);
+ version_ptr_->configurable(false);
+ }
+
+ return version_ptr_;
+}
+
+CLI11_INLINE Option *
+App::set_version_flag(std::string flag_name, std::function<std::string()> vfunc, const std::string &version_help) {
+ if(version_ptr_ != nullptr) {
+ remove_option(version_ptr_);
+ version_ptr_ = nullptr;
+ }
+
+ // Empty name will simply remove the version flag
+ if(!flag_name.empty()) {
+ version_ptr_ = add_flag_callback(
+ flag_name, [vfunc]() { throw(CLI::CallForVersion(vfunc(), 0)); }, version_help);
+ version_ptr_->configurable(false);
+ }
+
+ return version_ptr_;
+}
+
+CLI11_INLINE Option *App::_add_flag_internal(std::string flag_name, CLI::callback_t fun, std::string flag_description) {
+ Option *opt = nullptr;
+ if(detail::has_default_flag_values(flag_name)) {
+ // check for default values and if it has them
+ auto flag_defaults = detail::get_default_flag_values(flag_name);
+ detail::remove_default_flag_values(flag_name);
+ opt = add_option(std::move(flag_name), std::move(fun), std::move(flag_description), false);
+ for(const auto &fname : flag_defaults)
+ opt->fnames_.push_back(fname.first);
+ opt->default_flag_values_ = std::move(flag_defaults);
+ } else {
+ opt = add_option(std::move(flag_name), std::move(fun), std::move(flag_description), false);
+ }
+ // flags cannot have positional values
+ if(opt->get_positional()) {
+ auto pos_name = opt->get_name(true);
+ remove_option(opt);
+ throw IncorrectConstruction::PositionalFlag(pos_name);
+ }
+ opt->multi_option_policy(MultiOptionPolicy::TakeLast);
+ opt->expected(0);
+ opt->required(false);
+ return opt;
+}
+
+CLI11_INLINE Option *App::add_flag_callback(std::string flag_name,
+ std::function<void(void)> function, ///< A function to call, void(void)
+ std::string flag_description) {
+
+ CLI::callback_t fun = [function](const CLI::results_t &res) {
+ using CLI::detail::lexical_cast;
+ bool trigger{false};
+ auto result = lexical_cast(res[0], trigger);
+ if(result && trigger) {
+ function();
+ }
+ return result;
+ };
+ return _add_flag_internal(flag_name, std::move(fun), std::move(flag_description));
+}
+
+CLI11_INLINE Option *
+App::add_flag_function(std::string flag_name,
+ std::function<void(std::int64_t)> function, ///< A function to call, void(int)
+ std::string flag_description) {
+
+ CLI::callback_t fun = [function](const CLI::results_t &res) {
+ using CLI::detail::lexical_cast;
+ std::int64_t flag_count{0};
+ lexical_cast(res[0], flag_count);
+ function(flag_count);
+ return true;
+ };
+ return _add_flag_internal(flag_name, std::move(fun), std::move(flag_description))
+ ->multi_option_policy(MultiOptionPolicy::Sum);
+}
+
+CLI11_INLINE Option *App::set_config(std::string option_name,
+ std::string default_filename,
+ const std::string &help_message,
+ bool config_required) {
+
+ // Remove existing config if present
+ if(config_ptr_ != nullptr) {
+ remove_option(config_ptr_);
+ config_ptr_ = nullptr; // need to remove the config_ptr completely
+ }
+
+ // Only add config if option passed
+ if(!option_name.empty()) {
+ config_ptr_ = add_option(option_name, help_message);
+ if(config_required) {
+ config_ptr_->required();
+ }
+ if(!default_filename.empty()) {
+ config_ptr_->default_str(std::move(default_filename));
+ config_ptr_->force_callback_ = true;
+ }
+ config_ptr_->configurable(false);
+ // set the option to take the last value and reverse given by default
+ config_ptr_->multi_option_policy(MultiOptionPolicy::Reverse);
+ }
+
+ return config_ptr_;
+}
+
+CLI11_INLINE bool App::remove_option(Option *opt) {
+ // Make sure no links exist
+ for(Option_p &op : options_) {
+ op->remove_needs(opt);
+ op->remove_excludes(opt);
+ }
+
+ if(help_ptr_ == opt)
+ help_ptr_ = nullptr;
+ if(help_all_ptr_ == opt)
+ help_all_ptr_ = nullptr;
+
+ auto iterator =
+ std::find_if(std::begin(options_), std::end(options_), [opt](const Option_p &v) { return v.get() == opt; });
+ if(iterator != std::end(options_)) {
+ options_.erase(iterator);
+ return true;
+ }
+ return false;
+}
+
+CLI11_INLINE App *App::add_subcommand(std::string subcommand_name, std::string subcommand_description) {
+ if(!subcommand_name.empty() && !detail::valid_name_string(subcommand_name)) {
+ if(!detail::valid_first_char(subcommand_name[0])) {
+ throw IncorrectConstruction(
+ "Subcommand name starts with invalid character, '!' and '-' and control characters");
+ }
+ for(auto c : subcommand_name) {
+ if(!detail::valid_later_char(c)) {
+ throw IncorrectConstruction(std::string("Subcommand name contains invalid character ('") + c +
+ "'), all characters are allowed except"
+ "'=',':','{','}', ' ', and control characters");
+ }
+ }
+ }
+ CLI::App_p subcom = std::shared_ptr<App>(new App(std::move(subcommand_description), subcommand_name, this));
+ return add_subcommand(std::move(subcom));
+}
+
+CLI11_INLINE App *App::add_subcommand(CLI::App_p subcom) {
+ if(!subcom)
+ throw IncorrectConstruction("passed App is not valid");
+ auto *ckapp = (name_.empty() && parent_ != nullptr) ? _get_fallthrough_parent() : this;
+ const auto &mstrg = _compare_subcommand_names(*subcom, *ckapp);
+ if(!mstrg.empty()) {
+ throw(OptionAlreadyAdded("subcommand name or alias matches existing subcommand: " + mstrg));
+ }
+ subcom->parent_ = this;
+ subcommands_.push_back(std::move(subcom));
+ return subcommands_.back().get();
+}
+
+CLI11_INLINE bool App::remove_subcommand(App *subcom) {
+ // Make sure no links exist
+ for(App_p &sub : subcommands_) {
+ sub->remove_excludes(subcom);
+ sub->remove_needs(subcom);
+ }
+
+ auto iterator = std::find_if(
+ std::begin(subcommands_), std::end(subcommands_), [subcom](const App_p &v) { return v.get() == subcom; });
+ if(iterator != std::end(subcommands_)) {
+ subcommands_.erase(iterator);
+ return true;
+ }
+ return false;
+}
+
+CLI11_INLINE App *App::get_subcommand(const App *subcom) const {
+ if(subcom == nullptr)
+ throw OptionNotFound("nullptr passed");
+ for(const App_p &subcomptr : subcommands_)
+ if(subcomptr.get() == subcom)
+ return subcomptr.get();
+ throw OptionNotFound(subcom->get_name());
+}
+
+CLI11_NODISCARD CLI11_INLINE App *App::get_subcommand(std::string subcom) const {
+ auto *subc = _find_subcommand(subcom, false, false);
+ if(subc == nullptr)
+ throw OptionNotFound(subcom);
+ return subc;
+}
+
+CLI11_NODISCARD CLI11_INLINE App *App::get_subcommand(int index) const {
+ if(index >= 0) {
+ auto uindex = static_cast<unsigned>(index);
+ if(uindex < subcommands_.size())
+ return subcommands_[uindex].get();
+ }
+ throw OptionNotFound(std::to_string(index));
+}
+
+CLI11_INLINE CLI::App_p App::get_subcommand_ptr(App *subcom) const {
+ if(subcom == nullptr)
+ throw OptionNotFound("nullptr passed");
+ for(const App_p &subcomptr : subcommands_)
+ if(subcomptr.get() == subcom)
+ return subcomptr;
+ throw OptionNotFound(subcom->get_name());
+}
+
+CLI11_NODISCARD CLI11_INLINE CLI::App_p App::get_subcommand_ptr(std::string subcom) const {
+ for(const App_p &subcomptr : subcommands_)
+ if(subcomptr->check_name(subcom))
+ return subcomptr;
+ throw OptionNotFound(subcom);
+}
+
+CLI11_NODISCARD CLI11_INLINE CLI::App_p App::get_subcommand_ptr(int index) const {
+ if(index >= 0) {
+ auto uindex = static_cast<unsigned>(index);
+ if(uindex < subcommands_.size())
+ return subcommands_[uindex];
+ }
+ throw OptionNotFound(std::to_string(index));
+}
+
+CLI11_NODISCARD CLI11_INLINE CLI::App *App::get_option_group(std::string group_name) const {
+ for(const App_p &app : subcommands_) {
+ if(app->name_.empty() && app->group_ == group_name) {
+ return app.get();
+ }
+ }
+ throw OptionNotFound(group_name);
+}
+
+CLI11_NODISCARD CLI11_INLINE std::size_t App::count_all() const {
+ std::size_t cnt{0};
+ for(const auto &opt : options_) {
+ cnt += opt->count();
+ }
+ for(const auto &sub : subcommands_) {
+ cnt += sub->count_all();
+ }
+ if(!get_name().empty()) { // for named subcommands add the number of times the subcommand was called
+ cnt += parsed_;
+ }
+ return cnt;
+}
+
+CLI11_INLINE void App::clear() {
+
+ parsed_ = 0;
+ pre_parse_called_ = false;
+
+ missing_.clear();
+ parsed_subcommands_.clear();
+ for(const Option_p &opt : options_) {
+ opt->clear();
+ }
+ for(const App_p &subc : subcommands_) {
+ subc->clear();
+ }
+}
+
+CLI11_INLINE void App::parse(int argc, const char *const *argv) { parse_char_t(argc, argv); }
+CLI11_INLINE void App::parse(int argc, const wchar_t *const *argv) { parse_char_t(argc, argv); }
+
+namespace detail {
+
+// Do nothing or perform narrowing
+CLI11_INLINE const char *maybe_narrow(const char *str) { return str; }
+CLI11_INLINE std::string maybe_narrow(const wchar_t *str) { return narrow(str); }
+
+} // namespace detail
+
+template <class CharT> CLI11_INLINE void App::parse_char_t(int argc, const CharT *const *argv) {
+ // If the name is not set, read from command line
+ if(name_.empty() || has_automatic_name_) {
+ has_automatic_name_ = true;
+ name_ = detail::maybe_narrow(argv[0]);
+ }
+
+ std::vector<std::string> args;
+ args.reserve(static_cast<std::size_t>(argc) - 1U);
+ for(auto i = static_cast<std::size_t>(argc) - 1U; i > 0U; --i)
+ args.emplace_back(detail::maybe_narrow(argv[i]));
+
+ parse(std::move(args));
+}
+
+CLI11_INLINE void App::parse(std::string commandline, bool program_name_included) {
+
+ if(program_name_included) {
+ auto nstr = detail::split_program_name(commandline);
+ if((name_.empty()) || (has_automatic_name_)) {
+ has_automatic_name_ = true;
+ name_ = nstr.first;
+ }
+ commandline = std::move(nstr.second);
+ } else {
+ detail::trim(commandline);
+ }
+ // the next section of code is to deal with quoted arguments after an '=' or ':' for windows like operations
+ if(!commandline.empty()) {
+ commandline = detail::find_and_modify(commandline, "=", detail::escape_detect);
+ if(allow_windows_style_options_)
+ commandline = detail::find_and_modify(commandline, ":", detail::escape_detect);
+ }
+
+ auto args = detail::split_up(std::move(commandline));
+ // remove all empty strings
+ args.erase(std::remove(args.begin(), args.end(), std::string{}), args.end());
+ try {
+ detail::remove_quotes(args);
+ } catch(const std::invalid_argument &arg) {
+ throw CLI::ParseError(arg.what(), CLI::ExitCodes::InvalidError);
+ }
+ std::reverse(args.begin(), args.end());
+ parse(std::move(args));
+}
+
+CLI11_INLINE void App::parse(std::wstring commandline, bool program_name_included) {
+ parse(narrow(commandline), program_name_included);
+}
+
+CLI11_INLINE void App::parse(std::vector<std::string> &args) {
+ // Clear if parsed
+ if(parsed_ > 0)
+ clear();
+
+ // parsed_ is incremented in commands/subcommands,
+ // but placed here to make sure this is cleared when
+ // running parse after an error is thrown, even by _validate or _configure.
+ parsed_ = 1;
+ _validate();
+ _configure();
+ // set the parent as nullptr as this object should be the top now
+ parent_ = nullptr;
+ parsed_ = 0;
+
+ _parse(args);
+ run_callback();
+}
+
+CLI11_INLINE void App::parse(std::vector<std::string> &&args) {
+ // Clear if parsed
+ if(parsed_ > 0)
+ clear();
+
+ // parsed_ is incremented in commands/subcommands,
+ // but placed here to make sure this is cleared when
+ // running parse after an error is thrown, even by _validate or _configure.
+ parsed_ = 1;
+ _validate();
+ _configure();
+ // set the parent as nullptr as this object should be the top now
+ parent_ = nullptr;
+ parsed_ = 0;
+
+ _parse(std::move(args));
+ run_callback();
+}
+
+CLI11_INLINE void App::parse_from_stream(std::istream &input) {
+ if(parsed_ == 0) {
+ _validate();
+ _configure();
+ // set the parent as nullptr as this object should be the top now
+ }
+
+ _parse_stream(input);
+ run_callback();
+}
+
+CLI11_INLINE int App::exit(const Error &e, std::ostream &out, std::ostream &err) const {
+
+ /// Avoid printing anything if this is a CLI::RuntimeError
+ if(e.get_name() == "RuntimeError")
+ return e.get_exit_code();
+
+ if(e.get_name() == "CallForHelp") {
+ out << help();
+ return e.get_exit_code();
+ }
+
+ if(e.get_name() == "CallForAllHelp") {
+ out << help("", AppFormatMode::All);
+ return e.get_exit_code();
+ }
+
+ if(e.get_name() == "CallForVersion") {
+ out << e.what() << '\n';
+ return e.get_exit_code();
+ }
+
+ if(e.get_exit_code() != static_cast<int>(ExitCodes::Success)) {
+ if(failure_message_)
+ err << failure_message_(this, e) << std::flush;
+ }
+
+ return e.get_exit_code();
+}
+
+CLI11_INLINE std::vector<const App *> App::get_subcommands(const std::function<bool(const App *)> &filter) const {
+ std::vector<const App *> subcomms(subcommands_.size());
+ std::transform(
+ std::begin(subcommands_), std::end(subcommands_), std::begin(subcomms), [](const App_p &v) { return v.get(); });
+
+ if(filter) {
+ subcomms.erase(std::remove_if(std::begin(subcomms),
+ std::end(subcomms),
+ [&filter](const App *app) { return !filter(app); }),
+ std::end(subcomms));
+ }
+
+ return subcomms;
+}
+
+CLI11_INLINE std::vector<App *> App::get_subcommands(const std::function<bool(App *)> &filter) {
+ std::vector<App *> subcomms(subcommands_.size());
+ std::transform(
+ std::begin(subcommands_), std::end(subcommands_), std::begin(subcomms), [](const App_p &v) { return v.get(); });
+
+ if(filter) {
+ subcomms.erase(
+ std::remove_if(std::begin(subcomms), std::end(subcomms), [&filter](App *app) { return !filter(app); }),
+ std::end(subcomms));
+ }
+
+ return subcomms;
+}
+
+CLI11_INLINE bool App::remove_excludes(Option *opt) {
+ auto iterator = std::find(std::begin(exclude_options_), std::end(exclude_options_), opt);
+ if(iterator == std::end(exclude_options_)) {
+ return false;
+ }
+ exclude_options_.erase(iterator);
+ return true;
+}
+
+CLI11_INLINE bool App::remove_excludes(App *app) {
+ auto iterator = std::find(std::begin(exclude_subcommands_), std::end(exclude_subcommands_), app);
+ if(iterator == std::end(exclude_subcommands_)) {
+ return false;
+ }
+ auto *other_app = *iterator;
+ exclude_subcommands_.erase(iterator);
+ other_app->remove_excludes(this);
+ return true;
+}
+
+CLI11_INLINE bool App::remove_needs(Option *opt) {
+ auto iterator = std::find(std::begin(need_options_), std::end(need_options_), opt);
+ if(iterator == std::end(need_options_)) {
+ return false;
+ }
+ need_options_.erase(iterator);
+ return true;
+}
+
+CLI11_INLINE bool App::remove_needs(App *app) {
+ auto iterator = std::find(std::begin(need_subcommands_), std::end(need_subcommands_), app);
+ if(iterator == std::end(need_subcommands_)) {
+ return false;
+ }
+ need_subcommands_.erase(iterator);
+ return true;
+}
+
+CLI11_NODISCARD CLI11_INLINE std::string App::help(std::string prev, AppFormatMode mode) const {
+ if(prev.empty())
+ prev = get_name();
+ else
+ prev += " " + get_name();
+
+ // Delegate to subcommand if needed
+ auto selected_subcommands = get_subcommands();
+ if(!selected_subcommands.empty()) {
+ return selected_subcommands.back()->help(prev, mode);
+ }
+ return formatter_->make_help(this, prev, mode);
+}
+
+CLI11_NODISCARD CLI11_INLINE std::string App::version() const {
+ std::string val;
+ if(version_ptr_ != nullptr) {
+ // copy the results for reuse later
+ results_t rv = version_ptr_->results();
+ version_ptr_->clear();
+ version_ptr_->add_result("true");
+ try {
+ version_ptr_->run_callback();
+ } catch(const CLI::CallForVersion &cfv) {
+ val = cfv.what();
+ }
+ version_ptr_->clear();
+ version_ptr_->add_result(rv);
+ }
+ return val;
+}
+
+CLI11_INLINE std::vector<const Option *> App::get_options(const std::function<bool(const Option *)> filter) const {
+ std::vector<const Option *> options(options_.size());
+ std::transform(
+ std::begin(options_), std::end(options_), std::begin(options), [](const Option_p &val) { return val.get(); });
+
+ if(filter) {
+ options.erase(std::remove_if(std::begin(options),
+ std::end(options),
+ [&filter](const Option *opt) { return !filter(opt); }),
+ std::end(options));
+ }
+
+ return options;
+}
+
+CLI11_INLINE std::vector<Option *> App::get_options(const std::function<bool(Option *)> filter) {
+ std::vector<Option *> options(options_.size());
+ std::transform(
+ std::begin(options_), std::end(options_), std::begin(options), [](const Option_p &val) { return val.get(); });
+
+ if(filter) {
+ options.erase(
+ std::remove_if(std::begin(options), std::end(options), [&filter](Option *opt) { return !filter(opt); }),
+ std::end(options));
+ }
+
+ return options;
+}
+
+CLI11_INLINE Option *App::get_option_no_throw(std::string option_name) noexcept {
+ for(Option_p &opt : options_) {
+ if(opt->check_name(option_name)) {
+ return opt.get();
+ }
+ }
+ for(auto &subc : subcommands_) {
+ // also check down into nameless subcommands
+ if(subc->get_name().empty()) {
+ auto *opt = subc->get_option_no_throw(option_name);
+ if(opt != nullptr) {
+ return opt;
+ }
+ }
+ }
+ return nullptr;
+}
+
+CLI11_NODISCARD CLI11_INLINE const Option *App::get_option_no_throw(std::string option_name) const noexcept {
+ for(const Option_p &opt : options_) {
+ if(opt->check_name(option_name)) {
+ return opt.get();
+ }
+ }
+ for(const auto &subc : subcommands_) {
+ // also check down into nameless subcommands
+ if(subc->get_name().empty()) {
+ auto *opt = subc->get_option_no_throw(option_name);
+ if(opt != nullptr) {
+ return opt;
+ }
+ }
+ }
+ return nullptr;
+}
+
+CLI11_NODISCARD CLI11_INLINE std::string App::get_display_name(bool with_aliases) const {
+ if(name_.empty()) {
+ return std::string("[Option Group: ") + get_group() + "]";
+ }
+ if(aliases_.empty() || !with_aliases) {
+ return name_;
+ }
+ std::string dispname = name_;
+ for(const auto &lalias : aliases_) {
+ dispname.push_back(',');
+ dispname.push_back(' ');
+ dispname.append(lalias);
+ }
+ return dispname;
+}
+
+CLI11_NODISCARD CLI11_INLINE bool App::check_name(std::string name_to_check) const {
+ std::string local_name = name_;
+ if(ignore_underscore_) {
+ local_name = detail::remove_underscore(name_);
+ name_to_check = detail::remove_underscore(name_to_check);
+ }
+ if(ignore_case_) {
+ local_name = detail::to_lower(name_);
+ name_to_check = detail::to_lower(name_to_check);
+ }
+
+ if(local_name == name_to_check) {
+ return true;
+ }
+ for(std::string les : aliases_) { // NOLINT(performance-for-range-copy)
+ if(ignore_underscore_) {
+ les = detail::remove_underscore(les);
+ }
+ if(ignore_case_) {
+ les = detail::to_lower(les);
+ }
+ if(les == name_to_check) {
+ return true;
+ }
+ }
+ return false;
+}
+
+CLI11_NODISCARD CLI11_INLINE std::vector<std::string> App::get_groups() const {
+ std::vector<std::string> groups;
+
+ for(const Option_p &opt : options_) {
+ // Add group if it is not already in there
+ if(std::find(groups.begin(), groups.end(), opt->get_group()) == groups.end()) {
+ groups.push_back(opt->get_group());
+ }
+ }
+
+ return groups;
+}
+
+CLI11_NODISCARD CLI11_INLINE std::vector<std::string> App::remaining(bool recurse) const {
+ std::vector<std::string> miss_list;
+ for(const std::pair<detail::Classifier, std::string> &miss : missing_) {
+ miss_list.push_back(std::get<1>(miss));
+ }
+ // Get from a subcommand that may allow extras
+ if(recurse) {
+ if(!allow_extras_) {
+ for(const auto &sub : subcommands_) {
+ if(sub->name_.empty() && !sub->missing_.empty()) {
+ for(const std::pair<detail::Classifier, std::string> &miss : sub->missing_) {
+ miss_list.push_back(std::get<1>(miss));
+ }
+ }
+ }
+ }
+ // Recurse into subcommands
+
+ for(const App *sub : parsed_subcommands_) {
+ std::vector<std::string> output = sub->remaining(recurse);
+ std::copy(std::begin(output), std::end(output), std::back_inserter(miss_list));
+ }
+ }
+ return miss_list;
+}
+
+CLI11_NODISCARD CLI11_INLINE std::vector<std::string> App::remaining_for_passthrough(bool recurse) const {
+ std::vector<std::string> miss_list = remaining(recurse);
+ std::reverse(std::begin(miss_list), std::end(miss_list));
+ return miss_list;
+}
+
+CLI11_NODISCARD CLI11_INLINE std::size_t App::remaining_size(bool recurse) const {
+ auto remaining_options = static_cast<std::size_t>(std::count_if(
+ std::begin(missing_), std::end(missing_), [](const std::pair<detail::Classifier, std::string> &val) {
+ return val.first != detail::Classifier::POSITIONAL_MARK;
+ }));
+
+ if(recurse) {
+ for(const App_p &sub : subcommands_) {
+ remaining_options += sub->remaining_size(recurse);
+ }
+ }
+ return remaining_options;
+}
+
+CLI11_INLINE void App::_validate() const {
+ // count the number of positional only args
+ auto pcount = std::count_if(std::begin(options_), std::end(options_), [](const Option_p &opt) {
+ return opt->get_items_expected_max() >= detail::expected_max_vector_size && !opt->nonpositional();
+ });
+ if(pcount > 1) {
+ auto pcount_req = std::count_if(std::begin(options_), std::end(options_), [](const Option_p &opt) {
+ return opt->get_items_expected_max() >= detail::expected_max_vector_size && !opt->nonpositional() &&
+ opt->get_required();
+ });
+ if(pcount - pcount_req > 1) {
+ throw InvalidError(name_);
+ }
+ }
+
+ std::size_t nameless_subs{0};
+ for(const App_p &app : subcommands_) {
+ app->_validate();
+ if(app->get_name().empty())
+ ++nameless_subs;
+ }
+
+ if(require_option_min_ > 0) {
+ if(require_option_max_ > 0) {
+ if(require_option_max_ < require_option_min_) {
+ throw(InvalidError("Required min options greater than required max options", ExitCodes::InvalidError));
+ }
+ }
+ if(require_option_min_ > (options_.size() + nameless_subs)) {
+ throw(
+ InvalidError("Required min options greater than number of available options", ExitCodes::InvalidError));
+ }
+ }
+}
+
+CLI11_INLINE void App::_configure() {
+ if(default_startup == startup_mode::enabled) {
+ disabled_ = false;
+ } else if(default_startup == startup_mode::disabled) {
+ disabled_ = true;
+ }
+ for(const App_p &app : subcommands_) {
+ if(app->has_automatic_name_) {
+ app->name_.clear();
+ }
+ if(app->name_.empty()) {
+ app->fallthrough_ = false; // make sure fallthrough_ is false to prevent infinite loop
+ app->prefix_command_ = false;
+ }
+ // make sure the parent is set to be this object in preparation for parse
+ app->parent_ = this;
+ app->_configure();
+ }
+}
+
+CLI11_INLINE void App::run_callback(bool final_mode, bool suppress_final_callback) {
+ pre_callback();
+ // in the main app if immediate_callback_ is set it runs the main callback before the used subcommands
+ if(!final_mode && parse_complete_callback_) {
+ parse_complete_callback_();
+ }
+ // run the callbacks for the received subcommands
+ for(App *subc : get_subcommands()) {
+ if(subc->parent_ == this) {
+ subc->run_callback(true, suppress_final_callback);
+ }
+ }
+ // now run callbacks for option_groups
+ for(auto &subc : subcommands_) {
+ if(subc->name_.empty() && subc->count_all() > 0) {
+ subc->run_callback(true, suppress_final_callback);
+ }
+ }
+
+ // finally run the main callback
+ if(final_callback_ && (parsed_ > 0) && (!suppress_final_callback)) {
+ if(!name_.empty() || count_all() > 0 || parent_ == nullptr) {
+ final_callback_();
+ }
+ }
+}
+
+CLI11_NODISCARD CLI11_INLINE bool App::_valid_subcommand(const std::string ¤t, bool ignore_used) const {
+ // Don't match if max has been reached - but still check parents
+ if(require_subcommand_max_ != 0 && parsed_subcommands_.size() >= require_subcommand_max_) {
+ return parent_ != nullptr && parent_->_valid_subcommand(current, ignore_used);
+ }
+ auto *com = _find_subcommand(current, true, ignore_used);
+ if(com != nullptr) {
+ return true;
+ }
+ // Check parent if exists, else return false
+ return parent_ != nullptr && parent_->_valid_subcommand(current, ignore_used);
+}
+
+CLI11_NODISCARD CLI11_INLINE detail::Classifier App::_recognize(const std::string ¤t,
+ bool ignore_used_subcommands) const {
+ std::string dummy1, dummy2;
+
+ if(current == "--")
+ return detail::Classifier::POSITIONAL_MARK;
+ if(_valid_subcommand(current, ignore_used_subcommands))
+ return detail::Classifier::SUBCOMMAND;
+ if(detail::split_long(current, dummy1, dummy2))
+ return detail::Classifier::LONG;
+ if(detail::split_short(current, dummy1, dummy2)) {
+ if(dummy1[0] >= '0' && dummy1[0] <= '9') {
+ if(get_option_no_throw(std::string{'-', dummy1[0]}) == nullptr) {
+ return detail::Classifier::NONE;
+ }
+ }
+ return detail::Classifier::SHORT;
+ }
+ if((allow_windows_style_options_) && (detail::split_windows_style(current, dummy1, dummy2)))
+ return detail::Classifier::WINDOWS_STYLE;
+ if((current == "++") && !name_.empty() && parent_ != nullptr)
+ return detail::Classifier::SUBCOMMAND_TERMINATOR;
+ auto dotloc = current.find_first_of('.');
+ if(dotloc != std::string::npos) {
+ auto *cm = _find_subcommand(current.substr(0, dotloc), true, ignore_used_subcommands);
+ if(cm != nullptr) {
+ auto res = cm->_recognize(current.substr(dotloc + 1), ignore_used_subcommands);
+ if(res == detail::Classifier::SUBCOMMAND) {
+ return res;
+ }
+ }
+ }
+ return detail::Classifier::NONE;
+}
+
+CLI11_INLINE bool App::_process_config_file(const std::string &config_file, bool throw_error) {
+ auto path_result = detail::check_path(config_file.c_str());
+ if(path_result == detail::path_type::file) {
+ try {
+ std::vector<ConfigItem> values = config_formatter_->from_file(config_file);
+ _parse_config(values);
+ return true;
+ } catch(const FileError &) {
+ if(throw_error) {
+ throw;
+ }
+ return false;
+ }
+ } else if(throw_error) {
+ throw FileError::Missing(config_file);
+ } else {
+ return false;
+ }
+}
+
+CLI11_INLINE void App::_process_config_file() {
+ if(config_ptr_ != nullptr) {
+ bool config_required = config_ptr_->get_required();
+ auto file_given = config_ptr_->count() > 0;
+ if(!(file_given || config_ptr_->envname_.empty())) {
+ std::string ename_string = detail::get_environment_value(config_ptr_->envname_);
+ if(!ename_string.empty()) {
+ config_ptr_->add_result(ename_string);
+ }
+ }
+ config_ptr_->run_callback();
+
+ auto config_files = config_ptr_->as<std::vector<std::string>>();
+ bool files_used{file_given};
+ if(config_files.empty() || config_files.front().empty()) {
+ if(config_required) {
+ throw FileError("config file is required but none was given");
+ }
+ return;
+ }
+ for(const auto &config_file : config_files) {
+ if(_process_config_file(config_file, config_required || file_given)) {
+ files_used = true;
+ }
+ }
+ if(!files_used) {
+ // this is done so the count shows as 0 if no callbacks were processed
+ config_ptr_->clear();
+ bool force = config_ptr_->force_callback_;
+ config_ptr_->force_callback_ = false;
+ config_ptr_->run_callback();
+ config_ptr_->force_callback_ = force;
+ }
+ }
+}
+
+CLI11_INLINE void App::_process_env() {
+ for(const Option_p &opt : options_) {
+ if(opt->count() == 0 && !opt->envname_.empty()) {
+ std::string ename_string = detail::get_environment_value(opt->envname_);
+ if(!ename_string.empty()) {
+ std::string result = ename_string;
+ result = opt->_validate(result, 0);
+ if(result.empty()) {
+ opt->add_result(ename_string);
+ }
+ }
+ }
+ }
+
+ for(App_p &sub : subcommands_) {
+ if(sub->get_name().empty() || !sub->parse_complete_callback_) {
+ if(sub->count_all() > 0) {
+ // only process environment variables if the callback has actually been triggered already
+ sub->_process_env();
+ }
+ }
+ }
+}
+
+CLI11_INLINE void App::_process_callbacks() {
+
+ for(App_p &sub : subcommands_) {
+ // process the priority option_groups first
+ if(sub->get_name().empty() && sub->parse_complete_callback_) {
+ if(sub->count_all() > 0) {
+ sub->_process_callbacks();
+ sub->run_callback();
+ }
+ }
+ }
+
+ for(const Option_p &opt : options_) {
+ if((*opt) && !opt->get_callback_run()) {
+ opt->run_callback();
+ }
+ }
+ for(App_p &sub : subcommands_) {
+ if(!sub->parse_complete_callback_) {
+ sub->_process_callbacks();
+ }
+ }
+}
+
+CLI11_INLINE void App::_process_help_flags(bool trigger_help, bool trigger_all_help) const {
+ const Option *help_ptr = get_help_ptr();
+ const Option *help_all_ptr = get_help_all_ptr();
+
+ if(help_ptr != nullptr && help_ptr->count() > 0)
+ trigger_help = true;
+ if(help_all_ptr != nullptr && help_all_ptr->count() > 0)
+ trigger_all_help = true;
+
+ // If there were parsed subcommands, call those. First subcommand wins if there are multiple ones.
+ if(!parsed_subcommands_.empty()) {
+ for(const App *sub : parsed_subcommands_)
+ sub->_process_help_flags(trigger_help, trigger_all_help);
+
+ // Only the final subcommand should call for help. All help wins over help.
+ } else if(trigger_all_help) {
+ throw CallForAllHelp();
+ } else if(trigger_help) {
+ throw CallForHelp();
+ }
+}
+
+CLI11_INLINE void App::_process_requirements() {
+ // check excludes
+ bool excluded{false};
+ std::string excluder;
+ for(const auto &opt : exclude_options_) {
+ if(opt->count() > 0) {
+ excluded = true;
+ excluder = opt->get_name();
+ }
+ }
+ for(const auto &subc : exclude_subcommands_) {
+ if(subc->count_all() > 0) {
+ excluded = true;
+ excluder = subc->get_display_name();
+ }
+ }
+ if(excluded) {
+ if(count_all() > 0) {
+ throw ExcludesError(get_display_name(), excluder);
+ }
+ // if we are excluded but didn't receive anything, just return
+ return;
+ }
+
+ // check excludes
+ bool missing_needed{false};
+ std::string missing_need;
+ for(const auto &opt : need_options_) {
+ if(opt->count() == 0) {
+ missing_needed = true;
+ missing_need = opt->get_name();
+ }
+ }
+ for(const auto &subc : need_subcommands_) {
+ if(subc->count_all() == 0) {
+ missing_needed = true;
+ missing_need = subc->get_display_name();
+ }
+ }
+ if(missing_needed) {
+ if(count_all() > 0) {
+ throw RequiresError(get_display_name(), missing_need);
+ }
+ // if we missing something but didn't have any options, just return
+ return;
+ }
+
+ std::size_t used_options = 0;
+ for(const Option_p &opt : options_) {
+
+ if(opt->count() != 0) {
+ ++used_options;
+ }
+ // Required but empty
+ if(opt->get_required() && opt->count() == 0) {
+ throw RequiredError(opt->get_name());
+ }
+ // Requires
+ for(const Option *opt_req : opt->needs_)
+ if(opt->count() > 0 && opt_req->count() == 0)
+ throw RequiresError(opt->get_name(), opt_req->get_name());
+ // Excludes
+ for(const Option *opt_ex : opt->excludes_)
+ if(opt->count() > 0 && opt_ex->count() != 0)
+ throw ExcludesError(opt->get_name(), opt_ex->get_name());
+ }
+ // check for the required number of subcommands
+ if(require_subcommand_min_ > 0) {
+ auto selected_subcommands = get_subcommands();
+ if(require_subcommand_min_ > selected_subcommands.size())
+ throw RequiredError::Subcommand(require_subcommand_min_);
+ }
+
+ // Max error cannot occur, the extra subcommand will parse as an ExtrasError or a remaining item.
+
+ // run this loop to check how many unnamed subcommands were actually used since they are considered options
+ // from the perspective of an App
+ for(App_p &sub : subcommands_) {
+ if(sub->disabled_)
+ continue;
+ if(sub->name_.empty() && sub->count_all() > 0) {
+ ++used_options;
+ }
+ }
+
+ if(require_option_min_ > used_options || (require_option_max_ > 0 && require_option_max_ < used_options)) {
+ auto option_list = detail::join(options_, [this](const Option_p &ptr) {
+ if(ptr.get() == help_ptr_ || ptr.get() == help_all_ptr_) {
+ return std::string{};
+ }
+ return ptr->get_name(false, true);
+ });
+
+ auto subc_list = get_subcommands([](App *app) { return ((app->get_name().empty()) && (!app->disabled_)); });
+ if(!subc_list.empty()) {
+ option_list += "," + detail::join(subc_list, [](const App *app) { return app->get_display_name(); });
+ }
+ throw RequiredError::Option(require_option_min_, require_option_max_, used_options, option_list);
+ }
+
+ // now process the requirements for subcommands if needed
+ for(App_p &sub : subcommands_) {
+ if(sub->disabled_)
+ continue;
+ if(sub->name_.empty() && sub->required_ == false) {
+ if(sub->count_all() == 0) {
+ if(require_option_min_ > 0 && require_option_min_ <= used_options) {
+ continue;
+ // if we have met the requirement and there is nothing in this option group skip checking
+ // requirements
+ }
+ if(require_option_max_ > 0 && used_options >= require_option_min_) {
+ continue;
+ // if we have met the requirement and there is nothing in this option group skip checking
+ // requirements
+ }
+ }
+ }
+ if(sub->count() > 0 || sub->name_.empty()) {
+ sub->_process_requirements();
+ }
+
+ if(sub->required_ && sub->count_all() == 0) {
+ throw(CLI::RequiredError(sub->get_display_name()));
+ }
+ }
+}
+
+CLI11_INLINE void App::_process() {
+ try {
+ // the config file might generate a FileError but that should not be processed until later in the process
+ // to allow for help, version and other errors to generate first.
+ _process_config_file();
+
+ // process env shouldn't throw but no reason to process it if config generated an error
+ _process_env();
+ } catch(const CLI::FileError &) {
+ // callbacks and help_flags can generate exceptions which should take priority
+ // over the config file error if one exists.
+ _process_callbacks();
+ _process_help_flags();
+ throw;
+ }
+
+ _process_callbacks();
+ _process_help_flags();
+
+ _process_requirements();
+}
+
+CLI11_INLINE void App::_process_extras() {
+ if(!(allow_extras_ || prefix_command_)) {
+ std::size_t num_left_over = remaining_size();
+ if(num_left_over > 0) {
+ throw ExtrasError(name_, remaining(false));
+ }
+ }
+
+ for(App_p &sub : subcommands_) {
+ if(sub->count() > 0)
+ sub->_process_extras();
+ }
+}
+
+CLI11_INLINE void App::_process_extras(std::vector<std::string> &args) {
+ if(!(allow_extras_ || prefix_command_)) {
+ std::size_t num_left_over = remaining_size();
+ if(num_left_over > 0) {
+ args = remaining(false);
+ throw ExtrasError(name_, args);
+ }
+ }
+
+ for(App_p &sub : subcommands_) {
+ if(sub->count() > 0)
+ sub->_process_extras(args);
+ }
+}
+
+CLI11_INLINE void App::increment_parsed() {
+ ++parsed_;
+ for(App_p &sub : subcommands_) {
+ if(sub->get_name().empty())
+ sub->increment_parsed();
+ }
+}
+
+CLI11_INLINE void App::_parse(std::vector<std::string> &args) {
+ increment_parsed();
+ _trigger_pre_parse(args.size());
+ bool positional_only = false;
+
+ while(!args.empty()) {
+ if(!_parse_single(args, positional_only)) {
+ break;
+ }
+ }
+
+ if(parent_ == nullptr) {
+ _process();
+
+ // Throw error if any items are left over (depending on settings)
+ _process_extras(args);
+
+ // Convert missing (pairs) to extras (string only) ready for processing in another app
+ args = remaining_for_passthrough(false);
+ } else if(parse_complete_callback_) {
+ _process_env();
+ _process_callbacks();
+ _process_help_flags();
+ _process_requirements();
+ run_callback(false, true);
+ }
+}
+
+CLI11_INLINE void App::_parse(std::vector<std::string> &&args) {
+ // this can only be called by the top level in which case parent == nullptr by definition
+ // operation is simplified
+ increment_parsed();
+ _trigger_pre_parse(args.size());
+ bool positional_only = false;
+
+ while(!args.empty()) {
+ _parse_single(args, positional_only);
+ }
+ _process();
+
+ // Throw error if any items are left over (depending on settings)
+ _process_extras();
+}
+
+CLI11_INLINE void App::_parse_stream(std::istream &input) {
+ auto values = config_formatter_->from_config(input);
+ _parse_config(values);
+ increment_parsed();
+ _trigger_pre_parse(values.size());
+ _process();
+
+ // Throw error if any items are left over (depending on settings)
+ _process_extras();
+}
+
+CLI11_INLINE void App::_parse_config(const std::vector<ConfigItem> &args) {
+ for(const ConfigItem &item : args) {
+ if(!_parse_single_config(item) && allow_config_extras_ == config_extras_mode::error)
+ throw ConfigError::Extras(item.fullname());
+ }
+}
+
+CLI11_INLINE bool App::_parse_single_config(const ConfigItem &item, std::size_t level) {
+
+ if(level < item.parents.size()) {
+ try {
+ auto *subcom = get_subcommand(item.parents.at(level));
+ return subcom->_parse_single_config(item, level + 1);
+ } catch(const OptionNotFound &) {
+ return false;
+ }
+ }
+ // check for section open
+ if(item.name == "++") {
+ if(configurable_) {
+ increment_parsed();
+ _trigger_pre_parse(2);
+ if(parent_ != nullptr) {
+ parent_->parsed_subcommands_.push_back(this);
+ }
+ }
+ return true;
+ }
+ // check for section close
+ if(item.name == "--") {
+ if(configurable_ && parse_complete_callback_) {
+ _process_callbacks();
+ _process_requirements();
+ run_callback();
+ }
+ return true;
+ }
+ Option *op = get_option_no_throw("--" + item.name);
+ if(op == nullptr) {
+ if(item.name.size() == 1) {
+ op = get_option_no_throw("-" + item.name);
+ }
+ if(op == nullptr) {
+ op = get_option_no_throw(item.name);
+ }
+ }
+
+ if(op == nullptr) {
+ // If the option was not present
+ if(get_allow_config_extras() == config_extras_mode::capture)
+ // Should we worry about classifying the extras properly?
+ missing_.emplace_back(detail::Classifier::NONE, item.fullname());
+ for(const auto &input : item.inputs) {
+ missing_.emplace_back(detail::Classifier::NONE, input);
+ }
+ return false;
+ }
+
+ if(!op->get_configurable()) {
+ if(get_allow_config_extras() == config_extras_mode::ignore_all) {
+ return false;
+ }
+ throw ConfigError::NotConfigurable(item.fullname());
+ }
+
+ if(op->empty()) {
+
+ if(op->get_expected_min() == 0) {
+ if(item.inputs.size() <= 1) {
+ // Flag parsing
+ auto res = config_formatter_->to_flag(item);
+ bool converted{false};
+ if(op->get_disable_flag_override()) {
+ auto val = detail::to_flag_value(res);
+ if(val == 1) {
+ res = op->get_flag_value(item.name, "{}");
+ converted = true;
+ }
+ }
+
+ if(!converted) {
+ errno = 0;
+ res = op->get_flag_value(item.name, res);
+ }
+
+ op->add_result(res);
+ return true;
+ }
+ if(static_cast<int>(item.inputs.size()) > op->get_items_expected_max() &&
+ op->get_multi_option_policy() != MultiOptionPolicy::TakeAll) {
+ if(op->get_items_expected_max() > 1) {
+ throw ArgumentMismatch::AtMost(item.fullname(), op->get_items_expected_max(), item.inputs.size());
+ }
+
+ if(!op->get_disable_flag_override()) {
+ throw ConversionError::TooManyInputsFlag(item.fullname());
+ }
+ // if the disable flag override is set then we must have the flag values match a known flag value
+ // this is true regardless of the output value, so an array input is possible and must be accounted for
+ for(const auto &res : item.inputs) {
+ bool valid_value{false};
+ if(op->default_flag_values_.empty()) {
+ if(res == "true" || res == "false" || res == "1" || res == "0") {
+ valid_value = true;
+ }
+ } else {
+ for(const auto &valid_res : op->default_flag_values_) {
+ if(valid_res.second == res) {
+ valid_value = true;
+ break;
+ }
+ }
+ }
+
+ if(valid_value) {
+ op->add_result(res);
+ } else {
+ throw InvalidError("invalid flag argument given");
+ }
+ }
+ return true;
+ }
+ }
+ op->add_result(item.inputs);
+ op->run_callback();
+ }
+
+ return true;
+}
+
+CLI11_INLINE bool App::_parse_single(std::vector<std::string> &args, bool &positional_only) {
+ bool retval = true;
+ detail::Classifier classifier = positional_only ? detail::Classifier::NONE : _recognize(args.back());
+ switch(classifier) {
+ case detail::Classifier::POSITIONAL_MARK:
+ args.pop_back();
+ positional_only = true;
+ if((!_has_remaining_positionals()) && (parent_ != nullptr)) {
+ retval = false;
+ } else {
+ _move_to_missing(classifier, "--");
+ }
+ break;
+ case detail::Classifier::SUBCOMMAND_TERMINATOR:
+ // treat this like a positional mark if in the parent app
+ args.pop_back();
+ retval = false;
+ break;
+ case detail::Classifier::SUBCOMMAND:
+ retval = _parse_subcommand(args);
+ break;
+ case detail::Classifier::LONG:
+ case detail::Classifier::SHORT:
+ case detail::Classifier::WINDOWS_STYLE:
+ // If already parsed a subcommand, don't accept options_
+ retval = _parse_arg(args, classifier, false);
+ break;
+ case detail::Classifier::NONE:
+ // Probably a positional or something for a parent (sub)command
+ retval = _parse_positional(args, false);
+ if(retval && positionals_at_end_) {
+ positional_only = true;
+ }
+ break;
+ // LCOV_EXCL_START
+ default:
+ throw HorribleError("unrecognized classifier (you should not see this!)");
+ // LCOV_EXCL_STOP
+ }
+ return retval;
+}
+
+CLI11_NODISCARD CLI11_INLINE std::size_t App::_count_remaining_positionals(bool required_only) const {
+ std::size_t retval = 0;
+ for(const Option_p &opt : options_) {
+ if(opt->get_positional() && (!required_only || opt->get_required())) {
+ if(opt->get_items_expected_min() > 0 && static_cast<int>(opt->count()) < opt->get_items_expected_min()) {
+ retval += static_cast<std::size_t>(opt->get_items_expected_min()) - opt->count();
+ }
+ }
+ }
+ return retval;
+}
+
+CLI11_NODISCARD CLI11_INLINE bool App::_has_remaining_positionals() const {
+ for(const Option_p &opt : options_) {
+ if(opt->get_positional() && ((static_cast<int>(opt->count()) < opt->get_items_expected_min()))) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+CLI11_INLINE bool App::_parse_positional(std::vector<std::string> &args, bool haltOnSubcommand) {
+
+ const std::string &positional = args.back();
+ Option *posOpt{nullptr};
+
+ if(positionals_at_end_) {
+ // deal with the case of required arguments at the end which should take precedence over other arguments
+ auto arg_rem = args.size();
+ auto remreq = _count_remaining_positionals(true);
+ if(arg_rem <= remreq) {
+ for(const Option_p &opt : options_) {
+ if(opt->get_positional() && opt->required_) {
+ if(static_cast<int>(opt->count()) < opt->get_items_expected_min()) {
+ if(validate_positionals_) {
+ std::string pos = positional;
+ pos = opt->_validate(pos, 0);
+ if(!pos.empty()) {
+ continue;
+ }
+ }
+ posOpt = opt.get();
+ break;
+ }
+ }
+ }
+ }
+ }
+ if(posOpt == nullptr) {
+ for(const Option_p &opt : options_) {
+ // Eat options, one by one, until done
+ if(opt->get_positional() &&
+ (static_cast<int>(opt->count()) < opt->get_items_expected_min() || opt->get_allow_extra_args())) {
+ if(validate_positionals_) {
+ std::string pos = positional;
+ pos = opt->_validate(pos, 0);
+ if(!pos.empty()) {
+ continue;
+ }
+ }
+ posOpt = opt.get();
+ break;
+ }
+ }
+ }
+ if(posOpt != nullptr) {
+ parse_order_.push_back(posOpt);
+ if(posOpt->get_inject_separator()) {
+ if(!posOpt->results().empty() && !posOpt->results().back().empty()) {
+ posOpt->add_result(std::string{});
+ }
+ }
+ if(posOpt->get_trigger_on_parse() && posOpt->current_option_state_ == Option::option_state::callback_run) {
+ posOpt->clear();
+ }
+ posOpt->add_result(positional);
+ if(posOpt->get_trigger_on_parse()) {
+ posOpt->run_callback();
+ }
+
+ args.pop_back();
+ return true;
+ }
+
+ for(auto &subc : subcommands_) {
+ if((subc->name_.empty()) && (!subc->disabled_)) {
+ if(subc->_parse_positional(args, false)) {
+ if(!subc->pre_parse_called_) {
+ subc->_trigger_pre_parse(args.size());
+ }
+ return true;
+ }
+ }
+ }
+ // let the parent deal with it if possible
+ if(parent_ != nullptr && fallthrough_)
+ return _get_fallthrough_parent()->_parse_positional(args, static_cast<bool>(parse_complete_callback_));
+
+ /// Try to find a local subcommand that is repeated
+ auto *com = _find_subcommand(args.back(), true, false);
+ if(com != nullptr && (require_subcommand_max_ == 0 || require_subcommand_max_ > parsed_subcommands_.size())) {
+ if(haltOnSubcommand) {
+ return false;
+ }
+ args.pop_back();
+ com->_parse(args);
+ return true;
+ }
+ /// now try one last gasp at subcommands that have been executed before, go to root app and try to find a
+ /// subcommand in a broader way, if one exists let the parent deal with it
+ auto *parent_app = (parent_ != nullptr) ? _get_fallthrough_parent() : this;
+ com = parent_app->_find_subcommand(args.back(), true, false);
+ if(com != nullptr && (com->parent_->require_subcommand_max_ == 0 ||
+ com->parent_->require_subcommand_max_ > com->parent_->parsed_subcommands_.size())) {
+ return false;
+ }
+
+ if(positionals_at_end_) {
+ throw CLI::ExtrasError(name_, args);
+ }
+ /// If this is an option group don't deal with it
+ if(parent_ != nullptr && name_.empty()) {
+ return false;
+ }
+ /// We are out of other options this goes to missing
+ _move_to_missing(detail::Classifier::NONE, positional);
+ args.pop_back();
+ if(prefix_command_) {
+ while(!args.empty()) {
+ _move_to_missing(detail::Classifier::NONE, args.back());
+ args.pop_back();
+ }
+ }
+
+ return true;
+}
+
+CLI11_NODISCARD CLI11_INLINE App *
+App::_find_subcommand(const std::string &subc_name, bool ignore_disabled, bool ignore_used) const noexcept {
+ for(const App_p &com : subcommands_) {
+ if(com->disabled_ && ignore_disabled)
+ continue;
+ if(com->get_name().empty()) {
+ auto *subc = com->_find_subcommand(subc_name, ignore_disabled, ignore_used);
+ if(subc != nullptr) {
+ return subc;
+ }
+ }
+ if(com->check_name(subc_name)) {
+ if((!*com) || !ignore_used)
+ return com.get();
+ }
+ }
+ return nullptr;
+}
+
+CLI11_INLINE bool App::_parse_subcommand(std::vector<std::string> &args) {
+ if(_count_remaining_positionals(/* required */ true) > 0) {
+ _parse_positional(args, false);
+ return true;
+ }
+ auto *com = _find_subcommand(args.back(), true, true);
+ if(com == nullptr) {
+ // the main way to get here is using .notation
+ auto dotloc = args.back().find_first_of('.');
+ if(dotloc != std::string::npos) {
+ com = _find_subcommand(args.back().substr(0, dotloc), true, true);
+ if(com != nullptr) {
+ args.back() = args.back().substr(dotloc + 1);
+ args.push_back(com->get_display_name());
+ }
+ }
+ }
+ if(com != nullptr) {
+ args.pop_back();
+ if(!com->silent_) {
+ parsed_subcommands_.push_back(com);
+ }
+ com->_parse(args);
+ auto *parent_app = com->parent_;
+ while(parent_app != this) {
+ parent_app->_trigger_pre_parse(args.size());
+ if(!com->silent_) {
+ parent_app->parsed_subcommands_.push_back(com);
+ }
+ parent_app = parent_app->parent_;
+ }
+ return true;
+ }
+
+ if(parent_ == nullptr)
+ throw HorribleError("Subcommand " + args.back() + " missing");
+ return false;
+}
+
+CLI11_INLINE bool
+App::_parse_arg(std::vector<std::string> &args, detail::Classifier current_type, bool local_processing_only) {
+
+ std::string current = args.back();
+
+ std::string arg_name;
+ std::string value;
+ std::string rest;
+
+ switch(current_type) {
+ case detail::Classifier::LONG:
+ if(!detail::split_long(current, arg_name, value))
+ throw HorribleError("Long parsed but missing (you should not see this):" + args.back());
+ break;
+ case detail::Classifier::SHORT:
+ if(!detail::split_short(current, arg_name, rest))
+ throw HorribleError("Short parsed but missing! You should not see this");
+ break;
+ case detail::Classifier::WINDOWS_STYLE:
+ if(!detail::split_windows_style(current, arg_name, value))
+ throw HorribleError("windows option parsed but missing! You should not see this");
+ break;
+ case detail::Classifier::SUBCOMMAND:
+ case detail::Classifier::SUBCOMMAND_TERMINATOR:
+ case detail::Classifier::POSITIONAL_MARK:
+ case detail::Classifier::NONE:
+ default:
+ throw HorribleError("parsing got called with invalid option! You should not see this");
+ }
+
+ auto op_ptr = std::find_if(std::begin(options_), std::end(options_), [arg_name, current_type](const Option_p &opt) {
+ if(current_type == detail::Classifier::LONG)
+ return opt->check_lname(arg_name);
+ if(current_type == detail::Classifier::SHORT)
+ return opt->check_sname(arg_name);
+ // this will only get called for detail::Classifier::WINDOWS_STYLE
+ return opt->check_lname(arg_name) || opt->check_sname(arg_name);
+ });
+
+ // Option not found
+ if(op_ptr == std::end(options_)) {
+ for(auto &subc : subcommands_) {
+ if(subc->name_.empty() && !subc->disabled_) {
+ if(subc->_parse_arg(args, current_type, local_processing_only)) {
+ if(!subc->pre_parse_called_) {
+ subc->_trigger_pre_parse(args.size());
+ }
+ return true;
+ }
+ }
+ }
+
+ // don't capture missing if this is a nameless subcommand and nameless subcommands can't fallthrough
+ if(parent_ != nullptr && name_.empty()) {
+ return false;
+ }
+
+ // now check for '.' notation of subcommands
+ auto dotloc = arg_name.find_first_of('.', 1);
+ if(dotloc != std::string::npos) {
+ // using dot notation is equivalent to single argument subcommand
+ auto *sub = _find_subcommand(arg_name.substr(0, dotloc), true, false);
+ if(sub != nullptr) {
+ auto v = args.back();
+ args.pop_back();
+ arg_name = arg_name.substr(dotloc + 1);
+ if(arg_name.size() > 1) {
+ args.push_back(std::string("--") + v.substr(dotloc + 3));
+ current_type = detail::Classifier::LONG;
+ } else {
+ auto nval = v.substr(dotloc + 2);
+ nval.front() = '-';
+ if(nval.size() > 2) {
+ // '=' not allowed in short form arguments
+ args.push_back(nval.substr(3));
+ nval.resize(2);
+ }
+ args.push_back(nval);
+ current_type = detail::Classifier::SHORT;
+ }
+ auto val = sub->_parse_arg(args, current_type, true);
+ if(val) {
+ if(!sub->silent_) {
+ parsed_subcommands_.push_back(sub);
+ }
+ // deal with preparsing
+ increment_parsed();
+ _trigger_pre_parse(args.size());
+ // run the parse complete callback since the subcommand processing is now complete
+ if(sub->parse_complete_callback_) {
+ sub->_process_env();
+ sub->_process_callbacks();
+ sub->_process_help_flags();
+ sub->_process_requirements();
+ sub->run_callback(false, true);
+ }
+ return true;
+ }
+ args.pop_back();
+ args.push_back(v);
+ }
+ }
+ if(local_processing_only) {
+ return false;
+ }
+ // If a subcommand, try the main command
+ if(parent_ != nullptr && fallthrough_)
+ return _get_fallthrough_parent()->_parse_arg(args, current_type, false);
+
+ // Otherwise, add to missing
+ args.pop_back();
+ _move_to_missing(current_type, current);
+ return true;
+ }
+
+ args.pop_back();
+
+ // Get a reference to the pointer to make syntax bearable
+ Option_p &op = *op_ptr;
+ /// if we require a separator add it here
+ if(op->get_inject_separator()) {
+ if(!op->results().empty() && !op->results().back().empty()) {
+ op->add_result(std::string{});
+ }
+ }
+ if(op->get_trigger_on_parse() && op->current_option_state_ == Option::option_state::callback_run) {
+ op->clear();
+ }
+ int min_num = (std::min)(op->get_type_size_min(), op->get_items_expected_min());
+ int max_num = op->get_items_expected_max();
+ // check container like options to limit the argument size to a single type if the allow_extra_flags argument is
+ // set. 16 is somewhat arbitrary (needs to be at least 4)
+ if(max_num >= detail::expected_max_vector_size / 16 && !op->get_allow_extra_args()) {
+ auto tmax = op->get_type_size_max();
+ max_num = detail::checked_multiply(tmax, op->get_expected_min()) ? tmax : detail::expected_max_vector_size;
+ }
+ // Make sure we always eat the minimum for unlimited vectors
+ int collected = 0; // total number of arguments collected
+ int result_count = 0; // local variable for number of results in a single arg string
+ // deal with purely flag like things
+ if(max_num == 0) {
+ auto res = op->get_flag_value(arg_name, value);
+ op->add_result(res);
+ parse_order_.push_back(op.get());
+ } else if(!value.empty()) { // --this=value
+ op->add_result(value, result_count);
+ parse_order_.push_back(op.get());
+ collected += result_count;
+ // -Trest
+ } else if(!rest.empty()) {
+ op->add_result(rest, result_count);
+ parse_order_.push_back(op.get());
+ rest = "";
+ collected += result_count;
+ }
+
+ // gather the minimum number of arguments
+ while(min_num > collected && !args.empty()) {
+ std::string current_ = args.back();
+ args.pop_back();
+ op->add_result(current_, result_count);
+ parse_order_.push_back(op.get());
+ collected += result_count;
+ }
+
+ if(min_num > collected) { // if we have run out of arguments and the minimum was not met
+ throw ArgumentMismatch::TypedAtLeast(op->get_name(), min_num, op->get_type_name());
+ }
+
+ // now check for optional arguments
+ if(max_num > collected || op->get_allow_extra_args()) { // we allow optional arguments
+ auto remreqpos = _count_remaining_positionals(true);
+ // we have met the minimum now optionally check up to the maximum
+ while((collected < max_num || op->get_allow_extra_args()) && !args.empty() &&
+ _recognize(args.back(), false) == detail::Classifier::NONE) {
+ // If any required positionals remain, don't keep eating
+ if(remreqpos >= args.size()) {
+ break;
+ }
+ if(validate_optional_arguments_) {
+ std::string arg = args.back();
+ arg = op->_validate(arg, 0);
+ if(!arg.empty()) {
+ break;
+ }
+ }
+ op->add_result(args.back(), result_count);
+ parse_order_.push_back(op.get());
+ args.pop_back();
+ collected += result_count;
+ }
+
+ // Allow -- to end an unlimited list and "eat" it
+ if(!args.empty() && _recognize(args.back()) == detail::Classifier::POSITIONAL_MARK)
+ args.pop_back();
+ // optional flag that didn't receive anything now get the default value
+ if(min_num == 0 && max_num > 0 && collected == 0) {
+ auto res = op->get_flag_value(arg_name, std::string{});
+ op->add_result(res);
+ parse_order_.push_back(op.get());
+ }
+ }
+ // if we only partially completed a type then add an empty string if allowed for later processing
+ if(min_num > 0 && (collected % op->get_type_size_max()) != 0) {
+ if(op->get_type_size_max() != op->get_type_size_min()) {
+ op->add_result(std::string{});
+ } else {
+ throw ArgumentMismatch::PartialType(op->get_name(), op->get_type_size_min(), op->get_type_name());
+ }
+ }
+ if(op->get_trigger_on_parse()) {
+ op->run_callback();
+ }
+ if(!rest.empty()) {
+ rest = "-" + rest;
+ args.push_back(rest);
+ }
+ return true;
+}
+
+CLI11_INLINE void App::_trigger_pre_parse(std::size_t remaining_args) {
+ if(!pre_parse_called_) {
+ pre_parse_called_ = true;
+ if(pre_parse_callback_) {
+ pre_parse_callback_(remaining_args);
+ }
+ } else if(immediate_callback_) {
+ if(!name_.empty()) {
+ auto pcnt = parsed_;
+ missing_t extras = std::move(missing_);
+ clear();
+ parsed_ = pcnt;
+ pre_parse_called_ = true;
+ missing_ = std::move(extras);
+ }
+ }
+}
+
+CLI11_INLINE App *App::_get_fallthrough_parent() {
+ if(parent_ == nullptr) {
+ throw(HorribleError("No Valid parent"));
+ }
+ auto *fallthrough_parent = parent_;
+ while((fallthrough_parent->parent_ != nullptr) && (fallthrough_parent->get_name().empty())) {
+ fallthrough_parent = fallthrough_parent->parent_;
+ }
+ return fallthrough_parent;
+}
+
+CLI11_NODISCARD CLI11_INLINE const std::string &App::_compare_subcommand_names(const App &subcom,
+ const App &base) const {
+ static const std::string estring;
+ if(subcom.disabled_) {
+ return estring;
+ }
+ for(const auto &subc : base.subcommands_) {
+ if(subc.get() != &subcom) {
+ if(subc->disabled_) {
+ continue;
+ }
+ if(!subcom.get_name().empty()) {
+ if(subc->check_name(subcom.get_name())) {
+ return subcom.get_name();
+ }
+ }
+ if(!subc->get_name().empty()) {
+ if(subcom.check_name(subc->get_name())) {
+ return subc->get_name();
+ }
+ }
+ for(const auto &les : subcom.aliases_) {
+ if(subc->check_name(les)) {
+ return les;
+ }
+ }
+ // this loop is needed in case of ignore_underscore or ignore_case on one but not the other
+ for(const auto &les : subc->aliases_) {
+ if(subcom.check_name(les)) {
+ return les;
+ }
+ }
+ // if the subcommand is an option group we need to check deeper
+ if(subc->get_name().empty()) {
+ const auto &cmpres = _compare_subcommand_names(subcom, *subc);
+ if(!cmpres.empty()) {
+ return cmpres;
+ }
+ }
+ // if the test subcommand is an option group we need to check deeper
+ if(subcom.get_name().empty()) {
+ const auto &cmpres = _compare_subcommand_names(*subc, subcom);
+ if(!cmpres.empty()) {
+ return cmpres;
+ }
+ }
+ }
+ }
+ return estring;
+}
+
+CLI11_INLINE void App::_move_to_missing(detail::Classifier val_type, const std::string &val) {
+ if(allow_extras_ || subcommands_.empty()) {
+ missing_.emplace_back(val_type, val);
+ return;
+ }
+ // allow extra arguments to be places in an option group if it is allowed there
+ for(auto &subc : subcommands_) {
+ if(subc->name_.empty() && subc->allow_extras_) {
+ subc->missing_.emplace_back(val_type, val);
+ return;
+ }
+ }
+ // if we haven't found any place to put them yet put them in missing
+ missing_.emplace_back(val_type, val);
+}
+
+CLI11_INLINE void App::_move_option(Option *opt, App *app) {
+ if(opt == nullptr) {
+ throw OptionNotFound("the option is NULL");
+ }
+ // verify that the give app is actually a subcommand
+ bool found = false;
+ for(auto &subc : subcommands_) {
+ if(app == subc.get()) {
+ found = true;
+ }
+ }
+ if(!found) {
+ throw OptionNotFound("The Given app is not a subcommand");
+ }
+
+ if((help_ptr_ == opt) || (help_all_ptr_ == opt))
+ throw OptionAlreadyAdded("cannot move help options");
+
+ if(config_ptr_ == opt)
+ throw OptionAlreadyAdded("cannot move config file options");
+
+ auto iterator =
+ std::find_if(std::begin(options_), std::end(options_), [opt](const Option_p &v) { return v.get() == opt; });
+ if(iterator != std::end(options_)) {
+ const auto &opt_p = *iterator;
+ if(std::find_if(std::begin(app->options_), std::end(app->options_), [&opt_p](const Option_p &v) {
+ return (*v == *opt_p);
+ }) == std::end(app->options_)) {
+ // only erase after the insertion was successful
+ app->options_.push_back(std::move(*iterator));
+ options_.erase(iterator);
+ } else {
+ throw OptionAlreadyAdded("option was not located: " + opt->get_name());
+ }
+ } else {
+ throw OptionNotFound("could not locate the given Option");
+ }
+}
+
+CLI11_INLINE void TriggerOn(App *trigger_app, App *app_to_enable) {
+ app_to_enable->enabled_by_default(false);
+ app_to_enable->disabled_by_default();
+ trigger_app->preparse_callback([app_to_enable](std::size_t) { app_to_enable->disabled(false); });
+}
+
+CLI11_INLINE void TriggerOn(App *trigger_app, std::vector<App *> apps_to_enable) {
+ for(auto &app : apps_to_enable) {
+ app->enabled_by_default(false);
+ app->disabled_by_default();
+ }
+
+ trigger_app->preparse_callback([apps_to_enable](std::size_t) {
+ for(const auto &app : apps_to_enable) {
+ app->disabled(false);
+ }
+ });
+}
+
+CLI11_INLINE void TriggerOff(App *trigger_app, App *app_to_enable) {
+ app_to_enable->disabled_by_default(false);
+ app_to_enable->enabled_by_default();
+ trigger_app->preparse_callback([app_to_enable](std::size_t) { app_to_enable->disabled(); });
+}
+
+CLI11_INLINE void TriggerOff(App *trigger_app, std::vector<App *> apps_to_enable) {
+ for(auto &app : apps_to_enable) {
+ app->disabled_by_default(false);
+ app->enabled_by_default();
+ }
+
+ trigger_app->preparse_callback([apps_to_enable](std::size_t) {
+ for(const auto &app : apps_to_enable) {
+ app->disabled();
+ }
+ });
+}
+
+CLI11_INLINE void deprecate_option(Option *opt, const std::string &replacement) {
+ Validator deprecate_warning{[opt, replacement](std::string &) {
+ std::cout << opt->get_name() << " is deprecated please use '" << replacement
+ << "' instead\n";
+ return std::string();
+ },
+ "DEPRECATED"};
+ deprecate_warning.application_index(0);
+ opt->check(deprecate_warning);
+ if(!replacement.empty()) {
+ opt->description(opt->get_description() + " DEPRECATED: please use '" + replacement + "' instead");
+ }
+}
+
+CLI11_INLINE void retire_option(App *app, Option *opt) {
+ App temp;
+ auto *option_copy = temp.add_option(opt->get_name(false, true))
+ ->type_size(opt->get_type_size_min(), opt->get_type_size_max())
+ ->expected(opt->get_expected_min(), opt->get_expected_max())
+ ->allow_extra_args(opt->get_allow_extra_args());
+
+ app->remove_option(opt);
+ auto *opt2 = app->add_option(option_copy->get_name(false, true), "option has been retired and has no effect");
+ opt2->type_name("RETIRED")
+ ->default_str("RETIRED")
+ ->type_size(option_copy->get_type_size_min(), option_copy->get_type_size_max())
+ ->expected(option_copy->get_expected_min(), option_copy->get_expected_max())
+ ->allow_extra_args(option_copy->get_allow_extra_args());
+
+ Validator retired_warning{[opt2](std::string &) {
+ std::cout << "WARNING " << opt2->get_name() << " is retired and has no effect\n";
+ return std::string();
+ },
+ ""};
+ retired_warning.application_index(0);
+ opt2->check(retired_warning);
+}
+
+CLI11_INLINE void retire_option(App &app, Option *opt) { retire_option(&app, opt); }
+
+CLI11_INLINE void retire_option(App *app, const std::string &option_name) {
+
+ auto *opt = app->get_option_no_throw(option_name);
+ if(opt != nullptr) {
+ retire_option(app, opt);
+ return;
+ }
+ auto *opt2 = app->add_option(option_name, "option has been retired and has no effect")
+ ->type_name("RETIRED")
+ ->expected(0, 1)
+ ->default_str("RETIRED");
+ Validator retired_warning{[opt2](std::string &) {
+ std::cout << "WARNING " << opt2->get_name() << " is retired and has no effect\n";
+ return std::string();
+ },
+ ""};
+ retired_warning.application_index(0);
+ opt2->check(retired_warning);
+}
+
+CLI11_INLINE void retire_option(App &app, const std::string &option_name) { retire_option(&app, option_name); }
+
+namespace FailureMessage {
+
+CLI11_INLINE std::string simple(const App *app, const Error &e) {
+ std::string header = std::string(e.what()) + "\n";
+ std::vector<std::string> names;
+
+ // Collect names
+ if(app->get_help_ptr() != nullptr)
+ names.push_back(app->get_help_ptr()->get_name());
+
+ if(app->get_help_all_ptr() != nullptr)
+ names.push_back(app->get_help_all_ptr()->get_name());
+
+ // If any names found, suggest those
+ if(!names.empty())
+ header += "Run with " + detail::join(names, " or ") + " for more information.\n";
+
+ return header;
+}
+
+CLI11_INLINE std::string help(const App *app, const Error &e) {
+ std::string header = std::string("ERROR: ") + e.get_name() + ": " + e.what() + "\n";
+ header += app->help();
+ return header;
+}
+
+} // namespace FailureMessage
+
+
+
+
+namespace detail {
+
+std::string convert_arg_for_ini(const std::string &arg,
+ char stringQuote = '"',
+ char literalQuote = '\'',
+ bool disable_multi_line = false);
+
+/// Comma separated join, adds quotes if needed
+std::string ini_join(const std::vector<std::string> &args,
+ char sepChar = ',',
+ char arrayStart = '[',
+ char arrayEnd = ']',
+ char stringQuote = '"',
+ char literalQuote = '\'');
+
+void clean_name_string(std::string &name, const std::string &keyChars);
+
+std::vector<std::string> generate_parents(const std::string §ion, std::string &name, char parentSeparator);
+
+/// assuming non default segments do a check on the close and open of the segments in a configItem structure
+void checkParentSegments(std::vector<ConfigItem> &output, const std::string ¤tSection, char parentSeparator);
+} // namespace detail
+
+
+
+
+static constexpr auto multiline_literal_quote = R"(''')";
+static constexpr auto multiline_string_quote = R"(""")";
+
+namespace detail {
+
+CLI11_INLINE bool is_printable(const std::string &test_string) {
+ return std::all_of(test_string.begin(), test_string.end(), [](char x) {
+ return (isprint(static_cast<unsigned char>(x)) != 0 || x == '\n' || x == '\t');
+ });
+}
+
+CLI11_INLINE std::string
+convert_arg_for_ini(const std::string &arg, char stringQuote, char literalQuote, bool disable_multi_line) {
+ if(arg.empty()) {
+ return std::string(2, stringQuote);
+ }
+ // some specifically supported strings
+ if(arg == "true" || arg == "false" || arg == "nan" || arg == "inf") {
+ return arg;
+ }
+ // floating point conversion can convert some hex codes, but don't try that here
+ if(arg.compare(0, 2, "0x") != 0 && arg.compare(0, 2, "0X") != 0) {
+ using CLI::detail::lexical_cast;
+ double val = 0.0;
+ if(lexical_cast(arg, val)) {
+ if(arg.find_first_not_of("0123456789.-+eE") == std::string::npos) {
+ return arg;
+ }
+ }
+ }
+ // just quote a single non numeric character
+ if(arg.size() == 1) {
+ if(isprint(static_cast<unsigned char>(arg.front())) == 0) {
+ return binary_escape_string(arg);
+ }
+ if(arg == "'") {
+ return std::string(1, stringQuote) + "'" + stringQuote;
+ }
+ return std::string(1, literalQuote) + arg + literalQuote;
+ }
+ // handle hex, binary or octal arguments
+ if(arg.front() == '0') {
+ if(arg[1] == 'x') {
+ if(std::all_of(arg.begin() + 2, arg.end(), [](char x) {
+ return (x >= '0' && x <= '9') || (x >= 'A' && x <= 'F') || (x >= 'a' && x <= 'f');
+ })) {
+ return arg;
+ }
+ } else if(arg[1] == 'o') {
+ if(std::all_of(arg.begin() + 2, arg.end(), [](char x) { return (x >= '0' && x <= '7'); })) {
+ return arg;
+ }
+ } else if(arg[1] == 'b') {
+ if(std::all_of(arg.begin() + 2, arg.end(), [](char x) { return (x == '0' || x == '1'); })) {
+ return arg;
+ }
+ }
+ }
+ if(!is_printable(arg)) {
+ return binary_escape_string(arg);
+ }
+ if(detail::has_escapable_character(arg)) {
+ if(arg.size() > 100 && !disable_multi_line) {
+ return std::string(multiline_literal_quote) + arg + multiline_literal_quote;
+ }
+ return std::string(1, stringQuote) + detail::add_escaped_characters(arg) + stringQuote;
+ }
+ return std::string(1, stringQuote) + arg + stringQuote;
+}
+
+CLI11_INLINE std::string ini_join(const std::vector<std::string> &args,
+ char sepChar,
+ char arrayStart,
+ char arrayEnd,
+ char stringQuote,
+ char literalQuote) {
+ bool disable_multi_line{false};
+ std::string joined;
+ if(args.size() > 1 && arrayStart != '\0') {
+ joined.push_back(arrayStart);
+ disable_multi_line = true;
+ }
+ std::size_t start = 0;
+ for(const auto &arg : args) {
+ if(start++ > 0) {
+ joined.push_back(sepChar);
+ if(!std::isspace<char>(sepChar, std::locale())) {
+ joined.push_back(' ');
+ }
+ }
+ joined.append(convert_arg_for_ini(arg, stringQuote, literalQuote, disable_multi_line));
+ }
+ if(args.size() > 1 && arrayEnd != '\0') {
+ joined.push_back(arrayEnd);
+ }
+ return joined;
+}
+
+CLI11_INLINE std::vector<std::string>
+generate_parents(const std::string §ion, std::string &name, char parentSeparator) {
+ std::vector<std::string> parents;
+ if(detail::to_lower(section) != "default") {
+ if(section.find(parentSeparator) != std::string::npos) {
+ parents = detail::split_up(section, parentSeparator);
+ } else {
+ parents = {section};
+ }
+ }
+ if(name.find(parentSeparator) != std::string::npos) {
+ std::vector<std::string> plist = detail::split_up(name, parentSeparator);
+ name = plist.back();
+ plist.pop_back();
+ parents.insert(parents.end(), plist.begin(), plist.end());
+ }
+ // clean up quotes on the parents
+ try {
+ detail::remove_quotes(parents);
+ } catch(const std::invalid_argument &iarg) {
+ throw CLI::ParseError(iarg.what(), CLI::ExitCodes::InvalidError);
+ }
+ return parents;
+}
+
+CLI11_INLINE void
+checkParentSegments(std::vector<ConfigItem> &output, const std::string ¤tSection, char parentSeparator) {
+
+ std::string estring;
+ auto parents = detail::generate_parents(currentSection, estring, parentSeparator);
+ if(!output.empty() && output.back().name == "--") {
+ std::size_t msize = (parents.size() > 1U) ? parents.size() : 2;
+ while(output.back().parents.size() >= msize) {
+ output.push_back(output.back());
+ output.back().parents.pop_back();
+ }
+
+ if(parents.size() > 1) {
+ std::size_t common = 0;
+ std::size_t mpair = (std::min)(output.back().parents.size(), parents.size() - 1);
+ for(std::size_t ii = 0; ii < mpair; ++ii) {
+ if(output.back().parents[ii] != parents[ii]) {
+ break;
+ }
+ ++common;
+ }
+ if(common == mpair) {
+ output.pop_back();
+ } else {
+ while(output.back().parents.size() > common + 1) {
+ output.push_back(output.back());
+ output.back().parents.pop_back();
+ }
+ }
+ for(std::size_t ii = common; ii < parents.size() - 1; ++ii) {
+ output.emplace_back();
+ output.back().parents.assign(parents.begin(), parents.begin() + static_cast<std::ptrdiff_t>(ii) + 1);
+ output.back().name = "++";
+ }
+ }
+ } else if(parents.size() > 1) {
+ for(std::size_t ii = 0; ii < parents.size() - 1; ++ii) {
+ output.emplace_back();
+ output.back().parents.assign(parents.begin(), parents.begin() + static_cast<std::ptrdiff_t>(ii) + 1);
+ output.back().name = "++";
+ }
+ }
+
+ // insert a section end which is just an empty items_buffer
+ output.emplace_back();
+ output.back().parents = std::move(parents);
+ output.back().name = "++";
+}
+
+/// @brief checks if a string represents a multiline comment
+CLI11_INLINE bool hasMLString(std::string const &fullString, char check) {
+ if(fullString.length() < 3) {
+ return false;
+ }
+ auto it = fullString.rbegin();
+ return (*it == check) && (*(it + 1) == check) && (*(it + 2) == check);
+}
+} // namespace detail
+
+inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) const {
+ std::string line;
+ std::string buffer;
+ std::string currentSection = "default";
+ std::string previousSection = "default";
+ std::vector<ConfigItem> output;
+ bool isDefaultArray = (arrayStart == '[' && arrayEnd == ']' && arraySeparator == ',');
+ bool isINIArray = (arrayStart == '\0' || arrayStart == ' ') && arrayStart == arrayEnd;
+ bool inSection{false};
+ bool inMLineComment{false};
+ bool inMLineValue{false};
+
+ char aStart = (isINIArray) ? '[' : arrayStart;
+ char aEnd = (isINIArray) ? ']' : arrayEnd;
+ char aSep = (isINIArray && arraySeparator == ' ') ? ',' : arraySeparator;
+ int currentSectionIndex{0};
+
+ std::string line_sep_chars{parentSeparatorChar, commentChar, valueDelimiter};
+ while(getline(input, buffer)) {
+ std::vector<std::string> items_buffer;
+ std::string name;
+ line = detail::trim_copy(buffer);
+ std::size_t len = line.length();
+ // lines have to be at least 3 characters to have any meaning to CLI just skip the rest
+ if(len < 3) {
+ continue;
+ }
+ if(line.compare(0, 3, multiline_string_quote) == 0 || line.compare(0, 3, multiline_literal_quote) == 0) {
+ inMLineComment = true;
+ auto cchar = line.front();
+ while(inMLineComment) {
+ if(getline(input, line)) {
+ detail::trim(line);
+ } else {
+ break;
+ }
+ if(detail::hasMLString(line, cchar)) {
+ inMLineComment = false;
+ }
+ }
+ continue;
+ }
+ if(line.front() == '[' && line.back() == ']') {
+ if(currentSection != "default") {
+ // insert a section end which is just an empty items_buffer
+ output.emplace_back();
+ output.back().parents = detail::generate_parents(currentSection, name, parentSeparatorChar);
+ output.back().name = "--";
+ }
+ currentSection = line.substr(1, len - 2);
+ // deal with double brackets for TOML
+ if(currentSection.size() > 1 && currentSection.front() == '[' && currentSection.back() == ']') {
+ currentSection = currentSection.substr(1, currentSection.size() - 2);
+ }
+ if(detail::to_lower(currentSection) == "default") {
+ currentSection = "default";
+ } else {
+ detail::checkParentSegments(output, currentSection, parentSeparatorChar);
+ }
+ inSection = false;
+ if(currentSection == previousSection) {
+ ++currentSectionIndex;
+ } else {
+ currentSectionIndex = 0;
+ previousSection = currentSection;
+ }
+ continue;
+ }
+
+ // comment lines
+ if(line.front() == ';' || line.front() == '#' || line.front() == commentChar) {
+ continue;
+ }
+ std::size_t search_start = 0;
+ if(line.find_first_of("\"'`") != std::string::npos) {
+ while(search_start < line.size()) {
+ auto test_char = line[search_start];
+ if(test_char == '\"' || test_char == '\'' || test_char == '`') {
+ search_start = detail::close_sequence(line, search_start, line[search_start]);
+ ++search_start;
+ } else if(test_char == valueDelimiter || test_char == commentChar) {
+ --search_start;
+ break;
+ } else if(test_char == ' ' || test_char == '\t' || test_char == parentSeparatorChar) {
+ ++search_start;
+ } else {
+ search_start = line.find_first_of(line_sep_chars, search_start);
+ }
+ }
+ }
+ // Find = in string, split and recombine
+ auto delimiter_pos = line.find_first_of(valueDelimiter, search_start + 1);
+ auto comment_pos = line.find_first_of(commentChar, search_start);
+ if(comment_pos < delimiter_pos) {
+ delimiter_pos = std::string::npos;
+ }
+ if(delimiter_pos != std::string::npos) {
+
+ name = detail::trim_copy(line.substr(0, delimiter_pos));
+ std::string item = detail::trim_copy(line.substr(delimiter_pos + 1, std::string::npos));
+ bool mlquote =
+ (item.compare(0, 3, multiline_literal_quote) == 0 || item.compare(0, 3, multiline_string_quote) == 0);
+ if(!mlquote && comment_pos != std::string::npos) {
+ auto citems = detail::split_up(item, commentChar);
+ item = detail::trim_copy(citems.front());
+ }
+ if(mlquote) {
+ // mutliline string
+ auto keyChar = item.front();
+ item = buffer.substr(delimiter_pos + 1, std::string::npos);
+ detail::ltrim(item);
+ item.erase(0, 3);
+ inMLineValue = true;
+ bool lineExtension{false};
+ bool firstLine = true;
+ if(!item.empty() && item.back() == '\\') {
+ item.pop_back();
+ lineExtension = true;
+ }
+ while(inMLineValue) {
+ std::string l2;
+ if(!std::getline(input, l2)) {
+ break;
+ }
+ line = l2;
+ detail::rtrim(line);
+ if(detail::hasMLString(line, keyChar)) {
+ line.pop_back();
+ line.pop_back();
+ line.pop_back();
+ if(lineExtension) {
+ detail::ltrim(line);
+ } else if(!(firstLine && item.empty())) {
+ item.push_back('\n');
+ }
+ firstLine = false;
+ item += line;
+ inMLineValue = false;
+ if(!item.empty() && item.back() == '\n') {
+ item.pop_back();
+ }
+ if(keyChar == '\"') {
+ try {
+ item = detail::remove_escaped_characters(item);
+ } catch(const std::invalid_argument &iarg) {
+ throw CLI::ParseError(iarg.what(), CLI::ExitCodes::InvalidError);
+ }
+ }
+ } else {
+ if(lineExtension) {
+ detail::trim(l2);
+ } else if(!(firstLine && item.empty())) {
+ item.push_back('\n');
+ }
+ lineExtension = false;
+ firstLine = false;
+ if(!l2.empty() && l2.back() == '\\') {
+ lineExtension = true;
+ l2.pop_back();
+ }
+ item += l2;
+ }
+ }
+ items_buffer = {item};
+ } else if(item.size() > 1 && item.front() == aStart) {
+ for(std::string multiline; item.back() != aEnd && std::getline(input, multiline);) {
+ detail::trim(multiline);
+ item += multiline;
+ }
+ if(item.back() == aEnd) {
+ items_buffer = detail::split_up(item.substr(1, item.length() - 2), aSep);
+ } else {
+ items_buffer = detail::split_up(item.substr(1, std::string::npos), aSep);
+ }
+ } else if((isDefaultArray || isINIArray) && item.find_first_of(aSep) != std::string::npos) {
+ items_buffer = detail::split_up(item, aSep);
+ } else if((isDefaultArray || isINIArray) && item.find_first_of(' ') != std::string::npos) {
+ items_buffer = detail::split_up(item, '\0');
+ } else {
+ items_buffer = {item};
+ }
+ } else {
+ name = detail::trim_copy(line.substr(0, comment_pos));
+ items_buffer = {"true"};
+ }
+ std::vector<std::string> parents;
+ try {
+ parents = detail::generate_parents(currentSection, name, parentSeparatorChar);
+ detail::process_quoted_string(name);
+ // clean up quotes on the items and check for escaped strings
+ for(auto &it : items_buffer) {
+ detail::process_quoted_string(it, stringQuote, literalQuote);
+ }
+ } catch(const std::invalid_argument &ia) {
+ throw CLI::ParseError(ia.what(), CLI::ExitCodes::InvalidError);
+ }
+
+ if(parents.size() > maximumLayers) {
+ continue;
+ }
+ if(!configSection.empty() && !inSection) {
+ if(parents.empty() || parents.front() != configSection) {
+ continue;
+ }
+ if(configIndex >= 0 && currentSectionIndex != configIndex) {
+ continue;
+ }
+ parents.erase(parents.begin());
+ inSection = true;
+ }
+ if(!output.empty() && name == output.back().name && parents == output.back().parents) {
+ output.back().inputs.insert(output.back().inputs.end(), items_buffer.begin(), items_buffer.end());
+ } else {
+ output.emplace_back();
+ output.back().parents = std::move(parents);
+ output.back().name = std::move(name);
+ output.back().inputs = std::move(items_buffer);
+ }
+ }
+ if(currentSection != "default") {
+ // insert a section end which is just an empty items_buffer
+ std::string ename;
+ output.emplace_back();
+ output.back().parents = detail::generate_parents(currentSection, ename, parentSeparatorChar);
+ output.back().name = "--";
+ while(output.back().parents.size() > 1) {
+ output.push_back(output.back());
+ output.back().parents.pop_back();
+ }
+ }
+ return output;
+}
+
+CLI11_INLINE std::string &clean_name_string(std::string &name, const std::string &keyChars) {
+ if(name.find_first_of(keyChars) != std::string::npos || (name.front() == '[' && name.back() == ']') ||
+ (name.find_first_of("'`\"\\") != std::string::npos)) {
+ if(name.find_first_of('\'') == std::string::npos) {
+ name.insert(0, 1, '\'');
+ name.push_back('\'');
+ } else {
+ if(detail::has_escapable_character(name)) {
+ name = detail::add_escaped_characters(name);
+ }
+ name.insert(0, 1, '\"');
+ name.push_back('\"');
+ }
+ }
+ return name;
+}
+
+CLI11_INLINE std::string
+ConfigBase::to_config(const App *app, bool default_also, bool write_description, std::string prefix) const {
+ std::stringstream out;
+ std::string commentLead;
+ commentLead.push_back(commentChar);
+ commentLead.push_back(' ');
+
+ std::string commentTest = "#;";
+ commentTest.push_back(commentChar);
+ commentTest.push_back(parentSeparatorChar);
+
+ std::string keyChars = commentTest;
+ keyChars.push_back(literalQuote);
+ keyChars.push_back(stringQuote);
+ keyChars.push_back(arrayStart);
+ keyChars.push_back(arrayEnd);
+ keyChars.push_back(valueDelimiter);
+ keyChars.push_back(arraySeparator);
+
+ std::vector<std::string> groups = app->get_groups();
+ bool defaultUsed = false;
+ groups.insert(groups.begin(), std::string("Options"));
+ if(write_description && (app->get_configurable() || app->get_parent() == nullptr || app->get_name().empty())) {
+ out << commentLead << detail::fix_newlines(commentLead, app->get_description()) << '\n';
+ }
+ for(auto &group : groups) {
+ if(group == "Options" || group.empty()) {
+ if(defaultUsed) {
+ continue;
+ }
+ defaultUsed = true;
+ }
+ if(write_description && group != "Options" && !group.empty()) {
+ out << '\n' << commentLead << group << " Options\n";
+ }
+ for(const Option *opt : app->get_options({})) {
+
+ // Only process options that are configurable
+ if(opt->get_configurable()) {
+ if(opt->get_group() != group) {
+ if(!(group == "Options" && opt->get_group().empty())) {
+ continue;
+ }
+ }
+ std::string single_name = opt->get_single_name();
+ if(single_name.empty()) {
+ continue;
+ }
+
+ std::string value = detail::ini_join(
+ opt->reduced_results(), arraySeparator, arrayStart, arrayEnd, stringQuote, literalQuote);
+
+ if(value.empty() && default_also) {
+ if(!opt->get_default_str().empty()) {
+ value = detail::convert_arg_for_ini(opt->get_default_str(), stringQuote, literalQuote, false);
+ } else if(opt->get_expected_min() == 0) {
+ value = "false";
+ } else if(opt->get_run_callback_for_default()) {
+ value = "\"\""; // empty string default value
+ }
+ }
+
+ if(!value.empty()) {
+
+ if(!opt->get_fnames().empty()) {
+ try {
+ value = opt->get_flag_value(single_name, value);
+ } catch(const CLI::ArgumentMismatch &) {
+ bool valid{false};
+ for(const auto &test_name : opt->get_fnames()) {
+ try {
+ value = opt->get_flag_value(test_name, value);
+ single_name = test_name;
+ valid = true;
+ } catch(const CLI::ArgumentMismatch &) {
+ continue;
+ }
+ }
+ if(!valid) {
+ value = detail::ini_join(
+ opt->results(), arraySeparator, arrayStart, arrayEnd, stringQuote, literalQuote);
+ }
+ }
+ }
+ if(write_description && opt->has_description()) {
+ out << '\n';
+ out << commentLead << detail::fix_newlines(commentLead, opt->get_description()) << '\n';
+ }
+ clean_name_string(single_name, keyChars);
+
+ std::string name = prefix + single_name;
+
+ out << name << valueDelimiter << value << '\n';
+ }
+ }
+ }
+ }
+ auto subcommands = app->get_subcommands({});
+ for(const App *subcom : subcommands) {
+ if(subcom->get_name().empty()) {
+ if(!default_also && (subcom->count_all() == 0)) {
+ continue;
+ }
+ if(write_description && !subcom->get_group().empty()) {
+ out << '\n' << commentLead << subcom->get_group() << " Options\n";
+ }
+ /*if (!prefix.empty() || app->get_parent() == nullptr) {
+ out << '[' << prefix << "___"<< subcom->get_group() << "]\n";
+ } else {
+ std::string subname = app->get_name() + parentSeparatorChar + "___"+subcom->get_group();
+ const auto *p = app->get_parent();
+ while(p->get_parent() != nullptr) {
+ subname = p->get_name() + parentSeparatorChar +subname;
+ p = p->get_parent();
+ }
+ out << '[' << subname << "]\n";
+ }
+ */
+ out << to_config(subcom, default_also, write_description, prefix);
+ }
+ }
+
+ for(const App *subcom : subcommands) {
+ if(!subcom->get_name().empty()) {
+ if(!default_also && (subcom->count_all() == 0)) {
+ continue;
+ }
+ std::string subname = subcom->get_name();
+ clean_name_string(subname, keyChars);
+
+ if(subcom->get_configurable() && app->got_subcommand(subcom)) {
+ if(!prefix.empty() || app->get_parent() == nullptr) {
+
+ out << '[' << prefix << subname << "]\n";
+ } else {
+ std::string appname = app->get_name();
+ clean_name_string(appname, keyChars);
+ subname = appname + parentSeparatorChar + subname;
+ const auto *p = app->get_parent();
+ while(p->get_parent() != nullptr) {
+ std::string pname = p->get_name();
+ clean_name_string(pname, keyChars);
+ subname = pname + parentSeparatorChar + subname;
+ p = p->get_parent();
+ }
+ out << '[' << subname << "]\n";
+ }
+ out << to_config(subcom, default_also, write_description, "");
+ } else {
+ out << to_config(subcom, default_also, write_description, prefix + subname + parentSeparatorChar);
+ }
+ }
+ }
+
+ return out.str();
+}
+
+
+
+
+
+
+CLI11_INLINE std::string
+Formatter::make_group(std::string group, bool is_positional, std::vector<const Option *> opts) const {
+ std::stringstream out;
+
+ out << "\n" << group << ":\n";
+ for(const Option *opt : opts) {
+ out << make_option(opt, is_positional);
+ }
+
+ return out.str();
+}
+
+CLI11_INLINE std::string Formatter::make_positionals(const App *app) const {
+ std::vector<const Option *> opts =
+ app->get_options([](const Option *opt) { return !opt->get_group().empty() && opt->get_positional(); });
+
+ if(opts.empty())
+ return {};
+
+ return make_group(get_label("Positionals"), true, opts);
+}
+
+CLI11_INLINE std::string Formatter::make_groups(const App *app, AppFormatMode mode) const {
+ std::stringstream out;
+ std::vector<std::string> groups = app->get_groups();
+
+ // Options
+ for(const std::string &group : groups) {
+ std::vector<const Option *> opts = app->get_options([app, mode, &group](const Option *opt) {
+ return opt->get_group() == group // Must be in the right group
+ && opt->nonpositional() // Must not be a positional
+ && (mode != AppFormatMode::Sub // If mode is Sub, then
+ || (app->get_help_ptr() != opt // Ignore help pointer
+ && app->get_help_all_ptr() != opt)); // Ignore help all pointer
+ });
+ if(!group.empty() && !opts.empty()) {
+ out << make_group(group, false, opts);
+
+ if(group != groups.back())
+ out << "\n";
+ }
+ }
+
+ return out.str();
+}
+
+CLI11_INLINE std::string Formatter::make_description(const App *app) const {
+ std::string desc = app->get_description();
+ auto min_options = app->get_require_option_min();
+ auto max_options = app->get_require_option_max();
+ if(app->get_required()) {
+ desc += " " + get_label("REQUIRED") + " ";
+ }
+ if((max_options == min_options) && (min_options > 0)) {
+ if(min_options == 1) {
+ desc += " \n[Exactly 1 of the following options is required]";
+ } else {
+ desc += " \n[Exactly " + std::to_string(min_options) + " options from the following list are required]";
+ }
+ } else if(max_options > 0) {
+ if(min_options > 0) {
+ desc += " \n[Between " + std::to_string(min_options) + " and " + std::to_string(max_options) +
+ " of the follow options are required]";
+ } else {
+ desc += " \n[At most " + std::to_string(max_options) + " of the following options are allowed]";
+ }
+ } else if(min_options > 0) {
+ desc += " \n[At least " + std::to_string(min_options) + " of the following options are required]";
+ }
+ return (!desc.empty()) ? desc + "\n" : std::string{};
+}
+
+CLI11_INLINE std::string Formatter::make_usage(const App *app, std::string name) const {
+ std::string usage = app->get_usage();
+ if(!usage.empty()) {
+ return usage + "\n";
+ }
+
+ std::stringstream out;
+
+ out << get_label("Usage") << ":" << (name.empty() ? "" : " ") << name;
+
+ std::vector<std::string> groups = app->get_groups();
+
+ // Print an Options badge if any options exist
+ std::vector<const Option *> non_pos_options =
+ app->get_options([](const Option *opt) { return opt->nonpositional(); });
+ if(!non_pos_options.empty())
+ out << " [" << get_label("OPTIONS") << "]";
+
+ // Positionals need to be listed here
+ std::vector<const Option *> positionals = app->get_options([](const Option *opt) { return opt->get_positional(); });
+
+ // Print out positionals if any are left
+ if(!positionals.empty()) {
+ // Convert to help names
+ std::vector<std::string> positional_names(positionals.size());
+ std::transform(positionals.begin(), positionals.end(), positional_names.begin(), [this](const Option *opt) {
+ return make_option_usage(opt);
+ });
+
+ out << " " << detail::join(positional_names, " ");
+ }
+
+ // Add a marker if subcommands are expected or optional
+ if(!app->get_subcommands(
+ [](const CLI::App *subc) { return ((!subc->get_disabled()) && (!subc->get_name().empty())); })
+ .empty()) {
+ out << " " << (app->get_require_subcommand_min() == 0 ? "[" : "")
+ << get_label(app->get_require_subcommand_max() < 2 || app->get_require_subcommand_min() > 1 ? "SUBCOMMAND"
+ : "SUBCOMMANDS")
+ << (app->get_require_subcommand_min() == 0 ? "]" : "");
+ }
+
+ out << '\n';
+
+ return out.str();
+}
+
+CLI11_INLINE std::string Formatter::make_footer(const App *app) const {
+ std::string footer = app->get_footer();
+ if(footer.empty()) {
+ return std::string{};
+ }
+ return "\n" + footer + "\n";
+}
+
+CLI11_INLINE std::string Formatter::make_help(const App *app, std::string name, AppFormatMode mode) const {
+
+ // This immediately forwards to the make_expanded method. This is done this way so that subcommands can
+ // have overridden formatters
+ if(mode == AppFormatMode::Sub)
+ return make_expanded(app);
+
+ std::stringstream out;
+ if((app->get_name().empty()) && (app->get_parent() != nullptr)) {
+ if(app->get_group() != "Subcommands") {
+ out << app->get_group() << ':';
+ }
+ }
+
+ out << make_description(app);
+ out << make_usage(app, name);
+ out << make_positionals(app);
+ out << make_groups(app, mode);
+ out << make_subcommands(app, mode);
+ out << make_footer(app);
+
+ return out.str();
+}
+
+CLI11_INLINE std::string Formatter::make_subcommands(const App *app, AppFormatMode mode) const {
+ std::stringstream out;
+
+ std::vector<const App *> subcommands = app->get_subcommands({});
+
+ // Make a list in definition order of the groups seen
+ std::vector<std::string> subcmd_groups_seen;
+ for(const App *com : subcommands) {
+ if(com->get_name().empty()) {
+ if(!com->get_group().empty()) {
+ out << make_expanded(com);
+ }
+ continue;
+ }
+ std::string group_key = com->get_group();
+ if(!group_key.empty() &&
+ std::find_if(subcmd_groups_seen.begin(), subcmd_groups_seen.end(), [&group_key](std::string a) {
+ return detail::to_lower(a) == detail::to_lower(group_key);
+ }) == subcmd_groups_seen.end())
+ subcmd_groups_seen.push_back(group_key);
+ }
+
+ // For each group, filter out and print subcommands
+ for(const std::string &group : subcmd_groups_seen) {
+ out << "\n" << group << ":\n";
+ std::vector<const App *> subcommands_group = app->get_subcommands(
+ [&group](const App *sub_app) { return detail::to_lower(sub_app->get_group()) == detail::to_lower(group); });
+ for(const App *new_com : subcommands_group) {
+ if(new_com->get_name().empty())
+ continue;
+ if(mode != AppFormatMode::All) {
+ out << make_subcommand(new_com);
+ } else {
+ out << new_com->help(new_com->get_name(), AppFormatMode::Sub);
+ out << "\n";
+ }
+ }
+ }
+
+ return out.str();
+}
+
+CLI11_INLINE std::string Formatter::make_subcommand(const App *sub) const {
+ std::stringstream out;
+ detail::format_help(out,
+ sub->get_display_name(true) + (sub->get_required() ? " " + get_label("REQUIRED") : ""),
+ sub->get_description(),
+ column_width_);
+ return out.str();
+}
+
+CLI11_INLINE std::string Formatter::make_expanded(const App *sub) const {
+ std::stringstream out;
+ out << sub->get_display_name(true) << "\n";
+
+ out << make_description(sub);
+ if(sub->get_name().empty() && !sub->get_aliases().empty()) {
+ detail::format_aliases(out, sub->get_aliases(), column_width_ + 2);
+ }
+ out << make_positionals(sub);
+ out << make_groups(sub, AppFormatMode::Sub);
+ out << make_subcommands(sub, AppFormatMode::Sub);
+
+ // Drop blank spaces
+ std::string tmp = detail::find_and_replace(out.str(), "\n\n", "\n");
+ tmp = tmp.substr(0, tmp.size() - 1); // Remove the final '\n'
+
+ // Indent all but the first line (the name)
+ return detail::find_and_replace(tmp, "\n", "\n ") + "\n";
+}
+
+CLI11_INLINE std::string Formatter::make_option_name(const Option *opt, bool is_positional) const {
+ if(is_positional)
+ return opt->get_name(true, false);
+
+ return opt->get_name(false, true);
+}
+
+CLI11_INLINE std::string Formatter::make_option_opts(const Option *opt) const {
+ std::stringstream out;
+
+ if(!opt->get_option_text().empty()) {
+ out << " " << opt->get_option_text();
+ } else {
+ if(opt->get_type_size() != 0) {
+ if(!opt->get_type_name().empty())
+ out << " " << get_label(opt->get_type_name());
+ if(!opt->get_default_str().empty())
+ out << " [" << opt->get_default_str() << "] ";
+ if(opt->get_expected_max() == detail::expected_max_vector_size)
+ out << " ...";
+ else if(opt->get_expected_min() > 1)
+ out << " x " << opt->get_expected();
+
+ if(opt->get_required())
+ out << " " << get_label("REQUIRED");
+ }
+ if(!opt->get_envname().empty())
+ out << " (" << get_label("Env") << ":" << opt->get_envname() << ")";
+ if(!opt->get_needs().empty()) {
+ out << " " << get_label("Needs") << ":";
+ for(const Option *op : opt->get_needs())
+ out << " " << op->get_name();
+ }
+ if(!opt->get_excludes().empty()) {
+ out << " " << get_label("Excludes") << ":";
+ for(const Option *op : opt->get_excludes())
+ out << " " << op->get_name();
+ }
+ }
+ return out.str();
+}
+
+CLI11_INLINE std::string Formatter::make_option_desc(const Option *opt) const { return opt->get_description(); }
+
+CLI11_INLINE std::string Formatter::make_option_usage(const Option *opt) const {
+ // Note that these are positionals usages
+ std::stringstream out;
+ out << make_option_name(opt, true);
+ if(opt->get_expected_max() >= detail::expected_max_vector_size)
+ out << "...";
+ else if(opt->get_expected_max() > 1)
+ out << "(" << opt->get_expected() << "x)";
+
+ return opt->get_required() ? out.str() : "[" + out.str() + "]";
+}
+
+
+} // namespace CLI
--- /dev/null
+Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+--- Optional exception to the license ---
+
+As an exception, if, as a result of your compiling your source code, portions
+of this Software are embedded into a machine-executable object form of such
+source code, you may redistribute such embedded portions in such object form
+without including the above copyright and permission notices.
--- /dev/null
+// Formatting library for C++ - dynamic argument lists
+//
+// Copyright (c) 2012 - present, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_ARGS_H_
+#define FMT_ARGS_H_
+
+#include <functional> // std::reference_wrapper
+#include <memory> // std::unique_ptr
+#include <vector>
+
+#include "core.h"
+
+FMT_BEGIN_NAMESPACE
+
+namespace detail {
+
+template <typename T> struct is_reference_wrapper : std::false_type {};
+template <typename T>
+struct is_reference_wrapper<std::reference_wrapper<T>> : std::true_type {};
+
+template <typename T> auto unwrap(const T& v) -> const T& { return v; }
+template <typename T>
+auto unwrap(const std::reference_wrapper<T>& v) -> const T& {
+ return static_cast<const T&>(v);
+}
+
+class dynamic_arg_list {
+ // Workaround for clang's -Wweak-vtables. Unlike for regular classes, for
+ // templates it doesn't complain about inability to deduce single translation
+ // unit for placing vtable. So storage_node_base is made a fake template.
+ template <typename = void> struct node {
+ virtual ~node() = default;
+ std::unique_ptr<node<>> next;
+ };
+
+ template <typename T> struct typed_node : node<> {
+ T value;
+
+ template <typename Arg>
+ FMT_CONSTEXPR typed_node(const Arg& arg) : value(arg) {}
+
+ template <typename Char>
+ FMT_CONSTEXPR typed_node(const basic_string_view<Char>& arg)
+ : value(arg.data(), arg.size()) {}
+ };
+
+ std::unique_ptr<node<>> head_;
+
+ public:
+ template <typename T, typename Arg> auto push(const Arg& arg) -> const T& {
+ auto new_node = std::unique_ptr<typed_node<T>>(new typed_node<T>(arg));
+ auto& value = new_node->value;
+ new_node->next = std::move(head_);
+ head_ = std::move(new_node);
+ return value;
+ }
+};
+} // namespace detail
+
+/**
+ \rst
+ A dynamic version of `fmt::format_arg_store`.
+ It's equipped with a storage to potentially temporary objects which lifetimes
+ could be shorter than the format arguments object.
+
+ It can be implicitly converted into `~fmt::basic_format_args` for passing
+ into type-erased formatting functions such as `~fmt::vformat`.
+ \endrst
+ */
+template <typename Context>
+class dynamic_format_arg_store
+#if FMT_GCC_VERSION && FMT_GCC_VERSION < 409
+ // Workaround a GCC template argument substitution bug.
+ : public basic_format_args<Context>
+#endif
+{
+ private:
+ using char_type = typename Context::char_type;
+
+ template <typename T> struct need_copy {
+ static constexpr detail::type mapped_type =
+ detail::mapped_type_constant<T, Context>::value;
+
+ enum {
+ value = !(detail::is_reference_wrapper<T>::value ||
+ std::is_same<T, basic_string_view<char_type>>::value ||
+ std::is_same<T, detail::std_string_view<char_type>>::value ||
+ (mapped_type != detail::type::cstring_type &&
+ mapped_type != detail::type::string_type &&
+ mapped_type != detail::type::custom_type))
+ };
+ };
+
+ template <typename T>
+ using stored_type = conditional_t<
+ std::is_convertible<T, std::basic_string<char_type>>::value &&
+ !detail::is_reference_wrapper<T>::value,
+ std::basic_string<char_type>, T>;
+
+ // Storage of basic_format_arg must be contiguous.
+ std::vector<basic_format_arg<Context>> data_;
+ std::vector<detail::named_arg_info<char_type>> named_info_;
+
+ // Storage of arguments not fitting into basic_format_arg must grow
+ // without relocation because items in data_ refer to it.
+ detail::dynamic_arg_list dynamic_args_;
+
+ friend class basic_format_args<Context>;
+
+ auto get_types() const -> unsigned long long {
+ return detail::is_unpacked_bit | data_.size() |
+ (named_info_.empty()
+ ? 0ULL
+ : static_cast<unsigned long long>(detail::has_named_args_bit));
+ }
+
+ auto data() const -> const basic_format_arg<Context>* {
+ return named_info_.empty() ? data_.data() : data_.data() + 1;
+ }
+
+ template <typename T> void emplace_arg(const T& arg) {
+ data_.emplace_back(detail::make_arg<Context>(arg));
+ }
+
+ template <typename T>
+ void emplace_arg(const detail::named_arg<char_type, T>& arg) {
+ if (named_info_.empty()) {
+ constexpr const detail::named_arg_info<char_type>* zero_ptr{nullptr};
+ data_.insert(data_.begin(), {zero_ptr, 0});
+ }
+ data_.emplace_back(detail::make_arg<Context>(detail::unwrap(arg.value)));
+ auto pop_one = [](std::vector<basic_format_arg<Context>>* data) {
+ data->pop_back();
+ };
+ std::unique_ptr<std::vector<basic_format_arg<Context>>, decltype(pop_one)>
+ guard{&data_, pop_one};
+ named_info_.push_back({arg.name, static_cast<int>(data_.size() - 2u)});
+ data_[0].value_.named_args = {named_info_.data(), named_info_.size()};
+ guard.release();
+ }
+
+ public:
+ constexpr dynamic_format_arg_store() = default;
+
+ /**
+ \rst
+ Adds an argument into the dynamic store for later passing to a formatting
+ function.
+
+ Note that custom types and string types (but not string views) are copied
+ into the store dynamically allocating memory if necessary.
+
+ **Example**::
+
+ fmt::dynamic_format_arg_store<fmt::format_context> store;
+ store.push_back(42);
+ store.push_back("abc");
+ store.push_back(1.5f);
+ std::string result = fmt::vformat("{} and {} and {}", store);
+ \endrst
+ */
+ template <typename T> void push_back(const T& arg) {
+ if (detail::const_check(need_copy<T>::value))
+ emplace_arg(dynamic_args_.push<stored_type<T>>(arg));
+ else
+ emplace_arg(detail::unwrap(arg));
+ }
+
+ /**
+ \rst
+ Adds a reference to the argument into the dynamic store for later passing to
+ a formatting function.
+
+ **Example**::
+
+ fmt::dynamic_format_arg_store<fmt::format_context> store;
+ char band[] = "Rolling Stones";
+ store.push_back(std::cref(band));
+ band[9] = 'c'; // Changing str affects the output.
+ std::string result = fmt::vformat("{}", store);
+ // result == "Rolling Scones"
+ \endrst
+ */
+ template <typename T> void push_back(std::reference_wrapper<T> arg) {
+ static_assert(
+ need_copy<T>::value,
+ "objects of built-in types and string views are always copied");
+ emplace_arg(arg.get());
+ }
+
+ /**
+ Adds named argument into the dynamic store for later passing to a formatting
+ function. ``std::reference_wrapper`` is supported to avoid copying of the
+ argument. The name is always copied into the store.
+ */
+ template <typename T>
+ void push_back(const detail::named_arg<char_type, T>& arg) {
+ const char_type* arg_name =
+ dynamic_args_.push<std::basic_string<char_type>>(arg.name).c_str();
+ if (detail::const_check(need_copy<T>::value)) {
+ emplace_arg(
+ fmt::arg(arg_name, dynamic_args_.push<stored_type<T>>(arg.value)));
+ } else {
+ emplace_arg(fmt::arg(arg_name, arg.value));
+ }
+ }
+
+ /** Erase all elements from the store */
+ void clear() {
+ data_.clear();
+ named_info_.clear();
+ dynamic_args_ = detail::dynamic_arg_list();
+ }
+
+ /**
+ \rst
+ Reserves space to store at least *new_cap* arguments including
+ *new_cap_named* named arguments.
+ \endrst
+ */
+ void reserve(size_t new_cap, size_t new_cap_named) {
+ FMT_ASSERT(new_cap >= new_cap_named,
+ "Set of arguments includes set of named arguments");
+ data_.reserve(new_cap);
+ named_info_.reserve(new_cap_named);
+ }
+};
+
+FMT_END_NAMESPACE
+
+#endif // FMT_ARGS_H_
--- /dev/null
+// Formatting library for C++ - chrono support
+//
+// Copyright (c) 2012 - present, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_CHRONO_H_
+#define FMT_CHRONO_H_
+
+#include <algorithm>
+#include <chrono>
+#include <cmath> // std::isfinite
+#include <cstring> // std::memcpy
+#include <ctime>
+#include <iterator>
+#include <locale>
+#include <ostream>
+#include <type_traits>
+
+#include "ostream.h" // formatbuf
+
+FMT_BEGIN_NAMESPACE
+
+// Check if std::chrono::local_t is available.
+#ifndef FMT_USE_LOCAL_TIME
+# ifdef __cpp_lib_chrono
+# define FMT_USE_LOCAL_TIME (__cpp_lib_chrono >= 201907L)
+# else
+# define FMT_USE_LOCAL_TIME 0
+# endif
+#endif
+
+// Check if std::chrono::utc_timestamp is available.
+#ifndef FMT_USE_UTC_TIME
+# ifdef __cpp_lib_chrono
+# define FMT_USE_UTC_TIME (__cpp_lib_chrono >= 201907L)
+# else
+# define FMT_USE_UTC_TIME 0
+# endif
+#endif
+
+// Enable tzset.
+#ifndef FMT_USE_TZSET
+// UWP doesn't provide _tzset.
+# if FMT_HAS_INCLUDE("winapifamily.h")
+# include <winapifamily.h>
+# endif
+# if defined(_WIN32) && (!defined(WINAPI_FAMILY) || \
+ (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP))
+# define FMT_USE_TZSET 1
+# else
+# define FMT_USE_TZSET 0
+# endif
+#endif
+
+// Enable safe chrono durations, unless explicitly disabled.
+#ifndef FMT_SAFE_DURATION_CAST
+# define FMT_SAFE_DURATION_CAST 1
+#endif
+#if FMT_SAFE_DURATION_CAST
+
+// For conversion between std::chrono::durations without undefined
+// behaviour or erroneous results.
+// This is a stripped down version of duration_cast, for inclusion in fmt.
+// See https://github.com/pauldreik/safe_duration_cast
+//
+// Copyright Paul Dreik 2019
+namespace safe_duration_cast {
+
+template <typename To, typename From,
+ FMT_ENABLE_IF(!std::is_same<From, To>::value &&
+ std::numeric_limits<From>::is_signed ==
+ std::numeric_limits<To>::is_signed)>
+FMT_CONSTEXPR auto lossless_integral_conversion(const From from, int& ec)
+ -> To {
+ ec = 0;
+ using F = std::numeric_limits<From>;
+ using T = std::numeric_limits<To>;
+ static_assert(F::is_integer, "From must be integral");
+ static_assert(T::is_integer, "To must be integral");
+
+ // A and B are both signed, or both unsigned.
+ if (detail::const_check(F::digits <= T::digits)) {
+ // From fits in To without any problem.
+ } else {
+ // From does not always fit in To, resort to a dynamic check.
+ if (from < (T::min)() || from > (T::max)()) {
+ // outside range.
+ ec = 1;
+ return {};
+ }
+ }
+ return static_cast<To>(from);
+}
+
+/**
+ * converts From to To, without loss. If the dynamic value of from
+ * can't be converted to To without loss, ec is set.
+ */
+template <typename To, typename From,
+ FMT_ENABLE_IF(!std::is_same<From, To>::value &&
+ std::numeric_limits<From>::is_signed !=
+ std::numeric_limits<To>::is_signed)>
+FMT_CONSTEXPR auto lossless_integral_conversion(const From from, int& ec)
+ -> To {
+ ec = 0;
+ using F = std::numeric_limits<From>;
+ using T = std::numeric_limits<To>;
+ static_assert(F::is_integer, "From must be integral");
+ static_assert(T::is_integer, "To must be integral");
+
+ if (detail::const_check(F::is_signed && !T::is_signed)) {
+ // From may be negative, not allowed!
+ if (fmt::detail::is_negative(from)) {
+ ec = 1;
+ return {};
+ }
+ // From is positive. Can it always fit in To?
+ if (detail::const_check(F::digits > T::digits) &&
+ from > static_cast<From>(detail::max_value<To>())) {
+ ec = 1;
+ return {};
+ }
+ }
+
+ if (detail::const_check(!F::is_signed && T::is_signed &&
+ F::digits >= T::digits) &&
+ from > static_cast<From>(detail::max_value<To>())) {
+ ec = 1;
+ return {};
+ }
+ return static_cast<To>(from); // Lossless conversion.
+}
+
+template <typename To, typename From,
+ FMT_ENABLE_IF(std::is_same<From, To>::value)>
+FMT_CONSTEXPR auto lossless_integral_conversion(const From from, int& ec)
+ -> To {
+ ec = 0;
+ return from;
+} // function
+
+// clang-format off
+/**
+ * converts From to To if possible, otherwise ec is set.
+ *
+ * input | output
+ * ---------------------------------|---------------
+ * NaN | NaN
+ * Inf | Inf
+ * normal, fits in output | converted (possibly lossy)
+ * normal, does not fit in output | ec is set
+ * subnormal | best effort
+ * -Inf | -Inf
+ */
+// clang-format on
+template <typename To, typename From,
+ FMT_ENABLE_IF(!std::is_same<From, To>::value)>
+FMT_CONSTEXPR auto safe_float_conversion(const From from, int& ec) -> To {
+ ec = 0;
+ using T = std::numeric_limits<To>;
+ static_assert(std::is_floating_point<From>::value, "From must be floating");
+ static_assert(std::is_floating_point<To>::value, "To must be floating");
+
+ // catch the only happy case
+ if (std::isfinite(from)) {
+ if (from >= T::lowest() && from <= (T::max)()) {
+ return static_cast<To>(from);
+ }
+ // not within range.
+ ec = 1;
+ return {};
+ }
+
+ // nan and inf will be preserved
+ return static_cast<To>(from);
+} // function
+
+template <typename To, typename From,
+ FMT_ENABLE_IF(std::is_same<From, To>::value)>
+FMT_CONSTEXPR auto safe_float_conversion(const From from, int& ec) -> To {
+ ec = 0;
+ static_assert(std::is_floating_point<From>::value, "From must be floating");
+ return from;
+}
+
+/**
+ * safe duration cast between integral durations
+ */
+template <typename To, typename FromRep, typename FromPeriod,
+ FMT_ENABLE_IF(std::is_integral<FromRep>::value),
+ FMT_ENABLE_IF(std::is_integral<typename To::rep>::value)>
+auto safe_duration_cast(std::chrono::duration<FromRep, FromPeriod> from,
+ int& ec) -> To {
+ using From = std::chrono::duration<FromRep, FromPeriod>;
+ ec = 0;
+ // the basic idea is that we need to convert from count() in the from type
+ // to count() in the To type, by multiplying it with this:
+ struct Factor
+ : std::ratio_divide<typename From::period, typename To::period> {};
+
+ static_assert(Factor::num > 0, "num must be positive");
+ static_assert(Factor::den > 0, "den must be positive");
+
+ // the conversion is like this: multiply from.count() with Factor::num
+ // /Factor::den and convert it to To::rep, all this without
+ // overflow/underflow. let's start by finding a suitable type that can hold
+ // both To, From and Factor::num
+ using IntermediateRep =
+ typename std::common_type<typename From::rep, typename To::rep,
+ decltype(Factor::num)>::type;
+
+ // safe conversion to IntermediateRep
+ IntermediateRep count =
+ lossless_integral_conversion<IntermediateRep>(from.count(), ec);
+ if (ec) return {};
+ // multiply with Factor::num without overflow or underflow
+ if (detail::const_check(Factor::num != 1)) {
+ const auto max1 = detail::max_value<IntermediateRep>() / Factor::num;
+ if (count > max1) {
+ ec = 1;
+ return {};
+ }
+ const auto min1 =
+ (std::numeric_limits<IntermediateRep>::min)() / Factor::num;
+ if (detail::const_check(!std::is_unsigned<IntermediateRep>::value) &&
+ count < min1) {
+ ec = 1;
+ return {};
+ }
+ count *= Factor::num;
+ }
+
+ if (detail::const_check(Factor::den != 1)) count /= Factor::den;
+ auto tocount = lossless_integral_conversion<typename To::rep>(count, ec);
+ return ec ? To() : To(tocount);
+}
+
+/**
+ * safe duration_cast between floating point durations
+ */
+template <typename To, typename FromRep, typename FromPeriod,
+ FMT_ENABLE_IF(std::is_floating_point<FromRep>::value),
+ FMT_ENABLE_IF(std::is_floating_point<typename To::rep>::value)>
+auto safe_duration_cast(std::chrono::duration<FromRep, FromPeriod> from,
+ int& ec) -> To {
+ using From = std::chrono::duration<FromRep, FromPeriod>;
+ ec = 0;
+ if (std::isnan(from.count())) {
+ // nan in, gives nan out. easy.
+ return To{std::numeric_limits<typename To::rep>::quiet_NaN()};
+ }
+ // maybe we should also check if from is denormal, and decide what to do about
+ // it.
+
+ // +-inf should be preserved.
+ if (std::isinf(from.count())) {
+ return To{from.count()};
+ }
+
+ // the basic idea is that we need to convert from count() in the from type
+ // to count() in the To type, by multiplying it with this:
+ struct Factor
+ : std::ratio_divide<typename From::period, typename To::period> {};
+
+ static_assert(Factor::num > 0, "num must be positive");
+ static_assert(Factor::den > 0, "den must be positive");
+
+ // the conversion is like this: multiply from.count() with Factor::num
+ // /Factor::den and convert it to To::rep, all this without
+ // overflow/underflow. let's start by finding a suitable type that can hold
+ // both To, From and Factor::num
+ using IntermediateRep =
+ typename std::common_type<typename From::rep, typename To::rep,
+ decltype(Factor::num)>::type;
+
+ // force conversion of From::rep -> IntermediateRep to be safe,
+ // even if it will never happen be narrowing in this context.
+ IntermediateRep count =
+ safe_float_conversion<IntermediateRep>(from.count(), ec);
+ if (ec) {
+ return {};
+ }
+
+ // multiply with Factor::num without overflow or underflow
+ if (detail::const_check(Factor::num != 1)) {
+ constexpr auto max1 = detail::max_value<IntermediateRep>() /
+ static_cast<IntermediateRep>(Factor::num);
+ if (count > max1) {
+ ec = 1;
+ return {};
+ }
+ constexpr auto min1 = std::numeric_limits<IntermediateRep>::lowest() /
+ static_cast<IntermediateRep>(Factor::num);
+ if (count < min1) {
+ ec = 1;
+ return {};
+ }
+ count *= static_cast<IntermediateRep>(Factor::num);
+ }
+
+ // this can't go wrong, right? den>0 is checked earlier.
+ if (detail::const_check(Factor::den != 1)) {
+ using common_t = typename std::common_type<IntermediateRep, intmax_t>::type;
+ count /= static_cast<common_t>(Factor::den);
+ }
+
+ // convert to the to type, safely
+ using ToRep = typename To::rep;
+
+ const ToRep tocount = safe_float_conversion<ToRep>(count, ec);
+ if (ec) {
+ return {};
+ }
+ return To{tocount};
+}
+} // namespace safe_duration_cast
+#endif
+
+// Prevents expansion of a preceding token as a function-style macro.
+// Usage: f FMT_NOMACRO()
+#define FMT_NOMACRO
+
+namespace detail {
+template <typename T = void> struct null {};
+inline auto localtime_r FMT_NOMACRO(...) -> null<> { return null<>(); }
+inline auto localtime_s(...) -> null<> { return null<>(); }
+inline auto gmtime_r(...) -> null<> { return null<>(); }
+inline auto gmtime_s(...) -> null<> { return null<>(); }
+
+inline auto get_classic_locale() -> const std::locale& {
+ static const auto& locale = std::locale::classic();
+ return locale;
+}
+
+template <typename CodeUnit> struct codecvt_result {
+ static constexpr const size_t max_size = 32;
+ CodeUnit buf[max_size];
+ CodeUnit* end;
+};
+
+template <typename CodeUnit>
+void write_codecvt(codecvt_result<CodeUnit>& out, string_view in_buf,
+ const std::locale& loc) {
+#if FMT_CLANG_VERSION
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wdeprecated"
+ auto& f = std::use_facet<std::codecvt<CodeUnit, char, std::mbstate_t>>(loc);
+# pragma clang diagnostic pop
+#else
+ auto& f = std::use_facet<std::codecvt<CodeUnit, char, std::mbstate_t>>(loc);
+#endif
+ auto mb = std::mbstate_t();
+ const char* from_next = nullptr;
+ auto result = f.in(mb, in_buf.begin(), in_buf.end(), from_next,
+ std::begin(out.buf), std::end(out.buf), out.end);
+ if (result != std::codecvt_base::ok)
+ FMT_THROW(format_error("failed to format time"));
+}
+
+template <typename OutputIt>
+auto write_encoded_tm_str(OutputIt out, string_view in, const std::locale& loc)
+ -> OutputIt {
+ if (detail::is_utf8() && loc != get_classic_locale()) {
+ // char16_t and char32_t codecvts are broken in MSVC (linkage errors) and
+ // gcc-4.
+#if FMT_MSC_VERSION != 0 || \
+ (defined(__GLIBCXX__) && !defined(_GLIBCXX_USE_DUAL_ABI))
+ // The _GLIBCXX_USE_DUAL_ABI macro is always defined in libstdc++ from gcc-5
+ // and newer.
+ using code_unit = wchar_t;
+#else
+ using code_unit = char32_t;
+#endif
+
+ using unit_t = codecvt_result<code_unit>;
+ unit_t unit;
+ write_codecvt(unit, in, loc);
+ // In UTF-8 is used one to four one-byte code units.
+ auto u =
+ to_utf8<code_unit, basic_memory_buffer<char, unit_t::max_size * 4>>();
+ if (!u.convert({unit.buf, to_unsigned(unit.end - unit.buf)}))
+ FMT_THROW(format_error("failed to format time"));
+ return copy_str<char>(u.c_str(), u.c_str() + u.size(), out);
+ }
+ return copy_str<char>(in.data(), in.data() + in.size(), out);
+}
+
+template <typename Char, typename OutputIt,
+ FMT_ENABLE_IF(!std::is_same<Char, char>::value)>
+auto write_tm_str(OutputIt out, string_view sv, const std::locale& loc)
+ -> OutputIt {
+ codecvt_result<Char> unit;
+ write_codecvt(unit, sv, loc);
+ return copy_str<Char>(unit.buf, unit.end, out);
+}
+
+template <typename Char, typename OutputIt,
+ FMT_ENABLE_IF(std::is_same<Char, char>::value)>
+auto write_tm_str(OutputIt out, string_view sv, const std::locale& loc)
+ -> OutputIt {
+ return write_encoded_tm_str(out, sv, loc);
+}
+
+template <typename Char>
+inline void do_write(buffer<Char>& buf, const std::tm& time,
+ const std::locale& loc, char format, char modifier) {
+ auto&& format_buf = formatbuf<std::basic_streambuf<Char>>(buf);
+ auto&& os = std::basic_ostream<Char>(&format_buf);
+ os.imbue(loc);
+ const auto& facet = std::use_facet<std::time_put<Char>>(loc);
+ auto end = facet.put(os, os, Char(' '), &time, format, modifier);
+ if (end.failed()) FMT_THROW(format_error("failed to format time"));
+}
+
+template <typename Char, typename OutputIt,
+ FMT_ENABLE_IF(!std::is_same<Char, char>::value)>
+auto write(OutputIt out, const std::tm& time, const std::locale& loc,
+ char format, char modifier = 0) -> OutputIt {
+ auto&& buf = get_buffer<Char>(out);
+ do_write<Char>(buf, time, loc, format, modifier);
+ return get_iterator(buf, out);
+}
+
+template <typename Char, typename OutputIt,
+ FMT_ENABLE_IF(std::is_same<Char, char>::value)>
+auto write(OutputIt out, const std::tm& time, const std::locale& loc,
+ char format, char modifier = 0) -> OutputIt {
+ auto&& buf = basic_memory_buffer<Char>();
+ do_write<char>(buf, time, loc, format, modifier);
+ return write_encoded_tm_str(out, string_view(buf.data(), buf.size()), loc);
+}
+
+template <typename Rep1, typename Rep2>
+struct is_same_arithmetic_type
+ : public std::integral_constant<bool,
+ (std::is_integral<Rep1>::value &&
+ std::is_integral<Rep2>::value) ||
+ (std::is_floating_point<Rep1>::value &&
+ std::is_floating_point<Rep2>::value)> {
+};
+
+template <
+ typename To, typename FromRep, typename FromPeriod,
+ FMT_ENABLE_IF(is_same_arithmetic_type<FromRep, typename To::rep>::value)>
+auto fmt_duration_cast(std::chrono::duration<FromRep, FromPeriod> from) -> To {
+#if FMT_SAFE_DURATION_CAST
+ // Throwing version of safe_duration_cast is only available for
+ // integer to integer or float to float casts.
+ int ec;
+ To to = safe_duration_cast::safe_duration_cast<To>(from, ec);
+ if (ec) FMT_THROW(format_error("cannot format duration"));
+ return to;
+#else
+ // Standard duration cast, may overflow.
+ return std::chrono::duration_cast<To>(from);
+#endif
+}
+
+template <
+ typename To, typename FromRep, typename FromPeriod,
+ FMT_ENABLE_IF(!is_same_arithmetic_type<FromRep, typename To::rep>::value)>
+auto fmt_duration_cast(std::chrono::duration<FromRep, FromPeriod> from) -> To {
+ // Mixed integer <-> float cast is not supported by safe_duration_cast.
+ return std::chrono::duration_cast<To>(from);
+}
+
+template <typename Duration>
+auto to_time_t(
+ std::chrono::time_point<std::chrono::system_clock, Duration> time_point)
+ -> std::time_t {
+ // Cannot use std::chrono::system_clock::to_time_t since this would first
+ // require a cast to std::chrono::system_clock::time_point, which could
+ // overflow.
+ return fmt_duration_cast<std::chrono::duration<std::time_t>>(
+ time_point.time_since_epoch())
+ .count();
+}
+} // namespace detail
+
+FMT_BEGIN_EXPORT
+
+/**
+ Converts given time since epoch as ``std::time_t`` value into calendar time,
+ expressed in local time. Unlike ``std::localtime``, this function is
+ thread-safe on most platforms.
+ */
+inline auto localtime(std::time_t time) -> std::tm {
+ struct dispatcher {
+ std::time_t time_;
+ std::tm tm_;
+
+ dispatcher(std::time_t t) : time_(t) {}
+
+ auto run() -> bool {
+ using namespace fmt::detail;
+ return handle(localtime_r(&time_, &tm_));
+ }
+
+ auto handle(std::tm* tm) -> bool { return tm != nullptr; }
+
+ auto handle(detail::null<>) -> bool {
+ using namespace fmt::detail;
+ return fallback(localtime_s(&tm_, &time_));
+ }
+
+ auto fallback(int res) -> bool { return res == 0; }
+
+#if !FMT_MSC_VERSION
+ auto fallback(detail::null<>) -> bool {
+ using namespace fmt::detail;
+ std::tm* tm = std::localtime(&time_);
+ if (tm) tm_ = *tm;
+ return tm != nullptr;
+ }
+#endif
+ };
+ dispatcher lt(time);
+ // Too big time values may be unsupported.
+ if (!lt.run()) FMT_THROW(format_error("time_t value out of range"));
+ return lt.tm_;
+}
+
+#if FMT_USE_LOCAL_TIME
+template <typename Duration>
+inline auto localtime(std::chrono::local_time<Duration> time) -> std::tm {
+ return localtime(
+ detail::to_time_t(std::chrono::current_zone()->to_sys(time)));
+}
+#endif
+
+/**
+ Converts given time since epoch as ``std::time_t`` value into calendar time,
+ expressed in Coordinated Universal Time (UTC). Unlike ``std::gmtime``, this
+ function is thread-safe on most platforms.
+ */
+inline auto gmtime(std::time_t time) -> std::tm {
+ struct dispatcher {
+ std::time_t time_;
+ std::tm tm_;
+
+ dispatcher(std::time_t t) : time_(t) {}
+
+ auto run() -> bool {
+ using namespace fmt::detail;
+ return handle(gmtime_r(&time_, &tm_));
+ }
+
+ auto handle(std::tm* tm) -> bool { return tm != nullptr; }
+
+ auto handle(detail::null<>) -> bool {
+ using namespace fmt::detail;
+ return fallback(gmtime_s(&tm_, &time_));
+ }
+
+ auto fallback(int res) -> bool { return res == 0; }
+
+#if !FMT_MSC_VERSION
+ auto fallback(detail::null<>) -> bool {
+ std::tm* tm = std::gmtime(&time_);
+ if (tm) tm_ = *tm;
+ return tm != nullptr;
+ }
+#endif
+ };
+ auto gt = dispatcher(time);
+ // Too big time values may be unsupported.
+ if (!gt.run()) FMT_THROW(format_error("time_t value out of range"));
+ return gt.tm_;
+}
+
+template <typename Duration>
+inline auto gmtime(
+ std::chrono::time_point<std::chrono::system_clock, Duration> time_point)
+ -> std::tm {
+ return gmtime(detail::to_time_t(time_point));
+}
+
+namespace detail {
+
+// Writes two-digit numbers a, b and c separated by sep to buf.
+// The method by Pavel Novikov based on
+// https://johnnylee-sde.github.io/Fast-unsigned-integer-to-time-string/.
+inline void write_digit2_separated(char* buf, unsigned a, unsigned b,
+ unsigned c, char sep) {
+ unsigned long long digits =
+ a | (b << 24) | (static_cast<unsigned long long>(c) << 48);
+ // Convert each value to BCD.
+ // We have x = a * 10 + b and we want to convert it to BCD y = a * 16 + b.
+ // The difference is
+ // y - x = a * 6
+ // a can be found from x:
+ // a = floor(x / 10)
+ // then
+ // y = x + a * 6 = x + floor(x / 10) * 6
+ // floor(x / 10) is (x * 205) >> 11 (needs 16 bits).
+ digits += (((digits * 205) >> 11) & 0x000f00000f00000f) * 6;
+ // Put low nibbles to high bytes and high nibbles to low bytes.
+ digits = ((digits & 0x00f00000f00000f0) >> 4) |
+ ((digits & 0x000f00000f00000f) << 8);
+ auto usep = static_cast<unsigned long long>(sep);
+ // Add ASCII '0' to each digit byte and insert separators.
+ digits |= 0x3030003030003030 | (usep << 16) | (usep << 40);
+
+ constexpr const size_t len = 8;
+ if (const_check(is_big_endian())) {
+ char tmp[len];
+ std::memcpy(tmp, &digits, len);
+ std::reverse_copy(tmp, tmp + len, buf);
+ } else {
+ std::memcpy(buf, &digits, len);
+ }
+}
+
+template <typename Period>
+FMT_CONSTEXPR inline auto get_units() -> const char* {
+ if (std::is_same<Period, std::atto>::value) return "as";
+ if (std::is_same<Period, std::femto>::value) return "fs";
+ if (std::is_same<Period, std::pico>::value) return "ps";
+ if (std::is_same<Period, std::nano>::value) return "ns";
+ if (std::is_same<Period, std::micro>::value) return "µs";
+ if (std::is_same<Period, std::milli>::value) return "ms";
+ if (std::is_same<Period, std::centi>::value) return "cs";
+ if (std::is_same<Period, std::deci>::value) return "ds";
+ if (std::is_same<Period, std::ratio<1>>::value) return "s";
+ if (std::is_same<Period, std::deca>::value) return "das";
+ if (std::is_same<Period, std::hecto>::value) return "hs";
+ if (std::is_same<Period, std::kilo>::value) return "ks";
+ if (std::is_same<Period, std::mega>::value) return "Ms";
+ if (std::is_same<Period, std::giga>::value) return "Gs";
+ if (std::is_same<Period, std::tera>::value) return "Ts";
+ if (std::is_same<Period, std::peta>::value) return "Ps";
+ if (std::is_same<Period, std::exa>::value) return "Es";
+ if (std::is_same<Period, std::ratio<60>>::value) return "min";
+ if (std::is_same<Period, std::ratio<3600>>::value) return "h";
+ if (std::is_same<Period, std::ratio<86400>>::value) return "d";
+ return nullptr;
+}
+
+enum class numeric_system {
+ standard,
+ // Alternative numeric system, e.g. 十二 instead of 12 in ja_JP locale.
+ alternative
+};
+
+// Glibc extensions for formatting numeric values.
+enum class pad_type {
+ unspecified,
+ // Do not pad a numeric result string.
+ none,
+ // Pad a numeric result string with zeros even if the conversion specifier
+ // character uses space-padding by default.
+ zero,
+ // Pad a numeric result string with spaces.
+ space,
+};
+
+template <typename OutputIt>
+auto write_padding(OutputIt out, pad_type pad, int width) -> OutputIt {
+ if (pad == pad_type::none) return out;
+ return std::fill_n(out, width, pad == pad_type::space ? ' ' : '0');
+}
+
+template <typename OutputIt>
+auto write_padding(OutputIt out, pad_type pad) -> OutputIt {
+ if (pad != pad_type::none) *out++ = pad == pad_type::space ? ' ' : '0';
+ return out;
+}
+
+// Parses a put_time-like format string and invokes handler actions.
+template <typename Char, typename Handler>
+FMT_CONSTEXPR auto parse_chrono_format(const Char* begin, const Char* end,
+ Handler&& handler) -> const Char* {
+ if (begin == end || *begin == '}') return begin;
+ if (*begin != '%') FMT_THROW(format_error("invalid format"));
+ auto ptr = begin;
+ pad_type pad = pad_type::unspecified;
+ while (ptr != end) {
+ auto c = *ptr;
+ if (c == '}') break;
+ if (c != '%') {
+ ++ptr;
+ continue;
+ }
+ if (begin != ptr) handler.on_text(begin, ptr);
+ ++ptr; // consume '%'
+ if (ptr == end) FMT_THROW(format_error("invalid format"));
+ c = *ptr;
+ switch (c) {
+ case '_':
+ pad = pad_type::space;
+ ++ptr;
+ break;
+ case '-':
+ pad = pad_type::none;
+ ++ptr;
+ break;
+ case '0':
+ pad = pad_type::zero;
+ ++ptr;
+ break;
+ }
+ if (ptr == end) FMT_THROW(format_error("invalid format"));
+ c = *ptr++;
+ switch (c) {
+ case '%':
+ handler.on_text(ptr - 1, ptr);
+ break;
+ case 'n': {
+ const Char newline[] = {'\n'};
+ handler.on_text(newline, newline + 1);
+ break;
+ }
+ case 't': {
+ const Char tab[] = {'\t'};
+ handler.on_text(tab, tab + 1);
+ break;
+ }
+ // Year:
+ case 'Y':
+ handler.on_year(numeric_system::standard);
+ break;
+ case 'y':
+ handler.on_short_year(numeric_system::standard);
+ break;
+ case 'C':
+ handler.on_century(numeric_system::standard);
+ break;
+ case 'G':
+ handler.on_iso_week_based_year();
+ break;
+ case 'g':
+ handler.on_iso_week_based_short_year();
+ break;
+ // Day of the week:
+ case 'a':
+ handler.on_abbr_weekday();
+ break;
+ case 'A':
+ handler.on_full_weekday();
+ break;
+ case 'w':
+ handler.on_dec0_weekday(numeric_system::standard);
+ break;
+ case 'u':
+ handler.on_dec1_weekday(numeric_system::standard);
+ break;
+ // Month:
+ case 'b':
+ case 'h':
+ handler.on_abbr_month();
+ break;
+ case 'B':
+ handler.on_full_month();
+ break;
+ case 'm':
+ handler.on_dec_month(numeric_system::standard);
+ break;
+ // Day of the year/month:
+ case 'U':
+ handler.on_dec0_week_of_year(numeric_system::standard);
+ break;
+ case 'W':
+ handler.on_dec1_week_of_year(numeric_system::standard);
+ break;
+ case 'V':
+ handler.on_iso_week_of_year(numeric_system::standard);
+ break;
+ case 'j':
+ handler.on_day_of_year();
+ break;
+ case 'd':
+ handler.on_day_of_month(numeric_system::standard);
+ break;
+ case 'e':
+ handler.on_day_of_month_space(numeric_system::standard);
+ break;
+ // Hour, minute, second:
+ case 'H':
+ handler.on_24_hour(numeric_system::standard, pad);
+ break;
+ case 'I':
+ handler.on_12_hour(numeric_system::standard, pad);
+ break;
+ case 'M':
+ handler.on_minute(numeric_system::standard, pad);
+ break;
+ case 'S':
+ handler.on_second(numeric_system::standard, pad);
+ break;
+ // Other:
+ case 'c':
+ handler.on_datetime(numeric_system::standard);
+ break;
+ case 'x':
+ handler.on_loc_date(numeric_system::standard);
+ break;
+ case 'X':
+ handler.on_loc_time(numeric_system::standard);
+ break;
+ case 'D':
+ handler.on_us_date();
+ break;
+ case 'F':
+ handler.on_iso_date();
+ break;
+ case 'r':
+ handler.on_12_hour_time();
+ break;
+ case 'R':
+ handler.on_24_hour_time();
+ break;
+ case 'T':
+ handler.on_iso_time();
+ break;
+ case 'p':
+ handler.on_am_pm();
+ break;
+ case 'Q':
+ handler.on_duration_value();
+ break;
+ case 'q':
+ handler.on_duration_unit();
+ break;
+ case 'z':
+ handler.on_utc_offset(numeric_system::standard);
+ break;
+ case 'Z':
+ handler.on_tz_name();
+ break;
+ // Alternative representation:
+ case 'E': {
+ if (ptr == end) FMT_THROW(format_error("invalid format"));
+ c = *ptr++;
+ switch (c) {
+ case 'Y':
+ handler.on_year(numeric_system::alternative);
+ break;
+ case 'y':
+ handler.on_offset_year();
+ break;
+ case 'C':
+ handler.on_century(numeric_system::alternative);
+ break;
+ case 'c':
+ handler.on_datetime(numeric_system::alternative);
+ break;
+ case 'x':
+ handler.on_loc_date(numeric_system::alternative);
+ break;
+ case 'X':
+ handler.on_loc_time(numeric_system::alternative);
+ break;
+ case 'z':
+ handler.on_utc_offset(numeric_system::alternative);
+ break;
+ default:
+ FMT_THROW(format_error("invalid format"));
+ }
+ break;
+ }
+ case 'O':
+ if (ptr == end) FMT_THROW(format_error("invalid format"));
+ c = *ptr++;
+ switch (c) {
+ case 'y':
+ handler.on_short_year(numeric_system::alternative);
+ break;
+ case 'm':
+ handler.on_dec_month(numeric_system::alternative);
+ break;
+ case 'U':
+ handler.on_dec0_week_of_year(numeric_system::alternative);
+ break;
+ case 'W':
+ handler.on_dec1_week_of_year(numeric_system::alternative);
+ break;
+ case 'V':
+ handler.on_iso_week_of_year(numeric_system::alternative);
+ break;
+ case 'd':
+ handler.on_day_of_month(numeric_system::alternative);
+ break;
+ case 'e':
+ handler.on_day_of_month_space(numeric_system::alternative);
+ break;
+ case 'w':
+ handler.on_dec0_weekday(numeric_system::alternative);
+ break;
+ case 'u':
+ handler.on_dec1_weekday(numeric_system::alternative);
+ break;
+ case 'H':
+ handler.on_24_hour(numeric_system::alternative, pad);
+ break;
+ case 'I':
+ handler.on_12_hour(numeric_system::alternative, pad);
+ break;
+ case 'M':
+ handler.on_minute(numeric_system::alternative, pad);
+ break;
+ case 'S':
+ handler.on_second(numeric_system::alternative, pad);
+ break;
+ case 'z':
+ handler.on_utc_offset(numeric_system::alternative);
+ break;
+ default:
+ FMT_THROW(format_error("invalid format"));
+ }
+ break;
+ default:
+ FMT_THROW(format_error("invalid format"));
+ }
+ begin = ptr;
+ }
+ if (begin != ptr) handler.on_text(begin, ptr);
+ return ptr;
+}
+
+template <typename Derived> struct null_chrono_spec_handler {
+ FMT_CONSTEXPR void unsupported() {
+ static_cast<Derived*>(this)->unsupported();
+ }
+ FMT_CONSTEXPR void on_year(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_short_year(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_offset_year() { unsupported(); }
+ FMT_CONSTEXPR void on_century(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_iso_week_based_year() { unsupported(); }
+ FMT_CONSTEXPR void on_iso_week_based_short_year() { unsupported(); }
+ FMT_CONSTEXPR void on_abbr_weekday() { unsupported(); }
+ FMT_CONSTEXPR void on_full_weekday() { unsupported(); }
+ FMT_CONSTEXPR void on_dec0_weekday(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_dec1_weekday(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_abbr_month() { unsupported(); }
+ FMT_CONSTEXPR void on_full_month() { unsupported(); }
+ FMT_CONSTEXPR void on_dec_month(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_dec0_week_of_year(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_dec1_week_of_year(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_iso_week_of_year(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_day_of_year() { unsupported(); }
+ FMT_CONSTEXPR void on_day_of_month(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_day_of_month_space(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_24_hour(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_12_hour(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_minute(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_second(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_datetime(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_loc_date(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_loc_time(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_us_date() { unsupported(); }
+ FMT_CONSTEXPR void on_iso_date() { unsupported(); }
+ FMT_CONSTEXPR void on_12_hour_time() { unsupported(); }
+ FMT_CONSTEXPR void on_24_hour_time() { unsupported(); }
+ FMT_CONSTEXPR void on_iso_time() { unsupported(); }
+ FMT_CONSTEXPR void on_am_pm() { unsupported(); }
+ FMT_CONSTEXPR void on_duration_value() { unsupported(); }
+ FMT_CONSTEXPR void on_duration_unit() { unsupported(); }
+ FMT_CONSTEXPR void on_utc_offset(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_tz_name() { unsupported(); }
+};
+
+struct tm_format_checker : null_chrono_spec_handler<tm_format_checker> {
+ FMT_NORETURN void unsupported() { FMT_THROW(format_error("no format")); }
+
+ template <typename Char>
+ FMT_CONSTEXPR void on_text(const Char*, const Char*) {}
+ FMT_CONSTEXPR void on_year(numeric_system) {}
+ FMT_CONSTEXPR void on_short_year(numeric_system) {}
+ FMT_CONSTEXPR void on_offset_year() {}
+ FMT_CONSTEXPR void on_century(numeric_system) {}
+ FMT_CONSTEXPR void on_iso_week_based_year() {}
+ FMT_CONSTEXPR void on_iso_week_based_short_year() {}
+ FMT_CONSTEXPR void on_abbr_weekday() {}
+ FMT_CONSTEXPR void on_full_weekday() {}
+ FMT_CONSTEXPR void on_dec0_weekday(numeric_system) {}
+ FMT_CONSTEXPR void on_dec1_weekday(numeric_system) {}
+ FMT_CONSTEXPR void on_abbr_month() {}
+ FMT_CONSTEXPR void on_full_month() {}
+ FMT_CONSTEXPR void on_dec_month(numeric_system) {}
+ FMT_CONSTEXPR void on_dec0_week_of_year(numeric_system) {}
+ FMT_CONSTEXPR void on_dec1_week_of_year(numeric_system) {}
+ FMT_CONSTEXPR void on_iso_week_of_year(numeric_system) {}
+ FMT_CONSTEXPR void on_day_of_year() {}
+ FMT_CONSTEXPR void on_day_of_month(numeric_system) {}
+ FMT_CONSTEXPR void on_day_of_month_space(numeric_system) {}
+ FMT_CONSTEXPR void on_24_hour(numeric_system, pad_type) {}
+ FMT_CONSTEXPR void on_12_hour(numeric_system, pad_type) {}
+ FMT_CONSTEXPR void on_minute(numeric_system, pad_type) {}
+ FMT_CONSTEXPR void on_second(numeric_system, pad_type) {}
+ FMT_CONSTEXPR void on_datetime(numeric_system) {}
+ FMT_CONSTEXPR void on_loc_date(numeric_system) {}
+ FMT_CONSTEXPR void on_loc_time(numeric_system) {}
+ FMT_CONSTEXPR void on_us_date() {}
+ FMT_CONSTEXPR void on_iso_date() {}
+ FMT_CONSTEXPR void on_12_hour_time() {}
+ FMT_CONSTEXPR void on_24_hour_time() {}
+ FMT_CONSTEXPR void on_iso_time() {}
+ FMT_CONSTEXPR void on_am_pm() {}
+ FMT_CONSTEXPR void on_utc_offset(numeric_system) {}
+ FMT_CONSTEXPR void on_tz_name() {}
+};
+
+inline auto tm_wday_full_name(int wday) -> const char* {
+ static constexpr const char* full_name_list[] = {
+ "Sunday", "Monday", "Tuesday", "Wednesday",
+ "Thursday", "Friday", "Saturday"};
+ return wday >= 0 && wday <= 6 ? full_name_list[wday] : "?";
+}
+inline auto tm_wday_short_name(int wday) -> const char* {
+ static constexpr const char* short_name_list[] = {"Sun", "Mon", "Tue", "Wed",
+ "Thu", "Fri", "Sat"};
+ return wday >= 0 && wday <= 6 ? short_name_list[wday] : "???";
+}
+
+inline auto tm_mon_full_name(int mon) -> const char* {
+ static constexpr const char* full_name_list[] = {
+ "January", "February", "March", "April", "May", "June",
+ "July", "August", "September", "October", "November", "December"};
+ return mon >= 0 && mon <= 11 ? full_name_list[mon] : "?";
+}
+inline auto tm_mon_short_name(int mon) -> const char* {
+ static constexpr const char* short_name_list[] = {
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
+ };
+ return mon >= 0 && mon <= 11 ? short_name_list[mon] : "???";
+}
+
+template <typename T, typename = void>
+struct has_member_data_tm_gmtoff : std::false_type {};
+template <typename T>
+struct has_member_data_tm_gmtoff<T, void_t<decltype(T::tm_gmtoff)>>
+ : std::true_type {};
+
+template <typename T, typename = void>
+struct has_member_data_tm_zone : std::false_type {};
+template <typename T>
+struct has_member_data_tm_zone<T, void_t<decltype(T::tm_zone)>>
+ : std::true_type {};
+
+#if FMT_USE_TZSET
+inline void tzset_once() {
+ static bool init = []() -> bool {
+ _tzset();
+ return true;
+ }();
+ ignore_unused(init);
+}
+#endif
+
+// Converts value to Int and checks that it's in the range [0, upper).
+template <typename T, typename Int, FMT_ENABLE_IF(std::is_integral<T>::value)>
+inline auto to_nonnegative_int(T value, Int upper) -> Int {
+ if (!std::is_unsigned<Int>::value &&
+ (value < 0 || to_unsigned(value) > to_unsigned(upper))) {
+ FMT_THROW(fmt::format_error("chrono value is out of range"));
+ }
+ return static_cast<Int>(value);
+}
+template <typename T, typename Int, FMT_ENABLE_IF(!std::is_integral<T>::value)>
+inline auto to_nonnegative_int(T value, Int upper) -> Int {
+ if (value < 0 || value > static_cast<T>(upper))
+ FMT_THROW(format_error("invalid value"));
+ return static_cast<Int>(value);
+}
+
+constexpr auto pow10(std::uint32_t n) -> long long {
+ return n == 0 ? 1 : 10 * pow10(n - 1);
+}
+
+// Counts the number of fractional digits in the range [0, 18] according to the
+// C++20 spec. If more than 18 fractional digits are required then returns 6 for
+// microseconds precision.
+template <long long Num, long long Den, int N = 0,
+ bool Enabled = (N < 19) && (Num <= max_value<long long>() / 10)>
+struct count_fractional_digits {
+ static constexpr int value =
+ Num % Den == 0 ? N : count_fractional_digits<Num * 10, Den, N + 1>::value;
+};
+
+// Base case that doesn't instantiate any more templates
+// in order to avoid overflow.
+template <long long Num, long long Den, int N>
+struct count_fractional_digits<Num, Den, N, false> {
+ static constexpr int value = (Num % Den == 0) ? N : 6;
+};
+
+// Format subseconds which are given as an integer type with an appropriate
+// number of digits.
+template <typename Char, typename OutputIt, typename Duration>
+void write_fractional_seconds(OutputIt& out, Duration d, int precision = -1) {
+ constexpr auto num_fractional_digits =
+ count_fractional_digits<Duration::period::num,
+ Duration::period::den>::value;
+
+ using subsecond_precision = std::chrono::duration<
+ typename std::common_type<typename Duration::rep,
+ std::chrono::seconds::rep>::type,
+ std::ratio<1, detail::pow10(num_fractional_digits)>>;
+
+ const auto fractional = d - fmt_duration_cast<std::chrono::seconds>(d);
+ const auto subseconds =
+ std::chrono::treat_as_floating_point<
+ typename subsecond_precision::rep>::value
+ ? fractional.count()
+ : fmt_duration_cast<subsecond_precision>(fractional).count();
+ auto n = static_cast<uint32_or_64_or_128_t<long long>>(subseconds);
+ const int num_digits = detail::count_digits(n);
+
+ int leading_zeroes = (std::max)(0, num_fractional_digits - num_digits);
+ if (precision < 0) {
+ FMT_ASSERT(!std::is_floating_point<typename Duration::rep>::value, "");
+ if (std::ratio_less<typename subsecond_precision::period,
+ std::chrono::seconds::period>::value) {
+ *out++ = '.';
+ out = std::fill_n(out, leading_zeroes, '0');
+ out = format_decimal<Char>(out, n, num_digits).end;
+ }
+ } else {
+ *out++ = '.';
+ leading_zeroes = (std::min)(leading_zeroes, precision);
+ out = std::fill_n(out, leading_zeroes, '0');
+ int remaining = precision - leading_zeroes;
+ if (remaining != 0 && remaining < num_digits) {
+ n /= to_unsigned(detail::pow10(to_unsigned(num_digits - remaining)));
+ out = format_decimal<Char>(out, n, remaining).end;
+ return;
+ }
+ out = format_decimal<Char>(out, n, num_digits).end;
+ remaining -= num_digits;
+ out = std::fill_n(out, remaining, '0');
+ }
+}
+
+// Format subseconds which are given as a floating point type with an
+// appropriate number of digits. We cannot pass the Duration here, as we
+// explicitly need to pass the Rep value in the chrono_formatter.
+template <typename Duration>
+void write_floating_seconds(memory_buffer& buf, Duration duration,
+ int num_fractional_digits = -1) {
+ using rep = typename Duration::rep;
+ FMT_ASSERT(std::is_floating_point<rep>::value, "");
+
+ auto val = duration.count();
+
+ if (num_fractional_digits < 0) {
+ // For `std::round` with fallback to `round`:
+ // On some toolchains `std::round` is not available (e.g. GCC 6).
+ using namespace std;
+ num_fractional_digits =
+ count_fractional_digits<Duration::period::num,
+ Duration::period::den>::value;
+ if (num_fractional_digits < 6 && static_cast<rep>(round(val)) != val)
+ num_fractional_digits = 6;
+ }
+
+ fmt::format_to(std::back_inserter(buf), FMT_STRING("{:.{}f}"),
+ std::fmod(val * static_cast<rep>(Duration::period::num) /
+ static_cast<rep>(Duration::period::den),
+ static_cast<rep>(60)),
+ num_fractional_digits);
+}
+
+template <typename OutputIt, typename Char,
+ typename Duration = std::chrono::seconds>
+class tm_writer {
+ private:
+ static constexpr int days_per_week = 7;
+
+ const std::locale& loc_;
+ const bool is_classic_;
+ OutputIt out_;
+ const Duration* subsecs_;
+ const std::tm& tm_;
+
+ auto tm_sec() const noexcept -> int {
+ FMT_ASSERT(tm_.tm_sec >= 0 && tm_.tm_sec <= 61, "");
+ return tm_.tm_sec;
+ }
+ auto tm_min() const noexcept -> int {
+ FMT_ASSERT(tm_.tm_min >= 0 && tm_.tm_min <= 59, "");
+ return tm_.tm_min;
+ }
+ auto tm_hour() const noexcept -> int {
+ FMT_ASSERT(tm_.tm_hour >= 0 && tm_.tm_hour <= 23, "");
+ return tm_.tm_hour;
+ }
+ auto tm_mday() const noexcept -> int {
+ FMT_ASSERT(tm_.tm_mday >= 1 && tm_.tm_mday <= 31, "");
+ return tm_.tm_mday;
+ }
+ auto tm_mon() const noexcept -> int {
+ FMT_ASSERT(tm_.tm_mon >= 0 && tm_.tm_mon <= 11, "");
+ return tm_.tm_mon;
+ }
+ auto tm_year() const noexcept -> long long { return 1900ll + tm_.tm_year; }
+ auto tm_wday() const noexcept -> int {
+ FMT_ASSERT(tm_.tm_wday >= 0 && tm_.tm_wday <= 6, "");
+ return tm_.tm_wday;
+ }
+ auto tm_yday() const noexcept -> int {
+ FMT_ASSERT(tm_.tm_yday >= 0 && tm_.tm_yday <= 365, "");
+ return tm_.tm_yday;
+ }
+
+ auto tm_hour12() const noexcept -> int {
+ const auto h = tm_hour();
+ const auto z = h < 12 ? h : h - 12;
+ return z == 0 ? 12 : z;
+ }
+
+ // POSIX and the C Standard are unclear or inconsistent about what %C and %y
+ // do if the year is negative or exceeds 9999. Use the convention that %C
+ // concatenated with %y yields the same output as %Y, and that %Y contains at
+ // least 4 characters, with more only if necessary.
+ auto split_year_lower(long long year) const noexcept -> int {
+ auto l = year % 100;
+ if (l < 0) l = -l; // l in [0, 99]
+ return static_cast<int>(l);
+ }
+
+ // Algorithm: https://en.wikipedia.org/wiki/ISO_week_date.
+ auto iso_year_weeks(long long curr_year) const noexcept -> int {
+ const auto prev_year = curr_year - 1;
+ const auto curr_p =
+ (curr_year + curr_year / 4 - curr_year / 100 + curr_year / 400) %
+ days_per_week;
+ const auto prev_p =
+ (prev_year + prev_year / 4 - prev_year / 100 + prev_year / 400) %
+ days_per_week;
+ return 52 + ((curr_p == 4 || prev_p == 3) ? 1 : 0);
+ }
+ auto iso_week_num(int tm_yday, int tm_wday) const noexcept -> int {
+ return (tm_yday + 11 - (tm_wday == 0 ? days_per_week : tm_wday)) /
+ days_per_week;
+ }
+ auto tm_iso_week_year() const noexcept -> long long {
+ const auto year = tm_year();
+ const auto w = iso_week_num(tm_yday(), tm_wday());
+ if (w < 1) return year - 1;
+ if (w > iso_year_weeks(year)) return year + 1;
+ return year;
+ }
+ auto tm_iso_week_of_year() const noexcept -> int {
+ const auto year = tm_year();
+ const auto w = iso_week_num(tm_yday(), tm_wday());
+ if (w < 1) return iso_year_weeks(year - 1);
+ if (w > iso_year_weeks(year)) return 1;
+ return w;
+ }
+
+ void write1(int value) {
+ *out_++ = static_cast<char>('0' + to_unsigned(value) % 10);
+ }
+ void write2(int value) {
+ const char* d = digits2(to_unsigned(value) % 100);
+ *out_++ = *d++;
+ *out_++ = *d;
+ }
+ void write2(int value, pad_type pad) {
+ unsigned int v = to_unsigned(value) % 100;
+ if (v >= 10) {
+ const char* d = digits2(v);
+ *out_++ = *d++;
+ *out_++ = *d;
+ } else {
+ out_ = detail::write_padding(out_, pad);
+ *out_++ = static_cast<char>('0' + v);
+ }
+ }
+
+ void write_year_extended(long long year) {
+ // At least 4 characters.
+ int width = 4;
+ if (year < 0) {
+ *out_++ = '-';
+ year = 0 - year;
+ --width;
+ }
+ uint32_or_64_or_128_t<long long> n = to_unsigned(year);
+ const int num_digits = count_digits(n);
+ if (width > num_digits) out_ = std::fill_n(out_, width - num_digits, '0');
+ out_ = format_decimal<Char>(out_, n, num_digits).end;
+ }
+ void write_year(long long year) {
+ if (year >= 0 && year < 10000) {
+ write2(static_cast<int>(year / 100));
+ write2(static_cast<int>(year % 100));
+ } else {
+ write_year_extended(year);
+ }
+ }
+
+ void write_utc_offset(long offset, numeric_system ns) {
+ if (offset < 0) {
+ *out_++ = '-';
+ offset = -offset;
+ } else {
+ *out_++ = '+';
+ }
+ offset /= 60;
+ write2(static_cast<int>(offset / 60));
+ if (ns != numeric_system::standard) *out_++ = ':';
+ write2(static_cast<int>(offset % 60));
+ }
+ template <typename T, FMT_ENABLE_IF(has_member_data_tm_gmtoff<T>::value)>
+ void format_utc_offset_impl(const T& tm, numeric_system ns) {
+ write_utc_offset(tm.tm_gmtoff, ns);
+ }
+ template <typename T, FMT_ENABLE_IF(!has_member_data_tm_gmtoff<T>::value)>
+ void format_utc_offset_impl(const T& tm, numeric_system ns) {
+#if defined(_WIN32) && defined(_UCRT)
+# if FMT_USE_TZSET
+ tzset_once();
+# endif
+ long offset = 0;
+ _get_timezone(&offset);
+ if (tm.tm_isdst) {
+ long dstbias = 0;
+ _get_dstbias(&dstbias);
+ offset += dstbias;
+ }
+ write_utc_offset(-offset, ns);
+#else
+ if (ns == numeric_system::standard) return format_localized('z');
+
+ // Extract timezone offset from timezone conversion functions.
+ std::tm gtm = tm;
+ std::time_t gt = std::mktime(>m);
+ std::tm ltm = gmtime(gt);
+ std::time_t lt = std::mktime(<m);
+ long offset = gt - lt;
+ write_utc_offset(offset, ns);
+#endif
+ }
+
+ template <typename T, FMT_ENABLE_IF(has_member_data_tm_zone<T>::value)>
+ void format_tz_name_impl(const T& tm) {
+ if (is_classic_)
+ out_ = write_tm_str<Char>(out_, tm.tm_zone, loc_);
+ else
+ format_localized('Z');
+ }
+ template <typename T, FMT_ENABLE_IF(!has_member_data_tm_zone<T>::value)>
+ void format_tz_name_impl(const T&) {
+ format_localized('Z');
+ }
+
+ void format_localized(char format, char modifier = 0) {
+ out_ = write<Char>(out_, tm_, loc_, format, modifier);
+ }
+
+ public:
+ tm_writer(const std::locale& loc, OutputIt out, const std::tm& tm,
+ const Duration* subsecs = nullptr)
+ : loc_(loc),
+ is_classic_(loc_ == get_classic_locale()),
+ out_(out),
+ subsecs_(subsecs),
+ tm_(tm) {}
+
+ auto out() const -> OutputIt { return out_; }
+
+ FMT_CONSTEXPR void on_text(const Char* begin, const Char* end) {
+ out_ = copy_str<Char>(begin, end, out_);
+ }
+
+ void on_abbr_weekday() {
+ if (is_classic_)
+ out_ = write(out_, tm_wday_short_name(tm_wday()));
+ else
+ format_localized('a');
+ }
+ void on_full_weekday() {
+ if (is_classic_)
+ out_ = write(out_, tm_wday_full_name(tm_wday()));
+ else
+ format_localized('A');
+ }
+ void on_dec0_weekday(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard) return write1(tm_wday());
+ format_localized('w', 'O');
+ }
+ void on_dec1_weekday(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard) {
+ auto wday = tm_wday();
+ write1(wday == 0 ? days_per_week : wday);
+ } else {
+ format_localized('u', 'O');
+ }
+ }
+
+ void on_abbr_month() {
+ if (is_classic_)
+ out_ = write(out_, tm_mon_short_name(tm_mon()));
+ else
+ format_localized('b');
+ }
+ void on_full_month() {
+ if (is_classic_)
+ out_ = write(out_, tm_mon_full_name(tm_mon()));
+ else
+ format_localized('B');
+ }
+
+ void on_datetime(numeric_system ns) {
+ if (is_classic_) {
+ on_abbr_weekday();
+ *out_++ = ' ';
+ on_abbr_month();
+ *out_++ = ' ';
+ on_day_of_month_space(numeric_system::standard);
+ *out_++ = ' ';
+ on_iso_time();
+ *out_++ = ' ';
+ on_year(numeric_system::standard);
+ } else {
+ format_localized('c', ns == numeric_system::standard ? '\0' : 'E');
+ }
+ }
+ void on_loc_date(numeric_system ns) {
+ if (is_classic_)
+ on_us_date();
+ else
+ format_localized('x', ns == numeric_system::standard ? '\0' : 'E');
+ }
+ void on_loc_time(numeric_system ns) {
+ if (is_classic_)
+ on_iso_time();
+ else
+ format_localized('X', ns == numeric_system::standard ? '\0' : 'E');
+ }
+ void on_us_date() {
+ char buf[8];
+ write_digit2_separated(buf, to_unsigned(tm_mon() + 1),
+ to_unsigned(tm_mday()),
+ to_unsigned(split_year_lower(tm_year())), '/');
+ out_ = copy_str<Char>(std::begin(buf), std::end(buf), out_);
+ }
+ void on_iso_date() {
+ auto year = tm_year();
+ char buf[10];
+ size_t offset = 0;
+ if (year >= 0 && year < 10000) {
+ copy2(buf, digits2(static_cast<size_t>(year / 100)));
+ } else {
+ offset = 4;
+ write_year_extended(year);
+ year = 0;
+ }
+ write_digit2_separated(buf + 2, static_cast<unsigned>(year % 100),
+ to_unsigned(tm_mon() + 1), to_unsigned(tm_mday()),
+ '-');
+ out_ = copy_str<Char>(std::begin(buf) + offset, std::end(buf), out_);
+ }
+
+ void on_utc_offset(numeric_system ns) { format_utc_offset_impl(tm_, ns); }
+ void on_tz_name() { format_tz_name_impl(tm_); }
+
+ void on_year(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard)
+ return write_year(tm_year());
+ format_localized('Y', 'E');
+ }
+ void on_short_year(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard)
+ return write2(split_year_lower(tm_year()));
+ format_localized('y', 'O');
+ }
+ void on_offset_year() {
+ if (is_classic_) return write2(split_year_lower(tm_year()));
+ format_localized('y', 'E');
+ }
+
+ void on_century(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard) {
+ auto year = tm_year();
+ auto upper = year / 100;
+ if (year >= -99 && year < 0) {
+ // Zero upper on negative year.
+ *out_++ = '-';
+ *out_++ = '0';
+ } else if (upper >= 0 && upper < 100) {
+ write2(static_cast<int>(upper));
+ } else {
+ out_ = write<Char>(out_, upper);
+ }
+ } else {
+ format_localized('C', 'E');
+ }
+ }
+
+ void on_dec_month(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard)
+ return write2(tm_mon() + 1);
+ format_localized('m', 'O');
+ }
+
+ void on_dec0_week_of_year(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard)
+ return write2((tm_yday() + days_per_week - tm_wday()) / days_per_week);
+ format_localized('U', 'O');
+ }
+ void on_dec1_week_of_year(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard) {
+ auto wday = tm_wday();
+ write2((tm_yday() + days_per_week -
+ (wday == 0 ? (days_per_week - 1) : (wday - 1))) /
+ days_per_week);
+ } else {
+ format_localized('W', 'O');
+ }
+ }
+ void on_iso_week_of_year(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard)
+ return write2(tm_iso_week_of_year());
+ format_localized('V', 'O');
+ }
+
+ void on_iso_week_based_year() { write_year(tm_iso_week_year()); }
+ void on_iso_week_based_short_year() {
+ write2(split_year_lower(tm_iso_week_year()));
+ }
+
+ void on_day_of_year() {
+ auto yday = tm_yday() + 1;
+ write1(yday / 100);
+ write2(yday % 100);
+ }
+ void on_day_of_month(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard) return write2(tm_mday());
+ format_localized('d', 'O');
+ }
+ void on_day_of_month_space(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard) {
+ auto mday = to_unsigned(tm_mday()) % 100;
+ const char* d2 = digits2(mday);
+ *out_++ = mday < 10 ? ' ' : d2[0];
+ *out_++ = d2[1];
+ } else {
+ format_localized('e', 'O');
+ }
+ }
+
+ void on_24_hour(numeric_system ns, pad_type pad) {
+ if (is_classic_ || ns == numeric_system::standard)
+ return write2(tm_hour(), pad);
+ format_localized('H', 'O');
+ }
+ void on_12_hour(numeric_system ns, pad_type pad) {
+ if (is_classic_ || ns == numeric_system::standard)
+ return write2(tm_hour12(), pad);
+ format_localized('I', 'O');
+ }
+ void on_minute(numeric_system ns, pad_type pad) {
+ if (is_classic_ || ns == numeric_system::standard)
+ return write2(tm_min(), pad);
+ format_localized('M', 'O');
+ }
+
+ void on_second(numeric_system ns, pad_type pad) {
+ if (is_classic_ || ns == numeric_system::standard) {
+ write2(tm_sec(), pad);
+ if (subsecs_) {
+ if (std::is_floating_point<typename Duration::rep>::value) {
+ auto buf = memory_buffer();
+ write_floating_seconds(buf, *subsecs_);
+ if (buf.size() > 1) {
+ // Remove the leading "0", write something like ".123".
+ out_ = std::copy(buf.begin() + 1, buf.end(), out_);
+ }
+ } else {
+ write_fractional_seconds<Char>(out_, *subsecs_);
+ }
+ }
+ } else {
+ // Currently no formatting of subseconds when a locale is set.
+ format_localized('S', 'O');
+ }
+ }
+
+ void on_12_hour_time() {
+ if (is_classic_) {
+ char buf[8];
+ write_digit2_separated(buf, to_unsigned(tm_hour12()),
+ to_unsigned(tm_min()), to_unsigned(tm_sec()), ':');
+ out_ = copy_str<Char>(std::begin(buf), std::end(buf), out_);
+ *out_++ = ' ';
+ on_am_pm();
+ } else {
+ format_localized('r');
+ }
+ }
+ void on_24_hour_time() {
+ write2(tm_hour());
+ *out_++ = ':';
+ write2(tm_min());
+ }
+ void on_iso_time() {
+ on_24_hour_time();
+ *out_++ = ':';
+ on_second(numeric_system::standard, pad_type::unspecified);
+ }
+
+ void on_am_pm() {
+ if (is_classic_) {
+ *out_++ = tm_hour() < 12 ? 'A' : 'P';
+ *out_++ = 'M';
+ } else {
+ format_localized('p');
+ }
+ }
+
+ // These apply to chrono durations but not tm.
+ void on_duration_value() {}
+ void on_duration_unit() {}
+};
+
+struct chrono_format_checker : null_chrono_spec_handler<chrono_format_checker> {
+ bool has_precision_integral = false;
+
+ FMT_NORETURN void unsupported() { FMT_THROW(format_error("no date")); }
+
+ template <typename Char>
+ FMT_CONSTEXPR void on_text(const Char*, const Char*) {}
+ FMT_CONSTEXPR void on_day_of_year() {}
+ FMT_CONSTEXPR void on_24_hour(numeric_system, pad_type) {}
+ FMT_CONSTEXPR void on_12_hour(numeric_system, pad_type) {}
+ FMT_CONSTEXPR void on_minute(numeric_system, pad_type) {}
+ FMT_CONSTEXPR void on_second(numeric_system, pad_type) {}
+ FMT_CONSTEXPR void on_12_hour_time() {}
+ FMT_CONSTEXPR void on_24_hour_time() {}
+ FMT_CONSTEXPR void on_iso_time() {}
+ FMT_CONSTEXPR void on_am_pm() {}
+ FMT_CONSTEXPR void on_duration_value() const {
+ if (has_precision_integral) {
+ FMT_THROW(format_error("precision not allowed for this argument type"));
+ }
+ }
+ FMT_CONSTEXPR void on_duration_unit() {}
+};
+
+template <typename T,
+ FMT_ENABLE_IF(std::is_integral<T>::value&& has_isfinite<T>::value)>
+inline auto isfinite(T) -> bool {
+ return true;
+}
+
+template <typename T, FMT_ENABLE_IF(std::is_integral<T>::value)>
+inline auto mod(T x, int y) -> T {
+ return x % static_cast<T>(y);
+}
+template <typename T, FMT_ENABLE_IF(std::is_floating_point<T>::value)>
+inline auto mod(T x, int y) -> T {
+ return std::fmod(x, static_cast<T>(y));
+}
+
+// If T is an integral type, maps T to its unsigned counterpart, otherwise
+// leaves it unchanged (unlike std::make_unsigned).
+template <typename T, bool INTEGRAL = std::is_integral<T>::value>
+struct make_unsigned_or_unchanged {
+ using type = T;
+};
+
+template <typename T> struct make_unsigned_or_unchanged<T, true> {
+ using type = typename std::make_unsigned<T>::type;
+};
+
+template <typename Rep, typename Period,
+ FMT_ENABLE_IF(std::is_integral<Rep>::value)>
+inline auto get_milliseconds(std::chrono::duration<Rep, Period> d)
+ -> std::chrono::duration<Rep, std::milli> {
+ // this may overflow and/or the result may not fit in the
+ // target type.
+#if FMT_SAFE_DURATION_CAST
+ using CommonSecondsType =
+ typename std::common_type<decltype(d), std::chrono::seconds>::type;
+ const auto d_as_common = fmt_duration_cast<CommonSecondsType>(d);
+ const auto d_as_whole_seconds =
+ fmt_duration_cast<std::chrono::seconds>(d_as_common);
+ // this conversion should be nonproblematic
+ const auto diff = d_as_common - d_as_whole_seconds;
+ const auto ms =
+ fmt_duration_cast<std::chrono::duration<Rep, std::milli>>(diff);
+ return ms;
+#else
+ auto s = fmt_duration_cast<std::chrono::seconds>(d);
+ return fmt_duration_cast<std::chrono::milliseconds>(d - s);
+#endif
+}
+
+template <typename Char, typename Rep, typename OutputIt,
+ FMT_ENABLE_IF(std::is_integral<Rep>::value)>
+auto format_duration_value(OutputIt out, Rep val, int) -> OutputIt {
+ return write<Char>(out, val);
+}
+
+template <typename Char, typename Rep, typename OutputIt,
+ FMT_ENABLE_IF(std::is_floating_point<Rep>::value)>
+auto format_duration_value(OutputIt out, Rep val, int precision) -> OutputIt {
+ auto specs = format_specs<Char>();
+ specs.precision = precision;
+ specs.type = precision >= 0 ? presentation_type::fixed_lower
+ : presentation_type::general_lower;
+ return write<Char>(out, val, specs);
+}
+
+template <typename Char, typename OutputIt>
+auto copy_unit(string_view unit, OutputIt out, Char) -> OutputIt {
+ return std::copy(unit.begin(), unit.end(), out);
+}
+
+template <typename OutputIt>
+auto copy_unit(string_view unit, OutputIt out, wchar_t) -> OutputIt {
+ // This works when wchar_t is UTF-32 because units only contain characters
+ // that have the same representation in UTF-16 and UTF-32.
+ utf8_to_utf16 u(unit);
+ return std::copy(u.c_str(), u.c_str() + u.size(), out);
+}
+
+template <typename Char, typename Period, typename OutputIt>
+auto format_duration_unit(OutputIt out) -> OutputIt {
+ if (const char* unit = get_units<Period>())
+ return copy_unit(string_view(unit), out, Char());
+ *out++ = '[';
+ out = write<Char>(out, Period::num);
+ if (const_check(Period::den != 1)) {
+ *out++ = '/';
+ out = write<Char>(out, Period::den);
+ }
+ *out++ = ']';
+ *out++ = 's';
+ return out;
+}
+
+class get_locale {
+ private:
+ union {
+ std::locale locale_;
+ };
+ bool has_locale_ = false;
+
+ public:
+ get_locale(bool localized, locale_ref loc) : has_locale_(localized) {
+ if (localized)
+ ::new (&locale_) std::locale(loc.template get<std::locale>());
+ }
+ ~get_locale() {
+ if (has_locale_) locale_.~locale();
+ }
+ operator const std::locale&() const {
+ return has_locale_ ? locale_ : get_classic_locale();
+ }
+};
+
+template <typename FormatContext, typename OutputIt, typename Rep,
+ typename Period>
+struct chrono_formatter {
+ FormatContext& context;
+ OutputIt out;
+ int precision;
+ bool localized = false;
+ // rep is unsigned to avoid overflow.
+ using rep =
+ conditional_t<std::is_integral<Rep>::value && sizeof(Rep) < sizeof(int),
+ unsigned, typename make_unsigned_or_unchanged<Rep>::type>;
+ rep val;
+ using seconds = std::chrono::duration<rep>;
+ seconds s;
+ using milliseconds = std::chrono::duration<rep, std::milli>;
+ bool negative;
+
+ using char_type = typename FormatContext::char_type;
+ using tm_writer_type = tm_writer<OutputIt, char_type>;
+
+ chrono_formatter(FormatContext& ctx, OutputIt o,
+ std::chrono::duration<Rep, Period> d)
+ : context(ctx),
+ out(o),
+ val(static_cast<rep>(d.count())),
+ negative(false) {
+ if (d.count() < 0) {
+ val = 0 - val;
+ negative = true;
+ }
+
+ // this may overflow and/or the result may not fit in the
+ // target type.
+ // might need checked conversion (rep!=Rep)
+ s = fmt_duration_cast<seconds>(std::chrono::duration<rep, Period>(val));
+ }
+
+ // returns true if nan or inf, writes to out.
+ auto handle_nan_inf() -> bool {
+ if (isfinite(val)) {
+ return false;
+ }
+ if (isnan(val)) {
+ write_nan();
+ return true;
+ }
+ // must be +-inf
+ if (val > 0) {
+ write_pinf();
+ } else {
+ write_ninf();
+ }
+ return true;
+ }
+
+ auto days() const -> Rep { return static_cast<Rep>(s.count() / 86400); }
+ auto hour() const -> Rep {
+ return static_cast<Rep>(mod((s.count() / 3600), 24));
+ }
+
+ auto hour12() const -> Rep {
+ Rep hour = static_cast<Rep>(mod((s.count() / 3600), 12));
+ return hour <= 0 ? 12 : hour;
+ }
+
+ auto minute() const -> Rep {
+ return static_cast<Rep>(mod((s.count() / 60), 60));
+ }
+ auto second() const -> Rep { return static_cast<Rep>(mod(s.count(), 60)); }
+
+ auto time() const -> std::tm {
+ auto time = std::tm();
+ time.tm_hour = to_nonnegative_int(hour(), 24);
+ time.tm_min = to_nonnegative_int(minute(), 60);
+ time.tm_sec = to_nonnegative_int(second(), 60);
+ return time;
+ }
+
+ void write_sign() {
+ if (negative) {
+ *out++ = '-';
+ negative = false;
+ }
+ }
+
+ void write(Rep value, int width, pad_type pad = pad_type::unspecified) {
+ write_sign();
+ if (isnan(value)) return write_nan();
+ uint32_or_64_or_128_t<int> n =
+ to_unsigned(to_nonnegative_int(value, max_value<int>()));
+ int num_digits = detail::count_digits(n);
+ if (width > num_digits) {
+ out = detail::write_padding(out, pad, width - num_digits);
+ }
+ out = format_decimal<char_type>(out, n, num_digits).end;
+ }
+
+ void write_nan() { std::copy_n("nan", 3, out); }
+ void write_pinf() { std::copy_n("inf", 3, out); }
+ void write_ninf() { std::copy_n("-inf", 4, out); }
+
+ template <typename Callback, typename... Args>
+ void format_tm(const tm& time, Callback cb, Args... args) {
+ if (isnan(val)) return write_nan();
+ get_locale loc(localized, context.locale());
+ auto w = tm_writer_type(loc, out, time);
+ (w.*cb)(args...);
+ out = w.out();
+ }
+
+ void on_text(const char_type* begin, const char_type* end) {
+ std::copy(begin, end, out);
+ }
+
+ // These are not implemented because durations don't have date information.
+ void on_abbr_weekday() {}
+ void on_full_weekday() {}
+ void on_dec0_weekday(numeric_system) {}
+ void on_dec1_weekday(numeric_system) {}
+ void on_abbr_month() {}
+ void on_full_month() {}
+ void on_datetime(numeric_system) {}
+ void on_loc_date(numeric_system) {}
+ void on_loc_time(numeric_system) {}
+ void on_us_date() {}
+ void on_iso_date() {}
+ void on_utc_offset(numeric_system) {}
+ void on_tz_name() {}
+ void on_year(numeric_system) {}
+ void on_short_year(numeric_system) {}
+ void on_offset_year() {}
+ void on_century(numeric_system) {}
+ void on_iso_week_based_year() {}
+ void on_iso_week_based_short_year() {}
+ void on_dec_month(numeric_system) {}
+ void on_dec0_week_of_year(numeric_system) {}
+ void on_dec1_week_of_year(numeric_system) {}
+ void on_iso_week_of_year(numeric_system) {}
+ void on_day_of_month(numeric_system) {}
+ void on_day_of_month_space(numeric_system) {}
+
+ void on_day_of_year() {
+ if (handle_nan_inf()) return;
+ write(days(), 0);
+ }
+
+ void on_24_hour(numeric_system ns, pad_type pad) {
+ if (handle_nan_inf()) return;
+
+ if (ns == numeric_system::standard) return write(hour(), 2, pad);
+ auto time = tm();
+ time.tm_hour = to_nonnegative_int(hour(), 24);
+ format_tm(time, &tm_writer_type::on_24_hour, ns, pad);
+ }
+
+ void on_12_hour(numeric_system ns, pad_type pad) {
+ if (handle_nan_inf()) return;
+
+ if (ns == numeric_system::standard) return write(hour12(), 2, pad);
+ auto time = tm();
+ time.tm_hour = to_nonnegative_int(hour12(), 12);
+ format_tm(time, &tm_writer_type::on_12_hour, ns, pad);
+ }
+
+ void on_minute(numeric_system ns, pad_type pad) {
+ if (handle_nan_inf()) return;
+
+ if (ns == numeric_system::standard) return write(minute(), 2, pad);
+ auto time = tm();
+ time.tm_min = to_nonnegative_int(minute(), 60);
+ format_tm(time, &tm_writer_type::on_minute, ns, pad);
+ }
+
+ void on_second(numeric_system ns, pad_type pad) {
+ if (handle_nan_inf()) return;
+
+ if (ns == numeric_system::standard) {
+ if (std::is_floating_point<rep>::value) {
+ auto buf = memory_buffer();
+ write_floating_seconds(buf, std::chrono::duration<rep, Period>(val),
+ precision);
+ if (negative) *out++ = '-';
+ if (buf.size() < 2 || buf[1] == '.') {
+ out = detail::write_padding(out, pad);
+ }
+ out = std::copy(buf.begin(), buf.end(), out);
+ } else {
+ write(second(), 2, pad);
+ write_fractional_seconds<char_type>(
+ out, std::chrono::duration<rep, Period>(val), precision);
+ }
+ return;
+ }
+ auto time = tm();
+ time.tm_sec = to_nonnegative_int(second(), 60);
+ format_tm(time, &tm_writer_type::on_second, ns, pad);
+ }
+
+ void on_12_hour_time() {
+ if (handle_nan_inf()) return;
+ format_tm(time(), &tm_writer_type::on_12_hour_time);
+ }
+
+ void on_24_hour_time() {
+ if (handle_nan_inf()) {
+ *out++ = ':';
+ handle_nan_inf();
+ return;
+ }
+
+ write(hour(), 2);
+ *out++ = ':';
+ write(minute(), 2);
+ }
+
+ void on_iso_time() {
+ on_24_hour_time();
+ *out++ = ':';
+ if (handle_nan_inf()) return;
+ on_second(numeric_system::standard, pad_type::unspecified);
+ }
+
+ void on_am_pm() {
+ if (handle_nan_inf()) return;
+ format_tm(time(), &tm_writer_type::on_am_pm);
+ }
+
+ void on_duration_value() {
+ if (handle_nan_inf()) return;
+ write_sign();
+ out = format_duration_value<char_type>(out, val, precision);
+ }
+
+ void on_duration_unit() {
+ out = format_duration_unit<char_type, Period>(out);
+ }
+};
+
+} // namespace detail
+
+#if defined(__cpp_lib_chrono) && __cpp_lib_chrono >= 201907
+using weekday = std::chrono::weekday;
+#else
+// A fallback version of weekday.
+class weekday {
+ private:
+ unsigned char value;
+
+ public:
+ weekday() = default;
+ explicit constexpr weekday(unsigned wd) noexcept
+ : value(static_cast<unsigned char>(wd != 7 ? wd : 0)) {}
+ constexpr auto c_encoding() const noexcept -> unsigned { return value; }
+};
+
+class year_month_day {};
+#endif
+
+// A rudimentary weekday formatter.
+template <typename Char> struct formatter<weekday, Char> {
+ private:
+ bool localized = false;
+
+ public:
+ FMT_CONSTEXPR auto parse(basic_format_parse_context<Char>& ctx)
+ -> decltype(ctx.begin()) {
+ auto begin = ctx.begin(), end = ctx.end();
+ if (begin != end && *begin == 'L') {
+ ++begin;
+ localized = true;
+ }
+ return begin;
+ }
+
+ template <typename FormatContext>
+ auto format(weekday wd, FormatContext& ctx) const -> decltype(ctx.out()) {
+ auto time = std::tm();
+ time.tm_wday = static_cast<int>(wd.c_encoding());
+ detail::get_locale loc(localized, ctx.locale());
+ auto w = detail::tm_writer<decltype(ctx.out()), Char>(loc, ctx.out(), time);
+ w.on_abbr_weekday();
+ return w.out();
+ }
+};
+
+template <typename Rep, typename Period, typename Char>
+struct formatter<std::chrono::duration<Rep, Period>, Char> {
+ private:
+ format_specs<Char> specs_;
+ detail::arg_ref<Char> width_ref_;
+ detail::arg_ref<Char> precision_ref_;
+ bool localized_ = false;
+ basic_string_view<Char> format_str_;
+
+ public:
+ FMT_CONSTEXPR auto parse(basic_format_parse_context<Char>& ctx)
+ -> decltype(ctx.begin()) {
+ auto it = ctx.begin(), end = ctx.end();
+ if (it == end || *it == '}') return it;
+
+ it = detail::parse_align(it, end, specs_);
+ if (it == end) return it;
+
+ it = detail::parse_dynamic_spec(it, end, specs_.width, width_ref_, ctx);
+ if (it == end) return it;
+
+ auto checker = detail::chrono_format_checker();
+ if (*it == '.') {
+ checker.has_precision_integral = !std::is_floating_point<Rep>::value;
+ it = detail::parse_precision(it, end, specs_.precision, precision_ref_,
+ ctx);
+ }
+ if (it != end && *it == 'L') {
+ localized_ = true;
+ ++it;
+ }
+ end = detail::parse_chrono_format(it, end, checker);
+ format_str_ = {it, detail::to_unsigned(end - it)};
+ return end;
+ }
+
+ template <typename FormatContext>
+ auto format(std::chrono::duration<Rep, Period> d, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ auto specs = specs_;
+ auto precision = specs.precision;
+ specs.precision = -1;
+ auto begin = format_str_.begin(), end = format_str_.end();
+ // As a possible future optimization, we could avoid extra copying if width
+ // is not specified.
+ auto buf = basic_memory_buffer<Char>();
+ auto out = std::back_inserter(buf);
+ detail::handle_dynamic_spec<detail::width_checker>(specs.width, width_ref_,
+ ctx);
+ detail::handle_dynamic_spec<detail::precision_checker>(precision,
+ precision_ref_, ctx);
+ if (begin == end || *begin == '}') {
+ out = detail::format_duration_value<Char>(out, d.count(), precision);
+ detail::format_duration_unit<Char, Period>(out);
+ } else {
+ using chrono_formatter =
+ detail::chrono_formatter<FormatContext, decltype(out), Rep, Period>;
+ auto f = chrono_formatter(ctx, out, d);
+ f.precision = precision;
+ f.localized = localized_;
+ detail::parse_chrono_format(begin, end, f);
+ }
+ return detail::write(
+ ctx.out(), basic_string_view<Char>(buf.data(), buf.size()), specs);
+ }
+};
+
+template <typename Char, typename Duration>
+struct formatter<std::chrono::time_point<std::chrono::system_clock, Duration>,
+ Char> : formatter<std::tm, Char> {
+ FMT_CONSTEXPR formatter() {
+ this->format_str_ = detail::string_literal<Char, '%', 'F', ' ', '%', 'T'>{};
+ }
+
+ template <typename FormatContext>
+ auto format(std::chrono::time_point<std::chrono::system_clock, Duration> val,
+ FormatContext& ctx) const -> decltype(ctx.out()) {
+ using period = typename Duration::period;
+ if (detail::const_check(
+ period::num != 1 || period::den != 1 ||
+ std::is_floating_point<typename Duration::rep>::value)) {
+ const auto epoch = val.time_since_epoch();
+ auto subsecs = detail::fmt_duration_cast<Duration>(
+ epoch - detail::fmt_duration_cast<std::chrono::seconds>(epoch));
+
+ if (subsecs.count() < 0) {
+ auto second =
+ detail::fmt_duration_cast<Duration>(std::chrono::seconds(1));
+ if (epoch.count() < ((Duration::min)() + second).count())
+ FMT_THROW(format_error("duration is too small"));
+ subsecs += second;
+ val -= second;
+ }
+
+ return formatter<std::tm, Char>::do_format(gmtime(val), ctx, &subsecs);
+ }
+
+ return formatter<std::tm, Char>::format(gmtime(val), ctx);
+ }
+};
+
+#if FMT_USE_LOCAL_TIME
+template <typename Char, typename Duration>
+struct formatter<std::chrono::local_time<Duration>, Char>
+ : formatter<std::tm, Char> {
+ FMT_CONSTEXPR formatter() {
+ this->format_str_ = detail::string_literal<Char, '%', 'F', ' ', '%', 'T'>{};
+ }
+
+ template <typename FormatContext>
+ auto format(std::chrono::local_time<Duration> val, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ using period = typename Duration::period;
+ if (period::num != 1 || period::den != 1 ||
+ std::is_floating_point<typename Duration::rep>::value) {
+ const auto epoch = val.time_since_epoch();
+ const auto subsecs = detail::fmt_duration_cast<Duration>(
+ epoch - detail::fmt_duration_cast<std::chrono::seconds>(epoch));
+
+ return formatter<std::tm, Char>::do_format(localtime(val), ctx, &subsecs);
+ }
+
+ return formatter<std::tm, Char>::format(localtime(val), ctx);
+ }
+};
+#endif
+
+#if FMT_USE_UTC_TIME
+template <typename Char, typename Duration>
+struct formatter<std::chrono::time_point<std::chrono::utc_clock, Duration>,
+ Char>
+ : formatter<std::chrono::time_point<std::chrono::system_clock, Duration>,
+ Char> {
+ template <typename FormatContext>
+ auto format(std::chrono::time_point<std::chrono::utc_clock, Duration> val,
+ FormatContext& ctx) const -> decltype(ctx.out()) {
+ return formatter<
+ std::chrono::time_point<std::chrono::system_clock, Duration>,
+ Char>::format(std::chrono::utc_clock::to_sys(val), ctx);
+ }
+};
+#endif
+
+template <typename Char> struct formatter<std::tm, Char> {
+ private:
+ format_specs<Char> specs_;
+ detail::arg_ref<Char> width_ref_;
+
+ protected:
+ basic_string_view<Char> format_str_;
+
+ template <typename FormatContext, typename Duration>
+ auto do_format(const std::tm& tm, FormatContext& ctx,
+ const Duration* subsecs) const -> decltype(ctx.out()) {
+ auto specs = specs_;
+ auto buf = basic_memory_buffer<Char>();
+ auto out = std::back_inserter(buf);
+ detail::handle_dynamic_spec<detail::width_checker>(specs.width, width_ref_,
+ ctx);
+
+ auto loc_ref = ctx.locale();
+ detail::get_locale loc(static_cast<bool>(loc_ref), loc_ref);
+ auto w =
+ detail::tm_writer<decltype(out), Char, Duration>(loc, out, tm, subsecs);
+ detail::parse_chrono_format(format_str_.begin(), format_str_.end(), w);
+ return detail::write(
+ ctx.out(), basic_string_view<Char>(buf.data(), buf.size()), specs);
+ }
+
+ public:
+ FMT_CONSTEXPR auto parse(basic_format_parse_context<Char>& ctx)
+ -> decltype(ctx.begin()) {
+ auto it = ctx.begin(), end = ctx.end();
+ if (it == end || *it == '}') return it;
+
+ it = detail::parse_align(it, end, specs_);
+ if (it == end) return it;
+
+ it = detail::parse_dynamic_spec(it, end, specs_.width, width_ref_, ctx);
+ if (it == end) return it;
+
+ end = detail::parse_chrono_format(it, end, detail::tm_format_checker());
+ // Replace the default format_str only if the new spec is not empty.
+ if (end != it) format_str_ = {it, detail::to_unsigned(end - it)};
+ return end;
+ }
+
+ template <typename FormatContext>
+ auto format(const std::tm& tm, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ return do_format<FormatContext, std::chrono::seconds>(tm, ctx, nullptr);
+ }
+};
+
+FMT_END_EXPORT
+FMT_END_NAMESPACE
+
+#endif // FMT_CHRONO_H_
--- /dev/null
+// Formatting library for C++ - color support
+//
+// Copyright (c) 2018 - present, Victor Zverovich and fmt contributors
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_COLOR_H_
+#define FMT_COLOR_H_
+
+#include "format.h"
+
+FMT_BEGIN_NAMESPACE
+FMT_BEGIN_EXPORT
+
+enum class color : uint32_t {
+ alice_blue = 0xF0F8FF, // rgb(240,248,255)
+ antique_white = 0xFAEBD7, // rgb(250,235,215)
+ aqua = 0x00FFFF, // rgb(0,255,255)
+ aquamarine = 0x7FFFD4, // rgb(127,255,212)
+ azure = 0xF0FFFF, // rgb(240,255,255)
+ beige = 0xF5F5DC, // rgb(245,245,220)
+ bisque = 0xFFE4C4, // rgb(255,228,196)
+ black = 0x000000, // rgb(0,0,0)
+ blanched_almond = 0xFFEBCD, // rgb(255,235,205)
+ blue = 0x0000FF, // rgb(0,0,255)
+ blue_violet = 0x8A2BE2, // rgb(138,43,226)
+ brown = 0xA52A2A, // rgb(165,42,42)
+ burly_wood = 0xDEB887, // rgb(222,184,135)
+ cadet_blue = 0x5F9EA0, // rgb(95,158,160)
+ chartreuse = 0x7FFF00, // rgb(127,255,0)
+ chocolate = 0xD2691E, // rgb(210,105,30)
+ coral = 0xFF7F50, // rgb(255,127,80)
+ cornflower_blue = 0x6495ED, // rgb(100,149,237)
+ cornsilk = 0xFFF8DC, // rgb(255,248,220)
+ crimson = 0xDC143C, // rgb(220,20,60)
+ cyan = 0x00FFFF, // rgb(0,255,255)
+ dark_blue = 0x00008B, // rgb(0,0,139)
+ dark_cyan = 0x008B8B, // rgb(0,139,139)
+ dark_golden_rod = 0xB8860B, // rgb(184,134,11)
+ dark_gray = 0xA9A9A9, // rgb(169,169,169)
+ dark_green = 0x006400, // rgb(0,100,0)
+ dark_khaki = 0xBDB76B, // rgb(189,183,107)
+ dark_magenta = 0x8B008B, // rgb(139,0,139)
+ dark_olive_green = 0x556B2F, // rgb(85,107,47)
+ dark_orange = 0xFF8C00, // rgb(255,140,0)
+ dark_orchid = 0x9932CC, // rgb(153,50,204)
+ dark_red = 0x8B0000, // rgb(139,0,0)
+ dark_salmon = 0xE9967A, // rgb(233,150,122)
+ dark_sea_green = 0x8FBC8F, // rgb(143,188,143)
+ dark_slate_blue = 0x483D8B, // rgb(72,61,139)
+ dark_slate_gray = 0x2F4F4F, // rgb(47,79,79)
+ dark_turquoise = 0x00CED1, // rgb(0,206,209)
+ dark_violet = 0x9400D3, // rgb(148,0,211)
+ deep_pink = 0xFF1493, // rgb(255,20,147)
+ deep_sky_blue = 0x00BFFF, // rgb(0,191,255)
+ dim_gray = 0x696969, // rgb(105,105,105)
+ dodger_blue = 0x1E90FF, // rgb(30,144,255)
+ fire_brick = 0xB22222, // rgb(178,34,34)
+ floral_white = 0xFFFAF0, // rgb(255,250,240)
+ forest_green = 0x228B22, // rgb(34,139,34)
+ fuchsia = 0xFF00FF, // rgb(255,0,255)
+ gainsboro = 0xDCDCDC, // rgb(220,220,220)
+ ghost_white = 0xF8F8FF, // rgb(248,248,255)
+ gold = 0xFFD700, // rgb(255,215,0)
+ golden_rod = 0xDAA520, // rgb(218,165,32)
+ gray = 0x808080, // rgb(128,128,128)
+ green = 0x008000, // rgb(0,128,0)
+ green_yellow = 0xADFF2F, // rgb(173,255,47)
+ honey_dew = 0xF0FFF0, // rgb(240,255,240)
+ hot_pink = 0xFF69B4, // rgb(255,105,180)
+ indian_red = 0xCD5C5C, // rgb(205,92,92)
+ indigo = 0x4B0082, // rgb(75,0,130)
+ ivory = 0xFFFFF0, // rgb(255,255,240)
+ khaki = 0xF0E68C, // rgb(240,230,140)
+ lavender = 0xE6E6FA, // rgb(230,230,250)
+ lavender_blush = 0xFFF0F5, // rgb(255,240,245)
+ lawn_green = 0x7CFC00, // rgb(124,252,0)
+ lemon_chiffon = 0xFFFACD, // rgb(255,250,205)
+ light_blue = 0xADD8E6, // rgb(173,216,230)
+ light_coral = 0xF08080, // rgb(240,128,128)
+ light_cyan = 0xE0FFFF, // rgb(224,255,255)
+ light_golden_rod_yellow = 0xFAFAD2, // rgb(250,250,210)
+ light_gray = 0xD3D3D3, // rgb(211,211,211)
+ light_green = 0x90EE90, // rgb(144,238,144)
+ light_pink = 0xFFB6C1, // rgb(255,182,193)
+ light_salmon = 0xFFA07A, // rgb(255,160,122)
+ light_sea_green = 0x20B2AA, // rgb(32,178,170)
+ light_sky_blue = 0x87CEFA, // rgb(135,206,250)
+ light_slate_gray = 0x778899, // rgb(119,136,153)
+ light_steel_blue = 0xB0C4DE, // rgb(176,196,222)
+ light_yellow = 0xFFFFE0, // rgb(255,255,224)
+ lime = 0x00FF00, // rgb(0,255,0)
+ lime_green = 0x32CD32, // rgb(50,205,50)
+ linen = 0xFAF0E6, // rgb(250,240,230)
+ magenta = 0xFF00FF, // rgb(255,0,255)
+ maroon = 0x800000, // rgb(128,0,0)
+ medium_aquamarine = 0x66CDAA, // rgb(102,205,170)
+ medium_blue = 0x0000CD, // rgb(0,0,205)
+ medium_orchid = 0xBA55D3, // rgb(186,85,211)
+ medium_purple = 0x9370DB, // rgb(147,112,219)
+ medium_sea_green = 0x3CB371, // rgb(60,179,113)
+ medium_slate_blue = 0x7B68EE, // rgb(123,104,238)
+ medium_spring_green = 0x00FA9A, // rgb(0,250,154)
+ medium_turquoise = 0x48D1CC, // rgb(72,209,204)
+ medium_violet_red = 0xC71585, // rgb(199,21,133)
+ midnight_blue = 0x191970, // rgb(25,25,112)
+ mint_cream = 0xF5FFFA, // rgb(245,255,250)
+ misty_rose = 0xFFE4E1, // rgb(255,228,225)
+ moccasin = 0xFFE4B5, // rgb(255,228,181)
+ navajo_white = 0xFFDEAD, // rgb(255,222,173)
+ navy = 0x000080, // rgb(0,0,128)
+ old_lace = 0xFDF5E6, // rgb(253,245,230)
+ olive = 0x808000, // rgb(128,128,0)
+ olive_drab = 0x6B8E23, // rgb(107,142,35)
+ orange = 0xFFA500, // rgb(255,165,0)
+ orange_red = 0xFF4500, // rgb(255,69,0)
+ orchid = 0xDA70D6, // rgb(218,112,214)
+ pale_golden_rod = 0xEEE8AA, // rgb(238,232,170)
+ pale_green = 0x98FB98, // rgb(152,251,152)
+ pale_turquoise = 0xAFEEEE, // rgb(175,238,238)
+ pale_violet_red = 0xDB7093, // rgb(219,112,147)
+ papaya_whip = 0xFFEFD5, // rgb(255,239,213)
+ peach_puff = 0xFFDAB9, // rgb(255,218,185)
+ peru = 0xCD853F, // rgb(205,133,63)
+ pink = 0xFFC0CB, // rgb(255,192,203)
+ plum = 0xDDA0DD, // rgb(221,160,221)
+ powder_blue = 0xB0E0E6, // rgb(176,224,230)
+ purple = 0x800080, // rgb(128,0,128)
+ rebecca_purple = 0x663399, // rgb(102,51,153)
+ red = 0xFF0000, // rgb(255,0,0)
+ rosy_brown = 0xBC8F8F, // rgb(188,143,143)
+ royal_blue = 0x4169E1, // rgb(65,105,225)
+ saddle_brown = 0x8B4513, // rgb(139,69,19)
+ salmon = 0xFA8072, // rgb(250,128,114)
+ sandy_brown = 0xF4A460, // rgb(244,164,96)
+ sea_green = 0x2E8B57, // rgb(46,139,87)
+ sea_shell = 0xFFF5EE, // rgb(255,245,238)
+ sienna = 0xA0522D, // rgb(160,82,45)
+ silver = 0xC0C0C0, // rgb(192,192,192)
+ sky_blue = 0x87CEEB, // rgb(135,206,235)
+ slate_blue = 0x6A5ACD, // rgb(106,90,205)
+ slate_gray = 0x708090, // rgb(112,128,144)
+ snow = 0xFFFAFA, // rgb(255,250,250)
+ spring_green = 0x00FF7F, // rgb(0,255,127)
+ steel_blue = 0x4682B4, // rgb(70,130,180)
+ tan = 0xD2B48C, // rgb(210,180,140)
+ teal = 0x008080, // rgb(0,128,128)
+ thistle = 0xD8BFD8, // rgb(216,191,216)
+ tomato = 0xFF6347, // rgb(255,99,71)
+ turquoise = 0x40E0D0, // rgb(64,224,208)
+ violet = 0xEE82EE, // rgb(238,130,238)
+ wheat = 0xF5DEB3, // rgb(245,222,179)
+ white = 0xFFFFFF, // rgb(255,255,255)
+ white_smoke = 0xF5F5F5, // rgb(245,245,245)
+ yellow = 0xFFFF00, // rgb(255,255,0)
+ yellow_green = 0x9ACD32 // rgb(154,205,50)
+}; // enum class color
+
+enum class terminal_color : uint8_t {
+ black = 30,
+ red,
+ green,
+ yellow,
+ blue,
+ magenta,
+ cyan,
+ white,
+ bright_black = 90,
+ bright_red,
+ bright_green,
+ bright_yellow,
+ bright_blue,
+ bright_magenta,
+ bright_cyan,
+ bright_white
+};
+
+enum class emphasis : uint8_t {
+ bold = 1,
+ faint = 1 << 1,
+ italic = 1 << 2,
+ underline = 1 << 3,
+ blink = 1 << 4,
+ reverse = 1 << 5,
+ conceal = 1 << 6,
+ strikethrough = 1 << 7,
+};
+
+// rgb is a struct for red, green and blue colors.
+// Using the name "rgb" makes some editors show the color in a tooltip.
+struct rgb {
+ FMT_CONSTEXPR rgb() : r(0), g(0), b(0) {}
+ FMT_CONSTEXPR rgb(uint8_t r_, uint8_t g_, uint8_t b_) : r(r_), g(g_), b(b_) {}
+ FMT_CONSTEXPR rgb(uint32_t hex)
+ : r((hex >> 16) & 0xFF), g((hex >> 8) & 0xFF), b(hex & 0xFF) {}
+ FMT_CONSTEXPR rgb(color hex)
+ : r((uint32_t(hex) >> 16) & 0xFF),
+ g((uint32_t(hex) >> 8) & 0xFF),
+ b(uint32_t(hex) & 0xFF) {}
+ uint8_t r;
+ uint8_t g;
+ uint8_t b;
+};
+
+namespace detail {
+
+// color is a struct of either a rgb color or a terminal color.
+struct color_type {
+ FMT_CONSTEXPR color_type() noexcept : is_rgb(), value{} {}
+ FMT_CONSTEXPR color_type(color rgb_color) noexcept : is_rgb(true), value{} {
+ value.rgb_color = static_cast<uint32_t>(rgb_color);
+ }
+ FMT_CONSTEXPR color_type(rgb rgb_color) noexcept : is_rgb(true), value{} {
+ value.rgb_color = (static_cast<uint32_t>(rgb_color.r) << 16) |
+ (static_cast<uint32_t>(rgb_color.g) << 8) | rgb_color.b;
+ }
+ FMT_CONSTEXPR color_type(terminal_color term_color) noexcept
+ : is_rgb(), value{} {
+ value.term_color = static_cast<uint8_t>(term_color);
+ }
+ bool is_rgb;
+ union color_union {
+ uint8_t term_color;
+ uint32_t rgb_color;
+ } value;
+};
+} // namespace detail
+
+/** A text style consisting of foreground and background colors and emphasis. */
+class text_style {
+ public:
+ FMT_CONSTEXPR text_style(emphasis em = emphasis()) noexcept
+ : set_foreground_color(), set_background_color(), ems(em) {}
+
+ FMT_CONSTEXPR auto operator|=(const text_style& rhs) -> text_style& {
+ if (!set_foreground_color) {
+ set_foreground_color = rhs.set_foreground_color;
+ foreground_color = rhs.foreground_color;
+ } else if (rhs.set_foreground_color) {
+ if (!foreground_color.is_rgb || !rhs.foreground_color.is_rgb)
+ FMT_THROW(format_error("can't OR a terminal color"));
+ foreground_color.value.rgb_color |= rhs.foreground_color.value.rgb_color;
+ }
+
+ if (!set_background_color) {
+ set_background_color = rhs.set_background_color;
+ background_color = rhs.background_color;
+ } else if (rhs.set_background_color) {
+ if (!background_color.is_rgb || !rhs.background_color.is_rgb)
+ FMT_THROW(format_error("can't OR a terminal color"));
+ background_color.value.rgb_color |= rhs.background_color.value.rgb_color;
+ }
+
+ ems = static_cast<emphasis>(static_cast<uint8_t>(ems) |
+ static_cast<uint8_t>(rhs.ems));
+ return *this;
+ }
+
+ friend FMT_CONSTEXPR auto operator|(text_style lhs, const text_style& rhs)
+ -> text_style {
+ return lhs |= rhs;
+ }
+
+ FMT_CONSTEXPR auto has_foreground() const noexcept -> bool {
+ return set_foreground_color;
+ }
+ FMT_CONSTEXPR auto has_background() const noexcept -> bool {
+ return set_background_color;
+ }
+ FMT_CONSTEXPR auto has_emphasis() const noexcept -> bool {
+ return static_cast<uint8_t>(ems) != 0;
+ }
+ FMT_CONSTEXPR auto get_foreground() const noexcept -> detail::color_type {
+ FMT_ASSERT(has_foreground(), "no foreground specified for this style");
+ return foreground_color;
+ }
+ FMT_CONSTEXPR auto get_background() const noexcept -> detail::color_type {
+ FMT_ASSERT(has_background(), "no background specified for this style");
+ return background_color;
+ }
+ FMT_CONSTEXPR auto get_emphasis() const noexcept -> emphasis {
+ FMT_ASSERT(has_emphasis(), "no emphasis specified for this style");
+ return ems;
+ }
+
+ private:
+ FMT_CONSTEXPR text_style(bool is_foreground,
+ detail::color_type text_color) noexcept
+ : set_foreground_color(), set_background_color(), ems() {
+ if (is_foreground) {
+ foreground_color = text_color;
+ set_foreground_color = true;
+ } else {
+ background_color = text_color;
+ set_background_color = true;
+ }
+ }
+
+ friend FMT_CONSTEXPR auto fg(detail::color_type foreground) noexcept
+ -> text_style;
+
+ friend FMT_CONSTEXPR auto bg(detail::color_type background) noexcept
+ -> text_style;
+
+ detail::color_type foreground_color;
+ detail::color_type background_color;
+ bool set_foreground_color;
+ bool set_background_color;
+ emphasis ems;
+};
+
+/** Creates a text style from the foreground (text) color. */
+FMT_CONSTEXPR inline auto fg(detail::color_type foreground) noexcept
+ -> text_style {
+ return text_style(true, foreground);
+}
+
+/** Creates a text style from the background color. */
+FMT_CONSTEXPR inline auto bg(detail::color_type background) noexcept
+ -> text_style {
+ return text_style(false, background);
+}
+
+FMT_CONSTEXPR inline auto operator|(emphasis lhs, emphasis rhs) noexcept
+ -> text_style {
+ return text_style(lhs) | rhs;
+}
+
+namespace detail {
+
+template <typename Char> struct ansi_color_escape {
+ FMT_CONSTEXPR ansi_color_escape(detail::color_type text_color,
+ const char* esc) noexcept {
+ // If we have a terminal color, we need to output another escape code
+ // sequence.
+ if (!text_color.is_rgb) {
+ bool is_background = esc == string_view("\x1b[48;2;");
+ uint32_t value = text_color.value.term_color;
+ // Background ASCII codes are the same as the foreground ones but with
+ // 10 more.
+ if (is_background) value += 10u;
+
+ size_t index = 0;
+ buffer[index++] = static_cast<Char>('\x1b');
+ buffer[index++] = static_cast<Char>('[');
+
+ if (value >= 100u) {
+ buffer[index++] = static_cast<Char>('1');
+ value %= 100u;
+ }
+ buffer[index++] = static_cast<Char>('0' + value / 10u);
+ buffer[index++] = static_cast<Char>('0' + value % 10u);
+
+ buffer[index++] = static_cast<Char>('m');
+ buffer[index++] = static_cast<Char>('\0');
+ return;
+ }
+
+ for (int i = 0; i < 7; i++) {
+ buffer[i] = static_cast<Char>(esc[i]);
+ }
+ rgb color(text_color.value.rgb_color);
+ to_esc(color.r, buffer + 7, ';');
+ to_esc(color.g, buffer + 11, ';');
+ to_esc(color.b, buffer + 15, 'm');
+ buffer[19] = static_cast<Char>(0);
+ }
+ FMT_CONSTEXPR ansi_color_escape(emphasis em) noexcept {
+ uint8_t em_codes[num_emphases] = {};
+ if (has_emphasis(em, emphasis::bold)) em_codes[0] = 1;
+ if (has_emphasis(em, emphasis::faint)) em_codes[1] = 2;
+ if (has_emphasis(em, emphasis::italic)) em_codes[2] = 3;
+ if (has_emphasis(em, emphasis::underline)) em_codes[3] = 4;
+ if (has_emphasis(em, emphasis::blink)) em_codes[4] = 5;
+ if (has_emphasis(em, emphasis::reverse)) em_codes[5] = 7;
+ if (has_emphasis(em, emphasis::conceal)) em_codes[6] = 8;
+ if (has_emphasis(em, emphasis::strikethrough)) em_codes[7] = 9;
+
+ size_t index = 0;
+ for (size_t i = 0; i < num_emphases; ++i) {
+ if (!em_codes[i]) continue;
+ buffer[index++] = static_cast<Char>('\x1b');
+ buffer[index++] = static_cast<Char>('[');
+ buffer[index++] = static_cast<Char>('0' + em_codes[i]);
+ buffer[index++] = static_cast<Char>('m');
+ }
+ buffer[index++] = static_cast<Char>(0);
+ }
+ FMT_CONSTEXPR operator const Char*() const noexcept { return buffer; }
+
+ FMT_CONSTEXPR auto begin() const noexcept -> const Char* { return buffer; }
+ FMT_CONSTEXPR_CHAR_TRAITS auto end() const noexcept -> const Char* {
+ return buffer + std::char_traits<Char>::length(buffer);
+ }
+
+ private:
+ static constexpr size_t num_emphases = 8;
+ Char buffer[7u + 3u * num_emphases + 1u];
+
+ static FMT_CONSTEXPR void to_esc(uint8_t c, Char* out,
+ char delimiter) noexcept {
+ out[0] = static_cast<Char>('0' + c / 100);
+ out[1] = static_cast<Char>('0' + c / 10 % 10);
+ out[2] = static_cast<Char>('0' + c % 10);
+ out[3] = static_cast<Char>(delimiter);
+ }
+ static FMT_CONSTEXPR auto has_emphasis(emphasis em, emphasis mask) noexcept
+ -> bool {
+ return static_cast<uint8_t>(em) & static_cast<uint8_t>(mask);
+ }
+};
+
+template <typename Char>
+FMT_CONSTEXPR auto make_foreground_color(detail::color_type foreground) noexcept
+ -> ansi_color_escape<Char> {
+ return ansi_color_escape<Char>(foreground, "\x1b[38;2;");
+}
+
+template <typename Char>
+FMT_CONSTEXPR auto make_background_color(detail::color_type background) noexcept
+ -> ansi_color_escape<Char> {
+ return ansi_color_escape<Char>(background, "\x1b[48;2;");
+}
+
+template <typename Char>
+FMT_CONSTEXPR auto make_emphasis(emphasis em) noexcept
+ -> ansi_color_escape<Char> {
+ return ansi_color_escape<Char>(em);
+}
+
+template <typename Char> inline void reset_color(buffer<Char>& buffer) {
+ auto reset_color = string_view("\x1b[0m");
+ buffer.append(reset_color.begin(), reset_color.end());
+}
+
+template <typename T> struct styled_arg : detail::view {
+ const T& value;
+ text_style style;
+ styled_arg(const T& v, text_style s) : value(v), style(s) {}
+};
+
+template <typename Char>
+void vformat_to(buffer<Char>& buf, const text_style& ts,
+ basic_string_view<Char> format_str,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args) {
+ bool has_style = false;
+ if (ts.has_emphasis()) {
+ has_style = true;
+ auto emphasis = detail::make_emphasis<Char>(ts.get_emphasis());
+ buf.append(emphasis.begin(), emphasis.end());
+ }
+ if (ts.has_foreground()) {
+ has_style = true;
+ auto foreground = detail::make_foreground_color<Char>(ts.get_foreground());
+ buf.append(foreground.begin(), foreground.end());
+ }
+ if (ts.has_background()) {
+ has_style = true;
+ auto background = detail::make_background_color<Char>(ts.get_background());
+ buf.append(background.begin(), background.end());
+ }
+ detail::vformat_to(buf, format_str, args, {});
+ if (has_style) detail::reset_color<Char>(buf);
+}
+
+} // namespace detail
+
+inline void vprint(std::FILE* f, const text_style& ts, string_view fmt,
+ format_args args) {
+ // Legacy wide streams are not supported.
+ auto buf = memory_buffer();
+ detail::vformat_to(buf, ts, fmt, args);
+ if (detail::is_utf8()) {
+ detail::print(f, string_view(buf.begin(), buf.size()));
+ return;
+ }
+ buf.push_back('\0');
+ int result = std::fputs(buf.data(), f);
+ if (result < 0)
+ FMT_THROW(system_error(errno, FMT_STRING("cannot write to file")));
+}
+
+/**
+ \rst
+ Formats a string and prints it to the specified file stream using ANSI
+ escape sequences to specify text formatting.
+
+ **Example**::
+
+ fmt::print(fmt::emphasis::bold | fg(fmt::color::red),
+ "Elapsed time: {0:.2f} seconds", 1.23);
+ \endrst
+ */
+template <typename S, typename... Args,
+ FMT_ENABLE_IF(detail::is_string<S>::value)>
+void print(std::FILE* f, const text_style& ts, const S& format_str,
+ const Args&... args) {
+ vprint(f, ts, format_str,
+ fmt::make_format_args<buffer_context<char_t<S>>>(args...));
+}
+
+/**
+ \rst
+ Formats a string and prints it to stdout using ANSI escape sequences to
+ specify text formatting.
+
+ **Example**::
+
+ fmt::print(fmt::emphasis::bold | fg(fmt::color::red),
+ "Elapsed time: {0:.2f} seconds", 1.23);
+ \endrst
+ */
+template <typename S, typename... Args,
+ FMT_ENABLE_IF(detail::is_string<S>::value)>
+void print(const text_style& ts, const S& format_str, const Args&... args) {
+ return print(stdout, ts, format_str, args...);
+}
+
+template <typename S, typename Char = char_t<S>>
+inline auto vformat(
+ const text_style& ts, const S& format_str,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args)
+ -> std::basic_string<Char> {
+ basic_memory_buffer<Char> buf;
+ detail::vformat_to(buf, ts, detail::to_string_view(format_str), args);
+ return fmt::to_string(buf);
+}
+
+/**
+ \rst
+ Formats arguments and returns the result as a string using ANSI
+ escape sequences to specify text formatting.
+
+ **Example**::
+
+ #include <fmt/color.h>
+ std::string message = fmt::format(fmt::emphasis::bold | fg(fmt::color::red),
+ "The answer is {}", 42);
+ \endrst
+*/
+template <typename S, typename... Args, typename Char = char_t<S>>
+inline auto format(const text_style& ts, const S& format_str,
+ const Args&... args) -> std::basic_string<Char> {
+ return fmt::vformat(ts, detail::to_string_view(format_str),
+ fmt::make_format_args<buffer_context<Char>>(args...));
+}
+
+/**
+ Formats a string with the given text_style and writes the output to ``out``.
+ */
+template <typename OutputIt, typename Char,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, Char>::value)>
+auto vformat_to(OutputIt out, const text_style& ts,
+ basic_string_view<Char> format_str,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args)
+ -> OutputIt {
+ auto&& buf = detail::get_buffer<Char>(out);
+ detail::vformat_to(buf, ts, format_str, args);
+ return detail::get_iterator(buf, out);
+}
+
+/**
+ \rst
+ Formats arguments with the given text_style, writes the result to the output
+ iterator ``out`` and returns the iterator past the end of the output range.
+
+ **Example**::
+
+ std::vector<char> out;
+ fmt::format_to(std::back_inserter(out),
+ fmt::emphasis::bold | fg(fmt::color::red), "{}", 42);
+ \endrst
+*/
+template <
+ typename OutputIt, typename S, typename... Args,
+ bool enable = detail::is_output_iterator<OutputIt, char_t<S>>::value &&
+ detail::is_string<S>::value>
+inline auto format_to(OutputIt out, const text_style& ts, const S& format_str,
+ Args&&... args) ->
+ typename std::enable_if<enable, OutputIt>::type {
+ return vformat_to(out, ts, detail::to_string_view(format_str),
+ fmt::make_format_args<buffer_context<char_t<S>>>(args...));
+}
+
+template <typename T, typename Char>
+struct formatter<detail::styled_arg<T>, Char> : formatter<T, Char> {
+ template <typename FormatContext>
+ auto format(const detail::styled_arg<T>& arg, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ const auto& ts = arg.style;
+ const auto& value = arg.value;
+ auto out = ctx.out();
+
+ bool has_style = false;
+ if (ts.has_emphasis()) {
+ has_style = true;
+ auto emphasis = detail::make_emphasis<Char>(ts.get_emphasis());
+ out = std::copy(emphasis.begin(), emphasis.end(), out);
+ }
+ if (ts.has_foreground()) {
+ has_style = true;
+ auto foreground =
+ detail::make_foreground_color<Char>(ts.get_foreground());
+ out = std::copy(foreground.begin(), foreground.end(), out);
+ }
+ if (ts.has_background()) {
+ has_style = true;
+ auto background =
+ detail::make_background_color<Char>(ts.get_background());
+ out = std::copy(background.begin(), background.end(), out);
+ }
+ out = formatter<T, Char>::format(value, ctx);
+ if (has_style) {
+ auto reset_color = string_view("\x1b[0m");
+ out = std::copy(reset_color.begin(), reset_color.end(), out);
+ }
+ return out;
+ }
+};
+
+/**
+ \rst
+ Returns an argument that will be formatted using ANSI escape sequences,
+ to be used in a formatting function.
+
+ **Example**::
+
+ fmt::print("Elapsed time: {0:.2f} seconds",
+ fmt::styled(1.23, fmt::fg(fmt::color::green) |
+ fmt::bg(fmt::color::blue)));
+ \endrst
+ */
+template <typename T>
+FMT_CONSTEXPR auto styled(const T& value, text_style ts)
+ -> detail::styled_arg<remove_cvref_t<T>> {
+ return detail::styled_arg<remove_cvref_t<T>>{value, ts};
+}
+
+FMT_END_EXPORT
+FMT_END_NAMESPACE
+
+#endif // FMT_COLOR_H_
--- /dev/null
+// Formatting library for C++ - experimental format string compilation
+//
+// Copyright (c) 2012 - present, Victor Zverovich and fmt contributors
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_COMPILE_H_
+#define FMT_COMPILE_H_
+
+#include "format.h"
+
+FMT_BEGIN_NAMESPACE
+namespace detail {
+
+template <typename Char, typename InputIt>
+FMT_CONSTEXPR inline auto copy_str(InputIt begin, InputIt end,
+ counting_iterator it) -> counting_iterator {
+ return it + (end - begin);
+}
+
+// A compile-time string which is compiled into fast formatting code.
+class compiled_string {};
+
+template <typename S>
+struct is_compiled_string : std::is_base_of<compiled_string, S> {};
+
+/**
+ \rst
+ Converts a string literal *s* into a format string that will be parsed at
+ compile time and converted into efficient formatting code. Requires C++17
+ ``constexpr if`` compiler support.
+
+ **Example**::
+
+ // Converts 42 into std::string using the most efficient method and no
+ // runtime format string processing.
+ std::string s = fmt::format(FMT_COMPILE("{}"), 42);
+ \endrst
+ */
+#if defined(__cpp_if_constexpr) && defined(__cpp_return_type_deduction)
+# define FMT_COMPILE(s) \
+ FMT_STRING_IMPL(s, fmt::detail::compiled_string, explicit)
+#else
+# define FMT_COMPILE(s) FMT_STRING(s)
+#endif
+
+#if FMT_USE_NONTYPE_TEMPLATE_ARGS
+template <typename Char, size_t N,
+ fmt::detail_exported::fixed_string<Char, N> Str>
+struct udl_compiled_string : compiled_string {
+ using char_type = Char;
+ explicit constexpr operator basic_string_view<char_type>() const {
+ return {Str.data, N - 1};
+ }
+};
+#endif
+
+template <typename T, typename... Tail>
+auto first(const T& value, const Tail&...) -> const T& {
+ return value;
+}
+
+#if defined(__cpp_if_constexpr) && defined(__cpp_return_type_deduction)
+template <typename... Args> struct type_list {};
+
+// Returns a reference to the argument at index N from [first, rest...].
+template <int N, typename T, typename... Args>
+constexpr const auto& get([[maybe_unused]] const T& first,
+ [[maybe_unused]] const Args&... rest) {
+ static_assert(N < 1 + sizeof...(Args), "index is out of bounds");
+ if constexpr (N == 0)
+ return first;
+ else
+ return detail::get<N - 1>(rest...);
+}
+
+template <typename Char, typename... Args>
+constexpr int get_arg_index_by_name(basic_string_view<Char> name,
+ type_list<Args...>) {
+ return get_arg_index_by_name<Args...>(name);
+}
+
+template <int N, typename> struct get_type_impl;
+
+template <int N, typename... Args> struct get_type_impl<N, type_list<Args...>> {
+ using type =
+ remove_cvref_t<decltype(detail::get<N>(std::declval<Args>()...))>;
+};
+
+template <int N, typename T>
+using get_type = typename get_type_impl<N, T>::type;
+
+template <typename T> struct is_compiled_format : std::false_type {};
+
+template <typename Char> struct text {
+ basic_string_view<Char> data;
+ using char_type = Char;
+
+ template <typename OutputIt, typename... Args>
+ constexpr OutputIt format(OutputIt out, const Args&...) const {
+ return write<Char>(out, data);
+ }
+};
+
+template <typename Char>
+struct is_compiled_format<text<Char>> : std::true_type {};
+
+template <typename Char>
+constexpr text<Char> make_text(basic_string_view<Char> s, size_t pos,
+ size_t size) {
+ return {{&s[pos], size}};
+}
+
+template <typename Char> struct code_unit {
+ Char value;
+ using char_type = Char;
+
+ template <typename OutputIt, typename... Args>
+ constexpr OutputIt format(OutputIt out, const Args&...) const {
+ *out++ = value;
+ return out;
+ }
+};
+
+// This ensures that the argument type is convertible to `const T&`.
+template <typename T, int N, typename... Args>
+constexpr const T& get_arg_checked(const Args&... args) {
+ const auto& arg = detail::get<N>(args...);
+ if constexpr (detail::is_named_arg<remove_cvref_t<decltype(arg)>>()) {
+ return arg.value;
+ } else {
+ return arg;
+ }
+}
+
+template <typename Char>
+struct is_compiled_format<code_unit<Char>> : std::true_type {};
+
+// A replacement field that refers to argument N.
+template <typename Char, typename T, int N> struct field {
+ using char_type = Char;
+
+ template <typename OutputIt, typename... Args>
+ constexpr OutputIt format(OutputIt out, const Args&... args) const {
+ const T& arg = get_arg_checked<T, N>(args...);
+ if constexpr (std::is_convertible_v<T, basic_string_view<Char>>) {
+ auto s = basic_string_view<Char>(arg);
+ return copy_str<Char>(s.begin(), s.end(), out);
+ }
+ return write<Char>(out, arg);
+ }
+};
+
+template <typename Char, typename T, int N>
+struct is_compiled_format<field<Char, T, N>> : std::true_type {};
+
+// A replacement field that refers to argument with name.
+template <typename Char> struct runtime_named_field {
+ using char_type = Char;
+ basic_string_view<Char> name;
+
+ template <typename OutputIt, typename T>
+ constexpr static bool try_format_argument(
+ OutputIt& out,
+ // [[maybe_unused]] due to unused-but-set-parameter warning in GCC 7,8,9
+ [[maybe_unused]] basic_string_view<Char> arg_name, const T& arg) {
+ if constexpr (is_named_arg<typename std::remove_cv<T>::type>::value) {
+ if (arg_name == arg.name) {
+ out = write<Char>(out, arg.value);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ template <typename OutputIt, typename... Args>
+ constexpr OutputIt format(OutputIt out, const Args&... args) const {
+ bool found = (try_format_argument(out, name, args) || ...);
+ if (!found) {
+ FMT_THROW(format_error("argument with specified name is not found"));
+ }
+ return out;
+ }
+};
+
+template <typename Char>
+struct is_compiled_format<runtime_named_field<Char>> : std::true_type {};
+
+// A replacement field that refers to argument N and has format specifiers.
+template <typename Char, typename T, int N> struct spec_field {
+ using char_type = Char;
+ formatter<T, Char> fmt;
+
+ template <typename OutputIt, typename... Args>
+ constexpr FMT_INLINE OutputIt format(OutputIt out,
+ const Args&... args) const {
+ const auto& vargs =
+ fmt::make_format_args<basic_format_context<OutputIt, Char>>(args...);
+ basic_format_context<OutputIt, Char> ctx(out, vargs);
+ return fmt.format(get_arg_checked<T, N>(args...), ctx);
+ }
+};
+
+template <typename Char, typename T, int N>
+struct is_compiled_format<spec_field<Char, T, N>> : std::true_type {};
+
+template <typename L, typename R> struct concat {
+ L lhs;
+ R rhs;
+ using char_type = typename L::char_type;
+
+ template <typename OutputIt, typename... Args>
+ constexpr OutputIt format(OutputIt out, const Args&... args) const {
+ out = lhs.format(out, args...);
+ return rhs.format(out, args...);
+ }
+};
+
+template <typename L, typename R>
+struct is_compiled_format<concat<L, R>> : std::true_type {};
+
+template <typename L, typename R>
+constexpr concat<L, R> make_concat(L lhs, R rhs) {
+ return {lhs, rhs};
+}
+
+struct unknown_format {};
+
+template <typename Char>
+constexpr size_t parse_text(basic_string_view<Char> str, size_t pos) {
+ for (size_t size = str.size(); pos != size; ++pos) {
+ if (str[pos] == '{' || str[pos] == '}') break;
+ }
+ return pos;
+}
+
+template <typename Args, size_t POS, int ID, typename S>
+constexpr auto compile_format_string(S format_str);
+
+template <typename Args, size_t POS, int ID, typename T, typename S>
+constexpr auto parse_tail(T head, S format_str) {
+ if constexpr (POS !=
+ basic_string_view<typename S::char_type>(format_str).size()) {
+ constexpr auto tail = compile_format_string<Args, POS, ID>(format_str);
+ if constexpr (std::is_same<remove_cvref_t<decltype(tail)>,
+ unknown_format>())
+ return tail;
+ else
+ return make_concat(head, tail);
+ } else {
+ return head;
+ }
+}
+
+template <typename T, typename Char> struct parse_specs_result {
+ formatter<T, Char> fmt;
+ size_t end;
+ int next_arg_id;
+};
+
+enum { manual_indexing_id = -1 };
+
+template <typename T, typename Char>
+constexpr parse_specs_result<T, Char> parse_specs(basic_string_view<Char> str,
+ size_t pos, int next_arg_id) {
+ str.remove_prefix(pos);
+ auto ctx =
+ compile_parse_context<Char>(str, max_value<int>(), nullptr, next_arg_id);
+ auto f = formatter<T, Char>();
+ auto end = f.parse(ctx);
+ return {f, pos + fmt::detail::to_unsigned(end - str.data()),
+ next_arg_id == 0 ? manual_indexing_id : ctx.next_arg_id()};
+}
+
+template <typename Char> struct arg_id_handler {
+ arg_ref<Char> arg_id;
+
+ constexpr int on_auto() {
+ FMT_ASSERT(false, "handler cannot be used with automatic indexing");
+ return 0;
+ }
+ constexpr int on_index(int id) {
+ arg_id = arg_ref<Char>(id);
+ return 0;
+ }
+ constexpr int on_name(basic_string_view<Char> id) {
+ arg_id = arg_ref<Char>(id);
+ return 0;
+ }
+};
+
+template <typename Char> struct parse_arg_id_result {
+ arg_ref<Char> arg_id;
+ const Char* arg_id_end;
+};
+
+template <int ID, typename Char>
+constexpr auto parse_arg_id(const Char* begin, const Char* end) {
+ auto handler = arg_id_handler<Char>{arg_ref<Char>{}};
+ auto arg_id_end = parse_arg_id(begin, end, handler);
+ return parse_arg_id_result<Char>{handler.arg_id, arg_id_end};
+}
+
+template <typename T, typename Enable = void> struct field_type {
+ using type = remove_cvref_t<T>;
+};
+
+template <typename T>
+struct field_type<T, enable_if_t<detail::is_named_arg<T>::value>> {
+ using type = remove_cvref_t<decltype(T::value)>;
+};
+
+template <typename T, typename Args, size_t END_POS, int ARG_INDEX, int NEXT_ID,
+ typename S>
+constexpr auto parse_replacement_field_then_tail(S format_str) {
+ using char_type = typename S::char_type;
+ constexpr auto str = basic_string_view<char_type>(format_str);
+ constexpr char_type c = END_POS != str.size() ? str[END_POS] : char_type();
+ if constexpr (c == '}') {
+ return parse_tail<Args, END_POS + 1, NEXT_ID>(
+ field<char_type, typename field_type<T>::type, ARG_INDEX>(),
+ format_str);
+ } else if constexpr (c != ':') {
+ FMT_THROW(format_error("expected ':'"));
+ } else {
+ constexpr auto result = parse_specs<typename field_type<T>::type>(
+ str, END_POS + 1, NEXT_ID == manual_indexing_id ? 0 : NEXT_ID);
+ if constexpr (result.end >= str.size() || str[result.end] != '}') {
+ FMT_THROW(format_error("expected '}'"));
+ return 0;
+ } else {
+ return parse_tail<Args, result.end + 1, result.next_arg_id>(
+ spec_field<char_type, typename field_type<T>::type, ARG_INDEX>{
+ result.fmt},
+ format_str);
+ }
+ }
+}
+
+// Compiles a non-empty format string and returns the compiled representation
+// or unknown_format() on unrecognized input.
+template <typename Args, size_t POS, int ID, typename S>
+constexpr auto compile_format_string(S format_str) {
+ using char_type = typename S::char_type;
+ constexpr auto str = basic_string_view<char_type>(format_str);
+ if constexpr (str[POS] == '{') {
+ if constexpr (POS + 1 == str.size())
+ FMT_THROW(format_error("unmatched '{' in format string"));
+ if constexpr (str[POS + 1] == '{') {
+ return parse_tail<Args, POS + 2, ID>(make_text(str, POS, 1), format_str);
+ } else if constexpr (str[POS + 1] == '}' || str[POS + 1] == ':') {
+ static_assert(ID != manual_indexing_id,
+ "cannot switch from manual to automatic argument indexing");
+ constexpr auto next_id =
+ ID != manual_indexing_id ? ID + 1 : manual_indexing_id;
+ return parse_replacement_field_then_tail<get_type<ID, Args>, Args,
+ POS + 1, ID, next_id>(
+ format_str);
+ } else {
+ constexpr auto arg_id_result =
+ parse_arg_id<ID>(str.data() + POS + 1, str.data() + str.size());
+ constexpr auto arg_id_end_pos = arg_id_result.arg_id_end - str.data();
+ constexpr char_type c =
+ arg_id_end_pos != str.size() ? str[arg_id_end_pos] : char_type();
+ static_assert(c == '}' || c == ':', "missing '}' in format string");
+ if constexpr (arg_id_result.arg_id.kind == arg_id_kind::index) {
+ static_assert(
+ ID == manual_indexing_id || ID == 0,
+ "cannot switch from automatic to manual argument indexing");
+ constexpr auto arg_index = arg_id_result.arg_id.val.index;
+ return parse_replacement_field_then_tail<get_type<arg_index, Args>,
+ Args, arg_id_end_pos,
+ arg_index, manual_indexing_id>(
+ format_str);
+ } else if constexpr (arg_id_result.arg_id.kind == arg_id_kind::name) {
+ constexpr auto arg_index =
+ get_arg_index_by_name(arg_id_result.arg_id.val.name, Args{});
+ if constexpr (arg_index >= 0) {
+ constexpr auto next_id =
+ ID != manual_indexing_id ? ID + 1 : manual_indexing_id;
+ return parse_replacement_field_then_tail<
+ decltype(get_type<arg_index, Args>::value), Args, arg_id_end_pos,
+ arg_index, next_id>(format_str);
+ } else if constexpr (c == '}') {
+ return parse_tail<Args, arg_id_end_pos + 1, ID>(
+ runtime_named_field<char_type>{arg_id_result.arg_id.val.name},
+ format_str);
+ } else if constexpr (c == ':') {
+ return unknown_format(); // no type info for specs parsing
+ }
+ }
+ }
+ } else if constexpr (str[POS] == '}') {
+ if constexpr (POS + 1 == str.size())
+ FMT_THROW(format_error("unmatched '}' in format string"));
+ return parse_tail<Args, POS + 2, ID>(make_text(str, POS, 1), format_str);
+ } else {
+ constexpr auto end = parse_text(str, POS + 1);
+ if constexpr (end - POS > 1) {
+ return parse_tail<Args, end, ID>(make_text(str, POS, end - POS),
+ format_str);
+ } else {
+ return parse_tail<Args, end, ID>(code_unit<char_type>{str[POS]},
+ format_str);
+ }
+ }
+}
+
+template <typename... Args, typename S,
+ FMT_ENABLE_IF(detail::is_compiled_string<S>::value)>
+constexpr auto compile(S format_str) {
+ constexpr auto str = basic_string_view<typename S::char_type>(format_str);
+ if constexpr (str.size() == 0) {
+ return detail::make_text(str, 0, 0);
+ } else {
+ constexpr auto result =
+ detail::compile_format_string<detail::type_list<Args...>, 0, 0>(
+ format_str);
+ return result;
+ }
+}
+#endif // defined(__cpp_if_constexpr) && defined(__cpp_return_type_deduction)
+} // namespace detail
+
+FMT_BEGIN_EXPORT
+
+#if defined(__cpp_if_constexpr) && defined(__cpp_return_type_deduction)
+
+template <typename CompiledFormat, typename... Args,
+ typename Char = typename CompiledFormat::char_type,
+ FMT_ENABLE_IF(detail::is_compiled_format<CompiledFormat>::value)>
+FMT_INLINE std::basic_string<Char> format(const CompiledFormat& cf,
+ const Args&... args) {
+ auto s = std::basic_string<Char>();
+ cf.format(std::back_inserter(s), args...);
+ return s;
+}
+
+template <typename OutputIt, typename CompiledFormat, typename... Args,
+ FMT_ENABLE_IF(detail::is_compiled_format<CompiledFormat>::value)>
+constexpr FMT_INLINE OutputIt format_to(OutputIt out, const CompiledFormat& cf,
+ const Args&... args) {
+ return cf.format(out, args...);
+}
+
+template <typename S, typename... Args,
+ FMT_ENABLE_IF(detail::is_compiled_string<S>::value)>
+FMT_INLINE std::basic_string<typename S::char_type> format(const S&,
+ Args&&... args) {
+ if constexpr (std::is_same<typename S::char_type, char>::value) {
+ constexpr auto str = basic_string_view<typename S::char_type>(S());
+ if constexpr (str.size() == 2 && str[0] == '{' && str[1] == '}') {
+ const auto& first = detail::first(args...);
+ if constexpr (detail::is_named_arg<
+ remove_cvref_t<decltype(first)>>::value) {
+ return fmt::to_string(first.value);
+ } else {
+ return fmt::to_string(first);
+ }
+ }
+ }
+ constexpr auto compiled = detail::compile<Args...>(S());
+ if constexpr (std::is_same<remove_cvref_t<decltype(compiled)>,
+ detail::unknown_format>()) {
+ return fmt::format(
+ static_cast<basic_string_view<typename S::char_type>>(S()),
+ std::forward<Args>(args)...);
+ } else {
+ return fmt::format(compiled, std::forward<Args>(args)...);
+ }
+}
+
+template <typename OutputIt, typename S, typename... Args,
+ FMT_ENABLE_IF(detail::is_compiled_string<S>::value)>
+FMT_CONSTEXPR OutputIt format_to(OutputIt out, const S&, Args&&... args) {
+ constexpr auto compiled = detail::compile<Args...>(S());
+ if constexpr (std::is_same<remove_cvref_t<decltype(compiled)>,
+ detail::unknown_format>()) {
+ return fmt::format_to(
+ out, static_cast<basic_string_view<typename S::char_type>>(S()),
+ std::forward<Args>(args)...);
+ } else {
+ return fmt::format_to(out, compiled, std::forward<Args>(args)...);
+ }
+}
+#endif
+
+template <typename OutputIt, typename S, typename... Args,
+ FMT_ENABLE_IF(detail::is_compiled_string<S>::value)>
+auto format_to_n(OutputIt out, size_t n, const S& format_str, Args&&... args)
+ -> format_to_n_result<OutputIt> {
+ using traits = detail::fixed_buffer_traits;
+ auto buf = detail::iterator_buffer<OutputIt, char, traits>(out, n);
+ fmt::format_to(std::back_inserter(buf), format_str,
+ std::forward<Args>(args)...);
+ return {buf.out(), buf.count()};
+}
+
+template <typename S, typename... Args,
+ FMT_ENABLE_IF(detail::is_compiled_string<S>::value)>
+FMT_CONSTEXPR20 auto formatted_size(const S& format_str, const Args&... args)
+ -> size_t {
+ return fmt::format_to(detail::counting_iterator(), format_str, args...)
+ .count();
+}
+
+template <typename S, typename... Args,
+ FMT_ENABLE_IF(detail::is_compiled_string<S>::value)>
+void print(std::FILE* f, const S& format_str, const Args&... args) {
+ memory_buffer buffer;
+ fmt::format_to(std::back_inserter(buffer), format_str, args...);
+ detail::print(f, {buffer.data(), buffer.size()});
+}
+
+template <typename S, typename... Args,
+ FMT_ENABLE_IF(detail::is_compiled_string<S>::value)>
+void print(const S& format_str, const Args&... args) {
+ print(stdout, format_str, args...);
+}
+
+#if FMT_USE_NONTYPE_TEMPLATE_ARGS
+inline namespace literals {
+template <detail_exported::fixed_string Str> constexpr auto operator""_cf() {
+ using char_t = remove_cvref_t<decltype(Str.data[0])>;
+ return detail::udl_compiled_string<char_t, sizeof(Str.data) / sizeof(char_t),
+ Str>();
+}
+} // namespace literals
+#endif
+
+FMT_END_EXPORT
+FMT_END_NAMESPACE
+
+#endif // FMT_COMPILE_H_
--- /dev/null
+// Formatting library for C++ - the core API for char/UTF-8
+//
+// Copyright (c) 2012 - present, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_CORE_H_
+#define FMT_CORE_H_
+
+#include <cstddef> // std::byte
+#include <cstdio> // std::FILE
+#include <cstring> // std::strlen
+#include <iterator>
+#include <limits>
+#include <memory> // std::addressof
+#include <string>
+#include <type_traits>
+
+// The fmt library version in the form major * 10000 + minor * 100 + patch.
+#define FMT_VERSION 100201
+
+#if defined(__clang__) && !defined(__ibmxl__)
+# define FMT_CLANG_VERSION (__clang_major__ * 100 + __clang_minor__)
+#else
+# define FMT_CLANG_VERSION 0
+#endif
+
+#if defined(__GNUC__) && !defined(__clang__) && !defined(__INTEL_COMPILER) && \
+ !defined(__NVCOMPILER)
+# define FMT_GCC_VERSION (__GNUC__ * 100 + __GNUC_MINOR__)
+#else
+# define FMT_GCC_VERSION 0
+#endif
+
+#ifndef FMT_GCC_PRAGMA
+// Workaround _Pragma bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=59884.
+# if FMT_GCC_VERSION >= 504
+# define FMT_GCC_PRAGMA(arg) _Pragma(arg)
+# else
+# define FMT_GCC_PRAGMA(arg)
+# endif
+#endif
+
+#ifdef __ICL
+# define FMT_ICC_VERSION __ICL
+#elif defined(__INTEL_COMPILER)
+# define FMT_ICC_VERSION __INTEL_COMPILER
+#else
+# define FMT_ICC_VERSION 0
+#endif
+
+#ifdef _MSC_VER
+# define FMT_MSC_VERSION _MSC_VER
+# define FMT_MSC_WARNING(...) __pragma(warning(__VA_ARGS__))
+#else
+# define FMT_MSC_VERSION 0
+# define FMT_MSC_WARNING(...)
+#endif
+
+#ifdef _MSVC_LANG
+# define FMT_CPLUSPLUS _MSVC_LANG
+#else
+# define FMT_CPLUSPLUS __cplusplus
+#endif
+
+#ifdef __has_feature
+# define FMT_HAS_FEATURE(x) __has_feature(x)
+#else
+# define FMT_HAS_FEATURE(x) 0
+#endif
+
+#if defined(__has_include) || FMT_ICC_VERSION >= 1600 || FMT_MSC_VERSION > 1900
+# define FMT_HAS_INCLUDE(x) __has_include(x)
+#else
+# define FMT_HAS_INCLUDE(x) 0
+#endif
+
+#ifdef __has_cpp_attribute
+# define FMT_HAS_CPP_ATTRIBUTE(x) __has_cpp_attribute(x)
+#else
+# define FMT_HAS_CPP_ATTRIBUTE(x) 0
+#endif
+
+#define FMT_HAS_CPP14_ATTRIBUTE(attribute) \
+ (FMT_CPLUSPLUS >= 201402L && FMT_HAS_CPP_ATTRIBUTE(attribute))
+
+#define FMT_HAS_CPP17_ATTRIBUTE(attribute) \
+ (FMT_CPLUSPLUS >= 201703L && FMT_HAS_CPP_ATTRIBUTE(attribute))
+
+// Check if relaxed C++14 constexpr is supported.
+// GCC doesn't allow throw in constexpr until version 6 (bug 67371).
+#ifndef FMT_USE_CONSTEXPR
+# if (FMT_HAS_FEATURE(cxx_relaxed_constexpr) || FMT_MSC_VERSION >= 1912 || \
+ (FMT_GCC_VERSION >= 600 && FMT_CPLUSPLUS >= 201402L)) && \
+ !FMT_ICC_VERSION && (!defined(__NVCC__) || FMT_CPLUSPLUS >= 202002L)
+# define FMT_USE_CONSTEXPR 1
+# else
+# define FMT_USE_CONSTEXPR 0
+# endif
+#endif
+#if FMT_USE_CONSTEXPR
+# define FMT_CONSTEXPR constexpr
+#else
+# define FMT_CONSTEXPR
+#endif
+
+#if (FMT_CPLUSPLUS >= 202002L || \
+ (FMT_CPLUSPLUS >= 201709L && FMT_GCC_VERSION >= 1002)) && \
+ ((!defined(_GLIBCXX_RELEASE) || _GLIBCXX_RELEASE >= 10) && \
+ (!defined(_LIBCPP_VERSION) || _LIBCPP_VERSION >= 10000) && \
+ (!FMT_MSC_VERSION || FMT_MSC_VERSION >= 1928)) && \
+ defined(__cpp_lib_is_constant_evaluated)
+# define FMT_CONSTEXPR20 constexpr
+#else
+# define FMT_CONSTEXPR20
+#endif
+
+// Check if constexpr std::char_traits<>::{compare,length} are supported.
+#if defined(__GLIBCXX__)
+# if FMT_CPLUSPLUS >= 201703L && defined(_GLIBCXX_RELEASE) && \
+ _GLIBCXX_RELEASE >= 7 // GCC 7+ libstdc++ has _GLIBCXX_RELEASE.
+# define FMT_CONSTEXPR_CHAR_TRAITS constexpr
+# endif
+#elif defined(_LIBCPP_VERSION) && FMT_CPLUSPLUS >= 201703L && \
+ _LIBCPP_VERSION >= 4000
+# define FMT_CONSTEXPR_CHAR_TRAITS constexpr
+#elif FMT_MSC_VERSION >= 1914 && FMT_CPLUSPLUS >= 201703L
+# define FMT_CONSTEXPR_CHAR_TRAITS constexpr
+#endif
+#ifndef FMT_CONSTEXPR_CHAR_TRAITS
+# define FMT_CONSTEXPR_CHAR_TRAITS
+#endif
+
+// Check if exceptions are disabled.
+#ifndef FMT_EXCEPTIONS
+# if (defined(__GNUC__) && !defined(__EXCEPTIONS)) || \
+ (FMT_MSC_VERSION && !_HAS_EXCEPTIONS)
+# define FMT_EXCEPTIONS 0
+# else
+# define FMT_EXCEPTIONS 1
+# endif
+#endif
+
+// Disable [[noreturn]] on MSVC/NVCC because of bogus unreachable code warnings.
+#if FMT_EXCEPTIONS && FMT_HAS_CPP_ATTRIBUTE(noreturn) && !FMT_MSC_VERSION && \
+ !defined(__NVCC__)
+# define FMT_NORETURN [[noreturn]]
+#else
+# define FMT_NORETURN
+#endif
+
+#ifndef FMT_NODISCARD
+# if FMT_HAS_CPP17_ATTRIBUTE(nodiscard)
+# define FMT_NODISCARD [[nodiscard]]
+# else
+# define FMT_NODISCARD
+# endif
+#endif
+
+#ifndef FMT_INLINE
+# if FMT_GCC_VERSION || FMT_CLANG_VERSION
+# define FMT_INLINE inline __attribute__((always_inline))
+# else
+# define FMT_INLINE inline
+# endif
+#endif
+
+#ifdef _MSC_VER
+# define FMT_UNCHECKED_ITERATOR(It) \
+ using _Unchecked_type = It // Mark iterator as checked.
+#else
+# define FMT_UNCHECKED_ITERATOR(It) using unchecked_type = It
+#endif
+
+#ifndef FMT_BEGIN_NAMESPACE
+# define FMT_BEGIN_NAMESPACE \
+ namespace fmt { \
+ inline namespace v10 {
+# define FMT_END_NAMESPACE \
+ } \
+ }
+#endif
+
+#ifndef FMT_EXPORT
+# define FMT_EXPORT
+# define FMT_BEGIN_EXPORT
+# define FMT_END_EXPORT
+#endif
+
+#if FMT_GCC_VERSION || FMT_CLANG_VERSION
+# define FMT_VISIBILITY(value) __attribute__((visibility(value)))
+#else
+# define FMT_VISIBILITY(value)
+#endif
+
+#if !defined(FMT_HEADER_ONLY) && defined(_WIN32)
+# if defined(FMT_LIB_EXPORT)
+# define FMT_API __declspec(dllexport)
+# elif defined(FMT_SHARED)
+# define FMT_API __declspec(dllimport)
+# endif
+#elif defined(FMT_LIB_EXPORT) || defined(FMT_SHARED)
+# define FMT_API FMT_VISIBILITY("default")
+#endif
+#ifndef FMT_API
+# define FMT_API
+#endif
+
+// libc++ supports string_view in pre-c++17.
+#if FMT_HAS_INCLUDE(<string_view>) && \
+ (FMT_CPLUSPLUS >= 201703L || defined(_LIBCPP_VERSION))
+# include <string_view>
+# define FMT_USE_STRING_VIEW
+#elif FMT_HAS_INCLUDE("experimental/string_view") && FMT_CPLUSPLUS >= 201402L
+# include <experimental/string_view>
+# define FMT_USE_EXPERIMENTAL_STRING_VIEW
+#endif
+
+#ifndef FMT_UNICODE
+# define FMT_UNICODE !FMT_MSC_VERSION
+#endif
+
+#ifndef FMT_CONSTEVAL
+# if ((FMT_GCC_VERSION >= 1000 || FMT_CLANG_VERSION >= 1101) && \
+ (!defined(__apple_build_version__) || \
+ __apple_build_version__ >= 14000029L) && \
+ FMT_CPLUSPLUS >= 202002L) || \
+ (defined(__cpp_consteval) && \
+ (!FMT_MSC_VERSION || FMT_MSC_VERSION >= 1929))
+// consteval is broken in MSVC before VS2019 version 16.10 and Apple clang
+// before 14.
+# define FMT_CONSTEVAL consteval
+# define FMT_HAS_CONSTEVAL
+# else
+# define FMT_CONSTEVAL
+# endif
+#endif
+
+#ifndef FMT_USE_NONTYPE_TEMPLATE_ARGS
+# if defined(__cpp_nontype_template_args) && \
+ ((FMT_GCC_VERSION >= 903 && FMT_CPLUSPLUS >= 201709L) || \
+ __cpp_nontype_template_args >= 201911L) && \
+ !defined(__NVCOMPILER) && !defined(__LCC__)
+# define FMT_USE_NONTYPE_TEMPLATE_ARGS 1
+# else
+# define FMT_USE_NONTYPE_TEMPLATE_ARGS 0
+# endif
+#endif
+
+// GCC < 5 requires this-> in decltype
+#ifndef FMT_DECLTYPE_THIS
+# if FMT_GCC_VERSION && FMT_GCC_VERSION < 500
+# define FMT_DECLTYPE_THIS this->
+# else
+# define FMT_DECLTYPE_THIS
+# endif
+#endif
+
+// Enable minimal optimizations for more compact code in debug mode.
+FMT_GCC_PRAGMA("GCC push_options")
+#if !defined(__OPTIMIZE__) && !defined(__NVCOMPILER) && !defined(__LCC__) && \
+ !defined(__CUDACC__)
+FMT_GCC_PRAGMA("GCC optimize(\"Og\")")
+#endif
+
+FMT_BEGIN_NAMESPACE
+
+// Implementations of enable_if_t and other metafunctions for older systems.
+template <bool B, typename T = void>
+using enable_if_t = typename std::enable_if<B, T>::type;
+template <bool B, typename T, typename F>
+using conditional_t = typename std::conditional<B, T, F>::type;
+template <bool B> using bool_constant = std::integral_constant<bool, B>;
+template <typename T>
+using remove_reference_t = typename std::remove_reference<T>::type;
+template <typename T>
+using remove_const_t = typename std::remove_const<T>::type;
+template <typename T>
+using remove_cvref_t = typename std::remove_cv<remove_reference_t<T>>::type;
+template <typename T> struct type_identity {
+ using type = T;
+};
+template <typename T> using type_identity_t = typename type_identity<T>::type;
+template <typename T>
+using underlying_t = typename std::underlying_type<T>::type;
+
+// Checks whether T is a container with contiguous storage.
+template <typename T> struct is_contiguous : std::false_type {};
+template <typename Char>
+struct is_contiguous<std::basic_string<Char>> : std::true_type {};
+
+struct monostate {
+ constexpr monostate() {}
+};
+
+// An enable_if helper to be used in template parameters which results in much
+// shorter symbols: https://godbolt.org/z/sWw4vP. Extra parentheses are needed
+// to workaround a bug in MSVC 2019 (see #1140 and #1186).
+#ifdef FMT_DOC
+# define FMT_ENABLE_IF(...)
+#else
+# define FMT_ENABLE_IF(...) fmt::enable_if_t<(__VA_ARGS__), int> = 0
+#endif
+
+// This is defined in core.h instead of format.h to avoid injecting in std.
+// It is a template to avoid undesirable implicit conversions to std::byte.
+#ifdef __cpp_lib_byte
+template <typename T, FMT_ENABLE_IF(std::is_same<T, std::byte>::value)>
+inline auto format_as(T b) -> unsigned char {
+ return static_cast<unsigned char>(b);
+}
+#endif
+
+namespace detail {
+// Suppresses "unused variable" warnings with the method described in
+// https://herbsutter.com/2009/10/18/mailbag-shutting-up-compiler-warnings/.
+// (void)var does not work on many Intel compilers.
+template <typename... T> FMT_CONSTEXPR void ignore_unused(const T&...) {}
+
+constexpr FMT_INLINE auto is_constant_evaluated(
+ bool default_value = false) noexcept -> bool {
+// Workaround for incompatibility between libstdc++ consteval-based
+// std::is_constant_evaluated() implementation and clang-14.
+// https://github.com/fmtlib/fmt/issues/3247
+#if FMT_CPLUSPLUS >= 202002L && defined(_GLIBCXX_RELEASE) && \
+ _GLIBCXX_RELEASE >= 12 && \
+ (FMT_CLANG_VERSION >= 1400 && FMT_CLANG_VERSION < 1500)
+ ignore_unused(default_value);
+ return __builtin_is_constant_evaluated();
+#elif defined(__cpp_lib_is_constant_evaluated)
+ ignore_unused(default_value);
+ return std::is_constant_evaluated();
+#else
+ return default_value;
+#endif
+}
+
+// Suppresses "conditional expression is constant" warnings.
+template <typename T> constexpr FMT_INLINE auto const_check(T value) -> T {
+ return value;
+}
+
+FMT_NORETURN FMT_API void assert_fail(const char* file, int line,
+ const char* message);
+
+#ifndef FMT_ASSERT
+# ifdef NDEBUG
+// FMT_ASSERT is not empty to avoid -Wempty-body.
+# define FMT_ASSERT(condition, message) \
+ fmt::detail::ignore_unused((condition), (message))
+# else
+# define FMT_ASSERT(condition, message) \
+ ((condition) /* void() fails with -Winvalid-constexpr on clang 4.0.1 */ \
+ ? (void)0 \
+ : fmt::detail::assert_fail(__FILE__, __LINE__, (message)))
+# endif
+#endif
+
+#if defined(FMT_USE_STRING_VIEW)
+template <typename Char> using std_string_view = std::basic_string_view<Char>;
+#elif defined(FMT_USE_EXPERIMENTAL_STRING_VIEW)
+template <typename Char>
+using std_string_view = std::experimental::basic_string_view<Char>;
+#else
+template <typename T> struct std_string_view {};
+#endif
+
+#ifdef FMT_USE_INT128
+// Do nothing.
+#elif defined(__SIZEOF_INT128__) && !defined(__NVCC__) && \
+ !(FMT_CLANG_VERSION && FMT_MSC_VERSION)
+# define FMT_USE_INT128 1
+using int128_opt = __int128_t; // An optional native 128-bit integer.
+using uint128_opt = __uint128_t;
+template <typename T> inline auto convert_for_visit(T value) -> T {
+ return value;
+}
+#else
+# define FMT_USE_INT128 0
+#endif
+#if !FMT_USE_INT128
+enum class int128_opt {};
+enum class uint128_opt {};
+// Reduce template instantiations.
+template <typename T> auto convert_for_visit(T) -> monostate { return {}; }
+#endif
+
+// Casts a nonnegative integer to unsigned.
+template <typename Int>
+FMT_CONSTEXPR auto to_unsigned(Int value) ->
+ typename std::make_unsigned<Int>::type {
+ FMT_ASSERT(std::is_unsigned<Int>::value || value >= 0, "negative value");
+ return static_cast<typename std::make_unsigned<Int>::type>(value);
+}
+
+FMT_CONSTEXPR inline auto is_utf8() -> bool {
+ FMT_MSC_WARNING(suppress : 4566) constexpr unsigned char section[] = "\u00A7";
+
+ // Avoid buggy sign extensions in MSVC's constant evaluation mode (#2297).
+ using uchar = unsigned char;
+ return FMT_UNICODE || (sizeof(section) == 3 && uchar(section[0]) == 0xC2 &&
+ uchar(section[1]) == 0xA7);
+}
+} // namespace detail
+
+/**
+ An implementation of ``std::basic_string_view`` for pre-C++17. It provides a
+ subset of the API. ``fmt::basic_string_view`` is used for format strings even
+ if ``std::string_view`` is available to prevent issues when a library is
+ compiled with a different ``-std`` option than the client code (which is not
+ recommended).
+ */
+FMT_EXPORT
+template <typename Char> class basic_string_view {
+ private:
+ const Char* data_;
+ size_t size_;
+
+ public:
+ using value_type = Char;
+ using iterator = const Char*;
+
+ constexpr basic_string_view() noexcept : data_(nullptr), size_(0) {}
+
+ /** Constructs a string reference object from a C string and a size. */
+ constexpr basic_string_view(const Char* s, size_t count) noexcept
+ : data_(s), size_(count) {}
+
+ /**
+ \rst
+ Constructs a string reference object from a C string computing
+ the size with ``std::char_traits<Char>::length``.
+ \endrst
+ */
+ FMT_CONSTEXPR_CHAR_TRAITS
+ FMT_INLINE
+ basic_string_view(const Char* s)
+ : data_(s),
+ size_(detail::const_check(std::is_same<Char, char>::value &&
+ !detail::is_constant_evaluated(true))
+ ? std::strlen(reinterpret_cast<const char*>(s))
+ : std::char_traits<Char>::length(s)) {}
+
+ /** Constructs a string reference from a ``std::basic_string`` object. */
+ template <typename Traits, typename Alloc>
+ FMT_CONSTEXPR basic_string_view(
+ const std::basic_string<Char, Traits, Alloc>& s) noexcept
+ : data_(s.data()), size_(s.size()) {}
+
+ template <typename S, FMT_ENABLE_IF(std::is_same<
+ S, detail::std_string_view<Char>>::value)>
+ FMT_CONSTEXPR basic_string_view(S s) noexcept
+ : data_(s.data()), size_(s.size()) {}
+
+ /** Returns a pointer to the string data. */
+ constexpr auto data() const noexcept -> const Char* { return data_; }
+
+ /** Returns the string size. */
+ constexpr auto size() const noexcept -> size_t { return size_; }
+
+ constexpr auto begin() const noexcept -> iterator { return data_; }
+ constexpr auto end() const noexcept -> iterator { return data_ + size_; }
+
+ constexpr auto operator[](size_t pos) const noexcept -> const Char& {
+ return data_[pos];
+ }
+
+ FMT_CONSTEXPR void remove_prefix(size_t n) noexcept {
+ data_ += n;
+ size_ -= n;
+ }
+
+ FMT_CONSTEXPR_CHAR_TRAITS auto starts_with(
+ basic_string_view<Char> sv) const noexcept -> bool {
+ return size_ >= sv.size_ &&
+ std::char_traits<Char>::compare(data_, sv.data_, sv.size_) == 0;
+ }
+ FMT_CONSTEXPR_CHAR_TRAITS auto starts_with(Char c) const noexcept -> bool {
+ return size_ >= 1 && std::char_traits<Char>::eq(*data_, c);
+ }
+ FMT_CONSTEXPR_CHAR_TRAITS auto starts_with(const Char* s) const -> bool {
+ return starts_with(basic_string_view<Char>(s));
+ }
+
+ // Lexicographically compare this string reference to other.
+ FMT_CONSTEXPR_CHAR_TRAITS auto compare(basic_string_view other) const -> int {
+ size_t str_size = size_ < other.size_ ? size_ : other.size_;
+ int result = std::char_traits<Char>::compare(data_, other.data_, str_size);
+ if (result == 0)
+ result = size_ == other.size_ ? 0 : (size_ < other.size_ ? -1 : 1);
+ return result;
+ }
+
+ FMT_CONSTEXPR_CHAR_TRAITS friend auto operator==(basic_string_view lhs,
+ basic_string_view rhs)
+ -> bool {
+ return lhs.compare(rhs) == 0;
+ }
+ friend auto operator!=(basic_string_view lhs, basic_string_view rhs) -> bool {
+ return lhs.compare(rhs) != 0;
+ }
+ friend auto operator<(basic_string_view lhs, basic_string_view rhs) -> bool {
+ return lhs.compare(rhs) < 0;
+ }
+ friend auto operator<=(basic_string_view lhs, basic_string_view rhs) -> bool {
+ return lhs.compare(rhs) <= 0;
+ }
+ friend auto operator>(basic_string_view lhs, basic_string_view rhs) -> bool {
+ return lhs.compare(rhs) > 0;
+ }
+ friend auto operator>=(basic_string_view lhs, basic_string_view rhs) -> bool {
+ return lhs.compare(rhs) >= 0;
+ }
+};
+
+FMT_EXPORT
+using string_view = basic_string_view<char>;
+
+/** Specifies if ``T`` is a character type. Can be specialized by users. */
+FMT_EXPORT
+template <typename T> struct is_char : std::false_type {};
+template <> struct is_char<char> : std::true_type {};
+
+namespace detail {
+
+// A base class for compile-time strings.
+struct compile_string {};
+
+template <typename S>
+struct is_compile_string : std::is_base_of<compile_string, S> {};
+
+template <typename Char, FMT_ENABLE_IF(is_char<Char>::value)>
+FMT_INLINE auto to_string_view(const Char* s) -> basic_string_view<Char> {
+ return s;
+}
+template <typename Char, typename Traits, typename Alloc>
+inline auto to_string_view(const std::basic_string<Char, Traits, Alloc>& s)
+ -> basic_string_view<Char> {
+ return s;
+}
+template <typename Char>
+constexpr auto to_string_view(basic_string_view<Char> s)
+ -> basic_string_view<Char> {
+ return s;
+}
+template <typename Char,
+ FMT_ENABLE_IF(!std::is_empty<std_string_view<Char>>::value)>
+inline auto to_string_view(std_string_view<Char> s) -> basic_string_view<Char> {
+ return s;
+}
+template <typename S, FMT_ENABLE_IF(is_compile_string<S>::value)>
+constexpr auto to_string_view(const S& s)
+ -> basic_string_view<typename S::char_type> {
+ return basic_string_view<typename S::char_type>(s);
+}
+void to_string_view(...);
+
+// Specifies whether S is a string type convertible to fmt::basic_string_view.
+// It should be a constexpr function but MSVC 2017 fails to compile it in
+// enable_if and MSVC 2015 fails to compile it as an alias template.
+// ADL is intentionally disabled as to_string_view is not an extension point.
+template <typename S>
+struct is_string
+ : std::is_class<decltype(detail::to_string_view(std::declval<S>()))> {};
+
+template <typename S, typename = void> struct char_t_impl {};
+template <typename S> struct char_t_impl<S, enable_if_t<is_string<S>::value>> {
+ using result = decltype(to_string_view(std::declval<S>()));
+ using type = typename result::value_type;
+};
+
+enum class type {
+ none_type,
+ // Integer types should go first,
+ int_type,
+ uint_type,
+ long_long_type,
+ ulong_long_type,
+ int128_type,
+ uint128_type,
+ bool_type,
+ char_type,
+ last_integer_type = char_type,
+ // followed by floating-point types.
+ float_type,
+ double_type,
+ long_double_type,
+ last_numeric_type = long_double_type,
+ cstring_type,
+ string_type,
+ pointer_type,
+ custom_type
+};
+
+// Maps core type T to the corresponding type enum constant.
+template <typename T, typename Char>
+struct type_constant : std::integral_constant<type, type::custom_type> {};
+
+#define FMT_TYPE_CONSTANT(Type, constant) \
+ template <typename Char> \
+ struct type_constant<Type, Char> \
+ : std::integral_constant<type, type::constant> {}
+
+FMT_TYPE_CONSTANT(int, int_type);
+FMT_TYPE_CONSTANT(unsigned, uint_type);
+FMT_TYPE_CONSTANT(long long, long_long_type);
+FMT_TYPE_CONSTANT(unsigned long long, ulong_long_type);
+FMT_TYPE_CONSTANT(int128_opt, int128_type);
+FMT_TYPE_CONSTANT(uint128_opt, uint128_type);
+FMT_TYPE_CONSTANT(bool, bool_type);
+FMT_TYPE_CONSTANT(Char, char_type);
+FMT_TYPE_CONSTANT(float, float_type);
+FMT_TYPE_CONSTANT(double, double_type);
+FMT_TYPE_CONSTANT(long double, long_double_type);
+FMT_TYPE_CONSTANT(const Char*, cstring_type);
+FMT_TYPE_CONSTANT(basic_string_view<Char>, string_type);
+FMT_TYPE_CONSTANT(const void*, pointer_type);
+
+constexpr auto is_integral_type(type t) -> bool {
+ return t > type::none_type && t <= type::last_integer_type;
+}
+constexpr auto is_arithmetic_type(type t) -> bool {
+ return t > type::none_type && t <= type::last_numeric_type;
+}
+
+constexpr auto set(type rhs) -> int { return 1 << static_cast<int>(rhs); }
+constexpr auto in(type t, int set) -> bool {
+ return ((set >> static_cast<int>(t)) & 1) != 0;
+}
+
+// Bitsets of types.
+enum {
+ sint_set =
+ set(type::int_type) | set(type::long_long_type) | set(type::int128_type),
+ uint_set = set(type::uint_type) | set(type::ulong_long_type) |
+ set(type::uint128_type),
+ bool_set = set(type::bool_type),
+ char_set = set(type::char_type),
+ float_set = set(type::float_type) | set(type::double_type) |
+ set(type::long_double_type),
+ string_set = set(type::string_type),
+ cstring_set = set(type::cstring_type),
+ pointer_set = set(type::pointer_type)
+};
+
+// DEPRECATED!
+FMT_NORETURN FMT_API void throw_format_error(const char* message);
+
+struct error_handler {
+ constexpr error_handler() = default;
+
+ // This function is intentionally not constexpr to give a compile-time error.
+ FMT_NORETURN void on_error(const char* message) {
+ throw_format_error(message);
+ }
+};
+} // namespace detail
+
+/** Throws ``format_error`` with a given message. */
+using detail::throw_format_error;
+
+/** String's character type. */
+template <typename S> using char_t = typename detail::char_t_impl<S>::type;
+
+/**
+ \rst
+ Parsing context consisting of a format string range being parsed and an
+ argument counter for automatic indexing.
+ You can use the ``format_parse_context`` type alias for ``char`` instead.
+ \endrst
+ */
+FMT_EXPORT
+template <typename Char> class basic_format_parse_context {
+ private:
+ basic_string_view<Char> format_str_;
+ int next_arg_id_;
+
+ FMT_CONSTEXPR void do_check_arg_id(int id);
+
+ public:
+ using char_type = Char;
+ using iterator = const Char*;
+
+ explicit constexpr basic_format_parse_context(
+ basic_string_view<Char> format_str, int next_arg_id = 0)
+ : format_str_(format_str), next_arg_id_(next_arg_id) {}
+
+ /**
+ Returns an iterator to the beginning of the format string range being
+ parsed.
+ */
+ constexpr auto begin() const noexcept -> iterator {
+ return format_str_.begin();
+ }
+
+ /**
+ Returns an iterator past the end of the format string range being parsed.
+ */
+ constexpr auto end() const noexcept -> iterator { return format_str_.end(); }
+
+ /** Advances the begin iterator to ``it``. */
+ FMT_CONSTEXPR void advance_to(iterator it) {
+ format_str_.remove_prefix(detail::to_unsigned(it - begin()));
+ }
+
+ /**
+ Reports an error if using the manual argument indexing; otherwise returns
+ the next argument index and switches to the automatic indexing.
+ */
+ FMT_CONSTEXPR auto next_arg_id() -> int {
+ if (next_arg_id_ < 0) {
+ detail::throw_format_error(
+ "cannot switch from manual to automatic argument indexing");
+ return 0;
+ }
+ int id = next_arg_id_++;
+ do_check_arg_id(id);
+ return id;
+ }
+
+ /**
+ Reports an error if using the automatic argument indexing; otherwise
+ switches to the manual indexing.
+ */
+ FMT_CONSTEXPR void check_arg_id(int id) {
+ if (next_arg_id_ > 0) {
+ detail::throw_format_error(
+ "cannot switch from automatic to manual argument indexing");
+ return;
+ }
+ next_arg_id_ = -1;
+ do_check_arg_id(id);
+ }
+ FMT_CONSTEXPR void check_arg_id(basic_string_view<Char>) {}
+ FMT_CONSTEXPR void check_dynamic_spec(int arg_id);
+};
+
+FMT_EXPORT
+using format_parse_context = basic_format_parse_context<char>;
+
+namespace detail {
+// A parse context with extra data used only in compile-time checks.
+template <typename Char>
+class compile_parse_context : public basic_format_parse_context<Char> {
+ private:
+ int num_args_;
+ const type* types_;
+ using base = basic_format_parse_context<Char>;
+
+ public:
+ explicit FMT_CONSTEXPR compile_parse_context(
+ basic_string_view<Char> format_str, int num_args, const type* types,
+ int next_arg_id = 0)
+ : base(format_str, next_arg_id), num_args_(num_args), types_(types) {}
+
+ constexpr auto num_args() const -> int { return num_args_; }
+ constexpr auto arg_type(int id) const -> type { return types_[id]; }
+
+ FMT_CONSTEXPR auto next_arg_id() -> int {
+ int id = base::next_arg_id();
+ if (id >= num_args_) throw_format_error("argument not found");
+ return id;
+ }
+
+ FMT_CONSTEXPR void check_arg_id(int id) {
+ base::check_arg_id(id);
+ if (id >= num_args_) throw_format_error("argument not found");
+ }
+ using base::check_arg_id;
+
+ FMT_CONSTEXPR void check_dynamic_spec(int arg_id) {
+ detail::ignore_unused(arg_id);
+#if !defined(__LCC__)
+ if (arg_id < num_args_ && types_ && !is_integral_type(types_[arg_id]))
+ throw_format_error("width/precision is not integer");
+#endif
+ }
+};
+
+// Extracts a reference to the container from back_insert_iterator.
+template <typename Container>
+inline auto get_container(std::back_insert_iterator<Container> it)
+ -> Container& {
+ using base = std::back_insert_iterator<Container>;
+ struct accessor : base {
+ accessor(base b) : base(b) {}
+ using base::container;
+ };
+ return *accessor(it).container;
+}
+
+template <typename Char, typename InputIt, typename OutputIt>
+FMT_CONSTEXPR auto copy_str(InputIt begin, InputIt end, OutputIt out)
+ -> OutputIt {
+ while (begin != end) *out++ = static_cast<Char>(*begin++);
+ return out;
+}
+
+template <typename Char, typename T, typename U,
+ FMT_ENABLE_IF(
+ std::is_same<remove_const_t<T>, U>::value&& is_char<U>::value)>
+FMT_CONSTEXPR auto copy_str(T* begin, T* end, U* out) -> U* {
+ if (is_constant_evaluated()) return copy_str<Char, T*, U*>(begin, end, out);
+ auto size = to_unsigned(end - begin);
+ if (size > 0) memcpy(out, begin, size * sizeof(U));
+ return out + size;
+}
+
+/**
+ \rst
+ A contiguous memory buffer with an optional growing ability. It is an internal
+ class and shouldn't be used directly, only via `~fmt::basic_memory_buffer`.
+ \endrst
+ */
+template <typename T> class buffer {
+ private:
+ T* ptr_;
+ size_t size_;
+ size_t capacity_;
+
+ protected:
+ // Don't initialize ptr_ since it is not accessed to save a few cycles.
+ FMT_MSC_WARNING(suppress : 26495)
+ FMT_CONSTEXPR buffer(size_t sz) noexcept : size_(sz), capacity_(sz) {}
+
+ FMT_CONSTEXPR20 buffer(T* p = nullptr, size_t sz = 0, size_t cap = 0) noexcept
+ : ptr_(p), size_(sz), capacity_(cap) {}
+
+ FMT_CONSTEXPR20 ~buffer() = default;
+ buffer(buffer&&) = default;
+
+ /** Sets the buffer data and capacity. */
+ FMT_CONSTEXPR void set(T* buf_data, size_t buf_capacity) noexcept {
+ ptr_ = buf_data;
+ capacity_ = buf_capacity;
+ }
+
+ /** Increases the buffer capacity to hold at least *capacity* elements. */
+ // DEPRECATED!
+ virtual FMT_CONSTEXPR20 void grow(size_t capacity) = 0;
+
+ public:
+ using value_type = T;
+ using const_reference = const T&;
+
+ buffer(const buffer&) = delete;
+ void operator=(const buffer&) = delete;
+
+ FMT_INLINE auto begin() noexcept -> T* { return ptr_; }
+ FMT_INLINE auto end() noexcept -> T* { return ptr_ + size_; }
+
+ FMT_INLINE auto begin() const noexcept -> const T* { return ptr_; }
+ FMT_INLINE auto end() const noexcept -> const T* { return ptr_ + size_; }
+
+ /** Returns the size of this buffer. */
+ constexpr auto size() const noexcept -> size_t { return size_; }
+
+ /** Returns the capacity of this buffer. */
+ constexpr auto capacity() const noexcept -> size_t { return capacity_; }
+
+ /** Returns a pointer to the buffer data (not null-terminated). */
+ FMT_CONSTEXPR auto data() noexcept -> T* { return ptr_; }
+ FMT_CONSTEXPR auto data() const noexcept -> const T* { return ptr_; }
+
+ /** Clears this buffer. */
+ void clear() { size_ = 0; }
+
+ // Tries resizing the buffer to contain *count* elements. If T is a POD type
+ // the new elements may not be initialized.
+ FMT_CONSTEXPR20 void try_resize(size_t count) {
+ try_reserve(count);
+ size_ = count <= capacity_ ? count : capacity_;
+ }
+
+ // Tries increasing the buffer capacity to *new_capacity*. It can increase the
+ // capacity by a smaller amount than requested but guarantees there is space
+ // for at least one additional element either by increasing the capacity or by
+ // flushing the buffer if it is full.
+ FMT_CONSTEXPR20 void try_reserve(size_t new_capacity) {
+ if (new_capacity > capacity_) grow(new_capacity);
+ }
+
+ FMT_CONSTEXPR20 void push_back(const T& value) {
+ try_reserve(size_ + 1);
+ ptr_[size_++] = value;
+ }
+
+ /** Appends data to the end of the buffer. */
+ template <typename U> void append(const U* begin, const U* end);
+
+ template <typename Idx> FMT_CONSTEXPR auto operator[](Idx index) -> T& {
+ return ptr_[index];
+ }
+ template <typename Idx>
+ FMT_CONSTEXPR auto operator[](Idx index) const -> const T& {
+ return ptr_[index];
+ }
+};
+
+struct buffer_traits {
+ explicit buffer_traits(size_t) {}
+ auto count() const -> size_t { return 0; }
+ auto limit(size_t size) -> size_t { return size; }
+};
+
+class fixed_buffer_traits {
+ private:
+ size_t count_ = 0;
+ size_t limit_;
+
+ public:
+ explicit fixed_buffer_traits(size_t limit) : limit_(limit) {}
+ auto count() const -> size_t { return count_; }
+ auto limit(size_t size) -> size_t {
+ size_t n = limit_ > count_ ? limit_ - count_ : 0;
+ count_ += size;
+ return size < n ? size : n;
+ }
+};
+
+// A buffer that writes to an output iterator when flushed.
+template <typename OutputIt, typename T, typename Traits = buffer_traits>
+class iterator_buffer final : public Traits, public buffer<T> {
+ private:
+ OutputIt out_;
+ enum { buffer_size = 256 };
+ T data_[buffer_size];
+
+ protected:
+ FMT_CONSTEXPR20 void grow(size_t) override {
+ if (this->size() == buffer_size) flush();
+ }
+
+ void flush() {
+ auto size = this->size();
+ this->clear();
+ out_ = copy_str<T>(data_, data_ + this->limit(size), out_);
+ }
+
+ public:
+ explicit iterator_buffer(OutputIt out, size_t n = buffer_size)
+ : Traits(n), buffer<T>(data_, 0, buffer_size), out_(out) {}
+ iterator_buffer(iterator_buffer&& other)
+ : Traits(other), buffer<T>(data_, 0, buffer_size), out_(other.out_) {}
+ ~iterator_buffer() { flush(); }
+
+ auto out() -> OutputIt {
+ flush();
+ return out_;
+ }
+ auto count() const -> size_t { return Traits::count() + this->size(); }
+};
+
+template <typename T>
+class iterator_buffer<T*, T, fixed_buffer_traits> final
+ : public fixed_buffer_traits,
+ public buffer<T> {
+ private:
+ T* out_;
+ enum { buffer_size = 256 };
+ T data_[buffer_size];
+
+ protected:
+ FMT_CONSTEXPR20 void grow(size_t) override {
+ if (this->size() == this->capacity()) flush();
+ }
+
+ void flush() {
+ size_t n = this->limit(this->size());
+ if (this->data() == out_) {
+ out_ += n;
+ this->set(data_, buffer_size);
+ }
+ this->clear();
+ }
+
+ public:
+ explicit iterator_buffer(T* out, size_t n = buffer_size)
+ : fixed_buffer_traits(n), buffer<T>(out, 0, n), out_(out) {}
+ iterator_buffer(iterator_buffer&& other)
+ : fixed_buffer_traits(other),
+ buffer<T>(std::move(other)),
+ out_(other.out_) {
+ if (this->data() != out_) {
+ this->set(data_, buffer_size);
+ this->clear();
+ }
+ }
+ ~iterator_buffer() { flush(); }
+
+ auto out() -> T* {
+ flush();
+ return out_;
+ }
+ auto count() const -> size_t {
+ return fixed_buffer_traits::count() + this->size();
+ }
+};
+
+template <typename T> class iterator_buffer<T*, T> final : public buffer<T> {
+ protected:
+ FMT_CONSTEXPR20 void grow(size_t) override {}
+
+ public:
+ explicit iterator_buffer(T* out, size_t = 0) : buffer<T>(out, 0, ~size_t()) {}
+
+ auto out() -> T* { return &*this->end(); }
+};
+
+// A buffer that writes to a container with the contiguous storage.
+template <typename Container>
+class iterator_buffer<std::back_insert_iterator<Container>,
+ enable_if_t<is_contiguous<Container>::value,
+ typename Container::value_type>>
+ final : public buffer<typename Container::value_type> {
+ private:
+ Container& container_;
+
+ protected:
+ FMT_CONSTEXPR20 void grow(size_t capacity) override {
+ container_.resize(capacity);
+ this->set(&container_[0], capacity);
+ }
+
+ public:
+ explicit iterator_buffer(Container& c)
+ : buffer<typename Container::value_type>(c.size()), container_(c) {}
+ explicit iterator_buffer(std::back_insert_iterator<Container> out, size_t = 0)
+ : iterator_buffer(get_container(out)) {}
+
+ auto out() -> std::back_insert_iterator<Container> {
+ return std::back_inserter(container_);
+ }
+};
+
+// A buffer that counts the number of code units written discarding the output.
+template <typename T = char> class counting_buffer final : public buffer<T> {
+ private:
+ enum { buffer_size = 256 };
+ T data_[buffer_size];
+ size_t count_ = 0;
+
+ protected:
+ FMT_CONSTEXPR20 void grow(size_t) override {
+ if (this->size() != buffer_size) return;
+ count_ += this->size();
+ this->clear();
+ }
+
+ public:
+ counting_buffer() : buffer<T>(data_, 0, buffer_size) {}
+
+ auto count() -> size_t { return count_ + this->size(); }
+};
+} // namespace detail
+
+template <typename Char>
+FMT_CONSTEXPR void basic_format_parse_context<Char>::do_check_arg_id(int id) {
+ // Argument id is only checked at compile-time during parsing because
+ // formatting has its own validation.
+ if (detail::is_constant_evaluated() &&
+ (!FMT_GCC_VERSION || FMT_GCC_VERSION >= 1200)) {
+ using context = detail::compile_parse_context<Char>;
+ if (id >= static_cast<context*>(this)->num_args())
+ detail::throw_format_error("argument not found");
+ }
+}
+
+template <typename Char>
+FMT_CONSTEXPR void basic_format_parse_context<Char>::check_dynamic_spec(
+ int arg_id) {
+ if (detail::is_constant_evaluated() &&
+ (!FMT_GCC_VERSION || FMT_GCC_VERSION >= 1200)) {
+ using context = detail::compile_parse_context<Char>;
+ static_cast<context*>(this)->check_dynamic_spec(arg_id);
+ }
+}
+
+FMT_EXPORT template <typename Context> class basic_format_arg;
+FMT_EXPORT template <typename Context> class basic_format_args;
+FMT_EXPORT template <typename Context> class dynamic_format_arg_store;
+
+// A formatter for objects of type T.
+FMT_EXPORT
+template <typename T, typename Char = char, typename Enable = void>
+struct formatter {
+ // A deleted default constructor indicates a disabled formatter.
+ formatter() = delete;
+};
+
+// Specifies if T has an enabled formatter specialization. A type can be
+// formattable even if it doesn't have a formatter e.g. via a conversion.
+template <typename T, typename Context>
+using has_formatter =
+ std::is_constructible<typename Context::template formatter_type<T>>;
+
+// An output iterator that appends to a buffer.
+// It is used to reduce symbol sizes for the common case.
+class appender : public std::back_insert_iterator<detail::buffer<char>> {
+ using base = std::back_insert_iterator<detail::buffer<char>>;
+
+ public:
+ using std::back_insert_iterator<detail::buffer<char>>::back_insert_iterator;
+ appender(base it) noexcept : base(it) {}
+ FMT_UNCHECKED_ITERATOR(appender);
+
+ auto operator++() noexcept -> appender& { return *this; }
+ auto operator++(int) noexcept -> appender { return *this; }
+};
+
+namespace detail {
+
+template <typename Context, typename T>
+constexpr auto has_const_formatter_impl(T*)
+ -> decltype(typename Context::template formatter_type<T>().format(
+ std::declval<const T&>(), std::declval<Context&>()),
+ true) {
+ return true;
+}
+template <typename Context>
+constexpr auto has_const_formatter_impl(...) -> bool {
+ return false;
+}
+template <typename T, typename Context>
+constexpr auto has_const_formatter() -> bool {
+ return has_const_formatter_impl<Context>(static_cast<T*>(nullptr));
+}
+
+template <typename T>
+using buffer_appender = conditional_t<std::is_same<T, char>::value, appender,
+ std::back_insert_iterator<buffer<T>>>;
+
+// Maps an output iterator to a buffer.
+template <typename T, typename OutputIt>
+auto get_buffer(OutputIt out) -> iterator_buffer<OutputIt, T> {
+ return iterator_buffer<OutputIt, T>(out);
+}
+template <typename T, typename Buf,
+ FMT_ENABLE_IF(std::is_base_of<buffer<char>, Buf>::value)>
+auto get_buffer(std::back_insert_iterator<Buf> out) -> buffer<char>& {
+ return get_container(out);
+}
+
+template <typename Buf, typename OutputIt>
+FMT_INLINE auto get_iterator(Buf& buf, OutputIt) -> decltype(buf.out()) {
+ return buf.out();
+}
+template <typename T, typename OutputIt>
+auto get_iterator(buffer<T>&, OutputIt out) -> OutputIt {
+ return out;
+}
+
+struct view {};
+
+template <typename Char, typename T> struct named_arg : view {
+ const Char* name;
+ const T& value;
+ named_arg(const Char* n, const T& v) : name(n), value(v) {}
+};
+
+template <typename Char> struct named_arg_info {
+ const Char* name;
+ int id;
+};
+
+template <typename T, typename Char, size_t NUM_ARGS, size_t NUM_NAMED_ARGS>
+struct arg_data {
+ // args_[0].named_args points to named_args_ to avoid bloating format_args.
+ // +1 to workaround a bug in gcc 7.5 that causes duplicated-branches warning.
+ T args_[1 + (NUM_ARGS != 0 ? NUM_ARGS : +1)];
+ named_arg_info<Char> named_args_[NUM_NAMED_ARGS];
+
+ template <typename... U>
+ arg_data(const U&... init) : args_{T(named_args_, NUM_NAMED_ARGS), init...} {}
+ arg_data(const arg_data& other) = delete;
+ auto args() const -> const T* { return args_ + 1; }
+ auto named_args() -> named_arg_info<Char>* { return named_args_; }
+};
+
+template <typename T, typename Char, size_t NUM_ARGS>
+struct arg_data<T, Char, NUM_ARGS, 0> {
+ // +1 to workaround a bug in gcc 7.5 that causes duplicated-branches warning.
+ T args_[NUM_ARGS != 0 ? NUM_ARGS : +1];
+
+ template <typename... U>
+ FMT_CONSTEXPR FMT_INLINE arg_data(const U&... init) : args_{init...} {}
+ FMT_CONSTEXPR FMT_INLINE auto args() const -> const T* { return args_; }
+ FMT_CONSTEXPR FMT_INLINE auto named_args() -> std::nullptr_t {
+ return nullptr;
+ }
+};
+
+template <typename Char>
+inline void init_named_args(named_arg_info<Char>*, int, int) {}
+
+template <typename T> struct is_named_arg : std::false_type {};
+template <typename T> struct is_statically_named_arg : std::false_type {};
+
+template <typename T, typename Char>
+struct is_named_arg<named_arg<Char, T>> : std::true_type {};
+
+template <typename Char, typename T, typename... Tail,
+ FMT_ENABLE_IF(!is_named_arg<T>::value)>
+void init_named_args(named_arg_info<Char>* named_args, int arg_count,
+ int named_arg_count, const T&, const Tail&... args) {
+ init_named_args(named_args, arg_count + 1, named_arg_count, args...);
+}
+
+template <typename Char, typename T, typename... Tail,
+ FMT_ENABLE_IF(is_named_arg<T>::value)>
+void init_named_args(named_arg_info<Char>* named_args, int arg_count,
+ int named_arg_count, const T& arg, const Tail&... args) {
+ named_args[named_arg_count++] = {arg.name, arg_count};
+ init_named_args(named_args, arg_count + 1, named_arg_count, args...);
+}
+
+template <typename... Args>
+FMT_CONSTEXPR FMT_INLINE void init_named_args(std::nullptr_t, int, int,
+ const Args&...) {}
+
+template <bool B = false> constexpr auto count() -> size_t { return B ? 1 : 0; }
+template <bool B1, bool B2, bool... Tail> constexpr auto count() -> size_t {
+ return (B1 ? 1 : 0) + count<B2, Tail...>();
+}
+
+template <typename... Args> constexpr auto count_named_args() -> size_t {
+ return count<is_named_arg<Args>::value...>();
+}
+
+template <typename... Args>
+constexpr auto count_statically_named_args() -> size_t {
+ return count<is_statically_named_arg<Args>::value...>();
+}
+
+struct unformattable {};
+struct unformattable_char : unformattable {};
+struct unformattable_pointer : unformattable {};
+
+template <typename Char> struct string_value {
+ const Char* data;
+ size_t size;
+};
+
+template <typename Char> struct named_arg_value {
+ const named_arg_info<Char>* data;
+ size_t size;
+};
+
+template <typename Context> struct custom_value {
+ using parse_context = typename Context::parse_context_type;
+ void* value;
+ void (*format)(void* arg, parse_context& parse_ctx, Context& ctx);
+};
+
+// A formatting argument value.
+template <typename Context> class value {
+ public:
+ using char_type = typename Context::char_type;
+
+ union {
+ monostate no_value;
+ int int_value;
+ unsigned uint_value;
+ long long long_long_value;
+ unsigned long long ulong_long_value;
+ int128_opt int128_value;
+ uint128_opt uint128_value;
+ bool bool_value;
+ char_type char_value;
+ float float_value;
+ double double_value;
+ long double long_double_value;
+ const void* pointer;
+ string_value<char_type> string;
+ custom_value<Context> custom;
+ named_arg_value<char_type> named_args;
+ };
+
+ constexpr FMT_INLINE value() : no_value() {}
+ constexpr FMT_INLINE value(int val) : int_value(val) {}
+ constexpr FMT_INLINE value(unsigned val) : uint_value(val) {}
+ constexpr FMT_INLINE value(long long val) : long_long_value(val) {}
+ constexpr FMT_INLINE value(unsigned long long val) : ulong_long_value(val) {}
+ FMT_INLINE value(int128_opt val) : int128_value(val) {}
+ FMT_INLINE value(uint128_opt val) : uint128_value(val) {}
+ constexpr FMT_INLINE value(float val) : float_value(val) {}
+ constexpr FMT_INLINE value(double val) : double_value(val) {}
+ FMT_INLINE value(long double val) : long_double_value(val) {}
+ constexpr FMT_INLINE value(bool val) : bool_value(val) {}
+ constexpr FMT_INLINE value(char_type val) : char_value(val) {}
+ FMT_CONSTEXPR FMT_INLINE value(const char_type* val) {
+ string.data = val;
+ if (is_constant_evaluated()) string.size = {};
+ }
+ FMT_CONSTEXPR FMT_INLINE value(basic_string_view<char_type> val) {
+ string.data = val.data();
+ string.size = val.size();
+ }
+ FMT_INLINE value(const void* val) : pointer(val) {}
+ FMT_INLINE value(const named_arg_info<char_type>* args, size_t size)
+ : named_args{args, size} {}
+
+ template <typename T> FMT_CONSTEXPR20 FMT_INLINE value(T& val) {
+ using value_type = remove_const_t<T>;
+ custom.value = const_cast<value_type*>(std::addressof(val));
+ // Get the formatter type through the context to allow different contexts
+ // have different extension points, e.g. `formatter<T>` for `format` and
+ // `printf_formatter<T>` for `printf`.
+ custom.format = format_custom_arg<
+ value_type, typename Context::template formatter_type<value_type>>;
+ }
+ value(unformattable);
+ value(unformattable_char);
+ value(unformattable_pointer);
+
+ private:
+ // Formats an argument of a custom type, such as a user-defined class.
+ template <typename T, typename Formatter>
+ static void format_custom_arg(void* arg,
+ typename Context::parse_context_type& parse_ctx,
+ Context& ctx) {
+ auto f = Formatter();
+ parse_ctx.advance_to(f.parse(parse_ctx));
+ using qualified_type =
+ conditional_t<has_const_formatter<T, Context>(), const T, T>;
+ // Calling format through a mutable reference is deprecated.
+ ctx.advance_to(f.format(*static_cast<qualified_type*>(arg), ctx));
+ }
+};
+
+// To minimize the number of types we need to deal with, long is translated
+// either to int or to long long depending on its size.
+enum { long_short = sizeof(long) == sizeof(int) };
+using long_type = conditional_t<long_short, int, long long>;
+using ulong_type = conditional_t<long_short, unsigned, unsigned long long>;
+
+template <typename T> struct format_as_result {
+ template <typename U,
+ FMT_ENABLE_IF(std::is_enum<U>::value || std::is_class<U>::value)>
+ static auto map(U*) -> remove_cvref_t<decltype(format_as(std::declval<U>()))>;
+ static auto map(...) -> void;
+
+ using type = decltype(map(static_cast<T*>(nullptr)));
+};
+template <typename T> using format_as_t = typename format_as_result<T>::type;
+
+template <typename T>
+struct has_format_as
+ : bool_constant<!std::is_same<format_as_t<T>, void>::value> {};
+
+// Maps formatting arguments to core types.
+// arg_mapper reports errors by returning unformattable instead of using
+// static_assert because it's used in the is_formattable trait.
+template <typename Context> struct arg_mapper {
+ using char_type = typename Context::char_type;
+
+ FMT_CONSTEXPR FMT_INLINE auto map(signed char val) -> int { return val; }
+ FMT_CONSTEXPR FMT_INLINE auto map(unsigned char val) -> unsigned {
+ return val;
+ }
+ FMT_CONSTEXPR FMT_INLINE auto map(short val) -> int { return val; }
+ FMT_CONSTEXPR FMT_INLINE auto map(unsigned short val) -> unsigned {
+ return val;
+ }
+ FMT_CONSTEXPR FMT_INLINE auto map(int val) -> int { return val; }
+ FMT_CONSTEXPR FMT_INLINE auto map(unsigned val) -> unsigned { return val; }
+ FMT_CONSTEXPR FMT_INLINE auto map(long val) -> long_type { return val; }
+ FMT_CONSTEXPR FMT_INLINE auto map(unsigned long val) -> ulong_type {
+ return val;
+ }
+ FMT_CONSTEXPR FMT_INLINE auto map(long long val) -> long long { return val; }
+ FMT_CONSTEXPR FMT_INLINE auto map(unsigned long long val)
+ -> unsigned long long {
+ return val;
+ }
+ FMT_CONSTEXPR FMT_INLINE auto map(int128_opt val) -> int128_opt {
+ return val;
+ }
+ FMT_CONSTEXPR FMT_INLINE auto map(uint128_opt val) -> uint128_opt {
+ return val;
+ }
+ FMT_CONSTEXPR FMT_INLINE auto map(bool val) -> bool { return val; }
+
+ template <typename T, FMT_ENABLE_IF(std::is_same<T, char>::value ||
+ std::is_same<T, char_type>::value)>
+ FMT_CONSTEXPR FMT_INLINE auto map(T val) -> char_type {
+ return val;
+ }
+ template <typename T, enable_if_t<(std::is_same<T, wchar_t>::value ||
+#ifdef __cpp_char8_t
+ std::is_same<T, char8_t>::value ||
+#endif
+ std::is_same<T, char16_t>::value ||
+ std::is_same<T, char32_t>::value) &&
+ !std::is_same<T, char_type>::value,
+ int> = 0>
+ FMT_CONSTEXPR FMT_INLINE auto map(T) -> unformattable_char {
+ return {};
+ }
+
+ FMT_CONSTEXPR FMT_INLINE auto map(float val) -> float { return val; }
+ FMT_CONSTEXPR FMT_INLINE auto map(double val) -> double { return val; }
+ FMT_CONSTEXPR FMT_INLINE auto map(long double val) -> long double {
+ return val;
+ }
+
+ FMT_CONSTEXPR FMT_INLINE auto map(char_type* val) -> const char_type* {
+ return val;
+ }
+ FMT_CONSTEXPR FMT_INLINE auto map(const char_type* val) -> const char_type* {
+ return val;
+ }
+ template <typename T,
+ FMT_ENABLE_IF(is_string<T>::value && !std::is_pointer<T>::value &&
+ std::is_same<char_type, char_t<T>>::value)>
+ FMT_CONSTEXPR FMT_INLINE auto map(const T& val)
+ -> basic_string_view<char_type> {
+ return to_string_view(val);
+ }
+ template <typename T,
+ FMT_ENABLE_IF(is_string<T>::value && !std::is_pointer<T>::value &&
+ !std::is_same<char_type, char_t<T>>::value)>
+ FMT_CONSTEXPR FMT_INLINE auto map(const T&) -> unformattable_char {
+ return {};
+ }
+
+ FMT_CONSTEXPR FMT_INLINE auto map(void* val) -> const void* { return val; }
+ FMT_CONSTEXPR FMT_INLINE auto map(const void* val) -> const void* {
+ return val;
+ }
+ FMT_CONSTEXPR FMT_INLINE auto map(std::nullptr_t val) -> const void* {
+ return val;
+ }
+
+ // Use SFINAE instead of a const T* parameter to avoid a conflict with the
+ // array overload.
+ template <
+ typename T,
+ FMT_ENABLE_IF(
+ std::is_pointer<T>::value || std::is_member_pointer<T>::value ||
+ std::is_function<typename std::remove_pointer<T>::type>::value ||
+ (std::is_array<T>::value &&
+ !std::is_convertible<T, const char_type*>::value))>
+ FMT_CONSTEXPR auto map(const T&) -> unformattable_pointer {
+ return {};
+ }
+
+ template <typename T, std::size_t N,
+ FMT_ENABLE_IF(!std::is_same<T, wchar_t>::value)>
+ FMT_CONSTEXPR FMT_INLINE auto map(const T (&values)[N]) -> const T (&)[N] {
+ return values;
+ }
+
+ // Only map owning types because mapping views can be unsafe.
+ template <typename T, typename U = format_as_t<T>,
+ FMT_ENABLE_IF(std::is_arithmetic<U>::value)>
+ FMT_CONSTEXPR FMT_INLINE auto map(const T& val)
+ -> decltype(FMT_DECLTYPE_THIS map(U())) {
+ return map(format_as(val));
+ }
+
+ template <typename T, typename U = remove_const_t<T>>
+ struct formattable : bool_constant<has_const_formatter<U, Context>() ||
+ (has_formatter<U, Context>::value &&
+ !std::is_const<T>::value)> {};
+
+ template <typename T, FMT_ENABLE_IF(formattable<T>::value)>
+ FMT_CONSTEXPR FMT_INLINE auto do_map(T& val) -> T& {
+ return val;
+ }
+ template <typename T, FMT_ENABLE_IF(!formattable<T>::value)>
+ FMT_CONSTEXPR FMT_INLINE auto do_map(T&) -> unformattable {
+ return {};
+ }
+
+ template <typename T, typename U = remove_const_t<T>,
+ FMT_ENABLE_IF((std::is_class<U>::value || std::is_enum<U>::value ||
+ std::is_union<U>::value) &&
+ !is_string<U>::value && !is_char<U>::value &&
+ !is_named_arg<U>::value &&
+ !std::is_arithmetic<format_as_t<U>>::value)>
+ FMT_CONSTEXPR FMT_INLINE auto map(T& val)
+ -> decltype(FMT_DECLTYPE_THIS do_map(val)) {
+ return do_map(val);
+ }
+
+ template <typename T, FMT_ENABLE_IF(is_named_arg<T>::value)>
+ FMT_CONSTEXPR FMT_INLINE auto map(const T& named_arg)
+ -> decltype(FMT_DECLTYPE_THIS map(named_arg.value)) {
+ return map(named_arg.value);
+ }
+
+ auto map(...) -> unformattable { return {}; }
+};
+
+// A type constant after applying arg_mapper<Context>.
+template <typename T, typename Context>
+using mapped_type_constant =
+ type_constant<decltype(arg_mapper<Context>().map(std::declval<const T&>())),
+ typename Context::char_type>;
+
+enum { packed_arg_bits = 4 };
+// Maximum number of arguments with packed types.
+enum { max_packed_args = 62 / packed_arg_bits };
+enum : unsigned long long { is_unpacked_bit = 1ULL << 63 };
+enum : unsigned long long { has_named_args_bit = 1ULL << 62 };
+
+template <typename Char, typename InputIt>
+auto copy_str(InputIt begin, InputIt end, appender out) -> appender {
+ get_container(out).append(begin, end);
+ return out;
+}
+template <typename Char, typename InputIt>
+auto copy_str(InputIt begin, InputIt end,
+ std::back_insert_iterator<std::string> out)
+ -> std::back_insert_iterator<std::string> {
+ get_container(out).append(begin, end);
+ return out;
+}
+
+template <typename Char, typename R, typename OutputIt>
+FMT_CONSTEXPR auto copy_str(R&& rng, OutputIt out) -> OutputIt {
+ return detail::copy_str<Char>(rng.begin(), rng.end(), out);
+}
+
+#if FMT_GCC_VERSION && FMT_GCC_VERSION < 500
+// A workaround for gcc 4.8 to make void_t work in a SFINAE context.
+template <typename...> struct void_t_impl {
+ using type = void;
+};
+template <typename... T> using void_t = typename void_t_impl<T...>::type;
+#else
+template <typename...> using void_t = void;
+#endif
+
+template <typename It, typename T, typename Enable = void>
+struct is_output_iterator : std::false_type {};
+
+template <typename It, typename T>
+struct is_output_iterator<
+ It, T,
+ void_t<typename std::iterator_traits<It>::iterator_category,
+ decltype(*std::declval<It>() = std::declval<T>())>>
+ : std::true_type {};
+
+template <typename It> struct is_back_insert_iterator : std::false_type {};
+template <typename Container>
+struct is_back_insert_iterator<std::back_insert_iterator<Container>>
+ : std::true_type {};
+
+// A type-erased reference to an std::locale to avoid a heavy <locale> include.
+class locale_ref {
+ private:
+ const void* locale_; // A type-erased pointer to std::locale.
+
+ public:
+ constexpr FMT_INLINE locale_ref() : locale_(nullptr) {}
+ template <typename Locale> explicit locale_ref(const Locale& loc);
+
+ explicit operator bool() const noexcept { return locale_ != nullptr; }
+
+ template <typename Locale> auto get() const -> Locale;
+};
+
+template <typename> constexpr auto encode_types() -> unsigned long long {
+ return 0;
+}
+
+template <typename Context, typename Arg, typename... Args>
+constexpr auto encode_types() -> unsigned long long {
+ return static_cast<unsigned>(mapped_type_constant<Arg, Context>::value) |
+ (encode_types<Context, Args...>() << packed_arg_bits);
+}
+
+#if defined(__cpp_if_constexpr)
+// This type is intentionally undefined, only used for errors
+template <typename T, typename Char> struct type_is_unformattable_for;
+#endif
+
+template <bool PACKED, typename Context, typename T, FMT_ENABLE_IF(PACKED)>
+FMT_CONSTEXPR FMT_INLINE auto make_arg(T& val) -> value<Context> {
+ using arg_type = remove_cvref_t<decltype(arg_mapper<Context>().map(val))>;
+
+ constexpr bool formattable_char =
+ !std::is_same<arg_type, unformattable_char>::value;
+ static_assert(formattable_char, "Mixing character types is disallowed.");
+
+ // Formatting of arbitrary pointers is disallowed. If you want to format a
+ // pointer cast it to `void*` or `const void*`. In particular, this forbids
+ // formatting of `[const] volatile char*` printed as bool by iostreams.
+ constexpr bool formattable_pointer =
+ !std::is_same<arg_type, unformattable_pointer>::value;
+ static_assert(formattable_pointer,
+ "Formatting of non-void pointers is disallowed.");
+
+ constexpr bool formattable = !std::is_same<arg_type, unformattable>::value;
+#if defined(__cpp_if_constexpr)
+ if constexpr (!formattable) {
+ type_is_unformattable_for<T, typename Context::char_type> _;
+ }
+#endif
+ static_assert(
+ formattable,
+ "Cannot format an argument. To make type T formattable provide a "
+ "formatter<T> specialization: https://fmt.dev/latest/api.html#udt");
+ return {arg_mapper<Context>().map(val)};
+}
+
+template <typename Context, typename T>
+FMT_CONSTEXPR auto make_arg(T& val) -> basic_format_arg<Context> {
+ auto arg = basic_format_arg<Context>();
+ arg.type_ = mapped_type_constant<T, Context>::value;
+ arg.value_ = make_arg<true, Context>(val);
+ return arg;
+}
+
+template <bool PACKED, typename Context, typename T, FMT_ENABLE_IF(!PACKED)>
+FMT_CONSTEXPR inline auto make_arg(T& val) -> basic_format_arg<Context> {
+ return make_arg<Context>(val);
+}
+} // namespace detail
+FMT_BEGIN_EXPORT
+
+// A formatting argument. Context is a template parameter for the compiled API
+// where output can be unbuffered.
+template <typename Context> class basic_format_arg {
+ private:
+ detail::value<Context> value_;
+ detail::type type_;
+
+ template <typename ContextType, typename T>
+ friend FMT_CONSTEXPR auto detail::make_arg(T& value)
+ -> basic_format_arg<ContextType>;
+
+ template <typename Visitor, typename Ctx>
+ friend FMT_CONSTEXPR auto visit_format_arg(Visitor&& vis,
+ const basic_format_arg<Ctx>& arg)
+ -> decltype(vis(0));
+
+ friend class basic_format_args<Context>;
+ friend class dynamic_format_arg_store<Context>;
+
+ using char_type = typename Context::char_type;
+
+ template <typename T, typename Char, size_t NUM_ARGS, size_t NUM_NAMED_ARGS>
+ friend struct detail::arg_data;
+
+ basic_format_arg(const detail::named_arg_info<char_type>* args, size_t size)
+ : value_(args, size) {}
+
+ public:
+ class handle {
+ public:
+ explicit handle(detail::custom_value<Context> custom) : custom_(custom) {}
+
+ void format(typename Context::parse_context_type& parse_ctx,
+ Context& ctx) const {
+ custom_.format(custom_.value, parse_ctx, ctx);
+ }
+
+ private:
+ detail::custom_value<Context> custom_;
+ };
+
+ constexpr basic_format_arg() : type_(detail::type::none_type) {}
+
+ constexpr explicit operator bool() const noexcept {
+ return type_ != detail::type::none_type;
+ }
+
+ auto type() const -> detail::type { return type_; }
+
+ auto is_integral() const -> bool { return detail::is_integral_type(type_); }
+ auto is_arithmetic() const -> bool {
+ return detail::is_arithmetic_type(type_);
+ }
+
+ FMT_INLINE auto format_custom(const char_type* parse_begin,
+ typename Context::parse_context_type& parse_ctx,
+ Context& ctx) -> bool {
+ if (type_ != detail::type::custom_type) return false;
+ parse_ctx.advance_to(parse_begin);
+ value_.custom.format(value_.custom.value, parse_ctx, ctx);
+ return true;
+ }
+};
+
+/**
+ \rst
+ Visits an argument dispatching to the appropriate visit method based on
+ the argument type. For example, if the argument type is ``double`` then
+ ``vis(value)`` will be called with the value of type ``double``.
+ \endrst
+ */
+// DEPRECATED!
+template <typename Visitor, typename Context>
+FMT_CONSTEXPR FMT_INLINE auto visit_format_arg(
+ Visitor&& vis, const basic_format_arg<Context>& arg) -> decltype(vis(0)) {
+ switch (arg.type_) {
+ case detail::type::none_type:
+ break;
+ case detail::type::int_type:
+ return vis(arg.value_.int_value);
+ case detail::type::uint_type:
+ return vis(arg.value_.uint_value);
+ case detail::type::long_long_type:
+ return vis(arg.value_.long_long_value);
+ case detail::type::ulong_long_type:
+ return vis(arg.value_.ulong_long_value);
+ case detail::type::int128_type:
+ return vis(detail::convert_for_visit(arg.value_.int128_value));
+ case detail::type::uint128_type:
+ return vis(detail::convert_for_visit(arg.value_.uint128_value));
+ case detail::type::bool_type:
+ return vis(arg.value_.bool_value);
+ case detail::type::char_type:
+ return vis(arg.value_.char_value);
+ case detail::type::float_type:
+ return vis(arg.value_.float_value);
+ case detail::type::double_type:
+ return vis(arg.value_.double_value);
+ case detail::type::long_double_type:
+ return vis(arg.value_.long_double_value);
+ case detail::type::cstring_type:
+ return vis(arg.value_.string.data);
+ case detail::type::string_type:
+ using sv = basic_string_view<typename Context::char_type>;
+ return vis(sv(arg.value_.string.data, arg.value_.string.size));
+ case detail::type::pointer_type:
+ return vis(arg.value_.pointer);
+ case detail::type::custom_type:
+ return vis(typename basic_format_arg<Context>::handle(arg.value_.custom));
+ }
+ return vis(monostate());
+}
+
+// Formatting context.
+template <typename OutputIt, typename Char> class basic_format_context {
+ private:
+ OutputIt out_;
+ basic_format_args<basic_format_context> args_;
+ detail::locale_ref loc_;
+
+ public:
+ using iterator = OutputIt;
+ using format_arg = basic_format_arg<basic_format_context>;
+ using format_args = basic_format_args<basic_format_context>;
+ using parse_context_type = basic_format_parse_context<Char>;
+ template <typename T> using formatter_type = formatter<T, Char>;
+
+ /** The character type for the output. */
+ using char_type = Char;
+
+ basic_format_context(basic_format_context&&) = default;
+ basic_format_context(const basic_format_context&) = delete;
+ void operator=(const basic_format_context&) = delete;
+ /**
+ Constructs a ``basic_format_context`` object. References to the arguments
+ are stored in the object so make sure they have appropriate lifetimes.
+ */
+ constexpr basic_format_context(OutputIt out, format_args ctx_args,
+ detail::locale_ref loc = {})
+ : out_(out), args_(ctx_args), loc_(loc) {}
+
+ constexpr auto arg(int id) const -> format_arg { return args_.get(id); }
+ FMT_CONSTEXPR auto arg(basic_string_view<Char> name) -> format_arg {
+ return args_.get(name);
+ }
+ FMT_CONSTEXPR auto arg_id(basic_string_view<Char> name) -> int {
+ return args_.get_id(name);
+ }
+ auto args() const -> const format_args& { return args_; }
+
+ // DEPRECATED!
+ FMT_CONSTEXPR auto error_handler() -> detail::error_handler { return {}; }
+ void on_error(const char* message) { error_handler().on_error(message); }
+
+ // Returns an iterator to the beginning of the output range.
+ FMT_CONSTEXPR auto out() -> iterator { return out_; }
+
+ // Advances the begin iterator to ``it``.
+ void advance_to(iterator it) {
+ if (!detail::is_back_insert_iterator<iterator>()) out_ = it;
+ }
+
+ FMT_CONSTEXPR auto locale() -> detail::locale_ref { return loc_; }
+};
+
+template <typename Char>
+using buffer_context =
+ basic_format_context<detail::buffer_appender<Char>, Char>;
+using format_context = buffer_context<char>;
+
+template <typename T, typename Char = char>
+using is_formattable = bool_constant<!std::is_base_of<
+ detail::unformattable, decltype(detail::arg_mapper<buffer_context<Char>>()
+ .map(std::declval<T&>()))>::value>;
+
+/**
+ \rst
+ An array of references to arguments. It can be implicitly converted into
+ `~fmt::basic_format_args` for passing into type-erased formatting functions
+ such as `~fmt::vformat`.
+ \endrst
+ */
+template <typename Context, typename... Args>
+class format_arg_store
+#if FMT_GCC_VERSION && FMT_GCC_VERSION < 409
+ // Workaround a GCC template argument substitution bug.
+ : public basic_format_args<Context>
+#endif
+{
+ private:
+ static const size_t num_args = sizeof...(Args);
+ static constexpr size_t num_named_args = detail::count_named_args<Args...>();
+ static const bool is_packed = num_args <= detail::max_packed_args;
+
+ using value_type = conditional_t<is_packed, detail::value<Context>,
+ basic_format_arg<Context>>;
+
+ detail::arg_data<value_type, typename Context::char_type, num_args,
+ num_named_args>
+ data_;
+
+ friend class basic_format_args<Context>;
+
+ static constexpr unsigned long long desc =
+ (is_packed ? detail::encode_types<Context, Args...>()
+ : detail::is_unpacked_bit | num_args) |
+ (num_named_args != 0
+ ? static_cast<unsigned long long>(detail::has_named_args_bit)
+ : 0);
+
+ public:
+ template <typename... T>
+ FMT_CONSTEXPR FMT_INLINE format_arg_store(T&... args)
+ :
+#if FMT_GCC_VERSION && FMT_GCC_VERSION < 409
+ basic_format_args<Context>(*this),
+#endif
+ data_{detail::make_arg<is_packed, Context>(args)...} {
+ if (detail::const_check(num_named_args != 0))
+ detail::init_named_args(data_.named_args(), 0, 0, args...);
+ }
+};
+
+/**
+ \rst
+ Constructs a `~fmt::format_arg_store` object that contains references to
+ arguments and can be implicitly converted to `~fmt::format_args`. `Context`
+ can be omitted in which case it defaults to `~fmt::format_context`.
+ See `~fmt::arg` for lifetime considerations.
+ \endrst
+ */
+// Arguments are taken by lvalue references to avoid some lifetime issues.
+template <typename Context = format_context, typename... T>
+constexpr auto make_format_args(T&... args)
+ -> format_arg_store<Context, remove_cvref_t<T>...> {
+ return {args...};
+}
+
+/**
+ \rst
+ Returns a named argument to be used in a formatting function.
+ It should only be used in a call to a formatting function or
+ `dynamic_format_arg_store::push_back`.
+
+ **Example**::
+
+ fmt::print("Elapsed time: {s:.2f} seconds", fmt::arg("s", 1.23));
+ \endrst
+ */
+template <typename Char, typename T>
+inline auto arg(const Char* name, const T& arg) -> detail::named_arg<Char, T> {
+ static_assert(!detail::is_named_arg<T>(), "nested named arguments");
+ return {name, arg};
+}
+FMT_END_EXPORT
+
+/**
+ \rst
+ A view of a collection of formatting arguments. To avoid lifetime issues it
+ should only be used as a parameter type in type-erased functions such as
+ ``vformat``::
+
+ void vlog(string_view format_str, format_args args); // OK
+ format_args args = make_format_args(); // Error: dangling reference
+ \endrst
+ */
+template <typename Context> class basic_format_args {
+ public:
+ using size_type = int;
+ using format_arg = basic_format_arg<Context>;
+
+ private:
+ // A descriptor that contains information about formatting arguments.
+ // If the number of arguments is less or equal to max_packed_args then
+ // argument types are passed in the descriptor. This reduces binary code size
+ // per formatting function call.
+ unsigned long long desc_;
+ union {
+ // If is_packed() returns true then argument values are stored in values_;
+ // otherwise they are stored in args_. This is done to improve cache
+ // locality and reduce compiled code size since storing larger objects
+ // may require more code (at least on x86-64) even if the same amount of
+ // data is actually copied to stack. It saves ~10% on the bloat test.
+ const detail::value<Context>* values_;
+ const format_arg* args_;
+ };
+
+ constexpr auto is_packed() const -> bool {
+ return (desc_ & detail::is_unpacked_bit) == 0;
+ }
+ auto has_named_args() const -> bool {
+ return (desc_ & detail::has_named_args_bit) != 0;
+ }
+
+ FMT_CONSTEXPR auto type(int index) const -> detail::type {
+ int shift = index * detail::packed_arg_bits;
+ unsigned int mask = (1 << detail::packed_arg_bits) - 1;
+ return static_cast<detail::type>((desc_ >> shift) & mask);
+ }
+
+ constexpr FMT_INLINE basic_format_args(unsigned long long desc,
+ const detail::value<Context>* values)
+ : desc_(desc), values_(values) {}
+ constexpr basic_format_args(unsigned long long desc, const format_arg* args)
+ : desc_(desc), args_(args) {}
+
+ public:
+ constexpr basic_format_args() : desc_(0), args_(nullptr) {}
+
+ /**
+ \rst
+ Constructs a `basic_format_args` object from `~fmt::format_arg_store`.
+ \endrst
+ */
+ template <typename... Args>
+ constexpr FMT_INLINE basic_format_args(
+ const format_arg_store<Context, Args...>& store)
+ : basic_format_args(format_arg_store<Context, Args...>::desc,
+ store.data_.args()) {}
+
+ /**
+ \rst
+ Constructs a `basic_format_args` object from
+ `~fmt::dynamic_format_arg_store`.
+ \endrst
+ */
+ constexpr FMT_INLINE basic_format_args(
+ const dynamic_format_arg_store<Context>& store)
+ : basic_format_args(store.get_types(), store.data()) {}
+
+ /**
+ \rst
+ Constructs a `basic_format_args` object from a dynamic set of arguments.
+ \endrst
+ */
+ constexpr basic_format_args(const format_arg* args, int count)
+ : basic_format_args(detail::is_unpacked_bit | detail::to_unsigned(count),
+ args) {}
+
+ /** Returns the argument with the specified id. */
+ FMT_CONSTEXPR auto get(int id) const -> format_arg {
+ format_arg arg;
+ if (!is_packed()) {
+ if (id < max_size()) arg = args_[id];
+ return arg;
+ }
+ if (id >= detail::max_packed_args) return arg;
+ arg.type_ = type(id);
+ if (arg.type_ == detail::type::none_type) return arg;
+ arg.value_ = values_[id];
+ return arg;
+ }
+
+ template <typename Char>
+ auto get(basic_string_view<Char> name) const -> format_arg {
+ int id = get_id(name);
+ return id >= 0 ? get(id) : format_arg();
+ }
+
+ template <typename Char>
+ auto get_id(basic_string_view<Char> name) const -> int {
+ if (!has_named_args()) return -1;
+ const auto& named_args =
+ (is_packed() ? values_[-1] : args_[-1].value_).named_args;
+ for (size_t i = 0; i < named_args.size; ++i) {
+ if (named_args.data[i].name == name) return named_args.data[i].id;
+ }
+ return -1;
+ }
+
+ auto max_size() const -> int {
+ unsigned long long max_packed = detail::max_packed_args;
+ return static_cast<int>(is_packed() ? max_packed
+ : desc_ & ~detail::is_unpacked_bit);
+ }
+};
+
+/** An alias to ``basic_format_args<format_context>``. */
+// A separate type would result in shorter symbols but break ABI compatibility
+// between clang and gcc on ARM (#1919).
+FMT_EXPORT using format_args = basic_format_args<format_context>;
+
+// We cannot use enum classes as bit fields because of a gcc bug, so we put them
+// in namespaces instead (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=61414).
+// Additionally, if an underlying type is specified, older gcc incorrectly warns
+// that the type is too small. Both bugs are fixed in gcc 9.3.
+#if FMT_GCC_VERSION && FMT_GCC_VERSION < 903
+# define FMT_ENUM_UNDERLYING_TYPE(type)
+#else
+# define FMT_ENUM_UNDERLYING_TYPE(type) : type
+#endif
+namespace align {
+enum type FMT_ENUM_UNDERLYING_TYPE(unsigned char){none, left, right, center,
+ numeric};
+}
+using align_t = align::type;
+namespace sign {
+enum type FMT_ENUM_UNDERLYING_TYPE(unsigned char){none, minus, plus, space};
+}
+using sign_t = sign::type;
+
+namespace detail {
+
+// Workaround an array initialization issue in gcc 4.8.
+template <typename Char> struct fill_t {
+ private:
+ enum { max_size = 4 };
+ Char data_[max_size] = {Char(' '), Char(0), Char(0), Char(0)};
+ unsigned char size_ = 1;
+
+ public:
+ FMT_CONSTEXPR void operator=(basic_string_view<Char> s) {
+ auto size = s.size();
+ FMT_ASSERT(size <= max_size, "invalid fill");
+ for (size_t i = 0; i < size; ++i) data_[i] = s[i];
+ size_ = static_cast<unsigned char>(size);
+ }
+
+ constexpr auto size() const -> size_t { return size_; }
+ constexpr auto data() const -> const Char* { return data_; }
+
+ FMT_CONSTEXPR auto operator[](size_t index) -> Char& { return data_[index]; }
+ FMT_CONSTEXPR auto operator[](size_t index) const -> const Char& {
+ return data_[index];
+ }
+};
+} // namespace detail
+
+enum class presentation_type : unsigned char {
+ none,
+ dec, // 'd'
+ oct, // 'o'
+ hex_lower, // 'x'
+ hex_upper, // 'X'
+ bin_lower, // 'b'
+ bin_upper, // 'B'
+ hexfloat_lower, // 'a'
+ hexfloat_upper, // 'A'
+ exp_lower, // 'e'
+ exp_upper, // 'E'
+ fixed_lower, // 'f'
+ fixed_upper, // 'F'
+ general_lower, // 'g'
+ general_upper, // 'G'
+ chr, // 'c'
+ string, // 's'
+ pointer, // 'p'
+ debug // '?'
+};
+
+// Format specifiers for built-in and string types.
+template <typename Char = char> struct format_specs {
+ int width;
+ int precision;
+ presentation_type type;
+ align_t align : 4;
+ sign_t sign : 3;
+ bool alt : 1; // Alternate form ('#').
+ bool localized : 1;
+ detail::fill_t<Char> fill;
+
+ constexpr format_specs()
+ : width(0),
+ precision(-1),
+ type(presentation_type::none),
+ align(align::none),
+ sign(sign::none),
+ alt(false),
+ localized(false) {}
+};
+
+namespace detail {
+
+enum class arg_id_kind { none, index, name };
+
+// An argument reference.
+template <typename Char> struct arg_ref {
+ FMT_CONSTEXPR arg_ref() : kind(arg_id_kind::none), val() {}
+
+ FMT_CONSTEXPR explicit arg_ref(int index)
+ : kind(arg_id_kind::index), val(index) {}
+ FMT_CONSTEXPR explicit arg_ref(basic_string_view<Char> name)
+ : kind(arg_id_kind::name), val(name) {}
+
+ FMT_CONSTEXPR auto operator=(int idx) -> arg_ref& {
+ kind = arg_id_kind::index;
+ val.index = idx;
+ return *this;
+ }
+
+ arg_id_kind kind;
+ union value {
+ FMT_CONSTEXPR value(int idx = 0) : index(idx) {}
+ FMT_CONSTEXPR value(basic_string_view<Char> n) : name(n) {}
+
+ int index;
+ basic_string_view<Char> name;
+ } val;
+};
+
+// Format specifiers with width and precision resolved at formatting rather
+// than parsing time to allow reusing the same parsed specifiers with
+// different sets of arguments (precompilation of format strings).
+template <typename Char = char>
+struct dynamic_format_specs : format_specs<Char> {
+ arg_ref<Char> width_ref;
+ arg_ref<Char> precision_ref;
+};
+
+// Converts a character to ASCII. Returns '\0' on conversion failure.
+template <typename Char, FMT_ENABLE_IF(std::is_integral<Char>::value)>
+constexpr auto to_ascii(Char c) -> char {
+ return c <= 0xff ? static_cast<char>(c) : '\0';
+}
+template <typename Char, FMT_ENABLE_IF(std::is_enum<Char>::value)>
+constexpr auto to_ascii(Char c) -> char {
+ return c <= 0xff ? static_cast<char>(c) : '\0';
+}
+
+// Returns the number of code units in a code point or 1 on error.
+template <typename Char>
+FMT_CONSTEXPR auto code_point_length(const Char* begin) -> int {
+ if (const_check(sizeof(Char) != 1)) return 1;
+ auto c = static_cast<unsigned char>(*begin);
+ return static_cast<int>((0x3a55000000000000ull >> (2 * (c >> 3))) & 0x3) + 1;
+}
+
+// Return the result via the out param to workaround gcc bug 77539.
+template <bool IS_CONSTEXPR, typename T, typename Ptr = const T*>
+FMT_CONSTEXPR auto find(Ptr first, Ptr last, T value, Ptr& out) -> bool {
+ for (out = first; out != last; ++out) {
+ if (*out == value) return true;
+ }
+ return false;
+}
+
+template <>
+inline auto find<false, char>(const char* first, const char* last, char value,
+ const char*& out) -> bool {
+ out = static_cast<const char*>(
+ std::memchr(first, value, to_unsigned(last - first)));
+ return out != nullptr;
+}
+
+// Parses the range [begin, end) as an unsigned integer. This function assumes
+// that the range is non-empty and the first character is a digit.
+template <typename Char>
+FMT_CONSTEXPR auto parse_nonnegative_int(const Char*& begin, const Char* end,
+ int error_value) noexcept -> int {
+ FMT_ASSERT(begin != end && '0' <= *begin && *begin <= '9', "");
+ unsigned value = 0, prev = 0;
+ auto p = begin;
+ do {
+ prev = value;
+ value = value * 10 + unsigned(*p - '0');
+ ++p;
+ } while (p != end && '0' <= *p && *p <= '9');
+ auto num_digits = p - begin;
+ begin = p;
+ if (num_digits <= std::numeric_limits<int>::digits10)
+ return static_cast<int>(value);
+ // Check for overflow.
+ const unsigned max = to_unsigned((std::numeric_limits<int>::max)());
+ return num_digits == std::numeric_limits<int>::digits10 + 1 &&
+ prev * 10ull + unsigned(p[-1] - '0') <= max
+ ? static_cast<int>(value)
+ : error_value;
+}
+
+FMT_CONSTEXPR inline auto parse_align(char c) -> align_t {
+ switch (c) {
+ case '<':
+ return align::left;
+ case '>':
+ return align::right;
+ case '^':
+ return align::center;
+ }
+ return align::none;
+}
+
+template <typename Char> constexpr auto is_name_start(Char c) -> bool {
+ return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '_';
+}
+
+template <typename Char, typename Handler>
+FMT_CONSTEXPR auto do_parse_arg_id(const Char* begin, const Char* end,
+ Handler&& handler) -> const Char* {
+ Char c = *begin;
+ if (c >= '0' && c <= '9') {
+ int index = 0;
+ constexpr int max = (std::numeric_limits<int>::max)();
+ if (c != '0')
+ index = parse_nonnegative_int(begin, end, max);
+ else
+ ++begin;
+ if (begin == end || (*begin != '}' && *begin != ':'))
+ throw_format_error("invalid format string");
+ else
+ handler.on_index(index);
+ return begin;
+ }
+ if (!is_name_start(c)) {
+ throw_format_error("invalid format string");
+ return begin;
+ }
+ auto it = begin;
+ do {
+ ++it;
+ } while (it != end && (is_name_start(*it) || ('0' <= *it && *it <= '9')));
+ handler.on_name({begin, to_unsigned(it - begin)});
+ return it;
+}
+
+template <typename Char, typename Handler>
+FMT_CONSTEXPR FMT_INLINE auto parse_arg_id(const Char* begin, const Char* end,
+ Handler&& handler) -> const Char* {
+ FMT_ASSERT(begin != end, "");
+ Char c = *begin;
+ if (c != '}' && c != ':') return do_parse_arg_id(begin, end, handler);
+ handler.on_auto();
+ return begin;
+}
+
+template <typename Char> struct dynamic_spec_id_handler {
+ basic_format_parse_context<Char>& ctx;
+ arg_ref<Char>& ref;
+
+ FMT_CONSTEXPR void on_auto() {
+ int id = ctx.next_arg_id();
+ ref = arg_ref<Char>(id);
+ ctx.check_dynamic_spec(id);
+ }
+ FMT_CONSTEXPR void on_index(int id) {
+ ref = arg_ref<Char>(id);
+ ctx.check_arg_id(id);
+ ctx.check_dynamic_spec(id);
+ }
+ FMT_CONSTEXPR void on_name(basic_string_view<Char> id) {
+ ref = arg_ref<Char>(id);
+ ctx.check_arg_id(id);
+ }
+};
+
+// Parses [integer | "{" [arg_id] "}"].
+template <typename Char>
+FMT_CONSTEXPR auto parse_dynamic_spec(const Char* begin, const Char* end,
+ int& value, arg_ref<Char>& ref,
+ basic_format_parse_context<Char>& ctx)
+ -> const Char* {
+ FMT_ASSERT(begin != end, "");
+ if ('0' <= *begin && *begin <= '9') {
+ int val = parse_nonnegative_int(begin, end, -1);
+ if (val != -1)
+ value = val;
+ else
+ throw_format_error("number is too big");
+ } else if (*begin == '{') {
+ ++begin;
+ auto handler = dynamic_spec_id_handler<Char>{ctx, ref};
+ if (begin != end) begin = parse_arg_id(begin, end, handler);
+ if (begin != end && *begin == '}') return ++begin;
+ throw_format_error("invalid format string");
+ }
+ return begin;
+}
+
+template <typename Char>
+FMT_CONSTEXPR auto parse_precision(const Char* begin, const Char* end,
+ int& value, arg_ref<Char>& ref,
+ basic_format_parse_context<Char>& ctx)
+ -> const Char* {
+ ++begin;
+ if (begin == end || *begin == '}') {
+ throw_format_error("invalid precision");
+ return begin;
+ }
+ return parse_dynamic_spec(begin, end, value, ref, ctx);
+}
+
+enum class state { start, align, sign, hash, zero, width, precision, locale };
+
+// Parses standard format specifiers.
+template <typename Char>
+FMT_CONSTEXPR FMT_INLINE auto parse_format_specs(
+ const Char* begin, const Char* end, dynamic_format_specs<Char>& specs,
+ basic_format_parse_context<Char>& ctx, type arg_type) -> const Char* {
+ auto c = '\0';
+ if (end - begin > 1) {
+ auto next = to_ascii(begin[1]);
+ c = parse_align(next) == align::none ? to_ascii(*begin) : '\0';
+ } else {
+ if (begin == end) return begin;
+ c = to_ascii(*begin);
+ }
+
+ struct {
+ state current_state = state::start;
+ FMT_CONSTEXPR void operator()(state s, bool valid = true) {
+ if (current_state >= s || !valid)
+ throw_format_error("invalid format specifier");
+ current_state = s;
+ }
+ } enter_state;
+
+ using pres = presentation_type;
+ constexpr auto integral_set = sint_set | uint_set | bool_set | char_set;
+ struct {
+ const Char*& begin;
+ dynamic_format_specs<Char>& specs;
+ type arg_type;
+
+ FMT_CONSTEXPR auto operator()(pres pres_type, int set) -> const Char* {
+ if (!in(arg_type, set)) {
+ if (arg_type == type::none_type) return begin;
+ throw_format_error("invalid format specifier");
+ }
+ specs.type = pres_type;
+ return begin + 1;
+ }
+ } parse_presentation_type{begin, specs, arg_type};
+
+ for (;;) {
+ switch (c) {
+ case '<':
+ case '>':
+ case '^':
+ enter_state(state::align);
+ specs.align = parse_align(c);
+ ++begin;
+ break;
+ case '+':
+ case '-':
+ case ' ':
+ if (arg_type == type::none_type) return begin;
+ enter_state(state::sign, in(arg_type, sint_set | float_set));
+ switch (c) {
+ case '+':
+ specs.sign = sign::plus;
+ break;
+ case '-':
+ specs.sign = sign::minus;
+ break;
+ case ' ':
+ specs.sign = sign::space;
+ break;
+ }
+ ++begin;
+ break;
+ case '#':
+ if (arg_type == type::none_type) return begin;
+ enter_state(state::hash, is_arithmetic_type(arg_type));
+ specs.alt = true;
+ ++begin;
+ break;
+ case '0':
+ enter_state(state::zero);
+ if (!is_arithmetic_type(arg_type)) {
+ if (arg_type == type::none_type) return begin;
+ throw_format_error("format specifier requires numeric argument");
+ }
+ if (specs.align == align::none) {
+ // Ignore 0 if align is specified for compatibility with std::format.
+ specs.align = align::numeric;
+ specs.fill[0] = Char('0');
+ }
+ ++begin;
+ break;
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ case '{':
+ enter_state(state::width);
+ begin = parse_dynamic_spec(begin, end, specs.width, specs.width_ref, ctx);
+ break;
+ case '.':
+ if (arg_type == type::none_type) return begin;
+ enter_state(state::precision,
+ in(arg_type, float_set | string_set | cstring_set));
+ begin = parse_precision(begin, end, specs.precision, specs.precision_ref,
+ ctx);
+ break;
+ case 'L':
+ if (arg_type == type::none_type) return begin;
+ enter_state(state::locale, is_arithmetic_type(arg_type));
+ specs.localized = true;
+ ++begin;
+ break;
+ case 'd':
+ return parse_presentation_type(pres::dec, integral_set);
+ case 'o':
+ return parse_presentation_type(pres::oct, integral_set);
+ case 'x':
+ return parse_presentation_type(pres::hex_lower, integral_set);
+ case 'X':
+ return parse_presentation_type(pres::hex_upper, integral_set);
+ case 'b':
+ return parse_presentation_type(pres::bin_lower, integral_set);
+ case 'B':
+ return parse_presentation_type(pres::bin_upper, integral_set);
+ case 'a':
+ return parse_presentation_type(pres::hexfloat_lower, float_set);
+ case 'A':
+ return parse_presentation_type(pres::hexfloat_upper, float_set);
+ case 'e':
+ return parse_presentation_type(pres::exp_lower, float_set);
+ case 'E':
+ return parse_presentation_type(pres::exp_upper, float_set);
+ case 'f':
+ return parse_presentation_type(pres::fixed_lower, float_set);
+ case 'F':
+ return parse_presentation_type(pres::fixed_upper, float_set);
+ case 'g':
+ return parse_presentation_type(pres::general_lower, float_set);
+ case 'G':
+ return parse_presentation_type(pres::general_upper, float_set);
+ case 'c':
+ if (arg_type == type::bool_type)
+ throw_format_error("invalid format specifier");
+ return parse_presentation_type(pres::chr, integral_set);
+ case 's':
+ return parse_presentation_type(pres::string,
+ bool_set | string_set | cstring_set);
+ case 'p':
+ return parse_presentation_type(pres::pointer, pointer_set | cstring_set);
+ case '?':
+ return parse_presentation_type(pres::debug,
+ char_set | string_set | cstring_set);
+ case '}':
+ return begin;
+ default: {
+ if (*begin == '}') return begin;
+ // Parse fill and alignment.
+ auto fill_end = begin + code_point_length(begin);
+ if (end - fill_end <= 0) {
+ throw_format_error("invalid format specifier");
+ return begin;
+ }
+ if (*begin == '{') {
+ throw_format_error("invalid fill character '{'");
+ return begin;
+ }
+ auto align = parse_align(to_ascii(*fill_end));
+ enter_state(state::align, align != align::none);
+ specs.fill = {begin, to_unsigned(fill_end - begin)};
+ specs.align = align;
+ begin = fill_end + 1;
+ }
+ }
+ if (begin == end) return begin;
+ c = to_ascii(*begin);
+ }
+}
+
+template <typename Char, typename Handler>
+FMT_CONSTEXPR auto parse_replacement_field(const Char* begin, const Char* end,
+ Handler&& handler) -> const Char* {
+ struct id_adapter {
+ Handler& handler;
+ int arg_id;
+
+ FMT_CONSTEXPR void on_auto() { arg_id = handler.on_arg_id(); }
+ FMT_CONSTEXPR void on_index(int id) { arg_id = handler.on_arg_id(id); }
+ FMT_CONSTEXPR void on_name(basic_string_view<Char> id) {
+ arg_id = handler.on_arg_id(id);
+ }
+ };
+
+ ++begin;
+ if (begin == end) return handler.on_error("invalid format string"), end;
+ if (*begin == '}') {
+ handler.on_replacement_field(handler.on_arg_id(), begin);
+ } else if (*begin == '{') {
+ handler.on_text(begin, begin + 1);
+ } else {
+ auto adapter = id_adapter{handler, 0};
+ begin = parse_arg_id(begin, end, adapter);
+ Char c = begin != end ? *begin : Char();
+ if (c == '}') {
+ handler.on_replacement_field(adapter.arg_id, begin);
+ } else if (c == ':') {
+ begin = handler.on_format_specs(adapter.arg_id, begin + 1, end);
+ if (begin == end || *begin != '}')
+ return handler.on_error("unknown format specifier"), end;
+ } else {
+ return handler.on_error("missing '}' in format string"), end;
+ }
+ }
+ return begin + 1;
+}
+
+template <bool IS_CONSTEXPR, typename Char, typename Handler>
+FMT_CONSTEXPR FMT_INLINE void parse_format_string(
+ basic_string_view<Char> format_str, Handler&& handler) {
+ auto begin = format_str.data();
+ auto end = begin + format_str.size();
+ if (end - begin < 32) {
+ // Use a simple loop instead of memchr for small strings.
+ const Char* p = begin;
+ while (p != end) {
+ auto c = *p++;
+ if (c == '{') {
+ handler.on_text(begin, p - 1);
+ begin = p = parse_replacement_field(p - 1, end, handler);
+ } else if (c == '}') {
+ if (p == end || *p != '}')
+ return handler.on_error("unmatched '}' in format string");
+ handler.on_text(begin, p);
+ begin = ++p;
+ }
+ }
+ handler.on_text(begin, end);
+ return;
+ }
+ struct writer {
+ FMT_CONSTEXPR void operator()(const Char* from, const Char* to) {
+ if (from == to) return;
+ for (;;) {
+ const Char* p = nullptr;
+ if (!find<IS_CONSTEXPR>(from, to, Char('}'), p))
+ return handler_.on_text(from, to);
+ ++p;
+ if (p == to || *p != '}')
+ return handler_.on_error("unmatched '}' in format string");
+ handler_.on_text(from, p);
+ from = p + 1;
+ }
+ }
+ Handler& handler_;
+ } write = {handler};
+ while (begin != end) {
+ // Doing two passes with memchr (one for '{' and another for '}') is up to
+ // 2.5x faster than the naive one-pass implementation on big format strings.
+ const Char* p = begin;
+ if (*begin != '{' && !find<IS_CONSTEXPR>(begin + 1, end, Char('{'), p))
+ return write(begin, end);
+ write(begin, p);
+ begin = parse_replacement_field(p, end, handler);
+ }
+}
+
+template <typename T, bool = is_named_arg<T>::value> struct strip_named_arg {
+ using type = T;
+};
+template <typename T> struct strip_named_arg<T, true> {
+ using type = remove_cvref_t<decltype(T::value)>;
+};
+
+template <typename T, typename ParseContext>
+FMT_CONSTEXPR auto parse_format_specs(ParseContext& ctx)
+ -> decltype(ctx.begin()) {
+ using char_type = typename ParseContext::char_type;
+ using context = buffer_context<char_type>;
+ using mapped_type = conditional_t<
+ mapped_type_constant<T, context>::value != type::custom_type,
+ decltype(arg_mapper<context>().map(std::declval<const T&>())),
+ typename strip_named_arg<T>::type>;
+#if defined(__cpp_if_constexpr)
+ if constexpr (std::is_default_constructible<
+ formatter<mapped_type, char_type>>::value) {
+ return formatter<mapped_type, char_type>().parse(ctx);
+ } else {
+ type_is_unformattable_for<T, char_type> _;
+ return ctx.begin();
+ }
+#else
+ return formatter<mapped_type, char_type>().parse(ctx);
+#endif
+}
+
+// Checks char specs and returns true iff the presentation type is char-like.
+template <typename Char>
+FMT_CONSTEXPR auto check_char_specs(const format_specs<Char>& specs) -> bool {
+ if (specs.type != presentation_type::none &&
+ specs.type != presentation_type::chr &&
+ specs.type != presentation_type::debug) {
+ return false;
+ }
+ if (specs.align == align::numeric || specs.sign != sign::none || specs.alt)
+ throw_format_error("invalid format specifier for char");
+ return true;
+}
+
+#if FMT_USE_NONTYPE_TEMPLATE_ARGS
+template <int N, typename T, typename... Args, typename Char>
+constexpr auto get_arg_index_by_name(basic_string_view<Char> name) -> int {
+ if constexpr (is_statically_named_arg<T>()) {
+ if (name == T::name) return N;
+ }
+ if constexpr (sizeof...(Args) > 0)
+ return get_arg_index_by_name<N + 1, Args...>(name);
+ (void)name; // Workaround an MSVC bug about "unused" parameter.
+ return -1;
+}
+#endif
+
+template <typename... Args, typename Char>
+FMT_CONSTEXPR auto get_arg_index_by_name(basic_string_view<Char> name) -> int {
+#if FMT_USE_NONTYPE_TEMPLATE_ARGS
+ if constexpr (sizeof...(Args) > 0)
+ return get_arg_index_by_name<0, Args...>(name);
+#endif
+ (void)name;
+ return -1;
+}
+
+template <typename Char, typename... Args> class format_string_checker {
+ private:
+ using parse_context_type = compile_parse_context<Char>;
+ static constexpr int num_args = sizeof...(Args);
+
+ // Format specifier parsing function.
+ // In the future basic_format_parse_context will replace compile_parse_context
+ // here and will use is_constant_evaluated and downcasting to access the data
+ // needed for compile-time checks: https://godbolt.org/z/GvWzcTjh1.
+ using parse_func = const Char* (*)(parse_context_type&);
+
+ type types_[num_args > 0 ? static_cast<size_t>(num_args) : 1];
+ parse_context_type context_;
+ parse_func parse_funcs_[num_args > 0 ? static_cast<size_t>(num_args) : 1];
+
+ public:
+ explicit FMT_CONSTEXPR format_string_checker(basic_string_view<Char> fmt)
+ : types_{mapped_type_constant<Args, buffer_context<Char>>::value...},
+ context_(fmt, num_args, types_),
+ parse_funcs_{&parse_format_specs<Args, parse_context_type>...} {}
+
+ FMT_CONSTEXPR void on_text(const Char*, const Char*) {}
+
+ FMT_CONSTEXPR auto on_arg_id() -> int { return context_.next_arg_id(); }
+ FMT_CONSTEXPR auto on_arg_id(int id) -> int {
+ return context_.check_arg_id(id), id;
+ }
+ FMT_CONSTEXPR auto on_arg_id(basic_string_view<Char> id) -> int {
+#if FMT_USE_NONTYPE_TEMPLATE_ARGS
+ auto index = get_arg_index_by_name<Args...>(id);
+ if (index < 0) on_error("named argument is not found");
+ return index;
+#else
+ (void)id;
+ on_error("compile-time checks for named arguments require C++20 support");
+ return 0;
+#endif
+ }
+
+ FMT_CONSTEXPR void on_replacement_field(int id, const Char* begin) {
+ on_format_specs(id, begin, begin); // Call parse() on empty specs.
+ }
+
+ FMT_CONSTEXPR auto on_format_specs(int id, const Char* begin, const Char*)
+ -> const Char* {
+ context_.advance_to(begin);
+ // id >= 0 check is a workaround for gcc 10 bug (#2065).
+ return id >= 0 && id < num_args ? parse_funcs_[id](context_) : begin;
+ }
+
+ FMT_CONSTEXPR void on_error(const char* message) {
+ throw_format_error(message);
+ }
+};
+
+// Reports a compile-time error if S is not a valid format string.
+template <typename..., typename S, FMT_ENABLE_IF(!is_compile_string<S>::value)>
+FMT_INLINE void check_format_string(const S&) {
+#ifdef FMT_ENFORCE_COMPILE_STRING
+ static_assert(is_compile_string<S>::value,
+ "FMT_ENFORCE_COMPILE_STRING requires all format strings to use "
+ "FMT_STRING.");
+#endif
+}
+template <typename... Args, typename S,
+ FMT_ENABLE_IF(is_compile_string<S>::value)>
+void check_format_string(S format_str) {
+ using char_t = typename S::char_type;
+ FMT_CONSTEXPR auto s = basic_string_view<char_t>(format_str);
+ using checker = format_string_checker<char_t, remove_cvref_t<Args>...>;
+ FMT_CONSTEXPR bool error = (parse_format_string<true>(s, checker(s)), true);
+ ignore_unused(error);
+}
+
+template <typename Char = char> struct vformat_args {
+ using type = basic_format_args<
+ basic_format_context<std::back_insert_iterator<buffer<Char>>, Char>>;
+};
+template <> struct vformat_args<char> {
+ using type = format_args;
+};
+
+// Use vformat_args and avoid type_identity to keep symbols short.
+template <typename Char>
+void vformat_to(buffer<Char>& buf, basic_string_view<Char> fmt,
+ typename vformat_args<Char>::type args, locale_ref loc = {});
+
+FMT_API void vprint_mojibake(std::FILE*, string_view, format_args);
+#ifndef _WIN32
+inline void vprint_mojibake(std::FILE*, string_view, format_args) {}
+#endif
+} // namespace detail
+
+FMT_BEGIN_EXPORT
+
+// A formatter specialization for natively supported types.
+template <typename T, typename Char>
+struct formatter<T, Char,
+ enable_if_t<detail::type_constant<T, Char>::value !=
+ detail::type::custom_type>> {
+ private:
+ detail::dynamic_format_specs<Char> specs_;
+
+ public:
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> const Char* {
+ auto type = detail::type_constant<T, Char>::value;
+ auto end =
+ detail::parse_format_specs(ctx.begin(), ctx.end(), specs_, ctx, type);
+ if (type == detail::type::char_type) detail::check_char_specs(specs_);
+ return end;
+ }
+
+ template <detail::type U = detail::type_constant<T, Char>::value,
+ FMT_ENABLE_IF(U == detail::type::string_type ||
+ U == detail::type::cstring_type ||
+ U == detail::type::char_type)>
+ FMT_CONSTEXPR void set_debug_format(bool set = true) {
+ specs_.type = set ? presentation_type::debug : presentation_type::none;
+ }
+
+ template <typename FormatContext>
+ FMT_CONSTEXPR auto format(const T& val, FormatContext& ctx) const
+ -> decltype(ctx.out());
+};
+
+template <typename Char = char> struct runtime_format_string {
+ basic_string_view<Char> str;
+};
+
+/** A compile-time format string. */
+template <typename Char, typename... Args> class basic_format_string {
+ private:
+ basic_string_view<Char> str_;
+
+ public:
+ template <typename S,
+ FMT_ENABLE_IF(
+ std::is_convertible<const S&, basic_string_view<Char>>::value)>
+ FMT_CONSTEVAL FMT_INLINE basic_format_string(const S& s) : str_(s) {
+ static_assert(
+ detail::count<
+ (std::is_base_of<detail::view, remove_reference_t<Args>>::value &&
+ std::is_reference<Args>::value)...>() == 0,
+ "passing views as lvalues is disallowed");
+#ifdef FMT_HAS_CONSTEVAL
+ if constexpr (detail::count_named_args<Args...>() ==
+ detail::count_statically_named_args<Args...>()) {
+ using checker =
+ detail::format_string_checker<Char, remove_cvref_t<Args>...>;
+ detail::parse_format_string<true>(str_, checker(s));
+ }
+#else
+ detail::check_format_string<Args...>(s);
+#endif
+ }
+ basic_format_string(runtime_format_string<Char> fmt) : str_(fmt.str) {}
+
+ FMT_INLINE operator basic_string_view<Char>() const { return str_; }
+ FMT_INLINE auto get() const -> basic_string_view<Char> { return str_; }
+};
+
+#if FMT_GCC_VERSION && FMT_GCC_VERSION < 409
+// Workaround broken conversion on older gcc.
+template <typename...> using format_string = string_view;
+inline auto runtime(string_view s) -> string_view { return s; }
+#else
+template <typename... Args>
+using format_string = basic_format_string<char, type_identity_t<Args>...>;
+/**
+ \rst
+ Creates a runtime format string.
+
+ **Example**::
+
+ // Check format string at runtime instead of compile-time.
+ fmt::print(fmt::runtime("{:d}"), "I am not a number");
+ \endrst
+ */
+inline auto runtime(string_view s) -> runtime_format_string<> { return {{s}}; }
+#endif
+
+FMT_API auto vformat(string_view fmt, format_args args) -> std::string;
+
+/**
+ \rst
+ Formats ``args`` according to specifications in ``fmt`` and returns the result
+ as a string.
+
+ **Example**::
+
+ #include <fmt/core.h>
+ std::string message = fmt::format("The answer is {}.", 42);
+ \endrst
+*/
+template <typename... T>
+FMT_NODISCARD FMT_INLINE auto format(format_string<T...> fmt, T&&... args)
+ -> std::string {
+ return vformat(fmt, fmt::make_format_args(args...));
+}
+
+/** Formats a string and writes the output to ``out``. */
+template <typename OutputIt,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value)>
+auto vformat_to(OutputIt out, string_view fmt, format_args args) -> OutputIt {
+ auto&& buf = detail::get_buffer<char>(out);
+ detail::vformat_to(buf, fmt, args, {});
+ return detail::get_iterator(buf, out);
+}
+
+/**
+ \rst
+ Formats ``args`` according to specifications in ``fmt``, writes the result to
+ the output iterator ``out`` and returns the iterator past the end of the output
+ range. `format_to` does not append a terminating null character.
+
+ **Example**::
+
+ auto out = std::vector<char>();
+ fmt::format_to(std::back_inserter(out), "{}", 42);
+ \endrst
+ */
+template <typename OutputIt, typename... T,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value)>
+FMT_INLINE auto format_to(OutputIt out, format_string<T...> fmt, T&&... args)
+ -> OutputIt {
+ return vformat_to(out, fmt, fmt::make_format_args(args...));
+}
+
+template <typename OutputIt> struct format_to_n_result {
+ /** Iterator past the end of the output range. */
+ OutputIt out;
+ /** Total (not truncated) output size. */
+ size_t size;
+};
+
+template <typename OutputIt, typename... T,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value)>
+auto vformat_to_n(OutputIt out, size_t n, string_view fmt, format_args args)
+ -> format_to_n_result<OutputIt> {
+ using traits = detail::fixed_buffer_traits;
+ auto buf = detail::iterator_buffer<OutputIt, char, traits>(out, n);
+ detail::vformat_to(buf, fmt, args, {});
+ return {buf.out(), buf.count()};
+}
+
+/**
+ \rst
+ Formats ``args`` according to specifications in ``fmt``, writes up to ``n``
+ characters of the result to the output iterator ``out`` and returns the total
+ (not truncated) output size and the iterator past the end of the output range.
+ `format_to_n` does not append a terminating null character.
+ \endrst
+ */
+template <typename OutputIt, typename... T,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value)>
+FMT_INLINE auto format_to_n(OutputIt out, size_t n, format_string<T...> fmt,
+ T&&... args) -> format_to_n_result<OutputIt> {
+ return vformat_to_n(out, n, fmt, fmt::make_format_args(args...));
+}
+
+/** Returns the number of chars in the output of ``format(fmt, args...)``. */
+template <typename... T>
+FMT_NODISCARD FMT_INLINE auto formatted_size(format_string<T...> fmt,
+ T&&... args) -> size_t {
+ auto buf = detail::counting_buffer<>();
+ detail::vformat_to<char>(buf, fmt, fmt::make_format_args(args...), {});
+ return buf.count();
+}
+
+FMT_API void vprint(string_view fmt, format_args args);
+FMT_API void vprint(std::FILE* f, string_view fmt, format_args args);
+
+/**
+ \rst
+ Formats ``args`` according to specifications in ``fmt`` and writes the output
+ to ``stdout``.
+
+ **Example**::
+
+ fmt::print("Elapsed time: {0:.2f} seconds", 1.23);
+ \endrst
+ */
+template <typename... T>
+FMT_INLINE void print(format_string<T...> fmt, T&&... args) {
+ const auto& vargs = fmt::make_format_args(args...);
+ return detail::is_utf8() ? vprint(fmt, vargs)
+ : detail::vprint_mojibake(stdout, fmt, vargs);
+}
+
+/**
+ \rst
+ Formats ``args`` according to specifications in ``fmt`` and writes the
+ output to the file ``f``.
+
+ **Example**::
+
+ fmt::print(stderr, "Don't {}!", "panic");
+ \endrst
+ */
+template <typename... T>
+FMT_INLINE void print(std::FILE* f, format_string<T...> fmt, T&&... args) {
+ const auto& vargs = fmt::make_format_args(args...);
+ return detail::is_utf8() ? vprint(f, fmt, vargs)
+ : detail::vprint_mojibake(f, fmt, vargs);
+}
+
+/**
+ Formats ``args`` according to specifications in ``fmt`` and writes the
+ output to the file ``f`` followed by a newline.
+ */
+template <typename... T>
+FMT_INLINE void println(std::FILE* f, format_string<T...> fmt, T&&... args) {
+ return fmt::print(f, "{}\n", fmt::format(fmt, std::forward<T>(args)...));
+}
+
+/**
+ Formats ``args`` according to specifications in ``fmt`` and writes the output
+ to ``stdout`` followed by a newline.
+ */
+template <typename... T>
+FMT_INLINE void println(format_string<T...> fmt, T&&... args) {
+ return fmt::println(stdout, fmt, std::forward<T>(args)...);
+}
+
+FMT_END_EXPORT
+FMT_GCC_PRAGMA("GCC pop_options")
+FMT_END_NAMESPACE
+
+#ifdef FMT_HEADER_ONLY
+# include "format.h"
+#endif
+#endif // FMT_CORE_H_
--- /dev/null
+// Formatting library for C++ - implementation
+//
+// Copyright (c) 2012 - 2016, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_FORMAT_INL_H_
+#define FMT_FORMAT_INL_H_
+
+#include <algorithm>
+#include <cerrno> // errno
+#include <climits>
+#include <cmath>
+#include <exception>
+
+#ifndef FMT_STATIC_THOUSANDS_SEPARATOR
+# include <locale>
+#endif
+
+#if defined(_WIN32) && !defined(FMT_WINDOWS_NO_WCHAR)
+# include <io.h> // _isatty
+#endif
+
+#include "format.h"
+
+FMT_BEGIN_NAMESPACE
+namespace detail {
+
+FMT_FUNC void assert_fail(const char* file, int line, const char* message) {
+ // Use unchecked std::fprintf to avoid triggering another assertion when
+ // writing to stderr fails
+ std::fprintf(stderr, "%s:%d: assertion failed: %s", file, line, message);
+ // Chosen instead of std::abort to satisfy Clang in CUDA mode during device
+ // code pass.
+ std::terminate();
+}
+
+FMT_FUNC void throw_format_error(const char* message) {
+ FMT_THROW(format_error(message));
+}
+
+FMT_FUNC void format_error_code(detail::buffer<char>& out, int error_code,
+ string_view message) noexcept {
+ // Report error code making sure that the output fits into
+ // inline_buffer_size to avoid dynamic memory allocation and potential
+ // bad_alloc.
+ out.try_resize(0);
+ static const char SEP[] = ": ";
+ static const char ERROR_STR[] = "error ";
+ // Subtract 2 to account for terminating null characters in SEP and ERROR_STR.
+ size_t error_code_size = sizeof(SEP) + sizeof(ERROR_STR) - 2;
+ auto abs_value = static_cast<uint32_or_64_or_128_t<int>>(error_code);
+ if (detail::is_negative(error_code)) {
+ abs_value = 0 - abs_value;
+ ++error_code_size;
+ }
+ error_code_size += detail::to_unsigned(detail::count_digits(abs_value));
+ auto it = buffer_appender<char>(out);
+ if (message.size() <= inline_buffer_size - error_code_size)
+ fmt::format_to(it, FMT_STRING("{}{}"), message, SEP);
+ fmt::format_to(it, FMT_STRING("{}{}"), ERROR_STR, error_code);
+ FMT_ASSERT(out.size() <= inline_buffer_size, "");
+}
+
+FMT_FUNC void report_error(format_func func, int error_code,
+ const char* message) noexcept {
+ memory_buffer full_message;
+ func(full_message, error_code, message);
+ // Don't use fwrite_fully because the latter may throw.
+ if (std::fwrite(full_message.data(), full_message.size(), 1, stderr) > 0)
+ std::fputc('\n', stderr);
+}
+
+// A wrapper around fwrite that throws on error.
+inline void fwrite_fully(const void* ptr, size_t count, FILE* stream) {
+ size_t written = std::fwrite(ptr, 1, count, stream);
+ if (written < count)
+ FMT_THROW(system_error(errno, FMT_STRING("cannot write to file")));
+}
+
+#ifndef FMT_STATIC_THOUSANDS_SEPARATOR
+template <typename Locale>
+locale_ref::locale_ref(const Locale& loc) : locale_(&loc) {
+ static_assert(std::is_same<Locale, std::locale>::value, "");
+}
+
+template <typename Locale> auto locale_ref::get() const -> Locale {
+ static_assert(std::is_same<Locale, std::locale>::value, "");
+ return locale_ ? *static_cast<const std::locale*>(locale_) : std::locale();
+}
+
+template <typename Char>
+FMT_FUNC auto thousands_sep_impl(locale_ref loc) -> thousands_sep_result<Char> {
+ auto& facet = std::use_facet<std::numpunct<Char>>(loc.get<std::locale>());
+ auto grouping = facet.grouping();
+ auto thousands_sep = grouping.empty() ? Char() : facet.thousands_sep();
+ return {std::move(grouping), thousands_sep};
+}
+template <typename Char>
+FMT_FUNC auto decimal_point_impl(locale_ref loc) -> Char {
+ return std::use_facet<std::numpunct<Char>>(loc.get<std::locale>())
+ .decimal_point();
+}
+#else
+template <typename Char>
+FMT_FUNC auto thousands_sep_impl(locale_ref) -> thousands_sep_result<Char> {
+ return {"\03", FMT_STATIC_THOUSANDS_SEPARATOR};
+}
+template <typename Char> FMT_FUNC Char decimal_point_impl(locale_ref) {
+ return '.';
+}
+#endif
+
+FMT_FUNC auto write_loc(appender out, loc_value value,
+ const format_specs<>& specs, locale_ref loc) -> bool {
+#ifndef FMT_STATIC_THOUSANDS_SEPARATOR
+ auto locale = loc.get<std::locale>();
+ // We cannot use the num_put<char> facet because it may produce output in
+ // a wrong encoding.
+ using facet = format_facet<std::locale>;
+ if (std::has_facet<facet>(locale))
+ return std::use_facet<facet>(locale).put(out, value, specs);
+ return facet(locale).put(out, value, specs);
+#endif
+ return false;
+}
+} // namespace detail
+
+template <typename Locale> typename Locale::id format_facet<Locale>::id;
+
+#ifndef FMT_STATIC_THOUSANDS_SEPARATOR
+template <typename Locale> format_facet<Locale>::format_facet(Locale& loc) {
+ auto& numpunct = std::use_facet<std::numpunct<char>>(loc);
+ grouping_ = numpunct.grouping();
+ if (!grouping_.empty()) separator_ = std::string(1, numpunct.thousands_sep());
+}
+
+template <>
+FMT_API FMT_FUNC auto format_facet<std::locale>::do_put(
+ appender out, loc_value val, const format_specs<>& specs) const -> bool {
+ return val.visit(
+ detail::loc_writer<>{out, specs, separator_, grouping_, decimal_point_});
+}
+#endif
+
+FMT_FUNC auto vsystem_error(int error_code, string_view fmt, format_args args)
+ -> std::system_error {
+ auto ec = std::error_code(error_code, std::generic_category());
+ return std::system_error(ec, vformat(fmt, args));
+}
+
+namespace detail {
+
+template <typename F>
+inline auto operator==(basic_fp<F> x, basic_fp<F> y) -> bool {
+ return x.f == y.f && x.e == y.e;
+}
+
+// Compilers should be able to optimize this into the ror instruction.
+FMT_CONSTEXPR inline auto rotr(uint32_t n, uint32_t r) noexcept -> uint32_t {
+ r &= 31;
+ return (n >> r) | (n << (32 - r));
+}
+FMT_CONSTEXPR inline auto rotr(uint64_t n, uint32_t r) noexcept -> uint64_t {
+ r &= 63;
+ return (n >> r) | (n << (64 - r));
+}
+
+// Implementation of Dragonbox algorithm: https://github.com/jk-jeon/dragonbox.
+namespace dragonbox {
+// Computes upper 64 bits of multiplication of a 32-bit unsigned integer and a
+// 64-bit unsigned integer.
+inline auto umul96_upper64(uint32_t x, uint64_t y) noexcept -> uint64_t {
+ return umul128_upper64(static_cast<uint64_t>(x) << 32, y);
+}
+
+// Computes lower 128 bits of multiplication of a 64-bit unsigned integer and a
+// 128-bit unsigned integer.
+inline auto umul192_lower128(uint64_t x, uint128_fallback y) noexcept
+ -> uint128_fallback {
+ uint64_t high = x * y.high();
+ uint128_fallback high_low = umul128(x, y.low());
+ return {high + high_low.high(), high_low.low()};
+}
+
+// Computes lower 64 bits of multiplication of a 32-bit unsigned integer and a
+// 64-bit unsigned integer.
+inline auto umul96_lower64(uint32_t x, uint64_t y) noexcept -> uint64_t {
+ return x * y;
+}
+
+// Various fast log computations.
+inline auto floor_log10_pow2_minus_log10_4_over_3(int e) noexcept -> int {
+ FMT_ASSERT(e <= 2936 && e >= -2985, "too large exponent");
+ return (e * 631305 - 261663) >> 21;
+}
+
+FMT_INLINE_VARIABLE constexpr struct {
+ uint32_t divisor;
+ int shift_amount;
+} div_small_pow10_infos[] = {{10, 16}, {100, 16}};
+
+// Replaces n by floor(n / pow(10, N)) returning true if and only if n is
+// divisible by pow(10, N).
+// Precondition: n <= pow(10, N + 1).
+template <int N>
+auto check_divisibility_and_divide_by_pow10(uint32_t& n) noexcept -> bool {
+ // The numbers below are chosen such that:
+ // 1. floor(n/d) = floor(nm / 2^k) where d=10 or d=100,
+ // 2. nm mod 2^k < m if and only if n is divisible by d,
+ // where m is magic_number, k is shift_amount
+ // and d is divisor.
+ //
+ // Item 1 is a common technique of replacing division by a constant with
+ // multiplication, see e.g. "Division by Invariant Integers Using
+ // Multiplication" by Granlund and Montgomery (1994). magic_number (m) is set
+ // to ceil(2^k/d) for large enough k.
+ // The idea for item 2 originates from Schubfach.
+ constexpr auto info = div_small_pow10_infos[N - 1];
+ FMT_ASSERT(n <= info.divisor * 10, "n is too large");
+ constexpr uint32_t magic_number =
+ (1u << info.shift_amount) / info.divisor + 1;
+ n *= magic_number;
+ const uint32_t comparison_mask = (1u << info.shift_amount) - 1;
+ bool result = (n & comparison_mask) < magic_number;
+ n >>= info.shift_amount;
+ return result;
+}
+
+// Computes floor(n / pow(10, N)) for small n and N.
+// Precondition: n <= pow(10, N + 1).
+template <int N> auto small_division_by_pow10(uint32_t n) noexcept -> uint32_t {
+ constexpr auto info = div_small_pow10_infos[N - 1];
+ FMT_ASSERT(n <= info.divisor * 10, "n is too large");
+ constexpr uint32_t magic_number =
+ (1u << info.shift_amount) / info.divisor + 1;
+ return (n * magic_number) >> info.shift_amount;
+}
+
+// Computes floor(n / 10^(kappa + 1)) (float)
+inline auto divide_by_10_to_kappa_plus_1(uint32_t n) noexcept -> uint32_t {
+ // 1374389535 = ceil(2^37/100)
+ return static_cast<uint32_t>((static_cast<uint64_t>(n) * 1374389535) >> 37);
+}
+// Computes floor(n / 10^(kappa + 1)) (double)
+inline auto divide_by_10_to_kappa_plus_1(uint64_t n) noexcept -> uint64_t {
+ // 2361183241434822607 = ceil(2^(64+7)/1000)
+ return umul128_upper64(n, 2361183241434822607ull) >> 7;
+}
+
+// Various subroutines using pow10 cache
+template <typename T> struct cache_accessor;
+
+template <> struct cache_accessor<float> {
+ using carrier_uint = float_info<float>::carrier_uint;
+ using cache_entry_type = uint64_t;
+
+ static auto get_cached_power(int k) noexcept -> uint64_t {
+ FMT_ASSERT(k >= float_info<float>::min_k && k <= float_info<float>::max_k,
+ "k is out of range");
+ static constexpr const uint64_t pow10_significands[] = {
+ 0x81ceb32c4b43fcf5, 0xa2425ff75e14fc32, 0xcad2f7f5359a3b3f,
+ 0xfd87b5f28300ca0e, 0x9e74d1b791e07e49, 0xc612062576589ddb,
+ 0xf79687aed3eec552, 0x9abe14cd44753b53, 0xc16d9a0095928a28,
+ 0xf1c90080baf72cb2, 0x971da05074da7bef, 0xbce5086492111aeb,
+ 0xec1e4a7db69561a6, 0x9392ee8e921d5d08, 0xb877aa3236a4b44a,
+ 0xe69594bec44de15c, 0x901d7cf73ab0acda, 0xb424dc35095cd810,
+ 0xe12e13424bb40e14, 0x8cbccc096f5088cc, 0xafebff0bcb24aaff,
+ 0xdbe6fecebdedd5bf, 0x89705f4136b4a598, 0xabcc77118461cefd,
+ 0xd6bf94d5e57a42bd, 0x8637bd05af6c69b6, 0xa7c5ac471b478424,
+ 0xd1b71758e219652c, 0x83126e978d4fdf3c, 0xa3d70a3d70a3d70b,
+ 0xcccccccccccccccd, 0x8000000000000000, 0xa000000000000000,
+ 0xc800000000000000, 0xfa00000000000000, 0x9c40000000000000,
+ 0xc350000000000000, 0xf424000000000000, 0x9896800000000000,
+ 0xbebc200000000000, 0xee6b280000000000, 0x9502f90000000000,
+ 0xba43b74000000000, 0xe8d4a51000000000, 0x9184e72a00000000,
+ 0xb5e620f480000000, 0xe35fa931a0000000, 0x8e1bc9bf04000000,
+ 0xb1a2bc2ec5000000, 0xde0b6b3a76400000, 0x8ac7230489e80000,
+ 0xad78ebc5ac620000, 0xd8d726b7177a8000, 0x878678326eac9000,
+ 0xa968163f0a57b400, 0xd3c21bcecceda100, 0x84595161401484a0,
+ 0xa56fa5b99019a5c8, 0xcecb8f27f4200f3a, 0x813f3978f8940985,
+ 0xa18f07d736b90be6, 0xc9f2c9cd04674edf, 0xfc6f7c4045812297,
+ 0x9dc5ada82b70b59e, 0xc5371912364ce306, 0xf684df56c3e01bc7,
+ 0x9a130b963a6c115d, 0xc097ce7bc90715b4, 0xf0bdc21abb48db21,
+ 0x96769950b50d88f5, 0xbc143fa4e250eb32, 0xeb194f8e1ae525fe,
+ 0x92efd1b8d0cf37bf, 0xb7abc627050305ae, 0xe596b7b0c643c71a,
+ 0x8f7e32ce7bea5c70, 0xb35dbf821ae4f38c, 0xe0352f62a19e306f};
+ return pow10_significands[k - float_info<float>::min_k];
+ }
+
+ struct compute_mul_result {
+ carrier_uint result;
+ bool is_integer;
+ };
+ struct compute_mul_parity_result {
+ bool parity;
+ bool is_integer;
+ };
+
+ static auto compute_mul(carrier_uint u,
+ const cache_entry_type& cache) noexcept
+ -> compute_mul_result {
+ auto r = umul96_upper64(u, cache);
+ return {static_cast<carrier_uint>(r >> 32),
+ static_cast<carrier_uint>(r) == 0};
+ }
+
+ static auto compute_delta(const cache_entry_type& cache, int beta) noexcept
+ -> uint32_t {
+ return static_cast<uint32_t>(cache >> (64 - 1 - beta));
+ }
+
+ static auto compute_mul_parity(carrier_uint two_f,
+ const cache_entry_type& cache,
+ int beta) noexcept
+ -> compute_mul_parity_result {
+ FMT_ASSERT(beta >= 1, "");
+ FMT_ASSERT(beta < 64, "");
+
+ auto r = umul96_lower64(two_f, cache);
+ return {((r >> (64 - beta)) & 1) != 0,
+ static_cast<uint32_t>(r >> (32 - beta)) == 0};
+ }
+
+ static auto compute_left_endpoint_for_shorter_interval_case(
+ const cache_entry_type& cache, int beta) noexcept -> carrier_uint {
+ return static_cast<carrier_uint>(
+ (cache - (cache >> (num_significand_bits<float>() + 2))) >>
+ (64 - num_significand_bits<float>() - 1 - beta));
+ }
+
+ static auto compute_right_endpoint_for_shorter_interval_case(
+ const cache_entry_type& cache, int beta) noexcept -> carrier_uint {
+ return static_cast<carrier_uint>(
+ (cache + (cache >> (num_significand_bits<float>() + 1))) >>
+ (64 - num_significand_bits<float>() - 1 - beta));
+ }
+
+ static auto compute_round_up_for_shorter_interval_case(
+ const cache_entry_type& cache, int beta) noexcept -> carrier_uint {
+ return (static_cast<carrier_uint>(
+ cache >> (64 - num_significand_bits<float>() - 2 - beta)) +
+ 1) /
+ 2;
+ }
+};
+
+template <> struct cache_accessor<double> {
+ using carrier_uint = float_info<double>::carrier_uint;
+ using cache_entry_type = uint128_fallback;
+
+ static auto get_cached_power(int k) noexcept -> uint128_fallback {
+ FMT_ASSERT(k >= float_info<double>::min_k && k <= float_info<double>::max_k,
+ "k is out of range");
+
+ static constexpr const uint128_fallback pow10_significands[] = {
+#if FMT_USE_FULL_CACHE_DRAGONBOX
+ {0xff77b1fcbebcdc4f, 0x25e8e89c13bb0f7b},
+ {0x9faacf3df73609b1, 0x77b191618c54e9ad},
+ {0xc795830d75038c1d, 0xd59df5b9ef6a2418},
+ {0xf97ae3d0d2446f25, 0x4b0573286b44ad1e},
+ {0x9becce62836ac577, 0x4ee367f9430aec33},
+ {0xc2e801fb244576d5, 0x229c41f793cda740},
+ {0xf3a20279ed56d48a, 0x6b43527578c11110},
+ {0x9845418c345644d6, 0x830a13896b78aaaa},
+ {0xbe5691ef416bd60c, 0x23cc986bc656d554},
+ {0xedec366b11c6cb8f, 0x2cbfbe86b7ec8aa9},
+ {0x94b3a202eb1c3f39, 0x7bf7d71432f3d6aa},
+ {0xb9e08a83a5e34f07, 0xdaf5ccd93fb0cc54},
+ {0xe858ad248f5c22c9, 0xd1b3400f8f9cff69},
+ {0x91376c36d99995be, 0x23100809b9c21fa2},
+ {0xb58547448ffffb2d, 0xabd40a0c2832a78b},
+ {0xe2e69915b3fff9f9, 0x16c90c8f323f516d},
+ {0x8dd01fad907ffc3b, 0xae3da7d97f6792e4},
+ {0xb1442798f49ffb4a, 0x99cd11cfdf41779d},
+ {0xdd95317f31c7fa1d, 0x40405643d711d584},
+ {0x8a7d3eef7f1cfc52, 0x482835ea666b2573},
+ {0xad1c8eab5ee43b66, 0xda3243650005eed0},
+ {0xd863b256369d4a40, 0x90bed43e40076a83},
+ {0x873e4f75e2224e68, 0x5a7744a6e804a292},
+ {0xa90de3535aaae202, 0x711515d0a205cb37},
+ {0xd3515c2831559a83, 0x0d5a5b44ca873e04},
+ {0x8412d9991ed58091, 0xe858790afe9486c3},
+ {0xa5178fff668ae0b6, 0x626e974dbe39a873},
+ {0xce5d73ff402d98e3, 0xfb0a3d212dc81290},
+ {0x80fa687f881c7f8e, 0x7ce66634bc9d0b9a},
+ {0xa139029f6a239f72, 0x1c1fffc1ebc44e81},
+ {0xc987434744ac874e, 0xa327ffb266b56221},
+ {0xfbe9141915d7a922, 0x4bf1ff9f0062baa9},
+ {0x9d71ac8fada6c9b5, 0x6f773fc3603db4aa},
+ {0xc4ce17b399107c22, 0xcb550fb4384d21d4},
+ {0xf6019da07f549b2b, 0x7e2a53a146606a49},
+ {0x99c102844f94e0fb, 0x2eda7444cbfc426e},
+ {0xc0314325637a1939, 0xfa911155fefb5309},
+ {0xf03d93eebc589f88, 0x793555ab7eba27cb},
+ {0x96267c7535b763b5, 0x4bc1558b2f3458df},
+ {0xbbb01b9283253ca2, 0x9eb1aaedfb016f17},
+ {0xea9c227723ee8bcb, 0x465e15a979c1cadd},
+ {0x92a1958a7675175f, 0x0bfacd89ec191eca},
+ {0xb749faed14125d36, 0xcef980ec671f667c},
+ {0xe51c79a85916f484, 0x82b7e12780e7401b},
+ {0x8f31cc0937ae58d2, 0xd1b2ecb8b0908811},
+ {0xb2fe3f0b8599ef07, 0x861fa7e6dcb4aa16},
+ {0xdfbdcece67006ac9, 0x67a791e093e1d49b},
+ {0x8bd6a141006042bd, 0xe0c8bb2c5c6d24e1},
+ {0xaecc49914078536d, 0x58fae9f773886e19},
+ {0xda7f5bf590966848, 0xaf39a475506a899f},
+ {0x888f99797a5e012d, 0x6d8406c952429604},
+ {0xaab37fd7d8f58178, 0xc8e5087ba6d33b84},
+ {0xd5605fcdcf32e1d6, 0xfb1e4a9a90880a65},
+ {0x855c3be0a17fcd26, 0x5cf2eea09a550680},
+ {0xa6b34ad8c9dfc06f, 0xf42faa48c0ea481f},
+ {0xd0601d8efc57b08b, 0xf13b94daf124da27},
+ {0x823c12795db6ce57, 0x76c53d08d6b70859},
+ {0xa2cb1717b52481ed, 0x54768c4b0c64ca6f},
+ {0xcb7ddcdda26da268, 0xa9942f5dcf7dfd0a},
+ {0xfe5d54150b090b02, 0xd3f93b35435d7c4d},
+ {0x9efa548d26e5a6e1, 0xc47bc5014a1a6db0},
+ {0xc6b8e9b0709f109a, 0x359ab6419ca1091c},
+ {0xf867241c8cc6d4c0, 0xc30163d203c94b63},
+ {0x9b407691d7fc44f8, 0x79e0de63425dcf1e},
+ {0xc21094364dfb5636, 0x985915fc12f542e5},
+ {0xf294b943e17a2bc4, 0x3e6f5b7b17b2939e},
+ {0x979cf3ca6cec5b5a, 0xa705992ceecf9c43},
+ {0xbd8430bd08277231, 0x50c6ff782a838354},
+ {0xece53cec4a314ebd, 0xa4f8bf5635246429},
+ {0x940f4613ae5ed136, 0x871b7795e136be9a},
+ {0xb913179899f68584, 0x28e2557b59846e40},
+ {0xe757dd7ec07426e5, 0x331aeada2fe589d0},
+ {0x9096ea6f3848984f, 0x3ff0d2c85def7622},
+ {0xb4bca50b065abe63, 0x0fed077a756b53aa},
+ {0xe1ebce4dc7f16dfb, 0xd3e8495912c62895},
+ {0x8d3360f09cf6e4bd, 0x64712dd7abbbd95d},
+ {0xb080392cc4349dec, 0xbd8d794d96aacfb4},
+ {0xdca04777f541c567, 0xecf0d7a0fc5583a1},
+ {0x89e42caaf9491b60, 0xf41686c49db57245},
+ {0xac5d37d5b79b6239, 0x311c2875c522ced6},
+ {0xd77485cb25823ac7, 0x7d633293366b828c},
+ {0x86a8d39ef77164bc, 0xae5dff9c02033198},
+ {0xa8530886b54dbdeb, 0xd9f57f830283fdfd},
+ {0xd267caa862a12d66, 0xd072df63c324fd7c},
+ {0x8380dea93da4bc60, 0x4247cb9e59f71e6e},
+ {0xa46116538d0deb78, 0x52d9be85f074e609},
+ {0xcd795be870516656, 0x67902e276c921f8c},
+ {0x806bd9714632dff6, 0x00ba1cd8a3db53b7},
+ {0xa086cfcd97bf97f3, 0x80e8a40eccd228a5},
+ {0xc8a883c0fdaf7df0, 0x6122cd128006b2ce},
+ {0xfad2a4b13d1b5d6c, 0x796b805720085f82},
+ {0x9cc3a6eec6311a63, 0xcbe3303674053bb1},
+ {0xc3f490aa77bd60fc, 0xbedbfc4411068a9d},
+ {0xf4f1b4d515acb93b, 0xee92fb5515482d45},
+ {0x991711052d8bf3c5, 0x751bdd152d4d1c4b},
+ {0xbf5cd54678eef0b6, 0xd262d45a78a0635e},
+ {0xef340a98172aace4, 0x86fb897116c87c35},
+ {0x9580869f0e7aac0e, 0xd45d35e6ae3d4da1},
+ {0xbae0a846d2195712, 0x8974836059cca10a},
+ {0xe998d258869facd7, 0x2bd1a438703fc94c},
+ {0x91ff83775423cc06, 0x7b6306a34627ddd0},
+ {0xb67f6455292cbf08, 0x1a3bc84c17b1d543},
+ {0xe41f3d6a7377eeca, 0x20caba5f1d9e4a94},
+ {0x8e938662882af53e, 0x547eb47b7282ee9d},
+ {0xb23867fb2a35b28d, 0xe99e619a4f23aa44},
+ {0xdec681f9f4c31f31, 0x6405fa00e2ec94d5},
+ {0x8b3c113c38f9f37e, 0xde83bc408dd3dd05},
+ {0xae0b158b4738705e, 0x9624ab50b148d446},
+ {0xd98ddaee19068c76, 0x3badd624dd9b0958},
+ {0x87f8a8d4cfa417c9, 0xe54ca5d70a80e5d7},
+ {0xa9f6d30a038d1dbc, 0x5e9fcf4ccd211f4d},
+ {0xd47487cc8470652b, 0x7647c32000696720},
+ {0x84c8d4dfd2c63f3b, 0x29ecd9f40041e074},
+ {0xa5fb0a17c777cf09, 0xf468107100525891},
+ {0xcf79cc9db955c2cc, 0x7182148d4066eeb5},
+ {0x81ac1fe293d599bf, 0xc6f14cd848405531},
+ {0xa21727db38cb002f, 0xb8ada00e5a506a7d},
+ {0xca9cf1d206fdc03b, 0xa6d90811f0e4851d},
+ {0xfd442e4688bd304a, 0x908f4a166d1da664},
+ {0x9e4a9cec15763e2e, 0x9a598e4e043287ff},
+ {0xc5dd44271ad3cdba, 0x40eff1e1853f29fe},
+ {0xf7549530e188c128, 0xd12bee59e68ef47d},
+ {0x9a94dd3e8cf578b9, 0x82bb74f8301958cf},
+ {0xc13a148e3032d6e7, 0xe36a52363c1faf02},
+ {0xf18899b1bc3f8ca1, 0xdc44e6c3cb279ac2},
+ {0x96f5600f15a7b7e5, 0x29ab103a5ef8c0ba},
+ {0xbcb2b812db11a5de, 0x7415d448f6b6f0e8},
+ {0xebdf661791d60f56, 0x111b495b3464ad22},
+ {0x936b9fcebb25c995, 0xcab10dd900beec35},
+ {0xb84687c269ef3bfb, 0x3d5d514f40eea743},
+ {0xe65829b3046b0afa, 0x0cb4a5a3112a5113},
+ {0x8ff71a0fe2c2e6dc, 0x47f0e785eaba72ac},
+ {0xb3f4e093db73a093, 0x59ed216765690f57},
+ {0xe0f218b8d25088b8, 0x306869c13ec3532d},
+ {0x8c974f7383725573, 0x1e414218c73a13fc},
+ {0xafbd2350644eeacf, 0xe5d1929ef90898fb},
+ {0xdbac6c247d62a583, 0xdf45f746b74abf3a},
+ {0x894bc396ce5da772, 0x6b8bba8c328eb784},
+ {0xab9eb47c81f5114f, 0x066ea92f3f326565},
+ {0xd686619ba27255a2, 0xc80a537b0efefebe},
+ {0x8613fd0145877585, 0xbd06742ce95f5f37},
+ {0xa798fc4196e952e7, 0x2c48113823b73705},
+ {0xd17f3b51fca3a7a0, 0xf75a15862ca504c6},
+ {0x82ef85133de648c4, 0x9a984d73dbe722fc},
+ {0xa3ab66580d5fdaf5, 0xc13e60d0d2e0ebbb},
+ {0xcc963fee10b7d1b3, 0x318df905079926a9},
+ {0xffbbcfe994e5c61f, 0xfdf17746497f7053},
+ {0x9fd561f1fd0f9bd3, 0xfeb6ea8bedefa634},
+ {0xc7caba6e7c5382c8, 0xfe64a52ee96b8fc1},
+ {0xf9bd690a1b68637b, 0x3dfdce7aa3c673b1},
+ {0x9c1661a651213e2d, 0x06bea10ca65c084f},
+ {0xc31bfa0fe5698db8, 0x486e494fcff30a63},
+ {0xf3e2f893dec3f126, 0x5a89dba3c3efccfb},
+ {0x986ddb5c6b3a76b7, 0xf89629465a75e01d},
+ {0xbe89523386091465, 0xf6bbb397f1135824},
+ {0xee2ba6c0678b597f, 0x746aa07ded582e2d},
+ {0x94db483840b717ef, 0xa8c2a44eb4571cdd},
+ {0xba121a4650e4ddeb, 0x92f34d62616ce414},
+ {0xe896a0d7e51e1566, 0x77b020baf9c81d18},
+ {0x915e2486ef32cd60, 0x0ace1474dc1d122f},
+ {0xb5b5ada8aaff80b8, 0x0d819992132456bb},
+ {0xe3231912d5bf60e6, 0x10e1fff697ed6c6a},
+ {0x8df5efabc5979c8f, 0xca8d3ffa1ef463c2},
+ {0xb1736b96b6fd83b3, 0xbd308ff8a6b17cb3},
+ {0xddd0467c64bce4a0, 0xac7cb3f6d05ddbdf},
+ {0x8aa22c0dbef60ee4, 0x6bcdf07a423aa96c},
+ {0xad4ab7112eb3929d, 0x86c16c98d2c953c7},
+ {0xd89d64d57a607744, 0xe871c7bf077ba8b8},
+ {0x87625f056c7c4a8b, 0x11471cd764ad4973},
+ {0xa93af6c6c79b5d2d, 0xd598e40d3dd89bd0},
+ {0xd389b47879823479, 0x4aff1d108d4ec2c4},
+ {0x843610cb4bf160cb, 0xcedf722a585139bb},
+ {0xa54394fe1eedb8fe, 0xc2974eb4ee658829},
+ {0xce947a3da6a9273e, 0x733d226229feea33},
+ {0x811ccc668829b887, 0x0806357d5a3f5260},
+ {0xa163ff802a3426a8, 0xca07c2dcb0cf26f8},
+ {0xc9bcff6034c13052, 0xfc89b393dd02f0b6},
+ {0xfc2c3f3841f17c67, 0xbbac2078d443ace3},
+ {0x9d9ba7832936edc0, 0xd54b944b84aa4c0e},
+ {0xc5029163f384a931, 0x0a9e795e65d4df12},
+ {0xf64335bcf065d37d, 0x4d4617b5ff4a16d6},
+ {0x99ea0196163fa42e, 0x504bced1bf8e4e46},
+ {0xc06481fb9bcf8d39, 0xe45ec2862f71e1d7},
+ {0xf07da27a82c37088, 0x5d767327bb4e5a4d},
+ {0x964e858c91ba2655, 0x3a6a07f8d510f870},
+ {0xbbe226efb628afea, 0x890489f70a55368c},
+ {0xeadab0aba3b2dbe5, 0x2b45ac74ccea842f},
+ {0x92c8ae6b464fc96f, 0x3b0b8bc90012929e},
+ {0xb77ada0617e3bbcb, 0x09ce6ebb40173745},
+ {0xe55990879ddcaabd, 0xcc420a6a101d0516},
+ {0x8f57fa54c2a9eab6, 0x9fa946824a12232e},
+ {0xb32df8e9f3546564, 0x47939822dc96abfa},
+ {0xdff9772470297ebd, 0x59787e2b93bc56f8},
+ {0x8bfbea76c619ef36, 0x57eb4edb3c55b65b},
+ {0xaefae51477a06b03, 0xede622920b6b23f2},
+ {0xdab99e59958885c4, 0xe95fab368e45ecee},
+ {0x88b402f7fd75539b, 0x11dbcb0218ebb415},
+ {0xaae103b5fcd2a881, 0xd652bdc29f26a11a},
+ {0xd59944a37c0752a2, 0x4be76d3346f04960},
+ {0x857fcae62d8493a5, 0x6f70a4400c562ddc},
+ {0xa6dfbd9fb8e5b88e, 0xcb4ccd500f6bb953},
+ {0xd097ad07a71f26b2, 0x7e2000a41346a7a8},
+ {0x825ecc24c873782f, 0x8ed400668c0c28c9},
+ {0xa2f67f2dfa90563b, 0x728900802f0f32fb},
+ {0xcbb41ef979346bca, 0x4f2b40a03ad2ffba},
+ {0xfea126b7d78186bc, 0xe2f610c84987bfa9},
+ {0x9f24b832e6b0f436, 0x0dd9ca7d2df4d7ca},
+ {0xc6ede63fa05d3143, 0x91503d1c79720dbc},
+ {0xf8a95fcf88747d94, 0x75a44c6397ce912b},
+ {0x9b69dbe1b548ce7c, 0xc986afbe3ee11abb},
+ {0xc24452da229b021b, 0xfbe85badce996169},
+ {0xf2d56790ab41c2a2, 0xfae27299423fb9c4},
+ {0x97c560ba6b0919a5, 0xdccd879fc967d41b},
+ {0xbdb6b8e905cb600f, 0x5400e987bbc1c921},
+ {0xed246723473e3813, 0x290123e9aab23b69},
+ {0x9436c0760c86e30b, 0xf9a0b6720aaf6522},
+ {0xb94470938fa89bce, 0xf808e40e8d5b3e6a},
+ {0xe7958cb87392c2c2, 0xb60b1d1230b20e05},
+ {0x90bd77f3483bb9b9, 0xb1c6f22b5e6f48c3},
+ {0xb4ecd5f01a4aa828, 0x1e38aeb6360b1af4},
+ {0xe2280b6c20dd5232, 0x25c6da63c38de1b1},
+ {0x8d590723948a535f, 0x579c487e5a38ad0f},
+ {0xb0af48ec79ace837, 0x2d835a9df0c6d852},
+ {0xdcdb1b2798182244, 0xf8e431456cf88e66},
+ {0x8a08f0f8bf0f156b, 0x1b8e9ecb641b5900},
+ {0xac8b2d36eed2dac5, 0xe272467e3d222f40},
+ {0xd7adf884aa879177, 0x5b0ed81dcc6abb10},
+ {0x86ccbb52ea94baea, 0x98e947129fc2b4ea},
+ {0xa87fea27a539e9a5, 0x3f2398d747b36225},
+ {0xd29fe4b18e88640e, 0x8eec7f0d19a03aae},
+ {0x83a3eeeef9153e89, 0x1953cf68300424ad},
+ {0xa48ceaaab75a8e2b, 0x5fa8c3423c052dd8},
+ {0xcdb02555653131b6, 0x3792f412cb06794e},
+ {0x808e17555f3ebf11, 0xe2bbd88bbee40bd1},
+ {0xa0b19d2ab70e6ed6, 0x5b6aceaeae9d0ec5},
+ {0xc8de047564d20a8b, 0xf245825a5a445276},
+ {0xfb158592be068d2e, 0xeed6e2f0f0d56713},
+ {0x9ced737bb6c4183d, 0x55464dd69685606c},
+ {0xc428d05aa4751e4c, 0xaa97e14c3c26b887},
+ {0xf53304714d9265df, 0xd53dd99f4b3066a9},
+ {0x993fe2c6d07b7fab, 0xe546a8038efe402a},
+ {0xbf8fdb78849a5f96, 0xde98520472bdd034},
+ {0xef73d256a5c0f77c, 0x963e66858f6d4441},
+ {0x95a8637627989aad, 0xdde7001379a44aa9},
+ {0xbb127c53b17ec159, 0x5560c018580d5d53},
+ {0xe9d71b689dde71af, 0xaab8f01e6e10b4a7},
+ {0x9226712162ab070d, 0xcab3961304ca70e9},
+ {0xb6b00d69bb55c8d1, 0x3d607b97c5fd0d23},
+ {0xe45c10c42a2b3b05, 0x8cb89a7db77c506b},
+ {0x8eb98a7a9a5b04e3, 0x77f3608e92adb243},
+ {0xb267ed1940f1c61c, 0x55f038b237591ed4},
+ {0xdf01e85f912e37a3, 0x6b6c46dec52f6689},
+ {0x8b61313bbabce2c6, 0x2323ac4b3b3da016},
+ {0xae397d8aa96c1b77, 0xabec975e0a0d081b},
+ {0xd9c7dced53c72255, 0x96e7bd358c904a22},
+ {0x881cea14545c7575, 0x7e50d64177da2e55},
+ {0xaa242499697392d2, 0xdde50bd1d5d0b9ea},
+ {0xd4ad2dbfc3d07787, 0x955e4ec64b44e865},
+ {0x84ec3c97da624ab4, 0xbd5af13bef0b113f},
+ {0xa6274bbdd0fadd61, 0xecb1ad8aeacdd58f},
+ {0xcfb11ead453994ba, 0x67de18eda5814af3},
+ {0x81ceb32c4b43fcf4, 0x80eacf948770ced8},
+ {0xa2425ff75e14fc31, 0xa1258379a94d028e},
+ {0xcad2f7f5359a3b3e, 0x096ee45813a04331},
+ {0xfd87b5f28300ca0d, 0x8bca9d6e188853fd},
+ {0x9e74d1b791e07e48, 0x775ea264cf55347e},
+ {0xc612062576589dda, 0x95364afe032a819e},
+ {0xf79687aed3eec551, 0x3a83ddbd83f52205},
+ {0x9abe14cd44753b52, 0xc4926a9672793543},
+ {0xc16d9a0095928a27, 0x75b7053c0f178294},
+ {0xf1c90080baf72cb1, 0x5324c68b12dd6339},
+ {0x971da05074da7bee, 0xd3f6fc16ebca5e04},
+ {0xbce5086492111aea, 0x88f4bb1ca6bcf585},
+ {0xec1e4a7db69561a5, 0x2b31e9e3d06c32e6},
+ {0x9392ee8e921d5d07, 0x3aff322e62439fd0},
+ {0xb877aa3236a4b449, 0x09befeb9fad487c3},
+ {0xe69594bec44de15b, 0x4c2ebe687989a9b4},
+ {0x901d7cf73ab0acd9, 0x0f9d37014bf60a11},
+ {0xb424dc35095cd80f, 0x538484c19ef38c95},
+ {0xe12e13424bb40e13, 0x2865a5f206b06fba},
+ {0x8cbccc096f5088cb, 0xf93f87b7442e45d4},
+ {0xafebff0bcb24aafe, 0xf78f69a51539d749},
+ {0xdbe6fecebdedd5be, 0xb573440e5a884d1c},
+ {0x89705f4136b4a597, 0x31680a88f8953031},
+ {0xabcc77118461cefc, 0xfdc20d2b36ba7c3e},
+ {0xd6bf94d5e57a42bc, 0x3d32907604691b4d},
+ {0x8637bd05af6c69b5, 0xa63f9a49c2c1b110},
+ {0xa7c5ac471b478423, 0x0fcf80dc33721d54},
+ {0xd1b71758e219652b, 0xd3c36113404ea4a9},
+ {0x83126e978d4fdf3b, 0x645a1cac083126ea},
+ {0xa3d70a3d70a3d70a, 0x3d70a3d70a3d70a4},
+ {0xcccccccccccccccc, 0xcccccccccccccccd},
+ {0x8000000000000000, 0x0000000000000000},
+ {0xa000000000000000, 0x0000000000000000},
+ {0xc800000000000000, 0x0000000000000000},
+ {0xfa00000000000000, 0x0000000000000000},
+ {0x9c40000000000000, 0x0000000000000000},
+ {0xc350000000000000, 0x0000000000000000},
+ {0xf424000000000000, 0x0000000000000000},
+ {0x9896800000000000, 0x0000000000000000},
+ {0xbebc200000000000, 0x0000000000000000},
+ {0xee6b280000000000, 0x0000000000000000},
+ {0x9502f90000000000, 0x0000000000000000},
+ {0xba43b74000000000, 0x0000000000000000},
+ {0xe8d4a51000000000, 0x0000000000000000},
+ {0x9184e72a00000000, 0x0000000000000000},
+ {0xb5e620f480000000, 0x0000000000000000},
+ {0xe35fa931a0000000, 0x0000000000000000},
+ {0x8e1bc9bf04000000, 0x0000000000000000},
+ {0xb1a2bc2ec5000000, 0x0000000000000000},
+ {0xde0b6b3a76400000, 0x0000000000000000},
+ {0x8ac7230489e80000, 0x0000000000000000},
+ {0xad78ebc5ac620000, 0x0000000000000000},
+ {0xd8d726b7177a8000, 0x0000000000000000},
+ {0x878678326eac9000, 0x0000000000000000},
+ {0xa968163f0a57b400, 0x0000000000000000},
+ {0xd3c21bcecceda100, 0x0000000000000000},
+ {0x84595161401484a0, 0x0000000000000000},
+ {0xa56fa5b99019a5c8, 0x0000000000000000},
+ {0xcecb8f27f4200f3a, 0x0000000000000000},
+ {0x813f3978f8940984, 0x4000000000000000},
+ {0xa18f07d736b90be5, 0x5000000000000000},
+ {0xc9f2c9cd04674ede, 0xa400000000000000},
+ {0xfc6f7c4045812296, 0x4d00000000000000},
+ {0x9dc5ada82b70b59d, 0xf020000000000000},
+ {0xc5371912364ce305, 0x6c28000000000000},
+ {0xf684df56c3e01bc6, 0xc732000000000000},
+ {0x9a130b963a6c115c, 0x3c7f400000000000},
+ {0xc097ce7bc90715b3, 0x4b9f100000000000},
+ {0xf0bdc21abb48db20, 0x1e86d40000000000},
+ {0x96769950b50d88f4, 0x1314448000000000},
+ {0xbc143fa4e250eb31, 0x17d955a000000000},
+ {0xeb194f8e1ae525fd, 0x5dcfab0800000000},
+ {0x92efd1b8d0cf37be, 0x5aa1cae500000000},
+ {0xb7abc627050305ad, 0xf14a3d9e40000000},
+ {0xe596b7b0c643c719, 0x6d9ccd05d0000000},
+ {0x8f7e32ce7bea5c6f, 0xe4820023a2000000},
+ {0xb35dbf821ae4f38b, 0xdda2802c8a800000},
+ {0xe0352f62a19e306e, 0xd50b2037ad200000},
+ {0x8c213d9da502de45, 0x4526f422cc340000},
+ {0xaf298d050e4395d6, 0x9670b12b7f410000},
+ {0xdaf3f04651d47b4c, 0x3c0cdd765f114000},
+ {0x88d8762bf324cd0f, 0xa5880a69fb6ac800},
+ {0xab0e93b6efee0053, 0x8eea0d047a457a00},
+ {0xd5d238a4abe98068, 0x72a4904598d6d880},
+ {0x85a36366eb71f041, 0x47a6da2b7f864750},
+ {0xa70c3c40a64e6c51, 0x999090b65f67d924},
+ {0xd0cf4b50cfe20765, 0xfff4b4e3f741cf6d},
+ {0x82818f1281ed449f, 0xbff8f10e7a8921a5},
+ {0xa321f2d7226895c7, 0xaff72d52192b6a0e},
+ {0xcbea6f8ceb02bb39, 0x9bf4f8a69f764491},
+ {0xfee50b7025c36a08, 0x02f236d04753d5b5},
+ {0x9f4f2726179a2245, 0x01d762422c946591},
+ {0xc722f0ef9d80aad6, 0x424d3ad2b7b97ef6},
+ {0xf8ebad2b84e0d58b, 0xd2e0898765a7deb3},
+ {0x9b934c3b330c8577, 0x63cc55f49f88eb30},
+ {0xc2781f49ffcfa6d5, 0x3cbf6b71c76b25fc},
+ {0xf316271c7fc3908a, 0x8bef464e3945ef7b},
+ {0x97edd871cfda3a56, 0x97758bf0e3cbb5ad},
+ {0xbde94e8e43d0c8ec, 0x3d52eeed1cbea318},
+ {0xed63a231d4c4fb27, 0x4ca7aaa863ee4bde},
+ {0x945e455f24fb1cf8, 0x8fe8caa93e74ef6b},
+ {0xb975d6b6ee39e436, 0xb3e2fd538e122b45},
+ {0xe7d34c64a9c85d44, 0x60dbbca87196b617},
+ {0x90e40fbeea1d3a4a, 0xbc8955e946fe31ce},
+ {0xb51d13aea4a488dd, 0x6babab6398bdbe42},
+ {0xe264589a4dcdab14, 0xc696963c7eed2dd2},
+ {0x8d7eb76070a08aec, 0xfc1e1de5cf543ca3},
+ {0xb0de65388cc8ada8, 0x3b25a55f43294bcc},
+ {0xdd15fe86affad912, 0x49ef0eb713f39ebf},
+ {0x8a2dbf142dfcc7ab, 0x6e3569326c784338},
+ {0xacb92ed9397bf996, 0x49c2c37f07965405},
+ {0xd7e77a8f87daf7fb, 0xdc33745ec97be907},
+ {0x86f0ac99b4e8dafd, 0x69a028bb3ded71a4},
+ {0xa8acd7c0222311bc, 0xc40832ea0d68ce0d},
+ {0xd2d80db02aabd62b, 0xf50a3fa490c30191},
+ {0x83c7088e1aab65db, 0x792667c6da79e0fb},
+ {0xa4b8cab1a1563f52, 0x577001b891185939},
+ {0xcde6fd5e09abcf26, 0xed4c0226b55e6f87},
+ {0x80b05e5ac60b6178, 0x544f8158315b05b5},
+ {0xa0dc75f1778e39d6, 0x696361ae3db1c722},
+ {0xc913936dd571c84c, 0x03bc3a19cd1e38ea},
+ {0xfb5878494ace3a5f, 0x04ab48a04065c724},
+ {0x9d174b2dcec0e47b, 0x62eb0d64283f9c77},
+ {0xc45d1df942711d9a, 0x3ba5d0bd324f8395},
+ {0xf5746577930d6500, 0xca8f44ec7ee3647a},
+ {0x9968bf6abbe85f20, 0x7e998b13cf4e1ecc},
+ {0xbfc2ef456ae276e8, 0x9e3fedd8c321a67f},
+ {0xefb3ab16c59b14a2, 0xc5cfe94ef3ea101f},
+ {0x95d04aee3b80ece5, 0xbba1f1d158724a13},
+ {0xbb445da9ca61281f, 0x2a8a6e45ae8edc98},
+ {0xea1575143cf97226, 0xf52d09d71a3293be},
+ {0x924d692ca61be758, 0x593c2626705f9c57},
+ {0xb6e0c377cfa2e12e, 0x6f8b2fb00c77836d},
+ {0xe498f455c38b997a, 0x0b6dfb9c0f956448},
+ {0x8edf98b59a373fec, 0x4724bd4189bd5ead},
+ {0xb2977ee300c50fe7, 0x58edec91ec2cb658},
+ {0xdf3d5e9bc0f653e1, 0x2f2967b66737e3ee},
+ {0x8b865b215899f46c, 0xbd79e0d20082ee75},
+ {0xae67f1e9aec07187, 0xecd8590680a3aa12},
+ {0xda01ee641a708de9, 0xe80e6f4820cc9496},
+ {0x884134fe908658b2, 0x3109058d147fdcde},
+ {0xaa51823e34a7eede, 0xbd4b46f0599fd416},
+ {0xd4e5e2cdc1d1ea96, 0x6c9e18ac7007c91b},
+ {0x850fadc09923329e, 0x03e2cf6bc604ddb1},
+ {0xa6539930bf6bff45, 0x84db8346b786151d},
+ {0xcfe87f7cef46ff16, 0xe612641865679a64},
+ {0x81f14fae158c5f6e, 0x4fcb7e8f3f60c07f},
+ {0xa26da3999aef7749, 0xe3be5e330f38f09e},
+ {0xcb090c8001ab551c, 0x5cadf5bfd3072cc6},
+ {0xfdcb4fa002162a63, 0x73d9732fc7c8f7f7},
+ {0x9e9f11c4014dda7e, 0x2867e7fddcdd9afb},
+ {0xc646d63501a1511d, 0xb281e1fd541501b9},
+ {0xf7d88bc24209a565, 0x1f225a7ca91a4227},
+ {0x9ae757596946075f, 0x3375788de9b06959},
+ {0xc1a12d2fc3978937, 0x0052d6b1641c83af},
+ {0xf209787bb47d6b84, 0xc0678c5dbd23a49b},
+ {0x9745eb4d50ce6332, 0xf840b7ba963646e1},
+ {0xbd176620a501fbff, 0xb650e5a93bc3d899},
+ {0xec5d3fa8ce427aff, 0xa3e51f138ab4cebf},
+ {0x93ba47c980e98cdf, 0xc66f336c36b10138},
+ {0xb8a8d9bbe123f017, 0xb80b0047445d4185},
+ {0xe6d3102ad96cec1d, 0xa60dc059157491e6},
+ {0x9043ea1ac7e41392, 0x87c89837ad68db30},
+ {0xb454e4a179dd1877, 0x29babe4598c311fc},
+ {0xe16a1dc9d8545e94, 0xf4296dd6fef3d67b},
+ {0x8ce2529e2734bb1d, 0x1899e4a65f58660d},
+ {0xb01ae745b101e9e4, 0x5ec05dcff72e7f90},
+ {0xdc21a1171d42645d, 0x76707543f4fa1f74},
+ {0x899504ae72497eba, 0x6a06494a791c53a9},
+ {0xabfa45da0edbde69, 0x0487db9d17636893},
+ {0xd6f8d7509292d603, 0x45a9d2845d3c42b7},
+ {0x865b86925b9bc5c2, 0x0b8a2392ba45a9b3},
+ {0xa7f26836f282b732, 0x8e6cac7768d7141f},
+ {0xd1ef0244af2364ff, 0x3207d795430cd927},
+ {0x8335616aed761f1f, 0x7f44e6bd49e807b9},
+ {0xa402b9c5a8d3a6e7, 0x5f16206c9c6209a7},
+ {0xcd036837130890a1, 0x36dba887c37a8c10},
+ {0x802221226be55a64, 0xc2494954da2c978a},
+ {0xa02aa96b06deb0fd, 0xf2db9baa10b7bd6d},
+ {0xc83553c5c8965d3d, 0x6f92829494e5acc8},
+ {0xfa42a8b73abbf48c, 0xcb772339ba1f17fa},
+ {0x9c69a97284b578d7, 0xff2a760414536efc},
+ {0xc38413cf25e2d70d, 0xfef5138519684abb},
+ {0xf46518c2ef5b8cd1, 0x7eb258665fc25d6a},
+ {0x98bf2f79d5993802, 0xef2f773ffbd97a62},
+ {0xbeeefb584aff8603, 0xaafb550ffacfd8fb},
+ {0xeeaaba2e5dbf6784, 0x95ba2a53f983cf39},
+ {0x952ab45cfa97a0b2, 0xdd945a747bf26184},
+ {0xba756174393d88df, 0x94f971119aeef9e5},
+ {0xe912b9d1478ceb17, 0x7a37cd5601aab85e},
+ {0x91abb422ccb812ee, 0xac62e055c10ab33b},
+ {0xb616a12b7fe617aa, 0x577b986b314d600a},
+ {0xe39c49765fdf9d94, 0xed5a7e85fda0b80c},
+ {0x8e41ade9fbebc27d, 0x14588f13be847308},
+ {0xb1d219647ae6b31c, 0x596eb2d8ae258fc9},
+ {0xde469fbd99a05fe3, 0x6fca5f8ed9aef3bc},
+ {0x8aec23d680043bee, 0x25de7bb9480d5855},
+ {0xada72ccc20054ae9, 0xaf561aa79a10ae6b},
+ {0xd910f7ff28069da4, 0x1b2ba1518094da05},
+ {0x87aa9aff79042286, 0x90fb44d2f05d0843},
+ {0xa99541bf57452b28, 0x353a1607ac744a54},
+ {0xd3fa922f2d1675f2, 0x42889b8997915ce9},
+ {0x847c9b5d7c2e09b7, 0x69956135febada12},
+ {0xa59bc234db398c25, 0x43fab9837e699096},
+ {0xcf02b2c21207ef2e, 0x94f967e45e03f4bc},
+ {0x8161afb94b44f57d, 0x1d1be0eebac278f6},
+ {0xa1ba1ba79e1632dc, 0x6462d92a69731733},
+ {0xca28a291859bbf93, 0x7d7b8f7503cfdcff},
+ {0xfcb2cb35e702af78, 0x5cda735244c3d43f},
+ {0x9defbf01b061adab, 0x3a0888136afa64a8},
+ {0xc56baec21c7a1916, 0x088aaa1845b8fdd1},
+ {0xf6c69a72a3989f5b, 0x8aad549e57273d46},
+ {0x9a3c2087a63f6399, 0x36ac54e2f678864c},
+ {0xc0cb28a98fcf3c7f, 0x84576a1bb416a7de},
+ {0xf0fdf2d3f3c30b9f, 0x656d44a2a11c51d6},
+ {0x969eb7c47859e743, 0x9f644ae5a4b1b326},
+ {0xbc4665b596706114, 0x873d5d9f0dde1fef},
+ {0xeb57ff22fc0c7959, 0xa90cb506d155a7eb},
+ {0x9316ff75dd87cbd8, 0x09a7f12442d588f3},
+ {0xb7dcbf5354e9bece, 0x0c11ed6d538aeb30},
+ {0xe5d3ef282a242e81, 0x8f1668c8a86da5fb},
+ {0x8fa475791a569d10, 0xf96e017d694487bd},
+ {0xb38d92d760ec4455, 0x37c981dcc395a9ad},
+ {0xe070f78d3927556a, 0x85bbe253f47b1418},
+ {0x8c469ab843b89562, 0x93956d7478ccec8f},
+ {0xaf58416654a6babb, 0x387ac8d1970027b3},
+ {0xdb2e51bfe9d0696a, 0x06997b05fcc0319f},
+ {0x88fcf317f22241e2, 0x441fece3bdf81f04},
+ {0xab3c2fddeeaad25a, 0xd527e81cad7626c4},
+ {0xd60b3bd56a5586f1, 0x8a71e223d8d3b075},
+ {0x85c7056562757456, 0xf6872d5667844e4a},
+ {0xa738c6bebb12d16c, 0xb428f8ac016561dc},
+ {0xd106f86e69d785c7, 0xe13336d701beba53},
+ {0x82a45b450226b39c, 0xecc0024661173474},
+ {0xa34d721642b06084, 0x27f002d7f95d0191},
+ {0xcc20ce9bd35c78a5, 0x31ec038df7b441f5},
+ {0xff290242c83396ce, 0x7e67047175a15272},
+ {0x9f79a169bd203e41, 0x0f0062c6e984d387},
+ {0xc75809c42c684dd1, 0x52c07b78a3e60869},
+ {0xf92e0c3537826145, 0xa7709a56ccdf8a83},
+ {0x9bbcc7a142b17ccb, 0x88a66076400bb692},
+ {0xc2abf989935ddbfe, 0x6acff893d00ea436},
+ {0xf356f7ebf83552fe, 0x0583f6b8c4124d44},
+ {0x98165af37b2153de, 0xc3727a337a8b704b},
+ {0xbe1bf1b059e9a8d6, 0x744f18c0592e4c5d},
+ {0xeda2ee1c7064130c, 0x1162def06f79df74},
+ {0x9485d4d1c63e8be7, 0x8addcb5645ac2ba9},
+ {0xb9a74a0637ce2ee1, 0x6d953e2bd7173693},
+ {0xe8111c87c5c1ba99, 0xc8fa8db6ccdd0438},
+ {0x910ab1d4db9914a0, 0x1d9c9892400a22a3},
+ {0xb54d5e4a127f59c8, 0x2503beb6d00cab4c},
+ {0xe2a0b5dc971f303a, 0x2e44ae64840fd61e},
+ {0x8da471a9de737e24, 0x5ceaecfed289e5d3},
+ {0xb10d8e1456105dad, 0x7425a83e872c5f48},
+ {0xdd50f1996b947518, 0xd12f124e28f7771a},
+ {0x8a5296ffe33cc92f, 0x82bd6b70d99aaa70},
+ {0xace73cbfdc0bfb7b, 0x636cc64d1001550c},
+ {0xd8210befd30efa5a, 0x3c47f7e05401aa4f},
+ {0x8714a775e3e95c78, 0x65acfaec34810a72},
+ {0xa8d9d1535ce3b396, 0x7f1839a741a14d0e},
+ {0xd31045a8341ca07c, 0x1ede48111209a051},
+ {0x83ea2b892091e44d, 0x934aed0aab460433},
+ {0xa4e4b66b68b65d60, 0xf81da84d56178540},
+ {0xce1de40642e3f4b9, 0x36251260ab9d668f},
+ {0x80d2ae83e9ce78f3, 0xc1d72b7c6b42601a},
+ {0xa1075a24e4421730, 0xb24cf65b8612f820},
+ {0xc94930ae1d529cfc, 0xdee033f26797b628},
+ {0xfb9b7cd9a4a7443c, 0x169840ef017da3b2},
+ {0x9d412e0806e88aa5, 0x8e1f289560ee864f},
+ {0xc491798a08a2ad4e, 0xf1a6f2bab92a27e3},
+ {0xf5b5d7ec8acb58a2, 0xae10af696774b1dc},
+ {0x9991a6f3d6bf1765, 0xacca6da1e0a8ef2a},
+ {0xbff610b0cc6edd3f, 0x17fd090a58d32af4},
+ {0xeff394dcff8a948e, 0xddfc4b4cef07f5b1},
+ {0x95f83d0a1fb69cd9, 0x4abdaf101564f98f},
+ {0xbb764c4ca7a4440f, 0x9d6d1ad41abe37f2},
+ {0xea53df5fd18d5513, 0x84c86189216dc5ee},
+ {0x92746b9be2f8552c, 0x32fd3cf5b4e49bb5},
+ {0xb7118682dbb66a77, 0x3fbc8c33221dc2a2},
+ {0xe4d5e82392a40515, 0x0fabaf3feaa5334b},
+ {0x8f05b1163ba6832d, 0x29cb4d87f2a7400f},
+ {0xb2c71d5bca9023f8, 0x743e20e9ef511013},
+ {0xdf78e4b2bd342cf6, 0x914da9246b255417},
+ {0x8bab8eefb6409c1a, 0x1ad089b6c2f7548f},
+ {0xae9672aba3d0c320, 0xa184ac2473b529b2},
+ {0xda3c0f568cc4f3e8, 0xc9e5d72d90a2741f},
+ {0x8865899617fb1871, 0x7e2fa67c7a658893},
+ {0xaa7eebfb9df9de8d, 0xddbb901b98feeab8},
+ {0xd51ea6fa85785631, 0x552a74227f3ea566},
+ {0x8533285c936b35de, 0xd53a88958f872760},
+ {0xa67ff273b8460356, 0x8a892abaf368f138},
+ {0xd01fef10a657842c, 0x2d2b7569b0432d86},
+ {0x8213f56a67f6b29b, 0x9c3b29620e29fc74},
+ {0xa298f2c501f45f42, 0x8349f3ba91b47b90},
+ {0xcb3f2f7642717713, 0x241c70a936219a74},
+ {0xfe0efb53d30dd4d7, 0xed238cd383aa0111},
+ {0x9ec95d1463e8a506, 0xf4363804324a40ab},
+ {0xc67bb4597ce2ce48, 0xb143c6053edcd0d6},
+ {0xf81aa16fdc1b81da, 0xdd94b7868e94050b},
+ {0x9b10a4e5e9913128, 0xca7cf2b4191c8327},
+ {0xc1d4ce1f63f57d72, 0xfd1c2f611f63a3f1},
+ {0xf24a01a73cf2dccf, 0xbc633b39673c8ced},
+ {0x976e41088617ca01, 0xd5be0503e085d814},
+ {0xbd49d14aa79dbc82, 0x4b2d8644d8a74e19},
+ {0xec9c459d51852ba2, 0xddf8e7d60ed1219f},
+ {0x93e1ab8252f33b45, 0xcabb90e5c942b504},
+ {0xb8da1662e7b00a17, 0x3d6a751f3b936244},
+ {0xe7109bfba19c0c9d, 0x0cc512670a783ad5},
+ {0x906a617d450187e2, 0x27fb2b80668b24c6},
+ {0xb484f9dc9641e9da, 0xb1f9f660802dedf7},
+ {0xe1a63853bbd26451, 0x5e7873f8a0396974},
+ {0x8d07e33455637eb2, 0xdb0b487b6423e1e9},
+ {0xb049dc016abc5e5f, 0x91ce1a9a3d2cda63},
+ {0xdc5c5301c56b75f7, 0x7641a140cc7810fc},
+ {0x89b9b3e11b6329ba, 0xa9e904c87fcb0a9e},
+ {0xac2820d9623bf429, 0x546345fa9fbdcd45},
+ {0xd732290fbacaf133, 0xa97c177947ad4096},
+ {0x867f59a9d4bed6c0, 0x49ed8eabcccc485e},
+ {0xa81f301449ee8c70, 0x5c68f256bfff5a75},
+ {0xd226fc195c6a2f8c, 0x73832eec6fff3112},
+ {0x83585d8fd9c25db7, 0xc831fd53c5ff7eac},
+ {0xa42e74f3d032f525, 0xba3e7ca8b77f5e56},
+ {0xcd3a1230c43fb26f, 0x28ce1bd2e55f35ec},
+ {0x80444b5e7aa7cf85, 0x7980d163cf5b81b4},
+ {0xa0555e361951c366, 0xd7e105bcc3326220},
+ {0xc86ab5c39fa63440, 0x8dd9472bf3fefaa8},
+ {0xfa856334878fc150, 0xb14f98f6f0feb952},
+ {0x9c935e00d4b9d8d2, 0x6ed1bf9a569f33d4},
+ {0xc3b8358109e84f07, 0x0a862f80ec4700c9},
+ {0xf4a642e14c6262c8, 0xcd27bb612758c0fb},
+ {0x98e7e9cccfbd7dbd, 0x8038d51cb897789d},
+ {0xbf21e44003acdd2c, 0xe0470a63e6bd56c4},
+ {0xeeea5d5004981478, 0x1858ccfce06cac75},
+ {0x95527a5202df0ccb, 0x0f37801e0c43ebc9},
+ {0xbaa718e68396cffd, 0xd30560258f54e6bb},
+ {0xe950df20247c83fd, 0x47c6b82ef32a206a},
+ {0x91d28b7416cdd27e, 0x4cdc331d57fa5442},
+ {0xb6472e511c81471d, 0xe0133fe4adf8e953},
+ {0xe3d8f9e563a198e5, 0x58180fddd97723a7},
+ {0x8e679c2f5e44ff8f, 0x570f09eaa7ea7649},
+ {0xb201833b35d63f73, 0x2cd2cc6551e513db},
+ {0xde81e40a034bcf4f, 0xf8077f7ea65e58d2},
+ {0x8b112e86420f6191, 0xfb04afaf27faf783},
+ {0xadd57a27d29339f6, 0x79c5db9af1f9b564},
+ {0xd94ad8b1c7380874, 0x18375281ae7822bd},
+ {0x87cec76f1c830548, 0x8f2293910d0b15b6},
+ {0xa9c2794ae3a3c69a, 0xb2eb3875504ddb23},
+ {0xd433179d9c8cb841, 0x5fa60692a46151ec},
+ {0x849feec281d7f328, 0xdbc7c41ba6bcd334},
+ {0xa5c7ea73224deff3, 0x12b9b522906c0801},
+ {0xcf39e50feae16bef, 0xd768226b34870a01},
+ {0x81842f29f2cce375, 0xe6a1158300d46641},
+ {0xa1e53af46f801c53, 0x60495ae3c1097fd1},
+ {0xca5e89b18b602368, 0x385bb19cb14bdfc5},
+ {0xfcf62c1dee382c42, 0x46729e03dd9ed7b6},
+ {0x9e19db92b4e31ba9, 0x6c07a2c26a8346d2},
+ {0xc5a05277621be293, 0xc7098b7305241886},
+ {0xf70867153aa2db38, 0xb8cbee4fc66d1ea8},
+ {0x9a65406d44a5c903, 0x737f74f1dc043329},
+ {0xc0fe908895cf3b44, 0x505f522e53053ff3},
+ {0xf13e34aabb430a15, 0x647726b9e7c68ff0},
+ {0x96c6e0eab509e64d, 0x5eca783430dc19f6},
+ {0xbc789925624c5fe0, 0xb67d16413d132073},
+ {0xeb96bf6ebadf77d8, 0xe41c5bd18c57e890},
+ {0x933e37a534cbaae7, 0x8e91b962f7b6f15a},
+ {0xb80dc58e81fe95a1, 0x723627bbb5a4adb1},
+ {0xe61136f2227e3b09, 0xcec3b1aaa30dd91d},
+ {0x8fcac257558ee4e6, 0x213a4f0aa5e8a7b2},
+ {0xb3bd72ed2af29e1f, 0xa988e2cd4f62d19e},
+ {0xe0accfa875af45a7, 0x93eb1b80a33b8606},
+ {0x8c6c01c9498d8b88, 0xbc72f130660533c4},
+ {0xaf87023b9bf0ee6a, 0xeb8fad7c7f8680b5},
+ {0xdb68c2ca82ed2a05, 0xa67398db9f6820e2},
+#else
+ {0xff77b1fcbebcdc4f, 0x25e8e89c13bb0f7b},
+ {0xce5d73ff402d98e3, 0xfb0a3d212dc81290},
+ {0xa6b34ad8c9dfc06f, 0xf42faa48c0ea481f},
+ {0x86a8d39ef77164bc, 0xae5dff9c02033198},
+ {0xd98ddaee19068c76, 0x3badd624dd9b0958},
+ {0xafbd2350644eeacf, 0xe5d1929ef90898fb},
+ {0x8df5efabc5979c8f, 0xca8d3ffa1ef463c2},
+ {0xe55990879ddcaabd, 0xcc420a6a101d0516},
+ {0xb94470938fa89bce, 0xf808e40e8d5b3e6a},
+ {0x95a8637627989aad, 0xdde7001379a44aa9},
+ {0xf1c90080baf72cb1, 0x5324c68b12dd6339},
+ {0xc350000000000000, 0x0000000000000000},
+ {0x9dc5ada82b70b59d, 0xf020000000000000},
+ {0xfee50b7025c36a08, 0x02f236d04753d5b5},
+ {0xcde6fd5e09abcf26, 0xed4c0226b55e6f87},
+ {0xa6539930bf6bff45, 0x84db8346b786151d},
+ {0x865b86925b9bc5c2, 0x0b8a2392ba45a9b3},
+ {0xd910f7ff28069da4, 0x1b2ba1518094da05},
+ {0xaf58416654a6babb, 0x387ac8d1970027b3},
+ {0x8da471a9de737e24, 0x5ceaecfed289e5d3},
+ {0xe4d5e82392a40515, 0x0fabaf3feaa5334b},
+ {0xb8da1662e7b00a17, 0x3d6a751f3b936244},
+ {0x95527a5202df0ccb, 0x0f37801e0c43ebc9},
+ {0xf13e34aabb430a15, 0x647726b9e7c68ff0}
+#endif
+ };
+
+#if FMT_USE_FULL_CACHE_DRAGONBOX
+ return pow10_significands[k - float_info<double>::min_k];
+#else
+ static constexpr const uint64_t powers_of_5_64[] = {
+ 0x0000000000000001, 0x0000000000000005, 0x0000000000000019,
+ 0x000000000000007d, 0x0000000000000271, 0x0000000000000c35,
+ 0x0000000000003d09, 0x000000000001312d, 0x000000000005f5e1,
+ 0x00000000001dcd65, 0x00000000009502f9, 0x0000000002e90edd,
+ 0x000000000e8d4a51, 0x0000000048c27395, 0x000000016bcc41e9,
+ 0x000000071afd498d, 0x0000002386f26fc1, 0x000000b1a2bc2ec5,
+ 0x000003782dace9d9, 0x00001158e460913d, 0x000056bc75e2d631,
+ 0x0001b1ae4d6e2ef5, 0x000878678326eac9, 0x002a5a058fc295ed,
+ 0x00d3c21bcecceda1, 0x0422ca8b0a00a425, 0x14adf4b7320334b9};
+
+ static const int compression_ratio = 27;
+
+ // Compute base index.
+ int cache_index = (k - float_info<double>::min_k) / compression_ratio;
+ int kb = cache_index * compression_ratio + float_info<double>::min_k;
+ int offset = k - kb;
+
+ // Get base cache.
+ uint128_fallback base_cache = pow10_significands[cache_index];
+ if (offset == 0) return base_cache;
+
+ // Compute the required amount of bit-shift.
+ int alpha = floor_log2_pow10(kb + offset) - floor_log2_pow10(kb) - offset;
+ FMT_ASSERT(alpha > 0 && alpha < 64, "shifting error detected");
+
+ // Try to recover the real cache.
+ uint64_t pow5 = powers_of_5_64[offset];
+ uint128_fallback recovered_cache = umul128(base_cache.high(), pow5);
+ uint128_fallback middle_low = umul128(base_cache.low(), pow5);
+
+ recovered_cache += middle_low.high();
+
+ uint64_t high_to_middle = recovered_cache.high() << (64 - alpha);
+ uint64_t middle_to_low = recovered_cache.low() << (64 - alpha);
+
+ recovered_cache =
+ uint128_fallback{(recovered_cache.low() >> alpha) | high_to_middle,
+ ((middle_low.low() >> alpha) | middle_to_low)};
+ FMT_ASSERT(recovered_cache.low() + 1 != 0, "");
+ return {recovered_cache.high(), recovered_cache.low() + 1};
+#endif
+ }
+
+ struct compute_mul_result {
+ carrier_uint result;
+ bool is_integer;
+ };
+ struct compute_mul_parity_result {
+ bool parity;
+ bool is_integer;
+ };
+
+ static auto compute_mul(carrier_uint u,
+ const cache_entry_type& cache) noexcept
+ -> compute_mul_result {
+ auto r = umul192_upper128(u, cache);
+ return {r.high(), r.low() == 0};
+ }
+
+ static auto compute_delta(cache_entry_type const& cache, int beta) noexcept
+ -> uint32_t {
+ return static_cast<uint32_t>(cache.high() >> (64 - 1 - beta));
+ }
+
+ static auto compute_mul_parity(carrier_uint two_f,
+ const cache_entry_type& cache,
+ int beta) noexcept
+ -> compute_mul_parity_result {
+ FMT_ASSERT(beta >= 1, "");
+ FMT_ASSERT(beta < 64, "");
+
+ auto r = umul192_lower128(two_f, cache);
+ return {((r.high() >> (64 - beta)) & 1) != 0,
+ ((r.high() << beta) | (r.low() >> (64 - beta))) == 0};
+ }
+
+ static auto compute_left_endpoint_for_shorter_interval_case(
+ const cache_entry_type& cache, int beta) noexcept -> carrier_uint {
+ return (cache.high() -
+ (cache.high() >> (num_significand_bits<double>() + 2))) >>
+ (64 - num_significand_bits<double>() - 1 - beta);
+ }
+
+ static auto compute_right_endpoint_for_shorter_interval_case(
+ const cache_entry_type& cache, int beta) noexcept -> carrier_uint {
+ return (cache.high() +
+ (cache.high() >> (num_significand_bits<double>() + 1))) >>
+ (64 - num_significand_bits<double>() - 1 - beta);
+ }
+
+ static auto compute_round_up_for_shorter_interval_case(
+ const cache_entry_type& cache, int beta) noexcept -> carrier_uint {
+ return ((cache.high() >> (64 - num_significand_bits<double>() - 2 - beta)) +
+ 1) /
+ 2;
+ }
+};
+
+FMT_FUNC auto get_cached_power(int k) noexcept -> uint128_fallback {
+ return cache_accessor<double>::get_cached_power(k);
+}
+
+// Various integer checks
+template <typename T>
+auto is_left_endpoint_integer_shorter_interval(int exponent) noexcept -> bool {
+ const int case_shorter_interval_left_endpoint_lower_threshold = 2;
+ const int case_shorter_interval_left_endpoint_upper_threshold = 3;
+ return exponent >= case_shorter_interval_left_endpoint_lower_threshold &&
+ exponent <= case_shorter_interval_left_endpoint_upper_threshold;
+}
+
+// Remove trailing zeros from n and return the number of zeros removed (float)
+FMT_INLINE int remove_trailing_zeros(uint32_t& n, int s = 0) noexcept {
+ FMT_ASSERT(n != 0, "");
+ // Modular inverse of 5 (mod 2^32): (mod_inv_5 * 5) mod 2^32 = 1.
+ constexpr uint32_t mod_inv_5 = 0xcccccccd;
+ constexpr uint32_t mod_inv_25 = 0xc28f5c29; // = mod_inv_5 * mod_inv_5
+
+ while (true) {
+ auto q = rotr(n * mod_inv_25, 2);
+ if (q > max_value<uint32_t>() / 100) break;
+ n = q;
+ s += 2;
+ }
+ auto q = rotr(n * mod_inv_5, 1);
+ if (q <= max_value<uint32_t>() / 10) {
+ n = q;
+ s |= 1;
+ }
+ return s;
+}
+
+// Removes trailing zeros and returns the number of zeros removed (double)
+FMT_INLINE int remove_trailing_zeros(uint64_t& n) noexcept {
+ FMT_ASSERT(n != 0, "");
+
+ // This magic number is ceil(2^90 / 10^8).
+ constexpr uint64_t magic_number = 12379400392853802749ull;
+ auto nm = umul128(n, magic_number);
+
+ // Is n is divisible by 10^8?
+ if ((nm.high() & ((1ull << (90 - 64)) - 1)) == 0 && nm.low() < magic_number) {
+ // If yes, work with the quotient...
+ auto n32 = static_cast<uint32_t>(nm.high() >> (90 - 64));
+ // ... and use the 32 bit variant of the function
+ int s = remove_trailing_zeros(n32, 8);
+ n = n32;
+ return s;
+ }
+
+ // If n is not divisible by 10^8, work with n itself.
+ constexpr uint64_t mod_inv_5 = 0xcccccccccccccccd;
+ constexpr uint64_t mod_inv_25 = 0x8f5c28f5c28f5c29; // mod_inv_5 * mod_inv_5
+
+ int s = 0;
+ while (true) {
+ auto q = rotr(n * mod_inv_25, 2);
+ if (q > max_value<uint64_t>() / 100) break;
+ n = q;
+ s += 2;
+ }
+ auto q = rotr(n * mod_inv_5, 1);
+ if (q <= max_value<uint64_t>() / 10) {
+ n = q;
+ s |= 1;
+ }
+
+ return s;
+}
+
+// The main algorithm for shorter interval case
+template <typename T>
+FMT_INLINE decimal_fp<T> shorter_interval_case(int exponent) noexcept {
+ decimal_fp<T> ret_value;
+ // Compute k and beta
+ const int minus_k = floor_log10_pow2_minus_log10_4_over_3(exponent);
+ const int beta = exponent + floor_log2_pow10(-minus_k);
+
+ // Compute xi and zi
+ using cache_entry_type = typename cache_accessor<T>::cache_entry_type;
+ const cache_entry_type cache = cache_accessor<T>::get_cached_power(-minus_k);
+
+ auto xi = cache_accessor<T>::compute_left_endpoint_for_shorter_interval_case(
+ cache, beta);
+ auto zi = cache_accessor<T>::compute_right_endpoint_for_shorter_interval_case(
+ cache, beta);
+
+ // If the left endpoint is not an integer, increase it
+ if (!is_left_endpoint_integer_shorter_interval<T>(exponent)) ++xi;
+
+ // Try bigger divisor
+ ret_value.significand = zi / 10;
+
+ // If succeed, remove trailing zeros if necessary and return
+ if (ret_value.significand * 10 >= xi) {
+ ret_value.exponent = minus_k + 1;
+ ret_value.exponent += remove_trailing_zeros(ret_value.significand);
+ return ret_value;
+ }
+
+ // Otherwise, compute the round-up of y
+ ret_value.significand =
+ cache_accessor<T>::compute_round_up_for_shorter_interval_case(cache,
+ beta);
+ ret_value.exponent = minus_k;
+
+ // When tie occurs, choose one of them according to the rule
+ if (exponent >= float_info<T>::shorter_interval_tie_lower_threshold &&
+ exponent <= float_info<T>::shorter_interval_tie_upper_threshold) {
+ ret_value.significand = ret_value.significand % 2 == 0
+ ? ret_value.significand
+ : ret_value.significand - 1;
+ } else if (ret_value.significand < xi) {
+ ++ret_value.significand;
+ }
+ return ret_value;
+}
+
+template <typename T> auto to_decimal(T x) noexcept -> decimal_fp<T> {
+ // Step 1: integer promotion & Schubfach multiplier calculation.
+
+ using carrier_uint = typename float_info<T>::carrier_uint;
+ using cache_entry_type = typename cache_accessor<T>::cache_entry_type;
+ auto br = bit_cast<carrier_uint>(x);
+
+ // Extract significand bits and exponent bits.
+ const carrier_uint significand_mask =
+ (static_cast<carrier_uint>(1) << num_significand_bits<T>()) - 1;
+ carrier_uint significand = (br & significand_mask);
+ int exponent =
+ static_cast<int>((br & exponent_mask<T>()) >> num_significand_bits<T>());
+
+ if (exponent != 0) { // Check if normal.
+ exponent -= exponent_bias<T>() + num_significand_bits<T>();
+
+ // Shorter interval case; proceed like Schubfach.
+ // In fact, when exponent == 1 and significand == 0, the interval is
+ // regular. However, it can be shown that the end-results are anyway same.
+ if (significand == 0) return shorter_interval_case<T>(exponent);
+
+ significand |= (static_cast<carrier_uint>(1) << num_significand_bits<T>());
+ } else {
+ // Subnormal case; the interval is always regular.
+ if (significand == 0) return {0, 0};
+ exponent =
+ std::numeric_limits<T>::min_exponent - num_significand_bits<T>() - 1;
+ }
+
+ const bool include_left_endpoint = (significand % 2 == 0);
+ const bool include_right_endpoint = include_left_endpoint;
+
+ // Compute k and beta.
+ const int minus_k = floor_log10_pow2(exponent) - float_info<T>::kappa;
+ const cache_entry_type cache = cache_accessor<T>::get_cached_power(-minus_k);
+ const int beta = exponent + floor_log2_pow10(-minus_k);
+
+ // Compute zi and deltai.
+ // 10^kappa <= deltai < 10^(kappa + 1)
+ const uint32_t deltai = cache_accessor<T>::compute_delta(cache, beta);
+ const carrier_uint two_fc = significand << 1;
+
+ // For the case of binary32, the result of integer check is not correct for
+ // 29711844 * 2^-82
+ // = 6.1442653300000000008655037797566933477355632930994033813476... * 10^-18
+ // and 29711844 * 2^-81
+ // = 1.2288530660000000001731007559513386695471126586198806762695... * 10^-17,
+ // and they are the unique counterexamples. However, since 29711844 is even,
+ // this does not cause any problem for the endpoints calculations; it can only
+ // cause a problem when we need to perform integer check for the center.
+ // Fortunately, with these inputs, that branch is never executed, so we are
+ // fine.
+ const typename cache_accessor<T>::compute_mul_result z_mul =
+ cache_accessor<T>::compute_mul((two_fc | 1) << beta, cache);
+
+ // Step 2: Try larger divisor; remove trailing zeros if necessary.
+
+ // Using an upper bound on zi, we might be able to optimize the division
+ // better than the compiler; we are computing zi / big_divisor here.
+ decimal_fp<T> ret_value;
+ ret_value.significand = divide_by_10_to_kappa_plus_1(z_mul.result);
+ uint32_t r = static_cast<uint32_t>(z_mul.result - float_info<T>::big_divisor *
+ ret_value.significand);
+
+ if (r < deltai) {
+ // Exclude the right endpoint if necessary.
+ if (r == 0 && (z_mul.is_integer & !include_right_endpoint)) {
+ --ret_value.significand;
+ r = float_info<T>::big_divisor;
+ goto small_divisor_case_label;
+ }
+ } else if (r > deltai) {
+ goto small_divisor_case_label;
+ } else {
+ // r == deltai; compare fractional parts.
+ const typename cache_accessor<T>::compute_mul_parity_result x_mul =
+ cache_accessor<T>::compute_mul_parity(two_fc - 1, cache, beta);
+
+ if (!(x_mul.parity | (x_mul.is_integer & include_left_endpoint)))
+ goto small_divisor_case_label;
+ }
+ ret_value.exponent = minus_k + float_info<T>::kappa + 1;
+
+ // We may need to remove trailing zeros.
+ ret_value.exponent += remove_trailing_zeros(ret_value.significand);
+ return ret_value;
+
+ // Step 3: Find the significand with the smaller divisor.
+
+small_divisor_case_label:
+ ret_value.significand *= 10;
+ ret_value.exponent = minus_k + float_info<T>::kappa;
+
+ uint32_t dist = r - (deltai / 2) + (float_info<T>::small_divisor / 2);
+ const bool approx_y_parity =
+ ((dist ^ (float_info<T>::small_divisor / 2)) & 1) != 0;
+
+ // Is dist divisible by 10^kappa?
+ const bool divisible_by_small_divisor =
+ check_divisibility_and_divide_by_pow10<float_info<T>::kappa>(dist);
+
+ // Add dist / 10^kappa to the significand.
+ ret_value.significand += dist;
+
+ if (!divisible_by_small_divisor) return ret_value;
+
+ // Check z^(f) >= epsilon^(f).
+ // We have either yi == zi - epsiloni or yi == (zi - epsiloni) - 1,
+ // where yi == zi - epsiloni if and only if z^(f) >= epsilon^(f).
+ // Since there are only 2 possibilities, we only need to care about the
+ // parity. Also, zi and r should have the same parity since the divisor
+ // is an even number.
+ const auto y_mul = cache_accessor<T>::compute_mul_parity(two_fc, cache, beta);
+
+ // If z^(f) >= epsilon^(f), we might have a tie when z^(f) == epsilon^(f),
+ // or equivalently, when y is an integer.
+ if (y_mul.parity != approx_y_parity)
+ --ret_value.significand;
+ else if (y_mul.is_integer & (ret_value.significand % 2 != 0))
+ --ret_value.significand;
+ return ret_value;
+}
+} // namespace dragonbox
+} // namespace detail
+
+template <> struct formatter<detail::bigint> {
+ FMT_CONSTEXPR auto parse(format_parse_context& ctx)
+ -> format_parse_context::iterator {
+ return ctx.begin();
+ }
+
+ auto format(const detail::bigint& n, format_context& ctx) const
+ -> format_context::iterator {
+ auto out = ctx.out();
+ bool first = true;
+ for (auto i = n.bigits_.size(); i > 0; --i) {
+ auto value = n.bigits_[i - 1u];
+ if (first) {
+ out = fmt::format_to(out, FMT_STRING("{:x}"), value);
+ first = false;
+ continue;
+ }
+ out = fmt::format_to(out, FMT_STRING("{:08x}"), value);
+ }
+ if (n.exp_ > 0)
+ out = fmt::format_to(out, FMT_STRING("p{}"),
+ n.exp_ * detail::bigint::bigit_bits);
+ return out;
+ }
+};
+
+FMT_FUNC detail::utf8_to_utf16::utf8_to_utf16(string_view s) {
+ for_each_codepoint(s, [this](uint32_t cp, string_view) {
+ if (cp == invalid_code_point) FMT_THROW(std::runtime_error("invalid utf8"));
+ if (cp <= 0xFFFF) {
+ buffer_.push_back(static_cast<wchar_t>(cp));
+ } else {
+ cp -= 0x10000;
+ buffer_.push_back(static_cast<wchar_t>(0xD800 + (cp >> 10)));
+ buffer_.push_back(static_cast<wchar_t>(0xDC00 + (cp & 0x3FF)));
+ }
+ return true;
+ });
+ buffer_.push_back(0);
+}
+
+FMT_FUNC void format_system_error(detail::buffer<char>& out, int error_code,
+ const char* message) noexcept {
+ FMT_TRY {
+ auto ec = std::error_code(error_code, std::generic_category());
+ write(std::back_inserter(out), std::system_error(ec, message).what());
+ return;
+ }
+ FMT_CATCH(...) {}
+ format_error_code(out, error_code, message);
+}
+
+FMT_FUNC void report_system_error(int error_code,
+ const char* message) noexcept {
+ report_error(format_system_error, error_code, message);
+}
+
+FMT_FUNC auto vformat(string_view fmt, format_args args) -> std::string {
+ // Don't optimize the "{}" case to keep the binary size small and because it
+ // can be better optimized in fmt::format anyway.
+ auto buffer = memory_buffer();
+ detail::vformat_to(buffer, fmt, args);
+ return to_string(buffer);
+}
+
+namespace detail {
+#if !defined(_WIN32) || defined(FMT_WINDOWS_NO_WCHAR)
+FMT_FUNC auto write_console(int, string_view) -> bool { return false; }
+FMT_FUNC auto write_console(std::FILE*, string_view) -> bool { return false; }
+#else
+using dword = conditional_t<sizeof(long) == 4, unsigned long, unsigned>;
+extern "C" __declspec(dllimport) int __stdcall WriteConsoleW( //
+ void*, const void*, dword, dword*, void*);
+
+FMT_FUNC bool write_console(int fd, string_view text) {
+ auto u16 = utf8_to_utf16(text);
+ return WriteConsoleW(reinterpret_cast<void*>(_get_osfhandle(fd)), u16.c_str(),
+ static_cast<dword>(u16.size()), nullptr, nullptr) != 0;
+}
+
+FMT_FUNC auto write_console(std::FILE* f, string_view text) -> bool {
+ return write_console(_fileno(f), text);
+}
+#endif
+
+#ifdef _WIN32
+// Print assuming legacy (non-Unicode) encoding.
+FMT_FUNC void vprint_mojibake(std::FILE* f, string_view fmt, format_args args) {
+ auto buffer = memory_buffer();
+ detail::vformat_to(buffer, fmt, args);
+ fwrite_fully(buffer.data(), buffer.size(), f);
+}
+#endif
+
+FMT_FUNC void print(std::FILE* f, string_view text) {
+#ifdef _WIN32
+ int fd = _fileno(f);
+ if (_isatty(fd)) {
+ std::fflush(f);
+ if (write_console(fd, text)) return;
+ }
+#endif
+ fwrite_fully(text.data(), text.size(), f);
+}
+} // namespace detail
+
+FMT_FUNC void vprint(std::FILE* f, string_view fmt, format_args args) {
+ auto buffer = memory_buffer();
+ detail::vformat_to(buffer, fmt, args);
+ detail::print(f, {buffer.data(), buffer.size()});
+}
+
+FMT_FUNC void vprint(string_view fmt, format_args args) {
+ vprint(stdout, fmt, args);
+}
+
+namespace detail {
+
+struct singleton {
+ unsigned char upper;
+ unsigned char lower_count;
+};
+
+inline auto is_printable(uint16_t x, const singleton* singletons,
+ size_t singletons_size,
+ const unsigned char* singleton_lowers,
+ const unsigned char* normal, size_t normal_size)
+ -> bool {
+ auto upper = x >> 8;
+ auto lower_start = 0;
+ for (size_t i = 0; i < singletons_size; ++i) {
+ auto s = singletons[i];
+ auto lower_end = lower_start + s.lower_count;
+ if (upper < s.upper) break;
+ if (upper == s.upper) {
+ for (auto j = lower_start; j < lower_end; ++j) {
+ if (singleton_lowers[j] == (x & 0xff)) return false;
+ }
+ }
+ lower_start = lower_end;
+ }
+
+ auto xsigned = static_cast<int>(x);
+ auto current = true;
+ for (size_t i = 0; i < normal_size; ++i) {
+ auto v = static_cast<int>(normal[i]);
+ auto len = (v & 0x80) != 0 ? (v & 0x7f) << 8 | normal[++i] : v;
+ xsigned -= len;
+ if (xsigned < 0) break;
+ current = !current;
+ }
+ return current;
+}
+
+// This code is generated by support/printable.py.
+FMT_FUNC auto is_printable(uint32_t cp) -> bool {
+ static constexpr singleton singletons0[] = {
+ {0x00, 1}, {0x03, 5}, {0x05, 6}, {0x06, 3}, {0x07, 6}, {0x08, 8},
+ {0x09, 17}, {0x0a, 28}, {0x0b, 25}, {0x0c, 20}, {0x0d, 16}, {0x0e, 13},
+ {0x0f, 4}, {0x10, 3}, {0x12, 18}, {0x13, 9}, {0x16, 1}, {0x17, 5},
+ {0x18, 2}, {0x19, 3}, {0x1a, 7}, {0x1c, 2}, {0x1d, 1}, {0x1f, 22},
+ {0x20, 3}, {0x2b, 3}, {0x2c, 2}, {0x2d, 11}, {0x2e, 1}, {0x30, 3},
+ {0x31, 2}, {0x32, 1}, {0xa7, 2}, {0xa9, 2}, {0xaa, 4}, {0xab, 8},
+ {0xfa, 2}, {0xfb, 5}, {0xfd, 4}, {0xfe, 3}, {0xff, 9},
+ };
+ static constexpr unsigned char singletons0_lower[] = {
+ 0xad, 0x78, 0x79, 0x8b, 0x8d, 0xa2, 0x30, 0x57, 0x58, 0x8b, 0x8c, 0x90,
+ 0x1c, 0x1d, 0xdd, 0x0e, 0x0f, 0x4b, 0x4c, 0xfb, 0xfc, 0x2e, 0x2f, 0x3f,
+ 0x5c, 0x5d, 0x5f, 0xb5, 0xe2, 0x84, 0x8d, 0x8e, 0x91, 0x92, 0xa9, 0xb1,
+ 0xba, 0xbb, 0xc5, 0xc6, 0xc9, 0xca, 0xde, 0xe4, 0xe5, 0xff, 0x00, 0x04,
+ 0x11, 0x12, 0x29, 0x31, 0x34, 0x37, 0x3a, 0x3b, 0x3d, 0x49, 0x4a, 0x5d,
+ 0x84, 0x8e, 0x92, 0xa9, 0xb1, 0xb4, 0xba, 0xbb, 0xc6, 0xca, 0xce, 0xcf,
+ 0xe4, 0xe5, 0x00, 0x04, 0x0d, 0x0e, 0x11, 0x12, 0x29, 0x31, 0x34, 0x3a,
+ 0x3b, 0x45, 0x46, 0x49, 0x4a, 0x5e, 0x64, 0x65, 0x84, 0x91, 0x9b, 0x9d,
+ 0xc9, 0xce, 0xcf, 0x0d, 0x11, 0x29, 0x45, 0x49, 0x57, 0x64, 0x65, 0x8d,
+ 0x91, 0xa9, 0xb4, 0xba, 0xbb, 0xc5, 0xc9, 0xdf, 0xe4, 0xe5, 0xf0, 0x0d,
+ 0x11, 0x45, 0x49, 0x64, 0x65, 0x80, 0x84, 0xb2, 0xbc, 0xbe, 0xbf, 0xd5,
+ 0xd7, 0xf0, 0xf1, 0x83, 0x85, 0x8b, 0xa4, 0xa6, 0xbe, 0xbf, 0xc5, 0xc7,
+ 0xce, 0xcf, 0xda, 0xdb, 0x48, 0x98, 0xbd, 0xcd, 0xc6, 0xce, 0xcf, 0x49,
+ 0x4e, 0x4f, 0x57, 0x59, 0x5e, 0x5f, 0x89, 0x8e, 0x8f, 0xb1, 0xb6, 0xb7,
+ 0xbf, 0xc1, 0xc6, 0xc7, 0xd7, 0x11, 0x16, 0x17, 0x5b, 0x5c, 0xf6, 0xf7,
+ 0xfe, 0xff, 0x80, 0x0d, 0x6d, 0x71, 0xde, 0xdf, 0x0e, 0x0f, 0x1f, 0x6e,
+ 0x6f, 0x1c, 0x1d, 0x5f, 0x7d, 0x7e, 0xae, 0xaf, 0xbb, 0xbc, 0xfa, 0x16,
+ 0x17, 0x1e, 0x1f, 0x46, 0x47, 0x4e, 0x4f, 0x58, 0x5a, 0x5c, 0x5e, 0x7e,
+ 0x7f, 0xb5, 0xc5, 0xd4, 0xd5, 0xdc, 0xf0, 0xf1, 0xf5, 0x72, 0x73, 0x8f,
+ 0x74, 0x75, 0x96, 0x2f, 0x5f, 0x26, 0x2e, 0x2f, 0xa7, 0xaf, 0xb7, 0xbf,
+ 0xc7, 0xcf, 0xd7, 0xdf, 0x9a, 0x40, 0x97, 0x98, 0x30, 0x8f, 0x1f, 0xc0,
+ 0xc1, 0xce, 0xff, 0x4e, 0x4f, 0x5a, 0x5b, 0x07, 0x08, 0x0f, 0x10, 0x27,
+ 0x2f, 0xee, 0xef, 0x6e, 0x6f, 0x37, 0x3d, 0x3f, 0x42, 0x45, 0x90, 0x91,
+ 0xfe, 0xff, 0x53, 0x67, 0x75, 0xc8, 0xc9, 0xd0, 0xd1, 0xd8, 0xd9, 0xe7,
+ 0xfe, 0xff,
+ };
+ static constexpr singleton singletons1[] = {
+ {0x00, 6}, {0x01, 1}, {0x03, 1}, {0x04, 2}, {0x08, 8}, {0x09, 2},
+ {0x0a, 5}, {0x0b, 2}, {0x0e, 4}, {0x10, 1}, {0x11, 2}, {0x12, 5},
+ {0x13, 17}, {0x14, 1}, {0x15, 2}, {0x17, 2}, {0x19, 13}, {0x1c, 5},
+ {0x1d, 8}, {0x24, 1}, {0x6a, 3}, {0x6b, 2}, {0xbc, 2}, {0xd1, 2},
+ {0xd4, 12}, {0xd5, 9}, {0xd6, 2}, {0xd7, 2}, {0xda, 1}, {0xe0, 5},
+ {0xe1, 2}, {0xe8, 2}, {0xee, 32}, {0xf0, 4}, {0xf8, 2}, {0xf9, 2},
+ {0xfa, 2}, {0xfb, 1},
+ };
+ static constexpr unsigned char singletons1_lower[] = {
+ 0x0c, 0x27, 0x3b, 0x3e, 0x4e, 0x4f, 0x8f, 0x9e, 0x9e, 0x9f, 0x06, 0x07,
+ 0x09, 0x36, 0x3d, 0x3e, 0x56, 0xf3, 0xd0, 0xd1, 0x04, 0x14, 0x18, 0x36,
+ 0x37, 0x56, 0x57, 0x7f, 0xaa, 0xae, 0xaf, 0xbd, 0x35, 0xe0, 0x12, 0x87,
+ 0x89, 0x8e, 0x9e, 0x04, 0x0d, 0x0e, 0x11, 0x12, 0x29, 0x31, 0x34, 0x3a,
+ 0x45, 0x46, 0x49, 0x4a, 0x4e, 0x4f, 0x64, 0x65, 0x5c, 0xb6, 0xb7, 0x1b,
+ 0x1c, 0x07, 0x08, 0x0a, 0x0b, 0x14, 0x17, 0x36, 0x39, 0x3a, 0xa8, 0xa9,
+ 0xd8, 0xd9, 0x09, 0x37, 0x90, 0x91, 0xa8, 0x07, 0x0a, 0x3b, 0x3e, 0x66,
+ 0x69, 0x8f, 0x92, 0x6f, 0x5f, 0xee, 0xef, 0x5a, 0x62, 0x9a, 0x9b, 0x27,
+ 0x28, 0x55, 0x9d, 0xa0, 0xa1, 0xa3, 0xa4, 0xa7, 0xa8, 0xad, 0xba, 0xbc,
+ 0xc4, 0x06, 0x0b, 0x0c, 0x15, 0x1d, 0x3a, 0x3f, 0x45, 0x51, 0xa6, 0xa7,
+ 0xcc, 0xcd, 0xa0, 0x07, 0x19, 0x1a, 0x22, 0x25, 0x3e, 0x3f, 0xc5, 0xc6,
+ 0x04, 0x20, 0x23, 0x25, 0x26, 0x28, 0x33, 0x38, 0x3a, 0x48, 0x4a, 0x4c,
+ 0x50, 0x53, 0x55, 0x56, 0x58, 0x5a, 0x5c, 0x5e, 0x60, 0x63, 0x65, 0x66,
+ 0x6b, 0x73, 0x78, 0x7d, 0x7f, 0x8a, 0xa4, 0xaa, 0xaf, 0xb0, 0xc0, 0xd0,
+ 0xae, 0xaf, 0x79, 0xcc, 0x6e, 0x6f, 0x93,
+ };
+ static constexpr unsigned char normal0[] = {
+ 0x00, 0x20, 0x5f, 0x22, 0x82, 0xdf, 0x04, 0x82, 0x44, 0x08, 0x1b, 0x04,
+ 0x06, 0x11, 0x81, 0xac, 0x0e, 0x80, 0xab, 0x35, 0x28, 0x0b, 0x80, 0xe0,
+ 0x03, 0x19, 0x08, 0x01, 0x04, 0x2f, 0x04, 0x34, 0x04, 0x07, 0x03, 0x01,
+ 0x07, 0x06, 0x07, 0x11, 0x0a, 0x50, 0x0f, 0x12, 0x07, 0x55, 0x07, 0x03,
+ 0x04, 0x1c, 0x0a, 0x09, 0x03, 0x08, 0x03, 0x07, 0x03, 0x02, 0x03, 0x03,
+ 0x03, 0x0c, 0x04, 0x05, 0x03, 0x0b, 0x06, 0x01, 0x0e, 0x15, 0x05, 0x3a,
+ 0x03, 0x11, 0x07, 0x06, 0x05, 0x10, 0x07, 0x57, 0x07, 0x02, 0x07, 0x15,
+ 0x0d, 0x50, 0x04, 0x43, 0x03, 0x2d, 0x03, 0x01, 0x04, 0x11, 0x06, 0x0f,
+ 0x0c, 0x3a, 0x04, 0x1d, 0x25, 0x5f, 0x20, 0x6d, 0x04, 0x6a, 0x25, 0x80,
+ 0xc8, 0x05, 0x82, 0xb0, 0x03, 0x1a, 0x06, 0x82, 0xfd, 0x03, 0x59, 0x07,
+ 0x15, 0x0b, 0x17, 0x09, 0x14, 0x0c, 0x14, 0x0c, 0x6a, 0x06, 0x0a, 0x06,
+ 0x1a, 0x06, 0x59, 0x07, 0x2b, 0x05, 0x46, 0x0a, 0x2c, 0x04, 0x0c, 0x04,
+ 0x01, 0x03, 0x31, 0x0b, 0x2c, 0x04, 0x1a, 0x06, 0x0b, 0x03, 0x80, 0xac,
+ 0x06, 0x0a, 0x06, 0x21, 0x3f, 0x4c, 0x04, 0x2d, 0x03, 0x74, 0x08, 0x3c,
+ 0x03, 0x0f, 0x03, 0x3c, 0x07, 0x38, 0x08, 0x2b, 0x05, 0x82, 0xff, 0x11,
+ 0x18, 0x08, 0x2f, 0x11, 0x2d, 0x03, 0x20, 0x10, 0x21, 0x0f, 0x80, 0x8c,
+ 0x04, 0x82, 0x97, 0x19, 0x0b, 0x15, 0x88, 0x94, 0x05, 0x2f, 0x05, 0x3b,
+ 0x07, 0x02, 0x0e, 0x18, 0x09, 0x80, 0xb3, 0x2d, 0x74, 0x0c, 0x80, 0xd6,
+ 0x1a, 0x0c, 0x05, 0x80, 0xff, 0x05, 0x80, 0xdf, 0x0c, 0xee, 0x0d, 0x03,
+ 0x84, 0x8d, 0x03, 0x37, 0x09, 0x81, 0x5c, 0x14, 0x80, 0xb8, 0x08, 0x80,
+ 0xcb, 0x2a, 0x38, 0x03, 0x0a, 0x06, 0x38, 0x08, 0x46, 0x08, 0x0c, 0x06,
+ 0x74, 0x0b, 0x1e, 0x03, 0x5a, 0x04, 0x59, 0x09, 0x80, 0x83, 0x18, 0x1c,
+ 0x0a, 0x16, 0x09, 0x4c, 0x04, 0x80, 0x8a, 0x06, 0xab, 0xa4, 0x0c, 0x17,
+ 0x04, 0x31, 0xa1, 0x04, 0x81, 0xda, 0x26, 0x07, 0x0c, 0x05, 0x05, 0x80,
+ 0xa5, 0x11, 0x81, 0x6d, 0x10, 0x78, 0x28, 0x2a, 0x06, 0x4c, 0x04, 0x80,
+ 0x8d, 0x04, 0x80, 0xbe, 0x03, 0x1b, 0x03, 0x0f, 0x0d,
+ };
+ static constexpr unsigned char normal1[] = {
+ 0x5e, 0x22, 0x7b, 0x05, 0x03, 0x04, 0x2d, 0x03, 0x66, 0x03, 0x01, 0x2f,
+ 0x2e, 0x80, 0x82, 0x1d, 0x03, 0x31, 0x0f, 0x1c, 0x04, 0x24, 0x09, 0x1e,
+ 0x05, 0x2b, 0x05, 0x44, 0x04, 0x0e, 0x2a, 0x80, 0xaa, 0x06, 0x24, 0x04,
+ 0x24, 0x04, 0x28, 0x08, 0x34, 0x0b, 0x01, 0x80, 0x90, 0x81, 0x37, 0x09,
+ 0x16, 0x0a, 0x08, 0x80, 0x98, 0x39, 0x03, 0x63, 0x08, 0x09, 0x30, 0x16,
+ 0x05, 0x21, 0x03, 0x1b, 0x05, 0x01, 0x40, 0x38, 0x04, 0x4b, 0x05, 0x2f,
+ 0x04, 0x0a, 0x07, 0x09, 0x07, 0x40, 0x20, 0x27, 0x04, 0x0c, 0x09, 0x36,
+ 0x03, 0x3a, 0x05, 0x1a, 0x07, 0x04, 0x0c, 0x07, 0x50, 0x49, 0x37, 0x33,
+ 0x0d, 0x33, 0x07, 0x2e, 0x08, 0x0a, 0x81, 0x26, 0x52, 0x4e, 0x28, 0x08,
+ 0x2a, 0x56, 0x1c, 0x14, 0x17, 0x09, 0x4e, 0x04, 0x1e, 0x0f, 0x43, 0x0e,
+ 0x19, 0x07, 0x0a, 0x06, 0x48, 0x08, 0x27, 0x09, 0x75, 0x0b, 0x3f, 0x41,
+ 0x2a, 0x06, 0x3b, 0x05, 0x0a, 0x06, 0x51, 0x06, 0x01, 0x05, 0x10, 0x03,
+ 0x05, 0x80, 0x8b, 0x62, 0x1e, 0x48, 0x08, 0x0a, 0x80, 0xa6, 0x5e, 0x22,
+ 0x45, 0x0b, 0x0a, 0x06, 0x0d, 0x13, 0x39, 0x07, 0x0a, 0x36, 0x2c, 0x04,
+ 0x10, 0x80, 0xc0, 0x3c, 0x64, 0x53, 0x0c, 0x48, 0x09, 0x0a, 0x46, 0x45,
+ 0x1b, 0x48, 0x08, 0x53, 0x1d, 0x39, 0x81, 0x07, 0x46, 0x0a, 0x1d, 0x03,
+ 0x47, 0x49, 0x37, 0x03, 0x0e, 0x08, 0x0a, 0x06, 0x39, 0x07, 0x0a, 0x81,
+ 0x36, 0x19, 0x80, 0xb7, 0x01, 0x0f, 0x32, 0x0d, 0x83, 0x9b, 0x66, 0x75,
+ 0x0b, 0x80, 0xc4, 0x8a, 0xbc, 0x84, 0x2f, 0x8f, 0xd1, 0x82, 0x47, 0xa1,
+ 0xb9, 0x82, 0x39, 0x07, 0x2a, 0x04, 0x02, 0x60, 0x26, 0x0a, 0x46, 0x0a,
+ 0x28, 0x05, 0x13, 0x82, 0xb0, 0x5b, 0x65, 0x4b, 0x04, 0x39, 0x07, 0x11,
+ 0x40, 0x05, 0x0b, 0x02, 0x0e, 0x97, 0xf8, 0x08, 0x84, 0xd6, 0x2a, 0x09,
+ 0xa2, 0xf7, 0x81, 0x1f, 0x31, 0x03, 0x11, 0x04, 0x08, 0x81, 0x8c, 0x89,
+ 0x04, 0x6b, 0x05, 0x0d, 0x03, 0x09, 0x07, 0x10, 0x93, 0x60, 0x80, 0xf6,
+ 0x0a, 0x73, 0x08, 0x6e, 0x17, 0x46, 0x80, 0x9a, 0x14, 0x0c, 0x57, 0x09,
+ 0x19, 0x80, 0x87, 0x81, 0x47, 0x03, 0x85, 0x42, 0x0f, 0x15, 0x85, 0x50,
+ 0x2b, 0x80, 0xd5, 0x2d, 0x03, 0x1a, 0x04, 0x02, 0x81, 0x70, 0x3a, 0x05,
+ 0x01, 0x85, 0x00, 0x80, 0xd7, 0x29, 0x4c, 0x04, 0x0a, 0x04, 0x02, 0x83,
+ 0x11, 0x44, 0x4c, 0x3d, 0x80, 0xc2, 0x3c, 0x06, 0x01, 0x04, 0x55, 0x05,
+ 0x1b, 0x34, 0x02, 0x81, 0x0e, 0x2c, 0x04, 0x64, 0x0c, 0x56, 0x0a, 0x80,
+ 0xae, 0x38, 0x1d, 0x0d, 0x2c, 0x04, 0x09, 0x07, 0x02, 0x0e, 0x06, 0x80,
+ 0x9a, 0x83, 0xd8, 0x08, 0x0d, 0x03, 0x0d, 0x03, 0x74, 0x0c, 0x59, 0x07,
+ 0x0c, 0x14, 0x0c, 0x04, 0x38, 0x08, 0x0a, 0x06, 0x28, 0x08, 0x22, 0x4e,
+ 0x81, 0x54, 0x0c, 0x15, 0x03, 0x03, 0x05, 0x07, 0x09, 0x19, 0x07, 0x07,
+ 0x09, 0x03, 0x0d, 0x07, 0x29, 0x80, 0xcb, 0x25, 0x0a, 0x84, 0x06,
+ };
+ auto lower = static_cast<uint16_t>(cp);
+ if (cp < 0x10000) {
+ return is_printable(lower, singletons0,
+ sizeof(singletons0) / sizeof(*singletons0),
+ singletons0_lower, normal0, sizeof(normal0));
+ }
+ if (cp < 0x20000) {
+ return is_printable(lower, singletons1,
+ sizeof(singletons1) / sizeof(*singletons1),
+ singletons1_lower, normal1, sizeof(normal1));
+ }
+ if (0x2a6de <= cp && cp < 0x2a700) return false;
+ if (0x2b735 <= cp && cp < 0x2b740) return false;
+ if (0x2b81e <= cp && cp < 0x2b820) return false;
+ if (0x2cea2 <= cp && cp < 0x2ceb0) return false;
+ if (0x2ebe1 <= cp && cp < 0x2f800) return false;
+ if (0x2fa1e <= cp && cp < 0x30000) return false;
+ if (0x3134b <= cp && cp < 0xe0100) return false;
+ if (0xe01f0 <= cp && cp < 0x110000) return false;
+ return cp < 0x110000;
+}
+
+} // namespace detail
+
+FMT_END_NAMESPACE
+
+#endif // FMT_FORMAT_INL_H_
--- /dev/null
+/*
+ Formatting library for C++
+
+ Copyright (c) 2012 - present, Victor Zverovich
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ --- Optional exception to the license ---
+
+ As an exception, if, as a result of your compiling your source code, portions
+ of this Software are embedded into a machine-executable object form of such
+ source code, you may redistribute such embedded portions in such object form
+ without including the above copyright and permission notices.
+ */
+
+#ifndef FMT_FORMAT_H_
+#define FMT_FORMAT_H_
+
+#include <cmath> // std::signbit
+#include <cstdint> // uint32_t
+#include <cstring> // std::memcpy
+#include <initializer_list> // std::initializer_list
+#include <limits> // std::numeric_limits
+#include <memory> // std::uninitialized_copy
+#include <stdexcept> // std::runtime_error
+#include <system_error> // std::system_error
+
+#ifdef __cpp_lib_bit_cast
+# include <bit> // std::bit_cast
+#endif
+
+#include "core.h"
+
+#if defined __cpp_inline_variables && __cpp_inline_variables >= 201606L
+# define FMT_INLINE_VARIABLE inline
+#else
+# define FMT_INLINE_VARIABLE
+#endif
+
+#if FMT_HAS_CPP17_ATTRIBUTE(fallthrough)
+# define FMT_FALLTHROUGH [[fallthrough]]
+#elif defined(__clang__)
+# define FMT_FALLTHROUGH [[clang::fallthrough]]
+#elif FMT_GCC_VERSION >= 700 && \
+ (!defined(__EDG_VERSION__) || __EDG_VERSION__ >= 520)
+# define FMT_FALLTHROUGH [[gnu::fallthrough]]
+#else
+# define FMT_FALLTHROUGH
+#endif
+
+#ifndef FMT_DEPRECATED
+# if FMT_HAS_CPP14_ATTRIBUTE(deprecated) || FMT_MSC_VERSION >= 1900
+# define FMT_DEPRECATED [[deprecated]]
+# else
+# if (defined(__GNUC__) && !defined(__LCC__)) || defined(__clang__)
+# define FMT_DEPRECATED __attribute__((deprecated))
+# elif FMT_MSC_VERSION
+# define FMT_DEPRECATED __declspec(deprecated)
+# else
+# define FMT_DEPRECATED /* deprecated */
+# endif
+# endif
+#endif
+
+#ifndef FMT_NO_UNIQUE_ADDRESS
+# if FMT_CPLUSPLUS >= 202002L
+# if FMT_HAS_CPP_ATTRIBUTE(no_unique_address)
+# define FMT_NO_UNIQUE_ADDRESS [[no_unique_address]]
+// VS2019 v16.10 and later except clang-cl (https://reviews.llvm.org/D110485)
+# elif (FMT_MSC_VERSION >= 1929) && !FMT_CLANG_VERSION
+# define FMT_NO_UNIQUE_ADDRESS [[msvc::no_unique_address]]
+# endif
+# endif
+#endif
+#ifndef FMT_NO_UNIQUE_ADDRESS
+# define FMT_NO_UNIQUE_ADDRESS
+#endif
+
+// Visibility when compiled as a shared library/object.
+#if defined(FMT_LIB_EXPORT) || defined(FMT_SHARED)
+# define FMT_SO_VISIBILITY(value) FMT_VISIBILITY(value)
+#else
+# define FMT_SO_VISIBILITY(value)
+#endif
+
+#ifdef __has_builtin
+# define FMT_HAS_BUILTIN(x) __has_builtin(x)
+#else
+# define FMT_HAS_BUILTIN(x) 0
+#endif
+
+#if FMT_GCC_VERSION || FMT_CLANG_VERSION
+# define FMT_NOINLINE __attribute__((noinline))
+#else
+# define FMT_NOINLINE
+#endif
+
+#ifndef FMT_THROW
+# if FMT_EXCEPTIONS
+# if FMT_MSC_VERSION || defined(__NVCC__)
+FMT_BEGIN_NAMESPACE
+namespace detail {
+template <typename Exception> inline void do_throw(const Exception& x) {
+ // Silence unreachable code warnings in MSVC and NVCC because these
+ // are nearly impossible to fix in a generic code.
+ volatile bool b = true;
+ if (b) throw x;
+}
+} // namespace detail
+FMT_END_NAMESPACE
+# define FMT_THROW(x) detail::do_throw(x)
+# else
+# define FMT_THROW(x) throw x
+# endif
+# else
+# define FMT_THROW(x) \
+ ::fmt::detail::assert_fail(__FILE__, __LINE__, (x).what())
+# endif
+#endif
+
+#if FMT_EXCEPTIONS
+# define FMT_TRY try
+# define FMT_CATCH(x) catch (x)
+#else
+# define FMT_TRY if (true)
+# define FMT_CATCH(x) if (false)
+#endif
+
+#ifndef FMT_MAYBE_UNUSED
+# if FMT_HAS_CPP17_ATTRIBUTE(maybe_unused)
+# define FMT_MAYBE_UNUSED [[maybe_unused]]
+# else
+# define FMT_MAYBE_UNUSED
+# endif
+#endif
+
+#ifndef FMT_USE_USER_DEFINED_LITERALS
+// EDG based compilers (Intel, NVIDIA, Elbrus, etc), GCC and MSVC support UDLs.
+//
+// GCC before 4.9 requires a space in `operator"" _a` which is invalid in later
+// compiler versions.
+# if (FMT_HAS_FEATURE(cxx_user_literals) || FMT_GCC_VERSION >= 409 || \
+ FMT_MSC_VERSION >= 1900) && \
+ (!defined(__EDG_VERSION__) || __EDG_VERSION__ >= /* UDL feature */ 480)
+# define FMT_USE_USER_DEFINED_LITERALS 1
+# else
+# define FMT_USE_USER_DEFINED_LITERALS 0
+# endif
+#endif
+
+// Defining FMT_REDUCE_INT_INSTANTIATIONS to 1, will reduce the number of
+// integer formatter template instantiations to just one by only using the
+// largest integer type. This results in a reduction in binary size but will
+// cause a decrease in integer formatting performance.
+#if !defined(FMT_REDUCE_INT_INSTANTIATIONS)
+# define FMT_REDUCE_INT_INSTANTIATIONS 0
+#endif
+
+// __builtin_clz is broken in clang with Microsoft CodeGen:
+// https://github.com/fmtlib/fmt/issues/519.
+#if !FMT_MSC_VERSION
+# if FMT_HAS_BUILTIN(__builtin_clz) || FMT_GCC_VERSION || FMT_ICC_VERSION
+# define FMT_BUILTIN_CLZ(n) __builtin_clz(n)
+# endif
+# if FMT_HAS_BUILTIN(__builtin_clzll) || FMT_GCC_VERSION || FMT_ICC_VERSION
+# define FMT_BUILTIN_CLZLL(n) __builtin_clzll(n)
+# endif
+#endif
+
+// __builtin_ctz is broken in Intel Compiler Classic on Windows:
+// https://github.com/fmtlib/fmt/issues/2510.
+#ifndef __ICL
+# if FMT_HAS_BUILTIN(__builtin_ctz) || FMT_GCC_VERSION || FMT_ICC_VERSION || \
+ defined(__NVCOMPILER)
+# define FMT_BUILTIN_CTZ(n) __builtin_ctz(n)
+# endif
+# if FMT_HAS_BUILTIN(__builtin_ctzll) || FMT_GCC_VERSION || \
+ FMT_ICC_VERSION || defined(__NVCOMPILER)
+# define FMT_BUILTIN_CTZLL(n) __builtin_ctzll(n)
+# endif
+#endif
+
+#if FMT_MSC_VERSION
+# include <intrin.h> // _BitScanReverse[64], _BitScanForward[64], _umul128
+#endif
+
+// Some compilers masquerade as both MSVC and GCC-likes or otherwise support
+// __builtin_clz and __builtin_clzll, so only define FMT_BUILTIN_CLZ using the
+// MSVC intrinsics if the clz and clzll builtins are not available.
+#if FMT_MSC_VERSION && !defined(FMT_BUILTIN_CLZLL) && \
+ !defined(FMT_BUILTIN_CTZLL)
+FMT_BEGIN_NAMESPACE
+namespace detail {
+// Avoid Clang with Microsoft CodeGen's -Wunknown-pragmas warning.
+# if !defined(__clang__)
+# pragma intrinsic(_BitScanForward)
+# pragma intrinsic(_BitScanReverse)
+# if defined(_WIN64)
+# pragma intrinsic(_BitScanForward64)
+# pragma intrinsic(_BitScanReverse64)
+# endif
+# endif
+
+inline auto clz(uint32_t x) -> int {
+ unsigned long r = 0;
+ _BitScanReverse(&r, x);
+ FMT_ASSERT(x != 0, "");
+ // Static analysis complains about using uninitialized data
+ // "r", but the only way that can happen is if "x" is 0,
+ // which the callers guarantee to not happen.
+ FMT_MSC_WARNING(suppress : 6102)
+ return 31 ^ static_cast<int>(r);
+}
+# define FMT_BUILTIN_CLZ(n) detail::clz(n)
+
+inline auto clzll(uint64_t x) -> int {
+ unsigned long r = 0;
+# ifdef _WIN64
+ _BitScanReverse64(&r, x);
+# else
+ // Scan the high 32 bits.
+ if (_BitScanReverse(&r, static_cast<uint32_t>(x >> 32)))
+ return 63 ^ static_cast<int>(r + 32);
+ // Scan the low 32 bits.
+ _BitScanReverse(&r, static_cast<uint32_t>(x));
+# endif
+ FMT_ASSERT(x != 0, "");
+ FMT_MSC_WARNING(suppress : 6102) // Suppress a bogus static analysis warning.
+ return 63 ^ static_cast<int>(r);
+}
+# define FMT_BUILTIN_CLZLL(n) detail::clzll(n)
+
+inline auto ctz(uint32_t x) -> int {
+ unsigned long r = 0;
+ _BitScanForward(&r, x);
+ FMT_ASSERT(x != 0, "");
+ FMT_MSC_WARNING(suppress : 6102) // Suppress a bogus static analysis warning.
+ return static_cast<int>(r);
+}
+# define FMT_BUILTIN_CTZ(n) detail::ctz(n)
+
+inline auto ctzll(uint64_t x) -> int {
+ unsigned long r = 0;
+ FMT_ASSERT(x != 0, "");
+ FMT_MSC_WARNING(suppress : 6102) // Suppress a bogus static analysis warning.
+# ifdef _WIN64
+ _BitScanForward64(&r, x);
+# else
+ // Scan the low 32 bits.
+ if (_BitScanForward(&r, static_cast<uint32_t>(x))) return static_cast<int>(r);
+ // Scan the high 32 bits.
+ _BitScanForward(&r, static_cast<uint32_t>(x >> 32));
+ r += 32;
+# endif
+ return static_cast<int>(r);
+}
+# define FMT_BUILTIN_CTZLL(n) detail::ctzll(n)
+} // namespace detail
+FMT_END_NAMESPACE
+#endif
+
+FMT_BEGIN_NAMESPACE
+namespace detail {
+
+FMT_CONSTEXPR inline void abort_fuzzing_if(bool condition) {
+ ignore_unused(condition);
+#ifdef FMT_FUZZ
+ if (condition) throw std::runtime_error("fuzzing limit reached");
+#endif
+}
+
+template <typename CharT, CharT... C> struct string_literal {
+ static constexpr CharT value[sizeof...(C)] = {C...};
+ constexpr operator basic_string_view<CharT>() const {
+ return {value, sizeof...(C)};
+ }
+};
+
+#if FMT_CPLUSPLUS < 201703L
+template <typename CharT, CharT... C>
+constexpr CharT string_literal<CharT, C...>::value[sizeof...(C)];
+#endif
+
+// Implementation of std::bit_cast for pre-C++20.
+template <typename To, typename From, FMT_ENABLE_IF(sizeof(To) == sizeof(From))>
+FMT_CONSTEXPR20 auto bit_cast(const From& from) -> To {
+#ifdef __cpp_lib_bit_cast
+ if (is_constant_evaluated()) return std::bit_cast<To>(from);
+#endif
+ auto to = To();
+ // The cast suppresses a bogus -Wclass-memaccess on GCC.
+ std::memcpy(static_cast<void*>(&to), &from, sizeof(to));
+ return to;
+}
+
+inline auto is_big_endian() -> bool {
+#ifdef _WIN32
+ return false;
+#elif defined(__BIG_ENDIAN__)
+ return true;
+#elif defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__)
+ return __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__;
+#else
+ struct bytes {
+ char data[sizeof(int)];
+ };
+ return bit_cast<bytes>(1).data[0] == 0;
+#endif
+}
+
+class uint128_fallback {
+ private:
+ uint64_t lo_, hi_;
+
+ public:
+ constexpr uint128_fallback(uint64_t hi, uint64_t lo) : lo_(lo), hi_(hi) {}
+ constexpr uint128_fallback(uint64_t value = 0) : lo_(value), hi_(0) {}
+
+ constexpr auto high() const noexcept -> uint64_t { return hi_; }
+ constexpr auto low() const noexcept -> uint64_t { return lo_; }
+
+ template <typename T, FMT_ENABLE_IF(std::is_integral<T>::value)>
+ constexpr explicit operator T() const {
+ return static_cast<T>(lo_);
+ }
+
+ friend constexpr auto operator==(const uint128_fallback& lhs,
+ const uint128_fallback& rhs) -> bool {
+ return lhs.hi_ == rhs.hi_ && lhs.lo_ == rhs.lo_;
+ }
+ friend constexpr auto operator!=(const uint128_fallback& lhs,
+ const uint128_fallback& rhs) -> bool {
+ return !(lhs == rhs);
+ }
+ friend constexpr auto operator>(const uint128_fallback& lhs,
+ const uint128_fallback& rhs) -> bool {
+ return lhs.hi_ != rhs.hi_ ? lhs.hi_ > rhs.hi_ : lhs.lo_ > rhs.lo_;
+ }
+ friend constexpr auto operator|(const uint128_fallback& lhs,
+ const uint128_fallback& rhs)
+ -> uint128_fallback {
+ return {lhs.hi_ | rhs.hi_, lhs.lo_ | rhs.lo_};
+ }
+ friend constexpr auto operator&(const uint128_fallback& lhs,
+ const uint128_fallback& rhs)
+ -> uint128_fallback {
+ return {lhs.hi_ & rhs.hi_, lhs.lo_ & rhs.lo_};
+ }
+ friend constexpr auto operator~(const uint128_fallback& n)
+ -> uint128_fallback {
+ return {~n.hi_, ~n.lo_};
+ }
+ friend auto operator+(const uint128_fallback& lhs,
+ const uint128_fallback& rhs) -> uint128_fallback {
+ auto result = uint128_fallback(lhs);
+ result += rhs;
+ return result;
+ }
+ friend auto operator*(const uint128_fallback& lhs, uint32_t rhs)
+ -> uint128_fallback {
+ FMT_ASSERT(lhs.hi_ == 0, "");
+ uint64_t hi = (lhs.lo_ >> 32) * rhs;
+ uint64_t lo = (lhs.lo_ & ~uint32_t()) * rhs;
+ uint64_t new_lo = (hi << 32) + lo;
+ return {(hi >> 32) + (new_lo < lo ? 1 : 0), new_lo};
+ }
+ friend auto operator-(const uint128_fallback& lhs, uint64_t rhs)
+ -> uint128_fallback {
+ return {lhs.hi_ - (lhs.lo_ < rhs ? 1 : 0), lhs.lo_ - rhs};
+ }
+ FMT_CONSTEXPR auto operator>>(int shift) const -> uint128_fallback {
+ if (shift == 64) return {0, hi_};
+ if (shift > 64) return uint128_fallback(0, hi_) >> (shift - 64);
+ return {hi_ >> shift, (hi_ << (64 - shift)) | (lo_ >> shift)};
+ }
+ FMT_CONSTEXPR auto operator<<(int shift) const -> uint128_fallback {
+ if (shift == 64) return {lo_, 0};
+ if (shift > 64) return uint128_fallback(lo_, 0) << (shift - 64);
+ return {hi_ << shift | (lo_ >> (64 - shift)), (lo_ << shift)};
+ }
+ FMT_CONSTEXPR auto operator>>=(int shift) -> uint128_fallback& {
+ return *this = *this >> shift;
+ }
+ FMT_CONSTEXPR void operator+=(uint128_fallback n) {
+ uint64_t new_lo = lo_ + n.lo_;
+ uint64_t new_hi = hi_ + n.hi_ + (new_lo < lo_ ? 1 : 0);
+ FMT_ASSERT(new_hi >= hi_, "");
+ lo_ = new_lo;
+ hi_ = new_hi;
+ }
+ FMT_CONSTEXPR void operator&=(uint128_fallback n) {
+ lo_ &= n.lo_;
+ hi_ &= n.hi_;
+ }
+
+ FMT_CONSTEXPR20 auto operator+=(uint64_t n) noexcept -> uint128_fallback& {
+ if (is_constant_evaluated()) {
+ lo_ += n;
+ hi_ += (lo_ < n ? 1 : 0);
+ return *this;
+ }
+#if FMT_HAS_BUILTIN(__builtin_addcll) && !defined(__ibmxl__)
+ unsigned long long carry;
+ lo_ = __builtin_addcll(lo_, n, 0, &carry);
+ hi_ += carry;
+#elif FMT_HAS_BUILTIN(__builtin_ia32_addcarryx_u64) && !defined(__ibmxl__)
+ unsigned long long result;
+ auto carry = __builtin_ia32_addcarryx_u64(0, lo_, n, &result);
+ lo_ = result;
+ hi_ += carry;
+#elif defined(_MSC_VER) && defined(_M_X64)
+ auto carry = _addcarry_u64(0, lo_, n, &lo_);
+ _addcarry_u64(carry, hi_, 0, &hi_);
+#else
+ lo_ += n;
+ hi_ += (lo_ < n ? 1 : 0);
+#endif
+ return *this;
+ }
+};
+
+using uint128_t = conditional_t<FMT_USE_INT128, uint128_opt, uint128_fallback>;
+
+#ifdef UINTPTR_MAX
+using uintptr_t = ::uintptr_t;
+#else
+using uintptr_t = uint128_t;
+#endif
+
+// Returns the largest possible value for type T. Same as
+// std::numeric_limits<T>::max() but shorter and not affected by the max macro.
+template <typename T> constexpr auto max_value() -> T {
+ return (std::numeric_limits<T>::max)();
+}
+template <typename T> constexpr auto num_bits() -> int {
+ return std::numeric_limits<T>::digits;
+}
+// std::numeric_limits<T>::digits may return 0 for 128-bit ints.
+template <> constexpr auto num_bits<int128_opt>() -> int { return 128; }
+template <> constexpr auto num_bits<uint128_t>() -> int { return 128; }
+
+// A heterogeneous bit_cast used for converting 96-bit long double to uint128_t
+// and 128-bit pointers to uint128_fallback.
+template <typename To, typename From, FMT_ENABLE_IF(sizeof(To) > sizeof(From))>
+inline auto bit_cast(const From& from) -> To {
+ constexpr auto size = static_cast<int>(sizeof(From) / sizeof(unsigned));
+ struct data_t {
+ unsigned value[static_cast<unsigned>(size)];
+ } data = bit_cast<data_t>(from);
+ auto result = To();
+ if (const_check(is_big_endian())) {
+ for (int i = 0; i < size; ++i)
+ result = (result << num_bits<unsigned>()) | data.value[i];
+ } else {
+ for (int i = size - 1; i >= 0; --i)
+ result = (result << num_bits<unsigned>()) | data.value[i];
+ }
+ return result;
+}
+
+template <typename UInt>
+FMT_CONSTEXPR20 inline auto countl_zero_fallback(UInt n) -> int {
+ int lz = 0;
+ constexpr UInt msb_mask = static_cast<UInt>(1) << (num_bits<UInt>() - 1);
+ for (; (n & msb_mask) == 0; n <<= 1) lz++;
+ return lz;
+}
+
+FMT_CONSTEXPR20 inline auto countl_zero(uint32_t n) -> int {
+#ifdef FMT_BUILTIN_CLZ
+ if (!is_constant_evaluated()) return FMT_BUILTIN_CLZ(n);
+#endif
+ return countl_zero_fallback(n);
+}
+
+FMT_CONSTEXPR20 inline auto countl_zero(uint64_t n) -> int {
+#ifdef FMT_BUILTIN_CLZLL
+ if (!is_constant_evaluated()) return FMT_BUILTIN_CLZLL(n);
+#endif
+ return countl_zero_fallback(n);
+}
+
+FMT_INLINE void assume(bool condition) {
+ (void)condition;
+#if FMT_HAS_BUILTIN(__builtin_assume) && !FMT_ICC_VERSION
+ __builtin_assume(condition);
+#elif FMT_GCC_VERSION
+ if (!condition) __builtin_unreachable();
+#endif
+}
+
+// An approximation of iterator_t for pre-C++20 systems.
+template <typename T>
+using iterator_t = decltype(std::begin(std::declval<T&>()));
+template <typename T> using sentinel_t = decltype(std::end(std::declval<T&>()));
+
+// A workaround for std::string not having mutable data() until C++17.
+template <typename Char>
+inline auto get_data(std::basic_string<Char>& s) -> Char* {
+ return &s[0];
+}
+template <typename Container>
+inline auto get_data(Container& c) -> typename Container::value_type* {
+ return c.data();
+}
+
+// Attempts to reserve space for n extra characters in the output range.
+// Returns a pointer to the reserved range or a reference to it.
+template <typename Container, FMT_ENABLE_IF(is_contiguous<Container>::value)>
+#if FMT_CLANG_VERSION >= 307 && !FMT_ICC_VERSION
+__attribute__((no_sanitize("undefined")))
+#endif
+inline auto
+reserve(std::back_insert_iterator<Container> it, size_t n) ->
+ typename Container::value_type* {
+ Container& c = get_container(it);
+ size_t size = c.size();
+ c.resize(size + n);
+ return get_data(c) + size;
+}
+
+template <typename T>
+inline auto reserve(buffer_appender<T> it, size_t n) -> buffer_appender<T> {
+ buffer<T>& buf = get_container(it);
+ buf.try_reserve(buf.size() + n);
+ return it;
+}
+
+template <typename Iterator>
+constexpr auto reserve(Iterator& it, size_t) -> Iterator& {
+ return it;
+}
+
+template <typename OutputIt>
+using reserve_iterator =
+ remove_reference_t<decltype(reserve(std::declval<OutputIt&>(), 0))>;
+
+template <typename T, typename OutputIt>
+constexpr auto to_pointer(OutputIt, size_t) -> T* {
+ return nullptr;
+}
+template <typename T> auto to_pointer(buffer_appender<T> it, size_t n) -> T* {
+ buffer<T>& buf = get_container(it);
+ auto size = buf.size();
+ if (buf.capacity() < size + n) return nullptr;
+ buf.try_resize(size + n);
+ return buf.data() + size;
+}
+
+template <typename Container, FMT_ENABLE_IF(is_contiguous<Container>::value)>
+inline auto base_iterator(std::back_insert_iterator<Container> it,
+ typename Container::value_type*)
+ -> std::back_insert_iterator<Container> {
+ return it;
+}
+
+template <typename Iterator>
+constexpr auto base_iterator(Iterator, Iterator it) -> Iterator {
+ return it;
+}
+
+// <algorithm> is spectacularly slow to compile in C++20 so use a simple fill_n
+// instead (#1998).
+template <typename OutputIt, typename Size, typename T>
+FMT_CONSTEXPR auto fill_n(OutputIt out, Size count, const T& value)
+ -> OutputIt {
+ for (Size i = 0; i < count; ++i) *out++ = value;
+ return out;
+}
+template <typename T, typename Size>
+FMT_CONSTEXPR20 auto fill_n(T* out, Size count, char value) -> T* {
+ if (is_constant_evaluated()) {
+ return fill_n<T*, Size, T>(out, count, value);
+ }
+ std::memset(out, value, to_unsigned(count));
+ return out + count;
+}
+
+#ifdef __cpp_char8_t
+using char8_type = char8_t;
+#else
+enum char8_type : unsigned char {};
+#endif
+
+template <typename OutChar, typename InputIt, typename OutputIt>
+FMT_CONSTEXPR FMT_NOINLINE auto copy_str_noinline(InputIt begin, InputIt end,
+ OutputIt out) -> OutputIt {
+ return copy_str<OutChar>(begin, end, out);
+}
+
+// A public domain branchless UTF-8 decoder by Christopher Wellons:
+// https://github.com/skeeto/branchless-utf8
+/* Decode the next character, c, from s, reporting errors in e.
+ *
+ * Since this is a branchless decoder, four bytes will be read from the
+ * buffer regardless of the actual length of the next character. This
+ * means the buffer _must_ have at least three bytes of zero padding
+ * following the end of the data stream.
+ *
+ * Errors are reported in e, which will be non-zero if the parsed
+ * character was somehow invalid: invalid byte sequence, non-canonical
+ * encoding, or a surrogate half.
+ *
+ * The function returns a pointer to the next character. When an error
+ * occurs, this pointer will be a guess that depends on the particular
+ * error, but it will always advance at least one byte.
+ */
+FMT_CONSTEXPR inline auto utf8_decode(const char* s, uint32_t* c, int* e)
+ -> const char* {
+ constexpr const int masks[] = {0x00, 0x7f, 0x1f, 0x0f, 0x07};
+ constexpr const uint32_t mins[] = {4194304, 0, 128, 2048, 65536};
+ constexpr const int shiftc[] = {0, 18, 12, 6, 0};
+ constexpr const int shifte[] = {0, 6, 4, 2, 0};
+
+ int len = "\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\0\0\0\0\0\0\0\2\2\2\2\3\3\4"
+ [static_cast<unsigned char>(*s) >> 3];
+ // Compute the pointer to the next character early so that the next
+ // iteration can start working on the next character. Neither Clang
+ // nor GCC figure out this reordering on their own.
+ const char* next = s + len + !len;
+
+ using uchar = unsigned char;
+
+ // Assume a four-byte character and load four bytes. Unused bits are
+ // shifted out.
+ *c = uint32_t(uchar(s[0]) & masks[len]) << 18;
+ *c |= uint32_t(uchar(s[1]) & 0x3f) << 12;
+ *c |= uint32_t(uchar(s[2]) & 0x3f) << 6;
+ *c |= uint32_t(uchar(s[3]) & 0x3f) << 0;
+ *c >>= shiftc[len];
+
+ // Accumulate the various error conditions.
+ *e = (*c < mins[len]) << 6; // non-canonical encoding
+ *e |= ((*c >> 11) == 0x1b) << 7; // surrogate half?
+ *e |= (*c > 0x10FFFF) << 8; // out of range?
+ *e |= (uchar(s[1]) & 0xc0) >> 2;
+ *e |= (uchar(s[2]) & 0xc0) >> 4;
+ *e |= uchar(s[3]) >> 6;
+ *e ^= 0x2a; // top two bits of each tail byte correct?
+ *e >>= shifte[len];
+
+ return next;
+}
+
+constexpr FMT_INLINE_VARIABLE uint32_t invalid_code_point = ~uint32_t();
+
+// Invokes f(cp, sv) for every code point cp in s with sv being the string view
+// corresponding to the code point. cp is invalid_code_point on error.
+template <typename F>
+FMT_CONSTEXPR void for_each_codepoint(string_view s, F f) {
+ auto decode = [f](const char* buf_ptr, const char* ptr) {
+ auto cp = uint32_t();
+ auto error = 0;
+ auto end = utf8_decode(buf_ptr, &cp, &error);
+ bool result = f(error ? invalid_code_point : cp,
+ string_view(ptr, error ? 1 : to_unsigned(end - buf_ptr)));
+ return result ? (error ? buf_ptr + 1 : end) : nullptr;
+ };
+ auto p = s.data();
+ const size_t block_size = 4; // utf8_decode always reads blocks of 4 chars.
+ if (s.size() >= block_size) {
+ for (auto end = p + s.size() - block_size + 1; p < end;) {
+ p = decode(p, p);
+ if (!p) return;
+ }
+ }
+ if (auto num_chars_left = s.data() + s.size() - p) {
+ char buf[2 * block_size - 1] = {};
+ copy_str<char>(p, p + num_chars_left, buf);
+ const char* buf_ptr = buf;
+ do {
+ auto end = decode(buf_ptr, p);
+ if (!end) return;
+ p += end - buf_ptr;
+ buf_ptr = end;
+ } while (buf_ptr - buf < num_chars_left);
+ }
+}
+
+template <typename Char>
+inline auto compute_width(basic_string_view<Char> s) -> size_t {
+ return s.size();
+}
+
+// Computes approximate display width of a UTF-8 string.
+FMT_CONSTEXPR inline auto compute_width(string_view s) -> size_t {
+ size_t num_code_points = 0;
+ // It is not a lambda for compatibility with C++14.
+ struct count_code_points {
+ size_t* count;
+ FMT_CONSTEXPR auto operator()(uint32_t cp, string_view) const -> bool {
+ *count += detail::to_unsigned(
+ 1 +
+ (cp >= 0x1100 &&
+ (cp <= 0x115f || // Hangul Jamo init. consonants
+ cp == 0x2329 || // LEFT-POINTING ANGLE BRACKET
+ cp == 0x232a || // RIGHT-POINTING ANGLE BRACKET
+ // CJK ... Yi except IDEOGRAPHIC HALF FILL SPACE:
+ (cp >= 0x2e80 && cp <= 0xa4cf && cp != 0x303f) ||
+ (cp >= 0xac00 && cp <= 0xd7a3) || // Hangul Syllables
+ (cp >= 0xf900 && cp <= 0xfaff) || // CJK Compatibility Ideographs
+ (cp >= 0xfe10 && cp <= 0xfe19) || // Vertical Forms
+ (cp >= 0xfe30 && cp <= 0xfe6f) || // CJK Compatibility Forms
+ (cp >= 0xff00 && cp <= 0xff60) || // Fullwidth Forms
+ (cp >= 0xffe0 && cp <= 0xffe6) || // Fullwidth Forms
+ (cp >= 0x20000 && cp <= 0x2fffd) || // CJK
+ (cp >= 0x30000 && cp <= 0x3fffd) ||
+ // Miscellaneous Symbols and Pictographs + Emoticons:
+ (cp >= 0x1f300 && cp <= 0x1f64f) ||
+ // Supplemental Symbols and Pictographs:
+ (cp >= 0x1f900 && cp <= 0x1f9ff))));
+ return true;
+ }
+ };
+ // We could avoid branches by using utf8_decode directly.
+ for_each_codepoint(s, count_code_points{&num_code_points});
+ return num_code_points;
+}
+
+inline auto compute_width(basic_string_view<char8_type> s) -> size_t {
+ return compute_width(
+ string_view(reinterpret_cast<const char*>(s.data()), s.size()));
+}
+
+template <typename Char>
+inline auto code_point_index(basic_string_view<Char> s, size_t n) -> size_t {
+ size_t size = s.size();
+ return n < size ? n : size;
+}
+
+// Calculates the index of the nth code point in a UTF-8 string.
+inline auto code_point_index(string_view s, size_t n) -> size_t {
+ size_t result = s.size();
+ const char* begin = s.begin();
+ for_each_codepoint(s, [begin, &n, &result](uint32_t, string_view sv) {
+ if (n != 0) {
+ --n;
+ return true;
+ }
+ result = to_unsigned(sv.begin() - begin);
+ return false;
+ });
+ return result;
+}
+
+inline auto code_point_index(basic_string_view<char8_type> s, size_t n)
+ -> size_t {
+ return code_point_index(
+ string_view(reinterpret_cast<const char*>(s.data()), s.size()), n);
+}
+
+template <typename T> struct is_integral : std::is_integral<T> {};
+template <> struct is_integral<int128_opt> : std::true_type {};
+template <> struct is_integral<uint128_t> : std::true_type {};
+
+template <typename T>
+using is_signed =
+ std::integral_constant<bool, std::numeric_limits<T>::is_signed ||
+ std::is_same<T, int128_opt>::value>;
+
+template <typename T>
+using is_integer =
+ bool_constant<is_integral<T>::value && !std::is_same<T, bool>::value &&
+ !std::is_same<T, char>::value &&
+ !std::is_same<T, wchar_t>::value>;
+
+#ifndef FMT_USE_FLOAT
+# define FMT_USE_FLOAT 1
+#endif
+#ifndef FMT_USE_DOUBLE
+# define FMT_USE_DOUBLE 1
+#endif
+#ifndef FMT_USE_LONG_DOUBLE
+# define FMT_USE_LONG_DOUBLE 1
+#endif
+
+#ifndef FMT_USE_FLOAT128
+# ifdef __clang__
+// Clang emulates GCC, so it has to appear early.
+# if FMT_HAS_INCLUDE(<quadmath.h>)
+# define FMT_USE_FLOAT128 1
+# endif
+# elif defined(__GNUC__)
+// GNU C++:
+# if defined(_GLIBCXX_USE_FLOAT128) && !defined(__STRICT_ANSI__)
+# define FMT_USE_FLOAT128 1
+# endif
+# endif
+# ifndef FMT_USE_FLOAT128
+# define FMT_USE_FLOAT128 0
+# endif
+#endif
+
+#if FMT_USE_FLOAT128
+using float128 = __float128;
+#else
+using float128 = void;
+#endif
+template <typename T> using is_float128 = std::is_same<T, float128>;
+
+template <typename T>
+using is_floating_point =
+ bool_constant<std::is_floating_point<T>::value || is_float128<T>::value>;
+
+template <typename T, bool = std::is_floating_point<T>::value>
+struct is_fast_float : bool_constant<std::numeric_limits<T>::is_iec559 &&
+ sizeof(T) <= sizeof(double)> {};
+template <typename T> struct is_fast_float<T, false> : std::false_type {};
+
+template <typename T>
+using is_double_double = bool_constant<std::numeric_limits<T>::digits == 106>;
+
+#ifndef FMT_USE_FULL_CACHE_DRAGONBOX
+# define FMT_USE_FULL_CACHE_DRAGONBOX 0
+#endif
+
+template <typename T>
+template <typename U>
+void buffer<T>::append(const U* begin, const U* end) {
+ while (begin != end) {
+ auto count = to_unsigned(end - begin);
+ try_reserve(size_ + count);
+ auto free_cap = capacity_ - size_;
+ if (free_cap < count) count = free_cap;
+ std::uninitialized_copy_n(begin, count, ptr_ + size_);
+ size_ += count;
+ begin += count;
+ }
+}
+
+template <typename T, typename Enable = void>
+struct is_locale : std::false_type {};
+template <typename T>
+struct is_locale<T, void_t<decltype(T::classic())>> : std::true_type {};
+} // namespace detail
+
+FMT_BEGIN_EXPORT
+
+// The number of characters to store in the basic_memory_buffer object itself
+// to avoid dynamic memory allocation.
+enum { inline_buffer_size = 500 };
+
+/**
+ \rst
+ A dynamically growing memory buffer for trivially copyable/constructible types
+ with the first ``SIZE`` elements stored in the object itself.
+
+ You can use the ``memory_buffer`` type alias for ``char`` instead.
+
+ **Example**::
+
+ auto out = fmt::memory_buffer();
+ fmt::format_to(std::back_inserter(out), "The answer is {}.", 42);
+
+ This will append the following output to the ``out`` object:
+
+ .. code-block:: none
+
+ The answer is 42.
+
+ The output can be converted to an ``std::string`` with ``to_string(out)``.
+ \endrst
+ */
+template <typename T, size_t SIZE = inline_buffer_size,
+ typename Allocator = std::allocator<T>>
+class basic_memory_buffer final : public detail::buffer<T> {
+ private:
+ T store_[SIZE];
+
+ // Don't inherit from Allocator to avoid generating type_info for it.
+ FMT_NO_UNIQUE_ADDRESS Allocator alloc_;
+
+ // Deallocate memory allocated by the buffer.
+ FMT_CONSTEXPR20 void deallocate() {
+ T* data = this->data();
+ if (data != store_) alloc_.deallocate(data, this->capacity());
+ }
+
+ protected:
+ FMT_CONSTEXPR20 void grow(size_t size) override {
+ detail::abort_fuzzing_if(size > 5000);
+ const size_t max_size = std::allocator_traits<Allocator>::max_size(alloc_);
+ size_t old_capacity = this->capacity();
+ size_t new_capacity = old_capacity + old_capacity / 2;
+ if (size > new_capacity)
+ new_capacity = size;
+ else if (new_capacity > max_size)
+ new_capacity = size > max_size ? size : max_size;
+ T* old_data = this->data();
+ T* new_data =
+ std::allocator_traits<Allocator>::allocate(alloc_, new_capacity);
+ // Suppress a bogus -Wstringop-overflow in gcc 13.1 (#3481).
+ detail::assume(this->size() <= new_capacity);
+ // The following code doesn't throw, so the raw pointer above doesn't leak.
+ std::uninitialized_copy_n(old_data, this->size(), new_data);
+ this->set(new_data, new_capacity);
+ // deallocate must not throw according to the standard, but even if it does,
+ // the buffer already uses the new storage and will deallocate it in
+ // destructor.
+ if (old_data != store_) alloc_.deallocate(old_data, old_capacity);
+ }
+
+ public:
+ using value_type = T;
+ using const_reference = const T&;
+
+ FMT_CONSTEXPR20 explicit basic_memory_buffer(
+ const Allocator& alloc = Allocator())
+ : alloc_(alloc) {
+ this->set(store_, SIZE);
+ if (detail::is_constant_evaluated()) detail::fill_n(store_, SIZE, T());
+ }
+ FMT_CONSTEXPR20 ~basic_memory_buffer() { deallocate(); }
+
+ private:
+ // Move data from other to this buffer.
+ FMT_CONSTEXPR20 void move(basic_memory_buffer& other) {
+ alloc_ = std::move(other.alloc_);
+ T* data = other.data();
+ size_t size = other.size(), capacity = other.capacity();
+ if (data == other.store_) {
+ this->set(store_, capacity);
+ detail::copy_str<T>(other.store_, other.store_ + size, store_);
+ } else {
+ this->set(data, capacity);
+ // Set pointer to the inline array so that delete is not called
+ // when deallocating.
+ other.set(other.store_, 0);
+ other.clear();
+ }
+ this->resize(size);
+ }
+
+ public:
+ /**
+ \rst
+ Constructs a :class:`fmt::basic_memory_buffer` object moving the content
+ of the other object to it.
+ \endrst
+ */
+ FMT_CONSTEXPR20 basic_memory_buffer(basic_memory_buffer&& other) noexcept {
+ move(other);
+ }
+
+ /**
+ \rst
+ Moves the content of the other ``basic_memory_buffer`` object to this one.
+ \endrst
+ */
+ auto operator=(basic_memory_buffer&& other) noexcept -> basic_memory_buffer& {
+ FMT_ASSERT(this != &other, "");
+ deallocate();
+ move(other);
+ return *this;
+ }
+
+ // Returns a copy of the allocator associated with this buffer.
+ auto get_allocator() const -> Allocator { return alloc_; }
+
+ /**
+ Resizes the buffer to contain *count* elements. If T is a POD type new
+ elements may not be initialized.
+ */
+ FMT_CONSTEXPR20 void resize(size_t count) { this->try_resize(count); }
+
+ /** Increases the buffer capacity to *new_capacity*. */
+ void reserve(size_t new_capacity) { this->try_reserve(new_capacity); }
+
+ using detail::buffer<T>::append;
+ template <typename ContiguousRange>
+ void append(const ContiguousRange& range) {
+ append(range.data(), range.data() + range.size());
+ }
+};
+
+using memory_buffer = basic_memory_buffer<char>;
+
+template <typename T, size_t SIZE, typename Allocator>
+struct is_contiguous<basic_memory_buffer<T, SIZE, Allocator>> : std::true_type {
+};
+
+FMT_END_EXPORT
+namespace detail {
+FMT_API auto write_console(int fd, string_view text) -> bool;
+FMT_API auto write_console(std::FILE* f, string_view text) -> bool;
+FMT_API void print(std::FILE*, string_view);
+} // namespace detail
+
+FMT_BEGIN_EXPORT
+
+// Suppress a misleading warning in older versions of clang.
+#if FMT_CLANG_VERSION
+# pragma clang diagnostic ignored "-Wweak-vtables"
+#endif
+
+/** An error reported from a formatting function. */
+class FMT_SO_VISIBILITY("default") format_error : public std::runtime_error {
+ public:
+ using std::runtime_error::runtime_error;
+};
+
+namespace detail_exported {
+#if FMT_USE_NONTYPE_TEMPLATE_ARGS
+template <typename Char, size_t N> struct fixed_string {
+ constexpr fixed_string(const Char (&str)[N]) {
+ detail::copy_str<Char, const Char*, Char*>(static_cast<const Char*>(str),
+ str + N, data);
+ }
+ Char data[N] = {};
+};
+#endif
+
+// Converts a compile-time string to basic_string_view.
+template <typename Char, size_t N>
+constexpr auto compile_string_to_view(const Char (&s)[N])
+ -> basic_string_view<Char> {
+ // Remove trailing NUL character if needed. Won't be present if this is used
+ // with a raw character array (i.e. not defined as a string).
+ return {s, N - (std::char_traits<Char>::to_int_type(s[N - 1]) == 0 ? 1 : 0)};
+}
+template <typename Char>
+constexpr auto compile_string_to_view(detail::std_string_view<Char> s)
+ -> basic_string_view<Char> {
+ return {s.data(), s.size()};
+}
+} // namespace detail_exported
+
+class loc_value {
+ private:
+ basic_format_arg<format_context> value_;
+
+ public:
+ template <typename T, FMT_ENABLE_IF(!detail::is_float128<T>::value)>
+ loc_value(T value) : value_(detail::make_arg<format_context>(value)) {}
+
+ template <typename T, FMT_ENABLE_IF(detail::is_float128<T>::value)>
+ loc_value(T) {}
+
+ template <typename Visitor> auto visit(Visitor&& vis) -> decltype(vis(0)) {
+ return visit_format_arg(vis, value_);
+ }
+};
+
+// A locale facet that formats values in UTF-8.
+// It is parameterized on the locale to avoid the heavy <locale> include.
+template <typename Locale> class format_facet : public Locale::facet {
+ private:
+ std::string separator_;
+ std::string grouping_;
+ std::string decimal_point_;
+
+ protected:
+ virtual auto do_put(appender out, loc_value val,
+ const format_specs<>& specs) const -> bool;
+
+ public:
+ static FMT_API typename Locale::id id;
+
+ explicit format_facet(Locale& loc);
+ explicit format_facet(string_view sep = "",
+ std::initializer_list<unsigned char> g = {3},
+ std::string decimal_point = ".")
+ : separator_(sep.data(), sep.size()),
+ grouping_(g.begin(), g.end()),
+ decimal_point_(decimal_point) {}
+
+ auto put(appender out, loc_value val, const format_specs<>& specs) const
+ -> bool {
+ return do_put(out, val, specs);
+ }
+};
+
+namespace detail {
+
+// Returns true if value is negative, false otherwise.
+// Same as `value < 0` but doesn't produce warnings if T is an unsigned type.
+template <typename T, FMT_ENABLE_IF(is_signed<T>::value)>
+constexpr auto is_negative(T value) -> bool {
+ return value < 0;
+}
+template <typename T, FMT_ENABLE_IF(!is_signed<T>::value)>
+constexpr auto is_negative(T) -> bool {
+ return false;
+}
+
+template <typename T>
+FMT_CONSTEXPR auto is_supported_floating_point(T) -> bool {
+ if (std::is_same<T, float>()) return FMT_USE_FLOAT;
+ if (std::is_same<T, double>()) return FMT_USE_DOUBLE;
+ if (std::is_same<T, long double>()) return FMT_USE_LONG_DOUBLE;
+ return true;
+}
+
+// Smallest of uint32_t, uint64_t, uint128_t that is large enough to
+// represent all values of an integral type T.
+template <typename T>
+using uint32_or_64_or_128_t =
+ conditional_t<num_bits<T>() <= 32 && !FMT_REDUCE_INT_INSTANTIATIONS,
+ uint32_t,
+ conditional_t<num_bits<T>() <= 64, uint64_t, uint128_t>>;
+template <typename T>
+using uint64_or_128_t = conditional_t<num_bits<T>() <= 64, uint64_t, uint128_t>;
+
+#define FMT_POWERS_OF_10(factor) \
+ factor * 10, (factor) * 100, (factor) * 1000, (factor) * 10000, \
+ (factor) * 100000, (factor) * 1000000, (factor) * 10000000, \
+ (factor) * 100000000, (factor) * 1000000000
+
+// Converts value in the range [0, 100) to a string.
+constexpr auto digits2(size_t value) -> const char* {
+ // GCC generates slightly better code when value is pointer-size.
+ return &"0001020304050607080910111213141516171819"
+ "2021222324252627282930313233343536373839"
+ "4041424344454647484950515253545556575859"
+ "6061626364656667686970717273747576777879"
+ "8081828384858687888990919293949596979899"[value * 2];
+}
+
+// Sign is a template parameter to workaround a bug in gcc 4.8.
+template <typename Char, typename Sign> constexpr auto sign(Sign s) -> Char {
+#if !FMT_GCC_VERSION || FMT_GCC_VERSION >= 604
+ static_assert(std::is_same<Sign, sign_t>::value, "");
+#endif
+ return static_cast<Char>("\0-+ "[s]);
+}
+
+template <typename T> FMT_CONSTEXPR auto count_digits_fallback(T n) -> int {
+ int count = 1;
+ for (;;) {
+ // Integer division is slow so do it for a group of four digits instead
+ // of for every digit. The idea comes from the talk by Alexandrescu
+ // "Three Optimization Tips for C++". See speed-test for a comparison.
+ if (n < 10) return count;
+ if (n < 100) return count + 1;
+ if (n < 1000) return count + 2;
+ if (n < 10000) return count + 3;
+ n /= 10000u;
+ count += 4;
+ }
+}
+#if FMT_USE_INT128
+FMT_CONSTEXPR inline auto count_digits(uint128_opt n) -> int {
+ return count_digits_fallback(n);
+}
+#endif
+
+#ifdef FMT_BUILTIN_CLZLL
+// It is a separate function rather than a part of count_digits to workaround
+// the lack of static constexpr in constexpr functions.
+inline auto do_count_digits(uint64_t n) -> int {
+ // This has comparable performance to the version by Kendall Willets
+ // (https://github.com/fmtlib/format-benchmark/blob/master/digits10)
+ // but uses smaller tables.
+ // Maps bsr(n) to ceil(log10(pow(2, bsr(n) + 1) - 1)).
+ static constexpr uint8_t bsr2log10[] = {
+ 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5,
+ 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10,
+ 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 15, 15,
+ 15, 16, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19, 19, 19, 20};
+ auto t = bsr2log10[FMT_BUILTIN_CLZLL(n | 1) ^ 63];
+ static constexpr const uint64_t zero_or_powers_of_10[] = {
+ 0, 0, FMT_POWERS_OF_10(1U), FMT_POWERS_OF_10(1000000000ULL),
+ 10000000000000000000ULL};
+ return t - (n < zero_or_powers_of_10[t]);
+}
+#endif
+
+// Returns the number of decimal digits in n. Leading zeros are not counted
+// except for n == 0 in which case count_digits returns 1.
+FMT_CONSTEXPR20 inline auto count_digits(uint64_t n) -> int {
+#ifdef FMT_BUILTIN_CLZLL
+ if (!is_constant_evaluated()) {
+ return do_count_digits(n);
+ }
+#endif
+ return count_digits_fallback(n);
+}
+
+// Counts the number of digits in n. BITS = log2(radix).
+template <int BITS, typename UInt>
+FMT_CONSTEXPR auto count_digits(UInt n) -> int {
+#ifdef FMT_BUILTIN_CLZ
+ if (!is_constant_evaluated() && num_bits<UInt>() == 32)
+ return (FMT_BUILTIN_CLZ(static_cast<uint32_t>(n) | 1) ^ 31) / BITS + 1;
+#endif
+ // Lambda avoids unreachable code warnings from NVHPC.
+ return [](UInt m) {
+ int num_digits = 0;
+ do {
+ ++num_digits;
+ } while ((m >>= BITS) != 0);
+ return num_digits;
+ }(n);
+}
+
+#ifdef FMT_BUILTIN_CLZ
+// It is a separate function rather than a part of count_digits to workaround
+// the lack of static constexpr in constexpr functions.
+FMT_INLINE auto do_count_digits(uint32_t n) -> int {
+// An optimization by Kendall Willets from https://bit.ly/3uOIQrB.
+// This increments the upper 32 bits (log10(T) - 1) when >= T is added.
+# define FMT_INC(T) (((sizeof(#T) - 1ull) << 32) - T)
+ static constexpr uint64_t table[] = {
+ FMT_INC(0), FMT_INC(0), FMT_INC(0), // 8
+ FMT_INC(10), FMT_INC(10), FMT_INC(10), // 64
+ FMT_INC(100), FMT_INC(100), FMT_INC(100), // 512
+ FMT_INC(1000), FMT_INC(1000), FMT_INC(1000), // 4096
+ FMT_INC(10000), FMT_INC(10000), FMT_INC(10000), // 32k
+ FMT_INC(100000), FMT_INC(100000), FMT_INC(100000), // 256k
+ FMT_INC(1000000), FMT_INC(1000000), FMT_INC(1000000), // 2048k
+ FMT_INC(10000000), FMT_INC(10000000), FMT_INC(10000000), // 16M
+ FMT_INC(100000000), FMT_INC(100000000), FMT_INC(100000000), // 128M
+ FMT_INC(1000000000), FMT_INC(1000000000), FMT_INC(1000000000), // 1024M
+ FMT_INC(1000000000), FMT_INC(1000000000) // 4B
+ };
+ auto inc = table[FMT_BUILTIN_CLZ(n | 1) ^ 31];
+ return static_cast<int>((n + inc) >> 32);
+}
+#endif
+
+// Optional version of count_digits for better performance on 32-bit platforms.
+FMT_CONSTEXPR20 inline auto count_digits(uint32_t n) -> int {
+#ifdef FMT_BUILTIN_CLZ
+ if (!is_constant_evaluated()) {
+ return do_count_digits(n);
+ }
+#endif
+ return count_digits_fallback(n);
+}
+
+template <typename Int> constexpr auto digits10() noexcept -> int {
+ return std::numeric_limits<Int>::digits10;
+}
+template <> constexpr auto digits10<int128_opt>() noexcept -> int { return 38; }
+template <> constexpr auto digits10<uint128_t>() noexcept -> int { return 38; }
+
+template <typename Char> struct thousands_sep_result {
+ std::string grouping;
+ Char thousands_sep;
+};
+
+template <typename Char>
+FMT_API auto thousands_sep_impl(locale_ref loc) -> thousands_sep_result<Char>;
+template <typename Char>
+inline auto thousands_sep(locale_ref loc) -> thousands_sep_result<Char> {
+ auto result = thousands_sep_impl<char>(loc);
+ return {result.grouping, Char(result.thousands_sep)};
+}
+template <>
+inline auto thousands_sep(locale_ref loc) -> thousands_sep_result<wchar_t> {
+ return thousands_sep_impl<wchar_t>(loc);
+}
+
+template <typename Char>
+FMT_API auto decimal_point_impl(locale_ref loc) -> Char;
+template <typename Char> inline auto decimal_point(locale_ref loc) -> Char {
+ return Char(decimal_point_impl<char>(loc));
+}
+template <> inline auto decimal_point(locale_ref loc) -> wchar_t {
+ return decimal_point_impl<wchar_t>(loc);
+}
+
+// Compares two characters for equality.
+template <typename Char> auto equal2(const Char* lhs, const char* rhs) -> bool {
+ return lhs[0] == Char(rhs[0]) && lhs[1] == Char(rhs[1]);
+}
+inline auto equal2(const char* lhs, const char* rhs) -> bool {
+ return memcmp(lhs, rhs, 2) == 0;
+}
+
+// Copies two characters from src to dst.
+template <typename Char>
+FMT_CONSTEXPR20 FMT_INLINE void copy2(Char* dst, const char* src) {
+ if (!is_constant_evaluated() && sizeof(Char) == sizeof(char)) {
+ memcpy(dst, src, 2);
+ return;
+ }
+ *dst++ = static_cast<Char>(*src++);
+ *dst = static_cast<Char>(*src);
+}
+
+template <typename Iterator> struct format_decimal_result {
+ Iterator begin;
+ Iterator end;
+};
+
+// Formats a decimal unsigned integer value writing into out pointing to a
+// buffer of specified size. The caller must ensure that the buffer is large
+// enough.
+template <typename Char, typename UInt>
+FMT_CONSTEXPR20 auto format_decimal(Char* out, UInt value, int size)
+ -> format_decimal_result<Char*> {
+ FMT_ASSERT(size >= count_digits(value), "invalid digit count");
+ out += size;
+ Char* end = out;
+ while (value >= 100) {
+ // Integer division is slow so do it for a group of two digits instead
+ // of for every digit. The idea comes from the talk by Alexandrescu
+ // "Three Optimization Tips for C++". See speed-test for a comparison.
+ out -= 2;
+ copy2(out, digits2(static_cast<size_t>(value % 100)));
+ value /= 100;
+ }
+ if (value < 10) {
+ *--out = static_cast<Char>('0' + value);
+ return {out, end};
+ }
+ out -= 2;
+ copy2(out, digits2(static_cast<size_t>(value)));
+ return {out, end};
+}
+
+template <typename Char, typename UInt, typename Iterator,
+ FMT_ENABLE_IF(!std::is_pointer<remove_cvref_t<Iterator>>::value)>
+FMT_CONSTEXPR inline auto format_decimal(Iterator out, UInt value, int size)
+ -> format_decimal_result<Iterator> {
+ // Buffer is large enough to hold all digits (digits10 + 1).
+ Char buffer[digits10<UInt>() + 1] = {};
+ auto end = format_decimal(buffer, value, size).end;
+ return {out, detail::copy_str_noinline<Char>(buffer, end, out)};
+}
+
+template <unsigned BASE_BITS, typename Char, typename UInt>
+FMT_CONSTEXPR auto format_uint(Char* buffer, UInt value, int num_digits,
+ bool upper = false) -> Char* {
+ buffer += num_digits;
+ Char* end = buffer;
+ do {
+ const char* digits = upper ? "0123456789ABCDEF" : "0123456789abcdef";
+ unsigned digit = static_cast<unsigned>(value & ((1 << BASE_BITS) - 1));
+ *--buffer = static_cast<Char>(BASE_BITS < 4 ? static_cast<char>('0' + digit)
+ : digits[digit]);
+ } while ((value >>= BASE_BITS) != 0);
+ return end;
+}
+
+template <unsigned BASE_BITS, typename Char, typename It, typename UInt>
+FMT_CONSTEXPR inline auto format_uint(It out, UInt value, int num_digits,
+ bool upper = false) -> It {
+ if (auto ptr = to_pointer<Char>(out, to_unsigned(num_digits))) {
+ format_uint<BASE_BITS>(ptr, value, num_digits, upper);
+ return out;
+ }
+ // Buffer should be large enough to hold all digits (digits / BASE_BITS + 1).
+ char buffer[num_bits<UInt>() / BASE_BITS + 1] = {};
+ format_uint<BASE_BITS>(buffer, value, num_digits, upper);
+ return detail::copy_str_noinline<Char>(buffer, buffer + num_digits, out);
+}
+
+// A converter from UTF-8 to UTF-16.
+class utf8_to_utf16 {
+ private:
+ basic_memory_buffer<wchar_t> buffer_;
+
+ public:
+ FMT_API explicit utf8_to_utf16(string_view s);
+ operator basic_string_view<wchar_t>() const { return {&buffer_[0], size()}; }
+ auto size() const -> size_t { return buffer_.size() - 1; }
+ auto c_str() const -> const wchar_t* { return &buffer_[0]; }
+ auto str() const -> std::wstring { return {&buffer_[0], size()}; }
+};
+
+enum class to_utf8_error_policy { abort, replace };
+
+// A converter from UTF-16/UTF-32 (host endian) to UTF-8.
+template <typename WChar, typename Buffer = memory_buffer> class to_utf8 {
+ private:
+ Buffer buffer_;
+
+ public:
+ to_utf8() {}
+ explicit to_utf8(basic_string_view<WChar> s,
+ to_utf8_error_policy policy = to_utf8_error_policy::abort) {
+ static_assert(sizeof(WChar) == 2 || sizeof(WChar) == 4,
+ "Expect utf16 or utf32");
+ if (!convert(s, policy))
+ FMT_THROW(std::runtime_error(sizeof(WChar) == 2 ? "invalid utf16"
+ : "invalid utf32"));
+ }
+ operator string_view() const { return string_view(&buffer_[0], size()); }
+ auto size() const -> size_t { return buffer_.size() - 1; }
+ auto c_str() const -> const char* { return &buffer_[0]; }
+ auto str() const -> std::string { return std::string(&buffer_[0], size()); }
+
+ // Performs conversion returning a bool instead of throwing exception on
+ // conversion error. This method may still throw in case of memory allocation
+ // error.
+ auto convert(basic_string_view<WChar> s,
+ to_utf8_error_policy policy = to_utf8_error_policy::abort)
+ -> bool {
+ if (!convert(buffer_, s, policy)) return false;
+ buffer_.push_back(0);
+ return true;
+ }
+ static auto convert(Buffer& buf, basic_string_view<WChar> s,
+ to_utf8_error_policy policy = to_utf8_error_policy::abort)
+ -> bool {
+ for (auto p = s.begin(); p != s.end(); ++p) {
+ uint32_t c = static_cast<uint32_t>(*p);
+ if (sizeof(WChar) == 2 && c >= 0xd800 && c <= 0xdfff) {
+ // Handle a surrogate pair.
+ ++p;
+ if (p == s.end() || (c & 0xfc00) != 0xd800 || (*p & 0xfc00) != 0xdc00) {
+ if (policy == to_utf8_error_policy::abort) return false;
+ buf.append(string_view("\xEF\xBF\xBD"));
+ --p;
+ } else {
+ c = (c << 10) + static_cast<uint32_t>(*p) - 0x35fdc00;
+ }
+ } else if (c < 0x80) {
+ buf.push_back(static_cast<char>(c));
+ } else if (c < 0x800) {
+ buf.push_back(static_cast<char>(0xc0 | (c >> 6)));
+ buf.push_back(static_cast<char>(0x80 | (c & 0x3f)));
+ } else if ((c >= 0x800 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xffff)) {
+ buf.push_back(static_cast<char>(0xe0 | (c >> 12)));
+ buf.push_back(static_cast<char>(0x80 | ((c & 0xfff) >> 6)));
+ buf.push_back(static_cast<char>(0x80 | (c & 0x3f)));
+ } else if (c >= 0x10000 && c <= 0x10ffff) {
+ buf.push_back(static_cast<char>(0xf0 | (c >> 18)));
+ buf.push_back(static_cast<char>(0x80 | ((c & 0x3ffff) >> 12)));
+ buf.push_back(static_cast<char>(0x80 | ((c & 0xfff) >> 6)));
+ buf.push_back(static_cast<char>(0x80 | (c & 0x3f)));
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+};
+
+// Computes 128-bit result of multiplication of two 64-bit unsigned integers.
+inline auto umul128(uint64_t x, uint64_t y) noexcept -> uint128_fallback {
+#if FMT_USE_INT128
+ auto p = static_cast<uint128_opt>(x) * static_cast<uint128_opt>(y);
+ return {static_cast<uint64_t>(p >> 64), static_cast<uint64_t>(p)};
+#elif defined(_MSC_VER) && defined(_M_X64)
+ auto hi = uint64_t();
+ auto lo = _umul128(x, y, &hi);
+ return {hi, lo};
+#else
+ const uint64_t mask = static_cast<uint64_t>(max_value<uint32_t>());
+
+ uint64_t a = x >> 32;
+ uint64_t b = x & mask;
+ uint64_t c = y >> 32;
+ uint64_t d = y & mask;
+
+ uint64_t ac = a * c;
+ uint64_t bc = b * c;
+ uint64_t ad = a * d;
+ uint64_t bd = b * d;
+
+ uint64_t intermediate = (bd >> 32) + (ad & mask) + (bc & mask);
+
+ return {ac + (intermediate >> 32) + (ad >> 32) + (bc >> 32),
+ (intermediate << 32) + (bd & mask)};
+#endif
+}
+
+namespace dragonbox {
+// Computes floor(log10(pow(2, e))) for e in [-2620, 2620] using the method from
+// https://fmt.dev/papers/Dragonbox.pdf#page=28, section 6.1.
+inline auto floor_log10_pow2(int e) noexcept -> int {
+ FMT_ASSERT(e <= 2620 && e >= -2620, "too large exponent");
+ static_assert((-1 >> 1) == -1, "right shift is not arithmetic");
+ return (e * 315653) >> 20;
+}
+
+inline auto floor_log2_pow10(int e) noexcept -> int {
+ FMT_ASSERT(e <= 1233 && e >= -1233, "too large exponent");
+ return (e * 1741647) >> 19;
+}
+
+// Computes upper 64 bits of multiplication of two 64-bit unsigned integers.
+inline auto umul128_upper64(uint64_t x, uint64_t y) noexcept -> uint64_t {
+#if FMT_USE_INT128
+ auto p = static_cast<uint128_opt>(x) * static_cast<uint128_opt>(y);
+ return static_cast<uint64_t>(p >> 64);
+#elif defined(_MSC_VER) && defined(_M_X64)
+ return __umulh(x, y);
+#else
+ return umul128(x, y).high();
+#endif
+}
+
+// Computes upper 128 bits of multiplication of a 64-bit unsigned integer and a
+// 128-bit unsigned integer.
+inline auto umul192_upper128(uint64_t x, uint128_fallback y) noexcept
+ -> uint128_fallback {
+ uint128_fallback r = umul128(x, y.high());
+ r += umul128_upper64(x, y.low());
+ return r;
+}
+
+FMT_API auto get_cached_power(int k) noexcept -> uint128_fallback;
+
+// Type-specific information that Dragonbox uses.
+template <typename T, typename Enable = void> struct float_info;
+
+template <> struct float_info<float> {
+ using carrier_uint = uint32_t;
+ static const int exponent_bits = 8;
+ static const int kappa = 1;
+ static const int big_divisor = 100;
+ static const int small_divisor = 10;
+ static const int min_k = -31;
+ static const int max_k = 46;
+ static const int shorter_interval_tie_lower_threshold = -35;
+ static const int shorter_interval_tie_upper_threshold = -35;
+};
+
+template <> struct float_info<double> {
+ using carrier_uint = uint64_t;
+ static const int exponent_bits = 11;
+ static const int kappa = 2;
+ static const int big_divisor = 1000;
+ static const int small_divisor = 100;
+ static const int min_k = -292;
+ static const int max_k = 341;
+ static const int shorter_interval_tie_lower_threshold = -77;
+ static const int shorter_interval_tie_upper_threshold = -77;
+};
+
+// An 80- or 128-bit floating point number.
+template <typename T>
+struct float_info<T, enable_if_t<std::numeric_limits<T>::digits == 64 ||
+ std::numeric_limits<T>::digits == 113 ||
+ is_float128<T>::value>> {
+ using carrier_uint = detail::uint128_t;
+ static const int exponent_bits = 15;
+};
+
+// A double-double floating point number.
+template <typename T>
+struct float_info<T, enable_if_t<is_double_double<T>::value>> {
+ using carrier_uint = detail::uint128_t;
+};
+
+template <typename T> struct decimal_fp {
+ using significand_type = typename float_info<T>::carrier_uint;
+ significand_type significand;
+ int exponent;
+};
+
+template <typename T> FMT_API auto to_decimal(T x) noexcept -> decimal_fp<T>;
+} // namespace dragonbox
+
+// Returns true iff Float has the implicit bit which is not stored.
+template <typename Float> constexpr auto has_implicit_bit() -> bool {
+ // An 80-bit FP number has a 64-bit significand an no implicit bit.
+ return std::numeric_limits<Float>::digits != 64;
+}
+
+// Returns the number of significand bits stored in Float. The implicit bit is
+// not counted since it is not stored.
+template <typename Float> constexpr auto num_significand_bits() -> int {
+ // std::numeric_limits may not support __float128.
+ return is_float128<Float>() ? 112
+ : (std::numeric_limits<Float>::digits -
+ (has_implicit_bit<Float>() ? 1 : 0));
+}
+
+template <typename Float>
+constexpr auto exponent_mask() ->
+ typename dragonbox::float_info<Float>::carrier_uint {
+ using float_uint = typename dragonbox::float_info<Float>::carrier_uint;
+ return ((float_uint(1) << dragonbox::float_info<Float>::exponent_bits) - 1)
+ << num_significand_bits<Float>();
+}
+template <typename Float> constexpr auto exponent_bias() -> int {
+ // std::numeric_limits may not support __float128.
+ return is_float128<Float>() ? 16383
+ : std::numeric_limits<Float>::max_exponent - 1;
+}
+
+// Writes the exponent exp in the form "[+-]d{2,3}" to buffer.
+template <typename Char, typename It>
+FMT_CONSTEXPR auto write_exponent(int exp, It it) -> It {
+ FMT_ASSERT(-10000 < exp && exp < 10000, "exponent out of range");
+ if (exp < 0) {
+ *it++ = static_cast<Char>('-');
+ exp = -exp;
+ } else {
+ *it++ = static_cast<Char>('+');
+ }
+ if (exp >= 100) {
+ const char* top = digits2(to_unsigned(exp / 100));
+ if (exp >= 1000) *it++ = static_cast<Char>(top[0]);
+ *it++ = static_cast<Char>(top[1]);
+ exp %= 100;
+ }
+ const char* d = digits2(to_unsigned(exp));
+ *it++ = static_cast<Char>(d[0]);
+ *it++ = static_cast<Char>(d[1]);
+ return it;
+}
+
+// A floating-point number f * pow(2, e) where F is an unsigned type.
+template <typename F> struct basic_fp {
+ F f;
+ int e;
+
+ static constexpr const int num_significand_bits =
+ static_cast<int>(sizeof(F) * num_bits<unsigned char>());
+
+ constexpr basic_fp() : f(0), e(0) {}
+ constexpr basic_fp(uint64_t f_val, int e_val) : f(f_val), e(e_val) {}
+
+ // Constructs fp from an IEEE754 floating-point number.
+ template <typename Float> FMT_CONSTEXPR basic_fp(Float n) { assign(n); }
+
+ // Assigns n to this and return true iff predecessor is closer than successor.
+ template <typename Float, FMT_ENABLE_IF(!is_double_double<Float>::value)>
+ FMT_CONSTEXPR auto assign(Float n) -> bool {
+ static_assert(std::numeric_limits<Float>::digits <= 113, "unsupported FP");
+ // Assume Float is in the format [sign][exponent][significand].
+ using carrier_uint = typename dragonbox::float_info<Float>::carrier_uint;
+ const auto num_float_significand_bits =
+ detail::num_significand_bits<Float>();
+ const auto implicit_bit = carrier_uint(1) << num_float_significand_bits;
+ const auto significand_mask = implicit_bit - 1;
+ auto u = bit_cast<carrier_uint>(n);
+ f = static_cast<F>(u & significand_mask);
+ auto biased_e = static_cast<int>((u & exponent_mask<Float>()) >>
+ num_float_significand_bits);
+ // The predecessor is closer if n is a normalized power of 2 (f == 0)
+ // other than the smallest normalized number (biased_e > 1).
+ auto is_predecessor_closer = f == 0 && biased_e > 1;
+ if (biased_e == 0)
+ biased_e = 1; // Subnormals use biased exponent 1 (min exponent).
+ else if (has_implicit_bit<Float>())
+ f += static_cast<F>(implicit_bit);
+ e = biased_e - exponent_bias<Float>() - num_float_significand_bits;
+ if (!has_implicit_bit<Float>()) ++e;
+ return is_predecessor_closer;
+ }
+
+ template <typename Float, FMT_ENABLE_IF(is_double_double<Float>::value)>
+ FMT_CONSTEXPR auto assign(Float n) -> bool {
+ static_assert(std::numeric_limits<double>::is_iec559, "unsupported FP");
+ return assign(static_cast<double>(n));
+ }
+};
+
+using fp = basic_fp<unsigned long long>;
+
+// Normalizes the value converted from double and multiplied by (1 << SHIFT).
+template <int SHIFT = 0, typename F>
+FMT_CONSTEXPR auto normalize(basic_fp<F> value) -> basic_fp<F> {
+ // Handle subnormals.
+ const auto implicit_bit = F(1) << num_significand_bits<double>();
+ const auto shifted_implicit_bit = implicit_bit << SHIFT;
+ while ((value.f & shifted_implicit_bit) == 0) {
+ value.f <<= 1;
+ --value.e;
+ }
+ // Subtract 1 to account for hidden bit.
+ const auto offset = basic_fp<F>::num_significand_bits -
+ num_significand_bits<double>() - SHIFT - 1;
+ value.f <<= offset;
+ value.e -= offset;
+ return value;
+}
+
+// Computes lhs * rhs / pow(2, 64) rounded to nearest with half-up tie breaking.
+FMT_CONSTEXPR inline auto multiply(uint64_t lhs, uint64_t rhs) -> uint64_t {
+#if FMT_USE_INT128
+ auto product = static_cast<__uint128_t>(lhs) * rhs;
+ auto f = static_cast<uint64_t>(product >> 64);
+ return (static_cast<uint64_t>(product) & (1ULL << 63)) != 0 ? f + 1 : f;
+#else
+ // Multiply 32-bit parts of significands.
+ uint64_t mask = (1ULL << 32) - 1;
+ uint64_t a = lhs >> 32, b = lhs & mask;
+ uint64_t c = rhs >> 32, d = rhs & mask;
+ uint64_t ac = a * c, bc = b * c, ad = a * d, bd = b * d;
+ // Compute mid 64-bit of result and round.
+ uint64_t mid = (bd >> 32) + (ad & mask) + (bc & mask) + (1U << 31);
+ return ac + (ad >> 32) + (bc >> 32) + (mid >> 32);
+#endif
+}
+
+FMT_CONSTEXPR inline auto operator*(fp x, fp y) -> fp {
+ return {multiply(x.f, y.f), x.e + y.e + 64};
+}
+
+template <typename T, bool doublish = num_bits<T>() == num_bits<double>()>
+using convert_float_result =
+ conditional_t<std::is_same<T, float>::value || doublish, double, T>;
+
+template <typename T>
+constexpr auto convert_float(T value) -> convert_float_result<T> {
+ return static_cast<convert_float_result<T>>(value);
+}
+
+template <typename OutputIt, typename Char>
+FMT_NOINLINE FMT_CONSTEXPR auto fill(OutputIt it, size_t n,
+ const fill_t<Char>& fill) -> OutputIt {
+ auto fill_size = fill.size();
+ if (fill_size == 1) return detail::fill_n(it, n, fill[0]);
+ auto data = fill.data();
+ for (size_t i = 0; i < n; ++i)
+ it = copy_str<Char>(data, data + fill_size, it);
+ return it;
+}
+
+// Writes the output of f, padded according to format specifications in specs.
+// size: output size in code units.
+// width: output display width in (terminal) column positions.
+template <align::type align = align::left, typename OutputIt, typename Char,
+ typename F>
+FMT_CONSTEXPR auto write_padded(OutputIt out, const format_specs<Char>& specs,
+ size_t size, size_t width, F&& f) -> OutputIt {
+ static_assert(align == align::left || align == align::right, "");
+ unsigned spec_width = to_unsigned(specs.width);
+ size_t padding = spec_width > width ? spec_width - width : 0;
+ // Shifts are encoded as string literals because static constexpr is not
+ // supported in constexpr functions.
+ auto* shifts = align == align::left ? "\x1f\x1f\x00\x01" : "\x00\x1f\x00\x01";
+ size_t left_padding = padding >> shifts[specs.align];
+ size_t right_padding = padding - left_padding;
+ auto it = reserve(out, size + padding * specs.fill.size());
+ if (left_padding != 0) it = fill(it, left_padding, specs.fill);
+ it = f(it);
+ if (right_padding != 0) it = fill(it, right_padding, specs.fill);
+ return base_iterator(out, it);
+}
+
+template <align::type align = align::left, typename OutputIt, typename Char,
+ typename F>
+constexpr auto write_padded(OutputIt out, const format_specs<Char>& specs,
+ size_t size, F&& f) -> OutputIt {
+ return write_padded<align>(out, specs, size, size, f);
+}
+
+template <align::type align = align::left, typename Char, typename OutputIt>
+FMT_CONSTEXPR auto write_bytes(OutputIt out, string_view bytes,
+ const format_specs<Char>& specs) -> OutputIt {
+ return write_padded<align>(
+ out, specs, bytes.size(), [bytes](reserve_iterator<OutputIt> it) {
+ const char* data = bytes.data();
+ return copy_str<Char>(data, data + bytes.size(), it);
+ });
+}
+
+template <typename Char, typename OutputIt, typename UIntPtr>
+auto write_ptr(OutputIt out, UIntPtr value, const format_specs<Char>* specs)
+ -> OutputIt {
+ int num_digits = count_digits<4>(value);
+ auto size = to_unsigned(num_digits) + size_t(2);
+ auto write = [=](reserve_iterator<OutputIt> it) {
+ *it++ = static_cast<Char>('0');
+ *it++ = static_cast<Char>('x');
+ return format_uint<4, Char>(it, value, num_digits);
+ };
+ return specs ? write_padded<align::right>(out, *specs, size, write)
+ : base_iterator(out, write(reserve(out, size)));
+}
+
+// Returns true iff the code point cp is printable.
+FMT_API auto is_printable(uint32_t cp) -> bool;
+
+inline auto needs_escape(uint32_t cp) -> bool {
+ return cp < 0x20 || cp == 0x7f || cp == '"' || cp == '\\' ||
+ !is_printable(cp);
+}
+
+template <typename Char> struct find_escape_result {
+ const Char* begin;
+ const Char* end;
+ uint32_t cp;
+};
+
+template <typename Char>
+using make_unsigned_char =
+ typename conditional_t<std::is_integral<Char>::value,
+ std::make_unsigned<Char>,
+ type_identity<uint32_t>>::type;
+
+template <typename Char>
+auto find_escape(const Char* begin, const Char* end)
+ -> find_escape_result<Char> {
+ for (; begin != end; ++begin) {
+ uint32_t cp = static_cast<make_unsigned_char<Char>>(*begin);
+ if (const_check(sizeof(Char) == 1) && cp >= 0x80) continue;
+ if (needs_escape(cp)) return {begin, begin + 1, cp};
+ }
+ return {begin, nullptr, 0};
+}
+
+inline auto find_escape(const char* begin, const char* end)
+ -> find_escape_result<char> {
+ if (!is_utf8()) return find_escape<char>(begin, end);
+ auto result = find_escape_result<char>{end, nullptr, 0};
+ for_each_codepoint(string_view(begin, to_unsigned(end - begin)),
+ [&](uint32_t cp, string_view sv) {
+ if (needs_escape(cp)) {
+ result = {sv.begin(), sv.end(), cp};
+ return false;
+ }
+ return true;
+ });
+ return result;
+}
+
+#define FMT_STRING_IMPL(s, base, explicit) \
+ [] { \
+ /* Use the hidden visibility as a workaround for a GCC bug (#1973). */ \
+ /* Use a macro-like name to avoid shadowing warnings. */ \
+ struct FMT_VISIBILITY("hidden") FMT_COMPILE_STRING : base { \
+ using char_type FMT_MAYBE_UNUSED = fmt::remove_cvref_t<decltype(s[0])>; \
+ FMT_MAYBE_UNUSED FMT_CONSTEXPR explicit \
+ operator fmt::basic_string_view<char_type>() const { \
+ return fmt::detail_exported::compile_string_to_view<char_type>(s); \
+ } \
+ }; \
+ return FMT_COMPILE_STRING(); \
+ }()
+
+/**
+ \rst
+ Constructs a compile-time format string from a string literal *s*.
+
+ **Example**::
+
+ // A compile-time error because 'd' is an invalid specifier for strings.
+ std::string s = fmt::format(FMT_STRING("{:d}"), "foo");
+ \endrst
+ */
+#define FMT_STRING(s) FMT_STRING_IMPL(s, fmt::detail::compile_string, )
+
+template <size_t width, typename Char, typename OutputIt>
+auto write_codepoint(OutputIt out, char prefix, uint32_t cp) -> OutputIt {
+ *out++ = static_cast<Char>('\\');
+ *out++ = static_cast<Char>(prefix);
+ Char buf[width];
+ fill_n(buf, width, static_cast<Char>('0'));
+ format_uint<4>(buf, cp, width);
+ return copy_str<Char>(buf, buf + width, out);
+}
+
+template <typename OutputIt, typename Char>
+auto write_escaped_cp(OutputIt out, const find_escape_result<Char>& escape)
+ -> OutputIt {
+ auto c = static_cast<Char>(escape.cp);
+ switch (escape.cp) {
+ case '\n':
+ *out++ = static_cast<Char>('\\');
+ c = static_cast<Char>('n');
+ break;
+ case '\r':
+ *out++ = static_cast<Char>('\\');
+ c = static_cast<Char>('r');
+ break;
+ case '\t':
+ *out++ = static_cast<Char>('\\');
+ c = static_cast<Char>('t');
+ break;
+ case '"':
+ FMT_FALLTHROUGH;
+ case '\'':
+ FMT_FALLTHROUGH;
+ case '\\':
+ *out++ = static_cast<Char>('\\');
+ break;
+ default:
+ if (escape.cp < 0x100) {
+ return write_codepoint<2, Char>(out, 'x', escape.cp);
+ }
+ if (escape.cp < 0x10000) {
+ return write_codepoint<4, Char>(out, 'u', escape.cp);
+ }
+ if (escape.cp < 0x110000) {
+ return write_codepoint<8, Char>(out, 'U', escape.cp);
+ }
+ for (Char escape_char : basic_string_view<Char>(
+ escape.begin, to_unsigned(escape.end - escape.begin))) {
+ out = write_codepoint<2, Char>(out, 'x',
+ static_cast<uint32_t>(escape_char) & 0xFF);
+ }
+ return out;
+ }
+ *out++ = c;
+ return out;
+}
+
+template <typename Char, typename OutputIt>
+auto write_escaped_string(OutputIt out, basic_string_view<Char> str)
+ -> OutputIt {
+ *out++ = static_cast<Char>('"');
+ auto begin = str.begin(), end = str.end();
+ do {
+ auto escape = find_escape(begin, end);
+ out = copy_str<Char>(begin, escape.begin, out);
+ begin = escape.end;
+ if (!begin) break;
+ out = write_escaped_cp<OutputIt, Char>(out, escape);
+ } while (begin != end);
+ *out++ = static_cast<Char>('"');
+ return out;
+}
+
+template <typename Char, typename OutputIt>
+auto write_escaped_char(OutputIt out, Char v) -> OutputIt {
+ Char v_array[1] = {v};
+ *out++ = static_cast<Char>('\'');
+ if ((needs_escape(static_cast<uint32_t>(v)) && v != static_cast<Char>('"')) ||
+ v == static_cast<Char>('\'')) {
+ out = write_escaped_cp(out,
+ find_escape_result<Char>{v_array, v_array + 1,
+ static_cast<uint32_t>(v)});
+ } else {
+ *out++ = v;
+ }
+ *out++ = static_cast<Char>('\'');
+ return out;
+}
+
+template <typename Char, typename OutputIt>
+FMT_CONSTEXPR auto write_char(OutputIt out, Char value,
+ const format_specs<Char>& specs) -> OutputIt {
+ bool is_debug = specs.type == presentation_type::debug;
+ return write_padded(out, specs, 1, [=](reserve_iterator<OutputIt> it) {
+ if (is_debug) return write_escaped_char(it, value);
+ *it++ = value;
+ return it;
+ });
+}
+template <typename Char, typename OutputIt>
+FMT_CONSTEXPR auto write(OutputIt out, Char value,
+ const format_specs<Char>& specs, locale_ref loc = {})
+ -> OutputIt {
+ // char is formatted as unsigned char for consistency across platforms.
+ using unsigned_type =
+ conditional_t<std::is_same<Char, char>::value, unsigned char, unsigned>;
+ return check_char_specs(specs)
+ ? write_char(out, value, specs)
+ : write(out, static_cast<unsigned_type>(value), specs, loc);
+}
+
+// Data for write_int that doesn't depend on output iterator type. It is used to
+// avoid template code bloat.
+template <typename Char> struct write_int_data {
+ size_t size;
+ size_t padding;
+
+ FMT_CONSTEXPR write_int_data(int num_digits, unsigned prefix,
+ const format_specs<Char>& specs)
+ : size((prefix >> 24) + to_unsigned(num_digits)), padding(0) {
+ if (specs.align == align::numeric) {
+ auto width = to_unsigned(specs.width);
+ if (width > size) {
+ padding = width - size;
+ size = width;
+ }
+ } else if (specs.precision > num_digits) {
+ size = (prefix >> 24) + to_unsigned(specs.precision);
+ padding = to_unsigned(specs.precision - num_digits);
+ }
+ }
+};
+
+// Writes an integer in the format
+// <left-padding><prefix><numeric-padding><digits><right-padding>
+// where <digits> are written by write_digits(it).
+// prefix contains chars in three lower bytes and the size in the fourth byte.
+template <typename OutputIt, typename Char, typename W>
+FMT_CONSTEXPR FMT_INLINE auto write_int(OutputIt out, int num_digits,
+ unsigned prefix,
+ const format_specs<Char>& specs,
+ W write_digits) -> OutputIt {
+ // Slightly faster check for specs.width == 0 && specs.precision == -1.
+ if ((specs.width | (specs.precision + 1)) == 0) {
+ auto it = reserve(out, to_unsigned(num_digits) + (prefix >> 24));
+ if (prefix != 0) {
+ for (unsigned p = prefix & 0xffffff; p != 0; p >>= 8)
+ *it++ = static_cast<Char>(p & 0xff);
+ }
+ return base_iterator(out, write_digits(it));
+ }
+ auto data = write_int_data<Char>(num_digits, prefix, specs);
+ return write_padded<align::right>(
+ out, specs, data.size, [=](reserve_iterator<OutputIt> it) {
+ for (unsigned p = prefix & 0xffffff; p != 0; p >>= 8)
+ *it++ = static_cast<Char>(p & 0xff);
+ it = detail::fill_n(it, data.padding, static_cast<Char>('0'));
+ return write_digits(it);
+ });
+}
+
+template <typename Char> class digit_grouping {
+ private:
+ std::string grouping_;
+ std::basic_string<Char> thousands_sep_;
+
+ struct next_state {
+ std::string::const_iterator group;
+ int pos;
+ };
+ auto initial_state() const -> next_state { return {grouping_.begin(), 0}; }
+
+ // Returns the next digit group separator position.
+ auto next(next_state& state) const -> int {
+ if (thousands_sep_.empty()) return max_value<int>();
+ if (state.group == grouping_.end()) return state.pos += grouping_.back();
+ if (*state.group <= 0 || *state.group == max_value<char>())
+ return max_value<int>();
+ state.pos += *state.group++;
+ return state.pos;
+ }
+
+ public:
+ explicit digit_grouping(locale_ref loc, bool localized = true) {
+ if (!localized) return;
+ auto sep = thousands_sep<Char>(loc);
+ grouping_ = sep.grouping;
+ if (sep.thousands_sep) thousands_sep_.assign(1, sep.thousands_sep);
+ }
+ digit_grouping(std::string grouping, std::basic_string<Char> sep)
+ : grouping_(std::move(grouping)), thousands_sep_(std::move(sep)) {}
+
+ auto has_separator() const -> bool { return !thousands_sep_.empty(); }
+
+ auto count_separators(int num_digits) const -> int {
+ int count = 0;
+ auto state = initial_state();
+ while (num_digits > next(state)) ++count;
+ return count;
+ }
+
+ // Applies grouping to digits and write the output to out.
+ template <typename Out, typename C>
+ auto apply(Out out, basic_string_view<C> digits) const -> Out {
+ auto num_digits = static_cast<int>(digits.size());
+ auto separators = basic_memory_buffer<int>();
+ separators.push_back(0);
+ auto state = initial_state();
+ while (int i = next(state)) {
+ if (i >= num_digits) break;
+ separators.push_back(i);
+ }
+ for (int i = 0, sep_index = static_cast<int>(separators.size() - 1);
+ i < num_digits; ++i) {
+ if (num_digits - i == separators[sep_index]) {
+ out =
+ copy_str<Char>(thousands_sep_.data(),
+ thousands_sep_.data() + thousands_sep_.size(), out);
+ --sep_index;
+ }
+ *out++ = static_cast<Char>(digits[to_unsigned(i)]);
+ }
+ return out;
+ }
+};
+
+FMT_CONSTEXPR inline void prefix_append(unsigned& prefix, unsigned value) {
+ prefix |= prefix != 0 ? value << 8 : value;
+ prefix += (1u + (value > 0xff ? 1 : 0)) << 24;
+}
+
+// Writes a decimal integer with digit grouping.
+template <typename OutputIt, typename UInt, typename Char>
+auto write_int(OutputIt out, UInt value, unsigned prefix,
+ const format_specs<Char>& specs,
+ const digit_grouping<Char>& grouping) -> OutputIt {
+ static_assert(std::is_same<uint64_or_128_t<UInt>, UInt>::value, "");
+ int num_digits = 0;
+ auto buffer = memory_buffer();
+ switch (specs.type) {
+ case presentation_type::none:
+ case presentation_type::dec: {
+ num_digits = count_digits(value);
+ format_decimal<char>(appender(buffer), value, num_digits);
+ break;
+ }
+ case presentation_type::hex_lower:
+ case presentation_type::hex_upper: {
+ bool upper = specs.type == presentation_type::hex_upper;
+ if (specs.alt)
+ prefix_append(prefix, unsigned(upper ? 'X' : 'x') << 8 | '0');
+ num_digits = count_digits<4>(value);
+ format_uint<4, char>(appender(buffer), value, num_digits, upper);
+ break;
+ }
+ case presentation_type::bin_lower:
+ case presentation_type::bin_upper: {
+ bool upper = specs.type == presentation_type::bin_upper;
+ if (specs.alt)
+ prefix_append(prefix, unsigned(upper ? 'B' : 'b') << 8 | '0');
+ num_digits = count_digits<1>(value);
+ format_uint<1, char>(appender(buffer), value, num_digits);
+ break;
+ }
+ case presentation_type::oct: {
+ num_digits = count_digits<3>(value);
+ // Octal prefix '0' is counted as a digit, so only add it if precision
+ // is not greater than the number of digits.
+ if (specs.alt && specs.precision <= num_digits && value != 0)
+ prefix_append(prefix, '0');
+ format_uint<3, char>(appender(buffer), value, num_digits);
+ break;
+ }
+ case presentation_type::chr:
+ return write_char(out, static_cast<Char>(value), specs);
+ default:
+ throw_format_error("invalid format specifier");
+ }
+
+ unsigned size = (prefix != 0 ? prefix >> 24 : 0) + to_unsigned(num_digits) +
+ to_unsigned(grouping.count_separators(num_digits));
+ return write_padded<align::right>(
+ out, specs, size, size, [&](reserve_iterator<OutputIt> it) {
+ for (unsigned p = prefix & 0xffffff; p != 0; p >>= 8)
+ *it++ = static_cast<Char>(p & 0xff);
+ return grouping.apply(it, string_view(buffer.data(), buffer.size()));
+ });
+}
+
+// Writes a localized value.
+FMT_API auto write_loc(appender out, loc_value value,
+ const format_specs<>& specs, locale_ref loc) -> bool;
+template <typename OutputIt, typename Char>
+inline auto write_loc(OutputIt, loc_value, const format_specs<Char>&,
+ locale_ref) -> bool {
+ return false;
+}
+
+template <typename UInt> struct write_int_arg {
+ UInt abs_value;
+ unsigned prefix;
+};
+
+template <typename T>
+FMT_CONSTEXPR auto make_write_int_arg(T value, sign_t sign)
+ -> write_int_arg<uint32_or_64_or_128_t<T>> {
+ auto prefix = 0u;
+ auto abs_value = static_cast<uint32_or_64_or_128_t<T>>(value);
+ if (is_negative(value)) {
+ prefix = 0x01000000 | '-';
+ abs_value = 0 - abs_value;
+ } else {
+ constexpr const unsigned prefixes[4] = {0, 0, 0x1000000u | '+',
+ 0x1000000u | ' '};
+ prefix = prefixes[sign];
+ }
+ return {abs_value, prefix};
+}
+
+template <typename Char = char> struct loc_writer {
+ buffer_appender<Char> out;
+ const format_specs<Char>& specs;
+ std::basic_string<Char> sep;
+ std::string grouping;
+ std::basic_string<Char> decimal_point;
+
+ template <typename T, FMT_ENABLE_IF(is_integer<T>::value)>
+ auto operator()(T value) -> bool {
+ auto arg = make_write_int_arg(value, specs.sign);
+ write_int(out, static_cast<uint64_or_128_t<T>>(arg.abs_value), arg.prefix,
+ specs, digit_grouping<Char>(grouping, sep));
+ return true;
+ }
+
+ template <typename T, FMT_ENABLE_IF(!is_integer<T>::value)>
+ auto operator()(T) -> bool {
+ return false;
+ }
+};
+
+template <typename Char, typename OutputIt, typename T>
+FMT_CONSTEXPR FMT_INLINE auto write_int(OutputIt out, write_int_arg<T> arg,
+ const format_specs<Char>& specs,
+ locale_ref) -> OutputIt {
+ static_assert(std::is_same<T, uint32_or_64_or_128_t<T>>::value, "");
+ auto abs_value = arg.abs_value;
+ auto prefix = arg.prefix;
+ switch (specs.type) {
+ case presentation_type::none:
+ case presentation_type::dec: {
+ auto num_digits = count_digits(abs_value);
+ return write_int(
+ out, num_digits, prefix, specs, [=](reserve_iterator<OutputIt> it) {
+ return format_decimal<Char>(it, abs_value, num_digits).end;
+ });
+ }
+ case presentation_type::hex_lower:
+ case presentation_type::hex_upper: {
+ bool upper = specs.type == presentation_type::hex_upper;
+ if (specs.alt)
+ prefix_append(prefix, unsigned(upper ? 'X' : 'x') << 8 | '0');
+ int num_digits = count_digits<4>(abs_value);
+ return write_int(
+ out, num_digits, prefix, specs, [=](reserve_iterator<OutputIt> it) {
+ return format_uint<4, Char>(it, abs_value, num_digits, upper);
+ });
+ }
+ case presentation_type::bin_lower:
+ case presentation_type::bin_upper: {
+ bool upper = specs.type == presentation_type::bin_upper;
+ if (specs.alt)
+ prefix_append(prefix, unsigned(upper ? 'B' : 'b') << 8 | '0');
+ int num_digits = count_digits<1>(abs_value);
+ return write_int(out, num_digits, prefix, specs,
+ [=](reserve_iterator<OutputIt> it) {
+ return format_uint<1, Char>(it, abs_value, num_digits);
+ });
+ }
+ case presentation_type::oct: {
+ int num_digits = count_digits<3>(abs_value);
+ // Octal prefix '0' is counted as a digit, so only add it if precision
+ // is not greater than the number of digits.
+ if (specs.alt && specs.precision <= num_digits && abs_value != 0)
+ prefix_append(prefix, '0');
+ return write_int(out, num_digits, prefix, specs,
+ [=](reserve_iterator<OutputIt> it) {
+ return format_uint<3, Char>(it, abs_value, num_digits);
+ });
+ }
+ case presentation_type::chr:
+ return write_char(out, static_cast<Char>(abs_value), specs);
+ default:
+ throw_format_error("invalid format specifier");
+ }
+ return out;
+}
+template <typename Char, typename OutputIt, typename T>
+FMT_CONSTEXPR FMT_NOINLINE auto write_int_noinline(
+ OutputIt out, write_int_arg<T> arg, const format_specs<Char>& specs,
+ locale_ref loc) -> OutputIt {
+ return write_int(out, arg, specs, loc);
+}
+template <typename Char, typename OutputIt, typename T,
+ FMT_ENABLE_IF(is_integral<T>::value &&
+ !std::is_same<T, bool>::value &&
+ std::is_same<OutputIt, buffer_appender<Char>>::value)>
+FMT_CONSTEXPR FMT_INLINE auto write(OutputIt out, T value,
+ const format_specs<Char>& specs,
+ locale_ref loc) -> OutputIt {
+ if (specs.localized && write_loc(out, value, specs, loc)) return out;
+ return write_int_noinline(out, make_write_int_arg(value, specs.sign), specs,
+ loc);
+}
+// An inlined version of write used in format string compilation.
+template <typename Char, typename OutputIt, typename T,
+ FMT_ENABLE_IF(is_integral<T>::value &&
+ !std::is_same<T, bool>::value &&
+ !std::is_same<OutputIt, buffer_appender<Char>>::value)>
+FMT_CONSTEXPR FMT_INLINE auto write(OutputIt out, T value,
+ const format_specs<Char>& specs,
+ locale_ref loc) -> OutputIt {
+ if (specs.localized && write_loc(out, value, specs, loc)) return out;
+ return write_int(out, make_write_int_arg(value, specs.sign), specs, loc);
+}
+
+// An output iterator that counts the number of objects written to it and
+// discards them.
+class counting_iterator {
+ private:
+ size_t count_;
+
+ public:
+ using iterator_category = std::output_iterator_tag;
+ using difference_type = std::ptrdiff_t;
+ using pointer = void;
+ using reference = void;
+ FMT_UNCHECKED_ITERATOR(counting_iterator);
+
+ struct value_type {
+ template <typename T> FMT_CONSTEXPR void operator=(const T&) {}
+ };
+
+ FMT_CONSTEXPR counting_iterator() : count_(0) {}
+
+ FMT_CONSTEXPR auto count() const -> size_t { return count_; }
+
+ FMT_CONSTEXPR auto operator++() -> counting_iterator& {
+ ++count_;
+ return *this;
+ }
+ FMT_CONSTEXPR auto operator++(int) -> counting_iterator {
+ auto it = *this;
+ ++*this;
+ return it;
+ }
+
+ FMT_CONSTEXPR friend auto operator+(counting_iterator it, difference_type n)
+ -> counting_iterator {
+ it.count_ += static_cast<size_t>(n);
+ return it;
+ }
+
+ FMT_CONSTEXPR auto operator*() const -> value_type { return {}; }
+};
+
+template <typename Char, typename OutputIt>
+FMT_CONSTEXPR auto write(OutputIt out, basic_string_view<Char> s,
+ const format_specs<Char>& specs) -> OutputIt {
+ auto data = s.data();
+ auto size = s.size();
+ if (specs.precision >= 0 && to_unsigned(specs.precision) < size)
+ size = code_point_index(s, to_unsigned(specs.precision));
+ bool is_debug = specs.type == presentation_type::debug;
+ size_t width = 0;
+ if (specs.width != 0) {
+ if (is_debug)
+ width = write_escaped_string(counting_iterator{}, s).count();
+ else
+ width = compute_width(basic_string_view<Char>(data, size));
+ }
+ return write_padded(out, specs, size, width,
+ [=](reserve_iterator<OutputIt> it) {
+ if (is_debug) return write_escaped_string(it, s);
+ return copy_str<Char>(data, data + size, it);
+ });
+}
+template <typename Char, typename OutputIt>
+FMT_CONSTEXPR auto write(OutputIt out,
+ basic_string_view<type_identity_t<Char>> s,
+ const format_specs<Char>& specs, locale_ref)
+ -> OutputIt {
+ return write(out, s, specs);
+}
+template <typename Char, typename OutputIt>
+FMT_CONSTEXPR auto write(OutputIt out, const Char* s,
+ const format_specs<Char>& specs, locale_ref)
+ -> OutputIt {
+ if (specs.type == presentation_type::pointer)
+ return write_ptr<Char>(out, bit_cast<uintptr_t>(s), &specs);
+ if (!s) throw_format_error("string pointer is null");
+ return write(out, basic_string_view<Char>(s), specs, {});
+}
+
+template <typename Char, typename OutputIt, typename T,
+ FMT_ENABLE_IF(is_integral<T>::value &&
+ !std::is_same<T, bool>::value &&
+ !std::is_same<T, Char>::value)>
+FMT_CONSTEXPR auto write(OutputIt out, T value) -> OutputIt {
+ auto abs_value = static_cast<uint32_or_64_or_128_t<T>>(value);
+ bool negative = is_negative(value);
+ // Don't do -abs_value since it trips unsigned-integer-overflow sanitizer.
+ if (negative) abs_value = ~abs_value + 1;
+ int num_digits = count_digits(abs_value);
+ auto size = (negative ? 1 : 0) + static_cast<size_t>(num_digits);
+ auto it = reserve(out, size);
+ if (auto ptr = to_pointer<Char>(it, size)) {
+ if (negative) *ptr++ = static_cast<Char>('-');
+ format_decimal<Char>(ptr, abs_value, num_digits);
+ return out;
+ }
+ if (negative) *it++ = static_cast<Char>('-');
+ it = format_decimal<Char>(it, abs_value, num_digits).end;
+ return base_iterator(out, it);
+}
+
+// DEPRECATED!
+template <typename Char>
+FMT_CONSTEXPR auto parse_align(const Char* begin, const Char* end,
+ format_specs<Char>& specs) -> const Char* {
+ FMT_ASSERT(begin != end, "");
+ auto align = align::none;
+ auto p = begin + code_point_length(begin);
+ if (end - p <= 0) p = begin;
+ for (;;) {
+ switch (to_ascii(*p)) {
+ case '<':
+ align = align::left;
+ break;
+ case '>':
+ align = align::right;
+ break;
+ case '^':
+ align = align::center;
+ break;
+ }
+ if (align != align::none) {
+ if (p != begin) {
+ auto c = *begin;
+ if (c == '}') return begin;
+ if (c == '{') {
+ throw_format_error("invalid fill character '{'");
+ return begin;
+ }
+ specs.fill = {begin, to_unsigned(p - begin)};
+ begin = p + 1;
+ } else {
+ ++begin;
+ }
+ break;
+ } else if (p == begin) {
+ break;
+ }
+ p = begin;
+ }
+ specs.align = align;
+ return begin;
+}
+
+// A floating-point presentation format.
+enum class float_format : unsigned char {
+ general, // General: exponent notation or fixed point based on magnitude.
+ exp, // Exponent notation with the default precision of 6, e.g. 1.2e-3.
+ fixed, // Fixed point with the default precision of 6, e.g. 0.0012.
+ hex
+};
+
+struct float_specs {
+ int precision;
+ float_format format : 8;
+ sign_t sign : 8;
+ bool upper : 1;
+ bool locale : 1;
+ bool binary32 : 1;
+ bool showpoint : 1;
+};
+
+template <typename Char>
+FMT_CONSTEXPR auto parse_float_type_spec(const format_specs<Char>& specs)
+ -> float_specs {
+ auto result = float_specs();
+ result.showpoint = specs.alt;
+ result.locale = specs.localized;
+ switch (specs.type) {
+ case presentation_type::none:
+ result.format = float_format::general;
+ break;
+ case presentation_type::general_upper:
+ result.upper = true;
+ FMT_FALLTHROUGH;
+ case presentation_type::general_lower:
+ result.format = float_format::general;
+ break;
+ case presentation_type::exp_upper:
+ result.upper = true;
+ FMT_FALLTHROUGH;
+ case presentation_type::exp_lower:
+ result.format = float_format::exp;
+ result.showpoint |= specs.precision != 0;
+ break;
+ case presentation_type::fixed_upper:
+ result.upper = true;
+ FMT_FALLTHROUGH;
+ case presentation_type::fixed_lower:
+ result.format = float_format::fixed;
+ result.showpoint |= specs.precision != 0;
+ break;
+ case presentation_type::hexfloat_upper:
+ result.upper = true;
+ FMT_FALLTHROUGH;
+ case presentation_type::hexfloat_lower:
+ result.format = float_format::hex;
+ break;
+ default:
+ throw_format_error("invalid format specifier");
+ break;
+ }
+ return result;
+}
+
+template <typename Char, typename OutputIt>
+FMT_CONSTEXPR20 auto write_nonfinite(OutputIt out, bool isnan,
+ format_specs<Char> specs,
+ const float_specs& fspecs) -> OutputIt {
+ auto str =
+ isnan ? (fspecs.upper ? "NAN" : "nan") : (fspecs.upper ? "INF" : "inf");
+ constexpr size_t str_size = 3;
+ auto sign = fspecs.sign;
+ auto size = str_size + (sign ? 1 : 0);
+ // Replace '0'-padding with space for non-finite values.
+ const bool is_zero_fill =
+ specs.fill.size() == 1 && *specs.fill.data() == static_cast<Char>('0');
+ if (is_zero_fill) specs.fill[0] = static_cast<Char>(' ');
+ return write_padded(out, specs, size, [=](reserve_iterator<OutputIt> it) {
+ if (sign) *it++ = detail::sign<Char>(sign);
+ return copy_str<Char>(str, str + str_size, it);
+ });
+}
+
+// A decimal floating-point number significand * pow(10, exp).
+struct big_decimal_fp {
+ const char* significand;
+ int significand_size;
+ int exponent;
+};
+
+constexpr auto get_significand_size(const big_decimal_fp& f) -> int {
+ return f.significand_size;
+}
+template <typename T>
+inline auto get_significand_size(const dragonbox::decimal_fp<T>& f) -> int {
+ return count_digits(f.significand);
+}
+
+template <typename Char, typename OutputIt>
+constexpr auto write_significand(OutputIt out, const char* significand,
+ int significand_size) -> OutputIt {
+ return copy_str<Char>(significand, significand + significand_size, out);
+}
+template <typename Char, typename OutputIt, typename UInt>
+inline auto write_significand(OutputIt out, UInt significand,
+ int significand_size) -> OutputIt {
+ return format_decimal<Char>(out, significand, significand_size).end;
+}
+template <typename Char, typename OutputIt, typename T, typename Grouping>
+FMT_CONSTEXPR20 auto write_significand(OutputIt out, T significand,
+ int significand_size, int exponent,
+ const Grouping& grouping) -> OutputIt {
+ if (!grouping.has_separator()) {
+ out = write_significand<Char>(out, significand, significand_size);
+ return detail::fill_n(out, exponent, static_cast<Char>('0'));
+ }
+ auto buffer = memory_buffer();
+ write_significand<char>(appender(buffer), significand, significand_size);
+ detail::fill_n(appender(buffer), exponent, '0');
+ return grouping.apply(out, string_view(buffer.data(), buffer.size()));
+}
+
+template <typename Char, typename UInt,
+ FMT_ENABLE_IF(std::is_integral<UInt>::value)>
+inline auto write_significand(Char* out, UInt significand, int significand_size,
+ int integral_size, Char decimal_point) -> Char* {
+ if (!decimal_point)
+ return format_decimal(out, significand, significand_size).end;
+ out += significand_size + 1;
+ Char* end = out;
+ int floating_size = significand_size - integral_size;
+ for (int i = floating_size / 2; i > 0; --i) {
+ out -= 2;
+ copy2(out, digits2(static_cast<std::size_t>(significand % 100)));
+ significand /= 100;
+ }
+ if (floating_size % 2 != 0) {
+ *--out = static_cast<Char>('0' + significand % 10);
+ significand /= 10;
+ }
+ *--out = decimal_point;
+ format_decimal(out - integral_size, significand, integral_size);
+ return end;
+}
+
+template <typename OutputIt, typename UInt, typename Char,
+ FMT_ENABLE_IF(!std::is_pointer<remove_cvref_t<OutputIt>>::value)>
+inline auto write_significand(OutputIt out, UInt significand,
+ int significand_size, int integral_size,
+ Char decimal_point) -> OutputIt {
+ // Buffer is large enough to hold digits (digits10 + 1) and a decimal point.
+ Char buffer[digits10<UInt>() + 2];
+ auto end = write_significand(buffer, significand, significand_size,
+ integral_size, decimal_point);
+ return detail::copy_str_noinline<Char>(buffer, end, out);
+}
+
+template <typename OutputIt, typename Char>
+FMT_CONSTEXPR auto write_significand(OutputIt out, const char* significand,
+ int significand_size, int integral_size,
+ Char decimal_point) -> OutputIt {
+ out = detail::copy_str_noinline<Char>(significand,
+ significand + integral_size, out);
+ if (!decimal_point) return out;
+ *out++ = decimal_point;
+ return detail::copy_str_noinline<Char>(significand + integral_size,
+ significand + significand_size, out);
+}
+
+template <typename OutputIt, typename Char, typename T, typename Grouping>
+FMT_CONSTEXPR20 auto write_significand(OutputIt out, T significand,
+ int significand_size, int integral_size,
+ Char decimal_point,
+ const Grouping& grouping) -> OutputIt {
+ if (!grouping.has_separator()) {
+ return write_significand(out, significand, significand_size, integral_size,
+ decimal_point);
+ }
+ auto buffer = basic_memory_buffer<Char>();
+ write_significand(buffer_appender<Char>(buffer), significand,
+ significand_size, integral_size, decimal_point);
+ grouping.apply(
+ out, basic_string_view<Char>(buffer.data(), to_unsigned(integral_size)));
+ return detail::copy_str_noinline<Char>(buffer.data() + integral_size,
+ buffer.end(), out);
+}
+
+template <typename OutputIt, typename DecimalFP, typename Char,
+ typename Grouping = digit_grouping<Char>>
+FMT_CONSTEXPR20 auto do_write_float(OutputIt out, const DecimalFP& f,
+ const format_specs<Char>& specs,
+ float_specs fspecs, locale_ref loc)
+ -> OutputIt {
+ auto significand = f.significand;
+ int significand_size = get_significand_size(f);
+ const Char zero = static_cast<Char>('0');
+ auto sign = fspecs.sign;
+ size_t size = to_unsigned(significand_size) + (sign ? 1 : 0);
+ using iterator = reserve_iterator<OutputIt>;
+
+ Char decimal_point =
+ fspecs.locale ? detail::decimal_point<Char>(loc) : static_cast<Char>('.');
+
+ int output_exp = f.exponent + significand_size - 1;
+ auto use_exp_format = [=]() {
+ if (fspecs.format == float_format::exp) return true;
+ if (fspecs.format != float_format::general) return false;
+ // Use the fixed notation if the exponent is in [exp_lower, exp_upper),
+ // e.g. 0.0001 instead of 1e-04. Otherwise use the exponent notation.
+ const int exp_lower = -4, exp_upper = 16;
+ return output_exp < exp_lower ||
+ output_exp >= (fspecs.precision > 0 ? fspecs.precision : exp_upper);
+ };
+ if (use_exp_format()) {
+ int num_zeros = 0;
+ if (fspecs.showpoint) {
+ num_zeros = fspecs.precision - significand_size;
+ if (num_zeros < 0) num_zeros = 0;
+ size += to_unsigned(num_zeros);
+ } else if (significand_size == 1) {
+ decimal_point = Char();
+ }
+ auto abs_output_exp = output_exp >= 0 ? output_exp : -output_exp;
+ int exp_digits = 2;
+ if (abs_output_exp >= 100) exp_digits = abs_output_exp >= 1000 ? 4 : 3;
+
+ size += to_unsigned((decimal_point ? 1 : 0) + 2 + exp_digits);
+ char exp_char = fspecs.upper ? 'E' : 'e';
+ auto write = [=](iterator it) {
+ if (sign) *it++ = detail::sign<Char>(sign);
+ // Insert a decimal point after the first digit and add an exponent.
+ it = write_significand(it, significand, significand_size, 1,
+ decimal_point);
+ if (num_zeros > 0) it = detail::fill_n(it, num_zeros, zero);
+ *it++ = static_cast<Char>(exp_char);
+ return write_exponent<Char>(output_exp, it);
+ };
+ return specs.width > 0 ? write_padded<align::right>(out, specs, size, write)
+ : base_iterator(out, write(reserve(out, size)));
+ }
+
+ int exp = f.exponent + significand_size;
+ if (f.exponent >= 0) {
+ // 1234e5 -> 123400000[.0+]
+ size += to_unsigned(f.exponent);
+ int num_zeros = fspecs.precision - exp;
+ abort_fuzzing_if(num_zeros > 5000);
+ if (fspecs.showpoint) {
+ ++size;
+ if (num_zeros <= 0 && fspecs.format != float_format::fixed) num_zeros = 0;
+ if (num_zeros > 0) size += to_unsigned(num_zeros);
+ }
+ auto grouping = Grouping(loc, fspecs.locale);
+ size += to_unsigned(grouping.count_separators(exp));
+ return write_padded<align::right>(out, specs, size, [&](iterator it) {
+ if (sign) *it++ = detail::sign<Char>(sign);
+ it = write_significand<Char>(it, significand, significand_size,
+ f.exponent, grouping);
+ if (!fspecs.showpoint) return it;
+ *it++ = decimal_point;
+ return num_zeros > 0 ? detail::fill_n(it, num_zeros, zero) : it;
+ });
+ } else if (exp > 0) {
+ // 1234e-2 -> 12.34[0+]
+ int num_zeros = fspecs.showpoint ? fspecs.precision - significand_size : 0;
+ size += 1 + to_unsigned(num_zeros > 0 ? num_zeros : 0);
+ auto grouping = Grouping(loc, fspecs.locale);
+ size += to_unsigned(grouping.count_separators(exp));
+ return write_padded<align::right>(out, specs, size, [&](iterator it) {
+ if (sign) *it++ = detail::sign<Char>(sign);
+ it = write_significand(it, significand, significand_size, exp,
+ decimal_point, grouping);
+ return num_zeros > 0 ? detail::fill_n(it, num_zeros, zero) : it;
+ });
+ }
+ // 1234e-6 -> 0.001234
+ int num_zeros = -exp;
+ if (significand_size == 0 && fspecs.precision >= 0 &&
+ fspecs.precision < num_zeros) {
+ num_zeros = fspecs.precision;
+ }
+ bool pointy = num_zeros != 0 || significand_size != 0 || fspecs.showpoint;
+ size += 1 + (pointy ? 1 : 0) + to_unsigned(num_zeros);
+ return write_padded<align::right>(out, specs, size, [&](iterator it) {
+ if (sign) *it++ = detail::sign<Char>(sign);
+ *it++ = zero;
+ if (!pointy) return it;
+ *it++ = decimal_point;
+ it = detail::fill_n(it, num_zeros, zero);
+ return write_significand<Char>(it, significand, significand_size);
+ });
+}
+
+template <typename Char> class fallback_digit_grouping {
+ public:
+ constexpr fallback_digit_grouping(locale_ref, bool) {}
+
+ constexpr auto has_separator() const -> bool { return false; }
+
+ constexpr auto count_separators(int) const -> int { return 0; }
+
+ template <typename Out, typename C>
+ constexpr auto apply(Out out, basic_string_view<C>) const -> Out {
+ return out;
+ }
+};
+
+template <typename OutputIt, typename DecimalFP, typename Char>
+FMT_CONSTEXPR20 auto write_float(OutputIt out, const DecimalFP& f,
+ const format_specs<Char>& specs,
+ float_specs fspecs, locale_ref loc)
+ -> OutputIt {
+ if (is_constant_evaluated()) {
+ return do_write_float<OutputIt, DecimalFP, Char,
+ fallback_digit_grouping<Char>>(out, f, specs, fspecs,
+ loc);
+ } else {
+ return do_write_float(out, f, specs, fspecs, loc);
+ }
+}
+
+template <typename T> constexpr auto isnan(T value) -> bool {
+ return !(value >= value); // std::isnan doesn't support __float128.
+}
+
+template <typename T, typename Enable = void>
+struct has_isfinite : std::false_type {};
+
+template <typename T>
+struct has_isfinite<T, enable_if_t<sizeof(std::isfinite(T())) != 0>>
+ : std::true_type {};
+
+template <typename T, FMT_ENABLE_IF(std::is_floating_point<T>::value&&
+ has_isfinite<T>::value)>
+FMT_CONSTEXPR20 auto isfinite(T value) -> bool {
+ constexpr T inf = T(std::numeric_limits<double>::infinity());
+ if (is_constant_evaluated())
+ return !detail::isnan(value) && value < inf && value > -inf;
+ return std::isfinite(value);
+}
+template <typename T, FMT_ENABLE_IF(!has_isfinite<T>::value)>
+FMT_CONSTEXPR auto isfinite(T value) -> bool {
+ T inf = T(std::numeric_limits<double>::infinity());
+ // std::isfinite doesn't support __float128.
+ return !detail::isnan(value) && value < inf && value > -inf;
+}
+
+template <typename T, FMT_ENABLE_IF(is_floating_point<T>::value)>
+FMT_INLINE FMT_CONSTEXPR bool signbit(T value) {
+ if (is_constant_evaluated()) {
+#ifdef __cpp_if_constexpr
+ if constexpr (std::numeric_limits<double>::is_iec559) {
+ auto bits = detail::bit_cast<uint64_t>(static_cast<double>(value));
+ return (bits >> (num_bits<uint64_t>() - 1)) != 0;
+ }
+#endif
+ }
+ return std::signbit(static_cast<double>(value));
+}
+
+inline FMT_CONSTEXPR20 void adjust_precision(int& precision, int exp10) {
+ // Adjust fixed precision by exponent because it is relative to decimal
+ // point.
+ if (exp10 > 0 && precision > max_value<int>() - exp10)
+ FMT_THROW(format_error("number is too big"));
+ precision += exp10;
+}
+
+class bigint {
+ private:
+ // A bigint is stored as an array of bigits (big digits), with bigit at index
+ // 0 being the least significant one.
+ using bigit = uint32_t;
+ using double_bigit = uint64_t;
+ enum { bigits_capacity = 32 };
+ basic_memory_buffer<bigit, bigits_capacity> bigits_;
+ int exp_;
+
+ FMT_CONSTEXPR20 auto operator[](int index) const -> bigit {
+ return bigits_[to_unsigned(index)];
+ }
+ FMT_CONSTEXPR20 auto operator[](int index) -> bigit& {
+ return bigits_[to_unsigned(index)];
+ }
+
+ static constexpr const int bigit_bits = num_bits<bigit>();
+
+ friend struct formatter<bigint>;
+
+ FMT_CONSTEXPR20 void subtract_bigits(int index, bigit other, bigit& borrow) {
+ auto result = static_cast<double_bigit>((*this)[index]) - other - borrow;
+ (*this)[index] = static_cast<bigit>(result);
+ borrow = static_cast<bigit>(result >> (bigit_bits * 2 - 1));
+ }
+
+ FMT_CONSTEXPR20 void remove_leading_zeros() {
+ int num_bigits = static_cast<int>(bigits_.size()) - 1;
+ while (num_bigits > 0 && (*this)[num_bigits] == 0) --num_bigits;
+ bigits_.resize(to_unsigned(num_bigits + 1));
+ }
+
+ // Computes *this -= other assuming aligned bigints and *this >= other.
+ FMT_CONSTEXPR20 void subtract_aligned(const bigint& other) {
+ FMT_ASSERT(other.exp_ >= exp_, "unaligned bigints");
+ FMT_ASSERT(compare(*this, other) >= 0, "");
+ bigit borrow = 0;
+ int i = other.exp_ - exp_;
+ for (size_t j = 0, n = other.bigits_.size(); j != n; ++i, ++j)
+ subtract_bigits(i, other.bigits_[j], borrow);
+ while (borrow > 0) subtract_bigits(i, 0, borrow);
+ remove_leading_zeros();
+ }
+
+ FMT_CONSTEXPR20 void multiply(uint32_t value) {
+ const double_bigit wide_value = value;
+ bigit carry = 0;
+ for (size_t i = 0, n = bigits_.size(); i < n; ++i) {
+ double_bigit result = bigits_[i] * wide_value + carry;
+ bigits_[i] = static_cast<bigit>(result);
+ carry = static_cast<bigit>(result >> bigit_bits);
+ }
+ if (carry != 0) bigits_.push_back(carry);
+ }
+
+ template <typename UInt, FMT_ENABLE_IF(std::is_same<UInt, uint64_t>::value ||
+ std::is_same<UInt, uint128_t>::value)>
+ FMT_CONSTEXPR20 void multiply(UInt value) {
+ using half_uint =
+ conditional_t<std::is_same<UInt, uint128_t>::value, uint64_t, uint32_t>;
+ const int shift = num_bits<half_uint>() - bigit_bits;
+ const UInt lower = static_cast<half_uint>(value);
+ const UInt upper = value >> num_bits<half_uint>();
+ UInt carry = 0;
+ for (size_t i = 0, n = bigits_.size(); i < n; ++i) {
+ UInt result = lower * bigits_[i] + static_cast<bigit>(carry);
+ carry = (upper * bigits_[i] << shift) + (result >> bigit_bits) +
+ (carry >> bigit_bits);
+ bigits_[i] = static_cast<bigit>(result);
+ }
+ while (carry != 0) {
+ bigits_.push_back(static_cast<bigit>(carry));
+ carry >>= bigit_bits;
+ }
+ }
+
+ template <typename UInt, FMT_ENABLE_IF(std::is_same<UInt, uint64_t>::value ||
+ std::is_same<UInt, uint128_t>::value)>
+ FMT_CONSTEXPR20 void assign(UInt n) {
+ size_t num_bigits = 0;
+ do {
+ bigits_[num_bigits++] = static_cast<bigit>(n);
+ n >>= bigit_bits;
+ } while (n != 0);
+ bigits_.resize(num_bigits);
+ exp_ = 0;
+ }
+
+ public:
+ FMT_CONSTEXPR20 bigint() : exp_(0) {}
+ explicit bigint(uint64_t n) { assign(n); }
+
+ bigint(const bigint&) = delete;
+ void operator=(const bigint&) = delete;
+
+ FMT_CONSTEXPR20 void assign(const bigint& other) {
+ auto size = other.bigits_.size();
+ bigits_.resize(size);
+ auto data = other.bigits_.data();
+ copy_str<bigit>(data, data + size, bigits_.data());
+ exp_ = other.exp_;
+ }
+
+ template <typename Int> FMT_CONSTEXPR20 void operator=(Int n) {
+ FMT_ASSERT(n > 0, "");
+ assign(uint64_or_128_t<Int>(n));
+ }
+
+ FMT_CONSTEXPR20 auto num_bigits() const -> int {
+ return static_cast<int>(bigits_.size()) + exp_;
+ }
+
+ FMT_NOINLINE FMT_CONSTEXPR20 auto operator<<=(int shift) -> bigint& {
+ FMT_ASSERT(shift >= 0, "");
+ exp_ += shift / bigit_bits;
+ shift %= bigit_bits;
+ if (shift == 0) return *this;
+ bigit carry = 0;
+ for (size_t i = 0, n = bigits_.size(); i < n; ++i) {
+ bigit c = bigits_[i] >> (bigit_bits - shift);
+ bigits_[i] = (bigits_[i] << shift) + carry;
+ carry = c;
+ }
+ if (carry != 0) bigits_.push_back(carry);
+ return *this;
+ }
+
+ template <typename Int>
+ FMT_CONSTEXPR20 auto operator*=(Int value) -> bigint& {
+ FMT_ASSERT(value > 0, "");
+ multiply(uint32_or_64_or_128_t<Int>(value));
+ return *this;
+ }
+
+ friend FMT_CONSTEXPR20 auto compare(const bigint& lhs, const bigint& rhs)
+ -> int {
+ int num_lhs_bigits = lhs.num_bigits(), num_rhs_bigits = rhs.num_bigits();
+ if (num_lhs_bigits != num_rhs_bigits)
+ return num_lhs_bigits > num_rhs_bigits ? 1 : -1;
+ int i = static_cast<int>(lhs.bigits_.size()) - 1;
+ int j = static_cast<int>(rhs.bigits_.size()) - 1;
+ int end = i - j;
+ if (end < 0) end = 0;
+ for (; i >= end; --i, --j) {
+ bigit lhs_bigit = lhs[i], rhs_bigit = rhs[j];
+ if (lhs_bigit != rhs_bigit) return lhs_bigit > rhs_bigit ? 1 : -1;
+ }
+ if (i != j) return i > j ? 1 : -1;
+ return 0;
+ }
+
+ // Returns compare(lhs1 + lhs2, rhs).
+ friend FMT_CONSTEXPR20 auto add_compare(const bigint& lhs1,
+ const bigint& lhs2, const bigint& rhs)
+ -> int {
+ auto minimum = [](int a, int b) { return a < b ? a : b; };
+ auto maximum = [](int a, int b) { return a > b ? a : b; };
+ int max_lhs_bigits = maximum(lhs1.num_bigits(), lhs2.num_bigits());
+ int num_rhs_bigits = rhs.num_bigits();
+ if (max_lhs_bigits + 1 < num_rhs_bigits) return -1;
+ if (max_lhs_bigits > num_rhs_bigits) return 1;
+ auto get_bigit = [](const bigint& n, int i) -> bigit {
+ return i >= n.exp_ && i < n.num_bigits() ? n[i - n.exp_] : 0;
+ };
+ double_bigit borrow = 0;
+ int min_exp = minimum(minimum(lhs1.exp_, lhs2.exp_), rhs.exp_);
+ for (int i = num_rhs_bigits - 1; i >= min_exp; --i) {
+ double_bigit sum =
+ static_cast<double_bigit>(get_bigit(lhs1, i)) + get_bigit(lhs2, i);
+ bigit rhs_bigit = get_bigit(rhs, i);
+ if (sum > rhs_bigit + borrow) return 1;
+ borrow = rhs_bigit + borrow - sum;
+ if (borrow > 1) return -1;
+ borrow <<= bigit_bits;
+ }
+ return borrow != 0 ? -1 : 0;
+ }
+
+ // Assigns pow(10, exp) to this bigint.
+ FMT_CONSTEXPR20 void assign_pow10(int exp) {
+ FMT_ASSERT(exp >= 0, "");
+ if (exp == 0) return *this = 1;
+ // Find the top bit.
+ int bitmask = 1;
+ while (exp >= bitmask) bitmask <<= 1;
+ bitmask >>= 1;
+ // pow(10, exp) = pow(5, exp) * pow(2, exp). First compute pow(5, exp) by
+ // repeated squaring and multiplication.
+ *this = 5;
+ bitmask >>= 1;
+ while (bitmask != 0) {
+ square();
+ if ((exp & bitmask) != 0) *this *= 5;
+ bitmask >>= 1;
+ }
+ *this <<= exp; // Multiply by pow(2, exp) by shifting.
+ }
+
+ FMT_CONSTEXPR20 void square() {
+ int num_bigits = static_cast<int>(bigits_.size());
+ int num_result_bigits = 2 * num_bigits;
+ basic_memory_buffer<bigit, bigits_capacity> n(std::move(bigits_));
+ bigits_.resize(to_unsigned(num_result_bigits));
+ auto sum = uint128_t();
+ for (int bigit_index = 0; bigit_index < num_bigits; ++bigit_index) {
+ // Compute bigit at position bigit_index of the result by adding
+ // cross-product terms n[i] * n[j] such that i + j == bigit_index.
+ for (int i = 0, j = bigit_index; j >= 0; ++i, --j) {
+ // Most terms are multiplied twice which can be optimized in the future.
+ sum += static_cast<double_bigit>(n[i]) * n[j];
+ }
+ (*this)[bigit_index] = static_cast<bigit>(sum);
+ sum >>= num_bits<bigit>(); // Compute the carry.
+ }
+ // Do the same for the top half.
+ for (int bigit_index = num_bigits; bigit_index < num_result_bigits;
+ ++bigit_index) {
+ for (int j = num_bigits - 1, i = bigit_index - j; i < num_bigits;)
+ sum += static_cast<double_bigit>(n[i++]) * n[j--];
+ (*this)[bigit_index] = static_cast<bigit>(sum);
+ sum >>= num_bits<bigit>();
+ }
+ remove_leading_zeros();
+ exp_ *= 2;
+ }
+
+ // If this bigint has a bigger exponent than other, adds trailing zero to make
+ // exponents equal. This simplifies some operations such as subtraction.
+ FMT_CONSTEXPR20 void align(const bigint& other) {
+ int exp_difference = exp_ - other.exp_;
+ if (exp_difference <= 0) return;
+ int num_bigits = static_cast<int>(bigits_.size());
+ bigits_.resize(to_unsigned(num_bigits + exp_difference));
+ for (int i = num_bigits - 1, j = i + exp_difference; i >= 0; --i, --j)
+ bigits_[j] = bigits_[i];
+ std::uninitialized_fill_n(bigits_.data(), exp_difference, 0u);
+ exp_ -= exp_difference;
+ }
+
+ // Divides this bignum by divisor, assigning the remainder to this and
+ // returning the quotient.
+ FMT_CONSTEXPR20 auto divmod_assign(const bigint& divisor) -> int {
+ FMT_ASSERT(this != &divisor, "");
+ if (compare(*this, divisor) < 0) return 0;
+ FMT_ASSERT(divisor.bigits_[divisor.bigits_.size() - 1u] != 0, "");
+ align(divisor);
+ int quotient = 0;
+ do {
+ subtract_aligned(divisor);
+ ++quotient;
+ } while (compare(*this, divisor) >= 0);
+ return quotient;
+ }
+};
+
+// format_dragon flags.
+enum dragon {
+ predecessor_closer = 1,
+ fixup = 2, // Run fixup to correct exp10 which can be off by one.
+ fixed = 4,
+};
+
+// Formats a floating-point number using a variation of the Fixed-Precision
+// Positive Floating-Point Printout ((FPP)^2) algorithm by Steele & White:
+// https://fmt.dev/papers/p372-steele.pdf.
+FMT_CONSTEXPR20 inline void format_dragon(basic_fp<uint128_t> value,
+ unsigned flags, int num_digits,
+ buffer<char>& buf, int& exp10) {
+ bigint numerator; // 2 * R in (FPP)^2.
+ bigint denominator; // 2 * S in (FPP)^2.
+ // lower and upper are differences between value and corresponding boundaries.
+ bigint lower; // (M^- in (FPP)^2).
+ bigint upper_store; // upper's value if different from lower.
+ bigint* upper = nullptr; // (M^+ in (FPP)^2).
+ // Shift numerator and denominator by an extra bit or two (if lower boundary
+ // is closer) to make lower and upper integers. This eliminates multiplication
+ // by 2 during later computations.
+ bool is_predecessor_closer = (flags & dragon::predecessor_closer) != 0;
+ int shift = is_predecessor_closer ? 2 : 1;
+ if (value.e >= 0) {
+ numerator = value.f;
+ numerator <<= value.e + shift;
+ lower = 1;
+ lower <<= value.e;
+ if (is_predecessor_closer) {
+ upper_store = 1;
+ upper_store <<= value.e + 1;
+ upper = &upper_store;
+ }
+ denominator.assign_pow10(exp10);
+ denominator <<= shift;
+ } else if (exp10 < 0) {
+ numerator.assign_pow10(-exp10);
+ lower.assign(numerator);
+ if (is_predecessor_closer) {
+ upper_store.assign(numerator);
+ upper_store <<= 1;
+ upper = &upper_store;
+ }
+ numerator *= value.f;
+ numerator <<= shift;
+ denominator = 1;
+ denominator <<= shift - value.e;
+ } else {
+ numerator = value.f;
+ numerator <<= shift;
+ denominator.assign_pow10(exp10);
+ denominator <<= shift - value.e;
+ lower = 1;
+ if (is_predecessor_closer) {
+ upper_store = 1ULL << 1;
+ upper = &upper_store;
+ }
+ }
+ int even = static_cast<int>((value.f & 1) == 0);
+ if (!upper) upper = &lower;
+ bool shortest = num_digits < 0;
+ if ((flags & dragon::fixup) != 0) {
+ if (add_compare(numerator, *upper, denominator) + even <= 0) {
+ --exp10;
+ numerator *= 10;
+ if (num_digits < 0) {
+ lower *= 10;
+ if (upper != &lower) *upper *= 10;
+ }
+ }
+ if ((flags & dragon::fixed) != 0) adjust_precision(num_digits, exp10 + 1);
+ }
+ // Invariant: value == (numerator / denominator) * pow(10, exp10).
+ if (shortest) {
+ // Generate the shortest representation.
+ num_digits = 0;
+ char* data = buf.data();
+ for (;;) {
+ int digit = numerator.divmod_assign(denominator);
+ bool low = compare(numerator, lower) - even < 0; // numerator <[=] lower.
+ // numerator + upper >[=] pow10:
+ bool high = add_compare(numerator, *upper, denominator) + even > 0;
+ data[num_digits++] = static_cast<char>('0' + digit);
+ if (low || high) {
+ if (!low) {
+ ++data[num_digits - 1];
+ } else if (high) {
+ int result = add_compare(numerator, numerator, denominator);
+ // Round half to even.
+ if (result > 0 || (result == 0 && (digit % 2) != 0))
+ ++data[num_digits - 1];
+ }
+ buf.try_resize(to_unsigned(num_digits));
+ exp10 -= num_digits - 1;
+ return;
+ }
+ numerator *= 10;
+ lower *= 10;
+ if (upper != &lower) *upper *= 10;
+ }
+ }
+ // Generate the given number of digits.
+ exp10 -= num_digits - 1;
+ if (num_digits <= 0) {
+ denominator *= 10;
+ auto digit = add_compare(numerator, numerator, denominator) > 0 ? '1' : '0';
+ buf.push_back(digit);
+ return;
+ }
+ buf.try_resize(to_unsigned(num_digits));
+ for (int i = 0; i < num_digits - 1; ++i) {
+ int digit = numerator.divmod_assign(denominator);
+ buf[i] = static_cast<char>('0' + digit);
+ numerator *= 10;
+ }
+ int digit = numerator.divmod_assign(denominator);
+ auto result = add_compare(numerator, numerator, denominator);
+ if (result > 0 || (result == 0 && (digit % 2) != 0)) {
+ if (digit == 9) {
+ const auto overflow = '0' + 10;
+ buf[num_digits - 1] = overflow;
+ // Propagate the carry.
+ for (int i = num_digits - 1; i > 0 && buf[i] == overflow; --i) {
+ buf[i] = '0';
+ ++buf[i - 1];
+ }
+ if (buf[0] == overflow) {
+ buf[0] = '1';
+ if ((flags & dragon::fixed) != 0)
+ buf.push_back('0');
+ else
+ ++exp10;
+ }
+ return;
+ }
+ ++digit;
+ }
+ buf[num_digits - 1] = static_cast<char>('0' + digit);
+}
+
+// Formats a floating-point number using the hexfloat format.
+template <typename Float, FMT_ENABLE_IF(!is_double_double<Float>::value)>
+FMT_CONSTEXPR20 void format_hexfloat(Float value, int precision,
+ float_specs specs, buffer<char>& buf) {
+ // float is passed as double to reduce the number of instantiations and to
+ // simplify implementation.
+ static_assert(!std::is_same<Float, float>::value, "");
+
+ using info = dragonbox::float_info<Float>;
+
+ // Assume Float is in the format [sign][exponent][significand].
+ using carrier_uint = typename info::carrier_uint;
+
+ constexpr auto num_float_significand_bits =
+ detail::num_significand_bits<Float>();
+
+ basic_fp<carrier_uint> f(value);
+ f.e += num_float_significand_bits;
+ if (!has_implicit_bit<Float>()) --f.e;
+
+ constexpr auto num_fraction_bits =
+ num_float_significand_bits + (has_implicit_bit<Float>() ? 1 : 0);
+ constexpr auto num_xdigits = (num_fraction_bits + 3) / 4;
+
+ constexpr auto leading_shift = ((num_xdigits - 1) * 4);
+ const auto leading_mask = carrier_uint(0xF) << leading_shift;
+ const auto leading_xdigit =
+ static_cast<uint32_t>((f.f & leading_mask) >> leading_shift);
+ if (leading_xdigit > 1) f.e -= (32 - countl_zero(leading_xdigit) - 1);
+
+ int print_xdigits = num_xdigits - 1;
+ if (precision >= 0 && print_xdigits > precision) {
+ const int shift = ((print_xdigits - precision - 1) * 4);
+ const auto mask = carrier_uint(0xF) << shift;
+ const auto v = static_cast<uint32_t>((f.f & mask) >> shift);
+
+ if (v >= 8) {
+ const auto inc = carrier_uint(1) << (shift + 4);
+ f.f += inc;
+ f.f &= ~(inc - 1);
+ }
+
+ // Check long double overflow
+ if (!has_implicit_bit<Float>()) {
+ const auto implicit_bit = carrier_uint(1) << num_float_significand_bits;
+ if ((f.f & implicit_bit) == implicit_bit) {
+ f.f >>= 4;
+ f.e += 4;
+ }
+ }
+
+ print_xdigits = precision;
+ }
+
+ char xdigits[num_bits<carrier_uint>() / 4];
+ detail::fill_n(xdigits, sizeof(xdigits), '0');
+ format_uint<4>(xdigits, f.f, num_xdigits, specs.upper);
+
+ // Remove zero tail
+ while (print_xdigits > 0 && xdigits[print_xdigits] == '0') --print_xdigits;
+
+ buf.push_back('0');
+ buf.push_back(specs.upper ? 'X' : 'x');
+ buf.push_back(xdigits[0]);
+ if (specs.showpoint || print_xdigits > 0 || print_xdigits < precision)
+ buf.push_back('.');
+ buf.append(xdigits + 1, xdigits + 1 + print_xdigits);
+ for (; print_xdigits < precision; ++print_xdigits) buf.push_back('0');
+
+ buf.push_back(specs.upper ? 'P' : 'p');
+
+ uint32_t abs_e;
+ if (f.e < 0) {
+ buf.push_back('-');
+ abs_e = static_cast<uint32_t>(-f.e);
+ } else {
+ buf.push_back('+');
+ abs_e = static_cast<uint32_t>(f.e);
+ }
+ format_decimal<char>(appender(buf), abs_e, detail::count_digits(abs_e));
+}
+
+template <typename Float, FMT_ENABLE_IF(is_double_double<Float>::value)>
+FMT_CONSTEXPR20 void format_hexfloat(Float value, int precision,
+ float_specs specs, buffer<char>& buf) {
+ format_hexfloat(static_cast<double>(value), precision, specs, buf);
+}
+
+constexpr auto fractional_part_rounding_thresholds(int index) -> uint32_t {
+ // For checking rounding thresholds.
+ // The kth entry is chosen to be the smallest integer such that the
+ // upper 32-bits of 10^(k+1) times it is strictly bigger than 5 * 10^k.
+ // It is equal to ceil(2^31 + 2^32/10^(k + 1)).
+ // These are stored in a string literal because we cannot have static arrays
+ // in constexpr functions and non-static ones are poorly optimized.
+ return U"\x9999999a\x828f5c29\x80418938\x80068db9\x8000a7c6\x800010c7"
+ U"\x800001ae\x8000002b"[index];
+}
+
+template <typename Float>
+FMT_CONSTEXPR20 auto format_float(Float value, int precision, float_specs specs,
+ buffer<char>& buf) -> int {
+ // float is passed as double to reduce the number of instantiations.
+ static_assert(!std::is_same<Float, float>::value, "");
+ FMT_ASSERT(value >= 0, "value is negative");
+ auto converted_value = convert_float(value);
+
+ const bool fixed = specs.format == float_format::fixed;
+ if (value <= 0) { // <= instead of == to silence a warning.
+ if (precision <= 0 || !fixed) {
+ buf.push_back('0');
+ return 0;
+ }
+ buf.try_resize(to_unsigned(precision));
+ fill_n(buf.data(), precision, '0');
+ return -precision;
+ }
+
+ int exp = 0;
+ bool use_dragon = true;
+ unsigned dragon_flags = 0;
+ if (!is_fast_float<Float>() || is_constant_evaluated()) {
+ const auto inv_log2_10 = 0.3010299956639812; // 1 / log2(10)
+ using info = dragonbox::float_info<decltype(converted_value)>;
+ const auto f = basic_fp<typename info::carrier_uint>(converted_value);
+ // Compute exp, an approximate power of 10, such that
+ // 10^(exp - 1) <= value < 10^exp or 10^exp <= value < 10^(exp + 1).
+ // This is based on log10(value) == log2(value) / log2(10) and approximation
+ // of log2(value) by e + num_fraction_bits idea from double-conversion.
+ auto e = (f.e + count_digits<1>(f.f) - 1) * inv_log2_10 - 1e-10;
+ exp = static_cast<int>(e);
+ if (e > exp) ++exp; // Compute ceil.
+ dragon_flags = dragon::fixup;
+ } else if (precision < 0) {
+ // Use Dragonbox for the shortest format.
+ if (specs.binary32) {
+ auto dec = dragonbox::to_decimal(static_cast<float>(value));
+ write<char>(buffer_appender<char>(buf), dec.significand);
+ return dec.exponent;
+ }
+ auto dec = dragonbox::to_decimal(static_cast<double>(value));
+ write<char>(buffer_appender<char>(buf), dec.significand);
+ return dec.exponent;
+ } else {
+ // Extract significand bits and exponent bits.
+ using info = dragonbox::float_info<double>;
+ auto br = bit_cast<uint64_t>(static_cast<double>(value));
+
+ const uint64_t significand_mask =
+ (static_cast<uint64_t>(1) << num_significand_bits<double>()) - 1;
+ uint64_t significand = (br & significand_mask);
+ int exponent = static_cast<int>((br & exponent_mask<double>()) >>
+ num_significand_bits<double>());
+
+ if (exponent != 0) { // Check if normal.
+ exponent -= exponent_bias<double>() + num_significand_bits<double>();
+ significand |=
+ (static_cast<uint64_t>(1) << num_significand_bits<double>());
+ significand <<= 1;
+ } else {
+ // Normalize subnormal inputs.
+ FMT_ASSERT(significand != 0, "zeros should not appear here");
+ int shift = countl_zero(significand);
+ FMT_ASSERT(shift >= num_bits<uint64_t>() - num_significand_bits<double>(),
+ "");
+ shift -= (num_bits<uint64_t>() - num_significand_bits<double>() - 2);
+ exponent = (std::numeric_limits<double>::min_exponent -
+ num_significand_bits<double>()) -
+ shift;
+ significand <<= shift;
+ }
+
+ // Compute the first several nonzero decimal significand digits.
+ // We call the number we get the first segment.
+ const int k = info::kappa - dragonbox::floor_log10_pow2(exponent);
+ exp = -k;
+ const int beta = exponent + dragonbox::floor_log2_pow10(k);
+ uint64_t first_segment;
+ bool has_more_segments;
+ int digits_in_the_first_segment;
+ {
+ const auto r = dragonbox::umul192_upper128(
+ significand << beta, dragonbox::get_cached_power(k));
+ first_segment = r.high();
+ has_more_segments = r.low() != 0;
+
+ // The first segment can have 18 ~ 19 digits.
+ if (first_segment >= 1000000000000000000ULL) {
+ digits_in_the_first_segment = 19;
+ } else {
+ // When it is of 18-digits, we align it to 19-digits by adding a bogus
+ // zero at the end.
+ digits_in_the_first_segment = 18;
+ first_segment *= 10;
+ }
+ }
+
+ // Compute the actual number of decimal digits to print.
+ if (fixed) adjust_precision(precision, exp + digits_in_the_first_segment);
+
+ // Use Dragon4 only when there might be not enough digits in the first
+ // segment.
+ if (digits_in_the_first_segment > precision) {
+ use_dragon = false;
+
+ if (precision <= 0) {
+ exp += digits_in_the_first_segment;
+
+ if (precision < 0) {
+ // Nothing to do, since all we have are just leading zeros.
+ buf.try_resize(0);
+ } else {
+ // We may need to round-up.
+ buf.try_resize(1);
+ if ((first_segment | static_cast<uint64_t>(has_more_segments)) >
+ 5000000000000000000ULL) {
+ buf[0] = '1';
+ } else {
+ buf[0] = '0';
+ }
+ }
+ } // precision <= 0
+ else {
+ exp += digits_in_the_first_segment - precision;
+
+ // When precision > 0, we divide the first segment into three
+ // subsegments, each with 9, 9, and 0 ~ 1 digits so that each fits
+ // in 32-bits which usually allows faster calculation than in
+ // 64-bits. Since some compiler (e.g. MSVC) doesn't know how to optimize
+ // division-by-constant for large 64-bit divisors, we do it here
+ // manually. The magic number 7922816251426433760 below is equal to
+ // ceil(2^(64+32) / 10^10).
+ const uint32_t first_subsegment = static_cast<uint32_t>(
+ dragonbox::umul128_upper64(first_segment, 7922816251426433760ULL) >>
+ 32);
+ const uint64_t second_third_subsegments =
+ first_segment - first_subsegment * 10000000000ULL;
+
+ uint64_t prod;
+ uint32_t digits;
+ bool should_round_up;
+ int number_of_digits_to_print = precision > 9 ? 9 : precision;
+
+ // Print a 9-digits subsegment, either the first or the second.
+ auto print_subsegment = [&](uint32_t subsegment, char* buffer) {
+ int number_of_digits_printed = 0;
+
+ // If we want to print an odd number of digits from the subsegment,
+ if ((number_of_digits_to_print & 1) != 0) {
+ // Convert to 64-bit fixed-point fractional form with 1-digit
+ // integer part. The magic number 720575941 is a good enough
+ // approximation of 2^(32 + 24) / 10^8; see
+ // https://jk-jeon.github.io/posts/2022/12/fixed-precision-formatting/#fixed-length-case
+ // for details.
+ prod = ((subsegment * static_cast<uint64_t>(720575941)) >> 24) + 1;
+ digits = static_cast<uint32_t>(prod >> 32);
+ *buffer = static_cast<char>('0' + digits);
+ number_of_digits_printed++;
+ }
+ // If we want to print an even number of digits from the
+ // first_subsegment,
+ else {
+ // Convert to 64-bit fixed-point fractional form with 2-digits
+ // integer part. The magic number 450359963 is a good enough
+ // approximation of 2^(32 + 20) / 10^7; see
+ // https://jk-jeon.github.io/posts/2022/12/fixed-precision-formatting/#fixed-length-case
+ // for details.
+ prod = ((subsegment * static_cast<uint64_t>(450359963)) >> 20) + 1;
+ digits = static_cast<uint32_t>(prod >> 32);
+ copy2(buffer, digits2(digits));
+ number_of_digits_printed += 2;
+ }
+
+ // Print all digit pairs.
+ while (number_of_digits_printed < number_of_digits_to_print) {
+ prod = static_cast<uint32_t>(prod) * static_cast<uint64_t>(100);
+ digits = static_cast<uint32_t>(prod >> 32);
+ copy2(buffer + number_of_digits_printed, digits2(digits));
+ number_of_digits_printed += 2;
+ }
+ };
+
+ // Print first subsegment.
+ print_subsegment(first_subsegment, buf.data());
+
+ // Perform rounding if the first subsegment is the last subsegment to
+ // print.
+ if (precision <= 9) {
+ // Rounding inside the subsegment.
+ // We round-up if:
+ // - either the fractional part is strictly larger than 1/2, or
+ // - the fractional part is exactly 1/2 and the last digit is odd.
+ // We rely on the following observations:
+ // - If fractional_part >= threshold, then the fractional part is
+ // strictly larger than 1/2.
+ // - If the MSB of fractional_part is set, then the fractional part
+ // must be at least 1/2.
+ // - When the MSB of fractional_part is set, either
+ // second_third_subsegments being nonzero or has_more_segments
+ // being true means there are further digits not printed, so the
+ // fractional part is strictly larger than 1/2.
+ if (precision < 9) {
+ uint32_t fractional_part = static_cast<uint32_t>(prod);
+ should_round_up =
+ fractional_part >= fractional_part_rounding_thresholds(
+ 8 - number_of_digits_to_print) ||
+ ((fractional_part >> 31) &
+ ((digits & 1) | (second_third_subsegments != 0) |
+ has_more_segments)) != 0;
+ }
+ // Rounding at the subsegment boundary.
+ // In this case, the fractional part is at least 1/2 if and only if
+ // second_third_subsegments >= 5000000000ULL, and is strictly larger
+ // than 1/2 if we further have either second_third_subsegments >
+ // 5000000000ULL or has_more_segments == true.
+ else {
+ should_round_up = second_third_subsegments > 5000000000ULL ||
+ (second_third_subsegments == 5000000000ULL &&
+ ((digits & 1) != 0 || has_more_segments));
+ }
+ }
+ // Otherwise, print the second subsegment.
+ else {
+ // Compilers are not aware of how to leverage the maximum value of
+ // second_third_subsegments to find out a better magic number which
+ // allows us to eliminate an additional shift. 1844674407370955162 =
+ // ceil(2^64/10) < ceil(2^64*(10^9/(10^10 - 1))).
+ const uint32_t second_subsegment =
+ static_cast<uint32_t>(dragonbox::umul128_upper64(
+ second_third_subsegments, 1844674407370955162ULL));
+ const uint32_t third_subsegment =
+ static_cast<uint32_t>(second_third_subsegments) -
+ second_subsegment * 10;
+
+ number_of_digits_to_print = precision - 9;
+ print_subsegment(second_subsegment, buf.data() + 9);
+
+ // Rounding inside the subsegment.
+ if (precision < 18) {
+ // The condition third_subsegment != 0 implies that the segment was
+ // of 19 digits, so in this case the third segment should be
+ // consisting of a genuine digit from the input.
+ uint32_t fractional_part = static_cast<uint32_t>(prod);
+ should_round_up =
+ fractional_part >= fractional_part_rounding_thresholds(
+ 8 - number_of_digits_to_print) ||
+ ((fractional_part >> 31) &
+ ((digits & 1) | (third_subsegment != 0) |
+ has_more_segments)) != 0;
+ }
+ // Rounding at the subsegment boundary.
+ else {
+ // In this case, the segment must be of 19 digits, thus
+ // the third subsegment should be consisting of a genuine digit from
+ // the input.
+ should_round_up = third_subsegment > 5 ||
+ (third_subsegment == 5 &&
+ ((digits & 1) != 0 || has_more_segments));
+ }
+ }
+
+ // Round-up if necessary.
+ if (should_round_up) {
+ ++buf[precision - 1];
+ for (int i = precision - 1; i > 0 && buf[i] > '9'; --i) {
+ buf[i] = '0';
+ ++buf[i - 1];
+ }
+ if (buf[0] > '9') {
+ buf[0] = '1';
+ if (fixed)
+ buf[precision++] = '0';
+ else
+ ++exp;
+ }
+ }
+ buf.try_resize(to_unsigned(precision));
+ }
+ } // if (digits_in_the_first_segment > precision)
+ else {
+ // Adjust the exponent for its use in Dragon4.
+ exp += digits_in_the_first_segment - 1;
+ }
+ }
+ if (use_dragon) {
+ auto f = basic_fp<uint128_t>();
+ bool is_predecessor_closer = specs.binary32
+ ? f.assign(static_cast<float>(value))
+ : f.assign(converted_value);
+ if (is_predecessor_closer) dragon_flags |= dragon::predecessor_closer;
+ if (fixed) dragon_flags |= dragon::fixed;
+ // Limit precision to the maximum possible number of significant digits in
+ // an IEEE754 double because we don't need to generate zeros.
+ const int max_double_digits = 767;
+ if (precision > max_double_digits) precision = max_double_digits;
+ format_dragon(f, dragon_flags, precision, buf, exp);
+ }
+ if (!fixed && !specs.showpoint) {
+ // Remove trailing zeros.
+ auto num_digits = buf.size();
+ while (num_digits > 0 && buf[num_digits - 1] == '0') {
+ --num_digits;
+ ++exp;
+ }
+ buf.try_resize(num_digits);
+ }
+ return exp;
+}
+template <typename Char, typename OutputIt, typename T>
+FMT_CONSTEXPR20 auto write_float(OutputIt out, T value,
+ format_specs<Char> specs, locale_ref loc)
+ -> OutputIt {
+ float_specs fspecs = parse_float_type_spec(specs);
+ fspecs.sign = specs.sign;
+ if (detail::signbit(value)) { // value < 0 is false for NaN so use signbit.
+ fspecs.sign = sign::minus;
+ value = -value;
+ } else if (fspecs.sign == sign::minus) {
+ fspecs.sign = sign::none;
+ }
+
+ if (!detail::isfinite(value))
+ return write_nonfinite(out, detail::isnan(value), specs, fspecs);
+
+ if (specs.align == align::numeric && fspecs.sign) {
+ auto it = reserve(out, 1);
+ *it++ = detail::sign<Char>(fspecs.sign);
+ out = base_iterator(out, it);
+ fspecs.sign = sign::none;
+ if (specs.width != 0) --specs.width;
+ }
+
+ memory_buffer buffer;
+ if (fspecs.format == float_format::hex) {
+ if (fspecs.sign) buffer.push_back(detail::sign<char>(fspecs.sign));
+ format_hexfloat(convert_float(value), specs.precision, fspecs, buffer);
+ return write_bytes<align::right>(out, {buffer.data(), buffer.size()},
+ specs);
+ }
+ int precision = specs.precision >= 0 || specs.type == presentation_type::none
+ ? specs.precision
+ : 6;
+ if (fspecs.format == float_format::exp) {
+ if (precision == max_value<int>())
+ throw_format_error("number is too big");
+ else
+ ++precision;
+ } else if (fspecs.format != float_format::fixed && precision == 0) {
+ precision = 1;
+ }
+ if (const_check(std::is_same<T, float>())) fspecs.binary32 = true;
+ int exp = format_float(convert_float(value), precision, fspecs, buffer);
+ fspecs.precision = precision;
+ auto f = big_decimal_fp{buffer.data(), static_cast<int>(buffer.size()), exp};
+ return write_float(out, f, specs, fspecs, loc);
+}
+
+template <typename Char, typename OutputIt, typename T,
+ FMT_ENABLE_IF(is_floating_point<T>::value)>
+FMT_CONSTEXPR20 auto write(OutputIt out, T value, format_specs<Char> specs,
+ locale_ref loc = {}) -> OutputIt {
+ if (const_check(!is_supported_floating_point(value))) return out;
+ return specs.localized && write_loc(out, value, specs, loc)
+ ? out
+ : write_float(out, value, specs, loc);
+}
+
+template <typename Char, typename OutputIt, typename T,
+ FMT_ENABLE_IF(is_fast_float<T>::value)>
+FMT_CONSTEXPR20 auto write(OutputIt out, T value) -> OutputIt {
+ if (is_constant_evaluated()) return write(out, value, format_specs<Char>());
+ if (const_check(!is_supported_floating_point(value))) return out;
+
+ auto fspecs = float_specs();
+ if (detail::signbit(value)) {
+ fspecs.sign = sign::minus;
+ value = -value;
+ }
+
+ constexpr auto specs = format_specs<Char>();
+ using floaty = conditional_t<std::is_same<T, long double>::value, double, T>;
+ using floaty_uint = typename dragonbox::float_info<floaty>::carrier_uint;
+ floaty_uint mask = exponent_mask<floaty>();
+ if ((bit_cast<floaty_uint>(value) & mask) == mask)
+ return write_nonfinite(out, std::isnan(value), specs, fspecs);
+
+ auto dec = dragonbox::to_decimal(static_cast<floaty>(value));
+ return write_float(out, dec, specs, fspecs, {});
+}
+
+template <typename Char, typename OutputIt, typename T,
+ FMT_ENABLE_IF(is_floating_point<T>::value &&
+ !is_fast_float<T>::value)>
+inline auto write(OutputIt out, T value) -> OutputIt {
+ return write(out, value, format_specs<Char>());
+}
+
+template <typename Char, typename OutputIt>
+auto write(OutputIt out, monostate, format_specs<Char> = {}, locale_ref = {})
+ -> OutputIt {
+ FMT_ASSERT(false, "");
+ return out;
+}
+
+template <typename Char, typename OutputIt>
+FMT_CONSTEXPR auto write(OutputIt out, basic_string_view<Char> value)
+ -> OutputIt {
+ auto it = reserve(out, value.size());
+ it = copy_str_noinline<Char>(value.begin(), value.end(), it);
+ return base_iterator(out, it);
+}
+
+template <typename Char, typename OutputIt, typename T,
+ FMT_ENABLE_IF(is_string<T>::value)>
+constexpr auto write(OutputIt out, const T& value) -> OutputIt {
+ return write<Char>(out, to_string_view(value));
+}
+
+// FMT_ENABLE_IF() condition separated to workaround an MSVC bug.
+template <
+ typename Char, typename OutputIt, typename T,
+ bool check =
+ std::is_enum<T>::value && !std::is_same<T, Char>::value &&
+ mapped_type_constant<T, basic_format_context<OutputIt, Char>>::value !=
+ type::custom_type,
+ FMT_ENABLE_IF(check)>
+FMT_CONSTEXPR auto write(OutputIt out, T value) -> OutputIt {
+ return write<Char>(out, static_cast<underlying_t<T>>(value));
+}
+
+template <typename Char, typename OutputIt, typename T,
+ FMT_ENABLE_IF(std::is_same<T, bool>::value)>
+FMT_CONSTEXPR auto write(OutputIt out, T value,
+ const format_specs<Char>& specs = {}, locale_ref = {})
+ -> OutputIt {
+ return specs.type != presentation_type::none &&
+ specs.type != presentation_type::string
+ ? write(out, value ? 1 : 0, specs, {})
+ : write_bytes(out, value ? "true" : "false", specs);
+}
+
+template <typename Char, typename OutputIt>
+FMT_CONSTEXPR auto write(OutputIt out, Char value) -> OutputIt {
+ auto it = reserve(out, 1);
+ *it++ = value;
+ return base_iterator(out, it);
+}
+
+template <typename Char, typename OutputIt>
+FMT_CONSTEXPR_CHAR_TRAITS auto write(OutputIt out, const Char* value)
+ -> OutputIt {
+ if (value) return write(out, basic_string_view<Char>(value));
+ throw_format_error("string pointer is null");
+ return out;
+}
+
+template <typename Char, typename OutputIt, typename T,
+ FMT_ENABLE_IF(std::is_same<T, void>::value)>
+auto write(OutputIt out, const T* value, const format_specs<Char>& specs = {},
+ locale_ref = {}) -> OutputIt {
+ return write_ptr<Char>(out, bit_cast<uintptr_t>(value), &specs);
+}
+
+// A write overload that handles implicit conversions.
+template <typename Char, typename OutputIt, typename T,
+ typename Context = basic_format_context<OutputIt, Char>>
+FMT_CONSTEXPR auto write(OutputIt out, const T& value) -> enable_if_t<
+ std::is_class<T>::value && !is_string<T>::value &&
+ !is_floating_point<T>::value && !std::is_same<T, Char>::value &&
+ !std::is_same<T, remove_cvref_t<decltype(arg_mapper<Context>().map(
+ value))>>::value,
+ OutputIt> {
+ return write<Char>(out, arg_mapper<Context>().map(value));
+}
+
+template <typename Char, typename OutputIt, typename T,
+ typename Context = basic_format_context<OutputIt, Char>>
+FMT_CONSTEXPR auto write(OutputIt out, const T& value)
+ -> enable_if_t<mapped_type_constant<T, Context>::value == type::custom_type,
+ OutputIt> {
+ auto formatter = typename Context::template formatter_type<T>();
+ auto parse_ctx = typename Context::parse_context_type({});
+ formatter.parse(parse_ctx);
+ auto ctx = Context(out, {}, {});
+ return formatter.format(value, ctx);
+}
+
+// An argument visitor that formats the argument and writes it via the output
+// iterator. It's a class and not a generic lambda for compatibility with C++11.
+template <typename Char> struct default_arg_formatter {
+ using iterator = buffer_appender<Char>;
+ using context = buffer_context<Char>;
+
+ iterator out;
+ basic_format_args<context> args;
+ locale_ref loc;
+
+ template <typename T> auto operator()(T value) -> iterator {
+ return write<Char>(out, value);
+ }
+ auto operator()(typename basic_format_arg<context>::handle h) -> iterator {
+ basic_format_parse_context<Char> parse_ctx({});
+ context format_ctx(out, args, loc);
+ h.format(parse_ctx, format_ctx);
+ return format_ctx.out();
+ }
+};
+
+template <typename Char> struct arg_formatter {
+ using iterator = buffer_appender<Char>;
+ using context = buffer_context<Char>;
+
+ iterator out;
+ const format_specs<Char>& specs;
+ locale_ref locale;
+
+ template <typename T>
+ FMT_CONSTEXPR FMT_INLINE auto operator()(T value) -> iterator {
+ return detail::write(out, value, specs, locale);
+ }
+ auto operator()(typename basic_format_arg<context>::handle) -> iterator {
+ // User-defined types are handled separately because they require access
+ // to the parse context.
+ return out;
+ }
+};
+
+struct width_checker {
+ template <typename T, FMT_ENABLE_IF(is_integer<T>::value)>
+ FMT_CONSTEXPR auto operator()(T value) -> unsigned long long {
+ if (is_negative(value)) throw_format_error("negative width");
+ return static_cast<unsigned long long>(value);
+ }
+
+ template <typename T, FMT_ENABLE_IF(!is_integer<T>::value)>
+ FMT_CONSTEXPR auto operator()(T) -> unsigned long long {
+ throw_format_error("width is not integer");
+ return 0;
+ }
+};
+
+struct precision_checker {
+ template <typename T, FMT_ENABLE_IF(is_integer<T>::value)>
+ FMT_CONSTEXPR auto operator()(T value) -> unsigned long long {
+ if (is_negative(value)) throw_format_error("negative precision");
+ return static_cast<unsigned long long>(value);
+ }
+
+ template <typename T, FMT_ENABLE_IF(!is_integer<T>::value)>
+ FMT_CONSTEXPR auto operator()(T) -> unsigned long long {
+ throw_format_error("precision is not integer");
+ return 0;
+ }
+};
+
+template <typename Handler, typename FormatArg>
+FMT_CONSTEXPR auto get_dynamic_spec(FormatArg arg) -> int {
+ unsigned long long value = visit_format_arg(Handler(), arg);
+ if (value > to_unsigned(max_value<int>()))
+ throw_format_error("number is too big");
+ return static_cast<int>(value);
+}
+
+template <typename Context, typename ID>
+FMT_CONSTEXPR auto get_arg(Context& ctx, ID id) -> decltype(ctx.arg(id)) {
+ auto arg = ctx.arg(id);
+ if (!arg) ctx.on_error("argument not found");
+ return arg;
+}
+
+template <typename Handler, typename Context>
+FMT_CONSTEXPR void handle_dynamic_spec(int& value,
+ arg_ref<typename Context::char_type> ref,
+ Context& ctx) {
+ switch (ref.kind) {
+ case arg_id_kind::none:
+ break;
+ case arg_id_kind::index:
+ value = detail::get_dynamic_spec<Handler>(get_arg(ctx, ref.val.index));
+ break;
+ case arg_id_kind::name:
+ value = detail::get_dynamic_spec<Handler>(get_arg(ctx, ref.val.name));
+ break;
+ }
+}
+
+#if FMT_USE_USER_DEFINED_LITERALS
+# if FMT_USE_NONTYPE_TEMPLATE_ARGS
+template <typename T, typename Char, size_t N,
+ fmt::detail_exported::fixed_string<Char, N> Str>
+struct statically_named_arg : view {
+ static constexpr auto name = Str.data;
+
+ const T& value;
+ statically_named_arg(const T& v) : value(v) {}
+};
+
+template <typename T, typename Char, size_t N,
+ fmt::detail_exported::fixed_string<Char, N> Str>
+struct is_named_arg<statically_named_arg<T, Char, N, Str>> : std::true_type {};
+
+template <typename T, typename Char, size_t N,
+ fmt::detail_exported::fixed_string<Char, N> Str>
+struct is_statically_named_arg<statically_named_arg<T, Char, N, Str>>
+ : std::true_type {};
+
+template <typename Char, size_t N,
+ fmt::detail_exported::fixed_string<Char, N> Str>
+struct udl_arg {
+ template <typename T> auto operator=(T&& value) const {
+ return statically_named_arg<T, Char, N, Str>(std::forward<T>(value));
+ }
+};
+# else
+template <typename Char> struct udl_arg {
+ const Char* str;
+
+ template <typename T> auto operator=(T&& value) const -> named_arg<Char, T> {
+ return {str, std::forward<T>(value)};
+ }
+};
+# endif
+#endif // FMT_USE_USER_DEFINED_LITERALS
+
+template <typename Locale, typename Char>
+auto vformat(const Locale& loc, basic_string_view<Char> fmt,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args)
+ -> std::basic_string<Char> {
+ auto buf = basic_memory_buffer<Char>();
+ detail::vformat_to(buf, fmt, args, detail::locale_ref(loc));
+ return {buf.data(), buf.size()};
+}
+
+using format_func = void (*)(detail::buffer<char>&, int, const char*);
+
+FMT_API void format_error_code(buffer<char>& out, int error_code,
+ string_view message) noexcept;
+
+FMT_API void report_error(format_func func, int error_code,
+ const char* message) noexcept;
+} // namespace detail
+
+FMT_API auto vsystem_error(int error_code, string_view format_str,
+ format_args args) -> std::system_error;
+
+/**
+ \rst
+ Constructs :class:`std::system_error` with a message formatted with
+ ``fmt::format(fmt, args...)``.
+ *error_code* is a system error code as given by ``errno``.
+
+ **Example**::
+
+ // This throws std::system_error with the description
+ // cannot open file 'madeup': No such file or directory
+ // or similar (system message may vary).
+ const char* filename = "madeup";
+ std::FILE* file = std::fopen(filename, "r");
+ if (!file)
+ throw fmt::system_error(errno, "cannot open file '{}'", filename);
+ \endrst
+ */
+template <typename... T>
+auto system_error(int error_code, format_string<T...> fmt, T&&... args)
+ -> std::system_error {
+ return vsystem_error(error_code, fmt, fmt::make_format_args(args...));
+}
+
+/**
+ \rst
+ Formats an error message for an error returned by an operating system or a
+ language runtime, for example a file opening error, and writes it to *out*.
+ The format is the same as the one used by ``std::system_error(ec, message)``
+ where ``ec`` is ``std::error_code(error_code, std::generic_category()})``.
+ It is implementation-defined but normally looks like:
+
+ .. parsed-literal::
+ *<message>*: *<system-message>*
+
+ where *<message>* is the passed message and *<system-message>* is the system
+ message corresponding to the error code.
+ *error_code* is a system error code as given by ``errno``.
+ \endrst
+ */
+FMT_API void format_system_error(detail::buffer<char>& out, int error_code,
+ const char* message) noexcept;
+
+// Reports a system error without throwing an exception.
+// Can be used to report errors from destructors.
+FMT_API void report_system_error(int error_code, const char* message) noexcept;
+
+/** Fast integer formatter. */
+class format_int {
+ private:
+ // Buffer should be large enough to hold all digits (digits10 + 1),
+ // a sign and a null character.
+ enum { buffer_size = std::numeric_limits<unsigned long long>::digits10 + 3 };
+ mutable char buffer_[buffer_size];
+ char* str_;
+
+ template <typename UInt> auto format_unsigned(UInt value) -> char* {
+ auto n = static_cast<detail::uint32_or_64_or_128_t<UInt>>(value);
+ return detail::format_decimal(buffer_, n, buffer_size - 1).begin;
+ }
+
+ template <typename Int> auto format_signed(Int value) -> char* {
+ auto abs_value = static_cast<detail::uint32_or_64_or_128_t<Int>>(value);
+ bool negative = value < 0;
+ if (negative) abs_value = 0 - abs_value;
+ auto begin = format_unsigned(abs_value);
+ if (negative) *--begin = '-';
+ return begin;
+ }
+
+ public:
+ explicit format_int(int value) : str_(format_signed(value)) {}
+ explicit format_int(long value) : str_(format_signed(value)) {}
+ explicit format_int(long long value) : str_(format_signed(value)) {}
+ explicit format_int(unsigned value) : str_(format_unsigned(value)) {}
+ explicit format_int(unsigned long value) : str_(format_unsigned(value)) {}
+ explicit format_int(unsigned long long value)
+ : str_(format_unsigned(value)) {}
+
+ /** Returns the number of characters written to the output buffer. */
+ auto size() const -> size_t {
+ return detail::to_unsigned(buffer_ - str_ + buffer_size - 1);
+ }
+
+ /**
+ Returns a pointer to the output buffer content. No terminating null
+ character is appended.
+ */
+ auto data() const -> const char* { return str_; }
+
+ /**
+ Returns a pointer to the output buffer content with terminating null
+ character appended.
+ */
+ auto c_str() const -> const char* {
+ buffer_[buffer_size - 1] = '\0';
+ return str_;
+ }
+
+ /**
+ \rst
+ Returns the content of the output buffer as an ``std::string``.
+ \endrst
+ */
+ auto str() const -> std::string { return std::string(str_, size()); }
+};
+
+template <typename T, typename Char>
+struct formatter<T, Char, enable_if_t<detail::has_format_as<T>::value>>
+ : formatter<detail::format_as_t<T>, Char> {
+ template <typename FormatContext>
+ auto format(const T& value, FormatContext& ctx) const -> decltype(ctx.out()) {
+ using base = formatter<detail::format_as_t<T>, Char>;
+ return base::format(format_as(value), ctx);
+ }
+};
+
+#define FMT_FORMAT_AS(Type, Base) \
+ template <typename Char> \
+ struct formatter<Type, Char> : formatter<Base, Char> {}
+
+FMT_FORMAT_AS(signed char, int);
+FMT_FORMAT_AS(unsigned char, unsigned);
+FMT_FORMAT_AS(short, int);
+FMT_FORMAT_AS(unsigned short, unsigned);
+FMT_FORMAT_AS(long, detail::long_type);
+FMT_FORMAT_AS(unsigned long, detail::ulong_type);
+FMT_FORMAT_AS(Char*, const Char*);
+FMT_FORMAT_AS(std::basic_string<Char>, basic_string_view<Char>);
+FMT_FORMAT_AS(std::nullptr_t, const void*);
+FMT_FORMAT_AS(detail::std_string_view<Char>, basic_string_view<Char>);
+FMT_FORMAT_AS(void*, const void*);
+
+template <typename Char, size_t N>
+struct formatter<Char[N], Char> : formatter<basic_string_view<Char>, Char> {};
+
+/**
+ \rst
+ Converts ``p`` to ``const void*`` for pointer formatting.
+
+ **Example**::
+
+ auto s = fmt::format("{}", fmt::ptr(p));
+ \endrst
+ */
+template <typename T> auto ptr(T p) -> const void* {
+ static_assert(std::is_pointer<T>::value, "");
+ return detail::bit_cast<const void*>(p);
+}
+template <typename T, typename Deleter>
+auto ptr(const std::unique_ptr<T, Deleter>& p) -> const void* {
+ return p.get();
+}
+template <typename T> auto ptr(const std::shared_ptr<T>& p) -> const void* {
+ return p.get();
+}
+
+/**
+ \rst
+ Converts ``e`` to the underlying type.
+
+ **Example**::
+
+ enum class color { red, green, blue };
+ auto s = fmt::format("{}", fmt::underlying(color::red));
+ \endrst
+ */
+template <typename Enum>
+constexpr auto underlying(Enum e) noexcept -> underlying_t<Enum> {
+ return static_cast<underlying_t<Enum>>(e);
+}
+
+namespace enums {
+template <typename Enum, FMT_ENABLE_IF(std::is_enum<Enum>::value)>
+constexpr auto format_as(Enum e) noexcept -> underlying_t<Enum> {
+ return static_cast<underlying_t<Enum>>(e);
+}
+} // namespace enums
+
+class bytes {
+ private:
+ string_view data_;
+ friend struct formatter<bytes>;
+
+ public:
+ explicit bytes(string_view data) : data_(data) {}
+};
+
+template <> struct formatter<bytes> {
+ private:
+ detail::dynamic_format_specs<> specs_;
+
+ public:
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> const char* {
+ return parse_format_specs(ctx.begin(), ctx.end(), specs_, ctx,
+ detail::type::string_type);
+ }
+
+ template <typename FormatContext>
+ auto format(bytes b, FormatContext& ctx) -> decltype(ctx.out()) {
+ detail::handle_dynamic_spec<detail::width_checker>(specs_.width,
+ specs_.width_ref, ctx);
+ detail::handle_dynamic_spec<detail::precision_checker>(
+ specs_.precision, specs_.precision_ref, ctx);
+ return detail::write_bytes(ctx.out(), b.data_, specs_);
+ }
+};
+
+// group_digits_view is not derived from view because it copies the argument.
+template <typename T> struct group_digits_view {
+ T value;
+};
+
+/**
+ \rst
+ Returns a view that formats an integer value using ',' as a locale-independent
+ thousands separator.
+
+ **Example**::
+
+ fmt::print("{}", fmt::group_digits(12345));
+ // Output: "12,345"
+ \endrst
+ */
+template <typename T> auto group_digits(T value) -> group_digits_view<T> {
+ return {value};
+}
+
+template <typename T> struct formatter<group_digits_view<T>> : formatter<T> {
+ private:
+ detail::dynamic_format_specs<> specs_;
+
+ public:
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> const char* {
+ return parse_format_specs(ctx.begin(), ctx.end(), specs_, ctx,
+ detail::type::int_type);
+ }
+
+ template <typename FormatContext>
+ auto format(group_digits_view<T> t, FormatContext& ctx)
+ -> decltype(ctx.out()) {
+ detail::handle_dynamic_spec<detail::width_checker>(specs_.width,
+ specs_.width_ref, ctx);
+ detail::handle_dynamic_spec<detail::precision_checker>(
+ specs_.precision, specs_.precision_ref, ctx);
+ return detail::write_int(
+ ctx.out(), static_cast<detail::uint64_or_128_t<T>>(t.value), 0, specs_,
+ detail::digit_grouping<char>("\3", ","));
+ }
+};
+
+template <typename T> struct nested_view {
+ const formatter<T>* fmt;
+ const T* value;
+};
+
+template <typename T> struct formatter<nested_view<T>> {
+ FMT_CONSTEXPR auto parse(format_parse_context& ctx) -> const char* {
+ return ctx.begin();
+ }
+ auto format(nested_view<T> view, format_context& ctx) const
+ -> decltype(ctx.out()) {
+ return view.fmt->format(*view.value, ctx);
+ }
+};
+
+template <typename T> struct nested_formatter {
+ private:
+ int width_;
+ detail::fill_t<char> fill_;
+ align_t align_ : 4;
+ formatter<T> formatter_;
+
+ public:
+ constexpr nested_formatter() : width_(0), align_(align_t::none) {}
+
+ FMT_CONSTEXPR auto parse(format_parse_context& ctx) -> const char* {
+ auto specs = detail::dynamic_format_specs<char>();
+ auto it = parse_format_specs(ctx.begin(), ctx.end(), specs, ctx,
+ detail::type::none_type);
+ width_ = specs.width;
+ fill_ = specs.fill;
+ align_ = specs.align;
+ ctx.advance_to(it);
+ return formatter_.parse(ctx);
+ }
+
+ template <typename F>
+ auto write_padded(format_context& ctx, F write) const -> decltype(ctx.out()) {
+ if (width_ == 0) return write(ctx.out());
+ auto buf = memory_buffer();
+ write(std::back_inserter(buf));
+ auto specs = format_specs<>();
+ specs.width = width_;
+ specs.fill = fill_;
+ specs.align = align_;
+ return detail::write(ctx.out(), string_view(buf.data(), buf.size()), specs);
+ }
+
+ auto nested(const T& value) const -> nested_view<T> {
+ return nested_view<T>{&formatter_, &value};
+ }
+};
+
+// DEPRECATED! join_view will be moved to ranges.h.
+template <typename It, typename Sentinel, typename Char = char>
+struct join_view : detail::view {
+ It begin;
+ Sentinel end;
+ basic_string_view<Char> sep;
+
+ join_view(It b, Sentinel e, basic_string_view<Char> s)
+ : begin(b), end(e), sep(s) {}
+};
+
+template <typename It, typename Sentinel, typename Char>
+struct formatter<join_view<It, Sentinel, Char>, Char> {
+ private:
+ using value_type =
+#ifdef __cpp_lib_ranges
+ std::iter_value_t<It>;
+#else
+ typename std::iterator_traits<It>::value_type;
+#endif
+ formatter<remove_cvref_t<value_type>, Char> value_formatter_;
+
+ public:
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> const Char* {
+ return value_formatter_.parse(ctx);
+ }
+
+ template <typename FormatContext>
+ auto format(const join_view<It, Sentinel, Char>& value,
+ FormatContext& ctx) const -> decltype(ctx.out()) {
+ auto it = value.begin;
+ auto out = ctx.out();
+ if (it != value.end) {
+ out = value_formatter_.format(*it, ctx);
+ ++it;
+ while (it != value.end) {
+ out = detail::copy_str<Char>(value.sep.begin(), value.sep.end(), out);
+ ctx.advance_to(out);
+ out = value_formatter_.format(*it, ctx);
+ ++it;
+ }
+ }
+ return out;
+ }
+};
+
+/**
+ Returns a view that formats the iterator range `[begin, end)` with elements
+ separated by `sep`.
+ */
+template <typename It, typename Sentinel>
+auto join(It begin, Sentinel end, string_view sep) -> join_view<It, Sentinel> {
+ return {begin, end, sep};
+}
+
+/**
+ \rst
+ Returns a view that formats `range` with elements separated by `sep`.
+
+ **Example**::
+
+ std::vector<int> v = {1, 2, 3};
+ fmt::print("{}", fmt::join(v, ", "));
+ // Output: "1, 2, 3"
+
+ ``fmt::join`` applies passed format specifiers to the range elements::
+
+ fmt::print("{:02}", fmt::join(v, ", "));
+ // Output: "01, 02, 03"
+ \endrst
+ */
+template <typename Range>
+auto join(Range&& range, string_view sep)
+ -> join_view<detail::iterator_t<Range>, detail::sentinel_t<Range>> {
+ return join(std::begin(range), std::end(range), sep);
+}
+
+/**
+ \rst
+ Converts *value* to ``std::string`` using the default format for type *T*.
+
+ **Example**::
+
+ #include <fmt/format.h>
+
+ std::string answer = fmt::to_string(42);
+ \endrst
+ */
+template <typename T, FMT_ENABLE_IF(!std::is_integral<T>::value &&
+ !detail::has_format_as<T>::value)>
+inline auto to_string(const T& value) -> std::string {
+ auto buffer = memory_buffer();
+ detail::write<char>(appender(buffer), value);
+ return {buffer.data(), buffer.size()};
+}
+
+template <typename T, FMT_ENABLE_IF(std::is_integral<T>::value)>
+FMT_NODISCARD inline auto to_string(T value) -> std::string {
+ // The buffer should be large enough to store the number including the sign
+ // or "false" for bool.
+ constexpr int max_size = detail::digits10<T>() + 2;
+ char buffer[max_size > 5 ? static_cast<unsigned>(max_size) : 5];
+ char* begin = buffer;
+ return std::string(begin, detail::write<char>(begin, value));
+}
+
+template <typename Char, size_t SIZE>
+FMT_NODISCARD auto to_string(const basic_memory_buffer<Char, SIZE>& buf)
+ -> std::basic_string<Char> {
+ auto size = buf.size();
+ detail::assume(size < std::basic_string<Char>().max_size());
+ return std::basic_string<Char>(buf.data(), size);
+}
+
+template <typename T, FMT_ENABLE_IF(!std::is_integral<T>::value &&
+ detail::has_format_as<T>::value)>
+inline auto to_string(const T& value) -> std::string {
+ return to_string(format_as(value));
+}
+
+FMT_END_EXPORT
+
+namespace detail {
+
+template <typename Char>
+void vformat_to(buffer<Char>& buf, basic_string_view<Char> fmt,
+ typename vformat_args<Char>::type args, locale_ref loc) {
+ auto out = buffer_appender<Char>(buf);
+ if (fmt.size() == 2 && equal2(fmt.data(), "{}")) {
+ auto arg = args.get(0);
+ if (!arg) throw_format_error("argument not found");
+ visit_format_arg(default_arg_formatter<Char>{out, args, loc}, arg);
+ return;
+ }
+
+ struct format_handler : error_handler {
+ basic_format_parse_context<Char> parse_context;
+ buffer_context<Char> context;
+
+ format_handler(buffer_appender<Char> p_out, basic_string_view<Char> str,
+ basic_format_args<buffer_context<Char>> p_args,
+ locale_ref p_loc)
+ : parse_context(str), context(p_out, p_args, p_loc) {}
+
+ void on_text(const Char* begin, const Char* end) {
+ auto text = basic_string_view<Char>(begin, to_unsigned(end - begin));
+ context.advance_to(write<Char>(context.out(), text));
+ }
+
+ FMT_CONSTEXPR auto on_arg_id() -> int {
+ return parse_context.next_arg_id();
+ }
+ FMT_CONSTEXPR auto on_arg_id(int id) -> int {
+ return parse_context.check_arg_id(id), id;
+ }
+ FMT_CONSTEXPR auto on_arg_id(basic_string_view<Char> id) -> int {
+ int arg_id = context.arg_id(id);
+ if (arg_id < 0) throw_format_error("argument not found");
+ return arg_id;
+ }
+
+ FMT_INLINE void on_replacement_field(int id, const Char*) {
+ auto arg = get_arg(context, id);
+ context.advance_to(visit_format_arg(
+ default_arg_formatter<Char>{context.out(), context.args(),
+ context.locale()},
+ arg));
+ }
+
+ auto on_format_specs(int id, const Char* begin, const Char* end)
+ -> const Char* {
+ auto arg = get_arg(context, id);
+ // Not using a visitor for custom types gives better codegen.
+ if (arg.format_custom(begin, parse_context, context))
+ return parse_context.begin();
+ auto specs = detail::dynamic_format_specs<Char>();
+ begin = parse_format_specs(begin, end, specs, parse_context, arg.type());
+ detail::handle_dynamic_spec<detail::width_checker>(
+ specs.width, specs.width_ref, context);
+ detail::handle_dynamic_spec<detail::precision_checker>(
+ specs.precision, specs.precision_ref, context);
+ if (begin == end || *begin != '}')
+ throw_format_error("missing '}' in format string");
+ auto f = arg_formatter<Char>{context.out(), specs, context.locale()};
+ context.advance_to(visit_format_arg(f, arg));
+ return begin;
+ }
+ };
+ detail::parse_format_string<false>(fmt, format_handler(out, fmt, args, loc));
+}
+
+FMT_BEGIN_EXPORT
+
+#ifndef FMT_HEADER_ONLY
+extern template FMT_API void vformat_to(buffer<char>&, string_view,
+ typename vformat_args<>::type,
+ locale_ref);
+extern template FMT_API auto thousands_sep_impl<char>(locale_ref)
+ -> thousands_sep_result<char>;
+extern template FMT_API auto thousands_sep_impl<wchar_t>(locale_ref)
+ -> thousands_sep_result<wchar_t>;
+extern template FMT_API auto decimal_point_impl(locale_ref) -> char;
+extern template FMT_API auto decimal_point_impl(locale_ref) -> wchar_t;
+#endif // FMT_HEADER_ONLY
+
+} // namespace detail
+
+#if FMT_USE_USER_DEFINED_LITERALS
+inline namespace literals {
+/**
+ \rst
+ User-defined literal equivalent of :func:`fmt::arg`.
+
+ **Example**::
+
+ using namespace fmt::literals;
+ fmt::print("Elapsed time: {s:.2f} seconds", "s"_a=1.23);
+ \endrst
+ */
+# if FMT_USE_NONTYPE_TEMPLATE_ARGS
+template <detail_exported::fixed_string Str> constexpr auto operator""_a() {
+ using char_t = remove_cvref_t<decltype(Str.data[0])>;
+ return detail::udl_arg<char_t, sizeof(Str.data) / sizeof(char_t), Str>();
+}
+# else
+constexpr auto operator""_a(const char* s, size_t) -> detail::udl_arg<char> {
+ return {s};
+}
+# endif
+} // namespace literals
+#endif // FMT_USE_USER_DEFINED_LITERALS
+
+template <typename Locale, FMT_ENABLE_IF(detail::is_locale<Locale>::value)>
+inline auto vformat(const Locale& loc, string_view fmt, format_args args)
+ -> std::string {
+ return detail::vformat(loc, fmt, args);
+}
+
+template <typename Locale, typename... T,
+ FMT_ENABLE_IF(detail::is_locale<Locale>::value)>
+inline auto format(const Locale& loc, format_string<T...> fmt, T&&... args)
+ -> std::string {
+ return fmt::vformat(loc, string_view(fmt), fmt::make_format_args(args...));
+}
+
+template <typename OutputIt, typename Locale,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value&&
+ detail::is_locale<Locale>::value)>
+auto vformat_to(OutputIt out, const Locale& loc, string_view fmt,
+ format_args args) -> OutputIt {
+ using detail::get_buffer;
+ auto&& buf = get_buffer<char>(out);
+ detail::vformat_to(buf, fmt, args, detail::locale_ref(loc));
+ return detail::get_iterator(buf, out);
+}
+
+template <typename OutputIt, typename Locale, typename... T,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value&&
+ detail::is_locale<Locale>::value)>
+FMT_INLINE auto format_to(OutputIt out, const Locale& loc,
+ format_string<T...> fmt, T&&... args) -> OutputIt {
+ return vformat_to(out, loc, fmt, fmt::make_format_args(args...));
+}
+
+template <typename Locale, typename... T,
+ FMT_ENABLE_IF(detail::is_locale<Locale>::value)>
+FMT_NODISCARD FMT_INLINE auto formatted_size(const Locale& loc,
+ format_string<T...> fmt,
+ T&&... args) -> size_t {
+ auto buf = detail::counting_buffer<>();
+ detail::vformat_to<char>(buf, fmt, fmt::make_format_args(args...),
+ detail::locale_ref(loc));
+ return buf.count();
+}
+
+FMT_END_EXPORT
+
+template <typename T, typename Char>
+template <typename FormatContext>
+FMT_CONSTEXPR FMT_INLINE auto
+formatter<T, Char,
+ enable_if_t<detail::type_constant<T, Char>::value !=
+ detail::type::custom_type>>::format(const T& val,
+ FormatContext& ctx)
+ const -> decltype(ctx.out()) {
+ if (specs_.width_ref.kind == detail::arg_id_kind::none &&
+ specs_.precision_ref.kind == detail::arg_id_kind::none) {
+ return detail::write<Char>(ctx.out(), val, specs_, ctx.locale());
+ }
+ auto specs = specs_;
+ detail::handle_dynamic_spec<detail::width_checker>(specs.width,
+ specs.width_ref, ctx);
+ detail::handle_dynamic_spec<detail::precision_checker>(
+ specs.precision, specs.precision_ref, ctx);
+ return detail::write<Char>(ctx.out(), val, specs, ctx.locale());
+}
+
+FMT_END_NAMESPACE
+
+#ifdef FMT_HEADER_ONLY
+# define FMT_FUNC inline
+# include "format-inl.h"
+#else
+# define FMT_FUNC
+#endif
+
+#endif // FMT_FORMAT_H_
--- /dev/null
+// Formatting library for C++ - optional OS-specific functionality
+//
+// Copyright (c) 2012 - present, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_OS_H_
+#define FMT_OS_H_
+
+#include <cerrno>
+#include <cstddef>
+#include <cstdio>
+#include <system_error> // std::system_error
+
+#include "format.h"
+
+#if defined __APPLE__ || defined(__FreeBSD__)
+# if FMT_HAS_INCLUDE(<xlocale.h>)
+# include <xlocale.h> // for LC_NUMERIC_MASK on OS X
+# endif
+#endif
+
+#ifndef FMT_USE_FCNTL
+// UWP doesn't provide _pipe.
+# if FMT_HAS_INCLUDE("winapifamily.h")
+# include <winapifamily.h>
+# endif
+# if (FMT_HAS_INCLUDE(<fcntl.h>) || defined(__APPLE__) || \
+ defined(__linux__)) && \
+ (!defined(WINAPI_FAMILY) || \
+ (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP))
+# include <fcntl.h> // for O_RDONLY
+# define FMT_USE_FCNTL 1
+# else
+# define FMT_USE_FCNTL 0
+# endif
+#endif
+
+#ifndef FMT_POSIX
+# if defined(_WIN32) && !defined(__MINGW32__)
+// Fix warnings about deprecated symbols.
+# define FMT_POSIX(call) _##call
+# else
+# define FMT_POSIX(call) call
+# endif
+#endif
+
+// Calls to system functions are wrapped in FMT_SYSTEM for testability.
+#ifdef FMT_SYSTEM
+# define FMT_HAS_SYSTEM
+# define FMT_POSIX_CALL(call) FMT_SYSTEM(call)
+#else
+# define FMT_SYSTEM(call) ::call
+# ifdef _WIN32
+// Fix warnings about deprecated symbols.
+# define FMT_POSIX_CALL(call) ::_##call
+# else
+# define FMT_POSIX_CALL(call) ::call
+# endif
+#endif
+
+// Retries the expression while it evaluates to error_result and errno
+// equals to EINTR.
+#ifndef _WIN32
+# define FMT_RETRY_VAL(result, expression, error_result) \
+ do { \
+ (result) = (expression); \
+ } while ((result) == (error_result) && errno == EINTR)
+#else
+# define FMT_RETRY_VAL(result, expression, error_result) result = (expression)
+#endif
+
+#define FMT_RETRY(result, expression) FMT_RETRY_VAL(result, expression, -1)
+
+FMT_BEGIN_NAMESPACE
+FMT_BEGIN_EXPORT
+
+/**
+ \rst
+ A reference to a null-terminated string. It can be constructed from a C
+ string or ``std::string``.
+
+ You can use one of the following type aliases for common character types:
+
+ +---------------+-----------------------------+
+ | Type | Definition |
+ +===============+=============================+
+ | cstring_view | basic_cstring_view<char> |
+ +---------------+-----------------------------+
+ | wcstring_view | basic_cstring_view<wchar_t> |
+ +---------------+-----------------------------+
+
+ This class is most useful as a parameter type to allow passing
+ different types of strings to a function, for example::
+
+ template <typename... Args>
+ std::string format(cstring_view format_str, const Args & ... args);
+
+ format("{}", 42);
+ format(std::string("{}"), 42);
+ \endrst
+ */
+template <typename Char> class basic_cstring_view {
+ private:
+ const Char* data_;
+
+ public:
+ /** Constructs a string reference object from a C string. */
+ basic_cstring_view(const Char* s) : data_(s) {}
+
+ /**
+ \rst
+ Constructs a string reference from an ``std::string`` object.
+ \endrst
+ */
+ basic_cstring_view(const std::basic_string<Char>& s) : data_(s.c_str()) {}
+
+ /** Returns the pointer to a C string. */
+ auto c_str() const -> const Char* { return data_; }
+};
+
+using cstring_view = basic_cstring_view<char>;
+using wcstring_view = basic_cstring_view<wchar_t>;
+
+#ifdef _WIN32
+FMT_API const std::error_category& system_category() noexcept;
+
+namespace detail {
+FMT_API void format_windows_error(buffer<char>& out, int error_code,
+ const char* message) noexcept;
+}
+
+FMT_API std::system_error vwindows_error(int error_code, string_view format_str,
+ format_args args);
+
+/**
+ \rst
+ Constructs a :class:`std::system_error` object with the description
+ of the form
+
+ .. parsed-literal::
+ *<message>*: *<system-message>*
+
+ where *<message>* is the formatted message and *<system-message>* is the
+ system message corresponding to the error code.
+ *error_code* is a Windows error code as given by ``GetLastError``.
+ If *error_code* is not a valid error code such as -1, the system message
+ will look like "error -1".
+
+ **Example**::
+
+ // This throws a system_error with the description
+ // cannot open file 'madeup': The system cannot find the file specified.
+ // or similar (system message may vary).
+ const char *filename = "madeup";
+ LPOFSTRUCT of = LPOFSTRUCT();
+ HFILE file = OpenFile(filename, &of, OF_READ);
+ if (file == HFILE_ERROR) {
+ throw fmt::windows_error(GetLastError(),
+ "cannot open file '{}'", filename);
+ }
+ \endrst
+*/
+template <typename... Args>
+std::system_error windows_error(int error_code, string_view message,
+ const Args&... args) {
+ return vwindows_error(error_code, message, fmt::make_format_args(args...));
+}
+
+// Reports a Windows error without throwing an exception.
+// Can be used to report errors from destructors.
+FMT_API void report_windows_error(int error_code, const char* message) noexcept;
+#else
+inline auto system_category() noexcept -> const std::error_category& {
+ return std::system_category();
+}
+#endif // _WIN32
+
+// std::system is not available on some platforms such as iOS (#2248).
+#ifdef __OSX__
+template <typename S, typename... Args, typename Char = char_t<S>>
+void say(const S& format_str, Args&&... args) {
+ std::system(format("say \"{}\"", format(format_str, args...)).c_str());
+}
+#endif
+
+// A buffered file.
+class buffered_file {
+ private:
+ FILE* file_;
+
+ friend class file;
+
+ explicit buffered_file(FILE* f) : file_(f) {}
+
+ public:
+ buffered_file(const buffered_file&) = delete;
+ void operator=(const buffered_file&) = delete;
+
+ // Constructs a buffered_file object which doesn't represent any file.
+ buffered_file() noexcept : file_(nullptr) {}
+
+ // Destroys the object closing the file it represents if any.
+ FMT_API ~buffered_file() noexcept;
+
+ public:
+ buffered_file(buffered_file&& other) noexcept : file_(other.file_) {
+ other.file_ = nullptr;
+ }
+
+ auto operator=(buffered_file&& other) -> buffered_file& {
+ close();
+ file_ = other.file_;
+ other.file_ = nullptr;
+ return *this;
+ }
+
+ // Opens a file.
+ FMT_API buffered_file(cstring_view filename, cstring_view mode);
+
+ // Closes the file.
+ FMT_API void close();
+
+ // Returns the pointer to a FILE object representing this file.
+ auto get() const noexcept -> FILE* { return file_; }
+
+ FMT_API auto descriptor() const -> int;
+
+ void vprint(string_view format_str, format_args args) {
+ fmt::vprint(file_, format_str, args);
+ }
+
+ template <typename... Args>
+ inline void print(string_view format_str, const Args&... args) {
+ vprint(format_str, fmt::make_format_args(args...));
+ }
+};
+
+#if FMT_USE_FCNTL
+// A file. Closed file is represented by a file object with descriptor -1.
+// Methods that are not declared with noexcept may throw
+// fmt::system_error in case of failure. Note that some errors such as
+// closing the file multiple times will cause a crash on Windows rather
+// than an exception. You can get standard behavior by overriding the
+// invalid parameter handler with _set_invalid_parameter_handler.
+class FMT_API file {
+ private:
+ int fd_; // File descriptor.
+
+ // Constructs a file object with a given descriptor.
+ explicit file(int fd) : fd_(fd) {}
+
+ public:
+ // Possible values for the oflag argument to the constructor.
+ enum {
+ RDONLY = FMT_POSIX(O_RDONLY), // Open for reading only.
+ WRONLY = FMT_POSIX(O_WRONLY), // Open for writing only.
+ RDWR = FMT_POSIX(O_RDWR), // Open for reading and writing.
+ CREATE = FMT_POSIX(O_CREAT), // Create if the file doesn't exist.
+ APPEND = FMT_POSIX(O_APPEND), // Open in append mode.
+ TRUNC = FMT_POSIX(O_TRUNC) // Truncate the content of the file.
+ };
+
+ // Constructs a file object which doesn't represent any file.
+ file() noexcept : fd_(-1) {}
+
+ // Opens a file and constructs a file object representing this file.
+ file(cstring_view path, int oflag);
+
+ public:
+ file(const file&) = delete;
+ void operator=(const file&) = delete;
+
+ file(file&& other) noexcept : fd_(other.fd_) { other.fd_ = -1; }
+
+ // Move assignment is not noexcept because close may throw.
+ auto operator=(file&& other) -> file& {
+ close();
+ fd_ = other.fd_;
+ other.fd_ = -1;
+ return *this;
+ }
+
+ // Destroys the object closing the file it represents if any.
+ ~file() noexcept;
+
+ // Returns the file descriptor.
+ auto descriptor() const noexcept -> int { return fd_; }
+
+ // Closes the file.
+ void close();
+
+ // Returns the file size. The size has signed type for consistency with
+ // stat::st_size.
+ auto size() const -> long long;
+
+ // Attempts to read count bytes from the file into the specified buffer.
+ auto read(void* buffer, size_t count) -> size_t;
+
+ // Attempts to write count bytes from the specified buffer to the file.
+ auto write(const void* buffer, size_t count) -> size_t;
+
+ // Duplicates a file descriptor with the dup function and returns
+ // the duplicate as a file object.
+ static auto dup(int fd) -> file;
+
+ // Makes fd be the copy of this file descriptor, closing fd first if
+ // necessary.
+ void dup2(int fd);
+
+ // Makes fd be the copy of this file descriptor, closing fd first if
+ // necessary.
+ void dup2(int fd, std::error_code& ec) noexcept;
+
+ // Creates a pipe setting up read_end and write_end file objects for reading
+ // and writing respectively.
+ // DEPRECATED! Taking files as out parameters is deprecated.
+ static void pipe(file& read_end, file& write_end);
+
+ // Creates a buffered_file object associated with this file and detaches
+ // this file object from the file.
+ auto fdopen(const char* mode) -> buffered_file;
+
+# if defined(_WIN32) && !defined(__MINGW32__)
+ // Opens a file and constructs a file object representing this file by
+ // wcstring_view filename. Windows only.
+ static file open_windows_file(wcstring_view path, int oflag);
+# endif
+};
+
+// Returns the memory page size.
+auto getpagesize() -> long;
+
+namespace detail {
+
+struct buffer_size {
+ buffer_size() = default;
+ size_t value = 0;
+ auto operator=(size_t val) const -> buffer_size {
+ auto bs = buffer_size();
+ bs.value = val;
+ return bs;
+ }
+};
+
+struct ostream_params {
+ int oflag = file::WRONLY | file::CREATE | file::TRUNC;
+ size_t buffer_size = BUFSIZ > 32768 ? BUFSIZ : 32768;
+
+ ostream_params() {}
+
+ template <typename... T>
+ ostream_params(T... params, int new_oflag) : ostream_params(params...) {
+ oflag = new_oflag;
+ }
+
+ template <typename... T>
+ ostream_params(T... params, detail::buffer_size bs)
+ : ostream_params(params...) {
+ this->buffer_size = bs.value;
+ }
+
+// Intel has a bug that results in failure to deduce a constructor
+// for empty parameter packs.
+# if defined(__INTEL_COMPILER) && __INTEL_COMPILER < 2000
+ ostream_params(int new_oflag) : oflag(new_oflag) {}
+ ostream_params(detail::buffer_size bs) : buffer_size(bs.value) {}
+# endif
+};
+
+class file_buffer final : public buffer<char> {
+ file file_;
+
+ FMT_API void grow(size_t) override;
+
+ public:
+ FMT_API file_buffer(cstring_view path, const ostream_params& params);
+ FMT_API file_buffer(file_buffer&& other);
+ FMT_API ~file_buffer();
+
+ void flush() {
+ if (size() == 0) return;
+ file_.write(data(), size() * sizeof(data()[0]));
+ clear();
+ }
+
+ void close() {
+ flush();
+ file_.close();
+ }
+};
+
+} // namespace detail
+
+// Added {} below to work around default constructor error known to
+// occur in Xcode versions 7.2.1 and 8.2.1.
+constexpr detail::buffer_size buffer_size{};
+
+/** A fast output stream which is not thread-safe. */
+class FMT_API ostream {
+ private:
+ FMT_MSC_WARNING(suppress : 4251)
+ detail::file_buffer buffer_;
+
+ ostream(cstring_view path, const detail::ostream_params& params)
+ : buffer_(path, params) {}
+
+ public:
+ ostream(ostream&& other) : buffer_(std::move(other.buffer_)) {}
+
+ ~ostream();
+
+ void flush() { buffer_.flush(); }
+
+ template <typename... T>
+ friend auto output_file(cstring_view path, T... params) -> ostream;
+
+ void close() { buffer_.close(); }
+
+ /**
+ Formats ``args`` according to specifications in ``fmt`` and writes the
+ output to the file.
+ */
+ template <typename... T> void print(format_string<T...> fmt, T&&... args) {
+ vformat_to(std::back_inserter(buffer_), fmt,
+ fmt::make_format_args(args...));
+ }
+};
+
+/**
+ \rst
+ Opens a file for writing. Supported parameters passed in *params*:
+
+ * ``<integer>``: Flags passed to `open
+ <https://pubs.opengroup.org/onlinepubs/007904875/functions/open.html>`_
+ (``file::WRONLY | file::CREATE | file::TRUNC`` by default)
+ * ``buffer_size=<integer>``: Output buffer size
+
+ **Example**::
+
+ auto out = fmt::output_file("guide.txt");
+ out.print("Don't {}", "Panic");
+ \endrst
+ */
+template <typename... T>
+inline auto output_file(cstring_view path, T... params) -> ostream {
+ return {path, detail::ostream_params(params...)};
+}
+#endif // FMT_USE_FCNTL
+
+FMT_END_EXPORT
+FMT_END_NAMESPACE
+
+#endif // FMT_OS_H_
--- /dev/null
+// Formatting library for C++ - std::ostream support
+//
+// Copyright (c) 2012 - present, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_OSTREAM_H_
+#define FMT_OSTREAM_H_
+
+#include <fstream> // std::filebuf
+
+#ifdef _WIN32
+# ifdef __GLIBCXX__
+# include <ext/stdio_filebuf.h>
+# include <ext/stdio_sync_filebuf.h>
+# endif
+# include <io.h>
+#endif
+
+#include "format.h"
+
+FMT_BEGIN_NAMESPACE
+namespace detail {
+
+template <typename Streambuf> class formatbuf : public Streambuf {
+ private:
+ using char_type = typename Streambuf::char_type;
+ using streamsize = decltype(std::declval<Streambuf>().sputn(nullptr, 0));
+ using int_type = typename Streambuf::int_type;
+ using traits_type = typename Streambuf::traits_type;
+
+ buffer<char_type>& buffer_;
+
+ public:
+ explicit formatbuf(buffer<char_type>& buf) : buffer_(buf) {}
+
+ protected:
+ // The put area is always empty. This makes the implementation simpler and has
+ // the advantage that the streambuf and the buffer are always in sync and
+ // sputc never writes into uninitialized memory. A disadvantage is that each
+ // call to sputc always results in a (virtual) call to overflow. There is no
+ // disadvantage here for sputn since this always results in a call to xsputn.
+
+ auto overflow(int_type ch) -> int_type override {
+ if (!traits_type::eq_int_type(ch, traits_type::eof()))
+ buffer_.push_back(static_cast<char_type>(ch));
+ return ch;
+ }
+
+ auto xsputn(const char_type* s, streamsize count) -> streamsize override {
+ buffer_.append(s, s + count);
+ return count;
+ }
+};
+
+// Generate a unique explicit instantion in every translation unit using a tag
+// type in an anonymous namespace.
+namespace {
+struct file_access_tag {};
+} // namespace
+template <typename Tag, typename BufType, FILE* BufType::*FileMemberPtr>
+class file_access {
+ friend auto get_file(BufType& obj) -> FILE* { return obj.*FileMemberPtr; }
+};
+
+#if FMT_MSC_VERSION
+template class file_access<file_access_tag, std::filebuf,
+ &std::filebuf::_Myfile>;
+auto get_file(std::filebuf&) -> FILE*;
+#endif
+
+inline auto write_ostream_unicode(std::ostream& os, fmt::string_view data)
+ -> bool {
+ FILE* f = nullptr;
+#if FMT_MSC_VERSION
+ if (auto* buf = dynamic_cast<std::filebuf*>(os.rdbuf()))
+ f = get_file(*buf);
+ else
+ return false;
+#elif defined(_WIN32) && defined(__GLIBCXX__)
+ auto* rdbuf = os.rdbuf();
+ if (auto* sfbuf = dynamic_cast<__gnu_cxx::stdio_sync_filebuf<char>*>(rdbuf))
+ f = sfbuf->file();
+ else if (auto* fbuf = dynamic_cast<__gnu_cxx::stdio_filebuf<char>*>(rdbuf))
+ f = fbuf->file();
+ else
+ return false;
+#else
+ ignore_unused(os, data, f);
+#endif
+#ifdef _WIN32
+ if (f) {
+ int fd = _fileno(f);
+ if (_isatty(fd)) {
+ os.flush();
+ return write_console(fd, data);
+ }
+ }
+#endif
+ return false;
+}
+inline auto write_ostream_unicode(std::wostream&,
+ fmt::basic_string_view<wchar_t>) -> bool {
+ return false;
+}
+
+// Write the content of buf to os.
+// It is a separate function rather than a part of vprint to simplify testing.
+template <typename Char>
+void write_buffer(std::basic_ostream<Char>& os, buffer<Char>& buf) {
+ const Char* buf_data = buf.data();
+ using unsigned_streamsize = std::make_unsigned<std::streamsize>::type;
+ unsigned_streamsize size = buf.size();
+ unsigned_streamsize max_size = to_unsigned(max_value<std::streamsize>());
+ do {
+ unsigned_streamsize n = size <= max_size ? size : max_size;
+ os.write(buf_data, static_cast<std::streamsize>(n));
+ buf_data += n;
+ size -= n;
+ } while (size != 0);
+}
+
+template <typename Char, typename T>
+void format_value(buffer<Char>& buf, const T& value) {
+ auto&& format_buf = formatbuf<std::basic_streambuf<Char>>(buf);
+ auto&& output = std::basic_ostream<Char>(&format_buf);
+#if !defined(FMT_STATIC_THOUSANDS_SEPARATOR)
+ output.imbue(std::locale::classic()); // The default is always unlocalized.
+#endif
+ output << value;
+ output.exceptions(std::ios_base::failbit | std::ios_base::badbit);
+}
+
+template <typename T> struct streamed_view {
+ const T& value;
+};
+
+} // namespace detail
+
+// Formats an object of type T that has an overloaded ostream operator<<.
+template <typename Char>
+struct basic_ostream_formatter : formatter<basic_string_view<Char>, Char> {
+ void set_debug_format() = delete;
+
+ template <typename T, typename OutputIt>
+ auto format(const T& value, basic_format_context<OutputIt, Char>& ctx) const
+ -> OutputIt {
+ auto buffer = basic_memory_buffer<Char>();
+ detail::format_value(buffer, value);
+ return formatter<basic_string_view<Char>, Char>::format(
+ {buffer.data(), buffer.size()}, ctx);
+ }
+};
+
+using ostream_formatter = basic_ostream_formatter<char>;
+
+template <typename T, typename Char>
+struct formatter<detail::streamed_view<T>, Char>
+ : basic_ostream_formatter<Char> {
+ template <typename OutputIt>
+ auto format(detail::streamed_view<T> view,
+ basic_format_context<OutputIt, Char>& ctx) const -> OutputIt {
+ return basic_ostream_formatter<Char>::format(view.value, ctx);
+ }
+};
+
+/**
+ \rst
+ Returns a view that formats `value` via an ostream ``operator<<``.
+
+ **Example**::
+
+ fmt::print("Current thread id: {}\n",
+ fmt::streamed(std::this_thread::get_id()));
+ \endrst
+ */
+template <typename T>
+constexpr auto streamed(const T& value) -> detail::streamed_view<T> {
+ return {value};
+}
+
+namespace detail {
+
+inline void vprint_directly(std::ostream& os, string_view format_str,
+ format_args args) {
+ auto buffer = memory_buffer();
+ detail::vformat_to(buffer, format_str, args);
+ detail::write_buffer(os, buffer);
+}
+
+} // namespace detail
+
+FMT_EXPORT template <typename Char>
+void vprint(std::basic_ostream<Char>& os,
+ basic_string_view<type_identity_t<Char>> format_str,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args) {
+ auto buffer = basic_memory_buffer<Char>();
+ detail::vformat_to(buffer, format_str, args);
+ if (detail::write_ostream_unicode(os, {buffer.data(), buffer.size()})) return;
+ detail::write_buffer(os, buffer);
+}
+
+/**
+ \rst
+ Prints formatted data to the stream *os*.
+
+ **Example**::
+
+ fmt::print(cerr, "Don't {}!", "panic");
+ \endrst
+ */
+FMT_EXPORT template <typename... T>
+void print(std::ostream& os, format_string<T...> fmt, T&&... args) {
+ const auto& vargs = fmt::make_format_args(args...);
+ if (detail::is_utf8())
+ vprint(os, fmt, vargs);
+ else
+ detail::vprint_directly(os, fmt, vargs);
+}
+
+FMT_EXPORT
+template <typename... Args>
+void print(std::wostream& os,
+ basic_format_string<wchar_t, type_identity_t<Args>...> fmt,
+ Args&&... args) {
+ vprint(os, fmt, fmt::make_format_args<buffer_context<wchar_t>>(args...));
+}
+
+FMT_EXPORT template <typename... T>
+void println(std::ostream& os, format_string<T...> fmt, T&&... args) {
+ fmt::print(os, "{}\n", fmt::format(fmt, std::forward<T>(args)...));
+}
+
+FMT_EXPORT
+template <typename... Args>
+void println(std::wostream& os,
+ basic_format_string<wchar_t, type_identity_t<Args>...> fmt,
+ Args&&... args) {
+ print(os, L"{}\n", fmt::format(fmt, std::forward<Args>(args)...));
+}
+
+FMT_END_NAMESPACE
+
+#endif // FMT_OSTREAM_H_
--- /dev/null
+// Formatting library for C++ - legacy printf implementation
+//
+// Copyright (c) 2012 - 2016, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_PRINTF_H_
+#define FMT_PRINTF_H_
+
+#include <algorithm> // std::max
+#include <limits> // std::numeric_limits
+
+#include "format.h"
+
+FMT_BEGIN_NAMESPACE
+FMT_BEGIN_EXPORT
+
+template <typename T> struct printf_formatter {
+ printf_formatter() = delete;
+};
+
+template <typename Char> class basic_printf_context {
+ private:
+ detail::buffer_appender<Char> out_;
+ basic_format_args<basic_printf_context> args_;
+
+ static_assert(std::is_same<Char, char>::value ||
+ std::is_same<Char, wchar_t>::value,
+ "Unsupported code unit type.");
+
+ public:
+ using char_type = Char;
+ using parse_context_type = basic_format_parse_context<Char>;
+ template <typename T> using formatter_type = printf_formatter<T>;
+
+ /**
+ \rst
+ Constructs a ``printf_context`` object. References to the arguments are
+ stored in the context object so make sure they have appropriate lifetimes.
+ \endrst
+ */
+ basic_printf_context(detail::buffer_appender<Char> out,
+ basic_format_args<basic_printf_context> args)
+ : out_(out), args_(args) {}
+
+ auto out() -> detail::buffer_appender<Char> { return out_; }
+ void advance_to(detail::buffer_appender<Char>) {}
+
+ auto locale() -> detail::locale_ref { return {}; }
+
+ auto arg(int id) const -> basic_format_arg<basic_printf_context> {
+ return args_.get(id);
+ }
+
+ FMT_CONSTEXPR void on_error(const char* message) {
+ detail::error_handler().on_error(message);
+ }
+};
+
+namespace detail {
+
+// Checks if a value fits in int - used to avoid warnings about comparing
+// signed and unsigned integers.
+template <bool IsSigned> struct int_checker {
+ template <typename T> static auto fits_in_int(T value) -> bool {
+ unsigned max = max_value<int>();
+ return value <= max;
+ }
+ static auto fits_in_int(bool) -> bool { return true; }
+};
+
+template <> struct int_checker<true> {
+ template <typename T> static auto fits_in_int(T value) -> bool {
+ return value >= (std::numeric_limits<int>::min)() &&
+ value <= max_value<int>();
+ }
+ static auto fits_in_int(int) -> bool { return true; }
+};
+
+struct printf_precision_handler {
+ template <typename T, FMT_ENABLE_IF(std::is_integral<T>::value)>
+ auto operator()(T value) -> int {
+ if (!int_checker<std::numeric_limits<T>::is_signed>::fits_in_int(value))
+ throw_format_error("number is too big");
+ return (std::max)(static_cast<int>(value), 0);
+ }
+
+ template <typename T, FMT_ENABLE_IF(!std::is_integral<T>::value)>
+ auto operator()(T) -> int {
+ throw_format_error("precision is not integer");
+ return 0;
+ }
+};
+
+// An argument visitor that returns true iff arg is a zero integer.
+struct is_zero_int {
+ template <typename T, FMT_ENABLE_IF(std::is_integral<T>::value)>
+ auto operator()(T value) -> bool {
+ return value == 0;
+ }
+
+ template <typename T, FMT_ENABLE_IF(!std::is_integral<T>::value)>
+ auto operator()(T) -> bool {
+ return false;
+ }
+};
+
+template <typename T> struct make_unsigned_or_bool : std::make_unsigned<T> {};
+
+template <> struct make_unsigned_or_bool<bool> {
+ using type = bool;
+};
+
+template <typename T, typename Context> class arg_converter {
+ private:
+ using char_type = typename Context::char_type;
+
+ basic_format_arg<Context>& arg_;
+ char_type type_;
+
+ public:
+ arg_converter(basic_format_arg<Context>& arg, char_type type)
+ : arg_(arg), type_(type) {}
+
+ void operator()(bool value) {
+ if (type_ != 's') operator()<bool>(value);
+ }
+
+ template <typename U, FMT_ENABLE_IF(std::is_integral<U>::value)>
+ void operator()(U value) {
+ bool is_signed = type_ == 'd' || type_ == 'i';
+ using target_type = conditional_t<std::is_same<T, void>::value, U, T>;
+ if (const_check(sizeof(target_type) <= sizeof(int))) {
+ // Extra casts are used to silence warnings.
+ if (is_signed) {
+ auto n = static_cast<int>(static_cast<target_type>(value));
+ arg_ = detail::make_arg<Context>(n);
+ } else {
+ using unsigned_type = typename make_unsigned_or_bool<target_type>::type;
+ auto n = static_cast<unsigned>(static_cast<unsigned_type>(value));
+ arg_ = detail::make_arg<Context>(n);
+ }
+ } else {
+ if (is_signed) {
+ // glibc's printf doesn't sign extend arguments of smaller types:
+ // std::printf("%lld", -42); // prints "4294967254"
+ // but we don't have to do the same because it's a UB.
+ auto n = static_cast<long long>(value);
+ arg_ = detail::make_arg<Context>(n);
+ } else {
+ auto n = static_cast<typename make_unsigned_or_bool<U>::type>(value);
+ arg_ = detail::make_arg<Context>(n);
+ }
+ }
+ }
+
+ template <typename U, FMT_ENABLE_IF(!std::is_integral<U>::value)>
+ void operator()(U) {} // No conversion needed for non-integral types.
+};
+
+// Converts an integer argument to T for printf, if T is an integral type.
+// If T is void, the argument is converted to corresponding signed or unsigned
+// type depending on the type specifier: 'd' and 'i' - signed, other -
+// unsigned).
+template <typename T, typename Context, typename Char>
+void convert_arg(basic_format_arg<Context>& arg, Char type) {
+ visit_format_arg(arg_converter<T, Context>(arg, type), arg);
+}
+
+// Converts an integer argument to char for printf.
+template <typename Context> class char_converter {
+ private:
+ basic_format_arg<Context>& arg_;
+
+ public:
+ explicit char_converter(basic_format_arg<Context>& arg) : arg_(arg) {}
+
+ template <typename T, FMT_ENABLE_IF(std::is_integral<T>::value)>
+ void operator()(T value) {
+ auto c = static_cast<typename Context::char_type>(value);
+ arg_ = detail::make_arg<Context>(c);
+ }
+
+ template <typename T, FMT_ENABLE_IF(!std::is_integral<T>::value)>
+ void operator()(T) {} // No conversion needed for non-integral types.
+};
+
+// An argument visitor that return a pointer to a C string if argument is a
+// string or null otherwise.
+template <typename Char> struct get_cstring {
+ template <typename T> auto operator()(T) -> const Char* { return nullptr; }
+ auto operator()(const Char* s) -> const Char* { return s; }
+};
+
+// Checks if an argument is a valid printf width specifier and sets
+// left alignment if it is negative.
+template <typename Char> class printf_width_handler {
+ private:
+ format_specs<Char>& specs_;
+
+ public:
+ explicit printf_width_handler(format_specs<Char>& specs) : specs_(specs) {}
+
+ template <typename T, FMT_ENABLE_IF(std::is_integral<T>::value)>
+ auto operator()(T value) -> unsigned {
+ auto width = static_cast<uint32_or_64_or_128_t<T>>(value);
+ if (detail::is_negative(value)) {
+ specs_.align = align::left;
+ width = 0 - width;
+ }
+ unsigned int_max = max_value<int>();
+ if (width > int_max) throw_format_error("number is too big");
+ return static_cast<unsigned>(width);
+ }
+
+ template <typename T, FMT_ENABLE_IF(!std::is_integral<T>::value)>
+ auto operator()(T) -> unsigned {
+ throw_format_error("width is not integer");
+ return 0;
+ }
+};
+
+// Workaround for a bug with the XL compiler when initializing
+// printf_arg_formatter's base class.
+template <typename Char>
+auto make_arg_formatter(buffer_appender<Char> iter, format_specs<Char>& s)
+ -> arg_formatter<Char> {
+ return {iter, s, locale_ref()};
+}
+
+// The ``printf`` argument formatter.
+template <typename Char>
+class printf_arg_formatter : public arg_formatter<Char> {
+ private:
+ using base = arg_formatter<Char>;
+ using context_type = basic_printf_context<Char>;
+
+ context_type& context_;
+
+ void write_null_pointer(bool is_string = false) {
+ auto s = this->specs;
+ s.type = presentation_type::none;
+ write_bytes(this->out, is_string ? "(null)" : "(nil)", s);
+ }
+
+ public:
+ printf_arg_formatter(buffer_appender<Char> iter, format_specs<Char>& s,
+ context_type& ctx)
+ : base(make_arg_formatter(iter, s)), context_(ctx) {}
+
+ void operator()(monostate value) { base::operator()(value); }
+
+ template <typename T, FMT_ENABLE_IF(detail::is_integral<T>::value)>
+ void operator()(T value) {
+ // MSVC2013 fails to compile separate overloads for bool and Char so use
+ // std::is_same instead.
+ if (!std::is_same<T, Char>::value) {
+ base::operator()(value);
+ return;
+ }
+ format_specs<Char> fmt_specs = this->specs;
+ if (fmt_specs.type != presentation_type::none &&
+ fmt_specs.type != presentation_type::chr) {
+ return (*this)(static_cast<int>(value));
+ }
+ fmt_specs.sign = sign::none;
+ fmt_specs.alt = false;
+ fmt_specs.fill[0] = ' '; // Ignore '0' flag for char types.
+ // align::numeric needs to be overwritten here since the '0' flag is
+ // ignored for non-numeric types
+ if (fmt_specs.align == align::none || fmt_specs.align == align::numeric)
+ fmt_specs.align = align::right;
+ write<Char>(this->out, static_cast<Char>(value), fmt_specs);
+ }
+
+ template <typename T, FMT_ENABLE_IF(std::is_floating_point<T>::value)>
+ void operator()(T value) {
+ base::operator()(value);
+ }
+
+ /** Formats a null-terminated C string. */
+ void operator()(const char* value) {
+ if (value)
+ base::operator()(value);
+ else
+ write_null_pointer(this->specs.type != presentation_type::pointer);
+ }
+
+ /** Formats a null-terminated wide C string. */
+ void operator()(const wchar_t* value) {
+ if (value)
+ base::operator()(value);
+ else
+ write_null_pointer(this->specs.type != presentation_type::pointer);
+ }
+
+ void operator()(basic_string_view<Char> value) { base::operator()(value); }
+
+ /** Formats a pointer. */
+ void operator()(const void* value) {
+ if (value)
+ base::operator()(value);
+ else
+ write_null_pointer();
+ }
+
+ /** Formats an argument of a custom (user-defined) type. */
+ void operator()(typename basic_format_arg<context_type>::handle handle) {
+ auto parse_ctx = basic_format_parse_context<Char>({});
+ handle.format(parse_ctx, context_);
+ }
+};
+
+template <typename Char>
+void parse_flags(format_specs<Char>& specs, const Char*& it, const Char* end) {
+ for (; it != end; ++it) {
+ switch (*it) {
+ case '-':
+ specs.align = align::left;
+ break;
+ case '+':
+ specs.sign = sign::plus;
+ break;
+ case '0':
+ specs.fill[0] = '0';
+ break;
+ case ' ':
+ if (specs.sign != sign::plus) specs.sign = sign::space;
+ break;
+ case '#':
+ specs.alt = true;
+ break;
+ default:
+ return;
+ }
+ }
+}
+
+template <typename Char, typename GetArg>
+auto parse_header(const Char*& it, const Char* end, format_specs<Char>& specs,
+ GetArg get_arg) -> int {
+ int arg_index = -1;
+ Char c = *it;
+ if (c >= '0' && c <= '9') {
+ // Parse an argument index (if followed by '$') or a width possibly
+ // preceded with '0' flag(s).
+ int value = parse_nonnegative_int(it, end, -1);
+ if (it != end && *it == '$') { // value is an argument index
+ ++it;
+ arg_index = value != -1 ? value : max_value<int>();
+ } else {
+ if (c == '0') specs.fill[0] = '0';
+ if (value != 0) {
+ // Nonzero value means that we parsed width and don't need to
+ // parse it or flags again, so return now.
+ if (value == -1) throw_format_error("number is too big");
+ specs.width = value;
+ return arg_index;
+ }
+ }
+ }
+ parse_flags(specs, it, end);
+ // Parse width.
+ if (it != end) {
+ if (*it >= '0' && *it <= '9') {
+ specs.width = parse_nonnegative_int(it, end, -1);
+ if (specs.width == -1) throw_format_error("number is too big");
+ } else if (*it == '*') {
+ ++it;
+ specs.width = static_cast<int>(visit_format_arg(
+ detail::printf_width_handler<Char>(specs), get_arg(-1)));
+ }
+ }
+ return arg_index;
+}
+
+inline auto parse_printf_presentation_type(char c, type t)
+ -> presentation_type {
+ using pt = presentation_type;
+ constexpr auto integral_set = sint_set | uint_set | bool_set | char_set;
+ switch (c) {
+ case 'd':
+ return in(t, integral_set) ? pt::dec : pt::none;
+ case 'o':
+ return in(t, integral_set) ? pt::oct : pt::none;
+ case 'x':
+ return in(t, integral_set) ? pt::hex_lower : pt::none;
+ case 'X':
+ return in(t, integral_set) ? pt::hex_upper : pt::none;
+ case 'a':
+ return in(t, float_set) ? pt::hexfloat_lower : pt::none;
+ case 'A':
+ return in(t, float_set) ? pt::hexfloat_upper : pt::none;
+ case 'e':
+ return in(t, float_set) ? pt::exp_lower : pt::none;
+ case 'E':
+ return in(t, float_set) ? pt::exp_upper : pt::none;
+ case 'f':
+ return in(t, float_set) ? pt::fixed_lower : pt::none;
+ case 'F':
+ return in(t, float_set) ? pt::fixed_upper : pt::none;
+ case 'g':
+ return in(t, float_set) ? pt::general_lower : pt::none;
+ case 'G':
+ return in(t, float_set) ? pt::general_upper : pt::none;
+ case 'c':
+ return in(t, integral_set) ? pt::chr : pt::none;
+ case 's':
+ return in(t, string_set | cstring_set) ? pt::string : pt::none;
+ case 'p':
+ return in(t, pointer_set | cstring_set) ? pt::pointer : pt::none;
+ default:
+ return pt::none;
+ }
+}
+
+template <typename Char, typename Context>
+void vprintf(buffer<Char>& buf, basic_string_view<Char> format,
+ basic_format_args<Context> args) {
+ using iterator = buffer_appender<Char>;
+ auto out = iterator(buf);
+ auto context = basic_printf_context<Char>(out, args);
+ auto parse_ctx = basic_format_parse_context<Char>(format);
+
+ // Returns the argument with specified index or, if arg_index is -1, the next
+ // argument.
+ auto get_arg = [&](int arg_index) {
+ if (arg_index < 0)
+ arg_index = parse_ctx.next_arg_id();
+ else
+ parse_ctx.check_arg_id(--arg_index);
+ return detail::get_arg(context, arg_index);
+ };
+
+ const Char* start = parse_ctx.begin();
+ const Char* end = parse_ctx.end();
+ auto it = start;
+ while (it != end) {
+ if (!find<false, Char>(it, end, '%', it)) {
+ it = end; // find leaves it == nullptr if it doesn't find '%'.
+ break;
+ }
+ Char c = *it++;
+ if (it != end && *it == c) {
+ write(out, basic_string_view<Char>(start, to_unsigned(it - start)));
+ start = ++it;
+ continue;
+ }
+ write(out, basic_string_view<Char>(start, to_unsigned(it - 1 - start)));
+
+ auto specs = format_specs<Char>();
+ specs.align = align::right;
+
+ // Parse argument index, flags and width.
+ int arg_index = parse_header(it, end, specs, get_arg);
+ if (arg_index == 0) throw_format_error("argument not found");
+
+ // Parse precision.
+ if (it != end && *it == '.') {
+ ++it;
+ c = it != end ? *it : 0;
+ if ('0' <= c && c <= '9') {
+ specs.precision = parse_nonnegative_int(it, end, 0);
+ } else if (c == '*') {
+ ++it;
+ specs.precision = static_cast<int>(
+ visit_format_arg(printf_precision_handler(), get_arg(-1)));
+ } else {
+ specs.precision = 0;
+ }
+ }
+
+ auto arg = get_arg(arg_index);
+ // For d, i, o, u, x, and X conversion specifiers, if a precision is
+ // specified, the '0' flag is ignored
+ if (specs.precision >= 0 && arg.is_integral()) {
+ // Ignore '0' for non-numeric types or if '-' present.
+ specs.fill[0] = ' ';
+ }
+ if (specs.precision >= 0 && arg.type() == type::cstring_type) {
+ auto str = visit_format_arg(get_cstring<Char>(), arg);
+ auto str_end = str + specs.precision;
+ auto nul = std::find(str, str_end, Char());
+ auto sv = basic_string_view<Char>(
+ str, to_unsigned(nul != str_end ? nul - str : specs.precision));
+ arg = make_arg<basic_printf_context<Char>>(sv);
+ }
+ if (specs.alt && visit_format_arg(is_zero_int(), arg)) specs.alt = false;
+ if (specs.fill[0] == '0') {
+ if (arg.is_arithmetic() && specs.align != align::left)
+ specs.align = align::numeric;
+ else
+ specs.fill[0] = ' '; // Ignore '0' flag for non-numeric types or if '-'
+ // flag is also present.
+ }
+
+ // Parse length and convert the argument to the required type.
+ c = it != end ? *it++ : 0;
+ Char t = it != end ? *it : 0;
+ switch (c) {
+ case 'h':
+ if (t == 'h') {
+ ++it;
+ t = it != end ? *it : 0;
+ convert_arg<signed char>(arg, t);
+ } else {
+ convert_arg<short>(arg, t);
+ }
+ break;
+ case 'l':
+ if (t == 'l') {
+ ++it;
+ t = it != end ? *it : 0;
+ convert_arg<long long>(arg, t);
+ } else {
+ convert_arg<long>(arg, t);
+ }
+ break;
+ case 'j':
+ convert_arg<intmax_t>(arg, t);
+ break;
+ case 'z':
+ convert_arg<size_t>(arg, t);
+ break;
+ case 't':
+ convert_arg<std::ptrdiff_t>(arg, t);
+ break;
+ case 'L':
+ // printf produces garbage when 'L' is omitted for long double, no
+ // need to do the same.
+ break;
+ default:
+ --it;
+ convert_arg<void>(arg, c);
+ }
+
+ // Parse type.
+ if (it == end) throw_format_error("invalid format string");
+ char type = static_cast<char>(*it++);
+ if (arg.is_integral()) {
+ // Normalize type.
+ switch (type) {
+ case 'i':
+ case 'u':
+ type = 'd';
+ break;
+ case 'c':
+ visit_format_arg(char_converter<basic_printf_context<Char>>(arg), arg);
+ break;
+ }
+ }
+ specs.type = parse_printf_presentation_type(type, arg.type());
+ if (specs.type == presentation_type::none)
+ throw_format_error("invalid format specifier");
+
+ start = it;
+
+ // Format argument.
+ visit_format_arg(printf_arg_formatter<Char>(out, specs, context), arg);
+ }
+ write(out, basic_string_view<Char>(start, to_unsigned(it - start)));
+}
+} // namespace detail
+
+using printf_context = basic_printf_context<char>;
+using wprintf_context = basic_printf_context<wchar_t>;
+
+using printf_args = basic_format_args<printf_context>;
+using wprintf_args = basic_format_args<wprintf_context>;
+
+/**
+ \rst
+ Constructs an `~fmt::format_arg_store` object that contains references to
+ arguments and can be implicitly converted to `~fmt::printf_args`.
+ \endrst
+ */
+template <typename... T>
+inline auto make_printf_args(const T&... args)
+ -> format_arg_store<printf_context, T...> {
+ return {args...};
+}
+
+// DEPRECATED!
+template <typename... T>
+inline auto make_wprintf_args(const T&... args)
+ -> format_arg_store<wprintf_context, T...> {
+ return {args...};
+}
+
+template <typename Char>
+inline auto vsprintf(
+ basic_string_view<Char> fmt,
+ basic_format_args<basic_printf_context<type_identity_t<Char>>> args)
+ -> std::basic_string<Char> {
+ auto buf = basic_memory_buffer<Char>();
+ detail::vprintf(buf, fmt, args);
+ return to_string(buf);
+}
+
+/**
+ \rst
+ Formats arguments and returns the result as a string.
+
+ **Example**::
+
+ std::string message = fmt::sprintf("The answer is %d", 42);
+ \endrst
+*/
+template <typename S, typename... T,
+ typename Char = enable_if_t<detail::is_string<S>::value, char_t<S>>>
+inline auto sprintf(const S& fmt, const T&... args) -> std::basic_string<Char> {
+ return vsprintf(detail::to_string_view(fmt),
+ fmt::make_format_args<basic_printf_context<Char>>(args...));
+}
+
+template <typename Char>
+inline auto vfprintf(
+ std::FILE* f, basic_string_view<Char> fmt,
+ basic_format_args<basic_printf_context<type_identity_t<Char>>> args)
+ -> int {
+ auto buf = basic_memory_buffer<Char>();
+ detail::vprintf(buf, fmt, args);
+ size_t size = buf.size();
+ return std::fwrite(buf.data(), sizeof(Char), size, f) < size
+ ? -1
+ : static_cast<int>(size);
+}
+
+/**
+ \rst
+ Prints formatted data to the file *f*.
+
+ **Example**::
+
+ fmt::fprintf(stderr, "Don't %s!", "panic");
+ \endrst
+ */
+template <typename S, typename... T, typename Char = char_t<S>>
+inline auto fprintf(std::FILE* f, const S& fmt, const T&... args) -> int {
+ return vfprintf(f, detail::to_string_view(fmt),
+ fmt::make_format_args<basic_printf_context<Char>>(args...));
+}
+
+template <typename Char>
+FMT_DEPRECATED inline auto vprintf(
+ basic_string_view<Char> fmt,
+ basic_format_args<basic_printf_context<type_identity_t<Char>>> args)
+ -> int {
+ return vfprintf(stdout, fmt, args);
+}
+
+/**
+ \rst
+ Prints formatted data to ``stdout``.
+
+ **Example**::
+
+ fmt::printf("Elapsed time: %.2f seconds", 1.23);
+ \endrst
+ */
+template <typename... T>
+inline auto printf(string_view fmt, const T&... args) -> int {
+ return vfprintf(stdout, fmt, make_printf_args(args...));
+}
+template <typename... T>
+FMT_DEPRECATED inline auto printf(basic_string_view<wchar_t> fmt,
+ const T&... args) -> int {
+ return vfprintf(stdout, fmt, make_wprintf_args(args...));
+}
+
+FMT_END_EXPORT
+FMT_END_NAMESPACE
+
+#endif // FMT_PRINTF_H_
--- /dev/null
+// Formatting library for C++ - range and tuple support
+//
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_RANGES_H_
+#define FMT_RANGES_H_
+
+#include <initializer_list>
+#include <tuple>
+#include <type_traits>
+
+#include "format.h"
+
+FMT_BEGIN_NAMESPACE
+
+namespace detail {
+
+template <typename Range, typename OutputIt>
+auto copy(const Range& range, OutputIt out) -> OutputIt {
+ for (auto it = range.begin(), end = range.end(); it != end; ++it)
+ *out++ = *it;
+ return out;
+}
+
+template <typename OutputIt>
+auto copy(const char* str, OutputIt out) -> OutputIt {
+ while (*str) *out++ = *str++;
+ return out;
+}
+
+template <typename OutputIt> auto copy(char ch, OutputIt out) -> OutputIt {
+ *out++ = ch;
+ return out;
+}
+
+template <typename OutputIt> auto copy(wchar_t ch, OutputIt out) -> OutputIt {
+ *out++ = ch;
+ return out;
+}
+
+// Returns true if T has a std::string-like interface, like std::string_view.
+template <typename T> class is_std_string_like {
+ template <typename U>
+ static auto check(U* p)
+ -> decltype((void)p->find('a'), p->length(), (void)p->data(), int());
+ template <typename> static void check(...);
+
+ public:
+ static constexpr const bool value =
+ is_string<T>::value ||
+ std::is_convertible<T, std_string_view<char>>::value ||
+ !std::is_void<decltype(check<T>(nullptr))>::value;
+};
+
+template <typename Char>
+struct is_std_string_like<fmt::basic_string_view<Char>> : std::true_type {};
+
+template <typename T> class is_map {
+ template <typename U> static auto check(U*) -> typename U::mapped_type;
+ template <typename> static void check(...);
+
+ public:
+#ifdef FMT_FORMAT_MAP_AS_LIST // DEPRECATED!
+ static constexpr const bool value = false;
+#else
+ static constexpr const bool value =
+ !std::is_void<decltype(check<T>(nullptr))>::value;
+#endif
+};
+
+template <typename T> class is_set {
+ template <typename U> static auto check(U*) -> typename U::key_type;
+ template <typename> static void check(...);
+
+ public:
+#ifdef FMT_FORMAT_SET_AS_LIST // DEPRECATED!
+ static constexpr const bool value = false;
+#else
+ static constexpr const bool value =
+ !std::is_void<decltype(check<T>(nullptr))>::value && !is_map<T>::value;
+#endif
+};
+
+template <typename... Ts> struct conditional_helper {};
+
+template <typename T, typename _ = void> struct is_range_ : std::false_type {};
+
+#if !FMT_MSC_VERSION || FMT_MSC_VERSION > 1800
+
+# define FMT_DECLTYPE_RETURN(val) \
+ ->decltype(val) { return val; } \
+ static_assert( \
+ true, "") // This makes it so that a semicolon is required after the
+ // macro, which helps clang-format handle the formatting.
+
+// C array overload
+template <typename T, std::size_t N>
+auto range_begin(const T (&arr)[N]) -> const T* {
+ return arr;
+}
+template <typename T, std::size_t N>
+auto range_end(const T (&arr)[N]) -> const T* {
+ return arr + N;
+}
+
+template <typename T, typename Enable = void>
+struct has_member_fn_begin_end_t : std::false_type {};
+
+template <typename T>
+struct has_member_fn_begin_end_t<T, void_t<decltype(std::declval<T>().begin()),
+ decltype(std::declval<T>().end())>>
+ : std::true_type {};
+
+// Member function overload
+template <typename T>
+auto range_begin(T&& rng) FMT_DECLTYPE_RETURN(static_cast<T&&>(rng).begin());
+template <typename T>
+auto range_end(T&& rng) FMT_DECLTYPE_RETURN(static_cast<T&&>(rng).end());
+
+// ADL overload. Only participates in overload resolution if member functions
+// are not found.
+template <typename T>
+auto range_begin(T&& rng)
+ -> enable_if_t<!has_member_fn_begin_end_t<T&&>::value,
+ decltype(begin(static_cast<T&&>(rng)))> {
+ return begin(static_cast<T&&>(rng));
+}
+template <typename T>
+auto range_end(T&& rng) -> enable_if_t<!has_member_fn_begin_end_t<T&&>::value,
+ decltype(end(static_cast<T&&>(rng)))> {
+ return end(static_cast<T&&>(rng));
+}
+
+template <typename T, typename Enable = void>
+struct has_const_begin_end : std::false_type {};
+template <typename T, typename Enable = void>
+struct has_mutable_begin_end : std::false_type {};
+
+template <typename T>
+struct has_const_begin_end<
+ T,
+ void_t<
+ decltype(detail::range_begin(std::declval<const remove_cvref_t<T>&>())),
+ decltype(detail::range_end(std::declval<const remove_cvref_t<T>&>()))>>
+ : std::true_type {};
+
+template <typename T>
+struct has_mutable_begin_end<
+ T, void_t<decltype(detail::range_begin(std::declval<T>())),
+ decltype(detail::range_end(std::declval<T>())),
+ // the extra int here is because older versions of MSVC don't
+ // SFINAE properly unless there are distinct types
+ int>> : std::true_type {};
+
+template <typename T>
+struct is_range_<T, void>
+ : std::integral_constant<bool, (has_const_begin_end<T>::value ||
+ has_mutable_begin_end<T>::value)> {};
+# undef FMT_DECLTYPE_RETURN
+#endif
+
+// tuple_size and tuple_element check.
+template <typename T> class is_tuple_like_ {
+ template <typename U>
+ static auto check(U* p) -> decltype(std::tuple_size<U>::value, int());
+ template <typename> static void check(...);
+
+ public:
+ static constexpr const bool value =
+ !std::is_void<decltype(check<T>(nullptr))>::value;
+};
+
+// Check for integer_sequence
+#if defined(__cpp_lib_integer_sequence) || FMT_MSC_VERSION >= 1900
+template <typename T, T... N>
+using integer_sequence = std::integer_sequence<T, N...>;
+template <size_t... N> using index_sequence = std::index_sequence<N...>;
+template <size_t N> using make_index_sequence = std::make_index_sequence<N>;
+#else
+template <typename T, T... N> struct integer_sequence {
+ using value_type = T;
+
+ static FMT_CONSTEXPR auto size() -> size_t { return sizeof...(N); }
+};
+
+template <size_t... N> using index_sequence = integer_sequence<size_t, N...>;
+
+template <typename T, size_t N, T... Ns>
+struct make_integer_sequence : make_integer_sequence<T, N - 1, N - 1, Ns...> {};
+template <typename T, T... Ns>
+struct make_integer_sequence<T, 0, Ns...> : integer_sequence<T, Ns...> {};
+
+template <size_t N>
+using make_index_sequence = make_integer_sequence<size_t, N>;
+#endif
+
+template <typename T>
+using tuple_index_sequence = make_index_sequence<std::tuple_size<T>::value>;
+
+template <typename T, typename C, bool = is_tuple_like_<T>::value>
+class is_tuple_formattable_ {
+ public:
+ static constexpr const bool value = false;
+};
+template <typename T, typename C> class is_tuple_formattable_<T, C, true> {
+ template <std::size_t... Is>
+ static auto check2(index_sequence<Is...>,
+ integer_sequence<bool, (Is == Is)...>) -> std::true_type;
+ static auto check2(...) -> std::false_type;
+ template <std::size_t... Is>
+ static auto check(index_sequence<Is...>) -> decltype(check2(
+ index_sequence<Is...>{},
+ integer_sequence<bool,
+ (is_formattable<typename std::tuple_element<Is, T>::type,
+ C>::value)...>{}));
+
+ public:
+ static constexpr const bool value =
+ decltype(check(tuple_index_sequence<T>{}))::value;
+};
+
+template <typename Tuple, typename F, size_t... Is>
+FMT_CONSTEXPR void for_each(index_sequence<Is...>, Tuple&& t, F&& f) {
+ using std::get;
+ // Using a free function get<Is>(Tuple) now.
+ const int unused[] = {0, ((void)f(get<Is>(t)), 0)...};
+ ignore_unused(unused);
+}
+
+template <typename Tuple, typename F>
+FMT_CONSTEXPR void for_each(Tuple&& t, F&& f) {
+ for_each(tuple_index_sequence<remove_cvref_t<Tuple>>(),
+ std::forward<Tuple>(t), std::forward<F>(f));
+}
+
+template <typename Tuple1, typename Tuple2, typename F, size_t... Is>
+void for_each2(index_sequence<Is...>, Tuple1&& t1, Tuple2&& t2, F&& f) {
+ using std::get;
+ const int unused[] = {0, ((void)f(get<Is>(t1), get<Is>(t2)), 0)...};
+ ignore_unused(unused);
+}
+
+template <typename Tuple1, typename Tuple2, typename F>
+void for_each2(Tuple1&& t1, Tuple2&& t2, F&& f) {
+ for_each2(tuple_index_sequence<remove_cvref_t<Tuple1>>(),
+ std::forward<Tuple1>(t1), std::forward<Tuple2>(t2),
+ std::forward<F>(f));
+}
+
+namespace tuple {
+// Workaround a bug in MSVC 2019 (v140).
+template <typename Char, typename... T>
+using result_t = std::tuple<formatter<remove_cvref_t<T>, Char>...>;
+
+using std::get;
+template <typename Tuple, typename Char, std::size_t... Is>
+auto get_formatters(index_sequence<Is...>)
+ -> result_t<Char, decltype(get<Is>(std::declval<Tuple>()))...>;
+} // namespace tuple
+
+#if FMT_MSC_VERSION && FMT_MSC_VERSION < 1920
+// Older MSVC doesn't get the reference type correctly for arrays.
+template <typename R> struct range_reference_type_impl {
+ using type = decltype(*detail::range_begin(std::declval<R&>()));
+};
+
+template <typename T, std::size_t N> struct range_reference_type_impl<T[N]> {
+ using type = T&;
+};
+
+template <typename T>
+using range_reference_type = typename range_reference_type_impl<T>::type;
+#else
+template <typename Range>
+using range_reference_type =
+ decltype(*detail::range_begin(std::declval<Range&>()));
+#endif
+
+// We don't use the Range's value_type for anything, but we do need the Range's
+// reference type, with cv-ref stripped.
+template <typename Range>
+using uncvref_type = remove_cvref_t<range_reference_type<Range>>;
+
+template <typename Formatter>
+FMT_CONSTEXPR auto maybe_set_debug_format(Formatter& f, bool set)
+ -> decltype(f.set_debug_format(set)) {
+ f.set_debug_format(set);
+}
+template <typename Formatter>
+FMT_CONSTEXPR void maybe_set_debug_format(Formatter&, ...) {}
+
+// These are not generic lambdas for compatibility with C++11.
+template <typename ParseContext> struct parse_empty_specs {
+ template <typename Formatter> FMT_CONSTEXPR void operator()(Formatter& f) {
+ f.parse(ctx);
+ detail::maybe_set_debug_format(f, true);
+ }
+ ParseContext& ctx;
+};
+template <typename FormatContext> struct format_tuple_element {
+ using char_type = typename FormatContext::char_type;
+
+ template <typename T>
+ void operator()(const formatter<T, char_type>& f, const T& v) {
+ if (i > 0)
+ ctx.advance_to(detail::copy_str<char_type>(separator, ctx.out()));
+ ctx.advance_to(f.format(v, ctx));
+ ++i;
+ }
+
+ int i;
+ FormatContext& ctx;
+ basic_string_view<char_type> separator;
+};
+
+} // namespace detail
+
+template <typename T> struct is_tuple_like {
+ static constexpr const bool value =
+ detail::is_tuple_like_<T>::value && !detail::is_range_<T>::value;
+};
+
+template <typename T, typename C> struct is_tuple_formattable {
+ static constexpr const bool value =
+ detail::is_tuple_formattable_<T, C>::value;
+};
+
+template <typename Tuple, typename Char>
+struct formatter<Tuple, Char,
+ enable_if_t<fmt::is_tuple_like<Tuple>::value &&
+ fmt::is_tuple_formattable<Tuple, Char>::value>> {
+ private:
+ decltype(detail::tuple::get_formatters<Tuple, Char>(
+ detail::tuple_index_sequence<Tuple>())) formatters_;
+
+ basic_string_view<Char> separator_ = detail::string_literal<Char, ',', ' '>{};
+ basic_string_view<Char> opening_bracket_ =
+ detail::string_literal<Char, '('>{};
+ basic_string_view<Char> closing_bracket_ =
+ detail::string_literal<Char, ')'>{};
+
+ public:
+ FMT_CONSTEXPR formatter() {}
+
+ FMT_CONSTEXPR void set_separator(basic_string_view<Char> sep) {
+ separator_ = sep;
+ }
+
+ FMT_CONSTEXPR void set_brackets(basic_string_view<Char> open,
+ basic_string_view<Char> close) {
+ opening_bracket_ = open;
+ closing_bracket_ = close;
+ }
+
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) {
+ auto it = ctx.begin();
+ if (it != ctx.end() && *it != '}')
+ FMT_THROW(format_error("invalid format specifier"));
+ detail::for_each(formatters_, detail::parse_empty_specs<ParseContext>{ctx});
+ return it;
+ }
+
+ template <typename FormatContext>
+ auto format(const Tuple& value, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ ctx.advance_to(detail::copy_str<Char>(opening_bracket_, ctx.out()));
+ detail::for_each2(
+ formatters_, value,
+ detail::format_tuple_element<FormatContext>{0, ctx, separator_});
+ return detail::copy_str<Char>(closing_bracket_, ctx.out());
+ }
+};
+
+template <typename T, typename Char> struct is_range {
+ static constexpr const bool value =
+ detail::is_range_<T>::value && !detail::is_std_string_like<T>::value &&
+ !std::is_convertible<T, std::basic_string<Char>>::value &&
+ !std::is_convertible<T, detail::std_string_view<Char>>::value;
+};
+
+namespace detail {
+template <typename Context> struct range_mapper {
+ using mapper = arg_mapper<Context>;
+
+ template <typename T,
+ FMT_ENABLE_IF(has_formatter<remove_cvref_t<T>, Context>::value)>
+ static auto map(T&& value) -> T&& {
+ return static_cast<T&&>(value);
+ }
+ template <typename T,
+ FMT_ENABLE_IF(!has_formatter<remove_cvref_t<T>, Context>::value)>
+ static auto map(T&& value)
+ -> decltype(mapper().map(static_cast<T&&>(value))) {
+ return mapper().map(static_cast<T&&>(value));
+ }
+};
+
+template <typename Char, typename Element>
+using range_formatter_type =
+ formatter<remove_cvref_t<decltype(range_mapper<buffer_context<Char>>{}.map(
+ std::declval<Element>()))>,
+ Char>;
+
+template <typename R>
+using maybe_const_range =
+ conditional_t<has_const_begin_end<R>::value, const R, R>;
+
+// Workaround a bug in MSVC 2015 and earlier.
+#if !FMT_MSC_VERSION || FMT_MSC_VERSION >= 1910
+template <typename R, typename Char>
+struct is_formattable_delayed
+ : is_formattable<uncvref_type<maybe_const_range<R>>, Char> {};
+#endif
+} // namespace detail
+
+template <typename...> struct conjunction : std::true_type {};
+template <typename P> struct conjunction<P> : P {};
+template <typename P1, typename... Pn>
+struct conjunction<P1, Pn...>
+ : conditional_t<bool(P1::value), conjunction<Pn...>, P1> {};
+
+template <typename T, typename Char, typename Enable = void>
+struct range_formatter;
+
+template <typename T, typename Char>
+struct range_formatter<
+ T, Char,
+ enable_if_t<conjunction<std::is_same<T, remove_cvref_t<T>>,
+ is_formattable<T, Char>>::value>> {
+ private:
+ detail::range_formatter_type<Char, T> underlying_;
+ basic_string_view<Char> separator_ = detail::string_literal<Char, ',', ' '>{};
+ basic_string_view<Char> opening_bracket_ =
+ detail::string_literal<Char, '['>{};
+ basic_string_view<Char> closing_bracket_ =
+ detail::string_literal<Char, ']'>{};
+
+ public:
+ FMT_CONSTEXPR range_formatter() {}
+
+ FMT_CONSTEXPR auto underlying() -> detail::range_formatter_type<Char, T>& {
+ return underlying_;
+ }
+
+ FMT_CONSTEXPR void set_separator(basic_string_view<Char> sep) {
+ separator_ = sep;
+ }
+
+ FMT_CONSTEXPR void set_brackets(basic_string_view<Char> open,
+ basic_string_view<Char> close) {
+ opening_bracket_ = open;
+ closing_bracket_ = close;
+ }
+
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) {
+ auto it = ctx.begin();
+ auto end = ctx.end();
+
+ if (it != end && *it == 'n') {
+ set_brackets({}, {});
+ ++it;
+ }
+
+ if (it != end && *it != '}') {
+ if (*it != ':') FMT_THROW(format_error("invalid format specifier"));
+ ++it;
+ } else {
+ detail::maybe_set_debug_format(underlying_, true);
+ }
+
+ ctx.advance_to(it);
+ return underlying_.parse(ctx);
+ }
+
+ template <typename R, typename FormatContext>
+ auto format(R&& range, FormatContext& ctx) const -> decltype(ctx.out()) {
+ detail::range_mapper<buffer_context<Char>> mapper;
+ auto out = ctx.out();
+ out = detail::copy_str<Char>(opening_bracket_, out);
+ int i = 0;
+ auto it = detail::range_begin(range);
+ auto end = detail::range_end(range);
+ for (; it != end; ++it) {
+ if (i > 0) out = detail::copy_str<Char>(separator_, out);
+ ctx.advance_to(out);
+ auto&& item = *it;
+ out = underlying_.format(mapper.map(item), ctx);
+ ++i;
+ }
+ out = detail::copy_str<Char>(closing_bracket_, out);
+ return out;
+ }
+};
+
+enum class range_format { disabled, map, set, sequence, string, debug_string };
+
+namespace detail {
+template <typename T>
+struct range_format_kind_
+ : std::integral_constant<range_format,
+ std::is_same<uncvref_type<T>, T>::value
+ ? range_format::disabled
+ : is_map<T>::value ? range_format::map
+ : is_set<T>::value ? range_format::set
+ : range_format::sequence> {};
+
+template <range_format K, typename R, typename Char, typename Enable = void>
+struct range_default_formatter;
+
+template <range_format K>
+using range_format_constant = std::integral_constant<range_format, K>;
+
+template <range_format K, typename R, typename Char>
+struct range_default_formatter<
+ K, R, Char,
+ enable_if_t<(K == range_format::sequence || K == range_format::map ||
+ K == range_format::set)>> {
+ using range_type = detail::maybe_const_range<R>;
+ range_formatter<detail::uncvref_type<range_type>, Char> underlying_;
+
+ FMT_CONSTEXPR range_default_formatter() { init(range_format_constant<K>()); }
+
+ FMT_CONSTEXPR void init(range_format_constant<range_format::set>) {
+ underlying_.set_brackets(detail::string_literal<Char, '{'>{},
+ detail::string_literal<Char, '}'>{});
+ }
+
+ FMT_CONSTEXPR void init(range_format_constant<range_format::map>) {
+ underlying_.set_brackets(detail::string_literal<Char, '{'>{},
+ detail::string_literal<Char, '}'>{});
+ underlying_.underlying().set_brackets({}, {});
+ underlying_.underlying().set_separator(
+ detail::string_literal<Char, ':', ' '>{});
+ }
+
+ FMT_CONSTEXPR void init(range_format_constant<range_format::sequence>) {}
+
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) {
+ return underlying_.parse(ctx);
+ }
+
+ template <typename FormatContext>
+ auto format(range_type& range, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ return underlying_.format(range, ctx);
+ }
+};
+} // namespace detail
+
+template <typename T, typename Char, typename Enable = void>
+struct range_format_kind
+ : conditional_t<
+ is_range<T, Char>::value, detail::range_format_kind_<T>,
+ std::integral_constant<range_format, range_format::disabled>> {};
+
+template <typename R, typename Char>
+struct formatter<
+ R, Char,
+ enable_if_t<conjunction<bool_constant<range_format_kind<R, Char>::value !=
+ range_format::disabled>
+// Workaround a bug in MSVC 2015 and earlier.
+#if !FMT_MSC_VERSION || FMT_MSC_VERSION >= 1910
+ ,
+ detail::is_formattable_delayed<R, Char>
+#endif
+ >::value>>
+ : detail::range_default_formatter<range_format_kind<R, Char>::value, R,
+ Char> {
+};
+
+template <typename Char, typename... T> struct tuple_join_view : detail::view {
+ const std::tuple<T...>& tuple;
+ basic_string_view<Char> sep;
+
+ tuple_join_view(const std::tuple<T...>& t, basic_string_view<Char> s)
+ : tuple(t), sep{s} {}
+};
+
+// Define FMT_TUPLE_JOIN_SPECIFIERS to enable experimental format specifiers
+// support in tuple_join. It is disabled by default because of issues with
+// the dynamic width and precision.
+#ifndef FMT_TUPLE_JOIN_SPECIFIERS
+# define FMT_TUPLE_JOIN_SPECIFIERS 0
+#endif
+
+template <typename Char, typename... T>
+struct formatter<tuple_join_view<Char, T...>, Char> {
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) {
+ return do_parse(ctx, std::integral_constant<size_t, sizeof...(T)>());
+ }
+
+ template <typename FormatContext>
+ auto format(const tuple_join_view<Char, T...>& value,
+ FormatContext& ctx) const -> typename FormatContext::iterator {
+ return do_format(value, ctx,
+ std::integral_constant<size_t, sizeof...(T)>());
+ }
+
+ private:
+ std::tuple<formatter<typename std::decay<T>::type, Char>...> formatters_;
+
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto do_parse(ParseContext& ctx,
+ std::integral_constant<size_t, 0>)
+ -> decltype(ctx.begin()) {
+ return ctx.begin();
+ }
+
+ template <typename ParseContext, size_t N>
+ FMT_CONSTEXPR auto do_parse(ParseContext& ctx,
+ std::integral_constant<size_t, N>)
+ -> decltype(ctx.begin()) {
+ auto end = ctx.begin();
+#if FMT_TUPLE_JOIN_SPECIFIERS
+ end = std::get<sizeof...(T) - N>(formatters_).parse(ctx);
+ if (N > 1) {
+ auto end1 = do_parse(ctx, std::integral_constant<size_t, N - 1>());
+ if (end != end1)
+ FMT_THROW(format_error("incompatible format specs for tuple elements"));
+ }
+#endif
+ return end;
+ }
+
+ template <typename FormatContext>
+ auto do_format(const tuple_join_view<Char, T...>&, FormatContext& ctx,
+ std::integral_constant<size_t, 0>) const ->
+ typename FormatContext::iterator {
+ return ctx.out();
+ }
+
+ template <typename FormatContext, size_t N>
+ auto do_format(const tuple_join_view<Char, T...>& value, FormatContext& ctx,
+ std::integral_constant<size_t, N>) const ->
+ typename FormatContext::iterator {
+ auto out = std::get<sizeof...(T) - N>(formatters_)
+ .format(std::get<sizeof...(T) - N>(value.tuple), ctx);
+ if (N > 1) {
+ out = std::copy(value.sep.begin(), value.sep.end(), out);
+ ctx.advance_to(out);
+ return do_format(value, ctx, std::integral_constant<size_t, N - 1>());
+ }
+ return out;
+ }
+};
+
+namespace detail {
+// Check if T has an interface like a container adaptor (e.g. std::stack,
+// std::queue, std::priority_queue).
+template <typename T> class is_container_adaptor_like {
+ template <typename U> static auto check(U* p) -> typename U::container_type;
+ template <typename> static void check(...);
+
+ public:
+ static constexpr const bool value =
+ !std::is_void<decltype(check<T>(nullptr))>::value;
+};
+
+template <typename Container> struct all {
+ const Container& c;
+ auto begin() const -> typename Container::const_iterator { return c.begin(); }
+ auto end() const -> typename Container::const_iterator { return c.end(); }
+};
+} // namespace detail
+
+template <typename T, typename Char>
+struct formatter<
+ T, Char,
+ enable_if_t<conjunction<detail::is_container_adaptor_like<T>,
+ bool_constant<range_format_kind<T, Char>::value ==
+ range_format::disabled>>::value>>
+ : formatter<detail::all<typename T::container_type>, Char> {
+ using all = detail::all<typename T::container_type>;
+ template <typename FormatContext>
+ auto format(const T& t, FormatContext& ctx) const -> decltype(ctx.out()) {
+ struct getter : T {
+ static auto get(const T& t) -> all {
+ return {t.*(&getter::c)}; // Access c through the derived class.
+ }
+ };
+ return formatter<all>::format(getter::get(t), ctx);
+ }
+};
+
+FMT_BEGIN_EXPORT
+
+/**
+ \rst
+ Returns an object that formats `tuple` with elements separated by `sep`.
+
+ **Example**::
+
+ std::tuple<int, char> t = {1, 'a'};
+ fmt::print("{}", fmt::join(t, ", "));
+ // Output: "1, a"
+ \endrst
+ */
+template <typename... T>
+FMT_CONSTEXPR auto join(const std::tuple<T...>& tuple, string_view sep)
+ -> tuple_join_view<char, T...> {
+ return {tuple, sep};
+}
+
+template <typename... T>
+FMT_CONSTEXPR auto join(const std::tuple<T...>& tuple,
+ basic_string_view<wchar_t> sep)
+ -> tuple_join_view<wchar_t, T...> {
+ return {tuple, sep};
+}
+
+/**
+ \rst
+ Returns an object that formats `initializer_list` with elements separated by
+ `sep`.
+
+ **Example**::
+
+ fmt::print("{}", fmt::join({1, 2, 3}, ", "));
+ // Output: "1, 2, 3"
+ \endrst
+ */
+template <typename T>
+auto join(std::initializer_list<T> list, string_view sep)
+ -> join_view<const T*, const T*> {
+ return join(std::begin(list), std::end(list), sep);
+}
+
+FMT_END_EXPORT
+FMT_END_NAMESPACE
+
+#endif // FMT_RANGES_H_
--- /dev/null
+// Formatting library for C++ - formatters for standard library types
+//
+// Copyright (c) 2012 - present, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_STD_H_
+#define FMT_STD_H_
+
+#include <atomic>
+#include <bitset>
+#include <cstdlib>
+#include <exception>
+#include <memory>
+#include <thread>
+#include <type_traits>
+#include <typeinfo>
+#include <utility>
+#include <vector>
+
+#include "format.h"
+#include "ostream.h"
+
+#if FMT_HAS_INCLUDE(<version>)
+# include <version>
+#endif
+// Checking FMT_CPLUSPLUS for warning suppression in MSVC.
+#if FMT_CPLUSPLUS >= 201703L
+# if FMT_HAS_INCLUDE(<filesystem>)
+# include <filesystem>
+# endif
+# if FMT_HAS_INCLUDE(<variant>)
+# include <variant>
+# endif
+# if FMT_HAS_INCLUDE(<optional>)
+# include <optional>
+# endif
+#endif
+
+#if FMT_CPLUSPLUS > 201703L && FMT_HAS_INCLUDE(<source_location>)
+# include <source_location>
+#endif
+
+// GCC 4 does not support FMT_HAS_INCLUDE.
+#if FMT_HAS_INCLUDE(<cxxabi.h>) || defined(__GLIBCXX__)
+# include <cxxabi.h>
+// Android NDK with gabi++ library on some architectures does not implement
+// abi::__cxa_demangle().
+# ifndef __GABIXX_CXXABI_H__
+# define FMT_HAS_ABI_CXA_DEMANGLE
+# endif
+#endif
+
+// Check if typeid is available.
+#ifndef FMT_USE_TYPEID
+// __RTTI is for EDG compilers. In MSVC typeid is available without RTTI.
+# if defined(__GXX_RTTI) || FMT_HAS_FEATURE(cxx_rtti) || FMT_MSC_VERSION || \
+ defined(__INTEL_RTTI__) || defined(__RTTI)
+# define FMT_USE_TYPEID 1
+# else
+# define FMT_USE_TYPEID 0
+# endif
+#endif
+
+// For older Xcode versions, __cpp_lib_xxx flags are inaccurately defined.
+#ifndef FMT_CPP_LIB_FILESYSTEM
+# ifdef __cpp_lib_filesystem
+# define FMT_CPP_LIB_FILESYSTEM __cpp_lib_filesystem
+# else
+# define FMT_CPP_LIB_FILESYSTEM 0
+# endif
+#endif
+
+#ifndef FMT_CPP_LIB_VARIANT
+# ifdef __cpp_lib_variant
+# define FMT_CPP_LIB_VARIANT __cpp_lib_variant
+# else
+# define FMT_CPP_LIB_VARIANT 0
+# endif
+#endif
+
+#if FMT_CPP_LIB_FILESYSTEM
+FMT_BEGIN_NAMESPACE
+
+namespace detail {
+
+template <typename Char, typename PathChar>
+auto get_path_string(const std::filesystem::path& p,
+ const std::basic_string<PathChar>& native) {
+ if constexpr (std::is_same_v<Char, char> && std::is_same_v<PathChar, wchar_t>)
+ return to_utf8<wchar_t>(native, to_utf8_error_policy::replace);
+ else
+ return p.string<Char>();
+}
+
+template <typename Char, typename PathChar>
+void write_escaped_path(basic_memory_buffer<Char>& quoted,
+ const std::filesystem::path& p,
+ const std::basic_string<PathChar>& native) {
+ if constexpr (std::is_same_v<Char, char> &&
+ std::is_same_v<PathChar, wchar_t>) {
+ auto buf = basic_memory_buffer<wchar_t>();
+ write_escaped_string<wchar_t>(std::back_inserter(buf), native);
+ bool valid = to_utf8<wchar_t>::convert(quoted, {buf.data(), buf.size()});
+ FMT_ASSERT(valid, "invalid utf16");
+ } else if constexpr (std::is_same_v<Char, PathChar>) {
+ write_escaped_string<std::filesystem::path::value_type>(
+ std::back_inserter(quoted), native);
+ } else {
+ write_escaped_string<Char>(std::back_inserter(quoted), p.string<Char>());
+ }
+}
+
+} // namespace detail
+
+FMT_EXPORT
+template <typename Char> struct formatter<std::filesystem::path, Char> {
+ private:
+ format_specs<Char> specs_;
+ detail::arg_ref<Char> width_ref_;
+ bool debug_ = false;
+ char path_type_ = 0;
+
+ public:
+ FMT_CONSTEXPR void set_debug_format(bool set = true) { debug_ = set; }
+
+ template <typename ParseContext> FMT_CONSTEXPR auto parse(ParseContext& ctx) {
+ auto it = ctx.begin(), end = ctx.end();
+ if (it == end) return it;
+
+ it = detail::parse_align(it, end, specs_);
+ if (it == end) return it;
+
+ it = detail::parse_dynamic_spec(it, end, specs_.width, width_ref_, ctx);
+ if (it != end && *it == '?') {
+ debug_ = true;
+ ++it;
+ }
+ if (it != end && (*it == 'g')) path_type_ = *it++;
+ return it;
+ }
+
+ template <typename FormatContext>
+ auto format(const std::filesystem::path& p, FormatContext& ctx) const {
+ auto specs = specs_;
+# ifdef _WIN32
+ auto path_string = !path_type_ ? p.native() : p.generic_wstring();
+# else
+ auto path_string = !path_type_ ? p.native() : p.generic_string();
+# endif
+
+ detail::handle_dynamic_spec<detail::width_checker>(specs.width, width_ref_,
+ ctx);
+ if (!debug_) {
+ auto s = detail::get_path_string<Char>(p, path_string);
+ return detail::write(ctx.out(), basic_string_view<Char>(s), specs);
+ }
+ auto quoted = basic_memory_buffer<Char>();
+ detail::write_escaped_path(quoted, p, path_string);
+ return detail::write(ctx.out(),
+ basic_string_view<Char>(quoted.data(), quoted.size()),
+ specs);
+ }
+};
+FMT_END_NAMESPACE
+#endif // FMT_CPP_LIB_FILESYSTEM
+
+FMT_BEGIN_NAMESPACE
+FMT_EXPORT
+template <std::size_t N, typename Char>
+struct formatter<std::bitset<N>, Char> : nested_formatter<string_view> {
+ private:
+ // Functor because C++11 doesn't support generic lambdas.
+ struct writer {
+ const std::bitset<N>& bs;
+
+ template <typename OutputIt>
+ FMT_CONSTEXPR auto operator()(OutputIt out) -> OutputIt {
+ for (auto pos = N; pos > 0; --pos) {
+ out = detail::write<Char>(out, bs[pos - 1] ? Char('1') : Char('0'));
+ }
+
+ return out;
+ }
+ };
+
+ public:
+ template <typename FormatContext>
+ auto format(const std::bitset<N>& bs, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ return write_padded(ctx, writer{bs});
+ }
+};
+
+FMT_EXPORT
+template <typename Char>
+struct formatter<std::thread::id, Char> : basic_ostream_formatter<Char> {};
+FMT_END_NAMESPACE
+
+#ifdef __cpp_lib_optional
+FMT_BEGIN_NAMESPACE
+FMT_EXPORT
+template <typename T, typename Char>
+struct formatter<std::optional<T>, Char,
+ std::enable_if_t<is_formattable<T, Char>::value>> {
+ private:
+ formatter<T, Char> underlying_;
+ static constexpr basic_string_view<Char> optional =
+ detail::string_literal<Char, 'o', 'p', 't', 'i', 'o', 'n', 'a', 'l',
+ '('>{};
+ static constexpr basic_string_view<Char> none =
+ detail::string_literal<Char, 'n', 'o', 'n', 'e'>{};
+
+ template <class U>
+ FMT_CONSTEXPR static auto maybe_set_debug_format(U& u, bool set)
+ -> decltype(u.set_debug_format(set)) {
+ u.set_debug_format(set);
+ }
+
+ template <class U>
+ FMT_CONSTEXPR static void maybe_set_debug_format(U&, ...) {}
+
+ public:
+ template <typename ParseContext> FMT_CONSTEXPR auto parse(ParseContext& ctx) {
+ maybe_set_debug_format(underlying_, true);
+ return underlying_.parse(ctx);
+ }
+
+ template <typename FormatContext>
+ auto format(const std::optional<T>& opt, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ if (!opt) return detail::write<Char>(ctx.out(), none);
+
+ auto out = ctx.out();
+ out = detail::write<Char>(out, optional);
+ ctx.advance_to(out);
+ out = underlying_.format(*opt, ctx);
+ return detail::write(out, ')');
+ }
+};
+FMT_END_NAMESPACE
+#endif // __cpp_lib_optional
+
+#ifdef __cpp_lib_source_location
+FMT_BEGIN_NAMESPACE
+FMT_EXPORT
+template <> struct formatter<std::source_location> {
+ template <typename ParseContext> FMT_CONSTEXPR auto parse(ParseContext& ctx) {
+ return ctx.begin();
+ }
+
+ template <typename FormatContext>
+ auto format(const std::source_location& loc, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ auto out = ctx.out();
+ out = detail::write(out, loc.file_name());
+ out = detail::write(out, ':');
+ out = detail::write<char>(out, loc.line());
+ out = detail::write(out, ':');
+ out = detail::write<char>(out, loc.column());
+ out = detail::write(out, ": ");
+ out = detail::write(out, loc.function_name());
+ return out;
+ }
+};
+FMT_END_NAMESPACE
+#endif
+
+#if FMT_CPP_LIB_VARIANT
+FMT_BEGIN_NAMESPACE
+namespace detail {
+
+template <typename T>
+using variant_index_sequence =
+ std::make_index_sequence<std::variant_size<T>::value>;
+
+template <typename> struct is_variant_like_ : std::false_type {};
+template <typename... Types>
+struct is_variant_like_<std::variant<Types...>> : std::true_type {};
+
+// formattable element check.
+template <typename T, typename C> class is_variant_formattable_ {
+ template <std::size_t... Is>
+ static std::conjunction<
+ is_formattable<std::variant_alternative_t<Is, T>, C>...>
+ check(std::index_sequence<Is...>);
+
+ public:
+ static constexpr const bool value =
+ decltype(check(variant_index_sequence<T>{}))::value;
+};
+
+template <typename Char, typename OutputIt, typename T>
+auto write_variant_alternative(OutputIt out, const T& v) -> OutputIt {
+ if constexpr (is_string<T>::value)
+ return write_escaped_string<Char>(out, detail::to_string_view(v));
+ else if constexpr (std::is_same_v<T, Char>)
+ return write_escaped_char(out, v);
+ else
+ return write<Char>(out, v);
+}
+
+} // namespace detail
+
+template <typename T> struct is_variant_like {
+ static constexpr const bool value = detail::is_variant_like_<T>::value;
+};
+
+template <typename T, typename C> struct is_variant_formattable {
+ static constexpr const bool value =
+ detail::is_variant_formattable_<T, C>::value;
+};
+
+FMT_EXPORT
+template <typename Char> struct formatter<std::monostate, Char> {
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) {
+ return ctx.begin();
+ }
+
+ template <typename FormatContext>
+ auto format(const std::monostate&, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ return detail::write<Char>(ctx.out(), "monostate");
+ }
+};
+
+FMT_EXPORT
+template <typename Variant, typename Char>
+struct formatter<
+ Variant, Char,
+ std::enable_if_t<std::conjunction_v<
+ is_variant_like<Variant>, is_variant_formattable<Variant, Char>>>> {
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) {
+ return ctx.begin();
+ }
+
+ template <typename FormatContext>
+ auto format(const Variant& value, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ auto out = ctx.out();
+
+ out = detail::write<Char>(out, "variant(");
+ FMT_TRY {
+ std::visit(
+ [&](const auto& v) {
+ out = detail::write_variant_alternative<Char>(out, v);
+ },
+ value);
+ }
+ FMT_CATCH(const std::bad_variant_access&) {
+ detail::write<Char>(out, "valueless by exception");
+ }
+ *out++ = ')';
+ return out;
+ }
+};
+FMT_END_NAMESPACE
+#endif // FMT_CPP_LIB_VARIANT
+
+FMT_BEGIN_NAMESPACE
+FMT_EXPORT
+template <typename Char> struct formatter<std::error_code, Char> {
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) {
+ return ctx.begin();
+ }
+
+ template <typename FormatContext>
+ FMT_CONSTEXPR auto format(const std::error_code& ec, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ auto out = ctx.out();
+ out = detail::write_bytes(out, ec.category().name(), format_specs<Char>());
+ out = detail::write<Char>(out, Char(':'));
+ out = detail::write<Char>(out, ec.value());
+ return out;
+ }
+};
+
+FMT_EXPORT
+template <typename T, typename Char>
+struct formatter<
+ T, Char, // DEPRECATED! Mixing code unit types.
+ typename std::enable_if<std::is_base_of<std::exception, T>::value>::type> {
+ private:
+ bool with_typename_ = false;
+
+ public:
+ FMT_CONSTEXPR auto parse(basic_format_parse_context<Char>& ctx)
+ -> decltype(ctx.begin()) {
+ auto it = ctx.begin();
+ auto end = ctx.end();
+ if (it == end || *it == '}') return it;
+ if (*it == 't') {
+ ++it;
+ with_typename_ = FMT_USE_TYPEID != 0;
+ }
+ return it;
+ }
+
+ template <typename OutputIt>
+ auto format(const std::exception& ex,
+ basic_format_context<OutputIt, Char>& ctx) const -> OutputIt {
+ format_specs<Char> spec;
+ auto out = ctx.out();
+ if (!with_typename_)
+ return detail::write_bytes(out, string_view(ex.what()), spec);
+
+#if FMT_USE_TYPEID
+ const std::type_info& ti = typeid(ex);
+# ifdef FMT_HAS_ABI_CXA_DEMANGLE
+ int status = 0;
+ std::size_t size = 0;
+ std::unique_ptr<char, void (*)(void*)> demangled_name_ptr(
+ abi::__cxa_demangle(ti.name(), nullptr, &size, &status), &std::free);
+
+ string_view demangled_name_view;
+ if (demangled_name_ptr) {
+ demangled_name_view = demangled_name_ptr.get();
+
+ // Normalization of stdlib inline namespace names.
+ // libc++ inline namespaces.
+ // std::__1::* -> std::*
+ // std::__1::__fs::* -> std::*
+ // libstdc++ inline namespaces.
+ // std::__cxx11::* -> std::*
+ // std::filesystem::__cxx11::* -> std::filesystem::*
+ if (demangled_name_view.starts_with("std::")) {
+ char* begin = demangled_name_ptr.get();
+ char* to = begin + 5; // std::
+ for (char *from = to, *end = begin + demangled_name_view.size();
+ from < end;) {
+ // This is safe, because demangled_name is NUL-terminated.
+ if (from[0] == '_' && from[1] == '_') {
+ char* next = from + 1;
+ while (next < end && *next != ':') next++;
+ if (next[0] == ':' && next[1] == ':') {
+ from = next + 2;
+ continue;
+ }
+ }
+ *to++ = *from++;
+ }
+ demangled_name_view = {begin, detail::to_unsigned(to - begin)};
+ }
+ } else {
+ demangled_name_view = string_view(ti.name());
+ }
+ out = detail::write_bytes(out, demangled_name_view, spec);
+# elif FMT_MSC_VERSION
+ string_view demangled_name_view(ti.name());
+ if (demangled_name_view.starts_with("class "))
+ demangled_name_view.remove_prefix(6);
+ else if (demangled_name_view.starts_with("struct "))
+ demangled_name_view.remove_prefix(7);
+ out = detail::write_bytes(out, demangled_name_view, spec);
+# else
+ out = detail::write_bytes(out, string_view(ti.name()), spec);
+# endif
+ *out++ = ':';
+ *out++ = ' ';
+ return detail::write_bytes(out, string_view(ex.what()), spec);
+#endif
+ }
+};
+
+namespace detail {
+
+template <typename T, typename Enable = void>
+struct has_flip : std::false_type {};
+
+template <typename T>
+struct has_flip<T, void_t<decltype(std::declval<T>().flip())>>
+ : std::true_type {};
+
+template <typename T> struct is_bit_reference_like {
+ static constexpr const bool value =
+ std::is_convertible<T, bool>::value &&
+ std::is_nothrow_assignable<T, bool>::value && has_flip<T>::value;
+};
+
+#ifdef _LIBCPP_VERSION
+
+// Workaround for libc++ incompatibility with C++ standard.
+// According to the Standard, `bitset::operator[] const` returns bool.
+template <typename C>
+struct is_bit_reference_like<std::__bit_const_reference<C>> {
+ static constexpr const bool value = true;
+};
+
+#endif
+
+} // namespace detail
+
+// We can't use std::vector<bool, Allocator>::reference and
+// std::bitset<N>::reference because the compiler can't deduce Allocator and N
+// in partial specialization.
+FMT_EXPORT
+template <typename BitRef, typename Char>
+struct formatter<BitRef, Char,
+ enable_if_t<detail::is_bit_reference_like<BitRef>::value>>
+ : formatter<bool, Char> {
+ template <typename FormatContext>
+ FMT_CONSTEXPR auto format(const BitRef& v, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ return formatter<bool, Char>::format(v, ctx);
+ }
+};
+
+FMT_EXPORT
+template <typename T, typename Char>
+struct formatter<std::atomic<T>, Char,
+ enable_if_t<is_formattable<T, Char>::value>>
+ : formatter<T, Char> {
+ template <typename FormatContext>
+ auto format(const std::atomic<T>& v, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ return formatter<T, Char>::format(v.load(), ctx);
+ }
+};
+
+#ifdef __cpp_lib_atomic_flag_test
+FMT_EXPORT
+template <typename Char>
+struct formatter<std::atomic_flag, Char> : formatter<bool, Char> {
+ template <typename FormatContext>
+ auto format(const std::atomic_flag& v, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ return formatter<bool, Char>::format(v.test(), ctx);
+ }
+};
+#endif // __cpp_lib_atomic_flag_test
+
+FMT_END_NAMESPACE
+#endif // FMT_STD_H_
--- /dev/null
+// Formatting library for C++ - optional wchar_t and exotic character support
+//
+// Copyright (c) 2012 - present, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_XCHAR_H_
+#define FMT_XCHAR_H_
+
+#include <cwchar>
+
+#include "format.h"
+
+#ifndef FMT_STATIC_THOUSANDS_SEPARATOR
+# include <locale>
+#endif
+
+FMT_BEGIN_NAMESPACE
+namespace detail {
+
+template <typename T>
+using is_exotic_char = bool_constant<!std::is_same<T, char>::value>;
+
+inline auto write_loc(std::back_insert_iterator<detail::buffer<wchar_t>> out,
+ loc_value value, const format_specs<wchar_t>& specs,
+ locale_ref loc) -> bool {
+#ifndef FMT_STATIC_THOUSANDS_SEPARATOR
+ auto& numpunct =
+ std::use_facet<std::numpunct<wchar_t>>(loc.get<std::locale>());
+ auto separator = std::wstring();
+ auto grouping = numpunct.grouping();
+ if (!grouping.empty()) separator = std::wstring(1, numpunct.thousands_sep());
+ return value.visit(loc_writer<wchar_t>{out, specs, separator, grouping, {}});
+#endif
+ return false;
+}
+} // namespace detail
+
+FMT_BEGIN_EXPORT
+
+using wstring_view = basic_string_view<wchar_t>;
+using wformat_parse_context = basic_format_parse_context<wchar_t>;
+using wformat_context = buffer_context<wchar_t>;
+using wformat_args = basic_format_args<wformat_context>;
+using wmemory_buffer = basic_memory_buffer<wchar_t>;
+
+#if FMT_GCC_VERSION && FMT_GCC_VERSION < 409
+// Workaround broken conversion on older gcc.
+template <typename... Args> using wformat_string = wstring_view;
+inline auto runtime(wstring_view s) -> wstring_view { return s; }
+#else
+template <typename... Args>
+using wformat_string = basic_format_string<wchar_t, type_identity_t<Args>...>;
+inline auto runtime(wstring_view s) -> runtime_format_string<wchar_t> {
+ return {{s}};
+}
+#endif
+
+template <> struct is_char<wchar_t> : std::true_type {};
+template <> struct is_char<detail::char8_type> : std::true_type {};
+template <> struct is_char<char16_t> : std::true_type {};
+template <> struct is_char<char32_t> : std::true_type {};
+
+template <typename... T>
+constexpr auto make_wformat_args(const T&... args)
+ -> format_arg_store<wformat_context, T...> {
+ return {args...};
+}
+
+inline namespace literals {
+#if FMT_USE_USER_DEFINED_LITERALS && !FMT_USE_NONTYPE_TEMPLATE_ARGS
+constexpr auto operator""_a(const wchar_t* s, size_t)
+ -> detail::udl_arg<wchar_t> {
+ return {s};
+}
+#endif
+} // namespace literals
+
+template <typename It, typename Sentinel>
+auto join(It begin, Sentinel end, wstring_view sep)
+ -> join_view<It, Sentinel, wchar_t> {
+ return {begin, end, sep};
+}
+
+template <typename Range>
+auto join(Range&& range, wstring_view sep)
+ -> join_view<detail::iterator_t<Range>, detail::sentinel_t<Range>,
+ wchar_t> {
+ return join(std::begin(range), std::end(range), sep);
+}
+
+template <typename T>
+auto join(std::initializer_list<T> list, wstring_view sep)
+ -> join_view<const T*, const T*, wchar_t> {
+ return join(std::begin(list), std::end(list), sep);
+}
+
+template <typename Char, FMT_ENABLE_IF(!std::is_same<Char, char>::value)>
+auto vformat(basic_string_view<Char> format_str,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args)
+ -> std::basic_string<Char> {
+ auto buf = basic_memory_buffer<Char>();
+ detail::vformat_to(buf, format_str, args);
+ return to_string(buf);
+}
+
+template <typename... T>
+auto format(wformat_string<T...> fmt, T&&... args) -> std::wstring {
+ return vformat(fmt::wstring_view(fmt), fmt::make_wformat_args(args...));
+}
+
+// Pass char_t as a default template parameter instead of using
+// std::basic_string<char_t<S>> to reduce the symbol size.
+template <typename S, typename... T, typename Char = char_t<S>,
+ FMT_ENABLE_IF(!std::is_same<Char, char>::value &&
+ !std::is_same<Char, wchar_t>::value)>
+auto format(const S& format_str, T&&... args) -> std::basic_string<Char> {
+ return vformat(detail::to_string_view(format_str),
+ fmt::make_format_args<buffer_context<Char>>(args...));
+}
+
+template <typename Locale, typename S, typename Char = char_t<S>,
+ FMT_ENABLE_IF(detail::is_locale<Locale>::value&&
+ detail::is_exotic_char<Char>::value)>
+inline auto vformat(
+ const Locale& loc, const S& format_str,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args)
+ -> std::basic_string<Char> {
+ return detail::vformat(loc, detail::to_string_view(format_str), args);
+}
+
+template <typename Locale, typename S, typename... T, typename Char = char_t<S>,
+ FMT_ENABLE_IF(detail::is_locale<Locale>::value&&
+ detail::is_exotic_char<Char>::value)>
+inline auto format(const Locale& loc, const S& format_str, T&&... args)
+ -> std::basic_string<Char> {
+ return detail::vformat(loc, detail::to_string_view(format_str),
+ fmt::make_format_args<buffer_context<Char>>(args...));
+}
+
+template <typename OutputIt, typename S, typename Char = char_t<S>,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, Char>::value&&
+ detail::is_exotic_char<Char>::value)>
+auto vformat_to(OutputIt out, const S& format_str,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args)
+ -> OutputIt {
+ auto&& buf = detail::get_buffer<Char>(out);
+ detail::vformat_to(buf, detail::to_string_view(format_str), args);
+ return detail::get_iterator(buf, out);
+}
+
+template <typename OutputIt, typename S, typename... T,
+ typename Char = char_t<S>,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, Char>::value&&
+ detail::is_exotic_char<Char>::value)>
+inline auto format_to(OutputIt out, const S& fmt, T&&... args) -> OutputIt {
+ return vformat_to(out, detail::to_string_view(fmt),
+ fmt::make_format_args<buffer_context<Char>>(args...));
+}
+
+template <typename Locale, typename S, typename OutputIt, typename... Args,
+ typename Char = char_t<S>,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, Char>::value&&
+ detail::is_locale<Locale>::value&&
+ detail::is_exotic_char<Char>::value)>
+inline auto vformat_to(
+ OutputIt out, const Locale& loc, const S& format_str,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args) -> OutputIt {
+ auto&& buf = detail::get_buffer<Char>(out);
+ vformat_to(buf, detail::to_string_view(format_str), args,
+ detail::locale_ref(loc));
+ return detail::get_iterator(buf, out);
+}
+
+template <typename OutputIt, typename Locale, typename S, typename... T,
+ typename Char = char_t<S>,
+ bool enable = detail::is_output_iterator<OutputIt, Char>::value &&
+ detail::is_locale<Locale>::value &&
+ detail::is_exotic_char<Char>::value>
+inline auto format_to(OutputIt out, const Locale& loc, const S& format_str,
+ T&&... args) ->
+ typename std::enable_if<enable, OutputIt>::type {
+ return vformat_to(out, loc, detail::to_string_view(format_str),
+ fmt::make_format_args<buffer_context<Char>>(args...));
+}
+
+template <typename OutputIt, typename Char, typename... Args,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, Char>::value&&
+ detail::is_exotic_char<Char>::value)>
+inline auto vformat_to_n(
+ OutputIt out, size_t n, basic_string_view<Char> format_str,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args)
+ -> format_to_n_result<OutputIt> {
+ using traits = detail::fixed_buffer_traits;
+ auto buf = detail::iterator_buffer<OutputIt, Char, traits>(out, n);
+ detail::vformat_to(buf, format_str, args);
+ return {buf.out(), buf.count()};
+}
+
+template <typename OutputIt, typename S, typename... T,
+ typename Char = char_t<S>,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, Char>::value&&
+ detail::is_exotic_char<Char>::value)>
+inline auto format_to_n(OutputIt out, size_t n, const S& fmt, T&&... args)
+ -> format_to_n_result<OutputIt> {
+ return vformat_to_n(out, n, detail::to_string_view(fmt),
+ fmt::make_format_args<buffer_context<Char>>(args...));
+}
+
+template <typename S, typename... T, typename Char = char_t<S>,
+ FMT_ENABLE_IF(detail::is_exotic_char<Char>::value)>
+inline auto formatted_size(const S& fmt, T&&... args) -> size_t {
+ auto buf = detail::counting_buffer<Char>();
+ detail::vformat_to(buf, detail::to_string_view(fmt),
+ fmt::make_format_args<buffer_context<Char>>(args...));
+ return buf.count();
+}
+
+inline void vprint(std::FILE* f, wstring_view fmt, wformat_args args) {
+ auto buf = wmemory_buffer();
+ detail::vformat_to(buf, fmt, args);
+ buf.push_back(L'\0');
+ if (std::fputws(buf.data(), f) == -1)
+ FMT_THROW(system_error(errno, FMT_STRING("cannot write to file")));
+}
+
+inline void vprint(wstring_view fmt, wformat_args args) {
+ vprint(stdout, fmt, args);
+}
+
+template <typename... T>
+void print(std::FILE* f, wformat_string<T...> fmt, T&&... args) {
+ return vprint(f, wstring_view(fmt), fmt::make_wformat_args(args...));
+}
+
+template <typename... T> void print(wformat_string<T...> fmt, T&&... args) {
+ return vprint(wstring_view(fmt), fmt::make_wformat_args(args...));
+}
+
+template <typename... T>
+void println(std::FILE* f, wformat_string<T...> fmt, T&&... args) {
+ return print(f, L"{}\n", fmt::format(fmt, std::forward<T>(args)...));
+}
+
+template <typename... T> void println(wformat_string<T...> fmt, T&&... args) {
+ return print(L"{}\n", fmt::format(fmt, std::forward<T>(args)...));
+}
+
+/**
+ Converts *value* to ``std::wstring`` using the default format for type *T*.
+ */
+template <typename T> inline auto to_wstring(const T& value) -> std::wstring {
+ return format(FMT_STRING(L"{}"), value);
+}
+FMT_END_EXPORT
+FMT_END_NAMESPACE
+
+#endif // FMT_XCHAR_H_
--- /dev/null
+// Copyright 2016-2018 by Martin Moene
+//
+// https://github.com/martinmoene/variant-lite
+//
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+#pragma once
+
+#ifndef NONSTD_VARIANT_LITE_HPP
+#define NONSTD_VARIANT_LITE_HPP
+
+#define variant_lite_MAJOR 1
+#define variant_lite_MINOR 2
+#define variant_lite_PATCH 2
+
+#define variant_lite_VERSION \
+ variant_STRINGIFY(variant_lite_MAJOR) "." variant_STRINGIFY( \
+ variant_lite_MINOR) "." variant_STRINGIFY(variant_lite_PATCH)
+
+#define variant_STRINGIFY(x) variant_STRINGIFY_(x)
+#define variant_STRINGIFY_(x) #x
+
+// variant-lite configuration:
+
+#define variant_VARIANT_DEFAULT 0
+#define variant_VARIANT_NONSTD 1
+#define variant_VARIANT_STD 2
+
+#if !defined(variant_CONFIG_SELECT_VARIANT)
+#define variant_CONFIG_SELECT_VARIANT \
+ (variant_HAVE_STD_VARIANT ? variant_VARIANT_STD : variant_VARIANT_NONSTD)
+#endif
+
+#ifndef variant_CONFIG_OMIT_VARIANT_SIZE_V_MACRO
+#define variant_CONFIG_OMIT_VARIANT_SIZE_V_MACRO 0
+#endif
+
+#ifndef variant_CONFIG_OMIT_VARIANT_ALTERNATIVE_T_MACRO
+#define variant_CONFIG_OMIT_VARIANT_ALTERNATIVE_T_MACRO 0
+#endif
+
+// Control presence of exception handling (try and auto discover):
+
+#ifndef variant_CONFIG_NO_EXCEPTIONS
+#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
+#define variant_CONFIG_NO_EXCEPTIONS 0
+#else
+#define variant_CONFIG_NO_EXCEPTIONS 1
+#endif
+#endif
+
+// C++ language version detection (C++20 is speculative):
+// Note: VC14.0/1900 (VS2015) lacks too much from C++14.
+
+#ifndef variant_CPLUSPLUS
+#if defined(_MSVC_LANG) && !defined(__clang__)
+#define variant_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG)
+#else
+#define variant_CPLUSPLUS __cplusplus
+#endif
+#endif
+
+#define variant_CPP98_OR_GREATER (variant_CPLUSPLUS >= 199711L)
+#define variant_CPP11_OR_GREATER (variant_CPLUSPLUS >= 201103L)
+#define variant_CPP11_OR_GREATER_ (variant_CPLUSPLUS >= 201103L)
+#define variant_CPP14_OR_GREATER (variant_CPLUSPLUS >= 201402L)
+#define variant_CPP17_OR_GREATER (variant_CPLUSPLUS >= 201703L)
+#define variant_CPP20_OR_GREATER (variant_CPLUSPLUS >= 202000L)
+
+// Use C++17 std::variant if available and requested:
+
+#if variant_CPP17_OR_GREATER && defined(__has_include)
+#if __has_include(<variant> )
+#define variant_HAVE_STD_VARIANT 1
+#else
+#define variant_HAVE_STD_VARIANT 0
+#endif
+#else
+#define variant_HAVE_STD_VARIANT 0
+#endif
+
+#define variant_USES_STD_VARIANT \
+ ((variant_CONFIG_SELECT_VARIANT == variant_VARIANT_STD) || \
+ ((variant_CONFIG_SELECT_VARIANT == variant_VARIANT_DEFAULT) && variant_HAVE_STD_VARIANT))
+
+//
+// in_place: code duplicated in any-lite, expected-lite, optional-lite, value-ptr-lite,
+// variant-lite:
+//
+
+#ifndef nonstd_lite_HAVE_IN_PLACE_TYPES
+#define nonstd_lite_HAVE_IN_PLACE_TYPES 1
+
+// C++17 std::in_place in <utility>:
+
+#if variant_CPP17_OR_GREATER
+
+#include <utility>
+
+namespace nonstd {
+
+using std::in_place;
+using std::in_place_index;
+using std::in_place_index_t;
+using std::in_place_t;
+using std::in_place_type;
+using std::in_place_type_t;
+
+#define nonstd_lite_in_place_t(T) std::in_place_t
+#define nonstd_lite_in_place_type_t(T) std::in_place_type_t<T>
+#define nonstd_lite_in_place_index_t(K) std::in_place_index_t<K>
+
+#define nonstd_lite_in_place(T) \
+ std::in_place_t {}
+#define nonstd_lite_in_place_type(T) \
+ std::in_place_type_t<T> {}
+#define nonstd_lite_in_place_index(K) \
+ std::in_place_index_t<K> {}
+
+} // namespace nonstd
+
+#else // variant_CPP17_OR_GREATER
+
+#include <cstddef>
+
+namespace nonstd {
+namespace detail {
+
+template <class T> struct in_place_type_tag {};
+
+template <std::size_t K> struct in_place_index_tag {};
+
+} // namespace detail
+
+struct in_place_t {};
+
+template <class T>
+inline in_place_t in_place(detail::in_place_type_tag<T> = detail::in_place_type_tag<T>()) {
+ return in_place_t();
+}
+
+template <std::size_t K>
+inline in_place_t in_place(detail::in_place_index_tag<K> = detail::in_place_index_tag<K>()) {
+ return in_place_t();
+}
+
+template <class T>
+inline in_place_t in_place_type(detail::in_place_type_tag<T> = detail::in_place_type_tag<T>()) {
+ return in_place_t();
+}
+
+template <std::size_t K>
+inline in_place_t in_place_index(detail::in_place_index_tag<K> = detail::in_place_index_tag<K>()) {
+ return in_place_t();
+}
+
+// mimic templated typedef:
+
+#define nonstd_lite_in_place_t(T) nonstd::in_place_t (&)(nonstd::detail::in_place_type_tag<T>)
+#define nonstd_lite_in_place_type_t(T) nonstd::in_place_t (&)(nonstd::detail::in_place_type_tag<T>)
+#define nonstd_lite_in_place_index_t(K) \
+ nonstd::in_place_t (&)(nonstd::detail::in_place_index_tag<K>)
+
+#define nonstd_lite_in_place(T) nonstd::in_place_type<T>
+#define nonstd_lite_in_place_type(T) nonstd::in_place_type<T>
+#define nonstd_lite_in_place_index(K) nonstd::in_place_index<K>
+
+} // namespace nonstd
+
+#endif // variant_CPP17_OR_GREATER
+#endif // nonstd_lite_HAVE_IN_PLACE_TYPES
+
+//
+// Use C++17 std::variant:
+//
+
+#if variant_USES_STD_VARIANT
+
+#include <functional> // std::hash<>
+#include <variant>
+
+#if !variant_CONFIG_OMIT_VARIANT_SIZE_V_MACRO
+#define variant_size_V(T) nonstd::variant_size<T>::value
+#endif
+
+#if !variant_CONFIG_OMIT_VARIANT_ALTERNATIVE_T_MACRO
+#define variant_alternative_T(K, T) typename nonstd::variant_alternative<K, T>::type
+#endif
+
+namespace nonstd {
+
+using std::bad_variant_access;
+using std::hash;
+using std::monostate;
+using std::variant;
+using std::variant_alternative;
+using std::variant_alternative_t;
+using std::variant_size;
+using std::variant_size_v;
+
+using std::get;
+using std::get_if;
+using std::holds_alternative;
+using std::visit;
+using std::operator==;
+using std::operator!=;
+using std::operator<;
+using std::operator<=;
+using std::operator>;
+using std::operator>=;
+using std::swap;
+
+constexpr auto variant_npos = std::variant_npos;
+} // namespace nonstd
+
+#else // variant_USES_STD_VARIANT
+
+#include <cstddef>
+#include <limits>
+#include <new>
+#include <utility>
+
+#if variant_CONFIG_NO_EXCEPTIONS
+#include <cassert>
+#else
+#include <stdexcept>
+#endif
+
+// variant-lite type and visitor argument count configuration (script/generate_header.py):
+
+#define variant_CONFIG_MAX_TYPE_COUNT 16
+#define variant_CONFIG_MAX_VISITOR_ARG_COUNT 5
+
+// variant-lite alignment configuration:
+
+#ifndef variant_CONFIG_MAX_ALIGN_HACK
+#define variant_CONFIG_MAX_ALIGN_HACK 0
+#endif
+
+#ifndef variant_CONFIG_ALIGN_AS
+// no default, used in #if defined()
+#endif
+
+#ifndef variant_CONFIG_ALIGN_AS_FALLBACK
+#define variant_CONFIG_ALIGN_AS_FALLBACK double
+#endif
+
+// half-open range [lo..hi):
+#define variant_BETWEEN(v, lo, hi) ((lo) <= (v) && (v) < (hi))
+
+// Compiler versions:
+//
+// MSVC++ 6.0 _MSC_VER == 1200 variant_COMPILER_MSVC_VERSION == 60 (Visual Studio 6.0)
+// MSVC++ 7.0 _MSC_VER == 1300 variant_COMPILER_MSVC_VERSION == 70 (Visual Studio .NET 2002)
+// MSVC++ 7.1 _MSC_VER == 1310 variant_COMPILER_MSVC_VERSION == 71 (Visual Studio .NET 2003)
+// MSVC++ 8.0 _MSC_VER == 1400 variant_COMPILER_MSVC_VERSION == 80 (Visual Studio 2005)
+// MSVC++ 9.0 _MSC_VER == 1500 variant_COMPILER_MSVC_VERSION == 90 (Visual Studio 2008)
+// MSVC++ 10.0 _MSC_VER == 1600 variant_COMPILER_MSVC_VERSION == 100 (Visual Studio 2010)
+// MSVC++ 11.0 _MSC_VER == 1700 variant_COMPILER_MSVC_VERSION == 110 (Visual Studio 2012)
+// MSVC++ 12.0 _MSC_VER == 1800 variant_COMPILER_MSVC_VERSION == 120 (Visual Studio 2013)
+// MSVC++ 14.0 _MSC_VER == 1900 variant_COMPILER_MSVC_VERSION == 140 (Visual Studio 2015)
+// MSVC++ 14.1 _MSC_VER >= 1910 variant_COMPILER_MSVC_VERSION == 141 (Visual Studio 2017)
+// MSVC++ 14.2 _MSC_VER >= 1920 variant_COMPILER_MSVC_VERSION == 142 (Visual Studio 2019)
+
+#if defined(_MSC_VER) && !defined(__clang__)
+#define variant_COMPILER_MSVC_VER (_MSC_VER)
+#define variant_COMPILER_MSVC_VERSION (_MSC_VER / 10 - 10 * (5 + (_MSC_VER < 1900)))
+#else
+#define variant_COMPILER_MSVC_VER 0
+#define variant_COMPILER_MSVC_VERSION 0
+#endif
+
+#define variant_COMPILER_VERSION(major, minor, patch) (10 * (10 * (major) + (minor)) + (patch))
+
+#if defined(__clang__)
+#define variant_COMPILER_CLANG_VERSION \
+ variant_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__)
+#else
+#define variant_COMPILER_CLANG_VERSION 0
+#endif
+
+#if defined(__GNUC__) && !defined(__clang__)
+#define variant_COMPILER_GNUC_VERSION \
+ variant_COMPILER_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)
+#else
+#define variant_COMPILER_GNUC_VERSION 0
+#endif
+
+#if variant_BETWEEN(variant_COMPILER_MSVC_VER, 1300, 1900)
+#pragma warning(push)
+#pragma warning(disable : 4345) // initialization behavior changed
+#endif
+
+// Presence of language and library features:
+
+#define variant_HAVE(feature) (variant_HAVE_##feature)
+
+#ifdef _HAS_CPP0X
+#define variant_HAS_CPP0X _HAS_CPP0X
+#else
+#define variant_HAS_CPP0X 0
+#endif
+
+// Unless defined otherwise below, consider VC14 as C++11 for variant-lite:
+
+#if variant_COMPILER_MSVC_VER >= 1900
+#undef variant_CPP11_OR_GREATER
+#define variant_CPP11_OR_GREATER 1
+#endif
+
+#define variant_CPP11_90 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1500)
+#define variant_CPP11_100 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1600)
+#define variant_CPP11_110 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1700)
+#define variant_CPP11_120 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1800)
+#define variant_CPP11_140 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1900)
+#define variant_CPP11_141 (variant_CPP11_OR_GREATER_ || variant_COMPILER_MSVC_VER >= 1910)
+
+#define variant_CPP14_000 (variant_CPP14_OR_GREATER)
+#define variant_CPP17_000 (variant_CPP17_OR_GREATER)
+
+// Presence of C++11 language features:
+
+#define variant_HAVE_CONSTEXPR_11 variant_CPP11_140
+#define variant_HAVE_INITIALIZER_LIST variant_CPP11_120
+#define variant_HAVE_NOEXCEPT variant_CPP11_140
+#define variant_HAVE_NULLPTR variant_CPP11_100
+#define variant_HAVE_OVERRIDE variant_CPP11_140
+
+// Presence of C++14 language features:
+
+#define variant_HAVE_CONSTEXPR_14 variant_CPP14_000
+
+// Presence of C++17 language features:
+
+// no flag
+
+// Presence of C++ library features:
+
+#define variant_HAVE_CONDITIONAL variant_CPP11_120
+#define variant_HAVE_REMOVE_CV variant_CPP11_120
+#define variant_HAVE_STD_ADD_POINTER variant_CPP11_90
+#define variant_HAVE_TYPE_TRAITS variant_CPP11_90
+
+#define variant_HAVE_TR1_TYPE_TRAITS (!!variant_COMPILER_GNUC_VERSION)
+#define variant_HAVE_TR1_ADD_POINTER (!!variant_COMPILER_GNUC_VERSION)
+
+// C++ feature usage:
+
+#if variant_HAVE_CONSTEXPR_11
+#define variant_constexpr constexpr
+#else
+#define variant_constexpr /*constexpr*/
+#endif
+
+#if variant_HAVE_CONSTEXPR_14
+#define variant_constexpr14 constexpr
+#else
+#define variant_constexpr14 /*constexpr*/
+#endif
+
+#if variant_HAVE_NOEXCEPT
+#define variant_noexcept noexcept
+#else
+#define variant_noexcept /*noexcept*/
+#endif
+
+#if variant_HAVE_NULLPTR
+#define variant_nullptr nullptr
+#else
+#define variant_nullptr NULL
+#endif
+
+#if variant_HAVE_OVERRIDE
+#define variant_override override
+#else
+#define variant_override /*override*/
+#endif
+
+// additional includes:
+
+#if variant_CPP11_OR_GREATER
+#include <functional> // std::hash
+#endif
+
+#if variant_HAVE_INITIALIZER_LIST
+#include <initializer_list>
+#endif
+
+#if variant_HAVE_TYPE_TRAITS
+#include <type_traits>
+#elif variant_HAVE_TR1_TYPE_TRAITS
+#include <tr1/type_traits>
+#endif
+
+// Method enabling
+
+#if variant_CPP11_OR_GREATER
+
+#define variant_REQUIRES_0(...) \
+ template <bool B = (__VA_ARGS__), typename std::enable_if<B, int>::type = 0>
+
+#define variant_REQUIRES_T(...) , typename std::enable_if<(__VA_ARGS__), int>::type = 0
+
+#define variant_REQUIRES_R(R, ...) typename std::enable_if<(__VA_ARGS__), R>::type
+
+#define variant_REQUIRES_A(...) , typename std::enable_if<(__VA_ARGS__), void *>::type = nullptr
+
+#endif
+
+//
+// variant:
+//
+
+namespace nonstd {
+namespace variants {
+
+// C++11 emulation:
+
+namespace std11 {
+
+#if variant_HAVE_STD_ADD_POINTER
+
+using std::add_pointer;
+
+#elif variant_HAVE_TR1_ADD_POINTER
+
+using std::tr1::add_pointer;
+
+#else
+
+template <class T> struct remove_reference { typedef T type; };
+template <class T> struct remove_reference<T &> { typedef T type; };
+
+template <class T> struct add_pointer { typedef typename remove_reference<T>::type *type; };
+
+#endif // variant_HAVE_STD_ADD_POINTER
+
+#if variant_HAVE_REMOVE_CV
+
+using std::remove_cv;
+
+#else
+
+template <class T> struct remove_const { typedef T type; };
+template <class T> struct remove_const<const T> { typedef T type; };
+
+template <class T> struct remove_volatile { typedef T type; };
+template <class T> struct remove_volatile<volatile T> { typedef T type; };
+
+template <class T> struct remove_cv {
+ typedef typename remove_volatile<typename remove_const<T>::type>::type type;
+};
+
+#endif // variant_HAVE_REMOVE_CV
+
+#if variant_HAVE_CONDITIONAL
+
+using std::conditional;
+
+#else
+
+template <bool Cond, class Then, class Else> struct conditional;
+
+template <class Then, class Else> struct conditional<true, Then, Else> { typedef Then type; };
+
+template <class Then, class Else> struct conditional<false, Then, Else> { typedef Else type; };
+
+#endif // variant_HAVE_CONDITIONAL
+
+} // namespace std11
+
+/// type traits C++17:
+
+namespace std17 {
+
+#if variant_CPP17_OR_GREATER
+
+using std::is_nothrow_swappable;
+using std::is_swappable;
+
+#elif variant_CPP11_OR_GREATER
+
+namespace detail {
+
+using std::swap;
+
+struct is_swappable {
+ template <typename T, typename = decltype(swap(std::declval<T &>(), std::declval<T &>()))>
+ static std::true_type test(int);
+
+ template <typename> static std::false_type test(...);
+};
+
+struct is_nothrow_swappable {
+ // wrap noexcept(epr) in separate function as work-around for VC140 (VS2015):
+
+ template <typename T> static constexpr bool test() {
+ return noexcept(swap(std::declval<T &>(), std::declval<T &>()));
+ }
+
+ template <typename T> static auto test(int) -> std::integral_constant<bool, test<T>()> {}
+
+ template <typename> static std::false_type test(...);
+};
+
+} // namespace detail
+
+// is [nothow] swappable:
+
+template <typename T> struct is_swappable : decltype(detail::is_swappable::test<T>(0)) {};
+
+template <typename T>
+struct is_nothrow_swappable : decltype(detail::is_nothrow_swappable::test<T>(0)) {};
+
+#endif // variant_CPP17_OR_GREATER
+
+} // namespace std17
+
+// detail:
+
+namespace detail {
+
+// typelist:
+
+#define variant_TL1(T1) detail::typelist<T1, detail::nulltype>
+#define variant_TL2(T1, T2) detail::typelist<T1, variant_TL1(T2)>
+#define variant_TL3(T1, T2, T3) detail::typelist<T1, variant_TL2(T2, T3)>
+#define variant_TL4(T1, T2, T3, T4) detail::typelist<T1, variant_TL3(T2, T3, T4)>
+#define variant_TL5(T1, T2, T3, T4, T5) detail::typelist<T1, variant_TL4(T2, T3, T4, T5)>
+#define variant_TL6(T1, T2, T3, T4, T5, T6) detail::typelist<T1, variant_TL5(T2, T3, T4, T5, T6)>
+#define variant_TL7(T1, T2, T3, T4, T5, T6, T7) \
+ detail::typelist<T1, variant_TL6(T2, T3, T4, T5, T6, T7)>
+#define variant_TL8(T1, T2, T3, T4, T5, T6, T7, T8) \
+ detail::typelist<T1, variant_TL7(T2, T3, T4, T5, T6, T7, T8)>
+#define variant_TL9(T1, T2, T3, T4, T5, T6, T7, T8, T9) \
+ detail::typelist<T1, variant_TL8(T2, T3, T4, T5, T6, T7, T8, T9)>
+#define variant_TL10(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) \
+ detail::typelist<T1, variant_TL9(T2, T3, T4, T5, T6, T7, T8, T9, T10)>
+#define variant_TL11(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11) \
+ detail::typelist<T1, variant_TL10(T2, T3, T4, T5, T6, T7, T8, T9, T10, T11)>
+#define variant_TL12(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12) \
+ detail::typelist<T1, variant_TL11(T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12)>
+#define variant_TL13(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13) \
+ detail::typelist<T1, variant_TL12(T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13)>
+#define variant_TL14(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14) \
+ detail::typelist<T1, variant_TL13(T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14)>
+#define variant_TL15(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15) \
+ detail::typelist<T1, variant_TL14(T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15)>
+#define variant_TL16(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16) \
+ detail::typelist<T1, variant_TL15(T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, \
+ T16)>
+
+// variant parameter unused type tags:
+
+template <class T> struct TX : T {
+ inline TX<T> operator+() const { return TX<T>(); }
+ inline TX<T> operator-() const { return TX<T>(); }
+
+ inline TX<T> operator!() const { return TX<T>(); }
+ inline TX<T> operator~() const { return TX<T>(); }
+
+ inline TX<T> *operator&() const { return variant_nullptr; }
+
+ template <class U> inline TX<T> operator*(U const &)const { return TX<T>(); }
+ template <class U> inline TX<T> operator/(U const &) const { return TX<T>(); }
+
+ template <class U> inline TX<T> operator%(U const &) const { return TX<T>(); }
+ template <class U> inline TX<T> operator+(U const &) const { return TX<T>(); }
+ template <class U> inline TX<T> operator-(U const &) const { return TX<T>(); }
+
+ template <class U> inline TX<T> operator<<(U const &) const { return TX<T>(); }
+ template <class U> inline TX<T> operator>>(U const &) const { return TX<T>(); }
+
+ inline bool operator==(T const &) const { return false; }
+ inline bool operator<(T const &) const { return false; }
+
+ template <class U> inline TX<T> operator&(U const &)const { return TX<T>(); }
+ template <class U> inline TX<T> operator|(U const &) const { return TX<T>(); }
+ template <class U> inline TX<T> operator^(U const &) const { return TX<T>(); }
+
+ template <class U> inline TX<T> operator&&(U const &) const { return TX<T>(); }
+ template <class U> inline TX<T> operator||(U const &) const { return TX<T>(); }
+};
+
+struct S0 {};
+typedef TX<S0> T0;
+struct S1 {};
+typedef TX<S1> T1;
+struct S2 {};
+typedef TX<S2> T2;
+struct S3 {};
+typedef TX<S3> T3;
+struct S4 {};
+typedef TX<S4> T4;
+struct S5 {};
+typedef TX<S5> T5;
+struct S6 {};
+typedef TX<S6> T6;
+struct S7 {};
+typedef TX<S7> T7;
+struct S8 {};
+typedef TX<S8> T8;
+struct S9 {};
+typedef TX<S9> T9;
+struct S10 {};
+typedef TX<S10> T10;
+struct S11 {};
+typedef TX<S11> T11;
+struct S12 {};
+typedef TX<S12> T12;
+struct S13 {};
+typedef TX<S13> T13;
+struct S14 {};
+typedef TX<S14> T14;
+struct S15 {};
+typedef TX<S15> T15;
+
+struct nulltype {};
+
+template <class Head, class Tail> struct typelist {
+ typedef Head head;
+ typedef Tail tail;
+};
+
+// typelist max element size:
+
+template <class List> struct typelist_max;
+
+template <> struct typelist_max<nulltype> {
+ enum V { value = 0 };
+ typedef void type;
+};
+
+template <class Head, class Tail> struct typelist_max<typelist<Head, Tail>> {
+private:
+ enum TV { tail_value = size_t(typelist_max<Tail>::value) };
+
+ typedef typename typelist_max<Tail>::type tail_type;
+
+public:
+ enum V { value = (sizeof(Head) > tail_value) ? sizeof(Head) : std::size_t(tail_value) };
+
+ typedef typename std11::conditional<(sizeof(Head) > tail_value), Head, tail_type>::type type;
+};
+
+#if variant_CPP11_OR_GREATER
+
+// typelist max alignof element type:
+
+template <class List> struct typelist_max_alignof;
+
+template <> struct typelist_max_alignof<nulltype> {
+ enum V { value = 0 };
+};
+
+template <class Head, class Tail> struct typelist_max_alignof<typelist<Head, Tail>> {
+private:
+ enum TV { tail_value = size_t(typelist_max_alignof<Tail>::value) };
+
+public:
+ enum V { value = (alignof(Head) > tail_value) ? alignof(Head) : std::size_t(tail_value) };
+};
+
+#endif
+
+// typelist size (length):
+
+template <class List> struct typelist_size {
+ enum V { value = 1 };
+};
+
+template <> struct typelist_size<T0> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T1> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T2> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T3> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T4> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T5> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T6> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T7> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T8> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T9> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T10> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T11> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T12> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T13> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T14> {
+ enum V { value = 0 };
+};
+template <> struct typelist_size<T15> {
+ enum V { value = 0 };
+};
+
+template <> struct typelist_size<nulltype> {
+ enum V { value = 0 };
+};
+
+template <class Head, class Tail> struct typelist_size<typelist<Head, Tail>> {
+ enum V { value = typelist_size<Head>::value + typelist_size<Tail>::value };
+};
+
+// typelist index of type:
+
+template <class List, class T> struct typelist_index_of;
+
+template <class T> struct typelist_index_of<nulltype, T> {
+ enum V { value = -1 };
+};
+
+template <class Tail, class T> struct typelist_index_of<typelist<T, Tail>, T> {
+ enum V { value = 0 };
+};
+
+template <class Head, class Tail, class T> struct typelist_index_of<typelist<Head, Tail>, T> {
+private:
+ enum TV { nextVal = typelist_index_of<Tail, T>::value };
+
+public:
+ enum V { value = nextVal == -1 ? -1 : 1 + nextVal };
+};
+
+// typelist type at index:
+
+template <class List, std::size_t i> struct typelist_type_at;
+
+template <class Head, class Tail> struct typelist_type_at<typelist<Head, Tail>, 0> {
+ typedef Head type;
+};
+
+template <class Head, class Tail, std::size_t i> struct typelist_type_at<typelist<Head, Tail>, i> {
+ typedef typename typelist_type_at<Tail, i - 1>::type type;
+};
+
+#if variant_CONFIG_MAX_ALIGN_HACK
+
+// Max align, use most restricted type for alignment:
+
+#define variant_UNIQUE(name) variant_UNIQUE2(name, __LINE__)
+#define variant_UNIQUE2(name, line) variant_UNIQUE3(name, line)
+#define variant_UNIQUE3(name, line) name##line
+
+#define variant_ALIGN_TYPE(type) \
+ type variant_UNIQUE(_t); \
+ struct_t<type> variant_UNIQUE(_st)
+
+template <class T> struct struct_t { T _; };
+
+union max_align_t {
+ variant_ALIGN_TYPE(char);
+ variant_ALIGN_TYPE(short int);
+ variant_ALIGN_TYPE(int);
+ variant_ALIGN_TYPE(long int);
+ variant_ALIGN_TYPE(float);
+ variant_ALIGN_TYPE(double);
+ variant_ALIGN_TYPE(long double);
+ variant_ALIGN_TYPE(char *);
+ variant_ALIGN_TYPE(short int *);
+ variant_ALIGN_TYPE(int *);
+ variant_ALIGN_TYPE(long int *);
+ variant_ALIGN_TYPE(float *);
+ variant_ALIGN_TYPE(double *);
+ variant_ALIGN_TYPE(long double *);
+ variant_ALIGN_TYPE(void *);
+
+#ifdef HAVE_LONG_LONG
+ variant_ALIGN_TYPE(long long);
+#endif
+
+ struct Unknown;
+
+ Unknown (*variant_UNIQUE(_))(Unknown);
+ Unknown *Unknown::*variant_UNIQUE(_);
+ Unknown (Unknown::*variant_UNIQUE(_))(Unknown);
+
+ struct_t<Unknown (*)(Unknown)> variant_UNIQUE(_);
+ struct_t<Unknown * Unknown::*> variant_UNIQUE(_);
+ struct_t<Unknown (Unknown::*)(Unknown)> variant_UNIQUE(_);
+};
+
+#undef variant_UNIQUE
+#undef variant_UNIQUE2
+#undef variant_UNIQUE3
+
+#undef variant_ALIGN_TYPE
+
+#elif defined(variant_CONFIG_ALIGN_AS) // variant_CONFIG_MAX_ALIGN_HACK
+
+// Use user-specified type for alignment:
+
+#define variant_ALIGN_AS(unused) variant_CONFIG_ALIGN_AS
+
+#else // variant_CONFIG_MAX_ALIGN_HACK
+
+// Determine POD type to use for alignment:
+
+#define variant_ALIGN_AS(to_align) \
+ typename detail::type_of_size<detail::alignment_types, \
+ detail::alignment_of<to_align>::value>::type
+
+template <typename T> struct alignment_of;
+
+template <typename T> struct alignment_of_hack {
+ char c;
+ T t;
+ alignment_of_hack();
+};
+
+template <size_t A, size_t S> struct alignment_logic {
+ enum V { value = A < S ? A : S };
+};
+
+template <typename T> struct alignment_of {
+ enum V { value = alignment_logic<sizeof(alignment_of_hack<T>) - sizeof(T), sizeof(T)>::value };
+};
+
+template <typename List, size_t N> struct type_of_size {
+ typedef
+ typename std11::conditional<N == sizeof(typename List::head), typename List::head,
+ typename type_of_size<typename List::tail, N>::type>::type type;
+};
+
+template <size_t N> struct type_of_size<nulltype, N> {
+ typedef variant_CONFIG_ALIGN_AS_FALLBACK type;
+};
+
+template <typename T> struct struct_t { T _; };
+
+#define variant_ALIGN_TYPE(type) typelist < type, typelist < struct_t<type>
+
+struct Unknown;
+
+typedef variant_ALIGN_TYPE(char), variant_ALIGN_TYPE(short), variant_ALIGN_TYPE(int),
+ variant_ALIGN_TYPE(long), variant_ALIGN_TYPE(float), variant_ALIGN_TYPE(double),
+ variant_ALIGN_TYPE(long double),
+
+ variant_ALIGN_TYPE(char *), variant_ALIGN_TYPE(short *), variant_ALIGN_TYPE(int *),
+ variant_ALIGN_TYPE(long *), variant_ALIGN_TYPE(float *), variant_ALIGN_TYPE(double *),
+ variant_ALIGN_TYPE(long double *),
+
+ variant_ALIGN_TYPE(Unknown (*)(Unknown)), variant_ALIGN_TYPE(Unknown *Unknown::*),
+ variant_ALIGN_TYPE(Unknown (Unknown::*)(Unknown)),
+
+ nulltype >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> alignment_types;
+
+#undef variant_ALIGN_TYPE
+
+#endif // variant_CONFIG_MAX_ALIGN_HACK
+
+#if variant_CPP11_OR_GREATER
+
+template <typename T> inline std::size_t hash(T const &v) { return std::hash<T>()(v); }
+
+inline std::size_t hash(T0 const &) { return 0; }
+inline std::size_t hash(T1 const &) { return 0; }
+inline std::size_t hash(T2 const &) { return 0; }
+inline std::size_t hash(T3 const &) { return 0; }
+inline std::size_t hash(T4 const &) { return 0; }
+inline std::size_t hash(T5 const &) { return 0; }
+inline std::size_t hash(T6 const &) { return 0; }
+inline std::size_t hash(T7 const &) { return 0; }
+inline std::size_t hash(T8 const &) { return 0; }
+inline std::size_t hash(T9 const &) { return 0; }
+inline std::size_t hash(T10 const &) { return 0; }
+inline std::size_t hash(T11 const &) { return 0; }
+inline std::size_t hash(T12 const &) { return 0; }
+inline std::size_t hash(T13 const &) { return 0; }
+inline std::size_t hash(T14 const &) { return 0; }
+inline std::size_t hash(T15 const &) { return 0; }
+
+#endif // variant_CPP11_OR_GREATER
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+struct helper {
+ typedef signed char type_index_t;
+ typedef variant_TL16(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14,
+ T15) variant_types;
+
+ template <class U> static U *as(void *data) { return reinterpret_cast<U *>(data); }
+
+ template <class U> static U const *as(void const *data) {
+ return reinterpret_cast<const U *>(data);
+ }
+
+ static type_index_t to_index_t(std::size_t index) { return static_cast<type_index_t>(index); }
+
+ static void destroy(type_index_t index, void *data) {
+ switch (index) {
+ case 0:
+ as<T0>(data)->~T0();
+ break;
+ case 1:
+ as<T1>(data)->~T1();
+ break;
+ case 2:
+ as<T2>(data)->~T2();
+ break;
+ case 3:
+ as<T3>(data)->~T3();
+ break;
+ case 4:
+ as<T4>(data)->~T4();
+ break;
+ case 5:
+ as<T5>(data)->~T5();
+ break;
+ case 6:
+ as<T6>(data)->~T6();
+ break;
+ case 7:
+ as<T7>(data)->~T7();
+ break;
+ case 8:
+ as<T8>(data)->~T8();
+ break;
+ case 9:
+ as<T9>(data)->~T9();
+ break;
+ case 10:
+ as<T10>(data)->~T10();
+ break;
+ case 11:
+ as<T11>(data)->~T11();
+ break;
+ case 12:
+ as<T12>(data)->~T12();
+ break;
+ case 13:
+ as<T13>(data)->~T13();
+ break;
+ case 14:
+ as<T14>(data)->~T14();
+ break;
+ case 15:
+ as<T15>(data)->~T15();
+ break;
+ }
+ }
+
+#if variant_CPP11_OR_GREATER
+ template <class T, class... Args> static type_index_t construct_t(void *data, Args &&... args) {
+ new (data) T(std::forward<Args>(args)...);
+
+ return to_index_t(detail::typelist_index_of<variant_types, T>::value);
+ }
+
+ template <std::size_t K, class... Args>
+ static type_index_t construct_i(void *data, Args &&... args) {
+ using type = typename detail::typelist_type_at<variant_types, K>::type;
+
+ construct_t<type>(data, std::forward<Args>(args)...);
+
+ return to_index_t(K);
+ }
+
+ static type_index_t move_construct(type_index_t const from_index, void *from_value,
+ void *to_value) {
+ switch (from_index) {
+ case 0:
+ new (to_value) T0(std::move(*as<T0>(from_value)));
+ break;
+ case 1:
+ new (to_value) T1(std::move(*as<T1>(from_value)));
+ break;
+ case 2:
+ new (to_value) T2(std::move(*as<T2>(from_value)));
+ break;
+ case 3:
+ new (to_value) T3(std::move(*as<T3>(from_value)));
+ break;
+ case 4:
+ new (to_value) T4(std::move(*as<T4>(from_value)));
+ break;
+ case 5:
+ new (to_value) T5(std::move(*as<T5>(from_value)));
+ break;
+ case 6:
+ new (to_value) T6(std::move(*as<T6>(from_value)));
+ break;
+ case 7:
+ new (to_value) T7(std::move(*as<T7>(from_value)));
+ break;
+ case 8:
+ new (to_value) T8(std::move(*as<T8>(from_value)));
+ break;
+ case 9:
+ new (to_value) T9(std::move(*as<T9>(from_value)));
+ break;
+ case 10:
+ new (to_value) T10(std::move(*as<T10>(from_value)));
+ break;
+ case 11:
+ new (to_value) T11(std::move(*as<T11>(from_value)));
+ break;
+ case 12:
+ new (to_value) T12(std::move(*as<T12>(from_value)));
+ break;
+ case 13:
+ new (to_value) T13(std::move(*as<T13>(from_value)));
+ break;
+ case 14:
+ new (to_value) T14(std::move(*as<T14>(from_value)));
+ break;
+ case 15:
+ new (to_value) T15(std::move(*as<T15>(from_value)));
+ break;
+ }
+ return from_index;
+ }
+
+ static type_index_t move_assign(type_index_t const from_index, void *from_value, void *to_value) {
+ switch (from_index) {
+ case 0:
+ *as<T0>(to_value) = std::move(*as<T0>(from_value));
+ break;
+ case 1:
+ *as<T1>(to_value) = std::move(*as<T1>(from_value));
+ break;
+ case 2:
+ *as<T2>(to_value) = std::move(*as<T2>(from_value));
+ break;
+ case 3:
+ *as<T3>(to_value) = std::move(*as<T3>(from_value));
+ break;
+ case 4:
+ *as<T4>(to_value) = std::move(*as<T4>(from_value));
+ break;
+ case 5:
+ *as<T5>(to_value) = std::move(*as<T5>(from_value));
+ break;
+ case 6:
+ *as<T6>(to_value) = std::move(*as<T6>(from_value));
+ break;
+ case 7:
+ *as<T7>(to_value) = std::move(*as<T7>(from_value));
+ break;
+ case 8:
+ *as<T8>(to_value) = std::move(*as<T8>(from_value));
+ break;
+ case 9:
+ *as<T9>(to_value) = std::move(*as<T9>(from_value));
+ break;
+ case 10:
+ *as<T10>(to_value) = std::move(*as<T10>(from_value));
+ break;
+ case 11:
+ *as<T11>(to_value) = std::move(*as<T11>(from_value));
+ break;
+ case 12:
+ *as<T12>(to_value) = std::move(*as<T12>(from_value));
+ break;
+ case 13:
+ *as<T13>(to_value) = std::move(*as<T13>(from_value));
+ break;
+ case 14:
+ *as<T14>(to_value) = std::move(*as<T14>(from_value));
+ break;
+ case 15:
+ *as<T15>(to_value) = std::move(*as<T15>(from_value));
+ break;
+ }
+ return from_index;
+ }
+#endif
+
+ static type_index_t copy_construct(type_index_t const from_index, const void *from_value,
+ void *to_value) {
+ switch (from_index) {
+ case 0:
+ new (to_value) T0(*as<T0>(from_value));
+ break;
+ case 1:
+ new (to_value) T1(*as<T1>(from_value));
+ break;
+ case 2:
+ new (to_value) T2(*as<T2>(from_value));
+ break;
+ case 3:
+ new (to_value) T3(*as<T3>(from_value));
+ break;
+ case 4:
+ new (to_value) T4(*as<T4>(from_value));
+ break;
+ case 5:
+ new (to_value) T5(*as<T5>(from_value));
+ break;
+ case 6:
+ new (to_value) T6(*as<T6>(from_value));
+ break;
+ case 7:
+ new (to_value) T7(*as<T7>(from_value));
+ break;
+ case 8:
+ new (to_value) T8(*as<T8>(from_value));
+ break;
+ case 9:
+ new (to_value) T9(*as<T9>(from_value));
+ break;
+ case 10:
+ new (to_value) T10(*as<T10>(from_value));
+ break;
+ case 11:
+ new (to_value) T11(*as<T11>(from_value));
+ break;
+ case 12:
+ new (to_value) T12(*as<T12>(from_value));
+ break;
+ case 13:
+ new (to_value) T13(*as<T13>(from_value));
+ break;
+ case 14:
+ new (to_value) T14(*as<T14>(from_value));
+ break;
+ case 15:
+ new (to_value) T15(*as<T15>(from_value));
+ break;
+ }
+ return from_index;
+ }
+
+ static type_index_t copy_assign(type_index_t const from_index, const void *from_value,
+ void *to_value) {
+ switch (from_index) {
+ case 0:
+ *as<T0>(to_value) = *as<T0>(from_value);
+ break;
+ case 1:
+ *as<T1>(to_value) = *as<T1>(from_value);
+ break;
+ case 2:
+ *as<T2>(to_value) = *as<T2>(from_value);
+ break;
+ case 3:
+ *as<T3>(to_value) = *as<T3>(from_value);
+ break;
+ case 4:
+ *as<T4>(to_value) = *as<T4>(from_value);
+ break;
+ case 5:
+ *as<T5>(to_value) = *as<T5>(from_value);
+ break;
+ case 6:
+ *as<T6>(to_value) = *as<T6>(from_value);
+ break;
+ case 7:
+ *as<T7>(to_value) = *as<T7>(from_value);
+ break;
+ case 8:
+ *as<T8>(to_value) = *as<T8>(from_value);
+ break;
+ case 9:
+ *as<T9>(to_value) = *as<T9>(from_value);
+ break;
+ case 10:
+ *as<T10>(to_value) = *as<T10>(from_value);
+ break;
+ case 11:
+ *as<T11>(to_value) = *as<T11>(from_value);
+ break;
+ case 12:
+ *as<T12>(to_value) = *as<T12>(from_value);
+ break;
+ case 13:
+ *as<T13>(to_value) = *as<T13>(from_value);
+ break;
+ case 14:
+ *as<T14>(to_value) = *as<T14>(from_value);
+ break;
+ case 15:
+ *as<T15>(to_value) = *as<T15>(from_value);
+ break;
+ }
+ return from_index;
+ }
+};
+
+} // namespace detail
+
+//
+// Variant:
+//
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+class variant;
+
+// 19.7.8 Class monostate
+
+class monostate {};
+
+// 19.7.9 monostate relational operators
+
+inline variant_constexpr bool operator<(monostate, monostate) variant_noexcept { return false; }
+inline variant_constexpr bool operator>(monostate, monostate) variant_noexcept { return false; }
+inline variant_constexpr bool operator<=(monostate, monostate) variant_noexcept { return true; }
+inline variant_constexpr bool operator>=(monostate, monostate) variant_noexcept { return true; }
+inline variant_constexpr bool operator==(monostate, monostate) variant_noexcept { return true; }
+inline variant_constexpr bool operator!=(monostate, monostate) variant_noexcept { return false; }
+
+// 19.7.4 variant helper classes
+
+// obtain the size of the variant's list of alternatives at compile time
+
+template <class T> struct variant_size; /* undefined */
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+struct variant_size<variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>> {
+ enum _ {
+ value = detail::typelist_size<variant_TL16(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11,
+ T12, T13, T14, T15)>::value
+ };
+};
+
+#if variant_CPP14_OR_GREATER
+template <class T> constexpr std::size_t variant_size_v = variant_size<T>::value;
+#endif
+
+#if !variant_CONFIG_OMIT_VARIANT_SIZE_V_MACRO
+#define variant_size_V(T) nonstd::variant_size<T>::value
+#endif
+
+// obtain the type of the alternative specified by its index, at compile time:
+
+template <std::size_t K, class T> struct variant_alternative; /* undefined */
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+ class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+ class T15>
+struct variant_alternative<
+ K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>> {
+ typedef typename detail::typelist_type_at<variant_TL16(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9,
+ T10, T11, T12, T13, T14, T15),
+ K>::type type;
+};
+
+#if variant_CPP11_OR_GREATER
+template <std::size_t K, class T>
+using variant_alternative_t = typename variant_alternative<K, T>::type;
+#endif
+
+#if !variant_CONFIG_OMIT_VARIANT_ALTERNATIVE_T_MACRO
+#define variant_alternative_T(K, T) typename nonstd::variant_alternative<K, T>::type
+#endif
+
+// NTS:implement specializes the std::uses_allocator type trait
+// std::uses_allocator<nonstd::variant>
+
+// index of the variant in the invalid state (constant)
+
+#if variant_CPP11_OR_GREATER
+variant_constexpr std::size_t variant_npos = static_cast<std::size_t>(-1);
+#else
+static const std::size_t variant_npos = static_cast<std::size_t>(-1);
+#endif
+
+#if !variant_CONFIG_NO_EXCEPTIONS
+
+// 19.7.11 Class bad_variant_access
+
+class bad_variant_access : public std::exception {
+public:
+#if variant_CPP11_OR_GREATER
+ virtual const char *what() const variant_noexcept variant_override
+#else
+ virtual const char *what() const throw()
+#endif
+ {
+ return "bad variant access";
+ }
+};
+
+#endif // variant_CONFIG_NO_EXCEPTIONS
+
+// 19.7.3 Class template variant
+
+template <class T0, class T1 = detail::T1, class T2 = detail::T2, class T3 = detail::T3,
+ class T4 = detail::T4, class T5 = detail::T5, class T6 = detail::T6,
+ class T7 = detail::T7, class T8 = detail::T8, class T9 = detail::T9,
+ class T10 = detail::T10, class T11 = detail::T11, class T12 = detail::T12,
+ class T13 = detail::T13, class T14 = detail::T14, class T15 = detail::T15>
+class variant {
+ typedef detail::helper<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>
+ helper_type;
+ typedef variant_TL16(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14,
+ T15) variant_types;
+
+public:
+ // 19.7.3.1 Constructors
+
+ variant() : type_index(0) { new (ptr()) T0(); }
+
+ variant(T0 const &t0) : type_index(0) { new (ptr()) T0(t0); }
+ variant(T1 const &t1) : type_index(1) { new (ptr()) T1(t1); }
+ variant(T2 const &t2) : type_index(2) { new (ptr()) T2(t2); }
+ variant(T3 const &t3) : type_index(3) { new (ptr()) T3(t3); }
+ variant(T4 const &t4) : type_index(4) { new (ptr()) T4(t4); }
+ variant(T5 const &t5) : type_index(5) { new (ptr()) T5(t5); }
+ variant(T6 const &t6) : type_index(6) { new (ptr()) T6(t6); }
+ variant(T7 const &t7) : type_index(7) { new (ptr()) T7(t7); }
+ variant(T8 const &t8) : type_index(8) { new (ptr()) T8(t8); }
+ variant(T9 const &t9) : type_index(9) { new (ptr()) T9(t9); }
+ variant(T10 const &t10) : type_index(10) { new (ptr()) T10(t10); }
+ variant(T11 const &t11) : type_index(11) { new (ptr()) T11(t11); }
+ variant(T12 const &t12) : type_index(12) { new (ptr()) T12(t12); }
+ variant(T13 const &t13) : type_index(13) { new (ptr()) T13(t13); }
+ variant(T14 const &t14) : type_index(14) { new (ptr()) T14(t14); }
+ variant(T15 const &t15) : type_index(15) { new (ptr()) T15(t15); }
+
+#if variant_CPP11_OR_GREATER
+ variant(T0 &&t0) : type_index(0) { new (ptr()) T0(std::move(t0)); }
+ variant(T1 &&t1) : type_index(1) { new (ptr()) T1(std::move(t1)); }
+ variant(T2 &&t2) : type_index(2) { new (ptr()) T2(std::move(t2)); }
+ variant(T3 &&t3) : type_index(3) { new (ptr()) T3(std::move(t3)); }
+ variant(T4 &&t4) : type_index(4) { new (ptr()) T4(std::move(t4)); }
+ variant(T5 &&t5) : type_index(5) { new (ptr()) T5(std::move(t5)); }
+ variant(T6 &&t6) : type_index(6) { new (ptr()) T6(std::move(t6)); }
+ variant(T7 &&t7) : type_index(7) { new (ptr()) T7(std::move(t7)); }
+ variant(T8 &&t8) : type_index(8) { new (ptr()) T8(std::move(t8)); }
+ variant(T9 &&t9) : type_index(9) { new (ptr()) T9(std::move(t9)); }
+ variant(T10 &&t10) : type_index(10) { new (ptr()) T10(std::move(t10)); }
+ variant(T11 &&t11) : type_index(11) { new (ptr()) T11(std::move(t11)); }
+ variant(T12 &&t12) : type_index(12) { new (ptr()) T12(std::move(t12)); }
+ variant(T13 &&t13) : type_index(13) { new (ptr()) T13(std::move(t13)); }
+ variant(T14 &&t14) : type_index(14) { new (ptr()) T14(std::move(t14)); }
+ variant(T15 &&t15) : type_index(15) { new (ptr()) T15(std::move(t15)); }
+
+#endif
+
+ variant(variant const &other) : type_index(other.type_index) {
+ (void)helper_type::copy_construct(other.type_index, other.ptr(), ptr());
+ }
+
+#if variant_CPP11_OR_GREATER
+
+ variant(variant &&other) noexcept(
+ std::is_nothrow_move_constructible<T0>::value &&std::is_nothrow_move_constructible<T1>::value
+ &&std::is_nothrow_move_constructible<T2>::value &&std::is_nothrow_move_constructible<
+ T3>::value &&std::is_nothrow_move_constructible<T4>::value
+ &&std::is_nothrow_move_constructible<T5>::value &&std::is_nothrow_move_constructible<
+ T6>::value &&std::is_nothrow_move_constructible<T7>::value
+ &&std::is_nothrow_move_constructible<T8>::value
+ &&std::is_nothrow_move_constructible<T9>::value
+ &&std::is_nothrow_move_constructible<T10>::value
+ &&std::is_nothrow_move_constructible<T11>::value
+ &&std::is_nothrow_move_constructible<T12>::value
+ &&std::is_nothrow_move_constructible<T13>::value
+ &&std::is_nothrow_move_constructible<T14>::value
+ &&std::is_nothrow_move_constructible<T15>::value)
+ : type_index(other.type_index) {
+ (void)helper_type::move_construct(other.type_index, other.ptr(), ptr());
+ }
+
+ template <std::size_t K>
+ using type_at_t = typename detail::typelist_type_at<variant_types, K>::type;
+
+ template <class T, class... Args variant_REQUIRES_T(std::is_constructible<T, Args...>::value)>
+ explicit variant(nonstd_lite_in_place_type_t(T), Args &&... args) {
+ type_index = variant_npos_internal();
+ type_index = helper_type::template construct_t<T>(ptr(), std::forward<Args>(args)...);
+ }
+
+ template <class T, class U,
+ class... Args variant_REQUIRES_T(
+ std::is_constructible<T, std::initializer_list<U> &, Args...>::value)>
+ explicit variant(nonstd_lite_in_place_type_t(T), std::initializer_list<U> il, Args &&... args) {
+ type_index = variant_npos_internal();
+ type_index = helper_type::template construct_t<T>(ptr(), il, std::forward<Args>(args)...);
+ }
+
+ template <std::size_t K,
+ class... Args variant_REQUIRES_T(std::is_constructible<type_at_t<K>, Args...>::value)>
+ explicit variant(nonstd_lite_in_place_index_t(K), Args &&... args) {
+ type_index = variant_npos_internal();
+ type_index = helper_type::template construct_i<K>(ptr(), std::forward<Args>(args)...);
+ }
+
+ template <size_t K, class U,
+ class... Args variant_REQUIRES_T(
+ std::is_constructible<type_at_t<K>, std::initializer_list<U> &, Args...>::value)>
+ explicit variant(nonstd_lite_in_place_index_t(K), std::initializer_list<U> il, Args &&... args) {
+ type_index = variant_npos_internal();
+ type_index = helper_type::template construct_i<K>(ptr(), il, std::forward<Args>(args)...);
+ }
+
+#endif // variant_CPP11_OR_GREATER
+
+ // 19.7.3.2 Destructor
+
+ ~variant() {
+ if (!valueless_by_exception()) {
+ helper_type::destroy(type_index, ptr());
+ }
+ }
+
+ // 19.7.3.3 Assignment
+
+ variant &operator=(variant const &other) { return copy_assign(other); }
+
+#if variant_CPP11_OR_GREATER
+
+ variant &operator=(variant &&other) noexcept(
+ std::is_nothrow_move_assignable<T0>::value &&std::is_nothrow_move_assignable<T1>::value
+ &&std::is_nothrow_move_assignable<T2>::value &&std::is_nothrow_move_assignable<T3>::value
+ &&std::is_nothrow_move_assignable<T4>::value &&std::is_nothrow_move_assignable<
+ T5>::value &&std::is_nothrow_move_assignable<T6>::value
+ &&std::is_nothrow_move_assignable<T7>::value &&std::is_nothrow_move_assignable<
+ T8>::value &&std::is_nothrow_move_assignable<T9>::value &&
+ std::is_nothrow_move_assignable<T10>::value &&std::is_nothrow_move_assignable<
+ T11>::value &&std::is_nothrow_move_assignable<T12>::value
+ &&std::is_nothrow_move_assignable<T13>::value
+ &&std::is_nothrow_move_assignable<T14>::value
+ &&std::is_nothrow_move_assignable<T15>::value) {
+ return move_assign(std::move(other));
+ }
+
+ variant &operator=(T0 &&t0) { return assign_value<0>(std::move(t0)); }
+ variant &operator=(T1 &&t1) { return assign_value<1>(std::move(t1)); }
+ variant &operator=(T2 &&t2) { return assign_value<2>(std::move(t2)); }
+ variant &operator=(T3 &&t3) { return assign_value<3>(std::move(t3)); }
+ variant &operator=(T4 &&t4) { return assign_value<4>(std::move(t4)); }
+ variant &operator=(T5 &&t5) { return assign_value<5>(std::move(t5)); }
+ variant &operator=(T6 &&t6) { return assign_value<6>(std::move(t6)); }
+ variant &operator=(T7 &&t7) { return assign_value<7>(std::move(t7)); }
+ variant &operator=(T8 &&t8) { return assign_value<8>(std::move(t8)); }
+ variant &operator=(T9 &&t9) { return assign_value<9>(std::move(t9)); }
+ variant &operator=(T10 &&t10) { return assign_value<10>(std::move(t10)); }
+ variant &operator=(T11 &&t11) { return assign_value<11>(std::move(t11)); }
+ variant &operator=(T12 &&t12) { return assign_value<12>(std::move(t12)); }
+ variant &operator=(T13 &&t13) { return assign_value<13>(std::move(t13)); }
+ variant &operator=(T14 &&t14) { return assign_value<14>(std::move(t14)); }
+ variant &operator=(T15 &&t15) { return assign_value<15>(std::move(t15)); }
+
+#endif
+
+ variant &operator=(T0 const &t0) { return assign_value<0>(t0); }
+ variant &operator=(T1 const &t1) { return assign_value<1>(t1); }
+ variant &operator=(T2 const &t2) { return assign_value<2>(t2); }
+ variant &operator=(T3 const &t3) { return assign_value<3>(t3); }
+ variant &operator=(T4 const &t4) { return assign_value<4>(t4); }
+ variant &operator=(T5 const &t5) { return assign_value<5>(t5); }
+ variant &operator=(T6 const &t6) { return assign_value<6>(t6); }
+ variant &operator=(T7 const &t7) { return assign_value<7>(t7); }
+ variant &operator=(T8 const &t8) { return assign_value<8>(t8); }
+ variant &operator=(T9 const &t9) { return assign_value<9>(t9); }
+ variant &operator=(T10 const &t10) { return assign_value<10>(t10); }
+ variant &operator=(T11 const &t11) { return assign_value<11>(t11); }
+ variant &operator=(T12 const &t12) { return assign_value<12>(t12); }
+ variant &operator=(T13 const &t13) { return assign_value<13>(t13); }
+ variant &operator=(T14 const &t14) { return assign_value<14>(t14); }
+ variant &operator=(T15 const &t15) { return assign_value<15>(t15); }
+
+ std::size_t index() const {
+ return variant_npos_internal() == type_index ? variant_npos
+ : static_cast<std::size_t>(type_index);
+ }
+
+ // 19.7.3.4 Modifiers
+
+#if variant_CPP11_OR_GREATER
+ template <class T, class... Args variant_REQUIRES_T(std::is_constructible<T, Args...>::value)>
+ T &emplace(Args &&... args) {
+ helper_type::destroy(type_index, ptr());
+ type_index = variant_npos_internal();
+ type_index = helper_type::template construct_t<T>(ptr(), std::forward<Args>(args)...);
+
+ return *as<T>();
+ }
+
+ template <class T, class U,
+ class... Args variant_REQUIRES_T(
+ std::is_constructible<T, std::initializer_list<U> &, Args...>::value)>
+ T &emplace(std::initializer_list<U> il, Args &&... args) {
+ helper_type::destroy(type_index, ptr());
+ type_index = variant_npos_internal();
+ type_index = helper_type::template construct_t<T>(ptr(), il, std::forward<Args>(args)...);
+
+ return *as<T>();
+ }
+
+ template <size_t K,
+ class... Args variant_REQUIRES_T(std::is_constructible<type_at_t<K>, Args...>::value)>
+ variant_alternative_t<K, variant> &emplace(Args &&... args) {
+ return this->template emplace<type_at_t<K>>(std::forward<Args>(args)...);
+ }
+
+ template <size_t K, class U,
+ class... Args variant_REQUIRES_T(
+ std::is_constructible<type_at_t<K>, std::initializer_list<U> &, Args...>::value)>
+ variant_alternative_t<K, variant> &emplace(std::initializer_list<U> il, Args &&... args) {
+ return this->template emplace<type_at_t<K>>(il, std::forward<Args>(args)...);
+ }
+
+#endif // variant_CPP11_OR_GREATER
+
+ // 19.7.3.5 Value status
+
+ bool valueless_by_exception() const { return type_index == variant_npos_internal(); }
+
+ // 19.7.3.6 Swap
+
+ void swap(variant &other)
+#if variant_CPP11_OR_GREATER
+ noexcept(
+ std::is_nothrow_move_constructible<T0>::value &&std17::is_nothrow_swappable<
+ T0>::value &&std::is_nothrow_move_constructible<T1>::value
+ &&std17::is_nothrow_swappable<T1>::value &&std::is_nothrow_move_constructible<
+ T2>::value &&std17::is_nothrow_swappable<T2>::value
+ &&std::is_nothrow_move_constructible<T3>::value &&std17::is_nothrow_swappable<
+ T3>::value &&std::is_nothrow_move_constructible<T4>::value
+ &&std17::is_nothrow_swappable<T4>::value &&std::is_nothrow_move_constructible<
+ T5>::value &&std17::is_nothrow_swappable<T5>::value &&std::
+ is_nothrow_move_constructible<T6>::value &&std17::is_nothrow_swappable<
+ T6>::value &&std::is_nothrow_move_constructible<T7>::value &&std17::
+ is_nothrow_swappable<T7>::value &&std::is_nothrow_move_constructible<
+ T8>::value &&std17::is_nothrow_swappable<T8>::value
+ &&std::is_nothrow_move_constructible<
+ T9>::value &&std17::is_nothrow_swappable<T9>::value
+ &&std::is_nothrow_move_constructible<
+ T10>::value &&std17::is_nothrow_swappable<T10>::value
+ &&std::is_nothrow_move_constructible<
+ T11>::value &&std17::is_nothrow_swappable<T11>::value
+ &&std::is_nothrow_move_constructible<T12>::value
+ &&std17::is_nothrow_swappable<T12>::value &&
+ std::is_nothrow_move_constructible<T13>::value
+ &&std17::is_nothrow_swappable<T13>::value
+ &&std::is_nothrow_move_constructible<
+ T14>::value
+ &&std17::is_nothrow_swappable<
+ T14>::value &&std::
+ is_nothrow_move_constructible<
+ T15>::value &&std17::
+ is_nothrow_swappable<
+ T15>::value
+
+ )
+#endif
+ {
+ if (valueless_by_exception() && other.valueless_by_exception()) {
+ // no effect
+ } else if (type_index == other.type_index) {
+ this->swap_value(type_index, other);
+ } else {
+#if variant_CPP11_OR_GREATER
+ variant tmp(std::move(*this));
+ *this = std::move(other);
+ other = std::move(tmp);
+#else
+ variant tmp(*this);
+ *this = other;
+ other = tmp;
+#endif
+ }
+ }
+
+ //
+ // non-standard:
+ //
+
+ template <class T> static variant_constexpr std::size_t index_of() variant_noexcept {
+ return to_size_t(
+ detail::typelist_index_of<variant_types, typename std11::remove_cv<T>::type>::value);
+ }
+
+ template <class T> T &get() {
+#if variant_CONFIG_NO_EXCEPTIONS
+ assert(index_of<T>() == index());
+#else
+ if (index_of<T>() != index()) {
+ throw bad_variant_access();
+ }
+#endif
+ return *as<T>();
+ }
+
+ template <class T> T const &get() const {
+#if variant_CONFIG_NO_EXCEPTIONS
+ assert(index_of<T>() == index());
+#else
+ if (index_of<T>() != index()) {
+ throw bad_variant_access();
+ }
+#endif
+ return *as<const T>();
+ }
+
+ template <std::size_t K> typename variant_alternative<K, variant>::type &get() {
+ return this->template get<typename detail::typelist_type_at<variant_types, K>::type>();
+ }
+
+ template <std::size_t K> typename variant_alternative<K, variant>::type const &get() const {
+ return this->template get<typename detail::typelist_type_at<variant_types, K>::type>();
+ }
+
+private:
+ typedef typename helper_type::type_index_t type_index_t;
+
+ void *ptr() variant_noexcept { return &data; }
+
+ void const *ptr() const variant_noexcept { return &data; }
+
+ template <class U> U *as() { return reinterpret_cast<U *>(ptr()); }
+
+ template <class U> U const *as() const { return reinterpret_cast<U const *>(ptr()); }
+
+ template <class U> static variant_constexpr std::size_t to_size_t(U index) {
+ return static_cast<std::size_t>(index);
+ }
+
+ variant_constexpr type_index_t variant_npos_internal() const variant_noexcept {
+ return static_cast<type_index_t>(-1);
+ }
+
+ variant ©_assign(variant const &other) {
+ if (valueless_by_exception() && other.valueless_by_exception()) {
+ // no effect
+ } else if (!valueless_by_exception() && other.valueless_by_exception()) {
+ helper_type::destroy(type_index, ptr());
+ type_index = variant_npos_internal();
+ } else if (index() == other.index()) {
+ type_index = helper_type::copy_assign(other.type_index, other.ptr(), ptr());
+ } else {
+ helper_type::destroy(type_index, ptr());
+ type_index = variant_npos_internal();
+ type_index = helper_type::copy_construct(other.type_index, other.ptr(), ptr());
+ }
+ return *this;
+ }
+
+#if variant_CPP11_OR_GREATER
+
+ variant &move_assign(variant &&other) {
+ if (valueless_by_exception() && other.valueless_by_exception()) {
+ // no effect
+ } else if (!valueless_by_exception() && other.valueless_by_exception()) {
+ helper_type::destroy(type_index, ptr());
+ type_index = variant_npos_internal();
+ } else if (index() == other.index()) {
+ type_index = helper_type::move_assign(other.type_index, other.ptr(), ptr());
+ } else {
+ helper_type::destroy(type_index, ptr());
+ type_index = variant_npos_internal();
+ type_index = helper_type::move_construct(other.type_index, other.ptr(), ptr());
+ }
+ return *this;
+ }
+
+ template <std::size_t K, class T> variant &assign_value(T &&value) {
+ if (index() == K) {
+ *as<T>() = std::forward<T>(value);
+ } else {
+ helper_type::destroy(type_index, ptr());
+ type_index = variant_npos_internal();
+ new (ptr()) T(std::forward<T>(value));
+ type_index = K;
+ }
+ return *this;
+ }
+
+#endif // variant_CPP11_OR_GREATER
+
+ template <std::size_t K, class T> variant &assign_value(T const &value) {
+ if (index() == K) {
+ *as<T>() = value;
+ } else {
+ helper_type::destroy(type_index, ptr());
+ type_index = variant_npos_internal();
+ new (ptr()) T(value);
+ type_index = K;
+ }
+ return *this;
+ }
+
+ void swap_value(type_index_t index, variant &other) {
+ using std::swap;
+ switch (index) {
+ case 0:
+ swap(this->get<0>(), other.get<0>());
+ break;
+ case 1:
+ swap(this->get<1>(), other.get<1>());
+ break;
+ case 2:
+ swap(this->get<2>(), other.get<2>());
+ break;
+ case 3:
+ swap(this->get<3>(), other.get<3>());
+ break;
+ case 4:
+ swap(this->get<4>(), other.get<4>());
+ break;
+ case 5:
+ swap(this->get<5>(), other.get<5>());
+ break;
+ case 6:
+ swap(this->get<6>(), other.get<6>());
+ break;
+ case 7:
+ swap(this->get<7>(), other.get<7>());
+ break;
+ case 8:
+ swap(this->get<8>(), other.get<8>());
+ break;
+ case 9:
+ swap(this->get<9>(), other.get<9>());
+ break;
+ case 10:
+ swap(this->get<10>(), other.get<10>());
+ break;
+ case 11:
+ swap(this->get<11>(), other.get<11>());
+ break;
+ case 12:
+ swap(this->get<12>(), other.get<12>());
+ break;
+ case 13:
+ swap(this->get<13>(), other.get<13>());
+ break;
+ case 14:
+ swap(this->get<14>(), other.get<14>());
+ break;
+ case 15:
+ swap(this->get<15>(), other.get<15>());
+ break;
+ }
+ }
+
+private:
+ enum { data_size = detail::typelist_max<variant_types>::value };
+
+#if variant_CPP11_OR_GREATER
+
+ enum { data_align = detail::typelist_max_alignof<variant_types>::value };
+
+ using aligned_storage_t = typename std::aligned_storage<data_size, data_align>::type;
+ aligned_storage_t data;
+
+#elif variant_CONFIG_MAX_ALIGN_HACK
+
+ typedef union {
+ unsigned char data[data_size];
+ } aligned_storage_t;
+
+ detail::max_align_t hack;
+ aligned_storage_t data;
+
+#else
+ typedef typename detail::typelist_max<variant_types>::type max_type;
+
+ typedef variant_ALIGN_AS(max_type) align_as_type;
+
+ typedef union {
+ align_as_type data[1 + (data_size - 1) / sizeof(align_as_type)];
+ } aligned_storage_t;
+ aligned_storage_t data;
+
+ // # undef variant_ALIGN_AS
+
+#endif // variant_CONFIG_MAX_ALIGN_HACK
+
+ type_index_t type_index;
+};
+
+// 19.7.5 Value access
+
+template <class T, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+ class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool holds_alternative(
+ variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v)
+ variant_noexcept {
+ return v.index() == variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14,
+ T15>::template index_of<T>();
+}
+
+template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+ class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline R &get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> &v,
+ nonstd_lite_in_place_type_t(R) = nonstd_lite_in_place_type(R)) {
+ return v.template get<R>();
+}
+
+template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+ class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline R const &
+get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+ nonstd_lite_in_place_type_t(R) = nonstd_lite_in_place_type(R)) {
+ return v.template get<R>();
+}
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+ class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+ class T15>
+inline typename variant_alternative<
+ K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::type &
+get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> &v,
+ nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) {
+#if variant_CONFIG_NO_EXCEPTIONS
+ assert(K == v.index());
+#else
+ if (K != v.index()) {
+ throw bad_variant_access();
+ }
+#endif
+ return v.template get<K>();
+}
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+ class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+ class T15>
+inline typename variant_alternative<
+ K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::type const &
+get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+ nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) {
+#if variant_CONFIG_NO_EXCEPTIONS
+ assert(K == v.index());
+#else
+ if (K != v.index()) {
+ throw bad_variant_access();
+ }
+#endif
+ return v.template get<K>();
+}
+
+#if variant_CPP11_OR_GREATER
+
+template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+ class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline R &&get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> &&v,
+ nonstd_lite_in_place_type_t(R) = nonstd_lite_in_place_type(R)) {
+ return std::move(v.template get<R>());
+}
+
+template <class R, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+ class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline R const &&
+get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &&v,
+ nonstd_lite_in_place_type_t(R) = nonstd_lite_in_place_type(R)) {
+ return std::move(v.template get<R>());
+}
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+ class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+ class T15>
+inline typename variant_alternative<
+ K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::type &&
+get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> &&v,
+ nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) {
+#if variant_CONFIG_NO_EXCEPTIONS
+ assert(K == v.index());
+#else
+ if (K != v.index()) {
+ throw bad_variant_access();
+ }
+#endif
+ return std::move(v.template get<K>());
+}
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+ class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+ class T15>
+inline typename variant_alternative<
+ K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::type const &&
+get(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &&v,
+ nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) {
+#if variant_CONFIG_NO_EXCEPTIONS
+ assert(K == v.index());
+#else
+ if (K != v.index()) {
+ throw bad_variant_access();
+ }
+#endif
+ return std::move(v.template get<K>());
+}
+
+#endif // variant_CPP11_OR_GREATER
+
+template <class T, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+ class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline typename std11::add_pointer<T>::type
+get_if(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> *pv,
+ nonstd_lite_in_place_type_t(T) = nonstd_lite_in_place_type(T)) {
+ return (pv->index() == variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14,
+ T15>::template index_of<T>())
+ ? &get<T>(*pv)
+ : variant_nullptr;
+}
+
+template <class T, class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7,
+ class T8, class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline typename std11::add_pointer<const T>::type
+get_if(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const *pv,
+ nonstd_lite_in_place_type_t(T) = nonstd_lite_in_place_type(T)) {
+ return (pv->index() == variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14,
+ T15>::template index_of<T>())
+ ? &get<T>(*pv)
+ : variant_nullptr;
+}
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+ class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+ class T15>
+inline typename std11::add_pointer<typename variant_alternative<
+ K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::type>::type
+get_if(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> *pv,
+ nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) {
+ return (pv->index() == K) ? &get<K>(*pv) : variant_nullptr;
+}
+
+template <std::size_t K, class T0, class T1, class T2, class T3, class T4, class T5, class T6,
+ class T7, class T8, class T9, class T10, class T11, class T12, class T13, class T14,
+ class T15>
+inline typename std11::add_pointer<const typename variant_alternative<
+ K, variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::type>::type
+get_if(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const *pv,
+ nonstd_lite_in_place_index_t(K) = nonstd_lite_in_place_index(K)) {
+ return (pv->index() == K) ? &get<K>(*pv) : variant_nullptr;
+}
+
+// 19.7.10 Specialized algorithms
+
+template <
+ class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14,
+ class T15
+#if variant_CPP11_OR_GREATER
+ variant_REQUIRES_T(
+ std::is_move_constructible<T0>::value &&std17::is_swappable<
+ T0>::value &&std::is_move_constructible<T1>::value &&std17::is_swappable<T1>::value
+ &&std::is_move_constructible<T2>::value &&std17::is_swappable<
+ T2>::value &&std::is_move_constructible<T3>::value &&std17::is_swappable<T3>::
+ value &&std::is_move_constructible<T4>::value &&std17::is_swappable<T4>::value
+ &&std::is_move_constructible<T5>::value &&std17::is_swappable<
+ T5>::value &&std::is_move_constructible<T6>::value
+ &&std17::is_swappable<T6>::value &&std::is_move_constructible<
+ T7>::value &&std17::is_swappable<T7>::value
+ &&std::is_move_constructible<T8>::value &&std17::is_swappable<
+ T8>::value &&std::is_move_constructible<T9>::value
+ &&std17::is_swappable<T9>::value &&std::is_move_constructible<
+ T10>::value &&std17::is_swappable<T10>::value &&std::
+ is_move_constructible<T11>::value &&std17::is_swappable<
+ T11>::value &&std::is_move_constructible<T12>::value
+ &&std17::is_swappable<
+ T12>::value &&std::is_move_constructible<T13>::value
+ &&std17::is_swappable<T13>::value
+ &&std::is_move_constructible<T14>::value
+ &&std17::is_swappable<T14>::value
+ &&std::is_move_constructible<T15>::value
+ &&std17::is_swappable<T15>::value)
+#endif
+ >
+inline void swap(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> &a,
+ variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> &b)
+#if variant_CPP11_OR_GREATER
+ noexcept(noexcept(a.swap(b)))
+#endif
+{
+ a.swap(b);
+}
+
+// 19.7.7 Visitation
+
+// Variant 'visitor' implementation
+
+namespace detail {
+
+template <typename R, typename VT> struct VisitorApplicatorImpl {
+ template <typename Visitor, typename T> static R apply(Visitor const &v, T const &arg) {
+ return v(arg);
+ }
+};
+
+template <typename R, typename VT> struct VisitorApplicatorImpl<R, TX<VT>> {
+ template <typename Visitor, typename T> static R apply(Visitor const &, T) {
+ // prevent default construction of a const reference, see issue #39:
+ std::terminate();
+ }
+};
+
+template <typename R> struct VisitorApplicator;
+
+template <typename R, typename Visitor, typename V1> struct VisitorUnwrapper;
+
+#if variant_CPP11_OR_GREATER
+template <size_t NumVars, typename R, typename Visitor, typename... T>
+#else
+template <size_t NumVars, typename R, typename Visitor, typename T1, typename T2 = S0,
+ typename T3 = S0, typename T4 = S0, typename T5 = S0>
+#endif
+struct TypedVisitorUnwrapper;
+
+template <typename R, typename Visitor, typename T2>
+struct TypedVisitorUnwrapper<2, R, Visitor, T2> {
+ const Visitor &visitor;
+ T2 const &val2;
+
+ TypedVisitorUnwrapper(const Visitor &visitor_, T2 const &val2_)
+ : visitor(visitor_), val2(val2_)
+
+ {}
+
+ template <typename T> R operator()(const T &val1) const { return visitor(val1, val2); }
+};
+
+template <typename R, typename Visitor, typename T2, typename T3>
+struct TypedVisitorUnwrapper<3, R, Visitor, T2, T3> {
+ const Visitor &visitor;
+ T2 const &val2;
+ T3 const &val3;
+
+ TypedVisitorUnwrapper(const Visitor &visitor_, T2 const &val2_, T3 const &val3_)
+ : visitor(visitor_), val2(val2_), val3(val3_)
+
+ {}
+
+ template <typename T> R operator()(const T &val1) const { return visitor(val1, val2, val3); }
+};
+
+template <typename R, typename Visitor, typename T2, typename T3, typename T4>
+struct TypedVisitorUnwrapper<4, R, Visitor, T2, T3, T4> {
+ const Visitor &visitor;
+ T2 const &val2;
+ T3 const &val3;
+ T4 const &val4;
+
+ TypedVisitorUnwrapper(const Visitor &visitor_, T2 const &val2_, T3 const &val3_, T4 const &val4_)
+ : visitor(visitor_), val2(val2_), val3(val3_), val4(val4_)
+
+ {}
+
+ template <typename T> R operator()(const T &val1) const {
+ return visitor(val1, val2, val3, val4);
+ }
+};
+
+template <typename R, typename Visitor, typename T2, typename T3, typename T4, typename T5>
+struct TypedVisitorUnwrapper<5, R, Visitor, T2, T3, T4, T5> {
+ const Visitor &visitor;
+ T2 const &val2;
+ T3 const &val3;
+ T4 const &val4;
+ T5 const &val5;
+
+ TypedVisitorUnwrapper(const Visitor &visitor_, T2 const &val2_, T3 const &val3_, T4 const &val4_,
+ T5 const &val5_)
+ : visitor(visitor_), val2(val2_), val3(val3_), val4(val4_), val5(val5_)
+
+ {}
+
+ template <typename T> R operator()(const T &val1) const {
+ return visitor(val1, val2, val3, val4, val5);
+ }
+};
+
+template <typename R, typename Visitor, typename V2> struct VisitorUnwrapper {
+ const Visitor &visitor;
+ const V2 &r;
+
+ VisitorUnwrapper(const Visitor &visitor_, const V2 &r_) : visitor(visitor_), r(r_) {}
+
+ template <typename T1> R operator()(T1 const &val1) const {
+ typedef TypedVisitorUnwrapper<2, R, Visitor, T1> visitor_type;
+ return VisitorApplicator<R>::apply(visitor_type(visitor, val1), r);
+ }
+
+ template <typename T1, typename T2> R operator()(T1 const &val1, T2 const &val2) const {
+ typedef TypedVisitorUnwrapper<3, R, Visitor, T1, T2> visitor_type;
+ return VisitorApplicator<R>::apply(visitor_type(visitor, val1, val2), r);
+ }
+
+ template <typename T1, typename T2, typename T3>
+ R operator()(T1 const &val1, T2 const &val2, T3 const &val3) const {
+ typedef TypedVisitorUnwrapper<4, R, Visitor, T1, T2, T3> visitor_type;
+ return VisitorApplicator<R>::apply(visitor_type(visitor, val1, val2, val3), r);
+ }
+
+ template <typename T1, typename T2, typename T3, typename T4>
+ R operator()(T1 const &val1, T2 const &val2, T3 const &val3, T4 const &val4) const {
+ typedef TypedVisitorUnwrapper<5, R, Visitor, T1, T2, T3, T4> visitor_type;
+ return VisitorApplicator<R>::apply(visitor_type(visitor, val1, val2, val3, val4), r);
+ }
+
+ template <typename T1, typename T2, typename T3, typename T4, typename T5>
+ R operator()(T1 const &val1, T2 const &val2, T3 const &val3, T4 const &val4,
+ T5 const &val5) const {
+ typedef TypedVisitorUnwrapper<6, R, Visitor, T1, T2, T3, T4, T5> visitor_type;
+ return VisitorApplicator<R>::apply(visitor_type(visitor, val1, val2, val3, val4, val5), r);
+ }
+};
+
+template <typename R> struct VisitorApplicator {
+ template <typename Visitor, typename V1> static R apply(const Visitor &v, const V1 &arg) {
+ switch (arg.index()) {
+ case 0:
+ return apply_visitor<0>(v, arg);
+ case 1:
+ return apply_visitor<1>(v, arg);
+ case 2:
+ return apply_visitor<2>(v, arg);
+ case 3:
+ return apply_visitor<3>(v, arg);
+ case 4:
+ return apply_visitor<4>(v, arg);
+ case 5:
+ return apply_visitor<5>(v, arg);
+ case 6:
+ return apply_visitor<6>(v, arg);
+ case 7:
+ return apply_visitor<7>(v, arg);
+ case 8:
+ return apply_visitor<8>(v, arg);
+ case 9:
+ return apply_visitor<9>(v, arg);
+ case 10:
+ return apply_visitor<10>(v, arg);
+ case 11:
+ return apply_visitor<11>(v, arg);
+ case 12:
+ return apply_visitor<12>(v, arg);
+ case 13:
+ return apply_visitor<13>(v, arg);
+ case 14:
+ return apply_visitor<14>(v, arg);
+ case 15:
+ return apply_visitor<15>(v, arg);
+
+ // prevent default construction of a const reference, see issue #39:
+ default:
+ std::terminate();
+ }
+ }
+
+ template <size_t Idx, typename Visitor, typename V1>
+ static R apply_visitor(const Visitor &v, const V1 &arg) {
+
+#if variant_CPP11_OR_GREATER
+ typedef typename variant_alternative<Idx, typename std::decay<V1>::type>::type value_type;
+#else
+ typedef typename variant_alternative<Idx, V1>::type value_type;
+#endif
+ return VisitorApplicatorImpl<R, value_type>::apply(v, get<Idx>(arg));
+ }
+
+#if variant_CPP11_OR_GREATER
+ template <typename Visitor, typename V1, typename V2, typename... V>
+ static R apply(const Visitor &v, const V1 &arg1, const V2 &arg2, const V... args) {
+ typedef VisitorUnwrapper<R, Visitor, V1> Unwrapper;
+ Unwrapper unwrapper(v, arg1);
+ return apply(unwrapper, arg2, args...);
+ }
+#else
+
+ template <typename Visitor, typename V1, typename V2>
+ static R apply(const Visitor &v, V1 const &arg1, V2 const &arg2) {
+ typedef VisitorUnwrapper<R, Visitor, V1> Unwrapper;
+ Unwrapper unwrapper(v, arg1);
+ return apply(unwrapper, arg2);
+ }
+
+ template <typename Visitor, typename V1, typename V2, typename V3>
+ static R apply(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3) {
+ typedef VisitorUnwrapper<R, Visitor, V1> Unwrapper;
+ Unwrapper unwrapper(v, arg1);
+ return apply(unwrapper, arg2, arg3);
+ }
+
+ template <typename Visitor, typename V1, typename V2, typename V3, typename V4>
+ static R apply(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3, V4 const &arg4) {
+ typedef VisitorUnwrapper<R, Visitor, V1> Unwrapper;
+ Unwrapper unwrapper(v, arg1);
+ return apply(unwrapper, arg2, arg3, arg4);
+ }
+
+ template <typename Visitor, typename V1, typename V2, typename V3, typename V4, typename V5>
+ static R apply(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3, V4 const &arg4,
+ V5 const &arg5) {
+ typedef VisitorUnwrapper<R, Visitor, V1> Unwrapper;
+ Unwrapper unwrapper(v, arg1);
+ return apply(unwrapper, arg2, arg3, arg4, arg5);
+ }
+
+#endif
+};
+
+#if variant_CPP11_OR_GREATER
+template <size_t NumVars, typename Visitor, typename... V> struct VisitorImpl {
+ typedef decltype(
+ std::declval<Visitor>()(get<0>(static_cast<const V &>(std::declval<V>()))...)) result_type;
+ typedef VisitorApplicator<result_type> applicator_type;
+};
+#endif
+} // namespace detail
+
+#if variant_CPP11_OR_GREATER
+// No perfect forwarding here in order to simplify code
+template <typename Visitor, typename... V>
+inline auto visit(Visitor const &v, V const &... vars) ->
+ typename detail::VisitorImpl<sizeof...(V), Visitor, V...>::result_type {
+ typedef detail::VisitorImpl<sizeof...(V), Visitor, V...> impl_type;
+ return impl_type::applicator_type::apply(v, vars...);
+}
+#else
+
+template <typename R, typename Visitor, typename V1>
+inline R visit(const Visitor &v, V1 const &arg1) {
+ return detail::VisitorApplicator<R>::apply(v, arg1);
+}
+
+template <typename R, typename Visitor, typename V1, typename V2>
+inline R visit(const Visitor &v, V1 const &arg1, V2 const &arg2) {
+ return detail::VisitorApplicator<R>::apply(v, arg1, arg2);
+}
+
+template <typename R, typename Visitor, typename V1, typename V2, typename V3>
+inline R visit(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3) {
+ return detail::VisitorApplicator<R>::apply(v, arg1, arg2, arg3);
+}
+
+template <typename R, typename Visitor, typename V1, typename V2, typename V3, typename V4>
+inline R visit(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3, V4 const &arg4) {
+ return detail::VisitorApplicator<R>::apply(v, arg1, arg2, arg3, arg4);
+}
+
+template <typename R, typename Visitor, typename V1, typename V2, typename V3, typename V4,
+ typename V5>
+inline R visit(const Visitor &v, V1 const &arg1, V2 const &arg2, V3 const &arg3, V4 const &arg4,
+ V5 const &arg5) {
+ return detail::VisitorApplicator<R>::apply(v, arg1, arg2, arg3, arg4, arg5);
+}
+
+#endif
+
+// 19.7.6 Relational operators
+
+namespace detail {
+
+template <class Variant> struct Comparator {
+ static inline bool equal(Variant const &v, Variant const &w) {
+ switch (v.index()) {
+ case 0:
+ return get<0>(v) == get<0>(w);
+ case 1:
+ return get<1>(v) == get<1>(w);
+ case 2:
+ return get<2>(v) == get<2>(w);
+ case 3:
+ return get<3>(v) == get<3>(w);
+ case 4:
+ return get<4>(v) == get<4>(w);
+ case 5:
+ return get<5>(v) == get<5>(w);
+ case 6:
+ return get<6>(v) == get<6>(w);
+ case 7:
+ return get<7>(v) == get<7>(w);
+ case 8:
+ return get<8>(v) == get<8>(w);
+ case 9:
+ return get<9>(v) == get<9>(w);
+ case 10:
+ return get<10>(v) == get<10>(w);
+ case 11:
+ return get<11>(v) == get<11>(w);
+ case 12:
+ return get<12>(v) == get<12>(w);
+ case 13:
+ return get<13>(v) == get<13>(w);
+ case 14:
+ return get<14>(v) == get<14>(w);
+ case 15:
+ return get<15>(v) == get<15>(w);
+
+ default:
+ return false;
+ }
+ }
+
+ static inline bool less_than(Variant const &v, Variant const &w) {
+ switch (v.index()) {
+ case 0:
+ return get<0>(v) < get<0>(w);
+ case 1:
+ return get<1>(v) < get<1>(w);
+ case 2:
+ return get<2>(v) < get<2>(w);
+ case 3:
+ return get<3>(v) < get<3>(w);
+ case 4:
+ return get<4>(v) < get<4>(w);
+ case 5:
+ return get<5>(v) < get<5>(w);
+ case 6:
+ return get<6>(v) < get<6>(w);
+ case 7:
+ return get<7>(v) < get<7>(w);
+ case 8:
+ return get<8>(v) < get<8>(w);
+ case 9:
+ return get<9>(v) < get<9>(w);
+ case 10:
+ return get<10>(v) < get<10>(w);
+ case 11:
+ return get<11>(v) < get<11>(w);
+ case 12:
+ return get<12>(v) < get<12>(w);
+ case 13:
+ return get<13>(v) < get<13>(w);
+ case 14:
+ return get<14>(v) < get<14>(w);
+ case 15:
+ return get<15>(v) < get<15>(w);
+
+ default:
+ return false;
+ }
+ }
+};
+
+} // namespace detail
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool
+operator==(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+ variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &w) {
+ if (v.index() != w.index())
+ return false;
+ else if (v.valueless_by_exception())
+ return true;
+ else
+ return detail::Comparator<
+ variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>>::equal(v, w);
+}
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool
+operator!=(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+ variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &w) {
+ return !(v == w);
+}
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool
+operator<(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+ variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &w) {
+ if (w.valueless_by_exception())
+ return false;
+ else if (v.valueless_by_exception())
+ return true;
+ else if (v.index() < w.index())
+ return true;
+ else if (v.index() > w.index())
+ return false;
+ else
+ return detail::Comparator<variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13,
+ T14, T15>>::less_than(v, w);
+}
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool
+operator>(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+ variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &w) {
+ return w < v;
+}
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool
+operator<=(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+ variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &w) {
+ return !(v > w);
+}
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+inline bool
+operator>=(variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &v,
+ variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> const &w) {
+ return !(v < w);
+}
+
+} // namespace variants
+
+using namespace variants;
+
+} // namespace nonstd
+
+#if variant_CPP11_OR_GREATER
+
+// 19.7.12 Hash support
+
+namespace std {
+
+template <> struct hash<nonstd::monostate> {
+ std::size_t operator()(nonstd::monostate) const variant_noexcept { return 42; }
+};
+
+template <class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8,
+ class T9, class T10, class T11, class T12, class T13, class T14, class T15>
+struct hash<nonstd::variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>> {
+ std::size_t operator()(nonstd::variant<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13,
+ T14, T15> const &v) const variant_noexcept {
+ namespace nvd = nonstd::variants::detail;
+
+ switch (v.index()) {
+ case 0:
+ return nvd::hash(0) ^ nvd::hash(get<0>(v));
+ case 1:
+ return nvd::hash(1) ^ nvd::hash(get<1>(v));
+ case 2:
+ return nvd::hash(2) ^ nvd::hash(get<2>(v));
+ case 3:
+ return nvd::hash(3) ^ nvd::hash(get<3>(v));
+ case 4:
+ return nvd::hash(4) ^ nvd::hash(get<4>(v));
+ case 5:
+ return nvd::hash(5) ^ nvd::hash(get<5>(v));
+ case 6:
+ return nvd::hash(6) ^ nvd::hash(get<6>(v));
+ case 7:
+ return nvd::hash(7) ^ nvd::hash(get<7>(v));
+ case 8:
+ return nvd::hash(8) ^ nvd::hash(get<8>(v));
+ case 9:
+ return nvd::hash(9) ^ nvd::hash(get<9>(v));
+ case 10:
+ return nvd::hash(10) ^ nvd::hash(get<10>(v));
+ case 11:
+ return nvd::hash(11) ^ nvd::hash(get<11>(v));
+ case 12:
+ return nvd::hash(12) ^ nvd::hash(get<12>(v));
+ case 13:
+ return nvd::hash(13) ^ nvd::hash(get<13>(v));
+ case 14:
+ return nvd::hash(14) ^ nvd::hash(get<14>(v));
+ case 15:
+ return nvd::hash(15) ^ nvd::hash(get<15>(v));
+
+ default:
+ return 0;
+ }
+ }
+};
+
+} // namespace std
+
+#endif // variant_CPP11_OR_GREATER
+
+#if variant_BETWEEN(variant_COMPILER_MSVC_VER, 1300, 1900)
+#pragma warning(pop)
+#endif
+
+#endif // variant_USES_STD_VARIANT
+
+#endif // NONSTD_VARIANT_LITE_HPP
+//
+// Copyright (c) 2014-2018 Martin Moene
+//
+// https://github.com/martinmoene/optional-lite
+//
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+#pragma once
+
+#ifndef NONSTD_OPTIONAL_LITE_HPP
+#define NONSTD_OPTIONAL_LITE_HPP
+
+#define optional_lite_MAJOR 3
+#define optional_lite_MINOR 2
+#define optional_lite_PATCH 0
+
+#define optional_lite_VERSION \
+ optional_STRINGIFY(optional_lite_MAJOR) "." optional_STRINGIFY( \
+ optional_lite_MINOR) "." optional_STRINGIFY(optional_lite_PATCH)
+
+#define optional_STRINGIFY(x) optional_STRINGIFY_(x)
+#define optional_STRINGIFY_(x) #x
+
+// optional-lite configuration:
+
+#define optional_OPTIONAL_DEFAULT 0
+#define optional_OPTIONAL_NONSTD 1
+#define optional_OPTIONAL_STD 2
+
+#if !defined(optional_CONFIG_SELECT_OPTIONAL)
+#define optional_CONFIG_SELECT_OPTIONAL \
+ (optional_HAVE_STD_OPTIONAL ? optional_OPTIONAL_STD : optional_OPTIONAL_NONSTD)
+#endif
+
+// Control presence of exception handling (try and auto discover):
+
+#ifndef optional_CONFIG_NO_EXCEPTIONS
+#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
+#define optional_CONFIG_NO_EXCEPTIONS 0
+#else
+#define optional_CONFIG_NO_EXCEPTIONS 1
+#endif
+#endif
+
+// C++ language version detection (C++20 is speculative):
+// Note: VC14.0/1900 (VS2015) lacks too much from C++14.
+
+#ifndef optional_CPLUSPLUS
+#if defined(_MSVC_LANG) && !defined(__clang__)
+#define optional_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG)
+#else
+#define optional_CPLUSPLUS __cplusplus
+#endif
+#endif
+
+#define optional_CPP98_OR_GREATER (optional_CPLUSPLUS >= 199711L)
+#define optional_CPP11_OR_GREATER (optional_CPLUSPLUS >= 201103L)
+#define optional_CPP11_OR_GREATER_ (optional_CPLUSPLUS >= 201103L)
+#define optional_CPP14_OR_GREATER (optional_CPLUSPLUS >= 201402L)
+#define optional_CPP17_OR_GREATER (optional_CPLUSPLUS >= 201703L)
+#define optional_CPP20_OR_GREATER (optional_CPLUSPLUS >= 202000L)
+
+// C++ language version (represent 98 as 3):
+
+#define optional_CPLUSPLUS_V \
+ (optional_CPLUSPLUS / 100 - (optional_CPLUSPLUS > 200000 ? 2000 : 1994))
+
+// Use C++17 std::optional if available and requested:
+
+#if optional_CPP17_OR_GREATER && defined(__has_include)
+#if __has_include(<optional> )
+#define optional_HAVE_STD_OPTIONAL 1
+#else
+#define optional_HAVE_STD_OPTIONAL 0
+#endif
+#else
+#define optional_HAVE_STD_OPTIONAL 0
+#endif
+
+#define optional_USES_STD_OPTIONAL \
+ ((optional_CONFIG_SELECT_OPTIONAL == optional_OPTIONAL_STD) || \
+ ((optional_CONFIG_SELECT_OPTIONAL == optional_OPTIONAL_DEFAULT) && optional_HAVE_STD_OPTIONAL))
+
+//
+// in_place: code duplicated in any-lite, expected-lite, optional-lite, value-ptr-lite,
+// variant-lite:
+//
+
+#ifndef nonstd_lite_HAVE_IN_PLACE_TYPES
+#define nonstd_lite_HAVE_IN_PLACE_TYPES 1
+
+// C++17 std::in_place in <utility>:
+
+#if optional_CPP17_OR_GREATER
+
+#include <utility>
+
+namespace nonstd {
+
+using std::in_place;
+using std::in_place_index;
+using std::in_place_index_t;
+using std::in_place_t;
+using std::in_place_type;
+using std::in_place_type_t;
+
+#define nonstd_lite_in_place_t(T) std::in_place_t
+#define nonstd_lite_in_place_type_t(T) std::in_place_type_t<T>
+#define nonstd_lite_in_place_index_t(K) std::in_place_index_t<K>
+
+#define nonstd_lite_in_place(T) \
+ std::in_place_t {}
+#define nonstd_lite_in_place_type(T) \
+ std::in_place_type_t<T> {}
+#define nonstd_lite_in_place_index(K) \
+ std::in_place_index_t<K> {}
+
+} // namespace nonstd
+
+#else // optional_CPP17_OR_GREATER
+
+#include <cstddef>
+
+namespace nonstd {
+namespace detail {
+
+template <class T> struct in_place_type_tag {};
+
+template <std::size_t K> struct in_place_index_tag {};
+
+} // namespace detail
+
+struct in_place_t {};
+
+template <class T>
+inline in_place_t
+in_place(detail::in_place_type_tag<T> /*unused*/ = detail::in_place_type_tag<T>()) {
+ return in_place_t();
+}
+
+template <std::size_t K>
+inline in_place_t
+in_place(detail::in_place_index_tag<K> /*unused*/ = detail::in_place_index_tag<K>()) {
+ return in_place_t();
+}
+
+template <class T>
+inline in_place_t
+in_place_type(detail::in_place_type_tag<T> /*unused*/ = detail::in_place_type_tag<T>()) {
+ return in_place_t();
+}
+
+template <std::size_t K>
+inline in_place_t
+in_place_index(detail::in_place_index_tag<K> /*unused*/ = detail::in_place_index_tag<K>()) {
+ return in_place_t();
+}
+
+// mimic templated typedef:
+
+#define nonstd_lite_in_place_t(T) nonstd::in_place_t (&)(nonstd::detail::in_place_type_tag<T>)
+#define nonstd_lite_in_place_type_t(T) nonstd::in_place_t (&)(nonstd::detail::in_place_type_tag<T>)
+#define nonstd_lite_in_place_index_t(K) \
+ nonstd::in_place_t (&)(nonstd::detail::in_place_index_tag<K>)
+
+#define nonstd_lite_in_place(T) nonstd::in_place_type<T>
+#define nonstd_lite_in_place_type(T) nonstd::in_place_type<T>
+#define nonstd_lite_in_place_index(K) nonstd::in_place_index<K>
+
+} // namespace nonstd
+
+#endif // optional_CPP17_OR_GREATER
+#endif // nonstd_lite_HAVE_IN_PLACE_TYPES
+
+//
+// Using std::optional:
+//
+
+#if optional_USES_STD_OPTIONAL
+
+#include <optional>
+
+namespace nonstd {
+
+using std::bad_optional_access;
+using std::hash;
+using std::optional;
+
+using std::nullopt;
+using std::nullopt_t;
+
+using std::operator==;
+using std::operator!=;
+using std::operator<;
+using std::operator<=;
+using std::operator>;
+using std::operator>=;
+using std::make_optional;
+using std::swap;
+} // namespace nonstd
+
+#else // optional_USES_STD_OPTIONAL
+
+#include <cassert>
+#include <utility>
+
+// optional-lite alignment configuration:
+
+#ifndef optional_CONFIG_MAX_ALIGN_HACK
+#define optional_CONFIG_MAX_ALIGN_HACK 0
+#endif
+
+#ifndef optional_CONFIG_ALIGN_AS
+// no default, used in #if defined()
+#endif
+
+#ifndef optional_CONFIG_ALIGN_AS_FALLBACK
+#define optional_CONFIG_ALIGN_AS_FALLBACK double
+#endif
+
+// Compiler warning suppression:
+
+#if defined(__clang__)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wundef"
+#elif defined(__GNUC__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wundef"
+#elif defined(_MSC_VER)
+#pragma warning(push)
+#endif
+
+// half-open range [lo..hi):
+#define optional_BETWEEN(v, lo, hi) ((lo) <= (v) && (v) < (hi))
+
+// Compiler versions:
+//
+// MSVC++ 6.0 _MSC_VER == 1200 optional_COMPILER_MSVC_VERSION == 60 (Visual Studio 6.0)
+// MSVC++ 7.0 _MSC_VER == 1300 optional_COMPILER_MSVC_VERSION == 70 (Visual Studio .NET 2002)
+// MSVC++ 7.1 _MSC_VER == 1310 optional_COMPILER_MSVC_VERSION == 71 (Visual Studio .NET 2003)
+// MSVC++ 8.0 _MSC_VER == 1400 optional_COMPILER_MSVC_VERSION == 80 (Visual Studio 2005)
+// MSVC++ 9.0 _MSC_VER == 1500 optional_COMPILER_MSVC_VERSION == 90 (Visual Studio 2008)
+// MSVC++ 10.0 _MSC_VER == 1600 optional_COMPILER_MSVC_VERSION == 100 (Visual Studio 2010)
+// MSVC++ 11.0 _MSC_VER == 1700 optional_COMPILER_MSVC_VERSION == 110 (Visual Studio 2012)
+// MSVC++ 12.0 _MSC_VER == 1800 optional_COMPILER_MSVC_VERSION == 120 (Visual Studio 2013)
+// MSVC++ 14.0 _MSC_VER == 1900 optional_COMPILER_MSVC_VERSION == 140 (Visual Studio 2015)
+// MSVC++ 14.1 _MSC_VER >= 1910 optional_COMPILER_MSVC_VERSION == 141 (Visual Studio 2017)
+// MSVC++ 14.2 _MSC_VER >= 1920 optional_COMPILER_MSVC_VERSION == 142 (Visual Studio 2019)
+
+#if defined(_MSC_VER) && !defined(__clang__)
+#define optional_COMPILER_MSVC_VER (_MSC_VER)
+#define optional_COMPILER_MSVC_VERSION (_MSC_VER / 10 - 10 * (5 + (_MSC_VER < 1900)))
+#else
+#define optional_COMPILER_MSVC_VER 0
+#define optional_COMPILER_MSVC_VERSION 0
+#endif
+
+#define optional_COMPILER_VERSION(major, minor, patch) (10 * (10 * (major) + (minor)) + (patch))
+
+#if defined(__GNUC__) && !defined(__clang__)
+#define optional_COMPILER_GNUC_VERSION \
+ optional_COMPILER_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)
+#else
+#define optional_COMPILER_GNUC_VERSION 0
+#endif
+
+#if defined(__clang__)
+#define optional_COMPILER_CLANG_VERSION \
+ optional_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__)
+#else
+#define optional_COMPILER_CLANG_VERSION 0
+#endif
+
+#if optional_BETWEEN(optional_COMPILER_MSVC_VERSION, 70, 140)
+#pragma warning(disable : 4345) // initialization behavior changed
+#endif
+
+#if optional_BETWEEN(optional_COMPILER_MSVC_VERSION, 70, 150)
+#pragma warning(disable : 4814) // in C++14 'constexpr' will not imply 'const'
+#endif
+
+// Presence of language and library features:
+
+#define optional_HAVE(FEATURE) (optional_HAVE_##FEATURE)
+
+#ifdef _HAS_CPP0X
+#define optional_HAS_CPP0X _HAS_CPP0X
+#else
+#define optional_HAS_CPP0X 0
+#endif
+
+// Unless defined otherwise below, consider VC14 as C++11 for optional-lite:
+
+#if optional_COMPILER_MSVC_VER >= 1900
+#undef optional_CPP11_OR_GREATER
+#define optional_CPP11_OR_GREATER 1
+#endif
+
+#define optional_CPP11_90 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1500)
+#define optional_CPP11_100 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1600)
+#define optional_CPP11_110 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1700)
+#define optional_CPP11_120 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1800)
+#define optional_CPP11_140 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1900)
+#define optional_CPP11_141 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1910)
+
+#define optional_CPP11_140_490 \
+ ((optional_CPP11_OR_GREATER_ && optional_COMPILER_GNUC_VERSION >= 490) || \
+ (optional_COMPILER_MSVC_VER >= 1910))
+
+#define optional_CPP14_000 (optional_CPP14_OR_GREATER)
+#define optional_CPP17_000 (optional_CPP17_OR_GREATER)
+
+// Presence of C++11 language features:
+
+#define optional_HAVE_CONSTEXPR_11 optional_CPP11_140
+#define optional_HAVE_IS_DEFAULT optional_CPP11_140
+#define optional_HAVE_NOEXCEPT optional_CPP11_140
+#define optional_HAVE_NULLPTR optional_CPP11_100
+#define optional_HAVE_REF_QUALIFIER optional_CPP11_140_490
+#define optional_HAVE_INITIALIZER_LIST optional_CPP11_140
+
+// Presence of C++14 language features:
+
+#define optional_HAVE_CONSTEXPR_14 optional_CPP14_000
+
+// Presence of C++17 language features:
+
+#define optional_HAVE_NODISCARD optional_CPP17_000
+
+// Presence of C++ library features:
+
+#define optional_HAVE_CONDITIONAL optional_CPP11_120
+#define optional_HAVE_REMOVE_CV optional_CPP11_120
+#define optional_HAVE_TYPE_TRAITS optional_CPP11_90
+
+#define optional_HAVE_TR1_TYPE_TRAITS (!!optional_COMPILER_GNUC_VERSION)
+#define optional_HAVE_TR1_ADD_POINTER (!!optional_COMPILER_GNUC_VERSION)
+
+// C++ feature usage:
+
+#if optional_HAVE(CONSTEXPR_11)
+#define optional_constexpr constexpr
+#else
+#define optional_constexpr /*constexpr*/
+#endif
+
+#if optional_HAVE(IS_DEFAULT)
+#define optional_is_default = default;
+#else
+#define optional_is_default \
+ {}
+#endif
+
+#if optional_HAVE(CONSTEXPR_14)
+#define optional_constexpr14 constexpr
+#else
+#define optional_constexpr14 /*constexpr*/
+#endif
+
+#if optional_HAVE(NODISCARD)
+#define optional_nodiscard [[nodiscard]]
+#else
+#define optional_nodiscard /*[[nodiscard]]*/
+#endif
+
+#if optional_HAVE(NOEXCEPT)
+#define optional_noexcept noexcept
+#else
+#define optional_noexcept /*noexcept*/
+#endif
+
+#if optional_HAVE(NULLPTR)
+#define optional_nullptr nullptr
+#else
+#define optional_nullptr NULL
+#endif
+
+#if optional_HAVE(REF_QUALIFIER)
+// NOLINTNEXTLINE( bugprone-macro-parentheses )
+#define optional_ref_qual &
+#define optional_refref_qual &&
+#else
+#define optional_ref_qual /*&*/
+#define optional_refref_qual /*&&*/
+#endif
+
+// additional includes:
+
+#if optional_CONFIG_NO_EXCEPTIONS
+// already included: <cassert>
+#else
+#include <stdexcept>
+#endif
+
+#if optional_CPP11_OR_GREATER
+#include <functional>
+#endif
+
+#if optional_HAVE(INITIALIZER_LIST)
+#include <initializer_list>
+#endif
+
+#if optional_HAVE(TYPE_TRAITS)
+#include <type_traits>
+#elif optional_HAVE(TR1_TYPE_TRAITS)
+#include <tr1/type_traits>
+#endif
+
+// Method enabling
+
+#if optional_CPP11_OR_GREATER
+
+#define optional_REQUIRES_0(...) \
+ template <bool B = (__VA_ARGS__), typename std::enable_if<B, int>::type = 0>
+
+#define optional_REQUIRES_T(...) , typename std::enable_if<(__VA_ARGS__), int>::type = 0
+
+#define optional_REQUIRES_R(R, ...) typename std::enable_if<(__VA_ARGS__), R>::type
+
+#define optional_REQUIRES_A(...) , typename std::enable_if<(__VA_ARGS__), void *>::type = nullptr
+
+#endif
+
+//
+// optional:
+//
+
+namespace nonstd {
+namespace optional_lite {
+
+namespace std11 {
+
+#if optional_CPP11_OR_GREATER
+using std::move;
+#else
+template <typename T> T &move(T &t) { return t; }
+#endif
+
+#if optional_HAVE(CONDITIONAL)
+using std::conditional;
+#else
+template <bool B, typename T, typename F> struct conditional { typedef T type; };
+template <typename T, typename F> struct conditional<false, T, F> { typedef F type; };
+#endif // optional_HAVE_CONDITIONAL
+
+// gcc < 5:
+#if optional_CPP11_OR_GREATER
+#if optional_BETWEEN(optional_COMPILER_GNUC_VERSION, 1, 500)
+template <typename T> struct is_trivially_copy_constructible : std::true_type {};
+template <typename T> struct is_trivially_move_constructible : std::true_type {};
+#else
+using std::is_trivially_copy_constructible;
+using std::is_trivially_move_constructible;
+#endif
+#endif
+} // namespace std11
+
+#if optional_CPP11_OR_GREATER
+
+/// type traits C++17:
+
+namespace std17 {
+
+#if optional_CPP17_OR_GREATER
+
+using std::is_nothrow_swappable;
+using std::is_swappable;
+
+#elif optional_CPP11_OR_GREATER
+
+namespace detail {
+
+using std::swap;
+
+struct is_swappable {
+ template <typename T, typename = decltype(swap(std::declval<T &>(), std::declval<T &>()))>
+ static std::true_type test(int /*unused*/);
+
+ template <typename> static std::false_type test(...);
+};
+
+struct is_nothrow_swappable {
+ // wrap noexcept(expr) in separate function as work-around for VC140 (VS2015):
+
+ template <typename T> static constexpr bool satisfies() {
+ return noexcept(swap(std::declval<T &>(), std::declval<T &>()));
+ }
+
+ template <typename T>
+ static auto test(int /*unused*/) -> std::integral_constant<bool, satisfies<T>()> {}
+
+ template <typename> static auto test(...) -> std::false_type;
+};
+
+} // namespace detail
+
+// is [nothow] swappable:
+
+template <typename T> struct is_swappable : decltype(detail::is_swappable::test<T>(0)) {};
+
+template <typename T>
+struct is_nothrow_swappable : decltype(detail::is_nothrow_swappable::test<T>(0)) {};
+
+#endif // optional_CPP17_OR_GREATER
+
+} // namespace std17
+
+/// type traits C++20:
+
+namespace std20 {
+
+template <typename T> struct remove_cvref {
+ typedef typename std::remove_cv<typename std::remove_reference<T>::type>::type type;
+};
+
+} // namespace std20
+
+#endif // optional_CPP11_OR_GREATER
+
+/// class optional
+
+template <typename T> class optional;
+
+namespace detail {
+
+// C++11 emulation:
+
+struct nulltype {};
+
+template <typename Head, typename Tail> struct typelist {
+ typedef Head head;
+ typedef Tail tail;
+};
+
+#if optional_CONFIG_MAX_ALIGN_HACK
+
+// Max align, use most restricted type for alignment:
+
+#define optional_UNIQUE(name) optional_UNIQUE2(name, __LINE__)
+#define optional_UNIQUE2(name, line) optional_UNIQUE3(name, line)
+#define optional_UNIQUE3(name, line) name##line
+
+#define optional_ALIGN_TYPE(type) \
+ type optional_UNIQUE(_t); \
+ struct_t<type> optional_UNIQUE(_st)
+
+template <typename T> struct struct_t { T _; };
+
+union max_align_t {
+ optional_ALIGN_TYPE(char);
+ optional_ALIGN_TYPE(short int);
+ optional_ALIGN_TYPE(int);
+ optional_ALIGN_TYPE(long int);
+ optional_ALIGN_TYPE(float);
+ optional_ALIGN_TYPE(double);
+ optional_ALIGN_TYPE(long double);
+ optional_ALIGN_TYPE(char *);
+ optional_ALIGN_TYPE(short int *);
+ optional_ALIGN_TYPE(int *);
+ optional_ALIGN_TYPE(long int *);
+ optional_ALIGN_TYPE(float *);
+ optional_ALIGN_TYPE(double *);
+ optional_ALIGN_TYPE(long double *);
+ optional_ALIGN_TYPE(void *);
+
+#ifdef HAVE_LONG_LONG
+ optional_ALIGN_TYPE(long long);
+#endif
+
+ struct Unknown;
+
+ Unknown (*optional_UNIQUE(_))(Unknown);
+ Unknown *Unknown::*optional_UNIQUE(_);
+ Unknown (Unknown::*optional_UNIQUE(_))(Unknown);
+
+ struct_t<Unknown (*)(Unknown)> optional_UNIQUE(_);
+ struct_t<Unknown * Unknown::*> optional_UNIQUE(_);
+ struct_t<Unknown (Unknown::*)(Unknown)> optional_UNIQUE(_);
+};
+
+#undef optional_UNIQUE
+#undef optional_UNIQUE2
+#undef optional_UNIQUE3
+
+#undef optional_ALIGN_TYPE
+
+#elif defined(optional_CONFIG_ALIGN_AS) // optional_CONFIG_MAX_ALIGN_HACK
+
+// Use user-specified type for alignment:
+
+#define optional_ALIGN_AS(unused) optional_CONFIG_ALIGN_AS
+
+#else // optional_CONFIG_MAX_ALIGN_HACK
+
+// Determine POD type to use for alignment:
+
+#define optional_ALIGN_AS(to_align) \
+ typename type_of_size<alignment_types, alignment_of<to_align>::value>::type
+
+template <typename T> struct alignment_of;
+
+template <typename T> struct alignment_of_hack {
+ char c;
+ T t;
+ alignment_of_hack();
+};
+
+template <size_t A, size_t S> struct alignment_logic {
+ enum { value = A < S ? A : S };
+};
+
+template <typename T> struct alignment_of {
+ enum { value = alignment_logic<sizeof(alignment_of_hack<T>) - sizeof(T), sizeof(T)>::value };
+};
+
+template <typename List, size_t N> struct type_of_size {
+ typedef
+ typename std11::conditional<N == sizeof(typename List::head), typename List::head,
+ typename type_of_size<typename List::tail, N>::type>::type type;
+};
+
+template <size_t N> struct type_of_size<nulltype, N> {
+ typedef optional_CONFIG_ALIGN_AS_FALLBACK type;
+};
+
+template <typename T> struct struct_t { T _; };
+
+#define optional_ALIGN_TYPE(type) typelist < type, typelist < struct_t<type>
+
+struct Unknown;
+
+typedef optional_ALIGN_TYPE(char), optional_ALIGN_TYPE(short), optional_ALIGN_TYPE(int),
+ optional_ALIGN_TYPE(long), optional_ALIGN_TYPE(float), optional_ALIGN_TYPE(double),
+ optional_ALIGN_TYPE(long double),
+
+ optional_ALIGN_TYPE(char *), optional_ALIGN_TYPE(short *), optional_ALIGN_TYPE(int *),
+ optional_ALIGN_TYPE(long *), optional_ALIGN_TYPE(float *), optional_ALIGN_TYPE(double *),
+ optional_ALIGN_TYPE(long double *),
+
+ optional_ALIGN_TYPE(Unknown (*)(Unknown)), optional_ALIGN_TYPE(Unknown *Unknown::*),
+ optional_ALIGN_TYPE(Unknown (Unknown::*)(Unknown)),
+
+ nulltype >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> alignment_types;
+
+#undef optional_ALIGN_TYPE
+
+#endif // optional_CONFIG_MAX_ALIGN_HACK
+
+/// C++03 constructed union to hold value.
+
+template <typename T> union storage_t {
+ // private:
+ // template< typename > friend class optional;
+
+ typedef T value_type;
+
+ storage_t() optional_is_default
+
+ explicit storage_t(value_type const &v) {
+ construct_value(v);
+ }
+
+ void construct_value(value_type const &v) { ::new (value_ptr()) value_type(v); }
+
+#if optional_CPP11_OR_GREATER
+
+ explicit storage_t(value_type &&v) { construct_value(std::move(v)); }
+
+ void construct_value(value_type &&v) { ::new (value_ptr()) value_type(std::move(v)); }
+
+ template <class... Args> void emplace(Args &&... args) {
+ ::new (value_ptr()) value_type(std::forward<Args>(args)...);
+ }
+
+ template <class U, class... Args> void emplace(std::initializer_list<U> il, Args &&... args) {
+ ::new (value_ptr()) value_type(il, std::forward<Args>(args)...);
+ }
+
+#endif
+
+ void destruct_value() { value_ptr()->~T(); }
+
+ optional_nodiscard value_type const *value_ptr() const { return as<value_type>(); }
+
+ value_type *value_ptr() { return as<value_type>(); }
+
+ optional_nodiscard value_type const &value() const optional_ref_qual { return *value_ptr(); }
+
+ value_type &value() optional_ref_qual { return *value_ptr(); }
+
+#if optional_HAVE(REF_QUALIFIER)
+
+ optional_nodiscard value_type const &&value() const optional_refref_qual {
+ return std::move(value());
+ }
+
+ value_type &&value() optional_refref_qual { return std::move(value()); }
+
+#endif
+
+#if optional_CPP11_OR_GREATER
+
+ using aligned_storage_t =
+ typename std::aligned_storage<sizeof(value_type), alignof(value_type)>::type;
+ aligned_storage_t data;
+
+#elif optional_CONFIG_MAX_ALIGN_HACK
+
+ typedef struct {
+ unsigned char data[sizeof(value_type)];
+ } aligned_storage_t;
+
+ max_align_t hack;
+ aligned_storage_t data;
+
+#else
+ typedef optional_ALIGN_AS(value_type) align_as_type;
+
+ typedef struct {
+ align_as_type data[1 + (sizeof(value_type) - 1) / sizeof(align_as_type)];
+ } aligned_storage_t;
+ aligned_storage_t data;
+
+#undef optional_ALIGN_AS
+
+#endif // optional_CONFIG_MAX_ALIGN_HACK
+
+ optional_nodiscard void *ptr() optional_noexcept { return &data; }
+
+ optional_nodiscard void const *ptr() const optional_noexcept { return &data; }
+
+ template <typename U> optional_nodiscard U *as() { return reinterpret_cast<U *>(ptr()); }
+
+ template <typename U> optional_nodiscard U const *as() const {
+ return reinterpret_cast<U const *>(ptr());
+ }
+};
+
+} // namespace detail
+
+/// disengaged state tag
+
+struct nullopt_t {
+ struct init {};
+ explicit optional_constexpr nullopt_t(init /*unused*/) optional_noexcept {}
+};
+
+#if optional_HAVE(CONSTEXPR_11)
+constexpr nullopt_t nullopt{nullopt_t::init{}};
+#else
+// extra parenthesis to prevent the most vexing parse:
+const nullopt_t nullopt((nullopt_t::init()));
+#endif
+
+/// optional access error
+
+#if !optional_CONFIG_NO_EXCEPTIONS
+
+class bad_optional_access : public std::logic_error {
+public:
+ explicit bad_optional_access() : logic_error("bad optional access") {}
+};
+
+#endif // optional_CONFIG_NO_EXCEPTIONS
+
+/// optional
+
+template <typename T> class optional {
+private:
+ template <typename> friend class optional;
+
+ typedef void (optional::*safe_bool)() const;
+
+public:
+ typedef T value_type;
+
+ // x.x.3.1, constructors
+
+ // 1a - default construct
+ optional_constexpr optional() optional_noexcept : has_value_(false), contained() {}
+
+ // 1b - construct explicitly empty
+ // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions )
+ optional_constexpr optional(nullopt_t /*unused*/) optional_noexcept : has_value_(false),
+ contained() {}
+
+ // 2 - copy-construct
+#if optional_CPP11_OR_GREATER
+ // template< typename U = T
+ // optional_REQUIRES_T(
+ // std::is_copy_constructible<U>::value
+ // || std11::is_trivially_copy_constructible<U>::value
+ // )
+ // >
+#endif
+ optional_constexpr14 optional(optional const &other) : has_value_(other.has_value()) {
+ if (other.has_value()) {
+ contained.construct_value(other.contained.value());
+ }
+ }
+
+#if optional_CPP11_OR_GREATER
+
+ // 3 (C++11) - move-construct from optional
+ template <typename U = T optional_REQUIRES_T(std::is_move_constructible<U>::value ||
+ std11::is_trivially_move_constructible<U>::value)>
+ optional_constexpr14 optional(optional &&other)
+ // NOLINTNEXTLINE( performance-noexcept-move-constructor )
+ noexcept(std::is_nothrow_move_constructible<T>::value)
+ : has_value_(other.has_value()) {
+ if (other.has_value()) {
+ contained.construct_value(std::move(other.contained.value()));
+ }
+ }
+
+ // 4a (C++11) - explicit converting copy-construct from optional
+ template <typename U optional_REQUIRES_T(std::is_constructible<T, U const &>::value &&
+ !std::is_constructible<T, optional<U> &>::value &&
+ !std::is_constructible<T, optional<U> &&>::value &&
+ !std::is_constructible<T, optional<U> const &>::value &&
+ !std::is_constructible<T, optional<U> const &&>::value &&
+ !std::is_convertible<optional<U> &, T>::value &&
+ !std::is_convertible<optional<U> &&, T>::value &&
+ !std::is_convertible<optional<U> const &, T>::value &&
+ !std::is_convertible<optional<U> const &&, T>::value &&
+ !std::is_convertible<U const &, T>::value /*=> explicit
+ */
+ )>
+ explicit optional(optional<U> const &other) : has_value_(other.has_value()) {
+ if (other.has_value()) {
+ contained.construct_value(T{other.contained.value()});
+ }
+ }
+#endif // optional_CPP11_OR_GREATER
+
+ // 4b (C++98 and later) - non-explicit converting copy-construct from optional
+ template <typename U
+#if optional_CPP11_OR_GREATER
+ optional_REQUIRES_T(std::is_constructible<T, U const &>::value &&
+ !std::is_constructible<T, optional<U> &>::value &&
+ !std::is_constructible<T, optional<U> &&>::value &&
+ !std::is_constructible<T, optional<U> const &>::value &&
+ !std::is_constructible<T, optional<U> const &&>::value &&
+ !std::is_convertible<optional<U> &, T>::value &&
+ !std::is_convertible<optional<U> &&, T>::value &&
+ !std::is_convertible<optional<U> const &, T>::value &&
+ !std::is_convertible<optional<U> const &&, T>::value &&
+ std::is_convertible<U const &, T>::value /*=> non-explicit */
+ )
+#endif // optional_CPP11_OR_GREATER
+ >
+ // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions )
+ /*non-explicit*/ optional(optional<U> const &other) : has_value_(other.has_value()) {
+ if (other.has_value()) {
+ contained.construct_value(other.contained.value());
+ }
+ }
+
+#if optional_CPP11_OR_GREATER
+
+ // 5a (C++11) - explicit converting move-construct from optional
+ template <typename U optional_REQUIRES_T(std::is_constructible<T, U &&>::value &&
+ !std::is_constructible<T, optional<U> &>::value &&
+ !std::is_constructible<T, optional<U> &&>::value &&
+ !std::is_constructible<T, optional<U> const &>::value &&
+ !std::is_constructible<T, optional<U> const &&>::value &&
+ !std::is_convertible<optional<U> &, T>::value &&
+ !std::is_convertible<optional<U> &&, T>::value &&
+ !std::is_convertible<optional<U> const &, T>::value &&
+ !std::is_convertible<optional<U> const &&, T>::value &&
+ !std::is_convertible<U &&, T>::value /*=> explicit */
+ )>
+ explicit optional(optional<U> &&other) : has_value_(other.has_value()) {
+ if (other.has_value()) {
+ contained.construct_value(T{std::move(other.contained.value())});
+ }
+ }
+
+ // 5a (C++11) - non-explicit converting move-construct from optional
+ template <typename U optional_REQUIRES_T(std::is_constructible<T, U &&>::value &&
+ !std::is_constructible<T, optional<U> &>::value &&
+ !std::is_constructible<T, optional<U> &&>::value &&
+ !std::is_constructible<T, optional<U> const &>::value &&
+ !std::is_constructible<T, optional<U> const &&>::value &&
+ !std::is_convertible<optional<U> &, T>::value &&
+ !std::is_convertible<optional<U> &&, T>::value &&
+ !std::is_convertible<optional<U> const &, T>::value &&
+ !std::is_convertible<optional<U> const &&, T>::value &&
+ std::is_convertible<U &&, T>::value /*=> non-explicit */
+ )>
+ // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions )
+ /*non-explicit*/ optional(optional<U> &&other) : has_value_(other.has_value()) {
+ if (other.has_value()) {
+ contained.construct_value(std::move(other.contained.value()));
+ }
+ }
+
+ // 6 (C++11) - in-place construct
+ template <typename... Args optional_REQUIRES_T(std::is_constructible<T, Args &&...>::value)>
+ optional_constexpr explicit optional(nonstd_lite_in_place_t(T), Args &&... args)
+ : has_value_(true), contained(T(std::forward<Args>(args)...)) {}
+
+ // 7 (C++11) - in-place construct, initializer-list
+ template <typename U,
+ typename... Args optional_REQUIRES_T(
+ std::is_constructible<T, std::initializer_list<U> &, Args &&...>::value)>
+ optional_constexpr explicit optional(nonstd_lite_in_place_t(T), std::initializer_list<U> il,
+ Args &&... args)
+ : has_value_(true), contained(T(il, std::forward<Args>(args)...)) {}
+
+ // 8a (C++11) - explicit move construct from value
+ template <
+ typename U = T optional_REQUIRES_T(
+ std::is_constructible<T, U &&>::value &&
+ !std::is_same<typename std20::remove_cvref<U>::type, nonstd_lite_in_place_t(U)>::value &&
+ !std::is_same<typename std20::remove_cvref<U>::type, optional<T>>::value &&
+ !std::is_convertible<U &&, T>::value /*=> explicit */
+ )>
+ optional_constexpr explicit optional(U &&value)
+ : has_value_(true), contained(T{std::forward<U>(value)}) {}
+
+ // 8b (C++11) - non-explicit move construct from value
+ template <
+ typename U = T optional_REQUIRES_T(
+ std::is_constructible<T, U &&>::value &&
+ !std::is_same<typename std20::remove_cvref<U>::type, nonstd_lite_in_place_t(U)>::value &&
+ !std::is_same<typename std20::remove_cvref<U>::type, optional<T>>::value &&
+ std::is_convertible<U &&, T>::value /*=> non-explicit */
+ )>
+ // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions )
+ optional_constexpr /*non-explicit*/ optional(U &&value)
+ : has_value_(true), contained(std::forward<U>(value)) {}
+
+#else // optional_CPP11_OR_GREATER
+
+ // 8 (C++98)
+ optional(value_type const &value) : has_value_(true), contained(value) {}
+
+#endif // optional_CPP11_OR_GREATER
+
+ // x.x.3.2, destructor
+
+ ~optional() {
+ if (has_value()) {
+ contained.destruct_value();
+ }
+ }
+
+ // x.x.3.3, assignment
+
+ // 1 (C++98and later) - assign explicitly empty
+ optional &operator=(nullopt_t /*unused*/) optional_noexcept {
+ reset();
+ return *this;
+ }
+
+ // 2 (C++98and later) - copy-assign from optional
+#if optional_CPP11_OR_GREATER
+ // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature,
+ // misc-unconventional-assign-operator )
+ optional_REQUIRES_R(optional &, true
+ // std::is_copy_constructible<T>::value
+ // && std::is_copy_assignable<T>::value
+ )
+ operator=(optional const &other) noexcept(
+ std::is_nothrow_move_assignable<T>::value &&std::is_nothrow_move_constructible<T>::value)
+#else
+ optional &operator=(optional const &other)
+#endif
+ {
+ if ((has_value() == true) && (other.has_value() == false)) {
+ reset();
+ } else if ((has_value() == false) && (other.has_value() == true)) {
+ initialize(*other);
+ } else if ((has_value() == true) && (other.has_value() == true)) {
+ contained.value() = *other;
+ }
+ return *this;
+ }
+
+#if optional_CPP11_OR_GREATER
+
+ // 3 (C++11) - move-assign from optional
+ // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature,
+ // misc-unconventional-assign-operator )
+ optional_REQUIRES_R(optional &, true
+ // std::is_move_constructible<T>::value
+ // && std::is_move_assignable<T>::value
+ )
+ operator=(optional &&other) noexcept {
+ if ((has_value() == true) && (other.has_value() == false)) {
+ reset();
+ } else if ((has_value() == false) && (other.has_value() == true)) {
+ initialize(std::move(*other));
+ } else if ((has_value() == true) && (other.has_value() == true)) {
+ contained.value() = std::move(*other);
+ }
+ return *this;
+ }
+
+ // 4 (C++11) - move-assign from value
+ template <typename U = T>
+ // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature,
+ // misc-unconventional-assign-operator )
+ optional_REQUIRES_R(
+ optional &,
+ std::is_constructible<T, U>::value &&std::is_assignable<T &, U>::value &&
+ !std::is_same<typename std20::remove_cvref<U>::type, nonstd_lite_in_place_t(U)>::value &&
+ !std::is_same<typename std20::remove_cvref<U>::type, optional<T>>::value &&
+ !(std::is_scalar<T>::value && std::is_same<T, typename std::decay<U>::type>::value))
+ operator=(U &&value) {
+ if (has_value()) {
+ contained.value() = std::forward<U>(value);
+ } else {
+ initialize(T(std::forward<U>(value)));
+ }
+ return *this;
+ }
+
+#else // optional_CPP11_OR_GREATER
+
+ // 4 (C++98) - copy-assign from value
+ template <typename U /*= T*/> optional &operator=(U const &value) {
+ if (has_value())
+ contained.value() = value;
+ else
+ initialize(T(value));
+ return *this;
+ }
+
+#endif // optional_CPP11_OR_GREATER
+
+ // 5 (C++98 and later) - converting copy-assign from optional
+ template <typename U>
+#if optional_CPP11_OR_GREATER
+ // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature,
+ // misc-unconventional-assign-operator )
+ optional_REQUIRES_R(
+ optional &,
+ std::is_constructible<T, U const &>::value &&std::is_assignable<T &, U const &>::value &&
+ !std::is_constructible<T, optional<U> &>::value &&
+ !std::is_constructible<T, optional<U> &&>::value &&
+ !std::is_constructible<T, optional<U> const &>::value &&
+ !std::is_constructible<T, optional<U> const &&>::value &&
+ !std::is_convertible<optional<U> &, T>::value &&
+ !std::is_convertible<optional<U> &&, T>::value &&
+ !std::is_convertible<optional<U> const &, T>::value &&
+ !std::is_convertible<optional<U> const &&, T>::value &&
+ !std::is_assignable<T &, optional<U> &>::value &&
+ !std::is_assignable<T &, optional<U> &&>::value &&
+ !std::is_assignable<T &, optional<U> const &>::value &&
+ !std::is_assignable<T &, optional<U> const &&>::value)
+#else
+ optional &
+#endif // optional_CPP11_OR_GREATER
+ operator=(optional<U> const &other) {
+ return *this = optional(other);
+ }
+
+#if optional_CPP11_OR_GREATER
+
+ // 6 (C++11) - converting move-assign from optional
+ template <typename U>
+ // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature,
+ // misc-unconventional-assign-operator )
+ optional_REQUIRES_R(optional &,
+ std::is_constructible<T, U>::value &&std::is_assignable<T &, U>::value &&
+ !std::is_constructible<T, optional<U> &>::value &&
+ !std::is_constructible<T, optional<U> &&>::value &&
+ !std::is_constructible<T, optional<U> const &>::value &&
+ !std::is_constructible<T, optional<U> const &&>::value &&
+ !std::is_convertible<optional<U> &, T>::value &&
+ !std::is_convertible<optional<U> &&, T>::value &&
+ !std::is_convertible<optional<U> const &, T>::value &&
+ !std::is_convertible<optional<U> const &&, T>::value &&
+ !std::is_assignable<T &, optional<U> &>::value &&
+ !std::is_assignable<T &, optional<U> &&>::value &&
+ !std::is_assignable<T &, optional<U> const &>::value &&
+ !std::is_assignable<T &, optional<U> const &&>::value)
+ operator=(optional<U> &&other) {
+ return *this = optional(std::move(other));
+ }
+
+ // 7 (C++11) - emplace
+ template <typename... Args optional_REQUIRES_T(std::is_constructible<T, Args &&...>::value)>
+ T &emplace(Args &&... args) {
+ *this = nullopt;
+ contained.emplace(std::forward<Args>(args)...);
+ has_value_ = true;
+ return contained.value();
+ }
+
+ // 8 (C++11) - emplace, initializer-list
+ template <typename U,
+ typename... Args optional_REQUIRES_T(
+ std::is_constructible<T, std::initializer_list<U> &, Args &&...>::value)>
+ T &emplace(std::initializer_list<U> il, Args &&... args) {
+ *this = nullopt;
+ contained.emplace(il, std::forward<Args>(args)...);
+ has_value_ = true;
+ return contained.value();
+ }
+
+#endif // optional_CPP11_OR_GREATER
+
+ // x.x.3.4, swap
+
+ void swap(optional &other)
+#if optional_CPP11_OR_GREATER
+ noexcept(std::is_nothrow_move_constructible<T>::value &&std17::is_nothrow_swappable<T>::value)
+#endif
+ {
+ using std::swap;
+ if ((has_value() == true) && (other.has_value() == true)) {
+ swap(**this, *other);
+ } else if ((has_value() == false) && (other.has_value() == true)) {
+ initialize(std11::move(*other));
+ other.reset();
+ } else if ((has_value() == true) && (other.has_value() == false)) {
+ other.initialize(std11::move(**this));
+ reset();
+ }
+ }
+
+ // x.x.3.5, observers
+
+ optional_constexpr value_type const *operator->() const {
+ return assert(has_value()), contained.value_ptr();
+ }
+
+ optional_constexpr14 value_type *operator->() {
+ return assert(has_value()), contained.value_ptr();
+ }
+
+ optional_constexpr value_type const &operator*() const optional_ref_qual {
+ return assert(has_value()), contained.value();
+ }
+
+ optional_constexpr14 value_type &operator*() optional_ref_qual {
+ return assert(has_value()), contained.value();
+ }
+
+#if optional_HAVE(REF_QUALIFIER)
+
+ optional_constexpr value_type const &&operator*() const optional_refref_qual {
+ return std::move(**this);
+ }
+
+ optional_constexpr14 value_type &&operator*() optional_refref_qual { return std::move(**this); }
+
+#endif
+
+#if optional_CPP11_OR_GREATER
+ optional_constexpr explicit operator bool() const optional_noexcept { return has_value(); }
+#else
+ optional_constexpr operator safe_bool() const optional_noexcept {
+ return has_value() ? &optional::this_type_does_not_support_comparisons : 0;
+ }
+#endif
+
+ // NOLINTNEXTLINE( modernize-use-nodiscard )
+ /*optional_nodiscard*/ optional_constexpr bool has_value() const optional_noexcept {
+ return has_value_;
+ }
+
+ // NOLINTNEXTLINE( modernize-use-nodiscard )
+ /*optional_nodiscard*/ optional_constexpr14 value_type const &value() const optional_ref_qual {
+#if optional_CONFIG_NO_EXCEPTIONS
+ assert(has_value());
+#else
+ if (!has_value()) {
+ throw bad_optional_access();
+ }
+#endif
+ return contained.value();
+ }
+
+ optional_constexpr14 value_type &value() optional_ref_qual {
+#if optional_CONFIG_NO_EXCEPTIONS
+ assert(has_value());
+#else
+ if (!has_value()) {
+ throw bad_optional_access();
+ }
+#endif
+ return contained.value();
+ }
+
+#if optional_HAVE(REF_QUALIFIER) && \
+ (!optional_COMPILER_GNUC_VERSION || optional_COMPILER_GNUC_VERSION >= 490)
+
+ // NOLINTNEXTLINE( modernize-use-nodiscard )
+ /*optional_nodiscard*/ optional_constexpr value_type const &&value() const optional_refref_qual {
+ return std::move(value());
+ }
+
+ optional_constexpr14 value_type &&value() optional_refref_qual { return std::move(value()); }
+
+#endif
+
+#if optional_CPP11_OR_GREATER
+
+ template <typename U> optional_constexpr value_type value_or(U &&v) const optional_ref_qual {
+ return has_value() ? contained.value() : static_cast<T>(std::forward<U>(v));
+ }
+
+ template <typename U> optional_constexpr14 value_type value_or(U &&v) optional_refref_qual {
+ return has_value() ? std::move(contained.value()) : static_cast<T>(std::forward<U>(v));
+ }
+
+#else
+
+ template <typename U> optional_constexpr value_type value_or(U const &v) const {
+ return has_value() ? contained.value() : static_cast<value_type>(v);
+ }
+
+#endif // optional_CPP11_OR_GREATER
+
+ // x.x.3.6, modifiers
+
+ void reset() optional_noexcept {
+ if (has_value()) {
+ contained.destruct_value();
+ }
+
+ has_value_ = false;
+ }
+
+private:
+ void this_type_does_not_support_comparisons() const {}
+
+ template <typename V> void initialize(V const &value) {
+ assert(!has_value());
+ contained.construct_value(value);
+ has_value_ = true;
+ }
+
+#if optional_CPP11_OR_GREATER
+ template <typename V> void initialize(V &&value) {
+ assert(!has_value());
+ contained.construct_value(std::move(value));
+ has_value_ = true;
+ }
+
+#endif
+
+private:
+ bool has_value_;
+ detail::storage_t<value_type> contained;
+};
+
+// Relational operators
+
+template <typename T, typename U>
+inline optional_constexpr bool operator==(optional<T> const &x, optional<U> const &y) {
+ return bool(x) != bool(y) ? false : !bool(x) ? true : *x == *y;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator!=(optional<T> const &x, optional<U> const &y) {
+ return !(x == y);
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator<(optional<T> const &x, optional<U> const &y) {
+ return (!y) ? false : (!x) ? true : *x < *y;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator>(optional<T> const &x, optional<U> const &y) {
+ return (y < x);
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator<=(optional<T> const &x, optional<U> const &y) {
+ return !(y < x);
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator>=(optional<T> const &x, optional<U> const &y) {
+ return !(x < y);
+}
+
+// Comparison with nullopt
+
+template <typename T>
+inline optional_constexpr bool operator==(optional<T> const &x,
+ nullopt_t /*unused*/) optional_noexcept {
+ return (!x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator==(nullopt_t /*unused*/,
+ optional<T> const &x) optional_noexcept {
+ return (!x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator!=(optional<T> const &x,
+ nullopt_t /*unused*/) optional_noexcept {
+ return bool(x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator!=(nullopt_t /*unused*/,
+ optional<T> const &x) optional_noexcept {
+ return bool(x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator<(optional<T> const & /*unused*/,
+ nullopt_t /*unused*/) optional_noexcept {
+ return false;
+}
+
+template <typename T>
+inline optional_constexpr bool operator<(nullopt_t /*unused*/,
+ optional<T> const &x) optional_noexcept {
+ return bool(x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator<=(optional<T> const &x,
+ nullopt_t /*unused*/) optional_noexcept {
+ return (!x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator<=(nullopt_t /*unused*/,
+ optional<T> const & /*unused*/) optional_noexcept {
+ return true;
+}
+
+template <typename T>
+inline optional_constexpr bool operator>(optional<T> const &x,
+ nullopt_t /*unused*/) optional_noexcept {
+ return bool(x);
+}
+
+template <typename T>
+inline optional_constexpr bool operator>(nullopt_t /*unused*/,
+ optional<T> const & /*unused*/) optional_noexcept {
+ return false;
+}
+
+template <typename T>
+inline optional_constexpr bool operator>=(optional<T> const & /*unused*/,
+ nullopt_t /*unused*/) optional_noexcept {
+ return true;
+}
+
+template <typename T>
+inline optional_constexpr bool operator>=(nullopt_t /*unused*/,
+ optional<T> const &x) optional_noexcept {
+ return (!x);
+}
+
+// Comparison with T
+
+template <typename T, typename U>
+inline optional_constexpr bool operator==(optional<T> const &x, U const &v) {
+ return bool(x) ? *x == v : false;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator==(U const &v, optional<T> const &x) {
+ return bool(x) ? v == *x : false;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator!=(optional<T> const &x, U const &v) {
+ return bool(x) ? *x != v : true;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator!=(U const &v, optional<T> const &x) {
+ return bool(x) ? v != *x : true;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator<(optional<T> const &x, U const &v) {
+ return bool(x) ? *x < v : true;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator<(U const &v, optional<T> const &x) {
+ return bool(x) ? v < *x : false;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator<=(optional<T> const &x, U const &v) {
+ return bool(x) ? *x <= v : true;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator<=(U const &v, optional<T> const &x) {
+ return bool(x) ? v <= *x : false;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator>(optional<T> const &x, U const &v) {
+ return bool(x) ? *x > v : false;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator>(U const &v, optional<T> const &x) {
+ return bool(x) ? v > *x : true;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator>=(optional<T> const &x, U const &v) {
+ return bool(x) ? *x >= v : false;
+}
+
+template <typename T, typename U>
+inline optional_constexpr bool operator>=(U const &v, optional<T> const &x) {
+ return bool(x) ? v >= *x : true;
+}
+
+// Specialized algorithms
+
+template <
+ typename T
+#if optional_CPP11_OR_GREATER
+ optional_REQUIRES_T(std::is_move_constructible<T>::value &&std17::is_swappable<T>::value)
+#endif
+ >
+void swap(optional<T> &x, optional<T> &y)
+#if optional_CPP11_OR_GREATER
+ noexcept(noexcept(x.swap(y)))
+#endif
+{
+ x.swap(y);
+}
+
+#if optional_CPP11_OR_GREATER
+
+template <typename T>
+optional_constexpr optional<typename std::decay<T>::type> make_optional(T &&value) {
+ return optional<typename std::decay<T>::type>(std::forward<T>(value));
+}
+
+template <typename T, typename... Args>
+optional_constexpr optional<T> make_optional(Args &&... args) {
+ return optional<T>(nonstd_lite_in_place(T), std::forward<Args>(args)...);
+}
+
+template <typename T, typename U, typename... Args>
+optional_constexpr optional<T> make_optional(std::initializer_list<U> il, Args &&... args) {
+ return optional<T>(nonstd_lite_in_place(T), il, std::forward<Args>(args)...);
+}
+
+#else
+
+template <typename T> optional<T> make_optional(T const &value) { return optional<T>(value); }
+
+#endif // optional_CPP11_OR_GREATER
+
+} // namespace optional_lite
+
+using optional_lite::nullopt;
+using optional_lite::nullopt_t;
+using optional_lite::optional;
+
+#if !optional_CONFIG_NO_EXCEPTIONS
+using optional_lite::bad_optional_access;
+#endif
+
+using optional_lite::make_optional;
+
+} // namespace nonstd
+
+#if optional_CPP11_OR_GREATER
+
+// specialize the std::hash algorithm:
+
+namespace std {
+
+template <class T> struct hash<nonstd::optional<T>> {
+public:
+ std::size_t operator()(nonstd::optional<T> const &v) const optional_noexcept {
+ return bool(v) ? std::hash<T>{}(*v) : 0;
+ }
+};
+
+} // namespace std
+
+#endif // optional_CPP11_OR_GREATER
+
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#elif defined(__GNUC__)
+#pragma GCC diagnostic pop
+#elif defined(_MSC_VER)
+#pragma warning(pop)
+#endif
+
+#endif // optional_USES_STD_OPTIONAL
+
+#endif // NONSTD_OPTIONAL_LITE_HPP
+// Copyright 2017-2020 by Martin Moene
+//
+// string-view lite, a C++17-like string_view for C++98 and later.
+// For more information see https://github.com/martinmoene/string-view-lite
+//
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+#pragma once
+
+#ifndef NONSTD_SV_LITE_H_INCLUDED
+#define NONSTD_SV_LITE_H_INCLUDED
+
+#define string_view_lite_MAJOR 1
+#define string_view_lite_MINOR 6
+#define string_view_lite_PATCH 0
+
+#define string_view_lite_VERSION nssv_STRINGIFY(string_view_lite_MAJOR) "." nssv_STRINGIFY(string_view_lite_MINOR) "." nssv_STRINGIFY(string_view_lite_PATCH)
+
+#define nssv_STRINGIFY( x ) nssv_STRINGIFY_( x )
+#define nssv_STRINGIFY_( x ) #x
+
+// string-view lite configuration:
+
+#define nssv_STRING_VIEW_DEFAULT 0
+#define nssv_STRING_VIEW_NONSTD 1
+#define nssv_STRING_VIEW_STD 2
+
+// tweak header support:
+
+#ifdef __has_include
+# if __has_include(<nonstd/string_view.tweak.hpp>)
+# include <nonstd/string_view.tweak.hpp>
+# endif
+#define nssv_HAVE_TWEAK_HEADER 1
+#else
+#define nssv_HAVE_TWEAK_HEADER 0
+//# pragma message("string_view.hpp: Note: Tweak header not supported.")
+#endif
+
+// string_view selection and configuration:
+
+#if !defined( nssv_CONFIG_SELECT_STRING_VIEW )
+# define nssv_CONFIG_SELECT_STRING_VIEW ( nssv_HAVE_STD_STRING_VIEW ? nssv_STRING_VIEW_STD : nssv_STRING_VIEW_NONSTD )
+#endif
+
+#ifndef nssv_CONFIG_STD_SV_OPERATOR
+# define nssv_CONFIG_STD_SV_OPERATOR 0
+#endif
+
+#ifndef nssv_CONFIG_USR_SV_OPERATOR
+# define nssv_CONFIG_USR_SV_OPERATOR 1
+#endif
+
+#ifdef nssv_CONFIG_CONVERSION_STD_STRING
+# define nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS nssv_CONFIG_CONVERSION_STD_STRING
+# define nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS nssv_CONFIG_CONVERSION_STD_STRING
+#endif
+
+#ifndef nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS
+# define nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS 1
+#endif
+
+#ifndef nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS
+# define nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS 1
+#endif
+
+#ifndef nssv_CONFIG_NO_STREAM_INSERTION
+# define nssv_CONFIG_NO_STREAM_INSERTION 0
+#endif
+
+// Control presence of exception handling (try and auto discover):
+
+#ifndef nssv_CONFIG_NO_EXCEPTIONS
+# if defined(_MSC_VER)
+# include <cstddef> // for _HAS_EXCEPTIONS
+# endif
+# if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || (_HAS_EXCEPTIONS)
+# define nssv_CONFIG_NO_EXCEPTIONS 0
+# else
+# define nssv_CONFIG_NO_EXCEPTIONS 1
+# endif
+#endif
+
+// C++ language version detection (C++20 is speculative):
+// Note: VC14.0/1900 (VS2015) lacks too much from C++14.
+
+#ifndef nssv_CPLUSPLUS
+# if defined(_MSVC_LANG ) && !defined(__clang__)
+# define nssv_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG )
+# else
+# define nssv_CPLUSPLUS __cplusplus
+# endif
+#endif
+
+#define nssv_CPP98_OR_GREATER ( nssv_CPLUSPLUS >= 199711L )
+#define nssv_CPP11_OR_GREATER ( nssv_CPLUSPLUS >= 201103L )
+#define nssv_CPP11_OR_GREATER_ ( nssv_CPLUSPLUS >= 201103L )
+#define nssv_CPP14_OR_GREATER ( nssv_CPLUSPLUS >= 201402L )
+#define nssv_CPP17_OR_GREATER ( nssv_CPLUSPLUS >= 201703L )
+#define nssv_CPP20_OR_GREATER ( nssv_CPLUSPLUS >= 202000L )
+
+// use C++17 std::string_view if available and requested:
+
+#if nssv_CPP17_OR_GREATER && defined(__has_include )
+# if __has_include( <string_view> )
+# define nssv_HAVE_STD_STRING_VIEW 1
+# else
+# define nssv_HAVE_STD_STRING_VIEW 0
+# endif
+#else
+# define nssv_HAVE_STD_STRING_VIEW 0
+#endif
+
+#define nssv_USES_STD_STRING_VIEW ( (nssv_CONFIG_SELECT_STRING_VIEW == nssv_STRING_VIEW_STD) || ((nssv_CONFIG_SELECT_STRING_VIEW == nssv_STRING_VIEW_DEFAULT) && nssv_HAVE_STD_STRING_VIEW) )
+
+#define nssv_HAVE_STARTS_WITH ( nssv_CPP20_OR_GREATER || !nssv_USES_STD_STRING_VIEW )
+#define nssv_HAVE_ENDS_WITH nssv_HAVE_STARTS_WITH
+
+//
+// Use C++17 std::string_view:
+//
+
+#if nssv_USES_STD_STRING_VIEW
+
+#include <string_view>
+
+// Extensions for std::string:
+
+#if nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS
+
+namespace nonstd {
+
+template< class CharT, class Traits, class Allocator = std::allocator<CharT> >
+std::basic_string<CharT, Traits, Allocator>
+to_string( std::basic_string_view<CharT, Traits> v, Allocator const & a = Allocator() )
+{
+ return std::basic_string<CharT,Traits, Allocator>( v.begin(), v.end(), a );
+}
+
+template< class CharT, class Traits, class Allocator >
+std::basic_string_view<CharT, Traits>
+to_string_view( std::basic_string<CharT, Traits, Allocator> const & s )
+{
+ return std::basic_string_view<CharT, Traits>( s.data(), s.size() );
+}
+
+// Literal operators sv and _sv:
+
+#if nssv_CONFIG_STD_SV_OPERATOR
+
+using namespace std::literals::string_view_literals;
+
+#endif
+
+#if nssv_CONFIG_USR_SV_OPERATOR
+
+inline namespace literals {
+inline namespace string_view_literals {
+
+
+constexpr std::string_view operator "" _sv( const char* str, size_t len ) noexcept // (1)
+{
+ return std::string_view{ str, len };
+}
+
+constexpr std::u16string_view operator "" _sv( const char16_t* str, size_t len ) noexcept // (2)
+{
+ return std::u16string_view{ str, len };
+}
+
+constexpr std::u32string_view operator "" _sv( const char32_t* str, size_t len ) noexcept // (3)
+{
+ return std::u32string_view{ str, len };
+}
+
+constexpr std::wstring_view operator "" _sv( const wchar_t* str, size_t len ) noexcept // (4)
+{
+ return std::wstring_view{ str, len };
+}
+
+}} // namespace literals::string_view_literals
+
+#endif // nssv_CONFIG_USR_SV_OPERATOR
+
+} // namespace nonstd
+
+#endif // nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS
+
+namespace nonstd {
+
+using std::string_view;
+using std::wstring_view;
+using std::u16string_view;
+using std::u32string_view;
+using std::basic_string_view;
+
+// literal "sv" and "_sv", see above
+
+using std::operator==;
+using std::operator!=;
+using std::operator<;
+using std::operator<=;
+using std::operator>;
+using std::operator>=;
+
+using std::operator<<;
+
+} // namespace nonstd
+
+#else // nssv_HAVE_STD_STRING_VIEW
+
+//
+// Before C++17: use string_view lite:
+//
+
+// Compiler versions:
+//
+// MSVC++ 6.0 _MSC_VER == 1200 nssv_COMPILER_MSVC_VERSION == 60 (Visual Studio 6.0)
+// MSVC++ 7.0 _MSC_VER == 1300 nssv_COMPILER_MSVC_VERSION == 70 (Visual Studio .NET 2002)
+// MSVC++ 7.1 _MSC_VER == 1310 nssv_COMPILER_MSVC_VERSION == 71 (Visual Studio .NET 2003)
+// MSVC++ 8.0 _MSC_VER == 1400 nssv_COMPILER_MSVC_VERSION == 80 (Visual Studio 2005)
+// MSVC++ 9.0 _MSC_VER == 1500 nssv_COMPILER_MSVC_VERSION == 90 (Visual Studio 2008)
+// MSVC++ 10.0 _MSC_VER == 1600 nssv_COMPILER_MSVC_VERSION == 100 (Visual Studio 2010)
+// MSVC++ 11.0 _MSC_VER == 1700 nssv_COMPILER_MSVC_VERSION == 110 (Visual Studio 2012)
+// MSVC++ 12.0 _MSC_VER == 1800 nssv_COMPILER_MSVC_VERSION == 120 (Visual Studio 2013)
+// MSVC++ 14.0 _MSC_VER == 1900 nssv_COMPILER_MSVC_VERSION == 140 (Visual Studio 2015)
+// MSVC++ 14.1 _MSC_VER >= 1910 nssv_COMPILER_MSVC_VERSION == 141 (Visual Studio 2017)
+// MSVC++ 14.2 _MSC_VER >= 1920 nssv_COMPILER_MSVC_VERSION == 142 (Visual Studio 2019)
+
+#if defined(_MSC_VER ) && !defined(__clang__)
+# define nssv_COMPILER_MSVC_VER (_MSC_VER )
+# define nssv_COMPILER_MSVC_VERSION (_MSC_VER / 10 - 10 * ( 5 + (_MSC_VER < 1900 ) ) )
+#else
+# define nssv_COMPILER_MSVC_VER 0
+# define nssv_COMPILER_MSVC_VERSION 0
+#endif
+
+#define nssv_COMPILER_VERSION( major, minor, patch ) ( 10 * ( 10 * (major) + (minor) ) + (patch) )
+
+#if defined( __apple_build_version__ )
+# define nssv_COMPILER_APPLECLANG_VERSION nssv_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__)
+# define nssv_COMPILER_CLANG_VERSION 0
+#elif defined( __clang__ )
+# define nssv_COMPILER_APPLECLANG_VERSION 0
+# define nssv_COMPILER_CLANG_VERSION nssv_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__)
+#else
+# define nssv_COMPILER_APPLECLANG_VERSION 0
+# define nssv_COMPILER_CLANG_VERSION 0
+#endif
+
+#if defined(__GNUC__) && !defined(__clang__)
+# define nssv_COMPILER_GNUC_VERSION nssv_COMPILER_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)
+#else
+# define nssv_COMPILER_GNUC_VERSION 0
+#endif
+
+// half-open range [lo..hi):
+#define nssv_BETWEEN( v, lo, hi ) ( (lo) <= (v) && (v) < (hi) )
+
+// Presence of language and library features:
+
+#ifdef _HAS_CPP0X
+# define nssv_HAS_CPP0X _HAS_CPP0X
+#else
+# define nssv_HAS_CPP0X 0
+#endif
+
+// Unless defined otherwise below, consider VC14 as C++11 for variant-lite:
+
+#if nssv_COMPILER_MSVC_VER >= 1900
+# undef nssv_CPP11_OR_GREATER
+# define nssv_CPP11_OR_GREATER 1
+#endif
+
+#define nssv_CPP11_90 (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1500)
+#define nssv_CPP11_100 (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1600)
+#define nssv_CPP11_110 (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1700)
+#define nssv_CPP11_120 (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1800)
+#define nssv_CPP11_140 (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1900)
+#define nssv_CPP11_141 (nssv_CPP11_OR_GREATER_ || nssv_COMPILER_MSVC_VER >= 1910)
+
+#define nssv_CPP14_000 (nssv_CPP14_OR_GREATER)
+#define nssv_CPP17_000 (nssv_CPP17_OR_GREATER)
+
+// Presence of C++11 language features:
+
+#define nssv_HAVE_CONSTEXPR_11 nssv_CPP11_140
+#define nssv_HAVE_EXPLICIT_CONVERSION nssv_CPP11_140
+#define nssv_HAVE_INLINE_NAMESPACE nssv_CPP11_140
+#define nssv_HAVE_NOEXCEPT nssv_CPP11_140
+#define nssv_HAVE_NULLPTR nssv_CPP11_100
+#define nssv_HAVE_REF_QUALIFIER nssv_CPP11_140
+#define nssv_HAVE_UNICODE_LITERALS nssv_CPP11_140
+#define nssv_HAVE_USER_DEFINED_LITERALS nssv_CPP11_140
+#define nssv_HAVE_WCHAR16_T nssv_CPP11_100
+#define nssv_HAVE_WCHAR32_T nssv_CPP11_100
+
+#if ! ( ( nssv_CPP11_OR_GREATER && nssv_COMPILER_CLANG_VERSION ) || nssv_BETWEEN( nssv_COMPILER_CLANG_VERSION, 300, 400 ) )
+# define nssv_HAVE_STD_DEFINED_LITERALS nssv_CPP11_140
+#else
+# define nssv_HAVE_STD_DEFINED_LITERALS 0
+#endif
+
+// Presence of C++14 language features:
+
+#define nssv_HAVE_CONSTEXPR_14 nssv_CPP14_000
+
+// Presence of C++17 language features:
+
+#define nssv_HAVE_NODISCARD nssv_CPP17_000
+
+// Presence of C++ library features:
+
+#define nssv_HAVE_STD_HASH nssv_CPP11_120
+
+// Presence of compiler intrinsics:
+
+// Providing char-type specializations for compare() and length() that
+// use compiler intrinsics can improve compile- and run-time performance.
+//
+// The challenge is in using the right combinations of builtin availability
+// and its constexpr-ness.
+//
+// | compiler | __builtin_memcmp (constexpr) | memcmp (constexpr) |
+// |----------|------------------------------|---------------------|
+// | clang | 4.0 (>= 4.0 ) | any (? ) |
+// | clang-a | 9.0 (>= 9.0 ) | any (? ) |
+// | gcc | any (constexpr) | any (? ) |
+// | msvc | >= 14.2 C++17 (>= 14.2 ) | any (? ) |
+
+#define nssv_HAVE_BUILTIN_VER ( (nssv_CPP17_000 && nssv_COMPILER_MSVC_VERSION >= 142) || nssv_COMPILER_GNUC_VERSION > 0 || nssv_COMPILER_CLANG_VERSION >= 400 || nssv_COMPILER_APPLECLANG_VERSION >= 900 )
+#define nssv_HAVE_BUILTIN_CE ( nssv_HAVE_BUILTIN_VER )
+
+#define nssv_HAVE_BUILTIN_MEMCMP ( (nssv_HAVE_CONSTEXPR_14 && nssv_HAVE_BUILTIN_CE) || !nssv_HAVE_CONSTEXPR_14 )
+#define nssv_HAVE_BUILTIN_STRLEN ( (nssv_HAVE_CONSTEXPR_11 && nssv_HAVE_BUILTIN_CE) || !nssv_HAVE_CONSTEXPR_11 )
+
+#ifdef __has_builtin
+# define nssv_HAVE_BUILTIN( x ) __has_builtin( x )
+#else
+# define nssv_HAVE_BUILTIN( x ) 0
+#endif
+
+#if nssv_HAVE_BUILTIN(__builtin_memcmp) || nssv_HAVE_BUILTIN_VER
+# define nssv_BUILTIN_MEMCMP __builtin_memcmp
+#else
+# define nssv_BUILTIN_MEMCMP memcmp
+#endif
+
+#if nssv_HAVE_BUILTIN(__builtin_strlen) || nssv_HAVE_BUILTIN_VER
+# define nssv_BUILTIN_STRLEN __builtin_strlen
+#else
+# define nssv_BUILTIN_STRLEN strlen
+#endif
+
+// C++ feature usage:
+
+#if nssv_HAVE_CONSTEXPR_11
+# define nssv_constexpr constexpr
+#else
+# define nssv_constexpr /*constexpr*/
+#endif
+
+#if nssv_HAVE_CONSTEXPR_14
+# define nssv_constexpr14 constexpr
+#else
+# define nssv_constexpr14 /*constexpr*/
+#endif
+
+#if nssv_HAVE_EXPLICIT_CONVERSION
+# define nssv_explicit explicit
+#else
+# define nssv_explicit /*explicit*/
+#endif
+
+#if nssv_HAVE_INLINE_NAMESPACE
+# define nssv_inline_ns inline
+#else
+# define nssv_inline_ns /*inline*/
+#endif
+
+#if nssv_HAVE_NOEXCEPT
+# define nssv_noexcept noexcept
+#else
+# define nssv_noexcept /*noexcept*/
+#endif
+
+//#if nssv_HAVE_REF_QUALIFIER
+//# define nssv_ref_qual &
+//# define nssv_refref_qual &&
+//#else
+//# define nssv_ref_qual /*&*/
+//# define nssv_refref_qual /*&&*/
+//#endif
+
+#if nssv_HAVE_NULLPTR
+# define nssv_nullptr nullptr
+#else
+# define nssv_nullptr NULL
+#endif
+
+#if nssv_HAVE_NODISCARD
+# define nssv_nodiscard [[nodiscard]]
+#else
+# define nssv_nodiscard /*[[nodiscard]]*/
+#endif
+
+// Additional includes:
+
+#include <algorithm>
+#include <cassert>
+#include <iterator>
+#include <limits>
+#include <string> // std::char_traits<>
+
+#if ! nssv_CONFIG_NO_STREAM_INSERTION
+# include <ostream>
+#endif
+
+#if ! nssv_CONFIG_NO_EXCEPTIONS
+# include <stdexcept>
+#endif
+
+#if nssv_CPP11_OR_GREATER
+# include <type_traits>
+#endif
+
+// Clang, GNUC, MSVC warning suppression macros:
+
+#if defined(__clang__)
+# pragma clang diagnostic ignored "-Wreserved-user-defined-literal"
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wuser-defined-literals"
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wliteral-suffix"
+#endif // __clang__
+
+#if nssv_COMPILER_MSVC_VERSION >= 140
+# define nssv_SUPPRESS_MSGSL_WARNING(expr) [[gsl::suppress(expr)]]
+# define nssv_SUPPRESS_MSVC_WARNING(code, descr) __pragma(warning(suppress: code) )
+# define nssv_DISABLE_MSVC_WARNINGS(codes) __pragma(warning(push)) __pragma(warning(disable: codes))
+#else
+# define nssv_SUPPRESS_MSGSL_WARNING(expr)
+# define nssv_SUPPRESS_MSVC_WARNING(code, descr)
+# define nssv_DISABLE_MSVC_WARNINGS(codes)
+#endif
+
+#if defined(__clang__)
+# define nssv_RESTORE_WARNINGS() _Pragma("clang diagnostic pop")
+#elif defined(__GNUC__)
+# define nssv_RESTORE_WARNINGS() _Pragma("GCC diagnostic pop")
+#elif nssv_COMPILER_MSVC_VERSION >= 140
+# define nssv_RESTORE_WARNINGS() __pragma(warning(pop ))
+#else
+# define nssv_RESTORE_WARNINGS()
+#endif
+
+// Suppress the following MSVC (GSL) warnings:
+// - C4455, non-gsl : 'operator ""sv': literal suffix identifiers that do not
+// start with an underscore are reserved
+// - C26472, gsl::t.1 : don't use a static_cast for arithmetic conversions;
+// use brace initialization, gsl::narrow_cast or gsl::narow
+// - C26481: gsl::b.1 : don't use pointer arithmetic. Use span instead
+
+nssv_DISABLE_MSVC_WARNINGS( 4455 26481 26472 )
+//nssv_DISABLE_CLANG_WARNINGS( "-Wuser-defined-literals" )
+//nssv_DISABLE_GNUC_WARNINGS( -Wliteral-suffix )
+
+namespace nonstd { namespace sv_lite {
+
+namespace detail {
+
+// support constexpr comparison in C++14;
+// for C++17 and later, use provided traits:
+
+template< typename CharT >
+inline nssv_constexpr14 int compare( CharT const * s1, CharT const * s2, std::size_t count )
+{
+ while ( count-- != 0 )
+ {
+ if ( *s1 < *s2 ) return -1;
+ if ( *s1 > *s2 ) return +1;
+ ++s1; ++s2;
+ }
+ return 0;
+}
+
+#if nssv_HAVE_BUILTIN_MEMCMP
+
+// specialization of compare() for char, see also generic compare() above:
+
+inline nssv_constexpr14 int compare( char const * s1, char const * s2, std::size_t count )
+{
+ return nssv_BUILTIN_MEMCMP( s1, s2, count );
+}
+
+#endif
+
+#if nssv_HAVE_BUILTIN_STRLEN
+
+// specialization of length() for char, see also generic length() further below:
+
+inline nssv_constexpr std::size_t length( char const * s )
+{
+ return nssv_BUILTIN_STRLEN( s );
+}
+
+#endif
+
+#if defined(__OPTIMIZE__)
+
+// gcc, clang provide __OPTIMIZE__
+// Expect tail call optimization to make length() non-recursive:
+
+template< typename CharT >
+inline nssv_constexpr std::size_t length( CharT * s, std::size_t result = 0 )
+{
+ return *s == '\0' ? result : length( s + 1, result + 1 );
+}
+
+#else // OPTIMIZE
+
+// non-recursive:
+
+template< typename CharT >
+inline nssv_constexpr14 std::size_t length( CharT * s )
+{
+ std::size_t result = 0;
+ while ( *s++ != '\0' )
+ {
+ ++result;
+ }
+ return result;
+}
+
+#endif // OPTIMIZE
+
+} // namespace detail
+
+template
+<
+ class CharT,
+ class Traits = std::char_traits<CharT>
+>
+class basic_string_view;
+
+//
+// basic_string_view:
+//
+
+template
+<
+ class CharT,
+ class Traits /* = std::char_traits<CharT> */
+>
+class basic_string_view
+{
+public:
+ // Member types:
+
+ typedef Traits traits_type;
+ typedef CharT value_type;
+
+ typedef CharT * pointer;
+ typedef CharT const * const_pointer;
+ typedef CharT & reference;
+ typedef CharT const & const_reference;
+
+ typedef const_pointer iterator;
+ typedef const_pointer const_iterator;
+ typedef std::reverse_iterator< const_iterator > reverse_iterator;
+ typedef std::reverse_iterator< const_iterator > const_reverse_iterator;
+
+ typedef std::size_t size_type;
+ typedef std::ptrdiff_t difference_type;
+
+ // 24.4.2.1 Construction and assignment:
+
+ nssv_constexpr basic_string_view() nssv_noexcept
+ : data_( nssv_nullptr )
+ , size_( 0 )
+ {}
+
+#if nssv_CPP11_OR_GREATER
+ nssv_constexpr basic_string_view( basic_string_view const & other ) nssv_noexcept = default;
+#else
+ nssv_constexpr basic_string_view( basic_string_view const & other ) nssv_noexcept
+ : data_( other.data_)
+ , size_( other.size_)
+ {}
+#endif
+
+ nssv_constexpr basic_string_view( CharT const * s, size_type count ) nssv_noexcept // non-standard noexcept
+ : data_( s )
+ , size_( count )
+ {}
+
+ nssv_constexpr basic_string_view( CharT const * s) nssv_noexcept // non-standard noexcept
+ : data_( s )
+#if nssv_CPP17_OR_GREATER
+ , size_( Traits::length(s) )
+#elif nssv_CPP11_OR_GREATER
+ , size_( detail::length(s) )
+#else
+ , size_( Traits::length(s) )
+#endif
+ {}
+
+ // Assignment:
+
+#if nssv_CPP11_OR_GREATER
+ nssv_constexpr14 basic_string_view & operator=( basic_string_view const & other ) nssv_noexcept = default;
+#else
+ nssv_constexpr14 basic_string_view & operator=( basic_string_view const & other ) nssv_noexcept
+ {
+ data_ = other.data_;
+ size_ = other.size_;
+ return *this;
+ }
+#endif
+
+ // 24.4.2.2 Iterator support:
+
+ nssv_constexpr const_iterator begin() const nssv_noexcept { return data_; }
+ nssv_constexpr const_iterator end() const nssv_noexcept { return data_ + size_; }
+
+ nssv_constexpr const_iterator cbegin() const nssv_noexcept { return begin(); }
+ nssv_constexpr const_iterator cend() const nssv_noexcept { return end(); }
+
+ nssv_constexpr const_reverse_iterator rbegin() const nssv_noexcept { return const_reverse_iterator( end() ); }
+ nssv_constexpr const_reverse_iterator rend() const nssv_noexcept { return const_reverse_iterator( begin() ); }
+
+ nssv_constexpr const_reverse_iterator crbegin() const nssv_noexcept { return rbegin(); }
+ nssv_constexpr const_reverse_iterator crend() const nssv_noexcept { return rend(); }
+
+ // 24.4.2.3 Capacity:
+
+ nssv_constexpr size_type size() const nssv_noexcept { return size_; }
+ nssv_constexpr size_type length() const nssv_noexcept { return size_; }
+ nssv_constexpr size_type max_size() const nssv_noexcept { return (std::numeric_limits< size_type >::max)(); }
+
+ // since C++20
+ nssv_nodiscard nssv_constexpr bool empty() const nssv_noexcept
+ {
+ return 0 == size_;
+ }
+
+ // 24.4.2.4 Element access:
+
+ nssv_constexpr const_reference operator[]( size_type pos ) const
+ {
+ return data_at( pos );
+ }
+
+ nssv_constexpr14 const_reference at( size_type pos ) const
+ {
+#if nssv_CONFIG_NO_EXCEPTIONS
+ assert( pos < size() );
+#else
+ if ( pos >= size() )
+ {
+ throw std::out_of_range("nonstd::string_view::at()");
+ }
+#endif
+ return data_at( pos );
+ }
+
+ nssv_constexpr const_reference front() const { return data_at( 0 ); }
+ nssv_constexpr const_reference back() const { return data_at( size() - 1 ); }
+
+ nssv_constexpr const_pointer data() const nssv_noexcept { return data_; }
+
+ // 24.4.2.5 Modifiers:
+
+ nssv_constexpr14 void remove_prefix( size_type n )
+ {
+ assert( n <= size() );
+ data_ += n;
+ size_ -= n;
+ }
+
+ nssv_constexpr14 void remove_suffix( size_type n )
+ {
+ assert( n <= size() );
+ size_ -= n;
+ }
+
+ nssv_constexpr14 void swap( basic_string_view & other ) nssv_noexcept
+ {
+ const basic_string_view tmp(other);
+ other = *this;
+ *this = tmp;
+ }
+
+ // 24.4.2.6 String operations:
+
+ size_type copy( CharT * dest, size_type n, size_type pos = 0 ) const
+ {
+#if nssv_CONFIG_NO_EXCEPTIONS
+ assert( pos <= size() );
+#else
+ if ( pos > size() )
+ {
+ throw std::out_of_range("nonstd::string_view::copy()");
+ }
+#endif
+ const size_type rlen = (std::min)( n, size() - pos );
+
+ (void) Traits::copy( dest, data() + pos, rlen );
+
+ return rlen;
+ }
+
+ nssv_constexpr14 basic_string_view substr( size_type pos = 0, size_type n = npos ) const
+ {
+#if nssv_CONFIG_NO_EXCEPTIONS
+ assert( pos <= size() );
+#else
+ if ( pos > size() )
+ {
+ throw std::out_of_range("nonstd::string_view::substr()");
+ }
+#endif
+ return basic_string_view( data() + pos, (std::min)( n, size() - pos ) );
+ }
+
+ // compare(), 6x:
+
+ nssv_constexpr14 int compare( basic_string_view other ) const nssv_noexcept // (1)
+ {
+#if nssv_CPP17_OR_GREATER
+ if ( const int result = Traits::compare( data(), other.data(), (std::min)( size(), other.size() ) ) )
+#else
+ if ( const int result = detail::compare( data(), other.data(), (std::min)( size(), other.size() ) ) )
+#endif
+ {
+ return result;
+ }
+
+ return size() == other.size() ? 0 : size() < other.size() ? -1 : 1;
+ }
+
+ nssv_constexpr int compare( size_type pos1, size_type n1, basic_string_view other ) const // (2)
+ {
+ return substr( pos1, n1 ).compare( other );
+ }
+
+ nssv_constexpr int compare( size_type pos1, size_type n1, basic_string_view other, size_type pos2, size_type n2 ) const // (3)
+ {
+ return substr( pos1, n1 ).compare( other.substr( pos2, n2 ) );
+ }
+
+ nssv_constexpr int compare( CharT const * s ) const // (4)
+ {
+ return compare( basic_string_view( s ) );
+ }
+
+ nssv_constexpr int compare( size_type pos1, size_type n1, CharT const * s ) const // (5)
+ {
+ return substr( pos1, n1 ).compare( basic_string_view( s ) );
+ }
+
+ nssv_constexpr int compare( size_type pos1, size_type n1, CharT const * s, size_type n2 ) const // (6)
+ {
+ return substr( pos1, n1 ).compare( basic_string_view( s, n2 ) );
+ }
+
+ // 24.4.2.7 Searching:
+
+ // starts_with(), 3x, since C++20:
+
+ nssv_constexpr bool starts_with( basic_string_view v ) const nssv_noexcept // (1)
+ {
+ return size() >= v.size() && compare( 0, v.size(), v ) == 0;
+ }
+
+ nssv_constexpr bool starts_with( CharT c ) const nssv_noexcept // (2)
+ {
+ return starts_with( basic_string_view( &c, 1 ) );
+ }
+
+ nssv_constexpr bool starts_with( CharT const * s ) const // (3)
+ {
+ return starts_with( basic_string_view( s ) );
+ }
+
+ // ends_with(), 3x, since C++20:
+
+ nssv_constexpr bool ends_with( basic_string_view v ) const nssv_noexcept // (1)
+ {
+ return size() >= v.size() && compare( size() - v.size(), npos, v ) == 0;
+ }
+
+ nssv_constexpr bool ends_with( CharT c ) const nssv_noexcept // (2)
+ {
+ return ends_with( basic_string_view( &c, 1 ) );
+ }
+
+ nssv_constexpr bool ends_with( CharT const * s ) const // (3)
+ {
+ return ends_with( basic_string_view( s ) );
+ }
+
+ // find(), 4x:
+
+ nssv_constexpr14 size_type find( basic_string_view v, size_type pos = 0 ) const nssv_noexcept // (1)
+ {
+ return assert( v.size() == 0 || v.data() != nssv_nullptr )
+ , pos >= size()
+ ? npos
+ : to_pos( std::search( cbegin() + pos, cend(), v.cbegin(), v.cend(), Traits::eq ) );
+ }
+
+ nssv_constexpr14 size_type find( CharT c, size_type pos = 0 ) const nssv_noexcept // (2)
+ {
+ return find( basic_string_view( &c, 1 ), pos );
+ }
+
+ nssv_constexpr14 size_type find( CharT const * s, size_type pos, size_type n ) const // (3)
+ {
+ return find( basic_string_view( s, n ), pos );
+ }
+
+ nssv_constexpr14 size_type find( CharT const * s, size_type pos = 0 ) const // (4)
+ {
+ return find( basic_string_view( s ), pos );
+ }
+
+ // rfind(), 4x:
+
+ nssv_constexpr14 size_type rfind( basic_string_view v, size_type pos = npos ) const nssv_noexcept // (1)
+ {
+ if ( size() < v.size() )
+ {
+ return npos;
+ }
+
+ if ( v.empty() )
+ {
+ return (std::min)( size(), pos );
+ }
+
+ const_iterator last = cbegin() + (std::min)( size() - v.size(), pos ) + v.size();
+ const_iterator result = std::find_end( cbegin(), last, v.cbegin(), v.cend(), Traits::eq );
+
+ return result != last ? size_type( result - cbegin() ) : npos;
+ }
+
+ nssv_constexpr14 size_type rfind( CharT c, size_type pos = npos ) const nssv_noexcept // (2)
+ {
+ return rfind( basic_string_view( &c, 1 ), pos );
+ }
+
+ nssv_constexpr14 size_type rfind( CharT const * s, size_type pos, size_type n ) const // (3)
+ {
+ return rfind( basic_string_view( s, n ), pos );
+ }
+
+ nssv_constexpr14 size_type rfind( CharT const * s, size_type pos = npos ) const // (4)
+ {
+ return rfind( basic_string_view( s ), pos );
+ }
+
+ // find_first_of(), 4x:
+
+ nssv_constexpr size_type find_first_of( basic_string_view v, size_type pos = 0 ) const nssv_noexcept // (1)
+ {
+ return pos >= size()
+ ? npos
+ : to_pos( std::find_first_of( cbegin() + pos, cend(), v.cbegin(), v.cend(), Traits::eq ) );
+ }
+
+ nssv_constexpr size_type find_first_of( CharT c, size_type pos = 0 ) const nssv_noexcept // (2)
+ {
+ return find_first_of( basic_string_view( &c, 1 ), pos );
+ }
+
+ nssv_constexpr size_type find_first_of( CharT const * s, size_type pos, size_type n ) const // (3)
+ {
+ return find_first_of( basic_string_view( s, n ), pos );
+ }
+
+ nssv_constexpr size_type find_first_of( CharT const * s, size_type pos = 0 ) const // (4)
+ {
+ return find_first_of( basic_string_view( s ), pos );
+ }
+
+ // find_last_of(), 4x:
+
+ nssv_constexpr size_type find_last_of( basic_string_view v, size_type pos = npos ) const nssv_noexcept // (1)
+ {
+ return empty()
+ ? npos
+ : pos >= size()
+ ? find_last_of( v, size() - 1 )
+ : to_pos( std::find_first_of( const_reverse_iterator( cbegin() + pos + 1 ), crend(), v.cbegin(), v.cend(), Traits::eq ) );
+ }
+
+ nssv_constexpr size_type find_last_of( CharT c, size_type pos = npos ) const nssv_noexcept // (2)
+ {
+ return find_last_of( basic_string_view( &c, 1 ), pos );
+ }
+
+ nssv_constexpr size_type find_last_of( CharT const * s, size_type pos, size_type count ) const // (3)
+ {
+ return find_last_of( basic_string_view( s, count ), pos );
+ }
+
+ nssv_constexpr size_type find_last_of( CharT const * s, size_type pos = npos ) const // (4)
+ {
+ return find_last_of( basic_string_view( s ), pos );
+ }
+
+ // find_first_not_of(), 4x:
+
+ nssv_constexpr size_type find_first_not_of( basic_string_view v, size_type pos = 0 ) const nssv_noexcept // (1)
+ {
+ return pos >= size()
+ ? npos
+ : to_pos( std::find_if( cbegin() + pos, cend(), not_in_view( v ) ) );
+ }
+
+ nssv_constexpr size_type find_first_not_of( CharT c, size_type pos = 0 ) const nssv_noexcept // (2)
+ {
+ return find_first_not_of( basic_string_view( &c, 1 ), pos );
+ }
+
+ nssv_constexpr size_type find_first_not_of( CharT const * s, size_type pos, size_type count ) const // (3)
+ {
+ return find_first_not_of( basic_string_view( s, count ), pos );
+ }
+
+ nssv_constexpr size_type find_first_not_of( CharT const * s, size_type pos = 0 ) const // (4)
+ {
+ return find_first_not_of( basic_string_view( s ), pos );
+ }
+
+ // find_last_not_of(), 4x:
+
+ nssv_constexpr size_type find_last_not_of( basic_string_view v, size_type pos = npos ) const nssv_noexcept // (1)
+ {
+ return empty()
+ ? npos
+ : pos >= size()
+ ? find_last_not_of( v, size() - 1 )
+ : to_pos( std::find_if( const_reverse_iterator( cbegin() + pos + 1 ), crend(), not_in_view( v ) ) );
+ }
+
+ nssv_constexpr size_type find_last_not_of( CharT c, size_type pos = npos ) const nssv_noexcept // (2)
+ {
+ return find_last_not_of( basic_string_view( &c, 1 ), pos );
+ }
+
+ nssv_constexpr size_type find_last_not_of( CharT const * s, size_type pos, size_type count ) const // (3)
+ {
+ return find_last_not_of( basic_string_view( s, count ), pos );
+ }
+
+ nssv_constexpr size_type find_last_not_of( CharT const * s, size_type pos = npos ) const // (4)
+ {
+ return find_last_not_of( basic_string_view( s ), pos );
+ }
+
+ // Constants:
+
+#if nssv_CPP17_OR_GREATER
+ static nssv_constexpr size_type npos = size_type(-1);
+#elif nssv_CPP11_OR_GREATER
+ enum : size_type { npos = size_type(-1) };
+#else
+ enum { npos = size_type(-1) };
+#endif
+
+private:
+ struct not_in_view
+ {
+ const basic_string_view v;
+
+ nssv_constexpr explicit not_in_view( basic_string_view v_ ) : v( v_ ) {}
+
+ nssv_constexpr bool operator()( CharT c ) const
+ {
+ return npos == v.find_first_of( c );
+ }
+ };
+
+ nssv_constexpr size_type to_pos( const_iterator it ) const
+ {
+ return it == cend() ? npos : size_type( it - cbegin() );
+ }
+
+ nssv_constexpr size_type to_pos( const_reverse_iterator it ) const
+ {
+ return it == crend() ? npos : size_type( crend() - it - 1 );
+ }
+
+ nssv_constexpr const_reference data_at( size_type pos ) const
+ {
+#if nssv_BETWEEN( nssv_COMPILER_GNUC_VERSION, 1, 500 )
+ return data_[pos];
+#else
+ return assert( pos < size() ), data_[pos];
+#endif
+ }
+
+private:
+ const_pointer data_;
+ size_type size_;
+
+public:
+#if nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS
+
+ template< class Allocator >
+ basic_string_view( std::basic_string<CharT, Traits, Allocator> const & s ) nssv_noexcept
+ : data_( s.data() )
+ , size_( s.size() )
+ {}
+
+#if nssv_HAVE_EXPLICIT_CONVERSION
+
+ template< class Allocator >
+ explicit operator std::basic_string<CharT, Traits, Allocator>() const
+ {
+ return to_string( Allocator() );
+ }
+
+#endif // nssv_HAVE_EXPLICIT_CONVERSION
+
+#if nssv_CPP11_OR_GREATER
+
+ template< class Allocator = std::allocator<CharT> >
+ std::basic_string<CharT, Traits, Allocator>
+ to_string( Allocator const & a = Allocator() ) const
+ {
+ return std::basic_string<CharT, Traits, Allocator>( begin(), end(), a );
+ }
+
+#else
+
+ std::basic_string<CharT, Traits>
+ to_string() const
+ {
+ return std::basic_string<CharT, Traits>( begin(), end() );
+ }
+
+ template< class Allocator >
+ std::basic_string<CharT, Traits, Allocator>
+ to_string( Allocator const & a ) const
+ {
+ return std::basic_string<CharT, Traits, Allocator>( begin(), end(), a );
+ }
+
+#endif // nssv_CPP11_OR_GREATER
+
+#endif // nssv_CONFIG_CONVERSION_STD_STRING_CLASS_METHODS
+};
+
+//
+// Non-member functions:
+//
+
+// 24.4.3 Non-member comparison functions:
+// lexicographically compare two string views (function template):
+
+template< class CharT, class Traits >
+nssv_constexpr bool operator== (
+ basic_string_view <CharT, Traits> lhs,
+ basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; }
+
+template< class CharT, class Traits >
+nssv_constexpr bool operator!= (
+ basic_string_view <CharT, Traits> lhs,
+ basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+template< class CharT, class Traits >
+nssv_constexpr bool operator< (
+ basic_string_view <CharT, Traits> lhs,
+ basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) < 0; }
+
+template< class CharT, class Traits >
+nssv_constexpr bool operator<= (
+ basic_string_view <CharT, Traits> lhs,
+ basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) <= 0; }
+
+template< class CharT, class Traits >
+nssv_constexpr bool operator> (
+ basic_string_view <CharT, Traits> lhs,
+ basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) > 0; }
+
+template< class CharT, class Traits >
+nssv_constexpr bool operator>= (
+ basic_string_view <CharT, Traits> lhs,
+ basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) >= 0; }
+
+// Let S be basic_string_view<CharT, Traits>, and sv be an instance of S.
+// Implementations shall provide sufficient additional overloads marked
+// constexpr and noexcept so that an object t with an implicit conversion
+// to S can be compared according to Table 67.
+
+#if ! nssv_CPP11_OR_GREATER || nssv_BETWEEN( nssv_COMPILER_MSVC_VERSION, 100, 141 )
+
+// accommodate for older compilers:
+
+// ==
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator==(
+ basic_string_view<CharT, Traits> lhs,
+ CharT const * rhs ) nssv_noexcept
+{ return lhs.size() == detail::length( rhs ) && lhs.compare( rhs ) == 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator==(
+ CharT const * lhs,
+ basic_string_view<CharT, Traits> rhs ) nssv_noexcept
+{ return detail::length( lhs ) == rhs.size() && rhs.compare( lhs ) == 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator==(
+ basic_string_view<CharT, Traits> lhs,
+ std::basic_string<CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator==(
+ std::basic_string<CharT, Traits> rhs,
+ basic_string_view<CharT, Traits> lhs ) nssv_noexcept
+{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; }
+
+// !=
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator!=(
+ basic_string_view<CharT, Traits> lhs,
+ CharT const * rhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator!=(
+ CharT const * lhs,
+ basic_string_view<CharT, Traits> rhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator!=(
+ basic_string_view<CharT, Traits> lhs,
+ std::basic_string<CharT, Traits> rhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator!=(
+ std::basic_string<CharT, Traits> rhs,
+ basic_string_view<CharT, Traits> lhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+// <
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<(
+ basic_string_view<CharT, Traits> lhs,
+ CharT const * rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) < 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<(
+ CharT const * lhs,
+ basic_string_view<CharT, Traits> rhs ) nssv_noexcept
+{ return rhs.compare( lhs ) > 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<(
+ basic_string_view<CharT, Traits> lhs,
+ std::basic_string<CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) < 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<(
+ std::basic_string<CharT, Traits> rhs,
+ basic_string_view<CharT, Traits> lhs ) nssv_noexcept
+{ return rhs.compare( lhs ) > 0; }
+
+// <=
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<=(
+ basic_string_view<CharT, Traits> lhs,
+ CharT const * rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) <= 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<=(
+ CharT const * lhs,
+ basic_string_view<CharT, Traits> rhs ) nssv_noexcept
+{ return rhs.compare( lhs ) >= 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<=(
+ basic_string_view<CharT, Traits> lhs,
+ std::basic_string<CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) <= 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator<=(
+ std::basic_string<CharT, Traits> rhs,
+ basic_string_view<CharT, Traits> lhs ) nssv_noexcept
+{ return rhs.compare( lhs ) >= 0; }
+
+// >
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>(
+ basic_string_view<CharT, Traits> lhs,
+ CharT const * rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) > 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>(
+ CharT const * lhs,
+ basic_string_view<CharT, Traits> rhs ) nssv_noexcept
+{ return rhs.compare( lhs ) < 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>(
+ basic_string_view<CharT, Traits> lhs,
+ std::basic_string<CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) > 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>(
+ std::basic_string<CharT, Traits> rhs,
+ basic_string_view<CharT, Traits> lhs ) nssv_noexcept
+{ return rhs.compare( lhs ) < 0; }
+
+// >=
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>=(
+ basic_string_view<CharT, Traits> lhs,
+ CharT const * rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) >= 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>=(
+ CharT const * lhs,
+ basic_string_view<CharT, Traits> rhs ) nssv_noexcept
+{ return rhs.compare( lhs ) <= 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>=(
+ basic_string_view<CharT, Traits> lhs,
+ std::basic_string<CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) >= 0; }
+
+template< class CharT, class Traits>
+nssv_constexpr bool operator>=(
+ std::basic_string<CharT, Traits> rhs,
+ basic_string_view<CharT, Traits> lhs ) nssv_noexcept
+{ return rhs.compare( lhs ) <= 0; }
+
+#else // newer compilers:
+
+#define nssv_BASIC_STRING_VIEW_I(T,U) typename std::decay< basic_string_view<T,U> >::type
+
+#if defined(_MSC_VER) // issue 40
+# define nssv_MSVC_ORDER(x) , int=x
+#else
+# define nssv_MSVC_ORDER(x) /*, int=x*/
+#endif
+
+// ==
+
+template< class CharT, class Traits nssv_MSVC_ORDER(1) >
+nssv_constexpr bool operator==(
+ basic_string_view <CharT, Traits> lhs,
+ nssv_BASIC_STRING_VIEW_I(CharT, Traits) rhs ) nssv_noexcept
+{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; }
+
+template< class CharT, class Traits nssv_MSVC_ORDER(2) >
+nssv_constexpr bool operator==(
+ nssv_BASIC_STRING_VIEW_I(CharT, Traits) lhs,
+ basic_string_view <CharT, Traits> rhs ) nssv_noexcept
+{ return lhs.size() == rhs.size() && lhs.compare( rhs ) == 0; }
+
+// !=
+
+template< class CharT, class Traits nssv_MSVC_ORDER(1) >
+nssv_constexpr bool operator!= (
+ basic_string_view < CharT, Traits > lhs,
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+template< class CharT, class Traits nssv_MSVC_ORDER(2) >
+nssv_constexpr bool operator!= (
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs,
+ basic_string_view < CharT, Traits > rhs ) nssv_noexcept
+{ return !( lhs == rhs ); }
+
+// <
+
+template< class CharT, class Traits nssv_MSVC_ORDER(1) >
+nssv_constexpr bool operator< (
+ basic_string_view < CharT, Traits > lhs,
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) < 0; }
+
+template< class CharT, class Traits nssv_MSVC_ORDER(2) >
+nssv_constexpr bool operator< (
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs,
+ basic_string_view < CharT, Traits > rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) < 0; }
+
+// <=
+
+template< class CharT, class Traits nssv_MSVC_ORDER(1) >
+nssv_constexpr bool operator<= (
+ basic_string_view < CharT, Traits > lhs,
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) <= 0; }
+
+template< class CharT, class Traits nssv_MSVC_ORDER(2) >
+nssv_constexpr bool operator<= (
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs,
+ basic_string_view < CharT, Traits > rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) <= 0; }
+
+// >
+
+template< class CharT, class Traits nssv_MSVC_ORDER(1) >
+nssv_constexpr bool operator> (
+ basic_string_view < CharT, Traits > lhs,
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) > 0; }
+
+template< class CharT, class Traits nssv_MSVC_ORDER(2) >
+nssv_constexpr bool operator> (
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs,
+ basic_string_view < CharT, Traits > rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) > 0; }
+
+// >=
+
+template< class CharT, class Traits nssv_MSVC_ORDER(1) >
+nssv_constexpr bool operator>= (
+ basic_string_view < CharT, Traits > lhs,
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) >= 0; }
+
+template< class CharT, class Traits nssv_MSVC_ORDER(2) >
+nssv_constexpr bool operator>= (
+ nssv_BASIC_STRING_VIEW_I( CharT, Traits ) lhs,
+ basic_string_view < CharT, Traits > rhs ) nssv_noexcept
+{ return lhs.compare( rhs ) >= 0; }
+
+#undef nssv_MSVC_ORDER
+#undef nssv_BASIC_STRING_VIEW_I
+
+#endif // compiler-dependent approach to comparisons
+
+// 24.4.4 Inserters and extractors:
+
+#if ! nssv_CONFIG_NO_STREAM_INSERTION
+
+namespace detail {
+
+template< class Stream >
+void write_padding( Stream & os, std::streamsize n )
+{
+ for ( std::streamsize i = 0; i < n; ++i )
+ os.rdbuf()->sputc( os.fill() );
+}
+
+template< class Stream, class View >
+Stream & write_to_stream( Stream & os, View const & sv )
+{
+ typename Stream::sentry sentry( os );
+
+ if ( !os )
+ return os;
+
+ const std::streamsize length = static_cast<std::streamsize>( sv.length() );
+
+ // Whether, and how, to pad:
+ const bool pad = ( length < os.width() );
+ const bool left_pad = pad && ( os.flags() & std::ios_base::adjustfield ) == std::ios_base::right;
+
+ if ( left_pad )
+ write_padding( os, os.width() - length );
+
+ // Write span characters:
+ os.rdbuf()->sputn( sv.begin(), length );
+
+ if ( pad && !left_pad )
+ write_padding( os, os.width() - length );
+
+ // Reset output stream width:
+ os.width( 0 );
+
+ return os;
+}
+
+} // namespace detail
+
+template< class CharT, class Traits >
+std::basic_ostream<CharT, Traits> &
+operator<<(
+ std::basic_ostream<CharT, Traits>& os,
+ basic_string_view <CharT, Traits> sv )
+{
+ return detail::write_to_stream( os, sv );
+}
+
+#endif // nssv_CONFIG_NO_STREAM_INSERTION
+
+// Several typedefs for common character types are provided:
+
+typedef basic_string_view<char> string_view;
+typedef basic_string_view<wchar_t> wstring_view;
+#if nssv_HAVE_WCHAR16_T
+typedef basic_string_view<char16_t> u16string_view;
+typedef basic_string_view<char32_t> u32string_view;
+#endif
+
+}} // namespace nonstd::sv_lite
+
+//
+// 24.4.6 Suffix for basic_string_view literals:
+//
+
+#if nssv_HAVE_USER_DEFINED_LITERALS
+
+namespace nonstd {
+nssv_inline_ns namespace literals {
+nssv_inline_ns namespace string_view_literals {
+
+#if nssv_CONFIG_STD_SV_OPERATOR && nssv_HAVE_STD_DEFINED_LITERALS
+
+nssv_constexpr nonstd::sv_lite::string_view operator "" sv( const char* str, size_t len ) nssv_noexcept // (1)
+{
+ return nonstd::sv_lite::string_view{ str, len };
+}
+
+nssv_constexpr nonstd::sv_lite::u16string_view operator "" sv( const char16_t* str, size_t len ) nssv_noexcept // (2)
+{
+ return nonstd::sv_lite::u16string_view{ str, len };
+}
+
+nssv_constexpr nonstd::sv_lite::u32string_view operator "" sv( const char32_t* str, size_t len ) nssv_noexcept // (3)
+{
+ return nonstd::sv_lite::u32string_view{ str, len };
+}
+
+nssv_constexpr nonstd::sv_lite::wstring_view operator "" sv( const wchar_t* str, size_t len ) nssv_noexcept // (4)
+{
+ return nonstd::sv_lite::wstring_view{ str, len };
+}
+
+#endif // nssv_CONFIG_STD_SV_OPERATOR && nssv_HAVE_STD_DEFINED_LITERALS
+
+#if nssv_CONFIG_USR_SV_OPERATOR
+
+nssv_constexpr nonstd::sv_lite::string_view operator "" _sv( const char* str, size_t len ) nssv_noexcept // (1)
+{
+ return nonstd::sv_lite::string_view{ str, len };
+}
+
+nssv_constexpr nonstd::sv_lite::u16string_view operator "" _sv( const char16_t* str, size_t len ) nssv_noexcept // (2)
+{
+ return nonstd::sv_lite::u16string_view{ str, len };
+}
+
+nssv_constexpr nonstd::sv_lite::u32string_view operator "" _sv( const char32_t* str, size_t len ) nssv_noexcept // (3)
+{
+ return nonstd::sv_lite::u32string_view{ str, len };
+}
+
+nssv_constexpr nonstd::sv_lite::wstring_view operator "" _sv( const wchar_t* str, size_t len ) nssv_noexcept // (4)
+{
+ return nonstd::sv_lite::wstring_view{ str, len };
+}
+
+#endif // nssv_CONFIG_USR_SV_OPERATOR
+
+}}} // namespace nonstd::literals::string_view_literals
+
+#endif
+
+//
+// Extensions for std::string:
+//
+
+#if nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS
+
+namespace nonstd {
+namespace sv_lite {
+
+// Exclude MSVC 14 (19.00): it yields ambiguous to_string():
+
+#if nssv_CPP11_OR_GREATER && nssv_COMPILER_MSVC_VERSION != 140
+
+template< class CharT, class Traits, class Allocator = std::allocator<CharT> >
+std::basic_string<CharT, Traits, Allocator>
+to_string( basic_string_view<CharT, Traits> v, Allocator const & a = Allocator() )
+{
+ return std::basic_string<CharT,Traits, Allocator>( v.begin(), v.end(), a );
+}
+
+#else
+
+template< class CharT, class Traits >
+std::basic_string<CharT, Traits>
+to_string( basic_string_view<CharT, Traits> v )
+{
+ return std::basic_string<CharT, Traits>( v.begin(), v.end() );
+}
+
+template< class CharT, class Traits, class Allocator >
+std::basic_string<CharT, Traits, Allocator>
+to_string( basic_string_view<CharT, Traits> v, Allocator const & a )
+{
+ return std::basic_string<CharT, Traits, Allocator>( v.begin(), v.end(), a );
+}
+
+#endif // nssv_CPP11_OR_GREATER
+
+template< class CharT, class Traits, class Allocator >
+basic_string_view<CharT, Traits>
+to_string_view( std::basic_string<CharT, Traits, Allocator> const & s )
+{
+ return basic_string_view<CharT, Traits>( s.data(), s.size() );
+}
+
+}} // namespace nonstd::sv_lite
+
+#endif // nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS
+
+//
+// make types and algorithms available in namespace nonstd:
+//
+
+namespace nonstd {
+
+using sv_lite::basic_string_view;
+using sv_lite::string_view;
+using sv_lite::wstring_view;
+
+#if nssv_HAVE_WCHAR16_T
+using sv_lite::u16string_view;
+#endif
+#if nssv_HAVE_WCHAR32_T
+using sv_lite::u32string_view;
+#endif
+
+// literal "sv"
+
+using sv_lite::operator==;
+using sv_lite::operator!=;
+using sv_lite::operator<;
+using sv_lite::operator<=;
+using sv_lite::operator>;
+using sv_lite::operator>=;
+
+#if ! nssv_CONFIG_NO_STREAM_INSERTION
+using sv_lite::operator<<;
+#endif
+
+#if nssv_CONFIG_CONVERSION_STD_STRING_FREE_FUNCTIONS
+using sv_lite::to_string;
+using sv_lite::to_string_view;
+#endif
+
+} // namespace nonstd
+
+// 24.4.5 Hash support (C++11):
+
+// Note: The hash value of a string view object is equal to the hash value of
+// the corresponding string object.
+
+#if nssv_HAVE_STD_HASH
+
+#include <functional>
+
+namespace std {
+
+template<>
+struct hash< nonstd::string_view >
+{
+public:
+ std::size_t operator()( nonstd::string_view v ) const nssv_noexcept
+ {
+ return std::hash<std::string>()( std::string( v.data(), v.size() ) );
+ }
+};
+
+template<>
+struct hash< nonstd::wstring_view >
+{
+public:
+ std::size_t operator()( nonstd::wstring_view v ) const nssv_noexcept
+ {
+ return std::hash<std::wstring>()( std::wstring( v.data(), v.size() ) );
+ }
+};
+
+template<>
+struct hash< nonstd::u16string_view >
+{
+public:
+ std::size_t operator()( nonstd::u16string_view v ) const nssv_noexcept
+ {
+ return std::hash<std::u16string>()( std::u16string( v.data(), v.size() ) );
+ }
+};
+
+template<>
+struct hash< nonstd::u32string_view >
+{
+public:
+ std::size_t operator()( nonstd::u32string_view v ) const nssv_noexcept
+ {
+ return std::hash<std::u32string>()( std::u32string( v.data(), v.size() ) );
+ }
+};
+
+} // namespace std
+
+#endif // nssv_HAVE_STD_HASH
+
+nssv_RESTORE_WARNINGS()
+
+#endif // nssv_HAVE_STD_STRING_VIEW
+#endif // NONSTD_SV_LITE_H_INCLUDED
+//!
+//! termcolor
+//! ~~~~~~~~~
+//!
+//! termcolor is a header-only c++ library for printing colored messages
+//! to the terminal. Written just for fun with a help of the Force.
+//!
+//! :copyright: (c) 2013 by Ihor Kalnytskyi
+//! :license: BSD, see LICENSE for details
+//!
+
+#ifndef TERMCOLOR_HPP_
+#define TERMCOLOR_HPP_
+
+// the following snippet of code detects the current OS and
+// defines the appropriate macro that is used to wrap some
+// platform specific things
+#if defined(_WIN32) || defined(_WIN64)
+#define TERMCOLOR_OS_WINDOWS
+#elif defined(__APPLE__)
+#define TERMCOLOR_OS_MACOS
+#elif defined(__unix__) || defined(__unix)
+#define TERMCOLOR_OS_LINUX
+#else
+#error unsupported platform
+#endif
+
+// This headers provides the `isatty()`/`fileno()` functions,
+// which are used for testing whether a standart stream refers
+// to the terminal. As for Windows, we also need WinApi funcs
+// for changing colors attributes of the terminal.
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+#include <unistd.h>
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#include <io.h>
+#include <windows.h>
+#endif
+
+#include <cstdio>
+#include <iostream>
+
+namespace termcolor {
+// Forward declaration of the `_internal` namespace.
+// All comments are below.
+namespace _internal {
+// An index to be used to access a private storage of I/O streams. See
+// colorize / nocolorize I/O manipulators for details.
+static int colorize_index = std::ios_base::xalloc();
+
+inline FILE *get_standard_stream(const std::ostream &stream);
+inline bool is_colorized(std::ostream &stream);
+inline bool is_atty(const std::ostream &stream);
+
+#if defined(TERMCOLOR_OS_WINDOWS)
+inline void win_change_attributes(std::ostream &stream, int foreground, int background = -1);
+#endif
+} // namespace _internal
+
+inline std::ostream &colorize(std::ostream &stream) {
+ stream.iword(_internal::colorize_index) = 1L;
+ return stream;
+}
+
+inline std::ostream &nocolorize(std::ostream &stream) {
+ stream.iword(_internal::colorize_index) = 0L;
+ return stream;
+}
+
+inline std::ostream &reset(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[00m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, -1, -1);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &bold(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[1m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &dark(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[2m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &italic(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[3m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &underline(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[4m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &blink(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[5m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &reverse(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[7m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &concealed(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[8m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &crossed(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[9m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &grey(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[30m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream,
+ 0 // grey (black)
+ );
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &red(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[31m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, FOREGROUND_RED);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &green(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[32m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, FOREGROUND_GREEN);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &yellow(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[33m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, FOREGROUND_GREEN | FOREGROUND_RED);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &blue(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[34m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, FOREGROUND_BLUE);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &magenta(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[35m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, FOREGROUND_BLUE | FOREGROUND_RED);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &cyan(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[36m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, FOREGROUND_BLUE | FOREGROUND_GREEN);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &white(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[37m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &on_grey(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[40m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, -1,
+ 0 // grey (black)
+ );
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &on_red(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[41m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, -1, BACKGROUND_RED);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &on_green(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[42m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, -1, BACKGROUND_GREEN);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &on_yellow(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[43m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, -1, BACKGROUND_GREEN | BACKGROUND_RED);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &on_blue(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[44m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, -1, BACKGROUND_BLUE);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &on_magenta(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[45m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, -1, BACKGROUND_BLUE | BACKGROUND_RED);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &on_cyan(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[46m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, -1, BACKGROUND_GREEN | BACKGROUND_BLUE);
+#endif
+ }
+ return stream;
+}
+
+inline std::ostream &on_white(std::ostream &stream) {
+ if (_internal::is_colorized(stream)) {
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ stream << "\033[47m";
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ _internal::win_change_attributes(stream, -1,
+ BACKGROUND_GREEN | BACKGROUND_BLUE | BACKGROUND_RED);
+#endif
+ }
+
+ return stream;
+}
+
+//! Since C++ hasn't a way to hide something in the header from
+//! the outer access, I have to introduce this namespace which
+//! is used for internal purpose and should't be access from
+//! the user code.
+namespace _internal {
+//! Since C++ hasn't a true way to extract stream handler
+//! from the a given `std::ostream` object, I have to write
+//! this kind of hack.
+inline FILE *get_standard_stream(const std::ostream &stream) {
+ if (&stream == &std::cout)
+ return stdout;
+ else if ((&stream == &std::cerr) || (&stream == &std::clog))
+ return stderr;
+
+ return 0;
+}
+
+// Say whether a given stream should be colorized or not. It's always
+// true for ATTY streams and may be true for streams marked with
+// colorize flag.
+inline bool is_colorized(std::ostream &stream) {
+ return is_atty(stream) || static_cast<bool>(stream.iword(colorize_index));
+}
+
+//! Test whether a given `std::ostream` object refers to
+//! a terminal.
+inline bool is_atty(const std::ostream &stream) {
+ FILE *std_stream = get_standard_stream(stream);
+
+ // Unfortunately, fileno() ends with segmentation fault
+ // if invalid file descriptor is passed. So we need to
+ // handle this case gracefully and assume it's not a tty
+ // if standard stream is not detected, and 0 is returned.
+ if (!std_stream)
+ return false;
+
+#if defined(TERMCOLOR_OS_MACOS) || defined(TERMCOLOR_OS_LINUX)
+ return ::isatty(fileno(std_stream));
+#elif defined(TERMCOLOR_OS_WINDOWS)
+ return ::_isatty(_fileno(std_stream));
+#endif
+}
+
+#if defined(TERMCOLOR_OS_WINDOWS)
+//! Change Windows Terminal colors attribute. If some
+//! parameter is `-1` then attribute won't changed.
+inline void win_change_attributes(std::ostream &stream, int foreground, int background) {
+ // yeah, i know.. it's ugly, it's windows.
+ static WORD defaultAttributes = 0;
+
+ // Windows doesn't have ANSI escape sequences and so we use special
+ // API to change Terminal output color. That means we can't
+ // manipulate colors by means of "std::stringstream" and hence
+ // should do nothing in this case.
+ if (!_internal::is_atty(stream))
+ return;
+
+ // get terminal handle
+ HANDLE hTerminal = INVALID_HANDLE_VALUE;
+ if (&stream == &std::cout)
+ hTerminal = GetStdHandle(STD_OUTPUT_HANDLE);
+ else if (&stream == &std::cerr)
+ hTerminal = GetStdHandle(STD_ERROR_HANDLE);
+
+ // save default terminal attributes if it unsaved
+ if (!defaultAttributes) {
+ CONSOLE_SCREEN_BUFFER_INFO info;
+ if (!GetConsoleScreenBufferInfo(hTerminal, &info))
+ return;
+ defaultAttributes = info.wAttributes;
+ }
+
+ // restore all default settings
+ if (foreground == -1 && background == -1) {
+ SetConsoleTextAttribute(hTerminal, defaultAttributes);
+ return;
+ }
+
+ // get current settings
+ CONSOLE_SCREEN_BUFFER_INFO info;
+ if (!GetConsoleScreenBufferInfo(hTerminal, &info))
+ return;
+
+ if (foreground != -1) {
+ info.wAttributes &= ~(info.wAttributes & 0x0F);
+ info.wAttributes |= static_cast<WORD>(foreground);
+ }
+
+ if (background != -1) {
+ info.wAttributes &= ~(info.wAttributes & 0xF0);
+ info.wAttributes |= static_cast<WORD>(background);
+ }
+
+ SetConsoleTextAttribute(hTerminal, info.wAttributes);
+}
+#endif // TERMCOLOR_OS_WINDOWS
+
+} // namespace _internal
+
+} // namespace termcolor
+
+#undef TERMCOLOR_OS_WINDOWS
+#undef TERMCOLOR_OS_MACOS
+#undef TERMCOLOR_OS_LINUX
+
+#endif // TERMCOLOR_HPP_
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <algorithm>
+#include <cstdint>
+#include <string>
+
+#include <clocale>
+#include <locale>
+
+#include <cstdlib>
+// #include <tabulate/termcolor.hpp>
+#include <wchar.h>
+
+namespace tabulate {
+
+#if defined(__unix__) || defined(__unix) || defined(__APPLE__)
+inline int get_wcswidth(const std::string &string, const std::string &locale,
+ size_t max_column_width) {
+ if (string.size() == 0)
+ return 0;
+
+ // The behavior of wcswidth() depends on the LC_CTYPE category of the current
+ // locale. Set the current locale based on cell properties before computing
+ // width
+ auto old_locale = std::locale::global(std::locale(locale));
+
+ // Convert from narrow std::string to wide string
+ wchar_t *wide_string = new wchar_t[string.size()];
+ std::mbstowcs(wide_string, string.c_str(), string.size());
+
+ // Compute display width of wide string
+ int result = wcswidth(wide_string, max_column_width);
+ delete[] wide_string;
+
+ // Restore old locale
+ std::locale::global(old_locale);
+
+ return result;
+}
+#endif
+
+inline size_t get_sequence_length(const std::string &text, const std::string &locale,
+ bool is_multi_byte_character_support_enabled) {
+ if (!is_multi_byte_character_support_enabled)
+ return text.length();
+
+#if defined(_WIN32) || defined(_WIN64)
+ (void)locale; // unused parameter
+ return (text.length() - std::count_if(text.begin(), text.end(),
+ [](char c) -> bool { return (c & 0xC0) == 0x80; }));
+#elif defined(__unix__) || defined(__unix) || defined(__APPLE__)
+ auto result = get_wcswidth(text, locale, text.size());
+ if (result >= 0)
+ return result;
+ else
+ return (text.length() - std::count_if(text.begin(), text.end(),
+ [](char c) -> bool { return (c & 0xC0) == 0x80; }));
+#endif
+}
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+// #include <tabulate/termcolor.hpp>
+
+namespace tabulate {
+
+enum class Color { none, grey, red, green, yellow, blue, magenta, cyan, white };
+}
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+
+namespace tabulate {
+
+enum class FontAlign { left, right, center };
+}
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+
+namespace tabulate {
+
+enum class FontStyle { bold, dark, italic, underline, blink, reverse, concealed, crossed };
+}
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <iostream>
+#include <memory>
+#include <string>
+// #include <tabulate/format.hpp>
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+#include <algorithm>
+#include <cctype>
+#include <cstddef>
+#include <sstream>
+#include <string>
+// #include <tabulate/color.hpp>
+// #include <tabulate/font_align.hpp>
+// #include <tabulate/font_style.hpp>
+// #include <tabulate/utf8.hpp>
+
+#if __cplusplus >= 201703L
+#include <optional>
+using std::optional;
+#else
+// #include <tabulate/optional_lite.hpp>
+using nonstd::optional;
+#endif
+
+#include <vector>
+
+namespace tabulate {
+
+class Format {
+public:
+ Format &width(size_t value) {
+ width_ = value;
+ return *this;
+ }
+
+ Format &height(size_t value) {
+ height_ = value;
+ return *this;
+ }
+
+ Format &padding(size_t value) {
+ padding_left_ = value;
+ padding_right_ = value;
+ padding_top_ = value;
+ padding_bottom_ = value;
+ return *this;
+ }
+
+ Format &padding_left(size_t value) {
+ padding_left_ = value;
+ return *this;
+ }
+
+ Format &padding_right(size_t value) {
+ padding_right_ = value;
+ return *this;
+ }
+
+ Format &padding_top(size_t value) {
+ padding_top_ = value;
+ return *this;
+ }
+
+ Format &padding_bottom(size_t value) {
+ padding_bottom_ = value;
+ return *this;
+ }
+
+ Format &border(const std::string &value) {
+ border_left_ = value;
+ border_right_ = value;
+ border_top_ = value;
+ border_bottom_ = value;
+ return *this;
+ }
+
+ Format &border_color(Color value) {
+ border_left_color_ = value;
+ border_right_color_ = value;
+ border_top_color_ = value;
+ border_bottom_color_ = value;
+ return *this;
+ }
+
+ Format &border_background_color(Color value) {
+ border_left_background_color_ = value;
+ border_right_background_color_ = value;
+ border_top_background_color_ = value;
+ border_bottom_background_color_ = value;
+ return *this;
+ }
+
+ Format &border_left(const std::string &value) {
+ border_left_ = value;
+ return *this;
+ }
+
+ Format &border_left_color(Color value) {
+ border_left_color_ = value;
+ return *this;
+ }
+
+ Format &border_left_background_color(Color value) {
+ border_left_background_color_ = value;
+ return *this;
+ }
+
+ Format &border_right(const std::string &value) {
+ border_right_ = value;
+ return *this;
+ }
+
+ Format &border_right_color(Color value) {
+ border_right_color_ = value;
+ return *this;
+ }
+
+ Format &border_right_background_color(Color value) {
+ border_right_background_color_ = value;
+ return *this;
+ }
+
+ Format &border_top(const std::string &value) {
+ border_top_ = value;
+ return *this;
+ }
+
+ Format &border_top_color(Color value) {
+ border_top_color_ = value;
+ return *this;
+ }
+
+ Format &border_top_background_color(Color value) {
+ border_top_background_color_ = value;
+ return *this;
+ }
+
+ Format &border_bottom(const std::string &value) {
+ border_bottom_ = value;
+ return *this;
+ }
+
+ Format &border_bottom_color(Color value) {
+ border_bottom_color_ = value;
+ return *this;
+ }
+
+ Format &border_bottom_background_color(Color value) {
+ border_bottom_background_color_ = value;
+ return *this;
+ }
+
+ Format &show_border() {
+ show_border_top_ = true;
+ show_border_bottom_ = true;
+ show_border_left_ = true;
+ show_border_right_ = true;
+ return *this;
+ }
+
+ Format &hide_border() {
+ show_border_top_ = false;
+ show_border_bottom_ = false;
+ show_border_left_ = false;
+ show_border_right_ = false;
+ return *this;
+ }
+
+ Format &show_border_top() {
+ show_border_top_ = true;
+ return *this;
+ }
+
+ Format &hide_border_top() {
+ show_border_top_ = false;
+ return *this;
+ }
+
+ Format &show_border_bottom() {
+ show_border_bottom_ = true;
+ return *this;
+ }
+
+ Format &hide_border_bottom() {
+ show_border_bottom_ = false;
+ return *this;
+ }
+
+ Format &show_border_left() {
+ show_border_left_ = true;
+ return *this;
+ }
+
+ Format &hide_border_left() {
+ show_border_left_ = false;
+ return *this;
+ }
+
+ Format &show_border_right() {
+ show_border_right_ = true;
+ return *this;
+ }
+
+ Format &hide_border_right() {
+ show_border_right_ = false;
+ return *this;
+ }
+
+ Format &corner(const std::string &value) {
+ corner_top_left_ = value;
+ corner_top_right_ = value;
+ corner_bottom_left_ = value;
+ corner_bottom_right_ = value;
+ return *this;
+ }
+
+ Format &corner_color(Color value) {
+ corner_top_left_color_ = value;
+ corner_top_right_color_ = value;
+ corner_bottom_left_color_ = value;
+ corner_bottom_right_color_ = value;
+ return *this;
+ }
+
+ Format &corner_background_color(Color value) {
+ corner_top_left_background_color_ = value;
+ corner_top_right_background_color_ = value;
+ corner_bottom_left_background_color_ = value;
+ corner_bottom_right_background_color_ = value;
+ return *this;
+ }
+
+ Format &corner_top_left(const std::string &value) {
+ corner_top_left_ = value;
+ return *this;
+ }
+
+ Format &corner_top_left_color(Color value) {
+ corner_top_left_color_ = value;
+ return *this;
+ }
+
+ Format &corner_top_left_background_color(Color value) {
+ corner_top_left_background_color_ = value;
+ return *this;
+ }
+
+ Format &corner_top_right(const std::string &value) {
+ corner_top_right_ = value;
+ return *this;
+ }
+
+ Format &corner_top_right_color(Color value) {
+ corner_top_right_color_ = value;
+ return *this;
+ }
+
+ Format &corner_top_right_background_color(Color value) {
+ corner_top_right_background_color_ = value;
+ return *this;
+ }
+
+ Format &corner_bottom_left(const std::string &value) {
+ corner_bottom_left_ = value;
+ return *this;
+ }
+
+ Format &corner_bottom_left_color(Color value) {
+ corner_bottom_left_color_ = value;
+ return *this;
+ }
+
+ Format &corner_bottom_left_background_color(Color value) {
+ corner_bottom_left_background_color_ = value;
+ return *this;
+ }
+
+ Format &corner_bottom_right(const std::string &value) {
+ corner_bottom_right_ = value;
+ return *this;
+ }
+
+ Format &corner_bottom_right_color(Color value) {
+ corner_bottom_right_color_ = value;
+ return *this;
+ }
+
+ Format &corner_bottom_right_background_color(Color value) {
+ corner_bottom_right_background_color_ = value;
+ return *this;
+ }
+
+ Format &column_separator(const std::string &value) {
+ column_separator_ = value;
+ return *this;
+ }
+
+ Format &column_separator_color(Color value) {
+ column_separator_color_ = value;
+ return *this;
+ }
+
+ Format &column_separator_background_color(Color value) {
+ column_separator_background_color_ = value;
+ return *this;
+ }
+
+ Format &font_align(FontAlign value) {
+ font_align_ = value;
+ return *this;
+ }
+
+ Format &font_style(const std::vector<FontStyle> &style) {
+ if (font_style_.has_value()) {
+ for (auto &s : style)
+ font_style_->push_back(s);
+ } else {
+ font_style_ = style;
+ }
+ return *this;
+ }
+
+ Format &font_color(Color value) {
+ font_color_ = value;
+ return *this;
+ }
+
+ Format &font_background_color(Color value) {
+ font_background_color_ = value;
+ return *this;
+ }
+
+ Format &color(Color value) {
+ font_color(value);
+ border_color(value);
+ corner_color(value);
+ return *this;
+ }
+
+ Format &background_color(Color value) {
+ font_background_color(value);
+ border_background_color(value);
+ corner_background_color(value);
+ return *this;
+ }
+
+ Format &multi_byte_characters(bool value) {
+ multi_byte_characters_ = value;
+ return *this;
+ }
+
+ Format &locale(const std::string &value) {
+ locale_ = value;
+ return *this;
+ }
+
+ // Apply word wrap
+ // Given an input string and a line length, this will insert \n
+ // in strategic places in input string and apply word wrapping
+ static std::string word_wrap(const std::string &str, size_t width, const std::string &locale,
+ bool is_multi_byte_character_support_enabled) {
+ std::vector<std::string> words = explode_string(str, {" ", "-", "\t"});
+ size_t current_line_length = 0;
+ std::string result;
+
+ for (size_t i = 0; i < words.size(); ++i) {
+ std::string word = words[i];
+ // If adding the new word to the current line would be too long,
+ // then put it on a new line (and split it up if it's too long).
+ if (current_line_length +
+ get_sequence_length(word, locale, is_multi_byte_character_support_enabled) >
+ width) {
+ // Only move down to a new line if we have text on the current line.
+ // Avoids situation where wrapped whitespace causes emptylines in text.
+ if (current_line_length > 0) {
+ result += '\n';
+ current_line_length = 0;
+ }
+
+ // If the current word is too long to fit on a line even on it's own
+ // then split the word up.
+ while (get_sequence_length(word, locale, is_multi_byte_character_support_enabled) > width) {
+ result += word.substr(0, width - 1) + "-";
+ word = word.substr(width - 1);
+ result += '\n';
+ }
+
+ // Remove leading whitespace from the word so the new line starts flush
+ // to the left.
+ word = trim_left(word);
+ }
+ result += word;
+ current_line_length +=
+ get_sequence_length(word, locale, is_multi_byte_character_support_enabled);
+ }
+ return result;
+ }
+
+ static std::vector<std::string> split_lines(const std::string &text, const std::string &delimiter,
+ const std::string &locale,
+ bool is_multi_byte_character_support_enabled) {
+ std::vector<std::string> result{};
+ std::string input = text;
+ size_t pos = 0;
+ std::string token;
+ while ((pos = input.find(delimiter)) != std::string::npos) {
+ token = input.substr(0, pos);
+ result.push_back(token);
+ input.erase(0, pos + delimiter.length());
+ }
+ if (get_sequence_length(input, locale, is_multi_byte_character_support_enabled))
+ result.push_back(input);
+ return result;
+ };
+
+ // Merge two formats
+ // first has higher precedence
+ // e.g., first = cell-level formatting and
+ // second = row-level formatting
+ // Result has attributes of both with cell-level
+ // formatting taking precedence
+ static Format merge(Format first, Format second) {
+ Format result;
+
+ // Width and height
+ if (first.width_.has_value())
+ result.width_ = first.width_;
+ else
+ result.width_ = second.width_;
+
+ if (first.height_.has_value())
+ result.height_ = first.height_;
+ else
+ result.height_ = second.height_;
+
+ // Font styling
+ if (first.font_align_.has_value())
+ result.font_align_ = first.font_align_;
+ else
+ result.font_align_ = second.font_align_;
+
+ if (first.font_style_.has_value()) {
+ // Merge font styles using std::set_union
+ std::vector<FontStyle> merged_font_style(first.font_style_->size() +
+ second.font_style_->size());
+#if defined(_WIN32) || defined(_WIN64)
+ // Fixes error in Windows - Sequence not ordered
+ std::sort(first.font_style_->begin(), first.font_style_->end());
+ std::sort(second.font_style_->begin(), second.font_style_->end());
+#endif
+ std::set_union(first.font_style_->begin(), first.font_style_->end(),
+ second.font_style_->begin(), second.font_style_->end(),
+ merged_font_style.begin());
+ result.font_style_ = merged_font_style;
+ } else
+ result.font_style_ = second.font_style_;
+
+ if (first.font_color_.has_value())
+ result.font_color_ = first.font_color_;
+ else
+ result.font_color_ = second.font_color_;
+
+ if (first.font_background_color_.has_value())
+ result.font_background_color_ = first.font_background_color_;
+ else
+ result.font_background_color_ = second.font_background_color_;
+
+ // Padding
+ if (first.padding_left_.has_value())
+ result.padding_left_ = first.padding_left_;
+ else
+ result.padding_left_ = second.padding_left_;
+
+ if (first.padding_top_.has_value())
+ result.padding_top_ = first.padding_top_;
+ else
+ result.padding_top_ = second.padding_top_;
+
+ if (first.padding_right_.has_value())
+ result.padding_right_ = first.padding_right_;
+ else
+ result.padding_right_ = second.padding_right_;
+
+ if (first.padding_bottom_.has_value())
+ result.padding_bottom_ = first.padding_bottom_;
+ else
+ result.padding_bottom_ = second.padding_bottom_;
+
+ // Border
+ if (first.border_left_.has_value())
+ result.border_left_ = first.border_left_;
+ else
+ result.border_left_ = second.border_left_;
+
+ if (first.border_left_color_.has_value())
+ result.border_left_color_ = first.border_left_color_;
+ else
+ result.border_left_color_ = second.border_left_color_;
+
+ if (first.border_left_background_color_.has_value())
+ result.border_left_background_color_ = first.border_left_background_color_;
+ else
+ result.border_left_background_color_ = second.border_left_background_color_;
+
+ if (first.border_top_.has_value())
+ result.border_top_ = first.border_top_;
+ else
+ result.border_top_ = second.border_top_;
+
+ if (first.border_top_color_.has_value())
+ result.border_top_color_ = first.border_top_color_;
+ else
+ result.border_top_color_ = second.border_top_color_;
+
+ if (first.border_top_background_color_.has_value())
+ result.border_top_background_color_ = first.border_top_background_color_;
+ else
+ result.border_top_background_color_ = second.border_top_background_color_;
+
+ if (first.border_bottom_.has_value())
+ result.border_bottom_ = first.border_bottom_;
+ else
+ result.border_bottom_ = second.border_bottom_;
+
+ if (first.border_bottom_color_.has_value())
+ result.border_bottom_color_ = first.border_bottom_color_;
+ else
+ result.border_bottom_color_ = second.border_bottom_color_;
+
+ if (first.border_bottom_background_color_.has_value())
+ result.border_bottom_background_color_ = first.border_bottom_background_color_;
+ else
+ result.border_bottom_background_color_ = second.border_bottom_background_color_;
+
+ if (first.border_right_.has_value())
+ result.border_right_ = first.border_right_;
+ else
+ result.border_right_ = second.border_right_;
+
+ if (first.border_right_color_.has_value())
+ result.border_right_color_ = first.border_right_color_;
+ else
+ result.border_right_color_ = second.border_right_color_;
+
+ if (first.border_right_background_color_.has_value())
+ result.border_right_background_color_ = first.border_right_background_color_;
+ else
+ result.border_right_background_color_ = second.border_right_background_color_;
+
+ if (first.show_border_top_.has_value())
+ result.show_border_top_ = first.show_border_top_;
+ else
+ result.show_border_top_ = second.show_border_top_;
+
+ if (first.show_border_bottom_.has_value())
+ result.show_border_bottom_ = first.show_border_bottom_;
+ else
+ result.show_border_bottom_ = second.show_border_bottom_;
+
+ if (first.show_border_left_.has_value())
+ result.show_border_left_ = first.show_border_left_;
+ else
+ result.show_border_left_ = second.show_border_left_;
+
+ if (first.show_border_right_.has_value())
+ result.show_border_right_ = first.show_border_right_;
+ else
+ result.show_border_right_ = second.show_border_right_;
+
+ // Corner
+ if (first.corner_top_left_.has_value())
+ result.corner_top_left_ = first.corner_top_left_;
+ else
+ result.corner_top_left_ = second.corner_top_left_;
+
+ if (first.corner_top_left_color_.has_value())
+ result.corner_top_left_color_ = first.corner_top_left_color_;
+ else
+ result.corner_top_left_color_ = second.corner_top_left_color_;
+
+ if (first.corner_top_left_background_color_.has_value())
+ result.corner_top_left_background_color_ = first.corner_top_left_background_color_;
+ else
+ result.corner_top_left_background_color_ = second.corner_top_left_background_color_;
+
+ if (first.corner_top_right_.has_value())
+ result.corner_top_right_ = first.corner_top_right_;
+ else
+ result.corner_top_right_ = second.corner_top_right_;
+
+ if (first.corner_top_right_color_.has_value())
+ result.corner_top_right_color_ = first.corner_top_right_color_;
+ else
+ result.corner_top_right_color_ = second.corner_top_right_color_;
+
+ if (first.corner_top_right_background_color_.has_value())
+ result.corner_top_right_background_color_ = first.corner_top_right_background_color_;
+ else
+ result.corner_top_right_background_color_ = second.corner_top_right_background_color_;
+
+ if (first.corner_bottom_left_.has_value())
+ result.corner_bottom_left_ = first.corner_bottom_left_;
+ else
+ result.corner_bottom_left_ = second.corner_bottom_left_;
+
+ if (first.corner_bottom_left_color_.has_value())
+ result.corner_bottom_left_color_ = first.corner_bottom_left_color_;
+ else
+ result.corner_bottom_left_color_ = second.corner_bottom_left_color_;
+
+ if (first.corner_bottom_left_background_color_.has_value())
+ result.corner_bottom_left_background_color_ = first.corner_bottom_left_background_color_;
+ else
+ result.corner_bottom_left_background_color_ = second.corner_bottom_left_background_color_;
+
+ if (first.corner_bottom_right_.has_value())
+ result.corner_bottom_right_ = first.corner_bottom_right_;
+ else
+ result.corner_bottom_right_ = second.corner_bottom_right_;
+
+ if (first.corner_bottom_right_color_.has_value())
+ result.corner_bottom_right_color_ = first.corner_bottom_right_color_;
+ else
+ result.corner_bottom_right_color_ = second.corner_bottom_right_color_;
+
+ if (first.corner_bottom_right_background_color_.has_value())
+ result.corner_bottom_right_background_color_ = first.corner_bottom_right_background_color_;
+ else
+ result.corner_bottom_right_background_color_ = second.corner_bottom_right_background_color_;
+
+ // Column separator
+ if (first.column_separator_.has_value())
+ result.column_separator_ = first.column_separator_;
+ else
+ result.column_separator_ = second.column_separator_;
+
+ if (first.column_separator_color_.has_value())
+ result.column_separator_color_ = first.column_separator_color_;
+ else
+ result.column_separator_color_ = second.column_separator_color_;
+
+ if (first.column_separator_background_color_.has_value())
+ result.column_separator_background_color_ = first.column_separator_background_color_;
+ else
+ result.column_separator_background_color_ = second.column_separator_background_color_;
+
+ // Internationlization
+ if (first.multi_byte_characters_.has_value())
+ result.multi_byte_characters_ = first.multi_byte_characters_;
+ else
+ result.multi_byte_characters_ = second.multi_byte_characters_;
+
+ if (first.locale_.has_value())
+ result.locale_ = first.locale_;
+ else
+ result.locale_ = second.locale_;
+
+ return result;
+ }
+
+private:
+ friend class Cell;
+ friend class Row;
+ friend class Column;
+ friend class TableInternal;
+ friend class Printer;
+ friend class MarkdownExporter;
+ friend class LatexExporter;
+ friend class AsciiDocExporter;
+
+ void set_defaults() {
+ // NOTE: width and height are not set here
+ font_align_ = FontAlign::left;
+ font_style_ = std::vector<FontStyle>{};
+ font_color_ = font_background_color_ = Color::none;
+ padding_left_ = padding_right_ = 1;
+ padding_top_ = padding_bottom_ = 0;
+ border_top_ = border_bottom_ = "-";
+ border_left_ = border_right_ = "|";
+ show_border_left_ = show_border_right_ = show_border_top_ = show_border_bottom_ = true;
+ border_top_color_ = border_top_background_color_ = border_bottom_color_ =
+ border_bottom_background_color_ = border_left_color_ = border_left_background_color_ =
+ border_right_color_ = border_right_background_color_ = Color::none;
+ corner_top_left_ = corner_top_right_ = corner_bottom_left_ = corner_bottom_right_ = "+";
+ corner_top_left_color_ = corner_top_left_background_color_ = corner_top_right_color_ =
+ corner_top_right_background_color_ = corner_bottom_left_color_ =
+ corner_bottom_left_background_color_ = corner_bottom_right_color_ =
+ corner_bottom_right_background_color_ = Color::none;
+ column_separator_ = "|";
+ column_separator_color_ = column_separator_background_color_ = Color::none;
+ multi_byte_characters_ = false;
+ locale_ = "";
+ }
+
+ // Helper methods for word wrapping:
+
+ // trim white spaces from the left end of an input string
+ static std::string trim_left(const std::string &input_string) {
+ std::string result = input_string;
+ result.erase(result.begin(), std::find_if(result.begin(), result.end(),
+ [](int ch) { return !std::isspace(ch); }));
+ return result;
+ }
+
+ // trim white spaces from right end of an input string
+ static std::string trim_right(const std::string &input_string) {
+ std::string result = input_string;
+ result.erase(
+ std::find_if(result.rbegin(), result.rend(), [](int ch) { return !std::isspace(ch); })
+ .base(),
+ result.end());
+ return result;
+ }
+
+ // trim white spaces from either end of an input string
+ static std::string trim(const std::string &input_string) {
+ return trim_left(trim_right(input_string));
+ }
+
+ static size_t index_of_any(const std::string &input, size_t start_index,
+ const std::vector<std::string> &split_characters) {
+ std::vector<size_t> indices{};
+ for (auto &c : split_characters) {
+ auto index = input.find(c, start_index);
+ if (index != std::string::npos)
+ indices.push_back(index);
+ }
+ if (indices.size() > 0)
+ return *std::min_element(indices.begin(), indices.end());
+ else
+ return std::string::npos;
+ }
+
+ static std::vector<std::string> explode_string(const std::string &input,
+ const std::vector<std::string> &split_characters) {
+ std::vector<std::string> result{};
+ size_t start_index{0};
+ while (true) {
+ auto index = index_of_any(input, start_index, split_characters);
+
+ if (index == std::string::npos) {
+ result.push_back(input.substr(start_index));
+ return result;
+ }
+
+ std::string word = input.substr(start_index, index - start_index);
+ char next_character = input.substr(index, 1)[0];
+ // Unlike whitespace, dashes and the like should stick to the word
+ // occurring before it.
+ if (isspace(next_character)) {
+ result.push_back(word);
+ result.push_back(std::string(1, next_character));
+ } else {
+ result.push_back(word + next_character);
+ }
+ start_index = index + 1;
+ }
+
+ return result;
+ }
+
+ // Element width and height
+ optional<size_t> width_{};
+ optional<size_t> height_{};
+
+ // Font styling
+ optional<FontAlign> font_align_{};
+ optional<std::vector<FontStyle>> font_style_{};
+ optional<Color> font_color_{};
+ optional<Color> font_background_color_{};
+
+ // Element padding
+ optional<size_t> padding_left_{};
+ optional<size_t> padding_top_{};
+ optional<size_t> padding_right_{};
+ optional<size_t> padding_bottom_{};
+
+ // Element border
+ optional<bool> show_border_top_{};
+ optional<std::string> border_top_{};
+ optional<Color> border_top_color_{};
+ optional<Color> border_top_background_color_{};
+
+ optional<bool> show_border_bottom_{};
+ optional<std::string> border_bottom_{};
+ optional<Color> border_bottom_color_{};
+ optional<Color> border_bottom_background_color_{};
+
+ optional<bool> show_border_left_{};
+ optional<std::string> border_left_{};
+ optional<Color> border_left_color_{};
+ optional<Color> border_left_background_color_{};
+
+ optional<bool> show_border_right_{};
+ optional<std::string> border_right_{};
+ optional<Color> border_right_color_{};
+ optional<Color> border_right_background_color_{};
+
+ // Element corner
+ optional<std::string> corner_top_left_{};
+ optional<Color> corner_top_left_color_{};
+ optional<Color> corner_top_left_background_color_{};
+
+ optional<std::string> corner_top_right_{};
+ optional<Color> corner_top_right_color_{};
+ optional<Color> corner_top_right_background_color_{};
+
+ optional<std::string> corner_bottom_left_{};
+ optional<Color> corner_bottom_left_color_{};
+ optional<Color> corner_bottom_left_background_color_{};
+
+ optional<std::string> corner_bottom_right_{};
+ optional<Color> corner_bottom_right_color_{};
+ optional<Color> corner_bottom_right_background_color_{};
+
+ // Element column separator
+ optional<std::string> column_separator_{};
+ optional<Color> column_separator_color_{};
+ optional<Color> column_separator_background_color_{};
+
+ // Internationalization
+ optional<bool> multi_byte_characters_{};
+ optional<std::string> locale_{};
+};
+
+} // namespace tabulate
+
+// #include <tabulate/utf8.hpp>
+
+#if __cplusplus >= 201703L
+#include <optional>
+using std::optional;
+#else
+// #include <tabulate/optional_lite.hpp>
+using nonstd::optional;
+#endif
+
+#include <vector>
+
+namespace tabulate {
+
+class Cell {
+public:
+ explicit Cell(std::shared_ptr<class Row> parent) : parent_(parent) {}
+
+ void set_text(const std::string &text) { data_ = text; }
+
+ const std::string &get_text() { return data_; }
+
+ size_t size() {
+ return get_sequence_length(data_, locale(), is_multi_byte_character_support_enabled());
+ }
+
+ std::string locale() { return *format().locale_; }
+
+ Format &format();
+
+ bool is_multi_byte_character_support_enabled();
+
+private:
+ std::string data_;
+ std::weak_ptr<class Row> parent_;
+ optional<Format> format_;
+};
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <iostream>
+#include <memory>
+#include <string>
+// #include <tabulate/cell.hpp>
+
+#if __cplusplus >= 201703L
+#include <optional>
+using std::optional;
+#else
+// #include <tabulate/optional_lite.hpp>
+using nonstd::optional;
+#endif
+
+#include <vector>
+#ifdef max
+#undef max
+#endif
+#ifdef min
+#undef min
+#endif
+
+namespace tabulate {
+
+class Row {
+public:
+ explicit Row(std::shared_ptr<class TableInternal> parent) : parent_(parent) {}
+
+ void add_cell(std::shared_ptr<Cell> cell) { cells_.push_back(cell); }
+
+ Cell &operator[](size_t index) { return cell(index); }
+
+ Cell &cell(size_t index) { return *(cells_[index]); }
+
+ std::vector<std::shared_ptr<Cell>> cells() const { return cells_; }
+
+ size_t size() const { return cells_.size(); }
+
+ Format &format();
+
+ class CellIterator {
+ public:
+ explicit CellIterator(std::vector<std::shared_ptr<Cell>>::iterator ptr) : ptr(ptr) {}
+
+ CellIterator operator++() {
+ ++ptr;
+ return *this;
+ }
+ bool operator!=(const CellIterator &other) const { return ptr != other.ptr; }
+ Cell &operator*() { return **ptr; }
+
+ private:
+ std::vector<std::shared_ptr<Cell>>::iterator ptr;
+ };
+
+ auto begin() -> CellIterator { return CellIterator(cells_.begin()); }
+ auto end() -> CellIterator { return CellIterator(cells_.end()); }
+
+private:
+ friend class Printer;
+
+ // Returns the row height as configured
+ // For each cell in the row, check the cell.format.height
+ // property and return the largest configured row height
+ // This is used to ensure that all cells in a row are
+ // aligned when printing the column
+ size_t get_configured_height() {
+ size_t result{0};
+ for (size_t i = 0; i < size(); ++i) {
+ auto cell = cells_[i];
+ auto format = cell->format();
+ if (format.height_.has_value())
+ result = std::max(result, *format.height_);
+ }
+ return result;
+ }
+
+ // Computes the height of the row based on cell contents
+ // and configured cell padding
+ // For each cell, compute:
+ // padding_top + (cell_contents / column height) + padding_bottom
+ // and return the largest value
+ //
+ // This is useful when no cell.format.height is configured
+ // Call get_configured_height()
+ // - If this returns 0, then use get_computed_height()
+ size_t get_computed_height(const std::vector<size_t> &column_widths) {
+ size_t result{0};
+ for (size_t i = 0; i < size(); ++i) {
+ result = std::max(result, get_cell_height(i, column_widths[i]));
+ }
+ return result;
+ }
+
+ // Returns padding_top + cell_contents / column_height + padding_bottom
+ // for a given cell in the column
+ // e.g.,
+ // column width = 5
+ // cell_contents = "I love tabulate" (size/length = 15)
+ // padding top and padding bottom are 1
+ // then, cell height = 1 + (15 / 5) + 1 = 1 + 3 + 1 = 5
+ // The cell will look like this:
+ //
+ // .....
+ // I lov
+ // e tab
+ // ulate
+ // .....
+ size_t get_cell_height(size_t cell_index, size_t column_width) {
+ size_t result{0};
+ Cell &cell = *(cells_[cell_index]);
+ auto format = cell.format();
+ auto text = cell.get_text();
+
+ auto padding_left = *format.padding_left_;
+ auto padding_right = *format.padding_right_;
+
+ result += *format.padding_top_;
+
+ if (column_width > (padding_left + padding_right)) {
+ column_width -= (padding_left + padding_right);
+ }
+
+ // Check if input text has embedded newline characters
+ auto newlines_in_text = std::count(text.begin(), text.end(), '\n');
+ std::string word_wrapped_text;
+ if (newlines_in_text == 0) {
+ // No new lines in input
+ // Apply automatic word wrapping and compute row height
+ word_wrapped_text = Format::word_wrap(text, column_width, cell.locale(),
+ cell.is_multi_byte_character_support_enabled());
+ } else {
+ // There are embedded '\n' characters
+ // Respect these characters
+ word_wrapped_text = text;
+ }
+
+ auto newlines_in_wrapped_text =
+ std::count(word_wrapped_text.begin(), word_wrapped_text.end(), '\n');
+ auto estimated_row_height = newlines_in_wrapped_text;
+
+ if (!word_wrapped_text.empty() &&
+ word_wrapped_text[word_wrapped_text.size() - 1] != '\n') // text doesn't end with a newline
+ estimated_row_height += 1;
+
+ result += estimated_row_height;
+
+ result += *format.padding_bottom_;
+
+ return result;
+ }
+
+ std::vector<std::shared_ptr<Cell>> cells_;
+ std::weak_ptr<class TableInternal> parent_;
+ optional<Format> format_;
+};
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+
+namespace tabulate {
+
+class ColumnFormat : public Format {
+public:
+ explicit ColumnFormat(class Column &column) : column_(column) {}
+
+ ColumnFormat &width(size_t value);
+ ColumnFormat &height(size_t value);
+
+ // Padding
+ ColumnFormat &padding(size_t value);
+ ColumnFormat &padding_left(size_t value);
+ ColumnFormat &padding_right(size_t value);
+ ColumnFormat &padding_top(size_t value);
+ ColumnFormat &padding_bottom(size_t value);
+
+ // Border
+ ColumnFormat &border(const std::string &value);
+ ColumnFormat &border_color(Color value);
+ ColumnFormat &border_background_color(Color value);
+ ColumnFormat &border_left(const std::string &value);
+ ColumnFormat &border_left_color(Color value);
+ ColumnFormat &border_left_background_color(Color value);
+ ColumnFormat &border_right(const std::string &value);
+ ColumnFormat &border_right_color(Color value);
+ ColumnFormat &border_right_background_color(Color value);
+ ColumnFormat &border_top(const std::string &value);
+ ColumnFormat &border_top_color(Color value);
+ ColumnFormat &border_top_background_color(Color value);
+ ColumnFormat &border_bottom(const std::string &value);
+ ColumnFormat &border_bottom_color(Color value);
+ ColumnFormat &border_bottom_background_color(Color value);
+
+ // Corner
+ ColumnFormat &corner(const std::string &value);
+ ColumnFormat &corner_color(Color value);
+ ColumnFormat &corner_background_color(Color value);
+
+ // Column separator
+ ColumnFormat &column_separator(const std::string &value);
+ ColumnFormat &column_separator_color(Color value);
+ ColumnFormat &column_separator_background_color(Color value);
+
+ // Font styling
+ ColumnFormat &font_align(FontAlign value);
+ ColumnFormat &font_style(const std::vector<FontStyle> &style);
+ ColumnFormat &font_color(Color value);
+ ColumnFormat &font_background_color(Color value);
+ ColumnFormat &color(Color value);
+ ColumnFormat &background_color(Color value);
+
+ // Locale
+ ColumnFormat &multi_byte_characters(bool value);
+ ColumnFormat &locale(const std::string &value);
+
+private:
+ std::reference_wrapper<class Column> column_;
+};
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <algorithm>
+#include <functional>
+#include <iostream>
+#include <memory>
+
+#if __cplusplus >= 201703L
+#include <optional>
+using std::optional;
+#else
+// #include <tabulate/optional_lite.hpp>
+using nonstd::optional;
+#endif
+
+#include <string>
+// #include <tabulate/cell.hpp>
+// #include <tabulate/column_format.hpp>
+#include <vector>
+#ifdef max
+#undef max
+#endif
+#ifdef min
+#undef min
+#endif
+
+namespace tabulate {
+
+class Column {
+public:
+ explicit Column(std::shared_ptr<class TableInternal> parent) : parent_(parent) {}
+
+ void add_cell(Cell &cell) { cells_.push_back(cell); }
+
+ Cell &operator[](size_t index) { return cells_[index]; }
+
+ std::vector<std::reference_wrapper<Cell>> cells() const { return cells_; }
+
+ size_t size() const { return cells_.size(); }
+
+ ColumnFormat format() { return ColumnFormat(*this); }
+
+ class CellIterator {
+ public:
+ explicit CellIterator(std::vector<std::reference_wrapper<Cell>>::iterator ptr) : ptr(ptr) {}
+
+ CellIterator operator++() {
+ ++ptr;
+ return *this;
+ }
+ bool operator!=(const CellIterator &other) const { return ptr != other.ptr; }
+ Cell &operator*() { return *ptr; }
+
+ private:
+ std::vector<std::reference_wrapper<Cell>>::iterator ptr;
+ };
+
+ auto begin() -> CellIterator { return CellIterator(cells_.begin()); }
+ auto end() -> CellIterator { return CellIterator(cells_.end()); }
+
+private:
+ friend class ColumnFormat;
+ friend class Printer;
+
+ // Returns the column width as configured
+ // For each cell in the column, check the cell.format.width
+ // property and return the largest configured column width
+ // This is used to ensure that all cells in a column are
+ // aligned when printing the column
+ size_t get_configured_width() {
+ size_t result{0};
+ for (size_t i = 0; i < size(); ++i) {
+ auto cell = cells_[i];
+ auto format = cell.get().format();
+ if (format.width_.has_value())
+ result = std::max(result, *format.width_);
+ }
+ return result;
+ }
+
+ // Computes the width of the column based on cell contents
+ // and configured cell padding
+ // For each cell, compute padding_left + cell_contents + padding_right
+ // and return the largest value
+ //
+ // This is useful when no cell.format.width is configured
+ // Call get_configured_width()
+ // - If this returns 0, then use get_computed_width()
+ size_t get_computed_width() {
+ size_t result{0};
+ for (size_t i = 0; i < size(); ++i) {
+ result = std::max(result, get_cell_width(i));
+ }
+ return result;
+ }
+
+ // Returns padding_left + cell_contents.size() + padding_right
+ // for a given cell in the column
+ size_t get_cell_width(size_t cell_index) {
+ size_t result{0};
+ Cell &cell = cells_[cell_index].get();
+ auto format = cell.format();
+ if (format.padding_left_.has_value())
+ result += *format.padding_left_;
+
+ // Check if input text has newlines
+ auto text = cell.get_text();
+ auto split_lines = Format::split_lines(text, "\n", cell.locale(),
+ cell.is_multi_byte_character_support_enabled());
+
+ // If there are no newlines in input, set column_width = text.size()
+ if (split_lines.size() == 1) {
+ result += cell.size();
+ } else {
+ // There are newlines in input
+ // Find widest substring in input and use this as column_width
+ size_t widest_sub_string_size{0};
+ for (auto &line : split_lines)
+ if (get_sequence_length(line, cell.locale(),
+ cell.is_multi_byte_character_support_enabled()) >
+ widest_sub_string_size)
+ widest_sub_string_size = get_sequence_length(
+ line, cell.locale(), cell.is_multi_byte_character_support_enabled());
+ result += widest_sub_string_size;
+ }
+
+ if (format.padding_right_.has_value())
+ result += *format.padding_right_;
+
+ return result;
+ }
+
+ std::vector<std::reference_wrapper<Cell>> cells_;
+ std::weak_ptr<class TableInternal> parent_;
+};
+
+inline ColumnFormat &ColumnFormat::width(size_t value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().width(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::height(size_t value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().height(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::padding(size_t value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().padding(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::padding_left(size_t value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().padding_left(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::padding_right(size_t value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().padding_right(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::padding_top(size_t value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().padding_top(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::padding_bottom(size_t value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().padding_bottom(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border(const std::string &value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_background_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_background_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_left(const std::string &value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_left(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_left_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_left_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_left_background_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_left_background_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_right(const std::string &value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_right(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_right_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_right_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_right_background_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_right_background_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_top(const std::string &value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_top(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_top_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_top_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_top_background_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_top_background_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_bottom(const std::string &value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_bottom(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_bottom_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_bottom_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::border_bottom_background_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().border_bottom_background_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::corner(const std::string &value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().corner(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::corner_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().corner_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::corner_background_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().corner_background_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::column_separator(const std::string &value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().column_separator(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::column_separator_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().column_separator_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::column_separator_background_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().column_separator_background_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::font_align(FontAlign value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().font_align(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::font_style(const std::vector<FontStyle> &style) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().font_style(style);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::font_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().font_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::font_background_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().font_background_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::background_color(Color value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().background_color(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::multi_byte_characters(bool value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().multi_byte_characters(value);
+ return *this;
+}
+
+inline ColumnFormat &ColumnFormat::locale(const std::string &value) {
+ for (auto &cell : column_.get().cells_)
+ cell.get().format().locale(value);
+ return *this;
+}
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+// #include <tabulate/color.hpp>
+// #include <tabulate/font_style.hpp>
+#include <utility>
+#include <vector>
+
+namespace tabulate {
+
+class Printer {
+public:
+ static std::pair<std::vector<size_t>, std::vector<size_t>>
+ compute_cell_dimensions(TableInternal &table);
+
+ static void print_table(std::ostream &stream, TableInternal &table);
+
+ static void print_row_in_cell(std::ostream &stream, TableInternal &table,
+ const std::pair<size_t, size_t> &index,
+ const std::pair<size_t, size_t> &dimension, size_t num_columns,
+ size_t row_index,
+ const std::vector<std::string> &splitted_cell_text);
+
+ static bool print_cell_border_top(std::ostream &stream, TableInternal &table,
+ const std::pair<size_t, size_t> &index,
+ const std::pair<size_t, size_t> &dimension, size_t num_columns);
+ static bool print_cell_border_bottom(std::ostream &stream, TableInternal &table,
+ const std::pair<size_t, size_t> &index,
+ const std::pair<size_t, size_t> &dimension,
+ size_t num_columns);
+
+ static void apply_element_style(std::ostream &stream, Color foreground_color,
+ Color background_color,
+ const std::vector<FontStyle> &font_style) {
+ apply_foreground_color(stream, foreground_color);
+ apply_background_color(stream, background_color);
+ for (auto &style : font_style)
+ apply_font_style(stream, style);
+ }
+
+ static void reset_element_style(std::ostream &stream) { stream << termcolor::reset; }
+
+private:
+ static void print_content_left_aligned(std::ostream &stream, const std::string &cell_content,
+ const Format &format, size_t text_with_padding_size,
+ size_t column_width) {
+
+ // Apply font style
+ apply_element_style(stream, *format.font_color_, *format.font_background_color_,
+ *format.font_style_);
+ stream << cell_content;
+ // Only apply font_style to the font
+ // Not the padding. So calling apply_element_style with font_style = {}
+ reset_element_style(stream);
+ apply_element_style(stream, *format.font_color_, *format.font_background_color_, {});
+
+ if (text_with_padding_size < column_width) {
+ for (size_t j = 0; j < (column_width - text_with_padding_size); ++j) {
+ stream << " ";
+ }
+ }
+ }
+
+ static void print_content_center_aligned(std::ostream &stream, const std::string &cell_content,
+ const Format &format, size_t text_with_padding_size,
+ size_t column_width) {
+ auto num_spaces = column_width - text_with_padding_size;
+ if (num_spaces % 2 == 0) {
+ // Even spacing on either side
+ for (size_t j = 0; j < num_spaces / 2; ++j)
+ stream << " ";
+
+ // Apply font style
+ apply_element_style(stream, *format.font_color_, *format.font_background_color_,
+ *format.font_style_);
+ stream << cell_content;
+ // Only apply font_style to the font
+ // Not the padding. So calling apply_element_style with font_style = {}
+ reset_element_style(stream);
+ apply_element_style(stream, *format.font_color_, *format.font_background_color_, {});
+
+ for (size_t j = 0; j < num_spaces / 2; ++j)
+ stream << " ";
+ } else {
+ auto num_spaces_before = num_spaces / 2 + 1;
+ for (size_t j = 0; j < num_spaces_before; ++j)
+ stream << " ";
+
+ // Apply font style
+ apply_element_style(stream, *format.font_color_, *format.font_background_color_,
+ *format.font_style_);
+ stream << cell_content;
+ // Only apply font_style to the font
+ // Not the padding. So calling apply_element_style with font_style = {}
+ reset_element_style(stream);
+ apply_element_style(stream, *format.font_color_, *format.font_background_color_, {});
+
+ for (size_t j = 0; j < num_spaces - num_spaces_before; ++j)
+ stream << " ";
+ }
+ }
+
+ static void print_content_right_aligned(std::ostream &stream, const std::string &cell_content,
+ const Format &format, size_t text_with_padding_size,
+ size_t column_width) {
+ if (text_with_padding_size < column_width) {
+ for (size_t j = 0; j < (column_width - text_with_padding_size); ++j) {
+ stream << " ";
+ }
+ }
+
+ // Apply font style
+ apply_element_style(stream, *format.font_color_, *format.font_background_color_,
+ *format.font_style_);
+ stream << cell_content;
+ // Only apply font_style to the font
+ // Not the padding. So calling apply_element_style with font_style = {}
+ reset_element_style(stream);
+ apply_element_style(stream, *format.font_color_, *format.font_background_color_, {});
+ }
+
+ static void apply_font_style(std::ostream &stream, FontStyle style) {
+ switch (style) {
+ case FontStyle::bold:
+ stream << termcolor::bold;
+ break;
+ case FontStyle::dark:
+ stream << termcolor::dark;
+ break;
+ case FontStyle::italic:
+ stream << termcolor::italic;
+ break;
+ case FontStyle::underline:
+ stream << termcolor::underline;
+ break;
+ case FontStyle::blink:
+ stream << termcolor::blink;
+ break;
+ case FontStyle::reverse:
+ stream << termcolor::reverse;
+ break;
+ case FontStyle::concealed:
+ stream << termcolor::concealed;
+ break;
+ case FontStyle::crossed:
+ stream << termcolor::crossed;
+ break;
+ default:
+ break;
+ }
+ }
+
+ static void apply_foreground_color(std::ostream &stream, Color foreground_color) {
+ switch (foreground_color) {
+ case Color::grey:
+ stream << termcolor::grey;
+ break;
+ case Color::red:
+ stream << termcolor::red;
+ break;
+ case Color::green:
+ stream << termcolor::green;
+ break;
+ case Color::yellow:
+ stream << termcolor::yellow;
+ break;
+ case Color::blue:
+ stream << termcolor::blue;
+ break;
+ case Color::magenta:
+ stream << termcolor::magenta;
+ break;
+ case Color::cyan:
+ stream << termcolor::cyan;
+ break;
+ case Color::white:
+ stream << termcolor::white;
+ break;
+ case Color::none:
+ default:
+ break;
+ }
+ }
+
+ static void apply_background_color(std::ostream &stream, Color background_color) {
+ switch (background_color) {
+ case Color::grey:
+ stream << termcolor::on_grey;
+ break;
+ case Color::red:
+ stream << termcolor::on_red;
+ break;
+ case Color::green:
+ stream << termcolor::on_green;
+ break;
+ case Color::yellow:
+ stream << termcolor::on_yellow;
+ break;
+ case Color::blue:
+ stream << termcolor::on_blue;
+ break;
+ case Color::magenta:
+ stream << termcolor::on_magenta;
+ break;
+ case Color::cyan:
+ stream << termcolor::on_cyan;
+ break;
+ case Color::white:
+ stream << termcolor::on_white;
+ break;
+ case Color::none:
+ default:
+ break;
+ }
+ }
+};
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <algorithm>
+#include <iostream>
+#include <string>
+// #include <tabulate/column.hpp>
+// #include <tabulate/font_style.hpp>
+// #include <tabulate/printer.hpp>
+// #include <tabulate/row.hpp>
+// #include <tabulate/termcolor.hpp>
+#include <vector>
+#ifdef max
+#undef max
+#endif
+#ifdef min
+#undef min
+#endif
+
+namespace tabulate {
+
+class TableInternal : public std::enable_shared_from_this<TableInternal> {
+public:
+ static std::shared_ptr<TableInternal> create() {
+ auto result = std::shared_ptr<TableInternal>(new TableInternal());
+ result->format_.set_defaults();
+ return result;
+ }
+
+ void add_row(const std::vector<std::string> &cells) {
+ auto row = std::make_shared<Row>(shared_from_this());
+ for (auto &c : cells) {
+ auto cell = std::make_shared<Cell>(row);
+ cell->set_text(c);
+ row->add_cell(cell);
+ }
+ rows_.push_back(row);
+ }
+
+ Row &operator[](size_t index) { return *(rows_[index]); }
+
+ const Row &operator[](size_t index) const { return *(rows_[index]); }
+
+ Column column(size_t index) {
+ Column column(shared_from_this());
+ for (size_t i = 0; i < rows_.size(); ++i) {
+ auto row = rows_[i];
+ auto &cell = row->cell(index);
+ column.add_cell(cell);
+ }
+ return column;
+ }
+
+ size_t size() const { return rows_.size(); }
+
+ std::pair<size_t, size_t> shape() {
+ std::pair<size_t, size_t> result{0, 0};
+ std::stringstream stream;
+ print(stream);
+ auto buffer = stream.str();
+ auto lines = Format::split_lines(buffer, "\n", "", true);
+ if (lines.size()) {
+ result = {get_sequence_length(lines[0], "", true), lines.size()};
+ }
+ return result;
+ }
+
+ Format &format() { return format_; }
+
+ void print(std::ostream &stream) { Printer::print_table(stream, *this); }
+
+ size_t estimate_num_columns() const {
+ size_t result{0};
+ if (size()) {
+ auto first_row = operator[](size_t(0));
+ result = first_row.size();
+ }
+ return result;
+ }
+
+private:
+ friend class Table;
+ friend class MarkdownExporter;
+
+ TableInternal() {}
+ TableInternal &operator=(const TableInternal &);
+ TableInternal(const TableInternal &);
+
+ std::vector<std::shared_ptr<Row>> rows_;
+ Format format_;
+};
+
+inline Format &Cell::format() {
+ std::shared_ptr<Row> parent = parent_.lock();
+ if (!format_.has_value()) { // no cell format
+ format_ = parent->format(); // Use parent row format
+ } else {
+ // Cell has formatting
+ // Merge cell formatting with parent row formatting
+ format_ = Format::merge(*format_, parent->format());
+ }
+ return *format_;
+}
+
+inline bool Cell::is_multi_byte_character_support_enabled() {
+ return (*format().multi_byte_characters_);
+}
+
+inline Format &Row::format() {
+ std::shared_ptr<TableInternal> parent = parent_.lock();
+ if (!format_.has_value()) { // no row format
+ format_ = parent->format(); // Use parent table format
+ } else {
+ // Row has formatting rules
+ // Merge with parent table format
+ format_ = Format::merge(*format_, parent->format());
+ }
+ return *format_;
+}
+
+inline std::pair<std::vector<size_t>, std::vector<size_t>>
+Printer::compute_cell_dimensions(TableInternal &table) {
+ std::pair<std::vector<size_t>, std::vector<size_t>> result;
+ size_t num_rows = table.size();
+ size_t num_columns = table.estimate_num_columns();
+
+ std::vector<size_t> row_heights, column_widths{};
+
+ for (size_t i = 0; i < num_columns; ++i) {
+ Column column = table.column(i);
+ size_t configured_width = column.get_configured_width();
+ size_t computed_width = column.get_computed_width();
+ if (configured_width != 0)
+ column_widths.push_back(configured_width);
+ else
+ column_widths.push_back(computed_width);
+ }
+
+ for (size_t i = 0; i < num_rows; ++i) {
+ Row row = table[i];
+ size_t configured_height = row.get_configured_height();
+ size_t computed_height = row.get_computed_height(column_widths);
+
+ // NOTE: Unlike column width, row height is calculated as the max
+ // b/w configured height and computed height
+ // which means that .width() has higher precedence than .height()
+ // when both are configured by the user
+ //
+ // TODO: Maybe this can be configured?
+ // If such a configuration is exposed, i.e., prefer height over width
+ // then the logic will be reversed, i.e.,
+ // column_widths.push_back(std::max(configured_width, computed_width))
+ // and
+ // row_height = configured_height if != 0 else computed_height
+
+ row_heights.push_back(std::max(configured_height, computed_height));
+ }
+
+ result.first = row_heights;
+ result.second = column_widths;
+
+ return result;
+}
+
+inline void Printer::print_table(std::ostream &stream, TableInternal &table) {
+ size_t num_rows = table.size();
+ size_t num_columns = table.estimate_num_columns();
+ auto dimensions = compute_cell_dimensions(table);
+ auto row_heights = dimensions.first;
+ auto column_widths = dimensions.second;
+ auto splitted_cells_text = std::vector<std::vector<std::vector<std::string>>>(
+ num_rows, std::vector<std::vector<std::string>>(num_columns, std::vector<std::string>{}));
+
+ // Pre-compute the cells' content and split them into lines before actually
+ // iterating the cells.
+ for (size_t i = 0; i < num_rows; ++i) {
+ Row row = table[i];
+ for (size_t j = 0; j < num_columns; ++j) {
+ Cell cell = row.cell(j);
+ const std::string &text = cell.get_text();
+ auto padding_left = *cell.format().padding_left_;
+ auto padding_right = *cell.format().padding_right_;
+
+ // Check if input text has embedded \n that are to be respected
+ bool has_new_line = text.find_first_of('\n') != std::string::npos;
+
+ if (has_new_line) {
+ // Respect to the embedded '\n' characters
+ splitted_cells_text[i][j] = Format::split_lines(
+ text, "\n", cell.locale(), cell.is_multi_byte_character_support_enabled());
+ } else {
+ // If there are no embedded \n characters, then apply word wrap.
+ //
+ // Configured column width cannot be lower than (padding_left +
+ // padding_right) This is a bad configuration E.g., the user is trying
+ // to force the column width to be 5 when padding_left and padding_right
+ // are each configured to 3 (padding_left + padding_right) = 6 >
+ // column_width
+ auto content_width = column_widths[j] > padding_left + padding_right
+ ? column_widths[j] - padding_left - padding_right
+ : column_widths[j];
+ auto word_wrapped_text = Format::word_wrap(text, content_width, cell.locale(),
+ cell.is_multi_byte_character_support_enabled());
+ splitted_cells_text[i][j] = Format::split_lines(
+ word_wrapped_text, "\n", cell.locale(), cell.is_multi_byte_character_support_enabled());
+ }
+ }
+ }
+
+ // For each row,
+ for (size_t i = 0; i < num_rows; ++i) {
+
+ // Print top border
+ bool border_top_printed{true};
+ for (size_t j = 0; j < num_columns; ++j) {
+ border_top_printed &= print_cell_border_top(stream, table, {i, j},
+ {row_heights[i], column_widths[j]}, num_columns);
+ }
+ if (border_top_printed)
+ stream << termcolor::reset << "\n";
+
+ // Print row contents with word wrapping
+ for (size_t k = 0; k < row_heights[i]; ++k) {
+ for (size_t j = 0; j < num_columns; ++j) {
+ print_row_in_cell(stream, table, {i, j}, {row_heights[i], column_widths[j]}, num_columns, k,
+ splitted_cells_text[i][j]);
+ }
+ if (k + 1 < row_heights[i])
+ stream << termcolor::reset << "\n";
+ }
+
+ if (i + 1 == num_rows) {
+
+ // Check if there is bottom border to print:
+ auto bottom_border_needed{true};
+ for (size_t j = 0; j < num_columns; ++j) {
+ auto cell = table[i][j];
+ auto format = cell.format();
+ auto corner = *format.corner_bottom_left_;
+ auto border_bottom = *format.border_bottom_;
+ if (corner == "" && border_bottom == "") {
+ bottom_border_needed = false;
+ break;
+ }
+ }
+
+ if (bottom_border_needed)
+ stream << termcolor::reset << "\n";
+ // Print bottom border for table
+ for (size_t j = 0; j < num_columns; ++j) {
+ print_cell_border_bottom(stream, table, {i, j}, {row_heights[i], column_widths[j]},
+ num_columns);
+ }
+ }
+ if (i + 1 < num_rows)
+ stream << termcolor::reset << "\n"; // Don't add newline after last row
+ }
+}
+
+inline void Printer::print_row_in_cell(std::ostream &stream, TableInternal &table,
+ const std::pair<size_t, size_t> &index,
+ const std::pair<size_t, size_t> &dimension,
+ size_t num_columns, size_t row_index,
+ const std::vector<std::string> &splitted_cell_text) {
+ auto column_width = dimension.second;
+ auto cell = table[index.first][index.second];
+ auto locale = cell.locale();
+ auto is_multi_byte_character_support_enabled = cell.is_multi_byte_character_support_enabled();
+ auto old_locale = std::locale::global(std::locale(locale));
+ auto format = cell.format();
+ auto text_height = splitted_cell_text.size();
+ auto padding_top = *format.padding_top_;
+
+ if (*format.show_border_left_) {
+ apply_element_style(stream, *format.border_left_color_, *format.border_left_background_color_,
+ {});
+ stream << *format.border_left_;
+ reset_element_style(stream);
+ }
+
+ apply_element_style(stream, *format.font_color_, *format.font_background_color_, {});
+ if (row_index < padding_top) {
+ // Padding top
+ stream << std::string(column_width, ' ');
+ } else if (row_index >= padding_top && (row_index <= (padding_top + text_height))) {
+ // Retrieve padding left and right
+ // (column_width - padding_left - padding_right) is the amount of space
+ // available for cell text - Use this to word wrap cell contents
+ auto padding_left = *format.padding_left_;
+ auto padding_right = *format.padding_right_;
+
+ if (row_index - padding_top < text_height) {
+ auto line = splitted_cell_text[row_index - padding_top];
+
+ // Print left padding characters
+ stream << std::string(padding_left, ' ');
+
+ // Print word-wrapped line
+ line = Format::trim(line);
+ auto line_with_padding_size =
+ get_sequence_length(line, cell.locale(), is_multi_byte_character_support_enabled) +
+ padding_left + padding_right;
+ switch (*format.font_align_) {
+ case FontAlign::left:
+ print_content_left_aligned(stream, line, format, line_with_padding_size, column_width);
+ break;
+ case FontAlign::center:
+ print_content_center_aligned(stream, line, format, line_with_padding_size, column_width);
+ break;
+ case FontAlign::right:
+ print_content_right_aligned(stream, line, format, line_with_padding_size, column_width);
+ break;
+ }
+
+ // Print right padding characters
+ stream << std::string(padding_right, ' ');
+ } else
+ stream << std::string(column_width, ' ');
+
+ } else {
+ // Padding bottom
+ stream << std::string(column_width, ' ');
+ }
+
+ reset_element_style(stream);
+
+ if (index.second + 1 == num_columns) {
+ // Print right border after last column
+ if (*format.show_border_right_) {
+ apply_element_style(stream, *format.border_right_color_,
+ *format.border_right_background_color_, {});
+ stream << *format.border_right_;
+ reset_element_style(stream);
+ }
+ }
+ std::locale::global(old_locale);
+}
+
+inline bool Printer::print_cell_border_top(std::ostream &stream, TableInternal &table,
+ const std::pair<size_t, size_t> &index,
+ const std::pair<size_t, size_t> &dimension,
+ size_t num_columns) {
+ auto cell = table[index.first][index.second];
+ auto locale = cell.locale();
+ auto old_locale = std::locale::global(std::locale(locale));
+ auto format = cell.format();
+ auto column_width = dimension.second;
+
+ auto corner = *format.corner_top_left_;
+ auto corner_color = *format.corner_top_left_color_;
+ auto corner_background_color = *format.corner_top_left_background_color_;
+ auto border_top = *format.border_top_;
+
+ if ((corner == "" && border_top == "") || !*format.show_border_top_)
+ return false;
+
+ apply_element_style(stream, corner_color, corner_background_color, {});
+ stream << corner;
+ reset_element_style(stream);
+
+ for (size_t i = 0; i < column_width; ++i) {
+ apply_element_style(stream, *format.border_top_color_, *format.border_top_background_color_,
+ {});
+ stream << border_top;
+ reset_element_style(stream);
+ }
+
+ if (index.second + 1 == num_columns) {
+ // Print corner after last column
+ corner = *format.corner_top_right_;
+ corner_color = *format.corner_top_right_color_;
+ corner_background_color = *format.corner_top_right_background_color_;
+
+ apply_element_style(stream, corner_color, corner_background_color, {});
+ stream << corner;
+ reset_element_style(stream);
+ }
+ std::locale::global(old_locale);
+ return true;
+}
+
+inline bool Printer::print_cell_border_bottom(std::ostream &stream, TableInternal &table,
+ const std::pair<size_t, size_t> &index,
+ const std::pair<size_t, size_t> &dimension,
+ size_t num_columns) {
+ auto cell = table[index.first][index.second];
+ auto locale = cell.locale();
+ auto old_locale = std::locale::global(std::locale(locale));
+ auto format = cell.format();
+ auto column_width = dimension.second;
+
+ auto corner = *format.corner_bottom_left_;
+ auto corner_color = *format.corner_bottom_left_color_;
+ auto corner_background_color = *format.corner_bottom_left_background_color_;
+ auto border_bottom = *format.border_bottom_;
+
+ if ((corner == "" && border_bottom == "") || !*format.show_border_bottom_)
+ return false;
+
+ apply_element_style(stream, corner_color, corner_background_color, {});
+ stream << corner;
+ reset_element_style(stream);
+
+ for (size_t i = 0; i < column_width; ++i) {
+ apply_element_style(stream, *format.border_bottom_color_,
+ *format.border_bottom_background_color_, {});
+ stream << border_bottom;
+ reset_element_style(stream);
+ }
+
+ if (index.second + 1 == num_columns) {
+ // Print corner after last column
+ corner = *format.corner_bottom_right_;
+ corner_color = *format.corner_bottom_right_color_;
+ corner_background_color = *format.corner_bottom_right_background_color_;
+
+ apply_element_style(stream, corner_color, corner_background_color, {});
+ stream << corner;
+ reset_element_style(stream);
+ }
+ std::locale::global(old_locale);
+ return true;
+}
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+// #include <tabulate/table_internal.hpp>
+
+#if __cplusplus >= 201703L
+#include <string_view>
+#include <variant>
+using std::get_if;
+using std::holds_alternative;
+using std::string_view;
+using std::variant;
+using std::visit;
+#else
+// #include <tabulate/string_view_lite.hpp>
+// #include <tabulate/variant_lite.hpp>
+using nonstd::get_if;
+using nonstd::holds_alternative;
+using nonstd::string_view;
+using nonstd::variant;
+using nonstd::visit;
+#endif
+
+#include <utility>
+
+namespace tabulate {
+
+class Table {
+public:
+ Table() : table_(TableInternal::create()) {}
+
+ using Row_t = std::vector<variant<std::string, const char *, string_view, Table>>;
+
+ Table &add_row(const Row_t &cells) {
+
+ if (rows_ == 0) {
+ // This is the first row added
+ // cells.size() is the number of columns
+ cols_ = cells.size();
+ }
+
+ std::vector<std::string> cell_strings;
+ if (cells.size() < cols_) {
+ cell_strings.resize(cols_);
+ std::fill(cell_strings.begin(), cell_strings.end(), "");
+ } else {
+ cell_strings.resize(cells.size());
+ std::fill(cell_strings.begin(), cell_strings.end(), "");
+ }
+
+ for (size_t i = 0; i < cells.size(); ++i) {
+ auto cell = cells[i];
+ if (holds_alternative<std::string>(cell)) {
+ cell_strings[i] = *get_if<std::string>(&cell);
+ } else if (holds_alternative<const char *>(cell)) {
+ cell_strings[i] = *get_if<const char *>(&cell);
+ } else if (holds_alternative<string_view>(cell)) {
+ cell_strings[i] = std::string{*get_if<string_view>(&cell)};
+ } else {
+ auto table = *get_if<Table>(&cell);
+ std::stringstream stream;
+ table.print(stream);
+ cell_strings[i] = stream.str();
+ }
+ }
+
+ table_->add_row(cell_strings);
+ rows_ += 1;
+ return *this;
+ }
+
+ Row &operator[](size_t index) { return row(index); }
+
+ Row &row(size_t index) { return (*table_)[index]; }
+
+ Column column(size_t index) { return table_->column(index); }
+
+ Format &format() { return table_->format(); }
+
+ void print(std::ostream &stream) { table_->print(stream); }
+
+ std::string str() {
+ std::stringstream stream;
+ print(stream);
+ return stream.str();
+ }
+
+ size_t size() const { return table_->size(); }
+
+ std::pair<size_t, size_t> shape() { return table_->shape(); }
+
+ class RowIterator {
+ public:
+ explicit RowIterator(std::vector<std::shared_ptr<Row>>::iterator ptr) : ptr(ptr) {}
+
+ RowIterator operator++() {
+ ++ptr;
+ return *this;
+ }
+ bool operator!=(const RowIterator &other) const { return ptr != other.ptr; }
+ Row &operator*() { return **ptr; }
+
+ private:
+ std::vector<std::shared_ptr<Row>>::iterator ptr;
+ };
+
+ auto begin() -> RowIterator { return RowIterator(table_->rows_.begin()); }
+ auto end() -> RowIterator { return RowIterator(table_->rows_.end()); }
+
+private:
+ friend class MarkdownExporter;
+ friend class LatexExporter;
+ friend class AsciiDocExporter;
+
+ friend std::ostream &operator<<(std::ostream &stream, const Table &table);
+ size_t rows_{0};
+ size_t cols_{0};
+ std::shared_ptr<TableInternal> table_;
+};
+
+inline std::ostream &operator<<(std::ostream &stream, const Table &table) {
+ const_cast<Table &>(table).print(stream);
+ return stream;
+}
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <string>
+// #include <tabulate/table.hpp>
+
+namespace tabulate {
+
+class Exporter {
+public:
+ virtual std::string dump(Table &table) = 0;
+ virtual ~Exporter() {}
+};
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+// #include <tabulate/exporter.hpp>
+
+namespace tabulate {
+
+class MarkdownExporter : public Exporter {
+public:
+ std::string dump(Table &table) override {
+ std::string result{""};
+ apply_markdown_format(table);
+ result = table.str();
+ restore_table_format(table);
+ return result;
+ }
+
+ virtual ~MarkdownExporter() {}
+
+private:
+ void add_alignment_header_row(Table &table) {
+ auto &rows = table.table_->rows_;
+
+ if (rows.size() >= 1) {
+ auto alignment_row = std::make_shared<Row>(table.table_->shared_from_this());
+
+ // Create alignment header cells
+ std::vector<std::string> alignment_cells{};
+ for (auto &cell : table[0]) {
+ auto format = cell.format();
+ if (format.font_align_.value() == FontAlign::left) {
+ alignment_cells.push_back(":----");
+ } else if (format.font_align_.value() == FontAlign::center) {
+ alignment_cells.push_back(":---:");
+ } else if (format.font_align_.value() == FontAlign::right) {
+ alignment_cells.push_back("----:");
+ }
+ }
+
+ // Add alignment header cells to alignment row
+ for (auto &c : alignment_cells) {
+ auto cell = std::make_shared<Cell>(alignment_row);
+ cell->format()
+ .hide_border_top()
+ .hide_border_bottom()
+ .border_left("|")
+ .border_right("|")
+ .column_separator("|")
+ .corner("|");
+ cell->set_text(c);
+ if (c == ":---:")
+ cell->format().font_align(FontAlign::center);
+ else if (c == "----:")
+ cell->format().font_align(FontAlign::right);
+ alignment_row->add_cell(cell);
+ }
+
+ // Insert alignment header row
+ if (rows.size() > 1)
+ rows.insert(rows.begin() + 1, alignment_row);
+ else
+ rows.push_back(alignment_row);
+ }
+ }
+
+ void remove_alignment_header_row(Table &table) {
+ auto &rows = table.table_->rows_;
+ table.table_->rows_.erase(rows.begin() + 1);
+ }
+
+ void apply_markdown_format(Table &table) {
+ // Apply markdown format to cells in each row
+ for (auto row : table) {
+ for (auto &cell : row) {
+ auto format = cell.format();
+ formats_.push_back(format);
+ cell.format()
+ .hide_border_top()
+ .hide_border_bottom()
+ .border_left("|")
+ .border_right("|")
+ .column_separator("|")
+ .corner("|");
+ }
+ }
+ // Add alignment header row at position 1
+ add_alignment_header_row(table);
+ }
+
+ void restore_table_format(Table &table) {
+ // Remove alignment header row at position 1
+ remove_alignment_header_row(table);
+
+ // Restore original formatting for each cell
+ size_t format_index{0};
+ for (auto row : table) {
+ for (auto &cell : row) {
+ cell.format() = formats_[format_index];
+ format_index += 1;
+ }
+ }
+ }
+
+ std::vector<Format> formats_;
+};
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+// #include <tabulate/exporter.hpp>
+
+#if __cplusplus >= 201703L
+#include <optional>
+using std::optional;
+#else
+// #include <tabulate/optional_lite.hpp>
+using nonstd::optional;
+#endif
+
+namespace tabulate {
+
+class LatexExporter : public Exporter {
+
+ static const char new_line = '\n';
+
+public:
+ class ExportOptions {
+ public:
+ ExportOptions &indentation(std::size_t value) {
+ indentation_ = value;
+ return *this;
+ }
+
+ private:
+ friend class LatexExporter;
+ optional<size_t> indentation_;
+ };
+
+ ExportOptions &configure() { return options_; }
+
+ std::string dump(Table &table) override {
+ std::string result{"\\begin{tabular}"};
+ result += new_line;
+
+ result += add_alignment_header(table);
+ result += new_line;
+ const auto rows = table.rows_;
+ // iterate content and put text into the table.
+ for (size_t i = 0; i < rows; i++) {
+ auto &row = table[i];
+ // apply row content indentation
+ if (options_.indentation_.has_value()) {
+ result += std::string(options_.indentation_.value(), ' ');
+ }
+
+ for (size_t j = 0; j < row.size(); j++) {
+
+ result += row[j].get_text();
+
+ // check column position, need "\\" at the end of each row
+ if (j < row.size() - 1) {
+ result += " & ";
+ } else {
+ result += " \\\\";
+ }
+ }
+ result += new_line;
+ }
+
+ result += "\\end{tabular}";
+ return result;
+ }
+
+ virtual ~LatexExporter() {}
+
+private:
+ std::string add_alignment_header(Table &table) {
+ std::string result{"{"};
+
+ for (auto &cell : table[0]) {
+ auto format = cell.format();
+ if (format.font_align_.value() == FontAlign::left) {
+ result += 'l';
+ } else if (format.font_align_.value() == FontAlign::center) {
+ result += 'c';
+ } else if (format.font_align_.value() == FontAlign::right) {
+ result += 'r';
+ }
+ }
+
+ result += "}";
+ return result;
+ }
+ ExportOptions options_;
+};
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+#pragma once
+#include <algorithm>
+#include <optional>
+#include <sstream>
+#include <string>
+// #include <tabulate/exporter.hpp>
+
+namespace tabulate {
+
+class AsciiDocExporter : public Exporter {
+
+ static const char new_line = '\n';
+
+public:
+ std::string dump(Table &table) override {
+ std::stringstream ss;
+ ss << add_alignment_header(table);
+ ss << new_line;
+
+ const auto rows = table.rows_;
+ // iterate content and put text into the table.
+ for (size_t row_index = 0; row_index < rows; row_index++) {
+ auto &row = table[row_index];
+
+ for (size_t cell_index = 0; cell_index < row.size(); cell_index++) {
+ ss << "|";
+ ss << add_formatted_cell(row[cell_index]);
+ }
+ ss << new_line;
+ if (row_index == 0) {
+ ss << new_line;
+ }
+ }
+
+ ss << "|===";
+ return ss.str();
+ }
+
+ virtual ~AsciiDocExporter() {}
+
+private:
+ std::string add_formatted_cell(Cell &cell) const {
+ std::stringstream ss;
+ auto format = cell.format();
+ std::string cell_string = cell.get_text();
+
+ auto font_style = format.font_style_.value();
+
+ bool format_bold = false;
+ bool format_italic = false;
+ std::for_each(font_style.begin(), font_style.end(), [&](FontStyle &style) {
+ if (style == FontStyle::bold) {
+ format_bold = true;
+ } else if (style == FontStyle::italic) {
+ format_italic = true;
+ }
+ });
+
+ if (format_bold) {
+ ss << '*';
+ }
+ if (format_italic) {
+ ss << '_';
+ }
+
+ ss << cell_string;
+ if (format_italic) {
+ ss << '_';
+ }
+ if (format_bold) {
+ ss << '*';
+ }
+ return ss.str();
+ }
+
+ std::string add_alignment_header(Table &table) {
+ std::stringstream ss;
+ ss << (R"([cols=")");
+
+ size_t column_count = table[0].size();
+ size_t column_index = 0;
+ for (auto &cell : table[0]) {
+ auto format = cell.format();
+
+ if (format.font_align_.value() == FontAlign::left) {
+ ss << '<';
+ } else if (format.font_align_.value() == FontAlign::center) {
+ ss << '^';
+ } else if (format.font_align_.value() == FontAlign::right) {
+ ss << '>';
+ }
+
+ ++column_index;
+ if (column_index != column_count) {
+ ss << ",";
+ }
+ }
+
+ ss << R"("])";
+ ss << new_line;
+ ss << "|===";
+
+ return ss.str();
+ }
+};
+
+} // namespace tabulate
+
+/*
+ __ ___. .__ __
+_/ |______ \_ |__ __ __| | _____ _/ |_ ____
+\ __\__ \ | __ \| | \ | \__ \\ __\/ __ \
+ | | / __ \| \_\ \ | / |__/ __ \| | \ ___/
+ |__| (____ /___ /____/|____(____ /__| \___ >
+ \/ \/ \/ \/
+Table Maker for Modern C++
+https://github.com/p-ranav/tabulate
+
+Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+SPDX-License-Identifier: MIT
+Copyright (c) 2019 Pranav Srinivas Kumar <pranav.srinivas.kumar@gmail.com>.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+#ifndef TABULATE_EXPORT_HPP
+#define TABULATE_EXPORT_HPP
+
+// #ifdef _WIN32
+// #ifdef TABULATE_STATIC_LIB
+// #define TABULATE_API
+// #else
+// #ifdef TABULATE_EXPORTS
+// #define TABULATE_API __declspec(dllexport)
+// #else
+// #define TABULATE_API __declspec(dllimport)
+// #endif
+// #endif
+// #else
+// #define TABULATE_API
+// #endif
+
+// Project version
+#define TABULATE_VERSION_MAJOR 1
+#define TABULATE_VERSION_MINOR 5
+#define TABULATE_VERSION_PATCH 0
+
+// Composing the protocol version string from major, and minor
+#define TABULATE_CONCATENATE(A, B) TABULATE_CONCATENATE_IMPL(A, B)
+#define TABULATE_CONCATENATE_IMPL(A, B) A##B
+#define TABULATE_STRINGIFY(a) TABULATE_STRINGIFY_IMPL(a)
+#define TABULATE_STRINGIFY_IMPL(a) #a
+
+#endif
--- /dev/null
+///
+// expected - An implementation of std::expected with extensions
+// Written in 2017 by Sy Brand (tartanllama@gmail.com, @TartanLlama)
+//
+// Documentation available at http://tl.tartanllama.xyz/
+//
+// To the extent possible under law, the author(s) have dedicated all
+// copyright and related and neighboring rights to this software to the
+// public domain worldwide. This software is distributed without any warranty.
+//
+// You should have received a copy of the CC0 Public Domain Dedication
+// along with this software. If not, see
+// <http://creativecommons.org/publicdomain/zero/1.0/>.
+///
+
+#ifndef TL_EXPECTED_HPP
+#define TL_EXPECTED_HPP
+
+#define TL_EXPECTED_VERSION_MAJOR 1
+#define TL_EXPECTED_VERSION_MINOR 1
+#define TL_EXPECTED_VERSION_PATCH 0
+
+#include <exception>
+#include <functional>
+#include <type_traits>
+#include <utility>
+
+#if defined(__EXCEPTIONS) || defined(_CPPUNWIND)
+#define TL_EXPECTED_EXCEPTIONS_ENABLED
+#endif
+
+#if (defined(_MSC_VER) && _MSC_VER == 1900)
+#define TL_EXPECTED_MSVC2015
+#define TL_EXPECTED_MSVC2015_CONSTEXPR
+#else
+#define TL_EXPECTED_MSVC2015_CONSTEXPR constexpr
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 && \
+ !defined(__clang__))
+#define TL_EXPECTED_GCC49
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 4 && \
+ !defined(__clang__))
+#define TL_EXPECTED_GCC54
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 5 && \
+ !defined(__clang__))
+#define TL_EXPECTED_GCC55
+#endif
+
+#if !defined(TL_ASSERT)
+//can't have assert in constexpr in C++11 and GCC 4.9 has a compiler bug
+#if (__cplusplus > 201103L) && !defined(TL_EXPECTED_GCC49)
+#include <cassert>
+#define TL_ASSERT(x) assert(x)
+#else
+#define TL_ASSERT(x)
+#endif
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 && \
+ !defined(__clang__))
+// GCC < 5 doesn't support overloading on const&& for member functions
+
+#define TL_EXPECTED_NO_CONSTRR
+// GCC < 5 doesn't support some standard C++11 type traits
+#define TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \
+ std::has_trivial_copy_constructor<T>
+#define TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T) \
+ std::has_trivial_copy_assign<T>
+
+// This one will be different for GCC 5.7 if it's ever supported
+#define TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T) \
+ std::is_trivially_destructible<T>
+
+// GCC 5 < v < 8 has a bug in is_trivially_copy_constructible which breaks
+// std::vector for non-copyable types
+#elif (defined(__GNUC__) && __GNUC__ < 8 && !defined(__clang__))
+#ifndef TL_GCC_LESS_8_TRIVIALLY_COPY_CONSTRUCTIBLE_MUTEX
+#define TL_GCC_LESS_8_TRIVIALLY_COPY_CONSTRUCTIBLE_MUTEX
+namespace tl {
+namespace detail {
+template <class T>
+struct is_trivially_copy_constructible
+ : std::is_trivially_copy_constructible<T> {};
+#ifdef _GLIBCXX_VECTOR
+template <class T, class A>
+struct is_trivially_copy_constructible<std::vector<T, A>> : std::false_type {};
+#endif
+} // namespace detail
+} // namespace tl
+#endif
+
+#define TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \
+ tl::detail::is_trivially_copy_constructible<T>
+#define TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T) \
+ std::is_trivially_copy_assignable<T>
+#define TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T) \
+ std::is_trivially_destructible<T>
+#else
+#define TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \
+ std::is_trivially_copy_constructible<T>
+#define TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T) \
+ std::is_trivially_copy_assignable<T>
+#define TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T) \
+ std::is_trivially_destructible<T>
+#endif
+
+#if __cplusplus > 201103L
+#define TL_EXPECTED_CXX14
+#endif
+
+#ifdef TL_EXPECTED_GCC49
+#define TL_EXPECTED_GCC49_CONSTEXPR
+#else
+#define TL_EXPECTED_GCC49_CONSTEXPR constexpr
+#endif
+
+#if (__cplusplus == 201103L || defined(TL_EXPECTED_MSVC2015) || \
+ defined(TL_EXPECTED_GCC49))
+#define TL_EXPECTED_11_CONSTEXPR
+#else
+#define TL_EXPECTED_11_CONSTEXPR constexpr
+#endif
+
+namespace tl {
+template <class T, class E> class expected;
+
+#ifndef TL_MONOSTATE_INPLACE_MUTEX
+#define TL_MONOSTATE_INPLACE_MUTEX
+class monostate {};
+
+struct in_place_t {
+ explicit in_place_t() = default;
+};
+static constexpr in_place_t in_place{};
+#endif
+
+template <class E> class unexpected {
+public:
+ static_assert(!std::is_same<E, void>::value, "E must not be void");
+
+ unexpected() = delete;
+ constexpr explicit unexpected(const E &e) : m_val(e) {}
+
+ constexpr explicit unexpected(E &&e) : m_val(std::move(e)) {}
+
+ template <class... Args, typename std::enable_if<std::is_constructible<
+ E, Args &&...>::value>::type * = nullptr>
+ constexpr explicit unexpected(Args &&...args)
+ : m_val(std::forward<Args>(args)...) {}
+ template <
+ class U, class... Args,
+ typename std::enable_if<std::is_constructible<
+ E, std::initializer_list<U> &, Args &&...>::value>::type * = nullptr>
+ constexpr explicit unexpected(std::initializer_list<U> l, Args &&...args)
+ : m_val(l, std::forward<Args>(args)...) {}
+
+ constexpr const E &value() const & { return m_val; }
+ TL_EXPECTED_11_CONSTEXPR E &value() & { return m_val; }
+ TL_EXPECTED_11_CONSTEXPR E &&value() && { return std::move(m_val); }
+ constexpr const E &&value() const && { return std::move(m_val); }
+
+private:
+ E m_val;
+};
+
+#ifdef __cpp_deduction_guides
+template <class E> unexpected(E) -> unexpected<E>;
+#endif
+
+template <class E>
+constexpr bool operator==(const unexpected<E> &lhs, const unexpected<E> &rhs) {
+ return lhs.value() == rhs.value();
+}
+template <class E>
+constexpr bool operator!=(const unexpected<E> &lhs, const unexpected<E> &rhs) {
+ return lhs.value() != rhs.value();
+}
+template <class E>
+constexpr bool operator<(const unexpected<E> &lhs, const unexpected<E> &rhs) {
+ return lhs.value() < rhs.value();
+}
+template <class E>
+constexpr bool operator<=(const unexpected<E> &lhs, const unexpected<E> &rhs) {
+ return lhs.value() <= rhs.value();
+}
+template <class E>
+constexpr bool operator>(const unexpected<E> &lhs, const unexpected<E> &rhs) {
+ return lhs.value() > rhs.value();
+}
+template <class E>
+constexpr bool operator>=(const unexpected<E> &lhs, const unexpected<E> &rhs) {
+ return lhs.value() >= rhs.value();
+}
+
+template <class E>
+unexpected<typename std::decay<E>::type> make_unexpected(E &&e) {
+ return unexpected<typename std::decay<E>::type>(std::forward<E>(e));
+}
+
+struct unexpect_t {
+ unexpect_t() = default;
+};
+static constexpr unexpect_t unexpect{};
+
+namespace detail {
+template <typename E>
+[[noreturn]] TL_EXPECTED_11_CONSTEXPR void throw_exception(E &&e) {
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+ throw std::forward<E>(e);
+#else
+ (void)e;
+#ifdef _MSC_VER
+ __assume(0);
+#else
+ __builtin_unreachable();
+#endif
+#endif
+}
+
+#ifndef TL_TRAITS_MUTEX
+#define TL_TRAITS_MUTEX
+// C++14-style aliases for brevity
+template <class T> using remove_const_t = typename std::remove_const<T>::type;
+template <class T>
+using remove_reference_t = typename std::remove_reference<T>::type;
+template <class T> using decay_t = typename std::decay<T>::type;
+template <bool E, class T = void>
+using enable_if_t = typename std::enable_if<E, T>::type;
+template <bool B, class T, class F>
+using conditional_t = typename std::conditional<B, T, F>::type;
+
+// std::conjunction from C++17
+template <class...> struct conjunction : std::true_type {};
+template <class B> struct conjunction<B> : B {};
+template <class B, class... Bs>
+struct conjunction<B, Bs...>
+ : std::conditional<bool(B::value), conjunction<Bs...>, B>::type {};
+
+#if defined(_LIBCPP_VERSION) && __cplusplus == 201103L
+#define TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND
+#endif
+
+// In C++11 mode, there's an issue in libc++'s std::mem_fn
+// which results in a hard-error when using it in a noexcept expression
+// in some cases. This is a check to workaround the common failing case.
+#ifdef TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND
+template <class T>
+struct is_pointer_to_non_const_member_func : std::false_type {};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*)(Args...)>
+ : std::true_type {};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*)(Args...) &>
+ : std::true_type {};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*)(Args...) &&>
+ : std::true_type {};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*)(Args...) volatile>
+ : std::true_type {};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*)(Args...) volatile &>
+ : std::true_type {};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*)(Args...) volatile &&>
+ : std::true_type {};
+
+template <class T> struct is_const_or_const_ref : std::false_type {};
+template <class T> struct is_const_or_const_ref<T const &> : std::true_type {};
+template <class T> struct is_const_or_const_ref<T const> : std::true_type {};
+#endif
+
+// std::invoke from C++17
+// https://stackoverflow.com/questions/38288042/c11-14-invoke-workaround
+template <
+ typename Fn, typename... Args,
+#ifdef TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND
+ typename = enable_if_t<!(is_pointer_to_non_const_member_func<Fn>::value &&
+ is_const_or_const_ref<Args...>::value)>,
+#endif
+ typename = enable_if_t<std::is_member_pointer<decay_t<Fn>>::value>, int = 0>
+constexpr auto invoke(Fn &&f, Args &&...args) noexcept(
+ noexcept(std::mem_fn(f)(std::forward<Args>(args)...)))
+ -> decltype(std::mem_fn(f)(std::forward<Args>(args)...)) {
+ return std::mem_fn(f)(std::forward<Args>(args)...);
+}
+
+template <typename Fn, typename... Args,
+ typename = enable_if_t<!std::is_member_pointer<decay_t<Fn>>::value>>
+constexpr auto invoke(Fn &&f, Args &&...args) noexcept(
+ noexcept(std::forward<Fn>(f)(std::forward<Args>(args)...)))
+ -> decltype(std::forward<Fn>(f)(std::forward<Args>(args)...)) {
+ return std::forward<Fn>(f)(std::forward<Args>(args)...);
+}
+
+// std::invoke_result from C++17
+template <class F, class, class... Us> struct invoke_result_impl;
+
+template <class F, class... Us>
+struct invoke_result_impl<
+ F,
+ decltype(detail::invoke(std::declval<F>(), std::declval<Us>()...), void()),
+ Us...> {
+ using type =
+ decltype(detail::invoke(std::declval<F>(), std::declval<Us>()...));
+};
+
+template <class F, class... Us>
+using invoke_result = invoke_result_impl<F, void, Us...>;
+
+template <class F, class... Us>
+using invoke_result_t = typename invoke_result<F, Us...>::type;
+
+#if defined(_MSC_VER) && _MSC_VER <= 1900
+// TODO make a version which works with MSVC 2015
+template <class T, class U = T> struct is_swappable : std::true_type {};
+
+template <class T, class U = T> struct is_nothrow_swappable : std::true_type {};
+#else
+// https://stackoverflow.com/questions/26744589/what-is-a-proper-way-to-implement-is-swappable-to-test-for-the-swappable-concept
+namespace swap_adl_tests {
+// if swap ADL finds this then it would call std::swap otherwise (same
+// signature)
+struct tag {};
+
+template <class T> tag swap(T &, T &);
+template <class T, std::size_t N> tag swap(T (&a)[N], T (&b)[N]);
+
+// helper functions to test if an unqualified swap is possible, and if it
+// becomes std::swap
+template <class, class> std::false_type can_swap(...) noexcept(false);
+template <class T, class U,
+ class = decltype(swap(std::declval<T &>(), std::declval<U &>()))>
+std::true_type can_swap(int) noexcept(noexcept(swap(std::declval<T &>(),
+ std::declval<U &>())));
+
+template <class, class> std::false_type uses_std(...);
+template <class T, class U>
+std::is_same<decltype(swap(std::declval<T &>(), std::declval<U &>())), tag>
+uses_std(int);
+
+template <class T>
+struct is_std_swap_noexcept
+ : std::integral_constant<bool,
+ std::is_nothrow_move_constructible<T>::value &&
+ std::is_nothrow_move_assignable<T>::value> {};
+
+template <class T, std::size_t N>
+struct is_std_swap_noexcept<T[N]> : is_std_swap_noexcept<T> {};
+
+template <class T, class U>
+struct is_adl_swap_noexcept
+ : std::integral_constant<bool, noexcept(can_swap<T, U>(0))> {};
+} // namespace swap_adl_tests
+
+template <class T, class U = T>
+struct is_swappable
+ : std::integral_constant<
+ bool,
+ decltype(detail::swap_adl_tests::can_swap<T, U>(0))::value &&
+ (!decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value ||
+ (std::is_move_assignable<T>::value &&
+ std::is_move_constructible<T>::value))> {};
+
+template <class T, std::size_t N>
+struct is_swappable<T[N], T[N]>
+ : std::integral_constant<
+ bool,
+ decltype(detail::swap_adl_tests::can_swap<T[N], T[N]>(0))::value &&
+ (!decltype(detail::swap_adl_tests::uses_std<T[N], T[N]>(
+ 0))::value ||
+ is_swappable<T, T>::value)> {};
+
+template <class T, class U = T>
+struct is_nothrow_swappable
+ : std::integral_constant<
+ bool,
+ is_swappable<T, U>::value &&
+ ((decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value &&
+ detail::swap_adl_tests::is_std_swap_noexcept<T>::value) ||
+ (!decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value &&
+ detail::swap_adl_tests::is_adl_swap_noexcept<T, U>::value))> {};
+#endif
+#endif
+
+// Trait for checking if a type is a tl::expected
+template <class T> struct is_expected_impl : std::false_type {};
+template <class T, class E>
+struct is_expected_impl<expected<T, E>> : std::true_type {};
+template <class T> using is_expected = is_expected_impl<decay_t<T>>;
+
+template <class T, class E, class U>
+using expected_enable_forward_value = detail::enable_if_t<
+ std::is_constructible<T, U &&>::value &&
+ !std::is_same<detail::decay_t<U>, in_place_t>::value &&
+ !std::is_same<expected<T, E>, detail::decay_t<U>>::value &&
+ !std::is_same<unexpected<E>, detail::decay_t<U>>::value>;
+
+template <class T, class E, class U, class G, class UR, class GR>
+using expected_enable_from_other = detail::enable_if_t<
+ std::is_constructible<T, UR>::value &&
+ std::is_constructible<E, GR>::value &&
+ !std::is_constructible<T, expected<U, G> &>::value &&
+ !std::is_constructible<T, expected<U, G> &&>::value &&
+ !std::is_constructible<T, const expected<U, G> &>::value &&
+ !std::is_constructible<T, const expected<U, G> &&>::value &&
+ !std::is_convertible<expected<U, G> &, T>::value &&
+ !std::is_convertible<expected<U, G> &&, T>::value &&
+ !std::is_convertible<const expected<U, G> &, T>::value &&
+ !std::is_convertible<const expected<U, G> &&, T>::value>;
+
+template <class T, class U>
+using is_void_or = conditional_t<std::is_void<T>::value, std::true_type, U>;
+
+template <class T>
+using is_copy_constructible_or_void =
+ is_void_or<T, std::is_copy_constructible<T>>;
+
+template <class T>
+using is_move_constructible_or_void =
+ is_void_or<T, std::is_move_constructible<T>>;
+
+template <class T>
+using is_copy_assignable_or_void = is_void_or<T, std::is_copy_assignable<T>>;
+
+template <class T>
+using is_move_assignable_or_void = is_void_or<T, std::is_move_assignable<T>>;
+
+} // namespace detail
+
+namespace detail {
+struct no_init_t {};
+static constexpr no_init_t no_init{};
+
+// Implements the storage of the values, and ensures that the destructor is
+// trivial if it can be.
+//
+// This specialization is for where neither `T` or `E` is trivially
+// destructible, so the destructors must be called on destruction of the
+// `expected`
+template <class T, class E, bool = std::is_trivially_destructible<T>::value,
+ bool = std::is_trivially_destructible<E>::value>
+struct expected_storage_base {
+ constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {}
+ constexpr expected_storage_base(no_init_t) : m_no_init(), m_has_val(false) {}
+
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<T, Args &&...>::value> * =
+ nullptr>
+ constexpr expected_storage_base(in_place_t, Args &&...args)
+ : m_val(std::forward<Args>(args)...), m_has_val(true) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr expected_storage_base(in_place_t, std::initializer_list<U> il,
+ Args &&...args)
+ : m_val(il, std::forward<Args>(args)...), m_has_val(true) {}
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+ nullptr>
+ constexpr explicit expected_storage_base(unexpect_t, Args &&...args)
+ : m_unexpect(std::forward<Args>(args)...), m_has_val(false) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr explicit expected_storage_base(unexpect_t,
+ std::initializer_list<U> il,
+ Args &&...args)
+ : m_unexpect(il, std::forward<Args>(args)...), m_has_val(false) {}
+
+ ~expected_storage_base() {
+ if (m_has_val) {
+ m_val.~T();
+ } else {
+ m_unexpect.~unexpected<E>();
+ }
+ }
+ union {
+ T m_val;
+ unexpected<E> m_unexpect;
+ char m_no_init;
+ };
+ bool m_has_val;
+};
+
+// This specialization is for when both `T` and `E` are trivially-destructible,
+// so the destructor of the `expected` can be trivial.
+template <class T, class E> struct expected_storage_base<T, E, true, true> {
+ constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {}
+ constexpr expected_storage_base(no_init_t) : m_no_init(), m_has_val(false) {}
+
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<T, Args &&...>::value> * =
+ nullptr>
+ constexpr expected_storage_base(in_place_t, Args &&...args)
+ : m_val(std::forward<Args>(args)...), m_has_val(true) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr expected_storage_base(in_place_t, std::initializer_list<U> il,
+ Args &&...args)
+ : m_val(il, std::forward<Args>(args)...), m_has_val(true) {}
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+ nullptr>
+ constexpr explicit expected_storage_base(unexpect_t, Args &&...args)
+ : m_unexpect(std::forward<Args>(args)...), m_has_val(false) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr explicit expected_storage_base(unexpect_t,
+ std::initializer_list<U> il,
+ Args &&...args)
+ : m_unexpect(il, std::forward<Args>(args)...), m_has_val(false) {}
+
+ ~expected_storage_base() = default;
+ union {
+ T m_val;
+ unexpected<E> m_unexpect;
+ char m_no_init;
+ };
+ bool m_has_val;
+};
+
+// T is trivial, E is not.
+template <class T, class E> struct expected_storage_base<T, E, true, false> {
+ constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {}
+ TL_EXPECTED_MSVC2015_CONSTEXPR expected_storage_base(no_init_t)
+ : m_no_init(), m_has_val(false) {}
+
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<T, Args &&...>::value> * =
+ nullptr>
+ constexpr expected_storage_base(in_place_t, Args &&...args)
+ : m_val(std::forward<Args>(args)...), m_has_val(true) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr expected_storage_base(in_place_t, std::initializer_list<U> il,
+ Args &&...args)
+ : m_val(il, std::forward<Args>(args)...), m_has_val(true) {}
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+ nullptr>
+ constexpr explicit expected_storage_base(unexpect_t, Args &&...args)
+ : m_unexpect(std::forward<Args>(args)...), m_has_val(false) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr explicit expected_storage_base(unexpect_t,
+ std::initializer_list<U> il,
+ Args &&...args)
+ : m_unexpect(il, std::forward<Args>(args)...), m_has_val(false) {}
+
+ ~expected_storage_base() {
+ if (!m_has_val) {
+ m_unexpect.~unexpected<E>();
+ }
+ }
+
+ union {
+ T m_val;
+ unexpected<E> m_unexpect;
+ char m_no_init;
+ };
+ bool m_has_val;
+};
+
+// E is trivial, T is not.
+template <class T, class E> struct expected_storage_base<T, E, false, true> {
+ constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {}
+ constexpr expected_storage_base(no_init_t) : m_no_init(), m_has_val(false) {}
+
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<T, Args &&...>::value> * =
+ nullptr>
+ constexpr expected_storage_base(in_place_t, Args &&...args)
+ : m_val(std::forward<Args>(args)...), m_has_val(true) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr expected_storage_base(in_place_t, std::initializer_list<U> il,
+ Args &&...args)
+ : m_val(il, std::forward<Args>(args)...), m_has_val(true) {}
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+ nullptr>
+ constexpr explicit expected_storage_base(unexpect_t, Args &&...args)
+ : m_unexpect(std::forward<Args>(args)...), m_has_val(false) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr explicit expected_storage_base(unexpect_t,
+ std::initializer_list<U> il,
+ Args &&...args)
+ : m_unexpect(il, std::forward<Args>(args)...), m_has_val(false) {}
+
+ ~expected_storage_base() {
+ if (m_has_val) {
+ m_val.~T();
+ }
+ }
+ union {
+ T m_val;
+ unexpected<E> m_unexpect;
+ char m_no_init;
+ };
+ bool m_has_val;
+};
+
+// `T` is `void`, `E` is trivially-destructible
+template <class E> struct expected_storage_base<void, E, false, true> {
+ #if __GNUC__ <= 5
+ //no constexpr for GCC 4/5 bug
+ #else
+ TL_EXPECTED_MSVC2015_CONSTEXPR
+ #endif
+ expected_storage_base() : m_has_val(true) {}
+
+ constexpr expected_storage_base(no_init_t) : m_val(), m_has_val(false) {}
+
+ constexpr expected_storage_base(in_place_t) : m_has_val(true) {}
+
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+ nullptr>
+ constexpr explicit expected_storage_base(unexpect_t, Args &&...args)
+ : m_unexpect(std::forward<Args>(args)...), m_has_val(false) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr explicit expected_storage_base(unexpect_t,
+ std::initializer_list<U> il,
+ Args &&...args)
+ : m_unexpect(il, std::forward<Args>(args)...), m_has_val(false) {}
+
+ ~expected_storage_base() = default;
+ struct dummy {};
+ union {
+ unexpected<E> m_unexpect;
+ dummy m_val;
+ };
+ bool m_has_val;
+};
+
+// `T` is `void`, `E` is not trivially-destructible
+template <class E> struct expected_storage_base<void, E, false, false> {
+ constexpr expected_storage_base() : m_dummy(), m_has_val(true) {}
+ constexpr expected_storage_base(no_init_t) : m_dummy(), m_has_val(false) {}
+
+ constexpr expected_storage_base(in_place_t) : m_dummy(), m_has_val(true) {}
+
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+ nullptr>
+ constexpr explicit expected_storage_base(unexpect_t, Args &&...args)
+ : m_unexpect(std::forward<Args>(args)...), m_has_val(false) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr explicit expected_storage_base(unexpect_t,
+ std::initializer_list<U> il,
+ Args &&...args)
+ : m_unexpect(il, std::forward<Args>(args)...), m_has_val(false) {}
+
+ ~expected_storage_base() {
+ if (!m_has_val) {
+ m_unexpect.~unexpected<E>();
+ }
+ }
+
+ union {
+ unexpected<E> m_unexpect;
+ char m_dummy;
+ };
+ bool m_has_val;
+};
+
+// This base class provides some handy member functions which can be used in
+// further derived classes
+template <class T, class E>
+struct expected_operations_base : expected_storage_base<T, E> {
+ using expected_storage_base<T, E>::expected_storage_base;
+
+ template <class... Args> void construct(Args &&...args) noexcept {
+ new (std::addressof(this->m_val)) T(std::forward<Args>(args)...);
+ this->m_has_val = true;
+ }
+
+ template <class Rhs> void construct_with(Rhs &&rhs) noexcept {
+ new (std::addressof(this->m_val)) T(std::forward<Rhs>(rhs).get());
+ this->m_has_val = true;
+ }
+
+ template <class... Args> void construct_error(Args &&...args) noexcept {
+ new (std::addressof(this->m_unexpect))
+ unexpected<E>(std::forward<Args>(args)...);
+ this->m_has_val = false;
+ }
+
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+
+ // These assign overloads ensure that the most efficient assignment
+ // implementation is used while maintaining the strong exception guarantee.
+ // The problematic case is where rhs has a value, but *this does not.
+ //
+ // This overload handles the case where we can just copy-construct `T`
+ // directly into place without throwing.
+ template <class U = T,
+ detail::enable_if_t<std::is_nothrow_copy_constructible<U>::value>
+ * = nullptr>
+ void assign(const expected_operations_base &rhs) noexcept {
+ if (!this->m_has_val && rhs.m_has_val) {
+ geterr().~unexpected<E>();
+ construct(rhs.get());
+ } else {
+ assign_common(rhs);
+ }
+ }
+
+ // This overload handles the case where we can attempt to create a copy of
+ // `T`, then no-throw move it into place if the copy was successful.
+ template <class U = T,
+ detail::enable_if_t<!std::is_nothrow_copy_constructible<U>::value &&
+ std::is_nothrow_move_constructible<U>::value>
+ * = nullptr>
+ void assign(const expected_operations_base &rhs) noexcept {
+ if (!this->m_has_val && rhs.m_has_val) {
+ T tmp = rhs.get();
+ geterr().~unexpected<E>();
+ construct(std::move(tmp));
+ } else {
+ assign_common(rhs);
+ }
+ }
+
+ // This overload is the worst-case, where we have to move-construct the
+ // unexpected value into temporary storage, then try to copy the T into place.
+ // If the construction succeeds, then everything is fine, but if it throws,
+ // then we move the old unexpected value back into place before rethrowing the
+ // exception.
+ template <class U = T,
+ detail::enable_if_t<!std::is_nothrow_copy_constructible<U>::value &&
+ !std::is_nothrow_move_constructible<U>::value>
+ * = nullptr>
+ void assign(const expected_operations_base &rhs) {
+ if (!this->m_has_val && rhs.m_has_val) {
+ auto tmp = std::move(geterr());
+ geterr().~unexpected<E>();
+
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+ try {
+ construct(rhs.get());
+ } catch (...) {
+ geterr() = std::move(tmp);
+ throw;
+ }
+#else
+ construct(rhs.get());
+#endif
+ } else {
+ assign_common(rhs);
+ }
+ }
+
+ // These overloads do the same as above, but for rvalues
+ template <class U = T,
+ detail::enable_if_t<std::is_nothrow_move_constructible<U>::value>
+ * = nullptr>
+ void assign(expected_operations_base &&rhs) noexcept {
+ if (!this->m_has_val && rhs.m_has_val) {
+ geterr().~unexpected<E>();
+ construct(std::move(rhs).get());
+ } else {
+ assign_common(std::move(rhs));
+ }
+ }
+
+ template <class U = T,
+ detail::enable_if_t<!std::is_nothrow_move_constructible<U>::value>
+ * = nullptr>
+ void assign(expected_operations_base &&rhs) {
+ if (!this->m_has_val && rhs.m_has_val) {
+ auto tmp = std::move(geterr());
+ geterr().~unexpected<E>();
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+ try {
+ construct(std::move(rhs).get());
+ } catch (...) {
+ geterr() = std::move(tmp);
+ throw;
+ }
+#else
+ construct(std::move(rhs).get());
+#endif
+ } else {
+ assign_common(std::move(rhs));
+ }
+ }
+
+#else
+
+ // If exceptions are disabled then we can just copy-construct
+ void assign(const expected_operations_base &rhs) noexcept {
+ if (!this->m_has_val && rhs.m_has_val) {
+ geterr().~unexpected<E>();
+ construct(rhs.get());
+ } else {
+ assign_common(rhs);
+ }
+ }
+
+ void assign(expected_operations_base &&rhs) noexcept {
+ if (!this->m_has_val && rhs.m_has_val) {
+ geterr().~unexpected<E>();
+ construct(std::move(rhs).get());
+ } else {
+ assign_common(std::move(rhs));
+ }
+ }
+
+#endif
+
+ // The common part of move/copy assigning
+ template <class Rhs> void assign_common(Rhs &&rhs) {
+ if (this->m_has_val) {
+ if (rhs.m_has_val) {
+ get() = std::forward<Rhs>(rhs).get();
+ } else {
+ destroy_val();
+ construct_error(std::forward<Rhs>(rhs).geterr());
+ }
+ } else {
+ if (!rhs.m_has_val) {
+ geterr() = std::forward<Rhs>(rhs).geterr();
+ }
+ }
+ }
+
+ bool has_value() const { return this->m_has_val; }
+
+ TL_EXPECTED_11_CONSTEXPR T &get() & { return this->m_val; }
+ constexpr const T &get() const & { return this->m_val; }
+ TL_EXPECTED_11_CONSTEXPR T &&get() && { return std::move(this->m_val); }
+#ifndef TL_EXPECTED_NO_CONSTRR
+ constexpr const T &&get() const && { return std::move(this->m_val); }
+#endif
+
+ TL_EXPECTED_11_CONSTEXPR unexpected<E> &geterr() & {
+ return this->m_unexpect;
+ }
+ constexpr const unexpected<E> &geterr() const & { return this->m_unexpect; }
+ TL_EXPECTED_11_CONSTEXPR unexpected<E> &&geterr() && {
+ return std::move(this->m_unexpect);
+ }
+#ifndef TL_EXPECTED_NO_CONSTRR
+ constexpr const unexpected<E> &&geterr() const && {
+ return std::move(this->m_unexpect);
+ }
+#endif
+
+ TL_EXPECTED_11_CONSTEXPR void destroy_val() { get().~T(); }
+};
+
+// This base class provides some handy member functions which can be used in
+// further derived classes
+template <class E>
+struct expected_operations_base<void, E> : expected_storage_base<void, E> {
+ using expected_storage_base<void, E>::expected_storage_base;
+
+ template <class... Args> void construct() noexcept { this->m_has_val = true; }
+
+ // This function doesn't use its argument, but needs it so that code in
+ // levels above this can work independently of whether T is void
+ template <class Rhs> void construct_with(Rhs &&) noexcept {
+ this->m_has_val = true;
+ }
+
+ template <class... Args> void construct_error(Args &&...args) noexcept {
+ new (std::addressof(this->m_unexpect))
+ unexpected<E>(std::forward<Args>(args)...);
+ this->m_has_val = false;
+ }
+
+ template <class Rhs> void assign(Rhs &&rhs) noexcept {
+ if (!this->m_has_val) {
+ if (rhs.m_has_val) {
+ geterr().~unexpected<E>();
+ construct();
+ } else {
+ geterr() = std::forward<Rhs>(rhs).geterr();
+ }
+ } else {
+ if (!rhs.m_has_val) {
+ construct_error(std::forward<Rhs>(rhs).geterr());
+ }
+ }
+ }
+
+ bool has_value() const { return this->m_has_val; }
+
+ TL_EXPECTED_11_CONSTEXPR unexpected<E> &geterr() & {
+ return this->m_unexpect;
+ }
+ constexpr const unexpected<E> &geterr() const & { return this->m_unexpect; }
+ TL_EXPECTED_11_CONSTEXPR unexpected<E> &&geterr() && {
+ return std::move(this->m_unexpect);
+ }
+#ifndef TL_EXPECTED_NO_CONSTRR
+ constexpr const unexpected<E> &&geterr() const && {
+ return std::move(this->m_unexpect);
+ }
+#endif
+
+ TL_EXPECTED_11_CONSTEXPR void destroy_val() {
+ // no-op
+ }
+};
+
+// This class manages conditionally having a trivial copy constructor
+// This specialization is for when T and E are trivially copy constructible
+template <class T, class E,
+ bool = is_void_or<T, TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T)>::
+ value &&TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(E)::value>
+struct expected_copy_base : expected_operations_base<T, E> {
+ using expected_operations_base<T, E>::expected_operations_base;
+};
+
+// This specialization is for when T or E are not trivially copy constructible
+template <class T, class E>
+struct expected_copy_base<T, E, false> : expected_operations_base<T, E> {
+ using expected_operations_base<T, E>::expected_operations_base;
+
+ expected_copy_base() = default;
+ expected_copy_base(const expected_copy_base &rhs)
+ : expected_operations_base<T, E>(no_init) {
+ if (rhs.has_value()) {
+ this->construct_with(rhs);
+ } else {
+ this->construct_error(rhs.geterr());
+ }
+ }
+
+ expected_copy_base(expected_copy_base &&rhs) = default;
+ expected_copy_base &operator=(const expected_copy_base &rhs) = default;
+ expected_copy_base &operator=(expected_copy_base &&rhs) = default;
+};
+
+// This class manages conditionally having a trivial move constructor
+// Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it
+// doesn't implement an analogue to std::is_trivially_move_constructible. We
+// have to make do with a non-trivial move constructor even if T is trivially
+// move constructible
+#ifndef TL_EXPECTED_GCC49
+template <class T, class E,
+ bool = is_void_or<T, std::is_trivially_move_constructible<T>>::value
+ &&std::is_trivially_move_constructible<E>::value>
+struct expected_move_base : expected_copy_base<T, E> {
+ using expected_copy_base<T, E>::expected_copy_base;
+};
+#else
+template <class T, class E, bool = false> struct expected_move_base;
+#endif
+template <class T, class E>
+struct expected_move_base<T, E, false> : expected_copy_base<T, E> {
+ using expected_copy_base<T, E>::expected_copy_base;
+
+ expected_move_base() = default;
+ expected_move_base(const expected_move_base &rhs) = default;
+
+ expected_move_base(expected_move_base &&rhs) noexcept(
+ std::is_nothrow_move_constructible<T>::value)
+ : expected_copy_base<T, E>(no_init) {
+ if (rhs.has_value()) {
+ this->construct_with(std::move(rhs));
+ } else {
+ this->construct_error(std::move(rhs.geterr()));
+ }
+ }
+ expected_move_base &operator=(const expected_move_base &rhs) = default;
+ expected_move_base &operator=(expected_move_base &&rhs) = default;
+};
+
+// This class manages conditionally having a trivial copy assignment operator
+template <class T, class E,
+ bool = is_void_or<
+ T, conjunction<TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T),
+ TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T),
+ TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T)>>::value
+ &&TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(E)::value
+ &&TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(E)::value
+ &&TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(E)::value>
+struct expected_copy_assign_base : expected_move_base<T, E> {
+ using expected_move_base<T, E>::expected_move_base;
+};
+
+template <class T, class E>
+struct expected_copy_assign_base<T, E, false> : expected_move_base<T, E> {
+ using expected_move_base<T, E>::expected_move_base;
+
+ expected_copy_assign_base() = default;
+ expected_copy_assign_base(const expected_copy_assign_base &rhs) = default;
+
+ expected_copy_assign_base(expected_copy_assign_base &&rhs) = default;
+ expected_copy_assign_base &operator=(const expected_copy_assign_base &rhs) {
+ this->assign(rhs);
+ return *this;
+ }
+ expected_copy_assign_base &
+ operator=(expected_copy_assign_base &&rhs) = default;
+};
+
+// This class manages conditionally having a trivial move assignment operator
+// Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it
+// doesn't implement an analogue to std::is_trivially_move_assignable. We have
+// to make do with a non-trivial move assignment operator even if T is trivially
+// move assignable
+#ifndef TL_EXPECTED_GCC49
+template <class T, class E,
+ bool =
+ is_void_or<T, conjunction<std::is_trivially_destructible<T>,
+ std::is_trivially_move_constructible<T>,
+ std::is_trivially_move_assignable<T>>>::
+ value &&std::is_trivially_destructible<E>::value
+ &&std::is_trivially_move_constructible<E>::value
+ &&std::is_trivially_move_assignable<E>::value>
+struct expected_move_assign_base : expected_copy_assign_base<T, E> {
+ using expected_copy_assign_base<T, E>::expected_copy_assign_base;
+};
+#else
+template <class T, class E, bool = false> struct expected_move_assign_base;
+#endif
+
+template <class T, class E>
+struct expected_move_assign_base<T, E, false>
+ : expected_copy_assign_base<T, E> {
+ using expected_copy_assign_base<T, E>::expected_copy_assign_base;
+
+ expected_move_assign_base() = default;
+ expected_move_assign_base(const expected_move_assign_base &rhs) = default;
+
+ expected_move_assign_base(expected_move_assign_base &&rhs) = default;
+
+ expected_move_assign_base &
+ operator=(const expected_move_assign_base &rhs) = default;
+
+ expected_move_assign_base &
+ operator=(expected_move_assign_base &&rhs) noexcept(
+ std::is_nothrow_move_constructible<T>::value
+ &&std::is_nothrow_move_assignable<T>::value) {
+ this->assign(std::move(rhs));
+ return *this;
+ }
+};
+
+// expected_delete_ctor_base will conditionally delete copy and move
+// constructors depending on whether T is copy/move constructible
+template <class T, class E,
+ bool EnableCopy = (is_copy_constructible_or_void<T>::value &&
+ std::is_copy_constructible<E>::value),
+ bool EnableMove = (is_move_constructible_or_void<T>::value &&
+ std::is_move_constructible<E>::value)>
+struct expected_delete_ctor_base {
+ expected_delete_ctor_base() = default;
+ expected_delete_ctor_base(const expected_delete_ctor_base &) = default;
+ expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = default;
+ expected_delete_ctor_base &
+ operator=(const expected_delete_ctor_base &) = default;
+ expected_delete_ctor_base &
+ operator=(expected_delete_ctor_base &&) noexcept = default;
+};
+
+template <class T, class E>
+struct expected_delete_ctor_base<T, E, true, false> {
+ expected_delete_ctor_base() = default;
+ expected_delete_ctor_base(const expected_delete_ctor_base &) = default;
+ expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = delete;
+ expected_delete_ctor_base &
+ operator=(const expected_delete_ctor_base &) = default;
+ expected_delete_ctor_base &
+ operator=(expected_delete_ctor_base &&) noexcept = default;
+};
+
+template <class T, class E>
+struct expected_delete_ctor_base<T, E, false, true> {
+ expected_delete_ctor_base() = default;
+ expected_delete_ctor_base(const expected_delete_ctor_base &) = delete;
+ expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = default;
+ expected_delete_ctor_base &
+ operator=(const expected_delete_ctor_base &) = default;
+ expected_delete_ctor_base &
+ operator=(expected_delete_ctor_base &&) noexcept = default;
+};
+
+template <class T, class E>
+struct expected_delete_ctor_base<T, E, false, false> {
+ expected_delete_ctor_base() = default;
+ expected_delete_ctor_base(const expected_delete_ctor_base &) = delete;
+ expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = delete;
+ expected_delete_ctor_base &
+ operator=(const expected_delete_ctor_base &) = default;
+ expected_delete_ctor_base &
+ operator=(expected_delete_ctor_base &&) noexcept = default;
+};
+
+// expected_delete_assign_base will conditionally delete copy and move
+// constructors depending on whether T and E are copy/move constructible +
+// assignable
+template <class T, class E,
+ bool EnableCopy = (is_copy_constructible_or_void<T>::value &&
+ std::is_copy_constructible<E>::value &&
+ is_copy_assignable_or_void<T>::value &&
+ std::is_copy_assignable<E>::value),
+ bool EnableMove = (is_move_constructible_or_void<T>::value &&
+ std::is_move_constructible<E>::value &&
+ is_move_assignable_or_void<T>::value &&
+ std::is_move_assignable<E>::value)>
+struct expected_delete_assign_base {
+ expected_delete_assign_base() = default;
+ expected_delete_assign_base(const expected_delete_assign_base &) = default;
+ expected_delete_assign_base(expected_delete_assign_base &&) noexcept =
+ default;
+ expected_delete_assign_base &
+ operator=(const expected_delete_assign_base &) = default;
+ expected_delete_assign_base &
+ operator=(expected_delete_assign_base &&) noexcept = default;
+};
+
+template <class T, class E>
+struct expected_delete_assign_base<T, E, true, false> {
+ expected_delete_assign_base() = default;
+ expected_delete_assign_base(const expected_delete_assign_base &) = default;
+ expected_delete_assign_base(expected_delete_assign_base &&) noexcept =
+ default;
+ expected_delete_assign_base &
+ operator=(const expected_delete_assign_base &) = default;
+ expected_delete_assign_base &
+ operator=(expected_delete_assign_base &&) noexcept = delete;
+};
+
+template <class T, class E>
+struct expected_delete_assign_base<T, E, false, true> {
+ expected_delete_assign_base() = default;
+ expected_delete_assign_base(const expected_delete_assign_base &) = default;
+ expected_delete_assign_base(expected_delete_assign_base &&) noexcept =
+ default;
+ expected_delete_assign_base &
+ operator=(const expected_delete_assign_base &) = delete;
+ expected_delete_assign_base &
+ operator=(expected_delete_assign_base &&) noexcept = default;
+};
+
+template <class T, class E>
+struct expected_delete_assign_base<T, E, false, false> {
+ expected_delete_assign_base() = default;
+ expected_delete_assign_base(const expected_delete_assign_base &) = default;
+ expected_delete_assign_base(expected_delete_assign_base &&) noexcept =
+ default;
+ expected_delete_assign_base &
+ operator=(const expected_delete_assign_base &) = delete;
+ expected_delete_assign_base &
+ operator=(expected_delete_assign_base &&) noexcept = delete;
+};
+
+// This is needed to be able to construct the expected_default_ctor_base which
+// follows, while still conditionally deleting the default constructor.
+struct default_constructor_tag {
+ explicit constexpr default_constructor_tag() = default;
+};
+
+// expected_default_ctor_base will ensure that expected has a deleted default
+// consturctor if T is not default constructible.
+// This specialization is for when T is default constructible
+template <class T, class E,
+ bool Enable =
+ std::is_default_constructible<T>::value || std::is_void<T>::value>
+struct expected_default_ctor_base {
+ constexpr expected_default_ctor_base() noexcept = default;
+ constexpr expected_default_ctor_base(
+ expected_default_ctor_base const &) noexcept = default;
+ constexpr expected_default_ctor_base(expected_default_ctor_base &&) noexcept =
+ default;
+ expected_default_ctor_base &
+ operator=(expected_default_ctor_base const &) noexcept = default;
+ expected_default_ctor_base &
+ operator=(expected_default_ctor_base &&) noexcept = default;
+
+ constexpr explicit expected_default_ctor_base(default_constructor_tag) {}
+};
+
+// This specialization is for when T is not default constructible
+template <class T, class E> struct expected_default_ctor_base<T, E, false> {
+ constexpr expected_default_ctor_base() noexcept = delete;
+ constexpr expected_default_ctor_base(
+ expected_default_ctor_base const &) noexcept = default;
+ constexpr expected_default_ctor_base(expected_default_ctor_base &&) noexcept =
+ default;
+ expected_default_ctor_base &
+ operator=(expected_default_ctor_base const &) noexcept = default;
+ expected_default_ctor_base &
+ operator=(expected_default_ctor_base &&) noexcept = default;
+
+ constexpr explicit expected_default_ctor_base(default_constructor_tag) {}
+};
+} // namespace detail
+
+template <class E> class bad_expected_access : public std::exception {
+public:
+ explicit bad_expected_access(E e) : m_val(std::move(e)) {}
+
+ virtual const char *what() const noexcept override {
+ return "Bad expected access";
+ }
+
+ const E &error() const & { return m_val; }
+ E &error() & { return m_val; }
+ const E &&error() const && { return std::move(m_val); }
+ E &&error() && { return std::move(m_val); }
+
+private:
+ E m_val;
+};
+
+/// An `expected<T, E>` object is an object that contains the storage for
+/// another object and manages the lifetime of this contained object `T`.
+/// Alternatively it could contain the storage for another unexpected object
+/// `E`. The contained object may not be initialized after the expected object
+/// has been initialized, and may not be destroyed before the expected object
+/// has been destroyed. The initialization state of the contained object is
+/// tracked by the expected object.
+template <class T, class E>
+class expected : private detail::expected_move_assign_base<T, E>,
+ private detail::expected_delete_ctor_base<T, E>,
+ private detail::expected_delete_assign_base<T, E>,
+ private detail::expected_default_ctor_base<T, E> {
+ static_assert(!std::is_reference<T>::value, "T must not be a reference");
+ static_assert(!std::is_same<T, std::remove_cv<in_place_t>::type>::value,
+ "T must not be in_place_t");
+ static_assert(!std::is_same<T, std::remove_cv<unexpect_t>::type>::value,
+ "T must not be unexpect_t");
+ static_assert(
+ !std::is_same<T, typename std::remove_cv<unexpected<E>>::type>::value,
+ "T must not be unexpected<E>");
+ static_assert(!std::is_reference<E>::value, "E must not be a reference");
+
+ T *valptr() { return std::addressof(this->m_val); }
+ const T *valptr() const { return std::addressof(this->m_val); }
+ unexpected<E> *errptr() { return std::addressof(this->m_unexpect); }
+ const unexpected<E> *errptr() const {
+ return std::addressof(this->m_unexpect);
+ }
+
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ TL_EXPECTED_11_CONSTEXPR U &val() {
+ return this->m_val;
+ }
+ TL_EXPECTED_11_CONSTEXPR unexpected<E> &err() { return this->m_unexpect; }
+
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ constexpr const U &val() const {
+ return this->m_val;
+ }
+ constexpr const unexpected<E> &err() const { return this->m_unexpect; }
+
+ using impl_base = detail::expected_move_assign_base<T, E>;
+ using ctor_base = detail::expected_default_ctor_base<T, E>;
+
+public:
+ typedef T value_type;
+ typedef E error_type;
+ typedef unexpected<E> unexpected_type;
+
+#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \
+ !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55)
+ template <class F> TL_EXPECTED_11_CONSTEXPR auto and_then(F &&f) & {
+ return and_then_impl(*this, std::forward<F>(f));
+ }
+ template <class F> TL_EXPECTED_11_CONSTEXPR auto and_then(F &&f) && {
+ return and_then_impl(std::move(*this), std::forward<F>(f));
+ }
+ template <class F> constexpr auto and_then(F &&f) const & {
+ return and_then_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+ template <class F> constexpr auto and_then(F &&f) const && {
+ return and_then_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+
+#else
+ template <class F>
+ TL_EXPECTED_11_CONSTEXPR auto
+ and_then(F &&f) & -> decltype(and_then_impl(std::declval<expected &>(),
+ std::forward<F>(f))) {
+ return and_then_impl(*this, std::forward<F>(f));
+ }
+ template <class F>
+ TL_EXPECTED_11_CONSTEXPR auto
+ and_then(F &&f) && -> decltype(and_then_impl(std::declval<expected &&>(),
+ std::forward<F>(f))) {
+ return and_then_impl(std::move(*this), std::forward<F>(f));
+ }
+ template <class F>
+ constexpr auto and_then(F &&f) const & -> decltype(and_then_impl(
+ std::declval<expected const &>(), std::forward<F>(f))) {
+ return and_then_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+ template <class F>
+ constexpr auto and_then(F &&f) const && -> decltype(and_then_impl(
+ std::declval<expected const &&>(), std::forward<F>(f))) {
+ return and_then_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+#endif
+
+#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \
+ !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55)
+ template <class F> TL_EXPECTED_11_CONSTEXPR auto map(F &&f) & {
+ return expected_map_impl(*this, std::forward<F>(f));
+ }
+ template <class F> TL_EXPECTED_11_CONSTEXPR auto map(F &&f) && {
+ return expected_map_impl(std::move(*this), std::forward<F>(f));
+ }
+ template <class F> constexpr auto map(F &&f) const & {
+ return expected_map_impl(*this, std::forward<F>(f));
+ }
+ template <class F> constexpr auto map(F &&f) const && {
+ return expected_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#else
+ template <class F>
+ TL_EXPECTED_11_CONSTEXPR decltype(expected_map_impl(
+ std::declval<expected &>(), std::declval<F &&>()))
+ map(F &&f) & {
+ return expected_map_impl(*this, std::forward<F>(f));
+ }
+ template <class F>
+ TL_EXPECTED_11_CONSTEXPR decltype(expected_map_impl(std::declval<expected>(),
+ std::declval<F &&>()))
+ map(F &&f) && {
+ return expected_map_impl(std::move(*this), std::forward<F>(f));
+ }
+ template <class F>
+ constexpr decltype(expected_map_impl(std::declval<const expected &>(),
+ std::declval<F &&>()))
+ map(F &&f) const & {
+ return expected_map_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+ template <class F>
+ constexpr decltype(expected_map_impl(std::declval<const expected &&>(),
+ std::declval<F &&>()))
+ map(F &&f) const && {
+ return expected_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+#endif
+
+#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \
+ !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55)
+ template <class F> TL_EXPECTED_11_CONSTEXPR auto transform(F &&f) & {
+ return expected_map_impl(*this, std::forward<F>(f));
+ }
+ template <class F> TL_EXPECTED_11_CONSTEXPR auto transform(F &&f) && {
+ return expected_map_impl(std::move(*this), std::forward<F>(f));
+ }
+ template <class F> constexpr auto transform(F &&f) const & {
+ return expected_map_impl(*this, std::forward<F>(f));
+ }
+ template <class F> constexpr auto transform(F &&f) const && {
+ return expected_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#else
+ template <class F>
+ TL_EXPECTED_11_CONSTEXPR decltype(expected_map_impl(
+ std::declval<expected &>(), std::declval<F &&>()))
+ transform(F &&f) & {
+ return expected_map_impl(*this, std::forward<F>(f));
+ }
+ template <class F>
+ TL_EXPECTED_11_CONSTEXPR decltype(expected_map_impl(std::declval<expected>(),
+ std::declval<F &&>()))
+ transform(F &&f) && {
+ return expected_map_impl(std::move(*this), std::forward<F>(f));
+ }
+ template <class F>
+ constexpr decltype(expected_map_impl(std::declval<const expected &>(),
+ std::declval<F &&>()))
+ transform(F &&f) const & {
+ return expected_map_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+ template <class F>
+ constexpr decltype(expected_map_impl(std::declval<const expected &&>(),
+ std::declval<F &&>()))
+ transform(F &&f) const && {
+ return expected_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+#endif
+
+#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \
+ !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55)
+ template <class F> TL_EXPECTED_11_CONSTEXPR auto map_error(F &&f) & {
+ return map_error_impl(*this, std::forward<F>(f));
+ }
+ template <class F> TL_EXPECTED_11_CONSTEXPR auto map_error(F &&f) && {
+ return map_error_impl(std::move(*this), std::forward<F>(f));
+ }
+ template <class F> constexpr auto map_error(F &&f) const & {
+ return map_error_impl(*this, std::forward<F>(f));
+ }
+ template <class F> constexpr auto map_error(F &&f) const && {
+ return map_error_impl(std::move(*this), std::forward<F>(f));
+ }
+#else
+ template <class F>
+ TL_EXPECTED_11_CONSTEXPR decltype(map_error_impl(std::declval<expected &>(),
+ std::declval<F &&>()))
+ map_error(F &&f) & {
+ return map_error_impl(*this, std::forward<F>(f));
+ }
+ template <class F>
+ TL_EXPECTED_11_CONSTEXPR decltype(map_error_impl(std::declval<expected &&>(),
+ std::declval<F &&>()))
+ map_error(F &&f) && {
+ return map_error_impl(std::move(*this), std::forward<F>(f));
+ }
+ template <class F>
+ constexpr decltype(map_error_impl(std::declval<const expected &>(),
+ std::declval<F &&>()))
+ map_error(F &&f) const & {
+ return map_error_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+ template <class F>
+ constexpr decltype(map_error_impl(std::declval<const expected &&>(),
+ std::declval<F &&>()))
+ map_error(F &&f) const && {
+ return map_error_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+#endif
+#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \
+ !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55)
+ template <class F> TL_EXPECTED_11_CONSTEXPR auto transform_error(F &&f) & {
+ return map_error_impl(*this, std::forward<F>(f));
+ }
+ template <class F> TL_EXPECTED_11_CONSTEXPR auto transform_error(F &&f) && {
+ return map_error_impl(std::move(*this), std::forward<F>(f));
+ }
+ template <class F> constexpr auto transform_error(F &&f) const & {
+ return map_error_impl(*this, std::forward<F>(f));
+ }
+ template <class F> constexpr auto transform_error(F &&f) const && {
+ return map_error_impl(std::move(*this), std::forward<F>(f));
+ }
+#else
+ template <class F>
+ TL_EXPECTED_11_CONSTEXPR decltype(map_error_impl(std::declval<expected &>(),
+ std::declval<F &&>()))
+ transform_error(F &&f) & {
+ return map_error_impl(*this, std::forward<F>(f));
+ }
+ template <class F>
+ TL_EXPECTED_11_CONSTEXPR decltype(map_error_impl(std::declval<expected &&>(),
+ std::declval<F &&>()))
+ transform_error(F &&f) && {
+ return map_error_impl(std::move(*this), std::forward<F>(f));
+ }
+ template <class F>
+ constexpr decltype(map_error_impl(std::declval<const expected &>(),
+ std::declval<F &&>()))
+ transform_error(F &&f) const & {
+ return map_error_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+ template <class F>
+ constexpr decltype(map_error_impl(std::declval<const expected &&>(),
+ std::declval<F &&>()))
+ transform_error(F &&f) const && {
+ return map_error_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+#endif
+ template <class F> expected TL_EXPECTED_11_CONSTEXPR or_else(F &&f) & {
+ return or_else_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F> expected TL_EXPECTED_11_CONSTEXPR or_else(F &&f) && {
+ return or_else_impl(std::move(*this), std::forward<F>(f));
+ }
+
+ template <class F> expected constexpr or_else(F &&f) const & {
+ return or_else_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_EXPECTED_NO_CONSTRR
+ template <class F> expected constexpr or_else(F &&f) const && {
+ return or_else_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+ constexpr expected() = default;
+ constexpr expected(const expected &rhs) = default;
+ constexpr expected(expected &&rhs) = default;
+ expected &operator=(const expected &rhs) = default;
+ expected &operator=(expected &&rhs) = default;
+
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<T, Args &&...>::value> * =
+ nullptr>
+ constexpr expected(in_place_t, Args &&...args)
+ : impl_base(in_place, std::forward<Args>(args)...),
+ ctor_base(detail::default_constructor_tag{}) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr expected(in_place_t, std::initializer_list<U> il, Args &&...args)
+ : impl_base(in_place, il, std::forward<Args>(args)...),
+ ctor_base(detail::default_constructor_tag{}) {}
+
+ template <class G = E,
+ detail::enable_if_t<std::is_constructible<E, const G &>::value> * =
+ nullptr,
+ detail::enable_if_t<!std::is_convertible<const G &, E>::value> * =
+ nullptr>
+ explicit constexpr expected(const unexpected<G> &e)
+ : impl_base(unexpect, e.value()),
+ ctor_base(detail::default_constructor_tag{}) {}
+
+ template <
+ class G = E,
+ detail::enable_if_t<std::is_constructible<E, const G &>::value> * =
+ nullptr,
+ detail::enable_if_t<std::is_convertible<const G &, E>::value> * = nullptr>
+ constexpr expected(unexpected<G> const &e)
+ : impl_base(unexpect, e.value()),
+ ctor_base(detail::default_constructor_tag{}) {}
+
+ template <
+ class G = E,
+ detail::enable_if_t<std::is_constructible<E, G &&>::value> * = nullptr,
+ detail::enable_if_t<!std::is_convertible<G &&, E>::value> * = nullptr>
+ explicit constexpr expected(unexpected<G> &&e) noexcept(
+ std::is_nothrow_constructible<E, G &&>::value)
+ : impl_base(unexpect, std::move(e.value())),
+ ctor_base(detail::default_constructor_tag{}) {}
+
+ template <
+ class G = E,
+ detail::enable_if_t<std::is_constructible<E, G &&>::value> * = nullptr,
+ detail::enable_if_t<std::is_convertible<G &&, E>::value> * = nullptr>
+ constexpr expected(unexpected<G> &&e) noexcept(
+ std::is_nothrow_constructible<E, G &&>::value)
+ : impl_base(unexpect, std::move(e.value())),
+ ctor_base(detail::default_constructor_tag{}) {}
+
+ template <class... Args,
+ detail::enable_if_t<std::is_constructible<E, Args &&...>::value> * =
+ nullptr>
+ constexpr explicit expected(unexpect_t, Args &&...args)
+ : impl_base(unexpect, std::forward<Args>(args)...),
+ ctor_base(detail::default_constructor_tag{}) {}
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_constructible<
+ E, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ constexpr explicit expected(unexpect_t, std::initializer_list<U> il,
+ Args &&...args)
+ : impl_base(unexpect, il, std::forward<Args>(args)...),
+ ctor_base(detail::default_constructor_tag{}) {}
+
+ template <class U, class G,
+ detail::enable_if_t<!(std::is_convertible<U const &, T>::value &&
+ std::is_convertible<G const &, E>::value)> * =
+ nullptr,
+ detail::expected_enable_from_other<T, E, U, G, const U &, const G &>
+ * = nullptr>
+ explicit TL_EXPECTED_11_CONSTEXPR expected(const expected<U, G> &rhs)
+ : ctor_base(detail::default_constructor_tag{}) {
+ if (rhs.has_value()) {
+ this->construct(*rhs);
+ } else {
+ this->construct_error(rhs.error());
+ }
+ }
+
+ template <class U, class G,
+ detail::enable_if_t<(std::is_convertible<U const &, T>::value &&
+ std::is_convertible<G const &, E>::value)> * =
+ nullptr,
+ detail::expected_enable_from_other<T, E, U, G, const U &, const G &>
+ * = nullptr>
+ TL_EXPECTED_11_CONSTEXPR expected(const expected<U, G> &rhs)
+ : ctor_base(detail::default_constructor_tag{}) {
+ if (rhs.has_value()) {
+ this->construct(*rhs);
+ } else {
+ this->construct_error(rhs.error());
+ }
+ }
+
+ template <
+ class U, class G,
+ detail::enable_if_t<!(std::is_convertible<U &&, T>::value &&
+ std::is_convertible<G &&, E>::value)> * = nullptr,
+ detail::expected_enable_from_other<T, E, U, G, U &&, G &&> * = nullptr>
+ explicit TL_EXPECTED_11_CONSTEXPR expected(expected<U, G> &&rhs)
+ : ctor_base(detail::default_constructor_tag{}) {
+ if (rhs.has_value()) {
+ this->construct(std::move(*rhs));
+ } else {
+ this->construct_error(std::move(rhs.error()));
+ }
+ }
+
+ template <
+ class U, class G,
+ detail::enable_if_t<(std::is_convertible<U &&, T>::value &&
+ std::is_convertible<G &&, E>::value)> * = nullptr,
+ detail::expected_enable_from_other<T, E, U, G, U &&, G &&> * = nullptr>
+ TL_EXPECTED_11_CONSTEXPR expected(expected<U, G> &&rhs)
+ : ctor_base(detail::default_constructor_tag{}) {
+ if (rhs.has_value()) {
+ this->construct(std::move(*rhs));
+ } else {
+ this->construct_error(std::move(rhs.error()));
+ }
+ }
+
+ template <
+ class U = T,
+ detail::enable_if_t<!std::is_convertible<U &&, T>::value> * = nullptr,
+ detail::expected_enable_forward_value<T, E, U> * = nullptr>
+ explicit TL_EXPECTED_MSVC2015_CONSTEXPR expected(U &&v)
+ : expected(in_place, std::forward<U>(v)) {}
+
+ template <
+ class U = T,
+ detail::enable_if_t<std::is_convertible<U &&, T>::value> * = nullptr,
+ detail::expected_enable_forward_value<T, E, U> * = nullptr>
+ TL_EXPECTED_MSVC2015_CONSTEXPR expected(U &&v)
+ : expected(in_place, std::forward<U>(v)) {}
+
+ template <
+ class U = T, class G = T,
+ detail::enable_if_t<std::is_nothrow_constructible<T, U &&>::value> * =
+ nullptr,
+ detail::enable_if_t<!std::is_void<G>::value> * = nullptr,
+ detail::enable_if_t<
+ (!std::is_same<expected<T, E>, detail::decay_t<U>>::value &&
+ !detail::conjunction<std::is_scalar<T>,
+ std::is_same<T, detail::decay_t<U>>>::value &&
+ std::is_constructible<T, U>::value &&
+ std::is_assignable<G &, U>::value &&
+ std::is_nothrow_move_constructible<E>::value)> * = nullptr>
+ expected &operator=(U &&v) {
+ if (has_value()) {
+ val() = std::forward<U>(v);
+ } else {
+ err().~unexpected<E>();
+ ::new (valptr()) T(std::forward<U>(v));
+ this->m_has_val = true;
+ }
+
+ return *this;
+ }
+
+ template <
+ class U = T, class G = T,
+ detail::enable_if_t<!std::is_nothrow_constructible<T, U &&>::value> * =
+ nullptr,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr,
+ detail::enable_if_t<
+ (!std::is_same<expected<T, E>, detail::decay_t<U>>::value &&
+ !detail::conjunction<std::is_scalar<T>,
+ std::is_same<T, detail::decay_t<U>>>::value &&
+ std::is_constructible<T, U>::value &&
+ std::is_assignable<G &, U>::value &&
+ std::is_nothrow_move_constructible<E>::value)> * = nullptr>
+ expected &operator=(U &&v) {
+ if (has_value()) {
+ val() = std::forward<U>(v);
+ } else {
+ auto tmp = std::move(err());
+ err().~unexpected<E>();
+
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+ try {
+ ::new (valptr()) T(std::forward<U>(v));
+ this->m_has_val = true;
+ } catch (...) {
+ err() = std::move(tmp);
+ throw;
+ }
+#else
+ ::new (valptr()) T(std::forward<U>(v));
+ this->m_has_val = true;
+#endif
+ }
+
+ return *this;
+ }
+
+ template <class G = E,
+ detail::enable_if_t<std::is_nothrow_copy_constructible<G>::value &&
+ std::is_assignable<G &, G>::value> * = nullptr>
+ expected &operator=(const unexpected<G> &rhs) {
+ if (!has_value()) {
+ err() = rhs;
+ } else {
+ this->destroy_val();
+ ::new (errptr()) unexpected<E>(rhs);
+ this->m_has_val = false;
+ }
+
+ return *this;
+ }
+
+ template <class G = E,
+ detail::enable_if_t<std::is_nothrow_move_constructible<G>::value &&
+ std::is_move_assignable<G>::value> * = nullptr>
+ expected &operator=(unexpected<G> &&rhs) noexcept {
+ if (!has_value()) {
+ err() = std::move(rhs);
+ } else {
+ this->destroy_val();
+ ::new (errptr()) unexpected<E>(std::move(rhs));
+ this->m_has_val = false;
+ }
+
+ return *this;
+ }
+
+ template <class... Args, detail::enable_if_t<std::is_nothrow_constructible<
+ T, Args &&...>::value> * = nullptr>
+ void emplace(Args &&...args) {
+ if (has_value()) {
+ val().~T();
+ } else {
+ err().~unexpected<E>();
+ this->m_has_val = true;
+ }
+ ::new (valptr()) T(std::forward<Args>(args)...);
+ }
+
+ template <class... Args, detail::enable_if_t<!std::is_nothrow_constructible<
+ T, Args &&...>::value> * = nullptr>
+ void emplace(Args &&...args) {
+ if (has_value()) {
+ val().~T();
+ ::new (valptr()) T(std::forward<Args>(args)...);
+ } else {
+ auto tmp = std::move(err());
+ err().~unexpected<E>();
+
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+ try {
+ ::new (valptr()) T(std::forward<Args>(args)...);
+ this->m_has_val = true;
+ } catch (...) {
+ err() = std::move(tmp);
+ throw;
+ }
+#else
+ ::new (valptr()) T(std::forward<Args>(args)...);
+ this->m_has_val = true;
+#endif
+ }
+ }
+
+ template <class U, class... Args,
+ detail::enable_if_t<std::is_nothrow_constructible<
+ T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ void emplace(std::initializer_list<U> il, Args &&...args) {
+ if (has_value()) {
+ T t(il, std::forward<Args>(args)...);
+ val() = std::move(t);
+ } else {
+ err().~unexpected<E>();
+ ::new (valptr()) T(il, std::forward<Args>(args)...);
+ this->m_has_val = true;
+ }
+ }
+
+ template <class U, class... Args,
+ detail::enable_if_t<!std::is_nothrow_constructible<
+ T, std::initializer_list<U> &, Args &&...>::value> * = nullptr>
+ void emplace(std::initializer_list<U> il, Args &&...args) {
+ if (has_value()) {
+ T t(il, std::forward<Args>(args)...);
+ val() = std::move(t);
+ } else {
+ auto tmp = std::move(err());
+ err().~unexpected<E>();
+
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+ try {
+ ::new (valptr()) T(il, std::forward<Args>(args)...);
+ this->m_has_val = true;
+ } catch (...) {
+ err() = std::move(tmp);
+ throw;
+ }
+#else
+ ::new (valptr()) T(il, std::forward<Args>(args)...);
+ this->m_has_val = true;
+#endif
+ }
+ }
+
+private:
+ using t_is_void = std::true_type;
+ using t_is_not_void = std::false_type;
+ using t_is_nothrow_move_constructible = std::true_type;
+ using move_constructing_t_can_throw = std::false_type;
+ using e_is_nothrow_move_constructible = std::true_type;
+ using move_constructing_e_can_throw = std::false_type;
+
+ void swap_where_both_have_value(expected & /*rhs*/, t_is_void) noexcept {
+ // swapping void is a no-op
+ }
+
+ void swap_where_both_have_value(expected &rhs, t_is_not_void) {
+ using std::swap;
+ swap(val(), rhs.val());
+ }
+
+ void swap_where_only_one_has_value(expected &rhs, t_is_void) noexcept(
+ std::is_nothrow_move_constructible<E>::value) {
+ ::new (errptr()) unexpected_type(std::move(rhs.err()));
+ rhs.err().~unexpected_type();
+ std::swap(this->m_has_val, rhs.m_has_val);
+ }
+
+ void swap_where_only_one_has_value(expected &rhs, t_is_not_void) {
+ swap_where_only_one_has_value_and_t_is_not_void(
+ rhs, typename std::is_nothrow_move_constructible<T>::type{},
+ typename std::is_nothrow_move_constructible<E>::type{});
+ }
+
+ void swap_where_only_one_has_value_and_t_is_not_void(
+ expected &rhs, t_is_nothrow_move_constructible,
+ e_is_nothrow_move_constructible) noexcept {
+ auto temp = std::move(val());
+ val().~T();
+ ::new (errptr()) unexpected_type(std::move(rhs.err()));
+ rhs.err().~unexpected_type();
+ ::new (rhs.valptr()) T(std::move(temp));
+ std::swap(this->m_has_val, rhs.m_has_val);
+ }
+
+ void swap_where_only_one_has_value_and_t_is_not_void(
+ expected &rhs, t_is_nothrow_move_constructible,
+ move_constructing_e_can_throw) {
+ auto temp = std::move(val());
+ val().~T();
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+ try {
+ ::new (errptr()) unexpected_type(std::move(rhs.err()));
+ rhs.err().~unexpected_type();
+ ::new (rhs.valptr()) T(std::move(temp));
+ std::swap(this->m_has_val, rhs.m_has_val);
+ } catch (...) {
+ val() = std::move(temp);
+ throw;
+ }
+#else
+ ::new (errptr()) unexpected_type(std::move(rhs.err()));
+ rhs.err().~unexpected_type();
+ ::new (rhs.valptr()) T(std::move(temp));
+ std::swap(this->m_has_val, rhs.m_has_val);
+#endif
+ }
+
+ void swap_where_only_one_has_value_and_t_is_not_void(
+ expected &rhs, move_constructing_t_can_throw,
+ e_is_nothrow_move_constructible) {
+ auto temp = std::move(rhs.err());
+ rhs.err().~unexpected_type();
+#ifdef TL_EXPECTED_EXCEPTIONS_ENABLED
+ try {
+ ::new (rhs.valptr()) T(std::move(val()));
+ val().~T();
+ ::new (errptr()) unexpected_type(std::move(temp));
+ std::swap(this->m_has_val, rhs.m_has_val);
+ } catch (...) {
+ rhs.err() = std::move(temp);
+ throw;
+ }
+#else
+ ::new (rhs.valptr()) T(std::move(val()));
+ val().~T();
+ ::new (errptr()) unexpected_type(std::move(temp));
+ std::swap(this->m_has_val, rhs.m_has_val);
+#endif
+ }
+
+public:
+ template <class OT = T, class OE = E>
+ detail::enable_if_t<detail::is_swappable<OT>::value &&
+ detail::is_swappable<OE>::value &&
+ (std::is_nothrow_move_constructible<OT>::value ||
+ std::is_nothrow_move_constructible<OE>::value)>
+ swap(expected &rhs) noexcept(
+ std::is_nothrow_move_constructible<T>::value
+ &&detail::is_nothrow_swappable<T>::value
+ &&std::is_nothrow_move_constructible<E>::value
+ &&detail::is_nothrow_swappable<E>::value) {
+ if (has_value() && rhs.has_value()) {
+ swap_where_both_have_value(rhs, typename std::is_void<T>::type{});
+ } else if (!has_value() && rhs.has_value()) {
+ rhs.swap(*this);
+ } else if (has_value()) {
+ swap_where_only_one_has_value(rhs, typename std::is_void<T>::type{});
+ } else {
+ using std::swap;
+ swap(err(), rhs.err());
+ }
+ }
+
+ constexpr const T *operator->() const {
+ TL_ASSERT(has_value());
+ return valptr();
+ }
+ TL_EXPECTED_11_CONSTEXPR T *operator->() {
+ TL_ASSERT(has_value());
+ return valptr();
+ }
+
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ constexpr const U &operator*() const & {
+ TL_ASSERT(has_value());
+ return val();
+ }
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ TL_EXPECTED_11_CONSTEXPR U &operator*() & {
+ TL_ASSERT(has_value());
+ return val();
+ }
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ constexpr const U &&operator*() const && {
+ TL_ASSERT(has_value());
+ return std::move(val());
+ }
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ TL_EXPECTED_11_CONSTEXPR U &&operator*() && {
+ TL_ASSERT(has_value());
+ return std::move(val());
+ }
+
+ constexpr bool has_value() const noexcept { return this->m_has_val; }
+ constexpr explicit operator bool() const noexcept { return this->m_has_val; }
+
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ TL_EXPECTED_11_CONSTEXPR const U &value() const & {
+ if (!has_value())
+ detail::throw_exception(bad_expected_access<E>(err().value()));
+ return val();
+ }
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ TL_EXPECTED_11_CONSTEXPR U &value() & {
+ if (!has_value())
+ detail::throw_exception(bad_expected_access<E>(err().value()));
+ return val();
+ }
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ TL_EXPECTED_11_CONSTEXPR const U &&value() const && {
+ if (!has_value())
+ detail::throw_exception(bad_expected_access<E>(std::move(err()).value()));
+ return std::move(val());
+ }
+ template <class U = T,
+ detail::enable_if_t<!std::is_void<U>::value> * = nullptr>
+ TL_EXPECTED_11_CONSTEXPR U &&value() && {
+ if (!has_value())
+ detail::throw_exception(bad_expected_access<E>(std::move(err()).value()));
+ return std::move(val());
+ }
+
+ constexpr const E &error() const & {
+ TL_ASSERT(!has_value());
+ return err().value();
+ }
+ TL_EXPECTED_11_CONSTEXPR E &error() & {
+ TL_ASSERT(!has_value());
+ return err().value();
+ }
+ constexpr const E &&error() const && {
+ TL_ASSERT(!has_value());
+ return std::move(err().value());
+ }
+ TL_EXPECTED_11_CONSTEXPR E &&error() && {
+ TL_ASSERT(!has_value());
+ return std::move(err().value());
+ }
+
+ template <class U> constexpr T value_or(U &&v) const & {
+ static_assert(std::is_copy_constructible<T>::value &&
+ std::is_convertible<U &&, T>::value,
+ "T must be copy-constructible and convertible to from U&&");
+ return bool(*this) ? **this : static_cast<T>(std::forward<U>(v));
+ }
+ template <class U> TL_EXPECTED_11_CONSTEXPR T value_or(U &&v) && {
+ static_assert(std::is_move_constructible<T>::value &&
+ std::is_convertible<U &&, T>::value,
+ "T must be move-constructible and convertible to from U&&");
+ return bool(*this) ? std::move(**this) : static_cast<T>(std::forward<U>(v));
+ }
+};
+
+namespace detail {
+template <class Exp> using exp_t = typename detail::decay_t<Exp>::value_type;
+template <class Exp> using err_t = typename detail::decay_t<Exp>::error_type;
+template <class Exp, class Ret> using ret_t = expected<Ret, err_t<Exp>>;
+
+#ifdef TL_EXPECTED_CXX14
+template <class Exp, class F,
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Exp>()))>
+constexpr auto and_then_impl(Exp &&exp, F &&f) {
+ static_assert(detail::is_expected<Ret>::value, "F must return an expected");
+
+ return exp.has_value()
+ ? detail::invoke(std::forward<F>(f), *std::forward<Exp>(exp))
+ : Ret(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>()))>
+constexpr auto and_then_impl(Exp &&exp, F &&f) {
+ static_assert(detail::is_expected<Ret>::value, "F must return an expected");
+
+ return exp.has_value() ? detail::invoke(std::forward<F>(f))
+ : Ret(unexpect, std::forward<Exp>(exp).error());
+}
+#else
+template <class> struct TC;
+template <class Exp, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Exp>())),
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr>
+auto and_then_impl(Exp &&exp, F &&f) -> Ret {
+ static_assert(detail::is_expected<Ret>::value, "F must return an expected");
+
+ return exp.has_value()
+ ? detail::invoke(std::forward<F>(f), *std::forward<Exp>(exp))
+ : Ret(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>())),
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr>
+constexpr auto and_then_impl(Exp &&exp, F &&f) -> Ret {
+ static_assert(detail::is_expected<Ret>::value, "F must return an expected");
+
+ return exp.has_value() ? detail::invoke(std::forward<F>(f))
+ : Ret(unexpect, std::forward<Exp>(exp).error());
+}
+#endif
+
+#ifdef TL_EXPECTED_CXX14
+template <class Exp, class F,
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Exp>())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto expected_map_impl(Exp &&exp, F &&f) {
+ using result = ret_t<Exp, detail::decay_t<Ret>>;
+ return exp.has_value() ? result(detail::invoke(std::forward<F>(f),
+ *std::forward<Exp>(exp)))
+ : result(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Exp>())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto expected_map_impl(Exp &&exp, F &&f) {
+ using result = expected<void, err_t<Exp>>;
+ if (exp.has_value()) {
+ detail::invoke(std::forward<F>(f), *std::forward<Exp>(exp));
+ return result();
+ }
+
+ return result(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto expected_map_impl(Exp &&exp, F &&f) {
+ using result = ret_t<Exp, detail::decay_t<Ret>>;
+ return exp.has_value() ? result(detail::invoke(std::forward<F>(f)))
+ : result(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto expected_map_impl(Exp &&exp, F &&f) {
+ using result = expected<void, err_t<Exp>>;
+ if (exp.has_value()) {
+ detail::invoke(std::forward<F>(f));
+ return result();
+ }
+
+ return result(unexpect, std::forward<Exp>(exp).error());
+}
+#else
+template <class Exp, class F,
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Exp>())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+
+constexpr auto expected_map_impl(Exp &&exp, F &&f)
+ -> ret_t<Exp, detail::decay_t<Ret>> {
+ using result = ret_t<Exp, detail::decay_t<Ret>>;
+
+ return exp.has_value() ? result(detail::invoke(std::forward<F>(f),
+ *std::forward<Exp>(exp)))
+ : result(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Exp>())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+
+auto expected_map_impl(Exp &&exp, F &&f) -> expected<void, err_t<Exp>> {
+ if (exp.has_value()) {
+ detail::invoke(std::forward<F>(f), *std::forward<Exp>(exp));
+ return {};
+ }
+
+ return unexpected<err_t<Exp>>(std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+
+constexpr auto expected_map_impl(Exp &&exp, F &&f)
+ -> ret_t<Exp, detail::decay_t<Ret>> {
+ using result = ret_t<Exp, detail::decay_t<Ret>>;
+
+ return exp.has_value() ? result(detail::invoke(std::forward<F>(f)))
+ : result(unexpect, std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+
+auto expected_map_impl(Exp &&exp, F &&f) -> expected<void, err_t<Exp>> {
+ if (exp.has_value()) {
+ detail::invoke(std::forward<F>(f));
+ return {};
+ }
+
+ return unexpected<err_t<Exp>>(std::forward<Exp>(exp).error());
+}
+#endif
+
+#if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \
+ !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55)
+template <class Exp, class F,
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto map_error_impl(Exp &&exp, F &&f) {
+ using result = expected<exp_t<Exp>, detail::decay_t<Ret>>;
+ return exp.has_value()
+ ? result(*std::forward<Exp>(exp))
+ : result(unexpect, detail::invoke(std::forward<F>(f),
+ std::forward<Exp>(exp).error()));
+}
+template <class Exp, class F,
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto map_error_impl(Exp &&exp, F &&f) {
+ using result = expected<exp_t<Exp>, monostate>;
+ if (exp.has_value()) {
+ return result(*std::forward<Exp>(exp));
+ }
+
+ detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error());
+ return result(unexpect, monostate{});
+}
+template <class Exp, class F,
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto map_error_impl(Exp &&exp, F &&f) {
+ using result = expected<exp_t<Exp>, detail::decay_t<Ret>>;
+ return exp.has_value()
+ ? result()
+ : result(unexpect, detail::invoke(std::forward<F>(f),
+ std::forward<Exp>(exp).error()));
+}
+template <class Exp, class F,
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto map_error_impl(Exp &&exp, F &&f) {
+ using result = expected<exp_t<Exp>, monostate>;
+ if (exp.has_value()) {
+ return result();
+ }
+
+ detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error());
+ return result(unexpect, monostate{});
+}
+#else
+template <class Exp, class F,
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto map_error_impl(Exp &&exp, F &&f)
+ -> expected<exp_t<Exp>, detail::decay_t<Ret>> {
+ using result = expected<exp_t<Exp>, detail::decay_t<Ret>>;
+
+ return exp.has_value()
+ ? result(*std::forward<Exp>(exp))
+ : result(unexpect, detail::invoke(std::forward<F>(f),
+ std::forward<Exp>(exp).error()));
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<!std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto map_error_impl(Exp &&exp, F &&f) -> expected<exp_t<Exp>, monostate> {
+ using result = expected<exp_t<Exp>, monostate>;
+ if (exp.has_value()) {
+ return result(*std::forward<Exp>(exp));
+ }
+
+ detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error());
+ return result(unexpect, monostate{});
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto map_error_impl(Exp &&exp, F &&f)
+ -> expected<exp_t<Exp>, detail::decay_t<Ret>> {
+ using result = expected<exp_t<Exp>, detail::decay_t<Ret>>;
+
+ return exp.has_value()
+ ? result()
+ : result(unexpect, detail::invoke(std::forward<F>(f),
+ std::forward<Exp>(exp).error()));
+}
+
+template <class Exp, class F,
+ detail::enable_if_t<std::is_void<exp_t<Exp>>::value> * = nullptr,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto map_error_impl(Exp &&exp, F &&f) -> expected<exp_t<Exp>, monostate> {
+ using result = expected<exp_t<Exp>, monostate>;
+ if (exp.has_value()) {
+ return result();
+ }
+
+ detail::invoke(std::forward<F>(f), std::forward<Exp>(exp).error());
+ return result(unexpect, monostate{});
+}
+#endif
+
+#ifdef TL_EXPECTED_CXX14
+template <class Exp, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto or_else_impl(Exp &&exp, F &&f) {
+ static_assert(detail::is_expected<Ret>::value, "F must return an expected");
+ return exp.has_value() ? std::forward<Exp>(exp)
+ : detail::invoke(std::forward<F>(f),
+ std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+detail::decay_t<Exp> or_else_impl(Exp &&exp, F &&f) {
+ return exp.has_value() ? std::forward<Exp>(exp)
+ : (detail::invoke(std::forward<F>(f),
+ std::forward<Exp>(exp).error()),
+ std::forward<Exp>(exp));
+}
+#else
+template <class Exp, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+auto or_else_impl(Exp &&exp, F &&f) -> Ret {
+ static_assert(detail::is_expected<Ret>::value, "F must return an expected");
+ return exp.has_value() ? std::forward<Exp>(exp)
+ : detail::invoke(std::forward<F>(f),
+ std::forward<Exp>(exp).error());
+}
+
+template <class Exp, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ std::declval<Exp>().error())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+detail::decay_t<Exp> or_else_impl(Exp &&exp, F &&f) {
+ return exp.has_value() ? std::forward<Exp>(exp)
+ : (detail::invoke(std::forward<F>(f),
+ std::forward<Exp>(exp).error()),
+ std::forward<Exp>(exp));
+}
+#endif
+} // namespace detail
+
+template <class T, class E, class U, class F>
+constexpr bool operator==(const expected<T, E> &lhs,
+ const expected<U, F> &rhs) {
+ return (lhs.has_value() != rhs.has_value())
+ ? false
+ : (!lhs.has_value() ? lhs.error() == rhs.error() : *lhs == *rhs);
+}
+template <class T, class E, class U, class F>
+constexpr bool operator!=(const expected<T, E> &lhs,
+ const expected<U, F> &rhs) {
+ return (lhs.has_value() != rhs.has_value())
+ ? true
+ : (!lhs.has_value() ? lhs.error() != rhs.error() : *lhs != *rhs);
+}
+template <class E, class F>
+constexpr bool operator==(const expected<void, E> &lhs,
+ const expected<void, F> &rhs) {
+ return (lhs.has_value() != rhs.has_value())
+ ? false
+ : (!lhs.has_value() ? lhs.error() == rhs.error() : true);
+}
+template <class E, class F>
+constexpr bool operator!=(const expected<void, E> &lhs,
+ const expected<void, F> &rhs) {
+ return (lhs.has_value() != rhs.has_value())
+ ? true
+ : (!lhs.has_value() ? lhs.error() == rhs.error() : false);
+}
+
+template <class T, class E, class U>
+constexpr bool operator==(const expected<T, E> &x, const U &v) {
+ return x.has_value() ? *x == v : false;
+}
+template <class T, class E, class U>
+constexpr bool operator==(const U &v, const expected<T, E> &x) {
+ return x.has_value() ? *x == v : false;
+}
+template <class T, class E, class U>
+constexpr bool operator!=(const expected<T, E> &x, const U &v) {
+ return x.has_value() ? *x != v : true;
+}
+template <class T, class E, class U>
+constexpr bool operator!=(const U &v, const expected<T, E> &x) {
+ return x.has_value() ? *x != v : true;
+}
+
+template <class T, class E>
+constexpr bool operator==(const expected<T, E> &x, const unexpected<E> &e) {
+ return x.has_value() ? false : x.error() == e.value();
+}
+template <class T, class E>
+constexpr bool operator==(const unexpected<E> &e, const expected<T, E> &x) {
+ return x.has_value() ? false : x.error() == e.value();
+}
+template <class T, class E>
+constexpr bool operator!=(const expected<T, E> &x, const unexpected<E> &e) {
+ return x.has_value() ? true : x.error() != e.value();
+}
+template <class T, class E>
+constexpr bool operator!=(const unexpected<E> &e, const expected<T, E> &x) {
+ return x.has_value() ? true : x.error() != e.value();
+}
+
+template <class T, class E,
+ detail::enable_if_t<(std::is_void<T>::value ||
+ std::is_move_constructible<T>::value) &&
+ detail::is_swappable<T>::value &&
+ std::is_move_constructible<E>::value &&
+ detail::is_swappable<E>::value> * = nullptr>
+void swap(expected<T, E> &lhs,
+ expected<T, E> &rhs) noexcept(noexcept(lhs.swap(rhs))) {
+ lhs.swap(rhs);
+}
+} // namespace tl
+
+#endif
--- /dev/null
+
+///
+// optional - An implementation of std::optional with extensions
+// Written in 2017 by Sy Brand (tartanllama@gmail.com, @TartanLlama)
+//
+// Documentation available at https://tl.tartanllama.xyz/
+//
+// To the extent possible under law, the author(s) have dedicated all
+// copyright and related and neighboring rights to this software to the
+// public domain worldwide. This software is distributed without any warranty.
+//
+// You should have received a copy of the CC0 Public Domain Dedication
+// along with this software. If not, see
+// <http://creativecommons.org/publicdomain/zero/1.0/>.
+///
+
+#ifndef TL_OPTIONAL_HPP
+#define TL_OPTIONAL_HPP
+
+#define TL_OPTIONAL_VERSION_MAJOR 1
+#define TL_OPTIONAL_VERSION_MINOR 1
+#define TL_OPTIONAL_VERSION_PATCH 0
+
+#include <exception>
+#include <functional>
+#include <new>
+#include <type_traits>
+#include <utility>
+
+#if (defined(_MSC_VER) && _MSC_VER == 1900)
+#define TL_OPTIONAL_MSVC2015
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 && \
+ !defined(__clang__))
+#define TL_OPTIONAL_GCC49
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 4 && \
+ !defined(__clang__))
+#define TL_OPTIONAL_GCC54
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 5 && \
+ !defined(__clang__))
+#define TL_OPTIONAL_GCC55
+#endif
+
+#if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 && \
+ !defined(__clang__))
+// GCC < 5 doesn't support overloading on const&& for member functions
+#define TL_OPTIONAL_NO_CONSTRR
+
+// GCC < 5 doesn't support some standard C++11 type traits
+#define TL_OPTIONAL_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \
+ std::has_trivial_copy_constructor<T>::value
+#define TL_OPTIONAL_IS_TRIVIALLY_COPY_ASSIGNABLE(T) std::has_trivial_copy_assign<T>::value
+
+// This one will be different for GCC 5.7 if it's ever supported
+#define TL_OPTIONAL_IS_TRIVIALLY_DESTRUCTIBLE(T) std::is_trivially_destructible<T>::value
+
+// GCC 5 < v < 8 has a bug in is_trivially_copy_constructible which breaks std::vector
+// for non-copyable types
+#elif (defined(__GNUC__) && __GNUC__ < 8 && \
+ !defined(__clang__))
+#ifndef TL_GCC_LESS_8_TRIVIALLY_COPY_CONSTRUCTIBLE_MUTEX
+#define TL_GCC_LESS_8_TRIVIALLY_COPY_CONSTRUCTIBLE_MUTEX
+namespace tl {
+ namespace detail {
+ template<class T>
+ struct is_trivially_copy_constructible : std::is_trivially_copy_constructible<T>{};
+#ifdef _GLIBCXX_VECTOR
+ template<class T, class A>
+ struct is_trivially_copy_constructible<std::vector<T,A>>
+ : std::is_trivially_copy_constructible<T>{};
+#endif
+ }
+}
+#endif
+
+#define TL_OPTIONAL_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \
+ tl::detail::is_trivially_copy_constructible<T>::value
+#define TL_OPTIONAL_IS_TRIVIALLY_COPY_ASSIGNABLE(T) \
+ std::is_trivially_copy_assignable<T>::value
+#define TL_OPTIONAL_IS_TRIVIALLY_DESTRUCTIBLE(T) std::is_trivially_destructible<T>::value
+#else
+#define TL_OPTIONAL_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \
+ std::is_trivially_copy_constructible<T>::value
+#define TL_OPTIONAL_IS_TRIVIALLY_COPY_ASSIGNABLE(T) \
+ std::is_trivially_copy_assignable<T>::value
+#define TL_OPTIONAL_IS_TRIVIALLY_DESTRUCTIBLE(T) std::is_trivially_destructible<T>::value
+#endif
+
+#if __cplusplus > 201103L
+#define TL_OPTIONAL_CXX14
+#endif
+
+// constexpr implies const in C++11, not C++14
+#if (__cplusplus == 201103L || defined(TL_OPTIONAL_MSVC2015) || \
+ defined(TL_OPTIONAL_GCC49))
+#define TL_OPTIONAL_11_CONSTEXPR
+#else
+#define TL_OPTIONAL_11_CONSTEXPR constexpr
+#endif
+
+namespace tl {
+#ifndef TL_MONOSTATE_INPLACE_MUTEX
+#define TL_MONOSTATE_INPLACE_MUTEX
+/// Used to represent an optional with no data; essentially a bool
+class monostate {};
+
+/// A tag type to tell optional to construct its value in-place
+struct in_place_t {
+ explicit in_place_t() = default;
+};
+/// A tag to tell optional to construct its value in-place
+static constexpr in_place_t in_place{};
+#endif
+
+template <class T> class optional;
+
+namespace detail {
+#ifndef TL_TRAITS_MUTEX
+#define TL_TRAITS_MUTEX
+// C++14-style aliases for brevity
+template <class T> using remove_const_t = typename std::remove_const<T>::type;
+template <class T>
+using remove_reference_t = typename std::remove_reference<T>::type;
+template <class T> using decay_t = typename std::decay<T>::type;
+template <bool E, class T = void>
+using enable_if_t = typename std::enable_if<E, T>::type;
+template <bool B, class T, class F>
+using conditional_t = typename std::conditional<B, T, F>::type;
+
+// std::conjunction from C++17
+template <class...> struct conjunction : std::true_type {};
+template <class B> struct conjunction<B> : B {};
+template <class B, class... Bs>
+struct conjunction<B, Bs...>
+ : std::conditional<bool(B::value), conjunction<Bs...>, B>::type {};
+
+#if defined(_LIBCPP_VERSION) && __cplusplus == 201103L
+#define TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND
+#endif
+
+// In C++11 mode, there's an issue in libc++'s std::mem_fn
+// which results in a hard-error when using it in a noexcept expression
+// in some cases. This is a check to workaround the common failing case.
+#ifdef TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND
+template <class T> struct is_pointer_to_non_const_member_func : std::false_type{};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*) (Args...)> : std::true_type{};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*) (Args...)&> : std::true_type{};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*) (Args...)&&> : std::true_type{};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*) (Args...) volatile> : std::true_type{};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*) (Args...) volatile&> : std::true_type{};
+template <class T, class Ret, class... Args>
+struct is_pointer_to_non_const_member_func<Ret (T::*) (Args...) volatile&&> : std::true_type{};
+
+template <class T> struct is_const_or_const_ref : std::false_type{};
+template <class T> struct is_const_or_const_ref<T const&> : std::true_type{};
+template <class T> struct is_const_or_const_ref<T const> : std::true_type{};
+#endif
+
+// std::invoke from C++17
+// https://stackoverflow.com/questions/38288042/c11-14-invoke-workaround
+template <typename Fn, typename... Args,
+#ifdef TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND
+ typename = enable_if_t<!(is_pointer_to_non_const_member_func<Fn>::value
+ && is_const_or_const_ref<Args...>::value)>,
+#endif
+ typename = enable_if_t<std::is_member_pointer<decay_t<Fn>>::value>,
+ int = 0>
+constexpr auto invoke(Fn &&f, Args &&... args) noexcept(
+ noexcept(std::mem_fn(f)(std::forward<Args>(args)...)))
+ -> decltype(std::mem_fn(f)(std::forward<Args>(args)...)) {
+ return std::mem_fn(f)(std::forward<Args>(args)...);
+}
+
+template <typename Fn, typename... Args,
+ typename = enable_if_t<!std::is_member_pointer<decay_t<Fn>>::value>>
+constexpr auto invoke(Fn &&f, Args &&... args) noexcept(
+ noexcept(std::forward<Fn>(f)(std::forward<Args>(args)...)))
+ -> decltype(std::forward<Fn>(f)(std::forward<Args>(args)...)) {
+ return std::forward<Fn>(f)(std::forward<Args>(args)...);
+}
+
+// std::invoke_result from C++17
+template <class F, class, class... Us> struct invoke_result_impl;
+
+template <class F, class... Us>
+struct invoke_result_impl<
+ F, decltype(detail::invoke(std::declval<F>(), std::declval<Us>()...), void()),
+ Us...> {
+ using type = decltype(detail::invoke(std::declval<F>(), std::declval<Us>()...));
+};
+
+template <class F, class... Us>
+using invoke_result = invoke_result_impl<F, void, Us...>;
+
+template <class F, class... Us>
+using invoke_result_t = typename invoke_result<F, Us...>::type;
+
+#if defined(_MSC_VER) && _MSC_VER <= 1900
+// TODO make a version which works with MSVC 2015
+template <class T, class U = T> struct is_swappable : std::true_type {};
+
+template <class T, class U = T> struct is_nothrow_swappable : std::true_type {};
+#else
+// https://stackoverflow.com/questions/26744589/what-is-a-proper-way-to-implement-is-swappable-to-test-for-the-swappable-concept
+namespace swap_adl_tests {
+// if swap ADL finds this then it would call std::swap otherwise (same
+// signature)
+struct tag {};
+
+template <class T> tag swap(T &, T &);
+template <class T, std::size_t N> tag swap(T (&a)[N], T (&b)[N]);
+
+// helper functions to test if an unqualified swap is possible, and if it
+// becomes std::swap
+template <class, class> std::false_type can_swap(...) noexcept(false);
+template <class T, class U,
+ class = decltype(swap(std::declval<T &>(), std::declval<U &>()))>
+std::true_type can_swap(int) noexcept(noexcept(swap(std::declval<T &>(),
+ std::declval<U &>())));
+
+template <class, class> std::false_type uses_std(...);
+template <class T, class U>
+std::is_same<decltype(swap(std::declval<T &>(), std::declval<U &>())), tag>
+uses_std(int);
+
+template <class T>
+struct is_std_swap_noexcept
+ : std::integral_constant<bool,
+ std::is_nothrow_move_constructible<T>::value &&
+ std::is_nothrow_move_assignable<T>::value> {};
+
+template <class T, std::size_t N>
+struct is_std_swap_noexcept<T[N]> : is_std_swap_noexcept<T> {};
+
+template <class T, class U>
+struct is_adl_swap_noexcept
+ : std::integral_constant<bool, noexcept(can_swap<T, U>(0))> {};
+} // namespace swap_adl_tests
+
+template <class T, class U = T>
+struct is_swappable
+ : std::integral_constant<
+ bool,
+ decltype(detail::swap_adl_tests::can_swap<T, U>(0))::value &&
+ (!decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value ||
+ (std::is_move_assignable<T>::value &&
+ std::is_move_constructible<T>::value))> {};
+
+template <class T, std::size_t N>
+struct is_swappable<T[N], T[N]>
+ : std::integral_constant<
+ bool,
+ decltype(detail::swap_adl_tests::can_swap<T[N], T[N]>(0))::value &&
+ (!decltype(
+ detail::swap_adl_tests::uses_std<T[N], T[N]>(0))::value ||
+ is_swappable<T, T>::value)> {};
+
+template <class T, class U = T>
+struct is_nothrow_swappable
+ : std::integral_constant<
+ bool,
+ is_swappable<T, U>::value &&
+ ((decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value
+ &&detail::swap_adl_tests::is_std_swap_noexcept<T>::value) ||
+ (!decltype(detail::swap_adl_tests::uses_std<T, U>(0))::value &&
+ detail::swap_adl_tests::is_adl_swap_noexcept<T,
+ U>::value))> {
+};
+#endif
+#endif
+
+// std::void_t from C++17
+template <class...> struct voider { using type = void; };
+template <class... Ts> using void_t = typename voider<Ts...>::type;
+
+// Trait for checking if a type is a tl::optional
+template <class T> struct is_optional_impl : std::false_type {};
+template <class T> struct is_optional_impl<optional<T>> : std::true_type {};
+template <class T> using is_optional = is_optional_impl<decay_t<T>>;
+
+// Change void to tl::monostate
+template <class U>
+using fixup_void = conditional_t<std::is_void<U>::value, monostate, U>;
+
+template <class F, class U, class = invoke_result_t<F, U>>
+using get_map_return = optional<fixup_void<invoke_result_t<F, U>>>;
+
+// Check if invoking F for some Us returns void
+template <class F, class = void, class... U> struct returns_void_impl;
+template <class F, class... U>
+struct returns_void_impl<F, void_t<invoke_result_t<F, U...>>, U...>
+ : std::is_void<invoke_result_t<F, U...>> {};
+template <class F, class... U>
+using returns_void = returns_void_impl<F, void, U...>;
+
+template <class T, class... U>
+using enable_if_ret_void = enable_if_t<returns_void<T &&, U...>::value>;
+
+template <class T, class... U>
+using disable_if_ret_void = enable_if_t<!returns_void<T &&, U...>::value>;
+
+template <class T, class U>
+using enable_forward_value =
+ detail::enable_if_t<std::is_constructible<T, U &&>::value &&
+ !std::is_same<detail::decay_t<U>, in_place_t>::value &&
+ !std::is_same<optional<T>, detail::decay_t<U>>::value>;
+
+template <class T, class U, class Other>
+using enable_from_other = detail::enable_if_t<
+ std::is_constructible<T, Other>::value &&
+ !std::is_constructible<T, optional<U> &>::value &&
+ !std::is_constructible<T, optional<U> &&>::value &&
+ !std::is_constructible<T, const optional<U> &>::value &&
+ !std::is_constructible<T, const optional<U> &&>::value &&
+ !std::is_convertible<optional<U> &, T>::value &&
+ !std::is_convertible<optional<U> &&, T>::value &&
+ !std::is_convertible<const optional<U> &, T>::value &&
+ !std::is_convertible<const optional<U> &&, T>::value>;
+
+template <class T, class U>
+using enable_assign_forward = detail::enable_if_t<
+ !std::is_same<optional<T>, detail::decay_t<U>>::value &&
+ !detail::conjunction<std::is_scalar<T>,
+ std::is_same<T, detail::decay_t<U>>>::value &&
+ std::is_constructible<T, U>::value && std::is_assignable<T &, U>::value>;
+
+template <class T, class U, class Other>
+using enable_assign_from_other = detail::enable_if_t<
+ std::is_constructible<T, Other>::value &&
+ std::is_assignable<T &, Other>::value &&
+ !std::is_constructible<T, optional<U> &>::value &&
+ !std::is_constructible<T, optional<U> &&>::value &&
+ !std::is_constructible<T, const optional<U> &>::value &&
+ !std::is_constructible<T, const optional<U> &&>::value &&
+ !std::is_convertible<optional<U> &, T>::value &&
+ !std::is_convertible<optional<U> &&, T>::value &&
+ !std::is_convertible<const optional<U> &, T>::value &&
+ !std::is_convertible<const optional<U> &&, T>::value &&
+ !std::is_assignable<T &, optional<U> &>::value &&
+ !std::is_assignable<T &, optional<U> &&>::value &&
+ !std::is_assignable<T &, const optional<U> &>::value &&
+ !std::is_assignable<T &, const optional<U> &&>::value>;
+
+// The storage base manages the actual storage, and correctly propagates
+// trivial destruction from T. This case is for when T is not trivially
+// destructible.
+template <class T, bool = ::std::is_trivially_destructible<T>::value>
+struct optional_storage_base {
+ TL_OPTIONAL_11_CONSTEXPR optional_storage_base() noexcept
+ : m_dummy(), m_has_value(false) {}
+
+ template <class... U>
+ TL_OPTIONAL_11_CONSTEXPR optional_storage_base(in_place_t, U &&... u)
+ : m_value(std::forward<U>(u)...), m_has_value(true) {}
+
+ ~optional_storage_base() {
+ if (m_has_value) {
+ m_value.~T();
+ m_has_value = false;
+ }
+ }
+
+ struct dummy {};
+ union {
+ dummy m_dummy;
+ T m_value;
+ };
+
+ bool m_has_value;
+};
+
+// This case is for when T is trivially destructible.
+template <class T> struct optional_storage_base<T, true> {
+ TL_OPTIONAL_11_CONSTEXPR optional_storage_base() noexcept
+ : m_dummy(), m_has_value(false) {}
+
+ template <class... U>
+ TL_OPTIONAL_11_CONSTEXPR optional_storage_base(in_place_t, U &&... u)
+ : m_value(std::forward<U>(u)...), m_has_value(true) {}
+
+ // No destructor, so this class is trivially destructible
+
+ struct dummy {};
+ union {
+ dummy m_dummy;
+ T m_value;
+ };
+
+ bool m_has_value = false;
+};
+
+// This base class provides some handy member functions which can be used in
+// further derived classes
+template <class T> struct optional_operations_base : optional_storage_base<T> {
+ using optional_storage_base<T>::optional_storage_base;
+
+ void hard_reset() noexcept {
+ get().~T();
+ this->m_has_value = false;
+ }
+
+ template <class... Args> void construct(Args &&... args) {
+ new (std::addressof(this->m_value)) T(std::forward<Args>(args)...);
+ this->m_has_value = true;
+ }
+
+ template <class Opt> void assign(Opt &&rhs) {
+ if (this->has_value()) {
+ if (rhs.has_value()) {
+ this->m_value = std::forward<Opt>(rhs).get();
+ } else {
+ this->m_value.~T();
+ this->m_has_value = false;
+ }
+ }
+
+ else if (rhs.has_value()) {
+ construct(std::forward<Opt>(rhs).get());
+ }
+ }
+
+ bool has_value() const { return this->m_has_value; }
+
+ TL_OPTIONAL_11_CONSTEXPR T &get() & { return this->m_value; }
+ TL_OPTIONAL_11_CONSTEXPR const T &get() const & { return this->m_value; }
+ TL_OPTIONAL_11_CONSTEXPR T &&get() && { return std::move(this->m_value); }
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ constexpr const T &&get() const && { return std::move(this->m_value); }
+#endif
+};
+
+// This class manages conditionally having a trivial copy constructor
+// This specialization is for when T is trivially copy constructible
+template <class T, bool = TL_OPTIONAL_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T)>
+struct optional_copy_base : optional_operations_base<T> {
+ using optional_operations_base<T>::optional_operations_base;
+};
+
+// This specialization is for when T is not trivially copy constructible
+template <class T>
+struct optional_copy_base<T, false> : optional_operations_base<T> {
+ using optional_operations_base<T>::optional_operations_base;
+
+ optional_copy_base() = default;
+ optional_copy_base(const optional_copy_base &rhs)
+ : optional_operations_base<T>() {
+ if (rhs.has_value()) {
+ this->construct(rhs.get());
+ } else {
+ this->m_has_value = false;
+ }
+ }
+
+ optional_copy_base(optional_copy_base &&rhs) = default;
+ optional_copy_base &operator=(const optional_copy_base &rhs) = default;
+ optional_copy_base &operator=(optional_copy_base &&rhs) = default;
+};
+
+// This class manages conditionally having a trivial move constructor
+// Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it
+// doesn't implement an analogue to std::is_trivially_move_constructible. We
+// have to make do with a non-trivial move constructor even if T is trivially
+// move constructible
+#ifndef TL_OPTIONAL_GCC49
+template <class T, bool = std::is_trivially_move_constructible<T>::value>
+struct optional_move_base : optional_copy_base<T> {
+ using optional_copy_base<T>::optional_copy_base;
+};
+#else
+template <class T, bool = false> struct optional_move_base;
+#endif
+template <class T> struct optional_move_base<T, false> : optional_copy_base<T> {
+ using optional_copy_base<T>::optional_copy_base;
+
+ optional_move_base() = default;
+ optional_move_base(const optional_move_base &rhs) = default;
+
+ optional_move_base(optional_move_base &&rhs) noexcept(
+ std::is_nothrow_move_constructible<T>::value) {
+ if (rhs.has_value()) {
+ this->construct(std::move(rhs.get()));
+ } else {
+ this->m_has_value = false;
+ }
+ }
+ optional_move_base &operator=(const optional_move_base &rhs) = default;
+ optional_move_base &operator=(optional_move_base &&rhs) = default;
+};
+
+// This class manages conditionally having a trivial copy assignment operator
+template <class T, bool = TL_OPTIONAL_IS_TRIVIALLY_COPY_ASSIGNABLE(T) &&
+ TL_OPTIONAL_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) &&
+ TL_OPTIONAL_IS_TRIVIALLY_DESTRUCTIBLE(T)>
+struct optional_copy_assign_base : optional_move_base<T> {
+ using optional_move_base<T>::optional_move_base;
+};
+
+template <class T>
+struct optional_copy_assign_base<T, false> : optional_move_base<T> {
+ using optional_move_base<T>::optional_move_base;
+
+ optional_copy_assign_base() = default;
+ optional_copy_assign_base(const optional_copy_assign_base &rhs) = default;
+
+ optional_copy_assign_base(optional_copy_assign_base &&rhs) = default;
+ optional_copy_assign_base &operator=(const optional_copy_assign_base &rhs) {
+ this->assign(rhs);
+ return *this;
+ }
+ optional_copy_assign_base &
+ operator=(optional_copy_assign_base &&rhs) = default;
+};
+
+// This class manages conditionally having a trivial move assignment operator
+// Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it
+// doesn't implement an analogue to std::is_trivially_move_assignable. We have
+// to make do with a non-trivial move assignment operator even if T is trivially
+// move assignable
+#ifndef TL_OPTIONAL_GCC49
+template <class T, bool = std::is_trivially_destructible<T>::value
+ &&std::is_trivially_move_constructible<T>::value
+ &&std::is_trivially_move_assignable<T>::value>
+struct optional_move_assign_base : optional_copy_assign_base<T> {
+ using optional_copy_assign_base<T>::optional_copy_assign_base;
+};
+#else
+template <class T, bool = false> struct optional_move_assign_base;
+#endif
+
+template <class T>
+struct optional_move_assign_base<T, false> : optional_copy_assign_base<T> {
+ using optional_copy_assign_base<T>::optional_copy_assign_base;
+
+ optional_move_assign_base() = default;
+ optional_move_assign_base(const optional_move_assign_base &rhs) = default;
+
+ optional_move_assign_base(optional_move_assign_base &&rhs) = default;
+
+ optional_move_assign_base &
+ operator=(const optional_move_assign_base &rhs) = default;
+
+ optional_move_assign_base &
+ operator=(optional_move_assign_base &&rhs) noexcept(
+ std::is_nothrow_move_constructible<T>::value
+ &&std::is_nothrow_move_assignable<T>::value) {
+ this->assign(std::move(rhs));
+ return *this;
+ }
+};
+
+// optional_delete_ctor_base will conditionally delete copy and move
+// constructors depending on whether T is copy/move constructible
+template <class T, bool EnableCopy = std::is_copy_constructible<T>::value,
+ bool EnableMove = std::is_move_constructible<T>::value>
+struct optional_delete_ctor_base {
+ optional_delete_ctor_base() = default;
+ optional_delete_ctor_base(const optional_delete_ctor_base &) = default;
+ optional_delete_ctor_base(optional_delete_ctor_base &&) noexcept = default;
+ optional_delete_ctor_base &
+ operator=(const optional_delete_ctor_base &) = default;
+ optional_delete_ctor_base &
+ operator=(optional_delete_ctor_base &&) noexcept = default;
+};
+
+template <class T> struct optional_delete_ctor_base<T, true, false> {
+ optional_delete_ctor_base() = default;
+ optional_delete_ctor_base(const optional_delete_ctor_base &) = default;
+ optional_delete_ctor_base(optional_delete_ctor_base &&) noexcept = delete;
+ optional_delete_ctor_base &
+ operator=(const optional_delete_ctor_base &) = default;
+ optional_delete_ctor_base &
+ operator=(optional_delete_ctor_base &&) noexcept = default;
+};
+
+template <class T> struct optional_delete_ctor_base<T, false, true> {
+ optional_delete_ctor_base() = default;
+ optional_delete_ctor_base(const optional_delete_ctor_base &) = delete;
+ optional_delete_ctor_base(optional_delete_ctor_base &&) noexcept = default;
+ optional_delete_ctor_base &
+ operator=(const optional_delete_ctor_base &) = default;
+ optional_delete_ctor_base &
+ operator=(optional_delete_ctor_base &&) noexcept = default;
+};
+
+template <class T> struct optional_delete_ctor_base<T, false, false> {
+ optional_delete_ctor_base() = default;
+ optional_delete_ctor_base(const optional_delete_ctor_base &) = delete;
+ optional_delete_ctor_base(optional_delete_ctor_base &&) noexcept = delete;
+ optional_delete_ctor_base &
+ operator=(const optional_delete_ctor_base &) = default;
+ optional_delete_ctor_base &
+ operator=(optional_delete_ctor_base &&) noexcept = default;
+};
+
+// optional_delete_assign_base will conditionally delete copy and move
+// constructors depending on whether T is copy/move constructible + assignable
+template <class T,
+ bool EnableCopy = (std::is_copy_constructible<T>::value &&
+ std::is_copy_assignable<T>::value),
+ bool EnableMove = (std::is_move_constructible<T>::value &&
+ std::is_move_assignable<T>::value)>
+struct optional_delete_assign_base {
+ optional_delete_assign_base() = default;
+ optional_delete_assign_base(const optional_delete_assign_base &) = default;
+ optional_delete_assign_base(optional_delete_assign_base &&) noexcept =
+ default;
+ optional_delete_assign_base &
+ operator=(const optional_delete_assign_base &) = default;
+ optional_delete_assign_base &
+ operator=(optional_delete_assign_base &&) noexcept = default;
+};
+
+template <class T> struct optional_delete_assign_base<T, true, false> {
+ optional_delete_assign_base() = default;
+ optional_delete_assign_base(const optional_delete_assign_base &) = default;
+ optional_delete_assign_base(optional_delete_assign_base &&) noexcept =
+ default;
+ optional_delete_assign_base &
+ operator=(const optional_delete_assign_base &) = default;
+ optional_delete_assign_base &
+ operator=(optional_delete_assign_base &&) noexcept = delete;
+};
+
+template <class T> struct optional_delete_assign_base<T, false, true> {
+ optional_delete_assign_base() = default;
+ optional_delete_assign_base(const optional_delete_assign_base &) = default;
+ optional_delete_assign_base(optional_delete_assign_base &&) noexcept =
+ default;
+ optional_delete_assign_base &
+ operator=(const optional_delete_assign_base &) = delete;
+ optional_delete_assign_base &
+ operator=(optional_delete_assign_base &&) noexcept = default;
+};
+
+template <class T> struct optional_delete_assign_base<T, false, false> {
+ optional_delete_assign_base() = default;
+ optional_delete_assign_base(const optional_delete_assign_base &) = default;
+ optional_delete_assign_base(optional_delete_assign_base &&) noexcept =
+ default;
+ optional_delete_assign_base &
+ operator=(const optional_delete_assign_base &) = delete;
+ optional_delete_assign_base &
+ operator=(optional_delete_assign_base &&) noexcept = delete;
+};
+
+} // namespace detail
+
+/// A tag type to represent an empty optional
+struct nullopt_t {
+ struct do_not_use {};
+ constexpr explicit nullopt_t(do_not_use, do_not_use) noexcept {}
+};
+/// Represents an empty optional
+static constexpr nullopt_t nullopt{nullopt_t::do_not_use{},
+ nullopt_t::do_not_use{}};
+
+class bad_optional_access : public std::exception {
+public:
+ bad_optional_access() = default;
+ const char *what() const noexcept { return "Optional has no value"; }
+};
+
+/// An optional object is an object that contains the storage for another
+/// object and manages the lifetime of this contained object, if any. The
+/// contained object may be initialized after the optional object has been
+/// initialized, and may be destroyed before the optional object has been
+/// destroyed. The initialization state of the contained object is tracked by
+/// the optional object.
+template <class T>
+class optional : private detail::optional_move_assign_base<T>,
+ private detail::optional_delete_ctor_base<T>,
+ private detail::optional_delete_assign_base<T> {
+ using base = detail::optional_move_assign_base<T>;
+
+ static_assert(!std::is_same<T, in_place_t>::value,
+ "instantiation of optional with in_place_t is ill-formed");
+ static_assert(!std::is_same<detail::decay_t<T>, nullopt_t>::value,
+ "instantiation of optional with nullopt_t is ill-formed");
+
+public:
+// The different versions for C++14 and 11 are needed because deduced return
+// types are not SFINAE-safe. This provides better support for things like
+// generic lambdas. C.f.
+// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0826r0.html
+#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) && \
+ !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55)
+ /// Carries out some operation which returns an optional on the stored
+ /// object if there is one.
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto and_then(F &&f) & {
+ using result = detail::invoke_result_t<F, T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto and_then(F &&f) && {
+ using result = detail::invoke_result_t<F, T &&>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : result(nullopt);
+ }
+
+ template <class F> constexpr auto and_then(F &&f) const & {
+ using result = detail::invoke_result_t<F, const T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F> constexpr auto and_then(F &&f) const && {
+ using result = detail::invoke_result_t<F, const T &&>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : result(nullopt);
+ }
+#endif
+#else
+ /// Carries out some operation which returns an optional on the stored
+ /// object if there is one.
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR detail::invoke_result_t<F, T &> and_then(F &&f) & {
+ using result = detail::invoke_result_t<F, T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this) : result(nullopt);
+ }
+
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR detail::invoke_result_t<F, T &&> and_then(F &&f) && {
+ using result = detail::invoke_result_t<F, T &&>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : result(nullopt);
+ }
+
+ template <class F>
+ constexpr detail::invoke_result_t<F, const T &> and_then(F &&f) const & {
+ using result = detail::invoke_result_t<F, const T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F>
+ constexpr detail::invoke_result_t<F, const T &&> and_then(F &&f) const && {
+ using result = detail::invoke_result_t<F, const T &&>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : result(nullopt);
+ }
+#endif
+#endif
+
+#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) && \
+ !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55)
+ /// Carries out some operation on the stored object if there is one.
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto map(F &&f) & {
+ return optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto map(F &&f) && {
+ return optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+
+ template <class F> constexpr auto map(F &&f) const & {
+ return optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F> constexpr auto map(F &&f) const && {
+ return optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#else
+ /// Carries out some operation on the stored object if there is one.
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR decltype(optional_map_impl(std::declval<optional &>(),
+ std::declval<F &&>()))
+ map(F &&f) & {
+ return optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR decltype(optional_map_impl(std::declval<optional &&>(),
+ std::declval<F &&>()))
+ map(F &&f) && {
+ return optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+
+ template <class F>
+ constexpr decltype(optional_map_impl(std::declval<const optional &>(),
+ std::declval<F &&>()))
+ map(F &&f) const & {
+ return optional_map_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F>
+ constexpr decltype(optional_map_impl(std::declval<const optional &&>(),
+ std::declval<F &&>()))
+ map(F &&f) const && {
+ return optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+#endif
+
+#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) && \
+ !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55)
+ /// Carries out some operation on the stored object if there is one.
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto transform(F&& f) & {
+ return optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto transform(F&& f) && {
+ return optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+
+ template <class F> constexpr auto transform(F&& f) const & {
+ return optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F> constexpr auto transform(F&& f) const && {
+ return optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#else
+ /// Carries out some operation on the stored object if there is one.
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR decltype(optional_map_impl(std::declval<optional&>(),
+ std::declval<F&&>()))
+ transform(F&& f) & {
+ return optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR decltype(optional_map_impl(std::declval<optional&&>(),
+ std::declval<F&&>()))
+ transform(F&& f) && {
+ return optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+
+ template <class F>
+ constexpr decltype(optional_map_impl(std::declval<const optional&>(),
+ std::declval<F&&>()))
+ transform(F&& f) const & {
+ return optional_map_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F>
+ constexpr decltype(optional_map_impl(std::declval<const optional&&>(),
+ std::declval<F&&>()))
+ transform(F&& f) const && {
+ return optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+#endif
+
+ /// Calls `f` if the optional is empty
+ template <class F, detail::enable_if_ret_void<F> * = nullptr>
+ optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) & {
+ if (has_value())
+ return *this;
+
+ std::forward<F>(f)();
+ return nullopt;
+ }
+
+ template <class F, detail::disable_if_ret_void<F> * = nullptr>
+ optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) & {
+ return has_value() ? *this : std::forward<F>(f)();
+ }
+
+ template <class F, detail::enable_if_ret_void<F> * = nullptr>
+ optional<T> or_else(F &&f) && {
+ if (has_value())
+ return std::move(*this);
+
+ std::forward<F>(f)();
+ return nullopt;
+ }
+
+ template <class F, detail::disable_if_ret_void<F> * = nullptr>
+ optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) && {
+ return has_value() ? std::move(*this) : std::forward<F>(f)();
+ }
+
+ template <class F, detail::enable_if_ret_void<F> * = nullptr>
+ optional<T> or_else(F &&f) const & {
+ if (has_value())
+ return *this;
+
+ std::forward<F>(f)();
+ return nullopt;
+ }
+
+ template <class F, detail::disable_if_ret_void<F> * = nullptr>
+ optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) const & {
+ return has_value() ? *this : std::forward<F>(f)();
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F, detail::enable_if_ret_void<F> * = nullptr>
+ optional<T> or_else(F &&f) const && {
+ if (has_value())
+ return std::move(*this);
+
+ std::forward<F>(f)();
+ return nullopt;
+ }
+
+ template <class F, detail::disable_if_ret_void<F> * = nullptr>
+ optional<T> or_else(F &&f) const && {
+ return has_value() ? std::move(*this) : std::forward<F>(f)();
+ }
+#endif
+
+ /// Maps the stored value with `f` if there is one, otherwise returns `u`.
+ template <class F, class U> U map_or(F &&f, U &&u) & {
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : std::forward<U>(u);
+ }
+
+ template <class F, class U> U map_or(F &&f, U &&u) && {
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : std::forward<U>(u);
+ }
+
+ template <class F, class U> U map_or(F &&f, U &&u) const & {
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : std::forward<U>(u);
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F, class U> U map_or(F &&f, U &&u) const && {
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : std::forward<U>(u);
+ }
+#endif
+
+ /// Maps the stored value with `f` if there is one, otherwise calls
+ /// `u` and returns the result.
+ template <class F, class U>
+ detail::invoke_result_t<U> map_or_else(F &&f, U &&u) & {
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : std::forward<U>(u)();
+ }
+
+ template <class F, class U>
+ detail::invoke_result_t<U> map_or_else(F &&f, U &&u) && {
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : std::forward<U>(u)();
+ }
+
+ template <class F, class U>
+ detail::invoke_result_t<U> map_or_else(F &&f, U &&u) const & {
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : std::forward<U>(u)();
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F, class U>
+ detail::invoke_result_t<U> map_or_else(F &&f, U &&u) const && {
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : std::forward<U>(u)();
+ }
+#endif
+
+ /// Returns `u` if `*this` has a value, otherwise an empty optional.
+ template <class U>
+ constexpr optional<typename std::decay<U>::type> conjunction(U &&u) const {
+ using result = optional<detail::decay_t<U>>;
+ return has_value() ? result{u} : result{nullopt};
+ }
+
+ /// Returns `rhs` if `*this` is empty, otherwise the current value.
+ TL_OPTIONAL_11_CONSTEXPR optional disjunction(const optional &rhs) & {
+ return has_value() ? *this : rhs;
+ }
+
+ constexpr optional disjunction(const optional &rhs) const & {
+ return has_value() ? *this : rhs;
+ }
+
+ TL_OPTIONAL_11_CONSTEXPR optional disjunction(const optional &rhs) && {
+ return has_value() ? std::move(*this) : rhs;
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ constexpr optional disjunction(const optional &rhs) const && {
+ return has_value() ? std::move(*this) : rhs;
+ }
+#endif
+
+ TL_OPTIONAL_11_CONSTEXPR optional disjunction(optional &&rhs) & {
+ return has_value() ? *this : std::move(rhs);
+ }
+
+ constexpr optional disjunction(optional &&rhs) const & {
+ return has_value() ? *this : std::move(rhs);
+ }
+
+ TL_OPTIONAL_11_CONSTEXPR optional disjunction(optional &&rhs) && {
+ return has_value() ? std::move(*this) : std::move(rhs);
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ constexpr optional disjunction(optional &&rhs) const && {
+ return has_value() ? std::move(*this) : std::move(rhs);
+ }
+#endif
+
+ /// Takes the value out of the optional, leaving it empty
+ optional take() {
+ optional ret = std::move(*this);
+ reset();
+ return ret;
+ }
+
+ using value_type = T;
+
+ /// Constructs an optional that does not contain a value.
+ constexpr optional() noexcept = default;
+
+ constexpr optional(nullopt_t) noexcept {}
+
+ /// Copy constructor
+ ///
+ /// If `rhs` contains a value, the stored value is direct-initialized with
+ /// it. Otherwise, the constructed optional is empty.
+ TL_OPTIONAL_11_CONSTEXPR optional(const optional &rhs) = default;
+
+ /// Move constructor
+ ///
+ /// If `rhs` contains a value, the stored value is direct-initialized with
+ /// it. Otherwise, the constructed optional is empty.
+ TL_OPTIONAL_11_CONSTEXPR optional(optional &&rhs) = default;
+
+ /// Constructs the stored value in-place using the given arguments.
+ template <class... Args>
+ constexpr explicit optional(
+ detail::enable_if_t<std::is_constructible<T, Args...>::value, in_place_t>,
+ Args &&... args)
+ : base(in_place, std::forward<Args>(args)...) {}
+
+ template <class U, class... Args>
+ TL_OPTIONAL_11_CONSTEXPR explicit optional(
+ detail::enable_if_t<std::is_constructible<T, std::initializer_list<U> &,
+ Args &&...>::value,
+ in_place_t>,
+ std::initializer_list<U> il, Args &&... args) {
+ this->construct(il, std::forward<Args>(args)...);
+ }
+
+ /// Constructs the stored value with `u`.
+ template <
+ class U = T,
+ detail::enable_if_t<std::is_convertible<U &&, T>::value> * = nullptr,
+ detail::enable_forward_value<T, U> * = nullptr>
+ constexpr optional(U &&u) : base(in_place, std::forward<U>(u)) {}
+
+ template <
+ class U = T,
+ detail::enable_if_t<!std::is_convertible<U &&, T>::value> * = nullptr,
+ detail::enable_forward_value<T, U> * = nullptr>
+ constexpr explicit optional(U &&u) : base(in_place, std::forward<U>(u)) {}
+
+ /// Converting copy constructor.
+ template <
+ class U, detail::enable_from_other<T, U, const U &> * = nullptr,
+ detail::enable_if_t<std::is_convertible<const U &, T>::value> * = nullptr>
+ optional(const optional<U> &rhs) {
+ if (rhs.has_value()) {
+ this->construct(*rhs);
+ }
+ }
+
+ template <class U, detail::enable_from_other<T, U, const U &> * = nullptr,
+ detail::enable_if_t<!std::is_convertible<const U &, T>::value> * =
+ nullptr>
+ explicit optional(const optional<U> &rhs) {
+ if (rhs.has_value()) {
+ this->construct(*rhs);
+ }
+ }
+
+ /// Converting move constructor.
+ template <
+ class U, detail::enable_from_other<T, U, U &&> * = nullptr,
+ detail::enable_if_t<std::is_convertible<U &&, T>::value> * = nullptr>
+ optional(optional<U> &&rhs) {
+ if (rhs.has_value()) {
+ this->construct(std::move(*rhs));
+ }
+ }
+
+ template <
+ class U, detail::enable_from_other<T, U, U &&> * = nullptr,
+ detail::enable_if_t<!std::is_convertible<U &&, T>::value> * = nullptr>
+ explicit optional(optional<U> &&rhs) {
+ if (rhs.has_value()) {
+ this->construct(std::move(*rhs));
+ }
+ }
+
+ /// Destroys the stored value if there is one.
+ ~optional() = default;
+
+ /// Assignment to empty.
+ ///
+ /// Destroys the current value if there is one.
+ optional &operator=(nullopt_t) noexcept {
+ if (has_value()) {
+ this->m_value.~T();
+ this->m_has_value = false;
+ }
+
+ return *this;
+ }
+
+ /// Copy assignment.
+ ///
+ /// Copies the value from `rhs` if there is one. Otherwise resets the stored
+ /// value in `*this`.
+ optional &operator=(const optional &rhs) = default;
+
+ /// Move assignment.
+ ///
+ /// Moves the value from `rhs` if there is one. Otherwise resets the stored
+ /// value in `*this`.
+ optional &operator=(optional &&rhs) = default;
+
+ /// Assigns the stored value from `u`, destroying the old value if there was
+ /// one.
+ template <class U = T, detail::enable_assign_forward<T, U> * = nullptr>
+ optional &operator=(U &&u) {
+ if (has_value()) {
+ this->m_value = std::forward<U>(u);
+ } else {
+ this->construct(std::forward<U>(u));
+ }
+
+ return *this;
+ }
+
+ /// Converting copy assignment operator.
+ ///
+ /// Copies the value from `rhs` if there is one. Otherwise resets the stored
+ /// value in `*this`.
+ template <class U,
+ detail::enable_assign_from_other<T, U, const U &> * = nullptr>
+ optional &operator=(const optional<U> &rhs) {
+ if (has_value()) {
+ if (rhs.has_value()) {
+ this->m_value = *rhs;
+ } else {
+ this->hard_reset();
+ }
+ }
+
+ else if (rhs.has_value()) {
+ this->construct(*rhs);
+ }
+
+ return *this;
+ }
+
+ // TODO check exception guarantee
+ /// Converting move assignment operator.
+ ///
+ /// Moves the value from `rhs` if there is one. Otherwise resets the stored
+ /// value in `*this`.
+ template <class U, detail::enable_assign_from_other<T, U, U> * = nullptr>
+ optional &operator=(optional<U> &&rhs) {
+ if (has_value()) {
+ if (rhs.has_value()) {
+ this->m_value = std::move(*rhs);
+ } else {
+ this->hard_reset();
+ }
+ }
+
+ else if (rhs.has_value()) {
+ this->construct(std::move(*rhs));
+ }
+
+ return *this;
+ }
+
+ /// Constructs the value in-place, destroying the current one if there is
+ /// one.
+ template <class... Args> T &emplace(Args &&... args) {
+ static_assert(std::is_constructible<T, Args &&...>::value,
+ "T must be constructible with Args");
+
+ *this = nullopt;
+ this->construct(std::forward<Args>(args)...);
+ return value();
+ }
+
+ template <class U, class... Args>
+ detail::enable_if_t<
+ std::is_constructible<T, std::initializer_list<U> &, Args &&...>::value,
+ T &>
+ emplace(std::initializer_list<U> il, Args &&... args) {
+ *this = nullopt;
+ this->construct(il, std::forward<Args>(args)...);
+ return value();
+ }
+
+ /// Swaps this optional with the other.
+ ///
+ /// If neither optionals have a value, nothing happens.
+ /// If both have a value, the values are swapped.
+ /// If one has a value, it is moved to the other and the movee is left
+ /// valueless.
+ void
+ swap(optional &rhs) noexcept(std::is_nothrow_move_constructible<T>::value
+ &&detail::is_nothrow_swappable<T>::value) {
+ using std::swap;
+ if (has_value()) {
+ if (rhs.has_value()) {
+ swap(**this, *rhs);
+ } else {
+ new (std::addressof(rhs.m_value)) T(std::move(this->m_value));
+ this->m_value.T::~T();
+ }
+ } else if (rhs.has_value()) {
+ new (std::addressof(this->m_value)) T(std::move(rhs.m_value));
+ rhs.m_value.T::~T();
+ }
+ swap(this->m_has_value, rhs.m_has_value);
+ }
+
+ /// Returns a pointer to the stored value
+ constexpr const T *operator->() const {
+ return std::addressof(this->m_value);
+ }
+
+ TL_OPTIONAL_11_CONSTEXPR T *operator->() {
+ return std::addressof(this->m_value);
+ }
+
+ /// Returns the stored value
+ TL_OPTIONAL_11_CONSTEXPR T &operator*() & { return this->m_value; }
+
+ constexpr const T &operator*() const & { return this->m_value; }
+
+ TL_OPTIONAL_11_CONSTEXPR T &&operator*() && {
+ return std::move(this->m_value);
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ constexpr const T &&operator*() const && { return std::move(this->m_value); }
+#endif
+
+ /// Returns whether or not the optional has a value
+ constexpr bool has_value() const noexcept { return this->m_has_value; }
+
+ constexpr explicit operator bool() const noexcept {
+ return this->m_has_value;
+ }
+
+ /// Returns the contained value if there is one, otherwise throws bad_optional_access
+ TL_OPTIONAL_11_CONSTEXPR T &value() & {
+ if (has_value())
+ return this->m_value;
+ throw bad_optional_access();
+ }
+ TL_OPTIONAL_11_CONSTEXPR const T &value() const & {
+ if (has_value())
+ return this->m_value;
+ throw bad_optional_access();
+ }
+ TL_OPTIONAL_11_CONSTEXPR T &&value() && {
+ if (has_value())
+ return std::move(this->m_value);
+ throw bad_optional_access();
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ TL_OPTIONAL_11_CONSTEXPR const T &&value() const && {
+ if (has_value())
+ return std::move(this->m_value);
+ throw bad_optional_access();
+ }
+#endif
+
+ /// Returns the stored value if there is one, otherwise returns `u`
+ template <class U> constexpr T value_or(U &&u) const & {
+ static_assert(std::is_copy_constructible<T>::value &&
+ std::is_convertible<U &&, T>::value,
+ "T must be copy constructible and convertible from U");
+ return has_value() ? **this : static_cast<T>(std::forward<U>(u));
+ }
+
+ template <class U> TL_OPTIONAL_11_CONSTEXPR T value_or(U &&u) && {
+ static_assert(std::is_move_constructible<T>::value &&
+ std::is_convertible<U &&, T>::value,
+ "T must be move constructible and convertible from U");
+ return has_value() ? std::move(**this) : static_cast<T>(std::forward<U>(u));
+ }
+
+ /// Destroys the stored value if one exists, making the optional empty
+ void reset() noexcept {
+ if (has_value()) {
+ this->m_value.~T();
+ this->m_has_value = false;
+ }
+ }
+}; // namespace tl
+
+/// Compares two optional objects
+template <class T, class U>
+inline constexpr bool operator==(const optional<T> &lhs,
+ const optional<U> &rhs) {
+ return lhs.has_value() == rhs.has_value() &&
+ (!lhs.has_value() || *lhs == *rhs);
+}
+template <class T, class U>
+inline constexpr bool operator!=(const optional<T> &lhs,
+ const optional<U> &rhs) {
+ return lhs.has_value() != rhs.has_value() ||
+ (lhs.has_value() && *lhs != *rhs);
+}
+template <class T, class U>
+inline constexpr bool operator<(const optional<T> &lhs,
+ const optional<U> &rhs) {
+ return rhs.has_value() && (!lhs.has_value() || *lhs < *rhs);
+}
+template <class T, class U>
+inline constexpr bool operator>(const optional<T> &lhs,
+ const optional<U> &rhs) {
+ return lhs.has_value() && (!rhs.has_value() || *lhs > *rhs);
+}
+template <class T, class U>
+inline constexpr bool operator<=(const optional<T> &lhs,
+ const optional<U> &rhs) {
+ return !lhs.has_value() || (rhs.has_value() && *lhs <= *rhs);
+}
+template <class T, class U>
+inline constexpr bool operator>=(const optional<T> &lhs,
+ const optional<U> &rhs) {
+ return !rhs.has_value() || (lhs.has_value() && *lhs >= *rhs);
+}
+
+/// Compares an optional to a `nullopt`
+template <class T>
+inline constexpr bool operator==(const optional<T> &lhs, nullopt_t) noexcept {
+ return !lhs.has_value();
+}
+template <class T>
+inline constexpr bool operator==(nullopt_t, const optional<T> &rhs) noexcept {
+ return !rhs.has_value();
+}
+template <class T>
+inline constexpr bool operator!=(const optional<T> &lhs, nullopt_t) noexcept {
+ return lhs.has_value();
+}
+template <class T>
+inline constexpr bool operator!=(nullopt_t, const optional<T> &rhs) noexcept {
+ return rhs.has_value();
+}
+template <class T>
+inline constexpr bool operator<(const optional<T> &, nullopt_t) noexcept {
+ return false;
+}
+template <class T>
+inline constexpr bool operator<(nullopt_t, const optional<T> &rhs) noexcept {
+ return rhs.has_value();
+}
+template <class T>
+inline constexpr bool operator<=(const optional<T> &lhs, nullopt_t) noexcept {
+ return !lhs.has_value();
+}
+template <class T>
+inline constexpr bool operator<=(nullopt_t, const optional<T> &) noexcept {
+ return true;
+}
+template <class T>
+inline constexpr bool operator>(const optional<T> &lhs, nullopt_t) noexcept {
+ return lhs.has_value();
+}
+template <class T>
+inline constexpr bool operator>(nullopt_t, const optional<T> &) noexcept {
+ return false;
+}
+template <class T>
+inline constexpr bool operator>=(const optional<T> &, nullopt_t) noexcept {
+ return true;
+}
+template <class T>
+inline constexpr bool operator>=(nullopt_t, const optional<T> &rhs) noexcept {
+ return !rhs.has_value();
+}
+
+/// Compares the optional with a value.
+template <class T, class U>
+inline constexpr bool operator==(const optional<T> &lhs, const U &rhs) {
+ return lhs.has_value() ? *lhs == rhs : false;
+}
+template <class T, class U>
+inline constexpr bool operator==(const U &lhs, const optional<T> &rhs) {
+ return rhs.has_value() ? lhs == *rhs : false;
+}
+template <class T, class U>
+inline constexpr bool operator!=(const optional<T> &lhs, const U &rhs) {
+ return lhs.has_value() ? *lhs != rhs : true;
+}
+template <class T, class U>
+inline constexpr bool operator!=(const U &lhs, const optional<T> &rhs) {
+ return rhs.has_value() ? lhs != *rhs : true;
+}
+template <class T, class U>
+inline constexpr bool operator<(const optional<T> &lhs, const U &rhs) {
+ return lhs.has_value() ? *lhs < rhs : true;
+}
+template <class T, class U>
+inline constexpr bool operator<(const U &lhs, const optional<T> &rhs) {
+ return rhs.has_value() ? lhs < *rhs : false;
+}
+template <class T, class U>
+inline constexpr bool operator<=(const optional<T> &lhs, const U &rhs) {
+ return lhs.has_value() ? *lhs <= rhs : true;
+}
+template <class T, class U>
+inline constexpr bool operator<=(const U &lhs, const optional<T> &rhs) {
+ return rhs.has_value() ? lhs <= *rhs : false;
+}
+template <class T, class U>
+inline constexpr bool operator>(const optional<T> &lhs, const U &rhs) {
+ return lhs.has_value() ? *lhs > rhs : false;
+}
+template <class T, class U>
+inline constexpr bool operator>(const U &lhs, const optional<T> &rhs) {
+ return rhs.has_value() ? lhs > *rhs : true;
+}
+template <class T, class U>
+inline constexpr bool operator>=(const optional<T> &lhs, const U &rhs) {
+ return lhs.has_value() ? *lhs >= rhs : false;
+}
+template <class T, class U>
+inline constexpr bool operator>=(const U &lhs, const optional<T> &rhs) {
+ return rhs.has_value() ? lhs >= *rhs : true;
+}
+
+template <class T,
+ detail::enable_if_t<std::is_move_constructible<T>::value> * = nullptr,
+ detail::enable_if_t<detail::is_swappable<T>::value> * = nullptr>
+void swap(optional<T> &lhs,
+ optional<T> &rhs) noexcept(noexcept(lhs.swap(rhs))) {
+ return lhs.swap(rhs);
+}
+
+namespace detail {
+struct i_am_secret {};
+} // namespace detail
+
+template <class T = detail::i_am_secret, class U,
+ class Ret =
+ detail::conditional_t<std::is_same<T, detail::i_am_secret>::value,
+ detail::decay_t<U>, T>>
+inline constexpr optional<Ret> make_optional(U &&v) {
+ return optional<Ret>(std::forward<U>(v));
+}
+
+template <class T, class... Args>
+inline constexpr optional<T> make_optional(Args &&... args) {
+ return optional<T>(in_place, std::forward<Args>(args)...);
+}
+template <class T, class U, class... Args>
+inline constexpr optional<T> make_optional(std::initializer_list<U> il,
+ Args &&... args) {
+ return optional<T>(in_place, il, std::forward<Args>(args)...);
+}
+
+#if __cplusplus >= 201703L
+template <class T> optional(T)->optional<T>;
+#endif
+
+/// \exclude
+namespace detail {
+#ifdef TL_OPTIONAL_CXX14
+template <class Opt, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Opt>())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+constexpr auto optional_map_impl(Opt &&opt, F &&f) {
+ return opt.has_value()
+ ? detail::invoke(std::forward<F>(f), *std::forward<Opt>(opt))
+ : optional<Ret>(nullopt);
+}
+
+template <class Opt, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Opt>())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+auto optional_map_impl(Opt &&opt, F &&f) {
+ if (opt.has_value()) {
+ detail::invoke(std::forward<F>(f), *std::forward<Opt>(opt));
+ return make_optional(monostate{});
+ }
+
+ return optional<monostate>(nullopt);
+}
+#else
+template <class Opt, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Opt>())),
+ detail::enable_if_t<!std::is_void<Ret>::value> * = nullptr>
+
+constexpr auto optional_map_impl(Opt &&opt, F &&f) -> optional<Ret> {
+ return opt.has_value()
+ ? detail::invoke(std::forward<F>(f), *std::forward<Opt>(opt))
+ : optional<Ret>(nullopt);
+}
+
+template <class Opt, class F,
+ class Ret = decltype(detail::invoke(std::declval<F>(),
+ *std::declval<Opt>())),
+ detail::enable_if_t<std::is_void<Ret>::value> * = nullptr>
+
+auto optional_map_impl(Opt &&opt, F &&f) -> optional<monostate> {
+ if (opt.has_value()) {
+ detail::invoke(std::forward<F>(f), *std::forward<Opt>(opt));
+ return monostate{};
+ }
+
+ return nullopt;
+}
+#endif
+} // namespace detail
+
+/// Specialization for when `T` is a reference. `optional<T&>` acts similarly
+/// to a `T*`, but provides more operations and shows intent more clearly.
+template <class T> class optional<T &> {
+public:
+// The different versions for C++14 and 11 are needed because deduced return
+// types are not SFINAE-safe. This provides better support for things like
+// generic lambdas. C.f.
+// http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0826r0.html
+#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) && \
+ !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55)
+
+ /// Carries out some operation which returns an optional on the stored
+ /// object if there is one.
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto and_then(F &&f) & {
+ using result = detail::invoke_result_t<F, T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto and_then(F &&f) && {
+ using result = detail::invoke_result_t<F, T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+ template <class F> constexpr auto and_then(F &&f) const & {
+ using result = detail::invoke_result_t<F, const T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F> constexpr auto and_then(F &&f) const && {
+ using result = detail::invoke_result_t<F, const T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+#endif
+#else
+ /// Carries out some operation which returns an optional on the stored
+ /// object if there is one.
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR detail::invoke_result_t<F, T &> and_then(F &&f) & {
+ using result = detail::invoke_result_t<F, T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR detail::invoke_result_t<F, T &> and_then(F &&f) && {
+ using result = detail::invoke_result_t<F, T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+ template <class F>
+ constexpr detail::invoke_result_t<F, const T &> and_then(F &&f) const & {
+ using result = detail::invoke_result_t<F, const T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : result(nullopt);
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F>
+ constexpr detail::invoke_result_t<F, const T &> and_then(F &&f) const && {
+ using result = detail::invoke_result_t<F, const T &>;
+ static_assert(detail::is_optional<result>::value,
+ "F must return an optional");
+
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : result(nullopt);
+ }
+#endif
+#endif
+
+#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) && \
+ !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55)
+ /// Carries out some operation on the stored object if there is one.
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto map(F &&f) & {
+ return detail::optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto map(F &&f) && {
+ return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+
+ template <class F> constexpr auto map(F &&f) const & {
+ return detail::optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F> constexpr auto map(F &&f) const && {
+ return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#else
+ /// Carries out some operation on the stored object if there is one.
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR decltype(detail::optional_map_impl(std::declval<optional &>(),
+ std::declval<F &&>()))
+ map(F &&f) & {
+ return detail::optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR decltype(detail::optional_map_impl(std::declval<optional &&>(),
+ std::declval<F &&>()))
+ map(F &&f) && {
+ return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+
+ template <class F>
+ constexpr decltype(detail::optional_map_impl(std::declval<const optional &>(),
+ std::declval<F &&>()))
+ map(F &&f) const & {
+ return detail::optional_map_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F>
+ constexpr decltype(detail::optional_map_impl(std::declval<const optional &&>(),
+ std::declval<F &&>()))
+ map(F &&f) const && {
+ return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+#endif
+
+#if defined(TL_OPTIONAL_CXX14) && !defined(TL_OPTIONAL_GCC49) && \
+ !defined(TL_OPTIONAL_GCC54) && !defined(TL_OPTIONAL_GCC55)
+ /// Carries out some operation on the stored object if there is one.
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto transform(F&& f) & {
+ return detail::optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F> TL_OPTIONAL_11_CONSTEXPR auto transform(F&& f) && {
+ return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+
+ template <class F> constexpr auto transform(F&& f) const & {
+ return detail::optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ template <class F> constexpr auto transform(F&& f) const && {
+ return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#else
+ /// Carries out some operation on the stored object if there is one.
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR decltype(detail::optional_map_impl(std::declval<optional&>(),
+ std::declval<F&&>()))
+ transform(F&& f) & {
+ return detail::optional_map_impl(*this, std::forward<F>(f));
+ }
+
+ /// \group map
+ /// \synopsis template <class F> auto transform(F &&f) &&;
+ template <class F>
+ TL_OPTIONAL_11_CONSTEXPR decltype(detail::optional_map_impl(std::declval<optional&&>(),
+ std::declval<F&&>()))
+ transform(F&& f) && {
+ return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+
+ template <class F>
+ constexpr decltype(detail::optional_map_impl(std::declval<const optional&>(),
+ std::declval<F&&>()))
+ transform(F&& f) const & {
+ return detail::optional_map_impl(*this, std::forward<F>(f));
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F>
+ constexpr decltype(detail::optional_map_impl(std::declval<const optional&&>(),
+ std::declval<F&&>()))
+ transform(F&& f) const && {
+ return detail::optional_map_impl(std::move(*this), std::forward<F>(f));
+ }
+#endif
+#endif
+
+ /// Calls `f` if the optional is empty
+ template <class F, detail::enable_if_ret_void<F> * = nullptr>
+ optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) & {
+ if (has_value())
+ return *this;
+
+ std::forward<F>(f)();
+ return nullopt;
+ }
+
+ template <class F, detail::disable_if_ret_void<F> * = nullptr>
+ optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) & {
+ return has_value() ? *this : std::forward<F>(f)();
+ }
+
+ template <class F, detail::enable_if_ret_void<F> * = nullptr>
+ optional<T> or_else(F &&f) && {
+ if (has_value())
+ return std::move(*this);
+
+ std::forward<F>(f)();
+ return nullopt;
+ }
+
+ template <class F, detail::disable_if_ret_void<F> * = nullptr>
+ optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) && {
+ return has_value() ? std::move(*this) : std::forward<F>(f)();
+ }
+
+ template <class F, detail::enable_if_ret_void<F> * = nullptr>
+ optional<T> or_else(F &&f) const & {
+ if (has_value())
+ return *this;
+
+ std::forward<F>(f)();
+ return nullopt;
+ }
+
+ template <class F, detail::disable_if_ret_void<F> * = nullptr>
+ optional<T> TL_OPTIONAL_11_CONSTEXPR or_else(F &&f) const & {
+ return has_value() ? *this : std::forward<F>(f)();
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F, detail::enable_if_ret_void<F> * = nullptr>
+ optional<T> or_else(F &&f) const && {
+ if (has_value())
+ return std::move(*this);
+
+ std::forward<F>(f)();
+ return nullopt;
+ }
+
+ template <class F, detail::disable_if_ret_void<F> * = nullptr>
+ optional<T> or_else(F &&f) const && {
+ return has_value() ? std::move(*this) : std::forward<F>(f)();
+ }
+#endif
+
+ /// Maps the stored value with `f` if there is one, otherwise returns `u`
+ template <class F, class U> U map_or(F &&f, U &&u) & {
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : std::forward<U>(u);
+ }
+
+ template <class F, class U> U map_or(F &&f, U &&u) && {
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : std::forward<U>(u);
+ }
+
+ template <class F, class U> U map_or(F &&f, U &&u) const & {
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : std::forward<U>(u);
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F, class U> U map_or(F &&f, U &&u) const && {
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : std::forward<U>(u);
+ }
+#endif
+
+ /// Maps the stored value with `f` if there is one, otherwise calls
+ /// `u` and returns the result.
+ template <class F, class U>
+ detail::invoke_result_t<U> map_or_else(F &&f, U &&u) & {
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : std::forward<U>(u)();
+ }
+
+ template <class F, class U>
+ detail::invoke_result_t<U> map_or_else(F &&f, U &&u) && {
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : std::forward<U>(u)();
+ }
+
+ template <class F, class U>
+ detail::invoke_result_t<U> map_or_else(F &&f, U &&u) const & {
+ return has_value() ? detail::invoke(std::forward<F>(f), **this)
+ : std::forward<U>(u)();
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ template <class F, class U>
+ detail::invoke_result_t<U> map_or_else(F &&f, U &&u) const && {
+ return has_value() ? detail::invoke(std::forward<F>(f), std::move(**this))
+ : std::forward<U>(u)();
+ }
+#endif
+
+ /// Returns `u` if `*this` has a value, otherwise an empty optional.
+ template <class U>
+ constexpr optional<typename std::decay<U>::type> conjunction(U &&u) const {
+ using result = optional<detail::decay_t<U>>;
+ return has_value() ? result{u} : result{nullopt};
+ }
+
+ /// Returns `rhs` if `*this` is empty, otherwise the current value.
+ TL_OPTIONAL_11_CONSTEXPR optional disjunction(const optional &rhs) & {
+ return has_value() ? *this : rhs;
+ }
+
+ constexpr optional disjunction(const optional &rhs) const & {
+ return has_value() ? *this : rhs;
+ }
+
+ TL_OPTIONAL_11_CONSTEXPR optional disjunction(const optional &rhs) && {
+ return has_value() ? std::move(*this) : rhs;
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ constexpr optional disjunction(const optional &rhs) const && {
+ return has_value() ? std::move(*this) : rhs;
+ }
+#endif
+
+ TL_OPTIONAL_11_CONSTEXPR optional disjunction(optional &&rhs) & {
+ return has_value() ? *this : std::move(rhs);
+ }
+
+ constexpr optional disjunction(optional &&rhs) const & {
+ return has_value() ? *this : std::move(rhs);
+ }
+
+ TL_OPTIONAL_11_CONSTEXPR optional disjunction(optional &&rhs) && {
+ return has_value() ? std::move(*this) : std::move(rhs);
+ }
+
+#ifndef TL_OPTIONAL_NO_CONSTRR
+ constexpr optional disjunction(optional &&rhs) const && {
+ return has_value() ? std::move(*this) : std::move(rhs);
+ }
+#endif
+
+ /// Takes the value out of the optional, leaving it empty
+ optional take() {
+ optional ret = std::move(*this);
+ reset();
+ return ret;
+ }
+
+ using value_type = T &;
+
+ /// Constructs an optional that does not contain a value.
+ constexpr optional() noexcept : m_value(nullptr) {}
+
+ constexpr optional(nullopt_t) noexcept : m_value(nullptr) {}
+
+ /// Copy constructor
+ ///
+ /// If `rhs` contains a value, the stored value is direct-initialized with
+ /// it. Otherwise, the constructed optional is empty.
+ TL_OPTIONAL_11_CONSTEXPR optional(const optional &rhs) noexcept = default;
+
+ /// Move constructor
+ ///
+ /// If `rhs` contains a value, the stored value is direct-initialized with
+ /// it. Otherwise, the constructed optional is empty.
+ TL_OPTIONAL_11_CONSTEXPR optional(optional &&rhs) = default;
+
+ /// Constructs the stored value with `u`.
+ template <class U = T,
+ detail::enable_if_t<!detail::is_optional<detail::decay_t<U>>::value>
+ * = nullptr>
+ constexpr optional(U &&u) noexcept : m_value(std::addressof(u)) {
+ static_assert(std::is_lvalue_reference<U>::value, "U must be an lvalue");
+ }
+
+ template <class U>
+ constexpr explicit optional(const optional<U> &rhs) noexcept : optional(*rhs) {}
+
+ /// No-op
+ ~optional() = default;
+
+ /// Assignment to empty.
+ ///
+ /// Destroys the current value if there is one.
+ optional &operator=(nullopt_t) noexcept {
+ m_value = nullptr;
+ return *this;
+ }
+
+ /// Copy assignment.
+ ///
+ /// Rebinds this optional to the referee of `rhs` if there is one. Otherwise
+ /// resets the stored value in `*this`.
+ optional &operator=(const optional &rhs) = default;
+
+ /// Rebinds this optional to `u`.
+ template <class U = T,
+ detail::enable_if_t<!detail::is_optional<detail::decay_t<U>>::value>
+ * = nullptr>
+ optional &operator=(U &&u) {
+ static_assert(std::is_lvalue_reference<U>::value, "U must be an lvalue");
+ m_value = std::addressof(u);
+ return *this;
+ }
+
+ /// Converting copy assignment operator.
+ ///
+ /// Rebinds this optional to the referee of `rhs` if there is one. Otherwise
+ /// resets the stored value in `*this`.
+ template <class U> optional &operator=(const optional<U> &rhs) noexcept {
+ m_value = std::addressof(rhs.value());
+ return *this;
+ }
+
+ /// Rebinds this optional to `u`.
+ template <class U = T,
+ detail::enable_if_t<!detail::is_optional<detail::decay_t<U>>::value>
+ * = nullptr>
+ optional &emplace(U &&u) noexcept {
+ return *this = std::forward<U>(u);
+ }
+
+ void swap(optional &rhs) noexcept { std::swap(m_value, rhs.m_value); }
+
+ /// Returns a pointer to the stored value
+ constexpr const T *operator->() const noexcept { return m_value; }
+
+ TL_OPTIONAL_11_CONSTEXPR T *operator->() noexcept { return m_value; }
+
+ /// Returns the stored value
+ TL_OPTIONAL_11_CONSTEXPR T &operator*() noexcept { return *m_value; }
+
+ constexpr const T &operator*() const noexcept { return *m_value; }
+
+ constexpr bool has_value() const noexcept { return m_value != nullptr; }
+
+ constexpr explicit operator bool() const noexcept {
+ return m_value != nullptr;
+ }
+
+ /// Returns the contained value if there is one, otherwise throws bad_optional_access
+ TL_OPTIONAL_11_CONSTEXPR T &value() {
+ if (has_value())
+ return *m_value;
+ throw bad_optional_access();
+ }
+ TL_OPTIONAL_11_CONSTEXPR const T &value() const {
+ if (has_value())
+ return *m_value;
+ throw bad_optional_access();
+ }
+
+ /// Returns the stored value if there is one, otherwise returns `u`
+ template <class U> constexpr T value_or(U &&u) const & noexcept {
+ static_assert(std::is_copy_constructible<T>::value &&
+ std::is_convertible<U &&, T>::value,
+ "T must be copy constructible and convertible from U");
+ return has_value() ? **this : static_cast<T>(std::forward<U>(u));
+ }
+
+ /// \group value_or
+ template <class U> TL_OPTIONAL_11_CONSTEXPR T value_or(U &&u) && noexcept {
+ static_assert(std::is_move_constructible<T>::value &&
+ std::is_convertible<U &&, T>::value,
+ "T must be move constructible and convertible from U");
+ return has_value() ? **this : static_cast<T>(std::forward<U>(u));
+ }
+
+ /// Destroys the stored value if one exists, making the optional empty
+ void reset() noexcept { m_value = nullptr; }
+
+private:
+ T *m_value;
+}; // namespace tl
+
+
+
+} // namespace tl
+
+namespace std {
+// TODO SFINAE
+template <class T> struct hash<tl::optional<T>> {
+ ::std::size_t operator()(const tl::optional<T> &o) const {
+ if (!o.has_value())
+ return 0;
+
+ return std::hash<tl::detail::remove_const_t<T>>()(*o);
+ }
+};
+} // namespace std
+
+#endif