From 4f6eb285bf6abd31c6e96a2e723d61e0b597f4cb Mon Sep 17 00:00:00 2001 From: Leo Vivier Date: Tue, 10 Nov 2020 14:31:38 +0100 Subject: [PATCH] (feat): Overhaul org-roam-dailies (#978) Implement ideas from `org-journal` to `org-roam-dailies`. Update the doc. --- doc/org-roam.org | 142 ++++++++++++++-- doc/org-roam.texi | 186 +++++++++++++++++++-- org-roam-capture.el | 154 ++++++++++++----- org-roam-dailies.el | 392 +++++++++++++++++++++++++++++++++++--------- org-roam-faces.el | 5 + org-roam.el | 15 +- 6 files changed, 741 insertions(+), 153 deletions(-) diff --git a/doc/org-roam.org b/doc/org-roam.org index 144fa55..7654840 100644 --- a/doc/org-roam.org +++ b/doc/org-roam.org @@ -123,7 +123,7 @@ A slip-box requires a method of quickly capturing ideas. These are called *fleeting notes*: they are simple reminders of information or ideas that will need to be processed later on, or trashed. This is typically accomplished using ~org-capture~ (see info:org#capture), or using Org-roam's daily notes -functionality (see [[*Daily Notes][Daily Notes]]). This provides a central inbox for collecting +functionality (see [[*Daily-notes][Daily-notes]]). This provides a central inbox for collecting thoughts, to be processed later into permanent notes. Permanent notes are further split into two categories: *literature notes* and @@ -993,11 +993,13 @@ This protocol finds or creates a new note with a given ~roam_key~ (see [[*Anatom To use this, create the following [[https://en.wikipedia.org/wiki/Bookmarklet][bookmarklet]] in your browser: #+BEGIN_SRC javascript -javascript:location.href = -'org-protocol://roam-ref?template=r&ref=' -+ encodeURIComponent(location.href) -+ '&title=' -+ encodeURIComponent(document.title) + javascript:location.href = + 'org-protocol://roam-ref?template=r&ref=' + + encodeURIComponent(location.href) + + '&title=' + + encodeURIComponent(document.title) + + '&body=' + + encodeURIComponent(window.getSelection()) #+END_SRC or as a keybinding in ~qutebrowser~ in , using the ~config.py~ file (see @@ -1011,7 +1013,122 @@ where ~template~ is the template key for a template in ~org-roam-capture-ref-templates~ (see [[*The Templating System][The Templating System]]). These templates should contain a ~#+roam_key: ${ref}~ in it. -* TODO Daily Notes +* Daily-notes + +Org-roam provides journaling capabilities akin to +[[#org-journal][Org-journal]] with ~org-roam-dailies~. + +** Configuration + +For ~org-roam-dailies~ to work, you need to define two variables: + +- Variable: ~org-roam-dailies-directory~ + + Path to daily-notes. + +- Variable: ~org-roam-dailies-capture-templates~ + + Capture templates for daily-notes in Org-roam. + +Here is a sane default configuration: + +#+begin_src emacs-lisp + (setq org-roam-dailies-directory "daily/") + + (setq org-roam-dailies-capture-templates + '(("d" "default" entry + #'org-roam-capture--get-point + "* %?" + :file-name "daily/%<%Y-%m-%d>" + :head "#+title: %<%Y-%m-%d>\n\n"))) +#+end_src + +Make sure that ~org-roam-dailies-directory~ appears in ~:file-name~ for your +notes to be recognized as daily-notes. You can have different templates +placing their notes in different directories, but the one in +~org-roam-dailies-directory~ will be considered as the main one in commands. + +See [[*The Templating System][The Templating System]] for creating new +templates. ~org-roam-dailies~ provides an extra ~:olp~ option which allows +specifying the outline-path to a heading: + +#+begin_src emacs-lisp + (setq org-roam-dailies-capture-templates + '(("l" "lab" entry + #'org-roam-capture--get-point + "* %?" + :file-name "daily/%<%Y-%m-%d>" + :head "#+title: %<%Y-%m-%d>\n\n* Lab notes\n* Journal" + :olp ("Journal")) + + ("j" "journal" entry + #'org-roam-capture--get-point + "* %?" + :file-name "daily/%<%Y-%m-%d>" + :head "#+title: %<%Y-%m-%d>\n\n* Lab notes\n* Journal" + :olp ("Lab notes")))) +#+end_src + +The template ~l~ will put its notes under the heading ‘Lab notes’, and the +template ~j~ will put its notes under the heading ‘Journal’. When you use +~:olp~, make sure that the headings are present in ~:head~. + +** Capturing and finding daily-notes + +- Function: ~org-roam-dailies-capture-today~ &optional goto + + Create an entry in the daily note for today. + + When ~goto~ is non-nil, go the note without creating an entry. + +- Function: ~org-roam-dailies-find-today~ + + Find the daily note for today, creating it if necessary. + +There are variants of those commands for ~-yesterday~ and ~-tomorrow~: + +- Function: ~org-roam-dailies-capture-yesterday~ n &optional goto + + Create an entry in the daily note for yesteday. + + With numeric argument ~n~, use the daily note ~n~ days in the past. + +- Function: ~org-roam-dailies-find-yesterday~ + + With numeric argument N, use the daily-note N days in the future. + +There are also commands which allow you to use Emacs’s ~calendar~ to find the date + +- Function: ~org-roam-dailies-capture-date~ + + Create an entry in the daily note for a date using the calendar. + + Prefer past dates, unless ~prefer-future~ is non-nil. + + With a 'C-u' prefix or when ~goto~ is non-nil, go the note without + creating an entry. + +- Function: ~org-roam-dailies-find-date~ + + Find the daily note for a date using the calendar, creating it if necessary. + + Prefer past dates, unless ~prefer-future~ is non-nil. + +** Navigation + +You can navigate between daily-notes: + +- Function: ~org-roam-dailies-find-directory~ + + Find and open ~org-roam-dailies-directory~. + +- Function: ~org-roam-dailies-find-previous-note~ + + When in an daily-note, find the previous one. + +- Function: ~org-roam-dailies-find-next-note~ + + When in an daily-note, find the next one. * Diagnosing and Repairing Files @@ -1198,10 +1315,11 @@ that uses an external search engine and indexer. :CUSTOM_ID: org-journal :END: -[[https://github.com/bastibe/org-journal][Org-journal]] is a more -powerful alternative to the simple function ~org-roam-dailies-today~. It -provides better journaling capabilities, and a nice calendar interface -to see all dated entries. +[[https://github.com/bastibe/org-journal][Org-journal]] provides journaling +capabilities to Org-mode. A lot of its functionalities have been incorporated +into Org-roam under the name ~org-roam-dailies~. It remains a good tool if +you want to isolate your verbose journal entries from the ideas you would +write on a scratchpad. #+BEGIN_SRC emacs-lisp (use-package org-journal @@ -1210,7 +1328,7 @@ to see all dated entries. :custom (org-journal-date-prefix "#+title: ") (org-journal-file-format "%Y-%m-%d.org") - (org-journal-dir "/path/to/org-roam-files/") + (org-journal-dir "/path/to/journal/files/") (org-journal-date-format "%A, %d %B %Y")) #+END_SRC diff --git a/doc/org-roam.texi b/doc/org-roam.texi index 0a4e4cf..10bf1c1 100644 --- a/doc/org-roam.texi +++ b/doc/org-roam.texi @@ -78,7 +78,7 @@ General Public License for more details. * Graphing:: * Org-roam Completion System:: * Roam Protocol:: -* Daily Notes:: +* Daily-notes:: * Diagnosing and Repairing Files:: * Finding Unlinked References:: * Performance Optimization:: @@ -129,6 +129,12 @@ Roam Protocol * The roam-file protocol:: * The roam-ref protocol:: +Daily-notes + +* Configuration:: +* Capturing and finding daily-notes:: +* Navigation:: + Performance Optimization * Profiling Key Operations:: @@ -260,7 +266,7 @@ A slip-box requires a method of quickly capturing ideas. These are called @strong{fleeting notes}: they are simple reminders of information or ideas that will need to be processed later on, or trashed. This is typically accomplished using @code{org-capture} (see @ref{capture,,,org,}), or using Org-roam's daily notes -functionality (see @ref{Daily Notes}). This provides a central inbox for collecting +functionality (see @ref{Daily-notes}). This provides a central inbox for collecting thoughts, to be processed later into permanent notes. Permanent notes are further split into two categories: @strong{literature notes} and @@ -402,7 +408,7 @@ Where @code{/path/to/my/info/files} is the location where you keep info files. T @lisp (require 'info) (add-to-list 'Info-default-directory-list - "/path/to/my/info/files") + "/path/to/my/info/files") @end lisp You can also use one of the default locations, such as: @@ -1329,10 +1335,12 @@ To use this, create the following @uref{https://en.wikipedia.org/wiki/Bookmarkle @example javascript:location.href = -'org-protocol://roam-ref?template=r&ref=' -+ encodeURIComponent(location.href) -+ '&title=' -+ encodeURIComponent(document.title) + 'org-protocol://roam-ref?template=r&ref=' + + encodeURIComponent(location.href) + + '&title=' + + encodeURIComponent(document.title) + + '&body=' + + encodeURIComponent(window.getSelection()) @end example or as a keybinding in @code{qutebrowser} in , using the @code{config.py} file (see @@ -1346,8 +1354,153 @@ where @code{template} is the template key for a template in @code{org-roam-capture-ref-templates} (see @ref{The Templating System}). These templates should contain a @code{#+roam_key: $@{ref@}} in it. -@node Daily Notes -@chapter @strong{TODO} Daily Notes +@node Daily-notes +@chapter Daily-notes + +Org-roam provides journaling capabilities akin to +@ref{Org-journal} with @code{org-roam-dailies}. + +@menu +* Configuration:: +* Capturing and finding daily-notes:: +* Navigation:: +@end menu + +@node Configuration +@section Configuration + +For @code{org-roam-dailies} to work, you need to define two variables: + +@itemize +@item +Variable: @code{org-roam-dailies-directory} + +Path to daily-notes. + +@item +Variable: @code{org-roam-dailies-capture-templates} + +Capture templates for daily-notes in Org-roam. +@end itemize + +Here is a sane default configuration: + +@lisp +(setq org-roam-dailies-directory "daily/") + +(setq org-roam-dailies-capture-templates + '(("d" "default" entry + #'org-roam-capture--get-point + "* %?" + :file-name "daily/%<%Y-%m-%d>" + :head "#+title: %<%Y-%m-%d>\n\n"))) +@end lisp + +Make sure that @code{org-roam-dailies-directory} appears in @code{:file-name} for your +notes to be recognized as daily-notes. You can have different templates +placing their notes in different directories, but the one in +@code{org-roam-dailies-directory} will be considered as the main one in commands. + +See @ref{The Templating System} for creating new +templates. @code{org-roam-dailies} provides an extra @code{:olp} option which allows +specifying the outline-path to a heading: + +@lisp +(setq org-roam-dailies-capture-templates + '(("l" "lab" entry + #'org-roam-capture--get-point + "* %?" + :file-name "daily/%<%Y-%m-%d>" + :head "#+title: %<%Y-%m-%d>\n\n* Lab notes\n* Journal" + :olp ("Journal")) + + ("j" "journal" entry + #'org-roam-capture--get-point + "* %?" + :file-name "daily/%<%Y-%m-%d>" + :head "#+title: %<%Y-%m-%d>\n\n* Lab notes\n* Journal" + :olp ("Lab notes")))) +@end lisp + +The template @code{l} will put its notes under the heading ‘Lab notes’, and the +template @code{j} will put its notes under the heading ‘Journal’. When you use +@code{:olp}, make sure that the headings are present in @code{:head}. + +@node Capturing and finding daily-notes +@section Capturing and finding daily-notes + +@itemize +@item +Function: @code{org-roam-dailies-capture-today} &optional goto + +Create an entry in the daily note for today. + +When @code{goto} is non-nil, go the note without creating an entry. + +@item +Function: @code{org-roam-dailies-find-today} + +Find the daily note for today, creating it if necessary. +@end itemize + +There are variants of those commands for @code{-yesterday} and @code{-tomorrow}: + +@itemize +@item +Function: @code{org-roam-dailies-capture-yesterday} n &optional goto + +Create an entry in the daily note for yesteday. + +With numeric argument @code{n}, use the daily note @code{n} days in the past. + +@item +Function: @code{org-roam-dailies-find-yesterday} + +With numeric argument N, use the daily-note N days in the future. +@end itemize + +There are also commands which allow you to use Emacs’s @code{calendar} to find the date + +@itemize +@item +Function: @code{org-roam-dailies-capture-date} + +Create an entry in the daily note for a date using the calendar. + +Prefer past dates, unless @code{prefer-future} is non-nil. + +With a 'C-u' prefix or when @code{goto} is non-nil, go the note without +creating an entry. + +@item +Function: @code{org-roam-dailies-find-date} + +Find the daily note for a date using the calendar, creating it if necessary. + +Prefer past dates, unless @code{prefer-future} is non-nil. +@end itemize + +@node Navigation +@section Navigation + +You can navigate between daily-notes: + +@itemize +@item +Function: @code{org-roam-dailies-find-directory} + +Find and open @code{org-roam-dailies-directory}. + +@item +Function: @code{org-roam-dailies-find-previous-note} + +When in an daily-note, find the previous one. + +@item +Function: @code{org-roam-dailies-find-next-note} + +When in an daily-note, find the next one. +@end itemize @node Diagnosing and Repairing Files @chapter Diagnosing and Repairing Files @@ -1567,10 +1720,11 @@ that uses an external search engine and indexer. @node Org-journal @subsection Org-journal -@uref{https://github.com/bastibe/org-journal, Org-journal} is a more -powerful alternative to the simple function @code{org-roam-dailies-today}. It -provides better journaling capabilities, and a nice calendar interface -to see all dated entries. +@uref{https://github.com/bastibe/org-journal, Org-journal} provides journaling +capabilities to Org-mode. A lot of its functionalities have been incorporated +into Org-roam under the name @code{org-roam-dailies}. It remains a good tool if +you want to isolate your verbose journal entries from the ideas you would +write on a scratchpad. @lisp (use-package org-journal @@ -1579,7 +1733,7 @@ to see all dated entries. :custom (org-journal-date-prefix "#+title: ") (org-journal-file-format "%Y-%m-%d.org") - (org-journal-dir "/path/to/org-roam-files/") + (org-journal-dir "/path/to/journal/files/") (org-journal-date-format "%A, %d %B %Y")) @end lisp @@ -1708,5 +1862,5 @@ call @code{ivy-immediate-done}, typically bound to @code{C-M-j}. Alternatively, Org-roam should provide a selectable ``[?] bar'' candidate at the top of the candidate list. @end table -Emacs 28.0.50 (Org mode 9.4) -@bye +Emacs 27.1.50 (Org mode 9.4) +@bye \ No newline at end of file diff --git a/org-roam-capture.el b/org-roam-capture.el index b2cb550..03781ee 100644 --- a/org-roam-capture.el +++ b/org-roam-capture.el @@ -41,6 +41,9 @@ (defvar org-roam-directory) (defvar org-roam-mode) (defvar org-roam-title-to-slug-function) +(defvar org-roam-dailies-directory) +(defvar org-roam-dailies-capture--file-name-default) +(defvar org-roam-dailies-capture--header-default) (declare-function org-roam--get-title-path-completions "org-roam") (declare-function org-roam--get-ref-path-completions "org-roam") (declare-function org-roam--file-path-from-id "org-roam") @@ -49,6 +52,12 @@ (declare-function org-roam-mode "org-roam") (declare-function org-roam-completion--completing-read "org-roam-completion") +(defvar org-roam-capture--file-name-default "%<%Y%m%d%H%M%S>-${slug}" + "The default file-name format for Org-roam templates.") + +(defvar org-roam-capture--header-default "#+title: ${title}\n" + "The default header for Org-roam templates.") + (defvar org-roam-capture--file-path nil "The file path for the Org-roam capture. This variable is set during the Org-roam capture process.") @@ -74,14 +83,14 @@ note with the given `ref'.") (defvar org-roam-capture-additional-template-props nil "Additional props to be added to the Org-roam template.") -(defconst org-roam-capture--template-keywords '(:file-name :head) +(defconst org-roam-capture--template-keywords '(:file-name :head :olp) "Keywords used in `org-roam-capture-templates' specific to Org-roam.") (defcustom org-roam-capture-templates - '(("d" "default" plain (function org-roam-capture--get-point) + `(("d" "default" plain (function org-roam-capture--get-point) "%?" - :file-name "%<%Y%m%d%H%M%S>-${slug}" - :head "#+title: ${title}\n" + :file-name ,org-roam-capture--file-name-default + :head ,org-roam-capture--header-default :unnarrowed t)) "Capture templates for Org-roam. The Org-roam capture-templates builds on the default behaviours of @@ -217,12 +226,18 @@ Template string :\n%v") ((const :format "%v " :table-line-pos) (string)) ((const :format "%v " :kill-buffer) (const t)))))) +(defvar org-roam-capture-ref--file-name-default "${slug}" + "The default file-name for `org-roam-capture-ref-templates'.") + +(defvar org-roam-capture-ref--header-default "#+title: ${title}\n#+roam_key: ${ref}" + "The default header for `org-roam-capture-ref-templates'.") + (defcustom org-roam-capture-ref-templates - '(("r" "ref" plain (function org-roam-capture--get-point) - "%?" - :file-name "${slug}" - :head "#+title: ${title}\n#+roam_key: ${ref}\n" - :unnarrowed t)) + `(("r" "ref" plain (function org-roam-capture--get-point) + "%?" + :file-name ,org-roam-capture-ref--file-name-default + :head ,org-roam-capture--header-default + :unnarrowed t)) "The Org-roam templates used during a capture from the roam-ref protocol. Details on how to specify for the template is given in `org-roam-capture-templates'." :group 'org-roam @@ -387,6 +402,18 @@ The file is saved if the original value of :no-save is not t and (with-current-buffer (org-capture-get :buffer) (save-buffer))))) +(defun org-roam-capture--expand-file-name (file-name) + "Expand FILE-NAME for `org-roam-capture'. + +Prepend `org-roam-dailies-directory' to the value of `:file' when +capturing a daily-note." + (when file-name + (pcase org-roam-capture--context + ('dailies + (concat org-roam-dailies-directory file-name)) + (_ + file-name)))) + (defun org-roam-capture--new-file () "Return the path to the new file during an Org-roam capture. @@ -408,26 +435,52 @@ aborted, we do the following: 3. Add a function on `org-capture-before-finalize-hook' that saves the file if the original value of :no-save is not t and `org-note-abort' is not t." - (let* ((name-templ (org-roam-capture--get :file-name)) + (let* ((name-templ (or (org-roam-capture--get :file-name) + (pcase org-roam-capture--context + ('dailies + (or (-some-> org-roam-dailies-directory + (file-name-as-directory) + (concat org-roam-dailies-capture--file-name-default)) + (user-error "`org-roam-dailies-directory' cannot be nil"))) + ('ref + org-roam-capture-ref--file-name-default) + (_ + org-roam-capture--file-name-default)))) (new-id (s-trim (org-roam-capture--fill-template name-templ))) (file-path (org-roam--file-path-from-id new-id)) - (roam-head (org-roam-capture--get :head)) + (roam-head (or (org-roam-capture--get :head) + (pcase org-roam-capture--context + ('dailies + org-roam-dailies-capture--header-default) + ('ref + org-roam-capture-ref--header-default) + (_ + org-roam-capture--header-default)))) (org-template (org-capture-get :template)) (roam-template (concat roam-head org-template))) - (unless (file-exists-p file-path) + (unless (or (file-exists-p file-path) + (cl-some (lambda (buffer) + (string= (buffer-file-name buffer) + file-path)) + (buffer-list))) (make-directory (file-name-directory file-path) t) (org-roam-capture--put :orig-no-save (org-capture-get :no-save) :new-file t) - (org-capture-put :template - ;; Fixes org-capture-place-plain-text throwing 'invalid search bound' - ;; when both :unnarowed t and "%?" is missing from the template string; - ;; may become unnecessary when the upstream bug is fixed - (if (s-contains-p "%?" roam-template) - roam-template - (concat roam-template "%?")) - :type 'plain - :no-save t)) + (pcase org-roam-capture--context + ('dailies + ;; Populate the header of the daily file before capture to prevent it + ;; from appearing in the buffer-restriction + (save-window-excursion + (find-file file-path) + (insert (substring (org-capture-fill-template (concat roam-head "*")) + 0 -2)) + (set-buffer-modified-p nil)) + (org-capture-put :template org-template)) + (_ + (org-capture-put :template roam-template + :type 'plain))) + (org-capture-put :no-save t)) file-path)) (defun org-roam-capture--get-point () @@ -446,32 +499,53 @@ If there is no file with that ref, a file with that ref is created. This function is used solely in Org-roam's capture templates: see `org-roam-capture-templates'." - (let ((file-path (pcase org-roam-capture--context - ('capture - (or (cdr (assoc 'file org-roam-capture--info)) - (org-roam-capture--new-file))) - ('title - (org-roam-capture--new-file)) - ('dailies - (org-capture-put :default-time (cdr (assoc 'time org-roam-capture--info))) - (org-roam-capture--new-file)) - ('ref - (let ((completions (org-roam--get-ref-path-completions)) - (ref (cdr (assoc 'ref org-roam-capture--info)))) - (if-let ((pl (cdr (assoc ref completions)))) - (plist-get pl :path) - (org-roam-capture--new-file)))) - (_ (error "Invalid org-roam-capture-context"))))) + (let* ((file-path (pcase org-roam-capture--context + ('capture + (or (cdr (assoc 'file org-roam-capture--info)) + (org-roam-capture--new-file))) + ('title + (org-roam-capture--new-file)) + ('dailies + (org-capture-put :default-time (cdr (assoc 'time org-roam-capture--info))) + (org-roam-capture--new-file)) + ('ref + (let ((completions (org-roam--get-ref-path-completions)) + (ref (cdr (assoc 'ref org-roam-capture--info)))) + (if-let ((pl (cdr (assoc ref completions)))) + (plist-get pl :path) + (org-roam-capture--new-file)))) + (_ (error "Invalid org-roam-capture-context"))))) (org-capture-put :template - (org-roam-capture--fill-template (org-capture-get :template))) - (org-roam-capture--put :file-path file-path) + (org-roam-capture--fill-template (org-capture-get :template))) + (org-roam-capture--put :file-path file-path + :finalize (or (org-capture-get :finalize) + (org-roam-capture--get :finalize))) (while org-roam-capture-additional-template-props (let ((prop (pop org-roam-capture-additional-template-props)) (val (pop org-roam-capture-additional-template-props))) (org-roam-capture--put prop val))) (set-buffer (org-capture-target-buffer file-path)) (widen) - (goto-char (point-max)))) + (if-let* ((olp (when (eq org-roam-capture--context 'dailies) + (--> (org-roam-capture--get :olp) + (pcase it + ((pred stringp) + (list it)) + ((pred listp) + it) + (wrong-type + (signal 'wrong-type-argument + `((stringp listp) + ,wrong-type)))))))) + (condition-case err + (when-let ((marker (org-find-olp `(,file-path ,@olp)))) + (goto-char marker) + (set-marker marker nil)) + (error + (when (org-roam-capture--get :new-file) + (kill-buffer)) + (signal (car err) (cdr err)))) + (goto-char (point-min))))) (defun org-roam-capture--convert-template (template) "Convert TEMPLATE from Org-roam syntax to `org-capture-templates' syntax." diff --git a/org-roam-dailies.el b/org-roam-dailies.el index 52ebd91..7ff66bf 100644 --- a/org-roam-dailies.el +++ b/org-roam-dailies.el @@ -1,8 +1,10 @@ -;;; org-roam-dailies.el --- Daily notes for Org-roam -*- coding: utf-8; lexical-binding: t; -*- +;;; org-roam-dailies.el --- Daily-notes for Org-roam -*- coding: utf-8; lexical-binding: t; -*- ;;; ;; Copyright © 2020 Jethro Kuan +;; Copyright © 2020 Leo Vivier ;; Author: Jethro Kuan +;; Leo Vivier ;; URL: https://github.com/org-roam/org-roam ;; Keywords: org-mode, roam, convenience ;; Version: 1.2.2 @@ -27,7 +29,7 @@ ;;; Commentary: ;; -;; This library provides functionality for creating daily notes. This is a +;; This library provides functionality for creating daily-notes. This is a ;; concept borrowed from Roam Research. ;; ;;; Code: @@ -35,102 +37,336 @@ (require 'org-capture) (require 'org-roam-capture) (require 'org-roam-macs) +(require 'f) + +;;;; Declarations +(defvar org-roam-mode) +(defvar org-roam-directory) +(declare-function org-roam--org-file-p "org-roam") +(declare-function org-roam--file-path-from-id "org-roam") +(declare-function org-roam--find-file "org-roam") +(declare-function org-roam-mode "org-roam") + +(defvar org-roam-dailies-capture--file-name-default "%<%Y-%m-%d>" + "The default file-name for `org-roam-dailies-capture-templates'.") + +(defvar org-roam-dailies-capture--header-default "#+title: %<%Y-%m-%d>\n" + "The default header for `org-roam-dailies-capture-templates'.") + +;;;; Customizable variables +(defcustom org-roam-dailies-directory "daily/" + "Path to daily-notes." + :group 'org-roam + :type 'string) + +(defcustom org-roam-dailies-find-file-hook nil + "Hook that is run right after navigating to a daily-note." + :group 'org-roam + :type 'hook) (defcustom org-roam-dailies-capture-templates - '(("d" "daily" plain (function org-roam-capture--get-point) - "" - :immediate-finish t - :file-name "%<%Y-%m-%d>" - :head "#+title: %<%Y-%m-%d>")) - "Capture templates for daily notes in Org-roam." + '(("d" "default" entry (function org-roam-capture--get-point) + "* %?" + :file-name "daily/%<%Y-%m-%d>" + :head "#+title: %<%Y-%m-%d>\n")) + "Capture templates for daily-notes in Org-roam." :group 'org-roam ;; Adapted from `org-capture-templates' :type '(repeat - (choice :value ("d" "daily" plain (function org-roam-capture--get-point) - "" - :immediate-finish t - :file-name "%<%Y-%m-%d>" - :head "#+title: %<%Y-%m-%d>") - (list :tag "Multikey description" - (string :tag "Keys ") - (string :tag "Description")) - (list :tag "Template entry" - (string :tag "Keys ") - (string :tag "Description ") - (const :format "" plain) - (const :format "" (function org-roam-capture--get-point)) - (choice :tag "Template " - (string :tag "String" - :format "String:\n \ + (choice :value ("d" "default" plain (function org-roam-capture--get-point) + "%?" + :file-name "daily/%<%Y-%m-%d>" + :head "#+title: %<%Y-%m-%d>\n" + :unnarrowed t) + (list :tag "Multikey description" + (string :tag "Keys ") + (string :tag "Description")) + (list :tag "Template entry" + (string :tag "Keys ") + (string :tag "Description ") + (choice :tag "Type " + (const :tag "Plain" plain) + (const :tag "Entry (for creating headlines)" entry)) + (const :format "" #'org-roam-capture--get-point) + (choice :tag "Template " + (string :tag "String" + :format "String:\n \ Template string :\n%v") - (list :tag "File" - (const :format "" file) - (file :tag "Template file ")) - (list :tag "Function" - (const :format "" function) - (function :tag "Template function "))) - (const :format "" :immediate-finish) (const :format "" t) - (const :format "File name format :" :file-name) - (string :format " %v" :value "#+title: ${title}\n") - (const :format "Header format :" :head) - (string :format "\n%v" :value "%<%Y%m%d%H%M%S>-${slug}") - (plist :inline t - :tag "Options" - ;; Give the most common options as checkboxes - :options - (((const :format "%v " :prepend) (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 " :table-line-pos) (string)) - ((const :format "%v " :kill-buffer) (const t)) - ((const :format "%v " :unnarrowed) (const t)))))))) + (list :tag "File" + (const :format "" file) + (file :tag "Template file ")) + (list :tag "Function" + (const :format "" function) + (function :tag "Template function "))) + (const :format "File name format :" :file-name) + (string :format " %v" :value "daily/%<%Y-%m-%d>") + (const :format "Header format :" :head) + (string :format " %v" :value "#+title: ${title}\n") + (plist :inline t + :tag "Options" + ;; Give the most common options as checkboxes + :options + (((const :tag "Outline path" :olp) + (repeat :tag "Headings" + (string :tag "Heading"))) + ((const :format "%v " :unnarrowed) (const t)) + ((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 " :table-line-pos) (string)) + ((const :format "%v " :kill-buffer) (const t)))))))) -;; Declarations -(defvar org-roam-mode) -(declare-function org-roam--file-path-from-id "org-roam") -(declare-function org-roam-mode "org-roam") +;;;; Utilities +(defun org-roam-dailies-directory--get-absolute-path () + "Get absolute path to `org-roam-dailies-directory'." + (-> (concat + (file-name-as-directory org-roam-directory) + org-roam-dailies-directory) + (file-truename))) -(defun org-roam-dailies--file-for-time (time) - "Create and find file for TIME." - (let ((org-roam-capture-templates org-roam-dailies-capture-templates) +(defun org-roam-dailies-find-directory () + "Find and open `org-roam-dailies-directory'." + (interactive) + (org-roam--find-file (org-roam-dailies-directory--get-absolute-path))) + +(defun org-roam-dailies--daily-note-p (&optional file) + "Return t if FILE is an Org-roam daily-note, nil otherwise. + +If FILE is not specified, use the current buffer's file-path." + (when-let ((path (or file + (-> (buffer-base-buffer) + (buffer-file-name)))) + (directory (org-roam-dailies-directory--get-absolute-path))) + (setq path (file-truename path)) + (save-match-data + (and + (org-roam--org-file-p path) + (f-descendant-of-p path directory))))) + +(defun org-roam-dailies--capture (time &optional goto) + "Capture an entry in a daily-note for TIME, creating it if necessary. + +When GOTO is non-nil, go the note without creating an entry." + (unless org-roam-mode (org-roam-mode)) + (let ((org-roam-capture-templates (--> org-roam-dailies-capture-templates + (if goto (list (car it)) it))) (org-roam-capture--info (list (cons 'time time))) (org-roam-capture--context 'dailies)) (setq org-roam-capture-additional-template-props (list :finalize 'find-file)) - (org-roam-capture--capture))) + (org-roam-capture--capture (when goto '(4))))) -(defun org-roam-dailies-today () - "Create and find the daily note for today." +;;;; Commands +;;; Today +(defun org-roam-dailies-capture-today (&optional goto) + "Create an entry in the daily-note for today. + +When GOTO is non-nil, go the note without creating an entry." + (interactive "P") + (org-roam-dailies--capture (current-time) goto) + (when goto + (run-hooks 'org-roam-dailies-find-file-hook) + (message "Showing daily-note for today"))) + +(defun org-roam-dailies-find-today () + "Find the daily-note for today, creating it if necessary." (interactive) - (unless org-roam-mode (org-roam-mode)) - (org-roam-dailies--file-for-time (current-time))) + (org-roam-dailies-capture-today t)) -(defun org-roam-dailies-tomorrow (n) - "Create and find the daily note for tomorrow. -With numeric argument N, use N days in the future." +;;; Tomorrow +(defun org-roam-dailies-capture-tomorrow (n &optional goto) + "Create an entry in the daily-note for tomorrow. + +With numeric argument N, use the daily-note N days in the future. + +With a `C-u' prefix or when GOTO is non-nil, go the note without +creating an entry." (interactive "p") - (unless org-roam-mode (org-roam-mode)) - (org-roam-dailies--file-for-time (time-add (* n 86400) (current-time)))) + (org-roam-dailies--capture (time-add (* n 86400) (current-time)) goto)) -(defun org-roam-dailies-yesterday (n) - "Create and find the file for yesterday. -With numeric argument N, use N days in the past." +(defun org-roam-dailies-find-tomorrow (n) + "Find the daily-note for tomorrow, creating it if necessary. + +With numeric argument N, use the daily-note N days in the +future." (interactive "p") - (unless org-roam-mode (org-roam-mode)) - (org-roam-dailies-tomorrow (- n))) + (org-roam-dailies-capture-tomorrow n t)) -(defun org-roam-dailies-date () - "Create the file for any date using the calendar interface." +;;; Yesterday +(defun org-roam-dailies-capture-yesterday (n &optional goto) + "Create an entry in the daily-note for yesteday. + +With numeric argument N, use the daily-note N days in the past. + +When GOTO is non-nil, go the note without creating an entry." + (interactive "p") + (org-roam-dailies-capture-tomorrow (- n) goto)) + +(defun org-roam-dailies-find-yesterday (n) + "Find the daily-note for yesterday, creating it if necessary. + +With numeric argument N, use the daily-note N days in the +future." + (interactive "p") + (org-roam-dailies-capture-tomorrow (- n) t)) + +;;; Calendar +(defvar org-roam-dailies-calendar-hook (list 'org-roam-dailies-calendar-mark-entries) + "Hooks to run when showing the `org-roam-dailies-calendar'.") + +(defun org-roam-dailies-calendar--install-hook () + "Install Org-roam-dailies hooks to calendar." + (add-hook 'calendar-today-visible-hook #'org-roam-dailies-calendar--run-hook) + (add-hook 'calendar-today-invisible-hook #'org-roam-dailies-calendar--run-hook)) + +(defun org-roam-dailies-calendar--run-hook () + "Run Org-roam-dailies hooks to calendar." + (run-hooks 'org-roam-dailies-calendar-hook) + (remove-hook 'calendar-today-visible-hook #'org-roam-dailies-calendar--run-hook) + (remove-hook 'calendar-today-invisible-hook #'org-roam-dailies-calendar--run-hook)) + +(defun org-roam-dailies-calendar--file-to-date (&optional file) + "Convert FILE to date. + +Return (MONTH DAY YEAR)." + (let ((file (or file + (-> (buffer-base-buffer) + (buffer-file-name))))) + (cl-destructuring-bind (_ _ _ d m y _ _ _) + (-> file + (file-name-nondirectory) + (file-name-sans-extension) + (org-parse-time-string)) + (list m d y)))) + +(defun org-roam-dailies-calendar--date-to-time (date) + "Convert DATE as returned from the calendar (MONTH DAY YEAR) to a time." + (encode-time 0 0 0 (nth 1 date) (nth 0 date) (nth 2 date))) + +(defun org-roam-dailies-calendar-mark-entries () + "Mark days in the calendar for which a daily-note is present." + (when (file-exists-p (org-roam-dailies-directory--get-absolute-path)) + (dolist (date (mapcar #'org-roam-dailies-calendar--file-to-date + (org-roam-dailies--list-files))) + (when (calendar-date-is-visible-p date) + (calendar-mark-visible-date date 'org-roam-dailies-calendar-note))))) + +;;; Date +(defun org-roam-dailies-capture-date (&optional goto prefer-future) + "Create an entry in the daily-note for a date using the calendar. + +Prefer past dates, unless PREFER-FUTURE is non-nil. + +With a `C-u' prefix or when GOTO is non-nil, go the note without +creating an entry." + (interactive "P") + (org-roam-dailies-calendar--install-hook) + (let* ((time-str (let ((org-read-date-prefer-future prefer-future)) + (org-read-date nil nil nil (if goto + "Find daily-note: " + "Capture to daily-note: ")))) + (time (org-read-date nil t time-str))) + (org-roam-dailies--capture time goto) + (when goto + (run-hooks 'org-roam-dailies-find-file-hook) + (message "Showing note for %s" time-str)))) + +(defun org-roam-dailies-find-date (prefer-future) + "Find the daily-note for a date using the calendar, creating it if necessary. + +Prefer past dates, unless PREFER-FUTURE is non-nil." (interactive) - (let ((time (org-read-date nil 'to-time nil "Date: "))) - (org-roam-dailies--file-for-time time))) + (org-roam-dailies-capture-date t prefer-future)) + +;;; Navigation +(defun org-roam-dailies--list-files (&rest extra-files) + "List all files in `org-roam-dailies-directory'. + +EXTRA-FILES can be used to append extra files to the list." + (let ((dir (org-roam-dailies-directory--get-absolute-path))) + (append (--remove (let ((file (file-name-nondirectory it))) + (when (or (auto-save-file-name-p file) + (backup-file-name-p file) + (string-match "^\\." file)) + it)) + (directory-files-recursively dir "")) + extra-files))) + +(defun org-roam-dailies--find-next-note-path (&optional n file) + "Find next daily-note from FILE. + +With numeric argument N, find note N days in the future. If N is +negative, find note N days in the past. + +If FILE is not provided, use the file visited by the current +buffer." + (unless (org-roam-dailies--daily-note-p file) + (user-error "Not in a daily-note")) + (let ((n (or n 1)) + (file (or file + (-> (buffer-base-buffer) + (buffer-file-name))))) + ;; Ensure that the buffer is saved before moving + (save-buffer file) + (let* ((list (org-roam-dailies--list-files)) + (position + (cl-position-if (lambda (candidate) + (string= file candidate)) + list))) + (pcase n + ((pred (natnump)) + (if (eq position (- (length list) 1)) + (user-error "Already at newest note") + (message "Showing next daily-note"))) + ((pred (integerp)) + (if (eq position 0) + (user-error "Already at oldest note") + (message "Showing previous daily-note")))) + (nth (+ position n) list)))) + +(defun org-roam-dailies-find-next-note (&optional n) + "Find next daily-note. + +With numeric argument N, find note N days in the future. If N is +negative, find note N days in the past." + (interactive "p") + (let* ((n (or n 1)) + (next (org-roam-dailies--find-next-note-path n))) + (find-file next) + (run-hooks 'org-roam-dailies-find-file-hook))) + +(defun org-roam-dailies-find-previous-note (&optional n) + "Find previous daily-note. + +With numeric argument N, find note N days in the past. If N is +negative, find note N days in the future." + (interactive "p") + (let ((n (if n (- n) -1))) + (org-roam-dailies-find-next-note n))) + +;;;; Bindings +(defvar org-roam-dailies-map (make-sparse-keymap) + "Keymap for `org-roam-dailies'.") + +(define-prefix-command 'org-roam-dailies-map) + +(define-key org-roam-dailies-map (kbd "d") #'org-roam-dailies-find-today) +(define-key org-roam-dailies-map (kbd "y") #'org-roam-dailies-find-yesterday) +(define-key org-roam-dailies-map (kbd "t") #'org-roam-dailies-find-tomorrow) +(define-key org-roam-dailies-map (kbd "n") #'org-roam-dailies-capture-today) +(define-key org-roam-dailies-map (kbd "f") #'org-roam-dailies-find-next-note) +(define-key org-roam-dailies-map (kbd "b") #'org-roam-dailies-find-previous-note) +(define-key org-roam-dailies-map (kbd "c") #'org-roam-dailies-find-date) +(define-key org-roam-dailies-map (kbd "v") #'org-roam-dailies-capture-date) +(define-key org-roam-dailies-map (kbd ".") #'org-roam-dailies-find-directory) (provide 'org-roam-dailies) diff --git a/org-roam-faces.el b/org-roam-faces.el index b3e408d..1fdfb79 100644 --- a/org-roam-faces.el +++ b/org-roam-faces.el @@ -65,6 +65,11 @@ This face is used on the region target by `org-roam-insertion' during an `org-roam-capture'." :group 'org-roam-faces) +(defface org-roam-dailies-calendar-note + '((t :inherit (org-roam-link) :underline nil)) + "Face for dates with a daily-note in the calendar" + :group 'org-roam-faces) + ;;; _ (provide 'org-roam-faces) diff --git a/org-roam.el b/org-roam.el index 6965218..7fb1da0 100644 --- a/org-roam.el +++ b/org-roam.el @@ -374,13 +374,14 @@ Like `file-name-extension', but does not strip version number." If FILE is not specified, use the current buffer's file-path." (when-let ((path (or file org-roam-file-name - (buffer-file-name)))) - (save-match-data - (and - (org-roam--org-file-p path) - (not (and org-roam-file-exclude-regexp - (string-match-p org-roam-file-exclude-regexp path))) - (f-descendant-of-p path (expand-file-name org-roam-directory)))))) + (-> (buffer-base-buffer) + (buffer-file-name))))) + (save-match-data + (and + (org-roam--org-file-p path) + (not (and org-roam-file-exclude-regexp + (string-match-p org-roam-file-exclude-regexp path))) + (f-descendant-of-p path (expand-file-name org-roam-directory)))))) (defun org-roam--shell-command-files (cmd) "Run CMD in the shell and return a list of files. If no files are found, an empty list is returned."