Emacs

An icon indicating this blurb contains information

Earlier releases of this book included my previous, hand-maintained Emacs configuration. I have replaced it with my Doom config because it is what I use now, and also because it is a good example of generating multiple config files from the same Org file. My old Emacs config is still available at https://github.com/zzamboni/dot-emacs/blob/master/init.org.

An icon of a key

The contents of this section is included directly from my real Doom Emacs config file at https://github.com/zzamboni/dot-doom/blob/master/doom.org, using the following Org directive (the first 15 lines are skipped because they contain some global document directives which do not apply in this context):

1 #+include: "~/.doom.d/doom.org" :lines "14-"

This is my Doom Emacs configuration. From this org file, all the necessary Doom Emacs config files are generated.

This file is written in literate programming style using org-mode. See init.el, packages.el and config.el for the generated files. You can see this in a nicer format on my blog post My Doom Emacs configuration, with commentary.

References

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.
  • config.el contains all custom configuration and code.

There are other files that can be loaded, but theses are the main ones. The load order of different files is defined depending on the type of session being started.

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.

Figure 5. →init.el
 1 ;;; init.el -*- lexical-binding: t; -*-
 2 
 3 ;; DO NOT EDIT THIS FILE DIRECTLY
 4 ;; This is a file generated from a literate programing source file located at
 5 ;; https://gitlab.com/zzamboni/dot-doom/-/blob/master/doom.org
 6 ;; You should make any changes there and regenerate it from Emacs org-mode
 7 ;; using org-babel-tangle (C-c C-v t)
 8 
 9 ;; This file controls what Doom modules are enabled and what order they load
10 ;; in. Remember to run 'doom sync' after modifying it!
11 
12 ;; NOTE Press 'SPC h d h' (or 'C-h d h' for non-vim users) to access Doom's
13 ;;      documentation. There you'll find a "Module Index" link where you'll find
14 ;;      a comprehensive list of Doom's modules and what flags they support.
15 
16 ;; NOTE Move your cursor over a module's name (or its flags) and press 'K' (or
17 ;;      'C-c c k' for non-vim users) to view its documentation. This works on
18 ;;      flags as well (those symbols that start with a plus).
19 ;;
20 ;;      Alternatively, press 'gd' (or 'C-c c d') on a module to browse its
21 ;;      directory (for easy access to its source code).
Figure 6. →packages.el
 1 ;; -*- no-byte-compile: t; -*-
 2 ;;; $DOOMDIR/packages.el
 3 
 4 ;; DO NOT EDIT THIS FILE DIRECTLY
 5 ;; This is a file generated from a literate programing source file located at
 6 ;; https://gitlab.com/zzamboni/dot-doom/-/blob/master/doom.org
 7 ;; You should make any changes there and regenerate it from Emacs org-mode
 8 ;; using org-babel-tangle (C-c C-v t)
 9 
10 ;; To install a package with Doom you must declare them here and run 'doom sync'
11 ;; on the command line, then restart Emacs for the changes to take effect -- or
12 ;; use 'M-x doom/reload'.
13 
14 ;; To install SOME-PACKAGE from MELPA, ELPA or emacsmirror:
15 ;;(package! some-package)
16 
17 ;; To install a package directly from a remote git repo, you must specify a
18 ;; `:recipe'. You'll find documentation on what `:recipe' accepts here:
19 ;; https://github.com/raxod502/straight.el#the-recipe-format
20 ;;(package! another-package
21 ;;  :recipe (:host github :repo "username/repo"))
22 
23 ;; If the package you are trying to install does not contain a PACKAGENAME.el
24 ;; file, or is located in a subdirectory of the repo, you'll need to specify
25 ;; `:files' in the `:recipe':
26 ;;(package! this-package
27 ;;  :recipe (:host github :repo "username/repo"
28 ;;           :files ("some-file.el" "src/lisp/*.el")))
29 
30 ;; If you'd like to disable a package included with Doom, you can do so here
31 ;; with the `:disable' property:
32 ;;(package! builtin-package :disable t)
33 
34 ;; You can override the recipe of a built in package without having to specify
35 ;; all the properties for `:recipe'. These will inherit the rest of its recipe
36 ;; from Doom or MELPA/ELPA/Emacsmirror:
37 ;;(package! builtin-package :recipe (:nonrecursive t))
38 ;;(package! builtin-package-2 :recipe (:repo "myfork/package"))
39 
40 ;; Specify a `:branch' to install a package from a particular branch or tag.
41 ;; This is required for some packages whose default branch isn't 'master' (which
42 ;; our package manager can't deal with; see raxod502/straight.el#279)
43 ;;(package! builtin-package :recipe (:branch "develop"))
44 
45 ;; Use `:pin' to specify a particular commit to install.
46 ;;(package! builtin-package :pin "1a2b3c4d5e")
47 
48 ;; Doom's packages are pinned to a specific commit and updated from release to
49 ;; release. The `unpin!' macro allows you to unpin single packages...
50 ;;(unpin! pinned-package)
51 ;; ...or multiple packages
52 ;;(unpin! pinned-package another-pinned-package)
53 ;; ...Or *all* packages (NOT RECOMMENDED; will likely break things)
54 ;;(unpin! t)
Figure 7. →config.el
 1 ;;; $DOOMDIR/config.el -*- lexical-binding: t; -*-
 2 
 3 ;; DO NOT EDIT THIS FILE DIRECTLY
 4 ;; This is a file generated from a literate programing source file located at
 5 ;; https://gitlab.com/zzamboni/dot-doom/-/blob/master/doom.org
 6 ;; You should make any changes there and regenerate it from Emacs org-mode
 7 ;; using org-babel-tangle (C-c C-v t)
 8 
 9 ;; Place your private configuration here! Remember, you do not need to run 'doom
10 ;; sync' after modifying this file!
11 
12 ;; Some functionality uses this to identify you, e.g. GPG configuration, email
13 ;; clients, file templates and snippets.
14 ;; (setq user-full-name "John Doe"
15 ;;      user-mail-address "john@doe.com")
16 
17 ;; Doom exposes five (optional) variables for controlling fonts in Doom. Here
18 ;; are the three important ones:
19 ;;
20 ;; + `doom-font'
21 ;; + `doom-variable-pitch-font'
22 ;; + `doom-big-font' -- used for `doom-big-font-mode'; use this for
23 ;;   presentations or streaming.
24 ;;
25 ;; They all accept either a font-spec, font string ("Input Mono-12"), or xlfd
26 ;; font string. You generally only need these two:
27 ;; (setq doom-font (font-spec :family "monospace" :size 12 :weight 'semi-light)
28 ;;       doom-variable-pitch-font (font-spec :family "sans" :size 13))
29 
30 ;; There are two ways to load a theme. Both assume the theme is installed and
31 ;; available. You can either set `doom-theme' or manually load a theme with the
32 ;; `load-theme' function. This is the default:
33 ;; (setq doom-theme 'doom-one)
34 
35 ;; If you use `org' and don't want your org files in the default location below,
36 ;; change `org-directory'. It must be set before org loads!
37 ;; (setq org-directory "~/org/")
38 
39 ;; This determines the style of line numbers in effect. If set to `nil', line
40 ;; numbers are disabled. For relative line numbers, set this to `relative'.
41 ;; (setq display-line-numbers-type t)
42 
43 ;; Here are some additional functions/macros that could help you configure Doom:
44 ;;
45 ;; - `load!' for loading external *.el files relative to this one
46 ;; - `use-package!' for configuring packages
47 ;; - `after!' for running code after a package has loaded
48 ;; - `add-load-path!' for adding directories to the `load-path', relative to
49 ;;   this file. Emacs searches the `load-path' when you load packages with
50 ;;   `require' or `use-package'.
51 ;; - `map!' for binding new keys
52 ;;
53 ;; To get information about any of these functions/macros, move the cursor over
54 ;; the highlighted symbol at press 'K' (non-evil users must press 'C-c c k').
55 ;; This will open documentation for it, including demos of how they are used.
56 ;;
57 ;; You can also try 'gd' (or 'C-c c d') to jump to their definition and see how
58 ;; they are implemented.

Customized variables

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.

Doom modules

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.

Figure 8. →init.el
  1 (doom!
  2  :input
  3  ;;chinese
  4  ;;japanese
  5  ;;layout              ; auie,ctsrnm is the superior home row
  6 
  7  :completion
  8  (company +childframe) ; the ultimate code completion backend
  9  ;;helm                ; the *other* search engine for love and life
 10  ;;ido                 ; the other *other* search engine...
 11  (ivy +prescient -childframe
 12       -fuzzy +icons)   ; a search engine for love and life
 13 
 14  :ui
 15  ;;deft                ; notational velocity for Emacs
 16  doom                  ; what makes DOOM look the way it does
 17  doom-dashboard        ; a nifty splash screen for Emacs
 18  ;;doom-quit           ; DOOM quit-message prompts when you quit Emacs
 19  ;;fill-column         ; a `fill-column' indicator
 20  hl-todo               ; highlight TODO/FIXME/NOTE/DEPRECATED/HACK/REVIEW
 21  ;;hydra
 22  ;;indent-guides       ; highlighted indent columns
 23  (ligatures +extra)    ; ligatures or substitute text with pretty symbols
 24  ;;minimap             ; show a map of the code on the side
 25  modeline              ; snazzy, Atom-inspired modeline, plus API
 26  nav-flash             ; blink cursor line after big motions
 27  ;;neotree             ; a project drawer, like NERDTree for vim
 28  ophints               ; highlight the region an operation acts on
 29  (popup +defaults)   ; tame sudden yet inevitable temporary windows
 30  ;;tabs                ; a tab bar for Emacs
 31  ;;treemacs            ; a project drawer, like neotree but cooler
 32  ;;unicode             ; extended unicode support for various languages
 33  ;;vc-gutter           ; vcs diff in the fringe
 34  vi-tilde-fringe       ; fringe tildes to mark beyond EOB
 35  window-select         ; visually switch windows
 36  workspaces            ; tab emulation, persistence & separate workspaces
 37  zen                   ; distraction-free coding or writing
 38 
 39  :editor
 40  ;;(evil +everywhere)  ; come to the dark side, we have cookies
 41  file-templates        ; auto-snippets for empty files
 42  ;;fold                ; (nigh) universal code folding
 43  ;;(format +onsave)    ; automated prettiness
 44  ;;god                 ; run Emacs commands without modifier keys
 45  ;;lispy               ; vim for lisp, for people who don't like vim
 46  ;;multiple-cursors    ; editing in many places at once
 47  ;;objed               ; text object editing for the innocent
 48  ;;parinfer            ; turn lisp into python, sort of
 49  ;;rotate-text         ; cycle region at point between text candidates
 50  snippets              ; my elves. They type so I don't have to
 51  ;;word-wrap           ; soft wrapping with language-aware indent
 52 
 53  :emacs
 54  dired                 ; making dired pretty [functional]
 55  electric              ; smarter, keyword-based electric-indent
 56  ;;ibuffer             ; interactive buffer management
 57  undo                  ; persistent, smarter undo for your inevitable mistakes
 58  vc                    ; version-control and Emacs, sitting in a tree
 59 
 60  :term
 61  ;;eshell              ; the elisp shell that works everywhere
 62  ;;shell               ; simple shell REPL for Emacs
 63  ;;term                ; basic terminal emulator for Emacs
 64  vterm                 ; the best terminal emulation in Emacs
 65 
 66  :checkers
 67  (syntax +childframe)  ; tasing you for every semicolon you forget
 68  spell                 ; tasing you for misspelling mispelling
 69  ;;grammar             ; tasing grammar mistake every you make
 70 
 71  :tools
 72  ansible
 73  debugger              ; FIXME stepping through code, to help you add bugs
 74  ;;direnv
 75  ;;docker
 76  ;;editorconfig        ; let someone else argue about tabs vs spaces
 77  ;;ein                 ; tame Jupyter notebooks with emacs
 78  (eval +overlay)       ; run code, run (also, repls)
 79  gist                  ; interacting with github gists
 80  lookup                ; navigate your code and its documentation
 81  lsp
 82  (magit +forge)        ; a git porcelain for Emacs
 83  ;;make                ; run make tasks from Emacs
 84  pass                  ; password manager for nerds
 85  ;;pdf                 ; pdf enhancements
 86  ;;prodigy             ; FIXME managing external services & code builders
 87  ;;rgb                 ; creating color strings
 88  ;;taskrunner          ; taskrunner for all your projects
 89  ;;terraform           ; infrastructure as code
 90  ;;tmux                ; an API for interacting with tmux
 91  ;;upload              ; map local to remote projects via ssh/ftp
 92 
 93  :os
 94  (:if IS-MAC macos)    ; improve compatibility with macOS
 95  ;;tty                 ; improve the terminal Emacs experience
 96 
 97  :lang
 98  ;;agda                ; types of types of types of types...
 99  ;;cc                  ; C/C++/Obj-C madness
100  ;;clojure             ; java with a lisp
101  common-lisp           ; if you've seen one lisp, you've seen them all
102  ;;coq                 ; proofs-as-programs
103  ;;crystal             ; ruby at the speed of c
104  ;;csharp              ; unity, .NET, and mono shenanigans
105  ;;data                ; config/data formats
106  ;;(dart +flutter)     ; paint ui and not much else
107  ;;elixir              ; erlang done right
108  ;;elm                 ; care for a cup of TEA?
109  emacs-lisp            ; drown in parentheses
110  ;;erlang              ; an elegant language for a more civilized age
111  (ess +lsp)            ; emacs speaks statistics
112  ;;faust               ; dsp, but you get to keep your soul
113  ;;fsharp              ; ML stands for Microsoft's Language
114  ;;fstar               ; (dependent) types and (monadic) effects and Z3
115  ;;gdscript            ; the language you waited for
116  (go +lsp)             ; the hipster dialect
117  ;;(haskell +dante)    ; a language that's lazier than I am
118  ;;hy                  ; readability of scheme w/ speed of python
119  ;;idris               ; a language you can depend on
120  json                  ; At least it ain't XML
121  ;;(java +meghanada)   ; the poster child for carpal tunnel syndrome
122  ;;javascript          ; all(hope(abandon(ye(who(enter(here))))))
123  ;;julia               ; a better, faster MATLAB
124  ;;kotlin              ; a better, slicker Java(Script)
125  (latex +latexmk)      ; writing papers in Emacs has never been so fun
126  ;;lean
127  ;;factor
128  ;;ledger              ; an accounting system in Emacs
129  lua                   ; one-based indices? one-based indices
130  markdown              ; writing docs for people to ignore
131  ;;nim                 ; python + lisp at the speed of c
132  ;;nix                 ; I hereby declare "nix geht mehr!"
133  ;;ocaml               ; an objective camel
134  (org +pretty +journal ;-dragndrop
135       +hugo +roam +pandoc
136       +present)        ; organize your plain life in plain text
137  ;;php                 ; perl's insecure younger brother
138  plantuml              ; diagrams for confusing people more
139  ;;purescript          ; javascript, but functional
140  python                ; beautiful is better than ugly
141  ;;qt                  ; the 'cutest' gui framework ever
142  racket                ; a DSL for DSLs
143  ;;raku                ; the artist formerly known as perl6
144  ;;rest                ; Emacs as a REST client
145  rst                   ; ReST in peace
146  ;;(ruby +rails)       ; 1.step {|i| p "Ruby is #{i.even? ? 'love' : 'life'}"}
147  rust                  ; Fe2O3.unwrap().unwrap().unwrap().unwrap()
148  ;;scala               ; java, but good
149  ;;scheme              ; a fully conniving family of lisps
150  (sh +lsp)             ; she sells {ba,z,fi}sh shells on the C xor
151  ;;sml
152  ;;solidity            ; do you need a blockchain? No.
153  ;;swift               ; who asked for emoji variables?
154  ;;terra               ; Earth and Moon in alignment for performance.
155  ;;web                 ; the tubes
156  (yaml +lsp)           ; JSON, but readable
157 
158  :email
159  ;;(mu4e +gmail)
160  ;;notmuch
161  ;;(wanderlust +gmail)
162 
163  :app
164  ;;calendar
165  everywhere            ; *leave* Emacs!? You must be joking
166  irc                   ; how neckbeards socialize
167  ;;(rss +org)          ; emacs as an RSS reader
168  ;;twitter             ; twitter client https://twitter.com/vnought
169 
170  :config
171  ;;literate
172  (default +bindings +smartparens))

General configuration

My user information.

1 (setq user-full-name "Diego Zamboni"
2       user-mail-address "diego@zzamboni.org")

Change the Mac and Linux modifiers to my liking. I also disable passing Control characters to the system, to avoid that C-M-space launches the Character viewer instead of running mark-sexp.

1 (cond (IS-MAC
2        (setq mac-command-modifier       'meta
3              mac-option-modifier        'alt
4              mac-right-option-modifier  'alt
5              mac-pass-control-to-system nil))
6       (IS-LINUX
7        (setq x-meta-keysym 'super
8              x-super-keysym 'meta)))

When at the beginning of the line, make Ctrl-K remove the whole line, instead of just emptying it.

1 (setq kill-whole-line t)

Disable line numbers.

1 ;; This determines the style of line numbers in effect. If set to `nil', line
2 ;; numbers are disabled. For relative line numbers, set this to `relative'.
3 (setq display-line-numbers-type nil)

For some reason Doom disables auto-save and backup files by default. Let’s reenable them.

1 (setq auto-save-default t
2       make-backup-files t)

Disable exit confirmation.

1 (setq confirm-kill-emacs nil)

Doom configures auth-sources by default to include the Keychain on macOS, but it puts it at the beginning of the list. This causes creation of auth items to fail because the macOS Keychain sources do not support creation yet. I reverse it to leave ~/.authinfo.gpg at the beginning.

1 (after! auth-source
2   (setq auth-sources (nreverse auth-sources)))

Auto-save the desktop setup (open buffers, etc.)

1 (desktop-save-mode 1)
2 (setq! desktop-load-locked-desktop t)

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!

If you want to choose at random among a few different splash images, you can list them in alternatives.

You can find other splash images at the jeetelongname/doom-banners GitHub repository.

1 (let ((alternatives '("doom-emacs-bw-light.svg"
2                       "doom-emacs-flugo-slant_out_purple-small.png"
3                       "doom-emacs-flugo-slant_out_bw-small.png")))
4   (setq fancy-splash-image
5         (concat doom-private-dir "splash/"
6                 (nth (random (length alternatives)) alternatives))))

I eliminate all but the first two items in the dashboard menu, since those are the only ones I still use sometimes.

1 (setq +doom-dashboard-menu-sections (cl-subseq +doom-dashboard-menu-sections 0 2))

Set base and variable-pitch fonts. I currently like Fira Code and Alegreya (another favorite and my previous choice: ET Book).

1 (setq doom-font (font-spec :family "Fira Code Nerd Font" :size 16)
2       ;;doom-variable-pitch-font (font-spec :family "ETBembo" :size 18)
3       doom-variable-pitch-font (font-spec :family "Alegreya" :size 16))

Allow mixed fonts in a buffer. This is particularly useful for Org mode, so I can mix source and prose blocks in the same document. I also manually enable solaire-mode in Org mode as a workaround for font scaling not working properly.

1 (add-hook! 'org-mode-hook #'mixed-pitch-mode)
2 ;;(add-hook! 'org-mode-hook #'solaire-mode)
3 (setq mixed-pitch-variable-pitch-cursor nil)

Keybindings to increase/decrease font size.

1 (map! "C-="   #'doom/increase-font-size
2       "C--"   #'doom/decrease-font-size
3       "C-0"   #'doom/reset-font-size)

Set the theme to use. I like the Spacemacs-Light, which does not come with Doom, so we need to install it from package.el:

Figure 9. →packages.el
1 (package! spacemacs-theme)
2 ;; Trying https://github.com/agraul/doom-alabaster-theme
3 ;;(package! doom-alabaster-theme :recipe (:host github :repo "agraul/doom-alabaster-theme"))

And then from config.el we specify the theme to use.

1 ;;(setq doom-theme 'doom-alabaster)
2 (setq doom-theme 'spacemacs-light)
3 ;;(setq doom-theme 'doom-nord-light) ;;OK
4 ;;NO (setq doom-theme 'doom-solarized-light)
5 ;;(setq doom-theme 'doom-one-light) ;;MAYBE
6 ;;NO (setq doom-theme 'doom-opera-light)
7 ;;NO (setq doom-theme 'doom-tomorrow-day)
8 ;;NO (setq doom-theme 'doom-acario-light)

I love the spacemacs-light theme, but for some reason, the transparent dashboard images showed up with a light tint, which I eventually tracked to the fact that Doom by default uses the font-lock-comment-face for the dashboard banner image, and this this face has a background color in Spacemacs-light. I redefine the doom-dashboard-banner face to use the default face, which fixes the problem. Another way to fix it (commented out below) is to disable the background tint color in the theme. While we are at it, I also fix doom-dashboard-loaded, which suffers from the same problem.

1 (custom-set-faces!
2   '(doom-dashboard-banner :inherit default)
3   '(doom-dashboard-loaded :inherit default))
4 ;;(setq spacemacs-theme-comment-bg nil)

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.

1 ;;(add-hook 'window-setup-hook #'doom/quickload-session)

Maximize the window upon startup.

1 ;;(setq initial-frame-alist '((top . 1) (left . 1) (width . 114) (height . 32)))
2 ;;(add-to-list 'initial-frame-alist '(maximized))

Truncate lines in ivy childframes. Thanks Henrik! (disabled for now)

1 (setq posframe-arghandler
2       (lambda (buffer-or-name key value)
3         (or (and (eq key :lines-truncate)
4                  (equal ivy-posframe-buffer
5                         (if (stringp buffer-or-name)
6                             buffer-or-name
7                           (buffer-name buffer-or-name)))
8                  t)
9             value)))

I like ligatures, but some of the ones that get enabled by the (ligatures +extra) module don’t work in the font I use, or I don’t like them, so I disable them.

 1 (plist-put! +ligatures-extra-symbols
 2   :and           nil
 3   :or            nil
 4   :for           nil
 5   :not           nil
 6   :true          nil
 7   :false         nil
 8   :int           nil
 9   :float         nil
10   :str           nil
11   :bool          nil
12   :list          nil
13 )
1 (let ((ligatures-to-disable '(:true :false :int :float :str :bool :list :and :or :for :not)))
2   (dolist (sym ligatures-to-disable)
3     (plist-put! +ligatures-extra-symbols sym nil)))

Enable showing a word count in the modeline. This is only shown for the modes listed in doom-modeline-continuous-word-count-modes (Markdown, GFM and Org by default).

1 (setq doom-modeline-enable-word-count t)

Enable pixel scrolling

1 ;;(pixel-scroll-precision-mode 1)

Key bindings

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.

Miscellaneous keybindings

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.

1 (map! "C-x b"   #'counsel-buffer-or-recentf
2       "C-x C-b" #'counsel-switch-buffer)

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.

 1 (defun zz/counsel-buffer-or-recentf-candidates ()
 2   "Return candidates for `counsel-buffer-or-recentf'."
 3   (require 'recentf)
 4   (recentf-mode)
 5   (let ((buffers
 6          (delq nil
 7                (mapcar (lambda (b)
 8                          (when (buffer-file-name b)
 9                            (abbreviate-file-name (buffer-file-name b))))
10                        (delq (current-buffer) (buffer-list))))))
11     (append
12      buffers
13      (cl-remove-if (lambda (f) (member f buffers))
14                    (counsel-recentf-candidates)))))
15 
16 (advice-add #'counsel-buffer-or-recentf-candidates
17             :override #'zz/counsel-buffer-or-recentf-candidates)

The switch-buffer-functions package allows us to update the recentf buffer list as we switch between them, so that the list produced by counsel-buffer-or-recentf is shown in the order the buffers have been visited, rather than in the order they were opened. Thanks to @tau3000 for the tip.

Figure 10. →packages.el
1 (package! switch-buffer-functions)
1 (use-package! switch-buffer-functions
2   :after recentf
3   :preface
4   (defun my-recentf-track-visited-file (_prev _curr)
5     (and buffer-file-name
6          (recentf-add-file buffer-file-name)))
7   :init
8   (add-hook 'switch-buffer-functions #'my-recentf-track-visited-file))

Use +default/search-buffer for searching by default, I like the Swiper interface.

1 ;;(map! "C-s" #'counsel-grep-or-swiper)
2 (map! "C-s" #'+default/search-buffer)

Map C-c C-g to magit-status - I have too ingrained muscle memory for this keybinding.

1 (map! :after magit "C-c C-g" #'magit-status)

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.

Figure 11. →packages.el
1 (package! pcre2el)
2 (package! visual-regexp-steroids)
1 (use-package! visual-regexp-steroids
2   :defer 3
3   :config
4   (require 'pcre2el)
5   (setq vr/engine 'pcre2el)
6   (map! "C-c s r" #'vr/replace)
7   (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.

1 (after! undo-fu
2   (map! :map undo-fu-mode-map "C-?" #'undo-fu-only-redo))

Replace the default goto-line keybindings with avy-goto-line, which is more flexible and also falls back to goto-line if a number is typed.

1 (map! "M-g g" #'avy-goto-line)
2 (map! "M-g M-g" #'avy-goto-line)

Map a keybindings for counsel-outline, which allows easily navigating documents (it works best with Org documents, but it also tries to extract navigation information from other file types).

1 (map! "M-g o" #'counsel-outline)

Emulating vi’s % key

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 NavigatingParentheses, but modified to use smartparens instead of the default commands, and to work on brackets and braces.

 1 (after! smartparens
 2   (defun zz/goto-match-paren (arg)
 3     "Go to the matching paren/bracket, otherwise (or if ARG is not
 4     nil) insert %.  vi style of % jumping to matching brace."
 5     (interactive "p")
 6     (if (not (memq last-command '(set-mark
 7                                   cua-set-mark
 8                                   zz/goto-match-paren
 9                                   down-list
10                                   up-list
11                                   end-of-defun
12                                   beginning-of-defun
13                                   backward-sexp
14                                   forward-sexp
15                                   backward-up-list
16                                   forward-paragraph
17                                   backward-paragraph
18                                   end-of-buffer
19                                   beginning-of-buffer
20                                   backward-word
21                                   forward-word
22                                   mwheel-scroll
23                                   backward-word
24                                   forward-word
25                                   mouse-start-secondary
26                                   mouse-yank-secondary
27                                   mouse-secondary-save-then-kill
28                                   move-end-of-line
29                                   move-beginning-of-line
30                                   backward-char
31                                   forward-char
32                                   scroll-up
33                                   scroll-down
34                                   scroll-left
35                                   scroll-right
36                                   mouse-set-point
37                                   next-buffer
38                                   previous-buffer
39                                   previous-line
40                                   next-line
41                                   back-to-indentation
42                                   doom/backward-to-bol-or-indent
43                                   doom/forward-to-last-non-comment-or-eol
44                                   )))
45         (self-insert-command (or arg 1))
46       (cond ((looking-at "\\s\(") (sp-forward-sexp) (backward-char 1))
47             ((looking-at "\\s\)") (forward-char 1) (sp-backward-sexp))
48             (t (self-insert-command (or arg 1))))))
49   (map! "%" 'zz/goto-match-paren))

Org mode

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 homeworks and projects), but Org is the first tool I have encountered which makes it practical. Here are some of the resources I have found useful in learning it:

Doom’s Org module provides a lot of sane configuration settings, so I don’t have to configure so much as in my previous hand-crafted config.

General Org Configuration

Unpin Org to get around a current bug.

Figure 12. →packages.el
1 ;;(unpin! org-mode)

Default directory for Org files.

1 (setq org-directory "~/org/")

Hide Org markup indicators.

1 (after! org (setq org-hide-emphasis-markers t))

Insert Org headings at point, not after the current subtree (this is enabled by default by Doom).

1 (after! org (setq org-insert-heading-respect-content nil))

Enable logging of done tasks, and log stuff into the LOGBOOK drawer by default

1 (after! org
2   (setq org-log-done t)
3   (setq org-log-into-drawer t))

Use the special C-a, C-e and C-k definitions for Org, which enable some special behavior in headings.

1 (after! org
2   (setq org-special-ctrl-a/e t)
3   (setq org-special-ctrl-k t))

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.

1 (after! org
2   (setq org-use-speed-commands
3         (lambda ()
4           (and (looking-at org-outline-regexp)
5                (looking-back "^\**")))))

Disable electric-mode, which is now respected by Org and which creates some confusing indentation sometimes.

1 (add-hook! org-mode (electric-indent-local-mode -1))

I really dislike completion of words as I type prose (in code it’s OK), so I disable it in Org and Markdown modes.

1 (defun zz/adjust-org-company-backends ()
2   (remove-hook 'after-change-major-mode-hook '+company-init-backends-h)
3   (setq-local company-backends nil))
4 (add-hook! org-mode (zz/adjust-org-company-backends))
5 (add-hook! markdown-mode (zz/adjust-org-company-backends))

Org visual settings

Enable variable and visual line mode in Org mode by default.

1 (add-hook! org-mode :append
2            #'visual-line-mode
3            #'variable-pitch-mode)

Use org-appear to reveal emphasis markers when moving the cursor over them.

Figure 13. →packages.el
1 (package! org-appear
2   :recipe (:host github
3            :repo "awth13/org-appear"))
1 (add-hook! org-mode :append #'org-appear-mode)

Capturing and note taking

First, I define where all my Org-captured things can be found.

1 (after! org
2   (setq org-agenda-files
3         '("~/gtd" "~/Work/work.org.gpg" "~/org/")))

I define some global keybindings to open my frequently-used org files (original tip from Learn how to take notes more efficiently in Org Mode).

First, I define a helper function to define keybindings that open files. Note that this requires lexical binding to be enabled, so that the lambda creates a closure, otherwise the keybindings don’t work.

1 (defun zz/add-file-keybinding (key file &optional desc)
2   (let ((key key)
3         (file file)
4         (desc desc))
5     (map! :desc (or desc file)
6           key
7           (lambda () (interactive) (find-file file)))))

Now I define keybindings to access my commonly-used org files.

1 (zz/add-file-keybinding "C-c z w" "~/Work/work.org.gpg" "work.org")
2 (zz/add-file-keybinding "C-c z i" "~/org/ideas.org" "ideas.org")
3 (zz/add-file-keybinding "C-c z p" "~/org/projects.org" "projects.org")
4 (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.

1 (setq org-roam-directory "~/Dropbox/Personal/org-roam/")
2 (setq +org-roam-open-buffer-on-find-file t)

Configure attachments to be stored together with their Org document.

1 (setq org-attach-id-dir "attachments/")

Capturing images

Using org-download to make it easier to insert images into my org notes and blog posts. 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.

Figure 14. →packages.el
1 (package! org-download)
1 (after! org-download
2   (setq org-download-method 'directory)
3   (setq org-download-image-dir "images")
4   (setq org-download-heading-lvl nil)
5   (setq org-download-timestamp "%Y%m%d-%H%M%S_")
6   (setq org-image-actual-width 300))
7 (require 'org-download)

org-download implements all the basic machinery for downloading/copying and inserting an image in an org-mode file. However the user experience can be improved. I implemented two wrappers to fit my main use cases:

  • Pasting images from the clipboard;
  • Inserting images already in my laptop;
  • Images must be inserted as links to themselves.

The first function zz/org-paste-clipboard inserts an image from the clipboard using org-download-clipboard, but asking for the filename to use for storing it.

 1 (defun zz/org-paste-clipboard (&optional use-default-filename)
 2   (interactive "P")
 3   (require 'org-download)
 4   (let ((file
 5          (if (not use-default-filename)
 6              (read-string (format "Filename [%s]: "
 7                                   org-download-screenshot-basename)
 8                           nil nil org-download-screenshot-basename)
 9            nil)))
10     (org-download-clipboard file)))

The second function zz/org-attach-file allows me to choose a file to attach. The file is copied with the same name to the org-download-image-dir directory. If the chosen image is already from that directory, it only inserts the link without copying the file again.

 1 (defun zz/org-attach-file (&optional file)
 2   (interactive (list (read-file-name "File to insert: "
 3                                 (or (progn
 4                                       (require 'dired-aux)
 5                                       (dired-dwim-target-directory))
 6                                     default-directory))))
 7   (require 'org-download)
 8   (if (file-in-directory-p file org-download-image-dir)
 9       (org-download-insert-link (concat "file:" (file-relative-name file (org-attach-dir))) file)
10       (let ((file-url (concat "file://" file)))
11         (org-download-image file-url))))

I create keybindings for the two functions above.

1 (map! :map org-mode-map
2         "C-c l a y" #'zz/org-paste-clipboard
3         "C-M-y" #'zz/org-paste-clipboard
4         "C-c l a z" #'zz/org-attach-file
5         "C-M-z" #'zz/org-attach-file)

Finally, I like the images to be links to the image itself, so that in blog posts, for example, a thumbnail is shown, and clicking on it takes you to the full image. To this effect I define a variant of org-download-link-format-function which does this, and assign it to org-download-link-format-function.

 1 (defun zz/org-download-link-format-function-link-to-file (filename)
 2   "Insert the file as a link to itself."
 3   (if (and (>= (string-to-number org-version) 9.3)
 4            (eq org-download-method 'attach))
 5       ;; Respect the default behavior if org-download-method is 'attach
 6       (format "[[attachment:%s]]\n"
 7               (org-link-escape
 8                (file-relative-name filename (org-attach-dir))))
 9     (let ((formatted-filename (org-link-escape
10                                (funcall org-download-abbreviate-filename-function filename))))
11       ;; Here we use the correct link format so that the image is a link to itself
12       (format "[[file:%s][file:%s]]\n"
13              formatted-filename formatted-filename))))
14 
15 (setq! org-download-link-format-function #'zz/org-download-link-format-function-link-to-file)

I normally use counsel-org-link for linking between headings in an Org document. It shows me a searchable list of all the headings in the current document, and allows selecting one, automatically creating a link to it. Since it doesn’t have a keybinding by default, I give it one.

1 (map! :after counsel :map org-mode-map
2       "C-c l l h" #'counsel-org-link)

I also configure counsel-outline-display-style so that only the headline title is inserted into the link, instead of its full path within the document.

1 (after! counsel
2   (setq counsel-outline-display-style 'title))

counsel-org-link uses org-id as its backend which generates IDs using UUIDs, and it uses the ID property to store them. I prefer using human-readable IDs stored in the CUSTOM_ID property of each heading, so we need to make some changes.

First, configure org-id to use CUSTOM_ID if it exists. This affects the links generated by the org-store-link function.

1 (after! org-id
2   ;; Do not create ID if a CUSTOM_ID exists
3   (setq org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id))

Second, I override counsel-org-link-action, which is the function that actually generates and inserts the link, with a custom function that computes and inserts human-readable CUSTOM_ID links. This is supported by a few auxiliary functions for generating and storing the CUSTOM_ID.

 1 (defun zz/make-id-for-title (title)
 2   "Return an ID based on TITLE."
 3   (let* ((new-id (replace-regexp-in-string "[^[:alnum:]]" "-" (downcase title))))
 4     new-id))
 5 
 6 (defun zz/org-custom-id-create ()
 7   "Create and store CUSTOM_ID for current heading."
 8   (let* ((title (or (nth 4 (org-heading-components)) ""))
 9          (new-id (zz/make-id-for-title title)))
10     (org-entry-put nil "CUSTOM_ID" new-id)
11     (org-id-add-location new-id (buffer-file-name (buffer-base-buffer)))
12     new-id))
13 
14 (defun zz/org-custom-id-get-create (&optional where force)
15   "Get or create CUSTOM_ID for heading at WHERE.
16 
17 If FORCE is t, always recreate the property."
18   (org-with-point-at where
19     (let ((old-id (org-entry-get nil "CUSTOM_ID")))
20       ;; If CUSTOM_ID exists and FORCE is false, return it
21       (if (and (not force) old-id (stringp old-id))
22           old-id
23         ;; otherwise, create it
24         (zz/org-custom-id-create)))))
25 
26 ;; Now override counsel-org-link-action
27 (after! counsel
28   (defun counsel-org-link-action (x)
29     "Insert a link to X.
30 
31 X is expected to be a cons of the form (title . point), as passed
32 by `counsel-org-link'.
33 
34 If X does not have a CUSTOM_ID, create it based on the headline
35 title."
36     (let* ((id (zz/org-custom-id-get-create (cdr x))))
37       (org-insert-link nil (concat "#" id) (car x)))))

Ta-da! Now using counsel-org-link inserts nice, human-readable links.

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.

Figure 15. →packages.el
1 (when IS-MAC
2   (package! org-mac-link))
1 (when IS-MAC
2   (use-package! org-mac-link
3     :after org
4     :config
5     (setq org-mac-grab-Acrobat-app-p nil) ; Disable grabbing from Adobe Acrobat
6     (setq org-mac-grab-devonthink-app-p nil) ; Disable grabbinb from DevonThink
7     (map! :map org-mode-map
8           "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.

1 (after! org-agenda
2   ;; (setq org-agenda-prefix-format
3   ;;       '((agenda . " %i %-12:c%?-12t% s")
4   ;;         ;; Indent todo items by level to show nesting
5   ;;         (todo . " %i %-12:c%l")
6   ;;         (tags . " %i %-12:c")
7   ;;        (search . " %i %-12:c")))
8   (setq org-agenda-include-diary t))

Install and load some custom local holiday lists I’m interested in.

Figure 16. →packages.el
1 (package! mexican-holidays)
2 (package! swiss-holidays)
 1 (use-package! holidays
 2   :after org-agenda
 3   :config
 4   (require 'mexican-holidays)
 5   (require 'swiss-holidays)
 6   (setq swiss-holidays-zh-city-holidays
 7         '((holiday-float 4 1 3 "Sechseläuten")
 8           (holiday-float 9 1 3 "Knabenschiessen")))
 9   (setq calendar-holidays
10         (append '((holiday-fixed 1 1 "New Year's Day")
11                   (holiday-fixed 2 14 "Valentine's Day")
12                   (holiday-fixed 4 1 "April Fools' Day")
13                   (holiday-fixed 10 31 "Halloween")
14                   (holiday-easter-etc)
15                   (holiday-fixed 12 25 "Christmas")
16                   (solar-equinoxes-solstices))
17                 swiss-holidays
18                 swiss-holidays-labour-day
19                 swiss-holidays-catholic
20                 swiss-holidays-zh-city-holidays
21                 holiday-mexican-holidays)))

org-super-agenda provides great grouping and customization features to make agenda mode easier to use.

Figure 17. →packages.el
1 (package! org-super-agenda)
1 (use-package! org-super-agenda
2   :after org-agenda
3   :config
4   (setq org-super-agenda-groups '((:auto-dir-name t)))
5   (org-super-agenda-mode))

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.

1 (use-package! org-archive
2   :after org
3   :config
4   (setq org-archive-location "archive.org::datetree/"))

I have started using org-clock to track time I spend on tasks. Often I restart Emacs for different reasons in the middle of a session, so I want to persist all the running clocks and their history.

1 (after! org-clock
2   (setq org-clock-persist t)
3   (org-clock-persistence-insinuate))

GTD

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.

Figure 18. →packages.el
1 (package! org-gtd)
 1 ;; Supress org-gtd update warning
 2 (setq org-gtd-update-ack "2.1.0")
 3 (setq org-gtd-update-ack "4.0.0")
 4 (use-package! org-gtd
 5   :after org
 6   :config
 7   ;; where org-gtd will put its files. This value is also the default one.
 8   (setq org-gtd-directory "~/gtd/")
 9   ;; package: https://github.com/Malabarba/org-agenda-property
10   ;; this is so you can see who an item was delegated to in the agenda
11   (setq org-agenda-property-list '("DELEGATED_TO"))
12   ;; I think this makes the agenda easier to read
13   (setq org-agenda-property-position 'next-line)
14   ;; package: https://www.nongnu.org/org-edna-el/
15   ;; org-edna is used to make sure that when a project task gets DONE,
16   ;; the next TODO is automatically changed to NEXT.
17   (setq org-edna-use-inheritance t)
18   (org-edna-load)
19   :bind
20   (("C-c d c" . org-gtd-capture) ;; add item to inbox
21    ("C-c d a" . org-agenda-list) ;; see what's on your plate today
22    ("C-c d p" . org-gtd-process-inbox) ;; process entire inbox
23    ("C-c d n" . org-gtd-show-all-next) ;; see all NEXT items
24    ;; see projects that don't have a NEXT item
25    ("C-c d s" . org-gtd-show-stuck-projects)
26    ;; the keybinding to hit when you're done editing an item in the
27    ;; processing phase
28    ("C-c d f" . org-gtd-clarify-finalize)))

Capture templates

We define the corresponding Org-GTD capture templates.

 1 (after! (org-gtd org-capture)
 2   (add-to-list 'org-capture-templates
 3                '("i" "GTD item"
 4                  entry
 5                  (file (lambda () (org-gtd--path org-gtd-inbox-file-basename)))
 6                  "* %?\n%U\n\n  %i"
 7                  :kill-buffer t))
 8   (add-to-list 'org-capture-templates
 9                '("l" "GTD item with link to where you are in emacs now"
10                  entry
11                  (file (lambda () (org-gtd--path org-gtd-inbox-file-basename)))
12                  "* %?\n%U\n\n  %i\n  %a"
13                  :kill-buffer t))
14   (add-to-list 'org-capture-templates
15                '("m" "GTD item with link to current Outlook mail message"
16                  entry
17                  (file (lambda () (org-gtd--path org-gtd-inbox-file-basename)))
18                  "* %?\n%U\n\n  %i\n  %(org-mac-outlook-message-get-links)"
19                  :kill-buffer t)))

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.

1 (defadvice! +zz/load-org-gtd-before-capture (&optional goto keys)
2     :before #'org-capture
3     (require 'org-capture)
4     (require 'org-gtd))

Exporting a Curriculum Vitae

I use ox-awesomecv from Org-CV, to export my Curriculum Vitæ.

Org-CV is not yet in MELPA, so I install from its repository.

1 (package! org-cv
2   :recipe (:host gitlab
3            :repo "Titan-C/org-cv"))

For when I do development on it (I wrote the ox-awesomecv exporter), I check it out from my local repo - this is normally disabled.

Figure 19. →packages.el
1 (package! org-cv
2   :recipe (:local-repo "~/Dropbox/Personal/devel/emacs/org-cv"))
 1 (use-package! ox-awesomecv
 2   :after org
 3   :config
 4   (defun org-awesomecv--cventry-right-img-code (file)
 5   (if file
 6     (format "\\begin{wrapfigure}{r}{0.15\\textwidth}
 7   \\raggedleft\\vspace{-10.0mm}
 8   \\includegraphics[width=0.1\\textwidth]{%s}
 9 \\end{wrapfigure}" file) "")))
10 (use-package! ox-moderncv
11   :after org)

Publishing to LeanPub

I use LeanPub for self-publishing my books. Fortunately, it is possible to export from org-mode to both LeanPub-flavored Markdown and Markua, 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:

Building upon these, I developed a new ox-leanpub package which you can find in MELPA (source at https://github.com/zzamboni/ox-leanpub), and which I load and configure below.

The ox-leanpub module sets up Markua export automatically. I add the code for setting up the Markdown exporter too (I don’t use it, but just to keep an eye on any breakage):

Figure 20. →packages.el
1 (package! ox-leanpub
2   :recipe (:local-repo "~/Dropbox/Personal/devel/emacs/ox-leanpub"))
1 (use-package! ox-leanpub
2   :after org
3   :config
4   (require 'ox-leanpub-markdown)
5   (org-leanpub-book-setup-menu-markdown))

I highly recommend using Markua rather than Markdown, as it is the format 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.

If you are interested in learning more about publishing to Leanpub with Org-mode, check out my book Publishing with Emacs, Org-mode and Leanpub.

Blogging with Hugo

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.

1 (after! ox-hugo
2   (setq org-hugo-use-code-for-kbd t))

Define an org-capture template to create a new blog post skeleton automatically. Based on https://ox-hugo.scripter.co/doc/org-capture-setup/, with some customizations for my blog’s setup (like a default value for :featured_image). I also add :jump-to-captured t in the org-capture-templates definition so that when I create a new blog post snippet, I get automatically taken to that location so I can continue writing.

 1 (after! org-capture
 2   (defun org-hugo-new-subtree-post-capture-template ()
 3     "Returns `org-capture' template string for new Hugo post.
 4   See `org-capture-templates' for more information."
 5     (let* ((title (read-from-minibuffer "Post Title: ")) ;Prompt to enter the post title
 6            (fname (org-hugo-slug title)))
 7       (mapconcat #'identity
 8                  `(
 9                    ,(concat "* TODO " title)
10                    ":PROPERTIES:"
11                    ,(concat ":export_hugo_bundle: " (format-time-string "%Y-%m-%d-") fname)
12                    ":export_file_name: index"
13                    ,(concat ":custom_id: " fname)
14                    ":export_hugo_custom_front_matter: :featured_image /images/tram-zurich.jpg :toc false"
15                    ":END:"
16                    "#+BEGIN_DESCRIPTION\n%?\n#+END_DESCRIPTION"       ;Place the cursor here at the end
17                    "\n<write here>")
18                  "\n")))
19 
20 (add-to-list 'org-capture-templates
21                '("h"                ;`org-capture' binding + h
22                  "Hugo post"
23                  entry
24                  ;; It is assumed that below file is present in `org-directory'
25                  ;; and that it has an "Ideas" heading. It can even be a
26                  ;; symlink pointing to the actual location of all-posts.org!
27                  (file+olp "~/Personal/websites/zzamboni.org/content-org/zzamboni.org" "Ideas")
28                  (function org-hugo-new-subtree-post-capture-template)
29                  :jump-to-captured t)))

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.

1 (defun zz/org-if-str (str &optional desc)
2   (when (org-string-nw-p str)
3     (or (org-string-nw-p desc) str)))

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.

1 (defun zz/org-macro-hsapi-code (module &optional func desc)
2   (org-link-make-string
3    (concat "https://www.hammerspoon.org/docs/"
4            (concat module (zz/org-if-str func (concat "#" func))))
5    (or (org-string-nw-p desc)
6        (format "=%s="
7                (concat module
8                        (zz/org-if-str func (concat "." func)))))))

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.

 1 (defun zz/org-macro-keys-code-outer (str)
 2   (mapconcat (lambda (s)
 3                (concat "~" s "~"))
 4              (split-string str)
 5              (concat (string ?\u200B) "+" (string ?\u200B))))
 6 (defun zz/org-macro-keys-code-inner (str)
 7   (concat "~" (mapconcat (lambda (s)
 8                            (concat s))
 9                          (split-string str)
10                          (concat (string ?\u200B) "-" (string ?\u200B)))
11           "~"))
12 (defun zz/org-macro-keys-code (str)
13   (zz/org-macro-keys-code-inner str))

Links to a specific section/function of the Lua manual.

1 (defun zz/org-macro-luadoc-code (func &optional section desc)
2   (org-link-make-string
3    (concat "https://www.lua.org/manual/5.3/manual.html#"
4            (zz/org-if-str func section))
5    (zz/org-if-str func desc)))
1 (defun zz/org-macro-luafun-code (func &optional desc)
2   (org-link-make-string
3    (concat "https://www.lua.org/manual/5.3/manual.html#"
4            (concat "pdf-" func))
5    (zz/org-if-str (concat "=" func "()=") desc)))

Reformatting an Org buffer

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.

1 (defun zz/org-reformat-buffer ()
2   (interactive)
3   (when (y-or-n-p "Really format current buffer? ")
4     (let ((document (org-element-interpret-data (org-element-parse-buffer))))
5       (erase-buffer)
6       (insert document)
7       (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.

Figure 21. →packages.el
1 (package! org-pandoc-import
2   :recipe (:host github
3            :repo "tecosaur/org-pandoc-import"
4            :files ("*.el" "filters" "preprocessors")))
1 (use-package org-pandoc-import)

Reveal.js presentations

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.

 1 (defun zz/org-current-headline-number ()
 2   "Get the numbering of the innermost headline which contains the
 3 cursor. Returns nil if the cursor is above the first level-1
 4 headline, or at the very end of the file. Does not count
 5 headlines tagged with :noexport:"
 6   (require 'org-num)
 7   (let ((org-num--numbering nil)
 8         (original-point (point)))
 9     (save-mark-and-excursion
10       (let ((new nil))
11         (org-map-entries
12          (lambda ()
13            (when (org-at-heading-p)
14              (let* ((level (nth 1 (org-heading-components)))
15                     (numbering (org-num--current-numbering level nil)))
16                (let* ((current-subtree (save-excursion (org-element-at-point)))
17                       (point-in-subtree
18                        (<= (org-element-property :begin current-subtree)
19                            original-point
20                            (1- (org-element-property :end current-subtree)))))
21                  ;; Get numbering to current headline if the cursor is in it.
22                  (when point-in-subtree (push numbering
23                                               new))))))
24          "-noexport")
25         ;; New contains all the trees that contain the cursor (i.e. the
26         ;; innermost and all its parents), so we only return the innermost one.
27         ;; We reverse its order to make it more readable.
28         (reverse (car new))))))

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):

1 * Local variables :ARCHIVE:noexport:
2 # Local variables:
3 # eval: (add-hook! after-save :append :local (zz/refresh-reveal-prez))
4 # end:

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.

 1 (defun zz/refresh-reveal-prez ()
 2   ;; Export the file
 3   (org-re-reveal-export-to-html)
 4   (let* ((slide-list (zz/org-current-headline-number))
 5          (slide-str (string-join (mapcar #'number-to-string slide-list) "-"))
 6          ;; Determine the filename to use
 7          (file (concat (file-name-directory (buffer-file-name))
 8                        (org-export-output-file-name ".html" nil)))
 9          ;; Final URL including the slide number
10          (uri (concat "file://" file "#/slide-" slide-str))
11          ;; Get the document title
12          (title (cadar (org-collect-keywords '("TITLE"))))
13          ;; Command to reload the browser and move to the correct slide
14          (cmd (concat
15 "osascript -e \"tell application \\\"Brave\\\" to repeat with W in windows
16 set i to 0
17 repeat with T in (tabs in W)
18 set i to i + 1
19 if title of T is \\\"" title "\\\" then
20   reload T
21   delay 0.1
22   set URL of T to \\\"" uri "\\\"
23   set (active tab index of W) to i
24 end if
25 end repeat
26 end repeat\"")))
27     ;; Short sleep seems necessary for the file changes to be noticed
28     (sleep-for 0.2)
29     (call-process-shell-command cmd)))

Other exporters

ox-jira to export in Jira markup format.

Figure 22. →packages.el
1 (package! ox-jira)
1 (use-package! ox-jira
2   :after org)

org-jira for full Jira integration - manage issues from Org mode.

Figure 23. →packages.el
1 (package! org-jira)
1 (make-directory "~/.org-jira" 'ignore-if-exists)
2 (setq jiralib-url "https://jira.example.com/")

org-special-block-extras to enable additional special block types and their corresponding exports (disabled for now).

1 (package! org-special-block-extras)
1 (use-package! org-special-block-extras
2   :after org
3   :hook (org-mode . org-special-block-extras-mode))

Other Org stuff

Programming Org

Trying out org-ml for easier access to Org objects.

Figure 24. →packages.el
1 (package! org-ml)
1 (use-package! org-ml
2   :after org)

I’m also testing org-ql for structured queries on Org documents.

Figure 25. →packages.el
1 (package! org-ql)
1 (use-package! org-ql
2   :after org)

This function returns a list of all the headings in the given file which have the given tags.

 1 (defun zz/headings-with-tags (file tags)
 2   (string-join
 3    (org-ql-select file
 4      `(tags-local ,@tags)
 5      :action '(let ((title (org-get-heading 'no-tags 'no-todo)))
 6                 (concat "- "
 7                         (org-link-make-string
 8                          (format "file:%s::*%s" file title)
 9                          title))))
10    "\n"))

This function returns a list of all the headings in the given file which match the tags of the current heading.

1 (defun zz/headings-with-current-tags (file)
2   (let ((tags (s-split ":" (cl-sixth (org-heading-components)) t)))
3     (zz/headings-with-tags file tags)))

Coding

Tangle-on-save has revolutionized my literate programming workflow. It automatically runs org-babel-tangle upon saving any org-mode buffer, which means the resulting files will be automatically kept up to date. For a while I did this by manually adding org-babel-tangle to the after-save hook in Org mode, but now I use the org-auto-tangle package, which does this asynchronously and selectively for each Org file where it is desired.

Figure 26. →packages.el
1 (package! org-auto-tangle)
1 (use-package! org-auto-tangle
2   :defer t
3   :hook (org-mode . org-auto-tangle-mode)
4   :config
5   (setq org-auto-tangle-default t))

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.

 1 (defun zz/sp-enclose-next-sexp (num)
 2   (interactive "p")
 3   (insert-parentheses (or num 1)))
 4 
 5 (after! smartparens
 6   (add-hook! (clojure-mode
 7               emacs-lisp-mode
 8               lisp-mode
 9               cider-repl-mode
10               racket-mode
11               racket-repl-mode) :append #'smartparens-strict-mode)
12   (add-hook! smartparens-mode :append #'sp-use-paredit-bindings)
13   (map! :map (smartparens-mode-map smartparens-strict-mode-map)
14         "M-(" #'zz/sp-enclose-next-sexp))

Adding keybindings for some useful functions:

  • find-function-at-point gets bound to C-c l g p (grouped together with other “go to” functions bound by Doom) and to C-c C-f (analog to the existing C-c f) for faster access.

    1 (after! prog-mode
    2   (map! :map prog-mode-map "C-h C-f" #'find-function-at-point)
    3   (map! :map prog-mode-map
    4         :localleader
    5         :desc "Find function at point"
    6         "g p" #'find-function-at-point))
    

Some other languages I use.

  • Elvish shell, with support for org-babel.

    Figure 27. →packages.el
    1 (package! elvish-mode)
    2 (package! ob-elvish)
    
  • Fish shell.

    Figure 28. →packages.el
    1 (package! fish-mode)
    
  • CFEngine policy files. The cfengine3-mode package is included with Emacs, but I also install org-babel support.

    Figure 29. →packages.el
    1 (package! ob-cfengine3)
    
    1 (use-package! cfengine
    2   :defer t
    3   :commands cfengine3-mode
    4   :mode ("\\.cf\\'" . cfengine3-mode))
    
  • Graphviz for graph generation.

    Figure 30. →packages.el
    1 ;(package! graphviz-dot-mode)
    
    1 ;(use-package! graphviz-dot-mode)
    
  • I am learning Common LISP, which is well supported through the common-lisp Doom module, but I need to configure this in the ~/.slynkrc file for I/O in the Sly REPL to work fine (source).

    Figure 31. →~/.slynkrc
    1 (setf slynk:*use-dedicated-output-stream* nil)
    
  • package-lint for checking MELPA packages.

    Figure 32. →packages.el
    1 (package! package-lint)
    
  • Playing with Zig:

    Figure 33. →packages.el
    1 (package! zig-mode)
    

Other tools

Miscellaneous packages

  • Figure 34. →packages.el
    1 (package! typst-ts-mode :recipe (:host codeberg :repo "meow_king/typst-ts-mode"))
    
  • Figure 35. →packages.el
    1 (package! dockerfile-mode)
    
    1 (add-to-list 'auto-mode-alist '("Dockerfile\\'" . dockerfile-mode))
    2 (put 'dockerfile-image-name 'safe-local-variable #'stringp)
    

This prevents the docker command from producing ANSI sequences during the image build process, which results in a more readable output in the compilation buffer. From https://emacs.stackexchange.com/a/55340/11843:

1 (defun plain-pipe-for-process () (setq-local process-connection-type nil))
2 (add-hook 'compilation-mode-hook 'plain-pipe-for-process)
  • Use Emacs Everywhere!

    Figure 36. →packages.el
    1 (package! emacs-everywhere :pin nil)
    
    1 (use-package! emacs-everywhere
    2   :config
    3   (setq emacs-everywhere-major-mode-function #'org-mode))
    
  • 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.

     1 (after! magit
     2   (setq zz/repolist
     3         "~/.elvish/package-data/elvish-themes/chain-summary-repos.json")
     4   (defadvice! +zz/load-magit-repositories ()
     5     :before #'magit-list-repositories
     6     (setq magit-repository-directories
     7           (seq-map (lambda (e) (cons e 0)) (json-read-file zz/repolist))))
     8   (setq magit-repolist-columns
     9         '(("Name" 25 magit-repolist-column-ident nil)
    10           ("Status" 7 magit-repolist-column-flag nil)
    11           ("B<U" 3 magit-repolist-column-unpulled-from-upstream
    12            ((:right-align t)
    13             (:help-echo "Upstream changes not in branch")))
    14           ("B>U" 3 magit-repolist-column-unpushed-to-upstream
    15            ((:right-align t)
    16             (:help-echo "Local changes not in upstream")))
    17           ("Path" 99 magit-repolist-column-path nil))))
    
  • 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.

    1 (after! epa
    2   (set 'epg-pinentry-mode nil)
    3   (setq epa-file-encrypt-to '("diego@zzamboni.org")))
    
  • 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.

    Figure 37. →packages.el
    1 (package! iedit)
    
    1 (use-package! iedit
    2   :defer
    3   :config
    4   (set-face-background 'iedit-occurrence "Magenta")
    5   :bind
    6   ("C-;" . iedit-mode))
    
  • A useful macro (sometimes) for timing the execution of things. From StackOverflow.

    1 (defmacro zz/measure-time (&rest body)
    2   "Measure the time it takes to evaluate BODY."
    3   `(let ((time (current-time)))
    4      ,@body
    5      (float-time (time-since time))))
    
  • 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!

    1 (setq vterm-shell "/usr/local/bin/elvish")
    
  • Add “unfill” commands to parallel the “fill” ones, bind A-q to unfill-paragraph and rebind M-q to the unfill-toggle command, which fills/unfills paragraphs alternatively.

    Figure 38. →packages.el
    1 (package! unfill)
    
    1 (use-package! unfill
    2   :defer t
    3   :bind
    4   ("M-q" . unfill-toggle)
    5   ("A-q" . unfill-paragraph))
    
  • The annotate package is nice - allows adding annotations to files without modifying the file itself.

    Figure 39. →packages.el
    1 (package! annotate)
    
  • gift-mode for editing quizzes in GIFT format.

    Figure 40. →packages.el
    1 (package! gift-mode)
    
  • just-mode for editing Just files.

    Figure 41. →packages.el
    1 (package! just-mode)
    
  • chezmoi.el for interacting with chezmoi.

    Figure 42. →packages.el
    1 (package! chezmoi)
    
    1 (use-package! chezmoi
    2   :config
    3   (load-file "~/.emacs.d/.local/straight/repos/chezmoi.el/extensions/chezmoi-magit.el"))
    

Posting to 750words.com

I use 750words.com for recording some writing every day (1464-day streak as of this writing!). I wrote 750words-client to allow posting my words from the command line, and the code below integrates this into Emacs, so I can post text directly from the current buffer.

Figure 43. →packages.el
1 (package! 750words
2   :recipe (:host github
3            :repo "zzamboni/750words-client"
4            :files ("*.el")))
5 ;;(package! 750words
6 ;;  :recipe (:local-repo ;;"~/Dropbox/Personal/devel/750words-client"))
1 (use-package! 750words :defer t)
2 (use-package! ox-750words :defer t)

Experiments

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.

 1 (with-current-buffer (get-buffer-create "*Non-native functions*")
 2   (mapatoms
 3    (lambda (s)
 4      (when (and (functionp s)
 5                 (not (helpful--native-compiled-p s))
 6                 (not (helpful--primitive-p s t)))
 7        (insert (symbol-name s))
 8        (insert " --- ")
 9        (insert (or (cdr (find-function-library s)) "<no file>"))
10        (insert "\n"))
11      ))
12   )

Make ox-md export src blocks with backticks and the language name.

1 (defun org-md-example-block (example-block _contents info)
2   "Transcode EXAMPLE-BLOCK element into Markdown format.
3 CONTENTS is nil.  INFO is a plist used as a communication
4 channel."
5   (let ((lang (or (org-element-property :language example-block) "")))
6     (format "```%s\n%s```\n"
7             lang
8             (org-remove-indentation
9              (org-export-format-code-default example-block info)))))