config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

notmuch-hello.el (38483B)


      1 ;;; notmuch-hello.el --- welcome to notmuch, a frontend  -*- lexical-binding: t -*-
      2 ;;
      3 ;; Copyright © David Edmondson
      4 ;;
      5 ;; This file is part of Notmuch.
      6 ;;
      7 ;; Notmuch is free software: you can redistribute it and/or modify it
      8 ;; under the terms of the GNU General Public License as published by
      9 ;; the Free Software Foundation, either version 3 of the License, or
     10 ;; (at your option) any later version.
     11 ;;
     12 ;; Notmuch is distributed in the hope that it will be useful, but
     13 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
     14 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     15 ;; General Public License for more details.
     16 ;;
     17 ;; You should have received a copy of the GNU General Public License
     18 ;; along with Notmuch.  If not, see <https://www.gnu.org/licenses/>.
     19 ;;
     20 ;; Authors: David Edmondson <dme@dme.org>
     21 
     22 ;;; Code:
     23 
     24 (require 'widget)
     25 (require 'wid-edit) ; For `widget-forward'.
     26 
     27 (require 'notmuch-lib)
     28 (require 'notmuch-mua)
     29 
     30 (declare-function notmuch-search "notmuch"
     31 		  (&optional query oldest-first target-thread target-line
     32 			     no-display))
     33 (declare-function notmuch-poll "notmuch-lib" ())
     34 (declare-function notmuch-tree "notmuch-tree"
     35 		  (&optional query query-context target buffer-name
     36 			     open-target unthreaded parent-buffer oldest-first))
     37 (declare-function notmuch-unthreaded "notmuch-tree"
     38 		  (&optional query query-context target buffer-name
     39 			     open-target))
     40 
     41 
     42 ;;; Options
     43 
     44 (defun notmuch-saved-search-get (saved-search field)
     45   "Get FIELD from SAVED-SEARCH.
     46 
     47 If SAVED-SEARCH is a plist, this is just `plist-get', but for
     48 backwards compatibility, this also deals with the two other
     49 possible formats for SAVED-SEARCH: cons cells (NAME . QUERY) and
     50 lists (NAME QUERY COUNT-QUERY)."
     51   (cond
     52    ((keywordp (car saved-search))
     53     (plist-get saved-search field))
     54    ;; It is not a plist so it is an old-style entry.
     55    ((consp (cdr saved-search))
     56     (pcase-let ((`(,name ,query ,count-query) saved-search))
     57       (cl-case field
     58 	(:name name)
     59 	(:query query)
     60 	(:count-query count-query)
     61 	(t nil))))
     62    (t
     63     (pcase-let ((`(,name . ,query) saved-search))
     64       (cl-case field
     65 	(:name name)
     66 	(:query query)
     67 	(t nil))))))
     68 
     69 (defun notmuch-hello-saved-search-to-plist (saved-search)
     70   "Return a copy of SAVED-SEARCH in plist form.
     71 
     72 If saved search is a plist then just return a copy. In other
     73 cases, for backwards compatibility, convert to plist form and
     74 return that."
     75   (if (keywordp (car saved-search))
     76       (copy-sequence saved-search)
     77     (let ((fields (list :name :query :count-query))
     78 	  plist-search)
     79       (dolist (field fields plist-search)
     80 	(let ((string (notmuch-saved-search-get saved-search field)))
     81 	  (when string
     82 	    (setq plist-search (append plist-search (list field string)))))))))
     83 
     84 (defun notmuch-hello--saved-searches-to-plist (symbol)
     85   "Extract a saved-search variable into plist form.
     86 
     87 The new style saved search is just a plist, but for backwards
     88 compatibility we use this function to extract old style saved
     89 searches so they still work in customize."
     90   (let ((saved-searches (default-value symbol)))
     91     (mapcar #'notmuch-hello-saved-search-to-plist saved-searches)))
     92 
     93 (define-widget 'notmuch-saved-search-plist 'list
     94   "A single saved search property list."
     95   :tag "Saved Search"
     96   :args '((list :inline t
     97 		:format "%v"
     98 		(group :format "%v" :inline t
     99 		       (const :format "   Name: " :name)
    100 		       (string :format "%v"))
    101 		(group :format "%v" :inline t
    102 		       (const :format "  Query: " :query)
    103 		       (string :format "%v")))
    104 	  (checklist :inline t
    105 		     :format "%v"
    106 		     (group :format "%v" :inline t
    107 			    (const :format "Shortcut key: " :key)
    108 			    (key-sequence :format "%v"))
    109 		     (group :format "%v" :inline t
    110 			    (const :format "Count-Query: " :count-query)
    111 			    (string :format "%v"))
    112 		     (group :format "%v" :inline t
    113 			    (const :format "" :sort-order)
    114 			    (choice :tag " Sort Order"
    115 				    (const :tag "Default" nil)
    116 				    (const :tag "Oldest-first" oldest-first)
    117 				    (const :tag "Newest-first" newest-first)))
    118 		     (group :format "%v" :inline t
    119 			    (const :format "" :search-type)
    120 			    (choice :tag " Search Type"
    121 				    (const :tag "Search mode" nil)
    122 				    (const :tag "Tree mode" tree)
    123 				    (const :tag "Unthreaded mode" unthreaded))))))
    124 
    125 (defcustom notmuch-saved-searches
    126   `((:name "inbox" :query "tag:inbox" :key ,(kbd "i"))
    127     (:name "unread" :query "tag:unread" :key ,(kbd "u"))
    128     (:name "flagged" :query "tag:flagged" :key ,(kbd "f"))
    129     (:name "sent" :query "tag:sent" :key ,(kbd "t"))
    130     (:name "drafts" :query "tag:draft" :key ,(kbd "d"))
    131     (:name "all mail" :query "*" :key ,(kbd "a")))
    132   "A list of saved searches to display.
    133 
    134 The saved search can be given in 3 forms. The preferred way is as
    135 a plist. Supported properties are
    136 
    137   :name            Name of the search (required).
    138   :query           Search to run (required).
    139   :key             Optional shortcut key for `notmuch-jump-search'.
    140   :count-query     Optional extra query to generate the count
    141                    shown. If not present then the :query property
    142                    is used.
    143   :sort-order      Specify the sort order to be used for the search.
    144                    Possible values are `oldest-first', `newest-first'
    145                    or nil. Nil means use the default sort order.
    146   :search-type     Specify whether to run the search in search-mode,
    147                    tree mode or unthreaded mode. Set to `tree' to
    148                    specify tree mode, \\='unthreaded to specify
    149                    unthreaded mode, and set to nil (or anything
    150                    except tree and unthreaded) to specify search
    151                    mode.
    152 
    153 Other accepted forms are a cons cell of the form (NAME . QUERY)
    154 or a list of the form (NAME QUERY COUNT-QUERY)."
    155   ;; The saved-search format is also used by the all-tags notmuch-hello
    156   ;; section. This section generates its own saved-search list in one of
    157   ;; the latter two forms.
    158   :get 'notmuch-hello--saved-searches-to-plist
    159   :type '(repeat notmuch-saved-search-plist)
    160   :tag "List of Saved Searches"
    161   :group 'notmuch-hello)
    162 
    163 (defcustom notmuch-hello-recent-searches-max 10
    164   "The number of recent searches to display."
    165   :type 'integer
    166   :group 'notmuch-hello)
    167 
    168 (defcustom notmuch-show-empty-saved-searches nil
    169   "Should saved searches with no messages be listed?"
    170   :type 'boolean
    171   :group 'notmuch-hello)
    172 
    173 (defun notmuch-sort-saved-searches (saved-searches)
    174   "Generate an alphabetically sorted saved searches list."
    175   (sort (copy-sequence saved-searches)
    176 	(lambda (a b)
    177 	  (string< (notmuch-saved-search-get a :name)
    178 		   (notmuch-saved-search-get b :name)))))
    179 
    180 (defcustom notmuch-saved-search-sort-function nil
    181   "Function used to sort the saved searches for the notmuch-hello view.
    182 
    183 This variable controls how saved searches should be sorted. No
    184 sorting (nil) displays the saved searches in the order they are
    185 stored in `notmuch-saved-searches'. Sort alphabetically sorts the
    186 saved searches in alphabetical order. Custom sort function should
    187 be a function or a lambda expression that takes the saved
    188 searches list as a parameter, and returns a new saved searches
    189 list to be used. For compatibility with the various saved-search
    190 formats it should use notmuch-saved-search-get to access the
    191 fields of the search."
    192   :type '(choice (const :tag "No sorting" nil)
    193 		 (const :tag "Sort alphabetically" notmuch-sort-saved-searches)
    194 		 (function :tag "Custom sort function"
    195 			   :value notmuch-sort-saved-searches))
    196   :group 'notmuch-hello)
    197 
    198 (defvar notmuch-hello-indent 4
    199   "How much to indent non-headers.")
    200 
    201 (defimage notmuch-hello-logo ((:type svg :file "notmuch-logo.svg")))
    202 
    203 (defcustom notmuch-show-logo t
    204   "Should the notmuch logo be shown?"
    205   :type 'boolean
    206   :group 'notmuch-hello)
    207 
    208 (defcustom notmuch-show-all-tags-list nil
    209   "Should all tags be shown in the notmuch-hello view?"
    210   :type 'boolean
    211   :group 'notmuch-hello)
    212 
    213 (defcustom notmuch-hello-tag-list-make-query nil
    214   "Function or string to generate queries for the all tags list.
    215 
    216 This variable controls which query results are shown for each tag
    217 in the \"all tags\" list. If nil, it will use all messages with
    218 that tag. If this is set to a string, it is used as a filter for
    219 messages having that tag (equivalent to \"tag:TAG and (THIS-VARIABLE)\").
    220 Finally this can be a function that will be called for each tag and
    221 should return a filter for that tag, or nil to hide the tag."
    222   :type '(choice (const :tag "All messages" nil)
    223 		 (const :tag "Unread messages" "tag:unread")
    224 		 (string :tag "Custom filter"
    225 			 :value "tag:unread")
    226 		 (function :tag "Custom filter function"))
    227   :group 'notmuch-hello)
    228 
    229 (defcustom notmuch-hello-hide-tags nil
    230   "List of tags to be hidden in the \"all tags\"-section."
    231   :type '(repeat string)
    232   :group 'notmuch-hello)
    233 
    234 (defface notmuch-hello-logo-background
    235   '((((class color)
    236       (background dark))
    237      (:background "#5f5f5f"))
    238     (((class color)
    239       (background light))
    240      (:background "white")))
    241   "Background colour for the notmuch logo."
    242   :group 'notmuch-hello
    243   :group 'notmuch-faces)
    244 
    245 (defcustom notmuch-column-control t
    246   "Controls the number of columns for saved searches/tags in notmuch view.
    247 
    248 This variable has three potential sets of values:
    249 
    250 - t: automatically calculate the number of columns possible based
    251   on the tags to be shown and the window width,
    252 - an integer: a lower bound on the number of characters that will
    253   be used to display each column,
    254 - a float: a fraction of the window width that is the lower bound
    255   on the number of characters that should be used for each
    256   column.
    257 
    258 So:
    259 - if you would like two columns of tags, set this to 0.5.
    260 - if you would like a single column of tags, set this to 1.0.
    261 - if you would like tags to be 30 characters wide, set this to
    262   30.
    263 - if you don't want to worry about all of this nonsense, leave
    264   this set to `t'."
    265   :type '(choice
    266 	  (const :tag "Automatically calculated" t)
    267 	  (integer :tag "Number of characters")
    268 	  (float :tag "Fraction of window"))
    269   :group 'notmuch-hello)
    270 
    271 (defcustom notmuch-hello-thousands-separator " "
    272   "The string used as a thousands separator.
    273 
    274 Typically \",\" in the US and UK and \".\" or \" \" in Europe.
    275 The latter is recommended in the SI/ISO 31-0 standard and by the
    276 International Bureau of Weights and Measures."
    277   :type 'string
    278   :group 'notmuch-hello)
    279 
    280 (defcustom notmuch-hello-mode-hook nil
    281   "Functions called after entering `notmuch-hello-mode'."
    282   :type 'hook
    283   :group 'notmuch-hello
    284   :group 'notmuch-hooks)
    285 
    286 (defcustom notmuch-hello-refresh-hook nil
    287   "Functions called after updating a `notmuch-hello' buffer."
    288   :type 'hook
    289   :group 'notmuch-hello
    290   :group 'notmuch-hooks)
    291 
    292 (defconst notmuch-hello-url "https://notmuchmail.org"
    293   "The `notmuch' web site.")
    294 
    295 (defvar notmuch-hello-custom-section-options
    296   '((:filter (string :tag "Filter for each tag"))
    297     (:filter-count (string :tag "Different filter to generate message counts"))
    298     (:initially-hidden (const :tag "Hide this section on startup" t))
    299     (:show-empty-searches (const :tag "Show queries with no matching messages" t))
    300     (:hide-if-empty (const :tag "Hide this section if all queries are empty
    301 \(and not shown by show-empty-searches)" t)))
    302   "Various customization-options for notmuch-hello-tags/query-section.")
    303 
    304 (define-widget 'notmuch-hello-tags-section 'lazy
    305   "Customize-type for notmuch-hello tag-list sections."
    306   :tag "Customized tag-list section (see docstring for details)"
    307   :type
    308   `(list :tag ""
    309 	 (const :tag "" notmuch-hello-insert-tags-section)
    310 	 (string :tag "Title for this section")
    311 	 (plist
    312 	  :inline t
    313 	  :options
    314 	  ,(append notmuch-hello-custom-section-options
    315 		   '((:hide-tags (repeat :tag "Tags that will be hidden"
    316 					 string)))))))
    317 
    318 (define-widget 'notmuch-hello-query-section 'lazy
    319   "Customize-type for custom saved-search-like sections"
    320   :tag "Customized queries section (see docstring for details)"
    321   :type
    322   `(list :tag ""
    323 	 (const :tag "" notmuch-hello-insert-searches)
    324 	 (string :tag "Title for this section")
    325 	 (repeat :tag "Queries"
    326 		 (cons (string :tag "Name") (string :tag "Query")))
    327 	 (plist :inline t :options ,notmuch-hello-custom-section-options)))
    328 
    329 (defcustom notmuch-hello-sections
    330   (list #'notmuch-hello-insert-header
    331 	#'notmuch-hello-insert-saved-searches
    332 	#'notmuch-hello-insert-search
    333 	#'notmuch-hello-insert-recent-searches
    334 	#'notmuch-hello-insert-alltags
    335 	#'notmuch-hello-insert-footer)
    336   "Sections for notmuch-hello.
    337 
    338 The list contains functions which are used to construct sections in
    339 notmuch-hello buffer.  When notmuch-hello buffer is constructed,
    340 these functions are run in the order they appear in this list.  Each
    341 function produces a section simply by adding content to the current
    342 buffer.  A section should not end with an empty line, because a
    343 newline will be inserted after each section by `notmuch-hello'.
    344 
    345 Each function should take no arguments. The return value is
    346 ignored.
    347 
    348 For convenience an element can also be a list of the form (FUNC ARG1
    349 ARG2 .. ARGN) in which case FUNC will be applied to the rest of the
    350 list.
    351 
    352 A \"Customized tag-list section\" item in the customize-interface
    353 displays a list of all tags, optionally hiding some of them. It
    354 is also possible to filter the list of messages matching each tag
    355 by an additional filter query. Similarly, the count of messages
    356 displayed next to the buttons can be generated by applying a
    357 different filter to the tag query. These filters are also
    358 supported for \"Customized queries section\" items."
    359   :group 'notmuch-hello
    360   :type
    361   '(repeat
    362     (choice (function-item notmuch-hello-insert-header)
    363 	    (function-item notmuch-hello-insert-saved-searches)
    364 	    (function-item notmuch-hello-insert-search)
    365 	    (function-item notmuch-hello-insert-recent-searches)
    366 	    (function-item notmuch-hello-insert-alltags)
    367 	    (function-item notmuch-hello-insert-footer)
    368 	    (function-item notmuch-hello-insert-inbox)
    369 	    notmuch-hello-tags-section
    370 	    notmuch-hello-query-section
    371 	    (function :tag "Custom section"))))
    372 
    373 (defcustom notmuch-hello-auto-refresh t
    374   "Automatically refresh when returning to the notmuch-hello buffer."
    375   :group 'notmuch-hello
    376   :type 'boolean)
    377 
    378 ;;; Internal variables
    379 
    380 (defvar notmuch-hello-hidden-sections nil
    381   "List of sections titles whose contents are hidden.")
    382 
    383 (defvar notmuch-hello-first-run t
    384   "True if `notmuch-hello' is run for the first time, set to nil afterwards.")
    385 
    386 ;;; Widgets for inserters
    387 
    388 (define-widget 'notmuch-search-item 'item
    389   "A recent search."
    390   :format "%v\n"
    391   :value-create 'notmuch-search-item-value-create)
    392 
    393 (defun notmuch-search-item-value-create (widget)
    394   (let ((value (widget-get widget :value)))
    395     (widget-insert (make-string notmuch-hello-indent ?\s))
    396     (widget-create 'editable-field
    397 		   :size (widget-get widget :size)
    398 		   :parent widget
    399 		   :action #'notmuch-hello-search
    400 		   value)
    401     (widget-insert " ")
    402     (widget-create 'push-button
    403 		   :parent widget
    404 		   :notify #'notmuch-hello-add-saved-search
    405 		   "save")
    406     (widget-insert " ")
    407     (widget-create 'push-button
    408 		   :parent widget
    409 		   :notify #'notmuch-hello-delete-search-from-history
    410 		   "del")))
    411 
    412 (defun notmuch-search-item-field-width ()
    413   (max 8 ; Don't let the search boxes be less than 8 characters wide.
    414        (- (window-width)
    415 	  notmuch-hello-indent ; space at bol
    416 	  notmuch-hello-indent ; space at eol
    417 	  1    ; for the space before the [save] button
    418 	  6    ; for the [save] button
    419 	  1    ; for the space before the [del] button
    420 	  5))) ; for the [del] button
    421 
    422 ;;; Widget actions
    423 
    424 (defun notmuch-hello-search (widget &rest _event)
    425   (let ((search (widget-value widget)))
    426     (when search
    427       (setq search (string-trim search))
    428       (let ((history-delete-duplicates t))
    429 	(add-to-history 'notmuch-search-history search)))
    430     (notmuch-search search notmuch-search-oldest-first)))
    431 
    432 (defun notmuch-hello-add-saved-search (widget &rest _event)
    433   (let ((search (widget-value (widget-get widget :parent)))
    434 	(name (completing-read "Name for saved search: "
    435 			       notmuch-saved-searches)))
    436     ;; If an existing saved search with this name exists, remove it.
    437     (setq notmuch-saved-searches
    438 	  (cl-loop for elem in notmuch-saved-searches
    439 		   unless (equal name (notmuch-saved-search-get elem :name))
    440 		   collect elem))
    441     ;; Add the new one.
    442     (customize-save-variable 'notmuch-saved-searches
    443 			     (add-to-list 'notmuch-saved-searches
    444 					  (list :name name :query search) t))
    445     (message "Saved '%s' as '%s'." search name)
    446     (notmuch-hello-update)))
    447 
    448 (defun notmuch-hello-delete-search-from-history (widget &rest _event)
    449   (when (y-or-n-p "Are you sure you want to delete this search? ")
    450     (let ((search (widget-value (widget-get widget :parent))))
    451       (setq notmuch-search-history
    452 	    (delete search notmuch-search-history)))
    453     (notmuch-hello-update)))
    454 
    455 ;;; Button utilities
    456 
    457 ;; `notmuch-hello-query-counts', `notmuch-hello-nice-number' and
    458 ;; `notmuch-hello-insert-buttons' are used outside this section.
    459 ;; All other functions that are defined in this section are only
    460 ;; used by these two functions.
    461 
    462 (defun notmuch-hello-longest-label (searches-alist)
    463   (or (cl-loop for elem in searches-alist
    464 	       maximize (length (notmuch-saved-search-get elem :name)))
    465       0))
    466 
    467 (defun notmuch-hello-reflect-generate-row (ncols nrows row list)
    468   (let ((len (length list)))
    469     (cl-loop for col from 0 to (- ncols 1)
    470 	     collect (let ((offset (+ (* nrows col) row)))
    471 		       (if (< offset len)
    472 			   (nth offset list)
    473 			 ;; Don't forget to insert an empty slot in the
    474 			 ;; output matrix if there is no corresponding
    475 			 ;; value in the input matrix.
    476 			 nil)))))
    477 
    478 (defun notmuch-hello-reflect (list ncols)
    479   "Reflect a `ncols' wide matrix represented by `list' along the
    480 diagonal."
    481   ;; Not very lispy...
    482   (let ((nrows (ceiling (length list) ncols)))
    483     (cl-loop for row from 0 to (- nrows 1)
    484 	     append (notmuch-hello-reflect-generate-row ncols nrows row list))))
    485 
    486 (defun notmuch-hello-widget-search (widget &rest _ignore)
    487   (cl-case (widget-get widget :notmuch-search-type)
    488    (tree
    489     (let ((n (notmuch-search-format-buffer-name (widget-value widget) "tree" t)))
    490       (notmuch-tree (widget-get widget :notmuch-search-terms)
    491 		    nil nil n nil nil nil
    492 		    (widget-get widget :notmuch-search-oldest-first))))
    493    (unthreaded
    494     (let ((n (notmuch-search-format-buffer-name (widget-value widget)
    495 						"unthreaded" t)))
    496       (notmuch-unthreaded (widget-get widget :notmuch-search-terms) nil nil n)))
    497    (t
    498     (notmuch-search (widget-get widget :notmuch-search-terms)
    499 		    (widget-get widget :notmuch-search-oldest-first)))))
    500 
    501 (defun notmuch-saved-search-count (search)
    502   (car (notmuch--process-lines notmuch-command "count" search)))
    503 
    504 (defun notmuch-hello-tags-per-line (widest)
    505   "Determine how many tags to show per line and how wide they
    506 should be. Returns a cons cell `(tags-per-line width)'."
    507   (let ((tags-per-line
    508 	 (cond
    509 	  ((integerp notmuch-column-control)
    510 	   (max 1
    511 		(/ (- (window-width) notmuch-hello-indent)
    512 		   ;; Count is 9 wide (8 digits plus space), 1 for the space
    513 		   ;; after the name.
    514 		   (+ 9 1 (max notmuch-column-control widest)))))
    515 	  ((floatp notmuch-column-control)
    516 	   (let* ((available-width (- (window-width) notmuch-hello-indent))
    517 		  (proposed-width (max (* available-width notmuch-column-control)
    518 				       widest)))
    519 	     (floor available-width proposed-width)))
    520 	  (t
    521 	   (max 1
    522 		(/ (- (window-width) notmuch-hello-indent)
    523 		   ;; Count is 9 wide (8 digits plus space), 1 for the space
    524 		   ;; after the name.
    525 		   (+ 9 1 widest)))))))
    526     (cons tags-per-line (/ (max 1
    527 				(- (window-width) notmuch-hello-indent
    528 				   ;; Count is 9 wide (8 digits plus
    529 				   ;; space), 1 for the space after the
    530 				   ;; name.
    531 				   (* tags-per-line (+ 9 1))))
    532 			   tags-per-line))))
    533 
    534 (defun notmuch-hello-filtered-query (query filter)
    535   "Constructs a query to search all messages matching QUERY and FILTER.
    536 
    537 If FILTER is a string, it is directly used in the returned query.
    538 
    539 If FILTER is a function, it is called with QUERY as a parameter and
    540 the string it returns is used as the query. If nil is returned,
    541 the entry is hidden.
    542 
    543 Otherwise, FILTER is ignored."
    544   (cond
    545    ((functionp filter) (funcall filter query))
    546    ((stringp filter)
    547     (concat "(" query ") and (" filter ")"))
    548    (t query)))
    549 
    550 (defun notmuch-hello-query-counts (query-list &rest options)
    551   "Compute list of counts of matched messages from QUERY-LIST.
    552 
    553 QUERY-LIST must be a list of saved-searches. Ideally each of
    554 these is a plist but other options are available for backwards
    555 compatibility: see `notmuch-saved-searches' for details.
    556 
    557 The result is a list of plists each of which includes the
    558 properties :name NAME, :query QUERY and :count COUNT, together
    559 with any properties in the original saved-search.
    560 
    561 The values :show-empty-searches, :filter and :filter-count from
    562 options will be handled as specified for
    563 `notmuch-hello-insert-searches'. :disable-includes can be used to
    564 turn off the default exclude processing in `notmuch-count(1)'"
    565   (with-temp-buffer
    566     (dolist (elem query-list nil)
    567       (let ((count-query (or (notmuch-saved-search-get elem :count-query)
    568 			     (notmuch-saved-search-get elem :query))))
    569 	(insert
    570 	 (replace-regexp-in-string
    571 	  "\n" " "
    572 	  (notmuch-hello-filtered-query count-query
    573 					(or (plist-get options :filter-count)
    574 					    (plist-get options :filter))))
    575 	 "\n")))
    576     (unless (= (notmuch--call-process-region (point-min) (point-max) notmuch-command
    577 					     t t nil "count"
    578 					     (if (plist-get options :disable-excludes)
    579 						 "--exclude=false"
    580 					       "--exclude=true")
    581 					     "--batch") 0)
    582       (notmuch-logged-error
    583        "notmuch count --batch failed"
    584        "Please check that the notmuch CLI is new enough to support `count
    585 --batch'. In general we recommend running matching versions of
    586 the CLI and emacs interface."))
    587     (goto-char (point-min))
    588     (cl-mapcan
    589      (lambda (elem)
    590        (let* ((elem-plist (notmuch-hello-saved-search-to-plist elem))
    591 	      (search-query (plist-get elem-plist :query))
    592 	      (filtered-query (notmuch-hello-filtered-query
    593 			       search-query (plist-get options :filter)))
    594 	      (message-count (prog1 (read (current-buffer))
    595 			       (forward-line 1))))
    596 	 (when (and filtered-query (or (plist-get options :show-empty-searches)
    597 				       (> message-count 0)))
    598 	   (setq elem-plist (plist-put elem-plist :query filtered-query))
    599 	   (list (plist-put elem-plist :count message-count)))))
    600      query-list)))
    601 
    602 (defun notmuch-hello-nice-number (n)
    603   (let (result)
    604     (while (> n 0)
    605       (push (% n 1000) result)
    606       (setq n (/ n 1000)))
    607     (setq result (or result '(0)))
    608     (apply #'concat
    609 	   (number-to-string (car result))
    610 	   (mapcar (lambda (elem)
    611 		     (format "%s%03d" notmuch-hello-thousands-separator elem))
    612 		   (cdr result)))))
    613 
    614 (defun notmuch-hello-insert-buttons (searches)
    615   "Insert buttons for SEARCHES.
    616 
    617 SEARCHES must be a list of plists each of which should contain at
    618 least the properties :name NAME :query QUERY and :count COUNT,
    619 where QUERY is the query to start when the button for the
    620 corresponding entry is activated, and COUNT should be the number
    621 of messages matching the query.  Such a plist can be computed
    622 with `notmuch-hello-query-counts'."
    623   (let* ((widest (notmuch-hello-longest-label searches))
    624 	 (tags-and-width (notmuch-hello-tags-per-line widest))
    625 	 (tags-per-line (car tags-and-width))
    626 	 (column-width (cdr tags-and-width))
    627 	 (column-indent 0)
    628 	 (count 0)
    629 	 (reordered-list (notmuch-hello-reflect searches tags-per-line))
    630 	 ;; Hack the display of the buttons used.
    631 	 (widget-push-button-prefix "")
    632 	 (widget-push-button-suffix ""))
    633     ;; dme: It feels as though there should be a better way to
    634     ;; implement this loop than using an incrementing counter.
    635     (mapc (lambda (elem)
    636 	    ;; (not elem) indicates an empty slot in the matrix.
    637 	    (when elem
    638 	      (when (> column-indent 0)
    639 		(widget-insert (make-string column-indent ? )))
    640 	      (let* ((name (plist-get elem :name))
    641 		     (query (plist-get elem :query))
    642 		     (oldest-first (cl-case (plist-get elem :sort-order)
    643 				     (newest-first nil)
    644 				     (oldest-first t)
    645 				     (otherwise notmuch-search-oldest-first)))
    646 		     (search-type (plist-get elem :search-type))
    647 		     (msg-count (plist-get elem :count)))
    648 		(widget-insert (format "%8s "
    649 				       (notmuch-hello-nice-number msg-count)))
    650 		(widget-create 'push-button
    651 			       :notify #'notmuch-hello-widget-search
    652 			       :notmuch-search-terms query
    653 			       :notmuch-search-oldest-first oldest-first
    654 			       :notmuch-search-type search-type
    655 			       name)
    656 		(setq column-indent
    657 		      (1+ (max 0 (- column-width (length name)))))))
    658 	    (cl-incf count)
    659 	    (when (eq (% count tags-per-line) 0)
    660 	      (setq column-indent 0)
    661 	      (widget-insert "\n")))
    662 	  reordered-list)
    663     ;; If the last line was not full (and hence did not include a
    664     ;; carriage return), insert one now.
    665     (unless (eq (% count tags-per-line) 0)
    666       (widget-insert "\n"))))
    667 
    668 ;;; Mode
    669 
    670 (defun notmuch-hello-update ()
    671   "Update the notmuch-hello buffer."
    672   ;; Lazy - rebuild everything.
    673   (interactive)
    674   (notmuch-hello t))
    675 
    676 (defun notmuch-hello-window-configuration-change ()
    677   "Hook function to update the hello buffer when it is switched to."
    678   (let ((hello-buf (get-buffer "*notmuch-hello*"))
    679 	(do-refresh nil))
    680     ;; Consider all windows in the currently selected frame, since
    681     ;; that's where the configuration change happened.  This also
    682     ;; refreshes our snapshot of all windows, so we have to do this
    683     ;; even if we know we won't refresh (e.g., hello-buf is null).
    684     (dolist (window (window-list))
    685       (let ((last-buf (window-parameter window 'notmuch-hello-last-buffer))
    686 	    (cur-buf (window-buffer window)))
    687 	(unless (eq last-buf cur-buf)
    688 	  ;; This window changed or is new.  Update recorded buffer
    689 	  ;; for next time.
    690 	  (set-window-parameter window 'notmuch-hello-last-buffer cur-buf)
    691 	  (when (and (eq cur-buf hello-buf) last-buf)
    692 	    ;; The user just switched to hello in this window (hello
    693 	    ;; is currently visible, was not visible on the last
    694 	    ;; configuration change, and this is not a new window)
    695 	    (setq do-refresh t)))))
    696     (when (and do-refresh notmuch-hello-auto-refresh)
    697       ;; Refresh hello as soon as we get back to redisplay.  On Emacs
    698       ;; 24, we can't do it right here because something in this
    699       ;; hook's call stack overrides hello's point placement.
    700       ;; FIXME And on Emacs releases that we still support?
    701       (run-at-time nil nil #'notmuch-hello t))
    702     (unless hello-buf
    703       ;; Clean up hook
    704       (remove-hook 'window-configuration-change-hook
    705 		   #'notmuch-hello-window-configuration-change))))
    706 
    707 (defvar notmuch-hello-mode-map
    708   ;; Inherit both widget-keymap and notmuch-common-keymap.  We have
    709   ;; to use make-sparse-keymap to force this to be a new keymap (so
    710   ;; that when we modify map it does not modify widget-keymap).
    711   (let ((map (make-composed-keymap (list (make-sparse-keymap) widget-keymap))))
    712     (set-keymap-parent map notmuch-common-keymap)
    713     ;; Currently notmuch-hello-mode supports free text entry, but not
    714     ;; tagging operations, so provide standard undo.
    715     (define-key map [remap notmuch-tag-undo] #'undo)
    716     map)
    717   "Keymap for \"notmuch hello\" buffers.")
    718 
    719 (define-derived-mode notmuch-hello-mode fundamental-mode "notmuch-hello"
    720   "Major mode for convenient notmuch navigation. This is your entry
    721 portal into notmuch.
    722 
    723 Saved searches are \"bookmarks\" for arbitrary queries. Hit RET
    724 or click on a saved search to view matching threads. Edit saved
    725 searches with the `edit' button. Type `\\[notmuch-jump-search]'
    726 in any Notmuch screen for quick access to saved searches that
    727 have shortcut keys.
    728 
    729 Type new searches in the search box and hit RET to view matching
    730 threads. Hit RET in a recent search box to re-submit a previous
    731 search. Edit it first if you like. Save a recent search to saved
    732 searches with the `save' button.
    733 
    734 Hit `\\[notmuch-search]' or `\\[notmuch-tree]' in any Notmuch
    735 screen to search for messages and view matching threads or
    736 messages, respectively. Recent searches are available in the
    737 minibuffer history.
    738 
    739 Expand the all tags view with the `show' button (and collapse
    740 again with the `hide' button). Hit RET or click on a tag name to
    741 view matching threads.
    742 
    743 Hit `\\[notmuch-refresh-this-buffer]' to refresh the screen and
    744 `\\[notmuch-bury-or-kill-this-buffer]' to quit.
    745 
    746 The screen may be customized via `\\[customize]'.
    747 
    748 Complete list of currently available key bindings:
    749 
    750 \\{notmuch-hello-mode-map}"
    751   (setq notmuch-buffer-refresh-function #'notmuch-hello-update))
    752 
    753 ;;; Inserters
    754 
    755 (defun notmuch-hello-generate-tag-alist (&optional hide-tags)
    756   "Return an alist from tags to queries to display in the all-tags section."
    757   (cl-mapcan (lambda (tag)
    758 	       (and (not (member tag hide-tags))
    759 		    (list (cons tag
    760 				(concat "tag:"
    761 					(notmuch-escape-boolean-term tag))))))
    762 	     (notmuch--process-lines notmuch-command "search" "--output=tags" "*")))
    763 
    764 (defun notmuch-hello-insert-header ()
    765   "Insert the default notmuch-hello header."
    766   (when notmuch-show-logo
    767     (let ((image notmuch-hello-logo))
    768       ;; The notmuch logo uses transparency. That can display poorly
    769       ;; when inserting the image into an emacs buffer (black logo on
    770       ;; a black background), so force the background colour of the
    771       ;; image. We use a face to represent the colour so that
    772       ;; `defface' can be used to declare the different possible
    773       ;; colours, which depend on whether the frame has a light or
    774       ;; dark background.
    775       (setq image (cons 'image
    776 			(append (cdr image)
    777 				(list :background
    778 				      (face-background
    779 				       'notmuch-hello-logo-background)))))
    780       (insert-image image))
    781     (widget-insert "  "))
    782 
    783   (widget-insert "Welcome to ")
    784   ;; Hack the display of the links used.
    785   (let ((widget-link-prefix "")
    786 	(widget-link-suffix ""))
    787     (widget-create 'link
    788 		   :notify (lambda (&rest _ignore)
    789 			     (browse-url notmuch-hello-url))
    790 		   :help-echo "Visit the notmuch website."
    791 		   "notmuch")
    792     (widget-insert ". ")
    793     (widget-insert "You have ")
    794     (widget-create 'link
    795 		   :notify (lambda (&rest _ignore)
    796 			     (notmuch-hello-update))
    797 		   :help-echo "Refresh"
    798 		   (notmuch-hello-nice-number
    799 		    (string-to-number
    800 		     (car (notmuch--process-lines notmuch-command "count" "--exclude=false")))))
    801     (widget-insert " messages.\n")))
    802 
    803 (defun notmuch-hello-insert-saved-searches ()
    804   "Insert the saved-searches section."
    805   (let ((searches (notmuch-hello-query-counts
    806 		   (if notmuch-saved-search-sort-function
    807 		       (funcall notmuch-saved-search-sort-function
    808 				notmuch-saved-searches)
    809 		     notmuch-saved-searches)
    810 		   :show-empty-searches notmuch-show-empty-saved-searches)))
    811     (when searches
    812       (widget-insert "Saved searches: ")
    813       (widget-create 'push-button
    814 		     :notify (lambda (&rest _ignore)
    815 			       (customize-variable 'notmuch-saved-searches))
    816 		     "edit")
    817       (widget-insert "\n\n")
    818       (let ((start (point)))
    819 	(notmuch-hello-insert-buttons searches)
    820 	(indent-rigidly start (point) notmuch-hello-indent)))))
    821 
    822 (defun notmuch-hello-insert-search ()
    823   "Insert a search widget."
    824   (widget-insert "Search: ")
    825   (widget-create 'editable-field
    826 		 ;; Leave some space at the start and end of the
    827 		 ;; search boxes.
    828 		 :size (max 8 (- (window-width) notmuch-hello-indent
    829 				 (length "Search: ")))
    830 		 :action #'notmuch-hello-search)
    831   ;; Add an invisible dot to make `widget-end-of-line' ignore
    832   ;; trailing spaces in the search widget field.  A dot is used
    833   ;; instead of a space to make `show-trailing-whitespace'
    834   ;; happy, i.e. avoid it marking the whole line as trailing
    835   ;; spaces.
    836   (widget-insert (propertize "." 'invisible t))
    837   (widget-insert "\n"))
    838 
    839 (defun notmuch-hello-insert-recent-searches ()
    840   "Insert recent searches."
    841   (when notmuch-search-history
    842     (widget-insert "Recent searches: ")
    843     (widget-create
    844      'push-button
    845      :notify (lambda (&rest _ignore)
    846 	       (when (y-or-n-p "Are you sure you want to clear the searches? ")
    847 		 (setq notmuch-search-history nil)
    848 		 (notmuch-hello-update)))
    849      "clear")
    850     (widget-insert "\n\n")
    851     (let ((width (notmuch-search-item-field-width)))
    852       (dolist (search (seq-take notmuch-search-history
    853 				notmuch-hello-recent-searches-max))
    854 	(widget-create 'notmuch-search-item :value search :size width)))))
    855 
    856 (defun notmuch-hello-insert-searches (title query-list &rest options)
    857   "Insert a section with TITLE showing a list of buttons made from
    858 QUERY-LIST.
    859 
    860 QUERY-LIST should ideally be a plist but for backwards
    861 compatibility other forms are also accepted (see
    862 `notmuch-saved-searches' for details).  The plist should
    863 contain keys :name and :query; if :count-query is also present
    864 then it specifies an alternate query to be used to generate the
    865 count for the associated search.
    866 
    867 Supports the following entries in OPTIONS as a plist:
    868 :initially-hidden - if non-nil, section will be hidden on startup
    869 :show-empty-searches - show buttons with no matching messages
    870 :hide-if-empty - hide if no buttons would be shown
    871    (only makes sense without :show-empty-searches)
    872 :filter - This can be a function that takes the search query as
    873    its argument and returns a filter to be used in conjunction
    874    with the query for that search or nil to hide the
    875    element. This can also be a string that is used as a combined
    876    with each query using \"and\".
    877 :filter-count - Separate filter to generate the count displayed
    878    each search. Accepts the same values as :filter. If :filter
    879    and :filter-count are specified, this will be used instead of
    880    :filter, not in conjunction with it."
    881 
    882   (widget-insert title ": ")
    883   (when (and notmuch-hello-first-run (plist-get options :initially-hidden))
    884     (add-to-list 'notmuch-hello-hidden-sections title))
    885   (let ((is-hidden (member title notmuch-hello-hidden-sections))
    886 	(start (point)))
    887     (if is-hidden
    888 	(widget-create 'push-button
    889 		       :notify (lambda (&rest _ignore)
    890 				 (setq notmuch-hello-hidden-sections
    891 				       (delete title notmuch-hello-hidden-sections))
    892 				 (notmuch-hello-update))
    893 		       "show")
    894       (widget-create 'push-button
    895 		     :notify (lambda (&rest _ignore)
    896 			       (add-to-list 'notmuch-hello-hidden-sections
    897 					    title)
    898 			       (notmuch-hello-update))
    899 		     "hide"))
    900     (widget-insert "\n")
    901     (unless is-hidden
    902       (let ((searches (apply 'notmuch-hello-query-counts query-list options)))
    903 	(when (or (not (plist-get options :hide-if-empty))
    904 		  searches)
    905 	  (widget-insert "\n")
    906 	  (notmuch-hello-insert-buttons searches)
    907 	  (indent-rigidly start (point) notmuch-hello-indent))))))
    908 
    909 (defun notmuch-hello-insert-tags-section (&optional title &rest options)
    910   "Insert a section displaying all tags with message counts.
    911 
    912 TITLE defaults to \"All tags\".
    913 Allowed options are those accepted by `notmuch-hello-insert-searches' and the
    914 following:
    915 
    916 :hide-tags - List of tags that should be excluded."
    917   (apply 'notmuch-hello-insert-searches
    918 	 (or title "All tags")
    919 	 (notmuch-hello-generate-tag-alist (plist-get options :hide-tags))
    920 	 options))
    921 
    922 (defun notmuch-hello-insert-inbox ()
    923   "Show an entry for each saved search and inboxed messages for each tag."
    924   (notmuch-hello-insert-searches "What's in your inbox"
    925 				 (append
    926 				  notmuch-saved-searches
    927 				  (notmuch-hello-generate-tag-alist))
    928 				 :filter "tag:inbox"))
    929 
    930 (defun notmuch-hello-insert-alltags ()
    931   "Insert a section displaying all tags and associated message counts."
    932   (notmuch-hello-insert-tags-section
    933    nil
    934    :initially-hidden (not notmuch-show-all-tags-list)
    935    :hide-tags notmuch-hello-hide-tags
    936    :filter notmuch-hello-tag-list-make-query
    937    :disable-excludes t))
    938 
    939 (defun notmuch-hello-insert-footer ()
    940   "Insert the notmuch-hello footer."
    941   (let ((start (point)))
    942     (widget-insert "Hit `?' for context-sensitive help in any Notmuch screen.\n")
    943     (widget-insert "Customize ")
    944     (widget-create 'link
    945 		   :notify (lambda (&rest _ignore)
    946 			     (customize-group 'notmuch))
    947 		   :button-prefix "" :button-suffix ""
    948 		   "Notmuch")
    949     (widget-insert " or ")
    950     (widget-create 'link
    951 		   :notify (lambda (&rest _ignore)
    952 			     (customize-variable 'notmuch-hello-sections))
    953 		   :button-prefix "" :button-suffix ""
    954 		   "this page.")
    955     (let ((fill-column (- (window-width) notmuch-hello-indent)))
    956       (center-region start (point)))))
    957 
    958 ;;; Hello!
    959 
    960 ;;;###autoload
    961 (defun notmuch-hello (&optional no-display)
    962   "Run notmuch and display saved searches, known tags, etc."
    963   (interactive)
    964   (notmuch-assert-cli-sane)
    965   ;; This may cause a window configuration change, so if the
    966   ;; auto-refresh hook is already installed, avoid recursive refresh.
    967   (let ((notmuch-hello-auto-refresh nil))
    968     (if no-display
    969 	(set-buffer "*notmuch-hello*")
    970       (pop-to-buffer-same-window "*notmuch-hello*")))
    971   ;; Install auto-refresh hook
    972   (when notmuch-hello-auto-refresh
    973     (add-hook 'window-configuration-change-hook
    974 	      #'notmuch-hello-window-configuration-change))
    975   (let ((target-line (line-number-at-pos))
    976 	(target-column (current-column))
    977 	(inhibit-read-only t))
    978     ;; Delete all editable widget fields.  Editable widget fields are
    979     ;; tracked in a buffer local variable `widget-field-list' (and
    980     ;; others).  If we do `erase-buffer' without properly deleting the
    981     ;; widgets, some widget-related functions are confused later.
    982     (mapc 'widget-delete widget-field-list)
    983     (erase-buffer)
    984     (unless (eq major-mode 'notmuch-hello-mode)
    985       (notmuch-hello-mode))
    986     (let ((all (overlay-lists)))
    987       ;; Delete all the overlays.
    988       (mapc 'delete-overlay (car all))
    989       (mapc 'delete-overlay (cdr all)))
    990     (mapc
    991      (lambda (section)
    992        (let ((point-before (point)))
    993 	 (if (functionp section)
    994 	     (funcall section)
    995 	   (apply (car section) (cdr section)))
    996 	 ;; don't insert a newline when the previous section didn't
    997 	 ;; show anything.
    998 	 (unless (eq (point) point-before)
    999 	   (widget-insert "\n"))))
   1000      notmuch-hello-sections)
   1001     (widget-setup)
   1002     ;; Move point back to where it was before refresh. Use line and
   1003     ;; column instead of point directly to be insensitive to additions
   1004     ;; and removals of text within earlier lines.
   1005     (goto-char (point-min))
   1006     (forward-line (1- target-line))
   1007     (move-to-column target-column))
   1008   (run-hooks 'notmuch-hello-refresh-hook)
   1009   (setq notmuch-hello-first-run nil))
   1010 
   1011 ;;; _
   1012 
   1013 (provide 'notmuch-hello)
   1014 
   1015 ;;; notmuch-hello.el ends here