Update a field (#+LAST_MODIFIED: ) at save

I want to thank you all for your help so far. I am slowly working the kinks out of my workflow. I know this isn’t org-roam strictly, but I can’t find an answer online and it pertains only to my roam buffers.

In my capture and capture-ref templates I have a field #+LAST_MODIFIED: that inserts the time. I’d like it to update every time the file is saved after that to represent its last save.

I got this from someone else’s org-roam setup, but I don’t remember where. How would I setup emacs to update that line at save? I think I am on the right path with time-stamp but I’m not sure how to call it. Here is what I have so far:

(setq 
   time-stamp-active t          ; do enable time-stamps
   time-stamp-pattern "5/#+LAST_MODIFIED: [%04y-%02m-%02d %a %02H:%02M]" 
   time-stamp-format "%04y-%02m-%02d %a %02H:%02M") ; date format

Would this work with my capture template below? (See also the example file below.)
Here is the relevant capture template:

  (setq org-roam-capture-templates
    '(("d" "default" plain (function org-roam--capture-get-point)
       "%?"
       :file-name "${slug}"
       :head "#+TITLE: ${title} \n#+CREATED: %U\n#+LAST_MODIFIED: %U\n#+ROAM_ALIAS: \n\n- tags :: "
       :unnarrowed t)))
  )

And here is an example roam file:

#+TITLE: covenant as kinship 
#+CREATED: [2020-06-05 Fri 12:34]
#+LAST_MODIFIED: [2020-06-05 Fri 12:34]
- tags :: [[footcite:hahn09:_kinsh_coven][p. 28-9.]]

Bilateral covenants per Cross, Glueck, smith, FRymer-Kenski are
kinship-oriented. That is they extend kinship rights and
responsibilities to another party. This kinship relationship is made
via a [[file:covenantal_oath.org][covenantal oath]].

It is the 3rd line I’d like to modify.

1 Like

Here’s how I handle it in my config:

I don’t think this is something we should ship with Org-roam, but it’s not a hard-no.

2 Likes

Thank you! That is exactly what I needed. I can’t claim to weigh in on whether or not it should ship with org-roam. I am quite happy to have this in my config file(s). It is doing exactly what I want it to do.

1 Like

I would love to implement this in my configuration.
I use spacemacs, and I do not use use-package to load my layers.
Could you point me to how I can use this hook?
I think that I can simply put all the definitions of the functions in my config file.
But how should I introduce the before-save hook in you 2590 line?

Like this:

(add-hook 'before-save-hook #'zp/org-set-last-modified)
1 Like

thanks! but how does it know to activate it in org-mode? should it simply be in my user-config?

The check for Org-mode is run inside the function, so you don’t need to worry about it:

1 Like

Works like a charm. THANK YOU!

@roi.holtzman Could you put here your configuration for spacemacs for documentation? Thank you in advance.

Sure.

I added this part to my user-config in the init.el file.
(for spacemacs users that have their configuration in a .spacemacs file it should be in the user-config part of .spacemacs)

(add-hook 'before-save-hook #'zp/org-set-last-modified)

  (defun zp/org-find-time-file-property (property &optional anywhere)
    "Return the position of the time file PROPERTY if it exists.
When ANYWHERE is non-nil, search beyond the preamble."
    (save-excursion
      (goto-char (point-min))
      (let ((first-heading
             (save-excursion
               (re-search-forward org-outline-regexp-bol nil t))))
        (when (re-search-forward (format "^#\\+%s:" property)
                                 (if anywhere nil first-heading)
                                 t)
          (point)))))

  (defun zp/org-has-time-file-property-p (property &optional anywhere)
    "Return the position of time file PROPERTY if it is defined.
As a special case, return -1 if the time file PROPERTY exists but
is not defined."
    (when-let ((pos (zp/org-find-time-file-property property anywhere)))
      (save-excursion
        (goto-char pos)
        (if (and (looking-at-p " ")
                 (progn (forward-char)
                        (org-at-timestamp-p 'lax)))
            pos
          -1))))

  (defun zp/org-set-time-file-property (property &optional anywhere pos)
    "Set the time file PROPERTY in the preamble.
When ANYWHERE is non-nil, search beyond the preamble.
If the position of the file PROPERTY has already been computed,
it can be passed in POS."
    (when-let ((pos (or pos
                        (zp/org-find-time-file-property property))))
      (save-excursion
        (goto-char pos)
        (if (looking-at-p " ")
            (forward-char)
          (insert " "))
        (delete-region (point) (line-end-position))
        (let* ((now (format-time-string "[%Y-%m-%d %a %H:%M]")))
          (insert now)))))

  (defun zp/org-set-last-modified ()
    "Update the LAST_MODIFIED file property in the preamble."
    (when (derived-mode-p 'org-mode)
      (zp/org-set-time-file-property "LAST_MODIFIED")))
2 Likes

That’s great. Thank you !

Yes! This is a nice addition. It should be included in org-roam. Very useful.

2 Likes

A much simpler option is to use emacs’ in-built timestaamp functionality:

2 Likes

Could you show a example of config ?

that would simply be putting:

(require 'time-stamp)
(add-hook 'write-file-functions 'time-stamp)

in your config.

3 Likes

Is #+LAST_MODIFIED recognized by some other functions? I have tried to make time-stamp use the standard Org-mode #+date (I like lower case) keyword and only run it for Org mode files but I am not sure if #+date is allowed to have time.

(add-hook 'org-mode-hook
          (lambda () (add-hook 'before-save-hook 'time-stamp nil 'local)))

(add-hook 'org-mode-hook
          (lambda ()
            (set (make-local-variable 'time-stamp-pattern)
                 "8/^#\\+date: %%$")))

I just watched your tutorials, thanks for making them!

One of the first things I noticed was the LAST_MODIFIED property in your file headers, and immediately searched out your config to steal your code. I’ve wanted this for a long time, so thank you.

Using doom I do the following to update my timestamps:

(after! org
  (setq time-stamp-active t
    time-stamp-start "#\\+modified:[ \t]*"
    time-stamp-end "$"
    time-stamp-format "\[%04y-%02m-%02d %3a %02H:%02M\]")
(add-hook 'before-save-hook 'time-stamp))
1 Like

I’m trying @alexv suggestion which works very well with default time-stamp-format:

  (add-hook 'org-mode-hook
    (lambda () (add-hook 'before-save-hook 'time-stamp nil 'local)))

  (add-hook 'org-mode-hook
    (lambda ()
      (set (make-local-variable 'time-stamp-pattern)
     	   "7/^#\\+EDITED: %%$"
	   )))

but does not work if changing time-stamp-format in last but one line:

   	        "7/^#\\+EDITED: [%04y-%02m-%02d %3a %02H:%02M]"

Any idea how to make it work?

(add-hook 'org-mode-hook (lambda ()
                             (setq-local time-stamp-active t
                                         time-stamp-start "#\\+EDITED:[ \t]*"
                                         time-stamp-end "$"
                                         time-stamp-format "\[%04y-%02m-%02d %3a %02H:%02M\]")
                             (add-hook 'before-save-hook 'time-stamp nil 'local)))

There’s a comment in the source code saying not to change time-stamp-pattern.