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