5

In org-mode documents, I very often add drawer properties to hyperlinks. To illustrate, here is an example with various data attached to a GitHub repository link:

* https://github.com/caddyserver/caddy
:PROPERTIES:
:description: Fast, multi-platform web server with automatic HTTPS
:stars:    35252
:open-issues: 96
:language: Go
:created-at: 2015-01-13T19:45:03Z
:updated-at: 2021-11-21T10:37:08Z
:last-commit-at: 2021-11-16T20:08:22Z
:fetched-at: 2021-11-21T11:43:57Z
:END:

I would like to keep the data stored on disk this way, but have a way to (auto)toggle the display of the various dates in a relative fashion. To disambiguate what I mean by "relative", here is a more concrete example:

* https://github.com/caddyserver/caddy
:PROPERTIES:
:description: Fast, multi-platform web server with automatic HTTPS
:stars:    35252
:open-issues: 96
:language: Go
:created-at: 6 years and 10 months ago
:updated-at: 6 hours ago
:last-commit-at: 5 days ago
:fetched-at: 7 hours ago
:END:

What could be the approach to tackle this feature?

Does org-mode provide a hook when a property drawer is opened?

If so, should the invoked function use some kind of overlays to modify only the content of the date values in the drawer?

Delapouite
  • 215
  • 1
  • 6

1 Answers1

7

The command org+-dateprop defined in the following Elisp code does what you want.
It puts overlays with a display property on the absolute dates that shows the relative dates instead.
This has the advantage that the buffer contents is not modified by the function.
But, it may be a disadvantage that dates are not exported as shown but as absolute dates.

You can use org+-dateprop to update the time stamps after you have modified the buffer.

An alternative approach would be to define a new minor-mode that places the display text property by font-lock.
If the minor mode is active the dates would be shown as relative dates as soon as they match the regexp for a proper date.
But, this can also be a bit disturbing. So I stuck to the command org+-dateprop.

Addressing your questions:

  1. Question: What could be the approach to tackle this feature?
    Answer: Orgmode provides custom properties that can be hidden by
    org-toggle-custom-properties-visibility. One can use that
    function as an example. Instead of the invisible text property
    you can use the display property to show the absolute time
    entries as relative time entries without modifying the actual text.
  2. Question: Does org-mode provide a hook when a property drawer is opened?
    Answer: This question is not so much related to the actual problem.
    But, no, there is no predefined hook that is called when the visibility of a drawer changes.
    The visibility of the drawers is changed through org-flag-drawer.
    One could add a hook variable and an advice to that function that calls run-hooks or one of its relatives.
  3. Question: If so, should the invoked function use some kind of overlays to modify only the content of the date values in the drawer?
    Answer: You are on the right track with the overlays. But, you do not need to change the date values. You can just set the display property of the overlays to define what you want to show instead.
(require 'cl-lib)

(defcustom org+-dateprop-reltime-number-of-items 3
  "Number of time items shown for relative time."
  :type 'number
  :group 'org)

(defun org+-next-property-drawer (&optional limit)
  "Search for the next property drawer.
When a property drawer is found position point behind :PROPERTIES:
and return the property-drawer as org-element.
Otherwise position point at the end of the buffer and return nil."
  (let (found drawer)
    (while (and (setq found (re-search-forward org-drawer-regexp limit 1)
              found (match-string-no-properties 1))
        (or (and (setq drawer (org-element-context))
             (null (eq (org-element-type drawer) 'property-drawer)))
            (string-match found "END"))))
    (and found drawer)))

(defun org+-time-since-string (date)
  "Return a string representing the time since DATE."
  (let* ((time-diff (nreverse (seq-subseq (decode-time (time-subtract (current-time) (encode-time date))) 0 6)))
     (cnt 0))
    (setf (car time-diff) (- (car time-diff) 1970))
    (mapconcat
     #'identity
     (cl-loop
      for cnt from 1 upto org+-dateprop-reltime-number-of-items
      for val in time-diff
      for time-str in '("year" "month" "day" "hour" "minute" "second")
      unless (= val 0)
      collect (format "%d %s%s" val time-str (if (> val 1) "s" ""))
      )
     " ")))

(defvar-local org+-dateprop--overlays nil
  "List of overlays used for custom properties.")

(defun org+-dateprop-properties-re (properties)
  "Return regular expression corresponding to `org+-dateprop-properties'."
  (org-re-property (regexp-opt properties) t))

(defvar org+-dateprop--properties-re (org+-dateprop-properties-re org+-dateprop-properties)
  "Regular expression matching the properties listed in `org+-dateprop-properties'.
You should not set this regexp diretly but through customization of `org+-dateprop-properties'.")

(defun org+-dateprop (&optional absolute)
  "Toggle display of ABSOLUTE or relative time of
properties in `org-dateprop-properties'."
  (interactive "P")
  (if org+-dateprop--overlays
      (progn (mapc #'delete-overlay org+-dateprop--overlays)
         (setq org+-dateprop--overlays nil))
    (unless absolute
      (org-with-wide-buffer
       (goto-char (point-min))
       (let (drawer-el)
     (while (setq drawer-el (org+-next-property-drawer))
       (let ((drawer-end (org-element-property :contents-end drawer-el)))
         (while (re-search-forward org+-dateprop--properties-re drawer-end t)
           ;; See `org-property-re' for the regexp-groups.
           ;; Group 3 is PROPVAL without surrounding whitespace.
           (let* ((val-begin (match-beginning 3))
              (val-end (match-end 3))
              (time (org-parse-time-string (replace-regexp-in-string "[[:alpha:]]" " " (match-string 3))))
              (time-diff-string (format "%s ago" (org+-time-since-string time)))
              (o (make-overlay val-begin val-end)))
         (overlay-put o 'display time-diff-string)
         (overlay-put o 'org+-dateprop t)
         (push o org+-dateprop--overlays))
           ))))))))

(define-widget 'org+-dateprop-properties-widget
  'repeat
  "Like widget '(repeat string) but also updates `org+-dateprop-properties'."
  :value-to-external
  (lambda (widget value)
    (setq org+-dateprop--properties-re (org+-dateprop-properties-re value))
    value)
  :args '(string))

(defcustom org+-dateprop-properties '("created-at" "updated-at" "last-commit-at" "fetched-at")
  "Names of properties with dates."
  :type 'org+-dateprop-properties-widget
  :group 'org)

You can put that code in your init file.

Tobias
  • 32,569
  • 1
  • 34
  • 75
  • This is beautiful. Can it be modified to work for all Org dates, even if not in property drawers? – mankoff Nov 26 '21 at 00:19
  • Also, seems like a nice enough feature (especially if it has the option to change `[date]` or `` to `[date] (relative_date)` (instead of over-writing `[date]`) that this could be packaged and turned into a minor mode. – mankoff Nov 26 '21 at 00:19