:PROPERTIES: :ID: 22d678ce-7a3a-486c-abfb-f6cebdd77f90 :END: #+title: Org Agenda #+filetags: :emacs-load: # SPDX-FileCopyrightText: 2022 Richard Brežák # # SPDX-License-Identifier: LGPL-3.0-or-later Put state changes into the ~LOGBOOK~ section and not into a random spot. #+BEGIN_SRC emacs-lisp (setq org-log-into-drawer t) #+END_SRC Set priority levels to A, B, and C. #+BEGIN_SRC emacs-lisp :resutls none (setq org-highest-priority ?A) (setq org-default-priority ?B) (setq org-lowest-priority ?C) #+END_SRC * Dynamic Org Agenda using Org Roam DB #+BEGIN_NOTE This whole system depends on [[id:a56794cf-b8f9-4537-a390-bd7ee6bb35ae][Vulpea]] #+END_NOTE #+BEGIN_SRC emacs-lisp :results none (with-eval-after-load "vulpea" #+END_SRC First we have to exclude the =agenda= tag from inheritance. #+BEGIN_SRC emacs-lisp :results none (add-to-list 'org-tags-exclude-from-inheritance "project") #+END_SRC Then we need a function to check whether a buffer contains any todo entry. #+BEGIN_SRC emacs-lisp :results none (defun vulpea-project-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." (when (eq major-mode 'org-mode) (org-element-map (org-element-parse-buffer 'headline) 'headline (lambda (h) (eq (org-element-property :todo-type h) 'todo)) nil 'first-match))) #+END_SRC Then we need a function which will check whether the current buffer contains any TODOs and if so, then add a roam tag to that file, so that we can easily get a list of all files with TODOs. #+BEGIN_SRC emacs-lisp :results none (add-hook 'find-file-hook #'vulpea-project-update-tag) (add-hook 'before-save-hook #'vulpea-project-update-tag) (defun vulpea-project-update-tag () "Update PROJECT tag in the current buffer." (when (and (not (active-minibuffer-window)) (vulpea-buffer-p)) (save-excursion (goto-char (point-min)) (let* ((tags (vulpea-buffer-tags-get)) (original-tags tags)) (if (vulpea-project-p) (setq tags (cons "project" tags)) (setq tags (remove "project" tags))) ;; cleanup duplicates (setq tags (seq-uniq tags)) ;; update tags if changed (when (or (seq-difference tags original-tags) (seq-difference original-tags tags)) (apply #'vulpea-buffer-tags-set tags)))))) (defun vulpea-buffer-p () "Return non-nil if the currently visited buffer is a note." (and buffer-file-name (or (string-prefix-p (expand-file-name (file-name-as-directory org-roam-directory)) (file-name-directory buffer-file-name)) (string-prefix-p (expand-file-name (file-name-as-directory "~/dotfiles/emacs-lisp")) (file-name-directory buffer-file-name))))) #+END_SRC Now for the second last function, we need to actually return the list of files containing the =project= tag, to be consumed by org-agenda. #+BEGIN_SRC emacs-lisp :results none (defun vulpea-project-files () "Return a list of note files containing 'project' tag." ; (seq-uniq (seq-map #'car (org-roam-db-query [:select [nodes:file] :from tags :left-join nodes :on (= tags:node-id nodes:id) :where (or (like tag '"%project%") (like tag '"%project-forced%"))])))) #+END_SRC Finally we can update the list of project files before every =org-agenda= invocation. #+BEGIN_SRC emacs-lisp :results none (defun vulpea-agenda-files-update (&rest _) "Update the value of `org-agenda-files'." (setq org-agenda-files (vulpea-project-files))) (advice-add 'org-agenda :before #'vulpea-agenda-files-update) #+END_SRC ** Migration To migrate existing org-roam files to this new system, run this elisp code. #+BEGIN_SRC emacs-lisp :results none :tangle no (dolist (file (org-roam-list-files)) (message "processing %s" file) (with-current-buffer (or (find-buffer-visiting file) (find-file-noselect file)) (vulpea-project-update-tag) (save-buffer))) #+END_SRC #+BEGIN_SRC emacs-lisp :results none :exports none ) #+END_SRC * Custom Tags Define a number of custom tags to ease organisation. #+BEGIN_SRC emacs-lisp :results none (defun my/org-match-at-point-p (match) "Return non-nil if headline at point matches MATCH. Here MATCH is a match string of the same format used by `org-tags-view'." (funcall (cdr (org-make-tags-matcher match)) (org-get-todo-state) (org-get-tags-at) (org-reduced-level (org-current-level)))) (defun my/org-agenda-skip-without-match (match) "Skip current headline unless it matches MATCH. Return nil if headline containing point matches MATCH (which should be a match string of the same format used by `org-tags-view'). If headline does not match, return the position of the next headline in current buffer. Intended for use with `org-agenda-skip-function', where this will skip exactly those headlines that do not match." (save-excursion (unless (org-at-heading-p) (org-back-to-heading)) (let ((next-headline (save-excursion (or (outline-next-heading) (point-max))))) (if (my/org-match-at-point-p match) nil next-headline)))) (setq org-agenda-custom-commands '(("h" "Agenda and Home-related tasks" ((agenda "") (tags-todo "home") (tags "garden"))) ("v" "Vrije Universiteit schedule and homework" ((agenda "" ((org-agenda-ndays 21) (org-agenda-skip-function '(my/org-agenda-skip-without-match "+vu")) (org-deadline-warning-days 3))) (tags-todo "vu"))) ("o" "Agenda and Office-related tasks" ((agenda "") (tags-todo "work") (tags "office"))) ("i" "Agenda and School-related tasks" ((agenda "") (tags-todo "school") (tags "school"))))) #+END_SRC