diff --git a/extensions/org-roam-dailies.el b/extensions/org-roam-dailies.el index 686a9fc..4fee037 100644 --- a/extensions/org-roam-dailies.el +++ b/extensions/org-roam-dailies.el @@ -36,6 +36,18 @@ ;; One can use dailies for various purposes, e.g. journaling, fleeting notes, ;; 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: (require 'f) (require 'dash) @@ -49,8 +61,9 @@ ;;; Options (defcustom org-roam-dailies-directory "daily/" - "Path to daily-notes. -This path is relative to `org-roam-directory'." + "Path to daily-notes. This path is relative to `org-roam-directory'. +Daily based capture templates will automatically start from this +path." :group 'org-roam :type 'string) @@ -59,74 +72,6 @@ This path is relative to `org-roam-directory'." :group 'org-roam :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 ;;;; Today ;;;###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))) (org-roam-capture- :goto (when goto '(4)) :node (org-roam-node-create) - :templates org-roam-dailies-capture-templates - :props (list :override-default-time time))) + :props (list :kind 'daily + :override-default-time time))) (when goto (run-hooks 'org-roam-dailies-find-file-hook))) (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) (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 (defvar org-roam-dailies-map (make-sparse-keymap) "Keymap for `org-roam-dailies'.") diff --git a/extensions/org-roam-protocol.el b/extensions/org-roam-protocol.el index e4c8bf8..eae88d7 100644 --- a/extensions/org-roam-protocol.el +++ b/extensions/org-roam-protocol.el @@ -32,12 +32,21 @@ ;; 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 ;; -;; You can find detailed instructions on how to setup the protocol in the -;; documentation for Org-roam. +;; To add new capture templates dedicated for the protocol, specify ":kind +;; 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: (require 'org-protocol) -(require 'ol) ;; for org-link-decode +(require 'ol) ; to use `org-link-decode' (require 'org-roam) ;;; Options @@ -46,73 +55,12 @@ :type 'boolean :group 'org-roam) -(defcustom org-roam-capture-ref-templates - '(("r" "ref" plain "%?" - :if-new (file+head "${slug}.org" - "#+title: ${title}") - :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)))))))) +;;; Protocols +(mapc (lambda (spec) (cl-pushnew spec org-protocol-protocol-alist :test #'equal)) + '(("org-roam-ref" :protocol "roam-ref" :function org-roam-protocol-open-ref) + ("org-roam-node" :protocol "roam-node" :function org-roam-protocol-open-node))) -;;; Handlers +;;;; roam-ref (defun org-roam-protocol-open-ref (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)) :info (list :ref (plist-get info :ref) :body (plist-get info :body)) - :templates org-roam-capture-ref-templates) + :props '(:kind protocol)) 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) (defun org-roam-protocol--try-capture-to-ref-h () "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))) (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) diff --git a/org-roam-capture.el b/org-roam-capture.el index 4ed4a37..7df66f3 100644 --- a/org-roam-capture.el +++ b/org-roam-capture.el @@ -38,6 +38,33 @@ (defvar org-end-time-was-given) ;;; 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 '(("d" "default" plain "%?" :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.") (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.") +(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 ;;;###autoload (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. PROPS is a plist containing additional Org-roam properties for each template. 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 (mapcar (lambda (template) (org-roam-capture--convert-template template props)) - (or templates org-roam-capture-templates))) + templates)) (org-roam-capture--node node) (org-roam-capture--info info)) (when (and (not keys) @@ -429,6 +472,27 @@ valid for the capture (i.e. initialization, and finalization of the capture)." (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) "Get the value for KEYWORD from the `org-roam-capture-template'." (plist-get (plist-get org-capture-plist :org-roam) keyword)) diff --git a/org-roam-compat.el b/org-roam-compat.el index 76ab705..13598de 100644 --- a/org-roam-compat.el +++ b/org-roam-compat.el @@ -202,6 +202,10 @@ nodes." org-id-locations-file) ;;; Obsolete functions (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) ;;; org-roam-compat.el ends here diff --git a/org-roam-utils.el b/org-roam-utils.el index ae6cc89..4b74f05 100644 --- a/org-roam-utils.el +++ b/org-roam-utils.el @@ -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))) ,@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 (defun org-roam-format-template (template replacer) "Format TEMPLATE with the function REPLACER.