Introduction

I really like the idea of having a commented and organized org file for my whole (Doom) Emacs configuration. It is good for a naturally documented config, can be enriched with links and explanations, and can even be shared via org-mode’s export to HTML/LaTex/whatever.

This post will not discuss any actual configuration, plenty of those can be found online with search terms like “emacs org-mode literate config” or similar. Maybe some time in the future I will also share my own config like that, but I feel like I should clean it up before, so this post will really only be about the “literate” and “config” part.

Doom Emacs

Everything written here also works fine for vanilla Emacs or maybe someting like Spacemacs as well with some adjustments to the file paths. The actual config Elisp files may be slightly different in those cases, but the principle is all the same.

At its core, all configuration is put into one (big) org file. The actual config bits are then tangled into three (in the case of Doom Emacs) Elisp files that contain the whole setup.

Because I tend to forget to do the actual tangling after making changes, I also set up a hook to do it automatically and to move the tangled output files to the correct location.

Config files

Doom Emacs uses three config files:

  • config.el: here goes all the custom setup code that we need for org-mode or any other additional packages, as well as base Emacs settings we want to configure.
  • init.el: a very specific file that handles the modules from the Doom catalog that will be enabled and configured.
  • packages.el: all additional packages that are not part of Doom Emacs via its modules must be added via this file.

These three files must be present for a proper Doom Emacs config. Our literate config therefore has to produce these files.

The config.el file may be empty, when we are starting our configuration, same goes for the packages.el file, if we do not need any other packages (yet). The init.el however must contain some specific code that allows Doom to properly start. I do not want to place the full source here, only the first few lines, the full file (at the time of writing) can be found here:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#+BEGIN_SRC emacs-lisp :tangle init.el
(doom! :input
       ;;bidi              ; (tfel ot) thgir etirw uoy gnipleh
       ;;chinese
       ;;japanese
       ;;layout            ; auie,ctsrnm is the superior home row

       :completion
       company           ; the ultimate code completion backend
       ;;(corfu +orderless)  ; complete with cap(f), cape and a flying feather!
       (helm +fuzzy +icons)              ; the *other* search engine for love and life
       ;;ido               ; the other *other* search engine...
       ;;ivy               ; a search engine for love and life
       ;;vertico           ; the search engine of the future
...
#+END_SRC

Please not the different tangle target location this time.

In case of vanilla Emacs, this would be simply one init.el in something like ~/.emacs.d or ~/.config/emacs with pretty much the code that we have in config.el in case of Doom Emacs.

Headers

At the top of our config file we define a few options for the literate config tangling:

1
2
3
#+PROPERTY: header-args:emacs-lisp :tangle config.el :results output silent
#+PROPERTY: header-args :mkdirp yes :comments links
#+PROPERTY: header-args:bash :tangle no

By default all tangled output is written into config.el, because that is where 99% of the config goes. The notable exception is packages.el, where it looks something like this:

1
2
3
#+BEGIN_SRC emacs-lisp :tangle packages.el
(package! winum)
#+END_SRC

The additional settings for :results output silent are useful, when experimenting with the config to quickly make changes and then reevaluating the code without polluting the file with mostly empty outputs.

The no tangle setting for bash is useful, if you want to include setup commands for stuff like additional system packages or compilations.

The tangling

First we define a few path variables that will be used to guide the tangling process. We start with the config file in org format itself:

1
2
3
#+BEGIN_SRC emacs-lisp
(defconst my-config-file (expand-file-name "config.org" org-directory))
#+END_SRC

Then we define the names for the tangled files:

1
2
3
4
5
#+BEGIN_SRC emacs-lisp
(defconst my-config-file-el (expand-file-name "config.el" org-directory))
(defconst my-init-file-el (expand-file-name "init.el" org-directory))
(defconst my-packages-file-el (expand-file-name "packages.el" org-directory))
#+END_SRC

And finally we need the final locations in the Doom config folder:

1
2
3
4
5
#+BEGIN_SRC emacs-lisp
(defconst my-final-config-file-el (expand-file-name "config.el" doom-user-dir))
(defconst my-final-init-file-el (expand-file-name "init.el" doom-user-dir))
(defconst my-final-packages-file-el (expand-file-name "packages.el" doom-user-dir))
#+END_SRC

With all that in place, we can now do the actual tangling and copying:

1
2
3
4
5
6
7
8
#+BEGIN_SRC emacs-lisp
(defun my-tangle-config ()
  (interactive)
  (org-babel-tangle-file my-config-file)
  (copy-file my-config-file-el my-final-config-file-el)
  (copy-file my-init-file-el my-final-init-file-el)
  (copy-file my-packages-file-el my-final-packages-file-el))
#+END_SRC

Now we can call M-x my-tangle-config and everything will be tangled and copied corretcly. Beautiful!

One final touch

I tend to make changes to my config files, save them, reload Emacs, and then act surprised that nothing has changed. Of course the tangling has to happen every time a change is done, otherwise nothing will happen. Luckily, there is a really easy solution for that:

1
2
3
4
5
6
#+BEGIN_SRC emacs-lisp
(add-hook 'org-mode-hook
          (lambda ()
            (if (equal (buffer-file-name) my-config-file)
                (add-hook 'after-save-hook 'my-tangle-config :local t))))
#+END_SRC

Now the tangling happens automcatically, whenever we save the config.org file.

Conclusion

With this setup, I can configure my Doom Emacs setup completely in org-mode and whenever I save the file, everything is set in place to use it. I could add an automatic reload of the config on saving, but as I save very instinctively and sometimes every few seconds, this would take a lot of time. And when I need it, it is only one SPC h r r away.