GNU Emacs integrated computing environment

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

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 prot-emacs.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, say, 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. Main macros and other contents of my init.el (for Emacs 28)

The prot-emacs.org is loaded from an other file, named init.el per the Emacs conventions. Mine includes some helper macros for package configuration and is otherwise designed to initialise the package lists and load the file with my configurations (i.e. the present document).

Those macros are integral parts of my setup, as they control the configuration of all packages declared herein. In particular:

  • prot-emacs-builtin-package is used for libraries that are either shipped with Emacs or are part of my dotfiles' directory. The latter class consists of all those prot-*.el files, as well as a few others. What this macro does is to require the given feature and then evaluate all of its forms (variables, key bindings, hooks, etc.).
  • prot-emacs-elpa-package controls packages that I install from some Emacs Lisp Package Archive, like MELPA or GNU ELPA. This macro will load the package if it is already installed and then evaluate all of its forms. If the package is not installed, it will produce a warning telling the user that all the uninstalled-yet-declared packages can be downloaded in one go with the command prot-emacs-install-ensured (though read further below about auto-installing packages).
  • prot-emacs-manual-package handles the few packages that I install manually via their Git repository. Each of those repos must be inside (locate-user-emacs-file "contrib-lisp") (typically available at ~/.emacs.d/contrib-lisp). The macro will load the package normally and configure it accordingly if it exists at the desired path, else it will log a warning about what file path it expects to read. In concrete terms, if you want package-A you must first place all of its files at ~/.emacs.d/contrib-lisp/package-A.

I must stress that no package is automatically installed by default: the user is expected to do so on their own either by calling a command or by providing their explicit consent to the auto-installation of packages from Emacs Lisp Package Archives. The idea is to avoid the malpractice of installing software without asking the user to opt in to such a deal. To actually instruct my declared packages to be installed automatically, a user must create a new file called basic-init.el, place it in the same directory as my init.el and prot-emacs.org and include in it this form: (setq prot-emacs-autoinstall-elpa t).

For more read: How to reproduce your dotemacs?.

The init.el (reproduced further below) also sets some variables to their desired values and provides a couple of functions that control the start and end phases of my Emacs sessions.

  • prot-emacs-build-config is the final function from my part that runs before terminating the running Emacs process. It regenerates my configurations and byte compiles the output. This speeds things up the next time I launch Emacs, while it also ensures that I am always running the latest version of my setup.
  • prot-emacs-load-config will either load the output of the aforementioned function or, if that is not available, parse the literate program that holds my code (this Org file if you are viewing the source code or the document that produces the HTML of this web page). Either way, it load my configurations.
;;; init.el --- Personal configuration file -*- lexical-binding: t -*-

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

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:

;; This file sets up the essentials for incorporating my init org
;; file.  This is known as "literate programming", which I think is
;; particularly helpful for sharing Emacs configurations with a wider
;; audience that includes new or potential users (I am still very new
;; myself).
;;
;; See my dotfiles: https://gitlab.com/protesilaos/dotfiles

;;; Code:

(require 'package)

(add-to-list 'package-archives
             '("elpa-devel" . "https://elpa.gnu.org/devel/"))

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

(defvar prot-emacs-autoinstall-elpa nil
  "Whether `prot-emacs-elpa-package' should install packages.
The default nil value means never to automatically install
packages.  A non-nil value is always interpreted as consent for
auto-installing everything---this process does not cover manually
maintained git repos, controlled by `prot-emacs-manual-package'.")

(defvar prot-emacs-basic-init "basic-init.el"
  "Name of 'basic init' file.

This file is meant to store user configurations that are evaluated
before loading `prot-emacs-configuration-main-file' and, when
available, `prot-emacs-configuration-user-file'.  Those values
control the behaviour of the Emacs setup.

The only variable that is currently expected to be in the 'basic
init' file is `prot-emacs-autoinstall-elpa'.

See `prot-emacs-basic-init-setup' for the actual initialisation
process.")

(defun prot-emacs-basic-init-setup ()
  "Load 'basic-init.el' if it exists.
This is meant to evaluate forms that control the rest of my Emacs
setup."
  (let* ((init prot-emacs-basic-init)
         (file (locate-user-emacs-file init)))
    (when (file-exists-p file)
      (load-file file))))

;; This variable is incremented in prot-emacs.org.  The idea is to
;; produce a list of packages that we want to install on demand from an
;; ELPA, when `prot-emacs-autoinstall-elpa' is set to nil (the default).
;;
;; So someone who tries to reproduce my Emacs setup will first get a
;; bunch of warnings about unavailable packages, though not
;; show-stopping errors, and will then have to use the command
;; `prot-emacs-install-ensured'.  After that command does its job, a
;; re-run of my Emacs configurations will yield the expected results.
;;
;; The assumption is that such a user will want to inspect the elements
;; of `prot-emacs-ensure-install', remove from the setup whatever code
;; block they do not want, and then call the aforementioned command.
;;
;; I do not want to maintain a setup that auto-installs everything on
;; first boot without requiring explicit consent.  I think that is a bad
;; practice because it teaches the user to simply put their faith in the
;; provider.
(defvar prot-emacs-ensure-install nil
  "List of package names used by `prot-emacs-install-ensured'.")

(defun prot-emacs-install-ensured ()
  "Install all `prot-emacs-ensure-install' packages, if needed.
If a package is already installed, no further action is performed
on it."
  (interactive)
  (when (yes-or-no-p (format "Try to install %d packages?"
                             (length prot-emacs-ensure-install)))
    (package-refresh-contents)
    (mapc (lambda (package)
            (unless (package-installed-p package)
              (package-install package)))
          prot-emacs-ensure-install)))

(defmacro prot-emacs-builtin-package (package &rest body)
  "Set up builtin PACKAGE with rest BODY.
PACKAGE is a quoted symbol, while BODY consists of balanced
expressions."
  (declare (indent 1))
  `(progn
     (unless (require ,package nil 'noerror)
       (display-warning 'prot-emacs (format "Loading `%s' failed" ,package) :warning))
     ,@body))

(defmacro prot-emacs-elpa-package (package &rest body)
  "Set up PACKAGE from an Elisp archive with rest BODY.
PACKAGE is a quoted symbol, while BODY consists of balanced
expressions.

When `prot-emacs-autoinstall-elpa' is non-nil try to install the
package if it is missing."
  (declare (indent 1))
  `(progn
     (when (and prot-emacs-autoinstall-elpa
                (not (package-installed-p ,package)))
       (package-install ,package))
     (if (require ,package nil 'noerror)
         (progn ,@body)
       (display-warning 'prot-emacs (format "Loading `%s' failed" ,package) :warning)
       (add-to-list 'prot-emacs-ensure-install ,package)
       (display-warning
        'prot-emacs
        (format "Run `prot-emacs-install-ensured' to install all packages in `prot-emacs-ensure-install'")
        :warning))))

(defmacro prot-emacs-manual-package (package &rest body)
  "Set up manually installed PACKAGE with rest BODY.
PACKAGE is a quoted symbol, while BODY consists of balanced
expressions."
  (declare (indent 1))
  (let ((path (thread-last user-emacs-directory
                (expand-file-name "contrib-lisp")
                (expand-file-name (symbol-name (eval package))))))
    `(progn
       (eval-and-compile
         (add-to-list 'load-path ,path))
       (if (require ,package nil 'noerror)
	       (progn ,@body)
         (display-warning 'prot-emacs (format "Loading `%s' failed" ,package) :warning)
         (display-warning 'prot-emacs (format "This must be available at %s" ,path) :warning)))))

(require 'vc)
(setq vc-follow-symlinks t) ; Because my dotfiles are managed that way

;; "prot-lisp" is for all my custom libraries; "contrib-lisp" is for
;; third-party code that I handle manually; while "modus-themes"
;; contains my themes which I use directly from source for development
;; purposes.
(dolist (path '("prot-lisp" "contrib-lisp" "modus-themes"))
  (add-to-list 'load-path (locate-user-emacs-file path)))

;; Some basic settings
(setq frame-title-format '("%b"))
(setq default-input-method "greek")
(setq ring-bell-function 'ignore)

(setq use-short-answers t)    ; for Emacs28, replaces the defalias below
;; (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)

(setq initial-buffer-choice t)			; always start with *scratch*

;; I create an "el" version of my Org configuration file as a final step
;; before closing down Emacs (see further below).  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...

(defvar prot-emacs-configuration-main-file "prot-emacs"
  "Base name of the main configuration file.")

;; THIS IS EXPERIMENTAL.  Basically I want to test how we can let users
;; include their own customisations in addition to my own.  Those will
;; be stored in a separate Org file.
(defvar prot-emacs-configuration-user-file "user-emacs"
  "Base name of user-specific configuration file.")

(defun prot-emacs--expand-file-name (file extension)
  "Return canonical path to FILE to Emacs config with EXTENSION."
  (locate-user-emacs-file
   (concat file extension)))

(defun prot-emacs-load-config ()
  "Load main Emacs configurations, either '.el' or '.org' file."
  (let* ((main-init prot-emacs-configuration-main-file)
         (main-init-el (prot-emacs--expand-file-name main-init ".el"))
         (main-init-org (prot-emacs--expand-file-name main-init ".org"))
         (user-init prot-emacs-configuration-user-file)
         (user-init-el (prot-emacs--expand-file-name user-init ".el"))
         (user-init-org (prot-emacs--expand-file-name user-init ".org")))
    (prot-emacs-basic-init-setup)
    (require 'org)
    (if (file-exists-p main-init-el)    ; FIXME 2021-02-16: this should be improved
        (load-file main-init-el)
      (when (file-exists-p main-init-org)
        (org-babel-load-file main-init-org)))
    (if (file-exists-p user-init-el)
        (load-file user-init-el)
      (when (file-exists-p user-init-org)
        (org-babel-load-file user-init-org)))))

;; Load configurations.
(prot-emacs-load-config)

;; The following as for when we close the Emacs session.
(declare-function org-babel-tangle-file "ob-tangle")

(defun prot-emacs-build-config ()
  "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."
  (interactive)
  (let* ((main-init prot-emacs-configuration-main-file)
         (main-init-el (prot-emacs--expand-file-name main-init ".el"))
         (main-init-org (prot-emacs--expand-file-name main-init ".org"))
         (user-init prot-emacs-configuration-user-file)
         (user-init-el (prot-emacs--expand-file-name user-init ".el"))
         (user-init-org (prot-emacs--expand-file-name user-init ".org")))
    (when (file-exists-p main-init-el)
      (delete-file main-init-el))
    (when (file-exists-p user-init-el)
      (delete-file user-init-el))
    (require 'org)
    (when (file-exists-p main-init-org)
      (org-babel-tangle-file main-init-org main-init-el)
      (byte-compile-file main-init-el))
    (when (file-exists-p user-init-org)
      (org-babel-tangle-file user-init-org user-init-el)
      (byte-compile-file user-init-el))))

(add-hook 'kill-emacs-hook #'prot-emacs-build-config)

;;; init.el ends here
1.2.1.1. The "early init"

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

;;; early-init.el --- Early Init File -*- lexical-binding: t -*-

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

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:

;; Prior to Emacs 27, the `init.el' was supposed to handle the
;; initialisation of the package manager, by means of calling
;; `package-initialize'.  Starting with Emacs 27, the default
;; behaviour is to start the package manager before loading the init
;; file.
;;
;; See my dotfiles: https://gitlab.com/protesilaos/dotfiles

;;; Code:

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

(defvar package-quickstart)

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

;; Disable GUI elements
(menu-bar-mode -1)
(tool-bar-mode -1)
(scroll-bar-mode -1)
(setq inhibit-splash-screen t)
(setq use-dialog-box t)                 ; only for mouse events
(setq use-file-dialog nil)

(setq inhibit-startup-echo-area-message "prot") ; read the docstring
(setq inhibit-startup-screen t)
(setq inhibit-startup-buffer-menu t)

(setq native-comp-async-report-warnings-errors 'silent) ; emacs28 with native compilation

;;; early-init.el ends here

1.2.2. 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 facilities (also read my note on How do you learn Emacs?).

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 configurations for Org mode. 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.

Consult the section on Org-mode (personal information manager).

1.3. COPYING

Copyright (c) 2019-2021 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. Common auxiliary functions (prot-common.el)

There are a few utilities that I keep re-using in various parts of my Emacs code base. To keep things modular, I place them all in a dedicated prot-common.el file, which can then be marked as a dependency by other libraries of mine. As such, all we do here is load the file.

;;; Common auxiliary functions (prot-common.el)
(prot-emacs-builtin-package 'prot-common)

And here is prot-common.el in its totality. It is available as a file in my dotfiles' repo (same for all my Emacs libraries):

;;; prot-common.el --- Common functions for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Common functions for my Emacs: <https://protesilaos.com/emacs/dotemacs/>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(eval-when-compile
  (require 'subr-x))

(defgroup prot-common ()
  "Auxiliary functions for my dotemacs."
  :group 'editing)

;;;###autoload
(defun prot-common-number-even-p (n)
  "Test if N is an even number."
  (if (numberp n)
      (= (% n 2) 0)
    (error "%s is not a number" n)))

;;;###autoload
(defun prot-common-number-integer-p (n)
  "Test if N is an integer."
  (if (integerp n)
      n
    (error "%s is not an integer" n)))

;;;###autoload
(defun prot-common-number-integer-positive-p (n)
  "Test if N is a positive integer."
  (if (prot-common-number-integer-p n)
      (> n 0)
    (error "%s is not a positive integer" n)))

;; Thanks to Gabriel for providing a cleaner version of
;; `prot-common-number-negative': <https://github.com/gabriel376>.
;;;###autoload
(defun prot-common-number-negative (n)
  "Make N negative."
  (if (and (numberp n) (> n 0))
      (* -1 n)
    (error "%s is not a valid positive number" n)))

;;;###autoload
(defun prot-common-empty-buffer-p ()
  "Test whether the buffer is empty."
  (or (= (point-min) (point-max))
      (save-excursion
        (goto-char (point-min))
        (while (and (looking-at "^\\([a-zA-Z]+: ?\\)?$")
                    (zerop (forward-line 1))))
        (eobp))))

;;;###autoload
(defun prot-common-minor-modes-active ()
  "Return list of active minor modes for the current buffer."
  (let ((active-modes))
    (mapc (lambda (m)
            (when (and (boundp m) (symbol-value m))
              (push m active-modes)))
          minor-mode-list)
    active-modes))

;;;###autoload
(defun prot-common-truncate-lines-silently ()
  "Toggle line truncation without printing messages."
  (let ((inhibit-message t))
    (toggle-truncate-lines t)))

;;;###autoload
(defun prot-common-disable-hl-line ()
  "Disable Hl-Line-Mode (for hooks)."
  (hl-line-mode -1))

;;;###autoload
(defun prot-common-window-bounds ()
  "Determine start and end points in the window."
  (list (window-start) (window-end)))

;;;###autoload
(defun prot-common-read-data (file)
  "Read Elisp data from FILE."
  (with-temp-buffer
    (insert-file-contents file)
    (read (current-buffer))))

;; Thanks to Omar Antolín Camarena for providing this snippet!
;;;###autoload
(defun prot-common-completion-table (category candidates)
  "Pass appropriate metadata CATEGORY to completion CANDIDATES.

This is intended for bespoke functions that need to pass
completion metadata that can then be parsed by other
tools (e.g. `embark')."
  (lambda (string pred action)
    (if (eq action 'metadata)
        `(metadata (category . ,category))
      (complete-with-action action candidates string pred))))

;; Thanks to Igor Lima for the `prot-common-crm-exclude-selected-p':
;; <https://github.com/0x462e41>.
;; This is used as a filter predicate in the relevant prompts.
(defvar crm-separator)

;;;###autoload
(defun prot-common-crm-exclude-selected-p (input)
  "Filter out INPUT from `completing-read-multiple'.
Hide non-destructively the selected entries from the completion
table, thus avoiding the risk of inputting the same match twice.

To be used as the PREDICATE of `completing-read-multiple'."
  (if-let* ((pos (string-match-p crm-separator input))
            (rev-input (reverse input))
            (element (reverse
                      (substring rev-input 0
                                 (string-match-p crm-separator rev-input))))
            (flag t))
      (progn
        (while pos
          (if (string= (substring input 0 pos) element)
              (setq pos nil)
            (setq input (substring input (1+ pos))
                  pos (string-match-p crm-separator input)
                  flag (when pos t))))
        (not flag))
    t))

;; The `prot-common-line-regexp-p' and `prot-common--line-regexp-alist'
;; are contributed by Gabriel: <https://github.com/gabriel376>.  They
;; provide a more elegant approach to using a macro, as shown further
;; below.
(defvar prot-common--line-regexp-alist
  '((empty . "[\s\t]*$")
    (indent . "^[\s\t]+")
    (non-empty . "^.+$")
    (list . "^\\([\s\t#*+]+\\|[0-9]+[^\s]?[).]+\\)")
    (heading . "^[=-]+"))
  "Alist of regexp types used by `prot-common-line-regexp-p'.")

(defun prot-common-line-regexp-p (type &optional n)
  "Test for TYPE on line.
TYPE is the car of a cons cell in
`prot-common--line-regexp-alist'.  It matches a regular
expression.

With optional N, search in the Nth line from point."
  (save-excursion
    (goto-char (point-at-bol))
    (and (not (bobp))
         (or (beginning-of-line n) t)
         (save-match-data
           (looking-at
            (alist-get type prot-common--line-regexp-alist))))))

;; The `prot-common-shell-command-with-exit-code-and-output' function is
;; courtesy of Harold Carr, who also sent a patch that improved
;; `prot-eww-download-html' (from the `prot-eww.el' library).
;;
;; More about Harold: <http://haroldcarr.com/about/>.
(defun prot-common-shell-command-with-exit-code-and-output (command &rest args)
  "Run COMMAND with ARGS.
Return the exit code and output in a list."
  (with-temp-buffer
    (list (apply 'call-process command nil (current-buffer) nil args)
          (buffer-string))))

(defvar prot-common-url-regexp
  (concat
   "\\b\\(\\(www\\.\\|\\(s?https?\\|ftp\\|file\\|gopher\\|"
   "nntp\\|news\\|telnet\\|wais\\|mailto\\|info\\):\\)"
   "\\(//[-a-z0-9_.]+:[0-9]*\\)?"
   (let ((chars "-a-z0-9_=#$@~%&*+\\/[:word:]")
	     (punct "!?:;.,"))
     (concat
      "\\(?:"
      ;; Match paired parentheses, e.g. in Wikipedia URLs:
      ;; http://thread.gmane.org/47B4E3B2.3050402@gmail.com
      "[" chars punct "]+" "(" "[" chars punct "]+" ")"
      "\\(?:" "[" chars punct "]+" "[" chars "]" "\\)?"
      "\\|"
      "[" chars punct "]+" "[" chars "]"
      "\\)"))
   "\\)")
  "Regular expression that matches URLs.
Copy of variable `browse-url-button-regexp'.")

;; This was my old approach to the task:
;;
;; ;; Based on `org--line-empty-p'.
;; (defmacro prot-common--line-p (name regexp)
;;   "Make NAME function to match REGEXP on line n from point."
;;   `(defun ,name (n)
;;      (save-excursion
;;        (goto-char (point-at-bol))
;;        (and (not (bobp))
;; 	        (or (beginning-of-line n) t)
;; 	        (save-match-data
;; 	          (looking-at ,regexp))))))
;;
;; (prot-common--line-p
;;  prot-common-empty-line-p
;;  "[\s\t]*$")
;;
;; (prot-common--line-p
;;  prot-common-indent-line-p
;;  "^[\s\t]+")
;;
;; (prot-common--line-p
;;  prot-common-non-empty-line-p
;;  "^.+$")
;;
;; (prot-common--line-p
;;  prot-common-text-list-line-p
;;  "^\\([\s\t#*+]+\\|[0-9]+[^\s]?[).]+\\)")
;;
;; (prot-common--line-p
;;  prot-common-text-heading-line-p
;;  "^[=-]+")

(provide 'prot-common)
;;; prot-common.el ends here

2.2. Common custom functions (prot-simple.el)

prot-simple.el contains a wide range of commands that are broadly in line with the built-in simple.el and lisp.el libraries. While I could offer an overview of each item in my library, I feel the code and accompanying documentation strings are clear enough for you to peruse the source directly (reproduced further below).

Still, here are some highlights for those who don't like studying the source code:

  • prot-simple-insert-pair will surround either the symbol-at-point or the active region with a pair of delimiters. It prompts for completion on which pair to use, while the most recently used entry becomes the default, so next time the user can just add it with RET at the minibuffer prompt. With an optional prefix argument (C-u) it asks for how many times to insert the selected delimiters (e.g. you want to wrap two angled brackets around the region).
  • prot-simple-scratch-buffer produces a buffer with the major-mode of the current one. With a prefix argument (C-u) it instead applies the major-mode which is stored in the user customisation option prot-simple-scratch-buffer-default-mode. With a double prefix argument (C-u C-u) it prompts for completion on which major-mode to use. If the region is active, its contents are copied to the newly created scratch buffer. The idea is based on the scratch.el package by Ian Eure.
  • prot-simple-copy-line-or-region copies the current line or the region, if that is active. With a prefix argument (C-u) it creates a duplicate of it right below.

All of the other commands are optimisations for common motions or little quality-of-life improvements for oft-required operations (such as transposition of objects or marking of balanced expressions).

Given that this is a foundational piece of my Emacs setup, it is the appropriate place to re-bind or free up some common key combinations for use elsewhere.

;;; Common custom functions (prot-simple.el)
(prot-emacs-builtin-package 'prot-simple
  (setq prot-simple-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))     ; = =
          ("~ Tilde"               . (126 126))   ; ~ ~
          ("* Asterisks"           . (42 42))     ; * *
          ("/ Forward Slash"       . (47 47))     ; / /
          ("_ underscores"         . (95 95))))   ; _ _
  (setq prot-simple-date-specifier "%F")
  (setq prot-simple-time-specifier "%R %z")
  (setq delete-pair-blink-delay 0.15) ; Emacs28 -- see `prot-simple-delete-pair-dwim'
  (setq prot-simple-scratch-buffer-default-mode 'markdown-mode)
  (setq help-window-select t)

  ;; ;; DEPRECATED 2021-10-15: set `help-window-select' to non-nil.
  ;; (setq prot-simple-focusable-help-commands
  ;;       '( describe-symbol
  ;;          describe-function
  ;;          describe-mode
  ;;          describe-variable
  ;;          describe-key
  ;;          describe-char
  ;;          what-cursor-position
  ;;          describe-package
  ;;          view-lossage))
  ;; (prot-simple-focus-help-buffers 1)

  (prot-simple-rename-help-buffers 1)

  ;; General commands
  (let ((map global-map))
    (define-key map (kbd "<insert>") nil)
    (define-key map (kbd "C-z") nil)
    (define-key map (kbd "C-x C-z") nil)
    (define-key map (kbd "C-h h") nil)
    (define-key map (kbd "M-`") nil)
    (define-key map (kbd "C-h .") #'prot-simple-describe-symbol) ; overrides `display-local-help'
    (define-key map (kbd "C-h K") #'describe-keymap) ; overrides `Info-goto-emacs-key-command-node'
    (define-key map (kbd "C-h c") #'describe-char) ; overrides `describe-key-briefly'
    (define-key map (kbd "C-c s") #'prot-simple-scratch-buffer)
    ;; Commands for lines
    (define-key map (kbd "C-S-w") #'prot-simple-copy-line-or-region)
    (define-key map (kbd "C-S-y") #'prot-simple-yank-replace-line-or-region)
    (define-key map (kbd "M-SPC") #'cycle-spacing)
    (define-key map (kbd "M-o") #'delete-blank-lines)   ; alias for C-x C-o
    (define-key map (kbd "M-k") #'prot-simple-kill-line-backward)
    (define-key map (kbd "C-S-n") #'prot-simple-multi-line-next)
    (define-key map (kbd "C-S-p") #'prot-simple-multi-line-prev)
    (define-key map (kbd "<C-return>") #'prot-simple-new-line-below)
    (define-key map (kbd "<C-S-return>") #'prot-simple-new-line-above)
    ;; Commands for text insertion or manipulation
    (define-key map (kbd "C-=") #'prot-simple-insert-date)
    (define-key map (kbd "C-<") #'prot-simple-escape-url)
    (define-key map (kbd "C-'") #'prot-simple-insert-pair)
    (define-key map (kbd "M-'") #'prot-simple-insert-pair)
    (define-key map (kbd "M-\\") #'prot-simple-delete-pair-dwim)
    (define-key map (kbd "C-M-;") #'prot-simple-cite-region)
    (define-key map (kbd "C-M-^") #'prot-simple-insert-undercaret)
    (define-key map (kbd "<C-M-backspace>") #'backward-kill-sexp)
    (define-key map (kbd "M-c") #'capitalize-dwim)
    (define-key map (kbd "M-l") #'downcase-dwim)        ; "lower" case
    (define-key map (kbd "M-u") #'upcase-dwim)
    ;; Commands for object transposition
    (define-key map (kbd "C-t") #'prot-simple-transpose-chars)
    (define-key map (kbd "C-x C-t") #'prot-simple-transpose-lines)
    (define-key map (kbd "C-S-t") #'prot-simple-transpose-paragraphs)
    (define-key map (kbd "C-x M-t") #'prot-simple-transpose-sentences)
    (define-key map (kbd "C-M-t") #'prot-simple-transpose-sexps)
    (define-key map (kbd "M-t") #'prot-simple-transpose-words)
    ;; Commands for marking objects
    (define-key map (kbd "M-@") #'prot-simple-mark-word)       ; replaces `mark-word'
    (define-key map (kbd "C-M-SPC") #'prot-simple-mark-construct-dwim)
    (define-key map (kbd "C-M-d") #'prot-simple-downward-list)
    ;; Commands for paragraphs
    (define-key map (kbd "M-Q") #'prot-simple-unfill-region-or-paragraph)
    ;; Commands for windows
    (define-key map (kbd "C-x n n") #'prot-simple-narrow-dwim) ; replaces `narrow-to-region'
    (define-key map (kbd "C-x M") #'prot-simple-monocle)
    ;; Commands for buffers
    (define-key map (kbd "M-=") #'count-words)
    (define-key map (kbd "<C-f2>") #'prot-simple-rename-file-and-buffer)
    (define-key map (kbd "C-x K") #'prot-simple-kill-buffer-current)))

These are the contents of the prot-simple.el library (find the file in my dotfiles' repo (as with all my Elisp code)):

;;; prot-simple.el --- Common commands for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Common commands for my Emacs: <https://protesilaos.com/emacs/dotemacs/>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(eval-when-compile
  (require 'cl-lib))
(require 'prot-common)

(defgroup prot-simple ()
  "Generic utilities for my dotemacs."
  :group 'editing)

;; Got those numbers from `string-to-char'
(defcustom prot-simple-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))     ; = =
    ("~ Tilde"               . (126 126))   ; ~ ~
    ("* Asterisks"           . (42 42))     ; * *
    ("/ Forward Slash"       . (47 47))     ; / /
    ("_ underscores"         . (95 95)))    ; _ _
  "Alist of pairs for use with `prot-simple-insert-pair-completion'."
  :type 'alist
  :group 'prot-simple)

(defcustom prot-simple-date-specifier "%F"
  "Date specifier for `format-time-string'.
Used by `prot-simple-inset-date'."
  :type 'string
  :group 'prot-simple)

(defcustom prot-simple-time-specifier "%R %z"
  "Time specifier for `format-time-string'.
Used by `prot-simple-inset-date'."
  :type 'string
  :group 'prot-simple)

(defcustom prot-simple-focusable-help-commands
  '( describe-symbol describe-function
     describe-variable describe-key
     view-lossage)
  "Commands whose buffers should be focused when displayed.
This makes it easier to dismiss them at once.

Also see `prot-simple-focus-help-buffers'."
  :type '(repeat symbol)
  :group 'prot-simple)

(defcustom prot-simple-scratch-buffer-default-mode 'markdown-mode
  "Default major mode for `prot-simple-scratch-buffer'."
  :type 'symbol
  :group 'prot-simple)

;;; Generic setup

;;;; Scratch buffers
;; The idea is based on the `scratch.el' package by Ian Eure:
;; <https://github.com/ieure/scratch-el>.

;; Adapted from the `scratch.el' package by Ian Eure.
(defun prot-simple--scratch-list-modes ()
  "List known major modes."
  (cl-loop for sym the symbols of obarray
           for name = (symbol-name sym)
           when (and (functionp sym)
                     (not (member sym minor-mode-list))
                     (string-match "-mode$" name)
                     (not (string-match "--" name)))
           collect name))

(defun prot-simple--scratch-buffer-setup (region &optional mode)
  "Add contents to `scratch' buffer and name it accordingly.

REGION is added to the contents to the new buffer.

Use the current buffer's major mode by default.  With optional
MODE use that major mode instead."
  (let* ((major (or mode major-mode))
         (string (format "Scratch buffer for: %s\n\n" major))
         (text (concat string region))
         (buf (format "*Scratch for %s*" major)))
    (with-current-buffer (get-buffer-create buf)
      (funcall major)
	  (save-excursion
        (insert text)
        (goto-char (point-min))
        (comment-region (point-at-bol) (point-at-eol)))
	  (vertical-motion 2))
    (pop-to-buffer buf)))

;;;###autoload
(defun prot-simple-scratch-buffer (&optional arg)
  "Produce a bespoke scratch buffer matching current major mode.

With optional ARG as a prefix argument (\\[universal-argument]),
use `prot-simple-scratch-buffer-default-mode'.

With ARG as a double prefix argument, prompt for a major mode
with completion.

If region is active, copy its contents to the new scratch
buffer."
  (interactive "P")
  (let* ((default-mode prot-simple-scratch-buffer-default-mode)
         (modes (prot-simple--scratch-list-modes))
         (region (with-current-buffer (current-buffer)
                   (if (region-active-p)
                       (buffer-substring-no-properties
                        (region-beginning)
                        (region-end))
                     "")))
         (m))
    (pcase (prefix-numeric-value arg)
      (16 (progn
            (setq m (intern (completing-read "Select major mode: " modes nil t)))
            (prot-simple--scratch-buffer-setup region m)))
      (4 (prot-simple--scratch-buffer-setup region default-mode))
      (_ (prot-simple--scratch-buffer-setup region)))))

;; ;; DEPRECATED 2021-10-15: Just set `help-window-select' to non-nil.
;;
;; ;;;; Focus auxiliary buffers
;; 
;; ;; TODO 2021-08-27: Is there a more general way to do this without
;; ;; specifying the BUF?  That way we would only need one function.
;; (defmacro prot-simple--auto-focus-buffer (fn doc buf)
;;   "Produce FN with DOC for focusing BUF."
;;   `(defun ,fn (&rest _)
;;     ,doc
;;     (when-let ((window (get-buffer-window ,buf)))
;;       (select-window window))))
;; 
;; (prot-simple--auto-focus-buffer
;;  prot-simple--help-focus
;;   "Select window with Help buffer.
;; Intended as :after advice for `describe-symbol' and friends."
;;   (help-buffer))
;; 
;; (prot-simple--auto-focus-buffer
;;  prot-simple--messages-focus
;;   "Select window with Help buffer.
;; Intended as :after advice for `view-echo-area-messages'."
;;   (messages-buffer))
;; 
;; ;;;###autoload
;; (define-minor-mode prot-simple-focus-help-buffers
;;   "Add advice to focus `prot-simple-focusable-help-commands'."
;;   :lighter nil
;;   (if prot-simple-focus-help-buffers
;;       (progn
;;         (dolist (fn prot-simple-focusable-help-commands)
;;           (advice-add fn :after 'prot-simple--help-focus))
;;         (advice-add 'view-echo-area-messages :after 'prot-simple--messages-focus))
;;     (dolist (fn prot-simple-focusable-help-commands)
;;       (advice-remove fn 'prot-simple--help-focus))
;;     (advice-remove 'view-echo-area-messages 'prot-simple--messages-focus)))

;;;; Rename Help buffers (EXPERIMENTAL)

(defvar prot-simple-help-mode-post-render-hook nil
  "Hook that runs after Help is rendered (via `advice-add').")

(defun prot-simple--help-mode-post-render (&rest _)
  "Run `prot-simple-help-mode-post-render-hook'."
  (run-hooks 'prot-simple-help-mode-post-render-hook))

(defconst prot-simple--help-symbol-regexp
  ;; TODO 2021-10-12: Avoid duplication in regexp.
  (concat
   "^\\(.*?\\)\s\\(is an?\\|runs the\\)\s\\(command\\|function\\|variable\\|keymap variable"
   "\\|native compiled Lisp function\\|interactive native compiled Lisp function"
   "\\|built-in function\\|interactive built-in function\\|Lisp closure\\)\s"
   "\\(\\_<.*?\\_>\\)\\( (found in .*)\\)?")
  "Regexp to match Help buffer description.")

(defconst prot-simple--help-symbol-false-positives
  "\\(in\\|defined\\)"
  "False positives for `prot-simple--help-symbol-regexp'.")

(defun prot-simple--rename-help-buffer ()
  "Rename the current Help buffer."
  (with-current-buffer (help-buffer)
    (goto-char (point-min))
    (when (re-search-forward prot-simple--help-symbol-regexp nil t)
      (let* ((thing (match-string 1))
             (symbol (match-string 4))
             (scope (match-string 5))
             (description (cond
                           (scope
                            (concat symbol scope))
                           ((and (not (string-match-p prot-simple--help-symbol-false-positives symbol))
                                 (symbolp (intern symbol)))
                            symbol)
                           ((match-string 3)))))
        (rename-buffer
         (format "*%s (%s) # Help*" thing description)
         t)))))

;;;###autoload
(define-minor-mode prot-simple-rename-help-buffers
  "Rename Help buffers based on their contents."
  :init-value nil
  :global t
  (if prot-simple-rename-help-buffers
      (progn
        (advice-add #'help-window-setup :after #'prot-simple--help-mode-post-render)
        (add-hook 'prot-simple-help-mode-post-render-hook #'prot-simple--rename-help-buffer))
    (advice-remove #'help-window-setup #'prot-simple--help-mode-post-render)
    (remove-hook 'prot-simple-help-mode-post-render-hook #'prot-simple--rename-help-buffer)))

;;; Commands

;;;; General commands

(autoload 'symbol-at-point "thingatpt")

;;;###autoload
(defun prot-simple-describe-symbol ()
  "Run `describe-symbol' for the `symbol-at-point'."
  (interactive)
  (describe-symbol (symbol-at-point)))

;;;; Commands for lines

;;;###autoload
(defun prot-simple-new-line-below (&optional arg)
  "Create an empty line below the current one.
Move the point to the absolute beginning.  Adapt indentation by
passing optional prefix ARG (\\[universal-argument]).  Also see
`prot-simple-new-line-above'."
  (interactive "P")
  (end-of-line)
  (if arg
      (newline-and-indent)
    (newline)))

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

;;;###autoload
(defun prot-simple-copy-line-or-region (&optional arg)
  "Kill-save the current line or active region.
With optional ARG (\\[universal-argument]) duplicate the target
instead.  When region is active, also apply context-aware
indentation while duplicating."
  (interactive "P")
  (unless mark-ring                  ; needed when entering a new buffer
    (push-mark (point) t nil))
  (let* ((rbeg (region-beginning))
         (rend (region-end))
         (pbol (point-at-bol))
         (peol (point-at-eol))
         (indent (if (eq (or rbeg rend) pbol) nil arg)))
    (cond
     ((use-region-p)
      (if arg
          (let ((text (buffer-substring rbeg rend)))
            (when (eq (point) rbeg)
              (exchange-point-and-mark))
            (prot-simple-new-line-below indent)
            (insert text))
        (copy-region-as-kill rbeg rend)
        (message "Current region copied")))
     (t
      (if arg
          (let ((text (buffer-substring pbol peol)))
            (goto-char (point-at-eol))
            (newline)
            (insert text))
        (copy-region-as-kill pbol peol)
        (message "Current line copied"))))))

;;;###autoload
(defun prot-simple-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))

;;;###autoload
(defun prot-simple-multi-line-next ()
  "Move point 15 lines down."
  (interactive)
  (forward-line 15))

;;;###autoload
(defun prot-simple-multi-line-prev ()
  "Move point 15 lines up."
  (interactive)
  (forward-line -15))

;;;###autoload
(defun prot-simple-kill-line-backward ()
  "Kill from point to the beginning of the line."
  (interactive)
  (kill-line 0))

;;;; Commands for text insertion or manipulation

(defvar prot-simple--character-hist '()
  "History of inputs for `prot-simple-insert-pair-completion'.")

(defun prot-simple--character-prompt (chars)
  "Helper of `prot-simple-insert-pair-completion' to read CHARS."
  (let ((def (car prot-simple--character-hist)))
    (completing-read
     (format "Select character [%s]: " def)
     chars nil t nil 'prot-simple--character-hist def)))

(define-obsolete-function-alias
  'prot-simple-insert-pair-completion
  'prot-simple-insert-pair "2021-07-30")

;;;###autoload
(defun prot-simple-insert-pair (pair &optional count)
  "Insert PAIR from `prot-simple-insert-pair-alist'.
Operate on the symbol at point.  If the region is active, use it
instead.

With optional COUNT (either as a natural number from Lisp or a
universal prefix argument (\\[universal-argument]) when used
interactively) prompt for the number of delimiters to insert."
  (interactive
   (list
    (prot-simple--character-prompt prot-simple-insert-pair-alist)
    current-prefix-arg))
  (let* ((data prot-simple-insert-pair-alist)
         (left (cadr (assoc pair data)))
         (right (caddr (assoc pair data)))
         (n (cond
             ((and count (natnump count))
              count)
             (count
              (read-number "How many delimiters?" 2))
             (1)))
         (beg)
         (end))
    (cond
     ((region-active-p)
      (setq beg (region-beginning)
            end (region-end)))
     ((when (thing-at-point 'symbol)
        (let ((bounds (bounds-of-thing-at-point 'symbol)))
          (setq beg (car bounds)
                end (cdr bounds)))))
     (t (setq beg (point)
              end (point))))
    (save-excursion
      (goto-char end)
      (dotimes (_ n)
        (insert right))
      (goto-char beg)
      (dotimes (_ n)
        (insert left)))))

;;;###autoload
(defun prot-simple-delete-pair-dwim ()
  "Delete pair following or preceding point.
For Emacs version 28 or higher, the feedback's delay is
controlled by `delete-pair-blink-delay'."
  (interactive)
  (if (eq (point) (cdr (bounds-of-thing-at-point 'sexp)))
      (delete-pair -1)
    (delete-pair 1)))

;;;###autoload
(defun prot-simple-insert-date (&optional arg)
  "Insert the current date as `prot-simple-date-specifier'.

With optional prefix ARG (\\[universal-argument]) also append the
current time understood as `prot-simple-time-specifier'.

When region is active, delete the highlighted text and replace it
with the specified date."
  (interactive "P")
  (let* ((date prot-simple-date-specifier)
         (time prot-simple-time-specifier)
         (format (if arg (format "%s %s" date time) date)))
    (when (use-region-p)
      (delete-region (region-beginning) (region-end)))
    (insert (format-time-string format))))


(autoload 'ffap-url-at-point "ffap")
(defvar ffap-string-at-point-region)

;;;###autoload
(defun prot-simple-escape-url ()
  "Wrap URL (or email address) in angled brackets."
  (interactive)
  (when-let ((url (ffap-url-at-point)))
    (let* ((reg ffap-string-at-point-region)
           (beg (car reg))
           (end (cadr reg))
           (string (if (string-match-p "^mailto:" url)
                       (substring url 7)
                     url)))
      (delete-region beg end)
      (insert (format "<%s>" string)))))

;;;###autoload
(defun prot-simple-cite-region (beg end &optional arg)
  "Cite text in region lines between BEG and END.

Region lines are always understood in absolute terms, regardless
of whether the region boundaries coincide with them.

With optional prefix ARG (\\[universal-argument]) prompt for a
description that will be placed on a new line at the top of the
newly formatted text."
  (interactive "*r\nP")
  (let* ((absolute-beg (if (< beg end)
                           (progn (goto-char beg) (point-at-bol))
                         (progn (goto-char end) (point-at-eol))))
         (absolute-end (if (< beg end)
                           (progn (goto-char end) (point-at-eol))
                         (progn (goto-char beg) (point-at-bol))))
         (prefix-text (if (< beg end)
                          (buffer-substring-no-properties absolute-beg beg)
                        (buffer-substring-no-properties absolute-end end)))
         (prefix (if (string-match-p "\\`[\t\s]+\\'" prefix-text)
                     prefix-text
                   (replace-regexp-in-string "\\`\\([\t\s]+\\).*" "\\1" prefix-text)))
         (description (if arg
                          (format "+----[ %s ]\n"
                                  (read-string "Add description: "))
                        "+----\n"))
         (marked-text (buffer-substring-no-properties absolute-beg absolute-end))
         (marked-text-new (replace-regexp-in-string "^.*?" (concat prefix "|") marked-text))
         (text (with-temp-buffer
                 (insert marked-text-new)
                 (save-excursion
                   (goto-char (point-min))
                   (re-search-forward "^\\(^[\s\t]+\\)?.*?")
                   (forward-line -1)
                   (insert (concat prefix description))
                   (goto-char (point-max))
                   (forward-line 1)
                   (insert "\n")
                   (insert (concat prefix "+----")))
                 (buffer-substring-no-properties (point-min) (point-max)))))
    (delete-region absolute-beg absolute-end)
    (insert text)))

;; `prot-simple-insert-undercaret' was offered to me by Gregory
;; Heytings: <https://debbugs.gnu.org/cgi/bugreport.cgi?bug=45068#250>.
;;;###autoload
(defun prot-simple-insert-undercaret (&optional arg)
  "Draw carets below the characters on the current line or region."
  (interactive "p")
  (let* ((begin (if (region-active-p) (region-beginning) (line-beginning-position)))
         (end (if (region-active-p) (region-end) (line-end-position)))
         (lines (- (line-number-at-pos end) (line-number-at-pos begin) -1))
         (comment (and (/= arg 1) (= lines 1)))
         (final-forward-line -1))
    (goto-char begin)
    (dotimes (i lines)
      (let* ((line-begin (if (zerop i) begin (line-beginning-position)))
             (line-end (if (= (1+ i) lines) end (line-end-position)))
             (begin-column (progn (goto-char line-begin) (current-column)))
             (end-column (progn (goto-char line-end) (current-column)))
             (prefix-begin (line-beginning-position))
             (prefix-end (progn (beginning-of-line-text) (point)))
             (prefix-end-column (progn (goto-char prefix-end) (current-column)))
             (delta (if (< begin-column prefix-end-column) (- prefix-end-column begin-column) 0))
             (prefix-string (buffer-substring-no-properties prefix-begin prefix-end))
             (prefix (if (string-match-p "\\` *\\'" prefix-string) "" prefix-string))
             (whitespace (make-string (- (+ begin-column delta) (string-width prefix)) ?\ ))
             (do-under (< delta (- line-end line-begin)))
             (under (if do-under (make-string (- end-column begin-column delta) ?^) ""))
             (under-string (concat prefix whitespace under "\n")))
        (forward-line 1)
        (if do-under (insert under-string) (setq final-forward-line -2))
        (setq end (+ end (length under-string)))
        (when comment (insert prefix whitespace "\n"))))
    (forward-line final-forward-line)
    (goto-char (line-end-position))))

;;;; Commands for object transposition

(defmacro prot-simple-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-simple-transpose
 prot-simple-transpose-lines
 "lines"
 "Transpose lines or swap over active region.")

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

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

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

;;;###autoload
(defun prot-simple-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 the end of the
line."
  (interactive)
  (transpose-chars -1)
  (forward-char))

;;;###autoload
(defun prot-simple-transpose-words (arg)
  "Transpose ARG 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))))

;;;; Commands for marking syntactic constructs

(defmacro prot-simple-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-simple-mark
 prot-simple-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-simple-mark-symbol'
instead.")

(prot-simple-mark
 prot-simple-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.")

;;;###autoload
(defun prot-simple-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)))

;;;###autoload
(defun prot-simple-mark-construct-dwim (&optional arg)
  "Mark symbol or balanced expression at point.
A do-what-I-mean wrapper for `prot-simple-mark-sexp-backward',
`mark-sexp', and `prot-simple-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-simple-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-simple-mark-symbol arg t))
   ((eq (point) (cdr (bounds-of-thing-at-point 'sexp)))
    (prot-simple-mark-sexp-backward arg))
   (t
    (mark-sexp arg t))))

;;;; Commands for code navigation (work in progress)

;;;###autoload
(defun prot-simple-downward-list (&optional arg)
  "Like `backward-up-list' but defaults to a forward motion.
With optional ARG, move that many times in the given
direction (negative is forward due to this being a
'backward'-facing command)."
  (interactive "P")
  (backward-up-list (or arg -1)))

;;;; Commands for paragraphs

(defvar-local prot-simple--auto-fill-cycle-state 1
  "Representation of `prot-simple-auto-fill-cycle' state.")

;; Based on gungadin-cylocal.el (private communication with Christopher
;; Dimech---disclosed with permission).
;;;###autoload
(defun prot-simple-auto-fill-cycle ()
  "Cycles auto fill for comments, everything, nothing."
  (interactive)
  (let ((n prot-simple--auto-fill-cycle-state))
    (pcase n
      (2
       (message "Auto fill %s" (propertize "buffer" 'face 'warning))
       (setq-local comment-auto-fill-only-comments nil)
       (setq-local prot-simple--auto-fill-cycle-state (1+ n)))
      (3
       (message "Disable auto fill")
       (auto-fill-mode 0)
       (setq-local prot-simple--auto-fill-cycle-state (1+ n)))
      (_
       (message "Auto fill %s" (propertize "comments" 'face 'success))
       (setq-local comment-auto-fill-only-comments t)
       (auto-fill-mode 1)
       (setq-local prot-simple--auto-fill-cycle-state 2)))))

;;;###autoload
(defun prot-simple-unfill-region-or-paragraph (&optional beg end)
  "Unfill paragraph or, when active, the region.
Join all lines in region delimited by BEG and END, 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 "r")
  (let ((fill-column most-positive-fixnum))
    (if (use-region-p)
        (fill-region beg end)
      (fill-paragraph))))

;;;; Commands for windows

;;;###autoload
(defun prot-simple-narrow-visible-window ()
  "Narrow buffer to wisible window area.
Also check `prot-simple-narrow-dwim'."
  (interactive)
  (let* ((bounds (prot-common-window-bounds))
         (window-area (- (cadr bounds) (car bounds)))
         (buffer-area (- (point-max) (point-min))))
    (if (/= buffer-area window-area)
        (narrow-to-region (car bounds) (cadr bounds))
      (user-error "Buffer fits in the window; won't narrow"))))

;;;###autoload
(defun prot-simple-narrow-dwim ()
  "Do-what-I-mean narrowing.
If region is active, narrow the buffer to the region's
boundaries.

If no region is active, narrow to the visible portion of the
window.

If narrowing is in effect, widen the view."
  (interactive)
  (unless mark-ring                  ; needed when entering a new buffer
    (push-mark (point) t nil))
  (cond
   ((and (use-region-p)
         (null (buffer-narrowed-p)))
    (let ((beg (region-beginning))
          (end (region-end)))
      (narrow-to-region beg end)))
   ((null (buffer-narrowed-p))
    (prot-simple-narrow-visible-window))
   (t
    (widen)
    (recenter))))

;; Inspired by Pierre Neidhardt's windower:
;; https://gitlab.com/ambrevar/emacs-windower/-/blob/master/windower.el
(defvar prot-simple--windows-current nil
  "Current window configuration.")

;;;###autoload
(define-minor-mode prot-simple-monocle
  "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
  (let ((win prot-simple--windows-current))
    (if (one-window-p)
        (when win
          (set-window-configuration win))
      (setq prot-simple--windows-current (current-window-configuration))
      (delete-other-windows))))

(defun prot-simple--monocle-disable ()
  "Set variable `prot-simple-monocle' to nil, when appropriate.
To be hooked to `window-configuration-change-hook'."
  (when (and prot-simple-monocle (not (one-window-p)))
    (delete-other-windows)
    (prot-simple-monocle -1)
    (set-window-configuration prot-simple--windows-current)))

(add-hook 'window-configuration-change-hook #'prot-simple--monocle-disable)

;;;; Commands for buffers

;;;###autoload
(defun prot-simple-kill-buffer-current (&optional arg)
  "Kill current buffer or abort recursion when in minibuffer.
With optional prefix ARG (\\[universal-argument]) delete the
buffer's window as well."
  (interactive "P")
  (if (minibufferp)
      (abort-recursive-edit)
    (kill-buffer (current-buffer)))
  (when (and arg
             (not (one-window-p)))
    (delete-window)))

;;;###autoload
(defun prot-simple-rename-file-and-buffer (name)
  "Apply NAME to current file and rename its buffer.
Do not try to make a new directory or anything fancy."
  (interactive
   (list (read-string "Rename current file: " (buffer-file-name))))
  (let ((file (buffer-file-name)))
    (if (vc-registered file)
        (vc-rename-file file name)
      (rename-file file name))
    (set-visited-file-name name t t)))

(provide 'prot-simple)
;;; prot-simple.el ends here

2.2.1. prot-pulse.el (highlight cursor position)

pulse.el 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).

While prot-pulse.el (complete code further below) is a thin wrapper that provides some extensions that are useful to my workflow. Specifically, it declares a new face and defines a command that implements it: prot-pulse-pulse-line. This is useful to quickly highlight the line and buffer I am on, but can also be utilised by other tools that move the point an arbitrary distance.

;;; prot-pulse.el (highlight cursor position)
(prot-emacs-builtin-package 'prot-pulse
  (setq prot-pulse-pulse-command-list
        '(recenter-top-bottom
          move-to-window-line-top-bottom
          reposition-window
          bookmark-jump
          other-window))
  (prot-pulse-advice-commands-mode 1)
  (define-key global-map (kbd "C-x l") #'prot-pulse-pulse-line)) ; override `count-lines-page'

This is the code for prot-pulse.el (part of my dotfiles' repo, in case you wish to get the file):

;;; prot-pulse.el --- Extend pulse.el for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2020  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Extensions to the built-in `pulse.el' library for my Emacs
;; configuration: <https://protesilaos.com/emacs/dotemacs/>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'pulse)

(defgroup prot-pulse ()
  "Extensions for `pulse.el'."
  :group 'editing)

(defcustom prot-pulse-pulse-command-list
  '(recenter-top-bottom reposition-window)
  "Commands that should automatically `prot-pulse-pulse-line'.
You must restart function `prot-pulse-advice-commands-mode' for
changes to take effect."
  :type 'list
  :group 'prot-pulse)

(defface prot-pulse-line
  '((default :extend t)
    (((class color) (min-colors 88) (background light))
     :background "#8eecf4")
    (((class color) (min-colors 88) (background dark))
     :background "#004065")
    (t :inverse-video t))
  "Default face for `prot-pulse-pulse-line'."
  :group 'prot-pulse)

;;;###autoload
(defun prot-pulse-pulse-line (&optional face)
  "Temporarily highlight the current line with optional FACE."
  (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)))
    (pulse-momentary-highlight-region start end face)))

;;;###autoload
(defun prot-pulse-recentre-top ()
  "Reposition at the top and pulse line.
Add this to a hook, such as `imenu-after-jump-hook'."
  (let ((pulse-delay .05))
    (recenter 0)
    (prot-pulse-pulse-line)))

;;;###autoload
(defun prot-pulse-recentre-centre ()
  "Recentre and pulse line.
Add this to a hook, such as `imenu-after-jump-hook'."
  (let ((pulse-delay .05))
    (recenter nil)
    (prot-pulse-pulse-line)))

(autoload 'org-at-heading-p "org")
(autoload 'org-show-entry "org")
(autoload 'org-reveal "org")
(autoload 'outline-show-entry "outline")

;;;###autoload
(defun prot-pulse-show-entry ()
  "Reveal index at point in outline views.
To be used with a hook such as `imenu-after-jump-hook'."
  (cond
   ((and (eq major-mode 'org-mode)
         (org-at-heading-p))
    (org-show-entry)
    (org-reveal t))
   ((bound-and-true-p prot-outline-minor-mode)
    (outline-show-entry))))

(defvar prot-pulse-after-command-hook nil
  "Hook that runs after select commands.
To be used with `advice-add' after those functions declared in
`prot-pulse-pulse-command-list'.")

(defun prot-pulse-after-command (&rest _)
  "Run `prot-pulse-after-command-hook'."
  (run-hooks 'prot-pulse-after-command-hook))

;;;###autoload
(define-minor-mode prot-pulse-advice-commands-mode
  "Set up for `prot-pulse-pulse-command-list'."
  :init-value nil
  :global t
  (if prot-pulse-advice-commands-mode
      (progn
        (dolist (fn prot-pulse-pulse-command-list)
          (advice-add fn :after #'prot-pulse-after-command))
        (add-hook 'prot-pulse-after-command-hook #'prot-pulse-pulse-line))
    (dolist (fn prot-pulse-pulse-command-list)
      (advice-remove fn #'prot-pulse-after-command))
    (remove-hook 'prot-pulse-after-command-hook #'prot-pulse-pulse-line)))

(provide 'prot-pulse)
;;; prot-pulse.el ends here

2.3. Make Custom UI code disposable

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 custom-* snippet.

To avoid conflicts, I instruct Emacs to put all "custom" code in a temporary file (the /tmp path on Linux). Custom does its work as expected, but its state does not persist to mess up with my carefully designed (and version-controlled) configuration.

This feels kinda hacky though it gets the job done.

Together with Justin Schell we had experimented with using the null-device instead of a temporary file. However, that created problems under certain conditions, as Emacs would issue warnings about it and prompt on how to proceed.

;;; Make Custom UI code disposable
(prot-emacs-builtin-package 'cus-edit
  ;; Disable the damn thing
  (setq custom-file (make-temp-file "emacs-custom-")))

2.4. Propagation of shell environment variables (exec-path-from-shell.el)

Since 2021-09-21 I have been experimenting with Sway, a tiling window manager for the Wayland display protocol. One of the major changes to how the older Xorg display server would handle the initialisation of the environment is that Wayland does not have a login shell. Instead, programs are initialised by the session manager, which in my case is controlled by systemd (check my dotfiles' git repo for the technicalities).

To cut the long story short, Emacs needs to be made aware of the environment variables that are available to my Bash shell but which Wayland has obscured from this contexts. Thankfully, Steve Purcell's exec-path-from-shell handles the task splendidly. In the user option exec-path-from-shell-variables we specify the environment variables we want to inform Emacs about. Then we just invoke the function exec-path-from-shell-initialize and are good to go. Now we can get back to using Emacs the way we did on Xorg.

Note that Steve's package is not specifically about Wayland. Its documentation makes references to other operating systems.

(prot-emacs-elpa-package 'exec-path-from-shell
  (setq exec-path-from-shell-variables
        '("PATH" "MANPATH" "SSH_AUTH_SOCK"))
  (exec-path-from-shell-initialize))

2.5. Modus themes (my highly accessible themes)

This is a project I started as soon as I switched to Emacs in July 2019. About a year later the themes became part of upstream Emacs, available for Emacs version 28 (as of version 0.12.0 of the themes). I have benefited 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 for accessible readability. They conform with the highest 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 (the highest of its kind).

The themes are "Modus Operandi" (light) and "Modus Vivendi" (dark). The source code is available on their GitLab page while you can read the HTML version of their manual on my website. If you have the package installed or are using Emacs >=28, you can read the manual from the built-in Info reader. Evaluate: (info "(modus-themes) Top").

The manual covers everything from the basics to more advanced, "do-it-yourself" cases.

The list of supported packages is comprehensive and a lot of work goes into getting the details right. Plus, there are lots of customisation options to tweak the looks of the themes (note though that the values I set for those variables in the following code block are not indicative of my preferences, as I always try different combinations to test things across a range of scenaria).

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:

And if you do enjoy reading such entries, then you may also wish to check the Change Log of the Modus themes.

;;; Modus themes (my highly accessible themes)
(prot-emacs-builtin-package 'modus-themes
  ;; Add all your customizations prior to loading the themes
  ;;
  ;; NOTE: these are not my preferences!  I am always testing various
  ;; configurations.  Though I still like what I have here.
  (setq modus-themes-italic-constructs t
        modus-themes-bold-constructs nil
        modus-themes-mixed-fonts t
        modus-themes-subtle-line-numbers nil
        modus-themes-intense-markup nil
        modus-themes-success-deuteranopia nil
        modus-themes-tabs-accented nil
        modus-themes-inhibit-reload t ; only applies to `customize-set-variable' and related

        modus-themes-fringes nil ; {nil,'subtle,'intense}

        ;; Options for `modus-themes-lang-checkers' are either nil (the
        ;; default), or a list of properties that may include any of those
        ;; symbols: `straight-underline', `text-also', `background',
        ;; `intense' OR `faint'.
        modus-themes-lang-checkers '(text-also straight-underline)

        ;; Options for `modus-themes-mode-line' are either nil, or a
        ;; list that can combine any of `3d' OR `moody', `borderless',
        ;; `accented', `padded'.
        modus-themes-mode-line nil ; For Moody, also check `prot-moody'

        ;; This one only works when `modus-themes-mode-line' (above) has
        ;; the `padded' property.  It takes a positive integer.
        modus-themes-mode-line-padding 2

        ;; Options for `modus-themes-syntax' are either nil (the default),
        ;; or a list of properties that may include any of those symbols:
        ;; `faint', `yellow-comments', `green-strings', `alt-syntax'
        modus-themes-syntax nil

        ;; Options for `modus-themes-hl-line' are either nil (the default),
        ;; or a list of properties that may include any of those symbols:
        ;; `accented', `underline', `intense'
        modus-themes-hl-line nil

        ;; Options for `modus-themes-paren-match' are either nil (the
        ;; default), or a list of properties that may include any of those
        ;; symbols: `bold', `intense', `underline'
        modus-themes-paren-match nil

        ;; Options for `modus-themes-links' are either nil (the default),
        ;; or a list of properties that may include any of those symbols:
        ;; `neutral-underline' OR `no-underline', `faint' OR `no-color',
        ;; `bold', `italic', `background'
        modus-themes-links nil

        ;; Options for `modus-themes-prompts' are either nil (the
        ;; default), or a list of properties that may include any of
        ;; those symbols: `background', `bold', `gray', `intense',
        ;; `italic'
        modus-themes-prompts nil

        modus-themes-completions nil ; {nil,'moderate,'opinionated}

        modus-themes-mail-citations nil ; {nil,'faint,'monochrome}

        ;; Options for `modus-themes-region' are either nil (the default),
        ;; or a list of properties that may include any of those symbols:
        ;; `no-extend', `bg-only', `accented'
        modus-themes-region '(no-extend bg-only)

        ;; Options for `modus-themes-diffs': nil, 'desaturated,
        ;; 'bg-only, 'deuteranopia, 'fg-only-deuteranopia
        modus-themes-diffs 'bg-only

        modus-themes-org-blocks nil ; {nil,'gray-background,'tinted-background} (also read doc string)

        ;; This is an alist: read the manual or its doc string.
        modus-themes-org-agenda
        '((header-block . (variable-pitch scale-title))
          (header-date . (bold-today scale-heading ))
          (event . (accented italic))
          (scheduled . uniform)
          (habit . traffic-light-deuteranopia))

        ;; This is an alist: read the manual or its doc string.
        modus-themes-headings
        '((1 . (background overline))
          (2 . (background overline))
          (3 . (background rainbow overline))
          (t . (background rainbow no-bold overline)))

        modus-themes-variable-pitch-ui nil
        modus-themes-variable-pitch-headings nil
        modus-themes-scale-headings nil
        modus-themes-scale-1 1.1
        modus-themes-scale-2 1.15
        modus-themes-scale-3 1.21
        modus-themes-scale-4 1.27
        modus-themes-scale-title 1.33
        modus-themes-scale-small 0.9)

  ;; Load the theme files before enabling a theme (else you get an error).
  (modus-themes-load-themes)

  ;; Custom faces (for demo purposes---check the themes' manual for more
  ;; advanced uses).
  (defun prot/modus-themes-custom-faces ()
    (modus-themes-with-colors
      (custom-set-faces
       `(fill-column-indicator ((,class :background ,bg-inactive
                                        :foreground ,bg-inactive))))))

  (add-hook 'modus-themes-after-load-theme-hook #'prot/modus-themes-custom-faces)

  ;; Enable the theme at startup.  This is done after loading the files.
  ;; You only need `modus-themes-load-operandi' for the light theme or
  ;; `modus-themes-load-vivendi' for the dark one.  What I have here is
  ;; a simple test to load a light/dark theme based on some general time
  ;; ranges (just accounting for the hour and without checking for the
  ;; actual sunrise/sunset times).  Plus we have `modus-themes-toggle'
  ;; to switch themes at will.
  (let ((time (string-to-number (format-time-string "%H"))))
    (if (and (> time 5) (< time 18))
        (modus-themes-load-operandi)
      (modus-themes-load-vivendi)))

  ;; Also check my package configurations for `prot-fonts' because I use
  ;; the `modus-themes-after-load-theme-hook' for some typeface-related
  ;; tweaks (as those are made at the "face" level).
  (define-key global-map (kbd "<f5>") #'modus-themes-toggle))

2.6. LIN Is Noticeable (lin.el)

This is another package of mine. LIN locally remaps the hl-line face to a style that is optimal for major modes where line selection is the primary mode of interaction.

The idea is that hl-line cannot work equally well for contexts with competing priorities: (i) line selection, or (ii) simple line highlight. In the former case, the current line needs to be made prominent because it carries a specific meaning of some significance in the given context. Whereas in the latter case, the primary mode of interaction does not revolve around the line highlight itself: it may be because the focus is on editing text or reading through the buffer's contents, so the current line highlight is more of a gentle reminder of the point's location on the vertical axis.

lin-mode only shows its effect when hl-line-mode is active or, more specifically, when the hl-line face is used in the buffer. lin-mode DOES NOT activate hl-line-mode and does not do anything other than the aforementioned face remapping.

I just enable LIN in contexts where I know it is useful. Check their sections for further details:

(prot-emacs-builtin-package 'lin
  (setq lin-foreground-override nil)

  (dolist (hook '( elfeed-search-mode-hook notmuch-search-mode-hook
                   log-view-mode-hook package-menu-mode-hook
                   ibuffer-mode-hook))
    (add-hook hook #'lin-mode)))

2.7. Typeface configurations

2.7.1. Font configurations (prot-fonts.el)

Any font I choose must conform with the following:

  • support Latin and Greek character sets;
  • be readable at small sizes and look okay at large sizes;
  • offer both roman and italic variants, preferably with corresponding bold weights;
  • not be too thin or at least have multiple weights from which to choose from;
  • not have too short of an x-height, which makes combinations of text and numbers or delimiters somewhat unbalanced;
  • not have a name that directly advertises some brand (e.g. "Helvetica" is fine as a name, even though a certain corporation tried to be identified with it; but "CORPORATION Sans" is not);
  • not be flamboyant by seeking to call too much attention to its details or by exaggerating some glyph shapes;
  • be equally readable against light and dark backdrops (for instance, bitmap fonts are not legible on a white background—too bad because I really like Terminus);
  • 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 cover my fairly demanding needs. Some look good at large point sizes. Others lack Greek characters. Some do not have italics… Getting it right is very difficult.

The only font I could use over the years was Hack, which builds on the skeleton of DejaVu Sans Mono. Though there are issues with the default version of Hack which I do not like, such as the default shape of the zero and one numbers, the small shape of the backtick character… Little things which keep annoying me. My custom build of Hack uses some alternative glyphs from a variety of sources that alleviate most of those problems (though it obviously cannot alter the overall character of the typeface).

Moving on to my configurations, prot-fonts.el covers my elaborate typeface requirements. At its heart is prot-fonts-typeface-sets-alist. It contains associations of property lists (an alist of plists), each of which pertains to a give display type. For example, when I am doing a video presentation, I require font configurations that differ from what I normally use. So I have a large key in the alist to accommodate that particular need. Those associations provide granular control over font attributes, as they specify both mono and proportionately spaced font families, each with their corresponding weights and heights.

The command prot-fonts-set-fonts prompts with completion for a display type. It gives options like small, regular, large, which are drawn from the prot-fonts-typeface-sets-alist (more on completion in the section Completion framework and extras). When called from Lisp it expects a DISPLAY argument, which is a symbol that matches the car of a cons cell in the aforementioned alist.

prot-fonts-fonts-per-monitor is not useful with my current hardware, as I had designed it when I had a laptop (see What hardware do you use?). Still, the idea is interesting and I keep it around in case someone has a need for it. It sets the appropriate font family and size depending on whether the maximum width of the monitor is below a certain threshold defined in prot-fonts-max-small-resolution-width. So it will use the two first presets defined in the aforementioned alist, with the assumption that the first is the smallest, laptop-specific one.

As noted above, prot-fonts-typeface-sets-alist associates a list of typefaces with desired weights, including for their bold variation. This is useful when the active theme (e.g. my Modus themes) is designed in a way that parameterises or abstracts the exact value of a bold weight, by calling :inherit bold instead of specifying :weight bold. The former applies the attributes of the bold face, whereas the latter sets the weight at a constant value. When the theme is designed that way, we can configure the :weight of the bold face to, e.g., extrabold and have that propagate across all the faces that the theme defines.

Now a few general notes about setting fonts in Emacs.

While there are many ways to define a baseline or fallback font family, I find that the most consistent one in terms of overall configuration is to do it at the "face" level (read the docstring of set-face-attribute). Faces are understood as the domain of themes, though themes are just Elisp programs so there is no real distinction here and it is perfectly fine to have one program define some properties of a face while another specifies a few others. The key is to make those complementary. Put concretely, prot-fonts.el sets properties such as :family, while my themes handle things like colours.

To appreciate this point, consider that in Emacs parlance a "face" signifies a construct that bundles up together several display attributes, such as a foreground and a background colour, as well as all typography-related values. Multiple assignments are stacked, unless one new attribute explicitly overrules an existing one.

There are three faces that are of immediate interest to prot-fonts.el: the default, variable-pitch, and fixed-pitch. The first is the Emacs frame's main typeface, the second specifies a proportionately spaced font, and the third does the same for a monospaced family. There are various scenaria where text on display needs to be rendered in variable-pitch, while others must remain in fixed-pitch. For example, let org-mode inline code be presented in its monospaced font while using a variable width typeface for the main text, or just use variable-pitch for User Interface elements such as the mode line to economise on spacing, and so on. Again, my Modus themes cover that niche out-of-the-box (and offer relevant customisation options). We call this a "mixed font" setup: the easiest way to get to it is to enable variable-pitch-mode (read: Extensions for "focus mode" (prot-logos.el)).

Relevant publications of mine that are not part of this document:

;;; Font configurations (prot-fonts.el)
(prot-emacs-builtin-package 'prot-fonts
  ;; This is defined in Emacs C code: it belongs to font settings.
  (setq x-underline-at-descent-line t)

  ;; And this is for Emacs28.
  (setq-default text-scale-remap-header-line t)

  ;; Make sure to read the `prot-fonts-typeface-sets-alist' doc string,
  ;; as it explains what those property lists should contain.
  ;;
  ;; The version of "Hack" that I custom is a custom build on mine:
  ;; <https://gitlab.com/protesilaos/hack-font-mod>.  Same principle for
  ;; Iosevka: <https://gitlab.com/protesilaos/iosevka-comfy>.
  (setq prot-fonts-typeface-sets-alist
        '((small . ( :fixed-pitch-family "Hack"
                     :fixed-pitch-regular-weight regular
                     :fixed-pitch-heavy-weight bold
                     :fixed-pitch-height 75
                     :fixed-pitch-line-spacing 1
                     :variable-pitch-family "DejaVu Serif"
                     :variable-pitch-height 1.0
                     :variable-pitch-regular-weight normal))

          (regular . ( :fixed-pitch-family "Hack"
                       :fixed-pitch-regular-weight regular
                       :fixed-pitch-heavy-weight bold
                       :fixed-pitch-height 90
                       :fixed-pitch-line-spacing nil
                       :variable-pitch-family "DejaVu Serif"
                       :variable-pitch-height 1.0
                       :variable-pitch-regular-weight normal))

          (large . ( :fixed-pitch-family "Hack"
                     :fixed-pitch-regular-weight normal
                     :fixed-pitch-heavy-weight bold
                     :fixed-pitch-height 130
                     :fixed-pitch-line-spacing nil
                     :variable-pitch-family "DejaVu Serif"
                     :variable-pitch-height 1.0
                     :variable-pitch-regular-weight normal))

          (large-alt . ( :fixed-pitch-family "Iosevka Comfy"
                         :fixed-pitch-regular-weight book
                         :fixed-pitch-heavy-weight extrabold
                         :fixed-pitch-height 135
                         :fixed-pitch-line-spacing nil
                         :variable-pitch-family "Noto Serif"
                         :variable-pitch-height 1.0
                         :variable-pitch-regular-weight normal))))

  ;; TODO 2021-08-27: I no longer have a laptop.  Those configurations
  ;; are not relevant, but I keep them around as the idea is still good.

  ;; The value of `prot-fonts-laptop-desktop-keys-list' becomes '(small
  ;; regular) based on the car of the first two cons cells found in
  ;; `prot-fonts-typeface-sets-alist'.  The assumption is that those
  ;; contain sets from smaller to larger display types.
  (setq prot-fonts-laptop-desktop-keys-list
        (prot-fonts-laptop-desktop-keys))

  ;; This is the breakpoint, in pixels, for determining whether we are
  ;; on the small or large screen layout.  The number here is my
  ;; laptop's screen width, while it expands beyond that when I connect
  ;; it to an external monitor (how I normally set it up on my desk).
  (setq prot-fonts-max-small-resolution-width 1366)

  ;; And this just sets the right font depending on whether my laptop is
  ;; connected to an external monitor or not.
  (prot-fonts-fonts-per-monitor)

  ;; See theme section for this hook and also read the doc string of
  ;; `prot-fonts-restore-last'.
  (add-hook 'modus-themes-after-load-theme-hook #'prot-fonts-restore-last)

  (define-key global-map (kbd "C-c f") #'prot-fonts-set-fonts))

This is the source code of prot-fonts.el (you can always find the file if you directly clone my dotfiles' repo).

;;; prot-fonts.el --- Font configurations for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; This set of configurations pertains to my font settings, for use in
;; my Emacs setup: https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

;;; Customisation options
(defgroup prot-fonts ()
  "Font-related configurations for my dotemacs."
  :group 'font)

;; NOTE: "Hack" and "Iosevka Comfy" are personal builds of Hack and
;; Iosevka respectively:
;;
;; 1. https://gitlab.com/protesilaos/hack-font-mod
;; 2. https://gitlab.com/protesilaos/iosevka-comfy
(defcustom prot-fonts-typeface-sets-alist
  '((laptop  . ( :fixed-pitch-family "Hack"
                 :fixed-pitch-regular-weight normal
                 :fixed-pitch-heavy-weight bold
                 :fixed-pitch-height 90
                 :fixed-pitch-line-spacing 1
                 :variable-pitch-family "DejaVu Sans"
                 :variable-pitch-height 1.0
                 :variable-pitch-regular-weight normal))

    (desktop . ( :fixed-pitch-family "Iosevka Comfy"
                 :fixed-pitch-regular-weight book
                 :fixed-pitch-heavy-weight extrabold
                 :fixed-pitch-height 105
                 :fixed-pitch-line-spacing nil
                 :variable-pitch-family "FiraGO"
                 :variable-pitch-height 0.95
                 :variable-pitch-regular-weight normal))

    (video   . ( :fixed-pitch-family "Iosevka Comfy"
                 :fixed-pitch-regular-weight normal
                 :fixed-pitch-heavy-weight bold
                 :fixed-pitch-height 135
                 :fixed-pitch-line-spacing nil
                 :variable-pitch-family "Source Sans Pro"
                 :variable-pitch-height 1.0
                 :variable-pitch-regular-weight normal)))
  "Alist of desired typeface properties.

The car of each cons cell is an arbitrary key that broadly
describes the display type.  We use 'laptop', 'desktop' though
any symbol will do, e.g. 'video'.

The cdr is a plist that specifies the typographic properties of
fixed-pitch and variable-pitch fonts.  A few notes about those
properties:

- We specify typographic properties both for the `fixed-pitch'
  and `variable-pitch' faces.  This allows us to be explicit
  about all font families that may be used by the active
  theme (Modus themes) under various circumstances (e.g. enabling
  `variable-pitch' for the UI, or using `variable-pitch-mode').

- A semibold weight can only be used by font families that have
  one.  Otherwise it is better to specify bold, in order to avoid
  any potential unpredictable behaviour.

- Never set the :variable-pitch-height to an absolute number
  because that will break the layout of `text-scale-adjust'.  Use
  a floating point instead, so that when the text scale is
  adjusted those expand or contract accordingly.

- An absolute height is only need for the `default' face, which
  we here designated as a fixed-pitch typeface (so the faces
  `fixed-pitch' and `default' share the same font family, though
  their role remains distinct).

- The line height applies to the entirety of the Emacs session.
  We declare it as :fixed-pitch-line-spacing because the face
  `default' starts with a fixed-pitch font family.

- No tests are performed to determined the presence of the font
  families specified herein.  It is assumed that those always
  exist.

It is recommended that the order of the cons cells follows from
the smallest to largest font heights, to simplify the process of
identifying the set that belongs to the small and larger display
respectively (see code of `prot-fonts-laptop-desktop-keys')."
  :group 'prot-fonts
  :type 'alist)

(defun prot-fonts-laptop-desktop-keys ()
  "List laptop and desktop fontsets.
The elements of the list are the cars of the first two cons cells
of `prot-fonts-laptop-desktop-keys-list'"
  (let ((sets (mapcar #'car prot-fonts-typeface-sets-alist)))
    (list (nth 0 sets) (nth 1 sets))))

(defcustom prot-fonts-laptop-desktop-keys-list
  (prot-fonts-laptop-desktop-keys) ; '(laptop desktop)
  "Symbols for `prot-fonts-fonts-per-monitor'.
This is a list whose first item denotes the smallest desirable
entry in `prot-fonts-typeface-sets-alist' for use on a laptop or
just smaller monitor, while the second points to a larger
display's key in that same alist.

The helper function `prot-fonts-laptop-desktop-keys' picks the
first two entries in `prot-fonts-typeface-sets-alist'."
  :group 'prot-fonts
  :type 'list)

(defcustom prot-fonts-max-small-resolution-width 1366
  "Maximum width for use in `prot-fonts-fonts-per-monitor'.
If the screen width is higher than this value (measuring pixels),
then the larger fonts will be used, as specified by the nth 1 of
`prot-fonts-laptop-desktop-keys-list'.  Otherwise the smaller
fonts, else nth 0, are applied."
  :group 'prot-fonts
  :type 'integer)

(defvar prot-fonts--font-display-hist '()
  "History of inputs for display-related font associations.")

(defun prot-fonts--set-face-attribute (face family &optional weight height)
  "Set FACE font to FAMILY, with optional HEIGHT and WEIGHT."
  (let* ((u (if (eq face 'default) 100 1.0))
         (h (or height u))
         (w (or weight 'normal)))
    ;; ;; Read this: <https://debbugs.gnu.org/cgi/bugreport.cgi?bug=45920>
    ;; ;; Hence why the following fails.  Keeping it for posterity...
    ;; (set-face-attribute face nil :family family :weight w :height h)
    (if (eq (face-attribute face :weight) w)
          (internal-set-lisp-face-attribute face :family family 0)
      (internal-set-lisp-face-attribute face :weight w 0)
      (internal-set-lisp-face-attribute face :family family 0)
      (internal-set-lisp-face-attribute face :weight w 0))
    (internal-set-lisp-face-attribute face :height h 0)))

(defun prot-fonts--set-fonts-prompt ()
  "Promp for font set (used by `prot-fonts-set-fonts')."
  (let ((def (nth 1 prot-fonts--font-display-hist)))
    (completing-read
     (format "Select font set for DISPLAY [%s]: " def)
     (mapcar #'car prot-fonts-typeface-sets-alist)
     nil t nil 'prot-fonts--font-display-hist def)))

(defvar prot-fonts-set-typeface-hook nil
  "Hook that runs after `prot-fonts-set-fonts'.")

(defvar prot-fonts--current-spec nil
  "Current font set in `prot-fonts-typeface-sets-alist'.")

;;;###autoload
(defun prot-fonts-set-fonts (display)
  "Set fonts based on font set associated with DISPLAY.

DISPLAY is a symbol that represents the car of a cons cell in
`prot-fonts-typeface-sets-alist'."
  (interactive (list (prot-fonts--set-fonts-prompt)))
  (if window-system
      (let* ((fonts (if (stringp display) (intern display) display))
             (properties (alist-get fonts prot-fonts-typeface-sets-alist))
             (fixed-pitch-family (plist-get properties :fixed-pitch-family))
             (fixed-pitch-height (plist-get properties :fixed-pitch-height))
             (fixed-pitch-regular-weight (plist-get properties :fixed-pitch-regular-weight))
             (fixed-pitch-heavy-weight (plist-get properties :fixed-pitch-heavy-weight))
             (fixed-pitch-line-spacing (plist-get properties :fixed-pitch-line-spacing))
             (variable-pitch-family (plist-get properties :variable-pitch-family))
             (variable-pitch-height (plist-get properties :variable-pitch-height))
             (variable-pitch-regular-weight (plist-get properties :variable-pitch-regular-weight)))
        (prot-fonts--set-face-attribute
         'default fixed-pitch-family fixed-pitch-regular-weight fixed-pitch-height)
        (prot-fonts--set-face-attribute
         'fixed-pitch fixed-pitch-family fixed-pitch-regular-weight)
        (prot-fonts--set-face-attribute
         'variable-pitch variable-pitch-family variable-pitch-regular-weight variable-pitch-height)
        (set-face-attribute 'bold nil :weight fixed-pitch-heavy-weight)
        (setq-default line-spacing fixed-pitch-line-spacing)
        (add-to-history 'prot-fonts--font-display-hist (format "%s" display))
        (setq prot-fonts--current-spec (format "%s" display))
        (run-hooks 'prot-fonts-set-typeface-hook))
    (error "Not running a graphical Emacs; cannot set fonts")))

(defun prot-fonts-restore-last ()
  "Restore last fontset.
This is necessary when/if changes to face specs alter some
typographic properties.  For example, when switching themes the
:weight of the `bold' face will be set to whatever the theme
specifies, typically 'bold', which is not what we always have on
our end."
  (let ((ultimate (nth 0 prot-fonts--font-display-hist))
        (penultimate (nth 1 prot-fonts--font-display-hist)))
    (if (string= ultimate prot-fonts--current-spec)
        (prot-fonts-set-fonts ultimate)
      (prot-fonts-set-fonts penultimate))))

(defun prot-fonts--display-type-for-monitor (&optional smaller larger)
  "Determine typeface specs based on monitor width.
Optional SMALLER and LARGER are two keys that point to entries in
`prot-fonts-typeface-sets-alist'.  The default uses the relevant
keys from `prot-fonts-laptop-desktop-keys-list'."
  (let* ((keys prot-fonts-laptop-desktop-keys-list)
         (face-specs prot-fonts-typeface-sets-alist)
         (small (or smaller (nth 0 keys)))
         (large (or larger (nth 1 keys)))
         (max-width prot-fonts-max-small-resolution-width)
         (spec (if (<= (display-pixel-width) max-width)
                   small
                 large)))
    (unless (assoc spec face-specs)
      (error (concat "Key <<%s>> in `prot-fonts-laptop-desktop-keys-list' "
                     "does not reference anything in "
                     "`prot-fonts-typeface-sets-alist'")
             spec))
    spec))

;;;###autoload
(defun prot-fonts-fonts-per-monitor ()
  "Use font settings based on screen size.
The breakpoint is `prot-fonts-max-small-resolution-width', while
`prot-fonts-laptop-desktop-keys-list' contains the keys of the
two font sets to be used: its first element should point at
smaller fonts than the second element."
  (when window-system
    (let ((display (prot-fonts--display-type-for-monitor)))
      (prot-fonts-set-fonts display))))

(provide 'prot-fonts)
;;; prot-fonts.el ends here

2.7.2. 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.8. Repeatable key chords (repeat-mode)

Emacs28 comes with a built-in utility for repeating selected commands in a more convenient fashion. Once repeat-mode is enabled, a key binding or chord that invokes a command successfully can be repeated by typing in its tail or whatever the developer specifies. So other-window may be used like this: C-x o, o, o to switch three windows, instead of C-x o, C-x o, C-x o.

This is achieved by furnishing transient keymaps that get activated right after the command they belong to and only once the command in question has been added to the repeat-map. Here is a sample from window.el:

(defvar resize-window-repeat-map
  (let ((map (make-sparse-keymap)))
    ;; Standard keys:
    (define-key map "^" 'enlarge-window)
    (define-key map "}" 'enlarge-window-horizontally)
    (define-key map "{" 'shrink-window-horizontally) ; prot note: those three are C-x KEY
    ;; Additional keys:
    (define-key map "v" 'shrink-window) ; prot note: this is not bound by default
    map)
  "Keymap to repeat window resizing commands.  Used in `repeat-mode'.")
(put 'enlarge-window 'repeat-map 'resize-window-repeat-map)
(put 'enlarge-window-horizontally 'repeat-map 'resize-window-repeat-map)
(put 'shrink-window-horizontally 'repeat-map 'resize-window-repeat-map)
(put 'shrink-window 'repeat-map 'resize-window-repeat-map)

Once the keymap exists and its commands are in the repeat-map, such as with the above example, we do not have to recreate the entire setup if all we need is to change key bindings: we just have to rebind the commands to where it makes sense for us. I actually do this for resize-window-repeat-map (Window rules and basic tweaks (window.el)).

At any rate, all we need here is to activate repeat-mode and then implement the repetition mechanism wherever we want.

The repeat command is bound by default to C-x z, with s-z serving as my alias for it (read What is the meaning of the `s-KEY' bindings?). With the two variables I set in the following configurations, I make it so that subsequent repetitions require only hitting another z. Depending on what you do, a repeat can save you from multiple key presses. For more demanding tasks you are better off with keyboard macros.

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 any line 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, which is affected by set-mark-command-repeat-pop, 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 set-mark-command-repeat-pop 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' registers or bookmarks.

;;; Repeatable key chords (repeat-mode)
(prot-emacs-builtin-package 'repeat
  (setq repeat-on-final-keystroke t)
  (setq set-mark-command-repeat-pop t)

  (repeat-mode 1))

2.9. Handle performance for very long lines (so-long.el)

When you visit a file with very long lines, such as a minified javascript on a web page's source, Emacs will have trouble fontifying everything properly and performance will suffer as a result. We can prevent Emacs from even attempting to deal with such longs lines by enabling the built-in global-so-long-mode (for Emacs versions >= 27). It allows the active major mode to gracefully adapt to buffers with very long lines. What "very long" means is, of course, configurable: invoke M-x find-library and search for so-long to find several customisation options (declared with defcustom). Personally, I find that the defaults require no further intervention from my part.

;;; Handle performance for very long lines (so-long.el)
(prot-emacs-builtin-package 'so-long
  (global-so-long-mode 1))

3. Selection candidates and search methods

3.1. Completion framework and extras

Unlike the desktop metaphor, the optimal way to use 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, as we do elsewhere in this document.

To get a sense of my current completion framework, watch my presentation on Default Emacs completion and extras (2021-01-06). There have been some changes since then, but the core idea stands.

3.1.1. Orderless completion style (and prot-orderless.el)

The, dare I say, sublime “orderless” package is developed by Omar Antolín Camarena. 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). 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 for most part improve upon—the completion styles that come built into Emacs, adding to them the powerful out-of-order capability.

All we do here is set up Orderless. The orderless completion style is appended to the minibuffer's customisation option for completion-styles. That is defined in Minibuffer configurations and extras.

My prot-orderless.el contains the few minor tweaks I introduce (full code further below).

  1. It defines three style dispatchers. Those are single characters that acquire a special meaning while at the end of a given input:
    • With the equals sign appended to a sequence of characters, we call prot-orderless-literal-dispatcher which instructs orderless to match that sequence as a literal string.
    • A comma at the end of a string of characters treats that group as an initialism, per prot-orderless-initialism-dispatcher.
    • While a tilde (prot-orderless-flex-dispatcher) makes it makes it a flex match.
  2. prot-orderless-with-styles is a function that changes the default pattern-matching styles on a per-command basis. The idea is to use a certain style for most completion sessions, but prioritise an alternative when needed. I use this with some Consult commands (Enhanced minibuffer commands (consult.el and prot-consult.el)). The prot-orderless-default-styles and prot-orderless-alternative-styles variables are designed for this particular task.
;;; Orderless completion style (and prot-orderless.el)
(prot-emacs-builtin-package 'prot-orderless
  (setq prot-orderless-default-styles
        '(orderless-prefixes
          orderless-strict-leading-initialism
          orderless-regexp))
  (setq prot-orderless-alternative-styles
        '(orderless-literal
          orderless-prefixes
          orderless-strict-leading-initialism
          orderless-regexp)))

(prot-emacs-elpa-package 'orderless
  (setq orderless-component-separator " +")
  (setq orderless-matching-styles prot-orderless-default-styles)
  (setq orderless-style-dispatchers
        '(prot-orderless-literal-dispatcher
          prot-orderless-initialism-dispatcher
          prot-orderless-flex-dispatcher))
  ;; SPC should never complete: use it for `orderless' groups.
  (let ((map minibuffer-local-completion-map))
    (define-key map (kbd "SPC") nil)
    (define-key map (kbd "?") nil)))

These are the contents of the prot-orderless.el library (get the file from my dotfiles' repo (as with all my Elisp code)):

;;; prot-orderless.el --- Extensions for Orderless -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Extensions for the Orderless completion style for use in my Emacs
;; setup: <https://protesilaos.com/emacs/dotemacs/>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(defgroup prot-orderless ()
  "Tweaks for the Orderless completion style."
  :group 'minibuffer)

(defcustom prot-orderless-default-styles
  '(orderless-flex
    orderless-strict-leading-initialism
    orderless-regexp
    orderless-prefixes
    orderless-literal)
  "List that should be assigned to `orderless-matching-styles'."
  :type 'list
  :group 'prot-orderless)

(defcustom prot-orderless-alternative-styles
  '(orderless-literal
    orderless-prefixes
    orderless-strict-leading-initialism
    orderless-regexp)
  "Alternative list for `orderless-matching-styles'.

Unlike `prot-orderless-default-styles', this variable is intended
for use on a case-by-case basis, with the help of the function
`prot-orderless-with-styles'."
  :type 'list
  :group 'prot-orderless)

(defun prot-orderless-literal-dispatcher (pattern _index _total)
  "Literal style dispatcher using the equals sign as a suffix.
It matches PATTERN _INDEX and _TOTAL according to how Orderless
parses its input."
  (when (string-suffix-p "=" pattern)
    `(orderless-literal . ,(substring pattern 0 -1))))

(defun prot-orderless-initialism-dispatcher (pattern _index _total)
  "Leading initialism  dispatcher using the comma suffix.
It matches PATTERN _INDEX and _TOTAL according to how Orderless
parses its input."
  (when (string-suffix-p "," pattern)
    `(orderless-strict-leading-initialism . ,(substring pattern 0 -1))))

(defun prot-orderless-flex-dispatcher (pattern _index _total)
  "Flex  dispatcher using the tilde suffix.
It matches PATTERN _INDEX and _TOTAL according to how Orderless
parses its input."
  (when (string-suffix-p "~" pattern)
    `(orderless-flex . ,(substring pattern 0 -1))))

(defvar orderless-matching-styles)

;;;###autoload
(defun prot-orderless-with-styles (cmd &optional styles)
  "Call CMD with optional orderless STYLES.

STYLES is a list of pattern matching methods that is passed to
`orderless-matching-styles'.  Its fallback value is that of
`prot-orderless-alternative-styles'."
  (let ((orderless-matching-styles (or styles prot-orderless-alternative-styles))
        (this-command cmd))
    (call-interactively cmd)))

(provide 'prot-orderless)
;;; prot-orderless.el ends here

3.1.2. Completion annotations (marginalia)

This is a utility jointly developed by Daniel Mendler and Omar Antolín Camarena that provides annotations to completion candidates. It is meant to be framework-agnostic, so it works with Selectrum, Icomplete vertical, Embark's completions buffer, as well as the default completions' buffer (as of 2021-04-02, the latter is my choice for visualising the standard minibuffer completion candidates—refer to Minibuffer configurations and my extras (mct.el)).

;;; Completion annotations (marginalia)
(prot-emacs-elpa-package 'marginalia
  (setq marginalia-max-relative-age 0)  ; time is absolute here!
  (marginalia-mode 1))

3.1.3. Minibuffer configurations and my extras (mct.el)

UPDATE 2021-10-23: Watch my video demonstration of the package I wrote Minibuffer and Completions in Tandem (mct.el).

The minibuffer is the epicentre of extended interactivity with all sorts of Emacs workflows: to select a buffer, open a file, provide an answer to some prompt, such as a number, regular expression, password, and so on.

Emacs has built-in capabilities to perform two distinct tasks related to such interactions:

Narrowing
Use pattern matching algorithms to limit the list of choices (known as "candidates" or "completion candidates") to those matching the given input. There are several pattern matching styles already built-in, while we can opt to extend them further.
Selecting
Visualise the list of completion candidates and pick an item out of it using regular motions or concomitant extras. By default, Emacs visualises results in a special *Completions* buffer, which does not have lots of features and is not particularly pleasing to use.

There are third party completion frameworks that accomplish both of the aforementioned, such as Ivy and Helm. While others focus on the latter, namely, Selectrum, Vertico, and the built-in Icomplete. Then, there is also Embark which has a facility to display completion candidates in a live updating buffer (Embark is not limited to task—read the details of Extended minibuffer actions and more (embark.el and prot-embark.el)).

I have used Ivy and Icomplete in the past, though I felt that the best experience was with Embark. However, as of 2021-04-02 I realised that Emacs version 28 comes with a built-in way to render *Completions* in a vertical list, one item per line. So equipped with my knowledge of extending Embark, I implemented a layer of interactivity that, I feel, is just as good as what other frameworks provide. As of 2021-10-22 this is its own package, called "Minibuffer and Completions in Tandem", else MCT (or variants). The technicalities are in the mct.el file, which is reproduced after the following code block.

Read the official manual of MCT as it covers its basics, explains its key bindings, and shows how to configure it further.

Finally, here is an overview of the settings covered herein:

Completion styles

I rely on a mixture of built-in styles as well as the external orderless package by Omar Antolín Camarena. Orderless is placed last on the list because simpler searches work fine with the other styles. Orderless is better suited for complex pattern matching. The way completion styles work, when one style cannot match anything, Emacs tries the next one on the list, until one of them yields results. As such, it is easy to activate Orderless on demand, either by separate input groups by spaces or passing one of the style dispatchers that are acceptable (read the Orderless completion style).

For file queries in particular, there exists a niche functionality in the built-in initials and partial-completion styles to navigate abbreviated paths. Here is an example with the latter: you can type ~/.l/s/fo which will match ~/.local/share/fonts (combine that with the file shadowing mentioned above, for a minimalist, decluttered flow). The variable completion-category-overrides can control the standard option of completion-styles on a per-category basis.

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. Or simply call an M-x command while in the midst of a minibuffer session. 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 recursion indicator, represented as a number, next to the minibuffer prompt, if a recursive edit is in progress (also check Mode line recursion indicators).

Also check my setup for the Minibuffer history (savehist-mode). 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.

;;; Minibuffer configurations and my extras (mct.el)
(prot-emacs-builtin-package 'minibuffer
  ;; NOTE 2021-10-25: I am adding `basic' because it works better as a
  ;; default for some contexts.  Read:
  ;; <https://debbugs.gnu.org/cgi/bugreport.cgi?bug=50387>.
  (setq completion-styles
        '(basic substring initials flex partial-completion orderless))
  (setq completion-category-overrides
        '((file (styles . (basic partial-completion orderless)))))
  (setq completion-cycle-threshold 2)
  (setq completion-flex-nospace nil)
  (setq completion-pcm-complete-word-inserts-delimiters nil)
  (setq completion-pcm-word-delimiters "-_./:| ")
  (setq completion-ignore-case t)
  (setq completions-detailed t)
  (setq-default case-fold-search t)   ; For general regexp

  ;; Grouping of completions for Emacs 28
  (setq completions-group t)
  (setq completions-group-sort nil)
  (setq completions-group-format
        (concat
         (propertize "    " 'face 'completions-group-separator)
         (propertize " %s " 'face 'completions-group-title)
         (propertize " " 'face 'completions-group-separator
                     'display '(space :align-to right))))

  (setq read-buffer-completion-ignore-case t)
  (setq read-file-name-completion-ignore-case t)

  (setq enable-recursive-minibuffers t)
  (setq read-answer-short t) ; also check `use-short-answers' for Emacs28
  (setq resize-mini-windows t)
  (setq minibuffer-eldef-shorten-default t)

  (setq echo-keystrokes 0.25)           ; from the C source code

  ;; Do not allow the cursor to move inside the minibuffer prompt.  I
  ;; got this from the documentation of Daniel Mendler's Vertico
  ;; package: <https://github.com/minad/vertico>.
  (setq minibuffer-prompt-properties
        '(read-only t cursor-intangible t face minibuffer-prompt))

  (add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)

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

  (let ((map minibuffer-local-must-match-map))
    ;; I use this prefix for other searches
    (define-key map (kbd "M-s") nil)))

;; Source: <https://gitlab.com/protesilaos/mct.el>.
;; Manual: <https://protesilaos.com/emacs/mct>.
(prot-emacs-builtin-package 'mct
  (setq mct-remove-shadowed-file-names t) ; when `file-name-shadow-mode' is enabled
  (setq mct-hide-completion-mode-line t)
  (setq mct-show-completion-line-numbers nil)
  (setq mct-apply-completion-stripes nil)
  (setq mct-minimum-input 3)
  (setq mct-live-update-delay 0.6)
  (setq mct-completion-blocklist nil)
  (setq mct-completion-passlist
        '( embark-prefix-help-command Info-goto-node
           Info-index Info-menu vc-retrieve-tag
           prot-bookmark-cd-bookmark
           prot-bongo-playlist-insert-playlist-file))

  ;; You can place the Completions' buffer wherever you want, by
  ;; following the syntax of `display-buffer-alist' (check elsewhere in
  ;; this file).  For example, try this:

  ;; (setq mct-display-buffer-action
  ;;       (quote ((display-buffer-reuse-window
  ;;                display-buffer-in-side-window)
  ;;               (side . left)
  ;;               (slot . 99)
  ;;               (window-width . 0.3))))

  (mct-mode 1)

  (define-key global-map (kbd "C-x :") #'mct-focus-mini-or-completions))

And here is mct.el (from my dotfiles' repo):

;;; mct.el --- Minibuffer and Completions in Tandem -*- lexical-binding: t -*-

;; Copyright (C) 2021  Free Software Foundation, Inc.

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://gitlab.com/protesilaos/mct
;; Version: 0.2.0
;; Package-Requires: ((emacs "27.1"))

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; MCT enhances the default Emacs completion.  It makes the minibuffer
;; and Completions' buffer work together and look like a vertical
;; completion UI.
;;
;; Read the documentation for basic usage and configuration.

;;; Code:

;;;; General utilities

(defgroup mct ()
  "Extensions for the minibuffer."
  :group 'minibuffer)

(defcustom mct-completion-windows-regexp
  "\\`\\*Completions.*\\*\\'"
  "Regexp to match window names with completion candidates.
Used by `mct--get-completion-window'."
  :type 'string
  :group 'mct)

(defcustom mct-remove-shadowed-file-names nil
  "Delete shadowed parts of file names.

For example, if the user types ~/ after a long path name,
everything preceding the ~/ is removed so the interactive
selection process starts again from the user's $HOME.

Only works when variable `file-name-shadow-mode' is non-nil."
  :type 'boolean
  :group 'mct)

(defcustom mct-hide-completion-mode-line nil
  "Do not show a mode line in the Completions' buffer."
  :type 'boolean
  :group 'mct)

(defcustom mct-show-completion-line-numbers nil
  "Display line numbers in the Completions' buffer."
  :type 'boolean
  :group 'mct)

(defcustom mct-apply-completion-stripes nil
  "Display alternating backgrounds the Completions' buffer."
  :type 'boolean
  :group 'mct)

(defcustom mct-live-completion t
  "Control auto-display and live-update of Completions' buffer.

When nil, the user has to manually request completions, using the
regular activating commands.  The Completions' buffer is never
updated live to match user input.  Updating has to be handled
manually.  This is like the out-of-the-box minibuffer completion
experience.

When set to the value `visible', the Completions' buffer is live
updated only if it is visible.  The actual display of the
completions is still handled manually.  For this reason, the
`visible' style does not read the `mct-minimum-input', meaning
that it will always try to live update the visible completions,
regardless of input length.

When non-nil (the default), the Completions' buffer is
automatically displayed once the `mct-minimum-input' is met and
is hidden once the input drops below that threshold.  While
visible, the buffer is updated live to match the user input.

Note that every function in the `mct-completion-passlist' ignores
this option altogether.  This means that every such command will
always show the Completions' buffer automatically and will always
update its contents live.  Same principle for every function
declared in the `mct-completion-blocklist', which will always
disable both the automatic display and live updating of the
Completions' buffer."
  :type '(choice
          (const :tag "Disable live-updating" nil)
          (const :tag "Enable live-updating" t)
          (const :tag "Live update only visible Completions" 'visible))
  :group 'mct)

(defcustom mct-minimum-input 3
  "Live update completions when input is >= N.

Setting this to a value greater than 1 can help reduce the total
number of candidates that are being computed."
  :type 'natnum
  :group 'mct)

(defcustom mct-live-update-delay 0.3
  "Delay in seconds before updating the Completions' buffer.

Set this to 0 to disable the delay."
  :type 'number
  :group 'mct)

(defcustom mct-completion-blocklist nil
  "Functions that disable live completions.
This means that they ignore `mct-live-completion'.  They do not
automatically display the Completions' buffer, nor do they update
it to match user input.

The Completions' buffer can still be accessed with commands that
place it in a window (such as `mct-list-completions-toggle',
`mct-switch-to-completions-top').

A less drastic measure is to set `mct-minimum-input' to an
appropriate value."
  :type '(repeat symbol)
  :group 'mct)

(defcustom mct-completion-passlist nil
  "Functions that do live updating of completions from the start.
This means that they ignore the value of `mct-live-completion'
and the `mct-minimum-input'.  They also bypass any possible delay
introduced by `mct-live-update-delay'."
  :type '(repeat symbol)
  :group 'mct)

(defcustom mct-display-buffer-action
  '((display-buffer-reuse-window display-buffer-at-bottom))
  "The action used to display the Completions' buffer.

The value has the form (FUNCTION . ALIST), where FUNCTIONS is
either an \"action function\" or a possibly empty list of action
functions.  ALIST is a possibly empty \"action alist\".

Sample configuration:

    (setq mct-display-buffer-action
          (quote ((display-buffer-reuse-window
                   display-buffer-in-side-window)
                  (side . left)
                  (slot . 99)
                  (window-width . 0.3))))

See Info node `(elisp) Displaying Buffers' for more details
and/or the documentation string of `display-buffer'."
  :type '(cons (choice (function :tag "Display Function")
                       (repeat :tag "Display Functions" function))
               alist)
  :group 'mct)

(defcustom mct-completions-format 'one-column
  "The appearance and sorting used by `mct-mode'.
See `completions-format' for possible values.

NOTE that setting this option with `setq' requires a restart of
`mct-mode'."
  :set (lambda (var val)
         (when (bound-and-true-p mct-mode)
           (setq completions-format val))
         (set var val))
  :type '(choice (const horizontal) (const vertical) (const one-column))
  :group 'mct)

;;;; Completion metadata

(defun mct--minibuffer-field-beg ()
  "Determine beginning of completion in the minibuffer."
  (if-let ((window (active-minibuffer-window)))
      (with-current-buffer (window-buffer window)
        (minibuffer-prompt-end))
    (nth 0 completion-in-region--data)))

(defun mct--minibuffer-field-end ()
  "Determine end of completion in the minibuffer."
  (if-let ((window (active-minibuffer-window)))
      (with-current-buffer (window-buffer window)
        (point-max))
    (nth 1 completion-in-region--data)))

(defun mct--completion-category ()
  "Return completion category."
  (when-let ((window (active-minibuffer-window)))
    (with-current-buffer (window-buffer window)
      (let* ((beg (mct--minibuffer-field-beg))
             (md (completion--field-metadata beg)))
        (alist-get 'category (cdr md))))))

;;;; Basics of intersection between minibuffer and Completions' buffer

;; TODO 2021-11-16: Is there a better way to check that the current
;; command does not do completion?  This is fragile.
(defvar mct--no-complete-functions
  '( eval-expression query-replace query-replace-regexp
     isearch-forward isearch-backward
     isearch-forward-regexp isearch-backward-regexp)
  "List of functions that do not do completion.")

(define-obsolete-variable-alias
  'mct-hl-line 'mct-highlight-candidate "0.3.0")

(defface mct-highlight-candidate
  '((default :extend t)
    (((class color) (min-colors 88) (background light))
     :background "#b0d8ff" :foreground "#000000")
    (((class color) (min-colors 88) (background dark))
     :background "#103265" :foreground "#ffffff")
    (t :inherit highlight))
  "Face for current candidate in the completions' buffer."
  :group 'mct)

(declare-function display-line-numbers-mode "display-line-numbers")

(defun mct--display-line-numbers ()
  "Set up line numbers for the completions' buffer.
Add this to `completion-list-mode-hook'."
  (when (and (derived-mode-p 'completion-list-mode)
             mct-show-completion-line-numbers)
    (display-line-numbers-mode 1)))

;; Thanks to Omar Antolín Camarena for recommending the use of
;; `cursor-sensor-functions' and the concomitant hook with
;; `cursor-censor-mode' instead of the dirty hacks I had before to
;; prevent the cursor from moving to that position where no completion
;; candidates could be found at point (e.g. it would break `embark-act'
;; as it could not read the topmost candidate when point was at the
;; beginning of the line, unless the point was moved forward).
(defun mct--clean-completions ()
  "Keep only completion candidates in the Completions."
  (with-current-buffer standard-output
    (let ((inhibit-read-only t))
      (goto-char (point-min))
      (delete-region (point-at-bol) (1+ (point-at-eol)))
      (insert (propertize " "
                          'cursor-sensor-functions
                          (list
                           (lambda (_win prev dir)
                             (when (eq dir 'entered)
                               (goto-char prev))))))
      (put-text-property (point-min) (point) 'invisible t))))

(defun mct--fit-completions-window ()
  "Fit Completions' buffer to its window."
  (when-let ((window (mct--get-completion-window)))
    (with-current-buffer (window-buffer window)
      (setq-local window-resize-pixelwise t))
    (fit-window-to-buffer window (floor (frame-height) 2) 1)))

(defun mct--input-string ()
  "Return the contents of the minibuffer as a string."
  (buffer-substring-no-properties (minibuffer-prompt-end) (point-max)))

(defun mct--minimum-input ()
  "Test for minimum requisite input for live completions.
See `mct-minimum-input'."
  (>= (length (mct--input-string)) mct-minimum-input))

;;;;; Live-updating Completions' buffer

;; Adapted from Omar Antolín Camarena's live-completions library:
;; <https://github.com/oantolin/live-completions>.
(defun mct--live-completions (&rest _)
  "Update the *Completions* buffer.
Meant to be added to `after-change-functions'."
  (when (minibufferp) ; skip if we've exited already
    (let ((while-no-input-ignore-events '(selection-request)))
      (while-no-input
        (if (or (mct--minimum-input)
                (eq mct-live-completion 'visible))
            (condition-case nil
                (save-match-data
                  (save-excursion
                    (goto-char (point-max))
                    (let ((inhibit-message t)
                          ;; don't ring the bell in `minibuffer-completion-help'
                          ;; when <= 1 completion exists.
                          (ring-bell-function #'ignore))
                      (mct--show-completions))))
              (quit (abort-recursive-edit)))
          (minibuffer-hide-completions))))))

(defun mct--live-completions-timer (&rest _)
  "Update Completions with `mct-live-update-delay'."
  (let ((delay mct-live-update-delay))
    (when (>= delay 0)
      (run-with-idle-timer delay nil #'mct--live-completions))))

(defun mct--live-completions-visible-timer (&rest _)
  "Update visible Completions' buffer."
  (when (window-live-p (mct--get-completion-window))
    (mct--live-completions-timer)))

(defun mct--setup-completions ()
  "Set up the completions' buffer."
  (unless (memq this-command mct--no-complete-functions)
    (cond
     ((memq this-command mct-completion-passlist)
      (setq-local mct-minimum-input 0)
      (setq-local mct-live-update-delay 0)
      (mct--show-completions)
      (add-hook 'after-change-functions #'mct--live-completions nil t))
     ((null mct-live-completion))
     ((not (memq this-command mct-completion-blocklist))
      (if (eq mct-live-completion 'visible)
          (add-hook 'after-change-functions #'mct--live-completions-visible-timer nil t)
        (add-hook 'after-change-functions #'mct--live-completions-timer nil t))))))

;;;;; Alternating backgrounds (else "stripes")

;; Based on `stripes.el' (maintained by Štěpán Němec) and the
;; `embark-collect-zebra-minor-mode' from Omar Antolín Camarena's
;; Embark:
;;
;; 1. <https://gitlab.com/stepnem/stripes-el>
;; 2. <https://github.com/oantolin/embark>
(defface mct-stripe
  '((default :extend t)
    (((class color) (min-colors 88) (background light))
     :background "#f0f0f0")
    (((class color) (min-colors 88) (background dark))
     :background "#191a1b"))
  "Face for alternating backgrounds in the Completions' buffer."
  :group 'mct)

(defun mct--remove-stripes ()
  "Remove `mct-stripe' overlays."
  (remove-overlays nil nil 'face 'mct-stripe))

(defun mct--add-stripes ()
  "Overlay alternate rows with the `mct-stripe' face."
  (when (derived-mode-p 'completion-list-mode)
    (mct--remove-stripes)
    (save-excursion
      (goto-char (point-min))
      (when (overlays-at (point)) (forward-line))
      (while (not (eobp))
        (condition-case nil
            (forward-line 1)
          (user-error (goto-char (point-max))))
        (unless (eobp)
          (let ((pt (point))
                (overlay))
            (condition-case nil
                (forward-line 1)
              (user-error (goto-char (point-max))))
            ;; We set the overlay this way and give it a low priority so
            ;; that `mct--highlight-overlay' and/or the active region
            ;; can override it.
            (setq overlay (make-overlay pt (point)))
            (overlay-put overlay 'face 'mct-stripe)
            (overlay-put overlay 'priority -100)))))))

;;;; Commands and helper functions

;; TODO 2021-11-17: We must `autoload' instead of `declare-function' for
;; things to work on Emacs 27.  Perhaps we should keep the latter but
;; add (eval-when-compile (require 'text-property-search))?  That should
;; work for packages, but not if we just `eval-buffer', right?

(autoload 'text-property-search-backward "text-property-search")
(autoload 'text-property-search-forward "text-property-search")
(autoload 'prop-match-beginning "text-property-search")
(autoload 'prop-match-end "text-property-search")

;; (declare-function text-property-search-backward "text-property-search" (property &optional value predicate not-current))
;; (declare-function text-property-search-forward "text-property-search" (property &optional value predicate not-current))
;; (declare-function prop-match-beginning "text-property-search" (cl-x))
;; (declare-function prop-match-end "text-property-search" (cl-x))

;; We need this to make things work on Emacs 27.
(defun mct--one-column-p ()
  "Test if we have a one-column view available."
  (and (eq completions-format 'one-column)
       (eq mct-completions-format 'one-column)
       (>= emacs-major-version 28)))

;;;;; Focus minibuffer and/or show completions

;;;###autoload
(defun mct-focus-minibuffer ()
  "Focus the active minibuffer."
  (interactive nil mct-mode)
  (when-let ((mini (active-minibuffer-window)))
    (select-window mini)))

(defun mct--get-completion-window ()
  "Find a live window showing completion candidates."
  (get-window-with-predicate
   (lambda (window)
     (string-match-p
      mct-completion-windows-regexp
      (buffer-name (window-buffer window))))))

(defun mct--show-completions ()
  "Show the completions' buffer."
  (let ((display-buffer-alist
         (cons (cons mct-completion-windows-regexp mct-display-buffer-action)
               display-buffer-alist)))
    (save-excursion (minibuffer-completion-help)))
  (mct--fit-completions-window))

;;;###autoload
(defun mct-focus-mini-or-completions ()
  "Focus the active minibuffer or the completions' window.

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
`mct-focus-minibuffer' and `switch-to-completions' in
succession.

What constitutes a completions' window is ultimately determined
by `mct-completion-windows-regexp'."
  (interactive nil mct-mode)
  (let* ((mini (active-minibuffer-window))
         (completions (mct--get-completion-window)))
    (cond
     ((and mini (not (minibufferp)))
      (select-window mini nil))
     ((and completions (not (eq (selected-window) completions)))
      (select-window completions nil)))))

;;;###autoload
(defun mct-list-completions-toggle ()
  "Toggle the presentation of the completions' buffer."
  (interactive nil mct-mode)
  (if (mct--get-completion-window)
      (minibuffer-hide-completions)
    (mct--show-completions)))

;;;;; Commands for file completion

;; Adaptation of `icomplete-fido-backward-updir'.
(defun mct-backward-updir ()
  "Delete char before point or go up a directory."
  (interactive nil mct-mode)
  (cond
   ((and (eq (char-before) ?/)
         (eq (mct--completion-category) 'file))
    (when (string-equal (minibuffer-contents) "~/")
      (delete-region (mct--minibuffer-field-beg) (mct--minibuffer-field-end))
      (insert (expand-file-name "~/"))
      (goto-char (line-end-position)))
    (save-excursion
      (goto-char (1- (point)))
      (when (search-backward "/" (point-min) t)
        (delete-region (1+ (point)) (point-max)))))
   (t (call-interactively 'backward-delete-char))))

;;;;; Cyclic motions between minibuffer and completions' buffer

(defun mct--completions-completion-p ()
  "Return non-nil if there is a completion at point."
  (let ((point (point)))
    ;; The `or' is for Emacs 27 where there were no completion--string
    ;; properties.
    (or (get-text-property point 'completion--string)
        (get-text-property point 'mouse-face))))

(defun mct--first-completion-point ()
  "Return the `point' of the first completion."
  (save-excursion
    (goto-char (point-min))
    (next-completion 1)
    (point)))

(defun mct--last-completion-point ()
  "Return the `point' of the last completion."
  (save-excursion
    (goto-char (point-max))
    (next-completion -1)
    (point)))

(defun mct--completions-line-boundary (boundary)
  "Determine if current line has reached BOUNDARY.
BOUNDARY is a line position at the top or bottom of the
Completions' buffer.  See `mct--first-completion-point' or
`mct--last-completion-point'.

This check only applies when `completions-format' is not assigned
a `one-column' value."
  (and (= (line-number-at-pos) (line-number-at-pos boundary))
       (not (mct--one-column-p))))

(defun mct--completions-no-completion-line-p (arg)
  "Check if ARGth line has a completion candidate."
  (save-excursion
    (vertical-motion arg)
    (eq 'completions-group-separator (get-text-property (point) 'face))))

(defun mct--switch-to-completions ()
  "Subroutine for switching to the completions' buffer."
  (unless (mct--get-completion-window)
    (mct--show-completions))
  (switch-to-completions))

(defun mct--restore-old-point-in-grid (line)
  "Restore old point in window if LINE is on its line."
  (unless (mct--one-column-p)
    (let (old-line old-point)
      (when-let ((window (mct--get-completion-window)))
        (setq old-point (window-old-point window)
              old-line (line-number-at-pos old-point))
        (when (= (line-number-at-pos line) old-line)
          (if (eq old-point (point-min))
              (goto-char (mct--first-completion-point))
            (goto-char old-point)))))))

(defun mct-switch-to-completions-top ()
  "Switch to the top of the completions' buffer."
  (interactive nil mct-mode)
  (mct--switch-to-completions)
  (goto-char (mct--first-completion-point))
  (mct--restore-old-point-in-grid (point)))

(defun mct-switch-to-completions-bottom ()
  "Switch to the bottom of the completions' buffer."
  (interactive nil mct-mode)
  (mct--switch-to-completions)
  (goto-char (point-max))
  (next-completion -1)
  (goto-char (point-at-bol))
  (unless (mct--completions-completion-p)
    (next-completion 1))
  (mct--restore-old-point-in-grid (point))
  (recenter
   (- -1
      (min (max 0 scroll-margin)
           (truncate (/ (window-body-height) 4.0))))
   t))

(defun mct--bottom-of-completions-p (arg)
  "Test if point is at the notional bottom of the Completions.
ARG is a numeric argument for `next-completion', as described in
`mct-next-completion-or-mini'."
  (or (eobp)
      (mct--completions-line-boundary (mct--last-completion-point))
      (= (save-excursion (next-completion arg) (point)) (point-max))
      ;; The empty final line case...
      (save-excursion
        (goto-char (point-at-bol))
        (and (not (bobp))
	         (or (beginning-of-line (1+ arg)) t)
	         (save-match-data
	           (looking-at "[\s\t]*$"))))))

(defun mct-next-completion-or-mini (&optional arg)
  "Move to the next completion or switch to the minibuffer.
This performs a regular motion for optional ARG lines, but when
point can no longer move in that direction it switches to the
minibuffer."
  (interactive "p" mct-mode)
  (cond
   ((mct--bottom-of-completions-p (or arg 1))
    (mct-focus-minibuffer))
   (t
    (if (not (mct--one-column-p))
        ;; Retaining the column number ensures that things work
        ;; intuitively in a grid view.
        (let ((col (current-column)))
          ;; The `when' is meant to skip past lines that do not
          ;; contain completion candidates, such as those with
          ;; `completions-group-format'.
          (when (mct--completions-no-completion-line-p (or arg 1))
            (if arg
                (setq arg 2)
              (setq arg (1+ arg))))
          (vertical-motion (or arg 1))
          (unless (eq col (save-excursion (goto-char (point-at-bol)) (current-column)))
            (line-move-to-column col))
          (when (or (> (current-column) col)
                    (not (mct--completions-completion-p)))
            (next-completion -1)))
      (next-completion (or arg 1))))
   (setq this-command 'next-line)))

(defun mct--top-of-completions-p (arg)
  "Test if point is at the notional top of the Completions.
ARG is a numeric argument for `previous-completion', as described in
`mct-previous-completion-or-mini'."
  (or (bobp)
      (mct--completions-line-boundary (mct--first-completion-point))
      (= (save-excursion (previous-completion arg) (point)) (point-min))))

(defun mct-previous-completion-or-mini (&optional arg)
  "Move to the next completion or switch to the minibuffer.
This performs a regular motion for optional ARG lines, but when
point can no longer move in that direction it switches to the
minibuffer."
  (interactive "p" mct-mode)
  (cond
   ((mct--top-of-completions-p (if (natnump arg) arg 1))
    (mct-focus-minibuffer))
   ((if (not (mct--one-column-p))
        ;; Retaining the column number ensures that things work
        ;; intuitively in a grid view.
        (let ((col (current-column)))
          ;; The `when' is meant to skip past lines that do not
          ;; contain completion candidates, such as those with
          ;; `completions-group-format'.
          (when (mct--completions-no-completion-line-p (or (- arg) -1))
            (if arg
                (setq arg 2)
              (setq arg (1+ arg))))
          (vertical-motion (or (- arg) -1))
          (unless (eq col (save-excursion (goto-char (point-at-bol)) (current-column)))
            (line-move-to-column col))
          (when (or (> (current-column) col)
                    (not (mct--completions-completion-p)))
            (next-completion -1)))
      (previous-completion (if (natnump arg) arg 1))))))

(defun mct-next-completion-group (&optional arg)
  "Move to the next completion group.
If ARG is supplied, move that many completion groups at a time."
  (interactive "p" mct-mode)
  (dotimes (_ (or arg 1))
    (when-let (group (save-excursion
                       (text-property-search-forward 'face
                                                     'completions-group-separator
                                                     t nil)))
      (let ((pos (prop-match-end group)))
        (unless (eq pos (point-max))
          (goto-char pos)
          (next-completion 1))))))

(defun mct-previous-completion-group (&optional arg)
  "Move to the previous completion group.
If ARG is supplied, move that many completion groups at a time."
  (interactive "p" mct-mode)
  (dotimes (_ (or arg 1))
    ;; skip back, so if we're at the top of a group, we go to the previous one...
    (forward-line -1)
    (if-let (group (save-excursion
                     (text-property-search-backward 'face
                                                    'completions-group-separator
                                                    t nil)))
        (let ((pos (prop-match-beginning group)))
          (unless (eq pos (point-min))
            (goto-char pos)
            (next-completion 1)))
      ;; ...and if there was a match, go back down, so the point doesn't
      ;; end in the group separator
      (forward-line 1))))

;;;;; Candidate selection

(defun mct-choose-completion-exit ()
  "Run `choose-completion' in the Completions buffer and exit."
  (interactive nil mct-mode)
  (when (active-minibuffer-window)
    (when-let* ((window (mct--get-completion-window))
                (buffer (window-buffer)))
      (with-current-buffer buffer
        (choose-completion))
      (minibuffer-force-complete-and-exit))))

(defun mct-choose-completion-no-exit ()
  "Run `choose-completion' in the Completions without exiting."
  (interactive nil mct-mode)
  (when-let* ((window (mct--get-completion-window))
              (buffer (window-buffer))
              (mini (active-minibuffer-window)))
    (with-current-buffer buffer
      (let ((completion-no-auto-exit t))
        (choose-completion)))
    (select-window mini nil)))

(defvar display-line-numbers-mode)

(defun mct--line-completion (n)
  "Select completion on Nth line."
  (with-current-buffer (window-buffer (mct--get-completion-window))
    (goto-char (point-min))
    (forward-line (1- n))
    (mct-choose-completion-exit)))

(defun mct--line-bounds (n)
  "Test if Nth line is in the buffer."
  (with-current-buffer (window-buffer (mct--get-completion-window))
    (let ((bounds (count-lines (point-min) (point-max))))
      (unless (<= n bounds)
        (user-error "%d is not within the buffer bounds (%d)" n bounds)))))

(defun mct-goto-line ()
  "Go to line N in the Completions' buffer."
  (interactive nil mct-mode)
  (let ((n (read-number "Line number: ")))
    (mct--line-bounds n)
    (select-window (mct--get-completion-window))
    (mct--line-completion n)))

(defun mct--line-number-selection ()
  "Show line numbers and select one of them."
  (with-current-buffer (window-buffer (mct--get-completion-window))
    (let ((mct-show-completion-line-numbers t))
      (if (bound-and-true-p display-line-numbers-mode)
          (mct-goto-line)
        (unwind-protect
            (progn
              (mct--display-line-numbers)
              (mct-goto-line))
          (display-line-numbers-mode -1))))))

(defun mct-choose-completion-number ()
  "Select completion candidate on a given line number.
Upon selecting the candidate, exit the minibuffer (i.e. confirm
the choice right away).

If the Completions' buffer is not visible, it is displayed.  Line
numbers are shown on the side for during the operation (unless
`mct-show-completion-line-numbers' is non-nil, in which case they
are always visible).

This command can be invoked from either the minibuffer or the
Completions' buffer."
  (interactive nil mct-mode)
  (if (not (mct--one-column-p))
      (user-error "Cannot select by line in grid view")
    (let ((mct-remove-shadowed-file-names t)
          (mct-live-update-delay most-positive-fixnum)
          (enable-recursive-minibuffers t))
      (unless (mct--get-completion-window)
        (mct--show-completions))
      (if (or (and (derived-mode-p 'completion-list-mode)
                   (active-minibuffer-window))
              (and (minibufferp)
                   (mct--get-completion-window)))
          (mct--line-number-selection)))))

(defvar crm-completion-table)
(defvar crm-separator)

(defun mct--regex-to-separator (regex)
  "Parse REGEX of `crm-separator' in `mct-choose-completion-dwim'."
  (save-match-data
    (cond
     ;; whitespace-delimited, like default & org-set-tag-command
     ((string-match (rx
                     bos "[" (1+ blank) "]*"
                     (group (1+ any))
                     "[" (1+ blank) "]*" eos)
                    regex)
      (match-string 1 regex))
     ;; literal character
     ((string= regex (regexp-quote regex))
      regex))))

(defun mct-choose-completion-dwim ()
  "Append to minibuffer when at `completing-read-multiple' prompt.
In any other prompt use `mct-choose-completion-no-exit'."
  (interactive nil mct-mode)
  (when-let* ((mini (active-minibuffer-window))
              (window (mct--get-completion-window))
              (buffer (window-buffer window)))
    (mct-choose-completion-no-exit)
    (with-current-buffer (window-buffer mini)
      (when crm-completion-table
        (let ((separator (or (mct--regex-to-separator crm-separator)
                             ",")))
          (insert separator))
        (let ((inhibit-message t))
          (switch-to-completions))))))

(defun mct-edit-completion ()
  "Edit the current completion candidate inside the minibuffer.

The current candidate is the one at point while inside the
Completions' buffer.

When point is in the minibuffer, the current candidate is
determined as follows:

+ The one at the last known position in the Completions'
  window (if the window is deleted and produced again, this value
  is reset).

+ The first candidate in the Completions' buffer.

A candidate is recognised for as long as point is not past its
last character."
  (interactive nil mct-mode)
  (let* ((window (mct--get-completion-window))
         (buffer (window-buffer window))
         (mini (active-minibuffer-window))
         pos)
    (when (and mini window)
      (with-current-buffer buffer
        (when-let ((old-point (window-old-point window)))
          (if (= old-point (point-min))
              (setq pos (mct--first-completion-point))
            (setq pos old-point))))
      (when pos
        ;; NOTE 2021-10-26: why must we `switch-to-completions' to get a
        ;; valid candidate?  Why can't this be part of the above
        ;; `with-current-buffer'?
        (switch-to-completions)
        (goto-char pos)
        (mct-choose-completion-no-exit)))))

(defun mct-complete-and-exit ()
  "Complete current input and exit.

This is the same as with
\\<mct-minibuffer-local-completion-map>\\[mct-edit-completion],
followed by exiting the minibuffer with that candidate."
  (interactive nil mct-mode)
  (mct-edit-completion)
  (exit-minibuffer))

;;;;; Miscellaneous commands

;; This is needed to circumvent `mct--clean-completions' with regard to
;; `cursor-sensor-functions'.
(defun mct-beginning-of-buffer ()
  "Go to the top of the Completions buffer."
  (interactive nil mct-mode)
  (goto-char (mct--first-completion-point)))

(defun mct-keyboard-quit-dwim ()
  "Control the exit behaviour for completions' buffers.

If in a completions' buffer and unless the region is active, run
`abort-recursive-edit'.  Otherwise run `keyboard-quit'.

If the region is active, deactivate it.  A second invocation of
this command is then required to abort the session."
  (interactive nil mct-mode)
  (when (derived-mode-p 'completion-list-mode)
    (if (use-region-p)
        (keyboard-quit)
      (abort-recursive-edit))))

;;;; Global minor mode setup

;;;;; Stylistic tweaks and refinements

;; Thanks to Omar Antolín Camarena for providing the messageless and
;; stealthily.  Source: <https://github.com/oantolin/emacs-config>.
(defun mct--messageless (fn &rest args)
  "Set `minibuffer-message-timeout' to 0.
Meant as advice around minibuffer completion FN with ARGS."
  (let ((minibuffer-message-timeout 0))
    (apply fn args)))

;; Copied from Daniel Mendler's `vertico' library:
;; <https://github.com/minad/vertico>.
(defun mct--crm-indicator (args)
  "Add prompt indicator to `completing-read-multiple' filter ARGS."
  (cons (concat "[CRM] " (car args)) (cdr args)))

;; Adapted from Omar Antolín Camarena's live-completions library:
;; <https://github.com/oantolin/live-completions>.
(defun mct--honor-inhibit-message (fn &rest args)
  "Skip applying FN to ARGS if `inhibit-message' is t.
Meant as `:around' advice for `minibuffer-message', which does
not honor minibuffer message."
  (unless inhibit-message
    (apply fn args)))

;; Note that this solves bug#45686:
;; <https://debbugs.gnu.org/cgi/bugreport.cgi?bug=45686>
(defun mct--stealthily (fn &rest args)
  "Prevent minibuffer default from counting as a modification.
Meant as advice for FN `minibuf-eldef-setup-minibuffer' with rest
ARGS."
  (let ((inhibit-modification-hooks t))
    (apply fn args)))

(defun mct--setup-completions-styles ()
  "Set up variables for default completions."
  (when mct-hide-completion-mode-line
    (setq-local mode-line-format nil))
  (if mct-apply-completion-stripes
      (mct--add-stripes)
    (mct--remove-stripes)))

(defun mct--truncate-lines-silently ()
  "Toggle line truncation without printing messages."
  (let ((inhibit-message t))
    (toggle-truncate-lines t)))

;;;;; Shadowed path

;; Adapted from icomplete.el
(defun mct--shadow-filenames (&rest _)
  "Hide shadowed file names."
  (let ((saved-point (point)))
    (when (and
           mct-remove-shadowed-file-names
           (eq (mct--completion-category) 'file)
           rfn-eshadow-overlay (overlay-buffer rfn-eshadow-overlay)
           (eq this-command 'self-insert-command)
           (= saved-point (mct--minibuffer-field-end))
           (or (>= (- (point) (overlay-end rfn-eshadow-overlay)) 2)
               (eq ?/ (char-before (- (point) 2)))))
      (delete-region (overlay-start rfn-eshadow-overlay)
                     (overlay-end rfn-eshadow-overlay)))))

(defun mct--setup-shadow-files ()
  "Set up shadowed file name deletion.
To be assigned to `minibuffer-setup-hook'."
  (add-hook 'after-change-functions #'mct--shadow-filenames nil t))

;;;;; Highlight current candidate

(defvar-local mct--highlight-overlay nil
  "Overlay to highlight candidate in the Completions' buffer.")

(defvar mct--overlay-priority -50
  "Priority used on the `mct--highlight-overlay'.
This value means that it takes precedence over lines that have
the `mct-stripe' face, while it is overriden by the active
region.")

;; This is for Emacs 27 which does not have a completion--string text
;; property.
(defun mct--completions-text-property-search ()
  "Search for text property of completion candidate."
  (or (text-property-search-forward 'completion--string)
      (text-property-search-forward 'mouse-face)))

;; The `if-let' is to prevent highlighting of empty space, such as by
;; clicking on it with the mouse.
(defun mct--completions-completion-beg ()
  "Return point of completion candidate at START and END."
  (if-let ((string (mct--completions-completion-p)))
      (save-excursion
        (prop-match-beginning (mct--completions-text-property-search)))
    (point)))

;; Same as above for the `if-let'.
(defun mct--completions-completion-end ()
  "Return end of completion candidate."
  (if-let ((string (mct--completions-completion-p)))
      (save-excursion
        (if (mct--one-column-p)
            (1+ (point-at-eol))
          (prop-match-end (mct--completions-text-property-search))))
    (point)))

(defun mct--overlay-make ()
  "Make overlay to highlight current candidate."
  (let ((ol (make-overlay (point) (point))))
    (overlay-put ol 'priority mct--overlay-priority)
    (overlay-put ol 'face 'mct-highlight-candidate)
    ol))

(defun mct--overlay-move (overlay)
  "Highlight the candidate at point with OVERLAY."
  (let* ((beg (mct--completions-completion-beg))
         (end (mct--completions-completion-end)))
	(move-overlay overlay beg end)))

(defun mct--completions-candidate-highlight ()
  "Activate `mct--highlight-overlay'."
  (unless (overlayp mct--highlight-overlay)
    (setq mct--highlight-overlay (mct--overlay-make)))
  (mct--overlay-move mct--highlight-overlay))

(defun mct--completions-highlighting ()
  "Highlight the current completion in the Completions' buffer."
  (add-hook 'post-command-hook #'mct--completions-candidate-highlight nil t))

;;;;; Keymaps

(defvar mct-completion-list-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "M-v") #'scroll-down-command)
    (define-key map [remap keyboard-quit] #'mct-keyboard-quit-dwim)
    (define-key map [remap goto-line] #'mct-choose-completion-number)
    (define-key map [remap next-line] #'mct-next-completion-or-mini)
    (define-key map (kbd "n") #'mct-next-completion-or-mini)
    (define-key map [remap previous-line] #'mct-previous-completion-or-mini)
    (define-key map (kbd "M-p") #'mct-previous-completion-group)
    (define-key map (kbd "M-n") #'mct-next-completion-group)
    (define-key map (kbd "p") #'mct-previous-completion-or-mini)
    (define-key map (kbd "M-e") #'mct-edit-completion)
    (define-key map (kbd "<tab>") #'mct-choose-completion-no-exit)
    (define-key map (kbd "<return>") #'mct-choose-completion-exit)
    (define-key map (kbd "<M-return>") #'mct-choose-completion-dwim)
    (define-key map [remap beginning-of-buffer] #'mct-beginning-of-buffer)
    map)
  "Derivative of `completion-list-mode-map'.")

(defvar mct-minibuffer-local-completion-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-j") #'exit-minibuffer)
    (define-key map (kbd "<tab>") #'minibuffer-force-complete)
    (define-key map [remap goto-line] #'mct-choose-completion-number)
    (define-key map (kbd "M-e") #'mct-edit-completion)
    (define-key map (kbd "<C-return>") #'mct-complete-and-exit)
    (define-key map (kbd "C-n") #'mct-switch-to-completions-top)
    (define-key map (kbd "<down>") #'mct-switch-to-completions-top)
    (define-key map (kbd "C-p") #'mct-switch-to-completions-bottom)
    (define-key map (kbd "<up>") #'mct-switch-to-completions-bottom)
    (define-key map (kbd "C-l") #'mct-list-completions-toggle)
    map)
  "Derivative of `minibuffer-local-completion-map'.")

(defvar mct-minibuffer-local-filename-completion-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "<backspace>") #'mct-backward-updir)
    map)
  "Derivative of `minibuffer-local-filename-completion-map'.")

(defun mct--completion-list-mode-map ()
  "Hook to `completion-setup-hook'."
  (unless (memq this-command mct--no-complete-functions)
    (use-local-map
     (make-composed-keymap mct-completion-list-mode-map
                           (current-local-map)))))

(defun mct--minibuffer-local-completion-map ()
  "Hook to `minibuffer-setup-hook'."
  (unless (memq this-command mct--no-complete-functions)
    (use-local-map
     (make-composed-keymap mct-minibuffer-local-completion-map
                           (current-local-map)))))

(defun mct--minibuffer-local-filename-completion-map ()
  "Hook to `minibuffer-setup-hook'."
  (when (eq (mct--completion-category) 'file)
    (use-local-map
     (make-composed-keymap mct-minibuffer-local-filename-completion-map
                           (current-local-map)))))

;;;;; mct-mode declaration

(declare-function minibuf-eldef-setup-minibuffer "minibuf-eldef")

(defvar mct--resize-mini-windows nil)
(defvar mct--completion-show-help nil)
(defvar mct--completion-auto-help nil)
(defvar mct--completions-format nil)

;;;###autoload
(define-minor-mode mct-mode
  "Set up opinionated default completion UI."
  :global t
  :group 'mct
  (if mct-mode
      (progn
        (setq mct--resize-mini-windows resize-mini-windows
              mct--completion-show-help completion-show-help
              mct--completion-auto-help completion-auto-help
              mct--completions-format completions-format)
        (setq resize-mini-windows t
              completion-show-help nil
              completion-auto-help t
              completions-format mct-completions-format)
        (let ((hook 'minibuffer-setup-hook))
          (add-hook hook #'mct--setup-completions)
          (add-hook hook #'mct--minibuffer-local-completion-map)
          (add-hook hook #'mct--minibuffer-local-filename-completion-map)
          (add-hook hook #'mct--setup-shadow-files))
        (let ((hook 'completion-list-mode-hook))
          (add-hook hook #'mct--setup-completions-styles)
          (add-hook hook #'mct--completion-list-mode-map)
          (add-hook hook #'mct--truncate-lines-silently)
          (add-hook hook #'mct--completions-highlighting)
          (add-hook hook #'mct--display-line-numbers)
          (add-hook hook #'cursor-sensor-mode))
        (add-hook 'completion-setup-hook #'mct--clean-completions)
        (dolist (fn '(exit-minibuffer
                      choose-completion
                      minibuffer-force-complete
                      minibuffer-complete-and-exit
                      minibuffer-force-complete-and-exit))
          (advice-add fn :around #'mct--messageless))
        (advice-add #'completing-read-multiple :filter-args #'mct--crm-indicator)
        (advice-add #'minibuffer-message :around #'mct--honor-inhibit-message)
        (advice-add #'minibuf-eldef-setup-minibuffer :around #'mct--stealthily))
    (setq resize-mini-windows mct--resize-mini-windows
          completion-show-help mct--completion-show-help
          completion-auto-help mct--completion-auto-help
          completions-format mct--completions-format)
    (let ((hook 'minibuffer-setup-hook))
      (remove-hook hook #'mct--setup-completions)
      (remove-hook hook #'mct--minibuffer-local-completion-map)
      (remove-hook hook #'mct--minibuffer-local-filename-completion-map))
    (let ((hook 'completion-list-mode-hook))
      (remove-hook hook #'mct--setup-completions-styles)
      (remove-hook hook #'mct--completion-list-mode-map)
      (remove-hook hook #'mct--truncate-lines-silently)
      (remove-hook hook #'mct--completions-highlighting)
      (remove-hook hook #'mct--display-line-numbers)
      (remove-hook hook #'cursor-sensor-mode))
    (remove-hook 'completion-setup-hook #'mct--clean-completions)
    (dolist (fn '(exit-minibuffer
                  choose-completion
                  minibuffer-force-complete
                  minibuffer-complete-and-exit
                  minibuffer-force-complete-and-exit))
      (advice-remove fn #'mct--messageless))
    (advice-remove #'completing-read-multiple #'mct--crm-indicator)
    (advice-remove #'minibuffer-message #'mct--honor-inhibit-message)
    (advice-remove #'minibuf-eldef-setup-minibuffer #'mct--stealthily)))

(provide 'mct)
;;; mct.el ends here

3.1.4. Enhanced minibuffer commands (consult.el and prot-consult.el)

Daniel Mendler's Consult is a welcome addition to the ecosystem of modular, extensible tools that work with the standard minibuffer completion mechanisms and, by extension, with every user interface that largely conforms with them (Icomplete, Selectrum) or fully respects them (Embark and Daniel's own Vertico). For my case, this means that it works with everything included in Completion framework and extras.

Consult's value proposition is two-fold: (1) remain aligned with the Emacs completion paradigm, and (2) offer minibuffer-centric commands that either enhance aspects of interactivity and functionality found in existing commands or outright provide them from scratch.

Some Consult commands are drop-in replacements for built-in options. For example consult-complex-command offers an improved interactive experience over the default repeat-complex-command. Same principle for consult-goto-line which displays the line numbers and offers a live preview of where you are about to land.

Other commands enhance the defaults with a filtering mechanism that targets candidates by their type. A case in point is consult-imenu which recognises syntactic constructs that are variables, functions, macros (configurable via consult-imenu-narrow, consult-imenu-toplevel).

This "filtering" mechanism, which is internally known as "narrowing", can be accessed via a key binding for all commands that support it. In my case, that key is the right angled bracket, or greater than sign (>) from inside the minibuffer (configure consult-narrow-key). So you type the narrow key and follow it up with another key that matches the relevant targets. Hit backspace to remove the narrowing. As for the available keys, type ? which calls consult-narrow-help.

This narrowing-by-type mechanism can also be used without inputting the consult-narrow-key, just by typing in the appropriate character and inserting a space. For instance, to search only for functions in consult-imenu, you type f and then a space. Consult will add an indicator to the minibuffer prompt describing the active filter.

In general, commands that involve multiple groups can benefit from this type of narrowing. The prime example is consult-buffer which combines sources of recently visited files, bookmarks, and buffers (those are configurable via the variable consult-buffer-sources). Though others follow the same principle, such as the aforementioned consult-imenu and consult-bookmark.

Another intriguing facility of Consult is its asynchronous call to external processes, such as grep and find. Those calls can be configured to return some output based on a minimum number of characters, while they also allow for tweaks to their update delays. Interactivity is already a given, meaning that you can continue typing and see the results pop up. Furthermore, they implement a two-stage input scheme, separated by a configurable delimiter (# by default and controlled with consult-async-default-split):

  • First you type in the pattern that should be sent to the external program. This is what triggers the asynchronous call. So your input looks like this: #PATTERN. The pattern will typically consist of some text or a regular expression, but can also include command line flags for the underlying CLI program (check Consult's documentation for the technicalities).
  • Then you can add another field delimiter to instruct Consult to (i) keep the results that #PATTERN gave you and (ii) leverage Emacs' own mechanisms to further narrow the list. Now your input looks like this: #PATTERN#MORE-PATTERNS. The #MORE-PATTERNS will use whatever completion styles you have configured (check my completion-styles).

As already suggested, Consult provides previews for its commands. This feature should work without any further intervention.

Consult can shine when used in tandem with Embark to produce buffers that hold all the candidates of any given minibuffer completion command (Extended minibuffer actions and more (embark.el and prot-embark.el)). For example, embark-export can be called from inside consult-grep (and variants) to deliver a dedicated grep-mode buffer, which can then be edited with the help of the wgrep package (check wgrep (writable grep)). Use that to quickly refactor some pattern across your files.

Other nice extensions of Consult are (i) its ability to work as a generic front-end for completion, and (ii) its preview facility for registers. The former is done by consult-completion-in-region which provides completion for commands such as dabbrev-completion or the TAB key in programming buffers (see Tabs, indentation, and the TAB key). While the latter is an overall prettier presentation for the familiar register preview window (watch: Primer on Emacs “registers” (2020-03-08)).

As for registers themselves, Consult furnishes three commands, one focused on minibuffer completion and two as do-what-I-mean alternatives to the built-in facilities of storing and inserting—or jumping to—registered data.

  • consult-register is what you use for completion. It searches through the contents of the registered compartments and, thus, works well when you have text-heavy registers that you need to filter through before inserting one at point.
  • consult-register-store will save a "thing" to the specified key. What the thing is depends on the context:

    • If the region is active, it will operate on the affected text.
    • If you call it with a numeric argument, it will store that number.
    • If no region is active and no numeric prefix is supplied, it will let you select between the current position (point), window configuration (window), set of frames with their window configurations (frameset), or keyboard macro (kmacro).

    This do-what-I-mean facility is complemented by an actions' menu that offers hints on the keys you can use to specify the desired step forward. For example, if you are operating on a region, M-a will let you append the text to the given register.

  • consult-register-load simplifies the mental workload of actually using a register. Unlike the Emacs default where you need to know in advance what type of data does the register holds in order to use the right action for it, Consult's version just handles that for you. All you have to do is instruct it to use the given register and it will know whether it should insert some text or jump to a point/frameset, etc.

In practice, I only ever use consult-register because I have already developed muscle memory for the register-related actions. Though using all three of the above is easier to learn and more consistent overall.

Note that my prot-consult.el (reproduced after the following package configurations) defines some quick and dirty extensions or thin wrappers around Consult commands. The former will be reviewed in favour of better alternatives, even though they "simply work" with everything I try.

;;; Enhanced minibuffer commands (consult.el and prot-consult.el)
(prot-emacs-elpa-package 'consult
  (setq consult-line-numbers-widen t)
  ;; (setq completion-in-region-function #'consult-completion-in-region)
  (setq consult-async-min-input 3)
  (setq consult-async-input-debounce 0.5)
  (setq consult-async-input-throttle 0.8)
  (setq consult-narrow-key ">")
  (setq consult-imenu-config
        '((emacs-lisp-mode :toplevel "Functions"
                           :types ((?f "Functions" font-lock-function-name-face)
                                   (?m "Macros"    font-lock-keyword-face)
                                   (?p "Packages"  font-lock-constant-face)
                                   (?t "Types"     font-lock-type-face)
                                   (?v "Variables" font-lock-variable-name-face)))))
  ;; Search C-h f for more "bookmark jump" handlers.
  (setq consult-bookmark-narrow
        `((?d "Docview" ,#'doc-view-bookmark-jump)
          (?e "Eshell" ,#'eshell-bookmark-jump)
          (?f "File" ,#'bookmark-default-handler)
          (?h "Help" ,#'help-bookmark-jump)
          (?i "Info" ,#'Info-bookmark-jump)
          (?m "Man" ,#'Man-bookmark-jump)
          (?p "PDF" ,#'pdf-view-bookmark-jump)
          (?v "VC Dir" ,#'vc-dir-bookmark-jump)
          (?w "EWW" ,#'prot-eww-bookmark-jump)))
  (setq register-preview-delay 0.8
        register-preview-function #'consult-register-format)
  (setq consult-find-args "find . -not ( -wholename */.* -prune )")
  (setq consult-preview-key 'any)

  (add-hook 'completion-list-mode-hook #'consult-preview-at-point-mode)

  (let ((map global-map))
    (define-key map (kbd "C-x r b") #'consult-bookmark) ; override `bookmark-jump'
    (define-key map (kbd "C-x M-:") #'consult-complex-command)
    (define-key map (kbd "C-x M-m") #'consult-minor-mode-menu)
    (define-key map (kbd "C-x M-k") #'consult-kmacro)
    (define-key map [remap goto-line] #'consult-goto-line)
    (define-key map (kbd "M-K") #'consult-keep-lines) ; M-S-k is similar to M-S-5 (M-%)
    (define-key map (kbd "M-F") #'consult-focus-lines) ; same principle
    (define-key map (kbd "M-s M-b") #'consult-buffer)
    (define-key map (kbd "M-s M-f") #'consult-find)
    (define-key map (kbd "M-s M-g") #'consult-grep)
    (define-key map (kbd "M-s M-m") #'consult-mark)
    (define-key map (kbd "C-x r r") #'consult-register)) ; Use the register's prefix
  (define-key consult-narrow-map (kbd "?") #'consult-narrow-help))

(prot-emacs-builtin-package 'prot-consult
  (setq consult-project-root-function #'prot-consult-project-root)
  (setq prot-consult-command-centre-list
        '(consult-line
          prot-consult-line
          consult-mark))
  (setq prot-consult-command-top-list
        '(consult-outline
          consult-imenu
          prot-consult-outline
          prot-consult-imenu))
  (prot-consult-set-up-hooks-mode 1)
  (let ((map global-map))
    (define-key map (kbd "M-s M-i") #'prot-consult-imenu)
    (define-key map (kbd "M-s M-s") #'prot-consult-outline)
    (define-key map (kbd "M-s M-y") #'prot-consult-yank)
    (define-key map (kbd "M-s M-l") #'prot-consult-line)))

Here is prot-consult.el (part of my dotfiles' repo):

;;; prot-consult.el --- Tweak consult.el for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Tweaks for `consult.el' intended for my Emacs configuration:
;; <https://protesilaos.com/emacs/dotemacs/>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'consult nil t)
(require 'consult-imenu nil t)
(require 'prot-pulse)

(defgroup prot-consult ()
  "Tweaks for consult.el."
  :group 'minibuffer)

(defcustom prot-consult-command-centre-list '(consult-line consult-mark)
  "Commands to run `prot-consult-jump-recentre-hook'.
You must restart function `prot-consult-set-up-hooks-mode' for
changes to take effect."
  :group 'prot-consult
  :type 'list)

(defcustom prot-consult-command-top-list '(consult-outline)
  "Commands to run `prot-consult-jump-top-hook'.
You must restart function `prot-consult-set-up-hooks-mode' for
changes to take effect."
  :group 'prot-consult
  :type 'list)

;;;; Setup for some consult commands (TODO: needs review)

(defvar prot-consult-jump-recentre-hook nil
  "Hook that runs after select Consult commands.
To be used with `advice-add'.")

(defun prot-consult-after-jump-recentre (&rest _)
  "Run `prot-consult-jump-recentre-hook'."
  (run-hooks 'prot-consult-jump-recentre-hook))

(defvar prot-consult-jump-top-hook nil
  "Hook that runs after select Consult commands.
To be used with `advice-add'.")

(defun prot-consult-after-jump-top (&rest _)
  "Run `prot-consult-jump-top-hook'."
  (run-hooks 'prot-consult-jump-top-hook))

;;;###autoload
(define-minor-mode prot-consult-set-up-hooks-mode
  "Set up hooks for Consult."
  :init-value nil
  :global t
  (if prot-consult-set-up-hooks-mode
      (progn
        (dolist (fn prot-consult-command-centre-list)
          (advice-add fn :after #'prot-consult-after-jump-recentre))
        (dolist (fn prot-consult-command-top-list)
          (advice-add fn :after #'prot-consult-after-jump-top))
        (add-hook 'prot-consult-jump-recentre-hook #'prot-pulse-recentre-centre)
        (add-hook 'prot-consult-jump-top-hook #'prot-pulse-recentre-top)
        (add-hook 'prot-consult-jump-top-hook #'prot-pulse-show-entry))
    (dolist (fn prot-consult-command-centre-list)
      (advice-remove fn #'prot-consult-after-jump-recentre))
    (dolist (fn prot-consult-command-top-list)
      (advice-remove fn #'prot-consult-after-jump-top))
    (remove-hook 'prot-consult-jump-recentre-hook #'prot-pulse-recentre-centre)
    (remove-hook 'prot-consult-jump-top-hook #'prot-pulse-recentre-top)
    (remove-hook 'prot-consult-jump-top-hook #'prot-pulse-show-entry)))

;;;; Commands

(defvar consult--find-cmd)
(defvar consult--directory-prompt)
(declare-function consult--find "consult")
(autoload 'prot-orderless-with-styles "prot-orderless")

;;;###autoload
(defun prot-consult-project-root ()
  "Return path to project or `default-directory'.
Intended to be assigned to `consult-project-root-function'."
  (or (vc-root-dir)
      (locate-dominating-file "." ".git")
      default-directory))

;;;###autoload
(defun prot-consult-outline ()
  "Run `consult-outline' through `prot-orderless-with-styles'."
  (interactive)
  (prot-orderless-with-styles 'consult-outline))

;;;###autoload
(defun prot-consult-imenu ()
  "Run `consult-imenu' through `prot-orderless-with-styles'."
  (interactive)
  (prot-orderless-with-styles 'consult-imenu))

;;;###autoload
(defun prot-consult-line ()
  "Run `consult-line' through `prot-orderless-with-styles'."
  (interactive)
  (prot-orderless-with-styles 'consult-line))

;;;###autoload
(defun prot-consult-yank ()
  "Run Consult yank through `prot-orderless-with-styles'.
Wraps around the `consult-yank-from-kill-ring' command."
  (interactive)
  (prot-orderless-with-styles 'consult-yank-from-kill-ring))

(provide 'prot-consult)
;;; prot-consult.el ends here
3.1.4.1. Switch to directories (consult-dir.el)

This is another nifty package by Karthik Chikmagalur (author of project-x, among others, which I configure elsewhere in this document: Extra features for projects (project-x.el)). consult-dir provides an all-in-one interface with minibuffer completion for switching to a directory that belongs to (i) your list of bookmarks, (ii) the current project, (iii) your other projects, or (iv) recent files. It thus integrates nicely with other parts of the Emacs setup, where those are present:

As its name suggests, consult-dir builds on the Consult package by Daniel Mendler (Enhanced minibuffer commands (consult.el)). It thus retains the functionality of its base library, such as the ability to narrow the list of candidates to a subset in the same way as, e.g., the consult-buffer command does it: while at the empty minibuffer prompt, type r for recent directories, m for bookmarked entries, p for projects, and follow it up with an empty space. This applies the relevant filter. To remove it just delete backwards.

There are two main uses of the consult-dir command. The first is the general feature of switching to the buffer of choice. The other is when faced with a minibuffer prompt that expects a file path, such as when you copy or rename a file with Dired. In that case you want to perform the operation with one of your directories of interest as the target, so you call the consult-dir command, select one item in a recursive minibuffer (a minibuffer command inside a minibuffer command), and have that populate the original prompt.

;;; Switch to directories (consult-dir.el)
(prot-emacs-elpa-package 'consult-dir
  (setq consult-dir-sources '( consult-dir--source-bookmark
                               consult-dir--source-default
                               consult-dir--source-project
                               consult-dir--source-recentf))

  ;; Overrides `list-directory' in the `global-map', though I never used
  ;; that anyway.
  (dolist (map (list global-map minibuffer-local-filename-completion-map))
    (define-key map (kbd "C-x C-d") #'consult-dir)))

3.1.5. Extended minibuffer actions and more (embark.el and prot-embark.el)

Video introduction: Embark and my extras (2021-01-09). Also read Fifteen ways to use Embark (2021-10-06) by Karthik Chikmagalur.

[ NOTE 2021-04-02: the part in that video that deals with cycling through the completion candidates has been moved to the file prot-minibuffer.el, though the effect is practically the same. UPDATE 2021-10-22: It now is its own standalone package, called mct.el---Minibuffer configurations and my extras (mct.el). ]

Embark provides a unified framework of regular Emacs keymaps which let you carry out contextually relevant actions on targets through a common point of entry, typically a prefix key.

  • "Actions" are standard Emacs commands, such as describe-symbol or some interactive command you have defined that reads an argument from the minibuffer.
  • "Targets" are semantically sensitive constructs, such as the symbol at point, a URL, a file name, the active region, or the current completion candidate in the minibuffer (or the completions' buffer—more on that in the next section). Embark has so-called "clasifiers" which help it determine the category that the target belongs to.
  • The "contextually relevant [actions]" are defined in keymaps whose scope matches the category of the target. So embark-file-map holds all key and command associations for when Embark recognises a file name as its target. embark-region-map is for actions pertaining to the active region; embark-buffer-map for buffer names that you access through, say, switch-to-buffer (C-x b). And so on.
  • As for the "point of entry" or "prefix key", it is an Embark command, such as embark-act or embark-become. Those activate the appropriate keymap, thus granting you access to the relevant commands.

Embark can act on individual targets (e.g. the region) or sets of targets (e.g. the list of minibuffer completion candidates).

Emacs users are already familiar with this contextuality of Embark, even though they may not realise it. Think, for example, that hitting the j key in an org-mode buffer performs the action of inserting that letter in the buffer: you type something. While the same j key performs a different action in, say, a dired-mode buffer. There is no conflict between those actions because each of them is bound to a distinct keymap, and only one of those keymaps applies in their respective context.

The beauty of Embark's design is that you configure its contextuality in the exact same way you define all of your Emacs key bindings. So you can bind any command to whatever key you want and confine that action to a context you specify.

On Emacs 28, learn more about the keymaps with M-x describe-keymap and then search for embark.

Now a few things about actions that you can gain access to by invoking either of embark-act (most cases), or embark-become (where appropriate):

  • Embark has two ways to help you learn about its actions, though you will probably only ever need one of them. The first one, which is the default, is to display a detailed buffer when the variable embark-indicator is set to a value of embark-mixed-indicator or embark-verbose-indicator. Those will produce a buffer that shows the available actions, the keys they are bound to, and a description of what each action does. The other method, which is only really useful if embark-indicator is set to embark-minimal-indicator is to follow up the embark-act with C-h. That will produce a minibuffer prompt showing all available key bindings.
    • Keymaps aside, you can call any command after invoking embark-act. This can be either with M-x or via its key binding. For example, if you want to grep for the symbol at point in the current project, you can do embark-act and then C-x p g (project-find-regexp) (also read Projects (project.el and prot-project.el)). This will, of course, work as expected for commands that typically prompt you for something to operate on.
    • Using the C-h as a suffix is a standard procedure in Emacs to get a Help buffer that contains references to all commands+keys that extend a give key chord. So, for example, C-x r C-h will show you all commands under the C-x r prefix (see How do you learn Emacs?). While the generic C-h is fine in its own right, Embark offers an alternative that leverages minibuffer completion: the embark-prefix-help-command. You can either select an action from there or type @ and the corresponding key.
  • You will often be targeting individual items, such as the current completion candidate in the minibuffer, or the symbol at point. You can, however, collect the entire set of targets and store it in a buffer, which you can then re-use at your convenience or save it on disk (with write-file bound to C-x C-w by default). This is done by the embark-collect-snapshot command, which you can always access through embark-act.
    • The "Embark Collect" buffer can be presented as a grid or a list, with the possibility to manually switch between the two by means of the embark-collect-toggle-view command. The list view offers more room to the side of each candidate. It can be used to display annotations (see Completion annotations (marginalia)), such as the first line of a variable's doc string and current value, a command's key binding, the buffer's underlying file system path if it is visiting a file, and so on.
    • Embark's "collect" buffer also has a live-updating version, which can be use to filter the list of targets. This particular feature can, in fact, be used as a medium for visualising the list of candidates in the active minibuffer session. I used that setup for several months together with the default minibuffer as part my bespoke completion framework, though as of 2021-04-02 I handle the live-updating completions' buffer independently of Embark (Minibuffer configurations and my extras (mct.el)).
    • Other than producing a snapshot, Embark can also collect the targets and present them in a buffer whose major-mode is specialised to work on the category those targets belong to. This is done with the embark-export command. If you are targeting files, then the export takes you to a dired-mode buffer (also refer to this document's section on Dired (directory editor, file manager)); buffers go to ibuffer-mode (check Ibuffer and extras); grep results in a grep-mode buffer, and so on.

Finally, the prot-embark.el that is reproduced after the following block contains a few keymaps that integrate Embark with packages like consult (Enhanced minibuffer commands (consult.el and prot-consult.el)). The embark-consult package provides glue code that allows Embark to produce a correct export buffer while using relevant Consult commands, such as consult-grep.

;;; Extended minibuffer actions and more (embark.el and prot-embark.el)
(prot-emacs-elpa-package 'embark
  (setq prefix-help-command #'embark-prefix-help-command)
  ;; (setq prefix-help-command #'describe-prefix-bindings) ; the default of the above
  (setq embark-collect-initial-view-alist '((t . list)))
  (setq embark-quit-after-action t)     ; XXX: Read the doc string!
  (setq embark-cycle-key (kbd "C-."))   ; see the `embark-act' key
  (setq embark-collect-live-update-delay 0.5)
  (setq embark-collect-live-initial-delay 0.8)
  (setq embark-indicator #'embark-mixed-indicator)
  ;; NOTE 2021-07-31: The mixed indicator starts out with a minimal view
  ;; and then pops up the verbose buffer, so those variables matter.
  (setq embark-verbose-indicator-excluded-actions
        '("\\`embark-collect-" "\\`customize-" "\\(local\\|global\\)-set-key"
          set-variable embark-cycle embark-export
          embark-keymap-help embark-become embark-isearch))
  (setq embark-verbose-indicator-buffer-sections
        `(target "\n" shadowed-targets " " cycle "\n" bindings))
  (setq embark-mixed-indicator-both nil)
  (setq embark-mixed-indicator-delay 1.2)
  ;;  NOTE 2021-07-28: This is used when `embark-indicator' is set to
  ;;  `embark-mixed-indicator' or `embark-verbose-indicator'.  We can
  ;;  specify the window parameters here, but I prefer to do that in my
  ;;  `display-buffer-alist' (search this document) because it is easier
  ;;  to keep track of all my rules in one place.
  (setq embark-verbose-indicator-display-action nil)

  ;; Use alternating backgrounds, if `stripes' is available.
  (with-eval-after-load 'stripes
    (let ((hook 'embark-collect-mode-hook))
      (add-hook hook #'stripes-mode)
      (add-hook hook #'hl-line-mode)))

  (define-key global-map (kbd "C-,") #'embark-act)
  (let ((map minibuffer-local-completion-map))
    (define-key map (kbd "C-,") #'embark-act)
    (define-key map (kbd "C->") #'embark-become)
    (define-key map (kbd "M-q") #'embark-collect-toggle-view)) ; parallel of `fill-paragraph'
  (let ((map embark-collect-mode-map))
    (define-key map (kbd "C-,") #'embark-act)
    (define-key map (kbd "M-q") #'embark-collect-toggle-view))
  (let ((map embark-region-map))
    (define-key map (kbd "a") #'align-regexp)
    (define-key map (kbd "i") #'epa-import-keys-region)
    (define-key map (kbd "r") #'repunctuate-sentences) ; overrides `rot13-region'
    (define-key map (kbd "s") #'sort-lines)
    (define-key map (kbd "u") #'untabify))
  (let ((map embark-symbol-map))
    (define-key map (kbd ".") #'embark-find-definition)
    (define-key map (kbd "k") #'describe-keymap)))

;; Needed for correct exporting while using Embark with Consult
;; commands.
(prot-emacs-elpa-package 'embark-consult)

(prot-emacs-builtin-package 'prot-embark
  (prot-embark-keymaps 1)
  (prot-embark-setup-packages 1))

This is prot-embark.el (part of my dotfiles' repo):

;;; prot-embark.el --- Extensions to embark.el for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Extensions to `embark.el' for my Emacs configuration:
;; <https://protesilaos.com/emacs/dotemacs/>.
;;
;; NOTE 2021-04-02: Everything pertaining to the completions' buffer has
;; been moved to `prot-minibuffer.el'.
;;
;; NOTE 2021-04-10: What once was `prot-embark-extras.el' has been
;; merged here.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'cl-lib)
(require 'embark nil t)
(require 'prot-common)

(defgroup prot-embark ()
  "Extensions for `embark'."
  :group 'editing)

;;;; Extra keymaps

(autoload 'consult-grep "consult")
(autoload 'consult-line "consult")
(autoload 'consult-imenu "consult")
(autoload 'consult-outline "consult")

(defvar prot-embark-become-general-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "f") 'consult-find)
    (define-key map (kbd "g") 'consult-grep)
    map)
  "General custom cross-package `embark-become' keymap.")

(defvar prot-embark-become-line-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "l") 'consult-line)
    (define-key map (kbd "i") 'consult-imenu)
    (define-key map (kbd "s") 'consult-outline) ; as my default is 'M-s M-s'
    map)
  "Line-specific custom cross-package `embark-become' keymap.")

(defvar embark-become-file+buffer-map)
(autoload 'prot-recentf-recent-files "prot-recentf")
(autoload 'project-switch-to-buffer "project")
(autoload 'project-find-file "project")

(defvar prot-embark-become-file+buffer-map
  (let ((map (make-sparse-keymap)))
    (set-keymap-parent map embark-become-file+buffer-map)
    (define-key map (kbd "r") 'prot-recentf-recent-files)
    (define-key map (kbd "B") 'project-switch-to-buffer)
    (define-key map (kbd "F") 'project-find-file)
    map)
  "File+buffer custom cross-package `embark-become' keymap.")

(defvar embark-become-keymaps)

;;;###autoload
(define-minor-mode prot-embark-keymaps
  "Add or remove keymaps from Embark.
This is based on the value of `prot-embark-add-keymaps'
and is meant to keep things clean in case I ever wish to disable
those so-called 'extras'."
  :init-value nil
  :global t
  (let ((maps (list 'prot-embark-become-general-map
                    'prot-embark-become-line-map
                    'prot-embark-become-file+buffer-map)))
    (if prot-embark-keymaps
        (dolist (map maps)
          (cl-pushnew map embark-become-keymaps))
      (setq embark-become-keymaps
            (dolist (map maps)
              (delete map embark-become-keymaps))))))

;;;; Keycast integration

;; Got this from Embark's wiki.  Renamed it to placate the compiler:
;; <https://github.com/oantolin/embark/wiki/Additional-Configuration>.

(defvar keycast--this-command-keys)
(defvar keycast--this-command)

(defun prot-embark--store-action-key+cmd (cmd)
  "Configure keycast variables for keys and CMD.
To be used as filter-return advice to `embark-keymap-prompter'."
  (setq keycast--this-command-keys (this-single-command-keys)
        keycast--this-command cmd))

(advice-add 'embark-keymap-prompter :filter-return #'prot-embark--store-action-key+cmd)

(defun prot-embark--force-keycast-update (&rest _)
  "Update keycast's mode line.
To be passed as advice before `embark-act' and others."
  (force-mode-line-update t))

(autoload 'embark-act "embark")
(autoload 'embark-act-noexit "embark")
(autoload 'embark-become "embark")

;; NOTE: This has a generic name because my plan is to add more packages
;; to it.
;;;###autoload
(define-minor-mode prot-embark-setup-packages
  "Set up advice to integrate Embark with various commands."
  :init-value nil
  :global t
  (if (and prot-embark-setup-packages
           (require 'keycast nil t))
      (dolist (cmd '(embark-act embark-become))
        (advice-add cmd :before #'prot-embark--force-keycast-update))
    (dolist (cmd '(embark-act embark-become))
      (advice-remove cmd #'prot-embark--force-keycast-update))))

(provide 'prot-embark)
;;; prot-embark.el ends here

3.1.6. Projects (project.el and prot-project.el)

Starting with Emacs 28, the current development target, project.el contains lots of interesting additions that make it an all-round useful tool. Chief among them is a new prefix key bound to C-x p. This has good mnemonic value, like those for tabs (C-x t) and registers (C-x r).

A "project" is, in our case, a directory whose contents are related to each other in terms of the end product they can provide. Think, for example, how Emacs' source code is a single "project" that delivers the program we use. In practical terms, a project is a version controlled directory (or directory tree) governed by some program. For my case that is git though other backends are supported (by virtue of VC—see section on Version control framework (vc.el and prot-vc.el) as well as my related extras in Diff-mode (and prot-diff.el extensions)).

Using any of the commands listed in C-x p C-h will append the current project to a list of "known projects", stored in the dynamically updated project--list variable, whose contents are stored in a file defined by project-list-file (remember that C-h can be added to any key sequence to show its extensions and the commands associated with them—read my brief guide on How do you learn Emacs?). It is then possible to switch between your projects and proceed to immediately perform an action on them with C-x p p. A menu with possible commands will appear once you select a project. That is customisable via project-switch-commands.

Also note that C-x p p (project-switch-project) can be used to store a new version-controlled directory in the project--list. Look for the ... (choose a dir) option.

Now an overview of the prot-project.el commands, which build on top of an otherwise comprehensive system (full code further below):

  • prot-project-commit-log produces a list with the most recent commits in the project. The default count is controlled by a customisation option: prot-project-commit-log-limit. In case there is no project being acted upon, the command first prompts for completion against the project list.
  • prot-project-find-subdir provides completion for subdirectories in the current project. It opens the match in a Dired buffer. When no project is present, it prompts for completion.
  • prot-project-magit-status produces the magit-status buffer for the current project or prompts for completion.
  • prot-project-retrieve-tag lets you switch to an earlier tagged commit or branch using completion. As always, when no project is present, it asks for one before doing its work.

To aid me in my work, I copied code from Manuel Uberti's website (also referenced in the source code below this configuration block):

;;; Projects (project.el and prot-project.el)
(prot-emacs-builtin-package 'project
  ;; ;; Use this for Emacs 27 (I am on 28)
  ;; (add-to-list 'prot-emacs-ensure-install 'project)
  (setq project-switch-commands
        '((?f "File" project-find-file)
          (?s "Subdir" prot-project-find-subdir)
          (?g "Grep" project-find-regexp)
          (?d "Dired" project-dired)
          (?b "Buffer" project-switch-to-buffer)
          (?q "Query replace" project-query-replace-regexp)
          (?t "Tag switch" prot-project-retrieve-tag)
          (?m "Magit" prot-project-magit-status)
          (?v "VC dir" project-vc-dir)
          (?l "Log VC" prot-project-commit-log)
          (?e "Eshell" project-eshell)))
  (define-key global-map (kbd "C-x p q") #'project-query-replace-regexp)) ; C-x p is `project-prefix-map'

(prot-emacs-builtin-package 'prot-project
  (setq prot-project-project-roots '("~/Git/Projects/" "~/Git/build/"))
  (setq prot-project-commit-log-limit 25)
  (setq prot-project-large-file-lines 1000)
  (let ((map global-map))
    (define-key map (kbd "C-x p <delete>") #'prot-project-remove-project)
    (define-key map (kbd "C-x p l") #'prot-project-commit-log)
    (define-key map (kbd "C-x p m") #'prot-project-magit-status)
    (define-key map (kbd "C-x p s") #'prot-project-find-subdir)
    (define-key map (kbd "C-x p t") #'prot-project-retrieve-tag)))

This is prot-project.el (part of my dotfiles' repo):

;;; prot-project.el --- Extensions to project.el for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; This covers my project.el extensions, for use in my Emacs setup:
;; https://protesilaos.com/emacs/dotemacs.
;;
;; Make sure to also inspect prot-vc.el and prot-diff.el for a more
;; complete view of what I have on the topic of version control.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'cl-lib)
(require 'project)
(require 'prot-common)
(require 'vc)

(defgroup prot-project ()
  "Extensions for project.el and related libraries."
  :group 'project)

(defcustom prot-project-project-roots (list "~/Git/Projects/")
  "List of directories with version-controlled projects.
To be used by `prot-project-switch-project'."
  :type 'list
  :group 'prot-project)

(defcustom prot-project-commit-log-limit 25
  "Limit commit logs for project to N entries by default.
A value of 0 means 'unlimited'."
  :type 'integer
  :group 'prot-project)

(defcustom prot-project-large-file-lines 1000
  "How many lines constitute a 'large file' (integer).
This determines whether some automatic checks should be executed
or not, such as `prot-project-flymake-mode-activate'."
  :type 'integer
  :group 'prot-project)

;; Copied from Manuel Uberti:
;; <https://www.manueluberti.eu/emacs/2020/11/14/extending-project/>.
;;
;; Note that I prefer adding some dummy doc string over seeing spurious
;; compiler warnings.
(cl-defmethod project-root ((project (head local)))
  "Project root for PROJECT with HEAD and LOCAL."
  (cdr project))

;; Copied from Manuel Uberti and tweaked accordingly:
;; <https://www.manueluberti.eu/emacs/2020/11/14/extending-project/>.
(defun prot-project--project-files-in-directory (dir)
  "Use `fd' to list files in DIR."
  (unless (executable-find "fd")
    (error "Cannot find 'fd' command is shell environment $PATH"))
  (let* ((default-directory dir)
         (localdir (file-local-name (expand-file-name dir)))
         (command (format "fd -t f -0 . %s" localdir)))
    (project--remote-file-names
     (split-string (shell-command-to-string command) "\0" t))))

;; Copied from Manuel Uberti:
;; <https://www.manueluberti.eu/emacs/2020/11/14/extending-project/>.
;;
;; Same principle for the dummy doc string.
(cl-defmethod project-files ((project (head local)) &optional dirs)
  "Override `project-files' to use `fd' in local projects.

Project root for PROJECT with HEAD and LOCAL, plus optional
DIRS."
  (mapcan #'prot-project--project-files-in-directory
          (or dirs (list (project-root project)))))

(defun prot-project--list-projects ()
  "Produce list of projects in `prot-project-project-roots'."
  (let* ((dirs prot-project-project-roots)
         (dotless directory-files-no-dot-files-regexp)
         (cands (mapcan (lambda (d)
                          (directory-files d t dotless))
                        dirs)))
    (mapcar (lambda (d)
              (list (abbreviate-file-name d)))
            cands)))

;; FIXME: this is fragile since we do not store the original value of
;; `project--list' and may risk losing data.
;;;###autoload
(defun prot-project-add-projects ()
  "Append `prot-project--list-projects' to `project--list'."
  (interactive)
  (project--ensure-read-project-list)
  (let ((projects (prot-project--list-projects)))
    (setq project--list (append projects project--list))
    (project--write-project-list)))

;; TODO: use `completing-read-multiple' and learn how to delete a list
;; from an alist.
;;;###autoload
(defun prot-project-remove-project ()
  "Remove project from `project--list' using completion."
  (interactive)
  (project--ensure-read-project-list)
  (let* ((projects project--list)
         (dir (completing-read "REMOVE project from list: " projects nil t)))
    (setq project--list (delete (assoc dir projects) projects))
    (project--write-project-list)))

(defun prot-project--directory-subdirs (dir)
  "Return list of subdirectories in DIR."
  (cl-remove-if-not
   (lambda (x)
     (file-directory-p x))
   (directory-files-recursively dir ".*" t t)))

;; TODO: generalise this for all VC backends?  Which ones?
(defun prot-project--directory-subdirs-no-git (dir)
  "Remove .git dirs from DIR."
  (cl-remove-if
   (lambda (x)
     (string-match-p "\\.git" x))
   (prot-project--directory-subdirs dir)))

;; NOTE: in practice this is for `embark.el' (or equivalent
;; functionality), as it allows it to export the candidates in a Dired
;; buffer.
(defun prot-project--subdirs-completion-table (dir)
  "Return list of subdirectories in DIR with completion table."
  (prot-common-completion-table
   'file
   (prot-project--directory-subdirs-no-git dir)))

(defvar prot-project--subdir-hist '()
  "Minibuffer history for `prot-project-find-subdir'.")

;;;###autoload
(defun prot-project-find-subdir ()
  "Find subdirectories in the current project, using completion."
  (interactive)
  (let* ((pr (project-current t))
         (dir (cdr pr))
         (subdirs (prot-project--subdirs-completion-table dir))
         (directory (completing-read "Select Project subdir: " subdirs
                                     nil t nil 'prot-project--subdir-hist)))
    (dired directory)
    (add-to-history 'prot-project--subdir-hist dir)))

;; FIXME: the buttons at the bottom of the log for displaying more
;; commits do not seem to work with this.
;;;###autoload
(defun prot-project-commit-log (&optional arg)
  "Print commit log for the current project.
With optional prefix ARG (\\[universal-argument]) shows expanded
commit messages and corresponding diffs.

The log is limited to the integer specified by
`prot-project-commit-log-limit'.  A value of 0 means
'unlimited'."
  (interactive "P")
  (let* ((pr (project-current t))
         (dir (cdr pr))
         (default-directory dir) ; otherwise fails at spontaneous M-x calls
         (backend (vc-responsible-backend dir))
         (num prot-project-commit-log-limit)
         (int (prot-common-number-integer-p num))
         (limit (if (= int 0) t int))
         (diffs (if arg 'with-diff nil))
         (vc-log-short-style (unless diffs '(directory))))
    (vc-print-log-internal backend (list dir) nil nil limit diffs)))

;;;###autoload
(defun prot-project-retrieve-tag ()
  "Run `vc-retrieve-tag' on project and switch to the root dir.
Basically switches to a new branch or tag."
  (interactive)
  (let* ((pr (project-current t))
         (dir (cdr pr))
         (default-directory dir) ; otherwise fails at spontaneous M-x calls
         (name
          (vc-read-revision "Tag name: "
                            (list dir)
                            (vc-responsible-backend dir))))
    (vc-retrieve-tag dir name)
    (project-dired)))

(autoload 'magit-status "magit")

;;;###autoload
(defun prot-project-magit-status ()
  "Run `magit-status' on project."
  (interactive)
  (let* ((pr (project-current t))
         (dir (cdr pr)))
    (magit-status dir)))

(defun prot-project--max-line ()
  "Return the last line's number."
  (save-excursion
    (goto-char (point-max))
    (line-number-at-pos)))

(defun prot-project--large-file-p (&optional n)
  "Check if lines exceed `prot-project-large-file-lines'.
Optional N integer overrides that variable's value."
  (let* ((num (or n prot-project-large-file-lines))
         (int (prot-common-number-integer-p num)))
    (> (prot-project--max-line) int)))

;; Copied from Manuel Uberti, whom I had inspired with an earlier
;; version of this, and adapted accordingly:
;; <https://www.manueluberti.eu/emacs/2020/11/21/flymake-projects/>.
;;;###autoload
(defun prot-project-flymake-mode-activate ()
  "Activate Flymake only for `project-known-project-roots'."
  (project--ensure-read-project-list)
  (let ((known-projects (project-known-project-roots))
        (pr (or (vc-root-dir)
                (locate-dominating-file "." ".git")
                default-directory))
        (modes (prot-common-minor-modes-active)))
    (if (and (null buffer-read-only)
             (member pr known-projects)
             (not (prot-project--large-file-p))
             (not (member 'org-src-mode modes))
             (not (null buffer-file-truename)))
        (flymake-mode 1)
      (flymake-mode -1))))

(defvar org-src-mode-hook)

(add-hook 'org-src-mode-hook #'prot-project-flymake-mode-activate)
(add-hook 'prog-mode-hook #'prot-project-flymake-mode-activate)

(provide 'prot-project)
;;; prot-project.el ends here
3.1.6.1. Extra features for projects (project-x.el)

This package by Karthik Chikmagalur provides some helpful extensions to the project.el library (see Projects (project.el and prot-project.el)).

Persistent storage of window configuration
The window layout for the given project can be saved and restored at will. This is similar to how registers can store a window layout, with the key difference being that project-x's variants persist between Emacs sessions. So you can start work on a project, save the window configuration and revisit it the day after.
Arbitrary project declaration
By adding an empty .project file to the root of a directory, we make it a valid project. This means that we can revisit it with the familiar C-x p p (project-switch-project) and generally perform every project-related operation we want. The upside of using this method is that you can specify arbitrary file paths that (i) do not necessary work under version and (ii) you do not intend to treat them as your regular projects (e.g. the elpa directory where Emacs installs packages by default).

The project-x-mode streamlines the experience by adding a couple of key bindings to the C-x p project prefix key chord. Those bindings will be familiar to anyone who has ever used registers: C-x p w will capture the project's window configuration, while C-x p j will jump to an already stored layout.

;;; Extra features for projects (project-x.el)
;; Project repo: <https://github.com/karthink/project-x>.  This is one
;; of the packages I handle manually via git, at least until it becomes
;; available through an ELPA.
;;
;; `prot-emacs-manual-package' is defined in my init.el
(prot-emacs-manual-package 'project-x
  (setq project-x-window-list-file (locate-user-emacs-file "project-x-window-list"))
  (setq project-x-local-identifier ".project")
  (project-x-mode 1))

3.1.7. Completion for recent files and directories (prot-recentf.el)

recentf is a built-in minor mode that keeps track of the files you have opened, allowing you to 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 in prot-recentf.el 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 (refer to the mega-section Completion framework and extras).

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.

Also note that the Consult package provides its own recentf command as well as the consult-buffer which combines candidates from multiple sources, including bookmarks, recent files, and buffers (check more in Enhanced minibuffer commands (consult.el and prot-consult.el)).

;;; Completion for recent files and directories (prot-recentf.el)
(prot-emacs-builtin-package 'recentf
  (setq recentf-save-file (locate-user-emacs-file "recentf"))
  (setq recentf-max-saved-items 200)
  (setq recentf-exclude '(".gz" ".xz" ".zip" "/elpa/" "/ssh:" "/sudo:"))
  (add-hook 'after-init-hook #'recentf-mode))

(prot-emacs-builtin-package 'prot-recentf
  (add-to-list 'recentf-keep 'prot-recentf-keep-predicate)
  (let ((map global-map))
    (define-key map (kbd "C-x C-r") #'prot-recentf-recent-files-or-dirs)))

This is a copy of prot-recentf.el (part of my dotfiles' repo):

;;; prot-recentf.el --- Extensions to recentf.el for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Extensions to `recentf.el' for my Emacs configuration:
;; <https://protesilaos.com/emacs/dotemacs/>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'recentf)
(require 'prot-common)

;;;###autoload
(defun prot-recentf-keep-predicate (file)
  "Additional conditions for saving FILE in `recentf-list'.
Add this function to `recentf-keep'."
  (cond
   ((file-directory-p file) (file-readable-p file))))

(defvar prot-recentf--history-files '()
  "Minibuffer history for prot-recentf files.")

(defvar prot-recentf--history-dirs '()
  "Minibuffer history for prot-recentf directories.")

(defun prot-recentf--files ()
  "Return completion table with files in `recentf-list'."
  (prot-common-completion-table
   'file
   (mapcar 'abbreviate-file-name recentf-list)))

(defun prot-recentf--files-prompt (files)
  "Helper of `prot-recentf-recent-files' to read FILES."
  (let ((def (car prot-recentf--history-files)))
    (completing-read
     (format "Recentf [%s]: " def)
     files nil t nil 'prot-recentf--history-files def)))

;;;###autoload
(defun prot-recentf-recent-files (file)
  "Select FILE from `recentf-list' using completion."
  (interactive
   (list (prot-recentf--files-prompt (prot-recentf--files))))
  (find-file file)
  (add-to-history 'prot-recentf--history-files file))

(defun prot-recentf--dirs ()
  "Return completion table with directories in `recentf-list'."
  (let ((list (mapcar 'abbreviate-file-name recentf-list)))
    (prot-common-completion-table
     'file
     (delete-dups
      (mapcar (lambda (file)
                (if (file-directory-p file)
                    (directory-file-name file)
                  (substring (file-name-directory file) 0 -1)))
              list)))))

(defun prot-recentf--dirs-prompt (dirs)
  "Helper of `prot-recentf-recent-dirs' to read DIRS."
  (let ((def (car prot-recentf--history-dirs)))
    (completing-read
     (format "Recent dir [%s]: " def)
     dirs nil t nil 'prot-recentf--history-dirs def)))

;;;###autoload
(defun prot-recentf-recent-dirs (dir)
  "Select DIR from `recentf-list' using completion."
  (interactive
   (list (prot-recentf--dirs-prompt (prot-recentf--dirs))))
  (find-file dir)
  (add-to-history 'prot-recentf--history-dirs dir))

;;;###autoload
(defun prot-recentf-recent-files-or-dirs (&optional arg)
  "Select recent file or, with ARG, recent directory."
  (interactive "P")
  (if arg
      (call-interactively 'prot-recentf-recent-dirs)
    (call-interactively 'prot-recentf-recent-files)))

(provide 'prot-recentf)
;;; prot-recentf.el ends here

3.1.8. In-buffer completions

3.1.8.1. Corfu (Completion Overlay Region FUnction) and CAPE

corfu is another nimble package by Daniel Mendler (author of Consult and several others—search this document for a few of them), which focuses on text expansion within the buffer. Its User Interface involves a pop-up that shows the completion candidates for the text before point. This pop-up, technically a child frame, is invoked manually with the TAB key. While I would have preferred to use the minibuffer even for in-buffer text expansion, for the sake of consistency and predictability, I consider the added functionality more important than my mild preference against pop-ups (though the fact that Corfu is invoked manually makes this largely irrelevant).

As is the norm with Daniel's contributions, corfu is a sharp and focused tool that works with the standard completion mechanisms, as it reads from the completion-styles as well as the completion-category-overrides (refer to Minibuffer configurations and my extras (mct.el), though note that if you are a fan of Daniel's work, you will probably want to check the Vertico package instead of my hacks for the default minibuffer experience).

Unlike its more established counterpart, company-mode, Corfu does not implement its own backends for various programming languages. Instead, it plugs in to the standard completion-at-point-functions (CAPF). Perhaps that could be seen as a downside for users who program in a lot of languages and/or who require some of Company's bespoke functionality. For me though, the CAPF facility is good enough because it works with the only programming language I have been coding in for the last several months: Emacs Lisp. CAPF also is future-proof in that any package that provides its own completion table, automatically works with Corfu, such as my contacts manager (EBDB (mail contacts)). And I would assume that it also works for any major mode that is in sync with the current best practices in Emacs' design. Besides, this approach makes the code easier for me to understand and configure.

Once the Corfu overlay appears, use C-n and C-p to cycle through the candidates. Other motions work as well, such as C-v, M-v, M-<, M->, though regular incremental motions is what you would normally want. RET selects the current item and exits, while TAB tries to complete as much as possible, only exiting if the match is unique. Additionally, you can use M-h (corfu-show-documentation) and M-g (corfu-show-documentation) over the selected candidate to either display its doc string or its source code, respectively.

Note that for TAB to perform completion in addition to its primary function of adjusting indentation, we need tab-always-indent to be set to a complete value (check Tabs, indentation, and the TAB key and/or read that variable's doc string).

Finally, the cape package, also by Daniel, adds some more backends for the aforementioned CAPF facility. They make Corfu work in more contexts. Basically, this is the sort of thing that you set up and it "just works".

;;; Corfu (Completion Overlay Region FUnction)
(prot-emacs-elpa-package 'corfu
  ;; (dolist (mode '( message-mode-hook text-mode-hook prog-mode-hook
  ;;                  shell-mode-hook eshell-mode-hook))
  ;;   (add-hook mode #'corfu-mode))
  (corfu-global-mode 1)
  (define-key corfu-map (kbd "<tab>") #'corfu-complete))

(prot-emacs-elpa-package 'cape
  (dolist (backend '( cape-abbrev-capf cape-keyword-capf
                      cape-dict-capf cape-ispell-capf cape-file-capf
                      cape-dabbrev-capf))
    (add-to-list 'completion-at-point-functions backend)))
3.1.8.2. 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 helps you re-type what you already have.

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 (though for Emacs28 yank-pop on M-y will use completion if the previous command was not yank on C-y). To complete a phrase, matching the last succesful dabbrev-expand, you need to supply an empty space and call the command again. This will match the next word, and so on for N words.

Whereas dabbrev-completion benefits from minibuffer interactivity and the pattern matching styles in effect (Completion framework and extras). With the help of Corfu, the completion candidates are displayed in a pop-up window near point (Corfu for in-buffer completion).

The dabbrev-abbrev-char-regexp is configured to match both regular words and symbols (e.g. words separated by hyphens). This makes it equally suitable 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 character. 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 (regexp-builder).

;;; Dabbrev (dynamic word completion)
(prot-emacs-builtin-package 'dabbrev
  (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 nil)
  (setq dabbrev-case-replace 'case-replace)
  (setq dabbrev-check-other-buffers t)
  (setq dabbrev-eliminate-newlines t)
  (setq dabbrev-upcase-means-case-search t)
  (let ((map global-map))
    (define-key map (kbd "M-/") #'dabbrev-expand)
    (define-key map (kbd "C-x M-/") #'dabbrev-completion)))
3.1.8.3. Skeletons and abbreviations

NOTE 2020-06-08: Pending major review. UPDATE 2021-01-16: I still plan to review this.

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.

;;; Skeletons and abbreviations
(prot-emacs-builtin-package 'abbrev
  (setq abbrev-file-name (locate-user-emacs-file "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)

  (let ((map global-map))
    (define-key map (kbd "C-x a e") #'expand-abbrev) ; default, just here for visibility
    (define-key map (kbd "C-x a u") #'unexpand-abbrev))
  (add-hook 'text-mode-hook #'abbrev-mode)
  (add-hook '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, occur, grep, and extras (prot-search.el)

The built-in search mechanisms, defined in the libraries isearch.el and replace.el are minimal in their presentation, yet powerful in their applications. There are the main points of entry to the commands they offer:

  • isearch-forward (C-s) prompts for a string after point and offers live feedback on its progress. isearch-backward (C-r) moves in the opposite direction.
    • Two distinct keys may seem redundant at first, but you really appreciate this level of precision when recording keyboard macros (see, for example, my video about Isearch powers in keyboard macros (2020-01-21)).
    • Use C-M-s and C-M-r for running a search against a regular expression, or call isearch-toggle-regexp (M-r) after starting a regular isearch.
  • query-replace (M-%) replaces all matches of a string and asks you for confirmation on each of them. If you check its help page (press ? after invoking the command), you will learn that ! stands for an affirmative answer to all, which is a standard in all such prompts.
    • query-replace-regexp (C-M-%) does the same for regular expressions.
  • occur (M-s o) places all matches of a regular expression or string in a dedicated buffer. That can function as an index for moving to the relevant points in the buffer, but also as a means of refactoring all matches at once. Just make the *Occur* buffer editable with e. Running occur with a numeric argument provides N lines of context around the given match.

The beauty of the Occur and Replace commands is that they can be initiated from within an active Isearch session, using the same keys. So C-s INPUT M-s o will search for input and then run occur on it. Try C-h k C-s to get a 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 . Search for symbol at point
M-s M-. Search for thing at point (Emacs28)
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'

Every one of the above, except the first item, can be executed on their own, or as extensions of C-s (and variants). In the latter case, when you run a regexp-aware Isearch (C-M-s or C-M-r) a M-% will automatically be interpreted as C-M-%.

The Occur and Replace operations are aware of the active region, so if you highlight, say, a paragraph and do M-% you will only replace matches inside of that area (while not relevant to our point, this also works for undo (C-/), which is super useful). Though one can achieve pretty much the same result by leveraging Emacs' narrowing commands, like narrow-to-defun (learn about all of them with C-x n C-h)

Now here is a neat trick I discovered a while ago that makes Isearch even better for most tasks: the ability to interpret a space as a wildcard. This is due to the combined effect of the values assigned to the variables search-whitespace-regexp, isearch-lax-whitespace, isearch-regexp-lax-whitespace. So you can now search for something like se di bu al and it will return setq display-buffer-alist. And you can still combine it with all of the aforementioned! Note that 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 my prot-search.el library which provides some extensions to an already well-designed architecture (the code is reproduced after the package configurations).

  • prot-search-isearch-other-end simply places point at the opposite end of the current match. Particularly helpful while recording keyboard macros. This is to work around the default behaviour of Isearch which puts 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+exit 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, especially with the wildcard hack mentioned above. For those cases moving to the opposite end might require multiple key presses, which is bad when trying to record an efficient keyboard macro. 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).
  • prot-search-isearch-abort-dwim deletes the entirety of the non-matching input while leaving the valid parts in place. Otherwise it behaves like a standard backward character deletion. The built-in method to remove the entirety of a mismatched input is to hit C-g following a failed search. However, I find that the choice of key binding can prove problematic, since C-g also exits a successful search, while I also prefer a "do-what-I-mean" behaviour.
  • prot-search-isearch-replace-symbol runs a forward-looking query-replace for the symbol at point. Simple and effective for quickly refactoring a given function/variable name (and one of the reasons why I have never needed an extra package for such tasks).
  • prot-search-isearch-beginning-of-buffer and its counterpart prot-search-isearch-end-of-buffer move to the first or last instance of the symbol at point. They also accept a numeric argument, which they interpret as an offset. In practice, this is the same as running M-s . M-s < or M-s . M-s >.
  • prot-search-occur-urls gathers all URLs in the current buffer and places them in an Occur buffer without their context while also making them clickable (we say that it "buttonises" them).
  • prot-search-occur-outline produces an outline of the buffer based on a configurable association of major mode and regular expression. Check the variable prot-search-outline-regexp-alist. The command can prompt for a regexp style to use, defaulting to the current major mode if an outline regexp exists for it. An arbitrary regexp can also be inserted either through interactive use or from Lisp code.
  • prot-search-occur-browse-url gathers all URLs in the buffer and prompts you to select one with completion. It then browses that item using whatever browser you have for browse-url-browser-function.
  • prot-search-grep runs a local grep in the current directory. With a prefix argument, it runs recursively instead. This is a thin wrapper around the built-in lgrep and rgrep commands: it makes the process faster by not asking for a directory and file extension pattern. All output is placed in a separate buffer. Note that I also have a variant for git-controlled projects: it is prot-vc-git-grep from Version control framework (vc.el and prot-vc.el). Also note that Consult provides a live version: refer to the section on consult.el. (I normally use Consult and export the results with Embark, except for when I know exactly what I am looking for and want it in a buffer, so the added features are not needed).
;;; Isearch, occur, grep, and extras (prot-search.el)
(prot-emacs-builtin-package 'isearch
  (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)
  ;; These variables are from Emacs 28
  (setq isearch-repeat-on-direction-change t)
  (setq lazy-highlight-initial-delay 0.5)
  (setq lazy-highlight-no-delay-length 3)
  (setq isearch-wrap-pause t)

  (define-key minibuffer-local-isearch-map (kbd "M-/") #'isearch-complete-edit)
  (let ((map isearch-mode-map))
    (define-key map (kbd "C-g") #'isearch-cancel) ; instead of `isearch-abort'
    (define-key map (kbd "M-/") #'isearch-complete)))

(prot-emacs-builtin-package 'replace
  (setq list-matching-lines-jump-to-current-line t)
  (add-hook 'occur-mode-hook #'hl-line-mode)
  (add-hook 'occur-mode-hook #'prot-common-truncate-lines-silently) ; from `prot-common.el'
  (define-key occur-mode-map (kbd "t") #'toggle-truncate-lines))

(prot-emacs-builtin-package 'grep)

(prot-emacs-builtin-package 'prot-search
  (setq prot-search-outline-regexp-alist
        '((emacs-lisp-mode . "^\\((\\|;;;+ \\)")
          (org-mode . "^\\(\\*+ +\\|#\\+[Tt][Ii][Tt][Ll][Ee]:\\)")))

  (let ((map global-map))
    (define-key map (kbd "M-s %") #'prot-search-isearch-replace-symbol)
    (define-key map (kbd "M-s M-<") #'prot-search-isearch-beginning-of-buffer)
    (define-key map (kbd "M-s M->") #'prot-search-isearch-end-of-buffer)
    (define-key map (kbd "M-s g") #'prot-search-grep)
    (define-key map (kbd "M-s u") #'prot-search-occur-urls)
    (define-key map (kbd "M-s M-o") #'prot-search-occur-outline)
    (define-key map (kbd "M-s M-u") #'prot-search-occur-browse-url))
  (let ((map isearch-mode-map))
    (define-key map (kbd "<up>") #'prot-search-isearch-repeat-backward)
    (define-key map (kbd "<down>") #'prot-search-isearch-repeat-forward)
    (define-key map (kbd "<backspace>") #'prot-search-isearch-abort-dwim)
    (define-key map (kbd "<C-return>") #'prot-search-isearch-other-end)))

Here is prot-search.el (part of my dotfiles' repo):

;;; prot-search.el --- Extensions to isearch, replace, grep for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; This covers my isearch.el, replace.el, and grep.el extensions, for
;; use in my Emacs setup: <https://protesilaos.com/emacs/dotemacs>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'isearch)
(require 'replace)
(require 'grep)
(require 'prot-common)

(defgroup prot-search ()
  "Setup for Isearch, Occur, and related."
  :group 'search)

;; NOTE 2021-09-16: Based on my git config for headings in diffs.  Read:
;; <https://protesilaos.com/codelog/2021-01-26-git-diff-hunk-elisp-org/>.
(defcustom prot-search-outline-regexp-alist
  '((emacs-lisp-mode . "^\\((\\|;;;+ \\)")
    (org-mode . "^\\(\\*+ +\\|#\\+[Tt][Ii][Tt][Ll][Ee]:\\)"))
  "Alist of regular expressions per major mode.

For best results the key must be a symbol that corresponds to a
major mode.

To be used by `prot-search-occur-outline'."
  :type 'alist
  :group 'prot-search)

;;;; Isearch

;;;###autoload
(defun prot-search-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)))

;;;###autoload
(defun prot-search-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))

;;;###autoload
(defun prot-search-isearch-repeat-forward (&optional arg)
  "Move forward, keeping point at the beginning of the match.
Optionally move to ARGth match in the given direction."
  (interactive "p")
  (when (and isearch-forward isearch-other-end)
    (goto-char isearch-other-end))
  (isearch-repeat-forward (or arg 1)))

;;;###autoload
(defun prot-search-isearch-repeat-backward (&optional arg)
  "Move backward, keeping point at the beginning of the match.
Optionally move to ARGth match in the given direction."
  (interactive "p")
  (when (and (not isearch-forward) isearch-other-end)
    (goto-char isearch-other-end))
  (isearch-repeat-backward (or arg 1)))

(defmacro prot-search-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-search-isearch-occurrence
 prot-search-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-search-isearch-occurrence
 prot-search-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.")

;;;; Replace/Occur

;; TODO: make this work backwardly when given a negative argument
(defun prot-search-isearch-replace-symbol ()
  "Run `query-replace-regexp' for the symbol at point."
  (interactive)
  (isearch-forward-symbol-at-point)
  (isearch-query-replace-regexp))

(autoload 'goto-address-mode "goto-addr")

;;;###autoload
(defun prot-search-occur-urls ()
  "Produce buttonised list of all URLs in the current buffer."
  (interactive)
  (let ((buf-name (format "*links in <%s>*" (buffer-name))))
    (add-hook 'occur-hook #'goto-address-mode)
    (occur-1 prot-common-url-regexp "\\&" (list (current-buffer)) buf-name)
    (remove-hook 'occur-hook #'goto-address-mode)))

;;;###autoload
(defun prot-search-occur-browse-url ()
  "Point browser at a URL in the buffer using completion.
Which web browser to use depends on the value of the variable
`browse-url-browser-function'.

Also see `prot-search-occur-urls'."
  (interactive)
  (let ((matches nil))
    (save-excursion
      (goto-char (point-min))
      (while (search-forward-regexp prot-common-url-regexp nil t)
        (push (match-string-no-properties 0) matches)))
    (funcall browse-url-browser-function
             (completing-read "Browse URL: " matches nil t))))

(defvar prot-search--occur-outline-hist '()
  "Minibuffer history of `prot-search-occur-outline'.")

(defun prot-search--occur-outline-prompt ()
  "Helper prompt for `prot-search-occur-outline'."
  (let* ((alist prot-search-outline-regexp-alist)
         (key (car (assoc major-mode alist)))
         (default (or key (nth 1 prot-search--occur-outline-hist))))
    (completing-read
     (format "Outline style [%s]: " default)
     (mapcar #'car alist)
     nil nil nil 'prot-search--occur-outline-hist default)))

;;;###autoload
(defun prot-search-occur-outline (&optional arg)
  "Produce buffer outline from `prot-search-outline-regexp-alist'.

With optional prefix ARG (\\[universal-argument]), prompt for a
preset among the entries in `prot-search-outline-regexp-alist'.

ARG may also be a string (or regular expression) when called from
Lisp."
  (interactive "P")
  (let* ((regexp (when (and arg (not (stringp arg)))
                   (prot-search--occur-outline-prompt)))
         (rx (cond
              ((stringp arg)
               arg)
              ((and arg (string= major-mode regexp))
               (cdr (assoc regexp prot-search-outline-regexp-alist)))
              ((assoc major-mode prot-search-outline-regexp-alist)
               (cdr (assoc major-mode prot-search-outline-regexp-alist)))
              (t (user-error "Unknown outline style"))))
         (buf-name (format "*outline of <%s>*" (buffer-name))))
    (occur-1 rx nil (list (current-buffer)) buf-name)
    (add-to-history 'prot-search--occur-outline-hist regexp)))

;;;; Grep

(defvar prot-search--grep-hist '()
  "Input history of grep searches.")

;;;###autoload
(defun prot-search-grep (regexp &optional recursive)
  "Run grep for REGEXP.

Search in the current directory using `lgrep'.  With optional
prefix argument (\\[universal-argument]) for RECURSIVE, run a
search starting from the current directory with `rgrep'."
  (interactive
   (list
    (read-from-minibuffer (concat (if current-prefix-arg
                                      (propertize "Recursive" 'face 'warning)
                                    "Local")
                                  " grep for PATTERN: ")
                          nil nil nil 'prot-search--grep-hist)
    current-prefix-arg))
  (unless grep-command
    (grep-compute-defaults))
  (if recursive
      (rgrep regexp "*" default-directory)
    (lgrep regexp "*" default-directory)
    (add-to-history 'prot-search--grep-hist regexp)))

(provide 'prot-search)
;;; prot-search.el ends here

3.2.2. Test regular expressions (re-builder)

Emacs offers a built-in tool for testing regular expressions: invoke it with the regexp-builder or re-builder command. It pops up a buffer at the bottom of the current window, which lets you test a regular expression on the contents of the buffer from where the command was called. By default, re-builder uses Emacs-style notation, where escape sequences are written as a double backslash. 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 as it is what I also use when writing a pattern in some Elisp file.

To learn more about regular expressions, read the relevant pages in the official manual by evaluating this: (info "(emacs) Regexps"). Also consider watching my ~35 minute-long video primer on Emacs regexp (2020-01-23). If you actually need to do a regexp-aware query and replace operation that performs an arbitrary elisp function on a group check my article on how to use query-replace-regexp to downcase matches (2021-03-03). Remember that you can always get interactivity by first using something like isearch-forward-regexp and then switching to the query-replace operation with M-% (in this case, query-replace automatically becomes regexp-aware).

Also check: Isearch, occur, grep, and extras (prot-search.el).

;;; Test regular expressions (re-builder)
(prot-emacs-builtin-package 're-builder
  (setq reb-re-syntax 'read))

3.2.3. 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, query and replace a regexp…).

;;; wgrep (writable grep)
(prot-emacs-elpa-package 'wgrep
  (setq wgrep-auto-save-buffer t)
  (setq wgrep-change-readonly-file t)
  (let ((map grep-mode-map))
    (define-key map (kbd "e") #'wgrep-change-to-wgrep-mode)
    (define-key map (kbd "C-x C-q") #'wgrep-change-to-wgrep-mode)
    (define-key map (kbd "C-c C-c") #'wgrep-finish-edit)))

3.2.4. Cross-references (xref.el)

Xref provides helpful commands for code navigation and discovery, such as xref-find-definitions (M-.) and its counterpart xref-pop-marker-stack (M-,). It is a library that gets used by a variety of tools, including project.el (see Projects (project.el and prot-project.el)).

Here are just the basics. I might add more in the future.

;;; Cross-references (xref.el)
(prot-emacs-builtin-package 'xref
  ;; All those have been changed for Emacs 28
  (setq xref-show-definitions-function #'xref-show-definitions-completing-read) ; for M-.
  (setq xref-show-xrefs-function #'xref-show-definitions-buffer) ; for grep and the like
  (setq xref-file-name-display 'project-relative)
  (setq xref-search-program 'grep))

4. Directory, buffer, window management

4.1. Dired file manager (and prot-dired.el extras)

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, start an email with the current or marked files attached to the message, and more. Combine that with the possibility of matching items with regular expressions, such as for marking files or narrowing the list, or creating an editable Dired buffer to bulk rename entries, and you have everything you need to maximise your productivity.

Watch some of my older videos:

The following package configurations are fairly comprehensive. First an overview of the options I specify for Dired proper:

  • Copy and delete recursively. No need to be prompted about each action.
  • While in detailed view, search only file names when point is on one of them, else apply the query to the rest of the data.
  • Deletion sends items to the system's Trash, making it safer than the standard rm. The trash can be a life-saver, as it lets you restore deleted files (check: dired-like mode for the trash (trashed.el)).
  • Reformat output. Sort directories first. Show dotfiles and place them before anything else. Omit implicit directories (the single and double dots). Use human-readable size units. To learn everything about these switches, you need to read the manpage of ls. You can do so with M-x man RET ls or M-x woman.
    • 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 include the -X option (such as -AFXhlv --group-directories-first).
  • Hide all the details by default (permissions, size, etc.). Those can easily be toggled on with the left parenthesis. Also enable highlighting of the current line (hl-line-mode), which makes it even easier to spot the current item (I do not enable this globally, because I only want it for line-oriented interfaces, such as Dired's, but not for text editing).
  • While having two dired buffers open, the rename and copy operations will place the path of the inactive one as the target destination. When multiple dired buffers are present, this works between the current and most recently used ones, with M-n and M-p on the minibuffer prompt allowing you to switch between all possible targets.
  • For Emacs 27.1 or higher, Dired can automatically create destination directories for its copy and rename operations. So you can, for example, move (copy or rename) file to /non-existent-path/file and you will get what you want right away.
  • For Emacs 27.1 or higher, renaming a file of a version-controlled repository (git) will be done using the appropriate VC mechanism. This is to ensure that file name changes are tracked correctly (also check my detailed: Version control framework (vc.el and prot-vc.el)).

And here are a few words about the more specialised parts of the Dired ecosystem:

Dired subtree

This third-party package which is part of the dired-hacks project by Matus Goljer offers tree-style navigation, meaning that the subdirectories of the current Dired buffer can be expanded and contracted in place. It is possible to perform the same kind of folding on their subdirectories, and so on.

Tree-style navigation is useful in my workflow when all I want is a quick peek at a directory's contents.

Dired extras (dired-x)

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. (Note for Emacs 28 users: dired-jump is now part of dired.el).

'Jumping' works even when you are inside buffers that do not visit files, such as Magit, Diff, or Eshell: it just takes you to the default-directory. 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!

The other neat features of dired-x are (1) its ability to open Info files in place (dired-info command, bound to I), and (2) to open all marked files at once (dired-do-find-marked-files bound to F by default).

Writable Dired (wdired)
This is the standard 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. This practically means that you can rename files and change permissions (when the detailed list is available). Note that while renaming a file, any forward slash is treated like a directory and is created directly upon successful exit. Combine this utility with keyboard macros, rectangle edits, or query-replace (and its regexp variant---Isearch, occur, grep, and extras (prot-search.el)) and you have one potent tool at your disposal.
Image dired
This built-in library offers facilities for generating thumbnails out of a selection of images and displaying them in a separate buffer. An external program is needed to convert the images into thumbnails: imagemagick. Other useful external packages are optipng and sxiv (or equivalent). 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. As such, only use this for smaller collections. Besides, Emacs can open an image in a buffer and that works well for viewing individual items (else use ! or & to run an external process, with the latter being asynchronous). I don't keep a lot of images around, so I am not the best person to comment on this feature. Instead, I recommend you view the video of image-dired by Emacs Elements (Raoul Comninos).
prot-dired.el
This file (reproduced in its entirety below those configurations) contains a few custom extensions for streamlining some repetitive tasks. The video on my custom Dired extras (2021-07-21) may also be of help.
  1. It contains methods for opening media files using an external program. The idea is to provide a default option when invoking either ! or & on a given file. So if, for example, you try to open an image, it will search the system for the first program matching the elements in the prot-dired-image-viewers variable. Same principle for media players.
  2. The prot-dired-limit-regexp command is a convenience wrapper around built-in capabilities of narrowing the listing to the files that match (or don't) a given regular expression. When called directly, it prompts for a regexp and removes everything that does not match it. This operation does not delete files. It just hides them (restore the view either with g or by using the undo command). When the command is invoked with a universal prefix argument (C-u) it inverts the meaning of the regular expression so that it hides the matching entries.
  3. The prot-dired-insert-subdir is a general purpose command for inserting the contents of a subdirectory in the current Dired buffer. It basically provides a superset of features found in the standard dired-maybe-insert-subdir (bound to i by default). When items are marked, it searches among them for the subdirectories to the current directory and inserts them in the buffer, while ignoring invalid entries. If no marks are active and point is on a subdirectory line, it inserts it directly. If no marks are active and point is not on a subdirectory, then it prompts for minibuffer completion and inserts the selected item. When invoked with a single prefix argument (C-u) it first asks for the command-line flags to pass to the underlying ls program, which can be helpful if you want to get some more verbose information or pass the -R flag to directly insert a tree recursively. And when the command is called with a double prefix argument (C-u C-u) it removes all inserted subdirectories in one go. As always, the undo command can help you manage each step.

    Tip: to remove a single subdirectory, you can still use C-u k over its heading (dired-do-kill-lines with its ARG).

    Another tip: to update the ls switches for the whole buffer, type C-u s (dired-sort-toggle-or-edit with its ARG).

  4. An Imenu index is set up which lets you jump to the headers of all inserted directories using minibuffer completion (either with the generic M-x imenu or some third-party variant). The are commands which provide directional motions to move between such headings: prot-dired-subdirectory-next and prot-dired-subdirectory-previous.
;;; Dired file manager (and prot-dired.el extras)
(prot-emacs-builtin-package 'dired
  (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)
  (setq dired-auto-revert-buffer #'dired-directory-changed-p) ; also see `dired-do-revert-buffer'
  (setq dired-make-directory-clickable t) ; Emacs 29.1

  (add-hook 'dired-mode-hook #'dired-hide-details-mode)
  (add-hook 'dired-mode-hook #'hl-line-mode))

(prot-emacs-builtin-package 'dired-aux
  (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)
  ;; And this is for Emacs 28
  (setq dired-do-revert-buffer (lambda (dir) (not (file-remote-p dir))))

  (let ((map dired-mode-map))
    (define-key map (kbd "C-+") #'dired-create-empty-file)
    (define-key map (kbd "M-s f") #'nil)
    (define-key map (kbd "C-x v v") #'dired-vc-next-action))) ; Emacs 28

;; ;; NOTE 2021-05-10: I do not use `find-dired' and related commands
;; ;; because there are other tools that offer a better interface, such
;; ;; as `consult-find', `consult-grep', `project-find-file',
;; ;; `project-find-regexp', `prot-vc-git-grep'.
;; (prot-emacs-builtin-package 'find-dired
;;   (setq find-ls-option
;;         '("-ls" . "-AGFhlv --group-directories-first --time-style=long-iso"))
;;   (setq find-name-arg "-iname"))

(prot-emacs-builtin-package 'dired-x
  (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)
  (define-key dired-mode-map (kbd "I") #'dired-info))

(prot-emacs-builtin-package 'prot-dired
  (setq prot-dired-image-viewers '("feh" "sxiv"))
  (setq prot-dired-media-players '("mpv" "vlc"))
  (setq prot-dired-media-extensions
        "\\.\\(mp[34]\\|ogg\\|flac\\|webm\\|mkv\\)")
  (setq prot-dired-image-extensions
        "\\.\\(png\\|jpe?g\\|tiff\\)")
  (setq dired-guess-shell-alist-user ; those are the defaults for ! and & in Dired
        `((,prot-dired-image-extensions (prot-dired-image-viewer))
          (,prot-dired-media-extensions (prot-dired-media-player))))

  (add-hook 'dired-mode-hook #'prot-dired-setup-imenu)

  (let ((map dired-mode-map))
    (define-key map (kbd "i") #'prot-dired-insert-subdir) ; override `dired-maybe-insert-subdir'
    (define-key map (kbd "/") #'prot-dired-limit-regexp)
    (define-key map (kbd "C-c C-l") #'prot-dired-limit-regexp)
    (define-key map (kbd "M-n") #'prot-dired-subdirectory-next)
    (define-key map (kbd "C-c C-n") #'prot-dired-subdirectory-next)
    (define-key map (kbd "M-p") #'prot-dired-subdirectory-previous)
    (define-key map (kbd "C-c C-p") #'prot-dired-subdirectory-previous)))

(prot-emacs-elpa-package 'dired-subtree
  (setq dired-subtree-use-backgrounds nil)
  (let ((map dired-mode-map))
    (define-key map (kbd "<tab>") #'dired-subtree-toggle)
    (define-key map (kbd "<backtab>") #'dired-subtree-remove))) ; S-TAB

(prot-emacs-builtin-package 'wdired
  (setq wdired-allow-to-change-permissions t)
  (setq wdired-create-parent-directories t))

(prot-emacs-builtin-package 'image-dired
  (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)
  (define-key image-dired-thumbnail-mode-map
    (kbd "<return>") #'image-dired-thumbnail-display-external))

These are the contents of prot-dired.el (part of my dotfiles' repo):

;;; prot-dired.el --- Extensions to dired.el for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; This covers my dired.el extensions, for use in my Emacs setup:
;; https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(eval-when-compile (require 'cl-lib))
(require 'prot-common)

(defgroup prot-dired ()
  "Extensions for Dired."
  :group 'dired)

;;;; File associations

(defcustom prot-dired-media-extensions
  "\\.\\(mp[34]\\|ogg\\|flac\\|webm\\|mkv\\)"
  "Regular expression for media file extensions.

Also see the function `prot-dired-media-player' and the variable
`prot-dired-media-players'.

To be used in user configurations while setting up the variable
`dired-guess-shell-alist-user'."
  :type 'string
  :group 'prot-dired)

(defcustom prot-dired-image-extensions
  "\\.\\(png\\|jpe?g\\|tiff\\)"
  "Regular expression for media file extensions.

Also see the function `prot-dired-image-viewer' and the variable
`prot-dired-image-viewers'.

To be used in user configurations while setting up the variable
`dired-guess-shell-alist-user'."
  :type 'string
  :group 'prot-dired)

(defcustom prot-dired-media-players '("mpv" "vlc")
  "List of strings for media player programs.

Also see the function `prot-dired-media-player' and the variable
`prot-dired-media-extensions'.

To be used in user configurations while setting up the variable
`dired-guess-shell-alist-user'."
  :type '(repeat string)
  :group 'prot-dired)

(defcustom prot-dired-image-viewers '("feh" "sxiv")
  "List of strings for image viewer programs.

Also see the function `prot-dired-image-viewer' and the variable
`prot-dired-image-extensions'.

To be used in user configurations while setting up the variable
`dired-guess-shell-alist-user'."
  :type '(repeat string)
  :group 'prot-dired)

;; NOTE 2021-06-28: I am not sure why the compiler complains without
;; this, even though we require cl-lib.
(declare-function cl-remove-if "cl-lib")

(defmacro prot-dired-file-association (name programs)
  "Make NAME function to check for PROGRAMS."
  (declare (indent defun))
  `(defun ,name ()
     ,(format "Return available program.

This checks each entry in `%s' and returns the first program that
is available on the system.  If none is present, it falls back to
xdg-open (for GNU/Linux only).

This function is for use in `dired-guess-shell-alist-user'."
              programs)
     (catch :found
       (dolist (p (append ,programs '("xdg-open")))
         (when (executable-find p)
           (throw :found p))))))

(prot-dired-file-association
  prot-dired-media-player
  prot-dired-media-players)

(prot-dired-file-association
  prot-dired-image-viewer
  prot-dired-image-viewers)

;;;; General commands

(autoload 'dired-mark-files-regexp "dired")
(autoload 'dired-toggle-marks "dired")
(autoload 'dired-do-kill-lines "dired-aux")

(defvar prot-dired--limit-hist '()
  "Minibuffer history for `prot-dired-limit-regexp'.")

;;;###autoload
(defun prot-dired-limit-regexp (regexp omit)
  "Limit Dired to keep files matching REGEXP.

With optional OMIT argument as a prefix (\\[universal-argument]),
exclude files matching REGEXP.

Restore the buffer with \\<dired-mode-map>`\\[revert-buffer]'."
  (interactive
   (list
    (read-regexp
     (concat "Files "
             (when current-prefix-arg
               (propertize "NOT " 'face 'warning))
             "matching PATTERN: ")
     nil 'prot-dired--limit-hist)
    current-prefix-arg))
  (dired-mark-files-regexp regexp)
  (unless omit (dired-toggle-marks))
  (dired-do-kill-lines)
  (add-to-history 'prot-dired--limit-hist regexp))

;;;; Subdir extras and Imenu setup

(defvar prot-dired--directory-header-regexp "^ +\\(.+\\):\n"
  "Pattern to match Dired directory headings.")

;;;###autoload
(defun prot-dired-subdirectory-next (&optional arg)
  "Move to next or optional ARGth Dired subdirectory heading.
For more on such headings, read `dired-maybe-insert-subdir'."
  (interactive "p")
  (let ((pos (point))
        (subdir prot-dired--directory-header-regexp))
    (goto-char (point-at-eol))
    (if (re-search-forward subdir nil t (or arg nil))
        (progn
          (goto-char (match-beginning 1))
          (goto-char (point-at-bol)))
      (goto-char pos))))

;;;###autoload
(defun prot-dired-subdirectory-previous (&optional arg)
  "Move to previous or optional ARGth Dired subdirectory heading.
For more on such headings, read `dired-maybe-insert-subdir'."
  (interactive "p")
  (let ((pos (point))
        (subdir prot-dired--directory-header-regexp))
    (goto-char (point-at-bol))
    (if (re-search-backward subdir nil t (or arg nil))
        (goto-char (point-at-bol))
      (goto-char pos))))

(autoload 'dired-current-directory "dired")
(autoload 'dired-kill-subdir "dired-aux")

;;;###autoload
(defun prot-dired-remove-inserted-subdirs ()
  "Remove all inserted Dired subdirectories."
  (interactive)
  (goto-char (point-max))
  (while (and (prot-dired-subdirectory-previous)
              (not (equal (dired-current-directory)
                          (expand-file-name default-directory))))
      (dired-kill-subdir)))

(autoload 'cl-remove-if-not "cl-seq")

(defun prot-dired--dir-list (list)
  "Filter out non-directory file paths in LIST."
  (cl-remove-if-not
   (lambda (dir)
     (file-directory-p dir))
   list))

(defun prot-dired--insert-dir (dir &optional flags)
  "Insert DIR using optional FLAGS."
  (dired-maybe-insert-subdir (expand-file-name dir) (or flags nil)))

(autoload 'dired-get-filename "dired")
(autoload 'dired-get-marked-files "dired")
(autoload 'dired-maybe-insert-subdir "dired-aux")
(defvar dired-subdir-switches)
(defvar dired-actual-switches)

;;;###autoload
(defun prot-dired-insert-subdir (&optional arg)
  "Generic command to insert subdirectories in Dired buffers.

When items are marked, insert those which are subsirectories of
the current directory.  Ignore regular files.

If no marks are active and point is on a subdirectory line,
insert it directly.

If no marks are active and point is not on a subdirectory line,
prompt for a subdirectory using completion.

With optional ARG as a single prefix (`\\[universal-argument]')
argument, prompt for command line flags to pass to the underlying
'ls' program.

With optional ARG as a double prefix argument, remove all
inserted subdirectories."
  (interactive "p")
  (let* ((name (dired-get-marked-files))
         (flags (when (eq arg 4)
                  (read-string "Flags for `ls' listing: "
                               (or dired-subdir-switches dired-actual-switches)))))
    (cond  ; NOTE 2021-07-20: `length>', `length=' are from Emacs28
     ((eq arg 16)
      (prot-dired-remove-inserted-subdirs))
     ((and (length> name 1) (prot-dired--dir-list name))
      (mapc (lambda (file)
              (when (file-directory-p file)
                (prot-dired--insert-dir file flags)))
            name))
     ((and (length= name 1) (file-directory-p (car name)))
      (prot-dired--insert-dir (car name) flags))
     (t
      (let ((selection (read-directory-name "Insert directory: ")))
        (prot-dired--insert-dir selection flags))))))

(defun prot-dired--imenu-prev-index-position ()
  "Find the previous file in the buffer."
  (let ((subdir prot-dired--directory-header-regexp))
    (re-search-backward subdir nil t)))

(defun prot-dired--imenu-extract-index-name ()
  "Return the name of the file at point."
  (file-relative-name
   (buffer-substring-no-properties (+ (point-at-bol) 2)
                                   (1- (point-at-eol)))))

;;;###autoload
(defun prot-dired-setup-imenu ()
  "Configure imenu for the current dired buffer.
Add this to `dired-mode-hook'."
  (set (make-local-variable 'imenu-prev-index-position-function)
       'prot-dired--imenu-prev-index-position)
  (set (make-local-variable 'imenu-extract-index-name-function)
       'prot-dired--imenu-extract-index-name))

(provide 'prot-dired)
;;; prot-dired.el ends here

4.1.1. dired-like mode for the trash (trashed.el)

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.
;;; dired-like mode for the trash (trashed.el)
(prot-emacs-elpa-package 'trashed
  (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"))

4.2. Working with buffers

4.2.1. Keymap for buffers (Emacs28)

Starting with Emacs version 28, there is a keymap that can be accessed with the C-x x sequence. This new keymap (ctl-x-x-map), is meant to be used for commands that pertain to the current buffer. What I have here are just some tweaks to make it work the way I prefer.

;;; Keymap for buffers (Emacs28)
(let ((map ctl-x-x-map))              ; Emacs 28
  (define-key map "e" #'eval-buffer)
  (define-key map "f" #'follow-mode)  ; override `font-lock-update'
  (define-key map "r" #'rename-uniquely))

(with-eval-after-load 'org
  (define-key ctl-x-x-map "i" #'prot-org-id-headlines)
  (define-key ctl-x-x-map "h" #'prot-org-ox-html))

4.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 element of differentiation in accordance with the style of uniquify-buffer-name-style. While uniquify-strip-common-suffix will 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.

;;; Unique names for buffers
(prot-emacs-builtin-package 'uniquify
  (setq uniquify-buffer-name-style 'forward)
  (setq uniquify-strip-common-suffix t)
  (setq uniquify-after-kill-buffer-p t))

4.2.3. Ibuffer and extras (dired-like buffer list manager)

ibuffer.el ships with Emacs and it provides a drop-in replacement for list-buffers. Compared to its counterpart, it allows for granular control over the buffer list and is more powerful overall. 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 or query-replace-regexp on marked buffers.

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 configurations, 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; use the current one.
  • 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 that I introduced after I published that video, which pertain to my prot-ibuffer.el library (copied in its entirety below the package configurations):

  • prot-ibuffer-buffers-major-mode produces a filtered list of buffers that match the major mode of the current buffer and lets you pick one using minibuffer completion. With an optional prefix argument (C-u) it places the results in an Ibuffer list.
  • prot-ibuffer-buffers-vc-root filters the list to items that match the current buffer's version-controlled directory. In practice, this fills the same niche as the built-in project-switch-to-buffer (for Emacs 28+), with the crucial difference that it neither reads from nor writes to the list of known projects (also check my configurations for Projects (project.el and prot-project.el)). When called with an optional prefix argument, this command puts its matching candidates in an Ibuffer view.

For those two I received guidance from Omar Antolín Camarena with regard to the use of read-buffer and the lambda passed to it (any errors are my own). This method informs other tools that this type of completion pertains to buffers, so they can adapt accordingly. See, in particular, Extended minibuffer actions and more (embark.el and prot-embark.el).

;;; Ibuffer and extras (dired-like buffer list manager)
(prot-emacs-builtin-package 'ibuffer
  (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 40 40 :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)
  (add-hook 'ibuffer-mode-hook #'hl-line-mode)
  (define-key global-map (kbd "C-x C-b") #'ibuffer)
  (let ((map ibuffer-mode-map))
    (define-key map (kbd "* f") #'ibuffer-mark-by-file-name-regexp)
    (define-key map (kbd "* g") #'ibuffer-mark-by-content-regexp) ; "g" is for "grep"
    (define-key map (kbd "* n") #'ibuffer-mark-by-name-regexp)
    (define-key map (kbd "s n") #'ibuffer-do-sort-by-alphabetic)  ; "sort name" mnemonic
    (define-key map (kbd "/ g") #'ibuffer-filter-by-content)))

(prot-emacs-builtin-package 'prot-ibuffer
  (let ((map global-map))
    (define-key map (kbd "M-s b") #'prot-ibuffer-buffers-major-mode)
    (define-key map (kbd "M-s v") #'prot-ibuffer-buffers-vc-root)))

Here is prot-ibuffer.el (find everything in my dotfiles' repo):

;;; prot-ibuffer.el --- Extensions to ibuffer.el for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; This covers my ibuffer.el extensions, for use in my Emacs setup:
;; https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'ibuffer)

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

;;;###autoload
(defun prot-ibuffer-buffers-vc-root (&optional arg)
  "Select buffers that belong to the version controlled directory.
With optional prefix ARG (\\[universal-argument]) produce an
`ibuffer' filtered accordingly.  Else use standard completion."
  (interactive "P")
  (let* ((root (or (vc-root-dir)
                   (locate-dominating-file "." ".git")))
         (prompt "Buffers for VC"))
    (if root
        (if arg
            (ibuffer t (format "*%s %s*" prompt root)
                     (list (cons 'filename (expand-file-name root))))
          (switch-to-buffer
           (read-buffer
            (format "%s %s:" prompt root) nil t
            (lambda (pair) ; pair is (name-string . buffer-object)
              (with-current-buffer (cdr pair) (string= (vc-root-dir) root))))))
      (user-error "Not in a version-controlled directory"))))

(provide 'prot-ibuffer)
;;; prot-ibuffer.el ends here

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

4.3.1. Window rules and basic tweaks (window.el)

The display-buffer-alist is intended as a rule-set for controlling the placement of windows. This is mostly needed for ancillary buffers, such as shells, compilation output, and the like. The objective is to create a more intuitive workflow where targeted buffer groups or types are always shown at 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 (evaluate (elisp) Displaying Buffers). Information can also be found at all times via C-h f display-buffer and, for my particular settings, with C-h f display-buffer-in-side-window.

With regard to the key bindings you will find here, most combinations are complementary to the standard ones, such as C-x 1 being aliased as 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. Some involve the Super key, in accordance with the norms described in the relevant 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 +. Lastly, the resize-window-repeat-map is for repeatable key chords that work with the repeat-mode for Emacs28 (read my description of what it is and how to set it up: Repeatable key chords (repeat-mode)).

Make sure to also review the other window-related keys in those sections:

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

;;; Window rules and basic tweaks (window.el)
(prot-emacs-builtin-package 'window
  (setq display-buffer-alist
        `(;; no window
          ("\\`\\*Async Shell Command\\*\\'"
           (display-buffer-no-window))
          ;; top side window
          ("\\**prot-elfeed-bongo-queue.*"
           (display-buffer-reuse-window display-buffer-in-side-window)
           (window-height . 0.16)
           (side . top)
           (slot . -2))
          ("\\*\\(prot-elfeed-mpv-output\\|world-clock\\).*"
           (display-buffer-in-side-window)
           (window-height . 0.16)
           (side . top)
           (slot . -1))
          ("\\*\\(Flymake diagnostics\\|Package-Lint\\).*"
           (display-buffer-in-side-window)
           (window-height . 0.16)
           (side . top)
           (slot . 0))
          ("\\*Messages.*"
           (display-buffer-in-side-window)
           (window-height . 0.16)
           (side . top)
           (slot . 1))
          ("\\*\\(Backtrace\\|Warnings\\|Compile-Log\\|Flymake log\\)\\*"
           (display-buffer-in-side-window)
           (window-height . 0.16)
           (side . top)
           (slot . 2))
          ;; left side window
          ("\\*\\(.* # Help.*\\|Help\\)\\*"    ; See the hooks for `visual-line-mode'
           (display-buffer-reuse-mode-window display-buffer-in-side-window)
           (window-width . 0.25)
           (side . left)
           (slot . 0))
          ;; right side window
          ("\\*keycast\\*"
           (display-buffer-in-side-window)
           (dedicated . t)
           (window-width . 0.25)
           (side . right)
           (slot . -1)
           (window-parameters . ((no-other-window . t)
                                 (mode-line-format . none))))
          ;; bottom side window
          ("\\*Org Select\\*"
           (display-buffer-in-side-window)
           (dedicated . t)
           (side . bottom)
           (slot . 0)
           (window-parameters . ((mode-line-format . none))))
          ;; bottom buffer (NOT side window)
          ("\\*Embark Actions\\*"
           (display-buffer-reuse-mode-window display-buffer-at-bottom)
           (window-height . fit-window-to-buffer)
           (window-parameters . ((no-other-window . t)
                                 (mode-line-format . none))))
          ("\\*\\(Embark\\)?.*Completions.*"
           (display-buffer-reuse-mode-window display-buffer-at-bottom)
           (window-parameters . ((no-other-window . t))))
          ("\\*\\(Output\\|Register Preview\\).*"
           (display-buffer-reuse-mode-window display-buffer-at-bottom))
          ;; below current window
          ("\\*.*\\(e?shell\\|v?term\\).*"
           (display-buffer-reuse-mode-window display-buffer-below-selected))
          ("\\*\\vc-\\(incoming\\|outgoing\\|git : \\).*"
           (display-buffer-reuse-mode-window display-buffer-below-selected)
           ;; NOTE 2021-10-06: we cannot `fit-window-to-buffer' because
           ;; the height is not known in advance.
           (window-height . 0.2))
          ("\\*\\(Calendar\\|Bookmark Annotation\\).*"
           (display-buffer-reuse-mode-window display-buffer-below-selected)
           (window-height . fit-window-to-buffer))))
  (setq window-combination-resize t)
  (setq even-window-sizes 'height-only)
  (setq window-sides-vertical nil)
  (setq switch-to-buffer-in-dedicated-window 'pop)

  (add-hook 'help-mode-hook #'visual-line-mode)
  (add-hook 'custom-mode-hook #'visual-line-mode)

  (let ((map global-map))
    (define-key map (kbd "C-x <down>") #'next-buffer)
    (define-key map (kbd "C-x <up>") #'previous-buffer)
    (define-key map (kbd "C-x C-n") #'next-buffer)     ; override `set-goal-column'
    (define-key map (kbd "C-x C-p") #'previous-buffer) ; override `mark-page'
    (define-key map (kbd "C-x !") #'delete-other-windows-vertically)
    (define-key map (kbd "C-x _") #'balance-windows)      ; underscore
    (define-key map (kbd "C-x -") #'fit-window-to-buffer) ; hyphen
    (define-key map (kbd "C-x +") #'balance-windows-area)
    (define-key map (kbd "C-x }") #'enlarge-window)
    (define-key map (kbd "C-x {") #'shrink-window)
    (define-key map (kbd "C-x >") #'enlarge-window-horizontally) ; override `scroll-right'
    (define-key map (kbd "C-x <") #'shrink-window-horizontally)) ; override `scroll-left'
  (let ((map resize-window-repeat-map))
    (define-key map ">" #'enlarge-window-horizontally)
    (define-key map "<" #'shrink-window-horizontally)))

4.3.2. Window history (winner-mode)

Also check: Directional window motions (windmove).

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. As noted in the section about the tab-bar, the main problem with Winner is that it cannot keep parallel histories, each for a given tab (see Tabs for window layouts (tab-bar.el and prot-tab.el)). The alternative is to use tab-bar-history-mode and the commands it provides or, more specifically, the wrappers I have defined which fall back to Winner when tabs are not in use.

;;; Window history (winner-mode)
(prot-emacs-builtin-package 'winner
  (add-hook 'after-init-hook #'winner-mode)

  ;; ;; NOTE 2021-07-31: Those are superseded by the commands
  ;; ;; `prot-tab-winner-undo' and `prot-tab-winner-redo' in prot-tab.el
  ;; ;; (search this document).
  ;; (let ((map global-map))
  ;;   (define-key map (kbd "<M-s-right>") #'winner-redo)
  ;;   (define-key map (kbd "<M-s-left>") #'winner-undo))
  )

4.3.3. Directional window motions (windmove)

Windmove is also built into Emacs. It provides functions for selecting a window in any of the cardinal directions: a decent addition to the simpler other-window command (C-x o by default). It also has commands for deleting a window in the given direction as well as for switching the current window with the one in the given direction. I do not need the directional deletion motions, as they require extra key bindings while I feel that it is easy enough to select a window and delete it outright with delete-window (on C-x 0 by default).

The windmove-create-window variable 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 by mistake where more than those it would actually speed up my workflow.

Also read: Window history (winner-mode).

;;; Directional window motions (windmove)
(prot-emacs-builtin-package 'windmove
  (setq windmove-create-window nil)     ; Emacs 27.1
  (let ((map global-map))
    ;; Those override some commands that are already available with
    ;; C-M-u, C-M-f, C-M-b.
    (define-key map (kbd "C-M-<up>") #'windmove-up)
    (define-key map (kbd "C-M-<right>") #'windmove-right)
    (define-key map (kbd "C-M-<down>") #'windmove-down)
    (define-key map (kbd "C-M-<left>") #'windmove-left)
    (define-key map (kbd "C-M-S-<up>") #'windmove-swap-states-up)
    (define-key map (kbd "C-M-S-<right>") #'windmove-swap-states-right) ; conflicts with `org-increase-number-at-point'
    (define-key map (kbd "C-M-S-<down>") #'windmove-swap-states-down)
    (define-key map (kbd "C-M-S-<left>") #'windmove-swap-states-left)))

4.3.4. Tabs for window layouts (tab-bar.el and prot-tab.el)

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

Starting with Emacs 28, the tab-bar can re-use indicators from the mode line. With a bit of tweaking (and dirty hacks—but, hey, they work!) we can hide all mode lines and replace them with a single status bar that spans the length of the frame (just like the Tmux status line, if you have ever used that). I was aware of this possibility for a long time, as I keep track of developments in emacs.git, but I never actually tinkered with the available options… Until Philip Kaludercic published the article Emacs 28 has a global mode line (2021-07-29) which inspired me to finally start experimenting.

As of this writing (2021-07-30), upstream Emacs only covers the part of the mode line that shows the current time and the like (technically the global-mode-string). So my prot-tab.el library, which is reproduced after the following package configurations, implements some more indicators that I want to use.

The minor mode which takes care of this new "status line" concept is prot-tab-status-line. Check the code to notice the dirty hacks I alluded to.

In the past, I was using a command which would merely toggle the display of the tab-bar (prot-tab-bar-toggle), but now that I can finally avoid the duplication of information and keep things cleaner, I think I am not going back to the paradigm where each window has its own mode line.

To learn about the key bindings that the tab-bar uses, type its prefix key C-x t and follow it up with C-h (read How do you learn Emacs?).

Finally, now that tab-bar-history-mode is a de facto replacement for winner-mode. Like Winner, it stores a history of window layouts. Unlike Winner, it keeps histories that are specific to each tab. The problem with Winner is that when we switch between tabs, it continues to retain a linear history, so when we try to undo in one tab, we effectively get the state of the previous one. As such, my two commands prot-tab-winner-undo and prot-tab-winner-redo provide thin wrappers around the two modes. If tabs are present, then we use the history for them, else we fall back to Winner.

Here are my settings, followed by the entirety of prot-tab.el.

;;; Tabs for window layouts (tab-bar.el and prot-tab.el)
(prot-emacs-builtin-package 'tab-bar
  (setq tab-bar-close-button-show nil)
  (setq tab-bar-close-last-tab-choice nil)
  (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-current)

  (tab-bar-mode -1)                     ; see `prot-tab-status-line'

  ;; Same concept as `winner-mode'.  See the `prot-tab-winner-undo' and
  ;; its counterpart.
  (tab-bar-history-mode 1))

(prot-emacs-builtin-package 'prot-tab
  (setq prot-tab-tab-select-num-threshold 3)
  (setq tab-bar-format                    ; Emacs 28
        '(prot-tab-format-space-single
          prot-tab-format-mule-info
          prot-tab-format-modified
          tab-bar-format-tabs-groups
          prot-tab-format-space-double
          prot-tab-format-position
          prot-tab-format-space-double
          prot-tab-format-vc
          prot-tab-format-space-double
          prot-tab-format-modes         ; FIXME 2021-07-30: Make it work with `minions'.
          tab-bar-format-align-right
          prot-tab-format-misc-info
          prot-tab-format-space-double
          tab-bar-format-global
          prot-tab-format-space-single))

  (add-hook 'after-init-hook #'prot-tab-status-line)

  (let ((map global-map))
    (define-key map (kbd "C-x <right>") #'prot-tab-winner-redo)
    (define-key map (kbd "C-x <left>") #'prot-tab-winner-undo)
    (define-key map (kbd "<f8>") #'prot-tab-status-line) ; unopinionated alternative: `prot-tab-bar-toggle'
    (define-key map (kbd "C-x t t") #'prot-tab-select-tab-dwim)))

;; ;; This is only included as a reference.
;; (prot-emacs-builtin-package 'tab-line
;;   (global-tab-line-mode -1))

The prot-tab.el code, which is in my dotfiles' repo:

;;; prot-tab.el --- Tab bar (tab-bar.el) extras for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; This set of configurations pertains to my tab-bar.el extensions, for
;; use in my Emacs setup: https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'tab-bar)

(defgroup prot-tab ()
  "Extensions for tab-bar.el."
  :group 'tab-bar)

(defcustom prot-tab-tab-select-num-threshold 3
  "Minimum number of tabs to prompt for numeric selection.
This is used by `prot-tab-select-tab-dwim' to determine whether
it should prompt for completion, or to ask for just a tab number
to switch to.  If the number of open tabs is greater than this
variable's value, then the command will prompt for a number."
  :type 'integer
  :group 'prot-tab)

;;;; General commands

(defun prot-tab--tab-bar-tabs ()
  "Return a list of `tab-bar' tabs, minus the current one."
  (mapcar (lambda (tab)
            (alist-get 'name tab))
          (tab-bar--tabs-recent)))

;;;###autoload
(defun prot-tab-select-tab-dwim (&optional arg)
  "Do-What-I-Mean function for getting to a `tab-bar' tab.
If no other tab exists, or with optional prefix argument
ARG (\\[universal-argument]), create one and switch to it.

If there is one other tab (so two in total) switch to it without
further questions.

If the tabs are more than `prot-tab-tab-select-num-threshold',
show numeric hints (`tab-bar-tab-hints') and prompt for a number
to switch to.  Else prompt for full text completion."
  (interactive "P")
  (let ((tabs (prot-tab--tab-bar-tabs)))
    (cond
     ((or arg (null tabs))
      (tab-new))
     ((length= tabs 1)
      (tab-next))
     ((length> tabs (1- prot-tab-tab-select-num-threshold))
      (let ((tab-bar-tab-hints t)
            (bar tab-bar-mode))
        (unwind-protect
            (progn
              (unless bar
                (prot-tab-bar-toggle 1))
              (tab-bar-select-tab
               (read-number "Go to tab NUM: ")))
          (unless bar
            (prot-tab-bar-toggle -1)))))
     (t
      (tab-bar-switch-to-tab
       (completing-read "Select tab: " tabs nil t))))))

;;;###autoload
(define-minor-mode prot-tab-bar-toggle
  "Toggle `tab-bar' presentation."
  :init-value nil
  :global t
  (if (or prot-tab-bar-toggle
          (not (bound-and-true-p tab-bar-mode)))
      (progn
        (setq tab-bar-show t)
        (tab-bar-mode 1))
    (setq tab-bar-show nil)
    (tab-bar-mode -1)))

;;;; Window layout history

(declare-function winner-undo "winner")
(declare-function winner-redo "winner")

;;;###autoload
(defun prot-tab-winner-undo ()
  "Go to previous window layout in the history.
When Tab-Bar-Mode and Tab-Bar-History-Mode are active, use
history that is specific to the current tab.  Else try to call
`winner-undo' if Winner-Mode is active.  Signal an error
otherwise."
  (interactive)
  (if (and (bound-and-true-p tab-bar-mode)
           (bound-and-true-p tab-bar-history-mode))
      (progn
        (tab-bar-history-back)
        (setq this-command 'tab-bar-history-back))
    (if (bound-and-true-p winner-mode)
        (progn
          (winner-undo)
          (setq this-command 'winner-undo))
      (user-error "No `tab-bar-history-mode' or `winner-mode' active"))))

;;;###autoload
(defun prot-tab-winner-redo ()
  "Go to next window layout in the history.
When Tab-Bar-Mode and Tab-Bar-History-Mode are active, use
history that is specific to the current tab.  Else try to call
`winner-redo' if Winner-Mode is active.  Signal an error
otherwise."
  (interactive)
  (if (and (bound-and-true-p tab-bar-mode)
           (bound-and-true-p tab-bar-history-mode))
      (progn
        (tab-bar-history-forward)
        (setq this-command 'tab-bar-history-forward))
    (if (bound-and-true-p winner-mode)
        (progn
          (winner-redo)
          (setq this-command 'winner-redo))
      (user-error "No `tab-bar-history-mode' or `winner-mode' active"))))

;;;; Indicators for `tab-bar-format' --- EXPERIMENTAL

(defun prot-tab-format-mule-info ()
  "Format `mode-line-mule-info' for the tab bar."
  `((global menu-item ,(string-trim-right (format-mode-line mode-line-mule-info)) ignore)))

(defun prot-tab-format-modified ()
  "Format `mode-line-modified' for the tab bar."
  `((global menu-item ,(string-trim-right (format-mode-line mode-line-modified)) ignore)))

(defun prot-tab-format-modes ()
  "Format `mode-line-modes' for the tab bar."
  `((global menu-item ,(string-trim-right (format-mode-line mode-line-modes)) ignore)))

;; FIXME 2021-07-30: This does not update unless some other event takes
;; place, such as an ELDOC update.  Otherwise it updates every second.
(defun prot-tab-format-position ()
  "Format `mode-line-position' for the tab bar."
  `((global menu-item ,(string-trim-right (format-mode-line mode-line-position)) ignore)))

(defun prot-tab-format-vc ()
  "Format VC status for the tab bar."
  `((global menu-item ,(string-trim-right (format-mode-line vc-mode)) ignore)))

(defun prot-tab-format-misc-info ()
  "Format `mode-line-misc-info' for the tab bar."
  `((global menu-item ,(string-trim-right (format-mode-line mode-line-misc-info)) ignore)))

(defun prot-tab-format-space-single ()
  "Format space for the tab bar."
  `((global menu-item " " ignore)))

(defun prot-tab-format-space-double ()
  "Format double space for the tab bar."
  `((global menu-item "  " ignore)))

(defvar prot-tab--window-divider-place (default-value 'window-divider-default-places)
  "Last value of `window-divider-default-places'.
For use in Prot-Tab-Status-Line.")

(declare-function prot-notmuch-mail-indicator "prot-notmuch")

;; NOTE 2021-07-30: This is experimental and subject to review.
;;;###autoload
(define-minor-mode prot-tab-status-line
  "Make Tab bar a status line and configure the extras.
Hide the mode lines and change their colors."
  :global t
  :group 'prot-tab
  (if prot-tab-status-line
      (progn
        (setq tab-bar-show t)
        (tab-bar-mode 1)
        (tab-bar-history-mode 1)
        (setq window-divider-default-places t)
        (window-divider-mode 1)
        (display-time-mode 1)
        (when (featurep 'prot-notmuch)
          (prot-notmuch-mail-indicator 1))
        (custom-set-faces
         `(mode-line ((default :height 1 :box nil :overline nil :underline nil)
                      (((class color) (min-colors 88) (background light))
                       :background "#0000c0" ; OR ,@(list (face-attribute 'default :foreground))
                       :foreground "#0000c0")
                      (((class color) (min-colors 88) (background dark))
                       :background "#00bcff"
                       :foreground "#00bcff")
                      (t :inverse-video t)))
         `(mode-line-inactive ((default :height 1 :box nil :overline nil :underline nil)
                      (((class color) (min-colors 88) (background light))
                       :background "white"
                       :foreground "white")
                      (((class color) (min-colors 88) (background dark))
                       :background "black"
                       :foreground "black")))))
    (setq tab-bar-show nil)
    (tab-bar-mode -1)
    (tab-bar-history-mode -1)
    (setq window-divider-default-places prot-tab--window-divider-place)
    (window-divider-mode -1)
    (display-time-mode -1)
    (when (featurep 'prot-notmuch)
      (prot-notmuch-mail-indicator -1))
    (custom-set-faces
     `(mode-line (( )))
     `(mode-line-inactive (( ))))))

(provide 'prot-tab)
;;; prot-tab.el ends here

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

Remember that you can always repeat a command with C-x z. And if you have the right settings, you can repeat again just by pressing another z (see Repeatable key chords (repeat-mode)).

;;; Transposition and rotation of windows
(prot-emacs-elpa-package 'transpose-frame
  (let ((map global-map))
    (define-key map (kbd "C-x M-r") #'rotate-frame-clockwise)))

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

5.1. Built-in bookmarking framework (bookmark.el and prot-bookmark.el)

Bookmarks are compartments that store data persistently about a file, directory, Info node, Man or WoMan page, image-mode entry, Docview or pdf-tools document, Eshell buffer… Basically there can be a bookmark for anything that Emacs can access, as long as it has a handler function configured for it. Do M-x apropos-function and search for bookmark jump to see what is on offer (packages can add their own support).

When you set a bookmark with the command bookmark-set (bound by default to C-x r m), Emacs remembers both the current item and, when data persists on disk, the position of the point in it. The command prompts you for a name, which wil make it easier for you to retrieve the information afterwards. Alternatively, use bookmark-set-no-overwrite (C-x r M) to prevent yourself from overriding existing bookmarks (though I generally prefer to write meaningful names for my entries).

To access bookmarks with minibuffer completion, invoke the command bookmark-jump (C-x r b). To get an overview in a tabulated list, call list-bookmarks (C-x r l).

If you would like to browse bookmarks (and other sources such as recent files) through an all-in-one interface, use the consult-buffer command (see Enhanced minibuffer commands (consult.el and prot-consult.el)). Consult also offers the command consult-bookmark which is like the standard bookmark-jump except that it also has support for group headings (so all files are under one heading, all Info nodes in another). I thus consider it a drop-in replacement for C-x r b.

The list-bookmarks interface provides several commands for visiting, or deleting bookmarks. Use C-h m (describe-mode) to get a help buffer with an explanation of what those are (and remember to make best use of Emacs' self-documentation system: How do you learn Emacs?).

Internally, bookmarks are similar to registers. As such, I encourage you to read the official manual on the matter by evaluating this form: (info "(emacs) Registers"). Also watch my Primer on Emacs “registers” (2020-03-08).

bookmark.el offers a few customisation options, though I find that the out-of-the-box design works well for my needs. What I am still not sure about is the option to query for a longer form annotation (the variable bookmark-use-annotations) while recording a new bookmark. It pops up a buffer where you can write a note on what is about to be recorded. The note will appear in a separate buffer while visiting that bookmark (for its placement see Window rules and basic tweaks (window.el)). To skip annotating the bookmark, just do not insert any text and either kill the buffer or type C-c C-c to accept the empty annotation. In such a case, no buffer will pop up while visiting the bookmark. Perhaps a less intrusive alternative is to annotate bookmarks after the fact, through the list-bookmarks view by typing e with point over the entry of interest.

Speaking of the list-bookmarks view, items with an annotation have an asterisk prepended to their line. With point over such a line, type a to produce a buffer with the contents of the annotation, or A to show all bookmarks with their annotations or lack thereof.

On the presentation front, Emacs28 adds a fringe marker on the line where a bookmark is set or on the line where you land after visiting a bookmark. This is controlled by the variable bookmark-set-fringe-mark. While my prot-bookmark.el (reproduced after the following package configurations) defines some extra faces for the list-bookmarks view. Those help differentiate URLs from files, PDFs, and directories, though it is a work-in-progress and will likely cover more cases.

URL bookmarks are handled by my prot-eww.el extensions, by means of a custom handler that leverages the standard bookmark.el framework instead of the EWW-only bookmarking facility that is available by default (Simple HTML Renderer, Emacs Web Wowser, Elpher, prot-eww.el).

Also watch: Primer on Emacs bookmarks (2021-09-08).

;;; Built-in bookmarking framework (bookmark.el and prot-bookmark.el)
(prot-emacs-builtin-package 'bookmark
  (setq bookmark-use-annotations nil)
  (setq bookmark-automatically-show-annotations t)
  (setq bookmark-set-fringe-mark t) ; Emacs28

  (add-hook 'bookmark-bmenu-mode-hook #'hl-line-mode))

(prot-emacs-builtin-package 'prot-bookmark
  (prot-bookmark-extra-keywords 1))

Here is prot-bookmark.el (from my dotfiles' repo):

;;; prot-bookmark.el --- Bookmark extras for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Bookmark extras for my Emacs: <https://protesilaos.com/emacs/dotemacs/>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'prot-common)

(defgroup prot-bookmark ()
  "Bookmark extras for my dotemacs."
  :group 'matching)

;;;; Extend Bookmark menu font-lock

(defface prot-bookmark-url
  '((((class color) (min-colors 88) (background light))
     :foreground "#0000c0")
    (((class color) (min-colors 88) (background dark))
     :foreground "#00bcff")
    (t :foreground "blue"))
  "Face for URL bookmarks.")

(defface prot-bookmark-pdf
  '((((class color) (min-colors 88) (background light))
     :foreground "#7f1010")
    (((class color) (min-colors 88) (background dark))
     :foreground "#ffa0a0")
    (t :foreground "red"))
  "Face for PDF bookmarks.")

(defface prot-bookmark-directory
  '((((class color) (min-colors 88) (background light))
     :foreground "#0f3d8c")
    (((class color) (min-colors 88) (background dark))
     :foreground "#a0acef")
    (t :foreground "cyan"))
  "Face for directory bookmarks.")

;; TODO 2021-09-08: We should be able to filter out bookmarks from the
;; likes of Info and VC-Dir which set a file path even though they are
;; not really intended to be visited as files.
(defconst prot-bookmark-keywords
  `((,(concat "\\(.*\\)" " " prot-common-url-regexp)
     (1 '(bold prot-bookmark-url) t)
     (2 'prot-bookmark-url t))
    ("\\(.*\\)\\( [~/].*\\.pdf\\)"
     (1 '(bold prot-bookmark-pdf) t)
     (2 'prot-bookmark-pdf t))
    ("\\(.*\\)\\( [~/].*/$\\)"
     (1 '(bold prot-bookmark-directory) t)
     (2 'prot-bookmark-directory t))
    ("\\(.*org.*last-stored.*\\)"
     (1 'shadow t)))
  "Extra font-lock patterns for the Bookmark menu.")

;;;###autoload
(define-minor-mode prot-bookmark-extra-keywords
  "Apply extra font-lock rules to bookmark list buffers."
  :init-value nil
  :global t
  (if prot-bookmark-extra-keywords
      (progn
        (font-lock-flush (point-min) (point-max))
        (font-lock-add-keywords nil prot-bookmark-keywords nil)
        (add-hook 'bookmark-bmenu-mode-hook #'prot-bookmark-extra-keywords))
    (font-lock-remove-keywords nil prot-bookmark-keywords)
    (remove-hook 'bookmark-bmenu-mode-hook #'prot-bookmark-extra-keywords)
    (font-lock-flush (point-min) (point-max))))

(provide 'prot-bookmark)
;;; prot-bookmark.el ends here

5.1.1. Ibuffer-like bookmark list (blist.el)

NOTE 2021-09-16: Work-in-progress.

This is a new package by Sévère Durand which makes the listing of bookmarks look more organised and provides many functions for operating on them, just like how ibuffer does it:

  1. Built-in bookmarking framework (bookmark.el and prot-bookmark.el).
  2. Ibuffer and extras (dired-like buffer list manager).

Durand is also the author of the useful rlist package, which lets you see your registers in a list and, optionally, delete the ones you no longer need (Dired-like list for registers (rlist)).

;;; Ibuffer-like bookmark list (blist.el)
;; Project repo: <https://gitlab.com/mmemmew/blist>.  Its dependency is
;; `ilist', by the same author: <https://gitlab.com/mmemmew/blist>.
;;
;; I handle those manually via git, at least until they become available
;; through an ELPA.
;;
;; `prot-emacs-manual-package' is defined in my init.el
(prot-emacs-manual-package 'ilist)

(prot-emacs-manual-package 'blist
  (setq blist-expert t)
  (setq blist-discard-empty-p t)

  ;; NOTE 2021-09-16: This package is still in its early days.  Things
  ;; will change.
  (with-eval-after-load 'prot-eww
    (blist-define-criterion "eww" "EWW"
      (eq (bookmark-get-handler bookmark)
          #'prot-eww-bookmark-jump)))

  (with-eval-after-load 'prot-eshell
    (blist-define-criterion "eshell" "Eshell"
      (eq (bookmark-get-handler bookmark)
          #'prot-eshell-bookmark-jump)))

  (blist-define-criterion "info" "Info"
    (eq (bookmark-get-handler bookmark)
        #'Info-bookmark-jump))

  (with-eval-after-load 'pdf-tools
    (blist-define-criterion "pdf" "PDF"
      (eq (bookmark-get-handler bookmark)
          #'pdf-view-bookmark-jump-handler)))

  (setq blist-filter-groups
        (list
         (cons "EWW" #'blist-eww-p)
         (cons "Eshell" #'blist-eshell-p)
         (cons "PDF" #'blist-pdf-p)
         (cons "Info" #'blist-info-p)
         (cons "Default" #'blist-default-p)))

  (define-key global-map (kbd "C-x r l") #'blist-list-bookmarks))

5.2. Custom extensions for "focus mode" (prot-logos.el)

My prot-logos.el (copied verbatim after the package configurations) provides the necessary infrastructure for my preferred "focus mode" aesthetic. Everything is controlled by prot-logos-focus-mode. Bind that to a key and you are good to go. An overview of its components, which are contingent on other features:

Olivetti (centred buffer content)
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 covers that niche very nicely. It is not aggressive in its requirements, which is important to play well with my paragraph and fill-mode settings (Paragraphs and fill-mode (prot-fill.el)): it respects my existing line length 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 delegated to a specialised instrument. Thanks to Paul W. Rankin for providing such a nimble tool! For prot-logos Olivetti always gets activated.
variable-pitch-mode (mixed fonts)
This is a built-in mode that remaps the default face's font family to a proportionately spaced one (also see Font configurations (prot-fonts.el)). It can produce a prose-friendly presentation, especially if the variable-pitch face is set to some nice font family. As the effect is not particularly good in prog-mode buffers, due to misalignments in spacing and indentation, prot-logos only applies variable width fonts in text-mode buffers. The activation is further controlled by prot-logos-variable-pitch (off by default). Bear in mind that variable-pitch-mode is quite aggressive in its application, as it affects all other faces, unless the active theme (or some minor mode) makes provisions to retain fixed typographic spacing for those elements that require it, such as code blocks and inline code elements, tables, and indentation (refer to Modus themes (my highly accessible themes)).
org-tree-slide and org-indent
The former is a third-party package and the latter is part of the Org distribution. What the first does is convert headings into pseudo slides. While the other indents content visually, without actually affecting the underlying initial spacing, to match the heading's depth. Those two are disabled by default and the prot-logos-org-presentation toggle determines whether they should be activated.
Scroll lock
Sometimes you want the cursor to remain centred on the screen while your focus is on writing or reading. This is controlled by the variable prot-logos-scroll-lock (off by default), which controls the activation of the built-in scroll-lock-mode.
Modeline
The variable prot-logos-hidden-modeline (off by default) can be set to t to hide the modeline while entering the focused state. For me this is mostly useful for presentations.
Fringes
There is also a function that unconditionally disables fringes on the edge of the window. It ensures that we do not see that area and any indicators that may be placed on it while entering the focus state (refer to Fringe mode for the relevant configurations, while their overall presentation is controlled by the active theme).
Cursor
My prot-cursor.el defines some convenient extras for setting the overall style of the cursor: the shape and the blink rate. When in a "focus mode" we may want to have one particular style available, which differs from the default (for all the technicalities refer to Cursor appearance and tweaks (prot-cursor.el)).

All those combined contribute to an outcome that is appropriate for long reading or writing sessions, as well as presentations. I intentionally do not introduce any font-resizing effect, as my needs vary in that regard depending on the context (though do refer to the prot-fonts.el I linked to earlier).

For video demonstrations, albeit with earlier versions of my code, watch these:

;;; Custom extensions for "focus mode" (prot-logos.el)
(prot-emacs-builtin-package 'face-remap)

(prot-emacs-elpa-package 'olivetti
  (setq olivetti-body-width 0.7)
  (setq olivetti-minimum-body-width 80)
  (setq olivetti-recall-visual-line-mode-entry-state t))

(prot-emacs-elpa-package 'org-tree-slide
  (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
        (format "Presentation %s" (propertize "ON" 'face 'success)))
  (setq org-tree-slide-deactivate-message
        (format "Presentation %s" (propertize "OFF" 'face 'error)))
  (let ((map org-tree-slide-mode-map))
    (define-key map (kbd "<C-down>") #'org-tree-slide-display-header-toggle)
    (define-key map (kbd "<C-right>") #'org-tree-slide-move-next-tree)
    (define-key map (kbd "<C-left>") #'org-tree-slide-move-previous-tree)))

(prot-emacs-builtin-package 'prot-logos
  (setq prot-logos-org-presentation nil)
  (setq prot-logos-variable-pitch nil)
  (setq prot-logos-scroll-lock nil)
  (setq prot-logos-hidden-modeline t)
  (setq prot-logos-affect-prot-cursor t)
  (define-key global-map (kbd "<f9>") #'prot-logos-focus-mode))

And here is prot-logos.el in its totality. It is available as a file in my dotfiles' repo (same for all my Emacs libraries):

;;; prot-logos.el --- Extensions for my dotemacs to help read, write, present -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Extensions to help me read, write, present.  For use in my Emacs
;; setup: https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(defgroup prot-logos ()
  "Setup for reading and presenting text-heavy buffers."
  :group 'files)

(defcustom prot-logos-org-presentation nil
  "Org files should switch to presentation view.
This concerns cases where variable `prot-logos-focus-mode' is set
to non-nil and determines whether headings should be converted
into pseudo slides and indentation be adjusted accordingly."
  :type 'boolean
  :group 'prot-logos)

(defcustom prot-logos-variable-pitch nil
  "Non-programming buffers should switch to `variable-pitch-mode'.
In programming modes the default font is always used, as that is
assumed to be a monospaced typeface."
  :type 'boolean
  :group 'prot-logos)

(defcustom prot-logos-scroll-lock nil
  "Use centred scrolling while in focused view."
  :type 'boolean
  :group 'prot-logos)

(defcustom prot-logos-hidden-modeline nil
  "Hide the modeline."
  :type 'boolean
  :group 'prot-logos)


(defcustom prot-logos-affect-prot-cursor nil
  "Change the cursor style.
This expects the `prot-cursor.el' library."
  :type 'boolean
  :group 'prot-logos)

(defvar prot-logos--focus-mode-hook nil
  "Hook that runs from function `prot-logos-focus-mode'.")

;;;###autoload
(define-minor-mode prot-logos-focus-mode
  "Buffer-local wrapper mode for presentations.
Other tools should hook into `prot-logos--focus-mode-hook' to
introduce their effects.  Otherwise this minor mode has no effect
on its own."
  :init-value nil
  :global nil
  :lighter " -Λ-"           ; greek lambda majuscule
  (run-hooks 'prot-logos--focus-mode-hook))

(autoload 'buffer-face-mode "face-remap")
(autoload 'variable-pitch-mode "face-remap")

(defun prot-logos--variable-pitch-toggle ()
  "Make text use `variable-pitch' face, except for programming."
  (when (and prot-logos-variable-pitch
             (derived-mode-p 'text-mode))
    (if (or (bound-and-true-p buffer-face-mode)
            (not (bound-and-true-p prot-logos-focus-mode)))
        (variable-pitch-mode -1)
      (variable-pitch-mode 1))))

(add-hook 'prot-logos--focus-mode-hook #'prot-logos--variable-pitch-toggle)

(autoload 'olivetti-mode "olivetti")

(defun prot-logos--olivetti-toggle ()
  "Toggle the variable `olivetti-mode', if available."
  (if (or (bound-and-true-p olivetti-mode)
          (not (bound-and-true-p prot-logos-focus-mode)))
      (olivetti-mode -1)
    (olivetti-mode 1)))

(add-hook 'prot-logos--focus-mode-hook #'prot-logos--olivetti-toggle)

(defface prot-logos-fringe
  `((((class color) (background light))
     :background "#ffffff")
    (((class color) (background dark))
     :background "#000000")
    (t :background ,(face-attribute 'default :background)))
  "Face to remove background from fringes.
Only tested with the Modus themes.")

(defvar-local prot-logos--fringe-cookie nil
  "Cookie returned by `face-remap-add-relative'.")

(declare-function face-remap-add-relative "face-remap" (face &rest specs))
(declare-function face-remap-remove-relative "face-remap" (cookie))

(defun prot-logos--fringe-toggle ()
  "Toggle fringe visibility."
  (if (bound-and-true-p prot-logos-focus-mode)
      (progn
        (set-window-fringes (selected-window) 0 0)
        (setq prot-logos--fringe-cookie
              (face-remap-add-relative 'olivetti-fringe 'prot-logos-fringe)))
    (set-window-fringes (selected-window) nil)
    (face-remap-remove-relative prot-logos--fringe-cookie)))

(add-hook 'prot-logos--focus-mode-hook #'prot-logos--fringe-toggle)

(autoload 'org-tree-slide-mode "org-tree-slide")

(defun prot-logos--org-tree-slide-mode ()
  "Toggle variable `org-tree-slide-mode' if loaded and needed."
  (let* ((buf (window-buffer (get-mru-window)))
         (mode (with-current-buffer buf major-mode)))
    (when (and prot-logos-org-presentation
               (eq mode 'org-mode))
      (if (or (bound-and-true-p org-tree-slide-mode)
              (not (bound-and-true-p prot-logos-focus-mode)))
          (org-tree-slide-mode -1)
        (org-tree-slide-mode 1)))))

(add-hook 'prot-logos--focus-mode-hook #'prot-logos--org-tree-slide-mode)

(autoload 'org-indent-mode "org")

(defun prot-logos--org-indent-mode ()
  "Toggle variable `org-tree-slide-mode' if loaded and needed."
  (let* ((buf (window-buffer (get-mru-window)))
         (mode (with-current-buffer buf major-mode)))
    (when (and prot-logos-org-presentation
               (eq mode 'org-mode))
      (if (or (bound-and-true-p org-indent-mode)
              (not (bound-and-true-p prot-logos-focus-mode)))
          (org-indent-mode -1)
        (org-indent-mode 1)))))

(add-hook 'prot-logos--focus-mode-hook #'prot-logos--org-indent-mode)

(defun prot-logos--scroll-lock ()
  "Keep the point at the centre."
  (when prot-logos-scroll-lock
    (if (or (bound-and-true-p scroll-lock-mode)
            (not (bound-and-true-p prot-logos-focus-mode)))
        (scroll-lock-mode -1)
      (recenter nil)
      (scroll-lock-mode 1))))

(add-hook 'prot-logos--focus-mode-hook #'prot-logos--scroll-lock)

;; Based on Paul W. Rankin's code:
;; https://gist.github.com/rnkn/a522429ed7e784ae091b8760f416ecf8
(defun prot-logos--hidden-modeline ()
  "Toggle mode line visibility."
  (when prot-logos-hidden-modeline
    (if (or (null mode-line-format)
            (not (bound-and-true-p prot-logos-focus-mode)))
        (kill-local-variable 'mode-line-format)
      (setq-local mode-line-format nil)
      (force-mode-line-update))))

(add-hook 'prot-logos--focus-mode-hook #'prot-logos--hidden-modeline)

(defvar prot-logos--prot-cursor-preset nil
  "Current `prot-cursor-presets' preset.")

(defvar prot-cursor--style-hist)
(defvar prot-cursor--recovered-preset)
(declare-function prot-cursor-set-cursor "prot-cursor")

(defun prot-logos--prot-cursor-preset ()
  "Change cursor style using `prot-cursor.el'."
  (when prot-logos-affect-prot-cursor
    (if (bound-and-true-p prot-logos-focus-mode)
        (progn
          (cond
           (prot-cursor--style-hist
            (setq prot-logos--prot-cursor-preset
                  (intern (car prot-cursor--style-hist))))
           (prot-cursor--recovered-preset
            (setq prot-logos--prot-cursor-preset
                  prot-cursor--recovered-preset)))
          (prot-cursor-set-cursor 'box))
      (prot-cursor-set-cursor prot-logos--prot-cursor-preset))))

(add-hook 'prot-logos--focus-mode-hook #'prot-logos--prot-cursor-preset)

(provide 'prot-logos)
;;; prot-logos.el ends here

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

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. Study the entirety of my Completion framework and extras.

In the usls.el code I wanted to respect key binding conventions, so I did not bind any keys: this is a user-level customisation. The other options I have here are for the sake of visibility and are left to their default values.

The code for this project is on the USLS Gitlab repo and reproduced in the subsequent code block.

;;; USLS --- Unassuming Sidenotes of Little Significance
(prot-emacs-builtin-package 'usls
  (setq usls-directory (expand-file-name "~/Documents/notes/"))
  (setq usls-known-categories '("economics" "philosophy" "politics"))
  (setq usls-file-type-extension ".txt") ; {.txt,.org,.md}
  (setq usls-subdir-support nil)
  (setq usls-file-region-separator 'line) ; {'line,'heading, OR string of your choice}
  (setq usls-file-region-separator-heading-level 1)
  (setq usls-custom-header-function nil)

  (add-hook 'usls-mode-hook #'goto-address-mode)

  (let ((map global-map))               ; globally bound keys
    (define-key map (kbd "C-c n d") #'usls-dired)
    (define-key map (kbd "C-c n f") #'usls-find-file)
    (define-key map (kbd "C-c n a") #'usls-append-region-buffer-or-file)
    (define-key map (kbd "C-c n n") #'usls-new-note))
  (let ((map usls-mode-map))            ; only for usls buffers
    (define-key map (kbd "C-c n i") #'usls-id-insert)
    (define-key map (kbd "C-c n l") #'usls-follow-link)))

Here is the usls.el code, which is also part of my dotfiles' repo (as with all my Elisp code):

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

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Consult the project's README.

;;; Code:

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

;;; User-facing options

(defgroup usls ()
  "Simple tool for plain text notes."
  :group 'files
  :prefix "usls-")

(defcustom usls-directory (expand-file-name "~/Documents/notes/")
  "Directory for storing personal notes."
  :group 'usls
  :type 'directory)

(defcustom usls-known-categories '("economics" "philosophy" "politics")
  "List of strings with predefined categories for `usls-new-note'.

The implicit assumption is that a category is a single word.  If
you need a category to be multiple words long, use underscores to
separate them.  Do not use hyphens, as those are assumed to
demarcate distinct categories, per `usls--inferred-categories'.

Also see `usls-categories' for a dynamically generated list that
gets combined with this one in relevant prompts."
  :group 'usls
  :type '(repeat string))

(defcustom usls-subdir-support nil
  "Enable support for subdirectories in `usls-directory'.

The default workflow of USLS is to maintain a flat directory
where all the notes are stored in.  This allows us to omit the
common filesystem path and only show file names.

When set to non-nil, the usls workflow can handle subdirectories
at the expense of making all file names more verbose, as it needs
to include the complete path.

NOTE: such subdirectories must be created manually to make sure
that no destructive filesystem operations are performed by
accident."
  :group 'usls
  :type 'boolean)

(defcustom usls-file-type-extension ".txt"
  "File type extension for new USLS notes.

Available options cover plain text (.txt), Markdown (.md), and
Org (.org) formats."
  :group 'usls
  :type '(choice
          (const :tag "Plain text format" ".txt")
          (const :tag "Markdown format" ".md")
          (const :tag "Org format" ".org")))

(defcustom usls-file-region-separator 'line
  "Separator for `usls-new-note' delimiting the captured region.

The default value of 'line' produces a horizontal rule depending
on the `usls-file-type-extension'.

* For plain text and Markdown this results in the following
  string (without the quotes): '\\n\\n* * *\\n\\n'.  It means to put
  two new lines before and two after the three space-separated
  asterisks.  In practice, that means an empty line before and
  after.  This notation is a common way to denote a horizontal
  rule or page/section break and is a standard in Markdown.

* For Org files it produces five consecutive hyphens with
  newlines before and after ('\\n\\n-----\\n\\n').  This is the
  valid syntax for a horizontal rule in Org mode.

Option 'heading' produces a heading that is formatted according
to `usls-file-type-extension'.  Its text is 'Reference':

* For plain text, the formatting of the heading involves a series
  of hyphens below the heading's text, followed by an empty line.
  The length of the hyphens is equal to that of the heading's
  text.

* For Markdown and Org the heading is formatted per the
  respective major mode's syntax, plus an empty line before and
  after.

It is also possible to provide a string of your own.  This should
contain just the text that you wish to turn into a heading.  For
example, you want to use the word 'Captured region' instead of
'Reference', so provide only that.  Your input will be processed
according to `usls-file-type-extension' to offer the correct
heading format.  The result will mimic that of the aforementioned
options.

The level of the heading is controlled by the customisation
option `usls-file-region-separator-heading-level' and defaults to
1 (one # for Markdown or one * for Org)."
  :group 'usls
  :type '(choice
          (const :tag "Line with surrounding space (default)" line)
          (const :tag "A 'Reference' heading" heading)
          (string :tag "A heading with text of your choice")))

(defcustom usls-file-region-separator-heading-level 1
  "Heading level for `usls-file-region-separator'.
Has effect when `usls-file-type-extension' is either that for
Markdown or Org types."
  :group 'usls
  :type 'integer)

(defcustom usls-custom-header-function nil
  "Function to format headers for new files (EXPERIMENTAL!!!).

It should accept five arguments and catenate them as a string,
preferably with the appropriate new lines in place.  The
arguments are: title, date, categories, filename, id.  Those are
supplied by `usls-new-note'.

While all five arguments will be passed to this function, not all
of them need to be part of the output.  Users may prefer, for
example, to only include a title, a date, and a category.

For ideas on how to format such a function, refer to the source
code of `usls--file-meta-header'.

Although this customisation can be set globally, another viable
use-case is to `let' bind it in wrapper functions around
`usls-new-note'.  In that scenario, it could be desirable to also
set the value of `usls-file-type-extension', so as to generate a
different type of note than the default: such as to write
something in '.tex' while the default extension remains in tact.
In this case, users are expected to define a wrapper for
`usls-new-note' like this (without the backslashes that appear in
the source of this docstring):

  (defun my-usls-new-note-for-tex ()
    (let ((usls-file-type-extension \".tex\")
          (usls-custom-header-function #'my-usls-custom-header))
      (usls-new-note)))"
  :group 'usls
  :type '(choice (const nil) function))

;;; Main variables

(defconst usls-id "%Y%m%d_%H%M%S"
  "Format of ID prefix of a note's filename.")

(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\\|md\\|org\\)")
  "Regular expression to match file names from `usls-new-note'.")

(defvar usls--file-link-regexp "^\\(@@\\|\\^^\\) \\(.*\\.\\)\\(txt\\|md\\|org\\)"
  "Regexp for file links.")

;;;; Input history lists

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

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

(defvar usls--file-history '()
  "Used internally by `usls-find-file' to record file names.")

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

(defvar usls--subdirectory-history '()
  "Used internally by `usls-new-note' to record subdirectories.")

;;; Basic utilities

;; Contributed by Omar Antolín Camarena in another context:
;; <https://github.com/oantolin>.
(defun usls--completion-table (category candidates)
  "Pass appropriate metadata CATEGORY to completion CANDIDATES."
  (lambda (string pred action)
    (if (eq action 'metadata)
        `(metadata (category . ,category))
      (complete-with-action action candidates string pred))))

(defvar crm-separator)

;; Contributed by Igor Lima in another context :
;; <https://github.com/0x462e41>.
(defun usls-crm-exclude-selected-p (input)
  "Filter out INPUT from `completing-read-multiple'.
Hide non-destructively the selected entries from the completion
table, thus avoiding the risk of inputting the same match twice.

To be used as the PREDICATE of `completing-read-multiple'."
  (if-let* ((pos (string-match-p crm-separator input))
            (rev-input (reverse input))
            (element (reverse
                      (substring rev-input 0
                                 (string-match-p crm-separator rev-input))))
            (flag t))
      (progn
        (while pos
          (if (string= (substring input 0 pos) element)
              (setq pos nil)
            (setq input (substring input (1+ pos))
                  pos (string-match-p crm-separator input)
                  flag (when pos t))))
        (not flag))
    t))

(defvar usls-mode)

(defun usls--barf-non-text-usls-mode ()
  "Throw error if not in a proper USLS buffer."
  (unless (and usls-mode (derived-mode-p 'text-mode))
    (user-error "Not in a writable USLS buffer; aborting")))

;;;; File name helpers

(defun usls--directory ()
  "Valid name format for `usls-directory'."
  (file-name-as-directory usls-directory))

(defun usls--extract (regexp str &optional group)
  "Extract REGEXP from STR, with optional regexp GROUP."
  (when group
    (unless (and (integerp group) (> group 0))
      (error "`%s' is not a positive integer" group)))
  (with-temp-buffer
    (insert str)
    (when (re-search-forward regexp nil t -1)
      (match-string (or group 1)))))

(defvar usls--punctuation-regexp "[][{}!@#$%^&*()_=+'\"?,.\|;:~`‘’“”]*"
  "Regular expression of punctionation that should be removed.")

(defun usls--slug-no-punct (str)
  "Convert STR to a file name slug."
  (replace-regexp-in-string usls--punctuation-regexp "" str))

(defun usls--slug-hyphenate (str)
  "Replace spaces with hyphens in STR.
Also replace multiple hyphens with a single one and remove any
trailing hyphen."
  (replace-regexp-in-string
   "-$" ""
   (replace-regexp-in-string
    "-\\{2,\\}" "-"
    (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-flat ()
  "List `usls-directory' files, assuming flat directory."
  (let ((dotless directory-files-no-dot-files-regexp))
    (cl-remove-if
     (lambda (x)
       ;; TODO: generalise this for all VC backends?  Which ones?
       (or (string-match-p "\\.git" x)
           (file-directory-p x)))
     (directory-files (usls--directory) nil dotless t))))

(defun usls--directory-files-recursive ()
  "List `usls-directory' files, assuming directory tree."
    (cl-remove-if
     (lambda (x)
       ;; TODO: generalise this for all VC backends?  Which ones?
       (string-match-p "\\.git" x))
     (directory-files-recursively (usls--directory) ".*" nil t)))

(defun usls--directory-files ()
  "List directory files."
  (let ((path (usls--directory)))
    (unless (file-directory-p path)
      (make-directory path t))
    (if usls-subdir-support
        (usls--directory-files-recursive)
      (usls--directory-files-flat))))

(defun usls--directory-subdirs ()
  "Return list of subdirectories in `usls-directory'."
  (cl-remove-if-not
   (lambda (x)
     (file-directory-p x))
   (directory-files-recursively (usls--directory) ".*" t t)))

;; TODO: generalise this for all VC backends?  Which ones?
(defun usls--directory-subdirs-no-git ()
  "Remove .git directories from `usls--directory-subdirs'."
  (cl-remove-if
   (lambda (x)
     (string-match-p "\\.git" x))
   (usls--directory-subdirs)))

(defun usls--directory-subdirs-completion-table (dirs)
  "Match DIRS as a completion table."
  (let ((def (car usls--subdirectory-history))
        (table (usls--completion-table 'file dirs)))
    (completing-read
     (format "Subdirectory of new note [%s]: " def)
     table nil t nil 'usls--subdirectory-history def)))

(defun usls--directory-subdirs-prompt ()
  "Handle user input on choice of subdirectory."
  (let* ((subdirs
          (if (null (usls--directory-subdirs-no-git))
              (user-error "No subdirs in `%s'; create them manually"
                          (usls--directory))
            (usls--directory-subdirs-no-git)))
         (choice (usls--directory-subdirs-completion-table subdirs))
         (subdir (file-truename choice)))
    (add-to-history 'usls--subdirectory-history choice)
    subdir))

;;;; Categories

(defun usls--categories-in-files ()
  "Produce list of categories in `usls--directory-files'."
  (cl-remove-if nil
   (mapcar (lambda (x)
             (usls--extract usls-category-regexp x 2))
           (usls--directory-files))))

(defun usls--inferred-categories ()
  "Extract categories from `usls--directory-files'."
  (let ((sequence (usls--categories-in-files)))
    (mapcan (lambda (s)
              (split-string s "-" t))
            sequence)))

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

(defun usls--categories-prompt ()
  "Prompt for one or more categories.
In the case of multiple entries, those are separated by the
`crm-sepator', which typically is a comma.  In such a case, the
output is sorted with `string-lessp'."
  (let* ((categories (usls-categories))
         (choice (completing-read-multiple
                  "File category: " categories
                  #'usls-crm-exclude-selected-p
                  nil nil 'usls--category-history)))
    (if (= (length choice) 1)
        (car choice)
      (sort choice #'string-lessp))))

(defun usls--categories-hyphenate (categories)
  "Format CATEGORIES output of `usls--categories-prompt'."
  (if (and (> (length categories) 1)
           (not (stringp categories)))
      (mapconcat #'downcase categories "-")
    categories))

(defun usls--categories-capitalize (categories)
  "`capitalize' CATEGORIES output of `usls--categories-prompt'."
  (if (and (> (length categories) 1)
           (not (stringp categories)))
      (mapconcat #'capitalize categories ", ")
    (capitalize categories)))

(defun usls--categories-add-to-history (categories)
  "Append CATEGORIES to `usls--category-history'."
  (if (and (listp categories)
           (> (length categories) 1))
      (let ((cats (delete-dups
                   (mapc (lambda (cat)
                           (split-string cat "," t))
                         categories))))
        (mapc (lambda (cat)
                (add-to-history 'usls--category-history cat))
              cats)
        (setq usls--category-history
              (cl-remove-if (lambda (x)
                              (string-match-p crm-separator x))
                            usls--category-history)))
    (add-to-history 'usls--category-history categories)))

;;; Templates

(defun usls--file-meta-header (title date categories filename id)
  "Front matter template based on `usls-file-type-extension'.

This helper function is meant to integrate with `usls-new-note'.
As such TITLE, DATE, CATEGORIES, FILENAME, ID are all retrieved
from there."
  (let ((cat (usls--categories-capitalize categories)))
    (pcase usls-file-type-extension
      ;; TODO: make those templates somewhat customisable.  We need to
      ;; determine what should be parametrised.
      (".md" `(concat "---" "\n"
                      "title:     " ,title "\n"
                      "date:      " ,date "\n"
                      "category:  " ,cat "\n"
                      "orig_name: " ,filename "\n"
                      "orig_id:   " ,id "\n"
                      "---" "\n\n"))
      (".org" `(concat "#+title:     " ,title "\n"
                       "#+date:      " ,date "\n"
                       "#+category:  " ,cat "\n"
                       "#+orig_name: " ,filename "\n"
                       "#+orig_id:   " ,id "\n\n"))
      (_ `(concat "title:     " ,title "\n"
                  "date:      " ,date "\n"
                  "category:  " ,cat "\n"
                  "orig_name: " ,filename "\n"
                  "orig_id:   " ,id "\n"
                  (make-string 26 ?-) "\n\n")))))

(defun usls--file-region-separator-heading-level (mark str)
  "Format MARK and STR for `usls--file-region-separator-str'.
MARK must be a single character string.  For multiple character
strings only the first one is used."
  (let ((num usls-file-region-separator-heading-level)
        (char (when (stringp mark)
                (string-to-char (substring mark 0 1)))))
    (format "\n\n%s %s\n\n" (make-string num char) str)))

(defun usls--file-region-separator-str ()
  "Produce region delimiter string for use in `usls-new-note'."
  (let* ((str (format "%s" usls-file-region-separator))
         (num (length str)))
    (pcase usls-file-region-separator
      ('line (pcase usls-file-type-extension
               (".org" (format "\n\n%s\n\n" (make-string 5 ?-)))
               (_ "\n\n* * *\n\n")))
      ('heading (pcase usls-file-type-extension
                  (".md" (usls--file-region-separator-heading-level "#" "Reference"))
                  (".org" (usls--file-region-separator-heading-level "*" "Reference"))
                  (_ (format "\n\nReference\n%s\n\n" (make-string 9 ?-)))))
      (_ (pcase usls-file-type-extension
           (".md" (usls--file-region-separator-heading-level "#" str))
           (".org" (usls--file-region-separator-heading-level "*" str))
           (_ (format "\n\n%s\n%s\n\n" str (make-string num ?-))))))))

;; This just silences the compiler for the subsequent function
(defvar eww-data)

;; TODO: get some link for gnus, mu4e?  What else?
(defun usls--file-region-source ()
  "Capture path to file or URL for `usls--file-region'."
  (cond
   ((derived-mode-p 'eww-mode)
    (if (plist-get eww-data :title)
        (format "%s <%s>\n\n" (plist-get eww-data :title) (plist-get eww-data :url))
      (concat (plist-get eww-data :url) "\n\n")))
   ((buffer-file-name)
    (concat (buffer-file-name) "\n\n"))
   (t
    "")))

(defun usls--file-region-separator (region)
  "`usls--file-region-separator-str' and `usls-new-note' REGION."
  `(concat
    (usls--file-region-separator-str)
    (usls--file-region-source)
    ,region))

(defun usls--file-region ()
  "Capture active region for use in `usls-new-note'."
  (if (use-region-p)
      (eval (usls--file-region-separator
             (buffer-substring-no-properties
              (region-beginning)
              (region-end))))
    ""))

(defun usls--file-region-append ()
  "Capture active region for use in `usls-append-region-buffer-or-file'."
  (if (use-region-p)
      (eval (buffer-substring-no-properties
             (region-beginning)
             (region-end)))
    ""))

;;; Commands and their helper functions

;;;; New note

(defun usls--format-file (path id categories slug extension)
  "Helper for `usls-new-note' to format file names.
PATH, ID, CATEGORIES, SLUG, AND EXTENSION are expected to be
supplied by `usls-new-note': they will all be converted into a
single string."
  (format "%s%s--%s--%s%s"
          path
          id
          categories
          slug
          extension))

;;;###autoload
(defun usls-new-note (&optional arg)
  "Create new note with the appropriate metadata and file name.
If the region is active, append it to the newly created file.

This command first prompts for a file title and then for a
category.  The latter supports completion.

To input multiple categories, separate them with a comma or
whatever the value of `crm-separator' is on your end.  While
inputting multiple categories, those already selected are removed
from the list of completion candidates, meaning that it is not
possible to select the same item twice.

With prefix key (\\[universal-argument]) as optional ARG also
prompt for a subdirectory of `usls-directory' to place the new
note in.  Subdirectories must already exist."
  (interactive "P")
  (let* ((subdir (when arg (usls--directory-subdirs-prompt)))
         (title (read-string "File title: " nil 'usls--title-history))
         (categories (usls--categories-prompt))
         (slug (usls--sluggify title))
         (path (file-name-as-directory (or subdir usls-directory)))
         (id (format-time-string usls-id))
         (filename (usls--format-file path id
                    (usls--categories-hyphenate categories)
                    slug usls-file-type-extension))
         (date (format-time-string "%F"))
         (region (usls--file-region)))
    (with-current-buffer (find-file filename)
      (insert (eval (if usls-custom-header-function
                        (funcall usls-custom-header-function title date
                                 categories filename id)
                      (usls--file-meta-header title date categories filename id))))
      (save-excursion (insert region)))
    (add-to-history 'usls--title-history title)
    (usls--categories-add-to-history categories)))

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

;;;; Insert reference

(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)
  (usls--barf-non-text-usls-mode)
  (let* ((files (usls--completion-table 'file (usls--directory-files-not-current)))
         (file (completing-read "Link to: " files 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)))


;;;; Follow links

(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
         (concat (match-string-no-properties 2)
                 (match-string-no-properties 3))
         links)))
    (cl-remove-duplicates links)))

;;;###autoload
(defun usls-follow-link ()
  "Visit link referenced in the note using completion.
If no links are available, call `usls-find-file'."
  (interactive)
  (usls--barf-non-text-usls-mode)
  (let* ((default-directory (usls--directory))
         (links (usls--links))
         (refs (when links (usls--completion-table 'file links))))
    (if refs
        (find-file
         (completing-read "Follow link: " refs nil t))
      (call-interactively 'usls-find-file))))

;;;; Find file

(defun usls--file-name (file)
  "Return properly formatted name of FILE."
  (if usls-subdir-support
     (file-truename file)
    (file-truename (concat (usls--directory) file))))

(defun usls--find-file-prompt ()
  "Completion prompt for `usls-find-file'."
  (let ((files (usls--completion-table 'file (usls--directory-files))))
    (completing-read "Visit file: " files nil t nil 'usls--file-history)))

;;;###autoload
(defun usls-find-file (file)
  "Visit a FILE in `usls-directory'.
When called interactively use completion."
  (interactive (list (usls--find-file-prompt)))
  (let* ((default-directory (usls--directory))
         (item (usls--file-name file)))
    (find-file item)
    (add-to-history 'usls--file-history file)))

;;;; Append to file

;; REVIEW: Maybe all those filtered lists can be simplified into maybe
;; one or two.  This feels needlessly complex.

(defun usls--window-buffer-list ()
  "Return list of windows."
  (mapcar (lambda (x)
            (window-buffer x))
          (window-list)))

(defun usls--window-buffer-file-names-list ()
  "Return file names in `usls--window-buffer-list'."
  (cl-remove-if nil
   (mapcar (lambda (x)
             (buffer-file-name x))
           (usls--window-buffer-list))))

(defun usls--window-usls-file-buffers ()
  "Return USLS files in `usls--window-buffer-file-names-list'."
  (let ((files (usls--directory-files-recursive))
        (buf-files (mapcar #'abbreviate-file-name (usls--window-buffer-file-names-list))))
    (cl-remove-if nil
     (mapcar (lambda (x)
               (when (member x files)
                 x))
             buf-files))))

(defun usls--window-usls-buffers ()
  "Return buffer names from `usls--window-usls-file-buffers'."
  (mapcar (lambda (x)
            (get-file-buffer x))
          (usls--window-usls-file-buffers)))

(defun usls--window-buffers-live ()
  "Return live windows matching `usls--window-usls-buffers'."
  (cl-remove-if-not (lambda (x)
                      (window-live-p x))
                    (mapcar (lambda (y)
                              (get-buffer-window y))
                            (usls--window-usls-buffers))))

(defun usls--window-buffers ()
  "Return buffer names in `usls--window-buffers-live'."
  (mapcar (lambda (x)
            (window-buffer x))
          (usls--window-buffers-live)))

(defun usls--window-single-buffer-or-prompt ()
  "Return buffer name if one, else prompt with completion."
  (let* ((buffers
          (delete-dups
           (mapcar (lambda (x)
                     (format "%s" x))
                   (usls--window-buffers))))
         (bufs (usls--completion-table 'buffer buffers))
         (buf (if (> (length buffers) 1)
                  (completing-read "Pick buffer: "
                                   bufs nil t)
                (if (listp buffers) (car buffers) buffers))))
    (unless (null buf)
      (get-buffer-window buf))))

(defun usls--window-buffer-or-file ()
  "Return window with a USLS buffer or prompt for a file."
  (let ((files (usls--directory-files)))
    (or (usls--window-single-buffer-or-prompt)
        (completing-read "Visit file: " files nil t nil 'usls--file-history))))

(defun usls--append-region (buf region arg)
  "Routines to append active region.
All of BUF, REGION, ARG are intended to be passed by another
function, such as with `usls-append-region-buffer-or-file'."
  (let ((window (get-buffer-window buf))
        (mark (gensym)))
    (with-current-buffer buf
      (goto-char (if (not (null arg)) (point-max) (window-point window)))
      (setq mark (point))
      (insert region)
      (goto-char mark))))

;;;###autoload
(defun usls-append-region-buffer-or-file (&optional arg)
  "Append active region to buffer or file.

To 'append' is to insert at point.  To insert at the end of text
instead, pass a \\[universal-argument] prefix argument ARG.

If there exist one or more windows whose buffers visit a file
found in `usls-directory', then they are used as targets for
appending the active region.  When multiple windows are
available, a minibuffer prompt with completion is provided to
select one among them.

When no such windows are live, the minibuffer prompt asks for a
file to visit.

The appended region is not preceded by a delimiter, as is the
case with `usls-new-note'."
  (interactive "P")
  (let* ((object (usls--window-buffer-or-file))
         (buf (when (windowp object) (window-buffer object)))
         (region (usls--file-region-append))
         (append (if arg t nil)))
    (if (bufferp buf)
        (usls--append-region buf region append)
      (usls--append-region (find-file (usls--file-name object)) region append)
      ;; Only add to history when we are dealing with a file
      (add-to-history 'usls--file-history object))))

;;;; Dired

;;;###autoload
(defun usls-dired (&optional arg)
  "Switch to `usls-directory' using `dired'.
With optional \\[universal-argument] prefix ARG prompt for a usls
subdirectory to switch to.  If none is available, the main
directory will be directly displayed instead."
  (interactive "P")
  (let ((path usls-directory)
        (subdirs (usls--directory-subdirs-no-git)))
    (unless (file-directory-p path)
      (user-error "`usls-directory' not found at %s" usls-directory))
    (if (and arg subdirs)
        (dired (usls--directory-subdirs-prompt))
      (dired path))))

;;; User-facing setup

(defvar usls-mode-map
  (let ((map (make-sparse-keymap)))
    map)
  "Key map for use when USLS mode is active.")

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

(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 (abbreviate-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 "#2544bb")
    (((class color) (min-colors 88) (background dark))
     :foreground "#79a8ff")
    (t :inherit font-lock-string-face))
  "Face for header date entry.")

(defface usls-header-data-category
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#5317ac")
    (((class color) (min-colors 88) (background dark))
     :foreground "#b6a0ff")
    (t :inherit font-lock-keyword-face))
  "Face for header category entry.")

(defface usls-header-data-title
  '((default :inherit bold)
    (((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 header title entry.")

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

(defface usls-header-data-key
  '((((class color) (min-colors 88) (background light))
     :foreground "#00538b")
    (((class color) (min-colors 88) (background dark))
     :foreground "#00d3d0")
    (t :inherit font-lock-variable-name-face))
  "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 "#005077")
    (((class color) (min-colors 88) (background dark))
     :foreground "#90c4ed")
    (t :inherit font-lock-variable-name-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 "#5317ac")
    (((class color) (min-colors 88) (background dark))
     :foreground "#b6a0ff")
    (t :inherit font-lock-keyword-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.")

;; TODO: re-use regular expressions as is already done for
;; `usls-file-regexp'.
(defconst usls-font-lock-keywords
  `((,usls-file-regexp
     (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))
    ("\\(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 t))
    ("^\\(-\\{26\\}\\|[*\s]\\{5\\}\\)$"
     (1 'usls-section-delimiter))
    ("\\(\\^\\)\\([0-9_]\\{15\\}\\)"
     (1 'escape-glyph)
     (2 'font-lock-variable-name-face))
    (,usls--file-link-regexp
     (1 'escape-glyph)
     (2 'font-lock-constant-face t)
     (3 'font-lock-constant-face t)))
  "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 t)
    (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

5.4. tmr.el (TMR Must Recur)

Sometimes I need to set off a timer with a notification. I used to rely on a homegrown shell script for such a task, but where is the fun in that?

This package of mine provides a single command for setting a timer: tmr.

The command prompts for a unit of time, which is represented as a string that consists of a number and, optionally, a single character suffix which specifies the unit of time.

Valid formats:

Input Meaning
5 5 minutes
5m 5 minutes
5s 5 seconds
5h 5 hours

If tmr is called with an optional prefix argument (C-u), it also asks for a description which accompanies the given timer. Preconfigured candidates are specified in the user option tmr-descriptions-list, though any arbitrary input is acceptable at the minibuffer prompt.

When the timer is set, a message is sent to the echo area recording the current time and the point in the future when the timer elapses. Echo area messages can be reviewed with the view-echo-area-messages which is bound to C-h e by default.

Once the timer runs its course, it produces a desktop notification and plays an alarm sound. The notification's message is practically the same as that which is sent to the echo area. The sound file for the alarm is defined in tmr-sound-file, while the urgency of the notification can be set through the tmr-notification-urgency option.

The tmr-cancel command cancels the last tmr. Note that for the time being it has no notion of multiple timers—just the last one.

;;; TMR Must Recur (just my generic timer)
(prot-emacs-builtin-package 'tmr
  (setq tmr-sound-file
        "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga")
  (setq tmr-notification-urgency 'normal)
  (setq tmr-descriptions-list (list "Boil water" "Prepare tea" "Bake bread"))
  (let ((map global-map))
    (define-key map (kbd "C-c t t") #'tmr)
    (define-key map (kbd "C-c t c") #'tmr-cancel)))

This is its code (from my dotfiles' repo):

;;; tmr.el --- TMR Must Recur -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; TMR Must Recur.  Else a timer for my Emacs setup:
;; <https://protesilaos.com/emacs/dotemacs>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'notifications)

(defgroup tmr ()
  "TMR Must Recur (super simple timer for my private use)."
  :group 'data)

(defcustom tmr-sound-file
  "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga"
  "Path to sound file used by `tmr--play-sound'."
  :type 'file
  :group 'tmr)

(defcustom tmr-notification-urgency 'normal
  "The urgency level of the desktop notification.
Values can be `low', `normal' (default), or `critical'."
  :type '(choice
          (const :tag "Low" low)
          (const :tag "Normal" normal)
          (const :tag "Critical" critical))
  :group 'tmr)

(defcustom tmr-descriptions-list (list "Boil water" "Prepare tea" "Bake bread")
  "Optional description candidates for the current `tmr'."
  :type '(repeat string)
  :group 'tmr)

(defun tmr--unit (time)
  "Determine common time unit for TIME."
  (cond
   ((and (stringp time)
         (string-match-p "[0-9]\\'" time))
    (let ((time (string-to-number time)))
      (* time 60)))
   ((natnump time)
    (* time 60))
   (t
    (let* ((unit (substring time -1))
           (str (substring time 0 -1))
           (num (abs (string-to-number str))))
      (pcase unit
        ("s" num)
        ("h" (* num 60 60))
        ;; This is not needed, of course, but we should not miss a good
        ;; chance to make some fun of ourselves.
        ("w" (user-error "TMR Made Ridiculous; append [m]inutes, [h]ours, [s]econds"))
        (_ (* num 60)))))))

(defun tmr--play-sound ()
  "Play `tmr-sound-file' using the 'ffplay' executable (ffmpeg)."
  (let ((sound tmr-sound-file))
    (when (file-exists-p tmr-sound-file)
      (unless (executable-find "ffplay")
        (user-error "Cannot play %s without `ffplay'" sound))
      (call-process-shell-command
       (format "ffplay -nodisp -autoexit %s >/dev/null 2>&1" sound) nil 0))))

(defun tmr--notify-send (start &optional description)
  "Send system notification for timer with START time.
Optionally include DESCRIPTION."
  (let ((end (format-time-string "%T"))
        (desc-plain)
        (desc-propertized))
    (if description
        (setq desc-plain (concat "\n" description)
              desc-propertized (concat " [" (propertize description 'face 'bold) "]"))
      (setq desc-plain ""
            desc-propertized ""))
    ;; Read: (info "(elisp) Desktop Notifications")
    (notifications-notify
     :title "TMR Must Recur"
     :body (format "Time is up!\nStarted: %s\nEnded: %s%s"
                   start end desc-plain)
     :app-name "GNU Emacs"
     :urgency tmr-notification-urgency
     :sound-file tmr-sound-file)
    ;; TODO 2021-10-01: Maybe add those messages to a tmr buffer?
    (message
     "TMR %s %s ; %s %s%s"
     (propertize "Start:" 'face 'success) start
     (propertize "End:" 'face 'warning) end
     desc-propertized)
    (unless (plist-get (notifications-get-capabilities) :sound)
      (tmr--play-sound))))

;; TODO 2021-09-21: Maybe we should use a list instead of storing just
;; the last one?
(defvar tmr--last-timer nil
  "Last timer object, used by `tmr-cancel'.")

;;;###autoload
(defun tmr-cancel ()
  "Cancel last timer object set with `tmr' command."
  (interactive)
  (if tmr--last-timer
      (cancel-timer tmr--last-timer)
    (message "No `tmr' to cancel")))

(defun tmr--echo-area (time &optional description)
  "Produce `message' for current `tmr' TIME.
Optionally include DESCRIPTION."
  (let* ((specifier (substring time -1))
         (amount (substring time 0 -1))
         (start (format-time-string "%T"))
         (unit (pcase specifier
                 ("s" (format "%ss (s == second)" amount))
                 ("h" (format "%sh (h == hour)" amount))
                 (_   (concat time "m (m == minute)")))))
    (message "`tmr' started at %s for %s%s"
             ;; Remember: these are just faces.  Don't get caught in the
             ;; semantics.
             (propertize start 'face 'success)
             (propertize unit 'face 'error)
             (if description
                 (concat " [" (propertize description 'face 'bold) "]")
               ""))))

(defvar tmr--description-hist '()
  "Minibuffer history of `tmr' descriptions.")

(defun tmr--description-prompt ()
  "Helper prompt for descriptions in `tmr'."
  (let ((def (nth 0 tmr--description-hist)))
    (completing-read
     (format "Description for this tmr [%s]: " def)
     tmr-descriptions-list nil nil nil
     'tmr--description-hist def)))

;;;###autoload
(defun tmr (time &optional description)
  "Set timer to TIME duration and notify after it elapses.

When TIME is a number, it is interpreted as a count of minutes.
Otherwise TIME must be a string that consists of a number and a
special final character denoting a unit of time: 'h' for 'hours',
's' for 'seconds'.

With optional DESCRIPTION as a prefix (\\[universal-argument]),
prompt for a description among `tmr-descriptions-list', though
allow for any string to serve as valid input.

This command also plays back `tmr-sound-file'.

To cancel the timer, use the `tmr-cancel' command."
  (interactive
   (list
    (read-string "N minutes for timer (append `h' or `s' for other units): ")
    (when current-prefix-arg (tmr--description-prompt))))
  (let ((start (format-time-string "%T"))
        (unit (tmr--unit time)))
    (tmr--echo-area time description)
    (setq tmr--last-timer
          (run-with-timer
           unit nil
           'tmr--notify-send start description))))

(provide 'tmr)
;;; tmr.el ends here

5.5. Version control tools

5.5.1. Diff-mode (and prot-diff.el extensions)

This covers the standard diff-mode.el, which I use quite often, such as while interfacing with the built-in Version Control framework (see the section on Version control framework (vc.el and prot-vc.el)), or while browsing various code-related mailing lists through notmuch (refer to the email settings).

Overview of my preferences for how diffs should look:

  • 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 (diff-apply-hunk with C-c C-a) 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. 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.

Now some notes on my prot-diff.el extensions, combined with a description of the basics of diff-mode (as always you can get documentation about the current buffer's major mode with C-h m—read How do you learn Emacs? in the FAQ section appended to this document):

  • prot-diff-buffer-dwim will produce a diff that compares the current buffer to the last saved state of the underlying file. If the buffer has no unsaved edits, the command will produce a diff that compares the file to its last registered version-controlled state. Calling the command with an optional prefix argument (C-u) will enable word-wise highlighting across the diff.
  • prot-diff-refine-cycle is how I manually control word-wise diff highlights. This command has a buffer-local cyclic behaviour. The first time it is called, it acts on the diff hunk at point. Upon second invocation, it operates on the entire buffer. And on third call it removes the word-wise diffs altogether.
  • prot-diff-narrow-dwim narrows to the diff hunk at point. If narrowing is already present, it widens the buffer. When invoked with an optional prefix argument, it narrows to the current file.
  • 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).
  • 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.
  • With M-n and M-p you move between hunks. With M-} and M-{ or M-N, M-P do the same between files.

The prot-diff-* commands are part of my prot-diff.el library, reproduced in its entirety after this set of package configurations.

Pro tip: enable outline-minor-mode to make diff sections foldable. Check Outline mode, outline minor mode, and extras (prot-outline.el).

Also read these sections:

Changes to all tracked files are optionally highlighted in the fringe thanks to the diff-hl package by Dmitry Gutov (refer to the section on Line numbers and relevant indicators (prot-sideline.el)). 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).

;;; Diff-mode (and prot-diff.el extensions)
(prot-emacs-builtin-package 'diff-mode
  (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
  ;; The following is further controlled by
  ;; `prot-diff-modus-themes-diffs'
  (setq diff-font-lock-syntax 'hunk-also))

(prot-emacs-builtin-package 'prot-diff
  (prot-diff-modus-themes-diffs)
  (add-hook 'modus-themes-after-load-theme-hook #'prot-diff-modus-themes-diffs)

  (prot-diff-extra-keywords 1)

  ;; `prot-diff-buffer-dwim' replaces the default for `vc-diff' (which I
  ;; bind to another key---see VC section).
  (define-key global-map (kbd "C-x v =") #'prot-diff-buffer-dwim)
  (let ((map diff-mode-map))
    (define-key map (kbd "C-c C-b") #'prot-diff-refine-cycle) ; replace `diff-refine-hunk'
    (define-key map (kbd "C-c C-n") #'prot-diff-narrow-dwim)))

This is prot-diff.el (part of my dotfiles' repo):

;;; prot-diff.el --- Extensions to diff-mode.el for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; This covers my diff-mode.el extensions, for use in my Emacs setup:
;; https://protesilaos.com/emacs/dotemacs.
;;
;; Make sure to also inspect prot-vc.el and prot-project.el for a more
;; complete view of what I have on the topic of version control.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'diff-mode)

(defgroup prot-diff ()
  "Extensions for diff mode."
  :group 'diff)

;;;###autoload
(defun prot-diff-buffer-dwim (&optional arg)
  "Diff buffer with its file's last saved state, or run `vc-diff'.
With optional prefix ARG (\\[universal-argument]) enable
highlighting of word-wise changes (local to the current buffer)."
  (interactive "P")
  (let ((buf))
    (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)
        (unless diff-refine
          (setq-local diff-refine 'font-lock))))))

(defvar-local prot-diff--refine-diff-state 0
  "Current state of `prot-diff-refine-dwim'.")

;;;###autoload
(defun prot-diff-refine-cycle ()
  "Produce buffer-local, 'refined' or word-wise diffs in Diff mode.

Upon first invocation, refine the diff hunk at point or, when
none exists, the one closest to it.  On second call, operate on
the entire buffer.  And on the third time, remove all word-wise
fontification."
  (interactive)
  (let ((point (point)))
    (pcase prot-diff--refine-diff-state
      (0
       (diff-refine-hunk)
       (setq prot-diff--refine-diff-state 1))
      (1
       (setq-local diff-refine 'font-lock)
       (font-lock-flush)
       (goto-char point)
       (setq prot-diff--refine-diff-state 2))
      (_
       (revert-buffer)
       (goto-char point)
       (recenter)
       (setq prot-diff--refine-diff-state 0)))))

;;;###autoload
(defun prot-diff-narrow-dwim (&optional arg)
  "Use `diff-restrict-view', or widen when already narrowed.
By default the narrowing effect applies to the focused diff hunk.
With optional prefix ARG (\\[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")))))

(defvar modus-themes-diffs)

;;;###autoload
(defun prot-diff-modus-themes-diffs ()
  "Configure `diff-font-lock-syntax' for accessibility.

A non-nil value for that variable will apply fontification to the
text while also trying to add the familiar diff styles.  This can
easily result in inaccessible colour combinations.

My Modus themes, which are designed for the highest accessibility
standard in legibility, provide an option that can work well with
such non-nil values.  Otherwise `diff-font-lock-syntax' should be
set to nil.

Run this function at the post theme load phase, such as with the
hook `modus-themes-after-load-theme-hook'."
  (if (eq modus-themes-diffs 'bg-only)
      (setq diff-font-lock-syntax 'hunk-also)
    (setq diff-font-lock-syntax nil)))

;;; Extend diff-mode font lock

(defface prot-diff-diffstat-added
  '((t :inherit diff-indicator-added))
  "Face for diffstat added indicators (+).")

(defface prot-diff-diffstat-removed
  '((t :inherit diff-indicator-removed))
  "Face for diffstat removed indicators (-).")

(defface prot-diff-commit-header
  '((((class color) (min-colors 88) (background light))
     :foreground "#000000")
    (((class color) (min-colors 88) (background dark))
     :foreground "#ffffff"))
  "Face for diff commit header keys like 'Author:'.")

(defface prot-diff-commit-hash
  '((((class color) (min-colors 88) (background light))
     :foreground "#184034")
    (((class color) (min-colors 88) (background dark))
     :foreground "#bfebe0")
    (t :inherit shadow))
  "Face for diff commit unique identifier (hash).")

(defface prot-diff-commit-author
  '((((class color) (min-colors 88) (background light))
     :foreground "#00538b")
    (((class color) (min-colors 88) (background dark))
     :foreground "#00d3d0")
    (t :foreground "cyan"))
  "Face for diff commit author name.")

(defface prot-diff-commit-email
  '((((class color) (min-colors 88) (background light))
     :foreground "#0031a9")
    (((class color) (min-colors 88) (background dark))
     :foreground "#2fafff")
    (t :foreground "blue"))
  "Face for diff commit author email.")

(defface prot-diff-commit-date
  '((((class color) (min-colors 88) (background light))
     :foreground "#55348e")
    (((class color) (min-colors 88) (background dark))
     :foreground "#cfa6ff")
    (t :foreground "magenta"))
  "Face for diff commit date.")

(defface prot-diff-commit-subject
  '((((class color) (min-colors 88) (background light))
     :foreground "#005a5f")
    (((class color) (min-colors 88) (background dark))
     :foreground "#6ae4b9")
    (t :foreground "cyan"))
  "Face for diff commit message subject.")

;; NOTE 2021-01-30: These work in all scenaria I tried, but there may
;; still be errors or omissions.
(defconst prot-diff-keywords
  '(("\\(^[^+@-]?\\)\\(.*?\s+|\s+\\)\\([0-9]*\\) \\(\\++\\)"
     ;; (2 'prot-diff-diffstat-file-changed)
     (4 'prot-diff-diffstat-added))
    ("\\(^[^+-]?\\)\\(\\+\\{3\\}\\) \\([ab].*?\\)"
     (2 'prot-diff-diffstat-added))
    ("\\(^[^+-]?\\)\\(-+\\{3\\}\\) \\([ab].*?\\)"
     (2 'prot-diff-diffstat-removed))
    ("\\(^[^+@-]?\\)\\(.*?\s+|\s+\\)\\([0-9]*\\) \\(\\++\\)?\\(-+\\)"
     ;; (2 'prot-diff-diffstat-file-changed)
     (5 'prot-diff-diffstat-removed))
    ;; ("\\([0-9]+ files? changed,.*\\)"
    ;;  (0 'prot-diff-diffstat-file-changed))
    ("^---\n"
     (0 'prot-diff-commit-header))
    ("\\(^commit \\)\\(.*\\)"
     (1 'prot-diff-commit-header)
     (2 'prot-diff-commit-hash))
    ("\\(^Author: \\)\\(.*\\)\\(<\\)\\(.*\\)\\(>\\)"
     (1 'prot-diff-commit-header)
     (2 'prot-diff-commit-author)
     (3 'prot-diff-commit-header)
     (4 'prot-diff-commit-email)
     (5 'prot-diff-commit-header))
    ("\\(^From:\\|^To:\\|^Cc:\\) ?\\(.*\\)?\\(<\\)\\(.*\\)\\(>\\)"
     (1 'prot-diff-commit-header)
     (2 'prot-diff-commit-author)
     (3 'prot-diff-commit-header)
     (4 'prot-diff-commit-email)
     (5 'prot-diff-commit-header))
    ("\\(^Subject:\\) \\(.*\\)"
     (1 'prot-diff-commit-header)
     (2 'prot-diff-commit-subject))
    ("\\(^From\\)\\( [0-9a-zA-Z]+ \\)\\(.*\\)"
     (1 'prot-diff-commit-header)
     (2 'prot-diff-commit-hash)
     (3 'prot-diff-commit-date))
    ("\\(^Message-Id:\\) \\(<.+>\\)"
     (1 'prot-diff-commit-header)
     (2 'prot-diff-commit-hash))
    ("\\(^Date: \\)\\(.*\\)"
     (1 'prot-diff-commit-header)
     (2 'prot-diff-commit-date)))
  "Extra font-lock patterns for diff mode.")

;;;###autoload
(define-minor-mode prot-diff-extra-keywords
  "Apply extra font-lock rules to diff buffers."
  :init-value nil
  :global t
  (if prot-diff-extra-keywords
      (progn
        (font-lock-flush (point-min) (point-max))
        (font-lock-add-keywords nil prot-diff-keywords nil)
        (add-hook 'diff-mode-hook #'prot-diff-extra-keywords))
    (font-lock-remove-keywords nil prot-diff-keywords)
    (remove-hook 'diff-mode-hook #'prot-diff-extra-keywords)
    (font-lock-flush (point-min) (point-max))))

(provide 'prot-diff)
;;; prot-diff.el ends here

5.5.2. Version control framework (vc.el and prot-vc.el)

VC consists of set of libraries that provide the means for working with several version control systems, else "backends". It is built into Emacs. Compared with magit (see section on Magit configs), vc offers a more abstract, buffer-oriented paradigm that does a fine job at covering all basic versioning needs. It however never stands as Magit's peer when it comes to the sheer coverage of Git features.

To my mind, VC and Magit can be used as part of the same setup. Employ the former for common tasks such as viewing diffs and logs, committing changes in bulk, pushing and pulling from a remote. And let Magit handle the more involved and specialised cases of staging a partial diff, rebasing commits interactively, writing a commit fixup, and so on.

Also refer to the section on Diff-mode (and prot-diff.el extensions) which includes various neat extras, such as extra fontification rules for diff buffers. And watch my videos on this topic:

  1. Introduction to the Emacs Version Control framework (2020-03-30).
  2. Extensions for Emacs' vc-git (2021-01-22).
  3. My workflow with VC for Git projects (2021-02-08).

Here is an overview of the keys I define, with only a few of them being left to their default values (note that prot-diff-buffer-dwim is part of the prot-diff.el that I linked to above):

Command C-x v prefix Mnemonic
vc-update F Fetch and Fuse
vc-push P  
vc-log-incoming f Fetch only
vc-log-outgoing O  
vc-create-tag t  
vc-retrieve-tag b Branch/tag switch
vc-diff d Diff current file
vc-root-diff D Diff project
prot-diff-buffer-dwim = Equality check
prot-vc-project-or-dir p Project status
prot-vc-custom-log SPC  
prot-vc-git-patch-apply a Apply patch
prot-vc-git-patch-create-dwim c Create patch
prot-vc-git-show s Show commit
prot-vc-git-grep g  
prot-vc-git-log-grep G  
prot-vc-git-find-revision r Revisit version
prot-vc-git-blame-region-or-file B Blame
prot-vc-git-log-insert-commits i Insert commit log
prot-vc-git-reset R Reset (–soft)

My prot-vc.el library (reproduced after the package configurations) defines several commands that extend VC to suit my needs as a Git user. Check the key maps I assign those commands to, in order to further appreciate the scope of each action. In short:

  • prot-vc-git-grep is a simple wrapper around vc-git-grep. Instead of asking for a directory and a file extension pattern, it just prompts for a regexp and performs the search across the entire VC-controlled directory tree. All matches are placed directly in a buffer.
  • prot-vc-git-log-edit-extract-file-name is used in log-edit buffers to derive the file name of the item being committed. For example, as I am writing this, I may want to compose a summary of my changes like "prot-emacs: expand VC section docs". The "prot-emacs: " part comes directly from this command, which reads from the "prot-emacs.org" file. If there are multiple files to be committed, then a minibuffer prompt asks to pick one among them. This helps me write clean and meaningful summaries.
  • The commands prot-vc-git-log-edit-{next,previous,complete}-comment are used to access information about previous commit messages that are stored in the dedicated ring. The next/previous operations will cycle through the ring in the given direction. While the "complete" command will use minibuffer completion to insert the select item at point.
  • prot-vc-git-log-insert-commits will simply insert at point N number of commits starting from the HEAD of the current project. The number is inserted at the minibuffer following a prompt. This runs the git log shell command in the background. If the command is not called from inside a version-controlled directory or if it is invoked with a prefix argument (C-u), it first asks for a project and eventually prints a log for it. Again, this is useful while writing the message of a commit, as I occasionally need to reference earlier changes.
  • prot-vc-git-patch-apply prompts for a file system path to a patch file. By default, it applies the patch directly to the current git repo. If no such repository is found or if called with an optional prefix argument the command prompts for a project to use. This makes it very simple to apply a patch from anywhere, such as while reading my email (also refer to Email settings). Arguments that are passed to it are those specified in prot-vc-git-patch-apply-args, unless the command is called from Lisp, in which case it accepts ARGS as a list of strings (read the git am man pages if you ever need this).
  • prot-vc-git-patch-create-dwim produces a properly formatted patch for a given commit. The outputted file is saved in a directory that is selected via a minibuffer completion prompt: default candidates are stored in prot-vc-patch-output-dirs and are complemented by the root of the current project.
    • When browsing a log-view buffer, the commit is the one around point.
    • When the region is active in Log View buffers, the command will capture the included range of commits, instead of just the one at point.
    • With a prefix argument (C-u), a minibuffer completion prompt will ask for a commit to use as the base in a range against HEAD. This will skip the check for the commit at point, though an active region in Log View buffers will still take precedence.
    • Beware of how Git interprets those ranges: the base commit is the one before the earliest in the range, so if you need to produce patches for the topmost 4 commits, you must include the last 5 in the region.
    • That granted, I also use git-email.el for preparing patches that must be sent via email outright.
  • prot-vc-git-checkout-remote prompts for a remote branch and proceeds to checkout a local branch that is set up to track it. So if you have a remote named origin/dev it will do git checkout -b dev origin/dev. I only use this command inside VC-dir buffers.
  • prot-vc-custom-log prints a log of commits that matches a custom file set. This is of great value when you need to inspect the history of only some files rather than that of the entire repository. What files to choose is determined in two ways: (1) the file-at-point in Dired buffers, or all marked files, and (2) files in the current directory selected with minibuffer completion.
  • prot-vc-log-view-toggle-entry-all will toggle the visibility of all commits in a compact log view. I often employ this in tandem with prot-vc-custom-log.
  • prot-vc-git-show lets you read a given commit that you access with completion. A simple and effective wrapper for git show.
  • prot-vc-git-log-grep provides a search utility for commit logs. It accepts a regular expression, which may just be a string, and shows all commits whose message includes that pattern. When called with a universal prefix argument (C-u), the log will also include the corresponding diff of each commit.
  • prot-vc-git-find-revision allows you to revisit a previous state of the current file, by selecting a commit with completion. Quite powerful when you want to search, for example, my dotemacs from when I first introduced a certain package, say, prot-vc.el.
  • prot-vc-git-reset prompts for a commit to reset back to, using minibuffer completion. This is a "soft" undo process in that all changes are kept in place while any commits are removed. Remember to only do this for local logs as it is not good practice to reset publicly available histories: it will break the local copies of other users.
  • prot-vc-git-log-reset is like the above command, only that it is meant to be called from inside a Log View buffer (e.g. vc-print-root-log which is bound to C-x v L by default). The commit to reset back to is the one at point. Calling the command with a prefix argument (C-u) will change the meaning of the reset operation from a soft to a hard one. The latter deletes all commits up to the selected commit and removes all changes, so please be careful.
  • prot-vc-project-or-dir produces a vc-dir buffer for the current project (also see Projects (project.el and prot-project.el)). With a C-u prefix argument the command limits the matches to the present directory.
  • prot-vc-log-kill-hash appends to the kill-ring the hash of the commit around point. It is meant to be used in log-view buffers.
  • prot-vc-git-setup-mode is a minor mode that refashions the log edit buffer while adding a small tweak to the log view buffers.
    • Normally the log edit buffer (what you use to write the commit message) will pop up in a window with a smaller window below it showing the files to be committed. The window layout does not automatically show the corresponding diff, while there is no readily available information as to what branch we are about to commit the changes to. So my minor mode removes the small window with the files and in its stead adds a comment block in the main message composition buffer (like the standard git commit). It then displays the diff window on one side and the edit buffer on the other (yes, just like Magit, though the order of the windows is always the same). The prior window configuration and the point are saved before entering the log edit session and immediately restored upon exit (either by committing the changes or aborting).
    • The behaviour of cycling the ring of prior commits is reworked to account for the custom git comment. In addition to back/forth motions through the ring's items (M-p, M-n), a command for picking a commit message with minibuffer completion is also made available in the stead of the generic commands for searching through the ring, with M-s or M-r (the defaults lack visual feedback and are, in my opinion, not appropriate for the task).
    • The Amend pseudo header is displayed by default to make it easier to edit the last commit, if necessary, and to raise awareness about this possibility.
    • For the log view buffers (commit logs) the minor mode instructs the command that expands the message of a commit on the current line to include more information from git log than what it normally would. It shows diff stats and affected file names, while also creating some much needed negative space for better usability. Those file names are not purely cosmetic, as they can now serve to power Emacs' contextuality and "future history" such as when you put point over the name and type C-x p f (project-find-file): the file at point becomes the default choice and the one you will also get with M-n in the minibuffer (next-history-element).

Finally, a few tips for acting in the log-edit buffer (remember to use C-h m (M-x describe-mode) in every unfamiliar major mode and read the manual for more on the matter):

  • Use C-c C-d (log-edit-show-diff) to produce a diff of the changes to-be-committed. Of course this is of no use if my aforementioned minor mode is enabled. Still, it is good to know (by the way, this command also works in Magit's commit composition buffers).
  • With C-c C-w (log-edit-generate-changelog-from-diff) generate an overview of documented changes to the given file set. While this may not be useful for everyday commits, it is mandatory when preparing patches for upstream Emacs (and probably other GNU projects).
  • Normally the window layout is set up to include files for the given commit, but I disable that via my minor mode. You can opt to display them with C-c C-f (log-edit-show-files).
  • C-c C-k (log-edit-kill-buffer) cancels the log editing process.
  • M-n (log-edit-next-comment) and M-p (log-edit-previous-comment) let you cycle through prior commit messages.
;;; Version control framework (vc.el and prot-vc.el)
(prot-emacs-builtin-package 'vc
  ;; Those offer various types of functionality, such as blaming,
  ;; viewing logs, showing a dedicated buffer with changes to affected
  ;; files.
  (require 'vc-annotate)
  (require 'vc-dir)
  (require 'vc-git)
  (require 'add-log)
  (require 'log-view)

  ;; This one is for editing commit messages.
  (require 'log-edit)
  (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)

  ;; Note that `prot-vc-git-setup-mode' will run the following when
  ;; activated:
  ;;
  ;;   (remove-hook 'log-edit-hook #'log-edit-show-files)
  ;;
  ;; If you need the window to pop back up, do it manually with C-c C-f
  ;; which calls `log-edit-show-files'.

  (setq vc-find-revision-no-save t)
  (setq vc-annotate-display-mode 'scale) ; scale to oldest
  ;; I use a different account for git commits
  (setq add-log-mailing-address "info@protesilaos.com")
  (setq add-log-keep-changes-together t)
  (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))))

  (add-hook 'log-view-mode-hook #'hl-line-mode)

  ;; NOTE: I override lots of the defaults
  (let ((map global-map))
    (define-key map (kbd "C-x v b") #'vc-retrieve-tag)  ; "branch" switch
    (define-key map (kbd "C-x v t") #'vc-create-tag)
    (define-key map (kbd "C-x v f") #'vc-log-incoming)  ; the actual git fetch
    (define-key map (kbd "C-x v o") #'vc-log-outgoing)
    (define-key map (kbd "C-x v F") #'vc-update)        ; "F" because "P" is push
    (define-key map (kbd "C-x v d") #'vc-diff))
  (let ((map vc-dir-mode-map))
    (define-key map (kbd "b") #'vc-retrieve-tag)
    (define-key map (kbd "t") #'vc-create-tag)
    (define-key map (kbd "O") #'vc-log-outgoing)
    (define-key map (kbd "o") #'vc-dir-find-file-other-window)
    (define-key map (kbd "f") #'vc-log-incoming) ; replaces `vc-dir-find-file' (use RET)
    (define-key map (kbd "F") #'vc-update)       ; symmetric with P: `vc-push'
    (define-key map (kbd "d") #'vc-diff)         ; parallel to D: `vc-root-diff'
    (define-key map (kbd "k") #'vc-dir-clean-files)
    (define-key map (kbd "G") #'vc-revert)
    (let ((prot-vc-git-branch-map (make-sparse-keymap)))
      (define-key map "B" prot-vc-git-branch-map)
      (define-key prot-vc-git-branch-map "n" #'vc-create-tag) ; new branch/tag
      (define-key prot-vc-git-branch-map "s" #'vc-retrieve-tag) ; switch branch/tag
      (define-key prot-vc-git-branch-map "c" #'prot-vc-git-checkout-remote) ; "checkout" remote
      (define-key prot-vc-git-branch-map "l" #'vc-print-branch-log))
    (let ((prot-vc-git-stash-map (make-sparse-keymap)))
      (define-key map "S" prot-vc-git-stash-map)
      (define-key prot-vc-git-stash-map "c" 'vc-git-stash) ; "create" named stash
      (define-key prot-vc-git-stash-map "s" 'vc-git-stash-snapshot)))
  (let ((map vc-git-stash-shared-map))
    (define-key map "a" 'vc-git-stash-apply-at-point)
    (define-key map "c" 'vc-git-stash) ; "create" named stash
    (define-key map "D" 'vc-git-stash-delete-at-point)
    (define-key map "p" 'vc-git-stash-pop-at-point)
    (define-key map "s" 'vc-git-stash-snapshot))
  (let ((map vc-annotate-mode-map))
    (define-key map (kbd "M-q") #'vc-annotate-toggle-annotation-visibility)
    (define-key map (kbd "C-c C-c") #'vc-annotate-goto-line)
    (define-key map (kbd "<return>") #'vc-annotate-find-revision-at-line))
  (let ((map log-view-mode-map))
    (define-key map (kbd "<tab>") #'log-view-toggle-entry-display)
    (define-key map (kbd "<return>") #'log-view-find-revision)
    (define-key map (kbd "s") #'vc-log-search)
    (define-key map (kbd "o") #'vc-log-outgoing)
    (define-key map (kbd "f") #'vc-log-incoming)
    (define-key map (kbd "F") #'vc-update)
    (define-key map (kbd "P") #'vc-push)))

(prot-emacs-builtin-package 'prot-vc
  (setq prot-vc-log-limit 100)
  (setq prot-vc-log-bulk-action-limit 50)
  (setq prot-vc-git-log-edit-show-commits t)
  (setq prot-vc-git-log-edit-show-commit-count 10)
  (setq prot-vc-shell-output "*prot-vc-output*")
  (setq prot-vc-patch-output-dirs (list "~/" "~/Desktop/"))
  (add-to-list' log-edit-headers-alist '("Amend"))

  ;; This refashions log view and log edit buffers
  (prot-vc-git-setup-mode 1)

  ;; NOTE: I override lots of the defaults
  (let ((map global-map))
    (define-key map (kbd "C-x v i") #'prot-vc-git-log-insert-commits)
    (define-key map (kbd "C-x v p") #'prot-vc-project-or-dir)
    (define-key map (kbd "C-x v SPC") #'prot-vc-custom-log)
    (define-key map (kbd "C-x v g") #'prot-vc-git-grep)
    (define-key map (kbd "C-x v G") #'prot-vc-git-log-grep)
    (define-key map (kbd "C-x v a") #'prot-vc-git-patch-apply)
    (define-key map (kbd "C-x v c") #'prot-vc-git-patch-create-dwim)
    (define-key map (kbd "C-x v s") #'prot-vc-git-show)
    (define-key map (kbd "C-x v r") #'prot-vc-git-find-revision)
    (define-key map (kbd "C-x v B") #'prot-vc-git-blame-region-or-file)
    (define-key map (kbd "C-x v R") #'prot-vc-git-reset))
  (let ((map vc-git-log-edit-mode-map))
    (define-key map (kbd "C-C C-n") #'prot-vc-git-log-edit-extract-file-name)
    (define-key map (kbd "C-C C-i") #'prot-vc-git-log-insert-commits)
    ;; Also done by `prot-vc-git-setup-mode', but I am putting it here
    ;; as well for visibility.
    (define-key map (kbd "C-c C-c") #'prot-vc-git-log-edit-done)
    (define-key map (kbd "C-c C-a") #'prot-vc-git-log-edit-toggle-amend)
    (define-key map (kbd "M-p") #'prot-vc-git-log-edit-previous-comment)
    (define-key map (kbd "M-n") #'prot-vc-git-log-edit-next-comment)
    (define-key map (kbd "M-s") #'prot-vc-git-log-edit-complete-comment)
    (define-key map (kbd "M-r") #'prot-vc-git-log-edit-complete-comment))
  (let ((map log-view-mode-map))
    (define-key map (kbd "<C-tab>") #'prot-vc-log-view-toggle-entry-all)
    (define-key map (kbd "a") #'prot-vc-git-patch-apply)
    (define-key map (kbd "c") #'prot-vc-git-patch-create-dwim)
    (define-key map (kbd "R") #'prot-vc-git-log-reset)
    (define-key map (kbd "w") #'prot-vc-log-kill-hash)))

And here is prot-vc.el (part of my dotfiles' repo):

;;; prot-vc.el --- Extensions to vc.el for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; This covers my vc.el extensions, which mostly concern Git.  For use
;; in my Emacs setup: https://protesilaos.com/emacs/dotemacs.
;;
;; Make sure to also inspect prot-project.el and prot-diff.el for a more
;; complete view of what I have on the topic of version control.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'vc)
(require 'log-edit)
(require 'prot-common)

;;;; Customisation options

(defgroup prot-vc ()
  "Extensions for vc.el and related libraries."
  :group 'project)

(defcustom prot-vc-log-limit 100
  "Limit commits in `prot-vc-custom-log' and others."
  :type 'integer
  :group 'prot-vc)

(defcustom prot-vc-log-bulk-action-limit 50
  "Limit for `prot-vc-log-view-toggle-entry-all'.
This is to ensure that performance does not take a hit.  The
default value is conservative."
  :type 'integer
  :group 'prot-vc)

(defcustom prot-vc-git-log-edit-show-commits nil
  "Show recent commits in Git Log Edit comments."
  :type 'boolean
  :group 'prot-vc)

(defcustom prot-vc-git-log-edit-show-commit-count 10
  "Commit number for `prot-vc-git-log-edit-show-commits'."
  :type 'integer
  :group 'prot-vc)

(defcustom prot-vc-shell-output "*prot-vc-output*"
  "Name of buffer for VC-related shell output."
  :type 'string
  :group 'prot-vc)

(defcustom prot-vc-patch-output-dirs (list "~/" "~/Desktop/")
  "List of directories to save `prot-vc-patch-dwim' output."
  :type 'list
  :group 'prot-vc)

(defcustom prot-vc-git-patch-apply-args (list "--3way")
  "List of strings to pass as arguments to 'git am'."
  :type '(repeat string)
  :group 'prot-vc)

;;;; Commands and helper functions

(defun prot-vc--current-project ()
  "Return root directory of current project."
  (or (vc-root-dir)
      (locate-dominating-file "." ".git")))

;;;###autoload
(defun prot-vc-project-or-dir (&optional arg)
  "Run `vc-dir' for the current project root.
With optional prefix ARG (\\[universal-argument]), use the
`default-directory' instead."
  (interactive "P")
  (let* ((root (prot-vc--current-project))
         (dir (if arg default-directory root)))
    (vc-dir dir)))

(defun prot-vc--log-edit-files-prompt ()
  "Helper completion for `prot-vc-extract-file-name'."
  (let ((files (log-edit-files)))
    (completing-read
     "Derive shortname from: " files nil nil)))

;;;###autoload
(defun prot-vc-git-log-edit-extract-file-name ()
  "Insert at point shortname from file in log edit buffers.
If multiple files are part of the log, a minibuffer completion
prompt will be produced: it can be used to narrow down to an
existing item or input an arbitrary string of characters."
  (interactive)
  (unless (derived-mode-p 'log-edit-mode)
    (user-error "Only try this in Log Edit mode"))
  (let* ((files (log-edit-files))
         (file (if (> (length files) 1)
                   (prot-vc--log-edit-files-prompt)
                 (car files)))
         (name (file-name-sans-extension
                (file-name-nondirectory
                 file))))
    (insert (concat name ": "))))

(autoload 'project-current "project")

(defvar prot-vc--log-insert-num-hist '()
  "History for `prot-vc-git-log-insert-commits'.")

(declare-function project-prompt-project-dir "project")

;;;###autoload
(defun prot-vc-git-log-insert-commits (&optional arg)
  "Insert at point number of commits starting from git HEAD.
If in a version-controlled directory, the commit log is based on
the root of the project, else a prompt for project selection is
produced with `project-current'.

With optional prefix ARG (\\[universal-argument]) always prompt
for a known project."
  (interactive "P")
  (let* ((dir (when arg (project-prompt-project-dir)))
         (default-directory (or dir
                                (prot-vc--current-project)
                                (cdr (project-current t))))
         (number (number-to-string
                  (read-number "Insert N commits from HEAD: " 5
                               'prot-vc--log-insert-num-hist))))
    (insert
     (with-temp-buffer
       (apply 'vc-git-command t nil nil
              (list "log" "--pretty=format:%h  %cs  %s" "-n" number "--"))
       (buffer-string)))
    (add-to-history 'prot-vc--log-insert-num-hist number)))

(autoload 'log-view-current-entry "log-view")
(autoload 'dired-get-marked-files "dired")

(defun prot-vc--commit-num ()
  "Determime whether `prot-vc-log-limit' is a positive integer."
  (let ((num prot-vc-log-limit))
    (if (and (integerp num)
             (> num 0))
        num
      (error "'%s' is not a valid number" num))))

;;;###autoload
(defun prot-vc-custom-log (&optional arg)
  "Like `vc-print-log' but for a custom fileset.

With optional prefix ARG (\\[universal-argument]), prompt for a
number to limit the log to.  Then prompt the user for matching
files in the `default-directory' with `completing-read-multiple'.
The default limit is controlled by the `prot-vc-log-limit'
variable.

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.

With a double prefix ARG, 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)
                (prot-vc--commit-num)))
         (dir default-directory)
         (dotless directory-files-no-dot-files-regexp)
         (files (directory-files dir nil dotless t))
         (set (cond                     ; REVIEW: this is confusing
               ((equal arg '(16))
                files)
               ((eq major-mode 'dired-mode) ; REVIEW: any downside over `derived-mode-p'?
                (dired-get-marked-files t nil))
               (t
                (completing-read-multiple
                 "Select files in current dir: " files
                 #'prot-common-crm-exclude-selected-p t))))
         (backend (vc-backend set))
         (vc-log-short-style (if (> (length set) 1) '(file) '(directory))))
    (vc-print-log-internal backend set nil nil lim nil)))

(autoload 'log-view-msg-prev "log-view")
(autoload 'log-view-msg-next "log-view")
(autoload 'log-view-toggle-entry-display "log-view")

(defvar vc-git-root-log-format)

;;;###autoload
(defun prot-vc-log-view-toggle-entry-all ()
  "Run `log-view-toggle-entry-display' on all commits."
  (interactive)
  (let ((oldlines (count-lines (point-min) (point-max)))
        (point (point))
        (newlines)
        (commits (count-matches (nth 1 vc-git-root-log-format)
                                (point-min) (point-max)))
        (limit prot-vc-log-bulk-action-limit))
    (cond
     ((<= commits limit)
      (save-excursion
        (goto-char (point-max))
        (while (not (eq (line-number-at-pos) 1))
          (log-view-msg-prev)
          (log-view-toggle-entry-display))
        (goto-char point)
        (setq newlines (count-lines (point-min) (point-max))))
      (when (> newlines oldlines)
        (log-view-msg-next))
      (recenter))
     (t
      (user-error "%d commits here; won't expand more than %d" commits limit)))))

;;;###autoload
(defun prot-vc-log-kill-hash ()
  "Save to `kill-ring' contextual commit hash in `vc-print-log'."
  (interactive)
  (let ((commit (cadr (log-view-current-entry (point) t))))
    (kill-new (format "%s" commit))
    (message "Copied: %s" commit)))

(defvar prot-vc--commit-hist '()
  "Minibuffer history for commit logs.")

(defvar prot-vc--patch-output-hist '()
  "Minibuffer history for `prot-vc-patch-dwim' output.")

(defun prot-vc--log-commit-hash (fn)
  "Extract commit hash from FN.
FN is assumed to be something like `prot-vc--log-commit-prompt'."
  (string-match "· \\([a-z0-9]*\\) ·" fn)
  (match-string-no-properties 1 fn))

(defun prot-vc--log-commit-prompt (&optional prompt limit)
  "Select git log commit with completion.

Optional PROMPT pertains to the minibuffer's input field.  While
optional LIMIT will apply `prot-vc-log-limit' as a constraint,
instead of producing a complete log."
  (let ((text (or prompt "Select a commit: "))
        (vc (prot-vc--current-project))
        (num (cond
              ((integerp limit)
               (format "%d" limit))
              (limit
               (format "%d" (prot-vc--commit-num)))
              (t
               (format "%d" -1)))))
    (if vc
        (completing-read
         text
         (prot-common-completion-table
          'line
          (process-lines "git" "log" "--pretty=format:%d · %h · %cs %an: %s" "-n" num))
         nil t nil 'prot-vc--commit-hist)
      (error "'%s' is not under version control" default-directory))))

;;;###autoload
(defun prot-vc-git-patch-apply (patch project &optional args)
  "Apply PATCH to current project using 'git am'.

PROJECT is a path to the root of a git repo, automatically
defaulting to the current project.  If none is found or if this
command is called with a prefix argument (\\[universal-argument])
prompt for a project instead.

When called non-interactively, ARGS is a list of strings with
command line flags for 'git am'.  Otherwise it takes the value of
`prot-vc-git-patch-apply-args'."
  (interactive
   (list
    (read-file-name "Path to patch: ")
    (when (or current-prefix-arg
              (null (prot-vc--current-project)))
      (project-prompt-project-dir))))
  ;; FIXME 2021-06-30: Avoid calling `prot-vc--current-project' twice
  (let* ((default-directory (or project (prot-vc--current-project)))
         (buf-name prot-vc-shell-output)
         (buf (get-buffer-create buf-name))
         (resize-mini-windows nil)
         (arguments (or args prot-vc-git-patch-apply-args))
         (arg-string (mapconcat #'identity arguments " ")))
    (shell-command (format "git am %s %s" arg-string patch) buf)))

;;;###autoload
(defun prot-vc-git-patch-create-dwim (&optional arg)
  "Do-What-I-mean to output Git patches to a directory.

When the region is active inside of a Log View buffer, produce
patches for the commits within that range.  Remember how Git
interprets those ranges where the base commit is the one before
the earliest in the range: if you need to produce patches for the
topmost 4 commits, you must include the last 5 in the region.

With no active region, and while in a Log View buffer, a patch is
produced for the commit at point.

While not in a Log View buffer, prompt for a single commit to
produce a patch for.

Optional prefix ARG (\\[universal-argument]) prompts for a commit
using completion.  The selected item is used as the base of a
range against HEAD (in the format of 'base..HEAD').  When in Log
View buffers, and while no region is active, ARG will skip the
check for the commit at point in order to produce the prompt for
a base commit.  If the region is active in Log View buffers, ARG
is ignored.

Whatever the case, the list of completion candidates for commits
is always confined to `prot-vc-log-limit'."
  (interactive "P")
  (let* ((vc-dir (or (prot-vc--current-project)
                     default-directory))
         (dirs (append (list vc-dir) prot-vc-patch-output-dirs))
         (out-dir
          (completing-read
           "Output directory: "
           (prot-common-completion-table 'file dirs)
           nil t nil 'prot-vc--patch-output-hist))
         (buf (get-buffer-create prot-vc-shell-output)))
    (cond
     ((and (use-region-p) (derived-mode-p 'log-view-mode))
      (let* ((beg (region-beginning))
             (end (region-end))
             (one (cadr (log-view-current-entry beg t)))
             (two (cadr (log-view-current-entry end t)))
             (line-count (count-lines beg end))
             (range (if (> line-count 1)
                        (cond
                         ((> beg end)
                          (format "%s..%s" one two))
                         ((< beg end)
                          (format "%s..%s" two one)))
                      (format "-1 %s" (cadr (log-view-current-entry (point) t))))))
        (shell-command
         (format "git format-patch %s -o %s --" range out-dir) buf)
        (message "Prepared patch for `%s' and sent it to %s"
                 (propertize range 'face 'bold)
                 (propertize out-dir 'face 'success))))
     (arg
      (let ((base (prot-vc--log-commit-hash
                   (prot-vc--log-commit-prompt
                    "Select base commit for base..HEAD: " t))))
        (shell-command
         (format "git format-patch %s..HEAD -o %s --" base out-dir) buf)
        (message "Prepared patch for `%s..HEAD' and sent it to %s"
                 (propertize base 'face 'bold)
                 (propertize out-dir 'face 'success))))
     (t
      (let* ((commit-at-point (when (derived-mode-p 'log-view-mode)
                                (cadr (log-view-current-entry (point) t))))
             (commit (if (not commit-at-point)
                         (prot-vc--log-commit-hash
                          (prot-vc--log-commit-prompt
                           "Prepare patch for commit: " t))
                       commit-at-point)))
        (shell-command
         (format "git format-patch -1 %s -o %s --" commit out-dir) buf)
        (message "Prepared patch for `%s' and sent it to %s"
                 (propertize commit 'face 'bold)
                 (propertize out-dir 'face 'success))
        (add-to-history 'prot-vc--commit-hist commit)))
     (add-to-history 'prot-vc--patch-output-hist out-dir))))

;;;###autoload
(defun prot-vc-git-show (&optional limit)
  "Run git show for commit selected via completion.
With optional LIMIT as a prefix arg (\\[universal-argument]),
prompt for a number to confine the log to.  If LIMIT is a number,
accept it directly.  In the absence of LIMIT, `prot-vc-log-limit'
will be used instead."
  (interactive "P")
  (let* ((num (cond
               ((and limit (listp limit))
                (read-number "Limit to N commits: " 100))
               (limit
                (prefix-numeric-value limit))
               (t
                t)))
         (commit (prot-vc--log-commit-hash
                  (prot-vc--log-commit-prompt "Commit to git-show: " num)))
         (buf-name prot-vc-shell-output)
         (buf (get-buffer-create buf-name)))
    (shell-command (format "git show %s -u --stat -1 --" commit) buf)
    (with-current-buffer buf-name
      (setq-local revert-buffer-function nil)
      (diff-mode))
    (add-to-history 'prot-vc--commit-hist commit)))

(autoload 'vc-git-grep "vc-git")

;;;###autoload
(defun prot-vc-git-grep (regexp)
  "Run 'git grep' for REGEXP in current project.
This is a simple wrapper around `vc-git-grep' to streamline the
basic task of searching for a regexp in the current project.  Use
the original command for its other features."
  (interactive
   (list (read-regexp "git-grep for PATTERN: "
                      nil 'grep-history)))
  (vc-git-grep regexp "*" (prot-vc--current-project)))

(autoload 'vc-git-region-history-mode "vc-git")

;;;###autoload
(defun prot-vc-git-log-grep (pattern &optional diff)
  "Run ’git log --grep’ for PATTERN.
With optional DIFF as a prefix (\\[universal-argument])
argument, also show the corresponding diffs."
  (interactive
   (list (read-regexp "Run 'git log --grep' for PATTERN")
         current-prefix-arg))
  (let* ((buf-name prot-vc-shell-output)
         (buf (get-buffer-create buf-name))
         (diffs (if diff "-p" ""))
         (type (if diff 'with-diff 'log-search))
         (resize-mini-windows nil))
    (shell-command (format "git log %s --grep=%s -E --" diffs pattern) buf)
    (with-current-buffer buf
      (setq-local vc-log-view-type type)
      (setq-local revert-buffer-function nil)
      (vc-git-region-history-mode)
      (setq-local log-view-vc-backend 'git))))

(defun prot-vc-git--file-rev (file &optional limit)
  "Select revision for FILE using completion.
Optionally apply LIMIT to the log."
  (let ((num (cond
              ((integerp limit)
               (format "%d" limit))
              (limit
               (format "%d" (prot-vc--commit-num)))
              (t
               (format "%d" -1)))))
    (completing-read
     (format "Find revision for %s: " file)
     (prot-common-completion-table
      'line
      (process-lines "git" "log" "--pretty=format:%d · %h · %cs %an: %s" "-n" num "--" file))
     nil t nil 'prot-vc--commit-hist)))

;;;###autoload
(defun prot-vc-git-find-revision (&optional limit)
  "Visit a version of the current file using completion.
With optional LIMIT as a prefix arg (\\[universal-argument]),
prompt for a number to confine the log to.  If LIMIT is a number,
accept it directly.  In the absence of LIMIT, `prot-vc-log-limit'
will be used instead."
  (interactive "P")
  (let* ((num (cond
               ((and limit (listp limit))
                (read-number "Limit to N commits: " 100))
               (limit
                (prefix-numeric-value limit))
               (t
                t)))
         (rev (prot-vc--log-commit-hash
               (prot-vc-git--file-rev buffer-file-name num))))
    (switch-to-buffer-other-window
     (vc-find-revision buffer-file-name rev))
    (add-to-history 'prot-vc--commit-hist rev)))

(autoload 'vc-annotate-mode "vc-annotate")
(autoload 'vc-annotate-display-select "vc-annotate")

;; XXX NOTE XXX 2021-07-31: Those are meant to enable `revert-buffer'
;; inside of `prot-vc-git-blame-region-or-file'.  I do not know whether
;; this is a good approach.  It seems very convoluted and fragile.  But
;; anyway, I tried and it seems to work.
(defvar prot-vc--blame-beg nil)
(defvar prot-vc--blame-end nil)
(defvar prot-vc--blame-file nil)
(defvar prot-vc--blame-origin nil)

;;;###autoload
(defun prot-vc-git-blame-region-or-file (beg end &optional file)
  "Git blame lines in region between BEG and END.
Optionally specify FILE, else default to the current one."
  (interactive "r")
  (let* ((buf-name prot-vc-shell-output)
         (buf (get-buffer-create buf-name))
         (f (or file buffer-file-name))
         (backend (vc-backend f))
         (rev (vc-working-revision f))
         (e (if (region-active-p) beg (point-min)))
         (b (if (region-active-p) end (- (point-max) 1)))
         (beg-line (line-number-at-pos b t))
         (end-line (line-number-at-pos e t))
         (default-directory (prot-vc--current-project))
         (origin (current-buffer))
         (resize-mini-windows nil))
    (shell-command
     (format "git blame -L %d,%d -- %s" beg-line end-line f) buf)
    ;; FIXME 2021-07-31: Learn how to implement a cleaner
    ;; `revert-buffer'.  See NOTE above.
    (setq-local prot-vc--blame-beg beg
                prot-vc--blame-end end
                prot-vc--blame-file f
                prot-vc--blame-origin origin)
    (with-current-buffer buf-name
      (unless (equal major-mode 'vc-annotate-mode)
        (vc-annotate-mode))
      ;; FIXME 2021-07-31: Same issue with `revert-buffer'.
      (setq-local revert-buffer-function
                  (lambda (_ignore-auto _noconfirm)
                    (let ((inhibit-read-only t))
                      (with-current-buffer origin
                        (prot-vc-git-blame-region-or-file prot-vc--blame-beg
                                                          prot-vc--blame-end
                                                          prot-vc--blame-file)))))
      (setq-local vc-annotate-backend backend)
      (setq-local vc-annotate-parent-file f)
      (setq-local vc-annotate-parent-rev rev)
      (setq-local vc-annotate-parent-display-mode 'scale)
      (vc-annotate-display-select buf 'fullscale))))

(autoload 'vc-refresh-state "vc-hooks")

;;;###autoload
(defun prot-vc-git-reset (&optional limit)
  "Select commit to 'git reset --soft' back to.
With optional LIMIT as a prefix arg (\\[universal-argument]),
prompt for a number to confine the log to.  If LIMIT is a number,
accept it directly.  In the absence of LIMIT, `prot-vc-log-limit'
will be used instead."
  (interactive "P")
  (let* ((num (cond
               ((and limit (listp limit))
                (read-number "Limit to N commits: " 50))
               (limit
                (prefix-numeric-value limit))
               (t
                t)))
         (commit (prot-vc--log-commit-hash
                  (prot-vc--log-commit-prompt "Run 'git reset --soft' on: " num)))
         (buf-name prot-vc-shell-output)
         (buf (get-buffer-create buf-name)))
    (when (yes-or-no-p (format "Run 'git reset --soft %s'?" commit))
      (shell-command (format "git reset --soft %s --quiet --" commit) buf)
      (vc-refresh-state))))

;;;###autoload
(defun prot-vc-git-log-reset (&optional hard)
  "Select commit in VC Git Log to 'git reset --soft' back to.
With optional prefix argument (\\[universal-argument]) for HARD,
pass the '--hard' flag instead."
  (interactive "P")
  (let* ((commit (cadr (log-view-current-entry (point) t)))
         (buf-name prot-vc-shell-output)
         (buf (get-buffer-create buf-name))
         (flag (if hard "--hard" "--soft")))
    (when (yes-or-no-p (format "Run 'git reset %s %s'?" flag commit))
      (shell-command (format "git reset %s %s --quiet --" flag commit) buf)
      (revert-buffer))))

;;;###autoload
(defun prot-vc-git-checkout-remote (remote)
  "Checkout new local branch tracking REMOTE (git checkout -b)."
  (interactive
   (list (completing-read
          "Select remote tracking branch: "
          (mapcar #'string-trim (process-lines "git" "branch" "-r"))
          nil t)))
  (let* ((name (split-string remote "\\(->\\|[\/]\\)" t "[\s\t]+"))
         (local (if (> (length name) 1)
                    (car (reverse name)) ; Better way than car reverse?
                  (car name))))
    (shell-command (format "git checkout -b %s %s" local remote))))

;;;; User Interface setup

;; This is a tweaked variant of `vc-git-expanded-log-entry'
(defun prot-vc-git-expanded-log-entry (revision)
  "Expand git commit message for REVISION."
  (with-temp-buffer
    (apply 'vc-git-command t nil nil (list "log" revision "--stat" "-1" "--"))
    (goto-char (point-min))
    (unless (eobp)
      (while (re-search-forward "^" nil t)
        (replace-match "  ")
        (forward-line))
      (concat "\n" (buffer-string)))))

(defun prot-vc-git-expand-function ()
  "Set `log-view-expanded-log-entry-function' for `vc-git'."
  (when (eq vc-log-view-type 'short)
    (setq-local log-view-expanded-log-entry-function
                #'prot-vc-git-expanded-log-entry)))

(defvar prot-vc-git-log-view-mode-hook nil
  "Hook that runs after `vc-git-log-view-mode'.")

(defun prot-vc-git-log-view-add-hook (&rest _)
  "Run `prot-vc-git-log-view-mode-hook'."
  (run-hooks 'prot-vc-git-log-view-mode-hook))

(declare-function log-edit-add-field "log-edit")

(defun prot-vc--format-git-comment (branch remote files &optional commits)
  "Add Git Log Edit comment with BRANCH, REMOTE, FILES, COMMITS."
  (let ((log (if commits (concat "\n# Recent commits:\n#\n" commits "\n#") "")))
    (concat
     "\n\n# ---\n# "
     "Files to be committed to branch " "`" branch "' tracking `" remote "':"
     "\n#\n" files "\n#" log
     "\n# All lines starting with `#' are ignored.")))

(defun prot-vc-git-log-edit-comment (&optional no-headers)
  "Append comment block to Git Log Edit buffer.
With optional NO-HEADERS skip the step of inserting the special
headers 'Amend' and 'Summary'."
  (let* ((branch-name (process-lines "git" "branch" "--show-current"))
         (branch (or (car branch-name) "Detached HEAD"))
         (remotes (process-lines "git" "branch" "-r"))
         (remote-name (if remotes
                          (cl-remove-if-not (lambda (s)
                                              (string-match-p "->" s))
                                            remotes)
                        "None"))
         (remote (if (and remote-name (listp remote-name))
                     (cadr (split-string (car remote-name) "->" t "[\s\t]+"))
                   "No Remote Found"))
         (files (mapconcat (lambda (x)
                             (concat "#   " x))
                           (log-edit-files)
                           "\n"))
         (commits (when (and prot-vc-git-log-edit-show-commits
                             (ignore-errors (process-lines "git" "log" "-1")))
                    (mapconcat (lambda (x)
                                 (concat "#   " x))
                               (process-lines
                                "git" "log" "--pretty=format:%h  %cs  %s"
                                (format "-n %d" prot-vc-git-log-edit-show-commit-count))
                               "\n"))))
    (unless no-headers
      (save-excursion
        (rfc822-goto-eoh)
        (unless (re-search-backward "Amend: .*" nil t)
          (log-edit-add-field "Amend" ""))
        (rfc822-goto-eoh)
        (unless (re-search-backward "Summary: .*" nil t)
          (log-edit-add-field "Summary" ""))))
    (goto-char (point-max))
    (insert "\n")
    (insert (prot-vc--format-git-comment branch remote files commits))
    (rfc822-goto-eoh)
    (when (looking-at "\n") (forward-char -1))))

;;;###autoload
(defun prot-vc-git-log-edit-previous-comment (arg)
  "Cycle backwards through comment history.
With a numeric prefix ARG, go back ARG comments."
  (interactive "*p")
  (let ((len (ring-length log-edit-comment-ring)))
    (if (<= len 0)
        (progn (message "Empty comment ring") (ding))
      ;; Don't use `erase-buffer' because we don't want to `widen'.
      (delete-region (point-min) (point-max))
      (setq log-edit-comment-ring-index (log-edit-new-comment-index arg len))
      (message "Comment %d" (1+ log-edit-comment-ring-index))
      (insert (ring-ref log-edit-comment-ring log-edit-comment-ring-index))
      (prot-vc-git-log-edit-comment t)
      (save-excursion
        (goto-char (point-min))
        (search-forward "# ---")
        (forward-line -1)
        (delete-blank-lines)
        (newline 2)))))

;;;###autoload
(defun prot-vc-git-log-edit-next-comment (arg)
  "Cycle forwards through comment history.
With a numeric prefix ARG, go forward ARG comments."
  (interactive "*p")
  (prot-vc-git-log-edit-previous-comment (- arg)))

(defvar prot-vc--log-edit-comment-hist '()
  "History of inputs for `prot-vc-git-log-edit-complete-comment'.")

(defun prot-vc--log-edit-complete-prompt (comments)
  "Select entry from COMMENTS."
  (completing-read
   "Select comment: "
   comments nil t nil 'prot-vc--log-edit-comment-hist))

;;;###autoload
(defun prot-vc-git-log-edit-complete-comment ()
  "Insert text from Log Edit history ring using completion."
  (interactive)
  (let* ((newline (propertize "^J" 'face 'escape-glyph))
         (ring (ring-elements log-edit-comment-ring))
         (completions
          (mapcar (lambda (s)
                    (string-replace "\n" newline s))
                  ring))
         (selection (prot-vc--log-edit-complete-prompt completions))
         (comment (string-replace newline "\n" selection)))
    (add-to-history 'prot-vc--log-edit-comment-hist comment)
    (delete-region (point-min) (point-max))
    (insert comment)
    (prot-vc-git-log-edit-comment t)
    (save-excursion
      (goto-char (point-min))
      (search-forward "# ---")
      (forward-line -1)
      (delete-blank-lines)
      (newline 2))))

(defun prot-vc-git-log-remove-comment ()
  "Remove Git Log Edit comment, empty lines; keep final newline."
  (let ((buffer (get-buffer "*vc-log*"))) ; REVIEW: This is fragile
    (with-current-buffer (when (buffer-live-p buffer) buffer)
      (save-excursion
        (goto-char (point-min)))
      (when (derived-mode-p 'log-edit-mode)
        (flush-lines "^#")))))

;;;###autoload
(defun prot-vc-git-log-edit-toggle-amend ()
  "Toggle 'Amend' header for current Log Edit buffer.

Setting the header to 'yes' means that the current commit will
edit the previous one.

Unlike `vc-git-log-edit-toggle-amend', only change the state of
the 'Amend' header, without attempting to alter the contents of
the buffer."
  (interactive)
  (when (log-edit-toggle-header "Amend" "yes")))

(defun prot-vc--buffer-string-omit-comment ()
  "Remove Git comment and empty lines from buffer string."
  (let* ((buffer (get-buffer "*vc-log*"))
         (string (when buffer
                   (with-current-buffer buffer
                     (buffer-substring-no-properties (point-min) (point-max))))))
    (when string
      (replace-regexp-in-string "^#.*" "" string))))

(defvar log-edit-comment-ring)
(autoload 'ring-empty-p "ring")
(autoload 'ring-ref "ring")
(autoload 'ring-insert "ring")

(defun prot-vc-git-log-edit-remember-comment (&optional comment)
  "Store Log Edit text or optional COMMENT.
Remove special Git comment block before storing the genuine
commit message."
  (let ((commit (or comment (gensym))))
    (setq commit (prot-vc--buffer-string-omit-comment))
    (when (or (ring-empty-p log-edit-comment-ring)
              (not (equal commit (ring-ref log-edit-comment-ring 0))))
      (ring-insert log-edit-comment-ring commit))))

(declare-function log-edit-show-diff "log-edit")

(defvar prot-vc--current-window-configuration nil
  "Current window configuration for use with Log Edit.")

(defvar prot-vc--current-window-configuration-point nil
  "Point in current window configuration for use with Log Edit.")

(defun prot-vc--store-window-configuration ()
  "Store window configuration before calling `vc-start-logentry'.
This should be called via `prot-vc-git-pre-log-edit-hook'."
  (setq prot-vc--current-window-configuration (current-window-configuration))
  (setq prot-vc--current-window-configuration-point (point)))

(defvar prot-vc-git-pre-log-edit-hook nil
  "Hook that runs right before `vc-start-logentry'.")

(defun prot-vc-git-pre-log-edit (&rest _)
  "Run `prot-vc-git-pre-log-edit-hook'.
To be used as advice before `vc-start-logentry'."
  (run-hooks 'prot-vc-git-pre-log-edit-hook))

(defun prot-vc--log-edit-restore-window-configuration ()
  "Set window configuration to the pre Log Edit state."
  (when prot-vc--current-window-configuration
    (set-window-configuration prot-vc--current-window-configuration))
  (when prot-vc--current-window-configuration-point
    (goto-char prot-vc--current-window-configuration-point)))

(defun prot-vc--log-edit-diff-window-configuration ()
  "Show current diff for Git Log Edit buffer."
  (let ((buffer (get-buffer "*vc-log*")))
    (with-current-buffer (if (buffer-live-p buffer)
                             buffer
                           (window-buffer (get-mru-window)))
      (delete-other-windows)
      (when (ignore-errors ; This condition saves us from error on new repos
              (process-lines "git" "--no-pager" "diff-index" "-p" "HEAD" "--"))
        (log-edit-show-diff))
      (other-window -1))))

(defun prot-vc--kill-log-edit ()
  "Local hook to restore windows when Log Edit buffer is killed."
  (when (or (derived-mode-p 'log-edit-mode)
            (derived-mode-p 'diff-mode))
    (add-hook 'kill-buffer-hook #'prot-vc--log-edit-restore-window-configuration 0 t)))

(defvar prot-vc-git-log-edit-done-hook nil
  "Hook that runs after `prot-vc-git-log-edit-done'.")

;; FIXME: Why does `prot-vc-git-log-remove-comment' not work when added
;; to `log-edit-done-hook'?
;;;###autoload
(defun prot-vc-git-log-edit-done ()
  "Remove Git Log Edit comments and commit change set.
This is a thin wrapper around `log-edit-done', which first calls
`prot-vc-git-log-remove-comment'."
  (interactive)
  (prot-vc-git-log-remove-comment)
  (log-edit-done)
  (run-hooks 'prot-vc-git-log-edit-done-hook))

(defface prot-vc-git-log-edit-file-name
  '((default :inherit font-lock-comment-face)
    (((class color) (min-colors 88) (background light))
     :foreground "#2a486a")
    (((class color) (min-colors 88) (background dark))
     :foreground "#b0d6f5")
    (t :foreground "cyan"))
  "Face for file names in VC Git Log Edit buffers.")

(defface prot-vc-git-log-edit-local-branch-name
  '((default :inherit font-lock-comment-face)
    (((class color) (min-colors 88) (background light))
     :foreground "#0031a9")
    (((class color) (min-colors 88) (background dark))
     :foreground "#2fafff")
    (t :foreground "blue"))
  "Face for local branch name in VC Git Log Edit buffers.")

(defface prot-vc-git-log-edit-remote-branch-name
  '((default :inherit font-lock-comment-face)
    (((class color) (min-colors 88) (background light))
     :foreground "#55348e")
    (((class color) (min-colors 88) (background dark))
     :foreground "#cfa6ff")
    (t :foreground "magenta"))
  "Face for remote branch name in VC Git Log Edit buffers.")

(defconst prot-vc-git-log-edit-font-lock
  '(("^#.*"
     (0 'font-lock-comment-face))
    ("^#.*`\\(.+?\\)'.*`\\(.+?\\)'"
     (1 'prot-vc-git-log-edit-local-branch-name t)
     (2 'prot-vc-git-log-edit-remote-branch-name t))
    ("^#[\s\t][\s\t]+\\(.+\\)"
     (1 'prot-vc-git-log-edit-file-name t)))
  "Fontification rules for Log Edit buffers.")

(defun prot-vc-git-log-edit-extra-keywords ()
  "Apply `prot-vc-git-log-edit-font-lock' to Log Edit buffers."
  (font-lock-flush (point-min) (point-max))
  (font-lock-add-keywords nil prot-vc-git-log-edit-font-lock nil))

(autoload 'vc-git-log-view-mode "vc-git")
(autoload 'vc-git-checkin "vc-git")
(declare-function log-edit-show-files "log-edit")
(declare-function log-edit-kill-buffer "log-edit")
(declare-function log-edit-done "log-edit")
(declare-function log-edit-remember-comment "log-edit")
(declare-function vc-git-log-edit-toggle-amend "log-edit")
(defvar vc-git-log-edit-mode-map)

;;;###autoload
(define-minor-mode prot-vc-git-setup-mode
  "Extend `vc-git' Log View and Log Edit buffers.

Tweak the format of expanded commit messages in Log View buffers.  The
full information now includes a diff stat as well as all affected file
paths.  Those files can then be used for file-at-point operations like
`project-find-file'.

Make Log Edit window configurations split between the message
composition buffer and the corresponding diff view: the previous window
configuration is restored upon the successful conclusion of the commit
or its termination by means of `log-edit-kill-buffer'.

Append a comment block to the Log Edit buffer with information about the
files being committed and the branch they are a part of.  When
`prot-vc-git-log-edit-show-commits' is non-nil, also include a commit
log.  The number of commits in that log is controlled by
`prot-vc-git-log-edit-show-commit-count'.

For Log Edit buffers, bind C-c C-c to `prot-vc-git-log-edit-done' which
is designed to remove the comment block before checking in the changes.
Rebind other keys in the same vein.  `prot-vc-git-log-edit-done' calls
the normal hook `prot-vc-git-log-edit-done-hook' which is used to
restore the window layout.

Set up font-lock directives to make the aforementioned block look like a
comment in Log Edit buffers.  Also highlight file and branch names
inside the comment block."
  :init-value nil
  :global t
  (if prot-vc-git-setup-mode
      (progn
        ;; Log view expanded commits
        (advice-add #'vc-git-log-view-mode :after #'prot-vc-git-log-view-add-hook)
        (add-hook 'prot-vc-git-log-view-mode-hook #'prot-vc-git-expand-function)
        ;; Append comment block in Log edit showing branch and files.
        ;; This means that we no longer need the files' window to pop up
        ;; automatically
        (add-hook 'log-edit-hook #'prot-vc-git-log-edit-comment)
        (remove-hook 'log-edit-hook #'log-edit-show-files)
        ;; Window configuration with just the commit and the diff
        ;; (restores previous state after finalising or aborting the
        ;; commit).
        (advice-add #'vc-start-logentry :before #'prot-vc-git-pre-log-edit)
        (add-hook 'prot-vc-git-pre-log-edit-hook #'prot-vc--store-window-configuration)
        (advice-add #'log-edit-remember-comment :around #'prot-vc-git-log-edit-remember-comment)
        (let ((map vc-git-log-edit-mode-map))
          (define-key map (kbd "C-c C-c") #'prot-vc-git-log-edit-done)
          (define-key map (kbd "C-c C-e") #'prot-vc-git-log-edit-toggle-amend)
          (define-key map (kbd "M-p") #'prot-vc-git-log-edit-previous-comment)
          (define-key map (kbd "M-n") #'prot-vc-git-log-edit-next-comment)
          (define-key map (kbd "M-s") #'prot-vc-git-log-edit-complete-comment)
          (define-key map (kbd "M-r") #'prot-vc-git-log-edit-complete-comment))
        (add-hook 'log-edit-mode-hook #'prot-vc--kill-log-edit)
        (add-hook 'prot-vc-git-log-edit-done-hook #'prot-vc--log-edit-restore-window-configuration)
        (add-hook 'log-edit-hook #'prot-vc--log-edit-diff-window-configuration)
        ;; Extra font lock rules for Log Edit comment block
        (add-hook 'log-edit-hook #'prot-vc-git-log-edit-extra-keywords))
    (advice-remove #'vc-git-log-view-mode #'prot-vc-git-log-view-add-hook)
    (remove-hook 'prot-vc-git-log-view-mode-hook #'prot-vc-git-expand-function)
    (remove-hook 'log-edit-hook #'prot-vc-git-log-edit-comment)
    (add-hook 'log-edit-hook #'log-edit-show-files)
    (advice-remove #'vc-start-logentry #'prot-vc-git-pre-log-edit)
    (remove-hook 'prot-vc-git-pre-log-edit-hook #'prot-vc--store-window-configuration)
    (advice-remove #'log-edit-remember-comment #'prot-vc-git-log-edit-remember-comment)
    (let ((map vc-git-log-edit-mode-map))
      (define-key vc-git-log-edit-mode-map (kbd "C-c C-c") #'log-edit-done)
      (define-key vc-git-log-edit-mode-map (kbd "C-c C-e") #'vc-git-log-edit-toggle-amend)
      (define-key map (kbd "M-p") #'log-edit-previous-comment)
      (define-key map (kbd "M-n") #'log-edit-next-comment)
      (define-key map (kbd "M-s") #'log-edit-comment-search-forward)
      (define-key map (kbd "M-r") #'log-edit-comment-search-backward))
    (remove-hook 'log-edit-mode-hook #'prot-vc--kill-log-edit)
    (remove-hook 'prot-vc-git-log-edit-done-hook #'prot-vc--log-edit-restore-window-configuration)
    (remove-hook 'log-edit-hook #'prot-vc--log-edit-diff-window-configuration)
    (remove-hook 'log-edit-hook #'prot-vc-git-log-edit-extra-keywords)))

(provide 'prot-vc)
;;; prot-vc.el ends here
5.5.2.1. git-email.el for preparing patches

This neat library by Xinglu Chen streamlines the process of formatting and sending Git patches via email, all from the comfort of Emacs. Its main point of entry is the command git-email-format-patch, which prompts you for a commit that is read as the range between the current HEAD and the one you specify. In doing so, it allows you to prepare a series of patches, using the correct message headers.

git-email.el is meant to work with the standard message composition buffer, such as the one you get when you call M-x compose-email (by default that command is bound to C-x m and I keep it that way). Email clients like Gnus and Notmuch are also supported. For my case as a user of the latter, I just activate git-email-notmuch-mode in order to add the relevant settings to the message composition buffers. For more on Notmuch and all related configurations, refer to the mega-section on Email settings.

So here is the typical workflow with this package:

  • Visit a file and make some changes.
  • Commit those changes.
  • Invoke git-email-format-patch and select the base commit against which your commits are to be read. So if your commit is the current HEAD, then just pick the one right before.
  • Pass any optional flags. Multiple flags can be completed against using a comma as a separator (or whatever your crm-separator is).
  • Fill in the email details, which probably is just an email address (and a cover letter, if you use that option).
  • Send. Done!

The maintainer of the project will then be able to apply your patch, using standard git commands (read the manpages of git-apply for attachments and git-am for mailbox-type patches).

Alternatively, you may already have a patch available and wish to email it directly. Visit its directory and with the point over it call the command git-email-send-email. This also works for the marked items of the Dired buffer (Dired (directory editor, file manager)).

Overall, git-email.el is a welcome addition to the ecosystem. Apart from also working with the built-in Version Control framework of Emacs (consult Version control framework (vc.el and prot-vc.el)), it offers us the means to conveniently implement a truly decentralised workflow for collaboration: git and email empower you to utilise the tools you want, instead of forcing you through some unwieldy pull/merge request process that certain git forges encourage. I prefer this approach and am eager to see it getting more widespread adoption.

;;; git-email.el for preparing patches
;; Project repo: <https://git.sr.ht/~yoctocell/git-email>.  This is one
;; of the packages I handle manually via git, at least until it becomes
;; available through an ELPA.
;;
;; `prot-emacs-manual-package' is defined in my init.el
(prot-emacs-manual-package 'git-email
  (with-eval-after-load 'notmuch
    (require 'git-email-notmuch)
    (git-email-notmuch-mode 1))
  (define-key global-map (kbd "C-x v RET") #'git-email-format-patch) ; VC prefix and C-m
  (define-key dired-mode-map (kbd "C-x v RET") #'git-email-send-email))

5.5.3. Interactive and powerful 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.

Magit has great defaults and it should work admirably without any further tweaks or extra setup. That granted, the git-commit package (part of Magit) is configured in accordance with the guidelines provided by this article on writing a Git commit message. The gist is to compose commits that are clean and easy to read. The fill-column is set elsewhere in this document to 72 characters long.

;;; Interactive and powerful git front-end (Magit)
(prot-emacs-elpa-package 'magit
  (setq magit-define-global-key-bindings nil)
  (define-key global-map (kbd "C-c g") #'magit-status)

  (require 'git-commit)
  (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))

  (require 'magit-diff)
  (setq magit-diff-refine-hunk t)

  (require 'magit-repos)
  (setq magit-repository-directories
        '(("~/Git/Projects" . 1))))

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

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 nothing in terms of Smerge-related configurations. The package is small and does one thing well.

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

;;; Smerge and Ediff
(prot-emacs-builtin-package 'smerge-mode)

(prot-emacs-builtin-package 'ediff
  (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)))

5.6. Command-line shells

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 only the former category as I never use the likes of ansi-term or vterm.

5.6.1. Eshell and prot-eshell.el

Eshell is a strictly line-oriented command prompt written in Emacs Lisp. This comes with its pros and cons: it can understand Elisp but does not behave exactly like Unix shells, say, Bash. Eshell cannot display the kind of pseudo graphics a terminal emulator can, such as those you find in mutt, htop, ncmpcpp, newsboat and so on. As each user's needs are different, you will have to determine whether Eshell can fit into your workflow. Start by reading its fairly short, yet insightful, manual.

For me this tool is one of the most promising in the Emacs milieu because while it is a competent shell it can seamlessly integrate with the rest of Emacs' capabilities. This is best exemplified by its extensibility, such as what I am doing with prot-eshell.el. More on that below.

The fact that Eshell cannot reproduce the artefacts of the ncurses library does not pose a hindrance to my workflow, as I have replacements for all such "graphical" programs within Emacs. Notmuch has an Emacs client which handles my email (yes, the CLI works as well), M-x proced lets me interact with system processes, Bongo deals with media playback, while Elfeed fills the niche of following RSS/Atom feed.

Read relevant sections:

Now an overview of prot-eshell.el, with the full code reproduced right after the package configurations:

  • There are several prot-eshell-ffap-* commands that operate on the file at point. Say you have called ls and wish to expand the contents of a file at the command prompt. With point over the file name of interest, use prot-eshell-ffap-insert. Wish to visit the file instead, so that you may edit it? Try prot-eshell-ffap-find-file. The command prot-eshell-ffap-kill-save copies the file's full file system path, while prot-eshell-ffap-dired-jump opens a Dired buffer in that file's directory (see Dired (directory editor, file manager)).
  • prot-eshell-export takes the prompt and output of the last command and places it in a bespoke buffer. The name of the buffer is controlled by the variable prot-eshell-output-buffer. If that buffer does not exist, it is created. Otherwise subsequent invocations of this "export" command will append their contents to the existing ones. This is good for keeping a record of something you are working on. And because this is a standard buffer, you can edit it at will as well as call write-file (C-x C-w) to save it permanently to a file.
  • prot-eshell-redirect-to-buffer provides a completion prompt to help you redirect the output of a command to a given buffer. Simple and effective.
  • prot-eshell-narrow-output-highlight-regexp prompts for a regexp to highlight in the output of the last command. It then narrows the Eshell buffer to the contents of that output and emphasises the matches of the regexp. Very useful when you need to inspect some logs or other terse output. Remember that to widen the view you use the standard widen command, bound to C-x n w by default.
  • prot-eshell-complete-history lets you pick a command from your history using minibuffer completion. Forget about a non-interactive regexp search or, worse, consecutive calls to M-p and M-n to cycle through your recent inputs one at a time.
  • prot-eshell-complete-recent-dir provides a minibuffer prompt with completion that queries through all paths in your cd input history. This is much more convenient that standard actions like cd - or cd -N where N is the position of the item in the history of entries (retrieved with cd =).
  • prot-eshell-find-subdirectory-recursive uses completion to help you pick a subdirectory that extends the present working directory. It does so recursively, which makes it powerful, but can cause problems when called from the root of some massive directory tree. Exercise restraint.
  • prot-eshell-root-dir switches the present working directory to that of the current project's root directory, if one is found.

Here is a video on Eshell and my extras (2020-05-08) which, however, showcases older code than what I have here.

Also check these valuable resources:

;;; Eshell and prot-eshell.el
(prot-emacs-builtin-package 'eshell
  (require 'esh-mode)
  (require 'esh-module)
  (setq eshell-modules-list             ; It works but may need 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))
  (setenv "PAGER" "cat") ; solves issues, such as with 'git log' and the default 'less'
  (require 'em-cmpl)
  (require 'em-dirs)
  (setq eshell-cd-on-directory t)

  (require 'em-tramp)
  (setq password-cache t)
  (setq password-cache-expiry 600)

  (require 'em-hist)
  (setq eshell-hist-ignoredups t)
  (setq eshell-save-history-on-exit t))

(prot-emacs-builtin-package 'prot-eshell
  (setq prot-eshell-output-buffer "*Exported Eshell output*")
  (setq prot-eshell-output-delimiter "* * *")
  (let ((map eshell-mode-map))
    (define-key map (kbd "M-k") #'eshell-kill-input)
    (define-key map (kbd "C-c C-f") #'prot-eshell-ffap-find-file)
    (define-key map (kbd "C-c C-j") #'prot-eshell-ffap-dired-jump)
    (define-key map (kbd "C-c C-w") #'prot-eshell-ffap-kill-save)
    (define-key map (kbd "C-c C->") #'prot-eshell-redirect-to-buffer)
    (define-key map (kbd "C-c C-e") #'prot-eshell-export)
    (define-key map (kbd "C-c C-r") #'prot-eshell-root-dir))
  (let ((map eshell-cmpl-mode-map))
    (define-key map (kbd "C-c TAB") #'prot-eshell-ffap-insert) ; C-c C-i
    (define-key map (kbd "C-c M-h") #'prot-eshell-narrow-output-highlight-regexp))
  (let ((map eshell-hist-mode-map))
    (define-key map (kbd "M-s") #'nil) ; I use this prefix for lots of more useful commands
    (define-key map (kbd "M-r") #'prot-eshell-complete-history)
    (define-key map (kbd "C-c C-d") #'prot-eshell-complete-recent-dir)
    (define-key map (kbd "C-c C-s") #'prot-eshell-find-subdirectory-recursive)))

This is prot-eshell.el (part of my dotfiles' repo):

;;; prot-eshell.el --- Extensions to Eshell for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; This covers my Eshell extensions, for use in my Emacs setup:
;; https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'eshell)
(require 'esh-mode)
(require 'em-dirs)
(require 'em-hist)

;;;; Customisation options

(defgroup prot-eshell ()
  "Extensions for Eshell and related libraries."
  :group 'shell)

(defcustom prot-eshell-output-buffer "*Exported Eshell output*"
  "Name of buffer with the last output of Eshell command.
Used by `prot-eshell-export'."
  :type 'string
  :group 'prot-eshell)

(defcustom prot-eshell-output-delimiter "* * *"
  "Delimiter for successive `prot-eshell-export' outputs.
This is formatted internally to have newline characters before
and after it."
  :type 'string
  :group 'prot-eshell)

;;;; Commands

(autoload 'ffap-file-at-point "ffap.el")

(defmacro prot-eshell-ffap (name doc &rest body)
  "Make `find-file-at-point' commands for Eshell.
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-ffap-insert
 "Insert (cat) contents of file at point."
 (progn
   (goto-char (point-max))
   (insert (format "cat %s" file))
   (eshell-send-input)))

(prot-eshell-ffap
 prot-eshell-ffap-kill-save
 "Add to kill-ring the absolute path of file at point."
 (progn
   (kill-new (format "%s/%s" (eshell/pwd) file))
   (message "Copied full path of %s" file)))

(prot-eshell-ffap
 prot-eshell-ffap-find-file
 "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-ffap-dired-jump
 "Jump to the parent directory of the file at point."
 (dired (file-name-directory file)))

(defun prot-eshell--command-prompt-output ()
  "Capture last command prompt and its output."
  (let ((beg (save-excursion
               (goto-char (eshell-beginning-of-input))
               (goto-char (point-at-bol)))))
  (when (derived-mode-p 'eshell-mode)
    (buffer-substring-no-properties beg (eshell-end-of-output)))))

;;;###autoload
(defun prot-eshell-export ()
  "Produce a buffer with output of the last Eshell command.
If `prot-eshell-output-buffer' does not exist, create it.  Else
append to it, while separating multiple outputs with
`prot-eshell-output-delimiter'."
  (interactive)
  (let ((eshell-output (prot-eshell--command-prompt-output)))
    (with-current-buffer (get-buffer-create prot-eshell-output-buffer)
      (goto-char (point-max))
      (unless (eq (point-min) (point-max))
        (insert (format "\n%s\n\n" prot-eshell-output-delimiter)))
      (goto-char (point-at-bol))
      (insert eshell-output)
      (switch-to-buffer-other-window (current-buffer)))))

;;;###autoload
(defun prot-eshell-redirect-to-buffer (buffer)
  "Complete the syntax for appending Eshell output to BUFFER."
  (interactive
   (list (read-buffer "Redirect to buffer: ")))
  (insert
   (format " >>> #<%s>" buffer)))

;;;###autoload
(defun prot-eshell-narrow-output-highlight-regexp (regexp)
  "Narrow to last command output and highlight REGEXP."
  (interactive
   (list (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)
  (message "Narrowed to last output and highlighted < %s >" regexp))

;;;###autoload
(defun prot-eshell-complete-recent-dir (&optional arg)
  "Switch to a recent Eshell directory using completion.
With optional ARG prefix argument (\\[universal-argument]) also
open the directory in a `dired' buffer."
  (interactive "P")
  (let* ((dirs (ring-elements eshell-last-dir-ring))
         (dir (completing-read "Switch to recent dir: " dirs nil t)))
    (insert dir)
    (eshell-send-input)
    (when arg
      (dired dir))))

;;;###autoload
(defun prot-eshell-complete-history ()
  "Insert element from Eshell history using completion."
  (interactive)
  (let ((hist (ring-elements eshell-history-ring)))
    (insert
     (completing-read "Input from history: " hist nil t))))

(autoload 'cl-remove-if-not "cl-seq")

;;;###autoload
(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 call `eshell-interrupt-process'."
  (interactive)
  (let* ((dir (abbreviate-file-name (eshell/pwd)))
         (contents (directory-files-recursively dir ".*" t nil nil))
         (dirs (cl-remove-if-not (lambda (x)
                                   (or (file-directory-p x)
                                       (string-match-p "\\.git" x)))
                                 contents))
         (selection (completing-read
                     (format "Find sub-dir from %s: "
                             (propertize dir 'face 'success))
                     dirs nil t)))
    (insert selection)
    (eshell-send-input)))

;;;###autoload
(defun prot-eshell-root-dir ()
  "Switch to the root directory of the present project."
  (interactive)
  (let ((root (or (vc-root-dir)
                  (locate-dominating-file "." ".git"))))
    (if root
        (progn
          (insert root)
          (eshell-send-input))
      (user-error "Cannot find a project root here"))))

;;;; Bookmark handler for bookmark.el
;; The default pops up an existing Eshell buffer instead of creating a
;; new one which visits the bookmarked location.

(declare-function bookmark-get-handler "bookmark" (bookmark-name-or-record))
(declare-function bookmark-prop-get "bookmark" (bookmark prop))

;; Copied from the `eshell-conf.el' of JSDurand on 2021-09-17 17:47
;; +0300: <https://git.jsdurand.xyz/emacsd.git/tree/eshell-conf.el>.

(defun prot-eshell-bookmark-jump (bookmark)
  "Handle Eshell BOOKMARK in my preferred way."
  (let ((handler (bookmark-get-handler bookmark))
        (location (bookmark-prop-get bookmark 'location))
        (eshell-buffers
         (delq
          nil
          (mapcar
           (lambda (buffer)
             (cond
              ((provided-mode-derived-p
                (buffer-local-value
                 'major-mode buffer)
                'eshell-mode)
               buffer)))
           (buffer-list)))))
    (cond
     ((and (stringp location)
           (not (string= location ""))
           (memq handler (list #'eshell-bookmark-jump
                               #'prot-eshell-bookmark-jump)))
      (let (reuse-p)
        (mapc
         (lambda (buffer)
           (cond
            ((string= (buffer-local-value 'default-directory
                                          buffer)
                      location)
             (setq reuse-p buffer))))
         eshell-buffers)
        ;; Don't switch to that buffer, otherwise it will cause
        ;; problems if we want to open the bookmark in another window.
        (cond
         (reuse-p (set-buffer reuse-p))
         ;; eshell will pop the buffer
         ((let ((buffer (generate-new-buffer eshell-buffer-name)))
            (with-current-buffer buffer
              (setq-local default-directory location)
              (eshell-mode))
            (set-buffer buffer))))))
     ((user-error "Cannot jump to this bookmark")))))

(advice-add #'eshell-bookmark-jump :override #'prot-eshell-bookmark-jump)

(provide 'prot-eshell)
;;; prot-eshell.el ends here

5.6.2. Shell (M-x shell)

NOTE: I normally use Eshell. Refer to the Eshell and prot-eshell.el section.

This is a shell (Bash, in my case) that runs inside of Emacs. Unlike terminal emulators, 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.

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.

;;; Shell (M-x shell)
(prot-emacs-builtin-package 'shell
  (setq shell-command-prompt-show-cwd t) ; Emacs 27.1
  (setq ansi-color-for-comint-mode t))

5.7. Org-mode (personal information manager)

Also watch: Emacs as a 'second brain' and mindfulness (2021-08-31).

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 straightforward system of plain text notation.

Couched in those terms, Org is at once a distribution of well integrated libraries and a vibrant ecosystem that keeps producing new ideas and workflows on how to organise one's life with plain text.

The present document is written in org-mode while its website version is outputted by a tool (also part of Org) that exports Org notation to its HTML equivalent.

Regarding the following code block, I strongly encourage you to make liberal use of Emacs' documentation facilities to learn more about functions, variables, symbols provided herein. And do not forget to read Org's manual.

What follows in an exposition about each of the subsesctions of this package configurations:

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. Or to produce a properly formatted link to supported buffers you are visiting (e.g. another file).

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. Else just call org-insert-last-stored-link to skip the interactive process and insert the last link outright.

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.

Org capture

The org-capture tool is a powerful way to quickly produce some kind of structured information that gets stored in the appropriate place. 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.

The visibility of a template is explicitly controlled by the alist 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 buffers (also check my comprehensive configurations for email).

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 expression 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 %? specifier determines where the point shall end in once the template is parsed. While %i will insert the contents of the active region, if any.

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-existent files are created automatically once you call the relevant template. Same for their respective headings.

Finally, the prot-org--capture-no-delete-windows from my prot-org.el (reproduced after the Org configs) addresses a problem I have when org-capture fails to carry out its operations when it is 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.

Org agenda

The org-agenda is not just a single interface. It rather is your conduit to a set of utilities for reading timestamped tasks. From there you can keep track of all the relevant entries you have inserted in the files declared as part of org-agenda-files list.

Running 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.
  • Those granted, I prefer to just use the Diary and Calendar for my simple needs: Calendar and Diary (and prot-diary.el).
Org export
I do not have much to offer here, apart from the setup that handles consistent heading IDs and anchor tags (the latter concerns the HTML output). Everything in that segment, 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).

Finally, note that I sometimes deliver simple presentations using Org. Refer to Custom extensions for "focus mode" (prot-logos.el).

;;; Org-mode (personal information manager)
;; Pro tip: If you are reading the source code, use C-c '
;; (`org-edit-special') to put the code block in a dedicated buffer and
;; then activate `prot-outline-minor-mode-safe' to conveniently browse
;; this massive code block.
(prot-emacs-builtin-package 'org
  (setq org-directory (convert-standard-filename "~/Documents/org"))
  (setq org-imenu-depth 7)
;;;; general settings
  (setq org-adapt-indentation nil)      ; No, non, nein, όχι!
  (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-modules '(ol-info ol-eww))
  (setq org-use-sub-superscripts '{})
  (setq org-insert-heading-respect-content t)

;;;; 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)" "WAIT(w@/!)" "|" "CANCEL(c@)" "DONE(d!)")))
  (setq org-todo-keyword-faces
        '(("WAIT" . '(bold org-todo))
          ("CANCEL" . '(bold org-done))))
  (setq org-use-fast-todo-selection 'expert)
  (setq org-priority-faces
        '((?A . '(bold 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 nil)
  (setq org-highlight-latex-and-related nil) ; other options affect elisp regexp in src blocks
  (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 ; I don't really use those, but whatever
        '(("meeting")
          ("admin")
          ("emacs")
          ("modus")
          ("politics")
          ("economics")
          ("philosophy")
          ("book")
          ("essay")
          ("mail")
          ("purchase")
          ("hardware")
          ("software")
          ("website")))

;;;; log
  (setq org-log-done 'time)
  (setq org-log-into-drawer t)
  (setq org-log-note-clock-out nil)
  (setq org-log-redeadline 'time)
  (setq org-log-reschedule 'time)
  (setq org-read-date-prefer-future 'time)

;;;; links
  (setq org-link-keep-stored-after-insertion nil)
  ;; TODO 2021-10-15 org-link-make-description-function

  ;; See my prot-pulse.el for what this does.  Basically it recentres
  ;; the window the way I like and pulse the line at point to make it
  ;; easier to make sense of the context.
  (add-hook 'org-follow-link-hook #'prot-pulse-recentre-top)

;;;; capture
  (setq org-capture-templates
        `(("b" "Basic task for future review" entry
           (file+headline "tasks.org" "Tasks to be reviewed")
           ,(concat "* %^{Title}\n"
                    ":PROPERTIES:\n"
                    ":CAPTURED: %U\n"
                    ":END:\n\n"
                    "%i%l")
           :empty-lines-after 1)
          ("t" "Task with a due date" entry
           (file+headline "tasks.org" "Tasks with a date")
           ,(concat "* TODO %^{Title} %^g\n"
                    "SCHEDULED: %^t\n"
                    ":PROPERTIES:\n"
                    ":CAPTURED: %U\n"
                    ":END:\n\n"
                    "%i%?")
           :empty-lines-after 1)
          ("m" "Make email note" entry
           (file+headline "tasks.org" "Mail correspondence")
           ,(concat "* TODO %:subject :mail:\n"
                    "SCHEDULED: %t\n:"
                    "PROPERTIES:\n"
                    ":CONTEXT: %a\n"
                    ":END:\n\n"
                    "%i%?")
           :empty-lines-after 1)))

  (setq org-capture-templates-contexts
        '(("m" ((in-mode . "notmuch-search-mode")
                (in-mode . "notmuch-show-mode")
                (in-mode . "notmuch-tree-mode")))))

;;;; agenda
;;;;; Basic agenda setup
  (setq org-default-notes-file (thread-last org-directory (expand-file-name "notes.org")))
  (setq org-agenda-files `(,org-directory "~/Documents"))
  (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)

  ;; TODO 2021-08-27: Does this work?
  ;; Create reminders for tasks with a due date when this file is read.
  (run-at-time (* 60 5) nil #'org-agenda-to-appt)

;;;;; General agenda 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-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 ?—)

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

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

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

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

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

;;;;; Agenda 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
        (concat "Now " (make-string 36 ?—)))
  (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)

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

;;;;; Agenda 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 -100)

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

;;;;; Agenda logging and 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-agenda-sort-notime-is-late t) ; Org 9.4
  (setq org-agenda-sort-noeffort-is-high t) ; Org 9.4

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

;;;; code blocks
  (setq org-confirm-babel-evaluate nil)
  (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)

;;;; export
  (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)
  (require 'ox-texinfo)
  (require 'ox-md)
  ;; FIXME: how to remove everything else?
  (setq org-export-backends '(html texinfo md))

;;;; IDs
  (setq org-id-link-to-org-use-id
        'create-if-interactive-and-no-custom-id)

  (let ((map global-map))
    (define-key map (kbd "C-c a") #'org-agenda)
    (define-key map (kbd "C-c c") #'org-capture)
    (define-key map (kbd "C-c l") #'org-store-link))
  (let ((map org-mode-map))
    (define-key map (kbd "C-'") nil)
    (define-key map (kbd "C-,") nil)
    (define-key map (kbd "<C-return>") nil)
    (define-key map (kbd "<C-S-return>") nil)
    (define-key map (kbd "C-M-S-<right>") nil)
    (define-key map (kbd "C-M-S-<left>") nil)
    (define-key map (kbd "C-c S-l") #'org-toggle-link-display)
    (define-key map (kbd "C-c C-S-l") #'org-insert-last-stored-link)))

(prot-emacs-builtin-package 'prot-org
  (setq org-agenda-format-date #'prot-org-agenda-format-date-aligned)

  (add-to-list 'org-capture-templates
               '("j" "Music suggestion (jukebox)" entry
                 (file+headline "tasks.org" "Music suggestions")
                 #'prot-org-capture-jukebox
                 :empty-lines-after 1
                 :immediate-finish t)))

Here are my few extras for Org (from my dotfiles' repo):

;;; prot-org.el --- Tweaks for my org-mode configurations -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; This covers my tweaks for Org that are meant for use in my
;; Emacs setup: https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'prot-common)

(defgroup prot-org ()
  "Extensions for org.el."
  :group 'org)

;;;; Source blocks

(defvar modus-themes-org-blocks)
(defvar org-fontify-whole-block-delimiter-line)

(defun prot-org--modus-themes-fontify-block-delimiters ()
  "Match `org-fontify-whole-block-delimiter-line' to theme style.
Run this function at the post theme load phase, such as with the
hook `modus-themes-after-load-theme-hook'."
  (if (eq modus-themes-org-blocks 'gray-background)
      (setq org-fontify-whole-block-delimiter-line t)
    (setq org-fontify-whole-block-delimiter-line nil))
  (when (derived-mode-p 'org-mode)
    (font-lock-flush)))

(when (require 'modus-themes nil t)
  (add-hook 'modus-themes-after-load-theme-hook
            #'prot-org--modus-themes-fontify-block-delimiters))

;;;; org-capture

(defvar prot-org-agenda-after-edit-hook nil
  "Hook that runs after select Org commands.
To be used with `advice-add'.")

(declare-function prot-bongo-show "prot-bongo")

(defun prot-org-capture-jukebox ()
  "Capture template for current Bongo songo." ; NOTE 2021-10-06: Brilliant typo!
  (concat "* " (prot-bongo-show) " :jukebox:\n"
          ":PROPERTIES:\n"
          ":CAPTURED: %U\n"
          ":END:\n\n"))

(defun prot-org--agenda-after-edit (&rest _)
  "Run `prot-org-agenda-after-edit-hook'."
  (run-hooks 'prot-org-agenda-after-edit-hook))

(dolist (fn '(org-agenda-archive org-archive-subtree))
  (advice-add fn :after #'prot-org--agenda-after-edit))

(dolist (hook '(org-capture-after-finalize-hook
                  org-agenda-after-show-hook
                  prot-org-agenda-after-edit-hook))
  (add-hook hook #'org-agenda-to-appt))

(declare-function cl-letf "cl-lib")

;; Source: https://stackoverflow.com/a/54251825
(defun prot-org--capture-no-delete-windows (oldfun args)
  (cl-letf (((symbol-function 'delete-other-windows) 'ignore))
    (apply oldfun args)))

;; Same source as above
(advice-add 'org-capture-place-template
            :around 'prot-org--capture-no-delete-windows)

;;;; org-agenda

(declare-function calendar-day-name "calendar")
(declare-function calendar-day-of-week "calendar")
(declare-function calendar-month-name "calendar")
(declare-function org-days-to-iso-week "org")
(declare-function calendar-absolute-from-gregorian "calendar")

(defvar org-agenda-format-date)

;;;###autoload
(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)))

;;;; org-export

(declare-function org-html-export-as-html "org")
(declare-function org-texinfo-export-to-info "org")

;;;###autoload
(defun prot-org-ox-html ()
  "Streamline HTML export."
  (interactive)
  (org-html-export-as-html nil nil nil t nil))

;;;###autoload
(defun prot-org-ox-texinfo ()
  "Streamline Info export."
  (interactive)
  (org-texinfo-export-to-info))

;;;; org-id

(declare-function org-id-add-location "org")
(declare-function org-with-point-at "org")
(declare-function org-entry-get "org")
(declare-function org-id-new "org")
(declare-function org-entry-put "org")

;; Copied from this article (with minor tweaks from my side):
;; <https://writequit.org/articles/emacs-org-mode-generate-ids.html>.
(defun prot-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)))))

(declare-function org-map-entries "calendar")

;;;###autoload
(defun prot-org-id-headlines ()
  "Add missing CUSTOM_ID to all headlines in current file."
  (interactive)
  (org-map-entries
   (lambda () (prot-org--id-get (point) t))))

(provide 'prot-org)
;;; prot-org.el ends here

5.8. Calendar and Diary (and prot-diary.el)

Relevant information for what you are about to read in this section (the list will be updated accordingly):

Emacs provides a suite of tools for planning events or recording dates of interest in your life. These are part of the same workflow, but are divided into several smaller libraries. The two main ones are diary-lib.el and calendar.el. I extend them with prot-diary.el, which is reproduced at the end of this section, after the package configurations.

diary-lib.el defines everything that pertains to the diary-file (defaults to ~/.emacs.d/diary, which is what I want). The Diary has a two-fold purpose: (i) to store entries about calendar events, and (ii) to display such events for the given day or range thereof.

In the first case, the Diary functions as a regular buffer that you may edit freely. This is where you can write one-off or recurring events using various date and time formats expressed in natural language or as Elisp forms (a sexp or "symbolic expression").

When it comes to single events for a given day, I prefer to input them on a whole line using the ISO 8601 standard and a 24-hour clock, like this: 2021-04-13 15:00 Description of event.

For multiple events that are clustered on the same day, I write the date on its own line and below it place a description of each event optionally preceded by a timestamp. Entries with no timestamp come first, followed by those that are time sensitive. This is just a matter of personal style.

In this scenario of grouped events under a given date, each entry below the date has to start with at least one empty space, otherwise the Diary will try to interpret them as two distinct expressions (the exact number of spaces is a matter of preference). So the format looks like this (use C-x TAB (indent-rigidly) to incrementally adjust the indentation of the active region, or C-u C-x TAB to increase by four spaces):

2021-04-13
    This event is not time sensitive
    06:30-09:00 Hiking
    10:00 Prepare sourdough bread
    11:00-13:00 Write stuff

The following is also possible, but looks too busy for no good reason:

2021-04-13 This event is not time sensitive
2021-04-13 06:30-09:00 Hiking
2021-04-13 10:00 Prepare sourdough bread
2021-04-13 11:00-13:00 Write stuff

Though not required, you can separate events by an empty line. Keeps things easy to read. Remember that lines that contain at least one space are thought to belong to the closest date entry above them. Which means that for multiple events on a given day you can use something like this, if you need to (here dots represent empty spaces):

2021-04-13
    This event is not time sensitive
....
    06:30-09:00 Hiking
....
    10:00 Prepare sourdough bread
....
    11:00-13:00 Write stuff

Furthermore, it is possible to use nested levels of indentation or, in my case, call prot-simple-cite-region and indent it accordingly (check the source code: Common custom functions (prot-simple.el)). For example:

2021-04-13
    This event is not time sensitive
....
    06:30-09:00 Hiking
    +----[ Sample text ]
    | This is some random text
    | It relates to the above event
    +----
....
    10:00 Prepare sourdough bread
    11:00-13:00 Write stuff

Remember that these are mere stylistic considerations. I generally prefer to keep things simple, though I appreciate the flexibility of adapting the view of my evolving needs.

I prefer to defer all formatting related decisions until after I have typed in all the information. My prot-diary-align-timestamped-entries lets me align the negative space between an entry's timestamp and its description. Consider the following block and suppose that the region starts from the line below the date and ends at the end of the line with the last entry (demarcated by the | characters).

2021-04-13
|This event is not time sensitive
06:30-09:00 Hiking
10:00 Prepare sourdough bread
11:00-13:00 Write stuff|

...becomes...

2021-04-13
    This event is not time sensitive
    06:30-09:00 Hiking
    10:00       Prepare sourdough bread
    11:00-13:00 Write stuff

For anniversaries or cyclic events, I rely on Elisp. It is better to read the Emacs manual on the technicalities as there are lots of examples that should help you get started. Just bear in mind that the exact order for the YEAR, MONTH, DATE arguments depends on the value assigned to the variable calendar-date-style. As I already noted, I follow the ISO style (evaluate this: (format-time-string "%F")).

The second use case of the Diary, namely, of showing the events it holds is controlled by the variable diary-display-function. On my setup it should present the information in a read-only buffer that is internally referred to as the "fancy" style. To bring up that interface, you can either use the Calendar as a starting point (more on that later) or call a command directly, such as prot-diary-display-entries. The resulting buffer is fairly straightforward: there is a date header (or many depending on the prefix numeric argument) and below that one or more entries related to it. Those retain the indentation of the underlying Diary file, as described above. Hit RET over an entry to visit the editable version of the Diary.

Apart from displaying that buffer on demand, it is possible to get its information emailed to you. The built-in command for that is diary-mail-days, however it has the downside of sending a mail even when there are no events for the day[s] specified. I do not see the utility of such behaviour: what is the purpose of drawing attention to my email client, only to waste my time with some "nothing to see here" message? Whereas prot-diary-mail-entries only sends a message when that is the right thing to do, i.e. when there is something that requires our attention (for N days or diary-mail-days). It also has the benefit of not popping up the Diary display in another window as a side effect of sending the email (check prot-diary--mail-fn for the technicalities). I set it up to automatically try to send me an email briefing each morning when I launch Emacs.

Email briefings just offer an overview of events in the immediate future. We still need a mechanism to alert us some minutes in advance of a time sensitive appointment. This is where the appt.el library comes in handy. It already knows how to scan the Diary in search for time expressions and produce reminders for them: all it needs is to be activated with (appt-activate 1). Unlike email, this is a more persistent method of producing notifications inside of Emacs in the form of a pop-up window and a complementary modeline indicator with a countdown to the event. The default notification time is specified in the variable appt-message-warning-time, though individual Diary entries can declare their own time (in minutes), by holding some extra text as defined in appt-warning-time-regexp. Put that in a comment (diary-comment-start) to disambiguate it from the entry's description. For example, this Diary entry will work as expected with my configurations: 2021-04-13 18:00 Some task ;; appt 5. The ;; appt 5 part will not be shown in the Diary display, but will still activate the reminder. Nice and simple!

To recapitulate, the Diary is flexible enough to accommodate a variety of preferences on how to organise one's life events. The key to get started is to learn how to edit the actual file, which is fairly easy. It then is trivial to either display that information on demand or be notified about it.

Let us now cover the other major component of this suite of tools: the Calendar. The main point of entry is M-x calendar. It displays a horizontal three-month view centred on the current month. Moving in the calendar is consistent with regular Emacs motions: C-f and C-b move one day forward/backward, C-n and C-p go down and up one week, C-a and C-e place the point at the beginning or end of the week, respectively. Then we have the equivalent of paragraph and page motions: M-{ and M-} which travel forward/backward a full month, with C-v and M-v moving to the next and previous three-month set. To merely scroll the horizontal calendar strip, use < and > to do so in the given direction. As always, do not forget to type C-h m (describe-mode) in any major-mode buffer you are unfamiliar with to get help for it: you will learn about those motions and some more of them (and always check the manual as well).

The Calendar offers an overview of your Diary entries, as noted above, and, optionally, of all holidays that you have opted in to. For the latter, check the variable calendar-holidays. I set it to only a small set of records largely for experimental purposes, as I am not interested in either religious or secular special days. Consider this, if you will, an attempt at introducing the illusion of eventfulness in my calendar. I really don't care about any of this.

To mark Diary entries in the Calendar, use m (diary-mark-entries). That accepts a prefix argument (C-u) in case you wish to redraw those marks. Here we should clarify that "marks" are Emacs faces by default, unless you explicitly override them with characters. I prefer faces, which leave a more pleasant, less noisy impression. Similarly, use x (calendar-mark-holidays) to mark holidays. Both of those tasks can be performed automatically upon accessing the Calendar, by setting the variables calendar-mark-diary-entries-flag, calendar-mark-holidays-flag to non-nil values (which is what I do).

Events that should not appear in the Calendar when Diary entries are marked must be preceded with a special character, which is configurable by means of diary-nonmarking-symbol. I personally have no use for this, because if something is not worth being marked, then it is not pointless to keep track of it in my day-to-day planner.

While in the Calendar, you can move the point over a marked day and type d (diary-view-entries) to show the read-only variant of the Diary, as outlined above. Or pass a number to it to encompass the Nth days (e.g. typing 7 d will show Diary entries for seven days starting from the current one). If the mark pertains to a holiday, type h to get a message in the echo area about it (calendar-cursor-holidays).

If you opt to use my settings, the best way to learn how to disambiguate the two marks is to access the Calendar and hit u (calendar-unmark). Follow it up with m to mark Diary entries and x to render holiday marks.

Other than viewing what happens on a given day or date range, you can use the Calendar to record new entries in the Diary. I group those under the i prefix key: so type, i and then C-h to get help about all possible keys that complete that sequence and references to the commands they call. Note that to insert a block event you need to hit C-SPC on the starting date and then move point to the ending day at which you should do i b (diary-insert-block-entry).

It is possible to use more faces than the ones which are provided by default. To do so, you need to specify a face argument to the various Elisp expressions that can be written to the Diary. For example, my prot-diary.el (reproduced further below) has a bespoke face for "administrative tasks", which can be assigned to the given expression like this (new line and indentation are optional, as mentioned above, otherwise keep everything on one line):

 %%(diary-date t t 13 'prot-diary-calendar-administrative-mark)
     Some administrative monthly task
 %%(diary-anniversary 1900 4 13 'prot-diary-calendar-anniversary-mark)
     Some anniversary is %d years

The manual covers a lot of other potentially useful tasks you may accomplish with those tools. For example, it is possible to import an iCalendar, convert from one calendar to another (e.g. Gregorian to Chinese), get the phases of the moon, sunset and sunrise times in the given geographic coordinates… Evaluate this expression to start reading the relevant Info nodes: (info "(emacs) Calendar/Diary").

For the integration with outline-minor-mode that prot-diary.el provides, refer to the relevant section (in short: you get header folding like in Org for every line that starts with ;;;, given my diary-comment-start is ;;): Outline mode, outline minor mode (prot-outline.el).

Finally, note that I control the placement of those buffers in the display-buffer-alist (read: Window rules and basic tweaks).

;;; Calendar and Diary (and prot-diary.el)
(prot-emacs-builtin-package 'calendar
  (setq calendar-mark-diary-entries-flag t)
  (setq calendar-mark-holidays-flag t)
  (setq calendar-mode-line-format nil)
  (setq calendar-time-display-form
        '(24-hours ":" minutes
                   (when time-zone
                     (format "(%s)" time-zone))))
  (setq calendar-week-start-day 1)      ; Monday
  (setq calendar-date-style 'iso)
  (setq calendar-date-display-form calendar-iso-date-display-form)
  (setq calendar-time-zone-style 'numeric) ; Emacs 28.1

  (require 'solar)
  (setq calendar-latitude 35.17         ; Not my actual coordinates
        calendar-longitude 33.36)

  (require 'cal-dst)
  (setq calendar-standard-time-zone-name "+0200")
  (setq calendar-daylight-time-zone-name "+0300")

  (require 'diary-lib)
  (setq diary-mail-addr user-mail-address)
  (setq diary-date-forms diary-iso-date-forms)
  (setq diary-comment-start ";;")
  (setq diary-comment-end "")
  (setq diary-nonmarking-symbol "!")
  (setq diary-show-holidays-flag t)
  (setq diary-display-function #'diary-fancy-display) ; better than its alternative
  (setq diary-header-line-format nil)
  (setq diary-list-include-blanks nil)
  (setq diary-number-of-entries 2)
  (setq diary-mail-days 2)
  (setq diary-abbreviated-year-flag nil)

  (add-hook 'calendar-today-visible-hook #'calendar-mark-today)
  (add-hook 'diary-list-entries-hook 'diary-sort-entries t)
  (add-hook 'diary-mode-hook #'goto-address-mode) ; buttonise plain text links

  ;; Those presuppose (setq diary-display-function #'diary-fancy-display)
  (add-hook 'diary-list-entries-hook 'diary-include-other-diary-files)
  (add-hook 'diary-mark-entries-hook 'diary-mark-included-diary-files)

  ;; Prevent Org from interfering with my key bindings.
  (remove-hook 'calendar-mode-hook #'org--setup-calendar-bindings)

  (let ((map calendar-mode-map))
    (define-key map (kbd "s") #'calendar-sunrise-sunset)
    (define-key map (kbd "l") #'lunar-phases)
    (define-key map (kbd "i") nil) ; Org sets this, much to my chagrin (see `remove-hook' above)
    (define-key map (kbd "i a") #'diary-insert-anniversary-entry)
    (define-key map (kbd "i b") #'diary-insert-block-entry)
    (define-key map (kbd "i c") #'diary-insert-cyclic-entry)
    (define-key map (kbd "i d") #'diary-insert-entry) ; for current "day"
    (define-key map (kbd "i i") #'diary-insert-entry) ; most common action, easier to type
    (define-key map (kbd "i m") #'diary-insert-monthly-entry)
    (define-key map (kbd "i w") #'diary-insert-weekly-entry)
    (define-key map (kbd "i y") #'diary-insert-yearly-entry)
    (define-key map (kbd "M-n") #'calendar-forward-month)
    (define-key map (kbd "M-p") #'calendar-backward-month)))

(prot-emacs-builtin-package 'appt
  (setq appt-display-diary nil)
  (setq appt-disp-window-function #'appt-disp-window)
  (setq appt-display-mode-line t)
  (setq appt-display-interval 3)
  (setq appt-audible nil)
  (setq appt-warning-time-regexp "appt \\([0-9]+\\)")
  (setq appt-message-warning-time 15)

  (run-at-time 10 nil #'appt-activate 1))

(prot-emacs-builtin-package 'prot-diary
  ;; The idea is to get a reminder via email when I launch Emacs in the
  ;; morning and this file is evaluated.  Obviously this is not a super
  ;; sophisticated approach, though I do not need one.
  (let ((time (string-to-number (format-time-string "%H"))))
    (when (and (> time 4) (< time 9))
      (run-at-time (* 60 5) nil #'prot-diary-mail-entries)))

  (require 'holidays)
  (setq calendar-holidays (append holiday-solar-holidays prot-diary-local-holidays))

  (with-eval-after-load 'prot-outline
    (add-hook 'diary-mode-hook #'prot-outline-minor-mode-safe))

  (let ((map diary-mode-map))
    (define-key map (kbd "<M-return>") #'prot-diary-newline-indent)
    (define-key map (kbd "M-n") #'prot-diary-heading-next)
    (define-key map (kbd "M-p") #'prot-diary-heading-previous)
    (define-key map (kbd "C-c C-a") #'prot-diary-align-timestamped-entries))
  (let ((map global-map))
    (define-key map (kbd "C-c d c") #'calendar)
    (define-key map (kbd "C-c d d") #'prot-diary-display-entries)
    (define-key map (kbd "C-c d e") #'prot-diary-edit-diary)
    (define-key map (kbd "C-c d i") #'prot-diary-insert-entry)
    (define-key map (kbd "C-c d m") #'prot-diary-mail-entries)))

These are the contents of the prot-diary.el library (find the source code in my dotfiles' repo (as with all my Elisp code)):

;;; prot-diary.el --- Extensions for the diary and calendar -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Extensions for the diary and calendar, intended for my Emacs setup:
;; <https://protesilaos.com/emacs/dotemacs/>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'calendar)
(require 'diary-lib)
(require 'prot-common)

(defgroup prot-diary ()
  "Tweaks for the calendar and diary."
  :group 'diary)

;;;; Commands and utilities

(defun prot-diary--list-entries (n inhibit)
  "Check for N days with diary entries.
When optional INHIBIT is non-nil, do not show thediary buffer."
  (let ((inhibit-message t)
        (hide (if inhibit t nil)))
    (diary-list-entries (calendar-current-date) n hide)))

(defvar prot-diary--current-window-configuration nil
  "Current window configuration.")

(defvar prot-diary--current-window-configuration-point nil
  "Point in current window configuration.")

(defun prot-diary--store-window-configuration ()
  "Store current window configuration and point."
  (setq prot-diary--current-window-configuration (current-window-configuration))
  (setq prot-diary--current-window-configuration-point (point)))

(defun prot-diary--restore-window-configuration ()
  "Restore `prot-diary--store-window-configuration'."
  (when prot-diary--current-window-configuration
    (set-window-configuration prot-diary--current-window-configuration))
  (when prot-diary--current-window-configuration-point
    (goto-char prot-diary--current-window-configuration-point)))

(autoload 'message-goto-body "message")

;;;###autoload
(defun prot-diary-mail-entries (&optional ndays)
  "Email diary entries for NDAYS or `diary-mail-days'.

With optional DAYS as a positive integer, produce a list for N
days including the current one (so 2 is today and tomorrow).
Otherwise use `diary-mail-days'.

Alternative of `diary-mail-entries'.  Does not show the diary
buffer after sending the email and does not send a mail when no
entries are present (what is the point of first notifying me at
my inbox and then telling me 'Oh, nothing of interest here'?)."
  (interactive "p")
  (if (or (string-equal diary-mail-addr "")
          (null diary-mail-addr))
      (user-error "You must set `diary-mail-addr' to use this command")
    (let ((entries)
          (diary-display-function #'diary-fancy-display)
          (diary-mail-addr user-mail-address)
          (mail-user-agent 'message-user-agent)
          (n (or ndays diary-mail-days)))
      (prot-common-number-integer-positive-p n)
      (prot-diary--store-window-configuration)
      (diary-list-entries (calendar-current-date) (or n diary-mail-days))
      (if (prot-diary--list-entries n t)
          (progn
            (with-current-buffer (get-buffer diary-fancy-buffer)
              (setq entries (buffer-string))
              (kill-buffer) ; FIXME 2021-04-13: `bury-buffer' does not bury it...
              (prot-diary--restore-window-configuration))
            (compose-mail diary-mail-addr
                          (concat "Diary entries generated "
                                  (calendar-date-string (calendar-current-date)))
                          nil)
            (message-goto-body)
            (insert entries)
            (funcall (get mail-user-agent 'sendfunc)))
        (message "No diary entries; skipping email delivery")))))

;;;###autoload
(defun prot-diary-display-entries (&optional days)
  "Display diary entries, if any.
With optional DAYS as a positive integer, produce a list for N
days including the current one (so 2 is today and tomorrow).
Otherwise use `diary-mail-days'."
  (interactive "p")
  (let ((n (or days diary-mail-days)))
    (prot-common-number-integer-positive-p n)
    (unless (prot-diary--list-entries n nil)
      (message "No diary entries; skipping display"))))

;;;###autoload
(defun prot-diary-edit-diary ()
  "Visit `diary-file'."
  (interactive)
  (let ((diary diary-file))
    (if (and (boundp 'diary-file)
             (file-regular-p diary))
        (find-file (expand-file-name diary))
      (error "No regular file `diary-file' is available"))))

;;;###autoload
(defun prot-diary-insert-entry (date)
  "Insert diary entry for DATE formatted as plain text."
  (interactive
   (list (read-string "Input date: " (format-time-string "%F"))))
  (diary-make-entry
   (concat (or date (format-time-string "%F")) "\s")))

(defvar align-default-spacing)

;;;###autoload
(defun prot-diary-align-timestamped-entries (beg end)
  "Align and indent region between BEG and END."
  (interactive "r")
  (let ((align-default-spacing 3))
    (align-regexp beg end (concat diary-time-regexp "\\( \\)") 2)
    (indent-rigidly beg end 4)))

;;;###autoload
(defun prot-diary-newline-indent ()
  "Insert newline and indent by four spaces."
  (interactive)
  (delete-horizontal-space)
  (newline)
  (insert (make-string 4 ?\s)))

;;;; Fontification extras

(defface prot-diary-calendar-anniversary-mark
  '((((class color) (min-colors 88) (background light))
     :background "#fff1f0" :foreground "#a60000")
    (((class color) (min-colors 88) (background dark))
     :background "#2c0614" :foreground "#ff8059")
    (t :foreground "red"))
  "Face to mark anniversaries in the calendar.")

(defface prot-diary-calendar-administrative-mark
  '((((class color) (min-colors 88) (background light))
     :background "#fff3da" :foreground "#813e00")
    (((class color) (min-colors 88) (background dark))
     :background "#221000" :foreground "#eecc00")
    (t :foreground "yellow"))
  "Face to mark administrative tasks in the calendar.")

(defface prot-diary-calendar-event-mark
  '((((class color) (min-colors 88) (background light))
     :background "#aceaac" :foreground "#004c00")
    (((class color) (min-colors 88) (background dark))
     :background "#00422a" :foreground "#9ff0cf")
    (t :foreground "green"))
  "Face to mark events in the calendar.")

(defface prot-diary-calendar-mundane-mark
  '((((class color) (min-colors 88) (background light))
     :background "#f0f0f0" :foreground "#505050")
    (((class color) (min-colors 88) (background dark))
     :background "#191a1b" :foreground "#a8a8a8")
    (t :inherit shadow))
  "Face to mark mundane tasks in the calendar.")

;; I might expand this further, depending on my usage patterns and the
;; conventions I establish over time.
(defconst prot-diary-font-lock-keywords
  `((,(format "^%s?\\(%s\\)" (regexp-quote diary-nonmarking-symbol)
             (regexp-quote diary-sexp-entry-symbol))
     (1 'font-lock-constant-face t))
    (diary-font-lock-sexps
     (0 'font-lock-function-name-face t))
    (,(format "^%s" (regexp-quote diary-nonmarking-symbol))
     (0 'font-lock-negation-char-face t))
    (,(format "%s.*" diary-comment-start)
     (0 'font-lock-comment-face)))
  "Rules for extra Diary fontification.")

(defvar outline-regexp)
(defvar outline-heading-end-regexp)

(defun prot-diary--outline-level ()
  "Determine Outline heading level.
To be assigned to the variable `outline-level'."
  (let ((regexp "\\(;;+\\{2,\\}\\) [^ \t\n]"))
    (looking-at regexp)
    (- (- (match-end 1) (match-beginning 1)) 2)))

(defun prot-diary--extras-setup ()
  "Additional setup for Diary mode buffers.
Applies `prot-diary-font-lock-keywords' and specifies what
constitutes a heading for the purposes of Outline minor mode."
  (when (derived-mode-p 'diary-mode)
    (font-lock-flush (point-min) (point-max))
    (font-lock-add-keywords nil prot-diary-font-lock-keywords t)
    (setq outline-regexp (format "%s+\\{2,\\} [^ \t\n]" diary-comment-start))
    (setq outline-level #'prot-diary--outline-level)
    (setq outline-heading-end-regexp (format "%s$" diary-comment-end))))

(add-hook 'diary-mode-hook #'prot-diary--extras-setup)

(defconst prot-diary-date-pattern
  "^!?\\(\\([0-9]+\\|\\*\\)[-/]\\([0-9]+\\|\\*\\)[-/]\\([0-9]+\\|\\*\\)\\|%%\\)"
  "Date pattern found in my diary (NOT ALL POSSIBLE PATTERNS).")

;;;###autoload
(defun prot-diary-heading-next (&optional arg)
  "Move to next or optional ARGth Dired subdirectory heading.
For more on such headings, read `dired-maybe-insert-subdir'."
  (interactive "p")
  (let ((heading prot-diary-date-pattern))
    (goto-char (point-at-eol))
    (re-search-forward heading nil t (or arg nil))
    (goto-char (match-beginning 1))
    (goto-char (point-at-bol))))

;;;###autoload
(defun prot-diary-heading-previous (&optional arg)
  "Move to previous or optional ARGth Dired subdirectory heading.
For more on such headings, read `dired-maybe-insert-subdir'."
  (interactive "p")
  (let ((heading prot-diary-date-pattern))
    (goto-char (point-at-bol))
    (re-search-backward heading nil t (or arg nil))
    (goto-char (point-at-bol))))

;;;; Holidays

(defvar prot-diary-local-holidays
  '((holiday-greek-orthodox-easter 0 "Easter")
    (holiday-greek-orthodox-easter -48 "Green Monday") ; REVIEW: is this correct?
    (holiday-fixed 5 9 "Day of Europe"))
  "I don't care about any of those---EXPERIMENTAL.")

(provide 'prot-diary)
;;; prot-diary.el ends here

5.9. Email settings

Once you delve into the technical details, email is hard as it consists of arcane specs and protocols. Combined with Emacs' open-endedness as well as the wealth of free software command-line programs on offer, there are seemingly infinite ways to set things up. The toolset for my current setup consists of the following:

  • The external mbsync program to periodically synchronise my remote email server and my local mail directories. This allows me to keep a copy of my correspondence, while it removes the burden of updating mail sources from the client. The latter benefit is particularly important to avoid slowing down Emacs.
  • The Notmuch package which includes the CLI program and the Emacs library that implements a Mail User Agent. Notmuch is a mail indexer that can search through the database using a strictly tags-only workflow. I was using Gnus in the past (between early 2020 and May 2021), but ultimately decided to upgrade my workflow by going with the more flexible approach of Notmuch (I had also used Mu4e before, which is closer in spirit to Notmuch and is a good choice overall—in the source code of my dotfiles, from where you can find this document, there is the prot-mu4e-deprecated-conf.el file in case you need it; same for prot-gnus-deprecated-conf.org).
  • The built-in capabilities to compose and send email.
  • Other extensions to integrate email composition with encryption tools, Dired, Org, contact-completion (EBDB)…

This is a mega section that encompasses all of the aforementioned. Please study it carefully.

5.9.1. Client-agnostic email settings (and prot-mail.el)

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

  • 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 notmuch is in use, it will insert a special "Fcc" header whose task is to copy the outgoing message to the appropriate maildir path (this part is done in the Notmuch section).
  • 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 to read the documentation with M-x describe-variable RET format-time-string RET.
  • The mm-encode and mml-sec are meant to come into effect when encrypting and signing an outgoing message, such as with C-c C-m C-e (mml-secure-message-sign-encrypt). The optional 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 used that setup for a while, but ultimately decided that the extra steps did not make any difference to my workflow, as I always double-check before sending out an email.
  • The gnus-dired library can be used independently of Gnus (and should thus be refactored as "message-dired.el" or something). It provides glue code that integrates Dired buffers with message composition, so that one can attach either the file at point or the marked ones with C-c C-m C-a (gnus-dired-attach).
;;; Client-agnostic email settings (and prot-mail.el)
(prot-emacs-builtin-package 'auth-source
  (setq auth-sources '("~/.authinfo.gpg"))
  (setq user-full-name "Protesilaos Stavrou")
  (setq user-mail-address "public@protesilaos.com"))

(prot-emacs-builtin-package 'mm-encode
  (setq mm-encrypt-option nil) ; use 'guided if you need more control
  (setq mm-sign-option nil))   ; same

(prot-emacs-builtin-package 'mml-sec
  (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))

(prot-emacs-builtin-package 'message
  (setq mail-user-agent 'message-user-agent)
  (setq mail-header-separator (purecopy "*****"))
  (setq message-elide-ellipsis ">\n> [... %l lines elided]\n>\n>") ; NOTE 2021-07-13: experimental
  (setq compose-mail-user-agent-warnings nil)
  (setq message-mail-user-agent t)      ; use `mail-user-agent'
  (setq mail-signature "Protesilaos Stavrou\nhttps://protesilaos.com\n")
  (setq message-signature "Protesilaos Stavrou\nhttps://protesilaos.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)
  (add-to-list 'mm-body-charset-encoding-alist '(utf-8 . base64))

  (add-hook 'message-setup-hook #'message-sort-headers))

(prot-emacs-builtin-package 'gnus-dired ; does not require `gnus'
  (add-hook 'dired-mode-hook #'gnus-dired-mode))

(prot-emacs-builtin-package 'prot-mail
  ;; NOTE 2021-05-14: This is a generic indicator for new mail in the
  ;; maildir.  As I now use notmuch (see relevant section in this
  ;; document) I have an alternative approach in prot-notmuch.el.
  (setq prot-mail-maildir-path-regexp "~/.mail/*/Inbox/new/") ; shell regexp
  (setq prot-mail-mode-line-indicator-commands
        '(notmuch-refresh-this-buffer))
  ;; mode line indicator with the number of new mails
  (prot-mail-mail-indicator -1))

And here is prot-mail.el (part of my dotfiles' repo):

;;; prot-mail.el --- Mail tweaks for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; This covers my email tweaks, for use in my Emacs setup:
;; https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'prot-common)

(defgroup prot-mail ()
  "Extensions for mail."
  :group 'mail)

(defcustom prot-mail-maildir-path-regexp "~/.mail/*/Inbox/new/"
  "Path passed to 'find' for checking new mail in maildir.
As this is passed to a shell command, one can use glob patterns.

The user must ensure that this path or regexp matches the one
specified in the mail syncing program (e.g. mbsync)."
  :type 'string
  :group 'prot-mail)

(defcustom prot-mail-mode-line-indicator-commands '(notmuch-refresh-this-buffer)
  "List of commands that will be advised to update the mode line.
The advice is designed to run a hook which is used internally by
the function `prot-mail-mail-indicator'."
  :type 'list
  :group 'prot-mail)

;;;; Helper functions

(autoload 'auth-source-search "auth-source")

;;;###autoload
(defun prot-mail-auth-get-field (host prop)
  "Find PROP in `auth-sources' for HOST entry."
  (when-let ((source (auth-source-search :host host)))
    (if (eq prop :secret)
       (funcall (plist-get (car source) prop))
      (plist-get (flatten-list source) prop))))

(defvar ebdb-db-list)
(autoload 'ebdb-load "ebdb")

(when (require 'ebdb nil t)
  (defun prot-mail-ebdb-message-setup ()
    "Load EBDB if not done already.
Meant to be assigned to a hook, such as `message-setup-hook'."
    (unless ebdb-db-list
      (ebdb-load))))

;;;; Mode line indicator

;; NOTE 2021-05-14: The following is a more generic approach that uses
;; find to search for new mail.  In my prot-notmuch.el I define an
;; alternative that checks for the "unread" tag, which works better for
;; my particular setup (refer to my prot-emacs.org for the relevant
;; commentary).

(defface prot-mail-mail-count
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#61284f")
    (((class color) (min-colors 88) (background dark))
     :foreground "#fbd6f4")
    (t :foreground "magenta"))
  "Face for mode line indicator that shows a new mail count.")

(defvar prot-mail-new-mail-string nil
  "New maildir count number for the mode line.")

(defun prot-mail--new-mail ()
  "Search for new mail in personal maildir paths."
  (with-temp-buffer
    (shell-command
     (format "find %s -type f | wc -l" prot-mail-maildir-path-regexp) t)
    (buffer-substring-no-properties (point-min) (1- (point-max)))))

(defun prot-mail--mode-string (count)
  "Add properties to COUNT string."
  (when (not (string= count "0"))
    (propertize (format "@%s " count)
                'face 'prot-mail-mail-count
                'help-echo "Number of new items in maildirs")))

(defvar prot-mail--mode-line-mail-indicator nil
  "Internal variable used to store the state of new mails.")

(defun prot-mail--mode-line-mail-indicator ()
  "Prepare new mail count mode line indicator."
  (let* ((count (prot-mail--new-mail))
         (indicator (prot-mail--mode-string count))
         (old-indicator prot-mail--mode-line-mail-indicator))
    (when old-indicator
      (setq global-mode-string (delete old-indicator global-mode-string)))
    (cond
     ((>= (string-to-number count) 1)
      (setq global-mode-string (push indicator global-mode-string))
      (setq prot-mail--mode-line-mail-indicator indicator))
     (t
      (setq prot-mail--mode-line-mail-indicator nil)))))

(defvar prot-mail--mode-line-mail-sync-hook nil
  "Hook to refresh the mode line for the mail indicator.")

(defun prot-mail--add-hook (&rest _)
  "Run `prot-mail--mode-line-mail-sync-hook'.
Meant to be used as advice after specified commands that should
update the mode line indicator with the new mail count."
  (run-hooks 'prot-mail--mode-line-mail-sync-hook))

;;;###autoload
(define-minor-mode prot-mail-mail-indicator
  "Enable mode line indicator with counter for new mail."
  :init-value nil
  :global t
  (if prot-mail-mail-indicator
      (progn
        (run-at-time t 60 #'prot-mail--mode-line-mail-indicator)
        (when prot-mail-mode-line-indicator-commands
          (dolist (fn prot-mail-mode-line-indicator-commands)
            (advice-add fn :after #'prot-mail--add-hook)))
        (add-hook 'prot-mail--mode-line-mail-sync-hook #'prot-mail--mode-line-mail-indicator)
        (force-mode-line-update t))
    (cancel-function-timers #'prot-mail--mode-line-mail-indicator)
    (setq global-mode-string (delete prot-mail--mode-line-mail-indicator global-mode-string))
    (remove-hook 'prot-mail--mode-line-mail-sync-hook #'prot-mail--mode-line-mail-indicator)
    (when prot-mail-mode-line-indicator-commands
      (dolist (fn prot-mail-mode-line-indicator-commands)
        (advice-remove fn #'prot-mail--add-hook)))
    (force-mode-line-update t)))

(provide 'prot-mail)
;;; prot-mail.el ends here
5.9.1.1. Sample of authinfo.gpg

Below is a sample with the contents of my authinfo.gpg. This is read, inter alia, by notmuch and smtpmail to be able to both fetch and send messages from the given account. By default, one can use a generic authinfo file though I consider that reckless: 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 me to reference each name in the various package configurations in this document, and share them publicly without worrying about leaking sensitive data.

Which brings us to the point of actually retrieving those values. The prot-mail-auth-get-field is designed to return a field from the authinfo file (this function is defined in my prot-mail.el, which is reproduced at the end of Client-agnostic email settings (and prot-mail.el)). You will find this function used elsewhere in this document. For example, to get the username and password for host inf we do:

(prot-mail-auth-get-field "inf" :user)   ; login name
(prot-mail-auth-get-field "inf" :secret) ; password
5.9.1.2. .mbsyncrc

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 (see Sample of authinfo.gpg).

IMAPAccount pub
Host mail.gandi.net
UserCmd "gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/pub/ { print $(NF-2); exit; }'"
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
Far :pub-remote:
Near :pub-local:
# Include everything
Patterns *
Create Both
# Expunge Both
SyncState *

##########

IMAPAccount inf
Host mail.gandi.net
UserCmd "gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/inf/ { print $(NF-2); exit; }'"
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
Far :inf-remote:
Near :inf-local:
# Include everything
Patterns *
Create Both
# Expunge Both
SyncState *

##########

IMAPAccount prv
Host mail.gandi.net
UserCmd "gpg2 -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/prv/ { print $(NF-2); exit; }'"
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
Far :prv-remote:
Near :prv-local:
# Include everything
Patterns *
Create Both
# Expunge Both
SyncState *

5.9.2. Notmuch (mail indexer and mail user agent (MUA))

Also watch the video of what you are about to read in the following paragraphs: Demo of the Emacs front-end to Notmuch (2021-05-15).

At its core, Notmuch is a command-line program that maintains an index of a maildir directory structure and is capable to search through it using a tag-based method. Notmuch is both minimalist and powerful:

  1. its clearly delimited role as a mail indexer allows it to be used in a variety of setups that involve different tools on how to handle email traffic;
  2. while its tag-centric design lifts the inherent restrictions of complex filesystem paths on where a file must be stored, as messages can be grouped together dynamically depending on the search criteria.

In terms of overall setup, I use Notmuch together with mbsync to synchronise my maildir with the IMAP server (consult my .mbsyncrc). While I send messages with Emacs' own facility which relies on the sendmail program (refer to Sending email (SMTP)). The latter may change in the future, as I weigh the pros and cons of programs like msmtp, though this is low on my list of priorities. As for the actual composition of emails, it is done by Emacs' message.el library (Client-agnostic email settings (and prot-mail.el)).

Tagging is integral to the Notmuch experience, because it allows you to cope with evolving circumstances. It is best to consider this by means of an example. Suppose that you are working on some project: (1) there is an email in your private account from a close friend that contains valuable information but which is part of an otherwise long thread that is not pertinent in its entirety to the task at hand, (2) there is another message from a mailing list you do not actively participate in and this message lives in your "throwaway" account that you use only for mailing lists and the like, and (3) there is yet another mail from some client or employer that is specific to the project and which is kept in your designated "professional" inbox. Lets say that those correspond to "private@example.tld", "lists@example.tld", "work@example.tld". In a folder-based approach, it is hard to combine those otherwise unrelated files. Whereas with tags, say, +project, we can get everything into focus with a simple search for tag:project.

While we will be using the Emacs interface to Notmuch, it is important to spend some time reading the man pages for the CLI tools. Some examples for searching:

$ notmuch search from:prot*
$ notmuch search from:protesilaos tag:todo
$ notmuch search from:protesilaos or from:socrates
$ notmuch search 'from:"/(info|public)@protesilaos.com/"'
$ notmuch search 'from:"/(info|public)@protesilaos.com/"' date:yesterday..today
$ notmuch search '(from:"*@protesilaos.com*" not to:"*@protesilaos.com*")' date:today
$ notmuch search from:"*@protesilaos.com*" date:today..08:00
$ notmuch search body:recrudescence
$ notmuch search path:inf/Inbox/** date:2021-05

There is no need to develop expertise in that syntax at the outset. Just learn the basics and let the rest grow organically through regular usage. Though do read the man pages as they include important information such as what "stemming" is and how it affects your search results.

Now let's focus on the Emacs side of things, starting with the "notmuch-hello" buffer (it runs notmuch-hello-mode). What exactly shows up here depends on notmuch-hello-sections. I like to keep it clean. At any rate, from here we can switch to results from saved searches or tags, or perform a new search:

  • Type j to invoke notmuch-jump-search: it will produce a prompt for a key binding that corresponds to one of your saved searches (controlled by the variable notmuch-saved-searches).
  • With t for notmuch-search-by-tag you get minibuffer completion against all known tags.
  • Hit s to call notmuch-search which lets you type a query such as one of the aforementioned examples. In the "notmuch-hello" buffer I keep a list of recent searches, which can be helpful to re-use at a later point, though we can always benefit from minibuffer history navigation, such as M-n and M-p while at the notmuch-search prompt.
  • Same principle for z (notmuch-tree) only it differs from the above search in the overall presentation where it visualises each thread using indentation and arrows to show what belongs where. It also reduces the subject line to the first entry in each thread, making it easier to follow the results.

All of those will put you in a buffer that runs notmuch-search-mode. As always, you should use C-h m (describe-mode) to learn about everything related to the current major mode (as well as all active minor modes).

To further limit the results to an arbitrary list of search criteria, use l (notmuch-search-filter) and then supply whatever terms are needed. Or use t, which is here bound to notmuch-search-filter-by-tag to filter directly by tag using completion. For example, I can start from the "notmuch-hello" buffer with notmuch-search-by-tag, select "todo", and then use l or t to further narrow to, say, a "replied" tag or date:today.

Filtering of search results is the best way to narrow the list to relevant matches. You can try supplying just a search term without any keywords and you are likely to get what you are looking for. For example, I was in a search that included results from the emacs-devel and bug-gnu-emacs mailing lists and had several unread threads, though I only wanted to read about "group-function". So I just hit l (notmuch-search-filter) and then typed in that query. Same principle if you wish to exclude something, such as with not tag:flag.

The Notmuch presentation is compact when it comes to longer threads as it reduces them to a single line. We can still get a sense of context by viewing the total count of messages in the thread: this is controlled by the variable notmuch-search-result-format. While I like this as a default, there are cases where we need to visualise the email exchanges: notmuch-tree-from-unthreaded-current-query can do just that and is bound to Z in notmuch-search-mode-map. A similar variant is U for notmuch-unthreaded-from-search-current-query, as it expands the view without adding the tree indicators and without de-duplicating subject lines across the threads. Go back to the standard search view with S (notmuch-search-from-tree-current-query).

All those changes in presentation happen in new buffers: you do not lose anything, unless you explicitly kill those buffers. For the sake of convenience, Notmuch provides the notmuch-bury-or-kill-this-buffer command, bound to q in all those views. Read its doc string to know when it kills and when it buries the buffer. Of course, you may always use commands like previous-buffer as well as minibuffer completion and M-x ibuffer or whatever else you normally use. The point is that you can easily switch views to get the job done.

Speaking of multiple buffers, Notmuch provides two commands to refresh those: (i) notmuch-refresh-this-buffer, which is bound by default to the usual g key as well as =, and (ii) notmuch-refresh-all-buffers which is assigned to M-g. My prot-notmuch-refresh-buffer is a wrapper of those two and is mapped to g: when called with a C-u prefix argument, it refreshes all buffers, else it operates on the current one. There is also G (notmuch-poll-and-refresh-this-buffer) which runs notmuch new externally and then refreshes the buffer—personally I don't need this as mbsync automatically does that every few minutes.

To read a thread, hit RET (notmuch-search-show-thread) with point over it in any those search/tree views. That puts you in notmuch-show-mode (did I tell you about C-h m?). Notmuch organises all messages in the thread as headings, where the first line with the summary of the message uses a special face (a background colour, though that depends on the theme). With point over the heading's line RET expands or contracts the body of the message. The n and p keys move between expanded messages in the thread, while N and P always go to the next and previous items, respectively. An Imenu index is also available, so you can jump to any heading using minibuffer completion (with M-x imenu or some third-party extension like the consult-imenu that I use).

When you compose an email, such as by replying with r or R, you are taken to a buffer that leverages functionality provided by the built-in message.el. As always, run C-h m to get a sense of how things work and what commands you may call: C-c C-c dispatches the email, C-c C-a prompts for an attachment, C-c C-m C-e encrypts and signs, C-c C-m C-a from a Dired buffer attaches the file at point or any marked ones (refer to Client-agnostic email settings (and prot-mail.el)).

For searching to be productive, we must apply tags in accordance with a defined method of organising our workflow (more about my approach further below). Too many over-specialised or poorly considered tags will likely make things more difficult, while too few will probably prevent you from finding what you need. How you go about it is up to you. Notmuch is just a tool: don't believe in the hype of magical exominds or second brains that do the work on your behalf—maximise the potential of the one mind you have and the rest will follow from there.

Tag addition or removal is denoted by a plus or minus prefix to the name of the tag. The k binding exists in all relevant mode maps: it offers a shortcut to the "keys" (tags) that can be used to mark a give entry. That command will prompt for a choice among notmuch-tagging-keys. For example k d will do +del -inbox, based on what I currently have. Invert the meaning of those terms by prefixing the command with a C-u argument.

To freely tag all items in the current view (search, tree, show) use *. That gives you a completion prompt where multiple terms can be supplied, each delimited by a space (internally this is completing-read-multiple and the space is governed by a let-bound crm-separator). Combine that with the aforementioned methods for limiting the view to a given subset for maximum effect.

Other ways to perform tagging operations involve the + and - keys. They also use completing-read-multiple. What those commands offer, beside the minibuffer interface, is the ability to operate on the entries encompassed by the active region, where point and mark delimit the lines to be affected. There are cases where marking a region is faster than narrowing the view with new search criteria, so use whatever makes sense at the moment.

Remember the governing principle of Notmuch: it does not touch your mail. All those tags are specific to the Notmuch database (with the exception of some basic tags that are understood by the IMAP server and can be optionally affected by Notmuch (.notmuch-config has the synchronize_flags=true directive).

As Notmuch is not a traditional mail client, it has neither opinions nor capabilities on how one handles their maildir. It does not move files around nor does it have its own deletion and anti-spam mechanisms. It is up to the user to provide those. While this may seem needlessly limited at first, it is consistent with the precise function that Notmuch performs in the broader email toolkit. To actually delete mail, we can tag it accordingly (e.g. "del") and then run this command either manually or from a script with some cron job or systemd timer:

notmuch search --output=files --format=text0 tag:del | xargs -r0 rm

I actually prefer the manual method: mark items with +del and perform their removal from the disk whenever it is expedient. This has the upside of offering me the chance to review the messages, in case I have mislabelled some item: I have deleted legitimate mail before, so it is not a bad idea to be extra careful.

I have no pressing need to define commands for moving my mail files between different directories: I did not do that even while I was using Gnus. Though I might eventually do it just for the sake of completeness. As such, consider the following package configurations a work-in-progress, though they already offer all I need to get things done.

With regard to the use of tags for day-to-day usage, I like to employ the tag:inbox and tag:unread search as well as tag:inbox for practically everything that is unsorted or of general interest. Then I have specialised tags like "list" for mailing lists which should not belong in the inbox and "todo" for messages that require some further action (those can always be complemented by other tags for greater precision such as +list +emacs). To keep things in check, a "post-new" script outside the Emacs setup performs initial tagging on new mail. For example, my sent messages are marked as -unread while new mailing list traffic goes to its right place with tags like -inbox +list (refer to the sections on .notmuch-config and notmuch post-new rules). Those that I manually mark as +todo can optionally be combined with org-capture to record a task that may appear in the Org agenda: call the org-capture command while viewing such a message and Org will create a direct link back to it: the glue code is found in the section on ol-notmuch.el while my org-capture-templates and their org-capture-templates-contexts are defined in Org-mode (personal information manager)).

Unlike most Emacs packages, I install notmuch from my distro's repos. This is because (i) the CLI program is independent of Emacs, and (ii) notmuch.el is bundled up together with the command-line tool because the two are developed by the same group of people. All good! There is also this note in M-x find-library RET notmuch RET:

Note for MELPA users (and others tracking the development version of notmuch-emacs):

This emacs package needs a fairly closely matched version of the notmuch program. If you use the MELPA version of notmuch.el (as opposed to MELPA stable), you should be prepared to track the master development branch (i.e. build from git) for the notmuch program as well. Upgrading notmuch-emacs too far beyond the notmuch program can CAUSE YOUR EMAIL TO STOP WORKING.

TL;DR: notmuch-emacs from MELPA and notmuch from distro packages is NOT SUPPORTED.

Finally, my prot-notmuch.el contains various commands and some minor stylistic extras including a mode line indicator that shows a count of all items matching the search terms tag:unread and tag:inbox (can be configured by the variable prot-notmuch-mode-line-search-args).

;;; Notmuch (mail indexer and mail user agent (MUA))
;; I install notmuch from the distro's repos because the CLI program is
;; not dependent on Emacs.  Though the package also includes notmuch.el
;; which is what we use here (they are maintained by the same people).
(add-to-list 'load-path "/usr/share/emacs/site-lisp/")
(prot-emacs-builtin-package 'notmuch

;;; Account settings
  (setq notmuch-identities
        (let ((identities))
          (dolist (m `(,(prot-mail-auth-get-field "prv" :user)
                       ,(prot-mail-auth-get-field "inf" :user)
                       ,(prot-mail-auth-get-field "pub" :user)))
            (push (format "%s <%s>" user-full-name m) identities))
          identities))
  (setq notmuch-fcc-dirs
        `((,(prot-mail-auth-get-field "prv" :user) . "prv/Sent")
          (,(prot-mail-auth-get-field "inf" :user) . "inf/Sent")
          (,(prot-mail-auth-get-field "pub" :user) . "pub/Sent")))

;;; General UI
  (setq notmuch-show-logo nil)
  (setq notmuch-column-control t)
  (setq notmuch-hello-auto-refresh t)
  (setq notmuch-hello-recent-searches-max 20)
  (setq notmuch-hello-thousands-separator "")
  ;; ;; See my variant of it in `prot-notmuch' below.
  ;; (setq notmuch-hello-sections '(notmuch-hello-insert-saved-searches))
  (setq notmuch-show-all-tags-list nil)

;;; Search
  (setq notmuch-search-oldest-first nil)
  (setq notmuch-search-result-format
        '(("date" . "%12s  ")
          ("count" . "%-7s  ")
          ("authors" . "%-20s  ")
          ("subject" . "%-120s  ")
          ("tags" . "(%s)")))
  (setq notmuch-tree-result-format
        '(("date" . "%12s  ")
          ("authors" . "%-20s  ")
          ((("tree" . "%s")
            ("subject" . "%s"))
           . " %-120s  ")
          ("tags" . "(%s)")))
  (setq notmuch-search-line-faces
        '(("unread" . notmuch-search-unread-face)
          ("flag" . notmuch-search-flagged-face)))
  (setq notmuch-show-empty-saved-searches t)
  (setq notmuch-saved-searches
        `(( :name "inbox"
            :query "tag:inbox"
            :sort-order newest-first
            :key ,(kbd "i"))
          ( :name "unread (inbox)"
            :query "tag:unread and tag:inbox"
            :sort-order newest-first
            :key ,(kbd "u"))
          ( :name "unread all"
            :query "tag:unread not tag:archived"
            :sort-order newest-first
            :key ,(kbd "U"))
          ( :name "references"
            :query "tag:ref not tag:archived"
            :sort-order newest-first
            :key ,(kbd "r"))
          ( :name "todo"
            :query "tag:todo not tag:archived"
            :sort-order newest-first
            :key ,(kbd "t"))
          ( :name "mailing lists"
            :query "tag:list not tag:archived"
            :sort-order newest-first
            :key ,(kbd "m"))
          ;; Emacs
          ( :name "emacs-devel"
            :query "(from:emacs-devel@gnu.org or to:emacs-devel@gnu.org) not tag:archived"
            :sort-order newest-first
            :key ,(kbd "e d"))
          ( :name "emacs-orgmode"
            :query "(from:emacs-orgmode@gnu.org or to:emacs-orgmode@gnu.org) not tag:archived"
            :sort-order newest-first
            :key ,(kbd "e o"))
          ( :name "emacs-bugs"
            :query "'to:\"/*@debbugs.gnu.org*/\"' not tag:archived"
            :sort-order newest-first :key ,(kbd "e b"))
          ( :name "emacs-humanities"
            :query "(from:emacs-humanities@gnu.org or to:emacs-humanities@gnu.org) not tag:archived"
            :sort-order newest-first :key ,(kbd "e h"))
          ( :name "emacs-elpher"
            :query "(from:~michel-slm/elpher@lists.sr.ht or to:~michel-slm/elpher@lists.sr.ht) not tag:archived"
            :sort-order newest-first :key ,(kbd "e e"))
          ;; CLI tools
          ( :name "notmuch"
            :query "(from:notmuch@notmuchmail.org or to:notmuch@notmuchmail.org) not tag:archived"
            :sort-order newest-first
            :key ,(kbd "cn"))))

;;; Tags
  (setq notmuch-archive-tags '("-inbox" "+archived"))
  (setq notmuch-message-replied-tags '("+replied"))
  (setq notmuch-message-forwarded-tags '("+forwarded"))
  (setq notmuch-show-mark-read-tags '("-unread"))
  (setq notmuch-draft-tags '("+draft"))
  (setq notmuch-draft-folder "drafts")
  (setq notmuch-draft-save-plaintext 'ask)
  ;; ;; NOTE 2021-06-18: See an updated version in the `prot-notmuch'
  ;; ;; section below.
  ;; (setq notmuch-tagging-keys
  ;;       `((,(kbd "a") notmuch-archive-tags "Archive (remove from inbox)")
  ;;         (,(kbd "c") ("+archived" "-inbox" "-list" "-todo" "-ref" "-unread") "Complete and archive")
  ;;         (,(kbd "d") ("+del" "-inbox" "-archived" "-unread") "Mark for deletion")
  ;;         (,(kbd "f") ("+flag" "-unread") "Flag as important")
  ;;         ;; (,(kbd "r") notmuch-show-mark-read-tags "Mark as read")
  ;;         (,(kbd "r") ("+ref" "-unread") "Reference for the future")
  ;;         (,(kbd "s") ("+spam" "+del" "-inbox" "-unread") "Mark as spam")
  ;;         (,(kbd "t") ("+todo" "-unread") "To-do")
  ;;         (,(kbd "u") ("+unread") "Mark as unread")))
  (setq notmuch-tag-formats
        '(("unread" (propertize tag 'face 'notmuch-tag-unread))
          ("flag" (propertize tag 'face 'notmuch-tag-flagged))))
  (setq notmuch-tag-deleted-formats
        '(("unread" (notmuch-apply-face bare-tag `notmuch-tag-deleted))
          (".*" (notmuch-apply-face tag `notmuch-tag-deleted))))

;;; Email composition
  (setq notmuch-mua-compose-in 'current-window)
  (setq notmuch-mua-hidden-headers nil) ; TODO 2021-05-12: Review hidden headers
  (setq notmuch-address-command nil)    ; FIXME 2021-05-13: Make it work with EBDB
  (setq notmuch-always-prompt-for-sender t)
  (setq notmuch-mua-cite-function 'message-cite-original-without-signature)
  (setq notmuch-mua-reply-insert-header-p-function 'notmuch-show-reply-insert-header-p-never)
  (setq notmuch-mua-user-agent-function #'notmuch-mua-user-agent-full)
  (setq notmuch-maildir-use-notmuch-insert t)
  (setq notmuch-crypto-process-mime t)
  (setq notmuch-crypto-get-keys-asynchronously t)
  (setq notmuch-mua-attachment-regexp   ; see `notmuch-mua-send-hook'
        (concat "\\b\\(attache\?ment\\|attached\\|attach\\|"
                "pi[èe]ce\s+jointe?\\|"
                "συνημμ[εέ]νο\\|επισυν[αά]πτω\\)\\b"))

;;; Reading messages
  (setq notmuch-show-relative-dates t)
  (setq notmuch-show-all-multipart/alternative-parts nil)
  (setq notmuch-show-indent-messages-width 0)
  (setq notmuch-show-indent-multipart nil)
  (setq notmuch-show-part-button-default-action 'notmuch-show-save-part)
  (setq notmuch-show-text/html-blocked-images ".") ; block everything
  (setq notmuch-wash-citation-lines-prefix 6)
  (setq notmuch-wash-citation-lines-suffix 6)
  (setq notmuch-wash-wrap-lines-length 100)
  (setq notmuch-unthreaded-show-out nil)
  (setq notmuch-message-headers '("To" "Cc" "Subject" "Date"))
  (setq notmuch-message-headers-visible t)

;;; Hooks and key bindings
  (add-hook 'notmuch-mua-send-hook #'notmuch-mua-attachment-check)
  (remove-hook 'notmuch-show-hook #'notmuch-show-turn-on-visual-line-mode)
  (add-hook 'notmuch-show-hook (lambda () (setq-local header-line-format nil)))

  ;; Use alternating backgrounds, if `stripes' is available.
  (with-eval-after-load 'stripes
    (add-hook 'notmuch-search-hook #'stripes-mode)
    ;; ;; To disable `hl-line-mode':
    ;; (setq notmuch-search-hook nil)
    ;; (add-hook 'notmuch-search-hook #'prot-common-disable-hl-line)
    )

  (let ((map global-map))
    (define-key map (kbd "C-c m") #'notmuch)
    (define-key map (kbd "C-x m") #'notmuch-mua-new-mail)) ; override `compose-mail'
  (define-key notmuch-search-mode-map (kbd "/") #'notmuch-search-filter) ; alias for l
  (define-key notmuch-hello-mode-map (kbd "C-<tab>") nil))

(prot-emacs-builtin-package 'prot-notmuch
  ;; Those are for the actions that are available after pressing 'k'
  ;; (`notmuch-tag-jump').  For direct actions, refer to the key
  ;; bindings below.
  (setq notmuch-tagging-keys
        `((,(kbd "a") notmuch-archive-tags "Archive (remove from inbox)")
          (,(kbd "c") prot-notmuch-mark-complete-tags "Complete and archive")
          (,(kbd "d") prot-notmuch-mark-delete-tags "Mark for deletion")
          (,(kbd "f") prot-notmuch-mark-flag-tags "Flag as important")
          (,(kbd "s") prot-notmuch-mark-spam-tags "Mark as spam")
          (,(kbd "t") prot-notmuch-mark-todo-tags "To-do")
          (,(kbd "x") prot-notmuch-mark-reference-tags "Reference for the future")
          (,(kbd "r") ("-unread") "Mark as read")
          (,(kbd "u") ("+unread") "Mark as unread")))

  (setq prot-notmuch-search-field-width 100)
  (setq notmuch-hello-sections '(prot-notmuch-hello-insert-saved-searches
                                 ;; prot-notmuch-hello-insert-recent-searches
                                 ))

  (add-to-list 'notmuch-tag-formats
               '("encrypted" (propertize tag 'face 'prot-notmuch-encrypted-tag)))
  (add-to-list 'notmuch-tag-formats
               '("sent" (propertize tag 'face 'prot-notmuch-sent-tag)))
  (add-to-list 'notmuch-tag-formats
               '("ref" (propertize tag 'face 'prot-notmuch-ref-tag)))
  (add-to-list 'notmuch-tag-formats
               '("todo" (propertize tag 'face 'prot-notmuch-todo-tag)))
  (add-to-list 'notmuch-tag-formats
               '("spam" (propertize tag 'face 'prot-notmuch-spam-tag)))

  ;; NOTE 2021-05-14: I have an alternative method of finding new mail
  ;; in a maildir tree by using the find command.  It is somewhat
  ;; simplistic, though it worked just fine: see prot-mail.el.  I prefer
  ;; this implementation instead, as it leverages notmuch and so I can
  ;; pass arbitrary search terms to it.
  (setq prot-notmuch-mode-line-count-args "tag:unread and tag:inbox")
  (setq prot-notmuch-mode-line-indicator-commands
        '(notmuch notmuch-refresh-this-buffer))
  ;; Mode line indicator with the number of new mails.
  (prot-notmuch-mail-indicator 1)

  (add-hook 'notmuch-hello-mode-hook #'prot-notmuch-widget-field-face-remap)

  (let ((map notmuch-search-mode-map))
    (define-key map (kbd "a") nil) ; the default is too easy to hit accidentally
    (define-key map (kbd "A") #'notmuch-search-archive-thread)
    (define-key map (kbd "D") #'prot-notmuch-search-delete-thread)
    (define-key map (kbd "T") #'prot-notmuch-search-todo-thread)
    (define-key map (kbd "X") #'prot-notmuch-search-reference-thread)
    (define-key map (kbd "C") #'prot-notmuch-search-complete-thread)
    (define-key map (kbd "S") #'prot-notmuch-search-spam-thread)
    (define-key map (kbd "g") #'prot-notmuch-refresh-buffer))
  (let ((map notmuch-show-mode-map))
    (define-key map (kbd "a") nil) ; the default is too easy to hit accidentally
    (define-key map (kbd "A") #'notmuch-show-archive-message-then-next-or-next-thread)
    (define-key map (kbd "D") #'prot-notmuch-show-delete-message)
    (define-key map (kbd "T") #'prot-notmuch-show-todo-message)
    (define-key map (kbd "X") #'prot-notmuch-show-reference-message)
    (define-key map (kbd "C") #'prot-notmuch-show-complete-message)
    (define-key map (kbd "S") #'prot-notmuch-show-spam-message)))

And here is prot-notmuch.el (part of my dotfiles' repo):

;;; prot-notmuch.el --- Tweaks for my notmuch.el configurations -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; This covers my tweaks for notmuch.el that are meant for use in my
;; Emacs setup: https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'prot-common)

(defgroup prot-notmuch ()
  "Extensions for notmuch.el."
  :group 'notmuch)

(defcustom prot-notmuch-search-field-width 100 ; Or use something like (/ (frame-width) 2)
  "Number of characters for the width of search files.
Those fields appear in the Notmuch hello buffer.  See
`prot-notmuch-hello-insert-recent-searches'."
  :type 'integer
  :group 'prot-notmuch)

(defcustom prot-notmuch-delete-tag "del"
  "Single tag that applies to mail marked for deletion.
This is used by `prot-notmuch-delete-mail'."
  :type 'string
  :group 'prot-notmuch)

(defcustom prot-notmuch-mark-complete-tags '("+archived" "-inbox" "-list" "-todo" "-ref" "-unread")
  "List of tags to mark as completed."
  :type '(repeat string)
  :group 'prot-notmuch)

(defcustom prot-notmuch-mark-delete-tags '("+del" "-inbox" "-archived" "-unread")
  "List of tags to mark for deletion.
To actually delete email, refer to `prot-notmuch-delete-mail'."
  :type '(repeat string)
  :group 'prot-notmuch)

(defcustom prot-notmuch-mark-flag-tags '("+flag" "-unread")
  "List of tags to mark as important (flagged).
This gets the `notmuch-tag-flagged' face, if that is specified in
`notmuch-tag-formats'."
  :type '(repeat string)
  :group 'prot-notmuch)

(defcustom prot-notmuch-mark-spam-tags '("+spam" "+del" "-inbox" "-unread")
  "List of tags to mark as spam."
  :type '(repeat string)
  :group 'prot-notmuch)

(defcustom prot-notmuch-mark-todo-tags '("+todo" "-unread")
  "List of tags to mark as a to-do item."
  :type '(repeat string)
  :group 'prot-notmuch)

(defcustom prot-notmuch-mark-reference-tags '("+ref" "-unread")
  "List of tags to mark as a reference."
  :type '(repeat string)
  :group 'prot-notmuch)

;;;; Utilities

(defface prot-notmuch-encrypted-tag
  '((default :inherit italic)
    (((class color) (min-colors 88) (background light))
     :foreground "#5d3026")
    (((class color) (min-colors 88) (background dark))
     :foreground "#f8dec0"))
  "Face for the 'encrypted' tag or related in Notmuch.
Refer to the variable `notmuch-tag-formats' for how to assign
those.")

(defface prot-notmuch-sent-tag
  '((default :inherit italic)
    (((class color) (min-colors 88) (background light))
     :foreground "#005e00")
    (((class color) (min-colors 88) (background dark))
     :foreground "#44bc44"))
  "Face for the 'sent' tag or related in Notmuch.
Refer to the variable `notmuch-tag-formats' for how to assign
those.")

(defface prot-notmuch-spam-tag
  '((default :inherit italic)
    (((class color) (min-colors 88) (background light))
     :foreground "#70480f")
    (((class color) (min-colors 88) (background dark))
     :foreground "#c4d030"))
  "Face for the 'spam' tag or related in Notmuch.
Refer to the variable `notmuch-tag-formats' for how to assign
those.")

(defface prot-notmuch-ref-tag
  '((default :inherit italic)
    (((class color) (min-colors 88) (background light))
     :foreground "#005a5f")
    (((class color) (min-colors 88) (background dark))
     :foreground "#6ae4b9"))
  "Face for the 'ref' tag or related in Notmuch.
Refer to the variable `notmuch-tag-formats' for how to assign
those.")

(defface prot-notmuch-todo-tag
  '((default :inherit italic)
    (((class color) (min-colors 88) (background light))
     :foreground "#a60000")
    (((class color) (min-colors 88) (background dark))
     :foreground "#ff8059"))
  "Face for the 'todo' tag or related in Notmuch.
Refer to the variable `notmuch-tag-formats' for how to assign
those.")

(defface prot-notmuch-widget-field
  '((((class color) (min-colors 88) (background light))
     :underline "#d7d7d7")
    (((class color) (min-colors 88) (background dark))
     :underline "#323232")
    (t :underline t))
  "Face for search fields in the Notmuch hello buffer.")

(declare-function message-fetch-field "message")
(declare-function message-remove-header "message")
(declare-function message-add-header "message")
(declare-function message-sort-headers "message")
(declare-function notmuch-fcc-header-setup "notmuch")

;; NOTE 2021-05-18: I used to have something like this when I was using
;; Gnus and thought it would be useful here, but it ultimately isn't.  I
;; just use `notmuch-mua-new-mail'.

;; ;;;###autoload
;; (defun prot-notmuch-message-headers ()
;;   "While `notmuch' is running, configure From header.
;; Add this function to `message-header-setup-hook'."
;;   (when (and (eq mail-user-agent 'notmuch-user-agent)
;;              (eq last-command 'compose-mail))
;;     (when (message-fetch-field "From")
;;       (message-remove-header "From"))
;;     (message-add-header (format "From: %s <%s>" user-full-name user-mail-address))
;;     (notmuch-fcc-header-setup)
;;     (message-sort-headers)))

(defun prot-notmuch-widget-field-face-remap ()
  "Set up extra highlighting for widget fields in Notmuch hello.
Add this to `notmuch-hello-mode-hook'."
  (when (derived-mode-p 'notmuch-hello-mode)
    (face-remap-add-relative 'widget-field 'prot-notmuch-widget-field)))

(defvar notmuch-saved-searches)
(defvar notmuch-show-empty-saved-searches)
(defvar notmuch-search-oldest-first)
(defvar notmuch-saved-search-sort-function)

(declare-function notmuch-hello-query-counts "notmuch")
(declare-function notmuch-saved-search-sort-function "notmuch")
(declare-function notmuch-hello-nice-number "notmuch")
(declare-function notmuch-hello-reflect "notmuch")
(declare-function notmuch-hello-widget-search "notmuch")

;; Simplified variant of what is available in `notmuch-hello.el'.
(defun prot-notmuch-hello-insert-saved-searches ()
  "Single column saved search buttons for Notmuch hello.
Add this to `notmuch-hello-sections'."
  (let ((searches (notmuch-hello-query-counts
		           (if notmuch-saved-search-sort-function
		               (funcall notmuch-saved-search-sort-function
				                notmuch-saved-searches)
		             notmuch-saved-searches)
		           :show-empty-searches notmuch-show-empty-saved-searches))
        (count 0))
    (mapc (lambda (elem)
	        (when elem
	          (let* ((name (plist-get elem :name))
		             (query (plist-get elem :query))
		             (oldest-first (cl-case (plist-get elem :sort-order)
				                     (newest-first nil)
				                     (oldest-first t)
				                     (otherwise notmuch-search-oldest-first)))
		             (search-type (plist-get elem :search-type))
		             (msg-count (plist-get elem :count)))
		        (widget-insert (format "\n%8s "
				                       (notmuch-hello-nice-number msg-count)))
		        (widget-create 'push-button
			                   :notify #'notmuch-hello-widget-search
			                   :notmuch-search-terms query
			                   :notmuch-search-oldest-first oldest-first
			                   :notmuch-search-type search-type
			                   name)))
	        (cl-incf count))
	      (notmuch-hello-reflect searches 1))))
                                               
(defvar notmuch-hello-indent)
(defvar notmuch-search-history)
(defvar notmuch-hello-recent-searches-max)
(declare-function notmuch-hello-search "notmuch")

;; Adapted from `notmuch-hello.el'.
(define-widget 'prot-notmuch-search-item 'item
  "A widget that shows recent search queries."
  :format "%v\n"
  :value-create 'prot-notmuch-search-item-value-create)

;; Adapted from `notmuch-hello.el'.
(defun prot-notmuch-search-item-value-create (widget)
  "Specify value of search WIDGET."
  (let ((value (widget-get widget :value)))
    (widget-insert (make-string notmuch-hello-indent ?\s))
    (widget-create 'editable-field
		   :size (widget-get widget :size)
		   :parent widget
		   :action #'notmuch-hello-search
		   value)))

(defun prot-notmuch--search-width ()
  "Confirm `prot-notmuch-search-field-width' is positive integer."
  (let ((width prot-notmuch-search-field-width))
    (if (prot-common-number-integer-positive-p width)
        width
      (error "Search field width must be a positive integer"))))

;; Adapted from `notmuch-hello.el'.
(defun prot-notmuch-hello-insert-recent-searches ()
  "Insert widget with recent search terms.
Add this to `notmuch-hello-sections'."
  (when notmuch-search-history
    (widget-insert "\n\n")
    (widget-insert "Recent searches: ")
    (widget-insert "\n\n")
    (let ((width (prot-notmuch--search-width)))
      (dolist (search (seq-take notmuch-search-history
				notmuch-hello-recent-searches-max))
	(widget-create 'prot-notmuch-search-item :value search :size width)))))

;;;; Commands

(autoload 'notmuch-interactive-region "notmuch")
(autoload 'notmuch-tag-change-list "notmuch")
(autoload 'notmuch-search-next-thread "notmuch")
(autoload 'notmuch-search-tag "notmuch")

(defmacro prot-notmuch-search-tag-thread (name tags)
  "Produce NAME function parsing TAGS."
  (declare (indent defun))
  `(defun ,name (&optional untag beg end)
     ,(format
       "Mark with `%s' the currently selected thread.

Operate on each message in the currently selected thread.  With
optional BEG and END as points delimiting a region that
encompasses multiple threads, operate on all those messages
instead.

With optional prefix argument (\\[universal-argument]) as UNTAG,
reverse the application of the tags.

This function advances to the next thread when finished."
       tags)
     (interactive (cons current-prefix-arg (notmuch-interactive-region)))
     (when ,tags
       (notmuch-search-tag
        (notmuch-tag-change-list ,tags untag) beg end))
     (when (eq beg end)
       (notmuch-search-next-thread))))

(prot-notmuch-search-tag-thread
  prot-notmuch-search-complete-thread
  prot-notmuch-mark-complete-tags)

(prot-notmuch-search-tag-thread
  prot-notmuch-search-delete-thread
  prot-notmuch-mark-delete-tags)

(prot-notmuch-search-tag-thread
  prot-notmuch-search-flag-thread
  prot-notmuch-mark-flag-tags)

(prot-notmuch-search-tag-thread
  prot-notmuch-search-spam-thread
  prot-notmuch-mark-spam-tags)

(prot-notmuch-search-tag-thread
  prot-notmuch-search-todo-thread
  prot-notmuch-mark-todo-tags)

(prot-notmuch-search-tag-thread
  prot-notmuch-search-reference-thread
  prot-notmuch-mark-reference-tags)

(defmacro prot-notmuch-show-tag-message (name tags)
  "Produce NAME function parsing TAGS."
  (declare (indent defun))
  `(defun ,name (&optional untag)
     ,(format
       "Apply `%s' to message.

With optional prefix argument (\\[universal-argument]) as UNTAG,
reverse the application of the tags."
       tags)
     (interactive "P")
     (when ,tags
       (apply 'notmuch-show-tag-message
	          (notmuch-tag-change-list ,tags untag)))))

(prot-notmuch-show-tag-message
  prot-notmuch-show-complete-message
  prot-notmuch-mark-complete-tags)

(prot-notmuch-show-tag-message
  prot-notmuch-show-delete-message
  prot-notmuch-mark-delete-tags)

(prot-notmuch-show-tag-message
  prot-notmuch-show-flag-message
  prot-notmuch-mark-flag-tags)

(prot-notmuch-show-tag-message
  prot-notmuch-show-spam-message
  prot-notmuch-mark-spam-tags)

(prot-notmuch-show-tag-message
  prot-notmuch-show-todo-message
  prot-notmuch-mark-todo-tags)

(prot-notmuch-show-tag-message
  prot-notmuch-show-reference-message
  prot-notmuch-mark-reference-tags)

(autoload 'notmuch-refresh-this-buffer "notmuch")
(autoload 'notmuch-refresh-all-buffers "notmuch")

(defun prot-notmuch-refresh-buffer (&optional arg)
  "Run `notmuch-refresh-this-buffer'.
With optional prefix ARG (\\[universal-argument]) call
`notmuch-refresh-all-buffers'."
  (interactive "P")
  (if arg
      (notmuch-refresh-all-buffers)
    (notmuch-refresh-this-buffer)))

;;;###autoload
(defun prot-notmuch-delete-mail ()
  "Permanently delete mail marked as `prot-notmuch-delete-mail'.
Prompt for confirmation before carrying out the operation.

Do not attempt to refresh the index.  This will be done upon the
next invocation of 'notmuch new'."
  (interactive)
  (let* ((del-tag prot-notmuch-delete-tag)
         (count
          (string-to-number
           (with-temp-buffer
             (shell-command
              (format "notmuch count tag:%s" prot-notmuch-delete-tag) t)
             (buffer-substring-no-properties (point-min) (1- (point-max))))))
         (mail (if (> count 1) "mails" "mail")))
    (unless (> count 0)
      (user-error "No mail marked as `%s'" del-tag))
    (when (yes-or-no-p
           (format "Delete %d %s marked as `%s'?" count mail del-tag))
      (shell-command
       (format "notmuch search --output=files --format=text0 tag:%s | xargs -r0 rm" del-tag)
       t))))

;;;; Mode line unread indicator

;; NOTE 2021-05-14: I have an alternative to this in prot-mail.el which
;; does not rely on notmuch as it uses find instead.  The following
;; approach is specific to my setup and is what I prefer now.

(defcustom prot-notmuch-mode-line-count-args "tag:unread and tag:inbox"
  "Arguments to pass to 'notmuch count' for counting new mail."
  :type 'string
  :group 'prot-notmuch)

(defcustom prot-notmuch-mode-line-indicator-commands '(notmuch-refresh-this-buffer)
  "List of commands that will be advised to update the mode line.
The advice is designed to run a hook which is used internally by
the function `prot-notmuch-mail-indicator'."
  :type 'list
  :group 'prot-notmuch)

(defface prot-notmuch-mail-count
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#61284f")
    (((class color) (min-colors 88) (background dark))
     :foreground "#fbd6f4")
    (t :foreground "magenta"))
  "Face for mode line indicator that shows a new mail count.")

(defvar prot-notmuch-new-mail-string nil
  "New maildir count number for the mode line.")

(defun prot-notmuch--new-mail ()
  "Search for new mail in personal maildir paths."
  (with-temp-buffer
    (shell-command
     (format "notmuch count %s" prot-notmuch-mode-line-count-args) t)
    (buffer-substring-no-properties (point-min) (1- (point-max)))))

(defun prot-notmuch--mode-string (count)
  "Add properties to COUNT string."
  (when (not (string= count "0"))
    (propertize (format "@%s " count)
                'face 'prot-notmuch-mail-count
                'help-echo "New mails matching `prot-notmuch-mode-line-count-args'")))

(defvar prot-notmuch--mode-line-mail-indicator nil
  "Internal variable used to store the state of new mails.")

(defun prot-notmuch--mode-line-mail-indicator ()
  "Prepare new mail count mode line indicator."
  (let* ((count (prot-notmuch--new-mail))
         (indicator (prot-notmuch--mode-string count))
         (old-indicator prot-notmuch--mode-line-mail-indicator))
    (when old-indicator
      (setq global-mode-string (delete old-indicator global-mode-string)))
    (cond
     ((>= (string-to-number count) 1)
      (setq global-mode-string (push indicator global-mode-string))
      (setq prot-notmuch--mode-line-mail-indicator indicator))
     (t
      (setq prot-notmuch--mode-line-mail-indicator nil)))))

(defvar prot-notmuch--mode-line-mail-sync-hook nil
  "Hook to refresh the mode line for the mail indicator.")

(defun prot-notmuch--add-hook (&rest _)
  "Run `prot-notmuch--mode-line-mail-sync-hook'.
Meant to be used as advice after specified commands that should
update the mode line indicator with the new mail count."
  (run-hooks 'prot-notmuch--mode-line-mail-sync-hook))

;;;###autoload
(define-minor-mode prot-notmuch-mail-indicator
  "Enable mode line indicator with counter for new mail."
  :init-value nil
  :global t
  (if prot-notmuch-mail-indicator
      (progn
        (run-at-time t 60 #'prot-notmuch--mode-line-mail-indicator)
        (when prot-notmuch-mode-line-indicator-commands
          (dolist (fn prot-notmuch-mode-line-indicator-commands)
            (advice-add fn :after #'prot-notmuch--add-hook)))
        (add-hook 'prot-notmuch--mode-line-mail-sync-hook #'prot-notmuch--mode-line-mail-indicator)
        (force-mode-line-update t))
    (cancel-function-timers #'prot-notmuch--mode-line-mail-indicator)
    (setq global-mode-string (delete prot-notmuch--mode-line-mail-indicator global-mode-string))
    (remove-hook 'prot-notmuch--mode-line-mail-sync-hook #'prot-notmuch--mode-line-mail-indicator)
    (when prot-notmuch-mode-line-indicator-commands
      (dolist (fn prot-notmuch-mode-line-indicator-commands)
        (advice-remove fn #'prot-notmuch--add-hook)))
    (force-mode-line-update t)))

(provide 'prot-notmuch)
;;; prot-notmuch.el ends here
5.9.2.1. .notmuch-config

What follows is my ~/.notmuch-config. The tags=new configuration is what enables the use of initial tagging, as explained in the upstream documentation (basically you want to tag some mail right after a notmuch new such as, for example, to remove mailist list messages from the inbox—see notmuch post-new rules).

The Emacs configs: Notmuch (mail indexer and mail user agent (MUA)).

# .notmuch-config - Configuration file for the notmuch mail system
#
# For more information about notmuch, see https://notmuchmail.org

# Database configuration
#
# The only value supported here is 'path' which should be the top-level
# directory where your mail currently exists and to where mail will be
# delivered in the future. Files should be individual email messages.
# Notmuch will store its database within a sub-directory of the path
# configured here named ".notmuch".
#

[database]
path=/home/prot/.mail

# User configuration
#
# Here is where you can let notmuch know how you would like to be
# addressed. Valid settings are
#
#	name		Your full name.
#	primary_email	Your primary email address.
#	other_email	A list (separated by ';') of other email addresses
#			at which you receive email.
#
# Notmuch will use the various email addresses configured here when
# formatting replies. It will avoid including your own addresses in the
# recipient list of replies, and will set the From address based on the
# address to which the original email was addressed.
#

[user]
name=Protesilaos Stavrou
primary_email=public@protesilaos.com
other_email=info@protesilaos.com

# Configuration for "notmuch new"
#
# The following options are supported here:
#
#	tags	A list (separated by ';') of the tags that will be
#		added to all messages incorporated by "notmuch new".
#
#	ignore	A list (separated by ';') of file and directory names
#		that will not be searched for messages by "notmuch new".
#
#		NOTE: *Every* file/directory that goes by one of those
#		names will be ignored, independent of its depth/location
#		in the mail store.
#

[new]
tags=new
ignore=

# Search configuration
#
# The following option is supported here:
#
#	exclude_tags
#		A ;-separated list of tags that will be excluded from
#		search results by default.  Using an excluded tag in a
#		query will override that exclusion.
#

[search]
exclude_tags=del;spam;

# Maildir compatibility configuration
#
# The following option is supported here:
#
#	synchronize_flags      Valid values are true and false.
#
#	If true, then the following maildir flags (in message filenames)
#	will be synchronized with the corresponding notmuch tags:
#
#		Flag	Tag
#		----	-------
#		D	draft
#		F	flagged
#		P	passed
#		R	replied
#		S	unread (added when 'S' flag is not present)
#
#	The "notmuch new" command will notice flag changes in filenames
#	and update tags, while the "notmuch tag" and "notmuch restore"
#	commands will notice tag changes and update flags in filenames
#

[maildir]
synchronize_flags=true
5.9.2.2. notmuch post-new rules

And here are the rules that apply after running notmuch new. Its file system path is $maildir/.notmuch/hooks/post-new where $maildir is the database path specified in .notmuch-config (see .notmuch-config). Remember that this file needs to be an executable.

The Emacs configs: Notmuch (mail indexer and mail user agent (MUA)).

#!/bin/sh

# post-new --- Notmuch rules that run after notmuch new
#
# Copyright (c) 2021  Protesilaos Stavrou <info@protesilaos.com>
#
# This program 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 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 <http://www.gnu.org/licenses/>.
#
## Commentary:
#
# The order of those commands matters.  Maybe I will write something
# more sophisticated in the future.
#
# Part of my dotfiles: <https://gitlab.com/protesilaos/dotfiles>.


# remove "unread" from "replied"
notmuch tag -unread -new -- tag:replied

# tag all "new" messages "inbox" and "unread"
notmuch tag +inbox +unread -new -- tag:new

# tag my replies as "sent"
notmuch tag -new -unread +inbox +sent -- '(from:"*@protesilaos.com*" not to:"*@protesilaos.com*" not tag:list)'

# mailing lists
## emacs
notmuch tag -inbox +list +emacs -- from:emacs-devel@gnu.org or to:emacs-devel@gnu.org
notmuch tag -inbox +list +emacs -- from:emacs-orgmode@gnu.org or to:emacs-orgmode@gnu.org
notmuch tag -inbox +list +emacs -- 'to:"/*@debbugs.gnu.org*/"'
notmuch tag -inbox +list +emacs -- from:emacs-humanities@gnu.org or to:emacs-humanities@gnu.org not to:emacs-humanities-owner@gnu.org
notmuch tag -inbox +list +emacs -- 'to:"/*~michel-slm/elpher@lists.sr.ht*/"'

## unix (CLI tools)
notmuch tag -inbox +list +cli -- from:notmuch@notmuchmail.org or to:notmuch@notmuchmail.org

# include mailing lists sent to me into the "inbox"
notmuch tag +inbox -- '(to:"*@protesilaos.com*" tag:list)'
5.9.2.3. Org+Notmuch integration (ol-notmuch.el)

The ol-notmuch.el is part of the org-contrib package which can be found in the NonGNU ELPA (that archive is configured out-of-the-box for Emacs28, same as with GNU ELPA). However, that package contains approximately one zillion things I do not need, so I prefer to copy the file here and handle it on its own.

;;; ol-notmuch.el --- Links to notmuch messages

;; Copyright (C) 2010-2014  Matthieu Lemerre

;; Author: Matthieu Lemerre <racin@free.fr>
;; Keywords: outlines, hypermedia, calendar, wp
;; Homepage: https://orgmode.org

;; This file is not part of GNU Emacs.

;; 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 2, 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 GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; This file implements links to notmuch messages and "searches". A
;; search is a query to be performed by notmuch; it is the equivalent
;; to folders in other mail clients. Similarly, mails are referred to
;; by a query, so both a link can refer to several mails.

;; Links have one the following form
;; notmuch:<search terms>
;; notmuch-search:<search terms>.

;; The first form open the queries in notmuch-show mode, whereas the
;; second link open it in notmuch-search mode. Note that queries are
;; performed at the time the link is opened, and the result may be
;; different from when the link was stored.

;;; Code:

(require 'ol)

;; customisable notmuch open functions
(defcustom org-notmuch-open-function
  'org-notmuch-follow-link
  "Function used to follow notmuch links.

Should accept a notmuch search string as the sole argument."
  :group 'org-notmuch
  :version "24.4"
  :package-version '(Org . "8.0")
  :type 'function)

(defcustom org-notmuch-search-open-function
  'org-notmuch-search-follow-link
  "Function used to follow notmuch-search links.
Should accept a notmuch search string as the sole argument."
  :group 'org-notmuch
  :version "24.4"
  :package-version '(Org . "8.0")
  :type 'function)

(make-obsolete-variable 'org-notmuch-search-open-function nil "9.3")



;; Install the link type
(org-link-set-parameters "notmuch"
			 :follow #'org-notmuch-open
			 :store #'org-notmuch-store-link)

(defun org-notmuch-store-link ()
  "Store a link to a notmuch search or message."
  (when (memq major-mode '(notmuch-show-mode notmuch-tree-mode))
    (let* ((message-id (notmuch-show-get-message-id t))
	   (subject (notmuch-show-get-subject))
	   (to (notmuch-show-get-to))
	   (from (notmuch-show-get-from))
	   (date (org-trim (notmuch-show-get-date)))
	   desc link)
      (org-link-store-props :type "notmuch" :from from :to to :date date
       			    :subject subject :message-id message-id)
      (setq desc (org-link-email-description))
      (setq link (concat "notmuch:id:" message-id))
      (org-link-add-props :link link :description desc)
      link)))

(defun org-notmuch-open (path _)
  "Follow a notmuch message link specified by PATH."
  (funcall org-notmuch-open-function path))

(defun org-notmuch-follow-link (search)
  "Follow a notmuch link to SEARCH.

Can link to more than one message, if so all matching messages are shown."
  (require 'notmuch)
  (notmuch-show search))



(org-link-set-parameters "notmuch-search"
			 :follow #'org-notmuch-search-open
			 :store #'org-notmuch-search-store-link)

(defun org-notmuch-search-store-link ()
  "Store a link to a notmuch search or message."
  (when (eq major-mode 'notmuch-search-mode)
    (let ((link (concat "notmuch-search:" notmuch-search-query-string))
	  (desc (concat "Notmuch search: " notmuch-search-query-string)))
      (org-link-store-props :type "notmuch-search"
			    :link link
			    :description desc)
      link)))

(defun org-notmuch-search-open (path _)
  "Follow a notmuch message link specified by PATH."
  (message "%s" path)
  (org-notmuch-search-follow-link path))

(defun org-notmuch-search-follow-link (search)
  "Follow a notmuch link by displaying SEARCH in notmuch-search mode."
  (require 'notmuch)
  (notmuch-search search))



(org-link-set-parameters "notmuch-tree"
			 :follow #'org-notmuch-tree-open
			 :store #'org-notmuch-tree-store-link)

(defun org-notmuch-tree-store-link ()
  "Store a link to a notmuch search or message."
  (when (eq major-mode 'notmuch-tree-mode)
    (let ((link (concat "notmuch-tree:" (notmuch-tree-get-query)))
	  (desc (concat "Notmuch tree: " (notmuch-tree-get-query))))
      (org-link-store-props :type "notmuch-tree"
			    :link link
			    :description desc)
      link)))

(defun org-notmuch-tree-open (path _)
  "Follow a notmuch message link specified by PATH."
  (message "%s" path)
  (org-notmuch-tree-follow-link path))

(defun org-notmuch-tree-follow-link (search)
  "Follow a notmuch link by displaying SEARCH in notmuch-tree mode."
  (require 'notmuch)
  (notmuch-tree search))

(provide 'ol-notmuch)

;;; ol-notmuch.el ends here

And now make the thing work:

(require 'ol-notmuch)

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

;;; Sending email (SMTP)
(prot-emacs-builtin-package 'smtpmail
  (setq smtpmail-default-smtp-server "mail.gandi.net")
  (setq smtpmail-smtp-server "mail.gandi.net")
  (setq smtpmail-stream-type 'ssl)
  (setq smtpmail-smtp-service 465)
  (setq smtpmail-queue-mail nil))

(prot-emacs-builtin-package 'sendmail
  (setq send-mail-function 'smtpmail-send-it))

5.9.4. EBDB (mail contacts)

EBDB is a contacts' manager by Eric Abrahamsen that integrates with Mail User Agents inside of Emacs. I used to use it as part of my Gnus setup but as of 2021-05-13 my plan is to make it work with Notmuch as I have switched to it (Notmuch (mail indexer and mail user agent (MUA))).

What I need from it is to perform the task of storing every address that I exchange messages with. When I send someone a message, their email should be saved automatically. While if I merely read an item, say, on the emacs-devel mailing list, I prefer to only update existing records else do nothing.

One could set everything to automatically update in all contexts, though I find that such an approach will either create too many false positives that will distract you from the immediate task of handling your correspondence, or they will simply contribute to the creation of a database that contains a lot of largely useless information. Save only what matters—ignore the rest.

While "ignore the rest" can mean to simply pass it over in silence, EBBD actually provides a mechanism to permanently exclude individual addresses or domain names from ever being recorded. Those are stored in the file specified by the variable ebdb-permanent-ignores-file: you can edit it manually, adding one address per line with no attached name or angled brackets, or a domain name that starts with the @ sign. No regexp notation is accepted. For example:

@debbugs.gnu.org
help-debbugs@gnu.org
@reply.github.com

EBDB binds some common commands to the semicolon (;) prefix key inside Gnus' summary and article buffers. For example, ; : will update all records that are found in the message at point, while ; s will scan (called "snarf") the body of the message for names and email addresses in an attempt to create records of any findings. As always, append C-h to the prefix key to get help about all key bindings that complete the sequence and what commands they call.

The ebdb-mua-pop-up controls whether a window with relevant contact information should be displayed automatically. Perhaps it is good to have some extra feedback on what we know or what has been collected thus far, though I prefer not to see anything by default (it can still be displayed with the various commands under the ; prefix key). Its informative nature aside, this window can be used to further edit entries. With point over a field, type e to edit it or C-k to remove it (I bind D to delete). The latter command behaves differently when the point is before the record's main field, typically the name, where it will prompt to delete the whole entry altogether. For more about that specific major mode and its associated buffers, use C-h m (which invokes describe-mode).

To view all of your contacts, or just those matching a pattern (or string), use M-x ebdb, which will prompt for a search. Input an empty query if you prefer to view everything in the database. While in the *EBDB* buffer, you gain access to commands for operating on the records. Same principle as with the aforementioned ebdb-mua-pop-up: c to create a new entry, e to edit the field at point, i to insert a new datum to the current record, C-k (or my preferred D) to delete… Again, C-h m is your friend.

Apart from gathering data and operating on it, EBDB can auto-complete email addresses in the message composition buffer: hit TAB in a "To:", "Cc:" or equivalent header and then use the completion framework's interaction model to retrieve what you want.

Finally, note that this package is fairly comprehensive as it defines lots of options and commands: make sure to read its official manual.

;;; EBDB (mail contacts)
(prot-emacs-elpa-package 'ebdb
  (require 'ebdb-message)
  (require 'ebdb-notmuch) ; FIXME 2021-05-13: does not activate the corfu-mode UI
  (setq ebdb-sources (locate-user-emacs-file "ebdb"))
  (setq ebdb-permanent-ignores-file (locate-user-emacs-file "ebdb-permanent-ignores"))

  (setq ebdb-mua-pop-up nil)
  (setq ebdb-default-window-size 0.25)
  (setq ebdb-mua-default-formatter ebdb-default-multiline-formatter)

  (setq ebdb-mua-auto-update-p 'existing)
  (setq ebdb-mua-reader-update-p 'existing)
  (setq ebdb-mua-sender-update-p 'create)
  (setq ebdb-message-auto-update-p 'create)

  (setq ebdb-message-try-all-headers t)
  (setq ebdb-message-headers
        '((sender "From" "Resent-From" "Reply-To" "Sender")
          (recipients "Resent-To" "Resent-Cc" "Resent-CC" "To" "Cc" "CC" "Bcc" "BCC")))
  (setq ebdb-message-all-addresses t)

  (setq ebdb-complete-mail 'capf)
  (setq ebdb-mail-avoid-redundancy t)
  (setq ebdb-completion-display-record nil)
  (setq ebdb-complete-mail-allow-cycling nil)

  (setq ebdb-record-self "ace719a4-61f8-4bee-a1ca-2f07e2292305")
  (setq ebdb-user-name-address-re 'self) ; match the above
  (setq ebdb-save-on-exit t)

  (with-eval-after-load 'prot-mail ; check my `prot-mail.el'
    (add-hook 'message-setup-hook #'prot-mail-ebdb-message-setup))

  (let ((map ebdb-mode-map))
    (define-key map (kbd "D") #'ebdb-delete-field-or-record)
    (define-key map (kbd "M") #'ebdb-mail) ; disables `ebdb-mail-each'
    (define-key map (kbd "m") #'ebdb-toggle-record-mark)
    (define-key map (kbd "t") #'ebdb-toggle-all-record-marks)
    (define-key map (kbd "T") #'ebdb-toggle-records-format) ; disables `ebdb-toggle-all-records-format'
    (define-key map (kbd "U") #'ebdb-unmark-all-records)))

5.10. Rcirc (IRC client)

The first time I used IRC in earnest was at EmacsConf 2021. The event lasted two days (I also delivered a presentation). For the first day I tried the erc package that is built into Emacs, while for the second I opted for rcirc, another built-in option.

Overall, Rcirc feels more straightforward to me as a regular user. Whereas ERC seems to have too many options and a larger code base for technicalities I do not need to know about for the purposes of my casual usage of IRC.

To get started, run M-x irc and confirm your choice at the prompts. The defaults for the server and the connection port should be fine. In my case, those steps are skipped because of the values specified in the rcirc-server-alist. Note the presence of prot-mail-auth-get-field. This is a function that queries my ~/.authinfo.gpg file to retrieve encrypted information stored there (see Sample of authinfo.gpg). I use this method to (i) automate the process of logging in while (ii) not divulging sensitive data (also see the mega-section on Email settings).

IRC is fairly easy to use: you just type your message at the command prompt and everyone in the channel can see it. Some things I learnt:

  • Join a channel like #emacs by typing at the prompt /join #emacs.
  • Leave a channel with /leave #vim 😉.
  • To quit IRC do /quit, or optionally /quit Going for a brisk walk.
  • You can mention another user in the channel just by including their nick in your message, like Hello protesilaos. When replying to someone, do it like this protesilaos: My reply here.
  • Start a private channel with, say, protesilaos by running the command /msg protesilaos My message here. Then use the channel like any other.

There probably are more commands and other tricks we can employ. I expect to familiarise myself with them over time.

The following configuration is straightforward and the doc string of every variable is clear on what it is supposed to do.

(prot-emacs-builtin-package 'rcirc
  (setq rcirc-server-alist
        `(("irc.libera.chat"
           :channels ("#emacs" "#org-mode" "#rcirc")
           :port 6697 :encryption tls
           :password ,(prot-mail-auth-get-field "libera" :secret))))

  (setq rcirc-prompt "%t> ") ; Read the docs or use (customize-set-variable 'rcirc-prompt "%t> ")

  (setq rcirc-default-nick "protesilaos"
        rcirc-default-user-name rcirc-default-nick
        rcirc-default-full-name "Protesilaos Stavrou")

  ;; ;; NOTE 2021-11-28: demo from the days of EmacsConf 2021.  I don't
  ;; ;; actually need this.
  ;; (setq rcirc-bright-nicks '("bandali" "sachac" "zaeph"))

  ;; NOTE 2021-11-28: Is there a canonical way to disable this?
  (setq rcirc-timeout-seconds most-positive-fixnum)

  (rcirc-track-minor-mode 1)

  (define-key global-map (kbd "C-c i") #'irc))

5.11. Bongo music or media manager (and prot-bongo.el)

Bongo is a buffer oriented media manager. It provides an interface to external players, such as VLC or MPV. Those are known as "back-ends" (prot-bongo-enabled-backends sets up my preferred ones). A "library" buffer contains the media collection, which consists of music or video files, or even links to online streams. While a "playlist" buffer holds the items that wait in the queue to be played by the back-end program. We normally use the Library to browse our multimedia collection and to pick the items we wish to add to the playlist. In my case, the Library is just a Dired buffer, so I also benefit from everything this powerful tool provides (for configurations and commentary, check Dired (directory editor, file manager)).

I mostly use Bongo for my local Music collection, but also as an interface to the various video or audio streams I access via their RSS feed (the latter is done through the integration with Elfeed—as such, consult Elfeed feed reader, prot-elfeed.el and prot-elfeed-bongo.el).

My music directories inside ~/Music are, in principle, organised in file system paths that follow the pattern Artist/Album/Tracks. Each track inside of them 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, do not rely on it to either play back or organise my files. The directory structure and its concomitant naming conventions are sufficient and, in my opinion, more precise and easier to predict. My methodology aside, Bongo will read the media's file name and interpret each hyphen as a field delimiter that it can then use to better present the information in the playlist queue.

I prefer this manual approach to organising my music collection over all metadata-centric alternatives. Reading metadata requires more than just looking at a plain text name: it is opaque or at least not immediately obvious. Besides, how many times have you bought an album that has one track with a guest author and that one track gets auto-filtered in some random place because of its unique meta datum for the "artist" field? So you need to supply a track "artist", then an "album artist" to avoid the pains of unpredictability… This is the kind of busy work I want to eschew by organising my files in a way that I understand intuitively. As for whether Bongo can handle metadata, I do not know.

My way of 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. As already noted in the introduction, this practically means that I can access it with dired, while still benefiting from the Bongo-specific extensions (the technical implementation is handled by my comprehensive prot-bongo.el library, which is produced after the following package configurations).

Two main uses of the Dired+Bongo combination:

Enqueue items for immediate playback
Mark some directories or files the way you always do with Dired, and hit a key to insert them to the Bongo playlist (the command is prot-bongo-dired-insert). This will do a number of things, specifically, (i) enqueue those Dired marks to the playlist buffer, (ii) perform playback 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 playing. When there are no marked items in the Dired buffer, the file/directory at point will be used instead.
Add items to plain playlist files
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. This is done using minibuffer completion (check prot-bongo-dired-make-playlist-file). Those files can at any time be inserted in a Bongo playlist buffer to start playing their contents, following the same conditional patterns of behaviour described in the previous point (see, in particular, prot-bongo-playlist-insert-playlist-file). Put simply, you have a "rock" playlist file that includes file paths to "~/Music/Scorpions" and "~/Music/Queen", so inserting that file plays all songs that are found in those two directories (files are found recursively, so don't worry if you have multiple albums inside each directory).

Now a few words about my custom delimiters that the likes of prot-bongo-dired-insert will add to the Bongo playlist buffer:

  • The "section delimiters" (prot-bongo-playlist-section-delimiter) demarcate sets of inserted media. For example, if you mark three items in Dired and proceed to enqueue them, then the section delimiter goes after those three. Such delimiters provide visual feedback, but can also be used to either navigate between them and/or remove all of their contents in one go (with prot-bongo-playlist-kill-section).
  • Then there is my concept of "headings" that complements those section delimiters (prot-bongo-playlist-heading-delimiter). Headings are comments in the Bongo playlist buffer that contain the name of the directory or file that includes the tracks directly below them. Other than being informative, they function as anchors for navigation (e.g. with prot-bongo-playlist-heading-next), while they double as pointers in an M-x imenu index (by virtue of prot-bongo-imenu-setup). So we can use key bindings to go to the next or previous heading or employ minibuffer completion to jump directly to the heading of interest. The beauty of this is that we can then use the built-in Imenu, or some of its third-party extensions, to navigate to a heading using minibuffer completion. For more on the latter, refer to the mega-section on Completion framework and extras. It covers everything about the minibuffer, Consult, Embark, and more.

For the video demo of some of the aforementioned, you may want to watch the recording on Bongo media manager and my extras (2020-08-06). Though note that it showcases code that is considerably older than what I currently have with prot-bongo.el (as of 2021-01-18).

By default, all the Bongo buffers have a prominent header that provides some basic information about the program. As I have no use for that, I run the function prot-bongo-remove-headers: it takes care of clearing the buffers while setting them up. The idea for this is derived from the Emacs configuration file of Nicolas De Jaeghere.

Finally, note that I combine Bongo with Elfeed to keep track of video or audio streams that I follow. The code, shared as prot-elfeed-bongo.el, is included in the section on Elfeed (RSS/Atom feed reader). Thanks to Madhavan Krishnan who helped me flesh out this project by sharing code and ideas in a private exchange (disclosed with permission).

Also watch: Manage podcasts in Emacs with Elfeed and Bongo (2020-09-11), though please bear in my that my current code is not exactly what was demonstrated back then (as of 2021-01-18).

;;; Bongo music or media manager (and prot-bongo.el)
(prot-emacs-elpa-package 'bongo
  (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 nil) ; t slows down the playlist buffer
  (setq bongo-join-inserted-tracks nil)
  (setq bongo-field-separator (propertize " · " 'face 'shadow))
  (setq bongo-mark-played-tracks t)
  (setq bongo-vlc-program-name "cvlc")
  (bongo-mode-line-indicator-mode -1)
  (bongo-header-line-mode -1)
  (let ((map global-map))
    (define-key map (kbd "C-c b") #'bongo)
    (define-key map (kbd "<C-XF86AudioPlay>") #'bongo-pause/resume)
    (define-key map (kbd "<C-XF86AudioNext>") #'bongo-next)
    (define-key map (kbd "<C-XF86AudioPrev>") #'bongo-previous)
    (define-key map (kbd "<C-M-XF86AudioPlay>") #'bongo-play-random)
    (define-key map (kbd "<M-XF86AudioPlay>") #'bongo-show)
    (define-key map (kbd "<S-XF86AudioNext>") #'bongo-seek-forward-10)
    (define-key map (kbd "<S-XF86AudioPrev>") #'bongo-seek-backward-10))
  (let ((map bongo-playlist-mode-map))
    (define-key map (kbd "n") #'bongo-next-object)
    (define-key map (kbd "p") #'bongo-previous-object)
    (define-key map (kbd "R") #'bongo-rename-line)
    (define-key map (kbd "j") #'bongo-dired-line)       ; Jump to dir of file at point
    (define-key map (kbd "J") #'dired-jump)             ; Jump to library buffer
    (define-key map (kbd "I") #'bongo-insert-special)))

(with-eval-after-load 'bongo
  (prot-emacs-builtin-package 'prot-bongo
    (setq prot-bongo-enabled-backends '(mpv vlc))
    (setq prot-bongo-playlist-section-delimiter (make-string 30 ?*))
    (setq prot-bongo-playlist-heading-delimiter "§")
    (setq prot-bongo-playlist-directory
          (concat
           (file-name-as-directory bongo-default-directory)
           (file-name-as-directory "playlists")))
    ;; Those set up a few extras: read each function's doc string.  Pass
    ;; an argument to undo their effects.
    (prot-bongo-enabled-backends)
    (prot-bongo-remove-headers)
    (prot-bongo-imenu-setup)
    (add-hook 'dired-mode-hook #'prot-bongo-dired-library-enable)
    (add-hook 'wdired-mode-hook #'prot-bongo-dired-library-disable)
    (add-hook 'prot-bongo-playlist-change-track-hook #'prot-bongo-playlist-recenter)
    (let ((map bongo-playlist-mode-map))
      (define-key map (kbd "C-c C-n") #'prot-bongo-playlist-heading-next)
      (define-key map (kbd "C-c C-p") #'prot-bongo-playlist-heading-previous)
      (define-key map (kbd "M-n") #'prot-bongo-playlist-section-next)
      (define-key map (kbd "M-p") #'prot-bongo-playlist-section-previous)
      (define-key map (kbd "M-h") #'prot-bongo-playlist-mark-section)
      (define-key map (kbd "M-d") #'prot-bongo-playlist-kill-section)
      (define-key map (kbd "g") #'prot-bongo-playlist-reset)
      (define-key map (kbd "D") #'prot-bongo-playlist-terminate)
      (define-key map (kbd "r") #'prot-bongo-playlist-random-toggle)
      (define-key map (kbd "i") #'prot-bongo-playlist-insert-playlist-file))
    (let ((map bongo-dired-library-mode-map))
      (define-key map (kbd "<C-return>") #'prot-bongo-dired-insert)
      (define-key map (kbd "C-c SPC") #'prot-bongo-dired-insert)
      (define-key map (kbd "C-c +") #'prot-bongo-dired-make-playlist-file))))

Here is my prot-bongo.el library (part of my dotfiles' repo):

;;; prot-bongo.el --- Bongo extensions for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Extensions for Bongo, intended for use in my Emacs setup:
;; https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

;; XXX Written on 2021-01-18.  Remains to be reviewed.

(eval-when-compile (require 'subr-x))
(eval-when-compile (require 'cl-lib))
(require 'bongo nil t)
(require 'prot-common)

(defgroup prot-bongo ()
  "Personal extensions for Bongo."
  :group 'bongo)

(defcustom prot-bongo-enabled-backends '(mpv vlc)
  "List of enabled backends.
See `bongo-backends' for a list of available backends."
  :type 'list
  :group 'prot-bongo)

(defcustom prot-bongo-playlist-section-delimiter (make-string 30 ?*)
  "Delimiter for inserted groups in Bongo playlist buffers.
It is recommended to set this to a few character length, as it
should be placed on its own line to demacrate groups of enqueued
media."
  :type 'string
  :group 'prot-bongo)

(defcustom prot-bongo-playlist-heading-delimiter "§"
  "Delimiter for custom headings in Bongo playlist buffers.
It is recommended to set this to a single character, as it will
be complemented with the name of the enqueued item."
  :type 'string
  :group 'prot-bongo)

(defvar bongo-default-directory)

(defcustom prot-bongo-playlist-directory
  (concat
   (file-name-as-directory bongo-default-directory)
   (file-name-as-directory "playlists"))
  "Path to playlist files.
Such files are plain text documents that contain a filesystem
path on each line which points to a multimedia item (e.g. a
directory with music files).

Make sure this is a valid path, as we will not make any attempt
at creating it or running any other kind of check."
  :type 'string
  :group 'prot-bongo)

(defcustom prot-bongo-last-inserted-file
  (locate-user-emacs-file "prot-bongo-last-inserted")
  "File to save the last insertion from Dired into the playlist."
  :type 'file
  :group 'prot-bongo)

;;;; Basic setup

(defvar bongo-enabled-backends)

;;;###autoload
(defun prot-bongo-enabled-backends (&optional negation)
  "Assign variable `prot-bongo-enabled-backends' to Bongo.
With optional NEGATION, undo this assignment."
  (if negation
      (progn
        (setq bongo-enabled-backends nil)
        (remove-hook 'bongo-mode-hook #'prot-bongo-enabled-backends))
    (setq bongo-enabled-backends prot-bongo-enabled-backends)
    (add-hook 'bongo-mode-hook #'prot-bongo-enabled-backends)))

;; The original idea for the advice setup to hide the Bongo comment
;; headers comes from the Emacs configuration of Nicolas De Jaeghere:
;; <https://github.com/Koekelas/dotfiles/blob/master/emacs.org>.

(defvar bongo-default-playlist-buffer-name)
(defvar bongo-default-library-buffer-name)
(declare-function bongo-playlist-mode "bongo")
(declare-function bongo-library-mode "bongo")

(defun prot-bongo-playlist-buffer-no-banner ()
  "Set up a Bongo playlist buffer without its header commentary.
To be advised as override for `bongo-default-playlist-buffer'.

To actually enable this, evaluate `prot-bongo-remove-headers'."
  (with-current-buffer (get-buffer-create bongo-default-playlist-buffer-name)
    (unless (derived-mode-p 'bongo-playlist-mode)
      (bongo-playlist-mode))
    (current-buffer)))

(defun prot-bongo-library-buffer-no-banner ()
  "Set up a Bongo library buffer without its header commentary.
To be advised as override for `bongo-default-library-buffer'.

To actually enable this, evaluate `prot-bongo-remove-headers'."
  (with-current-buffer (get-buffer-create bongo-default-library-buffer-name)
    (unless (derived-mode-p 'bongo-library-mode)
      (bongo-library-mode))
    (current-buffer)))

;;;###autoload
(defun prot-bongo-remove-headers (&optional negation)
  "Remove comment headers from Bongo buffers.
With optional NEGATION undo the changes."
  (if negation
      (progn
        (advice-remove 'bongo-default-playlist-buffer #'prot-bongo-playlist-buffer-no-banner)
        (advice-remove 'bongo-default-library-buffer #'prot-bongo-library-buffer-no-banner))
    (advice-add 'bongo-default-playlist-buffer :override #'prot-bongo-playlist-buffer-no-banner)
    (advice-add 'bongo-default-library-buffer :override #'prot-bongo-library-buffer-no-banner)))

;;;; Custom delimiters for headings and sections

(declare-function bongo-insert-comment-text "bongo")

(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 ()
  "Make `prot-bongo-playlist-section-delimiter' comment."
  (bongo-insert-comment-text
   (format "\n%s\n\n" prot-bongo-playlist-section-delimiter)))

;;;; Motions and actions for custom sections

;; REVIEW: 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."
  (declare (indent defun))
  `(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."
  (format "^.*%s.*$" prot-bongo-playlist-heading-delimiter)
  're-search-forward
  'point-at-eol)

(prot-bongo-playlist-motion
  prot-bongo-playlist-heading-previous
  "Move to previous `bongo' playlist custom heading."
  (format "^.*%s.*$" prot-bongo-playlist-heading-delimiter)
  're-search-backward
  'point-at-bol)

(defun prot-bongo--section-delimiter-string ()
  "Format regexp for `prot-bongo-playlist-section-delimiter'."
  (let* ((string prot-bongo-playlist-section-delimiter)
         (char (regexp-quote (substring string 0 1))))
    (format "^%s+$" char)))

(prot-bongo-playlist-motion
  prot-bongo-playlist-section-next
  "Move to next `bongo' playlist custom section delimiter."
  (prot-bongo--section-delimiter-string)
  're-search-forward
  'point-at-eol)

(prot-bongo-playlist-motion
  prot-bongo-playlist-section-previous
  "Move to previous `bongo' playlist custom section delimiter."
  (prot-bongo--section-delimiter-string)
  're-search-backward
  'point-at-bol)

;;;###autoload
(defun prot-bongo-playlist-mark-section ()
  "Mark `bongo' playlist section, delimited by custom markers.
The marker is `prot-bongo-playlist-delimiter'."
  (interactive)
  (let ((section (prot-bongo--section-delimiter-string)))
    (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)))

(declare-function bongo-kill "bongo")

;;;###autoload
(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))

;;;; Imenu setup for custom sections

(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."
  (let* ((string prot-bongo-playlist-heading-delimiter)
         (char (substring string 0 1)))
    (nth 1
         (split-string
          (buffer-substring-no-properties (point-at-bol) (point-at-eol))
          (concat char " ")))))

;;;###autoload
(defun prot-bongo-imenu-setup (&optional negation)
  "Set up `imenu' bindings for the Bongo playlist buffer.
With optional NEGATION, remove them."
  (if negation
      (progn
        (dolist (local '(imenu-prev-index-position-function
                         imenu-extract-index-name-function))
          (kill-local-variable local))
        (remove-hook 'prot-bongo-playlist-setup-hook #'prot-bongo-imenu-setup))
    (add-hook 'prot-bongo-playlist-setup-hook #'prot-bongo-imenu-setup)
    (setq-local imenu-prev-index-position-function
                'prot-bongo-playlist-heading-previous)
    (setq-local imenu-extract-index-name-function
                'prot-bongo--playlist-imenu-heading)))

;;;; Commands

(defvar bongo-player)
(declare-function with-bongo-playlist-buffer "bongo" (body))
(declare-function bongo-format-infoset "bongo" (infoset))
(declare-function bongo-player-infoset "bongo" (player))

;;;###autoload
(defun prot-bongo-show ()
  "Echo Bongo track without elapsed time format.
This is a simplified variant of `bongo-show'."
  (interactive)
  (let* ((player (with-bongo-playlist-buffer
                   (or bongo-player
                       (error "No currently playing track"))))
         (string (bongo-format-infoset
                  (bongo-player-infoset player))))
      (message "%s" string)))

(declare-function bongo-erase-buffer "bongo")
(declare-function bongo-library-buffer-p "bongo")
(declare-function bongo-play-random "bongo")
(declare-function bongo-playing-p "bongo")
(declare-function bongo-playlist-buffer "bongo")
(declare-function bongo-playlist-buffer-p "bongo")
(declare-function bongo-progressive-playback-mode "bongo")
(declare-function bongo-random-playback-mode "bongo")
(declare-function bongo-recenter "bongo")
(declare-function bongo-reset-playlist "bongo")
(declare-function bongo-stop "bongo")

;;;###autoload
(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)))))

(defvar bongo-next-action)

;;;###autoload
(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)))

;;;###autoload
(defun prot-bongo-playlist-reset ()
  "Stop playback and reset Bongo playlist.
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)))

;;;###autoload
(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)))

(defvar prot-bongo--playlist-history '()
  "Input history of `prot-bongo-playlist-insert-playlist-file'.")

(defun prot-bongo--playlist-prompt ()
  "Prompt for a file in `prot-bongo-playlist-directory'.
Helper function for `prot-bongo-playlist-insert-playlist-file'."
  (let* ((path prot-bongo-playlist-directory)
         (dotless directory-files-no-dot-files-regexp)
         (playlists (mapc
                     #'abbreviate-file-name
                     (directory-files path nil dotless))))
    (completing-read-multiple
     "Add playlist: " playlists
     #'prot-common-crm-exclude-selected-p
     t nil 'prot-bongo--playlist-history)))

(declare-function bongo-insert-playlist-contents "bongo")

;;;###autoload
(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
character that matches `crm-separator' to complete the second,
and so on.

Also see `prot-bongo-dired-make-playlist-file'."
  (interactive)
  (let ((path prot-bongo-playlist-directory))
    (unless (file-directory-p path)
      (error "'%s' is not an existing directory" path))
    (let ((choice
           (if (bongo-playlist-buffer-p (current-buffer))
               (prot-bongo--playlist-prompt)
             (user-error "Not in a `bongo' playlist buffer"))))
      (mapc (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))))

;;;; Setup for track changing

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

;;;###autoload
(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)

(declare-function bongo-dired-library-mode "bongo")
(declare-function bongo-insert-directory-tree "bongo")
(declare-function bongo-insert-file "bongo")
(declare-function bongo-library-buffer "bongo")
(autoload 'dired-get-marked-files "bongo")
(autoload 'dired-next-line "bongo")

(defmacro prot-bongo-dired-library (name doc val)
  "Create Bongo library function NAME with DOC and VAL."
  (declare (indent defun))
  `(defun ,name ()
     ,doc
     (when (string-match-p (file-truename bongo-default-directory)
                           (file-truename 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)

(prot-bongo-dired-library
  prot-bongo-dired-library-disable
  "Disable `bongo-dired-library-mode' when accessing ~/Music.
This should be added to `wdired-mode-hook'.  For more, refer to
`prot-bongo-dired-library-enable'."
  -1)

(advice-add 'wdired-finish-edit :after #'prot-bongo-dired-library-enable)

(autoload 'dired-x-guess-file-name-at-point "dired-x")

(defvar prot-bongo--dired-last-inserted nil
  "Last input of `prot-bongo-dired-insert'.")

;; FIXME 2021-08-27: Fails when file does not exist.
(defun prot-bongo--save-last-inserted-file ()
  "Save `prot-bongo--dired-last-inserted' to a file.
The file is specified by `prot-bongo-last-inserted-file'."
  (let ((state (delete-dups prot-bongo--dired-last-inserted))
        (file prot-bongo-last-inserted-file))
    (cond
     ((unless state
        (setq prot-bongo--dired-last-inserted
              (delete-dups (prot-common-read-data file)))))
     (t (when file
          (with-temp-file file
            (insert (concat ";; Auto-generated file;"
                            " don't edit -*- mode: lisp-data -*-\n"))
            (pp state (current-buffer))))))))

(defun prot-bongo--dired-insert-files (&optional last-inserted crm)
  "Add files in a `dired' buffer to the `bongo' playlist.
With optional LAST-INSERTED try to add the last list of files or
directories.

With optional CRM use `completing-read-multiple' to select paths
from the history of inserted entries."
  (prot-bongo--save-last-inserted-file)
  (let* ((data prot-bongo--dired-last-inserted)
         (media (cond
                 (crm
                  (completing-read-multiple
                   "Select from recent picks: "
                   (flatten-tree prot-bongo--dired-last-inserted)
                   nil t))
                 ((if (and data last-inserted)
                      (car data)
                    (dired-get-marked-files))))))
    (cl-pushnew media prot-bongo--dired-last-inserted)
    (with-current-buffer (bongo-playlist-buffer)
      (goto-char (point-max))
      (mapc (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)))
  (prot-bongo--save-last-inserted-file))

;;;###autoload
(defun prot-bongo-dired-insert (&optional arg)
  "Add `dired' item at point or marked ones to Bongo playlist.

The playlist buffer 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-enable').

With optional prefix ARG (\\[universal-argument]) ignore the file
at point or any marked items and just try to insert what was last
added using this command (see `prot-bongo--dired-last-inserted').
If no record exists for the last choice of this sort, then either
the marked items or the file at point will be selected instead."
  (interactive "P")
  (when (bongo-library-buffer-p)
    (unless (bongo-playlist-buffer-p)
      (bongo-playlist-buffer))
    (pcase (prefix-numeric-value arg)
      (16 (prot-bongo--dired-insert-files t t))
      (4 (prot-bongo--dired-insert-files t))
      (_ (prot-bongo--dired-insert-files)))
    (prot-bongo-playlist-play-random)))

;;;###autoload
(defun prot-bongo-dired-make-playlist-file ()
  "Add `dired' marked items to playlist file using completion.

Files are stored in `prot-bongo-playlist-directory'.  These are
meant to reference filesystem paths: one path per line.  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 Dired marked items 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 the minibuffer input.

Also see `prot-bongo-playlist-insert-playlist-file'."
  (interactive)
  (let* ((dotless directory-files-no-dot-files-regexp)
         (pldir prot-bongo-playlist-directory)
         (playlists (mapcar
                     'abbreviate-file-name
                     (directory-files pldir nil dotless)))
         (plname (completing-read "Select playlist: " playlists nil))
         (plfile (concat 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))))

(provide 'prot-bongo)
;;; prot-bongo.el ends here

5.12. Elfeed feed reader, prot-elfeed.el and prot-elfeed-bongo.el

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/codelog.xml" personal)))

I keep the actual list in a GPG-encrypted file (defined in the variable prot-elfeed-feeds-file which is part of my prot-elfeed.el library that is reproduced after the following package configurations). Emacs can transparently decrypt and read gpg-protected files, 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. of the European Union and then I have other more general ones, such as "politics" and "economics". So, in this case, the European Central Bank gets tagged with eu, economics, while the European Parliament is eu, politics.

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 "=TITLE", 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.

My only inconvenience with elfeed-search-live-filter is that it does not support completion out-of-the-box. Instead it expects full user input, which ins understandable given the types of searches it can conduct. Since I only need this facility to filter by tag, I rebind s to my prot-elfeed-search-tag-filter. While S-s (shift and s) can still be used to access the original command, whenever we need more precise control over the search filters.

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.

For an older, albeit still relevant, demonstration of what I have here, watch my Elfeed video (2020-06-09).

As I also am a user of Bongo, and because Emacs lets one handle things with precision, there are a few functions here that are meant to make the Elfeed search buffer a bongo-aware media library, from where we can enqueue online multimedia sources (video links, or podcast enclosures). The key is to not interfere with the primary Bongo playlist and library tandem, which is dedicated to my local music collection, but to maintain a separate playlist which can be controlled independently. The entirety of my prot-elfeed-bongo.el is shared after prot-elfeed.el below the following package configurations (for my other extensions, refer to Bongo music or media manager (and prot-bongo.el)).

The placement of my custom buffers for Elfeed's multimedia output is controlled by display-buffer-alist (see Window rules and basic tweaks).

I benefited in this particular Elfeed+Bongo workflow from an email exchange I had 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). Video demo with older code: Manage podcasts in Emacs with Elfeed and Bongo (2020-09-11).

Also see: Sample configuration for MPV (Elfeed+Bongo extension).

;;; Elfeed feed reader, prot-elfeed.el and prot-elfeed-bongo.el
(prot-emacs-elpa-package 'elfeed
  (setq elfeed-use-curl t)
  (setq elfeed-curl-max-connections 10)
  (setq elfeed-db-directory (concat user-emacs-directory "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 160)
  (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))

  ;; Make sure to also check the section on shr and eww for how I handle
  ;; `shr-width' there.
  (add-hook 'elfeed-show-mode-hook
            (lambda () (setq-local shr-width (current-fill-column))))

  (prot-emacs-builtin-package 'prot-elfeed-bongo)

  (define-key global-map (kbd "C-c e") #'elfeed)
  (let ((map elfeed-search-mode-map))
    (define-key map (kbd "w") #'elfeed-search-yank)
    (define-key map (kbd "g") #'elfeed-update)
    (define-key map (kbd "G") #'elfeed-search-update--force)
    (define-key map (kbd "b") #'prot-elfeed-bongo-insert-item)
    (define-key map (kbd "h") #'prot-elfeed-bongo-switch-to-playlist)) ; "hop" mnemonic
  (let ((map elfeed-show-mode-map))
    (define-key map (kbd "w") #'elfeed-show-yank)
    (define-key map (kbd "b") #'prot-elfeed-bongo-insert-item)))

(with-eval-after-load 'elfeed
  (prot-emacs-builtin-package 'prot-elfeed
    (setq prot-elfeed-tag-faces t)
    (prot-elfeed-fontify-tags)
    (add-hook 'elfeed-search-mode-hook #'prot-elfeed-load-feeds)

    ;; Use alternating backgrounds, if `stripes' is available.
    (with-eval-after-load 'stripes
      (add-hook 'elfeed-search-mode-hook #'stripes-mode)
      ;; ;; To disable `hl-line-mode':
      ;; (advice-add #'elfeed-search-mode :after #'prot-common-disable-hl-line)
      )

    (let ((map elfeed-search-mode-map))
      (define-key map (kbd "s") #'prot-elfeed-search-tag-filter)
      (define-key map (kbd "o") #'prot-elfeed-search-open-other-window)
      (define-key map (kbd "q") #'prot-elfeed-kill-buffer-close-window-dwim)
      (define-key map (kbd "v") #'prot-elfeed-mpv-dwim)
      (define-key map (kbd "+") #'prot-elfeed-toggle-tag))
    (let ((map elfeed-show-mode-map))
      (define-key map (kbd "a") #'prot-elfeed-show-archive-entry)
      (define-key map (kbd "e") #'prot-elfeed-show-eww)
      (define-key map (kbd "q") #'prot-elfeed-kill-buffer-close-window-dwim)
      (define-key map (kbd "v") #'prot-elfeed-mpv-dwim)
      (define-key map (kbd "+") #'prot-elfeed-toggle-tag))))

This is prot-elfeed.el (part of my dotfiles' repo):

;;; prot-elfeed.el --- Elfeed extensions for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Extensions for Elfeed, intended for use in my Emacs setup:
;; https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(eval-when-compile (require 'subr-x))
(require 'elfeed nil t)
(require 'url-util)
(require 'prot-common)

(defgroup prot-elfeed ()
  "Personal extensions for Elfeed."
  :group 'elfeed)

(defcustom prot-elfeed-feeds-file (concat user-emacs-directory "feeds.el.gpg")
  "Path to file with `elfeed-feeds'."
  :type 'string
  :group 'prot-elfeed)

(defcustom prot-elfeed-archives-directory "~/Documents/feeds/"
  "Path to directory for storing Elfeed entries."
  :type 'string
  :group 'prot-elfeed)

(defcustom prot-elfeed-tag-faces nil
  "Add faces for certain tags.
The tags are: critical, important, personal."
  :type 'boolean
  :group 'prot-elfeed)

(defcustom prot-elfeed-laptop-resolution-breakpoint 1366
  "Determine video resolution based on this display width.
This is used to check whether I am on the laptop or whether an
external display is attached to it.  In the latter case, a
`prot-elfeed-video-resolution-large' video resolution will be
used, else `prot-elfeed-video-resolution-small'."
  :type 'integer
  :group 'prot-elfeed)

(defcustom prot-elfeed-video-resolution-small 720
  "Set video resolution width for smaller displays."
  :type 'integer
  :group 'prot-elfeed)

(defcustom prot-elfeed-video-resolution-large 1080
  "Set video resolution width for larger displays."
  :type 'integer
  :group 'prot-elfeed)

(defcustom prot-elfeed-search-tags '(critical important personal)
  "List of user-defined tags.
Used by `prot-elfeed-toggle-tag'."
  :type 'list
  :group 'prot-elfeed)

(defface prot-elfeed-entry-critical
  '((((class color) (min-colors 88) (background light))
     :inherit elfeed-search-title-face :foreground "#a60000")
    (((class color) (min-colors 88) (background dark))
     :inherit elfeed-search-title-face :foreground "#ff8059")
    (t :foreground "red"))
  "Face for Elfeed entries tagged with 'critical'.")

(defface prot-elfeed-entry-important
  '((((class color) (min-colors 88) (background light))
     :inherit elfeed-search-title-face :foreground "#813e00")
    (((class color) (min-colors 88) (background dark))
     :inherit elfeed-search-title-face :foreground "#f0ce43")
    (t :foreground "yellow"))
  "Face for Elfeed entries tagged with 'important'.")

(defface prot-elfeed-entry-personal
    '((((class color) (min-colors 88) (background light))
     :inherit elfeed-search-title-face :foreground "#0031a9")
    (((class color) (min-colors 88) (background dark))
     :inherit elfeed-search-title-face :foreground "#2fafff")
    (t :foreground "blue"))
  "Face for Elfeed entries tagged with 'personal'.")

;;;; Utilities

;;;###autoload
(defun prot-elfeed-load-feeds ()
  "Load file containing the `elfeed-feeds' list.
Add this to `elfeed-search-mode-hook'."
  (let ((feeds prot-elfeed-feeds-file))
    (if (file-exists-p feeds)
        (load-file feeds)
      (user-error "Missing feeds' file"))))

(defvar elfeed-search-face-alist)

;;;###autoload
(defun prot-elfeed-fontify-tags ()
  "Expand Elfeed faces if `prot-elfeed-tag-faces' is non-nil."
  (if prot-elfeed-tag-faces
      (setq elfeed-search-face-alist
            '((critical prot-elfeed-entry-critical)
              (important prot-elfeed-entry-important)
              (personal prot-elfeed-entry-personal)
              (unread elfeed-search-unread-title-face)))
    (setq elfeed-search-face-alist
          '((unread elfeed-search-unread-title-face)))))

(defvar prot-elfeed--tag-hist '()
  "History of inputs for `prot-elfeed-toggle-tag'.")

(defun prot-elfeed--character-prompt (tags)
  "Helper of `prot-elfeed-toggle-tag' to read TAGS."
  (let ((def (car prot-elfeed--tag-hist)))
    (completing-read
     (format "Toggle tag [%s]: " def)
     tags nil t nil 'prot-elfeed--tag-hist def)))

(defvar elfeed-show-entry)
(declare-function elfeed-tagged-p "elfeed")
(declare-function elfeed-search-toggle-all "elfeed")
(declare-function elfeed-show-tag "elfeed")
(declare-function elfeed-show-untag "elfeed")

;;;###autoload
(defun prot-elfeed-toggle-tag (tag)
  "Toggle TAG for the current item.

When the region is active in the `elfeed-search-mode' buffer, all
entries encompassed by it are affected.  Otherwise the item at
point is the target.  For `elfeed-show-mode', the current entry
is always the target.

The list of tags is provided by `prot-elfeed-search-tags'."
  (interactive
   (list
    (intern
     (prot-elfeed--character-prompt prot-elfeed-search-tags))))
  (if (derived-mode-p 'elfeed-show-mode)
      (if (elfeed-tagged-p tag elfeed-show-entry)
          (elfeed-show-untag tag)
        (elfeed-show-tag tag))
    (elfeed-search-toggle-all tag)))

(defvar elfeed-show-truncate-long-urls)
(declare-function elfeed-entry-title "elfeed")
(declare-function elfeed-show-refresh "elfeed")

;;;###autoload
(defun prot-elfeed-show-archive-entry ()
  "Store a plain text copy of the current `elfeed' entry.

The destination is defined in `prot-elfeed-archives-directory'
and will be created if it does not exist."
  (interactive)
  (let* ((entry (if (eq major-mode 'elfeed-show-mode)
                    elfeed-show-entry
                  (elfeed-search-selected :ignore-region)))
         (title (replace-regexp-in-string " " "-" (elfeed-entry-title entry)))
         (elfeed-show-truncate-long-urls nil)
         (archives (file-name-as-directory prot-elfeed-archives-directory))
         (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))))

;;;; General commands

;; NOTE 2021-10-15: This is a prototype of a "privay redirect" feature.
;; It should eventually find its way into prot-eww.el.
(defvar elfeed-show-entry)
(declare-function elfeed-search-selected "elfeed")
(declare-function elfeed-entry-link "elfeed")

(defcustom prot-elfeed-privacy-redirect-alist
  '(("www.reddit.com" . "libredd.it")
    ("www.youtube.com" . "yewtu.be")
    ;; Some sites block the proxy sites (e.g., Instagram) if they make
    ;; too many requests. In those cases a user may want to specify
    ;; something like this:
    ("www.instagram.com" . (lambda ()
                             (let ((sequence '("bibliogram.snopyta.org"
                                               "bibliogram.org"
                                               "insta.trom.tf")))
                               (seq-elt sequence (random (length sequence)))))))
  "Alist of sites and their privacy-respecting alternatives.
Alist KEY must be string.  VALUE can either be a string or a
thunk (function with no arguments) that returns a string."
  :type 'alist
  :group 'prot-elfeed)

;;;###autoload
(defun prot-elfeed-show-eww (&optional link)
  "Browse current entry's link or optional 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)))
         (parsed-url (url-generic-parse-url link))
         (replacement (alist-get (url-host parsed-url)
                                 prot-elfeed-privacy-redirect-alist
                                 nil
                                 nil
                                 #'equal)))
    (if replacement
        (progn
          (setq replacement (if (functionp replacement)
                                (funcall replacement)
                              replacement))
          (setf (url-host parsed-url) replacement)
          (eww (url-recreate-url parsed-url)))
      (eww link))
    (add-hook 'eww-after-render-hook 'eww-readable nil t)))

(declare-function elfeed-search-untag-all-unread "elfeed")
(declare-function elfeed-search-show-entry "elfeed")

;;;###autoload
(defun prot-elfeed-search-open-other-window (&optional arg)
  "Browse `elfeed' entry in the other window.
With optional prefix ARG (\\[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)))))

(declare-function elfeed-kill-buffer "elfeed")
(declare-function elfeed-search-quit-window "elfeed")

;;;###autoload
(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))))))

(defvar elfeed-search-filter-active)
(defvar elfeed-search-filter)
(declare-function elfeed-db-get-all-tags "elfeed")
(declare-function elfeed-search-update "elfeed")
(declare-function elfeed-search-clear-filter "elfeed")

;;;###autoload
(defun prot-elfeed-search-tag-filter ()
  "Filter Elfeed search buffer by tags using completion.

Completion accepts multiple inputs, delimited by `crm-separator'.
Arbitrary input is also possible, but you may have to exit the
minibuffer with something like `exit-minibuffer'."
  (interactive)
  (unwind-protect
      (elfeed-search-clear-filter)
    (let* ((elfeed-search-filter-active :live)
           (db-tags (elfeed-db-get-all-tags))
           (plus-tags (mapcar (lambda (tag)
                                (format "+%s" tag))
                              db-tags))
           (minus-tags (mapcar (lambda (tag)
                                 (format "-%s" tag))
                               db-tags))
           (all-tags (delete-dups (append plus-tags minus-tags)))
           (tags (completing-read-multiple
                  "Apply one or more tags: "
                  all-tags #'prot-common-crm-exclude-selected-p t))
           (input (string-join `(,elfeed-search-filter ,@tags) " ")))
      (setq elfeed-search-filter input))
    (elfeed-search-update :force)))

;;;; Elfeed multimedia extras

(defvar prot-elfeed-mpv-buffer-name "*prot-elfeed-mpv-output*"
  "Name of buffer holding Elfeed MPV output.")

(defun prot-elfeed--video-resolution ()
  "Determine display resolution.
This checks `prot-elfeed-laptop-resolution-breakpoint'."
  (if (<= (display-pixel-width) prot-elfeed-laptop-resolution-breakpoint)
      prot-elfeed-video-resolution-small
    prot-elfeed-video-resolution-large))

(defun prot-elfeed--get-mpv-buffer ()
  "Prepare `prot-elfeed-mpv-buffer-name' buffer."
  (let ((buf (get-buffer prot-elfeed-mpv-buffer-name))
        (inhibit-read-only t))
    (with-current-buffer buf
      (erase-buffer))))

(declare-function elfeed-entry-enclosures "elfeed")

;;;###autoload
(defun prot-elfeed-mpv-dwim ()
  "Play entry link with the external MPV program.
When there is an audio enclosure (assumed to be a 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 (prot-elfeed--video-resolution))
         (video                       ; this assumes mpv+youtube-dl
          (format "--ytdl-format=bestvideo[height\\<=?%s]+bestaudio/best" height))
         (buf (pop-to-buffer prot-elfeed-mpv-buffer-name)))
    (prot-elfeed--get-mpv-buffer)
    (if enclosure
        (progn
          (async-shell-command (format "mpv %s %s" audio enclosure) buf)
          (message "Launching MPV for %s" enclosure))
      (async-shell-command (format "mpv %s %s" video link) buf)
      (message "Launching MPV for %s" link))))

(provide 'prot-elfeed)
;;; prot-elfeed.el ends here

And here is prot-elfeed-bongo.el (part of my dotfiles' repo):

;;; prot-elfeed-bongo.el --- Bongo+Elfeed integration for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Extensions for integrating Elfeed with Bongo, intended for use in my
;; Emacs setup: https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

;; XXX Written on 2021-01-18.  Remains to be reviewed.

(eval-when-compile (require 'subr-x))
(require 'bongo nil t)
(require 'elfeed nil t)

(defgroup prot-elfeed-bongo ()
  "Personal extensions for Bongo."
  :group 'bongo)

(defcustom prot-elfeed-bongo-playlist "*prot-elfeed-bongo-queue*"
  "Name of the Elfeed+Bongo multimedia playlist."
  :type 'string
  :group 'prot-elfeed-bongo)

(autoload 'bongo-insert-comment-text "bongo")
(autoload 'bongo-insert-uri "bongo")
(autoload 'bongo-playlist-buffer "bongo")
(autoload 'bongo-playlist-buffer-p "bongo")
(autoload 'bongo-playlist-mode "bongo")
(autoload 'bongo-progressive-playback-mode "bongo")
(autoload 'bongo-recenter "bongo")
(autoload 'elfeed-entry-enclosures "elfeed")
(autoload 'elfeed-entry-link "elfeed")
(autoload 'elfeed-entry-title "elfeed")
(autoload 'elfeed-search-selected "elfeed")
(defvar elfeed-show-entry)

;;;###autoload
(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 variable `bongo-playlist-buffer'."
  (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))
         (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 '(vlc 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 ()
  "Switch to `prot-elfeed-bongo-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'."))))

(provide 'prot-elfeed-bongo)
;;; prot-elfeed-bongo.el ends here

5.12.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"

5.13. 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: it works fine.

My prot-proced.el (reproduced after the package configurations) simply adds some extra colours to the otherwise plain buffer. Makes it easier to keep track of the information on display.

;;; Proced (process monitor, similar to `top')
(prot-emacs-builtin-package 'proced
  (setq proced-auto-update-flag t)
  (setq proced-auto-update-interval 5)
  (setq proced-descend t)
  (setq proced-filter 'user)

  (with-eval-after-load 'stripes
    (add-hook 'proced-mode-hook #'stripes-mode)))

(prot-emacs-builtin-package 'prot-proced
  (prot-proced-extra-keywords 1))

Here is prot-proced.el (from my dotfiles' repo):

;;; prot-proced.el --- Extras for proced -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Extras for `proced', intended for use in my Emacs setup:
;; <https://protesilaos.com/emacs/dotemacs/>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(defgroup prot-proced ()
  "Proced extras for my dotemacs."
  :group 'proced)

;;;; Extend `proced' faces

(defface prot-proced-user '((t :inherit shadow))
  "Face for user indicator in `proced'.")

(defface prot-proced-pid
  '((((class color) (min-colors 88) (background light))
     :foreground "#5317ac")
    (((class color) (min-colors 88) (background dark))
     :foreground "#b6a0ff"))
  "Face for PID indicator in `proced'.")

(defface prot-proced-cpu
  '((((class color) (min-colors 88) (background light))
     :foreground "#8f0075")
    (((class color) (min-colors 88) (background dark))
     :foreground "#f78fe7"))
  "Face for memory indicator in `proced'.")

(defface prot-proced-mem
  '((((class color) (min-colors 88) (background light))
     :foreground "#0031a9")
    (((class color) (min-colors 88) (background dark))
     :foreground "#2fafff"))
  "Face for CPU indicator in `proced'.")

(defface prot-proced-time-start
  '((((class color) (min-colors 88) (background light))
     :foreground "#30517f")
    (((class color) (min-colors 88) (background dark))
     :foreground "#a0bfdf"))
  "Face for start time indicator in `proced'.")

(defface prot-proced-time-duration
  '((((class color) (min-colors 88) (background light))
     :foreground "#00538b")
    (((class color) (min-colors 88) (background dark))
     :foreground "#00cdc8"))
  "Face for time indicator in `proced'.")

(defface prot-proced-process nil
  "Face for process indicator in `proced'.")

(defconst prot-proced-keywords
  `((,(concat "^\s+\\(.*?\\)\s+\\(.*?\\)\s+\\(.*?\\)\s+\\(.*?\\)\s+"
             "\\(.*?\\)\s+\\(.*?\\)\s+\\(.*\\)")
     (1 'prot-proced-user)
     (2 'prot-proced-pid)
     (3 'prot-proced-cpu)
     (4 'prot-proced-mem)
     (5 'prot-proced-time-start)
     (6 'prot-proced-time-duration)
     (7 'prot-proced-process)))
  "Extra font-lock patterns for the `proced' menu.")

;;;###autoload
(define-minor-mode prot-proced-extra-keywords
  "Apply extra font-lock rules to diff buffers."
  :init-value nil
  :global t
  (if prot-proced-extra-keywords
      (progn
        (font-lock-flush (point-min) (point-max))
        (font-lock-add-keywords nil prot-proced-keywords nil)
        (add-hook 'proced-mode-hook #'prot-proced-extra-keywords))
    (font-lock-remove-keywords nil prot-proced-keywords)
    (remove-hook 'proced-mode-hook #'prot-proced-extra-keywords)
    (font-lock-flush (point-min) (point-max))))

(provide 'prot-proced)
;;; prot-proced.el ends here

5.14. 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 using completion methods (study Completion framework and extras). I use it to quickly store a password to the kill ring.

;;; Pass interface (password-store)
(prot-emacs-elpa-package 'password-store
  (setq password-store-time-before-clipboard-restore 30)
  ;; Mnemonic is the root of the "code" word (κώδικας).  But also to add
  ;; the password to the kill-ring.  Other options are already taken.
  (define-key global-map (kbd "C-c k") #'password-store-copy))

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.

(prot-emacs-elpa-package 'pass)

5.15. Simple HTML Renderer (shr), Emacs Web Wowser (eww), Elpher, and prot-eww.el

TODO 2021-09-08: This section needs to be expanded to cover the integration with the built-in bookmark.el framework as well as the third-party elpher package (Elpher is an Internet browser for the Gopher and Gemini protocols).

The following code block encompasses libraries that power Emacs' web browsing experience.

  • browse-url determines what Emacs should do when the user follows a link to the Internet. For my purposes, I configure it to open the web page inside of Emacs, while I let the Desktop Environment's default graphical browser be declared as a secondary option. Put simply, when I click on a URL, I go to an EWW or Elpher buffer. If the page is part of the World Wide Web (https? protocol) I can opt to open it in the GUI browser via eww-browse-with-external-browser, which is bound to & in eww-mode-map. That is only ever needed for websites that do not work well in a text-centric interface. For pages that implement the Gopher or Gemini protocols, there is no need whatsoever to open them externally as they are plain text anyway.
  • shr is the "Simple HTML Renderer", which basically means to interpret the underlying code of a web page and show it in the way it is intended for the end user. Unlike graphical web browsers, shr capabilities are limited, because it does not use style sheets (CSS) and more demanding resources (javascript, embedded multimedia, etc.).
  • eww is what we use to browse the Web. It effectively is the front-end to shr, adding on top of it the layer of interactivity that is expected of a basic browser, such as to keep a history of visited pages, handle bookmarks, navigate through links, and the like. As always, remember to read the manual of EWW, such as by evaluating this form: (info "(eww) Top"). It is short and should give you an idea of what its features are.
  • prot-eww adds commands that align EWW with my expectations and make it do what I want to achieve in a primarily text-based web browsing experience. It also provides the layer of integration between EWW and Elpher, making the resulting browsing experience consistent as one follows links that are implemented in the relevant protocols.

My configurations for browse-url and shr are straightforward and are meant to keep things simple. I want EWW to behave like the rest of Emacs, which means that it should not try to introduce random colours from web pages, nor use its own fonts. Both intefere with the consistency I seek in my computing environment (and why I use Emacs to begin with).

For eww, I repurpose some of its key bindings that I never plan to use, such as the toggle for proportional fonts, while I move around a few others. Just to make things easier for me.

As for prot-eww, it includes functionality that makes EWW good enough for my day-to-day needs.

In particular:

  • It includes an unconditional internal mechanism that renames EWW buffers based on the rendered web page's title or, when that is not available, the URL. This ensures that I can maintain multiple buffers without getting lost in the noise of what would otherwise be an awkward naming scheme in the form of *eww*, *eww<2>* and so on. Now it looks like *Emacs - ArchWiki # eww*, with a possible number identifier added only for duplicate entries.
  • The variable prot-eww-visited-history records the URL of every rendered page. This is then used by prot-eww-browse-dwim (more below). To keep things in check, prot-eww-visited-history is implemented as a minibuffer history which is ultimately limited to the value of history-length (read: Minibuffer history (savehist-mode)). Remember that each command can have its own history and this is common in a lot of my extras.
  • The prot-eww-browse-dwim is my main point of entry to EWW. When I call it from a random place inside of Emacs, it just offers a prompt that asks for a URL or search terms with which to query DuckDuckGo. In addition, it reads through a history of such inputs, so that M-p, M-n can quickly give me something I tried before. Plus it supports completion (check the mega-section Completion framework and extras). When this command is called from inside an EWW buffer it retains the aforementioned functionality, but its prompt is pre-populated with the URL of the current page. This is particularly useful for editing the path directly, such as to go from https://protesilaos.com/contact to https://protesilaos.com. When called with a universal prefix argument (C-u), it opens a new EWW buffer instead of using the last one.
  • prot-eww-search-engine offers selection among the custom search commands I define, such as to query Wikipedia directly or search for a bug number in the Emacs issue tracker. Those search engine commands can be invoked on their own, while each of them has its own minibuffer history (so if you searched for "elephant" in Wikipedia, you will not get that for the Arch Wiki).
  • prot-eww-bookmark-page does what its name suggests though, unlike the built-in equivalent, it first asks for how to name the bookmarked entry before storing it in the relevant memory compartment.
  • prot-eww-visit-bookmark prompts with completion to visit an entry in EWW's bookmark list. A C-u prefix means to open it in a new EWW buffer.
  • prot-eww-visit-url-on-page traverses the entire web page to construct a list of all links and their anchor text. The list can be narrowed with completion. Selecting a item with visit its corresponding web page.
  • prot-eww-jump-to-url-on-page is similar to the above, with the major difference that it only stores buffer positions and jumps to them. This makes it ideal for generating an ad-hoc index of "points of interest" in the current buffer with the help of Embark's ability to collect a list of candidates into a bespoke buffer (make sure to check Extended minibuffer actions and more (embark.el and prot-embark.el)). Of course, it can be used on its own to jump around the web page. Upon jumping, the line will pulse momentarily to help keep a sense of context (also read: prot-pulse.el (highlight cursor position)).
  • prot-eww-find-feed inspects the source code of the current web page and produces an occur buffer with links that represent RSS or Atom feeds. This command is quite useful for retrieving the feed of a website whose designers failed to provide a user-facing link for it (which is the norm nowadays). I read feeds on a daily basis: it is the best way to keep track of updates to web pages (read section Elfeed feed reader, prot-elfeed.el and prot-elfeed-bongo.el).
  • prot-eww-readable is a more opinionated take on the built-in option. It specifies a narrower shr-width, specifically at the 72 character limit which is my current-fill-column. Furthermore, it reduces images to a maximum of 0.35 of the window's width/height. This is not always a good approach, though it works most of the time for the kind of content I am interested in: text-heavy articles.
  • prot-eww-download-html downloads the current web page as an .html file. It asks for a name to give to it, defaulting to the title of the page, and it also prepends the current date and time. To ensure that the file name is easily accessible with Unix tools, all punctuation marks and spaces are removed or replaced with hyphens. So if the page is titled "GNU Emacs manual | My ‘cool’ “website”!?" the saved file will be processed and stored as a filesystem path like ~/Downloads/eww/20210324_185035--gnu-emacs-manual-my-cool-website.html. I find that this is superior to the generic eww-download which neither asks for a name, nor adds a date and file type extension. Where eww-download still excels is for downloading the link at point, when that is not an HTML file (e.g. a pdf or zip archive).
  • prot-eww-open-in-other-window is meant to open the link at point in a new buffer and in another window. It complements the default M-RET (eww-open-in-new-buffer) which re-uses the same window.

Some final tips:

  • g reloads the web page by fetching it anew over the network, while its C-u g counterpart re-renders what has already been downloaded. The latter is useful when, for example, you have a new window configuration and wish to fit the EWW buffer's contents in the window.
  • As already noted above, several commands that open an EWW buffer accept a C-u prefix to put their contents in a new buffer rather than re-use the existing one. With M-RET over a link in an EWW buffer, you can do the same.
  • When point is over an image, the shr-image-map gets activated. This allows you to run commands such as image-increase-size, bound to +, and image-decrease-size on -. This means that you can always view an image at an appropriate size, regardless of whether its initial dimensions were affected by shr-max-image-proportion and the size of the window holding the EWW buffer.

Also watch EWW and my extras (2021-03-25) for a video demonstration of some of the above.

;;; Simple HTML Renderer (shr), Emacs Web Wowser (eww), Elpher, and prot-eww.el
(prot-emacs-builtin-package 'browse-url
  (setq browse-url-browser-function 'eww-browse-url)
  (setq browse-url-secondary-browser-function 'browse-url-default-browser))

(prot-emacs-builtin-package 'shr
  (setq shr-use-colors nil)             ; t is bad for accessibility
  (setq shr-use-fonts nil)              ; t is not for me
  (setq shr-max-image-proportion 0.6)
  (setq shr-image-animate nil)          ; No GIFs, thank you!
  (setq shr-width nil)                  ; check `prot-eww-readable'
  (setq shr-discard-aria-hidden t)
  (setq shr-cookie-policy nil))

(prot-emacs-builtin-package 'url-cookie
  (setq url-cookie-untrusted-urls '(".*")))

(prot-emacs-builtin-package 'eww
  (setq eww-restore-desktop t)
  (setq eww-desktop-remove-duplicates t)
  (setq eww-header-line-format nil)
  (setq eww-search-prefix "https://duckduckgo.com/html/?q=")
  (setq eww-download-directory (expand-file-name "~/Documents/eww-downloads"))
  (setq eww-suggest-uris
        '(eww-links-at-point
          thing-at-point-url-at-point))
  (setq eww-bookmarks-directory (locate-user-emacs-file "eww-bookmarks/"))
  (setq eww-history-limit 150)
  (setq eww-use-external-browser-for-content-type
        "\\`\\(video/\\|audio\\)") ; On GNU/Linux check your mimeapps.list
  (setq eww-browse-url-new-window-is-tab nil)
  (setq eww-form-checkbox-selected-symbol "[X]")
  (setq eww-form-checkbox-symbol "[ ]")
  ;; NOTE `eww-retrieve-command' is for Emacs28.  I tried the following
  ;; two values.  The first would not render properly some plain text
  ;; pages, such as by messing up the spacing between paragraphs.  The
  ;; second is more reliable but feels slower.  So I just use the
  ;; default (nil), though I find wget to be a bit faster.  In that case
  ;; one could live with the occasional errors by using `eww-download'
  ;; on the offending page, but I prefer consistency.
  ;;
  ;; '("wget" "--quiet" "--output-document=-")
  ;; '("chromium" "--headless" "--dump-dom")
  (setq eww-retrieve-command nil)

  (define-key eww-link-keymap (kbd "v") nil) ; stop overriding `eww-view-source'
  (define-key eww-mode-map (kbd "L") #'eww-list-bookmarks)
  (define-key dired-mode-map (kbd "E") #'eww-open-file) ; to render local HTML files
  (define-key eww-buffers-mode-map (kbd "d") #'eww-bookmark-kill)   ; it actually deletes
  (define-key eww-bookmark-mode-map (kbd "d") #'eww-bookmark-kill)) ; same

(prot-emacs-elpa-package 'elpher)    ; NOTE 2021-07-24: work-in-progress

(prot-emacs-builtin-package 'prot-eww
  (setq prot-eww-save-history-file
        (locate-user-emacs-file "prot-eww-visited-history"))
  (setq prot-eww-save-visited-history t)
  (setq prot-eww-bookmark-link nil)

  (add-hook 'prot-eww-history-mode-hook #'hl-line-mode)

  (define-prefix-command 'prot-eww-map)
  (define-key global-map (kbd "C-c w") 'prot-eww-map)
  (let ((map prot-eww-map))
    (define-key map (kbd "b") #'prot-eww-visit-bookmark)
    (define-key map (kbd "e") #'prot-eww-browse-dwim)
    (define-key map (kbd "s") #'prot-eww-search-engine))
  (let ((map eww-mode-map))
    (define-key map (kbd "B") #'prot-eww-bookmark-page)
    (define-key map (kbd "D") #'prot-eww-download-html)
    (define-key map (kbd "F") #'prot-eww-find-feed)
    (define-key map (kbd "H") #'prot-eww-list-history)
    (define-key map (kbd "b") #'prot-eww-visit-bookmark)
    (define-key map (kbd "e") #'prot-eww-browse-dwim)
    (define-key map (kbd "o") #'prot-eww-open-in-other-window)
    (define-key map (kbd "E") #'prot-eww-visit-url-on-page)
    (define-key map (kbd "J") #'prot-eww-jump-to-url-on-page)
    (define-key map (kbd "R") #'prot-eww-readable)
    (define-key map (kbd "Q") #'prot-eww-quit)))

And here is prot-eww.el in its entirety. It is available as a file in my dotfiles' repo (same for all my Emacs libraries):

;;; prot-eww.el --- Extensions for EWW -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou, Abhiseck Paira

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;;         Abhiseck Paira <abhiseckpaira@disroot.org>
;; Maintainer: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "28.1"))

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Extensions for the eww, intended for my Emacs setup:
;; <https://protesilaos.com/emacs/dotemacs/>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.
;;
;; Thanks to Abhiseck Paira for the patches (see commit log for this
;; file, such as with C-x v l (vc-print-log)).  Some of those improved
;; on various aspects of the EWW-specific functionality, while others
;; provide the layer of integration with Elpher.  Abhiseck's online
;; presence:
;;
;; 1. <https://social.linux.pizza/@redstarfish>
;; 2. <gemini://redstarfish.flounder.online/>

;;; Code:

(require 'shr)
(require 'eww)
(require 'elpher nil t)
(require 'url-parse)
(require 'prot-common)
(require 'prot-pulse)

(defgroup prot-eww ()
  "Tweaks for EWW."
  :group 'eww)

;;;; Basic setup

;; TODO 2021-10-15: Deprecate this in favour of what we added to Emacs29.
;; <https://protesilaos.com/codelog/2021-10-15-emacs-29-eww-rename-buffers/>.

(defun prot-eww--rename-buffer ()
  "Rename EWW buffer using page title or URL.
To be used by `eww-after-render-hook'."
  (let ((name (if (eq "" (plist-get eww-data :title))
                  (plist-get eww-data :url)
                (plist-get eww-data :title))))
    (rename-buffer (format "*%s # eww*" name) t)))

(add-hook 'eww-after-render-hook #'prot-eww--rename-buffer)
(advice-add 'eww-back-url :after #'prot-eww--rename-buffer)
(advice-add 'eww-forward-url :after #'prot-eww--rename-buffer)

;;;; History extras

(defvar prot-eww-visited-history '()
  "History of visited URLs.")

(defcustom prot-eww-save-history-file
  (locate-user-emacs-file "prot-eww-visited-history")
  "File to save the value of `prot-eww-visited-history'."
  :type 'file
  :group 'prot-eww)

(defcustom prot-eww-save-visited-history nil
  "Whether to save `prot-eww-visited-history'.
If non-nil, save the value of `prot-eww-visited-history' in
`prot-eww-save-history-file'."
  :type 'boolean
  :group 'prot-eww)

(defcustom prot-eww-list-history-buffer "*prot-eww-history*"
  "Name of buffer for `prot-eww-list-history'."
  :type 'string
  :group 'prot-eww)

;; These history related functions are adapted from eww.
(defun prot-eww--save-visited-history ()
  "Save the value of `prot-eww-visited-history' in a file.
The file is determined by the variable `prot-eww-save-history-file'."
  (when prot-eww-save-visited-history
    (with-temp-file prot-eww-save-history-file
      (insert (concat ";; Auto-generated file;"
                      " don't edit -*- mode: lisp-data -*-\n"))
      (pp prot-eww-visited-history (current-buffer)))))

(defun prot-eww--read-visited-history (&optional error-out)
  "Read history from `prot-eww-save-history-file'.
If ERROR-OUT, signal `user-error' if there is no history."
  (when prot-eww-save-visited-history
    (let ((file prot-eww-save-history-file))
      (setq prot-eww-visited-history
            (unless (zerop
                     (or (file-attribute-size (file-attributes file))
                         0))
              (with-temp-buffer
                (insert-file-contents file)
                (read (current-buffer)))))
      (when (and error-out (not prot-eww-visited-history))
        (user-error "No history is defined")))))

(unless prot-eww-visited-history
  (prot-eww--read-visited-history t))

(defun prot-eww--history-prepare ()
  "Prepare dedicated buffer for browsing history."
  (set-buffer (get-buffer-create prot-eww-list-history-buffer))
  (prot-eww-history-mode)
  (let ((inhibit-read-only t)
        start)
    (erase-buffer)
    (setq-local header-line-format
                "Unified EWW and Elpher Browsing History (prot-eww)")
    (dolist (history prot-eww-visited-history)
      (setq start (point))
      (insert (format "%s" history) "\n")
      (put-text-property start (1+ start) 'prot-eww-history history))
    (goto-char (point-min))))

;;;###autoload
(defun prot-eww-list-history ()
  "Display `prot-eww-visited-history' in a dedicated buffer.
This is a replacement for `eww-list-histories' (or equivalent),
as it can combine URLs in the Gopher or Gemini protocols."
  (interactive)
  (when prot-eww-visited-history
    (prot-eww--save-visited-history))
  (prot-eww--read-visited-history t)
  (pop-to-buffer prot-eww-list-history-buffer)
  (prot-eww--history-prepare))

(defvar prot-eww-history-kill-ring nil
  "Store the killed history element.")

(defun prot-eww-history-kill ()
  "Kill the current history."
  (interactive)
  (let* ((start (line-beginning-position))
         (history (get-text-property start 'prot-eww-history))
         (inhibit-read-only t))
    (unless history
      (user-error "No history on the current line"))
    (forward-line 1)
    (push (buffer-substring start (point))
          prot-eww-history-kill-ring)
    (delete-region start (point))
    (setq prot-eww-visited-history (delq history
                                         prot-eww-visited-history))
    (prot-eww--save-visited-history)))

(defun prot-eww-history-yank ()
  "Yank a previously killed history to the current line."
  (interactive)
  (unless prot-eww-history-kill-ring
    (user-error "No previously killed history"))
  (beginning-of-line)
  (let ((inhibit-read-only t)
        (start (point))
        history)
    (insert (pop prot-eww-history-kill-ring))
    (setq history (get-text-property start 'prot-eww-history))
    (if (= start (point-min))
        (push history prot-eww-visited-history)
      (let ((line (count-lines start (point))))
        (setcdr (nthcdr (1- line) prot-eww-visited-history)
                (cons history (nthcdr line
                                      prot-eww-visited-history)))))
    (prot-eww--save-visited-history)))

(defun prot-eww-history-browse ()
  "Browse the history under point."
  (interactive)
  (let ((history (get-text-property (line-beginning-position)
                                     'prot-eww-history)))
    (unless history
      (user-error "No history on the current line"))
    (quit-window)
    (prot-eww history)))

(defvar prot-eww-history-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-k") 'prot-eww-history-kill)
    (define-key map (kbd "C-y") 'prot-eww-history-yank)
    (define-key map (kbd "<RET>") 'prot-eww-history-browse)

    (easy-menu-define nil map
      "Menu for `prot-eww-history-mode-map'."
      '("prot-eww history"
        ["Exit" quit-window t]
        ["Browse" prot-eww-history-browse
         :active (get-text-property (line-beginning-position)
                                    'prot-eww-history)]
        ["Kill" prot-eww-history-kill
         :active (get-text-property (line-beginning-position)
                                    'prot-eww-history)]
        ["Yank" prot-eww-history-yank
         :active prot-eww-history-kill-ring]))
    map))

(define-derived-mode prot-eww-history-mode
  special-mode
  "prot-eww-history"
  "Mode for listing history.

\\{prot-eww-history-mode-map}"
  (buffer-disable-undo)
  (setq truncate-lines t))

(defun prot-eww--record-history ()
  "Store URL in `prot-eww-visited-history'.
To be used by `eww-after-render-hook'."
  (let ((url (plist-get eww-data :url)))
    (add-to-history 'prot-eww-visited-history url)))

(autoload 'elpher-page-address "elpher")
(autoload 'elpher-address-to-url "elpher")
(defvar elpher-current-page)

(defun prot-eww--record-elpher-history (arg1 &optional arg2 arg3)
  "Store URLs visited using elpher in `prot-eww-visited-history'.
To be used by `elpher-visited-page'.  ARG1, ARG2, ARG3 are
unused."
  (let* ((address (elpher-page-address elpher-current-page))
         (url (elpher-address-to-url address)))
    ;; elpher-address-to-url checks for special pages.
    (when url
      (add-to-list 'prot-eww-visited-history url))))

(add-hook 'eww-after-render-hook #'prot-eww--record-history)
(advice-add 'eww-back-url :after #'prot-eww--record-history)
(advice-add 'eww-forward-url :after #'prot-eww--record-history)
(advice-add 'elpher-visit-page :after #'prot-eww--record-elpher-history)
;; Is there a better function to add this advice?

;;;; Commands

;; handler that browse-url calls.

(defun prot-eww--get-current-url ()
  "Return the current-page's URL."
  (cond ((eq major-mode 'elpher-mode)
         (elpher-address-to-url
          (elpher-page-address elpher-current-page)))
        ((eq major-mode 'eww-mode)
         (plist-get eww-data :url))
        ;; (t (user-error "Not a eww or elpher buffer"))
        ))

;; This is almost identical to browse-url-interactive-arg except it
;; calls thing-at-point-url-at-point instead of
;; browse-url-url-at-point[1]. The problem with [1] is that it cancats
;; "http" anything it finds, which is a problem for gemini, gopher
;; etc.  urls. I hope there's something similar or better way to do
;; it, we don't have to use this one.
(defun prot-eww--interactive-arg (prompt)
  "Read a URL from the minibuffer, prompting with PROMPT.
If Transient-mark-mode is non-nil and the mark is active, it
defaults to the current region, else to the URL at or before
point.  If invoked with a mouse button, it moves point to the
position clicked before acting.

Return URL for use in a interactive."
  (let ((event (elt (this-command-keys) 0)))
    (and (listp event) (mouse-set-point event)))
  (read-string prompt
               (or (and transient-mark-mode mark-active
                        ;; rfc2396 Appendix E.
                        (replace-regexp-in-string
                         "[\t\r\f\n ]+" ""
                         (buffer-substring-no-properties
                          (region-beginning) (region-end))))
                   (thing-at-point-url-at-point t))))

(declare-function elpher-go "elpher")

;;;###autoload
(defun prot-eww (url &optional arg)
  "Pass URL to appropriate client.
With optional ARG, use a new buffer."
  (interactive
   (list (prot-eww--interactive-arg "URL: ")
         current-prefix-arg))
  (let ((url-parsed (url-generic-parse-url url)))
    (pcase (url-type url-parsed)
      ((or "gemini" "gopher" "gophers" "finger")
       (elpher-go url))
      (_ (eww url arg)))))

;;;###autoload
(defun prot-eww-browse-dwim (url &optional arg)
  "Visit a URL, maybe from `eww-prompt-history', with completion.

With optional prefix ARG (\\[universal-argument]) open URL in a
new eww buffer.  If URL does not look like a valid link, run a
web query using `eww-search-prefix'.

When called from an eww buffer, provide the current link as
\\<minibuffer-local-map>\\[next-history-element]."
  (interactive
   (let ((all-history (delete-dups
                       (append prot-eww-visited-history
                               eww-prompt-history)))
         (current-url (prot-eww--get-current-url)))
     (list
      (completing-read "Run EWW on: " all-history
                       nil nil current-url 'eww-prompt-history current-url)
      (prefix-numeric-value current-prefix-arg))))
  (prot-eww url arg))

;; NOTE 2021-09-08: This uses the EWW-specific bookmarks, NOT those of
;; bookmark.el.  Further below I provide integration with the latter,
;; meaning that we must either make this obsolete or make it work with
;; the new system.
;;;###autoload
(defun prot-eww-visit-bookmark (&optional arg)
  "Visit bookmarked URL.

With optional prefix ARG (\\[universal-argument]) open URL in a
new EWW buffer."
  (interactive "P")
  (eww-read-bookmarks)
  (let ((list (gensym)))
    (dolist (bookmark eww-bookmarks)
      (push (plist-get bookmark :url) list))
    (if eww-bookmarks
        (eww (completing-read "Visit EWW bookmark: " list)
             (when arg 4))
      (user-error "No bookmarks"))))

(defun prot-eww--capture-url-on-page (&optional position)
  "Capture all the links on the current web page.

Return a list of strings.  Strings are in the form LABEL @ URL.
When optional argument POSITION is non-nil, include position info
in the strings too, so strings take the form
LABEL @ URL ~ POSITION."
  (let (links match)
    (save-excursion
      (goto-char (point-min))
      ;; NOTE 2021-07-25: The first clause in the `or' is meant to
      ;; address a bug where if a URL is in `point-min' it does not get
      ;; captured.
      (while (setq match (text-property-search-forward 'shr-url))
        (let* ((raw-url (prop-match-value match))
               (start-point-prop (prop-match-beginning match))
               (end-point-prop (prop-match-end match))
               (url (when (stringp raw-url)
                      (propertize raw-url 'face 'link)))
               (label (replace-regexp-in-string "\n" " " ; NOTE 2021-07-25: newlines break completion
                       (buffer-substring-no-properties
                       start-point-prop end-point-prop)))
               (point start-point-prop)
               (line (line-number-at-pos point t))
               (column (save-excursion (goto-char point) (current-column)))
               (coordinates (propertize
                             (format "%d,%d (%d)" line column point)
                             'face 'shadow)))
          (when url
            (if position
                (push (format "%-15s ~ %s  @ %s"
                              coordinates label url)
                      links)
              (push (format "%s  @ %s"
                            label url)
                    links))))))
    links))

(defmacro prot-eww-act-visible-window (&rest body)
  "Run BODY within narrowed-region.
If region is active run BODY within active region instead.
Return the value of the last form of BODY."
  `(save-restriction
     (if (use-region-p)
         (narrow-to-region (region-beginning) (region-end))
       (narrow-to-region (window-start) (window-end)))
     ,@body))

;;;###autoload
(defun prot-eww-visit-url-on-page (&optional arg)
  "Visit URL from list of links on the page using completion.

With optional prefix ARG (\\[universal-argument]) open URL in a
new EWW buffer."
  (interactive "P")
  (when (derived-mode-p 'eww-mode)
    (let* ((links (prot-eww--capture-url-on-page))
           (selection (completing-read "Browse URL from page: " links nil t))
           (url (replace-regexp-in-string ".*@ " "" selection)))
      (eww url (when arg 4)))))

;;;###autoload
(defun prot-eww-jump-to-url-on-page (&optional arg)
  "Jump to URL position on the page using completion.

When called without ARG (\\[universal-argument]) get URLs only
from the visible portion of the buffer.  But when ARG is provided
consider whole buffer."
  (interactive "P")
  (when (derived-mode-p 'eww-mode)
    (let* ((links
            (if arg
                (prot-eww--capture-url-on-page t)
              (prot-eww-act-visible-window
               (prot-eww--capture-url-on-page t))))
           (prompt-scope (if arg
                             (propertize "URL on the page" 'face 'warning)
                           "visible URL"))
           (prompt (format "Jump to %s: " prompt-scope))
           (selection (completing-read prompt links nil t))
           (position (replace-regexp-in-string "^.*(\\([0-9]+\\))[\s\t]+~" "\\1" selection))
           (point (string-to-number position)))
      (goto-char point)
      (prot-pulse-pulse-line))))

(defvar prot-eww--occur-feed-regexp
  (concat "\\(rss\\|atom\\)\\+xml.\\(.\\|\n\\)"
          ".*href=[\"']\\(.*?\\)[\"']")
  "Regular expression to match web feeds in HTML source.")

;;;###autoload
(defun prot-eww-find-feed ()
  "Produce bespoke buffer with RSS/Atom links from XML source."
  (interactive)
  (let* ((url (or (plist-get eww-data :start)
                  (plist-get eww-data :contents)
                  (plist-get eww-data :home)
                  (plist-get eww-data :url)))
         (title (or (plist-get eww-data :title) url))
         (source (plist-get eww-data :source))
         (buf-name (format "*feeds: %s # eww*" title)))
    (with-temp-buffer
      (insert source)
      (occur-1 prot-eww--occur-feed-regexp "\\3" (list (current-buffer)) buf-name))
    ;; Handle relative URLs, so that we get an absolute URL out of them.
    ;; Findings like "rss.xml" are not particularly helpful.
    ;;
    ;; NOTE 2021-03-31: the base-url heuristic may not always be
    ;; correct, though it has worked in all cases I have tested it on.
    (when (get-buffer buf-name)
      (with-current-buffer (get-buffer buf-name)
        (let ((inhibit-read-only t)
              (base-url (replace-regexp-in-string "\\(.*/\\)[^/]+\\'" "\\1" url)))
          (goto-char (point-min))
          (unless (re-search-forward prot-common-url-regexp nil t)
            (re-search-forward ".*")
            (replace-match (concat base-url "\\&"))))))))

;;TODO: Add this variable as user-option, that is, define it with
;;`defcustom' so that users can use the customization interface to
;;modify it.

(defvar prot-eww-search-engines
  '((debbugs . (debbugs
                "https://debbugs.gnu.org/cgi/bugreport.cgi?bug="
                hist-var prot-eww--debbugs-hist))
    (wikipedia . (wikipedia
                  "https://en.m.wikipedia.org/w/index.php?search="
                  hist-var prot-eww--wikipedia-hist))
    (archwiki . (archwiki
                 "https://wiki.archlinux.org/index.php?search="
                 hist-var prot-eww--archwiki-hist))
    (aur . (aur "https://aur.archlinux.org/packages/?K="
                hist-var prot-eww--aur-hist)))
  "Alist of Plist of web search engines related data.
From now on refer to this type of data as APLIST.  Each element
of APLIST is (KEY . VALUE) pair.  KEY is a symbol specifying
search engine name.  The VALUE is property list.

The plist has two key-value pairs.  K1 is the same symbol has KEY
and V1 is search string of the search engine.

K2 is the symbol 'hist-var', V2 is also a symbol that has a format
'prot-eww--K1-hist'.

NOTE: If you modify this variable after `prot-eww' is loaded you
need to run the following code after modification:

    (prot-eww--define-hist-var prot-eww-search-engines)")

;; Below 's-string' is short for 'search-string'. For wikipedia which
;; is this string: "https://en.m.wikipedia.org/w/index.php?search=". I
;; use this name because I don't know it's proper name.

;; Define constructor and selectors functions to access
;; `prot-eww-search-engines'.
;; the constructor
(defun prot-eww--cons-search-engines (name s-string)
  "Include a new Alist element.
The alist element is added to variable `prot-eww-search-engines'.

NAME should be symbol representing the search engine.  S-STRING
should be string, which is specific to named search engine."
  (let ((my-plist `(,name ,s-string))
        (hist-var-name (format "prot-eww--%s-hist"
                               (symbol-name name))))
    (plist-put my-plist 'hist-var (intern hist-var-name))
    (let ((my-alist (cons name my-plist)))
      (add-to-list 'prot-eww-search-engines my-alist))))

;; Selectors definitions start
(defun prot-eww--select-hist-name (aplist engine-name)
  "Get hist-var-name from APLIST of ENGINE-NAME."
  (let ((hist-var-name (plist-get
                        (alist-get engine-name aplist)
                        'hist-var)))
    hist-var-name))

(defun prot-eww--select-engine-names (aplist)
  "Return a list of search-engine names from APLIST.
Each value of the list is a string."
  (mapcar (lambda (x) (format "%s" (car x)))
          aplist))

(defun prot-eww--select-s-string (aplist engine-name)
  "Return the search-string for specified ENGINE-NAME from APLIST."
  (plist-get
   (alist-get engine-name aplist)
   engine-name))
;; Selector definitions end here.

(defun prot-eww--define-hist-var (aplist)
  "Initialize APLIST hist-variables to empty list; return nil."
  (let ((engine-names
         (prot-eww--select-engine-names aplist)))
    (dolist (engine engine-names)
      (let ((hist-var-name
             (prot-eww--select-hist-name aplist
                                         (intern engine))))
        (set hist-var-name '())))))

(prot-eww--define-hist-var prot-eww-search-engines)

;;;###autoload
(defun prot-eww-search-engine (engine s-term &optional arg)
  "Search S-TERM using ENGINE.
ENGINE is an assossiation defined in `prot-eww-search-engines'.

With optional prefix ARG (\\[universal-argument]) open the search
result in a new buffer."
  (interactive
   (let* ((engine-list (prot-eww--select-engine-names
                        prot-eww-search-engines))
          (engine-name (completing-read
                        "Search with: " engine-list nil t nil
                        'prot-eww--engine-hist))
          (history-list (prot-eww--select-hist-name
                         prot-eww-search-engines
                         (intern engine-name)))
          (search-term (read-string
                        "Search for: " nil history-list)))
     (list engine-name search-term
           (prefix-numeric-value current-prefix-arg))))
  (let* ((s-string
          (prot-eww--select-s-string prot-eww-search-engines
                                     (intern engine)))
         (eww-pass (format "%s%s" s-string s-term))
         (history-list (prot-eww--select-hist-name
                        prot-eww-search-engines
                        (intern engine))))
    (add-to-history history-list s-term)
    (eww eww-pass arg)))

;;;###autoload
(defun prot-eww-open-in-other-window ()
  "Use `eww-open-in-new-buffer' in another window."
  (interactive)
  (other-window-prefix)       ; For emacs28 -- it's a hack, but why not?
  (eww-open-in-new-buffer))

;;;###autoload
(defun prot-eww-readable ()
  "Use more opinionated `eww-readable'.

Set width is set to `current-fill-column'.  Adjust size of
images."
  (interactive)
  (let ((shr-width (current-fill-column))
        (shr-max-image-proportion 0.35))
    (eww-readable)))

;; NOTE 2021-09-08: This uses the EWW-specific bookmarks, NOT those of
;; bookmark.el.  Further below I provide integration with the latter,
;; meaning that we must either make this obsolete or make it work with
;; the new system.
;;;###autoload
(defun prot-eww-bookmark-page (title)
  "Add eww bookmark named with TITLE."
  (interactive
   (list
    (read-string "Set bookmark title: " (plist-get eww-data :title))))
  (plist-put eww-data :title title)
  (eww-add-bookmark))

(defvar prot-eww--punctuation-regexp "[][{}!@#$%^&*()_=+'\"?,.\|;:~`‘’“”]*"
  "Regular expression of punctionation that should be removed.")

(defun prot-eww--slug-no-punct (str)
  "Convert STR to a file name slug."
  (replace-regexp-in-string prot-eww--punctuation-regexp "" str))

(defun prot-eww--slug-hyphenate (str)
  "Replace spaces with hyphens in STR.
Also replace multiple hyphens with a single one and remove any
trailing hyphen."
  (replace-regexp-in-string
   "-$" ""
   (replace-regexp-in-string
    "-\\{2,\\}" "-"
    (replace-regexp-in-string "--+\\|\s+" "-" str))))

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

;;;###autoload
(defun prot-eww-download-html (name)
  "Download web page and call the file with NAME."
  (interactive
   (list
    (prot-eww--sluggify
     (read-string "Set downloaded file name: " (plist-get eww-data :title)))))
  (let* ((path (thread-last eww-download-directory
                 (expand-file-name
                  (concat (format-time-string "%Y%m%d_%H%M%S") "--" name ".html"))))
         (out (prot-common-shell-command-with-exit-code-and-output
               "wget" "-q" (format "%s" (plist-get eww-data :url))
               "-O" (format "%s" (shell-quote-argument path)))))
    (if (= (car out) 0)
        (message "Downloaded page at %s" path)
      (message "Error downloading page: %s" (cdr out)))))

(defun prot-eww--kill-buffers-when (predicate)
  "Kill buffers when PREDICATE is non-nil.

Loop through the buffer list, calling PREDICATE with each buffer.
When calling PREDICATE with a buffer returns non-nil, kill that
buffer.

PREDICATE must be function that takes buffer-object as the one
and only argument.  It should return nil or non-nil."
  (let ((list-buffers (buffer-list)))
    (dolist (buffer list-buffers)
      (when (funcall predicate buffer)
        (kill-buffer buffer)))))

(defun prot-eww--kill-eww-buffers-p (buffer)
  "Predicate function.  Return nil or non-nil.

Take BUFFER, make it current, check if it has 'eww-mode' as the
`major-mode' or if its major mode is derived from `special-mode'
and has \"eww\" in the buffer-name. Then return non-nil."
  (let ((case-fold-search t))  ; ignore case
    (with-current-buffer buffer
      (or (eq major-mode 'eww-mode)
          (and (derived-mode-p 'special-mode)
               (string-match "\\*.*eww.*\\*" (buffer-name)))))))

(defun prot-eww-kill-eww-buffers ()
  "Kill all EWW buffers.
Also kill special buffers made by EWW for example buffers like
\"*eww-bookmarks*\", \"*eww-history*\" etc."
  (prot-eww--kill-buffers-when 'prot-eww--kill-eww-buffers-p))

(defcustom prot-eww-delete-cookies t
  "If non-nil delete cookies when `prot-eww-quit' is called."
  :type 'boolean
  :group 'prot-eww)

(defun prot-eww-delete-cookies ()
  "Delete cookies from the cookie file."
  (when prot-eww-delete-cookies
    (url-cookie-delete-cookies)))

;; TODO: Make it defcustom
(defvar prot-eww-quit-hook nil
  "Run this hook when `prot-eww-quit' is called.")

;; Populate the hook with these functions.
(dolist (func '(prot-eww-delete-cookies
                prot-eww-kill-eww-buffers
                prot-eww--save-visited-history))
  (add-hook 'prot-eww-quit-hook func))

;;;###autoload
(defun prot-eww-quit ()
  "Quit eww, kill all its buffers, delete all cookies.
As a final step, save `prot-eww-visited-history' to a file (see
`prot-eww-save-history-file')."
  (interactive)
  (if prot-eww-save-visited-history
      (when (y-or-n-p "Are you sure you want to quit eww? ")
        (run-hooks 'prot-eww-quit-hook))
    ;;
    ;; Now users have full control what `prot-eww-quit' does, by
    ;; modifying `prot-eww-quit-hook'.
    (when (yes-or-no-p "Are you sure you want to quit eww?")
      (run-hooks 'prot-eww-quit-hook))))

;;;;; Bookmarks with bookmark.el
;; The following is adapted from vc-dir.el.

;; TODO 2021-09-08: Review all legacy bookmark functions defined herein.

(defcustom prot-eww-bookmark-link nil
  "Control the behaviour of bookmarking inside EWW buffers.

If non-nil bookmark the button at point, else the current page's
URL.  Otherwise only target the current page.

This concerns the standard bookmark.el framework, so it applies
to commands `bookmark-set' and `bookmark-set-no-overwrite'."
  :type 'boolean
  :group 'prot-eww)

(declare-function bookmark-make-record-default "bookmark" (&optional no-file no-context posn))
(declare-function bookmark-prop-get "bookmark" (bookmark prop))
(declare-function bookmark-default-handler "bookmark" (bmk))
(declare-function bookmark-get-bookmark-record "bookmark" (bmk))

(defun prot-eww--bookmark-make-record ()
  "Return a bookmark record.
If `prot-eww-bookmark-link' is non-nil and point is on a link button,
return a bookmark record for that link.  Otherwise, return a bookmark
record for the current EWW page."
  (let* ((button (and prot-eww-bookmark-link
                      (button-at (point))))
         (url (if button
                  (button-get button 'shr-url)
                (plist-get eww-data :url))))
    (unless url
      (error "No link found; cannot bookmark this"))
    (let* ((title (if button
                      url
                    (concat "(EWW) " (plist-get eww-data :title))))
           (pos (if button nil (point)))
           (defaults (delq nil (list title url))))
      `(,title
        ,@(bookmark-make-record-default 'no-file)
        (eww-url . ,url)
        (filename . ,url) ; This is a hack to get Marginalia annotations
        (position . ,pos)
        (handler . prot-eww-bookmark-jump)
        (defaults . ,defaults)))))

(defun prot-eww--set-bookmark-handler ()
  "Set appropriate `bookmark-make-record-function'.
Intended for use with `eww-mode-hook'."
  (setq-local bookmark-make-record-function #'prot-eww--bookmark-make-record))

(add-hook 'eww-mode-hook #'prot-eww--set-bookmark-handler)

(defun prot-eww--pop-to-buffer (buffer &rest _args)
  "Set BUFFER and ignore ARGS.
Just a temporary advice to override `pop-to-buffer'."
  (set-buffer buffer))

(declare-function bookmark-get-handler "bookmark" (bookmark-name-or-record))
(declare-function bookmark-get-front-context-string "bookmark" (bookmark-name-or-record))
(declare-function bookmark-get-rear-context-string "bookmark" (bookmark-name-or-record))
(declare-function bookmark-get-position "bookmark" (bookmark-name-or-record))
(declare-function bookmark-name-from-full-record "bookmark" (bookmark-record))
(declare-function bookmark-get-bookmark "bookmark" (bookmark-name-or-record &optional noerror))

;; Copied from the `eww-conf.el' of JSDurand on 2021-09-17 10:19 +0300:
;; <https://git.jsdurand.xyz/emacsd.git/tree/eww-conf.el>.  My previous
;; version would not work properly when trying to open the bookmark in
;; the other window from inside the Bookmarks' list view.

;;;###autoload
(defun prot-eww-bookmark-jump (bookmark)
  "Jump to BOOKMARK in EWW.
This is intended to be the handler for bookmark records created
by `prot-eww--bookmark-make-record'.

If there is already a buffer visiting the URL of the bookmark,
simply jump to that buffer and try to restore the point there.
Otherwise, fetch URL and afterwards try to restore the point."
  (let ((handler (bookmark-get-handler bookmark))
        (location (bookmark-prop-get bookmark 'eww-url))
        (front (cons 'front-context-string
                     (bookmark-get-front-context-string bookmark)))
        (rear (cons 'rear-context-string
                    (bookmark-get-rear-context-string bookmark)))
        (position (cons 'position (bookmark-get-position bookmark)))
        (eww-buffers
         (delq
          nil
          (mapcar
           (lambda (buffer)
             (cond
              ((provided-mode-derived-p
                (buffer-local-value
                 'major-mode buffer)
                'eww-mode)
               buffer)))
           (buffer-list))))
        buffer)
    (cond
     ((and (stringp location)
           (not (string= location ""))
           (eq handler #'prot-eww-bookmark-jump))
      (let (reuse-p)
        (mapc
         (lambda (temp-buffer)
           (cond
            ((string=
              (plist-get
               (buffer-local-value 'eww-data temp-buffer)
               :url)
              location)
             (setq reuse-p temp-buffer)
             (setq buffer temp-buffer))))
         eww-buffers)
        ;; Don't switch to that buffer, otherwise it will cause
        ;; problems if we want to open the bookmark in another window.
        (cond
         (reuse-p
          (set-buffer reuse-p)
          ;; we may use the default handler to restore the position here
          (with-current-buffer reuse-p
            (goto-char (cdr position))
            (cond
             ((search-forward (cdr front) nil t)
              (goto-char (match-beginning 0))))
            (cond
             ((search-forward (cdr rear) nil t)
              (goto-char (match-end 0))))))
         (t
          ;; HACK, GIANT HACK!
          
          (advice-add #'pop-to-buffer :override
                      #'prot-eww--pop-to-buffer)
          (eww location 4)
          ;; after the `set-buffer' in `eww', the current buffer is
          ;; the buffer we want
          (setq buffer (current-buffer))
          ;; restore the definition of pop-to-buffer...
          (advice-remove
           #'pop-to-buffer #'prot-eww--pop-to-buffer)
          ;; add a hook to restore the position

          ;; make sure each hook function is unique, so that different
          ;; hooks don't interfere with each other.
          (let ((function-symbol
                 (intern
                  (format
                   "eww-render-hook-%s"
                   (bookmark-name-from-full-record
                    (bookmark-get-bookmark bookmark))))))
            (fset function-symbol
                  (lambda ()
                    (remove-hook
                     'eww-after-render-hook function-symbol)
                    (bookmark-default-handler
                     (list
                      "" (cons 'buffer buffer)
                      front rear position))))
            (add-hook 'eww-after-render-hook function-symbol))))))
     ((user-error "Cannot jump to this bookmark")))))


;;; lynx dump

(defcustom prot-eww-post-lynx-dump-function nil
  "Function to run on lynx dumped buffer for post-processing.
Function is called with the URL of the page the buffer is
visiting.

Specifying nil turns off this variable, meaning that no
post-processing takes place."
  :group 'prot-eww
  :type '(choice (const :tag "Unspecified" nil)
                 function))

(defcustom prot-eww-lynx-dump-dir
  (if (stringp eww-download-directory)
      eww-download-directory
    (funcall eww-download-directory))
  "Directory to save lynx dumped files.
It should be an existing directory or a sexp that evaluates to an
existing directory."
  :group 'prot-eww
  :type '(choice directory sexp))

(defun prot-eww--lynx-available-p ()
  "Check if `lynx' is available in PATH."
  (executable-find "lynx"))

(defun prot-eww--get-text-property-string (prop)
  "Return string that has text property PROP at (point).
The string is from (point) to end of PROP.  If there is no text
property PROP at (point), return nil."
  (let* ((match (text-property-search-forward prop))
         (start-point-prop (prop-match-beginning match))
         (end-point-prop (prop-match-end match)))
    (and
     (<= start-point-prop (point) end-point-prop)
     (replace-regexp-in-string
      "\n" " "
      (buffer-substring-no-properties
       start-point-prop end-point-prop)))))

(defun prot-eww--current-page-title ()
  "Return title of the Web page EWW buffer is visiting."
  (plist-get eww-data :title))

(defun prot-eww-lynx-dump (url filename)
  "Run lynx -dump on URL and save output as FILENAME.
When run interactively in a eww buffer visiting a web page, run
lynx dump on the web page's URL.  If point is on a link, then run
lynx dump on that link instead."
  (interactive
   (let* ((default-url (or (get-text-property (point) 'shr-url)
                           (eww-current-url)))
          (dir prot-eww-lynx-dump-dir)
          (title (or
                  (prot-eww--get-text-property-string 'shr-url)
                  (prot-eww--current-page-title)))
          (def-file-name
            (file-name-concat dir
                              (concat (prot-eww--sluggify title) ".txt"))))
     (list
      (read-string (format "URL [%s]: " default-url) nil nil default-url)
      (read-file-name (format "File Name [%s]: " def-file-name) dir def-file-name))))
  (if (prot-eww--lynx-available-p)
      (progn
        (access-file prot-eww-lynx-dump-dir "Non existing directory specified")
        (with-temp-file filename
          (with-temp-message
              (format "Running `lynx --dump %s'" url)
            (let ((coding-system-for-read 'prefer-utf-8))
              (call-process "lynx" nil t nil "-dump" url)))
          (with-temp-message "Processing lynx dumped buffer..."
            (and
             (functionp prot-eww-post-lynx-dump-function)
             (funcall prot-eww-post-lynx-dump-function url)))))
    (error "`lynx' executable not found in PATH")))

(provide 'prot-eww)
;;; prot-eww.el ends here

5.16. Extensions for PDFs (pdf-tools)

The pdf-tools package builds on top of the external libraries, poppler and imagemagick (if Emacs is compiled with support for it) to deliver a series of minor modes for reading and interacting with PDF files from inside of Emacs. As it depends on those external files, it requires extra steps to make it work properly. Those are documented at length in its README and vary depending on your operating system.

All you need to start reading PDFs is to put the pdf-loader-install function in your configurations, which loads the tools once you open an appropriate file, such as through dired or with find-file. Once inside the resulting buffer, do C-h m (describe-mode) to learn about the key bindings and the commands they call. The basics:

Key Effect
+ Enlarge (zoom in)
- Shrink (zoom out)
0 Reset zoom
C-n Next line
C-p Previous line
SPC Scroll down
S-SPC Scroll up

To determine which minor modes out of the entire suite are activated, you need to configure the variable pdf-tools-enabled-modes. I reduce it only a subset of what is available by default because I do not need all the extras. That granted, there are at least two minor modes that users may find helpful: pdf-annot-minor-mode which provides annotation capabilities and pdf-sync-minor-mode which syncs the PDF with its corresponding TeX file when you are running some setup that compiles the latter to the former.

The value proposition of pdf-tools is that it works with isearch and occur so you can easily (i) search through the file and (ii) produce a buffer of locations with matching queries. As is the norm with pdf-tools, those facilities are implemented as minor modes: pdf-isearch-minor-mode, pdf-occur-global-minor-mode (this also works with ibuffer and dired).

Another helpful integration is with Emacs' outline-mode and imenu by means of pdf-outline-minor-mode. Simply hit o while viewing a PDF to produce an outline of the document and then, optionally, M-x imenu to navigate it using minibuffer completion (though for the latter task you may prefer something like consult-outline, which is part of the Consult package: Enhanced minibuffer commands (consult.el and prot-consult.el)).

Finally, I have some tweaks to change the backdrop of the buffer so that it is distinct from the page's background while using my light Modus Operandi theme (Modus themes (my highly accessible themes)). Plus, I make it automatically adapt to the modus-themes-toggle command, so that the PDF switches to a dark theme when it has to. Those are documented in the manual of the themes. If you are using them, evaluate this form: (info "(modus-themes) Backdrop for pdf-tools (DIY)").

;;; Extensions for PDFs (pdf-tools)
(prot-emacs-elpa-package 'pdf-tools
  (setq pdf-tools-enabled-modes         ; simplified from the defaults
        '(pdf-history-minor-mode
          pdf-isearch-minor-mode
          pdf-links-minor-mode
          pdf-outline-minor-mode
          pdf-misc-size-indication-minor-mode
          pdf-occur-global-minor-mode))
  (setq pdf-view-display-size 'fit-height)
  (setq pdf-view-continuous t)
  (setq pdf-view-use-dedicated-register nil)
  (setq pdf-view-max-image-width 1080)
  (setq pdf-outline-imenu-use-flat-menus t)

  (pdf-loader-install)

  ;; Those functions and hooks are adapted from the manual of my
  ;; modus-themes.  The idea is to (i) add a backdrop that is distinct
  ;; from the background of the PDF's page and (ii) make pdf-tools adapt
  ;; to theme switching via, e.g., `modus-themes-toggle'.
  (defun prot/pdf-tools-backdrop ()
    (face-remap-add-relative
     'default
     `(:background ,(modus-themes-color 'bg-alt))))

  (defun prot/pdf-tools-midnight-mode-toggle ()
    (when (derived-mode-p 'pdf-view-mode)
      (if (eq (car custom-enabled-themes) 'modus-vivendi)
          (pdf-view-midnight-minor-mode 1)
        (pdf-view-midnight-minor-mode -1))
      (prot/pdf-tools-backdrop)))

  (add-hook 'pdf-tools-enabled-hook #'prot/pdf-tools-midnight-mode-toggle)
  (add-hook 'modus-themes-after-load-theme-hook #'prot/pdf-tools-midnight-mode-toggle))

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

6.1. 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!

;;; Go to last change
(prot-emacs-elpa-package 'goto-last-change
  (define-key global-map (kbd "C-z") #'goto-last-change))

6.2. 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 "good enough".

In the code snippet right below I reshuffle some of the mode line indicators. Nothing too fancy. The mode-line-defining-kbd-macro is tweaked to use a more appropriate string for its indicator and to apply colours that are designed specifically for the mode line (the default uses the generic font-lock warning face).

Note that in Custom extensions for "focus mode" (prot-logos.el) I provide a snippet that optionally toggles the visibility of the modeline while entering a bespoke "focus mode" for writing/reading.

;;; Mode line
(setq mode-line-percent-position '(-3 "%p"))
(setq mode-line-position-column-line-format '(" %l,%c")) ; Emacs 28
(setq mode-line-defining-kbd-macro
      (propertize " Macro" 'face 'mode-line-emphasis))

;; Thanks to Daniel Mendler for this!  It removes the square brackets
;; that denote recursive edits in the modeline.  I do not need them
;; because I am using Daniel's `recursion-indicator':
;; <https://github.com/minad/recursion-indicator>.
(setq-default mode-line-modes
              (seq-filter (lambda (s)
                            (not (and (stringp s)
                                      (string-match-p
                                       "^\\(%\\[\\|%\\]\\)$" s))))
                          mode-line-modes))

(setq mode-line-compact nil)            ; Emacs 28
(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
                mode-line-modes
                "  "
                (vc-mode vc-mode)
                "  "
                mode-line-misc-info
                mode-line-end-spaces))

In the following sub-sections I provide customisations for some tools that place information on the mode line. Again, nothing flamboyant.

6.2.1. Moody.el (simple mode line configuration utility)

moody.el is a lightweight library that adds some flair to the mode line without complicating things. It is developed by Jonas Bernoulli. I have been using it on and off to make sure that it works well with my themes (see Modus themes (my highly accessible themes)).

My fairly minor tweaks in prot-moody.el (reproduced further below) align Moody with my Font configurations (prot-fonts.el). What I basically want is to make the mode line gracefully adapt to changes in font size.

;;; Moody.el (simple mode line configuration utility)
(prot-emacs-elpa-package 'moody)

(prot-emacs-builtin-package 'prot-moody
  ;; Adjust this and then evaluate `prot-moody-set-height'.  Not all
  ;; fonts work equally well with the same value.
  (setq prot-moody-font-height-multiplier 1.35)

  ;; Also check the Modus themes' `modus-themes-mode-line' which can set
  ;; the styles specifically for Moody.
  (prot-moody-set-height -1))

Here are my tweaks (from my dotfiles' repo):

;;; prot-moody.el --- Extensions to moody.el for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; This covers my moody.el extensions, for use in my Emacs setup:
;; https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'prot-common)
(require 'prot-fonts)
(require 'moody nil t)

(defgroup prot-moody ()
  "Tweaks for moody.el."
  :group 'mode-line)

(defcustom prot-moody-font-height-multiplier 1.65
  "Multiple of the font size to derive the moody height."
  :type 'number
  :group 'prot-moody)

(defun prot-moody--height ()
  "Set Moody height to an even number.
Bind this to a hook that gets called after loading/changing the
mode line's typeface (or the default one if they are the same)."
  (let* ((font (face-font 'mode-line))
         (height (truncate (* prot-moody-font-height-multiplier (aref (font-info font) 2))))
         (height-even (if (prot-common-number-even-p height) height (+ height 1))))
    (if font
        height-even
      24)))

(defvar moody-mode-line-height)

(defun prot-moody--mode-line-height ()
  "Set Moody height to the value of `prot-moody--height'."
  (let ((height (prot-moody--height)))
    (setq moody-mode-line-height height)))

(autoload 'moody-replace-mode-line-buffer-identification "moody")
(autoload 'moody-replace-vc-mode "moody")

;;;###autoload
(define-minor-mode prot-moody-set-height
  "Toggle Moody for the mode line and configure its fonts."
  :init-value nil
  :global t
  (if prot-moody-set-height
      (progn
        (moody-replace-mode-line-buffer-identification)
        (moody-replace-vc-mode)
        (add-hook 'prot-fonts-set-typeface-hook #'prot-moody--mode-line-height)
        (run-hooks 'prot-fonts-set-typeface-hook))
    (let ((format (default-value 'mode-line-format)))
      (when (member 'moody-mode-line-buffer-identification format)
        (moody-replace-mode-line-buffer-identification 'reverse))
      (when (member '(vc-mode moody-vc-mode) format)
        (moody-replace-vc-mode 'reverse)))
    (remove-hook 'prot-fonts-set-typeface-hook #'prot-moody--mode-line-height)))

(defvar keycast-insert-after)

(defun prot-moody-keycast-insert-after ()
  "Specify `keycast-insert-after' buffer identification."
  (setq keycast-insert-after
        (if prot-moody-set-height
            'moody-mode-line-buffer-identification
          'mode-line-buffer-identification)))

(provide 'prot-moody)
;;; prot-moody.el ends here

6.2.2. Hide modeline "lighters" (minions.el)

This package by Jonas Bernoulli neatly wraps up all minor mode "lighters" and hides them behind a single character. The "lighter" is the text that identifies the minor mode on the mode line. Having a few of them is usually okay, but a lot of them do not scale well.

;;; Hide modeline "lighters" (minions.el)
(prot-emacs-elpa-package 'minions
  (setq minions-mode-line-lighter ";")
  ;; NOTE: This will be expanded whenever I find a mode that should not
  ;; be hidden
  (setq minions-prominent-modes
        (list 'defining-kbd-macro
              'flymake-mode
              'prot-simple-monocle))
  (minions-mode 1))

6.2.3. Mode line recursion indicators

This utility by Daniel Mendler provides a couple of indicators for denoting minibuffer recursion. They offer a reminder that we are in the midst of a recursive editing session when we should, perhaps, not be in one. I consider recursion-indicator complementary to what is already built into Emacs in the form of minibuffer-depth-indicate-mode which shows the level of recursion at the current minibuffer prompt (refer to Minibuffer configurations and my extras (mct.el)).

;;; Mode line recursion indicators
(prot-emacs-elpa-package 'recursion-indicator
  (setq recursion-indicator-general "&")
  (setq recursion-indicator-minibuffer "@")
  (recursion-indicator-mode 1))

6.2.4. Display current time (and world-clock)

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 immersive experience, 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: just enable display-time-mode.

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 do not need the load average as it adds too much noise. As for the mail indicator, I used it for a while, but eventually decided to use my own approach which also shows the number of new emails (refer to the Email settings and search, in particular, for either the old prot-mail-mail-indicator or the newer prot-notmuch-mail-indicator).

Since the inception of prot-tab.el which creates a status line that replaces mode lines, I enable the clock and mail indicator only when the status line is enabled (see Tabs for window layouts).

Lastly, I use the world-clock command (for Emacs28+) when I need to get an overview of the current time in various parts of the planet.

;;; Display current time
(prot-emacs-builtin-package 'time
  (setq display-time-format "%a %e %b, %H:%M")
  ;;;; 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-default-load-average nil)
  ;; NOTE 2021-04-19: For all those, I have implemented a custom
  ;; solution that also shows the number of new items.  Refer to my
  ;; email settings, specifically `prot-mail-mail-indicator'.
  ;;
  ;; NOTE 2021-05-16: Or better check `prot-notmuch-mail-indicator'.
  (setq display-time-mail-directory nil)
  (setq display-time-mail-function nil)
  (setq display-time-use-mail-icon nil)
  (setq display-time-mail-string nil)
  (setq display-time-mail-face nil)

;;; World clock
  (setq zoneinfo-style-world-list
        '(("America/Los_Angeles" "Los Angeles")
          ("America/Chicago" "Chicago")
          ("Brazil/Acre" "Rio Branco")
          ("America/New_York" "New York")
          ("Brazil/East" "Brasília")
          ("Europe/Lisbon" "Lisbon")
          ("Europe/Brussels" "Brussels")
          ("Europe/Athens" "Athens")
          ("Asia/Tehran" "Tehran")
          ("Asia/Tbilisi" "Tbilisi")
          ("Asia/Yekaterinburg" "Yekaterinburg")
          ("Asia/Shanghai" "Shanghai")
          ("Asia/Tokyo" "Tokyo")
          ("Asia/Vladivostok" "Vladivostok")))

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

  ;; ;; NOTE 2021-10-04: Check `prot-tab-status-line'.
  ;; (add-hook 'after-init-hook #'display-time-mode)
  )

6.2.5. 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.el library offers that specific piece of functionality (though I also configure Moody for its primary purpose of styling the mode line).

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.

;;; Keycast mode
(prot-emacs-elpa-package 'keycast
  ;; Those are for `keycast-mode'
  (setq keycast-window-predicate 'moody-window-active-p) ; assumes `moody.el'
  (setq keycast-separator-width 1)
  (setq keycast-remove-tail-elements nil)

  (dolist (input '(self-insert-command
                   org-self-insert-command))
    (add-to-list 'keycast-substitute-alist `(,input "." "Typing…")))

  (dolist (event '(mouse-event-p
                   mouse-movement-p
                   mwheel-scroll))
    (add-to-list 'keycast-substitute-alist `(,event nil)))

  ;; Those are for the `keycast-log-mode'
  (setq keycast-log-format "%-20K%C\n")
  (setq keycast-log-frame-alist
        '((minibuffer . nil)))
  (setq keycast-log-newest-first t)

  ;; Specify `keycast-insert-after' buffer identification.  This make it
  ;; possible to seamlessly toggle `prot-moody-set-height' without
  ;; disrupting keycast.
  (with-eval-after-load 'prot-moody
    (add-hook 'prot-moody-set-height-hook #'prot-moody-keycast-insert-after)))

6.3. Window divider mode

This is a built-in mode that can draw both vertical and horizontal borders. It can be particularly helpful when used with windows that do not have a modeline, such as what happens when I enable my custom prot-tab-status-line on Emacs28, which moves all the relevant information to the tab bar (in effect, it creates a universal status line—see Tabs for window layouts (tab-bar.el and prot-tab.el)).

;;; Window divider mode
(setq window-divider-default-right-width 1)
(setq window-divider-default-bottom-width 1)
(setq window-divider-default-places 'right-only)

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

;;; Fringe mode
(prot-emacs-builtin-package 'fringe
  (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))

6.5. Color tools (ct.el)

The ct.el library by Github user "neeasade" provides various utilities for testing colour values across several spaces. The developer also has an interesting article on the matter: Reasoning about Colors. I may need some of those tools while developing my Modus themes.

;;; Color tools (ct.el)
(prot-emacs-elpa-package 'ct)

6.6. Rainbow mode for colour previewing (rainbow-mode.el)

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

;;; Rainbow mode for colour previewing (rainbow-mode.el)
(prot-emacs-elpa-package 'rainbow-mode
  (setq rainbow-ansi-colors nil)
  (setq rainbow-x-colors nil))

6.7. Depth-based code colourisation (prism.el)

Adam Porter's (aka alphapapa) prism.el colourises programming code based on the level of depth, rather than targeting syntactic constructs like keywords, strings, variables. This can be useful when working with highly-nested and/or unfamiliar code. It has two modes that are intended for specific types of programming languages:

  1. prism-mode is good for Lisp and languages that look like C (I only code in Elisp);
  2. prism-whitespace-mode is for languages that are more sensitive to indentation.

The degree of colouration for each level of depth is configurable, as are the colours to be used. Consult Prism's documentation for the technicalities.

My prot-prism.el (reproduced after the following configurations) provides a few extras that help me set the desired style of colouration. It provides an alist of palette presets. Those can be applied through minibuffer completion with the command prot-prism-set-colors. All the presets are designed to extract colour values from the active Modus theme (Modus themes (my highly accessible themes)). Those range from a minimalist style of drawing only four colours, to a more expansive sixteen-colour gamout.

The prot-prism-set-colors command also takes care to activate the appropriate Prism mode for languages whose major mode is declared as "indentation-sensitive". Those are added as a list to the variable prot-prism-negative-space-sensitive-modes.

To disable the effect, just do M-x prot-prism-disable.

;;; Depth-based code colourisation (prism.el)
(prot-emacs-elpa-package 'prism
  (setq prism-comments t))

(prot-emacs-builtin-package 'prot-prism
  (setq prot-prism-negative-space-sensitive-modes
        '(sh-mode yaml-mode))

  (setq prot-prism-presets-function #'prot-prism--colors))

Here are my tweaks (from my dotfiles' repo):

;;; prot-prism.el --- Tweaks for prism.el -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Tweaks for the `prism.el' library of Adam Porter (alphapapa).
;; Intended for my Emacs setup: <https://protesilaos.com/emacs/dotemacs/>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'prism nil t)
(require 'modus-themes nil t)

(defgroup prot-prism ()
  "Tweaks for cursor appearance."
  :group 'faces)

(declare-function modus-themes-with-colors "modus-themes" (&rest body))

(defcustom prot-prism-negative-space-sensitive-modes '(sh-mode yaml-mode)
  "Modes where negative space or indentation indicates depth."
  :type '(repeat symbol)
  :group 'prot-prism)

(defcustom prot-prism-presets-function #'prot-prism--colors
  "Function that returns alist of style presets.
The car of the alist is a number, indicating `prism-num-faces'.
The cdr is a list of strings that represent color names or
values.  The latter use hexadecimal RGB notation."
  :type 'symbol
  :group 'prot-prism)

(defun prot-prism--colors ()
  "Return alist with color presets.
See `prot-prism-presets-function'."
  ;; NOTE 2021-09-18: The `modus-themes-with-colors' is documented at
  ;; length in the themes' manual.
  (modus-themes-with-colors
    `(("4"  . ,(list blue
                     magenta
                     magenta-alt-other
                     green-alt))
      ("8"  . ,(list blue
                     magenta
                     magenta-alt-other
                     cyan-alt-other
                     fg-main
                     blue-alt
                     red-alt-other
                     cyan))
      ("16" . ,(list fg-main
                     magenta
                     cyan-alt-other
                     magenta-alt-other
                     blue
                     magenta-alt
                     cyan-alt
                     red-alt-other
                     green
                     fg-main
                     cyan
                     yellow
                     blue-alt
                     red-alt
                     green-alt-other
                     fg-special-warm)))))

(add-hook 'modus-themes-after-load-theme-hook #'prot-prism--colors)

(defvar prot-prism--preset-hist '()
  "Minibuffer history for `prot-prism-set-colors'.")

(defun prot-prism--set-colors-prompt ()
  "Helper prompt for `prot-prism-set-colors'."
  (let* ((hist prot-prism--preset-hist)
         (default (when hist (nth 0 hist))))
    (completing-read
     (format "Outline style [%s]: " default)
     (mapcar #'car (funcall prot-prism-presets-function))
     nil t nil 'prot-prism--preset-hist default)))

(autoload 'prism-set-colors "prism")
(defvar prism-num-faces)
(declare-function prism-mode "prism")
(declare-function prism-whitespace-mode "prism")

;;;###autoload
(defun prot-prism-set-colors (preset)
  "Set prism colors to PRESET in `prot-prism-presets-function'."
  (interactive (list (prot-prism--set-colors-prompt)))
  (let* ((alist (funcall prot-prism-presets-function))
         (num (car (assoc preset alist)))
         (colors (cdr (assoc preset alist))))
    (setq prism-num-faces (string-to-number num))
    (prism-set-colors
      :desaturations '(0) ; do not change---may lower the contrast ratio
      :lightens '(0)      ; same
      :colors colors)
    (if (member major-mode prot-prism-negative-space-sensitive-modes)
        (prism-whitespace-mode 1)
      (prism-mode 1))
    (add-to-history 'prot-prism--preset-hist num)))

;;;###autoload
(defun prot-prism-disable ()
  "Disable Prism coloration."
  (interactive)
  (if (or (member major-mode prot-prism-negative-space-sensitive-modes)
          (bound-and-true-p prism-whitespace-mode))
      (prism-whitespace-mode -1)
    (prism-mode -1)))

(provide 'prot-prism)
;;; prot-prism.el ends here

6.8. Line numbers and relevant indicators (prot-sideline.el)

prot-sideline.el (reproduced after the package configurations) is a set of simplete-minded features:

  1. It bundles up together three distinct visual elements as part of a common minor mode: prot-sideline-mode. Its constituents are current line highlighting (hl-line-mode), diff indicators (diff-hl-mode), and line numbers (display-line-numbers-mode). Line numbers and line highlighting are built into Emacs.
  2. A simple toggle for whitespace-mode, which I only ever use to double check some area's indentation or to confirm that no newline is missing at the end of the file.

Note that the diff-hl package offers some more features other than the obvious colour-coded highlighting of changes, such as the ability to move between diff hunks (with C-x v [ and C-x v ]) or to revert the current hunk (C-x v n). Those can come in handy (check my comprehensive extensions in Version control framework (vc.el and prot-vc.el)).

;;; Line numbers and relevant indicators (prot-sideline.el)
(prot-emacs-builtin-package 'prot-sideline
  (require 'display-line-numbers)
  ;; 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 0)
  (setq display-line-numbers-minor-tick 0)
  ;; Use absolute numbers in narrowed buffers
  (setq-default display-line-numbers-widen t)

  (prot-emacs-elpa-package 'diff-hl
    (setq diff-hl-draw-borders nil)
    (setq diff-hl-side 'left))

  (require 'hl-line)
  (setq hl-line-sticky-flag nil)
  (setq hl-line-overlay-priority -50) ; emacs28

  (require 'whitespace)

  (let ((map global-map))
    (define-key map (kbd "<f6>") #'prot-sideline-negative-space-toggle)
    (define-key map (kbd "<f7>") #'prot-sideline-mode)
    (define-key map (kbd "C-c z") #'delete-trailing-whitespace)))

This is prot-sideline.el (part of my dotfiles' repo):

;;; prot-sideline.el --- Extensions for line numbers and relevant indicators -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Extensions for line numbers and relevant indicators, intended to be
;; used as part of my Emacs setup: https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(defgroup prot-sideline ()
  "Setup for reading and presenting text-heavy buffers."
  :group 'files)

;;;###autoload
(define-minor-mode prot-sideline-mode
  "Buffer-local wrapper mode for presentations."
  :init-value nil
  :global nil)

(autoload 'diff-hl-mode "diff-hl")

(defun prot-sideline--diff-hl-toggle ()
  "Toggle buffer local diff indicators in the fringe."
  (if (or (bound-and-true-p diff-hl-mode)
          (not (bound-and-true-p prot-sideline-mode)))
      (diff-hl-mode -1)
    (diff-hl-mode 1)))

(add-hook 'prot-sideline-mode-hook #'prot-sideline--diff-hl-toggle)

(defun prot-sideline--numbers-toggle ()
  "Toggle line numbers."
  (if (or (bound-and-true-p display-line-numbers-mode)
          (not (bound-and-true-p prot-sideline-mode)))
      (display-line-numbers-mode -1)
    (display-line-numbers-mode 1)))

(add-hook 'prot-sideline-mode-hook #'prot-sideline--numbers-toggle)

(defun prot-sideline--hl-line-toggle ()
  "Toggle line highlight."
  (if (or (bound-and-true-p hl-line-mode)
          (not (bound-and-true-p prot-sideline-mode)))
      (hl-line-mode -1)
    (hl-line-mode 1)))

(add-hook 'prot-sideline-mode-hook #'prot-sideline--hl-line-toggle)

(autoload 'whitespace-mode "whitespace")

;; We keep this separate, as I do not want it bundled up together with
;; the rest of the functionality included here.
;;;###autoload
(defun prot-sideline-negative-space-toggle ()
  "Toggle the display of indentation and space characters."
  (interactive)
  (if (bound-and-true-p whitespace-mode)
      (whitespace-mode -1)
    (whitespace-mode)))

(provide 'prot-sideline)
;;; prot-sideline.el ends here

6.9. Outline mode, outline minor mode, and extras (prot-outline.el)

The outline.el 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 (actually Outline predates Org). The major mode is meant to work with plain text files, or be leveraged by other packages that need to have some structure and the accompanying benefits of outline folding. In practice, however, I never found a dedicated use for it that would justify it over the more featureful Org.

Where outline.el truly shines, in my experience, is in the minor mode it defines (outline-minor-mode), which provides the familiar structured, heading-folding facilities in other major modes, like emacs-lisp-mode or any arbitrary mode, like diff-mode and the diary.

Also read:

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 (;;;) followed by a space and then the text of the heading, such as ;;; Code:. That is considered a heading level 1. Every extra delimiter will increase heading levels accordingly. The buffer-local variable outline-regexp determines what constitutes a heading for this purpose.

Now on to my custom library, prot-outline.el which builds on those concepts:

  • Provide several new commands and minor extras for working with outlines.
  • Define a prot-outline-minor-mode-safe command that checks whether the current buffer's major mode is not a member of a blocklist. The idea is to not run outline-minor-mode with major modes that already provide its functionality: org-mode, outline-mode, markdown-mode.

Watch my video demo of outline-minor-mode and imenu (2020-07-20), though note that it was recorded long before I wrote prot-outline.el. In particular, older versions of my code would establish bindings for imenu.el. This was done via bespoke entry and exit hooks and was intended to complement the standard Imenu headings with those of the Outline. This is no longer needed because consult-outline does exactly that (Enhanced minibuffer commands (consult.el and prot-consult.el)). A quick reminder of why this matters: you can navigate the outline using minibuffer completion, which is my favourite way to browse a file. You can, in the same spirit, use Embark to produce a buffer with the completion candidates, i.e. the headings, or a subset you have narrowed to, and navigate therefrom. It really is an efficient workflow: Extended minibuffer actions and more (embark.el and prot-embark.el).

;;; Outline mode, outline minor mode, and extras (prot-outline.el)
(prot-emacs-builtin-package 'outline
  (setq outline-minor-mode-highlight 'override) ; emacs28
  (setq outline-minor-mode-cycle t)             ; emacs28
  (let ((map outline-minor-mode-map))
    ;; ;; NOTE 2021-07-25: Those two are already defined (emacs28).
    ;; (define-key map (kbd "TAB") #'outline-cycle)
    ;; (define-key map (kbd "<backtab>") #'outline-cycle-buffer) ; S-TAB
    (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-o") #'outline-hide-other)
    (define-key map (kbd "C-c C-u") #'outline-up-heading)))

(prot-emacs-builtin-package 'prot-outline
  (let ((map outline-minor-mode-map))
    (define-key map (kbd "C-c C-v") #'prot-outline-move-major-heading-down)
    (define-key map (kbd "M-<down>") #'prot-outline-move-major-heading-down)
    (define-key map (kbd "C-c M-v") #'prot-outline-move-major-heading-up)
    (define-key map (kbd "M-<up>") #'prot-outline-move-major-heading-up)
    (define-key map (kbd "C-x n s") #'prot-outline-narrow-to-subtree))
  (define-key global-map (kbd "<f10>") #'prot-outline-minor-mode-safe))

These are the contents of the prot-outline.el library (find the file in my dotfiles' repo (as with all my Elisp code)):

;;; prot-outline.el --- Extend outline.el for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Extensions to the built-in `outline.el' library for my Emacs
;; configuration: <https://protesilaos.com/emacs/dotemacs/>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'outline)
(require 'prot-common)

(defgroup prot-outline ()
  "Tweaks for Outline mode."
  :group 'outline)

;;;; Commands

;;;###autoload
(defun prot-outline-move-major-heading-down (&optional arg)
  "Move Outline major heading down one or, optionally, ARG times.
A major heading is one that has subheadings."
  (interactive "p")
  (if (or (and (outline-on-heading-p) (outline-has-subheading-p))
          (outline-invisible-p))
      (outline-move-subtree-down (or arg 1))
    (forward-line (or arg 1))))

;;;###autoload
(defun prot-outline-move-major-heading-up (&optional arg)
  "Move Outline major heading up one or, optionally, ARG times.
A major heading is one that has subheadings."
  (interactive "p")
  (if (or (and (outline-on-heading-p) (outline-has-subheading-p))
          (outline-invisible-p))
      (outline-move-subtree-up (or arg 1))
    (forward-line (- (or arg 1)))))

;;;###autoload
(defun prot-outline-narrow-to-subtree ()
  "Narrow to current Outline subtree."
  (interactive)
  (let ((start)
        (end)
        (point (point)))
    (when (and (prot-common-line-regexp-p 'empty)
               (not (eobp)))
      (forward-char 1))
    (when (or (outline-up-heading 1)
              (outline-back-to-heading))
      (setq start (point)))
    (if (outline-get-next-sibling)
        (forward-line -1)
      (goto-char (point-max)))
    (setq end (point))
    (narrow-to-region start end)
    (goto-char point)))

;;;; Minor mode setup

(defcustom prot-outline-headings-per-mode
  '((emacs-lisp-mode . ";\\{3,\\}+ [^\n]"))
  "Alist of major modes with `outline-regexp' values."
  :type '(alist :key-type symbol :value-type string)
  :group 'prot-outline)

(defcustom prot-outline-major-modes-blocklist
  '(org-mode outline-mode markdown-mode)
  "Major modes where Outline-minor-mode should not be enabled."
  :type '(repeat symbol)
  :group 'prot-outline)

;;;###autoload
(defun prot-outline-minor-mode-safe ()
  "Test to set variable `outline-minor-mode' to non-nil."
  (interactive)
  (let* ((blocklist prot-outline-major-modes-blocklist)
         (mode major-mode)
         (headings (alist-get mode prot-outline-headings-per-mode)))
    (when (derived-mode-p (car (member mode blocklist)))
      (error "Don't use `prot-outline-minor-mode' with `%s'" mode))
    (if (null outline-minor-mode)
        (progn
          (when (derived-mode-p mode)
            (setq-local outline-regexp headings))
          (outline-minor-mode 1)
          (message "Enabled `outline-minor-mode'"))
      (outline-minor-mode -1)
      (message "Disabled `outline-minor-mode'"))))

(provide 'prot-outline)
;;; prot-outline.el ends here

6.10. Cursor and mouse settings

6.10.1. Cursor appearance and tweaks (prot-cursor.el)

prot-cursor.el defines a set of presets that control the overall style of the cursor. There presuppose the blink-cursor-mode. The properties we pass to those presets mirror those we can pass for the various built-in variables for the cursor. Check the prot-cursor.el code, which is reproduced right after the following package configurations.

;;; Cursor appearance and tweaks (prot-cursor.el)
(prot-emacs-builtin-package 'prot-cursor
  (setq prot-cursor-presets
        '((bar . ( :cursor-type (bar . 2)
                   :cursor-no-selected hollow
                   :blinks 10
                   :blink-interval 0.5
                   :blink-delay 0.2))

          (box . ( :cursor-type box
                   :cursor-no-selected hollow
                   :blinks 10
                   :blink-interval 0.5
                   :blink-delay 0.2))

          (underscore . ( :cursor-type (hbar . 3)
                          :cursor-no-selected hollow
                          :blinks 50
                          :blink-interval 0.2
                          :blink-delay 0.2))))
  (setq prot-cursor-last-state-file
        (locate-user-emacs-file "prot-cursor-last-state"))

  (prot-cursor-restore-last-preset)

  ;; Sets my style on startup.
  (if prot-cursor--recovered-preset
      (prot-cursor-set-cursor prot-cursor--recovered-preset)
    (prot-cursor-set-cursor 'underscore))

  ;; The other side of `prot-cursor-restore-last-preset'.
  (add-hook 'kill-emacs-hook #'prot-cursor-store-last-preset)

  ;; We have to use the "point" mnemonic, because C-c c is for
  ;; `org-capture'.
  (define-key global-map (kbd "C-c p") #'prot-cursor-set-cursor))

This is prot-cursor.el (part of my dotfiles' repo):

;;; prot-cursor.el --- Extensions for the cursor -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; Extensions for the cursor, intended for my Emacs setup:
;; <https://protesilaos.com/emacs/dotemacs/>.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(defgroup prot-cursor ()
  "Tweaks for cursor appearance."
  :group 'cursor)

(defcustom prot-cursor-presets
  '((bar . ( :cursor-type (bar . 2)
             :cursor-no-selected hollow
             :blinks 10
             :blink-interval 0.5
             :blink-delay 0.2))

    (box  . ( :cursor-type box
              :cursor-no-selected hollow
              :blinks 10
              :blink-interval 0.5
              :blink-delay 0.2))

    (underscore . ( :cursor-type (hbar . 3)
                    :cursor-no-selected hollow
                    :blinks 50
                    :blink-interval 0.2
                    :blink-delay 0.2)))
  "Alist of desired typeface properties for Blink-cursor-mode.

The car of each cons cell is an arbitrary key that broadly
describes the display type.

The cdr is a plist that specifies the cursor type and blink
properties."
  :group 'prot-cursor
  :type 'alist)

(defcustom prot-cursor-last-state-file
  (locate-user-emacs-file "prot-cursor-last-state")
  "File to save the value of `prot-cursor-set-cursor'."
  :type 'file
  :group 'prot-cursor)

(defvar prot-cursor--style-hist '()
  "History of inputs for display-related font associations.")

(defun prot-cursor--set-cursor-prompt ()
  "Promp for font set (used by `prot-cursor-set-cursor')."
  (let ((def (nth 1 prot-cursor--style-hist)))
    (completing-read
     (format "Select cursor STYLE [%s]: " def)
     (mapcar #'car prot-cursor-presets)
     nil t nil 'prot-cursor--style-hist def)))

;;;###autoload
(defun prot-cursor-set-cursor (style)
  "Set cursor preset associated with STYLE.

STYLE is a symbol that represents the car of a cons cell in
`prot-cursor-presets'."
  (interactive (list (prot-cursor--set-cursor-prompt)))
  (when (or (eq style t) (null style))
    (setq style 'box))
  (let* ((styles (if (stringp style) (intern style) style))
         (properties (alist-get styles prot-cursor-presets))
         (type (plist-get properties :cursor-type))
         (type-no-select (plist-get properties :cursor-no-selected))
         (blinks (plist-get properties :blinks))
         (blink-interval (plist-get properties :blink-interval))
         (blink-delay (plist-get properties :blink-delay)))
    (setq-default cursor-type type
                  cursor-in-non-selected-windows type-no-select
                  blink-cursor-blinks blinks
                  blink-cursor-interval blink-interval
                  blink-cursor-delay blink-delay)
    (add-to-history 'prot-cursor--style-hist (format "%s" style))))

(defun prot-cursor-store-last-preset ()
  "Write latest cursor state to `prot-cursor-last-state-file'.
Can be assigned to `kill-emacs-hook'."
  (when prot-cursor--style-hist
    (with-temp-file prot-cursor-last-state-file
      (insert (concat ";; Auto-generated file;"
                      " don't edit -*- mode: lisp-data -*-\n"))
      (pp (intern (car prot-cursor--style-hist)) (current-buffer)))))

(defvar prot-cursor--recovered-preset nil
  "Recovered value of last store cursor preset.")

(defun prot-cursor-restore-last-preset ()
  "Restore last cursor style."
  (when-let ((file prot-cursor-last-state-file))
    (when (file-exists-p file)
      (setq prot-cursor--recovered-preset
            (unless (zerop
                     (or (file-attribute-size (file-attributes file))
                         0))
              (with-temp-buffer
                (insert-file-contents file)
                (read (current-buffer))))))))

(provide 'prot-cursor)
;;; prot-cursor.el ends here

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

tear-off-window places the current window in a new frame. On my generic mouse, <mouse-3> is the right click. Normally I call that command with M-x, though it does not hurt to rely on the mouse from time to time.

;;; Mouse wheel behaviour
(prot-emacs-builtin-package 'mouse
  ;; 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)
  (add-hook 'after-init-hook #'mouse-wheel-mode)
  (define-key global-map (kbd "C-M-<mouse-3>") #'tear-off-window))

6.10.3. 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 the point will stay at the top/bottom of the screen while moving in that direction (use C-l to reposition it).

The next-screen-context-lines ensures that when moving by screenfulls (with either C-v or M-v) we do not get any continuation lines from the previous screen. I find this more intuitive.

;;; Scrolling behaviour
(setq-default scroll-preserve-screen-position t)
(setq-default scroll-conservatively 1) ; affects `scroll-step'
(setq-default scroll-margin 0)
(setq-default next-screen-context-lines 0)

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

;;; Delete selection
(prot-emacs-builtin-package 'delsel
  (add-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.

6.10.5. 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.
;;; Tooltips (tooltip-mode)
(prot-emacs-builtin-package 'tooltip
  (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)))
  (add-hook 'after-init-hook #'tooltip-mode))

6.11. Dired-like list for registers (rlist)

This package by Sévère Durand implements a Dired-like interface for the purpose of deleting an entry from the list. I feel this is the one major feature that is missing from the registers' toolkit: it provides you with the means to remain in control of the data you accumulate in those compartments, so nothing ever gets out of hand.

Note that this is a fairly new project (as of 2021-02-05), so things are likely to change over the near term.

;;; Dired-like list for registers (rlist)
;; Project repo: <https://gitlab.com/mmemmew/rlist>.  This is one of the
;; packages I handle manually via git, at least until it becomes
;; available through an ELPA.
;;
;; `prot-emacs-manual-package' is defined in my init.el
(prot-emacs-manual-package 'rlist
  (setq rlist-expert t)
  (setq rlist-verbose t)
  (let ((map global-map))
    (define-key map (kbd "C-x r <backspace>") #'rlist-list-registers)
    (define-key map (kbd "C-x r <delete>") #'rlist-list-registers)))

6.12. Alternating background highlights (stripes.el)

With stripes-mode enabled, we get alternating backgrounds across the buffer. Those do not override the foreground colours, so they are easy to integrate with other parts of Emacs. Stripes are useful in long, text-heavy listings, such as those produced by the Elfeed package (Elfeed feed reader, prot-elfeed.el and prot-elfeed-bongo.el) or Notmuch (Notmuch (mail indexer and mail user agent (MUA))). Search this document for places where I enable stripes-mode.

;;; Alternating background highlights (stripes.el)
(prot-emacs-elpa-package 'stripes
  (setq stripes-unit 1))

6.13. Conveniences and minor extras

6.13.1. Automatic time stamps for files (time-stamp.el)

The built-in time-stamp.el provides the means to automatically update a predefined header with the time its file was last edited/saved. This is, in my experience, particularly useful for packages that have a stable version but also need to disambiguate their latest tagged release from their current development state.

By default, this is done by checking the first eight lines of the file for the Time-stamp: <> entry. Though that is configurable. To properly utilise this library, you need to implement file-local variables. Those should control the exact format of the time stamp. For examples, this is what I have in my modus-themes.el:

;; Local Variables:
;; time-stamp-start: "Last-Modified:[ \t]+\\\\?[\"<]"
;; time-stamp-end: "\\\\?[\">]"
;; time-stamp-format: "%Y-%02m-%02d %02H:%02M:%02S %5z"
;; End:

Check M-x find-library RET time-stamp RET for all variables you may want to control.

All we do here is enable the package and add a hook to insert a time stamp upon save, where relevant. It works seamlessly.

;;; Automatic time stamps for files (time-stamp.el)
(prot-emacs-builtin-package 'time-stamp
  (add-hook 'before-save-hook #'time-stamp))

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

;;; Auto revert mode
(prot-emacs-builtin-package 'autorevert
  (setq auto-revert-verbose t)
  (add-hook 'after-init-hook #'global-auto-revert-mode))

6.13.3. 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:

;;; Preserve contents of system clipboard
(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).

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

;;; Newline characters for file ending
(setq mode-require-final-newline 'visit-save)

6.13.5. Zap characters

There are two kinds of "zap" functionality:

  • zap-up-to-char will delete everything from point up to the character you provide it with. Think of how you may want to remove a file name but keep its file type extension, so M-x zap-up-to-char RET . will do just that.
  • zap-to-char, which is bound to M-z by default, will delete the target character as well.

I bind the former to M-z as I use it more often and leave its counterpart on M-Z (M-S-z).

;;; Zap characters
(let ((map global-map))
  (define-key map (kbd "M-z") #'zap-up-to-char)
  (define-key map (kbd "M-Z") #'zap-to-char)) ; M-S-z

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

;;; Package lists
(prot-emacs-builtin-package 'package
  ;; 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)
  (add-hook 'package-menu-mode-hook #'hl-line-mode))

7. Language settings for prose and code

This section is all about configurations and/or packages that deal with natural or programming language enhancements.

7.1. Support for various major modes

These provide syntax highlighting and additional features for environments that are not already supported by Emacs.

7.1.1. Plain text (text-mode with prot-text.el)

My prot-text.el (copied verbatim further below) is meant to provide a set of extensions for the built-in text-mode.el. Currently there is only one command:

  • prot-text-insert-heading lets you add a heading delimiter to the line at point. The length of the delimiter is equal to that of the line. By default, the delimiter consists of hyphens, but with a C-u prefix argument those are substituted for equals signs.

I used to have another two in this file, which have now (2021-04-13) been moved to prot-simple.el. Those are prot-simple-cite-region and prot-simple-insert-undercaret.

Refer to the source code in Common custom functions (prot-simple.el).

;;; Plain text (text-mode with prot-text.el)
(prot-emacs-builtin-package 'text-mode)

(prot-emacs-builtin-package 'prot-text
  (add-to-list 'auto-mode-alist '("\\(README\\|CHANGELOG\\|COPYING\\|LICENSE\\)$" . text-mode))
  (define-key text-mode-map (kbd "<M-return>") #'prot-text-insert-heading)
  (define-key org-mode-map (kbd "<M-return>") #'org-meta-return) ; don't override M-RET here
  (define-key org-mode-map (kbd "M-;") nil))

This is prot-text.el (find the file in my dotfiles' repo (as with all my Elisp code)):

;;; prot-text.el --- Extensions to text-mode.el for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; This covers my text-mode.el extensions, for use in my Emacs setup:
;; https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'prot-common)
(require 'prot-simple)

;;;###autoload
(defun prot-text-insert-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
compatible derivatives, such as Markdown mode, though not Org
mode which has its own conventions."
  (interactive "P")
  (cond
   ((derived-mode-p 'outline-mode)
    (user-error "Do not use `prot-text-insert-heading' in `outline-mode' or derivatives!"))
   ((derived-mode-p 'text-mode)
    (let* ((num (- (point-at-eol) (point-at-bol)))
           (char (string-to-char (if arg "=" "-"))))
      (cond
       ((and (eobp)
             (or (prot-common-line-regexp-p 'list 1)
                 (prot-common-line-regexp-p 'heading 1)
                 (prot-common-line-regexp-p 'empty 1)
                 (prot-common-line-regexp-p 'indent 1)))
        (newline 1))
       ((or (prot-common-line-regexp-p 'empty 1)
            (prot-common-line-regexp-p 'indent 1))
        (prot-simple-new-line-below))
       ((or (prot-common-line-regexp-p 'list 1)
            (prot-common-line-regexp-p 'heading 2))
        (if (prot-common-line-regexp-p 'empty 3)
            (beginning-of-line 3)
          (prot-simple-new-line-below)))
       ((or (prot-common-line-regexp-p 'empty 2)
            (prot-common-line-regexp-p 'indent 2))
        (prot-simple-new-line-below)
        (insert (make-string num char))
        (newline 1)
        (beginning-of-line 2))
       (t
        (prot-simple-new-line-below)
        (insert (make-string num char))
        (newline 2)))))))

(provide 'prot-text)
;;; prot-text.el ends here

7.1.2. Markdown (markdown-mode)

I edit lots of Markdown files. This makes things easier.

;;; Markdown (markdown-mode)
(prot-emacs-elpa-package 'markdown-mode
  (add-to-list 'auto-mode-alist '("\\.md$" . markdown-mode))
  (setq markdown-fontify-code-blocks-natively t))
;; Allows for fenced block focus with C-c ' (same as Org blocks).
(prot-emacs-elpa-package 'edit-indirect)

7.1.3. YAML (yaml-mode)

This adds support for YAML files.

;;; YAML (yaml-mode)
(prot-emacs-elpa-package 'yaml-mode
  (add-to-list 'auto-mode-alist '("\\.ya?ml$" . yaml-mode)))

7.1.4. CSS (css-mode)

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

;;; CSS (css-mode)
(prot-emacs-builtin-package 'css-mode
  (add-to-list 'auto-mode-alist '("\\.css$" . css-mode))
  (add-to-list 'auto-mode-alist '("\\.scss$" . scss-mode))
  (setq css-fontify-colors nil))

7.1.5. Shell scripts (sh-mode)

sh-mode.el is another built-in mode that targets shell scripts. I think it works well out-of-the-box, even though it provides lots of configuration options to further control its behaviour.

All I want here is to enable sh-mode in various files that are not obvious shell scripts, like Arch Linux's package recipes. As such, the value assigned to auto-mode-alist will be expanded each time I identify such a file.

;;; Shell scripts (sh-mode)
(prot-emacs-builtin-package 'sh-script
  (add-to-list 'auto-mode-alist '("PKGBUILD" . sh-mode)))

7.2. Paragraphs and fill-mode (prot-fill.el)

The prot-fill.el library (reproduced below) is a tiny wrapper around some Emacs settings and modes that are scrattered around several files, which control (i) how paragraphs or comments in programming modes should be wrapped to a given column count, and (ii) what constitutes a sentence. I put them all together here to make things easier to track.

  • With regard to paragraphs, I find that a double space is the best way to delimit sentences in source form, where a monospaced typeface is customary. 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 phrases easier to tell apart, but also to render unambiguous commands like forward-sentence.
  • prot-fill-fill-mode sets my desired default column width for all buffers, while it applies another value for programming modes (in case there is a need to control the two cases separately). Those values are stored in the variables prot-fill-default-column and prot-fill-prog-mode-column respectively. My minor mode also enables auto-fill-mode in text-mode and prog-mode buffers through the appropriate hooks. Disabling prot-fill-fill-mode will remove all those customisations.

Note that Common custom functions (prot-simple.el) contains some commands related to auto-fill. Besides, you can always do it manually for the current paragraph or the active region with M-x fill-paragraph, bound by default to M-q.

;;; Paragraphs and fill-mode (prot-fill.el)
(prot-emacs-builtin-package 'prot-fill
  (setq prot-fill-default-column 72)
  (setq prot-fill-prog-mode-column 72)  ; Set this to another value if you want
  ;; Those variables come from various sources, though they feel part of the
  ;; same conceptual framework.
  (setq sentence-end-double-space t)
  (setq sentence-end-without-period nil)
  (setq colon-double-space nil)
  (setq use-hard-newlines nil)
  (setq adaptive-fill-mode t)
  (prot-fill-fill-mode 1)
  (add-hook 'after-init-hook #'column-number-mode))

These are the contents of prot-fill.el (part of my dotfiles' repo):

;;; prot-fill.el --- Minor fill-mode tweaks for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; This covers my fill-mode extensions, for use in my Emacs setup:
;; https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(defgroup prot-fill ()
  "Tweak for filling paragraphs."
  :group 'fill)

(defcustom prot-fill-default-column 72
  "Default width for `fill-column'."
  :type 'integer
  :group 'prot-fill)

(defcustom prot-fill-prog-mode-column 80
  "`prog-mode' width for `fill-column'.
Also see `prot-fill-default-column'."
  :type 'integer
  :group 'prot-fill)

(defun prot-fill--fill-prog ()
  "Set local value of `fill-column' for programming modes.
Meant to be called via `prog-mode-hook'."
  (setq-local fill-column prot-fill-prog-mode-column))

;;;###autoload
(define-minor-mode prot-fill-fill-mode
  "Set up fill-mode and relevant variable."
  :init-value nil
  :global t
  (if prot-fill-fill-mode
      (progn
        (setq-default fill-column prot-fill-default-column)
        (add-hook 'prog-mode-hook #'prot-fill--fill-prog)
        (add-hook 'text-mode-hook #'turn-on-auto-fill))
    (setq-default fill-column 70)
    (remove-hook 'prog-mode-hook #'prot-fill--fill-prog)
    (remove-hook 'text-mode-hook #'turn-on-auto-fill)))

(provide 'prot-fill)
;;; prot-fill.el ends here

7.3. Comments (newcomment.el and prot-comment.el)

The built-in newcomment.el library offers several useful commands for working with comments in source code. While my prot-comment.el (reproduced after the package configurations) adds some simple extras.

The intent of my configurations here is straightforward: re-configure key bindings to make the most common action easier to access and then arrange the rest in a meaningful way, while also setting up the appropriate variables.

The most common action is the command prot-comment-comment-dwim which is bound to C-;. Note that C-; is normally occupied by some flyspell command (disabled in Flyspell and prot-spell.el (spell check)). Compare that keybinding to the one I have for the much more specialised prot-comment-timestamp-keyword: C-x C-;. What those commands do is documented in their docstrings, so please check the code below.

;;; Comments (newcomment.el and prot-comment.el)
(prot-emacs-builtin-package 'newcomment
  (setq comment-empty-lines t)
  (setq comment-fill-column nil)
  (setq comment-multi-line t)
  (setq comment-style 'multi-line)
  (let ((map global-map))
    (define-key map (kbd "C-:") #'comment-kill)         ; C-S-;
    (define-key map (kbd "M-;") #'comment-indent)))

(prot-emacs-builtin-package 'prot-comment
  (setq prot-comment-comment-keywords
        '("TODO" "NOTE" "XXX" "REVIEW" "FIXME"))
  (setq prot-comment-timestamp-format-concise "%F")
  (setq prot-comment-timestamp-format-verbose "%F %T %z")
  (let ((map global-map))
    (define-key map (kbd "C-;") #'prot-comment-comment-dwim)
    (define-key map (kbd "C-x C-;") #'prot-comment-timestamp-keyword)))

And here is prot-comment.el (part of my dotfiles' repo):

;;; prot-comment.el --- Extensions newcomment.el for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; This covers my newcomment.el extras, for use in my Emacs setup:
;; https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'prot-common)

(defgroup prot-comment ()
  "Extensions for newcomment.el."
  :group 'comment)

(defcustom prot-comment-comment-keywords
  '("TODO" "NOTE" "XXX" "REVIEW" "FIXME")
  "List of strings with comment keywords."
  :type '(repeat string)
  :group 'prot-comment)

(defcustom prot-comment-timestamp-format-concise "%F"
  "Specifier for date in `prot-comment-timestamp-keyword'.
Refer to the doc string of `format-time-string' for the available
options."
  :type 'string
  :group 'prot-comment)

(defcustom prot-comment-timestamp-format-verbose "%F %T %z"
  "Like `prot-comment-timestamp-format-concise', but longer."
  :type 'string
  :group 'prot-comment)

;;;###autoload
(defun prot-comment-comment-dwim (arg)
  "Flexible, do-what-I-mean commenting.

If region is active and ARG is either a numeric argument greater
than one or a universal prefix (\\[universal-argument]), then
apply `comment-kill' on all comments in the region.

If the region is active and no ARG is supplied, or is equal to a
numeric prefix of 1, then toggle the comment status of the region.

Else toggle the comment status of the line at point.  With a
numeric prefix ARG, do so for ARGth lines (negative prefix
operates on the lines before point)."
  (interactive "p")
  (cond
   ((and (> arg 1) (use-region-p))
    (let* ((beg (region-beginning))
           (end (region-end))
           (num (count-lines beg end)))
      (save-excursion
        (goto-char beg)
        (comment-kill num))))
   ((use-region-p)
    (comment-or-uncomment-region (region-beginning) (region-end)))
   (t
    (save-excursion (comment-line (or arg 1))))))

(defvar prot-comment--keyword-hist '()
  "Input history of selected comment keywords.")

(defun prot-comment--keyword-prompt (keywords)
  "Prompt for candidate among KEYWORDS."
  (let ((def (car prot-comment--keyword-hist)))
    (completing-read
     (format "Select keyword [%s]: " def)
     keywords nil nil nil 'prot-comment--keyword-hist def)))

;;;###autoload
(defun prot-comment-timestamp-keyword (keyword &optional verbose)
  "Add timestamped comment with KEYWORD.

When called interactively, the list of possible keywords is that
of `prot-comment-comment-keywords', though it is possible to
input arbitrary text.

If point is at the beginning of the line or if line is empty (no
characters at all or just indentation), the comment is started
there in accordance with `comment-style'.  Any existing text
after the point will be pushed to a new line and will not be
turned into a comment.

If point is anywhere else on the line, the comment is indented
with `comment-indent'.

The comment is always formatted as 'DELIMITER KEYWORD DATE:',
with the date format being controlled by the variable
`prot-comment-timestamp-format-concise'.

With optional VERBOSE argument (such as a prefix argument
`\\[universal-argument]'), use an alternative date format, as
specified by `prot-comment-timestamp-format-verbose'."
  (interactive
   (list
    (prot-comment--keyword-prompt prot-comment-comment-keywords)
    current-prefix-arg))
  (let* ((date (if verbose
                   prot-comment-timestamp-format-verbose
                 prot-comment-timestamp-format-concise))
         (string (format "%s %s: " keyword (format-time-string date)))
         (beg (point)))
    (cond
     ((or (eq beg (point-at-bol))
          (prot-common-line-regexp-p 'empty))
      (let* ((maybe-newline (unless (prot-common-line-regexp-p 'empty 1) "\n")))
        ;; NOTE 2021-07-24: we use this `insert' instead of
        ;; `comment-region' because of a yet-to-be-determined bug that
        ;; traps `undo' to the two states between the insertion of the
        ;; string and its transformation into a comment.
        (insert
         (concat comment-start
                 ;; NOTE 2021-07-24: See function `comment-add' for
                 ;; why we need this.
                 (make-string
                  (comment-add nil)
                  (string-to-char comment-start))
                 comment-padding
                 string
                 comment-end))
        (indent-region beg (point))
        (when maybe-newline
          (save-excursion (insert maybe-newline)))))
     (t
      (comment-indent t)
      (insert (concat " " string))))))

(provide 'prot-comment)
;;; prot-comment.el ends here

7.4. 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.
;;; Configure 'electric' behaviour
(prot-emacs-builtin-package 'electric
  (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)
  (electric-indent-mode 1)
  (electric-pair-mode -1)
  (electric-quote-mode -1))

7.5. 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.
;;; Parentheses (show-paren-mode)
(prot-emacs-builtin-package 'paren
  (setq show-paren-style 'parenthesis)
  (setq show-paren-when-point-in-periphery nil)
  (setq show-paren-when-point-inside-paren nil)
  (add-hook 'after-init-hook #'show-paren-mode))

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

;;; Tabs, indentation, and the TAB key
(setq-default tab-always-indent 'complete)
(setq-default tab-first-completion 'word-or-paren-or-punct) ; Emacs 27
(setq-default tab-width 4)
(setq-default indent-tabs-mode nil)

7.7. Flyspell and prot-spell.el (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 is distracting.

My workflow is to call an interactive command to perform spell checking. This is prot-spell-spell-dwim, which is part of my prot-spell.el library (reproduced after the following package configurations). What it does is search for errors in the active region or, if that does not apply, operate on the word at point. Its fallback condition is a call to prot-spell-change-dictionary, which I use to switch between languages using minibuffer completion.

Also bear in mind that the key binding C-; that Flyspell uses by default is disabled because I re-purpose it for a faster version of C-x C-; (much more useful for my work—see the section on comments).

;;; Flyspell and prot-spell.el (spell check)
(prot-emacs-builtin-package 'flyspell
  (setq flyspell-issue-message-flag nil)
  (setq flyspell-issue-welcome-flag nil)
  (setq ispell-program-name "aspell")
  (setq ispell-dictionary "en_GB")
  (define-key flyspell-mode-map (kbd "C-;") nil))

(prot-emacs-builtin-package 'prot-spell
  (setq prot-spell-dictionaries
        '(("EN English" . "en")
          ("EL Ελληνικά" . "el")
          ("FR Français" . "fr")
          ("ES Espanõl" . "es")))
  (let ((map global-map))
    (define-key map (kbd "M-$") #'prot-spell-spell-dwim)
    (define-key map (kbd "C-M-$") #'prot-spell-change-dictionary)))

This is prot-spell.el (part of my dotfiles' repo):

;;; prot-spell.el --- Spelling-related extensions for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program 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 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:
;;
;; This covers my spelling-related extensions, for use in my Emacs
;; setup: https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(defgroup prot-spell ()
  "Extensions for ispell and flyspell."
  :group 'ispell)

(defcustom prot-spell-dictionaries
  '(("EN English" . "en")
    ("EL Ελληνικά" . "el")
    ("FR Français" . "fr")
    ("ES Espanõl" . "es"))
  "Alist of strings with descriptions and dictionary keys.
Used by `prot-spell-change-dictionary'."
  :type 'alist
  :group 'prot-spell)

(defvar prot-spell--dictionary-hist '()
  "Input history for `prot-spell-change-dictionary'.")

(defun prot-spell--dictionary-prompt ()
  "Helper prompt to select from `prot-spell-dictionaries'."
  (let ((def (car prot-spell--dictionary-hist)))
    (completing-read
     (format "Select dictionary [%s]: " def)
     (mapcar #'car prot-spell-dictionaries)
     nil t nil 'prot-spell--dictionary-hist def)))

;;;###autoload
(defun prot-spell-change-dictionary (dictionary)
  "Select a DICTIONARY from `prot-spell-dictionaries'."
  (interactive
   (list (prot-spell--dictionary-prompt)))
  (let* ((key (cdr (assoc dictionary prot-spell-dictionaries)))
         (desc (car (assoc dictionary prot-spell-dictionaries))))
    (ispell-change-dictionary key)
    (message "Switched dictionary to %s" (propertize desc 'face 'bold))))

(autoload 'flyspell-region "flyspell")
(autoload 'thing-at-point "thingatpt")
(autoload 'ispell-word "ispell")

;;;###autoload
(defun prot-spell-spell-dwim (beg end)
  "Spellcheck between BEG END, current word, or select dictionary.

Use `flyspell-region' on the active region.  With point over a
word and no active region invoke `ispell-word'.  Else call
`prot-spell-change-dictionary'."
  (interactive "r")
  (cond
   ((use-region-p)
    (flyspell-region beg end))
   ((thing-at-point 'word)
    (call-interactively 'ispell-word))
   (t
    (call-interactively 'prot-spell-change-dictionary))))

(provide 'prot-spell)
;;; prot-spell.el ends here

7.8. Code and text linters

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

The external flymake-diagnostic-at-point package provides a simple and effective interface to displaying information about the warning at point.

;;; Flymake
(prot-emacs-builtin-package 'flymake
  (setq flymake-fringe-indicator-position 'left-fringe)
  (setq flymake-suppress-zero-counters t)
  (setq flymake-start-on-flymake-mode t)
  (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)
  (setq flymake-mode-line-format
        '("" flymake-mode-line-exception flymake-mode-line-counters))
  (setq flymake-mode-line-counter-format
        '(" " flymake-mode-line-error-counter
          flymake-mode-line-warning-counter
          flymake-mode-line-note-counter ""))

  (let ((map flymake-mode-map))
    (define-key map (kbd "C-c ! s") #'flymake-start)
    (define-key map (kbd "C-c ! d") #'flymake-show-buffer-diagnostics) ; Emacs28
    (define-key map (kbd "C-c ! n") #'flymake-goto-next-error)
    (define-key map (kbd "C-c ! p") #'flymake-goto-prev-error)))

(prot-emacs-elpa-package 'flymake-diagnostic-at-point
  (setq flymake-diagnostic-at-point-display-diagnostic-function
        'flymake-diagnostic-at-point-display-minibuffer))
7.8.1.1. Flymake + Shellcheck

The flymake-shellcheck package simply adds support for Shellcheck, the linter for shell scripts. It otherwise relies on the standard utilities of flymake-mode (see main Flymake configs).

;;; Flymake + Shellcheck
(prot-emacs-elpa-package 'flymake-shellcheck
  (add-hook 'sh-mode-hook 'flymake-shellcheck-load))
7.8.1.2. 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 (check my configurations for Flymake and then also review what I have to spelling in Flyspell and prot-spell.el (spell check)).

To run the program, you just need to hook it to whatever major-mode you use for prose. 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.

;;; Flymake + Proselint
(prot-emacs-elpa-package 'flymake-proselint
  (add-hook 'markdown-mode-hook #'flymake-proselint-setup)
  (add-hook 'org-mode-hook #'flymake-proselint-setup)
  (add-hook 'text-mode-hook #'flymake-proselint-setup))
7.8.1.2.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
    }
}

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

;;; Elisp packaging requirements
(prot-emacs-elpa-package 'package-lint-flymake
  (add-hook 'flymake-diagnostic-functions #'package-lint-flymake))

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

;;; Eldoc (elisp live documentation feedback)
(prot-emacs-builtin-package 'eldoc
  (global-eldoc-mode 1))

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

;;; Tools for manual pages (manpages)
(prot-emacs-builtin-package 'man
  (let ((map Man-mode-map))
    (define-key map (kbd "i") #'Man-goto-section)
    (define-key map (kbd "g") #'Man-update-manpage)))

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

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

;;; Emacs server and desktop
(prot-emacs-builtin-package 'server
  (add-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 what Emacs calls the desktop, which is an amalgamation of data such as the buffer list, visited files, and some important data like the content of registers (for various histories or other data see the following sections on recording various types of history).

Because I can always access files I have visited before using completion (see Completion for recent files and directories (prot-recentf.el)), I opt to discard all files and buffers from the desktop. Instead, I use it to restore everything in the variable desktop-globals-to-save.

(prot-emacs-builtin-package 'desktop
  (setq desktop-auto-save-timeout 300)
  (setq desktop-path `(,user-emacs-directory))
  (setq desktop-base-file-name "desktop")
  (setq desktop-files-not-to-save ".*")
  (setq desktop-buffers-not-to-save ".*")
  (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)
  (dolist (symbol '(kill-ring log-edit-comment-ring))
    (add-to-list 'desktop-globals-to-save symbol))

  (desktop-save-mode 1))

8.2. Record various types of history

8.2.1. 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 read the Minibuffer configurations and my extras (mct.el).

;;; Minibuffer history (savehist-mode)
(prot-emacs-builtin-package 'savehist
  (setq savehist-file (locate-user-emacs-file "savehist"))
  (setq history-length 10000)
  (setq history-delete-duplicates t)
  (setq savehist-save-minibuffer-history t)
  (add-hook 'after-init-hook #'savehist-mode))

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

;;; Record cursor position
(prot-emacs-builtin-package 'saveplace
  (setq save-place-file (locate-user-emacs-file "saveplace"))
  (setq save-place-forget-unreadable-files t)
  (save-place-mode 1))

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

;;; Backups
(setq backup-directory-alist
      `(("." . ,(concat user-emacs-directory "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)

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

9.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 (describe-function) allows you to search for documentation about functions.
  • C-h v (describe-variable) is the same for variables.
  • C-h o (describe-symbol) 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 (describe-key) 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.
  • C-h w (where-is) asks you for the symbol of a command and tells you to which key binding it is assigned to.
  • C-h l (view-lossage) produces a log with your most recent key presses and the commands they call. Emacs calls this the "lossage". Ever mistyped something and got to the wrong place? Use this as an opportunity to learn and, perhaps, a way to identify key sequences you would like to modify. (pro tip: you can edit/convert your lossage into a keyboard macro with C-x C-k l—also watch Edit keyboard macros (2020-03-14)).

In the above scenaria we see how C-h is used as a prefix: you are starting a key sequence with it. Now 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 combinations 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 displays the symbol's 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 followed by the name you are looking for. Those are called "features", by the way, and each library declares them using the provide form at the end of the file (so when you use require you pass a quoted feature symbol).

Help commands that ask you for a symbol to input can also be aware of the context of the point (the cursor). 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.

While browsing Elisp source code, there are two commands that can be of great help to navigate definitions. xref-find-definitions, bound to M-. will take you to the definition of the symbol at point. While its counterpart, the xref-pop-marker-stack (M-,), will bring you back to where you where before. Similarly, the built-in Isearch tool can search for the symbol at point with M-s . (the isearch-forward-symbol-at-point command), which can then be followed up with M-s o to produce an "Occur" buffer with all the results—use that as an index to move around (also check: Isearch, occur, and extras (prot-search.el)).

Finally, you should practice C-h m (describe-mode) as much as possible. This is the help command for getting information about the major mode you are in and for all the minor modes you have active and which are pertinent to the current buffer. 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 (see Dired (directory editor, file manager)).

9.2. Why maintain all those 'prot-*.el' files?

Those contain my custom Elisp code. Several of them provide extensions to existing tools, while others introduce some new functionality. They are written in accordance with the best coding practices and are, for all intents and purposes, regular packages even though they only target my use-case.

The main benefits of packaging my code are thus:

Lexical scope
The code is written in a way that does not introduce implicit dependencies on the environment. Everything within the file has to be known so that the compiler can properly interpret it.
Byte compilation
Because of the above, the code compiles cleanly. This allows me to execute my code a bit faster than it would otherwise be possible. The more I write, the greater the otherwise marginal performance gains.
Transparency
Users who copy my code are made aware of its dependencies, which saves me time answering emails why X or Y from my dotemacs does not work on another's setup.
Modularity
Since my files render their dependencies and bindings explicit, it is easier to catch errors and debug them.

While my prot-*.el are distributed as packages, please understand that I consider this an exercise in programming. I develop them because I believe they will be useful to me. Do not unilaterally put them in some package archive as I cannot promise that I will keep them around forever (distributing a package implies a commitment to its users).

9.3. What is the purpose of "prot/" and "contrib/"?

[Also read: Why maintain all those 'prot-*.el' files?]

The prot/ prefix in some unpackaged symbols works as a namespace that captures all my custom, yet-to-be-reviewed code. These can be functions or variables. The utility of this prefix 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 would 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/). This also means that I can do M-x occur prot/ to produce an Occur buffer with all my symbols (pass a numeric argument to display N lines of context C-u 5 M-s o). From there I can, say, browse them all easily or even edit them using the full array of Emacs' relevant powers (occur's results buffer is made editable with e, but you should be able to find that by using the information documented in How do you learn Emacs?).

The same principles apply to 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 informs 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, a newer version of Emacs, or I may eventually be able to furnish my own alternative.

Again, occur or similar tools will filter those 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.

9.4. Why do you use so many `setq'?

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. I do so for a couple of reasons:

  • It makes each variable easier to copy elsewhere, say, when sharing it online or to an emacs -Q scratch buffer.
  • It is trivial to run C-x C-e (eval-last-sexp) on each balanced expression individually (note that C-M-x (eval-defun) can be used in Elisp buffers to evaluate the expression at point).

There are some cases where I use a single setq to configure several closely related variables (Style B), but those are the exception to the norm.

Style B gives me more problems with copy-pasting, while it does not solve any real issues (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…

9.5. 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 the 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.

By the way, the fastest way to find a package's customisation options is to visit its source code and produce an Occur buffer for its defcustom configurations (which extends the ideas in How do you learn Emacs?).

9.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 study 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 (read my answer to the question Why maintain all those 'prot-*.el' files? as well as the one on 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 the multitude of Emacs' functionalities: there is an entire world of applications outside the narrow confines of editing code.

Since you read this (and the rest of my dotemacs, I presume), also consider two highly valuable blog posts by Karthik Chikmagalur:

9.7. Compare Gnus, Mu4e, Notmuch?

This question is best answered in reference to one's background and the intuitions made possible by it. The short answer is that all three are good in different ways: the best tool for the job is the one that makes you most productive in the moment.

Now to the longer reply, which fleshes out this time-sensitive subjectivity I am hinting at.

Before switching to Emacs in the summer of 2019, I used to use Mutt or Thunderbird as a plan B (starting from mid 2016). At that time I did not receive a lot of email, maybe 2-3 per week or even less, and did not follow any mailing lists. My Mutt setup was just a basic front-end to the IMAP server, meaning that I did not have anything like offlineimap or mbsync set up. My workflow back then was heavily focused on elaborate directories/folders to store the mail I wanted. For example, I had an "archive" directory for each of my three email accounts and inside of that I had yearly archives. To cut the long story short, in order to find an email I had to go looking for it at some exact location. It was not efficient, but I was okay with it as my mail traffic was very low.

Once I became a full-time Emacs user, I decided to push the boundaries of what I could do with email. The most popular option seemed to be Mu4e so I picked that one (autumn 2019). It was easy for me to set up and I liked its middle-of-the-road approach of combining a typical directory-based structure with a potent search tool. I had stopped doing the complex directory structures with my archives and would keep everything in my inbox. Still, the volume of incoming mail was very low: I was not really using the search features and was reverting to the directory views instead.

Thus I decided to go with Gnus (early 2020) whose value proposition seemed to be "directories are first-class citizens here, same for Newsgroup sources". Gnus calls all of them "groups" based on its useful metaphor of treating everything as ephemeral news. What I learnt from my time with Gnus is that:

  1. Mailing lists are worth paying attention to as you can learn a lot from other people's input in various conversations (emacs-devel, bug reports, etc.).
  2. It is good to use mbsync or equivalent as it removes the burden from Emacs and allows you to keep a copy of your mail correspondence. I had started with Gnus' nnimap backend, which communicated directly with the IMAP server, but once my email traffic started to increase, I learnt the hard way that mbsync was better suited for the task, as it did not block Emacs.
  3. Directory-based workflows do not scale. As I started to get more email, I realised that I often needed to quickly retrieve some piece of information that was kept in one of my three inbox directories or some other location. Gnus has search methods but there are inconsistencies between nntp, nnmaildir, etc. to the effect that I would need to have special rules for each case. Maybe I did not read the manual very carefully? Perhaps I did not understand something? At any rate, it did not feel like worth the effort.

The search engine I wanted to use with Gnus was notmuch, because it is mentioned in the Gnus manual. As I was looking for a way to be in full control of my email setup, I thought it would be better to try Notmuch without the layer of abstraction that Gnus added on top of it, stop following a directory-based workflow, and embrace filtering and narrowing (concepts I learnt to appreciate through my daily usage of Emacs).

My few test on the command-line convinced me to refactor my email setup. So I decided to read through all the manpages and then go with notmuch.el (made the switch between 2021-05-13 and 2021-05-14). I did not want to revert to Mu4e because I had the feeling that its hybrid approach to directories and search would push me back to my old inefficient habits. Whereas Notmuch is all about tagging and filtering, which makes me believe that I can push myself into new territory (the configs and docs: Notmuch (mail indexer and mail user agent (MUA))).

This is not to say that any of these tools is inherently better/worse than its peers. You can be productive with all three of them (my old configurations for Mu4e and Gnus can be found in my dotfiles' repo). What matters is to identify your requirements, set your expectations or ambitions, and pick the option that can optimise your productivity. The point is to not be dogmatic about it: just use what works for you the same way I did in through journey to where I currently am.

9.8. Have you tried Gnus for RSS feeds?

NOTE 2021-05-13: This question used to pop up when I was using Gnus as my mail client. Though I have since switched to Notmuch, I think the information provided here remains relevant.

For the relevant setup, refer to the mega-section on Email settings.

I tried to make feeds work with Gnus because I generally like it and wish to extend it further. The issues I encountered are manifold. These qualify as "problems" based on my expectations, so please do not take this as a pretence of objectivity.

  1. The nnrss backend does not support the Atom spec. To make feeds work you need to pipe them through some other script that will convert them to the RSS format. I do not like that, because it puts the burden on the user for something that should work out-of-the-box. The Atom spec has been around for a long time; long enough to expect feed readers to have caught up to the current state of affairs.
  2. The nnrss-opml-import command prompts you twice for every feed. First to confirm it and then to add a name for it. Try that for, say, 200 feeds. Not pretty. Maybe there is some way to bypass this and just do it programmatically. I could not find it. Again, this may be due to my false expectations, where I consider it a basic thing that should "just work" (because while I like customising everything, this is not a substitute for things working well by default).
  3. Fetching feeds blocks Emacs entirely. Especially when the list is long. Again, think of a 200-long list. I am fine with this happening for email when I choose to update things manually, because I only have three accounts and the interruption is barely noticeable. But it otherwise is a show-stopper. This means that you must run the process externally. Fine in principle, though I cannot avoid the fact that Elfeed already has an option for that (also refer to the section Elfeed feed reader, prot-elfeed.el and prot-elfeed-bongo.el).
  4. Updating feeds often produces duplicate items or fetches the same item again, even though it was already read. The solution is to customise nnrss-ignore-article-fields. Its default value is an XML tag that you only learn about if you actually read a relevant feed's source code: "(slash:comments)". So if you want to, say, ignore the published date, you need to check the XML and find the pubDate tag. Fine, I can do that, but it is not a good idea to expect users to know such technicalities.
  5. Gwene is a good compromise in lieu of a genuine solution, namely: proper support for the Atom spec and the option to fetch feed updates through curl/wget (or equivalent). However, I do not like the whole workflow around it. It runs contrary to what I am doing with Emacs: which is to control things from inside of Emacs. Gwene is a website facade and a public server. You go to the website to upload your OPML file. Where is the M-x command for that? And why should a list of decentralised feeds be dependent on a centralised server, anyway? And why should the list be made public? What about private feeds?

Apologies if this sounds too negative. I am sure there are workarounds and there is always a good chance that either I missed something, or I simply am not knowledgeable enough to make Gnus+Feeds work properly. I also wish I had better Elisp skills to help patch those issues in core Emacs, but I must work with what I have.

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

Those key bindings typically 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 are bound to my own custom commands.

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 those 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). For me this is not a problem, because I disable all of the DE's key bindings (also read: What is your distro and/or window manager?).

9.10. How to reproduce your dotemacs?

Remember that I am not using Emacs 27 or some other stable release. I currently am on GNU Emacs 29.0.50.

First you must understand that this is my personal setup: I have never tried to develop a framework that works out-of-the-box for other users. It runs contrary to how I approach Emacs as a long-term investment that involves learning everything from the basics to the more advanced issues: which means starting from scratch while being patient, persistent, and humble.

Furthermore, it is important to understand that the very nature of this setup makes it highly opinionated and, thus, several of its components may be predicated on implicit assumptions about preferences. For example, I only use my Modus themes because that is the design I want to interface with, and will therefore not make any effort whatsoever to provide options that can let someone pick a theme out of the multitude that is on offer: this is not to say that those options are inherently wrong, just that they make no sense in a personal Emacs setup.

As you may know from René Magritte's famous Ceci n'est pas une pipe painting, what you think you are looking at is not equivalent to its actuality. You may be led to believe that my dotemacs is in fact an "Emacs distro", or "starter kit", or whatnot, and that you can just clone it and re-use it right away. In truth ceci n'est pas une distribution Emacs. It is my personal setup.

With those granted, I understand that people may want to benefit from what I already make public and, in turn, I want to help them to that end. It is not my intent to create impediments to one's progress as an Emacs user, nor to obfuscate my otherwise readily available corpus of labour. I wish to make things easy and accessible, without prejudice to the aforementioned points about what this is.

To reproduce my setup, you first need to clone my dotfiles' repository. This includes more stuff than just my Emacs files, though it is what I use. Let's say you plan to clone the repo at ~/Git/prot-dotfiles. You invoke this command from your shell:

git clone https://gitlab.com/protesilaos/dotfiles.git ~/Git/prot-dotfiles

If you do not want to copy the entire history of the project, you can pass the --depth flag, like this:

git clone --depth 1 https://gitlab.com/protesilaos/dotfiles.git ~/Git/prot-dotfiles

That one fetches just the latest commit and is considerably faster. Though the full history is useful if you plan to retrieve some datum from it.

My dotfiles are managed with the help of the GNU Stow program. What that does is create and handle symlinks from a source directory to a destination. The file structure of my dotfiles is designed to reflect the expected end result at the $HOME directory.

Stow operates on what it calls a "package": a set of files whose file structure will be reproduced at the target filesystem path. Take a look at the tree representation of my "emacs package", per Stow's parlance (this output may not be exactly the same you will get depending on when you review it, but that is beside the point).

~/Git/prot-dotfiles $ tree -aF --dirsfirst emacs
emacs
└── .emacs.d/
    ├── modus-themes/
    │   ├── modus-operandi-theme.el
    │   ├── modus-themes.el
    │   └── modus-vivendi-theme.el
    ├── prot-lisp/
    │   ├── prot-bongo.el
    │   ├── prot-comment.el
    │   ├── prot-common.el
    │   ├── prot-consult.el
    │   ├── prot-cursor.el
    │   ├── prot-diff.el
    │   ├── prot-dired.el
    │   ├── prot-elfeed-bongo.el
    │   ├── prot-elfeed.el
    │   ├── prot-embark.el
    │   ├── prot-embark-extras.el
    │   ├── prot-eshell.el
    │   ├── prot-fill.el
    │   ├── prot-fonts.el
    │   ├── prot-mail.el
    │   ├── prot-ibuffer.el
    │   ├── prot-icomplete.el
    │   ├── prot-logos.el
    │   ├── prot-minibuffer.el
    │   ├── prot-moody.el
    │   ├── prot-orderless.el
    │   ├── prot-outline.el
    │   ├── prot-project.el
    │   ├── prot-pulse.el
    │   ├── prot-recentf.el
    │   ├── prot-search.el
    │   ├── prot-sideline.el
    │   ├── prot-simple.el
    │   ├── prot-spell.el
    │   ├── prot-tab.el
    │   ├── prot-text.el
    │   ├── prot-vc.el
    │   ├── tmr.el
    │   └── usls.el
    ├── basic-init.el
    ├── early-init.el
    ├── init.el
    ├── prot-emacs.org
    └── user-emacs.org

3 directories, 42 files

When we invoke a stow command on this emacs package we are instructing the program to create symlinks to a directory called .emacs.d and to place all relevant files/directories inside of it. What we want is to mirror this tree in our $HOME directory (I only use GNU/Linux, by the way):

~/Git/prot-dotfiles $ stow -t "$HOME" emacs

As you will learn from Stow's manpage, the -t flag points at the target destination. So we want to mirror the .emacs.d of my dotfiles to that found in ~/.emacs.d. If the latter exists, only the relevant files will be symlinked. Otherwise it will be created outright as a symlink itself.

If files that conflict with mine, like init.el, already exist at the target path, then Stow will throw an error and abort its operation. This is good: we do not want to overwrite existing data. So make sure to create backups of everything and move them to another location.

Whenever I add or remove a file, my "emacs package" needs to be updated accordingly: the symlinks have to be generated anew. Adding the -R flag does the trick:

~/Git/prot-dotfiles $ stow -t "$HOME" -R emacs

Similarly, if you ever want to delete those symlinks in a clean way, pass the -D flag instead of -R:

~/Git/prot-dotfiles $ stow -t "$HOME" -D emacs

The same is true for all other "packages" in my dotfiles' repo.

At this point you are ready to start using my Emacs setup. But not everything will work just yet. As was already discussed in the section about Main macros and other contents of my init.el (for Emacs 28), I have a policy of not auto-installing packages by default. If you want to do that when you first launch my Emacs, you must create a new file called basic-init.el and place it in the same place where my init.el and prot-emacs.org are found (the basic-init.el is read before initiating my main configuration file). In that file you must add the following:

(setq prot-emacs-autoinstall-elpa t)

This means that you explicitly opt in to automatically installing all my defined packages that are found in GNU ELPA or MELPA.

If you do not create the basic-init.el with those contents, then the default behaviour is to run my setup and produce a series of warnings about missing packages that you need to install on your own. The resulting log's messages will explain how to do that in one go, though you can always opt for another approach if you want. This default method offers you the opportunity to think carefully about what packages you really need and proceed to remove the ones you do not want to keep around.

Whatever you do with the installation of items from Emacs Lisp Package Archives, you will always have to manually configure the few packages I maintain through their source code. Again, the warning messages will tell you what they expect from you. Basically, you will need to look up their names in the prot-emacs.org file to find their repo's URL. Then you will have to clone that to the contrib-elisp path inside of your .emacs.d. Or comment out their code block (or delete it) if you do not want them.

You are finally done and ready to start using what I develop. And you have realised by now that this definitely is my personal Emacs setup and I only target my use-case which means, among others, that I will never add bells and whistles that I do not use just to satisfy demand for them (e.g. icons).

To append your own configurations, you can create a new user-emacs.org file and place it in the same path as my prot-emacs.org. It must include code blocks like the ones I provide in my Org config. Those will be evaluated at startup and everything will work as expected: user-emacs.org is loaded after prot-emacs.org and you assume responsibility for everything.

This hopefully covers it. If you have any questions, either open an issue in my dotfiles' repo or contact me directly. Remember that I wish to be helpful, though I have no plan to turn this into yet another Emacs distro.

9.11. 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 to ensure 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 many administrative tasks (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 not super important to me. Though I still like to configure things to my liking, partially because it gives me a predictable environment but also for the inherent fun of it.

I run a custom session centred around a tiling window manager. Normally this is bspwm (Binary Space Partitioning Window Manager), though my dotfiles also include configurations for (i) swaywm and (ii) herbstluftwm. The former is a Wayland compositor. I keep it as a reliable solution for the inevitable Wayland future (Xorg is in maintenance mode, but it still works perfectly fine for my needs). While the latter is conceptually similar to bspwm, with the major exception being how it handles monitors and workspaces.

As of 2021-11-08, I have an ultrawide display as part of a temporary exchange. My bspwm is not optimised for it, because contents start off far to the left. It is possible to make it behave in a desirable fashion, but I don't want to adapt my setup to this monitor and then revert back to what I had before. So I am using herbstluftwm for the time being, which lets me designate areas of the physical display as virtual monitors. So I divide the 2560x1080 area into two parts with the right one being 1920x1080. This means that my Emacs goes to the right virtual monitor and its left window (the Emacs concept of "window") is positioned closer to the centre of the display. My setup includes a shell script which splits the display in three, with the largest area in the middle.

Herbstluftwm's virtual monitors are not mere workspaces. They do confine everything inside of them, as if they were physical displays. Fullscreen mode, for example.

None of my tiling window managers is as well documented as my Emacs setup. I do plan to improve things at some point. Until then, my dotfiles are only intended for experienced users or, generally, those who understand that they will handle everything on their own: https://gitlab.com/protesilaos/dotfiles.

My Emacs is built from source, directly from trunk (i.e. the master branch).

9.12. What hardware do you use?

I used to have a Lenovo Thinkpad X220 which I purchased in 2018 for ~80 EUR. In early August 2021 its battery stopped working and, for reasons I do not understand, it does not power up without it even though the Internet tells me it should…

Watch: The Emacs community bought me a new PC (2021-08-29).

After the donations I received up until August 16 2021, which amounted to 430 EUR, I contacted a friend of mine who is tech-savvy and was able to build a custom desktop computer with his help. I don't have the full picture of all the specs, as I know very little about hardware (the first computer I ever owned was my laptop and now this one) and my friend was the one who took care of everything.

In the following table the discepancy between the values in the column "my cost" and "brand new" indicates that the component came second-hand.

Component Specification My cost Brand new
GPU VGA BIOSTAR AMD RADEON RX560 2GB VA5615RF21 100 140
CPU AMD RYZEN 5 1600 AF 3.20GHZ 6-CORE 120 180
RAM TEAM GROUP TED416G2400C1601 ELITE 16GB DDR4 2400MHZ 83 83
MOTHERBOARD ASUS PRIME B450M - A II 85 85
Fans Don't know 10 10
Power supply Don't know 30 30
Box Don't know 30 30
    458 558

Other components I already had: monitor, mouse, keyboard, speakers. I had acquired those over the years. The keyboard is a generic Qwerty layout that I got from a toy store for 7 EUR together with a small mouse (the size of the mouse is about half that of a regular one). The mouse is annoying to work with (thankfully not often), though the keyboard feels better than what its cost suggests—still toy-level though. Same for the speakers, which I think cost 8 or 10 EUR. The monitor is 1920x1080 and the best part is that I got it free of charge from a clearance several years ago, before I even had the Thinkpad.

As of 2021-11-08, I exchanged that monitor with a friend's ultrawide one, which is 2560x1080. I thought I would experiment with it for a while to see what kind of software solutions can be applied to such an otherwise awkward ratio (What is your distro and/or window manager?). While my friend needs to use a dual monitor setup for some work.

On the day I got the computer (August 6, 2021), I got a camera and microphone for 40 EUR combined. Now I have a good setup in place to do live streams in addition to the prerecorded videos I would produce.

Since we are here: the recording software is OBS Studio, while I do no video editing whatsoever.

10. Other people's Emacs work

Emacs is both a piece of software and a diverse community of people that are brought together by their shared interest in this unique program. Emacs' development unfolds through a distributed network of volunteers, coordinated by members of the GNU project. While the community at-large develops and ultimately internalises lots of valuable ideas to a pool of accumulated knowledge, such as configurations with custom Elisp code, video or written tutorials on particular workflows, and packages that cover a broad range of needs.

Outside the narrow confines of the computer, Emacs is its people.

Here is a non-exhaustive list of users that I have found to be helpful, each in their own unique way—the order is not significant:

Omar Antolín Camarena
Omar's work is mentioned several times in this document (author of Orderless, Embark, and co-author of Marginalia). Apart from those inherently useful packages, Omar has helped me several times with various programming issues by sharing concrete code examples. Make sure to check Omar's packages and also monitor the personal Emacs configuration which doubles as a laboratory of experimentation for new packages or other useful ideas.
Daniel Mendler
Daniel's name is referenced in a number of this document's sections (author of Consult, Corfu, Vertico, Recursion indicator, co-author of Marginalia together with Omar—you get the idea). As far as I can tell, Daniel does not share an Emacs configuration, though one can still learn a lot by studying the code of the numerous repositories on @minad's Github.
Manuel Uberti
Manuel's contributions have been of paramount importance to the development of my Modus themes. Manuel offers a lot to the Emacs milieu through code contributions but also by reporting issues and communicating with package maintainers. The domain name www.manueluberti.eu hosts a blog with Manuel's musings on Emacs. For example, you will find articles that I have already referenced in this document, such as Extending project.el. If you are interested in cinema like Manuel, do not miss the reviews on www.filmsinwords.eu.
Nicolas De Jaeghere
Nicolas is another contributor to my Modus themes and has helped me understand several Elisp concepts, such as by helping me refine the macros that are defined in my init.el. Nicolas maintains a personal dotfiles repo where you can find an emacs.org file with lots of advanced code snippets.
Adam Porter (aka alphapapa, github-alphapapa)
Adam is a prolific contributor to the Emacs packaging milieu and a prominent member of the community on the r/emacs subreddit. Adam's packages on Github cover a wide range of needs and are designed using excellent coding practices. These include a client for the Matrix communication protocol (ement.el), a robust query language for Org (org-ql), a code colouriser that applies colour based on the level of depth (prism.el), a powerful buffer-management system (bufler.el), a sticky header that shows the first line of the code's definition when that is outside the visible window (topsy.el), a tool that tracks your position across Emacs buffers so you can return exactly to where you need to be (dogears.el)… You get the idea. I still plan to incorporate some of those into my workflow, starting with dogears, topsy, and bufler.
Andrew Tropin
Andrew is a programmer and contributor to free software who also produces live streams on Youtube about various Emacs-related topics, as well as the Guix System. You will find valuable information there, such as a Git workflow about pull requests as compared to patches, an advanced email setup with Emacs+Notmuch, and project management. Andrew's rde is a suite of tools that manages computing environments in Guix.
Rainer König
Rainer has a series of video tutorials on how to organise your life using various Org features, such as check-lists and the agenda. Each video covers a single utility or closely related concepts, while the whole playlist offers a progression from basic to more advanced workflows. Highly recommended!
Mike Zamansky
Mike is a computer science professor who has been doing videos on Emacs for several years. There is a wealth of information to parse from those presentations as well as the corresponding dot-emacs code repository.
Emacs Elements
This is a channel on Youtube where Raoul Comninos shows how to get things done using Emacs. There are tutorials on how to use the diary, calendar, and org-agenda, while you will also find more advanced topics such as how to install, use, and make sense of the powerful Icicles completion framework, as well as other useful packages.
System Crafters
David Wilson's work on Emacs covers a broad range of themes, encapsulated in the Emacs from scratch configuration. The Youtube channel contains lots of videos on how to set up and use all sorts of packages in Emacs, such as Org and the Mu4e email client. It is fair to say that if something is related to Emacs, David has either already done a video about it or is planning to do one in the near future. The System Crafters website includes references to all such work, while it also covers contributions by the community that David has helped foster. The material on offer is top-notch. It serves as an excellent resource and point of reference for new and experienced users alike.
Alain M. Lafon
Alain is one of my early influences in my Emacs journey. As soon as I had made the switch, I watched the video presentation Play Emacs like an instrument, which shows some advanced workflows of using Emacs in a seemingly effortless fashion. I knew Emacs was good, but did not realise it could be that good. Alain is a programmer as well as a Zen monk, who is the founder and CEO of the 200ok consultancy. The consultancy's blog contains a lot of articles and guides on Emacs, while you can find video tutorials on similar topics over at Alain's Youtube channel.
Xah Lee
Xah is a well-known member of the community through years of contributions either with packages or informational websites on various aspects of Emacs. On Xah's Youtube channel you will find videos about Emacs on all sorts of topics. Those are live streamed.
Karthik Chikmagalur
Karthik's Batteries included with Emacs (2020-11-17) and More batteries included with Emacs (2020-12-11) are essential reading for anyone wanting to gain an overview of some of Emacs' built-in capabilities and learn about its 'hidden gems'. Another excellent piece is Fifteen ways to use Embark (2021-10-06). Karthik's website includes philosophical insights as well, such as those found in Thoughts on Strength Training (2019-03-02). You will also discover packages by Karthik in this document, such as consult-dir and project-x.
James Norman Vladimir Cash
James produces videos on Emacs, such as this one on customising the modeline and writes about similar topics, like reading email in Emacs. Though I also appreciate commentary of a political sort: Against Mindless Software Minimalism (2019-03-24).
Karl Voit
Karl is an expert in Personal Information Management (PIM) covering that and relevant topics over at karl-voit.at. There are lots of interesting articles on offer, such as how to organise data with Don't Do Complex Folder Hierarchies and to keep a web presence with Don't Contribute Anything Relevant in Web Forums. Karl also participated in the 2021 edition of the Grazer Linuxtage conference with a talk on Org features and extras.
Sacha Chua
Sacha's work is instrumental to the Emacs community's self awareness. The weekly "Emacs news" blog entries offers an overview of what is happening in our space—consider it essential reading. While Sacha was among the organisers of the last two yearly Emacsconf events and has shared a lot of valuable insights throughout the years, such as hand-drawn guides to using Emacs, chats with prominent members of the Emacs community, and more.
Bozhidar Batsov
Bozhidar is the maintainer of several popular Emacs packages (and not only), such as the Projectile library for interacting with projects (like a Git repo), the Prelude starter kit which tries to enhance—but otherwise remain faithful to—the standard Emacs experience, ports of the popular Zenburn and Solarized themes, as well as the informative blog Emacs redux.
Timothy (aka tecosaur)
Timothy, better known as TEC or tecosaur, is the designer of the current iteration of the Org-mode web page and provides, among others, the blog This Month in Org which, as it name implies, offers a monthly overview of noteworthy new snippets that concern the development of the deservedly beloved Org-mode.
Irreal
Jon Snader's Irreal blog is one of the best places to start learning about the people in the wider Emacs community and to continue keeping track of their projects. Irreal offers curated summaries of our fellow Emacsers' contributions, as well as original entries. Jon takes the time to cover the main points in one's work and, where appropriate, to highlight relevant information or offer a valuable insight.
Álvaro Ramírez (aka xenodium)
Álvaro maintains a blog on Emacs and related programming topics: https://xenodium.com. One of my favourite aspects about the posts you will find there are the high quality GIFs that capture some precise and very powerful Emacs motion, custom command, or workflow. Álvaro is, among others, the developer of a mobile app that helps you track your habits: Flat Habits. And the best part is that it is all powered by Org mode!

Please note that this is a non-exhaustive list. Lots of people contribute to the betterment of Emacs proper and to specialised packages, such as Jonas Bernoulli (developer of Magit, among many others), Oleh Krehel (developer of Ivy, Counsel, Swiper, and more), Thierry Volpiatto (maintainer of Helm), Bastien Guerry (Org maintainer), Eli Zaretskii (Emacs maintainer), Lars Ingebrigtsen (author of Gnus, co-maintainer of Emacs), Dmitry Gutov (maintainer of several built-in subsystems like project.el, as well as external packages like Company, diff-hl…), Henrik Lissner (author and maintainer of Doom Emacs), the ever-resourceful maintainers and contributors to Org-roam, and many others.

For my part, I wish to express my gratitude to all those who have contributed to my own projects, including the ones who have sent patches against my Emacs setup, but also users who have reported issues, shared insights, provided code and suggestions for my Modus themes. The themes' manual (which is 27k words as of this writing on 2021-07-27) contains an "Acknowledgements" section where everyone is included. I think the least we can do is acknowledge how much we as individuals benefit from the communities that organically grow around our projects.

Every bit counts: a bug report, a blog post detailing one's workflow, participation in a mailing list thread, etc. Do not hesitate to add your part and become a member of this wonderful community.

A big thank you to everyone!