I use one file as my org-mode daily driver. This file is called journal.org. It starts with a datetree, which is where I capture general notes, individual tasks, ideas, things to read, etc. etc. I have separate headings for other content, for example all of my projects are under a projects heading, further split by context. Roughly the file looks like this:
* 2021
** 2021-03 March
*** 2021-03-15 Monday
**** [2021-03-15 Mon 16:29] note title :tagA:tagB:
- note content here ooooo
**** TODO investigate org-capture to heading
* 2021-03-16 Tuesday
**** [2021-03-16 Tue 14:25] another note title :tagB:
* Projects :projects:
** Company
*** Project 1
*** Project 2
** Personal
*** Project 3
** Blerf
*** Project 4
For a while I tried to keep to the daily log structure for project tasks and notes as well. I would use the agenda and sparce trees to see anything related to a project, relying on tag filtering. This didn’t work so well for me though; I wanted to keep project context together. I could have refiled from the capture buffer but often the muscle memory of C-c C-c would win out.
It seemed that the best way for me to keep project context together would be to create specific capture templates for project notes or todos. I wanted the capture flow to prompt me to select a project heading and then capture underneath that heading.
What I came up with was a function that would gather the project headings, prompt me to select one, and then set the capture target location to that heading.
The function:
(defun sam/org-capture-to-project-heading ()
(let ((projects
(org-map-entries `(lambda () (nth 4 (org-heading-components)))
"+project+LEVEL=3" '("~/life/journal.org"))))
(setq choice (ivy-completing-read "Project:" projects nil t nil nil))
(org-capture-set-target-location (list 'file+headline "~/life/journal.org" choice))
))The capture templates that use it in:
("pt" "Project Todo" entry (file+function org-capture-journal-file sam/org-capture-to-project-heading)
"* TODO %?\n" :prepend t)
("pn" "Project Note" entry (file+function org-capture-journal-file sam/org-capture-to-project-heading)
"* %U %?\n%i\n%a" :prepend t)The real heavy lifter in my function is the call to org-map-entries.
(org-map-entries `(lambda () (nth 4 (org-heading-components)))
"+project+LEVEL=3" '("~/life/journal.org"))org-map-entries can find all entries satisfying some criteria and then apply a function to those entries. In this case we find all entries in the file life/journal.org with the project tag and are level 3 headings. The requirements here are a detail of how my file is structured. This match condition could easily be changed for other use-cases. The function applied passes each entry to org-heading-components and then takes the 4th element of that list (the heading title). This produces a list of heading titles that are projects in my journal file.
To choose the project heading, we use the ivy-completing-read function
(setq choice (ivy-completing-read "Project: " projects nil t nil nil))ivy-completing-read will read a string from the minibuffer with completion. The prompt will display the Project: string, and then the list of project heading titles from the output of org-map-entries will be displayed as the colleciton. The optional required-match paramenter is set to t, meaning that the final selection must be an element of the collection.
Now we have the heading we want to capture to, and we also know the file, so the logic of navigating to the correct point and inserting a new heading at the right level can all be left to the usual org-capture logic by reusing the org-capture-set-target-location.
(org-capture-set-target-location (list 'file+headline "~/life/journal.org" choice))We pass the new target as a file+headline with the file being the same journal.org we’ve been focusing on this whole time, and the heading set to the selected choice from the minibuffer.