~chambln/cosine.blue

2bec4469127213c938fa842d2d50d5bd43957b9b — Gregory Chamberlain a month ago bfcf168
Publish emacs-with-package
2 files changed, 168 insertions(+), 0 deletions(-)

A env/emacs-with-package.md.sh
A src/article/emacs-with-package.md
A env/emacs-with-package.md.sh => env/emacs-with-package.md.sh +8 -0
@@ 0,0 1,8 @@
title="Declarative Package Configuration In 5 Lines of Emacs Lisp"

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

date="Friday, 2 April 2021"

author="Gregory Chamberlain"

A src/article/emacs-with-package.md => src/article/emacs-with-package.md +160 -0
@@ 0,0 1,160 @@
[GNU Emacs] is a real phenomenon of computing, not least thanks to the
[wealth] of [free/libre] packages available.

  [GNU Emacs]: https://www.gnu.org/software/emacs/
  [free/libre]: https://www.gnu.org/philosophy/free-sw.html
  [wealth]: https://melpa.org
    "MELPA (Milkypostman’s Emacs Lisp Package Archive)"

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].

  [dired-subtree]: https://github.com/Fuco1/dired-hacks
    "Collection of useful dired additions"

```elisp
(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.

```elisp
(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 <code>M-x package-install
<i>pkg</i> RET</code>.  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
<code><i>pkg</i></code>, we need to do three things:

1.  Add <code><i>pkg</i></code> 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.

```elisp
(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.

```elisp
(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 &lt;3 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].

  [RSS]: /rss.xml
  [Mastodon]: https://mastodon.social/@chambln