GNU Emacs configuration

GNU Emacs configuration

GNU Emacs 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)

Author: David Mann

Created: 2026-08-22 Sat 20:40

Validate