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