Dynamic Org Agenda List Based on Denote Keywords

Porting some Functions for Org-Roam to Denote

Porting some Functions for Org-Roam to Denote

I read Boris Buliga’s “Task management with org-roam Vol. 5: Dynamic and fast agenda” post.

Boris provide code and explanation to automatically updating the org-agenda-files list with only the files that have a project tag. The org-agenda-files variable defines the files that Org-Mode 📖 uses to drive it’s agenda feature set. Show todos, run time reports, etc.

If the number of org-agenda-files becomes too large, then it begins to impact performance of the agenda feature set. Thus keeping a pruned list helps with performance.

I really like the idea and appreciate the implementation. However, I don’t use Org-Roam 📖 ; instead I used Denote 📖 . So I spent a bit of time mapping Boris’s code to my reality.

Below is my walk through.

First, I am going to use a different keyword than “project.” I’m favoring the explicit “agenda”. When the note has that keyword it is part of the agenda.

(defvar jf/org-mode/agenda-keyword
  "agenda"
  "The `denote' keyword that identifies a note as part of `org-mode' agenda.")

Next up is almost a direct copy of vulpea-project-p; it returns non-nil when there’s a “todo” keyword on any of the nodes.

(defun jf/org-mode/agenda-p ()
  "Return non-nil if current buffer has any todo entry.

TODO entries marked as done are ignored, meaning the this
function returns nil if current buffer contains only completed
tasks.

From https://d12frosted.io/posts/2021-01-16-task-management-with-roam-vol5.html"
  (when (derived-mode-p 'org-mode)
    (org-element-
     (org-element-parse-buffer 'headline)
     'headline
     (lambda (h)
       (eq (org-element-property :todo-type h)
           'todo))
     nil 'first-match)))

This is an echo of Boris’s vulpea-project-update-tag function. It’s interweaves with the functions used in Denote to determine a file’s keywords. Sidenote Aspects of the when-let* function could be compressed into a native Denote function.

(add-hook 'before-save-hook
          #'jf/org-mode/denote-update-project-update-tag)
(add-hook 'find-file-hook
          #'jf/org-mode/denote-update-project-update-tag)

(defun jf/org-mode/denote-update-project-update-tag ()
  "Update `jf/org-mode/agenda-keyword' tag in the current buffer."
  (when-let* ((_proceed (not (active-minibuffer-window)))
              (file (buffer-file-name))
              (_proceed (denote-file-is-note-p file))
              (file-type (denote-filetype-heuristics file))
              (new-keywords (denote-retrieve-keywords-value
                             file-type))
              (keywords new-keywords))
    (save-excursion
      (goto-char (point-min))
      (if (jf/org-mode/agenda-p)
          (setq new-keywords (cons
                              jf/org-mode/agenda-keyword
                              new-keywords))
        (setq new-keywords (remove
                            jf/org-mode/agenda-keyword
                            new-keywords)))

      ;; cleanup duplicates
      (setq new-keywords (seq-uniq new-keywords))

      ;; update tags if changed
      (when (or (seq-difference keywords new-keywords)
                (seq-difference new-keywords keywords))
        (message "Adjusting \"%s\" keyword for %s"
                 jf/org-mode/agenda-keyword file)
        (denote-rewrite-keywords file new-keywords file-type)t))))

Where Org-Roam uses SQLite for storying and accessing metadata (e.g. the tags/keywords), Denote opts instead for front-matter and file name conventions. What I have below uses the fd 📖 program to query the file system for the tags/keywords.

(defvar jf/org-mode/directory-for-agendas
  "~/git")

(defun jf/org-mode/agenda-files ()
  "Return a list of note files containing 'agenda' tag.

Uses the fd command (see https://github.com/sharkdp/fd)

We want files either begin with the `jf/org-mode/agenda-keyword'
 or by `denote' conventions have the keyword.  Hence the complex regular
expression."
  (let ((default-directory (file-truename
                            jf/org-mode/directory-for-agendas)))
    (s-split "\n"
             (s-trim
              (shell-command-to-string
               (concat "fd --no-ignore --absolute-path --extension org "
                       "'(^|_)" jf/org-mode/agenda-keyword "[_\\.]'"))))))

Next is similar code to update the org-agenda-files based on their tags.

(defun jf/org-mode/agenda-files-update (&rest _)
  "Update the value of `org-agenda-files'."
  (setq org-agenda-files (jf/org-mode/agenda-files)))
(advice-add 'org-agenda :before #'jf/org-mode/agenda-files-update)
(advice-add 'org-todo-list :before #'jf/org-mode/agenda-files-update)

Last is the most significant change. Because I’m relying on the file name to encode the keywords, I need to ensure some synchronization. In my experimentation, I ran into problems trying to rename the file during save. With this change, I rename the file when I close/kill it.

(defun jf/org-mode/kill-buffer-hook ()
    (when-let* ((_proceed (not (active-minibuffer-window)))
                (file (buffer-file-name))
                (_proceed (denote-file-is-note-p file)))
      (call-interactively #'denote-rename-file-using-front-matter file)))
  (add-hook 'kill-buffer-hook #'jf/org-mode/kill-buffer-hook)

I’m rolling this into my workflow; I can see this drifting into the space of a right and proper package. Let me know if there’s interest.