dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

org-roam-dailies.el (16485B)


      1 ;;; org-roam-dailies.el --- Daily-notes for Org-roam -*- coding: utf-8; lexical-binding: t; -*-
      2 ;;;
      3 ;; Copyright © 2020-2022 Jethro Kuan <jethrokuan95@gmail.com>
      4 ;; Copyright © 2020 Leo Vivier <leo.vivier+dev@gmail.com>
      5 
      6 ;; Author: Jethro Kuan <jethrokuan95@gmail.com>
      7 ;;      Leo Vivier <leo.vivier+dev@gmail.com>
      8 ;; URL: https://github.com/org-roam/org-roam
      9 ;; Keywords: org-mode, roam, convenience
     10 ;; Version: 2.2.2
     11 ;; Package-Requires: ((emacs "26.1") (dash "2.13") (org-roam "2.1"))
     12 
     13 ;; This file is NOT part of GNU Emacs.
     14 
     15 ;; This program is free software; you can redistribute it and/or modify
     16 ;; it under the terms of the GNU General Public License as published by
     17 ;; the Free Software Foundation; either version 3, or (at your option)
     18 ;; any later version.
     19 ;;
     20 ;; This program is distributed in the hope that it will be useful,
     21 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     22 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     23 ;; GNU General Public License for more details.
     24 ;;
     25 ;; You should have received a copy of the GNU General Public License
     26 ;; along with GNU Emacs; see the file COPYING.  If not, write to the
     27 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
     28 ;; Boston, MA 02110-1301, USA.
     29 
     30 ;;; Commentary:
     31 ;;
     32 ;; This extension provides functionality for creating daily-notes, or shortly
     33 ;; "dailies". Dailies implemented here as a unique node per unique file, where
     34 ;; each file named after certain date and stored in `org-roam-dailies-directory'.
     35 ;;
     36 ;; One can use dailies for various purposes, e.g. journaling, fleeting notes,
     37 ;; scratch notes or whatever else you can think of.
     38 ;;
     39 ;;; Code:
     40 (require 'dash)
     41 (require 'org-roam)
     42 
     43 ;;; Faces
     44 (defface org-roam-dailies-calendar-note
     45   '((t :inherit (org-link) :underline nil))
     46   "Face for dates with a daily-note in the calendar."
     47   :group 'org-roam-faces)
     48 
     49 ;;; Options
     50 (defcustom org-roam-dailies-directory "daily/"
     51   "Path to daily-notes.
     52 This path is relative to `org-roam-directory'."
     53   :group 'org-roam
     54   :type 'string)
     55 
     56 (defcustom org-roam-dailies-find-file-hook nil
     57   "Hook that is run right after navigating to a daily-note."
     58   :group 'org-roam
     59   :type 'hook)
     60 
     61 (defcustom org-roam-dailies-capture-templates
     62   `(("d" "default" entry
     63      "* %?"
     64      :target (file+head "%<%Y-%m-%d>.org"
     65                         "#+title: %<%Y-%m-%d>\n")))
     66   "Capture templates for daily-notes in Org-roam.
     67 Note that for daily files to show up in the calendar, they have to be of format
     68 \"org-time-string.org\".
     69 See `org-roam-capture-templates' for the template documentation."
     70   :group 'org-roam
     71   :type '(repeat
     72           (choice (list :tag "Multikey description"
     73                         (string :tag "Keys       ")
     74                         (string :tag "Description"))
     75                   (list :tag "Template entry"
     76                         (string :tag "Keys           ")
     77                         (string :tag "Description    ")
     78                         (choice :tag "Capture Type   " :value entry
     79                                 (const :tag "Org entry" entry)
     80                                 (const :tag "Plain list item" item)
     81                                 (const :tag "Checkbox item" checkitem)
     82                                 (const :tag "Plain text" plain)
     83                                 (const :tag "Table line" table-line))
     84                         (choice :tag "Template       "
     85                                 (string)
     86                                 (list :tag "File"
     87                                       (const :format "" file)
     88                                       (file :tag "Template file"))
     89                                 (list :tag "Function"
     90                                       (const :format "" function)
     91                                       (function :tag "Template function")))
     92                         (plist :inline t
     93                                ;; Give the most common options as checkboxes
     94                                :options (((const :format "%v " :target)
     95                                           (choice :tag "Node location"
     96                                                   (list :tag "File"
     97                                                         (const :format "" file)
     98                                                         (string :tag "  File"))
     99                                                   (list :tag "File & Head Content"
    100                                                         (const :format "" file+head)
    101                                                         (string :tag "  File")
    102                                                         (string :tag "  Head Content"))
    103                                                   (list :tag "File & Outline path"
    104                                                         (const :format "" file+olp)
    105                                                         (string :tag "  File")
    106                                                         (list :tag "Outline path"
    107                                                               (repeat (string :tag "Headline"))))
    108                                                   (list :tag "File & Head Content & Outline path"
    109                                                         (const :format "" file+head+olp)
    110                                                         (string :tag "  File")
    111                                                         (string :tag "  Head Content")
    112                                                         (list :tag "Outline path"
    113                                                               (repeat (string :tag "Headline"))))))
    114                                          ((const :format "%v " :prepend) (const t))
    115                                          ((const :format "%v " :immediate-finish) (const t))
    116                                          ((const :format "%v " :jump-to-captured) (const t))
    117                                          ((const :format "%v " :empty-lines) (const 1))
    118                                          ((const :format "%v " :empty-lines-before) (const 1))
    119                                          ((const :format "%v " :empty-lines-after) (const 1))
    120                                          ((const :format "%v " :clock-in) (const t))
    121                                          ((const :format "%v " :clock-keep) (const t))
    122                                          ((const :format "%v " :clock-resume) (const t))
    123                                          ((const :format "%v " :time-prompt) (const t))
    124                                          ((const :format "%v " :tree-type) (const week))
    125                                          ((const :format "%v " :unnarrowed) (const t))
    126                                          ((const :format "%v " :table-line-pos) (string))
    127                                          ((const :format "%v " :kill-buffer) (const t))))))))
    128 
    129 ;;; Commands
    130 ;;;; Today
    131 ;;;###autoload
    132 (defun org-roam-dailies-capture-today (&optional goto keys)
    133   "Create an entry in the daily-note for today.
    134 When GOTO is non-nil, go the note without creating an entry.
    135 
    136 ELisp programs can set KEYS to a string associated with a template.
    137 In this case, interactive selection will be bypassed."
    138   (interactive "P")
    139   (org-roam-dailies--capture (current-time) goto keys))
    140 
    141 ;;;###autoload
    142 (defun org-roam-dailies-goto-today (&optional keys)
    143   "Find the daily-note for today, creating it if necessary.
    144 
    145 ELisp programs can set KEYS to a string associated with a template.
    146 In this case, interactive selection will be bypassed."
    147   (interactive)
    148   (org-roam-dailies-capture-today t keys))
    149 
    150 ;;;; Tomorrow
    151 ;;;###autoload
    152 (defun org-roam-dailies-capture-tomorrow (n &optional goto keys)
    153   "Create an entry in the daily-note for tomorrow.
    154 
    155 With numeric argument N, use the daily-note N days in the future.
    156 
    157 With a `C-u' prefix or when GOTO is non-nil, go the note without
    158 creating an entry.
    159 
    160 ELisp programs can set KEYS to a string associated with a template.
    161 In this case, interactive selection will be bypassed."
    162   (interactive "p")
    163   (org-roam-dailies--capture (time-add (* n 86400) (current-time)) goto keys))
    164 
    165 ;;;###autoload
    166 (defun org-roam-dailies-goto-tomorrow (n &optional keys)
    167   "Find the daily-note for tomorrow, creating it if necessary.
    168 
    169 With numeric argument N, use the daily-note N days in the
    170 future.
    171 
    172 ELisp programs can set KEYS to a string associated with a template.
    173 In this case, interactive selection will be bypassed."
    174   (interactive "p")
    175   (org-roam-dailies-capture-tomorrow n t keys))
    176 
    177 ;;;; Yesterday
    178 ;;;###autoload
    179 (defun org-roam-dailies-capture-yesterday (n &optional goto keys)
    180   "Create an entry in the daily-note for yesteday.
    181 
    182 With numeric argument N, use the daily-note N days in the past.
    183 
    184 When GOTO is non-nil, go the note without creating an entry.
    185 
    186 ELisp programs can set KEYS to a string associated with a template.
    187 In this case, interactive selection will be bypassed."
    188   (interactive "p")
    189   (org-roam-dailies-capture-tomorrow (- n) goto keys))
    190 
    191 ;;;###autoload
    192 (defun org-roam-dailies-goto-yesterday (n &optional keys)
    193   "Find the daily-note for yesterday, creating it if necessary.
    194 
    195 With numeric argument N, use the daily-note N days in the
    196 future.
    197 
    198 ELisp programs can set KEYS to a string associated with a template.
    199 In this case, interactive selection will be bypassed."
    200   (interactive "p")
    201   (org-roam-dailies-capture-tomorrow (- n) t keys))
    202 
    203 ;;;; Date
    204 ;;;###autoload
    205 (defun org-roam-dailies-capture-date (&optional goto prefer-future keys)
    206   "Create an entry in the daily-note for a date using the calendar.
    207 Prefer past dates, unless PREFER-FUTURE is non-nil.
    208 With a `C-u' prefix or when GOTO is non-nil, go the note without
    209 creating an entry.
    210 
    211 ELisp programs can set KEYS to a string associated with a template.
    212 In this case, interactive selection will be bypassed."
    213   (interactive "P")
    214   (let ((time (let ((org-read-date-prefer-future prefer-future))
    215                 (org-read-date nil t nil (if goto
    216                                              "Find daily-note: "
    217                                            "Capture to daily-note: ")))))
    218     (org-roam-dailies--capture time goto keys)))
    219 
    220 ;;;###autoload
    221 (defun org-roam-dailies-goto-date (&optional prefer-future keys)
    222   "Find the daily-note for a date using the calendar, creating it if necessary.
    223 Prefer past dates, unless PREFER-FUTURE is non-nil.
    224 
    225 ELisp programs can set KEYS to a string associated with a template.
    226 In this case, interactive selection will be bypassed."
    227   (interactive)
    228   (org-roam-dailies-capture-date t prefer-future keys))
    229 
    230 ;;;; Navigation
    231 (defun org-roam-dailies-goto-next-note (&optional n)
    232   "Find next daily-note.
    233 
    234 With numeric argument N, find note N days in the future. If N is
    235 negative, find note N days in the past."
    236   (interactive "p")
    237   (unless (org-roam-dailies--daily-note-p)
    238     (user-error "Not in a daily-note"))
    239   (setq n (or n 1))
    240   (let* ((dailies (org-roam-dailies--list-files))
    241          (position
    242           (cl-position-if (lambda (candidate)
    243                             (string= (buffer-file-name (buffer-base-buffer)) candidate))
    244                           dailies))
    245          note)
    246     (unless position
    247       (user-error "Can't find current note file - have you saved it yet?"))
    248     (pcase n
    249       ((pred (natnump))
    250        (when (eq position (- (length dailies) 1))
    251          (user-error "Already at newest note")))
    252       ((pred (integerp))
    253        (when (eq position 0)
    254          (user-error "Already at oldest note"))))
    255     (setq note (nth (+ position n) dailies))
    256     (find-file note)
    257     (run-hooks 'org-roam-dailies-find-file-hook)))
    258 
    259 (defun org-roam-dailies-goto-previous-note (&optional n)
    260   "Find previous daily-note.
    261 
    262 With numeric argument N, find note N days in the past. If N is
    263 negative, find note N days in the future."
    264   (interactive "p")
    265   (let ((n (if n (- n) -1)))
    266     (org-roam-dailies-goto-next-note n)))
    267 
    268 (defun org-roam-dailies--list-files (&rest extra-files)
    269   "List all files in `org-roam-dailies-directory'.
    270 EXTRA-FILES can be used to append extra files to the list."
    271   (let ((dir (expand-file-name org-roam-dailies-directory org-roam-directory))
    272         (regexp (rx-to-string `(and "." (or ,@org-roam-file-extensions)))))
    273     (append (--remove (let ((file (file-name-nondirectory it)))
    274                         (when (or (auto-save-file-name-p file)
    275                                   (backup-file-name-p file)
    276                                   (string-match "^\\." file))
    277                           it))
    278                       (directory-files-recursively dir regexp))
    279             extra-files)))
    280 
    281 (defun org-roam-dailies--daily-note-p (&optional file)
    282   "Return t if FILE is an Org-roam daily-note, nil otherwise.
    283 If FILE is not specified, use the current buffer's file-path."
    284   (when-let ((path (expand-file-name
    285                     (or file
    286                         (buffer-file-name (buffer-base-buffer)))))
    287              (directory (expand-file-name org-roam-dailies-directory org-roam-directory)))
    288     (setq path (expand-file-name path))
    289     (save-match-data
    290       (and
    291        (org-roam-file-p path)
    292        (org-roam-descendant-of-p path directory)))))
    293 
    294 ;;;###autoload
    295 (defun org-roam-dailies-find-directory ()
    296   "Find and open `org-roam-dailies-directory'."
    297   (interactive)
    298   (find-file (expand-file-name org-roam-dailies-directory org-roam-directory)))
    299 
    300 ;;; Calendar integration
    301 (defun org-roam-dailies-calendar--file-to-date (file)
    302   "Convert FILE to date.
    303 Return (MONTH DAY YEAR) or nil if not an Org time-string."
    304   (ignore-errors
    305     (cl-destructuring-bind (_ _ _ d m y _ _ _)
    306         (org-parse-time-string
    307          (file-name-sans-extension
    308           (file-name-nondirectory file)))
    309       (list m d y))))
    310 
    311 (defun org-roam-dailies-calendar-mark-entries ()
    312   "Mark days in the calendar for which a daily-note is present."
    313   (when (file-exists-p (expand-file-name org-roam-dailies-directory org-roam-directory))
    314     (dolist (date (remove nil
    315                           (mapcar #'org-roam-dailies-calendar--file-to-date
    316                                   (org-roam-dailies--list-files))))
    317       (when (calendar-date-is-visible-p date)
    318         (calendar-mark-visible-date date 'org-roam-dailies-calendar-note)))))
    319 
    320 (add-hook 'calendar-today-visible-hook #'org-roam-dailies-calendar-mark-entries)
    321 (add-hook 'calendar-today-invisible-hook #'org-roam-dailies-calendar-mark-entries)
    322 
    323 ;;; Capture implementation
    324 (add-to-list 'org-roam-capture--template-keywords :override-default-time)
    325 
    326 (defun org-roam-dailies--capture (time &optional goto keys)
    327   "Capture an entry in a daily-note for TIME, creating it if necessary.
    328 When GOTO is non-nil, go the note without creating an entry.
    329 
    330 ELisp programs can set KEYS to a string associated with a template.
    331 In this case, interactive selection will be bypassed."
    332   (let ((org-roam-directory (expand-file-name org-roam-dailies-directory org-roam-directory))
    333         (org-roam-dailies-directory "./"))
    334     (org-roam-capture- :goto (when goto '(4))
    335                        :keys keys
    336                        :node (org-roam-node-create)
    337                        :templates org-roam-dailies-capture-templates
    338                        :props (list :override-default-time time)))
    339   (when goto (run-hooks 'org-roam-dailies-find-file-hook)))
    340 
    341 (add-hook 'org-roam-capture-preface-hook #'org-roam-dailies--override-capture-time-h)
    342 (defun org-roam-dailies--override-capture-time-h ()
    343   "Override the `:default-time' with the time from `:override-default-time'."
    344   (prog1 nil
    345     (when (org-roam-capture--get :override-default-time)
    346       (org-capture-put :default-time (org-roam-capture--get :override-default-time)))))
    347 
    348 ;;; Bindings
    349 (defvar org-roam-dailies-map (make-sparse-keymap)
    350   "Keymap for `org-roam-dailies'.")
    351 
    352 (define-prefix-command 'org-roam-dailies-map)
    353 
    354 (define-key org-roam-dailies-map (kbd "d") #'org-roam-dailies-goto-today)
    355 (define-key org-roam-dailies-map (kbd "y") #'org-roam-dailies-goto-yesterday)
    356 (define-key org-roam-dailies-map (kbd "t") #'org-roam-dailies-goto-tomorrow)
    357 (define-key org-roam-dailies-map (kbd "n") #'org-roam-dailies-capture-today)
    358 (define-key org-roam-dailies-map (kbd "f") #'org-roam-dailies-goto-next-note)
    359 (define-key org-roam-dailies-map (kbd "b") #'org-roam-dailies-goto-previous-note)
    360 (define-key org-roam-dailies-map (kbd "c") #'org-roam-dailies-goto-date)
    361 (define-key org-roam-dailies-map (kbd "v") #'org-roam-dailies-capture-date)
    362 (define-key org-roam-dailies-map (kbd ".") #'org-roam-dailies-find-directory)
    363 
    364 (provide 'org-roam-dailies)
    365 
    366 ;;; org-roam-dailies.el ends here