~/.emacs.d

1. Installation

This whole Emacs configuration, including the configuration file and the included packages is a Nix derivation. By installing Emacs through Nix, the editor, its packages and the configuration are bundled together in a single bundle. This allows for quick installs and reproducable builds.

As an example, to try out this Emacs configuration without affecting the rest of your system, run the following command. This downloads and compiles Emacs, including packages and the configuration, and starts the resulting Emacs.app.

nix run github:jeffkreeftmeijer/.emacs.d

1.1. Building Emacs from Git with Nix

Instead of using the stable Emacs from Nixpkgs, this configuration uses emacs-overlay to build Emacs from its master branch. The overlay updates daily, but this configuration only updates sporadically, when there’s reason to do so, to keep everything as stable as possible.

I’m inclined to use a stable version of Emacs instead of building from Git, as I’m not specifically looking to be on the bleeding edge. However, newly added features tend to pull me in.

Currently, there are two reasons I’m currently running on a prerelease version:

  1. Emacs master updated its included version of the modus-themes, including the tinted variants of modus-vivendi and modus-operandi, which are my preferred themes. Running Emacs master therefor requires one less dependency.
  2. Completion-preview.el, a fish-like completion-at-point package, was merged into master in e82d807a2845673e2d55a27915661b2f1374b89a.

To build a Nix derivation that intalls Emacs from Git using Emacs-overlay, import nixpkgs, and then apply the overlay from a tarball. Then, return pkgs.emacs-git:

{ pkgs ? import <nixpkgs> {
  overlays = [
    (import (builtins.fetchTarball {
      url = https://github.com/nix-community/emacs-overlay/archive/f7fcac1403356fd09e2320bc3d61ccefe36c1b91.tar.gz;
    }))
  ];
} }:

pkgs.emacs-git

In this example, the version of emacs-overlay (and thus Emacs itself) is locked to a specific version. To use the latest version, replace the revision hash with a branch name like master.

Assuming the derivation is saved to a file named emacs-git.nix, it can be built through nix build:

1.2. Enabling XWidgets in Emacs on macOS with Nix

Nix disables the withXwidgets option for Emacs on macOS, so simply enabling it won’t work yet:

{ pkgs ? import <nixpkgs> {
  overlays = [
    (import (builtins.fetchTarball {
      url = https://github.com/nix-community/emacs-overlay/archive/f7fcac1403356fd09e2320bc3d61ccefe36c1b91.tar.gz;
    }))
  ];
} }:

pkgs.emacs-git.overrideAttrs(old: {
  withXwidgets = true;
})

In the meantime, circumvent Nix’s option by manually adding the build flag. As expected, enabling XWidgets also requires the WebKit framework as a build input:

{ pkgs ? import <nixpkgs> {
  overlays = [
    (import (builtins.fetchTarball {
      url = https://github.com/nix-community/emacs-overlay/archive/f7fcac1403356fd09e2320bc3d61ccefe36c1b91.tar.gz;
    }))
  ];
} }:

pkgs.emacs-git.overrideAttrs(old: {
  buildInputs = old.buildInputs ++ [
    pkgs.darwin.apple_sdk.frameworks.WebKit
  ];

  configureFlags = old.configureFlags ++ ["--with-xwidgets"];
})

1.3. Applying Emacs Plus patches

Emacs Plus is a Homebrew formula to build Emacs on macOS, which applies a couple of patches while building. First, download the patches for the correct Emacs version. In this case, get the patches for Emacs 30:

curl https://raw.githubusercontent.com/d12frosted/homebrew-emacs-plus/master/patches/emacs-30/system-appearance.patch -o patches/system-appearance.patch
curl https://raw.githubusercontent.com/d12frosted/homebrew-emacs-plus/master/patches/emacs-30/round-undecorated-frame.patch -o patches/round-undecorated-frame.patch
curl https://raw.githubusercontent.com/d12frosted/homebrew-emacs-plus/master/patches/emacs-30/poll.patch -o patches/poll.patch
curl https://raw.githubusercontent.com/d12frosted/homebrew-emacs-plus/master/patches/emacs-28/fix-window-role.patch -o patches/fix-window-role.patch

Then, override the attributes in pkgs.emacs-git when using emacs-overlay—or pkgs.emacs when building Emacs from Nixpkgs—to add all path files to the package’s patches list:

{ pkgs ? import <nixpkgs> {
  overlays = [
    (import (builtins.fetchTarball {
      url = https://github.com/nix-community/emacs-overlay/archive/f7fcac1403356fd09e2320bc3d61ccefe36c1b91.tar.gz;
    }))
  ];
} }:

pkgs.emacs-git.overrideAttrs(old: {
  patches = old.patches ++ [
    ./patches/system-appearance.patch
    ./patches/round-undecorated-frame.patch
    ./patches/poll.patch
    ./patches/fix-window-role.patch
  ];
})

Assuming the derivation is saved to a file named emacs-patched.nix, it can be built through nix build:

nix build --file emacs-patched.nix
open /result/Applications/Emacs.app

1.4. Emacs with bundled configuration

The emacsWithPackagesFromUsePackage function parses configuration files in search of packages to bundle with Emacs. For example, to package Emacs with Evil and enable evil-mode on startup, add a use-package statement as the emacs configuration:

{ pkgs ? import <nixpkgs> {
  overlays = [
    (import (builtins.fetchTarball {
      url = https://github.com/nix-community/emacs-overlay/archive/f7fcac1403356fd09e2320bc3d61ccefe36c1b91.tar.gz;
    }))
  ];
} }:

pkgs.emacsWithPackagesFromUsePackage {
  package = pkgs.emacs-git;
  config = ''
  (use-package evil
    :ensure t
    :init
    (evil-mode 1))
  '';
  defaultInitFile = true;
}

Assuming the derivation is saved to a file named emacs-enil.nix, it can be built through nix build:

nix build --file emacs-evil.nix
open /result/Applications/Emacs.app

1.5. Configured Emacs

By combining the features in Emacs overlay, this configuration produces configured Emacs, a version of Emacs with macOS-specific patches applied, XWidgets enabled, packages installed and a full configuration loaded. The included configuration file is default.el, which is generated from the rest of this configuration.

{ pkgs ? import <nixpkgs> {
  overlays = [
    (import (builtins.fetchTarball {
      url = https://github.com/nix-community/emacs-overlay/archive/f7fcac1403356fd09e2320bc3d61ccefe36c1b91.tar.gz;
    }))
  ];
} }:

pkgs.emacsWithPackagesFromUsePackage {
  package = (
    pkgs.emacs-git.overrideAttrs(old: {
      patches = old.patches ++ [
        ./patches/system-appearance.patch
        ./patches/round-undecorated-frame.patch
        ./patches/poll.patch
        ./patches/fix-window-role.patch
      ];

      buildInputs = old.buildInputs ++ [
        pkgs.darwin.apple_sdk.frameworks.WebKit
      ];

      configureFlags = old.configureFlags ++ ["--with-xwidgets"];
    })
  );

  config = ./default.el;
  defaultInitFile = true;
}

2. Appearance

2.1. Frames

Disable the scroll bar, the tool bar, and the menu bar:

(scroll-bar-mode -1)
(tool-bar-mode -1)
(menu-bar-mode -1)

2.2. Fonts

Use Iosevka as a monospace font (fixed in Emacs lingo), and Iosevka’s “Aile” variant as a (quasi-)proportional font (variable-pitch in Emacs lingo).

Both variants are used with their regular weights, expanded widths, and a height of 150 (15 points × 10):

(defun jk/set-face-font (face family)
  (set-face-attribute
   face nil
   :family family :weight 'regular :width 'expanded :height 150))

(jk/set-face-font 'default "Iosevka")
(jk/set-face-font 'fixed-pitch "Iosevka")
(jk/set-face-font 'variable-pitch "Iosevka Aile")

The face-font-family-alternatives variable provides fallback fonts if the preferred fonts aren’t available. This produces a font list akin to CSS font-families, starting with the preferred font and falling back to an option that is most likely to be available on any system. Having a list of fallback fonts like this removes the need to explicitly depend on fonts being available.

This configuration falls back to Apple’s SF Mono and SF Pro if the Iosevka fonts aren’t available. Since the Apple fonts need to be downloaded explicitly, they aren’t more likely to be there than the Iosevka ones, but they’re included as they were the previous favorite.

If the SF fonts aren’t available, the fixed font falls back to Menlo before the default monospace font (which is most likely Courier). The variable pitch font falls back to SF Pro, Helvetica, and finally Arial:

(custom-set-variables
  '(face-font-family-alternatives
  '(("Iosevka" "SF Mono" "Menlo" "monospace")
    ("Iosevka Aile" "SF Pro" "Helvetica" "Arial"))))

2.3. Variable pitch

To use proportional fonts (as opposed to monospaced fonts) for non-code text, enable variable-pitch-mode for selected modes. While this mode is enabled, the default font face inherits from variable-pitch instead of fixed-pitch.

An often-recommended approach is to hook into text-mode, which is the major mode most text-based modes inherit from:

(add-hook 'text-mode-hook #'variable-pitch-mode))

Doing so automatically enables variable-pitch-mode thenever text-mode is enabled.

This works, but it’s a bit too eager for my liking. The above configuration enables variable-pitch-mode when editing Org files, but also when writing commit messages and editing YAML files. I consider text in the latter two as code, so I’d prefer to have those displayed in a monospace font.

Instead of hooking into text-mode, explicitly select the modes to use proportional fonts in Org and Markdown mode:

(add-hook 'org-mode-hook #'variable-pitch-mode)
(add-hook 'markdown-mode-hook #'variable-pitch-mode)

2.4. Themes

The Modus themes are a set of beautiful and customizable themes, which are shipped with Emacs since version 28.

The modus themes consist of two types: Modus Operandi is a light theme, and Modus Vivendi is its dark counterpart. The tinted variants shift the background colors from white and black to a more pleasant light ochre and dark blue.

When using the version of the Modus themes that’s included in Emacs, the themes need to be explicitly required using require-theme:

(require-theme 'modus-themes)

To select modus-operandi-tinted as the default theme, load it with the load-theme function:

(load-theme 'modus-operandi-tinted)

An interactive function named modus-themes-toggle switches between the light and dark themes. By default, the function switches between the non-tinted versions, but that can be overwritten to use the tinted versions through the modus-themes-to-toggle variable:

(setq modus-themes-to-toggle '(modus-operandi-tinted modus-vivendi-tinted))

2.4.1. Switching between dark and light mode

Auto-dark automatically switches between dark and light themes based on the operating system’s appearance.

(auto-dark-mode 1)

It uses the wombat and leuven themes by default, but these are configured to use the modus themes with the auto-dark-light-theme and auto-dark-dark-theme variables.

(setq (auto-dark-light-theme 'modus-operandi-tinted)
(auto-dark-dark-theme 'modus-vivendi-tinted))

With auto-dark in place, Emacs’ theme can be switched by toggling the system-wide dark mode instead of using modus-themes-toggle. The jk/dark and jk/light functions run an apple script to turn dark mode on and off from Emacs:

(defun jk/dark ()
  "Switch to macOS' dark appearance."
  (interactive)
  (do-applescript
   "tell application \"System Events\"
  tell appearance preferences
    set dark mode to true
  end tell
end tell"))

(defun jk/light ()
  "Switch to macOS' light appearance."
  (interactive)
  (do-applescript
   "tell application \"System Events\"
  tell appearance preferences
    set dark mode to false
  end tell
end tell"))

2.4.2. Customization

The Modus themes can optionally inherit from the fixed-pitch face for some faces, which allows for turning on variable-pitch-mode while keeping some text monospaced. To turn it on, set modus-themes-mixed-fonts, but make sure it’s set before loading one of the modus themes:

(setq modus-themes-mixed-fonts t)

The Modus themes come with the option to use italic and bold constructs, which is turned off by default. Enabling produces italic type for comments and contextual information, and bold type in syntax highlighting.

(setq
 modus-themes-italic-constructs t
 modus-themes-bold-constructs t)

Note that any configuration options to the themes themselves need to happen before the theme is loaded, or the theme needs to be reloaded through load-theme after setting the customizations.

2.5. Layout

The spacious-padding package adds spacing around windows and frames, as well as padding the mode line.

Turn on spacious-padding-mode to add spacing around windows and frames:

(spacious-padding-mode 1)

Turn on spacious-padding-subtile-mode-line for a more subtile mode line:

(setq spacious-padding-subtle-mode-line t)

3.1. Evil mode

Emacs is the best Vim emulator, and Evil is the best Vim mode. After installing Evil, turn on evil-mode globally:

Instead of enabling Evil’s gloval evil-mode hook, turn it on per buffer. By hooking into both prog-mode and text-mode, Evil mode is only turned on for programming and text editing buffers.

(add-hook 'prog-mode-hook 'turn-on-evil-mode)
(add-hook 'text-mode-hook 'turn-on-evil-mode)

3.2. Evil-commentary

Evil-commentary is an Evil port of vim-commentary which adds key bindings to call Emacs’ built in comment-or-uncomment-region function. Turn it on by calling evil-commentary-mode:

(evil-commentary-mode 1)

3.3. Cursors

An example of an essential difference between Emacs and Vim is how they handle the location of the cursor (named point in Emacs). In Vim, the cursor is on a character, while Emacs’ point is before it. In Evil mode, the cursor changes between a box in “normal mode” to a bar in “insert mode”. Because Emacs is always in a kind of insert mode, make the cursor a bar:

(setq-default cursor-type 'bar)

4. Completion

4.1. Vertical completion

Vertico is a vertical completion library, based on Emacs’ default completion system.

(vertico-mode 1)

4.2. Contextual information

Marginalia adds extra contextual information to minibuffer completions. For example, besides just showing command names when executing M-x, the package adds a description of the command and the key binding.

(marginalia-mode 1)

4.3. Enhanced navigation commands

Consult provides enhancements to built-in search and navigation commands. There is a long list of available commands, but this configuration mostly uses Consult for buffer switching with previews.

  1. Replace switch-to-buffer (C-x b) with consult-buffer:

    (global-set-key (kbd "C-x b") 'consult-buffer)
    
  2. Replace project-switch-to-buffer (C-x p b) with consult-project-buffer:

    (global-set-key (kbd "C-x p b") 'consult-project-buffer)
    
  3. Replace goto-line (M-g g and M-g M-g) with consult-goto-line:

    (global-set-key (kbd "M-g g") 'consult-goto-line)
    (global-set-key (kbd "M-g M-g") 'consult-goto-line)
    
  4. Replace project-find-regexp (C-x p g) with consult-ripgrep:

    (global-set-key (kbd "C-x p g") 'consult-ripgrep)
    

4.4. Pattern matching

Orderless is a completion style that divides the search pattern in space-separated components, and matches regardless of their order. After installing it, add it as a completion style by setting completion-styles:

(setq completion-styles '(orderless basic))

4.5. Minibuffer actions

Embark adds actions to minibuffer results. For example, when switching buffers with switch-to-buffer or consult-buffer, pressing C-. opens Embark’s list of key bindings. From there, you can act on results in the minibuffer. In this exampke, pressing k kills the currently selected buffer.

(global-set-key (kbd "C-.") 'embark-act)

4.6. Minibuffer history

Emacs’ savehist feature saves minibuffer history to ~/emacs.d/history. The history is then used to order vertical completion suggestions.

(savehist-mode 1)

4.7. Completion at point

Emacs 30 includes completion-preview.el, since e82d807a2845673e2d55a27915661b2f1374b89a, which adds grayed-out completion previews while typing, akin to the autocomplete in the Fish shell.

(global-completion-preview-mode 1)

5. Development

5.1. Major modes

5.1.1. Treesitter

The treesit-auto package automatically installs and uses the tree-sitter equivalent of installed major modes. For example, it automatically installs and uses rust-ts-mode when a Rust file is opened and rust-mode is installed.

To turn it on globally, enable global-treesit-auto-mode:

(global-treesit-auto-mode 1)

To automatically install missing major modes, enable treesit-auto-install. To have the package prompt before installing, set the variable to 'prompt:

(custom-set-variables
  '(treesit-auto-install 'prompt))

5.1.2. Additional major modes

In addition to the list of already installed major modes, this configuration adds adds more when they’re needed1.

5.1.2.1. beancount-mode

Bencount-mode requires hooking up the mode manually, so enable it for each file with a .beancount extension:

(use-package beancount
  :ensure t
  :mode ("\\.beancount\\'" . beancount-mode))
5.1.2.2. dockerfile-mode
(use-package dockerfile-mode
  :ensure t)
5.1.2.3. elixir-mode
(use-package elixir-mode
  :ensure t)
5.1.2.4. git-modes
(use-package git-modes
  :ensure t)
5.1.2.5. markdown-mode

There is currently no Emacs major mode for MDX, so enable Markdown-mode for files with a .mdx extension:

(use-package markdown-mode
  :ensure t
  :mode ("\\.mdx\\'" . markdown-mode))
5.1.2.6. nix-mode
(use-package nix-mode
  :ensure t)
5.1.2.7. rust-mode
(use-package rust-mode
  :ensure t)
5.1.2.8. typescript-mode
(use-package typescript-mode
  :ensure t)
5.1.2.9. yaml-mode
(use-package yaml-mode
  :ensure t)

5.2. Environments

Programming environments set up with Nix and direnv alter the environment and available programs based on the current directory. To provide access to programs on a per-directory level, use the Emacs direnv package:

(direnv-mode 1)

5.3. Language servers

Eglot is Emacs’ built-in Language Server Protocol client. Language servers are added through the eglot-server-programs variable:

(add-to-list 'eglot-server-programs '((rust-ts-mode rust-mode) "rust-analyzer"))
(add-to-list 'eglot-server-programs '((elixir-ts-mode elixir-mode) "elixir-ls"))
(add-to-list 'eglot-server-programs '((nix-mode) "nixd"))

Start eglot automatically for Nix an Rust files:

(add-hook 'nix-mode #'eglot-ensure)
(add-hook 'rust-mode #'eglot-ensure)
(add-hook 'rust-ts-mode #'eglot-ensure)

5.3.1. Automatically format files on save in Eglot-enabled buffers

The eglot-format-buffer function doesn’t check if Eglot is running in the current buffer. This means hooking using it as a global after-save-hook produces errors in the echo area whenever a file is saved while Eglot isn’t enabled:

(jsonrpc-error
 "No current JSON-RPC connection"
 (jsonrpc-error-code . -32603)
 (jsonrpc-error-message . "No current JSON-RPC connection"))

To remedy this, add a function that formats only when Eglot is enabled.

(defun jk/maybe-format-buffer ()
  (when (bound-and-true-p eglot-managed-p)
    (eglot-format-buffer)))

This function is then added as a global after-save-hook.

(add-hook 'after-save-hook 'jk/maybe-format-buffer)

Now, with the hook enabled, any Eglot-enabled buffer is formatted automatically on save.

6. Version control

Magit is a user interface for Git in Emacs. Even after years of using Git from the console, it’s the quickest way to use Git, and it’s one of the most sophisticated Emacs packages.

An interesting thing about Magit is that it doesn’t have many configuration options. It doesn’t need any, as it’s a great experience out of the box.

(use-package magit
  :ensure t)

7. Shell

7.1. Terminal emulation

Use Eat (Emulate A Terminal) as a terminal emulator. If Eat prints “garbled” text, run M-x eat-compile-terminfo, then restart the Eat buffer.

Aside from starting the terminal emulator with M-x eat and M-x eat-project, Eat adds terminal emulation to Eshell with eat-eshell-mode. This allows Eshell to run full screen terminal applications.

(eat-eshell-mode 1)

Because Eat now handles full screen terminal applications, Eshell no longer has to run programs in a term buffer. Therefor, the eshell-visual-commands list can be unset.

(setq eshell-visual-commands nil)

Now, an application like top will run in the Eshell buffer without a separate term buffer having to be opened.

7.2. History

Atuin is a cross-shell utility that stores shell history in a SQLite database. The eshell-atuin package adds support for both reading from and writing to the history from Eshell.

(eshell-atuin-mode)

To read the history in Eshell, bind the <up> key to eshell-atuin-history, which opens the shell history in the minibuffer. Also unset the <down> key, which was bound to eshell-next-input for cycling through history in reverse:

(keymap-set eshell-hist-mode-map "<up>" 'eshell-atuin-history)
(keymap-unset eshell-hist-mode-map "<down>")

By default, eshell-atuin only shows commands that completed succesfully. To show all commands, change the eshell-atuin-search-options variable from ("--exit" "0") to nil:

(setq eshell-atuin-search-options nil)

Shell history completion is different from other kinds of completion for two reasons:

  1. Other completion options are presented in a list from top to bottom, with the search prompt at the top. Because eshell-atuin-history is opened by pressing the <up> key and history is searched backward, the list is reversed by using vertico-reverse.
  2. The command history shouldn’t be ordered, as that’s already handled by Atuin. Instead of ordering the list again, pass identity as the vertico-sort-function.

Using vertico-multiform, which is enabled through vertico-multiform-mode, set the above options specifically for the eshell-atuin-history function:

(vertico-multiform-mode 1)
(setq vertico-multiform-commands
      '((eshell-atuin-history
         reverse
         (vertico-sort-function . identity))))

8. Dired

(dirvish-override-dired-mode)

9. Org

9.1. Note-taking

I’m trying out org-node, a just-released alternative to org-roam, my current note-taking solution. Currently, this configuration uses both packages.

9.1.1. Org-node

Org-node is not on any of the package repositories yet. This configuration doesn’t ensure the package is there, so it’s assumed it’s installed manually. I’ve installed org-node through package-vc-install for now.

Enable org-node by calling org-node-enable whenever an org-mode is enabled:

(add-hook 'org-mode-hook #'org-node-enable))

9.1.2. Org-roam

Org-roam stores notes in org-roam-directory, which is ~/org-roam by default. Use ~/notes instead:

(setq org-roam-directory (file-truename "~/notes"))

9.1.3. Org-roam-ui

Org-roam-ui is a graphical frontend for Org-roam, which displays all nodes in a graph for browsing the directory of nodes and discovering possible missing links.

9.2. Task management

Beorg is an iOS app that takes Org mode to iOS. It includes a list of tasks named inbox that’s synced via iCloud, meaning it can be added to the agenda through org-agenda-files.

(setq org-agenda-files '("/Users/jeff/Library/Mobile\ Documents/iCloud\~com\~appsonthemove\~beorg/Documents/org/inbox.org"))

9.3. Modern defaults for Org exports

Org files can be can be exported to other formats, like HTML. Due to backwards compatibility constraints, however, the produced documents have an xhtml-strict doctype with syntax to match. Luckily, Org’s exporters are endlessly configurable, and include support for more modern configurations.

9.3.1. Smart quotes

Automatically convert single and double quotes to their curly equivalents, depending on the document language.

(setq org-export-with-smart-quotes t)

9.3.2. Entities

Disable entities, like using &ldquo; instead of “ in HTML. This option only works for entities included in the document, not the entities added through smart quotes.

(setq org-export-with-entities nil)

9.3.3. Headline levels

Instead of 3, set the maximum headline level to 5. This matches the HTML standard of having six headline levels, when counting the document title as the first, leaving five.

(setq org-export-headline-levels 5)

9.3.4. Table of contents and section numbers

Disable both the table of contents and section numbers, as they’re easily turned on when needed, not needed for most exports, and not present in the source documents.

(setq
 org-export-with-toc nil
 org-export-section-numbers nil)

9.3.5. HTML 5

Aside from replacing the doctype in the document, setting org-html-doctype to html5 has modernizing effects on the output file. For example, it uses the charset attribute (as opposed to http-equiv) to set the character set, it drops the XML declaration from the header of the document, it switches to the HTML5 validator for the footer (which is then disabled later), and disables HTML table attributes2. Setting the doctype instantly transports the document from the start of the millenium to last decade.

To enable the HTML5 doctype , set the org-html-doctype variable:

(setq org-html-doctype "html5")

9.3.6. “Fancy” HTML tags

To continue modernizing, enable org-html-html5-fancy for fancy HTML5 elements. This means <figure> tags to wrap images, a <header> tag around the file’s main headline, and a <nav> tag around the table of contents. It also enables HTML5-powered special blocks to produce modern HTML elements from Org’s special blocks:

#+begin_aside
  An aside.
#+end_aside

Exports to:

<aside>
  An aside.
</aside>

To enable HTML5 “fancy” tags, set the org-html-html5-fancy variable:

(setq org-html-html5-fancy t)

9.3.7. Containers

Aside from the modern elements already enabled by the HTML5 doctype and org-html-html5-fancy, Org allows for more customizations to its HTML exports. Use org-html-container-element and org-html-divs to replace some of the standard <div> elements with HTML 5 alternatives:

  1. Use the <section> element instead of the main section <div> elements
  2. Use the <header> element to wrap document preambles
  3. Use the <main> element to wrap the document’s main section
  4. Use the <footer> element to wrap document postambles
(setq
 org-html-container-element "section"
 org-html-divs '((preamble  "header" "preamble")
                (content   "main" "content")
                (postamble "footer" "postamble")))

9.3.8. Summary

To configure Org mode’s HTML exporter to output HTML 5 with modern elements, set the following configuration.

(setq
 org-export-with-smart-quotes t
 org-export-with-entities nil
 org-export-headline-levels 5
 org-export-with-toc nil
 org-export-section-numbers nil
 org-html-doctype "html5"
 org-html-html5-fancy t
 org-html-container-element "section"
 org-html-divs '((preamble  "header" "preamble")
                (content   "main" "content")
                (postamble "footer" "postamble")))

When using use-package for configuration, hook into the ox-org package an use the :custom keyword.

(use-package ox-org
  :custom
  org-export-with-smart-quotes t
  org-export-with-entities nil
  org-export-headline-levels 5
  org-export-with-toc nil
  org-export-section-numbers nil
  org-html-doctype "html5"
  org-html-html5-fancy t
  org-html-container-element "section"
  org-html-divs '((preamble  "header" "preamble")
                (content   "main" "content")
                (postamble "footer" "postamble")))

9.4. Source code

One of Org’s most impressive features is source code evalutation through its Library of Babel. Babel can both evaluate (run a source code block from within an Org document) and extract (take a source code block from an Org document and place it in another file) code.

9.4.1. Evaluation

By default, Org only evaluates Emacs Lisp code, but other languages can be added via org-babel-load-languages:

(setq
 org-babel-load-languages '((emacs-lisp . t)
                            (shell . t)))

9.4.2. Extraction

Org extracts each code block that has a “tangle” attribute whenever the org-babel-tangle function is evaluated. It’s bound do C-c C-v t by default. However, it’s convenient to have code blocks tangled automatically when the source document is saved.

Automatic source code tangling can be enabled per-document by adding a document header line:

# -*- eval: (add-hook 'after-save-hook #'org-babel-tangle nil t); -*-

For documents where this header can’t be added, or situations where the header hasn’t been added yet, there’s a package named org-auto-tangle.

(add-hook 'org-mode-hook #'org-auto-tangle-mode)

The org-auto-tangle package automatically extracts code blocks for every document that has the #+auto_tangle: t option. To turn it on for all Org documents regardless, set org-auto-tangle-default:

(setq org-auto-tangle-default t)

10. Email

Use notmuch.el to read email.

11. Enhancements

This section covers general enhancements to Emacs which don’t warrant their own section.

11.1. Backups

Emacs automatically generates backups for files not stored in version control. Instead of storing them in the files’ directories, put everything in ~/.emacs.d/backups:

(setq backup-directory-alist `(("." . "~/.emacs.d/backups")))

11.2. Key suggestions

With which-key, Emacs shows suggestions when pausing during an incomplete keypress, which is especially useful when trying to learn Emacs’ key bindings. By default, Emacs only shows the already-typed portion of the command, which doesn’t help to find the next key to press.

(which-key-mode 1)

11.3. Projects

By default, project.el only takes projects into account that have a .git directory. Use project-x to allow for projects that are not under version control, and projects nested within other projects.

Project-x is not on any of the pacakge managers, so this configuration assumes it’s installed manually for now. Also, this configuration re-sets project-find-functions to try project-x-try-local before project-try-vc to make it work for projects nested within directories under version control.

(project-x-mode 1)
(setq project-find-functions '(project-x-try-local project-try-vc))

With project-x enabled, Emacs will recognise directories with a .project file as project directories.3

11.4. Precise scrolling

Added in Emacs 29, pixel-scroll-precision-mode enables smooth scrolling instead of scrolling line by line.

(pixel-scroll-precision-mode 1)

11.5. Indentation

Don’t use tabs for indentation.

(indent-tabs-mode 0)

12. Benchmarking

Use benchmark-init to benchmark Emacs’ initialization. Enable benchmark-init at the top of the configuration file, before any packages are loaded.

(use-package benchmark-init
  :ensure t
  :config
  (add-hook 'after-init-hook 'benchmark-init/deactivate))

After starting Emacs, the benchmarking results can be examined using the benchmark-init/show-durations-tree function. Aside from that, append the total duration to a file named ~/.emacs.d/benchmark.csv for future reference.

(write-region
 (format "%s,%s\n"
         (string-trim (shell-command-to-string "git --git-dir ~/emacs-config/.git rev-parse HEAD"))
         (benchmark-init/node-duration-adjusted benchmark-init/durations-tree))
 nil
 "~/.emacs.d/benchmark.csv"
 'append)

  1. I’d rather not worry about installing major modes and use a package like vim-polyglot, but I haven’t been able to find an equivalent for Emacs. ↩︎
  2. The easiest way to find out what each of these options does is to locate where the predicate functions are called in ox-html.el in Org’s source code. For example, to find out what changing the doctype to HTML5 does, search for org-html-html5-p.

    ↩︎
  3. Apparently, project.el now supports identifying projects based on a special file in its directory root. Project-x should be obsolete for this purpose, but I haven’t figured it out yet.

    ↩︎