Emacs config is an art, and I have learned a lot by reading through other people’s config files, and from many other resources. These are some of the best ones (several are also written in org mode). You will find snippets from all of these (and possibly others) throughout my config.
Note: a lot of manual configuration has been rendered moot by using Emacs Doom, which aggregates a well-maintained and organized collection of common configuration settings for performance optimization, package management, commonly used packages (e.g. Org) and much more.
Doom config file overview
Doom Emacs uses three config files:
init.el defines which of the existing Doom modules are loaded. A Doom module is a bundle of packages, configuration and commands, organized into a unit that can be toggled easily from this file.
packages.el defines which packages should be installed, beyond those that are installed and loaded as part of the enabled modules.
All the config files are generated from this Org file, to try and make its meaning as clear as possible. All package! declarations are written to packages.el, all other LISP code is written to config.el.
Config file headers
We start by simply defining the standard headers used by the three files. These headers come from the initial files generated by doom install, and contain either some Emacs-LISP relevant indicators like lexical-binding, or instructions about the contents of the file.
;;; init.el -*- lexical-binding: t; -*-;; DO NOT EDIT THIS FILE DIRECTLY;; This is a file generated from a literate programing source file located at;; https://gitlab.com/zzamboni/dot-doom/-/blob/master/doom.org;; You should make any changes there and regenerate it from Emacs org-mode;; using org-babel-tangle (C-c C-v t);; This file controls what Doom modules are enabled and what order they load;; in. Remember to run 'doom sync' after modifying it!;; NOTE Press 'SPC h d h' (or 'C-h d h' for non-vim users) to access Doom's;; documentation. There you'll find a "Module Index" link where you'll find;; a comprehensive list of Doom's modules and what flags they support.;; NOTE Move your cursor over a module's name (or its flags) and press 'K' (or;; 'C-c c k' for non-vim users) to view its documentation. This works on;; flags as well (those symbols that start with a plus).;;;; Alternatively, press 'gd' (or 'C-c c d') on a module to browse its;; directory (for easy access to its source code).
;; -*- no-byte-compile: t; -*-;;; $DOOMDIR/packages.el;; DO NOT EDIT THIS FILE DIRECTLY;; This is a file generated from a literate programing source file located at;; https://gitlab.com/zzamboni/dot-doom/-/blob/master/doom.org;; You should make any changes there and regenerate it from Emacs org-mode;; using org-babel-tangle (C-c C-v t);; To install a package with Doom you must declare them here and run 'doom sync';; on the command line, then restart Emacs for the changes to take effect -- or;; use 'M-x doom/reload'.;; To install SOME-PACKAGE from MELPA, ELPA or emacsmirror:;;(package! some-package);; To install a package directly from a remote git repo, you must specify a;; `:recipe'. You'll find documentation on what `:recipe' accepts here:;; https://github.com/raxod502/straight.el#the-recipe-format;;(package! another-package;; :recipe (:host github :repo "username/repo"));; If the package you are trying to install does not contain a PACKAGENAME.el;; file, or is located in a subdirectory of the repo, you'll need to specify;; `:files' in the `:recipe':;;(package! this-package;; :recipe (:host github :repo "username/repo";; :files ("some-file.el" "src/lisp/*.el")));; If you'd like to disable a package included with Doom, you can do so here;; with the `:disable' property:;;(package! builtin-package :disable t);; You can override the recipe of a built in package without having to specify;; all the properties for `:recipe'. These will inherit the rest of its recipe;; from Doom or MELPA/ELPA/Emacsmirror:;;(package! builtin-package :recipe (:nonrecursive t));;(package! builtin-package-2 :recipe (:repo "myfork/package"));; Specify a `:branch' to install a package from a particular branch or tag.;; This is required for some packages whose default branch isn't 'master' (which;; our package manager can't deal with; see raxod502/straight.el#279);;(package! builtin-package :recipe (:branch "develop"));; Use `:pin' to specify a particular commit to install.;;(package! builtin-package :pin "1a2b3c4d5e");; Doom's packages are pinned to a specific commit and updated from release to;; release. The `unpin!' macro allows you to unpin single packages...;;(unpin! pinned-package);; ...or multiple packages;;(unpin! pinned-package another-pinned-package);; ...Or *all* packages (NOT RECOMMENDED; will likely break things);;(unpin! t)
;;; $DOOMDIR/config.el -*- lexical-binding: t; -*-;; DO NOT EDIT THIS FILE DIRECTLY;; This is a file generated from a literate programing source file located at;; https://gitlab.com/zzamboni/dot-doom/-/blob/master/doom.org;; You should make any changes there and regenerate it from Emacs org-mode;; using org-babel-tangle (C-c C-v t);; Place your private configuration here! Remember, you do not need to run 'doom;; sync' after modifying this file!;; Some functionality uses this to identify you, e.g. GPG configuration, email;; clients, file templates and snippets.;; (setq user-full-name "John Doe";; user-mail-address "firstname.lastname@example.org");; Doom exposes five (optional) variables for controlling fonts in Doom. Here;; are the three important ones:;;;; + `doom-font';; + `doom-variable-pitch-font';; + `doom-big-font' -- used for `doom-big-font-mode'; use this for;; presentations or streaming.;;;; They all accept either a font-spec, font string ("Input Mono-12"), or xlfd;; font string. You generally only need these two:;; (setq doom-font (font-spec :family "monospace" :size 12 :weight 'semi-light);; doom-variable-pitch-font (font-spec :family "sans" :size 13));; There are two ways to load a theme. Both assume the theme is installed and;; available. You can either set `doom-theme' or manually load a theme with the;; `load-theme' function. This is the default:;; (setq doom-theme 'doom-one);; If you use `org' and don't want your org files in the default location below,;; change `org-directory'. It must be set before org loads!;; (setq org-directory "~/org/");; This determines the style of line numbers in effect. If set to `nil', line;; numbers are disabled. For relative line numbers, set this to `relative'.;; (setq display-line-numbers-type t);; Here are some additional functions/macros that could help you configure Doom:;;;; - `load!' for loading external *.el files relative to this one;; - `use-package!' for configuring packages;; - `after!' for running code after a package has loaded;; - `add-load-path!' for adding directories to the `load-path', relative to;; this file. Emacs searches the `load-path' when you load packages with;; `require' or `use-package'.;; - `map!' for binding new keys;;;; To get information about any of these functions/macros, move the cursor over;; the highlighted symbol at press 'K' (non-evil users must press 'C-c c k').;; This will open documentation for it, including demos of how they are used.;;;; You can also try 'gd' (or 'C-c c d') to jump to their definition and see how;; they are implemented.
Note: do not use M-x customize or the customize API in general. Doom is designed to be configured programmatically from your config.el, which can conflict with Customize’s way of modifying variables.
All necessary settings are therefore set by hand as part of this configuration file. The only exceptions are “safe variable” and “safe theme” settings, which are automatically saved by Emacs in custom.el, but this is OK as they don’t conflict with anything else from the config.
This code is written to the init.el to select which modules to load. Written here as-is for now, as it is quite well structured and clear.
When at the beginning of the line, make Ctrl-K remove the whole line, instead of just emptying it.
Disable line numbers.
;; This determines the style of line numbers in effect. If set to `nil', line;; numbers are disabled. For relative line numbers, set this to `relative'.(setqdisplay-line-numbers-typenil)
For some reason Doom disabled auto-save and backup files by default. Let’s reenable them.
Disable exit confirmation.
Visual, session and window settings
I made a super simple set of Doom-Emacs custom splash screens by combining a Doom logo with the word “Emacs” rendered in the Doom Font. You can see them at https://gitlab.com/zzamboni/dot-doom/-/tree/master/splash (you can also see one of them at the top of this file). I configure it to be used instead of the default splash screen. It took me all of 5 minutes to make, so improvements are welcome!
I like two of the images, so I select one at random.
In my previous configuration, I used to automatically restore the previous session upon startup. Doom Emacs starts up so fast that it does not feel right to do it automatically. In any case, from the Doom dashboard I can simply press Enter to invoke the first item, which is “Reload Last Session”. So this code is commented out now.
Maximize the window upon startup. The (fullscreen . maximized) value suggested in the Doom FAQ works, but results in a window that cannot be resized. For now I just manually set it to a large-enough window size by hand.
Doom Emacs has an extensive keybinding system, and most module functions are already bound. I modify some keybindings for simplicity of to match the muscle memory I have from my previous Emacs configuration.
Note: I do not use VI-style keybindings (which are the default for Doom) because I have decades of muscle memory with Emacs-style keybindings. You may need to adjust these if you want to use them.
Use counsel-buffer-or-recentf for C-x b. I like being able to see all recently opened files, instead of just the current ones. This makes it possible to use C-x b almost as a replacement for C-c C-f, for files that I edit often. Similarly, for switching between non-file buffers I use counsel-switch-buffer, mapped to C-x C-b.
The counsel-buffer-or-recentf function by default shows duplicated entries because it does not abbreviate the paths of the open buffers. The function below fixes this, I have submitted this change to the counsel library (https://github.com/abo-abo/swiper/pull/2687), in the meantime I define it here and integrate it via advice-add.
(defunzz/counsel-buffer-or-recentf-candidates()"Return candidates for `counsel-buffer-or-recentf'."(require'recentf)(recentf-mode)(let((buffers(delqnil(mapcar(lambda(b)(when(buffer-file-nameb)(abbreviate-file-name(buffer-file-nameb))))(delq(current-buffer)(buffer-list))))))(appendbuffers(cl-remove-if(lambda(f)(memberfbuffers))(counsel-recentf-candidates)))))(advice-add#'counsel-buffer-or-recentf-candidates:override#'zz/counsel-buffer-or-recentf-candidates)
Use +default/search-buffer for searching by default, I like the Swiper interface.
Map C-c C-g to magit-status - I have too ingrained muscle memory for this keybinding.
Interactive search key bindings - visual-regexp-steroids provides sane regular expressions and visual incremental search. I use the pcre2el package to support PCRE-style regular expressions.
(use-package!visual-regexp-steroids:defer3:config(require'pcre2el)(setqvr/engine'pcre2el)(map!"C-c s r"#'vr/replace)(map!"C-c s q"#'vr/query-replace))
The Doom undo package introduces the use of undo-fu, which makes undo/redo more “lineal”. I normally use C-/ for undo and Emacs doesn’t have a separate “redo” action, so I map C-? (in my keyboard, the same combination + Shift) for redo.
One of the few things I missed in Emacs from vi was the % key, which jumps to the parenthesis, bracket or brace which matches the one below the cursor. This function implements this functionality, bound to the same key. Inspired by http://www.emacswiki.org/emacs/NavigatingParentheses, but modified to use smartparens instead of the default commands, and to work on brackets and braces.
(after!smartparens(defunzz/goto-match-paren(arg)"Go to the matching paren/bracket, otherwise (or if ARG is not
nil) insert %. vi style of % jumping to matching brace."(interactive"p")(if(not(memqlast-command'(set-markcua-set-markzz/goto-match-parendown-listup-listend-of-defunbeginning-of-defunbackward-sexpforward-sexpbackward-up-listforward-paragraphbackward-paragraphend-of-bufferbeginning-of-bufferbackward-wordforward-wordmwheel-scrollbackward-wordforward-wordmouse-start-secondarymouse-yank-secondarymouse-secondary-save-then-killmove-end-of-linemove-beginning-of-linebackward-charforward-charscroll-upscroll-downscroll-leftscroll-rightmouse-set-pointnext-bufferprevious-bufferprevious-linenext-lineback-to-indentationdoom/backward-to-bol-or-indentdoom/forward-to-last-non-comment-or-eol)))(self-insert-command(orarg1))(cond((looking-at"\\s\(")(sp-forward-sexp)(backward-char1))((looking-at"\\s\)")(forward-char1)(sp-backward-sexp))(t(self-insert-command(orarg1))))))(map!"%"'zz/goto-match-paren))
Org-mode has become my primary tool for writing, blogging, coding, presentations and more. I am duly impressed. I have been a fan of the idea of literate programming for many years, and I have tried other tools before (most notably noweb, which I used during grad school for many of my homeworks and projects), but org-mode is the first tool I have encountered which seems to make it practical. Here are some of the resources I have found useful in learning it:
Enable Speed Keys, which allows quick single-key commands when the cursor is placed on a heading. Usually the cursor needs to be at the beginning of a headline line, but defining it with this function makes them active on any of the asterisks at the beginning of the line.
Now I define keybindings to access my commonly-used org files.
(zz/add-file-keybinding"C-c z w""~/Work/work.org.gpg""work.org")(zz/add-file-keybinding"C-c z i""~/org/ideas.org""ideas.org")(zz/add-file-keybinding"C-c z p""~/org/projects.org""projects.org")(zz/add-file-keybinding"C-c z d""~/org/diary.org""diary.org")
I’m still trying out org-roam, although I have not figured out very well how it works for my setup. For now I configure it to include my whole Org directory.
Using org-download to make it easier to insert images into my org notes. I don’t like the configuration provided by Doom as part of the (org +dragndrop) module, so I install the package by hand and configure it to my liking. I also define a new keybinding to paste an image from the clipboard, asking for the filename first.
(defunzz/org-download-paste-clipboard(&optionaluse-default-filename)(interactive"P")(require'org-download)(let((file(if(notuse-default-filename)(read-string(format"Filename [%s]: "org-download-screenshot-basename)nilnilorg-download-screenshot-basename)nil)))(org-download-clipboardfile)))(after!org(setqorg-download-method'directory)(setqorg-download-image-dir"images")(setqorg-download-heading-lvlnil)(setqorg-download-timestamp"%Y%m%d-%H%M%S_")(setqorg-image-actual-width300)(map!:maporg-mode-map"C-c l a y"#'zz/org-download-paste-clipboard"C-M-y"#'zz/org-download-paste-clipboard))
org-mac-link implements the ability to grab links from different Mac apps and insert them in the file. Bind C-c g to call org-mac-grab-link to choose an application and insert a link.
(use-package!org-mac-link:afterorg:config(setqorg-mac-grab-Acrobat-app-pnil); Disable grabbing from Adobe Acrobat(setqorg-mac-grab-devonthink-app-pnil); Disable grabbinb from DevonThink(map!:maporg-mode-map"C-c g"#'org-mac-grab-link))
Tasks and agenda
Customize the agenda display to indent todo items by level to show nesting, and enable showing holidays in the Org agenda display.
(after!org-agenda(setqorg-agenda-prefix-format'((agenda." %i %-12:c%?-12t% s");; Indent todo items by level to show nesting(todo." %i %-12:c%l")(tags." %i %-12:c")(search." %i %-12:c")))(setqorg-agenda-include-diaryt))
Install and load some custom local holiday lists I’m interested in.
I configure org-archive to archive completed TODOs by default to the archive.org file in the same directory as the source file, under the “date tree” corresponding to the task’s CLOSED date - this allows me to easily separate work from non-work stuff. Note that this can be overridden for specific files by specifying the desired value of org-archive-location in the #+archive: property at the top of the file.
I am trying out Trevoke’s org-gtd. I haven’t figured out my perfect workflow for tracking GTD with Org yet, but this looks like a very promising approach.
(use-package!org-gtd:afterorg:config;; where org-gtd will put its files. This value is also the default one.(setqorg-gtd-directory"~/gtd/");; package: https://github.com/Malabarba/org-agenda-property;; this is so you can see who an item was delegated to in the agenda(setqorg-agenda-property-list'("DELEGATED_TO"));; I think this makes the agenda easier to read(setqorg-agenda-property-position'next-line);; package: https://www.nongnu.org/org-edna-el/;; org-edna is used to make sure that when a project task gets DONE,;; the next TODO is automatically changed to NEXT.(setqorg-edna-use-inheritancet)(org-edna-load):bind(("C-c d c".org-gtd-capture);; add item to inbox("C-c d a".org-agenda-list);; see what's on your plate today("C-c d p".org-gtd-process-inbox);; process entire inbox("C-c d n".org-gtd-show-all-next);; see all NEXT items("C-c d s".org-gtd-show-stuck-projects);; see projects that don't have a NEXT item("C-c d f".org-gtd-clarify-finalize)));; the keybinding to hit when you're done editing an item in the processing phase
We define the corresponding Org-GTD capture templates.
(after!(org-gtdorg-capture)(add-to-list'org-capture-templates'("i""GTD item"entry(file(lambda()(org-gtd--pathorg-gtd-inbox-file-basename)))"* %?\n%U\n\n %i":kill-buffert))(add-to-list'org-capture-templates'("l""GTD item with link to where you are in emacs now"entry(file(lambda()(org-gtd--pathorg-gtd-inbox-file-basename)))"* %?\n%U\n\n %i\n %a":kill-buffert))(add-to-list'org-capture-templates'("m""GTD item with link to current Outlook mail message"entry(file(lambda()(org-gtd--pathorg-gtd-inbox-file-basename)))"* %?\n%U\n\n %i\n %(org-mac-outlook-message-get-links)":kill-buffert)))
I set up an advice before org-capture to make sure org-gtd and org-capture are loaded, which triggers the setup of the templates above.
I use LeanPub for self-publishing my books. Fortunately, it is possible to export from org-mode to both LeanPub-flavored Markdown and Markua, the new preferred Leanpub markup format, so I can use Org for writing the text and simply export it in the correct format and structure needed by Leanpub.
When I decided to use org-mode to write my books, I looked around for existing modules and code. Here are some of the resources I found:
I highly recommend using Markua rather than Markdown, as it is the future that Leanpub is guaranteed to support in the future, and where most of the new features are being developed.
With this setup, I can write my book in org-mode (I usually keep a single book.org file at the top of my repository), and then call the corresponding “Book” export commands. The manuscript directory, as well as the corresponding Book.txt and other necessary files are created and populated automatically.
ox-hugo is an awesome way to blog from org-mode. It makes it possible for posts in org-mode format to be kept separate, and it generates the Markdown files for Hugo. Hugo supports org files, but using ox-hugo has multiple advantages:
Parsing is done by org-mode natively, not by an external library. Although goorgeous (used by Hugo) is very good, it still lacks in many areas, which leads to text being interpreted differently as by org-mode.
Hugo is left to parse a native Markdown file, which means that many of its features such as shortcodes, TOC generation, etc., can still be used on the generated file.
Doom Emacs includes and configures ox-hugo as part of its (:lang org +hugo) module, so all that’s left is to configure some parameters to my liking.
I set org-hugo-use-code-for-kbd so that I can apply a custom style to keyboard bindings in my blog.
Code for org-mode macros
Here I define functions which get used in some of my org-mode macros
The first is a support function which gets used in some of the following, to return a string (or an optional custom string) only if it is a non-zero, non-whitespace string, and nil otherwise.
This function receives three arguments, and returns the org-mode code for a link to the Hammerspoon API documentation for the link module, optionally to a specific function. If desc is passed, it is used as the display text, otherwise section.function is used.
Split STR at spaces and wrap each element with the ~ char, separated by +. Zero-width spaces are inserted around the plus signs so that they get formatted correctly. Envisioned use is for formatting keybinding descriptions. There are two versions of this function: “outer” wraps each element in ~, the “inner” wraps the whole sequence in them.
I picked up this little gem in the org mailing list. A function that reformats the current buffer by regenerating the text from its internal parsed representation. Quite amazing.
(defunzz/org-reformat-buffer()(interactive)(when(y-or-n-p"Really format current buffer? ")(let((document(org-element-interpret-data(org-element-parse-buffer))))(erase-buffer)(insertdocument)(goto-char(point-min)))))
Avoiding non-Org mode files
org-pandoc-import is a mode that automates conversions to/from Org mode as much as possible.
I use org-re-reveal to make presentations. The functions below help me improve my workflow by automatically exporting the slides whenever I save the file, refreshing the presentation in my browser, and moving it to the slide where the cursor was when I saved the file. This helps keeping a “live” rendering of the presentation next to my Emacs window.
The first function is a modified version of the org-num--number-region function of the org-num package, but modified to only return the numbering of the innermost headline in which the cursor is currently placed.
(defunzz/org-current-headline-number()"Get the numbering of the innermost headline which contains the
cursor. Returns nil if the cursor is above the first level-1
headline, or at the very end of the file. Does not count
headlines tagged with :noexport:"(require'org-num)(let((org-num--numberingnil)(original-point(point)))(save-mark-and-excursion(let((newnil))(org-map-entries(lambda()(when(org-at-heading-p)(let*((level(nth1(org-heading-components)))(numbering(org-num--current-numberinglevelnil)))(let*((current-subtree(save-excursion(org-element-at-point)))(point-in-subtree(<=(org-element-property:begincurrent-subtree)original-point(1-(org-element-property:endcurrent-subtree)))));; Get numbering to current headline if the cursor is in it.(whenpoint-in-subtree(pushnumberingnew))))))"-noexport");; New contains all the trees that contain the cursor (i.e. the;; innermost and all its parents), so we only return the innermost one.;; We reverse its order to make it more readable.(reverse(carnew))))))
The zz/refresh-reveal-prez function makes use of the above to perform the presentation export, refresh and update. You can use it by adding an after-save hook like this (add at the end of the file):
* Local variables :ARCHIVE:noexport:
# Local variables:
# eval: (add-hook! after-save :append :local (zz/refresh-reveal-prez))
Note #1: This is specific to my OS (macOS) and the browser I use (Brave). I will make it more generic in the future, but for now feel free to change it to your needs.
Note #2: the presentation must be already open in the browser, so you must run “Export to reveal.js -> To file and browse” (C-c C-e v b) once by hand.
(defunzz/refresh-reveal-prez();; Export the file(org-re-reveal-export-to-html)(let*((slide-list(zz/org-current-headline-number));; Get the current slide number(slide-str(string-join(mapcar#'number-to-stringslide-list)"-"));; Determine the filename to use(file(concat(file-name-directory(buffer-file-name))(org-export-output-file-name".html"nil)));; Final URL including the slide number(uri(concat"file://"file"#/slide-"slide-str));; Get the document title(title(cadar(org-collect-keywords'("TITLE"))));; Command to reload the browser and move to the correct slide(cmd(concat"osascript -e \"tell application \\\"Brave\\\" to repeat with W in windows
set i to 0
repeat with T in (tabs in W)
set i to i + 1
if title of T is \\\""title"\\\" then
set URL of T to \\\""uri"\\\"
set (active tab index of W) to i
end repeat\"")));; Short sleep seems necessary for the file changes to be noticed(sleep-for0.2)(call-process-shell-commandcmd)))
Some useful settings for LISP coding - smartparens-strict-mode to enforce parenthesis to match. I map M-( to enclose the next expression as in paredit using a custom function. Prefix argument can be used to indicate how many expressions to enclose instead of just 1. E.g. C-u 3 M-( will enclose the next 3 sexps.
Trying out Magit’s multi-repository abilities. This stays in sync with the git repo list used by my chain:summary-status Elvish shell function by reading the file every time magit-list-repositories is called, using defadvice!. I also customize the display to add the Status column.
(after!magit(setqzz/repolist"~/.elvish/package-data/elvish-themes/chain-summary-repos.json")(defadvice!+zz/load-magit-repositories():before#'magit-list-repositories(setqmagit-repository-directories(seq-map(lambda(e)(conse0))(json-read-filezz/repolist))))(setqmagit-repolist-columns'(("Name"25magit-repolist-column-identnil)("Status"7magit-repolist-column-flagnil)("B<U"3magit-repolist-column-unpulled-from-upstream((:right-alignt)(:help-echo"Upstream changes not in branch")))("B>U"3magit-repolist-column-unpushed-to-upstream((:right-alignt)(:help-echo"Local changes not in upstream")))("Path"99magit-repolist-column-pathnil))))
I prefer to use the GPG graphical PIN entry utility. This is achieved by setting epg-pinentry-mode (epa-pinentry-mode before Emacs 27) to nil instead of the default 'loopback.
I find iedit absolutely indispensable when coding. In short: when you hit Ctrl-;, all occurrences of the symbol under the cursor (or the current selection) are highlighted, and any changes you make on one of them will be automatically applied to all others. It’s great for renaming variables in code, but it needs to be used with care, as it has no idea of semantics, it’s a plain string replacement, so it can inadvertently modify unintended parts of the code.
(defmacrozz/measure-time(&restbody)"Measure the time it takes to evaluate BODY."`(let((time(current-time))),@body(float-time(time-sincetime))))
I’m still not fully convinced of running a terminal inside Emacs, but vterm is much nicer than any of the previous terminal emulators, so I’m giving it a try. I configure it so that it runs my favorite shell. Vterm runs Elvish flawlessly!
Some experimental code to list functions which are not native-compiled. Sort of works but its very slow. This does not get tangled to my config.el, I just keep it here for reference.