From 93d1f5ae8ab112e0ea14399894040378a812da0f Mon Sep 17 00:00:00 2001 From: Szymon Szukalski Date: Wed, 8 Apr 2026 11:32:34 +1000 Subject: feat: switch capture to journal datetree --- AGENTS.md | 8 ++-- README.md | 38 ++++++++------- config.org | 155 ++++++++++++++++++++++++++++++++++++++++++++----------------- 3 files changed, 139 insertions(+), 62 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0b57bca..a47fcd4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,14 +9,14 @@ This repository is for the creation and maintenance of Emacs 30 configuration fi - `config.org` is the single hand-edited source of truth for Emacs configuration. - `init.el` and `early-init.el` are generated from `config.org` and should not be edited directly. - Structure the literate config by startup responsibility first, then by workflow domain. -- The current Org workflow lives under `~/org/` with `daily/`, `projects/`, `areas/`, `areas/people/people.org`, `resources/`, and `archives/`. +- The current Org workflow lives under `~/org/` with `journal.org`, `daily/`, `projects/`, `areas/`, `areas/people/people.org`, `resources/`, and `archives/`. - `~/org/` is external to this repository and must already exist. - The configuration may open files in `~/org/`, but it must not create directories, create files, or validate note structure. - `~/org/moc.org` is a normal note. The configuration may open it, but it must not create or manage it. - `~/org/areas/people/people.org` is part of the external note system and must already exist. -- Daily notes stay as plain Org files under `~/org/daily/`; durable notes created with Denote live in PARA directories under `~/org/`. -- Agenda files are discovered by recursively scanning `.org` files under `~/org/projects/`, `~/org/areas/`, and `~/org/resources/`. -- Agenda discovery must exclude `~/org/daily/` and `~/org/archives/`. +- The operational journal lives in `~/org/journal.org`; older daily notes may remain under `~/org/daily/`; durable notes created with Denote live in PARA directories under `~/org/`. +- Agenda files are discovered by explicitly including `~/org/journal.org` and recursively scanning `.org` files under `~/org/projects/`, `~/org/areas/`, and `~/org/resources/`. +- Agenda discovery must exclude `~/org/archives/`. - PARA is the organising model for durable notes, and folder placement carries meaning. - The config includes an experimental `gptel` setup that uses GitHub Copilot as the backend. - Do not treat `auto-save-list/` as source content. diff --git a/README.md b/README.md index c4d93a3..4068c8e 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ The config bootstraps packages with built-in `package.el` and uses `use-package` The current setup uses these packages and built-in modules: -- `org` and `org-capture` for agenda, capture, daily notes, and the literate configuration itself. +- `org` and `org-capture` for agenda, journal capture, and the literate configuration itself. - `denote` for durable notes, naming, keywords, and linking. - `git-auto-commit-mode` for optional automatic commits inside `~/org` when enabled by directory-local settings. - `vertico` for minibuffer completion UI. @@ -56,7 +56,8 @@ The current setup uses these packages and built-in modules: The note system lives under `~/org/` and is organised like this: -- `daily/` for plain daily Org files. +- `journal.org` for the operational work journal. +- `daily/` for older plain daily Org files that may still exist outside the active capture workflow. - `projects/` for project notes. - `areas/` for area notes. - `areas/people/people.org` for the structured people system. @@ -69,8 +70,9 @@ This is a PARA-style layout. Folder placement carries meaning. Denote keywords a The agenda is rule-based. +- The agenda must include `~/org/journal.org`. - The agenda must include recursive scans of `.org` files under `~/org/projects/`, `~/org/areas/`, and `~/org/resources/`. -- The agenda must exclude `~/org/daily/` and `~/org/archives/`. +- The agenda must exclude `~/org/archives/`. - The agenda must use recursive discovery and explicit include and exclude rules rather than heuristic selection. ### Completion setup @@ -152,21 +154,20 @@ from the repo root: The configuration may open the MOC automatically on startup, and `C-c n M` opens it manually. The configuration must not create or manage the file. -Its Quick Access section provides actionable links for opening the agenda, today's note, capture, and a new note, while the rest of the file stays lightweight and curated around active projects, areas, and a few high-leverage resources. +Its Quick Access section provides actionable links for opening the agenda, the journal, capture, and a new note, while the rest of the file stays lightweight and curated around active projects, areas, and a few high-leverage resources. ### Capture Model The capture model has two distinct modes. -### Daily notes +### Journal -Daily notes are plain Org files in `~/org/daily/`, named by date. The workflow expects these headings: +The operational journal lives in `~/org/journal.org`. -- `Tasks` -- `Notes` -- `Open Loops` - -Daily notes are a work journal. They use light structure and are optimised for speed. +- Journal capture uses an Org datetree. +- Each day keeps explicit `Tasks`, `Notes`, and `Meetings` headings beneath the date entry. +- Journal capture is the fast path for operational work. +- The configuration assumes `~/org/journal.org` already exists and does not create it. ### Durable notes @@ -186,12 +187,12 @@ Agenda file selection follows the canonical rules in the `Agenda Rules` section `C-c c` opens capture. The configured templates cover: -- daily tasks -- daily notes -- daily meetings +- journal tasks +- journal notes +- journal meetings - Denote-backed captures for generic notes, projects, areas, and resources -Daily task capture writes under `Tasks`. Daily note capture and daily meeting capture both write under `Notes`, and the meeting template prefixes the heading with a timestamp and the word `meeting`. +Journal task capture writes under the current day's `Tasks` heading. Journal note capture writes under `Notes`. Journal meeting capture writes under `Meetings`, and the meeting template prefixes the heading with a timestamp and the word `meeting`. Denote captures still prompt for title, keywords, and subdirectory placement where appropriate, but folder placement does most of the classification work. The project capture template prepopulates the `project` keyword. Area and resource captures do not inject structural keywords automatically, and there is no Denote-backed meeting capture template. @@ -209,7 +210,7 @@ Denote handles long-lived notes. The main bindings are: - `C-c n i` to insert a canonical person name. - `C-c n I` to insert a compact person summary. - `C-c n L` to show people grouped by location. -- `C-c n d` to open today's daily note. +- `C-c n d` to open `~/org/journal.org`. - `C-c n o` to restore the people overview. - `C-c n O` to show people grouped by role. - `C-c n p` to open `~/org/areas/people/people.org`. @@ -218,6 +219,11 @@ Denote handles long-lived notes. The main bindings are: Keyword prompts and directory placement are part of the workflow, not an afterthought. The config is set up so structure is created first, then capture writes into it, with folder placement carrying most of the durable type information. +The intended split is explicit: + +- `~/org/journal.org` is for fast operational capture. +- Denote notes in the PARA directories are for durable content. + ### Automatic note commits The configuration provides `git-auto-commit-mode` capability. Behaviour is defined in `~/org/.dir-locals.el`. diff --git a/config.org b/config.org index 1940874..bbf2457 100644 --- a/config.org +++ b/config.org @@ -759,19 +759,19 @@ short and phrase-like so summaries and completion annotations stay readable. * Notes workflow -The note-taking system remains deliberately small. Daily notes stay as plain -Org files in =~/org/daily/=, while longer-lived notes use Denote inside the +The note-taking system remains deliberately small. Fast operational capture +goes into =~/org/journal.org=, while longer-lived notes use Denote inside the same root directory and rely on links for relationships. ** Org foundations The Org setup establishes the shared directories used by the workflow and provides helpers that open existing notes at point of use. Agenda views stay -focused on PARA notes, so project, area, and resource files can surface TODOs -without pulling in daily or archived notes. A curated =moc.org= in the Org root -acts as the startup landing page and quick navigation surface. The config -assumes that file already exists and opens it directly during startup rather -than creating it on demand. +focused on the journal plus PARA notes, so quick operational tasks and durable +project, area, and resource files can surface TODOs without pulling in archived +notes. A curated =moc.org= in the Org root acts as the startup landing page and +quick navigation surface. The config assumes that file already exists and opens +it directly during startup rather than creating it on demand. #+begin_src emacs-lisp (use-package org @@ -782,8 +782,8 @@ than creating it on demand. (defconst ss/org-directory (expand-file-name "~/org/") "Root directory for Org files.") - (defconst ss/org-daily-directory (expand-file-name "daily/" ss/org-directory) - "Directory for plain daily Org notes.") + (defconst ss/journal-file (expand-file-name "journal.org" ss/org-directory) + "Single-file work journal for operational capture.") (defconst ss/org-projects-directory (expand-file-name "projects/" ss/org-directory) "Directory for project notes.") @@ -800,6 +800,10 @@ than creating it on demand. (defconst ss/moc-file (expand-file-name "moc.org" ss/org-directory) "Central MOC note.") + (defconst ss/journal-section-headings + '("Tasks" "Notes" "Meetings") + "Per-day section headings maintained under each journal datetree entry.") + (defconst ss/org-agenda-directories (list ss/org-projects-directory ss/org-areas-directory @@ -838,33 +842,100 @@ than creating it on demand. (memq :template prompts)) (denote-org-capture)))) + (defun ss/journal-capture-time () + "Return the effective timestamp for the current journal capture." + (or org-overriding-default-time + (org-capture-get :default-time) + (current-time))) + + (defun ss/journal-calendar-date (&optional time) + "Return TIME as a Gregorian date list for datetree helpers." + (calendar-gregorian-from-absolute + (time-to-days (or time (current-time))))) + + (defun ss/journal-goto-date (&optional time create) + "Move to TIME's journal date heading. +When CREATE is non-nil, create the datetree entry when missing." + (goto-char (point-min)) + (if create + (progn + (require 'org-datetree) + (org-datetree-find-date-create (ss/journal-calendar-date time)) + (org-back-to-heading t) + t) + (when (re-search-forward + (format "^\\*+ %s\\b" + (format-time-string "%Y-%m-%d" (or time (current-time)))) + nil t) + (goto-char (match-beginning 0)) + t))) + + (defun ss/journal-ensure-day-sections () + "Ensure the standard section headings exist under the current journal day." + (org-back-to-heading t) + (let ((section-level (1+ (org-outline-level)))) + (save-restriction + (org-narrow-to-subtree) + (dolist (section ss/journal-section-headings) + (goto-char (point-min)) + (unless (org-find-exact-headline-in-buffer section) + (goto-char (point-max)) + (unless (bolp) + (insert "\n")) + (insert (make-string section-level ?*) " " section "\n")))))) + + (defun ss/journal-goto-section (section &optional time) + "Move to SECTION beneath TIME's journal date, creating structure as needed." + (unless (member section ss/journal-section-headings) + (user-error "Unknown journal section: %s" section)) + (ss/journal-goto-date time 'create) + (ss/journal-ensure-day-sections) + (let ((section-level (1+ (org-outline-level))) + position) + (save-restriction + (org-narrow-to-subtree) + (goto-char (point-min)) + (when (re-search-forward + (format "^%s %s$" + (make-string section-level ?*) + (regexp-quote section)) + nil t) + (setq position (match-beginning 0)))) + (unless position + (user-error "Journal section not found: %s" section)) + (goto-char position) + (org-back-to-heading t))) + + (defun ss/journal-capture-target (section) + "Select SECTION under today's journal datetree entry for capture." + (set-buffer (find-file-noselect (ss/require-existing-file ss/journal-file))) + (widen) + (ss/journal-goto-section section (ss/journal-capture-time))) + (defun ss/refresh-org-agenda-files (&rest _) - "Refresh `org-agenda-files' from the current PARA directories. + "Refresh `org-agenda-files' from the journal and PARA directories. Ignore any arguments passed by advice wrappers." (require 'org-agenda) (setq org-agenda-files (sort (delete-dups - (apply #'append - (mapcar (lambda (directory) - (directory-files-recursively - (ss/require-existing-directory directory) - "\\.org\\'")) - ss/org-agenda-directories))) + (append + (list (ss/require-existing-file ss/journal-file)) + (apply #'append + (mapcar (lambda (directory) + (directory-files-recursively + (ss/require-existing-directory directory) + "\\.org\\'")) + ss/org-agenda-directories)))) #'string<))) - (defun ss/ensure-daily-note (&optional time) - "Return the existing daily note path for TIME." - (let* ((date (or time (current-time))) - (file (expand-file-name - (format-time-string "%Y-%m-%d.org" date) - ss/org-daily-directory))) - (ss/require-existing-file file))) - - (defun ss/open-todays-note () - "Open today's daily Org note." + (defun ss/open-journal () + "Open `ss/journal-file', moving to today's entry when it exists." (interactive) - (find-file (ss/ensure-daily-note))) + (find-file (ss/require-existing-file ss/journal-file)) + (widen) + (unless (ss/journal-goto-date) + (goto-char (point-max)))) (defun ss/open-moc () "Open the central MOC note." @@ -887,7 +958,7 @@ than creating it on demand. ("C-c n i" . ss/people-insert-name) ("C-c n I" . ss/people-insert-summary) ("C-c n L" . ss/people-report-by-location) - ("C-c n d" . ss/open-todays-note) + ("C-c n d" . ss/open-journal) ("C-c n o" . ss/people-overview) ("C-c n O" . ss/people-report-by-role) ("C-c n p" . ss/people-open) @@ -906,13 +977,13 @@ than creating it on demand. ** Capture entry points -Daily capture goes to today's plain Org file. Tasks land under =Tasks= while -notes and meetings land under =Notes=. Denote capture uses Denote's own Org -integration so note identity, metadata, and directories stay under Denote's -control rather than custom code. The convenience templates keep the familiar -entry points, but only project capture injects a structural keyword by default. -The people rolodex lives outside =org-capture=: adding a person uses the -dedicated =ss/people-add= command so =~/org/areas/people/people.org= stays a compact, +Fast operational capture goes to =~/org/journal.org= using a datetree with +per-day =Tasks=, =Notes=, and =Meetings= headings. Denote capture uses Denote's +own Org integration so note identity, metadata, and directories stay under +Denote's control rather than custom code. The convenience templates keep the +familiar entry points, but only project capture injects a structural keyword by +default. The people rolodex lives outside =org-capture=: adding a person uses +the dedicated =ss/people-add= command so =~/org/areas/people/people.org= stays a compact, structured card file rather than turning into another capture target. #+begin_src emacs-lisp @@ -921,15 +992,15 @@ structured card file rather than turning into another capture target. :after (org denote) :config (setq org-capture-templates - `(("d" "Daily") - ("dt" "Task" entry - (file+headline ,#'ss/ensure-daily-note "Tasks") + `(("j" "Journal") + ("jt" "Task" entry + (function (lambda () (ss/journal-capture-target "Tasks"))) "* TODO %?") - ("dn" "Note" entry - (file+headline ,#'ss/ensure-daily-note "Notes") + ("jn" "Note" entry + (function (lambda () (ss/journal-capture-target "Notes"))) "* %?") - ("dm" "Meeting" entry - (file+headline ,#'ss/ensure-daily-note "Notes") + ("jm" "Meeting" entry + (function (lambda () (ss/journal-capture-target "Meetings"))) "* %<%H:%M> meeting %?") ("n" "Denote") ("nn" "Generic" plain -- cgit v1.2.3