Literate Configuration
Literate Configuration
Buy on Leanpub

1 Introduction

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

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, agenda and todo 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, and which provides the weaving aspect of Literate Programming by allowing us to produce nicely rendered versions of our document in any of Org’s supported output formats (out of the box Org supports LaTeX, PDF, HTML and others, but many more are available through third-party modules), including both the text narration and the code. The output document may even include syntax highlighting appropriate for the language in which it is written.
  • Code block evaluation and exporting. This is enabled 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 at least a basic knowledge of Emacs, but if you don’t: don’t worry! It takes very 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, check out Spacemacs, which will 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 oft his writing) includes org-mode 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 very 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 very uncommon. 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-mode for literate programming

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

(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-mode buffer, which means the resulting files will be automatically kept up to date.

(org-mode . (lambda () (add-hook 'after-save-hook 'org-babel-tangle
                                 'run-at-end 'only-in-org-mode)))

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:

Instead of describing the configuration here, I’ll just point you to the Beautifying org-mode section of my 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 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-mode 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

4 Literate config examples

In this chapter 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, 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 the ox-leanpub package;
  • The corresponding blog posts in my blog, produced by exporting in Hugo Markdown format using the ox-hugo package;
  • Directly in Github, thanks to Github’s support for rendering Org files.

Emacs config

This is my Emacs configuration file.

This file is written in literate programming style using org-mode. See init.el for the generated file. You can see this in a nicer format on my blog post My 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.

Performance optimization

Lately I’ve been playing with optimizing my Emacs load time. I have found a couple of useful resources, including:

Based on these, I have added the code below.

First, a hook that reports how long and how many garbage collections the startup took. We use a hook to run it at the very end, so the message doesn’t get clobbered by other messages during startup.

(add-hook 'emacs-startup-hook
          (lambda ()
            (message "Emacs ready in %s with %d garbage collections."
                     (format "%.2f seconds"
                             (float-time
                              (time-subtract after-init-time before-init-time)))
                     gcs-done)))

Next, we wrap the whole init file in a block that sets file-name-handler-alist to nil to prevent any special-filename parsing of files loaded from the init file (e.g. remote files loaded through tramp, etc.). The let block gets closed in the Epilogue.

(let ((file-name-handler-alist nil))

Optionally enable debug-on-error - I do this only when I’m trying to figure out some problem in my config.

;;(setq debug-on-error t)

We set gc-cons-threshold to its maximum value, to prevent any garbage collection from happening during load time. We also reset this value in the Epilogue.

(setq gc-cons-threshold most-positive-fixnum)

Customized variables

Emacs has its own Customization mechanism for easily customizing many parameters. To make it easier to manage, I keep the customized variables and faces in a separate file and load it from the main file. A lot of my custom settings are configured from this init file as well, but there are always some which I change by hand for added flexibility.

(setq custom-file "~/.emacs.d/custom.el")
(load custom-file)

My current custom.el file can be found at https://github.com/zzamboni/dot-emacs/blob/master/custom.el.

Password management

Password management using auth-sources and pass (I normally use 1Password, but I have not found a good command-line/Emacs interface for it, so I am using pass for now for some items I need to add to my Emacs config file).

(require 'auth-source)
(require 'auth-source-pass)
(auth-source-pass-enable)

Package management

I use the wonderful use-package to manage most of the packages in my installation (one exception is org-mode, see below). As this is not bundled yet with Emacs, the first thing we do is install it by hand. All other packages are then declaratively installed and configured with use-package. This makes it possible to fully bootstrap Emacs using only this config file, everything else is downloaded, installed and configured automatically.

First, we declare the package repositories to use.

(customize-set-variable 'package-archives
                        '(("marmalade" . "https://marmalade-repo.org/packages/")
                          ("melpa"     . "https://melpa.org/packages/")))

Then we initialize the package system, refresh the list of packages and install use-package if needed.

(package-initialize)

(when (not package-archive-contents)
  (package-refresh-contents))

(when (not (package-installed-p 'use-package))
  (package-install 'use-package))

Finally, we load use-package.

(require 'use-package)

We set some configuration for use-package:

  • The use-package-always-ensure variable indicates that use-package should always try to install missing packages. For some libraries this is not appropriate, and in those cases you see the :ensure nil declaration as part of the use-package statement. This applies mainly to libraries which are installed as part of some other package (happens mostly with some libraries that come with org-mode).
    (customize-set-variable 'use-package-always-ensure t)
    
  • The use-package-always-defer sets :defer true as the default for all package declarations. This makes Emacs startup much faster by preventing packages from being loaded when Emacs starts, and only doing so when they are needed. Some packages don’t work well with this, so you’ll see some declarations when I explicitly set :defer nil to force the package to be loaded at startup, or :defer n to load the package, but only n seconds after startup.
    (customize-set-variable 'use-package-always-defer t)
    
  • The use-package-verbose variable enables verbose loading of packages, useful for debugging. I set/unset this according to need.
    (customize-set-variable 'use-package-verbose nil)
    

Testing quelpa and to install packages directly from their github repositories (and other places). I install quelpa using use-package first, and then install quelpa-use-package to allow using quelpa from within use-package declarations. Very recursive.

(use-package quelpa
  :defer nil)

(use-package quelpa-use-package
  :defer nil
  :after quelpa)

This variable tells Emacs to prefer the .el file if it’s newer, even if there is a corresponding .elc file. Also, use auto-compile to autocompile files as needed.

(customize-set-variable 'load-prefer-newer t)
(use-package auto-compile
  :defer nil
  :config (auto-compile-on-load-mode))

Set the load path to the directories from where I sometimes load things outside the package system. Note that the path for org-mode (which I load from a checkout of its git repository) is set as part of its use-package declaration, so it doesn’t appear here.

(add-to-list 'load-path "~/.emacs.d/lisp")

Giving a try to Paradox for an enhanced package management interface. We set paradox-github-token to t to disable GitHub integration (I don’t want to star repos).

(use-package paradox
  :defer nil
  :custom
  (paradox-github-token t)
  :config
  (paradox-enable))

Settings

Proxy settings

These are two short functions I wrote to be able to set/unset proxy settings within Emacs. I haven’t bothered to improve or automate this, as I pretty much only need it to be able to install packages sometimes when I’m at work. For now I just call them manually with M-x zz/(un)set-proxy when I need to.

(defun zz/set-proxy ()
  (interactive)
  (customize-set-variable 'url-proxy-services
                          '(("http"  . "proxy.corproot.net:8079")
                            ("https" . "proxy.corproot.net:8079"))))
(defun zz/unset-proxy ()
  (interactive)
  (customize-set-variable 'url-proxy-services nil))

Miscellaneous settings

  • Load the cl library to enable some additional macros (e.g. lexical-let).
    (require 'cl)
    
  • Start the Emacs server
    (server-start)
    
  • This is probably one of my oldest settings - I remember adding it around 1993 when I started learning Emacs, and it has been in my config ever since. When time-stamp is run before every save, the string Time-stamp: <> in the first 8 lines of the file will be updated with the current timestamp.
    (add-hook 'before-save-hook 'time-stamp)
    
  • When at the beginning of the line, make Ctrl-K remove the whole line, instead of just emptying it.
    (customize-set-variable 'kill-whole-line t)
    
  • Paste text where the cursor is, not where the mouse is.
    (customize-set-variable 'mouse-yank-at-point t)
    
  • Make completion case-insensitive.
    (setq completion-ignore-case t)
    (customize-set-variable 'read-file-name-completion-ignore-case t)
    (customize-set-variable 'read-buffer-completion-ignore-case t)
    
  • Show line numbers. I used linum-mode before, but it caused severe performance issues on large files. Emacs 26 introduces display-line-numbers-mode, which has no perceivable performance impact even on very large files. Disabled for now.
    (when (>= emacs-major-version 26)
      (use-package display-line-numbers
        :defer nil
        :ensure nil
        :config
        (global-display-line-numbers-mode)))
    
  • Highlight trailing whitespace in red, so it’s easily visible (disabled for now as it created a lot of noise in some modes, e.g. the org-mode export screen)
    (customize-set-variable 'show-trailing-whitespace nil)
    
  • Highlight matching parenthesis
    (show-paren-mode)
    
  • Don’t use hard tabs
    (customize-set-variable 'indent-tabs-mode nil)
    
  • Emacs automatically creates backup files, by default in the same folder as the original file, which often leaves backup files behind. This tells Emacs to put all backups in ~/.emacs.d/backups.
    (customize-set-variable
     'backup-directory-alist
     `(("." . ,(concat user-emacs-directory "backups"))))
    
  • WinnerMode makes it possible to cycle and undo window configuration changes (i.e. arrangement of panels, etc.)
    (when (fboundp 'winner-mode) (winner-mode))
    
  • 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.
    (use-package unfill
      :bind
      ("M-q" . unfill-toggle)
      ("A-q" . unfill-paragraph))
    
  • Save the place of the cursor in each file, and restore it upon opening it again.
    (use-package saveplace
      :defer nil
      :config
      (save-place-mode))
    
  • Provide mode-specific “bookmarks” - press M-i and you will be presented with a list of elements to which you can navigate - they can be headers in org-mode, function names in emacs-lisp, etc.
    (use-package imenu-anywhere
      :bind
      ("M-i" . helm-imenu-anywhere))
    
  • Smooth scrolling (line by line) instead of jumping by half-screens.
    (use-package smooth-scrolling
      :config
      (smooth-scrolling-mode 1))
    
  • Delete trailing whitespace before saving a file.
    (add-hook 'before-save-hook 'delete-trailing-whitespace)
    
  • Suppress “ad-handle-definition: .. redefined” warnings during Emacs startup.
    (customize-set-variable 'ad-redefinition-action 'accept)
    

System-specific configuration

Some settings maybe OS-specific, and this is where we set them. For now I only use Emacs on my Mac, so only the Mac section is filled out, but there are sections for Linux and Windows as well.

(cond ((eq system-type 'darwin)
       <<Mac settings>>
       )
      ((eq system-type 'windows-nt)
       <<Windows settings>>
       )
      ((eq system-type 'gnu/linux)
       <<Linux settings>>
       ))

Mac

First, we set the key modifiers correctly to my preferences: Make Command (⌘) act as Meta, Option as Alt, right-Option as Super

«Mac settings»≡
(customize-set-variable 'mac-command-modifier 'meta)
(customize-set-variable 'mac-option-modifier 'alt)
(customize-set-variable 'mac-right-option-modifier 'super)

We also make it possible to use the familiar ⌘-+ and ⌘-- to increase and decrease the font size. ⌘-= is also bound to “increase” because it’s on the same key in an English keyboard.

«Mac settings»≡
(bind-key "M-+" 'text-scale-increase)
(bind-key "M-=" 'text-scale-increase)
(bind-key "M--" 'text-scale-decrease)

Somewhat surprisingly, there seems to be no “reset” function, so I define my own and bind it to ⌘-0.

«Mac settings»≡
(defun zz/text-scale-reset ()
  (interactive)
  (text-scale-set 0))
(bind-key "M-0" 'zz/text-scale-reset)

We also use the exec-path-from-shell to make sure the path settings from the shell are loaded into Emacs (usually it starts up with the default system-wide path).

«Mac settings»≡
(use-package exec-path-from-shell
  :defer nil
  :config
  (exec-path-from-shell-initialize))

Linux

There are no Linux-specific settings for now.

Windows

There are no Windows-specific settings for now.

Keybindings

The which-key package makes Emacs functionality much easier to discover and explore: in short, after you start the input of a command and stop, pondering what key must follow, it will automatically open a non-intrusive buffer at the bottom of the screen offering you suggestions for completing the command. Extremely useful.

(use-package which-key
  :defer nil
  :diminish which-key-mode
  :config
  (which-key-mode))

I use the bind-key package to more easily keep track and manage user keybindings. bind-key comes with use-package so we just load it.

The main advantage of using this over define-key or global-set-key is that you can use M-x describe-personal-keybindings to see a list of all the customized keybindings you have defined.

(require 'bind-key)

Miscellaneous keybindings

  • M-g interactively asks for a line number and jump to it (goto-line).
    (bind-key "M-g" 'goto-line)
    
  • M-` focuses the next frame, if multiple ones are active (emulate the Mac “next app window” keybinding)
    (bind-key "M-`" 'other-frame)
    
  • Interactive search key bindings - visual-regexp-steroids provides sane regular expressions and visual incremental search. We make C-s and C-r run the visual-regexp functions. We leave C-M-s and C-M-r to run the default isearch-forward/backward functions, as a fallback. I use the pcre2el package to support PCRE-style regular expressions.
    (use-package pcre2el)
    (use-package visual-regexp-steroids
      :custom
      (vr/engine 'pcre2el "Use PCRE regular expressions")
      :bind
      ("C-c r" . vr/replace)
      ("C-c q" . vr/query-replace)
      ("C-r"   . vr/isearch-backward)
      ("C-S-s" . vr/isearch-forward)
      ("C-M-s" . isearch-forward)
      ("C-M-r" . isearch-backward))
    
  • Key binding to use “hippie expand” for text autocompletion
    (bind-key "M-/" 'hippie-expand)
    

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 the functionality. Inspired by http://www.emacswiki.org/emacs/NavigatingParentheses, but modified to use smartparens instead of the default commands, and to work on brackets and braces.

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

We bind this function to the % key.

(bind-key "%" 'zz/goto-match-paren)

Org mode

I have started using org-mode to writing, blogging, coding, presentations and more, thanks to the hearty recommendations and information from Nick and many others. I am duly impressed. I have been a fan of the idea of literate programming for many years, and I have tried other tools before (most notably noweb, which I used during grad school for many of my homeworks and projects), but org-mode is the first tool I have encountered which seems to make it practical. Here are some of the resources I have found useful in learning it:

This is the newest and most-in-flux section of my Emacs config, since I’m still learning org-mode myself.

I use use-package to load the org package, and put its configuration inside the corresponding sections for keybindings (:bind), custom variables (:custom), custom faces (:custom-face), hooks (:hook) and general configuration code (:config), respectively. The contents of each section is populated with the corresponding snippets that follow. See the sections below for the details on what goes into each configuration section, and some other configuration code that ends up outside this declaration.

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

General Org Configuration

Note that mode-specific configuration variables are defined under their corresponding packages, this section defines only global org-mode configuration variables, which are inserted in the main use-package declaration for org-mode.

  • Default directory for org files (not all are stored here).
    «org-mode-custom-vars»≡
    (org-directory "~/Dropbox/Personal/org")
    
  • Automatically log done times in todo items.
    «org-mode-custom-vars»≡
    (org-log-done t)
    
  • Keep the indentation well structured by setting org-startup-indented to t. This is a must have. Makes it feel less like editing a big text file and more like a purpose built editor for org-mode that forces the indentation. Thanks Nick for the tip!
    «org-mode-custom-vars»≡
    (org-startup-indented t)
    

    By default, org-indent produces an indicator "Ind" in the modeline. We use diminish to hide it. I also like to increase the indentation a bit so that the levels are more visible.

    (use-package org-indent
      :ensure nil
      :diminish
      :custom
      (org-indent-indentation-per-level 4))
    
  • Log stuff into the LOGBOOK drawer by default
    «org-mode-custom-vars»≡
    (org-log-into-drawer t)
    

General Org Keybindings

Note that other keybindings are configured under their corresponding packages, this section defines only global org-mode keybindings, which are inserted in the main use-package declaration for org-mode.

  • Set up C-c l to store a link to the current org object, in counterpart to the default C-c C-l to insert a link.
    «org-mode-keybindings»≡
    ("C-c l" . org-store-link)
    
  • The default keybinding for org-mark-element is M-h, which in macOS hides the current application, so I bind it to A-h.
    «org-mode-keybindings»≡
    ("A-h" . org-mark-element)
    

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 (useful with the font highlighting I use, as all but the last asterisk are sometimes not visible).

«org-mode-custom-vars»≡
(org-use-speed-commands
 (lambda ()
   (and (looking-at org-outline-regexp)
        (looking-back "^\**"))))

Task tracking

Org-Agenda is the umbrella for all todo, journal, calendar, and other views. I set up C-c a to call up agenda mode.

(use-package org-agenda
  :ensure nil
  :after org
  :bind
  ("C-c a" . org-agenda)
  :custom
  (org-agenda-include-diary t)
  (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")))
  (org-agenda-start-on-weekday nil))

I also provide some customization for the holidays package, since its entries are included in the Org Agenda through the org-agenda-include-diary integration.

(use-package mexican-holidays
  :defer nil)
(quelpa '(swiss-holidays :fetcher github :repo "egli/swiss-holidays"))
(require 'swiss-holidays)
(use-package holidays
  :defer nil
  :ensure nil
  :init
  (require 'mexican-holidays)
  :config
  (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-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.

(require 'org-habit)
(use-package org-super-agenda
  :custom
  (org-super-agenda-groups '((:auto-dir-name t))))

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
  :ensure nil
  :custom
  (org-archive-location "archive.org::datetree/"))

Capturing stuff

First, 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).

I define a helper function to define keybindings that open files. Since I use the which-key package, it also defines the description of the key that will appear in the which-key menu. Note the use of lexical-let so that the lambda creates a closure, otherwise the keybindings don’t work.

(defun zz/add-file-keybinding (key file &optional desc)
  (lexical-let ((key key)
                (file file)
                (desc desc))
    (global-set-key (kbd key) (lambda () (interactive) (find-file file)))
    (which-key-add-key-based-replacements key (or desc file))))

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

(zz/add-file-keybinding "C-c f w" "~/Work/work.org.gpg" "work.org")
(zz/add-file-keybinding "C-c f p" "~/org/projects.org" "projects.org")
(zz/add-file-keybinding "C-c f i" "~/org/ideas.org" "ideas.org")
(zz/add-file-keybinding "C-c f d" "~/org/diary.org" "diary.org")

org-capture provides a generic and extensible interface to capturing things into org-mode in different formats. I set up C-c c as the default keybinding for triggering org-capture. Usually setting up a new capture template requires some custom code, which gets defined in the corresponding package config sections and included in the :config section below.

(use-package org-capture
  :ensure nil
  :after org
  :defer 1
  :bind
  ("C-c c" . org-capture)
  :config
  <<org-capture-config>>
  )

Building presentations

org-reveal is an awesome package for building presentations with org-mode. The MELPA version of the package gives me a conflict with my hand-installed version of org-mode, so I also install it by hand and load it directly from its checked-out repository.

(use-package ox-reveal
  :load-path ("lisp/org-reveal")
  :defer 3
  :after org
  :custom
  (org-reveal-note-key-char nil)
  (org-reveal-root "file:///Users/taazadi1/.emacs.d/lisp/reveal.js"))
(use-package htmlize
  :defer 3
  :after ox-reveal)

Various exporters

One of the big strengths of org-mode is the ability to export a document in many different formats. Here I load some of the exporters I have found useful.

  • HTML
    (use-package ox-html
      :ensure nil
      :defer 3
      :after org
      :custom
      (org-html-checkbox-type 'unicode))
    
  • Markdown
    (use-package ox-md
      :ensure nil
      :defer 3
      :after org)
    
  • GitHub Flavored Markdown
    (use-package ox-gfm
      :defer 3
      :after org)
    
  • Jira markup. I also load org-jira, which provides a full interface to Jira through org-mode.
    (use-package ox-jira
      :defer 3
      :after org)
    
    (use-package org-jira
      :defer 3
      :after org
      :custom
      (jiralib-url "https://jira.work.com"))
    
  • Confluence markup.
    (use-package ox-confluence
      :defer 3
      :ensure nil
      :after org)
    
  • AsciiDoc
    (use-package ox-asciidoc
      :defer 3
      :after org)
    
  • TexInfo. I have found that the best way to produce a PDF from an org file is to export it to a .texi file, and then use texi2pdf to produce the PDF.
    (use-package ox-texinfo
      :load-path "lisp/org-mode/lisp"
      :defer 3
      :ensure nil
      :after org)
    
  • Some customizations for the LaTeX exporter. ox-latex gets loaded automatically, but we use use-package anyway so that the config code is only executed after the package is loaded. I add a pseudo-class which uses the document class book but without parts (only chapters at the top level).
    (use-package ox-latex
      :load-path "lisp/org-mode/lisp"
      :ensure nil
      :demand
      :after org
      :custom
      (org-latex-compiler "xelatex")
      (org-latex-pdf-process
       '("%latex -shell-escape -interaction nonstopmode -output-directory %o %f"
         "%latex -interaction nonstopmode -output-directory %o %f"
         "%latex -interaction nonstopmode -output-directory %o %f"))
      :config
      (setq org-latex-listings 'minted)
      (add-to-list 'org-latex-packages-alist '("newfloat" "minted"))
      (add-to-list 'org-latex-minted-langs '(lua "lua"))
      (add-to-list 'org-latex-minted-langs '(shell "shell"))
      (add-to-list 'org-latex-classes
                   '("book-no-parts" "\\documentclass[11pt,letterpaper]{book}"
                     ("\\chapter{%s}" . "\\chapter*{%s}")
                     ("\\section{%s}" . "\\section*{%s}")
                     ("\\subsection{%s}" . "\\subsection*{%s}")
                     ("\\subsubsection{%s}" . "\\subsubsection*{%s}")
                     ("\\paragraph{%s}" . "\\paragraph*{%s}")))
      (add-to-list 'org-latex-classes
                   '("awesome-cv" "\\documentclass{awesome-cv}"
                     ("\\cvsection{%s}" . "\\cvsection{%s}")
                     ("\\cvsubsection{%s}" . "\\cvsubsection{%s}")
                     ("\\subsection{%s}" . "\\subsection*{%s}")
                     ("\\subsubsection{%s}" . "\\subsubsection*{%s}")
                     ("\\cvparagraph{%s}" . "\\cvparagraph{%s}")))
      ;; Necessary for LuaLaTeX to work - see
      ;; https://tex.stackexchange.com/a/374391/10680
      (setenv "LANG" "en_US.UTF-8"))
    
  • ox-clip to export HTML-formatted snippets.
    (use-package ox-clip
      :bind
      ("A-C-M-k" . ox-clip-formatted-copy))
    

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.
  • I am intrigued by ox-hugo’s “one post per org subtree” proposed structure. So far I’ve always had one file per post, but with org-mode’s structuring features, it might make sense to give it a try.
(use-package ox-hugo
  :defer 3
  :after org
  ;; Testing hooks to automatically set the filename on an ox-hugo
  ;; blog entry when it gets marked as DONE
  ;; :hook
  ;; (org-mode . (lambda ()
  ;;               (add-hook 'org-after-todo-state-change-hook
  ;;                         (lambda ()
  ;;                           (org-set-property
  ;;                            "testprop"
  ;;                            (concat "org-state: " org-state
  ;;                                    " prev-state: " (org-get-todo-state))))
  ;;                         'run-at-end 'only-in-org-mode)))
  )

Configure a capture template for creating new ox-hugo blog posts, from ox-hugo’s Org Capture Setup.

«org-capture-config»≡
(defun org-hugo-new-subtree-post-capture-template ()
  "Returns `org-capture' template string for new Hugo post.
See `org-capture-templates' for more information."
  (let* ((title (read-from-minibuffer "Post Title: "))
         (fname (org-hugo-slug title)))
    (mapconcat #'identity
               `(,(concat "* TODO " title)
                 ":PROPERTIES:"
                 ,(concat ":EXPORT_HUGO_BUNDLE: " fname)
                 ":EXPORT_FILE_NAME: index"
                 ":END:"
                 "%?\n") ; Place the cursor here finally
               "\n")))
(add-to-list 'org-capture-templates
             '("z"       ;`org-capture' binding + z
               "zzamboni.org post"
               entry
               ;; It is assumed that below file is present in `org-directory'
               ;; and that it has an "Ideas" heading. It can even be a
               ;; symlink pointing to the actual location of all-posts.org!
               (file+olp "zzamboni.org" "Ideas")
               (function org-hugo-new-subtree-post-capture-template)))

Encryption

First, load the built-in EasyPG support. By calling (epa-file-enable), Emacs automatically encrypts/decrypts files with a .gpg extension. By default it asks about the key to use, but I configure it to always use my own GPG key.

(use-package epa-file
  :ensure nil ;; included with Emacs
  :config
  (setq epa-file-encrypt-to '("diego@zzamboni.org"))
  (epa-file-enable)
  :custom
  (epa-file-select-keys 'silent))

Then, load org-crypt to enable selective encryption/decryption using GPG within org-mode.

(use-package org-crypt
  :ensure nil  ;; included with org-mode
  :after org
  :config
  (org-crypt-use-before-save-magic)
  (setq org-tags-exclude-from-inheritance (quote ("crypt")))
  :custom
  (org-crypt-key "diego@zzamboni.org"))

Keeping a Journal

I use 750words for my personal Journal, and I used to write my entries locally using Scrivener. Now I am using org-journal for this, works quite well together with wc-mode to keep a count of how many words I have written.

In order to keep my journal entries encrypted there are two separate but confusingly named mechanisms:

  • org-journal-encrypt-journal, if set to t has the effect of transparently encrypting/decrypting the journal files as they are written to disk. This is what I use.
  • org-journal-enable-encryption, if set to t, enables integration with org-crypt (see above), so it automatically adds a :crypt: tag to new journal entries. This has the effect of automatically encrypting those entries upon save, replacing them with a blob of gpg-encrypted text which has to be further decrypted with org-decrypt-entry in order to read or edit them again. I have disabled it for now to make it more transparent to work with my journal entries while I am editing them.
(use-package org-journal
  :after org
  :custom
  (org-journal-dir (concat (file-name-as-directory org-directory) "journal"))
  (org-journal-file-format "%Y/%m/%Y%m%d")
  (org-journal-date-format "%A, %Y-%m-%d")
  (org-journal-encrypt-journal t)
  (org-journal-enable-encryption nil)
  (org-journal-enable-agenda-integration t)
  :bind
  ("C-c j" . org-journal-new-entry))

Literate programming

Org-mode is the first literate programming tool that seems practical and useful, since it’s easy to edit, execute and document code from within the same tool (Emacs) using all of its existing capabilities (i.e. each code block can be edited in its native Emacs mode, taking full advantage of indentation, completion, etc.)

First, we load the necessary programming language support. The base features and literate programming for Emacs LISP is built-in, but the ob-* packages provide the ability to execute code in different languages directly from within the Org buffer, beyond those included with org-mode. I load the modules for some of the languages I use frequently:

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

    We determine the location of the PlantUML jar file automatically from the installed Homebrew formula.

    «plantuml-jar-command»≡
    brew list plantuml | grep jar
    

Which in my current setup results in the following:

/usr/local/Cellar/plantuml/1.2019.12/libexec/plantuml.jar

The command defined above is used to define the value of the homebrew-plantuml-jar-path variable. If you don’t use Homebrew of have installed PlantUML some other way, you need to modify this command, or hard-code the path.

(require 'subr-x)
(setq homebrew-plantuml-jar-path
      (expand-file-name
       (string-trim
        (shell-command-to-string "<<plantuml-jar-command>>"))))

Finally, we use this value to configure both plantuml-mode (for syntax highlighting) and ob-plantuml (for evaluating PlantUML code and inserting the results in exported Org documents).

(use-package plantuml-mode
  :custom
  (plantuml-jar-path homebrew-plantuml-jar-path))

(use-package ob-plantuml
  :ensure nil
  :after org
  :custom
  (org-plantuml-jar-path homebrew-plantuml-jar-path))
  • Define shell-script-mode as an alias for console-mode, so that console src blocks can be edited and are fontified correctly.
    (defalias 'console-mode 'shell-script-mode)
    
  • Finally, from all the available languages, we configure the ones for which to load org-babel support.
    «org-mode-config»≡
    (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)))
    

Now, we configure some other org-babel settings:

  • This little snippet 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.
    «org-mode-hooks»≡
    (org-mode . (lambda () (add-hook 'after-save-hook 'org-babel-tangle
                                     'run-at-end 'only-in-org-mode)))
    
  • This is potentially dangerous: it suppresses the query before executing code from within org-mode. I use it because I am very careful and only press C-c C-c on blocks I absolutely understand.
    «org-mode-custom-vars»≡
    (org-confirm-babel-evaluate nil)
    
  • This makes it so that code within src blocks is fontified according to their corresponding Emacs mode, making the file much more readable.
    «org-mode-custom-vars»≡
    (org-src-fontify-natively t)
    
  • In principle this makes it so that indentation in src blocks works as in their native mode, but in my experience it does not always work reliably. For full proper indentation, always edit the code in a native buffer by pressing C-c '.
    «org-mode-custom-vars»≡
    (org-src-tab-acts-natively t)
    
  • Automatically show inline images, useful when executing code that produces them, such as PlantUML or Graphviz.
    «org-mode-hooks»≡
    (org-babel-after-execute . org-redisplay-inline-images)
    
  • I add hooks to measure and report how long the tangling took. I first define a function to compute and report the elapsed time:
    «org-mode-config»≡
    (defun zz/report-tangle-time (start-time)
      (message "org-babel-tangle took %s"
               (format "%.2f seconds"
                       (float-time (time-since start-time)))))
    

    And this function is used in the corresponding hooks before and after org-babel-tangle:

    «org-mode-hooks»≡
    (org-babel-pre-tangle  . (lambda ()
                               (setq zz/pre-tangle-time (current-time))))
    (org-babel-post-tangle . (lambda ()
                               (zz/report-tangle-time zz/pre-tangle-time)))
    

Beautifying org-mode

These settings make org-mode much more readable by using different fonts for headings, hiding some of the markup, etc. This was taken originally from Howard Abrams’ Org as a Word Processor, and subsequently tweaked and broken up in the different parts of the use-package declaration by me.

First, we set org-hid-emphasis-markers so that the markup indicators are not shown.

«org-mode-custom-vars»≡
(org-hide-emphasis-markers t)

We add an entry to the org-mode font-lock table so that list markers are shown with a middle dot instead of the original character.

«org-mode-config»≡
(font-lock-add-keywords
 'org-mode
 '(("^ *\\([-]\\) "
    (0 (prog1 () (compose-region (match-beginning 1) (match-end 1) "•"))))))

We use the org-bullets package to display the titles with nice unicode bullets instead of the text ones.

(use-package org-bullets
  :after org
  :hook
  (org-mode . (lambda () (org-bullets-mode 1))))

We choose a nice font for the document title and the section headings. The first one found in the system from the list below is used, and the same font is used for the different levels, in varying sizes.

«org-mode-config»≡
(let* ((variable-tuple
        (cond ((x-list-fonts   "Source Sans Pro") '(:font   "Source Sans Pro"))
              ((x-list-fonts   "Lucida Grande")   '(:font   "Lucida Grande"))
              ((x-list-fonts   "Verdana")         '(:font   "Verdana"))
              ((x-family-fonts "Sans Serif")      '(:family "Sans Serif"))
              (nil (warn "Cannot find a Sans Serif Font."))))
       (base-font-color (face-foreground 'default nil 'default))
       (headline `(:inherit default :weight bold
                            :foreground ,base-font-color)))

  (custom-theme-set-faces
   'user
   `(org-level-8        ((t (,@headline ,@variable-tuple))))
   `(org-level-7        ((t (,@headline ,@variable-tuple))))
   `(org-level-6        ((t (,@headline ,@variable-tuple))))
   `(org-level-5        ((t (,@headline ,@variable-tuple))))
   `(org-level-4        ((t (,@headline ,@variable-tuple :height 1.1))))
   `(org-level-3        ((t (,@headline ,@variable-tuple :height 1.25))))
   `(org-level-2        ((t (,@headline ,@variable-tuple :height 1.5))))
   `(org-level-1        ((t (,@headline ,@variable-tuple :height 1.75))))
   `(org-headline-done  ((t (,@headline ,@variable-tuple :strike-through t))))
   `(org-document-title ((t (,@headline ,@variable-tuple
                                        :height 2.0 :underline nil))))))

I use proportional fonts in org-mode for the text, while keeping fixed-width fonts for blocks, so that source code, tables, etc. are shown correctly. These settings include:

  • Setting up the variable-pitch face to the proportional font I like to use. I’m currently alternating between my two favorites, Source Sans Pro and Avenir Next.
    «org-mode-faces»≡
    (variable-pitch ((t (:family "Source Sans Pro" :height 160 :weight light))))
    ;;(variable-pitch ((t (:family "Avenir Next" :height 160 :weight light))))
    
  • Setting up the fixed-pitch face to be the same as my usual default face. My current one is Inconsolata.
    «org-mode-faces»≡
    (fixed-pitch ((t (:family "Inconsolata"))))
    
  • Configure org-indent to inherit from fixed-pitch to fix the vertical spacing in code blocks. Thanks to Ben for the tip!
    «org-mode-faces»≡
    (org-indent ((t (:inherit (org-hide fixed-pitch)))))
    
  • Configure org-fontify-done-headline to apply a special face to DONE items in org-mode, and configure the org-done face to be used. Note that org-done only applies to the “DONE” keyword itself, the face for the rest of a “done” headline is defined above as the org-headline-done face.
    «org-mode-custom-vars»≡
    (org-fontify-done-headline t)
    
    «org-mode-faces»≡
    (org-done ((t (:foreground "PaleGreen"
                               :strike-through t))))
    
  • Configuring the corresponding org-mode faces for blocks, verbatim code, and maybe a couple of other things. As these change more frequently, I do them directly from the customize-face interface, you can see their current settings in the Customized variables section.
  • Setting up visual-line-mode and making all my paragraphs one single line, so that the lines wrap around nicely in the window according to their proportional-font size, instead of at a fixed character count, which does not work so nicely when characters have varying widths. I set up a hook that automatically enables visual-line-mode and variable-pitch-mode when entering org-mode.
    «org-mode-hooks»≡
    (org-mode . visual-line-mode)
    (org-mode . variable-pitch-mode)
    
  • In variable-pitch mode, the default right-alignment for headline tags doesn’t work, and results in the tags being misaligned (as it uses character positions to do the alignment). This setting positions the tags right after the last character of the headline, so at least they are more consistent.
    «org-mode-custom-vars»≡
    (org-tags-column 0)
    
  • I also set org-todo-keyword-faces to highlight different types of org-mode TODO items with different colors.
    «org-mode-custom-vars»≡
    (org-todo-keyword-faces
     '(("AREA"         . "DarkOrchid1")
       ("[AREA]"       . "DarkOrchid1")
       ("INBOX"        . "cyan")
       ("[INBOX]"      . "cyan")
       ("PROPOSAL"     . "orange")
       ("[PROPOSAL]"   . "orange")
       ("DRAFT"        . "yellow")
       ("[DRAFT]"      . "yellow")
       ("INPROGRESS"   . "yellow")
       ("[INPROGRESS]" . "yellow")
       ("MEETING"      . "purple")
       ("[MEETING]"    . "purple")
       ("CANCELED"     . "blue")
       ("[CANCELED]"   . "blue")))
    

    These two modes produce modeline indicators, which I disable using diminish.

    «org-mode-config»≡
    (eval-after-load 'face-remap '(diminish 'buffer-face-mode))
    (eval-after-load 'simple '(diminish 'visual-line-mode))
    
  • Prettify checkbox lists - courtesy of https://blog.jft.rocks/emacs/unicode-for-orgmode-checkboxes.html. First, we add special characters for checkboxes:
    «org-mode-hooks»≡
    (org-mode . (lambda ()
                  "Beautify Org Checkbox Symbol"
                  (push '("[ ]" . "☐" ) prettify-symbols-alist)
                  (push '("[X]" . "☑" ) prettify-symbols-alist)
                  (push '("[-]" . "⊡" ) prettify-symbols-alist)
                  (prettify-symbols-mode)))
    

    Second, we define a special face for checked items.

    «org-mode-config»≡
    (defface org-checkbox-done-text
      '((t (:foreground "#71696A" :strike-through t)))
      "Face for the text part of a checked org-mode checkbox.")
    
    (font-lock-add-keywords
     'org-mode
     `(("^[ \t]*\\(?:[-+*]\\|[0-9]+[).]\\)[ \t]+\\(\\(?:\\[@\\(?:start:\\)?[0-9]+\\\
    ][ \t]*\\)?\\[\\(?:X\\|\\([0-9]+\\)/\\2\\)\\][^\n]*\n\\)"
        1 'org-checkbox-done-text prepend))
     'append)
    

Auto-generated table of contents

The toc-org package allows us to insert a table of contents in headings marked with :TOC:. This is useful for org files that are to be viewed directly on GitHub, which renders org files correctly, but does not generate a table of contents at the top. For an example, see this file on GitHub.

Note that this breaks HTML export by default, as the links generated by toc-org cannot be parsed properly by the html exporter. The workaround is to use :TOC:noexport: as the marker, which removed the generated TOC from the export, but still allows ox-html to insert its own TOC at the top.

(use-package toc-org
  :after org
  :hook
  (org-mode . toc-org-enable))

org-mac-link (included in contrib) 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
  :ensure nil
  :load-path "lisp/org-mode/contrib/lisp"
  :after org
  :bind (:map org-mode-map
              ("C-c g" . org-mac-grab-link)))

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

Remove a link. For some reason this is not part of org-mode. From https://emacs.stackexchange.com/a/10714/11843, I bind it to C-c C-M-u.

(defun afs/org-remove-link ()
    "Replace an org link by its description or if empty its address"
  (interactive)
  (if (org-in-regexp org-bracket-link-regexp 1)
      (let ((remove (list (match-beginning 0) (match-end 0)))
        (description (if (match-end 3)
                 (org-match-string-no-properties 3)
                 (org-match-string-no-properties 1))))
    (apply 'delete-region remove)
    (insert description))))
(bind-key "C-c C-M-u" 'afs/org-remove-link)

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

Publishing project configuration

Define a publishing function based on org-latex-publish-to-pdf but which opens the resulting file at the end.

(defun org-latex-publish-to-latex-and-open (plist file pub-dir)
  (org-open-file (org-latex-publish-to-pdf plist file pub-dir)))

Sample project configuration - disabled for now because this configuration has been incorporated into the structure.tex file and in the general ox-latex configuration, but kept here as a sample.

«none»≡
(org-publish-project-alist
 '(("mac-automation"
    :base-directory "~/Personal/writing/mac-automation/"
    :publishing-directory "~/Personal/writing/mac-automation/build/"
    :base-extension "org"
    :publishing-function org-latex-publish-to-latex-and-open
    :latex-compiler "xelatex"
    :latex-classes '("book-no-parts" "\\documentclass[11pt]{book}"
                      ("\\chapter{%s}" . "\\chapter*{%s}")
                      ("\\section{%s}" . "\\section*{%s}")
                      ("\\subsection{%s}" . "\\subsection*{%s}")
                      ("\\subsubsection{%s}" . "\\subsubsection*{%s}")
                      ("\\paragraph{%s}" . "\\paragraph*{%s}"))
    :latex-class "book-no-parts"
    :latex-title-command "\\makeatletter\\begingroup
  \\thispagestyle{empty}
  \\begin{tikzpicture}[remember picture,overlay]
  \\node[inner sep=0pt] (background) at (current page.center) {\\includegraphic\
s[width=\\paperwidth]{background}};
  \\draw (current page.center) node [fill=ocre!30!white,fill opacity=0.6,text o\
pacity=1,inner sep=1cm]{\\Huge\\centering\\bfseries\\sffamily\\parbox[c][][t]{\\
\paperwidth}{\\centering \\@title \\\\[15pt]
  {\\Large \\@subtitle }\\\\[20pt]
  {\\huge \\@author }}};
  \\end{tikzpicture}
  \\vfill
  \\endgroup\\makeatother
  \\chapterimage{chapter_head_1.pdf}"
    :latex-toc-command "\\pagestyle{empty}
\\tableofcontents
\\cleardoublepage
\\pagestyle{fancy}"
    )))

Publishing to LeanPub

I use LeanPub for self-publishing my books Learning Hammerspoon and Learning CFEngine. Fortunately, it is possible to export from org-mode to LeanPub-flavored Markdown.

Some references:

First, load ox-leanpub-markdown. This is based on Juan’s ox-leanpub, but with many changes of my own, including a rename. You can get it from my fork at https://github.com/zzamboni/ox-leanpub/tree/book-and-markua.

(use-package ox-leanpub-markdown
  :defer 1
  :ensure nil
  :after org
  :load-path "lisp/ox-leanpub")
(use-package ox-leanpub-markua
  :defer 1
  :ensure nil
  :after org
  :load-path "lisp/ox-leanpub")

Next, load my ox-leanpub-book module (also available at https://github.com/zzamboni/ox-leanpub/tree/book-and-markua). It defines a new export backend called leanpub-book, which adds three additional items in the LeanPub export section:

  • “Multifile: Whole book”, which exports the whole book as one-file-per-chapter;
  • “Multifile: Subset”, which exports only the chapters that should be included in Subset.txt (if any), according to the rules listed below. I use this together with #+LEANPUB_WRITE_SUBSET: current in my files to quickly export only the current chapter, to be able to quickly preview it using LeanPub’s subset-preview feature;
  • “Multifile: Current chapter” to explicitly export only the current chapter to its own file. This also updates Subset.txt, so it can be used to preview the current chapter without having to set #+LEANPUB_WRITE_SUBSET: current.

The book files are populated as follows:

  • Book.txt with all chapters, except those tagged with noexport.
  • Sample.txt with all chapters tagged with sample.
  • Subset.txt with chapters depending on the value of the #+LEANPUB_WRITE_SUBSET file property (if set):
    • Default or none: not created.
    • tagged: use all chapters tagged subset.
    • all: use the same chapters as Book.txt.
    • sample: use same chapters as Sample.txt.
    • current: export the current chapter (where the cursor is at the moment of the export) as the contents of Subset.txt.

If a heading has the frontmatter, mainmatter or backmatter tags, the corresponding markup is inserted in the output, before the headline. This way, you only need to tag the first chapter of the front, main, and backmatter, respectively.

Note that the org-leanpub-book-setup-menu-markdown function gets called in the :config section. This is because I am working on ox-markua to export Leanpub’s new Markua format, and I plan for ox-leanpub-book to also support it.

(use-package ox-leanpub-book
  :defer 1
  :ensure nil
  :after ox-leanpub-markdown
  :load-path "lisp/ox-leanpub"
  :config
  (progn (org-leanpub-book-setup-menu-markdown)
         (org-leanpub-book-setup-menu-markua)))

Miscellaneous org functions and configuration

Utility org-get-keyword function (from the org-mode mailing list) to get the value of file-level properties.

(defun org-get-keyword (key)
  (org-element-map (org-element-parse-buffer 'element) 'keyword
    (lambda (k)
      (when (string= key (org-element-property :key k))
        (org-element-property :value k)))
    nil t))

org-sidebar provides a configurable sidebar to org buffers, showing the agenda, headlines, etc.

(use-package org-sidebar)

Appearance, buffer/file management and theming

Here we take care of all the visual, UX and desktop-management settings.

You’ll notice that many of the packages in this section have :defer nil. This is because some of these package are never called explicitly because they operate in the background, but I want them loaded when Emacs starts so they can perform their necessary customization.

Emacs 26 (which I am trying now) introduces pixel-level scrolling.

(when (>= emacs-major-version 26)
  (pixel-scroll-mode))

The diminish package makes it possible to remove clutter from the modeline. Here we just load it, it gets enabled for individual packages in their corresponding declarations.

(use-package diminish
  :defer 1)

I have been playing with different themes, and I have settled for now in gruvbox. Some of my other favorites are also here so I don’t forget about them.

;;(use-package solarized-theme)
;;(use-package darktooth-theme)
;;(use-package kaolin-themes)
(use-package gruvbox-theme)
(load-theme 'gruvbox)

Install smart-mode-line for modeline goodness, including configurable abbreviation of directories, and other things.

(use-package smart-mode-line
  :defer 2
  :config
  (sml/setup)
  :custom
  (sml/theme 'dark)
  (sml/replacer-regexp-list
   '(("^~/\\.emacs\\.d/elpa/"                            ":ELPA:")
     ("^~/\\.emacs\\.d/"                                 ":ED:")
     ("^/sudo:.*:"                                       ":SU:")
     ("^~/Documents/"                                    ":Doc:")
     ("^:\\([^:]*\\):Documento?s/"                       ":\\1/Doc:")
     ("^~/Dropbox/"                                      ":DB:")
     ("^:DB:org"                                         ":Org:")
     ("^:DB:Personal/"                                   ":P:")
     ("^:DB:Personal/writing/"                           ":Write:")
     ("^:P:devel/"                                       ":Dev:")
     ("^:Write:learning-cfengine-3/learning-cfengine-3/" ":cf-learn:")
     ("^:Dev:go/src/github.com/elves/elvish/"            ":elvish:")
     ("^:Dev:zzamboni.org/zzamboni.org/"                 ":zz.org:"))))

Enable desktop-save mode, which saves the current buffer configuration on exit and reloads it on restart.

Desktop mode also includes the desktop-clear function, which can be used to kill all open buffers. I bind it to Control-Meta-super-k.

(use-package desktop
  :defer nil
  :custom
  (desktop-restore-eager   1 "Restore the first buffer right away")
  (desktop-lazy-idle-delay 1 "Restore the other buffers 1 second later")
  (desktop-lazy-verbose  nil "Be silent about lazily opening buffers")
  :bind
  ("C-M-s-k" . desktop-clear)
  :config
  (desktop-save-mode))

The uniquify package makes it much easier to identify different open files with the same name by prepending/appending their directory or some other information to them. I configure it to add the directory name after the filename. uniquify is included with Emacs, so I specify :ensure nil so that use-package doesn’t try to install it, and just loads and configures it.

(use-package uniquify
  :defer 1
  :ensure nil
  :custom
  (uniquify-after-kill-buffer-p t)
  (uniquify-buffer-name-style 'post-forward)
  (uniquify-strip-common-suffix t))

I like to highlight the current line. For this I use the built-in hl-line.

(use-package hl-line
  :defer nil
  :config
  <<hl-line custom line-range function>>
  (global-hl-line-mode))

I also provide a custom value for hl-line-range-function (thanks to Eric on the org-mode mailing list for the tip) which highlights only the current visual line in visual-line-mode, which I use for Org-mode files (see Beautifying org-mode).

«hl-line custom line-range function»≡
(defun zz/get-visual-line-range ()
  (let (b e)
    (save-excursion
      (beginning-of-visual-line)
      (setq b (point))
      (end-of-visual-line)
      (setq e (+ 1 (point)))
      )
    (cons b e)))
(setq hl-line-range-function #'zz/get-visual-line-range)

I have also experimented with highlighting the current column. At the moment the code below is all disabled because I find it too distracting, but I’m leaving it here for reference. I found two options to achieve this:

  • The col-highlight package, which highlights the column only after a defined interval has passed
  • The crosshairs package, which always highlights both the column and the line. It also has a “highlight crosshairs when idle” mode, but I prefer to have the current line always highlighted.
(use-package col-highlight
  :disabled
  :defer nil
  :config
  (col-highlight-toggle-when-idle)
  (col-highlight-set-interval 2))
(use-package crosshairs
  :disabled
  :defer nil
  :config
  (crosshairs-mode))

I also use recentf to keep a list of recently open buffers. These are visible in helm’s open-file mode.

(use-package recentf
  :defer 1
  :custom
  (recentf-max-menu-items 100)
  (recentf-max-saved-items 100)
  :init
  (recentf-mode))

The ibuffer package allows all sort of useful operations on the list of open buffers. I haven’t customized it yet, but I have a keybinding to open it. (Disabled for now as I am using helm’s helm-buffer-list).

(use-package ibuffer
  :disabled
  :bind
  ("C-x C-b" . ibuffer))

The smex package is incredibly useful, adding IDO integration and some other very nice features to M-x, which make it easier to discover and use Emacs commands. Highly recommended. (Disabled for now as I’m using helm’s helm-M-x).

(use-package smex
  :disabled
  :bind (("M-x" . smex))
  :config (smex-initialize))

midnight-mode purges buffers which haven’t been displayed in 3 days. We configure the period so that the cleanup happens every 2 hours (7200 seconds).

(use-package midnight
  :defer 3
  :config
  (setq midnight-period 7200)
  (midnight-mode 1))

For distraction-free writing, I’m testing out writeroom-mode.

(use-package writeroom-mode)

NeoTree shows a navigation tree on a sidebar, and allows a number of operations on the files and directories. I’m not much of a fan of this type of interface in Emacs, but I have set it up to check it out.

(use-package neotree
  :custom
  (neo-theme (if (display-graphic-p) 'icons 'arrow))
  (neo-smart-open t)
  (projectile-switch-project-action 'neotree-projectile-action)
  :config
  (defun neotree-project-dir ()
    "Open NeoTree using the git root."
    (interactive)
    (let ((project-dir (projectile-project-root))
          (file-name (buffer-file-name)))
      (neotree-toggle)
      (if project-dir
          (if (neo-global--window-exists-p)
              (progn
                (neotree-dir project-dir)
                (neotree-find file-name)))
        (message "Could not find git project root."))))
  :bind
  ([f8] . neotree-project-dir))

wc-mode allows counting characters and words, both on demand and continuously. It also allows setting up a word/character goal.

(use-package wc-mode
  :defer 3
  :hook
  (org-journal-mode . wc-mode))

The all-the-icons package provides a number of useful icons.

(use-package all-the-icons
  :defer 3)

Completion: IDO or Helm?

The battle rages on - helm or IDO? Both are nice completion frameworks for Emacs, and both integrate nicely with most main Emacs functions, including file opening, command and buffer selection, etc. I was using IDO for some time but are now giving helm a try. Both my configs are shown below, but only Helm is enabled at the moment.

Should I also look at ivy?

IDO

I use IDO mode to get better matching capabilities everywhere in Emacs (disabled while I give helm a try, see below).

(use-package ido
  :disabled
  :config
  (ido-mode t)
  (ido-everywhere 1)
  (setq ido-use-virtual-buffers t)
  (setq ido-enable-flex-matching t)
  (setq ido-use-filename-at-point nil)
  (setq ido-auto-merge-work-directories-length -1))

(use-package ido-completing-read+
  :disabled
  :config
  (ido-ubiquitous-mode 1))
Helm

This config came originally from Uncle Dave’s Emacs config, thought I have tweaked it a bit.

(use-package helm
  :defer 1
  :diminish helm-mode
  :bind
  (("C-x C-f"       . helm-find-files)
   ("C-x C-b"       . helm-buffers-list)
   ("C-x b"         . helm-multi-files)
   ("M-x"           . helm-M-x)
   :map helm-find-files-map
   ("C-<backspace>" . helm-find-files-up-one-level)
   ("C-f"           . helm-execute-persistent-action)
   ([tab]           . helm-ff-RET))
  :config
  (defun daedreth/helm-hide-minibuffer ()
    (when (with-helm-buffer helm-echo-input-in-header-line)
      (let ((ov (make-overlay (point-min) (point-max) nil nil t)))
        (overlay-put ov 'window (selected-window))
        (overlay-put ov 'face
                     (let ((bg-color (face-background 'default nil)))
                       `(:background ,bg-color :foreground ,bg-color)))
        (setq-local cursor-type nil))))
  (add-hook 'helm-minibuffer-set-up-hook 'daedreth/helm-hide-minibuffer)
  (setq helm-autoresize-max-height 0
        helm-autoresize-min-height 40
        helm-M-x-fuzzy-match t
        helm-buffers-fuzzy-matching t
        helm-recentf-fuzzy-match t
        helm-semantic-fuzzy-match t
        helm-imenu-fuzzy-match t
        helm-split-window-in-side-p nil
        helm-move-to-line-cycle-in-source nil
        helm-ff-search-library-in-sexp t
        helm-scroll-amount 8
        helm-echo-input-in-header-line nil)
  :init
  (helm-mode 1))

(require 'helm-config)
(helm-autoresize-mode 1)

(use-package helm-flx
  :custom
  (helm-flx-for-helm-find-files t)
  (helm-flx-for-helm-locate t)
  :config
  (helm-flx-mode +1))

(use-package swiper-helm
  :bind
  ("C-s" . swiper))

Coding

Coding is one of my primary uses for Emacs, although lately it has shifted toward more general writing. This used to be the largest section in my config until Org mode overtook it :)

General settings and modules

When enabled, subword allows navigating “sub words” individually in CamelCaseIdentifiers. For now I only enable it in clojure-mode.

(use-package subword
  :hook
  (clojure-mode . subword-mode))

With aggressive-indent, indentation is always kept up to date in the whole buffer. Sometimes it gets in the way, but in general it’s nice and saves a lot of work, so I enable it for all programming modes except for Python mode, where I explicitly disable as it often gets the indentation wrong and messes up existing code.

Disabled for now while I test how much I miss it (I often find it gets in the way, but I’m not sure how often it helps and I don’t even notice it)

(use-package aggressive-indent
  :disabled
  :diminish aggressive-indent-mode
  :hook
  (prog-mode . aggressive-indent-mode)
  (python-mode . (lambda () (aggressive-indent-mode -1))))

With company-mode, we get automatic completion - when there are completions available, a popup menu will appear when you stop typing for a moment, and you can either continue typing or accept the completion using the Enter key. I enable it globally.

(use-package company
  :diminish company-mode
  :hook
  (after-init . global-company-mode))

projectile-mode allows us to perform project-relative operations such as searches, navigation, etc.

(use-package projectile
  :defer 2
  :diminish projectile-mode
  :config
  (projectile-global-mode))

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.

(use-package iedit
  :config
  (set-face-background 'iedit-occurrence "Magenta")
  :bind
  ("C-;" . iedit-mode))

Turn on the online documentation mode for all programming modes (not all of them support it) and for the Clojure REPL cider mode.

(use-package eldoc
  :diminish
  :hook
  (prog-mode       . turn-on-eldoc-mode)
  (cider-repl-mode . turn-on-eldoc-mode))

On-the-fly spell checking. I enable it for all text modes.

(use-package flyspell
  :defer 1
  :diminish)

Clojure and LISP coding

I dabble in Clojure and Emacs LISP, and Emacs has some fantastic support for them. There’s a number of packages and configuration related to this, so I have a whole section for it.

The centerpiece is of course clojure-mode. In addition to files ending in .clj, I bind it automatically to .boot files (both by extension and by shebang line) and to the Riemann config files.

(use-package clojure-mode
  :mode "\\.clj.*$"
  :mode "riemann.config"
  :mode "\\.boot"
  :config
  (add-to-list 'magic-mode-alist '(".* boot" . clojure-mode)))

Enable some additional fontification for Clojure code.

(use-package clojure-mode-extra-font-locking)

The cider package provides a fantastic REPL built into Emacs. We configure a few aspects, including pretty printing, fontification, history size and others.

(use-package cider
  :custom
  ;; nice pretty printing
  (cider-repl-use-pretty-printing nil)
  ;; nicer font lock in REPL
  (cider-repl-use-clojure-font-lock t)
  ;; result prefix for the REPL
  (cider-repl-result-prefix "; => ")
  ;; never ending REPL history
  (cider-repl-wrap-history t)
  ;; looong history
  (cider-repl-history-size 5000)
  ;; persistent history
  (cider-repl-history-file "~/.emacs.d/cider-history")
  ;; error buffer not popping up
  (cider-show-error-buffer nil)
  ;; go right to the REPL buffer when it's finished connecting
  (cider-repl-pop-to-buffer-on-connect t))

We use clj-refactor for supporting advanced code refactoring in Clojure.

(use-package clj-refactor
  :config
  (defun my-clojure-mode-hook ()
    (clj-refactor-mode 1)
    (yas-minor-mode 1) ; for adding require/use/import statements
    ;; This choice of keybinding leaves cider-macroexpand-1 unbound
    (cljr-add-keybindings-with-prefix "C-c C-m"))
  :hook
  (clojure-mode . my-clojure-mode-hook))

Use emr for supporting refactoring in Emacs LISP and some other languages.

(use-package emr
  :config
  (bind-key "A-RET" 'emr-show-refactor-menu prog-mode-map))

When coding in LISP-like languages, rainbow-delimiters is a must-have - it marks each concentric pair of parenthesis with different colors, which makes it much easier to understand expressions and spot mistakes.

(use-package rainbow-delimiters
  :hook
  ((prog-mode cider-repl-mode) . rainbow-delimiters-mode))

Another useful addition for LISP coding - smartparens enforces parenthesis to match, and adds a number of useful operations for manipulating parenthesized expressions. 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)))

(use-package smartparens
  :diminish smartparens-mode
  :config
  (require 'smartparens-config)
  :custom
  (sp-base-key-bindings 'paredit)
  :hook
  ((clojure-mode
    emacs-lisp-mode
    lisp-mode
    cider-repl-mode
    racket-mode
    racket-repl-mode) . smartparens-strict-mode)
  (smartparens-mode  . sp-use-paredit-bindings)
  (smartparens-mode  . (lambda ()
                         (local-set-key (kbd "M-(")
                                        'zz/sp-enclose-next-sexp))))

Minor mode for highlighting the current sexp in LISP modes.

(use-package hl-sexp
  :hook
  ((clojure-mode lisp-mode emacs-lisp-mode) . hl-sexp-mode))

Trying out lispy for LISP code editing (disabled for now).

(use-package lispy
  :disabled
  :config
  (defun enable-lispy-mode () (lispy-mode 1))
  :hook
  ((clojure-mode
    emacs-lisp-mode
    common-lisp-mode
    scheme-mode
    lisp-mode) . enable-lispy-mode))

Other programming languages

Many other programming languages are well served by a single mode, without so much setup as Clojure/LISP.

  • CFEngine policy files.
    (use-package cfengine
      :commands cfengine3-mode
      :mode ("\\.cf\\'" . cfengine3-mode))
    
  • Perl.
    (use-package cperl-mode
      :mode "\\.p[lm]\\'"
      :interpreter "perl"
      :config
      (setq cperl-hairy t))
    
  • Fish shell.
    (use-package fish-mode
      :mode "\\.fish\\'"
      :interpreter "fish")
    
  • Lua, which I use for Hammerspoon configuration.
    (use-package lua-mode)
    
  • YAML, generally useful
    (use-package yaml-mode)
    
  • AppleScript
    (use-package applescript-mode)
    
  • Go
    (use-package go-mode)
    
  • Build and check MELPA package definitions
    (use-package package-build)
    (use-package package-lint)
    
  • Elvish shell
    (use-package elvish-mode)
    
  • Racket
    (use-package racket-mode)
    
  • Nix package files
    (use-package nix-mode)
    
  • Dockerfile files
    (use-package dockerfile-mode)
    
  • The Dhall configuration language
    (use-package dhall-mode
      :ensure t
      :mode "\\.dhall\\'")
    

Other tools

  • Use helm-pass as an interface to pass.
    (use-package helm-pass)
    
  • git interface with some simple configuration I picked up somewhere. When you press C-c C-g, magit-status runs full-screen, but when you press q, it restores your previous window setup. Very handy.
    (use-package magit
      :diminish auto-revert-mode
      :bind
      (("C-c C-g" . magit-status)
       :map magit-status-mode-map
       ("q"       . magit-quit-session))
      :config
      (defadvice magit-status (around magit-fullscreen activate)
        "Make magit-status run alone in a frame."
        (window-configuration-to-register :magit-fullscreen)
        ad-do-it
        (delete-other-windows))
    
      (defun magit-quit-session ()
        "Restore the previous window configuration and kill the magit buffer."
        (interactive)
        (kill-buffer)
        (jump-to-register :magit-fullscreen)))
    
  • Interface to use the silver-searcher
    (use-package ag)
    
  • Publishing with Hugo. I don’t use this anymore since I started blogging with ox-hugo. I keep it loaded, but without its keybinding, because it makes it easy sometimes to see the history of my Markdown posts.
    (use-package easy-hugo
      :custom
      (easy-hugo-basedir "~/Personal/devel/zzamboni.org/zzamboni.org/")
      (easy-hugo-url "http://zzamboni.org/")
      (easy-hugo-previewtime "300")
      ;;(define-key global-map (kbd "C-c C-e") 'easy-hugo)
      )
    
  • Function to randomize the order of lines in a region, from https://www.emacswiki.org/emacs/RandomizeBuffer.
    (defun my-randomize-region (beg end)
      "Randomize lines in region from BEG to END."
      (interactive "*r")
      (let ((lines (split-string
                    (delete-and-extract-region beg end) "\n")))
        (when (string-equal "" (car (last lines 1)))
          (setq lines (butlast lines 1)))
        (apply 'insert
               (mapcar 'cdr
                       (sort (mapcar
                              (lambda (x)
                                (cons (random) (concat x "\n")))
                              lines)
                             (lambda (a b) (< (car a) (car b))))))))
    
  • auto-insert mode for automatically inserting user-defined templates for certain file types. It’s included with Emacs, so I just configure its directory to one inside my Dropbox, and set the hook to run it automatically when opening a file.
    (use-package autoinsert
      :ensure nil
      :custom
      (auto-insert-directory (concat user-emacs-directory "auto-insert/"))
      :hook
      (find-file . auto-insert))
    
  • Create and manage GitHub gists. Setting gist-view-gist to t makes it open new gists in the web browser automatically after creating them.
    (use-package gist
      :custom
      (gist-view-gist t "Automatically open new gists in browser"))
    
  • Emacs Startup Profiler, to get detailed stats of what’s taking time during initialization.
    (use-package esup)
    
  • Macro to measure how long a command takes, from https://stackoverflow.com/questions/23622296/emacs-timing-execution-of-function-calls-in-emacs-lisp
(defmacro measure-time (&rest body)
  "Measure the time it takes to evaluate BODY."
  `(let ((time (current-time)))
     ,@body
     (message "%.06f" (float-time (time-since time)))))
  • Trying out Deft
    (use-package deft
      :custom
      (deft-use-filename-as-title nil)
      (deft-use-filter-string-for-filename t)
      (deft-file-naming-rules '((noslash . "-")
                                (nospace . "-")
                                (case-fn . downcase)))
      (deft-org-mode-title-prefix t)
      (deft-extensions '("org" "txt" "text" "md" "markdown"))
      (deft-default-extension "org"))
    
  • Ability to restart Emacs from within Emacs:
    (use-package restart-emacs)
    
  • Multiple cursors
    (use-package multiple-cursors
      :bind
      ("C-c m c"   . mc/edit-lines)
      ("C-c m <"   . mc/mark-next-like-this)
      ("C-c m >"   . mc/mark-previous-like-this)
      ("C-c m C-<" . mc/mark-all-like-this))
    

General text editing

In addition to coding, I configure some modes that can be used for text editing.

  • AsciiDoc, which I use for my book and some other text. I also set up visual-line-mode and variable-pitch-mode here. adoc-mode is not so granular as org-mode with respect to face assignments, so the variable/fixed distinction does not always work, but it’s still pretty good for long-text editing.
    (use-package adoc-mode
      :mode "\\.asciidoc\\'"
      :hook
      (adoc-mode . visual-line-mode)
      (adoc-mode . variable-pitch-mode))
    
  • Markdown, generally useful. I also set up variable pitch and visual line mode.
    (use-package markdown-mode
      :hook
      (markdown-mode . visual-line-mode)
      (markdown-mode . variable-pitch-mode))
    
  • When typopunct is enabled (needs to be enabled by hand), automatically inserts “pretty” quotes of the appropriate type.
    (use-package typopunct
      :config
      (typopunct-change-language 'english t))
    
  • undo-tree visualises undo history as a tree for easy navigation (found about this from Jamie’s config)
    (use-package undo-tree
      :ensure t
      :diminish undo-tree-mode
      :config
      (global-undo-tree-mode 1))
    

Cheatsheet and experiments

Playground and how to do different things, not necessarily used in my Emacs config but useful sometimes.

This is how we get a global header property in org-mode

(alist-get :tangle
           (org-babel-parse-header-arguments
            (org-entry-get-with-inheritance "header-args")))

Testing formatting org snippets to look like noweb-rendered output (disabled for now).

(eval-after-load 'ob
  (customize-set-variable
   'org-entities-user
   '(("llangle" "\\llangle" t "&lang;&lang;" "<<" "<<" "«")
     ("rrangle" "\\rrangle" t "&rang;&rang;" ">>" ">>" "»")))
  (setq org-babel-exp-code-template
        (concat "\n@@latex:\\noindent@@\\llangle​/%name/​\\rrangle\\equiv\n"
                org-babel-exp-code-template)))

An experiment to reduce file tangle time, from https://www.wisdomandwonder.com/article/10630/how-fast-can-you-tangle-in-org-mode. In my tests it doesn’t have a noticeable impact.

(setq help/default-gc-cons-threshold gc-cons-threshold)
(defun help/set-gc-cons-threshold (&optional multiplier notify)
  "Set `gc-cons-threshold' either to its default value or a
   `multiplier' thereof."
  (let* ((new-multiplier (or multiplier 1))
         (new-threshold (* help/default-gc-cons-threshold
                           new-multiplier)))
    (setq gc-cons-threshold new-threshold)
    (when notify (message "Setting `gc-cons-threshold' to %s"
                          new-threshold))))
(defun help/double-gc-cons-threshold () "Double `gc-cons-threshold'." (help/set\
-gc-cons-threshold 10))
(add-hook 'org-babel-pre-tangle-hook #'help/double-gc-cons-threshold)
(add-hook 'org-babel-post-tangle-hook #'help/set-gc-cons-threshold)

A work-in-progress Hammerspoon shell for Emacs, posted on the Hammerspoon mailing list.

;;===> hammerspoon-shell
;; Quick and dirty shell with interactive history search and persistence
;; Just drop into your ~/.emacs file.
;;
;; A hammerspoon buffer is any lua buffer visiting a pathname like
;;    **/*hammerspoon**/*.lua
;; Usage: M-x hammerspoon-shell, or Hyper-s in a hammerspoon buffer.
;; In any hammerspoon buffer, Hyper-c runs dofile(file) on the visited file.
;;
;; Tip: to reload a Spoon "MySpoon" without hs.reload:
;; package.loaded.MySpoon=false hs.spoons.use("MySpoon",{config={debug=true})
(add-hook 'lua-mode-hook
          (lambda ()
            (when (string-match "hammerspoon" buffer-file-name)
              (local-set-key (kbd "H-s") #'hammerspoon-shell)
              (local-set-key
               (kbd "H-c")
               (lambda ()
                 (interactive)
                 (save-buffer)
                 (let ((name buffer-file-name))
                   (unless (and (boundp 'hammerspoon-buffer)
                                (buffer-live-p hammerspoon-buffer))
                     (hammerspoon-shell))
                   (with-current-buffer hammerspoon-buffer
                     (goto-char (point-max))
                     (insert (concat "dofile(\"" name "\")"))
                     (comint-send-input))))))))

(defvar hammerspoon-buffer nil)
(defun hammerspoon-shell ()
  (interactive)
  (if (and hammerspoon-buffer (comint-check-proc hammerspoon-buffer))
      (pop-to-buffer hammerspoon-buffer)
    (setq hammerspoon-buffer (make-comint "hammerspoon"
                                          "/usr/local/bin/hs" nil "-C"))
    (let* ((process (get-buffer-process hammerspoon-buffer))
           (history-file "~/.hammerspoon/.hs-history"))
      (pop-to-buffer hammerspoon-buffer)
      (turn-on-comint-history history-file)
      (local-set-key (kbd "<down>") (lambda() (interactive)
                                      (comint-move-or-history nil)))
      (local-set-key (kbd "<up>") (lambda() (interactive)
                                    (comint-move-or-history 'up))))))

;; Comint configs and extensions
(setq comint-input-ring-size 1024
      comint-history-isearch 'dwim)
(defun comint-move-or-history (up &optional arg)
  "History if at process mark, move otherwise"
  (interactive)
  (let* ((proc (get-buffer-process (current-buffer)))
         (proc-pos (if proc (marker-position (process-mark proc))))
         (arg (or arg 1))
         (arg (if up arg (- arg))))
    (if (and proc
             (if up
                 (= (line-number-at-pos) (line-number-at-pos proc-pos))
               (= (line-number-at-pos) (line-number-at-pos (point-max)))))
        (comint-previous-input arg)
      (forward-line (- arg)))))

(defun comint-write-history-on-exit (process event)
  (comint-write-input-ring)
  (let ((buf (process-buffer process)))
    (when (buffer-live-p buf)
      (with-current-buffer buf
        (insert (format "\nProcess %s %s" process event))))))

(defun turn-on-comint-history (&optional file)
  (let ((process (get-buffer-process (current-buffer))))
    (when process
      (setq comint-input-ring-file-name
            (or file
                (format "~/.emacs.d/inferior-%s-history"
                        (process-name process))))
      (comint-read-input-ring)
      ;; Ensure input ring gets written
      (add-hook 'kill-buffer-hook 'comint-write-input-ring nil t)
      (set-process-sentinel process
                            #'comint-write-history-on-exit))))

;; Ensure all input rings get written on exit
(defun comint-write-input-ring-all-buffers ()
  (mapc (lambda (buffer)
          (with-current-buffer buffer
            (comint-write-input-ring)))
        (buffer-list)))
(add-hook 'kill-emacs-hook 'comint-write-input-ring-all-buffers)

Epilogue

Here we close the let expression from the preface.

)

We also reset the value of gc-cons-threshold, not to its original value, we still leave it larger than default so that GCs don’t happen so often.

(setq gc-cons-threshold (* 2 1000 1000))

Hammerspoon config

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 and shift_hyper as the modifiers for most of my key bindings, so I define them as variables here for easier referencing.

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

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

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.

JiraApp = "org.epichrome.app.Jira"
WikiApp = "org.epichrome.app.Wiki"
CollabApp = "org.epichrome.app.Collab"
OpsGenieApp = "org.epichrome.app.OpsGenie"

Install:andUse("URLDispatcher",
               {
                 config = {
                   url_patterns = {
                     { "https?://issue.work.com",         JiraApp },
                     { "https?://jira.work.com",          JiraApp },
                     { "https?://wiki.work.com",          WikiApp },
                     { "https?://collaboration.work.com", CollabApp },
                     { "https?://app.opsgenie.com",       OpsGenieApp },
                     { "https?://app.eu.opsgenie.com",    OpsGenieApp },
                   },
                   default_handler = "com.google.Chrome"
                   -- default_handler = "com.electron.brave"
                   -- default_handler = "com.brave.Browser.dev"
                 },
                 start = true
               }
)

Window and screen manipulation

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

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

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

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

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 = { { "6x4" } } },
                 hotkeys = {show_grid = {hyper, "g"}},
                 start = true
               }
)

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

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" } }
               }
)

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",
               {
                 config = {
                   quickentrydialog = false,
                   notifications = false
                 },
                 hotkeys = {
                   send_to_omnifocus = { hyper, "t" }
                 },
                 fn = OF_register_additional_apps,
               }
)

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

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

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

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

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 Hamerspoon 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
               }
)

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,
})

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},
                     German = {col.black, col.red, col.yellow},
                   }
                 },
                 start = true
               }
)

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" }
                 }
               }
)

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,
               }
)

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 = {hyper, "c"},
                   open_brew_cask_url = {shift_hyper, "c"},
                 }
               }
)

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, "/" }
}})

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

Other applications

The ToggleSkypeMute spoon sets up the missing keyboard bindings for toggling the mute button on Skype and Skype for Business. I’m not fully happy with this spoon - it should auto-detect the application instead of having separate keys for each application, and it could be extended to more generic use.

Install:andUse("ToggleSkypeMute",
               {
                 hotkeys = {
                   toggle_skype = { shift_hyper, "v" },
                   toggle_skype_for_business = { shift_hyper, "f" }
                 }
               }
)

The HeadphoneAutoPause spoon implements auto-pause/resume for iTunes, Spotify and others when the headphones are unplugged.

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

Seal

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 = "https://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.

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.

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 = "lit-config"  },
                     { slug = "zztestbook" },
                   }
                 },
                 start = true,
})

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!", "")

Elvish config

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.

E:GOPATH = ~/Dropbox/Personal/devel/go
E:RACKETPATH = ~/Library/Racket/7.2
paths = [
  ~/bin
  $E:GOPATH/bin
  $E:RACKETPATH/bin
  ~/Library/Python/3.7/bin
  /usr/local/opt/coreutils/libexec/gnubin
  /usr/local/opt/texinfo/bin
  /usr/local/opt/python/libexec/bin
  /usr/local/opt/ruby/bin
  ~/Dropbox/Personal/devel/hammerspoon/spoon/bin
  ~/.gem/ruby/2.4.0/bin
  /opt/X11/bin
  /Library/TeX/texbin
  /usr/local/bin
  /usr/local/sbin
  /usr/sbin
  /sbin
  /usr/bin
  /bin
]

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=$true   \
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://proxy.corproot.net:8079"

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|swissptt.ch)' /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

Base modules

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~
    

Aliases

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 -W -p -/dev/disk1s4,devfs,map
alias:new ls e:exa --color-scale --git --group-directories-first
alias:new cat bat
alias:new more bat --paging always
alias:new v vagrant

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:git-command = hub
git: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 = $true

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

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

Customize some of the glyphs for the font I use in my terminal.

chain:glyph[git-ahead]  = "⬆ "
chain:glyph[git-staged] = "✔ "

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:

edit:prompt-stale-transform = { each [x]{ styled $x[text] "gray" } }

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

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 narrow-based 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:location:matcher = [@a]{ edit:location:match-dir-pattern &ignore-case $@a }
edit:insert:binding[Ctrl-R] = {
  edit:histlist:start
  edit:histlist:toggle-case-sensitivity
}

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

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"

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

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)