diff options
| author | Raul Benencia <id@rbenencia.name> | 2026-04-20 08:24:33 -0700 |
|---|---|---|
| committer | Raul Benencia <id@rbenencia.name> | 2026-04-20 08:24:33 -0700 |
| commit | 0fd9047cfffa9732e8cccc14b855e651eb7fb0a1 (patch) | |
| tree | d4dd6ec334257c16affaa6885fc73dbc1e9f66a4 | |
| parent | fab6a5fbf53cb22a027cfeaa65279c160c96820f (diff) | |
emacs: notmuch qol improvements
| -rw-r--r-- | .emacs.d/rul-emacs.org | 85 | ||||
| -rw-r--r-- | .emacs.d/rul-lisp/packages/rul-mail.el | 105 |
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 |
