A nicer notmuch-hello screen for Emacs

Here I define my own hello screen for notmuch. However, I didn’t like it’s original “hello” screen not that much. So I wrote something to replace it.

This is how it used to look:

img

That’s nice for a start.

  • Notmuch’s hello page is using Emacs’ config feature. I however like to write everything out, and document the things while I doing them in my config.org file. → Get rid of buttons like “Save” or the “Customize …” text.
  • Notmuch doesn’t show the amount of messages. Okay, when I add query to notmuch-saved-searches, it will show them. But at an awkward position: around the center of the screen. Also it won’t differentiate between new and total counts .. or it would need two queries, not just one. → Present the queries better.
  • I see elements that i never use, e.g. the header, the footer, or the “Clear” button. → Get rid of them.

What I wrote now looks like this:

img

The implementation

Let’s start simple: define the queries.

Note that I define basic queries, e.g. at this point in time I don’t create two queries for the unread/total amount of messages.

;; This is GPLv2. If you still don't know the details, read
;; http://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html

(use-package notmuch-hello
  :defer t
  :config
  (setq notmuch-saved-searches
        '((:key "i" :name "inbox" :query "folder:INBOX")
          (:key "b" :name "barebox" :query "folder:barebox")
          (:key "c" :name "linux-can" :query "folder:linux-can")
          (:key "a" :name "linux-arm-kernel" :query "folder:linux-arm-kernel")
          (:key "m" :name "linux-mmc" :query "folder:linux-mmc")
          (:key "9" :name "ath9k-devel" :query "folder:ath9k-devel")
          (:key "e" :name "elecraft" :query "folder:elecraft")
          (:key "p" :name "powertop" :query "folder:powertop")
          (:key "D" :name "Deleted" :query "tag:deleted")
          (:key "F" :name "Flagged" :query "tag:flagged")
          (:key "S" :name "Sent" :query "folder:sent")
          (:key "u" :name "unread" :query "tag:unread")
          ))

  ;; We add items later in reverse order with (add-to-list ...):
  (setq notmuch-hello-sections '())

  ;; Add a thousand separator
  (setq notmuch-hello-thousands-separator ".")

We set notmuch-hello-sections to the empty list, so we add hello-section after hello-section with (add-to-list. This prepends, so we add the sections in reverse order.

List of recent searches

img

At the bottom are the recent searches, just without the “Save” and “Clear” buttons. This is just a slightly modified reimplementation of notmuch-hello-insert-recent-searches:

;; This is GPLv3. If you still don't know the details, read
;; http://www.gnu.org/licenses/gpl-3.0.en.html

(defun my-notmuch-hello-insert-recent-searches ()
  "Insert recent searches."
  (when notmuch-search-history
    (widget-insert "Recent searches:")
    (widget-insert "\n\n")
    (let ((start (point)))
      (loop for i from 1 to notmuch-hello-recent-searches-max
        for search in notmuch-search-history do
        (let ((widget-symbol (intern (format "notmuch-hello-search-%d" i))))
          (set widget-symbol
           (widget-create 'editable-field
                  ;; Don't let the search boxes be
                  ;; less than 8 characters wide.
                  :size (max 8
                         (- (window-width)
                        ;; Leave some space
                        ;; at the start and
                        ;; end of the
                        ;; boxes.
                        (* 2 notmuch-hello-indent)
                        ;; 1 for the space
                        ;; before the `[del]'
                        ;; button. 5 for the
                        ;; `[del]' button.
                        1 5))
                  :action (lambda (widget &rest ignore)
                        (notmuch-hello-search (widget-value widget)))
                  search))
          (widget-insert " ")
          (widget-create 'push-button
                 :notify (lambda (widget &rest ignore)
                       (when (y-or-n-p "Are you sure you want to delete this search? ")
                     (notmuch-hello-delete-search-from-history widget)))
                 :notmuch-saved-search-widget widget-symbol
                 "del"))
        (widget-insert "\n"))
      (indent-rigidly start (point) notmuch-hello-indent))
    nil))

  (add-to-list 'notmuch-hello-sections #'my-notmuch-hello-insert-recent-searches)

Simple search line

img

Then I want a simple search method. The original implementation suited my needs quite fine, I use it unmodified:

  (add-to-list 'notmuch-hello-sections #'notmuch-hello-insert-search)

Header face for the search screen

And finally we want out improved hello screen. Let’s start with the face for the header:

  ;; This is GPLv2. If you still don't know the details, read
  ;; http://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html

  (defface my-notmuch-hello-header-face
    '((t :foreground "white"
         :background "blue"
         :weight bold))
    "Font for the header in `my-notmuch-hello-insert-searches`."
    :group 'notmuch-faces)

Helper function to count messages

I implemented a simpler version of notmuch-hello-query-counts:

  ;; This is GPLv3. If you still don't know the details, read
  ;; http://www.gnu.org/licenses/gpl-3.0.en.html

  (defun my-count-query (query)
    (with-temp-buffer
      (insert query "\n")
      (unless (= (call-process-region (point-min) (point-max) notmuch-command
                                      t t nil "count" "--batch") 0)
        (notmuch-logged-error "notmuch count --batch failed"
"Please check that the notmuch CLI is new enough to support `count
--batch'. In general we recommend running matching versions of
the CLI and emacs interface."))

      (goto-char (point-min))
      (let ((n (read (current-buffer))))
        (if (= n 0)
            nil
          (notmuch-hello-nice-number n)))))

This function can be called like this:

(my-count-query "folder:linux-arm-kernel tag:unread")

It will return either nil or a string containing the nicely formatted amount of messages. Note that it doesn’t return the integer 0 or the string “0” but nil. I’ve done it so that I can easier depend on the return value in an (if ...) form — if considers “0” to be true.

Create query widget

The following either inserts a 'push-button widget (if the query has a count associated) or some empty spaces:

  ;; This is GPLv2. If you still don't know the details, read
  ;; http://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html

  (defun my-notmuch-hello-query-insert (cnt query elem)
    (if cnt
        (let* ((str (format "%s" cnt))
               (widget-push-button-prefix "")
               (widget-push-button-suffix "")
               (oldest-first (case (plist-get elem :sort-order)
                               (newest-first nil)
                               (oldest-first t)
                               (otherwise notmuch-search-oldest-first))))
          (widget-create 'push-button
                         :notify #'notmuch-hello-widget-search
                         :notmuch-search-terms query
                         :notmuch-search-oldest-first oldest-first
                         :notmuch-search-type 'tree
                         str)
          (widget-insert (make-string (- 8 (length str)) ? )))
      (widget-insert "        ")))

Binding everything together

And finally we iterate over the notmuch-saved-searches, get the base query, calculate the count of total messages into q_tot and the count of new messages into q_new. We use that information to create the widgets accordingly.

img

  ;; This is GPLv2. If you still don't know the details, read
  ;; http://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html

  (defun my-notmuch-hello-insert-searches ()
    "Insert the saved-searches section."
    (widget-insert (propertize "New     Total      Key  List\n" 'face 'my-notmuch-hello-header-face))
    (mapc (lambda (elem)
            (when elem
              (let* ((q_tot (plist-get elem :query))
                     (q_new (concat q_tot " AND tag:unread"))
                     (n_tot (my-count-query q_tot))
                     (n_new (my-count-query q_new)))
                (my-notmuch-hello-query-insert n_new q_new elem)
                (my-notmuch-hello-query-insert n_tot q_tot elem)
                (widget-insert "   ")
                (widget-insert (plist-get elem :key))
                (widget-insert "    ")
                (widget-insert (plist-get elem :name))
                (widget-insert "\n")
              ))
            )
          notmuch-saved-searches))

That’s all, folks.