My doom-emacs setup
Published on
A literate programming Doom emacs setup centered around GTD and Nix development.
The full source of my config is here.
Orgmode config #
General philosophy #
This is mostly written to organize my own thoughts into some coherent narrative so that I stop stumbling over my own shoelaces with ad-hoc configuration.
This is done on top of a pretty vanilla doom-emacs configuration.
GTD process implementation #
Capture anything that crosses your mind, nothing is too big or small #
As someone who uses orgmode to organize both my professional and personal life, I have four categories of incoming information:
- Tasks, containing some action to be done
- Notes, generally not actionable, or for future reference
- Meetings, for which notes should be taken
- Appointments, which are set sometime in the future
The difference between the middle two is that for meetings I usually want to capture additional meta information, like when the meeting took place and who attended it.
The four kinds of incoming information should easily distinguishable. To that end, there are two mechanisms:
- Keywords which denote the state of the task
- Tags
I will be using a limited amount of TODO states, only tasks will have a keyword.
Meetings will be marked with a “:meeting:” tag.
Appointments will be marked with a “:appointment:”
Tasks, notes and appointments will have a property that captures when they were created.
(setq org-capture-templates
`(("t" "Task" entry (file "inbox.org")
,(string-join '("* TODO %?"
":PROPERTIES:"
":CREATED: %U"
":END:")
"\n"))
("n" "Note" entry (file "inbox.org")
,(string-join '("* %?"
":PROPERTIES:"
":CREATED: %U"
":END:")
"\n"))
("m" "Meeting" entry (file "inbox.org")
,(string-join '("* %? :meeting:"
"<%<%Y-%m-%d %a %H:00>>"
""
"/Met with: /")
"\n"))
("a" "Appointment" entry (file "inbox.org")
,(string-join '("* %? :appointment:"
":PROPERTIES:"
":CREATED: %U"
":END:")
"\n"))
))
-
Task states (aka keywords)
In the spirit of simplicity, I will be using a limited set of task states:
TODO : to be done in future
STRT : being done right now
HOLD : cannot proceed, waits for some external blocker
DONE : terminal state, task is done
CNCL : terminal state, task canceled
(setq org-todo-keywords '((sequence "TODO(t)" "STRT(s)" "HOLD(h)" "|" "DONE(d)" "CNCL(c)")))
With their semantic meanings expressed by org-todo-keyword-faces:
(setq org-todo-keyword-faces '(("STRT" . +org-todo-active) ("HOLD" . +org-todo-onhold) ("CNCL" . +org-todo-cancel)))
Clarify what you’ve captured into clear and concrete action steps. #
Clarification step involves two parts:
- Locating objects to clarify
- Editing an item for clarity, potentially breaking it into several items
The first point will be implemented by a custom entry in org-agenda-custom-commands
:
(setq org-agenda-custom-commands
'(("g" "Get Things Done (GTD)"
;; Only show entries with the tag "inbox" -- just in case some entry outside inbox.org still has that file
((tags "inbox"
((org-agenda-prefix-format " %?-12t% s")
;; The list of items is already filtered by this tag, no point in showing that it exists
(org-agenda-hide-tags-regexp "inbox")
;; The header of this section should be "Inbox: clarify and organize"
(org-agenda-overriding-header "\nInbox: clarify and organize\n")))))))
“Editing an item for clarity” is a bit more ambiguous term, since “clear” and “concrete” are hard to measure in technical terms.
However, there are a couple of technical steps that can be taken:
-
Tasks can have an estimate, making it easier to pack into period when planning them. If I have an hour of free time - probably no point in tackling something that takes 2 hours to complete.
A list of estimates that can used will be specified in
inbox.org
as:Since this value is specified in the inbox.org file and org-capture-templates are added there - it’s possible to estimate effort directly when capturing a task.#+PROPERTY: Effort_ALL 0 0:05 0:10 0:15 0:30 0:45 1:00 2:00 4:00
-
Make a captured task and note link back to the context they were taken in.
I will be using
add-to-list
to shadow the previous values inorg-capture-templates
. The new versions of a task and note will have an extra line linking to the context they were taken in (placeholder%a
)The downside of this approach is that “t” and “n” appear twice in capture list, but it’s only cosmetic.
(add-to-list 'org-capture-templates `("t" "Task" entry (file "inbox.org") ,(string-join '("* TODO %?" ":PROPERTIES:" ":CREATED: %U" ":END:" "/Context:/ %a") "\n" ))) (add-to-list 'org-capture-templates `("n" "Note" entry (file "inbox.org") ,(string-join '("* %?" ":PROPERTIES:" ":CREATED: %U" ":END:" "/Context:/ %a") "\n")))
- DONE List tasks with huge(>1h) efforts as candidates for breaking down into smaller chunks feat
-
TODO Create a t-shirt size mapping with Effort_ALL feat
I prefer XSmall, Small, Medium, Large, XLarge approach to effort estimates. In Youtrack I have created a mapping between the two. Would be convenient to have it in the orgmode.
-
TODO Consider “CREATED” prop for headings entered inline. to-think
Here’s an implementation of this
-
TODO Fix extra entries in capture list fix
There are duplicates in the capture list because of add-to-list behavior. Not very important since only cosmetic.
Organize and put everything into the right place. #
- The end-result of organization - empty inbox
- Tasks get refiled into specific projects
- Tasks that don’t map to a project go to a special section of agenda
- Meetings get refiled into journal inside agenda
- Notes get refiled into specific project
- Notes without a project go to notes.org
Since I could not make the setup below work, the refiling is just something I will have to do mentally.
-
TODO Context-dependent refile targets feat
Implementation of this approach would require context-dependent refile targets – something that is not present in default emacs. However, it can be exnteded to support this (source: StackOverflow):
(require 'dash) (defvar org-refile-contexts "Contexts for `org-capture'. Takes the same values as `org-capture-templates-contexts' except that the first value of each entry should be a valid setting for `org-refile-targets'.") (defun org-refile--get-context-targets () "Get the refile targets for the current headline. Returns the first set of targets in `org-refile-contexts' that the current headline satisfies, or `org-refile-targets' if there are no such." (or (car (-first (lambda (x) (org-contextualize-validate-key (car x) org-refile-contexts)) org-refile-contexts )) org-refile-targets) ) (defun org-refile-with-context (&optional arg default-buffer rfloc msg) "Refile the headline to a location based on `org-refile-targets'. Changes the set of available refile targets based on `org-refile-contexts', but is otherwise identical to `org-refile'" (interactive "P") (let ((org-refile-targets (org-refile--get-context-targets))) (org-refile arg default-buffer rfloc msg) ))
Now, to implement the requirements at the beginning of this section:
(setq org-refile-contexts '((((("inbox.org") . (:regexp . "Projects"))) ;; example ((lambda () (string= (org-find-top-headline) "Inbox"))) ) ;; 6: Notes without a project go to notes.org (((("inbox.org") . (:regexp . "Notes"))) ;;((lambda () (string= (org-element-property :my_type (org-element-at-point)) "NOTE"))) ((lambda () ('regexp ":my_type:"))) ) ))
Journal-like results could be achieved through
(file+olp+datetree)
Review, update, and revise your lists. #
The idea behind the implementation is to create a view to help move tasks along from triage, into refiled into started and ultimately done.
When showing the “Can be done” list, it’s useful to have a quick reference to the day’s agenda to see if I can actually fit something.
(setq org-agenda-files (list "inbox.org" "agenda.org"
"notes.org" "projects.org"))
(setq org-agenda-custom-commands
'(("g" "Get Things Done (GTD)"
;; Only show entries with the tag "inbox" -- just in case some entry outside inbox.org still has that file
((tags "inbox"
((org-agenda-prefix-format " %?-12t% s")
;; The header of this section should be "Inbox: clarify and organize"
(org-agenda-overriding-header "\nInbox: clarify and organize\n")))
;; Show tasks that can be started and their estimates, do not show inbox
(todo "TODO"
((org-agenda-skip-function
'(org-agenda-skip-entry-if 'deadline 'scheduled))
(org-agenda-files (list "agenda.org" "notes.org" "projects.org"))
(org-agenda-prefix-format " %i %-12:c [%e] ")
(org-agenda-max-entries 5)
(org-agenda-overriding-header "\nTasks: Can be done\n")))
;; Show agenda around today
(agenda nil
((org-scheduled-past-days 0)
(org-deadline-warning-days 0)))
;; Show tasks on hold
(todo "HOLD"
((org-agenda-prefix-format " %i %-12:c [%e] ")
(org-agenda-overriding-header "\nTasks: on hold\n")))
;; Show tasks that are in progress
(todo "STRT"
((org-agenda-prefix-format " %i %-12:c [%e] ")
(org-agenda-overriding-header "\nTasks: in progress\n")))
;; Show tasks that I completed today
(tags "CLOSED>=\"<today>\""
((org-agenda-overriding-header "\nCompleted today\n"))))
(
;; The list of items is already filtered by this tag, no point in showing that it exists
(org-agenda-hide-tags-regexp "inbox")))
("G" "All tasks that can be done"
((todo "TODO"
((org-agenda-skip-function
'(org-agenda-skip-entry-if 'deadline 'scheduled))
(org-agenda-files (list "agenda.org" "notes.org" "projects.org")) (org-agenda-prefix-format " %i %-12:c [%e] ")
(org-agenda-overriding-header "\nTasks: Can be done\n")))
(agenda nil
((org-scheduled-past-days 0)
(org-deadline-warning-days 0)))))))
To show the agenda in a more compact manner and skip a time line when something is scheduled:
(setq org-agenda-time-grid
'((daily today require-timed remove-match)
(800 1000 1200 1400 1600 1800 2000)
"......"
"----------------"))
When something is scheduled for a specified time slot (08:00, 10:00, etc.), only the scheduled item will be shown, not the full “08:00 … …” line.
A view with tasks that are quick and slow would be useful:
- Quick(<=15m) tasks can be neatly packed into focus times
- Slow(>=2h) tasks are candidates for breaking down
;; taken from stackexchange
;; https://emacs.stackexchange.com/questions/59357/custom-agenda-view-based-on-effort-estimates
(defun fs/org-get-effort-estimate ()
"Return effort estimate when point is at a given org headline.
If no effort estimate is specified, return nil."
(let ((limits (org-get-property-block)))
(save-excursion
(when (and limits ; when non-nil
(re-search-forward ":Effort:[ ]*" ; has effort estimate
(cdr limits)
t))
(buffer-substring-no-properties (point)
(re-search-forward "[0-9:]*"
(cdr limits)))))))
(defun fs/org-search-for-quickpicks ()
"Display entries that have effort estimates inferior to 15.
ARG is taken as a number."
(let ((efforts (mapcar 'org-duration-from-minutes (number-sequence 1 15 1)))
(next-entry (save-excursion (or (outline-next-heading) (point-max)))))
(unless (member (fs/org-get-effort-estimate) efforts)
next-entry)))
(defun vt/org-search-for-long-tasks ()
"Display entries that have effort estimates longer than 1h "
(let ((efforts (mapcar 'org-duration-from-minutes (number-sequence 120 600 1)))
(next-entry (save-excursion (or (outline-next-heading) (point-max)))))
(unless (member (fs/org-get-effort-estimate) efforts)
next-entry)))
(add-to-list 'org-agenda-custom-commands
'("E" "Efforts view"
((alltodo ""
((org-agenda-skip-function 'fs/org-search-for-quickpicks)
(org-agenda-overriding-header "Quick tasks")))
(alltodo ""
((org-agenda-skip-function 'vt/org-search-for-long-tasks)
;; For longer tasks - show how long they are
(org-agenda-prefix-format "[%e] ")
(org-agenda-overriding-header "Long tasks"))))))
- DONE Remove scheduled tasks from ‘can be done’ fix
-
DONE Sort ‘can be done’ tasks by priority feat
The current default sorting is sufficient
- TODO Rewrite this using add-to-list to maintain coherency in tangled file chore
- TODO Add periodic tasks that were complete today to the list of complete tasks feat
-
CNCL Show task with duration as a continuous block feat
Maybe like so?
Not sure how it’s implementable now, but with the
org-agenda-time-grid
config is slightly better
-
TODO Do not show tasks under a project that is on HOLD feat
Applies to both “g” and “G” views.
If I put a project on pause - I don’t need to see its tasks
-
CNCL Add a view only for projects and their states feat
Just include top-level headlines from the
projects.org
and their states. Maybe percentage of tasks if I can figure out how to do it.Since in my setup the projects are just headlines - best way to view this is to
<S-TAB>
inprojects.org
.
- TODO Add effort/clocks to “Completed today” feat
- TODO Show recurring tasks that were completed today feat
- TODO Show tasks without a set time at the top of the list feat
Engage Get to work on the important stuff. #
Well that’s the easiest part. Just go and do stuff.
-
TODO Consider having separate views for the proces
A “review” view could be implemented like so
- TODO Make sure the projects priorities propagate to the tasks feat
Journaling process #
TODO Add a way to link the day note to the agenda feat #
Maybe a separate capture template like journal?
Habits #
File organization #
My ~/org/
directory should be as clean as possible with only the following files present:
inbox.org
- file for incoming notes
agenda.org
- file for
Literate programming #
While developing in literate programming style, I am using a certain set of languages – these structures help quickly add common code blocks to the documents:
(add-to-list 'org-structure-template-alist
'("elisp" . "src elisp\n"))
(add-to-list 'org-structure-template-alist
'("lua" . "src lua\n"))
(add-to-list 'org-structure-template-alist
'("nix" . "src nix\n"))
TODO add the auto-tangle templates and useful tags here #
Helpers #
Org-capture outside emacs #
Since apparently I still have operating system outside of Emacs - it’s useful to be able to capture something in org-mode without having to switch to emacs. The quick capture function from this source helps by providing a wrapper around org-capture that appears in a popup window, captures what’s needed and then disappears.
(defun abs--quick-capture ()
;; redefine the function that splits the frame upon org-capture
(defun abs--org-capture-place-template-dont-delete-windows (oldfun args)
(cl-letf (((symbol-function 'org-switch-to-buffer-other-window) 'switch-to-buffer))
(apply oldfun args)))
;; run-once hook to close window after capture
(defun abs--delete-frame-after-capture ()
(delete-frame)
(remove-hook 'org-capture-after-finalize-hook 'abs--delete-frame-after-capture)
)
;; set frame title
(set-frame-name "emacs org capture")
(add-hook 'org-capture-after-finalize-hook 'abs--delete-frame-after-capture)
(abs--org-capture-place-template-dont-delete-windows 'org-capture nil))
With this bit of code, OS can trigger something like:
setsid -f emacsclient -q -c -e '(abs--quick-capture)' >/dev/null 2>&1
To bring up the capture window.
Folding all other headlines #
This function (bound to <SPC> - l - c
) folds all headlines except for the current one.
(defun my-org-show-current-heading-tidily()
"Show current entry, keep other entries folded"
(interactive)
(if (save-excursion (end-of-line) (outline-invisible-p))
;; (progn (org-fold-show-entry) (outline-show-children)) ;; TODO: see if org-fold.el is needed
(progn (org-show-entry) (outline-show-children))
(outline-back-to-heading)
(unless (and (bolp) (org-at-heading-p))
(org-up-heading-safe)
(outline-hide-subtree)
(error "Boundary reached"))
(org-overview)
(org-reveal t)
;; (org-fold-show-entry) ;; TODO: see if org-fold.el is needed
(org-show-entry)
(outline-show-children)))
(after! org
(map! :leader
(:prefix-map ("l" . "link")
:desc "Show only the current heading, fold all others"
"c"
'my-org-show-current-heading-tidily)))
- TODO Walkable “fold mode” – detect move to other headline and close everything else feat
Default export settings #
These settings disable sub/super scripts by default and export headlines as unordered list.
(after! org
(setq org-export-with-superscripts '{})
(setq org-use-sub-scripts '{})
(setq org-export-with-section-numbers 'nil))
Automatically change TODO state based on checkbox #
When working on a composite TODO, it saves a few keystrokes when the TODO state and the completion cookie get updated automatically. Taking the code from this source:
;; Changes the TODO state based on statistics cookie
(defun org-todo-if-needed (state)
"Change header state to STATE unless the current item is in STATE already."
(unless (or
(string-equal (org-get-todo-state) state)
(string-equal (org-get-todo-state) nil)) ;; do not change item if it's not in a state
(org-todo state)))
(defun ct/org-summary-todo-cookie (n-done n-not-done)
"Switch header state to DONE when all subentries are DONE, to TODO when none are DONE, and to DOING otherwise"
(let (org-log-done org-log-states) ; turn off logging
(org-todo-if-needed (cond ((= n-done 0)
"TODO")
((= n-not-done 0)
"DONE")
(t
"STRT")))))
(add-hook 'org-after-todo-statistics-hook #'ct/org-summary-todo-cookie)
(defun ct/org-summary-checkbox-cookie ()
"Switch header state to DONE when all checkboxes are ticked, to TODO when none are ticked, and to DOING otherwise"
(let (beg end)
(unless (not (org-get-todo-state))
(save-excursion
(org-back-to-heading t)
(setq beg (point))
(end-of-line)
(setq end (point))
(goto-char beg)
;; Regex group 1: %-based cookie
;; Regex group 2 and 3: x/y cookie
(if (re-search-forward "\\[\\([0-9]*%\\)\\]\\|\\[\\([0-9]*\\)/\\([0-9]*\\)\\]"
end t)
(if (match-end 1)
;; [xx%] cookie support
(cond ((equal (match-string 1) "100%")
(org-todo-if-needed "DONE"))
((equal (match-string 1) "0%")
(org-todo-if-needed "TODO"))
(t
(org-todo-if-needed "STRT")))
;; [x/y] cookie support
(if (> (match-end 2) (match-beginning 2)) ; = if not empty
(cond ((equal (match-string 2) (match-string 3))
(org-todo-if-needed "DONE"))
((or (equal (string-trim (match-string 2)) "")
(equal (match-string 2) "0"))
(org-todo-if-needed "TODO"))
(t
(org-todo-if-needed "STRT")))
(org-todo-if-needed "DOING"))))))))
(add-hook 'org-checkbox-statistics-hook #'ct/org-summary-checkbox-cookie)
;; Reset the child checkboxes when a todo task is repeated
(add-hook 'org-todo-repeat-hook #'org-reset-checkbox-state-subtree)
-
DONE Add a property to not trigger this automatically for counter-only headlines feat
A better solution is to check if the state is nil and then don’t do anything
A custom home_maintenance view that is published locally #
I maintain a list of chores in a set of headlines tagged “home_maintenance”. To share that list - it can be published on an internal webserver (last argument for org-agenda-custom-commands
).
The custom command retrieves all headlines with tags “home_maintenance”([1]
), but does not display that actual tag ([2]
).
The state of the keyword (TODO/STRT/whatever) does not matter; as long as the task is not complete – it will show up in the list ([3]
);
(add-to-list 'org-agenda-custom-commands
'("h" "home maintenance"
((agenda ""
((org-agenda-span 7)
(org-agenda-start-on-weekday 1)
(org-agenda-time-grid nil)
(org-agenda-start-day "+0d") ;; Without this line the custom view seems to be stuck on the previous week
(org-agenda-repeating-timestamp-show-all t)
(org-agenda-prefix-format "%-12c: ")
(org-agenda-hide-tags-regexp "home_maintenance") ;; [2]
(org-agenda-sorting-strategy '((agenda priority-down category-up time-up)
(todo priority-down category-keep)
(tags priority-down category-keep)
(search category-keep)))
(org-agenda-todo-keyword-format "") ;; [3]
(org-agenda-tag-filter-preset '("+home_maintenance")) ;; [1]
)))
nil
("~/code/infra/services/dashy/home_maint.html")))
Now, whenever I need to publish this config, I can just run:
emacsclient -eval '(org-batch-store-agenda-views)'
which will save that custom view to the specified directory.
References #
- GET THINGS DONE WITH EMACS by NICOLAS P. ROUGIER
TODO Add other references chore #
General stuff #
Speedbar config #
I don’t care for images in speedbar:
(setq speedbar-use-images nil)
Development #
Nix LSP #
(use-package! lsp-mode
:ensure t)
(use-package! lsp-nix
:ensure lsp-mode
:after (lsp-mode)
:demand t
:custom
(lsp-nix-nil-formatter ["nixpkgs-fmt"]))
(use-package! nix-mode
:hook (nix-mode . lsp-deferred)
:ensure t)
TODO ispell setup works janky across machines fix #
TODO Language tool for spell/ortho checking feat #
Howto here
DONE Move the file to the root of the project chore #
DONE Add Excalidraw drawings to the sections chore #
DONE Add better faces for priorities feat #
DONE Add tangle on save hook feat #
TODO Add tangle on pre-commit hook feat #
TODO Integrate with TTRSS feat #
TODO Add auto-tangle back and forth feat #
DONE Record this file’s skeleton as default literate project file feat #
DONE Add org capture from terminal feat #
TODO Add notification mechanisms feat #
TODO Disable spell check in local variables chore #
Snippets #
README template #
Source: hackergrrl/art-of-readme
# -*- mode: snippet -*-
# name: README template
# key: readme
# --
- One-liner explaining the purpose of the module
- Necessary background context & links
- Potentially unfamiliar terms link to informative sources
- Clear, runnable example of usage
- Installation instructions
- Extensive API documentation
- Performs cognitive funneling: https://github.com/hackergrrl/art-of-readme#cognitive-funneling
- Caveats and limitations mentioned up-front
- Doesn't rely on images to relay critical information
- License
Project.org template #
A sample project.org file template that:
- Contains my default issue tags
- Adds a “Stats” headline to calculate how many things are still pending
- Tangles code blocks when file is saved
- Uses nixpkgs-fmt to run a formatter through the project (could be replaced for other langs as needed)
- Exports to hugo markdown
Located in snippets directory.
Project references #
- Zzamboni literate config
- Mic92 dotfiles, editor service reference. Reference for no-rebuild doom config.