Rewriting my Emacs config... again!
Motivation
Recently, I looked into other package management tools for Emacs. I looked at ElPaso, Borg, and Straight. They all seem to be ways to avoid using the Emacs built-in package.el
, which I'm not all that interested in doing. I find the simplicity of being able to list the available packages I might be interested in installing too useful to switch away to something else. They are interesting projects, and you should probably look into one or more of them to see if they make sense for your use.
Even though I was already using use-package, and am generally happy with how it works, I thought it might be fun to re-organize and rewrite my configuration. There is a lot of cruft and I needed to sort that out anyway.
For goals, I chose this list:
- Rewrite without
use-package
. - Keep start-up time to a minimum (my startup time was just above 2 seconds).
- Programmatic activation of chosen modules with byte compilation.
- Prefer versioned packages over “latest commit” packages (ie prefer other repositories versions over MELPA versions).
Influence
As mentioned previously, I used Prelude for a while and still take inspiration from its code and layout.
As a summary, Prelude is organized around some core configuration with some modules which can be configured or mixed in automatically depending on the mode needed for editing a file. For example, editing a clojure file would automatically activate the Clojure module in Prelude. This is a cool feature, but I'm not that ambitious, so it is not one I tried to emulate in my configuration. However, how the core and modules are organized, I did take a bit of inspiration from that.
Approach
Following Prelude's lead, I have the following directories:
core
: This is where the base configuration exists. It contains the elisp needed to install, activate and configure the base set of packages I always want to be enabled.modules
: This is the list of “modules” for various programming modes or other tools which I want to use, but not necessarily have activated on startup. For example, I might be interested in editing the Lua configuration for my AwesomeWM configuration, but since I do that rarely, I don't want it activated unless I edit a Lua file. This directory is not on theload-path
. More information below.personal
: This is where personal configuration goes. Most of the configuration will reside in either themycustom.el
file which is for general customization of various modules or for things that just don't fit anywhere else. Themymodules.el
file is where I activate the modules I want to use. This directory is on theload-path
, modules are symlinked here (or when I am on a Microsoft Windows machine, copied here).sample
: This folder contains an examplemymodules
file to copy into thepersonal
directory and edit there.
I make aggressive use of autoload
and with-eval-after-load
to control how and when packages are enabled. There is still more to do, I think using mode hooks would be a cleaner approach for some things, so I'm still considering how I might implement that concept, especially for programming modes and LSP.
Packages
This is how I configure my package-archives
, I have it listed in the order I prefer to get packages, but the ordering here is not relevant.
(setq package-archives
(quote
(("gnu" . "https://elpa.gnu.org/packages/")
("nongnu" . "https://elpa.nongnu.org/nongnu/")
("melpa-stable" . "https://stable.melpa.org/packages/")
("melpa" . "https://melpa.org/packages/")
;; [2021-03-31 Wed] org currently at 9.4.5, but this is about to
;; go away after the release of org 9.5. Org will be distributed
;; through GNU elpa and org-contrib will be distributed through
;; nongnu elpa (see above for both).
("org" . "https://orgmode.org/elpa/"))))
I continue to use the orgmode.org/elpa
until the release of Org 9.5, and will remove it from the list at that time.
To use melpa
and still prefer versions, I use a feature of package.el
called priorities. If a package exists in a higher priority elpa, then install from there, otherwise, try the next lower priority elpa and so on until the package is found. Some packages are only found in melpa
so I get them from there. This allows me to get a versioned package from one of four other elpas before trying melpa
N.B. elpa in the last paragraph refers to the Emacs Lisp Package Archive specification rather than a specific repository.
This is how I configure the priorities for packages, higher numbers are preferred over lower numbers when retrieving packages:
(setq package-archive-priorities
'(("gnu" . 99)
("nongnu" . 80)
("org" . 75) ; see comment above, this will
; be going away
("melpa-stable" . 70)
("melpa" . 0)))
I don't want all of the packages I have installed activated by default, so I pass t
to package-initialize
to not activate anything and allow the rest of my configuration to do that work.
(if (file-exists-p package-user-dir)
;; don't activate all packages, instead they will be added via
;; mypackage-require, which will install if needed as well.
(package-initialize t)
;; on a new system, need to activate everything initially
(package-initialize)
(package-refresh-contents))
Finally, Prelude has a nifty idea of tracking a default set of packages that should be installed initially, and to which additional packages are added as they are activated later. I cloned that idea directly from Prelude and use it as well. I do attribute the code and give credit to the author in my code. I did change some of the names to match the rest of my code (ie mypackage-
instead of prelude-
).
Modules
The modules portion of my config contains specific configuration files for each of the programming, text, and utility (ala docker) modes I currently use. The mymodules-config.el
has functions that create symlinks in the personal
directory and then byte compiles the file there. There are two variables used which can be configured using setq
:
mymodule-available-dir
: This is the directory where the modules live. They are symlinked or copied from here.mymodule-active-dir
: This is the directory where the modules are symlinked (or copied) to make them active.
To create the symlinks, I thought a macro would make sense:
(defmacro mymodule-activate (module)
"Install symlink to source and compiled MODULE files
Gets MODULE from MODULE-AVAILABLE-DIR to create a symlink in
MODULE-ACTIVE-DIR, byte compiles the module after creating the
symlink and then loads the MODULE. If the MODULE is already
installed, just load it."
;; initially create names for use in the macro itself. This is not
;; really needed as the only input is MODULE which is a string, but
;; it means the symbols created can *only* be used here in this
;; macro.
(let ((module-name (make-symbol (file-name-base module)))
(load-name (make-symbol "load-name"))
(available-module-file (make-symbol "available-module-file"))
(active-module-file (make-symbol "active-module-file")))
`(let* ((,module-name ,module)
(,load-name (file-name-base ,module))
(,available-module-file (expand-file-name ,module-name
mymodule-available-dir))
(,active-module-file (expand-file-name ,module-name
mymodule-active-dir)))
(if (file-exists-p ,active-module-file)
(load ,load-name t)
(if (string= "windows-nt" system-type)
;; if this is Microsoft Windows, we can't use symlinks, so
;; copy the file instead.
(copy-file ,available-module-file mymodule-active-dir)
;; Linux or Apple Mac can handle symlinks
(make-symbolic-link ,available-module-file
mymodule-active-dir
t))
(byte-compile-file ,active-module-file)
(load ,load-name t)))))
However, since symlinks don't work in Microsoft Windows, I have to check for that and just copy the files from the available directory to the active directory.
Customization
Out of the box, I don't need to do anything. If I don't have a mymodules.el
file, then just the default packages are loaded and I can still get a lot of work done. Things like Org-mode, Magit, Ivy, etc. allow me to do enough from day-to-day. I tend to spend a lot of time in Org-mode tracking work, tasks, meetings, notes, etc. so the base is sufficient.
However, as I also need to edit Java, Json, XML, and other things like Docker files and diagrams, so I activate those modules on the machine where I do that work. This gives me the flexibility to install the modules I use to configure Emacs for the machine I'm on at the time. My work configuration contains different modules than the configuration on the client environment, and from my personal computer.
Conclusion
This has been an interesting exercise! It took about eight hours to accomplish the rewrite to the point I have it now. I did clone some code from Prelude, and I did reach out to Bozhidar for permission, which he graciously provided.
There are still some things I think could be improved in this configuration. For example, I need to look into using mode hooks more frequently. Some of the autoload code does not activate the way I think it should, but a mode hook would most likely fix that problem.
As for meeting the goals:
- I have replaced
use-package
. To do so, I usedpp-macroexpand-expression
to understand what was happening and then implemented a solution using either or both ofautoload
andwith-eval-after-load
. - Startup time is about 1.6 seconds.
- The macro
module-activate
handles making symlinks or copies, byte compiling the module and loading it. - Using package priorities, I get versioned packages, or a version from MELPA if that is the only repository where a package is distributed.
The code: git repo.
Tags: #emacs