Abhilash Meesala

Journaling with org-mode

I began journaling with org-mode after the Day One app was acquired by Automattic. I started with the org-journal package, then migrated to org-roam dailies and later switched to my own code. Here’s how and why.

My journaling workflow begins with one file/journal per day. I open the journal buffer throughout the day and log the current activity with a timestamp. I end the day logging in my closing thoughts. Every day, I also like to look at entries for the month and day from past years. This is the format and structure of a typical Journal file I use:

#+title: 2021-01-31
#+date: 2021-01-31

* Log
  - 06:00 ....
  - 11:42 ....
  - 17:30 ....
  - 23:42 ....

* Reflections
  The quick brown fox jumps over the lazy dog.

org-journal and org-capture fit into my workflow perfectly. But searching using org-journal was painfully slow (I blame ripgrep for setting the bar up so high).

When org-roam came bundled with the dailies feature, I immediately switched to that. Seamlessly integrating knowledge management with journaling sounded too good. Fast-forward a few months, I realized it was not my cup of tea. There was a lot of noise in the graph from dailies, and the value added was minimal.

My journaling needs are far more straightforward than what org-journal and org-roam offered. So I hacked up what I wanted in a few lines of Emacs lisp.

(defvar maze-notes-templates-directory "/Volumes/Encrypted/templates/default-daily.org")
(defvar maze-notes-journal-directory "/Volumes/Encrypted/journal")

(defun maze-notes-template-content (template)
  (f-read-text (expand-file-name template maze-notes-templates-directory)))

(defun maze-journal-new-journal-template ()
  (maze-notes-template-content "default-daily.org"))

(defun maze-journal-filename (offset)
  "RETURN file name for journal OFFSET days from today. OFFSET needs to be an integer."
  (if (integerp offset)
      (format-time-string "%Y-%m-%d.org" (+ (float-time) (* 86400 offset)))
    (error "Invalid offset value '%s'. Expecting an integer" offset)))

(defun maze-journal-create-or-open (journal)
  "Visits JOURNAL if file exists or creates a new buffer with the result of `maze-journal-new-journal-template'."
  (find-file (expand-file-name journal maze-notes-journal-directory))
  (when (equal 1 (point-max))
    (insert (format-time-string (maze-journal-new-journal-template)))
    (goto-char (point-max))))

(defun maze/journal-goto-today ()
  "Open today's journal."
  (maze-journal-create-or-open (maze-journal-filename 0)))

(defun maze/journal-goto-yesterday ()
  "Open yesterday's journal."
  (maze-journal-create-or-open (maze-journal-filename -1)))

(defun maze/journal-goto-tomorrow ()
  "Open tomorrow's journal."
  (maze-journal-create-or-open (maze-journal-filename 1)))

(defun maze/journal-goto-date ()
  "Read a date and open journal for that particular date."
  (maze-journal-create-or-open (format "%s.org" (org-read-date))))

(defun maze-open-retrospective-for-glob (pattern)
  "Open a dired buffer filtering journals on PATTERN"
  (find-name-dired maze-notes-journal-directory pattern))

(defun maze/journal-retrospect-today ()
  "Open a dired buffer to revisit journals written on this month and day in years past."
  (maze-open-retrospective-for-glob (format-time-string "*-%m-%d.org")))

(defun maze/journal-retrospect-date ()
  "Open a dired buffer to revisit journals written on a month,day."
  (let* ((date (parse-time-string (org-read-date)))
         (month (nth 4 date))
         (day (nth 3 date)))
    (maze-open-retrospective-for-glob (format "*-%d-%d.org" month day))))

All the journals are in maze-notes-journal-directory. maze/journal-goto-* interactive functions open the respective journal entries. maze/journal-retrospect-* class of functions open a Dired buffer with all the journals written on this day.

The org-capture template below adds the functionality to add entries under the ‘Log’ outline path.

(defun maze-journal-append-to-todays-log-olp()
  "Opens today's journal and places `point' at the content-end of 'Log' outline path."
  (maze-journal-create-or-open (maze-journal-filename 0))
  (goto-char (point-min))
    (when (re-search-forward (format org-complex-heading-regexp-format (regexp-quote "Log")) nil t)
      (let ((point (org-element-property :contents-end (org-element-at-point))))
        (if point
            (goto-char point)
          (insert "\n"))))))

(setq org-capture-templates
    '(("l" "Log" plain (function maze-journal-append-to-todays-log-olp)
        "- %<%H:%M> %?")))

I have been using this bare-bones solution for the past month and like how it fits into my Emacs setup. Searches are blazing fast using ripgrep, and I have the exact functionality I need.

A note on encryption: Org-mode transparently handles encrypting and decrypting files ending in org.gpg extension. Having faced several issues over the years with pinentry and caching on Mac OS, I chose not to encrypt each journal. Instead, I use Cryptomator to mount an encrypted volume and store all the files in plain text on that encrypted volume.