Emacs: a basic and capable configuration

I have helped several people set up a basic Emacs configuration. In the process, I have learnt about some of the problems they have with the out-of-the-box experience.

What follows is a rather simple, yet fully capable, setup to get started with Emacs. At the end of the file is the code block with the complete configuration (The complete configuration). The other sections explain the rationale for each component.

Use this as a starting point. If you are curious about my own setup, check this: https://protesilaos.com/emacs/dotemacs.

Table of Contents

  1. Where to store the configurations
  2. Put all auto-generated configurations in a separate file
  3. Set up the package manager
  4. Set up use-package
  5. Do not show those confusing warnings when installing packages
  6. Delete the selected text upon text insertion
  7. Make C-g a bit more helpful
  8. Decide what to do with the graphical bars
  9. Use the preferred fonts
  10. Choose a theme and tweak the looks of Emacs
  11. Use icon fonts in various places
  12. Configure the minibuffer and related
  13. Tweak the dired Emacs file manager
  14. The complete configuration

Where to store the configurations

Emacs provides lots of possibilities, starting with where to put the main configuration file. In the interest of not overwhelming the user, I am being opinionated with certain choices:

  • Use ~/.emacs.d/init.el for your configurations.
  • If there is an ~/.emacs file on your system, make sure to delete it (or rename it if you care about its contents). Otherwise, Emacs will prioritise that over your ~/.emacs.d/init.el.

Put all auto-generated configurations in a separate file

Emacs has a graphical interface for modifying the variables that are intended for user configuration. Instead of writing Emacs Lisp code, the user clicks on buttons and fills in forms. This interface is accessed in a variety of ways, such as from the menu bar or with the command M-x customize.

When the user makes a modification in this way and requests to save it, Emacs will append a code block to the user’s configuration file. Putting this code in a separate file makes it easier to reason about what is written by the user and what is generated by the running session. We thus add the following to the configuration file (The complete configuration):

(setq custom-file (locate-user-emacs-file "custom.el"))
(load custom-file :no-error-if-file-is-missing)

Here we defined the value of the variable custom-file to be what the function call (locate-user-emacs-file "custom.el") returns. That function finds where the init.el is, gets the directory path, and then returns a new path consisting of that directory plus the name we give it. So it will return ~/.emacs.d/custom.el because of ~/.emacs.d/init.el (Where to store the configurations).

Set up the package manager

Emacs has a basic, yet sufficiently capable, package manager. We use it to extend Emacs with functionality that is provided by the community. There are a lot of useful features built into Emacs, but without the external packages we would be worse off. Also, bear in mind that a package being built into Emacs does not necessarily make it better than third-party alternatives. I do prefer some of the latter even though Emacs has similar functionality built-in.

Start by adding the following to the configuration file (The complete configuration):

(require 'package)
(package-initialize)

(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/"))

Those three lines do the following:

  • Load the package manager.
  • Initialise the installed packages.
  • Add the MELPA repository to list of known package archives. MELPA is a community-driven project that contains lots of useful packages that are not available elsewhere. The core Emacs maintainers do not want to talk about it for political reasons. I disagree with them: we are already understaffed and underfunded. I will not throw away the excellent work of hundreds of developers.

The function call (package-initialize) is technically not necessary, as Emacs can handle this requirement in a different way. In the interest of simplicity, I am leaving it here.

Set up use-package

With use-package, users have a powerful tool for configuring packages in a streamlined way (The complete configuration). Concretely, the user does not need to write a lot of repetitive code, figure out how to load packages when they are needed, installed missing dependencies, and the like. Emacs version 29 has use-package built-in, but earlier versions need to install it manually.

(when (< emacs-major-version 29)
  (unless (package-installed-p 'use-package)
    (unless package-archive-contents
      (package-refresh-contents))
    (package-install 'use-package)))

I explain use-package at greater length in this video: https://protesilaos.com/codelog/2024-07-23-emacs-use-package-essentials/.

Do not show those confusing warnings when installing packages

When installing a new package, Emacs will show a buffer that contains any warnings produced by the byte compiler (Set up the package manager). While this information is useful for developers, it is highly confusing for users. Many times I have received messages along the lines of “my Emacs just broke”, when in truth the warnings are about issues that are of no concern to the user.

The following snippet will prevent those buffers from popping up (The complete configuration). They are still available in the buffer list, if they are needed.

(add-to-list 'display-buffer-alist
             '("\\`\\*\\(Warnings\\|Compile-Log\\)\\*\\'"
               (display-buffer-no-window)
               (allow-no-window . t)))

Delete the selected text upon text insertion

Virtually every program out there will delete the selected/highlighted text as soon as the user types something. Emacs does not do this by default, even though it has the functionality available. Let us then enable it (The complete configuration):

(use-package delsel
  :ensure nil ; no need to install it as it is built-in
  :hook (after-init . delete-selection-mode))

Here we are activating the delete-selection-mode as soon as Emacs is initialised. In other words, it is available when Emacs starts.

Make C-g a bit more helpful

Having observed beginners struggle with C-g not closing the open minibuffer, I know that the following is a quality-of-life refinement:

(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)

This defines a new command (i.e. an interactive function that we can call with M-x or a key binding). This command provides a superset of the functionality provided by the generic keyboard-quit command, which is bound to C-g by default.

Decide what to do with the graphical bars

Emacs has a menu bar and tool bar by default. The menu bar is helpful to discover more functionality and access some commonly used commands. The tool bar is less useful, as it contains commands that are already in the menu bar. The following snippet contains the code for those two bars as well as the scroll bar. A value of 1 enables the functionality, while -1 disables it. Tweak those accordingly (The complete configuration):

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

I personally disable all three of them.

If you are interested in a technically more correct setup for those three, then know that Emacs will read an ~/.emacs.d/early-init.el file (Where to store the configurations). This file is evaluated before the initial Emacs frame is drawn, so you might prefer to disable all those graphical elements at the outset. Still, in the interest of simplicity, I suggest you keep everything in the init.el until you are a bit more experienced.

Use the preferred fonts

Fonts are an integral part of the text-centric Emacs experience (The complete configuration). With the following snippet, we configure the three “faces” that are used to specify font families. Emacs has the concept of a “face” for a bundle of text properties that include typographic properties (font family, font height, font weight, …) and colours (text/foreground colour, background colour).

(let ((mono-spaced-font "Monospace")
      (proportionately-spaced-font "Sans"))
  (set-face-attribute 'default nil :family mono-spaced-font :height 100)
  (set-face-attribute 'fixed-pitch nil :family mono-spaced-font :height 1.0)
  (set-face-attribute 'variable-pitch nil :family proportionately-spaced-font :height 1.0))

The default face is the only one that must have an absolute :height value. Everything else uses a floating point, which is understood as a multiple of the default.

Change the above snippet to use the preferred font family names. Also adjust the default height to a larger/smaller number.

Choose a theme and tweak the looks of Emacs

I am the author of tens of themes for Emacs. Among them is the modus-themes package, which is also available built-in to the core Emacs distribution. Themes are a personal choice, however, so please do not consider the following as a strong opinion (The complete configuration). I simply pick the theme that most people I have interacted with have chosen as their preferred one (a close second is the ef-dream theme from my ef-themes package). Use this as a template to eventually set up another theme.

(use-package modus-themes
  :ensure t
  :config
  (load-theme 'modus-vivendi-tinted :no-confirm-loading))

Use icon fonts in various places

Continuing with the stylistic refinements to Emacs, the following snippet will show complementary icons in the minibuffer (Configure the minibuffer and related) and in Dired (Tweak the dired Emacs file manager). To make this setup work, the user must type M-x and then call the command nerd-icons-install-fonts. This will store the icon font files in a local directory (on Linux this is ~/.local/share/fonts). To be sure everything is working, a restart to Emacs will guarantee that the new font files are read.

(use-package nerd-icons
  :ensure t)

(use-package nerd-icons-completion
  :ensure t
  :after marginalia
  :config
  (add-hook 'marginalia-mode-hook #'nerd-icons-completion-marginalia-setup))

(use-package nerd-icons-corfu
  :ensure t
  :after corfu
  :config
  (add-to-list 'corfu-margin-formatters #'nerd-icons-corfu-formatter))

(use-package nerd-icons-dired
  :ensure t
  :hook
  (dired-mode . nerd-icons-dired-mode))

Configure the minibuffer and related

[ Also watch my video about the modern minibuffer packages, some of which I do not include here because they are a bit more advanced: https://protesilaos.com/codelog/2024-02-17-emacs-modern-minibuffer-packages/. ]

The minibuffer is a central part of the Emacs experience. It is where the user interacts with Emacs to respond to prompts, switch to another buffer, open a new file, run a command by its full name, and so on. The default minibuffer is minimal, which might be good for experienced users, but does not help with discoverability. Instead of that, we use the vertico package to produce a vertical layout. This makes it easier to see what the available options are.

(use-package vertico
  :ensure t
  :hook (after-init . vertico-mode))

The marginalia package is a nice complement to the vertical layout, as it uses the free space to show helpful information about the options shown there. For example, when the user types M-x to see a list of command names, Marginalia will add a brief description of each command. Depending on the specifics of the minibuffer interaction (opening a file, selecting a buffer, …), there will be the relevant information on display.

(use-package marginalia
  :ensure t
  :hook (after-init . marginalia-mode))

The orderless package offers a life-saver for all those cases where we do not remember the exact order of words. For example, to toggle the display of line numbers in the current buffer, we use the command M-x display-line-numbers-mode. With orderless set up, we may type something like li num dis at the M-x prompt and still get the desired result. This is because Orderless matches the space-separated characters we provide in any order. Emacs has other pattern matching styles built-in, but orderless is a good place to start. We thus make sure the other relevant variables are set to a nil value, so that we get Orderless everywhere.

(use-package orderless
  :ensure t
  :config
  (setq completion-styles '(orderless basic))
  (setq completion-category-defaults nil)
  (setq completion-category-overrrides nil))

The built-in savehist package keeps a record of user inputs and stores them across sessions. Thus, the user will always see their latest choices closer to the top (such as with M-x).

(use-package savehist
  :ensure nil ; it is built-in
  :hook (after-init . savehist-mode))

The corfu package provides a popup interface for in-buffer completion. Conceptually, this is similar to what we do in the minibuffer, though it is meant for text expansion inside of a regular file. This is typically used for programming. There is more that can be done to make this useful for programming modes (namely, to set up the Language Server Protocol configuration), but because that will be too specific to each person’s requirements, I am providing only the core functionality here.

(use-package corfu
  :ensure t
  :hook (after-init . global-corfu-mode)
  :bind (:map corfu-map ("<tab>" . corfu-complete))
  :config
  (setq tab-always-indent 'complete)
  (setq corfu-preview-current nil)
  (setq corfu-min-width 20)

  (setq corfu-popupinfo-delay '(1.25 . 0.5))
  (corfu-popupinfo-mode 1) ; shows documentation after `corfu-popupinfo-delay'

  ;; Sort by input history (no need to modify `corfu-sort-function').
  (with-eval-after-load 'savehist
    (corfu-history-mode 1)
    (add-to-list 'savehist-additional-variables 'corfu-history)))

The minibuffer and the corfu popup will benefit from the font icons we set up (Use icon fonts in various places).

Tweak the dired Emacs file manager

Emacs comes with a powerful file manager built-in. I have talked about it at length in this video: https://protesilaos.com/codelog/2023-06-26-emacs-file-dired-basics/. The following settings define some basic refinements to the behaviour of common operations for copying, renaming, and deleting files.

The default presentation of Dired contains lots of details (The complete configuration). We use dired-hide-details-mode to hide those. They can be toggled on/off at any time with M-x dired-hide-details-mode, which is bound to the ( key in Dired buffers.

(use-package dired
  :ensure nil
  :commands (dired)
  :hook
  ((dired-mode . dired-hide-details-mode)
   (dired-mode . hl-line-mode))
  :config
  (setq dired-recursive-copies 'always)
  (setq dired-recursive-deletes 'always)
  (setq delete-by-moving-to-trash t)
  (setq dired-dwim-target t))

The dired-subtree package provides commands to quickly view the contents of a folder with the TAB key.

(use-package dired-subtree
  :ensure t
  :after dired
  :bind
  ( :map dired-mode-map
    ("<tab>" . dired-subtree-toggle)
    ("TAB" . dired-subtree-toggle)
    ("<backtab>" . dired-subtree-remove)
    ("S-TAB" . dired-subtree-remove))
  :config
  (setq dired-subtree-use-backgrounds nil))

Earlier we set up Dired to move deleted files to the system trash. The trashed package is then useful to provide a Dired-like interface for that compartment (it works the same way as Dired, so check the video I share above).

(use-package trashed
  :ensure t
  :commands (trashed)
  :config
  (setq trashed-action-confirmer 'y-or-n-p)
  (setq trashed-use-header-line t)
  (setq trashed-sort-key '("Date deleted" . t))
  (setq trashed-date-format "%Y-%m-%d %H:%M:%S"))

The complete configuration

Remember to put this in the right file (Where to store the configurations).

(setq custom-file (locate-user-emacs-file "custom.el"))
(load custom-file :no-error-if-file-is-missing)

;;; Set up the package manager

(require 'package)
(package-initialize)

(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/"))

(when (< emacs-major-version 29)
  (unless (package-installed-p 'use-package)
    (unless package-archive-contents
      (package-refresh-contents))
    (package-install 'use-package)))

(add-to-list 'display-buffer-alist
             '("\\`\\*\\(Warnings\\|Compile-Log\\)\\*\\'"
               (display-buffer-no-window)
               (allow-no-window . t)))

;;; Basic behaviour

(use-package delsel
  :ensure nil
  :hook (after-init . delete-selection-mode))

(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)

;;; Tweak the looks of Emacs

;; Those three belong in the early-init.el, but I am putting them here
;; for convenience.  If the early-init.el exists in the same directory
;; as the init.el, then Emacs will read+evaluate it before moving to
;; the init.el.
(menu-bar-mode 1)
(scroll-bar-mode 1)
(tool-bar-mode -1)

(let ((mono-spaced-font "Monospace")
      (proportionately-spaced-font "Sans"))
  (set-face-attribute 'default nil :family mono-spaced-font :height 100)
  (set-face-attribute 'fixed-pitch nil :family mono-spaced-font :height 1.0)
  (set-face-attribute 'variable-pitch nil :family proportionately-spaced-font :height 1.0))

(use-package modus-themes
  :ensure t
  :config
  (load-theme 'modus-vivendi-tinted :no-confirm-loading))

;; Remember to do M-x and run `nerd-icons-install-fonts' to get the
;; font files.  Then restart Emacs to see the effect.
(use-package nerd-icons
  :ensure t)

(use-package nerd-icons-completion
  :ensure t
  :after marginalia
  :config
  (add-hook 'marginalia-mode-hook #'nerd-icons-completion-marginalia-setup))

(use-package nerd-icons-corfu
  :ensure t
  :after corfu
  :config
  (add-to-list 'corfu-margin-formatters #'nerd-icons-corfu-formatter))

(use-package nerd-icons-dired
  :ensure t
  :hook
  (dired-mode . nerd-icons-dired-mode))

;;; Configure the minibuffer and completions

(use-package vertico
  :ensure t
  :hook (after-init . vertico-mode))

(use-package marginalia
  :ensure t
  :hook (after-init . marginalia-mode))

(use-package orderless
  :ensure t
  :config
  (setq completion-styles '(orderless basic))
  (setq completion-category-defaults nil)
  (setq completion-category-overrrides nil))

(use-package savehist
  :ensure nil ; it is built-in
  :hook (after-init . savehist-mode))

(use-package corfu
  :ensure t
  :hook (after-init . global-corfu-mode)
  :bind (:map corfu-map ("<tab>" . corfu-complete))
  :config
  (setq tab-always-indent 'complete)
  (setq corfu-preview-current nil)
  (setq corfu-min-width 20)

  (setq corfu-popupinfo-delay '(1.25 . 0.5))
  (corfu-popupinfo-mode 1) ; shows documentation after `corfu-popupinfo-delay'

  ;; Sort by input history (no need to modify `corfu-sort-function').
  (with-eval-after-load 'savehist
    (corfu-history-mode 1)
    (add-to-list 'savehist-additional-variables 'corfu-history)))

;;; The file manager (Dired)

(use-package dired
  :ensure nil
  :commands (dired)
  :hook
  ((dired-mode . dired-hide-details-mode)
   (dired-mode . hl-line-mode))
  :config
  (setq dired-recursive-copies 'always)
  (setq dired-recursive-deletes 'always)
  (setq delete-by-moving-to-trash t)
  (setq dired-dwim-target t))

(use-package dired-subtree
  :ensure t
  :after dired
  :bind
  ( :map dired-mode-map
    ("<tab>" . dired-subtree-toggle)
    ("TAB" . dired-subtree-toggle)
    ("<backtab>" . dired-subtree-remove)
    ("S-TAB" . dired-subtree-remove))
  :config
  (setq dired-subtree-use-backgrounds nil))

(use-package trashed
  :ensure t
  :commands (trashed)
  :config
  (setq trashed-action-confirmer 'y-or-n-p)
  (setq trashed-use-header-line t)
  (setq trashed-sort-key '("Date deleted" . t))
  (setq trashed-date-format "%Y-%m-%d %H:%M:%S"))