cosine.blue

Blog by Gregory Chamberlain.

Declarative Package Configuration In 5 Lines of Emacs Lisp

How I configure Emacs packages conditionally and with a consensual installation process.

History

Cover image: Photo of the back of a 2-in-1 laptop with several stickers including Tux the penguin, the Emacs logo, one that says “linux inside” and another with a love heart followed by the words “I Lisp.”

GNU Emacs is a real phenomenon of computing, not least thanks to the wealth of free/libre packages available.

Thing is, most of these packages are more like software libraries than programmes, having little to no effect out of the box; they provide all the Lego bricks, but leave it to the user to put them together. And like Lego, putting the bricks together is the fun part! The way we do that is by writing bits of Emacs Lisp.

For example, here are a few lines of Elisp you could write to configure one of my favourite packages, dired-subtree.

(setq dired-subtree-use-backgrounds nil)
(let ((map dired-mode-map))
  (define-key map (kbd "TAB") #'dired-subtree-cycle)
  (define-key map (kbd "M-^") #'dired-subtree-remove)))

However, there is a problem. What happens when you write a bit of Elisp to configure a certain package, but that package is not (yet) installed? You often get errors about undefined commands, files not found, and so on.

Conditional configuration

One way around this is to put package-specific configuration behind a conditional.

(when (package-installed-p 'dired-subtree)
  (setq dired-subtree-use-backgrounds nil)
  (let ((map dired-mode-map))
    (define-key map (kbd "TAB") #'dired-subtree-cycle)
    (define-key map (kbd "M-^") #'dired-subtree-remove)))

This works, but what if you want to then go ahead and install all packages for which you have a conditional configuration? There is no record of the names of those packages.

You could scan through your user-init-file by eye and manually install each package by hand using M-x package-install pkg RET. But that only gets more laborious the more packages you want to have.

Installing packages programmatically

A more viable solution is to use a macro that does a similar thing and collects the package names into a list variable at the same time. That way, you could programmatically install all such packages by iterating over the list.

As it happens, such a variable is already defined and documented by package.el and it goes by the name package-selected-packages. What’s more, there exists a command M-x package-install-selected-packages that asks the user for approval before installing them all. Making use of this variable, our job becomes very easy.

Each time this macro is called with the name of some package pkg, we need to do three things:

  1. Add pkg to package-selected-packages;
  2. Try to require the package feature;
  3. If successful, evaluate the body using progn.

Reproduced below is just 5 (logical) lines of Emacs Lisp sufficient to define this macro. I’ve named it with-package, analogous to the likes of with-current-buffer, with-selected-window, with-editor, etc.

(defmacro with-package (package &rest body)
  "Add PACKAGE to ‘package-selected-packages’, then
attempt to ‘require’ PACKAGE and, if successful, 
evaluate BODY."
  (declare (indent 1))
  `(and (add-to-list 'package-selected-packages ,package)
        (require ,package nil 'noerror)
        (progn ,@body)))

The declare expression is related to code formatting and is not strictly necessary for the macro to work. Its purpose is to inform Emacs that the body of the macro call should be indented by an additional character.

Here’s our dired-subtree example from above, this time in with-package form.

(with-package 'dired-subtree
  (setq dired-subtree-use-backgrounds nil)
  (let ((map dired-mode-map))
    (define-key map (kbd "TAB") #'dired-subtree-cycle)
    (define-key map (kbd "M-^") #'dired-subtree-remove)))

Note that, even though this is a macro, the name of the package should be quoted. Otherwise Emacs treats it as a variable and tries to find its value, which is not necessarily defined.

External solutions

All this is to neglect the elephant in the room, a highly popular package by the name of use-package which provides a macro of the same name that does what my little with-package macro does and lots more. For a long time and until recently, my user-init-file was scattered with calls to use-package for the external packages I employed.

I even had a little snippet near the top that would install the use-package package if it wasn’t present already. This practice is almost as popular as the package itself, it seems.

There are several reasons why I ditched use-package in favour of with-package:

There is a way to install all packages for which a use-package form has been evaluated, but it does not ask interactively for consent from the user before doing so. Rather, the packages are installed at startup without prompt nor warning.

use-package is a mighty big package that does way more than I need it to do. Simpler is usually better when you can get away with it.

Among its more advanced features is a way to selectively defer the loading of packages until they’re needed, shortening startup times. That sounds great, but personally I’m happy with my startup time as it is.

The last reason is that, honestly, I was rather pleased with myself after writing this macro. And if you ask me, DIY programming is what Emacs is all about.

So yeah, a 5 line Lisp macro may be all you need to take your init.el to the next level. If this post has inspired you to rip apart your carefully crafted Emacs configuration as I did mine, do send an email to greg@cosine.blue letting me know.


I ❤ Emacs!

The wide-reaching applications of GNU Emacs have slowly invaded my workflow over the past year and a half, and I have much more to write about it. To find out when new Emacs related content emerges on cosine.blue, subscribe via RSS or follow me on Mastodon.


To leave a comment, please send a plaintext email to ~chambln/public-inbox@lists.sr.ht and it will show up in my public inbox.