doct icon indicating copy to clipboard operation
doct copied to clipboard

DOCT: Declarative Org Capture Templates for Emacs

  • DOCT: Declarative Org Capture Templates [[https://melpa.org/#/doct][file:https://melpa.org/packages/doct-badge.svg]]

~doct~ is a function that provides an alternative, declarative syntax for describing Org capture templates.

** tl;dr :PROPERTIES: :TOC: ignore :END: #+begin_src emacs-lisp (defun my-org-template-hook () (when (string= (org-capture-get :key t) "p1") (message ""First Child" selected.")))

(add-hook 'org-capture-mode-hook 'my-org-template-hook)

(setq org-capture-templates '(("p" "Parent") ("p1" "First Child" entry (file+headline "~/example.org" "One") "* TODO %^{Description} \n:PROPERTIES:\n:Created: %U\n:END:\n%?" :prepend t) ("p2" "Second Child" entry (file+headline "~/example.org" "Two") "* NEXT %^{Description} \n:PROPERTIES:\n:Created: %U\n:END:\n%?" :prepend t) ("p3" "Third Child" entry (file+headline "~/example.org" "Three") "* MAYBE %^{Description} \n:PROPERTIES:\n:Created: %U\n:END:\n%?" :prepend t))) #+end_src

becomes:

#+begin_src emacs-lisp (setq org-capture-templates (doct '(("Parent" :keys "p" :file "~/example.org" :prepend t :template ("* %{todo-state} %^{Description}" ":PROPERTIES:" ":Created: %U" ":END:" "%?") :children (("First Child" :keys "1" :headline "One" :todo-state "TODO" :hook (lambda () (message ""First Child" selected."))) ("Second Child" :keys "2" :headline "Two" :todo-state "NEXT") ("Third Child" :keys "3" :headline "Three" :todo-state "MAYBE")))))) #+end_src

** Releases

*** 3.1.0

  • add [[#doct-add-to][doct-add-to]] function *** 3.0.0
  • drop support for recursive =%{keyword}= expansion. *** 2.0.0
  • drop support for =%doct(KEYWORD)= syntax. Replaced with =%{keyword}=
  • =doct-get= now accepts an optional parameter to determine whether to query =org-capture-plist= or =org-capture-current-plist=.
  • adopt semver ** Contents :PROPERTIES: :TOC: :include all :END: :CONTENTS:
  • [[#doct-declarative-org-capture-templates][DOCT: Declarative Org Capture Templates]]
    • [[#tldr][tl;dr]]
    • [[#releases][Releases]]
      • [[#310][3.1.0]]
      • [[#300][3.0.0]]
      • [[#200][2.0.0]]
    • [[#contents][Contents]]
    • [[#installation][Installation]]
      • [[#melpa][MELPA]]
      • [[#manual][Manual]]
    • [[#documentation][Documentation]]
      • [[#doct][doct]]
        • [[#child][Child]]
        • [[#parent][Parent]]
        • [[#group][Group]]
        • [[#name--keys][Name & Keys]]
        • [[#children][Children]]
        • [[#inherited-properties][Inherited Properties]]
        • [[#type][Type]]
        • [[#target][Target]]
        • [[#template][Template]]
        • [[#additional-options][Additional options]]
        • [[#custom-data][Custom data]]
        • [[#keyword-expansion][%{KEYWORD} Expansion]]
        • [[#hooks][Hooks]]
        • [[#contexts][Contexts]]
        • [[#disabling-templates][Disabling Templates]]
        • [[#disabling-warnings][Disabling Warnings]]
      • [[#doct-add-to][doct-add-to]]
      • [[#custom-variables][Custom Variables]]
      • [[#contributing][Contributing]] :END:

** Installation *** MELPA Try it with the [[https://github.com/raxod502/straight.el][straight package manager]]: #+begin_src emacs-lisp (straight-use-package 'doct) #+end_src

or [[https://github.com/jwiegley/use-package][use-package]]: #+begin_src emacs-lisp (use-package doct :ensure t ;;recommended: defer until calling doct :commands (doct)) #+end_src *** Manual Download ~doct~ and make sure it is in your load-path. Require it in your init file. #+begin_src emacs-lisp (require 'doct) #+end_src

** Documentation *** doct #+begin_src emacs-lisp (doct declarations) #+end_src

~doct~ expects a declaration or a list of declarations as its sole argument and converts it into a list of org-capture-templates. Each declaration is either a child, parent, or group. **** Child A child declaration must have:

  • a name
  • a ~:keys~ string
  • a template type
  • a target
  • a template

and may also have:

  • hook functions defined with the hook keywords
  • contexts declared via the ~:contexts~ keyword
  • additional ~:KEY VAL~ arguments

**** Parent A parent declaration must have:

  • a name
  • a ~:keys~ string
  • a list of ~:children~

and may also have additional properties inherited by its children.

**** Group A group is a special kind of parent declaration. Its children inherit its properties, but it is not added to the template selection menu. Its name must be the ~:group~ keyword. It may optionally have a descriptive string for the value of ~:group~. It must not have a ~:keys~ value.

#+begin_src emacs-lisp :results scalar raw (doct '(("Work" :keys "w" :file "~/org/work.org" :children ((:group "Clocked" :clock-in t :children (("Phone Call" :keys "p" :template "* Phone call with %?") ("Meeting" :keys "m" :template "* Meeting with %?"))) ("Browsing" :keys "b" :template "* Browsing %x"))))) #+end_src

returns:

#+begin_src emacs-lisp (("w" "Work") ("wp" "Phone Call" entry (file "~/org/work.org") "* Phone call with %?" :clock-in t) ("wm" "Meeting" entry (file "~/org/work.org") "* Meeting with %?" :clock-in t) ("wb" "Browsing" entry (file "~/org/work.org") "* Browsing %x")) #+end_src

**** Name & Keys Every declaration must define a name. Unless it is a group , it must also define a ~:keys~ value. The name is the first value in the declaration. The ~:keys~ keyword defines the keys to access the template from the capture menu.

#+begin_src emacs-lisp (doct '(("example" :keys "e" ...))) #+end_src

returns:

#+begin_src emacs-lisp (("e" "example" ...)) #+end_src

**** Children The ~:children~ keyword defines a parent's children. Its value may be a single declaration or a list of declarations. The parent's ~:keys~ prefix each child's ~:keys~.

#+begin_src emacs-lisp (doct '(("parent" :keys "p" :children (("child" :keys "c" :children (("grandchild" :keys "g" :file "" :type plain :template "test"))))))) #+end_src

returns:

#+begin_src emacs-lisp (("p" "parent") ("pc" "child") ("pcg" "grandchild" plain (file "") "test")) #+end_src

**** Inherited Properties A child inherits its ancestors' properties. It may optionally override an inherited property by specifying that property directly.

For example:

#+begin_src emacs-lisp (doct '(("Grandparent" :keys "g" :file "example.org" :children ("Parent" :keys "p" :children ("Child" :keys "c"))))) #+end_src

The "Child" template inherits its ~:file~ property from the "Grandparent" declaration. The "Parent" declaration could override this value:

#+begin_src emacs-lisp (doct '(("Grandparent" :keys "g" :file "example.org" :children ("Parent" :keys "p" :file "overridden.org" :children ("Child" :keys "c"))))) #+end_src

And the "Child" would have its ~:file~ property set to "overridden.org".

**** Type

The ~:type~ keyword defines the template's entry type and accepts the following symbols:

  • entry :: An Org node with a headline. The template becomes a child of the target entry or a top level entry.

  • item :: A plain list item, placed in the first plain list at the target location.

  • checkitem :: A checkbox item. Same as plain list item only it uses a different default template.

  • table-line :: A new line in the first table at target location.

  • plain :: Text inserted as is.

~doct-default-entry-type~ defines the entry type when the ~:type~ keyword is not provided.

For example, with ~doct-default-entry-type~ set to ~entry~ (the default):

#+begin_src emacs-lisp (doct '(("example" :keys "e" :type entry :file ""))) #+end_src

And

#+begin_src emacs-lisp (doct '(("example" :keys "e" :file ""))) #+end_src

Both return:

#+begin_src emacs-lisp (("e" "example" entry (file "") nil)) #+end_src

**** Target The target defines the location of the inserted template text.

The first keyword declared in the following group exclusively sets the target. The ~:file~ keyword is not necessary for these.

  • :id "id of existing Org entry" :: File as child of this entry, or in the body of the entry (see [[https://github.com/emacs-mirror/emacs/blob/d0e2a341dd9a9a365fd311748df024ecb25b70ec/lisp/org/org-id.el#L45][org-id-get-create]] and the [[https://orgmode.org/manual/Template-elements.html#Template-elements][Org Mode Manual]])

  • :clock t :: File to the currently clocked entry

  • :function (lambda () ;visit file and move point to desired location...) :: This keyword is exclusive when used without the ~:file~ keyword. It is responsible for finding the proper file and location to insert the capture item. If ~:file~ defines a target file, then the function is only responsible for moving point to the desired location within that file.

#+begin_src emacs-lisp (doct '(("example" :keys "e" :type entry :clock t ;;ignored because clock is first :function (lambda () (ignore)) ;;also ignored :id "1"))) #+end_src

returns:

#+begin_src emacs-lisp (("e" "example" entry (clock) nil)) #+end_src

The ~:file~ keyword defines the target file for the capture template. It may be:

  • a string:

#+begin_src emacs-lisp (doct ... :file "/path/to/target.org") ;;empty string defaults to `org-default-notes-file' (doct ... :file "") #+end_src

  • a function:

#+begin_src emacs-lisp ;;lambda (doct ... :file (lambda () (concat (read-string "Org Capture Path: ") ".org"))) ;;or a function symbol (doct ... :file my/get-file-path) #+end_src

  • or a variable:

#+begin_src emacs-lisp (doct ... :file my/file-path) #+end_src

The following keywords refine the target file location:

  • :headline "node headline" :: File under unique heading in target file.

  • :olp ("Level 1 heading" "Level 2 heading"...) ::

    Define the full outline in the target file.

  • :datetree nil|t :: Requires use of the ~:file~ keyword. If ~:datetree~ has a non-nil value, create a date tree for today’s date. If ~:olp~ is given, the date tree is added under that heading path. Use a non-nil ~:time-prompt~ property to prompt for a different date. Set the ~:tree-type~ property to the symbol ~week~ create a week tree instead of the default month tree.

  • :regexp "regexp describing location" ::

    File to the entry matching regexp in target file

  • :function location-finding-function ::

    If used in addition to the ~:file~ keyword, the value should be a function that finds the desired location in that file. If used as an exclusive keyword (see above), the function must locate both the target file and move point to the desired location.

**** Template The ~:template~ keyword defines the template for creating the capture item. It may be either a string, list of strings, or a function. ~doct~ joins the list with new lines. A function must return the template text.

#+begin_src emacs-lisp (doct '((... :template ("Test" "One" "Two")))) #+end_src

returns:

#+begin_src emacs-lisp ((... "Test\nOne\nTwo")) #+end_src

The ~:template-file~ keyword defines a file containing the text of the template. For example:

#+begin_src emacs-lisp (doct '((... :template-file "~/org/templates/template.txt"))) #+end_src

will use the text of template.txt as the template string.

The first keyword declared overrides any additional template declarations.

**** Additional options Key-value pairs define additional options.

#+begin_src emacs-lisp (doct '((... :immediate-finish t))) #+end_src

returns:

#+begin_src emacs-lisp ((... :immediate-finish t)) #+end_src

see the [[https://orgmode.org/manual/Template-elements.html#Template-elements][Org Mode Manual]] for a full list of additional options.

**** Custom data :PROPERTIES: :CUSTOM_ID: custom-data :END: ~doct~ stores unrecognized keywords on the template's [[https://github.com/emacs-mirror/emacs/blob/d0e2a341dd9a9a365fd311748df024ecb25b70ec/lisp/org/org-capture.el#L450][org-capture-plist]] as members of the doct-custom plist. This makes a template's metadata accessible during capture. See [[#keyword-expansion][%{KEYWORD} Expansion]] for details on using that data.

The ~:custom~ keyword accepts a plist. doct copies the plist's values to the doct-custom plist. This is only necessary if you wish to use a keyword which doct already uses.

For example: #+begin_src emacs-lisp (doct '(("Music Gear" :keys "m" :file "" :custom (:keys "Moog")))) #+end_src

returns:

#+begin_src emacs-lisp (#1="m" #2="Music Gear" entry (file #3="") nil :doct (#2# :keys #1# :file #3# :custom #4=(:keys "Moog") :doct-custom #4#)))) #+end_src

**** %{KEYWORD} Expansion :PROPERTIES: :CUSTOM_ID: keyword-expansion :END: A declaration ~:template~ may include a keyword's value during capture. The syntax is similar to other, built-in "%-escapes". ~%{KEYWORD}~ will insert the value declared with ~:KEYWORD~ in the declaration.

For example, with:

#+begin_src emacs-lisp (doct '(("Parent" :keys "p" :file "" :template "* %{todo-state} %?" :children (("One" :keys "1" :todo-state "TODO") ("Two" :keys "2" :todo-state "IDEA"))))) #+end_src

Each child template has its ~:todo-state~ value expanded in the inherited ~:template.~

Values should be strings, functions or nil.

#+begin_src emacs-lisp results raw (doct '(("%{string}" :keys "s" :type plain :file "" :string "string" :template "%{string}"))) #+end_src

Is replaced with:

#+begin_src emacs-lisp "string" #+end_src

#+begin_src emacs-lisp results raw (doct '(("%{fn}" :keys "f" :type plain :file "" :fn (lambda () "string returned from function") :template "%{fn}"))) #+end_src

Is replaced with:

#+begin_src emacs-lisp "string returned from function" #+end_src

#+begin_src emacs-lisp (doct '(("%{nil}" :keys "f" :type plain :file "" :nil nil :template "%{nil}"))) #+end_src

Is replaced with the empty string

#+begin_src emacs-lisp "" #+end_src

Custom keywords take precedence over other declaration keywords. For example, with:

#+begin_src emacs-lisp (doct '(("Music Gear" :keys "m" :file "" :type plain :custom (:keys "Moog") :template "%{keys}"))) #+end_src

The "Music Gear" template expands to "Moog" instead of "m". Nil values expand to an empty string.

**** Hooks Adding the following hook keywords in a declaration adds its value to the appropriate org-capture hook. The value may be a function or a variable.

  • :hook function ::

    • org-capture-mode-hook ::

      Runs FUNCTION when entering the org-capture-mode minor mode.

  • :prepare-finalize function ::

    • org-capture-prepare-finalize-hook ::

      Runs FUNCTION before the finalization starts. The capture buffer is current and narrowed.

  • :before-finalize function ::

    • org-capture-before-finalize-hook ::

      Runs FUNCTION right before a capture process finalizes. The capture buffer is still current and widened to the entire buffer.

  • :after-finalize function ::

    • org-capture-after-finalize-hook :: Runs FUNCTION right after a capture process finalizes. Suitable for window cleanup.

For example:

#+begin_src emacs-lisp (doct `(("example" :keys "e" :file "" :hook ,(defun my/fn () (ignore))))) #+end_src

runs ~my/fn~ during the ~org-capture-mode-hook~ when selecting the "example" template.

**** Contexts The ~:contexts~ keyword defines contextual rules for a template. Its value may be a single contextual rule or a list of rules. The following keywords are available to create contextual rules:

  • :in-buffer regexp :: Show template when REGEXP matches the current buffer's name. #+begin_src emacs-lisp :results scalar (doct '(("Only in scratch" :keys "n" :file "" :contexts (:in-buffer "^\scratch\$")))) #+end_src

  • :unless-buffer regexp :: Show template unless REGEXP matches the current buffer's name. #+begin_src emacs-lisp :results scalar (doct '(("Except in scratch" :keys "n" :file "" :contexts (:unless-buffer "^\scratch\$")))) #+end_src

  • :in-file regexp :: Show template when REGEXP matches the current buffer's file name. #+begin_src emacs-lisp :results scalar (doct '(("Only in work.org" :keys "n" :file "" :contexts (:in-file "work\.org$")))) #+end_src

  • :unless-file regexp :: Show template unless REGEXP matches the current buffer's file name. #+begin_src emacs-lisp :results scalar (doct '(("Except in work.org" :keys "n" :file "" :contexts (:unless-file "work\.org$")))) #+end_src

  • :in-mode regexp :: Show template when REGEXP matches the current buffer's major mode. #+begin_src emacs-lisp :results scalar (doct '(("Only in org-mode" :keys "n" :file "" :contexts (:in-mode "org-mode")))) #+end_src

  • :unless-mode regexp :: Show template unless REGEXP matches the current buffer's major mode. #+begin_src emacs-lisp :results scalar (doct '(("Except in org-mode" :keys "n" :file "" :contexts (:unless-mode "org-mode")))) #+end_src

  • :when condition :: Show template when CONDITION evaluates to a non-nil value. Condition may be a function or a single form. #+begin_src emacs-lisp :results scalar (doct '(("Show when my/predicate-p returns t" :keys "n" :file "" :contexts (:when my/predicate-p)))) #+end_src

#+begin_src emacs-lisp :results scalar (doct '(("1/3 chance of showing" :keys "n" :file "" :contexts (:when (= 2 (random 3)))))) #+end_src

  • :unless condition :: Show template when CONDITION evaluates to a nil value. Condition may be a function or a single form. #+begin_src emacs-lisp :results scalar (doct '(("Show when my/predicate-p returns nil" :keys "n" :file "" :contexts (:unless my/predicate-p)))) #+end_src

#+begin_src emacs-lisp :results scalar (doct '(("2/3 chance of showing" :keys "n" :file "" :contexts (:unless (= 2 (random 3)))))) #+end_src

  • :function function :: Show template when FUNCTION returns non-nil. The function is not passed any arguments. #+begin_src emacs-lisp :results scalar (doct '(("Between 9AM and 5PM" :keys "n" :file "" :contexts (:function (lambda () (<= 9 (string-to-number (format-time-string "%H")) 17))))))) #+end_src

Adding ~:keys~ to a rule does the same as above, but remaps the template's keys to the template with keys matching the ~:keys~ string. For example: #+begin_src emacs-lisp :results scalar (doct '(("In scratch remapped to t, else use original template" :keys "n" :file "" :contexts ((:unless-buffer "^\scratch\$" :keys "n") (:in-buffer "^\scratch\$" :keys "t"))))) #+end_src

The above rule keywords, spare ~:function~, ~:when~, and ~:unless~ may also take a list of strings for their values. #+begin_src emacs-lisp (doct '(("Only in org-mode or emacs-lisp-mode" :keys "n" :file "" :contexts (:in-mode ("org-mode" "emacs-lisp-mode"))))) #+end_src

**** Disabling Templates :PROPERTIES: :CUSTOM_ID: disabling-templates :END: Setting the ~:disabled~ keyword to t disables a template. The template's declaration is not error checked. This can be useful if you don't have the time to deal with an error right away. For example: #+begin_src emacs-lisp :results raw scalar (doct '((:group "All" :file "" :children ((:group "Enabled" :children (("One" :keys "1") ("Two" :keys "2") ("Three" :keys "3"))) (:group "Disabled" :disabled t :children (("Four" :keys 4) ("Five" :keys 5) ("Six" :keys 6))))))) #+end_src

returns:

#+begin_src emacs-lisp (("1" "One" entry (file "") nil) ("2" "Two" entry (file "") nil) ("3" "Three" entry (file "") nil)) #+end_src

Normally template "Four" would throw an error because its ~:keys~ are not a string.

**** Disabling Warnings The =:warn= keyword disables =doct='s warnings on a per-declaration basis. For example:

#+begin_src emacs-lisp (let ((doct-warnings t)) (doct '(("Ignore unbound symbol warnings" :keys "i" :warn (:not unbound) :file unbound-variable :function unbound-function) ("Warn here, though" :keys "w" :file unbound-variable :function unbound-function)))) #+end_src

For global control of warnings and an explanation of accepted values see =doct-warnings= in [[#custom-variables][Custom Variables]].

*** doct-add-to #+begin_src emacs-lisp (doct-add-to list declarations &optional append) #+end_src

Return a copy of =LIST= with converted =DECLARATIONS= added. If =APPEND= is non-nil, add to back of =LIST=. Otherwise, add to the front of =LIST=. =DECLARATIONS= are passed to =doct= and must be the same of the same form =doct= accepts.

e.g. #+begin_src emacs-lisp ;; `org-capture-templates' set earlier elsewhere...

(setq org-capture-templates (doct-add-to org-capture-templates '("example" :keys "e" ...) 'append)) #+end_src *** Custom Variables :PROPERTIES: :CUSTOM_ID: custom-variables :END: ~doct~ supports the following variables for customization:

  • doct-default-entry-type 'entry :: The default template entry type. It can be overridden on a per-declaration basis by using the ~:type~ keyword.

  • doct-after-conversion-functions :: Abnormal hook run after converting declarations to templates. Hook functions run with the list of templates as their only argument. The templates are not flattened at this point and are of the form: #+begin_src emacs-lisp (((parent) (child)...)...). #+end_src

  • doct-warnings :: When non-nil, doct will issue warnings. Valid values are:

    • t :: warn in all cases
    • nil :: do not warn Or a list containing any of the following symbols:
    • unbound :: warn when a symbol is unbound during conversion
    • template-keyword :: warn when %{KEYWORD} is not found on the declaration during conversion.
    • template-keyword-type :: warn when %{KEYWORD} expansion does not return a string.
    • template-entry-type :: warn when the expanded template string does not match the capture template's entry type
    • template-file :: warn when the ~:template-file~'s file is not found during conversion
    • option-type :: warn when additional options are not the proper type

    If the list's first element is the ~:not~ keyword, the list of warnings is disabled. It can be overridden on a per-declaration basis with the :warn keyword."

    For example: #+begin_src emacs-lisp (let ((doct-warnings t)) (doct '(("Ignore unbound symbol warnings" :keys "i" :warn (:not unbound) :file unbound-variable :function unbound-function) ("Warn here, though" :keys "w" :file unbound-variable :function unbound-function)))) #+end_src

*** Contributing

Pull/feature requests, code review, angry comments are all welcome.

Please add a test to the test suite if you introduce any changes.

Thanks, nv

LocalWords: DOCT MELPA init doct txt plist unintern nv tl dr semver