6d92980311
Why was it any other way?
231 lines
8.6 KiB
Org Mode
231 lines
8.6 KiB
Org Mode
#+title: Org Journaling
|
||
#+author: Howard X. Abrams
|
||
#+date: 2020-09-24
|
||
#+tags: emacs org
|
||
|
||
A literate programming configuration file for extending the Journaling capabilities.
|
||
|
||
#+begin_src emacs-lisp :exports none
|
||
;;; org-journaling --- Configuring journals in org. -*- lexical-binding: t; -*-
|
||
;;
|
||
;; © 2020-2023 Howard X. Abrams
|
||
;; This work is licensed under a Creative Commons Attribution 4.0 International License.
|
||
;; See http://creativecommons.org/licenses/by/4.0/
|
||
;;
|
||
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams>
|
||
;; Maintainer: Howard X. Abrams
|
||
;; Created: September 24, 2020
|
||
;;
|
||
;; This file is not part of GNU Emacs.
|
||
;;
|
||
;;; Commentary:
|
||
;;
|
||
;; *NB:* Do not edit this file. Instead, edit the original literate file at:
|
||
;; ~/src/hamacs/org-journaling.org
|
||
;; And tangle the file to recreate this one.
|
||
;;
|
||
;;; Code:
|
||
#+end_src
|
||
* Introduction
|
||
I used to use the [[https://github.com/bastibe/org-journal][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?
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(defvar org-journal-dir (expand-file-name "~/journal")
|
||
"Location of Journal entries, org files.")
|
||
#+END_SRC
|
||
|
||
And what identifies a Journal file?
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(defvar org-journal-rx (rx (group (= 4 digit))
|
||
(optional "-")
|
||
(group (= 2 digit))
|
||
(optional "-")
|
||
(group (= 2 digit)))
|
||
"A regular expression that identifies journal file entries.")
|
||
#+END_SRC
|
||
And a function to create a new entry:
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(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)))
|
||
#+END_SRC
|
||
|
||
And connect files that /look/ like a Journal entry with =org-mode=:
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(add-to-list 'auto-mode-alist `(,org-journal-rx . org-mode))
|
||
#+END_SRC
|
||
|
||
And pull ‘er up:
|
||
#+BEGIN_SRC emacs-lisp
|
||
(ha-leader "f j" '("journal" . org-journal-new-entry))
|
||
#+END_SRC
|
||
|
||
This depends on the following [[file:~/.doom.d/snippets/org-journal-mode/__journal][snippet/template file]]:
|
||
|
||
#+begin_src snippet :tangle ~/src/hamacs/templates/journal
|
||
#+title: Journal Entry- `(ha-journal-file-datestamp)`
|
||
|
||
$0
|
||
#+end_src
|
||
|
||
And the code to connect that template to those files:
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(ha-auto-insert-file (rx "journal/" (regexp org-journal-rx)) "journal")
|
||
#+END_SRC
|
||
|
||
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.
|
||
|
||
#+begin_src emacs-lisp
|
||
(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))))
|
||
#+end_src
|
||
|
||
And some unit tests to validate this function:
|
||
|
||
#+begin_src emacs-lisp :tangle no
|
||
(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))))
|
||
#+end_src
|
||
Using the "date" associated with a file, we can create our standard timestamp:
|
||
|
||
#+begin_src emacs-lisp
|
||
(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)))
|
||
#+end_src
|
||
* 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.
|
||
|
||
#+begin_src emacs-lisp
|
||
(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))
|
||
#+end_src
|
||
* 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...
|
||
|
||
#+begin_src emacs-lisp
|
||
(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))))))))
|
||
#+end_src
|
||
|
||
Which means that the following defines this function:
|
||
|
||
#+begin_src emacs-lisp :tangle no
|
||
(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"))))
|
||
#+end_src
|
||
|
||
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:
|
||
|
||
#+begin_src emacs-lisp
|
||
(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))))
|
||
#+end_src
|
||
|
||
And this allows us to create two simple functions that can load the "next" and "previous" files:
|
||
|
||
#+begin_src emacs-lisp
|
||
(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+)))
|
||
#+end_src
|
||
|
||
#+begin_src emacs-lisp
|
||
(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-)))
|
||
#+end_src
|
||
|
||
And we could bind those:
|
||
|
||
#+BEGIN_SRC emacs-lisp
|
||
(ha-leader "f +" '("next file" . find-file-increment)
|
||
"f -" '("previous file" . find-file-decrement))
|
||
#+END_SRC
|
||
|
||
* Technical Artifacts :noexport:
|
||
Let's =provide= a name so we can =require= this file.
|
||
|
||
#+begin_src emacs-lisp :exports none
|
||
(provide 'ha-org-journaling)
|
||
;;; ha-org-journaling.el ends here
|
||
#+end_src
|
||
|
||
Before you can build this on a new system, make sure that you put the cursor over any of these properties, and hit: ~C-c C-c~
|
||
|
||
#+description: A literate programming configuration file for extending the Journaling capabilities.
|
||
|
||
#+property: header-args:sh :tangle no
|
||
#+property: header-args:emacs-lisp :tangle yes
|
||
#+property: header-args :results none :eval no-export :comments no mkdirp yes
|
||
|
||
#+options: num:nil toc:t todo:nil tasks:nil tags:nil date:nil
|
||
#+options: skip:nil author:nil email:nil creator:nil timestamp:nil
|
||
#+infojs_opt: view:nil toc:t ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js
|