aboutsummaryrefslogtreecommitdiff
path: root/.emacs.d
diff options
context:
space:
mode:
authorRaul Benencia <id@rbenencia.name>2026-04-20 08:24:33 -0700
committerRaul Benencia <id@rbenencia.name>2026-04-20 08:24:33 -0700
commit0fd9047cfffa9732e8cccc14b855e651eb7fb0a1 (patch)
treed4dd6ec334257c16affaa6885fc73dbc1e9f66a4 /.emacs.d
parentfab6a5fbf53cb22a027cfeaa65279c160c96820f (diff)
emacs: notmuch qol improvements
Diffstat (limited to '.emacs.d')
-rw-r--r--.emacs.d/rul-emacs.org85
-rw-r--r--.emacs.d/rul-lisp/packages/rul-mail.el105
2 files changed, 127 insertions, 63 deletions
diff --git a/.emacs.d/rul-emacs.org b/.emacs.d/rul-emacs.org
index dd944c4..15175e9 100644
--- a/.emacs.d/rul-emacs.org
+++ b/.emacs.d/rul-emacs.org
@@ -683,6 +683,13 @@ Emacs can act as Mail User Agent. My preferred package for this is
;; Use sender to find GPG key.
(setq mml-secure-openpgp-sign-with-sender t)
+;; Keep HTML mail readable without sender-defined styling.
+(setq shr-use-colors nil
+ shr-use-fonts nil)
+
+(with-eval-after-load 'shr
+ (set-face-attribute 'shr-link nil :inherit 'default :underline t))
+
(use-package notmuch
:ensure t
:config
@@ -698,6 +705,53 @@ Emacs can act as Mail User Agent. My preferred package for this is
(setq notmuch-draft-folder "current/Drafts")
+ (defvar-local rul/notmuch-show-refreshing-for-olivetti nil)
+
+ (defun rul/notmuch-show-enable-olivetti ()
+ "Enable Olivetti before notmuch renders message HTML."
+ (setq-local olivetti-body-width 100
+ shr-width nil
+ shr-max-width olivetti-body-width)
+ (olivetti-mode 1))
+
+ (defun rul/notmuch-show-refresh-after-olivetti ()
+ "Refresh an already-rendered notmuch buffer after Olivetti changes width."
+ (when (and (derived-mode-p 'notmuch-show-mode)
+ olivetti-mode
+ notmuch-show-thread-id
+ (not rul/notmuch-show-refreshing-for-olivetti))
+ (setq rul/notmuch-show-refreshing-for-olivetti t)
+ (run-at-time
+ 0 nil
+ (lambda (buffer)
+ (when (buffer-live-p buffer)
+ (with-current-buffer buffer
+ (unwind-protect
+ (when (and (derived-mode-p 'notmuch-show-mode)
+ olivetti-mode
+ notmuch-show-thread-id)
+ (notmuch-refresh-this-buffer))
+ (setq rul/notmuch-show-refreshing-for-olivetti nil)))))
+ (current-buffer))))
+
+ (add-hook 'notmuch-show-mode-hook #'rul/notmuch-show-enable-olivetti)
+
+ (with-eval-after-load 'olivetti
+ (add-hook 'olivetti-mode-on-hook
+ #'rul/notmuch-show-refresh-after-olivetti
+ t))
+
+ (defun rul/notmuch-inline-override-images (types)
+ "Treat standalone image MIME parts as attachments in notmuch."
+ (if (member "image/.*" types)
+ types
+ (append types '("image/.*"))))
+
+ (unless (advice-member-p #'rul/notmuch-inline-override-images
+ 'notmuch--inline-override-types)
+ (advice-add 'notmuch--inline-override-types :filter-return
+ #'rul/notmuch-inline-override-images))
+
;; Keymaps
(defun rul/capture-mail()
"Capture mail to org mode."
@@ -731,20 +785,27 @@ Emacs can act as Mail User Agent. My preferred package for this is
(notmuch-search-tag (list "+spam" "-inbox" "-unread") beg end)))
;; Archive
+ ;; Stock `a` applies notmuch-archive-tags but doesn't re-run the search, so
+ ;; the thread stays visible in the tag:inbox buffer with stale results. Wrap
+ ;; both `a` and `A` to refresh the buffer so the thread drops out of view.
(setq notmuch-archive-tags (list "-inbox" "+archive"))
- (define-key notmuch-show-mode-map "A"
- (lambda ()
- "archive"
- (interactive)
- (notmuch-show-tag (list "+archive" "-inbox" "-unread"))
- (notmuch-refresh-this-buffer)))
- (define-key notmuch-search-mode-map "A"
- (lambda (&optional beg end)
- "archive thread"
- (interactive (notmuch-search-interactive-region))
- (notmuch-search-tag (list "+archive" "-inbox" "-unread") beg end)
- (notmuch-refresh-this-buffer)))
+ (defun rul/notmuch-search-archive (&optional unarchive beg end)
+ "Archive threads and refresh so they drop out of the inbox view."
+ (interactive (cons current-prefix-arg (notmuch-search-interactive-region)))
+ (notmuch-search-archive-thread unarchive beg end)
+ (notmuch-refresh-this-buffer))
+
+ (defun rul/notmuch-show-archive ()
+ "Archive the current message with +archive -inbox -unread and refresh."
+ (interactive)
+ (notmuch-show-tag (list "+archive" "-inbox" "-unread"))
+ (notmuch-refresh-this-buffer))
+
+ (define-key notmuch-search-mode-map "a" 'rul/notmuch-search-archive)
+ (define-key notmuch-search-mode-map "A" 'rul/notmuch-search-archive)
+ (define-key notmuch-show-mode-map "a" 'rul/notmuch-show-archive)
+ (define-key notmuch-show-mode-map "A" 'rul/notmuch-show-archive)
;; Mark as read
(define-key notmuch-search-mode-map "r"
diff --git a/.emacs.d/rul-lisp/packages/rul-mail.el b/.emacs.d/rul-lisp/packages/rul-mail.el
index a16614e..12846a0 100644
--- a/.emacs.d/rul-lisp/packages/rul-mail.el
+++ b/.emacs.d/rul-lisp/packages/rul-mail.el
@@ -4,6 +4,13 @@
;; Use sender to find GPG key.
(setq mml-secure-openpgp-sign-with-sender t)
+;; Keep HTML mail readable without sender-defined styling.
+(setq shr-use-colors nil
+ shr-use-fonts nil)
+
+(with-eval-after-load 'shr
+ (set-face-attribute 'shr-link nil :inherit 'default :underline t))
+
(use-package notmuch
:ensure t
:config
@@ -19,6 +26,53 @@
(setq notmuch-draft-folder "current/Drafts")
+ (defvar-local rul/notmuch-show-refreshing-for-olivetti nil)
+
+ (defun rul/notmuch-show-enable-olivetti ()
+ "Enable Olivetti before notmuch renders message HTML."
+ (setq-local olivetti-body-width 100
+ shr-width nil
+ shr-max-width olivetti-body-width)
+ (olivetti-mode 1))
+
+ (defun rul/notmuch-show-refresh-after-olivetti ()
+ "Refresh an already-rendered notmuch buffer after Olivetti changes width."
+ (when (and (derived-mode-p 'notmuch-show-mode)
+ olivetti-mode
+ notmuch-show-thread-id
+ (not rul/notmuch-show-refreshing-for-olivetti))
+ (setq rul/notmuch-show-refreshing-for-olivetti t)
+ (run-at-time
+ 0 nil
+ (lambda (buffer)
+ (when (buffer-live-p buffer)
+ (with-current-buffer buffer
+ (unwind-protect
+ (when (and (derived-mode-p 'notmuch-show-mode)
+ olivetti-mode
+ notmuch-show-thread-id)
+ (notmuch-refresh-this-buffer))
+ (setq rul/notmuch-show-refreshing-for-olivetti nil)))))
+ (current-buffer))))
+
+ (add-hook 'notmuch-show-mode-hook #'rul/notmuch-show-enable-olivetti)
+
+ (with-eval-after-load 'olivetti
+ (add-hook 'olivetti-mode-on-hook
+ #'rul/notmuch-show-refresh-after-olivetti
+ t))
+
+ (defun rul/notmuch-inline-override-images (types)
+ "Treat standalone image MIME parts as attachments in notmuch."
+ (if (member "image/.*" types)
+ types
+ (append types '("image/.*"))))
+
+ (unless (advice-member-p #'rul/notmuch-inline-override-images
+ 'notmuch--inline-override-types)
+ (advice-add 'notmuch--inline-override-types :filter-return
+ #'rul/notmuch-inline-override-images))
+
;; Keymaps
(defun rul/capture-mail()
"Capture mail to org mode."
@@ -51,57 +105,6 @@
(interactive (notmuch-search-interactive-region))
(notmuch-search-tag (list "+spam" "-inbox" "-unread") beg end)))
- ;; Unspam: restore false-positives back to INBOX. Unlike archive, the file
- ;; move has to happen inline — the hook's path:current/Spam/** rule would
- ;; otherwise re-tag +spam on the next post-new, reverting the user's action.
- ;; Move the file BEFORE changing tags so an interleaved post-new from mbsync
- ;; can't re-apply +spam between our tag change and our rename.
- (defun rul/notmuch-move-to-inbox (query)
- "Move files matching QUERY that live under spam/trash maildirs into
-~/mail/INBOX/cur/ with fresh uuid names. Reindex if any moved."
- (let ((inbox-cur (expand-file-name "~/mail/INBOX/cur/"))
- (spam-paths "(path:current/Spam/** or path:current/spam/** or path:current/Trash/** or path:current/trash/**)")
- (count 0))
- ;; Spam/trash are in search.exclude_tags, so include excluded files here.
- (dolist (f (process-lines "notmuch" "search" "--exclude=false" "--output=files"
- (concat "(" query ") and " spam-paths)))
- (when (file-exists-p f)
- (let* ((base (file-name-nondirectory f))
- (colon (cl-position ?: base :from-end t))
- (suffix (if colon (substring base colon) ""))
- (uuid (string-trim (shell-command-to-string "uuidgen"))))
- (rename-file f (concat inbox-cur uuid suffix))
- (cl-incf count))))
- (when (> count 0)
- (call-process "notmuch" nil nil nil "new" "--no-hooks"))
- count))
-
- (defun rul/notmuch-search-unspam (&optional beg end)
- "Restore threads from spam/trash: move files back to INBOX first (so a
-racing post-new can't re-tag +spam based on path), then tag -spam -trash
--killed +inbox +unread, then refresh."
- (interactive (notmuch-search-interactive-region))
- (let ((tids (save-excursion
- (goto-char beg)
- (cl-loop while (< (point) end)
- collect (notmuch-search-find-thread-id)
- do (forward-line 1)))))
- (dolist (tid tids)
- (rul/notmuch-move-to-inbox (concat "thread:" tid)))
- (notmuch-search-tag (list "-spam" "-trash" "-killed" "+inbox" "+unread") beg end))
- (notmuch-refresh-this-buffer))
-
- (defun rul/notmuch-show-unspam ()
- "Restore current message from spam/trash back to INBOX."
- (interactive)
- (let ((mid (notmuch-show-get-message-id)))
- (rul/notmuch-move-to-inbox mid)
- (notmuch-show-tag (list "-spam" "-trash" "-killed" "+inbox" "+unread")))
- (notmuch-refresh-this-buffer))
-
- (define-key notmuch-search-mode-map "U" 'rul/notmuch-search-unspam)
- (define-key notmuch-show-mode-map "U" 'rul/notmuch-show-unspam)
-
;; Archive
;; Stock `a` applies notmuch-archive-tags but doesn't re-run the search, so
;; the thread stays visible in the tag:inbox buffer with stale results. Wrap
nihil fit ex nihilo