Skip to content

Instantly share code, notes, and snippets.

@mmarshall540
Created October 19, 2025 15:03
Show Gist options
  • Select an option

  • Save mmarshall540/8fcefea266e041f9a54a1eb704cd9b3b to your computer and use it in GitHub Desktop.

Select an option

Save mmarshall540/8fcefea266e041f9a54a1eb704cd9b3b to your computer and use it in GitHub Desktop.
Add "MODIFIED" (and "HASH") properties to Org-mode entries on save
;; Org-mode: Custom property: MODIFIED
;; Adapted from: https://emacs.stackexchange.com/a/39376/31079
(defun yant/getentryhash ()
"Get the hash sum of the text in current entry.
Except :HASH: and :MODIFIED: property texts."
(save-excursion
(let* ((beg (progn (org-back-to-heading :invisible-ok) (point)))
(end (progn
(forward-char)
(if (not (re-search-forward "^\*+ " (point-max) t))
(point-max)
(match-beginning 0))))
(full-str (buffer-substring-no-properties beg end))
(str-nohash (if (string-match "^ *:HASH:.+\n" full-str)
(replace-match "" nil nil full-str)
full-str))
(str-nohash-nomod (if (string-match
"^ *:MODIFIED:.+\n" str-nohash)
(replace-match "" nil nil str-nohash)
str-nohash))
(str-nohash-nomod-nopropbeg (if (string-match
"^ *:PROPERTIES:\n"
str-nohash-nomod)
(replace-match
"" nil nil
str-nohash-nomod)
str-nohash-nomod))
(str-nohash-nomod-nopropbeg-end
(if (string-match "^ *:END:\n" str-nohash-nomod-nopropbeg)
(replace-match "" nil nil str-nohash-nomod-nopropbeg)
str-nohash-nomod-nopropbeg))
;; Do not change hash just because whitespace was trimmed
;; from line-endings or extra newlines removed or added to
;; the end of an entry.
(final-str
(concat
(string-trim-right
(replace-regexp-in-string
"[ \t]+[\n\r]" "\n"
str-nohash-nomod-nopropbeg-end))
"\n")))
(secure-hash 'md5 final-str))))
(defun yant/update-modification-time ()
"Set the :MODIFIED: property of the current entry.
Set to NOW and update :HASH: property."
(org-set-property "HASH" (format "%s" (yant/getentryhash)))
(org-set-property "MODIFIED"
(format-time-string
(org-time-stamp-format :with-hm :inactive)))
(setq my/updated-org-entries-count
(1+ my/updated-org-entries-count)))
(defun yant/skip-nonmodified ()
"Skip org entries, which were not modified."
(let ((next-headline (save-excursion (or (outline-next-heading)
(point-max)))))
(if (string= (org-entry-get (point) "HASH" nil)
(format "%s" (yant/getentryhash)))
next-headline
nil)))
(defvar my/updated-org-entries-count)
(defun my/org-update-modified-prop ()
"Update the MODIFIED property if major-mode is `org-mode'."
(setq-local my/updated-org-entries-count 0)
(when (and (eq major-mode 'org-mode)
(equal default-directory (expand-file-name "~/org/")))
(org-map-entries #'yant/update-modification-time
nil
'file
#'yant/skip-nonmodified)
(when (> my/updated-org-entries-count 0)
(message "Saved %s and updated %s entries!"
buffer-file-name
my/updated-org-entries-count))))
(add-hook 'before-save-hook 'my/org-update-modified-prop)
@mmarshall540
Copy link
Author

mmarshall540 commented Nov 17, 2025

I've already stopped using this after realizing that the TIMESTAMP_IA property can be used to match against the first inactive timestamp in an entry. The matched timestamp must have no keyword (such as "CLOSED:" or "CLOCK:") and must be outside of the :PROPERTIES: drawer. That means that timestamps added to an entry by org-add-note (C-c C-z) are perfect as a proxy for modification time.

The code in this gist works to automatically update a :MODIFIED: property every single time it is modified no matter how slight the change. But that's kind of like pounding a roofing nail with a sledgehammer. It's a bit much.

So I think it makes more sense to rely on intentionally adding a note (even if it has no text) in order to generate a list of recently-modified entries. Correcting a typo shouldn't cause an entry to jump to the top of the modified list.

The caveat to this new approach is that I'll have to get in the habit of adding updates using org-add-note and avoid making major changes to an entry without also adding a note.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment