About
This is my literate config for Doom Emacs. It makes extensive use of helper functions to embed/automate common workflows.
A “personal menu structure” underneath a custom leader gives me the ability to organize my keybinds logically according to how they fit into my workflows, instead of based on what feature/package/etc. implements them.
I’ve often felt inspired by other folks’ emacs configs, so I published mine in the hopes it similarly inspires others.
Be warned though, I’m no Emacs expert, so there might be some wrong conventions about the place.
Features:
- Config management (with org-mode)
- Notes system (with org-roam)
- Blogging (with ox-hugo)
This is a personal config. (My name is hardcoded in it, for one thing.) So feel free to be inspired, but you should not attempt to directly use this config.
Instead, I recommend you copy/paste code or ideas into your own config!
The following main principles underlie my config design:
Custom leader
All keybindings are defined behind a custom leader, M-SPC
by default (only
available in normal mode).
my/ prefix
All variables/functions are defined with the my/
prefix, e.g.
my/notes-directory
or my/concatd
.
Conventional folder structure
This config is designed to work with a particular folder structure:
~/notes
~/notes/config.org
~/projects
~/projects/blog
~/.doom.d/config.el
The exact paths can be configured by adjusting variables in the config. (Use
M-SPC c r
to tangle and reload the config after changes.)
Requirements
- Doom emacs
- Git
- Font: Fira Code
For blogging:
- Hugo v0.81
- (optional) PlantUML in
~/bin/plantuml.jar
These have to be enabled are enabled in ~/.doom.d/init.el
:
(org +roam)
This has to be in ~/.doom.d/packages.el
:
(package! ox-hugo :recipe (:host github :repo "luketurner/ox-hugo" :branch "master"))
In this case, I’m using my own fork of ox-hugo that has a simple patch to remove no-longer-needed underscore escaping in URLs (see: https://github.com/kaushalmodi/ox-hugo/issues/308).
Apparently this is only an issue for certain browser/website combinations that don’t un-escape them.
However, my fork is pretty far behind ox-hugo
at this point, I should probably
switch back to the main project soon.
Bootstrapping
To use this config for the first time (e.g. with a brand-new Emacs install):
- Make sure the the Shared and Config management variables are set to the correct values.
- Ensure this
.org
file is saved in the location specified bymy/config-literate-file
. - Run the following code block with
C-c C-c
<<shared>>
<<notes>>
<<config-mgmt>>
(my/config-tangle-compile)
Then restart Emacs to load the new config!
Once bootstrapped, reload the config without restarting with M-SPC c r
.
Shared
This section defines shared functions, variables, and macros that are used throughout the config.
Think of it as the “standard library” for the rest of the config.
(defvar my/name "Luke Turner")
(defvar my/email "redacted")
(defvar my/leader "M-SPC")
(defun my/concatd (&rest paths)
"Accepts any number of arguments, concatenating them together
into a path with appropriate directory separators. The last
argument is assumed to be a filename (i.e. it will not have a
path separator added to the end). A path ending in a path
separator can be generated by adding a nil to the end of your
path (indicating the filename part of the path is empty.)"
(let ((paths (reverse paths)))
(seq-reduce (lambda (m x) (concat (file-name-as-directory x) m))
(cdr paths)
(car paths))))
(defvar my/home-directory "~")
(defvar my/project-directory (my/concatd my/home-directory "projects" nil))
(defmacro my/map! (p pname &rest forms)
"Convenience macro for mapping keys underneath my/leader."
`(map! :prefix (,(concat my/leader " " p) . ,pname) ,@forms))
(require 'seq) ; make sure seq is loaded
(require 'which-key) ; sometimes needed for which-key-idle-delay to be detected?
(setq user-full-name my/name
user-mail-address my/email)
(setq doom-font (font-spec :family "Fira Code" :size 14))
(setq doom-theme 'doom-one)
(setq display-line-numbers-type t)
(setq which-key-idle-delay 0.1)
(setq confirm-kill-emacs nil)
(after! org
(setq org-agenda-custom-commands '())
(setq org-capture-templates '()))
(after! org-roam
(setq org-roam-capture-templates '()))
; Ensure leader is unbound (may not be necessary, depending on your leader)
(map! my/leader nil)
Notes system
My notes system is built around org-roam
, with additional conventions layered
on top. Put simply:
- A note is a single Org file stored in
~/notes
. - Notes are organized by linking them together.
- Notes can be published as blog posts (see Blogging)
A couple tips for interacting with notes:
- Use
M-SPC n f
for both opening and creating notes. (If you try to “find” a note that doesn’t exist, it will be created.) - When creating a personal note, use the
note
template. If note is intended to be published to the blog, useblog
template to pre-fill frontmatter. - Use
M-SPC n s
to do a ripgrep-search through all your notes. - Use
M-SPC n a t
to open a list of all the TODOs in your notes.
Besides the above, the Doom standard SPC n
prefix has has a bunch of Org notes
keybindings that can operate on ~/notes
.
(defvar my/note-directory (my/concatd my/home-directory "notes" nil))
(my/map! "n" "notes"
:desc "find note" "f" 'my/note-find
:desc "search notes" "s" 'my/note-search
:desc "daily agenda" "a" 'my/note-agenda)
(defun my/note-find () (interactive) (org-roam-find-file))
(defun my/note-search () (interactive) (+default/org-notes-search))
(defun my/note-agenda () (interactive) (org-agenda))
(setq org-directory my/note-directory)
(setq +org-capture-notes-file "inbox.org")
(after! org-roam
(setq org-roam-directory org-directory)
(add-to-list 'org-roam-capture-templates
'("n" "note" plain (function org-roam--capture-get-point)
"\n%?"
:file-name "%<%Y%m%d%H%M%S>-${title}"
:head "#+TITLE: ${title}\n"
:immediate-finish t)))
Config management
This section has the functionality for editing, tangling, and compiling this literate config.
The literate config file should be stored in ~/notes/config.org
. It
can be opened with M-SPC c o
.
The tangled config file should be in ~/.doom.d/config.el
. It will be created
automatically when you tangle the literate config file. It can be opened with
M-SPC c O
. Any changes will be overwritten when you re-tangle.
There are three steps:
- Tangling: Extracting all the source code from the
config.org
to generate aconfig.el
- Compiling: Byte-compiling the
config.el
into aconfig.elc
(improves startup time). - (Re)loading: (Optional) Loads the config file directly into the running Emacs process without restarting.
There are functions for each stage of the process, plus convenience functions for doing multiple steps at once.
Use M-SPC c r
to tangle, compile, and reload the config. Use M-SPC c R
to
tangle, compile, and restart Emacs.
Reloading the config without restarting the process can result in unexpected behavior. Old keybinds, for example, will continue to exist despite no longer being defined.
Restarting the process (M-SPC c R
) is the most effective way to ensure things
are consistent after a config change.
(defvar my/config-literate-file (expand-file-name "config.org" my/note-directory))
(defvar my/config-tangled-file (expand-file-name "config.el" doom-private-dir))
(my/map! "c" "config"
:desc "tangle config" "t" 'my/config-tangle-compile
:desc "tangle & reload" "r" 'my/config-tangle-compile-reload
:desc "tangle & restart" "R" 'my/config-tangle-compile-restart
:desc "open config" "o" 'my/config-find
:desc "open tangled" "O" 'my/config-find-tangled)
(defun my/config-find () (interactive) (find-file my/config-literate-file))
(defun my/config-find-tangled () (interactive) (find-file my/config-tangled-file))
(defun my/config-tangle () (interactive) (org-babel-tangle-file my/config-literate-file my/config-tangled-file))
(defun my/config-compile () (interactive) (byte-compile-file my/config-tangled-file))
(defun my/config-reload () (interactive) (load (file-name-sans-extension my/config-tangled-file)))
(defun my/config-tangle-compile () (interactive) (my/config-tangle) (my/config-compile))
(defun my/config-tangle-compile-restart () (interactive) (my/config-tangle-compile) (doom/restart-and-restore))
(defun my/config-tangle-compile-reload () (interactive) (my/config-tangle-compile) (my/config-reload))
Blogging
This section exposes keybindings for managing a personal blog. By default, the
blog is assumed to use Hugo and be stored in ~/projects/blog
.
Org files can be published to the blog (hosted on Github Pages) with the following steps:
- Export current file to blog:
M-SPC b e
- Build static HTML:
M-SPC b b
- Push to origin:
M-SPC b p
This uses ox-hugo to do the export, and hugo to build the HTML.
Posts are configured using Org file-level properties. The list of supported properties is defined in ox-hugo’s docs.
Besides the default properties, the theme supports custom front-matter parameters:
summary
version
To generate a new Org document with all these properties pre-filled, create a
new note using the blog
capture template, like: M-SPC n f <blog title> RET b
.
I created the custom theme lukes-hugo-theme specifically for this workflow, but the exported posts should (mostly) work with any Hugo theme.
For a visual explanation of the blogging process, reference my post about my Blogging workflow.
(defvar my/blog-directory (my/concatd my/project-directory "blog" nil))
(defvar my/blog-posts-directory (my/concatd my/blog-directory "content" "posts" nil))
(my/map! "b" "blog"
:desc "open blog" "o" 'my/blog-project-open
:desc "export this file" "e" 'my/blog-export-file
:desc "serve" "s" 'my/blog-serve
:desc "serve drafts" "S" 'my/blog-serve-drafts
:desc "build" "b" 'my/blog-build
:desc "commit and push" "p" 'my/blog-push)
(defun my/blog-export-file ()
"Exports the current Org file to the blog."
(interactive)
(require 'ox-hugo)
(let ((notename (my/concatd my/blog-posts-directory (concat (file-name-base (buffer-file-name)) ".md"))))
(org-export-to-file 'hugo notename)))
(defun my/blog-serve ()
"Launches a server for local blog development (no drafts)."
(interactive)
(let ((default-directory my/blog-directory))
(shell-command "hugo serve &")))
(defun my/blog-serve-drafts ()
"Launches a server for local blog development (with drafts)."
(interactive)
(let ((default-directory my/blog-directory))
(shell-command "hugo serve -D &")))
(defun my/blog-build ()
"Builds the blog (no drafts)"
(interactive)
(let ((default-directory my/blog-directory))
(shell-command "hugo")))
(defun my/blog-push ()
"Commits and pushes all changed files."
(interactive)
(let ((default-directory my/blog-directory))
(shell-command "git add .")
(shell-command "git commit -a -m \"automatic update\"")
(shell-command "git push origin")))
(defun my/blog-project-open ()
"Opens the blog project."
(interactive)
(projectile-add-known-project my/blog-directory)
(+workspaces-switch-to-project-h my/blog-directory))
(defvar my/blog-post-head (concat "#+TITLE: ${title}\n#+BLOG: true\n#+DATE: %t\n#+HUGO_DRAFT: true\n#+HUGO_SLUG: ${slug}\n#+AUTHOR: " my/name "\n#+HUGO_TAGS: ${tags}\n#+HUGO_BASE_DIR: " my/blog-directory "\n#+HUGO_CUSTOM_FRONT_MATTER: :version 0 :summary ${summary}\n"))
(after! org
(setq org-plantuml-jar-path (expand-file-name "~/bin/plantuml.jar")))
(after! ox-hugo
(setq org-hugo-default-static-subdirectory-for-externals "attachments"))
(after! org-roam
(add-to-list 'org-roam-capture-templates
`("b" "blog post" plain (function org-roam--capture-get-point)
"\n%?"
:file-name "%<%Y%m%d%H%M%S>-${title}"
:head ,my/blog-post-head
:immediate-finish t
)))