1 ;; notmuch-hello.el --- welcome to notmuch, a frontend
3 ;; Copyright © David Edmondson
5 ;; This file is part of Notmuch.
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.
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.
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/>.
20 ;; Authors: David Edmondson <dme@dme.org>
22 (eval-when-compile (require 'cl))
24 (require 'wid-edit) ; For `widget-forward'.
26 (require 'notmuch-lib)
27 (require 'notmuch-mua)
29 (declare-function notmuch-search "notmuch" (&optional query oldest-first target-thread target-line continuation))
30 (declare-function notmuch-poll "notmuch" ())
32 (defcustom notmuch-hello-recent-searches-max 10
33 "The number of recent searches to display."
35 :group 'notmuch-hello)
37 (defcustom notmuch-show-empty-saved-searches nil
38 "Should saved searches with no messages be listed?"
40 :group 'notmuch-hello)
42 (defun notmuch-sort-saved-searches (saved-searches)
43 "Generate an alphabetically sorted saved searches list."
44 (sort (copy-sequence saved-searches)
46 (string< (notmuch-saved-search-get a :name)
47 (notmuch-saved-search-get b :name)))))
49 (defcustom notmuch-saved-search-sort-function nil
50 "Function used to sort the saved searches for the notmuch-hello view.
52 This variable controls how saved searches should be sorted. No
53 sorting (nil) displays the saved searches in the order they are
54 stored in `notmuch-saved-searches'. Sort alphabetically sorts the
55 saved searches in alphabetical order. Custom sort function should
56 be a function or a lambda expression that takes the saved
57 searches list as a parameter, and returns a new saved searches
58 list to be used. For compatibility with the various saved-search
59 formats it should use notmuch-saved-search-get to access the
60 fields of the search."
61 :type '(choice (const :tag "No sorting" nil)
62 (const :tag "Sort alphabetically" notmuch-sort-saved-searches)
63 (function :tag "Custom sort function"
64 :value notmuch-sort-saved-searches))
65 :group 'notmuch-hello)
67 (defvar notmuch-hello-indent 4
68 "How much to indent non-headers.")
70 (defcustom notmuch-show-logo t
71 "Should the notmuch logo be shown?"
73 :group 'notmuch-hello)
75 (defcustom notmuch-show-all-tags-list nil
76 "Should all tags be shown in the notmuch-hello view?"
78 :group 'notmuch-hello)
80 (defcustom notmuch-hello-tag-list-make-query nil
81 "Function or string to generate queries for the all tags list.
83 This variable controls which query results are shown for each tag
84 in the \"all tags\" list. If nil, it will use all messages with
85 that tag. If this is set to a string, it is used as a filter for
86 messages having that tag (equivalent to \"tag:TAG and (THIS-VARIABLE)\").
87 Finally this can be a function that will be called for each tag and
88 should return a filter for that tag, or nil to hide the tag."
89 :type '(choice (const :tag "All messages" nil)
90 (const :tag "Unread messages" "tag:unread")
91 (string :tag "Custom filter"
93 (function :tag "Custom filter function"))
94 :group 'notmuch-hello)
96 (defcustom notmuch-hello-hide-tags nil
97 "List of tags to be hidden in the \"all tags\"-section."
98 :type '(repeat string)
99 :group 'notmuch-hello)
101 (defface notmuch-hello-logo-background
104 (:background "#5f5f5f"))
107 (:background "white")))
108 "Background colour for the notmuch logo."
109 :group 'notmuch-hello
110 :group 'notmuch-faces)
112 (defcustom notmuch-column-control t
113 "Controls the number of columns for saved searches/tags in notmuch view.
115 This variable has three potential sets of values:
117 - t: automatically calculate the number of columns possible based
118 on the tags to be shown and the window width,
119 - an integer: a lower bound on the number of characters that will
120 be used to display each column,
121 - a float: a fraction of the window width that is the lower bound
122 on the number of characters that should be used for each
126 - if you would like two columns of tags, set this to 0.5.
127 - if you would like a single column of tags, set this to 1.0.
128 - if you would like tags to be 30 characters wide, set this to
130 - if you don't want to worry about all of this nonsense, leave
133 (const :tag "Automatically calculated" t)
134 (integer :tag "Number of characters")
135 (float :tag "Fraction of window"))
136 :group 'notmuch-hello)
138 (defcustom notmuch-hello-thousands-separator " "
139 "The string used as a thousands separator.
141 Typically \",\" in the US and UK and \".\" or \" \" in Europe.
142 The latter is recommended in the SI/ISO 31-0 standard and by the
143 International Bureau of Weights and Measures."
145 :group 'notmuch-hello)
147 (defcustom notmuch-hello-mode-hook nil
148 "Functions called after entering `notmuch-hello-mode'."
150 :group 'notmuch-hello
151 :group 'notmuch-hooks)
153 (defcustom notmuch-hello-refresh-hook nil
154 "Functions called after updating a `notmuch-hello' buffer."
156 :group 'notmuch-hello
157 :group 'notmuch-hooks)
159 (defvar notmuch-hello-url "http://notmuchmail.org"
160 "The `notmuch' web site.")
162 (defvar notmuch-hello-custom-section-options
163 '((:filter (string :tag "Filter for each tag"))
164 (:filter-count (string :tag "Different filter to generate message counts"))
165 (:initially-hidden (const :tag "Hide this section on startup" t))
166 (:show-empty-searches (const :tag "Show queries with no matching messages" t))
167 (:hide-if-empty (const :tag "Hide this section if all queries are empty
168 \(and not shown by show-empty-searches)" t)))
169 "Various customization-options for notmuch-hello-tags/query-section.")
171 (define-widget 'notmuch-hello-tags-section 'lazy
172 "Customize-type for notmuch-hello tag-list sections."
173 :tag "Customized tag-list section (see docstring for details)"
176 (const :tag "" notmuch-hello-insert-tags-section)
177 (string :tag "Title for this section")
181 ,(append notmuch-hello-custom-section-options
182 '((:hide-tags (repeat :tag "Tags that will be hidden"
185 (define-widget 'notmuch-hello-query-section 'lazy
186 "Customize-type for custom saved-search-like sections"
187 :tag "Customized queries section (see docstring for details)"
190 (const :tag "" notmuch-hello-insert-searches)
191 (string :tag "Title for this section")
192 (repeat :tag "Queries"
193 (cons (string :tag "Name") (string :tag "Query")))
194 (plist :inline t :options ,notmuch-hello-custom-section-options)))
196 (defcustom notmuch-hello-sections
197 (list #'notmuch-hello-insert-header
198 #'notmuch-hello-insert-saved-searches
199 #'notmuch-hello-insert-search
200 #'notmuch-hello-insert-recent-searches
201 #'notmuch-hello-insert-alltags
202 #'notmuch-hello-insert-footer)
203 "Sections for notmuch-hello.
205 The list contains functions which are used to construct sections in
206 notmuch-hello buffer. When notmuch-hello buffer is constructed,
207 these functions are run in the order they appear in this list. Each
208 function produces a section simply by adding content to the current
209 buffer. A section should not end with an empty line, because a
210 newline will be inserted after each section by `notmuch-hello'.
212 Each function should take no arguments. The return value is
215 For convenience an element can also be a list of the form (FUNC ARG1
216 ARG2 .. ARGN) in which case FUNC will be applied to the rest of the
219 A \"Customized tag-list section\" item in the customize-interface
220 displays a list of all tags, optionally hiding some of them. It
221 is also possible to filter the list of messages matching each tag
222 by an additional filter query. Similarly, the count of messages
223 displayed next to the buttons can be generated by applying a
224 different filter to the tag query. These filters are also
225 supported for \"Customized queries section\" items."
226 :group 'notmuch-hello
229 (choice (function-item notmuch-hello-insert-header)
230 (function-item notmuch-hello-insert-saved-searches)
231 (function-item notmuch-hello-insert-search)
232 (function-item notmuch-hello-insert-recent-searches)
233 (function-item notmuch-hello-insert-alltags)
234 (function-item notmuch-hello-insert-footer)
235 (function-item notmuch-hello-insert-inbox)
236 notmuch-hello-tags-section
237 notmuch-hello-query-section
238 (function :tag "Custom section"))))
240 (defcustom notmuch-hello-auto-refresh t
241 "Automatically refresh when returning to the notmuch-hello buffer."
242 :group 'notmuch-hello
245 (defvar notmuch-hello-hidden-sections nil
246 "List of sections titles whose contents are hidden")
248 (defvar notmuch-hello-first-run t
249 "True if `notmuch-hello' is run for the first time, set to nil
252 (defun notmuch-hello-nice-number (n)
255 (push (% n 1000) result)
257 (setq result (or result '(0)))
259 (number-to-string (car result))
260 (mapcar (lambda (elem)
261 (format "%s%03d" notmuch-hello-thousands-separator elem))
264 (defun notmuch-hello-trim (search)
266 (if (string-match "^[[:space:]]*\\(.*[^[:space:]]\\)[[:space:]]*$" search)
267 (match-string 1 search)
270 (defun notmuch-hello-search (&optional search)
271 (unless (null search)
272 (setq search (notmuch-hello-trim search))
273 (let ((history-delete-duplicates t))
274 (add-to-history 'notmuch-search-history search)))
275 (notmuch-search search notmuch-search-oldest-first))
277 (defun notmuch-saved-search-get (saved-search field)
278 "Get FIELD from SAVED-SEARCH.
280 If SAVED-SEARCH is a plist, this is just `plist-get', but for
281 backwards compatibility, this also deals with the two other
282 possible formats for SAVED-SEARCH: cons cells (NAME . QUERY) and
283 lists (NAME QUERY COUNT-QUERY)."
285 ((keywordp (car saved-search))
286 (plist-get saved-search field))
287 ;; It is not a plist so it is an old-style entry.
288 ((consp (cdr saved-search)) ;; It is a list (NAME QUERY COUNT-QUERY)
290 (:name (first saved-search))
291 (:query (second saved-search))
292 (:count-query (third saved-search))
294 (t ;; It is a cons-cell (NAME . QUERY)
296 (:name (car saved-search))
297 (:query (cdr saved-search))
300 (defun notmuch-hello-saved-search-to-plist (saved-search)
301 "Return a copy of SAVED-SEARCH in plist form.
303 If saved search is a plist then just return a copy. In other
304 cases, for backwards compatability, convert to plist form and
306 (if (keywordp (car saved-search))
307 (copy-seq saved-search)
308 (let ((fields (list :name :query :count-query))
310 (dolist (field fields plist-search)
311 (let ((string (notmuch-saved-search-get saved-search field)))
313 (setq plist-search (append plist-search (list field string)))))))))
315 (defun notmuch-hello-add-saved-search (widget)
317 (let ((search (widget-value
319 (widget-get widget :notmuch-saved-search-widget))))
320 (name (completing-read "Name for saved search: "
321 notmuch-saved-searches)))
322 ;; If an existing saved search with this name exists, remove it.
323 (setq notmuch-saved-searches
324 (loop for elem in notmuch-saved-searches
326 (notmuch-saved-search-get elem :name)))
329 (customize-save-variable 'notmuch-saved-searches
330 (add-to-list 'notmuch-saved-searches
331 (list :name name :query search) t))
332 (message "Saved '%s' as '%s'." search name)
333 (notmuch-hello-update)))
335 (defun notmuch-hello-delete-search-from-history (widget)
337 (let ((search (widget-value
339 (widget-get widget :notmuch-saved-search-widget)))))
340 (setq notmuch-search-history (delete search
341 notmuch-search-history))
342 (notmuch-hello-update)))
344 (defun notmuch-hello-longest-label (searches-alist)
345 (or (loop for elem in searches-alist
346 maximize (length (notmuch-saved-search-get elem :name)))
349 (defun notmuch-hello-reflect-generate-row (ncols nrows row list)
350 (let ((len (length list)))
351 (loop for col from 0 to (- ncols 1)
352 collect (let ((offset (+ (* nrows col) row)))
355 ;; Don't forget to insert an empty slot in the
356 ;; output matrix if there is no corresponding
357 ;; value in the input matrix.
360 (defun notmuch-hello-reflect (list ncols)
361 "Reflect a `ncols' wide matrix represented by `list' along the
364 (let ((nrows (ceiling (length list) ncols)))
365 (loop for row from 0 to (- nrows 1)
366 append (notmuch-hello-reflect-generate-row ncols nrows row list))))
368 (defun notmuch-hello-widget-search (widget &rest ignore)
369 (notmuch-search (widget-get widget
370 :notmuch-search-terms)
372 :notmuch-search-oldest-first)))
374 (defun notmuch-saved-search-count (search)
375 (car (process-lines notmuch-command "count" search)))
377 (defun notmuch-hello-tags-per-line (widest)
378 "Determine how many tags to show per line and how wide they
379 should be. Returns a cons cell `(tags-per-line width)'."
382 ((integerp notmuch-column-control)
384 (/ (- (window-width) notmuch-hello-indent)
385 ;; Count is 9 wide (8 digits plus space), 1 for the space
387 (+ 9 1 (max notmuch-column-control widest)))))
389 ((floatp notmuch-column-control)
390 (let* ((available-width (- (window-width) notmuch-hello-indent))
391 (proposed-width (max (* available-width notmuch-column-control) widest)))
392 (floor available-width proposed-width)))
396 (/ (- (window-width) notmuch-hello-indent)
397 ;; Count is 9 wide (8 digits plus space), 1 for the space
401 (cons tags-per-line (/ (max 1
402 (- (window-width) notmuch-hello-indent
403 ;; Count is 9 wide (8 digits plus
404 ;; space), 1 for the space after the
406 (* tags-per-line (+ 9 1))))
409 (defun notmuch-hello-filtered-query (query filter)
410 "Constructs a query to search all messages matching QUERY and FILTER.
412 If FILTER is a string, it is directly used in the returned query.
414 If FILTER is a function, it is called with QUERY as a parameter and
415 the string it returns is used as the query. If nil is returned,
418 Otherwise, FILTER is ignored.
421 ((functionp filter) (funcall filter query))
423 (concat "(" query ") and (" filter ")"))
426 (defun notmuch-hello-query-counts (query-list &rest options)
427 "Compute list of counts of matched messages from QUERY-LIST.
429 QUERY-LIST must be a list of saved-searches. Ideally each of
430 these is a plist but other options are available for backwards
431 compatibility: see `notmuch-saved-searches' for details.
433 The result is a list of plists each of which includes the
434 properties :name NAME, :query QUERY and :count COUNT, together
435 with any properties in the original saved-search.
437 The values :show-empty-searches, :filter and :filter-count from
438 options will be handled as specified for
439 `notmuch-hello-insert-searches'."
441 (dolist (elem query-list nil)
442 (let ((count-query (or (notmuch-saved-search-get elem :count-query)
443 (notmuch-saved-search-get elem :query))))
445 (replace-regexp-in-string
447 (notmuch-hello-filtered-query count-query
448 (or (plist-get options :filter-count)
449 (plist-get options :filter))))
452 (unless (= (call-process-region (point-min) (point-max) notmuch-command
453 t t nil "count" "--batch") 0)
454 (notmuch-logged-error "notmuch count --batch failed"
455 "Please check that the notmuch CLI is new enough to support `count
456 --batch'. In general we recommend running matching versions of
457 the CLI and emacs interface."))
459 (goto-char (point-min))
461 (notmuch-remove-if-not
465 (let* ((elem-plist (notmuch-hello-saved-search-to-plist elem))
466 (search-query (plist-get elem-plist :query))
467 (filtered-query (notmuch-hello-filtered-query
468 search-query (plist-get options :filter)))
469 (message-count (prog1 (read (current-buffer))
471 (when (and filtered-query (or (plist-get options :show-empty-searches) (> message-count 0)))
472 (setq elem-plist (plist-put elem-plist :query filtered-query))
473 (plist-put elem-plist :count message-count))))
476 (defun notmuch-hello-insert-buttons (searches)
477 "Insert buttons for SEARCHES.
479 SEARCHES must be a list of plists each of which should contain at
480 least the properties :name NAME :query QUERY and :count COUNT,
481 where QUERY is the query to start when the button for the
482 corresponding entry is activated, and COUNT should be the number
483 of messages matching the query. Such a plist can be computed
484 with `notmuch-hello-query-counts'."
485 (let* ((widest (notmuch-hello-longest-label searches))
486 (tags-and-width (notmuch-hello-tags-per-line widest))
487 (tags-per-line (car tags-and-width))
488 (column-width (cdr tags-and-width))
491 (reordered-list (notmuch-hello-reflect searches tags-per-line))
492 ;; Hack the display of the buttons used.
493 (widget-push-button-prefix "")
494 (widget-push-button-suffix ""))
495 ;; dme: It feels as though there should be a better way to
496 ;; implement this loop than using an incrementing counter.
498 ;; (not elem) indicates an empty slot in the matrix.
500 (if (> column-indent 0)
501 (widget-insert (make-string column-indent ? )))
502 (let* ((name (plist-get elem :name))
503 (query (plist-get elem :query))
504 (oldest-first (case (plist-get elem :sort-order)
507 (otherwise notmuch-search-oldest-first)))
508 (msg-count (plist-get elem :count)))
509 (widget-insert (format "%8s "
510 (notmuch-hello-nice-number msg-count)))
511 (widget-create 'push-button
512 :notify #'notmuch-hello-widget-search
513 :notmuch-search-terms query
514 :notmuch-search-oldest-first oldest-first
517 (1+ (max 0 (- column-width (length name)))))))
518 (setq count (1+ count))
519 (when (eq (% count tags-per-line) 0)
520 (setq column-indent 0)
521 (widget-insert "\n")))
524 ;; If the last line was not full (and hence did not include a
525 ;; carriage return), insert one now.
526 (unless (eq (% count tags-per-line) 0)
527 (widget-insert "\n"))))
529 (defimage notmuch-hello-logo ((:type png :file "notmuch-logo.png")))
531 (defun notmuch-hello-update (&optional no-display)
532 "Update the current notmuch view."
533 ;; Lazy - rebuild everything.
534 (notmuch-hello no-display))
536 (defun notmuch-hello-window-configuration-change ()
537 "Hook function to update the hello buffer when it is switched to."
538 (let ((hello-buf (get-buffer "*notmuch-hello*"))
540 ;; Consider all windows in the currently selected frame, since
541 ;; that's where the configuration change happened. This also
542 ;; refreshes our snapshot of all windows, so we have to do this
543 ;; even if we know we won't refresh (e.g., hello-buf is null).
544 (dolist (window (window-list))
545 (let ((last-buf (window-parameter window 'notmuch-hello-last-buffer))
546 (cur-buf (window-buffer window)))
547 (when (not (eq last-buf cur-buf))
548 ;; This window changed or is new. Update recorded buffer
550 (set-window-parameter window 'notmuch-hello-last-buffer cur-buf)
551 (when (and (eq cur-buf hello-buf) last-buf)
552 ;; The user just switched to hello in this window (hello
553 ;; is currently visible, was not visible on the last
554 ;; configuration change, and this is not a new window)
555 (setq do-refresh t)))))
556 (when (and do-refresh notmuch-hello-auto-refresh)
557 ;; Refresh hello as soon as we get back to redisplay. On Emacs
558 ;; 24, we can't do it right here because something in this
559 ;; hook's call stack overrides hello's point placement.
560 (run-at-time nil nil #'notmuch-hello t))
561 (when (null hello-buf)
563 (remove-hook 'window-configuration-change-hook
564 #'notmuch-hello-window-configuration-change))))
566 ;; the following variable is defined as being defconst in notmuch-version.el
567 (defvar notmuch-emacs-version)
569 (defun notmuch-hello-versions ()
570 "Display the notmuch version(s)"
572 (let ((notmuch-cli-version (notmuch-version)))
573 (message "notmuch version %s"
574 (if (string= notmuch-emacs-version notmuch-cli-version)
576 (concat notmuch-cli-version
577 " (emacs mua version " notmuch-emacs-version ")")))))
579 (defvar notmuch-hello-mode-map
580 (let ((map (if (fboundp 'make-composed-keymap)
581 ;; Inherit both widget-keymap and notmuch-common-keymap
582 (make-composed-keymap widget-keymap)
583 ;; Before Emacs 24, keymaps didn't support multiple
584 ;; inheritance,, so just copy the widget keymap since
585 ;; it's unlikely to change.
586 (copy-keymap widget-keymap))))
587 (set-keymap-parent map notmuch-common-keymap)
588 (define-key map "v" 'notmuch-hello-versions)
589 (define-key map (kbd "<C-tab>") 'widget-backward)
591 "Keymap for \"notmuch hello\" buffers.")
592 (fset 'notmuch-hello-mode-map notmuch-hello-mode-map)
594 (defun notmuch-hello-mode ()
595 "Major mode for convenient notmuch navigation. This is your entry portal into notmuch.
597 Complete list of currently available key bindings:
599 \\{notmuch-hello-mode-map}"
601 (kill-all-local-variables)
602 (setq notmuch-buffer-refresh-function #'notmuch-hello-update)
603 (use-local-map notmuch-hello-mode-map)
604 (setq major-mode 'notmuch-hello-mode
605 mode-name "notmuch-hello")
606 (run-mode-hooks 'notmuch-hello-mode-hook)
607 ;;(setq buffer-read-only t)
610 (defun notmuch-hello-generate-tag-alist (&optional hide-tags)
611 "Return an alist from tags to queries to display in the all-tags section."
612 (mapcar (lambda (tag)
613 (cons tag (concat "tag:" (notmuch-escape-boolean-term tag))))
614 (notmuch-remove-if-not
616 (not (member tag hide-tags)))
617 (process-lines notmuch-command "search" "--output=tags" "*"))))
619 (defun notmuch-hello-insert-header ()
620 "Insert the default notmuch-hello header."
621 (when notmuch-show-logo
622 (let ((image notmuch-hello-logo))
623 ;; The notmuch logo uses transparency. That can display poorly
624 ;; when inserting the image into an emacs buffer (black logo on
625 ;; a black background), so force the background colour of the
626 ;; image. We use a face to represent the colour so that
627 ;; `defface' can be used to declare the different possible
628 ;; colours, which depend on whether the frame has a light or
630 (setq image (cons 'image
632 (list :background (face-background 'notmuch-hello-logo-background)))))
633 (insert-image image))
636 (widget-insert "Welcome to ")
637 ;; Hack the display of the links used.
638 (let ((widget-link-prefix "")
639 (widget-link-suffix ""))
641 :notify (lambda (&rest ignore)
642 (browse-url notmuch-hello-url))
643 :help-echo "Visit the notmuch website."
646 (widget-insert "You have ")
648 :notify (lambda (&rest ignore)
649 (notmuch-hello-update))
651 (notmuch-hello-nice-number
652 (string-to-number (car (process-lines notmuch-command "count")))))
653 (widget-insert " messages.\n")))
656 (defun notmuch-hello-insert-saved-searches ()
657 "Insert the saved-searches section."
658 (let ((searches (notmuch-hello-query-counts
659 (if notmuch-saved-search-sort-function
660 (funcall notmuch-saved-search-sort-function
661 notmuch-saved-searches)
662 notmuch-saved-searches)
663 :show-empty-searches notmuch-show-empty-saved-searches)))
665 (widget-insert "Saved searches: ")
666 (widget-create 'push-button
667 :notify (lambda (&rest ignore)
668 (customize-variable 'notmuch-saved-searches))
670 (widget-insert "\n\n")
671 (let ((start (point)))
672 (notmuch-hello-insert-buttons searches)
673 (indent-rigidly start (point) notmuch-hello-indent)))))
675 (defun notmuch-hello-insert-search ()
676 "Insert a search widget."
677 (widget-insert "Search: ")
678 (widget-create 'editable-field
679 ;; Leave some space at the start and end of the
681 :size (max 8 (- (window-width) notmuch-hello-indent
682 (length "Search: ")))
683 :action (lambda (widget &rest ignore)
684 (notmuch-hello-search (widget-value widget))))
685 ;; Add an invisible dot to make `widget-end-of-line' ignore
686 ;; trailing spaces in the search widget field. A dot is used
687 ;; instead of a space to make `show-trailing-whitespace'
688 ;; happy, i.e. avoid it marking the whole line as trailing
691 (put-text-property (1- (point)) (point) 'invisible t)
692 (widget-insert "\n"))
694 (defun notmuch-hello-insert-recent-searches ()
695 "Insert recent searches."
696 (when notmuch-search-history
697 (widget-insert "Recent searches: ")
698 (widget-create 'push-button
699 :notify (lambda (&rest ignore)
700 (when (y-or-n-p "Are you sure you want to clear the searches? ")
701 (setq notmuch-search-history nil)
702 (notmuch-hello-update)))
704 (widget-insert "\n\n")
705 (let ((start (point)))
706 (loop for i from 1 to notmuch-hello-recent-searches-max
707 for search in notmuch-search-history do
708 (let ((widget-symbol (intern (format "notmuch-hello-search-%d" i))))
710 (widget-create 'editable-field
711 ;; Don't let the search boxes be
712 ;; less than 8 characters wide.
719 (* 2 notmuch-hello-indent)
722 ;; `[save]' button. 6
727 ;; before the `[del]'
731 :action (lambda (widget &rest ignore)
732 (notmuch-hello-search (widget-value widget)))
735 (widget-create 'push-button
736 :notify (lambda (widget &rest ignore)
737 (notmuch-hello-add-saved-search widget))
738 :notmuch-saved-search-widget widget-symbol
741 (widget-create 'push-button
742 :notify (lambda (widget &rest ignore)
743 (when (y-or-n-p "Are you sure you want to delete this search? ")
744 (notmuch-hello-delete-search-from-history widget)))
745 :notmuch-saved-search-widget widget-symbol
747 (widget-insert "\n"))
748 (indent-rigidly start (point) notmuch-hello-indent))
751 (defun notmuch-hello-insert-searches (title query-list &rest options)
752 "Insert a section with TITLE showing a list of buttons made from QUERY-LIST.
754 QUERY-LIST should ideally be a plist but for backwards
755 compatibility other forms are also accepted (see
756 `notmuch-saved-searches' for details). The plist should
757 contain keys :name and :query; if :count-query is also present
758 then it specifies an alternate query to be used to generate the
759 count for the associated search.
761 Supports the following entries in OPTIONS as a plist:
762 :initially-hidden - if non-nil, section will be hidden on startup
763 :show-empty-searches - show buttons with no matching messages
764 :hide-if-empty - hide if no buttons would be shown
765 (only makes sense without :show-empty-searches)
766 :filter - This can be a function that takes the search query as its argument and
767 returns a filter to be used in conjuction with the query for that search or nil
768 to hide the element. This can also be a string that is used as a combined with
769 each query using \"and\".
770 :filter-count - Separate filter to generate the count displayed each search. Accepts
771 the same values as :filter. If :filter and :filter-count are specified, this
772 will be used instead of :filter, not in conjunction with it."
773 (widget-insert title ": ")
774 (if (and notmuch-hello-first-run (plist-get options :initially-hidden))
775 (add-to-list 'notmuch-hello-hidden-sections title))
776 (let ((is-hidden (member title notmuch-hello-hidden-sections))
779 (widget-create 'push-button
780 :notify `(lambda (widget &rest ignore)
781 (setq notmuch-hello-hidden-sections
782 (delete ,title notmuch-hello-hidden-sections))
783 (notmuch-hello-update))
785 (widget-create 'push-button
786 :notify `(lambda (widget &rest ignore)
787 (add-to-list 'notmuch-hello-hidden-sections
789 (notmuch-hello-update))
792 (when (not is-hidden)
793 (let ((searches (apply 'notmuch-hello-query-counts query-list options)))
794 (when (or (not (plist-get options :hide-if-empty))
797 (notmuch-hello-insert-buttons searches)
798 (indent-rigidly start (point) notmuch-hello-indent))))))
800 (defun notmuch-hello-insert-tags-section (&optional title &rest options)
801 "Insert a section displaying all tags with message counts.
803 TITLE defaults to \"All tags\".
804 Allowed options are those accepted by `notmuch-hello-insert-searches' and the
807 :hide-tags - List of tags that should be excluded."
808 (apply 'notmuch-hello-insert-searches
809 (or title "All tags")
810 (notmuch-hello-generate-tag-alist (plist-get options :hide-tags))
813 (defun notmuch-hello-insert-inbox ()
814 "Show an entry for each saved search and inboxed messages for each tag"
815 (notmuch-hello-insert-searches "What's in your inbox"
817 notmuch-saved-searches
818 (notmuch-hello-generate-tag-alist))
819 :filter "tag:inbox"))
821 (defun notmuch-hello-insert-alltags ()
822 "Insert a section displaying all tags and associated message counts"
823 (notmuch-hello-insert-tags-section
825 :initially-hidden (not notmuch-show-all-tags-list)
826 :hide-tags notmuch-hello-hide-tags
827 :filter notmuch-hello-tag-list-make-query))
829 (defun notmuch-hello-insert-footer ()
830 "Insert the notmuch-hello footer."
831 (let ((start (point)))
832 (widget-insert "Type a search query and hit RET to view matching threads.\n")
833 (when notmuch-search-history
834 (widget-insert "Hit RET to re-submit a previous search. Edit it first if you like.\n")
835 (widget-insert "Save recent searches with the `save' button.\n"))
836 (when notmuch-saved-searches
837 (widget-insert "Edit saved searches with the `edit' button.\n"))
838 (widget-insert "Hit RET or click on a saved search or tag name to view matching threads.\n")
839 (widget-insert "`=' to refresh this screen. `s' to search messages. `q' to quit.\n")
841 :notify (lambda (&rest ignore)
842 (customize-variable 'notmuch-hello-sections))
843 :button-prefix "" :button-suffix ""
845 (widget-insert " this page.")
846 (let ((fill-column (- (window-width) notmuch-hello-indent)))
847 (center-region start (point)))))
850 (defun notmuch-hello (&optional no-display)
851 "Run notmuch and display saved searches, known tags, etc."
854 (notmuch-assert-cli-sane)
855 ;; This may cause a window configuration change, so if the
856 ;; auto-refresh hook is already installed, avoid recursive refresh.
857 (let ((notmuch-hello-auto-refresh nil))
859 (set-buffer "*notmuch-hello*")
860 (switch-to-buffer "*notmuch-hello*")))
862 ;; Install auto-refresh hook
863 (when notmuch-hello-auto-refresh
864 (add-hook 'window-configuration-change-hook
865 #'notmuch-hello-window-configuration-change))
867 (let ((target-line (line-number-at-pos))
868 (target-column (current-column))
869 (inhibit-read-only t))
871 ;; Delete all editable widget fields. Editable widget fields are
872 ;; tracked in a buffer local variable `widget-field-list' (and
873 ;; others). If we do `erase-buffer' without properly deleting the
874 ;; widgets, some widget-related functions are confused later.
875 (mapc 'widget-delete widget-field-list)
879 (unless (eq major-mode 'notmuch-hello-mode)
880 (notmuch-hello-mode))
882 (let ((all (overlay-lists)))
883 ;; Delete all the overlays.
884 (mapc 'delete-overlay (car all))
885 (mapc 'delete-overlay (cdr all)))
889 (let ((point-before (point)))
890 (if (functionp section)
892 (apply (car section) (cdr section)))
893 ;; don't insert a newline when the previous section didn't
895 (unless (eq (point) point-before)
896 (widget-insert "\n"))))
897 notmuch-hello-sections)
900 ;; Move point back to where it was before refresh. Use line and
901 ;; column instead of point directly to be insensitive to additions
902 ;; and removals of text within earlier lines.
903 (goto-char (point-min))
904 (forward-line (1- target-line))
905 (move-to-column target-column))
906 (run-hooks 'notmuch-hello-refresh-hook)
907 (setq notmuch-hello-first-run nil))
909 (defun notmuch-folder ()
910 "Deprecated function for invoking notmuch---calling `notmuch' is preferred now."
916 (provide 'notmuch-hello)