-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathorg-review.el
263 lines (233 loc) · 9.34 KB
/
org-review.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
;;; org-review.el --- Schedule reviews for Org entries
;;
;; Copyright (C) 2024 Alan Schmitt
;;
;; Author: Alan Schmitt <[email protected]>
;; URL: https://github.com/brabalan/org-review
;; Version: 0.3
;; Keywords: calendar
;; This file is not part of GNU Emacs.
;; 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, 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
;;
;;; Commentary:
;;
;; This allows to schedule reviews of org entries.
;;
;; Entries will be scheduled for review if their NEXT_REVIEW or their
;; LAST_REVIEW property is set. The next review date is the
;; NEXT_REVIEW date, if it is present, otherwise it is computed from
;; the LAST_REVIEW property and the REVIEW_DELAY period, such as
;; "+1m". If REVIEW_DELAY is absent, a default period is used. Note
;; that the LAST_REVIEW property is not considered as inherited, but
;; REVIEW_DELAY is, allowing to set it for whole subtrees.
;;
;; Checking of review dates is done through an agenda view, using the
;; `org-review-agenda-skip' skipping function. This function is based
;; on `org-review-toreview-p', that returns `nil' if no review is
;; necessary (no review planned or it happened recently), otherwise it
;; returns the date the review was first necessary (NEXT_REVIEW, or
;; LAST_REVIEW + REVIEW_DELAY, if it is in the past).
;;
;; To mark an entry as reviewed, use the function
;; `org-review-insert-last-review' to set the LAST_REVIEW date to the
;; current date. If `org-review-sets-next-date' is set (which is the
;; default), this function also computes the date of the next review
;; and inserts it as NEXT_REVIEW.
;;
;; Example use.
;;
;; 1 - To display the things to review in the agenda.
;;
;; (setq org-agenda-custom-commands (quote ( ...
;; ("R" "Review projects" tags-todo "-CANCELLED/"
;; ((org-agenda-overriding-header "Reviews Scheduled")
;; (org-agenda-skip-function 'org-review-agenda-skip)
;; (org-agenda-cmp-user-defined 'org-review-compare)
;; (org-agenda-sorting-strategy '(user-defined-down)))) ... )))
;;
;; 2 - To set a key binding to review from the agenda
;;
;; (add-hook 'org-agenda-mode-hook (lambda () (local-set-key (kbd "C-c
;; C-r") 'org-review-insert-last-review)))
;;; Changes
;;
;; 2022-04-11: systematically insert name of week day in date
;; 2016-08-18: better detection of org-agenda buffers
;; 2014-05-08: added the ability to specify next review dates
;; TODO
;; - be able to specify a function to run when marking an item reviewed
;;; Code:
(require 'org)
(require 'org-agenda)
;;; User variables:
(defgroup org-review nil
"Org review scheduling."
:tag "Org Review Schedule"
:group 'org)
(defcustom org-review-last-timestamp-format 'naked
"Timestamp format for last review properties."
:type '(radio (const naked)
(const inactive)
(const active))
:group 'org-review)
(defcustom org-review-next-timestamp-format 'naked
"Timestamp format for last review properties."
:type '(radio (const naked)
(const inactive)
(const active))
:group 'org-review)
(defcustom org-review-last-property-name "LAST_REVIEW"
"The name of the property for the date of the last review."
:type 'string
:group 'org-review)
(defcustom org-review-delay-property-name "REVIEW_DELAY"
"The name of the property for setting the delay before the next review."
:type 'string
:group 'org-review)
(defcustom org-review-next-property-name "NEXT_REVIEW"
"The name of the property for setting the date of the next review."
:type 'string
:group 'org-review)
(defcustom org-review-delay "+1m"
"Time span between the date of last review and the next one.
The default value for this variable (\"+1m\") means that entries
will be marked for review one month after their last review.
If the review delay cannot be retrieved from the entry or the
subtree above, this delay is used."
:type 'string
:group 'org-review)
(defcustom org-review-sets-next-date t
"Indicates whether marking a project as reviewed automatically
sets the next NEXT_REVIEW according to the current date and
REVIEW_DELAY."
:type 'boolean
:group 'org-review)
;;; Functions:
(defun org-review-last-planned (last delay)
"Computes the next planned review, given the LAST review
date (in string format) and the review DELAY (in string
format)."
(let ((lt (org-read-date nil t last))
(ct (current-time)))
(time-add lt (time-subtract (org-read-date nil t delay) ct))))
;;;###autoload
(defun org-review-last-review-prop (&optional pos)
"Return the value of the last review property of the headline
at position POS, or the current headline if POS is not given."
(org-entry-get (or pos (point)) org-review-last-property-name))
;;;###autoload
(defun org-review-next-review-prop (&optional pos)
"Return the value of the review date property of the headline
at position POS, or the current headline if POS is not given."
(org-entry-get (or pos (point)) org-review-next-property-name))
(defun org-review-review-delay-prop (&optional pos)
"Return the value of the review delay property of the headline
at position POS, or the current headline if POS is not given,
considering inherited properties."
(org-entry-get (or pos (point)) org-review-delay-property-name t))
(defun org-review-toreview-p (&optional pos)
"Check if the entry at point should be marked for review.
Return nil if the entry does not need to be reviewed. Otherwise
return the date when the entry was first scheduled to be
reviewed.
If there is a next review date, consider it. Otherwise, if there
is a last review date, use it to compute the date of the next
review (adding the value of the review delay property, or
`org-review-delay' if there is no review delay property). If
there is no next review date and no last review date, return
nil."
(let* ((lp (org-review-last-review-prop pos))
(np (org-review-next-review-prop pos))
(nextreview
(cond
(np (org-read-date nil t np))
(lp (org-review-last-planned
lp
(or (org-review-review-delay-prop pos)
org-review-delay)))
(t nil))))
(and nextreview
(time-less-p nextreview (current-time))
nextreview)))
(defun org-review-insert-date (propname fmt date)
"Insert the DATE under property PROPNAME, in the format
specified by FMT."
(org-entry-put
(if (equal major-mode 'org-agenda-mode)
(or (org-get-at-bol 'org-marker)
(org-agenda-error))
(point))
propname
(cond
((eq fmt 'inactive)
(concat "[" date "]"))
((eq fmt 'active)
(concat "<" date ">"))
(t date))))
;;;###autoload
(defun org-review-insert-last-review (&optional prompt)
"Insert the current date as last review. If prefix argument:
prompt the user for the date. If `org-review-sets-next-date' is
set to t, also insert a next review date."
(interactive "P")
(let ((ts (if prompt
(format-time-string (car org-time-stamp-formats) (org-read-date nil t))
(format-time-string (car org-time-stamp-formats)))))
(org-review-insert-date org-review-last-property-name
org-review-last-timestamp-format
ts)
(when org-review-sets-next-date
(org-review-insert-date
org-review-next-property-name
org-review-next-timestamp-format
(format-time-string
(car org-time-stamp-formats)
(org-review-last-planned
ts
(or (org-review-review-delay-prop
(if (equal major-mode 'org-agenda-mode)
(or (org-get-at-bol 'org-marker)
(org-agenda-error))
(point)))
org-review-delay)))))))
;;;###autoload
(defun org-review-insert-next-review ()
"Prompt the user for the date of the next review, and insert
it as a property of the headline."
(interactive)
(let ((ts (format-time-string (car org-time-stamp-formats) (org-read-date nil t))))
(org-review-insert-date org-review-next-property-name
org-review-next-timestamp-format
ts)))
;;;###autoload
(defun org-review-agenda-skip ()
"To be used as an argument of `org-agenda-skip-function' to
skip entries that are not scheduled to be reviewed. This function
does not move the point; it returns nil if the entry is to be
kept, and the position to continue the search otherwise."
(and (not (org-review-toreview-p))
(org-with-wide-buffer (or (outline-next-heading) (point-max)))))
(defun org-review-compare (a b)
"Compares the date of scheduled review for the two agenda
entries, to be used with `org-agenda-cmp-user-defined'. Returns
+1 if A has been scheduled for longer and -1 otherwise."
(let* ((ma (or (get-text-property 0 'org-marker a)
(get-text-property 0 'org-hd-marker a)))
(mb (or (get-text-property 0 'org-marker b)
(get-text-property 0 'org-hd-marker b)))
(ra (org-review-toreview-p ma))
(rb (org-review-toreview-p mb)))
(if (time-less-p ra rb) 1 -1)))
(provide 'org-review)
;;; org-review.el ends here