fork is purely additive, this means that it has all the features from upstream BEn stands for Buffer ENvironments, it’s an asynchronous fork of envrc. This plus some extras.
The sections below list the additions Ben has added on top of envrc.
For more information on why these features were not added to envrc directly, see the links referenced in each of the following sections.
Whether to load the environments synchronously or asynchronously can be
configured with the variable ben-async-processing.
The main improvement of ben over envrc is the asynchronous processing of
environments, which prevents Emacs from freezing. This is especially useful
while loading computationally heavy environments, such when loading ‘.envrc’
files that rely on Guix. In these cases, computations can take hours to
complete. This package aims to facilitate loading such environments in the
background.
The following upstream PR give some overview on why this feature has not been merged upstream:
Since the discussion touches a few topics, here is the quote of the maintainer that explains why this is not merged:
<purcell>: There are a number of trade-offs around adding this support, and I’m not yet convinced I want to take on async support.
This also refactors the envrc lighter to replace it with a mode line
indicator. This indicator introduces two new states:
- A loading state, where a spinner hints buffers that are waiting for the
environment to load. The widget can be disabled by setting the
ben-add-to-mode-line-misc-infovariable tonil. - And the denied state, to indicate that an environment has been blocked by
issuing
direnv deny.
Read the following upstream PR to get some context on why this has not been merged:
In order to ease the management of asynchronous processes, a vtable based
process UI has been added. It can me invoked with M-x ben-list-processes.
It provides a few keybindings for interacting with the processes, they are
defined in ben-list-mode-map. For now it allows:
- k
- kill the process of the current line
- RET
- follow the element under point, PID, path or buffer. Following the PID opens a proced showing the selected process and it’s children in a tree form (this can be seen in the image below.
- g
- for refreshing the table
The module also provides ben-list-auto-update-flag which periodically
refreshes the ben process UI every ben-list-auto-update-interval.
The following upstream PR gives additional context on why this feature has not been merged upstream:
This fork of envrc introduces a new configuration variable,
ben-disable-in-minibuffer to control weather ben-mode should be enabled in
the minibuffer. This changes the default of upstream and allows different
minibuffer commands to pickup the right environment according to their
default-directory.
A GNU Emacs library which uses the direnv tool to determine
per-directory/project environment variables and then set those environment
variables on a per-buffer basis. This means that when you work across multiple
projects which have .envrc files, all processes launched from the buffers “in”
those projects will be executed with the environment variables specified in
those files. This allows different versions of linters and other tools to be
used in each project if desired.
direnv.el repeatedly changes the global Emacs environment, based on tracking what buffer you’re working on.
Instead, ben.el simply sets and stores the right environment in each buffer,
as a buffer-local variable.
From a user perspective, both are well tested and typically work fine, but the
ben.el approach feels cleaner to me.
Additionally, at the time of writing, ben.el has early TRAMP support, while
direnv.el does not.
Installable packages are available via MELPA: do M-x package-install RET ben
RET.
Add a snippet like the following to your init.el:
(use-package ben
:bind
(:map ben-mode-map
("C-c e" . ben-command-map))
;; NOTE: Optionally un-comment the following config section to customize the
;; mode-line status indicator with nerd-icons.
;; :config
;; ;; There is a bug in doom-modeline where some squares appear if the icon is
;; ;; propertized.
;; (setq ben-indicator `(,(substring-no-properties (nerd-icons-faicon "nf-fa-cubes"))
;; "[" (:eval (ben--status)) "]"))
:init
(add-hook 'after-init-hook #'ben-global-mode 99))Why must you enable the global mode late in your startup sequence like this?
You normally want ben-mode to be initialized in each buffer before other
minor modes like flycheck-mode which might look for
executables. Counter-intuitively, this means that ben-global-mode should be
enabled after other global minor modes, since each prepends itself to
various hooks.
The global mode will only have an effect if direnv is installed and available
in the default Emacs exec-path. (There is a local minor mode ben-mode, but
you should not try to enable this individually, e.g. for certain modes or
projects, because compilation and other buffers might not get set up with the
right environment.)
Regarding interaction with the mode, see ben-mode-map, and the commands
ben-reload, ben-allow and ben-deny. (There’s also ben-reload-all as a
“nuclear” reset, for now!)
In particular, you can enable keybindings for the above commands by binding your
preferred prefix to ben-command-map in ben-mode-map, e.g.
(with-eval-after-load 'ben
(define-key ben-mode-map (kbd "C-c e") 'ben-command-map))If you find that a particular Emacs command isn’t picking up the environment of
your current buffer, and you’re sure that ben-mode is active in that buffer,
then it’s possible you’ve found code that runs a process in a temp buffer and
neglects to propagate your environment to that buffer before doing so.
A couple of common Emacs commands that suffer from this defect are also patched
directly via advice in ben.el — shell-command-to-string is a prominent
example!
The inheritenv package was designed to handle this case in general.
By default, Emacs has a single global set of environment variables used for all
subprocesses, stored in the process-environment variable. direnv.el switches
that global environment using values from direnv when the user performs
certain actions, such as switching between buffers in different projects.
In practice, this is simple and mostly works very well. But there are some quirks, and it feels wrong to me to mutate the global environment in order to support per-directory environments.
Now, in Emacs we can also set process-environment locally in a buffer. If this
value could be correctly maintained in all buffers based on their various
respective .envrc files, then buffers across multiple projects could
simultaneously be “connected” to the environments of their corresponding project
directories. I wrote ben.el to explore this approach.
ben.el uses a global minor mode (ben-global-mode) to hook into practically
every buffer created by Emacs, including hidden and temporary ones. When a
buffer is found to be “inside” an .envrc-managed project,
process-environment is set buffer-locally by running direnv, the results of
which are also cached indefinitely so that this is not too costly overall. Each
buffer has a local minor mode (ben-mode) with an indicator which displays
whether or not a direnv is in effect in that buffer. (Hooking into every buffer
is important, rather than just those with certain major modes, since separate
temporary, compilation and repl buffers are routinely used for executing
processes.)
This approach also has some trade-offs:
Directory of the buffer which caused them to be created initially, and then those buffers often live for a long time. If you launch programs from such buffers while working on a different project, the results might not be what you expect. I might exclude certain modes to minimise confusion, but users will always have to be aware of the fact that environments are buffer-specific.
That happens quite a lot.
When switching between buffers that visit files in different directories,
whereas ben-mode caches the environment until the user refreshes it explicitly
with ben-reload.
Overall this approach works well in practice, and feels cleaner than trying to strategically modify the global environment.
It’s also possible that there’s a way to call direnv more aggressively by
allowing it to see values of DIRENV_* obtained previously such that it becomes
a no-op.
