(feat!)capture: unify template variables

Instead of using various capture template variables, like
org-roam-dailies-capture-templates or org-roam-capture-ref-templates,
just use org-roam-capture-templates for everything.

From now on, each individual template will able to optionally specify a
more detailed context for which they're dedicated for, using :kind and
:action properties. Both of them accept either, a symbol or a list of
symbols.

If it's a list, then it can optionally start with :not symbol to
indicate inverse logic, otherwise it will act akin to memq. If it's a
symbol, then it will work akin to eq, but with support for special 'any
value, which indicates that it will work in any contexts; 'any can be
also used inside of the list based values.

Org-roam and the built-in extension will provide the following contexts:
- :action {capture,goto,any}
- :kind {normal,daily,protocol,any}
but third party extensions / users can easily extend them for their own
needs.

If :action isn't specified it will implicitly default to 'capture, while
:kind will default to 'normal contexts.

BREAKING CHANGE: org-roam-dailies-capture-templates and
org-roam-capture-ref-templates are now removed in the favor of unified
approach.
This commit is contained in:
Wetlize
2021-08-25 00:36:17 +03:00
parent e9ae19c01c
commit b6a898d6d6
5 changed files with 156 additions and 165 deletions

View File

@ -36,6 +36,18 @@
;; One can use dailies for various purposes, e.g. journaling, fleeting notes, ;; One can use dailies for various purposes, e.g. journaling, fleeting notes,
;; scratch notes and whatever else you can came up with. ;; scratch notes and whatever else you can came up with.
;; ;;
;; To add new capture templates dedicated for dailies, specify ":kind daily" for
;; each of such template in `org-roam-capture-templates', e.g.
;;
;; (setq org-roam-capture-templates
;; '(("d" "daily" entry "* %?" :kind daily
;; :if-new (file+head "%<%Y-%m-%d>.org"
;; "#+title: %<%Y-%m-%d>\n"))))
;;
;; Note that in order for your daily files to properly integrate with the
;; calendar, each daily file should be named with a format understood by
;; `org-parse-time-string'.
;;
;;; Code: ;;; Code:
(require 'f) (require 'f)
(require 'dash) (require 'dash)
@ -49,8 +61,9 @@
;;; Options ;;; Options
(defcustom org-roam-dailies-directory "daily/" (defcustom org-roam-dailies-directory "daily/"
"Path to daily-notes. "Path to daily-notes. This path is relative to `org-roam-directory'.
This path is relative to `org-roam-directory'." Daily based capture templates will automatically start from this
path."
:group 'org-roam :group 'org-roam
:type 'string) :type 'string)
@ -59,74 +72,6 @@ This path is relative to `org-roam-directory'."
:group 'org-roam :group 'org-roam
:type 'hook) :type 'hook)
(defcustom org-roam-dailies-capture-templates
`(("d" "default" entry
"* %?"
:if-new (file+head "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>\n")))
"Capture templates for daily-notes in Org-roam.
Note that for daily files to show up in the calendar, they have to be of format
\"org-time-string.org\".
See `org-roam-capture-templates' for the template documentation."
:group 'org-roam
:type '(repeat
(choice (list :tag "Multikey description"
(string :tag "Keys ")
(string :tag "Description"))
(list :tag "Template entry"
(string :tag "Keys ")
(string :tag "Description ")
(choice :tag "Capture Type " :value entry
(const :tag "Org entry" entry)
(const :tag "Plain list item" item)
(const :tag "Checkbox item" checkitem)
(const :tag "Plain text" plain)
(const :tag "Table line" table-line))
(choice :tag "Template "
(string)
(list :tag "File"
(const :format "" file)
(file :tag "Template file"))
(list :tag "Function"
(const :format "" function)
(function :tag "Template function")))
(plist :inline t
;; Give the most common options as checkboxes
:options (((const :format "%v " :if-new)
(choice :tag "Node location"
(list :tag "File"
(const :format "" file)
(string :tag " File"))
(list :tag "File & Head Content"
(const :format "" file+head)
(string :tag " File")
(string :tag " Head Content"))
(list :tag "File & Outline path"
(const :format "" file+olp)
(string :tag " File")
(list :tag "Outline path"
(repeat (string :tag "Headline"))))
(list :tag "File & Head Content & Outline path"
(const :format "" file+head+olp)
(string :tag " File")
(string :tag " Head Content")
(list :tag "Outline path"
(repeat (string :tag "Headline"))))))
((const :format "%v " :prepend) (const t))
((const :format "%v " :immediate-finish) (const t))
((const :format "%v " :jump-to-captured) (const t))
((const :format "%v " :empty-lines) (const 1))
((const :format "%v " :empty-lines-before) (const 1))
((const :format "%v " :empty-lines-after) (const 1))
((const :format "%v " :clock-in) (const t))
((const :format "%v " :clock-keep) (const t))
((const :format "%v " :clock-resume) (const t))
((const :format "%v " :time-prompt) (const t))
((const :format "%v " :tree-type) (const week))
((const :format "%v " :unnarrowed) (const t))
((const :format "%v " :table-line-pos) (string))
((const :format "%v " :kill-buffer) (const t))))))))
;;; Commands ;;; Commands
;;;; Today ;;;; Today
;;;###autoload ;;;###autoload
@ -306,8 +251,8 @@ When GOTO is non-nil, go the note without creating an entry."
(let ((org-roam-directory (expand-file-name org-roam-dailies-directory org-roam-directory))) (let ((org-roam-directory (expand-file-name org-roam-dailies-directory org-roam-directory)))
(org-roam-capture- :goto (when goto '(4)) (org-roam-capture- :goto (when goto '(4))
:node (org-roam-node-create) :node (org-roam-node-create)
:templates org-roam-dailies-capture-templates :props (list :kind 'daily
:props (list :override-default-time time))) :override-default-time time)))
(when goto (run-hooks 'org-roam-dailies-find-file-hook))) (when goto (run-hooks 'org-roam-dailies-find-file-hook)))
(add-hook 'org-roam-capture-preface-hook #'org-roam-dailies--override-capture-time-h) (add-hook 'org-roam-capture-preface-hook #'org-roam-dailies--override-capture-time-h)
@ -317,6 +262,12 @@ When GOTO is non-nil, go the note without creating an entry."
(when (org-roam-capture--get :override-default-time) (when (org-roam-capture--get :override-default-time)
(org-capture-put :default-time (org-roam-capture--get :override-default-time))))) (org-capture-put :default-time (org-roam-capture--get :override-default-time)))))
(when (org-roam-capture--load-templates-p 'org-roam-dailies)
(push '("d" "daily" entry "* %?" :kind daily
:if-new (file+head "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>\n"))
org-roam-capture-templates))
;;; Bindings ;;; Bindings
(defvar org-roam-dailies-map (make-sparse-keymap) (defvar org-roam-dailies-map (make-sparse-keymap)
"Keymap for `org-roam-dailies'.") "Keymap for `org-roam-dailies'.")

View File

@ -32,12 +32,21 @@
;; 1. "roam-node": This protocol simply opens the node given by the node ID ;; 1. "roam-node": This protocol simply opens the node given by the node ID
;; 2. "roam-ref": This protocol creates or opens the node with the given REF ;; 2. "roam-ref": This protocol creates or opens the node with the given REF
;; ;;
;; You can find detailed instructions on how to setup the protocol in the ;; To add new capture templates dedicated for the protocol, specify ":kind
;; documentation for Org-roam. ;; protocol" for each of such template in `org-roam-capture-templates', e.g.
;;
;; (setq org-roam-capture-templates
;; '(("r" "ref" plain "%?" :kind protocol
;; :if-new (file+head "${slug}.org"
;; "#+title: ${title}")
;; :unnarrowed t)))
;;
;; You can find a detailed instruction on how to setup the protocol in the
;; manual for Org-roam.
;; ;;
;;; Code: ;;; Code:
(require 'org-protocol) (require 'org-protocol)
(require 'ol) ;; for org-link-decode (require 'ol) ; to use `org-link-decode'
(require 'org-roam) (require 'org-roam)
;;; Options ;;; Options
@ -46,73 +55,12 @@
:type 'boolean :type 'boolean
:group 'org-roam) :group 'org-roam)
(defcustom org-roam-capture-ref-templates ;;; Protocols
'(("r" "ref" plain "%?" (mapc (lambda (spec) (cl-pushnew spec org-protocol-protocol-alist :test #'equal))
:if-new (file+head "${slug}.org" '(("org-roam-ref" :protocol "roam-ref" :function org-roam-protocol-open-ref)
"#+title: ${title}") ("org-roam-node" :protocol "roam-node" :function org-roam-protocol-open-node)))
:unnarrowed t))
"The Org-roam templates used during a capture from the roam-ref protocol.
See `org-roam-capture-templates' for the template documentation."
:group 'org-roam
:type '(repeat
(choice (list :tag "Multikey description"
(string :tag "Keys ")
(string :tag "Description"))
(list :tag "Template entry"
(string :tag "Keys ")
(string :tag "Description ")
(choice :tag "Capture Type " :value entry
(const :tag "Org entry" entry)
(const :tag "Plain list item" item)
(const :tag "Checkbox item" checkitem)
(const :tag "Plain text" plain)
(const :tag "Table line" table-line))
(choice :tag "Template "
(string)
(list :tag "File"
(const :format "" file)
(file :tag "Template file"))
(list :tag "Function"
(const :format "" function)
(function :tag "Template function")))
(plist :inline t
;; Give the most common options as checkboxes
:options (((const :format "%v " :if-new)
(choice :tag "Node location"
(list :tag "File"
(const :format "" file)
(string :tag " File"))
(list :tag "File & Head Content"
(const :format "" file+head)
(string :tag " File")
(string :tag " Head Content"))
(list :tag "File & Outline path"
(const :format "" file+olp)
(string :tag " File")
(list :tag "Outline path"
(repeat (string :tag "Headline"))))
(list :tag "File & Head Content & Outline path"
(const :format "" file+head+olp)
(string :tag " File")
(string :tag " Head Content")
(list :tag "Outline path"
(repeat (string :tag "Headline"))))))
((const :format "%v " :prepend) (const t))
((const :format "%v " :immediate-finish) (const t))
((const :format "%v " :jump-to-captured) (const t))
((const :format "%v " :empty-lines) (const 1))
((const :format "%v " :empty-lines-before) (const 1))
((const :format "%v " :empty-lines-after) (const 1))
((const :format "%v " :clock-in) (const t))
((const :format "%v " :clock-keep) (const t))
((const :format "%v " :clock-resume) (const t))
((const :format "%v " :time-prompt) (const t))
((const :format "%v " :tree-type) (const week))
((const :format "%v " :unnarrowed) (const t))
((const :format "%v " :table-line-pos) (string))
((const :format "%v " :kill-buffer) (const t))))))))
;;; Handlers ;;;; roam-ref
(defun org-roam-protocol-open-ref (info) (defun org-roam-protocol-open-ref (info)
"Process an org-protocol://roam-ref?ref= style url with INFO. "Process an org-protocol://roam-ref?ref= style url with INFO.
@ -146,29 +94,9 @@ It opens or creates a note with the given ref.
:node (org-roam-node-create :title (plist-get info :title)) :node (org-roam-node-create :title (plist-get info :title))
:info (list :ref (plist-get info :ref) :info (list :ref (plist-get info :ref)
:body (plist-get info :body)) :body (plist-get info :body))
:templates org-roam-capture-ref-templates) :props '(:kind protocol))
nil) nil)
(defun org-roam-protocol-open-node (info)
"This handler simply opens the file with emacsclient.
INFO is a plist containing additional information passed by the protocol URL.
It should contain the FILE key, pointing to the path of the file to open.
Example protocol string:
org-protocol://roam-node?node=uuid"
(when-let ((node (plist-get info :node)))
(raise-frame)
(org-roam-node-visit (org-roam-populate (org-roam-node-create :id node)) nil 'force))
nil)
(push '("org-roam-ref" :protocol "roam-ref" :function org-roam-protocol-open-ref)
org-protocol-protocol-alist)
(push '("org-roam-node" :protocol "roam-node" :function org-roam-protocol-open-node)
org-protocol-protocol-alist)
;;; Capture implementation
(add-hook 'org-roam-capture-preface-hook #'org-roam-protocol--try-capture-to-ref-h) (add-hook 'org-roam-capture-preface-hook #'org-roam-protocol--try-capture-to-ref-h)
(defun org-roam-protocol--try-capture-to-ref-h () (defun org-roam-protocol--try-capture-to-ref-h ()
"Try to capture to an existing node that match the ref." "Try to capture to an existing node that match the ref."
@ -186,6 +114,27 @@ org-protocol://roam-node?node=uuid"
(when-let ((ref (plist-get org-roam-capture--info :ref))) (when-let ((ref (plist-get org-roam-capture--info :ref)))
(org-roam-ref-add ref))) (org-roam-ref-add ref)))
(when (org-roam-capture--load-templates-p 'org-roam-protocol)
(push '("r" "ref" plain "%?" :kind protocol
:if-new (file+head "${slug}.org"
"#+title: ${title}")
:unnarrowed t)
org-roam-capture-templates))
;;;; roam-node
(defun org-roam-protocol-open-node (info)
"This handler simply opens the file with emacsclient.
INFO is a plist containing additional information passed by the protocol URL.
It should contain the FILE key, pointing to the path of the file to open.
Example protocol string:
org-protocol://roam-node?node=uuid"
(when-let ((node (plist-get info :node)))
(raise-frame)
(org-roam-node-visit (org-roam-populate (org-roam-node-create :id node)) nil 'force))
nil)
(provide 'org-roam-protocol) (provide 'org-roam-protocol)

View File

@ -38,6 +38,33 @@
(defvar org-end-time-was-given) (defvar org-end-time-was-given)
;;; Options ;;; Options
(defcustom org-roam-capture-load-default-templates t
"Whether to include default Org-roam capture templates during the loading.
The value will also affect default templates provided by
Org-roam's extensions. It can be:
t
Include all the default capture templates provided by
Org-roam and its extensions.
nil
Don't include the default capture templates provided by
Org-roam and its extensions.
a list of symbols
Each symbol in the list corresponds to a `provide'd FEATURE,
for which the default capture templates will be automatically
included (if any). The list can start with a special value
`:not', in which case the logic will be inverted to
exclusion, e.g. (:not org-roam-dailies) won't include the
default templates provided by `org-roam-dailies', but will
include for other features."
:type '(choice
(const :tag "Yes" t)
(const :tag "No" nil)
(repeat :tag "List features" symbol))
:group 'org-roam)
(defcustom org-roam-capture-templates (defcustom org-roam-capture-templates
'(("d" "default" plain "%?" '(("d" "default" plain "%?"
:if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org" :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org"
@ -378,9 +405,18 @@ This variable is populated dynamically, and is only non-nil
during the Org-roam capture process.") during the Org-roam capture process.")
(defconst org-roam-capture--template-keywords (list :if-new :id :link-description :call-location (defconst org-roam-capture--template-keywords (list :if-new :id :link-description :call-location
:region) :region :kind :action)
"Keywords used in `org-roam-capture-templates' specific to Org-roam.") "Keywords used in `org-roam-capture-templates' specific to Org-roam.")
(defun org-roam-capture--load-templates-p (feature)
"Return t if capture templates for FEATURE are allowed to be loaded.
See `org-roam-capture-load-default-templates' for more details."
(let ((user-value org-roam-capture-load-default-templates))
(pcase user-value
((pred consp) (org-roam--valid-option-p feature user-value))
((pred null) nil)
('t t))))
;;; Main entry point ;;; Main entry point
;;;###autoload ;;;###autoload
(cl-defun org-roam-capture- (&key goto keys node info props templates) (cl-defun org-roam-capture- (&key goto keys node info props templates)
@ -390,11 +426,18 @@ INFO is a plist for filling up Org-roam's capture templates.
NODE is an `org-roam-node' construct containing information about the node. NODE is an `org-roam-node' construct containing information about the node.
PROPS is a plist containing additional Org-roam properties for each template. PROPS is a plist containing additional Org-roam properties for each template.
TEMPLATES is a list of org-roam templates." TEMPLATES is a list of org-roam templates."
(let* ((props (plist-put props :call-location (point-marker))) (let* ((props (thread-first props
(plist-put :call-location (point-marker))
(plist-put :action (or (plist-get props :action) (if goto 'goto 'capture)))
(plist-put :kind (or (plist-get props :kind) 'normal))))
(templates (org-roam-capture-get-templates
:action (plist-get props :action)
:kind (plist-get props :kind)
:templates (or templates org-roam-capture-templates)))
(org-capture-templates (org-capture-templates
(mapcar (lambda (template) (mapcar (lambda (template)
(org-roam-capture--convert-template template props)) (org-roam-capture--convert-template template props))
(or templates org-roam-capture-templates))) templates))
(org-roam-capture--node node) (org-roam-capture--node node)
(org-roam-capture--info info)) (org-roam-capture--info info))
(when (and (not keys) (when (and (not keys)
@ -429,6 +472,27 @@ valid for the capture (i.e. initialization, and finalization of
the capture)." the capture)."
(plist-get org-capture-plist :org-roam)) (plist-get org-capture-plist :org-roam))
(cl-defun org-roam-capture-get-templates (&key (kind 'normal)
(action 'capture)
(templates org-roam-capture-templates))
"Return list of narrowed down capture TEMPLATES to a suitable KIND and ACTION."
(cl-loop for templ in templates
for templ-kind = (or (plist-get templ :kind) 'normal)
for templ-action = (or (plist-get templ :action) 'capture)
when (and (org-roam--valid-option-p kind templ-kind)
(org-roam--valid-option-p action templ-action))
if (org-roam-capture--dull-template-p templ) collect it
else collect templ))
(defun org-roam-capture--dull-template-p (template)
"Return prefix key declaration of TEMPLATE if it's a prefix key based one.
Unlike `org-capture', in Org-roam such templates can also
optionally specify dedicated `:kind' and `:action' values."
(cl-loop for property in (cddr template) by #'cddr
unless (memq property '(:kind :action))
return nil
finally return (cl-subseq template 0 2)))
(defun org-roam-capture--get (keyword) (defun org-roam-capture--get (keyword)
"Get the value for KEYWORD from the `org-roam-capture-template'." "Get the value for KEYWORD from the `org-roam-capture-template'."
(plist-get (plist-get org-capture-plist :org-roam) keyword)) (plist-get (plist-get org-capture-plist :org-roam) keyword))

View File

@ -202,6 +202,10 @@ nodes." org-id-locations-file)
;;; Obsolete functions ;;; Obsolete functions
(make-obsolete 'org-roam-get-keyword 'org-collect-keywords "org-roam 2.0") (make-obsolete 'org-roam-get-keyword 'org-collect-keywords "org-roam 2.0")
;;; Obsolete variables
(make-obsolete-variable 'org-roam-dailies-capture-templates 'org-roam-capture-templates "org-roam 2.1")
(make-obsolete-variable 'org-roam-capture-ref-templates 'org-roam-capture-templates "org-roam 2.1")
(provide 'org-roam-compat) (provide 'org-roam-compat)
;;; org-roam-compat.el ends here ;;; org-roam-compat.el ends here

View File

@ -105,6 +105,29 @@ If FILE, set `default-directory' to FILE's directory and insert its contents."
(setq-local default-directory (file-name-directory ,file))) (setq-local default-directory (file-name-directory ,file)))
,@body))))) ,@body)))))
;;; Processing options
(defun org-roam--valid-option-p (option choice)
"Return t if OPTION satisfies CHOICE, else nil.
OPTION is a symbol, while CHOICE is either, a symbol or a list of
symbols that can optionally start with `:not' keyword.
If CHOICE is a list that indicates negation, then the function
will return t if OPTION isn't in the list. Otherwise it will
return t is OPTION is present in the CHOICE.
When CHOICE is a symbol, it will behave like `eq', except of
special 'any value, in which case it will always return t,
independently OPTION's value.
CHOICE as a list can too contain 'any, in which case any OPTION
value will be considered as part of the CHOICE, with respect to
negation."
(let* ((choices (-list choice))
(intersection (-intersection (list option 'any) choices))
(negation (eq :not (car choices))))
(or (and intersection (not negation))
(and (not intersection) negation))))
;;; Formatting ;;; Formatting
(defun org-roam-format-template (template replacer) (defun org-roam-format-template (template replacer)
"Format TEMPLATE with the function REPLACER. "Format TEMPLATE with the function REPLACER.