GNU Emacs integrated computing environment

My comprehensive "dotemacs" (.emacs) for GNU/Linux

  • Created: 2019-08-15
  • Updated: 2020-10-21 20:21 +0300.

See this file's upstream git history. Everything is part of my dotfiles' repository.

Table of Contents

1 Overview

1.1 Canonical links to this document

1.2 What is this

The present document, referred to in the source code version as emacs-init.org, contains the bulk of my configurations for GNU Emacs. It is designed using principles of "literate programming": a combination of ordinary language and inline code blocks. Emacs knows how to parse this file properly so as to evaluate only the Elisp ("Emacs Lisp") included herein. The rest is for humans to make sense of my additions and their underlying rationale.

Literate programming allows us to be more expressive and deliberate. Not only can we use typography to its maximum potential, but may also employ techniques such as internal links between sections. This makes the final product much more useful for end users than a terse script.

Each section provides information about the code it contains. In case you feel something is missing, I maintain a Frequently Asked Questions section (when in doubt, or to offer feedback, suggestions, further comments, etc., do contact me).

In more practical terms, this document is written using org-mode. It contains all package configurations for my Emacs setup. To actually work, it needs to be initialised from another file that only covers the absolute essentials.

1.2.1 Contents of my init.el

The emacs-init.org is actually loaded from an other file, named init.el as per the Emacs convention. Mine is designed to add the community-driven MELPA archive to the list of package repositories, configure use-package (see comprehensive explanation below) and then load the file with my configurations (i.e. the present document).

1.2.1.1 Old version prior to Emacs 27

For reference, these were the contents of my init.el prior to Emacs 27.1.

(require 'package)
(setq package-enable-at-startup nil)
(add-to-list 'package-archives
             '("melpa" . "https://melpa.org/packages/"))
(unless package--initialized (package-initialize))

(require 'org)
(org-babel-load-file (expand-file-name "~/.emacs.d/emacs-init.org"))
1.2.1.2 Current version for Emacs 27+

Whereas in Emacs 27.1 and onward, they are modified as follows. For the let* part, see section: Always rebuild init and refresh package cache.

(require 'package)

(add-to-list 'package-archives
             '("melpa" . "https://melpa.org/packages/"))

;; Make sure `use-package' is available.
(unless (package-installed-p 'use-package)
  (package-refresh-contents)
  (package-install 'use-package))

;; Configure `use-package' prior to loading it.
(eval-and-compile
  (setq use-package-always-ensure nil)
  (setq use-package-always-defer nil)
  (setq use-package-always-demand nil)
  (setq use-package-expand-minimally nil)
  (setq use-package-enable-imenu-support t)
  (setq use-package-compute-statistics nil)
  ;; The following is VERY IMPORTANT.  Write hooks using their real name
  ;; instead of a shorter version: after-init ==> `after-init-hook'.
  ;;
  ;; This is to empower help commands with their contextual awareness,
  ;; such as `describe-symbol'.
  (setq use-package-hook-name-suffix nil))

(eval-when-compile
  (require 'use-package))

(use-package vc
  :config
  (setq vc-follow-symlinks t)) ; Because my dotfiles are managed that way

;; For my custom libraries
(add-to-list 'load-path "~/.emacs.d/lisp/")

;; I create an "el" version of my Org configuration file as a final step
;; before closing down Emacs.  This is done to load the latest version
;; of my code upon startup.
;;
;; Also helps with initialisation times.  Not that I care too much about
;; those… Hence why I no longer bother with deferring package loading
;; either by default or on a case-by-case basis.
(let* ((conf "~/.emacs.d/emacs-init")
       (el (concat conf ".el"))
       (org (concat conf ".org")))
  (if (file-exists-p el)
      (load-file el)
    (use-package org)
    (org-babel-load-file org)))
1.2.1.3 The "early init" for Emacs 27+

Starting with Emacs 27.1, an early-init.el is now required to control things with greater precision. Its code is as follows:

;; Initialise installed packages
(setq package-enable-at-startup t)

;; Allow loading from the package cache.
(setq package-quickstart t)

;; Do not resize the frame at this early stage.
(setq frame-inhibit-implied-resize t)

These adjustments are of paramount importance due to changes in the way Emacs initialises the package manager. Prior to Emacs 27.1, the init.el was supposed to handle that task by means of calling package-initialize. Whereas for Emacs 27.1, the default behaviour is to start the package manager before loading the user's init file.

1.2.2 About `use-package'

This is a tool that streamlines the configuration of packages. It handles everything from assigning key bindings, setting the value of customisation options, writing hooks, declaring a package as a dependency for another, and so on.

Though it might not be readily apparent, a "package" in Emacs parlance is any elisp file that is evaluated by Emacs. This includes libraries that are shipped with the upstream distribution as well as code that comes from other sources.

As such use-package is not a package manager, in the sense of installing, removing, listing packages. It only configures things using a declarative syntax. The package manager of Emacs is package.el while there are other tools available from third parties, such as straight.el.

Unlike a typical extensible program, there is no real distinction between native Emacs code and the one that comes from third parties. There is no externally facing limited set of features that other tools can plug into. Emacs is an interpreter of lisp (Emacs Lisp), meaning that any elisp is evaluated in real time, making Emacs behave in accordance with it.

I have an hour long presentation about switching to Emacs, where this and other topics are discussed in greater detail. It is good to understand the context in order to appreciate the differences between the various use-package declarations documented herein.

The three types of use-package declarations that I use:

  1. To set up external packages. Those are denoted by the inclusion of :ensure which means that the package manager should make sure the package is installed.
  2. To configure default packages. No :ensure is needed for them.
  3. To declare custom or otherwise experimental packages that are not available in any repository and which I handle manually and plan to review at a later date. Those include a :load-path that makes their code available to my environment.

In several package declarations you will see a :diminish keyword that leverages the diminish package. This affects the so-called "lighter" that each package may define. The lighter is the piece of text that a tool will append to the mode line. For example, Flyspell's lighter is "Fly". With :diminish we demand that the lighter be removed (the information is still available when running C-h m).

With use-package we can improve the start-up performance of Emacs in a few fairly simple ways. Whenever a command is bound to a key it is configured to be loaded only once invoked. Otherwise we can specify which functions should be autoloaded by means of the :commands keyword.

Furthermore, and if absolutely necessary, I define all variables that are supposed to be immutable with the :custom keyword. This writes them to the custom.el that I specify further below. You should, however, consider that to be the exception, as all minor modes, custom functions, or other configurations are normally specified under the :config keyword. The activation of a mode should always be the very last thing, once all variables have been set. Make sure to read the manual for more information on the individual keywords.

I set the variable use-package-hook-name-suffix to nil in order to always type in the proper name of a hook. The default behaviour is to omit the suffix for convenience. But that means that we can no longer benefit from the contextual awareness of help/documentation commands (e.g. C-h o over any of the symbols/functions/variables below will put the thing at point as the first completion option).

Last but not least, you should be warned of a common error with handling package installs (with or without use-package): if Emacs complains that the package you want no longer exists, it means that you must refresh your package index because there is a new version of that package, so the old one that is still registered on your list has been removed from the source. Do that with either M-x package-refresh-contents or the package browser M-x list-packages.

The following snippet of elisp sets up and configures use-package to my liking. It is already referenced in the previous section concerning the contents of my init.el. This is due to changes in how Emacs 27.1 starts up. Whereas before I used to configure use-package from inside this document.

;; Make sure `use-package' is available.
(unless (package-installed-p 'use-package)
  (package-refresh-contents)
  (package-install 'use-package))

;; Configure `use-package' prior to loading it.
(eval-and-compile
  (setq use-package-always-ensure nil)
  (setq use-package-always-defer nil)
  (setq use-package-always-demand nil)
  (setq use-package-expand-minimally nil)
  (setq use-package-enable-imenu-support t)
  ;; The following is VERY IMPORTANT.  Write hooks using their real name
  ;; instead of a shorter version: after-init ==> `after-init-hook'.
  ;;
  ;; This is to empower help commands with their contextual awareness,
  ;; such as `describe-symbol'.
  (setq use-package-hook-name-suffix nil))

(eval-when-compile
  (require 'use-package))

Settings that do not have a corresponding package are declared using the special use-package emacs notation.

1.2.3 About the source code version of this document

In the org-mode version of this document, I make sure that the above-referenced code blocks are not declared as an emacs-lisp source but rather as mere examples, so they are not accidentally parsed by the actual setup.

Actual code blocks are wrapped between #+begin_src and #+end_src tags (not visible in the website version of this page). For Emacs 27.1, such templates can be quickly inserted with C-c C-, (this works both for empty blocks and active regions). For more on the matter, refer to Org's section further below.

As for the various settings included herein, you can learn even more about them by using Emacs' built-in documentation (great for discovering new features and pieces of functionality).

Additionally, you will notice some metadata tags specific to org-mode below each heading. These are generated by the functions that are defined in the package declaration for org-id. The idea is to keep anchor tags consistent when generating a new HTML version of this document.

This metadata also makes it possible to create immutable internal links, whenever a reference is needed. To create such links, you can use C-c l to capture the unique ID of the current section and then C-c C-l to create a link (the former is defined in the Org package declaration—this is an internal link in action).

1.3 COPYING

Copyright (c) 2019-2020 Protesilaos Stavrou <info@protesilaos.com>

This file is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This file is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this file. If not, see http://www.gnu.org/licenses/.

2 Base settings

This section contains the relatively few configurations that are needed prior to the setup of everything else.

2.1 Disable GUI components

Overview of these settings:

  • I normally use the GTK (GUI) variant of Emacs. I prefer not to have any of the elements that come with it. This keeps the window clean. The only "interface" component that remains in place is the mode line, which is not part of the GUI toolkit anyway…
  • The start-up screen that offers an overview of GNU Emacs is also disabled. It is useful for beginners, but is rendered obsolete once you familiarise yourself with the essentials.
  • The pair of key bindings that involve z minimise the Emacs frame. This is technically an interface action, in that it assumes my window manager has a minimise function or that I want to have such a motion inside of Emacs. Disable them.
  • Also disable the "hello file" function. No use for it and would hate to call it by accident.
(use-package emacs
  :init
  (menu-bar-mode -1)
  (tool-bar-mode -1)
  (scroll-bar-mode -1)
  :config
  (setq use-file-dialog nil)
  (setq use-dialog-box t)               ; only for mouse events
  (setq inhibit-splash-screen t)
  :bind (("C-z" . nil)
         ("C-x C-z" . nil)
         ("C-h h" . nil)))

2.2 Always rebuild init and refresh package cache

When Emacs expands this org-mode file into the actual elisp code, it creates a new document: emacs-init.org derives emacs-init.el. The latter holds my customisations in the state they were in at the time the document was created. Any updates require a rewrite.

To make sure that I do not load older settings after having made some tweaks to my dotemacs, I want to delete that derived file when I instruct Emacs to terminate its process. This ensures that edits I made to emacs-init.org are parsed into a new emacs-init.el at the next startup. Doing so at the kill-emacs-hook is a neat trick to roll over the cost of building this massive document. We now pay the few second penalty when terminating the Emacs session. Otherwise we would be facing delays at the initialisation stage.

Note that choosing to load the right file is the task of my init.el. Make sure to either browse the source code of my dotfiles, of which the present document is a part of, or read the section on my init.el.

Similarly, I want to maintain an up-to-date cache with the installed packages, which is used to speed up boot times, ceteris paribus. The "quickstart" method is part of Emacs 27 or higher.

(use-package emacs
  :config
  (defun prot/rebuild-emacs-init ()
    "Produce Elisp init from my Org dotemacs.
Add this to `kill-emacs-hook', to use the newest file in the next
session.  The idea is to reduce startup time, though just by
rolling it over to the end of a session rather than the beginning
of it."
    (let ((init-el "~/.emacs.d/emacs-init.el")
          (init-org "~/.emacs.d/emacs-init.org"))
      (when (file-exists-p init-el)
        (delete-file init-el))
      (org-babel-tangle-file init-org init-el)))
  :hook ((kill-emacs-hook . prot/rebuild-emacs-init)
         (kill-emacs-hook . package-quickstart-refresh)))

2.3 Remove modeline "lighters"

As was noted in the section about `use-package' we can remove the so-called "lighter" text that modes append to the mode line. This is all that Diminish does and you will see a :diminish keyword in the relevant packages.

If you actually want to edit the lighters, check the Delight package.

(use-package diminish
  :ensure
  :after use-package)

2.4 Put customisation settings in a "custom.el"

When you install a package or use the various customisation interfaces to tweak things to your liking, Emacs will append a piece of Elisp to your init file. In my experience, this is a common source of inconsistencies, arising from a conflict between the user's code and what is stored in that added custom snippet.

As such, I prefer to have all "custom" code stored in a separate, disposable file that I do not keep under version control. When something does not seem to work as intended it is easy to edit ~/.emacs.d/custom.el or outright delete it.

(use-package cus-edit
  :config
  (defvar prot/custom-file "~/.emacs.d/custom.el")

  (setq custom-file prot/custom-file)

  (defun prot/cus-edit ()
    (let ((file prot/custom-file))
      (unless (file-exists-p file)
        (make-empty-file file))
      (load-file file)))
  :hook (after-init-hook . prot/cus-edit))

2.5 Fundamentals for Hydras

A "hydra" is an ad-hoc mode for mapping commands to a group of more convenient key bindings. In an active hydra, all of its keys—the hydra's "heads"—can be typed in any order, keeping the mode active in the process (if wanted), while at least one key performs the function of exiting the hydra.

In practice, I do not use hydras as substitutes for mode maps. Instead, I treat them as complementary to minor modes that offer enhancements to a particular major mode but are otherwise not very useful outside that niche. Think of cases such as a code linter (the minor mode) as part of a programming session (major mode).

The reason I use hydras this way is due to the official key binding conventions that I [generally] follow (as documented in the Emacs manual). The idea of using C-c [a-z] for all these minor modes is good only if you have a few of them and only if they are more useful outside a particular task. In other words, it does not scale.

I thus employ C-c h as a common prefix for all my hydras (knowing that it is meant for private use), binding each sequence to the relevant minor mode's map. An extra key completes the chord following mnemonics. For instance, a hydra about the linter when that is active is C-c h l.

Look for the defhydra macro in the relevant package declarations (these will evolve over time). Hydras are colour-coded as a means of denoting their behaviour with regard to the use of non-defined keys and the criteria for exiting.

Colour Hydra heads Other keys
red Accept and Continue Accept and Exit
pink Accept and Continue Accept and Continue
amaranth Accept and Continue Reject and Continue
teal Exit Reject and Continue
blue Exit Accept and Exit

Also check the project's source for more information.

(use-package hydra :ensure)

2.6 Modus themes (my highly accessible themes)

This is a project I started as soon as I switched to Emacs in July 2019. Fast-forward to a year later and the themes are now part of the upstream Emacs distribution, available from the master branch. Note that this is not the latest stable release of Emacs, 27.1, but only the current development branch that will eventually become version 28.1. I benefitted a lot from community contributions, of which I am most thankful of, as discussed in My Modus themes are now shipped with Emacs (2020-08-27).

The Modus themes are designed to conform with the highest accessibility standard for colour contrast between foreground and background values. This stands for a minimum contrast ratio of 7:1, also known as the WCAG AAA standard. In simpler terms, they are good for readability.

The themes are "Modus Operandi" (light theme) and "Modus Vivendi" (dark). The source code is available on their GitLab page while you can read the HTML version of their documentation on my website. The list of supported packages is comprehensive. There also are lots of customisation options to tweak the looks of the themes.

The two themes are distributed as standalone packages under the terms of the GNU General Public License. Besides upstream Emacs, you can find them in the official GNU ELPA repository (latest tagged release), MELPA (latest commit to my master branch), MELPA Stable (latest tag). Each theme is its own package because I know that people tend to use one or the other. And also due to the fact that one is not a prerequisite for the other. I personally use both, switching between them on-the-fly by calling prot/modus-themes-toggle.

Note though that because I am using these themes locally as part of their development process, I am not configuring here the aforementioned archive or Emacs' built-in versions. What you see in this use-package declaration is a bit different than what others will be using.

Also note that the values I set for these variables are not indicative of my preferences: I try different combinations to test things across a range of scenaria.

The contrib/format-sexp and the subsequent dolist were copied and adapted from this comment on r/emacs.

Lastly, if you are curious about the underlying methodology, read my essay on the design of the Modus themes (2020-03-17).

And here are some more resources from my website for those who are really into the minutia and wish to get a glimpse of how much work goes into this project:

(use-package emacs
  :config
  (setq custom-safe-themes t)           ; Due to my dev needs

  (defmacro contrib/format-sexp (sexp &rest objects)
    `(eval (read (format ,(format "%S" sexp) ,@objects))))

  ;; This is currently not used in this section.  Search for it in the
  ;; section about setting fonts, `prot/font-bold-face' in particular.
  (defvar prot/modus-theme-after-load-hook nil
    "Hook that runs after loading a Modus theme.
See `prot/modus-operandi' or `prot/modus-vivendi'.")

  ;; The variables do not reveal my preferences.  Always testing things.
  (dolist (theme '("operandi" "vivendi"))
    (contrib/format-sexp
     (defun prot/modus-%1$s ()
       (setq modus-%1$s-theme-slanted-constructs nil
             modus-%1$s-theme-bold-constructs nil
             modus-%1$s-theme-fringes 'subtle ; {nil,'subtle,'intense}
             modus-%1$s-theme-mode-line nil ; {nil,'3d,'moody}
             modus-%1$s-theme-faint-syntax nil
             modus-%1$s-theme-intense-hl-line nil
             modus-%1$s-theme-intense-paren-match nil
             modus-%1$s-theme-links 'neutral-underline ; {nil,'faint,'neutral-underline,'faint-neutral-underline,'no-underline}
             modus-%1$s-theme-comments 'green ; {nil,'green,'yellow}
             modus-%1$s-theme-no-mixed-fonts nil
             modus-%1$s-theme-prompts 'subtle ; {nil,'subtle,'intense}
             modus-%1$s-theme-completions 'moderate ; {nil,'moderate,'opinionated}
             modus-%1$s-theme-diffs nil ; {nil,'desaturated,'fg-only}
             modus-%1$s-theme-org-blocks nil ; {nil,'grayscale,'rainbow}
             modus-%1$s-theme-headings  ; Read the manual for this one
             '((1 . section)
               (2 . section-no-bold)
               (3 . line-no-bold)
               (t . rainbow-no-bold))
             modus-%1$s-theme-variable-pitch-headings t
             modus-%1$s-theme-scale-headings t
             modus-%1$s-theme-scale-1 1.1
             modus-%1$s-theme-scale-2 1.15
             modus-%1$s-theme-scale-3 1.21
             modus-%1$s-theme-scale-4 1.27
             modus-%1$s-theme-scale-5 1.33)
       (load-theme 'modus-%1$s t)
       (run-hooks 'prot/modus-theme-after-load-hook))
     theme))

  (define-minor-mode prot/modus-themes-alt-mode
    "Override specific palette variables with custom values.

This is intended as a proof-of-concept.  It is, nonetheless, a
perfectly accessible alternative, conforming with the design
principles of the Modus themes.  It still is not as good as the
default colours."
    :init-value nil
    :global t
    (if prot/modus-themes-alt-mode
        (setq modus-operandi-theme-override-colors-alist
              '(("bg-main" . "#fefcf4")
                ("bg-dim" . "#faf6ef")
                ("bg-alt" . "#f7efe5")
                ("bg-hl-line" . "#f4f0e3")
                ("bg-active" . "#e8dfd1")
                ("bg-inactive" . "#f6ece5")
                ("bg-region" . "#c6bab1")
                ("bg-header" . "#ede3e0")
                ("bg-tab-bar" . "#dcd3d3")
                ("bg-tab-active" . "#fdf6eb")
                ("bg-tab-inactive" . "#c8bab8")
                ("fg-unfocused" . "#55556f"))
              modus-vivendi-theme-override-colors-alist
              '(("bg-main" . "#100b17")
                ("bg-dim" . "#161129")
                ("bg-alt" . "#181732")
                ("bg-hl-line" . "#191628")
                ("bg-active" . "#282e46")
                ("bg-inactive" . "#1a1e39")
                ("bg-region" . "#393a53")
                ("bg-header" . "#202037")
                ("bg-tab-bar" . "#262b41")
                ("bg-tab-active" . "#120f18")
                ("bg-tab-inactive" . "#3a3a5a")
                ("fg-unfocused" . "#9a9aab")))
      (setq modus-operandi-theme-override-colors-alist nil
            modus-vivendi-theme-override-colors-alist nil)))

  (defun prot/modus-themes-toggle (&optional arg)
    "Toggle between `prot/modus-operandi' and `prot/modus-vivendi'.

With optional \\[universal-argument] prefix, enable
`prot/modus-themes-alt-mode' for the loaded theme."
    (interactive "P")
    (if arg
        (prot/modus-themes-alt-mode 1)
      (prot/modus-themes-alt-mode -1))
    (if (eq (car custom-enabled-themes) 'modus-operandi)
        (progn
          (disable-theme 'modus-operandi)
          (prot/modus-vivendi))
      (disable-theme 'modus-vivendi)
      (prot/modus-operandi)))

  :hook (after-init-hook . prot/modus-operandi)
  :bind ("<f5>" . prot/modus-themes-toggle))

2.6.1 Measuring relative colour luminance

The following is an Elisp implementation of the formula that determines whether a combination of two colours is accessible or not. It was offered to me by Omar Antolín Camarena. For the purposes of my themes, the contrast ratio needs to be 7:1 or higher, which conforms with the highest accessibility standard (WCAG AAA).

To see it in action, you can read the org-mode report on the minor review of Modus Operandi palette (2020-05-10). Which also goes to show how tricky the selection of colours can be.

(use-package emacs
  :commands (wcag clr)
  :config
  (defun wcag (hex)
    (apply #'+
           (cl-mapcar
            (lambda (k x)
              (* k (if (<= x 0.03928)
                       (/ x 12.92)
                     (expt (/ (+ x 0.055) 1.055) 2.4))))
            '(0.2126 0.7152 0.0722)
            (color-name-to-rgb hex))))

  (defun clr (c1 c2)
    (let ((ct (/ (+ (wcag c1) 0.05)
                 (+ (wcag c2) 0.05))))
      (max ct (/ ct)))))

2.6.2 Rainbow mode for colour testing

The following package reads a colour value, such as hexadecimal RGB, and sets the background for the value in that colour. Quite useful when reviewing my themes (rainbow-mode is activated manually).

(use-package rainbow-mode
  :ensure
  :diminish
  :commands rainbow-mode
  :config
  (setq rainbow-ansi-colors nil)
  (setq rainbow-x-colors nil))

2.7 Typeface configurations

Any font I choose must support Latin and Greek character sets, be readable at both small and large sizes, preferably offer roman and italic variants with corresponding bold weights, not be too thin, not have too short of an x-height, not be too wide, not have a name that directly advertises some brand, not try to call too much attention to its details, be equally readable against light and dark backdrops, and use the *.ttf spec which yields the best results on GNU/Linux.

While there are many good free/libre options available, only a handful of them cope well with my fairly demanding needs. Some look good at large point sizes. Others lack Greek characters. While a few of them are virtually unreadable when cast on a light background (bitmap fonts in particular). The section on Primary and complementary font settings defines typefaces that I consider suitable to my needs.

Further below in this section I also specify certain settings that govern the Mode for proportional fonts and link to other relevant parts of this document, while my Modus themes are designed to cope well with mixed-font scenaria (particularly useful in Org).

Lastly, note that on a modern GNU/Linux system that uses the fontconfig library, per-user fonts are stored in ~/.local/share/fonts.

2.7.1 Primary and complementary font settings

Moving on to my configurations, prot/font-set-fonts is a command that lets me switch font sizes through a completion interface (more on that in Completion framework and extras). When the function is executed non-interactively, it can be given an arbitrary font size as well as family names for the {mono,proportionately}-spaced typefaces.

While there are many ways to define a baseline or fallback font family in Emacs, I find that the most consistent one in terms of overall configuration is to do it at the "face" level. For the main typeface, we configure the default face. Then we have variable-pitch for proportionately spaced fonts and fixed-pitch for monospaced ones.

In Emacs parlance, "face" signifies a construct that groups together several display attributes, such as a foreground and a background colour, as well as all typography-related values (Emacs themes are programs that configure those faces).

To understand the syntax used here, do C-h f set-face-attribute. In essence, by changing the default face we are specifying the family that should be used in case no other applies for the given construct. This is actually a good idea because there are many scenaria where you want a face to retain its own attributes (e.g. let org-mode inline code be presented in its monospaced font while using a variable width typeface for the main text—see Mode for proportional fonts).

Relevant blog posts of mine:

(use-package emacs
  :defines (prot/font-set-fonts-hook
            prot/font-monospaced-fonts-list
            prot/font-fonts-line-spacing-alist
            prot/font-fonts-bold-weight-alist)
  :functions (prot/font-adjustment
              prot/font-line-spacing
              prot/font-bold-face
              prot/font-fonts-per-monitor)
  :commands (prot/font-set-face-attribute
             prot/font-set-fonts
             prot/font-set-font-size-family
             prot/font-fonts-dwim)
  :config
  (setq x-underline-at-descent-line t)

  (defvar prot/font-set-fonts-hook nil
    "Hook that is called after setting fonts.
See, for example, `prot/font-set-fonts'.")

  ;; NOTE: "Hack" and "Iosevka Comfy" are my personal builds of Hack and
  ;; Iosevka respectively:
  ;;
  ;; 1. https://gitlab.com/protesilaos/hack-font-mod
  ;; 2. https://gitlab.com/protesilaos/iosevka-comfy
  (defconst prot/font-sizes-families-alist
    '(("pocket" . (105 "Hack" "DejaVu Sans"))
      ("laptop" . (110 "Hack" "Inter"))
      ("desktop" . (130 "Hack" "Inter"))
      ("reader" . (160 "Iosevka Comfy" "FiraGO"))
      ("presentation" . (190 "Iosevka Comfy" "FiraGO")))
    "Alist of desired typefaces and their point sizes.

Each association consists of a display type mapped to a point
size, followed by monospaced and proportionately spaced font
names.  The monospaced typeface is meant to be applied to the
`default' and `fixed-pitch' faces.  The proportionately spaced
font is intended for the `variable-pitch' face.")

  (defun prot/font-set-face-attribute (face family)
    "Set FACE font to FAMILY."
    (set-face-attribute `,face nil :family (format "%s" family)))

  (defun prot/font-set-fonts (&optional points font-mono font-var)
    "Set default font size using presets.

POINTS is the font's height.  FONT-MONO should be a monospaced
typeface, due to the alignment requirements of the `fixed-pitch'
face.  FONT-VAR could be a proportionately spaced typeface or
even a monospaced one, since the `variable-pitch' it applies to
is not supposed to be spacing-sensitive.  Both families must be
represented as a string holding the family's name."
    (interactive)
    (if window-system
        (let* ((data prot/font-sizes-families-alist)
               (displays (mapcar #'car data))
               (choice (or points
                           (completing-read "Pick display size: " displays nil t)))
               (size (or points
                         (nth 1 (assoc `,choice data))))
               (mono (or font-mono
                         (if (member choice displays)
                             (nth 2 (assoc `,choice data))
                           nil)))
               (var (or font-var
                        (if (member choice displays)
                            (nth 3 (assoc `,choice data))
                          nil)))
               (adjust (nth 4 (assoc `,choice data))))
          (set-face-attribute 'default nil :family mono :height size)
          (prot/font-set-face-attribute 'fixed-pitch mono)
          (prot/font-set-face-attribute 'variable-pitch var)
          (run-hooks 'prot/font-set-fonts-hook))
      (user-error "Not running a graphical Emacs.  Cannot set fonts.")))

  (defvar prot/font-monospaced-fonts-list
    '("Iosevka Comfy" "Hack" "Source Code Pro"
      "Ubuntu Mono" "Fantasque Sans Mono")
    "List of typefaces for coding.
See `prot/font-set-font-size-family' for how this is used
code-wise.")

  (defun prot/font-set-font-size-family ()
    "Set point size and main typeface.
This command is intended for testing various font families at
some common point sizes.

See `prot/font-set-fonts' for the function I would normally use
or `prot/font-fonts-dwim' which just wraps this one with that."
    (interactive)
    (if window-system
        (let* ((fonts prot/font-monospaced-fonts-list)
               (font (completing-read "Select main font: " fonts nil t))
               (nums (list 130 140 150 160))
               (sizes (mapcar 'number-to-string nums))
               (size (completing-read "Select or insert number: " sizes nil))
               (var (face-attribute 'variable-pitch :family)))
          (set-face-attribute 'default nil :family font :height (string-to-number size))
          (set-face-attribute 'fixed-pitch nil :family font)
          (prot/font-set-face-attribute 'variable-pitch var)
          (run-hooks 'prot/font-set-fonts-hook))
      (user-error "Not running a graphical Emacs.  Cannot set fonts.")))

  (defun prot/font-fonts-dwim (&optional arg)
    "Set fonts interactively.
This is just a wrapper around `prot/font-set-fonts' and
`prot/font-set-font-size-family', whose sole purpose is to
economise on dedicated key bindings."
    (interactive "P")
    (if arg
        (prot/font-set-font-size-family)
      (prot/font-set-fonts)))

  (defvar prot/font-fonts-line-spacing-alist
    '(("Source Code Pro" . 1)
      ("Ubuntu Mono" . 2))
    "Font families in need of extra `line-spacing'.
See `prot/font-line-spacing' for how this is used.")

  (defvar prot/font-fonts-bold-weight-alist
    '(("Source Code Pro" . semibold))
    "Font families in need of a different weight for `bold'.
See `prot/font-bold-face' for how this is used.")

  (defmacro prot/font-adjustment (fn doc alist cond1 cond2)
    "Macro for functions that employ `prot/font-switch-fonts-hook'.
NAME is the name of the resulting function.  DOC is its
docstring.  ALIST is an assosiation list of cons cells.  COND1
and COND2 is the body of an `if' statement's 'if' and 'then' part
respectively."
    `(defun ,fn ()
       ,doc
       (let* ((data ,alist)
              (fonts (mapcar #'car data))
              (font (face-attribute 'default :family))
              (x (cdr (assoc font data))))
         (if (member font fonts)
             ,cond1
           ,cond2))))

  (prot/font-adjustment
   prot/font-line-spacing
   "Determine desirable `line-spacing', based on font family."
   prot/font-fonts-line-spacing-alist
   (setq-default line-spacing `,x)
   (setq-default line-spacing nil))

  ;; XXX: This will not work with every theme, but only those that
  ;; inherit the `bold' face instead of specifying a weight property.
  ;; The intent is to configure this once and have it propagate wherever
  ;; a heavier weight is displayed.  My Modus themes handle this
  ;; properly.
  (prot/font-adjustment
   prot/font-bold-face
   "Determine weight for the `bold' face, based on font family."
   prot/font-fonts-bold-weight-alist
   (set-face-attribute 'bold nil :weight `,x)
   (set-face-attribute 'bold nil :weight 'bold))

  (defun prot/font-fonts-per-monitor ()
    "Use font settings based on screen size.
Meant to be used at some early initialisation stage, such as with
`after-init-hook'."
    (when window-system
      (let* ((display (if (<= (display-pixel-width) 1366)
                          "laptop"
                        "desktop"))
             (data prot/font-sizes-families-alist)
             (size (cadr (assoc `,display data)))
             (mono (nth 2 (assoc `,display data)))
             (var (nth 3 (assoc `,display data))))
        (set-face-attribute 'default nil :family mono :height size)
        (prot/font-set-face-attribute 'fixed-pitch mono)
        (prot/font-set-face-attribute 'variable-pitch var))
      (run-hooks 'prot/font-switch-fonts-hook)))

  :hook ((after-init-hook . prot/font-fonts-per-monitor)
         (prot/font-set-fonts-hook . prot/font-line-spacing)
         (prot/font-set-fonts-hook . prot/font-bold-face)
         ;; See theme section for this hook
         (prot/modus-theme-after-load-hook . prot/font-bold-face))
  ;; Awkward key because I do not need it very often.  Maybe once a day.
  ;; The "C-c f" is used elsewhere.
  :bind ("C-c F" . prot/font-fonts-dwim))

2.7.2 Mode for proportional fonts

When Emacs operates in a graphical terminal, it can display text using mixed font settings: a variety of heights and families (in addition to other typegraphic attributes). This means that it can draw text on the same line that has potentially profoundly different properties character-by-character. For example, a sans-serif typeface for prose with a monospaced font for inline code (for the underlying faces that may be used, see Primary and complementary font settings).

While we can handle things on our own by calling set-face-attribute for each face we are interested in, there is a built-in mechanism to quickly toggle the use of proportionately spaced fonts (in contrast to the monospaced ones which usually are the standard). Depending on your theme this will not yield good results in Org mode, so be warned.

For what it's worth, my Modus themes are designed to take full advantage of this facility (see Modus themes and other visuals). Any face that is sensitive to spacing or that would break the layout if displayed in a variable width is designed to explicitly inherit from the fixed-pitch face (the exact font family was specified in the previous section, though the only thing that matters is to be monospaced).

With the following package declaration my idea is to integrate prot/variable-pitch-mode into some "reading mode" configurations that I have, mutatis mutandis. Likely candidates are the toggle for olivetti-mode (see the section on “Focus mode” for writing) and my simple-yet-super-effective plain text presentations with Org mode (see simple presentations inside of Emacs).

Also check my Cursor appearance and tweaks.

(use-package face-remap
  :diminish buffer-face-mode            ; the actual mode
  :commands prot/variable-pitch-mode
  :config
  (define-minor-mode prot/variable-pitch-mode
    "Toggle `variable-pitch-mode', except for `prog-mode'."
    :init-value nil
    :global nil
    (if prot/variable-pitch-mode
        (unless (derived-mode-p 'prog-mode)
          (variable-pitch-mode 1))
      (variable-pitch-mode -1))))

2.7.3 Simple font suitability test

Here is a test I have come up with to make an initial assessment of the overall quality of a monospaced font that is meant to work well in a programming context: can you discern each character at a quick glance? If yes, your choice of typeface is good prima facie, otherwise search for something else.

Note that this test is not perfect, since many typefaces fall short in less obvious ways, such as the space between the characters. Also note that the website version of this document may not accurately represent the typeface I am using.

()[]{}<>«»‹›
6bB8&
0ODdoaoOQGC
I1tilIJL|
!¡ij
5$§SsS5
17ZzZ2
9gqpG6
hnmMN
uvvwWuuwvy
x×X
.,·°%
¡!¿?
:;
`''"‘’“”
—-~≈=≠+*_
…⋯
...

Sample character set
Check for monospacing and Greek glyphs

ABCDEFGHIJKLMNOPQRSTUVWXYZ
abcdefghijklmnopqrstuvwxyz
0123456789
~!@#$%^&*+
`'"‘’“”.,;:…
()[]{}—-_=|<>/\
ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ
αβγδεζηθικλμνξοπρστυφχψω

// NOTE that I got this from Hack's website:
// https://source-foundry.github.io/Hack/font-specimen.html
//  The four boxing wizards jump
#include <stdio.h> // <= quickly.
int main(int argc, char **argv) {
  long il1[]={1-2/3.4,5+6==7/8};
  int OxFaced=0xBAD||"[{(CQUINE";
  unsigned O0,l1,Z2,S5,G6,B8__XY;
  printf("@$Hamburgefo%c`",'\n');
  return ~7&8^9?0:l1|!"j->k+=*w";
}

2.7.4 Bidirectional writing and so-long.el

I only ever write/read in Latin and Greek alphabets. So, while I appreciate the fact that Emacs can natively handle other scripts, I have no use for that particular feature. Setting the default directionality to what my languages use can help improve the responsiveness of Emacs in some cases.

Consistent performance is the reason to also enable global-so-long-mode, built into Emacs versions >= 27, which allows the active major mode to gracefully adapt to buffers with very long lines. What "very long" means is, of course, configurable: M-x find-library so-long covers several customisation options, though I find that the defaults require no further intervention from my part.

The code below is a minor adaptation of the insights of Alain M. Lafon in the Comprehensive guide on handling long lines in Emacs (2020-09-29).

(use-package emacs
  :config
  (setq-default bidi-paragraph-direction 'left-to-right)
  (setq bidi-inhibit-bpa t))

(use-package so-long
  :config
  (global-so-long-mode 1))

3 Selection candidates and search methods

3.1 Completion framework and extras

As discussed in my video about Emacs' buffer and window management, the optimal way of using Emacs is through searching and narrowing selection candidates. Spend less time worrying about where things are on the screen and more on how fast you can bring them into focus. This is, of course, a matter of realigning priorities, as we still wish to control every aspect of the interface.

3.1.1 Tools for completion candidate listing

This section contains two main sub-sections covering the components that pertain to the user-facing features of Emacs' built-in completion framework. Those are (1) the minibuffer and (2) Icomplete. They can work in tandem as part of a singular experience.

The minibuffer is the locus of all extended interactions. Whether it is about offering input to a prompt or executing a function by its name, the minibuffer is always there. Simple and reliable.

Icomplete is the tool that visualises the list of candidates that may be presented at a completion prompt, as well as the incremental narrowing of such a list in reaction to user input.

For newcomers, watch my video demo of Icomplete (2020-02-26). This and subsequent sections contain lots of documentation and code. Make sure to study them carefully and also follow any links from here. Do contact me in case something is amiss. There is also an entry in the "Frequently Asked Questions" that might help you make sense of the complete picture: What about the completion UI? (typology of my completion interfaces).

3.1.1.1 Minibuffer configurations and extras

The code block below is specifically about the minibuffer setup. This does not cover the visualisation of completion candidates, which is handled by Icomplete—those are defined in the next section about Icomplete configurations and extras.

This is an overview of the settings covered herein:

Completion styles
The, dare I say, sublime “orderless” package is developed by Omar Antolín Camarena and you will find it on MELPA (also see the section on Icomplete vertical mode which configures another one of Omar's packages). It provides the orderless completion style for efficient out-of-order grouped pattern matching. The components can be determined using several styles, such as regexp, flex, prefix, initialism (check its README because there are lots of variations and extras). Delimiters are literal spaces by default, but can be configured to match other characters, with hyphens and slashes being likely choices. As such, Orderless can supersede—and improve upon—the completion styles that come built into Emacs, adding to them the powerful out-of-order capability.
  1. My prot/orderless-literal-dispatcher is based on an example from the Orderless README. By appending an equals sign to the end of a component I am instructing Orderless to match that component literally. Same idea for prot/orderless-initialism-dispatcher, which interprets the input as a strict initialism (i.e. each character matches the first letter in a series of words).
  2. The completion-styles try to match candidates using one style at a time (assuming you have more than one), moving from the first to the last until something is matched. In practice though, orderless is so powerful that there is no need to use the built-in styles. An exception is a niche functionality of the standard partial-completion style: with it you can navigate to a filesystem path like ~/.l/s/fo for ~/.local/share/fonts. So my recommendation is to use those two styles to cover every case.
Completions' buffer

The completions-format concerns the layout of the *Completions* buffer that pops up after trying to complete a non-unique match (or by typing ? from inside the minibuffer). By default, it can be focused directly with M-v while inside a minibuffer prompt, while I define commands to cycle between it and the minibuffer.

To enhance the experience of the Completions buffer, I define several keys that make motions easier and consistent with other read-only interfaces. The h key calls a command of mine to offer help (documentation) for the item at point, typically a function or a variable. i, j, and w call commands that operate on the symbol at point to (1) insert it in the most recently used buffer, (2) insert and exit all recursive minibuffers, and (3) add it to the kill-ring, while keeping the current minibuffer recursion in tact. I also define M-v to take me back to the minibuffer while inside the *Completions* and s-v to do it from anywhere else.

The window placement of the Completions, Help, and other buffers is defined in detail in the section about Window rules and basic tweaks, specifically within the display-buffer-alist.

Recursive minibuffers

I enable recursive minibuffers. This practically means that you can start something in the minibuffer, switch to another window, call the minibuffer again, run some commands, and then move back to what you initiated in the original minibuffer. To exit, hit C-] (abort-recursive-edit), though the regular C-g should also do the trick.

The minibuffer-depth-indicate-mode will show a depth indicator, represented as a number, next to the minibuffer prompt, if a recursive edit is in progress.

Key bindings

Note the nuances in the behaviour between RET and C-j. With the Return key, we instruct the minibuffer to expand the current candidate and then exit the session, if possible. Whereas C-j is meant to insert the minibuffer's contents exactly as they are and exit immediately. You need the latter in cases where you want foo but the match is for foobar.

The key bindings in the pattern of s-KEY follow the principles I outline in my note about the use of the Super key. They are included here because they related to minibuffer-centric actions.

Also check my configurations about the minibuffer history. After several months of full time usage, I am confident in the built-in mechanism's ability to sort things well enough and to surface the results I am most likely interested in, based on previous selections.

(use-package minibuffer
  :config
;;; General minibuffer configurations
  ;; Super-powerful completion style for out-of-order groups of matches
  ;; using a comprehensive set of matching styles.
  (use-package orderless
    :ensure
    :config
    ;;; NOTE: Not sure why I had that value before.  Keeping it around
    ;;; it case the one below proves problematic in some edge case…
    ;; (setq orderless-component-separator "[/\s_-]+")
    (setq orderless-component-separator " +")
    (setq orderless-matching-styles
          '(orderless-flex
            orderless-strict-leading-initialism
            orderless-regexp
            orderless-prefixes
            orderless-literal))

    (defun prot/orderless-literal-dispatcher (pattern _index _total)
      (when (string-suffix-p "=" pattern)
        `(orderless-literal . ,(substring pattern 0 -1))))

    (defun prot/orderless-initialism-dispatcher (pattern _index _total)
      (when (string-suffix-p "," pattern)
        `(orderless-strict-leading-initialism . ,(substring pattern 0 -1))))

    (setq orderless-style-dispatchers
          '(prot/orderless-literal-dispatcher
            prot/orderless-initialism-dispatcher))
    :bind (:map minibuffer-local-completion-map
                ("SPC" . nil)))       ; Space should never complete: use
                                      ; it for `orderless' groups.

  (setq completion-styles
        '(orderless partial-completion))
  (setq completion-category-defaults nil)
  (setq completion-cycle-threshold 3)
  (setq completion-flex-nospace nil)
  (setq completion-pcm-complete-word-inserts-delimiters t)
  (setq completion-pcm-word-delimiters "-_./:| ")
  (setq completion-show-help nil)
  (setq completion-ignore-case t)
  (setq read-buffer-completion-ignore-case t)
  (setq read-file-name-completion-ignore-case t)
  (setq completions-format 'vertical)   ; *Completions* buffer
  (setq enable-recursive-minibuffers t)
  (setq read-answer-short t)
  (setq resize-mini-windows t)
  (setq minibuffer-eldef-shorten-default t)

  (file-name-shadow-mode 1)
  (minibuffer-depth-indicate-mode 1)
  (minibuffer-electric-default-mode 1)

;;; General minibuffer functions
  (defun prot/focus-minibuffer ()
    "Focus the active minibuffer.

Bind this to `completion-list-mode-map' to M-v to easily jump
between the list of candidates present in the \\*Completions\\*
buffer and the minibuffer (because by default M-v switches to the
completions if invoked from inside the minibuffer."
    (interactive)
    (let ((mini (active-minibuffer-window)))
      (when mini
        (select-window mini))))

  (defun prot/focus-minibuffer-or-completions ()
    "Focus the active minibuffer or the \\*Completions\\*.

If both the minibuffer and the Completions are present, this
command will first move per invocation to the former, then the
latter, and then continue to switch between the two.

The continuous switch is essentially the same as running
`prot/focus-minibuffer' and `switch-to-completions' in
succession."
    (interactive)
    (let* ((mini (active-minibuffer-window))
           (completions (get-buffer-window "*Completions*")))
      (cond ((and mini
                  (not (minibufferp)))
             (select-window mini nil))
            ((and completions
                  (not (eq (selected-window)
                           completions)))
             (select-window completions nil)))))

;;; Completions' buffer actions
  ;; NOTE In practice I only use those while inspecting a long list
  ;; produced by C-h {f,o,v}.  To pop the Completions buffer, use
  ;; `minibuffer-completion-help', by default bound to ? from inside the
  ;; minibuffer.

  (defun prot/completions-kill-save-symbol ()
    "Add symbol-at-point to the kill ring.

Intended for use in the \\*Completions\\* buffer.  Bind this to a
key in `completion-list-mode-map'."
    (interactive)
    (kill-new (thing-at-point 'symbol)))

  (defmacro prot/completions-buffer-act (name doc &rest body)
    `(defun ,name ()
       ,doc
       (interactive)
       (let ((completions-window (get-buffer-window "*Completions*"))
             (completions-buffer (get-buffer "*Completions*"))
             (symbol (thing-at-point 'symbol)))
         (if (window-live-p completions-window)
             (with-current-buffer completions-buffer
               ,@body)
           (user-error "No live window with Completions")))))

  (prot/completions-buffer-act
   prot/completions-kill-symbol-at-point
   "Add \"Completions\" buffer symbol-at-point to the kill ring."
   (kill-new `,symbol)
   (message "Copied %s to kill-ring"
            (propertize `,symbol 'face 'success)))

  (prot/completions-buffer-act
   prot/completions-insert-symbol-at-point
   "Add \"Completions\" buffer symbol-at-point to active window."
   (let ((window (window-buffer (get-mru-window))))
     (with-current-buffer window
       (insert `,symbol)
       (message "Inserted %s"
                (propertize `,symbol 'face 'success)))))

  (prot/completions-buffer-act
   prot/completions-insert-symbol-at-point-exit
   "Like `prot/completions-insert-symbol-at-point' plus exit."
   (prot/completions-insert-symbol-at-point)
   (top-level))

;;; Miscellaneous functions and key bindings

  ;; Technically, this is not specific to the minibuffer, but I define
  ;; it here so that you can see how it is also used from inside the
  ;; "Completions" buffer
  (defun prot/describe-symbol-at-point (&optional arg)
    "Get help (documentation) for the symbol at point.

With a prefix argument, switch to the *Help* window.  If that is
already focused, switch to the most recently used window
instead."
    (interactive "P")
    (let ((symbol (symbol-at-point)))
      (when symbol
        (describe-symbol symbol)))
    (when arg
      (let ((help (get-buffer-window "*Help*")))
        (when help
          (if (not (eq (selected-window) help))
              (select-window help)
            (select-window (get-mru-window)))))))

  ;; Defines, among others, aliases for common minibuffer commands to
  ;; Super-KEY.  Normally these should go in individual package
  ;; declarations, but their grouping here makes things easier to
  ;; understand.  Besides, they are related to the minibuffer.
  :bind (("s-f" . find-file)
         ("s-F" . find-file-other-window)
         ("s-d" . dired)
         ("s-D" . dired-other-window)
         ("s-b" . switch-to-buffer)
         ("s-B" . switch-to-buffer-other-window)
         ("s-v" . prot/focus-minibuffer-or-completions)
         ("s-h" . prot/describe-symbol-at-point)
         ("s-H" . (lambda ()
                    (interactive)
                    (prot/describe-symbol-at-point '(4))))
         :map minibuffer-local-completion-map
         ("<return>" . minibuffer-force-complete-and-exit) ; exit with completion
         ("C-j" . exit-minibuffer)      ; force input unconditionally
         :map completion-list-mode-map
         ("h" . prot/describe-symbol-at-point)
         ("w" . prot/completions-kill-symbol-at-point)
         ("i" . prot/completions-insert-symbol-at-point)
         ("j" . prot/completions-insert-symbol-at-point-exit)
         ("n" . next-line)
         ("p" . previous-line)
         ("f" . next-completion)
         ("b" . previous-completion)
         ("M-v" . prot/focus-minibuffer)))
3.1.1.2 Icomplete configurations and extras

The following package declaration is for the built-in interactive minibuffer-based completion interface: icomplete. Remember that the underlying completion mechanisms are handled by the minibuffer itself—see section on Minibuffer configurations and extras.

Overview of the following package declaration:

  • The values of all variables that pertain to the delay of feedback are meant to introduce some delay in the feedback I get when the list of candidates is long. This paradoxically makes things feel faster while I am still typing because by that time the list has been narrowed to my input.
  • For versions of Emacs above 27, there is a minor mode called fido (Fake IDO, where ido is an alternative option to Icomplete that tries to do more than just completion). This new mode changes some of the primary key bindings and commands of icomplete so that it meets the expectations of Ido users. It is not meant as a fully fledged replacement of Ido, as its scope is much narrower, at least for the time being. If you are curious, check the source code for both icomplete and ido with the help of M-x find-library. I do not use Fido, though I think it is a welcome step in the right direction. For the sake completeness, note that you can always bind some Fido command without activating the minor mode.
  • The keybindings I specify define motions that ensure consistency between regular editing and the rotation of the candidates' list. The default icomplete key bindings leave something to be desired.
  • As with the same keys for the minibuffer, note the nuances in the behaviour between RET and C-j. With the Return key, we tell Icomplete to expand the current candidate and then exit the minibuffer if possible. Whereas C-j is meant to insert exactly what is in the minibuffer, which can be tricky in some circumstances. Use the latter when Icomplete is matching foobar but you only want foo (needed when renaming files or when creating links in Org to non-existent candidates).

Now a few words about my simple-yet-effective minibuffer actions.

  • From within the minibuffer, M-o functions as a prefix for "other" commands. For the time being, the key chord is finalised with any one of {i,j,w}.
  • prot/minibuffer-kill-completion (M-o w) will save the current candidate to the top of the kill-ring.
  • prot/minibuffer-insert-completion (M-o i) will insert the current candidate in the buffer from where the minibuffer was called.
  • prot/minibuffer-insert-completion-exit (M-o j) will do as above, but also exit all recursive minibuffers.

If you checked the previous section on the Minibuffer's setup, you will spot the parallels between those commands and the ones that are meant to be used inside the *Completions* buffer.

Note that these form part of a simplistic implementation which is good enough for my particular needs. Unless your workflow is the same as mine, I encourage you to check Omar Antolín Camarena's "Embark" package. It is not limited to Icomplete, has a more featureful implementation of a buffer that displays an overview of matching items, can be extended with ease, etc. It is just better for a minibuffer-actions-heavy workflow (mine is not that).

(use-package icomplete
  :demand
  :after minibuffer                     ; Read that section as well
  :config
  (setq icomplete-delay-completions-threshold 100)
  (setq icomplete-max-delay-chars 2)
  (setq icomplete-compute-delay 0.2)
  (setq icomplete-show-matches-on-no-input t)
  (setq icomplete-hide-common-prefix nil)
  (setq icomplete-prospects-height 1)
  (setq icomplete-separator (propertize " · " 'face 'shadow))
  ;; (setq icomplete-separator " │ ")
  ;; (setq icomplete-separator " ┆ ")
  ;; (setq icomplete-separator " ¦ ")
  ;; (setq icomplete-separator " ┆ ")
  (setq icomplete-with-completion-tables t)
  (setq icomplete-tidy-shadowed-file-names t)

  (fido-mode -1)                        ; Emacs 27.1
  (icomplete-mode 1)

  (defun prot/icomplete-minibuffer-truncate ()
    "Truncate minibuffer lines in `icomplete-mode'.
This should only affect the horizontal layout and is meant to
enforce `icomplete-prospects-height' being set to 1, which is
what I always want.

Hook it to `icomplete-minibuffer-setup-hook'."
    (when (and (minibufferp)
               (bound-and-true-p icomplete-mode))
      (setq truncate-lines t)))

;;; Minibuffer actions
  ;; For a fully fledged package that covers this niche and offers lots
  ;; of added functionality, check Omar Antolín Camarena's "embark"
  ;; library: https://github.com/oantolin/embark
  ;;
  ;; My idea here is to implement the three actions I had always relied
  ;; on, because they are the only ones I ever use.
  (defmacro prot/minibuffer-completion-act (name doc &rest body)
    `(defun ,name ()
       (interactive)
       (let ((candidate (car completion-all-sorted-completions)))
         (when (and (minibufferp)
                    (bound-and-true-p icomplete-mode))
           ,@body))))

  (prot/minibuffer-completion-act
   prot/minibuffer-kill-completion
   "Place minibuffer candidate to the top of the `kill-ring'."
   (kill-new `,candidate)
   (message "Copied %s to kill-ring" (propertize `,candidate 'face 'success)))

  (prot/minibuffer-completion-act
   prot/minibuffer-insert-completion
   "Insert minibuffer candidate in last active window."
   (with-minibuffer-selected-window (insert `,candidate)))

  (prot/minibuffer-completion-act
   prot/minibuffer-insert-completion-exit
   "Like `prot/minibuffer-insert-completion' but exit minibuffer."
   (prot/minibuffer-insert-completion)
   (top-level))

  (define-prefix-command 'prot/minibuffer-completion-map)

  (define-key prot/minibuffer-completion-map
    (kbd "w") 'prot/minibuffer-kill-completion)
  (define-key prot/minibuffer-completion-map
    (kbd "i") 'prot/minibuffer-insert-completion)
  (define-key prot/minibuffer-completion-map
    (kbd "j") 'prot/minibuffer-insert-completion-exit)

  (define-key minibuffer-local-completion-map
    (kbd "M-o") prot/minibuffer-completion-map)

  ;; Note that the the syntax for `use-package' hooks is controlled by
  ;; the `use-package-hook-name-suffix' variable.  The "-hook" suffix is
  ;; not an error of mine.
  :hook (icomplete-minibuffer-setup-hook . prot/icomplete-minibuffer-truncate)
  :bind (:map icomplete-minibuffer-map
              ("<tab>" . icomplete-force-complete)
              ("<return>" . icomplete-force-complete-and-exit) ; exit with completion
              ("C-j" . exit-minibuffer) ; force input unconditionally
              ("C-n" . icomplete-forward-completions)
              ("<right>" . icomplete-forward-completions)
              ("<down>" . icomplete-forward-completions)
              ("C-p" . icomplete-backward-completions)
              ("<left>" . icomplete-backward-completions)
              ("<up>" . icomplete-backward-completions)
              ;; The following command is from Emacs 27.1
              ("<C-backspace>" . icomplete-fido-backward-updir)))
3.1.1.2.1 Icomplete vertical mode

When I first switched to icomplete some time in late January to early February 2020, I had to implement my own admittedly sub-par tweaks for displaying candidates vertically and for tailoring that presentation to my particular needs. There was no package "ecosystem" around Icomplete that I could leverage.

Thankfully, Omar Antolín Camarena's icomplete-vertical is here (since early April 2020) to fill in this gap. The package provides a global minor mode for displaying the list of candidates vertically by default: simple and effective.

While fairly young, icomplete-vertical is a mature, feature-complete tool: it offers a robust experience out-of-the-box, while also providing facilities for users to introduce an element of ad-hoc verticality to their bespoke completion functions. More specifically:

  • With icomplete-vertical-toggle, which should be bound to a key inside the minibuffer, we can use whatever layout we want whenever we need it. Excellent!
  • While the icomplete-vertical-do macro can be used to parametrise a custom function with an optional height and unique separator. The latter comes with the option to pick from a list of presets: {solid,dashed,dotted}line.

The project's README should offer all the information you need. Several of my functions offer real-world implementations of the aforementioned (I did, after all, contribute some minor patches and user feedback in the early stages of this package, though all the real work is done by Omar—and it is a lot of work as confirmed by the commit logs and the scope of the diffs).

With regard to verticality, I am the kind of user that actually likes the standard horizontal view as a default presentation. It works splendidly for all my common workflows of executing a command by name, switching to a buffer, changing git branches, and the like. Verticality should, in my opinion, be the default only for lists that present naturally long candidates. A good example is the recentf-list that consists of full filesystem paths, with the kill-ring being another one.

Couched in those terms, the following package declaration does not enable icomplete-vertical-mode globally. Instead, it defines the elements that are necessary for activating verticality on an ad-hoc or per-function basis (and there are lots of custom functions like that—search for the icomplete-vertical-do macro).

In prot/kill-ring-yank-complete you will spot a function that disables the sorting of the list. Without it, the kills appear in a seemingly random order, which is highly undesired. I adapted that piece of functionality from the dotemacs of GitHub user jixiuf, following a comment I got from them on my video demo of Icomplete (2020-02-26).

With icomplete-vertical, I can now remove all the poor code I had in place when I got started, relying instead on the thoughtful design and features that Omar has so meticulously developed. In short: the package is a near must-have for any icomplete user.

Finally, not all "vertical" functions of mine are defined here. Some are also found in completion for projects and directory trees and others still across this document (search for icomplete-vertical-do).

(use-package icomplete-vertical
  :ensure
  :demand
  :after (minibuffer icomplete) ; do not forget to check those as well
  :config
  (setq icomplete-vertical-prospects-height (/ (frame-height) 6))
  (icomplete-vertical-mode -1)

  (defun prot/kill-ring-yank-complete ()
    "Insert the selected `kill-ring' item directly at point.
When region is active, `delete-region'.

Sorting of the `kill-ring' is disabled.  Items appear as they
normally would when calling `yank' followed by `yank-pop'."
    (interactive)
    (let ((kills                    ; do not sort items
           (lambda (string pred action)
             (if (eq action 'metadata)
                 '(metadata (display-sort-function . identity)
                            (cycle-sort-function . identity))
               (complete-with-action
                action kill-ring string pred)))))
      (icomplete-vertical-do
          (:separator 'dotted-line :height (/ (frame-height) 4))
        (when (use-region-p)
          (delete-region (region-beginning) (region-end)))
        (insert
         (completing-read "Yank from kill ring: " kills nil t)))))

  :bind (("s-y" . prot/kill-ring-yank-complete)
         :map icomplete-minibuffer-map
         ("C-v" . icomplete-vertical-toggle)))

3.1.2 Imenu (dynamic completion-based buffer navigation)

The imenu is a built-in library that builds an index of buffer positions pointing to semantically relevant constructs. It then displays the list through a completion interface. Selecting an item repositions the point there.

This is a great tool for quickly jumping to a position in the buffer. Combine it with the sheer power of the completion framework or the possibility to further extend it with little helper snippets and you get a whole new way of thinking about moving around in a buffer.

By default, imenu is designed to not refresh the index it builds, offering a "Rescan" option instead. I find that too conservative for my case, opting instead for automatic rescaning. This has the desirable side-effect of removing the manual refresh command from the list.

With prot/imenu-vertical I provide much-needed verticality to the list, because it typically consists of naturally long candidates that make for a poor fit in the horizontal layout of Icomplete. Notice that it configures the orderless completion style, while it also relies on icomplete-vertical. Review my Completion framework and extras.

The :hook here will simply scroll the buffer so that the landing position of an Imenu interaction becomes the first visible line at the top. I find this to be the easiest way to know where I am.

Note that Imenu combines nicely with other major/minor modes, such as outline-minor-mode (see: Outline mode, outline minor mode, and extras). This has the potential to upgrade your interaction with various buffers. I use this combo with Elisp and Markdown buffers. Make sure to watch my video demo of outline-minor-mode and imenu (2020-07-20). For Org, you need to set org-imenu-depth to a high value (for more, refer to the section on Org basic configurations).

(use-package imenu
  :config
  (setq imenu-use-markers t)
  (setq imenu-auto-rescan t)
  (setq imenu-auto-rescan-maxout 600000)
  (setq imenu-max-item-length 100)
  (setq imenu-use-popup-menu nil)
  (setq imenu-eager-completion-buffer t)
  (setq imenu-space-replacement " ")
  (setq imenu-level-separator "/")

  (defun prot/imenu-vertical ()
    "Use a vertical Icomplete layout for `imenu'.
Also configure the value of `orderless-matching-styles' to avoid
aggressive fuzzy-style matching for this particular command."
    (interactive)
    (let ((orderless-matching-styles    ; make sure to check `orderless'
           '(orderless-literal
             orderless-regexp
             orderless-prefixes)))
      (icomplete-vertical-do (:height (/ (frame-height) 4))
        (call-interactively 'imenu))))

  (defun prot/imenu-recenter-pulse ()
    "Recent `imenu' position at the top with subtle feedback.
Add this to `imenu-after-jump-hook'."
    (let ((pulse-delay .05))
      (recenter 0)
      (prot/pulse-line)))

  (defun prot/imenu-show-entry ()
    "Reveal index at point after successful `imenu' execution.
To be used with `imenu-after-jump-hook'."
    (cond
     ((and (eq major-mode 'org-mode)
           (org-at-heading-p))
      (org-show-entry)
      (org-reveal t))
     ((when prot/outline-minor-mode
        (outline-show-entry)))))

  :hook ((imenu-after-jump-hook . prot/imenu-recenter-pulse)
         (imenu-after-jump-hook . prot/imenu-show-entry))
  :bind ("C-." . prot/imenu-vertical))
3.1.2.1 Imenu-list (dedicated sidebar)

While completion for imenu is wonderful in its own right, you may still need to have a sense of the bigger picture. With imenu-list we can place the contents of the current list in a dedicated buffer that sits on the right side of the frame. We may then use that as an index to further support us in the task of navigating through the current buffer.

(use-package imenu-list
  :ensure
  :after imenu
  :config
  (defun prot/imenu-list-dwim (&optional arg)
    "Convenience wrapper for `imenu-list'.
Move between the current buffer and a dedicated window with the
contents of `imenu'.

The dedicated window is created if it does not exist, while it is
updated once it is focused again through this command.

With \\[universal-argument] toggle the display of the window."
    (interactive "P")
    (if arg
        (imenu-list-smart-toggle)
      (with-current-buffer
          (if (eq major-mode 'imenu-list-major-mode)
              (pop-to-buffer (other-buffer (current-buffer) t))
            (imenu-list)))))

  :bind ("C-," . prot/imenu-list-dwim))
3.1.2.2 Flimenu (flat imenu index)

This package offers a global and a local minor mode for flattening the index of imenu. By default Imenu produces a multi-level index, where appropriate. For example, it will put the heading of this section under that of its parent: Imenu (dynamic completion-based buffer navigation). So to navigate to this point, you need to select the parent and then the child path. Good for a tree view (see Imenu-list (dedicated sidebar)). But not great for fuzzy-style search through the completion UI, because it slows down things considerably, while not offering any typographic or layout means of recognising the structure at first sight.

In other words, a multi-level completion interface leaves much to be desired. By enabling the flimenu-global-mode we get a flat list for the completion-based interaction with imenu. Now we can just search directly for any item on the list. Great!

To make things even better, we can still access the fully fledged tree presentation of imenu-list, as it operates independently.

Lastly, flimenu could also be used as a toggle, by virtue of its local minor mode, though I cannot think of a scenario where I would want that, given the aforementioned.

(use-package flimenu
  :ensure
  :after imenu
  :config
  (flimenu-global-mode 1))

3.1.3 Generic completion for projects and directory trees

Bear in mind that this section only covers a set of generic commands for querying version-controlled directories, aka "projects", or directory trees in general. They do not cover every type of interaction with projects or directories, but only those that are not specific to other tools, such as dired and ibuffer.

Overview of "advanced searches" as of 2020-05-31 (subject to further refinements):

Function name Key Description
prot/find-file-vc-or-dir M-s f Recursive file search from project root or dir
prot/find-project M-s p Switch to project (projects across many paths)
prot/rg-vc-or-dir M-s g Grep regexp recursively (editable buffer)
prot/rg-ref-in-dir M-s r Grep for ref in present dir (edit buf)
prot/dired-fd-dirs M-s d Recursive search for subdirs
prot/dired-fd-files-and-dirs M-s z Recursive files and dirs
prot/buffers-major-mode M-s b Buffers matching current's major mode
prot/buffers-vc-root M-s v Buffers in current project

Only the first two are in this section. For the rest see:

Many of those accept a prefix argument (C-u), which will put the results in a Dired/Ibuffer. Otherwise they use the completion framework (Icomplete in my case, though technically the underlying completing-read should work with any compliant front-end).

Concerning the design of these key bindings, they are consistent with all my "advanced search methods" (e.g. the default M-s o for occur). If I ever identify a conflict, such as M-s f in dired-mode-map, I disable that function altogether in favour of my own (or remap its key if the function it calls is useful overall).

Recall that my motivation for writing those is to practice some Elisp. Otherwise you may be better off with a library like Projectile.

(use-package project
  :config
  (defun prot/find-file-vc-or-dir (&optional arg)
    "Find file by name that belongs to the current project or dir.
With \\[universal-argument] match files by contents.  This
requires the command-line executable called 'rg' or 'ripgrep'."
    (interactive "P")
    (let* ((default-directory (file-name-directory
                               (or (locate-dominating-file "." ".git" )
                                   default-directory))))
      (if arg
          (let* ((regexp (read-regexp
                          (concat "File contents matching REGEXP in "
                                  (propertize default-directory 'face 'bold)
                                  ": ")))
                 (results (process-lines "rg" "-l" "--hidden" "-m" "1" "-M" "120" regexp)))
            (find-file
             (icomplete-vertical-do ()
               (completing-read (concat
                                 "Files with contents matching "
                                 (propertize regexp 'face 'success)
                                 (format " (%s)" (length results))
                                 ": ")
                                results nil t))))
        (let* ((filenames-all (directory-files-recursively default-directory ".*" nil t))
               (filenames (cl-remove-if (lambda (x)
                                          (string-match-p "\\.git" x))
                                        filenames-all)))
          (icomplete-vertical-do ()
            (find-file
             (completing-read "Find file recursively: " filenames nil t)))))))

  (defun prot/find-project (&optional arg)
    "Switch to sub-directory at the specified locations.
With \\[universal-argument] produce a `dired' buffer instead with
all the possible candidates."
    (interactive "P")
    (let* ((dirs (list "~/Git/Projects/" "~/.emacs.d/prot-dev/"))
           (dotless directory-files-no-dot-files-regexp)
           (cands (mapcan (lambda (d)
                            (directory-files d t dotless))
                          dirs))
           (projects (mapcar 'abbreviate-file-name cands))
           (buf "*Projects Dired*"))
      (if arg
          (dired (cons (generate-new-buffer-name buf) projects))
        (icomplete-vertical-do ()
          (dired
           (completing-read "Find project: " projects nil t))))))

  :bind (("M-s p" . prot/find-project)
         ("M-s f" . prot/find-file-vc-or-dir)
         ("M-s l" . find-library)))

3.1.4 In-buffer completions

After trying the popular third-party "Company" package, I felt that it did not offer much of an added value to my typing experience, while its popup feature detracted from the otherwise frugal aesthetics of my setup. Furthermore, I felt like it was adding a second type of completion paradigm while ignoring the original one, i.e. the minibuffer—again, an offense against simplicity.

What I have in this section is a few simple tweaks and built-in ways to complete terms while typing text in a buffer. I think that, for most cases, the minibuffer can be used effectively to perform in-buffer completion: start from my Tools for completion candidate listing to understand how these fit into the broader framework.

I yanked contrib/completing-read-in-region straight from Omar Antolín Camarena's dotemacs repo. It provides a minibuffer-completion interface to the standard tab-completion mechanism (also check what I have on the topic of Tabs, indentation, and the TAB key). It does the same for dabbrev-completion (see next section).

(use-package emacs
  :config
  (defun contrib/completing-read-in-region (start end collection &optional predicate)
    "Prompt for completion of region in the minibuffer if non-unique.
Use as a value for `completion-in-region-function'."
    (if (and (minibufferp) (not (string= (minibuffer-prompt) "Eval: ")))
        (completion--in-region start end collection predicate)
      (let* ((initial (buffer-substring-no-properties start end))
             (limit (car (completion-boundaries initial collection predicate "")))
             (all (completion-all-completions initial collection predicate
                                              (length initial)))
             (completion (cond
                          ((atom all) nil)
                          ((and (consp all) (atom (cdr all)))
                           (concat (substring initial 0 limit) (car all)))
                          (t (completing-read
                              "Completion: " collection predicate t initial)))))
        (if (null completion)
            (progn (message "No completion") nil)
          (delete-region start end)
          (insert completion)
          t))))

  (setq completion-in-region-function #'contrib/completing-read-in-region)
  :bind (:map minibuffer-local-completion-map
              ("<tab>" . minibuffer-force-complete)))
3.1.4.1 Dabbrev (dynamic word completion)

This is Emacs' own approach to dynamic/arbitrary text completion inside the buffer: "dynamic abbreviation" or else dabbrev. This mechanism works by reading all text before point to find a suitable match. Different scenaria determine whether it should also look forward and in other buffers.

In essence, Dabbrev can help you type again what you already have. It will not draw findings from some knowledge bank, nor will it try to read your mind (though Emacs will definitely have an M-x conduit to such a technology).

With dabbrev-expand we make an attempt to complete the text at point. Repeated invocations will cycle through the candidates. No feedback is provided, much in the same way yanking from the kill-ring works. Whereas dabbrev-completion benefits from minibuffer interactivity, courtesy of contrib/completing-read-in-region that was defined in the section right above.

The dabbrev-abbrev-char-regexp is configured to match both regular words and symbols (e.g. words separated by hyphens). This makes it suitable both for code and ordinary language.

While the dabbrev-abbrev-skip-leading-regexp is instructed to also expand words and symbols that start with any of these: $, *, /, =, '. This regexp may be expanded in the future, but the idea is to be able to perform completion in contexts where the known word/symbol is preceded by a special characters. For example, in the org-mode version of this document, all inline code must be placed between the equals sign. So now typing the =, then a letter, will still allow me to expand text based on that input.

To check what I have on regular expressions, see further below my configurations and documentation for re-builder and visual-regexp.

(use-package dabbrev
  :after (minibuffer icomplete icomplete-vertical) ; read those as well
  :config
  (setq dabbrev-abbrev-char-regexp "\\sw\\|\\s_")
  (setq dabbrev-abbrev-skip-leading-regexp "[$*/=']")
  (setq dabbrev-backward-only nil)
  (setq dabbrev-case-distinction 'case-replace)
  (setq dabbrev-case-fold-search t)
  (setq dabbrev-case-replace 'case-replace)
  (setq dabbrev-check-other-buffers t)
  (setq dabbrev-eliminate-newlines nil)
  (setq dabbrev-upcase-means-case-search t)
  :bind (("M-/" . dabbrev-expand)
         ("C-M-/" . dabbrev-completion)
         ("s-/" . dabbrev-completion)))
3.1.4.2 Skeletons and abbreviations

NOTE 2020-06-08: Pending major review.

This section stores all the "skeletons" I define. These are snippets of text, typically templates or code statements, that are meant to speed up typing. While abbreviations are shorter versions of terms that automatically expand into what they correspond to. I combine skeletons with abbreviations.

Please note that these will be very simplistic at first. I am aware that they can be abstracted using elisp—need to learn more on that front. Also note that wherever you see " _ " it signifies the position of the cursor after the skeleton has been inserted.

(use-package abbrev
  :diminish
  :config
  (setq abbrev-file-name "~/.emacs.d/abbrevs")
  (setq only-global-abbrevs nil)

  ;;;;;;;;;;;;;;;;;;;;;;
  ;; simple skeletons ;;
  ;;;;;;;;;;;;;;;;;;;;;;
  (define-skeleton protesilaos-com-skeleton
    "Adds a link to my website while prompting for a possible
  extension."
    "Insert website extension: "
    "https://protesilaos.com/" str "")
  (define-abbrev global-abbrev-table "meweb"
    "" 'protesilaos-com-skeleton)

  (define-skeleton protesilaos-gitlab-skeleton
    "Adds a link to my GitLab account while prompting for a
  possible extension.  Makes it easy to link to my various git
  repos."
    "Website extension: "
    "https://gitlab.com/protesilaos/" str "")
  (define-abbrev global-abbrev-table "megit"
    "" 'protesilaos-gitlab-skeleton)

  ;; (define-skeleton org-block-skeleton
  ;;   "Insert an org block, querying for type."
  ;;   "Type: "
  ;;   "#+begin_" str "\n"
  ;;   _ - \n
  ;;   "#+end_" str "\n")
  ;;
  ;; (define-skeleton org-use-package
  ;;   "Org source block with `use-package' declaration."
  ;;   "Package: "
  ;;   "#+begin_src emacs-lisp\n"
  ;;   "(use-package " _ ")\n"
  ;;   "#+end_src\n")
  ;; (define-abbrev global-abbrev-table "meup"
  ;;   "" 'org-use-package)
  ;;
  ;; (define-skeleton markdown-jekyll-blog
  ;;   ""
  ;;   ""
  ;;   "---\n"
  ;;   (concat "title: '" (read-from-minibuffer
  ;;                       "Blog Title: ") "'\n")
  ;;   (concat "excerpt: '" (read-from-minibuffer
  ;;                         "Excerpt (max 156 chars): ") "'\n")
  ;;   "---\n\n"
  ;;   "")
  ;; (define-abbrev global-abbrev-table "mejb"
  ;;   "" 'markdown-jekyll-blog)
  :bind (("C-x a e" . expand-abbrev) ; default, just here for visibility
         ("C-x a u" . unexpand-abbrev))
  :hook ((text-mode-hook . abbrev-mode)
         (git-commit-mode-hook . abbrev-mode)))

3.2 Configurations for—or extensions to—built-in search commands

These are meant to enhance the functionality of tools that are already shipped with Emacs.

3.2.1 Isearch configurations and enhancements

The built-in search mechanism is a thing of beauty: minimal in its presentation, powerful in its applications.

I use isearch all the time for quick navigation, either to a visible part of the buffer or to some specific string I am aware of. It also is essential when used in the context of a keyboard macro, as demonstrated in my video about Isearch powers in keyboard macros (2020-01-21).

Run C-h k C-s to get an awesome help menu with all the extra keys you can use with isearch. These are the ones I use the most:

Key chord Description
C-s C-w Search char or word at point
M-s . Similar, but broader match
M-s o Run `occur' on regexp
M-s h r Highlight regexp
M-s h u Undo the highlight
C-s M-r Toggle regexp search
M-% Run `query-replace'
C-M-% `query-replace-regexp'

Many commands can be invoked while running isearch to operate on the current match. For example, C-s SEARCH M-s o will produce an "Occur" buffer with the contents of the search terms. Absolutely great!

With regard to the replace commands, note that you can use them on the active region. Furthermore, you do not need to confirm each action, but can instead type ! to answer "yes" to all possible replacements. Better only use this while having already limited the results to the active region, to some specialised editable buffer like the one of occur, or by using Emacs' narrowing techniques, such as narrow-to-region.

In the package declaration below, the combined effect of the variables for whitespace is a valuable hack: typing a space is the same as inserting a wildcard, which is much more useful as far as I am concerned. A single space represents a wildcard that matches items in a non-greedy fashion. This affects regular searches (the standard C-s and C-r). The regexp-sensitive functions C-M-s and C-M-r remain in tact. You can always toggle whitespace matching behaviour while performing a search, with M-s SPC (revert back to just literal spaces).

Now on to some custom functions, all of which are derived from the source code of isearch (do it with M-x find-library RET isearch RET). Here is an overview of what goes into this package declaration.

Mark isearch match
Replaces the default mark command following a successful search. I prefer to mark the match. This can be then used to insert multiple cursors (if you are using it), kill the region, etc. Besides, it is always possible to mark a region from point to search string by running C-x C-x following a successful search.
Move to opposite end
Isearch places the point at either the beginning or the end of the match, depending on the direction it is moving in. For single words or balanced expressions this is not an issue because you can always confirm a search by using a motion key (so, for example, move to the end of the matching word with M-f). There are, however, matches that are not limited to such boundaries. For those cases moving to the opposite end might require multiple key presses, which is bad when trying to record an efficient keyboard macro. prot/isearch-other-end addresses the issue. It is bound to C-RET while running a successful search. The direct inspiration is this forum answer. Note though that you can achieve the same result by changing the direction the search is moving towards with C-s or C-r (though I still prefer my minor addition).
Delete non-match
The built-in method to remove the entirety of a mismatched input is to hit C-g following a failed search. This keeps the valid part and allows you to continue searching. However, I find that the choice of key binding can prove problematic, since C-g also exits a standard/successful search. As such, the simple function prot/isearch-abort is designed to remove the entirety of a mismatch, just by hitting backspace (aka DEL). For valid searches, backspace functions exactly as expected, deleting one character at a time. Note, though, that it is no longer possible to delete part of a failed search, just by hitting backspace: you can still rely on C-M-d for that (or edit the input with M-e).
Replace symbol at point
Combine the built-in functions of isearch-forward-symbol-at-point and isearch-query-replace-regexp into a single command that is bound to the key chord M-s %. Simple and super effective (pro tip: hit ! to answer "yes" to all possible matches, which is possible in all cases where Emacs asks you for multiple confirmations).
Move to occurrence at start or end
Using the symbol at point, move to the first or optionally the ARGth occurrence from the beginning or the end of the buffer. I map these commands to the same keys they are bound to in isearch-mode-map, namely, M-s M-< and M-s M->.

The variables about the lazy count that are commented as "Emacs 27.1" effectively supersede the functionality of anzu, a package I once used.

(use-package isearch
  :diminish
  :config
  (setq search-highlight t)
  (setq search-whitespace-regexp ".*?")
  (setq isearch-lax-whitespace t)
  (setq isearch-regexp-lax-whitespace nil)
  (setq isearch-lazy-highlight t)
  ;; All of the following variables were introduced in Emacs 27.1.
  (setq isearch-lazy-count t)
  (setq lazy-count-prefix-format nil)
  (setq lazy-count-suffix-format " (%s/%s)")
  (setq isearch-yank-on-move 'shift)
  (setq isearch-allow-scroll 'unlimited)

  (defun prot/isearch-mark-and-exit ()
    "Mark the current search string and exit the search."
    (interactive)
    (push-mark isearch-other-end t 'activate)
    (setq deactivate-mark nil)
    (isearch-done))

  (defun prot/isearch-other-end ()
    "End current search in the opposite side of the match.
Particularly useful when the match does not fall within the
confines of word boundaries (e.g. multiple words)."
    (interactive)
    (isearch-done)
    (when isearch-other-end
      (goto-char isearch-other-end)))

  (defun prot/isearch-abort-dwim ()
    "Delete failed `isearch' input, single char, or cancel search.

This is a modified variant of `isearch-abort' that allows us to
perform the following, based on the specifics of the case: (i)
delete the entirety of a non-matching part, when present; (ii)
delete a single character, when possible; (iii) exit current
search if no character is present and go back to point where the
search started."
    (interactive)
    (if (eq (length isearch-string) 0)
        (isearch-cancel)
      (isearch-del-char)
      (while (or (not isearch-success) isearch-error)
        (isearch-pop-state)))
    (isearch-update))

  (defun prot/isearch-query-replace-symbol-at-point ()
    "Run `query-replace-regexp' for the symbol at point."
    (interactive)
    (isearch-forward-symbol-at-point)
    (isearch-query-replace-regexp))

  (defmacro prot/isearch-occurrence (name edge &optional doc)
    "Construct function for moving to `isearch' occurrence.
NAME is the name of the function.  EDGE is either the beginning
or the end of the buffer.  Optional DOC is the resulting
function's docstring."
    `(defun ,name (&optional arg)
       ,doc
       (interactive "p")
       (let ((x (or arg 1))
             (command (intern (format "isearch-%s-of-buffer" ,edge))))
         (isearch-forward-symbol-at-point)
         (funcall command x))))

  (prot/isearch-occurrence
   prot/isearch-beginning-of-buffer
   "beginning"
   "Run `isearch-beginning-of-buffer' for the symbol at point.
With numeric ARG, move to ARGth occurrence counting from the
beginning of the buffer.")

  (prot/isearch-occurrence
   prot/isearch-end-of-buffer
   "end"
   "Run `isearch-end-of-buffer' for the symbol at point.
With numeric ARG, move to ARGth occurrence counting from the
end of the buffer.")

  :bind (("M-s %" . prot/isearch-query-replace-symbol-at-point)
         ("M-s M-<" . prot/isearch-beginning-of-buffer)
         ("M-s M->" . prot/isearch-end-of-buffer)
         :map minibuffer-local-isearch-map
         ("M-/" . isearch-complete-edit)
         :map isearch-mode-map
         ("C-g" . isearch-cancel)       ; instead of `isearch-abort'
         ("M-/" . isearch-complete)
         ("C-SPC" . prot/isearch-mark-and-exit)
         ("<backspace>" . prot/isearch-abort-dwim)
         ("<C-return>" . prot/isearch-other-end)))

3.2.2 Occur (replace.el)

The setq forms are mere stylistic tweaks for the buffer that M-x occur produces. The faces are part of my Modus themes (see relevant section).

The functions are simple tools to either produce a list with all URLs present in the buffer (prot/occur-url), or prompt you for visiting a URL from those available using completion methods.

(use-package replace
  :config
  (setq list-matching-lines-jump-to-current-line t)
  ;; See my "Modus themes" for these inherited faces
  (setq list-matching-lines-buffer-name-face
        '(:inherit modus-theme-intense-neutral :weight bold))
  (setq list-matching-lines-current-line-face
        '(:inherit modus-theme-special-mild))

  (defun prot/occur-url ()
    "Produce list with all URLs in the current buffer."
    (interactive)
    (let ((urls browse-url-button-regexp))
      (occur urls "\\&")))

  (defun prot/occur-browse-url-in-buffer ()
    "Run `eww' on a URL from the buffer using completion.
Also see `prot/occur-url'."
    (interactive)
    (let ((matches nil))
      (save-excursion
        (goto-char (point-min))
        (while (search-forward-regexp browse-url-button-regexp nil t)
          (push (match-string-no-properties 0) matches)))
      (icomplete-vertical-do
          (:height (/ (frame-height) 4) :separator 'dotted-line)
        (eww
         (completing-read "Browse URL: " matches nil t)))))

  (defun prot/occur-visit-or-list-urls (&optional arg)
    "Wrap `prot/occur-visit-or-list-urls' and `prot/occur-url'.
Meant to economise on key bindings."
    (interactive "P")
    (if arg
        (prot/occur-url)
      (prot/occur-browse-url-in-buffer)))

  :hook ((occur-mode-hook . hl-line-mode)
         (occur-mode-hook . (lambda ()
                              (toggle-truncate-lines t))))
  :bind (("M-s u" . prot/occur-visit-or-list-urls)
         ("M-s M-o" . multi-occur)
         :map occur-mode-map
         ("t" . toggle-truncate-lines)))

3.2.3 Regular expressions: re-builder and visual-regexp

To learn more about regular expressions, read the relevant pages in the official manual. Assuming you have this installed properly on your system, run C-h r i regexp to get to the starting chapter.

Also watch my ~35 minute-long primer on Emacs regexp (2020-01-23).

Emacs offers a built-in package for practising regular expressions. By default, re-builder uses Emacs-style escape notation, in the form of double backslashes. You can switch between the various styles by using C-c TAB inside of the regexp builder's buffer. I choose to keep this style as the default. Other options are string and rx.

(use-package re-builder
  :config
  (setq reb-re-syntax 'read))

Another option (though the two are not mutually exclusive) is to use the third-party package visual-regexp. This one is meant as a drop-in replacement for query-replace (and the regexp variant). I prefer not to use it that way, but only invoke it via M-x when I need to test a regular expression that I would then replace with something else. The major upside of this tool is that it highlights groups individually and offers a live preview of the replacement, making it absolutely great when dealing with complex sets of regexp constructs.

(use-package visual-regexp
  :ensure
  :config
  (setq vr/default-replace-preview nil)
  (setq vr/match-separator-use-custom-face t))

3.2.4 wgrep (writable grep)

With wgrep we can directly edit the results of a grep and save the changes to all affected buffers. In principle, this is the same as what the built-in occur offers. We can use it to operate on a list of matches by leveraging the full power of Emacs' editing capabilities (e.g. keyboard macros, multiple cursors…).

(use-package wgrep
  :ensure
  :commands wgrep
  :config
  (setq wgrep-auto-save-buffer t)
  (setq wgrep-change-readonly-file t)
  :bind (:map grep-mode-map
              ("e" . wgrep-change-to-wgrep-mode)
              ("C-x C-q" . wgrep-change-to-wgrep-mode)))

3.2.5 ripgrep (rg.el)

This is a package that allows us to interface with the external command line program called "ripgrep". My video demo of rg.el (2020-03-25) covers the main features of this tool.

What I find particularly appealing about rg.el is that it follows the interface paradigms of built-in Emacs functions, such as grep or occur. With regard to the latter, it even uses the same key to convert the results' buffer into an editable one: e (the ability to write changes is provided by the wgrep package that I define right above).

Furthermore, rg.el interfaces with ibuffer, another built-in package, to list saved searches (see my prot/rg-save-search-as-name in the package declaration below). Saved searches are regular buffers. You can switch to any of them the normal way.

While inside of an rg.el buffer, hit m to produce a transient menu from where you can refine your search. This works just like magit. In addition, you can consult the universal C-h m for documentation concerning the major mode you are in.

Concerning the key bindings for navigating the results buffer, I find that the standard motions should retain their general function, while moving between file headings can be done with M-{n,p}.

rg.el is designed in such a way that it offers useful functionality without depending on a particular completion framework (e.g. Ivy, Helm). I consider this an advantage, especially when combined with the overall alignment of this package with standard Emacs tools.

Also see my configurations for project-related commands.

(use-package rg
  :ensure
  :config
  (setq rg-group-result t)
  (setq rg-hide-command t)
  (setq rg-show-columns nil)
  (setq rg-show-header t)
  (setq rg-custom-type-aliases nil)
  (setq rg-default-alias-fallback "all")

  (rg-define-search prot/rg-vc-or-dir
    "RipGrep in project root or present directory."
    :query ask
    :format regexp
    :files "everything"
    :dir (or (vc-root-dir)              ; search root project dir
             default-directory)         ; or from the current dir
    :confirm prefix
    :flags ("--hidden -g !.git"))

  (rg-define-search prot/rg-ref-in-dir
    "RipGrep for thing at point in present directory."
    :query point
    :format regexp
    :files "everything"
    :dir default-directory
    :confirm prefix
    :flags ("--hidden -g !.git"))

  (defun prot/rg-save-search-as-name ()
    "Save `rg' buffer, naming it after the current search query.

This function is meant to be mapped to a key in `rg-mode-map'."
    (interactive)
    (let ((pattern (car rg-pattern-history)))
      (rg-save-search-as-name (concat "«" pattern "»"))))

  :bind (("M-s g" . prot/rg-vc-or-dir)
         ("M-s r" . prot/rg-ref-in-dir)
         :map rg-mode-map
         ("s" . prot/rg-save-search-as-name)
         ("C-n" . next-line)
         ("C-p" . previous-line)
         ("M-n" . rg-next-file)
         ("M-p" . rg-prev-file)))

4 General movements and motions

I generally rely on the default keys to move around (plus my Super-KEY additions to economise on some repetitive tasks). There are, however, some motions that are rather cumbersome or too specialised. While there are some commands that are not available at all. For those cases, I use custom functions and/or remapped key bindings, as well as carefully considered external packages.

4.1 Custom commands in line with simple.el and lisp.el

The subsections provided herein contain small functions that facilitate my day-to-day work with Emacs. They are written by me, unless otherwise noted, and typically tweak or extend some built-in functionality. I also use these code blocks to map certain useful built-in functions to my preferred key bindings.

The commands are organised per their role. If you are viewing the source version of this document, you can place the point somewhere in these opening paragraphs and use C-x n s (org-narrow-to-subtree) to focus on the relevant parts. Remember that widening always happens with C-x n w (widen).

4.1.1 Custom commands for lines

The docstrings of my functions here should offer you all the information you need. Just a couple of notes:

  • delete-blank-lines is normally mapped to C-x C-o, whereas I also bind it to M-o, which makes it symmetrical with open-line (C-o).
  • By default, M-SPC (just-one-space) will reduce multiple spaces into a single one, while M-\ (delete-horizontal-space) will remove all space at once from point to the end of the line. Whereas the built-in wrapper of cycle-spacing lets us economise on key bindings: a single invocation will leave one space, a second removes all space, while a third consecutive call restores the original state.
  • The prot/line-p macro is a general purpose utility to confirm a match for a regular expression on a given line (1 is line at point). Refer to the section on custom commands for text insertion or manipulation, specifically its prot/text-mode-heading, for a demonstration of how the derived functions are used.
(use-package emacs
  :config
  (defun prot/new-line-below (&optional arg)
    "Create an empty line below the current one.
Move the point to the absolute beginning.  Adapt indentation by
passing \\[universal-argument].  Also see `prot/new-line-above'."
    (interactive "P")
    (end-of-line)
    (if arg
        (newline-and-indent)
      (newline)))

  (defun prot/new-line-above (&optional arg)
    "Create an empty line above the current one.
Move the point to the absolute beginning.  Adapt indentation by
passing \\[universal-argument]."
    (interactive "P")
    (let ((indent (or arg nil)))
      (if (or (bobp)
              (line-number-at-pos 1))
          (progn
            (beginning-of-line)
            (newline)
            (forward-line -1))
        (forward-line -1)
        (prot/new-line-below indent))))

  (defun prot/copy-line-or-region (&optional arg)
    "Kill-save the current line or active region.
With \\[universal-argument] duplicate the target instead.  When
region is active, also apply context-aware indentation while
duplicating."
    (interactive "P")
    (let* ((rbeg (region-beginning))
           (rend (region-end))
           (pbol (point-at-bol))
           (peol (point-at-eol))
           (indent (if (eq (or rbeg rend) pbol) nil arg)))
      (if arg
          (progn
            (if (use-region-p)
                (progn
                  (copy-region-as-kill rbeg rend)
                  (when (eq (point) rbeg)
                    (exchange-point-and-mark))
                  (prot/new-line-below indent))
              (copy-region-as-kill pbol peol)
              (prot/new-line-below))
            (yank))
        (copy-region-as-kill pbol peol)
        (message "Current line copied"))))

  (defun prot/yank-replace-line-or-region ()
    "Replace line or region with latest kill.
This command can then be followed by the standard
`yank-pop' (default is bound to \\[yank-pop])."
    (interactive)
    (if (use-region-p)
          (delete-region (region-beginning) (region-end))
      (delete-region (point-at-bol) (point-at-eol)))
      (yank))

  (defun prot/multi-line-next ()
    "Move point 15 lines down."
    (interactive)
    (forward-line 15))

  (defun prot/multi-line-prev ()
    "Move point 15 lines up."
    (interactive)
    (forward-line -15))

  (defun prot/kill-line-backward ()
    "Kill from point to the beginning of the line."
    (interactive)
    (kill-line 0))

  ;; Based on `org--line-empty-p'.
  (defmacro prot/line-p (name regexp)
    "Make NAME function to match REGEXP on line n from point."
    `(defun ,name (n)
       (save-excursion
         (and (not (bobp))
	          (or (beginning-of-line n) t)
	          (save-match-data
	            (looking-at ,regexp))))))

  (prot/line-p
   prot/empty-line-p
   "[\s\t]*$")

  (prot/line-p
   prot/indent-line-p
   "^[\s\t]+")

  (prot/line-p
   prot/non-empty-line-p
   "^.*$")

  (prot/line-p
   prot/text-list-line-p
   "^\\([\s\t#*+]+\\|[0-9]+[).]+\\)")

  (prot/line-p
   prot/text-heading-line-p
   "^[=-]+")

  :bind (("C-S-w" . prot/copy-line-or-region)
         ("C-S-y" . prot/yank-replace-line-or-region)
         ("M-SPC" . cycle-spacing)
         ("M-o" . delete-blank-lines)   ; alias for C-x C-o
         ("M-k" . prot/kill-line-backward)
         ("C-S-n" . prot/multi-line-next)
         ("C-S-p" . prot/multi-line-prev)
         ("<C-return>" . prot/new-line-below)
         ("<C-S-return>" . prot/new-line-above)))

4.1.2 Custom commands for text insertion or manipulation

Insert pairs with completion
With prot/insert-pair-completion I solve the problem of having to define a potentially large set of key bindings for the specialised task of inserting a pair of delimiters. Now I define an association list with the pairs I am interested in and let that function do the work.
Plain text headings
prot/text-mode-heading is my solution to the problem of having to insert numerous characters below a line of text so that it looks like a heading. I do this on occasion with emails, git commits, and the like, where a simple format is sufficient for the task at hand (Org is fine in general, though it definitely is overkill for such scenaria—use Org when you need its advanced text-manipulation functions and integration with its numerous extras). The prot/line-p macro from the section on custom commands for lines helps me define checks for what is on a given line, in order to determine whether it should be excluded from becoming a heading or not.
Change case "do what I mean"
Emacs provides *-dwim versions of its commands for changing the casing of a word or a region. With an active region, they operate on it, else they operate on the word. I bind them to the keys where you would find the originals (which, in turn, render the region-specific variants redundant).
(use-package emacs
  :config
  ;; Got those numbers from `string-to-char'
  (defconst prot/insert-pair-alist
    '(("' Single quote" . (39 39))           ; ' '
      ("\" Double quotes" . (34 34))         ; " "
      ("` Elisp quote" . (96 39))            ; ` '
      ("‘ Single apostrophe" . (8216 8217))  ; ‘ ’
      ("“ Double apostrophes" . (8220 8221)) ; “ ”
      ("( Parentheses" . (40 41))            ; ( )
      ("{ Curly brackets" . (123 125))       ; { }
      ("[ Square brackets" . (91 93))        ; [ ]
      ("< Angled brackets" . (60 62))        ; < >
      ("« Εισαγωγικά Gr quote" . (171 187))  ; « »
      ("= Equals signs" . (61 61))           ; = =
      ("* Asterisks" . (42 42))              ; * *
      ("_ underscores" . (95 95)))           ; _ _
    "Alist of pairs for use with `prot/insert-pair-completion'.")

  (defun prot/insert-pair-completion (&optional arg)
    "Insert pair from `prot/insert-pair-alist'."
    (interactive "P")
    (let* ((data prot/insert-pair-alist)
           (chars (mapcar #'car data))
           (choice (completing-read "Select character: " chars nil t))
           (left (cadr (assoc choice data)))
           (right (caddr (assoc choice data))))
      (insert-pair arg left right)))

  (defun prot/text-mode-heading (&optional arg)
    "Insert equal length heading delimiter below current line.

A heading delimiter is drawn as a series of dashes (-).  With
optional ARG, i.e. by prefixing \\[universal-argument], draw the
heading delimiter with equals signs (=).  The latter is
considered a heading level 1, while the former is level 2.

A heading delimiter is inserted only when that would not mess up
with existing headings or lists.  In such cases, point will move
to the next line.  For the purposes of this command, text that
starts with a number and no further delimiter is not consider a
list element.

This command is meant to be used in `text-mode' buffers and
derivatives, such as `markdown-mode', though not in `org-mode'."
    (interactive "P")
    (cond
     ((eq major-mode 'org-mode)
      (user-error "Do not use `prot/text-mode-heading' in `org-mode'!"))
     ((derived-mode-p 'text-mode)
      (let* ((count (- (point-at-eol) (point-at-bol)))
             (char (string-to-char (if arg "=" "-"))))
          (cond
           ((and (eobp)
                 (or (prot/text-list-line-p 1)
                     (prot/text-heading-line-p 1)
                     (prot/empty-line-p 1)
                     (prot/indent-line-p 1)))
            (newline 1))
           ((or (prot/empty-line-p 1)
                (prot/indent-line-p 1))
            (prot/new-line-below))
           ((or (prot/text-list-line-p 1)
                (prot/text-heading-line-p 2))
            (if (prot/empty-line-p 3)
                (beginning-of-line 3)
              (prot/new-line-below)))
           (t
            (prot/new-line-below)
            (insert-char char count nil)
            (newline 2)))))))

  :bind (("C-'" . prot/insert-pair-completion)
         ("M-'" . prot/insert-pair-completion)
         ("<C-M-backspace>" . backward-kill-sexp)
         ("M-c" . capitalize-dwim)
         ("M-l" . downcase-dwim)        ; "lower" case
         ("M-u" . upcase-dwim)
         :map text-mode-map
         ("<M-return>" . prot/text-mode-heading)))

4.1.3 Custom commands for object transposition

Transpose or swap objects
This is a series of functions that are constructed from the prot/transpose macro. They concern larger text objects: sentences, paragraphs, balanced expressions. The idea is to do a normal transposition of the text objects they operate on or, when the region is active, to swap the object at mark (region beginning) with the one at point (region end).
Transpose or swap words
prot/transpose-words follows the same principles as the other "swap" commands. Though it differs from the built-in transpose-words while at the beginning or end of line where it will only transpose the first or last two words after or before point. It thus avoids transposing words across lines or paragraphs, which I never want.
Transpose characters
prot/transpose-chars simply tweak the way the original command works, so that it always transposes the two characters before point. This will no longer have the effect of moving the character forward following repeated invocations of the command. Just a toggle to quickly fix the last typo.
(use-package emacs
  :commands (prot/transpose-chars
             prot/transpose-lines
             prot/transpose-paragraphs
             prot/transpose-sentences
             prot/transpose-sexps
             prot/transpose-words)
  :config
  (defmacro prot/transpose (name scope &optional doc)
    "Macro to produce transposition functions.
NAME is the function's symbol.  SCOPE is the text object to
operate on.  Optional DOC is the function's docstring.

Transposition over an active region will swap the object at
mark (region beginning) with the one at point (region end)"
    `(defun ,name (arg)
       ,doc
       (interactive "p")
       (let ((x (format "%s-%s" "transpose" ,scope)))
         (if (use-region-p)
             (funcall (intern x) 0)
           (funcall (intern x) arg)))))

  (prot/transpose
   prot/transpose-lines
   "lines"
   "Transpose lines or swap over active region.")

  (prot/transpose
   prot/transpose-paragraphs
   "paragraphs"
   "Transpose paragraphs or swap over active region.")

  (prot/transpose
   prot/transpose-sentences
   "sentences"
   "Transpose sentences or swap over active region.")

  (prot/transpose
   prot/transpose-sexps
   "sexps"
   "Transpose balanced expressions or swap over active region.")

  (defun prot/transpose-chars ()
    "Always transposes the two characters before point.
There is no 'dragging' the character forward.  This is the
behaviour of `transpose-chars' when point is at end-of-line."
    (interactive)
    (transpose-chars -1)
    (forward-char))

  (defun prot/transpose-words (arg)
    "Transpose words.

If region is active, swap the word at mark (region beginning)
with the one at point (region end).

Otherwise, and while inside a sentence, this behaves as the
built-in `transpose-words', dragging forward the word behind the
point.  The difference lies in its behaviour at the end or
beginnning of a line, where it will always transpose the word at
point with the one behind or ahead of it (effectively the
last/first two words)."
    (interactive "p")
    (cond
     ((use-region-p)
      (transpose-words 0))
     ((eq (point) (point-at-eol))
      (transpose-words -1))
     ((eq (point) (point-at-bol))
      (forward-word 1)
      (transpose-words 1))
     (t
      (transpose-words arg))))

  :bind (("C-t" . prot/transpose-chars)
         ("C-x C-t" . prot/transpose-lines)
         ("C-S-t" . prot/transpose-paragraphs)
         ("C-x M-t" . prot/transpose-sentences)
         ("C-M-t" . prot/transpose-sexps)
         ("M-t" . prot/transpose-words)))

4.1.4 Custom commands for marking syntactic constructs

I tried working with the external expand.el package, but realised I only ever used a tiny subset of its commands. Instead of paying the penalty, I am opting to tweak the built-in functions for marking the syntactic constructs that are specific to my needs.

All of the following accept a universal argument for specifying the total count of objects to mark and in which direction (e.g. C-u -3 will mark the current word and two more before it). Repeated calls of the command incrementally expand the region in the given direction.

Mark symbol at point
Marks the whole symbol at point. Recall that "symbol" refers to a proper/unique name, such as those of functions. That granted, prot/mark-symbol will apply to regular words if no symbol is available, making it ideal as a general purpose utility.
Mark word at point
Unlike the built-in mark-word which only highlights the word from point to either of its boundaries, my prot/mark-word operates on the entire word, while retaining the rest of the functionality (the use of the universal argument and incremental expansion, noted above). This command is not meant for regular use, as it is covered by prot/mark-symbol. Instead it is reserved for some surgical intervention in the context of keyboard macros and the like.
Mark sexp backward
prot/mark-sexp-backward is a simple tweak over the default command for marking symbolic expressions (mark-sexp), which tells it to always move backward.
Mark constructs DWIM
prot/mark-construct-dwim is the command that wraps all of the above except prot/mark-word (due to its specificity) in a convenient package.
(use-package emacs
  :commands (prot/mark-symbol
             prot/mark-sexp-backward)
  :config
  (defmacro prot/mark (name object &optional docstring)
    "Produce function for marking small syntactic constructs.
NAME is how the function should be called.  OBJECT is its scope.
Optional DOCSTRING describes the resulting function.

This is a slightly modified version of the built-in `mark-word'."
    `(defun ,name (&optional arg allow-extend)
       ,docstring
       (interactive "P\np")
       (let ((x (format "%s-%s" "forward" ,object)))
         (cond ((and allow-extend
                     (or (and (eq last-command this-command) (mark t))
                         (region-active-p)))
                (setq arg (if arg (prefix-numeric-value arg)
                            (if (< (mark) (point)) -1 1)))
                (set-mark
                 (save-excursion
                   (goto-char (mark))
                   (funcall (intern x) arg)
                   (point))))
               (t
                (let ((bounds (bounds-of-thing-at-point (intern ,object))))
                  (unless (consp bounds)
                    (user-error "No %s at point" ,object))
                  (if (>= (prefix-numeric-value arg) 0)
                      (goto-char (car bounds))
                    (goto-char (cdr bounds)))
                  (push-mark
                   (save-excursion
                     (funcall (intern x) (prefix-numeric-value arg))
                     (point)))
                  (activate-mark)))))))

  (prot/mark
   prot/mark-word
   "word"
   "Mark the whole word at point.
This function is a slightly modified version of the built-in
`mark-word', that I intend to use only in special circumstances,
such as when recording a keyboard macro where precision is
required.  For a general purpose utility, use `prot/mark-symbol'
instead.")

  (prot/mark
   prot/mark-symbol
   "symbol"
   "Mark the whole symbol at point.
With optional ARG, mark the current symbol and any remaining
ARGth symbols away from point.  A negative argument moves
backward. Repeated invocations of this command mark the next
symbol in the direction originally specified.

In the absence of a symbol and if a word is present at point,
this command will operate on it as described above.")

  (defun prot/mark-sexp-backward (&optional arg)
    "Mark previous or ARGth balanced expression[s].
Just a convenient backward-looking `mark-sexp'."
    (interactive "P")
    (if arg
        (mark-sexp (- arg) t)
      (mark-sexp (- 1) t)))

  (defun prot/mark-construct-dwim (&optional arg)
    "Mark symbol or balanced expression at point.
A do-what-I-mean wrapper for `prot/mark-sexp-backward',
`mark-sexp', and `prot/mark-symbol'.

When point is over a symbol, mark the entirety of it.  Regular
words are interpreted as symbols when an actual symbol is not
present.

For balanced expressions, a backward match will happen when point
is to the right of the closing delimiter.  A forward match is the
fallback condition and should work when point is before a
balanced expression, with or without whitespace in between it an
the opening delimiter.

Optional ARG will mark a total of ARGth objects while counting
the current one (so 3 would be 1+2 more).  A negative count moves
the mark backward (though that would invert the backward-moving
sexp matching of `prot/mark-sexp-backward', so be mindful of
where the point is).  Repeated invocations of this command
incrementally mark objects in the direction originally
specified."
    (interactive "P")
    (cond
     ((symbol-at-point)
      (prot/mark-symbol arg t))
     ((eq (point) (cdr (bounds-of-thing-at-point 'sexp)))
      (prot/mark-sexp-backward arg))
     (t
      (mark-sexp arg t))))

  :bind (("M-@" . prot/mark-word)       ; replaces `mark-word'
         ("C-M-SPC" . prot/mark-construct-dwim)))

4.2 Go to actionable beginning or end of buffer (beginend.el)

This package by Damien Cassou offers the ability to move to the first or last actionable point in a buffer rather than the absolute maximum or minimum point. It does so by wrapping M-< and M-> around a "do what I mean" behaviour where the initial command will take you to the actionable part, while another call will go to the absolute position. Nice and simple!

Check the package upstream for information on the supported modes and on how to contribute your own extensions (I provided support for rg.el, which I define elsewhere in this document).

Here the dolist combined with the :diminish keyword are meant to remove all lighters that this package produces: one for every minor mode it provides. I got this from issue 43 on the beginend repo.

(use-package beginend
  :ensure
  :demand
  :diminish beginend-global-mode
  :config
  (dolist (mode beginend-modes) (diminish (cdr mode)))
  (beginend-global-mode 1))

4.3 Go to last change

I could not find any built-in method of reliably moving back to the last change. Using the mark ring is always an option, but does not fill the exact same niche.

The C-z binding is disabled elsewhere in this document. It minimises the Emacs GUI by default. A complete waste of an extremely valuable key binding!

(use-package goto-last-change
  :ensure
  :bind ("C-z" . goto-last-change))

5 Directory, buffer, window management

5.1 Dired (directory editor, file manager)

The directory editor abbreviated as "Dired" (which I pronounce like "tired", "mired", etc.) is a built-in tool that performs file management operations inside of an Emacs buffer. It is simply superb! I use it daily for a number of tasks.

You can interactively copy, move (rename), symlink, delete files and directories, handle permissions, compress or extract archives, run shell commands, combine Dired with regular editing capabilities as part of a keyboard macro, search[+replace] across multiple files, encrypt/decrypt files, and more. Combine that with the possibility of matching items with regular expressions or creating an editable Dired buffer and you have everything you need to maximise your productivity.

Check some of my videos:

5.1.1 Base settings for Dired

The options here are meant to do the following:

  • Copy and delete recursively. Do not ask about it.
  • Search only file names while point is there, else the rest (useful when using the detailed view).
  • Deletion sends items to the system's Trash, making it safer than the standard rm.
  • Prettify output. Sort directories first. Show dotfiles first. Omit implicit directories (the single and double dots). Use human-readable size units. There are also options for tweaking the behaviour of find-name-dired, in the same spirit. To learn everything about these switches, you need to read the manpage of ls. You can do it with M-x man RET ls.
  • Hide all the verbose details by default (permissions, size, etc.). These can easily be toggled on using the left parenthesis ( inside a dired buffer. Also enable highlighting of the current line, which makes it even easier to spot the current item (I do not enable this globally, because I only want it for per-line interfaces, such as Dired's, but not for per-character ones, such as text editing).
  • While having two dired buffers side-by-side, the rename and copy operations of one are easily propagated to the other. Dired is smart about your intentions and uses the adjacent Dired buffer's path as a prefix when performing such actions.
  • For Emacs 27.1, Dired can automatically create destination directories for its copy and remove operations. So you can, for example, rename file to /non-existent-path/file and you will get what you want right away.
  • For Emacs 27.1, renaming a file of a version-controlled repository (git) will be done using the appropriate VC mechanism.
  • Let the relevant find commands use case-insensitive names.
  • Enable asynchronous mode. This is subject to change, as I need to test it a bit more.

Note that dired-listing-switches and find-ls-option are configured to show hidden directories and files before their non-hidden counterparts. If you want to reverse this order, you must append the -X option (such as -AFXhlv --group-directories-first).

The commands with the contrib/ prefix in dired-aux are copied from the Emacs configurations of Omar Antolín Camarena. They let you insert the path of a bookmarked directory while performing an action such as copying and renaming/moving a file. While my prot/dired-fd-* are conceptually similar to functions in projects and directory trees as well as the ones in the Ibuffer section.

Finally, to see how I get commits for marked files that belong to a given version-controlled (git) repo, see my prot/vc-print-log from the section of this document on the Generic version control (VC) framework.

(use-package dired
  :config
  (setq dired-recursive-copies 'always)
  (setq dired-recursive-deletes 'always)
  (setq delete-by-moving-to-trash t)
  (setq dired-listing-switches
        "-AGFhlv --group-directories-first --time-style=long-iso")
  (setq dired-dwim-target t)
  ;; Note that the the syntax for `use-package' hooks is controlled by
  ;; the `use-package-hook-name-suffix' variable.  The "-hook" suffix is
  ;; not an error of mine.
  :hook ((dired-mode-hook . dired-hide-details-mode)
         (dired-mode-hook . hl-line-mode)))

(use-package dired-aux
  :config
  (setq dired-isearch-filenames 'dwim)
  ;; The following variables were introduced in Emacs 27.1
  (setq dired-create-destination-dirs 'ask)
  (setq dired-vc-rename-file t)

  (defmacro prot/dired-fd (name doc prompt &rest flags)
    "Make commands for selecting 'fd' results with completion.
NAME is how the function should be named.  DOC is the function's
documentation string.  PROMPT describes the scope of the query.
FLAGS are the command-line arguments passed to the 'fd'
executable, each of which is a string."
    `(defun ,name (&optional arg)
       ,doc
       (interactive "P")
       (let* ((vc (vc-root-dir))
              (dir (expand-file-name (or vc default-directory)))
              (regexp (read-regexp
                       (format "%s matching REGEXP in %s: " ,prompt
                               (propertize dir 'face 'bold))))
              (names (process-lines "fd" ,@flags regexp dir))
              (buf "*FD Dired*"))
         (if names
             (if arg
                 (dired (cons (generate-new-buffer-name buf) names))
               (icomplete-vertical-do ()
                 (find-file
                  (completing-read (format "Items matching %s (%s): "
                                           (propertize regexp 'face 'success)
                                           (length names))
                                   names nil t)))))
         (user-error (format "No matches for « %s » in %s" regexp dir)))))

  (prot/dired-fd
   prot/dired-fd-dirs
   "Search for directories in VC root or PWD.
With \\[universal-argument] put the results in a `dired' buffer.
This relies on the external 'fd' executable."
   "Subdirectories"
   "-i" "-H" "-a" "-t" "d" "-c" "never")

  (prot/dired-fd
   prot/dired-fd-files-and-dirs
   "Search for files and directories in VC root or PWD.
With \\[universal-argument] put the results in a `dired' buffer.
This relies on the external 'fd' executable."
   "Files and dirs"
    "-i" "-H" "-a" "-t" "d" "-t" "f" "-c" "never")

  (defun contrib/cdb--bookmarked-directories ()
    (bookmark-maybe-load-default-file)
    (cl-loop for (name . props) in bookmark-alist
             for fn = (cdr (assq 'filename props))
             when (and fn (string-suffix-p "/" fn))
             collect (cons name fn)))

  (defun contrib/cd-bookmark (bm)
    "Insert the path of a bookmarked directory."
    (interactive
     (list (let ((enable-recursive-minibuffers t))
             (completing-read
              "Directory: " (contrib/cdb--bookmarked-directories) nil t))))
    (when (minibufferp)
      (delete-region (minibuffer-prompt-end) (point-max)))
    (insert (cdr (assoc bm (contrib/cdb--bookmarked-directories)))))

  :bind (("M-s d" .  prot/dired-fd-dirs)
         ("M-s z" . prot/dired-fd-files-and-dirs)
         :map dired-mode-map
         ("C-+" . dired-create-empty-file)
         ("M-s f" . nil)
         :map minibuffer-local-filename-completion-map
         ("C-c d" . contrib/cd-bookmark)))

;; NOTE de facto deprecated because of my `prot/dired-fd' functions
(use-package find-dired
  :disabled
  :after dired
  :config
  (setq find-ls-option
        '("-ls" . "-AGFhlv --group-directories-first --time-style=long-iso"))
  (setq find-name-arg "-iname"))

(use-package async :ensure)

(use-package dired-async
  :after (dired async)
  :hook (dired-mode-hook . dired-async-mode))

Pro tip while renaming or copying a file, M-n will return its original name, thus allowing you to easily {pre,ap}pend to it. This leverages an intriguing concept of Emacs' design called "future history" (because M-p goes back to your previous entries). The notion of the future history, when applied, is basically an educated guess of what the user would want to do in the current context, given that they are not searching through their previous actions.

5.1.1.1 TODO extend core Dired [1/3]
5.1.1.1.1 DONE front-end for `fd' executable
5.1.1.1.2 TODO better handling of `xdg-open'
5.1.1.1.3 TODO combine arbitrary dirs with selection

5.1.2 Narrowed dired

The standard way to produce a Dired buffer with only a handful of files is to mark them, either manually or with % m, then toggle the mark with t, and then remove (just from the view) everything with k. This will leave you with only the files you need to focus on. With g you get back to the unfiltered listing.

For dynamic filtering, use this package. It offers several commands, but I find that I only ever need to narrow by a regular expression (check the source code for all of them M-x find-library dired-narrow).

(use-package dired-narrow
  :ensure
  :after dired
  :config
  (setq dired-narrow-exit-when-one-left t)
  (setq dired-narrow-enable-blinking t)
  (setq dired-narrow-blink-time 0.3)
  :bind (:map dired-mode-map
         ("/" . dired-narrow-regexp)))

5.1.3 wdired (writable dired)

This is the editable state of a dired buffer. You can access it with C-x C-q. Write changes to files or directories, as if it were a regular buffer, then confirm them with C-c C-c.

  • While in writable state, allow the changing of permissions.
  • While renaming a file, any forward slash is treated like a directory and is created directly upon successful exit.
(use-package wdired
  :after dired
  :commands wdired-change-to-wdired-mode
  :config
  (setq wdired-allow-to-change-permissions t)
  (setq wdired-create-parent-directories t))

5.1.4 peep-dired (file previews including images)

By default, dired does not show previews of files, while image-dired is intended for a different purpose. We just want to toggle the behaviour while inside a regular dired buffer.

(use-package peep-dired
  :ensure
  :after dired
  :config
  (setq peep-dired-cleanup-on-disable t)
  (setq peep-dired-cleanup-eagerly t)
  (setq peep-dired-enable-on-directories nil)
  (setq peep-dired-ignored-extensions
        '("mkv" "webm" "mp4" "mp3" "ogg" "iso"))
  :bind (:map dired-mode-map
              ("P" . peep-dired)))

5.1.5 image-dired (image thumbnails and previews)

This tool offers facilities for generating thumbnails out of a selection of images and displaying them in a separate buffer. An external program is needed for converting the images into thumbnails: imagemagick. Other useful external packages are optipng and sxiv. The former is for operating on PNG files, while the latter is a lightweight image viewer.

I feel this process is a bit cumbersome and can be very slow if you try to generate lots of images at once. The culprit is the image converter.

(use-package image-dired
  :config
  (setq image-dired-external-viewer "xdg-open")
  (setq image-dired-thumb-size 80)
  (setq image-dired-thumb-margin 2)
  (setq image-dired-thumb-relief 0)
  (setq image-dired-thumbs-per-row 4)
  :bind (:map image-dired-thumbnail-mode-map
              ("<return>" . image-dired-thumbnail-display-external)))

5.1.6 dired-subtree (tree-style view/navigation)

Tree-style navigation means that the subdirectories of the current Dired buffer can be expanded and contracted in place. It then is possible to perform the same kind of folding on their subdirectories, and so on.

This is, in my opinion, a far more intuitive interaction than the default way of inserting subdirectories in the current buffer below their parent (type i over the target dir). There still are uses for that technique, but tree-style navigation is easier for day-to-day tasks.

What I have here:

  • The tab key will expand or contract the subdirectory at point.
  • C-TAB will behave just like org-mode handles its headings: hit it once to expand a subdir at point, twice to do it recursively, thrice to contract the tree.
  • I also have Shift-TAB for contracting the subtree when the point is inside of it.
(use-package dired-subtree
  :ensure
  :after dired
  :config
  (setq dired-subtree-use-backgrounds nil)
  :bind (:map dired-mode-map
              ("<tab>" . dired-subtree-toggle)
              ("<C-tab>" . dired-subtree-cycle)
              ("<S-iso-lefttab>" . dired-subtree-remove)))

5.1.7 dired-x (extra Dired functions)

These are some additional features that are shipped with Emacs. The one I need the most is dired-jump and its "other window" variant. These are among my favourite commands. They will always take you to the directory that contains the current buffer.

'Jumping' works even when you are inside buffers that do not visit files, such as Magit, Diff, or Eshell. This is its most valuable quality! Edit a file then proceed to do some file management, then invoke previous-buffer or winner-undo to go back to where you were (I have a few key bindings for those in the Window configuration section). Everything happens naturally. Emacs' interconnectedness at its best!

I keep dired-clean-confirm-killing-deleted-buffers to t as a safety mechanism: if a file is ever deleted by accident I can use its buffer to restore it. Never happened in practice, but still.

While in dired-mode, if you need to open all marked files at once, you can hit F. It calls dired-do-find-marked-files.

Note that contrib/cdb--bookmarked-directories is defined further above, in the section about the basic Dired settings.

(use-package dired-x
  :after dired
  :config
  (setq dired-clean-up-buffers-too t)
  (setq dired-clean-confirm-killing-deleted-buffers t)
  (setq dired-x-hands-off-my-keys t)    ; easier to show the keys I use
  (setq dired-bind-man nil)
  (setq dired-bind-info nil)

  (defun prot/dired-jump-extra (&optional arg)
    "Switch directories comprising context and bookmarks."
    (interactive "P")
    (let* ((vc (vc-root-dir))
           (buf-name (buffer-file-name))
           (path (or buf-name default-directory))
           (file (abbreviate-file-name path))
           (bookmarks (mapcar (lambda (b)
                                (cdr b))
                              (contrib/cdb--bookmarked-directories)))
           (collection (append bookmarks
                             (list (file-name-directory file)
                                   (when vc vc))))
           (files (cl-remove-if (lambda (f)
                                  (eq f nil))
                                collection)))
      (icomplete-vertical-do ()
        (dired
         (completing-read "Jump to context or bookmark: " files nil t)))))

  :bind (("C-c j" . prot/dired-jump-extra)
         ("C-x C-j" . dired-jump)
         ("s-j" . dired-jump)
         ("C-x 4 C-j" . dired-jump-other-window)
         ("s-J" . dired-jump-other-window)
         :map dired-mode-map
         ("I" . dired-info)))

5.1.8 dired-like view for the trash directory

trashed applies the principles of dired to the management of the user's filesystem trash. Use C-h m to see the docs and keybindings for its major mode.

Basically, its interaction model is as follows:

  • m to mark for some deferred action, such as D to delete, R to restore.
  • t to toggle the status of all items as marked. Use this without marks to m (mark) all items, then call a deferred action to operate on them.
  • d to mark for permanent deletion.
  • r to mark for restoration.
  • x to execute these special marks.
(use-package trashed
  :ensure
  :config
  (setq trashed-action-confirmer 'y-or-n-p)
  (setq trashed-use-header-line t)
  (setq trashed-sort-key '("Date deleted" . t))
  (setq trashed-date-format "%Y-%m-%d %H:%M:%S"))

5.1.9 Dired disk usage (du)

Sometimes I need to check the size of directories listed in a Dired buffer. I do this manually, by invoking dired-du-mode.

(use-package dired-du
  :ensure
  :commands dired-du-mode)

5.2 Working with buffers

5.2.1 General buffer commands

Here I collect general purpose commands for buffers. I also remap useful keybindings to commands I prefer to use.

  • By default, M-= will count the lines, words, characters in the region. I never use that. What I do use is the equivalent for the whole buffer, which is automatically adapted to the contents of the region, when active.
  • prot/kill-buffer-current is a faster way to kill the focused buffer, without further confirmation. Unsaved files will still ask for confirmation though, which is exactly as it should be.
  • contrib/rename-file-and-buffer will prompt you to rename the current buffer and its underlying file. It is a command I extracted from the Crux package.
(use-package emacs
  :config
  (defun prot/kill-buffer-current (&optional arg)
    "Kill current buffer or abort recursion when in minibuffer."
    (interactive "P")
    (if (minibufferp)
        (abort-recursive-edit)
      (kill-buffer (current-buffer)))
    (when (and arg
               (not (one-window-p)))
      (delete-window)))

  (defun contrib/rename-file-and-buffer ()
    "Rename current buffer and, if available, its file."
    (interactive)
    (let ((filename (buffer-file-name)))
      (if (not (and filename (file-exists-p filename)))
          (rename-buffer (read-from-minibuffer "New name: " (buffer-name)))
        (let* ((new-name (read-from-minibuffer "New name: " filename))
               (containing-dir (file-name-directory new-name)))
          (make-directory containing-dir t)
          (cond
           ((vc-backend filename) (vc-rename-file filename new-name))
           (t
            (rename-file filename new-name t)
            (set-visited-file-name new-name t t)))))))
  :bind (("M-=" . count-words)
         ("<C-f2>" . contrib/rename-file-and-buffer)
         ("s-k" . prot/kill-buffer-current)))

5.2.2 Unique names for buffers

These settings make it easier to work with multiple buffers. When two buffers have the same name, Emacs will try to disambiguate them by displaying their unique path inside angled brackets. With the addition of uniquify-strip-common-suffix it will also remove the part of the file system path they have in common.

All such operations are reversed once an offending buffer is removed from the list, allowing Emacs to revert to the standard of displaying only the buffer's name.

(use-package uniquify
  :config
  (setq uniquify-buffer-name-style 'post-forward-angle-brackets)
  (setq uniquify-strip-common-suffix t)
  (setq uniquify-after-kill-buffer-p t))
5.2.2.1 TODO make uniquify better for Help, Info buffers

5.2.3 Ibuffer and extras (dired-like buffer list manager)

ibuffer is a built-in replacement for list-buffers that allows for fine-grained control over the buffer list. For this reason I bind it to C-x C-b.

Overview of its features:

  • mark and delete buffers same way you do in dired (see the previous sections on dired (directory editor, file manager));
  • mark by a predicate, such as name, major mode, etc.;
  • sort buffers by name, filesystem path, major mode, size;
  • run occur on the marked buffers (remember: Occur produces a buffer that you can edit once you enable the editable state with e);
  • run query-replace on marked buffers or its regular-expression-aware equivalent.

Run the universal help command for major mode documentation (C-h m) while inside ibuffer to get a detailed list of all available commands and their key bindings.

With regard to the following package declaration, these are my tweaks to the default behaviour and presentation:

  • Prompt for confirmation only when deleting a modified buffer.
  • Hide the summary.
  • Do not open on the other window (not focused window).
  • Do not show empty filter groups.
  • Do not cycle movements. So do not go to the top when moving downward at the last item on the list.

Also watch my introduction to Ibuffer (2020-04-02).

Now some extras after I published that video: prot/buffers-major-mode and prot/buffers-vc-root will produce a filtered list based on the current buffer's major mode or root version-control directory respectively. Their standard mode of interaction is through minibuffer completions (start with Tools for completion candidate listing). With a prefix argument C-u they switch to a dedicated Ibuffer view instead.

For those two I received help from Omar Antolín Camarena with regard to the use of read-buffer and the lambda passed to it: my prototype was using the generic completing-read with cl-remove-if-not for filtering the list of candidates (all possible errors are my own). The upside of using Omar's approach is that it informs other tools that this kind of completion concerns buffers, so they can adapt accordingly.

I consider these two commands to be conceptually related to what I have defined in Completion for projects and directory trees. The key chords follow the same pattern, with the M-s prefix being considered an entry point for "advanced search" methods, inspired by the default for occur (M-s o) as well as a few others.

(use-package ibuffer
  :config
  (setq ibuffer-expert t)
  (setq ibuffer-display-summary nil)
  (setq ibuffer-use-other-window nil)
  (setq ibuffer-show-empty-filter-groups nil)
  (setq ibuffer-movement-cycle nil)
  (setq ibuffer-default-sorting-mode 'filename/process)
  (setq ibuffer-use-header-line t)
  (setq ibuffer-default-shrink-to-minimum-size nil)
  (setq ibuffer-formats
        '((mark modified read-only locked " "
                (name 30 30 :left :elide)
                " "
                (size 9 -1 :right)
                " "
                (mode 16 16 :left :elide)
                " " filename-and-process)
          (mark " "
                (name 16 -1)
                " " filename)))
  (setq ibuffer-saved-filter-groups nil)
  (setq ibuffer-old-time 48)

  (defun prot/buffers-major-mode (&optional arg)
    "Select buffers that match the current buffer's major mode.
With \\[universal-argument] produce an `ibuffer' filtered
accordingly.  Else use standard completion."
    (interactive "P")
    (let* ((major major-mode)
           (prompt "Buffers for ")
           (mode-string (format "%s" major))
           (mode-string-pretty (propertize mode-string 'face 'success)))
      (if arg
          (ibuffer t (concat "*" prompt mode-string "*")
                   (list (cons 'used-mode major)))
        (switch-to-buffer
         (read-buffer
          (concat prompt mode-string-pretty ": ") nil t
          (lambda (pair) ; pair is (name-string . buffer-object)
            (with-current-buffer (cdr pair) (derived-mode-p major))))))))

  (defun prot/buffers-vc-root (&optional arg)
    "Select buffers that match the present `vc-root-dir'.
With \\[universal-argument] produce an `ibuffer' filtered
accordingly.  Else use standard completion.

When no VC root is available, use standard `switch-to-buffer'."
    (interactive "P")
    (let* ((root (vc-root-dir))
           (prompt "Buffers for VC ")
           (vc-string (format "%s" root))
           (vc-string-pretty (propertize vc-string 'face 'success)))
      (if root
          (if arg
              (ibuffer t (concat "*" prompt vc-string "*")
                       (list (cons 'filename (expand-file-name root))))
            (switch-to-buffer
             (read-buffer
              (concat prompt vc-string-pretty ": ") nil t
              (lambda (pair) ; pair is (name-string . buffer-object)
                (with-current-buffer (cdr pair) (string= (vc-root-dir) root))))))
        (call-interactively 'switch-to-buffer))))

  :hook (ibuffer-mode-hook . hl-line-mode)
  :bind (("M-s b" . prot/buffers-major-mode)
         ("M-s v" . prot/buffers-vc-root)
         ("C-x C-b" . ibuffer)
         :map ibuffer-mode-map
         ("* f" . ibuffer-mark-by-file-name-regexp)
         ("* g" . ibuffer-mark-by-content-regexp) ; "g" is for "grep"
         ("* n" . ibuffer-mark-by-name-regexp)
         ("s n" . ibuffer-do-sort-by-alphabetic)  ; "sort name" mnemonic
         ("/ g" . ibuffer-filter-by-content)))
5.2.3.1 Ibuffer integration with VC (version control framework)

This package offers a few functions for operating on ibuffer items based on their corresponding version control data.

To me the most common case is to establish filter groups on a per-project basis with ibuffer-vc-set-filter-groups-by-vc-root. This makes it easier to get an overview of where each buffer belongs, especially in cases where you have similar looking names.

In terms of functionality, filter groups allow for per-group actions:

  • Move between group headings with M-n and M-p.
  • Toggle the visibility of the group with RET, while the point is over the heading.
  • With point over a heading, m will mark all its buffers, while d will mark them for deletion (confirm the latter with x, same as with dired).

The above granted, I prefer to create such filter groups manually via a convenient key binding, rather than calling the function through a relevant hook.

Make sure to also read the section on version control tools.

(use-package ibuffer-vc
  :ensure
  :after (ibuffer vc)
  :bind (:map ibuffer-mode-map
              ("/ V" . ibuffer-vc-set-filter-groups-by-vc-root)
              ("/ <deletechar>" . ibuffer-clear-filter-groups)))

5.2.4 Scratch buffers per-major-mode

This package will produce a buffer that matches the major mode of the one you are currently in. Use it with M-x scratch. Doing that with a prefix argument (C-u) will prompt for a major mode instead. Simple yet super effective!

The prot/scratch-buffer-setup simply adds some text in the buffer and renames it appropriately for the sake of easier discovery. I got the idea of copying the region from a snippet shared by eev2 on GitHub.

(use-package scratch
  :ensure
  :config
  (defun prot/scratch-buffer-setup ()
  "Add contents to `scratch' buffer and name it accordingly.
If region is active, add its contents to the new buffer."
  (let* ((mode major-mode)
         (string (format "Scratch buffer for: %s\n\n" mode))
         (region (with-current-buffer (current-buffer)
                     (if (region-active-p)
                         (buffer-substring-no-properties
                          (region-beginning)
                          (region-end)))
                     ""))
         (text (concat string region)))
    (when scratch-buffer
      (save-excursion
        (insert text)
        (goto-char (point-min))
        (comment-region (point-at-bol) (point-at-eol)))
      (forward-line 2))
    (rename-buffer (format "*Scratch for %s*" mode) t)))
  :hook (scratch-create-buffer-hook . prot/scratch-buffer-setup)
  :bind ("C-c s" . scratch))

5.3 Window configuration

I believe that Emacs' true power lies in its buffer management rather than its multiplexing. The latter becomes inefficient at scale, since it tries to emulate the limitations of the real world, namely, the placement of things on a desk.

By leveraging the power of the computer, we can use search methods to easily reach any item. There is no need to remain confined to the idea of a finite space (screen real estate) that needs to be carefully managed.

That granted, Emacs' multiplexing can be turned into a powerhouse as well, covering everything from window placement rules, to the recording of history and layouts, as well as directional or direct window navigation.

The prot/window-single-toggle is based on Pierre Neidhardt's windower.

(use-package emacs
  :config
  (defvar prot/window-configuration nil
    "Current window configuration.
Intended for use by `prot/window-monocle'.")

  (define-minor-mode prot/window-single-toggle
    "Toggle between multiple windows and single window.
This is the equivalent of maximising a window.  Tiling window
managers such as DWM, BSPWM refer to this state as 'monocle'."
    :lighter " [M]"
    :global nil
    (if (one-window-p)
        (when prot/window-configuration
          (set-window-configuration prot/window-configuration))
      (setq prot/window-configuration (current-window-configuration))
      (delete-other-windows)))
  :bind ("s-m" . prot/window-single-toggle))

5.3.1 Window rules and basic tweaks

The display-buffer-alist and all other functions grouped together with prot/window-dired-vc-root-left are considered experimental and subject to review. The former is intended as a rule-set for controlling the display of windows. While the latter serves as a series of tangible examples of passing certain rules programmatically, in combination with a few relevant extras. The objective is to create a more intuitive workflow where targeted buffer groups or types are always shown in a given location, on the premise that predictability improves usability.

For each buffer action in display-buffer-alist we can define several functions for selecting the appropriate window. These are executed in sequence, but my usage thus far suggests that a simpler method is just as effective for my case.

Everything pertaining to buffer actions is documented at length in the GNU Emacs Lisp Reference Manual, currently corresponding to version 26.3. Information can also be found via C-h f display-buffer and, for my settings, C-h f display-buffer-in-side-window.

With regard to the contents of the :bind keyword of the window library, most key combinations are complementary to the standard ones, such as C-x 1 becoming s-1, C-x o turning into s-o and the like. They do not replace the defaults: they just provide more convenient access to their corresponding functions. They all involve the Super key, following the norms described in the introductory note on the matter. Concerning the balance-windows-area I find that it is less intrusive than the original balance-windows normally bound to the same C-x +.

For a demo of the display-buffer-alist and the functions that accompany it, watch my video on rules for buffer placement (2020-01-07).

(use-package window
  :init
  (setq display-buffer-alist
        '(;; top side window
          ("\\*Bongo-Elfeed Queue.*"
           (display-buffer-reuse-window display-buffer-in-side-window)
           (window-height . 0.16)
           (side . top)
           (slot . -2))
          ("\\*\\(elfeed-mpv-output\\|world-clock\\).*"
           (display-buffer-in-side-window)
           (window-height . 0.16)
           (side . top)
           (slot . -1))
          ("\\*\\(Flymake\\|Package-Lint\\|vc-git :\\).*"
           (display-buffer-in-side-window)
           (window-height . 0.16)
           (side . top)
           (slot . 0)
           (window-parameters . ((no-other-window . t))))
          ("\\*Messages.*"
           (display-buffer-in-side-window)
           (window-height . 0.16)
           (side . top)
           (slot . 1)
           (window-parameters . ((no-other-window . t))))
          ("\\*\\(Backtrace\\|Warnings\\|Compile-Log\\)\\*"
           (display-buffer-in-side-window)
           (window-height . 0.16)
           (side . top)
           (slot . 2)
           (window-parameters . ((no-other-window . t))))
          ;; bottom side window
          ("\\*\\(Output\\|Register Preview\\).*"
           (display-buffer-in-side-window)
           (window-width . 0.16)       ; See the :hook
           (side . bottom)
           (slot . -1)
           (window-parameters . ((no-other-window . t))))
          (".*\\*Completions.*"
           (display-buffer-in-side-window)
           (window-height . 0.16)
           (side . bottom)
           (slot . 0)
           (window-parameters . ((no-other-window . t))))
          ("^\\(\\*e?shell\\|vterm\\).*"
           (display-buffer-in-side-window)
           (window-height . 0.16)
           (side . bottom)
           (slot . 1))
          ;; left side window
          ("\\*Help.*"
           (display-buffer-in-side-window)
           (window-width . 0.20)       ; See the :hook
           (side . left)
           (slot . 0)
           (window-parameters . ((no-other-window . t))))
          ;; right side window
          ("\\*Faces\\*"
           (display-buffer-in-side-window)
           (window-width . 0.25)
           (side . right)
           (slot . 0)
           (window-parameters
            . ((no-other-window . t)
               (mode-line-format
                . (" "
                   mode-line-buffer-identification)))))
          ("\\*Custom.*"
           (display-buffer-in-side-window)
           (window-width . 0.25)
           (side . right)
           (slot . 1))
          ;; bottom buffer (NOT side window)
          ("\\*\\vc-\\(incoming\\|outgoing\\).*"
           (display-buffer-at-bottom))))
  (setq window-combination-resize t)
  (setq even-window-sizes 'height-only)
  (setq window-sides-vertical nil)
  (setq switch-to-buffer-in-dedicated-window 'pop)
  ;; Note that the the syntax for `use-package' hooks is controlled by
  ;; the `use-package-hook-name-suffix' variable.  The "-hook" suffix is
  ;; not an error of mine.
  :hook ((help-mode-hook . visual-line-mode)
         (custom-mode-hook . visual-line-mode))
  :bind (("s-n" . next-buffer)
         ("s-p" . previous-buffer)
         ("s-o" . other-window)
         ("s-2" . split-window-below)
         ("s-3" . split-window-right)
         ("s-0" . delete-window)
         ("s-1" . delete-other-windows)
         ("s-5" . delete-frame)
         ("C-x _" . balance-windows)
         ("C-x +" . balance-windows-area)
         ("s-q" . window-toggle-side-windows)))

;; These are all experimental.  Just showcasing the power of passing
;; parameters to windows or frames.
(use-package emacs
  :commands (prot/window-dired-vc-root-left
             prot/make-frame-floating-with-current-buffer
             prot/display-buffer-at-bottom)
  :config
  (defun prot/window-dired-vc-root-left ()
    "Open project or dir `dired' in a side window.

NOTE: For demo purposes."
    (interactive)
    (let ((dir (if (eq (vc-root-dir) nil)
                   (dired-noselect default-directory)
                 (dired-noselect (vc-root-dir)))))
      (display-buffer-in-side-window
       dir `((side . left)
             (slot . -1)
             (window-width . 0.16)
             (window-parameters
              . ((no-other-window . t)
                 (no-delete-other-windows . t)
                 (mode-line-format
                  . (" "
                     mode-line-buffer-identification))))))
      (with-current-buffer dir
        (rename-buffer "*Dired-Side*")
        (setq-local window-size-fixed 'width)))
    (with-eval-after-load 'ace-window
      (when (boundp 'aw-ignored-buffers)
        (add-to-list 'aw-ignored-buffers "*Dired-Side*"))))

  (defun prot/make-frame-floating-with-current-buffer ()
    "Display the current buffer in a new floating frame.

This passes certain parameters to the newly created frame:

- use a different name than the default;
- use a graphical frame;
- do not display the minibuffer.

The name is meant to be used by the external rules of my tiling
window manager (BSPWM) to present the frame in a floating state.

NOTE: For demo purposes."
    (interactive)
    (make-frame '((name . "my_float_window")
                  (window-system . x)
                  (minibuffer . nil))))

  (defun prot/display-buffer-at-bottom ()
    "Move the current buffer to the bottom of the frame.
This is useful to take a buffer out of a side window.

NOTE: For demo purposes."
    (interactive)
    (let ((buffer (current-buffer)))
      (with-current-buffer buffer
        (delete-window)
        (display-buffer-at-bottom
         buffer
         `((window-parameters
            . ((mode-line-format
                . (" "
                   mode-line-buffer-identification))))))))))

5.3.2 Window history (winner-mode)

Winner is a built-in tool that keeps a record of buffer and window layout changes. It then allows us to move back and forth in the history of said changes. I have it enabled by default, while I assign its two main functions to Super and the right/left arrow keys.

(use-package winner
  :hook (after-init-hook . winner-mode)
  :bind (("<s-right>" . winner-redo)
         ("<s-left>" . winner-undo)))

Windmove is also built into Emacs. It provides functions for selecting a window in any of the cardinal directions. I use the Vim keys while holding down Super and Meta because other mnemonics-based actions involving just Super or Meta are already occupied.

5.3.3 Tabs for window layouts and buffers (Emacs 27+)

Starting with version 27.1, Emacs has built-in support for two distinct concepts of "tabs":

  1. Work spaces that contain windows in any given layout.
  2. A list of buffers presented as buttons at the top of the window.

The former, represented by the tab-bar library, is best understood as the equivalent of "virtual desktops", as these are used in most desktop environments or window managers.

The latter, implemented in tab-line, is the same as the tabs you are used to in web browsers. Each buffer is assigned to a single tab. Clicking on the tab takes you to the corresponding buffer.

I do not need the tab-line as I find such tabs to be inefficient at scale. Finding a buffer through search mechanisms is generally faster: it does not matter whether you have ten or a hundred buffers on the list (unless, of course, they all have similar names in which case you are in trouble either way—do not forget to check my Ibuffer settings).

On the other hand, the work spaces (tab-bar) are very useful for organising the various applications that are running inside of Emacs. You can, for example, have your current project on tab (workspace) 1, your email and news reader on 2, music on 3, and so on. Of course, this can also be achieved by using separate frames for each of these, though I generally prefer working in a single frame (plus you can define a window configuration or frameset in a register—see relevant section).

For me tabs are useful as groups of buffers in a given window configuration. I do not want a persistent bar with buttons that introduces extra visual clutter. Switching to tabs is done through completion, specifically prot/tab-bar-select-tab-dwim. This extends the configurations in my Tools for completion candidate listing. Otherwise, we can use tab-switcher which produces a buffer with the entire list, plus a basic command for marking an item for deletion (same principle as with, e.g., dired).

All settings I configure here are meant to work in accordance with this abstract conception of "tabs are work spaces". Here are the main key chords for tab-bar (they will all work properly if you keep the mode active):

Key Description
C-x t b Open a buffer in a new tab
C-x t d Open a directory in a new tab
C-x t f Open a file in a new tab
C-x t 0 Close current tab
C-x t 1 Close all other tabs
C-x t 2 Open current buffer in new tab

These are consistent with the standard commands for handling windows and accessing buffers/files in the "other window" (the C-x 4 KEY pattern). There is also a command for giving a name to the current tab, accessed via C-x t r, though I find I do not use it.

(use-package tab-bar
  :config
  (setq tab-bar-close-button-show nil)
  (setq tab-bar-close-last-tab-choice 'tab-bar-mode-disable)
  (setq tab-bar-close-tab-select 'recent)
  (setq tab-bar-new-tab-choice t)
  (setq tab-bar-new-tab-to 'right)
  (setq tab-bar-position nil)
  (setq tab-bar-show nil)
  (setq tab-bar-tab-hints nil)
  (setq tab-bar-tab-name-function 'tab-bar-tab-name-all)

  (tab-bar-mode -1)
  (tab-bar-history-mode -1)

  (defun prot/tab-bar-select-tab-dwim ()
    "Do-What-I-Mean function for getting to a `tab-bar-mode' tab.
If no other tab exists, create one and switch to it.  If there is
one other tab (so two in total) switch to it without further
questions.  Else use completion to select the tab to switch to."
    (interactive)
    (let ((tabs (mapcar (lambda (tab)
                          (alist-get 'name tab))
                        (tab-bar--tabs-recent))))
      (cond ((eq tabs nil)
             (tab-new))
            ((eq (length tabs) 1)
             (tab-next))
            (t
             (icomplete-vertical-do ()
               (tab-bar-switch-to-tab
                (completing-read "Select tab: " tabs nil t)))))))

  :bind (("C-x t t" . prot/tab-bar-select-tab-dwim)
         ("s-t" . prot/tab-bar-select-tab-dwim)
         ("<s-tab>" . tab-next)
         ("<S-s-iso-lefttab>" . tab-previous)))

;; This is only included as a reference.
(use-package tab-line
  :disabled
  :commands (tab-line-mode global-tab-line-mode)
  :config
  (global-tab-line-mode -1))
5.3.3.1 TODO tab-line for groups that make sense, such as EWW

5.3.4 Directional window motions (windmove)

While C-x o (other-window) is very useful when working with two or three windows, it can become tiresome. Thankfully, Emacs comes with a built-in package to move to a window in the given direction. I bind the cardinal directions to Super+Ctrl plus the Vim keys (heresy!), with aliases for the arrows you find on a standard keyboard's number pad.

The windmove-create-window specifies what should happen when trying to move past the edge of the frame. The idea with this is to allow it to create a new window with the contents of the current buffer. I tried it for a while but felt that the times it would interfere with my layout where more than those it would actually speed up my workflow.

(use-package windmove
  :config
  (setq windmove-create-window nil)     ; Emacs 27.1
  :bind (("C-s-k" . windmove-up)
         ("C-s-l" . windmove-right)
         ("C-s-j" . windmove-down)
         ("C-s-h" . windmove-left)
         ;; numpad keys clockwise: 8 6 2 4
         ("<kp-up>" . windmove-up)
         ("<kp-right>" . windmove-right)
         ("<kp-down>" . windmove-down)
         ("<kp-left>" . windmove-left)))

5.3.5 Transposition and rotation of windows

The transpose-frame library defines a set of commands for shifting the layout of Emacs windows. Rather than me describing how these work, I strongly encourage you to read the "Commentary" section in the source code. Do it with M-x find-library transpose-frame.

(use-package transpose-frame
  :ensure
  :commands (transpose-frame
             flip-frame
             flop-frame
             rotate-frame
             rotate-frame-clockwise
             rotate-frame-anticlockwise)
  :bind (("C-s-t" . flop-frame) ; what I consider "transpose" in this context
         ("C-s-r" . rotate-frame-clockwise)))

6 Applications and utilities

This section includes configurations for programs like email clients, news reader, music players… Anything you would normally see in a standalone application. The end goal is to eventually integrate every aspect of my computing inside of Emacs.

6.1 Calendar

Some basic settings for this tool. It is used by the diary (next section), as well as all Org-mode facilities that require date/time input (see following sections).

(use-package calendar
  :config
  (setq calendar-mark-diary-entries-flag t)
  (setq calendar-time-display-form
        '(24-hours ":" minutes
                   (when time-zone
                     (concat " (" time-zone ")"))))
  (setq calendar-week-start-day 1)      ; Monday
  (setq calendar-date-style 'iso)
  (setq calendar-christian-all-holidays-flag nil)
  (setq calendar-holidays
        (append holiday-local-holidays  ; TODO set local holidays
                holiday-solar-holidays))

  (use-package solar
    :config
    (setq calendar-latitude 35.17
          calendar-longitude 33.36))

  (use-package lunar
    :config
    (setq lunar-phase-names
          '("New Moon"
            "First Quarter Moon"
            "Full Moon"
            "Last Quarter Moon")))

  :hook (calendar-today-visible-hook . calendar-mark-today))

6.2 Diary

Emacs comes with a built-in facility to record tasks and create notifications for them. It is simply called diary. I am still assessing its overall utility in my workflow, though these configurations should be good enough at this stage.

(use-package diary-lib
  :config
  (setq diary-file "~/.emacs.d/diary")
  (setq diary-entry-marker "diary")
  (setq diary-show-holidays-flag t)
  (setq diary-header-line-flag nil)
  (setq diary-mail-addr "public@protesilaos.com")
  (setq diary-mail-days 3)
  (setq diary-number-of-entries 3)
  (setq diary-comment-start ";")
  (setq diary-comment-end "")
  (setq diary-date-forms
        '((day "/" month "[^/0-9]")
          (day "/" month "/" year "[^0-9]")
          (day " *" monthname " *" year "[^0-9]")
          (monthname " *" day "[^,0-9]")
          (monthname " *" day ", *" year "[^0-9]")
          (year "[-/]" month "[-/]" day "[^0-9]")
          (dayname "\\W"))))

6.3 Org-mode (personal information manager)

Org offers you the basic tools to organise your life in super-efficient ways using nothing but plain text.

In its purest form, Org is a markup language that is similar to Markdown: symbols are used to denote the meaning of a construct in its context, such as what may represent a headline element or a phrase that calls for emphasis.

What lends Org its super powers though is everything else built around it: a rich corpus of elisp functions that automate, link, combine, enhance, structure, or otherwise enrich the process of using this rather simple markup language. This very document is written in org-mode while its website version is produced by a function that exports Org notation to its HTML equivalent.

The present section contains several sub-sections, each dedicated to a particular aspect of Org. Unless explicitly stated, everything should be considered a work-in-progress as I gradually build up my knowledge of this killer app.

6.3.1 Org basic configurations

These are the base settings that other more specialised functions of Org depend on. Here is an overview:

Agenda and default setup
The bulk of the org-agenda configurations is defined in a subsequent section. Here we just declare the default file system paths for searching for relevant files. The "notes" file is meant as a fallback option for when org-capture has not been given a file to write to (also see the org-capture section).
Re-filing items
This is done with C-c C-w which then prompts us for a heading under which the current item should be positioned. I set my Org agenda files as one possible target and the current buffer as the other. The maximum depth should be 2 levels. Re-filing can also be done from inside an org-capture interface. Any new entry should go at the end of the heading it is filled under.
To-do settings
I generally use a very simple system of writing tasks, in the sense that I do not really care about time-tracking or assigning an intermediate state, etc. I let tags and the description further qualify the meaning. The letter inside parentheses is for faster access when using the C-c C-t method. The upside of having lots of specialised keywords is that it becomes easier to filter your tasks in the relevant agenda views.
Logging meta data
I do not really care about tracking all the minutia of why a deadline was reviewed or whatnot. Though it is kind of nice to have a timestamp of when a task was concluded (still don't care about it).

Now on to the miscellaneous settings:

  • With the t value of org-special-ctrl-a/e we assign a special meaning to the motions that take us to the beginning or end of the line when those are performed over a heading. The idea here is that we can always perform changes to the absolute beginning of the line, such as by increasing the heading's depth with M-right.
  • I do not enable this sort of contextual awareness for the C-k command, because I do consider it rather unpredictable.
  • All the markup characters should be hidden from view, in the same way links are. This generally reduces the distractions in the document.
  • The org-structure-template-alist had its value and functionality changed in Org version 9.3, which ships with Emacs 27. To insert a template you must now use C-c C-,.
  • The return key should never follow a link because I sometimes call it by accident. Use C-c C-o instead.
  • With org-loop-over-headlines-in-active-region we can perform actions such as tagging and scheduling on the items within the active region. I configure it to only apply to headings of the same level, in order to avoid possible inconveniences.
(use-package org
  ;; NOTE: I track Org from its Git source
  :load-path "/home/prot/.emacs.d/prot-dev/org-mode/lisp/"
  :config
  ;; agenda and basic directory structure
  (setq org-directory "~/Org")
  (setq org-default-notes-file "~/Org/notes.org")
  (setq org-agenda-files
        '("~/Org"
          "~/.emacs.d"
          "~/Documents"))
  ;; refile, todo
  (setq org-refile-targets
        '((org-agenda-files . (:maxlevel . 2))
          (nil . (:maxlevel . 2))))
  (setq org-refile-use-outline-path t)
  (setq org-refile-allow-creating-parent-nodes 'confirm)
  (setq org-refile-use-cache t)
  (setq org-reverse-note-order nil)
  (setq org-todo-keywords
        '((sequence "TODO(t)" "|" "DONE(D)" "CANCEL(C)")
          (sequence "MEET(m)" "|" "MET(M)")
          (sequence "STUDY(s)" "|" "STUDIED(S)")
          (sequence "WRITE(w)" "|" "WROTE(W)")))
  (setq org-todo-keyword-faces
        '(("MEET" . '(font-lock-preprocessor-face org-todo))
          ("STUDY" . '(font-lock-variable-name-face org-todo))
          ("WRITE" . '(font-lock-type-face org-todo))))
  (setq org-priority-faces
        '((?A . '(org-scheduled-today org-priority))
          (?B . org-priority)
          (?C . '(shadow org-priority))))
  (setq org-fontify-done-headline nil)
  (setq org-fontify-quote-and-verse-blocks t)
  (setq org-fontify-whole-heading-line nil)
  (setq org-fontify-whole-block-delimiter-line t)
  (setq org-enforce-todo-dependencies t)
  (setq org-enforce-todo-checkbox-dependencies t)
  (setq org-track-ordered-property-with-tag t)
  (setq org-highest-priority ?A)
  (setq org-lowest-priority ?C)
  (setq org-default-priority ?A)
  ;; tags
  (setq org-tag-alist                   ; TODO review org tag list
        '((:startgroup)
          ("@work")
          ("@priv")
          (:endgroup)
          ("emacs")
          ("modus")
          ("politics")
          ("economics")
          ("philosophy")
          ("paper")
          ("book")
          ("essay")
          ("article")
          ("mail")
          ("website")))
  ;; code blocks
  (setq org-confirm-babel-evaluate nil)
  ;; log
  (setq org-log-done 'time)
  (setq org-log-note-clock-out nil)
  (setq org-log-redeadline nil)
  (setq org-log-reschedule nil)
  (setq org-read-date-prefer-future 'time)
  ;; general
  (setq org-adapt-indentation nil)      ; No, thanks!
  (setq org-special-ctrl-a/e nil)
  (setq org-special-ctrl-k nil)
  (setq org-M-RET-may-split-line '((default . nil)))
  (setq org-hide-emphasis-markers t)
  (setq org-hide-macro-markers t)
  (setq org-hide-leading-stars nil)
  (setq org-structure-template-alist    ; CHANGED in Org 9.3, Emacs 27.1
        '(("s" . "src")
          ("E" . "src emacs-lisp")
          ("e" . "example")
          ("q" . "quote")
          ("v" . "verse")
          ("V" . "verbatim")
          ("c" . "center")
          ("C" . "comment")))
  (setq org-catch-invisible-edits 'show)
  (setq org-return-follows-link nil)
  (setq org-loop-over-headlines-in-active-region 'start-level)
  (setq org-imenu-depth 7)
  (setq org-modules '(ol-gnus ol-info ol-eww))
  :bind (:map org-mode-map
              ("<C-return>" . nil)
              ("<C-S-return>" . nil)))

6.3.2 Org links

The org-store-link is one of the commands I use the most, as it allows me to, inter alia, connect the various sections of this document. Use it to store a direct link to the heading you are currently under (also see Consistent Org heading IDs (and anchor tags)).

There are several ways to insert such links. With C-c C-l (which calls org-insert-link) you will be prompted to select a stored link from the link. It will be inserted at point, using the right markup, but will first ask you for a description text. Otherwise you can invoke C-c C-l with an active region, to create a link to that location with the selected text becoming the description. Otherwise you can just call org-insert-last-stored-link to skip the interactive process and just insert the last link.

In addition to these, org-insert-link can be used to create references on demand. Say you have a URL on the kill-ring: C-c C-l, then C-y followed by RET to confirm your input. Complete the process with a description and you are good to go.

(use-package ol
  :config
  (setq org-link-keep-stored-after-insertion t)
  :bind (("C-c l" . org-store-link)
         :map org-mode-map
         ("C-c S-l" . org-toggle-link-display)
         ("C-c C-S-l" . org-insert-last-stored-link)))

6.3.3 Org-capture templates

The org-capture tool is a powerful way to quickly produce some kind of structured information. The type of data and the way to store is determined by a system of templates which accepts a series of possible specifiers as well as the evaluated part of arbitrary elisp code.

Each template is accessed via a key. These are listed in a temporary buffer when you call org-capture. Unique keys give direct access to their template, whereas templates that share a common initial key will produce a second selection list with the remaining options. In the latter case, the initial key entry has no call to an actual function, but is just written as a heading. For an example, look how I do the "Work" section: all templates whose keys follow the pattern wX are only visible after hitting w and are then accessed via X.

The visibility of a template is further controlled by another variable: org-capture-templates-contexts. This allows us to tell Org the context in which we want certain options to appear in. Otherwise they remain concealed from our view. Equipped with this piece of functionality, we can freely write highly specialised templates that capture structured text when viewing some particular item, but are not needed for more general purposes. I do this for certain actions that only come into effect when reading email inside of the relevant gnus buffers (also check my comprehensive configurations for email and the Gnus news/mail reader).

Speaking of mail, you will notice some specifiers like :fromname. This refers to the From field in emails and will capture the name part only. Other similar keywords are :from (name and email), :fromaddress (email only), :subject.

Specifiers that start with the caret sign (^) represent prompts for further user input. The pattern ^{TEXT} is a prompt whose name is TEXT. To offer possible options, use ^{Initial|ONE|TWO|THREE}, where the first entry is the text of the prompt and all the rest are the available choices (depending on your completion framework, you may need to add an empty option as well, with ||, should you ever want to insert nothing). In some templates I use the ^t specifier, which is a built-in method to ask for a specific date.

The text that goes into a template can be written as part of a string or inside a function that is then evaluated. I generally prefer to use simple strings, though I might revise this approach going forward. To insert a new line inside of a string, use \n.

The %? determines where the point should be once the template is filled in. While %i will insert the contents of the active region.

As things currently stand, my capture templates always write to headings inside of files. Note though that there are more possibilities, as described in the manual.

A file can be specified by its absolute path or just a name. In the latter case, its location is understood relative to org-directory. When using the file+headline pattern, non-existing files are created automatically once you call the relevant template. Same for their respective headings.

Finally, the contrib/org-capture-no-delete-windows and relevant advice address a problem I have when org-capture fails to conclude its actions when called from inside of a side window (for more on those, refer to the section on Window rules and basic tweaks). The code is taken directly from this Stack Overflow thread.

Consider watching my primer on org-capture (2020-02-04) which shows all of the above in action.

(use-package org-capture
  :after org
  :config
  (setq org-capture-templates
        `(("a" "Article to write" entry
           (file+headline "tasks.org" "Writing list")
           ,(concat "* WRITE %^{Title} %^g\n"
                    "SCHEDULED: %^t\n"
                    ":PROPERTIES:\n"
                    ":CAPTURED: %U\n:END:\n\n"
                    "%i%?"))
          ("b" "Basic task for future review" entry
           (file+headline "tasks.org" "Basic tasks that need to be reviewed")
           ,(concat "* %^{Title}\n"
                    ":PROPERTIES:\n"
                    ":CAPTURED: %U\n"
                    ":END:\n\n"
                    "%i%l"))
          ("w" "Work")
          ("wt" "Task or assignment" entry
           (file+headline "work.org" "Tasks and assignments")
           ,(concat "* TODO [#A] %^{Title} :@work:\n"
                    "SCHEDULED: %^t\n"
                    ":PROPERTIES:\n:CAPTURED: %U\n:END:\n\n"
                    "%i%?"))
          ("wm" "Meeting, event, appointment" entry
           (file+headline "work.org" "Meetings, events, and appointments")
           ,(concat "* MEET [#A] %^{Title} :@work:\n"
                    "SCHEDULED: %^T\n"
                    ":PROPERTIES:\n"
                    ":CAPTURED: %U\n:END:\n\n"
                    "%i%?"))
          ("t" "Task with a due date" entry
           (file+headline "tasks.org" "Task list with a date")
           ,(concat "* %^{Scope of task||TODO|STUDY|MEET} %^{Title} %^g\n"
                    "SCHEDULED: %^t\n"
                    ":PROPERTIES:\n:CAPTURED: %U\n:END:\n\n"
                    "%i%?"))
          ("r" "Reply to an email" entry
           (file+headline "tasks.org" "Mail correspondence")
           ,(concat "* TODO [#B] %:subject :mail:\n"
                    "SCHEDULED: %t\n:"
                    "PROPERTIES:\n:CONTEXT: %a\n:END:\n\n"
                    "%i%?"))))

  (setq org-capture-templates-contexts
        '(("r" ((in-mode . "gnus-article-mode")
                (in-mode . "gnus-summary-mode")))))

  (defun contrib/org-capture-no-delete-windows (oldfun args)
    (cl-letf (((symbol-function 'delete-other-windows) 'ignore))
      (apply oldfun args)))

  (advice-add 'org-capture-place-template
              :around 'contrib/org-capture-no-delete-windows)

  :bind ("C-c c" . org-capture))

6.3.4 Org agenda

The org-agenda is not just a single interface. It rather is your conduit to a set of utilities from where you can keep track of all the tasks you have written in the files declared as part of org-agenda-files (see its value in the section that covers the base Org configurations). Calling org-agenda will present you with a list of possible options: the "dispatcher" as it called.

Here is a primer (there are many more functions documented in the manual):

  • From the dispatcher, the a is where you keep track of all the items that have a date assigned to them, be it SCHEDULED or DEADLINE. To assign such a value to a heading use C-c C-s or C-c C-d respectively. Run those commands with a universal prefix (C-u) to remove the timestamp. Hit / to filter this view to match particular tags.
  • In the dispatcher's menu, the t will list all your tasks, regardless of whether they have a date assigned to them. You can then filter by keyword, regular expression, etc. Check the top of the buffer for information on how to do that.
  • And the n in the dispatcher will offer you a combined view of the above.

Now a few words about some of my customisations (remember to use Emacs' documentation facilities over each item, such as C-h v):

  • Always ask for confirmation when hitting C-k from the agenda views. That command removes the entry in the original file.
  • Show all dates in the current view, including those that have no entries. I find it easier that way to assess how far apart the tasks are.
  • Do not produce a message in the echo area showing the outline path. It is too distracting.
  • Any valid time expressions in headlines should be ignored. Otherwise they are used as part of the relevant sorting methods.
  • The default view should just show me a three day span. All the commands for changing views still work as expected (e.g. hit w to show the current week).
  • Disable follow mode by default. This produces a view of the current item's original context in the other window. It can be toggled on by hitting F.
  • Time stamps should always be expressed in 24h format.
  • Stylistic tweaks to the time grid view format.
  • Disable the key bindings that cycle through the agenda files: too easy to hit while trying to perform some other action. Besides, I do not need that kind of functionality.
(use-package org-agenda
  :after org
  :config
  ;; Basic setup
  (setq org-agenda-span 14)
  (setq org-agenda-start-on-weekday 1)  ; Monday
  (setq org-agenda-confirm-kill t)
  (setq org-agenda-show-all-dates t)
  (setq org-agenda-show-outline-path nil)
  (setq org-agenda-window-setup 'current-window)
  (setq org-agenda-skip-comment-trees t)
  (setq org-agenda-menu-show-matcher t)
  (setq org-agenda-menu-two-columns nil)
  (setq org-agenda-sticky nil)
  (setq org-agenda-custom-commands-contexts nil)
  (setq org-agenda-max-entries nil)
  (setq org-agenda-max-todos nil)
  (setq org-agenda-max-tags nil)
  (setq org-agenda-max-effort nil)

  ;; General view options
  (setq org-agenda-prefix-format
        '((agenda . " %i %-12:c%?-12t% s")
          (todo . " %i %-12:c")
          (tags . " %i %-12:c")
          (search . " %i %-12:c")))
  (setq org-agenda-sorting-strategy
        '(((agenda habit-down time-up priority-down category-keep)
           (todo priority-down category-keep)
           (tags priority-down category-keep)
           (search category-keep))))
  (setq org-agenda-breadcrumbs-separator "->")
  (setq org-agenda-todo-keyword-format "%-1s")
  (setq org-agenda-diary-sexp-prefix nil)
  (setq org-agenda-fontify-priorities 'cookies)
  (setq org-agenda-category-icon-alist nil)
  (setq org-agenda-remove-times-when-in-prefix nil)
  (setq org-agenda-remove-timeranges-from-blocks nil)
  (setq org-agenda-compact-blocks nil)
  (setq org-agenda-block-separator ?—)

  (defun prot/org-agenda-format-date-aligned (date)
    "Format a DATE string for display in the daily/weekly agenda.
This function makes sure that dates are aligned for easy reading.

Slightly tweaked version of `org-agenda-format-date-aligned' that
produces dates with a fixed length."
    (require 'cal-iso)
    (let* ((dayname (calendar-day-name date t))
           (day (cadr date))
           (day-of-week (calendar-day-of-week date))
           (month (car date))
           (monthname (calendar-month-name month t))
           (year (nth 2 date))
           (iso-week (org-days-to-iso-week
                      (calendar-absolute-from-gregorian date)))
           (weekyear (cond ((and (= month 1) (>= iso-week 52))
                            (1- year))
                           ((and (= month 12) (<= iso-week 1))
                            (1+ year))
                           (t year)))
           (weekstring (if (= day-of-week 1)
                           (format " (W%02d)" iso-week)
                         "")))
      (format "%s %2d %s %4d%s"
              dayname day monthname year weekstring)))

  (setq org-agenda-format-date #'prot/org-agenda-format-date-aligned)

  ;; Marks
  (setq org-agenda-bulk-mark-char "#")
  (setq org-agenda-persistent-marks nil)

  ;; Diary entries
  (setq org-agenda-insert-diary-strategy 'date-tree)
  (setq org-agenda-insert-diary-extract-time nil)
  (setq org-agenda-include-diary t)

  ;; Follow mode
  (setq org-agenda-start-with-follow-mode nil)
  (setq org-agenda-follow-indirect t)

  ;; Multi-item tasks
  (setq org-agenda-dim-blocked-tasks t)
  (setq org-agenda-todo-list-sublevels t)

  ;; Filters and restricted views
  (setq org-agenda-persistent-filter nil)
  (setq org-agenda-restriction-lock-highlight-subtree t)

  ;; Items with deadline and scheduled timestamps
  (setq org-agenda-include-deadlines t)
  (setq org-deadline-warning-days 5)
  (setq org-agenda-skip-scheduled-if-done nil)
  (setq org-agenda-skip-scheduled-if-deadline-is-shown t)
  (setq org-agenda-skip-timestamp-if-deadline-is-shown t)
  (setq org-agenda-skip-deadline-if-done nil)
  (setq org-agenda-skip-deadline-prewarning-if-scheduled 1)
  (setq org-agenda-skip-scheduled-delay-if-deadline nil)
  (setq org-agenda-skip-additional-timestamps-same-entry nil)
  (setq org-agenda-skip-timestamp-if-done nil)
  (setq org-agenda-search-headline-for-time t)
  (setq org-scheduled-past-days 365)
  (setq org-deadline-past-days 365)
  (setq org-agenda-move-date-from-past-immediately-to-today t)
  (setq org-agenda-show-future-repeats t)
  (setq org-agenda-prefer-last-repeat nil)
  (setq org-agenda-timerange-leaders
        '("" "(%d/%d): "))
  (setq org-agenda-scheduled-leaders
        '("Scheduled: " "Sched.%2dx: "))
  (setq org-agenda-inactive-leader "[")
  (setq org-agenda-deadline-leaders
        '("Deadline:  " "In %3d d.: " "%2d d. ago: "))
  ;; Time grid
  (setq org-agenda-time-leading-zero t)
  (setq org-agenda-timegrid-use-ampm nil)
  (setq org-agenda-use-time-grid t)
  (setq org-agenda-show-current-time-in-grid t)
  (setq org-agenda-current-time-string
        "Now -·-·-·-·-·-·-")
  (setq org-agenda-time-grid
        '((daily today require-timed)
          (0600 0700 0800 0900 1000 1100
                1200 1300 1400 1500 1600
                1700 1800 1900 2000 2100)
          " ....." "-----------------"))
  (setq org-agenda-default-appointment-duration nil)

  ;; Global to-do list
  (setq org-agenda-todo-ignore-with-date t)
  (setq org-agenda-todo-ignore-timestamp t)
  (setq org-agenda-todo-ignore-scheduled t)
  (setq org-agenda-todo-ignore-deadlines t)
  (setq org-agenda-todo-ignore-time-comparison-use-seconds t)
  (setq org-agenda-tags-todo-honor-ignore-options nil)

  ;; Tagged items
  (setq org-agenda-show-inherited-tags t)
  (setq org-agenda-use-tag-inheritance
        '(todo search agenda))
  (setq org-agenda-hide-tags-regexp nil)
  (setq org-agenda-remove-tags nil)
  (setq org-agenda-tags-column -120)

  ;; Agenda entry
  ;;
  ;; NOTE I do not use this right now.  Leaving everything to its
  ;; default value.
  (setq org-agenda-start-with-entry-text-mode nil)
  (setq org-agenda-entry-text-maxlines 5)
  (setq org-agenda-entry-text-exclude-regexps nil)
  (setq org-agenda-entry-text-leaders "    > ")

  ;; Logging, clocking
  ;;
  ;; NOTE I do not use these yet, though I plan to.  Leaving everything to
  ;; its default value for the time being.
  (setq org-agenda-log-mode-items '(closed clock))
  (setq org-agenda-clock-consistency-checks
        '((:max-duration "10:00" :min-duration 0 :max-gap "0:05" :gap-ok-around
                         ("4:00")
                         :default-face ; This should definitely be reviewed
                         ((:background "DarkRed")
                          (:foreground "white"))
                         :overlap-face nil :gap-face nil :no-end-time-face nil
                         :long-face nil :short-face nil)))
  (setq org-agenda-log-mode-add-notes t)
  (setq org-agenda-start-with-log-mode nil)
  (setq org-agenda-start-with-clockreport-mode nil)
  (setq org-agenda-clockreport-parameter-plist '(:link t :maxlevel 2))
  (setq org-agenda-search-view-always-boolean nil)
  (setq org-agenda-search-view-force-full-words nil)
  (setq org-agenda-search-view-max-outline-level 0)
  (setq org-agenda-search-headline-for-time t)
  (setq org-agenda-use-time-grid t)
  (setq org-agenda-cmp-user-defined nil)
  (setq org-sort-agenda-notime-is-late t)
  (setq org-sort-agenda-noeffort-is-high t)

  ;; Agenda column view
  ;;
  ;; NOTE I do not use these, but may need them in the future.
  (setq org-agenda-view-columns-initially nil)
  (setq org-agenda-columns-show-summaries t)
  (setq org-agenda-columns-compute-summary-properties t)
  (setq org-agenda-columns-add-appointments-to-effort-sum nil)
  (setq org-agenda-auto-exclude-function nil)
  (setq org-agenda-bulk-custom-functions nil)

  :bind (("C-c a" . org-agenda)
         :map org-mode-map
         ("C-'" . nil)
         ("C-," . nil)))

6.3.5 Org source code blocks

These are just some basic settings that are particularly useful when inserting source code blocks. I do not want Org to mess up with my indentation, while I need to see the native syntax highlighting for that language.

The org-src-window-setup is accessed via the C-c ' key once inside a code block that has a language assigned to it.

(use-package org-src
  :after org
  :config
  (setq org-src-window-setup 'current-window)
  (setq org-edit-src-persistent-message nil)
  (setq org-src-fontify-natively t)
  (setq org-src-preserve-indentation t)
  (setq org-src-tab-acts-natively t)
  (setq org-edit-src-content-indentation 0))

6.3.6 Org export

Org's "export" facility has the power to convert a .org file into a number of common formats, including .pdf. I only ever use it to produce the HTML version of this document or similar tasks along those lines. In the future, I might spend some time leveraging its potential for more demanding workflows.

(use-package ox
  :after org
  :commands (prot/ox-html prot/ox-texinfo)
  :config
  (setq org-export-with-toc t)
  (setq org-export-headline-levels 8)
  (setq org-export-dispatch-use-expert-ui nil)
  (setq org-html-htmlize-output-type nil)
  (setq org-html-head-include-default-style nil)
  (setq org-html-head-include-scripts nil)
  (use-package ox-texinfo)
  ;; FIXME: how to remove everything else?
  (setq org-export-backends '(html texinfo))

  (defun prot/ox-html ()
    (interactive)
    (org-html-export-as-html nil nil nil t nil))

  (defun prot/ox-texinfo ()
    (interactive)
    (org-texinfo-export-to-info)))

6.3.7 Consistent Org heading IDs (and anchor tags)

Everything in this section, minus some minor tweaks from my part, is copied from this detailed tutorial on Org header IDs. Basically, the problem is that exported HTML does not have reliable anchor tags for the various sections of the document. This fixes the issue (read the article for more).

(use-package org-id
  :after org
  :commands contrib/org-id-headlines
  :config
  (setq org-id-link-to-org-use-id
        'create-if-interactive-and-no-custom-id)

  (defun contrib/org-id-get (&optional pom create prefix)
    "Get the CUSTOM_ID property of the entry at point-or-marker
POM. If POM is nil, refer to the entry at point. If the entry
does not have an CUSTOM_ID, the function returns nil. However,
when CREATE is non nil, create a CUSTOM_ID if none is present
already. PREFIX will be passed through to `org-id-new'. In any
case, the CUSTOM_ID of the entry is returned."
    (org-with-point-at pom
      (let ((id (org-entry-get nil "CUSTOM_ID")))
        (cond
         ((and id (stringp id) (string-match "\\S-" id))
          id)
         (create
          (setq id (org-id-new (concat prefix "h")))
          (org-entry-put pom "CUSTOM_ID" id)
          (org-id-add-location id (format "%s" (buffer-file-name (buffer-base-buffer))))
          id)))))

  (defun contrib/org-id-headlines ()
    "Add CUSTOM_ID properties to all headlines in the current
file which do not already have one."
    (interactive)
    (org-map-entries
     (lambda () (contrib/org-id-get (point) t)))))

6.3.8 Simple presentations inside of Emacs (org-tree-slide)

I like the idea of easily converting an .org file into a set of pseudo slides. It is simple and has no external dependencies. My needs are pretty simple and straightforward: just show some text, narrowing it to the current section.

To make presentations happen, I use org-tree-slide to narrow the buffer to each heading. With prot/olivetti-mode (and its multiple accoutrements) I have a distraction-free view that centres the text on the screen and removes other visual elements (see my configurations in Centred, focused text mode (olivetti-mode)). While org-superstar prettifies the heading bullets, while removing the leading stars. The other tweaks in the code block below are complementary to this "slideshow effect".

For the font functions specified herein, make sure to understand the overall configurations by reading the section on primary font settings. I opted not to use text-scale-adjust or some variant thereof, because that only operates on the text of the focused window, whereas I want all interfaces to adapt to the new size so that I can, for example, show the minibuffer while doing a presentation.

(use-package org-tree-slide
  :ensure
  :after org
  :commands prot/org-presentation-mode
  :config
  (setq org-tree-slide-breadcrumbs nil)
  (setq org-tree-slide-header nil)
  (setq org-tree-slide-slide-in-effect nil)
  (setq org-tree-slide-heading-emphasis nil)
  (setq org-tree-slide-cursor-init t)
  (setq org-tree-slide-modeline-display nil)
  (setq org-tree-slide-skip-done nil)
  (setq org-tree-slide-skip-comments t)
  (setq org-tree-slide-fold-subtrees-skipped t)
  (setq org-tree-slide-skip-outline-level 8)
  (setq org-tree-slide-never-touch-face t)
  (setq org-tree-slide-activate-message
        (propertize "Presentation mode ON" 'face 'success))
  (setq org-tree-slide-deactivate-message
        (propertize "Presentation mode OFF" 'face 'error))

  ;; TODO: these should be handled with hooks.
  (define-minor-mode prot/org-presentation-mode
    "Parameters for plain text presentations with `org-mode'."
    :init-value nil
    :global nil
    (if prot/org-presentation-mode
        (progn
          (unless (eq major-mode 'org-mode)
            (user-error "Not in an Org buffer"))
          (org-tree-slide-mode 1)
          (prot/olivetti-mode 1)
          (org-indent-mode 1))
      (org-tree-slide-mode -1)
      (prot/olivetti-mode -1)
      (org-indent-mode -1)))

  :bind (("C-c P" . prot/org-presentation-mode)
         :map org-tree-slide-mode-map
         ("<C-down>" . org-tree-slide-display-header-toggle)
         ("<C-right>" . org-tree-slide-move-next-tree)
         ("<C-left>" . org-tree-slide-move-previous-tree)))

6.4 USLS — Unassuming Sidenotes of Little Significance

This is a library that I am developing to help me flesh out my note-taking system. In essence, usls is a set of helper functions around standard Emacs tools, such as find-file, dired, and internal libraries like thingatpt. It has no external dependencies whatsoever. This blog post of mine documents the principles and general ideas about it: My simple note-taking system for Emacs (without Org) (2020-10-08).

Note that usls is not meant to replace what Org does with to-do lists, the agenda, etc. This is about private records of longer form notes, such as commentary on a paper I read. I still use Org to keep track of the minutia (and also to produce documents such as this one or the Info manual of my Modus themes).

Because this is standard Emacs stuff, I can always benefit from the rest of my setup, such as to search for file contents in the current directory. See Generic completion for projects and directory trees and Icomplete configurations and extras.

;;; usls.el --- Unassuming Sidenotes of Little Significance -*- lexical-binding: t -*-

;; Copyright (C) 2020  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "25.1"))

;; This file is NOT part of GNU Emacs.

;; This program is free software; you can redistribute it and/or
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; Read about my rationale and workflow with this tool:
;; <https://protesilaos.com/codelog/2020-10-08-intro-usls-emacs-notes/>
;;
;; `usls', aka the "Unassuming Sidenotes of Little Significance" (USLS),
;; is a personal system of storing notes of arbitrary length in a flat
;; directory listing.
;;
;; `usls' leverages built-in Emacs functions to help streamline the
;; process of making and linking together plain text notes.  It does not
;; rely on `org-mode' or any other major library.
;;
;; The totally unintentional constraint of this library is that both its
;; name (`usls') and its expanded description are unwieldly.  The author
;; is not aware of an elegant solution.  Users may instead opt to call
;; it a common word that denotes its utility to the wider public and
;; contains the characters "u" "s" "l" "s".
;;
;;; Code:

(require 'cl-lib)
(require 'crm)
(require 'ffap)
(require 'thingatpt)

;;; Main variables

(defvar usls-directory "~/Documents/notes/"
  "Directory for storing personal notes.")

(defconst usls-id "%Y%m%d_%H%M%S"
  "Function to produce a unique ID prefix for note filenames.")

(defconst usls-id-regexp "\\([0-9_]+\\{15\\}\\)"
  "Regular expression to match `usls-id'.")

(defconst usls-category-regexp "--\\([0-9A-Za-z_-]*\\)--"
  "Regular expression to match `usls-categories'.")

(defconst usls-file-regexp (concat usls-id-regexp usls-category-regexp ".*.txt")
  "Regular expression to match file names from `usls-new-note'.")

;;; Basic utilities

;;;; File name helpers
(defun usls-extract (regexp str)
  "Extract REGEXP from STR."
  (with-temp-buffer
    (insert str)
    (when (re-search-forward regexp nil t -1)
      (match-string 1))))

;; REVIEW: any character class that captures those?  It seems to work
;; though...
(defun usls--slug-no-punct (str)
  "Convert STR to a file name slug."
  (replace-regexp-in-string "[][{}!@#$%^&*()_=+'\"?,.\|;:~`]*" "" str))

;; REVIEW: this looks inelegant.  We want to remove spaces or multiple
;; hyphens, as well as a final hyphen.
(defun usls--slug-hyphenate (str)
  "Replace spaces with hyphens in STR."
  (replace-regexp-in-string "-$" "" (replace-regexp-in-string "--+\\|\s+" "-" str)))

(defun usls-sluggify (str)
  "Make STR an appropriate file name slug."
  (downcase (usls--slug-hyphenate (usls--slug-no-punct str))))

;;;; Files in directory

(defun usls--directory-files ()
  "List directory files."
  (let ((dotless directory-files-no-dot-files-regexp))
    (directory-files usls-directory nil dotless t)))

;;;; Categories

;; NOTE: The implicit assumption is that a "category" is a single word.
;; If you need it to be more than one, separate them with an underscore.
;; Do not use a hyphen though, as it is assumed to demarcate distinct
;; categories.  See `usls--inferred-categories'.
(defvar usls-known-categories
  '(economics philosophy politics)
  "List of predefined categories for `usls-new-note'.
Also see `usls-categories' for a dynamically generated list that
gets combined with this one in relevant prompts.")

(defun usls--inferred-categories ()
  "Extract categories from `usls--directory-files'."
  (let ((sequence (mapcar (lambda (x)
                    (usls-extract usls-category-regexp x))
                  (usls--directory-files))))
    (mapcan (lambda (s)
              (split-string s "-" t))
            sequence)))

(defun usls-categories ()
  "Combine `usls--inferred-categories' with `usls-known-categories'."
  (append (usls--inferred-categories) usls-known-categories))

;;;; Input history lists

(defvar usls--title-history nil
  "Used internally by `usls-new-note' to record titles.")

(defvar usls--category-history nil
  "Used internally by `usls-new-note' to record categories.")

(defvar usls--link-history nil
  "Used internally by `usls-id-insert' to record links.")

;;; Interactive functions

;;;###autoload
(defun usls-new-note ()
  "Create new note with the appropriate metadata.
If the region is active, append it to the newly created file."
  (interactive)
  (let* ((titlehist '(usls--title-history . 0))
         (cathist '(usls--category-history . 0))
         (title (read-string "File title: " nil titlehist))
         (categories (usls-categories))
         (crm-separator "[, ]") ; Insert multiple categories with comma/space between them
         (category (completing-read-multiple "File category: " categories nil nil nil cathist))
         (slug (usls-sluggify title))
         (path usls-directory)
         (id (format-time-string usls-id))
         (filename
          (format "%s%s--%s--%s.txt"
                  path
                  id
                  (mapconcat #'downcase category "-")
                  slug))
         (date (format-time-string "%F"))
         (region (with-current-buffer (current-buffer)
                   (if (region-active-p)
                       (concat "\n\n* * *\n\n"
                               (buffer-substring-no-properties
                                (region-beginning)
                                (region-end)))
                     ""))))
    (unless (file-directory-p path)
      (make-directory path))
    (with-current-buffer (find-file filename)
      (usls-mode 1)
      (insert (concat "title: " title "\n"
                      "date: " date "\n"
                      "category: " (mapconcat #'capitalize category ", ") "\n"
                      "orig_name: " filename "\n"
                      "orig_id: " id "\n"))
      (insert-char ?- 24 nil)
      (insert "\n\n")
      (save-excursion (insert region)))
    (add-to-history 'usls--title-history title)
    (add-to-history 'usls--category-history category)))

(defun usls--directory-files-not-current ()
  "Return list of files minus the current one."
  (cl-remove-if
   (lambda (x)
     (string= (file-name-nondirectory (buffer-file-name)) x))
   (usls--directory-files)))

(defun usls--insert-file-reference (file delimiter)
  "Insert formatted reference to FILE with DELIMITER."
  (save-excursion
    (goto-char (point-max))
    (newline 1)
    (insert
     (format "%s %s\n" delimiter file))))

(defun usls--delete-duplicate-links ()
  "Remove duplicate references to files."
  (delete-duplicate-lines
   (save-excursion
     (goto-char (point-min))
     (search-forward-regexp "\\(@@\\|\\^\\^\\) " nil t nil))
   (point-max)))

;;;###autoload
(defun usls-id-insert ()
  "Insert at point the identity of a file using completion."
  (interactive)
  (let* ((file (completing-read "Link to: "
                                (usls--directory-files-not-current)
                                nil t nil 'usls--link-history))
         (this-file (file-name-nondirectory (buffer-file-name)))
         (id (usls-extract usls-id-regexp file)))
    (insert (concat "^" id))
    (usls--insert-file-reference (format "%s" file) "^^")
    (with-current-buffer (find-file-noselect file)
      (save-excursion
        (usls--insert-file-reference this-file "@@")
        (usls--delete-duplicate-links))
      (save-buffer)
      (kill-buffer))
    (usls--delete-duplicate-links)
    (add-to-history 'usls--link-history file)))

(defvar usls--file-link-regexp "\\(^\\^\\^ \\)\\(.*\\.txt\\)"
  "Regexp for file links at the end of the buffer.")

(defun usls--links ()
  "Gather links to files in the current buffer."
  (let ((links))
    (save-excursion
      (goto-char (point-min))
      (while (search-forward-regexp usls--file-link-regexp nil t)
        (push (match-string-no-properties 2) links)))
    (cl-remove-duplicates links)))

;;;###autoload
(defun usls-follow-link ()
  "Visit link referenced in the note using completion."
  (interactive)
  (let ((links (usls--links)))
    (if links
        (find-file
         (completing-read "Follow link: " links nil t))
      (usls-find-file))))

;;;###autoload
(defun usls-find-file ()
  "Visit a file in `usls-directory' using completion."
  (interactive)
  (let* ((files (usls--directory-files))
         (file (completing-read "Visit file: " files nil t nil 'usls--link-history))
         (item (concat (file-truename (concat usls-directory file)))))
    (find-file item)
    (add-to-history 'usls--link-history item)))

;;;###autoload
(defun usls-dired ()
  "Switch to `usls-directory'."
  (interactive)
  (let ((path usls-directory))
    (if (file-directory-p path)
        (dired path)
      (error "`usls-directory' not found"))))

;;; User-facing setup

;; TODO: how to define a prefix key?
;;
;; NOTE: Users are expected to bind this to something more useful.  Did
;; not want to violate key binding conventions.
(global-set-key (kbd "C-c _ d") 'usls-dired)
(global-set-key (kbd "C-c _ f") 'usls-find-file)
(global-set-key (kbd "C-c _ n") 'usls-new-note)

(defvar usls-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-c _ i") 'usls-id-insert)
    (define-key map (kbd "C-c _ l") 'usls-follow-link)
    map)
  "Key map for use when variable `usls-mode' is non-nil.")

(defvar usls-mode-hook nil
  "Hook called when variable `usls-mode' is non-nil.")

(define-minor-mode usls-mode
  "Extras for working with `usls' notes.

\\{usls-mode-map}"
  :init-value nil
  :global nil
  :lighter " usls"
  :keymap usls-mode-map
  (run-hooks 'usls-mode-hook))

(defun usls-mode-activate ()
  "Activate mode when inside `usls-directory'."
  (when (or (string-match-p (expand-file-name usls-directory) default-directory)
            (string-match-p usls-directory default-directory))
    (usls-mode 1)))

(add-hook 'find-file-hook #'usls-mode-activate)
(add-hook 'dired-mode-hook #'usls-mode-activate)

(defgroup usls-faces ()
  "Faces for `usls-mode'."
  :group 'faces)

(defface usls-header-data-date
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#0031a9")
    (((class color) (min-colors 88) (background dark))
     :foreground "#2fafff")
    (t :foreground "blue"))
  "Face for header date entry.")

(defface usls-header-data-category
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#721045")
    (((class color) (min-colors 88) (background dark))
     :foreground "#feacd0")
    (t :foreground "magenta"))
  "Face for header category entry.")

(defface usls-header-data-title
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#000000")
    (((class color) (min-colors 88) (background dark))
     :foreground "#ffffff")
    (t :foreground "blue"))
  "Face for header title entry.")

(defface usls-header-data-secondary
  '((((class color) (min-colors 88) (background light))
     :foreground "#093060")
    (((class color) (min-colors 88) (background dark))
     :foreground "#c6eaff")
    (t :inherit (bold shadow)))
  "Face for secondary header information.")

(defface usls-header-data-key
  '((((class color) (min-colors 88) (background light))
     :foreground "#505050")
    (((class color) (min-colors 88) (background dark))
     :foreground "#a8a8a8")
    (t :inherit shadow))
  "Face for secondary header information.")

(defface usls-section-delimiter
  '((((class color) (min-colors 88) (background light))
     :background "#d7d7d7" :foreground "#404148")
    (((class color) (min-colors 88) (background dark))
     :background "#323232" :foreground "#bfc0c4")
    (t :inherit shadow))
  "Face for section delimiters.")

(defface usls-dired-field-date
  '((((class color) (min-colors 88) (background light))
     :foreground "#2544bb")
    (((class color) (min-colors 88) (background dark))
     :foreground "#79a8ff")
    (t :inherit font-lock-string-face))
  "Face for file name date in `dired-mode' buffers.")

(defface usls-dired-field-delimiter
  '((t :inherit shadow))
  "Face for file name field delimiters in `dired-mode' buffers.")

(defface usls-dired-field-category
  '((((class color) (min-colors 88) (background light))
     :foreground "#8f0075")
    (((class color) (min-colors 88) (background dark))
     :foreground "#f78fe7")
    (t :inherit font-lock-builtin-face))
  "Face for file name category in `dired-mode' buffers.")

(defface usls-dired-field-name
  '((((class color) (min-colors 88) (background light))
     :foreground "#000000")
    (((class color) (min-colors 88) (background dark))
     :foreground "#ffffff")
    (t :inherit default))
  "Face for file name title in `dired-mode' buffers.")

(defconst usls-font-lock-keywords
  '(("\\(title:\\) \\(.*\\)"
     (1 'usls-header-data-key)
     (2 'usls-header-data-title))
    ("\\(date:\\) \\(.*\\)"
     (1 'usls-header-data-key)
     (2 'usls-header-data-date))
    ("\\(category:\\) \\(.*\\)"
     (1 'usls-header-data-key)
     (2 'usls-header-data-category))
    ("\\(orig_\\(name\\|id\\):\\) \\(.*\\)"
     (1 'usls-header-data-key)
     (2 'usls-header-data-key)
     (3 'usls-header-data-secondary))
    ("\\(-\\{24\\}\\|[*\s]\\{5\\}\\)"
     (1 'usls-section-delimiter))
    ("\\(\\^\\)\\([0-9_]\\{15\\}\\)"
     (1 'escape-glyph)
     (2 'font-lock-variable-name-face))
    ("\\(^\\(@@\\|\\^^\\)\\) \\([0-9_]+\\{15\\}.*\\.txt\\)"
     (1 'escape-glyph t)
     (2 'escape-glyph t)
     (3 'font-lock-constant-face t))
    ;; These conflict with `diredfl-mode'.  Maybe there is some way to
    ;; avoid that?
    ("\\([0-9_]\\{15\\}\\)\\(--\\)\\([0-9A-Za-z_-]*\\)\\(--\\)\\(.*\\)\\(\\.txt\\)"
     (1 'usls-dired-field-date)
     (2 'usls-dired-field-delimiter)
     (3 'usls-dired-field-category)
     (4 'usls-dired-field-delimiter)
     (5 'usls-dired-field-name)
     (6 'usls-dired-field-delimiter)))
  "Rules to apply font-lock highlighting with `usls--fontify'.")

(defun usls--fontify ()
  "Font-lock setup for `usls-font-lock-keywords'."
  (font-lock-flush (point-min) (point-max))
  (if usls-mode
      (font-lock-add-keywords nil usls-font-lock-keywords)
    (font-lock-remove-keywords nil usls-font-lock-keywords))
  (font-lock-flush (point-min) (point-max)))

(add-hook 'usls-mode-hook #'usls--fontify)

(provide 'usls)

;;; usls.el ends here

Now that we have defined the library, we can configure it to our liking. I tried to respect key binding conventions, but on my own setup I can do whatever I want. So let's make sure we use a more convenient set of key combinations.

(use-package usls
  :bind (("C-c n d" . usls-dired)
         ("C-c n f" . usls-find-file)
         ("C-c n n" . usls-new-note)
         :map usls-mode-map
         ("C-c n i" . usls-id-insert)
         ("C-c n l" . usls-follow-link)))

6.5 Email settings

Configuring email can be quite the challenge, largely because we have been used to the likes of Thunderbird, where you log in once and then everything "just works". The toolset for my current setup consists of the following:

  • mbsync to synchronise my email server and my local mail directories. This is a new addition to my toolkit. Gnus used to fetch mail directly, but a dedicated tool is better for performance and flexibility. Also good for keeping local copies of messages and for using multiple mail clients.
  • Gnus (also pronounced as "News" or "Nooz", etc.), which is a powerful newsreader and email client that is built into Emacs. It serves as my default interface for reading email and Usenet sources.
  • Mu4e as my secondary mail user agent, intended for archiving purposes and arbitrary searches. I used to only have Gnus configured, but its search capabilities left something to be desired.
  • The built-in capabilities to compose and send email, which apply to both Gnus and mu4e.

6.5.1 Client-agnostic email settings (credentials, message composition, encryption)

Before configuring any mail user agent, we need to establish the absolute essentials: who we are, where our credentials are stored, and whether encryption is supported. We must also define how message composition should work. This is what the following configurations are about.

The prot/auth-get-field is a generic tool for finding the values pertaining to our login credentials. You will see this function used elsewhere in this document. For example, to find the username and password for host hostname we do:

(prot/auth-get-field "hostname" :user)   ; login name
(prot/auth-get-field "hostname" :secret) ; password

Then we set up the interface for composing emails:

  • The mail-user-agent and message-mail-user-agent concern the default email composition buffer, called with C-x m or any other facility that falls back to the compose-mail function. The default is message-mode. When Gnus is running, it will insert relevant paraphernalia, the most important of which is the "Gcc" header. The Gcc saves a copy of the outgoing message to a specified group. In my case that is the "Sent" directory of my default account.
  • Function prot/message-header-add-gcc is directly related to the above. The inserted header points to my public email account, which is declared in user-mail-address. This concerns only the creation of new emails. While replying to a message, the appropriate information is filled in automatically, based on parameters I specify in the section about account settings and essential configurations.
  • The value of message-citation-line-format is expanded into something like "On 2020-02-19, 13:54 +0200, NAME <EMAIL> wrote:". To learn about all the date-related specifiers, it is better for you to read the documentation with C-h v format-time-string.
  • As for the configurations of mm-encode and mml-sec, these are meant to come into effect when encrypting and signing an outgoing message with C-c C-m C-e (mml-secure-message-sign-encrypt). The guided key selection will ask for confirmation on who to encrypt to. It presents a list with the available keys. Items are marked with m and then the mail can be sent with the standard commands (e.g. C-c C-c). I select myself and whomever the other party is. This is an extra step just to make sure that I have everything right with regard to the keys and the correspondent[s] when using encryption. If this becomes a task I use regularly, I will need to streamline things. For the time being, I want the added confirmation.
(use-package auth-source
  :config
  (setq auth-sources '("~/.authinfo.gpg"))
  (setq user-full-name "Protesilaos Stavrou")
  (setq user-mail-address "public@protesilaos.com")

  (defun prot/auth-get-field (host prop)
    "Find PROP in `auth-sources' for HOST entry."
    (let* ((source (auth-source-search :host host))
           (field (plist-get
                   (flatten-list source)
                   prop)))
      (if source
          field
        (user-error "No entry in auth sources")))))

(use-package mm-encode
  :config
  (setq mm-encrypt-option 'guided)
  (setq mm-sign-option 'guided))

(use-package mml-sec
  :config
  (setq mml-secure-openpgp-encrypt-to-self t)
  (setq mml-secure-openpgp-sign-with-sender t)
  (setq mml-secure-smime-encrypt-to-self t)
  (setq mml-secure-smime-sign-with-sender t))

(use-package message
  :config
  (setq mail-user-agent 'message-user-agent)
  (setq compose-mail-user-agent-warnings nil)
  (setq message-mail-user-agent nil)    ; default is `gnus'
  (setq mail-signature "Protesilaos Stavrou\nprotesilaos.com\n")
  (setq message-signature "Protesilaos Stavrou\nprotesilaos.com\n")
  (setq message-citation-line-format "On %Y-%m-%d, %R %z, %f wrote:\n")
  (setq message-citation-line-function
        'message-insert-formatted-citation-line)
  (setq message-confirm-send nil)
  (setq message-kill-buffer-on-exit t)
  (setq message-wide-reply-confirm-recipients t)
  (setq message-default-charset 'utf-8)
  (add-to-list 'mm-body-charset-encoding-alist '(utf-8 . base64))

  (defun prot/message-header-add-gcc ()
    "While `gnus' is running, add pre-populated Gcc header.

The Gcc header places a copy of the outgoing message to the
appropriate directory of the IMAP server, as per the contents of
~/.authinfo.gpg.

In the absence of a Gcc header, the outgoing message will not
appear in the appropriate maildir directory, though it will still
be sent.

Add this function to `message-header-setup-hook'."
    (if (gnus-alive-p)
        (progn
          (when (message-fetch-field "Gcc")
            (message-remove-header "Gcc"))
          (message-add-header "Gcc: nnmaildir+pub:Sent"))
      (message "Gnus is not running. No GCC field inserted.")))

  :hook ((message-header-setup-hook . prot/message-header-add-gcc)
         (message-setup-hook . message-sort-headers)))

Below is a sample with the contents of my authinfo.gpg. This is read, inter alia, by gnus and smtpmail to be able to both fetch and send messages from the given account. I strongly encourage you to encrypt this file if you add your login credentials there. Do it from inside dired with : e while the point is over the file. Emacs can decrypt all encrypted files automatically.

machine prv port 993 login MAIL password SECRET
machine inf port 993 login MAIL password SECRET
machine pub port 993 login MAIL password SECRET

machine mail.gandi.net port 465 login MAIL password SECRET
machine mail.gandi.net port 465 login MAIL password SECRET
machine mail.gandi.net port 465 login MAIL password SECRET

Refer to your email provider's documentation in order to determine the port number and server address you need to use for sending and receiving messages. The MAIL is either your email address or some username for logging into the account.

Note that the terms I use above for prv, inf, and pub are just arbitrary names for the given MAIL and SECRET combination. This allows us to reference each name in the Gnus configurations, and share those in a public document like this one, without worrying about leaking private data.

6.5.2 Contents of `~/.mbsyncrc' (external tool)

I already noted in the introduction to Email settings that my emails are synced locally using the mbsync executable. This program is part of a package that, depending on your operating system, is called "isync". Read the Arch Wiki entry on mbsync.

My ~/.mbsyncrc is furnished below. Note that the awk call reads from the ~/.authinfo.gpg that I document at the end of the section on Client-agnostic email settings.

IMAPAccount pub
Host mail.gandi.net
User EMAIL-ADDRESS-HERE
PassCmd "gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/pub/ { print $NF; exit; }'"
SSLType IMAPS

IMAPStore pub-remote
Account pub

MaildirStore pub-local
Subfolders Verbatim
# The trailing "/" is important
Path ~/.mail/pub/
Inbox ~/.mail/pub/Inbox

Channel pub
Master :pub-remote:
Slave :pub-local:
# Include everything
Patterns *

Create Both
# Expunge Both
SyncState *

##########

IMAPAccount inf
Host mail.gandi.net
User EMAIL-ADDRESS-HERE
PassCmd "gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/inf/ { print $NF; exit; }'"
SSLType IMAPS

IMAPStore inf-remote
Account inf

MaildirStore inf-local
Subfolders Verbatim
# The trailing "/" is important
Path ~/.mail/inf/
Inbox ~/.mail/inf/Inbox

Channel inf
Master :inf-remote:
Slave :inf-local:
# Include everything
Patterns *

Create Both
# Expunge Both
SyncState *

##########

IMAPAccount prv
Host mail.gandi.net
User EMAIL-ADDRESS-HERE
PassCmd "gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/prv/ { print $NF; exit; }'"
SSLType IMAPS

IMAPStore prv-remote
Account prv

MaildirStore prv-local
Subfolders Verbatim
# The trailing "/" is important
Path ~/.mail/prv/
Inbox ~/.mail/prv/Inbox

Channel prv
Master :prv-remote:
Slave :prv-local:
# Include everything
Patterns *

Create Both
# Expunge Both
SyncState *

6.5.3 Gnus for reading email, mailing lists, and more

The documentation describes Gnus as the "coffee-brewing, all singing, all dancing, kitchen sink newsreader". I chuckled when I first read it, thinking to myself that the developers have an interesting sense of humour. Then I decided to quickly go through the list of user-facing customisation options: M-x customize-apropos-groups RET gnus RET … Not so funny after all!

Simply put, Gnus is massive. This makes it both extremely powerful and incredibly complicated for new users. Do not let that scare you though: start small and gradually tweak things as you go. This is how you approach Emacs itself. Learn the basics and then figure out your needs as you go. This is what I always do.

Now some basic information on the abstractions that Gnus relies on:

  1. The default Gnus buffer is called "Group". It will present you with a list of all the news sources you have subscribed to. By default, Gnus only displays messages that have not been read. The same applies for groups. The "Group" buffer will be empty the very first time you log in because you have not subscribed to anything yet. Use g to fetch new messages from the sources. If you only want to refresh the group at point, do it with M-g.
  2. The "Server" buffer contains a list with all the sources you have specified for discovering news. In my case, these are my email accounts and a Usenet server where mailing lists are hosted. To access the "Server" buffer from inside the "Group" buffer, just hit the caret sign ^. To subscribe to an item, place the point over it and hit u. Do that for your email's inbox and for whatever mailing lists you intend to follow.
  3. The "Summary" buffer contains all the messages of a group. Hitting the return key over a message will split the view in two, with the list above and the message below. Use n or p to move to the next or previous unread message (or N and P to just the next/prev). You access the "Summary" buffer both from the "Group" and the "Server" by entering a group.

It is essential to take things slowly (and first test whether your messages are being sent and that you can receive them). Each buffer has some unique functions that are relevant to the current interface. To learn more about them, use C-h m. Do it for all three of the above. Also rely on C-h k to get information about what each key does in the given context (or just start a key sequence and then hit C-h to display possible combinations in a new Help buffer).

Now a couple more things about the "Group" buffer:

  • A group can be assigned a level of importance. This is a grade whose highest score is 1 and the lowest is 6 (customisable though). Each level has a different colour. To assign a new value to the group at point, do it with S l and then give it a number. Once you have graded your groups, you can perform various actions on a per-level basis. For example, to refresh all levels from 1 up to 3 but not higher, pass a numeric argument to the standard g command. So C-3 g (this is the same as C-u 3 g).
  • Groups can be organised by topic. Create a new one with T n and give it a name. Move a group to a topic with T m. To toggle the view of topics use t (I have a hook that does this automatically at startup). The level of indentation tells us whether a topic is a sub-set of another. Use TAB or C-u TAB to adjust it accordingly. As with levels, you can operate on a per-topic basis. For example, to catch up on all the news of a given topic (mark all as read), you place the point over it, hit c and then confirm your choice.

As noted, Gnus will only show you a list of unread items. To view all your groups, hit L. Use the lower case version l to view only the unread ones. To produce a Summary buffer with read items, hit C-u RET over a group and specify the number of messages you want to list (the other option is C-u M-g from inside the Summary). Another useful trick for the Summary buffer is the use of the caret sign (^) to show you the previous message that the current item is a reply to.

Consider watching my Introduction to Gnus (2020-02-02).

Notwithstanding the numerous customisation options and certain perhaps idiosyncratic design choices, some prior experience with Emacs' various interfaces will definitely come in handy: Gnus uses similar metaphors for navigating and parsing information. It still is important to read the manual though.

Now here comes the nice part of leveraging the integration that Emacs offers: in my Org mode configurations I have a simple template to capture the current buffer's link. This means that we can quickly convert any item into a task/note and always be able to go back to the original message by following the link. Found an interesting suggestion in some mailing list? Capture it. Need to act on an email later? Capture, capture, capture. Same principle applies to the integration with Dired as a means of attaching files to emails (see next section), and to the EPA subsystem for GPG encryption.

The package declarations below are divided into several subsections to make things easier to read and keep track of. Remember to use C-h v VAR to read documentation about each VAR or simply place the point over it and then hit C-h v to pre-populate the results (C-h f is the equivalent for functions, C-h o for other symbols). Whenever you see some formatting customisations concerning time units, it is better refer to the documentation of the function format-time-string to understand the meaning of the various date/time specifiers.

6.5.3.1 Gnus account settings and essential configurations

Here I only furnish the essentials for the basic Gnus functionality. Subsequent sections expand on the particulars.

  • The gnus-select-method sets the default method for fetching news items. As I want to read mail from several accounts in addition to following Usenet sources, I choose to set it to nil.
  • The gnus-secondary-select-methods is where my accounts are specified. Each nnmaildir list points to a specific line in my authinfo.gpg file (whose format I described in the base email settings). My emails all use the same server so this method allows me to specify the username (email) and password combination for each of them without making this information public.
  • The gnus-parameters are designed to move my outgoing messages to the "Sent" folder of the account that replies to a given email and to use the right email address, depending on the context. While the variable gnus-gcc-mark-as-read ensures that the outgoing messages are marked as read. The prot/auth-get-field is defined in Base email settings. It is used to get the relevant user name.
  • The "agent" is enabled here and configured in the following section.
  • Setting the gnus-novice-user to nil has the effect of reducing prompts for potentially destructive commands, such as deleting an email. Too many confirmations end up being annoying, but you might opt to keep this to t if you are still new to Gnus.
  • The variables concerning the "dribble" file may be reviewed. The idea is to store the state of Gnus in case Emacs crashes. This has never happened and, therefore, I am not putting too much effort into solving a highly unlikely problem.
  • Consider reviewing nnmail-expiry-wait only after you have some experience with Gnus. I set it to a fairly high value.
(use-package gnus
  :config
  ;; accounts
  (setq gnus-select-method '(nnnil ""))
  (setq gnus-secondary-select-methods
        '((nntp "news.gwene.org")
          (nnmaildir "prv" (directory "~/.mail/prv"))
          (nnmaildir "inf" (directory "~/.mail/inf"))
          (nnmaildir "pub" (directory "~/.mail/pub"))))

  (setq nnir-method-default-engines
        '((nnmaildir . notmuch)))

  (setq gnus-parameters
        '((".*"                         ; fallback option
           (posting-style
            (gcc "nnmaildir+inf:Sent")
            (From
             (format "%s <%s>" user-full-name
                     (prot/auth-get-field "inf" :user)))))
          ("prv"
           (posting-style
            (gcc "nnmaildir+prv:Sent")
            (From
             (format "%s <%s>" user-full-name
                     (prot/auth-get-field "prv" :user)))))
          ("pub"
           (posting-style               ; Uses default name+mail
            (gcc "nnmaildir+pub:Sent")))))

  (setq gnus-gcc-mark-as-read t)
  (setq gnus-agent t)
  (setq gnus-novice-user nil)
  ;; checking sources
  (setq gnus-check-new-newsgroups 'ask-server)
  (setq gnus-read-active-file 'some)
  ;; dribble
  (setq gnus-use-dribble-file t)
  (setq gnus-always-read-dribble-file t)
  :bind ("C-c m" . gnus))

(use-package nnmail
  :config
  (setq nnmail-expiry-wait 30))
6.5.3.2 Gnus agent

The "agent" is a technical term described in the Gnus manual which basically represents the bridge between our Gnus and the server to which it connects to. Gnus is said to be "plugged" when a connection is established. Else it is "unplugged".

Technicalities aside, we can use the agent to configure the handling of messages. For example, we can set an expiry date, after which the message is deleted, or we can create a queue of outgoing messages when Gnus is in an unplugged state.

(use-package gnus-agent
  :after gnus
  :config
  (setq gnus-agent-article-alist-save-format 1)  ; uncompressed
  (setq gnus-agent-cache t)
  (setq gnus-agent-confirmation-function 'y-or-n-p)
  (setq gnus-agent-consider-all-articles nil)
  (setq gnus-agent-directory "~/News/agent/")
  (setq gnus-agent-enable-expiration 'ENABLE)
  (setq gnus-agent-expire-all nil)
  (setq gnus-agent-expire-days 30)
  (setq gnus-agent-mark-unread-after-downloaded t)
  (setq gnus-agent-queue-mail t)        ; queue if unplugged
  (setq gnus-agent-synchronize-flags nil))
6.5.3.3 Gnus article (message view)

In Gnus parlance, the "article" is the window that contains the content of the summary's selected item. This has its own major mode, which is great for us: we can define behaviours and key bindings that only apply when the article is in focus.

I have no particular interest in the HTML-related variables, because I practically never have to read such messages. As a general rule, email that can only be read in HTML is likely spam or annoying enough to be treated as such.

With regard to images, I prefer to inhibit any inline items. If I need to see it, I can always call gnus-article-show-images.

Note that gnus-article-sort-functions requires the most important function to be declared last.

With regard to the key bindings, I have redefined some of the existing ones to suit my workflow and better match my intuitions. For example, in the Article view, hitting s takes you to the Summary buffer. I find that to be a waste, since we can already move between buffers with standard keys. Instead, the s can be used to save the attachment at point. Similarly, I want o to behave just like in dired, where it opens the attachment at point (MIME part) in another buffer.

Finally, here is a tip that I do not configure as I always prefer a manual check: when you receive someone's public PGP key, you can mark it and epa-import-keys-region (though I should probably write a function for this task).

(use-package gnus-art
  :after gnus
  :demand
  :config
  (setq gnus-article-browse-delete-temp 'ask)
  (setq gnus-article-over-scroll nil)
  (setq gnus-article-show-cursor t)
  (setq gnus-article-sort-functions
        '((not gnus-article-sort-by-number)
          (not gnus-article-sort-by-date)))
  (setq gnus-article-truncate-lines nil)
  (setq gnus-html-frame-width 80)
  (setq gnus-html-image-automatic-caching t)
  (setq gnus-inhibit-images t)
  (setq gnus-max-image-proportion 0.7)
  (setq gnus-treat-display-smileys nil)
  (setq gnus-article-mode-line-format "%G %S %m")
  (setq gnus-visible-headers
        '("^From:" "^Subject:" "^To:" "^Cc:" "^Newsgroups:" "^Date:"
          "Followup-To:" "Reply-To:" "^Organization:" "^X-Newsreader:"
          "^X-Mailer:"))
  (setq gnus-sorted-header-list gnus-visible-headers)
  :hook (gnus-article-mode-hook . (lambda ()
                                    (setq-local fill-column 80)))
  :bind (:map gnus-article-mode-map
              ("i" . gnus-article-show-images)
              ("s" . gnus-mime-save-part)
              ("o" . gnus-mime-copy-part)))
6.5.3.4 Gnus asynchronous operations

By default, Gnus performs all its actions in a synchronous fashion. This means that Emacs is blocked until Gnus has finished. By enabling this library, we can use certain functions in a non-blocking way. I do this for sending email.

(use-package gnus-async
  :after gnus
  :config
  (setq gnus-asynchronous t)
  (setq gnus-use-article-prefetch 15))
6.5.3.5 Gnus group (main interface)

I already outlined the utility of the group buffer in the introductory section on Gnus for reading email, mailing lists, and more. In short, it is the epicentre of Gnus, where all your subscribed groups are presented and from where you can browse through your updates.

I use groups in tandem with topics, which allows me to quickly follow updates on the theme I am interested in at the moment. It also allows me to perform per-topic actions, such as updating only the groups it contains or "catching up" to them (marking them as read).

I choose to disable the default behaviour of always showing a group that has "ticked" items (the equivalent of starred or marked as important).

Note that gnus-group-sort-functions requires the most important function to be declared last.

(use-package gnus-group
  :after gnus
  :demand
  :config
  (setq gnus-level-subscribed 6)
  (setq gnus-level-unsubscribed 7)
  (setq gnus-level-zombie 8)
  (setq gnus-activate-level 2)
  (setq gnus-list-groups-with-ticked-articles nil)
  (setq gnus-group-sort-function
        '((gnus-group-sort-by-unread)
          (gnus-group-sort-by-alphabet)
          (gnus-group-sort-by-rank)))
  (setq gnus-group-line-format "%M%p%P%5y:%B%(%g%)\n")
  (setq gnus-group-mode-line-format "%%b")
  :hook ((gnus-group-mode-hook . hl-line-mode)
         (gnus-select-group-hook . gnus-group-set-timestamp))
  :bind (:map gnus-group-mode-map
              ("M-n" . gnus-topic-goto-next-topic)
              ("M-p" . gnus-topic-goto-previous-topic)))

(use-package gnus-topic
  :after (gnus gnus-group)
  :config
  (setq gnus-topic-display-empty-topics nil)
  :hook (gnus-group-mode-hook . gnus-topic-mode))
6.5.3.6 Gnus summary

This section assumes you have already read my introductory remarks on Gnus for reading email, mailing lists, and more.

Note that the various sort functions expect the primary filter method to be declared last, in case more that one function is to be invoked. The sorting is set to reverse chronological order (newest first).

Threads should not be hidden, while messages whose root has been removed should be grouped together in some meaningful way (which may not always be fully accurate). Furthermore, when moving up or down in the list of messages using just n or p, I want to go to the next message, regardless of whether it has been read or not. I can otherwise rely on standard Emacs motions.

The gnus-user-date-format-alist basically adapts the date to whether the message was within the day or the one before, else falls back to a default ISO-style value. It is then called with %&user-date;.

Also notice the standard behaviour of the %f specifier that is used in the gnus-summary-line-format. It has a conditional behaviour, where it will show the contents of the "From" header field, unless these match some exception, defined in gnus-ignored-from-addresses. When the exception is met, the specifier will fetch the contents of the "To" field instead, prepending to them gnus-summary-to-prefix and/or gnus-summary-newsgroup-prefix (I have no use for the latter). This is useful when viewing a summary buffer with, say, all your sent messages.

(use-package gnus-sum
  :after (gnus gnus-group)
  :demand
  :config
  (setq gnus-auto-select-first nil)
  (setq gnus-summary-ignore-duplicates t)
  (setq gnus-suppress-duplicates t)
  (setq gnus-save-duplicate-list t)
  (setq gnus-summary-goto-unread nil)
  (setq gnus-summary-make-false-root 'adopt)
  (setq gnus-summary-thread-gathering-function
        'gnus-gather-threads-by-subject)
  (setq gnus-summary-gather-subject-limit 'fuzzy)
  (setq gnus-thread-sort-functions
        '((not gnus-thread-sort-by-date)
          (not gnus-thread-sort-by-number)))
  (setq gnus-subthread-sort-functions
        'gnus-thread-sort-by-date)
  (setq gnus-thread-hide-subtree nil)
  (setq gnus-thread-ignore-subject nil)
  (setq gnus-user-date-format-alist
        '(((gnus-seconds-today) . "Today at %R")
          ((+ (* 60 60 24) (gnus-seconds-today)) . "Yesterday, %R")
          (t . "%Y-%m-%d %R")))

  ;; When the %f specifier in `gnus-summary-line-format' matches my
  ;; name, this will use the contents of the "To:" field, prefixed by
  ;; the string I specify.  Useful when checking your "Sent" summary or
  ;; a mailing list you participate in.
  (setq gnus-ignored-from-addresses "Protesilaos Stavrou")
  (setq gnus-summary-to-prefix "To: ")

  (setq gnus-summary-line-format "%U%R %-18,18&user-date; %4L:%-25,25f %B%s\n")
  (setq gnus-summary-mode-line-format "[%U] %p")
  (setq gnus-sum-thread-tree-false-root "")
  (setq gnus-sum-thread-tree-indent " ")
  (setq gnus-sum-thread-tree-single-indent "")
  (setq gnus-sum-thread-tree-leaf-with-other "+-> ")
  (setq gnus-sum-thread-tree-root "")
  (setq gnus-sum-thread-tree-single-leaf "\\-> ")
  (setq gnus-sum-thread-tree-vertical "|")

  (defun prot/gnus-summary-save-parts-all ()
    "Save relevant message MIME parts to a desired directory.
EXPERIMENTAL"
    (interactive)
    (let ((dir "~/Downloads/mail-attachments"))
      (if major-mode 'gnus-summary-mode
        (progn
          (unless (file-directory-p dir)
            (make-directory dir))
          (gnus-summary-save-parts "text/.*" dir nil t))
        (user-error "Not in a `gnus' summary buffer"))))

  :hook (gnus-summary-mode-hook . hl-line-mode)
  :bind (:map gnus-summary-mode-map
              ("X x" . prot/gnus-summary-save-parts-all)
              ("<delete>" . gnus-summary-delete-article)
              ("n" . gnus-summary-next-article)
              ("p" . gnus-summary-prev-article)
              ("N" . gnus-summary-next-unread-article)
              ("P" . gnus-summary-prev-unread-article)
              ("M-n" . gnus-summary-next-thread)
              ("M-p" . gnus-summary-prev-thread)
              ("C-M-n" . gnus-summary-next-group)
              ("C-M-p" . gnus-summary-prev-group)
              ("C-M-^" . gnus-summary-refer-thread)))
6.5.3.7 Gnus server

The "server" is where your news sources are listed and from where you can browse items you would like to subscribe to (e.g. your email account's Inbox or some mailing list on Usenet). Make sure to read about these concepts in the introductory section about Gnus.

(use-package gnus-srvr
  :after gnus
  :hook ((gnus-browse-mode-hook gnus-server-mode-hook) . hl-line-mode))
6.5.3.8 Gnus window layout

NOTE 2020-06-15: I am putting this here for visibility, but I do not care about intricate Gnus layouts.

The following is adapted from the example in the official manual. To read the chapter, evaluate (info "(gnus) Window Layout") (either do it with C-x C-e after the closing parenthesis or place this expression in the prompt of M-:). I tried it for a while: it works, but I do not care about looking at a sidebar of groups I am subscribed to. To move between groups while in the Summary view, I do C-M-{n,p} (see the keybindings in the Gnus summary section). If I do not need to switch summary views in a linear fashion, I just revisit the Group buffer and proceed from there to find the group I am interested in.

(use-package gnus-win
  :config
  (gnus-add-configuration
   '(article
     (horizontal 1.0
                 (vertical 40 (group 1.0))
                 (vertical 1.0
                           (summary 0.16 point)
                           (article 1.0)))))

  (gnus-add-configuration
   '(summary
     (horizontal 1.0
                 (vertical 40 (group 1.0))
                 (vertical 1.0 (summary 1.0 point))))))
6.5.3.9 Gnus intersection with Dired

We can use the built-in directory editor (file manager) as a more convenient way of performing certain tasks that relate to emails, such as attaching all the marked items of the dired buffer to an email we are currently composing or wish to initiate the composition of.

Run C-h m inside of a Dired buffer that has gnus-dired-mode enabled and search for "gnus" to see all the relevant key bindings and the functions they call. I only ever use C-c C-m C-a (C-m is the same as RET).

By the way, make sure to check my comprehensive Dired configurations.

(use-package gnus-dired
  :after (gnus dired)
  :hook (dired-mode-hook . gnus-dired-mode))

6.5.4 Mu4e (email client for the `mu' mail indexer)

As outlined in the opening remarks to Email settings, I use both Gnus and Mu4e. The former is my primary tool for handling email and mailing lists. While the latter is kept around for its superior search capabilities, in those cases where I really need to have a carefully considered filtered output of my messages.

Put differently, I use mu4e because Gnus' nnir backend does not work as expected with my multi-email-account maildir setup: if anyone has a maildir with multiple subdirectories that can be queried with notmuch from inside of Gnus, then please contact me.

My particular setup aside, mu4e is a superb tool. Its interface is clean. It comes with well-considered defaults. The best part though, is that it offers a front-end to the mu command line tool for email indexing and searching.

Further notes:

  • The following code is a work-in-progress, though it already works the way I want.
  • Run M-x man RET mu-query to understand how to construct a search command.
  • The prot/auth-get-field is defined in Client-agnostic email settings.
(use-package mu4e
  :load-path "/usr/share/emacs/site-lisp/"
  :config
  ;; (setq mail-user-agent 'mu4e-user-agent)
  (setq mu4e-use-fancy-chars nil)
  (setq mu4e-headers-advance-after-mark t)
  (setq mu4e-headers-auto-update t)
  (setq mu4e-headers-date-format "%F")
  (setq mu4e-headers-time-format "%R")
  (setq mu4e-headers-long-date-format "%F, %R")

  (setq mu4e-headers-fields
        '((:human-date . 12)
          (:flags . 6)
          (:mailing-list . 10)
          (:from . 22)
          (:subject)))

  (setq mu4e-get-mail-command "true")
  (setq mu4e-hide-index-messages t)
  (setq mu4e-update-interval (* 60 5))
  (setq mu4e-completing-read-function 'completing-read)
  (setq mu4e-compose-signature "Protesilaos Stavrou\nprotesilaos.com\n")
  (setq mu4e-compose-signature-auto-include t)
  (setq mu4e-maildir "~/.mail")
  (setq mu4e-attachment-dir "~/Downloads")
  (setq mu4e-sent-messages-behavior 'sent)
  (setq mu4e-view-show-addresses t)
  (setq mu4e-context-policy 'pick-first)
  (setq mu4e-compose-context-policy 'ask)
  (setq mu4e-index-lazy-check t)
  (setq mu4e-change-filenames-when-moving t) ; better for `mbsync'?
  (setq mu4e-modeline-max-width 30)
  (setq mu4e-display-update-status-in-modeline t)
  (setq mu4e-view-show-images nil)
  (setq mu4e-decryption-policy 'ask)

  (setq mu4e-contexts
        `(,(make-mu4e-context
            :name "vrp" ; Is there no way to specify a key for switching?
            :enter-func (lambda () (mu4e-message "Entering PRV"))
            :leave-func (lambda () (mu4e-message "Leaving PRV"))
            :match-func (lambda (msg)
                          (when msg
                            (mu4e-message-contact-field-matches
                             msg :to (prot/auth-get-field "prv" :user))))
            :vars `((user-mail-address . ,(prot/auth-get-field "prv" :user))))
          ,(make-mu4e-context
            :name "inf"
            :match-func (lambda (msg)
                          (when msg
                            (mu4e-message-contact-field-matches
                             msg :to (prot/auth-get-field "inf" :user))))
            :vars `((user-mail-address . ,(prot/auth-get-field "inf" :user))))
          ,(make-mu4e-context
            :name "pub"
            :match-func (lambda (msg)
                          (when msg
                            (mu4e-message-contact-field-matches
                             msg :to (prot/auth-get-field "pub" :user))))
            :vars `((user-mail-address . ,(prot/auth-get-field "pub" :user))))))

  (setq mu4e-bookmarks
        '((:name "Unread messages" :query "g:unread AND NOT g:trashed" :key ?u)
          (:name "Today's messages" :query "d:today..now" :key ?t)
          (:name "Last 7 days" :query "d:7d..now" :key ?w)
          (:name "PRV Unread"
                 :query `,(format "to:%s %s"
                                  (prot/auth-get-field "prv" :user)
                                  "AND g:unread AND NOT g:trashed")
                 :key ?v)
          (:name "PRV Inbox"
                 :query `,(format "to:%s"
                                  (prot/auth-get-field "prv" :user))
                 :key ?V)
          (:name "INF Unread"
                 :query `,(format "to:%s %s"
                                  (prot/auth-get-field "inf" :user)
                                  "AND g:unread AND NOT g:trashed")
                 :key ?i)
          (:name "INF Inbox"
                 :query `,(format "to:%s"
                                  (prot/auth-get-field "inf" :user))
                 :key ?I)
          (:name "PUB Unread"
                 :query `,(format "to:%s %s"
                                  (prot/auth-get-field "pub" :user)
                                  "AND g:unread AND NOT g:trashed")
                 :key ?p)
          (:name "PUB Inbox"
                 :query `,(format "to:%s"
                                  (prot/auth-get-field "pub" :user))
                 :key ?P)))

  :bind (("C-c M" . mu4e)
         :map mu4e-headers-mode-map
         ("!" .  (lambda (&optional arg)
                   (interactive "P")
                   (if arg
                       (mu4e-headers-mark-for-unflag)
                     (mu4e-headers-mark-for-flag))))
         ("r" . mu4e-headers-mark-for-read)
         ("u" . mu4e-headers-mark-for-unread)
         ("M-u" . mu4e-headers-mark-for-unmark)
         ("C-M-u" . mu4e-mark-unmark-all)))
6.5.4.1 mu4e extension for org-capture

With this little snippet, we allow org-capture convert any email into a note, to-do item or whatever. The killer feature is that we get a direct link back to the original email. This way, we can avoid the problem of searching through a pile of messages until we find the one we really need. Nice!

(use-package org-mu4e                   ; no need for `:ensure'
  :after (org mu4e)
  :config
  (setq org-mu4e-link-query-in-headers-mode nil))
6.5.4.1.1 TODO Review need for org-mu4e

6.5.5 Sending email (SMTP)

These are the base settings for the SMTP functionality. Passwords and other critical information are stored in ~/.authinfo.gpg, as demonstrated in the base email settings. What follows is just a mirroring of the contents of that file.

With regard to the asynchronous functionality, it is meant to improve performance by carrying out the relevant tasks in a non-blocking way.

(use-package smtpmail
  :init
  (setq smtpmail-default-smtp-server "mail.gandi.net")
  :config
  (setq smtpmail-smtp-server "mail.gandi.net")
  (setq smtpmail-stream-type 'ssl)
  (setq smtpmail-smtp-service 465)
  (setq smtpmail-queue-mail nil))

(use-package smtpmail-async
  :after smtpmail
  :config
  (setq send-mail-function 'async-smtpmail-send-it)
  (setq message-send-mail-function 'async-smtpmail-send-it))

6.5.6 Contact management (EBDB)

This is a contact manager that integrates nicely with Gnus (see previous sections in email settings). It is an alternative to the more established bbdb package. I use it to store names and addresses from my email correspondence. Nothing fancy here.

ebdb has a comprehensive manual and a broad range of customisation options. My needs are simple though, as I have basically lived without any contact management app for ages (I practised “social distancing” before it became a thing!).

This is how EBDB works inside of the Gnus "Summary" or "Article" buffers:

  • Hit ; ; to display a window on the side of the message with information about the records in the database that correspond to those in the message. If no record exists, the window will not be displayed. Alternatively, set ebdb-mua-pop-up to t to always get the window, whether empty or not.
  • With ; : you can create new database entries with the information harvested from the current message. There are ways to make this automatic, but I did not experiment with them. If I store someone's contact information, it means I have used it before or have a clear intent to do so. No automatic records, please.
  • Type ; followed by C-h to bring up a help buffer with all the available keybindings (this suffix works for every key chord, by the way).

Try M-: (info "(ebdb) Interactive Commands") for more on the topic.

Some useful interactive functions that are not specific to Gnus:

  • In a message composition buffer (using e.g. the compose-mail bound to C-x m), hit TAB while in a header field where emails are appropriate to either complete what you have typed in or get feedback on possible completion candidates.
  • ebdb-cite-records lets you input a contact in the current buffer. If you do this in an org-mode buffer, it will prefix the email address with a mailto: tag.

It is also possible to connect EBDB with the Diary and with Org's Agenda views. I have no use for such features. If, however, my needs ever evolve to something more demanding, I am confident this tool will cope with them. For the time being, this package declaration is enough.

(use-package ebdb
  :ensure
  :pin gnu                              ; Prefer ELPA version
  :config
  (use-package ebdb-gnus)
  (use-package ebdb-message)
  (setq ebdb-default-window-size 0.2)
  (setq ebdb-mua-pop-up nil)

  (defun prot/ebdb-auto-update ()
    "Conditions for updating EBDB records."
    (if (ebdb-mua-message-header "Newsgroups")
        'query
      'existing))

  (setq ebdb-mua-auto-update-p #'prot/ebdb-auto-update)
  (setq ebdb-mua-sender-update-p #'prot/ebdb-auto-update)
  (setq ebdb-mua-reader-update-p #'prot/ebdb-auto-update)
  (setq ebdb-add-aka t)
  (setq ebdb-add-name nil)
  (setq ebdb-add-mails 'query)
  (setq ebdb-complete-mail 'capf)
  (setq ebdb-mail-avoid-redundancy nil)
  (setq ebdb-complete-mail-allow-cycling 3))

6.6 Version control tools

6.6.1 Diff-mode

This covers the standard diff-mode, which I use when interfacing with the built-in Version Control framework (see the section on VC), but also find while browsing various newsgroups via Gnus (see email settings).

Overview of these tweaks:

  • Always start the buffer in a read-only state. A typo will mess things up when trying to apply a patch.
  • After applying a diff hunk move on to the next one.
  • Update hunk headers automatically following an edit to the diff.
  • Do not show word-wise ("refined") changes upon activation. I prefer to do so manually with C-c C-b for the current diff hunk. All such highlights are removed if you generate the buffer again (with g as expected) and the default is to not show word-wise changes.
  • Do not prettify headers. I like the standard "patch" looks. It also makes it easier to copy the diff elsewhere such as in an email or on an issue tracker.
  • Do not do any further syntax highlighting. This is not the place to check your pretty code, plus it will most likely render colours highly inaccessible.

Basics for diff-mode buffers (as always you can learn more about the current buffer's major mode with C-h m—read How do you learn Emacs? in the FAQ below):

  • C-c C-c or M-o takes you to the point of the changes in the source file. If you run this of the diff hunk's heading, you go to the beginning of the context. But if you place the point somewhere inside of the diff's added changes or context, you will visit that exact position in the original file (does not work for removed text because technically it does not exist). Awesome feature!
  • When working with patches to source code, which are distributed e.g. through email, you can apply the current hunk with C-c C-a or test for compatibility with C-c C-t. This is a nice way to easily merge contributions from others, without having to go through the workflow of some proprietary Git/Version-Control forge.
  • By default C-c C-n offers a convenience wrapper for Emacs' narrowing commands (such as C-x n n for narrow-to-region. The standard is to narrow to the current diff hunk. With the C-u prefix it will narrow to the current file instead (obviously this only makes sense when inspecting a multi-file set of changes). If you use the default, you must manually exit narrowing with the standard C-x n w, but for this case I prefer prot/diff-restrict-view-dwim which will widen the view when narrowing is already in effect, else narrow as intended (I bind it to C-c C-n to replace the original).
  • With prot/diff-buffer-with-file I have a simple wrapper around a built-in command that compares a buffer to its underlying file. This compares the changes made since the last save and my wrapper is about picking the current buffer directly. If there are no differences, then the command with run vc-diff instead. Note that in my configurations for the Generic version control (VC) framework, I remap the keys that pertain to diffs.
  • With M-n and M-p you move between hunks. With M-} and M-{ or M-N, M-P do the same between files.
(use-package diff
  :commands prot/diff-refine-buffer
  :config
  (setq diff-default-read-only t)
  (setq diff-advance-after-apply-hunk t)
  (setq diff-update-on-the-fly t)
  ;; The following are from Emacs 27.1
  (setq diff-refine nil)                ; I do it on demand
  (setq diff-font-lock-prettify nil)    ; better for patches
  (setq diff-font-lock-syntax nil)      ; good for accessibility

  (defun prot/diff-buffer-with-file (&optional arg)
    "Compare buffer to its file, else run `vc-diff'.
With \\[universal-argument] also enable highlighting of word-wise
changes, local to the current buffer."
    (interactive "P")
    (let ((buf nil))     ; this method will "fail" if multi diff buffers
      (if (buffer-modified-p)
          (progn
            (diff-buffer-with-file (current-buffer))
            (setq buf "*Diff*"))
        (vc-diff)
        (setq buf "*vc-diff*"))
      (when arg
        (with-current-buffer (get-buffer buf)
          (setq-local diff-refine 'font-lock)))))

  (defun prot/diff-refine-buffer ()
    "Produce word-wise, 'refined' diffs in `diff-mode' buffer.
Also see `prot/diff-refine-hunk-or-buf' that is a wrapper for the
current command."
    (interactive)
    (let ((position (point)))
      (when (derived-mode-p 'diff-mode)
        (setq-local diff-refine 'font-lock)
        (font-lock-flush (point-min) (point-max))
        (goto-char position))))

  (defun prot/diff-refine-hunk-or-buf (&optional arg)
    "Apply word-wise, 'refined' diffs to hunk or buffer.
With prefix ARG (\\[universal-argument]), refine the entire
buffer, else the diff hunk at point.

This is a wrapper around `prot/diff-refine-buffer' and
`diff-refine-hunk', meant to economise on key bindings."
    (interactive "P")
    (if arg
        (prot/diff-refine-buffer)
      (diff-refine-hunk)))

  (defun prot/diff-restrict-view-dwim (&optional arg)
    "Use `diff-restrict-view', or widen when already narrowed.
By default the narrowing effect applies to the focused diff hunk.
With \\[universal-argument] do it for the current file instead."
    (interactive "P")
    (when (derived-mode-p 'diff-mode)
      (if (buffer-narrowed-p)
          (progn
            (widen)
            (message "Widened the view"))
        (if arg
            (progn
              (diff-restrict-view arg)
              (message "Narrowed to file"))
          (diff-restrict-view)
          (message "Narrowed to diff hunk")))))

  ;; `prot/diff-buffer-with-file' replaces the default for `vc-diff'
  ;; (which I bind to another key---see VC section).
  :bind (("C-x v =" . prot/diff-buffer-with-file)
         :map diff-mode-map
         ("C-c C-b" . prot/diff-refine-hunk-or-buf) ; replace `diff-refine-hunk'
         ("C-c C-n" . prot/diff-restrict-view-dwim)))

6.6.2 Generic version control (VC) framework

VC is a generic framework that works with several version control systems, else "backends". In practice though, I only ever use it with git. Compared with magit (see section on Magit configs), vc offers a more abstract, buffer-oriented workflow that, I feel, covers all common version control cases.

With VC we can track and ignore files, commit changes, view diffs and logs, push and pull from a remote… Everything you would expect from a version control system. But I am not sure it is possible or convenient to perform tasks such as staging only a particular part of a diff, handling multiple remotes, interactively rebasing the commit log, etc.

As such, I employ VC as my generalist interface to the most common Git interactions: diffs, commits, logs. While I rely on Magit for expedient access to the more advanced features of Git, all of which are rendered approachable through an interactive/modal interface.

I started using VC with this a common scenario: to produce a diff of my current changes formatted as a ready-to-apply patch. This is done with vc-diff (also see the vc-git-diff-switches variable). A diff hunk can be applied while in the *vc-diff* buffer by hitting C-c C-a. Fairly easy. One can always save the diff buffer using the standard C-x C-w command (write-file). Use these to send patches (e.g. via email), without having to go through proprietary web tools.

Another nice feature is a region-specific commit log with C-x v h (vc-region-history). Highlight a region and run the command to get a clear view of how it took form. Quite an easy way to "git blame". For a more macroscopic view of the file's entire history you can always rely on C-x v g or my alias C-x v a which calls vc-annotate. Commits are colour-coded with those on the red side of the spectrum being the newest ones, while those on the blue end representing the older ones.

C-x v v (vc-next-action) is a hidden gem: a paradigm of minimalism done right. On a new project it will first ask you for a backend. Selecting git will run git init. Invoking the command again will track the file and commit the changes. If the file is already being tracked, it will just proceed to the next step. It always follows a logical sequence which translates into a powerful metaphor of going through the process of committing changes to files.

The commit buffer presents you with a summary section followed by the body of the commit message, separated by an empty line (shown as a border) as per the conventions that govern good commit messages. In the lower part you can see the affected files (just the current file by default). The standard C-c C-c confirms the commit, while C-c C-k cancels it. Furthermore, there is C-c C-f to view the file set concerned (in case it is not already visible below the message area), as well as C-c C-d to produce a diff of what is being committed.

You can review the commit log for the current file with C-x v l (vc-print-log) or for the entirety of the current project with C-x v L (vc-print-root-log). From there you can display any diff with d or find the prior state of the commit at point with f. Use the latter as a starting point for reverting to a prior state. Also note that while inside the root log view, you can use an active region to show diffs in that range of commits.

With vc-dir, which I rebind to the more logical "project" mnemonic of C-x v p (prot/vc-dir-project), you can mark with m several files to add to a commit. Use M to mark all files with the same status. This mode offers easy access to the standard VC actions. To commit the file at point or the marked ones hit v. To push do P. Same principle for diffs, logs, etc.

By the way, rebinding vc-dir has another upside of allowing its original key binding to be assigned to vc-diff; which in turn makes that mnemonically consistent with the C-x v D sequence, else vc-root-diff (make sure to check my key re-bindings or aliases).

Some more common actions (read their docstrings for further help):

Command Key chord VC-dir key
vc-update C-x v + +
vc-push C-x v P P
vc-log-incoming C-x v I I
vc-log-outgoing C-x v O O

Based on these (and there are more), you can already see how VC may be used as your main tool for version control, from committing changes, to pulling/pushing from/to a remote, viewing commit logs, etc. It depends on your needs and preferences. Run C-x v C-h to get a full list of the possible commands. Read the docstring of each command for further information. Then in each of the various VC modes try C-h m for further help. Also consult the manual and make liberal use of C-h f or C-h v.

Changes to all tracked files are highlighted in the fringe thanks to the diff-hl package which is defined elsewhere in this document (as I consider it an "interface" element). Any rules that control the placement of VC-related (and other) buffers are defined in the section on window rules and basic tweaks (specifically, refer to the variable display-buffer-alist).

Also watch my Introduction to the Emacs Version Control framework (2020-03-30).

Careful with the keybindings I define. Many of them do other things by default and I might still review them further. When in doubt, stick with the defaults.

(use-package vc
  :config
  (setq vc-find-revision-no-save t)

  (use-package add-log
    :config
    ;; I use a different account for git commits
    (setq add-log-mailing-address "info@protesilaos.com")
    (setq add-log-keep-changes-together t))

  (use-package log-view
    :config
    (defun prot/vc-print-log (&optional arg)
      "Like `vc-print-log' but for a custom fileset.

With optional prefix ARG (\\[universal-argument]), query for a
number to limit the log to.  Then prompt the user for matching
files in the `default-directory'.  A literal space delimits
multiple files (inserting a space will renew the prompt, asking
for another file match).

In a `dired-mode' buffer, print log for the file at point, or any
marked files, except for when a double prefix argument is passed.
A single prefix arg still provides for a limit to the log.

If a double prefix ARG is passed, prompt for a limit and produce
a log that covers all files in the present directory."
      (interactive "P")
      (let* ((lim (if arg
                      (read-number "Limit log to N entries: " 5)
                    20))
             (dir default-directory)
             (dotless directory-files-no-dot-files-regexp)
             (files (directory-files dir nil dotless t))
             (crm-separator " ")
             (set (cond
                   ((equal arg '(16))
                    files)
                   ((eq major-mode 'dired-mode)
                    (dired-get-marked-files t nil))
                   (t
                    (icomplete-vertical-do ()
                      (completing-read-multiple
                       "Select files in current dir: " files nil t)))))
             (backend (vc-backend set)))
        (vc-print-log-internal backend set nil nil lim 'with-diff)))

    (defun prot/log-view-extract-commit ()
      "Kill commit from around point in `vc-print-log'."
      (interactive)
      (let ((commit (cadr (log-view-current-entry (point) t))))
        (kill-new (format "%s" commit))
        (message "Copied: %s" commit)))

    :bind (("C-x v SPC" . prot/vc-print-log)
           :map log-view-mode-map
           ("<tab>" . log-view-toggle-entry-display)
           ("<return>" . log-view-find-revision)
           ("w" . prot/log-view-extract-commit)
           ("s" . vc-log-search)
           ("o" . vc-log-outgoing)
           ("f" . vc-log-incoming)
           ("F" . vc-update)
           ("P" . vc-push)))

  :bind (("C-x v b" . vc-retrieve-tag)  ; "branch" switch
         ("C-x v t" . vc-create-tag)
         ("C-x v f" . vc-log-incoming)  ; the actual git fetch
         ("C-x v F" . vc-update)        ; "F" because "P" is push
         ("C-x v d" . vc-diff)))

(use-package vc-dir
  :config
  (defun prot/vc-dir (&optional arg)
    "Run `vc-dir' for the current project or directory.
With optional ARG (\\[universal-argument]), use the present
working directory, else default to the root of the current
project, as defined by `vc-root-dir'."
    (interactive "P")
    (let ((dir (if arg default-directory (vc-root-dir))))
      (vc-dir dir)))
  :bind (("C-x v p" . prot/vc-dir)
         :map vc-dir-mode-map
         ("b" . vc-retrieve-tag)
         ("t" . vc-create-tag)
         ("o" . vc-log-outgoing)
         ("f" . vc-log-incoming) ; replaces `vc-dir-find-file' (use RET)
         ("F" . vc-update)       ; symmetric with P: `vc-push'
         ("d" . vc-diff)         ; align with D: `vc-root-diff'
         ("k" . vc-dir-clean-files)))

(use-package vc-git
  :config
  (setq vc-git-diff-switches '("--patch-with-stat" "--histogram"))
  (setq vc-git-print-log-follow t)
  (setq vc-git-revision-complete-only-branches nil) ; Emacs 28
  (setq vc-git-root-log-format
        '("%d%h %ad %an: %s"
          ;; The first shy group matches the characters drawn by --graph.
          ;; We use numbered groups because `log-view-message-re' wants the
          ;; revision number to be group 1.
          "^\\(?:[*/\\| ]+ \\)?\
\\(?2: ([^)]+)\\)?\\(?1:[0-9a-z]+\\) \
\\(?4:[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\) \
\\(?3:.*?\\):"
          ((1 'log-view-message)
           (2 'change-log-list nil lax)
           (3 'change-log-name)
           (4 'change-log-date)))))

(use-package vc-annotate
  :config
  (setq vc-annotate-display-mode 'scale) ; scale to oldest
  :bind (("C-x v a" . vc-annotate)       ; `vc-update-change-log' is not in git
         :map vc-annotate-mode-map
         ("<tab>" . vc-annotate-toggle-annotation-visibility)))
6.6.2.1 Commit log editing

The log-edit library defines a set of general commands that we can use to, inter alia, commit changes to Git, using the surprisingly powerful built-in Version Control (VC) framework.

Overview of the following tweaks:

  • While editing a commit message, only prompt for confirmation if the list of files has changed since the beginning of the editing session.
  • Once the commit is done, remove its buffer.
  • Always add a newline.
  • Do not offer to manually write an Author: header. Though this can be useful if someone sends a patch.

Also make sure to read the guide for writing a Git commit message. I always use auto-fill-mode to wrap lines to the value of fill-column (see relevant configs).

(use-package log-edit
  :config
  (setq log-edit-confirm 'changed)
  (setq log-edit-keep-buffer nil)
  (setq log-edit-require-final-newline t)
  (setq log-edit-setup-add-author nil))

6.6.3 Interactive git front-end (Magit)

As noted in the section on the built-in Version Control framework I use Magit for easy access to the advanced features of Git. While I rely on the built-in tools for all day-to-day operations.

Magit offers a modal interface where the full power of git is neatly organised in sets of keys that are directly accessible without holding down any modifiers.

While inside the magit-status buffer, hit ? to produce a transient menu with the possible vectors to action. Do it again inside each of the Magit buffers to view the keys that work for their context.

Consider viewing my Introduction to Magit (2020-04-04) for how to stage diffs, commit changes, view logs, create branches, and so on.

6.6.3.1 Base Magit settings

Magit has great defaults. I only found a few things that I would like to customise, which I do in the following package declarations.

(use-package magit
  :ensure
  :bind ("C-c g" . magit-status))
6.6.3.2 Magit commits

The following package is configured in accordance with the guidelines provided by this article on writing a Git commit message. The gist is to write commits that are clean and easy to read. The fill-column is set elsewhere in this document to 72 characters long.

(use-package git-commit
  :after magit
  :config
  (setq git-commit-summary-max-length 50)
  (setq git-commit-known-pseudo-headers
        '("Signed-off-by"
          "Acked-by"
          "Modified-by"
          "Cc"
          "Suggested-by"
          "Reported-by"
          "Tested-by"
          "Reviewed-by"))
  (setq git-commit-style-convention-checks
        '(non-empty-second-line
          overlong-summary-line)))
6.6.3.3 Magit diffs

The settings below are for the diff screens that Magit produces. I just want to highlight changes within a line, not just the line itself. I enable it only for the focused hunk (there is an option for 'all).

(use-package magit-diff
  :after magit
  :config
  (setq magit-diff-refine-hunk t))
6.6.3.4 Magit repo list

When maintaining a number of projects, it sometimes is necessary to produce a full list of them with their corresponding Magit status. That way you can determine very quickly which repositories need to be examined further.

(use-package magit-repos
  :after magit
  :commands magit-list-repositories
  :config
  (setq magit-repository-directories
        '(("~/Git/Projects" . 1))))

6.6.4 Smerge and Ediff

Read this section, because it matters more than the code below it.

Sometimes we face a situation where we have conflicting versions of a file and the version control backend cannot solve them on its own. This can happen fairly often when collaborating with other people or, more generally, when we keep our work spread across multiple feature branches with diverging histories. Whenever such conflicts arise, Emacs will automatically annotate the offending files with special markers that show the conflicting differences. Visiting those files will then activate smerge-mode. At which point we are in control.

Smerge revolves around the concept of dividing the conflicting part into an "upper" (red) and a "lower" section (green), possibly with their common ancestor or last point of convergence in the middle (yellow).

With this in mind we can operate on the marked differences by relying on the functions that Smerge provides, all of which are accessed by default through the common prefix of C-c ^. Start by typing the prefix followed by C-h to see all possible key chords. These are the commands I have used the most:

  • C-c ^ u (smerge-keep-upper)
  • C-c ^ l (smerge-keep-lower)
  • C-c ^ b (smerge-keep-base)
  • C-c ^ a (smerge-keep-all)
  • C-c ^ n (smerge-next)
  • C-c ^ p (smerge-prev)

Also check the defhydra I provide for it to make things simpler.

Proceed to edit the file the way you want until no more conflicts exist. You can also do things through manual editing, with standard commands and motions, but that can be prone to errors (which lead to more conflicts). At any moment in this process, you can switch to ediff, which offers a more powerful way of working with differences. Type the key chord C-c ^ E (smerge-ediff).

Ediff is a powerhouse that is likely to cover all your needs in this area (including those you are not aware of). For our purposes, what matters is to understand the basic concepts.

The way this tool works is that it starts by producing a layout of the two conflicting versions with access to a "control panel" for operating on them. By default, the panel is positioned on a new frame, but I find that rather awkward—my config puts it inside an Emacs window instead. While focusing the control panel, you can move between each diff range with n and p. The focused section will be coloured using red, green, and yellow, while all other diffs will be presented in gray.

On each diff, you have three options: to use the version of buffer A (red), of buffer B (green), or a combination of the two. The keys for each of those are a, b, and + respectively. Your choice will be reflected in buffer C (the yellow one). Use these to resolve all conflicts and then quit the session with q.

Concerning the combination of versions between A and B, Ediff has the behaviour of also inserting as plain text the annotation markers that Smerge relied on. As of this writing (2020-04-10), I am not aware of an automatic or convenient way to omit those prior to confirming our edits. To that end, I tweak the wording of the markers to some unique string (see package below) and then run flush-lines to remove them before saving the resulting buffer (so right after the q). For more on this, check prot/ediff-flush-combination-pattern.

For git users, to actually reference the common ancestor (the point before the branching paths started) we must run this command once in our command-line prompt (writes to your global .gitconfig file):

git config --global merge.conflictStyle diff3

This is optional, but I find that I like it. At any rate, the configurations I have below are straightforward (learn more about this powerful tool by hitting ? inside of its control panel and by consulting its comprehensive manual):

  • Do not keep all the buffers after exiting the Ediff session.
  • Keep buffers in an editable state. Otherwise it is impossible to perform the changes we are interested in.
  • Show the common ancestor in another buffer. This helps provide further context of how things took their form.
  • Show only the conflicting parts. This is not a review of all diffs.
  • Prefer putting windows side-by-side, rather than one below the other.
  • Do not enter the ediff session in a new frame. This also means that the control panel will be inside an Emacs window (at the bottom part) rather than in a tiny frame of its own.

There actually is not much in terms of Smerge-related configurations. The package is small and does one thing well. I do, however, specify a hydra (as explained in the relevant section) to make it easier to navigate conflicts. Since smerge-mode only gets activated when visiting a file with diff conflicts, we can load the hydra upon accessing it. Makes things nice and simple. No need to have a separate key binding.

Also watch my video of Smerge and Ediff for git conflict resolution (2020-04-10).

(use-package smerge-mode
  :config
  (defhydra prot/hydra-smerge-mode
    (:color pink :hint nil :post (smerge-auto-leave))
    "
   ^Motions^      ^Actions^
---^^-------------^^-------
_n_: Next      _b_: Base
_p_: Prev      _u_: Upper
^^             _l_: Lower
^^             _a_: All
"
    ("n" smerge-next)
    ("p" smerge-prev)
    ("b" smerge-keep-base)
    ("u" smerge-keep-upper)
    ("l" smerge-keep-lower)
    ("a" smerge-keep-all)
    ("q" nil "cancel" :color blue))

  (defun prot/smerge-mode-hydra ()
    "Load `prot/hydra-smerge-mode' in `smerge-mode'."
    (when smerge-mode
      (prot/hydra-smerge-mode/body)))
  :hook ((vc-find-file-hook . prot/smerge-mode-hydra)
         (magit-diff-visit-file-hook . prot/smerge-mode-hydra)))

(use-package ediff
  :config
  (setq ediff-keep-variants nil)
  (setq ediff-make-buffers-readonly-at-startup nil)
  (setq ediff-merge-revisions-with-ancestor t)
  (setq ediff-show-clashes-only t)
  (setq ediff-split-window-function 'split-window-horizontally)
  (setq ediff-window-setup-function 'ediff-setup-windows-plain)

  ;; Tweak those for safer identification and removal
  (setq ediff-combination-pattern
        '("<<<<<<< prot-ediff-combine Variant A" A
          ">>>>>>> prot-ediff-combine Variant B" B
          "####### prot-ediff-combine Ancestor" Ancestor
          "======= prot-ediff-combine End"))

  ;; TODO automate process in a robust way, or at least offer a good key
  ;; binding.
  (defun prot/ediff-flush-combination-pattern ()
    "Remove my custom `ediff-combination-pattern' markers.

This is a quick-and-dirty way to get rid of the markers that are
left behind by `smerge-ediff' when combining the output of two
diffs.  While this could be automated via a hook, I am not yet
sure this is a good approach."
    (interactive)
    (flush-lines ".*prot-ediff.*" (point-min) (point-max) nil)))

6.7 Shells and terminal emulators

It should come to no surprise that Emacs can operate as both a terminal emulator for command line shells and toolkit for terminal emulators. The present section covers both cases.

6.7.1 Command-line shells

6.7.1.1 Shell (M-x shell)

This is a shell (Bash, in my case) that runs inside of Emacs. Unlike the terminal emulators defined below, this one can use standard Emacs keys and behaves much like an ordinary buffer. It also integrates nicely with the built-in completion tools, which makes it particularly nice to work with (start from Tools for completion candidate listing).

The one area where this Shell differs substantially from ordinary buffers is with regard to the command prompt: you can re-run a command on the scroll-back buffer by just hitting RET while point is on its line (no need to go back to the end and cycle the command history with M-p or M-n).

Another peculiarity relative to the standard commands in the terminal is to search backward through your history with M-r (whereas in a terminal emulator you use C-r).

Run C-h m inside of a shell buffer to learn about all the key bindings and corresponding functions.

(use-package shell
  :commands shell-command
  :config
  (setq ansi-color-for-comint-mode t)
  (setq shell-command-prompt-show-cwd t) ; Emacs 27.1
  :bind (("<s-S-return>" . shell)))
6.7.1.2 Eshell (like M-x shell but in elisp)

While I did read the eshell manual fairly early in my introduction to Emacs (July 2019), I failed to appreciate its added value. Sure, you could run Elisp commands as well as those of the standard shells. But that did not seem like an obvious step forward. More of a lateral movement at best, for it was asking for a whole new way of thinking about the shell that could potentially complicate things in other areas.

After gaining some experience with Emacs, I now (April 2020) have developed a newfound appreciation of Eshell's flexibility. The fact that it can understand elisp means that you can develop entirely new modes of interaction that involve the command line and extend into other Emacs major modes. For example, you can cd into a directory and then dired to access the full power of Emacs' superb file manager (also check the section on Dired).

The core value of Eshell is that it behaves like an ordinary buffer. This means that we can build our own functions for moving around and operating on text. prot/eshell-put-last-output-to-buffer puts that in concrete terms. The function will take the output of the last command and put it in a new buffer. You can then select that buffer, edit it as you will and, say, C-x C-w to write it permanently to a file.

Here is another case where integration with standard Emacs commands truly shines. In Eshell, cd followed by the equals sign will produce a numbered list of the directories you have accessed. With cd -NUM you can switch to the one you want. So far so good, how about we also switch to a dired buffer from there? Do it with cd -NUM && dired.

Speaking of directories, eshell-cd-on-directory allows you to omit cd and just type in the path to a directory. Awesome! And while on the topic of Dired, you can use dired-jump to always switch to a dired buffer of the present working directory. To that I add my own little utility (prot/eshell-file-parent-dir) for jumping to the directory of the file at point: very useful in cases where you get files from different sources, such as with find (though do also check the various options for running find with dired, like find-name-dired).

Extensibility is seamless. prot/eshell-complete-history offers a completion interface to the input history. No need for awkard back and forth or for searching for a regexp without any immediate feedback. In a similar fashion prot/eshell-complete-recent-dir provides completion for switching directly to a recent directory. While the more general prot/eshell-find-subdirectory-recursive lets you switch to a subdirectory somewhere inside the path you are.

Instead of outlining the remaining features of Eshell, I strongly encourage you to read the manual. It is fairly short. What I have here is just an early sample of what can be achieved with a few tweaks.

Also check these valuable resources:

Finally, a few notes about contributions I have received:

  • Phil Hudson (@dije on Gitlab) explained how concat is superfluous when combined with format and demonstrated how to use a specifier (%s) to make a uniform string out of the output of some command combined with predefined text.
  • Phil also helped me with prot/eshell-find-subdirectory-recursive by highlighting some inefficient routines in the original design. This inspired me to revisit the code and make it cleaner overall. The gist is that only the present working directory needs to be abbreviated.
  • Another user who preferred to remain anonymous sent me contrib/eshell-cat-with-syntax-highlight which will apply syntax highlighting to the output of a cat command. The code I got was using define-advice, so I tweaked it a bit to work with advice-add because I find that syntax easier to follow. I also made minor tweaks to the docstring.
  • Same principle as above for contrib/electrify-ls.
  • Any errors are my own.
(use-package eshell
  :bind ("<s-return>" . eshell))

(use-package esh-mode
  :commands prot/eshell-minor-mode
  :config
  ;; used by other functions below
  (declare-function ffap-file-at-point "ffap.el")

  (defmacro prot/eshell-ffap (name doc &rest body)
    "Make commands for `eshell' find-file-at-point.
NAME is how the function is called.  DOC is the function's
documentation string.  BODY is the set of arguments passed to the
`if' statement to be evaluated when a file at point is present."
    `(defun ,name ()
       ,doc
       (interactive)
       (let ((file (ffap-file-at-point)))
         (if file
             ,@body
           (user-error "No file at point")))))

  (prot/eshell-ffap
   prot/eshell-insert-file-at-point
   "Insert (cat) contents of file at point."
   (progn
     (goto-char (point-max))
     (insert (concat "cat " file))
     (eshell-send-input)))

  (prot/eshell-ffap
   prot/eshell-kill-save-file-at-point
   "Add to kill-ring the absolute path of file at point."
   (progn
     (kill-new (concat (eshell/pwd) "/" file))
     (message "Copied full path of %s" file)))

  (prot/eshell-ffap
   prot/eshell-find-file-at-point
   "Run `find-file' for file at point (ordinary file or dir).
Recall that this will produce a `dired' buffer if the file is a
directory."
   (find-file file))

  (prot/eshell-ffap
   prot/eshell-file-parent-dir
   "Open `dired' with the parent directory of file at point."
   (dired (file-name-directory file)))

  (defun prot/eshell-put-last-output-to-buffer ()
    "Produce a buffer with output of last `eshell' command."
    (interactive)
    (let ((eshell-output (buffer-substring-no-properties
                          (eshell-beginning-of-output)
                          (eshell-end-of-output))))
      (with-current-buffer (get-buffer-create "*last-eshell-output*")
        (erase-buffer)
        (insert eshell-output)
        (switch-to-buffer-other-window (current-buffer)))))

  (defun prot/eshell-complete-redirect-to-buffer ()
    "Complete the syntax for appending to a buffer via `eshell'."
    (interactive)
    (insert
     (format " >>> #<%s>"
             (read-buffer-to-switch "Switch to buffer: "))))

  (defun prot/eshell-narrow-output-highlight-regexp ()
    (interactive)
    (let ((regexp (read-regexp "Regexp to highlight")))
      (narrow-to-region (eshell-beginning-of-output)
                        (eshell-end-of-output))
      (goto-char (point-min))
      (highlight-regexp regexp 'hi-yellow)))

  ;;;; NOTE by Prot 2020-06-16: the following two advice-add snippets
  ;;;; will need to be reviewed to make sure they do not produce
  ;;;; undesirable side effects.

  ;; syntax highlighting implementation modified from
  ;; https://emacs.stackexchange.com/questions/50385/use-emacs-syntax-coloring-when-not-in-emacs
  ;;
  ;; This command also makes it possible to, e.g., cat an encrypted and/or
  ;; compressed file.
  (defun contrib/eshell-cat-with-syntax-highlight (&rest args)
    "Like `eshell/cat' but with syntax highlighting.
To be used as `:override' advice to `eshell/cat'."
    (setq args (eshell-stringify-list (flatten-tree args)))
    (dolist (filename args)
      (let ((existing-buffer (get-file-buffer filename))
            (buffer (find-file-noselect filename)))
        (eshell-print
         (with-current-buffer buffer
           (if (fboundp 'font-lock-ensure)
               (font-lock-ensure)
             (with-no-warnings
               (font-lock-fontify-buffer)))
           (let ((contents (buffer-string)))
             (remove-text-properties 0 (length contents) '(read-only nil) contents)
             contents)))
        (unless existing-buffer
          (kill-buffer buffer)))))

  (advice-add 'eshell/cat :override #'contrib/eshell-cat-with-syntax-highlight)

  ;; Turn ls results into clickable links.  Especially useful when
  ;; combined with link-hint.  Modified from
  ;; https://www.emacswiki.org/emacs/EshellEnhancedLS
  (define-button-type 'eshell-ls
    'supertype 'button
    'help-echo "RET, mouse-2: visit this file"
    'follow-link t)

  (defun contrib/electrify-ls (name)
    "Buttonise `eshell' ls file names.
Visit them with RET or mouse click.  This function is meant to be
used as `:filter-return' advice to `eshell-ls-decorated-name'."
    (add-text-properties 0 (length name)
                         (list 'button t
                               'keymap button-map
                               'mouse-face 'highlight
                               'evaporate t
                               'action #'find-file
                               'button-data (expand-file-name name)
                               'category 'eshell-ls)
                         name)
    name)

  (advice-add 'eshell-ls-decorated-name :filter-return #'contrib/electrify-ls)

  ;; XXX HACK: This and the following minor mode are only introduced to
  ;; circumvent an issue I have encountered with `eshell-mode-map' being
  ;; set to nil and breaking my key bindings by forcing them into the
  ;; global map.
  ;;
  ;; What I do is to define a minor mode that runs from
  ;; `eshell-mode-hook' and pass all my custom key bindings there.
  (defvar prot/eshell-minor-mode-map
    (let ((map (make-sparse-keymap)))
      (define-key map (kbd "M-n") 'outline-next-visible-heading)
      (define-key map (kbd "M-k") 'eshell-kill-input)
      (define-key map (kbd "C-c w") 'prot/eshell-kill-save-file-at-point)
      (define-key map (kbd "C-c i") 'prot/eshell-insert-file-at-point)
      (define-key map (kbd "C-c f") 'prot/eshell-find-file-at-point)
      (define-key map (kbd "C-c C-f") 'prot/eshell-find-file-at-point)
      (define-key map (kbd "C-c o") 'prot/eshell-put-last-output-to-buffer)
      (define-key map (kbd "C-c >") 'prot/eshell-complete-redirect-to-buffer)
      (define-key map (kbd "C-c C-j") 'prot/eshell-file-parent-dir)
      (define-key map (kbd "C-c h") 'prot/eshell-narrow-output-highlight-regexp)
      map)
    "Key map with custom commands for `eshell'.")

  ;; HACK: See comment above.
  (define-minor-mode prot/eshell-minor-mode
    "Special minor mode to enable custom keys in `eshell'.

\\{prot/eshell-minor-mode-map}"
    :init-value nil
    :keymap prot/eshell-minor-mode-map)

  :hook (eshell-mode-hook . prot/eshell-minor-mode))

(use-package esh-module
  :config
  (setq eshell-modules-list             ; Needs review
        '(eshell-alias
          eshell-basic
          eshell-cmpl
          eshell-dirs
          eshell-glob
          eshell-hist
          eshell-ls
          eshell-pred
          eshell-prompt
          eshell-script
          eshell-term
          eshell-tramp
          eshell-unix)))

(use-package em-dirs
  :after esh-mode
  :config
  (setq eshell-cd-on-directory t))

(use-package em-tramp
  :after esh-mode
  :config
  (setq password-cache t)
  (setq password-cache-expiry 600))

(use-package em-hist
  :after esh-mode
  :config
  (setq eshell-hist-ignoredups t)
  (setq eshell-save-history-on-exit t)

  (defun prot/eshell-complete-history ()
    "Insert element from `eshell' history using completion."
    (interactive)
    (let ((hist (ring-elements eshell-history-ring)))
      (insert
       (completing-read "Input history: " hist nil t))))

  (defun prot/eshell-complete-recent-dir (&optional arg)
    "Switch to a recent `eshell' directory using completion.
With \\[universal-argument] also open the directory in a `dired'
buffer."
    (interactive "P")
    (let* ((dirs (ring-elements eshell-last-dir-ring))
           (dir (icomplete-vertical-do ()
                  (completing-read "Switch to recent dir: " dirs nil t))))
      (insert dir)                      ; Not good enough
      (eshell-send-input)               ; Should cd directly…
      (when arg
        (dired dir))))

  ;; `cl-remove-if' is used right below
  (declare-function cl-remove-if "cl-seq")

  (defun prot/eshell-find-subdirectory-recursive ()
    "Recursive `eshell/cd' to subdirectory.
This command has the potential for infinite recursion: use it
wisely or prepare to use `eshell-interrupt-process'."
    (interactive)
    (let* ((dir (abbreviate-file-name (eshell/pwd)))
           (contents (directory-files-recursively dir ".*" t nil nil))
           (dirs (cl-remove-if (lambda (x)
                                 (or (not (file-directory-p x))
                                     (string-match-p "\\.git" x)))
                               contents))
           (selection (icomplete-vertical-do ()
                        (completing-read
                         (format "Find sub-dir from %s (%s): "
                                 (propertize dir 'face 'success)
                                 (length dirs))
                         dirs nil t))))
      (insert selection)
      (eshell-send-input)))

  :bind (:map prot/eshell-minor-mode-map
              ("M-s" . nil) ; I use this for lots of more useful commands
              ("M-r" . prot/eshell-complete-history) ; use this to find input history
              ("C-c d" . prot/eshell-find-subdirectory-recursive)
              ("C-c =" . prot/eshell-complete-recent-dir)))

6.7.2 Terminals inside of Emacs

6.7.2.1 Vterm terminal emulator

The built-in terminal emulators are not up to par with the likes of Xterm and its peers. Perhaps they were a good compromise in yester years, but we have come to expect better from our system. Thankfully there is a package that might eventually makes its way into Emacs proper, provided all legal requirements are met. This is Vterm, which is an implementation of the external libvterm library (and which requires cmake in order to be compiled).

Vterm is a fully fledged terminal emulator (not mere shell) inside of Emacs. Its main differences with Shell (M-x shell) can be summarised thus:

  • Vterm can handle graphics and ANSI escape sequences. Shell cannot.
  • Shell behaves more like an ordinary Emacs buffer. Vterm is like an external application that has been embedded in the Emacs frame.
  • Vterm does tab-completion like a standard terminal. Shell can use the Emacs completion framework. To make this concrete, with Vterm if you type cd G <tab> it expands the Git subdirectory. Whereas in Shell with my completion framework (and tweaks) I can do cd G/P/m-t <tab> to expand to cd Git/Projects/modus-themes/.

My workflow is to keep the Shell as my main conduit to the command line, such as for when I need to call one of my scripts, and only use Vterm when I really need a CLI tool that is likely to produce graphical artefacts.

Pro tip: if you always can put the current buffer in another frame with the M-x tear-off-window command. Works nicely when you need to keep Vterm in sight but out of your main editing space (e.g. to put it on a secondary monitor).

(use-package vterm
  :ensure
  :commands vterm
  :config
  (setq vterm-disable-bold-font nil)
  (setq vterm-disable-inverse-video nil)
  (setq vterm-disable-underline nil)
  (setq vterm-kill-buffer-on-exit nil)
  (setq vterm-max-scrollback 9999)
  (setq vterm-shell "/bin/bash")
  (setq vterm-term-environment-variable "xterm-256color"))
6.7.2.2 Built-in terminals (fallback option)

term and ansi-term are built-in terminal emulators like Vterm. Not to be confused with the command line shells. They run inside of Emacs but are basically alien to the rest of the Emacs milieu: they do not reuse standard key bindings like C-n.

Only call those if you absolutely need them AND you have no access to either Vterm or a standalone, fully fledged terminal emulator.

As far as I can tell, based on reading the comments in term.el and elsewhere in the docs, the major difference between term and ansi-term is the ability of the latter to run one or multiple buffers simultaneously. Better check the documentation for this point. It does not seem to be a strong point, since this is also possible with the other options in the Emacs space (e.g. with C-u M-x shell).

(use-package term
  :commands (term ansi-term)
  :config
  (setq term-buffer-maximum-size 9999)
  (setq term-completion-autolist t)
  (setq term-completion-recexact t)
  (setq term-scroll-to-bottom-on-output nil))

6.8 Tools for manual pages (manpages)

Emacs offers a couple of commands for reading manual pages: man and woman. The former relies on the standard Unix tools, while the latter is an elisp implementation of the same idea. As I only ever run a GNU/Linux system, I am okay with just man.

Why bother?

  • All the goodies of consistency: fonts, themes, operating on text with your familiar Emacs functionality, handling buffers…
  • Each manpage provides direct links to other items it references.

What you can do inside such a buffer (with minor tweaks by me):

  • Hit i to go to the information node you want using completion (same principle as with the Info pages of C-h i and the like).
  • g will generate the buffer anew. Do it to reformat the text manually, though this should also happen automatically when adjusting a window's size.
  • n and p move between section headings.
  • Hit RET while over a referenced manpage to produce a new buffer with its contents.
  • s takes you directly to the familiar "See Also" section.
  • Use m to search for another manpage using your completion framework. If you invoke this command while point is over a referenced manpage, it becomes the default choice (same concept as with common help commands, C-h f, C-h v, and with many others like find-library).

Need to filter out those man buffers? Check my Ibuffer entry.

While there are customisation options for this tool, I find the defaults to work as expected. Note that the capitalisation of those symbols is canonical.

(use-package man
  :bind (:map Man-mode-map
              ("i" . Man-goto-section)
              ("g" . Man-update-manpage)))

6.9 Proced (process monitor, similar to `top')

This is a built-in tool that allows you to monitor running processes and act on them accordingly. These are the basic settings I have right now. Would need to experiment with it a bit more. It works fine though.

(use-package proced
  :commands proced
  :config
  (setq proced-auto-update-flag t)
  (setq proced-auto-update-interval 1)
  (setq proced-descend t)
  (setq proced-filter 'user))

And with this nimble tool we get live narrowing of the list, based on the terms of our search.

(use-package proced-narrow
  :ensure
  :after proced
  :diminish
  :bind (:map proced-mode-map
              ("/" . proced-narrow)))

6.10 Pass interface (password-store)

The external pass program, aka "password-store", is a password manager that uses GPG and standard UNIX tools to handle passwords. Encrypted files are stored in a plain directory structure. Very simple, very nice: now all data is available with a variety of interfaces, such as standard CLI, a dmenu interface, a graphical front-end like qtpass, etc.

The package below provides an Emacs interface to some of the most common actions, in the form of a list of candidates that can be narrowed down (such as with icomplete). I use it to quickly store a password to the kill ring.

(use-package password-store
  :ensure
  :commands (password-store-copy
             password-store-edit
             password-store-insert)
  :config
  (setq password-store-time-before-clipboard-restore 30))

And this one adds a major mode for browsing the pass keychain. Call it with M-x pass. There is a helpful section at the top with key bindings and their functions.

(use-package pass
  :ensure
  :commands pass)

6.11 Elfeed (RSS/Atom feed reader)

This is a standalone feed reader by Christopher Wellons that comes with good defaults and is very well designed overall. It treats the stream of updates as a flat list that can be narrowed incrementally using an efficient tagging system.

In terms of feed format specifications, i.e. Atom vs RSS, things should just work without any kind of configuration from your side (a huge plus compared to, say, Gnus' nnrss backend that only supports the latter).

Elfeed stores feed sources in a single list that associates a valid URL to one or more tags. These are then written to a database. The Elisp part users should care about looks like this:

(setq elfeed-feeds
      '(("https://www.archlinux.org/feeds/news/" linux distro)
        ("https://planet.emacslife.com/atom.xml" emacs community)
        ("https://www.ecb.europa.eu/rss/press.html" economics eu)
        ("http://feed.pippa.io/public/shows/teamhuman" podcast culture)
        ("https://www.youtube.com/feeds/videos.xml?channel_id=UC0uTPqBCFIpZxlz_Lv1tk_g" personal video)
        ("https://protesilaos.com/politics.xml" personal)))

I keep the actual list in a GPG-encrypted file. Emacs can decrypt and read those transparently, making it a great way to safely store sensitive data while still keeping everything perfectly functional.

A good tagging system for your feeds will offer a strong foundation for catching up with the news. I generally use 2-3 tags per feed, while I make sure that conceptually similar items will share at least one tag. My tags are not particularly sophisticated, though they are not random either: for example, I have a particular "EU" entry for all institutions, bodies, agencies, etc. and then I have other more general ones, such as "politics" and "economics".

The user interface consists of two distinct types of buffers:

  1. The *elfeed-search* buffer that holds the list with all the news items.
  2. The individual item entries.

By default, hitting s (elfeed-search-live-filter) in the search buffer will place the point inside the minibuffer, where you can then edit the applicable filters. The Elfeed README offers a detailed explanation of how to apply such filters. The ones I use the most:

  • Prepend a + to the name of a tag to only show items that include that tag. With - show items that do not include it.
  • Type in a regular string with the equals sign in front of it, say "=Prot", to show feeds whose name contains it.

Other common cases are regular expressions and date ranges, though I have found that I never use those. Probably because the tagging system is sufficiently powerful for my particular needs.

With regard to the package declaration below, I have lots of functions that customise the behaviour of Elfeed to better match my expectations. For example, I do not like the fact that the aforementioned default s command (elfeed-search-live-filter) does not provide a completion UI. So I implement an alternative that allows for multiple choices, as well as arbitrary input. In the case of the latter, you may need to "force insert" the final command that locks in the filter with C-j (exit-minibuffer), if Emacs complains that there is no matching input (has not happened in my case). I define that key binding in my comprehensive configurations on Tools for completion candidate listing.

Another function of interest which you will not find herein, is the integration with my prot/buffers-major-mode, which I bind to M-s b (start from: generic completion for projects and directory trees). You can use this in an Elfeed entry buffer to get a list of all entries you have opened. Calling that command with C-u (prefix argument) will put the list in an ibuffer view. From there you can, for instance, delete them in bulk.

The MPV-related functions require the external mpv program. They will play a video in a new app window at a resolution that matches the current setup's display width or, in the case of an enclosure (presumably a podcast), play just the audio file without popping up a new app window. The process runs in a dedicated buffer, so it can be terminated by killing the buffer. In the future I might make this cleaner, so that it understands input from, e.g., playerctl, though it is not a priority as the current simplistic design is "good enough" for my case.

The placement of the MPV buffer is controlled by display-buffer-alist (see Window rules and basic tweaks).

For a demonstration of what I have here, watch my Elfeed video (2020-06-09).

As I also am a user of Bongo (see Bongo (music or media manager)), there are a few functions here that are meant to make the Elfeed search buffer a bongo-aware library, from where we can enqueue online multimedia sources (videos, podcasts).

The key part here is to not interfere with the main Bongo playlist and library, which are dedicated to my local music collection, but to maintain a separate playlist which can be controlled independently.

The placement of the *Bongo-Elfeed Queue* buffer is controlled by the display-buffer-alist (see Window rules and basic tweaks).

I benefited in this particular workflow from an email exchange with Madhavan Krishnan: we shared code and ideas that helped establish the modalities of interaction between Elfeed and Bongo (this information is made public with permission).

Demo: Manage podcasts in Emacs with Elfeed and Bongo (2020-09-11).

Also see: Sample configuration for MPV (Elfeed+Bongo extension).

(use-package elfeed
  :ensure
  :config
  (setq elfeed-use-curl t)
  (setq elfeed-curl-max-connections 10)
  (setq elfeed-db-directory "~/.emacs.d/elfeed/")
  (setq elfeed-enclosure-default-dir "~/Downloads/")
  (setq elfeed-search-filter "@4-months-ago +unread")
  (setq elfeed-sort-order 'descending)
  (setq elfeed-search-clipboard-type 'CLIPBOARD)
  (setq elfeed-search-title-max-width 100)
  (setq elfeed-search-title-min-width 30)
  (setq elfeed-search-trailing-width 25)
  (setq elfeed-show-truncate-long-urls t)
  (setq elfeed-show-unique-buffers t)
  (setq elfeed-search-date-format '("%F %R" 16 :left))

;;; Elfeed general purpose commands

  (defun prot/elfeed-feeds ()
    "Load file containing the `elfeed-feeds' list.
Add this to `elfeed-search-mode-hook'."
    (let ((feeds "~/.emacs.d/feeds.el.gpg"))
      (if (file-exists-p feeds)
          (load-file feeds)
        (user-error "Missing feeds' file"))))

  (defun prot/elfeed-show-eww (&optional link)
    "Browse current `elfeed' entry link in `eww'.
Only show the readable part once the website loads.  This can
fail on poorly-designed websites."
    (interactive)
    (let* ((entry (if (eq major-mode 'elfeed-show-mode)
                      elfeed-show-entry
                    (elfeed-search-selected :ignore-region)))
           (link (or link (elfeed-entry-link entry))))
      (eww link)
      (add-hook 'eww-after-render-hook 'eww-readable nil t)))

  (defun prot/elfeed-search-other-window (&optional arg)
    "Browse `elfeed' entry in the other window.
With \\[universal-argument] browse the entry in `eww' using the
`prot/elfeed-show-eww' wrapper."
    (interactive "P")
    (let* ((entry (if (eq major-mode 'elfeed-show-mode)
                      elfeed-show-entry
                    (elfeed-search-selected :ignore-region)))
           (link (elfeed-entry-link entry))
           (win (selected-window)))
      (with-current-buffer (get-buffer "*elfeed-search*")
        (unless (one-window-p)              ; experimental
          (delete-other-windows win))
        (split-window win (/ (frame-height) 5) 'below)
        (other-window 1)
        (if arg
            (progn
              (when (eq major-mode 'elfeed-search-mode)
                (elfeed-search-untag-all-unread))
              (prot/elfeed-show-eww link))
          (elfeed-search-show-entry entry)))))

  (defun prot/elfeed-kill-buffer-close-window-dwim ()
    "Do-what-I-mean way to handle `elfeed' windows and buffers.

When in an entry buffer, kill the buffer and return to the Elfeed
Search view.  If the entry is in its own window, delete it as
well.

When in the search view, close all other windows.  Else just kill
the buffer."
    (interactive)
    (let ((win (selected-window)))
      (cond ((eq major-mode 'elfeed-show-mode)
             (elfeed-kill-buffer)
             (unless (one-window-p) (delete-window win))
             (switch-to-buffer "*elfeed-search*"))
            ((eq major-mode 'elfeed-search-mode)
             (if (one-window-p)
                 (elfeed-search-quit-window)
               (delete-other-windows win))))))

;;; Elfeed multimedia extras

  (defvar prot/elfeed-mpv-hook nil
    "Hook called before `prot/elfeed-mpv-dwim'.")

  (defun prot/elfeed-mpv-buffer ()
    "Prepare \"*elfeed-mpv-output*\" buffer.
For use by `prot/elfeed-mpv-dwim'.  To be called from
`prot/elfeed-mpv-hook'."
    (let ((buf (get-buffer "*elfeed-mpv-output*"))
          (inhibit-read-only t))
      (with-current-buffer buf
        (erase-buffer))))

  (defun prot/elfeed-mpv-dwim ()
    "Play entry link with external `mpv' program.
When there is an audio enclosure (podcast), play just the audio.
Else spawn a video player at a resolution that accounts for the
current monitor's width."
    (interactive)
    (let* ((entry (if (eq major-mode 'elfeed-show-mode)
                      elfeed-show-entry
                    (elfeed-search-selected :ignore-region)))
           (link (elfeed-entry-link entry))
           (enclosure (elt (car (elfeed-entry-enclosures entry)) 0)) ; fragile?
           (audio "--no-video")
           ;; Here the display width checks if I am on the laptop
           (height (if (<= (display-pixel-width ) 1366) 720 1080))
           (video                       ; this assumes mpv+youtube-dl
            (format "%s --ytdl-format=bestvideo[height\\<=?%s]+bestaudio/best"
                    "--hwdec=auto-safe" height))
           (buf (pop-to-buffer "*elfeed-mpv-output*")))
      (run-hooks 'prot/elfeed-mpv-hook)
      (if enclosure
          (progn
            (async-shell-command (format "mpv %s %s" audio enclosure) buf)
            (message
             (concat "Launching MPV for "
                     (propertize enclosure 'face 'success))))
        (async-shell-command (format "mpv %s %s" video link) buf)
        (message
         (concat "Launching MPV for "
                 (propertize link 'face 'success))))))

;;; Elfeed and Bongo integration

  (defvar prot/elfeed-bongo-playlist "*Bongo-Elfeed Queue*"
    "Name of the Elfeed+Bongo multimedia playlist.")

  (defun prot/elfeed-bongo-insert-item ()
    "Insert `elfeed' multimedia links in `bongo' playlist buffer.

The playlist buffer has a unique name so that it will never
interfere with the default `bongo-playlist-buffer'."
    (interactive)
    (with-eval-after-load 'bongo
      (let* ((entry (if (eq major-mode 'elfeed-show-mode)
                        elfeed-show-entry
                      (elfeed-search-selected :ignore-region)))
             (link (elfeed-entry-link entry))
             (enclosure (elt (car (elfeed-entry-enclosures entry)) 0))
             (url (or enclosure link))
             (title (elfeed-entry-title entry))
             (bongo-pl prot/elfeed-bongo-playlist)
             (buffer (get-buffer-create bongo-pl)))
        (unless (bongo-playlist-buffer)
          (bongo-playlist-buffer))
        (display-buffer buffer)
        (with-current-buffer buffer
 	      (when (not (bongo-playlist-buffer-p))
 	        (bongo-playlist-mode)
            (setq-local bongo-library-buffer (get-buffer "*elfeed-search*"))
            (setq-local bongo-enabled-backends '(mpv))
            (bongo-progressive-playback-mode))
 	      (goto-char (point-max))
          (bongo-insert-uri url title)
          (bongo-insert-comment-text (format "     ==> %s\n" url))
          (let ((inhibit-read-only t))
            (delete-duplicate-lines (point-min) (point-max)))
          (bongo-recenter))
        (message "Enqueued %s “%s” in %s"
                 (if enclosure "podcast" "video")
                 (propertize title 'face 'italic)
                 (propertize bongo-pl 'face 'bold)))))

  (defun prot/elfeed-bongo-switch-to-playlist ()
    (interactive)
    (let* ((bongo-pl prot/elfeed-bongo-playlist)
           (buffer (get-buffer bongo-pl)))
      (if buffer
          (switch-to-buffer buffer)
        (message "No `bongo' playlist is associated with `elfeed'."))))

;;; Elfeed search extensions

  (defun prot/elfeed-show-search-update (direction)
    "Update `elfeed-search-buffer' to match entry in DIRECTION.

This is useful when Elfeed is split in two windows, with the
search buffer on one side and an entry buffer on the other.  The
user is changing entries while in the latter, while the former
gets updated to put point on the current item.

EXPERIMENTAL."
    (interactive "s")
    (let* ((entry (if (eq major-mode 'elfeed-show-mode)
                      elfeed-show-entry
                    (elfeed-search-selected :ignore-region)))
           (title (elfeed-entry-title entry))
           (es "*elfeed-search*")
           (buf (get-buffer es))
           (win (get-buffer-window buf)))
      (funcall (intern (concat "elfeed-show-"
                               (substring `,direction 0 4))))
      (when (window-live-p win)
        (with-current-buffer buf
          (goto-char (point-min)) ; Elfeed way to find entry window?
          (search-forward (format "%s" title))
          (funcall (intern (concat `,direction "-line")))
          (set-window-point win (point-at-bol))
          (prot/pulse-line)))))

  (defun prot/elfeed-search-tag-filter ()
    "Filter `elfeed' by tags using completion.

Arbitrary input is also possible, but you may need to exit the
minibuffer with `exit-minibuffer' (I bind it to C-j in
`minibuffer-local-completion-map')."
    (interactive)
    (unwind-protect
        (elfeed-search-clear-filter)
      ;; NOTE for the `crm-separator' to work with just a space, you
      ;; need to make SPC self-insert in the minibuffer (the default is
      ;; to behave like tab-completion).
      (let* ((crm-separator " ")
             (elfeed-search-filter-active :live)
             (db-tags (elfeed-db-get-all-tags))
             (plus-tags (delete-dups
                         (mapcar (lambda (x)
                                   (concat "+" (format "%s" x)))
                                 db-tags)))
             (minus-tags (delete-dups
                          (mapcar (lambda (x)
                                    (concat "-" (format "%s" x)))
                                  db-tags)))
             (all-tags (append plus-tags minus-tags))
             (tags (completing-read-multiple
                    "Apply tag: "
                    all-tags nil t))
             (input (string-join `(,elfeed-search-filter ,@tags) " ")))
        (setq elfeed-search-filter input))
      (elfeed-search-update :force)))

  (defun prot/elfeed-show-archive ()
    "Create an archive copy of the current `elfeed' entry."
    (interactive)
    (let* ((entry (if (eq major-mode 'elfeed-show-mode)
                      elfeed-show-entry
                    (elfeed-search-selected :ignore-region)))
           ;; TODO how to cleanly add hyphens instead of spaces?
           (title (elfeed-entry-title entry))
           (elfeed-show-truncate-long-urls nil)
           (archives "~/Documents/feeds/")
           (file (format "%s%s.txt" archives title)))
      (unless (file-exists-p archives)
        (make-directory archives t))
      (when (derived-mode-p 'elfeed-show-mode)
        ;; Refresh to expand truncated URLs
        (elfeed-show-refresh)
        (write-file file t)
        (message "Saved buffer at %s" file))))

  (defface prot/elfeed-entry-critical
    '((t :inherit (font-lock-builtin-face elfeed-search-title-face)))
    "Face for Elfeed entries tagged 'critical'.")

  (defface prot/elfeed-entry-essential
    '((t :inherit (font-lock-keyword-face elfeed-search-title-face)))
    "Face for Elfeed entries tagged 'essential'.")

  (defface prot/elfeed-entry-important
    '((t :inherit (font-lock-string-face elfeed-search-title-face)))
    "Face for Elfeed entries tagged 'important'.")

  (defface prot/elfeed-entry-personal
    '((t :inherit (font-lock-warning-face elfeed-search-title-face)))
    "Face for Elfeed entries tagged 'personal'.")

  (setq elfeed-search-face-alist
        '((critical prot/elfeed-entry-critical)
          (essential prot/elfeed-entry-essential)
          (important prot/elfeed-entry-important)
          (personal prot/elfeed-entry-personal)
          (unread elfeed-search-unread-title-face)))

  (defvar prot/elfeed-search-tags
    '(critical essential important)
    "List of tags used by `prot/elfeed-toggle-tag'.")

  (defun prot/elfeed-toggle-tag (&optional tag)
    "Toggle tag on current item.

A list of tags is provided by `prot/elfeed-search-tags'.
Otherwise an optional TAG symbol will suffice."
    (interactive)
    (let* ((tags prot/elfeed-search-tags)
           (input (or tag (intern (completing-read "Set tag: " tags nil t)))))
      (elfeed-search-toggle-all input)))

;;; Hooks and key bindings

  :hook ((elfeed-search-mode-hook . prot/elfeed-feeds)
         (prot/elfeed-mpv-hook . prot/elfeed-mpv-buffer))
  :bind (("C-c f" . elfeed)
         :map elfeed-search-mode-map
         ("s" . prot/elfeed-search-tag-filter)
         ("w" . elfeed-search-yank)
         ("g" . elfeed-update)
         ("G" . elfeed-search-update--force)
         ("o" . prot/elfeed-search-other-window)
         ("v" . prot/elfeed-mpv-dwim)
         ("q" . prot/elfeed-kill-buffer-close-window-dwim)
         ("!" . (lambda ()
                  (interactive)
                  (prot/elfeed-toggle-tag 'important)))
         ("+" . prot/elfeed-toggle-tag)
         ("b" . prot/elfeed-bongo-insert-item)
         ("h" . prot/elfeed-bongo-switch-to-playlist) ; "hop" mnemonic
         :map elfeed-show-mode-map
         ("a" . prot/elfeed-show-archive)
         ("b" . prot/elfeed-bongo-insert-item)
         ("n" . (lambda ()
                  (interactive)
                  (prot/elfeed-show-search-update "next")))
         ("p" . (lambda ()
                  (interactive)
                  (prot/elfeed-show-search-update "previous")))
         ("e" . prot/elfeed-show-eww)
         ("q" . prot/elfeed-kill-buffer-close-window-dwim)
         ("v" . prot/elfeed-mpv-dwim)
         ("w" . elfeed-show-yank)))

6.11.1 Sample configuration for MPV (Elfeed+Bongo extension)

In the previous section I configure Elfeed to integrate with the Bongo media manager. The external mpv executable is used to play back audio and video links. Instead of passing command-line arguments to control the settings of the player, I just add the following to my local configuration files, specifically ~/.config/mpv/mpv.conf:

hwdec=auto-safe
ytdl-format="bestvideo[height<=?1080]+bestaudio/best"

6.12 Emacs Simple HTML Renderer (shr)

NOTE 2020-08-16: This section is subject to major changes contingent on the eventual review of eww (which is documented in the next section: Emacs Web Wowser (EWW)).

As far as I can tell, the following shr-* variables concern an HTML parser that is used by a variety of tools, including Elfeed (defined right above). I guess we could scope them by using hooks, but I see no need for different settings.

What these do:

  • Open links in a new Emacs window, instead of the system's browser. This Emacs web browser is called eww.
  • Use monospaced fonts, since that is what I want to have everywhere in Emacs.
  • Do not preserve colours from websites, as they may be inaccessible (see my Modus theme).
  • Keep images to 70% of the window. This number is arbitrary. It just feels like a good upper limit (not a fan of decorative images inside of blog posts).
  • Line length at same number of characters as fill-column (defined in the section about Line length (column count) at 72).
(use-package shr
  :config
  (setq shr-use-fonts nil)
  (setq shr-use-colors nil)
  (setq shr-max-image-proportion 0.7)
  (setq shr-image-animate nil)
  (setq shr-width (current-fill-column)))

Support the HTML pre tag with proper syntax highlighting.

(use-package shr-tag-pre-highlight
  :ensure
  :after shr
  :config
  (add-to-list 'shr-external-rendering-functions
               '(pre . shr-tag-pre-highlight)))

6.13 Emacs Web Wowser (EWW)

WORK IN PROGRESS 2020-08-07

(use-package eww
  :config
  (setq eww-restore-desktop nil)
  (setq eww-desktop-remove-duplicates t)
  (setq eww-header-line-format "%u")
  (setq eww-search-prefix "https://duckduckgo.com/html/?q=")
  (setq eww-download-directory "~/Downloads/")
  (setq eww-suggest-uris
        '(eww-links-at-point
          thing-at-point-url-at-point))
  (setq eww-bookmarks-directory "~/.emacs.d/eww-bookmarks/")
  (setq eww-history-limit 150)
  (setq eww-use-external-browser-for-content-type
        "\\`\\(video/\\|audio/\\|application/pdf\\)")
  (setq eww-browse-url-new-window-is-tab nil)
  (setq eww-form-checkbox-selected-symbol "[X]")
  (setq eww-form-checkbox-symbol "[ ]")

  (defun prot/eww-visit-history (&optional arg)
    "Revisit a URL from `eww-prompt-history' using completion.
With \\[universal-argument] produce a new buffer."
    (interactive "P")
    (let ((history eww-prompt-history)  ; eww-bookmarks
          (new (if arg t nil)))
      (icomplete-vertical-do ()
        (eww
         (completing-read "Visit website from history: " history nil t)
         new))))

  ;; eww-view-source

  (defvar prot/eww-mode-global-map
    (let ((map (make-sparse-keymap)))
      (define-key map "s" 'eww-search-words)
      (define-key map "o" 'eww-open-in-new-buffer)
      (define-key map "f" 'eww-open-file)
      (define-key map "w" 'prot/eww-visit-history)
      map)
    "Key map to scope `eww' bindings for global usage.
The idea is to bind this to a prefix sequence, so that its
defined keys follow the pattern of <PREFIX> <KEY>.")
  :bind-keymap ("C-c w" . prot/eww-mode-global-map)
  :bind (:map eww-mode-map
              ("n" . next-line)
              ("p" . previous-line)
              ("f" . forward-char)
              ("b" . backward-char)
              ("a" . prot/eww-org-archive-current-url)
              ("B" . eww-back-url)
              ("N" . eww-next-url)
              ("P" . eww-previous-url)))

(use-package browse-url
  :after eww
  :config
  (setq browse-url-browser-function 'eww-browse-url))

6.14 Emacs IRC client

WORK IN PROGRESS 2020-05-13

My .authinfo.gpg entry for IRC looks like this:

machine irc.freenode.net port 6667 login NAME password PASS

The prot/auth-get-field is defined in Base email settings (may be better to have it elsewhere, but I keep it now together with the email configurations).

(use-package erc
  :disabled                             ; Needs testing and refinements
  :config
  (setq erc-prompt-for-password nil)

  (defun prot/erc-dwim ()
    "Switch to latest `erc' buffer or log in."
    (interactive)
    (let* ((irc "irc.freenode.net")
           (nick (prot/auth-get-field irc :user))
           ;; Gets the password, but does not read(?) it
           (pass (prot/auth-get-field irc :secret))
           (bufs (erc-buffer-list)))
      (if bufs
          (erc-track-switch-buffer 1)
        (erc :server irc
             :nick nick
             :password pass             ; Does not work
             :full-name nick))))
  :bind ("C-c e" . prot/erc-dwim))

6.15 Bongo (music or media manager)

Bongo is a buffer oriented media manager. It provides an interface to external media players, such as VLC or MPV. Those are known as "back-ends". A "library" buffer contains the media collection, which consists of music or video files, or even links to online streams. A "playlist" buffer holds the items that wait in the queue to be played by the back-end program.

I use Bongo for my local Music collection. My directories are, in principle, organised following the pattern Artist/Album/Tracks. Each track is named in the style of Artist - Album - Title. As part of this collection comes from physical discs, I never bothered writing metadata for all them and, consequently, I do not rely on it to play back my files. The directory structure and its concomitant naming conventions are sufficient. Besides, reading metadata requires more than just looking at a plain text name: it is opaque. As such, I never bothered trying to make Bongo handle metadata the way, say, the Music Player Daemon does (external program).

My approach to listening to music is straightforward: load up a directory or directory tree, randomise the playback order, and let it play in the background. To ease this workflow, I make my ~/Music directory a valid Bongo library. I can access it with dired, while still benefiting from the Bongo-specific extras (see the macro prot/bongo-dired-library and its derived functions).

Two main uses of the Dired+Bongo combination:

  1. Mark some directories or files the way you always do with Dired, hit a key that will (i) enqueue those to the playlist buffer, (ii) perform the randomisation where appropriate, (iii) mark this inserted group by bespoke section delimiters for easier future retrieval, and (iv) start playing an item unless one is already active (see prot/bongo-dired-insert-files).
  2. Add the absolute filesystem path of marked items (typically directories) to either an existing playlist file or a new one that is created on demand (check prot/bongo-dired-make-playlist-file). These files can be inserted in a Bongo playlist buffer to start playing music right away, following the same conditional patterns of behaviour described in the previous point (see, in particular, prot/bongo-playlist-insert-playlist-file).

Let us press on with the logic of prot/bongo-dired-insert-files: it will start playing immediately if the playlist buffer is empty, but will not do so if the buffer already plays back some track. It therefore is trivial to keep adding items to the queue. The "section delimiters" (prot/bongo-playlist-section) it inserts, set up the playlist buffer in a way that is easier to navigate (e.g. prot/bongo-paylist-section-next) and to edit blocks in the queue.

These custom sections separate batches of inserted media. They follow my approach of picking an ad-hoc collection of related items that match a certain theme. I want to treat them as an indivisible unit for the purposes of adding or removing sets from the playlist buffer. With point within a section, call prot/bongo-playlist-kill-section to remove it from the playlist, then optionally follow it up with a new set you wish to insert, using the aforementioned techniques.

Concerning the customisations below, these can be summarised thus:

  • Hide all icons that can be easily hidden.
  • No mode line indicators. Same for the header line.
  • Add visual indicators for played tracks and the progress on the current one (by the way, my Modus themes are designed to enhance this experience).
  • Do not ask for confirmation upon inserting a directory tree.
  • The bongo-join-inserted-tracks defaults to adding inline headers when multiple items share name components. I dislike that because sometimes I mix songs from various places and only some subsets are named appropriately to fully benefit from this. Basically, the results do not look consistent. Also, I have no use for foldable headings of this sort.

Watch Bongo media manager and my extras (2020-08-06) for a demonstration of the aforementioned.

Since then I have added an extra feature to automatically re-centre the playlist buffer whenever a track changes. This is useful when I am not working on the computer (e.g. cooking) but choose to keep music playing in the background with the playlist's window in focus: it saves me from the trouble of having to manually find the name of the currently playing track. For more on the implementation front, refer to the prot/bongo-playlist-change-track-hook and its accompaniments.

The contrib/bongo-playlist-no-banner advice, which hides the commentary banner from the standard Bongo playlist buffer, is copied with a minor rewording from the Emacs configuration file of Nicolas De Jaeghere.

The define-bongo-backend mpv you see in this section is meant to facilitate the integration with Elfeed, where we want to maintain a distinct playlist+library for tracking online multimedia sources (videos and podcasts). See Elfeed (RSS/Atom feed reader) for the implementation details. Madhavan Krishnan helped me flesh out this project by sharing code and ideas in a private exchange (shared with permission).

Watch: Manage podcasts in Emacs with Elfeed and Bongo (2020-09-11).

(use-package bongo
  :ensure
  :config
  (setq bongo-default-directory "~/Music")
  (setq bongo-prefer-library-buffers nil)
  (setq bongo-insert-whole-directory-trees t)
  (setq bongo-logo nil)
  (setq bongo-display-track-icons nil)
  (setq bongo-display-track-lengths nil)
  (setq bongo-display-header-icons nil)
  (setq bongo-display-playback-mode-indicator t)
  (setq bongo-display-inline-playback-progress t)
  (setq bongo-join-inserted-tracks nil)
  (setq bongo-field-separator (propertize " · " 'face 'shadow))
  (setq bongo-mark-played-tracks t)
  (setq bongo-header-line-mode nil)
  (setq bongo-mode-line-indicator-mode nil)
  (setq bongo-enabled-backends '(vlc mpv))
  (setq bongo-vlc-program-name "cvlc")

;;; Bongo playlist buffer
;;;; Generic playlist extras
  ;; Hide the playlist's banner
  (define-advice bongo-default-playlist-buffer
      (:override () contrib/bongo-playlist-no-banner)
    (with-current-buffer (get-buffer-create bongo-default-playlist-buffer-name)
      (unless (derived-mode-p 'bongo-playlist-mode)
        (bongo-playlist-mode))
      (current-buffer)))

;;;; Bongo playlist sections
  (defvar prot/bongo-playlist-section-delimiter
    "\n******************************\n\n"
    "Delimiter for inserted groups in `bongo' playlist buffers.")

  (defconst prot/bongo-playlist-heading-delimiter "§"
    "Delimiter for custom headings in `bongo' playlist buffers.")

  (defun prot/bongo-playlist-heading (title &optional description)
    "Insert `bongo' comment with TITLE and DESCRIPTION.
Use this to add a custom heading for the enqueued media items."
    (bongo-insert-comment-text
     (format "%s %s%s\n" prot/bongo-playlist-heading-delimiter
             title (if description (concat " " description) ""))))

  (defun prot/bongo-playlist-section ()
    (bongo-insert-comment-text
     prot/bongo-playlist-section-delimiter))

  ;; NOTE: there probably is a better way to parametrise move-buf and
  ;; move-point so that one key checks for appropriate forward or
  ;; backward motions, but this is okay right now.
  (defmacro prot/bongo-playlist-motion (fn desc rx move-buf move-point)
    "Produce interactive commands to navigate custom bongo delimiters.

FN is the resulting interactive function's name.  DESC is its doc
string.  RX is the regular expression that matches the custom
bongo playlist delimiter (see `prot/bongo-playlist-delimiter' and
`prot/bongo-playlist-heading').

MOVE-BUF is a motion across an arbitrary number of lines.
Currently it assumes (though does test) either
`re-search-forward' or `re-search-backward'.  Likewise,
MOVE-POINT expects `point-at-eol' or `point-at-bol'.  These
motions should go in pairs, in the order they are presented here."
    `(defun ,fn ()
       ,desc
       (interactive)
       (let ((section ,rx))
         (when (save-excursion (funcall ,move-buf section nil t))
           (goto-char (funcall ,move-point))
           (funcall ,move-buf section nil t)))))

  (prot/bongo-playlist-motion
   prot/bongo-playlist-heading-next
   "Move to next `bongo' playlist custom heading."
   "^.*§.*$"
   're-search-forward
   'point-at-eol)

  (prot/bongo-playlist-motion
   prot/bongo-playlist-heading-previous
   "Move to previous `bongo' playlist custom heading."
   "^.*§.*$"
   're-search-backward
   'point-at-bol)

  (prot/bongo-playlist-motion
   prot/bongo-playlist-section-next
   "Move to next `bongo' playlist custom section delimiter."
   "^\\*+$"
   're-search-forward
   'point-at-eol)

  (prot/bongo-playlist-motion
   prot/bongo-playlist-section-previous
   "Move to previous `bongo' playlist custom section delimiter."
   "^\\*+$"
   're-search-backward
   'point-at-bol)

  (defun prot/bongo-playlist-mark-section ()
    "Mark `bongo' playlist section, delimited by custom markers.
The marker is `prot/bongo-playlist-delimiter'."
    (interactive)
    (let ((section "^\\*+$"))
      (search-forward-regexp section nil t)
      (push-mark nil t)
      (forward-line -1)
      ;; REVIEW any predicate to replace this `save-excursion'?
      (if (save-excursion (re-search-backward section nil t))
          (progn
            (search-backward-regexp section nil t)
            (forward-line 1))
        (goto-char (point-min)))
      (activate-mark)))

  (defun prot/bongo-playlist-kill-section ()
    "Kill `bongo' playlist-section at point.
This operates on a custom delimited section of the buffer.  See
`prot/bongo-playlist-kill-section'."
    (interactive)
    (prot/bongo-playlist-mark-section)
    (bongo-kill))

;;;; Bongo playlist imenu setup
  (defvar prot/bongo-playlist-setup-hook nil
    "Hook that runs after inserting items to the `bongo' playlist.
See, for example, `prot/bongo-playlist-insert-playlist-file' or
`prot/bongo-dired-insert-files'.")

  (defun prot/bongo-playlist-imenu-heading ()
    "Return the text of the custom `bongo' playlist heading."
    (interactive)
    (nth 1
         (split-string
          (buffer-substring-no-properties (point-at-bol) (point-at-eol))
          "§ ")))

  (defun prot/bongo-playlist-imenu-setup ()
    "`imenu' bindings for the `bongo' playlist buffer."
    (setq-local imenu-prev-index-position-function
                'prot/bongo-playlist-heading-previous)
    (setq-local imenu-extract-index-name-function
                'prot/bongo-playlist-imenu-heading))

  (add-hook 'prot/bongo-playlist-setup-hook #'prot/bongo-playlist-imenu-setup)

;;;; Bongo playlist custom commands
  (defun prot/bongo-playlist-play-random ()
    "Play random `bongo' track and determine further conditions."
    (interactive)
    (unless (bongo-playlist-buffer)
      (bongo-playlist-buffer))
    (when (or (bongo-playlist-buffer-p)
              (bongo-library-buffer-p))
      (unless (bongo-playing-p)
        (with-current-buffer (bongo-playlist-buffer)
          (bongo-play-random)
          (bongo-random-playback-mode)
          (bongo-recenter)))))

  (defun prot/bongo-playlist-random-toggle ()
    "Toggle `bongo-random-playback-mode' in playlist buffers."
    (interactive)
    (if (eq bongo-next-action 'bongo-play-random-or-stop)
        (bongo-progressive-playback-mode)
      (bongo-random-playback-mode)))

  (defun prot/bongo-playlist-reset ()
    "Stop playback and reset `bongo' playlist marks.
To reset the playlist is to undo the marks produced by non-nil
`bongo-mark-played-tracks'."
    (interactive)
    (when (bongo-playlist-buffer-p)
      (bongo-stop)
      (bongo-reset-playlist)))

  (defun prot/bongo-playlist-terminate ()
    "Stop playback and clear the entire `bongo' playlist buffer.
Contrary to the standard `bongo-erase-buffer', this also removes
the currently playing track."
    (interactive)
    (when (bongo-playlist-buffer-p)
      (bongo-stop)
      (bongo-erase-buffer)))

  (defun prot/bongo-playlist-insert-playlist-file ()
    "Insert contents of playlist file to a `bongo' playlist.
Upon insertion, playback starts immediately, in accordance with
`prot/bongo-play-random'.

The available options at the completion prompt are pre-configured
files that contain absolute filesystem paths of directories or
media items one per line.  Think of them as meta-directories that
mix manually selected media items (yes, I never liked 'smart'
playlists).

To insert multiple playlists complete the first, then type a
space, complete a second, etc.  This assumes that SPC can
self-insert in the minibuffer or you use C-q SPC.

Also see `prot/bongo-dired-make-playlist-file'."
    (interactive)
    (let* ((path "~/Music/playlists/")
           (dotless directory-files-no-dot-files-regexp)
           (playlists (mapcar
                       'abbreviate-file-name
                       (directory-files path nil dotless)))
           (crm-separator " ")          ; Space to separate entries
           (choice
            (if (not (bongo-playlist-buffer-p (current-buffer)))
                (user-error "Not in a `bongo' playlist buffer")
              (completing-read-multiple "Add playlist: " playlists nil t))))
      (mapcar (lambda (x)
                (save-excursion
                  (goto-char (point-max))
                  (prot/bongo-playlist-heading x "playlist file")
                  (bongo-insert-playlist-contents
                   (format "%s%s" path x))
                  (prot/bongo-playlist-section)))
              choice)
      (prot/bongo-playlist-play-random)
      (run-hooks 'prot/bongo-playlist-setup-hook)))

  (defvar prot/bongo-playlist-change-track-hook nil
    "Hook that runs after `bongo' switches to a new track.")

  (defun prot/bongo-playlist-run-hook-change-track (&rest _)
    "Run `prot/bongo-playlist-run-hook-change-track'.
This is meant to be loaded after the relevant `bongo' functions
that change tracks, such as `bongo-play-next-or-stop' and
`bongo-play-random-or-stop'."
    (run-hooks 'prot/bongo-playlist-change-track-hook))

  (dolist (fn '(bongo-play-next-or-stop bongo-play-random-or-stop))
    (advice-add fn :after #'prot/bongo-playlist-run-hook-change-track))

  (defun prot/bongo-playlist-recenter ()
    "Recenter `bongo' playlist buffer while in a live window.
Add to `prot/bongo-playlist-change-track-hook'."
    (with-current-buffer (bongo-playlist-buffer)
      (bongo-recenter)))

;;; Bongo + Dired (bongo library buffer)
  (defmacro prot/bongo-dired-library (name doc val)
    "Create `bongo' library function NAME with DOC and VAL."
    `(defun ,name ()
       ,doc
       (when (string-match-p "\\`~/Music/" default-directory)
         (bongo-dired-library-mode ,val))))

  (prot/bongo-dired-library
   prot/bongo-dired-library-enable
   "Set `bongo-dired-library-mode' when accessing ~/Music.

Add this to `dired-mode-hook'.  Upon activation, the directory
and all its sub-directories become a valid library buffer for
Bongo, from where we can, among others, add tracks to playlists.
The added benefit is that Dired will continue to behave as
normal, making this a superior alternative to a purpose-specific
library buffer.

Note, though, that this will interfere with `wdired-mode'.  See
`prot/bongo-dired-library-disable'."
   1)

  ;; NOTE `prot/bongo-dired-library-enable' does not get reactivated
  ;; upon exiting `wdired-mode'.
  ;;
  ;; TODO reactivate bongo dired library upon wdired exit
  (prot/bongo-dired-library
   prot/bongo-dired-library-disable
   "Unset `bongo-dired-library-mode' when accessing ~/Music.
This should be added `wdired-mode-hook'.  For more, refer to
`prot/bongo-dired-library-enable'."
   -1)

  (defun prot/bongo-dired-insert-files ()
    "Add files in a `dired' buffer to the `bongo' playlist."
    (let ((media (or (dired-get-marked-files) (dired-filename-at-point))))
      (with-current-buffer (bongo-playlist-buffer)
        (goto-char (point-max))
        (mapcar (lambda (x)
                  (if (file-directory-p x)
                      (progn
                        (prot/bongo-playlist-heading (file-name-base x))
                        (bongo-insert-directory-tree x))
                    (bongo-insert-file x)))
                media)
        (prot/bongo-playlist-section)
        (run-hooks 'prot/bongo-playlist-setup-hook))
      (with-current-buffer (bongo-library-buffer)
        (dired-next-line 1))))

  (defun prot/bongo-dired-insert ()
    "Add `dired' item at point or marks to `bongo' playlist.

The playlist is created, if necessary, while some other tweaks
are introduced.  See `prot/bongo-dired-insert-files' as well as
`prot/bongo-playlist-play-random'.

Meant to work while inside a `dired' buffer that doubles as a
library buffer (see `prot/bongo-dired-library')."
    (interactive)
    (when (bongo-library-buffer-p)
      (unless (bongo-playlist-buffer-p)
        (bongo-playlist-buffer))
      (prot/bongo-dired-insert-files)
      (prot/bongo-playlist-play-random)))

  (defun prot/bongo-dired-make-playlist-file ()
    "Add `dired' marked items to playlist file using completion.

These files are meant to reference filesystem paths.  They ease
the task of playing media from closely related directory trees,
without having to interfere with the user's directory
structure (e.g. a playlist file 'rock' can include the paths of
~/Music/Scorpions and ~/Music/Queen).

This works by appending the absolute filesystem path of each item
to the selected playlist file.  If no marks are available, the
item at point will be used instead.

Selecting a non-existent file at the prompt will create a new
entry whose name matches user input.  Depending on the completion
framework, such as with `icomplete-mode', this may require a
forced exit (e.g. \\[exit-minibuffer] to parse the input without
further questions).

Also see `prot/bongo-playlist-insert-playlist-file'."
    (interactive)
    (let* ((dotless directory-files-no-dot-files-regexp)
           (pldir "~/Music/playlists")
           (playlists (mapcar
                       'abbreviate-file-name
                       (directory-files pldir nil dotless)))
           (plname (completing-read "Select playlist: " playlists nil nil))
           (plfile (format "%s/%s" pldir plname))
           (media-paths
            (if (derived-mode-p 'dired-mode)
                ;; TODO more efficient way to do ensure newline ending?
                ;;
                ;; The issue is that we need to have a newline at the
                ;; end of the file, so that when we append again we
                ;; start on an empty line.
                (concat
                 (mapconcat #'identity
                            (dired-get-marked-files)
                            "\n")
                 "\n")
              (user-error "Not in a `dired' buffer"))))
      ;; The following `when' just checks for an empty string.  If we
      ;; wanted to make this more robust we should also check for names
      ;; that contain only spaces and/or invalid characters…  This is
      ;; good enough for me.
      (when (string-empty-p plname)
        (user-error "No playlist file has been specified"))
      (unless (file-directory-p pldir)
        (make-directory pldir))
      (unless (and (file-exists-p plfile)
                   (file-readable-p plfile)
                   (not (file-directory-p plfile)))
        (make-empty-file plfile))
      (append-to-file media-paths nil plfile)
      (with-current-buffer (find-file-noselect plfile)
        (delete-duplicate-lines (point-min) (point-max))
        (sort-lines nil (point-min) (point-max))
        (save-buffer)
        (kill-buffer))))

;;; Bongo + Elfeed integration

  ;; Here we define a slightly tweaked variant of the standard mpv
  ;; backend.  We will be using this to play back audio and video URLs.
  ;; The latter will spawn a new MPV player window.  Refer to my Elfeed
  ;; section for the implementation details.
  (define-bongo-backend mpv
    ;; :constructor 'bongo-start-mpv-player
    :program-name 'mpv
    :extra-program-arguments nil
    :matcher '((local-file "file:" "http:" "ftp:")
               "ogg" "flac" "mp3" "mka" "wav" "wma"
               "mpg" "mpeg" "vob" "avi" "ogm" "mp4" "mkv"
               "mov" "asf" "wmv" "rm" "rmvb" "ts")
    :matcher '(("mms:" "mmst:" "rtp:" "rtsp:" "udp:" "unsv:"
                "dvd:" "vcd:" "tv:" "dvb:" "mf:" "cdda:" "cddb:"
                "cue:" "sdp:" "mpst:" "tivo:") . t)
    :matcher '(("http:" "https:") . t))

;;; Hooks and key bindings

  :hook ((dired-mode-hook . prot/bongo-dired-library-enable)
         (wdired-mode-hook . prot/bongo-dired-library-disable)
         (prot/bongo-playlist-change-track-hook . prot/bongo-playlist-recenter))
  :bind (("C-c b" . bongo)
         ("<C-XF86AudioPlay>" . bongo-pause/resume)
         ("<C-XF86AudioNext>" . bongo-next)
         ("<C-XF86AudioPrev>" . bongo-previous)
         ("<M-XF86AudioPlay>" . bongo-show)
         ("<S-XF86AudioNext>" . bongo-seek-forward-10)
         ("<S-XF86AudioPrev>" . bongo-seek-backward-10)
         :map bongo-playlist-mode-map
         ("n" . bongo-next-object)
         ("p" . bongo-previous-object)
         ("C-c C-n" . prot/bongo-playlist-heading-next)
         ("C-c C-p" . prot/bongo-playlist-heading-previous)
         ("M-n" . prot/bongo-playlist-section-next)
         ("M-p" . prot/bongo-playlist-section-previous)
         ("M-h" . prot/bongo-playlist-mark-section)
         ("M-d" . prot/bongo-playlist-kill-section)
         ("g" . prot/bongo-playlist-reset)
         ("D" . prot/bongo-playlist-terminate)
         ("r" . prot/bongo-playlist-random-toggle)
         ("R" . bongo-rename-line)
         ("j" . bongo-dired-line)       ; Jump to dir of file at point
         ("J" . dired-jump)             ; Jump to library buffer
         ("i" . prot/bongo-playlist-insert-playlist-file)
         ("I" . bongo-insert-special)
         :map bongo-dired-library-mode-map
         ("<C-return>" . prot/bongo-dired-insert)
         ("C-c SPC" . prot/bongo-dired-insert)
         ("C-c +" . prot/bongo-dired-make-playlist-file)))

7 General interface and interactions

This section contains configurations for all aspects of the Emacs user interface, as well lots of small or self-contained tweaks that cover a wide range of built-in libraries.

7.1 Mode line

The mode line is an integral part of the Emacs interface. While there are lots of third party packages that style it in a variety of ways, I find the default to be particularly good.

I just configure it to reshuffle some of the indicators. Nothing too fancy.

Meanwhile, the mode-line-defining-kbd-macro is tweaked to use a more appropriate string for its indicator and colours that are designed specifically for the mode line (the default uses the generic font-lock warning face).

I got the idea for prot/mode-line-hidden-mode from William Rankin. Note that William is the author of the wonderful olivetti-mode (see elsewhere in this document “Focus mode” for writing (olivetti-mode)).

(use-package emacs
  :commands prot/mode-line-hidden-mode
  :config
  (setq mode-line-percent-position '(-3 "%p"))
  (setq mode-line-defining-kbd-macro
        (propertize " Macro" 'face 'mode-line-emphasis))
  (setq-default mode-line-format
                '("%e"
                  mode-line-front-space
                  mode-line-mule-info
                  mode-line-client
                  mode-line-modified
                  mode-line-remote
                  mode-line-frame-identification
                  mode-line-buffer-identification
                  "  "
                  mode-line-position
                  (vc-mode vc-mode)
                  " "
                  mode-line-modes
                  " "
                  mode-line-misc-info
                  mode-line-end-spaces))

  (define-minor-mode prot/mode-line-hidden-mode
    "Toggle modeline visibility in the current buffer."
    :init-value nil
    :global nil
    (if prot/mode-line-hidden-mode
        (setq-local mode-line-format nil)
      (kill-local-variable 'mode-line-format)
      (force-mode-line-update))))

In the following sub-sections I provide customisations for some tools that place information on the mode line. Again, nothing flamboyant.

7.1.1 Battery status

Emacs offers a built-in library for presenting information about the status of the laptop's battery. Using it allows me to eliminate my reliance on the system panel and thus keep Emacs in full screen view without any interruptions.

The default update interval is set to a single minute (in seconds), which is generally fine though I find that a slightly higher value works just as well. As for the format, it is designed to show a context-dependent, single character indicator about the current status, as well as the battery's overall percentage.

Variable battery-mode-line-limit will hide the indicator if the value is above the declared threshold. 95 basically means "full" for me. I use that instead of a 100 because sometimes the battery only ever fills up to a lower threshold, meaning that the indicator remains present at all times.

(use-package battery
  :config
  (setq battery-mode-line-format " [%b%p%%]")
  (setq battery-mode-line-limit 95)
  (setq battery-update-interval 180)
  (setq battery-load-low 20)
  (setq battery-load-critical 10)
  :hook (after-init-hook . display-battery-mode))

7.1.2 Display current time

I normally use Emacs in fullscreen view. No system panels, no window decorations, no icons and blinking indicators. Nothing to distract me. While I really like this environment, sometimes I need to take a look at the time… Thankfully Emacs offers a convenient, built-in way of displaying such information in the mode line.

The display-time-format can be configured to show the current date and time in all the various formats we would expect, using a string of specifiers (find the docs with C-h v format-time-string). Setting its value to nil means that the information on display will be the combined result of display-time-24hr-format and display-time-day-and-date. I prefer to just write a string directly, keeping those two inactive.

The display-time-mode can output more than just the current time. It also shows the load average and an email indicator. I only need the time and date. The rest is noise.

Sometimes I need to check the current time on various timezones. This library's world-clock command gets the job done.

(use-package time
  :commands world-clock
  :config
  (setq display-time-format "%H:%M  %Y-%m-%d")
  ;;;; Covered by `display-time-format'
  ;; (setq display-time-24hr-format t)
  ;; (setq display-time-day-and-date t)
  (setq display-time-interval 60)
  (setq display-time-mail-directory nil)
  (setq display-time-default-load-average nil)

;;; World clock
  (setq zoneinfo-style-world-list
        '(("America/Los_Angeles" "Los Angeles")
          ("America/New_York" "New York")
          ("Europe/Brussels" "Brussels")
          ("Asia/Shanghai" "Shanghai")
          ("Asia/Tokyo" "Tokyo")))

  ;; All of the following variables are for Emacs 28
  (setq world-clock-list t)
  (setq world-clock-time-format "%R %z  %A %d %B")
  (setq world-clock-buffer-name "*world-clock*") ; Placement handled by
                                                 ; `display-buffer-alist'
  (setq world-clock-timer-enable t)
  (setq world-clock-timer-second 60)

  :hook (after-init-hook . display-time-mode))

7.1.3 Keycast mode

Once enabled, this package uses the mode line to show the keys being pressed and the command they call. It is quite useful for screen casting.

The placement of the indicator is controlled by keycast-window-predicate which I set to the current window. The moody library offers that specific piece of functionality. Moody can also be used to customise the looks of the mode line, though I do not want that.

The tweaks to the keycast-substitute-alist prevent the display of self-inserting characters and some other commands that are not particularly useful while screen casting. Now the indicator will only show commands, which looks cleaner. I got the idea and original piece of Elisp from the dotfiles of André Alexandre Gomes and then added a few tweaks of my own.

(use-package keycast
  :ensure
  :commands keycast-mode
  :config
  (use-package moody :ensure)

  (setq keycast-window-predicate 'moody-window-active-p)
  (setq keycast-separator-width 1)
  (setq keycast-insert-after 'mode-line-buffer-identification)
  (setq keycast-remove-tail-elements nil)

  (dolist (input '(self-insert-command
                   org-self-insert-command))
    (add-to-list 'keycast-substitute-alist `(,input "." "Typing…")))

  ;;; TODO identify all events that should be excluded
  ;; mouse-set-point
  ;; mouse-set-region
  ;; mouse-drag-secondary
  ;; mouse-drag-line
  ;; mouse-drag-drag
  ;; mouse-start-end
  ;; mouse-drag-region nil
  ;; mouse-drag-track nil
  ;; mouse-drag-region-rectangle
  ;; mouse-drag-and-drop-region
  ;; mwheel-event-button
  ;; dframe-mouse-event-p
  ;; mouse-drag-events-are-point-events-p

  (dolist (event '(mouse-event-p
                   mouse-movement-p
                   mwheel-scroll))
    (add-to-list 'keycast-substitute-alist `(,event nil))))

7.2 Window divider mode

This is a built-in mode that draws vertical window borders in a slightly different way than the default, which I find more consistent. Only using it because of that, though it can also adjust the size of the borders as well as their placement.

(use-package emacs
  :config
  (setq window-divider-default-right-width 1)
  (setq window-divider-default-bottom-width 1)
  (setq window-divider-default-places 'right-only)
  :hook (after-init-hook . window-divider-mode))

7.3 Fringe mode

The fringes are areas to the right and left side of an Emacs frame. They can be used to show status-related or contextual feedback such as line truncation indicators, continuation lines, code linting markers, etc.

The default fringe width (nil) is 8 pixels on either side, which I approve of. It is possible to set the value of the fringe-mode to something like '(10 . 5) which applies the varied width to the left and right side respectively. Otherwise, we can use a single integer that controls both sides.

The use of setq-default is necessary, otherwise these values become buffer-local.

(use-package fringe
  :config
  (fringe-mode nil)
  (setq-default fringes-outside-margins nil)
  (setq-default indicate-buffer-boundaries nil)
  (setq-default indicate-empty-lines nil)
  (setq-default overflow-newline-into-fringe t))

7.3.1 Diff highlights in the fringe (diff-hl)

The diff-hl package uses the left or right fringe to highlight changes in the current buffer. The indicators are colour-coded to denote whether a change is an addition, removal, or change that includes a bit of both.

The package offers some more features, such as the ability to move between diff hunks while editing the buffer. I still need to experiment with those before customising them to my liking.

At any rate, this package is meant a general tool for version control systems, rather than a git-specific one. Much like the built-in VC (see section on the generic version control (VC) framework).

(use-package diff-hl
  :ensure
  :after vc
  :config
  (setq diff-hl-draw-borders nil)
  (setq diff-hl-side 'left)
  :hook (after-init-hook . global-diff-hl-mode))

7.4 Highlight comment keywords (hl-todo)

This tool by Jonas Bernoulli will apply highlighting to keywords that are normally used in code comments. Simple and effective!

(use-package hl-todo
  :ensure
  :hook (prog-mode-hook . hl-todo-mode))

7.5 Tone down parentheses and autoload cookies in Elisp

Both of these packages are developed by Jonas Bernoulli (aka tarsius).

paren-face
Apply a dedicated face to parentheses in Elisp modes. A more subtle grey than the default foreground text is used.
dim-autoload
De-emphasise the special ###autoload cookie that Emacs uses to load things where they are needed. This package also offers the possibility to completely hide those cookies, though I have no use for that.
(use-package paren-face
  :ensure
  :hook (emacs-lisp-mode-hook . paren-face-mode))

(use-package dim-autoload
  :ensure
  :hook (emacs-lisp-mode-hook . dim-autoload-cookies-mode))

7.6 Optional visual indicators or layout elements

This is a collection of modes or interfaces I seldom use or, rather, I use under special circumstances. They are useful, but there is not need for them to be available at all times.

7.6.1 Current line highlight (hl-line-mode)

This is a mode that I only activate via hooks for certain buffers where the current line itself is more important that the actual column (e.g. in Dired buffers). Here I configure it so that the highlight applies only to the current window. There is also a "global" variant, for when the equivalent mode is used (I have no plan to use that).

(use-package hl-line
  :config
  (setq hl-line-sticky-flag nil))

7.6.2 Toggles for line numbers and whitespace indicators

Display line numbers (buffer-local)
I seldom use line numbers, but here it is. This toggles the setting for the local buffer and also activates hl-line-mode.
Display invisible characters (whitespace)
Viewing invisible characters (whitespace) can be very helpful under certain circumstances. Generally though, I do not keep it active.

As for delete-trailing-whitespace, I prefer to call it manually because sometimes it causes problems, such as with diffs.

(use-package display-line-numbers
  :config
  ;; Set absolute line numbers.  A value of "relative" is also useful.
  (setq display-line-numbers-type t)
  ;; Those two variables were introduced in Emacs 27.1
  (setq display-line-numbers-major-tick 20)
  (setq display-line-numbers-minor-tick 5)
  ;; Use absolute numbers in narrowed buffers
  (setq display-line-numbers-widen t)

  (define-minor-mode prot/display-line-numbers-mode
    "Toggle `display-line-numbers-mode' and `hl-line-mode'."
    :init-value nil
    :global nil
    (if prot/display-line-numbers-mode
        (progn
          (display-line-numbers-mode 1)
          (hl-line-mode 1)
          (setq-local truncate-lines t))
      (display-line-numbers-mode -1)
      (hl-line-mode -1)
      (setq-local truncate-lines nil)))
  :bind ("<f7>" . prot/display-line-numbers-mode))

(use-package whitespace
  :config
  (defun prot/toggle-invisibles ()
    "Toggles the display of indentation and space characters."
    (interactive)
    (if (bound-and-true-p whitespace-mode)
        (whitespace-mode -1)
      (whitespace-mode)))
  :bind (("<f6>" . prot/toggle-invisibles)
         ("C-c z" . delete-trailing-whitespace)))

7.6.3 Dim context and focus-to-thing (focus-mode)

Sometimes I opt to enlarge the font size and keep a single window so as to narrow in on the task at hand, say, to write an essay or refine some piece of code syntax. With the help of focus-mode, it is possible to also fade out the context, so that I may further facilitate my attempt at offering my undivided attention to the given element.

Also check prot/scroll-centre-cursor in the Scrolling behaviour section.

(use-package focus
  :ensure
  :commands (prot/focus-mode-to-thing
             prot/focus-mode)
  :config
  (setq-default focus-mode-to-thing
                '((prog-mode . defun)
                  (text-mode . paragraph)))

  ;; TODO review in accordance with `prot/focus-mode'
  (defun prot/focus-mode-to-thing (&optional narrow)
    "Set local values for `focus-mode-to-thing'.
With optional NARROW limit the scope of `focus-mode' to smaller
syntactic units."
    (interactive "P")
    (let* ((units (if narrow
                      (list 'sexp 'sentence)
                    (list 'defun 'paragraph)))
           (prog (car units))
           (text (cadr units)))
      (setq-local focus-mode-to-thing
                  `((prog-mode . ,prog)
                    (text-mode . ,text)))))

  ;; TODO use `define-minor-mode' instead of this
  (defun prot/focus-mode (&optional arg)
    "Toggle the use of `focus-mode'.
With optional \\[universal-argument] prefix, also invoke
`prot/focus-mode-to-thing' with its optional argument for a
narrower field of view."
    (interactive "P")
    (if (bound-and-true-p focus-mode)
        (focus-mode -1)
      (if arg
          (prot/focus-mode-to-thing t)
        (kill-local-variable 'focus-mode-to-thing))
      (focus-mode 1)))

  ;; C-c o is used by `prot/olivetti-mode' and this is a good mnemonic
  ;; for a concept of "Only the thing".
  :bind (("C-c O" . prot/focus-mode)))

7.6.4 Centred, focused text mode (olivetti-mode)

I spend much of my time in Emacs reading and writing long form texts. It is nice to be able to easily toggle a mode that centres the buffer, allowing for greater comfort.

Olivetti fulfils that niche very nicely. It is not aggressive in its interface requirements, respects my existing line settings and my preference for auto-filling text, while it does not introduce any kind of functionality beyond the scope of bringing the current window's buffer to the centre of the view. This is exactly what I need. Any other enhancement, such as a larger font size can be handled by another function.

The prot/olivetti-mode offers a toggle for activating this mode, passing additional font-related parameters (see base typeface configurations), removing the fringe on both sides of the selected window (check my fringe-mode settings), switching to a blinking bar cursor (see section on Cursor appearance and tweaks), and hiding the mode line in accordance with prot/mode-line-hidden-mode defined in the Mode line section.

Also review prot/scroll-centre-cursor-mode in the Scrolling behaviour section.

For video demonstrantions watch these:

(use-package olivetti
  :ensure
  :diminish
  :config
  (setq olivetti-body-width 0.7)
  (setq olivetti-minimum-body-width 80)
  (setq olivetti-recall-visual-line-mode-entry-state t)

  (define-minor-mode prot/olivetti-mode
    "Toggle buffer-local `olivetti-mode' with additional parameters.

Fringes are disabled.  The modeline is hidden, except for
`prog-mode' buffers (see `prot/mode-line-hidden-mode').  The
default typeface is set to a proportionately spaced family,
except for programming modes (see `prot/variable-pitch-mode').
The cursor becomes a blinking bar, per `prot/cursor-type-mode'."
    :init-value nil
    :global nil
    (if prot/olivetti-mode
        (progn
          (olivetti-mode 1)
          (set-window-fringes (selected-window) 0 0)
          (prot/variable-pitch-mode 1)
          (prot/cursor-type-mode 1)
          (unless (derived-mode-p 'prog-mode)
            (prot/mode-line-hidden-mode 1)))
      (olivetti-mode -1)
      (set-window-fringes (selected-window) nil) ; Use default width
      (prot/variable-pitch-mode -1)
      (prot/cursor-type-mode -1)
      (unless (derived-mode-p 'prog-mode)
        (prot/mode-line-hidden-mode -1))))
  :bind ("C-c o" . prot/olivetti-mode))

7.6.5 Auto-dim unfocused buffers

The concept of auto-dim-other-buffers-mode is straightforward: apply a different background on all unfocused windows, so that the current one pops out more. This is controlled at the theme level and, as with all packages, my Modus themes provide full support for it.

In my workflow, this utility is of great value while offering a screen cast or presentation. It is safer to draw attention to the current window, with its fairly large surface area, than expect the viewer to discern the more nuanced elements of focus that are discernible in the modeline and/or the cursor's shape.

(use-package auto-dim-other-buffers
  :ensure
  :commands auto-dim-other-buffers-mode
  :config
  (setq auto-dim-other-buffers-dim-on-switch-to-minibuffer nil)
  (setq auto-dim-other-buffers-dim-on-focus-out t))

7.6.6 Outline mode, outline minor mode, and extras

The outline library defines a major mode (outline-mode) that is similar to org-mode in that it consists of headings which can be expanded or contracted individually or as a group. It is meant to work on plain text files, or be leveraged by other packages, that need to have some structure and the accompanying benefits of this mode. In practice, however, I never found a dedicated use for this major mode that would justify it over the more featureful Org.

Where the outline library truly shines is in the minor mode it defines (outline-minor-mode), which provides the familiar structured, heading-folding facilities in other major modes, such as emacs-lisp-mode and markdown-mode (note: it may also work with other programming modes, though I am not a programmer so I cannot really test it). With some careful tweaks you can continue to work on your code while also benefitting from a more effective means of organising and reviewing what you have.

In practice, to make an outline for Elisp buffers, you need to start a comment line without leading spaces and make it at least three comment delimiters long (;;;). That is considered a heading level 1. Every extra delimiter will increase heading levels accordingly. Markdown headings should be recognised right away.

Now on to my configurations:

  • The built-in foldout library provides two key functions for zooming in and out of trees. This is the same as narrowing to a subtree, while also preserving whatever folding may already exist. The same could be done with standard narrowing commands, but I am keeping this for the time being.
  • The bicycle package by Jonas Bernoulli provides the cyclic folding behaviour of expanding a tree, its children, their contents, and then contracting everything at once. Same principle as with Org.
  • The outline-minor-faces package, also by Jonas, will apply colouration to the headings produced by outline-minor-mode. These inherit from outline-mode (they are configured to look the same as the ones of Org in my Modus themes—see relevant section).
  • My imenu hacks greatly expand the index so that it includes every code point in the buffer as well as all outline-mode headings. Put simply, this is excellent news because we can now use the full power of my configurations for Imenu to navigate to practically every point of interest in the buffer. Sublime!

For the now-deprecated defhydra, refer to my fundamentals for hydras. I am still not sure what my niche is for those, but let's just say that I am not convinced they offer an upgrade over what I include here with bespoke keymaps, a dedicated minor mode, etc.

Also watch my video demo of outline-minor-mode and imenu (2020-07-20).

(use-package outline
  :diminish outline-minor-mode
  :config
  ;;; NOTE change this to provide support for your major mode, in case
  ;;; it is missing.
  ;; (setq outline-regexp "[*\^L]+")

  (use-package outline-minor-faces
    :ensure
    :hook
    (outline-minor-mode-hook . outline-minor-faces-add-font-lock-keywords))

  (use-package foldout)
  (use-package bicycle :ensure)

  ;;; NOTE this could also be used to auto-reveal a fold, though I find
  ;;; it a bit annoying when things like that happen automatically.
  ;; (use-package reveal :diminish)

  ;;; NOTE DEPRECATED I am keeping this older piece for reference, but I
  ;;; am discontinuing it: I prefer the approach further below.
  ;;   (defhydra prot/hydra-outline (:idle nil :color pink
  ;;                                       :hint nil :post (deactivate-mark))
  ;;     "
  ;;    ^Motions^                            ^Show^
  ;; ---^------------------------------------^----------------
  ;; _n_: Next heading                    _a_: All
  ;; _p_: Prev heading                    _e_: Entry
  ;; _f_: Fore same level               _TAB_: Cycle
  ;; _b_: Back same level   _<S-iso-lefttab>_: Cycle global
  ;; "
  ;;     ("n" outline-next-visible-heading)
  ;;     ("p" outline-previous-visible-heading)
  ;;     ("f" outline-forward-same-level)
  ;;     ("b" outline-backward-same-level)
  ;;     ("a" outline-show-all)
  ;;     ("e" outline-show-entry)
  ;;     ("TAB" bicycle-cycle)
  ;;     ("<S-iso-lefttab>" bicycle-cycle-global)
  ;;     ("q" nil "cancel" :color blue))

  (defun prot/outline-hide-all ()
    "Hide all `outline-mode' subtrees."
    (interactive)
    (outline-map-region 'outline-hide-subtree (point-min) (point-max)))

  (defun prot/outline-down-heading ()
    "Move to the next `outline-mode' subtree."
    (interactive)
    ;; Hacky, but it kinda works.
    (outline-up-heading 1 t)
    (outline-forward-same-level 1))

  (defun prot/bicycle-cycle-tab-dwim ()
    "Convenience wrapper for TAB key in `outline-mode'."
    (interactive)
    (if (outline-on-heading-p)
        (bicycle-cycle)
      (indent-for-tab-command)))

  (defvar prot/outline-minor-mode-map
    (let ((map (make-sparse-keymap)))
      (define-key map (kbd "M-n") 'outline-next-visible-heading)
      (define-key map (kbd "M-p") 'outline-previous-visible-heading)
      (define-key map (kbd "C-c C-n") 'outline-next-visible-heading)
      (define-key map (kbd "C-c C-p") 'outline-previous-visible-heading)
      (define-key map (kbd "C-c C-f") 'outline-forward-same-level)
      (define-key map (kbd "C-c C-b") 'outline-backward-same-level)
      (define-key map (kbd "C-c C-a") 'outline-show-all)
      (define-key map (kbd "C-c C-q") 'prot/outline-hide-all)
      (define-key map (kbd "C-c C-u") 'outline-up-heading)
      (define-key map (kbd "C-c C-d") 'prot/outline-down-heading)
      (define-key map (kbd "C-c C-z") 'foldout-zoom-subtree)
      (define-key map (kbd "C-c C-x") 'foldout-exit-fold)
      (define-key map (kbd "<tab>") 'prot/bicycle-cycle-tab-dwim)
      (define-key map (kbd "<C-tab>") 'bicycle-cycle)
      (define-key map (kbd "<S-iso-lefttab>") 'bicycle-cycle-global)
      map)
    "Key map for `prot/outline-minor-mode'.
The idea is to make `outline-minor-mode' keys a bit easier to
work with.")

  (defvar prot/outline-minor-mode-enter-hook nil
    "Hook that runs when `prot/outline-minor-mode' is enabled.")

  (defvar prot/outline-minor-mode-exit-hook nil
    "Hook that runs when `prot/outline-minor-mode' is disabled.")

  ;; `imenu' integration with `outline-minor-mode'
  (defun prot/outline-imenu-heading ()
    "Move to the previous `outline-mode' heading.
This is because `imenu' produces its index by moving backward
from the bottom."
    (outline-previous-heading))

  (defun prot/outline-imenu-title ()
    "Return the text of the `outline-mode' heading."
    (interactive)
    ;; NOTE This may be too simplistic and error-prone, though I have
    ;; not ran into any problems.
    (buffer-substring (point-at-bol)
                      (point-at-eol)))

  (defun prot/outline-imenu-setup ()
    "`imenu' bindings for the local `outline-mode' buffer.
To be used in tandem with `prot/outline-minor-mode-enter-hook'."
    (setq-local imenu-prev-index-position-function
                'prot/outline-imenu-heading)
    (setq-local imenu-extract-index-name-function
                'prot/outline-imenu-title))

  (defun prot/outline-imenu-restore ()
    "Restore `imenu' list when `prot/outline-minor-mode' is off.
The new index should be the same as the one you would get in a
standard invocation of `imenu'.

To be used in `prot/outline-minor-mode-exit-hook'."
    (dolist (var '(imenu-prev-index-position-function
                   imenu-extract-index-name-function))
      (kill-local-variable var))
    (save-excursion
      (imenu-default-create-index-function)
      (message "Refreshed `imenu' index")))

  ;; XXX This could prove problematic when more than one major modes are
  ;; active, such as when you use C-c ' in an Org block.
  (defun prot/outline-minor-refontify ()
    "Re-enable the current buffer's major mode.
Add this to `prot/outline-minor-mode-exit-hook'."
    (let ((mode major-mode))
      (when (or (derived-mode-p 'text-mode)
                (derived-mode-p 'prog-mode))
        (funcall mode)
        (message "Re-enabled %s" mode))))

  (define-minor-mode prot/outline-minor-mode
    "Toggles `outline-minor-mode' and extras.

\\{prot/outline-minor-mode-map}"
    :init-value nil
    :lighter " =┆"
    :global nil
    :keymap prot/outline-minor-mode-map
    (if prot/outline-minor-mode
        (progn
          (when (eq major-mode 'org-mode)
            (user-error "Don't use `outline-minor-mode' with Org"))
          (outline-minor-mode 1)
          (run-hooks 'prot/outline-minor-mode-enter-hook))
      (outline-minor-mode -1)
      (run-hooks 'prot/outline-minor-mode-exit-hook)))

  :hook ((prot/outline-minor-mode-enter-hook . prot/outline-imenu-setup)
         (prot/outline-minor-mode-exit-hook . prot/outline-imenu-restore)
         (prot/outline-minor-mode-exit-hook . prot/outline-minor-refontify))
  ;; key replaces `menu-bar-open', which I never use
  :bind ("<f10>" . prot/outline-minor-mode))
7.6.6.1 TODO Cleanly restore all major modes for outline-mode re-fontification
7.6.6.2 TODO How to re-fontify outline-mode without re-running the major mode?

7.7 Registers

Registers are compartments that hold data of various sorts. They offer the means for advanced, highly efficient workflows, especially when combined with keyboard macros.

Registers are called by a single character, which can be a letter (case-sensitive), number, or symbol. Each character can only contain a single register at a time.

To define a register, you call the appropriate command (see table below) and then specify the character you want to store that data at.

Key chord Command
C-x r n number-to-register
C-x r s copy-to-register (think "save string")
C-x r r copy-rectangle-to-register
C-x r SPC point-to-register
C-x r w window-configuration-to-register
C-x r f frameset-to-register (frames and their windows)
C-x r + increment-register (better used with numbers)
C-x r i insert-register (text, number, rectangle)
C-x r j jump-to-register (to point or window/frameset config)

Notes about some of the above:

  • Using point-to-register allows you to revisit a specific location in a buffer, but also reopen the file visited by that buffer in case the buffer is deleted.
  • Calling number-to-register without an argument will just store the number 0 to the register you specify. Whereas C-u 100 C-x r n will store 100. In practice, you often want to use the latter method.
  • Use increment-register to increment a number by one. Pass a numeric argument to increment by that amount instead. For example, to increment by five do C-u 5 C-x r + and then select the register you want to operate on. This only affects the value stored in the register. It does not also insert it in the buffer.
  • Number registers are particularly useful when you want to increment several numbers through a keyboard macro. You can record the motions you need, run increment-register as noted above followed by the standard insert-register. If, however, you just want to increment a single number through a keyboard macro, then just use the counter provided by that facility (refer to my video on easier kmacro counter from 2019-10-14).
  • The registers that store text as a string or a rectangle rely on the active region to capture the data. They are also great for keyboard macros where you cannot rely on yanking from the head of the kill ring (because, say, you are killing other things which push that value further down). Note though that increment-register has a different behaviour when applied to them, where it will append to the register instead (with an option to override the previous value).
  • While on the topic of appending to registers, I define several functions that change how the accumulation of text is supposed to happen. They introduce a space or line separator between the entries you {ap,pre}-pend to the register.
  • In my testing, I could never make window-configuration-to-register persist between sessions (see section on Emacs server and desktop). Whereas all other registers retain their values. So use this command to store window configurations that are otherwise transient in nature. For more permanent setups, rely on frameset-to-register which will produce a new frame (or more if you had) with all the windows in place. Remember that you can always go back to your previous window configuration without using registers, such as with the built-in winner-mode (see section on window history and directional motions).
  • While on the topics of storing registers across sessions, I find that sometimes I will collect too many registers that I do not really need any more. So prot/clear-registers just resets the list.
(use-package register
  :commands (prot/clear-registers
             prot/append-register-line-separator
             prot/append-register-space-separator
             prot/prepend-register-line-separator
             prot/prepend-register-space-separator)
  :config
  ;;;;;; Use this if you need it
  ;;;; Define f5 as an alias for C-x r
  ;; (global-set-key (kbd "<f5>") (lookup-key global-map (kbd "C-x r")))

  (defun prot/clear-registers ()
    "Remove all saved registers."
    (interactive)
    (setq register-alist nil))

  (defun prot/append-register-space-separator (start end)
    "Append region to register with space in between entries.

A prompt will ask for the register to operate on."
    (interactive "r")
    (let* ((prompt (register-read-with-preview
                    "Append to register with space separator: ")))
      (setq register-separator ?+)
      (set-register register-separator " ")
      (append-to-register prompt start end nil)))

  (defun prot/append-register-line-separator (start end)
    "Append region to register with an empty line in between entries.

A prompt will ask for the register to operate on."
    (interactive "r")
    (let* ((prompt (register-read-with-preview
                    "Append to register with line separator: ")))
      (setq register-separator ?+)
      (set-register register-separator "\n\n")
      (append-to-register prompt start end nil)))

  (defun prot/prepend-register-space-separator (start end)
    "Prepend region to register with space in between entries.

A prompt will ask for the register to operate on."
    (interactive "r")
    (let* ((prompt (register-read-with-preview
                    "Prepend to register with space separator: ")))
      (setq register-separator ?+)
      (set-register register-separator " ")
      (prepend-to-register prompt start end nil)))

  (defun prot/prepend-register-line-separator (start end)
    "Prepend region to register with an empty line in between entries.

A prompt will ask for the register to operate on."
    (interactive "r")
    (let* ((prompt (register-read-with-preview
                    "Prepend to register with line separator: ")))
      (setq register-separator ?+)
      (set-register register-separator "\n\n")
      (prepend-to-register prompt start end nil))))

7.8 Visual bookmarks (bm.el)

This is a deceptively simple-yet-effective package for marking a point in a buffer for future review. I learnt about it from Manuel Uberti's blog entry on the matter (2020-03-19). As Manuel puts it:

I find bm.el really useful when studying source code from others, or when I want to quickly set jumping points in a log file cluttered with stacktraces. It’s quicker then moving around with Isearch or helm-occur, and unlike avy it’s not limited to what is currently visible on the screen.

I use visual bookmarks in much the same way.

(use-package bm
  :ensure
  :init
  (setq bm-restore-repository-on-load t)
  :config
  (setq bm-annotate-on-create nil)
  (setq bm-buffer-persistence t)
  (setq bm-cycle-all-buffers t)
  (setq bm-goto-position nil)
  (setq bm-highlight-style 'bm-highlight-only-fringe)
  (setq bm-marker 'bm-marker-left)
  (setq bm-in-lifo-order nil)
  (setq bm-recenter t)
  (setq bm-repository-file "~/.emacs.d/bm-bookmarks")
  (setq bm-repository-size 100)
  (setq bm-show-annotations t)
  (setq bm-wrap-immediately t)
  (setq bm-wrap-search t)
  :bind (("<C-f8>" . bm-next)
         ("<C-S-f8>" . bm-previous)
         ("<s-f8>" . bm-toggle-buffer-persistence)
         ("<f8>" . bm-toggle)))

7.9 Annotations (annotate.el)

While inline statements are often the best way to take a note in its context, there are cases where you do not want to edit the underlying file or buffer yet still comment on it. This is the basic idea of the annotate library, by Bastian Bechtold.

With the point over a word, or with an active region, call the command to write an annotation (annotate-annotate). Follow it up with your input. The note will be displayed using appropriate typographic styles and colours. While your file/buffer will remain in tact.

The above granted, it is always possible to write these annotations inline, by means of annotate-integrate-annotations. This can be particularly helpful when sharing plain text via email. Another option is to export the annotation as a diff patch, but I have no use for it.

Note that the annotation's text is not displayed when it is inside an Org block (such as the following code snippet). This is a known issue that is documented in the project's Git repo.

(use-package annotate
  :ensure
  :diminish
  :commands (annotate-annotate
             prot/annotate-annotate
             annotate-goto-next-annotation
             annotate-goto-previous-annotation
             annotate-export-annotations
             annotate-integrate-annotations
             annotate-show-annotation-summary)
  :config
  (setq annotate-file "~/.emacs.d/annotations")
  (setq annotate-annotation-column 73)
  (setq annotate-diff-export-context 5)
  (setq annotate-use-messages nil)
  (setq annotate-integrate-marker "")
  (setq annotate-integrate-higlight ?^)
  (setq annotate-fallback-comment "#")
  (setq annotate-blacklist-major-mode '())
  (setq annotate-annotation-max-size-not-place-new-line 50)
  (setq annotate-search-region-lines-delta 4)
  (setq annotate-annotation-position-policy :by-length)
  (setq annotate-summary-ask-query nil)

  (defun prot/annotate-mode ()
    "Toggles `annotate-mode' for the current buffer."
    (if (bound-and-true-p annotate-mode)
        (annotate-mode -1)
      (annotate-mode 1)))

  (defun prot/annotate-annotate ()
    "Ensure `annotate-mode' is enabled for `annotate-annotate'."
    (unless (bound-and-true-p annotate-mode)
      (annotate-mode 1))
    (annotate-annotate))

  (defun prot/annotate-annotate-dwim (&optional arg)
    "Common points of entry for annotations.
Write an annotation or toggle `annotate-mode' by prefixing this
function with the \\[universal-argument]."
    (interactive "P")
    (if arg
        (prot/annotate-mode)
      (prot/annotate-annotate)))

  (defhydra prot/hydra-annotate (:idle nil :color pink :hint nil
                                       :post (deactivate-mark))
    "
   ^Motions^                ^Actions^
---^------------------------^----------------
_n_: Next annotation     _a_: Annotate
_p_: Prev annotation     _i_: Integrate
^^                       _s_: Show summary
"
    ("n" annotate-goto-next-annotation)
    ("p" annotate-goto-previous-annotation)
    ("a" prot/annotate-annotate)
    ("i" annotate-integrate-annotations)
    ("s" annotate-show-annotation-summary)
    ("q" nil "cancel" :color blue))

  :bind (("<f9>" . prot/annotate-annotate-dwim)
         ("<C-f9>" . annotate-goto-next-annotation)
         ("<C-S-f9>" . annotate-goto-previous-annotation)
         :map annotate-mode-map
         ("C-c h a" . prot/hydra-annotate/body)))

7.10 Cursor and mouse settings

7.10.1 Cursor appearance and tweaks

My cursor for the current window is a box character that does not blink. Other windows use a vertical bar that is 2 pixels wide.

With prot/cursor-type-mode I switch to a bar cursor that blinks. This is visually better when proportionately spaced fonts are on display, such as with my prot/variable-pitch-mode and prot/olivetti-mode. Otherwise it is a convenient toggle for when I need to have that effect.

(use-package frame
  :commands prot/cursor-type-mode
  :config
  (setq-default cursor-type 'box)
  (setq-default cursor-in-non-selected-windows '(bar . 2))
  (setq-default blink-cursor-blinks 50)
  (setq-default blink-cursor-interval nil) ; 0.75 would be my choice
  (setq-default blink-cursor-delay 0.2)

  (blink-cursor-mode -1)

  (define-minor-mode prot/cursor-type-mode
    "Toggle between static block and pulsing bar cursor."
    :init-value nil
    :global t
    (if prot/cursor-type-mode
        (progn
          (setq-local blink-cursor-interval 0.75
                      cursor-type '(bar . 2)
                      cursor-in-non-selected-windows 'hollow)
          (blink-cursor-mode 1))
      (dolist (local '(blink-cursor-interval
                       cursor-type
                       cursor-in-non-selected-windows))
        (kill-local-variable `,local))
      (blink-cursor-mode -1))))

7.10.2 Mouse avoidance mode

While my normal Emacs usage does not involve the mouse, I do sometimes have to use it. Minutes later the pointer is in the way of whatever it is I am staring at and so I must manually move it out of the way.

Thankfully Emacs has a built-in mode that automates that kind of process. mouse-avoidance-mode has several possible styles for moving the pointer out of your way: read them with C-h f mouse-avoidance-mode.

(use-package avoid
  :config
  (setq mouse-avoidance-banish-position
        '((frame-or-window . frame)
          (side . right)
          (side-pos . 0)
          (top-or-bottom . top)
          (top-or-bottom-pos . 0)))

  (mouse-avoidance-mode 'animate))

7.10.3 Multiple cursors (edit in several parts at once)

The multiple-cursors package offers a comprehensive list of commands to edit text in several points of the screen at once. Though I normally rely on keyboard macros and query-replace to perform complex edits in numerous parts of a buffer or set thereof, it sometimes is easier to just pop a few extra cursors and type directly, especially for quick, one-time operations.

For the technically minded, make sure to read Christopher Wellons' article on Gap Buffers Are Not Optimized for Multiple Cursors. Also quoting from the source code (M-x find-library multiple-cursors):

Known limitations

  • isearch-forward and isearch-backward aren't supported with multiple cursors. You should feel free to add a simplified version that can work with it.
  • Commands run with `M-x` won't be repeated for all cursors.
  • All key bindings that refer to lambdas are always run for all cursors. If you need to limit it, you will have to give it a name.
  • Redo might screw with your cursors. Undo works very well.

So learn keyboard macros and query-replace and let this package handle more straightforward workflows.

(use-package multiple-cursors
  :ensure
  :bind (("C-<" . mc/mark-previous-like-this)
         ("C->" . mc/mark-next-like-this)
         ("s->" . mc/mark-all-like-this)))

7.10.4 pulse.el (highlight cursor position)

This is a library that provides utilities for highlighting the region or area around point. It is meant to be used by other packages as a means of offering visual feedback, as is the case with, for example. M-. (xref-find-definitions). I also make use of this in my settings for Imenu (dynamic completion-based navigation) and Elfeed RSS reader.

Here I define a simple command for highlighting the current line on demand. It fully replaces the niche I had carved for the beacon package, which is to make clear where my cursor is while doing a video demo.

(use-package pulse
  :config
  (defface prot/pulse-line-modus-theme
    '((t :inherit modus-theme-subtle-green :extend t))
    "Ad-hoc face for `prot/pulse-line'.
This is done because it is not possible to highlight empty lines
without the `:extend' property.")

  (defun prot/pulse-line (&optional face)
    "Temporarily highlight the current line."
    (interactive)
    (let ((start (if (eobp)
                     (line-beginning-position 0)
                   (line-beginning-position)))
          (end (line-beginning-position 2))
          (pulse-delay .04)
          (face (or face 'prot/pulse-line-modus-theme)))
      (pulse-momentary-highlight-region start end face)))
  :bind ("<s-escape>" . prot/pulse-line))

7.10.5 Mouse wheel behaviour

The value of mouse-wheel-scroll-amount means the following:

  • By default scroll by one line.
  • Hold down Shift to do so by five lines.
  • Hold down Meta to scroll half a screen.
  • Hold down Control to adjust the size of the text. This was added in Emacs 27.

The other options in short:

  • Hide mouse pointer while typing.
  • Enable mouse scroll.
  • Faster wheel movement means faster scroll.
  • Scroll window under mouse pointer regardless of whether it is the current one or not.

Note that if we enable mouse-drag-copy-region we automatically place the mouse selection to the kill ring. This is the same behaviour as terminal emulators that place the selection to the clipboard (or the primary selection). I choose not to use this here.

(use-package mouse
  :config
  ;; In Emacs 27+, use Control + mouse wheel to scale text.
  (setq mouse-wheel-scroll-amount
        '(1
          ((shift) . 5)
          ((meta) . 0.5)
          ((control) . text-scale)))
  (setq mouse-drag-copy-region nil)
  (setq make-pointer-invisible t)
  (setq mouse-wheel-progressive-speed t)
  (setq mouse-wheel-follow-mouse t)
  :hook (after-init-hook . mouse-wheel-mode))

7.10.6 Scrolling behaviour

By default, page scrolling should keep the point at the same visual position, rather than force it to the top or bottom of the viewport. This eliminates the friction of guessing where the point has warped to.

As for per-line scrolling, I dislike the default behaviour of visually re-centring the point: it is too aggressive as a standard mode of interaction. With the following setq-default, the point will stay at the top/bottom of the screen while moving in that direction (use C-l to reposition it).

While prot/scroll-centre-cursor-mode is a simple and super effective way to get the always-centred behaviour on demand. I use it when I am reading prose or reviewing code and expect to perform few or no edits. As such, this piece of fumctionality makes for a natural complement to my configs for olivetti-mode as well as the somewhat more specialised focus-mode for dimming context.

(use-package emacs
  :config
  (setq-default scroll-preserve-screen-position t)
  (setq-default scroll-conservatively 1) ; affects `scroll-step'
  (setq-default scroll-margin 0)

  (define-minor-mode prot/scroll-centre-cursor-mode
    "Toggle centred cursor scrolling behaviour."
    :init-value nil
    :lighter " S="
    :global nil
    (if prot/scroll-centre-cursor-mode
        (setq-local scroll-margin (* (frame-height) 2)
                    scroll-conservatively 0
                    maximum-scroll-margin 0.5)
      (dolist (local '(scroll-preserve-screen-position
                       scroll-conservatively
                       maximum-scroll-margin
                       scroll-margin))
        (kill-local-variable `,local))))

  ;; C-c l is used for `org-store-link'.  The mnemonic for this is to
  ;; focus the Line and also works as a variant of C-l.
  :bind ("s-l" . prot/scroll-centre-cursor-mode))

7.10.7 Delete selection

This is a very helpful mode. It kills the marked region when inserting directly to it. It also has checks to ensure that yanking over a selected region will not insert itself when mouse-drag-copy-region is in effect (see the section on the mouse wheel behaviour).

(use-package delsel
  :hook (after-init-hook . delete-selection-mode))

Pro tip: On Emacs 27.1 or higher you can create a rectangular region by holding down Ctrl and Meta while dragging the mouse with the left click pressed.

7.10.8 Tooltips (tooltip-mode)

These settings control how tool tips are to be handled when hovering the mouse over an actionable item:

  • I just want to make sure that the GTK theme is not used for those: I prefer the generic display which follows my current theme's styles.
  • The delay is slightly reduced for the initial pop-up, while it has been increased for immediate pop-ups thereafter.
(use-package tooltip
  :config
  (setq tooltip-delay 0.5)
  (setq tooltip-short-delay 0.5)
  (setq x-gtk-use-system-tooltips nil)
  (setq tooltip-frame-parameters
        '((name . "tooltip")
          (internal-border-width . 6)
          (border-width . 0)
          (no-special-glyphs . t)))
  :hook (after-init-hook . tooltip-mode))

7.11 Conveniences and minor extras

7.11.1 Auto revert mode

This mode ensures that the buffer is updated whenever the file changes. A change can happen externally or by some other tool inside of Emacs (e.g. kill a Magit diff).

(use-package autorevert
  :diminish
  :config
  (setq auto-revert-verbose t)
  :hook (after-init-hook . global-auto-revert-mode))

7.11.2 Preserve contents of system clipboard

Say you copied a link from your web browser, then switched to Emacs to paste it somewhere. Before you do that, you notice something you want to kill. Doing that will place the last kill to the clipboard, thus overriding the thing you copied earlier. We can have a kill ring solution to this with the following:

(use-package emacs
  :config
  (setq save-interprogram-paste-before-kill t))

Now the contents of the clipboard are stored in the kill ring and can be retrieved from there (e.g. with M-y).

7.11.3 Generic feedback

The common thread of these options is the feedback they provide us with or simplify common tasks so that their feedback does not cause friction:

  • Show the current buffer's name as the frame's title. This only affects window managers that have window decorations. If you do not know what that means, then you are most likely using an environment where frame titles are already visible.
  • Faster feedback for key chords (keys appear in the echo area).
  • Allow inputting Greek while preserving Emacs keys. Toggle with C-\.
  • Ignore visual or audible bells. Emacs has more appropriate ways of providing error/warning messages or hints that something is not producing the desired results (e.g. a failed isearch will return no results, while the failed match will be styled accordingly in the echo area). By the way, use C-h e to bring up the log with the echo area's messages.
  • Answer with just the initials when dealing with "yes/no" questions.
  • Enable actions for narrowing the buffer, region {up,down}casing (all caps or no caps), dired single-buffer navigation (bound to a). Disable overwrite-mode.
(use-package emacs
  :config
  (setq frame-title-format '("%b"))
  (setq echo-keystrokes 0.25)
  (setq default-input-method "greek")
  (setq ring-bell-function 'ignore)

  (defalias 'yes-or-no-p 'y-or-n-p)
  (put 'narrow-to-region 'disabled nil)
  (put 'upcase-region 'disabled nil)
  (put 'downcase-region 'disabled nil)
  (put 'dired-find-alternate-file 'disabled nil)
  (put 'overwrite-mode 'disabled t))

7.11.4 Newline characters for file ending

For some major modes, such as diff-mode, a final newline is of paramount importance. Without it you cannot, for instance, apply a patch cleanly. As such, the mode-require-final-newline will add a newline character when visiting or saving a buffer of relevance.

(use-package emacs
  :config
  (setq mode-require-final-newline 'visit-save))

7.11.5 Altered zap and easier repeats

I seldom use the functionality related to this section, but when I do I prefer it to work the way I expect. zap-up-to-char will delete everything from point up to the character you provide it with. Think of how you may want to delete a file name but keep its file type extension.

The repeat command is bound by default to C-x z. I make it so that subsequent repetitions require only hitting another z. In practice though, you should not bother with this. Let keyboard macros handle that task.

Pro tip: to make a keyboard macro out of your most recent commands, use C-x C-k l which calls kmacro-edit-lossage. The list is editable, so remove anything that is not required and then save what is left. The result is stored as the latest keyboard macro (and you also have the power to cycle through kmacros, store them in specific keys, etc.).

Moving on to the mark, practically every Emacs motion that operates on a portion of text will set the mark automatically. You can also do it manually with C-SPC (hit it twice if you do not wish to activate the region). It is then possible to cycle through the marks in reverse order by passing a prefix argument C-u C-SPC. With the evaluation of set-mark-command-repeat-pop as t we can continue cycling by repeated presses of C-SPC. Again though, this is not the type of functionality I rely on: for more deliberate actions of this sort, consider Emacs' notion of "registers".

(use-package emacs
  :config
  (setq repeat-on-final-keystroke t)
  (setq set-mark-command-repeat-pop t)
  :bind ("M-z" . zap-up-to-char))

7.11.6 Package lists

With this I just want to enable line highlighting when browsing the list of packages. I generally use hl-line-mode on all interfaces where the current line is more important than the exact column of the point.

(use-package package
  :commands (list-packages
             package-refresh-contents
             package-list-packages)
  :config
  ;; All variables are for Emacs 28+
  (setq package-name-column-width 40)
  (setq package-version-column-width 14)
  (setq package-status-column-width 12)
  (setq package-archive-column-width 8)
  :hook (package-menu-mode-hook . hl-line-mode))

8 Language settings for prose and code

This section is all about configurations and/or packages that deal with natural or programming language enhancements.

8.1 Support for various major modes

These provide syntax highlighting and additional features for environments that are not already supported by Emacs.

8.1.1 Markdown

I edit lots of Markdown files. This makes things easier.

(use-package markdown-mode
  :ensure
  :config
  ;; Allows for fenced block focus with C-c ' (same as Org blocks).
  (use-package edit-indirect :ensure)
  (setq markdown-fontify-code-blocks-natively t)
  :mode ("\\.md$" . markdown-mode))

8.1.2 YAML

This adds support for YAML files.

(use-package yaml-mode
  :ensure
  :mode (("\\.yml$" . yaml-mode)
         ("\\.yaml$" . yaml-mode)))

8.1.3 CSS

This is the built-in mode for working with CSS and SCSS. I just want it to not apply previews to colour references. If I ever need that, there is rainbow-mode (see relevant section).

(use-package css-mode
  :mode (("\\.css$" . css-mode)
         ("\\.scss$" . scss-mode))
  :config
  (setq css-fontify-colors nil))

8.1.4 Arch Linux package recipes (pkgbuild-mode)

If you are on Arch or derivatives and check PKGBUILD files, this is for you.

(use-package pkgbuild-mode
  :ensure
  :mode ("PKGBUILD" . pkgbuild-mode))

Since we are here, I build Emacs from the master branch source, using a sightly modified script than the one in AUR's repos. Below is the file on which I perform makepkg (followed by makepkg -i NAME.pkg.tar.zst), all from inside Emacs of course:

# Maintainer: Pedro A. López-Valencia <https://aur.archlinux.org/users/vorbote>

################################################################################
# CAVEAT LECTOR: This PKGBUILD is highly opinionated. I give you
#                enough rope to hang yourself, but by default it
#                only enables the features I use.
#
#        TLDR: yaourt users, cry me a river.
#
#        Everyone else: do not update blindly this PKGBUILD. At least
#        make sure to compare and understand the changes.
#
################################################################################

################################################################################
# Assign "YES" to the variable you want enabled; empty or any other value
# for NO.
#
# Where you read experimental, replace with foobar.
# =================================================
#
################################################################################
CHECK=            # Run tests. May fail, this is developement after all.
CLANG=            # Use clang.
GOLD=             # Use the gold linker.
LTO=              # Enable link-time optimization. Read emacs's INSTALL before
                  # attempting to use it with clang.
CLI=              # CLI only binary.
NOTKIT=           # Use no toolkit widgets. Like B&W Twm (001d sk00l).
LUCID=            # Use the lucid, a.k.a athena, toolkit. Like XEmacs, sorta.
                  #
                  # Read https://wiki.archlinux.org/index.php/X_resources
                  # https://en.wikipedia.org/wiki/X_resources
                  # and https://www.emacswiki.org/emacs/XftGnuEmacs
                  # for some tips on using outline fonts with
                  # Xft, if you choose no toolkit or Lucid.
                  #
GTK2=             # GTK2 support. Why would you?
NOCAIRO=          # Disable here.
XWIDGETS=         # Use GTK+ widgets pulled from webkit2gtk. Usable.
DOCS_HTML=        # Generate and install html documentation.
DOCS_PDF=         # Generate and install pdf documentation.
MAGICK=           # ImageMagick 7 support. Deprecated (read the logs).
                  # ImageMagick, like flash, won't die...
                  # -->>If you just *believe* you need ImageMagick, you don't.<<--
NOGZ="NO"         # Don't compress .el files.
################################################################################

################################################################################
pkgname="emacs-git"
pkgver=28.0.50.142787
pkgrel=1
pkgdesc="GNU Emacs. Development master branch."
arch=('x86_64' )
url="http://www.gnu.org/software/emacs/"
license=('GPL3' )
depends=('alsa-lib' 'gnutls' 'libxml2' 'jansson' 'm17n-lib' 'libotf' 'harfbuzz' 'gpm')
makedepends=('git')
provides=('emacs' 'emacs-seq')
conflicts=('emacs' 'emacs26-git' 'emacs-27-git' 'emacs-seq')
replaces=('emacs26-git' 'emacs27-git' 'emacs-seq')
source=("emacs-git::git://git.savannah.gnu.org/emacs.git")
# If Savannah access is blocked for reasons, use Github instead.
# Edit the config file of your local repo copy as well.
#source=("emacs-git::git://github.com/emacs-mirror/emacs.git")
options=(!strip)
md5sums=('SKIP')
################################################################################

################################################################################

if [[ $GOLD == "YES" && ! $CLANG == "YES" ]]; then
  export LD=/usr/bin/ld.gold
  export CFLAGS+=" -fuse-ld=gold";
  export CXXFLAGS+=" -fuse-ld=gold";
elif [[ $GOLD == "YES" && $CLANG == "YES" ]]; then
  echo "";
  echo "Clang rather uses its own linker.";
  echo "";
  exit 1;
fi

if [[ $CLANG == "YES" ]]; then
  export CC="/usr/bin/clang" ;
  export CXX="/usr/bin/clang++" ;
  export CPP="/usr/bin/clang -E" ;
  export LD="/usr/bin/lld" ;
  export AR="/usr/bin/llvm-ar" ;
  export AS="/usr/bin/llvm-as" ;
  export CCFLAGS+=' -fuse-ld=lld' ;
  export CXXFLAGS+=' -fuse-ld=lld' ;
  makedepends+=( 'clang' 'lld' 'llvm') ;
fi

if [[ $LTO == "YES" ]] && [[ $CLANG != "YES" ]]; then
  CFLAGS+=" -flto -fuse-linker-plugin"
  CXXFLAGS+=" -flto -fuse-linker-plugin"
else
  CFLAGS+=" -flto"
  CXXFLAGS+=" -flto"
fi

if [[ $NOTKIT == "YES" ]]; then
  depends+=( 'dbus' 'hicolor-icon-theme' 'libxinerama' 'libxrandr' 'lcms2' 'librsvg' 'libxfixes' );
  makedepends+=( 'xorgproto' );
elif [[ $LUCID == "YES" ]]; then
  depends+=( 'dbus' 'hicolor-icon-theme' 'libxinerama' 'libxfixes' 'lcms2' 'librsvg' 'xaw3d' 'libxrandr' );
  makedepends+=( 'xorgproto' );
elif [[ $GTK2 == "YES" ]]; then
  depends+=( 'gtk2' 'lcms2' );
  makedepends+=( 'xorgproto' );
else
  depends+=( 'gtk3' );
  makedepends+=( 'xorgproto' );
fi

if [[ $MAGICK == "YES" ]]; then
  depends+=( 'imagemagick'  'libjpeg-turbo' 'giflib' );
elif [[ ! $NOX == "YES" ]]; then
  depends+=( 'libjpeg-turbo' 'giflib' );
else
  depends+=();
fi

if [[ ! $NOCAIRO == "YES" ]]; then
  depends+=( 'cairo' );
fi

if [[ $XWIDGETS == "YES" ]]; then
  if [[ $GTK2 == "YES" ]] || [[ $LUCID == "YES" ]] || [[ $NOTKIT == "YES" ]] || [[ $CLI == "YES" ]]; then
    echo "";
    echo "";
    echo "Xwidgets support *requires* gtk+3!!!";
    echo "";
    echo "";
    exit 1;
  else
    depends+=( 'webkit2gtk' );
  fi
fi

if [[ $DOCS_PDF == "YES" ]]; then
  makedepends+=( 'texlive-core' );
fi
################################################################################

################################################################################
pkgver() {
  cd "$srcdir/emacs-git"

  printf "%s.%s" \
    "$(grep AC_INIT configure.ac | \
    sed -e 's/^.\+\ \([0-9]\+\.[0-9]\+\.[0-9]\+\?\).\+$/\1/')" \
    "$(git rev-list --count HEAD)"
}

# There is no need to run autogen.sh after first checkout.
# Doing so, breaks incremental compilation.
prepare() {
  cd "$srcdir/emacs-git"
  [[ -x configure ]] || ( ./autogen.sh git && ./autogen.sh autoconf )
}

if [[ $CHECK == "YES" ]]; then
check() {
  cd "$srcdir/emacs-git"
  make check
}
fi

build() {
  cd "$srcdir/emacs-git"

  local _conf=(
    --prefix=/usr
    --sysconfdir=/etc
    --libexecdir=/usr/lib
    --localstatedir=/var
    --mandir=/usr/share/man
    --with-gameuser=:games
    --with-sound=alsa
    --with-modules
# Beware https://debbugs.gnu.org/cgi/bugreport.cgi?bug=25228
# dconf and gconf break font settings you set in ~/.emacs.
# If you insist you'll need to read that bug report in *full*.
# Good luck!
   --without-gconf
   --without-gsettings
  )

################################################################################

################################################################################

if [[ $CLANG == "YES" ]]; then
  _conf+=(
    '--enable-autodepend'
 );
fi

if [[ $LTO == "YES" ]]; then
  _conf+=(
    '--enable-link-time-optimization'
  );
fi

if [[ $CLI == "YES" ]]; then
  _conf+=( '--without-x' '--with-x-toolkit=no' '--without-xft' '--without-lcms2' '--without-rsvg' );
elif [[ $NOTKIT == "YES" ]]; then
  _conf+=( '--with-x-toolkit=no' '--without-toolkit-scroll-bars' '--with-xft' '--without-xaw3d' );
elif [[ $LUCID == "YES" ]]; then
  _conf+=( '--with-x-toolkit=lucid' '--with-xft' '--with-xaw3d' );
elif [[ $GTK2 == "YES" ]]; then
  _conf+=( '--with-x-toolkit=gtk2' '--without-gsettings' '--without-xaw3d' );
else
  _conf+=( '--with-x-toolkit=gtk3' '--without-xaw3d' );
fi

if [[ $MAGICK == "YES" ]]; then
  _conf+=( '--with-imagemagick');
else
  _conf+=();
fi

if [[ $NOCAIRO == "YES" ]]; then
  _conf+=( '--without-cairo' );
fi

if [[ $XWIDGETS == "YES" ]]; then
  _conf+=( '--with-xwidgets' );
fi

if [[ $NOGZ == "YES" ]]; then
  _conf+=( '--without-compress-install' );
fi
################################################################################

################################################################################

  ./configure "${_conf[@]}"

  # Using "make" instead of "make bootstrap" enables incremental
  # compiling. Less time recompiling. Yay! But you may
  # need to use bootstrap sometimes to unbreak the build.
  # Just add it to the command line.
  #
  # Please note that incremental compilation implies that you
  # are reusing your src directory!
  #
  make

  # You may need to run this if 'loaddefs.el' files become corrupt.
  #cd "$srcdir/emacs-git/lisp"
  #make autoloads
  #cd ../

  # Optional documentation formats.
  if [[ $DOCS_HTML == "YES" ]]; then
    make html;
  fi
  if [[ $DOCS_PDF == "YES" ]]; then
    make pdf;
  fi

}

package() {
  cd "$srcdir/emacs-git"

  make DESTDIR="$pkgdir/" install

  # Install optional documentation formats
  if [[ $DOCS_HTML == "YES" ]]; then make DESTDIR="$pkgdir/" install-html; fi
  if [[ $DOCS_PDF == "YES" ]]; then make DESTDIR="$pkgdir/" install-pdf; fi

  # remove conflict with ctags package
  mv "$pkgdir"/usr/bin/{ctags,ctags.emacs}

  if [[ $NOGZ == "YES" ]]; then
    mv "$pkgdir"/usr/share/man/man1/{ctags.1,ctags.emacs.1};
  else
    mv "$pkgdir"/usr/share/man/man1/{ctags.1.gz,ctags.emacs.1.gz}
  fi

  # fix user/root permissions on usr/share files
  find "$pkgdir"/usr/share/emacs/ | xargs chown root:root

  # fix permssions on /var/games
  mkdir -p "$pkgdir"/var/games/emacs
  chmod 775 "$pkgdir"/var/games
  chmod 775 "$pkgdir"/var/games/emacs
  chown -R root:games "$pkgdir"/var/games

}

################################################################################
# vim:set ft=sh ts=2 sw=2 et:

8.2 Line length (column count)

The column count is set to 72. The standard line length is 80 characters, so having it at something less allows for such things as quoting plain text, indenting, etc. git commit messages also make good use of this method. The column count is used by auto-fill-mode and similar tools (or when manually invoking text formatting with fill-paragraph or equivalent—normally bound to M-q).

(use-package emacs
  :config
  (setq-default fill-column 72)
  :hook (after-init-hook . column-number-mode))

8.3 Paragraphs and fill-mode

Let us first configure how paragraphs should be treated. I find that a double space is the best way to delimit sentences in source form. There is no worry that this will be shown on a website or rendered version of a document, because processors know how to handle spacing. We do this to make things easier to tell apart, but also to render unambiguous commands like forward-sentence.

(use-package emacs
  :config
  (setq sentence-end-double-space t)
  (setq colon-double-space nil)
  (setq use-hard-newlines nil)
  (setq sentence-end-without-period nil))

Now we need to make sure we run the minor mode that keeps paragraphs within the column limit I prefer: you can always do it manually for the current paragraph or the active region with fill-paragraph, bound by default to M-q.

By targeting text-mode we also affect every major mode derived from it, which means that we correctly do not get this utility in programming-related modes (in principle, those come from prog-mode). The adaptive mode improves the handling of things like bulleted and numbered lists, where it recognises the text's prefix and tries to align to it rather than the absolute beginning of the line.

(use-package emacs
  :diminish auto-fill-function
  :config
  (setq adaptive-fill-mode t)

  (defun prot/unfill-region-or-paragraph (&optional region)
    "Unfill paragraph or, when active, the region.
Join all lines in a region, if active, while respecting any empty
lines (so multiple paragraphs are not joined, just unfilled).  If
no region is active, operate on the paragraph.  The idea is to
produce the opposite effect of both `fill-paragraph' and
`fill-region'."
    (interactive)
    (let ((fill-column most-positive-fixnum))
      (if (use-region-p)
          (fill-region (region-beginning) (region-end))
        (fill-paragraph nil region))))

  :hook (text-mode-hook . (lambda ()
                            (turn-on-auto-fill)))
  :bind ("M-Q" . prot/unfill-region-or-paragraph))

8.4 Recognise subwords

It is better you do C-h f subword-mode. Basically, this alters the way Emacs understands word boundaries. So, camelCaseWords are exposed as their constituents rather than one long word, meaning that motions will behave accordingly.

(use-package subword
  :diminish
  :hook (prog-mode-hook . subword-mode))

8.5 Comment lines, regions, boxes, etc.

Just some basic configurations for commenting structured text. This is mostly a placeholder for potentially more targeted and detailed settings that would involve per-mode hooks.

The purpose of my reviewed key bindings is to make them more consistent. Helps with mnemonics. They also are more ergonomic. To this end, I have the following:

  • The standard commenting function is now bound to the simple C-;. This runs a "do what I meant" function I have defined, whose detailed documentation can be read below.
  • C-: (C-S-;) will kill the comment on the current line. This is particularly helpful when the comment follows text you would like to keep. The operation can be performed regardless of where the point is on the line. Some modes disable this behaviour (e.g. trying it on source code inside of org-mode—for those cases, focus the block with C-c ').
  • The M-; will just append a comment to the line, rather than the default comment-dwim.

Note that C-; is occupied by some flyspell command that I have no use for (disabled in the relevant package declaration).

Lastly, use M-j (alias C-M-j) when you want to continue an existing comment on a new line with respect for the current indentation. If you are not inside of a comment, this will just create an indentation-aware new line.

(use-package newcomment
  :config
  (setq comment-empty-lines t)
  (setq comment-fill-column nil)
  (setq comment-multi-line t)
  (setq comment-style 'multi-line)

  (defun prot/comment-dwim (&optional arg)
    "Alternative to `comment-dwim': offers a simple wrapper
around `comment-line' and `comment-dwim'.

If the region is active, then toggle the comment status of the
region or, if the major mode defines as much, of all the lines
implied by the region boundaries.

Else toggle the comment status of the line at point."
    (interactive "*P")
    (if (use-region-p)
        (comment-dwim arg)
      (save-excursion
        (comment-line arg))))

  :bind (("C-;" . prot/comment-dwim)
         ("C-:" . comment-kill)
         ("M-;" . comment-indent)
         ("C-x C-;" . comment-box)))

8.6 Configure 'electric' behaviour

Emacs labels as "electric" any behaviour that involves contextual auto-insertion of characters. This is a summary of my settings:

  • Indent automatically.
  • If electric-pair-mode is enabled (which I might do manually), insert quotes and brackets in pairs. Only do so if there is no alphabetic character after the cursor.
  • The cryptic numbers in the pairs set, correspond to curly single and double quotes and these «». The contents of this set are always inserted in pairs, regardless of major mode.
    • To get those numbers, evaluate (string-to-char CHAR) where CHAR is the one you are interested in. For example, get the literal tab's character with (string-to-char "\t").
  • While inputting a pair, inserting the closing character will just skip over the existing one, rather than add a new one. So typing ( will insert () and then typing ) will just be the same as moving forward one character C-f.
  • Do not skip over whitespace when operating on pairs. Combined with the above point, this means that a new character will be inserted, rather than be skipped over. I find this better, because it prevents the point from jumping forward, plus it allows for more natural editing.
  • The whitespace characters are space (\s), tab (\t), and newline (\n).
  • The rest concern the conditions for transforming quotes into their curly equivalents. I keep this disabled, because curly quotes are distinct characters. It is difficult to search for them. Just note that on GNU/Linux you can type them directly by hitting the "compose" key and then an angled bracket (< or >) followed by a quote mark.
(use-package electric
  :config
  (setq electric-pair-inhibit-predicate'electric-pair-conservative-inhibit)
  (setq electric-pair-preserve-balance t)
  (setq electric-pair-pairs
        '((8216 . 8217)
          (8220 . 8221)
          (171 . 187)))
  (setq electric-pair-skip-self 'electric-pair-default-skip-self)
  (setq electric-pair-skip-whitespace nil)
  (setq electric-pair-skip-whitespace-chars
        '(9
          10
          32))
  (setq electric-quote-context-sensitive t)
  (setq electric-quote-paragraph t)
  (setq electric-quote-string nil)
  (setq electric-quote-replace-double t)
  :hook (after-init-hook . (lambda ()
                             (electric-indent-mode 1)
                             (electric-pair-mode -1)
                             (electric-quote-mode -1))))

8.7 Parentheses (show-paren-mode)

Configure the mode that highlights matching delimiters or parentheses. I consider this of utmost importance when working with languages such as elisp.

Summary of what these do:

  • Activate the mode upon startup.
  • Show the matching delimiter/parenthesis if on screen, else show nothing. It is possible to highlight the expression enclosed by the delimiters, by using either mixed or expression. The latter always highlights the entire balanced expression, while the former will only do so if the matching delimiter is off screen.
  • show-paren-when-point-in-periphery lets you highlight parentheses even if the point is in their vicinity. This means the beginning or end of the line, with space in between. I used that for a long while and it server me well. Now that I have a better understanding of Elisp, I disable it.
  • Do not highlight a match when the point is on the inside of the parenthesis.
(use-package paren
  :config
  (setq show-paren-style 'parenthesis)
  (setq show-paren-when-point-in-periphery nil)
  (setq show-paren-when-point-inside-paren nil)
  :hook (after-init-hook . show-paren-mode))

8.8 Tabs, indentation, and the TAB key

I believe tabs, in the sense of inserting the tab character, are best suited for indentation. While spaces are superior at precisely aligning text. However, I understand that elisp uses its own approach, which I do not want to interfere with. Also, Emacs tends to perform alignments by mixing tabs with spaces, which can actually lead to misalignments depending on certain variables such as the size of the tab. As such, I am disabling tabs by default.

If there ever is a need to use different settings in other modes, we can customise them via hooks. This is not an issue I have encountered yet and am therefore refraining from solving a problem that does not affect me.

Note that tab-always-indent will first do indentation and then try to complete whatever you have typed in. I control how completion works for that particular function in my in-buffer completions section.

(use-package emacs
  :config
  (setq-default tab-always-indent 'complete)
  (setq-default tab-width 4)
  (setq-default indent-tabs-mode nil))

8.9 Dictionary (and local sdcv variant)

Sometimes I need to access an offline version of Webster's Revised Unabridged Dictionary (1913) that I got following the helpful instructions in Marcin Borkowski's blog. Though I use the sdcv from the package archives instead of the one recommended by Marcin.

The hook I add here is to remove the rather awkward use of highlighting. Also because I do not think you need designerism to get in the way of a dictionary entry.

(use-package sdcv
  :ensure
  :hook (sdcv-mode-hook . (lambda ()
                            (font-lock-mode -1))))

When the above does not yield any helpful results, I switch to the dictionary package by Torsten Hilbrich. It works with an Internet connection and has access to a wide range of dictionaries.

(use-package dictionary
  :ensure)

8.10 Flyspell (spell check)

I need spell checking mostly for English, though I also install dictionaries for Greek, French, and Spanish. These are external to Emacs and are provided by the aspell library.

In previous versions of this section I had configurations that would automate spell checking. It worked but was rather slow. Upon further inspection, I realised that I seldom need to work in mixed language circumstances. Moreover, I now understand that I do not need to have spell checking always on. It can be activated manually, with the flyspell functions defined in the :commands segment below or with my prot/flyspell-dwim for the active region.

Also bear in mind that the key binding C-; that Flyspell uses by default is disabled because I re-purpose that for a faster version of C-x C-; (much more useful for my work—see the section on comments).

Note that my use-case for Flyspell is to mark a region and test for errors in it. If I need to review the entire buffer, especially if it is about some long-form text, I use a flymake interface instead (refer to the section on flymake-aspell).

(use-package flyspell
  :config
  (setq flyspell-issue-message-flag nil)
  (setq flyspell-issue-welcome-flag nil)
  (setq ispell-program-name "aspell")
  (setq ispell-dictionary "en_GB")

  (defvar prot/ispell-dictionaries
    '(("English" . "en")
      ("Ελληνικά" . "el")
      ("Français" . "fr")
      ("Espanõl" . "es"))
    "Alist of dictionaries I may use.
It is used by `prot/ispell-dictionaries-complete'.")

  (defun prot/ispell-dictionaries-complete ()
    "Select an item from `prot/ispell-dictionaries'."
    (interactive)
    (let* ((dicts (mapcar #'car prot/ispell-dictionaries))
           (choice (completing-read "Select dictionary: " dicts nil t))
           (key (cdr (assoc `,choice prot/ispell-dictionaries))))
      (ispell-change-dictionary key)
      (message "Switched to %s" key)))

  (defun prot/flyspell-dwim ()
    "Spell check region or select dictionary.

Use `flyspell-region' on the active region, else invoke
`prot/ispell-dictionaries-complete'."
    (interactive)
    (let ((beg (region-beginning))
          (end (region-end)))
      (if (use-region-p)
          (flyspell-region beg end)
        (prot/ispell-dictionaries-complete))))

  :bind (("M-$" . prot/flyspell-dwim)
         ("C-M-$" . prot/ispell-dictionaries-complete)
         :map flyspell-mode-map
         ("C-;" . nil)))

8.11 Code and text linters

8.11.1 Flymake

This is a built-in linter interface. It visualises in a buffer what you would otherwise get on the command-line prompt (or compilation log), while it also marks the line[s] where the note, warning, or error occurs. In short, it is quite a nice tool to have.

Several extensions to Flymake are already available, mostly targeted at programmers. For my case, there is no need for any of those, while Flymake can lint Elisp without any further configuration.

While it is possible to run the linter globally or through hooks, I prefer to only do so manually on a per-buffer basis. There are many cases where I am merely browsing code, where whatever diagnostics are of no import to the task at hand.

With regard to the defhydra calls in this section, remember to refer to my fundamentals for hydras.

(use-package flymake
  :commands flymake-mode
  :config
  (setq flymake-fringe-indicator-position 'left-fringe)
  (setq flymake-suppress-zero-counters t)
  (setq flymake-start-on-flymake-mode nil)
  (setq flymake-no-changes-timeout nil)
  (setq flymake-start-on-save-buffer t)
  (setq flymake-proc-compilation-prevents-syntax-check t)
  (setq flymake-wrap-around nil)

  (defhydra prot/hydra-flymake (:color pink :hint nil)
    "
Actions
-------
_s_: Start checks
_n_: Next error
_p_: Previous error
_d_: Diagnostics' buffer
"
    ("s" flymake-start)
    ("d" flymake-show-diagnostics-buffer)
    ("n" flymake-goto-next-error)
    ("p" flymake-goto-prev-error)
    ("q" nil "cancel" :color blue))
  :bind (:map flymake-mode-map
              ("C-c h l" . prot/hydra-flymake/body)))

(use-package flymake-diagnostic-at-point
  :ensure
  :after flymake
  :config
  (setq flymake-diagnostic-at-point-display-diagnostic-function
        'flymake-diagnostic-at-point-display-minibuffer))
8.11.1.1 Flymake + Proselint

Manuel Uberti has published flymake-proselint on Github and MELPA. It offers a Flymake interface to the external proselint executable (see Proselint configuration).

This comes in handy when I need to review some long-form text for common inconsistencies and stylistic irregularities. Errors will be marked on the fringe, while you can quickly get an overview with pointers to the precise line number by invoking flymake-show-diagnostics-buffer (also check my configurations for Flymake).

To run the program, you just need to hook it to whatever major-mode you use for prose. The following dolist handles this nicely. Then you need to enable flymake-mode to start using it. I prefer to do the final step manually, as I normally do not run a linter while writing: it is too distracting.

(use-package flymake-proselint
  :ensure
  :after flymake
  :init
  (dolist (mode '("markdown-mode" "org-mode" "text-mode"))
    (add-hook (intern (concat mode "-hook")) #'flymake-proselint-setup)))
8.11.1.1.1 Proselint configuration

This is my configuration for the external proselint executable (check that project's README). The following should be made available at ~/.config/proselint/config.

See Flymake + Proselint for how I use this tool to review my long-form writing.

{
    "max_errors": 200,
    "checks": {
        "airlinese.misc"                : false,
        "annotations.misc"              : true,
        "archaism.misc"                 : true,
        "cliches.hell"                  : true,
        "cliches.misc"                  : true,
        "consistency.spacing"           : true,
        "consistency.spelling"          : true,
        "corporate_speak.misc"          : false,
        "cursing.filth"                 : false,
        "cursing.nfl"                   : false,
        "cursing.nword"                 : false,
        "dates_times.am_pm"             : false,
        "dates_times.dates"             : false,
        "hedging.misc"                  : true,
        "hyperbole.misc"                : true,
        "jargon.misc"                   : true,
        "lexical_illusions.misc"        : true,
        "lgbtq.offensive_terms"         : true,
        "lgbtq.terms"                   : true,
        "links.broken"                  : false,
        "malapropisms.misc"             : true,
        "misc.apologizing"              : true,
        "misc.back_formations"          : true,
        "misc.bureaucratese"            : true,
        "misc.but"                      : true,
        "misc.capitalization"           : true,
        "misc.chatspeak"                : false,
        "misc.commercialese"            : true,
        "misc.composition"              : true,
        "misc.currency"                 : false,
        "misc.debased"                  : true,
        "misc.false_plurals"            : true,
        "misc.illogic"                  : true,
        "misc.inferior_superior"        : true,
        "misc.institution_name"	        : true,
        "misc.latin"                    : true,
        "misc.many_a"                   : false,
        "misc.metaconcepts"             : true,
        "misc.metadiscourse"            : true,
        "misc.narcissism"               : true,
        "misc.not_guilty"               : true,
        "misc.phrasal_adjectives"       : true,
        "misc.preferred_forms"          : true,
        "misc.pretension"               : true,
        "misc.professions"              : true,
        "misc.punctuation"              : true,
        "misc.scare_quotes"             : true,
        "misc.suddenly"                 : false,
        "misc.tense_present"            : true,
        "misc.waxed"                    : true,
        "misc.whence"                   : false,
        "mixed_metaphors.misc"          : true,
        "mondegreens.misc"              : true,
        "needless_variants.misc"        : true,
        "nonwords.misc"                 : true,
        "oxymorons.misc"                : true,
        "psychology.misc"               : true,
        "redundancy.misc"               : true,
        "redundancy.ras_syndrome"       : true,
        "skunked_terms.misc"            : true,
        "spelling.able_atable"          : true,
        "spelling.able_ible"            : true,
        "spelling.athletes"             : false,
        "spelling.em_im_en_in"          : true,
        "spelling.er_or"                : true,
        "spelling.in_un"                : true,
        "spelling.misc"                 : true,
        "security.credit_card"          : false,
        "security.password"             : false,
        "sexism.misc"                   : true,
        "terms.animal_adjectives"       : true,
        "terms.denizen_labels"          : true,
        "terms.eponymous_adjectives"    : true,
        "terms.venery"                  : true,
        "typography.diacritical_marks"  : false,
        "typography.exclamation"        : true,
        "typography.symbols"            : true,
        "uncomparables.misc"            : true,
        "weasel_words.misc"             : true,
        "weasel_words.very"             : false
    }
}
8.11.1.2 Flymake + Aspell

I normally review a region's spelling with Flyspell, using my own extensions for it (see Flyspell (spell check)). I do, however, prefer to have an overview of all spelling warnings/errors as a final test before publishing a long-form text (e.g. an article on my website).

This is where flymake-aspell by Leo Gaskin comes in handy. It uses the familiar Flymake interface (see Flymake configuration) to highlight all spelling issues. Simple and effective!

Note that flyspell is not a dependency, but I still list it in the :after directive because that is where the ispell dictionary is specified (as it is what I normally use). This would normally be a bad practice, though I do not run into problems since flyspell is always invoked before I ever have any need for flymake-aspell, so the dependency is satisfied.

For a related package, consult my configurations for flymake-proselint.

(use-package flymake-aspell
  :after (flyspell flymake)
  :ensure
  :init
  (dolist (mode '("markdown-mode" "org-mode" "text-mode"))
    (add-hook (intern (concat mode "-hook")) #'flymake-aspell-setup)))

8.11.2 Elisp packaging requirements

With this in place we can perform checks that pertain to Emacs lisp packaging. I use it for my themes but also for any elisp library I may want to send patches to.

(use-package package-lint-flymake
  :ensure
  :after flymake
  :hook (emacs-lisp-mode-hook . package-lint-flymake-setup))

8.12 Eldoc (elisp live documentation feedback)

When editing elisp, this mode will display useful information about the construct at point in the echo area. For functions it will display the list of arguments they accept. While it will show the the first sentence of a variable's documentation string.

At first, I dismissed this package. Upon closer inspection, it does offer a lightweight complementary facility to that of the standard help commands: C-h f FUNCTION, C-h v VARIABLE.

(use-package eldoc
  :diminish
  :config
  (global-eldoc-mode 1))

9 History and state

This section contains configurations for packages that are dedicated to the task of recording the state of various Emacs tools, such as the history of the minibuffer or the list of recently visited files.

In practice, these are some of the most useful configurations one can make, as lots of functions depend on them. For example, a record of the minibuffer's history of inputs allows the completion framework to guess the most likely course of action. Typing M-x g gives me gnus as the first possible option, which is exactly what I want.

9.1 Emacs server and desktop

The following uses the first running process of Emacs as the one others may connect to. This means that calling emacsclient (with or without --create-frame), will share the same buffer list and data as the original running process, aka "the server". The server persists for as long as there is an Emacs frame attached to it.

(use-package server
  :hook (after-init-hook . server-start))

With some exceptions aside, I only ever use Emacs in a single frame. What I find more useful is the ability to save the state I was in: the name the of buffers, the cursor's position in each of them, the recent file list, the minibuffer history, my stored registers.

The state of the available buffers and the values of each register are called the "desktop" (for the other items see the following sections on recording various types of history).

Preserving the "desktop" saves me from any possible crash or when I need to close Emacs and re-launch it later (my hardware is limited, so I do not keep it running while I am away).

Overview of my settings:

  • Enable the mode that saves the "desktop", instructing it to load a small number of buffers at launch (desktop-restore-eager). The remainder of the buffer list will be loaded lazily.
  • Now we must tell it where to store the files it generates and how often it should save. Concerning the latter, the default is to store the state every time it changes. I find that a bit too much, so I set a timeout of five minutes of idleness.
  • Note the desktop-load-locked-desktop. By default, Emacs locks the desktop file while it runs. The lock is removed upon exiting. This is a safety mechanism. There are two cases where the lock can create issues:
    • Emacs has crashed, meaning that it exited abruptly and was not able to unlock the desktop. Upon re-launch Emacs will prompt you whether to load the locked file. You normally want to answer affirmatively.
    • Emacs runs in daemon mode, where it does not ask questions upon loading. In this case the lock is ignored.
    • Because I am only affected by the former, I choose to disable the prompt and just load the thing directly. Otherwise, I would set it to nil.
  • Do not restore frame configurations. If I need to store one of those, I use registers, specifically C-x r f (see section on Registers).
  • Ask what to do in case the session has a newer file that the one it initially started out with (e.g. when a new frame runs in parallel to the older one).
(use-package desktop
  :config
  (setq desktop-auto-save-timeout 300)
  (setq desktop-dirname "~/.emacs.d/")
  (setq desktop-base-file-name "desktop")
  (setq desktop-files-not-to-save nil)
  (setq desktop-globals-to-clear nil)
  (setq desktop-load-locked-desktop t)
  (setq desktop-missing-file-warning nil)
  (setq desktop-restore-eager 0)
  (setq desktop-restore-frames nil)
  (setq desktop-save 'ask-if-new)
  (desktop-save-mode 1))

And here we make sure that Emacs starts in the *scratch* buffer, no matter what.

(use-package emacs
  :init
  (setq initial-buffer-choice t)
  (setq inhibit-startup-echo-area-message "prot") ; read the docstring
  (setq inhibit-startup-screen t)
  (setq inhibit-startup-buffer-menu t))

9.2 Record various types of history

9.2.1 Recentf (recent files and directories)

This is a built-in minor mode that keeps track of the files you have opened, allowing you revisit them faster. Its true power consists in the fact that its data, maintained in recentf-list, is a simple variable. This means that we can access it through any relevant piece of Elisp functionality.

To that end, the functions I define herein are meant to either control the contents of the list or allow me to access them through my completion framework or a dedicated file listing (see, in particular, Tools for completion candidate listing and configurations for Dired).

Note that there exists a built-in recentf-open-files function for accessing the recent files through a bespoke buffer. I find that I have no use for it.

(use-package recentf
  :config
  (setq recentf-save-file "~/.emacs.d/recentf")
  (setq recentf-max-saved-items 200)
  (setq recentf-exclude '(".gz" ".xz" ".zip" "/elpa/" "/ssh:" "/sudo:"))

  (defun prot/recentf-keep-predicate (file)
    "Additional conditions for saving in `recentf-list'.
Add this function to `recentf-keep'.

NEEDS REVIEW."
    (cond
     ((file-directory-p file) (file-readable-p file))))
  (add-to-list 'recentf-keep 'prot/recentf-keep-default-predicate)

  (defun prot/recentf (&optional input)
    "Select item from `recentf-list' using completion.
Use INPUT as an initial, yet editable, filter.

The user's $HOME directory is abbreviated as a tilde."
    (interactive)
    (let* ((files (mapcar 'abbreviate-file-name recentf-list))
           (f (icomplete-vertical-do ()
                (completing-read "Open recentf entry: " files nil t
                                 (when input input)))))
      (find-file f)))

  (defun prot/recentf-dirs (&optional arg)
    "Select directory from `recentf-list' using completion.
With \\[universal-argument] present the list in a `dired' buffer.
This buffer is meant to be reused by subsequent invocations of
this command (otherwise you need to remove the `when' expression.

Without \\[universal-argument], the user's $HOME directory is
abbreviated as a tilde.  In the Dired buffer paths are absolute."
    (interactive "P")
    (let* ((list (mapcar 'abbreviate-file-name recentf-list))
           (dirs (delete-dups
                  (mapcar (lambda (file)
                            (if (file-directory-p file)
                                (directory-file-name file)
                              (substring (file-name-directory file) 0 -1)))
                          list)))
           (buf "*Recentf Dired*")
           (default-directory "~"))
      (when (get-buffer buf)
        (kill-buffer buf))
      (if arg
          (dired (cons (generate-new-buffer-name buf) dirs))
        (icomplete-vertical-do ()
          (find-file
           (completing-read "Recent dirs: " dirs nil t))))))

  :hook (after-init-hook . recentf-mode)
  :bind (("s-r" . prot/recentf)
         ("C-x C-r" . prot/recentf-dirs)))

9.2.2 Minibuffer history (savehist-mode)

Keeps a record of actions involving the minibuffer. This is of paramount importance to a fast and efficient workflow involving any completion framework that leverages the built-in mechanisms.

Emacs will remember your input and choices and will surface the desired results towards the top as the most likely candidates. Make sure to also check the section on Tools for completion candidate listing.

(use-package savehist
  :config
  (setq savehist-file "~/.emacs.d/savehist")
  (setq history-length 1000)
  (setq history-delete-duplicates t)
  (setq savehist-save-minibuffer-history t)
  :hook (after-init-hook . savehist-mode))

9.2.3 Record cursor position

Just remember where the point is in any given file. This can often be a subtle reminder of what you were doing the last time you visited that file, allowing you to pick up from there.

(use-package saveplace
  :config
  (setq save-place-file "~/.emacs.d/saveplace")
  (setq save-place-forget-unreadable-files t)
  (save-place-mode 1))

9.2.4 Backups

And here are some settings pertaining to backups. I rarely need those, but I prefer to be safe in the knowledge that if something goes awry there is something to fall back to.

(use-package emacs
  :config
  (setq backup-directory-alist
        '(("." . "~/.emacs.d/backup/")))
  (setq backup-by-copying t)
  (setq version-control t)
  (setq delete-old-versions t)
  (setq kept-new-versions 6)
  (setq kept-old-versions 2)
  (setq create-lockfiles nil))

10 Frequently Asked Questions about this document

There are some persistent questions that pop up in my email exchange, so I thought I would cover them all in this section.

10.1 How do you learn Emacs?

There is no one-size-fits-all approach to learning. What one finds satisfactory, another may consider insufficient. In my opinion, the best way to learn Emacs is to start small, be patient, and brace yourself for a lot of reading and experimentation.

The best skill you can master, the one that will always help you in your Emacs journey, is the built-in help system. Learn to ask Emacs about things you do not know about. This section documents the essentials of Emacs' introspectable nature.

Know that C-h is the universal key for help commands (broadly understood). It works both as a prefix and as a suffix. Some common help commands:

  • C-h f allows you to search for documentation about functions.
  • C-h v is the same for variables.
  • C-h o is a wrapper of the above two, so you are searching for functions or variables. The proper name for any of these items is called a "symbol" (think of name-of-thing as symbolising a definition in the source code).
  • C-h k will prompt you for a key binding. Once you type it in you will get help about what command corresponds to that key (if any). Note that this depends on the major mode you are in. For example C-c C-c does something different in Org and Eshell buffers. Try C-h k C-c C-c to find about the different functions these will invoke in their respective major mode.

In the above scenaria we see how C-h is used as a prefix. You are starting a key sequence with it. So here are some cases of using it as a suffix:

  • For every incomplete key sequence if you follow it up with C-h you will get a help buffer about all possible key combination that complete that sequence. For example, if you type C-c C-h inside of an Org buffer you will get all possible commands for Org mode and for all other minor modes you have active.
  • The C-h suffix can be appended to longer key sequences. Indeed the length is irrelevant. Suppose you want to learn more about some of the advanced features of registers. C-x r is the common prefix for those commands, so you just do C-x r C-h and you get a buffer with more information.
  • And, as you may imagine, you can even append the C-h suffix to the C-h prefix. This is a fancy way of saying that C-h C-h will show you help about help commands themselves. But because this is a special case, it comes with some extras. Try it!

All help buffers include links to other parts of Emacs, from where you can learn ever more information. For example, the help for C-c C-h includes links to the commands that correspond to each key chord. Follow the link to get documentation about that symbol.

More generally, you will find that a symbol is linked to its source. Look carefully at the top of the buffer that display symbol documentation and you will find a link to the source code (library) from where the function/variable (symbol) comes from.

Also know that the source code can be accessed at any time by means of M-x find-library.

Help commands that ask you for a symbol to input can also be aware of the context of the point (the cursor). So if you are over the name of a function and you type C-h f, that function will be the default match. Hitting RET (Enter) will take you to its documentation. This is a great way to study source code, because it will guide you to other libraries or other parts of the same library from where you can understand how things are pieced together. And it also works with the find-library command.

Finally, you should practice C-h m as much as possible. This is the help command for getting information about the major mode you are in and all the minor modes you have active. It will show you some valuable documentation as well as the main key bindings and their commands. Try it whenever you use something you have not mastered yet. For example, do it in a dired buffer to see the main operations you can perform inside of it.

10.2 What is the purpose of "prot/" and "contrib/"?

The prot/ prefix in symbols works as a namespace that captures all my additions. These can be functions or variables. Its utility or purpose is two-fold:

  1. It informs others that this symbol is not part of core Emacs or some other package. Otherwise it can be difficult to understand why something you copied did not work. Say, for example, I have a function that accepts an argument: (prot/function prot/variable). If none of these had the namespace you could be misled into thinking that your Emacs setup already includes those symbols (and then you will get an error message).
  2. It makes it easier for me (and others) to quickly discover what additions I have made, for whatever reason that may. For instance, M-x prot/ will give me matches for all my interactive functions (depending on the completion framework, one can access those with just M-x p/ as in my Tools for completion candidate listing). This also means that I can do M-s o prot/ (or M-x occur prot/) to produce an Occur buffer with all my symbols (pass a number argument for displaying N lines of context C-u 5 M-s o). From there I can, say, browse them all easily or even edit them using all of Emacs' relevant powers (Occur is made editable with e, but you should be able to find that by using the information in How do you learn Emacs?).

The same principles apply for the contrib/ namespace. Whenever I copy something from another user, I use that namespace to tell others that this is not part of Emacs or any other package (and I always link to the source).

Adding contrib/ also has another longer-term benefit. It tells my future self that some bespoke configuration was needed to solve a particular problem and, maybe, this has since been solved by a good package or even a newer version of Emacs. A case in point is the hook I use for my themes: contrib/after-load-theme-hook. As of 2020-05-03 there is no built-in way of accomplishing that bit of functionality (as far as I know), but there may be one at some point and it might be an improvement overall.

Again, occur or similar tools will filter these out when necessary. Imagine having to do that without any namespaces… You would need to check each symbol one by one to determine its origin.

The convention of separating namespaces with a forward slash is not particularly important, though the linter for Elisp packaging will complain about it, if you ever go down that path. It could be something like prot-- or my- or whatever. What matters is to keep things consistent and fairly easy to identify.

10.3 Why not `:custom' and why do you use so many `setq'?

I normally do not use the :custom keyword that use-package provides, opting instead for :config or :init. This is for a couple of closely related reasons:

  • It makes each variable more difficult to copy elsewhere, say, when sharing it online or to an emacs -Q scratch buffer. You either need to edit the balanced expression, or hope that users will know the syntax that the :custom keyword expects.
  • It is impossible to run C-x C-e (eval-last-sexp) on each balanced expression because it lacks the setq special form. Again, this introduces an impediment to quick copy operations.

Now you may be wondering why I define each variable in its own balanced expression. To be clear, these are equivalent:

;; Style A
(setq var-1 'symbol)
(setq var-2 '(a b c))
(setq var-3 '((a . b) (c . d)))

;; Style B
(setq var-1 'symbol
      var-2 '(a b c)
      var-3 '((a . b) (c . d)))

You will notice that most of my configurations follow Style A. The reasons are the same as above: (i) copying, (ii) evaluating.

Style B used to give me more problems with copy-pasting, while it did not solve any real issue (besides, I finalise style A using a keyboard macro, so there is no real difference in typing).

I find that Style A consists of balanced expressions that are easier to keep consistent. This is especially true when you have a mixture of values: boolean types, property lists, association lists…

Of course, experience is key to understanding that all these styles are valid.

10.4 Why do you explicitly set variables the same as default?

You may have realised that many of my configurations will use a setq that declares a value that is the same as its original in the source code. I do this for two reasons:

  1. To raise awareness of its existence. If someone does not like how the defaults work, they know where to look.
  2. I guard against future versions that could be changing the defaults.

Obviously point 2 is not particularly strong for built-in libraries that are already very stable, though I find that, on balance, nothing bad comes out of it.

At any rate, one must always read the NEWS (C-h n for view-emacs-news) whenever they upgrade to a new version of Emacs. Though there is no equivalent mechanism for individual packages… So here we are.

10.5 What is your distro and/or window manager?

I have been on GNU/Linux since the summer of 2016. For the most time I have used Debian and Arch Linux, switching between the two. As of 2020-05-03, I am back on Arch. My criterion for picking a distro is that it is community-driven and has a strong following that ensures its longer-term continuity and overall stability. This happens to be consistent with my current focus on Emacs: I need things to work so that I may not be bothered by too much admin work (and yes, Arch is super-stable once you know what you are doing—and, well, Debian is designed for that).

Given that I mostly live inside of Emacs, the desktop environment is no longer important to me, provided it does not impede my usage of Emacs, which practically means that it does not bind any keys system-wide (with the exception of some standard ones like those for accessing TTYs).

Prior to my Emacs days, I used to have a highly customised session centred around BSPWM (the Binary Space Partitioning Window Manager), while I also spent time with i3WM, DWM, Herbestluftwm. The tiling window manager paradigm offers little to no value now that practically my entire computing experience happens inside of a single application.

My Emacs is built from source, using the master branch. Check the AUR if you are on Arch or derivatives, or try my PKGBUILD which I documented in the section about Arch Linux package recipes—remember to always read the shell scripts before you download random stuff.

10.6 Did you know XYZ package is better than the defaults?

As a rule of thumb, I choose external packages only after I give a fair chance to the defaults. The idea is to take things slowly and learn along the way, while consulting the official manual and relevant documentation (I strongly encourage you to master the information I provide in How do you learn Emacs?).

Without exposure to the built-in tools it is impossible to make an informed decision about what is actually missing and what could be improved further. Additionally, it is difficult to appreciate the underlying rationale that led to a given element of design without actually trying to put yourself in that mindset or workflow.

Put differently, keep an open mind about the alternatives before deciding to reproduce the thing you had before, else you are assuming your prior knowledge to be true in advance of any possible evidence to the contrary (a sign of dogmatism).

The process of learning the internals of Emacs means that I write my own Elisp functions when I feel that a standard tool could be tweaked on the margin of its core utility in order to do what I want (see my answer to What is the purpose of "prot/" and "contrib/"?). External packages that I do use are either a clear upgrade over the defaults or otherwise extend the functionality of what is already available.

You will not find any superficialities herein: no rainbow-coloured mode lines, no icons, no tool bars with bells and spinning wheels, nothing. I respect the fact that they exist, but find that they do not contribute to my productivity.

Though a former Vim user for ~3 years, I decided not to use evil-mode or any kind of Vi emulation (remember the point about keeping an open mind?). I wanted to do things differently in order to ultimately set on the best approach going forward. I have eventually settled on a system that builds on top of the "Emacs way" to key bindings, which I discuss in my hour-long presentation about Switching to Emacs (2019-12-20). I believe that a mnemonics-based set of keys is easier to get started with. It expands organically as you familiarise yourself with multitude of Emacs' functionalities: there is an entire world of applications outside the narrow confines of editing code.

10.7 What is the meaning of the `s-KEY' bindings?

Some sections of this document assign functions to key combinations that involve the "Super" key (also known as the "Windows key" on some commercial keyboards). This is represented as a lower case s.

In most cases, those key bindings are alternative ways of invoking common commands that are by default bound to longer key chords. The original keys will continue to function as intended (for example, C-x o is also s-o). Otherwise they bind my own custom functions.

To find all my keybindings of this sort in the source code version of this document from inside of Emacs, do M-s o (or M-x occur) followed by the pattern "[a-zA-Z<]?-?s-.+?" (please contact me if you know how to improve this).

Note that your choice of desktop environment (or window manager) might render some of these useless. The DE/WM will simply intercept the key chord before it is ever sent to Emacs. For example, GNOME has a hidden key mapping to s-p, which does something with monitors (last time I checked on GNOME 3.30). Such bindings are scattered throughout the config database that is normally accessed with gsettings on the command line or the graphical dconf-editor (not pretty either way).

10.8 What hardware do you use?

I am using a Lenovo Thinkpad X220 that I got in 2018 for ~80 EUR. This is the first computer I ever bought: before that I had a Macbook that was offered to me as part of an office job—but do not ask me about it because at the time I only knew how to copy/paste using right click and that sort of thing, while I only ever bothered with the hardware side of things once I got better at using the computer (my switch to GNU/Linux was about freedom and consumer sovereignty, i.e. politics, not tech-only considerations).

The laptop is mostly deployed as part of a desktop setup, attached to an external monitor, mouse, and keyboard. The monitor is 1080p and I got it free of charge from a clearance. As for the mouse and keyboard, I bought those from a toy store for 7 EUR combined. The keyboard's layout is US Qwerty.

For my videos I use the built-in camera and microphone (sorry if the production quality is sub-standard!). Since we are here: the recording software is OBS Studio, while I do no video editing.

10.9 What about the completion UI? (typology of my completion interfaces)

In Completion framework and extras I define lots of completion-related functions and configure their accoutrements to deliver on a consistent experience (some completion-related functions are defined in a few other places as well, always with the appropriate references, though they extend the principles documented herein).

This is how I map completion-related workflows as part of my general Emacs usage:

One-off commands

These are the regular means one would use to transition from state A to state B, such as to switch buffers or call an interactive function like M-x eval-buffer. The plan is to get on with the task with as little friction as possible. There is no real need for extended interactions with the minibuffer. Things should happen quickly and seamlessly. Preferably with few or no extra visual effects and with as little fanfare as possible.

This explains why I use the standard horizontal layout of Icomplete by default. It does not interfere with my windows because that kind of feedback is not needed for the given interaction.

However, the horizontal view fails when the list consists of naturally long candidates, such as those returned by imenu (see configuration for Imenu (dynamic completion-based buffer navigation)). In those cases we still want a transient interface, but must rely on verticality to make sense of the nuances between the completion candidates.

With icomplete-vertical we can cope with that requirement through a simple toggle that is available on demand. Though we can also enable it presciently on a per-function basis, which is what I normally do for a whole range of commands (search for the icomplete-vertical-do macro call, with common cases other than Imenu being to select an entry from the kill-ring or choose a filesystem path among the items in the recentf-list—also see section on recentf as well as all the commands referenced in completion for projects and directory trees).

Involved interactions

These are scenaria where one expects to interact with the list of candidates itself. There likely is a need to have a view of all the matches as well as a means of running additional commands that are specific to the task and contribute towards a more complex workflow.

In this light, the standard "Completions" buffer is essential. It feels natural to have a sense of all the available matches for those cases where you need to search, say, for some documentation and are probably interested in reading more than one entry (e.g. when you want to configure a new package), as well as potentially perform some other action as part of a longer process of interaction (with or without recursive minibuffers).

In more concrete terms: search for help with C-h f, C-h v, C-h o. The Completions buffer will show you what matches your input (default key is ? from inside the minibuffer: it calls minibuffer-completion-help). Switch to that buffer (e.g. prot/focus-minibuffer-or-completions), place point over an item you are interested in and run a bespoke command for describe-symbol for the thing at point (this is, for instance, what my prot/describe-symbol-at-point does, and I bind it to convenient keys). The help buffer will pop up, allowing you to, say, switch to it in order to copy something, then go to a file that is being edited in the other window to iterate on it, and then get back to the Completions buffer for more of the same. This is also why recursive minibuffers are valuable, as they allow you to keep the current session active while still accessing things like M-x.

As for the "Completions" buffer itself, I must stress that this is a buffer. It allows you to, for example, write its contents to a regular file (C-x C-w). And with dedicated key maps such as the built-in completion-list-mode-map, you can always define context-specific bindings for your bespoke commands (e.g. I use h for help, w for writing to the kill-ring). Plus you can always control the placement of a buffer's window with display-buffer-alist (see the section on Window rules and basic tweaks).

Finally, I also have commands that connect the completion-specific interface to more general tools, like ibuffer and dired. So when some involved type of interaction demands extra attention, I can always leverage the full power of the wider Emacs ecosystem.