├── .github
├── FUNDING.yml
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── LICENSE
├── README.org
├── consult-mu-embark.el
├── consult-mu.el
├── consult-mu.org
└── extras
├── consult-mu-compose-embark.el
├── consult-mu-compose.el
├── consult-mu-contacts-embark.el
└── consult-mu-contacts.el
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [armindarvish] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Important Information:**
27 | - OS: [e.g. macOS]
28 | - Version of Emacs [e.g. 29] (or other Emacsen you use)
29 | - Version of `mu` (run `mu --version` in a shell)
30 | - Version of `consult` (see [pkg-info](https://github.com/emacsorphanage/pkg-info))
31 | - The installation method and the configuration you are using with your consult-mu.
32 | - If there is an error message, turn debug-on-error on (by `M-x toggle-debug-on-error`) and include the backtrace content in your report.
33 | - If the error only exists when you have some other packages installed, list those packages (e.g. problem happens when evil is installed)
34 |
35 | **Additional context**
36 | Add any other context about the problem here.
37 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled
2 | *.elc
3 |
4 | # Packaging
5 | .cask
6 |
7 | # Backup files
8 | *~
9 |
10 | # Undo-tree save-files
11 | *.~undo-tree
12 |
13 | # test files
14 | tests.org
15 | /tests
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.org:
--------------------------------------------------------------------------------
1 | #+include: ~/OrgFiles/armin/org-macros.setup
2 | #+OPTIONS: h:1 num:nil toc:nil d:nil
3 |
4 | #+TITLE: consult-mu - use consult to search mu4e dynamically or asynchronously
5 | #+AUTHOR: Armin Darvish
6 | #+LANGUAGE: en
7 |
8 | #+html:
9 | #+html:
10 |
11 | * About consult-mu
12 | Consult-mu provides a dynamically updated search interface to mu4e. It uses the awesome package [[https://github.com/minad/consult][consult]] by [[https://github.com/minad][Daniel Mendler]] and [[https://github.com/djcb/mu][mu/mu4e]] by [[https://github.com/djcb/][Dirk-Jan C. Binnema,]] and optionally [[https://github.com/oantolin/embark][Embark]] by [[https://github.com/oantolin][Omar Antolín Camarena]] to improve the search experience of mu4e.
13 |
14 | ** Main Interactive Commands
15 | There two main interactive commands:
16 |
17 | 1. =consult-mu-dynamic=: Provides a dynamic version of =mu4e-search=. As the user types inputs, the result gets updated. This command uses a modified version of =mu4e-search= and then takes the content of =mu4e-headers= buffer to populate minibuffer completion table. This allows the user to change the query or search properties (such as number of results, sort-field, sort-direction, ...) dynamically by changing the input in the minibuffer. In addition previews of the results can be viewed similar to other consult functions. Once a candidate is selected, the user will see the search result sin =mu4e-headers=, and =mu4e-view= buffers similar to mu4e-search results.
18 |
19 | 2. =consult-mu-async=: This function provides a very fast search without loading mu4e-headers, which means mu4e functionalities (like marks, reply, forwards, etc.) are not available in the preview buffer. This is very useful for finding individual emails or threads in a large pool quickly (in other words "a needle in a haystack" scenarios!). Previews can be seen while the results are being populated asynchronously (without populating mu4e-headers buffer). Upon selection of a candidate, mu4e-headers buffer is populated with only an individual email (or thread). From here all the normal functionalities of mu4e is again available.
20 |
21 | 3. =consult-mu=: This is simply a wrapper that calls =consult-mu-default-command=, which can be set to either the dynamic or async command for quick access. In other words, set =consult-mu-default-command= to either 1 or 2 above depending on your use case and then use =consult-mu= for convinience and you don't need to remember the difference between the two anymore.
22 |
23 | The advantage of =consult-mu-async= over =consult-mu-dynamic= is that it is very fast for searching several thousands of messages (even faster than [[https://github.com/emacsmirror/consult-notmuch][consult-notmuch]]!), but cannot populate a mu4e-headers buffer with all the results. On the other hand, =consult-mu-dynamic= is slower when there are thousands of hits for the search term but provides full functionality for all the results (provides full functionality of =mu4e-search=). Therefore, depending on the use case, the user can chose which functions serves the purpose better.
24 |
25 | Furthermore, =consult-mu=, also provides a number of useful [[https://github.com/oantolin/embark][Embark]] actions that can be called from within minibuffer (see examples below). However, when using embark actions, be advised that sometimes you may get an error especially when using =consult-mu-async=. These are likely not critical errors hapening when the databse is out of sync with the results. In such cases syncing the database should resolve the issue.
26 |
27 |
28 | * Getting Started
29 | ** Installation
30 | Before you start, make sure you understand that this is work in progress in its early stage and bugs and breaks are very much expected.
31 |
32 | *note that*: Because [[https://github.com/djcb/mu][mu4e]] tends to take over buffer/windows management, I had to reimplement (a.k.a. hack) some of the functionalities in order to provide quick previews that stay out of your way when the minibuffer command is done (or canceled), and as a result there is a good chance that errors will arise in edge cases that I have not tested.
33 |
34 | *** Requirements
35 | In order to use consult-mu, you need the following requirements:
36 |
37 | **** [[https://github.com/djcb/mu][mu4e]]:
38 |
39 | You can access the official documentation for mu4e here: [[https://www.djcbsoftware.nl/code/mu/mu4e/][mu4e official manual]]. If you need step-by-step instructions or prefer videos, there are many useful tutorials online. Here are a few good links:
40 |
41 | - EmacsWiki: [[https://www.emacswiki.org/emacs/mu4e][EmacsWiki: mu4e]]
42 | - SystemCrafters Videos: [[https://www.youtube.com/watch?v=yZRyEhi4y44][Streamline Your E-mail Management with mu4e - Emacs Mail - YouTube]] & [[https://www.youtube.com/watch?v=olXpfaSnf0o][Managing Multiple Email Accounts with mu4e and mbsync - Emacs Mail - YouTube]]
43 | - Setting Mu4e on MacOS: [[https://macowners.club/posts/email-emacs-mu4e-macos/][Email setup in Emacs with Mu4e on macOS | macOS & (open-source) Software]]
44 |
45 | **** [[https://github.com/minad/consult][consult]]:
46 |
47 | To install consult follow the official instructions here: [[https://github.com/minad/consult#configuration][Configuration of Consult.]]
48 |
49 | Also, make sure you review Consult's README since it recommends some other packages and useful configurations for different settings. Some of those may improve your experience of consult-mu as well. In particular, the section about [[https://github.com/minad/consult#asynchronous-search][asynchronous search]] is important for learning how to use inputs to search for result and narrow down in minibuffer.
50 |
51 |
52 | *** Installing consult-mu package
53 | consult-mu is not currently on [[https://elpa.gnu.org/packages/consult.html][ELPA]] or [[https://melpa.org/#/consult][MELPA]]. Therefore, you need to install it using an alternative non-standard package manager such as [[https://github.com/radian-software/straight.el][straight.el]] or use manual installation.
54 |
55 | **** straight.el
56 | To install consult-mu with straight.el you can use the following command. Make sure you load consult-mu after loading mu4e and consult (e.g. =require consult=, =require mu4e=).
57 |
58 | #+begin_src emacs-lisp
59 | (straight-use-package
60 | '(consult-mu :type git :host github :repo "armindarvish/consult-mu" :branch "main" :files (:defaults "extras/*.el")))
61 | #+end_src
62 |
63 | or if you use =use-package= macro with straight, you can do:
64 |
65 | #+begin_src emacs-lisp
66 | (use-package consult-mu
67 | :straight (consult-mu :type git :host github :repo "armindarvish/consult-mu" :files (:defaults "extras/*.el"))
68 | :after (mu4e consult)
69 | )
70 | #+end_src
71 |
72 | You can also fork this repository and use your own repo.
73 |
74 | **** manual installation
75 | Clone this repo and make sure the files are on your load path, as described on [[https://www.emacswiki.org/emacs/LoadPath][EmacsWiki]].
76 |
77 | Make sure you load consult and mu4e (e.g. =require consult=, =require mu4e=) before you load consult-mu.
78 |
79 | ** Configuration
80 | consult-mu is built with the idea that the user should be able to customize everything based on their use-case, therefore the user is very much expected to configure consult-mu.
81 |
82 | I recommend you read through this section and understand how to configure the package according to your needs and for your specific use-case, but if you just want a drop-in minimal config, look at the snippet below (for snippet with extended settings see [[id:99667231-6F96-4913-834F-7C031E3CC44C][extended feature config]]).
83 |
84 | *** Minimal Config
85 | #+begin_src emacs-lisp
86 | (use-package consult-mu
87 | :straight (consult-mu :type git :host github :repo "armindarvish/consult-mu" :branch "main")
88 | :after (consult mu4e)
89 | :custom
90 | ;;maximum number of results shown in minibuffer
91 | (consult-mu-maxnum 200)
92 | ;;show preview when pressing any keys
93 | (consult-mu-preview-key 'any)
94 | ;;do not mark email as read when previewed
95 | (consult-mu-mark-previewed-as-read nil)
96 | ;;do not amrk email as read when selected. This is a good starting point to ensure you would not miss important emails marked as read by mistake especially when trying this package out. Later you can change this to t.
97 | (consult-mu-mark-viewed-as-read nil)
98 | ;; open the message in mu4e-view-buffer when selected.
99 | (consult-mu-action #'consult-mu--view-action)
100 | )
101 | #+end_src
102 |
103 | *** Extended Feature Config
104 | :PROPERTIES:
105 | :ID: 99667231-6F96-4913-834F-7C031E3CC44C
106 | :END:
107 |
108 | Here is a customization that gives you the full feature experience including utilities for attaching/detaching files and searching contacts, etc.
109 |
110 | #+begin_src emacs-lisp
111 | (use-package consult-mu
112 | :straight (consult-mu :type git :host github :repo "armindarvish/consult-mu" :branch "develop" :files (:defaults "extras/*.el"))
113 | :after (consult mu4e)
114 | :custom
115 | ;;maximum number of results shown in minibuffer
116 | (consult-mu-maxnum 200)
117 | ;;show preview when pressing any keys
118 | (consult-mu-preview-key 'any)
119 | ;;do not mark email as read when previewed. If you turn this to t, be aware that the auto-loaded preview if the preview-key above is 'any would also get marked as read!
120 | (consult-mu-mark-previewed-as-read nil)
121 | ;;mark email as read when selected.
122 | (consult-mu-mark-viewed-as-read t)
123 | ;;use reply to all when composing reply emails
124 | (consult-mu-use-wide-reply t)
125 | ;; define a template for headers view in minibuffer. The example below adjusts the width based on the width of the screen.
126 | (consult-mu-headers-template (lambda () (concat "%f" (number-to-string (floor (* (frame-width) 0.15))) "%s" (number-to-string (floor (* (frame-width) 0.5))) "%d13" "%g" "%x")))
127 |
128 | :config
129 | ;;create a list of saved searches for quick access using `histroy-next-element' with `M-n' in minibuffer. Note the "#" character at the beginning of each query! Change these according to
130 | (setq consult-mu-saved-searches-dynamics '("#flag:unread"))
131 | (setq consult-mu-saved-searches-async '("#flag:unread"))
132 | ;; require embark actions for marking, replying, forwarding, etc. directly from minibuffer
133 | (require 'consult-mu-embark)
134 | ;; require extra module for composing (e.g. for interactive attachment) as well as embark actions
135 | (require 'consult-mu-compose)
136 | (require 'consult-mu-compose-embark)
137 | ;; require extra module for searching contacts and runing embark actions on contacts
138 | (require 'consult-mu-contacts)
139 | (require 'consult-mu-contacts-embark)
140 | ;; change the prefiew key for compose so you don't open a preview of every file when selecting files to attach
141 | (setq consult-mu-compose-preview-key "M-o")
142 | ;; pick a key to bind to consult-mu-compose-attach in embark-file-map
143 | (setq consult-mu-embark-attach-file-key "C-a")
144 | (setq consult-mu-contacts-ignore-list '("^.*no.*reply.*"))
145 | (setq consult-mu-contacts-ignore-case-fold-search t)
146 | (consult-mu-compose-embark-bind-attach-file-key)
147 | ;; choose if you want to use dired for attaching files (choice of 'always, 'in-dired, or nil)
148 | (setq consult-mu-compose-use-dired-attachment 'in-dired)
149 | )
150 |
151 | #+end_src
152 |
153 | *** Customization Variables
154 | The following customizable variables are provided:
155 |
156 | **** main
157 | ***** =consult-mu-default-command=
158 | A command function that is called when =M-x consult-mu= is called. This is useful for defining special conditions to use =consult-mu-dynamics= or =consult-mu-async=. By default it is bound to =consult-mu-dynamics=, therefore =M-x consult-mu= simply calls =consult-mu-dynamics=.
159 |
160 | ***** =consult-mu-headers-buffer-name=
161 | This is the default name for HEADERS buffer explicitly for consult-mu. It is, by default, set to ="**consult-mu-headers**"=. Note that currently, the header-buffer name is a constant string and shared between all instances of consult-mu calls. This is to prevent running operations in parallel that cause out-of-sync issues.
162 |
163 | ***** =consult-mu-view-buffer-name=
164 | This is the default name for VIEW buffer explicitly for consult-mu. It is, by default, set to ="**consult-mu-view**"=. Note that currently, the view-buffer name is a constant string and shared between all instances of consult-mu calls. This is to prevent creating too many preview buffers and executing operations (such as marking) in parallel that cause out-of-sync issues.
165 |
166 | ***** =consult-mu-args=
167 | This is the default name of the =mu= command line argument. It is set to ="mu"= by default, but can be modified for example if mu is at a different path on your system.
168 |
169 | ***** =consult-mu-maxnum=
170 | Maximum number of messages shown in search results (in consult-mu minibuffer completion table). This is a global option for consult-mu and consult-mu-async, but can be overriden by providing command line arguments in input for example, the following search would fetch up to 1000 results
171 |
172 | #+begin_example
173 | #github -- --maxnum 1000
174 | #+end_example
175 |
176 | ***** =consult-mu-headers-fields=
177 | This variable is used to format the headers inside minibuffer. This takes a similar format to =mu4e-headers-fields= and changes the format of the header only in minibuffer (=consult-mu-dynamic= or =consult-mu-async=).
178 |
179 | Note that it is generally recommended to use =consult-mu-headers-template= below because it has more options and does not affect the consult-mu-headers buffer.
180 |
181 | ***** =consult-mu-headers-template=
182 | :PROPERTIES:
183 | :ID: 155CF359-63D0-4D4D-B0BD-7C089E2D9B3C
184 | :END:
185 | This is a template string that overrides the consult-mu-headers-field to format headers. It provides more options than =consult-mu-headers-field= and is generally the recommended approach to make custom headers.
186 | Special chaacters in the string (either “%[char]" or “%[char][integer]”) in the string get expanded to create headers. Each character represents a different field and the integer defines the length of the field. For exmaple "%d15%s50" means 15 characters for date and 50 charcters for subject.
187 |
188 | The list of available fields are:
189 |
190 | %f sender(s) (e.g. from: field of email)
191 | %t receivers(s) (i.e. to: field of email)
192 | %s subject (i.e. title of email)
193 | %d date (i.e. the date email was sent/received) with the format "Thu 09 Nov 23"
194 | %p priority
195 | %z size
196 | %i message-id (as defined by mu)
197 | %g flags (as defined by mu)
198 | %G pretty flags (this uses mu4e~headers-flags-str to pretify flags)
199 | %x tags (as defined by mu)
200 | %c cc (i.e. cc: field of the email)
201 | %h bcc (i.e. bcc: field of the email)
202 | %r date chaged (as defined by :changed in mu4e)
203 |
204 | For example, the string ="%d13%s50%f17%G"= would make a header containing =13= characters for =date=, =50= characters for =subject=, and =20= characters for =from= field, making headers that looks like this:
205 | #+attr_org: :width 800px :height nilpx
206 | #+attr_latex: :width 800px :height nilpx
207 | #+attr_html: :width 800px :height nilpx
208 | [[https://github.com/armindarvish/consult-mu/blob/screenshots/screenshots/consult-mu-headers-template.png]]
209 |
210 | ***** =consult-mu-search-sort-field=
211 | This defines the field that is used for sorting the results (refer to documentation on the variable =mu4e-search-sort-field= for more info). It has to be one of the keywords:
212 | - =:date= sort by date
213 | - =:subject= sort by title of the email
214 | - =:size= sort by file size
215 | - =:prio= sort by priority
216 | - =:from= sort by name/email of the sender(s)
217 | - =:to= sort by name/email of receivers
218 | - =:list= sort by mailing list
219 |
220 |
221 | Note that the sort field can dynamically be changed by providing command line arguments in the minibuffer input.
222 |
223 | For example the following input in the minibuffer will search for emails that are flagged unread but then overrides the sort field and change it t =subject=. For details on how to use command line arguments refer to mu manual (e.g. by running =mu find --help= in the command line)
224 |
225 | #+begin_example
226 | #flag:unread -- -s s
227 | #+end_example
228 |
229 | ***** =consult-mu-search-sort-direction=
230 | Direction of sort. It can either be ='ascending= for A->Z (low number to high number) or ='descending= for Z->A (high number to low number). Note that if a command line argument for reverse order (either -z or --reverse) is provided in the minibuffer, the order will be reverse of the setting defined by this variable.
231 |
232 | For example, if =consult-mu-search-sort-field= is set to =:date= and =consult-mu-search-sort-direction= is set to ='descending= the messages are sortes chronologically from the newest on top to the oldest. Then providing a reverse order argument in the minibuffer can dynamically reverse the sort direction:
233 |
234 | For example the following input in the minibuffer searches for all =unread= emails under ="./inbox"= path then *reverses the sort direction* (because of =-z= command line argument)
235 | #+begin_example
236 | #(maildir:/inbox) AND flag:unread -- -z
237 | #+end_example
238 |
239 |
240 | ***** =consult-mu-search-threads=
241 | This variable determines whether threads are calculated for search results or not similar to the =mu4e-search-threads= variable.
242 |
243 | Note that per mu4e docs:
244 | When threading is enabled, the headers are exclusively sorted chronologically (:date) by the newest message in the thread.
245 |
246 | When this variable is set to nil, it can still be truned on by adding command line arguments (i.e. =-t= or =--thread=) in the input. For example the following input in the minibuffer will ensure that threads are on even if =consult-mu-search-threads= is set to nil.
247 |
248 | #+begin_example
249 | #flag:unread -- -t
250 | #+end_example
251 |
252 | ***** =consult-mu-group-by=
253 | This variable determines what field is used to group messages. This aloows quick movement between groups (For example with =vertico-next-group= if you use [[https://github.com/minad/vertico][vertico]])
254 |
255 | By default it is set to :date. But can be any of the following keywords:
256 |
257 | - =:subject= group by mail title
258 | - =:from= group by name/email of the sender(s)
259 | - =:to= group by name/email of the receiver(s)
260 | - =:date= group by date in the format "Thu 09 Nov 23"
261 | - =:time= group by the time of email in the format "20:30:07"
262 | - =:datetime= group by date and time of the email with the format "2023-11-04 08:30:07 PM"
263 | - =:year= group by the year of the email (i.e. 2023, 2022, ...)
264 | - =:month= group by the month of the email (i.e. Jan, Feb, ..., Dec)
265 | - =:week= group by the week number of the email (.i.e. 1, 2, 3, ..., 52)
266 | - =:day-of-week= group by the day email was sent (i.e. Monday, Tuesday, ...)
267 | - =:size= group by the file size of the email
268 | - =:flags= group by flags (as defined by mu)
269 | - =:tags= group by tags (as defined by mu)
270 | - =:changed= group by the date changed (as defined by :changed field in mu4e)
271 |
272 | Note that grouping works alongside sorting. For example if =consult-mu-group-by= is set to =:day-of-week= and =consult-mu-search-sort-field= is set to =:date=, then the messages are grouped by day of week (e.g. all emails on Tuesdays will be in one group) then ordered chronologically within each group. The screenshot below shows some examples:
273 | #+attr_org: :width 800px :height nilpx
274 | #+attr_latex: :width 800px :height nilpx
275 | #+attr_html: :width 800px :height nilpx
276 | [[https://github.com/armindarvish/consult-mu/blob/screenshots/screenshots/consult-mu-grouping-example.png]]
277 |
278 | ***** =consult-mu-mark-previewed-as-read=
279 | This determines whether a message is marked as =read= when it is simply previewed (see =consult-mu-preview-key= above and documentation on =consult-preview-key=).
280 |
281 | Note that when =consult-mu-preview-key= is set to ='any=, then as soon as =consult-mu= or =consult-mu-async= retrieve some results a preview for the first message is shown and this can be marked as read if =consult-mu-mark-previewed-as-read= is set to t.
282 |
283 | ***** =consult-mu-mark-viewed-as-read=
284 | This determines whether a message is marked as =read= when it is viewed (i.e. when the minibuffer candidate is selected by hitting =RET=).
285 |
286 | ***** =consult-mu-preview-key=
287 | This is similar to =consult-preview-key= but only for =consult-mu=. By default, it is set to the value of consult-preview-key to keep consistent experience across different consult packages, but you can set this variable explicitly for consult-mu.
288 |
289 | The recommended option is to set this to ='any= so as you naviagte over the candidates you see previews updated.
290 |
291 | #+begin_src emacs-lisp
292 | (setq consult-mu-preview-key 'any)
293 | #+end_src
294 |
295 | If the option above slows down your system, and you only want to load previews on demand, then you can set it to a specific key such as ="M-o"=.
296 |
297 | #+begin_src emacs-lisp
298 | (setq consult-mu-preview-key "M-o")
299 | #+end_src
300 |
301 | You can also turn previews off by setting this variable to =nil=, but this is not generally recommended.
302 |
303 |
304 | ***** =consult-mu-highlight-matches=
305 | This variable determines if consult-mu highlights search queries in minibuffer or preview buffers. By default it is set to t.
306 |
307 | When it is set to t, all matches of the search term are highlighted in the minibuffer allowing you to notice why the email is a search hit.
308 |
309 | For example in the screenshot below, I am searching for the term =consult= and all the matches of consult are highlighted in the titles.
310 | #+attr_org: :width 800px :height nilpx
311 | #+attr_latex: :width 800px :height nilpx
312 | #+attr_html: :width 800px :height nilpx
313 | [[https://github.com/armindarvish/consult-mu/blob/screenshots/screenshots/consult-mu-highlight-matches-minibuffer.png]]
314 |
315 |
316 | Furthermore if I look at a preview, all the instances of consult-gh matches in the preview buffer are also highlighted:
317 | #+attr_org: :width 800px :height nilpx
318 | #+attr_latex: :width 800px :height nilpx
319 | #+attr_html: :width 800px :height nilpx
320 | [[https://github.com/armindarvish/consult-mu/blob/screenshots/screenshots/consult-mu-highlight-matches-preview.png]]
321 |
322 | Note that when the candidate is selected (i.e. by pressing =RET=), the highlight overlay is turned off, so you can see the orignial message as is, but you can call =consult-mu-overlays-toggle= (i.e. =M-x consult-mu-overlays-toggle=) to see the highlights of the query again.
323 | #+attr_org: :width 800px :height nilpx
324 | #+attr_latex: :width 800px :height nilpx
325 | #+attr_html: :width 800px :height nilpx
326 | [[https://github.com/armindarvish/consult-mu/blob/screenshots/screenshots/consult-mu-highlight-matches-view.png]]
327 |
328 | ***** =consult-mu-action=
329 | This variable stores the function that is called when a message is selected (i.e. =RET= is pressed in the minibuffer). By default it is bound to =consult-mu--view-action= which opens both the headers buffer and view buffer and shows the content of the selected message.
330 |
331 | **** compose
332 | These variables are only available after loading the compose module;
333 | #+begin_src emacs-lisp
334 | (require 'consult-mu-compose)
335 | #+end_src
336 |
337 | ***** =consult-mu-compose-use-dired-attachment=
338 | This variable defines, whether =consult-mu= uses dired buffers for selecting files to attach. If it is set to ='always=, consult-mu will always jump to a dired buffer for selecting files to attach. It it is set to ='in-dired=, consult-mu only uses dired (a.k.a. file at point or marked files) for attaching files when already inside a dired buffer. If it is set to =nil=, consult-mu uses minibuffer completion for file selection for attachments.
339 | ***** =consult-mu-large-file-warning-threshold=
340 | A threshold to make sure very large files are not accidentally opened when previewing files that the user wants to attach to an email. If file is larger than this threshold, the user is asked to confirm before loading the file buffer.
341 |
342 | ***** =consult-mu-compose-preview-key=
343 | This is similar to =consult-mu-preview-key= but only for =consult-mu-compose=. This is used to preview files when selecting files for attachments. By default, it is set to the follow the value of =consult-mu-preview-key= to keep consistent experience across the package. *But it is recommended to change this to something other than ='any= becuase otherweise every file is previewd as the user is navigating through folders to select files to attach to an email.
344 |
345 | #+begin_src emacs-lisp
346 | (setq consult-mu-compose-preview-key "M-o")
347 | #+end_src
348 |
349 | ***** =consult-mu-embark-attach-file-key=
350 | This variable defines a key that can be bound to =consult-mu-compose-attach= in =emabrk-file-map=, therefore one can call embark on any file and use this key to attach it to an email. This can be bound to "a" or "C-a":
351 |
352 | #+begin_src emacs-lisp
353 | (setq consult-mu-embark-attach-file-key "C-a")
354 | #+end_src
355 |
356 | Running the function =consult-mu-compose-embark-bind-attach-file-key= binds this key, but any other key can be passed to this function as well to override the customization variable:
357 |
358 | #+begin_src emacs-lisp
359 | (consult-mu-compose-embark-bind-attach-file-key)
360 | #+end_src
361 |
362 | **** contacts
363 | These variables are only available after loading the contacts module;
364 | #+begin_src emacs-lisp
365 | (require 'consult-mu-contacts)
366 | #+end_src
367 |
368 | ***** =consult-mu-contacts-group-by=
369 | This variable determines what field is used to group contacts, which allows quick movement between groups (For example with =vertico-next-group= if you use [[https://github.com/minad/vertico][vertico]])
370 |
371 | By default it is set to name. But can be any of the following keywords:
372 |
373 | - =:name= group by contact name
374 | - =:email= group by email of the contact
375 | - =:domain= group by the domain of the contact's email (e.g. domain.com in user@domain.com)
376 | - =:user= group by the ncontact's user name (e.g. user in user@domain.com)
377 |
378 | For example if =consult-mu-contacts-group-by= is set to =:domain= then the domain of the email address is used to group contacts. This is useful for example if you are looking for all emails from a specific company!
379 |
380 | ***** =consult-mu-contacts-action=
381 | This variable stores the function that is called when a contact is selected (i.e. =RET= is pressed in the minibuffer). By default it is bound to =consult-mu-contacts--list-messages-action= which searches for all the messages from that contact by calling =consult-mu=. You can also set this action to other functions such =consult-mu-contacts--insert-email-action= or =consult-mu-contacts--copy-email-action= for inserting or copying the email from contact.
382 |
383 | Note that the default action for =consult-mu-contacts-embark= is inserting the email. This is useful for quickly adding contacts when composing messages. You cna also use =embark-collect= or =embark-export= to select multiple contacts and act on all of them (e.g. insert all in "To:" field in a compose buffer).
384 |
385 | ***** =consult-mu-contacts-ignore-list=
386 | This is a list of rgexp that gets ignored when searching contacts.
387 | This is useful to filter certain invalid addreses (e.g. "no-reply" addresses from contacts). For example you can remove no-reply adresses by setting this variable as follows;
388 | #+begin_src emacs-lisp
389 | (setq consult-mu-contacts-ignore-list '("^.*no.*reply.*$"))
390 |
391 | #+end_src
392 |
393 | ***** =consult-mu-contacts-ignore-case-fold-search=
394 | This variable defines whether =consult-mu-contacts= uses case insensitive search when matching against =consult-mu-contacts-ignore-list= (see above). if you set this to =t=, it will preform case *in*sensitive match.
395 |
396 | * Features and Demos
397 | For a detailed description of why this package is useful, and some useful screenshosts showing work flows, you can read my blog post here:
398 |
399 | [[https://www.armindarvish.com/post/improve_your_mu4e_workflow_with_consult-mu/]]
400 |
401 | ** Search
402 | consult-mu uses [[https://github.com/minad/consult#asynchronous-search][consult's asynchronous search]] feature. In order to search a query you can type your standard Mu4e query after =#= sign in the minibuffer. Here are some examples:
403 |
404 | #+begin_example
405 | #tickets
406 | #flag:unread
407 | #(flag:unread AND maildir:/inbox)
408 | #(maildir:/drafts OR maildir:/sent)
409 | #+end_example
410 |
411 |
412 | In addition, you can pass command line arguments that you can normally pass to =mu= (see =mu find --help=) in the command line to consult-mu by using a =--= separator like this:
413 |
414 | #+begin_example
415 | #tickets -- --maxnum 500 -s s
416 | #+end_example
417 |
418 | In the example above, the maximum number of results to retrieve are set to 500 (=--maxnum 500=) and the result are sorted by the subject (=-s s=). For more details on command line options, you can refer to mu's help (i.e. run =mu find --help= in terminal).
419 |
420 | Once consult-mu retrieves a list of results, you can further narrow down the list of candidates by using a second =#=.
421 |
422 | #+begin_example
423 | #tickets -- -z #concert
424 | #+end_example
425 |
426 | In the example above, we first run a search for *tickets* and sort the results with reverse order (using =-z= option) then narrow down the list of candidates by the word, *concert*.
427 |
428 | Note that narrow downs are similar to other narrow downs in the minibuffer, therefore here you cannot use mu4e search syntax but you can use regular expressions. Also, the narrow down searches in the entire header string, therefore you can use any information available in the headers (date, title, email addresses, flags,...) for narrow down. (to change the format of the header, take a look at [[id:155CF359-63D0-4D4D-B0BD-7C089E2D9B3C][=consult-mu-headers-template=]]. Here is a screenshot for doing a search and narrow:
429 |
430 | #+ATTR_ORG: :width 800px
431 | #+ATTR_LATEX: :width 800px
432 | #+ATTR_HTML: :width 800px
433 | [[https://github.com/armindarvish/consult-mu/blob/screenshots/screenshots/consult-mu-search-narrow.gif]]
434 |
435 | ** Saved Searches and Quick Access History
436 | consult-mu does have a history variable and keeps record of your previous searches. These searches can quickly be accessed using =previous-history-element= (bound to =M-p= by default). In addition, you can have a list of saved searches for quick access by modifying the variable =consult-mu-saved-searches-dynamic= or =consult-mu-saved-searches-async= for =consult-mu-dynamic= and =consult-mu-async=, respectively. These are lists of mu4e query strings that get added to future-history when you run consult-mu-dynamic or consult-mu-async. By default, =consult-mu-saved-searches-async= is set to inherit from =consult-mu-saved-searches-dynamic=, but you can modify them independently. This allows separating saved searches that are more suitable for async search from those that are better done with dynamic collection.
437 |
438 | Similarly =consult-mu-compose-attach=, =consult-mu-compose-detach=, and =consult-mu-contacts= have their own history elements as wels future history elemetns (for example email in the current message buffer gets added to future-history for searching contacts).
439 |
440 | ** Integration with Embark
441 | consult-mu provides integration with [[https://github.com/oantolin/embark][embark]] defined in =consult-mu-embark.el= If you use embark, you can use these commands to run actions on the search results directly from minibuffer. For example, you can call =consult-mu-embark-reply= to reply to a message or =consult-mu-embark-mark-for-refile= to refile (a.k.a. archive) the message and so on.
442 |
443 | For marking, note that, by default when consult-mu-embark is loaded, it creates a function for every item in the =mu4e-marks= and binds them to the =:char= for that mark in =consult-mu-embark-messages-actions-map=.
444 |
445 | You can also use =embark-select= and =embark-act-all= to run the same command on multiple messages, but this only works in consult-mu-dynamic and not so well with consult-mu-async because with consult-my-async, only one message at a time is added to the headers view and therefore running commands on multiple candidates with embark is not supported.
446 |
447 | ** Extras (attachments, contacts, ...)
448 | In addition to improvements for searching, consult-mu also provides extra features for composing, contacts, ... These features are defined in the files under the =extras= folder.
449 | *** Attachments
450 | By default, attaching files to emails with mu4e is not very intuitive or interactive. If you use [[https://github.com/jeremy-compostella/org-msg][org-msg]], it provides an interactive command for attaching files, but it is only for one file at a time and for each file you need to go through some menus. Some emacs configs, like [[https://github.com/doomemacs/doomemacs][doomemacs]], do provide their own commands to allow using a dired buffer to attach multiple files to an email (see [[https://github.com/doomemacs/doomemacs/blob/986398504d09e585c7d1a8d73a6394024fe6f164/modules/email/mu4e/autoload/email.el#L272C8-L272C26][+mu4e/attach-files]]). Personally, I prefer using a minibuffer completion (instead of switching to a dired buffer) unless I am already in a dired buffer. The [[file:extras/consult-mu-compose.el][consult-mu-compose]] provides the utilities and customization settings to achieve interactive multi-file attachment.
451 |
452 | To use attachment extras, you need to load the compose module by:
453 | #+begin_src emacs-lisp
454 | (require 'consult-mu-compose)
455 | (require 'consult-mu-compose-embark) ;;optionally load embark actions for compose
456 | #+end_src
457 |
458 | It is highly recommended that you also change the preview key binding for =consult-mu-compose= to something other than ='any= because seeing a preview of every file when you are navigating through the minibuffer may be slow and annoying. It's easier to get a preview of the files you are interested in with a key binding like =M-o= than to see a preview of every file.
459 |
460 | #+begin_src emacs-lisp
461 | (setq consult-mu-compose-preview-key "M-o")
462 | #+end_src
463 | **** Attaching Files
464 | =consult-mu-compose-attach= provides an interactive command to attach files to an email and it tries its best to provide an intuitive interface.
465 |
466 | Here are some features:
467 |
468 | - interactively selecitng draft compose buffer:
469 | In a compose buffer (e.g. =mu4e-compose-mode=, =message-mode=, =org-msg-edit-mode=), it assumes that the user wants to attach files to the current message. In other buffers it asks the user to select a current compose buffer or creates a new one if noe exists.
470 |
471 | - interactively selecting files:
472 | For selecting files, it can either use minibuffer completion or a dired buffer. If the customization variable =consult-mu-compose-use-dired-attachment= is set to =always=, it always uses dired buffer similar to what dom emacs does. If it is set to ='in-dired= it only uses dired when inside a dired-mode buffer. and if it is set to =nil=, it will always use minibuffer completion.
473 | When using dired to chose files, one can mark multiple files to attach to a message. Furthermore, if the command =consult-mu-compose-attach= is called from within a compose buffer (e.g. =mu4e-compose-mode=, =message-mode=, =org-msg-edit-mode=), runing =embrak-dwim= on the file will attach the file to the current message. This is specially useful if you use =embark-dwim= with =no-quit= option ([[https://github.com/oantolin/embark#quitting-the-minibuffer-after-an-action][embark#quitting-the-minibuffer-after-an-action]]). Here is an example on how to define =embark-dwim-noquit=:
474 |
475 | #+begin_src emacs-lisp
476 | (defun embark-dwim-noquit ()
477 | "Run action but don't quit the minibuffer afterwards."
478 | (interactive)
479 | (let ((embark-quit-after-action nil))
480 | (embark-dwim)))
481 | #+end_src
482 | Then you can just attach multiple files to the same message from any folder by using the minibuffer completion. See the screenshot in my blog post here: https://www.armindarvish.com/post/improve_your_mu4e_workflow_with_consult-mu/
483 |
484 | **** Removing attachments:
485 | Similarly, =consult-mu-compose-detach= provides an interactive command to remove files that are already attached to the current message. It uses minibuffer completion to select a file to remove. Similar to what mentioned above, embark-dwim (or a no-quit version of it) can be used to remove multiple files from the same message. note that the minibuffer completion list does not get dynamically updated when a file is removed (This is something I may improve in the future but for now it works just fine I think).
486 | See the screenshot in my blog post here: https://www.armindarvish.com/post/improve_your_mu4e_workflow_with_consult-mu/
487 |
488 |
489 | *** Contacts
490 | =consult-mu-contacts= provides an interactive way to search mu contacs. By default, when a candidate is selected, all the emails form the contact is searched by using =consult-mu=. This can be customized to oyher functions (e.g. compose an email to the contact) by seeting =consult-mu-contacts-action=. Note that mu does not have a stored list of contacts. Rather, contacts are generated dynamically from the email fields in all messages in the database, including email fields that might be invalid! To use =consult-mu-contacts=, you need to load the contacts module by:
491 |
492 | #+begin_src emacs-lisp
493 | (require 'consult-mu-contacts)
494 | (require 'consult-mu-contacts-embark) ;;optionally load embark actions for contacts
495 | #+end_src
496 |
497 | See the screenshot in my blog post here: https://www.armindarvish.com/post/improve_your_mu4e_workflow_with_consult-mu/
498 |
499 |
500 | * Bug reports
501 | To report bug, first check if it is already reported in the [[https://github.com/armindarvish/consult-mu/issues][*issue tracker*]] and see if there is an existing solution or add relevant comments and discussion under the same issue. If not file a new issue following these steps:
502 |
503 | 1. Make sure the dependencies are installed, and both =mu4e= and =consult= work as expected.
504 |
505 | 3. Remove the package and install the latest version (along with dependencies) and see if the issue persists.
506 |
507 | 4. In a bare bone vanilla Emacs (>=28) (e.g. =emacs -Q=), install the latest version of consult-mu (and its dependencies) without any configuration or other packages and see if the issue still persists.
508 |
509 | 5. File an issue and provide important information and context in as much detail as possible in your bug report. Important information can include:
510 | - Your operating system, version of Emacs (or the version of emacsen you are using), version of mu/mu4e and consult (see [[https://github.com/emacsorphanage/pkg-info][pkg-info]]).
511 | - The installation method and the configuration you are using with your consult-mu.
512 | - If there is an error message, turn debug-on-error on (by =M-x toggle-debug-on-error=) and include the backtrace content in your report.
513 | - If the error only exists when you have some other packages installed, list those packages (e.g. problem happens when evil is installed)
514 | - It would be useful, if you can look at consult-mu buffers while the minibuffer command is active (by default they are named =*consult-mu-headers*= and =*consult-mu-view*= buffers) and report whether theya re getting populated properly or not.
515 |
516 | * Contributions
517 | This is an open source package, and I appreciate feedback, suggestions, ideas, etc. There are lots of functionalities that can be added to this package to improve different user's workflows, so if you have some ideas, feel free to file an issue for a feature request.
518 |
519 | If you want to contribute to the code, please note that the main branch is currently stable (as stable as a work in progress like this can be) and the develop branch is the current work in progress. So, *start from the develop branch* to get the latest work-in-progress updates and create a new branch with names such as feature/name-of-the-feature or fix/issue, ... Do the edits and then create a new pull request to merge back with the *develop* branch when you are done with your edits.
520 |
521 | Importantly, keep in mind that I am using a *literate programming approach* (given that this is a small project with very limited number of files) where everything goes into *consult-mu.org* and then gets tangled to appropriate files (for now that includes consult-mu.el and consult-mu-embark.el). If you open a pull-request where you directly edited the .el files, I will likely not approve it because that will then get overwritten later when I tangle from the .org file. In other words, *Do Not Edit The .el Files!* only edit the .org file and tangle to .el files.
522 |
523 |
524 | * What about other packages? Why we need a new package such as consult-mu?
525 | While mu4e's built-in search is great and provides ways to edit search terms (e.g. =mu4e-search-edit=) or toggle search properties (e.g. =mu4e-search-toggle-property=), the interface is not really intuitive. We are simply too impatient in 2024 to use a static search field and edit in steps. A dynamically updated search results is somewhat expected with any modern tools. Try searching in Thunderbird, or Outlook, and you instantly see suggestions and results. This is what consult-mu provides. Hopefully, in the future a dynamics search approach comes built-in with mu4e but until then, this package intends to fill the gap.
526 |
527 | *** What about alternative approaches like [[https://github.com/seanfarley/counsel-mu][counsel-mu]] or [[https://github.com/emacsmirror/consult-notmuch][consult-notmuch]]?
528 |
529 | You can read my blog post for some thoughts on this:
530 | https://www.armindarvish.com/post/improve_your_mu4e_workflow_with_consult-mu/
531 |
532 |
533 | Both [[https://github.com/seanfarley/counsel-mu][counsel-mu]] and [[https://github.com/emacsmirror/consult-notmuch][consult-notmuch]] inspired this package. But this package provides soemthing more than those packages. Here is the comparison:
534 |
535 | - [[https://github.com/seanfarley/counsel-mu][counsel-mu]]: counsel-mu provides an async search for mu, and when the candidate is selected, the single message is loaded by using =mu4e-view-message-with-message-id=. =consult-mu= takes a similar approach in =consult-mu-async= but expands on that with the following features:
536 |
537 | a. ability to dynamically add command line options (e.g. using =query -- -s s -z= as input).
538 | This includes dynamically changing the number of results. This is important because with counsel-mu, one ha to change the variable =mu4e-search-results-limit= globally to see many results, which then affects every mu4e-search that is done.
539 |
540 | b. ability to load a preview without leaving minibuffer search
541 |
542 | c. ability to use emabrk actions on candidates from the minibuffer with =consult-mu-embark=
543 |
544 | d. ability to see whole threads when selecting a candidate rather than just single messages
545 |
546 | In addition, =consult-mu= provides the =consult-mu-dynamic= interactive command that uses dynamic collection using =mu4e-search= (and not =mu= commands). This allows doing dynamic search in the minibuffer and then getting a full list of results similar to the built-in mu4e-search.
547 |
548 |
549 | - [[https://github.com/emacsmirror/consult-notmuch][consult-notmuch]]: consult-notmuch is great IF you use [[https://notmuchmail.org/notmuch-emacs/][notmuch-emacs]]. consult-mu on the other hand provides similar functionality for [[https://github.com/djcb/mu][mu/mu4e]].
550 | The comparison therefore comes down to comparing mu4e and notmuch. While notmuch is light and fast, I think it lacks lots of basic functionalities of an email client. It is supposed to be *not much* after all! As a result, using notmuch as an everyday email client can be challenging especially if you want to use it along with other IMAP-based email clients (e.g. mobile apps). This is because the philosophy of using tags instead of folders requires a complete redesign of some workflows. While some services, like Gmail, use labels, others may not and even if they do, in IMAP-based clients labels are treated as different folders and therefore syncing back custom notmuch labels everywhere (e.g. between notmuch on your desktop machine and your mobile app client) becomes tricky. Of course, in Emacs nothing is impossible and there are ways to improve the experience by adding additional custom elisp (see [[https://www.youtube.com/watch?v=g7iF11qamh8][Emacs: Notmuch demo (notmuch.el) - YouTube]] and [[https://www.reddit.com/r/emacs/comments/qo3eza/notmuch_as_an_alternative_to_mu4e/?share_id=8WUviRO3gGGIO4bQgoSzU&utm_content=2&utm_medium=android_app&utm_name=androidcss&utm_source=share&utm_term=10][Notmuch as an alternative to mu4e : emacs]] for some examples), but at the end, the results will still likely lack some important features (like multi-account contexts, ...).
551 |
552 | More importantly, a common reason to choose notmuch over mu4e is its speed, but using the underlying =mu= server and command line can also be very fast. In fact, at least in my own tests, =consult-mu-async=, which uses the command line =mu= commands was faster than =consult-notmuch=. With the latest release of mu4e (version 1.12), even =consult-mu-dynamics= (a.k.a. built-in mu4e-search) is now very fast. Therefore, using =mu4e= with =consult-mu= provides the best of both worlds to me. When I need speed and simplicity, I can use =consult-mu-async= to do a fast search (and find thousands of hits) and quickly narrow down to what I am looking for; and when I need a more complete and full-feature client I can use =mu4e= built-in functionalities, and with addition of dynamically built searches with =consult-mu-dynamic=, I have a modern intuitive interface as well.
553 |
554 | * Acknowledgments
555 | Obviously this package would not have been possible without the fabulous [[https://github.com/djcb/mu][mu/mu4e]], and [[https://github.com/minad/consult][consult]] packages. It also took inspiration from other packages including but not limited to [[https://github.com/seanfarley/counsel-mu][counsel-mu]], [[https://github.com/emacsmirror/consult-notmuch][consult-notmuch]], and [[https://github.com/doomemacs/doomemacs][doomemacs]].
556 |
--------------------------------------------------------------------------------
/consult-mu-embark.el:
--------------------------------------------------------------------------------
1 | ;;; consult-mu-embark.el --- Emabrk Actions for consult-mu -*- lexical-binding: t -*-
2 |
3 | ;; Copyright (C) 2021-2023
4 |
5 | ;; Author: Armin Darvish
6 | ;; Maintainer: Armin Darvish
7 | ;; Created: 2023
8 | ;; Version: 1.0
9 | ;; Package-Requires: ((emacs "28.0") (consult "2.0"))
10 | ;; Homepage: https://github.com/armindarvish/consult-mu
11 | ;; Keywords: convenience, matching, tools, email
12 | ;; Homepage: https://github.com/armindarvish/consult-mu
13 |
14 | ;; SPDX-License-Identifier: GPL-3.0-or-later
15 |
16 | ;; This file is free software: you can redistribute it and/or modify
17 | ;; it under the terms of the GNU General Public License as published
18 | ;; by the Free Software Foundation, either version 3 of the License,
19 | ;; or (at your option) any later version.
20 | ;;
21 | ;; This file is distributed in the hope that it will be useful,
22 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
23 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 | ;; GNU General Public License for more details.
25 | ;;
26 | ;; You should have received a copy of the GNU General Public License
27 | ;; along with this file. If not, see .
28 |
29 |
30 | ;;; Commentary:
31 |
32 | ;; This package provides an alternative interactive serach interface for
33 | ;; mu and mu4e (see URL `https://djcbsoftware.nl/code/mu/mu4e.html').
34 | ;; It uses a consult-based minibuffer completion for searching and
35 | ;; selecting, and marking emails, as well as additional utilities for
36 | ;; composing emails and more.
37 |
38 | ;; This package requires mu4e version "1.10.8" or later.
39 |
40 | ;;; Code:
41 |
42 | ;;; Requirements
43 | (require 'embark)
44 | (require 'consult-mu)
45 |
46 | ;;; Customization Variables
47 | (defcustom consult-mu-embark-noconfirm-before-execute nil
48 | "Should consult-mu-embark skip confirmation when executing marks?"
49 | :group 'consult-mu
50 | :type 'boolean)
51 |
52 | ;;; Define Embark Action Functions
53 | (defun consult-mu-embark-default-action (cand)
54 | "Run `consult-mu-action' on the candidate, CAND."
55 | (let* ((msg (get-text-property 0 :msg cand))
56 | (query (get-text-property 0 :query cand))
57 | (type (get-text-property 0 :type cand))
58 | (newcand (cons cand `(:msg ,msg :query ,query :type ,type))))
59 | (if (equal type :async)
60 | (consult-mu--update-headers query t msg :async))
61 | (funcall consult-mu-action newcand)))
62 |
63 |
64 |
65 | (defun consult-mu-embark-reply (cand)
66 | "Reply to message in CAND."
67 | (let* ((msg (get-text-property 0 :msg cand))
68 | (query (get-text-property 0 :query cand))
69 | (type (get-text-property 0 :type cand)))
70 | (if (equal type :async)
71 | (consult-mu--update-headers query t msg :async))
72 | (consult-mu--reply msg nil)))
73 |
74 | (defun consult-mu-embark-wide-reply (cand)
75 | "Reply all for message in CAND."
76 | (let* ((msg (get-text-property 0 :msg cand))
77 | (query (get-text-property 0 :query cand))
78 | (type (get-text-property 0 :type cand)))
79 | (if (equal type :async)
80 | (consult-mu--update-headers query t msg :async))
81 | (consult-mu--reply msg )))
82 |
83 | (defun consult-mu-embark-forward (cand)
84 | "Forward the message in CAND."
85 | (let* ((msg (get-text-property 0 :msg cand))
86 | (query (get-text-property 0 :query cand))
87 | (type (get-text-property 0 :type cand)))
88 | (if (equal type :async)
89 | (consult-mu--update-headers query t msg :async))
90 | (consult-mu--forward msg)))
91 |
92 | (defun consult-mu-embark-kill-message-field (cand)
93 | "Get a header field of message in CAND."
94 | (let* ((msg (get-text-property 0 :msg cand))
95 | (query (get-text-property 0 :query cand))
96 | (type (get-text-property 0 :type cand))
97 | (msg-id (plist-get msg :message-id)))
98 | (if (equal type :async)
99 | (consult-mu--update-headers query t msg :async))
100 | (with-current-buffer consult-mu-headers-buffer-name
101 | (unless (equal (mu4e-message-field-at-point :message-id) msg-id)
102 | (mu4e-headers-goto-message-id msg-id))
103 | (if (equal (mu4e-message-field-at-point :message-id) msg-id)
104 | (progn
105 | (mu4e~headers-update-handler msg nil nil))))
106 |
107 | (with-current-buffer consult-mu-view-buffer-name
108 | (kill-new (consult-mu--message-get-header-field))
109 | (consult-mu--pulse-region (point) (line-end-position)))))
110 |
111 | (defun consult-mu-embark-save-attachmnts (cand)
112 | "Save attachments of CAND."
113 | (let* ((msg (get-text-property 0 :msg cand))
114 | (query (get-text-property 0 :query cand))
115 | (type (get-text-property 0 :type cand))
116 | (msg-id (plist-get msg :message-id)))
117 |
118 | (if (equal type :async)
119 | (consult-mu--update-headers query t msg :async))
120 |
121 | (with-current-buffer consult-mu-headers-buffer-name
122 | (unless (equal (mu4e-message-field-at-point :message-id) msg-id)
123 | (mu4e-headers-goto-message-id msg-id))
124 | (if (equal (mu4e-message-field-at-point :message-id) msg-id)
125 | (progn
126 | (mu4e~headers-update-handler msg nil nil))))
127 |
128 | (with-current-buffer consult-mu-view-buffer-name
129 | (goto-char (point-min))
130 | (re-search-forward "^\\(Attachment\\|Attachments\\): " nil t)
131 | (consult-mu--pulse-region (point) (line-end-position))
132 | (mu4e-view-save-attachments t))))
133 |
134 | (defun consult-mu-embark-search-messages-from-contact (cand)
135 | "Search messages from the same sender as the message in CAND."
136 | (let* ((msg (get-text-property 0 :msg cand))
137 | (from (car (plist-get msg :from)))
138 | (email (plist-get from :email)))
139 | (consult-mu (concat "from:" email))))
140 |
141 | (defun consult-mu-embark-search-messages-with-subject (cand)
142 | "Search all messages for the same subject as the message in CAND."
143 | (let* ((msg (get-text-property 0 :msg cand))
144 | ;;(subject (replace-regexp-in-string ":\\|#\\|\\.\\|\\+" "" (plist-get msg :subject)))
145 | (subject (replace-regexp-in-string ":\\|#\\|\\.\\|\\+\\|\\(\\[.*\\]\\)" "" (format "%s" (plist-get msg :subject)))))
146 | (consult-mu (concat "subject:" subject))))
147 |
148 | ;; macro for defining functions for marks
149 | (defmacro consult-mu-embark--defun-mark-for (mark)
150 | "Define a function mu4e-view-mark-for- MARK."
151 | (let ((funcname (intern (format "consult-mu-embark-mark-for-%s" mark)))
152 | (docstring (format "Mark the current message for %s." mark)))
153 | `(progn
154 | (defun ,funcname (cand) ,docstring
155 | (let* ((msg (get-text-property 0 :msg cand))
156 | (msgid (plist-get msg :message-id))
157 | (query (get-text-property 0 :query cand))
158 | (buf (get-buffer consult-mu-headers-buffer-name)))
159 | (if buf
160 | (progn
161 | (with-current-buffer buf
162 | (if (eq major-mode 'mu4e-headers-mode)
163 | (progn
164 | (goto-char (point-min))
165 | (mu4e-headers-goto-message-id msgid)
166 | (if (equal (mu4e-message-field-at-point :message-id) msgid)
167 | (mu4e-headers-mark-and-next ',mark)
168 | (progn
169 | (consult-mu--update-headers query t msg :async)
170 | (with-current-buffer buf
171 | (goto-char (point-min))
172 | (mu4e-headers-goto-message-id msgid)
173 | (if (equal (mu4e-message-field-at-point :message-id) msgid)
174 | (mu4e-headers-mark-and-next ',mark))))))
175 | (progn
176 | (consult-mu--update-headers query t msg :async)
177 | (with-current-buffer buf
178 | (goto-char (point-min))
179 | (mu4e-headers-goto-message-id msgid)
180 | (if (equal (mu4e-message-field-at-point :message-id) msgid)
181 | (mu4e-headers-mark-and-next ',mark)))))))))))))
182 |
183 | ;; add embark functions for marks
184 | (defun consult-mu-embark--defun-func-for-marks (marks)
185 | "Run the macro `consult-mu-embark--defun-mark-for' on MARKS.
186 |
187 | MARKS is a list of marks.
188 |
189 | This is useful for creating embark functions for all the `mu4e-marks'
190 | elements."
191 | (mapcar (lambda (mark) (eval `(consult-mu-embark--defun-mark-for ,mark))) marks))
192 |
193 | ;; use consult-mu-embark--defun-func-for-marks to make a function for each `mu4e-marks' element.
194 | (consult-mu-embark--defun-func-for-marks (mapcar 'car mu4e-marks))
195 |
196 | ;;; Define Embark Keymaps
197 | (defvar-keymap consult-mu-embark-general-actions-map
198 | :doc "Keymap for consult-mu-embark"
199 | :parent embark-general-map)
200 |
201 | (add-to-list 'embark-keymap-alist '(consult-mu . consult-mu-embark-general-actions-map))
202 |
203 |
204 | (defvar-keymap consult-mu-embark-messages-actions-map
205 | :doc "Keymap for consult-mu-embark-messages"
206 | :parent consult-mu-embark-general-actions-map
207 | "r" #'consult-mu-embark-reply
208 | "w" #'consult-mu-embark-wide-reply
209 | "f" #'consult-mu-embark-forward
210 | "?" #'consult-mu-embark-kill-message-field
211 | "c" #'consult-mu-embark-search-messages-from-contact
212 | "s" #'consult-mu-embark-search-messages-with-subject
213 | "S" #'consult-mu-embark-save-attachmnts)
214 |
215 | (add-to-list 'embark-keymap-alist '(consult-mu-messages . consult-mu-embark-messages-actions-map))
216 |
217 |
218 | ;; add mark keys to `consult-mu-embark-messages-actions-map' keymap
219 | (defun consult-mu-embark--add-keys-for-marks (marks)
220 | "Add a key for each mark in MARKS to embark map.
221 |
222 | Adds the keys in `consult-mu-embark-messages-actions-map', and binds the
223 | combination “m key”, where key is the :char in mark plist in the
224 | `consult-mu-embark-messages-actions-map' to the function defined by the
225 | prefix “consult-mu-embark-mark-for-” and mark.
226 |
227 | This is useful for adding all `mu4e-marks' to embark key bindings under a
228 | submenu (called by “m”), for example, the default mark-for-archive mark
229 | that is bound to r in mu4e buffers can be called in embark by “m r”."
230 | (mapcar (lambda (mark)
231 | (let* ((key (plist-get (cdr mark) :char))
232 | (key (cond ((consp key) (car key)) ((stringp key) key)))
233 | (func (intern (concat "consult-mu-embark-mark-for-" (format "%s" (car mark)))))
234 | (key (concat "m" key)))
235 | (define-key consult-mu-embark-messages-actions-map key func)))
236 | marks))
237 |
238 | ;; add all `mu4e-marks to embark keybindings. See `consult-mu-embark--add-keys-for-marks' above for more details
239 | (consult-mu-embark--add-keys-for-marks mu4e-marks)
240 |
241 | ;; change the default action on `consult-mu-messages' category.
242 | (add-to-list 'embark-default-action-overrides '(consult-mu-messages . consult-mu-embark-default-action))
243 |
244 |
245 | ;;; Provide `consult-mu-embark' module
246 |
247 | (provide 'consult-mu-embark)
248 |
249 | ;;; consult-mu-embark.el ends here
250 |
--------------------------------------------------------------------------------
/consult-mu.el:
--------------------------------------------------------------------------------
1 | ;;; consult-mu.el --- Consult Mu4e asynchronously -*- lexical-binding: t -*-
2 |
3 | ;; Copyright (C) 2023 Armin Darvish
4 |
5 | ;; Author: Armin Darvish
6 | ;; Maintainer: Armin Darvish
7 | ;; Created: 2023
8 | ;; Version: 1.0
9 | ;; Package-Requires: ((emacs "28.0") (consult "2.0"))
10 | ;; Keywords: convenience, matching, tools, email
11 | ;; Homepage: https://github.com/armindarvish/consult-mu
12 |
13 | ;; SPDX-License-Identifier: GPL-3.0-or-later
14 |
15 | ;; This file is free software: you can redistribute it and/or modify
16 | ;; it under the terms of the GNU General Public License as published
17 | ;; by the Free Software Foundation, either version 3 of the License,
18 | ;; or (at your option) any later version.
19 | ;;
20 | ;; This file is distributed in the hope that it will be useful,
21 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
22 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 | ;; GNU General Public License for more details.
24 | ;;
25 | ;; You should have received a copy of the GNU General Public License
26 | ;; along with this file. If not, see .
27 |
28 |
29 | ;;; Commentary:
30 |
31 | ;; This package provides an alternative interactive serach interface for
32 | ;; mu and mu4e (see URL `https://djcbsoftware.nl/code/mu/mu4e.html').
33 | ;; It uses a consult-based minibuffer completion for searching and
34 | ;; selecting, and marking emails, as well as additional utilities for
35 | ;; composing emails and more.
36 |
37 | ;; This package requires mu4e version "1.10.8" or later.
38 |
39 | ;;; Code:
40 |
41 | ;;; Requirements
42 | (require 'consult)
43 | (require 'mu4e)
44 |
45 | ;;; Group
46 |
47 | (defgroup consult-mu nil
48 | "Options for `consult-mu'."
49 | :group 'convenience
50 | :group 'minibuffer
51 | :group 'consult
52 | :group 'mu4e
53 | :prefix "consult-mu-")
54 |
55 | ;;; Customization Variables
56 |
57 | (defcustom consult-mu-args '("mu")
58 | "Command line arguments to call `mu` asynchronously.
59 |
60 | The dynamically computed arguments are appended.
61 | Can be either a string, or a list of strings or expressions."
62 | :group 'consult-mu
63 | :type '(choice string (repeat (choice string sexp))))
64 |
65 | (defcustom consult-mu-maxnum mu4e-search-results-limit
66 | "Maximum number of results.
67 |
68 | This is normally passed to “--maxnum” in the command line or is defined by
69 | `mu4e-search-results-limit'. By default inherits from
70 | `mu4e-search-results-limit'."
71 | :group 'consult-mu
72 | :type '(choice (const :tag "Unlimited" -1)
73 | (integer :tag "Limit")))
74 |
75 | (defcustom consult-mu-search-sort-field mu4e-search-sort-field
76 | "What field to sort results by?
77 |
78 | By defualt inherits from `mu4e-search-sort-field'."
79 | :group 'consult-mu
80 | :type '(radio (const :tag "Date" :date)
81 | (const :tag "Subject" :subject)
82 | (const :tag "File Size" :size)
83 | (const :tag "Priority" :prio)
84 | (const :tag "From (Sender)" :from)
85 | (const :tag "To (Recipients)" :to)
86 | (const :tag "Mailing List" :list)))
87 |
88 | (defcustom consult-mu-headers-fields mu4e-headers-fields
89 | "A list of header fields to show in the headers buffer.
90 |
91 | By default inherits from `mu4e-headers-field'.
92 |
93 | From mu4e docs:
94 |
95 | Each element has the form (HEADER . WIDTH), where HEADER is one of
96 | the available headers (see `mu4e-header-info') and WIDTH is the
97 | respective width in characters.
98 |
99 | A width of nil means “unrestricted”, and this is best reserved
100 | for the rightmost \(last\) field. Note that Emacs may become very
101 | slow with excessively long lines \(1000s of characters\), so if you
102 | regularly get such messages, you want to avoid fields with nil
103 | altogether."
104 | :group 'consult-mu
105 | :type `(repeat (cons (choice ,@(mapcar (lambda (h)
106 | (list 'const
107 | :tag (plist-get (cdr h) :help)
108 | (car h)))
109 | mu4e-header-info))
110 | (choice (integer :tag "width")
111 | (const :tag "unrestricted width" nil)))))
112 |
113 | (defcustom consult-mu-headers-template nil
114 | "A template string to make custom header formats.
115 |
116 | If non-nil, `consult-mu' uses this string to format the headers instead of
117 | `consult-mu-headers-field'.
118 |
119 | The string should be of the format “%[char][integer]%[char][integer]...”,
120 | and allow dynamic insertion of the content. Each “%[char][integer]“ chunk
121 | represents a different field and the integer defines the length of the
122 | field.
123 |
124 | The list of available fields are:
125 |
126 | %f sender(s) \(e.g. from: field of email\)
127 | %t receivers(s) \(i.e. to: field of email\)
128 | %s subject \(i.e. title of email\)
129 | %d date \(i.e. the date email was sent/received\)
130 | %p priority
131 | %z size
132 | %i message-id \(as defined by mu\)
133 | %g flags \(as defined by mu\)
134 | %G pretty flags \(this uses `mu4e~headers-flags-str' to pretify flags\)
135 | %x tags \(as defined by mu\)
136 | %c cc \(i.e. cc: field of the email\)
137 | %h bcc \(i.e. bcc: field of the email\)
138 | %r date chaged \(as defined by :changed in mu4e\)
139 |
140 | For exmaple, “%d15%s50” means 15 characters for date and 50 charcters for
141 | subject, and “%d13%s37%f17” would make a header containing 13 characters
142 | for Date, 37 characters for Subject, and 20 characters for From field,
143 | making a header that looks like this:
144 |
145 | Thu 09 Nov 23 Title of the Email Limited to 50 Char... example@domain..."
146 | :group 'consult-mu
147 | :type '(choice (const :tag "Fromatted String" :format "%{%%d13%%s50%%f17%}")
148 | (function :tag "Custom Function")))
149 |
150 | (defcustom consult-mu-search-sort-direction mu4e-search-sort-direction
151 | "Direction to sort by a symbol.
152 |
153 | By defualt inherits from `mu4e-search-sort-direction', and can either be
154 | \='descending (sorting Z->A) or \='ascending (sorting A->Z)."
155 |
156 | :group 'consult-mu
157 | :type '(radio (const ascending)
158 | (const descending)))
159 |
160 |
161 | (defcustom consult-mu-search-threads mu4e-search-threads
162 | "Whether to calculate threads for search results.
163 |
164 | By defualt inherits from `mu4e-search-threads'.
165 |
166 | Note that per mu4e docs:
167 | When threading is enabled, the headers are exclusively sorted
168 | chronologically (:date) by the newest message in the thread."
169 | :group 'consult-mu
170 | :type 'boolean)
171 |
172 | (defcustom consult-mu-group-by nil
173 | "What field to use to group the results in the minibuffer.
174 |
175 | By default it is set to :date, but can be any of:
176 |
177 | :subject group by subject
178 | :from group by the name/email the sender(s)
179 | :to group by name/email of the reciver(s)
180 | :date group by date
181 | :time group by the time of email \(i.e. hour, minute, seconds\)
182 | :datetime group by date and time of the email
183 | :year group by the year of the email \(i.e. 2023, 2022, ...\)
184 | :month group by the month of the email \(i.e. Jan, Feb, ..., Dec\)
185 | :week group by the week number of the email
186 | \(i.e. 1st week, 2nd week, ... 52nd week\)
187 | :day-of-week group by the day email was sent (i.e. Mondays, Tuesdays, ...)
188 | :day group by the day email was sent (similar to :day-of-week)
189 | :size group by the file size of the email
190 | :flags group by flags (as defined by mu)
191 | :tags group by tags (as defined by mu)
192 | :changed group by the date changed
193 | \(as defined by :changed field in mu4e\)"
194 | :group 'consult-mu
195 | :type '(radio (const :date)
196 | (const :subject)
197 | (const :from)
198 | (const :to)
199 | (const :time)
200 | (const :datetime)
201 | (const :year)
202 | (const :month)
203 | (const :week)
204 | (const :day-of-week)
205 | (const :day)
206 | (const :size)
207 | (const :flags)
208 | (const :tags)
209 | (const :changed)
210 | (const nil)))
211 |
212 | (defcustom consult-mu-mark-previewed-as-read nil
213 | "Whether to mark PREVIEWED emails as read or not?"
214 | :group 'consult-mu
215 | :type 'boolean)
216 |
217 | (defcustom consult-mu-mark-viewed-as-read t
218 | "Whether to mark VIEWED emails as read or not?"
219 | :group 'consult-mu
220 | :type 'boolean)
221 |
222 | (defcustom consult-mu-headers-buffer-name "*consult-mu-headers*"
223 | "Default name for HEADERS buffer explicitly for `consult-mu'.
224 |
225 | For more info see `mu4e-headers-buffer-name'."
226 | :group 'consult-mu
227 | :type 'string)
228 |
229 | (defcustom consult-mu-view-buffer-name "*consult-mu-view*"
230 | "Default name for VIEW buffer explicitly for `consult-mu'.
231 |
232 | For more info see `mu4e-view-buffer-name'."
233 | :group 'consult-mu
234 | :type 'string)
235 |
236 | (defcustom consult-mu-preview-key consult-preview-key
237 | "Preview key for `consult-mu'.
238 |
239 | This is similar to `consult-preview-key' but explicitly for `consult-mu'."
240 | :group 'consult-mu
241 | :type '(choice (symbol :tag "Any key" 'any)
242 | (list :tag "Debounced"
243 | (const :debounce)
244 | (float :tag "Seconds" 0.1)
245 | (const any))
246 | (const :tag "No preview" nil)
247 | (key :tag "Key")
248 | (repeat :tag "List of keys" key)))
249 |
250 |
251 | (defcustom consult-mu-highlight-matches t
252 | "Should `consult-mu' highlight search queries in preview buffers?"
253 | :group 'consult-mu
254 | :type 'boolean)
255 |
256 | (defcustom consult-mu-use-wide-reply 'ask
257 | "Reply to all or not?
258 |
259 | This defines whether `consult-mu--reply-action' should reply to all or not."
260 | :group 'consult-mu
261 | :type '(choice (symbol :tag "Ask for confirmation" 'ask)
262 | (const :tag "Do not reply to all" nil)
263 | (const :tag "Always reply to all" t)))
264 |
265 | (defcustom consult-mu-action #'consult-mu--view-action
266 | "The function that is used when selecting a message.
267 | By default it is bound to `consult-mu--view-action'."
268 | :group 'consult-mu
269 | :type '(choice (function :tag "(Default) View Message in Mu4e Buffers" consult-mu--view-action)
270 | (function :tag "Reply to Message" consult-mu--reply-action)
271 | (function :tag "Forward Message" consult-mu--forward-action)
272 | (function :tag "Custom Function")))
273 |
274 | (defcustom consult-mu-default-command #'consult-mu-dynamic
275 | "Which command should `consult-mu' call."
276 | :group 'consult-mu
277 | :type '(choice (function :tag "(Default) Use Dynamic Collection (i.e. `consult-mu-dynamic')" #'consult-mu-dynamic)
278 | (function :tag "Use Async Collection (i.e. `consult-mu-async')" #'consult-mu-async)
279 | (function :tag "Custom Function")))
280 |
281 | ;;; Other Variables
282 | (defvar consult-mu-category 'consult-mu
283 | "Category symbol for the `consult-mu' package.")
284 |
285 | (defvar consult-mu-messages-category 'consult-mu-messages
286 | "Category symbol for messages in `consult-mu' package.")
287 |
288 | (defvar consult-mu--view-buffers-list (list)
289 | "List of currently open preview buffers for `consult-mu'.")
290 |
291 | (defvar consult-mu--history nil
292 | "History variable for `consult-mu'.")
293 |
294 | (defvar consult-mu-delimiter " "
295 | "Delimiter to use for fields in mu command output.
296 |
297 | The idea is Taken from https://github.com/seanfarley/counsel-mu.")
298 |
299 | (defvar consult-mu-saved-searches-dynamic (list)
300 | "List of Favorite searches for `consult-mu-dynamic'.")
301 |
302 | (defvar consult-mu-saved-searches-async consult-mu-saved-searches-dynamic
303 | "List of Favorite searches for `consult-mu-async'.")
304 |
305 | (defvar consult-mu--override-group nil
306 | "Override grouping in `consult-mu' based on user input.")
307 |
308 | (defvar consult-mu--mail-headers '("Subject" "From" "To" "From/To" "Cc" "Bcc" "Reply-To" "Date" "Attachments" "Tags" "Flags" "Maildir" "Summary" "List" "Path" "Size" "Message-Id" "List-Id" "Changed")
309 | "List of possible headers in a message.")
310 |
311 | ;;; Faces
312 |
313 | (defface consult-mu-highlight-match-face
314 | `((t :inherit 'consult-highlight-match))
315 | "Highlight match face in `consult-mu' view buffer.
316 |
317 | By default inherits from `consult-highlight-match'.
318 | This is used to highlight matches of search queries in the minibufffer
319 | completion list.")
320 |
321 | (defface consult-mu-preview-match-face
322 | `((t :inherit 'consult-preview-match))
323 | "Preview match face in `consult-mu' preview buffers.
324 |
325 | By default inherits from `consult-preview-match'.
326 | This is used to highlight matches of search query terms in preview buffers
327 | \(i.e. `consult-mu-view-buffer-name'\).")
328 |
329 | (defface consult-mu-default-face
330 | `((t :inherit 'default))
331 | "Default face in `consult-mu' minibuffer annotations.
332 |
333 | By default inherits from `default' face.")
334 |
335 | (defface consult-mu-subject-face
336 | `((t :inherit 'font-lock-keyword-face))
337 | "Subject face in `consult-mu' minibuffer annotations.
338 |
339 | By default inherits from `font-lock-keyword-face'.")
340 |
341 | (defface consult-mu-sender-face
342 | `((t :inherit 'font-lock-variable-name-face))
343 | "Contact face in `consult-mu' minibuffer annotations.
344 |
345 | By default inherits from `font-lock-variable-name-face'.")
346 |
347 | (defface consult-mu-receiver-face
348 | `((t :inherit 'font-lock-variable-name-face))
349 | "Contact face in `consult-mu' minibuffer annotations.
350 |
351 | By default inherits from `font-lock-variable-name-face'.")
352 |
353 | (defface consult-mu-date-face
354 | `((t :inherit 'font-lock-preprocessor-face))
355 | "Date face in `consult-mu' minibuffer annotations.
356 |
357 | By default inherits from `font-lock-preprocessor-face'.")
358 |
359 | (defface consult-mu-count-face
360 | `((t :inherit 'font-lock-string-face))
361 | "Count face in `consult-mu' minibuffer annotations.
362 |
363 | By default inherits from `font-lock-string-face'.")
364 |
365 | (defface consult-mu-size-face
366 | `((t :inherit 'font-lock-string-face))
367 | "Size face in `consult-mu' minibuffer annotations.
368 |
369 | By default inherits from `font-lock-string-face'.")
370 |
371 | (defface consult-mu-tags-face
372 | `((t :inherit 'font-lock-comment-face))
373 | "Tags/Comments face in `consult-mu' minibuffer annotations.
374 |
375 | By default inherits from `font-lock-comment-face'.")
376 |
377 | (defface consult-mu-flags-face
378 | `((t :inherit 'font-lock-function-call-face))
379 | "Flags face in `consult-mu' minibuffer annotations.
380 |
381 | By default inherits from `font-lock-function-call-face'.")
382 |
383 | (defface consult-mu-url-face
384 | `((t :inherit 'link))
385 | "URL face in `consult-mu' minibuffer annotations;
386 |
387 | By default inherits from `link'.")
388 |
389 | (defun consult-mu--pulse-regexp (regexp)
390 | "Find and pulse REGEXP."
391 | (goto-char (point-min))
392 | (while (re-search-forward regexp nil t)
393 | (when-let* ((m (match-data))
394 | (beg (car m))
395 | (end (cadr m))
396 | (ov (make-overlay beg end))
397 | (pulse-delay 0.075))
398 | (pulse-momentary-highlight-overlay ov 'highlight))))
399 |
400 | (defun consult-mu--pulse-region (beg end)
401 | "Find and pulse region from BEG to END."
402 | (let ((ov (make-overlay beg end))
403 | (pulse-delay 0.075))
404 | (pulse-momentary-highlight-overlay ov 'highlight)))
405 |
406 | (defun consult-mu--pulse-line ()
407 | "Pulse line at point momentarily."
408 | (let* ((pulse-delay 0.055)
409 | (ov (make-overlay (car (bounds-of-thing-at-point 'line))
410 | (cdr (bounds-of-thing-at-point 'line)))))
411 | (pulse-momentary-highlight-overlay ov 'highlight)))
412 |
413 | (defun consult-mu--set-string-width (string width &optional prepend)
414 | "Set the STRING width to a fixed value, WIDTH.
415 |
416 | If the STRING is longer than WIDTH, it truncates the string and adds
417 | ellipsis, “...”. If the string is shorter, it adds whitespace to the
418 | string. If PREPEND is non-nil, it truncates or adds whitespace from the
419 | beginning of string, instead of the end."
420 | (let* ((string (format "%s" string))
421 | (w (string-width string)))
422 | (when (< w width)
423 | (if prepend
424 | (setq string (format "%s%s" (make-string (- width w) ?\s) (substring string)))
425 | (setq string (format "%s%s" (substring string) (make-string (- width w) ?\s)))))
426 | (when (> w width)
427 | (if prepend
428 | (setq string (format "...%s" (substring string (- w (- width 3)) w)))
429 | (setq string (format "%s..." (substring string 0 (- width (+ w 3)))))))
430 | string))
431 |
432 | (defun consult-mu--justify-left (string prefix maxwidth)
433 | "Set the width of STRING+PREFIX justified from left.
434 |
435 | Use `consult-mu--set-string-width' to the width of the concatenate of
436 | STRING+PREFIX \(e.g. “(concat prefix string)”\) within MAXWIDTH. This is
437 | used for aligning marginalia info in the minibuffer."
438 | (let ((w (string-width prefix)))
439 | (if (> maxwidth w)
440 | (consult-mu--set-string-width string (- maxwidth w) t)
441 | string)))
442 |
443 | (defun consult-mu--highlight-match (regexp str ignore-case)
444 | "Highlight REGEXP in STR.
445 |
446 | If a REGEXP contains a capturing group, only the captured group is
447 | highlighted, otherwise, the whole match is highlighted.
448 | Case is ignored if IGNORE-CASE is non-nil.
449 | \(This is adapted from `consult--highlight-regexps'.\)"
450 | (let ((i 0))
451 | (while (and (let ((case-fold-search ignore-case))
452 | (string-match regexp str i))
453 | (> (match-end 0) i))
454 | (let ((m (match-data)))
455 | (setq i (cadr m)
456 | m (or (cddr m) m))
457 | (while m
458 | (when (car m)
459 | (add-face-text-property (car m) (cadr m)
460 | 'consult-mu-highlight-match-face nil str))
461 | (setq m (cddr m))))))
462 | str)
463 |
464 | (defun consult-mu--overlay-match (match-str buffer ignore-case)
465 | "Highlight MATCH-STR in BUFFER using an overlay.
466 |
467 | If IGNORE-CASE is non-nil, it uses case-insensitive match.
468 |
469 | This is used to highlight matches to use queries when viewing emails. See
470 | `consult-mu-overlays-toggle' for toggling highligths on/off."
471 | (with-current-buffer (or (get-buffer buffer) (current-buffer))
472 | (remove-overlays (point-min) (point-max) 'consult-mu-overlay t)
473 | (goto-char (point-min))
474 | (let ((case-fold-search ignore-case))
475 | (while (search-forward match-str nil t)
476 | (when-let* ((m (match-data))
477 | (beg (car m))
478 | (end (cadr m))
479 | (overlay (make-overlay beg end)))
480 | (overlay-put overlay 'consult-mu-overlay t)
481 | (overlay-put overlay 'face 'consult-mu-highlight-match-face))))))
482 |
483 | (defun consult-mu-overlays-toggle (&optional buffer)
484 | "Toggle overlay highlight in BUFFER.
485 |
486 | BUFFER defaults to `current-buffer'."
487 | (interactive)
488 | (let ((buffer (or buffer (current-buffer))))
489 | (with-current-buffer buffer
490 | (dolist (o (overlays-in (point-min) (point-max)))
491 | (when (overlay-get o 'consult-mu-overlay)
492 | (if (and (overlay-get o 'face) (eq (overlay-get o 'face) 'consult-mu-highlight-match-face))
493 | (overlay-put o 'face nil)
494 | (overlay-put o 'face 'consult-mu-highlight-match-face)))))))
495 |
496 | (defun consult-mu--format-date (string)
497 | "Format the date STRING from mu output.
498 |
499 | STRING is the output form a mu command, for example:
500 | `mu find query --fields d`
501 | Returns the date in the format Day-of-Week Month Day Year Time
502 | \(e.g. Sat Nov 04 2023 09:46:54\)"
503 | (let ((string (replace-regexp-in-string " " "0" string)))
504 | (format "%s %s %s"
505 | (substring string 0 10)
506 | (substring string -4 nil)
507 | (substring string 11 -4))))
508 |
509 | (defun consult-mu-flags-to-string (FLAG)
510 | "Covert FLAGS, from mu output to strings.
511 |
512 | FLAG is the output form mu command in the terminal, for example:
513 | `mu find query --fields g`.
514 | This function converts each character in FLAG to an expanded string of the
515 | flag and returns the list of these strings."
516 | (cl-loop for c across FLAG
517 | collect
518 | (pcase (string c)
519 | ("D" 'draft)
520 | ("F" 'flagged)
521 | ("N" 'new)
522 | ("P" 'forwarded)
523 | ("R" 'replied)
524 | ("S" 'read)
525 | ("T" 'trashed)
526 | ("a" 'attachment)
527 | ("x" 'encrrypted)
528 | ("s" 'signed)
529 | ("u" 'unread)
530 | ("l" 'list)
531 | ("q" 'personal)
532 | ("c" 'calendar)
533 | (_ nil))))
534 |
535 | (defun consult-mu--message-extract-email-from-string (string)
536 | "Find and return the first email address in the STRING."
537 | (when (stringp string)
538 | (string-match "[a-zA-Z0-9\_\.\+\-]+@[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-\.]+" string)
539 | (match-string 0 string)))
540 |
541 | (defun consult-mu--message-emails-string-to-list (string)
542 | "Convert comma-separated STRING of email addresses to a list."
543 | (when (stringp string)
544 | (remove '(" " "\s" "\t")
545 | (mapcar #'consult-mu--message-extract-email-from-string
546 | (split-string string ",\\|;\\|\t" t)))))
547 |
548 | (defun consult-mu--message-get-header-field (&optional field)
549 | "Retrive FIELD header from the message/mail in the current buffer."
550 | (save-match-data
551 | (save-excursion
552 | (when (or (derived-mode-p 'message-mode)
553 | (derived-mode-p 'mu4e-view-mode)
554 | (derived-mode-p 'org-msg-edit-mode)
555 | (derived-mode-p 'mu4e-compose-mode))
556 | (let* ((case-fold-search t)
557 | (header-regexp (mapconcat (lambda (str) (concat "\n" str ": "))
558 | consult-mu--mail-headers "\\|"))
559 | (field (or (downcase field)
560 | (downcase (consult--read consult-mu--mail-headers
561 | :prompt "Header Field: ")))))
562 | (if (string-prefix-p "attachment" field) (setq field "\\(attachment\\|attachments\\)"))
563 | (goto-char (point-min))
564 | (message-goto-body)
565 | (let* ((match (re-search-backward (concat "^" field ": \\(?1:[[:ascii:][:nonascii:]]*?\\)\n\\(.*?:\\|\n\\)") nil t))
566 | (str (if (and match (match-string 1)) (string-trim (match-string 1)))))
567 | (if (string-empty-p str) nil str)))))))
568 |
569 | (defun consult-mu--headers-append-handler (msglst)
570 | "Append one-line descriptions of messages in MSGLST.
571 |
572 | This is used to override `mu4e~headers-append-handler' to ensure that
573 | buffer handling is done right for `consult-mu'."
574 | (with-current-buffer "*consult-mu-headers*"
575 | (let ((inhibit-read-only t))
576 | (seq-do
577 | ;; I use mu4e-column-faces and it overrides the default append-handler. To get the same effect I check if mu4e-column-faces is active and enabled.
578 | (if (and (featurep 'mu4e-column-faces) mu4e-column-faces-mode)
579 | (lambda (msg)
580 | (mu4e-column-faces--insert-header msg (point-max)))
581 | (lambda (msg)
582 | (mu4e~headers-insert-header msg (point-max))))
583 | msglst))))
584 |
585 | (defun consult-mu--view-msg (msg &optional buffername)
586 | "Display the message MSG in a buffer with BUFFERNAME.
587 |
588 | BUFFERNAME defaults to `consult-mu-view-buffer-name'.
589 |
590 | This s used to overrides `mu4e-view' to ensure that buffer handling is done
591 | right for `consult-mu'."
592 | (let* ((linked-headers-buffer (mu4e-get-headers-buffer "*consult-mu-headers*" t))
593 | (mu4e-view-buffer-name (or buffername consult-mu-view-buffer-name)))
594 | (setq gnus-article-buffer (mu4e-get-view-buffer linked-headers-buffer t))
595 | (with-current-buffer gnus-article-buffer
596 | (let ((inhibit-read-only t))
597 | (remove-overlays (point-min) (point-max) 'mu4e-overlay t)
598 | (erase-buffer)
599 | (insert-file-contents-literally
600 | (mu4e-message-readable-path msg) nil nil nil t)
601 | (setq-local mu4e--view-message msg)
602 | (mu4e--view-render-buffer msg)
603 | (mu4e-loading-mode 0)
604 | (with-current-buffer linked-headers-buffer
605 | (setq-local mu4e~headers-view-win (mu4e-display-buffer gnus-article-buffer nil)))
606 | (run-hooks 'mu4e-view-rendered-hook)))))
607 |
608 | (defun consult-mu--headers-clear (&optional text)
609 | "Clear the headers buffer and related data structures.
610 |
611 | Optionally, show TEXT.
612 |
613 | This is used to override `mu4e~headers-clear' to ensure that buffer
614 | handling is done right for `consult-mu'."
615 | (setq mu4e~headers-render-start (float-time)
616 | mu4e~headers-hidden 0)
617 | (with-current-buffer "*consult-mu-headers*"
618 | (let ((inhibit-read-only t))
619 | (mu4e--mark-clear)
620 | (erase-buffer)
621 | (when text
622 | (goto-char (point-min))
623 | (insert (propertize text 'face 'mu4e-system-face 'intangible t))))))
624 |
625 | (defun consult-mu--set-mu4e-search-sortfield (opts)
626 | "Dynamically set the `mu4e-search-sort-field' based on user input.
627 |
628 | Uses user input (i.e. from `consult-mu' command) to define the sort field.
629 |
630 | OPTS is the command line options for mu and can be set by entering options
631 | in the minibuffer input. For more details, refer to `consult-grep' and
632 | consult async documentation.
633 |
634 | For example if the user enters the following in the minibuffer:
635 |
636 | “#query -- --maxnum 400 --sortfield from”
637 |
638 | `mu4e-search-sort-field' is set to :from
639 |
640 | Note that per mu4e docs:
641 | When threading is enabled, the headers are exclusively sorted
642 | chronologically (:date) by the newest message in the thread."
643 | (let* ((sortfield (cond
644 | ((member "-s" opts) (nth (+ (cl-position "-s" opts :test 'equal) 1) opts))
645 | ((member "--sortfield" opts) (nth (+ (cl-position "--sortfield" opts :test 'equal) 1) opts))
646 | (t consult-mu-search-sort-field))))
647 | (pcase sortfield
648 | ('nil
649 | consult-mu-search-sort-field)
650 | ((or "date" "d")
651 | :date)
652 | ((or "subject" "s")
653 | :subject)
654 | ((or "size" "z")
655 | :size)
656 | ((or "prio" "p")
657 | :prio)
658 | ((or "from" "f")
659 | :from)
660 | ((or "to" "t")
661 | :to)
662 | ((or "list" "v")
663 | :list)
664 | ;; ((or "tags" "x")
665 | ;; :tags)
666 | (_
667 | consult-mu-search-sort-field))))
668 |
669 | (defun consult-mu--set-mu4e-search-sort-direction (opts)
670 | "Dynamically set the `mu4e-search-sort-direction' based on user input.
671 |
672 | Uses user input \(i.e. from `consult-mu' command\) to define the sort field.
673 |
674 | OPTS is the command line options for mu and can be set by entering options
675 | in the minibuffer input. For more details, refer to `consult-grep' and
676 | consult async documentation.
677 |
678 | For example, if the user enters the following in the minibuffer:
679 |
680 | “#query -- --maxnum 400 --sortfield from --reverse”
681 |
682 | The `mu4e-search-sort-direction' is reversed; If it is set to
683 | \='ascending, it is toggled to \='descending and vise versa."
684 | (if (or (member "-z" opts) (member "--reverse" opts))
685 | (pcase consult-mu-search-sort-direction
686 | ('descending
687 | 'ascending)
688 | ('ascending
689 | 'descending))
690 | consult-mu-search-sort-direction))
691 |
692 | (defun consult-mu--set-mu4e-skip-duplicates (opts)
693 | "Dynamically set the `mu4e-search-skip-duplicates' based on user input.
694 |
695 | Uses user input \(i.e. from `consult-mu' command\) to define whether to
696 | skip duplicates.
697 |
698 | OPTS is the command line options for mu and can be set by entering options
699 | in the minibuffer input. For more details, refer to `consult-grep' and
700 | consult async documentation.
701 |
702 | For example, if the user enters the following in the minibuffer:
703 |
704 | “#query -- --maxnum 400 --skip-dups”
705 |
706 | The `mu4e-search-skip-duplicates' is set to t."
707 | (if (or (member "--skip-dups" opts) mu4e-search-skip-duplicates) t nil))
708 |
709 | (defun consult-mu--set-mu4e-results-limit (opts)
710 | "Dynamically set the `mu4e-search-results-limit' based on user input.
711 |
712 |
713 | Uses user input \(i.e. from `consult-mu' command\) to define the number of
714 | results shown.
715 |
716 | OPTS is the command line options for mu and can be set by entering options
717 | in the minibuffer input. For more details, refer to `consult-grep' and
718 | consult async documentation.
719 |
720 | For example, if the user enters the following in the minibuffer:
721 |
722 | “#query -- --maxnum 400”
723 |
724 | The `mu4e-search-results-limit' is set to 400."
725 | (cond
726 | ((member "-n" opts) (string-to-number (nth (+ (cl-position "-n" opts :test 'equal) 1) opts)))
727 | ((member "--maxnum" opts) (string-to-number (nth (+ (cl-position "--maxnum" opts :test 'equal) 1) opts)))
728 | (t consult-mu-maxnum)))
729 |
730 |
731 | (defun consult-mu--set-mu4e-include-related (opts)
732 | "Dynamically set the `mu4e-search-include-related' based on user input.
733 |
734 | Uses user input \(i.e. from `consult-mu' command\) to define whether to
735 | include related messages.
736 |
737 | OPTS is the command line options for mu and can be set by entering options
738 | in the minibuffer input. For more details, refer to `consult-grep' and
739 | consult async documentation.
740 |
741 | For example if the user enters the following in the minibuffer:
742 |
743 | “#query -- --include-related”
744 |
745 | The `mu4e-search-include-related' is set to t."
746 | (if (or (member "-r" opts) (member "--include-related" opts) mu4e-search-include-related) t nil))
747 |
748 |
749 |
750 | (defun consult-mu--set-mu4e-threads (opts)
751 | "Set the `mu4e-search-threads' based on `mu4e-search-sort-field'.
752 |
753 | Uses user input \(i.e. from `consult-mu' command\) to define whether to
754 | show threads.
755 |
756 | OPTS is the command line options for mu and can be set by entering options
757 | in the minibuffer input. For more details, refer to `consult-grep' and
758 | consult async documentation.
759 |
760 | Note that per mu4e docs, when threading is enabled, the headers are
761 | exclusively sorted by date. Here the logic is reversed in order to allow
762 | dynamically sorting by fields other than date \(even when threads are
763 | enabled\). In other words, if the sort-field is not the :date, threading
764 | is disabled because otherwise sort field will be ignored. This allows the
765 | user to use command line arguments to sort messages by fields other than
766 | the date. For example, the user can enter the following in the minibuffer
767 | input to sort by subject
768 |
769 | “#query -- --sortfield subject”
770 |
771 | When the sort-field is :date, the default setting,
772 | `consult-mu-search-threads' is used, and if that is set to nil, the user
773 | can use command line arguments \(a.k.a. -t or --thread\) to enable it
774 | dynamically."
775 | (cond
776 | ((not (equal mu4e-search-sort-field :date))
777 | nil)
778 | ((or (member "-t" opts) (member "--threads" opts) consult-mu-search-threads)
779 | t)))
780 |
781 | (defun consult-mu--update-headers (query ignore-history msg type)
782 | "Search for QUERY, and update `consult-mu-headers-buffer-name' buffer.
783 |
784 | If IGNORE-HISTORY is true, does *not* update the query history stack,
785 | `mu4e--search-query-past'.
786 | If MSG is non-nil, put the cursor on MSG.
787 | TYPE can be either \=':dynamic or \=':async"
788 | (consult-mu--execute-all-marks)
789 | (cl-letf* (((symbol-function #'mu4e~headers-append-handler) #'consult-mu--headers-append-handler))
790 | (unless (mu4e-running-p) (mu4e--server-start))
791 | (let* ((buf (mu4e-get-headers-buffer consult-mu-headers-buffer-name t))
792 | (view-buffer (get-buffer consult-mu-view-buffer-name))
793 | (expr (car (consult--command-split (substring-no-properties query))))
794 | (rewritten-expr (funcall mu4e-query-rewrite-function expr))
795 | (mu4e-headers-fields consult-mu-headers-fields))
796 | (pcase type
797 | (:dynamic)
798 | (:async
799 | (setq rewritten-expr (funcall mu4e-query-rewrite-function (concat "msgid:" (plist-get msg :message-id)))))
800 | (_ ))
801 |
802 | (with-current-buffer buf
803 | (save-excursion
804 | (let ((inhibit-read-only t))
805 | (erase-buffer)
806 | (mu4e-headers-mode)
807 | (setq-local mu4e-view-buffer-name consult-mu-view-buffer-name)
808 | (if view-buffer
809 | (setq-local mu4e~headers-view-win (mu4e-display-buffer gnus-article-buffer nil)))
810 | (unless ignore-history
811 | ; save the old present query to the history list
812 | (when mu4e--search-last-query
813 | (mu4e--search-push-query mu4e--search-last-query 'past)))
814 | (setq mu4e--search-last-query rewritten-expr)
815 | (setq list-buffers-directory rewritten-expr)
816 | (mu4e--modeline-update)
817 | (run-hook-with-args 'mu4e-search-hook expr)
818 | (consult-mu--headers-clear mu4e~search-message)
819 | (setq mu4e~headers-search-start (float-time))
820 |
821 | (pcase-let* ((`(,_arg . ,opts) (consult--command-split query))
822 | (mu4e-search-sort-field (consult-mu--set-mu4e-search-sortfield opts))
823 | (mu4e-search-sort-direction (consult-mu--set-mu4e-search-sort-direction opts))
824 | (mu4e-search-skip-duplicates (consult-mu--set-mu4e-skip-duplicates opts))
825 | (mu4e-search-results-limit (consult-mu--set-mu4e-results-limit opts))
826 | (mu4e-search-threads (consult-mu--set-mu4e-threads opts))
827 | (mu4e-search-include-related (consult-mu--set-mu4e-include-related opts)))
828 | (mu4e--server-find
829 | rewritten-expr
830 | mu4e-search-threads
831 | mu4e-search-sort-field
832 | mu4e-search-sort-direction
833 | mu4e-search-results-limit
834 | mu4e-search-skip-duplicates
835 | mu4e-search-include-related))
836 | (while (or (string-empty-p (buffer-substring (point-min) (point-max)))
837 | (equal (buffer-substring (point-min) (+ (point-min) (length mu4e~search-message))) mu4e~search-message)
838 | (not (or (equal (buffer-substring (- (point-max) (length mu4e~no-matches)) (point-max)) mu4e~no-matches) (equal (buffer-substring (- (point-max) (length mu4e~end-of-results)) (point-max)) mu4e~end-of-results))))
839 | (sleep-for 0.005))))))))
840 |
841 | (defun consult-mu--execute-all-marks (&optional no-confirmation)
842 | "Execute the actions for all marked messages.
843 |
844 | Executes all actions for marked messages in the buffer
845 | `consult-mu-headers-buffer-name'.
846 |
847 | If NO-CONFIRMATION is non-nil, don't ask user for confirmation.
848 |
849 | This is similar to `mu4e-mark-execute-all' but, with buffer/window
850 | handling set accordingly for `consult-mu'."
851 | (interactive "P")
852 | (when-let* ((buf (get-buffer consult-mu-headers-buffer-name)))
853 | (with-current-buffer buf
854 | (when (eq major-mode 'mu4e-headers-mode)
855 | (mu4e--mark-in-context
856 | (let* ((marknum (mu4e-mark-marks-num)))
857 | (unless (zerop marknum)
858 | (pop-to-buffer buf)
859 | (unless (one-window-p) (delete-other-windows))
860 | (mu4e-mark-execute-all no-confirmation)
861 | (quit-window))))))))
862 |
863 | (defun consult-mu--headers-goto-message-id (msgid)
864 | "Jump to message with MSGID.
865 |
866 | This is done in `consult-mu-headers-buffer-name' buffer."
867 | (when-let ((buffer consult-mu-headers-buffer-name))
868 | (with-current-buffer buffer
869 | (setq mu4e-view-buffer-name consult-mu-view-buffer-name)
870 | (mu4e-headers-goto-message-id msgid))))
871 |
872 | (defun consult-mu--get-message-by-id (msgid)
873 | "Find the message with MSGID and return the mu4e MSG plist for it."
874 | (cl-letf* (((symbol-function #'mu4e-view) #'consult-mu--view-msg))
875 | (when-let ((buffer consult-mu-headers-buffer-name))
876 | (with-current-buffer buffer
877 | (setq mu4e-view-buffer-name consult-mu-view-buffer-name)
878 | (mu4e-headers-goto-message-id msgid)
879 | (mu4e-message-at-point)))))
880 |
881 | (defun consult-mu--contact-string-to-plist (string)
882 | "Convert STRING for contacts to plist.
883 |
884 | STRING is the output form mu command, for example from:
885 | `mu find query --fields f`
886 |
887 | Returns a plist with \=':email and \':name keys.
888 |
889 | For example
890 |
891 | “John Doe ”
892 |
893 | will be converted to
894 |
895 | \(:name “John Doe” :email “john.doe@example.com”\)"
896 | (let* ((string (replace-regexp-in-string ">,\s\\|>;\s" ">\n" string))
897 | (list (split-string string "\n" t)))
898 | (mapcar (lambda (item)
899 | (cond
900 | ((string-match "\\(?2:.*\\)\s+<\\(?1:.+\\)>" item)
901 | (list :email (or (match-string 1 item) nil) :name (or (match-string 2 item) nil)))
902 | ((string-match "^\\(?1:[a-zA-Z0-9\_\.\+\-]+@[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-\.]+\\)" item)
903 | (list :email (or (match-string 1 item) nil) :name nil))
904 | (t
905 | (list :email (format "%s" item) :name nil)))) list)))
906 |
907 | (defun consult-mu--contact-name-or-email (contact)
908 | "Retrieve name or email of CONTACT.
909 |
910 | Looks at the contact plist \(e.g. (:name “John Doe” :email
911 | “john.doe@example.com”)\) and returns the name. If the name is missing,
912 | returns the email address."
913 | (cond
914 | ((stringp contact)
915 | contact)
916 | ((listp contact)
917 | (mapconcat (lambda (item) (or (plist-get item :name) (plist-get item :email) "")) contact ","))))
918 |
919 | (defun consult-mu--headers-template ()
920 | "Make headers template using `consult-mu-headers-template'."
921 | (if (and consult-mu-headers-template (functionp consult-mu-headers-template))
922 | (funcall consult-mu-headers-template)
923 | consult-mu-headers-template))
924 |
925 | (defun consult-mu--expand-headers-template (msg string)
926 | "Expand STRING to create a custom header format for MSG.
927 |
928 | See `consult-mu-headers-template' for explanation of the format of
929 | STRING."
930 |
931 | (cl-loop for c in (split-string string "%" t)
932 | concat (concat (pcase (substring c 0 1)
933 | ("f" (let ((sender (consult-mu--contact-name-or-email (plist-get msg :from)))
934 | (length (string-to-number (substring c 1 nil))))
935 | (if sender
936 | (propertize (if (> length 0) (consult-mu--set-string-width sender length) sender) 'face 'consult-mu-sender-face))))
937 | ("t" (let ((receiver (consult-mu--contact-name-or-email (plist-get msg :to)))
938 | (length (string-to-number (substring c 1 nil))))
939 | (if receiver
940 | (propertize (if (> length 0) (consult-mu--set-string-width receiver length) receiver) 'face 'consult-mu-sender-face))))
941 | ("s" (let ((subject (plist-get msg :subject))
942 | (length (string-to-number (substring c 1 nil))))
943 | (if subject
944 | (propertize (if (> length 0) (consult-mu--set-string-width subject length) subject) 'face 'consult-mu-subject-face))))
945 | ("d" (let ((date (format-time-string "%a %d %b %y" (plist-get msg :date)))
946 | (length (string-to-number (substring c 1 nil))))
947 | (if date
948 | (propertize (if (> length 0) (consult-mu--set-string-width date length) date) 'face 'consult-mu-date-face))))
949 |
950 | ("p" (let ((priority (plist-get msg :priority))
951 | (length (string-to-number (substring c 1 nil))))
952 | (if priority
953 | (propertize (if (> length 0) (consult-mu--set-string-width (format "%s" priority) length) (format "%s" priority)) 'face 'consult-mu-size-face))))
954 | ("z" (let ((size (file-size-human-readable (plist-get msg :size)))
955 | (length (string-to-number (substring c 1 nil))))
956 | (if size
957 | (propertize (if (> length 0) (consult-mu--set-string-width size length) size) 'face 'consult-mu-size-face))))
958 | ("i" (let ((id (plist-get msg :message-id))
959 | (length (string-to-number (substring c 1 nil))))
960 | (if id
961 | (propertize (if (> length 0) (consult-mu--set-string-width id length) id) 'face 'consult-mu-default-face))))
962 |
963 | ("g" (let ((flags (plist-get msg :flags))
964 | (length (string-to-number (substring c 1 nil))))
965 | (if flags
966 | (propertize (if (> length 0) (consult-mu--set-string-width (format "%s" flags) length) (format "%s" flags)) 'face 'consult-mu-flags-face))))
967 |
968 | ("G" (let ((flags (plist-get msg :flags))
969 | (length (string-to-number (substring c 1 nil))))
970 | (if flags
971 | (propertize (if (> length 0) (consult-mu--set-string-width (format "%s" (mu4e~headers-flags-str flags)) length) (format "%s" (mu4e~headers-flags-str flags))) 'face 'consult-mu-flags-face))))
972 |
973 | ("x" (let ((tags (plist-get msg :tags))
974 | (length (string-to-number (substring c 1 nil))))
975 | (if tags
976 | (propertize (if (> length 0) (consult-mu--set-string-width tags length) tags) 'face 'consult-mu-tags-face) nil)))
977 |
978 | ("c" (let ((cc (consult-mu--contact-name-or-email (plist-get msg :cc)))
979 | (length (string-to-number (substring c 1 nil))))
980 | (if cc
981 | (propertize (if (> length 0) (consult-mu--set-string-width cc length) cc) 'face 'consult-mu-tags-face))))
982 |
983 | ("h" (let ((bcc (consult-mu--contact-name-or-email (plist-get msg :bcc)))
984 | (length (string-to-number (substring c 1 nil))))
985 | (if bcc
986 | (propertize (if (> length 0) (consult-mu--set-string-width bcc length) bcc) 'face 'consult-mu-tags-face))))
987 |
988 | ("r" (let ((changed (format-time-string "%a %d %b %y" (plist-get msg :changed)))
989 | (length (string-to-number (substring c 1 nil))))
990 | (if changed
991 | (propertize (if (> length 0) (consult-mu--set-string-width changed length) changed) 'face 'consult-mu-tags-face))))
992 | (_ nil))
993 | " ")))
994 |
995 | (defun consult-mu--quit-header-buffer ()
996 | "Quits `consult-mu-headers-buffer-name' buffer."
997 | (save-mark-and-excursion
998 | (when-let* ((buf (get-buffer consult-mu-headers-buffer-name)))
999 | (with-current-buffer buf
1000 | (if (eq major-mode 'mu4e-headers-mode)
1001 | (mu4e-mark-handle-when-leaving)
1002 | (quit-window t)
1003 | ;; clear the decks before going to the main-view
1004 | (mu4e--query-items-refresh 'reset-baseline))))))
1005 |
1006 | (defun consult-mu--quit-view-buffer ()
1007 | "Quits `consult-mu-view-buffer-name' buffer."
1008 | (when-let* ((buf (get-buffer consult-mu-view-buffer-name)))
1009 | (with-current-buffer buf
1010 | (if (eq major-mode 'mu4e-view-mode)
1011 | (mu4e-view-quit)))))
1012 |
1013 | (defun consult-mu--quit-main-buffer ()
1014 | "Quits `mu4e-main-buffer-name' buffer."
1015 | (when-let* ((buf (get-buffer mu4e-main-buffer-name)))
1016 | (with-current-buffer buf
1017 | (if (eq major-mode 'mu4e-main-mode)
1018 | (mu4e-quit)))))
1019 |
1020 | (defun consult-mu--lookup ()
1021 | "Lookup function for `consult-mu' or `consult-mu-async' candidates.
1022 |
1023 | This is passed as LOOKUP to `consult--read' on candidates and is used to
1024 | format the output when a candidate is selected."
1025 | (lambda (sel cands &rest _args)
1026 | (let* ((info (cdr (assoc sel cands)))
1027 | (msg (plist-get info :msg))
1028 | (subject (plist-get msg :subject)))
1029 | (cons subject info))))
1030 |
1031 | (defun consult-mu--group-name (cand)
1032 | "Get the group name of CAND using `consult-mu-group-by'.
1033 |
1034 | See `consult-mu-group-by' for details of grouping options."
1035 | (let* ((msg (get-text-property 0 :msg cand))
1036 | (group (or consult-mu--override-group consult-mu-group-by))
1037 | (field (if (not (keywordp group)) (intern (concat ":" (format "%s" group))) group)))
1038 | (pcase field
1039 | (:date (format-time-string "%a %d %b %y" (plist-get msg field)))
1040 | (:from (cond
1041 | ((listp (plist-get msg field))
1042 | (mapconcat (lambda (item) (or (plist-get item :name) (plist-get item :email))) (plist-get msg field) ";"))
1043 | ((stringp (plist-get msg field)) (plist-get msg field))))
1044 | (:to (cond
1045 | ((listp (plist-get msg field))
1046 | (mapconcat (lambda (item) (or (plist-get item :name) (plist-get item :email))) (plist-get msg field) ";"))
1047 | ((stringp (plist-get msg field)) (plist-get msg field))))
1048 | (:changed (format-time-string "%a %d %b %y" (plist-get msg field)))
1049 | (:datetime (format-time-string "%F %r" (plist-get msg :date)))
1050 | (:time (format-time-string "%X" (plist-get msg :date)))
1051 | (:year (format-time-string "%Y" (plist-get msg :date)))
1052 | (:month (format-time-string "%B" (plist-get msg :date)))
1053 | (:day-of-week (format-time-string "%A" (plist-get msg :date)))
1054 | (:day (format-time-string "%A" (plist-get msg :date)))
1055 | (:week (format-time-string "%V" (plist-get msg :date)))
1056 | (:size (file-size-human-readable (plist-get msg field)))
1057 | (:flags (format "%s" (plist-get msg field)))
1058 | (:tags (format "%s" (plist-get msg field)))
1059 | (_ (if (plist-get msg field) (format "%s" (plist-get msg field)) nil)))))
1060 |
1061 | (defun consult-mu--group (cand transform)
1062 | "Group function for `consult-mu' or `consult-mu-async'.
1063 |
1064 | CAND is passed to `consult-mu--group-name' to get the group for CAND.
1065 | When TRANSFORM is non-nil, the name of CAND is used for group."
1066 | (when-let ((name (consult-mu--group-name cand)))
1067 | (if transform (substring cand) name)))
1068 |
1069 | (defun consult-mu--view (msg noselect mark-as-read match-str)
1070 | "Opens MSG in `consult-mu-headers' and `consult-mu-view'.
1071 |
1072 | If NOSELECT is non-nil, does not select the view buffer/window.
1073 | If MARK-AS-READ is non-nil, marks the MSG as read.
1074 | If MATCH-STR is non-nil, highlights the MATCH-STR in the view buffer."
1075 | (let ((msgid (plist-get msg :message-id)))
1076 | (when-let ((buf (mu4e-get-headers-buffer consult-mu-headers-buffer-name t)))
1077 | (with-current-buffer buf
1078 | ;;(mu4e-headers-mode)
1079 | (goto-char (point-min))
1080 | (setq mu4e-view-buffer-name consult-mu-view-buffer-name)
1081 | (unless noselect
1082 | (switch-to-buffer buf))))
1083 |
1084 | (consult-mu--view-msg msg consult-mu-view-buffer-name)
1085 |
1086 | (with-current-buffer consult-mu-headers-buffer-name
1087 | (if msgid
1088 | (progn
1089 | (mu4e-headers-goto-message-id msgid)
1090 | (if mark-as-read
1091 | (mu4e--server-move (mu4e-message-field-at-point :docid) nil "+S-u-N")))))
1092 |
1093 | (when match-str
1094 | (add-to-history 'search-ring match-str)
1095 | (consult-mu--overlay-match match-str consult-mu-view-buffer-name t))
1096 |
1097 | (with-current-buffer consult-mu-view-buffer-name
1098 | (goto-char (point-min)))
1099 |
1100 | (unless noselect
1101 | (when msg
1102 | (select-window (get-buffer-window consult-mu-view-buffer-name))))
1103 | consult-mu-view-buffer-name))
1104 |
1105 |
1106 | (defun consult-mu--view-action (cand)
1107 | "Open the candidate, CAND.
1108 |
1109 | This is a wrapper function around `consult-mu--view'. It parses CAND to
1110 | extract relevant MSG plist and other information and passes them to
1111 | `consult-mu--view'.
1112 |
1113 | To use this as the default action for `consult-mu', set
1114 | `consult-mu-default-action' to \=#'consult-mu--view-action."
1115 |
1116 | (let* ((info (cdr cand))
1117 | (msg (plist-get info :msg))
1118 | (query (plist-get info :query))
1119 | (match-str (car (consult--command-split query))))
1120 | (consult-mu--view msg nil consult-mu-mark-viewed-as-read match-str)
1121 | (consult-mu-overlays-toggle consult-mu-view-buffer-name)))
1122 |
1123 | (defun consult-mu--reply (msg &optional wide-reply)
1124 | "Reply to MSG using `mu4e-compose-reply'.
1125 |
1126 | If WIDE-REPLY is non-nil use wide-reply \(a.k.a. reply all\) with
1127 | `mu4e-compose-wide-reply'."
1128 | (let ((msgid (plist-get msg :message-id)))
1129 | (when-let ((buf (mu4e-get-headers-buffer consult-mu-headers-buffer-name t)))
1130 | (with-current-buffer buf
1131 | (goto-char (point-min))
1132 | (setq mu4e-view-buffer-name consult-mu-view-buffer-name)))
1133 |
1134 |
1135 | (with-current-buffer consult-mu-headers-buffer-name
1136 | (mu4e-headers-goto-message-id msgid)
1137 | (if (not wide-reply)
1138 | (mu4e-compose-reply)
1139 | (mu4e-compose-wide-reply)))))
1140 |
1141 | (defun consult-mu--reply-action (cand &optional wide-reply)
1142 | "Reply to CAND.
1143 |
1144 | This is a wrapper function around `consult-mu--reply'. It passes
1145 | relevant message plist, from CAND, as well as WIDE-REPLY to
1146 | `consult-mu--reply'.
1147 |
1148 | To use this as the default action for `consult-mu', set
1149 | `consult-mu-default-action' to \=#'consult-mu--reply-action."
1150 | (let* ((info (cdr cand))
1151 | (msg (plist-get info :msg))
1152 | (wide-reply (or wide-reply
1153 | (pcase consult-mu-use-wide-reply
1154 | ('ask (y-or-n-p "Reply All?"))
1155 | ('nil nil)
1156 | ('t t)))))
1157 | (consult-mu--reply msg wide-reply)))
1158 |
1159 | (defun consult-mu--forward (msg)
1160 | "Forward the MSG using `mu4e-compose-forward'."
1161 | (let ((msgid (plist-get msg :message-id)))
1162 | (when-let ((buf (mu4e-get-headers-buffer consult-mu-headers-buffer-name t)))
1163 | (with-current-buffer buf
1164 | (goto-char (point-min))
1165 | (setq mu4e-view-buffer-name consult-mu-view-buffer-name)))
1166 | (with-current-buffer consult-mu-headers-buffer-name
1167 | (mu4e-headers-goto-message-id msgid)
1168 | (mu4e-compose-forward))))
1169 |
1170 | (defun consult-mu--forward-action (cand)
1171 | "Forward CAND.
1172 |
1173 | This is a wrapper function around `consult-mu--forward'. It passes
1174 | the relevant message plist, from CAND to `consult-mu--forward'.
1175 |
1176 | To use this as the default action for `consult-mu', set
1177 | `consult-mu-default-action' to \=#'consult-mu--forward-action."
1178 | (let* ((info (cdr cand))
1179 | (msg (plist-get info :msg)))
1180 | (consult-mu--forward msg)))
1181 |
1182 | (defun consult-mu--get-split-style-character (&optional style)
1183 | "Get the character for consult async split STYLE.
1184 |
1185 | STYLE defaults to `consult-async-split-style'."
1186 | (let ((style (or style consult-async-split-style 'none)))
1187 | (or (char-to-string (plist-get (alist-get style consult-async-split-styles-alist) :initial))
1188 | (char-to-string (plist-get (alist-get style consult-async-split-styles-alist) :separator))
1189 | "")))
1190 |
1191 | (defun consult-mu--dynamic-format-candidate (cand highlight)
1192 | "Format minibuffer candidate, CAND.
1193 |
1194 | CAND is the minibuffer completion candidate \(a mu4e message collected by
1195 | `consult-mu--dynamic-collection'\). If HIGHLIGHT is non-nil, it is
1196 | highlighted with `consult-mu-highlight-match-face'."
1197 |
1198 | (let* ((string (car cand))
1199 | (info (cadr cand))
1200 | (msg (plist-get info :msg))
1201 | (query (plist-get info :query))
1202 | (match-str (if (stringp query) (consult--split-escaped (car (consult--command-split query))) nil))
1203 | (headers-template (consult-mu--headers-template))
1204 | (str (if headers-template
1205 | (consult-mu--expand-headers-template msg headers-template)
1206 | string))
1207 | (str (propertize str :msg msg :query query :type :dynamic)))
1208 | (if (and consult-mu-highlight-matches highlight)
1209 | (cond
1210 | ((listp match-str)
1211 | (mapc (lambda (match) (setq str (consult-mu--highlight-match match str t))) match-str))
1212 | ((stringp match-str)
1213 | (setq str (consult-mu--highlight-match match-str str t))))
1214 | str)
1215 | (when msg
1216 | (cons str (list :msg msg :query query :type :dynamic)))))
1217 |
1218 | (defun consult-mu--dynamic-collection (input)
1219 | "Dynamically collect mu4e search results.
1220 |
1221 | INPUT is the user input. It is passed as QUERY to
1222 | `consult-mu--update-headers', appends the result to
1223 | `consult-mu-headers-buffer-name' and returns a list of found
1224 | messages."
1225 |
1226 | (save-excursion
1227 | (pcase-let* ((`(,_arg . ,opts) (consult--command-split input)))
1228 | (consult-mu--update-headers (substring-no-properties input) nil nil :dynamic)
1229 | (if (or (member "-g" opts) (member "--group" opts))
1230 | (cond
1231 | ((member "-g" opts)
1232 | (setq consult-mu--override-group (intern (or (nth (+ (cl-position "-g" opts :test 'equal) 1) opts) "nil"))))
1233 | ((member "--group" opts)
1234 | (setq consult-mu--override-group (intern (or (nth (+ (cl-position "--group" opts :test 'equal) 1) opts) "nil")))))
1235 | (setq consult-mu--override-group nil)))
1236 |
1237 | (with-current-buffer consult-mu-headers-buffer-name
1238 | (goto-char (point-min))
1239 | (remove nil
1240 | (cl-loop until (eobp)
1241 | collect (consult-mu--dynamic-format-candidate (list (buffer-substring (point) (line-end-position)) (list :msg (ignore-errors (mu4e-message-at-point)) :query input)) t)
1242 | do (forward-line 1))))))
1243 |
1244 | (defun consult-mu--dynamic-state ()
1245 | "State function for `consult-mu' candidates.
1246 | This is passed as STATE to `consult--read' and is used to preview or do
1247 | other actions on the candidate."
1248 | (lambda (action cand)
1249 | (let ((preview (consult--buffer-preview)))
1250 | (pcase action
1251 | ('preview
1252 | (if cand
1253 | (when-let* ((info (cdr cand))
1254 | (msg (plist-get info :msg))
1255 | (query (plist-get info :query))
1256 | (msgid (substring-no-properties (plist-get msg :message-id)))
1257 | (match-str (car (consult--command-split query)))
1258 | (match-str (car (consult--command-split query)))
1259 | (mu4e-headers-buffer-name consult-mu-headers-buffer-name)
1260 | (buffer consult-mu-view-buffer-name))
1261 | ;;(get-buffer-create consult-mu-view-buffer-name)
1262 | (add-to-list 'consult-mu--view-buffers-list buffer)
1263 | (funcall preview action
1264 | (consult-mu--view msg t consult-mu-mark-previewed-as-read match-str))
1265 | (with-current-buffer consult-mu-view-buffer-name
1266 | (unless (one-window-p) (delete-other-windows))))))
1267 | ('return
1268 | (save-mark-and-excursion
1269 | (consult-mu--execute-all-marks))
1270 | (setq consult-mu--override-group nil)
1271 | cand)))))
1272 |
1273 | (defun consult-mu--dynamic (prompt collection &optional initial)
1274 | "Query mu4e messages dyunamically.
1275 |
1276 | This is a non-interactive internal function. For the interactive version
1277 | see `consult-mu'.
1278 |
1279 | It runs the `consult-mu--dynamic-collection' to do a `mu4e-search' with
1280 | user input \(e.g. INITIAL\) and returns the results \(list of messages
1281 | found\) as a completion table in minibuffer.
1282 |
1283 | The completion table gets dynamically updated as the user types in the
1284 | minibuffer. Each candidate in the minibuffer is formatted by
1285 | `consult-mu--dynamic-format-candidate' to add annotation and other info to
1286 | the candidate.
1287 |
1288 | Description of Arguments:
1289 | PROMPT the prompt in the minibuffer
1290 | \(passed as PROMPT to `consult--read'\)
1291 | COLLECTION a colection function passed to `consult--dynamic-collection'.
1292 | INITIAL an optional arg for the initial input in the minibuffer.
1293 | \(passed as INITITAL to `consult--read'\)
1294 |
1295 | commandline arguments/options \(see `mu find --help` in the command line
1296 | for details\) can be passed to the minibuffer input similar to
1297 | `consult-grep'. For example the user can enter:
1298 |
1299 | “#paper -- --maxnum 200 --sortfield from --reverse”
1300 |
1301 | this will search for mu4e messages with the query “paper”, retrives a
1302 | maximum of 200 messages and sorts them by the “from:” field and reverses
1303 | the sort direction (opposite of `consult-mu-search-sort-field').
1304 |
1305 | Note that some command line arguments are not supported by mu4e (for
1306 | example sorting based on cc: or bcc: fields are not supported in
1307 | `mu4e-search-sort-field')
1308 |
1309 | Also, the results can further be narrowed by
1310 | `consult-async-split-style' \(e.g. by entering “#” when
1311 | `consult-async-split-style' is set to \='perl\).
1312 |
1313 | For example:
1314 |
1315 | “#paper -- --maxnum 200 --sortfield from --reverse#accepted”
1316 |
1317 | will retrieve the message as the example above, then narrows down the
1318 | candidates to those that that match “accepted”."
1319 | (consult--read
1320 | (consult--dynamic-collection (or collection #'consult-mu--dynamic-collection))
1321 | :prompt (or prompt "Select: ")
1322 | :lookup (consult-mu--lookup)
1323 | :state (funcall #'consult-mu--dynamic-state)
1324 | :initial initial
1325 | :group #'consult-mu--group
1326 | :add-history (append (list (thing-at-point 'symbol))
1327 | consult-mu-saved-searches-dynamic)
1328 | :history '(:input consult-mu--history)
1329 | :require-match t
1330 | :category 'consult-mu-messages
1331 | :preview-key consult-mu-preview-key
1332 | :sort nil))
1333 |
1334 | (defun consult-mu-dynamic (&optional initial noaction)
1335 | "Lists results of `mu4e-search' dynamically.
1336 |
1337 | This is an interactive wrapper function around `consult-mu--dynamic'. It
1338 | queries the user for a search term in the minibuffer, then fetches a list
1339 | of messages for the entered search term as a minibuffer completion table
1340 | for selection. The list of candidates in the completion table are
1341 | dynamically updated as the user changes the entry.
1342 |
1343 | Upon selection of a candidate either
1344 | - the candidate is returned if NOACTION is non-nil
1345 | or
1346 | - the candidate is passed to `consult-mu-action' if NOACTION is nil.
1347 |
1348 | Additional commandline arguments can be passed in the minibuffer entry by
1349 | typing “--” followed by command line arguments.
1350 |
1351 | For example, the user can enter:
1352 |
1353 | “#consult-mu -- -n 10”
1354 |
1355 | this will run a `mu4e-search' with the query “consult-mu” and changes the
1356 | search limit \(i.e. `mu4e-search-results-limit' to 10\).
1357 |
1358 |
1359 | Also, the results can further be narrowed by
1360 | `consult-async-split-style' \(e.g. by entering “#” when
1361 | `consult-async-split-style' is set to \='perl\).
1362 |
1363 | For example:
1364 |
1365 | “#consult-mu -- -n 10#github”
1366 |
1367 | will retrieve the messages as the example above, then narrows down the
1368 | completion table to candidates that match “github”.
1369 |
1370 | INITIAL is an optional arg for the initial input in the minibuffer.
1371 | \(passed as INITITAL to `consult-mu--dynamic'\)
1372 |
1373 | For more details on consult--async functionalities, see `consult-grep' and
1374 | the official manual of consult, here:
1375 | URL `https://github.com/minad/consult'"
1376 | (interactive)
1377 | (save-mark-and-excursion
1378 | (consult-mu--execute-all-marks))
1379 | (let* ((sel
1380 | (consult-mu--dynamic (concat "[" (propertize "consult-mu-dynamic" 'face 'consult-mu-sender-face) "]" " Search For: ") #'consult-mu--dynamic-collection initial)))
1381 | (save-mark-and-excursion
1382 | (consult-mu--execute-all-marks))
1383 | (if noaction
1384 | sel
1385 | (progn
1386 | (funcall consult-mu-action sel)
1387 | sel))))
1388 |
1389 | (defun consult-mu--async-format-candidate (string input highlight)
1390 | "Formats minibuffer candidates for `consult-mu-async'.
1391 |
1392 | STRING is the output retrieved from `mu find INPUT ...` in the command line.
1393 | INPUT is the query from the user.
1394 |
1395 | If HIGHLIGHT is t, input is highlighted with
1396 | `consult-mu-highlight-match-face' in the minibuffer."
1397 |
1398 | (let* ((query input)
1399 | (parts (split-string (replace-regexp-in-string "^\\\\->\s\\|^\\\/->\s" "" string) consult-mu-delimiter))
1400 | (msgid (car parts))
1401 | (date (date-to-time (cadr parts)))
1402 | (sender (cadr (cdr parts)))
1403 | (sender (consult-mu--contact-string-to-plist sender))
1404 | (receiver (cadr (cdr (cdr parts))))
1405 | (receiver (consult-mu--contact-string-to-plist receiver))
1406 | (subject (cadr (cdr (cdr (cdr parts)))))
1407 | (size (string-to-number (cadr (cdr (cdr (cdr (cdr parts)))))))
1408 | (flags (consult-mu-flags-to-string (cadr (cdr (cdr (cdr (cdr (cdr parts))))))))
1409 | (tags (cadr (cdr (cdr (cdr (cdr (cdr (cdr parts))))))))
1410 | (priority (cadr (cdr (cdr (cdr (cdr (cdr (cdr (cdr parts)))))))))
1411 | (cc (cadr (cdr (cdr (cdr (cdr (cdr (cdr (cdr (cdr parts))))))))))
1412 | (cc (consult-mu--contact-string-to-plist cc))
1413 | (bcc (cadr (cdr (cdr (cdr (cdr (cdr (cdr (cdr (cdr (cdr parts)))))))))))
1414 | (bcc (consult-mu--contact-string-to-plist bcc))
1415 | (path (cadr (cdr (cdr (cdr (cdr (cdr (cdr (cdr (cdr (cdr (cdr parts))))))))))))
1416 | (msg (list :subject subject :date date :from sender :to receiver :size size :message-id msgid :flags flags :tags tags :priority priority :cc cc :bcc bcc :path path))
1417 | (match-str (if (stringp input) (consult--split-escaped (car (consult--command-split query))) nil))
1418 | (headers-template (consult-mu--headers-template))
1419 | (str (if headers-template
1420 | (consult-mu--expand-headers-template msg headers-template)
1421 | (format "%s\s\s%s\s\s%s\s\s%s\s\s%s\s\s%s"
1422 | (propertize (consult-mu--set-string-width
1423 | (format-time-string "%x" date) 10)
1424 | 'face 'consult-mu-date-face)
1425 | (propertize (consult-mu--set-string-width (consult-mu--contact-name-or-email sender) (floor (* (frame-width) 0.2))) 'face 'consult-mu-sender-face)
1426 | (propertize (consult-mu--set-string-width subject (floor (* (frame-width) 0.55))) 'face 'consult-mu-subject-face)
1427 | (propertize (file-size-human-readable size) 'face 'consult-mu-size-face)
1428 | (propertize (format "%s" flags) 'face 'consult-mu-flags-face)
1429 | (propertize (if tags (format "%s" tags) nil) 'face 'consult-mu-tags-face))))
1430 | (str (propertize str :msg msg :query query :type :async)))
1431 | (if (and consult-mu-highlight-matches highlight)
1432 | (cond
1433 | ((listp match-str)
1434 | (mapc (lambda (match) (setq str (consult-mu--highlight-match match str t))) match-str))
1435 | ((stringp match-str)
1436 | (setq str (consult-mu--highlight-match match-str str t))))
1437 | str)
1438 | (cons str (list :msg msg :query query :type :async))))
1439 |
1440 | (defun consult-mu--async-state ()
1441 | "State function for `consult-mu-async' candidates.
1442 |
1443 | This is passed as STATE to `consult--read' and is used to preview or do
1444 | other actions on the candidate."
1445 | (lambda (action cand)
1446 | (let ((preview (consult--buffer-preview)))
1447 | (pcase action
1448 | ('preview
1449 | (if cand
1450 | (when-let* ((info (cdr cand))
1451 | (msg (plist-get info :msg))
1452 | (msgid (substring-no-properties (plist-get msg :message-id)))
1453 | (query (plist-get info :query))
1454 | (match-str (car (consult--command-split query)))
1455 | (mu4e-headers-buffer-name consult-mu-headers-buffer-name)
1456 | (buffer consult-mu-view-buffer-name))
1457 | (add-to-list 'consult-mu--view-buffers-list buffer)
1458 | (funcall preview action
1459 | (consult-mu--view msg t consult-mu-mark-previewed-as-read match-str))
1460 | (with-current-buffer consult-mu-view-buffer-name
1461 | (unless (one-window-p) (delete-other-windows))))))
1462 | ('return
1463 | (save-mark-and-excursion
1464 | (consult-mu--execute-all-marks))
1465 | cand)))))
1466 |
1467 | (defun consult-mu--async-transform (input)
1468 | "Add annotation to minibuffer candiates for `consult-mu'.
1469 |
1470 | Format each candidates with `consult-gh--repo-format' and INPUT."
1471 | (lambda (cands)
1472 | (cl-loop for cand in cands
1473 | collect
1474 | (consult-mu--async-format-candidate cand input t))))
1475 |
1476 | (defun consult-mu--async-builder (input)
1477 | "Build mu command line for searching messages by INPUT (e.g. `mu find INPUT)`."
1478 | (pcase-let* ((consult-mu-args (append consult-mu-args '("find")))
1479 | (cmd (consult--build-args consult-mu-args))
1480 | (`(,arg . ,opts) (consult--command-split input))
1481 | (flags (append cmd opts))
1482 | (sortfield (cond
1483 | ((member "-s" flags) (nth (+ (cl-position "-s" opts :test 'equal) 1) flags))
1484 | ((member "--sortfield" flags) (nth (+ (cl-position "--sortfield" flags :test 'equal) 1) flags))
1485 | (t (substring (symbol-name consult-mu-search-sort-field) 1))))
1486 | (threads (if (not (equal sortfield :date)) nil (or (member "-t" flags) (member "--threads" flags) mu4e-search-threads)))
1487 | (skip-dups (or (member "-u" flags) (member "--skip-dups" flags) mu4e-search-skip-duplicates))
1488 | (include-related (or (member "-r" flags) (member "--include-related" flags) mu4e-search-include-related)))
1489 | (if (or (member "-g" flags) (member "--group" flags))
1490 | (cond
1491 | ((member "-g" flags)
1492 | (setq consult-mu--override-group (intern (or (nth (+ (cl-position "-g" opts :test 'equal) 1) opts) "nil")))
1493 | (setq opts (remove "-g" (remove (nth (+ (cl-position "-g" opts :test 'equal) 1) opts) opts))))
1494 | ((member "--group" flags)
1495 | (setq consult-mu--override-group (intern (or (nth (+ (cl-position "--group" opts :test 'equal) 1) opts) "nil")))
1496 | (setq opts (remove "--group" (remove (nth (+ (cl-position "--group" opts :test 'equal) 1) opts) opts)))))
1497 | (setq consult-mu--override-group nil))
1498 | (setq opts (append opts (list "--nocolor")))
1499 | (setq opts (append opts (list "--fields" (format "i%sd%sf%st%ss%sz%sg%sx%sp%sc%sh%sl"
1500 | consult-mu-delimiter consult-mu-delimiter consult-mu-delimiter consult-mu-delimiter consult-mu-delimiter consult-mu-delimiter consult-mu-delimiter consult-mu-delimiter consult-mu-delimiter consult-mu-delimiter consult-mu-delimiter))))
1501 | (unless (or (member "-s" flags) (member "--sortfiled" flags))
1502 | (setq opts (append opts (list "--sortfield" (substring (symbol-name consult-mu-search-sort-field) 1)))))
1503 | (if threads (setq opts (append opts (list "--thread"))))
1504 | (if skip-dups (setq opts (append opts (list "--skip-dups"))))
1505 | (if include-related (setq opts (append opts (list "--include-related"))))
1506 | (cond
1507 | ((and (member "-n" flags) (< (string-to-number (nth (+ (cl-position "-n" opts :test 'equal) 1) opts)) 0))
1508 | (setq opts (remove "-n" (remove (nth (+ (cl-position "-n" opts :test 'equal) 1) opts) opts))))
1509 | ((and (member "--maxnum" flags) (< (string-to-number (nth (+ (cl-position "--maxnum" opts :test 'equal) 1) opts)) 0))
1510 | (setq opts (remove "--maxnum" (remove (nth (+ (cl-position "--maxnum" opts :test 'equal) 1) opts) opts)))))
1511 | (unless (or (member "-n" flags) (member "--maxnum" flags))
1512 | (if (and consult-mu-maxnum (> consult-mu-maxnum 0))
1513 | (setq opts (append opts (list "--maxnum" (format "%s" consult-mu-maxnum))))))
1514 |
1515 | (pcase consult-mu-search-sort-direction
1516 | ('descending
1517 | (if (or (member "-z" flags) (member "--reverse" flags))
1518 | (setq opts (remove "-z" (remove "--reverse" opts)))
1519 | (setq opts (append opts (list "--reverse")))))
1520 | ('ascending)
1521 | (_))
1522 | (pcase-let* ((`(,re . ,hl) (funcall consult--regexp-compiler arg 'basic t)))
1523 | (when re
1524 | (cons (append cmd
1525 | (list (string-join re " "))
1526 | opts)
1527 | hl)))))
1528 |
1529 | (defun consult-mu--async (prompt builder &optional initial)
1530 | "Query mu4e messages asynchronously.
1531 |
1532 | This is a non-interactive internal function. For the interactive
1533 | version, see `consult-mu-async'.
1534 |
1535 | It runs the command line from `consult-mu--async-builder' in an async
1536 | process and returns the results (list of messages) as a completion table
1537 | in minibuffer that will be passed to `consult--read'. The completion
1538 | table gets dynamically updated as the user types in the minibuffer. Each
1539 | candidate in the minibuffer is formatted by `consult-mu--async-transform'
1540 | to add annotation and other info to the candidate.
1541 |
1542 | Description of Arguments:
1543 |
1544 | PROMPT the prompt in the minibuffer
1545 | \(passed as PROMPT to `consult--red'\)
1546 | BUILDER an async builder function passed to `consult--async-command'
1547 | INITIAL an optional arg for the initial input in the minibuffer
1548 | \(passed as INITITAL to `consult--read'\)
1549 |
1550 | commandline arguments/options \(see `mu find --help` in the command line
1551 | for details\) can be passed to the minibuffer input similar to
1552 | `consult-grep'. For example the user can enter:
1553 |
1554 | “#paper -- --maxnum 200 --sortfield from --reverse”
1555 |
1556 | this will search for mu4e messages with the query “paper”, retrives a
1557 | maximum of 200 messages sorts them by the “from:” field and reverses the
1558 | sort direction (opposite of `consult-mu-search-sort-field').
1559 |
1560 | Also, the results can further be narrowed by
1561 | `consult-async-split-style' \(e.g. by entering “#” when
1562 | `consult-async-split-style' is set to \='perl\).
1563 |
1564 | For example:
1565 |
1566 | `#paper -- --maxnum 200 --sortfield from --reverse#accepted'
1567 |
1568 | will retrieve the message as the example above, then narrows down the
1569 | completion table to candidates that match “accepted”."
1570 | (consult--read
1571 | (consult--process-collection builder
1572 | :transform (consult--async-transform-by-input #'consult-mu--async-transform))
1573 | :prompt prompt
1574 | :lookup (consult-mu--lookup)
1575 | :state (funcall #'consult-mu--async-state)
1576 | :initial initial
1577 | :group #'consult-mu--group
1578 | :add-history (append (list (thing-at-point 'symbol))
1579 | consult-mu-saved-searches-async)
1580 | :history '(:input consult-mu--history)
1581 | :require-match t
1582 | :category 'consult-mu-messages
1583 | :preview-key consult-mu-preview-key
1584 | :sort nil))
1585 |
1586 | (defun consult-mu-async (&optional initial noaction)
1587 | "Lists results of `mu find` Asynchronously.
1588 |
1589 | This is an interactive wrapper function around `consult-mu--async'. It
1590 | queries the user for a search term in the minibuffer, then fetches a list
1591 | of messages for the entered search term as a minibuffer completion table
1592 | for selection. The list of candidates in the completion table are
1593 | dynamically updated as the user changes the entry.
1594 |
1595 | Upon selection of a candidate either
1596 | - the candidate is returned if NOACTION is non-nil
1597 | or
1598 | - the candidate is passed to `consult-mu-action' if NOACTION is nil.
1599 |
1600 | Additional commandline arguments can be passed in the minibuffer entry by
1601 | typing `--` followed by command line arguments.
1602 |
1603 | For example the user can enter:
1604 |
1605 | `#consult-mu -- -n 10'
1606 |
1607 | this will run a `mu4e-search' with the query \"consult-my\" and changes the
1608 | search limit (i.e. `mu4e-search-results-limit' to 10.
1609 |
1610 |
1611 | Also, the results can further be narrowed by `consult-async-split-style'
1612 | \(e.g. by entering “#” when `consult-async-split-style' is set to \='perl\).
1613 |
1614 | For example:
1615 |
1616 | “#consult-mu -- -n 10#github”
1617 |
1618 | will retrieve the message as the example above, then narrows down the
1619 | completion table to candidates that match “github”.
1620 |
1621 | INITIAL is an optional arg for the initial input in the minibuffer.
1622 | \(passed as INITITAL to `consult-mu--async'\).
1623 |
1624 | For more details on consult--async functionalities, see `consult-grep' and
1625 | the official manual of consult, here:
1626 | URL `https://github.com/minad/consult'
1627 |
1628 | Note that this is the async search directly using the commandline `mu`
1629 | command and not mu4e-search. As a result, mu4e-headers buffers are not
1630 | created until a single message is selected \(or interacted with using
1631 | embark, etc.\) Previews are shown in a mu4e-view buffer \(see
1632 | `consult-mu-view-buffer-name'\) attached to an empty mu4e-headers buffer
1633 | \(i.e. `consult-mu-headers-buffer-name'\). This allows quick retrieval of
1634 | many messages \(tens of thousands\) and previews, but not opening the
1635 | results in a mu4e-headers buffer. If you want ot open the results in a
1636 | mu4e-headers buffer for other work flow, then you should use the
1637 | dynamically collected function `consult-mu' which is slower if searching
1638 | for many emails but allows follow up interactions in a mu4e-headers
1639 | buffer."
1640 | (interactive)
1641 | (save-mark-and-excursion
1642 | (consult-mu--execute-all-marks))
1643 | (let* ((sel
1644 | (consult-mu--async (concat "[" (propertize "consult-mu async" 'face 'consult-mu-sender-face) "]" " Search For: ") #'consult-mu--async-builder initial))
1645 | (info (cdr sel))
1646 | (msg (plist-get info :msg))
1647 | (query (plist-get info :query)))
1648 | (save-mark-and-excursion
1649 | (consult-mu--execute-all-marks))
1650 | (if noaction
1651 | sel
1652 | (progn
1653 | (consult-mu--update-headers query t msg :async))
1654 | (funcall consult-mu-action sel)
1655 | sel)))
1656 |
1657 | (defun consult-mu (&optional initial noaction)
1658 | "Default interactive command.
1659 |
1660 | This is a wrapper function that calls `consult-mu-default-command' with
1661 | INITIAL and NOACTION.
1662 |
1663 | For example, the `consult-mu-default-command can be set to
1664 | `#'consult-mu-dynamic' sets the default behavior to dynamic collection
1665 | `#'consult-mu-async' sets the default behavior to async collection"
1666 |
1667 | (interactive "P")
1668 | (funcall consult-mu-default-command initial noaction))
1669 |
1670 | ;;; provide `consult-mu' module
1671 | (provide 'consult-mu)
1672 |
1673 | ;;; consult-mu.el ends here
1674 |
--------------------------------------------------------------------------------
/extras/consult-mu-compose-embark.el:
--------------------------------------------------------------------------------
1 | ;;; consult-mu-compose-embark.el --- Emabrk Actions for consult-mu-compose -*- lexical-binding: t -*-
2 |
3 | ;; Copyright (C) 2021-2023
4 |
5 | ;; Author: Armin Darvish
6 | ;; Maintainer: Armin Darvish
7 | ;; Created: 2023
8 | ;; Version: 1.0
9 | ;; Package-Requires: ((emacs "28.0") (consult "2.0"))
10 | ;; Homepage: https://github.com/armindarvish/consult-mu
11 | ;; Keywords: convenience, matching, tools, email
12 | ;; Homepage: https://github.com/armindarvish/consult-mu
13 |
14 | ;; SPDX-License-Identifier: GPL-3.0-or-later
15 |
16 | ;; This file is free software: you can redistribute it and/or modify
17 | ;; it under the terms of the GNU General Public License as published
18 | ;; by the Free Software Foundation, either version 3 of the License,
19 | ;; or (at your option) any later version.
20 | ;;
21 | ;; This file is distributed in the hope that it will be useful,
22 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
23 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 | ;; GNU General Public License for more details.
25 | ;;
26 | ;; You should have received a copy of the GNU General Public License
27 | ;; along with this file. If not, see .
28 |
29 |
30 | ;;; Commentary:
31 |
32 | ;; This package provides an alternative interactive serach interface for
33 | ;; mu and mu4e (see URL `https://djcbsoftware.nl/code/mu/mu4e.html').
34 | ;; It uses a consult-based minibuffer completion for searching and
35 | ;; selecting, and marking emails, as well as additional utilities for
36 | ;; composing emails and more.
37 |
38 | ;; This package requires mu4e version "1.10.8" or later.
39 |
40 |
41 | ;;; Code:
42 |
43 | ;;; Requirements
44 | (require 'embark)
45 | (require 'consult-mu)
46 | (require 'consult-mu-embark)
47 |
48 | (defun consult-mu-compose-embark-attach-file (cand)
49 | "Run `consult-mu-attach-files' on CAND."
50 | (funcall (apply-partially #'consult-mu-compose-attach cand)))
51 |
52 | ;;; add consult-mu-attach to embark-file-map
53 | (defun consult-mu-compose-embark-bind-attach-file-key (&optional key)
54 | "Binds `consult-mu-embark-attach-file-key'.
55 |
56 | Bind `consult-mu-embark-attach-file-key' to
57 | `consult-mu-compose-embark-attach-file' in `embark-file-map'. If KEY is
58 | non-nil binds KEY instead of `consult-mu-embark-attach-file-key'."
59 | (if-let ((keyb (or key (kbd consult-mu-embark-attach-file-key))))
60 | (define-key embark-file-map keyb #'consult-mu-compose-embark-attach-file)))
61 |
62 | (consult-mu-compose-embark-bind-attach-file-key)
63 |
64 | ;; change the default action on `consult-mu-contacts category.
65 | (add-to-list 'embark-default-action-overrides '((file . consult-mu-compose--read-file-attach) . consult-mu-compose-attach))
66 | (add-to-list 'embark-default-action-overrides '((file . consult-mu-compose-attach) . consult-mu-compose-attach))
67 |
68 | ;;; Provide `consult-mu-compose-embark' module
69 |
70 | (provide 'consult-mu-compose-embark)
71 |
72 | ;;; consult-mu-compose-embark.el ends here
73 |
--------------------------------------------------------------------------------
/extras/consult-mu-compose.el:
--------------------------------------------------------------------------------
1 | ;;; consult-mu-compose.el --- Consult Mu4e asynchronously -*- lexical-binding: t -*-
2 |
3 | ;; Copyright (C) 2023 Armin Darvish
4 |
5 | ;; Author: Armin Darvish
6 | ;; Maintainer: Armin Darvish
7 | ;; Created: 2023
8 | ;; Version: 1.0
9 | ;; Package-Requires: ((emacs "28.0") (consult "2.0"))
10 | ;; Homepage: https://github.com/armindarvish/consult-mu
11 | ;; Keywords: convenience, matching, tools, email
12 | ;; Homepage: https://github.com/armindarvish/consult-mu
13 |
14 | ;; SPDX-License-Identifier: GPL-3.0-or-later
15 |
16 | ;; This file is free software: you can redistribute it and/or modify
17 | ;; it under the terms of the GNU General Public License as published
18 | ;; by the Free Software Foundation, either version 3 of the License,
19 | ;; or (at your option) any later version.
20 | ;;
21 | ;; This file is distributed in the hope that it will be useful,
22 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
23 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 | ;; GNU General Public License for more details.
25 | ;;
26 | ;; You should have received a copy of the GNU General Public License
27 | ;; along with this file. If not, see .
28 |
29 |
30 | ;;; Commentary:
31 |
32 | ;; This package provides an alternative interactive serach interface for
33 | ;; mu and mu4e (see URL `https://djcbsoftware.nl/code/mu/mu4e.html').
34 | ;; It uses a consult-based minibuffer completion for searching and
35 | ;; selecting, and marking emails, as well as additional utilities for
36 | ;; composing emails and more.
37 |
38 | ;; This package requires mu4e version "1.10.8" or later.
39 |
40 | ;;; Code:
41 |
42 | (require 'consult-mu)
43 |
44 | ;;; Customization Variables
45 | (defcustom consult-mu-compose-use-dired-attachment 'in-dired
46 | "Use a Dired buffer for multiple file attachment?
47 |
48 | If set to \='in-dired uses `dired' buffer and `dired' marks only when inside
49 | a `dired' buffer. If \='t, a `dired' buffer will be used for selecting attachment files similar to what Doom Emacs does:
50 | URL `https://github.com/doomemacs/doomemacs/blob/bea81278fd2ecb65db6a63dbcd6db2f52921ee41/modules/email/mu4e/autoload/email.el#L272'.
51 |
52 | If \='nil, consult-mu uses minibuffer completion for selection files to
53 | attach, even if inside a `dired' buffer.
54 |
55 | By default this is set to \='in-dired."
56 | :group 'consult-mu
57 | :type '(choice (const :tag "Only use Dired if inside Dired Buffer" 'in-dired)
58 | (const :tag "Always use Dired" t)
59 | (const :tag "Never use Dired" nil)))
60 |
61 | (defcustom consult-mu-large-file-warning-threshold large-file-warning-threshold
62 | "Threshold for size of file to require confirmation for preview.
63 |
64 | This is used when selecting files to attach to emails. Files larger than this value in size will require user confirmation before previewing the file. Default value is set by `large-file-warning-threshold'. If nil, no cofnirmation is required."
65 | :group 'consult-mu
66 | :type '(choice integer (const :tag "Never request confirmation" nil)))
67 |
68 |
69 | (defcustom consult-mu-compose-preview-key consult-mu-preview-key
70 | "Preview key for `consult-mu-compose'.
71 |
72 | This is similar to `consult-mu-preview-key' but explicitly for
73 | consult-mu-compose. It is recommended to set this to something other than
74 | \='any to avoid loading preview buffers for each file."
75 | :group 'consult-mu
76 | :type '(choice (const :tag "Any key" any)
77 | (list :tag "Debounced"
78 | (const :debounce)
79 | (float :tag "Seconds" 0.1)
80 | (const any))
81 | (const :tag "No preview" nil)
82 | (key :tag "Key")
83 | (repeat :tag "List of keys" key)))
84 |
85 | (defcustom consult-mu-embark-attach-file-key nil
86 | "Embark key binding for interactive file attachement."
87 | :group 'consult-mu
88 | :type '(choice (key :tag "Key")
89 | (const :tag "no key binding" nil)))
90 |
91 | (defvar consult-mu-compose-attach-history nil
92 | "History variable for file attachment.
93 |
94 | It is used in `consult-mu-compose--read-file-attach'.")
95 |
96 | (defvar consult-mu-compose-current-draft-buffer nil
97 | "Store the buffer that is being edited.")
98 |
99 | (defun consult-mu-compose--read-file-attach (&optional initial)
100 | "Read files in the minibuffer to attach to an email.
101 |
102 | INITIAL is the initial input in the minibuffer."
103 | (consult--read (completion-table-in-turn #'completion--embedded-envvar-table
104 | #'completion--file-name-table)
105 | :prompt "Attach File: "
106 | :require-match t
107 | :category 'file
108 | :initial (or initial default-directory)
109 | :lookup (lambda (sel cands &rest args)
110 | (file-truename sel))
111 | :state (lambda (action cand)
112 | (let ((preview (consult--buffer-preview)))
113 | (pcase action
114 | ('preview
115 | (if cand
116 | (when (not (file-directory-p cand))
117 | (let* ((filename (file-truename cand))
118 | (filesize (float
119 | (file-attribute-size
120 | (file-attributes filename))))
121 | (confirm (if (and filename
122 | (>= filesize consult-mu-large-file-warning-threshold))
123 | (yes-or-no-p (format "File is %s Bytes. Do you really want to preview it?" filesize))
124 | t)))
125 | (if confirm
126 | (funcall preview action
127 | (find-file-noselect (file-truename cand))))))))
128 | ('return
129 | cand))))
130 | :preview-key consult-mu-compose-preview-key
131 | :add-history (list mu4e-attachment-dir)
132 | :history 'consult-mu-compose-attach-history))
133 |
134 | (defun consult-mu-compose--read-file-remove (&optional initial)
135 | "Select attached files to remove from email.
136 |
137 | INITIAL is the initial input in the minibuffer."
138 |
139 | (if-let ((current-files (pcase major-mode
140 | ('org-msg-edit-mode
141 | (org-msg-get-prop "attachment"))
142 | ((or 'mu4e-compose-mode 'message-mode)
143 | (goto-char (point-max))
144 | (cl-loop while (re-search-backward "<#part.*filename=\"\\(?1:.*\\)\"[[:ascii:][:nonascii:]]*?/part>" nil t)
145 | collect (match-string-no-properties 1)))
146 | (_
147 | (error "Not in a compose message buffer")
148 | nil))))
149 |
150 | (consult--read current-files
151 | :prompt "Remove File:"
152 | :category 'file
153 | :state (lambda (action cand)
154 | (let ((preview (consult--buffer-preview)))
155 | (pcase action
156 | ('preview
157 | (if cand
158 | (when (not (file-directory-p cand))
159 | (let* ((filename (file-truename cand))
160 | (filesize (float
161 | (file-attribute-size
162 | (file-attributes filename))))
163 | (confirm (if (and filename
164 | (>= filesize consult-mu-large-file-warning-threshold))
165 | (yes-or-no-p (format "File is %s Bytes. Do you really want to preview it?" filesize))
166 | t)))
167 | (if confirm
168 | (funcall preview action
169 | (find-file-noselect (file-truename cand))))))))
170 | ('return
171 | cand))))
172 | :preview-key consult-mu-compose-preview-key
173 | :initial initial)
174 | (progn
175 | (message "No files currently attached!")
176 | nil)))
177 |
178 | (defun consult-mu-compose-get-draft-buffer ()
179 | "Query user to select a mu4e compose draft buffer."
180 | (save-excursion
181 | (if (and (consult-mu-compose-get-current-buffers)
182 | (y-or-n-p "Attach the files to an existing compose buffer? "))
183 | (consult--read (consult-mu-compose-get-current-buffers)
184 | :prompt "Select Message Buffer: "
185 | :require-match nil
186 | :category 'consult-mu-messages
187 | :preview-key consult-mu-preview-key
188 | :lookup (lambda (sel cands &rest args)
189 | (or (get-buffer sel) sel))
190 | :state (lambda (action cand)
191 | (let ((preview (consult--buffer-preview)))
192 | (pcase action
193 | ('preview
194 | (if (and cand (buffer-live-p cand))
195 | (funcall preview action
196 | cand)))
197 | ('return
198 | cand))))))))
199 |
200 | (defun consult-mu-compose-get-current-buffers ()
201 | "Return a list of active compose message buffers."
202 | (let (buffers)
203 | (save-current-buffer
204 | (dolist (buffer (buffer-list t))
205 | (set-buffer buffer)
206 | (when (or (and (derived-mode-p 'message-mode)
207 | (null message-sent-message-via))
208 | (derived-mode-p 'org-msg-edit-mode)
209 | (derived-mode-p 'mu4e-compose-mode))
210 | (push (buffer-name buffer) buffers))))
211 | (nreverse buffers)))
212 |
213 | (defun consult-mu-compose--attach-files (files &optional mail-buffer &rest _args)
214 | "Attach FILES to email in MAIL-BUFFER compose buffer."
215 | (let ((files (if (stringp files) (list files) files))
216 | (mail-buffer (or mail-buffer (if (version<= mu4e-mu-version "1.12")
217 | (mu4e-compose 'new) (mu4e-compose-new)))))
218 | (with-current-buffer mail-buffer
219 | (pcase major-mode
220 | ('org-msg-edit-mode
221 | (save-excursion
222 | (let* ((new-files (delete-dups (append (org-msg-get-prop "attachment") files))))
223 | (org-msg-set-prop "attachment" new-files))
224 | (goto-last-change 0)
225 | (org-reveal)
226 | (consult-mu--pulse-line)))
227 | ((or 'mu4e-compose-mode 'message-mode)
228 | (save-excursion
229 | (dolist (file files)
230 | (goto-char (point-max))
231 | (unless (eq (current-column) 0)
232 | (insert "\n\n")
233 | (forward-line 2))
234 | (mail-add-attachment (file-truename file))
235 | (goto-last-change 0)
236 | (forward-line -2)
237 | (consult-mu--pulse-line))))
238 | (_
239 | (error "%s is not a compose buffer" (current-buffer)))))))
240 |
241 | (defun consult-mu-compose--remove-files (files &optional mail-buffer &rest _args)
242 | "Remove FILES from current attachments in MAIL-BUFFER."
243 | (let ((files (if (stringp files) (list files) files))
244 | (mail-buffer (or mail-buffer (current-buffer))))
245 | (with-current-buffer mail-buffer
246 | (save-excursion
247 | (pcase major-mode
248 | ('org-msg-edit-mode
249 | (let ((current-files (org-msg-get-prop "attachment"))
250 | (removed-files (list)))
251 | (mapcar (lambda (file)
252 | (when (member file current-files)
253 | (org-msg-set-prop "attachment" (delete-dups (remove file current-files)))
254 | (add-to-list 'removed-files file)
255 | (setq current-files (org-msg-get-prop "attachment"))
256 | (goto-last-change 0)
257 | (org-reveal)
258 | (consult-mu--pulse-line)))
259 | files)
260 | (message "file(s) %s detached" (mapconcat 'identity removed-files ","))))
261 | ('mu4e-compose-mode
262 | (let ((removed-files (list)))
263 | (mapcar (lambda (file)
264 | (goto-char (point-min))
265 | (while (re-search-forward (format "<#part.*filename=\"%s\"[[:ascii:][:nonascii:]]*?/part>" file) nil t)
266 | (replace-match "" nil nil)
267 | (setq removed-files (append removed-files (list file)))
268 | (goto-last-change 0)
269 | (consult-mu--pulse-line)
270 | (whitespace-cleanup)))
271 | files)
272 | (message "file(s) %s detached" (mapconcat 'identity removed-files ", ")))))))))
273 |
274 | (defun consult-mu-compose-attach (&optional files mail-buffer)
275 | "Attach FILES to email in MAIL-BUFFER interactively.
276 |
277 | MAIL-BUFFER defaults to `consult-mu-compose-current-draft-buffer'."
278 | (interactive)
279 | (let* ((consult-mu-compose-current-draft-buffer (cond
280 | ((or (derived-mode-p 'mu4e-compose-mode) (derived-mode-p 'org-msg-edit-mode) (derived-mode-p 'message-mode)) (current-buffer))
281 | ((derived-mode-p 'dired-mode)
282 | (and (bound-and-true-p dired-mail-buffer) (buffer-live-p dired-mail-buffer) dired-mail-buffer))
283 | (t
284 | consult-mu-compose-current-draft-buffer)))
285 | (mail-buffer (or mail-buffer
286 | (and (buffer-live-p consult-mu-compose-current-draft-buffer) consult-mu-compose-current-draft-buffer)
287 | nil))
288 | (files (or files
289 | (if (and (derived-mode-p 'dired-mode) consult-mu-compose-use-dired-attachment)
290 | (delq nil
291 | (mapcar
292 | ;; don't attach directories
293 | (lambda (f) (if (file-directory-p f)
294 | nil
295 | f))
296 | (nreverse (dired-map-over-marks (dired-get-filename) nil))))
297 | (consult-mu-compose--read-file-attach files)))))
298 | (pcase major-mode
299 | ((or 'mu4e-compose-mode 'org-msg-edit-mode 'message-mode)
300 | (setq mail-buffer (current-buffer))
301 | (setq consult-mu-compose-current-draft-buffer mail-buffer)
302 | (cond
303 | ((stringp files)
304 | (cond
305 | ((and (not (file-directory-p files)) (file-truename files))
306 | (consult-mu-compose--attach-files (file-truename files) mail-buffer))
307 | ((and (file-directory-p files) (eq consult-mu-compose-use-dired-attachment 'always))
308 | (progn
309 | (split-window-sensibly)
310 | (with-current-buffer (dired files)
311 | (setq-local dired-mail-buffer mail-buffer))))
312 | ((and (file-directory-p files) (not (eq consult-mu-compose-use-dired-attachment 'always)))
313 | (progn
314 | (while (file-directory-p files)
315 | (setq files (consult-mu-compose--read-file-attach files)))
316 | (consult-mu-compose--attach-files (file-truename files) mail-buffer)))))
317 | ((listp files)
318 | (consult-mu-compose--attach-files files mail-buffer))))
319 | ('dired-mode
320 | (setq mail-buffer (or (and (bound-and-true-p dired-mail-buffer) (buffer-live-p dired-mail-buffer) dired-mail-buffer)
321 | (consult-mu-compose-get-draft-buffer)
322 | (if (version<= mu4e-mu-version "1.12")
323 | (mu4e-compose 'new) (mu4e-compose-new))))
324 |
325 | (cond
326 | ((and mail-buffer (buffer-live-p mail-buffer)))
327 | ((stringp mail-buffer) (with-current-buffer (if (version<= mu4e-mu-version "1.12")
328 | (mu4e-compose 'new) (mu4e-compose-new))
329 | (save-excursion (message-goto-subject)
330 | (insert mail-buffer)
331 | (rename-buffer mail-buffer t)))
332 | (setq mail-buffer (get-buffer mail-buffer))))
333 |
334 | (if (and mail-buffer (buffer-live-p mail-buffer))
335 | (progn
336 | (setq-local dired-mail-buffer mail-buffer)
337 | (switch-to-buffer mail-buffer)
338 | (cond
339 | ((not files)
340 | (message "no files were selected!"))
341 | ((stringp files)
342 | (cond
343 | ((and (file-truename files) (not (file-directory-p files)))
344 | (consult-mu-compose--attach-files (file-truename files) mail-buffer))
345 | ((and (not consult-mu-compose-use-dired-attachment) (file-directory-p files))
346 | (progn
347 | (while (file-directory-p files)
348 | (setq files (consult-mu-compose--read-file-attach files)))
349 | (consult-mu-compose--attach-files (file-truename files) mail-buffer)))))
350 | ((listp files)
351 | (consult-mu-compose--attach-files files mail-buffer))))))
352 | (_
353 | (setq mail-buffer (or
354 | consult-mu-compose-current-draft-buffer
355 | (consult-mu-compose-get-draft-buffer)
356 | (if (version<= mu4e-mu-version "1.12")
357 | (mu4e-compose 'new) (mu4e-compose-new))))
358 | (cond
359 | ((and mail-buffer (buffer-live-p mail-buffer)))
360 | ((stringp mail-buffer) (with-current-buffer (if (version<= mu4e-mu-version "1.12")
361 | (mu4e-compose 'new) (mu4e-compose-new))
362 | (save-excursion (message-goto-subject)
363 | (insert mail-buffer)
364 | (rename-buffer mail-buffer t)))
365 | (setq mail-buffer (get-buffer mail-buffer))))
366 | (if (and mail-buffer (buffer-live-p mail-buffer))
367 | (progn
368 | (switch-to-buffer mail-buffer)
369 | (setq consult-mu-compose-current-draft-buffer mail-buffer)
370 | (cond
371 | ((and (not (file-directory-p files)) (file-truename files))
372 | (consult-mu-compose--attach-files (file-truename files) mail-buffer))
373 | ((and (file-directory-p files) (eq consult-mu-compose-use-dired-attachment 'always))
374 | (progn
375 | (split-window-sensibly)
376 | (with-current-buffer (dired files)
377 | (setq-local dired-mail-buffer mail-buffer)
378 | )))
379 | ((and (file-directory-p files) (not (eq consult-mu-compose-use-dired-attachment 'always)))
380 | (progn
381 | (while (file-directory-p files)
382 | (setq files (consult-mu-compose--read-file-attach files)))
383 | (consult-mu-compose--attach-files (file-truename files) mail-buffer)))
384 | ((listp files)
385 | (consult-mu-compose--attach-files files mail-buffer))))))))
386 | mail-buffer)
387 |
388 | (defun consult-mu-compose-detach (&optional file)
389 | "Remove FILE from email attachments interactively."
390 | (interactive)
391 | (save-mark-and-excursion
392 | (when-let (file (consult-mu-compose--read-file-remove))
393 | (consult-mu-compose--remove-files file))))
394 |
395 | ;;; provide `consult-mu-compose' module
396 | (provide 'consult-mu-compose)
397 |
398 | ;;; consult-mu-compose.el ends here
399 |
--------------------------------------------------------------------------------
/extras/consult-mu-contacts-embark.el:
--------------------------------------------------------------------------------
1 | ;;; consult-mu-contacts-embark.el --- Emabrk Actions for consult-mu-contacts -*- lexical-binding: t -*-
2 |
3 | ;; Copyright (C) 2021-2023
4 |
5 | ;; Author: Armin Darvish
6 | ;; Maintainer: Armin Darvish
7 | ;; Created: 2023
8 | ;; Version: 1.0
9 | ;; Package-Requires: ((emacs "28.0") (consult "2.0"))
10 | ;; Homepage: https://github.com/armindarvish/consult-mu
11 | ;; Keywords: convenience, matching, tools, email
12 | ;; Homepage: https://github.com/armindarvish/consult-mu
13 |
14 | ;; SPDX-License-Identifier: GPL-3.0-or-later
15 |
16 | ;; This file is free software: you can redistribute it and/or modify
17 | ;; it under the terms of the GNU General Public License as published
18 | ;; by the Free Software Foundation, either version 3 of the License,
19 | ;; or (at your option) any later version.
20 | ;;
21 | ;; This file is distributed in the hope that it will be useful,
22 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
23 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 | ;; GNU General Public License for more details.
25 | ;;
26 | ;; You should have received a copy of the GNU General Public License
27 | ;; along with this file. If not, see .
28 |
29 |
30 | ;;; Commentary:
31 |
32 | ;; This package provides an alternative interactive serach interface for
33 | ;; mu and mu4e (see URL `https://djcbsoftware.nl/code/mu/mu4e.html').
34 | ;; It uses a consult-based minibuffer completion for searching and
35 | ;; selecting, and marking emails, as well as additional utilities for
36 | ;; composing emails and more.
37 |
38 | ;; This package requires mu4e version "1.10.8" or later.
39 |
40 |
41 | ;;; Code:
42 |
43 | ;;; Requirements
44 |
45 | (require 'embark)
46 | (require 'consult-mu)
47 | (require 'consult-mu-embark)
48 |
49 | (defun consult-mu-contacts-embark-insert-email (cand)
50 | "Embark function for inserting CAND's email."
51 | (let* ((contact (get-text-property 0 :contact cand))
52 | (email (plist-get contact :email)))
53 | (insert (concat email "; "))))
54 |
55 | (defun consult-mu-contacts-embark-kill-email (cand)
56 | "Embark function for copying CAND's email."
57 | (let* ((contact (get-text-property 0 :contact cand))
58 | (email (plist-get contact :email)))
59 | (kill-new email)))
60 |
61 | (defun consult-mu-contacts-embark-get-alternative (cand)
62 | "Embark function for copying CAND's email."
63 | (let* ((contact (get-text-property 0 :contact cand))
64 | (name (string-trim (plist-get contact :name)))
65 | (email (plist-get contact :email))
66 | (user (string-trim (replace-regexp-in-string "@.*" "" email))))
67 | (consult-mu-contacts (cond
68 | ((not (string-empty-p name))
69 | name)
70 | ((not (string-empty-p user))
71 | user)
72 | ((t ""))))))
73 |
74 | (defun consult-mu-contacts-embark-compose (cand)
75 | "Embark function for composing an email to CAND."
76 | (let* ((contact (get-text-property 0 :contact cand)))
77 | (consult-mu-contacts--compose-to contact)))
78 |
79 | (defun consult-mu-contacts-embark-search-messages (cand)
80 | "Embark function for searching messages from CAND using `consult-mu'."
81 | (let* ((contact (get-text-property 0 :contact cand))
82 | (email (plist-get contact :email)))
83 | (consult-mu (concat "from:" email))))
84 |
85 | (defun consult-mu-contacts-embark-default-action (cand)
86 | "Run `consult-mu-contacts-action' on CAND."
87 | (let* ((contact (get-text-property 0 :contact cand))
88 | (query (get-text-property 0 :query cand))
89 | (newcand (cons cand `(:contact ,contact :query ,query))))
90 | (funcall #'consult-mu-contacts--insert-email-action newcand)))
91 |
92 | ;;; Define Embark Keymaps
93 | (defvar-keymap consult-mu-embark-contacts-actions-map
94 | :doc "Keymap for consult-mu-embark-contacts"
95 | :parent consult-mu-embark-general-actions-map
96 | "c" #'consult-mu-contacts-embark-compose
97 | "s" #'consult-mu-contacts-embark-search-messages
98 | "i" #'consult-mu-contacts-embark-insert-email
99 | "w" #'consult-mu-contacts-embark-kill-email
100 | "a" #'consult-mu-contacts-embark-get-alternative)
101 |
102 |
103 | (add-to-list 'embark-keymap-alist '(consult-mu-contacts . consult-mu-embark-contacts-actions-map))
104 |
105 | ;; change the default action on `consult-mu-contacts category.
106 | (add-to-list 'embark-default-action-overrides '(consult-mu-contacts . consult-mu-contacts-embark-default-action))
107 |
108 | ;;; Provide `consult-mu-contacts-embark' module
109 |
110 | (provide 'consult-mu-contacts-embark)
111 |
112 | ;;; consult-mu-contacts-embark.el ends here
113 |
--------------------------------------------------------------------------------
/extras/consult-mu-contacts.el:
--------------------------------------------------------------------------------
1 | ;;; consult-mu-contacts.el --- Consult Mu4e asynchronously -*- lexical-binding: t -*-
2 |
3 | ;; Copyright (C) 2023 Armin Darvish
4 |
5 | ;; Author: Armin Darvish
6 | ;; Maintainer: Armin Darvish
7 | ;; Created: 2023
8 | ;; Version: 1.0
9 | ;; Package-Requires: ((emacs "28.0") (consult "2.0"))
10 | ;; Homepage: https://github.com/armindarvish/consult-mu
11 | ;; Keywords: convenience, matching, tools, email
12 | ;; Homepage: https://github.com/armindarvish/consult-mu
13 |
14 | ;; SPDX-License-Identifier: GPL-3.0-or-later
15 |
16 | ;; This file is free software: you can redistribute it and/or modify
17 | ;; it under the terms of the GNU General Public License as published
18 | ;; by the Free Software Foundation, either version 3 of the License,
19 | ;; or (at your option) any later version.
20 | ;;
21 | ;; This file is distributed in the hope that it will be useful,
22 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
23 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 | ;; GNU General Public License for more details.
25 | ;;
26 | ;; You should have received a copy of the GNU General Public License
27 | ;; along with this file. If not, see .
28 |
29 |
30 | ;;; Commentary:
31 |
32 | ;; This package provides an alternative interactive serach interface for
33 | ;; mu and mu4e (see URL `https://djcbsoftware.nl/code/mu/mu4e.html').
34 | ;; It uses a consult-based minibuffer completion for searching and
35 | ;; selecting, and marking emails, as well as additional utilities for
36 | ;; composing emails and more.
37 |
38 | ;; This package requires mu4e version "1.10.8" or later.
39 |
40 |
41 | ;;; Code:
42 |
43 | (require 'consult-mu)
44 |
45 | ;;; Customization Variables
46 |
47 | (defcustom consult-mu-contacts-group-by :name
48 | "What field to use to group the results in the minibuffer?
49 |
50 | By default it is set to :name, but can be any of:
51 |
52 | :name group by contact name
53 | :email group by email of the contact
54 | :domain group by the domain of the contact's email
55 | \(e.g. domain.com in user@domain.com\)
56 | :user group by the ncontact's user name
57 | \(e.g. user in user@domain.com\)"
58 | :group 'consult-mu
59 | :type '(radio (const :name)
60 | (const :email)
61 | (const :domain)
62 | (const :user)))
63 |
64 | (defcustom consult-mu-contacts-action #'consult-mu-contacts--list-messages-action
65 | "Which function to use when selecting a contact?
66 |
67 | By default it is bound to
68 | `consult-mu-contacts--list-messages-action'."
69 | :group 'consult-mu
70 | :type '(choice (function :tag "(Default) Show Messages from Contact" #'consult-mu-contacts--list-messages-action)
71 | (function :tag "Insert Email" #'consult-mu-contacts--insert-email-action)
72 | (function :tag "Copy Email to Kill Ring" #'consult-mu-contacts--copy-email-action)
73 | (function :tag "Custom Function")))
74 |
75 | (defcustom consult-mu-contacts-ignore-list (list)
76 | "List of Regexps to ignore when searching contacts.
77 |
78 | This is useful to filter certain addreses from contacts. For example, you
79 | can remove no-reply adresses by setting this variable to
80 | \='((“no-reply@example.com”))."
81 | :group 'consult-mu
82 | :type '(repeat :tag "Regexp List" regexp))
83 |
84 | (defcustom consult-mu-contacts-ignore-case-fold-search case-fold-search
85 | "Whether to ignore case when matching against ignore-list?
86 |
87 | When non-nil, `consult-mu-contacts' performs case *insensitive* match with
88 | `consult-mu-contacts-ignore-list' and removes matches from candidates.
89 |
90 | By default it is inherited from `case-fold-search'."
91 | :group 'consult-mu
92 | :type 'boolean)
93 |
94 | ;;; Other Variables
95 |
96 | (defvar consult-mu-contacts-category 'consult-mu-contacts
97 | "Category symbol for contacts in `consult-mu' package.")
98 |
99 | (defvar consult-mu-contacts--override-group nil
100 | "Override grouping in `consult-mu-contacs' based on user input.")
101 |
102 | (defvar consult-mu-contacts--history nil
103 | "History variable for `consult-mu-contacts'.")
104 |
105 | (defun consult-mu-contacts--list-messages (contact)
106 | "List messages from CONTACT using `consult-mu'."
107 | (let* ((consult-mu-maxnum nil)
108 | (email (plist-get contact :email)))
109 | (consult-mu (format "contact:%s" email))))
110 |
111 | (defun consult-mu-contacts--list-messages-action (cand)
112 | "Search the messages from contact candidate, CAND.
113 |
114 | This is a wrapper function around `consult-mu-contacts--list-messages'. It
115 | parses CAND to extract relevant CONTACT plist and other information and
116 | passes them to `consult-mu-contacts--list-messages'.
117 |
118 | To use this as the default action for consult-mu-contacts, set
119 | `consult-mu-contacts-default-action' to
120 | \=#'consult-mu-contacts--list-messages-action."
121 |
122 |
123 | (let* ((info (cdr cand))
124 | (contact (plist-get info :contact)))
125 | (consult-mu-contacts--list-messages contact)))
126 |
127 | (defun consult-mu-contacts--insert-email (contact)
128 | "Insert email of CONTACT at point.
129 |
130 | This is useful for inserting email when composing an email to contact."
131 | (let* ((email (plist-get contact :email)))
132 | (insert (concat email "; "))))
133 |
134 | (defun consult-mu-contacts--insert-email-action (cand)
135 | "Insert the email from contact candidate, CAND.
136 |
137 | This is a wrapper function around `consult-mu-contacts--insert-email'. It
138 | parses CAND to extract relevant CONTACT plist and other information and
139 | passes them to `consult-mu-contacts--insert-email'.
140 |
141 | To use this as the default action for consult-mu-contacts, set
142 | `consult-mu-contacts-default-action' to
143 | \=#'consult-mu-contacts--insert-email-action."
144 | (let* ((info (cdr cand))
145 | (contact (plist-get info :contact)))
146 | (consult-mu-contacts--insert-email contact)))
147 |
148 | (defun consult-mu-contacts--copy-email (contact)
149 | "Copy email of CONTACT to kill ring."
150 | (let* ((email (plist-get contact :email)))
151 | (kill-new email)))
152 |
153 | (defun consult-mu-contacts--copy-email-action (cand)
154 | "Copy the email from contact candidate, CAND, to kill ring.
155 |
156 | This is a wrapper function around `consult-mu-contacts--copy-email'. It
157 | parses CAND to extract relevant CONTACT plist and other information and
158 | passes them to `consult-mu-contacts--copy-email'.
159 |
160 | To use this as the default action for consult-mu-contacts, set
161 | `consult-mu-contacts-default-action' to
162 | \=#'consult-mu-contacts--copy-email-action."
163 | (let* ((info (cdr cand))
164 | (contact (plist-get info :contact)))
165 | (consult-mu-contacts--copy-email contact)))
166 |
167 | (defun consult-mu-contacts--compose-to (contact)
168 | "Compose an email to CONTACT using `mu4e-compose-new'."
169 | (let* ((email (plist-get contact :email)))
170 | (mu4e-compose-new email)))
171 |
172 | (defun consult-mu-contacts--compose-to-action (cand)
173 | "Open a new buffer to compose a message to contact candidate, CAND.
174 |
175 | This is a wrapper function around `consult-mu-contacts--compose-to'. It
176 | parses CAND to extract relevant CONTACT plist and other information and
177 | passes them to `consult-mu-contacts--compose-to'.
178 |
179 | To use this as the default action for consult-mu-contacts, set
180 | `consult-mu-contacts-default-action' to \=#'consult-mu-contacts--compose-to-action."
181 |
182 | (let* ((info (cdr cand))
183 | (contact (plist-get info :contact)))
184 | (consult-mu-contacts--compose-to contact)))
185 |
186 | (defun consult-mu-contacts--format-candidate (string input highlight)
187 | "Format minibuffer candidates for `consult-mu-contacts'.
188 |
189 | STRING is the output retrieved from “mu cfind INPUT ...” in the command
190 | line.
191 |
192 | INPUT is the query from the user.
193 |
194 | If HIGHLIGHT is non-nil, input is highlighted with
195 | `consult-mu-highlight-match-face' in the minibuffer."
196 | (let* ((query input)
197 | (email (consult-mu--message-extract-email-from-string string))
198 | (name (string-trim (replace-regexp-in-string email "" string nil t nil nil)))
199 | (contact (list :name name :email email))
200 | (match-str (if (stringp input) (consult--split-escaped (car (consult--command-split query))) nil))
201 | (str (format "%s\s\s%s"
202 | (propertize (consult-mu--set-string-width email (floor (* (frame-width) 0.55))) 'face 'consult-mu-sender-face)
203 | (propertize name 'face 'consult-mu-subject-face)))
204 | (str (propertize str :contact contact :query query)))
205 | (if (and consult-mu-highlight-matches highlight)
206 | (cond
207 | ((listp match-str)
208 | (mapc (lambda (match) (setq str (consult-mu--highlight-match match str t))) match-str))
209 | ((stringp match-str)
210 | (setq str (consult-mu--highlight-match match-str str t))))
211 | str)
212 | (cons str (list :contact contact :query query))))
213 |
214 | (defun consult-mu-contacts--add-history ()
215 | "Get list of emails in the current buffer.
216 |
217 | This is used to add the emails in the current buffer to history."
218 | (let ((add (list)))
219 | (pcase major-mode
220 | ((or mu4e-view-mode mu4e-compose-mode org-msg-edit-mode message-mode)
221 | (mapcar (lambda (item)
222 | (concat "#" (consult-mu--message-extract-email-from-string item)))
223 | (append add
224 | (consult-mu--message-emails-string-to-list (consult-mu--message-get-header-field "from"))
225 | (consult-mu--message-emails-string-to-list (consult-mu--message-get-header-field "to"))
226 | (consult-mu--message-emails-string-to-list (consult-mu--message-get-header-field "cc"))
227 | (consult-mu--message-emails-string-to-list (consult-mu--message-get-header-field "bcc"))
228 | (consult-mu--message-emails-string-to-list (consult-mu--message-get-header-field "reply-to")))))
229 | (_ (list)))))
230 |
231 | (defun consult-mu-contacts--group-name (cand)
232 | "Get the group name of CAND using `consult-mu-contacts-group-by'.
233 |
234 | See `consult-mu-contacts-group-by' for details of grouping options."
235 | (let* ((contact (get-text-property 0 :contact cand))
236 | (email (plist-get contact :email))
237 | (name (plist-get contact :name))
238 | (_ (string-match "\\(?1:[a-zA-Z0-9\_\.\+\-]+\\)@\\(?2:[a-zA-Z0-9\-]+\.[a-zA-Z0-9\-\.]+\\)" email))
239 | (user (match-string 1 email))
240 | (domain (match-string 2 email))
241 | (group (or consult-mu-contacts--override-group consult-mu-contacts-group-by))
242 | (field (if (not (keywordp group)) (intern (concat ":" (format "%s" group))) group)))
243 | (pcase field
244 | (:email email)
245 | (:name (if (string-empty-p name) "n/a" name))
246 | (:domain domain)
247 | (:user user)
248 | (_ nil))))
249 |
250 | (defun consult-mu-contacts--group (cand transform)
251 | "Group function for `consult-mu-contacts' candidates.
252 |
253 | CAND `consult-mu-contacts--group-name' to get the group name for contact.
254 | When TRANSFORM is non-nil, the name of the candiate is used as group title."
255 | (when-let ((name (consult-mu-contacts--group-name cand)))
256 | (if transform (substring cand) name)))
257 |
258 | (defun consult-mu-contacs--lookup ()
259 | "Lookup function for `consult-mu-contacs' minibuffer candidates.
260 |
261 | This is passed as LOOKUP to `consult--read' on candidates and is used to
262 | format the output when a candidate is selected."
263 | (lambda (sel cands &rest args)
264 | (let* ((info (cdr (assoc sel cands)))
265 | (contact (plist-get info :contact))
266 | (name (plist-get contact :name))
267 | (email (plist-get contact :email)))
268 | (cons (or name email) info))))
269 |
270 | (defun consult-mu-contatcs--predicate (cand)
271 | "Predicate function for `consult-mu-contacs' candidate, CAND.
272 |
273 | This is passed as Predicate to `consult--read' on candidates and is used to
274 | remove contacts matching `consult-mu-contacts-ignore-list' from the list of
275 | candidtaes.
276 |
277 | Note that `consult-mu-contacts-ignore-case-fold-search' is used to define
278 | case (in)sensitivity as well."
279 |
280 | (let* ((contact (plist-get (cdr cand) :contact))
281 | (email (plist-get contact :email))
282 | (name (plist-get contact :name))
283 | (case-fold-search consult-mu-contacts-ignore-case-fold-search))
284 | (if (seq-empty-p (seq-filter (lambda (reg) (or (string-match-p reg email)
285 | (string-match-p reg name)))
286 | consult-mu-contacts-ignore-list))
287 | t
288 | nil)))
289 |
290 | (defun consult-mu-contacts--state ()
291 | "State function for `consult-mu-contacts' candidates.
292 |
293 | This is passed as STATE to `consult--read' and is used to preview or do
294 | other actions on the candidate."
295 | (lambda (action cand)
296 | (let ((preview (consult--buffer-preview)))
297 | (pcase action
298 | ('preview)
299 | ('return
300 | (save-mark-and-excursion
301 | (consult-mu--execute-all-marks))
302 | (setq consult-mu-contacts--override-group nil)
303 | cand)))))
304 |
305 | (defun consult-mu-contacts--transform (input)
306 | "Add annotation to minibuffer candiates for `consult-mu-contacts'.
307 |
308 | Format each candidates with `consult-gh--repo-format' and INPUT."
309 | (lambda (cands)
310 | (cl-loop for cand in cands
311 | collect
312 | (consult-mu-contacts--format-candidate cand input t))))
313 |
314 | (defun consult-mu-contacts--builder (input)
315 | "Build mu command line for searching contacts by INPUT."
316 | (pcase-let* ((consult-mu-args (append consult-mu-args '("cfind")))
317 | (cmd (consult--build-args consult-mu-args))
318 | (`(,arg . ,opts) (consult--command-split input))
319 | (flags (append cmd opts)))
320 | (unless (or (member "-n" flags) (member "--maxnum" flags))
321 | (if (and consult-mu-maxnum (> consult-mu-maxnum 0))
322 | (setq opts (append opts (list "--maxnum" (format "%s" consult-mu-maxnum))))))
323 | (if (or (member "-g" opts) (member "--group" opts))
324 | (cond
325 | ((member "-g" opts)
326 | (setq consult-mu-contacts--override-group (ignore-errors (intern (nth (+ (cl-position "-g" opts :test 'equal) 1) opts))))
327 | (setq opts (remove "-g" (remove (ignore-errors (nth (+ (cl-position "-g" opts :test 'equal) 1) opts)) opts))))
328 | ((member "--group" opts)
329 | (setq consult-mu-contacts--override-group (ignore-errors (intern (nth (+ (cl-position "--group" opts :test 'equal) 1) opts))))
330 | (setq opts (remove "--group" (remove (ignore-errors (nth (+ (cl-position "--group" opts :test 'equal) 1) opts)) opts)))))
331 | (setq consult-mu-contacts--override-group nil))
332 | (pcase-let* ((`(,re . ,hl) (funcall consult--regexp-compiler arg 'pcre t)))
333 | (when re
334 | (cons (append cmd
335 | (list (string-join re " "))
336 | opts)
337 | hl)))))
338 |
339 | (defun consult-mu-contacts--async (prompt builder &optional initial)
340 | "Query mu4e contacts asynchronously.
341 |
342 | This is a non-interactive internal function. For the interactive version
343 | see `consult-mu-contacts'.
344 |
345 | It runs the command line from `consult-mu-contacts--builder' in an async
346 | process and returns the results \(list of contacts\) as a completion table
347 | in minibuffer that will be passed to `consult--read'. The completion table
348 | gets dynamically updated as the user types in the minibuffer. Each
349 | candidate in the minibuffer is formatted by
350 | `consult-mu-contacts--transform' to add annotation and other info to the
351 | candidate.
352 |
353 | Description of Arguments:
354 | PROMPT the prompt in the minibuffer.
355 | \(passed as PROMPT to `consult--red'\)
356 | BUILDER an async builder function passed to `consult--async-command'.
357 | INITIAL an optional arg for the initial input in the minibuffer.
358 | \(passed as INITITAL to `consult--read'\)
359 |
360 | commandline arguments/options \(run “mu cfind --help” in the command line
361 | for details\) can be passed to the minibuffer input similar to
362 | `consult-grep'. For example the user can enter:
363 |
364 | “#john -- --maxnum 10”
365 |
366 | This will search for contacts with the query “john”, and retrives a maximum
367 | of 10 contacts.
368 |
369 | Also, the results can further be narrowed by
370 | `consult-async-split-style' \(e.g. by entering “#” when
371 | `consult-async-split-style' is set to \='perl\).
372 |
373 | For example:
374 |
375 | “#john -- --maxnum 10#@gmail”
376 |
377 | Will retrieve the message as the example above, then narrows down the
378 | completion table to candidates that match “@gmail”."
379 | (consult--read
380 | (consult--process-collection builder
381 | :transform (consult--async-transform-by-input #'consult-mu-contacts--transform))
382 | :prompt prompt
383 | :lookup (consult-mu-contacs--lookup)
384 | :state (funcall #'consult-mu-contacts--state)
385 | :initial initial
386 | :group #'consult-mu-contacts--group
387 | :add-history (consult-mu-contacts--add-history)
388 | :history '(:input consult-mu-contacts--history)
389 | :category 'consult-mu-contacts
390 | :preview-key consult-mu-preview-key
391 | :predicate #'consult-mu-contatcs--predicate
392 | :sort t))
393 |
394 | (defun consult-mu-contacts (&optional initial noaction)
395 | "List results of “mu cfind” asynchronously.
396 |
397 | This is an interactive wrapper function around
398 | `consult-mu-contacts--async'. It queries the user for a search term in the
399 | minibuffer, then fetches a list of contacts for the entered search term as
400 | a minibuffer completion table for selection. The list of candidates in the
401 | completion table are dynamically updated as the user changes the entry.
402 |
403 | INITIAL is an optional arg for the initial input in the minibuffer \(passed
404 | as INITITAL to `consult-mu-contacts--async'\).
405 |
406 | Upon selection of a candidate either
407 | - the candidate is returned if NOACTION is non-nil
408 | or
409 | - the candidate is passed to `consult-mu-contacts-action' if NOACTION is
410 | nil.
411 |
412 | Additional commandline arguments can be passed in the minibuffer entry by
413 | typing “--” followed by command line arguments.
414 |
415 | For example the user can enter:
416 |
417 | “#john doe -- -n 10”
418 |
419 | This will run a contact search with the query “john doe” and changes the
420 | search limit to 10.
421 |
422 | Also, the results can further be narrowed by `consult-async-split-style'
423 | \(e.g. by entering “#” when `consult-async-split-style' is set to \='perl\).
424 |
425 |
426 | For example:
427 |
428 | “#john doe -- -n 10#@gmail”
429 |
430 | will retrieve the message as the example above, then narrows down to the
431 | candidates that match “@gmail”.
432 |
433 | For more details on consult--async functionalities, see `consult-grep' and
434 | the official manual of consult, here: https://github.com/minad/consult."
435 | (interactive)
436 | (save-mark-and-excursion
437 | (consult-mu--execute-all-marks))
438 | (let* ((sel
439 | (consult-mu-contacts--async (concat "[" (propertize "consult-mu-contacts" 'face 'consult-mu-sender-face) "]" " Search Contacts: ") #'consult-mu-contacts--builder initial)))
440 | (save-mark-and-excursion
441 | (consult-mu--execute-all-marks))
442 | (if noaction
443 | sel
444 | (progn
445 | (funcall consult-mu-contacts-action sel)
446 | sel))))
447 |
448 | ;;; provide `consult-mu-contacts' module
449 | (provide 'consult-mu-contacts)
450 |
451 | ;;; consult-mu-contacts.el ends here
452 |
--------------------------------------------------------------------------------