Literate Configuration
Literate Configuration
Diego Zamboni
Buy on Leanpub

Literate configuration

Welcome! In this booklet I will introduce you to Literate Configuration, which is the application of Literate Programming to configuration files. If you are already familiar with Literate Programming you might find most of the basic concepts familiar. Literate Programming is a beautiful concept, but it is ambitious and hard to apply in general software development. However, I have found that it can be especially applicable to configuration files, for several reasons:

  • Configuration files are inherently focused, since they correspond to a single application, program or set of programs, all related. This makes it easier to draw a narrative for them;
  • Most configuration files are self-contained but their structure and syntax may not be immediately evident, so they benefit from a human-readable explanation of their contents;
  • Configuration files are often shared and read by others, as we all like to learn by reading the config files of other people. Applying Literate Programming to config files makes them much easier to share, since their explanation is naturally woven into the code;
  • Org mode has emerged in the last few years as a powerful and simple markup language for general writing, but with unique features that make it easy to include code within the text, and even further, to easily extract that code into stand-alone source files which can be interpreted by their corresponding programs.

Whether you already use Emacs and Org or not, I hope you will find value in this book by seeing how uniquely Literate Programming can help you better write, maintain, understand and share your config files.

1 Literate Programming

Literate programming was first proposed by Donald Knuth in 1984 (Literate Programming, The Computer Journal, 27 (2): 97–111) as a technique of producing computer code from prose. The general idea is that you write code as you would write a story or an article, describing what you want to do and how to solve it. The code gets interweaved with the code as you explain it. Ultimately, you can produce one of two outputs from the same file:

  • A human-readable version of the code, nicely typeset with all the explanations and code, through a process called weaving;
  • A computer-executable version of the code, which extracts only the code and puts it in the correct order for the computer to run, through a process called tangling.

The idea of literate programming has been around since then, and a number of tools have been created, starting from Knuth’s own CWEB, Ramsey’s noweb, and others. However, the lack of a standard set of tools has made its adoption slow and limited to specialized instances.

During my first encounter with literate programming I used noweb, but beyond some school projects and homeworks, I could never get Literate Programming to “stick”, until I encountered Org mode.

Org mode

Org mode is a markup format and corresponding toolset developed originally within the Emacs text editor. Org provides a wide range of functionality, including text formatting, calendar, task tracking, and much more. For our purposes, we are interested in the combination of two of its functions:

  • Text formatting and exporting. This is one of Org mode’s core functionalities, which provides the weaving aspect of Literate Programming by allowing us to produce nicely rendered versions of our document in a number of output formats, including both the text narration and the code. Out of the box, Org supports exporting to LaTeX, Markdown, HTML and other common formats, but many more are available through third-party libraries. The output document may even include syntax highlighting appropriate for the language in which it is written.
  • Code block evaluation and exporting. This is made possible by org-babel, one of Org’s built-in modules, which allows flexible manipulation, execution and exporting of code embedded within an Org document. Babel supports tangling code blocks into one or more separate files which include only the code portions of the document.

Using Org mode with Babel for literate programming is, of course, not a new idea. However, most cases of Literate Configuration that I have seen focus on using Literate Programming for configuring Emacs itself. This is a good use case, but limited in my opinion. In this booklet we will explore how you can use Literate Programming for all your configuration files.

2 The Tools and Process

Emacs and Org mode

The basic tools in our setup will be Emacs and Org mode. I assume you have a basic knowledge of Emacs, but if you don’t: don’t worry! It takes little to get started. If you want a gentle introduction, check out the Guided Tour of Emacs. If you have already installed Emacs, you can start an interactive tutorial within Emacs itself by pressing C-h followed by t (if you are using a graphical version of Emacs, this is likely also available from within the Help menu). If you currently use vi or vim, you can check out Doom Emacs or Spacemacs, both are mature and stable Emacs distributions which use vi-style keybindings (through the Emacs evil module) by default, and which allow you to start using Emacs without having to relearn any new keybindings.

Org mode is included with all recent versions of Emacs, so chances are you already have it installed. Emacs 26.3 (the one I use as of this writing) includes Org 9.1.9, which is quite recent and more than enough for the uses I describe here. If you want to upgrade to the latest version, see the Org mode installation instructions.

How it works

In general, the process of generating config files from Org files can be illustrated as follows:

Org mode is extremely flexible, so you can decide how to structure and organize both your source (Org) and destination (config) files in the way that suits you best. Here are some options:

  • Org file stored in the same directory as the resulting config file. Usually one org file corresponds to one config file, but multiple related config files (e.g. for the same program or application) could be combined in the same Org file, from which all of them are generated. The advantage of this method is that the source Org files and the configuration files they produce are stored in the same location, which makes it easier to relate them for your own use, and to share them if you put them, for example, in a Github repository. The disadvantage is that the config directory gets “polluted” by the source files, and some applications might even produce errors or complain about having extraneous files in their configuration directories. However, this is uncommon and can be dealt with if needed. This is the setup I use and recommend, and which is described in this booklet.
  • All the org files stored in a single directory (or directory tree, or even in a single file), separate from the final destinations of the config files they contain. This has the advantage of giving you a central location for all your org files, regardless of the config files they produce. The drawback is that it makes it harder to selectively share the configuration files for a single program.
  • Of course, you can combine the two approaches if it makes sense for you. For example, you might choose to have all your shell’s config files produced from a single Org file, while having separate Org files for other, individual config files.

Ultimately it is up to you to decide which scheme to use. The basic techniques are the same, and the point of literate config is to make things easier for you to understand and maintain.

Configuring Emacs and Org for literate programming

To get started with literate configuration, the only really indispensable package is org, which can be loaded without prior installation since it is included with Emacs:

(require 'org)

As you write your config file, you can use the org-babel-tangle command (bound by default to C-c C-v t) to extract the code from the current file into the corresponding code files. Conversely, you can use org-export-dispatch (C-c C-e) to export your file to any of Org’s supported output formats.

There are other optional configurations which you can use to make the experience more efficient and pleasant. You can find these below.

Automatic tangle on save

After some time, it can get tedious to press C-c C-v t to tangle your files, and you run the risk of forgetting to do it after a change, resulting in a discrepancy between your Org file and your config files. To avoid this, you can configure a hook that automatically runs org-babel-tangle upon saving any Org buffer, which means the resulting files will be automatically kept up to date.

(add-hook 'org-mode-hook
          (lambda () (add-hook 'after-save-hook #'org-babel-tangle
                          :append :local)))

Language-specific org-babel support

You can load and enable org-babel language-specific packages. Many are included in Org mode, while others can be found online. Most org-babel support packages are named ob-<language>. For example, these are some of the ones I use:

  • CFEngine, used extensively for my book Learning CFEngine.
    (use-package ob-cfengine3
      :after org)
    
  • Elvish, my favorite shell.
    (use-package ob-elvish
      :after org)
    

After you load any necessary packages, you should configure the languages for which to load org-babel support. Note that this does not affect the ability to export/tangle code, but allows you to execute snippets of code from within the Org buffer by pressing C-c C-c on them, and have the results automatically inserted into the buffer.

(org-babel-do-load-languages
 'org-babel-load-languages
 '((cfengine3 . t)
   (ruby      . t)
   (latex     . t)
   (plantuml  . t)
   (python    . t)
   (shell     . t)
   (elvish    . t)
   (calc      . t)
   (dot       . t)
   (ditaa     . t)))

Beautifying Org mode

Org mode allows extensive configuration of its fonts and colors, and doing so can significantly improve the experience of editing literate code with Emacs. In the end, you can have an Emacs setup for editing org documents which looks very nice, with proportional fonts for text and monospaced fonts for code blocks, examples and other elements. For example, here is what a fragment of my Emacs config file looks like:

This is the look of Org in Doom Emacs with the Spacemacs Light theme and my favorite fonts, ET Book for text and Fira Code for code:

→packages.el
(package! spacemacs-theme)
→config.el
(setq doom-font (font-spec :family "Fira Code Retina" :size 18)
      doom-variable-pitch-font (font-spec :family "ETBembo" :size 18))
(setq doom-theme 'spacemacs-light)

Of course, if you use plain Emacs, you can also achieve similar results. Instead of describing the configuration here, I’ll just point you to the Beautifying Org mode section of my Old Emacs config - you can find all the details there.

Structure of a literate config file

A literate config file is simply an org file which has source code blocks in it with the :tangle option to specify the file to which they should be extracted. If more than one source block specifies the same file, they will be appended in the order they appear. For example, a very simple bash config file can be produced as follows:

Minimal bash literate config file
* Bash config

Set the PATH environment variable to include my ~$HOME/bin~ directory.

#+begin_src bash :tangle ~/.profile
  PATH=($HOME/bin $PATH)
#+end_src

If your whole org file gets tangled to a single config file, you can specify the :tangle header globally to avoid having to specify it for every code block. To do this, you specify a header-args global property corresponding to the language of the source blocks you want to export. For example, I have the following line at the top my init.org Hammerspoon config source file:

#+property: header-args:lua :tangle init.lua

If the basename of your org file is the same as the exported source file (for example, init.org produces init.lua), you can even use a bit of Emacs Lisp magic to avoid having to specify the output filename by hand, just the extension (shown here in two lines, make sure you type everything in a single line):

#+property: header-args:lua :tangle
   (concat (file-name-sans-extension (buffer-file-name)) ".lua")

Even if you specify the tangle filename globally, you can still change its value for individual code blocks. For example, you can prevent a code block from being tangled by specifying :tangle no, or specify a different filename to write it to a file different from the global one.

3 Tips and tricks

We now look at some useful tips that will make your life easier when writing literate config files.

Converting your existing config files

You probably already have a number of long and complex config files. The beauty of literate config is that you don’t have to do a lot of work to start using it. As a first step, you can simply include your whole file into a single source block. For example, if you already have a /.profile file, you can create /.profile.org with the following:

* My Bash config

#+begin_src bash :tangle ~/.profile
<contents of your existing .profile file>
#+end_src

You can then start breaking it up in logical blocks, adding commentary and other structure as you see fit. For this, you may find useful the org-babel-demarcate-block command, bound by default to C-c C-v C-d. When invoked within a source block, this command splits the block at that point, creating two source blocks with the same language and any other header arguments as the original one.

Using noweb references to structure your code

Exporting source blocks in the sequence in which they appear in the Org file is useful, but ultimately little more than extensively documenting your file with comments. The real power of literate programming comes from being able to express the logic of your code in a way that makes sense to a human reading it. This does not necessarily match the order or structure in which the code needs to be read by the computer.

This is possible in Org by using noweb references, so called because they follow the same convention introduced by the early noweb literate programming tool. In this convention, a code block can have an arbitrary name, and be referenced (included) in other blocks by specifying its name inside double angle brackets: <<code block name>>.

Processing of noweb references is enabled by the :noweb header argument in source blocks. This argument can have multiple values, but I have found the following to be the most useful:

  • :noweb yes enables Noweb processing in the block, both for tangling and for exporting (i.e. when you export the code block to HTML, LaTeX or some other format, the noweb references will be expanded as well);
  • :noweb no-export enables Noweb processing in the block when tangling, but not when exporting. This is useful for leaving the noweb indicators in the human-readable exports of your file, which can make it easier to understand.

The name of an individual code block can be specified with the :noweb-ref header argument. If multiple blocks have the same :noweb-ref value, they will be concatenated when referenced. You can also specify the name of a block using a #+name: property line before the source block, but in this case there can be only one block with the given name.

Consider the following org code:

Literate config with noweb references
Prepend some paths to the previous value.

#+begin_src bash :tangle ~/.profile :noweb no-export
export PATH=<<Personal paths>>:<<Homebrew paths>>:$PATH
#+end_src

These are the paths in which I store my personal scripts and binaries:

#+begin_src bash :noweb-ref Personal paths
~/bin
#+end_src

These are the directories in which Homebrew binaries are installed:

#+begin_src bash :noweb-ref Homebrew paths
/usr/local/bin:/usr/local/sbin:/usr/local/opt/coreutils/libexec/gnubin
#+end_src

Here we can see that the first block references the two other by their names, and they get combined to produce the final tangled output. This is of course a very simple example, but in complex config files, this technique can make it much easier to see the overall structure of the code before delving in the details. For a more realistic example, see the Org mode section of my old Emacs config. Here you can see a top-level block as follows:

(use-package org
  :pin manual
  :load-path ("lisp/org-mode/lisp" "lisp/org-mode/lisp/contrib/lisp")
  :bind
    <<org-mode-keybindings>>
  :custom
    <<org-mode-custom-vars>>
  :custom-face
    <<org-mode-faces>>
  :hook
    <<org-mode-hooks>>
  :config
    <<org-mode-config>>)

The different noweb references get “filled in” throughout the rest of the configuration by assigning them to the corresponding :noweb-ref values. This makes it possible to specify keybindings, variables, faces, hooks and custom config code in the sections where it makes logical sense, rather than where they need to be included in the code.

Multiple config files per org file

Most Literate Config Org files will probably generate a single config file. However, as illustrated in How it works, you can easily produce multiple config files from a single org file. For this, you can use one of the following techniques:

  • Manually specify the file to which each source block should be tangled, using the :tangle header argument to specify it;
  • Specify a main tangle target globally, as described in Structure of a literate config file, and specify the :tangle argument for the blocks that should be written to a different file;
  • If the different files are produced from within different sections of the Org document, you can specify the :tangle argument per section, by specifying it within a :PROPERTIES drawer of the corresponding headline. For example, in the following source, each section gets tangled to a different output file:
    * Bash .profile
      :PROPERTIES:
      :header-args:bash: :tangle ~/.profile
      :END:
    
    #+begin_src bash
      this code goes to ~/.profile
    #+end_src
    
    * Bash .bashrc
      :PROPERTIES:
      :header-args:bash: :tangle ~/.bashrc
      :END:
    
    #+begin_src bash
      this code goes to ~/.bashrc
    #+end_src
    

Literate config examples

In this part of the book you will find three real-world examples of literate configuration files created, maintained and used by me. These are the real thing—what you see here is included directly from the Org files that contain the configuration I use for Emacs (with Doom Emacs), Hammerspoon and Elvish, respectively. You can find the latest version of these files, both in Org and in their corresponding tangled output, in Github at the locations specified in each of the sections.

Note also that each section links to a post in my blog containing the corresponding config file. This illustrates another advantage of literate configuration: that the same Org file can be used to produce the output in multiple formats. Each one of the literate config files you see below are rendered in the following places:

  • This book, produced by exporting the Org files in Leanpub’s Markua format using my own ox-leanpub package;
  • The corresponding blog posts in my blog, produced by exporting in Hugo Markdown format using the excellent ox-hugo package;
  • Directly in GitHub and GitLab, thanks to their support for rendering Org files.

4 Emacs

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.

→init.el
;;; 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).
→packages.el
;; -*- 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)
→config.el
;;; $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 "john@doe.com")

;; 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.

Customized variables

Doom does not recommend the Emacs customize mechanism:

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.

→init.el
(doom!
 :input
 ;;chinese
 ;;japanese
 ;;layout              ; auie,ctsrnm is the superior home row

 :completion
 (company +childframe) ; the ultimate code completion backend
 ;;helm                ; the *other* search engine for love and life
 ;;ido                 ; the other *other* search engine...
 (ivy +prescient
      -fuzzy +icons)   ; a search engine for love and life

 :ui
 ;;deft                ; notational velocity for Emacs
 doom                  ; what makes DOOM look the way it does
 doom-dashboard        ; a nifty splash screen for Emacs
 ;;doom-quit           ; DOOM quit-message prompts when you quit Emacs
 ;;fill-column         ; a `fill-column' indicator
 hl-todo               ; highlight TODO/FIXME/NOTE/DEPRECATED/HACK/REVIEW
 ;;hydra
 ;;indent-guides       ; highlighted indent columns
 (ligatures +extra)    ; ligatures or substitute text with pretty symbols
 ;;minimap             ; show a map of the code on the side
 modeline              ; snazzy, Atom-inspired modeline, plus API
 nav-flash             ; blink cursor line after big motions
 ;;neotree             ; a project drawer, like NERDTree for vim
 ophints               ; highlight the region an operation acts on
 ;;(popup +defaults)   ; tame sudden yet inevitable temporary windows
 ;;tabs                ; a tab bar for Emacs
 ;;treemacs            ; a project drawer, like neotree but cooler
 ;;unicode             ; extended unicode support for various languages
 ;;vc-gutter           ; vcs diff in the fringe
 vi-tilde-fringe       ; fringe tildes to mark beyond EOB
 window-select         ; visually switch windows
 workspaces            ; tab emulation, persistence & separate workspaces
 zen                   ; distraction-free coding or writing

 :editor
 ;;(evil +everywhere)  ; come to the dark side, we have cookies
 file-templates        ; auto-snippets for empty files
 fold                  ; (nigh) universal code folding
 ;;(format +onsave)    ; automated prettiness
 ;;god                 ; run Emacs commands without modifier keys
 ;;lispy               ; vim for lisp, for people who don't like vim
 ;;multiple-cursors    ; editing in many places at once
 ;;objed               ; text object editing for the innocent
 ;;parinfer            ; turn lisp into python, sort of
 ;;rotate-text         ; cycle region at point between text candidates
 snippets              ; my elves. They type so I don't have to
 ;;word-wrap           ; soft wrapping with language-aware indent

 :emacs
 dired                 ; making dired pretty [functional]
 electric              ; smarter, keyword-based electric-indent
 ;;ibuffer             ; interactive buffer management
 undo                  ; persistent, smarter undo for your inevitable mistakes
 vc                    ; version-control and Emacs, sitting in a tree

 :term
 ;;eshell              ; the elisp shell that works everywhere
 ;;shell               ; simple shell REPL for Emacs
 ;;term                ; basic terminal emulator for Emacs
 vterm                 ; the best terminal emulation in Emacs

 :checkers
 (syntax +childframe)  ; tasing you for every semicolon you forget
 spell                 ; tasing you for misspelling mispelling
 ;;grammar             ; tasing grammar mistake every you make

 :tools
 ;;ansible
 debugger              ; FIXME stepping through code, to help you add bugs
 ;;direnv
 ;;docker
 ;;editorconfig        ; let someone else argue about tabs vs spaces
 ;;ein                 ; tame Jupyter notebooks with emacs
 (eval +overlay)       ; run code, run (also, repls)
 gist                  ; interacting with github gists
 lookup                ; navigate your code and its documentation
 lsp
 (magit +forge)        ; a git porcelain for Emacs
 ;;make                ; run make tasks from Emacs
 pass                  ; password manager for nerds
 pdf                   ; pdf enhancements
 ;;prodigy             ; FIXME managing external services & code builders
 ;;rgb                 ; creating color strings
 ;;taskrunner          ; taskrunner for all your projects
 ;;terraform           ; infrastructure as code
 ;;tmux                ; an API for interacting with tmux
 ;;upload              ; map local to remote projects via ssh/ftp

 :os
 (:if IS-MAC macos)    ; improve compatibility with macOS
 ;;tty                 ; improve the terminal Emacs experience

 :lang
 ;;agda                ; types of types of types of types...
 ;;cc                  ; C/C++/Obj-C madness
 ;;clojure             ; java with a lisp
 ;;common-lisp         ; if you've seen one lisp, you've seen them all
 ;;coq                 ; proofs-as-programs
 ;;crystal             ; ruby at the speed of c
 ;;csharp              ; unity, .NET, and mono shenanigans
 ;;data                ; config/data formats
 ;;(dart +flutter)     ; paint ui and not much else
 ;;elixir              ; erlang done right
 ;;elm                 ; care for a cup of TEA?
 emacs-lisp            ; drown in parentheses
 ;;erlang              ; an elegant language for a more civilized age
 (ess +lsp)            ; emacs speaks statistics
 ;;faust               ; dsp, but you get to keep your soul
 ;;fsharp              ; ML stands for Microsoft's Language
 ;;fstar               ; (dependent) types and (monadic) effects and Z3
 ;;gdscript            ; the language you waited for
 (go +lsp)             ; the hipster dialect
 ;;(haskell +dante)    ; a language that's lazier than I am
 ;;hy                  ; readability of scheme w/ speed of python
 ;;idris               ; a language you can depend on
 json                  ; At least it ain't XML
 ;;(java +meghanada)   ; the poster child for carpal tunnel syndrome
 ;;javascript          ; all(hope(abandon(ye(who(enter(here))))))
 ;;julia               ; a better, faster MATLAB
 ;;kotlin              ; a better, slicker Java(Script)
 latex                 ; writing papers in Emacs has never been so fun
 ;;lean
 ;;factor
 ;;ledger              ; an accounting system in Emacs
 lua                   ; one-based indices? one-based indices
 markdown              ; writing docs for people to ignore
 ;;nim                 ; python + lisp at the speed of c
 ;;nix                 ; I hereby declare "nix geht mehr!"
 ;;ocaml               ; an objective camel
 (org +pretty +journal
      +hugo +roam +pandoc
      +present)        ; organize your plain life in plain text
 ;;php                 ; perl's insecure younger brother
 plantuml              ; diagrams for confusing people more
 ;;purescript          ; javascript, but functional
 python                ; beautiful is better than ugly
 ;;qt                  ; the 'cutest' gui framework ever
 racket                ; a DSL for DSLs
 ;;raku                ; the artist formerly known as perl6
 ;;rest                ; Emacs as a REST client
 rst                   ; ReST in peace
 ;;(ruby +rails)       ; 1.step {|i| p "Ruby is #{i.even? ? 'love' : 'life'}"}
 ;;rust                ; Fe2O3.unwrap().unwrap().unwrap().unwrap()
 ;;scala               ; java, but good
 ;;scheme              ; a fully conniving family of lisps
 (sh +lsp)             ; she sells {ba,z,fi}sh shells on the C xor
 ;;sml
 ;;solidity            ; do you need a blockchain? No.
 ;;swift               ; who asked for emoji variables?
 ;;terra               ; Earth and Moon in alignment for performance.
 ;;web                 ; the tubes
 (yaml +lsp)           ; JSON, but readable

 :email
 ;;(mu4e +gmail)
 ;;notmuch
 ;;(wanderlust +gmail)

 :app
 ;;calendar
 irc                   ; how neckbeards socialize
 ;;(rss +org)          ; emacs as an RSS reader
 ;;twitter             ; twitter client https://twitter.com/vnought

 :config
 ;;literate
 (default +bindings +smartparens))

General configuration

My user information.

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

Change the Mac modifiers to my liking

(cond (IS-MAC
       (setq mac-command-modifier      'meta
             mac-option-modifier       'alt
             mac-right-option-modifier 'alt)))

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

(setq kill-whole-line t)

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'.
(setq display-line-numbers-type nil)

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

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

Disable exit confirmation.

(setq confirm-kill-emacs nil)

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.

(setq fancy-splash-image
      (concat doom-private-dir "splash/"
              (nth (random 2) '("doom-emacs-color.png"
                                "doom-emacs-bw-light.svg"))))

Set base and variable-pitch fonts. I currently like Fira Code and ET Book.

(setq doom-font (font-spec :family "Fira Code Retina" :size 18)
      doom-variable-pitch-font (font-spec :family "ETBembo" :size 18))

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.

→packages.el
(package! mixed-pitch)
(use-package! mixed-pitch
  :defer
  :config
  (setq mixed-pitch-variable-pitch-cursor nil)
  :hook
  (text-mode . mixed-pitch-mode))

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:

→packages.el
(package! spacemacs-theme)

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

(setq doom-theme 'spacemacs-light)
;;(setq doom-theme 'doom-nord-light)
;;(setq doom-theme 'doom-solarized-light)

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.

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

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.

;;(add-to-list 'initial-frame-alist '(fullscreen . maximized))
(setq initial-frame-alist '((top . 1) (left . 1) (width . 129) (height . 37)))

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.

(map! "C-x b"   #'counsel-buffer-or-recentf
      "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.

(defun zz/counsel-buffer-or-recentf-candidates ()
  "Return candidates for `counsel-buffer-or-recentf'."
  (require 'recentf)
  (recentf-mode)
  (let ((buffers
         (delq nil
               (mapcar (lambda (b)
                         (when (buffer-file-name b)
                           (abbreviate-file-name (buffer-file-name b))))
                       (delq (current-buffer) (buffer-list))))))
    (append
     buffers
     (cl-remove-if (lambda (f) (member f buffers))
                   (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-s" #'counsel-grep-or-swiper)
(map! "C-s" #'+default/search-buffer)

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

(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.

→packages.el
(package! pcre2el)
(package! visual-regexp-steroids)
(use-package! visual-regexp-steroids
  :defer 3
  :config
  (require 'pcre2el)
  (setq vr/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.

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

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.

(after! smartparens
  (defun zz/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 (memq last-command '(set-mark
                                  cua-set-mark
                                  zz/goto-match-paren
                                  down-list
                                  up-list
                                  end-of-defun
                                  beginning-of-defun
                                  backward-sexp
                                  forward-sexp
                                  backward-up-list
                                  forward-paragraph
                                  backward-paragraph
                                  end-of-buffer
                                  beginning-of-buffer
                                  backward-word
                                  forward-word
                                  mwheel-scroll
                                  backward-word
                                  forward-word
                                  mouse-start-secondary
                                  mouse-yank-secondary
                                  mouse-secondary-save-then-kill
                                  move-end-of-line
                                  move-beginning-of-line
                                  backward-char
                                  forward-char
                                  scroll-up
                                  scroll-down
                                  scroll-left
                                  scroll-right
                                  mouse-set-point
                                  next-buffer
                                  previous-buffer
                                  previous-line
                                  next-line
                                  back-to-indentation
                                  doom/backward-to-bol-or-indent
                                  doom/forward-to-last-non-comment-or-eol
                                  )))
        (self-insert-command (or arg 1))
      (cond ((looking-at "\\s\(") (sp-forward-sexp) (backward-char 1))
            ((looking-at "\\s\)") (forward-char 1) (sp-backward-sexp))
            (t (self-insert-command (or arg 1))))))
  (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

Default directory for Org files.

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

Hide Org markup indicators.

(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).

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

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

(after! org
  (setq org-log-done t)
  (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.

(after! org
  (setq org-special-ctrl-a/e t)
  (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.

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

Org visual settings

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

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

Capturing and note taking

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

(after! org
  (setq org-agenda-files
        '("~/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.

(defun zz/add-file-keybinding (key file &optional desc)
  (let ((key key)
        (file file)
        (desc desc))
    (map! :desc (or desc file)
          key
          (lambda () (interactive) (find-file file)))))

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.

(setq org-roam-directory org-directory)
(setq +org-roam-open-buffer-on-find-file nil)

Capturing images

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.

→packages.el
(package! org-download)
(defun zz/org-download-paste-clipboard (&optional use-default-filename)
  (interactive "P")
  (require 'org-download)
  (let ((file
         (if (not use-default-filename)
             (read-string (format "Filename [%s]: "
                                  org-download-screenshot-basename)
                          nil nil org-download-screenshot-basename)
           nil)))
    (org-download-clipboard file)))

(after! org
  (setq org-download-method 'directory)
  (setq org-download-image-dir "images")
  (setq org-download-heading-lvl nil)
  (setq org-download-timestamp "%Y%m%d-%H%M%S_")
  (setq org-image-actual-width 300)
  (map! :map org-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
  :after org
  :config
  (setq org-mac-grab-Acrobat-app-p nil) ; Disable grabbing from Adobe Acrobat
  (setq org-mac-grab-devonthink-app-p nil) ; Disable grabbinb from DevonThink
  (map! :map org-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
  (setq org-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")))
  (setq org-agenda-include-diary t))

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

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

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

→packages.el
(package! org-super-agenda)
(use-package! org-super-agenda
  :after org-agenda
  :config
  (setq org-super-agenda-groups '((:auto-dir-name t)))
  (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.

(use-package! org-archive
  :after org
  :config
  (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.

(after! org-clock
  (setq org-clock-persist t)
  (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.

→packages.el
(package! org-gtd)
(use-package! org-gtd
  :after org
  :config
  ;; where org-gtd will put its files. This value is also the default one.
  (setq org-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
  (setq org-agenda-property-list '("DELEGATED_TO"))
  ;; I think this makes the agenda easier to read
  (setq org-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.
  (setq org-edna-use-inheritance t)
  (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
   ;; see projects that don't have a NEXT item
   ("C-c d s" . org-gtd-show-stuck-projects)
   ;; the keybinding to hit when you're done editing an item in the
   ;; processing phase
   ("C-c d f" . org-gtd-clarify-finalize)))

We define the corresponding Org-GTD capture templates.

(after! (org-gtd org-capture)
  (add-to-list 'org-capture-templates
               '("i" "GTD item"
                 entry
                 (file (lambda () (org-gtd--path org-gtd-inbox-file-basename)))
                 "* %?\n%U\n\n  %i"
                 :kill-buffer t))
  (add-to-list 'org-capture-templates
               '("l" "GTD item with link to where you are in emacs now"
                 entry
                 (file (lambda () (org-gtd--path org-gtd-inbox-file-basename)))
                 "* %?\n%U\n\n  %i\n  %a"
                 :kill-buffer t))
  (add-to-list 'org-capture-templates
               '("m" "GTD item with link to current Outlook mail message"
                 entry
                 (file (lambda () (org-gtd--path org-gtd-inbox-file-basename)))
                 "* %?\n%U\n\n  %i\n  %(org-mac-outlook-message-get-links)"
                 :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.

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

Exporting a Curriculum Vitae

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

My ox-awesomecv package is not yet merged into the main Org-CV distribution, so I install from my branch for now.

→packages.el
(package! org-cv
  :recipe (:host gitlab :repo "zzamboni/org-cv" :branch "awesomecv"))
(use-package! ox-awesomecv
  :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):

→packages.el
(package! ox-leanpub
  :recipe (:local-repo "~/Dropbox/Personal/devel/emacs/ox-leanpub"))
(use-package! ox-leanpub
  :after org
  :config
  (require 'ox-leanpub-markdown)
  (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.

(after! ox-hugo
  (setq org-hugo-use-code-for-kbd 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.

(defun zz/org-if-str (str &optional desc)
  (when (org-string-nw-p str)
    (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.

(defun zz/org-macro-hsapi-code (module &optional func desc)
  (org-link-make-string
   (concat "https://www.hammerspoon.org/docs/"
           (concat module (zz/org-if-str func (concat "#" func))))
   (or (org-string-nw-p desc)
       (format "=%s="
               (concat module
                       (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.

(defun zz/org-macro-keys-code-outer (str)
  (mapconcat (lambda (s)
               (concat "~" s "~"))
             (split-string str)
             (concat (string ?\u200B) "+" (string ?\u200B))))
(defun zz/org-macro-keys-code-inner (str)
  (concat "~" (mapconcat (lambda (s)
                           (concat s))
                         (split-string str)
                         (concat (string ?\u200B) "-" (string ?\u200B)))
          "~"))
(defun zz/org-macro-keys-code (str)
  (zz/org-macro-keys-code-inner str))

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

(defun zz/org-macro-luadoc-code (func &optional section desc)
  (org-link-make-string
   (concat "https://www.lua.org/manual/5.3/manual.html#"
           (zz/org-if-str func section))
   (zz/org-if-str func desc)))
(defun zz/org-macro-luafun-code (func &optional desc)
  (org-link-make-string
   (concat "https://www.lua.org/manual/5.3/manual.html#"
           (concat "pdf-" func))
   (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.

(defun zz/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)
      (insert document)
      (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.

→packages.el
(package! org-pandoc-import
  :recipe (:host github
           :repo "tecosaur/org-pandoc-import"
           :files ("*.el" "filters" "preprocessors")))
(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.

(defun zz/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--numbering nil)
        (original-point (point)))
    (save-mark-and-excursion
      (let ((new nil))
        (org-map-entries
         (lambda ()
           (when (org-at-heading-p)
             (let* ((level (nth 1 (org-heading-components)))
                    (numbering (org-num--current-numbering level nil)))
               (let* ((current-subtree (save-excursion (org-element-at-point)))
                      (point-in-subtree
                       (<= (org-element-property :begin current-subtree)
                           original-point
                           (1- (org-element-property :end current-subtree)))))
                 ;; Get numbering to current headline if the cursor is in it.
                 (when point-in-subtree (push numbering
                                              new))))))
         "-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 (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):

* Local variables :ARCHIVE:noexport:
# Local variables:
# eval: (add-hook! after-save :append :local (zz/refresh-reveal-prez))
# 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.

(defun zz/refresh-reveal-prez ()
  ;; Export the file
  (org-re-reveal-export-to-html)
  (let* ((slide-list (zz/org-current-headline-number))
         (slide-str (string-join (mapcar #'number-to-string slide-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
  reload T
  delay 0.1
  set URL of T to \\\"" uri "\\\"
  set (active tab index of W) to i
end if
end repeat
end repeat\"")))
    ;; Short sleep seems necessary for the file changes to be noticed
    (sleep-for 0.2)
    (call-process-shell-command cmd)))

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.

(add-hook! org-mode :append
  (lambda () (add-hook 'after-save-hook
                  #'org-babel-tangle :append :local)))

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.

(defun zz/sp-enclose-next-sexp (num)
  (interactive "p")
  (insert-parentheses (or num 1)))

(after! smartparens
  (add-hook! (clojure-mode
              emacs-lisp-mode
              lisp-mode
              cider-repl-mode
              racket-mode
              racket-repl-mode) :append #'smartparens-strict-mode)
  (add-hook! smartparens-mode :append #'sp-use-paredit-bindings)
  (map! :map (smartparens-mode-map smartparens-strict-mode-map)
        "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.
    (after! prog-mode
      (map! :map prog-mode-map "C-h C-f" #'find-function-at-point)
      (map! :map prog-mode-map
            :localleader
            :desc "Find function at point"
            "g p" #'find-function-at-point))
    

Some other languages I use.

  • Elvish shell, with support for org-babel.
    →packages.el
    (package! elvish-mode)
    (package! ob-elvish)
    
  • CFEngine policy files. The cfengine3-mode package is included with Emacs, but I also install org-babel support.
    →packages.el
    (package! ob-cfengine3)
    
    (use-package! cfengine
      :defer t
      :commands cfengine3-mode
      :mode ("\\.cf\\'" . cfengine3-mode))
    
  • Graphviz for graph generation.
    →packages.el
    (package! graphviz-dot-mode)
    
    (use-package! graphviz-dot-mode)
    

Other tools

  • 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
      (setq zz/repolist
            "~/.elvish/package-data/elvish-themes/chain-summary-repos.json")
      (defadvice! +zz/load-magit-repositories ()
        :before #'magit-list-repositories
        (setq magit-repository-directories
              (seq-map (lambda (e) (cons e 0)) (json-read-file zz/repolist))))
      (setq magit-repolist-columns
            '(("Name" 25 magit-repolist-column-ident nil)
              ("Status" 7 magit-repolist-column-flag nil)
              ("B<U" 3 magit-repolist-column-unpulled-from-upstream
               ((:right-align t)
                (:help-echo "Upstream changes not in branch")))
              ("B>U" 3 magit-repolist-column-unpushed-to-upstream
               ((:right-align t)
                (:help-echo "Local changes not in upstream")))
              ("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.
    (after! epa
      (set (if EMACS27+
               'epg-pinentry-mode
             'epa-pinentry-mode) ; DEPRECATED `epa-pinentry-mode'
           nil))
    
  • 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.
    →packages.el
    (package! iedit)
    
    (use-package! iedit
      :defer
      :config
      (set-face-background 'iedit-occurrence "Magenta")
      :bind
      ("C-;" . iedit-mode))
    
  • A useful macro (sometimes) for timing the execution of things. From StackOverflow.
    (defmacro zz/measure-time (&rest body)
      "Measure the time it takes to evaluate BODY."
      `(let ((time (current-time)))
         ,@body
         (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!
    (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.
    →packages.el
    (package! unfill)
    
    (use-package! unfill
      :defer t
      :bind
      ("M-q" . unfill-toggle)
      ("A-q" . unfill-paragraph))
    

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.

(with-current-buffer (get-buffer-create "*Non-native functions*")
  (mapatoms
   (lambda (s)
     (when (and (functionp s)
                (not (helpful--native-compiled-p s))
                (not (helpful--primitive-p s t)))
       (insert (symbol-name s))
       (insert " --- ")
       (insert (or (cdr (find-function-library s)) "<no file>"))
       (insert "\n"))
     ))
  )

5 Hammerspoon

This is my Hammerspoon configuration file.

This file is written in literate programming style using org-mode. See init.lua for the generated file. You can see this in a nicer format on my blog post My Hammerspoon Configuration, With Commentary.

If you want to learn more about Hammerspoon, check out my book Learning Hammerspoon!

General variables and configuration

Global log level. Per-spoon log level can be configured in each Install:andUse block below.

hs.logger.defaultLogLevel="info"

I use hyper, shift_hyper and ctrl_cmd as the modifiers for most of my key bindings, so I define them as variables here for easier use.

hyper       = {"cmd","alt","ctrl"}
shift_hyper = {"cmd","alt","ctrl","shift"}
ctrl_cmd    = {"cmd","ctrl"}

Set up an abbreviation for hs.drawing.color.x11 since I use it repeatedly later on.

col = hs.drawing.color.x11

Work’s logo, which I use in some of my Seal shortcuts later on.

work_logo = hs.image.imageFromPath(hs.configdir .. "/files/work_logo_2x.png")

Spoon Management

Set up SpoonInstall - this is the only spoon that needs to be manually installed (it is already there if you check out this repository), all the others are installed and configured automatically.

hs.loadSpoon("SpoonInstall")

Configuration of my personal spoon repository, which contains Spoons that have not been merged in the main repo. See the descriptions at https://zzamboni.github.io/zzSpoons/.

spoon.SpoonInstall.repos.zzspoons = {
  url = "https://github.com/zzamboni/zzSpoons",
  desc = "zzamboni's spoon repository",
}

I prefer sync notifications, makes them easier to read.

spoon.SpoonInstall.use_syncinstall = true

This is just a shortcut to make the declarations below look more readable, i.e. Install:andUse instead of spoon.SpoonInstall:andUse.

Install=spoon.SpoonInstall

BetterTouchTool integration (experimental)

I’m currently working on a new BetterTouchTool.spoon which provides integration with the BetterTouchTool AppleScript API. This is in heavy development! See the configuration for the Hammer spoon in System and UI for an example of how to use it.

Install:andUse("BetterTouchTool", { loglevel = 'debug' })
BTT = spoon.BetterTouchTool

URL dispatching to site-specific browsers

The URLDispatcher spoon makes it possible to open URLs with different browsers. I have created different site-specific browsers using Epichrome, which allows me to keep site-specific bookmarks, search settings, etc. I also use the url_redir_decoders parameter to rewrite some URLs before they are opened, both to redirect certain URLs directly to their corresponding applications (instead of going through the web browser) and to fix a bug I have experienced in opening URLs from PDF documents using Preview.

DefaultBrowser = "com.brave.Browser.dev"
JiraApp = "org.epichrome.app.Jira"
WikiApp = "org.epichrome.app.Wiki"
OpsGenieApp = DefaultBrowser

Install:andUse("URLDispatcher",
               {
                 config = {
                   url_patterns = {
                     { "https?://issue.work.com",      JiraApp },
                     { "https?://jira.work.com",       JiraApp },
                     { "https?://wiki.work.com",       WikiApp },
                     { "https?://app.opsgenie.com",    OpsGenieApp },
                     { "https?://app.eu.opsgenie.com", OpsGenieApp },
                     { "msteams:",                     "com.microsoft.teams" }
                   },
                   url_redir_decoders = {
                     -- Send MS Teams URLs directly to the app
                     { "MS Teams URLs",
                       "(https://teams.microsoft.com.*)", "msteams:%1", true },
                     -- Preview incorrectly encodes the anchor
                     -- character in URLs as %23, we fix it
                     { "Fix broken Preview anchor URLs",
                       "%%23", "#", false, "Preview" },
                   },
                   default_handler = DefaultBrowser
                 },
                 start = true
               }
)

Window and screen manipulation

The WindowHalfsAndThirds spoon sets up multiple key bindings for manipulating the size and position of windows.

This was one of the first spoons I wrote, and I used it for window resizing until I discovered MiroWindowsManager (see below), which I started using now.

Install:andUse("WindowHalfsAndThirds",
               {
                 config = {
                   use_frame_correctness = true
                 },
                 hotkeys = 'default'
               }
)

MiroWindowsManager allows more granular window resizing and movement. One thing to keep in mind is that this spoon uses the hs.grid module internally. If you also use the WindowGrid spoon (see below), make sure both spoons use the same grid size to avoid conflicts.

myGrid = { w = 6, h = 4 }
Install:andUse("MiroWindowsManager",
               {
                 disable = true,
                 config = {
                   GRID = myGrid
                 },
                 hotkeys = {
                   up =         { ctrl_cmd, "up" },
                   right =      { ctrl_cmd, "right" },
                   down =       { ctrl_cmd, "down" },
                   left =       { ctrl_cmd, "left" },
                   fullscreen = { hyper,    "up" }
                 }
               }
)

The WindowGrid spoon sets up a key binding (Hyper-g here) to overlay a grid that allows resizing windows by specifying their opposite corners.

Install:andUse("WindowGrid",
               {
                 config = { gridGeometries =
                              { { myGrid.w .."x" .. myGrid.h } } },
                 hotkeys = {show_grid = {hyper, "g"}},
                 start = true
               }
)

The WindowScreenLeftAndRight spoon sets up key bindings for moving windows between multiple screens.

Install:andUse("WindowScreenLeftAndRight",
               {
                 config = {
                   animationDuration = 0
                 },
                 hotkeys = 'default'
               }
)

The ToggleScreenRotation spoon sets up a key binding to rotate the external screen (the spoon can set up keys for multiple screens if needed, but by default it rotates the first external screen).

Install:andUse("ToggleScreenRotation",
               {
                 hotkeys = { first = {hyper, "f15"} }
               }
)

Organization and Productivity

Universal Archiving

The UniversalArchive spoon sets up a single key binding (Ctrl-Cmd-a) to archive the current item in Evernote, Mail and Outlook.

Install:andUse("UniversalArchive",
               {
                 config = {
                   evernote_archive_notebook = ".Archive",
                   archive_notifications = false
                 },
                 hotkeys = { archive = { { "ctrl", "cmd" }, "a" } }
               }
)

Filing to Omnifocus

Note: I no longer use OmniFocus so the Spoon below is diabled, but this section is still here as an example.

The SendToOmniFocus spoon sets up a single key binding (Hyper-t) to send the current item to OmniFocus from multiple applications. We use the fn attribute of Install:andUse to call a function which registers some of the Epichrome site-specific-browsers I use, so that the Spoon knows how to collect items from them.

function chrome_item(n)
  return { apptype = "chromeapp", itemname = n }
end
function OF_register_additional_apps(s)
  s:registerApplication("Collab", chrome_item("tab"))
  s:registerApplication("Wiki", chrome_item("wiki page"))
  s:registerApplication("Jira", chrome_item("issue"))
  s:registerApplication("Brave Browser Dev", chrome_item("page"))
end
Install:andUse("SendToOmniFocus",
               {
                 disable = true,
                 config = {
                   quickentrydialog = false,
                   notifications = false
                 },
                 hotkeys = {
                   send_to_omnifocus = { hyper, "t" }
                 },
                 fn = OF_register_additional_apps,
               }
)

Evernote filing and tagging

The EvernoteOpenAndTag spoon sets up some missing key bindings for note manipulation in Evernote. I no longer use Evernote for GTD, so I have it disabled for now.

Install:andUse("EvernoteOpenAndTag",
               {
                 disable = true,
                 hotkeys = {
                   open_note = { hyper, "o" },
                   ["open_and_tag-+work"] = { hyper, "w" },
                   ["open_and_tag-+personal"] = { hyper, "p" },
                   ["tag-@zzdone"] = { hyper, "z" }
                 }
               }
)

Clipboard history

The TextClipboardHistory spoon implements a clipboard history, only for text items. It is invoked with Cmd-Shift-v.

Note: This is disabled for the moment as I experiment with BetterTouchTool’s built-in clipboard history, which I have bound to the same key combination for consistency in my workflow.

Install:andUse("TextClipboardHistory",
               {
                 disable = true,
                 config = {
                   show_in_menubar = false,
                 },
                 hotkeys = {
                   toggle_clipboard = { { "cmd", "shift" }, "v" } },
                 start = true,
               }
)

System and UI

General Hammerspoon utilities

The BTT_restart_Hammerspoon function sets up a BetterTouchTool widget which also executes the config_reload action from the spoon. This gets assigned to the fn config parameter in the configuration of the Hammer spoon below, which has the effect of calling the function with the Spoon object as its parameter.

This is still very manual - the uuid parameter contains the ID of the BTT widget to configure, and for now you have to get it by hand from BTT and paste it here.

function BTT_restart_hammerspoon(s)
  BTT:bindSpoonActions(s, {
                         config_reload = {
                           kind = 'touchbarButton',
                           uuid = "FF8DA717-737F-4C42-BF91-E8826E586FA1",
                           name = "Restart",
                           icon = hs.image.imageFromName(
                             hs.image.systemImageNames.ApplicationIcon),
                           color = hs.drawing.color.x11.orange,
  }})
end

The Hammer spoon (get it? hehe) is a simple wrapper around some common Hammerspoon configuration variables. Note that this gets loaded from my personal repo, since it’s not in the official repository.

Install:andUse("Hammer",
               {
                 repo = 'zzspoons',
                 config = { auto_reload_config = false },
                 hotkeys = {
                   config_reload = {hyper, "r"},
                   toggle_console = {hyper, "y"}
                 },
                 fn = BTT_restart_Hammerspoon,
                 start = true
               }
)

Caffeine: Control system/display sleep

The Caffeine spoon allows preventing the display and the machine from sleeping. I use it frequently when playing music from my machine, to avoid having to unlock the screen whenever I want to change the music. In this case we also create a function BTT_caffeine_widget to configure the widget to both execute the corresponding function, and to set its icon according to the current state.

function BTT_caffeine_widget(s)
  BTT:bindSpoonActions(s, {
                         toggle = {
                           kind = 'touchbarWidget',
                           uuid = '72A96332-E908-4872-A6B4-8A6ED2E3586F',
                           name = 'Caffeine',
                           widget_code = [[
do
  title = " "
  icon = hs.image.imageFromPath(spoon.Caffeine.spoonPath.."/caffeine-off.pdf")
  if (hs.caffeinate.get('displayIdle')) then
    icon = hs.image.imageFromPath(spoon.Caffeine.spoonPath.."/caffeine-on.pdf")
  end
  print(hs.json.encode({ text = title,
                         icon_data = BTT:hsimageToBTTIconData(icon) }))
end
      ]],
                           code = "spoon.Caffeine.clicked()",
                           widget_interval = 1,
                           color = hs.drawing.color.x11.black,
                           icon_only = true,
                           icon_size = hs.geometry.size(15,15),
                           BTTTriggerConfig = {
                             BTTTouchBarFreeSpaceAfterButton = 0,
                             BTTTouchBarItemPadding = -6,
                           },
                         }
  })
end
Install:andUse("Caffeine", {
                 start = true,
                 hotkeys = {
                   toggle = { hyper, "1" }
                 },
                 fn = BTT_caffeine_widget,
})

Colorize menubar according to keyboard layout

The MenubarFlag spoon colorizes the menubar according to the selected keyboard language or layout (functionality inspired by ShowyEdge). I use English, Spanish and German, so those are the colors I have defined.

Install:andUse("MenubarFlag",
               {
                 config = {
                   colors = {
                     ["U.S."] = { },
                     Spanish = {col.green, col.white, col.red},
                     ["Latin American"] = {col.green, col.white, col.red},
                     German = {col.black, col.red, col.yellow},
                   }
                 },
                 start = true
               }
)

Locating the mouse

The MouseCircle spoon shows a circle around the mouse pointer when triggered. I have it disabled for now because I have the macOS shake-to-grow feature enabled.

Install:andUse("MouseCircle",
               {
                 disable = true,
                 config = {
                   color = hs.drawing.color.x11.rebeccapurple
                 },
                 hotkeys = {
                   show = { hyper, "m" }
                 }
               }
)

Finding colors

One of my original bits of Hammerspoon code, now made into a spoon (although I keep it disabled, since I don’t really use it). The ColorPicker spoon shows a menu of the available color palettes, and when you select one, it draws swatches in all the colors in that palette, covering the whole screen. You can click on any of them to copy its name to the clipboard, or cmd-click to copy its RGB code.

Install:andUse("ColorPicker",
               {
                 disable = true,
                 hotkeys = {
                   show = { hyper, "z" }
                 },
                 config = {
                   show_in_menubar = false,
                 },
                 start = true,
               }
)

Homebrew information popups

I use Homebrew, and when I run brew update, I often wonder about what some of the formulas shown are (names are not always obvious). The BrewInfo spoon allows me to point at a Formula or Cask name and press Hyper-b or Hyper-c (for Casks) to have the output of the info command in a popup window, or the same key with Shift-Hyper to open the URL of the Formula/Cask.

Install:andUse("BrewInfo",
               {
                 config = {
                   brew_info_style = {
                     textFont = "Inconsolata",
                     textSize = 14,
                     radius = 10 }
                 },
                 hotkeys = {
                   -- brew info
                   show_brew_info = {hyper, "b"},
                   open_brew_url = {shift_hyper, "b"},
                   -- brew cask info
                   show_brew_cask_info = {shift_hyper, "c"},
                   open_brew_cask_url = {hyper, "c"},
                 }
               }
)

Displaying keyboard shortcuts

The KSheet spoon traverses the current application’s menus and builds a cheatsheet of the keyboard shortcuts, showing it in a nice popup window.

Install:andUse("KSheet",
               {
                 hotkeys = {
                   toggle = { hyper, "/" }
}})

TimeMachine backup monitoring

The TimeMachineProgress spoon shows an indicator about the progress of the ongoing Time Machine backup. The indicator disappears when there is no backup going on.

Install:andUse("TimeMachineProgress",
               {
                 start = true
               }
)

Disabling Turbo Boost

The TurboBoost spoon shows an indicator of the CPU’s Turbo Boost status, and allows disabling/enabling. This requires Turbo Boost Switcher to be installed.

Install:andUse("TurboBoost",
               {
                 config = {
                   disable_on_start = true
                 },
                 hotkeys = {
                   toggle = { hyper, "0" }
                 },
                 start = true,
                 --                   loglevel = 'debug'
               }
)

Unmounting external disks on sleep

The EjectMenu spoon automatically ejects all external disks before the system goes to sleep. I use this to avoid warnings from macOS when I close my laptop and disconnect it from my hub without explicitly unmounting my backup disk before. I disable the menubar icon, which is shown by default by the Spoon.

Install:andUse("EjectMenu", {
                 config = {
                   eject_on_lid_close = true,
                   show_in_menubar = true,
                   notify = true,
                 },
                 hotkeys = { ejectAll = { hyper, "=" } },
                 start = true,
                 loglevel = 'debug'
})

Other applications

The HeadphoneAutoPause spoon implements auto-pause/resume for iTunes, Spotify and others when the headphones are unplugged. Note that this goes unused since I started using wireless headphones.

Install:andUse("HeadphoneAutoPause",
               {
                 start = true
               }
)

Seal application launcher/controller

The Seal spoon is a powerhouse. It implements a Spotlight-like launcher, but which allows for infinite configurability of what can be done or searched from the launcher window. I use Seal as my default launcher, triggered with Cmd-space, although I still keep Spotlight around under Hyper-space, mainly for its search capabilities.

We start by loading the spoon, and specifying which plugins we want.

Install:andUse("Seal",
               {
                 hotkeys = { show = { {"cmd"}, "space" } },
                 fn = function(s)
                   s:loadPlugins({"apps", "calc", "safari_bookmarks",
                                  "screencapture", "useractions"})
                   s.plugins.safari_bookmarks.always_open_with_safari = false
                   s.plugins.useractions.actions =
                     {
                         <<useraction-definitions>>
                     }
                   s:refreshAllCommands()
                 end,
                 start = true,
               }
)

The useractions Seal plugin allows me to define my own shortcuts. For example, a bookmark to the Hammerspoon documentation page:

«useraction-definitions»≡
["Hammerspoon docs webpage"] = {
  url = "http://hammerspoon.org/docs/",
  icon = hs.image.imageFromName(hs.image.systemImageNames.ApplicationIcon),
},

Or to manually trigger my work/non-work transition scripts (see below):

«useraction-definitions»≡
["Leave corpnet"] = {
  fn = function()
    spoon.WiFiTransitions:processTransition('foo', 'corpnet01')
  end,
  icon = work_logo,
},
["Arrive in corpnet"] = {
  fn = function()
    spoon.WiFiTransitions:processTransition('corpnet01', 'foo')
  end,
  icon = work_logo,
},

Or to translate things using dict.leo.org:

«useraction-definitions»≡
["Translate using Leo"] = {
  url = "http://dict.leo.org/englisch-deutsch/${query}",
  icon = 'favicon',
  keyword = "leo",
}

Network transitions

The WiFiTransitions spoon allows triggering arbitrary actions when the SSID changes. I am interested in the change from my work network (corpnet01) to other networks, mainly because at work I need a proxy for all connections to the Internet. I have two applications which don’t handle these transitions gracefully on their own: Spotify and Adium. So I have written a couple of functions for helping them along.

The reconfigSpotifyProxy function quits Spotify, updates the proxy settings in its config file, and restarts it.

function reconfigSpotifyProxy(proxy)
  local spotify = hs.appfinder.appFromName("Spotify")
  local lastapp = nil
  if spotify then
    lastapp = hs.application.frontmostApplication()
    spotify:kill()
    hs.timer.usleep(40000)
  end
  -- I use CFEngine to reconfigure the Spotify preferences
  cmd = string.format(
    "/usr/local/bin/cf-agent -K -f %s/files/spotify-proxymode.cf%s",
    hs.configdir, (proxy and " -DPROXY" or " -DNOPROXY"))
  output, status, t, rc = hs.execute(cmd)
  if spotify and lastapp then
    hs.timer.doAfter(
      3,
      function()
        if not hs.application.launchOrFocus("Spotify") then
          hs.notify.show("Error launching Spotify", "", "")
        end
        if lastapp then
          hs.timer.doAfter(0.5, hs.fnutils.partial(lastapp.activate, lastapp))
        end
    end)
  end
end

The reconfigAdiumProxy function uses AppleScript to tell Adium about the change without having to restart it - only if Adium is already running.

function reconfigAdiumProxy(proxy)
  app = hs.application.find("Adium")
  if app and app:isRunning() then
    local script = string.format([[
  tell application "Adium"
    repeat with a in accounts
      if (enabled of a) is true then
        set proxy enabled of a to %s
      end if
    end repeat
    go offline
    go online
  end tell
  ]], hs.inspect(proxy))
    hs.osascript.applescript(script)
  end
end

Functions to stop applications that are disallowed in the work network.

function stopApp(name)
  app = hs.application.get(name)
  if app and app:isRunning() then
    app:kill()
  end
end

function forceKillProcess(name)
  hs.execute("pkill " .. name)
end

function startApp(name)
  hs.application.open(name)
end

The configuration for the WiFiTransitions spoon invoked these functions with the appropriate parameters.

Install:andUse("WiFiTransitions",
               {
                 config = {
                   actions = {
                     -- { -- Test action just to see the SSID transitions
                     --    fn = function(_, _, prev_ssid, new_ssid)
                     --       hs.notify.show("SSID change",
                     --          string.format("From '%s' to '%s'",
                     --          prev_ssid, new_ssid), "")
                     --    end
                     -- },
                     { -- Enable proxy config when joining corp network
                       to = "corpnet01",
                       fn = {hs.fnutils.partial(reconfigSpotifyProxy, true),
                             hs.fnutils.partial(reconfigAdiumProxy, true),
                             hs.fnutils.partial(forceKillProcess, "Dropbox"),
                             hs.fnutils.partial(stopApp, "Evernote"),
                       }
                     },
                     { -- Disable proxy config when leaving corp network
                       from = "corpnet01",
                       fn = {hs.fnutils.partial(reconfigSpotifyProxy, false),
                             hs.fnutils.partial(reconfigAdiumProxy, false),
                             hs.fnutils.partial(startApp, "Dropbox"),
                       }
                     },
                   }
                 },
                 start = true,
               }
)

Pop-up translation

I live in Switzerland, and my German is far from perfect, so the PopupTranslateSelection spoon helps me a lot. It allows me to select some text and, with a keystroke, translate it to any of three languages using Google Translate. Super useful! Usually, Google’s auto-detect feature works fine, so the translate_to_<lang> keys are sufficient. I have some translate_<from>_<to> keys set up for certain language pairs for when this doesn’t quite work (I don’t think I’ve ever needed them).

local wm=hs.webview.windowMasks
Install:andUse("PopupTranslateSelection",
               {
                 config = {
                   popup_style = wm.utility|wm.HUD|wm.titled|
                     wm.closable|wm.resizable,
                 },
                 hotkeys = {
                   translate_to_en = { hyper, "e" },
                   translate_to_de = { hyper, "d" },
                   translate_to_es = { hyper, "s" },
                   translate_de_en = { shift_hyper, "e" },
                   translate_en_de = { shift_hyper, "d" },
                 }
               }
)

I am now testing DeepLTranslate, based on PopupTranslateSelection but which uses the DeepL translator (this is disabled because I have the DeepL app installed, which binds its own global hotkeys).

Install:andUse("DeepLTranslate",
               {
                 disable = true,
                 config = {
                   popup_style = wm.utility|wm.HUD|wm.titled|
                     wm.closable|wm.resizable,
                 },
                 hotkeys = {
                   translate = { hyper, "e" },
                 }
               }
)

Leanpub integration

The Leanpub spoon provides monitoring of book build jobs. You can read more about how I use this in my blog post Automating Leanpub book publishing with Hammerspoon and CircleCI.

Install:andUse("Leanpub",
               {
                 config = {
                   watch_books = {
                     -- api_key gets set in init-local.lua like this:
                     -- spoon.Leanpub.api_key = "my-api-key"
                     { slug = "learning-hammerspoon" },
                     { slug = "learning-cfengine" },
                     { slug = "emacs-org-leanpub" },
                     { slug = "be-safe-on-the-internet" },
                     { slug = "lit-config"  },
                     { slug = "zztestbook" },
                     { slug = "cisspexampreparationguide" },
                   },
                   books_sync_to_dropbox = true,
                 },
                 start = true,
})

Showing application keybindings

The KSheet spoon provides for showing the keybindings for the currently active application.

Install:andUse("KSheet", {
                 hotkeys = {
                   toggle = { hyper, "/" }
                 }
})

Loading private configuration

In init-local.lua I keep experimental or private stuff (like API tokens) that I don’t want to publish in my main config. This file is not committed to any publicly-accessible git repositories.

local localfile = hs.configdir .. "/init-local.lua"
if hs.fs.attributes(localfile) then
  dofile(localfile)
end

End-of-config animation

The FadeLogo spoon simply shows an animation of the Hammerspoon logo to signal the end of the config load.

Install:andUse("FadeLogo",
               {
                 config = {
                   default_run = 1.0,
                 },
                 start = true
               }
)

If you don’t want to use FadeLogo, you can have a regular notification.

-- hs.notify.show("Welcome to Hammerspoon", "Have fun!", "")

6 Elvish

This is my main config file for Elvish.

This file is written in literate programming style using org-mode. See rc.elv for the generated file. You can see this in a nicer format on my blog post My Elvish Configuration With Commentary.

Paths

First we set up the executable paths. We set the GOPATH environment variable while we are at it, since we need to use it as part of the path.

# Where all the Go stuff is
E:GOPATH = ~/Dropbox/Personal/devel/go
# I use the gccemacs build for macOS, from
# https://github.com/jimeh/build-emacs-for-macos
emacs-path = ~/Applications/Emacs.app/Contents/MacOS

paths = [
  $emacs-path $emacs-path/bin
  ~/bin
  ~/.emacs.d/bin
  $E:GOPATH/bin
  /usr/local/opt/coreutils/libexec/gnubin
  /usr/local/opt/texinfo/bin
  /usr/local/opt/python/libexec/bin
  ~/Library/Python/3.8/bin
  /usr/local/opt/ruby@2.6/bin
  /usr/local/bin
  /usr/local/sbin
  /usr/sbin
  /sbin
  /usr/bin
  /bin
]

I have a quick sanity check because sometimes certain paths disappear depending on new versions, etc. This prints a warning when opening a new shell, if there are any non-existing directories in $paths.

each [p]{
  if (not (-is-dir $p)) {
    echo (styled "Warning: directory "$p" in $paths no longer exists." red)
  }
} $paths

Package installation

The bundled epm module allows us to install and manage Elvish packages.

use epm

For now I use these packages:

epm:install &silent-if-installed         ^
  github.com/zzamboni/elvish-modules     ^
  github.com/zzamboni/elvish-completions ^
  github.com/zzamboni/elvish-themes      ^
  github.com/xiaq/edit.elv               ^
  github.com/muesli/elvish-libs          ^
  github.com/iwoloschin/elvish-packages

The modules within each package get loaded individually below.

Automatic proxy settings

When I am in the office, I need to use a proxy to access the Internet. For macOS applications, the proxy is set automatically using a company-provided PAC file. For the environment variables http_proxy and https_proxy, commonly used by command-line programs, the proxy module allows me to define a test which determines when the proxy should be used, so that the change is done automatically. We load this early on so that other modules which need to access the network get the correct settings already.

First, we load the module and set the proxy host.

use github.com/zzamboni/elvish-modules/proxy
proxy:host = "http://aproxy.corproot.net:8080"

Next, we set the test function to enable proxy auto-setting. In my case, the /etc/resolv.conf file contains the corproot.net domain (set through DHCP) when I’m in the corporate network, so I can check for that.

proxy:test = {
  and ?(test -f /etc/resolv.conf) ^
  ?(egrep -q '^(search|domain).*(corproot.net|company.com)' /etc/resolv.conf)
}

We run an initial check so that other commands in rc.org get the correctd settings already, even before the first prompt.

proxy:autoset

General modules and settings

Load the bundled re module to have access to regular expression functions.

use re

The bundled readline-binding module associates some Emacs-like keybindings for manipulation of the command line.

use readline-binding

I add a couple of keybindings which are missing from the default readline-binding module:

  • Alt-backspace to delete small-word
    edit:insert:binding[Alt-Backspace] = $edit:kill-small-word-left~
    
  • Alt-d to delete the small-word under the cursor
    edit:insert:binding[Alt-d] = $edit:kill-small-word-right~
    
  • I also bind “instant preview mode” to Alt-m. This is useful to see the results of a command while you are typing it.
    edit:insert:binding[Alt-m] = $edit:-instant:start~
    
  • Limit the height of location and history mode so that they don’t cover the whole screen.
    edit:max-height = 20
    

Aliases and miscellaneous functions

Elvish does not have built-in alias functionality, but this is implemented easily using the alias module, which stores the alias definitions as functions under ~/.elvish/aliases/ and loads them automatically.

use github.com/zzamboni/elvish-modules/alias

For reference, I define here a few of my commonly-used aliases:

alias:new dfc e:dfc -p -/dev/disk1s4,devfs,map,com.apple.TimeMachine
alias:new cat bat
alias:new more bat --paging always
alias:new v vagrant

bat man (using bat as the pager for man pages).

E:MANPAGER="sh -c 'col -bx | bat -l man -p'"

Open man pages as PDF, I gathered this tip from https://twitter.com/MrAhmadAwais/status/1279066968981635075. Neat but not very useful for daily use, particularly with the bat integration above.

fn manpdf [@cmds]{
  each [c]{
    man -t $c | open -f -a /System/Applications/Preview.app
  } $cmds
}

Completions

The smart-matcher module tries prefix match, smart-case prefix match, substring match, smart-case substring match, subsequence match and smart-case subsequence match automatically.

use github.com/xiaq/edit.elv/smart-matcher
smart-matcher:apply

Other possible values for edit:completion:matcher are [p]{ edit:match-prefix &smart-case $p } for smart-case completion (if your pattern is entirely lower case it ignores case, otherwise it’s case sensitive). &smart-case can be replaced with &ignore-case to make it always case-insensitive.

I also configure Tab to trigger completion mode, but also to automatically enter “filter mode”, so I can keep typing the filename I want, without having to use the arrow keys. Disabled as this is the default behavior starting with commit b24e4a7, but you may need it if you are running an older version for any reason and want this behavior.

# edit:insert:binding[Tab] = {
#   edit:completion:smart-start
#   edit:completion:trigger-filter
# }

I load some command-specific completions from the elvish-completions package:

use github.com/zzamboni/elvish-completions/vcsh
use github.com/zzamboni/elvish-completions/cd
use github.com/zzamboni/elvish-completions/ssh
use github.com/zzamboni/elvish-completions/builtins

I configure the git completer to use hub instead of git (if you use plain git, you don’t need to call git:init)

use github.com/zzamboni/elvish-completions/git git-completions
git-completions:git-command = hub
git-completions:init

This is not usually necessary, but I load the comp library specifically since I do a lot of tests and development of completions.

use github.com/zzamboni/elvish-completions/comp

Prompt theme

I use the chain prompt theme, ported from the fish theme at https://github.com/oh-my-fish/theme-chain.

use github.com/zzamboni/elvish-themes/chain
chain:bold-prompt = $false

I set the color of the directory segment, the prompt chains and the prompt arrow in my prompt to a session-identifying color (a different color for each session).

chain:segment-style = [
  &dir=          session
  &chain=        session
  &arrow=        session
  &git-combined= session
  &git-repo=     bright-blue
]

Customize some of the glyphs for the font I use in my terminal. I use the Fira Code font which includes ligatures, so I disable the last chain, and set the arrow segment to a combination of characters which shows up as a nice arrow.

chain:glyph[arrow]  = "|>"
chain:show-last-chain = $false

Elvish has a comprehensive mechanism for displaying prompts with useful information while avoiding getting blocked by prompt functions which take too long to finish. For the most part the defaults work well. One change I like to make is to change the stale prompt transformer function to make the prompt dim when stale (the default is to show the prompt in inverse video):

edit:prompt-stale-transform = [x]{ styled $x "bright-black" }

Another possibility is to make the prompt stay the same when stale - useful to avoid distractions (disabled for now):

#  edit:prompt-stale-transform = $all~

I also like the continuous update of the prompt as I type (by default it only updates on Enter and on $pwd changes, but I like also git status changes to be updated automatically), so I increase its eagerness.

edit:-prompt-eagerness = 10

iTerm2 shell integration support

The iterm2 module provides support for iTerm2’s Shell Integration features. Note that iterm2:init must be called after setting up the prompt, hence this is done after loading the chain module above.

use github.com/zzamboni/elvish-modules/iterm2
iterm2:init
edit:insert:binding[Ctrl-L] = $iterm2:clear-screen~

Long-running-command notifications

The long-running-notifications module allows for producing a notification when a command takes longer than a certain time to finish (by default the period is 10 seconds). The module automatically detects when terminal-notifier is available on macOS and uses it to produce Mac-style notifications, otherwise it prints a notification on the terminal.

use github.com/zzamboni/elvish-modules/long-running-notifications

Directory and command navigation and history

Elvish comes with built-in location and command history modes, and these are the main mechanism for accessing prior directories and commands. The weight-keeping in location mode makes the most-used directories automatically raise to the top of the list over time.

I have decades of muscle memory using !! and !$ to insert the last command and its last argument, respectively. The bang-bang module allows me to keep using them.

use github.com/zzamboni/elvish-modules/bang-bang

The dir module implements a directory history and some related functions. I alias the cd command to dir:cd so that any directory changes are kept in the history. I also alias cdb to dir:cdb function, which allows changing to the base directory of the argument.

use github.com/zzamboni/elvish-modules/dir
alias:new cd &use=[github.com/zzamboni/elvish-modules/dir] dir:cd
alias:new cdb &use=[github.com/zzamboni/elvish-modules/dir] dir:cdb

dir also implements a custom directory history chooser, which I bind to Alt-i (I have found I don’t use this as much as I thought I would - the built-in location mode works nicely).

edit:insert:binding[Alt-i] = $dir:history-chooser~

I bind Alt-b/f to dir:left-small-word-or-prev-dir and dir:right-small-word-or-next-dir respectively, which “do the right thing” depending on the current content of the command prompt: if it’s empty, they move back/forward in the directory history, otherwise they move through the words of the current command. In my Terminal.app setup, Alt-left/right also produce Alt-b/f, so these bindings work for those keys as well.

edit:insert:binding[Alt-b] = $dir:left-small-word-or-prev-dir~
edit:insert:binding[Alt-f] = $dir:right-small-word-or-next-dir~

The following makes the location and history modes be case-insensitive by default:

edit:insert:binding[Ctrl-R] = {
  edit:histlist:start
  edit:histlist:toggle-case-sensitivity
}

I use exa as a replacement for the ls command, so I alias ls to it. Unfortunately, exa does not understand the -t option to sort files by modification time, so I explicitly look for the -lrt option combination (which I use very often, and it always trips me off) and replace them with the correct options for exa. All other options are passed as-is.

fn ls [@_args]{
  e:exa --color-scale --git --group-directories-first (each [o]{
      if (eq $o "-lrt") { put "-lsnew" } else { put $o }
  } $_args)
}

Dynamic terminal title

The terminal-title module handles setting the terminal title dynamically according to the current directory or the current command being executed.

use github.com/zzamboni/elvish-modules/terminal-title

Loading private settings

The private module sets up some private settings such as authentication tokens. This is not on github :) The $private-loaded variable gets set to $ok if the module was loaded correctly.

private-loaded = ?(use private)

O’Reilly Atlas

I sometimes use the O’Reilly Atlas publishing platform. The atlas module contains some useful functions for triggering and accessing document builds.

use github.com/zzamboni/elvish-modules/atlas

OpsGenie

I used OpsGenie at work for a while, so I put together the opsgenie library to make API operations easier. I don’t actively use or maintain this anymore.

use github.com/zzamboni/elvish-modules/opsgenie

LeanPub

I use LeanPub for publishing my books, so I have written a few utility functions. I don’t use this regularly, I have much better integration using Hammerspoon and CircleCI, I wrote about it in my blog: Automating Leanpub book publishing with Hammerspoon and CircleCI.

use github.com/zzamboni/elvish-modules/leanpub

TinyTeX

Tiny module with some utility functions for using TinyTeX.

use github.com/zzamboni/elvish-modules/tinytex

Environment variables

Default options to less.

E:LESS = "-i -R"

Use vim as the editor from the command line (although I am an Emacs fan, I still sometimes use vim for quick editing).

E:EDITOR = "vim"

Locale setting.

E:LC_ALL = "en_US.UTF-8"

PKGCONFIG configuration

E:PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig"

Utility functions

The util module includes various utility functions.

use github.com/zzamboni/elvish-modules/util

I use muesli’s git utilities module.

use github.com/muesli/elvish-libs/git

The update.elv package prints a message if there are new commits in Elvish after the running version.

use github.com/iwoloschin/elvish-packages/update
update:curl-timeout = 3
update:check-commit &verbose

Set up electric delimiters in the command line.

util:electric-delimiters

ASCII spinners and TTY escape code generation.

use github.com/zzamboni/elvish-modules/spinners
use github.com/zzamboni/elvish-modules/tty

Work-specific stuff

I have a private library which contains some work-specific functions.

use work

Exporting aliases

We populate $-exports- with the alias definitions so that they become available in the interactive namespace.

-exports- = (alias:export)