-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathelfeed-score.el
289 lines (240 loc) · 11.4 KB
/
elfeed-score.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
;;; elfeed-score.el --- Gnus-style scoring for Elfeed -*- lexical-binding: t; -*-
;; Copyright (C) 2019-2024 Michael Herstine <sp1ff@pobox.com>
;; Author: Michael Herstine <sp1ff@pobox.com>
;; Version: 1.2.8
;; Package-Requires: ((emacs "26.1") (elfeed "3.3.0"))
;; Keywords: news
;; URL: https://github.com/sp1ff/elfeed-score
;; 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 <https://www.gnu.org/licenses/>.
;;; Commentary:
;; `elfeed-score' is an add-on for `elfeed', an RSS reader for
;; Emacs. It brings Gnus-style scoring to your RSS feeds. Elfeed, by
;; default, displays feed entries by date. This package allows you to
;; setup rules for assigning numeric scores to entries, and sorting
;; entries with higher scores ahead of those with lower, regardless of
;; date. The idea is to prioritize content important to you.
;; After installing this file, enable scoring by invoking
;; `elfeed-score-enable'. This will setup the Elfeed new entry hook,
;; the Elfeed sort function, and load the score file (if it exists).
;; Turn off scoring by invoking `elfeed-score-unload'.
;;; Code:
(require 'elfeed-score-log)
(require 'elfeed-score-rules)
(require 'elfeed-score-rule-stats)
(require 'elfeed-score-serde)
(require 'elfeed-score-scoring)
(require 'elfeed-score-maint)
(defconst elfeed-score-version "1.2.8")
(defgroup elfeed-score nil
"Gnus-style scoring for Elfeed entries."
:group 'comm)
(defcustom elfeed-score-score-format '("%d " 6 :right)
"Format for scores when displayed in the Elfeed search buffer.
This is a three-tuple: the `format' format string, target width,
and alignment. This should be (string integer keyword)
for (format width alignment). Possible alignments are :left and
:right."
:group 'elfeed-score
:type '(list string integer (choice (const :left) (const :right))))
(defun elfeed-score-sort (a b)
"Return non-nil if A should sort before B.
`elfeed-score' will substitute this for the Elfeed scoring function."
(let ((a-score (elfeed-score-scoring-get-score-from-entry a))
(b-score (elfeed-score-scoring-get-score-from-entry b)))
(if (> a-score b-score)
t
(let ((a-date (elfeed-entry-date a))
(b-date (elfeed-entry-date b)))
(and (eq a-score b-score) (> a-date b-date))))))
(defun elfeed-score-set-score (score &optional ignore-region)
"Set the score of one or more Elfeed entries to SCORE.
Their scores will be set to `elfeed-score-scoring-default-score'
by default.
If IGNORE-REGION is nil (as it will be when called
interactively), then all entries in the current region will have
their scores re-set. If the region is not active, then only the
entry under point will be affected. If IGNORE-REGION is t, then
only the entry under point will be affected, regardless of the
region's state."
(interactive "P")
(let ((score
(if score
(prefix-numeric-value score)
elfeed-score-scoring-default-score))
(entries (elfeed-search-selected ignore-region)))
(dolist (entry entries)
(elfeed-score-log 'info "entry %s ('%s') was directly set to %d"
(elfeed-entry-id entry ) (elfeed-entry-title entry) score)
;; Set the score, marking it as "sticky"...
(elfeed-score-scoring-set-score-on-entry entry score t)
;; & update the entry.
(elfeed-search-update-entry entry))))
(defun elfeed-score-get-score ()
"Return the score of the entry under point.
If called interactively, print a message."
(interactive)
(let* ((entry (elfeed-search-selected t))
(score (elfeed-score-scoring-get-score-from-entry entry)))
(if (called-interactively-p 'any)
(message "%s has a score of %d." (elfeed-entry-title entry) score))
score))
(defun elfeed-score-format-score (score)
"Format SCORE for printing in `elfeed-search-mode'.
The customization `elfeed-score-score-format' sets the
formatting. This implementation is based on that of
`elfeed-search-format-date'."
(cl-destructuring-bind (format target alignment) elfeed-score-score-format
(let* ((string (format format score))
(width (string-width string)))
(cond
((> width target)
(if (eq alignment :left)
(substring string 0 target)
(substring string (- width target) width)))
((< width target)
(let ((pad (make-string (- target width) ?\s)))
(if (eq alignment :left)
(concat string pad)
(concat pad string))))
(string)))))
(defun elfeed-score-explain (&optional ignore-region)
"Explain why some entries were scored the way they were.
Explain the scores for all the selected entries, unless
IGNORE-REGION is non-nil, in which case only the entry under
point will be explained. If the region is not active, only the
entry under point will be explained."
(interactive)
(let ((entries (elfeed-search-selected ignore-region)))
(with-help-window elfeed-score-scoring-explanation-buffer-name
(with-current-buffer elfeed-score-scoring-explanation-buffer-name
(dolist (entry entries)
(elfeed-score-scoring-explain-entry entry (current-buffer)))))
(elfeed-search-update t)))
(defun elfeed-score-load-score-file (score-file)
"Load SCORE-FILE into the current scoring rules."
(interactive
(list
(read-file-name "score file: " nil elfeed-score-serde-score-file t
elfeed-score-serde-score-file)))
(elfeed-score-serde-load-score-file score-file))
(defun elfeed-score-score (&optional ignore-region)
"Score some entries.
Score all selected entries, unless IGNORE-REGION is non-nil, in
which case only the entry under point will be scored. If the
region is not active, only the entry under point will be scored."
(interactive "P")
;; Inhibit automatic flushing of rule stats to file...
(let ((entries (elfeed-search-selected ignore-region))
(elfeed-score-rule-stats-dirty-threshold nil))
(dolist (entry entries)
(elfeed-score-scoring-score-entry entry))
(elfeed-search-update t))
;; *Now* flush stats.
(if elfeed-score-rule-stats-file
(elfeed-score-rule-stats-write elfeed-score-rule-stats-file)))
(defun elfeed-score-score-search ()
"Score the current set of search results."
(interactive)
(elfeed-score-scoring-score-search))
(defvar elfeed-score-map
(let ((map (make-sparse-keymap)))
(prog1 map
(suppress-keymap map)
(define-key map "e" #'elfeed-score-set-score)
(define-key map "g" #'elfeed-score-get-score)
(define-key map "l" #'elfeed-score-load-score-file)
(define-key map "s" #'elfeed-score-score)
(define-key map "v" #'elfeed-score-score-search)
(define-key map "w" #'elfeed-score-serde-write-score-file)
(define-key map "x" #'elfeed-score-explain)
(define-key map "aT" #'elfeed-score-maint-add-title-rule)
(define-key map "ac" #'elfeed-score-maint-add-content-rule)
(define-key map "af" #'elfeed-score-maint-add-feed-rule)
(define-key map "aa" #'elfeed-score-maint-add-authors-rule)
(define-key map "at" #'elfeed-score-maint-add-tag-rule)
(define-key map "al" #'elfeed-score-maint-add-link-rule)
(define-key map "ao" #'elfeed-score-maint-add-title-or-content-rule)))
"Keymap for `elfeed-score' commands.")
(defvar elfeed-score--old-sort-function nil
"Original value of `elfeed-search-sort-function'.")
(defvar elfeed-score--old-print-entry-function nil
"Original value of `elfed-search-print-entry-function'.")
(defun elfeed-score-print-entry (entry)
"Print ENTRY to the Elfeed search buffer.
This implementation is derived from `elfeed-search-print-entry--default'."
(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 (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))
(score
(elfeed-score-format-score
(elfeed-score-scoring-get-score-from-entry entry))))
(insert score)
(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 ")"))))
;;;###autoload
(defun elfeed-score-enable (&optional arg depth)
"Enable `elfeed-score'. With prefix ARG do not install a custom sort function.
Add the score function at DEPTH (default 0)."
(interactive "P")
;; Begin scoring on every new entry...
(add-hook 'elfeed-new-entry-hook #'elfeed-score-scoring-score-entry (or depth 0))
;; sort based on score...
(unless arg
(setq elfeed-score--old-sort-function elfeed-search-sort-function
elfeed-search-sort-function #'elfeed-score-sort
elfeed-score--old-print-entry-function elfeed-search-print-entry-function))
;; load the default score file (if it's defined & exists)...
(if (and elfeed-score-serde-score-file
(file-exists-p elfeed-score-serde-score-file))
(elfeed-score-load-score-file elfeed-score-serde-score-file))
;; load the stats file (again if it's defined & exists)...
(if (and elfeed-score-rule-stats-file
(file-exists-p elfeed-score-rule-stats-file))
(elfeed-score-rule-stats-read elfeed-score-rule-stats-file))
(elfeed-score-serde-cleanup-stats)
;; & finally, arrange to write stats after every `elfeed' update.
(add-hook 'elfeed-update-hooks #'elfeed-score-rule-stats-update-hook))
(defun elfeed-score-unload ()
"Unload `elfeed-score'."
(interactive)
;; No need to write the score file at this point; what's on-disk is
;; the SoT, anyway.
(elfeed-score-serde-cleanup-stats)
(if elfeed-score-rule-stats-file
(elfeed-score-rule-stats-write elfeed-score-rule-stats-file))
(if elfeed-score--old-sort-function
(setq elfeed-search-sort-function elfeed-score--old-sort-function))
(if elfeed-score--old-print-entry-function
(setq elfeed-search-print-entry-function elfeed-score--old-print-entry-function))
(remove-hook 'elfeed-new-entry-hook #'elfeed-score-scoring-score-entry)
(remove-hook 'elfeed-update-hooks #'elfeed-score-rule-stats-update-hook))
(provide 'elfeed-score)
;;; elfeed-score.el ends here