-;; notmuch.el --- run notmuch within emacs
+;;; notmuch.el --- run notmuch within emacs
;;
;; Copyright © Carl Worth
;;
;; General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
-;; along with Notmuch. If not, see <http://www.gnu.org/licenses/>.
+;; along with Notmuch. If not, see <https://www.gnu.org/licenses/>.
;;
;; Authors: Carl Worth <cworth@cworth.org>
+;; Homepage: https://notmuchmail.org/
+
+;;; Commentary:
;; This is an emacs-based interface to the notmuch mail system.
;;
;; You will first need to have the notmuch program installed and have a
;; notmuch database built in order to use this. See
-;; http://notmuchmail.org for details.
+;; https://notmuchmail.org for details.
;;
;; To install this software, copy it to a directory that is on the
;; `load-path' variable within emacs (a good candidate is
;;
;; Then, to actually run it, add:
;;
-;; (require 'notmuch)
+;; (autoload 'notmuch "notmuch" "Notmuch mail" t)
;;
;; to your ~/.emacs file, and then run "M-x notmuch" from within emacs,
;; or run:
;;
;; Have fun, and let us know if you have any comment, questions, or
;; kudos: Notmuch list <notmuch@notmuchmail.org> (subscription is not
-;; required, but is available from http://notmuchmail.org).
+;; required, but is available from https://notmuchmail.org).
+
+;;; Code:
(eval-when-compile (require 'cl))
(require 'mm-view)
(require 'notmuch-lib)
(require 'notmuch-tag)
(require 'notmuch-show)
+(require 'notmuch-tree)
(require 'notmuch-mua)
(require 'notmuch-hello)
(require 'notmuch-maildir-fcc)
:type '(alist :key-type (string) :value-type (string))
:group 'notmuch-search)
+;; The name of this variable `notmuch-init-file' is consistent with the
+;; convention used in e.g. emacs and gnus. The value, `notmuch-config[.el[c]]'
+;; is consistent with notmuch cli configuration file `~/.notmuch-config'.
+(defcustom notmuch-init-file (locate-user-emacs-file "notmuch-config")
+ "Your Notmuch Emacs-Lisp configuration file name.
+If a file with one of the suffixes defined by `get-load-suffixes' exists,
+it will be read instead.
+This file is read once when notmuch is loaded; the notmuch hooks added
+there will be called at other points of notmuch execution."
+ :type 'file
+ :group 'notmuch)
+
(defvar notmuch-query-history nil
"Variable to store minibuffer history for notmuch queries")
(mm-save-part p))))
mm-handle))
-(defun notmuch-documentation-first-line (symbol)
- "Return the first line of the documentation string for SYMBOL."
- (let ((doc (documentation symbol)))
- (if doc
- (with-temp-buffer
- (insert (documentation symbol t))
- (goto-char (point-min))
- (let ((beg (point)))
- (end-of-line)
- (buffer-substring beg (point))))
- "")))
-
-(defun notmuch-prefix-key-description (key)
- "Given a prefix key code, return a human-readable string representation.
-
-This is basically just `format-kbd-macro' but we also convert ESC to M-."
- (let ((desc (format-kbd-macro (vector key))))
- (if (string= desc "ESC")
- "M-"
- (concat desc " "))))
-
-;; I would think that emacs would have code handy for walking a keymap
-;; and generating strings for each key, and I would prefer to just call
-;; that. But I couldn't find any (could be all implemented in C I
-;; suppose), so I wrote my own here.
-(defun notmuch-substitute-one-command-key-with-prefix (prefix binding)
- "For a key binding, return a string showing a human-readable
-representation of the prefixed key as well as the first line of
-documentation from the bound function.
-
-For a mouse binding, return nil."
- (let ((key (car binding))
- (action (cdr binding)))
- (if (mouse-event-p key)
- nil
- (if (keymapp action)
- (let ((substitute (apply-partially 'notmuch-substitute-one-command-key-with-prefix (notmuch-prefix-key-description key)))
- (as-list))
- (map-keymap (lambda (a b)
- (push (cons a b) as-list))
- action)
- (mapconcat substitute as-list "\n"))
- (concat prefix (format-kbd-macro (vector key))
- "\t"
- (notmuch-documentation-first-line action))))))
-
-(defun notmuch-substitute-command-keys-one (key)
- ;; A `keymap' key indicates inheritance from a parent keymap - the
- ;; inherited mappings follow, so there is nothing to print for
- ;; `keymap' itself.
- (when (not (eq key 'keymap))
- (notmuch-substitute-one-command-key-with-prefix nil key)))
-
-(defun notmuch-substitute-command-keys (doc)
- "Like `substitute-command-keys' but with documentation, not function names."
- (let ((beg 0))
- (while (string-match "\\\\{\\([^}[:space:]]*\\)}" doc beg)
- (let* ((keymap-name (substring doc (match-beginning 1) (match-end 1)))
- (keymap (symbol-value (intern keymap-name))))
- (setq doc (replace-match
- (mapconcat #'notmuch-substitute-command-keys-one
- (cdr keymap) "\n")
- 1 1 doc)))
- (setq beg (match-end 0)))
- doc))
-
-(defun notmuch-help ()
- "Display help for the current notmuch mode."
- (interactive)
- (let* ((mode major-mode)
- (doc (substitute-command-keys (notmuch-substitute-command-keys (documentation mode t)))))
- (with-current-buffer (generate-new-buffer "*notmuch-help*")
- (insert doc)
- (goto-char (point-min))
- (set-buffer-modified-p nil)
- (view-buffer (current-buffer) 'kill-buffer-if-not-modified))))
-
(require 'hl-line)
(defun notmuch-hl-line-mode ()
(defvar notmuch-search-mode-map
(let ((map (make-sparse-keymap)))
- (define-key map "?" 'notmuch-help)
- (define-key map "q" 'notmuch-search-quit)
- (define-key map "x" 'notmuch-search-quit)
+ (set-keymap-parent map notmuch-common-keymap)
+ (define-key map "x" 'notmuch-bury-or-kill-this-buffer)
(define-key map (kbd "<DEL>") 'notmuch-search-scroll-down)
(define-key map "b" 'notmuch-search-scroll-down)
(define-key map " " 'notmuch-search-scroll-up)
(define-key map "n" 'notmuch-search-next-thread)
(define-key map "r" 'notmuch-search-reply-to-thread-sender)
(define-key map "R" 'notmuch-search-reply-to-thread)
- (define-key map "m" 'notmuch-mua-new-mail)
- (define-key map "s" 'notmuch-search)
(define-key map "o" 'notmuch-search-toggle-order)
(define-key map "c" 'notmuch-search-stash-map)
- (define-key map "=" 'notmuch-search-refresh-view)
- (define-key map "G" 'notmuch-search-poll-and-refresh-view)
(define-key map "t" 'notmuch-search-filter-by-tag)
- (define-key map "f" 'notmuch-search-filter)
+ (define-key map "l" 'notmuch-search-filter)
(define-key map [mouse-1] 'notmuch-search-show-thread)
+ (define-key map "k" 'notmuch-tag-jump)
(define-key map "*" 'notmuch-search-tag-all)
(define-key map "a" 'notmuch-search-archive-thread)
(define-key map "-" 'notmuch-search-remove-tag)
(define-key map "+" 'notmuch-search-add-tag)
(define-key map (kbd "RET") 'notmuch-search-show-thread)
+ (define-key map "Z" 'notmuch-tree-from-search-current-query)
map)
"Keymap for \"notmuch search\" buffers.")
(fset 'notmuch-search-mode-map notmuch-search-mode-map)
(defvar notmuch-search-stash-map
(let ((map (make-sparse-keymap)))
(define-key map "i" 'notmuch-search-stash-thread-id)
+ (define-key map "q" 'notmuch-stash-query)
+ (define-key map "?" 'notmuch-subkeymap-help)
map)
"Submap for stash commands")
(fset 'notmuch-search-stash-map notmuch-search-stash-map)
(interactive)
(notmuch-common-do-stash (notmuch-search-find-thread-id)))
+(defun notmuch-stash-query ()
+ "Copy current query to kill-ring."
+ (interactive)
+ (notmuch-common-do-stash (notmuch-search-get-query)))
+
(defvar notmuch-search-query-string)
(defvar notmuch-search-target-thread)
(defvar notmuch-search-target-line)
-(defvar notmuch-search-continuation)
(defvar notmuch-search-disjunctive-regexp "\\<[oO][rR]\\>")
-(defun notmuch-search-quit ()
- "Exit the search buffer, calling any defined continuation function."
- (interactive)
- (let ((continuation notmuch-search-continuation))
- (notmuch-kill-this-buffer)
- (when continuation
- (funcall continuation))))
-
(defun notmuch-search-scroll-up ()
"Move forward through search results by one window's worth."
(interactive)
:group 'notmuch-search
:group 'notmuch-faces)
-(defun notmuch-search-mode ()
+(defface notmuch-search-flagged-face
+ '((t
+ (:weight bold)))
+ "Face used in search mode face for flagged threads.
+
+This face is the default value for the \"flagged\" tag in
+`notmuch-search-line-faces`."
+ :group 'notmuch-search
+ :group 'notmuch-faces)
+
+(defface notmuch-search-unread-face
+ '((t
+ (:foreground "blue")))
+ "Face used in search mode for unread threads.
+
+This face is the default value for the \"unread\" tag in
+`notmuch-search-line-faces`."
+ :group 'notmuch-search
+ :group 'notmuch-faces)
+
+(define-derived-mode notmuch-search-mode fundamental-mode "notmuch-search"
"Major mode displaying results of a notmuch search.
This buffer contains the results of a \"notmuch search\" of your
participants in the thread, a representative subject line, and
any tags).
-Pressing \\[notmuch-search-show-thread] on any line displays that thread. The '\\[notmuch-search-add-tag]' and '\\[notmuch-search-remove-tag]'
-keys can be used to add or remove tags from a thread. The '\\[notmuch-search-archive-thread]' key
-is a convenience for archiving a thread (removing the \"inbox\"
-tag). The '\\[notmuch-search-tag-all]' key can be used to add and/or remove tags from all
-messages (as opposed to threads) that match the current query. Use with caution, as this
-will also tag matching messages that arrived *after* constructing the buffer.
-
-Other useful commands are '\\[notmuch-search-filter]' for filtering the current search
-based on an additional query string, '\\[notmuch-search-filter-by-tag]' for filtering to include
-only messages with a given tag, and '\\[notmuch-search]' to execute a new, global
-search.
+Pressing \\[notmuch-search-show-thread] on any line displays that
+thread. The '\\[notmuch-search-add-tag]' and
+'\\[notmuch-search-remove-tag]' keys can be used to add or remove
+tags from a thread. The '\\[notmuch-search-archive-thread]' key
+is a convenience for archiving a thread (applying changes in
+`notmuch-archive-tags'). The '\\[notmuch-search-tag-all]' key can
+be used to add and/or remove tags from all messages (as opposed
+to threads) that match the current query. Use with caution, as
+this will also tag matching messages that arrived *after*
+constructing the buffer.
+
+Other useful commands are '\\[notmuch-search-filter]' for
+filtering the current search based on an additional query string,
+'\\[notmuch-search-filter-by-tag]' for filtering to include only
+messages with a given tag, and '\\[notmuch-search]' to execute a
+new, global search.
Complete list of currently available key bindings:
\\{notmuch-search-mode-map}"
- (interactive)
- (kill-all-local-variables)
(make-local-variable 'notmuch-search-query-string)
(make-local-variable 'notmuch-search-oldest-first)
(make-local-variable 'notmuch-search-target-thread)
(make-local-variable 'notmuch-search-target-line)
- (set (make-local-variable 'notmuch-search-continuation) nil)
+ (setq notmuch-buffer-refresh-function #'notmuch-search-refresh-view)
(set (make-local-variable 'scroll-preserve-screen-position) t)
(add-to-invisibility-spec (cons 'ellipsis t))
- (use-local-map notmuch-search-mode-map)
(setq truncate-lines t)
- (setq major-mode 'notmuch-search-mode
- mode-name "notmuch-search")
(setq buffer-read-only t))
(defun notmuch-search-get-result (&optional pos)
(let ((thread (plist-get (notmuch-search-get-result) :thread)))
(when thread (concat (unless bare "thread:") thread))))
-(defun notmuch-search-find-thread-id-region (beg end)
- "Return a list of threads for the current region"
- (mapcar (lambda (thread) (concat "thread:" thread))
- (notmuch-search-properties-in-region :thread beg end)))
+(defun notmuch-search-find-stable-query ()
+ "Return the stable queries for the current thread.
+
+This returns a list (MATCHED-QUERY UNMATCHED-QUERY) for the
+matched and unmatched messages in the current thread."
+ (plist-get (notmuch-search-get-result) :query))
-(defun notmuch-search-find-thread-id-region-search (beg end)
- "Return a search string for threads for the current region"
- (mapconcat 'identity (notmuch-search-find-thread-id-region beg end) " or "))
+(defun notmuch-search-find-stable-query-region (beg end &optional only-matched)
+ "Return the stable query for the current region.
+
+If ONLY-MATCHED is non-nil, include only matched messages. If it
+is nil, include both matched and unmatched messages. If there are
+no messages in the region then return nil."
+ (let ((query-list nil) (all (not only-matched)))
+ (dolist (queries (notmuch-search-properties-in-region :query beg end))
+ (when (first queries)
+ (push (first queries) query-list))
+ (when (and all (second queries))
+ (push (second queries) query-list)))
+ (when query-list
+ (concat "(" (mapconcat 'identity query-list ") or (") ")"))))
(defun notmuch-search-find-authors ()
"Return the authors for the current thread"
"Return a list of authors for the current region"
(notmuch-search-properties-in-region :subject beg end))
-(defun notmuch-search-show-thread ()
- "Display the currently selected thread."
- (interactive)
+(defun notmuch-search-show-thread (&optional elide-toggle)
+ "Display the currently selected thread.
+
+With a prefix argument, invert the default value of
+`notmuch-show-only-matching-messages' when displaying the
+thread."
+ (interactive "P")
(let ((thread-id (notmuch-search-find-thread-id))
(subject (notmuch-search-find-subject)))
(if (> (length thread-id) 0)
(notmuch-show thread-id
+ elide-toggle
(current-buffer)
notmuch-search-query-string
;; Name the buffer based on the subject.
(concat "*" (truncate-string-to-width subject 30 nil nil t) "*"))
(message "End of search results."))))
+(defun notmuch-tree-from-search-current-query ()
+ "Call notmuch tree with the current query"
+ (interactive)
+ (notmuch-tree notmuch-search-query-string))
+
+(defun notmuch-tree-from-search-thread ()
+ "Show the selected thread with notmuch-tree"
+ (interactive)
+ (notmuch-tree (notmuch-search-find-thread-id)
+ notmuch-search-query-string
+ nil
+ (notmuch-prettify-subject (notmuch-search-find-subject))
+ t))
+
(defun notmuch-search-reply-to-thread (&optional prompt-for-sender)
"Begin composing a reply-all to the entire current thread in a new buffer."
(interactive "P")
(let ((message-id (notmuch-search-find-thread-id)))
(notmuch-mua-new-reply message-id prompt-for-sender nil)))
-(defun notmuch-call-notmuch-process (&rest args)
- "Synchronously invoke \"notmuch\" with the given list of arguments.
-
-If notmuch exits with a non-zero status, output from the process
-will appear in a buffer named \"*Notmuch errors*\" and an error
-will be signaled."
- (with-temp-buffer
- (let ((status (apply #'call-process notmuch-command nil t nil args)))
- (notmuch-check-exit-status status (cons notmuch-command args)
- (buffer-string)))))
-
(defun notmuch-search-set-tags (tags &optional pos)
(let ((new-result (plist-put (notmuch-search-get-result pos) :tags tags)))
(notmuch-search-update-result new-result pos)))
(setq output (append output (notmuch-search-get-tags pos)))))
output))
-(defun notmuch-search-tag-region (beg end &optional tag-changes)
- "Change tags for threads in the given region."
- (let ((search-string (notmuch-search-find-thread-id-region-search beg end)))
- (setq tag-changes (notmuch-tag search-string tag-changes))
+(defun notmuch-search-interactive-region ()
+ "Return the bounds of the current interactive region.
+
+This returns (BEG END), where BEG and END are the bounds of the
+region if the region is active, or both `point' otherwise."
+ (if (region-active-p)
+ (list (region-beginning) (region-end))
+ (list (point) (point))))
+
+(defun notmuch-search-interactive-tag-changes (&optional initial-input)
+ "Prompt for tag changes for the current thread or region.
+
+Returns (TAG-CHANGES REGION-BEGIN REGION-END)."
+ (let* ((region (notmuch-search-interactive-region))
+ (beg (first region)) (end (second region))
+ (prompt (if (= beg end) "Tag thread" "Tag region")))
+ (cons (notmuch-read-tag-changes
+ (notmuch-search-get-tags-region beg end) prompt initial-input)
+ region)))
+
+(defun notmuch-search-tag (tag-changes &optional beg end only-matched)
+ "Change tags for the currently selected thread or region.
+
+See `notmuch-tag' for information on the format of TAG-CHANGES.
+When called interactively, this uses the region if the region is
+active. When called directly, BEG and END provide the region.
+If these are nil or not provided, this applies to the thread at
+point.
+
+If ONLY-MATCHED is non-nil, only tag matched messages."
+ (interactive (notmuch-search-interactive-tag-changes))
+ (unless (and beg end) (setq beg (point) end (point)))
+ (let ((search-string (notmuch-search-find-stable-query-region
+ beg end only-matched)))
+ (notmuch-tag search-string tag-changes)
(notmuch-search-foreach-result beg end
(lambda (pos)
(notmuch-search-set-tags
(notmuch-update-tags (notmuch-search-get-tags pos) tag-changes)
pos)))))
-(defun notmuch-search-tag (&optional tag-changes)
- "Change tags for the currently selected thread or region.
+(defun notmuch-search-add-tag (tag-changes &optional beg end)
+ "Change tags for the current thread or region (defaulting to add).
-See `notmuch-tag' for information on the format of TAG-CHANGES."
- (interactive)
- (let* ((beg (if (region-active-p) (region-beginning) (point)))
- (end (if (region-active-p) (region-end) (point))))
- (notmuch-search-tag-region beg end tag-changes)))
+Same as `notmuch-search-tag' but sets initial input to '+'."
+ (interactive (notmuch-search-interactive-tag-changes "+"))
+ (notmuch-search-tag tag-changes beg end))
-(defun notmuch-search-add-tag ()
- "Same as `notmuch-search-tag' but sets initial input to '+'."
- (interactive)
- (notmuch-search-tag "+"))
+(defun notmuch-search-remove-tag (tag-changes &optional beg end)
+ "Change tags for the current thread or region (defaulting to remove).
-(defun notmuch-search-remove-tag ()
- "Same as `notmuch-search-tag' but sets initial input to '-'."
- (interactive)
- (notmuch-search-tag "-"))
+Same as `notmuch-search-tag' but sets initial input to '-'."
+ (interactive (notmuch-search-interactive-tag-changes "-"))
+ (notmuch-search-tag tag-changes beg end))
-(defun notmuch-search-archive-thread (&optional unarchive)
- "Archive the currently selected thread.
+(put 'notmuch-search-archive-thread 'notmuch-prefix-doc
+ "Un-archive the currently selected thread.")
+(defun notmuch-search-archive-thread (&optional unarchive beg end)
+ "Archive the currently selected thread or region.
Archive each message in the currently selected thread by applying
the tag changes in `notmuch-archive-tags' to each (remove the
`notmuch-archive-tags' will be reversed).
This function advances the next thread when finished."
- (interactive "P")
+ (interactive (cons current-prefix-arg (notmuch-search-interactive-region)))
(when notmuch-archive-tags
(notmuch-search-tag
- (notmuch-tag-change-list notmuch-archive-tags unarchive)))
- (notmuch-search-next-thread))
+ (notmuch-tag-change-list notmuch-archive-tags unarchive) beg end))
+ (when (eq beg end)
+ (notmuch-search-next-thread)))
(defun notmuch-search-update-result (result &optional pos)
"Replace the result object of the thread at POS (or point) by
(goto-char (point-min))
(forward-line (1- notmuch-search-target-line)))))))))
-(defcustom notmuch-search-line-faces '(("unread" :weight bold)
- ("flagged" :foreground "blue"))
- "Tag/face mapping for line highlighting in notmuch-search.
+(defcustom notmuch-search-line-faces
+ '(("unread" 'notmuch-search-unread-face)
+ ("flagged" 'notmuch-search-flagged-face))
+ "Alist of tags to faces for line highlighting in notmuch-search.
+Each element looks like (TAG . FACE).
+A thread with TAG will have FACE applied.
Here is an example of how to color search results based on tags.
(the following text would be placed in your ~/.emacs file):
- (setq notmuch-search-line-faces '((\"deleted\" . (:foreground \"red\"
- :background \"blue\"))
- (\"unread\" . (:foreground \"green\"))))
+ (setq notmuch-search-line-faces '((\"unread\" . (:foreground \"green\"))
+ (\"deleted\" . (:foreground \"red\"
+ :background \"blue\"))))
-The attributes defined for matching tags are merged, with later
-attributes overriding earlier. A message having both \"deleted\"
-and \"unread\" tags with the above settings would have a green
-foreground and blue background."
+The FACE must be a face name (a symbol or string), a property
+list of face attributes, or a list of these. The faces for
+matching tags are merged, with earlier attributes overriding
+later. A message having both \"deleted\" and \"unread\" tags with
+the above settings would have a green foreground and blue
+background."
:type '(alist :key-type (string) :value-type (custom-face-edit))
:group 'notmuch-search
:group 'notmuch-faces)
(defun notmuch-search-color-line (start end line-tag-list)
"Colorize lines in `notmuch-show' based on tags."
- (mapc (lambda (elem)
- (let ((tag (car elem))
- (attributes (cdr elem)))
- (when (member tag line-tag-list)
- (notmuch-combine-face-text-property start end attributes))))
- ;; Reverse the list so earlier entries take precedence
- (reverse notmuch-search-line-faces)))
+ ;; Reverse the list so earlier entries take precedence
+ (dolist (elem (reverse notmuch-search-line-faces))
+ (let ((tag (car elem))
+ (face (cdr elem)))
+ (when (member tag line-tag-list)
+ (notmuch-apply-face nil face nil start end)))))
(defun notmuch-search-author-propertize (authors)
"Split `authors' into matching and non-matching authors and
(plist-get result :total)))
'face 'notmuch-search-count)))
((string-equal field "subject")
- (insert (propertize (format format-string (plist-get result :subject))
+ (insert (propertize (format format-string
+ (notmuch-sanitize (plist-get result :subject)))
'face 'notmuch-search-subject)))
((string-equal field "authors")
- (notmuch-search-insert-authors format-string (plist-get result :authors)))
+ (notmuch-search-insert-authors
+ format-string (notmuch-sanitize (plist-get result :authors))))
((string-equal field "tags")
- (let ((tags (plist-get result :tags)))
- (insert (format format-string (notmuch-tag-format-tags tags)))))))
+ (let ((tags (plist-get result :tags))
+ (orig-tags (plist-get result :orig-tags)))
+ (insert (format format-string (notmuch-tag-format-tags tags orig-tags)))))))
-(defun notmuch-search-show-result (result &optional pos)
- "Insert RESULT at POS or the end of the buffer if POS is null."
+(defun notmuch-search-show-result (result pos)
+ "Insert RESULT at POS."
;; Ignore excluded matches
(unless (= (plist-get result :matched) 0)
- (let ((beg (or pos (point-max))))
- (save-excursion
- (goto-char beg)
- (dolist (spec notmuch-search-result-format)
- (notmuch-search-insert-field (car spec) (cdr spec) result))
- (insert "\n")
- (notmuch-search-color-line beg (point) (plist-get result :tags))
- (put-text-property beg (point) 'notmuch-search-result result))
- (when (string= (plist-get result :thread) notmuch-search-target-thread)
- (setq notmuch-search-target-thread "found")
- (goto-char beg)))))
+ (save-excursion
+ (goto-char pos)
+ (dolist (spec notmuch-search-result-format)
+ (notmuch-search-insert-field (car spec) (cdr spec) result))
+ (insert "\n")
+ (notmuch-search-color-line pos (point) (plist-get result :tags))
+ (put-text-property pos (point) 'notmuch-search-result result))))
+
+(defun notmuch-search-append-result (result)
+ "Insert RESULT at the end of the buffer.
+
+This is only called when a result is first inserted so it also
+sets the :orig-tag property."
+ (let ((new-result (plist-put result :orig-tags (plist-get result :tags)))
+ (pos (point-max)))
+ (notmuch-search-show-result new-result pos)
+ (when (string= (plist-get result :thread) notmuch-search-target-thread)
+ (setq notmuch-search-target-thread "found")
+ (goto-char pos))))
(defun notmuch-search-process-filter (proc string)
"Process and filter the output of \"notmuch search\""
(parse-buf (process-get proc 'parse-buf))
(inhibit-read-only t)
done)
- (if (not (buffer-live-p results-buf))
- (delete-process proc)
+ (when (buffer-live-p results-buf)
(with-current-buffer parse-buf
;; Insert new data
(save-excursion
(goto-char (point-max))
(insert string))
- (notmuch-sexp-parse-partial-list 'notmuch-search-show-result
+ (notmuch-sexp-parse-partial-list 'notmuch-search-append-result
results-buf)))))
-(defun notmuch-search-tag-all (&optional tag-changes)
+(defun notmuch-search-tag-all (tag-changes)
"Add/remove tags from all messages in current search buffer.
See `notmuch-tag' for information on the format of TAG-CHANGES."
- (interactive)
- (apply 'notmuch-tag notmuch-search-query-string tag-changes))
+ (interactive
+ (list (notmuch-read-tag-changes
+ (notmuch-search-get-tags-region (point-min) (point-max)) "Tag all")))
+ (notmuch-search-tag tag-changes (point-min) (point-max) t))
(defun notmuch-search-buffer-title (query)
"Returns the title for a buffer with notmuch search results."
(let (longest
(longest-length 0))
(loop for tuple in notmuch-saved-searches
- if (let ((quoted-query (regexp-quote (cdr tuple))))
+ if (let ((quoted-query (regexp-quote (notmuch-saved-search-get tuple :query))))
(and (string-match (concat "^" quoted-query) query)
(> (length (match-string 0 query))
longest-length)))
do (setq longest tuple))
longest))
- (saved-search-name (car saved-search))
- (saved-search-query (cdr saved-search)))
+ (saved-search-name (notmuch-saved-search-get saved-search :name))
+ (saved-search-query (notmuch-saved-search-get saved-search :query)))
(cond ((and saved-search (equal saved-search-query query))
;; Query is the same as saved search (ignoring case)
(concat "*notmuch-saved-search-" saved-search-name "*"))
"Read a notmuch-query from the minibuffer with completion.
PROMPT is the string to prompt with."
- (lexical-let
- ((completions
- (append (list "folder:" "thread:" "id:" "date:" "from:" "to:"
- "subject:" "attachment:")
- (mapcar (lambda (tag)
- (concat "tag:" (notmuch-escape-boolean-term tag)))
- (process-lines notmuch-command "search" "--output=tags" "*")))))
+ (lexical-let*
+ ((all-tags
+ (mapcar (lambda (tag) (notmuch-escape-boolean-term tag))
+ (process-lines notmuch-command "search" "--output=tags" "*")))
+ (completions
+ (append (list "folder:" "path:" "thread:" "id:" "date:" "from:" "to:"
+ "subject:" "attachment:")
+ (mapcar (lambda (tag) (concat "tag:" tag)) all-tags)
+ (mapcar (lambda (tag) (concat "is:" tag)) all-tags)
+ (mapcar (lambda (mimetype) (concat "mimetype:" mimetype)) (mailcap-mime-types)))))
(let ((keymap (copy-keymap minibuffer-local-map))
+ (current-query (case major-mode
+ (notmuch-search-mode (notmuch-search-get-query))
+ (notmuch-show-mode (notmuch-show-get-query))
+ (notmuch-tree-mode (notmuch-tree-get-query))))
(minibuffer-completion-table
(completion-table-dynamic
(lambda (string)
(define-key keymap (kbd "TAB") 'minibuffer-complete)
(let ((history-delete-duplicates t))
(read-from-minibuffer prompt nil keymap nil
- 'notmuch-search-history nil nil)))))
+ 'notmuch-search-history current-query nil)))))
+
+(defun notmuch-search-get-query ()
+ "Return the current query in this search buffer"
+ notmuch-search-query-string)
;;;###autoload
-(defun notmuch-search (&optional query oldest-first target-thread target-line continuation)
- "Run \"notmuch search\" with the given `query' and display results.
+(put 'notmuch-search 'notmuch-doc "Search for messages.")
+(defun notmuch-search (&optional query oldest-first target-thread target-line no-display)
+ "Display threads matching QUERY in a notmuch-search buffer.
-If `query' is nil, it is read interactively from the minibuffer.
+If QUERY is nil, it is read interactively from the minibuffer.
Other optional parameters are used as follows:
- oldest-first: A Boolean controlling the sort order of returned threads
- target-thread: A thread ID (without the thread: prefix) that will be made
+ OLDEST-FIRST: A Boolean controlling the sort order of returned threads
+ TARGET-THREAD: A thread ID (without the thread: prefix) that will be made
current if it appears in the search results.
- target-line: The line number to move to if the target thread does not
- appear in the search results."
- (interactive)
+ TARGET-LINE: The line number to move to if the target thread does not
+ appear in the search results.
+ NO-DISPLAY: Do not try to foreground the search results buffer. If it is
+ already foregrounded i.e. displayed in a window, this has no
+ effect, meaning the buffer will remain visible.
+
+When called interactively, this will prompt for a query and use
+the configured default sort order."
+ (interactive
+ (list
+ ;; Prompt for a query
+ nil
+ ;; Use the default search order (if we're doing a search from a
+ ;; search buffer, ignore any buffer-local overrides)
+ (default-value 'notmuch-search-oldest-first)))
+
(let* ((query (or query (notmuch-read-query "Notmuch search: ")))
(buffer (get-buffer-create (notmuch-search-buffer-title query))))
- (switch-to-buffer buffer)
+ (if no-display
+ (set-buffer buffer)
+ (switch-to-buffer buffer))
(notmuch-search-mode)
;; Don't track undo information for this buffer
(set 'buffer-undo-list t)
(set 'notmuch-search-oldest-first oldest-first)
(set 'notmuch-search-target-thread target-thread)
(set 'notmuch-search-target-line target-line)
- (set 'notmuch-search-continuation continuation)
+ (notmuch-tag-clear-cache)
(let ((proc (get-buffer-process (current-buffer)))
(inhibit-read-only t))
(if proc
(save-excursion
(let ((proc (notmuch-start-notmuch
"notmuch-search" buffer #'notmuch-search-process-sentinel
- "search" "--format=sexp" "--format-version=1"
+ "search" "--format=sexp" "--format-version=2"
(if oldest-first
"--sort=oldest-first"
"--sort=newest-first")
(let ((target-line (line-number-at-pos))
(oldest-first notmuch-search-oldest-first)
(target-thread (notmuch-search-find-thread-id 'bare))
- (query notmuch-search-query-string)
- (continuation notmuch-search-continuation))
- (notmuch-kill-this-buffer)
- (notmuch-search query oldest-first target-thread target-line continuation)
+ (query notmuch-search-query-string))
+ (notmuch-bury-or-kill-this-buffer)
+ (notmuch-search query oldest-first target-thread target-line)
(goto-char (point-min))))
-(defcustom notmuch-poll-script nil
- "An external script to incorporate new mail into the notmuch database.
-
-This variable controls the action invoked by
-`notmuch-search-poll-and-refresh-view' and
-`notmuch-hello-poll-and-update' (each have a default keybinding
-of 'G') to incorporate new mail into the notmuch database.
-
-If set to nil (the default), new mail is processed by invoking
-\"notmuch new\". Otherwise, this should be set to a string that
-gives the name of an external script that processes new mail. If
-set to the empty string, no command will be run.
-
-The external script could do any of the following depending on
-the user's needs:
-
-1. Invoke a program to transfer mail to the local mail store
-2. Invoke \"notmuch new\" to incorporate the new mail
-3. Invoke one or more \"notmuch tag\" commands to classify the mail
-
-Note that the recommended way of achieving the same is using
-\"notmuch new\" hooks."
- :type '(choice (const :tag "notmuch new" nil)
- (const :tag "Disabled" "")
- (string :tag "Custom script"))
- :group 'notmuch-external)
-
-(defun notmuch-poll ()
- "Run \"notmuch new\" or an external script to import mail.
-
-Invokes `notmuch-poll-script', \"notmuch new\", or does nothing
-depending on the value of `notmuch-poll-script'."
- (interactive)
- (if (stringp notmuch-poll-script)
- (unless (string= notmuch-poll-script "")
- (call-process notmuch-poll-script nil nil))
- (call-process notmuch-command nil nil nil "new")))
-
-(defun notmuch-search-poll-and-refresh-view ()
- "Invoke `notmuch-poll' to import mail, then refresh the current view."
- (interactive)
- (notmuch-poll)
- (notmuch-search-refresh-view))
-
(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 filtered searches created by
-`notmuch-search-filter' retain the search order of the parent
-search."
+This command toggles the sort order for the current search. The
+default sort order is defined by `notmuch-search-oldest-first'."
(interactive)
(set 'notmuch-search-oldest-first (not notmuch-search-oldest-first))
(notmuch-search-refresh-view))
+(defun notmuch-group-disjunctive-query-string (query-string)
+ "Group query if it contains a complex expression.
+
+Enclose QUERY-STRING in parentheses if it matches
+`notmuch-search-disjunctive-regexp'."
+ (if (string-match-p notmuch-search-disjunctive-regexp query-string)
+ (concat "( " query-string " )")
+ query-string))
+
(defun notmuch-search-filter (query)
- "Filter the current search results based on an additional query string.
+ "Filter or LIMIT 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 (list (notmuch-read-query "Filter search: ")))
- (let ((grouped-query (if (string-match-p notmuch-search-disjunctive-regexp query)
- (concat "( " query " )")
- query)))
- (notmuch-search (if (string= notmuch-search-query-string "*")
+ (let ((grouped-query (notmuch-group-disjunctive-query-string query))
+ (grouped-original-query (notmuch-group-disjunctive-query-string
+ notmuch-search-query-string)))
+ (notmuch-search (if (string= grouped-original-query "*")
grouped-query
- (concat notmuch-search-query-string " and " grouped-query)) notmuch-search-oldest-first)))
+ (concat grouped-original-query " and " grouped-query))
+ notmuch-search-oldest-first)))
(defun notmuch-search-filter-by-tag (tag)
"Filter the current search results based on a single tag.
(setq mail-user-agent 'notmuch-user-agent)
(provide 'notmuch)
+
+;; After provide to avoid loops if notmuch was require'd via notmuch-init-file.
+(if init-file-user ; don't load init file if the -q option was used.
+ (let ((init-file (locate-file notmuch-init-file '("/")
+ (get-load-suffixes))))
+ (if init-file (load init-file nil t t))))
+
+;;; notmuch.el ends here