My Doom Emacs Config

Links

General Settings

Set name and email:

(setq user-full-name "Niklas Bühler"
      user-mail-address "hi@niklasbuehler.com")

Set default frame size:

(add-to-list 'default-frame-alist '(height . 36))
(add-to-list 'default-frame-alist '(width . 120))

Set default directory:

(setq default-directory "~/org")

Add contrib directory to load path:

(add-to-list 'load-path "~/.doom.d/contrib/")

Editing

Define some general undo and saving settings:

(setq undo-limit 80000000
      evil-want-fine-undo t
      auto-save-default t
      make-backup-files t)

Use relative line numbers, but also make them work with folds:

(setq display-line-numbers-type 'visual)

Use auto-fill in text modes (to keep lines short):

(add-hook 'text-mode-hook 'turn-on-auto-fill)

Evil Mode

Define vim commands:

(evil-ex-define-cmd "W[rite]" 'save-buffer)
(evil-ex-define-cmd "V[split]" 'evil-window-vsplit)

Behaviour

Navigation

Dired

Define evil mappings for dired:

(map! :map dired-mode-map
      :n "h" #'dired-up-directory
      :n "l" #'dired-find-alternate-file)

Deft

Configure deft:

(setq deft-directory "~/org/agenda")
(setq deft-recursive t)

Make deft quickly accessible:

(map! :leader :desc "Open deft" "d" #'deft)

Text Completion

Enable completion:

(after! company
  (setq company-idle-delay 0.5
        company-minimum-prefix-length 2)
  (setq company-show-numbers t)
  (add-hook 'evil-normal-state-entry-hook #'company-abort))

And improve its memory:

(setq-default history-length 1000)
(setq-default prescient-history-length 1000)

Enable Ispell:

(set-company-backend!
  '(text-mode
    markdown-mode
    gfm-mode)
  '(:seperate
    company-ispell
    company-files
    company-yasnippet))

Show the key bindings popup a bit faster:

(setq which-key-idle-delay 0.25)

Looks

Theme

Set the light doom-one theme as default:

(setq doom-theme 'doom-one-light)

Fonts

Configure fonts:

;; If you or Emacs can't find your font, use 'M-x describe-font' to look them
;; up, `M-x eval-region' to execute elisp code, and 'M-x doom/reload-font' to
;; refresh your font settings. If Emacs still can't find your font, it likely
;; wasn't installed correctly. Font issues are rarely Doom issues!
(setq doom-font (font-spec :family "Fira Code" :size 16) ;; "Noto Sans Mono"
      doom-big-font (font-spec :family "Fira Code" :size 24)
      doom-variable-pitch-font (font-spec :family "Overpass" :size 16)
      doom-unicode-font nil
      doom-serif-font (font-spec :family "Bitstream Vera Serif" :size 16))
;; Adapt the variable-pitch size to fit better into mixed-pitch-mode
(set-face-attribute 'variable-pitch nil :height 1.2)
(setq mixed-pitch-set-height t)

Define different font sizes in org headings and the agenda view:

(after! org
  (custom-set-faces!
    '(org-document-title :height 1.5 :weight extra-bold)
    '(org-level-1 :inherit outline-1 :weight extra-bold :height 1.4)
    '(org-level-2 :inherit outline-2 :weight bold :height 1.3)
    '(org-level-3 :inherit outline-3 :weight bold :height 1.2)
    '(org-level-4 :inherit outline-4 :weight bold :height 1.1)
    '(org-level-5 :inherit outline-5 :weight semi-bold :height 1.0)
    '(org-level-6 :inherit outline-6 :weight semi-bold :height 1.0)
    '(org-level-7 :inherit outline-7 :weight semi-bold)
    '(org-level-8 :inherit outline-8 :weight semi-bold)
    '(org-super-agenda-header :weight semi-bold :height 1.25)
    ;; Ensure that anything that should be fixed-pitch in org buffers appears that
    ;; way
    '(org-block nil :foreground nil :inherit 'fixed-pitch)
    '(org-code nil   :inherit '(shadow fixed-pitch))
    '(org-table nil   :inherit '(shadow fixed-pitch))
    '(org-verbatim nil :inherit '(shadow fixed-pitch))
    '(org-special-keyword nil :inherit '(font-lock-comment-face fixed-pitch))
    '(org-meta-line nil :inherit '(font-lock-comment-face fixed-pitch) :height 0.5)
    '(org-checkbox nil :inherit 'fixed-pitch)
    ))

Define fancy priorities:

(setq org-fancy-priorities-list '((?A . "‼")
                                  (?B . "❗")
                                  (?C . "🟩")))

Org-Mode

General

Define the org and agenda file paths:

(setq org-directory "~/org/"
      org-agenda-files (list
                        "~/org/agenda"
                        "\.org$")
      org-archive-location (concat org-directory ".trash/archive.org::"))

Looks

Start in overview mode and hide blocks (drawers) on startup:

(after! org
  (setq org-startup-folded 'overview
        org-ellipsis " ▾ "
        org-hide-block-startup t
        org-list-demote-modify-bullet '(("+" . "-") ("-" . "+"))))

Enable org-pretty-mode and mixed-pitch-mode:

(add-hook! 'org-mode-hook #'+org-pretty-mode #'mixed-pitch-mode) ;; #'gkh/org-mode-visual)

Add spacing around org headings:

(customize-set-variable 'org-blank-before-new-entry
                        '((heading . nil)
                          (plain-list-item . nil)))
(setq org-cycle-separator-lines 1)

Show one newline in overview mode, if there are 2 newlines between headings:

(setq org-cycle-separator-lines 1)

Don’t assign a tag when attaching an image:

(setq org-attach-auto-tag nil)

Automatically resize images in org mode:

(setq org-image-actual-width '(600))

Automatically render images and latex previews, but show the latex source when the cursor enters:

;;(setq org-startup-with-latex-preview 1)
(setq org-startup-with-inline-images 1)
(add-hook 'org-mode-hook #'org-fragtog-mode)

Fix the background color of latex snippets:

(setq org-highlight-latex-and-related '(native script entities))
;;(setq org-highlight-latex-and-related nil)

Set the tag column:

(setq org-tags-column 0)

Behaviour

General

Define TODO keywords:

  • ! stores a timestamp when entered
  • @ stores a timestamp + note when entered
  • Show available colors with list-colors-display
(after! org
  (setq org-log-done nil
        org-log-into-drawer 't
        org-todo-keywords '((sequence "TODO(t)" "PROJ(p)" "HOLD(h)" "WAIT(w)" "BUY(b)" "IDEA(i)" "|" "DONE(d)" "KILL(k)"))
        org-todo-keyword-faces
        '(("TODO" :foreground "lime green" :weight extra-bold :underline t :family "Fira Code")
          ("BUY" :foreground "sky blue" :weight extra-bold :underline t :family "Fira Code")
          ("PROJ" :foreground "light slate gray" :weight normal :underline nil :family "Fira Code")
          ("WAIT" :foreground "slate blue" :weight normal :underline nil :family "Fira Code")
          ("IDEA" :foreground "light green" :weight normal :underline nil :family "Fira Code")
          ("HOLD" :foreground "dark slate blue" :weight normal :underline nil :family "Fira Code")
          ("DONE" :foreground "slate gray" :weight normal :underline nil :strike-through t :family "Fira Code")
          ("KILL" :foreground "red" :weight normal :underline nil :strike-through t :family "Fira Code"))))

Reset checkboxes after completion of TODO item:

(require 'org-checklist)

Navigation

Enable rifle and add a key binding:

(map! :leader
      :desc "Rifle on agenda files"
      "/" #'helm-org-agenda-files-headings);
;; Call either helm-org-rifle-agenda-files or helm-org-agenda-files-headings

Add key bindings for narrowing/folding org headings:

(defun my/restore-startup-visibility ()
  (interactive)
  (org-set-startup-visibility))

(defun my/org-focus-subtree ()
  "Toggle org narrow subtree / show everything"
  (interactive)
  (if (buffer-narrowed-p)
      (widen)
    (org-narrow-to-subtree)))

(defun my/org-show-current-heading-tidily ()
  "Show next entry, keeping other entries closed."
  (interactive)
  (if (save-excursion (end-of-line) (outline-invisible-p))
      (progn (org-show-entry) (show-children))
    (outline-back-to-heading)
    (unless (and (bolp) (org-on-heading-p))
      (org-up-heading-safe)
      (hide-subtree)
      (error "Boundary reached"))
    (org-overview)
    (org-reveal t)
    (org-show-entry)
    (show-children)))

(map! :leader
      (:prefix-map ("v" . "visibility")
      :desc "Restore startup visibility" "v" #'my/restore-startup-visibility
      :desc "Narrow on org subtree" "n" #'my/org-focus-subtree
      :desc "Fold everything except heading" "f" #'my/org-show-current-heading-tidily))
(setq org-cycle-emulate-tab nil)
;;(setq org-startup-folded 'content)

Tag clicks show sparse tree instead of agenda view:

(defun tag-at-point-in-heading ()
  "Returns the tag at the current point in the string"
  (let ((str (buffer-string))
        (begin (point))
        (end (point)))
    (while (not (equal (aref str begin) ?:))
      (setq begin (- begin 1)))
    (while (not (equal (aref str end) ?:))
      (setq end (+ end 1)))
    (substring str (+ 1 begin) end)))

(defun open-sparse-view ()
  "Shows a sparse tree on clicking a tag instead of org-tags-view"
  ;; From org-open-at-point, sanity checking that we're on a headline with tags
  (when (and (org-element-lineage (org-element-context)
                                  '(headline inlinetask)
                                  t)
             (progn (save-excursion (beginning-of-line)
                                    (looking-at org-complex-heading-regexp))
                    (and (match-beginning 5)
                         (> (point) (match-beginning 5)))))
    (org-match-sparse-tree nil (concat "+" (tag-at-point-in-heading)))
    't))

(add-hook 'org-open-at-point-functions
          'open-sparse-view)

Capturing

Define the capture templates:

(setq org-capture-templates
      '(("t" "Todo" entry (file+headline "~/org/agenda/todo.org" "📥 Inbox")
         "* TODO %?"
         :empty-lines 0)
        ("j" "Journal" entry (file+datetree "~/org/journal.org")
         "* %U %?"
         :empty-lines 1)
        ("n" "Note" entry (file+datetree "~/org/notes.org")
         "* %U %?"
         :empty-lines 1)
        ("g" "Groceries" entry (file+headline "~/org/agenda/groceries.org" "✏ Captures")
         "* BUY %?"
         :empty-lines 0)
        ("v" "Visualization" entry (file+datetree "~/org/journal.org")
         "* %U Who do I want to be and how would that person act today? :VISUALIZATION:\n- %?"
         :empty-lines 1)
        ("r" "Review")
        ("rm" "Monthly Review" entry (file+datetree "~/org/journal.org")
         "* Monthly Review %? :REVIEW:MONTH:\n** Good stuff\n- \n** Bad stuff\n- \n** Key Insights\n- \n** Next Level\n- \n** Yearly Goal Review\n- "
         :empty-lines 1)
        ("rw" "Weekly Review" entry (file+datetree "~/org/journal.org")
         "* Weekly Review KW %? :REVIEW:WEEK:\n** Good stuff\n- \n** Bad stuff\n- \n** Key Insights\n- \n** Next Level\n- "
         :empty-lines 1)
        ("rd" "Daily Review" entry (file+datetree "~/org/journal.org")
         "\n* %u Daily Review :REVIEW:DAY:\n** Daily Gratitude\n1. %?\n2. \n3. \n** What did I learn today?\n- "
         :empty-lines 1)))

Super Agenda

Enable super agenda:

(org-super-agenda-mode)

Open at the exact heading when pressing RET in agenda view:

(defun my/center-on-heading ()
  (recenter (/ (window-height) 2))
  (org-back-to-heading t))
(add-hook 'org-agenda-after-show-hook #'my/center-on-heading)

Remap j and k to movement keys in agenda:

(define-key org-super-agenda-header-map "h" 'evil-backward-char)
(define-key org-super-agenda-header-map "j" 'evil-next-line)
(define-key org-super-agenda-header-map "k" 'evil-previous-line)
(define-key org-super-agenda-header-map "l" 'evil-forward-char)

Define icons per category (list of unicodes):

(setq org-agenda-category-icon-alist
      '(("adventure" (print "🏕") nil nil :ascent center)
        ("vacation" (print "🏖") nil nil :ascent center)
        ("books" (print "📚") nil nil :ascent center)
        ("orga" (print "🗓") nil nil :ascent center)
        ("routines" (print "☑") nil nil :ascent center)
        ("reset" (print "🧹") nil nil :ascent center)
        ("review" (print "✍") nil nil :ascent center)
        ("finances" (print "💵") nil nil :ascent center)
        ("digital" (print "💻") nil nil :ascent center)
        ("accounting" (print "💰") nil nil :ascent center)
        ("investing" (print "📈") nil nil :ascent center)
        ("groceries" (print "🛒") nil nil :ascent center)
        ("health" (print "❤") nil nil :ascent center)
        ("cardio" (print "🏃") nil nil :ascent center)
        ("strength" (print "🏋") nil nil :ascent center)
        ("sports" (print "🏅") nil nil :ascent center)
        ("newcon" (print "🟢") nil nil :ascent center)
        ("bus+" (print "🚌") nil nil :ascent center)
        ("shortbreak" (print "⏸") nil nil :ascent center)
        ("project" (print "🛠") nil nil :ascent center)
        ("selfdev" (print "🚀") nil nil :ascent center)
        ("shopping" (print "🛒") nil nil :ascent center)
        ("social" (print "👥") nil nil :ascent center)
        ("random" (print "❔") nil nil :ascent center)
        ("MLGS" (print "🤖") nil nil :ascent center)
        ("TN" (print "🌐") nil nil :ascent center)
        ("MLRG" (print "🧬") nil nil :ascent center)
        ("ES" (print "🇪🇸") nil nil :ascent center)
        ("PPI" (print "🔬") nil nil :ascent center)
        ("BDL" (print "🧠") nil nil :ascent center)
        ("ADLM" (print "🩻") nil nil :ascent center)
        ("erasmus" (print "✈") nil nil :ascent center)
        ("uni" (print "🎓") nil nil :ascent center)
        ))

Define a custom super agenda view:

(setq org-agenda-skip-scheduled-if-done t
      org-agenda-skip-deadline-prewarning-if-scheduled t
      org-agenda-skip-deadline-if-done t
      org-agenda-include-deadlines t
      org-agenda-block-separator nil
      org-agenda-compact-blocks t
      org-agenda-start-day nil ;; i.e. today
      org-agenda-span 1
      org-agenda-start-on-weekday nil)
(setq org-agenda-custom-commands
      '(("d" "Daily Super Agenda"
         ((agenda "" ((org-agenda-overriding-header "")
                      (org-enforce-todo-dependencies nil)
                      (org-super-agenda-groups
                       '((:name "☑ Daily"
                          :time-grid t
                          :order 0)
                         ;;(:discard (:scheduled future))
                         (:name "🐸 Frogs"
                          ;;:priority ("A" "B")
                          :tag ("FROG")
                          :order 1)
                         (:name "🤖 Batching"
                          :tag ("BATCH")
                          :order 100)
                         (:name "☑ Routines"
                          ;;:category ("orga" "reset" "review")
                          :file-path ("routines.org")
                          :order 10)
                         (:name "🏅 Sports"
                          :category ("sports" "cardio" "strength")
                          :order 20)
                         (:name "🎓 Uni"
                          ;;:category ("uni" "erasmus" "MLGS" "MLRG" "TN" "ES" "PPI" "BDL" "ADLM")
                          :file-path ("uni")
                          :order 30)
                         (:name "🟢 newcon GmbH"
                          ;;:category ("newcon" "bus+" "shortbreak")
                          :file-path ("newcon.org")
                          :order 40)
                         (:name "👥 Social"
                          ;;:category "social"
                          :file-path ("social.org")
                          :order 50)
                         (:name "🚀 Selfdev"
                          :file-path ("selfdev.org")
                          :order 60)
                         (:name "💵 Finances"
                          ;;:category ("finances" "accounting" "investing")
                          :file-path ("finances.org")
                          :order 70)
                         (:name "🛠 Projects"
                          :category "projects"
                          :order 80)
                         (:name "❔ Random"
                          :anything t
                          :order 99)
                         ;;(:name "❔ Random"
                         ;;:category "random"
                         ;;:order 99)
                         ;;(:auto-category t
                         ;;:order 100)
                         ))
                      (org-agenda-list)))))
        ("u" "Unscheduled TODOs"
         ((alltodo "" ((org-agenda-overriding-header "Unscheduled TODOs")
                       (org-agenda-dim-blocked-tasks 'invisible)
                       (org-enforce-todo-dependencies t)
                       (org-super-agenda-groups
                        '((:discard (:scheduled past
                                     :scheduled today
                                     :scheduled future
                                     :deadline past
                                     :deadline today
                                     :deadline future))
                          (:discard (:todo ("WAIT" "HOLD" "IDEA")
                                     :file-path "groceries.org"
                                     :category "routines"))
                         (:name "🏅 Sports"
                          :category ("sports" "cardio" "strength")
                          :order 20)
                         (:name "🎓 Uni"
                          ;;:category ("uni" "erasmus" "MLGS" "MLRG" "TN" "ES" "PPI" "BDL" "ADLM")
                          :file-path ("uni.org")
                          :order 30)
                         (:name "🟢 newcon GmbH"
                          ;;:category ("newcon" "bus+" "shortbreak")
                          :file-path ("newcon.org")
                          :order 40)
                         (:name "❤ Health"
                          :file-path ("health.org")
                          :order 45)
                         (:name "👥 Social"
                          ;;:category "social"
                          :file-path ("social.org")
                          :order 50)
                         (:name "🚀 Selfdev"
                          :file-path ("selfdev.org")
                          :order 60)
                         (:name "💻 Digital Life"
                          :file-path ("digital.org")
                          :order 65)
                         (:name "💵 Finances"
                          ;;:category ("finances" "accounting" "investing")
                          :file-path ("finances.org")
                          :order 70)
                         (:name "🛠 Projects"
                          :category "projects"
                          :order 80)
                         (:name "🛒 Shopping"
                          :category "shopping"
                          :order 90))
                         ;;(:discard
                          ;;(:category "random")))
                        )
                       (org-todo-list)))))
        ("r" "Review stuck TODOs"
         ((alltodo "" ((org-agenda-overriding-header "Review stuck TODOs")
                       (org-super-agenda-groups
                        '((:discard (:todo "PROJ"
                                     :file-path "groceries.org"
                                     :category "routines"))
                          (:name "⏱ WAIT: Review"
                           :todo "WAIT"
                           :order 0)
                          (:name "🖐 HOLD: Review"
                           :todo "HOLD"
                           :order 10)
                          (:name "💡 IDEA: Review"
                           :todo "IDEA"
                           :order 16)
                          (:discard (:scheduled past
                                     :scheduled today
                                     :scheduled future
                                     :deadline past
                                     :deadline today
                                     :deadline future))
                          (:name "❔ Random: Refile"
                           :category "random"
                           :order 15)
                          (:discard (:anything t)))
                        )
                       (org-todo-list)))))
        ))

Make the daily view quickly accessible:

(map! :leader
      :prefix ("a" . "Super Agenda")
      :desc "Daily" "a" #'(lambda () (interactive) (org-agenda nil "d"))
      :desc "Unscheduled" "u" #'(lambda () (interactive) (org-agenda nil "u"))
      :desc "Review" "r" #'(lambda () (interactive) (org-agenda nil "r")))

Roam

Configure roam2:

(use-package! org-roam
  :after org
  :init
  (setq org-roam-directory (file-truename "~/org/roam")
        org-roam-db-location "~/.emacs.d/.local/cache/org-roam.db"
        org-roam-mode-section-functions (list #'org-roam-backlinks-section #'org-roam-reflinks-section #'org-roam-unlinked-references-section))
  :config
  (map! :leader
        :prefix ("r" . "roam")
        :desc "Find Node" "f" #'org-roam-node-find
        :desc "Insert Node" "i" #'org-roam-node-insert
        :desc "Toggle Buffer" "b" #'org-roam-buffer-toggle
        :desc "Find Ref" "r" #'org-roam-ref-find
        :desc "Show Graph" "g" #'org-roam-graph
        :desc "Capture" "c" #'org-roam-capture)
  (org-roam-db-autosync-enable))

This is a pretty ugly fix for updating the roam buffer by closing it and opening it again on every org link navigation:

;;(defun my/toggle-roam-buffer-twice ()
    ;;(interactive)
    ;;(dotimes (_ 2)
    ;;(org-roam-buffer-toggle)))
;;(add-hook 'org-follow-link-hook #'my/toggle-roam-buffer-twice)

Roam UI

Configure roam-ui:

(use-package! websocket
    :after org-roam)

(use-package! org-roam-ui
    :after org-roam ;; or :after org
;;         normally we'd recommend hooking orui after org-roam, but since org-roam does not have
;;         a hookable mode anymore, you're advised to pick something yourself
;;         if you don't care about startup time, use
    ;;:hook (after-init . org-roam-ui-mode)
    :init
    (map! :leader
          :prefix ("r". "roam")
          :desc "Open Roam UI" "u" #'org-roam-ui-open)
    :config
    (setq org-roam-ui-sync-theme t
          org-roam-ui-follow t
          org-roam-ui-update-on-save t
          org-roam-ui-open-on-start t))

Automatically sync roam-ui theme to emacs theme:

;;(defvar after-load-theme-hook nil
  ;;"Hook run after a color theme is loaded using `load-theme'.")
;;(defadvice load-theme (after run-after-load-theme-hook activate)
  ;;"Run `after-load-theme-hook'."
  ;;(run-hooks 'after-load-theme-hook))
;;(add-hook 'after-load-theme-hook #'org-roam-ui-sync-theme)

Website

Define the org project to be published:

(setq org-publish-project-alist
      '(("org-notes"
        :base-directory "~/org/website/src"
        :base-extension "org"
        :publishing-directory "~/org/website/pub/"
        :recursive t
        :publishing-function org-html-publish-to-html
        :headline-levels 4
        :with-author nil
        :time-stamp-file nil
        :with-creator nil
        :with-toc nil
        :section-numbers nil
        :auto-preamble t)
        ("org-static"
        :base-directory "~/org/website/src"
        :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf\\|ico"
        :publishing-directory "~/org/website/pub/"
        :recursive t
        :publishing-function org-publish-attachment)
        ("org" :components ("org-notes" "org-static"))
        ))

Reading

Configure calibredb:

(use-package calibredb
  :defer t
  :config
  (setq calibredb-root-dir "~/Library")
  (setq calibredb-db-dir (expand-file-name "metadata.db" calibredb-root-dir))
  (setq calibredb-library-alist '(("~/Library"))))

Supply surrogate for required string-replace function:

;; backport for older emacs versions
(if (not (fboundp 'string-replace))
    (progn
      (defun string-replace (from to in)
        (replace-regexp-in-string (regexp-quote from) to in nil 'literal))
      (declare-function string-replace "nov")
    ))

Automatically open ebooks in nov-mode:

(add-to-list 'auto-mode-alist '("\\.epub\\'" . nov-mode))

Shoplist

Configure the org-shoplist package:

(setq org-shoplist-default-format 'org-shoplist-shoplist-as-todo-list)
(setq org-shoplist-buffer-name "🛍 Groceries")
(setq org-shoplist-auto-add-unit 't)
(setq org-shoplist-keyword "BUY")

Fixes

Empty src field for quick experiments:

  • Hit C-c C-c to apply code

TODOs

  1. [ ] Disable crossref information (slows down)?
  2. [ ] Disable highlighting of lines containing LaTeX fragments
    • Blocks
  3. [ ] Fixed Pitch Mode for Checklist numbers
  4. [ ] Define shortcut for SPC n m (org-tag-view)