diff options
| author | Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> | 2026-04-08 20:45:32 +0300 |
|---|---|---|
| committer | Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> | 2026-04-10 20:07:42 +0300 |
| commit | e870b2d3302925187f06cb2ef4cb3b4c9c5f9acf (patch) | |
| tree | 9434dd0e28c3073654eb876d675ba71928abbfd3 /mu4e | |
| parent | 397c7172fc48b8b792b1ba34af28a803482a2097 (diff) | |
mu4e: support icons for mime-types
In mu4e-view-mime-part-action, support icons or, well, "icons".
For this to work user needs to install a package like nerd-icons and set
mu4e-file-name-to-icon-function to #'nerd-icons-icon-for-file. (see
docstring for details)
Diffstat (limited to 'mu4e')
| -rw-r--r-- | mu4e/mu4e-helpers.el | 211 | ||||
| -rw-r--r-- | mu4e/mu4e-mime-parts.el | 119 |
2 files changed, 187 insertions, 143 deletions
diff --git a/mu4e/mu4e-helpers.el b/mu4e/mu4e-helpers.el index 1770562..1925cbc 100644 --- a/mu4e/mu4e-helpers.el +++ b/mu4e/mu4e-helpers.el @@ -1,6 +1,6 @@ ;;; mu4e-helpers.el --- Helper functions -*- lexical-binding: t -*- -;; Copyright (C) 2022-2025 Dirk-Jan C. Binnema +;; Copyright (C) 2022-2026 Dirk-Jan C. Binnema ;; Author: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> ;; Maintainer: Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> @@ -35,95 +35,55 @@ (require 'mu4e-window) (require 'mu4e-config) +(require 'mailcap) ;;; Customization -(defcustom mu4e-debug nil - "When set to non-nil, log debug information to the mu4e log buffer." - :type 'boolean - :group 'mu4e) - -(defcustom mu4e-completing-read-function #'ido-completing-read - "Function to be used to receive user-input during completion. - -Suggested possible values are: - * `completing-read': Emacs' built-in completion method - * `ido-completing-read': dynamic completion within the minibuffer. - -The function is used in two contexts - -1) directly - for instance in when listing _other_ maildirs - in `mu4e-ask-maildir' -2) if `mu4e-read-option-use-builtin' is nil, it is used - as part of `mu4e-read-option' in many places. - -Set it to `completing-read' when you want to use completion -frameworks such as Helm, Ivy or Vertico. In that case, you -might want to add something like the following in your configuration. +;;; Icons - (setq mu4e-read-option-use-builtin nil - mu4e-completing-read-function \\='completing-read) -." - :type 'function - :options '(completing-read ido-completing-read) - :group 'mu4e) +(defcustom mu4e-file-name-to-icon-function nil + "Function to return an icon for a file name, or nil. +When set, this should be a function that takes a file name and +returns a string (icon) or nil. -(defcustom mu4e-read-option-use-builtin t - "Whether to use mu4e's traditional completion for `mu4e-read-option'. - -If nil, use the value of `mu4e-completing-read-function', integrated -into mu4e. - -Many of the third-party completion frameworks - such as Helm, Ivy -and Vertico - influence `completion-read', so to have mu4e follow -your overall settings, try the equivalent of - - (setq mu4e-read-option-use-builtin nil - mu4e-completing-read-function \\='completing-read) - -Tastes differ, but without any such frameworks, the unaugmented -Emacs `completing-read' is rather Spartan." - :type 'boolean - :group 'mu4e) +If you have the `nerd-icons' package, you can put +`nerd-icons-icon-for-file'. If those icons are too big, consider +installing the special nerd fonts, or perhaps use a value like: -(defcustom mu4e-use-fancy-chars nil - "When set, allow fancy (Unicode) characters for marks/threads. -You can customize the exact fancy characters used with -`mu4e-marks' and various `mu4e-headers-..-mark' and -`mu4e-headers..-prefix' variables." - :type 'boolean + (lambda (name) + (nerd-icons-icon-for-file name :height 1.0)))" + :type '(choice (const :tag "None" nil) function) :group 'mu4e) -;; maybe move the next ones... but they're convenient -;; here because they're needed in multiple buffers. - -(defcustom mu4e-view-auto-mark-as-read t - "Automatically mark messages as read when you read them. -This is the default behavior, but can be turned off, for example -when using a read-only file-system. - -This can also be set to a function; if so, receives a message -plist which should evaluate to nil if the message should *not* be -marked as read-only, or non-nil otherwise." - :type '(choice - boolean - function) - :group 'mu4e-view) - -(defun mu4e-select-other-view () - "Switch between headers view and message view." - (interactive) - (let* ((other-buf - (cond - ((mu4e-current-buffer-type-p 'view) - (mu4e-get-headers-buffer)) - ((mu4e-current-buffer-type-p 'headers) - (mu4e-get-view-buffer)) - (t (mu4e-error - "This window is neither the headers nor the view window")))) - (other-win (and other-buf (get-buffer-window other-buf)))) - (if (window-live-p other-win) - (select-window other-win) - (mu4e-message "No window to switch to")))) +(defcustom mu4e-mime-type-to-icon-function nil + "Function to return an icon for a MIME type, or nil. +When set, this should be a function that takes a MIME-type string +and returns a string (icon) or nil. + +When nil, `mu4e-mime-type-to-icon' falls back to converting the +MIME type to a file extension and using +`mu4e-file-name-to-icon-function'." + :type '(choice (const :tag "None" nil) function) + :group 'mu4e) + +(defun mu4e-file-name-to-icon (filename) + "Return an icon string for FILENAME, or nil. +Uses `mu4e-file-name-to-icon-function' if set." + (when (and filename mu4e-file-name-to-icon-function) + (funcall mu4e-file-name-to-icon-function filename))) + +(defun mu4e-mime-type-to-icon (mime-type) + "Return an icon string for MIME-TYPE, or nil. +Uses `mu4e-mime-type-to-icon-function' if set; otherwise +falls back to `mu4e-file-name-to-icon' with a dummy filename +derived from the MIME type via `mailcap-mime-type-to-extension'." + (when mime-type + (or (and mu4e-mime-type-to-icon-function + (funcall mu4e-mime-type-to-icon-function mime-type)) + (let ((ext (mailcap-mime-type-to-extension mime-type))) + (when ext + (mu4e-file-name-to-icon + (concat "file." (symbol-name ext)))))))) ;;; Messages, warnings and errors (defun mu4e-format (frm &rest args) @@ -163,6 +123,48 @@ Does a local-exit and does not return." ;;; Reading user input +(defcustom mu4e-completing-read-function #'ido-completing-read + "Function to be used to receive user-input during completion. + +Suggested possible values are: + * `completing-read': Emacs' built-in completion method + * `ido-completing-read': dynamic completion within the minibuffer. + +The function is used in two contexts - +1) directly - for instance in when listing _other_ maildirs + in `mu4e-ask-maildir' +2) if `mu4e-read-option-use-builtin' is nil, it is used + as part of `mu4e-read-option' in many places. + +Set it to `completing-read' when you want to use completion +frameworks such as Helm, Ivy or Vertico. In that case, you +might want to add something like the following in your configuration. + + (setq mu4e-read-option-use-builtin nil + mu4e-completing-read-function \\='completing-read) +." + :type 'function + :options '(completing-read ido-completing-read) + :group 'mu4e) + +(defcustom mu4e-read-option-use-builtin t + "Whether to use mu4e's traditional completion for `mu4e-read-option'. + +If nil, use the value of `mu4e-completing-read-function', integrated +into mu4e. + +Many of the third-party completion frameworks - such as Helm, Ivy +and Vertico - influence `completion-read', so to have mu4e follow +your overall settings, try the equivalent of + + (setq mu4e-read-option-use-builtin nil + mu4e-completing-read-function \\='completing-read) + +Tastes differ, but without any such frameworks, the unaugmented +Emacs `completing-read' is rather Spartan." + :type 'boolean + :group 'mu4e) + (defun mu4e--plist-get (lst prop) "Get PROP from plist LST and raise an error if not present." (or (plist-get lst prop) @@ -322,6 +324,11 @@ Function returns the value (cdr) of the matching cell." ;;; Logging / debugging +(defcustom mu4e-debug nil + "When set to non-nil, log debug information to the mu4e log buffer." + :type 'boolean + :group 'mu4e) + (defconst mu4e--log-max-size 1000000 "Max number of characters to keep around in the log buffer.") (defconst mu4e--log-buffer-name "*mu4e-log*" @@ -527,6 +534,46 @@ Or go to the top level if there is none." ('mu4e-view-mode "(mu4e)Message view") (_ "mu4e")))) +(defcustom mu4e-use-fancy-chars nil + "When set, allow fancy (Unicode) characters for marks/threads. +You can customize the exact fancy characters used with +`mu4e-marks' and various `mu4e-headers-..-mark' and +`mu4e-headers..-prefix' variables." + :type 'boolean + :group 'mu4e) + +;; maybe move the next ones... but they're convenient +;; here because they're needed in multiple buffers. + +(defcustom mu4e-view-auto-mark-as-read t + "Automatically mark messages as read when you read them. +This is the default behavior, but can be turned off, for example +when using a read-only file-system. + +This can also be set to a function; if so, receives a message +plist which should evaluate to nil if the message should *not* be +marked as read-only, or non-nil otherwise." + :type '(choice + boolean + function) + :group 'mu4e-view) + +(defun mu4e-select-other-view () + "Switch between headers view and message view." + (interactive) + (let* ((other-buf + (cond + ((mu4e-current-buffer-type-p 'view) + (mu4e-get-headers-buffer)) + ((mu4e-current-buffer-type-p 'headers) + (mu4e-get-view-buffer)) + (t (mu4e-error + "This window is neither the headers nor the view window")))) + (other-win (and other-buf (get-buffer-window other-buf)))) + (if (window-live-p other-win) + (select-window other-win) + (mu4e-message "No window to switch to")))) + ;;; Emacs bookmarks (defcustom mu4e-emacs-bookmark-policy 'message "The policy to decide what kind of Emacs bookmark to create. diff --git a/mu4e/mu4e-mime-parts.el b/mu4e/mu4e-mime-parts.el index 2a01dee..119e74f 100644 --- a/mu4e/mu4e-mime-parts.el +++ b/mu4e/mu4e-mime-parts.el @@ -176,57 +176,51 @@ Note: this is not compatible with `helm-mode'." :lighter "" :keymap mu4e-view-completion-minor-mode-map) -(defun mu4e--part-annotation (candidate part type longest-filename) - "Calculate the annotation candidates as per annotation. -I.e., `:annotation-function' (see `completion-extra-properties') - -CANDIDATE is the value to annotate. - -PART is the matching MIME-part for the annotation, (as per -`mu4e-view-mime-part'). - -TYPE is the of what to annotate, a symbol, either ATTACHMENT or -MIME-PART. - -LONGEST-FILENAME is the length of the longest filename; this -information' is used for alignment." - (let* ((filename (propertize (or (plist-get part :filename) "") - 'face 'mu4e-header-key-face)) - (mimetype (propertize (or (plist-get part :mime-type) "") - 'face 'mu4e-header-value-face)) - (target (propertize (or (plist-get part :target-dir) "") - 'face 'mu4e-system-face))) - - ;; Sadly, we need too align by hand; this makes some assumptions - ;; such a mono-type font and enough space in the minibuffer; and - ;; mixing values and representation; ideally Emacs would allow - ;; just take some columns and align them (since it knows the display - ;; details). - - (pcase type - ('attachment - ;; in case we're annotating an attachment, the filename is - ;; the candidate (completion), so we don't need it in the - ;; the annotation. We just need to but some space at beginning - ;; for alignment - (concat - (make-string (- (+ longest-filename 2) - (length (format "%s" candidate))) ?\s) - (format "%20s" mimetype) - " " - (format "%s" (concat "-> " target)))) - ('mime-part - ;; when we're annotating a mime-part, the candidate is just a number, - ;; and the filename is part of the annotation. - (concat - " " - filename - (make-string (- (+ longest-filename 2) - (length filename)) ?\s) - (format "%20s" mimetype) - " " - (format "%s" (concat "-> " target)))) - (_ (mu4e-error "Unsupported annotation type %s" type))))) +(defun mu4e--part-affixation (candidates-alist type longest-filename completions) + "Calculate the affixation for COMPLETIONS. +I.e., `:affixation-function' (see `completion-extra-properties'). + +Returns a list of (CANDIDATE PREFIX SUFFIX) triples. + +CANDIDATES-ALIST is the full alist of candidates (id . part). +TYPE is what to annotate, a symbol, either ATTACHMENT or MIME-PART. +LONGEST-FILENAME is the length of the longest filename; used for alignment. +COMPLETIONS is the list of completion strings to affixate." + (mapcar + (lambda (candidate) + (let* ((part (cdr-safe (assoc candidate candidates-alist))) + (raw-filename (or (plist-get part :filename) "")) + (filename (propertize raw-filename + 'face 'mu4e-header-key-face)) + (mimetype (propertize (or (plist-get part :mime-type) "") + 'face 'mu4e-header-value-face)) + (target (propertize (or (plist-get part :target-dir) "") + 'face 'mu4e-system-face)) + (icon (or (mu4e-mime-type-to-icon + (plist-get part :mime-type)) + (mu4e-file-name-to-icon raw-filename))) + (prefix (if icon (concat icon " ") "")) + (suffix + (pcase type + ('attachment + (concat + (make-string (- (+ longest-filename 2) + (length (format "%s" candidate))) ?\s) + (format "%20s" mimetype) + " " + (format "%s" (concat "-> " target)))) + ('mime-part + (concat + " " + filename + (make-string (- (+ longest-filename 2) + (length filename)) ?\s) + (format "%20s" mimetype) + " " + (format "%s" (concat "-> " target)))) + (_ (mu4e-error "Unsupported annotation type %s" type))))) + (list candidate prefix suffix))) + completions)) (defvar helm-comp-read-use-marked) (defun mu4e--completing-read-real (prompt candidates multi) @@ -257,7 +251,7 @@ the current message. - PROMPT is a string informing the user what to complete - CANDIDATES is an alist of candidates of the form (id . part) -- TYPE is the annotation type to uses as per `mu4e--part-annotation'. +- TYPE is the annotation type to use as per `mu4e--part-affixation'. Optionally, - MULTI if t, allow for completing _multiple_ candidates." (cl-assert candidates) @@ -265,20 +259,23 @@ Optionally, (seq-map (lambda (c) (length (plist-get (cdr c) :filename))) candidates))) - (annotation-func (lambda (candidate) - (mu4e--part-annotation candidate - (cdr-safe - (assoc candidate candidates)) - type longest-filename))) + (affixation-func (lambda (completions) + (mu4e--part-affixation candidates type + longest-filename + completions))) + (table (lambda (string pred action) + (if (eq action 'metadata) + `(metadata + (category . mime-part) + (affixation-function . ,affixation-func)) + (complete-with-action action candidates string pred)))) (completion-extra-properties - `(;; :affixation-function requires emacs 28 - :annotation-function ,annotation-func - :exit-function (lambda (_a _b) (setq mu4e--completions-table nil))))) + `(:exit-function (lambda (_a _b) (setq mu4e--completions-table nil))))) (setq mu4e--completions-table candidates) (minibuffer-with-setup-hook (lambda () (mu4e-view-completion-minor-mode)) - (mu4e--completing-read-real prompt candidates multi)))) + (mu4e--completing-read-real prompt table multi)))) (defun mu4e-view-save-attachments (&optional ask-dir) "Save files from the current view buffer. |
