├── .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: Armin Darvish 9 | #+html: GNU Emacs 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 | --------------------------------------------------------------------------------