Created
September 26, 2021 09:37
-
-
Save rougier/5886fe306fc5d43a0be94bd1a090bf58 to your computer and use it in GitHub Desktop.
Emacs / Mu4e configuration for threaded view (with folding)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; 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