]> git.notmuchmail.org Git - notmuch/blobdiff - notmuch.el
notmuch.el: Indent messages to show nested structure of thread.
[notmuch] / notmuch.el
index 0eac1ccbed60efa586cdea6eafa223b2373127ac..07193066a511d38e7bedb500c31c35168d052b8e 100644 (file)
@@ -19,7 +19,8 @@
 ;
 ; Authors: Carl Worth <cworth@cworth.org>
 
 ;
 ; Authors: Carl Worth <cworth@cworth.org>
 
-(load "cl-seq")
+(require 'cl)
+(require 'mm-view)
 
 (defvar notmuch-show-mode-map
   (let ((map (make-sparse-keymap)))
 
 (defvar notmuch-show-mode-map
   (let ((map (make-sparse-keymap)))
     (define-key map "b" 'notmuch-show-toggle-body-read-visible)
     (define-key map "c" 'notmuch-show-toggle-citations-visible)
     (define-key map "h" 'notmuch-show-toggle-headers-visible)
     (define-key map "b" 'notmuch-show-toggle-body-read-visible)
     (define-key map "c" 'notmuch-show-toggle-citations-visible)
     (define-key map "h" 'notmuch-show-toggle-headers-visible)
+    (define-key map "m" 'message-mail)
     (define-key map "n" 'notmuch-show-next-message)
     (define-key map "N" 'notmuch-show-mark-read-then-next-open-message)
     (define-key map "p" 'notmuch-show-previous-message)
     (define-key map (kbd "C-n") 'notmuch-show-next-line)
     (define-key map (kbd "C-p") 'notmuch-show-previous-line)
     (define-key map "q" 'kill-this-buffer)
     (define-key map "n" 'notmuch-show-next-message)
     (define-key map "N" 'notmuch-show-mark-read-then-next-open-message)
     (define-key map "p" 'notmuch-show-previous-message)
     (define-key map (kbd "C-n") 'notmuch-show-next-line)
     (define-key map (kbd "C-p") 'notmuch-show-previous-line)
     (define-key map "q" 'kill-this-buffer)
+    (define-key map "r" 'notmuch-show-reply)
     (define-key map "s" 'notmuch-show-toggle-signatures-visible)
     (define-key map "s" 'notmuch-show-toggle-signatures-visible)
+    (define-key map "v" 'notmuch-show-view-all-mime-parts)
     (define-key map "w" 'notmuch-show-view-raw-message)
     (define-key map "x" 'kill-this-buffer)
     (define-key map "+" 'notmuch-show-add-tag)
     (define-key map "w" 'notmuch-show-view-raw-message)
     (define-key map "x" 'kill-this-buffer)
     (define-key map "+" 'notmuch-show-add-tag)
@@ -53,7 +57,7 @@
   "Keymap for \"notmuch show\" buffers.")
 (fset 'notmuch-show-mode-map notmuch-show-mode-map)
 
   "Keymap for \"notmuch show\" buffers.")
 (fset 'notmuch-show-mode-map notmuch-show-mode-map)
 
-(defvar notmuch-show-signature-lines-max 6
+(defvar notmuch-show-signature-lines-max 12
   "Maximum length of signature that will be hidden by default.")
 
 (set 'notmuch-show-message-begin-regexp    "\fmessage{")
   "Maximum length of signature that will be hidden by default.")
 
 (set 'notmuch-show-message-begin-regexp    "\fmessage{")
@@ -68,7 +72,8 @@
 (set 'notmuch-show-part-end-regexp         "\fpart}")
 (set 'notmuch-show-marker-regexp "\f\\(message\\|header\\|body\\|attachment\\|part\\)[{}].*$")
 
 (set 'notmuch-show-part-end-regexp         "\fpart}")
 (set 'notmuch-show-marker-regexp "\f\\(message\\|header\\|body\\|attachment\\|part\\)[{}].*$")
 
-(set 'notmuch-show-id-regexp "id:\\([^ ]*\\)")
+(set 'notmuch-show-id-regexp "\\(id:[^ ]*\\)")
+(set 'notmuch-show-depth-regexp " depth:\\([0-9]*\\) ")
 (set 'notmuch-show-filename-regexp "filename:\\(.*\\)$")
 (set 'notmuch-show-tags-regexp "(\\([^)]*\\))$")
 
 (set 'notmuch-show-filename-regexp "filename:\\(.*\\)$")
 (set 'notmuch-show-tags-regexp "(\\([^)]*\\))$")
 
@@ -153,7 +158,7 @@ Unlike builtin `next-line' this version accepts no arguments."
   (apply 'notmuch-call-notmuch-process
         (append (cons "tag"
                       (mapcar (lambda (s) (concat "+" s)) toadd))
   (apply 'notmuch-call-notmuch-process
         (append (cons "tag"
                       (mapcar (lambda (s) (concat "+" s)) toadd))
-                (cons (concat "id:" (notmuch-show-get-message-id)) nil)))
+                (cons (notmuch-show-get-message-id) nil)))
   (notmuch-show-set-tags (sort (union toadd (notmuch-show-get-tags) :test 'string=) 'string<)))
 
 (defun notmuch-show-remove-tag (&rest toremove)
   (notmuch-show-set-tags (sort (union toadd (notmuch-show-get-tags) :test 'string=) 'string<)))
 
 (defun notmuch-show-remove-tag (&rest toremove)
@@ -165,7 +170,7 @@ Unlike builtin `next-line' this version accepts no arguments."
          (apply 'notmuch-call-notmuch-process
                 (append (cons "tag"
                               (mapcar (lambda (s) (concat "-" s)) toremove))
          (apply 'notmuch-call-notmuch-process
                 (append (cons "tag"
                               (mapcar (lambda (s) (concat "-" s)) toremove))
-                        (cons (concat "id:" (notmuch-show-get-message-id)) nil)))
+                        (cons (notmuch-show-get-message-id) nil)))
          (notmuch-show-set-tags (sort (set-difference tags toremove :test 'string=) 'string<))))))
 
 (defun notmuch-show-archive-thread-maybe-mark-read (markread)
          (notmuch-show-set-tags (sort (set-difference tags toremove :test 'string=) 'string<))))))
 
 (defun notmuch-show-archive-thread-maybe-mark-read (markread)
@@ -184,6 +189,7 @@ Unlike builtin `next-line' this version accepts no arguments."
     (if parent-buffer
        (progn
          (switch-to-buffer parent-buffer)
     (if parent-buffer
        (progn
          (switch-to-buffer parent-buffer)
+         (forward-line)
          (notmuch-search-show-thread)))))
 
 (defun notmuch-show-mark-read-then-archive-thread ()
          (notmuch-search-show-thread)))))
 
 (defun notmuch-show-mark-read-then-archive-thread ()
@@ -220,6 +226,31 @@ buffer."
   (interactive)
   (view-file (notmuch-show-get-filename)))
 
   (interactive)
   (view-file (notmuch-show-get-filename)))
 
+(defun notmuch-show-view-all-mime-parts ()
+  "Use external viewers (according to mailcap) to view all MIME-encoded parts."
+  (interactive)
+  (save-excursion
+    (let ((filename (notmuch-show-get-filename)))
+      (switch-to-buffer (generate-new-buffer (concat "*notmuch-mime-"
+                                                    filename
+                                                    "*")))
+      (insert-file-contents filename nil nil nil t)
+      (mm-display-parts (mm-dissect-buffer))
+      (kill-this-buffer))))
+
+(defun notmuch-show-reply ()
+  "Begin composing a reply to the current message in a new buffer."
+  (interactive)
+  (let ((message-id (notmuch-show-get-message-id)))
+    (switch-to-buffer (generate-new-buffer "notmuch-draft"))
+    (call-process "notmuch" nil t nil "reply" message-id)
+    (goto-char (point-min))
+    (if (re-search-forward "^$" nil t)
+       (progn
+         (insert "--text follows this line--")
+         (forward-line)))
+    (message-mode)))
+
 (defun notmuch-show-pipe-message (command)
   "Pipe the contents of the current message to the given command.
 
 (defun notmuch-show-pipe-message (command)
   "Pipe the contents of the current message to the given command.
 
@@ -395,59 +426,98 @@ which this thread was originally shown."
        (if last
            (notmuch-show-archive-thread))))))
 
        (if last
            (notmuch-show-archive-thread))))))
 
-(defun notmuch-show-markup-citations-region (beg end)
+(defun notmuch-show-markup-citations-region (beg end depth)
   (goto-char beg)
   (beginning-of-line)
   (while (< (point) end)
   (goto-char beg)
   (beginning-of-line)
   (while (< (point) end)
-    (let ((beg-sub (point)))
-      (if (looking-at ">")
+    (let ((beg-sub (point-marker))
+         (indent (make-string depth ? ))
+         (citation "[[:space:]]*>"))
+      (if (looking-at citation)
          (progn
          (progn
-           (while (looking-at ">")
+           (while (looking-at citation)
              (forward-line))
            (let ((overlay (make-overlay beg-sub (point))))
              (overlay-put overlay 'invisible 'notmuch-show-citation)
              (overlay-put overlay 'before-string
              (forward-line))
            (let ((overlay (make-overlay beg-sub (point))))
              (overlay-put overlay 'invisible 'notmuch-show-citation)
              (overlay-put overlay 'before-string
-                          (concat "[" (number-to-string (count-lines beg-sub (point)))
-                                  " quoted lines.]\n")))))
-      (if (looking-at "--[ ]?$")
-         (let ((sig-lines (count-lines beg-sub end)))
+                          (concat indent
+                                  "[" (number-to-string (count-lines beg-sub (point)))
+                                  "-line citation. Press 'c' to show.]\n")))))
+      (if (looking-at "[[:space:]]*-- ?$")
+         (let ((sig-lines (- (count-lines beg-sub end) 1)))
            (if (<= sig-lines notmuch-show-signature-lines-max)
                (progn
            (if (<= sig-lines notmuch-show-signature-lines-max)
                (progn
-                 (overlay-put (make-overlay beg-sub (+ beg-sub 1))
-                              'before-string
-                              (concat "[" (number-to-string sig-lines)
-                                      "-line signature.]"))
-                 (overlay-put (make-overlay (+ beg-sub 2) end)
+                 (overlay-put (make-overlay beg-sub end)
                               'invisible 'notmuch-show-signature)
                               'invisible 'notmuch-show-signature)
+                 (overlay-put (make-overlay beg (- beg-sub 1))
+                              'after-string
+                              (concat "\n" indent
+                                      "[" (number-to-string sig-lines)
+                                      "-line signature. Press 's' to show.]"))
                  (goto-char end)))))
       (forward-line))))
 
                  (goto-char end)))))
       (forward-line))))
 
-(defun notmuch-show-markup-body ()
+(defun notmuch-show-markup-part (beg end depth)
+  (if (re-search-forward notmuch-show-part-begin-regexp nil t)
+      (progn
+       (forward-line)
+       (let ((beg (point-marker)))
+         (re-search-forward notmuch-show-part-end-regexp)
+         (let ((end (copy-marker (match-beginning 0))))
+           (indent-rigidly beg end depth)
+           (notmuch-show-markup-citations-region beg end depth)
+           ; Advance to the next part (if any) (so the outer loop can
+           ; determine whether we've left the current message.
+           (re-search-forward notmuch-show-part-begin-regexp nil t))))
+    (goto-char end)))
+
+(defun notmuch-show-markup-parts-region (beg end depth)
+  (save-excursion
+    (goto-char beg)
+    (while (< (point) end)
+      (notmuch-show-markup-part beg end depth))))
+
+(defun notmuch-show-markup-body (depth)
   (re-search-forward notmuch-show-body-begin-regexp)
   (re-search-forward notmuch-show-body-begin-regexp)
-  (next-line 1)
-  (beginning-of-line)
-  (let ((beg (point)))
+  (forward-line)
+  (let ((beg (point-marker)))
     (re-search-forward notmuch-show-body-end-regexp)
     (re-search-forward notmuch-show-body-end-regexp)
-    (let ((end (match-beginning 0)))
-      (notmuch-show-markup-citations-region beg end)
+    (let ((end (copy-marker (match-beginning 0))))
+      (notmuch-show-markup-parts-region beg end depth)
       (if (not (notmuch-show-message-unread-p))
          (overlay-put (make-overlay beg end)
       (if (not (notmuch-show-message-unread-p))
          (overlay-put (make-overlay beg end)
-                      'invisible 'notmuch-show-body-read)))))
+                      'invisible 'notmuch-show-body-read))
+      (set-marker beg nil)
+      (set-marker end nil)
+      )))
 
 
-(defun notmuch-show-markup-header ()
+(defun notmuch-show-markup-header (depth)
   (re-search-forward notmuch-show-header-begin-regexp)
   (re-search-forward notmuch-show-header-begin-regexp)
-  (next-line 2)
-  (beginning-of-line)
-  (let ((beg (point)))
-    (re-search-forward notmuch-show-header-end-regexp)
-    (overlay-put (make-overlay beg (match-beginning 0))
-                'invisible 'notmuch-show-header)))
+  (forward-line)
+  (let ((beg (point-marker)))
+    (end-of-line)
+    ; Inverse video for subject
+    (overlay-put (make-overlay beg (point)) 'face '((cons :inverse-video t)))
+    (forward-line 2)
+    (let ((beg-hidden (point-marker)))
+      (re-search-forward notmuch-show-header-end-regexp)
+      (beginning-of-line)
+      (let ((end (point-marker)))
+       (indent-rigidly beg end depth)
+       (overlay-put (make-overlay beg-hidden end)
+                    'invisible 'notmuch-show-header)
+       (set-marker beg nil)
+       (set-marker beg-hidden nil)
+       (set-marker end nil)
+       ))))
 
 (defun notmuch-show-markup-message ()
   (if (re-search-forward notmuch-show-message-begin-regexp nil t)
       (progn
 
 (defun notmuch-show-markup-message ()
   (if (re-search-forward notmuch-show-message-begin-regexp nil t)
       (progn
-       (notmuch-show-markup-header)
-       (notmuch-show-markup-body))
+       (re-search-forward notmuch-show-depth-regexp)
+       (let ((depth (string-to-number (buffer-substring (match-beginning 1) (match-end 1)))))
+         (notmuch-show-markup-header depth)
+         (notmuch-show-markup-body depth)))
     (goto-char (point-max))))
 
 (defun notmuch-show-hide-markers ()
     (goto-char (point-max))))
 
 (defun notmuch-show-hide-markers ()
@@ -593,7 +663,9 @@ thread from that buffer can be show when done with this one)."
     (define-key map "a" 'notmuch-search-archive-thread)
     (define-key map "b" 'notmuch-search-scroll-down)
     (define-key map "f" 'notmuch-search-filter)
     (define-key map "a" 'notmuch-search-archive-thread)
     (define-key map "b" 'notmuch-search-scroll-down)
     (define-key map "f" 'notmuch-search-filter)
+    (define-key map "m" 'message-mail)
     (define-key map "n" 'next-line)
     (define-key map "n" 'next-line)
+    (define-key map "o" 'notmuch-search-toggle-order)
     (define-key map "p" 'previous-line)
     (define-key map "q" 'kill-this-buffer)
     (define-key map "s" 'notmuch-search)
     (define-key map "p" 'previous-line)
     (define-key map "q" 'kill-this-buffer)
     (define-key map "s" 'notmuch-search)
@@ -663,6 +735,7 @@ global search.
   (interactive)
   (kill-all-local-variables)
   (make-local-variable 'notmuch-search-query-string)
   (interactive)
   (kill-all-local-variables)
   (make-local-variable 'notmuch-search-query-string)
+  (make-local-variable 'notmuch-search-oldest-first)
   (set (make-local-variable 'scroll-preserve-screen-position) t)
   (add-to-invisibility-spec 'notmuch-search)
   (use-local-map notmuch-search-mode-map)
   (set (make-local-variable 'scroll-preserve-screen-position) t)
   (add-to-invisibility-spec 'notmuch-search)
   (use-local-map notmuch-search-mode-map)
@@ -674,15 +747,16 @@ global search.
   (save-excursion
     (beginning-of-line)
     (let ((beg (point)))
   (save-excursion
     (beginning-of-line)
     (let ((beg (point)))
-      (re-search-forward "thread:[a-fA-F0-9]*")
+      (re-search-forward "thread:[a-fA-F0-9]*" nil t)
       (filter-buffer-substring beg (point)))))
 
 (defun notmuch-search-markup-this-thread-id ()
   (beginning-of-line)
   (let ((beg (point)))
       (filter-buffer-substring beg (point)))))
 
 (defun notmuch-search-markup-this-thread-id ()
   (beginning-of-line)
   (let ((beg (point)))
-    (re-search-forward "thread:[a-fA-F0-9]*")
-    (forward-char)
-    (overlay-put (make-overlay beg (point)) 'invisible 'notmuch-search)))
+    (if (re-search-forward "thread:[a-fA-F0-9]*" nil t)
+       (progn
+         (forward-char)
+         (overlay-put (make-overlay beg (point)) 'invisible 'notmuch-search)))))
 
 (defun notmuch-search-markup-thread-ids ()
   (save-excursion
 
 (defun notmuch-search-markup-thread-ids ()
   (save-excursion
@@ -691,22 +765,9 @@ global search.
       (notmuch-search-markup-this-thread-id)
       (next-line))))
 
       (notmuch-search-markup-this-thread-id)
       (next-line))))
 
-(defun notmuch-search-hide-thread-ids ()
-  (interactive)
-  (add-to-invisibility-spec 'notmuch-search)
-  (force-window-update)
-  (redisplay t))
-
-(defun notmuch-search-show-thread-ids ()
-  (interactive)
-  (remove-from-invisibility-spec 'notmuch-search)
-  (force-window-update)
-  (redisplay t))
-
 (defun notmuch-search-show-thread ()
   (interactive)
   (let ((thread-id (notmuch-search-find-thread-id)))
 (defun notmuch-search-show-thread ()
   (interactive)
   (let ((thread-id (notmuch-search-find-thread-id)))
-    (forward-line)
     (if (> (length thread-id) 0)
        (notmuch-show thread-id (current-buffer))
       (error "End of search results"))))
     (if (> (length thread-id) 0)
        (notmuch-show thread-id (current-buffer))
       (error "End of search results"))))
@@ -752,12 +813,12 @@ and will also appear in a buffer named \"*Notmuch errors*\"."
 
 (defun notmuch-search-add-tag (tag)
   (interactive "sTag to add: ")
 
 (defun notmuch-search-add-tag (tag)
   (interactive "sTag to add: ")
-  (notmuch-call-notmuch-process "tag" (concat "+" tag) (concat "thread:" (notmuch-search-find-thread-id)))
+  (notmuch-call-notmuch-process "tag" (concat "+" tag) (notmuch-search-find-thread-id))
   (notmuch-search-set-tags (delete-dups (sort (cons tag (notmuch-search-get-tags)) 'string<))))
 
 (defun notmuch-search-remove-tag (tag)
   (interactive "sTag to remove: ")
   (notmuch-search-set-tags (delete-dups (sort (cons tag (notmuch-search-get-tags)) 'string<))))
 
 (defun notmuch-search-remove-tag (tag)
   (interactive "sTag to remove: ")
-  (notmuch-call-notmuch-process "tag" (concat "-" tag) (concat "thread:" (notmuch-search-find-thread-id)))
+  (notmuch-call-notmuch-process "tag" (concat "-" tag) (notmuch-search-find-thread-id))
   (notmuch-search-set-tags (delete tag (notmuch-search-get-tags))))
 
 (defun notmuch-search-archive-thread ()
   (notmuch-search-set-tags (delete tag (notmuch-search-get-tags))))
 
 (defun notmuch-search-archive-thread ()
@@ -768,13 +829,14 @@ This function advances point to the next line when finished."
   (notmuch-search-remove-tag "inbox")
   (forward-line))
 
   (notmuch-search-remove-tag "inbox")
   (forward-line))
 
-(defun notmuch-search (query)
+(defun notmuch-search (query &optional oldest-first)
   "Run \"notmuch search\" with the given query string and display results."
   (interactive "sNotmuch search: ")
   (let ((buffer (get-buffer-create (concat "*notmuch-search-" query "*"))))
     (switch-to-buffer buffer)
     (notmuch-search-mode)
     (set 'notmuch-search-query-string query)
   "Run \"notmuch search\" with the given query string and display results."
   (interactive "sNotmuch search: ")
   (let ((buffer (get-buffer-create (concat "*notmuch-search-" query "*"))))
     (switch-to-buffer buffer)
     (notmuch-search-mode)
     (set 'notmuch-search-query-string query)
+    (set 'notmuch-search-oldest-first oldest-first)
     (let ((proc (get-buffer-process (current-buffer)))
          (inhibit-read-only t))
       (if proc
     (let ((proc (get-buffer-process (current-buffer)))
          (inhibit-read-only t))
       (if proc
@@ -783,7 +845,9 @@ This function advances point to the next line when finished."
       (erase-buffer)
       (goto-char (point-min))
       (save-excursion
       (erase-buffer)
       (goto-char (point-min))
       (save-excursion
-       (call-process "notmuch" nil t nil "search" query)
+       (if oldest-first
+           (call-process "notmuch" nil t nil "search" query)
+         (call-process "notmuch" nil t nil "search" "--reverse" query))
        (notmuch-search-markup-thread-ids)
        ))))
 
        (notmuch-search-markup-thread-ids)
        ))))
 
@@ -797,22 +861,41 @@ thread. Otherwise, point will be moved to attempt to be in the
 same relative position within the new buffer."
   (interactive)
   (let ((here (point))
 same relative position within the new buffer."
   (interactive)
   (let ((here (point))
+       (oldest-first notmuch-search-oldest-first)
        (thread (notmuch-search-find-thread-id))
        (query notmuch-search-query-string))
     (kill-this-buffer)
        (thread (notmuch-search-find-thread-id))
        (query notmuch-search-query-string))
     (kill-this-buffer)
-    (notmuch-search query)
+    (notmuch-search query oldest-first)
     (goto-char (point-min))
     (if (re-search-forward (concat "^" thread) nil t)
        (beginning-of-line)
       (goto-char here))))
 
     (goto-char (point-min))
     (if (re-search-forward (concat "^" thread) nil t)
        (beginning-of-line)
       (goto-char here))))
 
+(defun notmuch-search-toggle-order ()
+  "Toggle the current search order.
+
+By default, the \"inbox\" view created by `notmuch' is displayed
+in chronological order (oldest thread at the beginning of the
+buffer), while any global searches created by `notmuch-search'
+are displayed in reverse-chronological order (newest thread at
+the beginning of the buffer).
+
+This command toggles the sort order for the current search.
+
+Note that any fitlered searches created by
+`notmuch-search-filter' retain the search order of the parent
+search."
+  (interactive)
+  (set 'notmuch-search-oldest-first (not notmuch-search-oldest-first))
+  (notmuch-search-refresh-view))
+
 (defun notmuch-search-filter (query)
   "Filter the current search results based on an additional query string.
 
 Runs a new search matching only messages that match both the
 current search results AND the additional query string provided."
   (interactive "sFilter search: ")
 (defun notmuch-search-filter (query)
   "Filter the current search results based on an additional query string.
 
 Runs a new search matching only messages that match both the
 current search results AND the additional query string provided."
   (interactive "sFilter search: ")
-  (notmuch-search (concat notmuch-search-query-string " and " query)))
+  (notmuch-search (concat notmuch-search-query-string " and " query) notmuch-search-oldest-first))
 
 (defun notmuch-search-filter-by-tag (tag)
   "Filter the current search results based on a single tag.
 
 (defun notmuch-search-filter-by-tag (tag)
   "Filter the current search results based on a single tag.
@@ -820,11 +903,11 @@ current search results AND the additional query string provided."
 Runs a new search matching only messages that match both the
 current search results AND that are tagged with the given tag."
   (interactive "sFilter by tag: ")
 Runs a new search matching only messages that match both the
 current search results AND that are tagged with the given tag."
   (interactive "sFilter by tag: ")
-  (notmuch-search (concat notmuch-search-query-string " and tag:" tag)))
+  (notmuch-search (concat notmuch-search-query-string " and tag:" tag) notmuch-search-oldest-first))
 
 (defun notmuch ()
   "Run notmuch to display all mail with tag of 'inbox'"
   (interactive)
 
 (defun notmuch ()
   "Run notmuch to display all mail with tag of 'inbox'"
   (interactive)
-  (notmuch-search "tag:inbox"))
+  (notmuch-search "tag:inbox" t))
 
 (provide 'notmuch)
 
 (provide 'notmuch)