How to write your own modules

To create your own module you need only create a directory for it in ~/.doom.d/modules/abc/xyz , then add :abc xyz to your doom! block in ~/.doom.d/init.el to enable it.

:pushpin: In this example, :abc is called the category and xyz is the name of the module. Doom refers to modules in one of two formats: :abc xyz and abc/xyz .

If a private module possesses the same name as a built-in Doom module (say, :lang org ), it replaces the built-in module. Use this fact to rewrite modules you don’t agree with.

Of course, an empty module isn’t terribly useful, but it goes to show that nothing in a module is required. The typical module will have:

  • A packages.el to declare all the packages it will install,
  • A config.el to configure and load those packages,
  • And, sometimes, an autoload.el to store that module’s functions, to be loaded when they are used.

These are a few exceptional examples of a well-rounded module:

The remainder of this guide will go over the technical details of a Doom module.

File structure

Doom recognizes a handful of special file names, none of which are required for a module to function. They are:

category/
  module/
    test/*.el
    autoload/*.el
    autoload.el
    init.el
    cli.el
    config.el
    packages.el
    doctor.el

init.el

This file is loaded early, before anything else, but after Doom core is loaded. It is loaded in both interactive and non-interactive sessions (it’s the only file, besides cli.el that is loaded when the bin/doom starts up).

Do:

  • Configure Emacs or perform setup/teardown operations that must be set early; before other modules are (or this module is) loaded.
  • Reconfigure packages defined in Doom modules with use-package-hook! (as a last resort, when after! and hooks aren’t enough).
  • Configure behavior of bin/doom in a way that must also apply in interactive sessions.

Don’t:

  • Configure packages with use-package! or after! from here
  • Preform expensive or error-prone operations; these files are evaluated whenever bin/doom is used; a fatal error in this file can make Doom unbootable (but not irreversibly).
  • Define new bin/doom commands here. That’s what cli.el is for.

config.el

The heart of every module. Code in this file should expect dependencies (in packages.el ) to be installed and available. Use it to load and configure its packages.

Do:

  • Use after! or use-package! to configure packages.
;; from modules/completion/company/config.el
(use-package! company  ; `use-package!' is a thin wrapper around `use-package'
                       ; it is required that you use this in Doom's modules,
                       ; but not required to be used in your private config.
  :commands (company-mode global-company-mode company-complete
             company-complete-common company-manual-begin company-grab-line)
  :config
  (setq company-idle-delay nil
        company-tooltip-limit 10
        company-dabbrev-downcase nil
        company-dabbrev-ignore-case nil)
   [...])
  • Lazy load packages with use-package ’s :defer property.
  • Use the featurep! macro to make some configuration conditional based on the state of another module or the presence of a flag.

Don’t:

  • Use package!
  • Install packages with package.el or use-package ’s :ensure property. Doom has its own package manager. That’s what packages.el is for.

packages.el

This file is where package declarations belong. It’s also a good place to look if you want to see what packages a module manages (and where they are installed from).

Do:

  • Declare packages with the package! macro
  • Disable single packages with package! ’s :disable property or multiple packages with the disable-packages! macro.
  • Use the featurep! macro to make packages conditional based on the state of another module or the presence of a flag.

Don’t:

  • Configure packages here (definitely no use-package! or after! in here!). This file is read in an isolated environment and will have no lasting effect. The only exception is configuration targeting straight.el .
  • Perform expensive calculations. These files are read often and sometimes multiple times.
  • Produce any side-effects, for the same reason.

:pushpin: The ”Package Management” section goes over the package! macro and how to deal with packages.

autoload/*.el OR autoload.el

These files are where you’ll store functions that shouldn’t be loaded until they’re needed and logic that should be autoloaded (evaluated very, very early at startup).

This is all made possible thanks to these autoload cookie: ;;;###autoload . Placing this on top of a lisp form will do one of two things:

  1. Add a autoload call to Doom’s autoload file (found in ~/.emacs.d/.local/autoloads.el , which is read very early in the startup process).
  2. Or copy that lisp form to Doom’s autoload file verbatim (usually the case for anything other then def* forms, like defun or defmacro ).

Doom’s autoload file is generated by scanning these files when you execute doom sync .

For example:

;; from modules/lang/org/autoload/org.el
;;;###autoload
(defun +org/toggle-checkbox ()
  (interactive)
  [...])

;; from modules/lang/org/autoload/evil.el
;;;###autoload (autoload '+org:attach "lang/org/autoload/evil" nil t)
(evil-define-command +org:attach (&optional uri)
  (interactive "<a>")
  [...])

doctor.el

When you execute doom doctor , this file defines a series of tests for the module. These should perform sanity checks on the environment, such as:

  • Check if the module’s dependencies are satisfied,
  • Warn if any of the enabled flags are incompatible,
  • Check if the system has any issues that may interfere with the operation of this module.

Use the warn! , error! and explain! macros to communicate issues to the user and, ideally, explain how to fix them.

For example, the :lang cc module’s doctor checks to see if the irony server is installed:

;; from lang/cc/doctor.el
(require 'irony)
(unless (file-directory-p irony-server-install-prefix)
  (warn! "Irony server isn't installed. Run M-x irony-install-server"))

cli.el

This file is read when bin/doom starts up. Use it to define your own CLI commands or reconfigure existing ones.

test/**/test-*.el

Doom’s unit tests go here. More information on them to come…

Additional files

Any files beyond the ones I have already named are not given special treatment. They must be loaded manually to be loaded at all. In this way modules can be organized in any way you wish. Still, there is one convention that has emerged in Doom’s community that you may choose to adopt: extra files in the root of the module are prefixed with a plus, e.g. +extra.el . There is no syntactical or functional significance to this convention.

These can be loaded with the load! macro, which will load an elisp file relative to the file it’s used from. e.g.

;; Omitting the file extension allows Emacs to load the byte-compiled version,
;; if it is available:
(load! "+git")   ; loads ./+git.el

This can be useful for splitting up your configuration into multiple files, saving you the hassle of creating multiple modules.

16 Likes