RSS

阅读 RSS 使用的是 elfeed,下面两个插件搭配起来使用,体验会更流程:

  • elfeed-org 使用 org 文件来定义 feeds 列表,它可以很方便进行打标签,重命名
  • elfeed-dashboard 把定义的 feeds 列表按标签展示
(use-package elfeed
  :custom ((elfeed-use-curl t)
           (elfeed-db-directory "~/Downloads/elfeed/")
           (elfeed-curl-timeout 20)
           (elfeed-curl-extra-arguments `("-x" ,socks-proxy)))
  :bind (:map elfeed-show-mode-map
         ("8" . my/elfeed-toggle-star)
         ("9" . my/elfeed-show-images)
         ("g" . elfeed-show-refresh)
         :map elfeed-search-mode-map
         ("SPC" . scroll-up-command)
         ("DEL" . scroll-down-command)
         ("f" . my/elfeed-search-by-title)
         ("b" . my/elfeed-browse)
         ("8" . my/elfeed-toggle-star))
  :init
  (progn
    (defun my/elfeed-browse ()
      (interactive)
      (let ((entries (elfeed-search-selected)))
        (elfeed-untag entries 'unread)
        (mapc #'elfeed-search-update-entry entries))
      (elfeed-search-browse-url))

    (defun my/elfeed-add-title-tag (entry)
      (when-let* ((feed (elfeed-entry-feed entry))
                  (title (cl-destructuring-bind (&key title &allow-other-keys)
                             (elfeed-feed-meta feed)
                           title)))
        (elfeed-tag entry (intern title))))

    (add-hook 'elfeed-new-entry-hook 'my/elfeed-add-title-tag 1)

    (defun my/elfeed-hook ()
      ;; (variable-pitch-mode)
      ;; (setq-local shr-inhibit-images nil)
      (setq-local line-spacing 0.5)
      (setq-local shr-width 85))

    (defun my/elfeed-search-print-entry (entry)
      (let* ((date (elfeed-search-format-date (elfeed-entry-date entry)))
             (title (or (elfeed-meta entry :title) (elfeed-entry-title entry) ""))
             (title-faces (elfeed-search--faces (elfeed-entry-tags entry)))
             (feed (elfeed-entry-feed entry))
             (feed-title (when feed
                           (or (elfeed-meta feed :title) (elfeed-feed-title feed))))
             (tags (seq-filter
                    (lambda (tag) (not (string-equal feed-title tag)))
                    (mapcar #'symbol-name (elfeed-entry-tags entry))))
             (tags-str (mapconcat
                        (lambda (s) (propertize s 'face 'elfeed-search-tag-face))
                        tags ","))
             (title-width (- (window-width) 10 elfeed-search-trailing-width))
             (title-column (elfeed-format-column
                            title (elfeed-clamp
                                   elfeed-search-title-min-width
                                   title-width
                                   elfeed-search-title-max-width)
                            :left)))
        (insert (propertize date 'face 'elfeed-search-date-face) " ")
        (insert (propertize title-column 'face title-faces 'kbd-help title) " ")
        (when feed-title
          (insert (propertize feed-title 'face 'elfeed-search-feed-face) " "))
        (when tags
          (insert "(" tags-str ")"))))

    (setq elfeed-search-print-entry-function 'my/elfeed-search-print-entry))

  :hook ((elfeed-search-mode elfeed-show-mode) . my/elfeed-hook)
  :config
  (progn
    (setq elfeed-search-filter "@1-months-ago +unread #100")

    (evil-add-hjkl-bindings elfeed-show-mode-map)
    (evil-add-hjkl-bindings elfeed-search-mode-map)

    (defun my/elfeed-clear-queue ()
      "Sometime elfeed update will end with pending task, which popup at minibuffer, this fix the annoying message"
      (interactive)
      (setq elfeed-curl-queue-active 0))

    (defun my/elfeed-show-images ()
      (interactive)
      (let ((shr-inhibit-images nil))
        (elfeed-show-refresh)))
    ;;functions to support syncing .elfeed between machines
    ;;makes sure elfeed reads index from disk before launching
    (defun my/elfeed-open-db-and-load ()
      "Wrapper to load the elfeed db from disk before opening"
      (interactive)
      (elfeed-db-load)
      (elfeed)
      (elfeed-search-update--force))

    ;;write to disk when quiting
    (defun my/elfeed-close-db-and-save ()
      "Wrapper to save the elfeed db to disk before burying buffer"
      (interactive)
      (elfeed-db-save)
      ;; (quit-window)
      )

    (defun my/elfeed-toggle-star ()
      (interactive)
      (let* ((entry (or elfeed-show-entry
                        (first (elfeed-search-selected))))
             (tag (intern "starred"))
             (taggged (elfeed-tagged-p tag entry)))
        (if taggged
            (elfeed-untag entry tag)
          (elfeed-tag entry tag))
        (message "Starred: %s" (not taggged))))

    (defun my/elfeed-search-star ()
      (interactive)
	  (let ((tag (intern "starred"))
            (entries (elfeed-search-selected)))
	    (cl-loop for entry in entries do (elfeed-tag entry tag))
	    (mapc #'elfeed-search-update-entry entries)
	    (unless (use-region-p) (forward-line))))

    (defun my/elfeed-search-unstar ()
      "Remove starred tag from all selected entries."
      (interactive)
	  (let ((tag (intern "starred"))
            (entries (elfeed-search-selected)))
	    (cl-loop for entry in entries do (elfeed-untag entry tag))
	    (mapc #'elfeed-search-update-entry entries)
	    (unless (use-region-p) (forward-line))))

    (setq my/elfeed-export-dir (expand-file-name "~/Sync/"))
    (defun my/elfeed-export (dir)
      (interactive (list (read-directory-name "Export dir: " my/elfeed-export-dir)))
      (require 'f)
      (let* ((sf (elfeed-search-parse-filter "+starred"))
	         (uf (elfeed-search-parse-filter "-unread"))
	         (starred-entries '())
	         (read-entries '())
	         (hash-table (make-hash-table))
             (output (expand-file-name (format-time-string "elfeed-%Y-%m-%d.el" (current-time))
                                       dir)))
        (with-elfeed-db-visit (entry feed)
          (let ((title-and-link  (cons (elfeed-entry-title entry)
                                       (elfeed-entry-link entry))))
	        (when (elfeed-search-filter sf entry feed)
	          (add-to-list 'starred-entries title-and-link))
	        (when (elfeed-search-filter uf entry feed)
	          (add-to-list 'read-entries title-and-link))))

        (puthash :starred starred-entries hash-table)
        (puthash :read read-entries hash-table)
        (f-write-text (prin1-to-string hash-table) 'utf-8 output)

        (message "Export to %s. starred: %d, read: %d" output (length starred-entries) (length read-entries))))

    (defun my/elfeed-import (f)
      (interactive (list (read-file-name "Backup file: " my/elfeed-export-dir)))
      (require 'f)
      (let* ((hash-table (read (f-read-text f)))
             (starred-entries (thread-last (gethash :starred hash-table)
                                           (mapcar 'cdr)))
             (read-entries (thread-last (gethash :read hash-table)
                                        (mapcar 'cdr))))
        (with-elfeed-db-visit (entry feed)
          (let* ((link (elfeed-entry-link entry)))
            (when (member link starred-entries)
              (elfeed-tag entry (intern "starred")))
            (when (member link read-entries)
              (elfeed-untag entry (intern "unread")))))

        (message "Import starred: %d, read: %d" (length starred-entries) (length read-entries))))

    (defun my/elfeed-search-by-title (title)
      (interactive (list (or (when-let* ((entry (car (elfeed-search-selected)))
                                         (feed (elfeed-entry-feed entry)))
                               (or (cl-destructuring-bind (&key title &allow-other-keys)
                                       (elfeed-feed-meta feed)
                                     title)
                                   (elfeed-feed-title feed)))
                             (read-from-minibuffer "Feed Title: "))))
      (unwind-protect
          (let ((elfeed-search-filter-active :live))
            (setq elfeed-search-filter (concat "+" title)))
        (elfeed-search-update :force)))

    (custom-set-faces
     '(elfeed-search-unread-title-face ((((class color) (background light)) (:foreground "#000" :weight normal :strike-through nil))
                                        (((class color) (background dark)) (:foreground "#fff" :weight normal :strike-through nil))))

     '(elfeed-search-title-face ((((class color) (background light)) (:foreground "grey" :strike-through t))
							     (((class color) (background dark)) (:foreground "grey" :strike-through t)))))

    ;; face for starred articles
    (defface elfeed-search-starred-title-face
      '((t :foreground "#f77" :strike-through nil))
      "Marks a starred Elfeed entry.")

    (push '(starred elfeed-search-starred-title-face) elfeed-search-face-alist)))

(use-package elfeed-dashboard
  :ensure nil
  :commands (elfeed-dashboard)
  :bind (:map elfeed-dashboard-mode-map
         ("/" . my/feed-choose-by-tag)
         :map elfeed-search-mode-map
         ("/" . my/feed-choose-by-tag))
  :config
  (setq elfeed-dashboard-file (no-littering-expand-etc-file-name "elfeed-dashboard.org"))
  ;; Disable this update, it cost too much cpu when there are many feeds
  ;; Update feed counts on elfeed-quit
  ;; (advice-add 'elfeed-search-quit-window :after #'elfeed-dashboard-update-links)

  (defun my/feed-choose-by-tag ()
    (interactive)
    (ivy-read "Tag: " (elfeed-db-get-all-tags)
              :action (lambda (tag)
                        (elfeed-dashboard-query (format "+unread +%s" tag))))))

(use-package elfeed-org
  :ensure nil
  :custom ((rmh-elfeed-org-files `(,(no-littering-expand-etc-file-name "elfeed-feeds.org"))))
  :hook (elfeed-dashboard-mode . elfeed-org)
  :init
  (progn
    (defun my/reload-org-feeds ()
      (interactive)
      (rmh-elfeed-org-process rmh-elfeed-org-files rmh-elfeed-org-tree-id))
    (advice-add 'elfeed-dashboard-update :before #'my/reload-org-feeds)))

下面阐述 elfeed 核心配置:

  • elfeed-curl-extra-arguments 可以配置 curl 的参数,比较常用的是配置代理
  • elfeed 默认使用 shr 展示,在加载图片是会卡住,可通过设置 shr-inhibit-imagesnil 来禁用图片,之后通过自定义函数 my/show-feed-images 来开启图片
  • 为了方便记录重要文章,定义 my/elfeed-toggle-star 函数来实现「标星」效果,并可以使用 +starred 来搜索
  • 默认 elfeed 列表行间距太小,可以通过 (setq-local line-spacing 0.3) 调整
  • elfeed 的数据库存放在本地,可以把这个目录防止云盘的同步目录里。我这里实现了两个函数,来对指定的 tag 进行导出/导入,这样相当于只备份元数据,更轻量些。

    • 导出时主要有两个 tag,一个是 +starred ,表示加星的文章;另一个是 -unread ,表示已读的。

查询语法

查询语句含义
@6-months-ago +unread6 个月内的未读文章
-unread +youtube #10已读的与 youtube 相关的前 10 篇文章
+emacs =http://example.org/feed指定 feed 内,与 emacs 有关的文章

常用快捷键

elfeed-search

  • s 在 minibuffer 重新输入搜索词
  • y yank 当前行文章的 URL,并去掉 unread 标签
  • b 调用外部浏览器打开所选行文章
  • g 刷新当前 feed 列表
  • G 重新抓取 feed 记录

elfeed-show

  • TAB 切换到下一个超链接处
  • g 刷新
  • b 调用外部浏览器打开文章