Skip to content

Instantly share code, notes, and snippets.

@samspills
Last active August 18, 2024 17:26
Show Gist options
  • Save samspills/f8d6635cd102909894e4aa0d3ca38ae1 to your computer and use it in GitHub Desktop.
Save samspills/f8d6635cd102909894e4aa0d3ca38ae1 to your computer and use it in GitHub Desktop.
Org-capture prompt for heading

How to prompt for a heading during org-capture

Background

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

Problem Description

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.

The Solution

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)

How the function works

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.

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