mu icon indicating copy to clipboard operation
mu copied to clipboard

[mu4e rfe] Use a completing-read alternative instead of mu4e--read-char-choice

Open haji-ali opened this issue 4 years ago • 4 comments

Is your feature request related to a problem? Please describe. No.

Describe the solution you'd like I think it would be better to be more consistent with other completion frameworks. This allows more customizations and a more consistent experience in Emacs.

Describe alternatives you've considered A possible implementation (best visualized with a completion framework like selectrum or vertico, though the code does not depend on a particular framework).

(defun new/mu4e--read-char-choice (prompt candidates)
  "Run a quick `completing-read' for the given CANDIDATES.

List of CANDIDATES is a list of strings. The first character is
used for quick selection."
  (let* ((candidates-alist
          (mapcar (lambda (cand)
                    (prog1
                        (cons
                         (concat "["
                                 (propertize (substring cand 0 1)
                                             'face 'mu4e-highlight-face)
                                 "]"
                                 (substring cand 1))
                         cand)))
                  candidates))
         (metadata `(metadata
                     (display-sort-function . ,#'identity)
                     (cycle-sort-function . ,#'identity)))
         (quick-result)
         (result
          (minibuffer-with-setup-hook
              (lambda ()
                (add-hook 'post-command-hook
                          (lambda ()
                            ;; Exit directly if a quick key is pressed
                            (let ((prefix (minibuffer-contents-no-properties)))
                              (unless (string-empty-p prefix)
                                (mapc (lambda (cand)
                                        (when (string-prefix-p prefix (cdr cand) t)
                                          (setq quick-result cand)
                                          (exit-minibuffer)))
                                      candidates-alist))))
                          -1 'local))
            (completing-read
             prompt
             ;; Use function with metadata to disable sorting.
             (lambda (input predicate action)
               (if (eq action 'metadata)
                   metadata
                 (complete-with-action action candidates-alist input predicate)))
             ;; Require confirmation, if the input does not match a suggestion
             nil t nil nil nil))))
    (or (cdr quick-result)
        (cdr (assoc result candidates-alist)))))

(defun new/mu4e-read-option (prompt options)
  "Ask user for an option from a list on the input area.
PROMPT describes a multiple-choice question to the user.
OPTIONS describe the options, and is a list of cells describing
particular options. Cells have the following structure:

   (OPTIONSTRING . RESULT)

where OPTIONSTRING is a non-empty string describing the
option. The first character of OPTIONSTRING is used as the
shortcut, and obviously all shortcuts must be different, so you
can prefix the string with an uniquifying character.

The options are provided as a list for the user to choose from;
user can then choose by typing CHAR.  Example:
  (mu4e-read-option \"Choose an animal: \"
              '((\"Monkey\" . monkey) (\"Gnu\" . gnu) (\"xMoose\" . moose)))

User now will be presented with a list: \"Choose an animal:
   [M]onkey, [G]nu, [x]Moose\".

Function will return the cdr of the list element."
  

  (let* ((prompt (mu4e-format "%s" prompt))
         (string-options
          (mapcar
           (lambda (option)
             ;; try to detect old-style options, and warn
             (when (characterp (car-safe (cdr-safe option)))
               (mu4e-error
                (concat "Please use the new format for options/actions; "
                        "see the manual")))
             (car option))
           options))
         (response
          (new/mu4e--read-char-choice prompt string-options))
         (chosen
          (seq-find
           (lambda (option) (eq response (car option)))
           options)))
    (if chosen
        (cdr chosen)
      (mu4e-warn "Unknown shortcut '%c'" response))))

Test with

(new/mu4e-read-option
 "Choose an animal: "
 '(("Monkey" . monkey) ("Gnu" . gnu) ("xMoose" . moose)))

Additional context Using the completion framework allows additional features and customizations. For example, in the following I add the unread count to the list of bookmarks

(defun new/mu4e-ask-bookmark (prompt)
  (let* ((bookmarks (cl-loop with bmks = (mu4e-bookmarks)
                             with longest = (mu4e--longest-of-maildirs-and-bookmarks)
                             with queries = (mu4e-last-query-results)
                             for bm in bmks
                             for key = (string (plist-get bm :key))
                             for name = (plist-get bm :name)
                             for query = (funcall (or mu4e-search-query-rewrite-function #'identity)
                                                  (plist-get bm :query))
                             for qcounts = (and (stringp query)
                                                (cl-loop for q in queries
                                                         when (string=
                                                               (decode-coding-string
                                                                (plist-get q :query) 'utf-8 t)
                                                               query)
                                                         collect q))
                             for unread = (and qcounts (plist-get (car qcounts) :unread))
                             when (not (plist-get bm :hide))
                             when (not (and mu4e-main-hide-fully-read (eq unread 0)))
                             collect
                             (cons
                              (concat
                               key
                               name
                               ;; append all/unread numbers, if available.
                               (if qcounts
                                   (let ((unread (plist-get (car qcounts) :unread))
                                         (count  (plist-get (car qcounts) :count)))
                                     (format
                                      "%s (%s/%s)"
                                      (make-string (- longest (string-width name)) ? )
                                      (propertize (number-to-string unread)
                                                  'face 'mu4e-header-key-face)
                                      count))
                                 ""))
                              query))))
    (new/mu4e-read-option prompt bookmarks)))

Test with

(new/mu4e-ask-bookmark "Choose bookmark: ")

This actually allowed me to do away with mu4e-main completely since the only information that is really needed from this view, for me at least, is the unread counts. Instead, I just directly open mu4e-headers to my Inbox and then all the information that mu4e-main shows is shown when needed (for example when jumping to a bookmark or when jumping to a maildir).

haji-ali avatar Oct 13 '21 09:10 haji-ali

Looks interesting, but

(new/mu4e-read-option
 "Choose an animal: "
 '(("Monkey" . monkey) ("Gnu" . gnu) ("xMoose" . moose)))

and

(mu4e-read-option
 "Choose an animal: "
 '(("Monkey" . monkey) ("Gnu" . gnu) ("xMoose" . moose)))

behave differently, since the former requires G RET to choose, while the latter only needs G.

djcb avatar Oct 13 '21 14:10 djcb

@djcb, I've tried this on my Emacs configuration with vertico and an emacs -Q and in both cases I only need to press G to get the result (without RET). Did you observe a different behavior in your configuration?

haji-ali avatar Oct 13 '21 16:10 haji-ali

I'm using ivy.

djcb avatar Oct 13 '21 16:10 djcb

Ah I see. ivy is not 100% completing-read compatible, so different arrangement would have to be made for it.

Maybe one thing that could be done is to have mu4e-read-option be used more consistently throughout mu4e (I note that mu4e-ask-maildir and mu4e-ask-bookmark use separate code for the same functionality). Then mu4e-read-option can be customized (by redefining it or making it custom like mu4e-completing-read-function) to use other completing frameworks. This would also mean that the completion category should be set correctly to be able to add specific customizations.

haji-ali avatar Oct 14 '21 08:10 haji-ali

So, good news, I integrated new/mu4e--read-char-choice and reworked it a bit; it's predicated on mu4e-read-option-builtin (which is the default, current behavior; set to non-nil to get the new behavior.

Also mu4e-search-bookmark / mu4e-search-maildir now honor this. Thanks for your work.

djcb avatar Feb 17 '23 20:02 djcb