Emerging from dotemacs bankruptcy the hard way: package management
Elpa, Melpa, git repo. Vendor package straight from source. It compiled? Fetch some more! Elpa, Melpa, git repo. In more adult terms, we learn to use use-package to fetch, install, initialise, configure useful packages that enhance our Emacs experience.

A nearly half-century old community-run software project that bills itself "the advanced, extensible, customisable, self-documenting editor" 1 is going to have an interesting customisation story. The following note may give you a more visceral sense of what I mean.

I ended up reading documentation for the built-in auto-save-mode function, as I want to configure a certain kind of auto-save behaviour 2. I noticed its documentation says "Probably introduced at or before Emacs version 1.6". That is, version One point Six. Probably. Whereas the Emacs version history page3 goes back only to the point Emacs was announced to the public on Usenet, which was version 13.0, which was Thirty Eight years ago; i.e. auto save mode has been customisable far longer than that.

Generally speaking, there are three approaches to customisation.

  • Using the GUI: The so-called "Easy Customization Interface" 4. This limits us to modifying values of variables exposed by modes and packages (which we can M-x package-install). This is pretty powerful in itself, but can get tedious and brittle to maintain and to repeat across machines.
  • Using elisp; whether written by others (packages, starter kits 5), or written by oneself (init file configurations, custom functions, "defadvice"s, and even bespoke major/minor modes).
  • Using X Options and Resources; which is obscure enough to appear in Appendix D of the GNU Emacs manual, so I am not even going there.

And as with anything the effort investment is about "build" v/s "buy". Writing a custom dotemacs (instead of just using a starter kit) is the "build" part, and using packages is "buy" part (which starter kits make ever so easy).

Part of the allure of starter kits is that they reduce the need to write a lot of boilerplate and/or repetitive elisp. However elisp can also write the boring elisp for us. And that is exactly what use-package does.

Lisp macrology FTW 6! For example…

The most basic use-package definition.

(use-package delight)

Translates to…

 '(use-package delight))

This elisp code…

(use-package-ensure-elpa 'delight '(t) 'nil)
(require 'delight nil nil)

That was not very impressive, visually-speaking. A more typical definition with some basic customisations is more eye-opening.

(use-package company
    :bind (:map global-map ; because we make company-mode global
                ("TAB" . company-complete-common-or-cycle))
    (setq company-idle-delay 0.1)
    (global-company-mode t)

Let's macroexpand that whole form.

 '(use-package company
    :bind (:map global-map ; because we make company-mode global
                ("TAB" . company-complete-common-or-cycle))
    (setq company-idle-delay 0.1)
    (global-company-mode t)

The result is code we would have to hand-write if we chose to do the same with regular elisp and the bind-keys macro provided by the bind-keys package, which comes bundled with use-package. Meaning, if we did not have a utility package like bind-keys, we would end up writing even more elisp to safely bind keys 7.

(use-package-ensure-elpa 'company '(t) 'nil)

(unless (fboundp 'company-complete-common-or-cycle)
  (autoload #'company-complete-common-or-cycle "company" nil t))

(eval-after-load 'company
  '(progn (setq company-idle-delay 0.1)
          (global-company-mode t)
          (if (fboundp 'diminish)
              (diminish 'company-mode)) t))

 :package company
 :map global-map
 ("TAB" . company-complete-common-or-cycle))

Starter kits like prelude, spacemacs, or doom-emacs do just this sort of elisp heavy lifting for us. I like those, but I feel use-package is in a class of its own. A fantastic utility with an elegant interface that delivers handsomely on its sales pitch. For example:

So that completes the use-package story.

Going further, I want to move my workflow to the "new" Emacs as fast as possible. So to begin with, I chose to enhance code completions (company-mode), lisp editing (paredit), and version control (magit). Now developing my dotemacs in my new Emacs is nicer!

The init.el so far (previously)…

Enhanced Lisp editing, using use-package
;;; init.el  -*- lexical-binding: t -*-  --- My Emacs configuration.

;;; Commentary:

;;; This file is not part of GNU Emacs.

;;; Author: Aditya Athalye
;;; Created on: 30 June 2023
;;; Copyright (c) 2023 Aditya Athalye

;;; License:
;;; This program is free software; you can redistribute it and/or
;;; modify it under the terms of the MIT license, which is included
;;; with this distribution. See the LICENCE.txt file.

;;; Code:

;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Globals

;; Always load newest byte code
(setq load-prefer-newer t) ; cf. bbatsov/prelude

;; Directory structure
;; Take clues from bbatsov/prelude, except keep structure relative to our
;; initial dotemacs-dir path. This way we can start the user's emacs via
;; ~/.emacs.d symlinked to the dotemacs repo, and develop/debug against
;; the repo without potentially overwriting transient state files of the
;; daily driver .emacs.d.
(defvar dotemacs-dir
  (file-name-directory (or load-file-name (buffer-file-name)))
  "The dotemacs' root.  Normally it should be ~/.emacs.d.")

(defvar dotemacs-savefile-dir (expand-file-name "savefile" dotemacs-dir)
  "This folder stores all the automatically generated save/history-files.")
(unless (file-exists-p dotemacs-savefile-dir)
  (make-directory dotemacs-savefile-dir))

;; Make emacs add customisations here, instead of the init file.
;; Usually customisations made from the UI go into custom-file.
(setq custom-file (expand-file-name "custom.el" dotemacs-dir))
(unless (file-exists-p custom-file)
  (make-empty-file custom-file))

;; Sundries
(setq indent-tabs-mode nil) ; no hard tabs
(setq create-lockfiles nil) ; no lockfiles
(setq ring-bell-function 'ignore) ;

;; Visual Aesthetics

(setq inhibit-startup-message t)

;; More screen real estate
(scroll-bar-mode 0)
(tool-bar-mode 0)
(menu-bar-mode 0)
(set-fringe-mode '(5 . 13)) ;; describe variable fringe-mode

;; Go easy on the eyes
;; This high-contrast darkmode theme is built into Emacs as of
;; Emacs version 28.1
(load-theme 'modus-vivendi)

;; Package management

(require 'package)
;; Explicitly set the exact package archives list
(setq package-archives '(("melpa" . "https://melpa.org/packages/")
                         ("org" . "https://orgmode.org/elpa/")
                         ("elpa" . "https://elpa.gnu.org/packages/")))
;; Set package download directory relative to the dotemacs-dir
(setq package-user-dir (expand-file-name "elpa" dotemacs-dir))

(unless package-archive-contents

;; Use use-package

;; Ian Y.E. Pan's tutorial is a nice quick overview.
;; https://ianyepan.github.io/posts/setting-up-use-package/

(unless (package-installed-p 'use-package)
  (package-install 'use-package))

(require 'use-package)
(setq use-package-always-ensure t)
(setq use-package-expand-minimally t) ; set nil to debug use-package forms

;; All the packages!

;;; Packages useful to configure packages
(use-package diminish)
(use-package delight)

;;; COMplete ANYthing, please!
;;; h/t suvratapte/dot-emacs-dot-d
(use-package company
  :bind (:map global-map
              ("TAB" . company-complete-common-or-cycle))
  (setq company-idle-delay 0.1)
  (global-company-mode t)

;;; General code editing
(global-display-line-numbers-mode 1)

;;; Lispy editing support

;; Tweak settings of built-in paren package
(use-package paren
  :ensure nil ; it already exists, don't try to search online
  (setq show-paren-delay 0)
  (show-paren-mode t)

(use-package paredit
  (add-hook 'emacs-lisp-mode-hook #'enable-paredit-mode)
  (("M-[" . paredit-wrap-square)
   ("M-{" . paredit-wrap-curly))

(use-package magit

(provide 'init)
;;; init.el ends here

Our current place in the rough plan:

  • [✓] Set the very preliminaries.
  • [✓] Set up package management. I'll probably stick with the old familiars; elpa and melpa. I'm not sure about straight.el at this time.
  • [✓] Choose use-package to get and configure each package. I like how neat configs are, when defined with use-package.
  • [WIP] Make completions and "getting about" work (the right mix of ivy, consul, swiper, company, helm, imenu). Someone mentioned newer alternatives to helm. Have a look at that.
  • [WIP] Fix general text editing stuff (keybindings, multiple cursors, snippets etc.)
  • [WIP] Add support for favourite programming languages.
    • [✓] Emacs Lisp
    • many others…
  • org-mode specifics
  • then let's see…

Next, I will fully enhance completions and code/text navigation. It will make all my programming and writing much more ergonomic. And it will help me straighten my mental model about why and how to make those enhancements.


  1. The Emacs Editor, GNU Manual↩︎

  2. In the end, I decided to use the super-save package, instead of futzing around with built-in auto-save settings. It will "Save Emacs buffers when they lose focus". Yes please.↩︎

  3. Emacs version history page goes back only 38 years… https://www.gnu.org/software/emacs/history.html↩︎

  4. 49.1 Easy Customization Interface manual page is but one small part of the whole customisation story.↩︎

  5. Emacs wiki lists many packages (M-x package-list-packages in Emacs), and many emacs configuration starter kits.↩︎

  6. Emacs Lisp macrology: 14 Macros.

    The use-package macro allows you to isolate package configuration in your .emacs file in a way that is both performance-oriented and, well, tidy. I created it because I have over 80 packages that I use in Emacs, and things were getting difficult to manage. Yet with this utility my total load time is around 2 seconds, with no loss of functionality!


  7. The bind-keys form macroexpands to this.

       :package company
       :map global-map
       ("TAB" . company-complete-common-or-cycle)))
    (let* ((name "TAB")
           (key " ")
           (kmap (or (if (and nil (symbolp nil))
                         (symbol-value nil)
           (kdesc (cons (if (stringp name)
                          (key-description name))
                        (if (symbolp nil) nil 'nil)))
           (binding (lookup-key kmap key)))
      (let ((entry (assoc kdesc personal-keybindings))
            (details (list #'company-complete-common-or-cycle
                           (unless (numberp binding) binding))))
        (if entry
            (setcdr entry details)
          (add-to-list 'personal-keybindings (cons kdesc details))))
      (define-key kmap key #'company-complete-common-or-cycle))