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:
- Add
pkg
topackage-selected-packages
; - Try to
require
the package feature; - If successful, evaluate the
body
usingprogn
.
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.