Commit d56b55a79d46

Vincent Demeester <vincent@sbr.pm>
2025-01-24 00:11:40
tools/emacs: testing org-review
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
1 parent c2f0205
Changed files (2)
tools
tools/emacs/config/config-org.el
@@ -222,7 +222,15 @@
            ((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)))))))
+	    (org-agenda-sorting-strategy '(user-defined-down)))))))
+
+(use-package org-review
+  :config
+  (setopt org-review-delay "+1w")
+  (add-hook 'org-agenda-mode-hook
+          (lambda ()
+            (local-set-key (kbd "C-c C-r")
+                           'org-review-insert-last-review))))
 
 ;; Make sure we load org-protocol
 (use-package org-protocol
tools/emacs/lisp/org-review.el
@@ -0,0 +1,263 @@
+;;; org-review.el --- Schedule reviews for Org entries
+;;
+;; Copyright (C) 2024 Alan Schmitt
+;;
+;; Author: Alan Schmitt <alan.schmitt@polytechnique.org>
+;; 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