]> git.notmuchmail.org Git - notmuch/blob - notmuch.el
notmuch.el: convert sparse keymap to a list in notmuch-substitute-one-command-key...
[notmuch] / notmuch.el
1 ; notmuch.el --- run notmuch within emacs
2 ;
3 ; Copyright © Carl Worth
4 ;
5 ; This file is part of Notmuch.
6 ;
7 ; Notmuch is free software: you can redistribute it and/or modify it
8 ; under the terms of the GNU General Public License as published by
9 ; the Free Software Foundation, either version 3 of the License, or
10 ; (at your option) any later version.
11 ;
12 ; Notmuch is distributed in the hope that it will be useful, but
13 ; WITHOUT ANY WARRANTY; without even the implied warranty of
14 ; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 ; General Public License for more details.
16 ;
17 ; You should have received a copy of the GNU General Public License
18 ; along with Notmuch.  If not, see <http://www.gnu.org/licenses/>.
19 ;
20 ; Authors: Carl Worth <cworth@cworth.org>
21
22 ; This is an emacs-based interface to the notmuch mail system.
23 ;
24 ; You will first need to have the notmuch program installed and have a
25 ; notmuch database built in order to use this. See
26 ; http://notmuchmail.org for details.
27 ;
28 ; To install this software, copy it to a directory that is on the
29 ; `load-path' variable within emacs (a good candidate is
30 ; /usr/local/share/emacs/site-lisp). If you are viewing this from the
31 ; notmuch source distribution then you can simply run:
32 ;
33 ;       sudo make install-emacs
34 ;
35 ; to install it.
36 ;
37 ; Then, to actually run it, add:
38 ;
39 ;       (require 'notmuch)
40 ;
41 ; to your ~/.emacs file, and then run "M-x notmuch" from within emacs,
42 ; or run:
43 ;
44 ;       emacs -f notmuch
45 ;
46 ; Have fun, and let us know if you have any comment, questions, or
47 ; kudos: Notmuch list <notmuch@notmuchmail.org> (subscription is not
48 ; required, but is available from http://notmuchmail.org).
49
50 (require 'cl)
51 (require 'mm-view)
52 (require 'message)
53
54 (defvar notmuch-show-mode-map
55   (let ((map (make-sparse-keymap)))
56     (define-key map "?" 'notmuch-help)
57     (define-key map "q" 'kill-this-buffer)
58     (define-key map (kbd "C-p") 'notmuch-show-previous-line)
59     (define-key map (kbd "C-n") 'notmuch-show-next-line)
60     (define-key map (kbd "M-TAB") 'notmuch-show-previous-button)
61     (define-key map (kbd "TAB") 'notmuch-show-next-button)
62     (define-key map "s" 'notmuch-search)
63     (define-key map "m" 'message-mail)
64     (define-key map "f" 'notmuch-show-forward-current)
65     (define-key map "r" 'notmuch-show-reply)
66     (define-key map "|" 'notmuch-show-pipe-message)
67     (define-key map "w" 'notmuch-show-save-attachments)
68     (define-key map "V" 'notmuch-show-view-raw-message)
69     (define-key map "v" 'notmuch-show-view-all-mime-parts)
70     (define-key map "b" 'notmuch-show-toggle-current-body)
71     (define-key map "h" 'notmuch-show-toggle-current-header)
72     (define-key map "-" 'notmuch-show-remove-tag)
73     (define-key map "+" 'notmuch-show-add-tag)
74     (define-key map "X" 'notmuch-show-mark-read-then-archive-then-exit)
75     (define-key map "x" 'notmuch-show-archive-thread-then-exit)
76     (define-key map "A" 'notmuch-show-mark-read-then-archive-thread)
77     (define-key map "a" 'notmuch-show-archive-thread)
78     (define-key map "p" 'notmuch-show-previous-message)
79     (define-key map "N" 'notmuch-show-mark-read-then-next-open-message)
80     (define-key map "n" 'notmuch-show-next-message)
81     (define-key map (kbd "DEL") 'notmuch-show-rewind)
82     (define-key map " " 'notmuch-show-advance-marking-read-and-archiving)
83     map)
84   "Keymap for \"notmuch show\" buffers.")
85 (fset 'notmuch-show-mode-map notmuch-show-mode-map)
86
87 (defvar notmuch-show-signature-regexp "\\(-- ?\\|_+\\)$"
88   "Pattern to match a line that separates content from signature.
89
90 The regexp can (and should) include $ to match the end of the
91 line, but should not include ^ to match the beginning of the
92 line. This is because notmuch may have inserted additional space
93 for indentation at the beginning of the line. But notmuch will
94 move past the indentation when testing this pattern, (so that the
95 pattern can still test against the entire line).")
96
97 (defvar notmuch-show-signature-button-format
98   "[ %d-line signature. Click/Enter to toggle visibility. ]"
99   "String used to construct button text for hidden signatures
100
101 Can use up to one integer format parameter, i.e. %d")
102
103 (defvar notmuch-show-citation-button-format
104   "[ %d more citation lines. Click/Enter to toggle visibility. ]"
105   "String used to construct button text for hidden citations.
106
107 Can use up to one integer format parameter, i.e. %d")
108
109 (defvar notmuch-show-signature-lines-max 12
110   "Maximum length of signature that will be hidden by default.")
111
112 (defvar notmuch-show-citation-lines-prefix 4
113   "Always show at least this many lines of a citation.
114
115 If there is one more line, show that, otherwise collapse
116 remaining lines into a button.")
117
118 (defvar notmuch-command "notmuch"
119   "Command to run the notmuch binary.")
120
121 (defvar notmuch-show-message-begin-regexp    "\fmessage{")
122 (defvar notmuch-show-message-end-regexp      "\fmessage}")
123 (defvar notmuch-show-header-begin-regexp     "\fheader{")
124 (defvar notmuch-show-header-end-regexp       "\fheader}")
125 (defvar notmuch-show-body-begin-regexp       "\fbody{")
126 (defvar notmuch-show-body-end-regexp         "\fbody}")
127 (defvar notmuch-show-attachment-begin-regexp "\fattachment{")
128 (defvar notmuch-show-attachment-end-regexp   "\fattachment}")
129 (defvar notmuch-show-part-begin-regexp       "\fpart{")
130 (defvar notmuch-show-part-end-regexp         "\fpart}")
131 (defvar notmuch-show-marker-regexp "\f\\(message\\|header\\|body\\|attachment\\|part\\)[{}].*$")
132
133 (defvar notmuch-show-id-regexp "\\(id:[^ ]*\\)")
134 (defvar notmuch-show-depth-match-regexp " depth:\\([0-9]*\\).*match:\\([01]\\) ")
135 (defvar notmuch-show-filename-regexp "filename:\\(.*\\)$")
136 (defvar notmuch-show-contentype-regexp "Content-type: \\(.*\\)")
137
138 (defvar notmuch-show-tags-regexp "(\\([^)]*\\))$")
139
140 (defvar notmuch-show-parent-buffer nil)
141 (defvar notmuch-show-body-read-visible nil)
142 (defvar notmuch-show-citations-visible nil)
143 (defvar notmuch-show-signatures-visible nil)
144 (defvar notmuch-show-headers-visible nil)
145
146 ; XXX: This should be a generic function in emacs somewhere, not here
147 (defun point-invisible-p ()
148   "Return whether the character at point is invisible.
149
150 Here visibility is determined by `buffer-invisibility-spec' and
151 the invisible property of any overlays for point. It doesn't have
152 anything to do with whether point is currently being displayed
153 within the current window."
154   (let ((prop (get-char-property (point) 'invisible)))
155     (if (eq buffer-invisibility-spec t)
156         prop
157       (or (memq prop buffer-invisibility-spec)
158           (assq prop buffer-invisibility-spec)))))
159
160 (defun notmuch-select-tag-with-completion (prompt &rest search-terms)
161   (let ((tag-list
162          (with-output-to-string
163            (with-current-buffer standard-output
164              (apply 'call-process notmuch-command nil t nil "search-tags" search-terms)))))
165     (completing-read prompt (split-string tag-list "\n+" t) nil nil nil)))
166
167 (defun notmuch-show-next-line ()
168   "Like builtin `next-line' but ensuring we end on a visible character.
169
170 By advancing forward until reaching a visible character.
171
172 Unlike builtin `next-line' this version accepts no arguments."
173   (interactive)
174   (set 'this-command 'next-line)
175   (call-interactively 'next-line)
176   (while (point-invisible-p)
177     (forward-char)))
178
179 (defun notmuch-show-previous-line ()
180   "Like builtin `previous-line' but ensuring we end on a visible character.
181
182 By advancing forward until reaching a visible character.
183
184 Unlike builtin `previous-line' this version accepts no arguments."
185   (interactive)
186   (set 'this-command 'previous-line)
187   (call-interactively 'previous-line)
188   (while (point-invisible-p)
189     (forward-char)))
190
191 (defun notmuch-show-get-message-id ()
192   (save-excursion
193     (beginning-of-line)
194     (if (not (looking-at notmuch-show-message-begin-regexp))
195         (re-search-backward notmuch-show-message-begin-regexp))
196     (re-search-forward notmuch-show-id-regexp)
197     (buffer-substring-no-properties (match-beginning 1) (match-end 1))))
198
199 (defun notmuch-show-get-filename ()
200   (save-excursion
201     (beginning-of-line)
202     (if (not (looking-at notmuch-show-message-begin-regexp))
203         (re-search-backward notmuch-show-message-begin-regexp))
204     (re-search-forward notmuch-show-filename-regexp)
205     (buffer-substring-no-properties (match-beginning 1) (match-end 1))))
206
207 (defun notmuch-show-set-tags (tags)
208   (save-excursion
209     (beginning-of-line)
210     (if (not (looking-at notmuch-show-message-begin-regexp))
211         (re-search-backward notmuch-show-message-begin-regexp))
212     (re-search-forward notmuch-show-tags-regexp)
213     (let ((inhibit-read-only t)
214           (beg (match-beginning 1))
215           (end (match-end 1)))
216       (delete-region beg end)
217       (goto-char beg)
218       (insert (mapconcat 'identity tags " ")))))
219
220 (defun notmuch-show-get-tags ()
221   (save-excursion
222     (beginning-of-line)
223     (if (not (looking-at notmuch-show-message-begin-regexp))
224         (re-search-backward notmuch-show-message-begin-regexp))
225     (re-search-forward notmuch-show-tags-regexp)
226     (split-string (buffer-substring (match-beginning 1) (match-end 1)))))
227
228 (defun notmuch-show-get-bcc ()
229   "Return BCC address(es) of current message"
230   (notmuch-show-get-header-field 'bcc))
231
232 (defun notmuch-show-get-cc ()
233   "Return CC address(es) of current message"
234   (notmuch-show-get-header-field 'cc))
235
236 (defun notmuch-show-get-date ()
237   "Return Date of current message"
238   (notmuch-show-get-header-field 'date))
239
240 (defun notmuch-show-get-from ()
241   "Return From address of current message"
242   (notmuch-show-get-header-field 'from))
243
244 (defun notmuch-show-get-subject ()
245   "Return Subject of current message"
246   (notmuch-show-get-header-field 'subject))
247
248 (defun notmuch-show-get-to ()
249   "Return To address(es) of current message"
250   (notmuch-show-get-header-field 'to))
251
252 (defun notmuch-show-get-header-field (name)
253   "Retrieve the header field NAME from the current message.
254 NAME should be a symbol, in lower case, as returned by
255 mail-header-extract-no-properties"
256   (let* ((result (assoc name (notmuch-show-get-header)))
257         (val (and result (cdr result))))
258     val))
259
260 (defun notmuch-show-get-header ()
261   "Retrieve and parse the header from the current message. Returns an alist with of (header . value)
262 where header is a symbol and value is a string.  The summary from notmuch-show is returned as the
263 pseudoheader summary"
264   (require 'mailheader)
265   (save-excursion
266     (beginning-of-line)
267     (if (not (looking-at notmuch-show-message-begin-regexp))
268         (re-search-backward notmuch-show-message-begin-regexp))
269     (re-search-forward (concat notmuch-show-header-begin-regexp "\n[[:space:]]*\\(.*\\)\n"))
270     (let* ((summary (buffer-substring-no-properties (match-beginning 1) (match-end 1)))
271           (beg (point)))
272       (re-search-forward notmuch-show-header-end-regexp)
273       (let ((text (buffer-substring beg (match-beginning 0))))
274         (with-temp-buffer
275           (insert text)
276           (goto-char (point-min))
277           (while (looking-at "\\([[:space:]]*\\)[A-Za-z][-A-Za-z0-9]*:")
278             (delete-region (match-beginning 1) (match-end 1))
279             (forward-line)
280             )
281           (goto-char (point-min))
282           (cons (cons 'summary summary) (mail-header-extract-no-properties)))))))
283
284 (defun notmuch-show-add-tag (&rest toadd)
285   "Add a tag to the current message."
286   (interactive
287    (list (notmuch-select-tag-with-completion "Tag to add: ")))
288   (apply 'notmuch-call-notmuch-process
289          (append (cons "tag"
290                        (mapcar (lambda (s) (concat "+" s)) toadd))
291                  (cons (notmuch-show-get-message-id) nil)))
292   (notmuch-show-set-tags (sort (union toadd (notmuch-show-get-tags) :test 'string=) 'string<)))
293
294 (defun notmuch-show-remove-tag (&rest toremove)
295   "Remove a tag from the current message."
296   (interactive
297    (list (notmuch-select-tag-with-completion "Tag to remove: " (notmuch-show-get-message-id))))
298   (let ((tags (notmuch-show-get-tags)))
299     (if (intersection tags toremove :test 'string=)
300         (progn
301           (apply 'notmuch-call-notmuch-process
302                  (append (cons "tag"
303                                (mapcar (lambda (s) (concat "-" s)) toremove))
304                          (cons (notmuch-show-get-message-id) nil)))
305           (notmuch-show-set-tags (sort (set-difference tags toremove :test 'string=) 'string<))))))
306
307 (defun notmuch-show-archive-thread-maybe-mark-read (markread)
308   (save-excursion
309     (goto-char (point-min))
310     (while (not (eobp))
311       (if markread
312           (notmuch-show-remove-tag "unread" "inbox")
313         (notmuch-show-remove-tag "inbox"))
314       (if (not (eobp))
315           (forward-char))
316       (if (not (re-search-forward notmuch-show-message-begin-regexp nil t))
317           (goto-char (point-max)))))
318   (let ((parent-buffer notmuch-show-parent-buffer))
319     (kill-this-buffer)
320     (if parent-buffer
321         (progn
322           (switch-to-buffer parent-buffer)
323           (forward-line)
324           (notmuch-search-show-thread)))))
325
326 (defun notmuch-show-mark-read-then-archive-thread ()
327   "Remove unread tags from thread, then archive and show next thread.
328
329 Archive each message currently shown by removing the \"unread\"
330 and \"inbox\" tag from each. Then kill this buffer and show the
331 next thread from the search from which this thread was originally
332 shown.
333
334 Note: This command is safe from any race condition of new messages
335 being delivered to the same thread. It does not archive the
336 entire thread, but only the messages shown in the current
337 buffer."
338   (interactive)
339   (notmuch-show-archive-thread-maybe-mark-read t))
340
341 (defun notmuch-show-archive-thread ()
342   "Archive each message in thread, then show next thread from search.
343
344 Archive each message currently shown by removing the \"inbox\"
345 tag from each. Then kill this buffer and show the next thread
346 from the search from which this thread was originally shown.
347
348 Note: This command is safe from any race condition of new messages
349 being delivered to the same thread. It does not archive the
350 entire thread, but only the messages shown in the current
351 buffer."
352   (interactive)
353   (notmuch-show-archive-thread-maybe-mark-read nil))
354
355 (defun notmuch-show-archive-thread-then-exit ()
356   "Archive each message in thread, then exit back to search results."
357   (interactive)
358   (notmuch-show-archive-thread)
359   (kill-this-buffer))
360
361 (defun notmuch-show-mark-read-then-archive-then-exit ()
362   "Remove unread tags from thread, then archive and exit to search results."
363   (interactive)
364   (notmuch-show-mark-read-then-archive-thread)
365   (kill-this-buffer))
366
367 (defun notmuch-show-view-raw-message ()
368   "View the raw email of the current message."
369   (interactive)
370   (view-file (notmuch-show-get-filename)))
371
372 (defmacro with-current-notmuch-show-message (&rest body)
373   "Evaluate body with current buffer set to the text of current message"
374   `(save-excursion
375      (let ((filename (notmuch-show-get-filename)))
376        (let ((buf (generate-new-buffer (concat "*notmuch-msg-" filename "*"))))
377          (with-current-buffer buf
378            (insert-file-contents filename nil nil nil t)
379            ,@body)
380          (kill-buffer buf)))))
381
382 (defun notmuch-show-view-all-mime-parts ()
383   "Use external viewers to view all attachments from the current message."
384   (interactive)
385   (with-current-notmuch-show-message
386    ; We ovverride the mm-inline-media-tests to indicate which message
387    ; parts are already sufficiently handled by the original
388    ; presentation of the message in notmuch-show mode. These parts
389    ; will be inserted directly into the temporary buffer of
390    ; with-current-notmuch-show-message and silently discarded.
391    ;
392    ; Any MIME part not explicitly mentioned here will be handled by an
393    ; external viewer as configured in the various mailcap files.
394    (let ((mm-inline-media-tests '(
395                                   ("text/.*" ignore identity)
396                                   ("application/pgp-signature" ignore identity)
397                                   ("multipart/alternative" ignore identity)
398                                   ("multipart/mixed" ignore identity)
399                                   ("multipart/related" ignore identity)
400                                  )))
401      (mm-display-parts (mm-dissect-buffer)))))
402
403 (defun notmuch-foreach-mime-part (function mm-handle)
404   (cond ((stringp (car mm-handle))
405          (dolist (part (cdr mm-handle))
406            (notmuch-foreach-mime-part function part)))
407         ((bufferp (car mm-handle))
408          (funcall function mm-handle))
409         (t (dolist (part mm-handle)
410              (notmuch-foreach-mime-part function part)))))
411
412 (defun notmuch-count-attachments (mm-handle)
413   (let ((count 0))
414     (notmuch-foreach-mime-part
415      (lambda (p)
416        (let ((disposition (mm-handle-disposition p)))
417          (and (listp disposition)
418               (or (equal (car disposition) "attachment")
419                   (and (equal (car disposition) "inline")
420                        (assq 'filename disposition)))
421               (incf count))))
422      mm-handle)
423     count))
424
425 (defun notmuch-save-attachments (mm-handle &optional queryp)
426   (notmuch-foreach-mime-part
427    (lambda (p)
428      (let ((disposition (mm-handle-disposition p)))
429        (and (listp disposition)
430             (or (equal (car disposition) "attachment")
431                 (and (equal (car disposition) "inline")
432                      (assq 'filename disposition)))
433             (or (not queryp)
434                 (y-or-n-p
435                  (concat "Save '" (cdr (assq 'filename disposition)) "' ")))
436             (mm-save-part p))))
437    mm-handle))
438
439 (defun notmuch-show-save-attachments ()
440   "Save all attachments from the current message."
441   (interactive)
442   (with-current-notmuch-show-message
443    (let ((mm-handle (mm-dissect-buffer)))
444      (notmuch-save-attachments
445       mm-handle (> (notmuch-count-attachments mm-handle) 1))))
446   (message "Done"))
447
448 (defun notmuch-reply (query-string)
449   (switch-to-buffer (generate-new-buffer "notmuch-draft"))
450   (call-process notmuch-command nil t nil "reply" query-string)
451   (message-insert-signature)
452   (goto-char (point-min))
453   (if (re-search-forward "^$" nil t)
454       (progn
455         (insert "--text follows this line--")
456         (forward-line)))
457   (message-mode))
458
459 (defun notmuch-show-reply ()
460   "Begin composing a reply to the current message in a new buffer."
461   (interactive)
462   (let ((message-id (notmuch-show-get-message-id)))
463     (notmuch-reply message-id)))
464
465 (defun notmuch-show-forward-current ()
466   "Forward the current message."
467   (interactive)
468   (with-current-notmuch-show-message
469    (message-forward)))
470
471 (defun notmuch-show-pipe-message (command)
472   "Pipe the contents of the current message to the given command.
473
474 The given command will be executed with the raw contents of the
475 current email message as stdin. Anything printed by the command
476 to stdout or stderr will appear in the *Messages* buffer."
477   (interactive "sPipe message to command: ")
478   (apply 'start-process-shell-command "notmuch-pipe-command" "*notmuch-pipe*"
479          (list command " < " (shell-quote-argument (notmuch-show-get-filename)))))
480
481 (defun notmuch-show-move-to-current-message-summary-line ()
482   "Move to the beginning of the one-line summary of the current message.
483
484 This gives us a stable place to move to and work from since the
485 summary line is always visible. This is important since moving to
486 an invisible location is unreliable, (the main command loop moves
487 point either forward or backward to the next visible character
488 when a command ends with point on an invisible character).
489
490 Emits an error if point is not within a valid message, (that is
491 no pattern of `notmuch-show-message-begin-regexp' could be found
492 by searching backward)."
493   (beginning-of-line)
494   (if (not (looking-at notmuch-show-message-begin-regexp))
495       (if (re-search-backward notmuch-show-message-begin-regexp nil t)
496           (forward-line 2)
497         (error "Not within a valid message."))
498     (forward-line 2)))
499
500 (defun notmuch-show-last-message-p ()
501   "Predicate testing whether point is within the last message."
502   (save-window-excursion
503     (save-excursion
504       (notmuch-show-move-to-current-message-summary-line)
505       (not (re-search-forward notmuch-show-message-begin-regexp nil t)))))
506
507 (defun notmuch-show-message-unread-p ()
508   "Predicate testing whether current message is unread."
509   (member "unread" (notmuch-show-get-tags)))
510
511 (defun notmuch-show-message-open-p ()
512   "Predicate testing whether current message is open (body is visible)."
513   (let ((btn (previous-button (point) t)))
514     (while (not (button-has-type-p btn 'notmuch-button-body-toggle-type))
515       (setq btn (previous-button (button-start btn))))
516     (not (invisible-p (button-get btn 'invisibility-spec)))))
517
518 (defun notmuch-show-next-message ()
519   "Advance to the beginning of the next message in the buffer.
520
521 Moves to the last visible character of the current message if
522 already on the last message in the buffer.
523
524 Returns nil if already on the last message in the buffer."
525   (interactive)
526   (notmuch-show-move-to-current-message-summary-line)
527   (if (re-search-forward notmuch-show-message-begin-regexp nil t)
528       (progn
529         (notmuch-show-move-to-current-message-summary-line)
530         (recenter 0)
531         t)
532     (goto-char (- (point-max) 1))
533     (while (point-invisible-p)
534       (backward-char))
535     (recenter 0)
536     nil))
537
538 (defun notmuch-show-find-next-message ()
539   "Returns the position of the next message in the buffer.
540
541 Or the position of the last visible character of the current
542 message if already within the last message in the buffer."
543   ; save-excursion doesn't save our window position
544   ; save-window-excursion doesn't save point
545   ; Looks like we have to use both.
546   (save-excursion
547     (save-window-excursion
548       (notmuch-show-next-message)
549       (point))))
550
551 (defun notmuch-show-next-unread-message ()
552   "Advance to the beginning of the next unread message in the buffer.
553
554 Moves to the last visible character of the current message if
555 there are no more unread messages past the current point."
556   (notmuch-show-next-message)
557   (while (and (not (notmuch-show-last-message-p))
558               (not (notmuch-show-message-unread-p)))
559     (notmuch-show-next-message))
560   (if (not (notmuch-show-message-unread-p))
561       (notmuch-show-next-message)))
562
563 (defun notmuch-show-next-open-message ()
564   "Advance to the next open message (that is, body is not invisible)."
565   (while (and (notmuch-show-next-message)
566               (not (notmuch-show-message-open-p)))))
567
568 (defun notmuch-show-previous-message ()
569   "Backup to the beginning of the previous message in the buffer.
570
571 If within a message rather than at the beginning of it, then
572 simply move to the beginning of the current message."
573   (interactive)
574   (let ((start (point)))
575     (notmuch-show-move-to-current-message-summary-line)
576     (if (not (< (point) start))
577         ; Go backward twice to skip the current message's marker
578         (progn
579           (re-search-backward notmuch-show-message-begin-regexp nil t)
580           (re-search-backward notmuch-show-message-begin-regexp nil t)
581           (notmuch-show-move-to-current-message-summary-line)
582           ))
583     (recenter 0)))
584
585 (defun notmuch-show-find-previous-message ()
586   "Returns the position of the previous message in the buffer.
587
588 Or the position of the beginning of the current message if point
589 is originally within the message rather than at the beginning of
590 it."
591   ; save-excursion doesn't save our window position
592   ; save-window-excursion doesn't save point
593   ; Looks like we have to use both.
594   (save-excursion
595     (save-window-excursion
596       (notmuch-show-previous-message)
597       (point))))
598
599 (defun notmuch-show-mark-read-then-next-open-message ()
600   "Remove unread tag from this message, then advance to next open message."
601   (interactive)
602   (notmuch-show-remove-tag "unread")
603   (notmuch-show-next-open-message))
604
605 (defun notmuch-show-rewind ()
606   "Backup through the thread, (reverse scrolling compared to \\[notmuch-show-advance-marking-read-and-archiving]).
607
608 Specifically, if the beginning of the previous email is fewer
609 than `window-height' lines from the current point, move to it
610 just like `notmuch-show-previous-message'.
611
612 Otherwise, just scroll down a screenful of the current message.
613
614 This command does not modify any message tags, (it does not undo
615 any effects from previous calls to
616 `notmuch-show-advance-marking-read-and-archiving'."
617   (interactive)
618   (let ((previous (notmuch-show-find-previous-message)))
619     (if (> (count-lines previous (point)) (- (window-height) next-screen-context-lines))
620         (progn
621           (condition-case nil
622               (scroll-down nil)
623             ((beginning-of-buffer) nil))
624           (goto-char (window-start)))
625       (notmuch-show-previous-message))))
626
627 (defun notmuch-show-advance-marking-read-and-archiving ()
628   "Advance through thread, marking read and archiving.
629
630 This command is intended to be one of the simplest ways to
631 process a thread of email. It does the following:
632
633 If the current message in the thread is not yet fully visible,
634 scroll by a near screenful to read more of the message.
635
636 Otherwise, (the end of the current message is already within the
637 current window), remove the \"unread\" tag (if present) from the
638 current message and advance to the next open message.
639
640 Finally, if there is no further message to advance to, and this
641 last message is already read, then archive the entire current
642 thread, (remove the \"inbox\" tag from each message). Also kill
643 this buffer, and display the next thread from the search from
644 which this thread was originally shown."
645   (interactive)
646   (let ((next (notmuch-show-find-next-message))
647         (unread (notmuch-show-message-unread-p)))
648     (if (> next (window-end))
649         (scroll-up nil)
650       (let ((last (notmuch-show-last-message-p)))
651         (notmuch-show-mark-read-then-next-open-message)
652         (if last
653             (notmuch-show-archive-thread))))))
654
655 (defun notmuch-show-next-button ()
656   "Advance point to the next button in the buffer."
657   (interactive)
658   (forward-button 1))
659
660 (defun notmuch-show-previous-button ()
661   "Move point back to the previous button in the buffer."
662   (interactive)
663   (backward-button 1))
664
665 (defun notmuch-toggle-invisible-action (cite-button)
666   (let ((invis-spec (button-get cite-button 'invisibility-spec)))
667         (if (invisible-p invis-spec)
668             (remove-from-invisibility-spec invis-spec)
669           (add-to-invisibility-spec invis-spec)
670           ))
671   (force-window-update)
672   (redisplay t))
673
674 (defun notmuch-show-toggle-current-body ()
675   "Toggle the display of the current message body."
676   (interactive)
677   (save-excursion
678     (notmuch-show-move-to-current-message-summary-line)
679     (unless (button-at (point))
680       (notmuch-show-next-button))
681     (push-button))
682   )
683
684 (defun notmuch-show-toggle-current-header ()
685   "Toggle the display of the current message header."
686   (interactive)
687   (save-excursion
688     (notmuch-show-move-to-current-message-summary-line)
689     (forward-line)
690     (unless (button-at (point))
691       (notmuch-show-next-button))
692     (push-button))
693   )
694
695 (define-button-type 'notmuch-button-invisibility-toggle-type
696   'action 'notmuch-toggle-invisible-action
697   'follow-link t
698   'face 'font-lock-comment-face)
699 (define-button-type 'notmuch-button-citation-toggle-type 'help-echo "mouse-1, RET: Show citation"
700   :supertype 'notmuch-button-invisibility-toggle-type)
701 (define-button-type 'notmuch-button-signature-toggle-type 'help-echo "mouse-1, RET: Show signature"
702   :supertype 'notmuch-button-invisibility-toggle-type)
703 (define-button-type 'notmuch-button-headers-toggle-type 'help-echo "mouse-1, RET: Show headers"
704   :supertype 'notmuch-button-invisibility-toggle-type)
705 (define-button-type 'notmuch-button-body-toggle-type
706   'help-echo "mouse-1, RET: Show message"
707   'face 'notmuch-message-summary-face
708   :supertype 'notmuch-button-invisibility-toggle-type)
709
710 (defun notmuch-show-citation-regexp (depth)
711   "Build a regexp for matching citations at a given DEPTH (indent)"
712   (let ((line-regexp (format "[[:space:]]\\{%d\\}>.*\n" depth)))
713     (concat "\\(?:^" line-regexp
714             "\\(?:[[:space:]]*\n" line-regexp
715             "\\)?\\)+")))
716
717 (defun notmuch-show-region-to-button (beg end type prefix button-text)
718   "Auxilary function to do the actual making of overlays and buttons
719
720 BEG and END are buffer locations. TYPE should a string, either
721 \"citation\" or \"signature\". PREFIX is some arbitrary text to
722 insert before the button, probably for indentation.  BUTTON-TEXT
723 is what to put on the button."
724
725 ;; This uses some slightly tricky conversions between strings and
726 ;; symbols because of the way the button code works. Note that
727 ;; replacing intern-soft with make-symbol will cause this to fail,
728 ;; since the newly created symbol has no plist.
729
730   (let ((overlay (make-overlay beg end))
731         (invis-spec (make-symbol (concat "notmuch-" type "-region")))
732         (button-type (intern-soft (concat "notmuch-button-"
733                                           type "-toggle-type"))))
734     (add-to-invisibility-spec invis-spec)
735     (overlay-put overlay 'invisible invis-spec)
736     (goto-char (1+ end))
737     (save-excursion
738       (goto-char (1- beg))
739       (insert prefix)
740       (insert-button button-text
741                      'invisibility-spec invis-spec
742                      :type button-type)
743       )))
744
745
746 (defun notmuch-show-markup-citations-region (beg end depth)
747   "Markup citations, and up to one signature in the given region"
748   ;; it would be nice if the untabify was not required, but
749   ;; that would require notmuch to indent with spaces.
750   (untabify beg end)
751   (let ((citation-regexp (notmuch-show-citation-regexp depth))
752         (signature-regexp (concat (format "^[[:space:]]\\{%d\\}" depth)
753                                   notmuch-show-signature-regexp))
754         (indent (concat "\n" (make-string depth ? ))))
755     (goto-char beg)
756     (beginning-of-line)
757     (while (and (< (point) end)
758                 (re-search-forward citation-regexp end t))
759       (let* ((cite-start (match-beginning 0))
760              (cite-end  (match-end 0))
761              (cite-lines (count-lines cite-start cite-end)))
762         (when (> cite-lines (1+ notmuch-show-citation-lines-prefix))
763           (goto-char cite-start)
764           (forward-line notmuch-show-citation-lines-prefix)
765           (notmuch-show-region-to-button
766            (point) cite-end
767            "citation"
768            indent
769            (format notmuch-show-citation-button-format
770                    (- cite-lines notmuch-show-citation-lines-prefix))
771            ))))
772     (if (and (< (point) end)
773              (re-search-forward signature-regexp end t))
774         (let* ((sig-start (match-beginning 0))
775                (sig-end (match-end 0))
776                (sig-lines (1- (count-lines sig-start end))))
777           (if (<= sig-lines notmuch-show-signature-lines-max)
778               (notmuch-show-region-to-button
779                sig-start
780                end
781                "signature"
782                indent
783                (format notmuch-show-signature-button-format sig-lines)
784                ))))))
785
786 (defun notmuch-show-markup-part (beg end depth)
787   (if (re-search-forward notmuch-show-part-begin-regexp nil t)
788       (progn
789         (let (mime-message mime-type)
790           (save-excursion
791             (re-search-forward notmuch-show-contentype-regexp end t)
792             (setq mime-type (car (split-string (buffer-substring
793                                                 (match-beginning 1) (match-end 1))))))
794
795           (if (equal mime-type "text/html")
796               (let ((filename (notmuch-show-get-filename)))
797                 (with-temp-buffer
798                   (insert-file-contents filename nil nil nil t)
799                   (setq mime-message (mm-dissect-buffer)))))
800           (forward-line)
801           (let ((beg (point-marker)))
802             (re-search-forward notmuch-show-part-end-regexp)
803             (let ((end (copy-marker (match-beginning 0))))
804               (goto-char end)
805               (if (not (bolp))
806                   (insert "\n"))
807               (indent-rigidly beg end depth)
808               (if (not (eq mime-message nil))
809                   (save-excursion
810                     (goto-char beg)
811                     (forward-line -1)
812                     (let ((handle-type (mm-handle-type mime-message))
813                           mime-type)
814                       (if (sequencep (car handle-type))
815                           (setq mime-type (car handle-type))
816                         (setq mime-type (car (car (cdr handle-type))))
817                         )
818                       (if (equal mime-type "text/html")
819                           (mm-display-part mime-message))))
820                 )
821               (notmuch-show-markup-citations-region beg end depth)
822               ; Advance to the next part (if any) (so the outer loop can
823               ; determine whether we've left the current message.
824               (if (re-search-forward notmuch-show-part-begin-regexp nil t)
825                   (beginning-of-line)))))
826         (goto-char end))
827     (goto-char end)))
828
829 (defun notmuch-show-markup-parts-region (beg end depth)
830   (save-excursion
831     (goto-char beg)
832     (while (< (point) end)
833       (notmuch-show-markup-part beg end depth))))
834
835 (defun notmuch-show-markup-body (depth match btn)
836   "Markup a message body, (indenting, buttonizing citations,
837 etc.), and conditionally hiding the body itself if the message
838 has been read and does not match the current search.
839
840 DEPTH specifies the depth at which this message appears in the
841 tree of the current thread, (the top-level messages have depth 0
842 and each reply increases depth by 1). MATCH indicates whether
843 this message is regarded as matching the current search. BTN is
844 the button which is used to toggle the visibility of this
845 message.
846
847 When this function is called, point must be within the message, but
848 before the delimiter marking the beginning of the body."
849   (re-search-forward notmuch-show-body-begin-regexp)
850   (forward-line)
851   (let ((beg (point-marker)))
852     (re-search-forward notmuch-show-body-end-regexp)
853     (let ((end (copy-marker (match-beginning 0))))
854       (notmuch-show-markup-parts-region beg end depth)
855       (let ((invis-spec (make-symbol "notmuch-show-body-read")))
856         (overlay-put (make-overlay beg end)
857                      'invisible invis-spec)
858         (button-put btn 'invisibility-spec invis-spec)
859         (if (not (or (notmuch-show-message-unread-p) match))
860             (add-to-invisibility-spec invis-spec)))
861       (set-marker beg nil)
862       (set-marker end nil)
863       )))
864
865 (defun notmuch-fontify-headers ()
866   (while (looking-at "[[:space:]]")
867     (forward-char))
868   (if (looking-at "[Tt]o:")
869       (progn
870         (overlay-put (make-overlay (point) (re-search-forward ":"))
871                      'face 'message-header-name)
872         (overlay-put (make-overlay (point) (re-search-forward ".*$"))
873                      'face 'message-header-to))
874     (if (looking-at "[B]?[Cc][Cc]:")
875         (progn
876           (overlay-put (make-overlay (point) (re-search-forward ":"))
877                        'face 'message-header-name)
878           (overlay-put (make-overlay (point) (re-search-forward ".*$"))
879                        'face 'message-header-cc))
880       (if (looking-at "[Ss]ubject:")
881           (progn
882             (overlay-put (make-overlay (point) (re-search-forward ":"))
883                          'face 'message-header-name)
884             (overlay-put (make-overlay (point) (re-search-forward ".*$"))
885                          'face 'message-header-subject))
886         (if (looking-at "[Ff]rom:")
887             (progn
888               (overlay-put (make-overlay (point) (re-search-forward ":"))
889                            'face 'message-header-name)
890               (overlay-put (make-overlay (point) (re-search-forward ".*$"))
891                            'face 'message-header-other)))))))
892
893 (defun notmuch-show-markup-header (message-begin depth)
894   "Buttonize and decorate faces in a message header.
895
896 MESSAGE-BEGIN is the position of the absolute first character in
897 the message (including all delimiters that will end up being
898 invisible etc.). This is to allow a button to reliably extend to
899 the beginning of the message even if point is positioned at an
900 invisible character (such as the beginning of the buffer).
901
902 DEPTH specifies the depth at which this message appears in the
903 tree of the current thread, (the top-level messages have depth 0
904 and each reply increases depth by 1)."
905   (re-search-forward notmuch-show-header-begin-regexp)
906   (forward-line)
907   (let ((beg (point-marker))
908         (summary-end (copy-marker (line-beginning-position 2)))
909         (subject-end (copy-marker (line-end-position 2)))
910         (invis-spec (make-symbol "notmuch-show-header"))
911         (btn nil))
912     (re-search-forward notmuch-show-header-end-regexp)
913     (beginning-of-line)
914     (let ((end (point-marker)))
915       (indent-rigidly beg end depth)
916       (goto-char beg)
917       (setq btn (make-button message-begin summary-end :type 'notmuch-button-body-toggle-type))
918       (forward-line)
919       (add-to-invisibility-spec invis-spec)
920       (overlay-put (make-overlay subject-end end)
921                    'invisible invis-spec)
922       (make-button (line-beginning-position) subject-end
923                    'invisibility-spec invis-spec
924                    :type 'notmuch-button-headers-toggle-type)
925       (while (looking-at "[[:space:]]*[A-Za-z][-A-Za-z0-9]*:")
926         (beginning-of-line)
927         (notmuch-fontify-headers)
928         (forward-line)
929         )
930       (goto-char end)
931       (insert "\n")
932       (set-marker beg nil)
933       (set-marker summary-end nil)
934       (set-marker subject-end nil)
935       (set-marker end nil)
936       )
937   btn))
938
939 (defun notmuch-show-markup-message ()
940   (if (re-search-forward notmuch-show-message-begin-regexp nil t)
941       (let ((message-begin (match-beginning 0)))
942         (re-search-forward notmuch-show-depth-match-regexp)
943         (let ((depth (string-to-number (buffer-substring (match-beginning 1) (match-end 1))))
944               (match (string= "1" (buffer-substring (match-beginning 2) (match-end 2))))
945               (btn nil))
946           (setq btn (notmuch-show-markup-header message-begin depth))
947           (notmuch-show-markup-body depth match btn)))
948     (goto-char (point-max))))
949
950 (defun notmuch-show-hide-markers ()
951   (save-excursion
952     (goto-char (point-min))
953     (while (not (eobp))
954       (if (re-search-forward notmuch-show-marker-regexp nil t)
955           (progn
956             (overlay-put (make-overlay (match-beginning 0) (+ (match-end 0) 1))
957                          'invisible 'notmuch-show-marker))
958         (goto-char (point-max))))))
959
960 (defun notmuch-show-markup-messages ()
961   (save-excursion
962     (goto-char (point-min))
963     (while (not (eobp))
964       (notmuch-show-markup-message)))
965   (notmuch-show-hide-markers))
966
967 (defun notmuch-documentation-first-line (symbol)
968   "Return the first line of the documentation string for SYMBOL."
969   (let ((doc (documentation symbol)))
970     (if doc
971         (with-temp-buffer
972           (insert (documentation symbol t))
973           (goto-char (point-min))
974           (let ((beg (point)))
975             (end-of-line)
976             (buffer-substring beg (point))))
977       "")))
978
979 (defun notmuch-prefix-key-description (key)
980   "Given a prefix key code, return a human-readable string representation.
981
982 This is basically just `format-kbd-macro' but we also convert ESC to M-."
983   (let ((desc (format-kbd-macro (vector key))))
984     (if (string= desc "ESC")
985         "M-"
986       (concat desc " "))))
987
988 ; I would think that emacs would have code handy for walking a keymap
989 ; and generating strings for each key, and I would prefer to just call
990 ; that. But I couldn't find any (could be all implemented in C I
991 ; suppose), so I wrote my own here.
992 (defun notmuch-substitute-one-command-key-with-prefix (prefix binding)
993   "For a key binding, return a string showing a human-readable
994 representation of the prefixed key as well as the first line of
995 documentation from the bound function.
996
997 For a mouse binding, return nil."
998   (let ((key (car binding))
999         (action (cdr binding)))
1000     (if (mouse-event-p key)
1001         nil
1002       (if (keymapp action)
1003           (let ((substitute (apply-partially 'notmuch-substitute-one-command-key-with-prefix (notmuch-prefix-key-description key)))
1004                 (as-list))
1005             (map-keymap (lambda (a b)
1006                           (push (cons a b) as-list))
1007                         action)
1008             (mapconcat substitute as-list "\n"))
1009         (concat prefix (format-kbd-macro (vector key))
1010                 "\t"
1011                 (notmuch-documentation-first-line action))))))
1012
1013 (defalias 'notmuch-substitute-one-command-key
1014   (apply-partially 'notmuch-substitute-one-command-key-with-prefix nil))
1015
1016 (defun notmuch-substitute-command-keys (doc)
1017   "Like `substitute-command-keys' but with documentation, not function names."
1018   (let ((beg 0))
1019     (while (string-match "\\\\{\\([^}[:space:]]*\\)}" doc beg)
1020       (let ((map (substring doc (match-beginning 1) (match-end 1))))
1021         (setq doc (replace-match (mapconcat 'notmuch-substitute-one-command-key
1022                                             (cdr (symbol-value (intern map))) "\n") 1 1 doc)))
1023       (setq beg (match-end 0)))
1024     doc))
1025
1026 (defun notmuch-help ()
1027   "Display help for the current notmuch mode."
1028   (interactive)
1029   (let* ((mode major-mode)
1030          (doc (substitute-command-keys (notmuch-substitute-command-keys (documentation mode t)))))
1031     (with-current-buffer (generate-new-buffer "*notmuch-help*")
1032       (insert doc)
1033       (goto-char (point-min))
1034       (set-buffer-modified-p nil)
1035       (view-buffer (current-buffer) 'kill-buffer-if-not-modified))))
1036
1037 ;;;###autoload
1038 (defun notmuch-show-mode ()
1039   "Major mode for viewing a thread with notmuch.
1040
1041 This buffer contains the results of the \"notmuch show\" command
1042 for displaying a single thread of email from your email archives.
1043
1044 By default, various components of email messages, (citations,
1045 signatures, already-read messages), are hidden. You can make
1046 these parts visible by clicking with the mouse button or by
1047 pressing RET after positioning the cursor on a hidden part, (for
1048 which \\[notmuch-show-next-button] and \\[notmuch-show-previous-button] are helpful).
1049
1050 Reading the thread sequentially is well-supported by pressing
1051 \\[notmuch-show-advance-marking-read-and-archiving]. This will scroll the current message (if necessary),
1052 advance to the next message, or advance to the next thread (if
1053 already on the last message of a thread). As each message is
1054 scrolled away its \"unread\" tag will be removed, and as each
1055 thread is scrolled away the \"inbox\" tag will be removed from
1056 each message in the thread.
1057
1058 Other commands are available to read or manipulate the thread more
1059 selectively, (such as '\\[notmuch-show-next-message]' and '\\[notmuch-show-previous-message]' to advance to messages without
1060 removing any tags, and '\\[notmuch-show-archive-thread]' to archive an entire thread without
1061 scrolling through with \\[notmuch-show-advance-marking-read-and-archiving]).
1062
1063 You can add or remove arbitary tags from the current message with
1064 '\\[notmuch-show-add-tag]' or '\\[notmuch-show-remove-tag]'.
1065
1066 All currently available key bindings:
1067
1068 \\{notmuch-show-mode-map}"
1069   (interactive)
1070   (kill-all-local-variables)
1071   (add-to-invisibility-spec 'notmuch-show-marker)
1072   (use-local-map notmuch-show-mode-map)
1073   (setq major-mode 'notmuch-show-mode
1074         mode-name "notmuch-show")
1075   (setq buffer-read-only t))
1076
1077 (defgroup notmuch nil
1078   "Notmuch mail reader for Emacs."
1079   :group 'mail)
1080
1081 (defcustom notmuch-show-hook nil
1082   "List of functions to call when notmuch displays a message."
1083   :type 'hook
1084   :options '(goto-address)
1085   :group 'notmuch)
1086
1087 (defcustom notmuch-search-hook nil
1088   "List of functions to call when notmuch displays the search results."
1089   :type 'hook
1090   :options '(hl-line-mode)
1091   :group 'notmuch)
1092
1093 ; Make show mode a bit prettier, highlighting URLs and using word wrap
1094
1095 (defun notmuch-show-pretty-hook ()
1096   (goto-address-mode 1)
1097   (visual-line-mode))
1098
1099 (add-hook 'notmuch-show-hook 'notmuch-show-pretty-hook)
1100 (add-hook 'notmuch-search-hook
1101           (lambda()
1102             (hl-line-mode 1) ))
1103
1104 (defun notmuch-show (thread-id &optional parent-buffer query-context)
1105   "Run \"notmuch show\" with the given thread ID and display results.
1106
1107 The optional PARENT-BUFFER is the notmuch-search buffer from
1108 which this notmuch-show command was executed, (so that the next
1109 thread from that buffer can be show when done with this one).
1110
1111 The optional QUERY-CONTEXT is a notmuch search term. Only messages from the thread
1112 matching this search term are shown if non-nil. "
1113   (interactive "sNotmuch show: ")
1114   (let ((buffer (get-buffer-create (concat "*notmuch-show-" thread-id "*"))))
1115     (switch-to-buffer buffer)
1116     (notmuch-show-mode)
1117     (set (make-local-variable 'notmuch-show-parent-buffer) parent-buffer)
1118     (let ((proc (get-buffer-process (current-buffer)))
1119           (inhibit-read-only t))
1120       (if proc
1121           (error "notmuch search process already running for query `%s'" thread-id)
1122         )
1123       (erase-buffer)
1124       (goto-char (point-min))
1125       (save-excursion
1126         (let* ((basic-args (list notmuch-command nil t nil "show" "--entire-thread" thread-id))
1127                 (args (if query-context (append basic-args (list "and (" query-context ")")) basic-args)))
1128           (apply 'call-process args)
1129           (when (and (eq (buffer-size) 0) query-context)
1130             (apply 'call-process basic-args)))
1131         (notmuch-show-markup-messages)
1132         )
1133       (run-hooks 'notmuch-show-hook)
1134       ; Move straight to the first open message
1135       (if (not (notmuch-show-message-open-p))
1136           (notmuch-show-next-open-message))
1137       )))
1138
1139 (defvar notmuch-search-authors-width 40
1140   "Number of columns to use to display authors in a notmuch-search buffer.")
1141
1142 (defvar notmuch-search-mode-map
1143   (let ((map (make-sparse-keymap)))
1144     (define-key map "?" 'notmuch-help)
1145     (define-key map "q" 'kill-this-buffer)
1146     (define-key map "x" 'kill-this-buffer)
1147     (define-key map (kbd "<DEL>") 'notmuch-search-scroll-down)
1148     (define-key map "b" 'notmuch-search-scroll-down)
1149     (define-key map " " 'notmuch-search-scroll-up)
1150     (define-key map "<" 'notmuch-search-first-thread)
1151     (define-key map ">" 'notmuch-search-last-thread)
1152     (define-key map "p" 'notmuch-search-previous-thread)
1153     (define-key map "n" 'notmuch-search-next-thread)
1154     (define-key map "r" 'notmuch-search-reply-to-thread)
1155     (define-key map "m" 'message-mail)
1156     (define-key map "s" 'notmuch-search)
1157     (define-key map "o" 'notmuch-search-toggle-order)
1158     (define-key map "=" 'notmuch-search-refresh-view)
1159     (define-key map "t" 'notmuch-search-filter-by-tag)
1160     (define-key map "f" 'notmuch-search-filter)
1161     (define-key map [mouse-1] 'notmuch-search-show-thread)
1162     (define-key map "*" 'notmuch-search-operate-all)
1163     (define-key map "a" 'notmuch-search-archive-thread)
1164     (define-key map "-" 'notmuch-search-remove-tag)
1165     (define-key map "+" 'notmuch-search-add-tag)
1166     (define-key map (kbd "RET") 'notmuch-search-show-thread)
1167     map)
1168   "Keymap for \"notmuch search\" buffers.")
1169 (fset 'notmuch-search-mode-map notmuch-search-mode-map)
1170
1171 (defvar notmuch-search-query-string)
1172 (defvar notmuch-search-oldest-first t
1173   "Show the oldest mail first in the search-mode")
1174
1175 (defvar notmuch-search-disjunctive-regexp      "\\<[oO][rR]\\>")
1176
1177 (defun notmuch-search-scroll-up ()
1178   "Move forward through search results by one window's worth."
1179   (interactive)
1180   (condition-case nil
1181       (scroll-up nil)
1182     ((end-of-buffer) (notmuch-search-last-thread))))
1183
1184 (defun notmuch-search-scroll-down ()
1185   "Move backward through the search results by one window's worth."
1186   (interactive)
1187   ; I don't know why scroll-down doesn't signal beginning-of-buffer
1188   ; the way that scroll-up signals end-of-buffer, but c'est la vie.
1189   ;
1190   ; So instead of trapping a signal we instead check whether the
1191   ; window begins on the first line of the buffer and if so, move
1192   ; directly to that position. (We have to count lines since the
1193   ; window-start position is not the same as point-min due to the
1194   ; invisible thread-ID characters on the first line.
1195   (if (equal (count-lines (point-min) (window-start)) 0)
1196       (goto-char (point-min))
1197     (scroll-down nil)))
1198
1199 (defun notmuch-search-next-thread ()
1200   "Select the next thread in the search results."
1201   (interactive)
1202   (forward-line 1))
1203
1204 (defun notmuch-search-previous-thread ()
1205   "Select the previous thread in the search results."
1206   (interactive)
1207   (forward-line -1))
1208
1209 (defun notmuch-search-last-thread ()
1210   "Select the last thread in the search results."
1211   (interactive)
1212   (goto-char (point-max))
1213   (forward-line -2))
1214
1215 (defun notmuch-search-first-thread ()
1216   "Select the first thread in the search results."
1217   (interactive)
1218   (goto-char (point-min)))
1219
1220 (defface notmuch-message-summary-face
1221  '((((class color) (background light)) (:background "#f0f0f0"))
1222    (((class color) (background dark)) (:background "#303030")))
1223  "Face for the single-line message summary in notmuch-show-mode."
1224  :group 'notmuch)
1225
1226 (defface notmuch-tag-face
1227   '((((class color)
1228       (background dark))
1229      (:foreground "OliveDrab1"))
1230     (((class color)
1231       (background light))
1232      (:foreground "navy blue" :bold t))
1233     (t
1234      (:bold t)))
1235   "Notmuch search mode face used to highligh tags."
1236   :group 'notmuch)
1237
1238 (defvar notmuch-tag-face-alist nil
1239   "List containing the tag list that need to be highlighed")
1240
1241 (defvar notmuch-search-font-lock-keywords  nil)
1242
1243 ;;;###autoload
1244 (defun notmuch-search-mode ()
1245   "Major mode displaying results of a notmuch search.
1246
1247 This buffer contains the results of a \"notmuch search\" of your
1248 email archives. Each line in the buffer represents a single
1249 thread giving a summary of the thread (a relative date, the
1250 number of matched messages and total messages in the thread,
1251 participants in the thread, a representative subject line, and
1252 any tags).
1253
1254 Pressing \\[notmuch-search-show-thread] on any line displays that thread. The '\\[notmuch-search-add-tag]' and '\\[notmuch-search-remove-tag]'
1255 keys can be used to add or remove tags from a thread. The '\\[notmuch-search-archive-thread]' key
1256 is a convenience for archiving a thread (removing the \"inbox\"
1257 tag). The '\\[notmuch-search-operate-all]' key can be used to add or remove a tag from all
1258 threads in the current buffer.
1259
1260 Other useful commands are '\\[notmuch-search-filter]' for filtering the current search
1261 based on an additional query string, '\\[notmuch-search-filter-by-tag]' for filtering to include
1262 only messages with a given tag, and '\\[notmuch-search]' to execute a new, global
1263 search.
1264
1265 Complete list of currently available key bindings:
1266
1267 \\{notmuch-search-mode-map}"
1268   (interactive)
1269   (kill-all-local-variables)
1270   (make-local-variable 'notmuch-search-query-string)
1271   (make-local-variable 'notmuch-search-oldest-first)
1272   (set (make-local-variable 'scroll-preserve-screen-position) t)
1273   (add-to-invisibility-spec 'notmuch-search)
1274   (use-local-map notmuch-search-mode-map)
1275   (setq truncate-lines t)
1276   (setq major-mode 'notmuch-search-mode
1277         mode-name "notmuch-search")
1278   (setq buffer-read-only t)
1279   (if (not notmuch-tag-face-alist)
1280       (add-to-list 'notmuch-search-font-lock-keywords (list
1281                 "(\\([^)]*\\))$" '(1  'notmuch-tag-face)))
1282     (let ((notmuch-search-tags (mapcar 'car notmuch-tag-face-alist)))
1283       (loop for notmuch-search-tag  in notmuch-search-tags
1284             do (add-to-list 'notmuch-search-font-lock-keywords (list
1285                         (concat "([^)]*\\(" notmuch-search-tag "\\)[^)]*)$")
1286                         `(1  ,(cdr (assoc notmuch-search-tag notmuch-tag-face-alist))))))))
1287   (set (make-local-variable 'font-lock-defaults)
1288          '(notmuch-search-font-lock-keywords t)))
1289
1290 (defun notmuch-search-find-thread-id ()
1291   "Return the thread for the current thread"
1292   (get-text-property (point) 'notmuch-search-thread-id))
1293
1294 (defun notmuch-search-find-authors ()
1295   "Return the authors for the current thread"
1296   (get-text-property (point) 'notmuch-search-authors))
1297
1298 (defun notmuch-search-find-subject ()
1299   "Return the subject for the current thread"
1300   (get-text-property (point) 'notmuch-search-subject))
1301
1302 (defun notmuch-search-show-thread ()
1303   "Display the currently selected thread."
1304   (interactive)
1305   (let ((thread-id (notmuch-search-find-thread-id)))
1306     (if (> (length thread-id) 0)
1307         (notmuch-show thread-id (current-buffer) notmuch-search-query-string)
1308       (error "End of search results"))))
1309
1310 (defun notmuch-search-reply-to-thread ()
1311   "Begin composing a reply to the entire current thread in a new buffer."
1312   (interactive)
1313   (let ((message-id (notmuch-search-find-thread-id)))
1314     (notmuch-reply message-id)))
1315
1316 (defun notmuch-call-notmuch-process (&rest args)
1317   "Synchronously invoke \"notmuch\" with the given list of arguments.
1318
1319 Output from the process will be presented to the user as an error
1320 and will also appear in a buffer named \"*Notmuch errors*\"."
1321   (let ((error-buffer (get-buffer-create "*Notmuch errors*")))
1322     (with-current-buffer error-buffer
1323         (erase-buffer))
1324     (if (eq (apply 'call-process notmuch-command nil error-buffer nil args) 0)
1325         (point)
1326       (progn
1327         (with-current-buffer error-buffer
1328           (let ((beg (point-min))
1329                 (end (- (point-max) 1)))
1330             (error (buffer-substring beg end))
1331             ))))))
1332
1333 (defun notmuch-search-set-tags (tags)
1334   (save-excursion
1335     (end-of-line)
1336     (re-search-backward "(")
1337     (forward-char)
1338     (let ((beg (point))
1339           (inhibit-read-only t))
1340       (re-search-forward ")")
1341       (backward-char)
1342       (let ((end (point)))
1343         (delete-region beg end)
1344         (insert (mapconcat  'identity tags " "))))))
1345
1346 (defun notmuch-search-get-tags ()
1347   (save-excursion
1348     (end-of-line)
1349     (re-search-backward "(")
1350     (let ((beg (+ (point) 1)))
1351       (re-search-forward ")")
1352       (let ((end (- (point) 1)))
1353         (split-string (buffer-substring beg end))))))
1354
1355 (defun notmuch-search-add-tag (tag)
1356   "Add a tag to the currently selected thread.
1357
1358 The tag is added to messages in the currently selected thread
1359 which match the current search terms."
1360   (interactive
1361    (list (notmuch-select-tag-with-completion "Tag to add: ")))
1362   (notmuch-call-notmuch-process "tag" (concat "+" tag) (notmuch-search-find-thread-id))
1363   (notmuch-search-set-tags (delete-dups (sort (cons tag (notmuch-search-get-tags)) 'string<))))
1364
1365 (defun notmuch-search-remove-tag (tag)
1366   "Remove a tag from the currently selected thread.
1367
1368 The tag is removed from messages in the currently selected thread
1369 which match the current search terms."
1370   (interactive
1371    (list (notmuch-select-tag-with-completion "Tag to remove: " (notmuch-search-find-thread-id))))
1372   (notmuch-call-notmuch-process "tag" (concat "-" tag) (notmuch-search-find-thread-id))
1373   (notmuch-search-set-tags (delete tag (notmuch-search-get-tags))))
1374
1375 (defun notmuch-search-archive-thread ()
1376   "Archive the currently selected thread (remove its \"inbox\" tag).
1377
1378 This function advances the next thread when finished."
1379   (interactive)
1380   (notmuch-search-remove-tag "inbox")
1381   (forward-line))
1382
1383 (defun notmuch-search-process-sentinel (proc msg)
1384   "Add a message to let user know when \"notmuch search\" exits"
1385   (let ((buffer (process-buffer proc))
1386         (status (process-status proc))
1387         (exit-status (process-exit-status proc)))
1388     (if (memq status '(exit signal))
1389         (if (buffer-live-p buffer)
1390             (with-current-buffer buffer
1391               (save-excursion
1392                 (let ((inhibit-read-only t))
1393                   (goto-char (point-max))
1394                   (if (eq status 'signal)
1395                       (insert "Incomplete search results (search process was killed).\n"))
1396                   (if (eq status 'exit)
1397                       (progn
1398                         (insert "End of search results.")
1399                         (if (not (= exit-status 0))
1400                             (insert (format " (process returned %d)" exit-status)))
1401                         (insert "\n"))))))))))
1402
1403 (defun notmuch-search-process-filter (proc string)
1404   "Process and filter the output of \"notmuch search\""
1405   (let ((buffer (process-buffer proc)))
1406     (if (buffer-live-p buffer)
1407         (with-current-buffer buffer
1408           (save-excursion
1409             (let ((line 0)
1410                   (more t)
1411                   (inhibit-read-only t))
1412               (while more
1413                 (if (string-match "^\\(thread:[0-9A-Fa-f]*\\) \\(.*\\) \\(\\[[0-9/]*\\]\\) \\([^;]*\\); \\(.*\\) (\\([^()]*\\))$" string line)
1414                     (let* ((thread-id (match-string 1 string))
1415                            (date (match-string 2 string))
1416                            (count (match-string 3 string))
1417                            (authors (match-string 4 string))
1418                            (authors-length (length authors))
1419                            (subject (match-string 5 string))
1420                            (tags (match-string 6 string)))
1421                       (if (> authors-length 40)
1422                           (set 'authors (concat (substring authors 0 (- 40 3)) "...")))
1423                       (goto-char (point-max))
1424                       (let ((beg (point-marker)))
1425                         (insert (format "%s %-7s %-40s %s (%s)\n" date count authors subject tags))
1426                         (put-text-property beg (point-marker) 'notmuch-search-thread-id thread-id)
1427                         (put-text-property beg (point-marker) 'notmuch-search-authors authors)
1428                         (put-text-property beg (point-marker) 'notmuch-search-subject subject))
1429                       (set 'line (match-end 0)))
1430                   (set 'more nil))))))
1431       (delete-process proc))))
1432
1433 (defun notmuch-search-operate-all (action)
1434   "Add/remove tags from all matching messages.
1435
1436 Tis command adds or removes tags from all messages matching the
1437 current search terms. When called interactively, this command
1438 will prompt for tags to be added or removed. Tags prefixed with
1439 '+' will be added and tags prefixed with '-' will be removed.
1440
1441 Each character of the tag name may consist of alphanumeric
1442 characters as well as `_.+-'.
1443 "
1444   (interactive "sOperation (+add -drop): notmuch tag ")
1445   (let ((action-split (split-string action " +")))
1446     ;; Perform some validation
1447     (let ((words action-split))
1448       (when (null words) (error "No operation given"))
1449       (while words
1450         (unless (string-match-p "^[-+][-+_.[:word:]]+$" (car words))
1451           (error "Action must be of the form `+thistag -that_tag'"))
1452         (setq words (cdr words))))
1453     (apply 'notmuch-call-notmuch-process "tag"
1454            (append action-split (list notmuch-search-query-string) nil))))
1455
1456 ;;;###autoload
1457 (defun notmuch-search (query &optional oldest-first)
1458   "Run \"notmuch search\" with the given query string and display results."
1459   (interactive "sNotmuch search: ")
1460   (let ((buffer (get-buffer-create (concat "*notmuch-search-" query "*"))))
1461     (switch-to-buffer buffer)
1462     (notmuch-search-mode)
1463     (set 'notmuch-search-query-string query)
1464     (set 'notmuch-search-oldest-first oldest-first)
1465     (let ((proc (get-buffer-process (current-buffer)))
1466           (inhibit-read-only t))
1467       (if proc
1468           (error "notmuch search process already running for query `%s'" query)
1469         )
1470       (erase-buffer)
1471       (goto-char (point-min))
1472       (save-excursion
1473         (let ((proc (start-process-shell-command
1474                      "notmuch-search" buffer notmuch-command "search"
1475                      (if oldest-first "--sort=oldest-first" "--sort=newest-first")
1476                      (shell-quote-argument query))))
1477           (set-process-sentinel proc 'notmuch-search-process-sentinel)
1478           (set-process-filter proc 'notmuch-search-process-filter))))
1479     (run-hooks 'notmuch-search-hook)))
1480
1481 (defun notmuch-search-refresh-view ()
1482   "Refresh the current view.
1483
1484 Kills the current buffer and runs a new search with the same
1485 query string as the current search. If the current thread is in
1486 the new search results, then point will be placed on the same
1487 thread. Otherwise, point will be moved to attempt to be in the
1488 same relative position within the new buffer."
1489   (interactive)
1490   (let ((here (point))
1491         (oldest-first notmuch-search-oldest-first)
1492         (thread (notmuch-search-find-thread-id))
1493         (query notmuch-search-query-string))
1494     (kill-this-buffer)
1495     (notmuch-search query oldest-first)
1496     (goto-char (point-min))
1497     (if (re-search-forward (concat "^" thread) nil t)
1498         (beginning-of-line)
1499       (goto-char here))))
1500
1501 (defun notmuch-search-toggle-order ()
1502   "Toggle the current search order.
1503
1504 By default, the \"inbox\" view created by `notmuch' is displayed
1505 in chronological order (oldest thread at the beginning of the
1506 buffer), while any global searches created by `notmuch-search'
1507 are displayed in reverse-chronological order (newest thread at
1508 the beginning of the buffer).
1509
1510 This command toggles the sort order for the current search.
1511
1512 Note that any filtered searches created by
1513 `notmuch-search-filter' retain the search order of the parent
1514 search."
1515   (interactive)
1516   (set 'notmuch-search-oldest-first (not notmuch-search-oldest-first))
1517   (notmuch-search-refresh-view))
1518
1519 (defun notmuch-search-filter (query)
1520   "Filter the current search results based on an additional query string.
1521
1522 Runs a new search matching only messages that match both the
1523 current search results AND the additional query string provided."
1524   (interactive "sFilter search: ")
1525   (let ((grouped-query (if (string-match-p notmuch-search-disjunctive-regexp query) (concat "( " query " )") query)))
1526     (notmuch-search (concat notmuch-search-query-string " and " grouped-query) notmuch-search-oldest-first)))
1527
1528 (defun notmuch-search-filter-by-tag (tag)
1529   "Filter the current search results based on a single tag.
1530
1531 Runs a new search matching only messages that match both the
1532 current search results AND that are tagged with the given tag."
1533   (interactive
1534    (list (notmuch-select-tag-with-completion "Filter by tag: ")))
1535   (notmuch-search (concat notmuch-search-query-string " and tag:" tag) notmuch-search-oldest-first))
1536
1537
1538 ;;;###autoload
1539 (defun notmuch ()
1540   "Run notmuch to display all mail with tag of 'inbox'"
1541   (interactive)
1542   (notmuch-search "tag:inbox" notmuch-search-oldest-first))
1543
1544 (setq mail-user-agent 'message-user-agent)
1545
1546 (defvar notmuch-folder-mode-map
1547   (let ((map (make-sparse-keymap)))
1548     (define-key map "?" 'notmuch-help)
1549     (define-key map "x" 'kill-this-buffer)
1550     (define-key map "q" 'kill-this-buffer)
1551     (define-key map "m" 'message-mail)
1552     (define-key map "e" 'notmuch-folder-show-empty-toggle)
1553     (define-key map ">" 'notmuch-folder-last)
1554     (define-key map "<" 'notmuch-folder-first)
1555     (define-key map "=" 'notmuch-folder)
1556     (define-key map "s" 'notmuch-search)
1557     (define-key map [mouse-1] 'notmuch-folder-show-search)
1558     (define-key map (kbd "RET") 'notmuch-folder-show-search)
1559     (define-key map " " 'notmuch-folder-show-search)
1560     (define-key map "p" 'notmuch-folder-previous)
1561     (define-key map "n" 'notmuch-folder-next)
1562     map)
1563   "Keymap for \"notmuch folder\" buffers.")
1564
1565 (fset 'notmuch-folder-mode-map notmuch-folder-mode-map)
1566
1567 (defcustom notmuch-folders (quote (("inbox" . "tag:inbox") ("unread" . "tag:unread")))
1568   "List of searches for the notmuch folder view"
1569   :type '(alist :key-type (string) :value-type (string))
1570   :group 'notmuch)
1571
1572 (defun notmuch-folder-mode ()
1573   "Major mode for showing notmuch 'folders'.
1574
1575 This buffer contains a list of message counts returned by a
1576 customizable set of searches of your email archives. Each line in
1577 the buffer shows the name of a saved search and the resulting
1578 message count.
1579
1580 Pressing RET on any line opens a search window containing the
1581 results for the saved search on that line.
1582
1583 Here is an example of how the search list could be
1584 customized, (the following text would be placed in your ~/.emacs
1585 file):
1586
1587 (setq notmuch-folders '((\"inbox\" . \"tag:inbox\")
1588                         (\"unread\" . \"tag:inbox AND tag:unread\")
1589                         (\"notmuch\" . \"tag:inbox AND to:notmuchmail.org\")))
1590
1591 Of course, you can have any number of folders, each configured
1592 with any supported search terms (see \"notmuch help search-terms\").
1593
1594 Currently available key bindings:
1595
1596 \\{notmuch-folder-mode-map}"
1597   (interactive)
1598   (kill-all-local-variables)
1599   (use-local-map 'notmuch-folder-mode-map)
1600   (setq truncate-lines t)
1601   (hl-line-mode 1)
1602   (setq major-mode 'notmuch-folder-mode
1603         mode-name "notmuch-folder")
1604   (setq buffer-read-only t))
1605
1606 (defun notmuch-folder-next ()
1607   "Select the next folder in the list."
1608   (interactive)
1609   (forward-line 1)
1610   (if (eobp)
1611       (forward-line -1)))
1612
1613 (defun notmuch-folder-previous ()
1614   "Select the previous folder in the list."
1615   (interactive)
1616   (forward-line -1))
1617
1618 (defun notmuch-folder-first ()
1619   "Select the first folder in the list."
1620   (interactive)
1621   (goto-char (point-min)))
1622
1623 (defun notmuch-folder-last ()
1624   "Select the last folder in the list."
1625   (interactive)
1626   (goto-char (point-max))
1627   (forward-line -1))
1628
1629 (defun notmuch-folder-count (search)
1630   (car (process-lines notmuch-command "count" search)))
1631
1632 (setq notmuch-folder-show-empty t)
1633
1634 (defun notmuch-folder-show-empty-toggle ()
1635   "Toggle the listing of empty folders"
1636   (interactive)
1637   (setq notmuch-folder-show-empty (not notmuch-folder-show-empty))
1638   (notmuch-folder))
1639
1640 (defun notmuch-folder-add (folders)
1641   (if folders
1642       (let* ((name (car (car folders)))
1643             (inhibit-read-only t)
1644             (search (cdr (car folders)))
1645             (count (notmuch-folder-count search)))
1646         (if (or notmuch-folder-show-empty
1647                 (not (equal count "0")))
1648             (progn
1649               (insert name)
1650               (indent-to 16 1)
1651               (insert count)
1652               (insert "\n")
1653               )
1654           )
1655         (notmuch-folder-add (cdr folders)))))
1656
1657 (defun notmuch-folder-find-name ()
1658   (save-excursion
1659     (beginning-of-line)
1660     (let ((beg (point)))
1661       (re-search-forward "\\([ \t]*[^ \t]+\\)")
1662       (filter-buffer-substring (match-beginning 1) (match-end 1)))))
1663
1664 (defun notmuch-folder-show-search (&optional folder)
1665   "Show a search window for the search related to the specified folder."
1666   (interactive)
1667   (if (null folder)
1668       (setq folder (notmuch-folder-find-name)))
1669   (let ((search (assoc folder notmuch-folders)))
1670     (if search
1671         (notmuch-search (cdr search) notmuch-search-oldest-first))))
1672
1673 ;;;###autoload
1674 (defun notmuch-folder ()
1675   "Show the notmuch folder view and update the displayed counts."
1676   (interactive)
1677   (let ((buffer (get-buffer-create "*notmuch-folders*")))
1678     (switch-to-buffer buffer)
1679     (let ((inhibit-read-only t)
1680           (n (line-number-at-pos)))
1681       (erase-buffer)
1682       (notmuch-folder-mode)
1683       (notmuch-folder-add notmuch-folders)
1684       (goto-char (point-min))
1685       (goto-line n))))
1686
1687 (provide 'notmuch)