Sunday 12 March 2017

Take Elfeed everywhere: Mobile rss reading Emacs-style (for free/cheap)

After the demise of Google Reader, I switched for some time to NewsBlur. Unfortunately, its Android app never worked well for me. And integrating more of my life into Emacs is always desirable, so once I saw there was an Android interface for the fantastic Emacs RSS reader Elfeed, I made the switch.

The tricky thing with the Elfeed Android client is that it wants to connect to web interface of an instance of Elfeed running inside of Emacs. I could have done with my home computer, but that would require poking a hole through the firewall and in any case would be non-ideal when for instance I was travelling.

About a month ago I hit upon a cheap (in fact, free) solution for running a remote instance of Emacs running Elfeed that is connectable with the Android app. The VPS provider Wishosting offers an OpenVZ mini for $4/year, and if you stick a link to Wishosting on your own domain you can get it for free.

On my home desktop, work desktop, and laptop, I have Syncthing installed and I use this to keep the Elfeed database in sync between these machines.

In this blogpost I outline how to add a remote always-on instance of Elfeed running in Wishosting’s OpenVZ mini which also remains in sync with all of the other machines. (I use elfeed-org to organise my feeds, and just keep the elfeed.org file in ~/.elfeed/.) Just use Syncthing to keep the ~/.elfeed directory sync’ed between all of the machines (including any VPS’s).

I set up an Ubuntu 16.04 LTS VPS on Wishosting, and then installed Emacs and Syncthing, and that is what I would recommend.

Steps
  • Create your Ubuntu 16.04 LTS VPS.
  • Make a (non-root) user with superuser capability. Login with this user.
  • Install Emacs and Syncthing.
  • Configure Syncthing appropriately. You can connect remotely to the Web GUI.
  • Create and save a .emacs file in your user’s ~ with the following contents:
    ;; package setup here
    (require 'package)
    
    (package-initialize nil)
    (setq package-enable-at-startup nil)
    
    (add-to-list 'package-archives '("org" . "http://orgmode.org/elpa/") t)
    
    (add-to-list 'package-archives
          '("melpa" . "https://melpa.org/packages/") t)
    
    (add-to-list 'package-archives
          '("marmalade" .
            "http://marmalade-repo.org/packages/"))
    
    (package-initialize)
    
    ;; general add packages to list
    (let ((default-directory  "~/.emacs.d/elpa/"))
      (normal-top-level-add-subdirs-to-load-path))
    
    ;; make sure 'use-package is installed
    (unless (package-installed-p 'use-package)
      (package-refresh-contents)
      (package-install 'use-package))
    
    ;;; use-package
    (require 'use-package)
    
    ;; Load elfeed
    (use-package elfeed
      :ensure t
      :bind (:map elfeed-search-mode-map
         ;              ("A" . bjm/elfeed-show-all)
         ;              ("E" . bjm/elfeed-show-emacs)
         ;              ("D" . bjm/elfeed-show-daily)
           ("q" . bjm/elfeed-save-db-and-bury)))
    
    (require 'elfeed)
    
    ;; Load elfeed-org
    (use-package elfeed-org
      :ensure t
      :config
      (elfeed-org)
      (setq rmh-elfeed-org-files (list "~/.elfeed/elfeed.org"))
      )
    
    ;; Laod elfeed-goodies
    (use-package elfeed-goodies
      :ensure t
      )
    
    (elfeed-goodies/setup)
    
    ;; Load elfeed-web
    (use-package elfeed-web
      :ensure t
      )
    
    ;;; Elfeed
    (global-set-key (kbd "C-x w") 'bjm/elfeed-load-db-and-open)
    
    (define-key elfeed-show-mode-map (kbd "j") 'elfeed-goodies/split-show-next)
    (define-key elfeed-show-mode-map (kbd "k") 'elfeed-goodies/split-show-prev)
    (define-key elfeed-search-mode-map (kbd "j") 'next-line)
    (define-key elfeed-search-mode-map (kbd "k") 'previous-line)
    (define-key elfeed-show-mode-map (kbd "S-SPC") 'scroll-down-command)
    
    
    ;;write to disk when quiting
    (defun bjm/elfeed-save-db-and-bury ()
      "Wrapper to save the elfeed db to disk before burying buffer"
      (interactive)
      (elfeed-db-save)
      (quit-window))
    
    ;;functions to support syncing .elfeed between machines
    ;;makes sure elfeed reads index from disk before launching
    (defun bjm/elfeed-load-db-and-open ()
      "Wrapper to load the elfeed db from disk before opening"
      (interactive)
      (elfeed-db-load)
      (elfeed)
      (elfeed-search-update--force)
      (elfeed-update))
    
    (defun bjm/elfeed-updater ()
      "Wrapper to load the elfeed db from disk before opening"
      (interactive)
      (elfeed-db-save)
      (quit-window)
      (elfeed-db-load)
      (elfeed)
      (elfeed-search-update--force)
      (elfeed-update))
    
    (run-with-timer 0 (* 30 60) 'bjm/elfeed-updater)
    
    (setq httpd-port NNNNN)   ; replace NNNNN with a port equalling your start port + 10 (or whatever)
    
    (elfeed-web-start)
    
Note that you’ll need to configure the httpd-port appropriately as per the comment in the elisp above.
  • Then create the following systemd unit at ~/.config/systemd/user/emacs.service:
[Unit]
Description=Emacs: the extensible, self-documenting text editor

[Service]
Type=forking
ExecStart=/usr/bin/emacs --daemon
ExecStop=/usr/bin/emacsclient --eval "(kill-emacs)"
Restart=always

[Install]
WantedBy=default.target
  • Enable and start this unit with:
$ systemctl --user enable --now emacs
  • And then make sure it runs persistently even when you’re not connected to your VPS via ssh (this tripped me up for some time):
# loginctl enable-linger USERNAME
  • Install the Elfeed Android app on your mobile, enter the app’s Settings, and put in whatever your Wishosting ip is plus the port you chose above (NNNNN), e.g. http://199.39.100.23:54169, for the Elfeed web url.
  • That should be it. Now you’ll have access to your rss feed all over the world and Syncthing will ensure that all changes will be propagated to all of your Emacs instances including your VPS. These changes would include adding and deleting rss feeds and marking of posts as ’read’.
  • Note: on my desktops/laptop I use the following setup for Elfeed:
    ;; Load elfeed
    (use-package elfeed
      :ensure t
      :bind (:map elfeed-search-mode-map
    ;              ("A" . bjm/elfeed-show-all)
    ;              ("E" . bjm/elfeed-show-emacs)
    ;              ("D" . bjm/elfeed-show-daily)
           ("q" . bjm/elfeed-save-db-and-bury)))
    
    
    (require 'elfeed)
    
    ;; Load elfeed-org
    (use-package elfeed-org
      :ensure t
      :config
      (elfeed-org)
      (setq rmh-elfeed-org-files (list "~/.elfeed/elfeed.org"))
      )
    
    ;; Laod elfeed-goodies
    (use-package elfeed-goodies
      :ensure t
      )
    
    (elfeed-goodies/setup)
    
    ;; Load elfeed-web
    (use-package elfeed-web
      :ensure t
      )
    
    ;;functions to support syncing .elfeed between machines
    ;;makes sure elfeed reads index from disk before launching
    (defun bjm/elfeed-load-db-and-open ()
      "Wrapper to load the elfeed db from disk before opening"
      (interactive)
      (elfeed-db-load)
      (elfeed)
      (elfeed-search-update--force)
      (elfeed-update))
    
    ;;write to disk when quiting
    (defun bjm/elfeed-save-db-and-bury ()
      "Wrapper to save the elfeed db to disk before burying buffer"
      (interactive)
      (elfeed-db-save)
      (quit-window))
    
    
    ;;; Elfeed
    (global-set-key (kbd "C-x w") 'bjm/elfeed-load-db-and-open)
    
    
    
    (define-key elfeed-show-mode-map (kbd ";") 'visual-fill-column-mode)
    
    (define-key elfeed-show-mode-map (kbd "j") 'elfeed-goodies/split-show-next)
    (define-key elfeed-show-mode-map (kbd "k") 'elfeed-goodies/split-show-prev)
    (define-key elfeed-search-mode-map (kbd "j") 'next-line)
    (define-key elfeed-search-mode-map (kbd "k") 'previous-line)
    (define-key elfeed-show-mode-map (kbd "S-SPC") 'scroll-down-command)
    
    ;; probably temporary: hack for elfeed-goodies date column:
    (defun elfeed-goodies/search-header-draw ()
      "Returns the string to be used as the Elfeed header."
      (if (zerop (elfeed-db-last-update))
          (elfeed-search--intro-header)
        (let* ((separator-left (intern (format "powerline-%s-%s"
            elfeed-goodies/powerline-default-separator
            (car powerline-default-separator-dir))))
        (separator-right (intern (format "powerline-%s-%s"
             elfeed-goodies/powerline-default-separator
             (cdr powerline-default-separator-dir))))
        (db-time (seconds-to-time (elfeed-db-last-update)))
        (stats (-elfeed/feed-stats))
        (search-filter (cond
          (elfeed-search-filter-active
           "")
          (elfeed-search-filter
           elfeed-search-filter)
          (""))))
          (if (>= (window-width) (* (frame-width) elfeed-goodies/wide-threshold))
       (search-header/draw-wide separator-left separator-right search-filter stats db-time)
     (search-header/draw-tight separator-left separator-right search-filter stats db-time)))))
    
    (defun elfeed-goodies/entry-line-draw (entry)
      "Print ENTRY to the buffer."
    
      (let* ((title (or (elfeed-meta entry :title) (elfeed-entry-title entry) ""))
      (date (elfeed-search-format-date (elfeed-entry-date 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 (mapcar #'symbol-name (elfeed-entry-tags entry)))
      (tags-str (concat "[" (mapconcat 'identity tags ",") "]"))
      (title-width (- (window-width) elfeed-goodies/feed-source-column-width
        elfeed-goodies/tag-column-width 4))
      (title-column (elfeed-format-column
       title (elfeed-clamp
              elfeed-search-title-min-width
              title-width
              title-width)
       :left))
      (tag-column (elfeed-format-column
            tags-str (elfeed-clamp (length tags-str)
              elfeed-goodies/tag-column-width
              elfeed-goodies/tag-column-width)
            :left))
      (feed-column (elfeed-format-column
             feed-title (elfeed-clamp elfeed-goodies/feed-source-column-width
          elfeed-goodies/feed-source-column-width
          elfeed-goodies/feed-source-column-width)
             :left)))
    
        (if (>= (window-width) (* (frame-width) elfeed-goodies/wide-threshold))
     (progn
       (insert (propertize date 'face 'elfeed-search-date-face) " ")
       (insert (propertize feed-column 'face 'elfeed-search-feed-face) " ")
       (insert (propertize tag-column 'face 'elfeed-search-tag-face) " ")
       (insert (propertize title 'face title-faces 'kbd-help title)))
          (insert (propertize title 'face title-faces 'kbd-help title)))))
    
Screenshots:
[Also posted at r/emacs]