]> git.notmuchmail.org Git - notmuch/blob - emacs/notmuch-tree.el
364da2402887ea57a5ab5c829c738631e1298339
[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.
600 If it at end go to next message."
601   (interactive)
602   (when (notmuch-tree-scroll-message-window)
603     (notmuch-tree-next-matching-message)))
604
605 (defun notmuch-tree-quit (&optional kill-both)
606   "Close the split view or exit tree."
607   (interactive "P")
608   (when (or (not (notmuch-tree-close-message-window)) kill-both)
609     (kill-buffer (current-buffer))))
610
611 (defun notmuch-tree-close-message-window ()
612   "Close the message-window. Return t if close succeeds."
613   (interactive)
614   (when (and (window-live-p notmuch-tree-message-window)
615              (eq (window-buffer notmuch-tree-message-window)
616                  notmuch-tree-message-buffer))
617     (delete-window notmuch-tree-message-window)
618     (unless (get-buffer-window-list notmuch-tree-message-buffer)
619       (kill-buffer notmuch-tree-message-buffer))
620     t))
621
622 (defun notmuch-tree-archive-message (&optional unarchive)
623   "Archive the current message.
624
625 Archive the current message by applying the tag changes in
626 `notmuch-archive-tags' to it. If a prefix argument is given, the
627 message will be \"unarchived\", i.e. the tag changes in
628 `notmuch-archive-tags' will be reversed."
629   (interactive "P")
630   (when notmuch-archive-tags
631     (notmuch-tree-tag
632      (notmuch-tag-change-list notmuch-archive-tags unarchive))))
633
634 (defun notmuch-tree-archive-message-then-next (&optional unarchive)
635   "Archive the current message and move to next matching message."
636   (interactive "P")
637   (notmuch-tree-archive-message unarchive)
638   (notmuch-tree-next-matching-message))
639
640 (defun notmuch-tree-archive-thread-then-exit ()
641   "Archive all messages in the current buffer, then exit notmuch-tree."
642   (interactive)
643   (notmuch-tree-archive-thread)
644   (notmuch-tree-quit t))
645
646 (defun notmuch-tree-archive-message-then-next-or-exit ()
647   "Archive current message, then show next open message in current thread.
648
649 If at the last open message in the current thread, then exit back
650 to search results."
651   (interactive)
652   (notmuch-tree-archive-message)
653   (notmuch-tree-next-matching-message t))
654
655 (defun notmuch-tree-next-message ()
656   "Move to next message."
657   (interactive)
658   (forward-line)
659   (when (window-live-p notmuch-tree-message-window)
660     (notmuch-tree-show-message-in)))
661
662 (defun notmuch-tree-prev-message ()
663   "Move to previous message."
664   (interactive)
665   (forward-line -1)
666   (when (window-live-p notmuch-tree-message-window)
667     (notmuch-tree-show-message-in)))
668
669 (defun notmuch-tree-goto-matching-message (&optional prev)
670   "Move to the next or previous matching message.
671
672 Returns t if there was a next matching message in the thread to show,
673 nil otherwise."
674   (let ((dir (if prev -1 nil))
675         (eobfn (if prev #'bobp #'eobp)))
676     (while (and (not (funcall eobfn))
677                 (not (notmuch-tree-get-match)))
678       (forward-line dir))
679     (not (funcall eobfn))))
680
681 (defun notmuch-tree-matching-message (&optional prev pop-at-end)
682   "Move to the next or previous matching message."
683   (interactive "P")
684   (forward-line (if prev -1 nil))
685   (if (and (not (notmuch-tree-goto-matching-message prev)) pop-at-end)
686       (notmuch-tree-quit pop-at-end)
687     (when (window-live-p notmuch-tree-message-window)
688       (notmuch-tree-show-message-in))))
689
690 (defun notmuch-tree-prev-matching-message (&optional pop-at-end)
691   "Move to previous matching message."
692   (interactive "P")
693   (notmuch-tree-matching-message t pop-at-end))
694
695 (defun notmuch-tree-next-matching-message (&optional pop-at-end)
696   "Move to next matching message."
697   (interactive "P")
698   (notmuch-tree-matching-message nil pop-at-end))
699
700 (defun notmuch-tree-refresh-view (&optional view)
701   "Refresh view."
702   (interactive)
703   (when (get-buffer-process (current-buffer))
704     (error "notmuch tree process already running for current buffer"))
705   (let ((inhibit-read-only t)
706         (basic-query notmuch-tree-basic-query)
707         (unthreaded (cond ((eq view 'unthreaded) t)
708                           ((eq view 'tree) nil)
709                           (t notmuch-tree-unthreaded)))
710         (query-context notmuch-tree-query-context)
711         (target (notmuch-tree-get-message-id)))
712     (erase-buffer)
713     (notmuch-tree-worker basic-query
714                          query-context
715                          target
716                          nil
717                          unthreaded)))
718
719 (defun notmuch-tree-thread-top ()
720   (when (notmuch-tree-get-message-properties)
721     (while (not (or (notmuch-tree-get-prop :first) (eobp)))
722       (forward-line -1))))
723
724 (defun notmuch-tree-prev-thread ()
725   (interactive)
726   (forward-line -1)
727   (notmuch-tree-thread-top))
728
729 (defun notmuch-tree-next-thread ()
730   "Get the next thread in the current tree. Returns t if a thread was
731 found or nil if not."
732   (interactive)
733   (forward-line 1)
734   (while (not (or (notmuch-tree-get-prop :first) (eobp)))
735     (forward-line 1))
736   (not (eobp)))
737
738 (defun notmuch-tree-thread-mapcar (function)
739   "Iterate through all messages in the current thread
740  and call FUNCTION for side effects."
741   (save-excursion
742     (notmuch-tree-thread-top)
743     (cl-loop collect (funcall function)
744              do (forward-line)
745              while (and (notmuch-tree-get-message-properties)
746                         (not (notmuch-tree-get-prop :first))))))
747
748 (defun notmuch-tree-get-messages-ids-thread-search ()
749   "Return a search string for all message ids of messages in the current thread."
750   (mapconcat 'identity
751              (notmuch-tree-thread-mapcar 'notmuch-tree-get-message-id)
752              " or "))
753
754 (defun notmuch-tree-tag-thread (tag-changes)
755   "Tag all messages in the current thread."
756   (interactive
757    (let ((tags (apply #'append (notmuch-tree-thread-mapcar
758                                 (lambda () (notmuch-tree-get-tags))))))
759      (list (notmuch-read-tag-changes tags "Tag thread"))))
760   (when (notmuch-tree-get-message-properties)
761     (notmuch-tag (notmuch-tree-get-messages-ids-thread-search) tag-changes)
762     (notmuch-tree-thread-mapcar
763      (lambda () (notmuch-tree-tag-update-display tag-changes)))))
764
765 (defun notmuch-tree-archive-thread (&optional unarchive)
766   "Archive each message in thread.
767
768 Archive each message currently shown by applying the tag changes
769 in `notmuch-archive-tags' to each. If a prefix argument is given,
770 the messages will be \"unarchived\", i.e. the tag changes in
771 `notmuch-archive-tags' will be reversed.
772
773 Note: This command is safe from any race condition of new messages
774 being delivered to the same thread. It does not archive the
775 entire thread, but only the messages shown in the current
776 buffer."
777   (interactive "P")
778   (when notmuch-archive-tags
779     (notmuch-tree-tag-thread
780      (notmuch-tag-change-list notmuch-archive-tags unarchive))))
781
782 ;; Functions below here display the tree buffer itself.
783
784 (defun notmuch-tree-clean-address (address)
785   "Try to clean a single email ADDRESS for display. Return
786 AUTHOR_NAME if present, otherwise return AUTHOR_EMAIL. Return
787 unchanged ADDRESS if parsing fails."
788   (let* ((clean-address (notmuch-clean-address address))
789          (p-address (car clean-address))
790          (p-name (cdr clean-address)))
791
792     ;; If we have a name return that otherwise return the address.
793     (or p-name p-address)))
794
795 (defun notmuch-tree-format-field (field format-string msg)
796   "Format a FIELD of MSG according to FORMAT-STRING and return string."
797   (let* ((headers (plist-get msg :headers))
798          (match (plist-get msg :match)))
799     (cond
800      ((listp field)
801       (format format-string (notmuch-tree-format-field-list field msg)))
802
803      ((string-equal field "date")
804       (let ((face (if match
805                       'notmuch-tree-match-date-face
806                     'notmuch-tree-no-match-date-face)))
807         (propertize (format format-string (plist-get msg :date_relative))
808                     'face face)))
809
810      ((string-equal field "tree")
811       (let ((tree-status (plist-get msg :tree-status))
812             (face (if match
813                       'notmuch-tree-match-tree-face
814                     'notmuch-tree-no-match-tree-face)))
815
816         (propertize (format format-string
817                             (mapconcat #'identity (reverse tree-status) ""))
818                     'face face)))
819
820      ((string-equal field "subject")
821       (let ((bare-subject (notmuch-show-strip-re (plist-get headers :Subject)))
822             (previous-subject notmuch-tree-previous-subject)
823             (face (if match
824                       'notmuch-tree-match-subject-face
825                     'notmuch-tree-no-match-subject-face)))
826
827         (setq notmuch-tree-previous-subject bare-subject)
828         (propertize (format format-string
829                             (if (string= previous-subject bare-subject)
830                                 " ..."
831                               bare-subject))
832                     'face face)))
833
834      ((string-equal field "authors")
835       (let ((author (notmuch-tree-clean-address (plist-get headers :From)))
836             (len (length (format format-string "")))
837             (face (if match
838                       'notmuch-tree-match-author-face
839                     'notmuch-tree-no-match-author-face)))
840         (when (> (length author) len)
841           (setq author (substring author 0 len)))
842         (propertize (format format-string author) 'face face)))
843
844      ((string-equal field "tags")
845       (let ((tags (plist-get msg :tags))
846             (orig-tags (plist-get msg :orig-tags))
847             (face (if match
848                       'notmuch-tree-match-tag-face
849                     'notmuch-tree-no-match-tag-face)))
850         (format format-string (notmuch-tag-format-tags tags orig-tags face)))))))
851
852 (defun notmuch-tree-format-field-list (field-list msg)
853   "Format fields of MSG according to FIELD-LIST and return string."
854   (let ((face (if (plist-get msg :match)
855                   'notmuch-tree-match-face
856                 'notmuch-tree-no-match-face))
857         (result-string))
858     (dolist (spec field-list result-string)
859       (let ((field-string (notmuch-tree-format-field (car spec) (cdr spec) msg)))
860         (setq result-string (concat result-string field-string))))
861     (notmuch-apply-face result-string face t)))
862
863 (defun notmuch-tree-insert-msg (msg)
864   "Insert the message MSG according to notmuch-tree-result-format."
865   ;; We need to save the previous subject as it will get overwritten
866   ;; by the insert-field calls.
867   (let ((previous-subject notmuch-tree-previous-subject))
868     (insert (notmuch-tree-format-field-list (notmuch-tree-result-format) msg))
869     (notmuch-tree-set-message-properties msg)
870     (notmuch-tree-set-prop :previous-subject previous-subject)
871     (insert "\n")))
872
873 (defun notmuch-tree-goto-and-insert-msg (msg)
874   "Insert msg at the end of the buffer. Move point to msg if it is the target."
875   (save-excursion
876     (goto-char (point-max))
877     (notmuch-tree-insert-msg msg))
878   (let ((msg-id (notmuch-id-to-query (plist-get msg :id)))
879         (target notmuch-tree-target-msg))
880     (when (or (and (not target) (plist-get msg :match))
881               (string= msg-id target))
882       (setq notmuch-tree-target-msg "found")
883       (goto-char (point-max))
884       (forward-line -1)
885       (when notmuch-tree-open-target
886         (notmuch-tree-show-message-in)))))
887
888 (defun notmuch-tree-insert-tree (tree depth tree-status first last)
889   "Insert the message tree TREE at depth DEPTH in the current thread.
890
891 A message tree is another name for a single sub-thread: i.e., a
892 message together with all its descendents."
893   (let ((msg (car tree))
894         (replies (cadr tree)))
895     (cond
896      ((and (< 0 depth) (not last))
897       (push "├" tree-status))
898      ((and (< 0 depth) last)
899       (push "╰" tree-status))
900      ((and (eq 0 depth) first last)
901       ;; Choice between these two variants is a matter of taste.
902       ;; (push "─" tree-status))
903       (push " " tree-status))
904      ((and (eq 0 depth) first (not last))
905       (push "┬" tree-status))
906      ((and (eq 0 depth) (not first) last)
907       (push "╰" tree-status))
908      ((and (eq 0 depth) (not first) (not last))
909       (push "├" tree-status)))
910     (push (concat (if replies "┬" "─") "►") tree-status)
911     (setq msg (plist-put msg :first (and first (eq 0 depth))))
912     (setq msg (plist-put msg :tree-status tree-status))
913     (setq msg (plist-put msg :orig-tags (plist-get msg :tags)))
914     (notmuch-tree-goto-and-insert-msg msg)
915     (pop tree-status)
916     (pop tree-status)
917     (if last
918         (push " " tree-status)
919       (push "│" tree-status))
920     (notmuch-tree-insert-thread replies (1+ depth) tree-status)))
921
922 (defun notmuch-tree-insert-thread (thread depth tree-status)
923   "Insert the collection of sibling sub-threads THREAD at depth DEPTH in the current forest."
924   (let ((n (length thread)))
925     (cl-loop for tree in thread
926              for count from 1 to n
927              do (notmuch-tree-insert-tree tree depth tree-status
928                                           (eq count 1)
929                                           (eq count n)))))
930
931 (defun notmuch-tree-insert-forest-thread (forest-thread)
932   "Insert a single complete thread."
933   (let (tree-status)
934     ;; Reset at the start of each main thread.
935     (setq notmuch-tree-previous-subject nil)
936     (notmuch-tree-insert-thread forest-thread 0 tree-status)))
937
938 (defun notmuch-tree-insert-forest (forest)
939   "Insert a forest of threads.
940
941 This function inserts a collection of several complete threads as
942 passed to it by notmuch-tree-process-filter."
943   (mapc 'notmuch-tree-insert-forest-thread forest))
944
945 (define-derived-mode notmuch-tree-mode fundamental-mode "notmuch-tree"
946   "Major mode displaying messages (as opposed to threads) of a notmuch search.
947
948 This buffer contains the results of a \"notmuch tree\" of your
949 email archives. Each line in the buffer represents a single
950 message giving the relative date, the author, subject, and any
951 tags.
952
953 Pressing \\[notmuch-tree-show-message] on any line displays that message.
954
955 Complete list of currently available key bindings:
956
957 \\{notmuch-tree-mode-map}"
958   (setq notmuch-buffer-refresh-function #'notmuch-tree-refresh-view)
959   (hl-line-mode 1)
960   (setq buffer-read-only t)
961   (setq truncate-lines t))
962
963 (defun notmuch-tree-process-sentinel (proc msg)
964   "Add a message to let user know when \"notmuch tree\" exits."
965   (let ((buffer (process-buffer proc))
966         (status (process-status proc))
967         (exit-status (process-exit-status proc))
968         (never-found-target-thread nil))
969     (when (memq status '(exit signal))
970       (kill-buffer (process-get proc 'parse-buf))
971       (when (buffer-live-p buffer)
972         (with-current-buffer buffer
973           (save-excursion
974             (let ((inhibit-read-only t)
975                   (atbob (bobp)))
976               (goto-char (point-max))
977               (when (eq status 'signal)
978                 (insert "Incomplete search results (tree view process was killed).\n"))
979               (when (eq status 'exit)
980                 (insert "End of search results.")
981                 (unless (= exit-status 0)
982                   (insert (format " (process returned %d)" exit-status)))
983                 (insert "\n")))))))))
984
985 (defun notmuch-tree-process-filter (proc string)
986   "Process and filter the output of \"notmuch show\" for tree view."
987   (let ((results-buf (process-buffer proc))
988         (parse-buf (process-get proc 'parse-buf))
989         (inhibit-read-only t)
990         done)
991     (if (not (buffer-live-p results-buf))
992         (delete-process proc)
993       (with-current-buffer parse-buf
994         ;; Insert new data
995         (save-excursion
996           (goto-char (point-max))
997           (insert string))
998         (notmuch-sexp-parse-partial-list 'notmuch-tree-insert-forest-thread
999                                          results-buf)))))
1000
1001 (defun notmuch-tree-worker (basic-query &optional query-context target open-target unthreaded)
1002   "Insert the tree view of the search in the current buffer.
1003
1004 This is is a helper function for notmuch-tree. The arguments are
1005 the same as for the function notmuch-tree."
1006   (interactive)
1007   (notmuch-tree-mode)
1008   (add-hook 'post-command-hook #'notmuch-tree-command-hook t t)
1009   (setq notmuch-tree-unthreaded unthreaded)
1010   (setq notmuch-tree-basic-query basic-query)
1011   (setq notmuch-tree-query-context (if (or (string= query-context "")
1012                                            (string= query-context "*"))
1013                                        nil
1014                                      query-context))
1015   (setq notmuch-tree-target-msg target)
1016   (setq notmuch-tree-open-target open-target)
1017   ;; Set the default value for `notmuch-show-process-crypto' in this
1018   ;; buffer. Although we don't use this some of the functions we call
1019   ;; (such as reply) do. It is a buffer local variable so setting it
1020   ;; will not affect genuine show buffers.
1021   (setq notmuch-show-process-crypto notmuch-crypto-process-mime)
1022   (erase-buffer)
1023   (goto-char (point-min))
1024   (let* ((search-args (concat basic-query
1025                               (and query-context
1026                                    (concat " and (" query-context ")"))))
1027          (message-arg (if unthreaded "--unthreaded" "--entire-thread")))
1028     (when (equal (car (process-lines notmuch-command "count" search-args)) "0")
1029       (setq search-args basic-query))
1030     (notmuch-tag-clear-cache)
1031     (let ((proc (notmuch-start-notmuch
1032                  "notmuch-tree" (current-buffer) #'notmuch-tree-process-sentinel
1033                  "show" "--body=false" "--format=sexp" "--format-version=4"
1034                  message-arg search-args))
1035           ;; Use a scratch buffer to accumulate partial output.
1036           ;; This buffer will be killed by the sentinel, which
1037           ;; should be called no matter how the process dies.
1038           (parse-buf (generate-new-buffer " *notmuch tree parse*")))
1039       (process-put proc 'parse-buf parse-buf)
1040       (set-process-filter proc 'notmuch-tree-process-filter)
1041       (set-process-query-on-exit-flag proc nil))))
1042
1043 (defun notmuch-tree-get-query ()
1044   "Return the current query in this tree buffer."
1045   (if notmuch-tree-query-context
1046       (concat notmuch-tree-basic-query
1047               " and ("
1048               notmuch-tree-query-context
1049               ")")
1050     notmuch-tree-basic-query))
1051
1052 (defun notmuch-tree (&optional query query-context target buffer-name open-target unthreaded)
1053   "Display threads matching QUERY in tree view.
1054
1055 The arguments are:
1056   QUERY: the main query. This can be any query but in many cases will be
1057       a single thread. If nil this is read interactively from the minibuffer.
1058   QUERY-CONTEXT: is an additional term for the query. The query used
1059       is QUERY and QUERY-CONTEXT unless that does not match any messages
1060       in which case we fall back to just QUERY.
1061   TARGET: A message ID (with the id: prefix) that will be made
1062       current if it appears in the tree view results.
1063   BUFFER-NAME: the name of the buffer to display the tree view. If
1064       it is nil \"*notmuch-tree\" followed by QUERY is used.
1065   OPEN-TARGET: If TRUE open the target message in the message pane.
1066   UNTHREADED: If TRUE only show matching messages in an unthreaded view."
1067   (interactive)
1068   (unless query
1069     (setq query (notmuch-read-query (concat "Notmuch "
1070                                             (if unthreaded "unthreaded " "tree ")
1071                                             "view search: "))))
1072   (let ((buffer (get-buffer-create (generate-new-buffer-name
1073                                     (or buffer-name
1074                                         (concat "*notmuch-"
1075                                                 (if unthreaded "unthreaded-" "tree-")
1076                                                 query "*")))))
1077         (inhibit-read-only t))
1078     (switch-to-buffer buffer))
1079   ;; Don't track undo information for this buffer
1080   (set 'buffer-undo-list t)
1081   (notmuch-tree-worker query query-context target open-target unthreaded)
1082   (setq truncate-lines t))
1083
1084 (defun notmuch-unthreaded (&optional query query-context target buffer-name open-target)
1085   (interactive)
1086   (notmuch-tree query query-context target buffer-name open-target t))
1087
1088 ;;
1089
1090 (provide 'notmuch-tree)
1091
1092 ;;; notmuch-tree.el ends here