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:
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
Reference in New Issue
Block a user