]> git.notmuchmail.org Git - notmuch/blobdiff - notmuch.el
notmuch show: Implement proper thread ordering/nesting of messages.
[notmuch] / notmuch.el
index cd5c896449a38c1c264b10096a829f796235c35c..7e95f95671801e0c2e1c75c47d3bfddd8b792489 100644 (file)
@@ -19,6 +19,9 @@
 ;
 ; Authors: Carl Worth <cworth@cworth.org>
 
 ;
 ; Authors: Carl Worth <cworth@cworth.org>
 
+(require 'cl)
+(require 'mm-view)
+
 (defvar notmuch-show-mode-map
   (let ((map (make-sparse-keymap)))
     ; I don't actually want all of these toggle commands occupying
 (defvar notmuch-show-mode-map
   (let ((map (make-sparse-keymap)))
     ; I don't actually want all of these toggle commands occupying
     ; toggling visibility of these components. Probably using
     ; overlays-at to query and manipulate the current overlay.
     (define-key map "a" 'notmuch-show-archive-thread)
     ; toggling visibility of these components. Probably using
     ; overlays-at to query and manipulate the current overlay.
     (define-key map "a" 'notmuch-show-archive-thread)
+    (define-key map "A" 'notmuch-show-mark-read-then-archive-thread)
     (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-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 "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)
@@ -64,8 +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-filename-regexp "Filename: \\(.*\\)$")
+(set 'notmuch-show-id-regexp "\\(id:[^ ]*\\)")
+(set 'notmuch-show-filename-regexp "filename:\\(.*\\)$")
 (set 'notmuch-show-tags-regexp "(\\([^)]*\\))$")
 
 ; XXX: This should be a generic function in emacs somewhere, not here
 (set 'notmuch-show-tags-regexp "(\\([^)]*\\))$")
 
 ; XXX: This should be a generic function in emacs somewhere, not here
@@ -143,35 +151,34 @@ Unlike builtin `next-line' this version accepts no arguments."
     (re-search-forward notmuch-show-tags-regexp)
     (split-string (buffer-substring (match-beginning 1) (match-end 1)))))
 
     (re-search-forward notmuch-show-tags-regexp)
     (split-string (buffer-substring (match-beginning 1) (match-end 1)))))
 
-(defun notmuch-show-add-tag (tag)
+(defun notmuch-show-add-tag (&rest toadd)
+  "Add a tag to the current message."
   (interactive "sTag to add: ")
   (interactive "sTag to add: ")
-  (notmuch-call-notmuch-process "tag" (concat "+" tag) (concat "id:" (notmuch-show-get-message-id)))
-  (notmuch-show-set-tags (delete-dups (sort (cons tag (notmuch-show-get-tags)) 'string<))))
-
-(defun notmuch-show-remove-tag (tag)
+  (apply 'notmuch-call-notmuch-process
+        (append (cons "tag"
+                      (mapcar (lambda (s) (concat "+" s)) toadd))
+                (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)
+  "Remove a tag from the current message."
   (interactive "sTag to remove: ")
   (let ((tags (notmuch-show-get-tags)))
   (interactive "sTag to remove: ")
   (let ((tags (notmuch-show-get-tags)))
-    (if (member tag tags)
+    (if (intersection tags toremove :test 'string=)
        (progn
        (progn
-         (notmuch-call-notmuch-process "tag" (concat "-" tag) (concat "id:" (notmuch-show-get-message-id)))
-         (notmuch-show-set-tags (delete tag tags))))))
-
-(defun notmuch-show-archive-thread ()
-  "Archive each message in thread, and show next thread from search.
+         (apply 'notmuch-call-notmuch-process
+                (append (cons "tag"
+                              (mapcar (lambda (s) (concat "-" s)) toremove))
+                        (cons (notmuch-show-get-message-id) nil)))
+         (notmuch-show-set-tags (sort (set-difference tags toremove :test 'string=) 'string<))))))
 
 
-Archive each message currrently shown by removing the \"inbox\"
-tag from each. Then kill this buffer and show the next thread
-from the search from which this thread was originally shown.
-
-Note: This command is safe from any race condition of new messages
-being delivered to the same thread. It does not archive the
-entire thread, but only the messages shown in the current
-buffer."
-  (interactive)
+(defun notmuch-show-archive-thread-maybe-mark-read (markread)
   (save-excursion
     (goto-char (point-min))
     (while (not (eobp))
   (save-excursion
     (goto-char (point-min))
     (while (not (eobp))
-      (notmuch-show-remove-tag "inbox")
+      (if markread
+         (notmuch-show-remove-tag "unread" "inbox")
+       (notmuch-show-remove-tag "inbox"))
       (if (not (eobp))
          (forward-char))
       (if (not (re-search-forward notmuch-show-message-begin-regexp nil t))
       (if (not (eobp))
          (forward-char))
       (if (not (re-search-forward notmuch-show-message-begin-regexp nil t))
@@ -181,13 +188,68 @@ buffer."
     (if parent-buffer
        (progn
          (switch-to-buffer parent-buffer)
     (if parent-buffer
        (progn
          (switch-to-buffer parent-buffer)
+         (forward-line)
          (notmuch-search-show-thread)))))
 
          (notmuch-search-show-thread)))))
 
+(defun notmuch-show-mark-read-then-archive-thread ()
+  "Remove \"unread\" tag from each message, then archive and show next thread.
+
+Archive each message currrently shown by removing the \"unread\"
+and \"inbox\" tag from each. Then kill this buffer and show the
+next thread from the search from which this thread was originally
+shown.
+
+Note: This command is safe from any race condition of new messages
+being delivered to the same thread. It does not archive the
+entire thread, but only the messages shown in the current
+buffer."
+  (interactive)
+  (notmuch-show-archive-thread-maybe-mark-read t))
+
+(defun notmuch-show-archive-thread ()
+  "Archive each message in thread, and show next thread from search.
+
+Archive each message currrently shown by removing the \"inbox\"
+tag from each. Then kill this buffer and show the next thread
+from the search from which this thread was originally shown.
+
+Note: This command is safe from any race condition of new messages
+being delivered to the same thread. It does not archive the
+entire thread, but only the messages shown in the current
+buffer."
+  (interactive)
+  (notmuch-show-archive-thread-maybe-mark-read nil))
+
 (defun notmuch-show-view-raw-message ()
   "View the raw email of the current message."
   (interactive)
   (view-file (notmuch-show-get-filename)))
 
 (defun notmuch-show-view-raw-message ()
   "View the raw email of the current message."
   (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.
 
@@ -358,11 +420,10 @@ which this thread was originally shown."
        (unread (notmuch-show-message-unread-p)))
     (if (> next (window-end))
        (scroll-up nil)
        (unread (notmuch-show-message-unread-p)))
     (if (> next (window-end))
        (scroll-up nil)
-      (if unread
-         (notmuch-show-mark-read-then-next-open-message)
-       (if (notmuch-show-last-message-p)
-           (notmuch-show-archive-thread)
-         (notmuch-show-next-open-message))))))
+      (let ((last (notmuch-show-last-message-p)))
+       (notmuch-show-mark-read-then-next-open-message)
+       (if last
+           (notmuch-show-archive-thread))))))
 
 (defun notmuch-show-markup-citations-region (beg end)
   (goto-char beg)
 
 (defun notmuch-show-markup-citations-region (beg end)
   (goto-char beg)
@@ -377,7 +438,7 @@ which this thread was originally shown."
              (overlay-put overlay 'invisible 'notmuch-show-citation)
              (overlay-put overlay 'before-string
                           (concat "[" (number-to-string (count-lines 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")))))
+                                  "-line citation. Press 'c' to show.]\n")))))
       (if (looking-at "--[ ]?$")
          (let ((sig-lines (count-lines beg-sub end)))
            (if (<= sig-lines notmuch-show-signature-lines-max)
       (if (looking-at "--[ ]?$")
          (let ((sig-lines (count-lines beg-sub end)))
            (if (<= sig-lines notmuch-show-signature-lines-max)
@@ -385,7 +446,7 @@ which this thread was originally shown."
                  (overlay-put (make-overlay beg-sub (+ beg-sub 1))
                               'before-string
                               (concat "[" (number-to-string sig-lines)
                  (overlay-put (make-overlay beg-sub (+ beg-sub 1))
                               'before-string
                               (concat "[" (number-to-string sig-lines)
-                                      "-line signature.]"))
+                                      "-line signature. Press 's' to show.]"))
                  (overlay-put (make-overlay (+ beg-sub 2) end)
                               'invisible 'notmuch-show-signature)
                  (goto-char end)))))
                  (overlay-put (make-overlay (+ beg-sub 2) end)
                               'invisible 'notmuch-show-signature)
                  (goto-char end)))))
@@ -405,9 +466,15 @@ which this thread was originally shown."
 
 (defun notmuch-show-markup-header ()
   (re-search-forward notmuch-show-header-begin-regexp)
 
 (defun notmuch-show-markup-header ()
   (re-search-forward notmuch-show-header-begin-regexp)
-  (next-line 2)
+  (forward-line 1)
   (beginning-of-line)
   (let ((beg (point)))
   (beginning-of-line)
   (let ((beg (point)))
+    (end-of-line)
+    ; Inverse video for subject
+    (overlay-put (make-overlay beg (point)) 'face '((cons :inverse-video t)))
+    (beginning-of-line)
+    (forward-line 2)
+    (set 'beg (point))
     (re-search-forward notmuch-show-header-end-regexp)
     (overlay-put (make-overlay beg (match-beginning 0))
                 'invisible 'notmuch-show-header)))
     (re-search-forward notmuch-show-header-end-regexp)
     (overlay-put (make-overlay beg (match-beginning 0))
                 'invisible 'notmuch-show-header)))
@@ -562,7 +629,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)
@@ -632,6 +701,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)
@@ -643,15 +713,16 @@ global search.
   (save-excursion
     (beginning-of-line)
     (let ((beg (point)))
   (save-excursion
     (beginning-of-line)
     (let ((beg (point)))
-      (re-search-forward "[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 "[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
@@ -660,27 +731,18 @@ 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"))))
 
 (defun notmuch-call-notmuch-process (&rest args)
     (if (> (length thread-id) 0)
        (notmuch-show thread-id (current-buffer))
       (error "End of search results"))))
 
 (defun notmuch-call-notmuch-process (&rest args)
+  "Synchronously invoke \"notmuch\" with the given list of arguments.
+
+Output from the process will be presented to the user as an error
+and will also appear in a buffer named \"*Notmuch errors*\"."
   (let ((error-buffer (get-buffer-create "*Notmuch errors*")))
     (with-current-buffer error-buffer
        (erase-buffer))
   (let ((error-buffer (get-buffer-create "*Notmuch errors*")))
     (with-current-buffer error-buffer
        (erase-buffer))
@@ -717,12 +779,12 @@ global search.
 
 (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 ()
@@ -733,13 +795,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
@@ -748,7 +811,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)
        ))))
 
@@ -762,22 +827,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.
@@ -785,11 +869,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)