X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=blobdiff_plain;f=emacs%2Fnotmuch-hello.el;h=c17b46bc3ea2cbc5bfee680f62e2589da71a0a20;hp=c1c6f4b42186698f75fee9549ca1ab62daf58bd3;hb=e1a700067a22214f54064c281219fbbbef87de06;hpb=8bee3c417c44afdf04fda7a3495d598a99f6d9bc diff --git a/emacs/notmuch-hello.el b/emacs/notmuch-hello.el index c1c6f4b4..c17b46bc 100644 --- a/emacs/notmuch-hello.el +++ b/emacs/notmuch-hello.el @@ -1,4 +1,4 @@ -;; notmuch-hello.el --- welcome to notmuch, a frontend +;;; notmuch-hello.el --- welcome to notmuch, a frontend ;; ;; Copyright © David Edmondson ;; @@ -15,19 +15,145 @@ ;; General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License -;; along with Notmuch. If not, see . +;; along with Notmuch. If not, see . ;; ;; Authors: David Edmondson -(eval-when-compile (require 'cl)) +;;; Code: + +(eval-when-compile (require 'cl-lib)) + (require 'widget) (require 'wid-edit) ; For `widget-forward'. (require 'notmuch-lib) (require 'notmuch-mua) -(declare-function notmuch-search "notmuch" (&optional query oldest-first target-thread target-line continuation)) +(declare-function notmuch-search "notmuch" + (&optional query oldest-first target-thread target-line continuation)) (declare-function notmuch-poll "notmuch" ()) +(declare-function notmuch-tree "notmuch-tree" + (&optional query query-context target buffer-name open-target unthreaded)) +(declare-function notmuch-unthreaded + (&optional query query-context target buffer-name open-target)) + + +(defun notmuch-saved-search-get (saved-search field) + "Get FIELD from SAVED-SEARCH. + +If SAVED-SEARCH is a plist, this is just `plist-get', but for +backwards compatibility, this also deals with the two other +possible formats for SAVED-SEARCH: cons cells (NAME . QUERY) and +lists (NAME QUERY COUNT-QUERY)." + (cond + ((keywordp (car saved-search)) + (plist-get saved-search field)) + ;; It is not a plist so it is an old-style entry. + ((consp (cdr saved-search)) + (pcase-let ((`(,name ,query ,count-query) saved-search)) + (cl-case field + (:name name) + (:query query) + (:count-query count-query) + (t nil)))) + (t + (pcase-let ((`(,name . ,query) saved-search)) + (cl-case field + (:name name) + (:query query) + (t nil)))))) + +(defun notmuch-hello-saved-search-to-plist (saved-search) + "Return a copy of SAVED-SEARCH in plist form. + +If saved search is a plist then just return a copy. In other +cases, for backwards compatibility, convert to plist form and +return that." + (if (keywordp (car saved-search)) + (copy-sequence saved-search) + (let ((fields (list :name :query :count-query)) + plist-search) + (dolist (field fields plist-search) + (let ((string (notmuch-saved-search-get saved-search field))) + (when string + (setq plist-search (append plist-search (list field string))))))))) + +(defun notmuch-hello--saved-searches-to-plist (symbol) + "Extract a saved-search variable into plist form. + +The new style saved search is just a plist, but for backwards +compatibility we use this function to extract old style saved +searches so they still work in customize." + (let ((saved-searches (default-value symbol))) + (mapcar #'notmuch-hello-saved-search-to-plist saved-searches))) + +(define-widget 'notmuch-saved-search-plist 'list + "A single saved search property list." + :tag "Saved Search" + :args '((list :inline t + :format "%v" + (group :format "%v" :inline t + (const :format " Name: " :name) + (string :format "%v")) + (group :format "%v" :inline t + (const :format " Query: " :query) + (string :format "%v"))) + (checklist :inline t + :format "%v" + (group :format "%v" :inline t + (const :format "Shortcut key: " :key) + (key-sequence :format "%v")) + (group :format "%v" :inline t + (const :format "Count-Query: " :count-query) + (string :format "%v")) + (group :format "%v" :inline t + (const :format "" :sort-order) + (choice :tag " Sort Order" + (const :tag "Default" nil) + (const :tag "Oldest-first" oldest-first) + (const :tag "Newest-first" newest-first))) + (group :format "%v" :inline t + (const :format "" :search-type) + (choice :tag " Search Type" + (const :tag "Search mode" nil) + (const :tag "Tree mode" tree) + (const :tag "Unthreaded mode" unthreaded)))))) + +(defcustom notmuch-saved-searches + `((:name "inbox" :query "tag:inbox" :key ,(kbd "i")) + (:name "unread" :query "tag:unread" :key ,(kbd "u")) + (:name "flagged" :query "tag:flagged" :key ,(kbd "f")) + (:name "sent" :query "tag:sent" :key ,(kbd "t")) + (:name "drafts" :query "tag:draft" :key ,(kbd "d")) + (:name "all mail" :query "*" :key ,(kbd "a"))) + "A list of saved searches to display. + +The saved search can be given in 3 forms. The preferred way is as +a plist. Supported properties are + + :name Name of the search (required). + :query Search to run (required). + :key Optional shortcut key for `notmuch-jump-search'. + :count-query Optional extra query to generate the count + shown. If not present then the :query property + is used. + :sort-order Specify the sort order to be used for the search. + Possible values are 'oldest-first 'newest-first or + nil. Nil means use the default sort order. + :search-type Specify whether to run the search in search-mode, + tree mode or unthreaded mode. Set to 'tree to specify tree + mode, 'unthreaded to specify unthreaded mode, and set to nil + (or anything except tree and unthreaded) to specify search mode. + +Other accepted forms are a cons cell of the form (NAME . QUERY) +or a list of the form (NAME QUERY COUNT-QUERY)." + ;; The saved-search format is also used by the all-tags notmuch-hello + ;; section. This section generates its own saved-search list in one of + ;; the latter two forms. + :get 'notmuch-hello--saved-searches-to-plist + :type '(repeat notmuch-saved-search-plist) + :tag "List of Saved Searches" + :group 'notmuch-hello) (defcustom notmuch-hello-recent-searches-max 10 "The number of recent searches to display." @@ -39,9 +165,12 @@ :type 'boolean :group 'notmuch-hello) -(defun notmuch-sort-saved-searches (alist) - "Generate an alphabetically sorted saved searches alist." - (sort (copy-sequence alist) (lambda (a b) (string< (car a) (car b))))) +(defun notmuch-sort-saved-searches (saved-searches) + "Generate an alphabetically sorted saved searches list." + (sort (copy-sequence saved-searches) + (lambda (a b) + (string< (notmuch-saved-search-get a :name) + (notmuch-saved-search-get b :name))))) (defcustom notmuch-saved-search-sort-function nil "Function used to sort the saved searches for the notmuch-hello view. @@ -51,8 +180,10 @@ sorting (nil) displays the saved searches in the order they are stored in `notmuch-saved-searches'. Sort alphabetically sorts the saved searches in alphabetical order. Custom sort function should be a function or a lambda expression that takes the saved -searches alist as a parameter, and returns a new saved searches -alist to be used." +searches list as a parameter, and returns a new saved searches +list to be used. For compatibility with the various saved-search +formats it should use notmuch-saved-search-get to access the +fields of the search." :type '(choice (const :tag "No sorting" nil) (const :tag "Sort alphabetically" notmuch-sort-saved-searches) (function :tag "Custom sort function" @@ -151,7 +282,7 @@ International Bureau of Weights and Measures." :group 'notmuch-hello :group 'notmuch-hooks) -(defvar notmuch-hello-url "http://notmuchmail.org" +(defvar notmuch-hello-url "https://notmuchmail.org" "The `notmuch' web site.") (defvar notmuch-hello-custom-section-options @@ -232,8 +363,13 @@ supported for \"Customized queries section\" items." notmuch-hello-query-section (function :tag "Custom section")))) +(defcustom notmuch-hello-auto-refresh t + "Automatically refresh when returning to the notmuch-hello buffer." + :group 'notmuch-hello + :type 'boolean) + (defvar notmuch-hello-hidden-sections nil - "List of sections titles whose contents are hidden") + "List of sections titles whose contents are hidden.") (defvar notmuch-hello-first-run t "True if `notmuch-hello' is run for the first time, set to nil @@ -246,10 +382,10 @@ afterwards.") (setq n (/ n 1000))) (setq result (or result '(0))) (apply #'concat - (number-to-string (car result)) - (mapcar (lambda (elem) - (format "%s%03d" notmuch-hello-thousands-separator elem)) - (cdr result))))) + (number-to-string (car result)) + (mapcar (lambda (elem) + (format "%s%03d" notmuch-hello-thousands-separator elem)) + (cdr result))))) (defun notmuch-hello-trim (search) "Trim whitespace." @@ -258,13 +394,11 @@ afterwards.") search)) (defun notmuch-hello-search (&optional search) - (interactive) (unless (null search) (setq search (notmuch-hello-trim search)) (let ((history-delete-duplicates t)) (add-to-history 'notmuch-search-history search))) - (notmuch-search search notmuch-search-oldest-first nil nil - #'notmuch-hello-search-continuation)) + (notmuch-search search notmuch-search-oldest-first)) (defun notmuch-hello-add-saved-search (widget) (interactive) @@ -275,46 +409,63 @@ afterwards.") notmuch-saved-searches))) ;; If an existing saved search with this name exists, remove it. (setq notmuch-saved-searches - (loop for elem in notmuch-saved-searches - if (not (equal name - (car elem))) - collect elem)) + (cl-loop for elem in notmuch-saved-searches + if (not (equal name + (notmuch-saved-search-get elem :name))) + collect elem)) ;; Add the new one. (customize-save-variable 'notmuch-saved-searches (add-to-list 'notmuch-saved-searches - (cons name search) t)) + (list :name name :query search) t)) (message "Saved '%s' as '%s'." search name) (notmuch-hello-update))) +(defun notmuch-hello-delete-search-from-history (widget) + (interactive) + (let ((search (widget-value + (symbol-value + (widget-get widget :notmuch-saved-search-widget))))) + (setq notmuch-search-history (delete search + notmuch-search-history)) + (notmuch-hello-update))) + (defun notmuch-hello-longest-label (searches-alist) - (or (loop for elem in searches-alist - maximize (length (car elem))) + (or (cl-loop for elem in searches-alist + maximize (length (notmuch-saved-search-get elem :name))) 0)) (defun notmuch-hello-reflect-generate-row (ncols nrows row list) (let ((len (length list))) - (loop for col from 0 to (- ncols 1) - collect (let ((offset (+ (* nrows col) row))) - (if (< offset len) - (nth offset list) - ;; Don't forget to insert an empty slot in the - ;; output matrix if there is no corresponding - ;; value in the input matrix. - nil))))) + (cl-loop for col from 0 to (- ncols 1) + collect (let ((offset (+ (* nrows col) row))) + (if (< offset len) + (nth offset list) + ;; Don't forget to insert an empty slot in the + ;; output matrix if there is no corresponding + ;; value in the input matrix. + nil))))) (defun notmuch-hello-reflect (list ncols) "Reflect a `ncols' wide matrix represented by `list' along the diagonal." ;; Not very lispy... (let ((nrows (ceiling (length list) ncols))) - (loop for row from 0 to (- nrows 1) - append (notmuch-hello-reflect-generate-row ncols nrows row list)))) + (cl-loop for row from 0 to (- nrows 1) + append (notmuch-hello-reflect-generate-row ncols nrows row list)))) (defun notmuch-hello-widget-search (widget &rest ignore) - (notmuch-search (widget-get widget - :notmuch-search-terms) - notmuch-search-oldest-first - nil nil #'notmuch-hello-search-continuation)) + (cond + ((eq (widget-get widget :notmuch-search-type) 'tree) + (notmuch-tree (widget-get widget + :notmuch-search-terms))) + ((eq (widget-get widget :notmuch-search-type) 'unthreaded) + (notmuch-unthreaded (widget-get widget + :notmuch-search-terms))) + (t + (notmuch-search (widget-get widget + :notmuch-search-terms) + (widget-get widget + :notmuch-search-oldest-first))))) (defun notmuch-saved-search-count (search) (car (process-lines notmuch-command "count" search))) @@ -330,19 +481,17 @@ should be. Returns a cons cell `(tags-per-line width)'." ;; Count is 9 wide (8 digits plus space), 1 for the space ;; after the name. (+ 9 1 (max notmuch-column-control widest))))) - ((floatp notmuch-column-control) (let* ((available-width (- (window-width) notmuch-hello-indent)) - (proposed-width (max (* available-width notmuch-column-control) widest))) + (proposed-width (max (* available-width notmuch-column-control) + widest))) (floor available-width proposed-width))) - (t (max 1 (/ (- (window-width) notmuch-hello-indent) ;; Count is 9 wide (8 digits plus space), 1 for the space ;; after the name. (+ 9 1 widest))))))) - (cons tags-per-line (/ (max 1 (- (window-width) notmuch-hello-indent ;; Count is 9 wide (8 digits plus @@ -360,67 +509,71 @@ If FILTER is a function, it is called with QUERY as a parameter and the string it returns is used as the query. If nil is returned, the entry is hidden. -Otherwise, FILTER is ignored. -" +Otherwise, FILTER is ignored." (cond ((functionp filter) (funcall filter query)) ((stringp filter) (concat "(" query ") and (" filter ")")) (t query))) -(defun notmuch-hello-query-counts (query-alist &rest options) - "Compute list of counts of matched messages from QUERY-ALIST. +(defun notmuch-hello-query-counts (query-list &rest options) + "Compute list of counts of matched messages from QUERY-LIST. -QUERY-ALIST must be a list containing elements of the form (NAME . QUERY) -or (NAME QUERY COUNT-QUERY). If the latter form is used, -COUNT-QUERY specifies an alternate query to be used to generate -the count for the associated query. +QUERY-LIST must be a list of saved-searches. Ideally each of +these is a plist but other options are available for backwards +compatibility: see `notmuch-saved-searches' for details. -The result is the list of elements of the form (NAME QUERY COUNT). +The result is a list of plists each of which includes the +properties :name NAME, :query QUERY and :count COUNT, together +with any properties in the original saved-search. The values :show-empty-searches, :filter and :filter-count from options will be handled as specified for `notmuch-hello-insert-searches'." (with-temp-buffer - (dolist (elem query-alist nil) - (let ((count-query (if (consp (cdr elem)) - ;; do we have a different query for the message count? - (third elem) - (cdr elem)))) + (dolist (elem query-list nil) + (let ((count-query (or (notmuch-saved-search-get elem :count-query) + (notmuch-saved-search-get elem :query)))) (insert - (notmuch-hello-filtered-query count-query - (or (plist-get options :filter-count) - (plist-get options :filter))) + (replace-regexp-in-string + "\n" " " + (notmuch-hello-filtered-query count-query + (or (plist-get options :filter-count) + (plist-get options :filter)))) "\n"))) - - (call-process-region (point-min) (point-max) notmuch-command - t t nil "count" "--batch") + (unless (= (call-process-region (point-min) (point-max) notmuch-command + t t nil "count" "--batch") 0) + (notmuch-logged-error + "notmuch count --batch failed" + "Please check that the notmuch CLI is new enough to support `count +--batch'. In general we recommend running matching versions of +the CLI and emacs interface.")) (goto-char (point-min)) - (notmuch-remove-if-not #'identity (mapcar (lambda (elem) - (let ((name (car elem)) - (search-query (if (consp (cdr elem)) - ;; do we have a different query for the message count? - (second elem) - (cdr elem))) - (message-count (prog1 (read (current-buffer)) + (let* ((elem-plist (notmuch-hello-saved-search-to-plist elem)) + (search-query (plist-get elem-plist :query)) + (filtered-query (notmuch-hello-filtered-query + search-query (plist-get options :filter))) + (message-count (prog1 (read (current-buffer)) (forward-line 1)))) - (and (or (plist-get options :show-empty-searches) (> message-count 0)) - (list name (notmuch-hello-filtered-query - search-query (plist-get options :filter)) - message-count)))) - query-alist)))) + (when (and filtered-query (or (plist-get options :show-empty-searches) + (> message-count 0))) + (setq elem-plist (plist-put elem-plist :query filtered-query)) + (plist-put elem-plist :count message-count)))) + query-list)))) (defun notmuch-hello-insert-buttons (searches) "Insert buttons for SEARCHES. -SEARCHES must be a list containing lists of the form (NAME QUERY COUNT), where -QUERY is the query to start when the button for the corresponding entry is -activated. COUNT should be the number of messages matching the query. -Such a list can be computed with `notmuch-hello-query-counts'." +SEARCHES must be a list of plists each of which should contain at +least the properties :name NAME :query QUERY and :count COUNT, +where QUERY is the query to start when the button for the +corresponding entry is activated, and COUNT should be the number +of messages matching the query. Such a plist can be computed +with `notmuch-hello-query-counts'." (let* ((widest (notmuch-hello-longest-label searches)) (tags-and-width (notmuch-hello-tags-per-line widest)) (tags-per-line (car tags-and-width)) @@ -436,16 +589,23 @@ Such a list can be computed with `notmuch-hello-query-counts'." (mapc (lambda (elem) ;; (not elem) indicates an empty slot in the matrix. (when elem - (if (> column-indent 0) - (widget-insert (make-string column-indent ? ))) - (let* ((name (first elem)) - (query (second elem)) - (msg-count (third elem))) + (when (> column-indent 0) + (widget-insert (make-string column-indent ? ))) + (let* ((name (plist-get elem :name)) + (query (plist-get elem :query)) + (oldest-first (cl-case (plist-get elem :sort-order) + (newest-first nil) + (oldest-first t) + (otherwise notmuch-search-oldest-first))) + (search-type (plist-get elem :search-type)) + (msg-count (plist-get elem :count))) (widget-insert (format "%8s " (notmuch-hello-nice-number msg-count))) (widget-create 'push-button :notify #'notmuch-hello-widget-search :notmuch-search-terms query + :notmuch-search-oldest-first oldest-first + :notmuch-search-type search-type name) (setq column-indent (1+ (max 0 (- column-width (length name))))))) @@ -454,7 +614,6 @@ Such a list can be computed with `notmuch-hello-query-counts'." (setq column-indent 0) (widget-insert "\n"))) reordered-list) - ;; If the last line was not full (and hence did not include a ;; carriage return), insert one now. (unless (eq (% count tags-per-line) 0) @@ -462,52 +621,108 @@ Such a list can be computed with `notmuch-hello-query-counts'." (defimage notmuch-hello-logo ((:type png :file "notmuch-logo.png"))) -(defun notmuch-hello-search-continuation() - (notmuch-hello-update t)) - -(defun notmuch-hello-update (&optional no-display) - "Update the current notmuch view." +(defun notmuch-hello-update () + "Update the notmuch-hello buffer." ;; Lazy - rebuild everything. (interactive) - (notmuch-hello no-display)) - -(defun notmuch-hello-poll-and-update () - "Invoke `notmuch-poll' to import mail, then refresh the current view." + (notmuch-hello t)) + +(defun notmuch-hello-window-configuration-change () + "Hook function to update the hello buffer when it is switched to." + (let ((hello-buf (get-buffer "*notmuch-hello*")) + (do-refresh nil)) + ;; Consider all windows in the currently selected frame, since + ;; that's where the configuration change happened. This also + ;; refreshes our snapshot of all windows, so we have to do this + ;; even if we know we won't refresh (e.g., hello-buf is null). + (dolist (window (window-list)) + (let ((last-buf (window-parameter window 'notmuch-hello-last-buffer)) + (cur-buf (window-buffer window))) + (unless (eq last-buf cur-buf) + ;; This window changed or is new. Update recorded buffer + ;; for next time. + (set-window-parameter window 'notmuch-hello-last-buffer cur-buf) + (when (and (eq cur-buf hello-buf) last-buf) + ;; The user just switched to hello in this window (hello + ;; is currently visible, was not visible on the last + ;; configuration change, and this is not a new window) + (setq do-refresh t))))) + (when (and do-refresh notmuch-hello-auto-refresh) + ;; Refresh hello as soon as we get back to redisplay. On Emacs + ;; 24, we can't do it right here because something in this + ;; hook's call stack overrides hello's point placement. + (run-at-time nil nil #'notmuch-hello t)) + (unless hello-buf + ;; Clean up hook + (remove-hook 'window-configuration-change-hook + #'notmuch-hello-window-configuration-change)))) + +;; the following variable is defined as being defconst in notmuch-version.el +(defvar notmuch-emacs-version) + +(defun notmuch-hello-versions () + "Display the notmuch version(s)." (interactive) - (notmuch-poll) - (notmuch-hello-update)) - + (let ((notmuch-cli-version (notmuch-cli-version))) + (message "notmuch version %s" + (if (string= notmuch-emacs-version notmuch-cli-version) + notmuch-cli-version + (concat notmuch-cli-version + " (emacs mua version " notmuch-emacs-version ")"))))) (defvar notmuch-hello-mode-map - (let ((map (make-sparse-keymap))) - (set-keymap-parent map widget-keymap) - (define-key map "v" (lambda () "Display the notmuch version" (interactive) - (message "notmuch version %s" (notmuch-version)))) - (define-key map "?" 'notmuch-help) - (define-key map "q" 'notmuch-kill-this-buffer) - (define-key map "=" 'notmuch-hello-update) - (define-key map "G" 'notmuch-hello-poll-and-update) + (let ((map (if (fboundp 'make-composed-keymap) + ;; Inherit both widget-keymap and + ;; notmuch-common-keymap. We have to use + ;; make-sparse-keymap to force this to be a new + ;; keymap (so that when we modify map it does not + ;; modify widget-keymap). + (make-composed-keymap (list (make-sparse-keymap) widget-keymap)) + ;; Before Emacs 24, keymaps didn't support multiple + ;; inheritance,, so just copy the widget keymap since + ;; it's unlikely to change. + (copy-keymap widget-keymap)))) + (set-keymap-parent map notmuch-common-keymap) + (define-key map "v" 'notmuch-hello-versions) (define-key map (kbd "") 'widget-backward) - (define-key map "m" 'notmuch-mua-new-mail) - (define-key map "s" 'notmuch-hello-search) map) "Keymap for \"notmuch hello\" buffers.") (fset 'notmuch-hello-mode-map notmuch-hello-mode-map) -(defun notmuch-hello-mode () - "Major mode for convenient notmuch navigation. This is your entry portal into notmuch. +(define-derived-mode notmuch-hello-mode fundamental-mode "notmuch-hello" + "Major mode for convenient notmuch navigation. This is your entry portal into notmuch. + +Saved searches are \"bookmarks\" for arbitrary queries. Hit RET +or click on a saved search to view matching threads. Edit saved +searches with the `edit' button. Type `\\[notmuch-jump-search]' +in any Notmuch screen for quick access to saved searches that +have shortcut keys. + +Type new searches in the search box and hit RET to view matching +threads. Hit RET in a recent search box to re-submit a previous +search. Edit it first if you like. Save a recent search to saved +searches with the `save' button. + +Hit `\\[notmuch-search]' or `\\[notmuch-tree]' in any Notmuch +screen to search for messages and view matching threads or +messages, respectively. Recent searches are available in the +minibuffer history. + +Expand the all tags view with the `show' button (and collapse +again with the `hide' button). Hit RET or click on a tag name to +view matching threads. + +Hit `\\[notmuch-refresh-this-buffer]' to refresh the screen and +`\\[notmuch-bury-or-kill-this-buffer]' to quit. + +The screen may be customized via `\\[customize]'. Complete list of currently available key bindings: \\{notmuch-hello-mode-map}" - (interactive) - (kill-all-local-variables) - (use-local-map notmuch-hello-mode-map) - (setq major-mode 'notmuch-hello-mode - mode-name "notmuch-hello") - (run-mode-hooks 'notmuch-hello-mode-hook) - ;;(setq buffer-read-only t) -) + (setq notmuch-buffer-refresh-function #'notmuch-hello-update) + ;;(setq buffer-read-only t) + ) (defun notmuch-hello-generate-tag-alist (&optional hide-tags) "Return an alist from tags to queries to display in the all-tags section." @@ -531,7 +746,9 @@ Complete list of currently available key bindings: ;; dark background. (setq image (cons 'image (append (cdr image) - (list :background (face-background 'notmuch-hello-logo-background))))) + (list :background + (face-background + 'notmuch-hello-logo-background))))) (insert-image image)) (widget-insert " ")) @@ -551,10 +768,10 @@ Complete list of currently available key bindings: (notmuch-hello-update)) :help-echo "Refresh" (notmuch-hello-nice-number - (string-to-number (car (process-lines notmuch-command "count"))))) + (string-to-number + (car (process-lines notmuch-command "count"))))) (widget-insert " messages.\n"))) - (defun notmuch-hello-insert-saved-searches () "Insert the saved-searches section." (let ((searches (notmuch-hello-query-counts @@ -599,51 +816,66 @@ Complete list of currently available key bindings: (widget-insert "Recent searches: ") (widget-create 'push-button :notify (lambda (&rest ignore) - (setq notmuch-search-history nil) - (notmuch-hello-update)) + (when (y-or-n-p "Are you sure you want to clear the searches? ") + (setq notmuch-search-history nil) + (notmuch-hello-update))) "clear") (widget-insert "\n\n") (let ((start (point))) - (loop for i from 1 to notmuch-hello-recent-searches-max - for search in notmuch-search-history do - (let ((widget-symbol (intern (format "notmuch-hello-search-%d" i)))) - (set widget-symbol - (widget-create 'editable-field - ;; Don't let the search boxes be - ;; less than 8 characters wide. - :size (max 8 - (- (window-width) - ;; Leave some space - ;; at the start and - ;; end of the - ;; boxes. - (* 2 notmuch-hello-indent) - ;; 1 for the space - ;; before the - ;; `[save]' button. 6 - ;; for the `[save]' - ;; button. - 1 6)) - :action (lambda (widget &rest ignore) - (notmuch-hello-search (widget-value widget))) - search)) - (widget-insert " ") - (widget-create 'push-button - :notify (lambda (widget &rest ignore) - (notmuch-hello-add-saved-search widget)) - :notmuch-saved-search-widget widget-symbol - "save")) - (widget-insert "\n")) + (cl-loop for i from 1 to notmuch-hello-recent-searches-max + for search in notmuch-search-history do + (let ((widget-symbol (intern (format "notmuch-hello-search-%d" i)))) + (set widget-symbol + (widget-create 'editable-field + ;; Don't let the search boxes be + ;; less than 8 characters wide. + :size (max 8 + (- (window-width) + ;; Leave some space + ;; at the start and + ;; end of the + ;; boxes. + (* 2 notmuch-hello-indent) + ;; 1 for the space + ;; before the + ;; `[save]' button. 6 + ;; for the `[save]' + ;; button. + 1 6 + ;; 1 for the space + ;; before the `[del]' + ;; button. 5 for the + ;; `[del]' button. + 1 5)) + :action (lambda (widget &rest ignore) + (notmuch-hello-search (widget-value widget))) + search)) + (widget-insert " ") + (widget-create 'push-button + :notify (lambda (widget &rest ignore) + (notmuch-hello-add-saved-search widget)) + :notmuch-saved-search-widget widget-symbol + "save") + (widget-insert " ") + (widget-create 'push-button + :notify (lambda (widget &rest ignore) + (when (y-or-n-p "Are you sure you want to delete this search? ") + (notmuch-hello-delete-search-from-history widget))) + :notmuch-saved-search-widget widget-symbol + "del")) + (widget-insert "\n")) (indent-rigidly start (point) notmuch-hello-indent)) nil)) -(defun notmuch-hello-insert-searches (title query-alist &rest options) - "Insert a section with TITLE showing a list of buttons made from QUERY-ALIST. +(defun notmuch-hello-insert-searches (title query-list &rest options) + "Insert a section with TITLE showing a list of buttons made from QUERY-LIST. -QUERY-ALIST must be a list containing elements of the form (NAME . QUERY) -or (NAME QUERY COUNT-QUERY). If the latter form is used, -COUNT-QUERY specifies an alternate query to be used to generate -the count for the associated item. +QUERY-LIST should ideally be a plist but for backwards +compatibility other forms are also accepted (see +`notmuch-saved-searches' for details). The plist should +contain keys :name and :query; if :count-query is also present +then it specifies an alternate query to be used to generate the +count for the associated search. Supports the following entries in OPTIONS as a plist: :initially-hidden - if non-nil, section will be hidden on startup @@ -651,15 +883,15 @@ Supports the following entries in OPTIONS as a plist: :hide-if-empty - hide if no buttons would be shown (only makes sense without :show-empty-searches) :filter - This can be a function that takes the search query as its argument and - returns a filter to be used in conjuction with the query for that search or nil + returns a filter to be used in conjunction with the query for that search or nil to hide the element. This can also be a string that is used as a combined with each query using \"and\". :filter-count - Separate filter to generate the count displayed each search. Accepts the same values as :filter. If :filter and :filter-count are specified, this will be used instead of :filter, not in conjunction with it." (widget-insert title ": ") - (if (and notmuch-hello-first-run (plist-get options :initially-hidden)) - (add-to-list 'notmuch-hello-hidden-sections title)) + (when (and notmuch-hello-first-run (plist-get options :initially-hidden)) + (add-to-list 'notmuch-hello-hidden-sections title)) (let ((is-hidden (member title notmuch-hello-hidden-sections)) (start (point))) (if is-hidden @@ -676,8 +908,8 @@ Supports the following entries in OPTIONS as a plist: (notmuch-hello-update)) "hide")) (widget-insert "\n") - (when (not is-hidden) - (let ((searches (apply 'notmuch-hello-query-counts query-alist options))) + (unless is-hidden + (let ((searches (apply 'notmuch-hello-query-counts query-list options))) (when (or (not (plist-get options :hide-if-empty)) searches) (widget-insert "\n") @@ -698,7 +930,7 @@ following: options)) (defun notmuch-hello-insert-inbox () - "Show an entry for each saved search and inboxed messages for each tag" + "Show an entry for each saved search and inboxed messages for each tag." (notmuch-hello-insert-searches "What's in your inbox" (append notmuch-saved-searches @@ -706,7 +938,7 @@ following: :filter "tag:inbox")) (defun notmuch-hello-insert-alltags () - "Insert a section displaying all tags and associated message counts" + "Insert a section displaying all tags and associated message counts." (notmuch-hello-insert-tags-section nil :initially-hidden (not notmuch-show-all-tags-list) @@ -716,20 +948,19 @@ following: (defun notmuch-hello-insert-footer () "Insert the notmuch-hello footer." (let ((start (point))) - (widget-insert "Type a search query and hit RET to view matching threads.\n") - (when notmuch-search-history - (widget-insert "Hit RET to re-submit a previous search. Edit it first if you like.\n") - (widget-insert "Save recent searches with the `save' button.\n")) - (when notmuch-saved-searches - (widget-insert "Edit saved searches with the `edit' button.\n")) - (widget-insert "Hit RET or click on a saved search or tag name to view matching threads.\n") - (widget-insert "`=' to refresh this screen. `s' to search messages. `q' to quit.\n") + (widget-insert "Hit `?' for context-sensitive help in any Notmuch screen.\n") + (widget-insert "Customize ") + (widget-create 'link + :notify (lambda (&rest ignore) + (customize-group 'notmuch)) + :button-prefix "" :button-suffix "" + "Notmuch") + (widget-insert " or ") (widget-create 'link :notify (lambda (&rest ignore) (customize-variable 'notmuch-hello-sections)) :button-prefix "" :button-suffix "" - "Customize") - (widget-insert " this page.") + "this page.") (let ((fill-column (- (window-width) notmuch-hello-indent))) (center-region start (point))))) @@ -737,31 +968,32 @@ following: (defun notmuch-hello (&optional no-display) "Run notmuch and display saved searches, known tags, etc." (interactive) - - (if no-display - (set-buffer "*notmuch-hello*") - (switch-to-buffer "*notmuch-hello*")) - + (notmuch-assert-cli-sane) + ;; This may cause a window configuration change, so if the + ;; auto-refresh hook is already installed, avoid recursive refresh. + (let ((notmuch-hello-auto-refresh nil)) + (if no-display + (set-buffer "*notmuch-hello*") + (switch-to-buffer "*notmuch-hello*"))) + ;; Install auto-refresh hook + (when notmuch-hello-auto-refresh + (add-hook 'window-configuration-change-hook + #'notmuch-hello-window-configuration-change)) (let ((target-line (line-number-at-pos)) (target-column (current-column)) (inhibit-read-only t)) - ;; Delete all editable widget fields. Editable widget fields are ;; tracked in a buffer local variable `widget-field-list' (and ;; others). If we do `erase-buffer' without properly deleting the ;; widgets, some widget-related functions are confused later. (mapc 'widget-delete widget-field-list) - (erase-buffer) - (unless (eq major-mode 'notmuch-hello-mode) (notmuch-hello-mode)) - (let ((all (overlay-lists))) ;; Delete all the overlays. (mapc 'delete-overlay (car all)) (mapc 'delete-overlay (cdr all))) - (mapc (lambda (section) (let ((point-before (point))) @@ -774,7 +1006,6 @@ following: (widget-insert "\n")))) notmuch-hello-sections) (widget-setup) - ;; Move point back to where it was before refresh. Use line and ;; column instead of point directly to be insensitive to additions ;; and removals of text within earlier lines. @@ -792,3 +1023,5 @@ following: ;; (provide 'notmuch-hello) + +;;; notmuch-hello.el ends here