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)
This commit is contained in:
Dirk-Jan C. Binnema
2026-04-08 20:45:32 +03:00
committed by Seth Ladygo
parent 4ed2663117
commit 1dc02a72ac
3 changed files with 190 additions and 141 deletions

View File

@ -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,14 +35,94 @@
(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
;;; Icons
(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.
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:
(lambda (name)
(nerd-icons-icon-for-file name :height 1.0)))"
:type '(choice (const :tag "None" nil) function)
:group 'mu4e)
(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)
"Create [mu4e]-prefixed string based on format FRM and ARGS."
(concat
"[" (propertize "mu4e" 'face 'mu4e-title-face) "] "
(apply 'format frm
(mapcar (lambda (x)
(if (stringp x)
(decode-coding-string x 'utf-8)
x))
args))))
(defun mu4e-message (frm &rest args)
"Display FRM with ARGS like `message' in mu4e style.
If we're waiting for user-input or if there's some message in the
echo area, don't show anything."
(unless (or (active-minibuffer-window))
(message "%s" (apply 'mu4e-format frm args))))
(declare-function mu4e~loading-close "mu4e-headers")
(defun mu4e-error (frm &rest args)
"Display an error with FRM and ARGS like `mu4e-message'.
Create [mu4e]-prefixed error based on format FRM and ARGS. Does a
local-exit and does not return, and raises a
debuggable (backtrace) error."
(mu4e-log 'error (apply 'mu4e-format frm args))
(error "%s" (apply 'mu4e-format frm args)))
(defun mu4e-warn (frm &rest args)
"Create [mu4e]-prefixed warning based on format FRM and ARGS.
Does a local-exit and does not return."
(mu4e-log 'error (apply 'mu4e-format frm args))
(user-error "%s" (apply 'mu4e-format frm args)))
;;; Reading user input
(defcustom mu4e-completing-read-function #'ido-completing-read
"Function to be used to receive user-input during completion.
@ -85,84 +165,6 @@ Emacs `completing-read' is rather Spartan."
:type 'boolean
:group '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"))))
;;; Messages, warnings and errors
(defun mu4e-format (frm &rest args)
"Create [mu4e]-prefixed string based on format FRM and ARGS."
(concat
"[" (propertize "mu4e" 'face 'mu4e-title-face) "] "
(apply 'format frm
(mapcar (lambda (x)
(if (stringp x)
(decode-coding-string x 'utf-8)
x))
args))))
(defun mu4e-message (frm &rest args)
"Display FRM with ARGS like `message' in mu4e style.
If we're waiting for user-input or if there's some message in the
echo area, don't show anything."
(unless (or (active-minibuffer-window))
(message "%s" (apply 'mu4e-format frm args))))
(declare-function mu4e~loading-close "mu4e-headers")
(defun mu4e-error (frm &rest args)
"Display an error with FRM and ARGS like `mu4e-message'.
Create [mu4e]-prefixed error based on format FRM and ARGS. Does a
local-exit and does not return, and raises a
debuggable (backtrace) error."
(mu4e-log 'error (apply 'mu4e-format frm args))
(error "%s" (apply 'mu4e-format frm args)))
(defun mu4e-warn (frm &rest args)
"Create [mu4e]-prefixed warning based on format FRM and ARGS.
Does a local-exit and does not return."
(mu4e-log 'error (apply 'mu4e-format frm args))
(user-error "%s" (apply 'mu4e-format frm args)))
;;; Reading user input
(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.

View File

@ -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')
(defun mu4e--part-affixation (candidates-alist type longest-filename completions)
"Calculate the affixation for COMPLETIONS.
I.e., `:affixation-function' (see `completion-extra-properties').
CANDIDATE is the value to annotate.
Returns a list of (CANDIDATE PREFIX SUFFIX) triples.
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)))))
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.