#+END_SRC
+*** elfeed
+#+BEGIN_SRC emacs-lisp
+ (if (= emacs-major-version 24)
+ (progn
+ (require 'elfeed-org)
+ (elfeed-org)
+ (setq rmh-elfeed-org-files (list "~/org/elfeed.org"))))
+#+END_SRC
*** [[http://www.emacswiki.org/emacs/MalyonMode][Mylon]]
#+BEGIN_SRC emacs-lisp
(define-key bba/launcher "b" 'browse-url-at-point)
(define-key bba/launcher "c" 'calculator)
(define-key bba/launcher "d" 'ediff-buffers)
+ (define-key bba/launcher "e" 'elfeed)
(define-key bba/launcher "i" 'ielm)
(define-key bba/launcher "s" 'shell)
(define-key bba/launcher "S" 'synonyms)
--- /dev/null
+;;; elfeed-autoloads.el --- automatically extracted autoloads
+;;
+;;; Code:
+
+\f
+;;;### (autoloads (elfeed-export-opml elfeed-load-opml elfeed elfeed-update)
+;;;;;; "elfeed" "elfeed.el" (21691 56987 0 0))
+;;; Generated autoloads from elfeed.el
+
+(autoload 'elfeed-update "elfeed" "\
+Update all the feeds in `elfeed-feeds'.
+
+\(fn)" t nil)
+
+(autoload 'elfeed "elfeed" "\
+Enter elfeed.
+
+\(fn)" t nil)
+
+(autoload 'elfeed-load-opml "elfeed" "\
+Load feeds from an OPML file into `elfeed-feeds'.
+When called interactively, the changes to `elfeed-feeds' are
+saved to your customization file.
+
+\(fn FILE)" t nil)
+
+(autoload 'elfeed-export-opml "elfeed" "\
+Export the current feed listing to OPML-formatted FILE.
+
+\(fn FILE)" t nil)
+
+;;;***
+\f
+;;;### (autoloads nil nil ("elfeed-db.el" "elfeed-lib.el" "elfeed-pkg.el"
+;;;;;; "elfeed-search.el" "elfeed-show.el" "xml-query.el") (21691
+;;;;;; 56987 496165 0))
+
+;;;***
+\f
+(provide 'elfeed-autoloads)
+;; Local Variables:
+;; version-control: never
+;; no-byte-compile: t
+;; no-update-autoloads: t
+;; coding: utf-8
+;; End:
+;;; elfeed-autoloads.el ends here
--- /dev/null
+;;; elfeed-db.el --- database and model for elfeed -*- lexical-binding: t; -*-
+
+;; This is free and unencumbered software released into the public domain.
+
+;;; Commentary:
+
+;; Elfeed is aware of two type of things: feeds and entries. All dates
+;; are stored as floating point epoch seconds.
+
+;; Feeds are keyed by their user-provided feed URL, which acts as the
+;; feed identity regardless of any other stated identity. Feeds have a
+;; list of entries.
+
+;; Entries are keyed in order of preference by id (Atom), guid (RSS),
+;; or link. To avoid circular references, entries refer to their
+;; parent feeds by URL.
+
+;; Feed content is stored in a content-addressable loose-file
+;; database, very similar to an unpacked Git object database. Entries
+;; have references to items in this database (elfeed-ref), keeping the
+;; actual entry struct memory footprint small. Most importantly, this
+;; keeps the core index small so that it can quickly be written as a
+;; whole to the filesystem. The wire format is just the s-expression
+;; print form of the top-level hash table.
+
+;; The database can be compacted into a small number of compressed
+;; files with the interactive function `elfeed-db-compact'. This could
+;; be used as a kill-emacs hook.
+
+;; An AVL tree containing all database entries ordered by date is
+;; maintained as part of the database. We almost always want to look
+;; at entries ordered by date and this step accomplished that very
+;; efficiently with the AVL tree. This is the reasoning behind the
+;; `with-elfeed-db-visit' interface.
+
+;; Unfortunately there's a nasty bug (bug#15190) in the reader that
+;; makes hash tables and `print-circle' incompatible. It's been fixed
+;; in trunk, but many users will likely be stuck with this bug for the
+;; next few years. This means the database format can't exploit
+;; circular references.
+
+;; Entry and feed objects can have arbitrary metadata attached,
+;; automatically stored in the database. The setf-able `elfeed-meta'
+;; function is used to access these.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'avl-tree)
+(require 'elfeed-lib)
+
+(defcustom elfeed-db-directory "~/.elfeed"
+ "Directory where elfeed will store its database."
+ :group 'elfeed
+ :type 'directory)
+
+(defvar elfeed-db nil
+ "The core database for elfeed.")
+
+(defvar elfeed-db-feeds nil
+ "Feeds hash table, part of `elfeed-db'.")
+
+(defvar elfeed-db-entries nil
+ "Entries hash table, part of `elfeed-db'.")
+
+(defvar elfeed-db-index nil
+ "Collection of all entries sorted by date, part of `elfeed-db'.")
+
+(defvar elfeed-db-version "0.0.2"
+ "The database version this version of Elfeed expects to use.")
+
+(defvar elfeed-new-entry-hook ()
+ "Functions in this list are called with the new entry as its
+argument. This is a chance to add cutoms tags to new entries.")
+
+(defvar elfeed-db-update-hook ()
+ "Functions in this list are called with no arguments any time
+the :last-update time is updated.")
+
+;; Data model:
+
+(cl-defstruct (elfeed-feed (:constructor elfeed-feed--create))
+ "A web feed, contains elfeed-entry structs."
+ id url title author meta)
+
+(cl-defstruct (elfeed-entry (:constructor elfeed-entry--create))
+ "A single entry from a feed, normalized towards Atom."
+ id title link date content content-type enclosures tags feed-id meta)
+
+(defun elfeed-entry-merge (a b)
+ "Merge B into A, preserving A's tags. Return true if an actual
+update occurred, not counting content."
+ (setf (elfeed-entry-tags b) (elfeed-entry-tags a)
+ (elfeed-entry-content a) (elfeed-entry-content b))
+ (not
+ (zerop
+ (cl-loop for i from 0 below (length a)
+ for part-a = (aref a i)
+ for part-b = (aref b i)
+ count (not (equal part-a part-b))
+ do (setf (aref a i) part-b)))))
+
+(defun elfeed-db-get-feed (id)
+ "Get/create the feed for ID."
+ (elfeed-db-ensure)
+ (let ((feed (gethash id elfeed-db-feeds)))
+ (or feed
+ (setf (gethash id elfeed-db-feeds)
+ (elfeed-feed--create :id id)))))
+
+(defun elfeed-db-get-entry (id)
+ "Get the entry for ID."
+ (elfeed-db-ensure)
+ (gethash id elfeed-db-entries))
+
+(defun elfeed-db-compare (a b)
+ "Return true if entry A is newer than entry B."
+ (let* ((entry-a (elfeed-db-get-entry a))
+ (entry-b (elfeed-db-get-entry b))
+ (date-a (elfeed-entry-date entry-a))
+ (date-b (elfeed-entry-date entry-b)))
+ (if (= date-a date-b)
+ (string< (prin1-to-string b) (prin1-to-string a))
+ (> date-a date-b))))
+
+(defun elfeed-db-set-update-time ()
+ "Update the database last-update time."
+ (plist-put elfeed-db :last-update (float-time))
+ (run-hooks 'elfeed-db-update-hook))
+
+(defun elfeed-db-add (entries)
+ "Add ENTRIES to the database."
+ (elfeed-db-ensure)
+ (cl-loop for entry in entries
+ for id = (elfeed-entry-id entry)
+ for original = (gethash id elfeed-db-entries)
+ for new-date = (elfeed-entry-date entry)
+ for original-date = (and original (elfeed-entry-date original))
+ do (elfeed-deref-entry entry)
+ when original count
+ (if (= new-date original-date)
+ (elfeed-entry-merge original entry)
+ (avl-tree-delete elfeed-db-index id)
+ (prog1 (elfeed-entry-merge original entry)
+ (avl-tree-enter elfeed-db-index id)))
+ into change-count
+ else count
+ (setf (gethash id elfeed-db-entries) entry)
+ into change-count
+ and do
+ (progn
+ (avl-tree-enter elfeed-db-index id)
+ (cl-loop for hook in elfeed-new-entry-hook
+ do (funcall hook entry)))
+ finally
+ (unless (zerop change-count)
+ (elfeed-db-set-update-time)))
+ :success)
+
+(defun elfeed-entry-feed (entry)
+ "Get the feed struct for ENTRY."
+ (elfeed-db-get-feed (elfeed-entry-feed-id entry)))
+
+(defun elfeed-normalize-tags (tags &rest more-tags)
+ "Return the normalized tag list for TAGS."
+ (let ((all (copy-sequence (apply #'append tags more-tags))))
+ (cl-remove-duplicates (cl-sort all #'string< :key #'symbol-name))))
+
+(defun elfeed-tag (entry &rest tags)
+ "Add TAGS to ENTRY."
+ (let ((current (elfeed-entry-tags entry)))
+ (setf (elfeed-entry-tags entry)
+ (elfeed-normalize-tags (append tags current)))))
+
+(defun elfeed-untag (entry &rest tags)
+ "Remove TAGS from ENTRY."
+ (setf (elfeed-entry-tags entry)
+ (cl-loop for tag in (elfeed-entry-tags entry)
+ unless (member tag tags) collect tag)))
+
+(defun elfeed-tagged-p (tag entry)
+ "Return true if ENTRY is tagged by TAG."
+ (member tag (elfeed-entry-tags entry)))
+
+(defun elfeed-db-last-update ()
+ "Return the last database update time in (`float-time') seconds."
+ (elfeed-db-ensure)
+ (or (plist-get elfeed-db :last-update) 0))
+
+(defmacro with-elfeed-db-visit (entry-and-feed &rest body)
+ "Visit each entry in the database from newest to oldest.
+Use `elfeed-db-return' to exit early and optionally return data.
+
+ (with-elfeed-db-visit (entry feed)
+ (do-something entry)
+ (when (some-date-criteria-p entry)
+ (elfeed-db-return)))"
+ (declare (indent defun))
+ `(catch 'elfeed-db-done
+ (prog1 nil
+ (elfeed-db-ensure)
+ (avl-tree-mapc
+ (lambda (id)
+ (let* ((,(cl-first entry-and-feed) (elfeed-db-get-entry id))
+ (,(cl-second entry-and-feed)
+ (elfeed-entry-feed ,(cl-first entry-and-feed))))
+ ,@body))
+ elfeed-db-index))))
+
+(defun elfeed-apply-hooks-now ()
+ "Apply `elfeed-new-entry-hook' to all entries in the database."
+ (interactive)
+ (with-elfeed-db-visit (entry _)
+ (cl-loop for hook in elfeed-new-entry-hook
+ do (funcall hook entry))))
+
+(defmacro elfeed-db-return (&optional value)
+ "Use this to exit early and return VALUE from `with-elfeed-db-visit'."
+ `(throw 'elfeed-db-done ,value))
+
+;; Saving and Loading:
+
+(defun elfeed-db-save ()
+ "Write the database index to the filesystem."
+ (elfeed-db-ensure)
+ (plist-put elfeed-db :version elfeed-db-version)
+ (mkdir elfeed-db-directory t)
+ (let ((coding-system-for-write 'utf-8))
+ (with-temp-file (expand-file-name "index" elfeed-db-directory)
+ (let ((standard-output (current-buffer))
+ (print-level nil)
+ (print-length nil)
+ (print-circle nil))
+ (prin1 elfeed-db)
+ :success))))
+
+(defun elfeed-db-upgrade ()
+ "Upgrade the database from a previous format.
+This function increases the size of the structs in the database."
+ (cl-loop with feed-size = (length (elfeed-feed--create))
+ for feed hash-values in elfeed-db-feeds
+ using (hash-key id)
+ unless (elfeed-feed-p feed)
+ do (setf (gethash id elfeed-db-feeds)
+ (elfeed-resize-vector feed feed-size)))
+ (cl-loop with entry-size = (length (elfeed-entry--create))
+ for entry hash-values in elfeed-db-entries
+ using (hash-key id)
+ unless (elfeed-entry-p entry)
+ do (setf (gethash id elfeed-db-entries)
+ (elfeed-resize-vector entry entry-size)))
+ (plist-put elfeed-db :version elfeed-db-version)
+ elfeed-db-version)
+
+(defun elfeed-db-load ()
+ "Load the database index from the filesystem."
+ (let ((index (expand-file-name "index" elfeed-db-directory)))
+ (if (not (file-exists-p index))
+ (let ((db (setf elfeed-db (list :version elfeed-db-version))))
+ (plist-put db :feeds (make-hash-table :test 'equal))
+ (plist-put db :entries (make-hash-table :test 'equal))
+ ;; Compiler will warn about this (bug#15327):
+ (plist-put db :index (avl-tree-create #'elfeed-db-compare)))
+ (with-current-buffer (find-file-noselect index)
+ (goto-char (point-min))
+ (setf elfeed-db (read (current-buffer)))
+ (kill-buffer)))
+ (setf elfeed-db-feeds (plist-get elfeed-db :feeds)
+ elfeed-db-entries (plist-get elfeed-db :entries)
+ elfeed-db-index (plist-get elfeed-db :index)
+ ;; Internal function use required for security!
+ (avl-tree--cmpfun elfeed-db-index) #'elfeed-db-compare)
+ (when (version< (or (plist-get elfeed-db :version) "0") elfeed-db-version)
+ (elfeed-db-upgrade))))
+
+(defun elfeed-db-ensure ()
+ "Ensure that the database has been loaded."
+ (when (null elfeed-db) (elfeed-db-load)))
+
+(defun elfeed-db-size ()
+ "Return a count of the number of entries in the database."
+ (let ((count-table (hash-table-count elfeed-db-entries))
+ (count-tree (avl-tree-size elfeed-db-index)))
+ (if (= count-table count-tree)
+ count-table
+ (error "Elfeed database error: entry count mismatch."))))
+
+;; Metadata:
+
+(defun elfeed-meta--plist (thing)
+ "Get the metadata plist for THING."
+ (cl-typecase thing
+ (elfeed-feed (elfeed-feed-meta thing))
+ (elfeed-entry (elfeed-entry-meta thing))
+ (otherwise (error "Don't know how to access metadata on %S" thing))))
+
+(defun elfeed-meta--set-plist (thing plist)
+ "Set the metadata plist on THING to PLIST."
+ (cl-typecase thing
+ (elfeed-feed (setf (elfeed-feed-meta thing) plist))
+ (elfeed-entry (setf (elfeed-entry-meta thing) plist))
+ (otherwise (error "Don't know how to access metadata on %S" thing))))
+
+(defun elfeed-meta (thing key)
+ "Access metadata for THING (entry, feed) under KEY."
+ (plist-get (elfeed-meta--plist thing) key))
+
+(defun elfeed-meta--put (thing key value)
+ "Set metadata to VALUE on THING under KEY."
+ (when (not (elfeed-readable-p value)) (error "New value must be readable."))
+ (let ((new-plist (plist-put (elfeed-meta--plist thing) key value)))
+ (prog1 value
+ (elfeed-meta--set-plist thing new-plist))))
+
+(gv-define-simple-setter elfeed-meta elfeed-meta--put)
+
+;; Filesystem storage:
+
+(defvar elfeed-ref-archive nil
+ "Index of archived/packed content.")
+
+(defvar elfeed-ref-cache nil
+ "Temporary storage of the full archive content.")
+
+(cl-defstruct (elfeed-ref (:constructor elfeed-ref--create))
+ id)
+
+(defun elfeed-ref--file (ref)
+ "Determine the storage filename for REF."
+ (let* ((id (elfeed-ref-id ref))
+ (root (expand-file-name "data" elfeed-db-directory))
+ (subdir (expand-file-name (substring id 0 2) root)))
+ (expand-file-name id subdir)))
+
+(cl-defun elfeed-ref-archive-filename (&optional (suffix ""))
+ "Return the base filename of the archive files."
+ (concat (expand-file-name "data/archive" elfeed-db-directory) suffix))
+
+(defun elfeed-ref-archive-load ()
+ "Load the archived ref index."
+ (let ((archive-index (elfeed-ref-archive-filename ".index")))
+ (if (file-exists-p archive-index)
+ (with-temp-buffer
+ (insert-file-contents archive-index)
+ (setf elfeed-ref-archive (read (current-buffer))))
+ (setf elfeed-ref-archive :empty))))
+
+(defun elfeed-ref-archive-ensure ()
+ "Ensure that the archive index is loaded."
+ (when (null elfeed-ref-archive) (elfeed-ref-archive-load)))
+
+(defun elfeed-ref-exists-p (ref)
+ "Return true if REF can be dereferenced."
+ (elfeed-ref-archive-ensure)
+ (or (and (hash-table-p elfeed-ref-archive)
+ (not (null (gethash (elfeed-ref-id ref) elfeed-ref-archive))))
+ (file-exists-p (elfeed-ref--file ref))))
+
+(defun elfeed-deref (ref)
+ "Fetch the content behind the reference, or nil if non-existent."
+ (elfeed-ref-archive-ensure)
+ (if (not (elfeed-ref-p ref))
+ ref
+ (let ((index (and (hash-table-p elfeed-ref-archive)
+ (gethash (elfeed-ref-id ref) elfeed-ref-archive)))
+ (archive-file (elfeed-ref-archive-filename ".gz")))
+ (if (and index (file-exists-p archive-file))
+ (progn
+ (when (null elfeed-ref-cache)
+ (with-temp-buffer
+ (insert-file-contents archive-file)
+ (setf elfeed-ref-cache (buffer-string)))
+ ;; Clear cache on next turn.
+ (run-at-time 0 nil (lambda () (setf elfeed-ref-cache nil))))
+ (substring elfeed-ref-cache (car index) (cdr index)))
+ (let ((file (elfeed-ref--file ref)))
+ (when (file-exists-p file)
+ (with-temp-buffer
+ (insert-file-contents file)
+ (buffer-string))))))))
+
+(defun elfeed-ref (content)
+ "Create a reference to CONTENT, to be persistently stored."
+ (if (elfeed-ref-p content)
+ content
+ (let* ((id (secure-hash 'sha1 (encode-coding-string content 'utf-8 t)))
+ (ref (elfeed-ref--create :id id))
+ (file (elfeed-ref--file ref)))
+ (prog1 ref
+ (unless (elfeed-ref-exists-p ref)
+ (mkdir (file-name-directory file) t)
+ (let ((coding-system-for-write 'utf-8)
+ ;; Content data loss is a tolerable risk.
+ ;; Fsync will occur soon on index write anyway.
+ (write-region-inhibit-fsync t))
+ (with-temp-file file
+ (insert content))))))))
+
+(defun elfeed-deref-entry (entry)
+ "Move ENTRY's content to filesystem storage. Return the entry."
+ (let ((content (elfeed-entry-content entry)))
+ (prog1 entry
+ (when (stringp content)
+ (setf (elfeed-entry-content entry) (elfeed-ref content))))))
+
+(defun elfeed-ref-delete (ref)
+ "Remove the content behind REF from the database."
+ (ignore-errors
+ (delete-file (elfeed-ref--file ref))))
+
+(defun elfeed-db-gc (&optional stats-p)
+ "Clean up unused content from the content database. If STATS is
+true, return the space cleared in bytes."
+ (let* ((data (expand-file-name "data" elfeed-db-directory))
+ (dirs (directory-files data t "^[0-9a-z]\\{2\\}$"))
+ (ids (cl-mapcan (lambda (d) (directory-files d nil nil t)) dirs))
+ (table (make-hash-table :test 'equal)))
+ (dolist (id ids)
+ (setf (gethash id table) nil))
+ (with-elfeed-db-visit (entry _)
+ (let ((content (elfeed-entry-content entry)))
+ (when (elfeed-ref-p content)
+ (setf (gethash (elfeed-ref-id content) table) t))))
+ (cl-loop for id hash-keys of table using (hash-value used)
+ for used-p = (or used (member id '("." "..")))
+ when (and (not used-p) stats-p)
+ sum (let* ((ref (elfeed-ref--create :id id))
+ (file (elfeed-ref--file ref)))
+ (* 1.0 (nth 7 (file-attributes file))))
+ unless used-p
+ do (elfeed-ref-delete (elfeed-ref--create :id id))
+ finally (cl-loop for dir in dirs
+ when (elfeed-directory-empty-p dir)
+ do (delete-directory dir)))))
+
+(defun elfeed-db-pack ()
+ "Pack all content into a single archive for efficient storage."
+ (let ((coding-system-for-write 'utf-8)
+ (next-archive (make-hash-table :test 'equal))
+ (packed ()))
+ (make-directory (expand-file-name "data" elfeed-db-directory) t)
+ (with-temp-file (elfeed-ref-archive-filename ".gz")
+ (with-elfeed-db-visit (entry _)
+ (let ((ref (elfeed-entry-content entry))
+ (start (1- (point))))
+ (when (elfeed-ref-p ref)
+ (let ((content (elfeed-deref ref)))
+ (when content
+ (push ref packed)
+ (insert content)
+ (setf (gethash (elfeed-ref-id ref) next-archive)
+ (cons start (1- (point))))))))))
+ (with-temp-file (elfeed-ref-archive-filename ".index")
+ (let ((standard-output (current-buffer))
+ (print-level nil)
+ (print-length nil)
+ (print-circle nil))
+ (prin1 next-archive)))
+ (setf elfeed-ref-cache nil)
+ (setf elfeed-ref-archive next-archive)
+ (mapc #'elfeed-ref-delete packed)
+ :success))
+
+(defun elfeed-db-compact ()
+ "Minimize the Elfeed database storage size on the filesystem.
+This requires that auto-compression-mode can handle
+gzip-compressed files, so the gzip program must be in your PATH."
+ (interactive)
+ (unless (elfeed-gzip-supported-p)
+ (error "aborting compaction: gzip auto-compression-mode unsupported"))
+ (elfeed-db-pack)
+ (elfeed-db-gc))
+
+(defun elfeed-db-gc-safe ()
+ "Run `elfeed-db-gc' without triggering any errors, for use as a safe hook."
+ (ignore-errors (elfeed-db-gc)))
+
+(unless noninteractive
+ (add-hook 'kill-emacs-hook #'elfeed-db-gc-safe :append)
+ (add-hook 'kill-emacs-hook #'elfeed-db-save))
+
+(provide 'elfeed-db)
+
+;;; elfeed-db.el ends here
--- /dev/null
+;;; elfeed-lib.el --- misc functions for elfeed -*- lexical-binding: t; -*-
+
+;; This is free and unencumbered software released into the public domain.
+
+;;; Commentary:
+
+;; These are general functions that aren't specific to web feeds. It's
+;; a library of useful functions to Elfeed.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'time-date)
+(require 'url-parse)
+
+(defun elfeed-expose (function &rest args)
+ "Return an interactive version of FUNCTION, 'exposing' it to the user."
+ (lambda () (interactive) (apply function args)))
+
+(defun elfeed-goto-line (n)
+ "Like `goto-line' but for non-interactive use."
+ (goto-char (point-min))
+ (forward-line (1- n)))
+
+(defun elfeed-kill-buffer ()
+ "Kill the current buffer."
+ (interactive)
+ (kill-buffer (current-buffer)))
+
+(defun elfeed-kill-line ()
+ "Clear out the current line without touching anything else."
+ (beginning-of-line)
+ (let ((start (point)))
+ (end-of-line)
+ (delete-region start (point))))
+
+(defun elfeed-time-duration (time)
+ "Turn a time expression into a number of seconds. Uses
+`timer-duration' but allows a bit more flair."
+ (if (numberp time)
+ time
+ (timer-duration (replace-regexp-in-string "\\(ago\\|old\\|-\\)" "" time))))
+
+(defun elfeed-looks-like-url-p (string)
+ "Return true if STRING looks like it could be a URL."
+ (and (stringp string)
+ (not (string-match-p "[ \n\t\r]" string))
+ (not (null (url-type (url-generic-parse-url string))))))
+
+(defun elfeed-format-column (string width &optional align)
+ "Return STRING truncated or padded to WIDTH following ALIGNment.
+Align should be a keyword :left or :right."
+ (if (<= width 0)
+ ""
+ (format (format "%%%s%d.%ds" (if (eq align :left) "-" "") width width)
+ string)))
+
+(defun elfeed-clamp (min value max)
+ "Clamp a value between two values."
+ (min max (max min value)))
+
+(defun elfeed-valid-regexp-p (regexp)
+ "Return t if REGEXP is a valid REGEXP."
+ (ignore-errors
+ (prog1 t
+ (string-match-p regexp ""))))
+
+(defun elfeed-cleanup (name)
+ "Trim trailing and leading spaces and collapse multiple spaces."
+ (let ((trim (replace-regexp-in-string "[\n\t]+" " " (or name ""))))
+ (replace-regexp-in-string "^ +\\| +$" "" trim)))
+
+(defun elfeed-float-time (&optional date)
+ "Like `float-time' but accept anything reasonable for DATE,
+defaulting to the current time if DATE could not be parsed. Date
+is allowed to be relative to now (`elfeed-time-duration')."
+ (cl-typecase date
+ (string
+ (let ((duration (elfeed-time-duration date)))
+ (if duration
+ (- (float-time) duration)
+ (let ((time (ignore-errors (date-to-time date))))
+ (if (equal time '(14445 17280)) ; date-to-time silently failed
+ (float-time)
+ (float-time time))))))
+ (integer date)
+ (list (float-time date))
+ (otherwise (float-time))))
+
+(defun elfeed-xml-parse-region (&optional beg end buffer parse-dtd parse-ns)
+ "Decode (if needed) and parse XML file. Uses coding system from
+XML encoding declaration."
+ (let ((coding-system nil))
+ (progn
+ (unless beg (setq beg (point-min)))
+ (unless end (setq end (point-max)))
+ (goto-char beg)
+ (if (re-search-forward
+ "<\\?xml.*?encoding=[\"']\\([^\"']+\\)[\"'].*?\\?>" nil t)
+ (setq coding-system
+ (ignore-errors (check-coding-system
+ (intern (downcase (match-string 1)))))))
+ (when coding-system
+ (setq end (+ beg
+ (decode-coding-region beg end coding-system))))
+ (goto-char beg)))
+ (xml-parse-region beg end buffer parse-dtd parse-ns))
+
+(defun elfeed-directory-empty-p (dir)
+ "Return non-nil if DIR is empty."
+ (null (cddr (directory-files dir))))
+
+(defun elfeed-slurp (file &optional literally)
+ "Return the contents of FILE as a string."
+ (with-temp-buffer
+ (if literally
+ (insert-file-contents-literally file)
+ (insert-file-contents file))
+ (buffer-string)))
+
+(cl-defun elfeed-spit (file string &key fsync append (encoding 'utf-8))
+ "Write STRING to FILE."
+ (let ((coding-system-for-write encoding)
+ (write-region-inhibit-fsync (not fsync)))
+ (with-temp-buffer
+ (insert string)
+ (write-region nil nil file append 0))))
+
+(defvar elfeed-gzip-supported-p--cache :unknown
+ "To avoid running the relatively expensive test more than once.")
+
+(defun elfeed-gzip-supported-p ()
+ "Return non-nil if `auto-compression-mode' can handle gzip."
+ (if (not (eq elfeed-gzip-supported-p--cache :unknown))
+ elfeed-gzip-supported-p--cache
+ (setf elfeed-gzip-supported-p--cache
+ (and (executable-find "gzip")
+ (ignore-errors
+ (save-window-excursion
+ (let ((file (make-temp-file "gziptest" nil ".gz"))
+ (data (cl-loop for i from 32 to 3200
+ collect i into chars
+ finally
+ (return (apply #'string chars)))))
+ (unwind-protect
+ (progn
+ (elfeed-spit file data)
+ (and (string= data (elfeed-slurp file))
+ (not (string= data (elfeed-slurp file t)))))
+ (delete-file file)))))))))
+
+(defun elfeed-libxml-supported-p ()
+ "Return non-nil if `libxml-parse-html-region' is available."
+ (with-temp-buffer
+ (insert "<html></html>")
+ (and (fboundp 'libxml-parse-html-region)
+ (not (null (libxml-parse-html-region (point-min) (point-max)))))))
+
+(defun elfeed-keyword->symbol (keyword)
+ "If a keyword, convert KEYWORD into a plain symbol (remove the colon)."
+ (if (keywordp keyword)
+ (intern (substring (symbol-name keyword) 1))
+ keyword))
+
+(defun elfeed-resize-vector (vector length)
+ "Return a copy of VECTOR set to size LENGTH."
+ (let ((new-vector (make-vector length nil)))
+ (prog1 new-vector ; don't use dotimes result (bug#16206)
+ (dotimes (i (min (length new-vector) (length vector)))
+ (setf (aref new-vector i) (aref vector i))))))
+
+(defun elfeed-readable-p (value)
+ "Return non-nil if VALUE can be serialized."
+ (condition-case _
+ (prog1 t (read (prin1-to-string value)))
+ (error nil)))
+
+(defun elfeed-strip-properties (string)
+ "Return a copy of STRING with all properties removed.
+If STRING is nil, returns nil."
+ (when string
+ (let ((copy (copy-sequence string)))
+ (prog1 copy
+ (set-text-properties 0 (length copy) nil copy)))))
+
+(defun elfeed-clipboard-get ()
+ "Try to get a sensible value from the system clipboard.
+On systems running X, it will try to use the PRIMARY selection
+first, then fall back onto the standard clipboard like other
+systems."
+ (elfeed-strip-properties
+ (or (and (fboundp 'x-get-selection-value)
+ (funcall 'x-get-selection-value))
+ (and (functionp interprogram-paste-function)
+ (funcall interprogram-paste-function))
+ (and (fboundp 'w32-get-clipboard-data)
+ (funcall 'w32-get-clipboard-data))
+ (current-kill 0 :non-destructively))))
+
+(defun elfeed-move-to-first-empty-line ()
+ "Place point after first blank line, for use with `url-retrieve'.
+If no such line exists, point is left in place."
+ (let ((start (point)))
+ (setf (point) (point-min))
+ (unless (search-forward-regexp "^$" nil t)
+ (setf (point) start))))
+
+(provide 'elfeed-lib)
+
+;;; elfeed-lib.el ends here
--- /dev/null
+(define-package "elfeed" "20150101.1457" "an Emacs Atom/RSS feed reader"
+ '((emacs "24.1")))
+;; Local Variables:
+;; no-byte-compile: t
+;; End:
--- /dev/null
+;;; elfeed-search.el --- list feed entries -*- lexical-binding: t; -*-
+
+;; This is free and unencumbered software released into the public domain.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'browse-url)
+(require 'wid-edit) ; widget-inactive face
+
+(provide 'elfeed-search)
+
+(require 'elfeed)
+(require 'elfeed-db)
+(require 'elfeed-lib)
+(require 'elfeed-show)
+
+(defvar elfeed-search-entries ()
+ "List of the entries currently on display.")
+
+(defvar elfeed-search-filter-history nil
+ "Filter history for `completing-read'.")
+
+(defvar elfeed-search-last-update 0
+ "The last time the buffer was redrawn in epoch seconds.")
+
+(defvar elfeed-search-refresh-timer nil
+ "The timer used to keep things updated as the database updates.")
+
+(defcustom elfeed-search-filter "@6-months-ago +unread"
+ "Query string filtering shown entries."
+ :group 'elfeed
+ :type 'string)
+
+(defcustom elfeed-sort-order 'descending
+ "The order in which entries should be displayed, by time."
+ :group 'elfeed
+ :type '(choice (const descending) (const ascending)))
+
+(defcustom elfeed-search-refresh-rate 3
+ "How often the buffer should update against the datebase in seconds."
+ :group 'elfeed
+ :type 'number)
+
+(defcustom elfeed-search-clipboard-type 'PRIMARY
+ "Selects the clipboard `elfeed-search-yank' should use.
+Choices are the symbols PRIMARY, SECONDARY, or CLIPBOARD."
+ :group 'elfeed
+ :type '(choice (const PRIMARY) (const SECONDARY) (const CLIPBOARD)))
+
+(defvar elfeed-search-live nil
+ "When true, Elfeed is currently reading a filter from the minibuffer.")
+
+(defvar elfeed-search-cache (make-hash-table :test 'equal)
+ "Cache the generated entry buffer lines and such.")
+
+(defvar elfeed-search--offset 2
+ "Offset between line numbers and entry list position.")
+
+(defalias 'elfeed-search-tag-all-unread
+ (elfeed-expose #'elfeed-search-tag-all 'unread))
+
+(defalias 'elfeed-search-untag-all-unread
+ (elfeed-expose #'elfeed-search-untag-all 'unread))
+
+(defalias 'elfeed-search-update--force
+ (elfeed-expose #'elfeed-search-update :force))
+
+(defvar elfeed-search-mode-map
+ (let ((map (make-sparse-keymap)))
+ (prog1 map
+ (suppress-keymap map)
+ (define-key map "q" 'quit-window)
+ (define-key map "g" 'elfeed-search-update--force)
+ (define-key map "G" 'elfeed-update)
+ (define-key map (kbd "RET") 'elfeed-search-show-entry)
+ (define-key map "s" 'elfeed-search-live-filter)
+ (define-key map "S" 'elfeed-search-set-filter)
+ (define-key map "b" 'elfeed-search-browse-url)
+ (define-key map "y" 'elfeed-search-yank)
+ (define-key map "u" 'elfeed-search-tag-all-unread)
+ (define-key map "r" 'elfeed-search-untag-all-unread)
+ (define-key map "n" 'next-line)
+ (define-key map "p" 'previous-line)
+ (define-key map "+" 'elfeed-search-tag-all)
+ (define-key map "-" 'elfeed-search-untag-all)))
+ "Keymap for elfeed-search-mode.")
+
+(defun elfeed-search-mode ()
+ "Major mode for listing elfeed feed entries.
+\\{elfeed-search-mode-map}"
+ (interactive)
+ (kill-all-local-variables)
+ (use-local-map elfeed-search-mode-map)
+ (setq major-mode 'elfeed-search-mode
+ mode-name "elfeed-search"
+ truncate-lines t
+ buffer-read-only t)
+ (buffer-disable-undo)
+ (hl-line-mode)
+ (make-local-variable 'elfeed-search-entries)
+ (make-local-variable 'elfeed-search-filter)
+ (when (null elfeed-search-refresh-timer)
+ (setf elfeed-search-refresh-timer
+ (run-at-time elfeed-search-refresh-rate elfeed-search-refresh-rate
+ #'elfeed-search-update)))
+ (add-hook 'kill-buffer-hook #'elfeed-db-save t t)
+ (add-hook 'kill-buffer-hook
+ (lambda ()
+ (ignore-errors (cancel-timer elfeed-search-refresh-timer))
+ (setf elfeed-search-refresh-timer nil))
+ t t)
+ (elfeed-search-update :force)
+ (run-hooks 'elfeed-search-mode-hook))
+
+(defun elfeed-search-buffer ()
+ (get-buffer-create "*elfeed-search*"))
+
+(defun elfeed-search-format-date (date)
+ "Format a date for printing in elfeed-search-mode."
+ (let ((string (format-time-string "%Y-%m-%d" (seconds-to-time date))))
+ (format "%-10.10s" string)))
+
+(defface elfeed-search-date-face
+ '((((class color) (background light)) (:foreground "#aaa"))
+ (((class color) (background dark)) (:foreground "#77a")))
+ "Face used in search mode for dates."
+ :group 'elfeed)
+
+(defface elfeed-search-title-face
+ '((((class color) (background light)) (:foreground "#000"))
+ (((class color) (background dark)) (:foreground "#fff")))
+ "Face used in search mode for titles."
+ :group 'elfeed)
+
+(defface elfeed-search-feed-face
+ '((((class color) (background light)) (:foreground "#aa0"))
+ (((class color) (background dark)) (:foreground "#ff0")))
+ "Face used in search mode for feed titles."
+ :group 'elfeed)
+
+(defface elfeed-search-tag-face
+ '((((class color) (background light)) (:foreground "#070"))
+ (((class color) (background dark)) (:foreground "#0f0")))
+ "Face used in search mode for tags."
+ :group 'elfeed)
+
+(defcustom elfeed-search-title-max-width 70
+ "Maximum column width for titles in the elfeed-search buffer.
+Clear `elfeed-search-cache' (or restart Emacs) after setting."
+ :group 'elfeed
+ :type 'integer)
+
+(defcustom elfeed-search-title-min-width 16
+ "Minimum column width for titles in the elfeed-search buffer.
+Clear `elfeed-search-cache' (or restart Emacs) after setting."
+ :group 'elfeed
+ :type 'integer)
+
+(defcustom elfeed-search-trailing-width 30
+ "Space reserved for displaying the feed and tag information.
+Clear `elfeed-search-cache' (or restart Emacs) after setting."
+ :group 'elfeed
+ :type 'integer)
+
+(defun elfeed-search-print (entry)
+ "Print ENTRY to the buffer."
+ (let* ((date (elfeed-search-format-date (elfeed-entry-date entry)))
+ (title (or (elfeed-entry-title entry) ""))
+ (title-faces '(elfeed-search-title-face))
+ (feed (elfeed-entry-feed entry))
+ (feed-title (if feed (elfeed-feed-title feed)))
+ (tags (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)))
+ (when (elfeed-tagged-p 'unread entry)
+ (push 'bold title-faces))
+ (insert (propertize date 'face 'elfeed-search-date-face) " ")
+ (insert (propertize title-column 'face title-faces) " ")
+ (when feed-title
+ (insert (propertize feed-title 'face 'elfeed-search-feed-face) " "))
+ (when tags
+ (insert "(" tags-str ")"))))
+
+(defun elfeed-search-parse-filter (filter)
+ "Parse the elements of a search filter."
+ (let ((must-have ())
+ (must-not-have ())
+ (after nil)
+ (matches ())
+ (not-matches ()))
+ (cl-loop for element in (split-string filter)
+ for type = (aref element 0)
+ do (cl-case type
+ (?+ (push (intern (substring element 1)) must-have))
+ (?- (push (intern (substring element 1)) must-not-have))
+ (?@ (setf after (elfeed-time-duration (substring element 1))))
+ (?! (let ((re (substring element 1)))
+ (when (elfeed-valid-regexp-p re)
+ (push re not-matches))))
+ (otherwise (when (elfeed-valid-regexp-p element)
+ (push element matches)))))
+ (list after must-have must-not-have matches not-matches)))
+
+(defun elfeed-search-filter (filter entry feed)
+ "Filter out only entries that match the filter. See
+`elfeed-search-set-filter' for format/syntax documentation."
+ (cl-destructuring-bind (after must-have must-not-have matches not-matches) filter
+ (let* ((tags (elfeed-entry-tags entry))
+ (date (elfeed-entry-date entry))
+ (age (- (float-time) date))
+ (title (elfeed-entry-title entry))
+ (link (elfeed-entry-link entry))
+ (feed-title (or (elfeed-feed-title feed) "")))
+ (when (and after (> age after))
+ (elfeed-db-return))
+ (and (cl-every (lambda (tag) (member tag tags)) must-have)
+ (cl-notany (lambda (tag) (member tag tags)) must-not-have)
+ (or (null matches)
+ (cl-every
+ (lambda (m)
+ (or (and title (string-match-p m title))
+ (and link (string-match-p m link))
+ (and feed-title (string-match-p m feed-title))))
+ matches))
+ (cl-notany (lambda (m)
+ (or (and title (string-match-p m title))
+ (and link (string-match-p m link))
+ (and feed-title (string-match-p m feed-title))))
+ not-matches)))))
+
+(defun elfeed-search--prompt (current)
+ "Prompt for a new filter, starting with CURRENT."
+ (read-from-minibuffer
+ "Filter: "
+ (if (or (string= "" current)
+ (string-match-p " $" current))
+ current
+ (concat current " "))
+ nil nil 'elfeed-search-filter-history))
+
+(defun elfeed-search-set-filter (new-filter)
+ "Set a new search filter for the elfeed-search buffer.
+
+When given a prefix argument, the current filter is not displayed
+in the minibuffer when prompting for a new filter.
+
+Any component beginning with a + or - is treated as a tag. If +
+the tag must be present on the entry. If - the tag must *not* be
+present on the entry. Ex. \"+unread\" or \"+unread -comic\".
+
+Any component beginning with an @ is an age limit. No posts older
+than this are allowed. Ex. \"@3-days-ago\" or \"@1-year-old\".
+
+Every other space-seperated element is treated like a regular
+expression, matching against entry link, title, and feed title."
+ (interactive (list (elfeed-search--prompt
+ (if current-prefix-arg "" elfeed-search-filter))))
+ (with-current-buffer (elfeed-search-buffer)
+ (setf elfeed-search-filter new-filter)
+ (elfeed-search-update :force)))
+
+(defun elfeed-search-insert-header-text (text)
+ "Insert TEXT into buffer using header face."
+ (insert (propertize text 'face '(widget-inactive italic))))
+
+(defun elfeed-search-insert-intro-header ()
+ "Insert the intro header with buttons."
+ (cl-flet ((button (f)
+ (insert-button (symbol-name f)
+ 'follow-link t
+ 'action (lambda (_) (call-interactively f)))))
+ (elfeed-search-insert-header-text "Database empty. Use ")
+ (button 'elfeed-add-feed)
+ (elfeed-search-insert-header-text ", or ")
+ (button 'elfeed-load-opml)
+ (elfeed-search-insert-header-text ", or ")
+ (button 'elfeed-update)
+ (elfeed-search-insert-header-text ".")))
+
+(defun elfeed-search-insert-header ()
+ "Insert a one-line status header."
+ (if (or elfeed-waiting elfeed-connections)
+ (elfeed-search-insert-header-text
+ (format "%d feeds pending, %d in process ..."
+ (length elfeed-waiting) (length elfeed-connections)))
+ (let ((time (seconds-to-time (elfeed-db-last-update))))
+ (if (zerop (float-time time))
+ (elfeed-search-insert-intro-header)
+ (elfeed-search-insert-header-text
+ (format "Database last updated %s"
+ (format-time-string "%A, %B %d %Y %H:%M:%S %Z" time)))))))
+
+(defun elfeed-search--update-list ()
+ "Update `elfeed-search-filter' list."
+ (let* ((filter (elfeed-search-parse-filter elfeed-search-filter))
+ (head (list nil))
+ (tail head))
+ (with-elfeed-db-visit (entry feed)
+ (when (elfeed-search-filter filter entry feed)
+ (setf (cdr tail) (list entry)
+ tail (cdr tail))))
+ (setf elfeed-search-entries
+ (if (eq elfeed-sort-order 'ascending)
+ (nreverse (cdr head))
+ (cdr head)))))
+
+(defmacro elfeed-save-excursion (&rest body)
+ "Like `save-excursion', but by entry/line/column instead of point."
+ (declare (indent defun))
+ `(let ((entry (elfeed-search-selected :single))
+ (line (line-number-at-pos))
+ (column (current-column)))
+ (unwind-protect
+ (progn ,@body)
+ (let ((entry-position (cl-position entry elfeed-search-entries)))
+ (elfeed-goto-line (if entry-position
+ (+ elfeed-search--offset entry-position)
+ line))
+ (move-to-column column)))))
+
+(defun elfeed-search-update (&optional force)
+ "Update the display to match the database."
+ (interactive)
+ (with-current-buffer (elfeed-search-buffer)
+ (if (or force (and (not elfeed-search-live)
+ (< elfeed-search-last-update (elfeed-db-last-update))))
+ (elfeed-save-excursion
+ (let ((inhibit-read-only t)
+ (standard-output (current-buffer)))
+ (erase-buffer)
+ (elfeed-search-insert-header)
+ (insert "\n")
+ (elfeed-search--update-list)
+ (cl-loop for entry in elfeed-search-entries
+ when (gethash (list (window-width) entry)
+ elfeed-search-cache)
+ do (insert it)
+ else
+ do (insert
+ (with-temp-buffer
+ (elfeed-search-print entry)
+ (setf (gethash (list (window-width)
+ (copy-sequence entry))
+ elfeed-search-cache)
+ (buffer-string))))
+ do (insert "\n"))
+ (insert "End of entries.\n")
+ (setf elfeed-search-last-update (float-time))))
+ (let ((inhibit-read-only t))
+ (elfeed-save-excursion
+ (goto-char (point-min))
+ (elfeed-kill-line)
+ (elfeed-search-insert-header))))))
+
+(defun elfeed-search-update-line (&optional n)
+ "Redraw the current line."
+ (let ((inhibit-read-only t))
+ (save-excursion
+ (when n (elfeed-goto-line n))
+ (let ((entry (elfeed-search-selected :ignore-region)))
+ (when entry
+ (elfeed-kill-line)
+ (elfeed-search-print entry))))))
+
+(defun elfeed-search-update-entry (entry)
+ "Redraw a specific entry."
+ (let ((n (cl-position entry elfeed-search-entries)))
+ (when n (elfeed-search-update-line (+ elfeed-search--offset n)))))
+
+(defun elfeed-search-selected (&optional ignore-region)
+ "Return a list of the currently selected feeds."
+ (let ((use-region (and (not ignore-region) (use-region-p))))
+ (let ((start (if use-region (region-beginning) (point)))
+ (end (if use-region (region-end) (point))))
+ (cl-loop for line from (line-number-at-pos start)
+ to (line-number-at-pos end)
+ for offset = (- line elfeed-search--offset)
+ when (and (>= offset 0) (nth offset elfeed-search-entries))
+ collect it into selected
+ finally (return (if ignore-region (car selected) selected))))))
+
+(defun elfeed-search-browse-url ()
+ "Visit the current entry in your browser using `browse-url'."
+ (interactive)
+ (let ((entries (elfeed-search-selected)))
+ (cl-loop for entry in entries
+ do (elfeed-untag entry 'unread)
+ when (elfeed-entry-link entry)
+ do (browse-url it))
+ (mapc #'elfeed-search-update-entry entries)
+ (unless (use-region-p) (forward-line))))
+
+(defun elfeed-search-yank ()
+ "Copy the selected feed item to "
+ (interactive)
+ (let* ((entry (elfeed-search-selected :ignore-region))
+ (link (and entry (elfeed-entry-link entry))))
+ (when entry
+ (elfeed-untag entry 'unread)
+ (x-set-selection elfeed-search-clipboard-type link)
+ (message "Copied: %s" link)
+ (elfeed-search-update-line)
+ (forward-line))))
+
+(defun elfeed-search-tag-all (tag)
+ "Apply TAG to all selected entries."
+ (interactive (list (intern (read-from-minibuffer "Tag: "))))
+ (let ((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 elfeed-search-untag-all (tag)
+ "Remove TAG from all selected entries."
+ (interactive (list (intern (read-from-minibuffer "Tag: "))))
+ (let ((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))))
+
+(defun elfeed-search-show-entry (entry)
+ "Display the currently selected item in a buffer."
+ (interactive (list (elfeed-search-selected :ignore-region)))
+ (when (elfeed-entry-p entry)
+ (elfeed-untag entry 'unread)
+ (elfeed-search-update-entry entry)
+ (forward-line)
+ (elfeed-show-entry entry)))
+
+;; Live Filters
+
+(defun elfeed-search--minibuffer-setup ()
+ "Set up the minibuffer for live filtering."
+ (when elfeed-search-live
+ (add-hook 'post-command-hook 'elfeed-search--live-update nil :local)))
+
+(add-hook 'minibuffer-setup-hook 'elfeed-search--minibuffer-setup)
+
+(defun elfeed-search--live-update ()
+ "Update the elfeed-search buffer based on the contents of the minibuffer."
+ (when elfeed-search-live
+ (let ((buffer (elfeed-search-buffer))
+ (current-filter (minibuffer-contents-no-properties)))
+ (when buffer
+ (with-current-buffer buffer
+ (let ((elfeed-search-filter current-filter))
+ (elfeed-search-update :force)))))))
+
+(defun elfeed-search-live-filter ()
+ "Filter the elfeed-search buffer as the filter is written."
+ (interactive)
+ (unwind-protect
+ (let ((elfeed-search-live t))
+ (setq elfeed-search-filter
+ (read-from-minibuffer "Filter: " elfeed-search-filter)))
+ (elfeed-search-update :force)))
+
+;;; elfeed-search.el ends here
--- /dev/null
+;;; elfeed-show.el --- display feed entries -*- lexical-binding: t; -*-
+
+;; This is free and unencumbered software released into the public domain.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'shr)
+(require 'url-parse)
+(require 'browse-url)
+(require 'message) ; faces
+(require 'elfeed-db)
+(require 'elfeed-lib)
+(require 'elfeed-search)
+
+(defcustom elfeed-show-truncate-long-urls t
+ "When non-nil, use an ellipsis to shorten very long displayed URLs."
+ :group 'elfeed
+ :type 'bool)
+
+(defvar elfeed-show-entry nil
+ "The entry being displayed in this buffer.")
+
+(defvar elfeed-show-mode-map
+ (let ((map (make-sparse-keymap)))
+ (prog1 map
+ (suppress-keymap map)
+ (define-key map "q" 'elfeed-kill-buffer)
+ (define-key map "g" 'elfeed-show-refresh)
+ (define-key map "n" 'elfeed-show-next)
+ (define-key map "p" 'elfeed-show-prev)
+ (define-key map "s" 'elfeed-show-new-live-search)
+ (define-key map "b" 'elfeed-show-visit)
+ (define-key map "y" 'elfeed-show-yank)
+ (define-key map "u" (elfeed-expose #'elfeed-show-tag 'unread))
+ (define-key map "+" 'elfeed-show-tag)
+ (define-key map "-" 'elfeed-show-untag)
+ (define-key map (kbd "SPC") 'scroll-up-command)
+ (define-key map (kbd "DEL") 'scroll-down-command)
+ (define-key map "\t" 'shr-next-link)
+ (define-key map [tab] 'shr-next-link)
+ (define-key map "\e\t" 'shr-previous-link)
+ (define-key map [backtab] 'shr-previous-link)
+ (define-key map [mouse-2] 'shr-browse-url)))
+ "Keymap for `elfeed-show-mode'.")
+
+(defun elfeed-show-mode ()
+ "Mode for displaying Elfeed feed entries.
+\\{elfeed-show-mode-map}"
+ (interactive)
+ (kill-all-local-variables)
+ (use-local-map elfeed-show-mode-map)
+ (setq major-mode 'elfeed-show-mode
+ mode-name "elfeed-show"
+ buffer-read-only t)
+ (buffer-disable-undo)
+ (make-local-variable 'elfeed-show-entry)
+ (run-hooks 'elfeed-show-mode-hook))
+
+(defun elfeed-insert-html (html &optional base-url)
+ "Converted HTML markup to a propertized string."
+ (shr-insert-document
+ (if (elfeed-libxml-supported-p)
+ (with-temp-buffer
+ ;; insert <base> to work around libxml-parse-html-region bug
+ (insert (format "<base href=\"%s\">" base-url))
+ (insert html)
+ (libxml-parse-html-region (point-min) (point-max) base-url))
+ '(i () "Elfeed: libxml2 functionality is unavailable"))))
+
+(cl-defun elfeed-insert-link (url &optional (content url))
+ "Insert a clickable hyperlink to URL titled CONTENT."
+ (when (and elfeed-show-truncate-long-urls
+ (integerp shr-width)
+ (> (length content) (- shr-width 8)))
+ (let ((len (- (/ shr-width 2) 10)))
+ (setq content (format "%s[...]%s"
+ (substring content 0 len)
+ (substring content (- len))))))
+ (elfeed-insert-html (format "<a href=\"%s\">%s</a>" url content)))
+
+(defun elfeed-compute-base (url)
+ "Return the base URL for URL, useful for relative paths."
+ (let ((obj (url-generic-parse-url url)))
+ (setf (url-filename obj) nil)
+ (setf (url-target obj) nil)
+ (url-recreate-url obj)))
+
+(defun elfeed-show-refresh ()
+ "Update the buffer to match the selected entry."
+ (interactive)
+ (let* ((inhibit-read-only t)
+ (title (elfeed-entry-title elfeed-show-entry))
+ (date (seconds-to-time (elfeed-entry-date elfeed-show-entry)))
+ (link (elfeed-entry-link elfeed-show-entry))
+ (tags (elfeed-entry-tags elfeed-show-entry))
+ (tagsstr (mapconcat #'symbol-name tags ", "))
+ (nicedate (format-time-string "%a, %e %b %Y %T %Z" date))
+ (content (elfeed-deref (elfeed-entry-content elfeed-show-entry)))
+ (type (elfeed-entry-content-type elfeed-show-entry))
+ (feed (elfeed-entry-feed elfeed-show-entry))
+ (feed-title (elfeed-feed-title feed))
+ (base (and feed (elfeed-compute-base (elfeed-feed-url feed)))))
+ (erase-buffer)
+ (insert (format (propertize "Title: %s\n" 'face 'message-header-name)
+ (propertize title 'face 'message-header-subject)))
+ (insert (format (propertize "Date: %s\n" 'face 'message-header-name)
+ (propertize nicedate 'face 'message-header-other)))
+ (insert (format (propertize "Feed: %s\n" 'face 'message-header-name)
+ (propertize feed-title 'face 'message-header-other)))
+ (when tags
+ (insert (format (propertize "Tags: %s\n" 'face 'message-header-name)
+ (propertize tagsstr 'face 'message-header-other))))
+ (insert (propertize "Link: " 'face 'message-header-name))
+ (elfeed-insert-link link link)
+ (insert "\n")
+ (cl-loop for enclosure in (elfeed-entry-enclosures elfeed-show-entry)
+ do (insert (propertize "Enclosure: " 'face 'message-header-name))
+ do (elfeed-insert-link (car enclosure))
+ do (insert "\n"))
+ (insert "\n")
+ (if content
+ (if (eq type 'html)
+ (elfeed-insert-html content base)
+ (insert content))
+ (insert (propertize "(empty)\n" 'face 'italic)))
+ (goto-char (point-min))))
+
+(defun elfeed-show-entry (entry)
+ "Display ENTRY in the current buffer."
+ (let ((title (elfeed-entry-title entry)))
+ (switch-to-buffer (get-buffer-create (format "*elfeed %s*" title)))
+ (unless (eq major-mode 'elfeed-show-mode)
+ (elfeed-show-mode))
+ (setq elfeed-show-entry entry)
+ (elfeed-show-refresh)))
+
+(defun elfeed-show-next ()
+ "Show the next item in the elfeed-search buffer."
+ (interactive)
+ (elfeed-kill-buffer)
+ (with-current-buffer (elfeed-search-buffer)
+ (call-interactively #'elfeed-search-show-entry)))
+
+(defun elfeed-show-prev ()
+ "Show the previous item in the elfeed-search buffer."
+ (interactive)
+ (elfeed-kill-buffer)
+ (with-current-buffer (elfeed-search-buffer)
+ (forward-line -2)
+ (call-interactively #'elfeed-search-show-entry)))
+
+(defun elfeed-show-new-live-search ()
+ "Kill the current buffer, search again in *elfeed-search*."
+ (interactive)
+ (elfeed-kill-buffer)
+ (elfeed)
+ (elfeed-search-live-filter))
+
+(defun elfeed-show-visit ()
+ "Visit the current entry in the browser."
+ (interactive)
+ (let ((link (elfeed-entry-link elfeed-show-entry)))
+ (when link
+ (message "Sent to browser: %s" link)
+ (browse-url link))))
+
+(defun elfeed-show-yank ()
+ "Copy the current entry link URL to the clipboard."
+ (interactive)
+ (let ((link (elfeed-entry-link elfeed-show-entry)))
+ (when link
+ (x-set-selection 'PRIMARY link)
+ (message "Yanked: %s" link))))
+
+(defun elfeed-show-tag (&rest tags)
+ "Add TAGS to the displayed entry."
+ (interactive (list (intern (read-from-minibuffer "Tag: "))))
+ (let ((entry elfeed-show-entry))
+ (apply #'elfeed-tag entry tags)
+ (with-current-buffer (elfeed-search-buffer)
+ (elfeed-search-update-entry entry))
+ (elfeed-show-refresh)))
+
+(defun elfeed-show-untag (&rest tags)
+ "Remove TAGS from the displayed entry."
+ (interactive (let* ((tags (elfeed-entry-tags elfeed-show-entry))
+ (names (mapcar #'symbol-name tags))
+ (select (completing-read "Untag: " names nil :match)))
+ (list (intern select))))
+ (let ((entry elfeed-show-entry))
+ (apply #'elfeed-untag entry tags)
+ (with-current-buffer (elfeed-search-buffer)
+ (elfeed-search-update-entry entry))
+ (elfeed-show-refresh)))
+
+(provide 'elfeed-show)
+
+;;; elfeed-show.el ends here
--- /dev/null
+;;; elfeed.el --- an Emacs Atom/RSS feed reader -*- lexical-binding: t; -*-
+
+;; This is free and unencumbered software released into the public domain.
+
+;; Author: Christopher Wellons <wellonsNOSPAMnullprogram.com>
+;; URL: https://github.com/skeeto/elfeed
+
+;;; Commentary:
+
+;; Elfreed is a web feed client for Emacs, inspired by notmuch. See
+;; the README for full documentation.
+
+;; Elfreed requires Emacs 24 (lexical closures).
+
+;;; History:
+
+;; Version 1.1.2: fixes
+;; * Fixed support for non-HTTP protocols
+;; * Add ! search syntax
+;; * Add elfeed-unjam
+;; * Combine regexp search terms by AND instead of OR
+;; * Link navigation keybindings (tab)
+;; * Add elfeed-show-truncate-long-urls
+;; * Add elfeed-search-filter customization
+;; * Various bug fixes
+;; Version 1.1.1: fixes
+;; * Fix database corruption issue
+;; * Properly handle URLs from XML
+;; * Slightly better RSS date guessing
+;; * User interface tweaks
+;; * Add `elfeed-sort-order'
+;; * Use tab and backtab to move between links
+;; Version 1.1.0: features and fixes
+;; * Autotagging support
+;; * Better database performance
+;; * Database packing
+;; * Arbitrary struct metadata
+;; * Added `elfeed-search-clipboard-type'
+;; * Update to cl-lib from cl
+;; * Lots of bug fixes
+;; Version 1.0.1: features and fixes
+;; * Live filter editing
+;; * Support for RSS 1.0
+;; * OPML import/export
+;; * Fix multibyte support (thanks cellscape)
+;; * Fix date-change database corruption
+;; * Add n and p bindings to elfeed-search, like notmuch
+;; * Friendlier intro header
+;; * Automated builds
+;; * Lots of small bug fixes
+;; Version 1.0.0: initial public release
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'xml)
+(require 'xml-query)
+(require 'url-parse)
+
+(defgroup elfeed nil
+ "An Emacs web feed reader."
+ :group 'comm)
+
+(defcustom elfeed-feeds ()
+ "List of all feeds that Elfeed should follow. You must add your
+feeds to this list.
+
+In its simplest form this will be a list of strings of feed URLs.
+Items in this list can also be list whose car is the feed URL
+and cdr is a list of symbols to be applied to all discovered
+entries as tags (\"autotags\"). For example,
+
+ (setq elfeed-feeds '(\"http://foo/\"
+ \"http://bar/\"
+ (\"http://baz/\" comic)))
+
+All entries from the \"baz\" feed will be tagged as \"comic\"
+when they are first discovered."
+ :group 'elfeed
+ :type 'list)
+
+(provide 'elfeed)
+
+(require 'elfeed-search)
+(require 'elfeed-lib)
+(require 'elfeed-db)
+
+(defcustom elfeed-initial-tags '(unread)
+ "Initial tags for new entries."
+ :group 'elfeed
+ :type 'list)
+
+;; Fetching:
+
+(defcustom elfeed-max-connections
+ ;; Windows Emacs cannot open many sockets at once.
+ (if (or (not (and (fboundp 'gnutls-available-p) (gnutls-available-p)))
+ (eq system-type 'windows-nt))
+ 1
+ 4)
+ "The maximum number of feeds to be fetched in parallel."
+ :group 'elfeed
+ :type 'integer)
+
+(defvar elfeed-connections nil
+ "List of callbacks awaiting responses.")
+
+(defvar elfeed-waiting nil
+ "List of requests awaiting connections.")
+
+(defvar elfeed--connection-counter 0
+ "Provides unique connection identifiers.")
+
+(defun elfeed--check-queue ()
+ "Start waiting connections if connection slots are available."
+ (while (and elfeed-waiting
+ (< (length elfeed-connections) elfeed-max-connections))
+ (let ((request (pop elfeed-waiting)))
+ (cl-destructuring-bind (_ url cb) request
+ (push request elfeed-connections)
+ (condition-case error
+ (url-retrieve url cb nil :silent)
+ (error (with-temp-buffer (funcall cb (list :error error)))))))))
+
+(defun elfeed--wrap-callback (id cb)
+ "Return a function that manages the elfeed queue."
+ (lambda (status)
+ (unwind-protect
+ (funcall cb status)
+ (setf elfeed-connections (cl-delete id elfeed-connections :key #'car))
+ (elfeed--check-queue))))
+
+(defun elfeed-fetch (url callback)
+ "Basically wraps `url-retrieve' but uses the connection limiter."
+ (let* ((id (cl-incf elfeed--connection-counter))
+ (cb (elfeed--wrap-callback id callback)))
+ (push (list id url cb) elfeed-waiting)
+ (elfeed--check-queue)))
+
+(defmacro with-elfeed-fetch (url &rest body)
+ "Asynchronously run BODY in a buffer with the contents from
+URL. This macro is anaphoric, with STATUS referring to the status
+from `url-retrieve'."
+ (declare (indent defun))
+ `(elfeed-fetch ,url (lambda (status) ,@body (kill-buffer))))
+
+(defun elfeed-unjam ()
+ "Manually clear the connection pool when connections fail to timeout.
+This is a short-term workaround for connection handling issues."
+ (interactive)
+ (let ((fails (mapcar #'cl-second elfeed-connections)))
+ (when fails
+ (message "Elfeed aborted feeds: %s" (mapconcat #'identity fails " ")))
+ (setf elfeed-connections nil)
+ (elfeed--check-queue)))
+
+;; Parsing:
+
+(defun elfeed-feed-type (content)
+ "Return the feed type given the parsed content (:atom, :rss) or
+NIL for unknown."
+ (let ((top (xml-query-strip-ns (caar content))))
+ (cadr (assoc top '((feed :atom)
+ (rss :rss)
+ (RDF :rss1.0))))))
+
+(defun elfeed-generate-id (&optional content)
+ "Generate an ID based on CONTENT or from the current time."
+ (concat "urn:sha1:" (sha1 (format "%s" (or content (float-time))))))
+
+(defun elfeed-entries-from-atom (url xml)
+ "Turn parsed Atom content into a list of elfeed-entry structs."
+ (let* ((feed-id url)
+ (feed (elfeed-db-get-feed feed-id))
+ (title (elfeed-cleanup (xml-query '(feed title *) xml)))
+ (autotags (elfeed-feed-autotags url)))
+ (setf (elfeed-feed-url feed) url
+ (elfeed-feed-title feed) title)
+ (cl-loop for entry in (xml-query-all '(feed entry) xml) collect
+ (let* ((title (or (xml-query '(title *) entry) ""))
+ (anylink (xml-query '(link :href) entry))
+ (altlink (xml-query '(link [rel "alternate"] :href) entry))
+ (link (or altlink anylink))
+ (date (or (xml-query '(published *) entry)
+ (xml-query '(updated *) entry)
+ (xml-query '(date *) entry)))
+ (content (or (xml-query '(content *) entry)
+ (xml-query '(summary *) entry)))
+ (id (or (xml-query '(id *) entry) link
+ (elfeed-generate-id content)))
+ (type (or (xml-query '(content :type) entry)
+ (xml-query '(summary :type) entry)
+ ""))
+ (etags (xml-query-all '(link [rel "enclosure"]) entry))
+ (enclosures
+ (cl-loop for enclosure in etags
+ for wrap = (list enclosure)
+ for href = (xml-query '(:href) wrap)
+ for type = (xml-query '(:type) wrap)
+ for length = (xml-query '(:length) wrap)
+ collect (list href type length))))
+ (elfeed-entry--create
+ :title (elfeed-cleanup title)
+ :feed-id feed-id
+ :id (cons feed-id (elfeed-cleanup id))
+ :link (elfeed-cleanup link)
+ :tags (elfeed-normalize-tags autotags elfeed-initial-tags)
+ :date (elfeed-float-time date)
+ :content content
+ :enclosures enclosures
+ :content-type (if (string-match-p "html" type) 'html nil))))))
+
+(defun elfeed-entries-from-rss (url xml)
+ "Turn parsed RSS content into a list of elfeed-entry structs."
+ (let* ((feed-id url)
+ (feed (elfeed-db-get-feed feed-id))
+ (title (elfeed-cleanup (xml-query '(rss channel title *) xml)))
+ (autotags (elfeed-feed-autotags url)))
+ (setf (elfeed-feed-url feed) url
+ (elfeed-feed-title feed) title)
+ (cl-loop for item in (xml-query-all '(rss channel item) xml) collect
+ (let* ((title (or (xml-query '(title *) item) ""))
+ (guid (xml-query '(guid *) item))
+ (link (or (xml-query '(link *) item) guid))
+ (date (or (xml-query '(pubDate *) item)
+ (xml-query '(date *) item)))
+ (description (xml-query '(description *) item))
+ (id (or guid link (elfeed-generate-id description)))
+ (full-id (cons feed-id (elfeed-cleanup id)))
+ (original (elfeed-db-get-entry full-id))
+ (etags (xml-query-all '(enclosure) item))
+ (enclosures
+ (cl-loop for enclosure in etags
+ for wrap = (list enclosure)
+ for url = (xml-query '(:url) wrap)
+ for type = (xml-query '(:type) wrap)
+ for length = (xml-query '(:length) wrap)
+ collect (list url type length))))
+ (elfeed-entry--create
+ :title (elfeed-cleanup title)
+ :id full-id
+ :feed-id feed-id
+ :link (elfeed-cleanup link)
+ :tags (elfeed-normalize-tags autotags elfeed-initial-tags)
+ :date (if (and original (null date))
+ (elfeed-entry-date original)
+ (elfeed-float-time date))
+ :enclosures enclosures
+ :content description
+ :content-type 'html)))))
+
+(defun elfeed-entries-from-rss1.0 (url xml)
+ "Turn parsed RSS 1.0 content into a list of elfeed-entry structs."
+ (let* ((feed-id url)
+ (feed (elfeed-db-get-feed feed-id))
+ (title (elfeed-cleanup (xml-query '(RDF channel title *) xml)))
+ (autotags (elfeed-feed-autotags url)))
+ (setf (elfeed-feed-url feed) url
+ (elfeed-feed-title feed) title)
+ (cl-loop for item in (xml-query-all '(RDF item) xml) collect
+ (let* ((title (or (xml-query '(title *) item) ""))
+ (link (xml-query '(link *) item))
+ (date (or (xml-query '(pubDate *) item)
+ (xml-query '(date *) item)))
+ (description (xml-query '(description *) item))
+ (id (or link (elfeed-generate-id description)))
+ (full-id (cons feed-id (elfeed-cleanup id)))
+ (original (elfeed-db-get-entry full-id)))
+ (elfeed-entry--create
+ :title (elfeed-cleanup title)
+ :id full-id
+ :feed-id feed-id
+ :link (elfeed-cleanup link)
+ :tags (elfeed-normalize-tags autotags elfeed-initial-tags)
+ :date (if (and original (null date))
+ (elfeed-entry-date original)
+ (elfeed-float-time date))
+ :content description
+ :content-type 'html)))))
+
+(defun elfeed-feed-list ()
+ "Return a flat list version of `elfeed-feeds'.
+Only a list of strings will be returned."
+ (cl-loop for feed in elfeed-feeds
+ when (listp feed) collect (car feed)
+ else collect feed))
+
+(defun elfeed-feed-autotags (url-or-feed)
+ "Return tags to automatically apply to all entries from URL-OR-FEED."
+ (let ((url (if (elfeed-feed-p url-or-feed)
+ (or (elfeed-feed-url url-or-feed)
+ (elfeed-feed-id url-or-feed))
+ url-or-feed)))
+ (mapcar #'elfeed-keyword->symbol (cdr (assoc url elfeed-feeds)))))
+
+(defun elfeed-update-feed (url)
+ "Update a specific feed."
+ (interactive (list (completing-read "Feed: " (elfeed-feed-list))))
+ (with-elfeed-fetch url
+ (if (and status (eq (car status) :error))
+ (let ((print-escape-newlines t))
+ (message "Elfeed update failed for %s: %S" url status))
+ (condition-case error
+ (progn
+ (elfeed-move-to-first-empty-line)
+ (set-buffer-multibyte t)
+ (let* ((xml (elfeed-xml-parse-region (point) (point-max)))
+ (entries (cl-case (elfeed-feed-type xml)
+ (:atom (elfeed-entries-from-atom url xml))
+ (:rss (elfeed-entries-from-rss url xml))
+ (:rss1.0 (elfeed-entries-from-rss1.0 url xml))
+ (otherwise (error "Unknown feed type.")))))
+ (elfeed-db-add entries)))
+ (error (message "Elfeed update failed for %s: %s" url error))))))
+
+(defun elfeed-add-feed (url)
+ "Manually add a feed to the database."
+ (interactive (list
+ (let ((clipboard (elfeed-cleanup (elfeed-clipboard-get))))
+ (read-from-minibuffer
+ "URL: " (when (elfeed-looks-like-url-p clipboard)
+ clipboard)))))
+ (cl-pushnew url elfeed-feeds)
+ (when (called-interactively-p 'any)
+ (customize-save-variable 'elfeed-feeds elfeed-feeds))
+ (elfeed-update-feed url))
+
+;;;###autoload
+(defun elfeed-update ()
+ "Update all the feeds in `elfeed-feeds'."
+ (interactive)
+ (message "Elfeed update: %s" (format-time-string "%B %e %Y %H:%M:%S %Z"))
+ (mapc #'elfeed-update-feed (elfeed-feed-list))
+ (elfeed-db-save))
+
+;;;###autoload
+(defun elfeed ()
+ "Enter elfeed."
+ (interactive)
+ (switch-to-buffer (elfeed-search-buffer))
+ (unless (eq major-mode 'elfeed-search-mode)
+ (elfeed-search-mode))
+ (elfeed-search-update))
+
+;; New entry filtering
+
+(cl-defun elfeed-make-tagger
+ (&key feed-title feed-url entry-title entry-link after before
+ add remove callback)
+ "Create a function that adds or removes tags on matching entries.
+
+FEED-TITLE, FEED-URL, ENTRY-TITLE, and ENTRY-LINK are regular
+expressions or a list (not <regex>), which indicates a negative
+match. AFTER and BEFORE are relative times (see
+`elfeed-time-duration'). Entries must match all provided
+expressions. If an entry matches, add tags ADD and remove tags
+REMOVE.
+
+Examples,
+
+ (elfeed-make-tagger :feed-url \"youtube\\\\.com\"
+ :add '(video youtube))
+
+ (elfeed-make-tagger :before \"1 week ago\"
+ :remove 'unread)
+
+ (elfeed-make-tagger :feed-url \"example\\\\.com\"
+ :entry-title '(not \"something interesting\")
+ :add 'junk)
+
+The returned function should be added to `elfeed-new-entry-hook'."
+ (let ((after-time (and after (elfeed-time-duration after)))
+ (before-time (and before (elfeed-time-duration before))))
+ (when (and add (symbolp add)) (setf add (list add)))
+ (when (and remove (symbolp remove)) (setf remove (list remove)))
+ (lambda (entry)
+ (let ((feed (elfeed-entry-feed entry))
+ (date (elfeed-entry-date entry))
+ (case-fold-search t))
+ (cl-flet ((match (r s)
+ (or (null r)
+ (if (listp r)
+ (not (string-match-p (cl-second r) s))
+ (string-match-p r s)))))
+ (when (and
+ (match feed-title (elfeed-feed-title feed))
+ (match feed-url (elfeed-feed-url feed))
+ (match entry-title (elfeed-entry-title entry))
+ (match entry-link (elfeed-entry-link entry))
+ (or (not after-time) (> date (- (float-time) after-time)))
+ (or (not before-time) (< date (- (float-time) before-time))))
+ (when add
+ (apply #'elfeed-tag entry add))
+ (when remove
+ (apply #'elfeed-untag entry remove))
+ (when callback
+ (funcall callback entry))
+ entry))))))
+
+;; OPML
+
+(defun elfeed--parse-opml (xml)
+ "Parse XML (from `xml-parse-region') into `elfeed-feeds' list."
+ (cl-loop for (tag attr . content) in (cl-remove-if-not #'listp xml)
+ count tag into work-around-bug ; bug#15326
+ when (assoc 'xmlUrl attr) collect (cdr it)
+ else append (elfeed--parse-opml content)))
+
+;;;###autoload
+(defun elfeed-load-opml (file)
+ "Load feeds from an OPML file into `elfeed-feeds'.
+When called interactively, the changes to `elfeed-feeds' are
+saved to your customization file."
+ (interactive "fOPML file: ")
+ (let* ((xml (xml-parse-file file))
+ (feeds (elfeed--parse-opml xml))
+ (full (append feeds elfeed-feeds)))
+ (prog1 (setf elfeed-feeds (cl-delete-duplicates full :test #'string=))
+ (when (called-interactively-p 'any)
+ (customize-save-variable 'elfeed-feeds elfeed-feeds)
+ (message "%d feeds loaded from %s" (length feeds) file)))))
+
+;;;###autoload
+(defun elfeed-export-opml (file)
+ "Export the current feed listing to OPML-formatted FILE."
+ (interactive "FOutput OPML file: ")
+ (with-temp-file file
+ (let ((standard-output (current-buffer)))
+ (princ "<?xml version=\"1.0\"?>\n")
+ (xml-print
+ `((opml ((version . "1.0"))
+ (head () (title () "Elfeed Export"))
+ (body ()
+ ,@(cl-loop for url in (elfeed-feed-list)
+ for feed = (elfeed-db-get-feed url)
+ for title = (or (elfeed-feed-title feed) "")
+ collect `(outline ((xmlUrl . ,url)
+ (title . ,title)))))))))))
+
+;;; elfeed.el ends here
--- /dev/null
+;;; xml-query.el --- query engine complimenting the xml package
+
+;; This is free and unencumbered software released into the public domain.
+
+;;; Commentary:
+
+;; This provides a very rudimentary s-expression oriented, jQuery-like
+;; XML query language. It operates on the output of the xml package,
+;; such as `xml-parse-region' and `xml-parse-file'. It was written to
+;; support Elfeed.
+
+;; See the docstring for `xml-query-all'.
+
+;; Examples:
+
+;; This query grabs the top-level paragraph content from XHTML.
+
+;; (xml-query-all '(html body p *) xhtml)
+
+;; This query extracts all the links from an Atom feed.
+
+;; (xml-query-all '(feed entry link [rel "alternate"] :href) xml)
+
+;;; Code:
+
+(require 'cl-lib)
+
+(defun xml-query-strip-ns (tag)
+ "Remove the namespace, if any, from TAG."
+ (when (symbolp tag)
+ (let ((name (symbol-name tag)))
+ (if (cl-find ?\: name)
+ (intern (replace-regexp-in-string "^.+:" "" name))
+ tag))))
+
+(defun xml-query--tag-all (match xml)
+ (cl-loop for (tag attribs . content) in (cl-remove-if-not #'listp xml)
+ when (or (eq tag match) (eq (xml-query-strip-ns tag) match))
+ collect (cons tag (cons attribs content))))
+
+(defun xml-query--attrib-all (attrib value xml)
+ (cl-loop for (tag attribs . content) in (cl-remove-if-not #'listp xml)
+ when (equal (cdr (assoc attrib attribs)) value)
+ collect (cons tag (cons attribs content))))
+
+(defun xml-query--keyword (matcher xml)
+ (cl-loop with match = (intern (substring (symbol-name matcher) 1))
+ for (tag attribs . content) in (cl-remove-if-not #'listp xml)
+ when (cdr (assoc match attribs))
+ collect it))
+
+(defun xml-query--symbol (matcher xml)
+ (xml-query--tag-all matcher xml))
+
+(defun xml-query--vector (matcher xml)
+ (let ((attrib (aref matcher 0))
+ (value (aref matcher 1)))
+ (xml-query--attrib-all attrib value xml)))
+
+(defun xml-query--list (matchers xml)
+ (cl-loop for matcher in matchers
+ append (xml-query-all (if (listp matcher)
+ matcher
+ (list matcher)) xml)))
+
+(defun xml-query--append (xml)
+ (cl-loop for (tag attribs . content) in (cl-remove-if-not #'listp xml)
+ append content))
+
+(defun xml-query-all (query xml)
+ "Given a list of tags, XML, apply QUERY and return a list of
+matching tags.
+
+A query is a list of matchers.
+ - SYMBOL: filters to matching tags
+ - LIST: each element is a full sub-query, whose results are concatenated
+ - VECTOR: filters to tags with matching attribute, [tag attrib value]
+ - KEYWORD: filters to an attribute value (must be last)
+ - * (an asterisk symbol): filters to content strings (must be last)
+
+For example, to find all the 'alternate' link URL in a typical
+Atom feed:
+
+ (xml-query-all '(feed entry link [rel \"alternate\"] :href) xml)"
+ (if (null query)
+ xml
+ (cl-destructuring-bind (matcher . rest) query
+ (cond
+ ((keywordp matcher) (xml-query--keyword matcher xml))
+ ((eq matcher '*)
+ (let ((strings (cl-remove-if-not #'stringp (xml-query--append xml))))
+ (when strings
+ (mapconcat #'identity strings " "))))
+ (:else
+ (let ((matches
+ (cl-etypecase matcher
+ (symbol (xml-query--symbol matcher xml))
+ (vector (xml-query--vector matcher xml))
+ (list (xml-query--list matcher xml)))))
+ (cond
+ ((null rest) matches)
+ ((and (or (symbolp (car rest))
+ (listp (car rest)))
+ (not (keywordp (car rest)))
+ (not (eq '* (car rest))))
+ (xml-query-all (cdr query) (xml-query--append matches)))
+ (:else (xml-query-all rest matches)))))))))
+
+(defun xml-query (query xml)
+ "Like `xml-query-all' but only return the first result."
+ (let ((result (xml-query-all query xml)))
+ (if (stringp result)
+ result
+ (car (xml-query-all query xml)))))
+
+(provide 'xml-query)
+
+;;; xml-query.el ends here
--- /dev/null
+;;; elfeed-org-autoloads.el --- automatically extracted autoloads
+;;
+;;; Code:
+
+\f
+;;;### (autoloads (elfeed-org) "elfeed-org" "elfeed-org.el" (21691
+;;;;;; 57075 0 0))
+;;; Generated autoloads from elfeed-org.el
+
+(autoload 'elfeed-org "elfeed-org" "\
+Hook up rmh-elfeed-org to read the `org-mode' configuration when elfeed starts.
+
+\(fn)" t nil)
+
+;;;***
+\f
+;;;### (autoloads nil nil ("elfeed-org-pkg.el") (21691 57075 182397
+;;;;;; 0))
+
+;;;***
+\f
+(provide 'elfeed-org-autoloads)
+;; Local Variables:
+;; version-control: never
+;; no-byte-compile: t
+;; no-update-autoloads: t
+;; coding: utf-8
+;; End:
+;;; elfeed-org-autoloads.el ends here
--- /dev/null
+(define-package "elfeed-org" "20150108.1406" "Configure elfeed with one or more org-mode files" (quote ((elfeed "1.1.1") (org "7"))))
--- /dev/null
+;;; elfeed-org.el --- Configure elfeed with one or more org-mode files
+
+;; Copyright (C) 2014 Remy Honig
+
+;; Author : Remy Honig <remyhonigNOSPAMgmail.com>
+;; Package-Requires : ((elfeed "1.1.1") (org "7"))
+;; URL : https://github.com/remyhonig/elfeed-org
+;; Version: 20150108.1406
+;; X-Original-Version : 20141003.1
+;; Keywords : news
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;; Maintaining tags for all rss feeds is cumbersome using the regular
+;; flat list where there is no hierarchy and tag names are duplicated
+;; a lot. Org-mode makes the book keeping of tags and feeds much
+;; easier. Tags get inherited from parent headlines. Multiple files
+;; can be specified to separate your private from your work feeds for
+;; example. You may also use tagging rules to tag feeds by entry-title
+;; keywords. See https://github.com/remyhonig/elfeed-org for usage.
+
+;;; Code:
+
+(require 'elfeed)
+(require 'cl)
+(require 'org)
+
+
+(defgroup elfeed-org nil
+ "Configure the Elfeed RSS reader with an Orgmode file"
+ :group 'comm)
+
+
+(defcustom rmh-elfeed-org-tree-id "elfeed"
+ "The ID property of the tree containing the RSS feeds."
+ :group 'elfeed-org
+ :type 'string)
+
+
+(defcustom rmh-elfeed-org-files (list "~/.emacs.d/elfeed.org")
+ "The files where we look to find the tree with the `rmh-elfeed-org-tree-id'."
+ :group 'elfeed-org
+ :type '(repeat (file :tag "org-mode file")))
+
+
+(defun rmh-elfeed-org-read-tree (tree-id match)
+ "Convert org tree with TREE-ID into a feed configuration structure for Elfeed.
+Filter out headlines that contain MATCH."
+ (let* ((m (org-id-find tree-id 'marker))
+ (buf (marker-buffer m)))
+ (save-excursion
+ (with-current-buffer buf
+ (goto-char m)
+ (move-marker m nil)
+ (remove-if-not
+ (lambda (x)
+ (and (string-match match (car x)) x))
+ (rmh-elfeed-org-tags-inherited
+ (lambda ()
+ (org-map-entries
+ '(let ((url (substring-no-properties (org-get-heading t)))
+ (tags (mapcar 'intern (org-get-tags-at))))
+ (append (list url) tags))
+ nil
+ rmh-elfeed-org-files))))))))
+
+
+(defun rmh-elfeed-org-tags-inherited (func)
+ "Call FUNC while ensuring tags are inherited."
+ (let ((original org-use-tag-inheritance))
+ (setq org-use-tag-inheritance 't)
+ (let ((feeds (funcall func)))
+ (setq org-use-tag-inheritance original) feeds)))
+
+
+(defun rmh-elfeed-org-add-new-entry-hooks (keywords)
+ "Add new entry hooks for tagging KEYWORDS."
+ (mapc
+ (lambda (x)
+ (let ((term (car (cdr (split-string (car x) ": "))))
+ (tags (cdr x)))
+ (add-hook 'elfeed-new-entry-hook (elfeed-make-tagger :entry-title term :add (cdr x)))))
+ keywords))
+
+
+(defun rmh-elfeed-org-check-configuration-file (file)
+ "Make sure FILE exists. If not, ask user what to do."
+ (when (not (file-exists-p file))
+ (error "Elfeed-org cannot open %s. Make sure it exists customize the variable \'rmh-elfeed-org-files\'"
+ (abbreviate-file-name file))))
+
+
+(defun rmh-elfeed-org-configure ()
+ "Clear and reload the elfeed feeds- and tagging configuration."
+
+ ;; To regenerate the elfeed config after the org mode tree has
+ ;; changed we want this function to be interactive
+ (interactive)
+
+ ;; Make sure all files really exists
+ (mapc (lambda (file) (rmh-elfeed-org-check-configuration-file file)) rmh-elfeed-org-files)
+
+ ;; Clear hooks for auto tagging
+ (setq elfeed-new-entry-hook (list))
+
+ ;; Clear registered feeds
+ (setq elfeed-feeds (list))
+
+ ;; Add feeds and auto tagging rules to elfeed configuration
+ (mapc (lambda (file) (with-current-buffer (find-file-noselect file)
+
+ ;; Extract tagging rules and feeds from the found file
+ (let* ((tree-id rmh-elfeed-org-tree-id)
+ (tagging-rules (rmh-elfeed-org-read-tree tree-id "entry-title"))
+ (feeds (rmh-elfeed-org-read-tree tree-id "http")))
+
+ ;; Add feeds to elfeed configuration
+ (setq elfeed-feeds (append elfeed-feeds feeds))
+
+ ;; Add auto tagging rules to elfeed configuration
+ (rmh-elfeed-org-add-new-entry-hooks tagging-rules))))
+
+ ;; The files we are operating on
+ rmh-elfeed-org-files)
+
+ ;; Tell user what we did
+ (message "elfeed-org loaded %i feeds, %i rules"
+ (length elfeed-feeds)
+ (length elfeed-new-entry-hook)))
+
+
+;;;###autoload
+(defun elfeed-org ()
+ "Hook up rmh-elfeed-org to read the `org-mode' configuration when elfeed starts."
+ (interactive)
+ (message "elfeed-org is set up to handle elfeed configuration.")
+ ;; Use an advice to load the configuration.
+ (defadvice elfeed (before configure-elfeed activate)
+ "Load all feed settings before elfeed is started."
+ (rmh-elfeed-org-configure)))
+
+
+(provide 'elfeed-org)
+;;; elfeed-org.el ends here
--- /dev/null
+Maintaining tags for all rss feeds is cumbersome using the regular
+flat list where there is no hierarchy and tag names are duplicated
+a lot. Org-mode makes the book keeping of tags and feeds much
+easier. Tags get inherited from parent headlines. Multiple files
+can be specified to separate your private from your work feeds for
+example. You may also use tagging rules to tag feeds by entry-title
+keywords. See https://github.com/remyhonig/elfeed-org for usage.
--- /dev/null
+Elfreed is a web feed client for Emacs, inspired by notmuch. See
+the README for full documentation.
+
+Elfreed requires Emacs 24 (lexical closures).