My Emacs Configuration

TLDR, take me to the code already!

This page contains a HTMLized version of my Emacs configuration. Sharing configs has become somewhat of a thing in the Emacs-sphere and I vividly remember how helpful and inspiring simply looking at the online configs of other Emacsers was during the days when I got started.

Most people share their set-up using public GitHub repos, but I do something slightly different here. As you will see below (and as some people already know; this is not the first iteration of me uploading my config to my website), I save my Emacs config as an org-mode literate program. This means I get to export the plain text sources to presentable and browsable HTML.

You can find the links to the actual lisp code below. The rest of this page goes on about what I want out of a "good" Emacs config, why I chose the particular way of doing things that I chose, and how this was all built and how it works.



I started using Emacs in 2015 (of course I blogged about it. I had time back then). Initially, I used a plain init.el file and dumped all my customizations into it. Some time after this, I re-structured my customizations and moved towards an org-mode literate program-style config. This style is nice for further compartmentalization and commenting of your code, and makes it incredibly easy to export to HTML like this page.

I hear of a lot of people declaring "init.el bankruptcy". While I understand what they mean, I'd rather take an optimistic standpoint and claim that I do large overhauls of my set-up whenever I have both a) found a "better" way of doing things and b) have the time to do it. During my last "large overhaul", I really wanted to make sure to get the right set-up for me and my daily computing needs. It took a while (approx. 2 years) for me to write about this set-up, but I think that it might be interesting for some Emacsers out there. I also haven't moved away from this style of config yet, which I think is a testament to me getting it right enough.

OK, so what are my requirements for a "good" config, or why am I building a rube-goldberg-esque Emacs init file? Let's talk about this.


Time is money. If you accept this statement or at least some version of it, you realize that time spent fixing random bugs introduced by updates in one of your Emacs packages is lost money. MELPA is a great resource for Emacs packages, but if I understand it correctly, it essentially packages the latest commit or release of a package directly from GitHub. This, of course, is a recipe for disaster for people attempting to get any kind of work done.

Ideally, I'd like to avoid random updates and instead would like to introduce some notion of control over which version of a package runs in my Emacs and when I update. I'd also like to be able to roll back updates and as a bonus, want to be able to back up the sources of all packages I am using. So what are my options?

  • There is MELPA Stable, but AFAIK it does not have the same selection of packages as, for example, MELPA.
  • There are alternatives to the built-in package.el, for example straight.el. It looks like a great project. But the manual is incredibly long, it seems like an incredibly "different" way to handle packages, and I am frankly intimidated by it's apparent complexity.
  • There is the famous git-submodule method of package management which shifts the burden of dependency management to me, the user. (no references, but I remember trying and not liking it at all)
  • John Wiegley solves this problem by using Nix, apparently.
  • Another option would be to use your OS's package manager, like pacman + PKGBUILDs in my case. This is also too much work.

Looking at this list, I just couldn't find a solution that fit the bill. I needed something close enough to how package.el works, with control over package versions, but without having to spend "too much" time thinking about packages.

Flexibility, availability, reproducibility?

I thought hard about the title of this bullet (not really). I think the concept that I am looking for here is some combination of the three. This is best explained using some examples.

Like most computer users, my main machine is a laptop. Laptops and the hardware within them break incredibly easily. Over the years, I've had my fair share of SSD failures, motherboard failures, and other hardware issues. Other things to consider are more pedestrian problems like having the bag containing your laptop stolen, or wrecking a car and breaking your machine in the process.

A topic only tangentially related to the fragility of laptop computers is the migration of a lot of dev environments to remote servers. As an example, for a few years, my main development machine was a beefy GPU machine buried in some server rack at the university somewhere. In addition, a lot of people now write code in VMs on AWS or other rather transient environments.

What do these things have in common? In both situations, I want to be able to get started writing code as quickly as possible, ideally a quick curl | bash away. In other words, I want the "hard truth" of my emacs configuration sit somewhere in the cloud where I can access it from anywhere in the world and I want the machine that I install it to to be ready to rock'n'roll right away.

What does this mean for an Emacs config? It means that it needs to be as self-contained and machine-agnostic as possible, much in the spirit of the "portable" programs that we were running off of USB drives in the early 2000s.

I wasn't able to find any pre-packaged solutions for this problem, so of course I rolled my own. Before we get to that, let's briefly talk about my last requirement.


Like all Emacs fiends, I like trying shiny new things packages (it is not lost on me that this somewhat conflicts with my first requirement). So I want to be able to maintain the quick, fire-up-package.el-and-download-this-thing-fresh-from-MELPA approach for discovering and playing with Emacs packages.

So, what gives? After deliberating my requirements for a while, I realized that I can meet all of them with very simple tools, most of them included in Emacs already! Let's dive in to my overall approach.

How does this work?

This thing really consists of two parts:

The actual "configuration files" part of my config consists of a set of org files, where each contains one "component" or "layer" as I named them when I started this (shoutouts to spacemacs). You can check them out below. Some of these are longer than others, and sometimes there is less rhyme and reason to it than I'd like. But as we all know… customizing Emacs is a life-long process.

The "magic" happens behind the scenes: The files containing my config are actually all sitting on the same VPS that is hosting this site and are backed up and version controlled using a private GitHub repository. I edit these files remotely using either TRAMP or a bare-bones Emacs on the server itself.

This "remote Emacs" is also the one I use to download and update packages from sources like ELPA and MELPA. I then use elpa-mirror to create a "mirror" of all the packages on the web server and serve it as my private ELPA repository using the same nginx that I am using to serve the website you are looking at. This private ELPA repository (and all other config-related code) is protected using HTTP basic auth to give me some false sense of privacy. Finally, I use a set of two bash scripts to plumb everything together:

  1. A script named bundle takes a snapshot of the current state of the config (on the server), tangles all org files into one init.el, creates a tarball "bundle" of the entire .emacs.d directory, and puts it in my web server's webroot. This bundle contains my configuration at that point in time and in case anything goes wrong, I can just switch to the previous bundle which still sits in my webroot. This neatly checks off the "reproducibility" requirement.
  2. My server also serves the script, which is designed to be curlbashed onto a fresh machine to install the bundle into the correct place and byte-compile the init.el file. During the initial byte-compilation, package.el is pointed at my private ELPA repo sitting on the same server to download all of my packages. I thus only ever install the set of packages that I mirror myself (and, in theory, have vetted myself). No more randomly pulling stuff from across the net. This nicely satisfies my "stability" as well as my "flexibility" requirements.

All this stuff might be best explained with the following cartoon:


Almost forgot: Since I want to be mostly self-contained, I also distribute a copy of LLVM (which I use for clangd) on my server and automatically download and set it up inside of .emacs.d as part of the curl | bash process. This essentially gives me a full-featured Linux C++ IDE in "just" one line of bash on a fresh machine:

curl -s -u https://username:[email protected]/emacs/ | bash -s ~/.emacs.d/

And that's it! Also, just in case you are asking: The emacs script also contains a quick hack to only download the init.el and byte-compile it for the case where I don't need to download the full bundle. This is actually my main workflow of adding to or manipulating my config: First I try some lisp code out locally and make sure it works. Then I add it to one of the config files on the server, before commiting it to version control and making a new bundle. Finally, I curlbash the changes back to my machine.

This is slightly more involved than before, but it lets me sleep at night, so I'll take it and claim that it fulfills my "customizability" requirement just fine. In case that this is not enough: For some extra machine- or user-specific stuff, my config loads a small file called ~/.emacs-site.el if it exists in the filesystem.

And… That's really mostly it. Nothing revolutionary, but I also haven't seen anyone else do it this way. Might be inspiring for some. That's why I figured I'd write it all down for you all to read.

Finally, if you are interested in how I am turning a set of org files containing my Emacs config to this collection of websites, see the section on Exporting below for some actual code.

With all that said, the next section has the links to individual components of the config. Enjoy!


This section contains links to the individual components of the config. They are ordered in the way I tangle them into the final init.el (I think). This stuff is very much always in flux, so don't expect any consistency here. I am just changing stuff when I feel like it or when I have the time. I have largely converged on the set of tools and packages I need, so some of this stuff might seem a little behind the times.

Also, there is a lot of noweb sprinkled around in these files, so don't get confused when a lisp block contains references like <<do.mail/html>> or something like that. It usually just refers to a code block that I am printing with comments somewhere else in the file. Noweb has helped to compartmentalize some code, for example to add multiple code blocks under the same use-package form.

So here you go:

And that's it! The rest of the page contains a bunch of code listings that you can read if you're interested, I guess?

Lots of code listings


This section contains the plumbing code I referred to the description section.

# Author: Dennis Ogbe <[email protected]>

# set-up and preliminaries

# bounce when there are errors
set -e

# for debug
#set -v
#set -x

# make everything relative to the emacs/ directory
BASED="$(cd $(dirname "$0") && pwd)"

# options
function print_usage {
    echo "Usage: $(basename $0)"
    echo ""
    echo "    Create a new emacs config bundle"
    echo ""
if [[ "$1" = "--help" ]] || [[ "$1" = "-h" ]]; then
    exit 0

# tangling

# tangle all source files into one big list of forms
TMPD=$(mktemp -d)
mkdir -p "$BUNDLE_DIR"
function tangle {
    [[ "$#" != 2 ]] && return 1
    emacs -Q -nw -l org --batch --eval "(org-babel-tangle-file \"$1\" \"$2\")"

# the first file gets tangled without extras
echo ";; $LAYER layer starts here" >> $INITEL
tangle "$BASED/config/$" "$TMPD/$LAYER.el"
cat "$TMPD/$LAYER.el" > $INITEL
echo ";; $LAYER layer ends here" >> $INITEL

# these layers need some massaging
LAYERS=(minimal faces org terminal snippets mode-line completion latex references lsp cpp octave scheme julia python matlab misc mail)
for LAYER in "${LAYERS[@]}"; do
    tangle "$BASED/config/$" "$TMPD/$LAYER.el"
    echo ";; $LAYER layer starts here" >> $INITEL
    echo "(when (member '$LAYER do.layers/enabled-layers)" >> $INITEL
    if [[ "$LAYER" = "mail" ]]; then
        echo "(when (executable-find \"mu\")" >> $INITEL
        # add four spaces of indentation
        sed -e 's/^/    /g' "$TMPD/$LAYER.el" >> $INITEL
        echo ")" >> $INITEL
        # add two spaces of indentation
        sed -e 's/^/  /g' "$TMPD/$LAYER.el" >> $INITEL
    echo ")" >> $INITEL
    echo ";; $LAYER layer ends here" >> $INITEL

# compile the bundle
sed -i -e 's/[[:space:]]*$//' "$INITEL" # drop trailing whitespace
rsync -rv "$BASED/random/" "$TMPD/bundle/random"
rsync -rv "$BASED/snippets/" "$TMPD/bundle/snippets"
cp "$BASED/customize.el" "$TMPD/bundle"

# package and save the bundle
HASH=$(cd $BASED && git rev-parse --short HEAD)
OUTFILE="$(cd $HOME/config/webroot && pwd)/dennis-emacs-$HASH.tgz"
INIT_OUT="$(cd $HOME/config/webroot && pwd)/init-$HASH.el"
(cd "$TMPD/bundle" && tar -czf "$OUTFILE" *)
(cp "$INITEL" "$INIT_OUT")
echo ""
echo "Success. Output is in $OUTFILE"
[[ -d "$TMPD" ]] && rm -rf "$TMPD"
[[ -h "$BUNDLE_LINK" ]] && unlink "$BUNDLE_LINK" && cd "$HOME/config/webroot" && ln -s $(basename "$OUTFILE") $(basename "$BUNDLE_LINK")
[[ -h "$INIT_LINK" ]] && unlink "$INIT_LINK" && cd "$HOME/config/webroot" && ln -s $(basename "$INIT_OUT") $(basename "$INIT_LINK")

# cp "$BASED/" "$HOME/config/webroot" # just symlink it

# Bootstrap my emacs distribution
# Author: Dennis Ogbe <[email protected]>

# preliminaries
set -e    # always bounce as soon as something goes wrong
NPROC="1" # check for number of CPU cores for faster build
which nproc >/dev/null 2>&1 && NPROC=$(nproc)

# where is my source of truth?

# a wrapper for which
have_tool() {
  if which $1 >/dev/null 2>&1; then
    return 0
    echo "$1 not found."
    return 1

# how to use this?
print_usage() {
  echo "Usage:"
  echo "   curl -s -u <user>:<password> $REPO/ | bash -s <destination|init-only> [optional args]"
  echo ""
  echo "   where <user> and <password> are your auth, and <destination> the"
  echo "   folder where to put your emacs distribution."
  echo ""
  echo "   optional arguments:"
  echo "     --no-cpp               - don't install the cpp layer (rtags, ycmd, etc)"
  echo "     --no-mail              - don't enable the mail layer (mu4e)"
  echo "     --emacs /path/to/emacs - specify path to emacs executable"
  echo ""
  echo "   to only install the init file use 'init-only' instead of <destination>."
  echo "     init-only              - only update and compile the init file"
  echo ""
  echo "   dependencies: tar, curl, emacs"
  echo "     if mail: mu/mu4e"

# get relative path from absolute path
relpath () {
  [ $# -ge 1 ] && [ $# -le 2 ] || return 1
  [ "$target" != . ] || target=/
  [ "$current" != . ] || current=/
  while appendix="${target#"$current"/}"
        [ "$current" != '/' ] && [ "$appendix" = "$target" ]; do
    if [ "$current" = "$appendix" ]; then
      echo "${relative#/}"
      return 0
  echo "$relative"

# test args (--no-cpp, --no-mail, --emacs)
# first command line argument is destination
[ $# -lt 1 ] && \
  print_usage && exit 1\
      || DESTDIR="$1"
# now check for other arguments
while [ $# -gt 0 ]; do
  case "$1" in
      shift # past arg
      shift # past arg
      shift # past arg
      shift # past val
      exit 0

# small hack
if [[ "$DESTDIR" = "init-only" ]]; then
    echo "Updating and compiling init file..."
    INIT_LOC=$(readlink -f "$HOME/.emacs.d/init.el")
    curl -u "$AUTH" "$REPO/init.el" > "$INIT_LOC"
    $EMACS --batch -l "$INIT_LOC" --eval="(byte-compile-file \"$INIT_LOC\")"
    echo "Done."
    exit 0

[ -a "$DESTDIR" ] && echo "Destination $DESTDIR exists, choose another." && exit 1
! mkdir -p "$DESTDIR" && echo "Error creating Destination $DESTDIR. Aborting" && exit 1
# get the absolute path of everything
DESTDIR=$(cd "$DESTDIR" && pwd)

# log all output
exec > >(tee -a "$DESTDIR/build.log") 2>&1

# test deps
# tar, curl, emacs, if cpp: make, cmake, python headers (python-dev python3-dev)
! have_tool tar && exit 1
! have_tool curl && exit 1
! [ $($EMACS -Q -nw --batch --no-site-file --eval="(message \"$TRUE\")" 2>&1) = $TRUE ] && \
  echo "Could not start emacs. Aborting." && exit 1

# download the archive and unpack
echo "Downloading distribution bundle..."
mkdir -p "$DESTDIR"
curl -u "$AUTH" "$REPO/emacs.tgz" | tar -xzf - -C "$DESTDIR" && echo "Done."

# download and compile packages / byte-compile the init file (experimental)
$EMACS --batch -l "$DESTDIR/init.el" \
       --eval="(byte-compile-file \"$DESTDIR/init.el\")"

# install a site file if it does not exist
if [ ! -a "$SITEFILE" ]; then
  LAYERS="minimal faces org terminal snippets mode-line completion latex octave scheme julia python matlab misc"
  [ "$CPP" = "$TRUE" ] && LAYERS="${LAYERS} cpp"
  [ "$MAIL" = "$TRUE" ] && LAYERS="${LAYERS} mail"

  cat <<EOF > "$SITEFILE"
;;; Emacs site-file
;; Some local settings

;; layers
(setq do.layers/enabled-layers

;; enable the light theme
(setq do.theme/enabled-theme 'dark)
;;(setq do.theme/enabled-theme 'light)

;; set the matlab location
(setq my-matlab-version "R2016b")
(setq matlab-shell-command
      (concat (file-name-as-directory "/data/MATLAB")
              my-matlab-version "/bin/matlab"))

(setq do.matlab/mlint-program
      (concat (file-name-as-directory "/data/MATLAB")


# done


The code blocks in this section contain the lisp code used to export and create the website you are currently looking at. Every time I update my config, I evaluate the last block below and Emacs converts my config files into a set of browsable HTML files for your viewing pleasure. I'm just putting this here because a) it needs to be somewhere and b) might as well show all of the setup

Let's go ahead then. To publish this part of the configuration (remember, this never leaves the web server…), we need to build up an org-publish-project-alist. We start by letting emacs know where to find the source files and where to put them. The "true" paths are redacted.

(setq emacs-config-base-dir "path/to/base")
(setq emacs-config-config-dir "path/to/configuration")
(setq emacs-config-www-dir "path/to/webroot")

Some misc. configuration comes next. The blocks below essentially ensure that the exported HTML looks and feels just like a regular page of my website.

(setq emacs-config-html-head
      ;; point to the css and favicons of the main site
          "<link rel='stylesheet' href='../res/fonts.css' />\n" ; fonts
          "<link rel='stylesheet' href='../res/code.css' />\n" ; code highlighting
          "<link rel='stylesheet' href='../res/main.css' />\n" ; main css
          "<link rel='stylesheet' href='../res/blog.css' />\n" ; blog style
          "<link rel='shortcut icon' href='../img/favicon.ico'>\n" ; favicon
          "<link rel='alternate' type='application/rss+xml' title='RSS Feed for' href='../blog.xml' />\n"))

(setq emacs-config-local-mathjax
      '((path "/mathjax/tex-chtml.js")
        (scale "100") (align "center") (indent "2em") (tagside "right") (autonumber "AMS")
        (mathml nil)))
(setq emacs-config-extra-mathjax-config
MathJax = {
  tex: {
    inlineMath: [['$', '$'], ['\\\\(', '\\\\)']]
  svg: {
    fontCache: 'global'

(setq emacs-config-website-header
      ;; extract the <header> element from index.html
      (let ((index-file (concat (file-name-as-directory emacs-config-www-dir) "../index.html")))
          (insert-file-contents index-file)
          (when (re-search-forward "\\(<header>.*\\(?:\n.*\\)*?</header>\\)")
            (match-string-no-properties 1)))))

(setq emacs-config-config-header
      ;; add an "UP" button to the navbar for sub-config pages
        (insert emacs-config-website-header)
        (goto-char (point-min))
        (re-search-forward "<a.*Writings</a>")
        (insert "\n<a href=\"/emacs/emacs.html#Components\">UP</a>")

(defun emacs-config-mangle-names (project-plist)
  "Change the name of the top emacs config page to 'emacs.html'"
  (let ((pubdir (file-name-as-directory (plist-get project-plist :publishing-directory))))
    (rename-file (concat pubdir "README.html")
                 (concat pubdir "emacs.html")

(setq org-export-html-coding-system 'utf-8-unix)
(setq org-html-htmlize-output-type 'css)

;; massage org-time-stamps
(setq org-time-stamp-custom-formats '("%B %d %Y" . "%A, %B %d %Y, %H:%M"))
(defun my-org-export-ensure-custom-times (backend)
  (setq-local org-display-custom-times t))
(add-hook 'org-export-before-processing-hook 'my-org-export-ensure-custom-times)

This next block sets up the project alist to convert my configuration source files to HTML.

(setq org-publish-project-alist
         :components ("config-main-page" "config-pages"))
         :base-directory ,emacs-config-base-dir
         :publishing-directory ,emacs-config-www-dir
         :exclude ".*"
         :include ("")
         :htmlized-source t
         :publishing-function org-html-publish-to-html
         :completion-function emacs-config-mangle-names

         :with-author t
         :with-creator nil
         :with-date t
         :headline-level 5
         :section-numbers nil
         :with-toc nil
         :with-drawers nil
         :with-sub-superscript nil

         :html-link-up ""
         :html-link-home ""
         :html-home/up-format ""
         :html-head nil
         :html-head-extra ,emacs-config-html-head
         :html-head-include-default-style nil
         :html-head-include-scripts nil
         :html-mathjax-options ,emacs-config-local-mathjax
         :html-mathjax-template ,(concat emacs-config-extra-mathjax-config "<script type=\"text/javascript\" src=\"%PATH\"></script>")
         :html-footnotes-section "<div id='footnotes'><!--%s-->%s</div>"
         :html-preamble ,emacs-config-website-header
         :html-postamble nil
         :auto-sitemap nil)
         :base-directory ,emacs-config-config-dir
         :publishing-directory ,emacs-config-www-dir
         :htmlized-source t
         :publishing-function org-html-publish-to-html

         :with-author t
         :with-creator nil
         :with-date t
         :headline-level 5
         :section-numbers nil
         :with-toc nil
         :with-drawers nil
         :with-sub-superscript nil

         :html-link-up ""
         :html-link-home ""
         :html-home/up-format ""
         :html-head nil
         :html-head-extra ,emacs-config-html-head
         :html-head-include-default-style nil
         :html-head-include-scripts nil
         :html-mathjax-options ,emacs-config-local-mathjax
         :html-mathjax-template "<script type=\"text/javascript\" src=\"%PATH\"></script>"
         :html-footnotes-section "<div id='footnotes'><!--%s-->%s</div>"
         :html-preamble ,emacs-config-config-header
         :html-postamble nil
         :auto-sitemap nil)))

Finally, this next code block, when evaluated, exports the project. It pulls all of the above blocks in using noweb.

(require 'org)


(org-publish "config" t)


I track changes in the config in a private GitHub repo, this list just counts any major revisions of this document:

  • April 24 2020: Initial version
  • December 29 2022: Fix minor typos, the config has changed roughly 4x since the initial post. So it seems like I am doing major upgrades on a roughly half-year cadence.


I think the right thing to do here is to place all of the config-related code on this page and on the config pages linked to by this page in the public domain. So I am attempting to do this with the unlicense.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.


For more information, please refer to <>