Created
October 19, 2025 15:03
-
-
Save mmarshall540/8fcefea266e041f9a54a1eb704cd9b3b to your computer and use it in GitHub Desktop.
Add "MODIFIED" (and "HASH") properties to Org-mode entries on save
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ;; 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) |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I've already stopped using this after realizing that the
TIMESTAMP_IAproperty 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 byorg-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-noteand avoid making major changes to an entry without also adding a note.