Org-roam-capture subdirectories creation

Hi there,
I am trying to create a node in a non-existent directory. I.e.

("f"
"foo"
plain
"%?"
:target (file+head "${slug}/${slug}.org" "#+title: ${title}")
:unnarrowed t)

however, the message is

Use M-x make-directory RET RET to create the directory and its parents

It seems as the directory needs to be created in advance.
That has some drawbacks:

  1. Creating directories even though the file is not yet persisted to disk.
  2. No way to undo the creation if I choose to abort the capture

Furthermore, the prompt blocks the ability to abort the capture.

Is there a way to defer creation of the directories to the point where I press C-c C-c or maybe delete directory when I press C-c C-k?

This long exchange with Jethro (author of Org-roam) would shed some lights on the reason for the design and possible customization.

This exchange has very useful info, thanks!
However, I encountered a problem when trying to abort the capture:

  1. I abort the capture (C-c C-k).
  2. There is a prompt to create the missing directories (Directory /path/to/dir/ does not exist; create? (y or n)).
  3. Choosing n returns me to the capture buffer.
  4. Selecting y yields in another prompt to delete the aborted capture (Delete file for aborted capture?(yes or no)).

This makes aborting the capture impossible. It seems as the issue lies in org-capture-finalize, judging by org-capture-kill (which is called when the capture is aborted) – it probably always (tries to) create the captured file.

Is there a way to make aborting possible in this scenario?

I agree that the y/n prompts are awkward and the new directory remains, so it’s not really a clean workflow. You can abort and delete the file, though, so “impossible” seems to be an exaggeration.

Alternatively, how about trying something like this below? I have lightly tested it and it seems to work. It creates a new directory if it does not exist as part of the capture process, and deletes it if you abort the capture process. I did not expect this, it seems to remove the awkward y/n prompts, so it helps you achieve a smoother workflow.

If you have auto-save-visited-mode and/or auto-save-mode your experience might be slightly different. I suggest to turn them off when you try the code below.

EDIT: Corrected a typo in the name of a hook in the code below.

(add-hook 'org-roam-capture-new-node-hook #'my/create-new-dir-maybe)

(defun my/create-new-dir-maybe ()
  "Create a new dir during org capture process.
It also sets a hook to delete the newly created dir if the
capture process is aborted.

Meant to be set to `org-roam-capture-new-node-hook'."
  (let* ((filename (org-roam-capture--get :new-file))
         (new-dir (file-name-directory filename)))
    (unless (file-exists-p new-dir)
      (make-directory (file-name-directory filename) :PARENTS)
      ;; Depth 90 is there to ensure it is run after
      ;; `org-roam-capture--finalize', which prompts for file deletion and
      ;; if yes deletes the new file.
      ;; Set this to run only when the dir is new and has not existed before
      (add-hook 'org-capture-after-finalize-hook #'my/delete-new-dir-maybe 90))))

(defun my/delete-new-dir-maybe ()
  "Delete a newly created dir when org capture is aborted.

Meant to be set to `org-capture-after-finalize-hook' dynamically
by `my/create-new-dir-maybe' and removes itself from the hook at the end."
  (when org-note-abort
    (let* ((filename (org-roam-capture--get :new-file))
           (new-dir (file-name-directory filename)))
      (delete-directory new-dir)))
  (remove-hook 'org-capture-after-finalize-hook #'my/delete-new-dir-maybe))

It is my fault for not being clear enough – when trying to abort, the prompt would not accept n as an answer and close the capture, and would just return to the capture try (where I can again choose C-c C-c and C-c C-k again). In a sense, aborting was available only if I opted to create the directory, which as you have said, is not really a clean workflow – but indeed possible.

Why is this important? It does not seem as my/delete-new-dir-maybe is affected by the decision – unless your intention was to make delete-directory fail (thus, not deleting the directory) if (and only if) no was selected – as the directory would be non-empty.

Regardless, I would like to thank you very much for all your help, this code snippet is the exact idea I was looking for.

It is important because otherwise the new file would remain in the new directly at the time you were to delete it. I believe you would get another prompt asking if it was OK to delete recursively.

But… maybe you are aborting the capture before the file gets written thus this might not be important.

In any case, did the snippet work for you?

From my (limited) use, it refuses to delete the directory Removing directory: Directory not empty, /path/to/dir. But it is not hard to fix (file-exists-p in my/delete-new-dir-maybe), but now it becomes clear that org-roam-capture--finalize should run before my/delete-new-dir-maybe.

Yes, the only some-what annoying is the message Use M-x make-directory RET RET to create the directory and its parents which is generated (by find-file I guess) when opening the capture. my/create-new-dir-maybe is only called afterward.

I tried to use find-file-not-found-functions with a function to detect when I am in a capture context (similar to org-roam-capture-p, but for org-capture – “org-capture-p”) and only then to auto-create the missing directories. For some reason, (org-capture-get :buffer) yielded unexpected results (I could not compare the buffers, as it returned nil sometimes), so I had to fall back to the other (somewhat inelegant) method (extracting and comparing the filenames the buffers visit, which somehow always resulted the expected result). Maybe it has to do with the context in which find-file-not-found-functions hooks are running in.

Given that it is only a mild issue (which is resolved when I use org-roam-capture-p to determine the capture context), I just want to know why the above behavior happens (for educational purposes), but overall the snippet does work for me.