827acebf7a83bf667c1d0a8f41622c43bd82df39
[notmuch] / emacs / notmuch-tree.el
1 ;;; notmuch-tree.el --- displaying notmuch forests.
2 ;;
3 ;; Copyright © Carl Worth
4 ;; Copyright © David Edmondson
5 ;; Copyright © Mark Walters
6 ;;
7 ;; This file is part of Notmuch.
8 ;;
9 ;; Notmuch is free software: you can redistribute it and/or modify it
10 ;; under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation, either version 3 of the License, or
12 ;; (at your option) any later version.
13 ;;
14 ;; Notmuch is distributed in the hope that it will be useful, but
15 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17 ;; General Public License for more details.
18 ;;
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
21 ;;
22 ;; Authors: David Edmondson <dme@dme.org>
23 ;;          Mark Walters <markwalters1009@gmail.com>
24
25 ;;; Code:
26
27 (eval-when-compile (require 'cl-lib))
28
29 (require 'mail-parse)
30
31 (require 'notmuch-lib)
32 (require 'notmuch-query)
33 (require 'notmuch-show)
34 (require 'notmuch-tag)
35 (require 'notmuch-parser)
36
37 (declare-function notmuch-search "notmuch"
38                   (&optional query oldest-first target-thread target-line))
39 (declare-function notmuch-call-notmuch-process "notmuch" (&rest args))
40 (declare-function notmuch-read-query "notmuch" (prompt))
41 (declare-function notmuch-search-find-thread-id "notmuch" (&optional bare))
42 (declare-function notmuch-search-find-subject "notmuch" ())
43
44 ;; the following variable is defined in notmuch.el
45 (defvar notmuch-search-query-string)
46
47 ;; this variable distinguishes the unthreaded display from the normal tree display
48 (defvar notmuch-tree-unthreaded nil
49   "A buffer local copy of argument unthreaded to the function notmuch-tree.")
50 (make-variable-buffer-local 'notmuch-tree-unthreaded)
51
52 (defgroup notmuch-tree nil
53   "Showing message and thread structure."
54   :group 'notmuch)
55
56 (defcustom notmuch-tree-show-out nil
57   "View selected messages in new window rather than split-pane."
58   :type 'boolean
59   :group 'notmuch-tree)
60
61 (defcustom notmuch-unthreaded-show-out t
62   "View selected messages in new window rather than split-pane."
63   :type 'boolean
64   :group 'notmuch-tree)
65
66 (defun notmuch-tree-show-out ()
67   (if notmuch-tree-unthreaded
68       notmuch-unthreaded-show-out
69     notmuch-tree-show-out))
70
71 (defcustom notmuch-tree-result-format
72   `(("date" . "%12s  ")
73     ("authors" . "%-20s")
74     ((("tree" . "%s")("subject" . "%s")) ." %-54s ")
75     ("tags" . "(%s)"))
76   "Result formatting for tree view. Supported fields are: date,
77 authors, subject, tree, tags.  Tree means the thread tree
78 box graphics. The field may also be a list in which case
79 the formatting rules are applied recursively and then the
80 output of all the fields in the list is inserted
81 according to format-string.
82
83 Note the author string should not contain
84 whitespace (put it in the neighbouring fields instead).
85 For example:
86         (setq notmuch-tree-result-format \(\(\"authors\" . \"%-40s\"\)
87                                           \(\"subject\" . \"%s\"\)\)\)"
88   :type '(alist :key-type (string) :value-type (string))
89   :group 'notmuch-tree)
90
91 (defcustom notmuch-unthreaded-result-format
92   `(("date" . "%12s  ")
93     ("authors" . "%-20s")
94     ((("subject" . "%s")) ." %-54s ")
95     ("tags" . "(%s)"))
96   "Result formatting for unthreaded tree view. Supported fields are: date,
97 authors, subject, tree, tags.  Tree means the thread tree
98 box graphics. The field may also be a list in which case
99 the formatting rules are applied recursively and then the
100 output of all the fields in the list is inserted
101 according to format-string.
102
103 Note the author string should not contain
104 whitespace (put it in the neighbouring fields instead).
105 For example:
106         (setq notmuch-tree-result-format \(\(\"authors\" . \"%-40s\"\)
107                                           \(\"subject\" . \"%s\"\)\)\)"
108   :type '(alist :key-type (string) :value-type (string))
109   :group 'notmuch-tree)
110
111 (defun notmuch-tree-result-format ()
112   (if notmuch-tree-unthreaded
113       notmuch-unthreaded-result-format
114     notmuch-tree-result-format))
115
116 ;; Faces for messages that match the query.
117 (defface notmuch-tree-match-face
118   '((t :inherit default))
119   "Default face used in tree mode face for matching messages"
120   :group 'notmuch-tree
121   :group 'notmuch-faces)
122
123 (defface notmuch-tree-match-date-face
124   nil
125   "Face used in tree mode for the date in messages matching the query."
126   :group 'notmuch-tree
127   :group 'notmuch-faces)
128
129 (defface notmuch-tree-match-author-face
130   '((((class color)
131       (background dark))
132      (:foreground "OliveDrab1"))
133     (((class color)
134       (background light))
135      (:foreground "dark blue"))
136     (t
137      (:bold t)))
138   "Face used in tree mode for the date in messages matching the query."
139   :group 'notmuch-tree
140   :group 'notmuch-faces)
141
142 (defface notmuch-tree-match-subject-face
143   nil
144   "Face used in tree mode for the subject in messages matching the query."
145   :group 'notmuch-tree
146   :group 'notmuch-faces)
147
148 (defface notmuch-tree-match-tree-face
149   nil
150   "Face used in tree mode for the thread tree block graphics in messages matching the query."
151   :group 'notmuch-tree
152   :group 'notmuch-faces)
153
154 (defface notmuch-tree-match-tag-face
155   '((((class color)
156       (background dark))
157      (:foreground "OliveDrab1"))
158     (((class color)
159       (background light))
160      (:foreground "navy blue" :bold t))
161     (t
162      (:bold t)))
163   "Face used in tree mode for tags in messages matching the query."
164   :group 'notmuch-tree
165   :group 'notmuch-faces)
166
167 ;; Faces for messages that do not match the query.
168 (defface notmuch-tree-no-match-face
169   '((t (:foreground "gray")))
170   "Default face used in tree mode face for non-matching messages."
171   :group 'notmuch-tree
172   :group 'notmuch-faces)
173
174 (defface notmuch-tree-no-match-date-face
175   nil
176   "Face used in tree mode for non-matching dates."
177   :group 'notmuch-tree
178   :group 'notmuch-faces)
179
180 (defface notmuch-tree-no-match-subject-face
181   nil
182   "Face used in tree mode for non-matching subjects."
183   :group 'notmuch-tree
184   :group 'notmuch-faces)
185
186 (defface notmuch-tree-no-match-tree-face
187   nil
188   "Face used in tree mode for the thread tree block graphics in messages matching the query."
189   :group 'notmuch-tree
190   :group 'notmuch-faces)
191
192 (defface notmuch-tree-no-match-author-face
193   nil
194   "Face used in tree mode for the date in messages matching the query."
195   :group 'notmuch-tree
196   :group 'notmuch-faces)
197
198 (defface notmuch-tree-no-match-tag-face
199   nil
200   "Face used in tree mode face for non-matching tags."
201   :group 'notmuch-tree
202   :group 'notmuch-faces)
203
204 (defvar notmuch-tree-previous-subject
205   "The subject of the most recent result shown during the async display.")
206 (make-variable-buffer-local 'notmuch-tree-previous-subject)
207
208 (defvar notmuch-tree-basic-query nil
209   "A buffer local copy of argument query to the function notmuch-tree.")
210 (make-variable-buffer-local 'notmuch-tree-basic-query)
211
212 (defvar notmuch-tree-query-context nil
213   "A buffer local copy of argument query-context to the function notmuch-tree.")
214 (make-variable-buffer-local 'notmuch-tree-query-context)
215
216 (defvar notmuch-tree-target-msg nil
217   "A buffer local copy of argument target to the function notmuch-tree.")
218 (make-variable-buffer-local 'notmuch-tree-target-msg)
219
220 (defvar notmuch-tree-open-target nil
221   "A buffer local copy of argument open-target to the function notmuch-tree.")
222 (make-variable-buffer-local 'notmuch-tree-open-target)
223
224 (defvar notmuch-tree-message-window nil
225   "The window of the message pane.
226
227 It is set in both the tree buffer and the child show buffer. It
228 is used to try and close the message pane when quitting tree view
229 or the child show buffer.")
230 (make-variable-buffer-local 'notmuch-tree-message-window)
231 (put 'notmuch-tree-message-window 'permanent-local t)
232
233 (defvar notmuch-tree-message-buffer nil
234   "The buffer name of the show buffer in the message pane.
235
236 This is used to try and make sure we don't close the message pane
237 if the user has loaded a different buffer in that window.")
238 (make-variable-buffer-local 'notmuch-tree-message-buffer)
239 (put 'notmuch-tree-message-buffer 'permanent-local t)
240
241 (defun notmuch-tree-to-message-pane (func)
242   "Execute FUNC in message pane.
243
244 This function returns a function (so can be used as a keybinding)
245 which executes function FUNC in the message pane if it is
246 open (if the message pane is closed it does nothing)."
247   `(lambda ()
248      ,(concat "(In message pane) " (documentation func t))
249      (interactive)
250      (when (window-live-p notmuch-tree-message-window)
251        (with-selected-window notmuch-tree-message-window
252          (call-interactively #',func)))))
253
254 (defun notmuch-tree-inherit-from-message-pane (sym)
255   "Return value of SYM in message-pane if open, or tree-pane if not."
256   (if (window-live-p notmuch-tree-message-window)
257       (with-selected-window notmuch-tree-message-window
258         (symbol-value sym))
259     (symbol-value sym)))
260
261 (defun notmuch-tree-button-activate (&optional button)
262   "Activate BUTTON or button at point.
263
264 This function does not give an error if there is no button."
265   (interactive)
266   (let ((button (or button (button-at (point)))))
267     (when button (button-activate button))))
268
269 (defun notmuch-tree-close-message-pane-and (func)
270   "Close message pane and execute FUNC.
271
272 This function returns a function (so can be used as a keybinding)
273 which closes the message pane if open and then executes function
274 FUNC."
275   `(lambda ()
276      ,(concat "(Close message pane and) " (documentation func t))
277      (interactive)
278      (let ((notmuch-show-process-crypto
279             (notmuch-tree-inherit-from-message-pane 'notmuch-show-process-crypto)))
280        (notmuch-tree-close-message-window)
281        (call-interactively #',func))))
282
283 (defvar notmuch-tree-mode-map
284   (let ((map (make-sparse-keymap)))
285     (set-keymap-parent map notmuch-common-keymap)
286     ;; The following override the global keymap.
287     ;; Override because we want to close message pane first.
288     (define-key map [remap notmuch-help]
289       (notmuch-tree-close-message-pane-and #'notmuch-help))
290     ;; Override because we first close message pane and then close tree buffer.
291     (define-key map [remap notmuch-bury-or-kill-this-buffer] 'notmuch-tree-quit)
292     ;; Override because we close message pane after the search query is entered.
293     (define-key map [remap notmuch-search] 'notmuch-tree-to-search)
294     ;; Override because we want to close message pane first.
295     (define-key map [remap notmuch-mua-new-mail]
296       (notmuch-tree-close-message-pane-and #'notmuch-mua-new-mail))
297     ;; Override because we want to close message pane first.
298     (define-key map [remap notmuch-jump-search]
299       (notmuch-tree-close-message-pane-and #'notmuch-jump-search))
300
301     (define-key map "S" 'notmuch-search-from-tree-current-query)
302     (define-key map "U" 'notmuch-unthreaded-from-tree-current-query)
303     (define-key map "Z" 'notmuch-tree-from-unthreaded-current-query)
304
305     ;; these use notmuch-show functions directly
306     (define-key map "|" 'notmuch-show-pipe-message)
307     (define-key map "w" 'notmuch-show-save-attachments)
308     (define-key map "v" 'notmuch-show-view-all-mime-parts)
309     (define-key map "c" 'notmuch-show-stash-map)
310     (define-key map "b" 'notmuch-show-resend-message)
311
312     ;; these apply to the message pane
313     (define-key map (kbd "M-TAB")
314       (notmuch-tree-to-message-pane #'notmuch-show-previous-button))
315     (define-key map (kbd "<backtab>")
316       (notmuch-tree-to-message-pane #'notmuch-show-previous-button))
317     (define-key map (kbd "TAB")
318       (notmuch-tree-to-message-pane #'notmuch-show-next-button))
319     (define-key map "$"
320       (notmuch-tree-to-message-pane #'notmuch-show-toggle-process-crypto))
321
322     ;; bindings from show (or elsewhere) but we close the message pane first.
323     (define-key map "f"
324       (notmuch-tree-close-message-pane-and #'notmuch-show-forward-message))
325     (define-key map "r"
326       (notmuch-tree-close-message-pane-and #'notmuch-show-reply-sender))
327     (define-key map "R"
328       (notmuch-tree-close-message-pane-and #'notmuch-show-reply))
329     (define-key map "V"
330       (notmuch-tree-close-message-pane-and #'notmuch-show-view-raw-message))
331
332     ;; The main tree view bindings
333     (define-key map (kbd "RET") 'notmuch-tree-show-message)
334     (define-key map [mouse-1] 'notmuch-tree-show-message)
335     (define-key map "x" 'notmuch-tree-archive-message-then-next-or-exit)
336     (define-key map "X" 'notmuch-tree-archive-thread-then-exit)
337     (define-key map "A" 'notmuch-tree-archive-thread)
338     (define-key map "a" 'notmuch-tree-archive-message-then-next)
339     (define-key map "z" 'notmuch-tree-to-tree)
340     (define-key map "n" 'notmuch-tree-next-matching-message)
341     (define-key map "p" 'notmuch-tree-prev-matching-message)
342     (define-key map "N" 'notmuch-tree-next-message)
343     (define-key map "P" 'notmuch-tree-prev-message)
344     (define-key map (kbd "M-p") 'notmuch-tree-prev-thread)
345     (define-key map (kbd "M-n") 'notmuch-tree-next-thread)
346     (define-key map "k" 'notmuch-tag-jump)
347     (define-key map "-" 'notmuch-tree-remove-tag)
348     (define-key map "+" 'notmuch-tree-add-tag)
349     (define-key map "*" 'notmuch-tree-tag-thread)
350     (define-key map " " 'notmuch-tree-scroll-or-next)
351     (define-key map (kbd "DEL") 'notmuch-tree-scroll-message-window-back)
352     (define-key map "e" 'notmuch-tree-resume-message)
353     map))
354 (fset 'notmuch-tree-mode-map notmuch-tree-mode-map)
355
356 (defun notmuch-tree-get-message-properties ()
357   "Return the properties of the current message as a plist.
358
359 Some useful entries are:
360 :headers - Property list containing the headers :Date, :Subject, :From, etc.
361 :tags - Tags for this message."
362   (save-excursion
363     (beginning-of-line)
364     (get-text-property (point) :notmuch-message-properties)))
365
366 (defun notmuch-tree-set-message-properties (props)
367   (save-excursion
368     (beginning-of-line)
369     (put-text-property (point)
370                        (+ (point) 1)
371                        :notmuch-message-properties props)))
372
373 (defun notmuch-tree-set-prop (prop val &optional props)
374   (let ((inhibit-read-only t)
375         (props (or props
376                    (notmuch-tree-get-message-properties))))
377     (plist-put props prop val)
378     (notmuch-tree-set-message-properties props)))
379
380 (defun notmuch-tree-get-prop (prop &optional props)
381   (let ((props (or props
382                    (notmuch-tree-get-message-properties))))
383     (plist-get props prop)))
384
385 (defun notmuch-tree-set-tags (tags)
386   "Set the tags of the current message."
387   (notmuch-tree-set-prop :tags tags))
388
389 (defun notmuch-tree-get-tags ()
390   "Return the tags of the current message."
391   (notmuch-tree-get-prop :tags))
392
393 (defun notmuch-tree-get-message-id (&optional bare)
394   "Return the message id of the current message."
395   (let ((id (notmuch-tree-get-prop :id)))
396     (if id
397         (if bare
398             id
399           (notmuch-id-to-query id))
400       nil)))
401
402 (defun notmuch-tree-get-match ()
403   "Return whether the current message is a match."
404   (interactive)
405   (notmuch-tree-get-prop :match))
406
407 (defun notmuch-tree-refresh-result ()
408   "Redisplay the current message line.
409
410 This redisplays the current line based on the messages
411 properties (as they are now). This is used when tags are
412 updated."
413   (let ((init-point (point))
414         (end (line-end-position))
415         (msg (notmuch-tree-get-message-properties))
416         (inhibit-read-only t))
417     (beginning-of-line)
418     ;; This is a little tricky: we override
419     ;; notmuch-tree-previous-subject to get the decision between
420     ;; ... and a subject right and it stops notmuch-tree-insert-msg
421     ;; from overwriting the buffer local copy of
422     ;; notmuch-tree-previous-subject if this is called while the
423     ;; buffer is displaying.
424     (let ((notmuch-tree-previous-subject
425            (notmuch-tree-get-prop :previous-subject)))
426       (delete-region (point) (1+ (line-end-position)))
427       (notmuch-tree-insert-msg msg))
428     (let ((new-end (line-end-position)))
429       (goto-char (if (= init-point end)
430                      new-end
431                    (min init-point (- new-end 1)))))))
432
433 (defun notmuch-tree-tag-update-display (&optional tag-changes)
434   "Update display for TAG-CHANGES to current message.
435
436 Updates the message in the message pane if appropriate, but does
437 NOT change the database."
438   (let* ((current-tags (notmuch-tree-get-tags))
439          (new-tags (notmuch-update-tags current-tags tag-changes))
440          (tree-msg-id (notmuch-tree-get-message-id)))
441     (unless (equal current-tags new-tags)
442       (notmuch-tree-set-tags new-tags)
443       (notmuch-tree-refresh-result)
444       (when (window-live-p notmuch-tree-message-window)
445         (with-selected-window notmuch-tree-message-window
446           (when (string= tree-msg-id (notmuch-show-get-message-id))
447             (notmuch-show-update-tags new-tags)))))))
448
449 (defun notmuch-tree-tag (tag-changes)
450   "Change tags for the current message."
451   (interactive
452    (list (notmuch-read-tag-changes (notmuch-tree-get-tags) "Tag message")))
453   (notmuch-tag (notmuch-tree-get-message-id) tag-changes)
454   (notmuch-tree-tag-update-display tag-changes))
455
456 (defun notmuch-tree-add-tag (tag-changes)
457   "Same as `notmuch-tree-tag' but sets initial input to '+'."
458   (interactive
459    (list (notmuch-read-tag-changes (notmuch-tree-get-tags) "Tag message" "+")))
460   (notmuch-tree-tag tag-changes))
461
462 (defun notmuch-tree-remove-tag (tag-changes)
463   "Same as `notmuch-tree-tag' but sets initial input to '-'."
464   (interactive
465    (list (notmuch-read-tag-changes (notmuch-tree-get-tags) "Tag message" "-")))
466   (notmuch-tree-tag tag-changes))
467
468 (defun notmuch-tree-resume-message ()
469   "Resume EDITING the current draft message."
470   (interactive)
471   (notmuch-tree-close-message-window)
472   (let ((id (notmuch-tree-get-message-id)))
473     (if id
474         (notmuch-draft-resume id)
475       (message "No message to resume!"))))
476
477 ;; The next two functions close the message window before calling
478 ;; notmuch-search or notmuch-tree but they do so after the user has
479 ;; entered the query (in case the user was basing the query on
480 ;; something in the message window).
481
482 (defun notmuch-tree-to-search ()
483   "Run \"notmuch search\" with the given `query' and display results."
484   (interactive)
485   (let ((query (notmuch-read-query "Notmuch search: ")))
486     (notmuch-tree-close-message-window)
487     (notmuch-search query)))
488
489 (defun notmuch-tree-to-tree ()
490   "Run a query and display results in tree view."
491   (interactive)
492   (let ((query (notmuch-read-query "Notmuch tree view search: ")))
493     (notmuch-tree-close-message-window)
494     (notmuch-tree query)))
495
496 (defun notmuch-unthreaded-from-tree-current-query ()
497   "Switch from tree view to unthreaded view."
498   (interactive)
499   (unless notmuch-tree-unthreaded
500     (notmuch-tree-refresh-view 'unthreaded)))
501
502 (defun notmuch-tree-from-unthreaded-current-query ()
503   "Switch from unthreaded view to tree view."
504   (interactive)
505   (when notmuch-tree-unthreaded
506     (notmuch-tree-refresh-view 'tree)))
507
508 (defun notmuch-search-from-tree-current-query ()
509   "Call notmuch search with the current query."
510   (interactive)
511   (notmuch-tree-close-message-window)
512   (notmuch-search (notmuch-tree-get-query)))
513
514 (defun notmuch-tree-message-window-kill-hook ()
515   "Close the message pane when exiting the show buffer."
516   (let ((buffer (current-buffer)))
517     (when (and (window-live-p notmuch-tree-message-window)
518                (eq (window-buffer notmuch-tree-message-window) buffer))
519       ;; We do not want an error if this is the sole window in the
520       ;; frame and I do not know how to test for that in emacs pre
521       ;; 24. Hence we just ignore-errors.
522       (ignore-errors
523         (delete-window notmuch-tree-message-window)))))
524
525 (defun notmuch-tree-command-hook ()
526   (when (eq major-mode 'notmuch-tree-mode)
527     ;; We just run the notmuch-show-command-hook on the message pane.
528     (when (buffer-live-p notmuch-tree-message-buffer)
529       (with-current-buffer notmuch-tree-message-buffer
530         (notmuch-show-command-hook)))))
531
532 (defun notmuch-tree-show-message-in ()
533   "Show the current message (in split-pane)."
534   (interactive)
535   (let ((id (notmuch-tree-get-message-id))
536         (inhibit-read-only t)
537         buffer)
538     (when id
539       ;; We close and reopen the window to kill off un-needed buffers
540       ;; this might cause flickering but seems ok.
541       (notmuch-tree-close-message-window)
542       (setq notmuch-tree-message-window
543             (split-window-vertically (/ (window-height) 4)))
544       (with-selected-window notmuch-tree-message-window
545         ;; Since we are only displaying one message do not indent.
546         (let ((notmuch-show-indent-messages-width 0)
547               (notmuch-show-only-matching-messages t))
548           (setq buffer (notmuch-show id))))
549       ;; We need the `let' as notmuch-tree-message-window is buffer local.
550       (let ((window notmuch-tree-message-window))
551         (with-current-buffer buffer
552           (setq notmuch-tree-message-window window)
553           (add-hook 'kill-buffer-hook 'notmuch-tree-message-window-kill-hook)))
554       (when notmuch-show-mark-read-tags
555         (notmuch-tree-tag-update-display notmuch-show-mark-read-tags))
556       (setq notmuch-tree-message-buffer buffer))))
557
558 (defun notmuch-tree-show-message-out ()
559   "Show the current message (in whole window)."
560   (interactive)
561   (let ((id (notmuch-tree-get-message-id))
562         (inhibit-read-only t)
563         buffer)
564     (when id
565       ;; We close the window to kill off un-needed buffers.
566       (notmuch-tree-close-message-window)
567       (notmuch-show id))))
568
569 (defun notmuch-tree-show-message (arg)
570   "Show the current message.
571
572 Shows in split pane or whole window according to value of
573 `notmuch-tree-show-out'. A prefix argument reverses the choice."
574   (interactive "P")
575   (if (or (and (notmuch-tree-show-out) (not arg))
576           (and (not (notmuch-tree-show-out)) arg))
577       (notmuch-tree-show-message-out)
578     (notmuch-tree-show-message-in)))
579
580 (defun notmuch-tree-scroll-message-window ()
581   "Scroll the message window (if it exists)."
582   (interactive)
583   (when (window-live-p notmuch-tree-message-window)
584     (with-selected-window notmuch-tree-message-window
585       (if (pos-visible-in-window-p (point-max))
586           t
587         (scroll-up)))))
588
589 (defun notmuch-tree-scroll-message-window-back ()
590   "Scroll the message window back(if it exists)."
591   (interactive)
592   (when (window-live-p notmuch-tree-message-window)
593     (with-selected-window notmuch-tree-message-window
594       (if (pos-visible-in-window-p (point-min))
595           t
596         (scroll-down)))))
597
598 (defun notmuch-tree-scroll-or-next ()
599   "Scroll the message window. If it at end go to next message."
600   (interactive)
601   (when (notmuch-tree-scroll-message-window)
602     (notmuch-tree-next-matching-message)))
603
604 (defun notmuch-tree-quit (&optional kill-both)
605   "Close the split view or exit tree."
606   (interactive "P")
607   (when (or (not (notmuch-tree-close-message-window)) kill-both)
608     (kill-buffer (current-buffer))))
609
610 (defun notmuch-tree-close-message-window ()
611   "Close the message-window. Return t if close succeeds."
612   (interactive)
613   (when (and (window-live-p notmuch-tree-message-window)
614              (eq (window-buffer notmuch-tree-message-window)
615                  notmuch-tree-message-buffer))
616     (delete-window notmuch-tree-message-window)
617     (unless (get-buffer-window-list notmuch-tree-message-buffer)
618       (kill-buffer notmuch-tree-message-buffer))
619     t))
620
621 (defun notmuch-tree-archive-message (&optional unarchive)
622   "Archive the current message.
623
624 Archive the current message by applying the tag changes in
625 `notmuch-archive-tags' to it. If a prefix argument is given, the
626 message will be \"unarchived\", i.e. the tag changes in
627 `notmuch-archive-tags' will be reversed."
628   (interactive "P")
629   (when notmuch-archive-tags
630     (notmuch-tree-tag
631      (notmuch-tag-change-list notmuch-archive-tags unarchive))))
632
633 (defun notmuch-tree-archive-message-then-next (&optional unarchive)
634   "Archive the current message and move to next matching message."
635   (interactive "P")
636   (notmuch-tree-archive-message unarchive)
637   (notmuch-tree-next-matching-message))
638
639 (defun notmuch-tree-archive-thread-then-exit ()
640   "Archive all messages in the current buffer, then exit notmuch-tree."
641   (interactive)
642   (notmuch-tree-archive-thread)
643   (notmuch-tree-quit t))
644
645 (defun notmuch-tree-archive-message-then-next-or-exit ()
646   "Archive current message, then show next open message in current thread.
647
648 If at the last open message in the current thread, then exit back
649 to search results."
650   (interactive)
651   (notmuch-tree-archive-message)
652   (notmuch-tree-next-matching-message t))
653
654 (defun notmuch-tree-next-message ()
655   "Move to next message."
656   (interactive)
657   (forward-line)
658   (when (window-live-p notmuch-tree-message-window)
659     (notmuch-tree-show-message-in)))
660
661 (defun notmuch-tree-prev-message ()
662   "Move to previous message."
663   (interactive)
664   (forward-line -1)
665   (when (window-live-p notmuch-tree-message-window)
666     (notmuch-tree-show-message-in)))
667
668 (defun notmuch-tree-goto-matching-message (&optional prev)
669   "Move to the next or previous matching message.
670
671 Returns t if there was a next matching message in the thread to show,
672 nil otherwise."
673   (let ((dir (if prev -1 nil))
674         (eobfn (if prev #'bobp #'eobp)))
675     (while (and (not (funcall eobfn))
676                 (not (notmuch-tree-get-match)))
677       (forward-line dir))
678     (not (funcall eobfn))))
679
680 (defun notmuch-tree-matching-message (&optional prev pop-at-end)
681   "Move to the next or previous matching message."
682   (interactive "P")
683   (forward-line (if prev -1 nil))
684   (if (and (not (notmuch-tree-goto-matching-message prev)) pop-at-end)
685       (notmuch-tree-quit pop-at-end)
686     (when (window-live-p notmuch-tree-message-window)
687       (notmuch-tree-show-message-in))))
688
689 (defun notmuch-tree-prev-matching-message (&optional pop-at-end)
690   "Move to previous matching message."
691   (interactive "P")
692   (notmuch-tree-matching-message t pop-at-end))
693
694 (defun notmuch-tree-next-matching-message (&optional pop-at-end)
695   "Move to next matching message."
696   (interactive "P")
697   (notmuch-tree-matching-message nil pop-at-end))
698
699 (defun notmuch-tree-refresh-view (&optional view)
700   "Refresh view."
701   (interactive)
702   (when (get-buffer-process (current-buffer))
703     (error "notmuch tree process already running for current buffer"))
704   (let ((inhibit-read-only t)
705         (basic-query notmuch-tree-basic-query)
706         (unthreaded (cond ((eq view 'unthreaded) t)
707                           ((eq view 'tree) nil)
708                           (t notmuch-tree-unthreaded)))
709         (query-context notmuch-tree-query-context)
710         (target (notmuch-tree-get-message-id)))
711     (erase-buffer)
712     (notmuch-tree-worker basic-query
713                          query-context
714                          target
715                          nil
716                          unthreaded)))
717
718 (defun notmuch-tree-thread-top ()
719   (when (notmuch-tree-get-message-properties)
720     (while (not (or (notmuch-tree-get-prop :first) (eobp)))
721       (forward-line -1))))
722
723 (defun notmuch-tree-prev-thread ()
724   (interactive)
725   (forward-line -1)
726   (notmuch-tree-thread-top))
727
728 (defun notmuch-tree-next-thread ()
729   "Get the next thread in the current tree. Returns t if a thread was
730 found or nil if not."
731   (interactive)
732   (forward-line 1)
733   (while (not (or (notmuch-tree-get-prop :first) (eobp)))
734     (forward-line 1))
735   (not (eobp)))
736
737 (defun notmuch-tree-thread-mapcar (function)
738   "Iterate through all messages in the current thread
739  and call FUNCTION for side effects."
740   (save-excursion
741     (notmuch-tree-thread-top)
742     (cl-loop collect (funcall function)
743              do (forward-line)
744              while (and (notmuch-tree-get-message-properties)
745                         (not (notmuch-tree-get-prop :first))))))
746
747 (defun notmuch-tree-get-messages-ids-thread-search ()
748   "Return a search string for all message ids of messages in the current thread."
749   (mapconcat 'identity
750              (notmuch-tree-thread-mapcar 'notmuch-tree-get-message-id)
751              " or "))
752
753 (defun notmuch-tree-tag-thread (tag-changes)
754   "Tag all messages in the current thread."
755   (interactive
756    (let ((tags (apply #'append (notmuch-tree-thread-mapcar
757                                 (lambda () (notmuch-tree-get-tags))))))
758      (list (notmuch-read-tag-changes tags "Tag thread"))))
759   (when (notmuch-tree-get-message-properties)
760     (notmuch-tag (notmuch-tree-get-messages-ids-thread-search) tag-changes)
761     (notmuch-tree-thread-mapcar
762      (lambda () (notmuch-tree-tag-update-display tag-changes)))))
763
764 (defun notmuch-tree-archive-thread (&optional unarchive)
765   "Archive each message in thread.
766
767 Archive each message currently shown by applying the tag changes
768 in `notmuch-archive-tags' to each. If a prefix argument is given,
769 the messages will be \"unarchived\", i.e. the tag changes in
770 `notmuch-archive-tags' will be reversed.
771
772 Note: This command is safe from any race condition of new messages
773 being delivered to the same thread. It does not archive the
774 entire thread, but only the messages shown in the current
775 buffer."
776   (interactive "P")
777   (when notmuch-archive-tags
778     (notmuch-tree-tag-thread
779      (notmuch-tag-change-list notmuch-archive-tags unarchive))))
780
781 ;; Functions below here display the tree buffer itself.
782
783 (defun notmuch-tree-clean-address (address)
784   "Try to clean a single email ADDRESS for display. Return
785 AUTHOR_NAME if present, otherwise return AUTHOR_EMAIL. Return
786 unchanged ADDRESS if parsing fails."
787   (let* ((clean-address (notmuch-clean-address address))
788          (p-address (car clean-address))
789          (p-name (cdr clean-address)))
790
791     ;; If we have a name return that otherwise return the address.
792     (or p-name p-address)))
793
794 (defun notmuch-tree-format-field (field format-string msg)
795   "Format a FIELD of MSG according to FORMAT-STRING and return string."
796   (let* ((headers (plist-get msg :headers))
797          (match (plist-get msg :match)))
798     (cond
799      ((listp field)
800       (format format-string (notmuch-tree-format-field-list field msg)))
801
802      ((string-equal field "date")
803       (let ((face (if match
804                       'notmuch-tree-match-date-face
805                     'notmuch-tree-no-match-date-face)))
806         (propertize (format format-string (plist-get msg :date_relative))
807                     'face face)))
808
809      ((string-equal field "tree")
810       (let ((tree-status (plist-get msg :tree-status))
811             (face (if match
812                       'notmuch-tree-match-tree-face
813                     'notmuch-tree-no-match-tree-face)))
814
815         (propertize (format format-string
816                             (mapconcat #'identity (reverse tree-status) ""))
817                     'face face)))
818
819      ((string-equal field "subject")
820       (let ((bare-subject (notmuch-show-strip-re (plist-get headers :Subject)))
821             (previous-subject notmuch-tree-previous-subject)
822             (face (if match
823                       'notmuch-tree-match-subject-face
824                     'notmuch-tree-no-match-subject-face)))
825
826         (setq notmuch-tree-previous-subject bare-subject)
827         (propertize (format format-string
828                             (if (string= previous-subject bare-subject)
829                                 " ..."
830                               bare-subject))
831                     'face face)))
832
833      ((string-equal field "authors")
834       (let ((author (notmuch-tree-clean-address (plist-get headers :From)))
835             (len (length (format format-string "")))
836             (face (if match
837                       'notmuch-tree-match-author-face
838                     'notmuch-tree-no-match-author-face)))
839         (when (> (length author) len)
840           (setq author (substring author 0 len)))
841         (propertize (format format-string author) 'face face)))
842
843      ((string-equal field "tags")
844       (let ((tags (plist-get msg :tags))
845             (orig-tags (plist-get msg :orig-tags))
846             (face (if match
847                       'notmuch-tree-match-tag-face
848                     'notmuch-tree-no-match-tag-face)))
849         (format format-string (notmuch-tag-format-tags tags orig-tags face)))))))
850
851 (defun notmuch-tree-format-field-list (field-list msg)
852   "Format fields of MSG according to FIELD-LIST and return string."
853   (let ((face (if (plist-get msg :match)
854                   'notmuch-tree-match-face
855                 'notmuch-tree-no-match-face))
856         (result-string))
857     (dolist (spec field-list result-string)
858       (let ((field-string (notmuch-tree-format-field (car spec) (cdr spec) msg)))
859         (setq result-string (concat result-string field-string))))
860     (notmuch-apply-face result-string face t)))
861
862 (defun notmuch-tree-insert-msg (msg)
863   "Insert the message MSG according to notmuch-tree-result-format."
864   ;; We need to save the previous subject as it will get overwritten
865   ;; by the insert-field calls.
866   (let ((previous-subject notmuch-tree-previous-subject))
867     (insert (notmuch-tree-format-field-list (notmuch-tree-result-format) msg))
868     (notmuch-tree-set-message-properties msg)
869     (notmuch-tree-set-prop :previous-subject previous-subject)
870     (insert "\n")))
871
872 (defun notmuch-tree-goto-and-insert-msg (msg)
873   "Insert msg at the end of the buffer. Move point to msg if it is the target."
874   (save-excursion
875     (goto-char (point-max))
876     (notmuch-tree-insert-msg msg))
877   (let ((msg-id (notmuch-id-to-query (plist-get msg :id)))
878         (target notmuch-tree-target-msg))
879     (when (or (and (not target) (plist-get msg :match))
880               (string= msg-id target))
881       (setq notmuch-tree-target-msg "found")
882       (goto-char (point-max))
883       (forward-line -1)
884       (when notmuch-tree-open-target
885         (notmuch-tree-show-message-in)))))
886
887 (defun notmuch-tree-insert-tree (tree depth tree-status first last)
888   "Insert the message tree TREE at depth DEPTH in the current thread.
889
890 A message tree is another name for a single sub-thread: i.e., a
891 message together with all its descendents."
892   (let ((msg (car tree))
893         (replies (cadr tree)))
894     (cond
895      ((and (< 0 depth) (not last))
896       (push "├" tree-status))
897      ((and (< 0 depth) last)
898       (push "╰" tree-status))
899      ((and (eq 0 depth) first last)
900       ;; Choice between these two variants is a matter of taste.
901       ;; (push "─" tree-status))
902       (push " " tree-status))
903      ((and (eq 0 depth) first (not last))
904       (push "┬" tree-status))
905      ((and (eq 0 depth) (not first) last)
906       (push "╰" tree-status))
907      ((and (eq 0 depth) (not first) (not last))
908       (push "├" tree-status)))
909     (push (concat (if replies "┬" "─") "►") tree-status)
910     (setq msg (plist-put msg :first (and first (eq 0 depth))))
911     (setq msg (plist-put msg :tree-status tree-status))
912     (setq msg (plist-put msg :orig-tags (plist-get msg :tags)))
913     (notmuch-tree-goto-and-insert-msg msg)
914     (pop tree-status)
915     (pop tree-status)
916     (if last
917         (push " " tree-status)
918       (push "│" tree-status))
919     (notmuch-tree-insert-thread replies (1+ depth) tree-status)))
920
921 (defun notmuch-tree-insert-thread (thread depth tree-status)
922   "Insert the collection of sibling sub-threads THREAD at depth DEPTH in the current forest."
923   (let ((n (length thread)))
924     (cl-loop for tree in thread
925              for count from 1 to n
926              do (notmuch-tree-insert-tree tree depth tree-status
927                                           (eq count 1)
928                                           (eq count n)))))
929
930 (defun notmuch-tree-insert-forest-thread (forest-thread)
931   "Insert a single complete thread."
932   (let (tree-status)
933     ;; Reset at the start of each main thread.
934     (setq notmuch-tree-previous-subject nil)
935     (notmuch-tree-insert-thread forest-thread 0 tree-status)))
936
937 (defun notmuch-tree-insert-forest (forest)
938   "Insert a forest of threads.
939
940 This function inserts a collection of several complete threads as
941 passed to it by notmuch-tree-process-filter."
942   (mapc 'notmuch-tree-insert-forest-thread forest))
943
944 (define-derived-mode notmuch-tree-mode fundamental-mode "notmuch-tree"
945   "Major mode displaying messages (as opposed to threads) of a notmuch search.
946
947 This buffer contains the results of a \"notmuch tree\" of your
948 email archives. Each line in the buffer represents a single
949 message giving the relative date, the author, subject, and any
950 tags.
951
952 Pressing \\[notmuch-tree-show-message] on any line displays that message.
953
954 Complete list of currently available key bindings:
955
956 \\{notmuch-tree-mode-map}"
957   (setq notmuch-buffer-refresh-function #'notmuch-tree-refresh-view)
958   (hl-line-mode 1)
959   (setq buffer-read-only t)
960   (setq truncate-lines t))
961
962 (defun notmuch-tree-process-sentinel (proc msg)
963   "Add a message to let user know when \"notmuch tree\" exits."
964   (let ((buffer (process-buffer proc))
965         (status (process-status proc))
966         (exit-status (process-exit-status proc))
967         (never-found-target-thread nil))
968     (when (memq status '(exit signal))
969       (kill-buffer (process-get proc 'parse-buf))
970       (if (buffer-live-p buffer)
971           (with-current-buffer buffer
972             (save-excursion
973               (let ((inhibit-read-only t)
974                     (atbob (bobp)))
975                 (goto-char (point-max))
976                 (if (eq status 'signal)
977                     (insert "Incomplete search results (tree view process was killed).\n"))
978                 (when (eq status 'exit)
979                   (insert "End of search results.")
980                   (unless (= exit-status 0)
981                     (insert (format " (process returned %d)" exit-status)))
982                   (insert "\n")))))))))
983
984 (defun notmuch-tree-process-filter (proc string)
985   "Process and filter the output of \"notmuch show\" for tree view."
986   (let ((results-buf (process-buffer proc))
987         (parse-buf (process-get proc 'parse-buf))
988         (inhibit-read-only t)
989         done)
990     (if (not (buffer-live-p results-buf))
991         (delete-process proc)
992       (with-current-buffer parse-buf
993         ;; Insert new data
994         (save-excursion
995           (goto-char (point-max))
996           (insert string))
997         (notmuch-sexp-parse-partial-list 'notmuch-tree-insert-forest-thread
998                                          results-buf)))))
999
1000 (defun notmuch-tree-worker (basic-query &optional query-context target open-target unthreaded)
1001   "Insert the tree view of the search in the current buffer.
1002
1003 This is is a helper function for notmuch-tree. The arguments are
1004 the same as for the function notmuch-tree."
1005   (interactive)
1006   (notmuch-tree-mode)
1007   (add-hook 'post-command-hook #'notmuch-tree-command-hook t t)
1008   (setq notmuch-tree-unthreaded unthreaded)
1009   (setq notmuch-tree-basic-query basic-query)
1010   (setq notmuch-tree-query-context (if (or (string= query-context "")
1011                                            (string= query-context "*"))
1012                                        nil query-context))
1013   (setq notmuch-tree-target-msg target)
1014   (setq notmuch-tree-open-target open-target)
1015   ;; Set the default value for `notmuch-show-process-crypto' in this
1016   ;; buffer. Although we don't use this some of the functions we call
1017   ;; (such as reply) do. It is a buffer local variable so setting it
1018   ;; will not affect genuine show buffers.
1019   (setq notmuch-show-process-crypto notmuch-crypto-process-mime)
1020   (erase-buffer)
1021   (goto-char (point-min))
1022   (let* ((search-args (concat basic-query
1023                               (if query-context
1024                                   (concat " and (" query-context ")"))))
1025          (message-arg (if unthreaded "--unthreaded" "--entire-thread")))
1026     (if (equal (car (process-lines notmuch-command "count" search-args)) "0")
1027         (setq search-args basic-query))
1028     (notmuch-tag-clear-cache)
1029     (let ((proc (notmuch-start-notmuch
1030                  "notmuch-tree" (current-buffer) #'notmuch-tree-process-sentinel
1031                  "show" "--body=false" "--format=sexp" "--format-version=4"
1032                  message-arg search-args))
1033           ;; Use a scratch buffer to accumulate partial output.
1034           ;; This buffer will be killed by the sentinel, which
1035           ;; should be called no matter how the process dies.
1036           (parse-buf (generate-new-buffer " *notmuch tree parse*")))
1037       (process-put proc 'parse-buf parse-buf)
1038       (set-process-filter proc 'notmuch-tree-process-filter)
1039       (set-process-query-on-exit-flag proc nil))))
1040
1041 (defun notmuch-tree-get-query ()
1042   "Return the current query in this tree buffer."
1043   (if notmuch-tree-query-context
1044       (concat notmuch-tree-basic-query
1045               " and ("
1046               notmuch-tree-query-context
1047               ")")
1048     notmuch-tree-basic-query))
1049
1050 (defun notmuch-tree (&optional query query-context target buffer-name open-target unthreaded)
1051   "Display threads matching QUERY in tree view.
1052
1053 The arguments are:
1054   QUERY: the main query. This can be any query but in many cases will be
1055       a single thread. If nil this is read interactively from the minibuffer.
1056   QUERY-CONTEXT: is an additional term for the query. The query used
1057       is QUERY and QUERY-CONTEXT unless that does not match any messages
1058       in which case we fall back to just QUERY.
1059   TARGET: A message ID (with the id: prefix) that will be made
1060       current if it appears in the tree view results.
1061   BUFFER-NAME: the name of the buffer to display the tree view. If
1062       it is nil \"*notmuch-tree\" followed by QUERY is used.
1063   OPEN-TARGET: If TRUE open the target message in the message pane.
1064   UNTHREADED: If TRUE only show matching messages in an unthreaded view."
1065   (interactive)
1066   (if (null query)
1067       (setq query (notmuch-read-query (concat "Notmuch "
1068                                               (if unthreaded "unthreaded " "tree ")
1069                                               "view search: "))))
1070   (let ((buffer (get-buffer-create (generate-new-buffer-name
1071                                     (or buffer-name
1072                                         (concat "*notmuch-"
1073                                                 (if unthreaded "unthreaded-" "tree-")
1074                                                 query "*")))))
1075         (inhibit-read-only t))
1076     (switch-to-buffer buffer))
1077   ;; Don't track undo information for this buffer
1078   (set 'buffer-undo-list t)
1079   (notmuch-tree-worker query query-context target open-target unthreaded)
1080   (setq truncate-lines t))
1081
1082 (defun notmuch-unthreaded (&optional query query-context target buffer-name open-target)
1083   (interactive)
1084   (notmuch-tree query query-context target buffer-name open-target t))
1085
1086 ;;
1087
1088 (provide 'notmuch-tree)
1089
1090 ;;; notmuch-tree.el ends here