My init.org Setup

Posted on
emacs org

Like many others, I’ve found Org Mode to be a great tool to manage my Emacs configuration. While overall the process has been extremely smooth there are a few tricks I picked up/stole along the way that I wanted to share in hopes of helping others.

Tangle on Save

I keep my configuration in ~/.emacs.d/init.org. This makes it easy to call org-babel-tangle to produce my init.el file.

A lot of people simply call org-bable-load-file from init.el which means they don’t have to worry about tangling their config, it’s just done when they load Emacs. However, I prefer to keep everything in 1 file. Plus, although I use Emacs server and so startup time isn’t a big concern, I can’t get over feeling that tangling the Org file each time Emacs is loaded is inefficient.

So I have my configuration set to tangle on each save with the following (at the bottom of my init.org as is required for file variables):

* Local Variables
# Local Variables:
# eval: (add-hook 'after-save-hook (lambda ()(org-babel-tangle)) nil t)
# End:

Keeping Changes in Sync During Commit

I’ll often make lots of small changes to my init.org throughout the day. Then once I’m sure I’m satisfied with something I’ll commit it to my repo.

I keep both my init.org and the generated init.el under version control and like to keep each commit as focused as is possible/reasonable which meant I had to stage the relevant hunks from init.org and then find the corresponding changes in init.el and stage those before making a commit. While it’s not super onerous, I’d often forget or just find myself frustrated with the process, so I found a way to offload the menial bit.

I now use git hooks to make things easier. If you’re not familiar, git hooks essentially let you run a script when certain events occur.

I have the following two hooks in my .emacs.d repo:

.git/hooks/pre-commit

#!/bin/sh
# Create a temp file
TMPFILE=`mktemp` || exit 1
# Specify as an Org file for Emacs
echo "-*- mode: org -*-" >> $TMPFILE
# Write the staged version of init.org
git show :init.org >> $TMPFILE
# Tangle the temp file
TANGLED=`emacsclient -e "(let ((enable-local-variables :safe)) (car (org-babel-tangle-file \"$TMPFILE\")))"`
# Overwrite .emacs.d/init.el with the file that is based on the staged changes
mv -f "${TANGLED//\"}" init.el
# Stage the file
git add init.el

.git/hooks/post-commit

#!/bin/sh
# Retangle init.org as it is so all changes are reflected in init.el
emacsclient -e "(let ((enable-local-variables :safe)) (car (org-babel-tangle-config)))"

When I call git commit the pre-commit hook essentially tangles a version of init.org which only reflects the staged changes and then stages the resulting version of init.el so that it’s part of the commit I’m making. See this section of the git revisions documentation for more info on accessing the staged version of a file.

Once I’m done with my commit, the post-commit hook tangles the init.org normally so that all changes are reflected in init.el, committed or not.

As you can see I use emacsclient in both hooks instead of emacs. This makes things much faster since I’m not waiting for my Emacs configuration to be loaded just to tangle the file. Note that this requires that I’m running Emacs as a server, which I do by calling (unless (server-running-p) (server-start)) in my config.

Syncing Custom Set Variables

One pain point I had with using an Org for my config file is I would always end up losing custom-set-variables. This is because custom.el sets those in init.el which I’m constantly overwriting when I tangle init.org. I don’t set a lot of variables this way but for a few packages I use it’s way easier to configure things this way.

To solve this problem I use org-babel-detangle to detangle the changes in init.el back to init.org before tangling in the opposite direction. Since I don’t want this overwriting the changes I make in init.org I have it setup so it only detangles my Custom Set Variables section with the following in init.org:

* Custom Set Variables
:PROPERTIES:
:ID: 1234
:END:

#+begin_src emacs-lisp :comments link
  (custom-set-variables
   ;; custom-set-variables was added by Custom.
   ;; If you edit it by hand, you could mess it up, so be careful.
   ;; Your init file should contain only one such instance.
   ;; If there is more than one, they won't work right.

   ;; Custom set variables here
  )
#+end_src

Then I use the following function to tangle my config file (including in my after-save-hook):

(defun org-babel-tangle-config+ ()
  "Tangle emacs config file.  Uses the following custom logic:

1. Detangle init.el back to org file in order to pick up changes
to custom variables. Should only pick up changes to that block as
that's the only one exported with links enabled.

2. Tangle file with only id type links available. This is a
workaround to prevent git links from being used when in a git
repo."
  (interactive)
  (let ((org-link-parameters '(("id" :follow org-id-open))))
    ;; Read back changes to custom variables in init.el
    (save-window-excursion
      (org-babel-detangle "init.el"))
    (let
        ;; Avoid infinite recursion
        ((after-save-hook (remove 'org-babel-tangle-config+ after-save-hook)))
      (org-babel-tangle-file (concat user-emacs-directory "init.org")))))

As mentioned in the docstring I also limit org-link-parameters to id since my config is in a git directory and I have ol-git-link loaded and git links don’t work properly with org-babel-detangle.

One issue I ran into is org-babel-detangle doesn’t properly handle false positive matches of org-link-bracket-re which is an issue if you have any in your config. I’ve submitted a patch and am working on copyright assignment in order to get it added to Org.