Org Journaling

Table of Contents

A literate programming configuration file for extending the Journaling capabilities.

Introduction

I used to use the org-journal project to create daily journal entries, but what I like is one file per journal entry, and that project did more than I needed. I made my own.

My requirements are simple:

  1. A directory of entries, ~/journal. Each file is based on the date, e.g. 20241228 or 20130401.
  2. A minor mode to identify a journal entry as opposed to a regular org file.

Where do I store these?

(defvar org-journal-dir (expand-file-name "~/journal")
  "Location of Journal entries, org files.")

And what identifies a Journal file?

(defvar org-journal-rx (rx (group (= 4 digit))
                           (optional "-")
                           (group (= 2 digit))
                           (optional "-")
                           (group (= 2 digit)))
  "A regular expression that identifies journal file entries.")

And a function to create a new entry:

(defun org-journal-new-entry ()
  "Opens today's journal entry file."
  (interactive)
  (find-file (expand-file-name (format-time-string "%Y%m%d")
                               org-journal-dir)))

And connect files that look like a Journal entry with org-mode:

(add-to-list 'auto-mode-alist `(,org-journal-rx . org-mode))

And pull ‘er up:

(ha-leader "f j" '("journal" . org-journal-new-entry))

This depends on the following snippet/template file:

#+title: Journal Entry- `(ha-journal-file-datestamp)`

$0

And the code to connect that template to those files:

(ha-auto-insert-file (rx "journal/" (regexp org-journal-rx)) "journal")

Note that when I create a new journal entry, I want a title that should insert a date to match the file’s name, not necessarily the current date (see below).

Journal Filename to Date

Since the Journal’s filename represents a date, I should be able to get the “date” associated with a file.

(defun ha-journal-file-date (&optional datename)
  "Return a Lisp date-timestamp from current filename.
If DATENAME is given, return that timestamp."
  (unless datename
    (setq datename (file-name-base (buffer-file-name))))

  (when (string-match org-journal-rx datename)
    (let ((day (string-to-number   (match-string 3 datename)))
          (month (string-to-number (match-string 2 datename)))
          (year(string-to-number   (match-string 1 datename))))
      (encode-time 0 0 0 day month year))))

And some unit tests to validate this function:

(ert-deftest ha-journal-file-date-test ()
  (should (equal (ha-journal-file-date "20240817") '(26304 19056)))
  (should (equal (ha-journal-file-date "2024-08-17") '(26304 19056))))

Using the “date” associated with a file, we can create our standard timestamp:

(defun ha-journal-file-datestamp (&optional datename)
  "Return date string of DATENAME if given.
If nil, use the buffer's filename's date."
  (format-time-string "%e %b %Y (%A)" (ha-journal-file-date datename)))

Journal Capture

Capturing a task (that when uncompleted, would then spillover to following days) could go to the daily journal entry. This requires a special function that opens today’s journal, but specifies a non-nil prefix argument in order to inhibit inserting the heading, as org-capture will insert the heading.

(defun org-journal-find-location ()
  "Create or load a new journal buffer. Used for `org-capture'."
  (org-journal-new-entry)
  (goto-char (point-max)))

(defvar org-capture-templates (list))

(add-to-list 'org-capture-templates
             '("j" "Journal Task/Entry" plain
               (function org-journal-find-location)
               "* %?\n\n  %i\n\n  From: %a"
               :empty-lines 1 :jump-to-captured t :immediate-finish t))

Next and Previous File

Sometimes it is obvious what is the next file based on the one I’m currently reading. For instance, in my journal entries, the filename is a number that can be incremented. Same with presentation files…

(defun split-string-with-number (str)
  "Return list of three components of string, STR.
The first is the text prior to any numbers, The second is the
embedded number, and the third is the rest of the text in the
string."
  (let* ((ms (string-match (rx (one-or-more digit)) str)))
    (when ms
      (list (substring str 0 ms)
            (match-string 0 str)
            (substring str
                       (+ ms
                          (length (match-string 0 str))))))))

Which means that the following defines this function:

(ert-deftest split-string-default-separatorsg-with-number-test ()
  (should (equal (split-string-with-number "abc42xyz") '("abc" "42" "xyz")))
  (should (equal (split-string-with-number "42xyz")    '("" "42" "xyz")))
  (should (equal (split-string-with-number "abc42")    '("abc" "42" "")))
  (should (equal (split-string-with-number "20140424") '("" "20140424" "")))
  (should (null  (split-string-with-number "abcxyz"))))

Given this splitter function, we create a function that takes some sort of operator and return a new filename based on the conversion that happens:

(defun find-file-number-change (f)
  "Return a filename based on applying F to current buffer.
Where F would be something like `1+' or `1-'."
  (let* ((filename (buffer-file-name))
         (parts    (split-string-with-number
                    (file-name-base filename)))
         (new-name (number-to-string
                    (funcall f (string-to-number (nth 1 parts))))))
    (concat (file-name-directory filename)
            (nth 0 parts)
            new-name
            (nth 2 parts))))

And this allows us to create two simple functions that can load the “next” and “previous” files:

(defun find-file-increment ()
  "Load file that is _one more_ than the file in current buffer.
This requires that the current file contain a number that can be
incremented."
  (interactive)
  (find-file (find-file-number-change '1+)))
(defun find-file-decrement ()
  "Load file that is _one less_ than the file in current buffer.
This requires that the current file contain a number that can be
decremented."
  (interactive)
  (find-file (find-file-number-change '1-)))

And we could bind those:

(ha-leader "f +" '("next file" . find-file-increment)
           "f -" '("previous file" . find-file-decrement))