nftable-migration
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 <https://www.gnu.org/licenses/>.
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
60The dynamically computed arguments are appended.
61Can 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
68This 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
78By 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
91By default inherits from `mu4e-headers-field'.
92
93From mu4e docs:
94
95Each element has the form (HEADER . WIDTH), where HEADER is one of
96the available headers (see `mu4e-header-info') and WIDTH is the
97respective width in characters.
98
99A width of nil means “unrestricted”, and this is best reserved
100for the rightmost \(last\) field. Note that Emacs may become very
101slow with excessively long lines \(1000s of characters\), so if you
102regularly get such messages, you want to avoid fields with nil
103altogether."
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
116If non-nil, `consult-mu' uses this string to format the headers instead of
117`consult-mu-headers-field'.
118
119The string should be of the format “%[char][integer]%[char][integer]...”,
120and allow dynamic insertion of the content. Each “%[char][integer]“ chunk
121represents a different field and the integer defines the length of the
122field.
123
124The 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
140For exmaple, “%d15%s50” means 15 characters for date and 50 charcters for
141subject, and “%d13%s37%f17” would make a header containing 13 characters
142for Date, 37 characters for Subject, and 20 characters for From field,
143making a header that looks like this:
144
145Thu 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
153By 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
164By defualt inherits from `mu4e-search-threads'.
165
166Note that per mu4e docs:
167When threading is enabled, the headers are exclusively sorted
168chronologically (: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
175By 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
225For 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
232For 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
239This 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
259This 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.
267By 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
297The 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
317By default inherits from `consult-highlight-match'.
318This is used to highlight matches of search queries in the minibufffer
319completion 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
325By default inherits from `consult-preview-match'.
326This 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
333By 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
339By 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
345By 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
351By 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
357By 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
363By 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
369By 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
375By 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
381By 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
387By 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
416If the STRING is longer than WIDTH, it truncates the string and adds
417ellipsis, “...”. If the string is shorter, it adds whitespace to the
418string. If PREPEND is non-nil, it truncates or adds whitespace from the
419beginning 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
435Use `consult-mu--set-string-width' to the width of the concatenate of
436STRING+PREFIX \(e.g. “(concat prefix string)”\) within MAXWIDTH. This is
437used 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
446If a REGEXP contains a capturing group, only the captured group is
447highlighted, otherwise, the whole match is highlighted.
448Case 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
467If IGNORE-CASE is non-nil, it uses case-insensitive match.
468
469This 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
486BUFFER 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
499STRING is the output form a mu command, for example:
500`mu find query --fields d`
501Returns 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
512FLAG is the output form mu command in the terminal, for example:
513 `mu find query --fields g`.
514This function converts each character in FLAG to an expanded string of the
515flag 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
572This is used to override `mu4e~headers-append-handler' to ensure that
573buffer 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
588BUFFERNAME defaults to `consult-mu-view-buffer-name'.
589
590This s used to overrides `mu4e-view' to ensure that buffer handling is done
591right 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
611Optionally, show TEXT.
612
613This is used to override `mu4e~headers-clear' to ensure that buffer
614handling 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
628Uses user input (i.e. from `consult-mu' command) to define the sort field.
629
630OPTS is the command line options for mu and can be set by entering options
631in the minibuffer input. For more details, refer to `consult-grep' and
632consult async documentation.
633
634For 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
640Note that per mu4e docs:
641When threading is enabled, the headers are exclusively sorted
642chronologically (: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
672Uses user input \(i.e. from `consult-mu' command\) to define the sort field.
673
674OPTS is the command line options for mu and can be set by entering options
675in the minibuffer input. For more details, refer to `consult-grep' and
676consult async documentation.
677
678For example, if the user enters the following in the minibuffer:
679
680“#query -- --maxnum 400 --sortfield from --reverse”
681
682The `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
695Uses user input \(i.e. from `consult-mu' command\) to define whether to
696skip duplicates.
697
698OPTS is the command line options for mu and can be set by entering options
699in the minibuffer input. For more details, refer to `consult-grep' and
700consult async documentation.
701
702For example, if the user enters the following in the minibuffer:
703
704“#query -- --maxnum 400 --skip-dups”
705
706The `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
713Uses user input \(i.e. from `consult-mu' command\) to define the number of
714results shown.
715
716OPTS is the command line options for mu and can be set by entering options
717in the minibuffer input. For more details, refer to `consult-grep' and
718consult async documentation.
719
720For example, if the user enters the following in the minibuffer:
721
722“#query -- --maxnum 400”
723
724The `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
734Uses user input \(i.e. from `consult-mu' command\) to define whether to
735include related messages.
736
737OPTS is the command line options for mu and can be set by entering options
738in the minibuffer input. For more details, refer to `consult-grep' and
739consult async documentation.
740
741For example if the user enters the following in the minibuffer:
742
743“#query -- --include-related”
744
745The `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
753Uses user input \(i.e. from `consult-mu' command\) to define whether to
754show threads.
755
756OPTS is the command line options for mu and can be set by entering options
757in the minibuffer input. For more details, refer to `consult-grep' and
758consult async documentation.
759
760Note that per mu4e docs, when threading is enabled, the headers are
761exclusively sorted by date. Here the logic is reversed in order to allow
762dynamically sorting by fields other than date \(even when threads are
763enabled\). In other words, if the sort-field is not the :date, threading
764is disabled because otherwise sort field will be ignored. This allows the
765user to use command line arguments to sort messages by fields other than
766the date. For example, the user can enter the following in the minibuffer
767input to sort by subject
768
769“#query -- --sortfield subject”
770
771When the sort-field is :date, the default setting,
772`consult-mu-search-threads' is used, and if that is set to nil, the user
773can use command line arguments \(a.k.a. -t or --thread\) to enable it
774dynamically."
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
784If IGNORE-HISTORY is true, does *not* update the query history stack,
785`mu4e--search-query-past'.
786If MSG is non-nil, put the cursor on MSG.
787TYPE 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
844Executes all actions for marked messages in the buffer
845`consult-mu-headers-buffer-name'.
846
847If NO-CONFIRMATION is non-nil, don't ask user for confirmation.
848
849This is similar to `mu4e-mark-execute-all' but, with buffer/window
850handling 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
866This 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
884STRING is the output form mu command, for example from:
885`mu find query --fields f`
886
887Returns a plist with \=':email and \':name keys.
888
889For example
890
891“John Doe <john.doe@example.com>”
892
893will 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
910Looks at the contact plist \(e.g. (:name “John Doe” :email
911“john.doe@example.com”)\) and returns the name. If the name is missing,
912returns 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
928See `consult-mu-headers-template' for explanation of the format of
929STRING."
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
1023This is passed as LOOKUP to `consult--read' on candidates and is used to
1024format 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
1034See `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
1064CAND is passed to `consult-mu--group-name' to get the group for CAND.
1065When 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
1072If NOSELECT is non-nil, does not select the view buffer/window.
1073If MARK-AS-READ is non-nil, marks the MSG as read.
1074If 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
1109This is a wrapper function around `consult-mu--view'. It parses CAND to
1110extract relevant MSG plist and other information and passes them to
1111`consult-mu--view'.
1112
1113To 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
1126If 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
1144This is a wrapper function around `consult-mu--reply'. It passes
1145relevant message plist, from CAND, as well as WIDE-REPLY to
1146`consult-mu--reply'.
1147
1148To 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
1173This is a wrapper function around `consult-mu--forward'. It passes
1174the relevant message plist, from CAND to `consult-mu--forward'.
1175
1176To 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
1185STYLE 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
1194CAND is the minibuffer completion candidate \(a mu4e message collected by
1195`consult-mu--dynamic-collection'\). If HIGHLIGHT is non-nil, it is
1196highlighted 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
1221INPUT 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
1224messages."
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.
1246This is passed as STATE to `consult--read' and is used to preview or do
1247other 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
1276This is a non-interactive internal function. For the interactive version
1277see `consult-mu'.
1278
1279It runs the `consult-mu--dynamic-collection' to do a `mu4e-search' with
1280user input \(e.g. INITIAL\) and returns the results \(list of messages
1281found\) as a completion table in minibuffer.
1282
1283The completion table gets dynamically updated as the user types in the
1284minibuffer. Each candidate in the minibuffer is formatted by
1285`consult-mu--dynamic-format-candidate' to add annotation and other info to
1286the candidate.
1287
1288Description 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
1295commandline arguments/options \(see `mu find --help` in the command line
1296for 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
1301this will search for mu4e messages with the query “paper”, retrives a
1302maximum of 200 messages and sorts them by the “from:” field and reverses
1303the sort direction (opposite of `consult-mu-search-sort-field').
1304
1305Note that some command line arguments are not supported by mu4e (for
1306example sorting based on cc: or bcc: fields are not supported in
1307`mu4e-search-sort-field')
1308
1309Also, 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
1313For example:
1314
1315“#paper -- --maxnum 200 --sortfield from --reverse#accepted”
1316
1317will retrieve the message as the example above, then narrows down the
1318candidates 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
1337This is an interactive wrapper function around `consult-mu--dynamic'. It
1338queries the user for a search term in the minibuffer, then fetches a list
1339of messages for the entered search term as a minibuffer completion table
1340for selection. The list of candidates in the completion table are
1341dynamically updated as the user changes the entry.
1342
1343Upon 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
1348Additional commandline arguments can be passed in the minibuffer entry by
1349typing “--” followed by command line arguments.
1350
1351For example, the user can enter:
1352
1353“#consult-mu -- -n 10”
1354
1355this will run a `mu4e-search' with the query “consult-mu” and changes the
1356search limit \(i.e. `mu4e-search-results-limit' to 10\).
1357
1358
1359Also, 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
1363For example:
1364
1365“#consult-mu -- -n 10#github”
1366
1367will retrieve the messages as the example above, then narrows down the
1368completion table to candidates that match “github”.
1369
1370INITIAL is an optional arg for the initial input in the minibuffer.
1371\(passed as INITITAL to `consult-mu--dynamic'\)
1372
1373For more details on consult--async functionalities, see `consult-grep' and
1374the official manual of consult, here:
1375URL `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
1392STRING is the output retrieved from `mu find INPUT ...` in the command line.
1393INPUT is the query from the user.
1394
1395If 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
1443This is passed as STATE to `consult--read' and is used to preview or do
1444other 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
1470Format 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
1532This is a non-interactive internal function. For the interactive
1533version, see `consult-mu-async'.
1534
1535It runs the command line from `consult-mu--async-builder' in an async
1536process and returns the results (list of messages) as a completion table
1537in minibuffer that will be passed to `consult--read'. The completion
1538table gets dynamically updated as the user types in the minibuffer. Each
1539candidate in the minibuffer is formatted by `consult-mu--async-transform'
1540to add annotation and other info to the candidate.
1541
1542Description of Arguments:
1543
1544PROMPT the prompt in the minibuffer
1545 \(passed as PROMPT to `consult--red'\)
1546BUILDER an async builder function passed to `consult--async-command'
1547INITIAL an optional arg for the initial input in the minibuffer
1548 \(passed as INITITAL to `consult--read'\)
1549
1550commandline arguments/options \(see `mu find --help` in the command line
1551for 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
1556this will search for mu4e messages with the query “paper”, retrives a
1557maximum of 200 messages sorts them by the “from:” field and reverses the
1558sort direction (opposite of `consult-mu-search-sort-field').
1559
1560Also, 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
1564For example:
1565
1566`#paper -- --maxnum 200 --sortfield from --reverse#accepted'
1567
1568will retrieve the message as the example above, then narrows down the
1569completion 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
1589This is an interactive wrapper function around `consult-mu--async'. It
1590queries the user for a search term in the minibuffer, then fetches a list
1591of messages for the entered search term as a minibuffer completion table
1592for selection. The list of candidates in the completion table are
1593dynamically updated as the user changes the entry.
1594
1595Upon 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
1600Additional commandline arguments can be passed in the minibuffer entry by
1601typing `--` followed by command line arguments.
1602
1603For example the user can enter:
1604
1605`#consult-mu -- -n 10'
1606
1607this will run a `mu4e-search' with the query \"consult-my\" and changes the
1608search limit (i.e. `mu4e-search-results-limit' to 10.
1609
1610
1611Also, 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
1614For example:
1615
1616“#consult-mu -- -n 10#github”
1617
1618will retrieve the message as the example above, then narrows down the
1619completion table to candidates that match “github”.
1620
1621INITIAL is an optional arg for the initial input in the minibuffer.
1622\(passed as INITITAL to `consult-mu--async'\).
1623
1624For more details on consult--async functionalities, see `consult-grep' and
1625the official manual of consult, here:
1626URL `https://github.com/minad/consult'
1627
1628Note that this is the async search directly using the commandline `mu`
1629command and not mu4e-search. As a result, mu4e-headers buffers are not
1630created until a single message is selected \(or interacted with using
1631embark, 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
1634many messages \(tens of thousands\) and previews, but not opening the
1635results in a mu4e-headers buffer. If you want ot open the results in a
1636mu4e-headers buffer for other work flow, then you should use the
1637dynamically collected function `consult-mu' which is slower if searching
1638for many emails but allows follow up interactions in a mu4e-headers
1639buffer."
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
1660This is a wrapper function that calls `consult-mu-default-command' with
1661INITIAL and NOACTION.
1662
1663For 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