From 1dc02a72ac851ddea7cea539eb003b4033d8bba6 Mon Sep 17 00:00:00 2001 From: "Dirk-Jan C. Binnema" Date: Wed, 8 Apr 2026 20:45:32 +0300 Subject: [PATCH] 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) --- NEWS.org | 5 + mu4e/mu4e-helpers.el | 211 ++++++++++++++++++++++++---------------- mu4e/mu4e-mime-parts.el | 115 +++++++++++----------- 3 files changed, 190 insertions(+), 141 deletions(-) diff --git a/NEWS.org b/NEWS.org index f7096ec5..786470e5 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 17705624..1925cbc6 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 ;; Maintainer: Dirk-Jan C. Binnema @@ -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. diff --git a/mu4e/mu4e-mime-parts.el b/mu4e/mu4e-mime-parts.el index 2a01dee0..119e74f8 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') +(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.