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:

  1. Tasks, containing some action to be done
  2. Notes, generally not actionable, or for future reference
  3. Meetings, for which notes should be taken
  4. 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:

  1. Keywords which denote the state of the task
  2. 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"))
        ))

Clarify what you’ve captured into clear and concrete action steps. #

Clarification step involves two parts:

  1. Locating objects to clarify
  2. 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:

  1. 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:

          #+PROPERTY: Effort_ALL 0 0:05 0:10 0:15 0:30 0:45 1:00 2:00 4:00
    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.

  2. 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 in org-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")))

Organize and put everything into the right place. #

  1. The end-result of organization - empty inbox
  2. Tasks get refiled into specific projects
  3. Tasks that don’t map to a project go to a special section of agenda
  4. Meetings get refiled into journal inside agenda
  5. Notes get refiled into specific project
  6. 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.

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:

  1. Quick(<=15m) tasks can be neatly packed into focus times
  2. 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"))))))

Engage Get to work on the important stuff. #

Well that’s the easiest part. Just go and do stuff.

Journaling process #

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)))

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)

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 #

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:

Located in snippets directory.

Project references #