summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDirk-Jan C. Binnema <djcb@djcbsoftware.nl>2026-04-08 20:45:32 +0300
committerDirk-Jan C. Binnema <djcb@djcbsoftware.nl>2026-04-10 20:07:42 +0300
commite870b2d3302925187f06cb2ef4cb3b4c9c5f9acf (patch)
tree9434dd0e28c3073654eb876d675ba71928abbfd3
parent397c7172fc48b8b792b1ba34af28a803482a2097 (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)
-rw-r--r--NEWS.org5
-rw-r--r--mu4e/mu4e-helpers.el211
-rw-r--r--mu4e/mu4e-mime-parts.el119
3 files changed, 192 insertions, 143 deletions
diff --git a/NEWS.org b/NEWS.org
index bcae3e9..e61cc6d 100644
--- a/NEWS.org
+++ b/NEWS.org
@@ -23,6 +23,11 @@
- ~mu4e-compose-post-hook~ only runs once per message (buffer) (1.14.1)
+ - You can get icons for MIME-types when using ~mu4e-view-mime-part-action~, by
+ configuring ~mu4e-file-name-to-icon-function~ (see docstring). E.g. when you
+ have the "Nerd Icons" package, you can set this to
+ ~nerd-icons-icon-for-file~. (1.14.1)
+
*** scm
- added new ~--eval~ command-line option, so you can do e.g.
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.