Skip to content

Instantly share code, notes, and snippets.

@rougier
Created September 26, 2021 09:37
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rougier/5886fe306fc5d43a0be94bd1a090bf58 to your computer and use it in GitHub Desktop.
Save rougier/5886fe306fc5d43a0be94bd1a090bf58 to your computer and use it in GitHub Desktop.
Emacs / Mu4e configuration for threaded view (with folding)
;; mu4e setup -*- lexical-binding: t; -*-
;; This file is not part of GNU Emacs.
;;
;; This program is free software: you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation, either version 3 of the
;; License, or (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>
(require 'ts)
(require 'mu4e)
(require 'svg-lib)
(setq mu4e-headers-thread-root-prefix '("" . "")
mu4e-headers-thread-first-child-prefix '("| " . "| ")
mu4e-headers-thread-child-prefix '("| " . "| ")
mu4e-headers-thread-last-child-prefix '("| " . "| ")
mu4e-headers-thread-connection-prefix '("| " . "| ")
mu4e-headers-thread-blank-prefix '("" . "")
mu4e-headers-thread-orphan-prefix '("" . "")
mu4e-headers-thread-single-orphan-prefix '("| " . "| ")
mu4e-headers-thread-duplicate-prefix '("=" . "="))
(defun mu4e-mockup-string-fit (string width)
"Make a string to fit the given width"
(cond ((> (length string) width)
(format "%s…" (substring string 0 (- width 1))))
(t string)))
(defun mu4e-mockup-compose (left right &optional width)
"Concatenate two strings and make them fit the given width (with
at least 3 spaces between them)"
(let* ((window (get-buffer-window (current-buffer)))
(width (cond ((and width (< width 0)) (+ (window-body-width) width))
((and width (> width 0)) width)
(t (window-body-width))))
(left (mu4e-mockup-string-fit left (- width (length right) 3)))
(spacing (- width (length left) (length right))))
(concat left (make-string spacing ?\ ) right)))
(defun mu4e-mockup-tag (tag &optional face stroke)
(let* ((face (or face 'default-face)))
(propertize (concat tag " ") ;; The additional space compensates for the padding
'mouse-face 'highlight
'help-echo (concat "tag:" tag)
'display (svg-lib-tag tag nil
:padding 1
:margin 0
:stroke (or stroke 2)
:radius 4
:foreground (face-foreground face nil 'default)
:background (face-background face nil 'default)))))
(defun mu4e-mockup-get-account (msg)
(let* ((maildir (mu4e-message-field msg :maildir))
(maildir (substring maildir 1)))
(nth 0 (split-string maildir "/"))))
(defun mu4e-mockup-get-maildir (msg)
(let* ((maildir (mu4e-message-field msg :maildir))
(maildir (substring maildir 1)))
(nth 0 (reverse (split-string maildir "/")))))
(defun mu4e-mockup-get-mailbox (msg)
(format "%s - %s" (mu4e-get-account msg) (mu4e-get-maildir msg)))
(defun mu4e-mockup-is-today (date)
(let ((today (ts-now)))
(and (= (ts-year date) (ts-year today))
(= (ts-month date) (ts-month today))
(= (ts-day date) (ts-day today)))))
(defun mu4e-mockup-is-yesterday (date)
(let ((yesterday (ts-dec 'day 1 (ts-now))))
(and (= (ts-year date) (ts-year yesterday))
(= (ts-month date) (ts-month yesterday))
(= (ts-day date) (ts-day yesterday)))))
(defun mu4e-mockup-relative-date (msg)
(let* ((unread (memq 'unread (mu4e-message-field msg :flags)))
(date (ts-parse (format-time-string "%F %T"
(mu4e-msg-field msg :date))))
(now (ts-now))
(days (plist-get (ts-human-duration (ts-difference now date)) :days))
(delta (ts-diff now date)))
(cond ((< delta (* 3 60)) "now")
((< delta (* 60 60)) (format "%d minutes ago" (/ delta 60)))
((< delta (* 6 60 60)) (format "%d hours ago" (/ delta 3600)))
((mu4e-mockup-is-today date) (ts-format "Today at %H:%M" date))
((mu4e-mockup-is-yesterday date) (ts-format "Yesterday"))
((< delta (* 4 24 60 60)) (format "%d days ago" (+ days 1)))
(t (ts-format "%d %b %Y" date)))))
(defun mu4e-mockup-sender (msg &optional root)
(let ((addr (cdr-safe (car-safe (mu4e-message-field msg :from)))))
(if (and addr (mu4e-personal-address-p addr))
(concat (mu4e-mockup-tag "To" (if root 'nano-salient 'nano-faded) 2)
" "
(mu4e~headers-contact-str (mu4e-message-field msg :to)))
(mu4e~headers-contact-str (mu4e-message-field msg :from)))))
(defun mu4e-mockup-headers (msg)
(let* ((thread (mu4e-message-field msg :thread))
(prefix (mu4e~headers-thread-prefix thread))
(root (= (length prefix) 0))
(child (not root))
(prefix (if child (substring prefix 2) prefix))
(unread (memq 'unread (mu4e-message-field msg :flags)))
(attach (memq 'attach (mu4e-message-field msg :flags)))
(flagged (memq 'flagged (mu4e-message-field msg :flags)))
(replied (memq 'replied (mu4e-message-field msg :flags)))
(draft (memq 'draft (mu4e-message-field msg :flags)))
(date (mu4e-mockup-relative-date msg))
(tags (mu4e-message-field msg :tags))
(maildir (mu4e-mockup-get-maildir msg))
(sender (mu4e-mockup-sender msg))
(related mu4e-headers-include-related)
(subject (mu4e-message-field msg :subject)))
(cond (related ;; Threaded view
(let ((left (concat
(if child " ")
(cond ((and root unread)
(propertize "●" 'face 'nano-salient ))
((and child unread)
(propertize "●" 'face 'nano-faded))
(t
(propertize " " 'face 'nano-default)))
(if root
(propertize " ") ;; 'display '(raise 0.0))
" ")
(propertize prefix 'face 'nano-faded)
(if child
(concat (propertize (mu4e-mockup-sender msg)'face 'nano-faded)
(if (string= maildir "inbox")
(concat " " (mu4e-mockup-tag "INBOX" 'nano-faded 2)))
(if tags
(concat
" "
(mapconcat #'(lambda (tag) (mu4e-mockup-tag tag 'nano-popout-i 0)) tags " ")
" ")))
(propertize (mu4e-mockup-sender msg t) 'face '(:inherit (nano-salient nano-strong))))
(if root
(concat (if (string= maildir "inbox")
(concat " " (mu4e-mockup-tag "INBOX" 'nano-salient-i 0)))
" — "
(if tags
(concat
(mapconcat #'(lambda (tag) (mu4e-mockup-tag tag 'nano-popout-i 0)) tags " ")
" "))
(propertize subject 'face 'nano-default)))))
(right (concat
(propertize date 'face 'nano-faded)
" "
(cond (flagged (propertize "" 'face '(:inherit nano-popout :height 120)))
(draft (propertize "" 'face '(:inherit nano-faded :height 120)))
(attach (propertize "" 'face '(:inherit nano-faded :height 120)))
(t " ")))))
(mu4e-mockup-compose left right -3)))
(t ;; Compact view
(let ((left (concat
(cond (unread
(propertize "● " 'face 'nano-salient))
(t
(propertize " " 'face 'nano-faded)))
(propertize sender 'face '(:inherit ( nano-salient nano-strong)))
" — "
(propertize subject 'face 'nano-default)))
(right (concat
(propertize date 'face 'nano-faded)
" "
(cond (flagged (propertize "" 'face '(:inherit nano-popout :height 120)))
(draft (propertize "" 'face '(:inherit nano-faded :height 120)))
(attach (propertize "" 'face '(:inherit nano-faded :height 120)))
(t " ")))))
(mu4e-mockup-compose left right -3))))))
(add-to-list 'mu4e-header-info-custom
'(:mockup . (:name "Mockup"
:shortname ""
:function mu4e-mockup-headers)))
(setq mu4e-headers-fields '((:mockup . nil)))
(defun mu4e-mockup-is-unfolded-child ()
"Check if the line at point is an unfolded thread child.
This is detected by the presence of non-breaking space."
(interactive)
(save-excursion
(beginning-of-line)
(and (not (mu4e-mockup-is-folded-children))
(search-forward " " (line-end-position) t))))
(defun mu4e-mockup-is-folded-children ()
"Check if the line at point is a folded thread.
This is detected by the presence of an overlay with value 'overlay."
(interactive)
(save-excursion
(beginning-of-line)
(let ((overlays (overlays-at (point)))
(found nil))
(while overlays
(if (overlay-get (car overlays) 'overlay)
(setq found t))
(setq overlays (cdr overlays)))
found)))
(defun mu4e-mockup-is-root ()
"Check if the line at point is a thread root."
(interactive)
(and (not (mu4e-mockup-is-unfolded-child))
(not (mu4e-mockup-is-folded-children))))
(defun mu4e-mockup-is-unread ()
"Check if the line at point is an unread message."
(save-excursion
(beginning-of-line)
(search-forward "●" (line-end-position) t)))
(defun mu4e-mockup-thread-toggle ()
"Toggle thread at point."
(interactive)
(save-excursion
(beginning-of-line)
(if (mu4e-mockup-is-root)
(forward-line))
(cond ((mu4e-mockup-is-folded-children)
(mu4e-mockup-thread-unfold))
((mu4e-mockup-is-unfolded-child)
(mu4e-mockup-thread-fold)))))
(defun mu4e-mockup-thread-unfold ()
"Unfold thread at point."
(interactive)
(if (mu4e-mockup-is-root)
(forward-line))
(let ((overlays (overlays-at (point))))
(while overlays
(let ((overlay (car overlays)))
(if (overlay-get overlay 'overlay)
(delete-overlay (overlay-get overlay 'overlay))))
(setq overlays (cdr overlays)))))
(defun mu4e-mockup-thread-fold ()
"Fold thread at point."
(interactive)
;; Move to thread start
(beginning-of-line)
(while (and (> (point) (point-min))
(mu4e-mockup-is-unfolded-child))
(forward-line -1))
(forward-line +1)
;; Hide all children, count them and count unread
(beginning-of-line)
(let ((start (point))
(end (+ (point) 1))
(unread 0)
(count 0))
(while (and (< (point) (point-max))
(mu4e-mockup-is-unfolded-child))
;; Count unread
(beginning-of-line)
(if (mu4e-mockup-is-unread)
(setq unread (+ unread 1)))
;; Count thread
(setq count (+ count 1))
;; Set new end for the overlay
(setq end (+ (line-end-position) 1))
(forward-line +1)
(beginning-of-line))
;; Add overlay
(let* ((overlay (make-overlay start (- end 1)))
(face (if (> unread 0)
'(:inherit (nano-faded nano-strong))
'(:inherit nano-faded)))
(text (if (> unread 0)
(format " ● --- %d hidden messages (%d unread) --- " count unread)
(format " --- %d hidden messages --- " count))))
;; No overlay if only 1 child
(when (> count 1)
(overlay-put overlay 'display (propertize text 'face face))
(overlay-put overlay 'overlay overlay)))))
(defun mu4e-mockup-thread-fold-all ()
"Fold all threads independently of their current state."
(interactive)
(save-excursion
(goto-char (point-min))
(while (not (eobp))
(mu4e-mockup-thread-fold)
(forward-line))))
(defun mu4e-mockup-thread-unfold-all ()
"Unfold all threads, independently of their current state."
(interactive)
(save-excursion
(goto-char (point-min))
(while (not (eobp))
(mu4e-mockup-thread-unfold)
(forward-line))))
(defvar mu4e-mockup-thread-folding-state nil
"Global folding state")
(defun mu4e-mockup-thread-toggle-all ()
"Toggle global folding state."
(interactive)
(when mu4e-headers-include-related
(setq mu4e-mockup-thread-folding-state (not mu4e-mockup-thread-folding-state))
(mu4e-mockup-thread-apply-folding)))
(defun mu4e-mockup-thread-apply-folding ()
"Apply folding according to the global folding state."
(interactive)
(if mu4e-mockup-thread-folding-state
(mu4e-mockup-thread-fold-all)
(mu4e-mockup-thread-unfold-all)))
(add-hook 'mu4e-headers-found-hook #'mu4e-mockup-thread-apply-folding)
(define-key mu4e-headers-mode-map (kbd "TAB") 'mu4e-mockup-thread-toggle)
(define-key mu4e-headers-mode-map (kbd "<backtab>") 'mu4e-mockup-thread-toggle-all)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment