GNU Emacs configuration
Table of Contents
- 1. Installation
- 2. The
early-init.elfile - 3. The
init.elfile- 3.1. Insert lexical binding cookie
- 3.2. Package initialization
- 3.3. Handle Emacs generated files
- 3.4. Disabled commands
- 3.5. Abbreviations
- 3.6. Auto-update
- 3.7. Font
- 3.8. Theme
- 3.9. Mouse
- 3.10. Deletion
- 3.11. Moving between windows
- 3.12. Completion
- 3.13. Bookmarks
- 3.14. Dired
- 3.15. Evil mode
- 3.16. Org mode
- 3.17. Olivetti
- 3.18. Denote
- 3.19. Spell checking
- 3.20. PDF tools
- 3.21. Org-roam
- 3.22. Magit
- 3.23. Transient
- 3.24. Project
- 3.25. Citar
- 3.26. Citar-Denote
- 3.27. Consult
- 3.28. Consult-Denote
- 3.29. Consult Notes
- 3.30. Ledger
- 3.31. Corfu
- 3.32. Language support
- 3.33. Load private configuration
This is my literate Emacs configuration file, created using an approach advocated by Prot (Protesilaos) in his Emacs configuration.
To produce the Emacs Lisp configuration files, the code blocks must be tangled using C-c C-v C-t.
My philosophy is to only install what I currently am using. I have a strong bias for built-in Emacs functionality. When using packages, I prefer GNU ELPA packages, followed by NonGNU ELPA, and finally MELPA. I have cut out a lot of eye-candy and fluff. The resulting configuration is minimalist, which I find pleasing.
I will write up the details of how I converted from my old configuration to this new one in a blog post.
Here is a comparison of the startup time of my new configuration versus the old one, using M-x emacs-init-time:
Old configuration: 6.265956 seconds
New configuration: 1.599190 seconds
1. Installation
This configuration uses as much as possible the default file locations for each package. I have set up my macOS system and my Windows system running under Parallels to use identical file paths, by providing symbolic links in Windows to the macOS file system.
Symbolic links in Windows are created by running the Command Prompt with administrator privileges. The commands are:
- For a File:
mklink <LinkPath> <TargetPath> - For a Folder (Directory):
mklink /D <LinkPath> <TargetPath>
In the earlier configuration, ~/.emacs.d/ was a symbolic link created by GNU Stow to the dotfiles/emacs/ directory, which meant that temporary files, downloaded Lisp code and everything else were under version control, which was not a good idea.
Now the three files, dem-emacs.org, early-init.el and init.el are individually linked to symbolic links in ~/.emacs.d/ using a specific Stow command:
cd ~/dotfiles
stow --no-folding emacs
Thus ~/dotfiles/emacs/.emacs.d/ contains just three files:
~/dotfiles/emacs/.emacs.d $ tree . . ├── dem-emacs.org ├── early-init.el └── init.el
~/.emacs.d/ contains symbolic links to those files, plus directories to contain the files that Emacs uses and that should not be put under version control.
~/.emacs.d $ tree -L 1 . ├── cache ├── dem-emacs.org -> ../dotfiles/emacs/.emacs.d/dem-emacs.org ├── early-init.el -> ../dotfiles/emacs/.emacs.d/early-init.el ├── elpa ├── init.el -> ../dotfiles/emacs/.emacs.d/init.el ├── private └── var
If I want to use my old configuration I can do this:
emacs --init-directory="$HOME/archive/emacs-old"
2. The early-init.el file
2.1. Insert lexical binding cookie
;;; -*- lexical-binding: t; -*-
2.2. Set some defaults
(setq frame-resize-pixelwise t ; makes frame resizing smoother
frame-title-format '("%b") ; just the buffer name
ring-bell-function #'ignore ; ignore function returns nil
use-dialog-box t ; mouse functions use dialogs...
use-file-dialog nil ; ...except for file dialogs
use-short-answers nil ; I don't mind typing long answers
inhibit-splash-screen t
inhibit-startup-screen t
inhibit-startup-buffer-menu t)
2.3. Frame size
Some initial Emacs frame setup, including frame size.
(menu-bar-mode t)
(tool-bar-mode -1)
(scroll-bar-mode -1)
;; We don't set menu-bar-lines to 0 because we keep the menu.
(setq initial-frame-alist `((horizontal-scroll-bars . nil)
(tool-bar-lines . 0)
(vertical-scroll-bars . nil)
(width . (text-pixels . 800))
(height . (text-pixels . 600))
(border-width . 0)))
2.4. Define some directories
;; Define the var directory, for multiple uses
(defconst dem-var-directory
(expand-file-name "var/" user-emacs-directory))
(make-directory dem-var-directory t)
(defconst dem-cache-directory
(expand-file-name "cache/" user-emacs-directory))
(make-directory dem-cache-directory t)
2.5. Auto-save-list
Put auto-save-list in the var/ directory.
;; Create auto-save-list directory
(let ((dir (expand-file-name "auto-save-list/" dem-var-directory)))
(make-directory dir t)
(setq auto-save-list-file-prefix
(expand-file-name ".saves-" dir)))
2.6. Package variable initialization
This should be done in early-init.el, before packages are initialized.
;; Package files installed by package.el
(setq package-user-dir
(expand-file-name "elpa/" user-emacs-directory))
;; Native-compilation cache
(startup-redirect-eln-cache
(expand-file-name "cache/eln/" user-emacs-directory))
3. The init.el file
The major part of the configuration is in this file.
3.1. Insert lexical binding cookie
;;; -*- lexical-binding: t; -*-
3.2. Package initialization
Emacs 31 includes GNU ELPA and NonGNU ELPA repositories by default, so no need to add them. In addition, much of the package initialization code I used to use is no longer necessary or desired. However package.el does need to be loaded to add new package archives.
(require 'package)
(add-to-list 'package-archives
'("melpa" . "https://melpa.org/packages/")
t)
;; Prioritize ELPA repositories over MELPA.
(setq package-archive-priorities
'(("gnu" . 30)
("nongnu" . 20)
("melpa" . 10)))
3.3. Handle Emacs generated files
Emacs generates backup and temporary files. I would like to keep them out of the top-level Emacs configuration directory.
Set the directory to hold backups:
;; Define and create directories for Emacs backup files.
(defconst dem-backup-directory
(expand-file-name "backups/" dem-var-directory))
(make-directory dem-backup-directory t)
;; Configure backup files.
(setq backup-directory-alist
`(("." . ,dem-backup-directory))
version-control t
kept-new-versions 5
kept-old-versions 2
delete-old-versions t
backup-by-copying t)
Set the directory to hold auto-save files:
;; Define and create directory for auto-save files.
(defconst dem-auto-save-directory
(expand-file-name "auto-save/" dem-var-directory))
(make-directory dem-auto-save-directory t)
(setq auto-save-file-name-transforms
`((".*" ,dem-auto-save-directory t)))
Settings saved through the Customize interface are written to custom.el. We want the dem-emacs.org file to be the definitive source of startup packages, so we won’t load custom.el on startup.
;; Setup custom.el but don't load it on startup.
(setq custom-file
(expand-file-name "custom.el" dem-var-directory))
Use savehist-mode to save the minibuffer history. It also uses a directory in var/.
;; Preserve minibuffer history between Emacs sessions.
(setq savehist-file
(expand-file-name "savehist" dem-var-directory))
(savehist-mode 1)
3.4. Disabled commands
Disable some warnings from Emacs for rarely used commands.
(put 'upcase-region 'disabled nil)
(put 'downcase-region 'disabled nil)
3.5. Abbreviations
Store abbreviations in the var/ directory.
(use-package abbrev
:ensure nil
:hook
(text-mode . abbrev-mode)
:custom
(save-abbrevs 'silently)
(abbrev-file-name
(expand-file-name "abbrev_defs" dem-var-directory)))
3.6. Auto-update
Ensure that file changes and directory changes made by external applications are automatically updated in Emacs.
;; Refresh file and non-file buffers when their underlying data changes.
(setq global-auto-revert-non-file-buffers t)
(global-auto-revert-mode 1)
3.7. Font
I have used Source Code Pro in the past, but recently I have tried Prot’s Aporetic Sans Mono and like it. It is present on both my macOS and Windows systems.
(set-frame-font "Aporetic Sans Mono-18" nil t)
3.8. Theme
I think I like the Doric themes the best, but for the moment we will startup with Prot’s standard-themes.
(use-package modus-themes
:ensure t)
(use-package ef-themes
:ensure t)
(use-package doric-themes
:ensure t)
(use-package standard-themes
:ensure t
:demand t
:config
(load-theme 'standard-light-tinted t)) ; preferred theme at the moment
3.9. Mouse
With modern Emacs, the only mouse tweak that is needed is to activate pixel-scroll-precision-mode.
;; Smooth trackpad scrolling.
(pixel-scroll-precision-mode 1)
3.10. Deletion
Deleted files should always go to the trash. Note that Emacs knows how to do this on whatever system it is running. There is no need to define a trash directory.
;; Deleted files and directories go to the trash.
(setq delete-by-moving-to-trash t)
3.11. Moving between windows
Emacs uses old-fashioned terminology, calling what modern interfaces call windows frames, and panels within these frames windows.
I prefer alternatives to C-x o for moving between windows. I change the default keybindings of Windmove from S-<arrow key> to s-<arrow key> (s = super key = command key on macOS). This also avoids a conflict with the movement keys in the Org mode calendar.
(use-package windmove
:ensure nil
:if (eq system-type 'darwin)
:config
(windmove-default-keybindings 'super))
(global-set-key (kbd "C-x C-n") #'other-window)
(global-set-key
(kbd "C-x C-p")
(lambda ()
(interactive)
(other-window -1)))
3.12. Completion
I use modern Emacs completion packages that are all available from GNU ELPA.
(use-package vertico
:ensure t
:init
(vertico-mode 1))
(use-package marginalia
:ensure t
:init
(marginalia-mode 1))
(use-package orderless
:ensure t
:custom
(completion-styles '(orderless basic))
(completion-category-overrides
'((file (styles basic partial-completion)))))
3.13. Bookmarks
Put the bookmark file in ~/.emacs.d/var/. Set up a few named bookmarks.
(use-package bookmark
:ensure nil
:custom
(bookmark-default-file
(expand-file-name "bookmarks" dem-var-directory)))
;; Quick register shortcut to this file's symbolic link
(set-register ?c `(file . ,(expand-file-name "dem-emacs.org" user-emacs-directory)))
3.14. Dired
I make some minor tweaks to Dired, the directory manager for Emacs.
;; Make Dired better behaved.
(use-package dired
:ensure nil
:hook
((dired-mode . dired-hide-details-mode)
(dired-mode . hl-line-mode))
:custom
(dired-dwim-target t)) ; used for copying from one dired buffer to another
3.15. Evil mode
I prefer Evil mode with Vi/Vim keybindings for editing text to Emacs keybindings. However for other Emacs buffers, Evil mode is a poor fit. In the past I tried some compatibility modes or tried selecting the individual buffer types with which I wanted to use Emacs keybindings. However my latest strategy is to just use Evil for modes derived from text-mode and prog-mode, and use pure Emacs for the rest. If I want to do Evil key-binding in those buffers, there is a function dem-toggle-evil-local-mode that is bound to C-c e.
;; Use evil-mode for Vim-like editing in text and prog-mode buffers.
;; Note that I use C-u for scrolling, like in Vim. However,
;; in Insert mode, C-u acts like it does in Emacs, which is handy.
(use-package evil
:ensure t
:init
(setq
evil-want-integration t
evil-want-keybinding nil
evil-want-C-u-scroll t
evil-want-C-i-jump nil
evil-undo-system 'undo-redo)
;; Work around Evil 1.15.0 bug under Emacs 31.
;; Remove when a fixed Evil release reaches GNU/NonGNU ELPA.
(defvar evil-mode-buffers nil)
:config
;; Make vertical movement keys follow visual rather than logical lines.
(define-key evil-normal-state-map [remap evil-next-line] #'evil-next-visual-line)
(define-key evil-normal-state-map [remap evil-previous-line] #'evil-previous-visual-line)
(define-key evil-motion-state-map [remap evil-next-line] #'evil-next-visual-line)
(define-key evil-motion-state-map [remap evil-previous-line] #'evil-previous-visual-line)
(setq-default evil-cross-lines t) ; motion keys go to next line
(defun dem-toggle-evil-local-mode ()
"Toggle Evil in the current buffer."
(interactive)
(if evil-local-mode
(progn
(evil-local-mode -1)
(message "Evil disabled in this buffer"))
(evil-local-mode 1)
(message "Evil enabled in this buffer")))
(global-set-key (kbd "C-c e") #'dem-toggle-evil-local-mode)
(defun dem-enable-evil-for-selected-modes ()
"Enable Evil only in buffers derived from text or programming modes."
(when (derived-mode-p 'text-mode 'prog-mode)
(evil-local-mode 1)))
(add-hook 'after-change-major-mode-hook #'dem-enable-evil-for-selected-modes))
;; use evil-matchit to match more tags with "%"
(use-package evil-matchit
:ensure t
:config
(global-evil-matchit-mode 1))
;; implement number functions
(use-package evil-numbers
:ensure t
:bind
(:map evil-normal-state-map
("C-=" . evil-numbers/inc-at-pt)
("C--" . evil-numbers/dec-at-pt)))
3.16. Org mode
Org mode is a markup language, an organizer, an outliner, and more. I have streamlined my org configuration and will have to see if I need to add features back in.
Note that my org directory ~/org/ is a symbolic link to a Dropbox directory. This link is present both for my Mac and for Windows run on my Mac via Parallels.
Since ~/org/ is the default value of org-directory, there is no need for platform-specific configuration here.
(use-package org
:ensure nil
:init
;; We must define org-directory here even though it is the
;; default, to avoid an uninitialized variable error on startup.
(setq org-directory "~/org"
org-default-notes-file
(expand-file-name "inbox.org" org-directory))
(setq org-agenda-files
'("inbox.org"
"personal.org"
"home.org"
"epstudios.org"
"family.org"
"org.org"))
(setq org-todo-keywords
'((sequence "TODO(t)" "NEXT(n)" "|" "DONE(d!)")
(sequence "PENDING(p)" "WAITING(w@/!)" "HOLD(h@/!)" "SOMEDAY(s@/!)" "|" "CANCELLED(c@/!)")))
(setq org-capture-templates
'(("t" "todo" entry
(file+headline org-default-notes-file "Tasks")
"* TODO %?\n%U\n%a\n")))
:bind
(("C-c l" . org-store-link)
("C-c a" . org-agenda)
("C-c c" . org-capture)
("C-c b" . org-switchb))
:hook
(org-mode . visual-line-mode)
:custom
(org-agenda-include-diary t)
(org-hide-leading-stars t)
(org-startup-indented t)
(org-hide-emphasis-markers t)
(org-log-done 'time)
(org-log-into-drawer "LOGBOOK")
(org-tags-column 0)) ; tags are just after headline
;; Other options
;; to be tested to see if these are indeed useful
;; (org-M-RET-may-split-line '((default . nil)))
;; (org-insert-heading-respect-content t)
Org-appear shows org delimiters when the point is over them, making their editing easier.
;; org-appear-mode shows and hides emphasis delimiters
(use-package org-appear
:ensure t
:hook (org-mode . org-appear-mode))
3.17. Olivetti
This is pleasant “no distractions” mode that I use when typing text on full-screen Emacs.
(use-package olivetti
:ensure t)
3.18. Denote
The default Denote directory is ~/Documents/notes/. This directory exists in macOS and there is symbolic link to the macOS directory in Windows with the same path. So there is no need to set denote-directory here.
(use-package denote
:ensure t
:hook
(dired-mode . denote-dired-mode)
:config
(denote-rename-buffer-mode 1))
;; Oddly, denote seems to lack a function to just open the notes directory.
(defun dem-denote-open-denote-dir ()
"Open the Denote directory."
(interactive)
(require 'denote)
(dired denote-directory))
3.19. Spell checking
I use jinx for spell checking.
(use-package jinx
:ensure t
:hook (text-mode . jinx-mode)
:bind
(("M-$" . jinx-correct)
("C-M-$" . jinx-languages)
("M-n" . jinx-next)
("M-p" . jinx-previous)))
3.20. PDF tools
PDF Tools allows annotation and navigation of PDF files.
(use-package pdf-tools
:ensure t
:init
(pdf-loader-install)
:hook
(pdf-view-mode . pdf-view-roll-minor-mode) ; smoother PDF scrolling
:config
(keymap-set pdf-view-mode-map "C-s" #'isearch-forward)
(keymap-set pdf-view-mode-map "C-r" #'isearch-backward))
(use-package org-pdftools
:ensure t
:after (org pdf-tools)
:hook
(org-mode . org-pdftools-setup-link))
3.20.1. TODO Test on Windows.
PDF tools may not work well on Windows.
3.21. Org-roam
Org-roam was my prior note-taking system, which I have abandoned and replaced by Denote. However I still have lots of Org-roam notes that I’d like someday to transfer to Denote. Until that day arrives, I will continue to need access to the Org-roam notes.
(use-package org-roam
:ensure t
:after org
:custom
(org-roam-directory
(file-truename "~/Documents/org-roam")) ; default is ~/org-roam
(org-roam-db-location
(expand-file-name "org-roam.db" dem-var-directory))
:bind
(("C-c n l" . org-roam-buffer-toggle)
("C-c n f" . org-roam-node-find)
("C-c n i" . org-roam-node-insert))
:config
(setq org-roam-capture-templates
'(("d" "default" plain "%?"
:target
(file+head "%<%Y%m%d%H%M%S>-${slug}.org"
"#+title: ${title}\n#+filetags: ")
:unnarrowed t)
("r" "bibliography reference" plain "%?"
:target
(file+head "references/${citekey}.org"
"#+title: ${title}\n")
:unnarrowed t)))
(setq org-roam-node-display-template
(concat "${title} "
(propertize "${tags}" 'face 'org-tag)))
(org-roam-db-autosync-mode 1))
3.21.1. TODO Make symbolic link in Windows pointing to ~/Documents/org-roam
3.22. Magit
Magit is a useful UI for Git.
(use-package magit
:ensure t
:bind
("C-x g" . magit-status))
3.23. Transient
Transient is built-in and used by Magit. Make it keep its files in the var/ directory.
(use-package transient
:ensure nil
:custom
(transient-history-file
(expand-file-name "transient/history.el" dem-var-directory))
(transient-values-file
(expand-file-name "transient/values.el" dem-var-directory))
(transient-levels-file
(expand-file-name "transient/levels.el" dem-var-directory)))
3.24. Project
I used to use projectile, but the built-in package project caught up in features and is fine. Magit is added to the options when opening a project. We hide the projects file under the var/ directory.
(use-package project
:ensure nil
:init
(setq project-list-file
(expand-file-name "projects.eld" dem-var-directory))
:config
(keymap-set project-prefix-map "m" #'magit-project-status)
(add-to-list 'project-switch-commands
'(magit-project-status "Magit")
t))
3.25. Citar
Citar is useful for providing references. I emulate the identical path in Windows of the relevant files using symbolic links.
(use-package citar
:ensure t
:after org
:custom
(org-cite-global-bibliography
'("~/Documents/Bibtex/My Library.bib"))
(org-cite-csl-styles-dir
(expand-file-name "~/Zotero/styles/"))
(org-cite-insert-processor 'citar)
(org-cite-follow-processor 'citar)
(org-cite-activate-processor 'citar)
(org-cite-export-processors
'((t . (csl "american-medical-association.csl"))))
(citar-bibliography org-cite-global-bibliography)
:bind
(:map org-mode-map
("C-c C-b" . org-cite-insert)))
3.26. Citar-Denote
Citar-Denote allows the creation of bibliographic notes.
(use-package citar-denote
:ensure t
:after (citar denote)
:config
(citar-denote-mode 1))
3.27. Consult
Consult has many features to improve standard Emacs functions.
(use-package consult
:ensure t
:bind
(("M-g M-g" . consult-goto-line)
("C-x b" . consult-buffer)
("M-s r" . consult-ripgrep)
("C-s" . consult-line)))
3.28. Consult-Denote
Consult-Denote integrates Denote with Consult.
(use-package consult-denote
:ensure t
:after (consult denote)
:config
(consult-denote-mode 1))
3.29. Consult Notes
Consult Notes combines Denote and Org-roam files into one searchable list using Consult. At some point if I migrate away from Org-roam, I could eliminate this.
(use-package consult-notes
:ensure t
:custom
(consult-notes-file-dir-sources
'(("Org Roam" ?r "~/Documents/org-roam")
("Bibliographic References" ?b "~/Documents/org-roam/references")
("Denote" ?d "~/Documents/notes"))))
3.30. Ledger
I do home accounting using the Ledger program and Ledger mode. I no longer use Flycheck but instead use Ledger mode’s built-in ledger-flymake. You can use M-x flymake-show-buffer-diagnostics to display an error window. Use M-g M-n and M-g M-p to go to next and previous errors.
One difference from Flycheck is that Flymake only runs when the buffer is saved. This makes it much faster, and since I save often, it’s not an impediment.
Note that you must activate ledger-mode’s built-in flymake functionality with the function ledger-flymake-enable.
(use-package ledger-mode
:ensure t
:custom
(ledger-clear-whole-transactions 1)
:hook
(ledger-mode . ledger-flymake-enable)
:mode
(("\\.ledger\\'" . ledger-mode)
("\\.dat\\'" . ledger-mode)))
3.31. Corfu
Corfu uses Emacs’s built-in completion framework. We are using it in place of company-mode. Completion is set up to be automatic after a short delay, and one can scroll up and down the completion list with arrow keys or M-n and M-p.
(use-package corfu
:ensure t
:custom
(corfu-auto t)
(corfu-cycle t)
:init
(global-corfu-mode 1))
3.32. Language support
I am just adding the minimum syntax highlighting at this point with optional eglot support. For the moment I have only added eglot support to Swift, and it is not on by default. To add it automatically, add eglot-ensure as a hook to swift-mode.
;; Objective-C and Java support are built in.
(use-package cc-mode
:ensure nil)
;; C# has built-in support in modern Emacs.
(use-package csharp-mode
:ensure nil)
(use-package kotlin-mode
:ensure t)
(use-package swift-mode
:ensure t)
(use-package eglot
:ensure nil
:config
(add-to-list 'eglot-server-programs
'(swift-mode . ("sourcekit-lsp"))))
I am working through the Scheme code in the book Structure and Interpretaton of Computer Programs and so need a compatible Scheme language and a REPL. This only works if Racket is installed on the system. I only do this on macOS, so this won’t work on Windows, but that’s not a problem.
(use-package geiser
:ensure t)
(use-package geiser-racket
:ensure t)
3.33. Load private configuration
(load (expand-file-name "private/private.el" user-emacs-directory)
'noerror)
Created: 2026-08-22 Sat 20:40