#+title: Gnus #+PROPERTY: header-args :tangle ~/.emacs.d/gnus.el :comments yes :results silent #+auto_tangle: t * Feature switching vars #+begin_src emacs-lisp (defvar jao-gnus-use-local-imap t) (defvar jao-gnus-use-leafnode t) (defvar jao-gnus-use-gandi-imap nil) (defvar jao-gnus-use-pm-imap nil) (defvar jao-gnus-use-gmane nil) (defvar jao-gnus-use-nnml nil) (defvar jao-gnus-use-maildirs nil) #+end_src * Startup and kill #+begin_src emacs-lisp ;;;;; close gnus when closing emacs, but ask when exiting (setq gnus-interactive-exit t) (defun jao-gnus-started-hook () (add-hook 'before-kill-emacs-hook 'gnus-group-exit)) (add-hook 'gnus-started-hook 'jao-gnus-started-hook) (defun jao-gnus-after-exiting-hook () (remove-hook 'before-kill-emacs-hook 'gnus-group-exit)) (add-hook 'gnus-after-exiting-gnus-hook 'jao-gnus-after-exiting-hook) ;; define a wrapper around the save-buffers-kill-emacs ;; to run the new hook before: (defadvice save-buffers-kill-emacs (before my-save-buffers-kill-emacs activate) "Install hook when emacs exits before emacs asks to save this and that." (run-hooks 'before-kill-emacs-hook)) #+end_src * Directories #+begin_src emacs-lisp (defun jao-gnus-dir (dir) (expand-file-name dir gnus-home-directory)) (setq smtpmail-queue-dir (jao-gnus-dir "Mail/queued-mail/")) (setq mail-source-directory (jao-gnus-dir "Mail/") message-directory (jao-gnus-dir "Mail/")) (setq gnus-default-directory (expand-file-name "~") gnus-startup-file (jao-gnus-dir "newsrc") gnus-agent-directory (jao-gnus-dir "News/agent") gnus-home-score-file (jao-gnus-dir "scores") gnus-article-save-directory (jao-gnus-dir "saved/") nntp-authinfo-file (jao-gnus-dir "authinfo") nnmail-message-id-cache-file (jao-gnus-dir "nnmail-cache") nndraft-directory (jao-gnus-dir "drafts") nnrss-directory (jao-gnus-dir "rss")) #+end_src * Looks *** Verbosity #+begin_src emacs-lisp (setq gnus-verbose 5) #+end_src *** Geometry #+begin_src emacs-lisp ;;; geometry: (defvar jao-gnus-use-three-panes t) (setq gnus-use-trees nil gnus-generate-tree-function 'gnus-generate-horizontal-tree gnus-tree-minimize-window nil) (when jao-gnus-use-three-panes (let ((side-bar '(vertical 1.0 ("inbox.org" 0.4) ("*Org Agenda*" 1.0) ("*Calendar*" 8))) (wide-len 190)) (gnus-add-configuration `(article (horizontal 1.0 (vertical 60 (group 1.0)) (vertical 130 (summary 0.25 point) (article 1.0)) ,side-bar))) (gnus-add-configuration `(group (horizontal 1.0 (group ,wide-len point) ,side-bar))) (gnus-add-configuration `(message (horizontal 1.0 (message ,wide-len point) ,side-bar))) (gnus-add-configuration `(reply-yank (horizontal 1.0 (message ,wide-len point) ,side-bar))) (gnus-add-configuration `(summary (horizontal 1.0 (vertical 60 (group 1.0)) (vertical 130 (summary 1.0 point)) ,side-bar))) (gnus-add-configuration `(reply (horizontal 1.0 (message 90 point) (article 100) ,side-bar))))) #+end_src *** No blue icon #+begin_src emacs-lisp ;; (defalias 'gnus-mode-line-buffer-identification 'identity) (advice-add 'gnus-mode-line-buffer-identification :override #'identity) (setq gnus-mode-line-image-cache nil) #+end_src * Search [[info:gnus#Searching][info:gnus#Searching]] #+begin_src emacs-lisp (setq gnus-search-use-parsed-queries t jao-gnus-search-prefix (expand-file-name "~/var/mail/")) (defun jao-gnus-search-engine (engine) `(gnus-search-engine ,engine (remove-prefix ,jao-gnus-search-prefix))) #+end_src * News server #+begin_src emacs-lisp (setq gnus-select-method (cond (jao-gnus-use-leafnode `(nntp "localhost" ,(jao-gnus-search-engine 'gnus-search-notmuch))) (jao-gnus-use-gmane '(nntp "news.gmane.io")) (t '(nnnil "")))) (setq gnus-secondary-select-methods '()) ;; nntp options (setq nnheader-read-timeout 0.02) #+end_src * IMAP servers #+begin_src emacs-lisp ;; archiving messages (setq gnus-message-archive-group nil) ;; imap (when jao-gnus-use-local-imap (add-to-list 'gnus-secondary-select-methods `(nnimap "" (nnimap-address "localhost")))) (when jao-gnus-use-pm-imap (add-to-list 'gnus-secondary-select-methods '(nnimap "pm" (nnimap-address "127.0.0.1") (nnimap-stream network) (nnimap-server-port 1143)))) (when jao-gnus-use-gandi-imap (add-to-list 'gnus-secondary-select-methods '(nnimap "gandi" (nnimap-address "mail.gandi.net")))) #+end_src * Mailbox and maildir servers #+begin_src emacs-lisp (setq mail-sources '((file :path "/var/mail/jao"))) (setq nnml-get-new-mail t nnmail-treat-duplicates 'delete nnmail-scan-directory-mail-source-once t nnmail-cache-accepted-message-ids t nnmail-message-id-cache-length 50000 nnmail-cache-ignore-groups ".*\\(trove\\.\\|feeds\\.\\|spamish\\).*" nnmail-split-fancy-with-parent-ignore-groups nil nnmail-crosspost t) (setq nnmail-resplit-incoming t nnmail-mail-splitting-decodes t nnmail-split-methods 'nnmail-split-fancy) (when jao-gnus-use-nnml (add-to-list 'gnus-secondary-select-methods `(nnml ""))) (when jao-gnus-use-maildirs (add-to-list 'gnus-secondary-select-methods '(nnmaildir "bml" (directory "/home/jao/var/maildir/bigml/"))) (add-to-list 'gnus-secondary-select-methods '(nnmaildir "jao" (directory "/home/jao/var/maildir/jao/"))) (add-to-list 'gnus-secondary-select-methods '(nnmaildir "gmail" (directory "/home/jao/var/maildir/gmail/")))) #+end_src * Demons and notifications #+begin_src emacs-lisp (setq mail-user-agent 'gnus-user-agent) (require 'gnus-demon) (gnus-demon-add-rescan) ;; synchronicity (setq gnus-asynchronous t) ;;; prefetch as many articles as possible (setq gnus-use-article-prefetch nil) (setq gnus-save-killed-list nil) (setq gnus-check-new-newsgroups nil) (defvar jao-gnus-tracked-groups '(("^nnimap:\\(bigml\\|jao\\)" "" default) ("nnimap:bigml/inbox" "B" jao-themes-f00) ("nnimap:bigml/bugs" "b" jao-themes-error) ("nnimap:bigml/support" "S" default) ("nnimap:jao/inbox" "I" jao-themes-f01) ("nnimap:bigml" "W" jao-themes-dimm) ("nnimap:jao" "J" jao-themes-dimm) ("nnimap:feeds/[^e]" "F" jao-themes-dimm) ("^gmane\\.emacs\\|nnimap:feeds/emacs" "E" jao-themes-dimm) ("^gmane\\.[^e]" "N" jao-themes-dimm))) (defun jao-gnus--unread-counts () (seq-reduce (lambda (r g) (let ((n (gnus-group-unread (car g)))) (if (and (numberp n) (> n 0)) (cons (cons (car g) n) r) r))) gnus-newsrc-alist ())) (defun jao-gnus--unread-label (counts rx label face) (let ((n (seq-reduce (lambda (n c) (if (string-match-p rx (car c)) (+ n (cdr c)) n)) counts 0))) (when (> n 0) `(:propertize ,(format "%s%d " label n) face ,face)))) (defvar jao-gnus--notify-strs ()) (defun jao-gnus--notify-strs () (let ((counts (jao-gnus--unread-counts))) (seq-filter #'identity (seq-map `(lambda (args) (apply 'jao-gnus--unread-label ',counts args)) jao-gnus-tracked-groups)))) (defun jao-gnus--notify () (setq jao-gnus--notify-strs (jao-gnus--notify-strs)) (save-window-excursion (jao-minibuffer-refresh))) (add-hook 'gnus-after-getting-new-news-hook #'jao-gnus--notify) (add-hook 'gnus-started-hook #'jao-gnus--notify) (defun jao-gnus--summary-done () (let ((inhibit-message t) (message-log-max nil)) (save-window-excursion (jao-gnus--notify) (org-agenda-list)))) (add-hook 'gnus-summary-exit-hook #'jao-gnus--summary-done) (with-eval-after-load "jao-minibuffer" (jao-minibuffer-add-variable 'jao-gnus--notify-strs -20)) #+end_src * Delayed messages #+BEGIN_SRC emacs-lisp ;;; delayed messages (C-cC-j in message buffer) (require 'gnus-util) (gnus-delay-initialize) (setq gnus-delay-default-delay "3h") ;;; so that the Date is set when the message is sent, not when it's ;;; delayed (eval-after-load "message" '(setq message-draft-headers (remove 'Date message-draft-headers))) #+END_SRC * Add-ons *** icalendar #+begin_src emacs-lisp (use-package gnus-icalendar :demand t :init (setq gnus-icalendar-org-capture-file (expand-file-name "inbox.org" org-directory) gnus-icalendar-org-capture-headline '("Appointments")) :config (gnus-icalendar-org-setup)) #+end_src *** bbdb #+begin_src emacs-lisp (with-eval-after-load "bbdb" (bbdb-initialize 'gnus 'message 'pgp 'mail) (bbdb-mua-auto-update-init 'gnus) (eval-after-load "gnus-sum" '(progn (define-key gnus-summary-mode-map ":" 'bbdb-mua-annotate-sender) (define-key gnus-summary-mode-map ";" 'bbdb-mua-annotate-recipients)))) #+end_src *** randomsig #+begin_src emacs-lisp (with-eval-after-load "randomsig" (with-eval-after-load "gnus-sum" (define-key gnus-summary-save-map "-" 'gnus/randomsig-summary-read-sig))) #+end_src *** notmuch -> gnus #+begin_src emacs-lisp (defun jao-notmuch-goto-message-in-gnus () "Open a summary buffer containing the current notmuch article." (interactive) (let ((group (jao-maildir-file-to-group (notmuch-show-get-filename))) (message-id (replace-regexp-in-string "^id:" "" (notmuch-show-get-message-id)))) (if (and group message-id) (org-gnus-follow-link group message-id) (message "Couldn't get relevant infos for switching to Gnus.")))) (defalias 'jao-open-gnus-frame 'jao-afio--goto-mail) (eval-after-load "notmuch-show" '(define-key notmuch-show-mode-map (kbd "C-c C-c") #'jao-notmuch-goto-message-in-gnus)) #+end_src *** gnus-recent #+begin_src emacs-lisp :load no (use-package gnus-recent :ensure t :after gnus :bind (:map gnus-summary-mode-map (("l" . #'gnus-recent-goto-previous)) :map gnus-group-mode-map (("C-c l" . #'gnus-recent-goto-previous) ("C-c r" . #'gnus-recent)))) #+end_src * Groups buffer #+begin_src emacs-lisp ;; (setq gnus-group-line-format " %m%S%p%P:%~(pad-right 35)c %3y %B\n") (setq gnus-group-line-format " %m%S%p%3y%P%* %~(pad-right 45)G %B\n") (setq gnus-topic-line-format "%i[ %(%{%n%}%) -- %A ]%v\n") (setq gnus-group-uncollapsed-levels 2) (setq gnus-auto-select-subject 'unread) (setq-default gnus-large-newsgroup 2000) (add-hook 'gnus-select-group-hook 'gnus-group-set-timestamp) (add-hook 'gnus-group-mode-hook 'gnus-topic-mode) (defvar jao-gnus--expire-every 50) (defvar jao-gnus--get-count (1+ jao-gnus--expire-every)) (defun jao-gnus-get-new-news (&optional arg) (interactive "p") (when (and jao-gnus--expire-every (> jao-gnus--get-count jao-gnus--expire-every)) (when jao-gnus-use-pm-imap (gnus-group-catchup "nnimap:pm/spam" t)) (gnus-group-expire-all-groups) (setq jao-gnus--get-count 0)) (setq jao-gnus--get-count (1+ jao-gnus--get-count)) (gnus-group-get-new-news (max (if (= 1 jao-gnus--get-count) 4 3) (or arg 0)))) ;; To limit expiration to the `g' count, `jao-gnus--get-count': ;; (remove-hook 'gnus-summary-prepare-exit-hook 'gnus-summary-expire-articles) ;; (define-key gnus-group-mode-map "g" 'jao-gnus-get-new-news) (defun jao-gnus-restart-servers () (interactive) (message "Restarting all servers...") (gnus-group-enter-server-mode) (gnus-server-close-all-servers) (gnus-server-open-all-servers) (gnus-server-exit) (message "Restarting all servers... done")) (define-key gnus-group-mode-map "z" #'notmuch) (define-key gnus-group-mode-map "Z" #'jao-gnus-restart-servers) (define-key gnus-group-mode-map "Gg" #'consult-notmuch) (define-key gnus-group-mode-map "GG" #'jao-consult-notmuch-folder) (defun jao-gnus--first-group () (when (derived-mode-p 'gnus-group-mode) (gnus-group-first-unread-group))) (with-eval-after-load "jao-afio" (add-hook 'jao-afio-switch-hook #'jao-gnus--first-group)) #+end_src * Group parameters #+begin_src emacs-lisp (setq jao-gnus-expirable (format (concat "^nnimap:\\(" "\\(\\(bigml\\|bml\\)/%s\\)\\|" "\\(jao/%s\\)\\|" "\\(feeds/.+\\)\\|trash\\|spam" "\\)") (regexp-opt '("support" "reports" "deploys" "lists" "drivel" "bugs")) (regexp-opt '("books" "think" "local" "drivel" "lists" "emacs" "bills" "gnu")))) (setq gnus-permanently-visible-groups "^nnselect") (setq gnus-parameters `(("^nnimap:jao/.*" (jao-gnus--trash-group "nnimap:jao/trash") (jao-gnus--spam-group "nnimap:jao/spam") (jao-gnus--archiving-group "nnimap:trove/jao")) ("^nnimap:\\(jao\\|pm\\|bigml\\)/\\(trash\\|spam\\)" (gcc-self . nil) (auto-expire . t) (total-expire . t) (expiry-wait . 1) (jao-gnus--trash-group nil) (expiry-target . delete)) ("^nnselect:.*-today" (nnselect-rescan . t)) ("^nnimap:jao/inbox" (gcc-self . t)) ("^nnimap:bigml/.*" (posting-style (address "jao@bigml.com")) (jao-gnus--spam-group "nnimap:bigml/spam")) ("^nnimap:bigml/inbox" (gcc-self . t) (auto-expire . t) (total-expire . t) (expiry-wait . 14) (jao-gnus--trash-group "nnimap:trash") (expiry-target . "nnimap:trove/bigml")) ("^nnimap:bigml/support" (posting-style (address "support@bigml.com"))) (,jao-gnus-expirable (jao-gnus--trash-group nil) (gcc-self . nil) (auto-expire . t) (total-expire . t) (expiry-wait . 7) (expiry-target . delete)) ("^nnimap:feeds/podcasts" (auto-expire . nil) (total-expire . nil)) ("^nnimap:feeds/\\(papers\\|programming\\|math\\|physics\\)$" (expiry-wait . 30) (jao-gnus--archiving-group "nnimap:trove/tech") (posting-style (address "jao@gnu.org"))) ("^nnimap:jao/hacking$" (jao-gnus--archiving-group "nnimap:trove/tech")) ("^nnimap:jao/gnu$" (expiry-target . "nnimap:trove/gnu") (jao-gnus--archiving-group "nnimap:trove/gnu")) ("^nnimap:jao/bills$" (expiry-target . "nnimap:trove/bills") (jao-gnus--archiving-group "nnimap:trove/bills")) ("\\(gmane\\|gwene\\)\\..*" (jao-gnus--archiving-group "nnimap:trove/tech") (posting-style (address "jao@gnu.org"))))) #+end_src * Summary buffer *** Configuration, summary line #+BEGIN_SRC emacs-lisp (setq gnus-summary-ignore-duplicates t gnus-suppress-duplicates t gnus-summary-ignored-from-addresses jao-mails-regexp) (setq gnus-show-threads t gnus-thread-hide-subtree t gnus-summary-make-false-root 'adopt gnus-summary-gather-subject-limit 120 gnus-sort-gathered-threads-function 'gnus-thread-sort-by-date gnus-thread-sort-functions '(gnus-thread-sort-by-date)) (setq gnus-face-1 'jao-gnus-face-tree) (setq gnus-not-empty-thread-mark ?·) ; ↓) (setq jao-gnus--summary-line-fmt (concat "%%U %%*%%R %%uj " "[ %%~(max-right 20)~(pad-right 20)n " " %%I%%~(pad-left 2)t ] %%s" "%%-%s=" "%%~(max-right 8)~(pad-left 8)&user-date;" "\n")) (defun jao-gnus--set-summary-line (&optional w) (let* ((d (if jao-gnus-use-three-panes 75 12)) (w (- (or w (window-width)) d))) (setq gnus-summary-line-format (format jao-gnus--summary-line-fmt w)))) ;; (add-hook 'gnus-select-group-hook 'jao-gnus--set-summary-line) (jao-gnus--set-summary-line 190) (add-to-list 'nnmail-extra-headers 'Cc) (add-to-list 'nnmail-extra-headers 'BCc) (add-to-list 'gnus-extra-headers 'Cc) (add-to-list 'gnus-extra-headers 'BCc) (defun gnus-user-format-function-j (headers) (let ((to (gnus-extra-header 'To headers))) (if (string-match jao-mails-regexp to) (if (string-match "," to) "¬" "»") ;; "~" "=") (if (or (string-match jao-mails-regexp (gnus-extra-header 'Cc headers)) (string-match jao-mails-regexp (gnus-extra-header 'BCc headers))) "¬" ;; "~" " ")))) (setq gnus-summary-user-date-format-alist '(((gnus-seconds-today) . "%H:%M") ((+ 86400 (gnus-seconds-today)) . "'%H:%M") ;; (604800 . "%a %H:%M") ;;that's one week ((gnus-seconds-month) . "%a %d") ((gnus-seconds-year) . "%b %d") (t . "%b '%y"))) ;; old name, for emacs 23 (setq gnus-user-date-format-alist gnus-summary-user-date-format-alist) #+END_SRC *** Moving messages around #+BEGIN_SRC emacs-lisp (defvar-local jao-gnus--spam-group nil) (defvar-local jao-gnus--archiving-group nil) (defvar-local jao-gnus--archive-as-copy-p nil) (defvar jao-gnus--last-move nil) (defun jao-gnus-move-hook (a headers c to d) (setq jao-gnus--last-move (cons to (mail-header-id headers)))) (defun jao-gnus-goto-last-moved () (interactive) (when jao-gnus--last-move (when (eq major-mode 'gnus-summary-mode) (gnus-summary-exit)) (gnus-group-goto-group (car jao-gnus--last-move)) (gnus-group-select-group) (gnus-summary-goto-article (cdr jao-gnus--last-move) nil t))) (add-hook 'gnus-summary-article-move-hook 'jao-gnus-move-hook) (defun jao-gnus-archive (follow) (interactive "P") (if jao-gnus--archiving-group (progn (if (or jao-gnus--archive-as-copy-p (not (gnus-check-backend-function 'request-move-article gnus-newsgroup-name))) (gnus-summary-copy-article nil jao-gnus--archiving-group) (gnus-summary-move-article nil jao-gnus--archiving-group)) (when follow (jao-gnus-goto-last-moved))) (gnus-summary-mark-as-read) (gnus-summary-delete-article))) (defun jao-gnus-archive-tickingly () (interactive) (gnus-summary-tick-article) (jao-gnus-archive) (when jao-gnus--archive-as-copy-p (gnus-summary-mark-as-read))) (defun jao-gnus-show-tickled () (interactive) (gnus-summary-limit-to-marks "!")) (make-variable-buffer-local (defvar jao-gnus--trash-group nil)) (defun jao-gnus-trash () (interactive) (gnus-summary-mark-as-read) (if jao-gnus--trash-group (gnus-summary-move-article nil jao-gnus--trash-group) (gnus-summary-delete-article))) (defun jao-gnus-move-to-spam () (interactive) (gnus-summary-mark-as-read) (gnus-summary-move-article nil jao-gnus--spam-group)) (define-key gnus-summary-mode-map "Ba" 'jao-gnus-archive) (define-key gnus-summary-mode-map "BA" 'jao-gnus-archive-tickingly) (define-key gnus-summary-mode-map "Bl" 'jao-gnus-goto-last-moved) (define-key gnus-summary-mode-map (kbd "B DEL") 'jao-gnus-trash) (define-key gnus-summary-mode-map (kbd "B ") 'jao-gnus-trash) (define-key gnus-summary-mode-map "Bs" 'jao-gnus-move-to-spam) (define-key gnus-summary-mode-map "/!" 'jao-gnus-show-tickled) (define-key gnus-summary-mode-map [f7] 'gnus-summary-force-verify-and-decrypt) #+END_SRC *** Writing emails #+BEGIN_SRC emacs-lisp (setq gnus-default-article-saver 'gnus-summary-save-article-mail) (defvar jao-gnus-file-save-directory (expand-file-name "~/tmp")) (defun jao-gnus-file-save (newsgroup headers &optional last-file) (expand-file-name (format "%s.eml" (mail-header-subject headers)) jao-gnus-file-save-directory)) (setq gnus-mail-save-name 'jao-gnus-file-save) #+END_SRC *** arXiv capture #+begin_src emacs-lisp (use-package org-capture :config (add-to-list 'org-capture-templates '("X" "arXiv" entry (file "notes/physics/arxiv.org") "* %:subject\n %i" :immediate-finish t) t) (org-capture-upgrade-templates org-capture-templates)) (defun jao-gnus-arXiv-capture () (interactive) (gnus-summary-select-article-buffer) (gnus-article-goto-part 0) (forward-paragraph) (setq-local transient-mark-mode 'lambda) (set-mark (point)) (goto-char (point-max)) (org-capture nil "X")) #+end_src * Article buffer *** Config, headers #+begin_src emacs-lisp (setq mail-source-delete-incoming t) (setq gnus-gcc-mark-as-read t) (setq gnus-treat-display-smileys nil) (setq gnus-treat-fill-long-lines nil) (setq gnus-treat-fill-article nil) (setq gnus-article-auto-eval-lisp-snippets nil) (setq gnus-posting-styles '((".*" (name "Jose A. Ortega Ruiz")))) (setq gnus-single-article-buffer nil) (setq gnus-article-update-lapsed-header 60) (setq gnus-article-update-date-headers 60) (eval-after-load "gnus-art" '(setq gnus-visible-headers (concat gnus-visible-headers "\\|^List-[iI][Dd]:\\|^X-Newsreader:\\|^X-Mailer:\\|User-Agent:\\|X-User-Agent:"))) #+end_src *** HTML email #+BEGIN_SRC emacs-lisp (setq gnus-button-url 'browse-url-generic gnus-inhibit-images t mm-discouraged-alternatives nil ;; '("text/html" "text/richtext") mm-inline-large-images 'resize) ;; no html in From: (washing articles from arxiv feeds) (require 'shr) (defun jao-gnus-remove-anchors () (save-excursion (goto-char (point-min)) (when (re-search-forward " .+updates on arXiv.org: +" nil t) (replace-match " ") (let ((begin (point))) (when (re-search-forward "^\\(To\\|Subject\\):" nil t) (beginning-of-line) (let ((shr-width 1000)) (shr-render-region begin (1- (point))))))))) (add-hook 'gnus-part-display-hook 'jao-gnus-remove-anchors) ;; show images (defun jao-gnus-show-image (&optional external) (interactive "P") (when (eq major-mode 'gnus-summary-mode) (gnus-summary-select-article-buffer)) (let ((pos (next-single-property-change (point) 'w3m-image))) (if (not pos) (gnus-article-show-images) (goto-char pos) (if external (w3m-view-image) (w3m-toggle-inline-image))))) (defun jao-gnus-show-images (&optional external) (interactive "P") (save-window-excursion (gnus-summary-select-article-buffer) (save-excursion (let ((pos (next-single-property-change (point) 'w3m-image))) (if (not pos) (gnus-article-show-images) (goto-char pos) (w3m-toggle-inline-images)))))) #+END_SRC *** Follow links and enclosures #+begin_src emacs-lisp (defun jao-gnus-follow-link (&optional external) (interactive "P") (when (eq major-mode 'gnus-summary-mode) (gnus-summary-select-article-buffer)) (save-excursion (goto-char (point-min)) (when (or (search-forward-regexp "^Via: h" nil t) (search-forward-regexp "^URL: h" nil t) (and (search-forward-regexp "^Link$" nil t) (not (beginning-of-line)))) (if external (jao-browse-with-external-browser) (browse-url (jao-url-around-point)))))) (defun jao-gnus-open-enclosure (&optional playp) (interactive "P") (gnus-summary-select-article-buffer) (save-excursion (goto-char (point-min)) (when (search-forward "Enclosure:") (forward-char 2) (when-let ((url (thing-at-point-url-at-point))) (message "%s %s ..." (if playp "Playing" "Adding") url) (if playp (emms-play-url url) (emms-add-url url)) (when playp (sit-for 1) (jao-emms-echo)))))) #+end_src * Keyboard shortcuts #+BEGIN_SRC emacs-lisp (define-key gnus-article-mode-map "i" 'jao-gnus-show-images) (define-key gnus-summary-mode-map "i" 'jao-gnus-show-images) (define-key gnus-article-mode-map "\M-g" 'jao-gnus-follow-link) (define-key gnus-summary-mode-map "\M-g" 'jao-gnus-follow-link) (define-key gnus-summary-mode-map "v" 'scroll-other-window) (define-key gnus-summary-mode-map "V" 'scroll-other-window-down) (define-key gnus-summary-mode-map "X" 'jao-gnus-arXiv-capture) (major-mode-hydra-define gnus-summary-mode nil ("Browse" (("g" jao-gnus-follow-link "Follow link in emacs") ("G" (lambda () (interactive) (jao-gnus-follow-link t)) "Follow link in external browser")) "Capture" (("x" jao-gnus-arXiv-capture "Capture arXiv entry") ("e" jao-gnus-open-enclosure "Add enclosure to playlist") ("E" (jao-gnus-open-enclosure t) "Play enclosure")) "Images" (("i" jao-gnus-show-images "Show images")) "Toot" (("t" jao-gnus-tweet-link "Tweet article") ("T" jao-gnus-toot-link "Toot article")))) (major-mode-hydra-define gnus-article-mode nil ("Browse" (("g" jao-gnus-follow-link "Follow link in emacs") ("G" (lambda () (interactive) (jao-gnus-follow-link t)) "Follow link in external browser")) "Capture" (("x" jao-gnus-arXiv-capture "Capture arXiv entry") ("e" jao-gnus-open-enclosure "Add enclosure to playlist") ("E" (jao-gnus-open-enclosure t) "Play enclosure")) "Images" (("z" w3m-lnum-zoom-in-image "Zoom image at point") ("I" (if (fboundp 'w3m-view-image) (w3m-view-image) (eww-display-image)) "View image at point") ("i" jao-gnus-show-images "Show images")) "Toot" (("t" jao-gnus-tweet-link "Tweet article") ("T" jao-gnus-toot-link "Toot article")))) #+END_SRC