I’ve always had a natural affinity for the native text-editing interface in Emacs. The shortcuts (called key-bindings in Emacs terminology) and mnemonic patterns make intuitive sense to me. So for nearly 2.5 decades, I happily used Emacs as “just a text editor.”
In late 2023, I decided to immerse myself in the world of Emacs Lisp. This really wasn’t driven by any specific goal; it was simply me encouraging my curiosity. It was around this time that it hit me:
Emacs is not a text editor, it’s a tool construction kit that lets you build any text-based tool you want. It is the embodiment of a philosophy of user empowerment.
As I’ve explored the implications of this idea, my Emacs configuration has evolved significantly. It’s no longer limited to a handful of snippets I’ve cribbed from the internet. It now involves multiple packages, relies on a package repository, leans into powerful systems built upon Emacs (like Magit and Org Mode), and most importantly, adapts Emacs to be exactly what I need it to be.
This file gathers all these ideas in one place, and it aims to be two things simultaneously:
- Literature that is readable and understandable for humans.
- A resource that can be directly exported (
C-c C-v tin Org Mode) to my actual Emacs configuration files.
The structure of this file was inspired by a much richer literate configuration by Prot.
Please write to me if you need any help with how it’s used, or if you’d like to suggest any improvements.
There are some things (mostly pertinent to the base UI) that need to be set really early in Emacs’ startup. This is so that the UI doesn’t first show up uncustomised, and then flash as it redraws based on any later UI customisation (such as a change of theme).
This early initialisation configuration goes into a handily-named file
called early-init.el.
;; -*- lexical-binding: t -*-Depending on your system, there might be some default configuration
shared by other Emacs users in a file called default.el. To ensure
our Emacs behaves consistently everywhere, we ignore this and start
from a blank slate.
(setq inhibit-default-init t)There are a few elements like a graphical menu and a scroll-bar that are useful for beginners when first getting acquainted with Emacs. But as you get more experienced navigating the app with a keyboard, they get less useful. As a more advanced user, I remove them from the UI to let me focus more on the content.
(menu-bar-mode -1)
(tool-bar-mode -1)
(scroll-bar-mode -1)In the snippet above, -1 is convention for disabling the
corresponding mode.
I generally use Emacs on macOS, and this needs me to tweak it a tiny bit to my liking.
To be as fast as possible, modern Emacs byte compiles Emacs Lisp
code into an efficient format for use at runtime. For some reason, on
my Mac, Emacs fails to fully understand where GCC is installed, and
then complains that it cannot find libgccjit.so very many times when
installing packages.
No matter, we help it along by explicitly telling it where to look.
(when (eq system-type 'darwin)
(add-to-list 'exec-path "/opt/local/bin")
(setenv "PATH" (concat "/opt/local/bin:" (getenv "PATH"))))The paths above are hard-coded for MacPorts and you can tweak them for your system if it’s different.
Emacs has two primary modifier keys, the Control key (C) and the
Meta key (M). M is traditionally mapped to Alt on most
keyboards, but on a Mac, Command is so much more comfortable.
(when (eq system-type 'darwin)
(setq mac-command-modifier 'meta
mac-option-modifier 'none))These settings make Emacs’ titlebar blend more seamlessly with the dark theme. Enforcing night mode ensures that the title is in a colour that is visible against the dark titlebar.
(when (eq system-type 'darwin)
(add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))
(add-to-list 'default-frame-alist '(ns-appearance . dark)))As Emacs runs, it allocates memory from the operating system to store the data it needs. When tasks complete, not all of this data needs to persist and can be discarded. To find and reclaim this unused memory, Emacs runs a garbage collection process after:
- a certain number of bytes have been allocated, or
- a certain fraction of the current heap size has been used up
since the previous garbage collection.
The trouble is, the default thresholds for these are quite low, e.g. 800,000 bytes (~800 kB) on a 64-bit computer. This means garbage collection triggers unnecessarily often, causing noticeable pauses that slow things down.
To address this, we follow an approach that traces back to Andrea Corallo. We defer garbage collection entirely during startup and minibuffer activity to keep the experience snappy, then restore it to a reasonable threshold (64 MB) for normal editing.
(setq gc-cons-threshold most-positive-fixnum
gc-cons-percentage 1.0)
(defconst hn/gc-normal-threshold (* 64 1024 1024)) ;; 64MB
(defconst hn/gc-normal-percentage 0.1)
(add-hook 'emacs-startup-hook
(lambda ()
(setq gc-cons-threshold hn/gc-normal-threshold
gc-cons-percentage hn/gc-normal-percentage)))
(add-hook 'minibuffer-setup-hook
(lambda () (setq gc-cons-threshold most-positive-fixnum)))
(add-hook 'minibuffer-exit-hook
(lambda () (setq gc-cons-threshold hn/gc-normal-threshold
gc-cons-percentage hn/gc-normal-percentage)))(defvar hn/file-name-handler-alist-backup file-name-handler-alist)
(setq file-name-handler-alist nil)
(add-hook 'emacs-startup-hook
(lambda ()
(setq file-name-handler-alist
(delete-dups (append file-name-handler-alist
hn/file-name-handler-alist-backup)))));; -*- lexical-binding: t -*-In addition to being configured with the source code in this file,
Emacs can also be configured using a graphical interface (M-x
customize). When using this GUI, the standard behaviour is to persist
these settings directly by editing the default Emacs config file.
The following configuration puts this into its own file, so we can clearly separate these two concepts.
(setq custom-file (locate-user-emacs-file "custom.el"))
(load custom-file 'noerror 'nomessage)In addition to the packages that come built-in with Emacs, there is a lot out there that can add to its functionality. We turn to a popular, community-driven package repository called Melpa to access this goodness.
(require 'package)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)(require 'use-package)
(setq use-package-always-ensure t)In the sections that follow, I use use-package to install, configure
and load multiple packages. Loading all packages at startup can slow
things down significantly (adding multiple seconds to startup time).
To identify which packages are slowing things down, I temporarily
enable statistics collection by adding (setq
use-package-compute-statistics t) at the top of init.el. After
restarting Emacs, I run M-x use-package-report to see a table of
package load times sorted by duration.
Armed with this data, I selectively defer expensive packages so they only load when actually needed. The configurations below reflect this optimisation. I’ve deferred heavy packages to keep startup fast.
I like seeing the current buffer name and the machine I’m on right in the title bar, so I don’t lose track when I have multiple frames or hosts open.
(setq frame-title-format
(concat "%b - emacs@" (system-name)))I prefer a relatively clean and empty state as I start, so the following removes a startup splash screen and brings up an empty plain text buffer, so I can start typing immediately.
(setq inhibit-startup-screen t)
(setq initial-scratch-message "")
(setq initial-major-mode 'text-mode)
(setq default-major-mode 'text-mode);; (setq split-width-threshold 0)On my Mac, I sometimes accidentally trigger zooming of text because I
have my hand on the C key and scroll on the trackpad. The following
configuration ignores this input.
(when (eq system-type 'darwin)
(define-key key-translation-map (kbd "C-<wheel-up>") (kbd "<wheel-up>"))
(define-key key-translation-map (kbd "C-<wheel-down>") (kbd "<wheel-down>"))
(define-key special-event-map [pinch] #'ignore))The basic visual styling of my Emacs is controlled by a highly accessible theme called Modus Vivendi Tritanopia. A version of this theme comes built-in with Emacs, but it is not the most up-to-date version. We fetch the most recent version from the package repository and customise it a little. This is a highly customisable theme with many options, but I try to keep it simple.
(use-package modus-themes
:demand t
:custom
(modus-themes-italic-constructs t)
(modus-themes-bold-constructs t)
(modus-themes-prompts '(bold))
(modus-themes-to-toggle '(modus-operandi-tritanopia modus-vivendi-tritanopia))
(modus-themes-common-palette-overrides
'((border-mode-line-active bg-mode-line-active)
(border-mode-line-inactive bg-mode-line-inactive)))
(modus-themes-headings
'((1 . (1.2))
(2 . (1.1))
(agenda-date . (1.1))
(agenda-structure . (1.2))
(t . (1.0))))
:config
(modus-themes-load-theme 'modus-vivendi-tritanopia)
:bind (("<f5>" . modus-themes-toggle)))(transient-mark-mode 1)
(delete-selection-mode 1)(setq show-paren-delay 0)
(show-paren-mode 1)
(column-number-mode 1)(pixel-scroll-precision-mode 1)(setq-default show-trailing-whitespace t
indicate-empty-lines t
indicate-buffer-boundaries 'right
sentence-end-double-space nil)(setq mouse-drag-copy-region t);; prevent extraneous tabs and use 2 spaces
(setq-default indent-tabs-mode nil
tab-width 2);; enable up- and down-casing
(put 'downcase-region 'disabled nil)
(put 'upcase-region 'disabled nil)Emacs comes with a built-in spell-checker called Flyspell mode that automatically checks your spelling as you type. I have used this for many years, but in large documents it can be very slow and hampers smooth scrolling.
The community has largely moved onto a third party package called =jinx.el=, which is a really fast just-in-time spell-checker, and that’s what I am switching to too.
For this to work, you need a few system packages installed. For me (using MacPorts), these are:
aspellaspell-dict-enenchant2 +aspell
(use-package jinx
:hook ((text-mode . jinx-mode)
(prog-mode . jinx-mode))
:bind ([remap ispell-word] . jinx-correct)
:custom
(jinx-languages "en_GB"))The minibuffer is the small interface at the bottom of the Emacs window where you can enter commands, input parameters, see results of these commands and so on.
;; Vertical completion interface
(use-package vertico
:custom
(vertico-resize t)
:init
(vertico-mode))
;; Rich annotations in the minibuffer
(use-package marginalia
:init
(marginalia-mode))
;; Flexible matching
(use-package orderless
:custom
(completion-styles '(orderless basic))
(completion-category-defaults nil)
(completion-category-overrides '((file (styles partial-completion)))))
;; Persist minibuffer history across sessions
(use-package savehist
:ensure nil
:init
(savehist-mode))
;; Track recently opened files
(use-package recentf
:ensure nil
:init
(recentf-mode)
:custom
(recentf-max-saved-items 100));; In-buffer completion popup
(use-package corfu
:custom
(corfu-cycle t)
(corfu-preselect 'prompt)
(corfu-scroll-margin 5)
(corfu-separator ?\s)
:init
(global-corfu-mode)
:config
(corfu-popupinfo-mode))
(use-package emacs
:ensure nil
:custom
(tab-always-indent 'complete)
(completion-cycle-threshold 3)
(enable-recursive-minibuffers t)
(read-extended-command-predicate #'command-completion-default-include-p)
(minibuffer-prompt-properties
'(read-only t cursor-intangible t face minibuffer-prompt)))(setq c-default-style "bsd")
(setq-default c-basic-offset 2)
(setq-default sgml-basic-offset 2);; setup tree-sitter
(use-package tree-sitter
:config
(global-tree-sitter-mode)
(add-hook 'tree-sitter-after-on-hook #'tree-sitter-hl-mode))
(use-package tree-sitter-langs
:ensure t
:after tree-sitter);; configure a development environment for python
(use-package python
:hook ((python-mode . eglot-ensure)
(python-mode . tree-sitter-hl-mode)))(use-package geiser
:defer t
:custom
(geiser-active-implementations '(mit guile))
:config
(setenv "DISPLAY" ":0")
(add-hook 'geiser-repl-mode-hook 'hn/disable-trailing-whitespace-and-empty-lines))
(use-package geiser-guile
:defer t
:custom
(geiser-guile-binary "/opt/local/bin/guile"))
(use-package geiser-mit
:defer t
:custom
(geiser-mit-binary "/Users/harish/Applications/mit-scheme/bin/mit-scheme")
:config
(setenv "MITSCHEME_HEAP_SIZE" "100000")
(setenv "MITSCHEME_LIBRARY_PATH" "/Users/harish/Applications/mit-scheme/lib/mit-scheme-svm1-64le-12.1")
(setenv "MITSCHEME_BAND" "mechanics.com"))(use-package tex
:ensure auctex
:defer t
:hook (tex-mode . auto-fill-mode))(use-package go-mode :defer t)
(use-package julia-mode :defer t)
(use-package php-mode :defer t)
(use-package markdown-mode :defer t)
(use-package yaml-mode :defer t)
(use-package graphviz-dot-mode :defer t)(add-to-list 'auto-mode-alist '("\\.m\\'" . octave-mode))(use-package magit
:defer t)(setq ediff-split-window-function 'split-window-horizontally)
(setq ediff-window-setup-function 'ediff-setup-windows-plain)TODO: Consider https://github.com/minad/org-modern
(setq org-edit-src-content-indentation 0)
(use-package org-bullets
:hook (org-mode . org-bullets-mode))(org-babel-do-load-languages
'org-babel-load-languages
'((scheme . t)))
(defun hn/org-confirm-babel-evaluate (lang body)
(not (string= lang "scheme")))
(setq org-confirm-babel-evaluate #'hn/org-confirm-babel-evaluate)(global-set-key (kbd "C-c a") 'org-agenda)
;; consider https://github.com/minad/org-modern
(setq org-agenda-files '("~/Notes/todo.org"))(setq org-export-with-smart-quotes t)
(use-package htmlize
:defer t)(use-package unfill)(use-package gptel
:defer t);; (use-package mastodon
;; :config (setq mastodon-instance-url "https://hachyderm.io/"
;; mastodon-active-user "harish"))These are specific to my needs, and are likely not useful for other
people. They are prefixed with my initials, hn/.
(defun hn/journal-todo (start-date end-date &optional prefix)
"Generate a todo list for journal entries from START-DATE to END-DATE with an optional PREFIX."
(interactive
(list
(read-string "Enter start date (YYYY-MM-DD): ")
(read-string "Enter end date (YYYY-MM-DD): ")
(read-string "Enter prefix: " "Write a journal entry for ")))
(let* ((start-time (date-to-time start-date))
(end-time (date-to-time end-date))
(one-day (seconds-to-time 86400)) ; 24 hours * 60 minutes * 60 seconds
(current-time start-time))
(while (time-less-p current-time (time-add end-time one-day))
(let ((entry-date (format-time-string "%A %d-%m-%Y" current-time)))
(insert (format "%s%s\n" (or prefix "** Write entry for ") entry-date)))
(setq current-time (time-add current-time one-day)))))(defun hn/disable-trailing-whitespace-and-empty-lines ()
"Disable showing trailing whitespace and indicating empty lines in the current buffer."
(setq-local show-trailing-whitespace nil)
(setq-local indicate-empty-lines nil))Moving from tree-sitter package to Emacs 30’s built-in treesit with treesit-auto for automatic grammar installation.
(defun prot/keyboard-quit-dwim ()
"Do-What-I-Mean behaviour for a general `keyboard-quit'.
The generic `keyboard-quit' does not do the expected thing when
the minibuffer is open. Whereas we want it to close the
minibuffer, even without explicitly focusing it.
The DWIM behaviour of this command is as follows:
- When the region is active, disable it.
- When a minibuffer is open, but not focused, close the minibuffer.
- When the Completions buffer is selected, close it.
- In every other case use the regular `keyboard-quit'."
(interactive)
(cond
((region-active-p)
(keyboard-quit))
((derived-mode-p 'completion-list-mode)
(delete-completion-window))
((> (minibuffer-depth) 0)
(abort-recursive-edit))
(t
(keyboard-quit))))
(define-key global-map (kbd "C-g") #'prot/keyboard-quit-dwim)straight integrates well with use-package and replaces the
internal packaging system.
- We also need some way to use
vertico-mouse. - Consider adding consult, embark, embark-consult, wgrep and cape.
(delete-by-moving-to-trash t)