dotfiles/emacs-lisp/org_gtd.org
2022-09-17 11:55:41 +02:00

271 lines
14 KiB
Org Mode

:PROPERTIES:
:ID: 07d8e392-19ab-44d3-b4dc-cf68d73f64b6
:header-args:emacs-lisp: :comments link :tangle "org_gtd.el" :results none
:END:
#+title: Org GTD
#+filetags: emacs-load
So let me preface this file with a little... preface. When I began this file, it was a few hours after I decided it's time to finally take up GTD and step my game up when it comes to organisation. So this file, along with [[id:18476d68-cccb-48f4-aa77-caefe213d8bd][Org Roam]] and [[id:986ca7a5-d225-49bb-9e35-f2dffafe8aee][Org Mode]] are the culmination of my efforts.
* End Goal
What I've very quickly, that GTD is only working properly when at no point, you stop to think "What did I want to/was supposed to do", if that ever happens to you, you're doing GTD wrong. With that in mind and me starting university in.. 5 days.. I want to ensure that forgetting an appointment, assignment, homework and forgetting to call a friend or reply to an email are all things of the past. To that end, I'll from now on, capture everything, be it on my phone or at one of my workstations.
** The Phone Thing
What I've learned after crafting my own workflows and using Emacs and Linux for almost 4 years now (WOW!) is that if a workflow isn't reliable, annoyance-free and convenient, I won't succeed in using it long term. Therefore any new workflow I adopt, must fill all those checkboxes or it won't stick. That's why I'm on the look out for [[id:3bc7f35e-bcb5-4e55-9ec7-623afa456a98][handheld computers]] or [[id:3bc7f35e-bcb5-4e55-9ec7-623afa456a98][linux phones]] which would enable me to use Emacs conveniently on the go. I've yet to find any which would be pocket sized or cheap enough that I could afford them. One requirement i have except for the Linux thing is that they must have a physical keyboard if I'm to lug around a second device.
For now, I've decided to make due with Emacs installed in Termux with [[id:3bc7f35e-bcb5-4e55-9ec7-623afa456a98][home-manager]].
* Implementation
So let's start with the entry point to the whole thing. Those would be the ~org-roam-capture-templates~ and ~org-roam-capture-ref-templates~. I've gone with more than just what was shown in [[id:c3b7951f-b8f2-41dc-856d-07373724ef99][Get Things Done with Emacs]] to remove some burden from me when I'm refiling each day. The cognitive overhead created by having to decide on what you're capturing isn't big enough for me to just capture everything into one disorganized heading. At least that's what I think now, we'll see how it goes.
I've split the capturing process into 7 different templates, first we have the resource templates, of which I have 2 currently, one for automatic capturing from a web browser and one for capturing manually from Emacs. Then we have a special ~event~ template, which can be quickly used to capture events and have them immediately show up in you agenda. Next up is a special template for capturing Emacs LISP code, that's mostly used when adding new packages and playing with the configuration. Second to last is a catch all email capturing template and lastly a catch all generic capturing template.
So the process for capturing is to trigger the capture with ~C-c o c~ and then quickly decide between one of the categories, simple enough hopefully.
#+begin_src emacs-lisp
(setq org-roam-capture-ref-templates
`(("rw" "Web resource" entry
,(concat "* ${title} :resource:inbox:\n"
":PROPERTIES:\n"
":CREATED: %U\n"
":ROAM_REFS: ${ref}\n"
":ID: %(org-id-uuid)\n"
":END:\n\n"
"${body}")
:target (file+olp "inbox.org" ("Resources")))
("s" "Shopping list" entry
,(concat "* ${title} :inbox:\n"
":PROPERTIES:\n"
":CREATED: %U\n"
":END:\n\n"
"\n"
"${ref}\n\n"
"${body}")
:target (file+olp "inbox.org" ("Shopping list")))))
(setq org-roam-capture-templates
`(("i" "Inbox" entry
,(concat "* ${title} :inbox:\n"
":PROPERTIES:\n"
":CREATED: %U\n"
":ID: %(org-id-uuid)\n"
":END:\n\n"
"%?")
:target (file+olp "inbox.org" ("All")))
("@" "Inbox [mu4e]" entry
,(concat "* Process \"%a\" %? :inbox:\n"
":PROPERTIES:\n"
":CREATED: %U\n"
":ID: %(org-id-uuid)\n"
":END:\n\n"
"%?")
:target (file+olp "inbox.org" ("All")))
("t" "TODO" entry
,(concat "* TODO ${title} :inbox:\n"
":PROPERTIES:\n"
":CREATED: %U\n"
":ID: %(org-id-uuid)\n"
":END:\n\n"
"%?")
:target (file+olp "inbox.org" ("Todo")))
("r" "Resource")
("rw" "Web resource" entry
,(concat "* ${title} :resource:inbox:\n"
":PROPERTIES:\n"
":CREATED: %U\n"
":ROAM_REFS: %(completing-read \"URL for resource: \" nil nil nil nil nil (or (substring-no-properties (car kill-ring)) nil))\n"
":ID: %(org-id-uuid)\n"
":END:\n\n")
:target (file+olp "inbox.org" ("Resources" "Web")))
("E" "Event" entry
,(concat "* ${title} :event:inbox:\n"
":PROPERTIES:\n"
":DATE: %(org-time-stamp nil)\n"
":CREATED: %U\n"
":ID: %(org-id-uuid)\n"
":END:\n\n"
"%?")
:target (file+olp "inbox.org" ("Events")))
("e" "Emacs Lisp" plain "%?"
:target (file+head "emacs-lisp/${slug}.org"
,(concat ":PROPERTIES:\n"
":header-args:emacs-lisp: :comments link :tangle \"org_gtd.el\" :results none\n"
":END:\n"
"#+title: ${title}\n"
"#+filetags: emacs-load")))))
#+end_src
A special version of ~org-refile~ follows. What makes it special is that it first asks you for which [[id:18476d68-cccb-48f4-aa77-caefe213d8bd][Org Roam]] node you want to refile to and only then refiles. This eases the workflow quite a lot and also makes refiling snappy is it only needs to parse one file not a few thousand.
#+begin_src emacs-lisp
;; Make the refile completing read prompt also list the file itself in case it's empty
;; and also not require multiple consecutive selections in case of nested headings.
(setq org-refile-use-outline-path 'file)
(setq org-outline-path-complete-in-steps nil)
(defun org-roam-refile-incremental ()
(interactive)
(let* ((node (org-roam-node-read))
(org-refile-target-verify-function nil)
(org-refile-targets `((,(org-roam-node-file node) :maxlevel . 9))))
(org-refile-cache-clear)
(call-interactively 'org-refile)))
#+end_src
Please ignore this next block, this is some code that took me way too long to figure out and even longer to realize it's already been implemented upstream.
#+begin_src emacs-lisp :tangle no
(element (org-element-at-point))
(while (not (eq (car element) 'headline))
(setq element (plist-get (car (cdr element)) :parent)))
(setq element (car (cdr element)))
(let ((properties (org-entry-properties
(plist-get element :begin)
'standard)))
(message "%s" properties))
(delete-region
(plist-get element :begin)
(plist-get element :end))
(if (org-roam-node-file node)
(progn)
(org-roam-capture-
:node node
:templates `()
:info `()
:keys ""
:props '(:finalize )))
#+end_src
This little advice cleans up after the ~org-roam-promote-entire-buffer~ function a bit. It leaves the buffer in a state that isn't quite what I want formatting and structure wise, so this just quickly fixes that.
#+begin_src emacs-lisp
(defun magic_rb/org-roam-promote-buffer-cleanup ()
(goto-char 1)
(org-roam-end-of-meta-data)
(delete-region (point) (progn (skip-chars-forward " \t") (point)))
(org-next-visible-heading 1)
(unless (eq (point) (point-max)) (insert "\n\n"))
(whitespace-cleanup)
(org-roam-db-update-file)
(org-roam-remove-tag "inbox"))
(advice-add 'org-roam-promote-entire-buffer :after #'magic_rb/org-roam-promote-buffer-cleanup)
#+end_src
Next this function actually fixes a bug and cleans up after the ref capture. As of now [2022-08-31] the ~org-ref-capture~ protocol has a few minor issues:
1. you can't tell it not not create a ~ROAM_REFS~ property
2. it always creates the ~ROAM_REFS~ property at the root of the [[id:18476d68-cccb-48f4-aa77-caefe213d8bd][Org Roam]] node it's capturing into so if you're capturing into a heading for later refilling without an ID, it'll create a ~ROAM_REFS~ property but at the wrong place
3. when you capture into a buffer that already has a ~ROAM_REFS~ it'll break completely, so we must remove it after it adds it
#+begin_src emacs-lisp
(defun magic_rb/clear-roam-refs-if-in-inbox ()
(with-current-buffer (org-capture-get :buffer)
(let* ((target-file (file-name-nondirectory (buffer-file-name))))
(when (and (org-roam-capture-p)
(string-equal target-file "inbox.org"))
(save-excursion
(goto-char (point-min))
(org-entry-delete nil "ROAM_REFS"))))))
(add-hook 'org-capture-after-finalize-hook #'magic_rb/clear-roam-refs-if-in-inbox)
#+end_src
** Agenda
First we define a few functions, which I got from [[https://stackoverflow.com/questions/10074016/org-mode-filter-on-tag-in-agenda-view][Stack Overflow]]. They allow you to filter in [[id:22d678ce-7a3a-486c-abfb-f6cebdd77f90][Org Agenda]] views with the syntax as described in [[info:org#Matching tags and properties][Matching tags and properties]]. They're not used currently, but may come in handy so I just keep them here.
#+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))))
#+END_SRC
Now the fun part. I only define one unified agenda view for now. It allows
#+BEGIN_SRC emacs-lisp :results none
(setq org-todo-keywords '((sequence "TODO(t)" "NEXT(n)" "INPROGRESS(i)" "STUCK(s)" "|" "DONE(d)" "CANCELLED(c)"))
org-use-fast-todo-selection t)
(setq org-agenda-custom-commands
'(("g" "Get Things Done (GTD)"
((agenda ""
((org-agenda-skip-function
'(org-agenda-skip-entry-if 'deadline))
(org-deadline-warning-days 0)))
(todo "INPROGRESS"
((org-agenda-skip-function
'(org-agenda-skip-entry-if 'deadline))
(org-agenda-prefix-format " %i %-12:c [%e] ")
(org-agenda-overriding-header "\nTasks started\n")))
(todo "NEXT"
((org-agenda-skip-function
'(org-agenda-skip-entry-if 'deadline))
(org-agenda-prefix-format " %i %-12:c [%e] ")
(org-agenda-overriding-header "\nTasks planned\n")))
(agenda nil
((org-agenda-entry-types '(:deadline))
(org-agenda-format-date "")
(org-deadline-warning-days 21)
(org-agenda-skip-function
'(org-agenda-skip-entry-if 'notregexp "\\* NEXT"))
(org-agenda-overriding-header "\nDeadlines")))
(todo "TODO"
((org-agenda-prefix-format " %?-12t% s")
(org-agenda-skip-function
'(my/org-agenda-skip-without-match "-inbox"))
(org-agenda-overriding-header "\nTo be done\n")))
(tags "inbox"
((org-agenda-prefix-format " %?-12t% s")
(org-agenda-overriding-header "\nInbox\n")))
(tags "CLOSED>=\"<today>\""
((org-agenda-overriding-header "\nCompleted today\n")))))))
#+END_SRC
** Keybindings
#+begin_src emacs-lisp
(defun org-capture-inbox ()
(interactive)
(call-interactively 'org-store-link)
(org-roam-capture nil "i"))
(defun org-capture-mail ()
(interactive)
(call-interactively 'org-store-link)
(org-roam-capture nil "@"))
(setq org-agenda-hide-tags-regexp (regexp-opt '("project" "inbox")))
(general-define-key
:keymaps 'global
"C-c i" 'org-capture-inbox)
(general-define-key
:keymaps '(mu4e-headers-mode-map mu4e-view-mode-map)
"C-c i" 'org-capture-mail)
#+end_src