;;; org-roam-dailies.el --- Daily-notes for Org-roam -*- coding: utf-8; lexical-binding: t; -*- ;;; ;; Copyright © 2020-2025 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: 2.3.1 ;; Package-Requires: ((emacs "26.1") (dash "2.13") (org-roam "2.1")) ;; This file is NOT part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 3, or (at your option) ;; any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs; see the file COPYING. If not, write to the ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, ;; Boston, MA 02110-1301, USA. ;;; Commentary: ;; ;; This extension provides functionality for creating daily-notes, or shortly ;; "dailies". Dailies implemented here as a unique node per unique file, where ;; each file named after certain date and stored in `org-roam-dailies-directory'. ;; ;; One can use dailies for various purposes, e.g. journaling, fleeting notes, ;; scratch notes or whatever else you can think of. ;; ;;; Code: (require 'dash) (require 'org-roam) ;;; Faces (defface org-roam-dailies-calendar-note '((t :inherit (org-link) :underline nil)) "Face for dates with a daily-note in the calendar." :group 'org-roam-faces) ;;; Options (defcustom org-roam-dailies-directory "daily/" "Path to daily-notes. This path is relative to `org-roam-directory'." :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" "default" entry "* %?" :target (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 " :target) (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 (defun org-roam-dailies-capture-today (&optional goto keys) "Create an entry in the daily-note for today. When GOTO is non-nil, go the note without creating an entry. ELisp programs can set KEYS to a string associated with a template. In this case, interactive selection will be bypassed." (interactive "P") (org-roam-dailies--capture (current-time) goto keys)) ;;;###autoload (defun org-roam-dailies-goto-today (&optional keys) "Find the daily-note for today, creating it if necessary. ELisp programs can set KEYS to a string associated with a template. In this case, interactive selection will be bypassed." (interactive) (org-roam-dailies-capture-today t keys)) ;;;; Tomorrow ;;;###autoload (defun org-roam-dailies-capture-tomorrow (n &optional goto keys) "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. ELisp programs can set KEYS to a string associated with a template. In this case, interactive selection will be bypassed." (interactive "p") (org-roam-dailies--capture (time-add (* n 86400) (current-time)) goto keys)) ;;;###autoload (defun org-roam-dailies-goto-tomorrow (n &optional keys) "Find the daily-note for tomorrow, creating it if necessary. With numeric argument N, use the daily-note N days in the future. ELisp programs can set KEYS to a string associated with a template. In this case, interactive selection will be bypassed." (interactive "p") (org-roam-dailies-capture-tomorrow n t keys)) ;;;; Yesterday ;;;###autoload (defun org-roam-dailies-capture-yesterday (n &optional goto keys) "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. ELisp programs can set KEYS to a string associated with a template. In this case, interactive selection will be bypassed." (interactive "p") (org-roam-dailies-capture-tomorrow (- n) goto keys)) ;;;###autoload (defun org-roam-dailies-goto-yesterday (n &optional keys) "Find the daily-note for yesterday, creating it if necessary. With numeric argument N, use the daily-note N days in the future. ELisp programs can set KEYS to a string associated with a template. In this case, interactive selection will be bypassed." (interactive "p") (org-roam-dailies-capture-tomorrow (- n) t keys)) ;;;; Date ;;;###autoload (defun org-roam-dailies-capture-date (&optional goto prefer-future keys) "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. ELisp programs can set KEYS to a string associated with a template. In this case, interactive selection will be bypassed." (interactive "P") (let ((time (let ((org-read-date-prefer-future prefer-future)) (org-read-date nil t nil (if goto "Find daily-note: " "Capture to daily-note: "))))) (org-roam-dailies--capture time goto keys))) ;;;###autoload (defun org-roam-dailies-goto-date (&optional prefer-future keys) "Find the daily-note for a date using the calendar, creating it if necessary. Prefer past dates, unless PREFER-FUTURE is non-nil. ELisp programs can set KEYS to a string associated with a template. In this case, interactive selection will be bypassed." (interactive) (org-roam-dailies-capture-date t prefer-future keys)) ;;;; Navigation (defun org-roam-dailies-goto-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") (unless (org-roam-dailies--daily-note-p) (user-error "Not in a daily-note")) (setq n (or n 1)) (let* ((dailies (org-roam-dailies--list-files)) (position (cl-position-if (lambda (candidate) (string= (buffer-file-name (buffer-base-buffer)) candidate)) dailies)) note) (unless position (user-error "Can't find current note file - have you saved it yet?")) (pcase n ((pred (natnump)) (when (eq position (- (length dailies) 1)) (user-error "Already at newest note"))) ((pred (integerp)) (when (eq position 0) (user-error "Already at oldest note")))) (setq note (nth (+ position n) dailies)) (find-file note) (run-hooks 'org-roam-dailies-find-file-hook))) (defun org-roam-dailies-goto-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-goto-next-note n))) (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 (expand-file-name org-roam-dailies-directory org-roam-directory)) (regexp (rx-to-string `(and "." (or ,@org-roam-file-extensions))))) (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 regexp)) extra-files))) (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 (expand-file-name (or file (buffer-file-name (buffer-base-buffer))))) (directory (expand-file-name org-roam-dailies-directory org-roam-directory))) (setq path (expand-file-name path)) (save-match-data (and (org-roam-file-p path) (org-roam-descendant-of-p path directory))))) ;;;###autoload (defun org-roam-dailies-find-directory () "Find and open `org-roam-dailies-directory'." (interactive) (find-file (expand-file-name org-roam-dailies-directory org-roam-directory))) ;;; Calendar integration (defun org-roam-dailies-calendar--file-to-date (file) "Convert FILE to date. Return (MONTH DAY YEAR) or nil if not an Org time-string." (ignore-errors (cl-destructuring-bind (_ _ _ d m y _ _ _) (org-parse-time-string (file-name-sans-extension (file-name-nondirectory file))) (list m d y)))) (defun org-roam-dailies-calendar-mark-entries () "Mark days in the calendar for which a daily-note is present." (when (file-exists-p (expand-file-name org-roam-dailies-directory org-roam-directory)) (dolist (date (remove nil (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))))) (add-hook 'calendar-today-visible-hook #'org-roam-dailies-calendar-mark-entries) (add-hook 'calendar-today-invisible-hook #'org-roam-dailies-calendar-mark-entries) ;;; Capture implementation (add-to-list 'org-roam-capture--template-keywords :override-default-time) (defun org-roam-dailies--capture (time &optional goto keys) "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. ELisp programs can set KEYS to a string associated with a template. In this case, interactive selection will be bypassed." (let ((org-roam-directory (expand-file-name org-roam-dailies-directory org-roam-directory)) (org-roam-dailies-directory "./")) (org-roam-capture- :goto (when goto '(4)) :keys keys :node (org-roam-node-create) :templates org-roam-dailies-capture-templates :props (list :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) (defun org-roam-dailies--override-capture-time-h () "Override the `:default-time' with the time from `:override-default-time'." (prog1 nil (when (org-roam-capture--get :override-default-time) (org-capture-put :default-time (org-roam-capture--get :override-default-time))))) ;;; 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-goto-today) (define-key org-roam-dailies-map (kbd "y") #'org-roam-dailies-goto-yesterday) (define-key org-roam-dailies-map (kbd "t") #'org-roam-dailies-goto-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-goto-next-note) (define-key org-roam-dailies-map (kbd "b") #'org-roam-dailies-goto-previous-note) (define-key org-roam-dailies-map (kbd "c") #'org-roam-dailies-goto-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) ;;; org-roam-dailies.el ends here