GNU Emacs configuration

An advanced literate configuration that produces modular code

Last revised and exported on 2024-12-01 12:52:35 +0200 with a word count of 106591.

This is my literate Emacs configuration file. It is a combination of prose and code. You can either read this page or check my dotfiles to find everything related to my Emacs setup.

What you are now reading is not a common literate configuration of Emacs. In most such cases, you have a generic init.el with a call to the org-babel-load-file function that gets an Org document as its value. That method works but is very slow, because we have to load Org before starting Emacs (and Org loads a bunch of other things we do not need at such an early stage).

Whereas this Org document serves as (i) a single point of entry to my Emacs setup and (ii) the origin of all of my Emacs configurations. While I am defining everything in a single Org file, I am not actually starting Emacs by reading this file. Rather, I am instructing Org to put the code blocks defined herein in standalone files, organised by scope. The end result is something where you cannot tell whether a literate program was executed or not.

This is the beauty of it. I can keep editing a single file as the “source of truth”, though I can still handle each of the files individually (e.g. someone wants to see how I do a specific thing, so I share only that file as an email attachment—no need to send over this massive document).

When I want to modify my Emacs setup, I edit this file and then evaluate the following code block or do C-c C-v C-t. All files will be updated accordingly.

(org-babel-tangle)

Note that I always build Emacs from source because I maintain lots of packages and need to be on the bleeding edge (Details of my Emacs build). This means that my code may not necessarily work with your version of Emacs.

Table of Contents

1. Details of my Emacs build

I track the emacs.git trunk, as I am the maintainer of several Emacs packages and a contributor to Emacs core. Here are my current settings (2024-12-01 12:52:35 +0200):

system-configuration-options
--prefix=/usr/local --with-x-toolkit=gtk3 --disable-gc-mark-trace --with-native-compilation=aot --without-gif --without-tiff --without-selinux --without-xinput2 --without-gpm --without-compress-install --without-xft --with-cairo --with-harfbuzz --with-tree-sitter=ifavailable --without-gsettings --without-gconf

Users of Arch Linux can refer to this PKGBUILD I maintain for my purposes:

2. Anatomy of my Emacs configuration

[ Also read: Why many modules instead of one init.el? and Why use Org when you can have an outline in Elisp? ]

What you are now reading is the prot-emacs.org file. It is the document that generates—and thus controls—every other file that underpins my Emacs configuration.

This Org file is not loaded directly. Its sole purpose is to produce the files that do the actual work. These files are organised by their purpose and function:

The standard early-init.el
It includes optimisations for starting up Emacs and sets some basics in place, such as to avoid the flash of light when starting Emacs while in a dark environment.
The standard init.el
It contains foundational blocks of my system, i.e. Lisp macros I define, and loads the individual configuration modules.
The prot-emacs-modules directory
It includes all my configuration modules. Each module is about a specific type of functionality, such a prot-emacs-theme.el for themes and prot-emacs-essentials.el for basic tools. These configuration modules tweak packages and are not meant to define extra functionality.
The prot-lisp directory
Here are the custom libraries I maintain as part of my Emacs setup. They are written in accordance with the best practices for packaging Emacs Lisp, though are only meant to be used as part of my setup. As such, they are not necessarily up to par with the public-facing packages I maintain for Emacs: https://protesilaos.com/emacs.
The prot-emacs-pre-custom file
It is evaluated before the modules are loaded. It is intended for users of my configuration who want to make use of the options I provide (The init.el user options).
The prot-emacs-post-custom file
Like the above, this file is meant for users of my setup. It is evaluated after the rest of my setup is loaded. Users can include whatever code they want in this file to either override existing functionality of define new one.
The prot-emacs.org file
The source of what you are currently reading.

Here is a schematic representation of this directory structure (files shown here may not reflect the latest state of the project):

~/Git/Projects/dotfiles/emacs/.emacs.d $ tree -aF
./
├── early-init.el
├── init.el
├── prot-emacs-modules/
│   ├── prot-emacs-completion.el
│   ├── prot-emacs-dired.el
│   ├── prot-emacs-ef-themes.el
│   ├── prot-emacs-email.el
│   ├── prot-emacs-essentials.el
│   ├── prot-emacs-git.el
│   ├── prot-emacs-gnus.el
│   ├── prot-emacs-icons.el
│   ├── prot-emacs-langs.el
│   ├── prot-emacs-mct.el
│   ├── prot-emacs-modeline.el
│   ├── prot-emacs-modus-themes.el
│   ├── prot-emacs-mu4e.el
│   ├── prot-emacs-notmuch.el
│   ├── prot-emacs-org.el
│   ├── prot-emacs-search.el
│   ├── prot-emacs-standard-themes.el
│   ├── prot-emacs-theme.el
│   ├── prot-emacs-vertico.el
│   ├── prot-emacs-web.el
│   ├── prot-emacs-which-key.el
│   └── prot-emacs-window.el
├── prot-emacs.org
├── prot-emacs-post-custom.el
├── prot-emacs-pre-custom.el
└── prot-lisp/
    ├── prot-abbrev.el
    ├── prot-coach.el
    ├── prot-comment.el
    ├── prot-common.el
    ├── prot-dired.el
    ├── prot-elfeed.el
    ├── prot-embark.el
    ├── prot-eww.el
    ├── prot-marginalia.el
    ├── prot-modeline.el
    ├── prot-notmuch.el
    ├── prot-orderless.el
    ├── prot-org.el
    ├── prot-pair.el
    ├── prot-prefix.el
    ├── prot-project.el
    ├── prot-scratch.el
    ├── prot-search.el
    ├── prot-shell.el
    ├── prot-simple.el
    ├── prot-spell.el
    ├── prot-vertico.el
    └── prot-window.el

3 directories, 50 files

The reason I have this modular setup is because it is easier to debug it but also to share individual snippets with others. The prot-emacs.org file is not a hindrance in this regard: it provides an additional way of sharing my work in the form of this consolidated view you are now seeing.

When I want to make a change to my Emacs setup, I do the edits in this prot-emacs.org and then type C-c C-v C-t (M-x org-babel-tangle) to propagate the changes to the relevant files.

On a new computer, I put all my Emacs files where they are meant to be (inside the ~/.emacs.d directory) with this command, which uses the stow system package (all my dotfiles are stowed in place with this program):

~/Git/Projects/dotfiles $ stow -t $HOME emacs

If I ever add/remove files, I do this instead:

~/Git/Projects/dotfiles $ stow -t $HOME -R emacs

I have built my setup from scratch and am observing best practices with regard to how Emacs expects things to run. I do not use the Emacs daemon, as I have encountered instabilities with it. Instead, I run a single instance of Emacs and then configure it to act as the server. This means that I can still connect to the running session via emacsclient, which is useful when I want to evaluate Elisp code from outside of Emacs (e.g. with my delight shell script that switches the entire “environment” theme of my tiling window manager or desktop environment—see my dotfiles for the technicalities).

3. The early initialisation of Emacs (early-init.el)

This is the first file that Emacs reads when starting up. It should contain code that does not depend on any package or the proportions of the Emacs frame. In general, this early initialisation file is meant to set up a few basic things before Emacs produces the initial frame by delegating to the init.el (Anatomy of my Emacs configuration).

3.1. The early-init.el macro to run code only in a Desktop Environment

There are a few parts of my setup where I need to run code based on whether I am using a regular desktop environment. This is not the norm, as I default to a tiling window manager (check my dotfiles for their specifics). What I have here is a macro which I can then use to wrap any code that should only be evaluated when I am not in one of my tiling window managers.

(defvar prot-emacs-tiling-window-manager-regexp "bspwm\\|herbstluftwm\\|i3"
  "Regular expression to  tiling window managers.
See definition of `prot-emacs-with-desktop-session'.")

(defmacro prot-emacs-with-desktop-session (&rest body)
  "Expand BODY if desktop session is not a tiling window manager.
See `prot-emacs-tiling-window-manager-regexp' for what
constitutes a matching tiling window manager."
  (declare (indent 0))
  `(when-let* ((session (getenv "DESKTOP_SESSION"))
               ((not (string-match-p session prot-emacs-tiling-window-manager-regexp))))
     ,@body))

3.2. The early-init.el code to set frame parameters

Here I am setting parameters for the size of the Emacs frame: the first as well as any future one. In a tiling window manager, these parameters are not relevant, since all windows are forcibly made to fit into rectangles (tiles) that fill up the entire screen. So I use the prot-emacs-with-desktop-session macro that I described above to set these parameters only when I am in a regular desktop environment (The early-init.el macro to run code only in a Desktop Environment).

The initial-frame-alist is about the first frame that is produced when starting Emacs. The default-frame-alist is for all frames after that.

(defun prot-emacs-add-to-list (list element)
  "Add to symbol of LIST the given ELEMENT.
Simplified version of `add-to-list'."
  (set list (cons element (symbol-value list))))

(prot-emacs-with-desktop-session
  (mapc
   (lambda (var)
     (prot-emacs-add-to-list var '(width . (text-pixels . 800)))
     (prot-emacs-add-to-list var '(height . (text-pixels . 900)))
     (prot-emacs-add-to-list var '(scroll-bar-width  . 10)))
   '(default-frame-alist initial-frame-alist)))

3.3. The early-init.el basic frame settings

These are some general settings for frames and the basics of the toolkit. In short, I want to keep things minimal. Notice the frame-resize-pixelwise and frame-inhibit-implied-resize: by default Emacs will resize the frame if you adjust the font size, which I never want.

(setq frame-resize-pixelwise t
      frame-inhibit-implied-resize t
      frame-title-format '("%b")
      ring-bell-function 'ignore
      use-dialog-box t ; only for mouse events, which I seldom use
      use-file-dialog nil
      use-short-answers t
      inhibit-splash-screen t
      inhibit-startup-screen t
      inhibit-x-resources t
      inhibit-startup-echo-area-message user-login-name ; read the docstring
      inhibit-startup-buffer-menu t)

;; I do not use those graphical elements by default, but I do enable
;; them from time-to-time for testing purposes or to demonstrate
;; something.  NEVER tell a beginner to disable any of these.  They
;; are helpful.
(menu-bar-mode -1)
(scroll-bar-mode -1)
(tool-bar-mode -1)

3.4. The early-init.el tweaks to startup time and garbage collection

I do not have a deep understanding of “garbage collection”, though I have learnt through trial and error that I can maximise the threshold during startup to make Emacs boot a bit faster. What I am doing here is to arrange for the relevant values to be set to very high values during startup and then be brought down to something more practical once Emacs is done loading.

;; Temporarily increase the garbage collection threshold.  These
;; changes help shave off about half a second of startup time.  The
;; `most-positive-fixnum' is DANGEROUS AS A PERMANENT VALUE.  See the
;; `emacs-startup-hook' a few lines below for what I actually use.
(setq gc-cons-threshold most-positive-fixnum
      gc-cons-percentage 0.5)

;; Same idea as above for the `file-name-handler-alist' and the
;; `vc-handled-backends' with regard to startup speed optimisation.
;; Here I am storing the default value with the intent of restoring it
;; via the `emacs-startup-hook'.
(defvar prot-emacs--file-name-handler-alist file-name-handler-alist)
(defvar prot-emacs--vc-handled-backends vc-handled-backends)

(setq file-name-handler-alist nil
      vc-handled-backends nil)

(add-hook 'emacs-startup-hook
          (lambda ()
            (setq gc-cons-threshold (* 1000 1000 8)
                  gc-cons-percentage 0.1
                  file-name-handler-alist prot-emacs--file-name-handler-alist
                  vc-handled-backends prot-emacs--vc-handled-backends)))

3.5. The early-init.el initialises the package cache

I use the standard package.el to manage my Emacs packages. It works for me and I never had a need for more (The init.el settings for packages (package.el)). If I have to tinker with a package’s source code, I use Git ordinarily—no need for a package manager to also be a development tool.

;; Initialise installed packages at this early stage, by using the
;; available cache.  I had tried a setup with this set to nil in the
;; early-init.el, but (i) it ended up being slower and (ii) various
;; package commands, like `describe-package', did not have an index of
;; packages to work with, requiring a `package-refresh-contents'.
(setq package-enable-at-startup t)

3.6. The early-init.el defines general theme-related functions

Here I am defining helper functions that check what theme I should be using. I then rely on these functions to load a light or dark theme accordingly. This is done in the sections about themes:

;;;; General theme code

(defun prot-emacs-theme-gsettings-dark-p ()
  "Return non-nil if gsettings (GNOME) has a dark theme.
Return nil if the DESKTOP_SESSION is either bspwm or
herbstluftwm, per the configuration of my dotfiles.  Also check
the `delight' shell script."
  (prot-emacs-with-desktop-session
    (string-match-p
     "dark"
     (shell-command-to-string "gsettings get org.gnome.desktop.interface color-scheme"))))

(defun prot-emacs-theme-twm-dark-p ()
  "Return non-nil if my custom setup has a dark theme.
I place a file in ~/.config/prot-xtwm-active-theme which contains
a single word describing my system-wide theme.  This is part of
my dotfiles.  Check my `delight' shell script for more."
  (when-let* ((file "~/.config/prot-xtwm-active-theme")
              ((file-exists-p file)))
    (string-match-p
     "dark"
     (with-temp-buffer
       (insert-file-contents file)
       (buffer-string)))))

(defun prot-emacs-theme-environment-dark-p ()
  "Return non-nil if environment theme is dark."
  (or (prot-emacs-theme-twm-dark-p)
      (prot-emacs-theme-gsettings-dark-p)))

(defun prot-emacs-re-enable-frame-theme (_frame)
  "Re-enable active theme, if any, upon FRAME creation.
Add this to `after-make-frame-functions' so that new frames do
not retain the generic background set by the function
`prot-emacs-avoid-initial-flash-of-light'."
  (when-let* ((theme (car custom-enabled-themes)))
    (enable-theme theme)))

3.7. The early-init.el takes care to avoid the initial flash of light

Since I put in the effort to define the above theme-related functions, I can now benefit by having Emacs set an appropriate set of basic colour values at startup to eliminate the flash of light it normally displays (The early-init.el defines general theme-related functions). By default, Emacs loads a light theme, but this is terrible if I am in a fairly dark environment. Whereas my arrangement here makes sure that Emacs uses a black background if the environment is dark.

Note that in the snippet below I hardcode the black colour (#000000) to avoid any extra calculations at this early stage. Otherwise, I would have to check which theme will be loaded and then set its background here. That would be too slow for what we need in the early initialisation file, thus defeating the purpose of not having a flash of light at startup.

;; NOTE 2023-02-05: The reason the following works is because (i) the
;; `mode-line-format' is specified again and (ii) the
;; `prot-emacs-theme-gsettings-dark-p' will load a dark theme.
(defun prot-emacs-avoid-initial-flash-of-light ()
  "Avoid flash of light when starting Emacs, if needed.
New frames are instructed to call `prot-emacs-re-enable-frame-theme'."
  (when (prot-emacs-theme-environment-dark-p)
    (setq mode-line-format nil)
    (set-face-attribute 'default nil :background "#000000" :foreground "#ffffff")
    (set-face-attribute 'mode-line nil :background "#000000" :foreground "#ffffff" :box 'unspecified)
    (add-hook 'after-make-frame-functions #'prot-emacs-re-enable-frame-theme)))

(prot-emacs-avoid-initial-flash-of-light)

3.8. The early-init.el gives a name to the default frame

Finally, I like to call my default frame home. This is because I use my beframe package to group the list of buffers on a per-frame basis (The prot-emacs-window.el section about beframe). The multi-frame arrangement is the best thing I ever did to boost my productivity: bonus points when used in tandem with a tiling window manager.

Naming frames allows you to select them using completion. Emacs can do this (M-x select-frame-by-name), though it is not always reliable as it depends on the window manager (it works fine on GNOME, from what I can tell). For minimalist window managers on Linux, something like the rofi program can select system windows based on their name.

(add-hook 'after-init-hook (lambda () (set-frame-name "home")))

4. The main initialisation of Emacs (init.el)

This is where I define the Lisp macros used in my setup and load all the invidiual modules.

4.1. The init.el user options

I define several user options for my Emacs. These are useful to me if I need to quickly test some aspect of my setup, though I provide them mostly for those who use my files as a basis for their configuration.

All user options must be set in a prot-emacs-pre-custom.el file in the same directory as the init.el (Anatomy of my Emacs configuration).

;; For those who use my dotfiles and need an easy way to write their
;; own extras on top of what I already load: search below for the files
;; prot-emacs-pre-custom.el and prot-emacs-post-custom.el
(defgroup prot-emacs nil
  "User options for my dotemacs.
These produce the expected results only when set in a file called
prot-emacs-pre-custom.el.  This file must be in the same
directory as the init.el."
  :group 'file)

4.1.1. The init.el user option to load a theme family

I am the developer/maintainer of three distinct theme packages. You can read more about them (and see pictures) on their respective web pages:

In short:

Modus themes
They conform with the highest accessibility standard for colour contrast (WCAG AAA). They are elegant and designed with attention to detail. I consider the modus-operandi and modus-vivendi themes to be the standard of what a default accessible theme should be like.
Ef themes
Highly legible (WCAG AA or WCAG AAA) and more colourful than the Modus themes. The collection of palettes is also wider to match a broad variety of preferences.
Standard themes
A re-imagination of the default Emacs looks. They bring consistency and customisability to those who like how Emacs is out-of-the-box.

Here we specify which module to load at startup. Remember to read how these options come into effect (The init.el user options).

(defcustom prot-emacs-load-theme-family 'modus
  "Set of themes to load.
Valid values are the symbols `ef', `modus', and `standard', which
reference the `ef-themes', `modus-themes', and `standard-themes',
respectively.

A nil value does not load any of the above (use Emacs without a
theme).

This user option must be set in the `prot-emacs-pre-custom.el'
file.  If that file exists in the Emacs directory, it is loaded
before all other modules of my setup."
  :group 'prot-emacs
  :type '(choice :tag "Set of themes to load" :value modus
                 (const :tag "The `ef-themes' module" ef)
                 (const :tag "The `modus-themes' module" modus)
                 (const :tag "The `standard-themes' module" standard)
                 (const :tag "Do not load a theme module" nil)))

4.1.2. The init.el user option to load a minibuffer user interface

I normally use vertico with my own custom extensions to it (The prot-vertico.el library), though I also maintain the mct package for those who want to use the built-in completion framework with a few extras for better movement and the like. I think mct will eventually be replaced by built-in facilities, given there are developments on that front for Emacs 30. It still has its place in the meantime.

Here we specify which module to load at startup. Remember to read how these options come into effect (The init.el user options).

(defcustom prot-emacs-completion-ui 'vertico
  "Choose minibuffer completion UI between `mct' or `vertico'.
If the value is nil, the default completion user interface is
used.  On Emacs 30, this is close the experience with `mct'.

This user option must be set in the `prot-emacs-pre-custom.el'
file.  If that file exists in the Emacs directory, it is loaded
before all other modules of my setup."
  :group 'prot-emacs
  :type '(choice :tag "Minibuffer user interface"
                 (const :tag "Default user interface" nil)
                 (const :tag "The `mct' module" mct)
                 (const :tag "The `vertico' module" vertico)))

4.1.3. The init.el user option to load extras for minibuffer completion

I normally load some packages that enhance the experience with the minibuffer. The upside is that we get more power out of Emacs. The downside is that they have a learning curve. Users who do not need these features can set the option to nil.

Remember to read how these options come into effect (The init.el user options).

(defcustom prot-emacs-completion-extras t
  "When non-nil load extras for minibuffer completion.
These include packages such as `consult' and `embark'."
  :group 'prot-emacs
  :type 'boolean)

4.1.4. The init.el user option to load treesitter extras

I configure Emacs to support the tree-sitter program, though I do not use anything that leverages it. I either code in Emacs Lisp or write prose. This user option is in place for those who want to use my configuration as a basis for their own setup.

Remember to read how these options come into effect (The init.el user options).

(defcustom prot-emacs-treesitter-extras t
  "When non-nil load extras for tree-sitter integration
These include packages such as `expreg' and generally anything
that adds functionality on top of what the major mode provides."
  :group 'prot-emacs
  :type 'boolean)

4.1.5. The init.el user option to enable which-key

The which-key package provides hints for keys that complete the currently incomplete sequence. Here we determine whether to load the module or not. I personally never rely on which-key even if I enable its mode. If I ever need to review which key bindings are available I will either type C-h to complete a key sequence (produces a Help buffer with relevant keys) or I will do C-h m (M-x describe-mode to get information about the current major mode).

Remember to read how these options come into effect (The init.el user options).

Also check the prot-emacs-which-key.el module.

(defcustom prot-emacs-load-which-key nil
  "When non-nil, display key binding hints after a short delay.
This user option must be set in the `prot-emacs-pre-custom.el'
file.  If that file exists in the Emacs directory, it is loaded
before all other modules of my setup."
  :group 'prot-emacs
  :type 'boolean)

4.1.6. The init.el user option to load icons (nerd-icons.el)

Here we check whether to load decorative icons in a number of places, such as Dired buffers and the completion user interface (where relevant). My setup does not try to install the font files: the user must do this manually with M-x nerd-icons-install-fonts.

Remember to read how these options come into effect (The init.el user options).

Also check the prot-emacs-icons.el module.

(defcustom prot-emacs-load-icons nil
  "When non-nil, enable iconography in various contexts.
This installs and uses the `nerd-icons' package and its variants.
NOTE that you still need to invoke `nerd-icons-install-fonts'
manually to first get the icon files.

This user option must be set in the `prot-emacs-pre-custom.el'
file.  If that file exists in the Emacs directory, it is loaded
before all other modules of my setup."
  :group 'prot-emacs
  :type 'boolean)

4.2. The init.el basic configurations to disable backups and lockfiles

By default, Emacs tries to lock down files so that they are not modified by other programs. It also keeps backups. These are features I do not need because (i) if I am ever modifying my files externally, then I know what I am doing and (ii) all the files I care about are either under version control or backed up to a flash drive.

(setq make-backup-files nil)
(setq backup-inhibited nil) ; Not sure if needed, given `make-backup-files'
(setq create-lockfiles nil)

4.3. The init.el tweaks to make native compilation silent

The --with-native-compilation=yes build option of Emacs is very nice: it enables the “native compilation” of Emacs Lisp, translating it down to machine code. However, the default setting for reporting errors is set to a verbose value which, in my coaching experience, confuses users: it produces warnings for compilation issues that only the developer of the given package needs to deal with. These include innocuous facts like docstrings being wider than a certain character count. To make things even worse, the buffer that shows these warnings uses the stop sign character, resulting in a long list of lines with red spots everywhere, as if we have totally broken Emacs.

;; Make native compilation silent and prune its cache.
(when (native-comp-available-p)
  (setq native-comp-async-report-warnings-errors 'silent) ; Emacs 28 with native compilation
  (setq native-compile-prune-cache t)) ; Emacs 29

4.4. The init.el setting to send custom-file to oblivion

By default, Emacs writes persistent customisations to the end of the user’s init file. These are encapsulated in a “custom” block. Emacs writes those whenever the user does something with M-x customize or related.

I personally prefer writing the actual Elisp over using the interface of M-x customize. I also want my init file to only ever contain what I wrote and to never—ever!—evaluate code I have not called myself.

As such, I want to set the custom-file variable to tell Emacs to write its persistent variables to that file instead of my init.el. Though since I have no intent to ever use this file, I choose to make it disposable by placing it inside of the /tmp directory (this is what the make-temp-file function does). So when I close the computer, the file is gone. Finally, I do not invoke the load function because I will never rely on the custom-file. I would prefer to just have an option to avoid the Custom infrastructure altogether, but this is not possible. So here we are…

;; Disable the damn thing by making it disposable.
(setq custom-file (make-temp-file "emacs-custom-"))

4.5. The init.el settings for multilingual editing (input methods)

This sets up Emacs for me to be able to type in Greek while still using Emacs key bindings involving modifier keys (I don’t get this if I switch keyboard layouts at the system level).

Watch my video about multilingual editing: https://protesilaos.com/codelog/2023-12-12-emacs-multilingual-editing/.

(setq default-input-method "greek") ; also check "greek-postfix"
(setq default-transient-input-method "greek")

4.6. The init.el settings to enable certain commands and disable others

Some Emacs commands are disabled by default. This means that Emacs will produce a warning when we try to invoke them and will ask us for confirmation. Here I define which commands I want to have enabled and then put some on the disabled list.

;; Enable these
(mapc
 (lambda (command)
   (put command 'disabled nil))
 '(list-timers narrow-to-region narrow-to-page upcase-region downcase-region))

;; And disable these
(mapc
 (lambda (command)
   (put command 'disabled t))
 '(eshell project-eshell overwrite-mode iconify-frame diary))

4.7. The init.el setting to always start with the *scratch* buffer

I like starting with a scratch buffer. I know that a lot of users specify a dashboard or an Org agenda view, but I prefer to keep things generic in this regard.

(setq initial-buffer-choice t)
(setq initial-major-mode 'lisp-interaction-mode)
(setq initial-scratch-message
      (format ";; This is `%s'.  Use `%s' to evaluate and print results.\n\n"
              'lisp-interaction-mode
              (propertize
               (substitute-command-keys "\\<lisp-interaction-mode-map>\\[eval-print-last-sexp]")
               'face 'help-key-binding)))

4.8. The init.el arrangements for my own modules and custom libraries

I use a literate configuration as the “source of truth” for my Emacs configuration. What I do is to specify everything in one file and provide instructions for where things should go. The end product consists of a large set of files, encompassing the early-init.el (The early initialisation of Emacs (early-init.el)), the init.el (The main initialisation of Emacs (init.el)), the modules of my init, and the custom libraries I wrote.

In the code snippet further below, I add two directories to the load-path. Concretely, any Emacs Lisp file inside these directories is thus declared to Emacs and we can load it properly. Here is what these two directories are about:

The prot-emacs-modules directory

This is where I store all the individual components of my Emacs setup. When I run Emacs, the directory is a subdirectory of ~/.emacs.d/. All files are prefixed with prot-emacs-, followed by a word that broadly describes their scope of application, such as prot-emacs-font, prot-emacs-window

Each module consists of ordinary Elisp and a final call to provide the set of configurations as a feature that can then be loaded via require from the init.el. What Emacs calls a “feature” is, in essence, a variable whose value is the entirety of the file that has a provide call in it. Features are symbols that are named after the file name minus its file type extension: prot-emacs-theme is the feature provided by prot-emacs-theme.el.

Modules are intended only for configuration purposes. They do not define any major variables/functions, unless those are too small/specific to be extracted into their own library.

The prot-lisp directory

As with the aforementioned modules, this directory is a subdirectory of ~/.emacs.d/. This is where I keep all my custom code that individual modules configure. The contents of this directory can be understood as fully fledged “packages” and, in fact, many of my actual packages started out as prot-lisp experiments.

Each file is written in accordance with the conventions on Emacs packaging, even though they are only intended for use in my setup and are not polished to the level of my actual public-facing packages (meaning the ones listed here: https://protesilaos.com/emacs).

All this may not matter to you if you are reading either the prot-emacs.org file or its web page version. Still, this arrangement gives me maximum flexbility, as I can still share my code the way it would look. Plus, if I ever decide to stop using the literate config, I can simply stop editing it and perfom the edits directly in the files that are already placed where I need them to be.

(mapc
 (lambda (string)
   (add-to-list 'load-path (locate-user-emacs-file string)))
 '("prot-lisp" "prot-emacs-modules"))

4.9. The init.el settings for packages (package.el)

The package.el is built into Emacs and is perfectly fine for my use-case. We do not need to load it explicitly, as it will be called by use-package when it needs it. Since the introduction of the early-init.el file, we also do not need to initialise the packages at this point: we activate the cache instead (The early-init.el initialises the package cache).

With regard to the settings here, make sure to read my article about package archives, pinning packages, and setting priorities: https://protesilaos.com/codelog/2022-05-13-emacs-elpa-devel/.

;;;; Packages

(setq package-vc-register-as-project nil) ; Emacs 30

(add-hook 'package-menu-mode-hook #'hl-line-mode)

;; Also read: <https://protesilaos.com/codelog/2022-05-13-emacs-elpa-devel/>
(setq package-archives
      '(("gnu-elpa" . "https://elpa.gnu.org/packages/")
        ("gnu-elpa-devel" . "https://elpa.gnu.org/devel/")
        ("nongnu" . "https://elpa.nongnu.org/nongnu/")
        ("melpa" . "https://melpa.org/packages/")))

;; Highest number gets priority (what is not mentioned has priority 0)
(setq package-archive-priorities
      '(("gnu-elpa" . 3)
        ("melpa" . 2)
        ("nongnu" . 1)))

;; NOTE 2023-08-21: I build Emacs from source, so I always get the
;; latest version of built-in packages.  However, this is a good
;; solution to set to non-nil if I ever switch to a stable release.
(setq package-install-upgrade-built-in nil)

I want to use my own packages from the GNU-devel ELPA. I am thus pinning them to that archive by setting the value of package-pinned-packages. This way, I get to run the latest version while also making sure the actual package works properly. All other packages will rely on package-archive-priorities.

(defvar prot-emacs-my-packages
  '(agitate
    altcaps
    beframe
    cursory
    denote
    dired-preview
    ef-themes
    fontaine
    lin
    logos
    mct
    modus-themes
    notmuch-indicator
    pulsar
    show-font
    spacious-padding
    standard-themes
    substitute
    sxhkdrc-mode
    theme-buffet
    tmr)
  "List of symbols representing the packages I develop/maintain.")

;; Also read: <https://protesilaos.com/codelog/2022-05-13-emacs-elpa-devel/>
(setq package-pinned-packages
      `(,@(mapcar
           (lambda (package)
             (cons package "gnu-elpa-devel"))
           prot-emacs-my-packages)))

4.10. The init.el option to declare all themes as safe

When loading a theme, Emacs will produce a warning explaining how themes are ordinary Elisp and thus can run harmful code. I understand why this message is there, but I do not need to be reminded about it. Setting this to non-nil saves me from the occasional warning if I ever run load-theme without a NO-CONFIRM argument (like this: (load-theme 'modus-operandi :no-confirm)).

[ Note that Emacs considers the built-in themes “safe”. This includes my modus-themes. ]

(setq custom-safe-themes t)

4.11. The init.el macro to do nothing with Elisp code (prot-emacs-comment)

This is something I learnt while studying Clojure: a comment macro that wraps some code, effectively commenting it out, while keeping indentation and syntax highlighting intact.

What I have here is technically not commenting out the code, because the expansion of the macro is nil, not the actual code with comments around it.

(defmacro prot-emacs-comment (&rest body)
  "Do nothing with BODY and return nil, with no side effects."
  (declare (indent defun))
  nil)

The above is an example. What I actually use is the following. It behaves the same as above, except when it reads a plist of the form (:eval t). The idea is for me to quickly activate something I want to test by passing that to the macro. So here we have it:

(defmacro prot-emacs-comment (&rest body)
  "Determine what to do with BODY.

If BODY contains an unquoted plist of the form (:eval t) then
return BODY inside a `progn'.

Otherwise, do nothing with BODY and return nil, with no side
effects."
  (declare (indent defun))
  (let ((eval))
    (dolist (element body)
      (when-let* (((plistp element))
                  (key (car element))
                  ((eq key :eval))
                  (val (cadr element)))
        (setq eval val
              body (delq element body))))
    (when eval `(progn ,@body))))

And here is a function I might develop further to quickly insert prot-emacs-comment (though another is needed to also remove it and then a Do-What-I-Mean wrapper to switch between the two):

;; Sample use of `prot-emacs-comment'.  The function
;; `prot-emacs-insert-comment-macro' is never evaluated.
(prot-emacs-comment
  (defun prot-emacs-insert-comment-macro (beg end)
    "Wrap region between BEG and END in `prot-emacs-comment'."
    (interactive "r")
    (if (region-active-p)
        (let ((text (buffer-substring beg end)))
          (delete-region beg end)
          (insert (format "(prot-emacs-comment\n%s)" text))
          (indent-region beg end))
      (user-error "No active region; will not insert `prot-emacs-comment' here"))))

4.12. The init.el macro to bind keys (prot-emacs-keybind)

[ Watch: define prefix/leader key (nested key maps) (2024-01-29). ]

This Lisp macro does not try to be too smart. It simply reduces the typing we have to do to define key bindings. As with the underlying define-key function, it can bind a key sequence to a command, a nil value, or even a keymap. The constraint it imposes is that the arguments supplied to it as an even number and the odd ones are key bindings (strings that can be passed to the kbd function). This means that it does not try to cover the case of [remap COMMAND] (I am not a fan of it because the code alone does not tell us which key we end up using (Why don’t you remap keys?)).

(defmacro prot-emacs-keybind (keymap &rest definitions)
  "Expand key binding DEFINITIONS for the given KEYMAP.
DEFINITIONS is a sequence of string and command pairs."
  (declare (indent 1))
  (unless (zerop (% (length definitions) 2))
    (error "Uneven number of key+command pairs"))
  (let ((keys (seq-filter #'stringp definitions))
        ;; We do accept nil as a definition: it unsets the given key.
        (commands (seq-remove #'stringp definitions)))
    `(when-let* (((keymapp ,keymap))
                 (map ,keymap))
       ,@(mapcar
          (lambda (pair)
            (let* ((key (car pair))
                   (command (cdr pair)))
              (unless (and (null key) (null command))
                `(define-key map (kbd ,key) ,command))))
          (cl-mapcar #'cons keys commands)))))

;; Sample of `prot-emacs-keybind'

;; (prot-emacs-keybind global-map
;;   "C-z" nil
;;   "C-x b" #'switch-to-buffer
;;   "C-x C-c" nil
;; ;; Notice the -map as I am binding keymap here, not a command:
;;   "C-c b" beframe-prefix-map
;;   "C-x k" #'kill-buffer)

4.13. The init.el macro to define abbreviations (prot-emacs-abbrev)

[ Watch: abbreviations with abbrev-mode (quick text expansion) (2024-02-03). ]

This is the same idea as prot-emacs-keybind, adjusted to work with the define-abbrev function (The init.el macro to bind keys (prot-emacs-keybind)). I probably do not need this, as I only write a small number of abbreviations. Though it is good to practice some programming.

(defmacro prot-emacs-abbrev (table &rest definitions)
  "Expand abbrev DEFINITIONS for the given TABLE.
DEFINITIONS is a sequence of (i) string pairs mapping the
abbreviation to its expansion or (ii) a string and symbol pair
making an abbreviation to a function."
  (declare (indent 1))
  (unless (zerop (% (length definitions) 2))
    (error "Uneven number of key+command pairs"))
  `(if (abbrev-table-p ,table)
       (progn
         ,@(mapcar
            (lambda (pair)
              (let ((abbrev (nth 0 pair))
                    (expansion (nth 1 pair)))
                (if (stringp expansion)
                    `(define-abbrev ,table ,abbrev ,expansion)
                  `(define-abbrev ,table ,abbrev "" ,expansion))))
            (seq-split definitions 2)))
     (error "%s is not an abbrev table" ,table)))

4.14. The init.el addition of highlighting for my macros

In the previous sections, I define a few Lisp macros that I use throughout my setup. The following makes these known to Emacs and specifies how they should be colourised.

(defvar prot-emacs-package-form-regexp
  "^(\\(prot-emacs-keybind\\|prot-emacs-abbrev\\) +'?\\([0-9a-zA-Z-]+\\)"
  "Regexp to add packages to `lisp-imenu-generic-expression'.")

(eval-after-load 'lisp-mode
  `(add-to-list 'lisp-imenu-generic-expression
                (list "Packages" ,prot-emacs-package-form-regexp 2)))

(defconst prot-emacs-font-lock-keywords
  '(("(\\(prot-emacs-\\(keybind\\|abbrev\\)\\)\\_>[ \t']*\\(\\(\\sw\\|\\s_\\)+\\)?"
     (3 font-lock-variable-name-face nil t))
    ("(\\(prot-emacs-comment\\)\\_>[ \t']*"
     (1 font-lock-preprocessor-face nil t))))

(font-lock-add-keywords 'emacs-lisp-mode prot-emacs-font-lock-keywords)

4.15. The init.el final part to load the individual modules

My configuration is split into several modules (The init.el arrangements for my own modules and custom libraries). This makes it easier for me to share parts of my code but also to review it as code without delving into a large Org file (though the one Org file has its advantages, which is why you are reading this).

All I do here is load the modules. Note that some of these are subject to user options (The init.el user options). Those who need to set those options must have the prot-emacs-pre-custom.el file in place, as I have already explained in the section about these user options.

The individual modules are documented in a section of their own under the modules of my Emacs configuration.

;; For those who use my dotfiles and need an easy way to write their
;; own extras on top of what I already load.  The file must exist at
;; ~/.emacs.d/prot-emacs-pre-custom.el
;;
;; The purpose of this file is for the user to define their
;; preferences BEFORE loading any of the modules.
(load (locate-user-emacs-file "prot-emacs-pre-custom.el") :no-error :no-message)

(require 'prot-emacs-theme)
(require 'prot-emacs-essentials)
(require 'prot-emacs-modeline)
(require 'prot-emacs-completion)
(require 'prot-emacs-search)
(require 'prot-emacs-dired)
(require 'prot-emacs-window)
(require 'prot-emacs-git)
(require 'prot-emacs-org)
(require 'prot-emacs-langs)
(require 'prot-emacs-email)
(require 'prot-emacs-web)
(when prot-emacs-load-which-key
  (require 'prot-emacs-which-key))
(when prot-emacs-load-icons
  (require 'prot-emacs-icons))

;; For those who use my dotfiles and need an easy way to write their
;; own extras on top of what I already load.  The file must exist at
;; ~/.emacs.d/prot-emacs-post-custom.el
;;
;; The purpose of the "post customisations" is to make tweaks to what
;; I already define, such as to change the default theme.  See above
;; for the `prot-emacs-pre-custom.el' to make changes BEFORE loading
;; any of my other configurations.
(load (locate-user-emacs-file "prot-emacs-post-custom.el") :no-error :no-message)

5. The modules of my Emacs configuration

In my init.el I have a section where I add my modules to the load-path so that Emacs can run their code (The init.el arrangements for my own modules and custom libraries). The subheadings of this chapter define modules, each of which is loaded at the end of my init.el (The init.el final part to load the individual modules).

5.1. The prot-emacs-theme.el module

In this module I define everything broadly related to the aesthetics of Emacs.

5.1.1. The prot-emacs-theme.el section to load a theme (prot-emacs-load-theme-family)

We start by loading one of my themes (The init.el option to load a theme family). These are actually defined in modules of their own, though this is the only place where they are used.

;;; Theme setup and related

;;;; Load the desired theme module
;; These all reference my packages: `modus-themes', `ef-themes',
;; `standard-themes'.
(when prot-emacs-load-theme-family
  (require
   (pcase prot-emacs-load-theme-family
     ('ef 'prot-emacs-ef-themes)
     ('modus 'prot-emacs-modus-themes)
     ('standard 'prot-emacs-standard-themes))))
5.1.1.1. The prot-emacs-modus-themes.el module

This is one of the components of the prot-emacs-theme.el module (The prot-emacs-theme.el section to load a theme (prot-emacs-load-theme-family)). It defines some theme settings and also includes code I use when I need to test things.

The modus-themes are highly accessible themes, conforming with the highest standard for colour contrast between background and foreground values (WCAG AAA). They also are optimised for users with red-green or blue-yellow colour deficiency.

The themes are very customisable and provide support for a wide range of packages. Their manual is detailed so that new users can get started, while it also provides custom code for all sorts of more advanced customisations.

Since August 2020, the original Modus themes (modus-operandi, modus-vivendi) are built into Emacs version 28 or higher. Emacs 28 ships with modus-themes version 1.6.0. Emacs 29 includes version 3.0.0. Emacs 30 provides a newer, refactored version that thoroughly refashions how the themes are implemented and customized. Such major versions are not backward-compatible due to the limited resources at my disposal to support multiple versions of Emacs and of the themes across the years.

Note that the prot-emacs-comment is there for my testing purposes (The init.el macro to do nothing with Elisp code (prot-emacs-comment)).

;;; The Modus themes

;; The themes are highly customisable.  Read the manual:
;; <https://protesilaos.com/emacs/modus-themes>.
(use-package modus-themes
  :ensure t
  :demand t
  :bind (("<f5>" . modus-themes-toggle)
         ("C-<f5>" . modus-themes-select)
         ("M-<f5>" . modus-themes-rotate))
  :config
  (setq modus-themes-custom-auto-reload nil
        modus-themes-to-toggle '(modus-operandi modus-vivendi)
        ;; modus-themes-to-toggle '(modus-operandi-tinted modus-vivendi-tinted)
        ;; modus-themes-to-toggle '(modus-operandi-deuteranopia modus-vivendi-deuteranopia)
        ;; modus-themes-to-toggle '(modus-operandi-tritanopia modus-vivendi-tritanopia)
        modus-themes-to-rotate modus-themes-items
        modus-themes-mixed-fonts t
        modus-themes-variable-pitch-ui t
        modus-themes-italic-constructs t
        modus-themes-bold-constructs nil
        modus-themes-completions '((t . (extrabold)))
        modus-themes-prompts '(extrabold)
        modus-themes-headings
        '((agenda-structure . (variable-pitch light 2.2))
          (agenda-date . (variable-pitch regular 1.3))
          (t . (regular 1.15))))

  (setq modus-themes-common-palette-overrides nil)

  (if (prot-emacs-theme-environment-dark-p)
      (modus-themes-load-theme (cadr modus-themes-to-toggle))
    (modus-themes-load-theme (car modus-themes-to-toggle))))

;; NOTE: For testing purposes
(prot-emacs-comment
  (:eval nil)
  (progn
    (mapc #'disable-theme custom-enabled-themes)

    (add-to-list 'load-path "/home/prot/Git/Projects/modus-themes/")

    (require 'modus-themes)

    (setq modus-themes-custom-auto-reload nil
          modus-themes-to-toggle '(modus-operandi modus-vivendi)
          ;; modus-themes-to-toggle '(modus-operandi-tinted modus-vivendi-tinted)
          ;; modus-themes-to-toggle '(modus-operandi-deuteranopia modus-vivendi-deuteranopia)
          ;; modus-themes-to-toggle '(modus-operandi-tritanopia modus-vivendi-tritanopia)
          modus-themes-mixed-fonts t
          modus-themes-variable-pitch-ui nil
          modus-themes-italic-constructs t
          modus-themes-bold-constructs t
          modus-themes-completions '((t . (extrabold)))
          modus-themes-prompts nil
          modus-themes-headings
          '((agenda-structure . (variable-pitch light 2.2))
            (agenda-date . (variable-pitch regular 1.3))
            (t . (regular 1.15))))

    ;; (setq modus-themes-common-palette-overrides nil)

    (setq modus-themes-common-palette-overrides
          `((fringe unspecified)
            ;; (bg-mode-line-active bg-lavender)
            ;; (border-mode-line-active unspecified)
            ;; (border-mode-line-inactive unspecified)
            (bg-line-number-active bg-hl-line)
            (bg-line-number-inactive unspecified)
            (fg-line-number-active fg-main)
            ;; ,@modus-themes-preset-overrides-warmer
            ))

    ;; ;; For testing purposes I only want the overrides for those two
    ;; ;; Modus themes.  The rest have their own styles already.
    ;;
    ;; (let ((overrides '((cursor blue-intense)
    ;;                    (keybind green-cooler)
    ;;                    (comment red-faint)
    ;;                    (bg-paren-match unspecified)
    ;;                    (fg-paren-match magenta-intense)
    ;;                    (underline-paren-match magenta-intense))))
    ;;   (setq modus-operandi-palette-overrides overrides
    ;;         modus-vivendi-palette-overrides overrides))


    ;; ;; Make the active mode line have a pseudo 3D effect (this assumes
    ;; ;; you are using the default mode line and not an extra package).
    ;; (custom-set-faces
    ;;  '(mode-line ((t :box (:style unspecified)))))

    (if (prot-emacs-theme-environment-dark-p)
        (modus-themes-load-theme (cadr modus-themes-to-toggle))
      (modus-themes-load-theme (car modus-themes-to-toggle)))

    ;; Also check `modus-themes-select'.  To list the palette's colours,
    ;; use `modus-themes-list-colors', `modus-themes-list-colors-current'.
    (define-key global-map (kbd "<f5>") #'modus-themes-toggle)))

(provide 'prot-emacs-modus-themes)
5.1.1.2. The prot-emacs-ef-themes.el module

This is one of the components of the prot-emacs-theme.el module (The prot-emacs-theme.el section to load a theme (prot-emacs-load-theme-family)). It defines some theme settings and also includes code I use when I need to test things.

The ef-themes are a collection of light and dark themes for GNU Emacs that provide colourful (“pretty”) yet legible options for users who want something with a bit more flair than the modus-themes (also designed by me).

Note that the prot-emacs-comment is there for my testing purposes (The init.el macro to do nothing with Elisp code (prot-emacs-comment)).

;;; The Ef (εὖ) themes

;; The themes are customisable.  Read the manual:
;; <https://protesilaos.com/emacs/ef-themes>.
(use-package ef-themes
  :ensure t
  :demand t
  :bind
  (("<f5>" . ef-themes-rotate)
   ("C-<f5>" . ef-themes-select))
  :config
  (setq ef-themes-variable-pitch-ui t
        ef-themes-mixed-fonts t
        ef-themes-rotate ef-themes-items
        ef-themes-headings ; read the manual's entry of the doc string
        '((0 . (variable-pitch light 1.9))
          (1 . (variable-pitch light 1.8))
          (2 . (variable-pitch regular 1.7))
          (3 . (variable-pitch regular 1.6))
          (4 . (variable-pitch regular 1.5))
          (5 . (variable-pitch 1.4)) ; absence of weight means `bold'
          (6 . (variable-pitch 1.3))
          (7 . (variable-pitch 1.2))
          (agenda-date . (semilight 1.5))
          (agenda-structure . (variable-pitch light 1.9))
          (t . (variable-pitch 1.1))))

  ;; The `ef-themes' provide lots of themes.  I want to pick one at
  ;; random when I start Emacs: the `ef-themes-load-random' does just
  ;; that (it can be called interactively as well).  I just check with
  ;; my desktop environment to determine if the choice should be about
  ;; a light or a dark theme.  Those functions are in my init.el.
  (if (prot-emacs-theme-environment-dark-p)
      (ef-themes-load-random 'dark)
    (ef-themes-load-random 'light)))

;; NOTE: For testing purposes
(prot-emacs-comment
  (:eval nil)
  (progn
    (mapc #'disable-theme custom-enabled-themes)

    (add-to-list 'load-path "/home/prot/Git/Projects/ef-themes/")

    (require 'ef-themes)
    (load-theme 'ef-arbutus t t)
    (load-theme 'ef-autumn t t)
    (load-theme 'ef-bio t t)
    (load-theme 'ef-cherie t t)
    (load-theme 'ef-cyprus t t)
    (load-theme 'ef-dark t t)
    (load-theme 'ef-day t t)
    (load-theme 'ef-deuteranopia-dark t t)
    (load-theme 'ef-deuteranopia-light t t)
    (load-theme 'ef-duo-dark t t)
    (load-theme 'ef-duo-light t t)
    (load-theme 'ef-eagle t t)
    (load-theme 'ef-frost t t)
    (load-theme 'ef-kassio t t)
    (load-theme 'ef-light t t)
    (load-theme 'ef-melissa-dark t t)
    (load-theme 'ef-melissa-light t t)
    (load-theme 'ef-night t t)
    (load-theme 'ef-owl t t)
    (load-theme 'ef-rosa t t)
    (load-theme 'ef-spring t t)
    (load-theme 'ef-summer t t)
    (load-theme 'ef-symbiosis t t)
    (load-theme 'ef-trio-dark t t)
    (load-theme 'ef-trio-light t t)
    (load-theme 'ef-tritanopia-dark t t)
    (load-theme 'ef-tritanopia-light t t)
    (load-theme 'ef-winter t t)

    (setq ef-themes-headings ; read the manual's entry or the doc string
          '((0 . (variable-pitch light 1.9))
            (1 . (variable-pitch light 1.8))
            (2 . (variable-pitch light 1.7))
            (3 . (variable-pitch semilight 1.6))
            (4 . (variable-pitch semilight 1.5))
            (5 . (variable-pitch regular 1.4))
            (6 . (variable-pitch regular 1.3))
            (7 . (variable-pitch regular 1.2))    ; absence of weight means `bold'
            (agenda-date . (semilight 1.5))
            (agenda-structure . (variable-pitch light 1.9))
            (t . (variable-pitch regular 1.1))))

    ;; They are nil by default...
    (setq ef-themes-mixed-fonts t
          ef-themes-variable-pitch-ui nil)

    (mapcar (lambda (theme)
              (add-to-list
               'custom-theme-load-path
               (concat "/home/prot/Git/Projects/ef-themes/" (symbol-name theme) "-theme.el")))
            (ef-themes--list-enabled-themes))

    (if (prot-emacs-theme-environment-dark-p)
        (ef-themes-load-random 'dark)
      (ef-themes-load-random 'light))

    (define-key global-map (kbd "<f5>") #'ef-themes-rotate)
    (define-key global-map (kbd "C-<f5>") #'ef-themes-select)))

(provide 'prot-emacs-ef-themes)
5.1.1.3. The prot-emacs-standard-themes.el module

This is one of the components of the prot-emacs-theme.el module (The prot-emacs-theme.el section to load a theme (prot-emacs-load-theme-family)). It defines some theme settings and also includes code I use when I need to test things.

The standard-themes are a pair of light and dark themes for GNU Emacs. They emulate the out-of-the-box looks of Emacs (which technically do NOT constitute a theme) while bringing to them thematic consistency, customizability, and extensibility. In practice, the Standard themes take the default style of the font-lock and Org faces, complement it with a wider and harmonious colour palette, address many inconsistencies, and apply established semantic patterns across all interfaces by supporting a large number of packages.

Note that the prot-emacs-comment is there for my testing purposes (The init.el macro to do nothing with Elisp code (prot-emacs-comment)).

;;; The Standard themes

;; The themes are customisable.  Read the manual:
;; <https://protesilaos.com/emacs/standard-themes>.

(use-package standard-themes
  :ensure t
  :demand t
  :bind ("<f5>" . standard-themes-toggle)
  :config
  (setq standard-themes-bold-constructs t
        standard-themes-italic-constructs t
        standard-themes-mixed-fonts t
        standard-themes-variable-pitch-ui t
        standard-themes-mode-line-accented nil

        ;; Accepts a symbol value
        standard-themes-fringes 'subtle

        ;; The following accept lists of properties
        standard-themes-links nil
        standard-themes-region nil
        standard-themes-prompts nil

        ;; more complex alist to set weight, height, and optional
        ;; `variable-pitch' per heading level (t is for any level not
        ;; specified)
        standard-themes-headings
        '((0 . (variable-pitch light 1.9))
          (1 . (variable-pitch light 1.8))
          (2 . (variable-pitch light 1.7))
          (3 . (variable-pitch semilight 1.6))
          (4 . (variable-pitch semilight 1.5))
          (5 . (variable-pitch 1.4))
          (6 . (variable-pitch 1.3))
          (7 . (variable-pitch 1.2))
          (agenda-date . (1.3))
          (agenda-structure . (variable-pitch light 1.8))
          (t . (variable-pitch 1.1))))

  ;; Load a theme that is consistent with my session's theme.  Those
  ;; functions are defined in my init.el.
  (if (prot-emacs-theme-environment-dark-p)
      (standard-themes-load-dark)
    (standard-themes-load-light)))

;; NOTE: For testing purposes
(prot-emacs-comment
  (:eval nil)
  (progn
    (mapc #'disable-theme custom-enabled-themes)

    (add-to-list 'load-path "/home/prot/Git/Projects/standard-themes/")

    (require 'standard-themes)
    (load-theme 'standard-dark t t)
    (load-theme 'standard-light t t)

    (setq standard-themes-bold-constructs t
          standard-themes-italic-constructs t
          standard-themes-disable-other-themes t
          standard-themes-mixed-fonts t
          standard-themes-variable-pitch-ui t
          standard-themes-mode-line-accented nil

          ;; Accepts a symbol value
          standard-themes-fringes 'subtle

          ;; The following accept lists of properties
          standard-themes-links nil
          standard-themes-region nil
          standard-themes-prompts nil

          ;; more complex alist to set weight, height, and optional
          ;; `variable-pitch' per heading level (t is for any level not
          ;; specified)
          standard-themes-headings
          '((0 . (variable-pitch light 1.9))
            (1 . (variable-pitch light 1.8))
            (2 . (variable-pitch light 1.7))
            (3 . (variable-pitch semilight 1.6))
            (4 . (variable-pitch semilight 1.5))
            (5 . (variable-pitch 1.4))
            (6 . (variable-pitch 1.3))
            (7 . (variable-pitch 1.2))
            (agenda-date . (1.3))
            (agenda-structure . (variable-pitch light 1.8))
            (t . (variable-pitch 1.1))))

    (mapcar (lambda (theme)
              (add-to-list
               'custom-theme-load-path
               (concat "/home/prot/Git/Projects/standard-themes/" (symbol-name theme) "-theme.el")))
            (standard-themes--list-enabled-themes))

    (if (prot-emacs-theme-environment-dark-p)
        (load-theme 'standard-dark :no-confirm)
      (load-theme 'standard-light :no-confirm))

    (define-key global-map (kbd "<f5>") #'standard-themes-toggle)))

(provide 'prot-emacs-standard-themes)

5.1.2. The prot-emacs-theme.el section for pulsar

This is a small package of mine that temporarily highlights the current line after a given function is invoked. The affected functions are defined in the user option pulsar-pulse-functions. What Pulsar does is set up an advice so that those functions run a hook after they are called. The pulse effect is added there (pulsar-after-function-hook).

;;;; Pulsar
;; Read the pulsar manual: <https://protesilaos.com/emacs/pulsar>.
(use-package pulsar
  :ensure t
  :config
  (setopt pulsar-pulse t
          pulsar-delay 0.055
          pulsar-iterations 10
          pulsar-face 'pulsar-green
          pulsar-highlight-face 'pulsar-magenta)

  (pulsar-global-mode 1)
  :hook
  ;; There are convenience functions/commands which pulse the line using
  ;; a specific colour: `pulsar-pulse-line-red' is one of them.
  ((next-error . (pulsar-pulse-line-red pulsar-recenter-top pulsar-reveal-entry))
   (minibuffer-setup . pulsar-pulse-line-red))
  :bind
  ;; pulsar does not define any key bindings.  This is just my personal
  ;; preference.  Remember to read the manual on the matter.  Evaluate:
  ;;
  ;; (info "(elisp) Key Binding Conventions")
  (("C-x l" . pulsar-pulse-line) ; override `count-lines-page'
   ("C-x L" . pulsar-highlight-dwim))) ; or use `pulsar-highlight-line'

5.1.3. The prot-emacs-theme.el section for lin

My lin package is a stylistic enhancement for Emacs’ built-in hl-line-mode. It remaps the hl-line face (or equivalent) buffer-locally to a style that is optimal for major modes where line selection is the primary mode of interaction.

The idea is that hl-line-mode 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: the user has to select a line. 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 reminder of the point’s location on the vertical axis.

;;;; Lin
;; Read the lin manual: <https://protesilaos.com/emacs/lin>.
(use-package lin
  :ensure t
  :hook (after-init . lin-global-mode) ; applies to all `lin-mode-hooks'
  :config
  ;; You can use this to live update the face:
  ;;
  ;; (customize-set-variable 'lin-face 'lin-green)
  ;;
  ;; Or `setopt' on Emacs 29: (setopt lin-face 'lin-yellow)
  ;;
  ;; I still prefer `setq' for consistency.
  (setq lin-face 'lin-cyan))

5.1.4. The prot-emacs-theme.el section for spacious-padding

This package provides a global minor mode to increase the spacing/padding of Emacs windows and frames. The idea is to make editing and reading feel more comfortable. Enable the mode with M-x spacious-padding-mode. Adjust the exact spacing values by modifying the user option spacious-padding-widths.

Inspiration for this package comes from Nicolas Rougier’s impressive designs and Daniel Mendler’s org-modern package.

I also take care to make it work with my customised mode line (The prot-emacs-modeline.el module).

;;;; Increase padding of windows/frames
;; Yet another one of my packages:
;; <https://protesilaos.com/codelog/2023-06-03-emacs-spacious-padding/>.
(use-package spacious-padding
  :ensure t
  :if (display-graphic-p)
  :hook (after-init . spacious-padding-mode)
  :bind ("<f8>" . spacious-padding-mode)
  :init
  ;; These are the defaults, but I keep it here for visiibility.
  (setq spacious-padding-widths
        '( :internal-border-width 30
           :header-line-width 4
           :mode-line-width 6
           :tab-width 4
           :right-divider-width 30
           :scroll-bar-width 8
           :left-fringe-width 20
           :right-fringe-width 20))

  ;; (setq spacious-padding-subtle-mode-line
  ;;       `( :mode-line-active ,(if (or (eq prot-emacs-load-theme-family 'modus)
  ;;                                     (eq prot-emacs-load-theme-family 'standard))
  ;;                                 'default
  ;;                               'help-key-binding)
  ;;          :mode-line-inactive window-divider))

  ;; Read the doc string of `spacious-padding-subtle-mode-line' as
  ;; it is very flexible.
  (setq spacious-padding-subtle-mode-line nil))

5.1.5. The prot-emacs-theme.el section for rainbow-mode

This package produces an in-buffer preview of a colour value. I use those while developing my themes, hence the prot/rainbow-mode-in-themes to activate rainbow-mode if I am editing a theme file.

;;;; Rainbow mode for colour previewing (rainbow-mode.el)
(use-package rainbow-mode
  :ensure t
  :init
  (setq rainbow-ansi-colors nil)
  (setq rainbow-x-colors nil)

  (defun prot/rainbow-mode-in-themes ()
    (when-let* ((file (buffer-file-name))
                ((derived-mode-p 'emacs-lisp-mode))
                ((string-match-p "-theme" file)))
      (rainbow-mode 1)))
  :bind ( :map ctl-x-x-map
          ("c" . rainbow-mode)) ; C-x x c
  :hook (emacs-lisp-mode . prot/rainbow-mode-in-themes))

5.1.6. The prot-emacs-theme.el section for cursory

My cursory package provides a thin wrapper around built-in variables that affect the style of the Emacs cursor on graphical terminals. The intent is to allow the user to define preset configurations such as “block with slow blinking” or “bar with fast blinking” and set them on demand. The use-case for such presets is to adapt to evolving interface requirements and concomitant levels of expected comfort, such as in the difference between writing and reading.

;;; Cursor appearance (cursory)
;; Read the manual: <https://protesilaos.com/emacs/cursory>.
(use-package cursory
  :ensure t
  :demand t
  :if (display-graphic-p)
  :config
  (setq cursory-presets
        '((box
           :blink-cursor-interval 1.2)
          (box-no-blink
           :blink-cursor-mode -1)
          (bar
           :cursor-type (bar . 2)
           :blink-cursor-interval 0.8)
          (bar-no-other-window
           :inherit bar
           :cursor-in-non-selected-windows nil)
          (bar-no-blink
           :cursor-type (bar . 2)
           :blink-cursor-mode -1)
          (underscore
           :cursor-type (hbar . 3)
           :blink-cursor-interval 0.3
           :blink-cursor-blinks 50)
          (underscore-no-other-window
           :inherit underscore
           :cursor-in-non-selected-windows nil)
          (underscore-thick
           :cursor-type (hbar . 8)
           :blink-cursor-interval 0.3
           :blink-cursor-blinks 50
           :cursor-in-non-selected-windows (hbar . 3))
          (underscore-thick-no-blink
           :blink-cursor-mode -1
           :cursor-type (hbar . 8)
           :cursor-in-non-selected-windows (hbar . 3))
          (t ; the default values
           :cursor-type box
           :cursor-in-non-selected-windows hollow
           :blink-cursor-mode 1
           :blink-cursor-blinks 10
           :blink-cursor-interval 0.2
           :blink-cursor-delay 0.2)))

  ;; I am using the default values of `cursory-latest-state-file'.

  ;; Set last preset or fall back to desired style from `cursory-presets'.
  (cursory-set-preset (or (cursory-restore-latest-preset) 'box))

  (cursory-mode 1)
  :bind
  ;; We have to use the "point" mnemonic, because C-c c is often the
  ;; suggested binding for `org-capture' and is the one I use as well.
  ("C-c p" . cursory-set-preset))

5.1.7. The prot-emacs-theme.el section for theme-buffet

The theme-buffet package arranges to automatically change themes during specific times of the day or at fixed intervals. The collection of themes is customisable, with the default options covering the built-in Emacs themes as well as my modus-themes and ef-themes.

Bruno Boal is the lead developer and I am a co-maintainer.

;;;; Theme buffet
(use-package theme-buffet
  :ensure t
  :after (:any modus-themes ef-themes)
  :defer 1
  :config
  (let ((modus-themes-p (featurep 'modus-themes))
        (ef-themes-p (featurep 'ef-themes)))
    (setq theme-buffet-menu 'end-user)
    (setq theme-buffet-end-user
          (cond
           ((and modus-themes-p ef-themes-p)
            '( :night     (modus-vivendi ef-dark ef-winter ef-autumn ef-night ef-duo-dark ef-symbiosis)
               :morning   (modus-operandi ef-light ef-cyprus ef-spring ef-frost ef-duo-light)
               :afternoon (modus-operandi-tinted ef-arbutus ef-day ef-kassio ef-summer ef-elea-light ef-maris-light ef-melissa-light ef-trio-light ef-reverie)
               :evening   (modus-vivendi-tinted ef-rosa ef-elea-dark ef-maris-dark ef-melissa-dark ef-trio-dark ef-dream)))
           (ef-themes-p
            '( :night     (ef-dark ef-winter ef-autumn ef-night ef-duo-dark ef-symbiosis ef-owl)
               :morning   (ef-light ef-cyprus ef-spring ef-frost ef-duo-light ef-eagle)
               :afternoon (ef-arbutus ef-day ef-kassio ef-summer ef-elea-light ef-maris-light ef-melissa-light ef-trio-light ef-reverie)
               :evening   (ef-rosa ef-elea-dark ef-maris-dark ef-melissa-dark ef-trio-dark ef-dream)))
           (modus-themes-p
            '( :night     (modus-vivendi modus-vivendi-tinted modus-vivendi-tritanopia modus-vivendi-deuteranopia)
               :morning   (modus-operandi modus-operandi-tinted modus-operandi-tritanopia modus-operandi-deuteranopia)
               :afternoon (modus-operandi modus-operandi-tinted modus-operandi-tritanopia modus-operandi-deuteranopia)
               :evening   (modus-vivendi modus-vivendi-tinted modus-vivendi-tritanopia modus-vivendi-deuteranopia)))))

    (when (or modus-themes-p ef-themes-p)
      (theme-buffet-timer-hours 1))))

5.1.8. The prot-emacs-theme.el section about fontaine

[ Watch: Customise Emacs fonts (2024-01-16) ]

My fontaine package allows the user to define detailed font configurations and set them on demand. For example, one can have a regular-editing preset and another for presentation-mode (these are arbitrary, user-defined symbols): the former uses small fonts which are optimised for writing, while the latter applies typefaces that are pleasant to read at comfortable point sizes.

You will notice that all my fonts are Iosevka Comfy variants. I explain what this is about (Information about my Iosevka Comfy fonts).

Another section defines some complementary functionality (The prot-emacs-theme.el section about variable-pitch-mode and font resizing).

Also check my show-font package (The prot-emacs-theme.el section about show-font).

;;;; Fontaine (font configurations)
;; Read the manual: <https://protesilaos.com/emacs/fontaine>
(use-package fontaine
  :ensure t
  :if (display-graphic-p)
  :hook
  ;; Persist the latest font preset when closing/starting Emacs and
  ;; while switching between themes.
  ((after-init . fontaine-mode)
   (after-init . (lambda ()
                   ;; Set last preset or fall back to desired style from `fontaine-presets'.
                   (fontaine-set-preset (or (fontaine-restore-latest-preset) 'regular)))))
  :bind (("C-c f" . fontaine-set-preset)
         ("C-c F" . fontaine-toggle-preset))
  :config
  ;; This is defined in Emacs C code: it belongs to font settings.
  (setq x-underline-at-descent-line nil)

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

  ;; This is the default value.  Just including it here for
  ;; completeness.
  (setq fontaine-latest-state-file (locate-user-emacs-file "fontaine-latest-state.eld"))

  (setq fontaine-presets
        '((small
           :default-height 80)
          (regular) ; like this it uses all the fallback values and is named `regular'
          (medium
           :default-weight semilight
           :default-height 115
           :bold-weight extrabold)
          (large
           :inherit medium
           :default-height 150)
          (live-stream
           :default-family "Iosevka Comfy Wide Motion"
           :default-height 150
           :default-weight medium
           :fixed-pitch-family "Iosevka Comfy Wide Motion"
           :variable-pitch-family "Iosevka Comfy Wide Duo"
           :bold-weight extrabold)
          (presentation
           :default-height 180)
          (jumbo
           :default-height 260)
          (t
           ;; I keep all properties for didactic purposes, but most can be
           ;; omitted.  See the fontaine manual for the technicalities:
           ;; <https://protesilaos.com/emacs/fontaine>.
           :default-family "Iosevka Comfy"
           :default-weight regular
           :default-slant normal
           :default-width normal
           :default-height 100

           :fixed-pitch-family "Iosevka Comfy"
           :fixed-pitch-weight nil
           :fixed-pitch-slant nil
           :fixed-pitch-width nil
           :fixed-pitch-height 1.0

           :fixed-pitch-serif-family nil
           :fixed-pitch-serif-weight nil
           :fixed-pitch-serif-slant nil
           :fixed-pitch-serif-width nil
           :fixed-pitch-serif-height 1.0

           :variable-pitch-family "Iosevka Comfy Motion Duo"
           :variable-pitch-weight nil
           :variable-pitch-slant nil
           :variable-pitch-width nil
           :variable-pitch-height 1.0

           :mode-line-active-family nil
           :mode-line-active-weight nil
           :mode-line-active-slant nil
           :mode-line-active-width nil
           :mode-line-active-height 1.0

           :mode-line-inactive-family nil
           :mode-line-inactive-weight nil
           :mode-line-inactive-slant nil
           :mode-line-inactive-width nil
           :mode-line-inactive-height 1.0

           :header-line-family nil
           :header-line-weight nil
           :header-line-slant nil
           :header-line-width nil
           :header-line-height 1.0

           :line-number-family nil
           :line-number-weight nil
           :line-number-slant nil
           :line-number-width nil
           :line-number-height 1.0

           :tab-bar-family nil
           :tab-bar-weight nil
           :tab-bar-slant nil
           :tab-bar-width nil
           :tab-bar-height 1.0

           :tab-line-family nil
           :tab-line-weight nil
           :tab-line-slant nil
           :tab-line-width nil
           :tab-line-height 1.0

           :bold-family nil
           :bold-slant nil
           :bold-weight bold
           :bold-width nil
           :bold-height 1.0

           :italic-family nil
           :italic-weight nil
           :italic-slant italic
           :italic-width nil
           :italic-height 1.0

           :line-spacing nil)))

  (with-eval-after-load 'pulsar
    (add-hook 'fontaine-set-preset-hook #'pulsar-pulse-line)))

5.1.9. The prot-emacs-theme.el section about show-font

This is yet another package of mine. It lets you preview a font inside of Emacs. It does so in three ways:

  • Prompt for a font on the system and display it in a buffer.
  • List all known fonts in a buffer, with a short preview for each.
  • Provide a major mode to preview a font whose file is among the installed ones.

Check out its sources:

To actually set fonts, I use my fontaine package (The prot-emacs-theme.el section about fontaine).

;;;; Show Font (preview fonts)
;; Read the manual: <https://protesilaos.com/emacs/show-font>
(use-package show-font
  :ensure t
  :commands (show-font-select-preview show-font-list)
  :config
  ;; These are the defaults, but I keep them here for easier access.
  (setq show-font-pangram 'prot)
  (setq show-font-character-sample
        "
ABCDEFGHIJKLMNOPQRSTUVWXYZ
abcdefghijklmnopqrstuvwxyz
0123456789   !@#$¢%^&*~|
`'\"‘’“”.,;:  ()[]{}—-_+=<>

()[]{}<>«»‹› 6bB8&0ODdoa 1tiIlL|\/
!ij c¢ 5$Ss 7Z2z 9gqp nmMNNMW uvvwWuuw
x×X .,·°;:¡!¿?`'‘’   ÄAÃÀ TODO
"))

5.1.10. The prot-emacs-theme.el section about variable-pitch-mode and font resizing

[ Watch: Customise Emacs fonts (2024-01-16) ]

The built-in variable-pitch-mode makes the current buffer use a proportionately spaced font. In technical terms, it remaps the default face to variable-pitch, so whatever applies to the latter takes effect over the former. I take care of their respective font families in my fontaine setup (The prot-emacs-theme.el section about fontaine).

I want to activate variable-pitch-mode in all buffers where I normally focus on prose. The exact mode hooks are specified in the variable prot/enable-variable-pitch-in-hooks. Exceptions to these are major modes that I do not consider related to prose (and which in my opinion should not be derived from text-mode): these are excluded in the function prot/enable-variable-pitch.

Then I also arrange the key bindings that resize text on-the-fly. I want those to have a global effect, meaning that they affect all buffers and the minibuffer, instead of the default behaviour to only adjust the current buffer’s text size (Why don’t you remap keys?).

;;;;; `variable-pitch-mode' setup
(use-package face-remap
  :ensure nil
  :functions prot/enable-variable-pitch
  :bind ( :map ctl-x-x-map
          ("v" . variable-pitch-mode))
  :hook ((text-mode notmuch-show-mode elfeed-show-mode) . prot/enable-variable-pitch)
  :config
  ;; NOTE 2022-11-20: This may not cover every case, though it works
  ;; fine in my workflow.  I am still undecided by EWW.
  (defun prot/enable-variable-pitch ()
    (unless (derived-mode-p 'mhtml-mode 'nxml-mode 'yaml-mode)
      (variable-pitch-mode 1)))
;;;;; Resize keys with global effect
  :bind
  ;; Emacs 29 introduces commands that resize the font across all
  ;; buffers (including the minibuffer), which is what I want, as
  ;; opposed to doing it only in the current buffer.  The keys are the
  ;; same as the defaults.
  (("C-x C-=" . global-text-scale-adjust)
   ("C-x C-+" . global-text-scale-adjust)
   ("C-x C-0" . global-text-scale-adjust)))
5.1.10.1. Information about my Iosevka Comfy fonts

Iosevka Comfy is a customised build of the Iosevka typeface, with a consistent rounded style and overrides for almost all individual glyphs in both roman (upright) and italic (slanted) variants. Many font families are available, covering a broad range of typographic weights. The README file in the git repository covers all the technicalities.

Family Shapes Spacing Style Ligatures
Iosevka Comfy Sans Compact Monospaced Yes
Iosevka Comfy Fixed Sans Compact Monospaced No
Iosevka Comfy Duo Sans Compact Duospaced Yes
Iosevka Comfy Motion Slab Compact Monospaced Yes
Iosevka Comfy Motion Fixed Slab Compact Monospaced No
Iosevka Comfy Motion Duo Slab Compact Duospaced Yes
Iosevka Comfy Wide Sans Wide Monospaced Yes
Iosevka Comfy Wide Fixed Sans Wide Monospaced No
Iosevka Comfy Wide Duo Sans Wide Duospaced Yes
Iosevka Comfy Wide Motion Slab Wide Monospaced Yes
Iosevka Comfy Wide Motion Fixed Slab Wide Monospaced No
Iosevka Comfy Wide Motion Duo Slab Wide Duospaced Yes

5.1.11. The prot-emacs-theme.el call to provide

Finally, we provide the module. This is the mirror function of require (The init.el final part to load the individual modules).

(provide 'prot-emacs-theme)

5.2. The prot-emacs-essentials.el module

This module load basic configurations that apply to most facets of Emacs. Many of my own custom libraries are loaded here (The init.el arrangements for my own modules and custom libraries).

5.2.1. The prot-emacs-essentials.el block with basic configurations

Better use C-h v (M-x describe-variable) to learn about each of these variables. Since I am here, I also unbind or repurpose some of the default key bindings.

;;; Essential configurations
(use-package emacs
  :ensure nil
  :demand t
  :config
;;;; General settings and common custom functions (prot-simple.el)
  (setq blink-matching-paren nil)
  (setq delete-pair-blink-delay 0.1) ; Emacs28 -- see `prot-simple-delete-pair-dwim'
  (setq delete-pair-push-mark t) ; Emacs 31
  (setq help-window-select t)
  (setq next-error-recenter '(4)) ; center of the window
  (setq find-library-include-other-files nil) ; Emacs 29
  (setq remote-file-name-inhibit-delete-by-moving-to-trash t) ; Emacs 30
  (setq remote-file-name-inhibit-auto-save t)                 ; Emacs 30
  (setq tramp-connection-timeout (* 60 10)) ; seconds
  (setq save-interprogram-paste-before-kill t)
  (setq mode-require-final-newline 'visit-save)
  (setq-default truncate-partial-width-windows nil)
  (setq eval-expression-print-length nil)
  (setq kill-do-not-save-duplicates t)
  (setq duplicate-line-final-position -1 ; both are Emacs 29
        duplicate-region-final-position -1)
  (setq scroll-error-top-bottom t)
  (setq echo-keystrokes-help nil) ; Emacs 30
  (setq epa-keys-select-method 'minibuffer) ; Emacs 30

  ;; Keys I unbind here are either to avoid accidents or to bind them
  ;; elsewhere later in the configuration.
  :bind
  ( :map global-map
    ("<f2>" . toggle-input-method)  ; F2 overrides that two-column gimmick.  Sorry, but no!
    ("<insert>" . nil)
    ("<menu>" . nil)
    ("C-x C-d" . nil) ; never use it
    ("C-x C-v" . nil) ; never use it
    ("C-z" . nil) ; I have a window manager, thanks!
    ("C-x C-z" . nil) ; same idea as above
    ("C-x C-c" . nil) ; avoid accidentally exiting Emacs
    ("C-x C-c C-c" . save-buffers-kill-emacs) ; more cumbersome, less error-prone
    ("C-x C-r" . restart-emacs) ; override `find-file-read-only'
    ("C-h h" . nil) ; Never show that "hello" file
    ("M-`" . nil)
    ("M-o" . delete-blank-lines) ; alias for C-x C-o
    ("M-SPC" . cycle-spacing)
    ("M-z" . zap-up-to-char) ; NOT `zap-to-char'
    ("M-c" . capitalize-dwim)
    ("M-l" . downcase-dwim) ; "lower" case
    ("M-u" . upcase-dwim)
    ("M-=" . count-words)
    ("C-x O" . next-multiframe-window)
    ("C-h K" . describe-keymap) ; overrides `Info-goto-emacs-key-command-node'
    ("C-h u" . apropos-user-option)
    ("C-h F" . apropos-function) ; lower case is `describe-function'
    ("C-h V" . apropos-variable) ; lower case is `describe-variable'
    ("C-h L" . apropos-library) ; lower case is `view-lossage'
    ("C-h c" . describe-char) ; overrides `describe-key-briefly'

    :map prog-mode-map
    ("C-M-d" . up-list) ; confusing name for what looks like "down" to me
    ("<C-M-backspace>" . backward-kill-sexp)

    ;; Keymap for buffers (Emacs28)
    :map ctl-x-x-map
    ("f" . follow-mode)  ; override `font-lock-update'
    ("r" . rename-uniquely)
    ("l" . visual-line-mode)))

5.2.2. The prot-emacs-essentials.el section about prot-common.el (custom basic functions)

The prot-common.el library contains custom snippets that I use in various other parts of my setup, notably my custom libraries (“packages”). All I do here is load it, so that others will have it available by the time they need it.

Since the prot-common.el defines some basic functions that can be used outside the narrow confines of my custom code, I set them up here:

  1. Truncate lines by default in a number of places and do not produce a message about the fact. Note that the function used to achieve this, i.e. prot-common-truncate-lines-silently, may also be set up elsewhere and described in that context. Here I only cover the basic parent modes.
  2. Make M-x not produce any message about how such and such command can also be called via this or that key binding. If I am doing something with M-x instead of a key binding, I have a good reason for it and do not want the extra noise.
(use-package prot-common
  :ensure nil
  :functions (prot-common-truncate-lines-silently)
  :hook ((fundamental-mode text-mode prog-mode dired-mode) . prot-common-truncate-lines-silently)
  :config
  ;; NEVER tell me which key can call a command that I specifically
  ;; invoked with M-x: I have a good reason to use it that way.
  (advice-add #'execute-extended-command--describe-binding-msg :override #'prot-common-ignore))

5.2.3. The prot-emacs-essentials.el section about prot-simple.el (custom basic commands)

The prot-simple.el library is done in the same spirit as the built-in simple.el: it is a file with a collection of little commands that are useful for everyday tasks. I bind these commands to keys.

The prot-simple-display-unsaved-buffers-on-exit produces a list of unsaved, file-visiting buffers before closing Emacs. I am doing this using the advice mechanism to redefine the behaviour of the original command (save-buffers-kill-emacs), since I cannot find a way to do this via some hook.

(use-package prot-simple
  :ensure nil
  :demand t
  :config
  (setq prot-simple-date-specifier "%F")
  (setq prot-simple-time-specifier "%R %z")

  (advice-add #'save-buffers-kill-emacs :before #'prot-simple-display-unsaved-buffers-on-exit)

  (with-eval-after-load 'pulsar
    (add-hook 'prot-simple-file-to-register-jump-hook #'pulsar-recenter-center)
    (add-hook 'prot-simple-file-to-register-jump-hook #'pulsar-reveal-entry))
  :bind
  ( ("ESC ESC" . prot-simple-keyboard-quit-dwim)
    ("C-g" . prot-simple-keyboard-quit-dwim)
    ("C-M-SPC" . prot-simple-mark-sexp)   ; will be overriden by `expreg' if tree-sitter is available
    ;; Commands for lines
    ("M-k" . prot-simple-kill-line-backward)
    ("M-j" . delete-indentation)
    ("M-w" . prot-simple-kill-ring-save)
    ("C-S-d" . prot-simple-duplicate-line-or-region)
    ("C-S-w" . prot-simple-copy-line)
    ("C-S-y" . prot-simple-yank-replace-line-or-region)
    ("C-v" . prot-simple-multi-line-below) ; overrides `scroll-up-command'
    ("<next>" . prot-simple-multi-line-below) ; overrides `scroll-up-command'
    ("M-v" . prot-simple-multi-line-above) ; overrides `scroll-down-command'
    ("<prior>" . prot-simple-multi-line-above) ; overrides `scroll-down-command'
    ("<C-return>" . prot-simple-new-line-below)
    ("<C-S-return>" . prot-simple-new-line-above)
    ("C-x x a" . prot-simple-auto-fill-visual-line-mode) ; auto-fill/visual-line toggle
    ;; Commands for text insertion or manipulation
    ("C-=" . prot-simple-insert-date)
    ("C-<" . prot-simple-escape-url-dwim)
    ;; "C->" prot-simple-insert-line-prefix-dwim
    ("M-Z" . prot-simple-zap-to-char-backward)
    ;; Commands for object transposition
    ("C-S-p" . prot-simple-move-above-dwim)
    ("C-S-n" . prot-simple-move-below-dwim)
    ("C-t" . prot-simple-transpose-chars)
    ("C-x C-t" . prot-simple-transpose-lines)
    ("C-S-t" . prot-simple-transpose-paragraphs)
    ("C-x M-t" . prot-simple-transpose-sentences)
    ("C-M-t" . prot-simple-transpose-sexps)
    ("M-t" . prot-simple-transpose-words)
    ;; Commands for paragraphs
    ("M-Q" . prot-simple-unfill-region-or-paragraph)
    ;; Commands for windows and pages
    ("C-x o" . prot-simple-other-window)
    ("C-x n k" . prot-simple-delete-page-delimiters)
    ("C-x M-r" . prot-simple-swap-window-buffers)
    ;; Commands for buffers
    ("<C-f2>" . prot-simple-rename-file-and-buffer)
    ("C-x k" . prot-simple-kill-buffer-current)
    ("C-x K" . kill-buffer) ; leaving this here to contrast with the above
    ("M-s b" . prot-simple-buffers-major-mode)
    ("M-s v" . prot-simple-buffers-vc-root)
    ;; Commands for files
    ("C-x r ." . prot-simple-file-to-register)))

5.2.4. The prot-emacs-essentials.el section about prot-scratch.el (scratch buffer per major mode)

The prot-scratch.el library provides the means to create a scratch buffer for a given major mode. It has the option to set a default major mode to use. It can also copy the active region into the scratch buffer. Read the doc string of the command prot-scratch-buffer.

;;;; Scratch buffers per major mode (prot-scratch.el)
(use-package prot-scratch
  :ensure nil
  :bind ("C-c s" . prot-scratch-buffer)
  :config
  (setq prot-scratch-default-mode 'text-mode))

5.2.5. The prot-emacs-essentials.el section about prot-pair.el (insert character pairs)

The prot-pair.el library defines a mechanism to insert character pairs around the symbol at point or the active region. There is a user option called prot-pair-pairs, which specifies which characters form pairs. This can also run a function to set a pair according to some context, environment, or any such condition. I use it to insert quotation marks specific to natural languages I have set up.

;;;; Insert character pairs (prot-pair.el)
(use-package prot-pair
  :ensure nil
  :bind
  (("C-'" . prot-pair-insert)
   ("M-'" . prot-pair-insert)
   ("M-\\" . prot-pair-delete)))

5.2.6. The prot-emacs-essentials.el section for comments

The prot-comment.el library expands the built-in commenting facilities with what makes sense to me. The prot-comment command is like the built-in comment-dwim, but toggles linewise commenting instead of appending to them by default. While the prot-comment-timestamp-keyword prompts for a keyword among prot-comment-keywords and formats it as a comment with a timestamp next to it. The format of the latter is controlled by the user option prot-comment-timestamp-format-concise or prot-comment-timestamp-format-verbose (the verbose is set when the command is called with a prefix argument (C-u by default)).

A big part of writing code is the ability to quickly insert comments. I have a custom library that builds on what Emacs makes available by default (The prot-comment.el library). While I use this section of the module to set my desired configurations.

;;;; Comments (prot-comment.el)
(use-package prot-comment
  :ensure nil
  :init
  (setq comment-empty-lines t)
  (setq comment-fill-column nil)
  (setq comment-multi-line t)
  (setq comment-style 'multi-line)
  (setq-default comment-column 0)

  (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")
  :bind
  (("C-;" . prot-comment)
   ("C-x C-;" . prot-comment-timestamp-keyword)))

5.2.7. The prot-emacs-essentials.el section about prot-prefix.el (prefix nested keymaps)

[ Watch: define prefix/leader key (nested key maps) (2024-01-29). ]

The prot-prefix.el library defines a set of keymaps with commonly used commands and puts them behind a prefix map. The idea is to hit a series of keys to get to the desired command. Keymaps are organised thematically and rely on strong mnemonics, such as b for buffers, w for windows, and so on. The which-key package is a nice addition for this purpose, as it visualises incomplete key bindings after a configurable amount of time (The prot-emacs-which-key.el module).

;;;; Prefix keymap (prot-prefix.el)
(use-package prot-prefix
  :ensure nil
  :bind-keymap
  (("<insert>" . prot-prefix)
   ("C-z" . prot-prefix)))

5.2.8. The prot-emacs-essentials.el configuration to track recently visited files

Emacs can keep track of recently visited files. Then we can revisit them with the command recent-open, which provides minibuffer completion (The prot-emacs-completion.el module).

Recent files are also available in the consult-buffer interface, which makes it a one-stop-shop for opening buffers, recent files, or bookmarks (The prot-emacs-completion.el settings for consult). This can be better than having to remember if something is a buffer or is stored by bookmarks/recentf. Same idea for using one command instead of three (or more).

I generally do not rely on recentf-mode, as most of my work is done in projects, which I switch to directly. Though I sometimes need to revisit a file that I do not need to keep track of.

(use-package recentf
  :ensure nil
  :hook (after-init . recentf-mode)
  :config
  (setq recentf-max-saved-items 100)
  (setq recentf-max-menu-items 25) ; I don't use the `menu-bar-mode', but this is good to know
  (setq recentf-save-file-modes nil)
  (setq recentf-keep nil)
  (setq recentf-auto-cleanup nil)
  (setq recentf-initialize-file-name-history nil)
  (setq recentf-filename-handlers nil)
  (setq recentf-show-file-shortcuts-flag nil))

5.2.9. The prot-emacs-essentials.el mouse configurations

I do use the mouse on occasion. More so when I am doing a video demonstration.

;;;; Mouse and mouse wheel behaviour
(use-package mouse
  :ensure nil
  :hook (after-init . mouse-wheel-mode)
  :config
  ;; Some of these variables are defined in places other than
  ;; mouse.el, but this is fine.
  (setq mouse-autoselect-window t) ; complements the auto-selection of my tiling window manager

  ;; In Emacs 27+, use Control + mouse wheel to scale text.
  (setq mouse-wheel-scroll-amount
        '(1
          ((shift) . 5)
          ((meta) . 0.5)
          ((control) . text-scale))
        mouse-drag-copy-region nil
        make-pointer-invisible t
        mouse-wheel-progressive-speed t
        mouse-wheel-follow-mouse t)

  ;; Scrolling behaviour
  (setq-default scroll-preserve-screen-position t
                scroll-conservatively 1 ; affects `scroll-step'
                scroll-margin 0
                next-screen-context-lines 0))

5.2.10. The prot-emacs-essentials.el settings for repeat-mode

The repeat-mode is designed to find when a “repeatable” command is called and arrange so that it can be called again with single key press. A case in point is the other-window command, bound to C-x o by default. With repeat-mode enabled, we can type C-x o to invoke the command and then type o to call it again. So C-x o o o runs other-window three times. This is quite convenient.

I think repeatable commands are not easy to define because (i) we have to put a property to their symbol and (ii) have them in a keymap that repeat-mode knows about. The defvar-keymap of Emacs 29 makes this a bit easier for users, though it still is a rather advanced feature.

In most cases, just know that repeat-mode is nice to have, though you can probably use Emacs just fine without it.

;;;; Repeatable key chords (repeat-mode)
(use-package repeat
  :ensure nil
  :hook (after-init . repeat-mode)
  :config
  (setq repeat-on-final-keystroke t
        repeat-exit-timeout 5
        repeat-exit-key "<escape>"
        repeat-keep-prefix nil
        repeat-check-key t
        repeat-echo-function 'ignore
        ;; Technically, this is not in repeal.el, though it is the
        ;; same idea.
        set-mark-command-repeat-pop t))

5.2.11. The prot-emacs-essentials.el settings for bookmarks

Bookmarks are compartments that store arbitrary information about a file or buffer. The records are used to recreate that file/buffer inside of Emacs. Put differently, we can easily jump back to a file or directory (or anything that has a bookmark recorder+handler, really). Use the bookmark-set command (C-x r m by default) to record a bookmark and then visit one of your bookmarks with bookmark-jump (C-x r b by default).

Also see the prot-emacs-essentials.el settings for registers.

;;;; Built-in bookmarking framework (bookmark.el)
(use-package bookmark
  :ensure nil
  :commands (bookmark-set bookmark-jump bookmark-bmenu-list)
  :hook (bookmark-bmenu-mode . hl-line-mode)
  :config
  (setq bookmark-use-annotations nil)
  (setq bookmark-automatically-show-annotations nil)
  (setq bookmark-fringe-mark nil) ; Emacs 29 to hide bookmark fringe icon
  ;; Write changes to the bookmark file as soon as 1 modification is
  ;; made (addition or deletion).  Otherwise Emacs will only save the
  ;; bookmarks when it closes, which may never happen properly
  ;; (e.g. power failure).
  (setq bookmark-save-flag 1))

5.2.12. The prot-emacs-essentials.el settings for registers

[ Watch: Mark and register basics (2023-06-28). ]

Much like bookmarks, registers store data that we can reinstate quickly (The prot-emacs-essentials.el settings for bookmarks). A common use-case is to write some text to a register and then insert that text by calling the given register. This is much better than relying on the kill-ring, because registers are meant to be overwritten by the user, whereas the kill-ring accumulates lots of text that we do not necessarily need.

To me, registers are essential for keyboard macros. By default, registers do not persist between Emacs sessions, though I do need to re-use them from time to time, hence the arrangement to record them with savehist-mode (The prot-emacs-completion.el settings for saving the history (savehist-mode)).

;;;; Registers (register.el)
(use-package register
  :ensure nil
  :defer t ; its commands are autoloaded, so this will be loaded then
  :config
  (setq register-preview-delay 0.8
        register-preview-function #'register-preview-default)

  (with-eval-after-load 'savehist
    (add-to-list 'savehist-additional-variables 'register-alist)))

5.2.13. The prot-emacs-essentials.el settings for auto revert

The “auto revert” facility makes Emacs update the contents of a saved buffer when its underlying file is changed externally. This can happen, for example, when a git pull modifies the file we are already displaying in a buffer. Emacs thus automatically reverts the buffer to reflect the new file contents.

;;;; Auto revert mode
(use-package autorevert
  :ensure nil
  :hook (after-init . global-auto-revert-mode)
  :config
  (setq auto-revert-verbose t))

5.2.14. The prot-emacs-essentials.el section for delete-selection-mode

Every graphical application I have ever used will delete the selected text upon the insertion of new text. Emacs does not do this by default. With delete-selection-mode we get it.

;;;; Delete selection
(use-package delsel
  :ensure nil
  :hook (after-init . delete-selection-mode))

5.2.15. The prot-emacs-essentials.el settings for tooltips

With these settings in place, Emacs will use its own faces and frame infrastructure to display tooltips. I prefer it this way because then we can benefit from the text properties that can be added to these messages (e.g. a different colour or a slant).

;;;; Tooltips (tooltip-mode)
(use-package tooltip
  :ensure nil
  :hook (after-init . tooltip-mode)
  :config
  (setq tooltip-delay 0.5
        tooltip-short-delay 0.5
        x-gtk-use-system-tooltips t
        tooltip-frame-parameters
        '((name . "tooltip")
          (internal-border-width . 10)
          (border-width . 0)
          (no-special-glyphs . t))))

5.2.16. The prot-emacs-essentials.el configurations for the date and time (display-time-mode)

I like to display the current date and time on the mode line(The prot-emacs-modeline.el module). This is what display-time-mode does. Note that my custom modeline shows the time only in the active/selected window. Otherwise, the default is to show the time on all mode lines, which is annoying.

The time.el library which provides the display-time-mode also defines functions to get the load average and check a directory for new emails. I have no use for the load avergae, while emails counters are best handled by my notmuch-indicator package (The prot-emacs-email.el submodule for notmuch (prot-emacs-notmuch.el)).

;;;; Display current time
(use-package time
  :ensure nil
  :hook (after-init . display-time-mode)
  :config
  (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 2022-09-21: For all those, I have implemented my own solution
  ;; that also shows the number of new items, although it depends on
  ;; notmuch: the `notmuch-indicator' package.
  (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)

  ;; I don't need the load average and the mail indicator, so let this
  ;; be simple:
  (setq display-time-string-forms
        '((propertize
           (format-time-string display-time-format now)
           'face 'display-time-date-and-time
           'help-echo (format-time-string "%a %b %e, %Y" now))
          " ")))

5.2.17. The prot-emacs-essentials.el settings for the world-clock

I communicate with people from across the globe. Knowing their local time is of paramount importance. With M-x world-clock we get a buffer with all cities and concomitant time zones specified in zoneinfo-style-world-list. The contents are displayed according to the world-clock-time-format. Note that I control the placement of these and many other buffers by configuring the display-buffer-alist (The prot-emacs-window.el module).

;;;; World clock (M-x world-clock)
(use-package time
  :ensure nil
  :commands (world-clock)
  :config
  (setq display-time-world-list t)
  (setq zoneinfo-style-world-list ; M-x shell RET timedatectl list-timezones
        '(("America/Los_Angeles" "Los Angeles")
          ("America/Vancouver" "Vancouver")
          ("Canada/Pacific" "Canada/Pacific")
          ("America/Chicago" "Chicago")
          ("Brazil/Acre" "Rio Branco")
          ("America/Toronto" "Toronto")
          ("America/New_York" "New York")
          ("Canada/Atlantic" "Canada/Atlantic")
          ("Brazil/East" "Brasília")
          ("UTC" "UTC")
          ("Europe/Lisbon" "Lisbon")
          ("Europe/Brussels" "Brussels")
          ("Europe/Athens" "Athens")
          ("Asia/Riyadh" "Riyadh")
          ("Asia/Tehran" "Tehran")
          ("Asia/Tbilisi" "Tbilisi")
          ("Asia/Yekaterinburg" "Yekaterinburg")
          ("Asia/Kolkata" "Kolkata")
          ("Asia/Singapore" "Singapore")
          ("Asia/Shanghai" "Shanghai")
          ("Asia/Seoul" "Seoul")
          ("Asia/Tokyo" "Tokyo")
          ("Asia/Vladivostok" "Vladivostok")
          ("Australia/Brisbane" "Brisbane")
          ("Australia/Sydney" "Sydney")
          ("Pacific/Auckland" "Auckland")))

  ;; All of the following variables are for Emacs 28
  (setq world-clock-list t)
  (setq world-clock-time-format "%R %z (%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))

5.2.18. The prot-emacs-essentials.el settings for manpages

Most buffers conform with rules we define in the display-buffer-alist (The prot-emacs-window.el module). However, M-x man does not do this because it has its own behaviour. At least, it is customisable. The Man-notify-method is a very old option, according to what the Help buffer is telling me (check its documentation with C-h v or M-x describe-variable), so I suspect this was never updated to conform with the newer display-buffer-alist

;;;; `man' (manpages)
(use-package man
  :ensure nil
  :commands (man)
  :config
  (setq Man-notify-method 'pushy)) ; does not obey `display-buffer-alist'

5.2.19. The prot-emacs-essentials.el settings for proced

The M-x proced command produces a listing of all running processes on the system. This is like the top program on the command-line. While inside the *Proced* buffer, type C-h m (M-x describe-mode) to learn about keys/commands you can use therein. Personally, I mostly use this interface to check if some process is doing more than it should.

;;;; `proced' (process monitor, similar to `top')
(use-package proced
  :ensure nil
  :commands (proced)
  :config
  (setq proced-auto-update-flag 'visible) ; Emacs 30 supports more the `visible' value
  (setq proced-enable-color-flag t) ; Emacs 29
  (setq proced-auto-update-interval 5)
  (setq proced-descend t)
  (setq proced-filter 'user))

5.2.20. The prot-emacs-essentials.el arrangement to run Emacs as a server

The “server” is functionally like the daemon, except it is run by the first Emacs frame we launch. With a running server, we can connect to it through a new emacsclient call. This is useful if we want to launch new frames that share resources with the existing running process. Though the emacsclient can be used to simply evaluate code outside of an Emacs frame (e.g. to load a new theme, as I do with my delight shell script as part of my dotfiles).

When we close the last frame, the server is terminated. Whereas the daemon remains active even if all Emacs frames are closed. For me, the server is easier to work with, as I know for sure when its life cycle starts and ends. Beside that, I did give the daemon an honest try. Emacs would crash whenever I would encounter an error in some Lisp evaluation. Whereas the server works just fine.

Note that I only ever server-start if there is no server running. One is enough.

;;;; Emacs server (allow emacsclient to connect to running session)
(use-package server
  :ensure nil
  :defer 1
  :config
  (setq server-client-instructions nil)
  (unless (server-running-p)
    (server-start)))

5.2.21. The prot-emacs-essentials.el section about substitute

I use substitute to efficiently replace targets in the buffer or context. The substitute package provides a set of commands that perform text replacement (i) throughout the buffer, (ii) limited to the current definition (per `narrow-to-defun`), (iii) from point to the end of the buffer, and (iv) from point to the beginning of the buffer.

These substitutions are meant to be as quick as possible and to not move the point. As such, they differ from the standard query-replace (which I still use where relevant). The provided commands prompt for substitute text and perform the substitution outright, without moving the point. Better check the video I did on the matter: https://protesilaos.com/codelog/2023-01-16-emacs-substitute-package-demo/.

;;; Substitute
;; Another package of mine... Video demo:
;; <https://protesilaos.com/codelog/2023-01-16-emacs-substitute-package-demo/>.
(use-package substitute
  :ensure t
  :defer 1
  ;; Produce a message after the substitution that reports on what
  ;; happened.  It is a single line, like "Substituted `TARGET' with
  ;; `SUBSTITUTE' N times across the buffer.
  :hook (substitute-post-replace . substitute-report-operation)
  :commands
  (substitute-target-below-point ; Forward motion like isearch (C-s)
   substitute-target-above-point ; Backward motion like isearch (C-r)
   substitute-target-in-defun    ; inside of the current definition
   substitute-target-in-buffer)  ; throughout the buffer
  :config
  ;; Set this to non-nil to highlight all occurrences of the current
  ;; target.
  (setopt substitute-highlight t)

  ;; Set this to t if you want to always treat the letter casing
  ;; literally.  Otherwise each command accepts a `C-u' prefix
  ;; argument to do this on-demand.
  (setq substitute-fixed-letter-case nil)

  ;; C-c s is occupied by `prot-scratch-buffer'.
  (define-key global-map (kbd "C-c r") #'substitute-prefix-map))

5.2.22. The prot-emacs-essentials.el section about goto-chg (go to change)

The goto-chg package, authored by David Andersson and maintained by Vasilij Schneidermann, moves the cursor to the point where the last change happened. Calling the command again cycles to the point before that and so on. Simple and super effective.

(use-package goto-chg
  :ensure t
  :bind
  (("C-(" . goto-last-change)
   ("C-)" . goto-last-change-reverse)))

5.2.23. The prot-emacs-essentials.el section about expreg (tree-sitter mark syntactically)

The expreg package by Yuan Fu (aka casouri) uses the tree-sitter framework to incrementally expand the region from the smallest to the largest syntactic unit in the given context. This is a powerful feature, though it (i) requires Emacs to be built with tree-sitter support and (ii) for the user to be running a major mode that is designed for tree-sitter (Lisp seems to work regardless).

The package offers the expreg-expand and expreg-contract commands. I believe I have never used the latter. I find it easier to just abort and start again than to have a special key for the rare scenario where I widened the selection more than I should. For that case, we can always exchange point and mark. I have explained this in my video about mark and register basics: https://protesilaos.com/codelog/2023-06-28-emacs-mark-register-basics/.

If tree-sitter functionality is not available, then the C-M-SPC binding is taken by prot-simple-mark-sexp which is similar in spirit (The prot-emacs-essentials.el section about prot-simple.el (custom basic commands)).

For me, expreg is mostly an investment into the future, as I am monitoring developments on the tree-sitter front. My setup provides a user option to not load such tree-sitter extras (The init.el user option to load treesitter extras).

Note that in the code block below I define two small commands. Custom code belong in libraries, though not in cases where it is ad-hoc like this (The custom libraries of my configuration).

;;; Mark syntactic constructs efficiently if tree-sitter is available (expreg)
(when (and (treesit-available-p) prot-emacs-treesitter-extras)
  (use-package expreg
    :ensure t
    :functions (prot/expreg-expand prot/expreg-expand-dwim)
    ;; There is also an `expreg-contract' command, though I have no use for it.
    :bind ("C-M-SPC" . prot/expreg-expand-dwim) ; overrides `mark-sexp'
    :config
    (defun prot/expreg-expand (n)
      "Expand to N syntactic units, defaulting to 1 if none is provided interactively."
      (interactive "p")
      (dotimes (_ n)
        (expreg-expand)))

    (defun prot/expreg-expand-dwim ()
      "Do-What-I-Mean `expreg-expand' to start with symbol or word.
If over a real symbol, mark that directly, else start with a
word.  Fall back to regular `expreg-expand'."
      (interactive)
      (let ((symbol (bounds-of-thing-at-point 'symbol)))
        (cond
         ((equal (bounds-of-thing-at-point 'word) symbol)
          (prot/expreg-expand 1))
         (symbol (prot/expreg-expand 2))
         (t (expreg-expand)))))))

5.2.24. The prot-emacs-essentials.el section about vundo (visualise undo steps)

The vundo package by Yuan Fu (aka “casouri”) builds on top of the standard undo infrastructure to provide a tree view of the undo steps. It is an essential complement to what is otherwise a powerful mechanism.

I personally like minimalist interfaces by default, meaning that I prefer nothing to pop up unless it is necessary. To this end, my command prot/vundo-if-repeat-undo produces a visualisation of the undo steps only after I repeat the undo command. The assumption is that if I am repeating, I am already interested in something further back in history, at which point having a representation of it is helpful. I am implementing this using the advice mechanism, so that (i) the command calls the original function if needed, and (ii) I can extend the functionality to many functions without needing to rebind any keys.

To make this feel natural, I bind keys in the vundo-mode-map that are consistent with the defaults for undo and undo-redo. This way, I can keep operating on the buffer without switching contexts. The visualisation is a nice extra (The init.el macro to bind keys (prot-emacs-keybind)).

I did contribute to the vundo package a while ago to define the vundo-after-undo-functions. This was an abnormal hook that we could employ for advanced uses, such as to display a diff with the relevant changes (per https://github.com/casouri/vundo/pull/74). It seems that the source code of the package now has the diffing functionality built-in, though the hook I introduced is no longer there. That is unfortunate, because we could use it for other things as well, such as to pulse the region.

;;; Visualise undo ring (`vundo')
(use-package vundo
  :ensure t
  :defer 1
  :bind
  ( :map vundo-mode-map
    ("C-/" . vundo-backward)
    ("C-?" . vundo-forward)
    ("u" . vundo-backward)
    ("U" . vundo-forward)
    ("g" . vundo-goto-last-saved)
    ("." . vundo-goto-last-saved)
    ("h" . vundo-backward)
    ("j" . vundo-next)
    ("k" . vundo-previous)
    ("l" . vundo-forward))
  :config
  (setq vundo-glyph-alist vundo-unicode-symbols)

  (defvar prot/vundo-undo-functions '(undo undo-only undo-redo)
    "List of undo functions to check if we need to visualise the undo ring.")

  (defvar prot/vundo-undo-command #'undo
    "Command to call if we are not going to visualise the undo ring.")

  (defun prot/vundo-if-repeat-undo (&rest args)
    "Use `vundo' if the last command is among `prot/vundo-undo-functions'.
In other words, start visualising the undo ring if we are going
to be cycling through the edits."
    (interactive)
    (if (and (member last-command prot/vundo-undo-functions)
             (not undo-in-region))
        (call-interactively 'vundo)
      (apply args)))

  (mapc
   (lambda (fn)
     (advice-add fn :around #'prot/vundo-if-repeat-undo))
   prot/vundo-undo-functions)

  (with-eval-after-load 'pulsar
    (add-hook 'vundo-post-exit-hook #'pulsar-pulse-line-green)))

5.2.25. The prot-emacs-essentials.el section about tmr (set timers)

This is a package that I wrote and for which I received substantial code contributions from Damien Cassou and Daniel Mendler. With tmr we set timers using a convenient notation. The point of entry is the tmr command (or tmr-with-description if you want to describe what the timer is about).

Set a timer by specifying one of these:

Input Meaning
5 5 Minutes from now
5m Same as abovre
1h 1 hour from now
06:35 From now until 06:35

To view the running timers in a tabulated list, invoke the command tmr-tabulated-view. From there, type C-h m (or M-x describe-mode) to learn about all the available commands and their respective key bindings.

;;; TMR May Ring (tmr is used to set timers)
;; Read the manual: <https://protesilaos.com/emacs/tmr>.
(use-package tmr
  :ensure t
  :bind
  (("C-c t t" . tmr)
   ("C-c t T" . tmr-with-description)
   ("C-c t l" . tmr-tabulated-view) ; "list timers" mnemonic
   ("C-c t c" . tmr-clone)
   ("C-c t k" . tmr-cancel)
   ("C-c t s" . tmr-reschedule)
   ("C-c t e" . tmr-edit-description)
   ("C-c t r" . tmr-remove)
   ("C-c t R" . tmr-remove-finished))
  :config
  (setq tmr-sound-file "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga"
        tmr-notification-urgency 'normal
        tmr-description-list 'tmr-description-history))

5.2.26. The prot-emacs-essentials.el section about password-store

The pass program (aka password-store) is a Unix-style password manager. Each password entry is its own encrypted file, stored in a local directory (~/.password-store, by default). Encryption is done with GPG, though I believe Age can also be used. It can generate strong passwords and allows us to quickly retrieve the password associated with a given file.

The Emacs interface makes it easy to access passowrds with M-x pass. The resulting buffer shows a tree representation of the ~/.password-store and provides an overview of the available key bindings. From here we can write a new entry, generate a password for an existing one, and so on.

My most common interaction with it is via the command password-store-copy which uses minibuffer completion to match an entry and get the password from it. On this note, I have a convention of naming password files based on their scope, like username@website. This makes it easy to retrieve what I need.

;;; Pass interface (password-store)
(use-package password-store
  :ensure t
  ;; Mnemonic is the root of the "code" word (κώδικας).  But also to add
  ;; the password to the kill-ring.  Other options are already taken.
  :bind ("C-c k" . password-store-copy)
  :config
  (setq password-store-time-before-clipboard-restore 30))

(use-package pass
 :ensure t
 :commands (pass))

5.2.27. The prot-emacs-essentials.el section about shell

Before using Emacs, I did not have a clear idea of what the distinction between a “shell” and a “terminal” is. But I quickly learnt that a terminal (“terminal emulator”) is an application that provides a text-centric interface and handles all the technicalities of presenting text accordingly. Whereas the “shell” (or “command-line shell”) is the program that runs inside the terminal whose job is to interpret the user’s input and communicate with the computer. Something like xterm or gnome-terminal is a terminal. While the likes of bash, zsh, and fish are shells.

In Emacs we can have both. Emacs can run a process that constitutes a fully fledged terminal emulator, such as with the vterm package, or it can provide the interface necessary for a mere shell to handle the command-line interactivity.

A terminal emulator is only needed if we use programs that require Terminal User Interface (TUI) capabilities, such as htop. I do not run any of those, in large part because Emacs has better or equally capable alternatives like M-x proced to do what htop does in the terminal (The prot-emacs-essentials.el settings for proced).

With the TUI out of the way, we can have M-x shell run a native Unix shell for us. Mine is bash because I am a simpleton, but also because my prot-shell.el provides a few niceties that improve the user experience (The prot-shell.el library).

I run a shell to do things like interface with my system’s package manager or run a program with some flags. M-x shell is more than enough for this purpose. To make it a bit easier to work with multiple shells that need to be named after the directory they are in, I use the command prot-shell: it not only uses a unique and informative buffer name, but it also keeps track of cd commands to update the buffer name accordingly.

Note that there also exists a shell implemented in Emacs Lisp. It is called eshell. Unlike shell, it does not read the ~/.bashrc and is its own little Emacs-only thing with its own command-line syntax. In short, it is “okay” in a vacuum but I have no use for it beside tinkering with Elisp, while I prefer to have a reliable ~/.bashrc at all times. As such, the eshell command is on the list of disabled commands (The init.el settings to enable certain commands and disable others).

My prot-shell-mode defines a few extra key bindings (per the prot-shell-mode-map) and also implements a bookmark handler for shell buffers (The prot-emacs-essentials.el settings for bookmarks). The bookmarking functionality is a wonderful extra, as it leverages Emacs’ TRAMP infrastructure to re-establish the connection to the given host. For example, if I do M-x find-file and then input /sudo::/usr/share/ to go to /usr/share/ with sudo privileges, then I can open a shell there and bookmark it. When I jump back to the bookmark, Emacs will automatically handle the sudo part while taking me to that shell in its directory. Relevant note from my source code:

;; NOTE 2023-08-18: I sent this to the Emacs maintainers as a patch
;; (bug#65039).  I received approval to proceed with the change, but I
;; did not do it because a user reported an issue with SSH (TRAMP).  I
;; do not have access to SSH and am not familiar with such workflows.
;; If/when that changes, I will try again.  In the meantime, this is
;; good code and it works for me.

Now the actual configurations:

;;; Shell (M-x shell)
(use-package shell
  :ensure nil
  :bind
  ( :map shell-mode-map
    ("C-c C-k" . comint-clear-buffer)
    ("C-c C-w" . comint-write-output))
  :config
  ;; Check my .bashrc which handles `comint-terminfo-terminal':
  ;;
  ;; # Default pager.  The check for the terminal is useful for Emacs with
  ;; # M-x shell (which is how I usually interact with bash these days).
  ;; #
  ;; # The COLORTERM is documented in (info "(emacs) General Variables").
  ;; # I found the reference to `dumb-emacs-ansi' in (info "(emacs)
  ;; # Connection Variables").
  ;; if [ "$TERM" = "dumb" ] && [ "$INSIDE_EMACS" ] || [ "$TERM" = "dumb-emacs-ansi" ] && [ "$INSIDE_EMACS" ]
  ;; then
  ;;     export PAGER="cat"
  ;;     alias less="cat"
  ;;     export TERM=dumb-emacs-ansi
  ;;     export COLORTERM=1
  ;; else
  ;;     # Quit once you try to scroll past the end of the file.
  ;;     export PAGER="less --quit-at-eof"
  ;; fi

  (setq shell-command-prompt-show-cwd t) ; Emacs 27.1
  (setq ansi-color-for-comint-mode t)
  (setq shell-input-autoexpand 'input)
  (setq shell-highlight-undef-enable t) ; Emacs 29.1
  (setq shell-has-auto-cd nil) ; Emacs 29.1
  (setq shell-get-old-input-include-continuation-lines t) ; Emacs 30.1
  (setq shell-kill-buffer-on-exit t) ; Emacs 29.1
  (setq shell-completion-fignore '("~" "#" "%"))
  (setq-default comint-scroll-to-bottom-on-input t)
  (setq-default comint-scroll-to-bottom-on-output nil)
  (setq-default comint-input-autoexpand 'input)
  (setq comint-prompt-read-only t)
  (setq comint-buffer-maximum-size 9999)
  (setq comint-completion-autolist t)
  (setq comint-input-ignoredups t)
  (setq tramp-default-remote-shell "/bin/bash")

  (setq shell-font-lock-keywords
        '(("[ \t]\\([+-][^ \t\n]+\\)" 1 font-lock-builtin-face)
          ("^[^ \t\n]+:.*" . font-lock-string-face)
          ("^\\[[1-9][0-9]*\\]" . font-lock-constant-face)))

  ;; Support for OS-specific escape sequences such as what `ls
  ;; --hyperlink' uses.  I normally don't use those, but I am checking
  ;; this to see if there are any obvious advantages/disadvantages.
  (add-hook 'comint-output-filter-functions 'comint-osc-process-output))

(use-package prot-shell
  :ensure nil
  :bind (("<f1>" . prot-shell)) ; I don't use F1 for help commands
  :hook (shell-mode . prot-shell-mode))

5.2.28. The prot-emacs-essentials.el section about the laptop-specific settings

Due to my limited electricity setup, I spend about half my computer time on the laptop (What hardware and software do you use?). It does not add to the load that my solar-powered batteries have to handle and I can thus keep the lights on until ~21:00.

When I am working on my laptop, I want to make a few tweaks to the default dimensions of Emacs frames (The early-init.el code to set frame parameters). I also arrange for the battery indicator to be displayed on the mode line.

;;; Laptop settings
(unless (directory-empty-p "/sys/class/power_supply/")
  (add-to-list 'default-frame-alist '(width . (text-pixels . 800)))
  (add-to-list 'default-frame-alist '(height . (text-pixels . 600)))

  (use-package battery
    :ensure nil
    :hook (after-init . display-battery-mode)
    :config
;;;; Show battery status on the mode line (battery.el)
    (setq battery-mode-line-format
          (cond
           ((eq battery-status-function #'battery-linux-proc-acpi)
	        "⏻%b%p%%,%d°C ")
	       (battery-status-function
	        "⏻%b%p%% ")))))

5.2.29. The prot-emacs-essentials.el call to provide

Finally, we provide the module. This is the mirror function of require (The init.el final part to load the individual modules).

(provide 'prot-emacs-essentials)

5.3. The prot-emacs-modeline.el module

[ Watch: write a custom mode line (2023-07-29) and customise mode line colours (2024-01-13). ]

I use a custom mode line that is close in spirit to the default one. The main difference is that I design most of the components to only appear in the focused window. This way, I get a minimal view while also avoid the repetition of global indicators, such as for the time (The prot-emacs-essentials.el configurations for the date and time (display-time-mode)) or the notmuch-indicator (The prot-emacs-notmuch.el section about the notmuch-indicator).

Most items that go into the mode-line-format are defined in the prot-modeline.el library.

Notice the use of setq-default. This is like setq but sets the default value of variable that normally are buffer-local. You will only find a few cases where this is needed.

In this section I also take care to provide integration with my spacious-padding package (The prot-emacs-theme.el section for spacious-padding). It adds, among others, a box effect to mode line constructs. To make the faces of prot-modeline.el look right in this scenario, I add a box to them as well. They then adopt whatever padding is there. The with-eval-after-load pattern is how to evaluate some code as soon as the given package/feature is loaded.

;;; Mode line
(use-package prot-modeline
  :ensure nil
  :config
  (setq mode-line-compact nil) ; Emacs 28
  (setq mode-line-right-align-edge 'right-margin) ; Emacs 30
  (setq-default mode-line-format
                '("%e"
                  prot-modeline-kbd-macro
                  prot-modeline-narrow
                  prot-modeline-buffer-status
                  prot-modeline-window-dedicated-status
                  prot-modeline-input-method
                  "  "
                  prot-modeline-buffer-identification
                  "  "
                  prot-modeline-major-mode
                  prot-modeline-process
                  "  "
                  prot-modeline-vc-branch
                  "  "
                  prot-modeline-eglot
                  "  "
                  prot-modeline-flymake
                  "  "
                  mode-line-format-right-align ; Emacs 30
                  prot-modeline-notmuch-indicator
                  "  "
                  prot-modeline-misc-info))

  (with-eval-after-load 'spacious-padding
    (defun prot/modeline-spacious-indicators ()
      "Set box attribute to `'prot-modeline-indicator-button' if spacious-padding is enabled."
      (if (bound-and-true-p spacious-padding-mode)
          (set-face-attribute 'prot-modeline-indicator-button nil :box t)
        (set-face-attribute 'prot-modeline-indicator-button nil :box 'unspecified)))

    ;; Run it at startup and then afterwards whenever
    ;; `spacious-padding-mode' is toggled on/off.
    (prot/modeline-spacious-indicators)

    (add-hook 'spacious-padding-mode-hook #'prot/modeline-spacious-indicators)))

5.3.1. The prot-emacs-modeline.el section about keycast

This is a helpful package by Jonas Bernoulli that echoes the key presses and corresponding commands on the mode line, tab bar, header line, or a special buffer.

I usually enable keycast-mode-line-mode when I do a presentation. It shows an indicator on the focused mode line.

Note that the value of keycast-mode-line-insert-after only works with my customised mode line (The prot-emacs-modeline.el module).

;;; Keycast mode
(use-package keycast
  :ensure t
  :after prot-modeline
  :commands (keycast-mode-line-mode keycast-header-line-mode keycast-tab-bar-mode keycast-log-mode)
  :init
  (setq keycast-mode-line-format "%2s%k%c%R")
  (setq keycast-mode-line-insert-after 'prot-modeline-vc-branch)
  (setq keycast-mode-line-window-predicate 'mode-line-window-selected-p)
  (setq keycast-mode-line-remove-tail-elements nil)
  :config
  (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 handle-select-window
                    mouse-set-point mouse-drag-region))
    (add-to-list 'keycast-substitute-alist `(,event nil))))

5.3.2. The prot-emacs-essentials.el call to provide

Finally, we provide the module. This is the mirror function of require (The init.el final part to load the individual modules).

(provide 'prot-emacs-modeline)

5.4. The prot-emacs-completion.el module

The term “completion” describes a process where user input is assisted by pattern matching algorithms to type out incomplete terms. The most basic way of this model of interaction is what we get in a command-line prompt, where we can hit TAB to expand the word before point to something the program already knows about (e.g ema followed by TAB may complete to emacs).

In Emacs, completion encompasses user interfaces that show the available candidates (the likely options) right away, as well as provide more advanced capabilities for storing the history of previous inputs, displaying helpful annotations next to each candidate, and “completion styles” to control how user input is matched to candidates. Because we use the minibuffer for most common interactions, completion is an integral part of any setup.

5.4.1. The prot-emacs-completion.el settings for completion styles

The completion-styles are pattern matching algorithms. They interpret user input and match candidates accordingly.

emacs22
Prefix completion that only operates on the text before point. If we are in prefix|suffix, with | representing the cursor, it will consider everything that expands prefix and then add back to it the suffix.
basic
Prefix completion that also accounts for the text after point. Using the above example, this one will consider patterns that match all of emacs22 as well as anything that completes suffix.
partial-completion
This is used for file navigation. Instead of typing out a full path like ~/.local/share/fonts, we do ~/.l/s/f or variants thereof to make the matches unique such as ~/.l/sh/fon. It is a joy to navigate the file system in this way.
substring
Matches the given sequence of characters literally regardless of where it is in a word. So pro will match professional as well as reproduce.
flex
Completion of an in-order subset of characters. It does not matter where the charactes are in the word, so long as they are encountered in the given order. The input lad will thus match list-faces-display as well as pulsar-highlight-dwim.
initials
Completion of acronyms and initialisms. Typing lfd will thus match list-faces-display. This completion style can also be used for file system navigation, though I prefer to only have partial-completion handle that task.
orderless
This is the only completion style I use which is not built into Emacs and which I tweak further in a separate section (The prot-emacs-completion.el for the orderless completion style). It matches patterns out-of-order. Patterns are typically words separated by spaces, though they can also be regular expressions, and even styles that are the same as the aforementioned flex and initials.

Now that you know about the completion styles I use, take a look at the value of my completion-styles. You will notice that orderless, which is the most powerful/flexible is placed last. I do this because Emacs tries the styles in the given order from left to right, moving the next one until it finds a match. As such, I usually want to start with tight matches (e.g. li-fa-di for list-faces-display) and only widen the scope of the search as I need to. This is easy to do because none of the built-in completion styles parses the empty space, so as soon as I type a space after some characters I am using orderless.

Notice that this is not all, as we still have to consider what happens when the minibuffer prompt we are using defines a specific completion category whose pattern matching styles differ from what we have in the completion-styles. To that end, we also set up a more fine-grained set of completion styles on a per-category basis using overrides.

The completion-styles is the fallback option in case there is no provision for the given completion category. The completion category is a piece of metadata that is associated with the completion table we are matching against while using the minibuffer. For example, the find-file command has the file category, while the switch-to-buffer command uses the buffer category. The defaults for those are specified in the variable completion-category-defaults, while overrides for them can be set in the completion-category-overrides.

While we can override only the categories we care about, the presence of those completion-category-defaults will surprise us in some cases because we will not be using what we specified in the completion-styles. As such, I set completion-category-defaults to nil, to always fall back to my preferred completion-styles and then I further configure overrides where those make sense to me.

We can opt for per-category styles by configuring the user option completion-category-overrides. Notice, for example, how I arrange for partial-completion to be set only for the file completion category, as I only ever need it there. Also bear in mind what I described above about why orderless is placed last on the list: Emacs uses the completion styles from left to right until something matches the given input. So I do not need to have partial-completion first as basic will never match something like ~/.l/s/fo for ~/.local/share/fonts.

;;; General minibuffer settings
(use-package minibuffer
  :ensure nil
  :config
;;;; Completion styles
  (setq completion-styles '(basic substring initials flex orderless)) ; also see `completion-category-overrides'
  (setq completion-pcm-leading-wildcard t) ; Emacs 31: make `partial-completion' behave like `substring'

  ;; Reset all the per-category defaults so that (i) we use the
  ;; standard `completion-styles' and (ii) can specify our own styles
  ;; in the `completion-category-overrides' without having to
  ;; explicitly override everything.
  (setq completion-category-defaults nil)

  ;; A non-exhaustve list of known completion categories:
  ;;
  ;; - `bookmark'
  ;; - `buffer'
  ;; - `charset'
  ;; - `coding-system'
  ;; - `color'
  ;; - `command' (e.g. `M-x')
  ;; - `customize-group'
  ;; - `environment-variable'
  ;; - `expression'
  ;; - `face'
  ;; - `file'
  ;; - `function' (the `describe-function' command bound to `C-h f')
  ;; - `info-menu'
  ;; - `imenu'
  ;; - `input-method'
  ;; - `kill-ring'
  ;; - `library'
  ;; - `minor-mode'
  ;; - `multi-category'
  ;; - `package'
  ;; - `project-file'
  ;; - `symbol' (the `describe-symbol' command bound to `C-h o')
  ;; - `theme'
  ;; - `unicode-name' (the `insert-char' command bound to `C-x 8 RET')
  ;; - `variable' (the `describe-variable' command bound to `C-h v')
  ;; - `consult-grep'
  ;; - `consult-isearch'
  ;; - `consult-kmacro'
  ;; - `consult-location'
  ;; - `embark-keybinding'
  ;;
  (setq completion-category-overrides
        ;; 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>.
        ;;
        ;; `partial-completion' is a killer app for files, because it
        ;; can expand ~/.l/s/fo to ~/.local/share/fonts.
        ;;
        ;; If `basic' cannot match my current input, Emacs tries the
        ;; next completion style in the given order.  In other words,
        ;; `orderless' kicks in as soon as I input a space or one of its
        ;; style dispatcher characters.
        '((file (styles . (basic partial-completion orderless)))
          (bookmark (styles . (basic substring)))
          (library (styles . (basic substring)))
          (embark-keybinding (styles . (basic substring)))
          (imenu (styles . (basic substring orderless)))
          (consult-location (styles . (basic substring orderless)))
          (kill-ring (styles . (emacs22 orderless)))
          (eglot (styles . (emacs22 substring orderless))))))

5.4.2. The prot-emacs-completion.el for the orderless completion style

The orderless package by Omar Antolín Camarena provides one of the completion styles that I use (The prot-emacs-completion.el settings for completion styles). It is a powerful pattern matching algorithm that parses user input and interprets it out-of-order, so that in pa will cover insert-pair as well as package-install. Components of the search are space-separated, by default, though we can modify the user option orderless-component-separator to have something else (but I cannot think of a better value). In the section about completion styles, I explain how I use orderless and why its power does not result in lots of false positives.

With orderless we can also define so-called “style dispatchers”. These are characters attached to the input which instruct orderless to use a specific pattern for that component. My prot-orderless.el defines such style dispatchers as postfix operators: they are added to the end of the input (The prot-orderless.el library). The equals sign takes the input literally, the dot interprets it as a file type extension, while the tilde means to match the input either at the beginning or the end. Granted, these are overkill most of the time. It is easier to just continue typing to narrow the list of candidates. Still, they do have their uses.

;;; Orderless completion style (and prot-orderless.el)
(use-package orderless
  :ensure t
  :demand t
  :after minibuffer
  :config
  ;; Remember to check my `completion-styles' and the
  ;; `completion-category-overrides'.
  (setq orderless-matching-styles '(orderless-prefixes orderless-regexp))

  ;; SPC should never complete: use it for `orderless' groups.
  ;; The `?' is a regexp construct.
  :bind ( :map minibuffer-local-completion-map
          ("SPC" . nil)
          ("?" . nil)))

(use-package prot-orderless
  :ensure nil
  :config
  (setq orderless-style-dispatchers
        '(prot-orderless-literal
          prot-orderless-file-ext
          prot-orderless-beg-or-end)))

5.4.3. The prot-emacs-completion.el settings to ignore letter casing

I never really need to match letters case-sensitively in the minibuffer. Let’s have everything ignore casing by default.

[ In some Elisp that I write there is a let binding for case-fold-search to make the search case-sensitive. But those are the exceptions. ]

(setq completion-ignore-case t)
(setq read-buffer-completion-ignore-case t)
(setq-default case-fold-search t)   ; For general regexp
(setq read-file-name-completion-ignore-case t)

5.4.4. The prot-emacs-completion.el settings for recursive minibuffers

“Recursive minibuffers” are of those advanced features that you don’t need frequently, but when you do, it is an excellent addition to your workflow. The concept describes the use of a minibuffer while another minibuffer is already open.

The need to have multiple (i.e. “recursive”) minibuffers arises when you initiate a command, such as M-x followed by some incomplete command where you remember that you forgot to perform another command before confirming the first one. I mostly use this as a combination of M-x (execute-extended-command) and M-: (eval-expression).

The read-minibuffer-restore-windows restores the window layout that was in place when the minibuffer recursion started. I personally do not want that: just leave me where I am.

The minibuffer-depth-indicate-mode shows a number next to the minibuffer prompt, indicating the level of depth in the recursion, starting with 2.

(use-package mb-depth
  :ensure nil
  :hook (after-init . minibuffer-depth-indicate-mode)
  :config
  (setq read-minibuffer-restore-windows nil) ; Emacs 28
  (setq enable-recursive-minibuffers t))

5.4.5. The prot-emacs-completion.el settings for default values

Minibuffer prompts often have a default value. This is used when the user types RET without inputting anything. The out-of-the-box behaviour of Emacs is to append informative text to the prompt like (default some-default-value). With the tweak to minibuffer-default-prompt-format we get a more compact style of [some-default-value], which looks better to me.

The minibuffer-electric-default-mode displays the default value next to the prompt only if RET will actually use the default in that situation. This means that while you start typing in the minibuffer, the [some-default-value] indicator disappears, since it is no longer applicable. Without this mode, the indicator stays there at all times, which can be confusing or distracting.

(use-package minibuf-eldef
  :ensure nil
  :hook (after-init . minibuffer-electric-default-mode)
  :config
  (setq minibuffer-default-prompt-format " [%s]")) ; Emacs 29

5.4.6. The prot-emacs-completion.el settings for common interactions

Here I combine several small tweaks to improve the overall minibuffer experience.

  • The need to resize-mini-windows arises on some occasions where Emacs has to show text spanning multiple lines in the “mini windows”. A common scenario for me is in Org mode buffers where I set the TODO keyword of a task with C-c C-t (M-x org-todo) and have this as my setting: (setq org-use-fast-todo-selection 'expert) Otherwise, this is not an issue anyway and I may also like other options for org-use-fast-todo-selection.
  • The read-answer-short is complementary to use-short-answers. This is about providing the shorter version to some confirmation prompt, such as y instead of yes.
  • The echo-keystrokes is set to a low value to show in the echo area the incomplete key sequence I have just typed. This is especially helpful for demonstration purposes but also to double check that I did not mistype something (I cannot touch-type, so this happens a lot).
  • The minibuffer-prompt-properties and advice to completing-read-multiple make it so that (i) the minibuffer prompt is not accessible with regular motions to avoid mistakes and (ii) prompts that complete multiple targets show an indicator about this fact. With regard to the latter in particular, we have prompts like that of Org to set tags for a heading (with C-c C-q else M-x org-set-tags-command) where more than one candidate can be provided using completion, provided each candidate is separated by the crm-separator (a comma by default, though Org uses : in that scenario).

    Remember that when using completion in the minibuffer, you can hit TAB to expand the selected choice without exiting with it. For cases when multiple candidates can be selected, you select the candidate, TAB, then input the crm-separator, and repeat until you are done selecting at which point you type RET.

  • Finally the file-name-shadow-mode is a neat little feature to remove the “shadowed” part of a file prompt while using something like C-x C-f (M-x find-file). File name shadowing happens when we invoke find-file and instead of first deleting the contents of the minibuffer, we start typing out the file system path we wish to visit. For example, I am in ~/Git/Projects/ and type directly after it something like ~/.local/share/fonts/, so Emacs displays ~/Git/Projects/~/.local/share/fonts/ with the original part greyed out. With file-name-shadow-mode the “shadowed” part is removed altogether. This is especially nice when combined with the completion style called partial-completion (The prot-emacs-completion.el settings for completion styles).
(use-package rfn-eshadow
  :ensure nil
  :hook (minibuffer-setup . cursor-intangible-mode)
  :config
  ;; Not everything here comes from rfn-eshadow.el, but this is fine.

  (setq resize-mini-windows t)
  (setq read-answer-short t) ; also check `use-short-answers' for Emacs28
  (setq echo-keystrokes 0.25)
  (setq kill-ring-max 60) ; Keep it small

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

  ;; MCT has a variant of this built-in.
  (unless (eq prot-emacs-completion-ui 'mct)
    ;; Add prompt indicator to `completing-read-multiple'.  We display
    ;; [`completing-read-multiple': <separator>], e.g.,
    ;; [`completing-read-multiple': ,] if the separator is a comma.  This
    ;; is adapted from the README of the `vertico' package by Daniel
    ;; Mendler.  I made some small tweaks to propertize the segments of
    ;; the prompt.
    (defun crm-indicator (args)
      (cons (format "[`completing-read-multiple': %s]  %s"
                    (propertize
                     (replace-regexp-in-string
                      "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" ""
                      crm-separator)
                     'face 'error)
                    (car args))
            (cdr args)))

    (advice-add #'completing-read-multiple :filter-args #'crm-indicator))

  (file-name-shadow-mode 1))

5.4.7. The prot-emacs-completion.el generic minibuffer UI settings

These are some settings for the default completion user interface. These do not come into effect unless prot-emacs-completion-ui is set to a nil value or when we are not using any package for in-buffer completion (such as the corfu package).

(use-package minibuffer
  :ensure nil
  :demand t
  :config
  (setq completions-format 'one-column)
  (setq completion-show-help nil)
  (setq completion-auto-help 'always)
  (setq completion-auto-select nil)
  (setq completions-detailed t)
  (setq completion-show-inline-help nil)
  (setq completions-max-height 6)
  (setq completions-header-format (propertize "%s candidates:\n" 'face 'bold-italic))
  (setq completions-highlight-face 'completions-highlight)
  (setq minibuffer-completion-auto-choose t)
  (setq minibuffer-visible-completions t) ; Emacs 30
  (setq completions-sort 'historical)

  (unless prot-emacs-completion-ui
    (prot-emacs-keybind minibuffer-local-completion-map
      "<up>" #'minibuffer-previous-line-completion
      "<down>" #'minibuffer-next-line-completion)

    (add-hook 'completion-list-mode-hook #'prot-common-truncate-lines-silently)))

5.4.8. The prot-emacs-completion.el settings for saving the history (savehist-mode)

Minibuffer prompts can have their own history. When they do not, they share a common history of user inputs. Emacs keeps track of that history in the current session, but loses it as soon as we close it. With savehist-mode enabled, all minibuffer histories are written to a file and are restored when we start Emacs again.

Histories are useful in two ways:

  1. Recent choices appear at the top, so we can find them more easily.
  2. The M-p (previous-history-element) and M-n (next-history-element) commands in the minibuffer will be useful right away upon restoring Emacs (all my packages make good use of minibuffer histories per prompt, so M-p and M-n only show relevant values).

Since we are already recording minibuffer histories, we can instruct savehist-mode to also keep track of additional variables and restore them next time we use Emacs. Hence savehist-additional-variables. I do this in a few of places:

Note that the user option history-length applies to each individual history variable: it is not about all histories combined.

Overall, I am happy with this feature and benefit from it on a daily basis.

;;;; `savehist' (minibuffer and related histories)
(use-package savehist
  :ensure nil
  :hook (after-init . savehist-mode)
  :config
  (setq savehist-file (locate-user-emacs-file "savehist"))
  (setq history-length 100)
  (setq history-delete-duplicates t)
  (setq savehist-save-minibuffer-history t)
  (add-to-list 'savehist-additional-variables 'kill-ring))

5.4.9. The prot-emacs-completion.el settings for dynamic text expansion (dabbrev)

The built-in dabbrev package provides a text completion method that reads the contents of a buffer and expands the text before the cursor to match possible candidates. This is done with M-/ (dabbrev-expand) which is what I use most of the time to perform in-buffer completions.

I like dabbrev because it is minimal. It does not produce any popup or affect the window layout and so it is keeping me focused on what I am doing. I wish it had a behaviour where we could initiate it and at any point demand a fully fledged minibuffer presentation of what it is trying to match, instead of cycling through the candidates with repeated M-/. Granted, I normally do not cycle in that way, as I typically type out enough to get an exact match or be one M-/ away from it.

Apart from the dabbrev-expand command, we have dabbrev-completion. I do not use it because it does not feel natural while typing to stop, check the minibuffer for some text, select it, and go back to typing. Perhaps this is because I have a style of writing without interruptions and without going back to immediately edit what I wrote (unless I am doing a demonstration, where the viewer needs to follow along).

The term “dabbrev” stands for “dynamic abbreviation”. Emacs also has static, user-defined abbreviations (The prot-emacs-completion.el settings for static text expansion (abbrev)).

(use-package dabbrev
  :ensure nil
  :commands (dabbrev-expand dabbrev-completion)
  :config
;;;; `dabbrev' (dynamic word completion (dynamic abbreviations))
  (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)
  (setq dabbrev-ignored-buffer-modes
        '(archive-mode image-mode docview-mode pdf-view-mode)))

5.4.10. The prot-emacs-completion.el settings for static text expansion (abbrev)

[ Watch: abbreviations with abbrev-mode (quick text expansion) (2024-02-03). ]

Unlike “dynamic abbreviations” that depend on the text already available in a buffer, we can define abbreviations that always expand to what we have specified (The prot-emacs-completion.el settings for dynamic text expansion (dabbrev)).

Abbreviations, else the abbrev mechanism, are strings of characters that when typed out are replaced by another string. For example, if I want to type in my website’s URL, I insert meweb and continue typing. Emacs will expand that word into https://protesilaos.com.

Unless you are documenting what your abbreviation does or have abbrevs that are easy to mistype, you will never need to tell Emacs not to expand the given input. Note that the command unexpand-abbrev is there if you need it. I had to use it in the above paragraph to first type out the abbrev I used as an example.

Similarly, we can expand an abbrev anywhere with the command expand-abbrev, though we normally do not have to do this because it happens automatically as we type.

Emacs is smart about how we define and use abbrevs. Each major mode has its own abbrev table, to which we add our definitions. When we are in a buffer that has that major mode, we gain access to the relevant abbreviations. A global-abbrev-table is also available. In case of a conflict between a major-mode-specific table and the global one, the former takes precedence. Furthermore, abbrev tables conform with the same inheritence principle as major modes at-large, meaning that something like Org mode will inherit the text-mode-abbrev-table because org-mode is derived from text-mode. Abbrev tables are thus consistent with how hooks and keymaps work in terms of precedence and inheritence.

As you will notice below, I use the prot-emacs-abbrev macro to make it easier to define these (The init.el macro to define abbreviations (prot-emacs-abbrev)). Most of my abbrevs are simple letter casing tweaks. Though keep in mind that the underlying mechanism is powerful and can evaluate arbitrary Elisp code (I played around with it, though I don’t use templates to have a real need for it—but it works).

;;;; `abbrev' (Abbreviations, else Abbrevs)
(use-package abbrev
  :ensure nil
  ;; message-mode derives from text-mode, so we don't need a separate
  ;; hook for it.
  :hook ((text-mode prog-mode git-commit-mode) . abbrev-mode)
  :config
  (setq only-global-abbrevs nil)

  (prot-emacs-abbrev global-abbrev-table
    "meweb"   "https://protesilaos.com"
    "megit"   "https://github.com/protesilaos"
    "mehub"   "https://github.com/protesilaos"
    "meclone" "git@github.com/protesilaos/"
    "melab"   "https://gitlab.com/protesilaos"
    "medrive" "hyper://5cr7mxac8o8aymun698736tayrh1h4kbqf359cfk57swjke716gy/"
    ";web"   "https://protesilaos.com"
    ";git"   "https://github.com/protesilaos"
    ";hub"   "https://github.com/protesilaos"
    ";clone" "git@github.com/protesilaos/"
    ";lab"   "https://gitlab.com/protesilaos"
    ";drive" "hyper://5cr7mxac8o8aymun698736tayrh1h4kbqf359cfk57swjke716gy/")

  (prot-emacs-abbrev text-mode-abbrev-table
    "asciidoc"       "AsciiDoc"
    "auctex"         "AUCTeX"
    "cafe"           "café"
    "cliche"         "cliché"
    "clojurescript"  "ClojureScript"
    "emacsconf"      "EmacsConf"
    "github"         "GitHub"
    "gitlab"         "GitLab"
    "javascript"     "JavaScript"
    "latex"          "LaTeX"
    "libreplanet"    "LibrePlanet"
    "linkedin"       "LinkedIn"
    "paypal"         "PayPal"
    "sourcehut"      "SourceHut"
    "texmacs"        "TeXmacs"
    "typescript"     "TypeScript"
    "visavis"        "vis-à-vis"
    "deja"           "déjà"
    "youtube"        "YouTube"
    ";up"            "🙃"
    ";uni"           "🦄"
    ";laugh"         "🤣"
    ";smile"         "😀"
    ";sun"           "☀️")

  ;; Allow abbrevs with a prefix colon, semicolon, or underscore.  I demonstrated
  ;; this here: <https://protesilaos.com/codelog/2024-02-03-emacs-abbrev-mode/>.
  (abbrev-table-put global-abbrev-table :regexp "\\(?:^\\|[\t\s]+\\)\\(?1:[:;_].*\\|.*\\)")

  (with-eval-after-load 'text-mode
    (abbrev-table-put text-mode-abbrev-table :regexp "\\(?:^\\|[\t\s]+\\)\\(?1:[:;_].*\\|.*\\)"))

  (with-eval-after-load 'org
    (prot-emacs-abbrev org-mode-abbrev-table
      ";dev" "}")
    (abbrev-table-put org-mode-abbrev-table :regexp "\\(?:^\\|[\t\s]+\\)\\(?1:[:;_].*\\|.*\\)"))

  (with-eval-after-load 'message
    (prot-emacs-abbrev message-mode-abbrev-table
      "bestregards"  "Best regards,\nProtesilaos (or simply \"Prot\")"
      "allthebest"   "All the best,\nProtesilaos (or simply \"Prot\")"
      "niceday"      "Have a nice day,\nProtesilaos (or simply \"Prot\")"
      "abest"        "All the best,\nProt"
      "bregards"     "Best regards,\nProt"
      "nday"         "Have a nice day,\nProt"
      "nosrht"       "P.S. I am phasing out SourceHut: <https://protesilaos.com/codelog/2024-01-27-sourcehut-no-more/>.
Development continues on GitHub with GitLab as a mirror."))

  ;; The `prot-emacs-abbrev' macro, which simplifies how we use
  ;; `define-abbrev', does not only expand a static text.  It can take
  ;; a pair of string and function to trigger the latter when the
  ;; former is inserted.  Think of it like the basis of a simplistic
  ;; templating system.
  (require 'prot-abbrev)
  (prot-emacs-abbrev global-abbrev-table
    "metime" #'prot-abbrev-current-time
    "medate" #'prot-abbrev-current-date
    "mejitsi" #'prot-abbrev-jitsi-link
    ";time" #'prot-abbrev-current-time
    ";date" #'prot-abbrev-current-date
    ";jitsi" #'prot-abbrev-jitsi-link)

  (prot-emacs-abbrev text-mode-abbrev-table
    ";update" #'prot-abbrev-update-html)

  ;; Because the *scratch* buffer is produced before we load this, we
  ;; have to explicitly activate the mode there.
  (when-let* ((scratch (get-buffer "*scratch*")))
    (with-current-buffer scratch
      (abbrev-mode 1)))

  ;; By default, abbrev asks for confirmation on whether to use
  ;; `abbrev-file-name' to save abbrevations.  I do not need that, nor
  ;; do I want it.
  (remove-hook 'save-some-buffers-functions #'abbrev--possibly-save))

5.4.11. The prot-emacs-completion.el for in-buffer completion popup (corfu)

I generally do not rely on in-buffer text completion. I feel it slows me down and distracts me. When I do, however, need to rely on it, I have the corfu package by Daniel Mendler: it handles the task splendidly as it works with Emacs’ underlying infrastructure for completion-at-point-functions.

Completion is triggered with the TAB key, which produces a popup where the cursor is. The companion corfu-popupinfo-mode will show a secondary documentation popup if we move over a candidate but do not do anything with it.

Also see the prot-emacs-completion.el settings for dynamic text expansion (dabbrev).

;;; Corfu (in-buffer completion popup)
(use-package corfu
  :ensure t
  :hook (after-init . global-corfu-mode)
  ;; I also have (setq tab-always-indent 'complete) for TAB to complete
  ;; when it does not need to perform an indentation change.
  :bind (:map corfu-map ("<tab>" . corfu-complete))
  :config
  (setq corfu-preview-current nil)
  (setq corfu-min-width 20)

  (setq corfu-popupinfo-delay '(1.25 . 0.5))
  (corfu-popupinfo-mode 1) ; shows documentation after `corfu-popupinfo-delay'

  ;; Sort by input history (no need to modify `corfu-sort-function').
  (with-eval-after-load 'savehist
    (corfu-history-mode 1)
    (add-to-list 'savehist-additional-variables 'corfu-history)))

5.4.12. The prot-emacs-completion.el settings for consult

[ This feature is subject to The init.el user option to load extras for minibuffer completion. ]

consult is another wonderful package by Daniel Mendler. It provides a number of commands that turbocharge the minibuffer with advanced capabilities for filtering, asynchronous input, and previewing of the current candidate’s context.

  • A case where filtering is in use is the consult-buffer command, which many users have as a drop-in replacement to the generic C-x b (M-x switch-to-buffer). It is a one-stop-shop for buffers, recently visited files (if recentf-mode is used—I don’t), bookmarks (The prot-emacs-essentials.el settings for bookmarks), and, in principle, anything else that defines a source for this interface. To filter those source, we can type at the empty minibuffer b SPC, which will insert a filter specific to buffers. Delete back to remove the [Buffer] filter and insert another filter. Available filters are displayed by typing ? at the prompt (I define it this way to call the command consult-narrow-help). Every multi-source command from consult relies on this paradigm.
  • Asynchronous input pertains to the intersection between Emacs and external search programs. A case in point is consult-grep, which calls the system’s grep program. The prompt distinguishes between what is sent to the external program and what is only shown to Emacs by wrapping the former inside of #. So the input #prot-#completion will send prot- to the grep program and then use completion inside of the minibuffer to perform the subsequent pattern-matching (e.g. with help from orderless (The prot-emacs-completion.el for the orderless completion style). The part that is sent to the external program does not block Emacs. It is handled asynchronously, so everything stays responsive.
  • As for previewing, consult commands show the context of the current match and update the window as we move between completion candidates in the minibuffer. For example, the consult-line command performs an in-buffer search and lets us move between matches in the minibuffer while seeing in the window above what the surrounding text looks like. This is an excellent feature when we are trying to find something and do not quite remember all the search terms to narrow down to it simply by typing at the minibuffer prompt.

Also check: The prot-emacs-search.el module.

;;; Enhanced minibuffer commands (consult.el)
(when prot-emacs-completion-extras
  (use-package consult
    :ensure t
    :hook (completion-list-mode . consult-preview-at-point-mode)
    :bind
    ( :map global-map
      ("M-g M-g" . consult-goto-line)
      ("M-K" . consult-keep-lines) ; M-S-k is similar to M-S-5 (M-%)
      ("M-F" . consult-focus-lines) ; same principle
      ("M-s M-b" . consult-buffer)
      ("M-s M-f" . consult-find)
      ("M-s M-g" . consult-grep)
      ("M-s M-h" . consult-history)
      ("M-s M-i" . consult-imenu)
      ("M-s M-l" . consult-line)
      ("M-s M-m" . consult-mark)
      ("M-s M-y" . consult-yank-pop)
      ("M-s M-s" . consult-outline)
      :map consult-narrow-map
      ("?" . consult-narrow-help))
    :config
    (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 nil)
    (setq consult-find-args
          (concat "find . -not ( "
                  "-path */.git* -prune "
                  "-or -path */.cache* -prune )"))
    (setq consult-preview-key 'any)
    (setq consult-project-function nil) ; always work from the current directory (use `cd' to switch directory)

    (add-to-list 'consult-mode-histories '(vc-git-log-edit-mode . log-edit-comment-ring))

    (require 'consult-imenu) ; the `imenu' extension is in its own file

    (with-eval-after-load 'pulsar
      ;; see my `pulsar' package: <https://protesilaos.com/emacs/pulsar>
      (setq consult-after-jump-hook nil) ; reset it to avoid conflicts with my function
      (dolist (fn '(pulsar-recenter-top pulsar-reveal-entry))
        (add-hook 'consult-after-jump-hook fn)))))

5.4.13. The prot-emacs-completion.el section about embark

[ This feature is subject to The init.el user option to load extras for minibuffer completion. ]

The embark package by Omar Antolín Camarena provides a mechanism to perform relevant actions in the given context. What constitutes “the given context” depends on where the cursor is, such as if it is at the end of a symbolic expression in Lisp code or inside the minibuffer. The single point of entry is the embark-act command or variants like embark-dwim.

With embark-act we gain access to a customisable list of commands for the given context. If we are over a Lisp symbol, one possible action is to describe it (i.e. produce documentation about it). If we are browsing files in the minibuffer, possible actions include file operations such as to delete or rename the file. And so on for everything.

The embark-dwim command always performs the default action for the given context. It is like invoking embark-act and then typing the RET key.

A killer feature of embark is the concepts of “collect” and “export”. These are used in the minibuffer to produce a dedicated buffer that contains all the completion candidates. For example, if we are reading documentation about embark- and have 10 items there, we can “collect” the results in their own buffer and then navigate it as if it were the minibuffer: RET will perform the action that the actual minibuffer would have carried out (to show documentation, in this case). Similarly, the export mechanism takes the completion candidates out of the minibuffer, though it also puts them in a major mode that is appropriate for them. Files, for instance, will be placed in a Dired buffer (The prot-emacs-dired.el module).

Depending on the configurations about the “indicator”, the embark-act command will display an informative buffer with keys and their corresponding commands. We can control its placement the same way we do with all well-behaved buffers (The prot-emacs-window.el rules for displaying buffers (display-buffer-alist)).

One downside of embark is that it is hard to know what the context is. I have had this experience myself several times, where I though I was targeting the URL at point while the actions were about Org source blocks, headings, and whatnot. Embark is probably correct in such a case, though I cannot make my brain think the way it expects.

Another downside, which is also true for which-key (The prot-emacs-which-key.el module), is the sheer number of options for each context. I feel that the defaults should be more conservative, to have 3-4 actions per context to make it easier to find stuff. Those who need more, can add them. Documentation can also be provided to that end. Adding commands to such a list is not a trivial task, because the user must modify keymaps and thus understand the relevant concepts. Sure, we can all learn, but this is not your usual setq tweak.

All things considered, I do not recommend embark to new users as I know for a fact that people have trouble using it effectively. Power users can benefit from it, though you will notice in the following code block and in prot-embark.el how even power users need to put in some work (The prot-embark.el library). Whether it is worth it or not depends on one’s use-case.

Karthik Chikmagalur has an excellently written and presented essay on Fifteen ways to use Embark. If you plan on becoming an embark power user, this will help you. Quote from Karthik:

Despite what these examples suggest, I estimate that I use less than a third of what Embark provides. Even so, in allowing me to change or chain actions at any time, it lets me pilot Emacs by the seat of my pants. A second, unforeseen benefit is that it makes commands and listings that I would never use available in a frictionless way: commands like transpose-regions and apply-macro-to-region-lines, or custom dired, ibuffer and package-menu listings that are interactively inaccessible otherwise. The ability to quickly whip up such buffers makes knowing how to use dired or ibuffer pay off several fold. In composing such features seamlessly with minibuffer interaction or with text-regions, Embark acts as a lever to amplify the power of Emacs’ myriad built in commands and libraries.

I have been using Emacs since 2019 and not once did I have a use for transpose-regions or apply-macro-to-region-lines. To run a keyboard macro in a region, narrow to it, and call the kmacro with a 0 argument (run until you encounter an error or the end of the buffer). Why remember such a specialised function when the general pattern is getting the job done? And why be presented with the option of that same function every single time you embark-act on a region?

Personally, I would be content with a package that does the equivalent of “collect” and “export”. The rest is about organising keybindings and how you approach a given task (workflow as opposed to core functionality).

;;; Extended minibuffer actions and more (embark.el and prot-embark.el)
(when prot-emacs-completion-extras
  (use-package embark
    :ensure t
    :defer 1
    :config
    (setq embark-confirm-act-all nil)
    ;; The prot-embark.el has an advice to further simplify the
    ;; minimal indicator.  It shows cycling, which I never want to see
    ;; or do.
    (setq embark-mixed-indicator-both nil)
    (setq embark-mixed-indicator-delay 1.0)
    (setq embark-indicators '(embark-mixed-indicator embark-highlight-indicator))
    (setq embark-verbose-indicator-nested nil) ; I think I don't have them, but I do not want them either
    (setq embark-verbose-indicator-buffer-sections '(bindings))
    (setq embark-verbose-indicator-excluded-actions
          '(embark-cycle embark-act-all embark-collect embark-export embark-insert))

    ;; I never cycle and want to disable the damn thing.  Normally, a
    ;; nil value disables a key binding but here that value is
    ;; interpreted as the binding for `embark-act'.  So I just add
    ;; some obscure key that I do not have.  I absolutely do not want
    ;; to cycle!
    (setq embark-cycle-key "<XF86Travel>")

    ;; I do not want `embark-org' and am not sure what is loading it.
    ;; So I just unsert all the keymaps... This is the nuclear option
    ;; but here we are.
    (with-eval-after-load 'embark-org
      (defvar prot/embark-org-keymaps
        '(embark-org-table-cell-map
          embark-org-table-map
          embark-org-link-copy-map
          embark-org-link-map
          embark-org-src-block-map
          embark-org-item-map
          embark-org-plain-list-map
          embark-org-export-in-place-map)
        "List of Embark keymaps for Org.")

      ;; Reset `prot/embark-org-keymaps'.
      (seq-do
       (lambda (keymap)
         (set keymap (make-sparse-keymap)))
       prot/embark-org-keymaps)))

  ;; I define my own keymaps because I only use a few functions in a
  ;; limited number of contexts.
  (use-package prot-embark
    :ensure nil
    :after embark
    :bind
    ( :map global-map
      ("C-," . prot-embark-act-no-quit)
      ("C-." . prot-embark-act-quit)
      :map embark-collect-mode-map
      ("C-," . prot-embark-act-no-quit)
      ("C-." . prot-embark-act-quit)
      :map minibuffer-local-filename-completion-map
      ("C-," . prot-embark-act-no-quit)
      ("C-." . prot-embark-act-quit))
    :config
    (setq embark-keymap-alist
          '((buffer prot-embark-buffer-map)
            (command prot-embark-command-map)
            (expression prot-embark-expression-map)
            (file prot-embark-file-map)
            (function prot-embark-function-map)
            (identifier prot-embark-identifier-map)
            (package prot-embark-package-map)
            (region prot-embark-region-map)
            (symbol prot-embark-symbol-map)
            (url prot-embark-url-map)
            (variable prot-embark-variable-map)
            (t embark-general-map))))

  ;; Needed for correct exporting while using Embark with Consult
  ;; commands.
  (use-package embark-consult
    :ensure t
    :after (embark consult)))

5.4.14. The prot-emacs-completion.el section to configure completion annotations (marginalia)

The marginalia package, co-authored by Daniel Mendler and Omar Antolín Camarena, provides helpful annotations to the side of completion candidates. We see its effect, for example, when we call M-x: each command has a brief description next to it (taken from its doc string) as well as a key binding, if it has one.

Annotations are provided on a per-category basis. Categories are metadata associated with the completion table, which describe what the candidates are about. I cover this concept also in my section on completion styles where I configure pattern-matching algorithms accordingly (The prot-emacs-completion.el settings for completion styles).

The out-of-the-box settings of marginalia are perfectly usable. Though there are some categories that I either do not want to annotate (like file) or that I want to tweak their presentation of. You will thus notice how my prot-marginalia.el configures the user option marginalia-annotator-registry (The prot-marginalia.el library).

;;; Detailed completion annotations (marginalia.el)
(use-package marginalia
  :ensure t
  :hook (after-init . marginalia-mode)
  :config
  (setq marginalia-max-relative-age 0)) ; absolute time

;;;; Custom completion annotations
(use-package prot-marginalia
  :ensure nil
  :after marginalia
  :config
  (setq marginalia-annotator-registry
        '((bookmark prot-marginalia-bookmark)
          (buffer prot-marginalia-buffer)
          (command marginalia-annotate-command)
          (function prot-marginalia-symbol)
          (symbol prot-marginalia-symbol)
          (variable prot-marginalia-symbol)
          (face marginalia-annotate-face)
          (imenu marginalia-annotate-imenu)
          (package prot-marginalia-package)
          (unicode-name marginalia-annotate-char))))

5.4.15. The prot-emacs-completion.el setting to load a minibuffer UI submodule

In my init.el I define a user option to select a user interface for the minibuffer (The init.el option to load a minibuffer user interface). The choice is between my mct package and vertico by Daniel Mendler. I think vertico is the better choice overall and that is why I set it as the default. Whereas mct is for users who are more familiar with the default minibuffer interface and want a bit more interactivity or convenience on top.

Newer versions of Emacs keep receiving MCT-like tweaks to the default UI, which means that mct will eventually be superseded by built-in options. Until then, I keep it around in a maintenance-only state for those who need it.

Note that due to my particular needs to not display the minibuffer eagerly (I do not wish to accidentaly share private details when doing a presentation), I configure vertico to be more “private” and thus function as close to mct as possible.

;;; The minibuffer user interface (mct, vertico, or none)
(when prot-emacs-completion-ui
  (require
   (pcase prot-emacs-completion-ui
     ('mct 'prot-emacs-mct)
     ('vertico 'prot-emacs-vertico))))

(provide 'prot-emacs-completion)
5.4.15.1. The prot-emacs-completion.el submodule for mct (prot-emacs-mct.el)

[ This is subject to a user option: The prot-emacs-completion.el setting to load a minibuffer UI submodule. ]

The mct package is my set of tweaks on top of the default minibuffer and *Completions* interface to make them work as part of a contiguous space. It all starts by providing cycling motions from the minibuffer to the *Completions* and back, as well as a few extras to perform the necessary actions. Candidates are updated live to match the user input.

With mct we can specify a passlist of commands or completion categories that eagerly display the *Completions*. Or we can have a blocklist to never display that buffer, unless explicitly requested. Overall, mct is a capable tool with a minimal aesthetic.

Newer versions of Emacs keep gaining MCT-like capabilities, so I expect the out-of-the-box experience to eventually supersede my package. Until then, I keep it around for those who like it.

Note that I normally use vertico in my setup, but configure it to behave similar to what I have here for mct, meaning that it does not pop up the minibuffer eagerly (The prot-emacs-completion.el submodule for vertico (prot-emacs-vertico.el)).

;;; Minibuffer and Completions in Tandem or Minibuffer Confines Transcended (mct)
;; Read the manual: <https://protesilaos.com/emacs/mct>.
(use-package mct
  :ensure t
  :hook (after-init . mct-mode)
  :config
  (setq mct-hide-completion-mode-line t)
  (setq mct-completing-read-multiple-indicator t)
  ;; The blocklist and passlist accept either commands/functions or
  ;; completion categories.
  (setq mct-completion-blocklist '(notmuch-mua-new-mail notmuch-mua-prompt-for-sender))
  (setq mct-completion-passlist
        '(;; Some commands
          prot-search-outline
          select-frame-by-name
          Info-goto-node
          Info-index
          Info-menu
          vc-retrieve-tag
          ;; Some completion categories
          consult-buffer
          consult-location
          embark-keybinding
          imenu
          file
          project-file
          buffer
          kill-ring
          consult-location))
  (setq mct-remove-shadowed-file-names t)
  (setq mct-completion-window-size (cons #'mct-frame-height-third 1))
  (setq mct-live-completion 'visible)
  (setq completions-sort #'mct-sort-multi-category)

  ;; Adaptation of `icomplete-fido-backward-updir'.
  (defun prot/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-minibuffer-contents)
        (insert (expand-file-name "~/"))
        (goto-char (line-end-position)))
      (save-excursion
        (goto-char (1- (point)))
        (when (search-backward "/" (minibuffer-prompt-end) t)
          (delete-region (1+ (point)) (point-max)))))
     (t (call-interactively 'backward-delete-char))))

  (define-key minibuffer-local-filename-completion-map (kbd "DEL") #'prot/mct-backward-updir))

(provide 'prot-emacs-mct)
5.4.15.2. The prot-emacs-completion.el submodule for vertico (prot-emacs-vertico.el)

[ This is subject to a user option: The prot-emacs-completion.el setting to load a minibuffer UI submodule. ]

The vertico package by Daniel Mendler displays the minibuffer in a vertical layout. Under the hood, it takes care to be responsive and to handle even massive completion tables gracefully. Whereas, say, the built-in completion user interface (and thus mct) will suffer from a noticeable performance penalty.

All we need to get a decent experience with vertico is to enable the vertico-mode. For most users this is enough. In my case though, I have to use the “multiform” mechanism of this package to make it not show up eagerly. I do this frequently to control what I am displaying while doing a presentation. As such, the overall experience I get with vertico is the same as with mct, albeit with better performance (The prot-emacs-completion.el submodule for mct (prot-emacs-mct.el)).

Beside what I am using it for, the “multiform” mechanism allows us to change the layout of vertico on a per-command or per-category basis. We can, for instance, have a horizontal presentation for some items. I have tried this for a while, but ultimately decided to go with a more predictable scheme.

The extras I provide for vertico are in the prot-vertico.el library.

;;; Vertical completion layout (vertico)
(use-package vertico
  :ensure t
  :hook (after-init . vertico-mode)
  :config
  (setq vertico-scroll-margin 0)
  (setq vertico-count 5)
  (setq vertico-resize t)
  (setq vertico-cycle t)

  (with-eval-after-load 'rfn-eshadow
    ;; This works with `file-name-shadow-mode' enabled.  When you are in
    ;; a sub-directory and use, say, `find-file' to go to your home '~/'
    ;; or root '/' directory, Vertico will clear the old path to keep
    ;; only your current input.
    (add-hook 'rfn-eshadow-update-overlay-hook #'vertico-directory-tidy)))

;;; Custom tweaks for vertico (prot-vertico.el)
(use-package prot-vertico
  :ensure nil
  :demand t
  :after vertico
  :bind
  ( :map vertico-map
    ("<left>" . backward-char)
    ("<right>" . forward-char)
    ("TAB" . prot-vertico-private-complete)
    ("DEL" . vertico-directory-delete-char)
    ("M-DEL" . vertico-directory-delete-word)
    ("M-," . vertico-quick-insert)
    ("M-." . vertico-quick-exit)
    :map vertico-multiform-map
    ("C-n" . prot-vertico-private-next)
    ("<down>" . prot-vertico-private-next)
    ("C-p" . prot-vertico-private-previous)
    ("<up>" . prot-vertico-private-previous)
    ("C-l" . vertico-multiform-vertical))
  :config
  (setq vertico-multiform-categories
        `(;; Maximal
          (embark-keybinding ,@prot-vertico-multiform-maximal)
          (multi-category ,@prot-vertico-multiform-maximal)
          (consult-location ,@prot-vertico-multiform-maximal)
          (imenu ,@prot-vertico-multiform-maximal)
          (unicode-name ,@prot-vertico-multiform-maximal)
          ;; Minimal
          (file ,@prot-vertico-multiform-minimal
                (vertico-preselect . prompt)
                (vertico-sort-function . prot-vertico-sort-directories-first))
          (t ,@prot-vertico-multiform-minimal)))

  (vertico-multiform-mode 1))

(provide 'prot-emacs-vertico)

5.5. The prot-emacs-search.el module

[ Watch: Emacs: basics of search and replace (2023-06-10). ]

Emacs provides lots of useful facilities to search the contents of buffers or files. The most common scenario is to type C-s (isearch-forward) to perform a search forward from point or C-r (isearch-backward) to do so in reverse. These commands pack a ton of functionality and they integrate nicely with related facilities, such as those of (i) permanently highlighting the thing being searched, (ii) putting all results in a buffer that is useful for navigation purposes, among others, and (iii) replacing the given matching items with another term.

Here I summarise the functionality, though do check the video I did on the basics of search and replace:

C-s (isearch-forward)
Search forward from point (incremental search); retype C-s to move forth.
C-r (isearch-backward)
Search backward from point (incremental); retype C-r to move back. While using either C-s and C-r you can move in the opposite direction with either of those keys when performing a repeat.
C-M-s (isearch-forward-regexp)
Same as C-s but matches a regular expression. The C-s and C-r motions are the same after matches are found.
C-M-r (isearch-backward-regexp)
The counterpart of the above C-M-s for starting in reverse.
C-s C-w (isearch-yank-word-or-char)
Search forward for word-at-point. Again, C-s and C-r move forth and back, respectively.
C-r C-w (isearch-yank-word-or-char)
Same as above, but backward.
M-s o (occur)
Search for the given regular expression throughout the buffer and collect the matches in an *occur* buffer. Also check what I am doing with this in my custom extensions: The prot-emacs-search.el extras provided by the prot-search.el library.
C-u 5 M-s o (occur)
Like the above, but give it N lines of context when N is the prefix numeric argument (5 in this example).
C-s SEARCH followed by M-s o (isearch-forward –> occur)
Like C-s but then put the matches in an occur buffer.
(no term)
C-s SEARCH followed by C-u 5 M-s o (isearch-forward –> occur) :: Same as above, but now with N lines of context (5 in this example).
M-% (query-replace)
Prompt for target to replace and then prompt for its replacement (see explanation)
C-M-% (query-replace-regexp)
Same as above, but for REGEXP
C-s SEARCH followed by M-% (isearch-forward –> query-replace)
Search with C-s and then perform a query-replace for the following matches.
(no term)
C-M-s SEARCH M-% (isearch-forward-regexp –> query-replace-regexp) :: As above, but regexp-aware.
C-s SEARCH C-M-% (isearch-forward –> query-replace-regexp)
Same as above.
M-s h r (highlight-regexp)
Prompt for a face (like hi-yellow) to highlight the given regular expression.
M-s h u (unhighlight-regexp)
Prompt for an already highlighted regular expression to unhighlight (do it after the above).

For starters, just learn:

  • C-s
  • C-r
  • M-s o
  • M-%

Now on to the configurations.

5.5.1. The prot-emacs-search.el on isearch lax space

As noted in the introduction to this module, the built-in Isearch mechanism is at the centre of lots of useful patterns of interaction (The prot-emacs-search.el module).

The first thing I want to do for Isearch, is make it more convenient for me to match words that occur in sequence but are not necessarily following each other. By default, we can do that with something like C-M-s (isearch-forward-regexp) followed by one.*two. Though it is inconvenient to be a regexp-aware search mode when all we want is to just type one two and have the space be interpreted as “intermediate characters” rather than a literal space. The following do exactly this for regular C-s (isearch-forward) and C-r (isearch-backward).

;;; Isearch, occur, grep, and extras (prot-search.el)
(use-package isearch
  :ensure nil
  :demand t
  :config
  (setq search-whitespace-regexp ".*?" ; one `setq' here to make it obvious they are a bundle
        isearch-lax-whitespace t
        isearch-regexp-lax-whitespace nil))

5.5.2. The prot-emacs-search.el settings for isearch highlighting

Here I am just tweaking the delay that affects when deferred highlights are applied. The current match is highlighted immediately. The rest are done after lazy-highlight-initial-delay unless they are longer in character count than lazy-highlight-no-delay-length.

(use-package isearch
  :ensure nil
  :demand t
  :config
  (setq search-highlight t)
  (setq isearch-lazy-highlight t)
  (setq lazy-highlight-initial-delay 0.5)
  (setq lazy-highlight-no-delay-length 4))

5.5.3. The prot-emacs-search.el on isearch match counter

I think the following options should be enabled by default. They produce a counter next to the isearch prompt that shows the position of the current match relative to the total count (like 5/20). As we move to the next/previous match, the counter is updated accordingly. We have the option to place this information after the search terms, though I prefer to have them as a prefix so as not to interfere with what I am typing.

(use-package isearch
  :ensure nil
  :demand t
  :config
  (setq isearch-lazy-count t)
  (setq lazy-count-prefix-format "(%s/%s) ")
  (setq lazy-count-suffix-format nil))

5.5.4. The prot-emacs-search.el tweaks to the isearch motion behaviour

With the default settings, when we are repeating an isearch in the opposite direction, Emacs does not move directly to the next/previous match. Instead, it places the cursor at the opposite end of the current match. So, if we start with C-s and search for word we now see word| where the bar represents the cursor. With C-r we now have |word on the same match we were on. I do not like this behaviour so I configure isearch-repeat-on-direction-change accordingly. Furthermore, I can always control where the cursor is left after exiting the search either by performing the given motion (e.g. M-f (forward-word)) or by using my custom command to exit on the opposite end with C-RET while in an isearch (prot-search-isearch-other-end).

[ See: The prot-emacs-search.el extras provided by the prot-search.el library. ]

If you are using keyboard macros that rely on isearch, DO NOT set isearch-wrap-pause to the no-ding value. That disables the error isearch produces when it reaches the end of the matches. This error exits the keyboard macro, which is exactly what you want if you are calling it with a 0 numeric argument (to run from point until the end of the buffer).

(use-package isearch
  :ensure nil
  :demand t
  :config
  (setq isearch-wrap-pause t) ; `no-ding' makes keyboard macros never quit
  (setq isearch-repeat-on-direction-change t))

5.5.5. The prot-emacs-search.el tweaks for the occur buffer

Here I am making some minor tweaks to *occur* buffer (remember to read the introduction to this section (The prot-emacs-search.el module)). I always want (i) the cursor to be at the top of the buffer, (ii) the current line to be highlighted, as it is easier for selection purposes, and (iii) for long lines to be truncated, meaning to stretch beyond the visible portion of the window without wrapping below, and for this to be done silently without messaging me about it.

The latter depends on my prot-common.el library, which is loaded early at startup (The prot-emacs-essentials.el block that loads my custom libraries).

(use-package isearch
  :ensure nil
  :demand t
  :config
  (setq list-matching-lines-jump-to-current-line nil) ; do not jump to current line in `*occur*' buffers
  (add-hook 'occur-mode-hook #'prot-common-truncate-lines-silently) ; from `prot-common.el'
  (add-hook 'occur-mode-hook #'hl-line-mode))

5.5.6. The prot-emacs-search.el modified isearch and occur key bindings

These are some minor tweaks to the key maps for isearch and occur. I don’t feel strongly about the rest, but the change to C-g is important for me as I want to exit the search altogether, not resume the search of the previous succesful match.

(use-package isearch
  :ensure nil
  :demand t
  :bind
  ( :map minibuffer-local-isearch-map
    ("M-/" . isearch-complete-edit)
    :map occur-mode-map
    ("t" . toggle-truncate-lines)
    :map isearch-mode-map
    ("C-g" . isearch-cancel) ; instead of `isearch-abort'
    ("M-/" . isearch-complete)))

5.5.7. The prot-emacs-search.el extras provided by the prot-search.el library

My prot-search.el provides lots of useful extras that I use on a regular basis (The prot-search.el library).

For isearch, I have:

  • Move to next/previous match in isearch with the down/up arrow keys (C-s or C-r still work though).
  • Place the cursor on the opposite end of an isearch when exiting. Do it with C-RET while in isearch (The prot-emacs-search.el tweaks to the isearch motion behaviour).
  • Delete the non-matching portion of a query in isearch with a single backspace instead of doing it character-by-character.
  • Type M-s M-< (prot-search-isearch-beginning-of-buffer) or M-s M-> (prot-search-isearch-end-of-buffer) to search for the symbol at point starting from the beginning/end of the buffer.

For variants of occur or grep, which also benefit from the rules I have on where windows/buffers are displayed (The prot-emacs-window.el rules for displaying buffers (display-buffer-alist)):

  • Type M-s s (prot-search-outline) to use minibuffer completion to match an entry across the buffer’s outline. This is probably my most used command. What I like about it is that it benefits from the extensive customisations I make to the completion mechanism and the minibuffer user interface (The prot-emacs-completion.el module). Concretely, I can benefit from the visualisation produced by the vertico or mct packages and match the heading with an out-of-order pattern using orderless. Simple and super effective! Note that the consult package provides the consult-outline command, which was the basis for my variant. Unlike mine, it shows a preview of the current match. I find previews disorienting when I type quickly and do not want to experience any motion sickness while using Emacs (this is not the fault of consult, just me needing to have a more static interface). Though there are times I am slowing down the search and need the preview, at which point I call the Consult command on M-s M-s (The prot-emacs-completion.el settings for consult).
  • Type M-s M-o (prot-search-occur-outline) to produce an outline of the given buffer. What constitutes an outline is defined in the user option prot-search-outline-regexp-alist. I only configure it for the major modes I care about, though the mechanism should work for every buffer that has an outline.
  • Type M-s M-t (prot-search-grep-todo-keywords) to produce an *occur* buffer that matches keywords like TODO. The full regular expression is specified in the user option prot-search-todo-keywords.
  • Type C-u M-s M-t or M-s M-T (prot-search-git-grep-todo-keywords) to do the same as above but with the git-grep program instead of Emacs’ occur. This will match the keywords throughout the current Git repository. Grep buffers are editable, like those of occur (The prot-emacs-search.el setup for editable grep buffers (wgrep)).
  • Type M-s g (prot-search-grep) to perform a “local grep” across the current directory. Do C-u M-s g to perform a “recursive grep” from the current directory and into all subdirectories. This is basically a streamlined version of M-x lgrep and M-x rgrep and is one of my favourite commands.

Note that the consult package provides lots of useful commands that perform a search while also displaying a preview of what you are matching (The prot-emacs-completion.el settings for consult). Depending on your workflow, this is better, though I seldom need the preview as I know what to expect or otherwise peruse contents either by visiting individual files or by using a grep buffer as an index.

(use-package prot-search
  :ensure nil
  :bind
  ( :map global-map
    ("M-s M-%" . prot-search-replace-markup) ; see `prot-search-markup-replacements'
    ("M-s M-<" . prot-search-isearch-beginning-of-buffer)
    ("M-s M->" . prot-search-isearch-end-of-buffer)
    ("M-s g" . prot-search-grep)
    ("M-s u" . prot-search-occur-urls)
    ("M-s t" . prot-search-occur-todo-keywords)
    ("M-s M-t" . prot-search-grep-todo-keywords) ; With C-u it runs `prot-search-git-grep-todo-keywords'
    ("M-s M-T" . prot-search-git-grep-todo-keywords)
    ("M-s s" . prot-search-outline)
    ("M-s M-o" . prot-search-occur-outline)
    ("M-s M-u" . prot-search-occur-browse-url)
    :map isearch-mode-map
    ("<up>" . prot-search-isearch-repeat-backward)
    ("<down>" . prot-search-isearch-repeat-forward)
    ("<backspace>" . prot-search-isearch-abort-dwim)
    ("<C-return>" . prot-search-isearch-other-end))
  :config
  (setq prot-search-outline-regexp-alist
        '((emacs-lisp-mode . "^\\((\\|;;;+ \\)")
          (org-mode . "^\\(\\*+ +\\|#\\+[Tt][Ii][Tt][Ll][Ee]:\\)")
          (outline-mode . "^\\*+ +")
          (emacs-news-view-mode . "^\\*+ +")
          (conf-toml-mode . "^\\[")
          (markdown-mode . "^#+ +")))
  (setq prot-search-todo-keywords
        (concat "TODO\\|FIXME\\|NOTE\\|REVIEW\\|XXX\\|KLUDGE"
                "\\|HACK\\|WARN\\|WARNING\\|DEPRECATED\\|BUG"))

  (with-eval-after-load 'pulsar
    (add-hook 'prot-search-outline-hook #'pulsar-recenter-center)
    (add-hook 'prot-search-outline-hook #'pulsar-reveal-entry)))

5.5.8. The prot-emacs-search.el tweaks to xref, re-builder and grep

The xref.el provides the infrastructure to jump to and from a definition. For example, with point over a function call, xref-find-definitions will jump to the file and location where the function is defined or provide an option to pick one among multiple definitions, where applicable. The grep.el is a wrapper for the Unix program of the same name. Not much to add there. While re-builder.el defines a command that lets us write a regexp that matches against the current buffer, allowing us to test it live.

Note the use of the let to decide whether I use the grep or rg (ripgrep) program: this covers Xref as well.

;;; grep and xref
(use-package re-builder
  :ensure nil
  :commands (re-builder regexp-builder)
  :config
  (setq reb-re-syntax 'read))

(use-package xref
  :ensure nil
  :commands (xref-find-definitions xref-go-back)
  :config
  ;; 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))

(use-package grep
  :ensure nil
  :commands (grep lgrep rgrep)
  :config
  (setq grep-save-buffers nil)
  (setq grep-use-headings t) ; Emacs 30

  (let ((executable (or (executable-find "rg") "grep"))
        (rgp (string-match-p "rg" grep-program)))
    (setq grep-program executable)
    (setq grep-template
          (if rgp
              "/usr/bin/rg -nH --null -e <R> <F>"
            "/usr/bin/grep <X> <C> -nH --null -e <R> <F>"))
    (setq xref-search-program (if rgp 'ripgrep 'grep))))

5.5.9. The prot-emacs-search.el setup for editable grep buffers (grep-edit-mode or wgrep)

Starting with Emacs 31, buffers using grep-mode can now be edited directly. The idea is to collect the results of a search in one place and quickly apply a change across all or some of them. We have the same concept with occur (M-x occur) as well as with Dired buffers (The prot-emacs-dired.el section about wdired (writable Dired)). I use this in tandem with my prot-search-grep command.

For older versions of Emacs, we have the wgrep package by Masahiro Hayashi. I configure it to have key bindings like those of the occur edit mode, which grep-edit-mode also uses.

;;; wgrep (writable grep)
;; See the `grep-edit-mode' for the new built-in feature.
(unless (>= emacs-major-version 31)
  (use-package wgrep
    :ensure t
    :after grep
    :bind
    ( :map grep-mode-map
      ("e" . wgrep-change-to-wgrep-mode)
      ("C-x C-q" . wgrep-change-to-wgrep-mode)
      ("C-c C-c" . wgrep-finish-edit))
    :config
    (setq wgrep-auto-save-buffer t)
    (setq wgrep-change-readonly-file t)))

5.5.10. The prot-emacs-search.el call to provide

Finally, we provide the module. This is the mirror function of require (The init.el final part to load the individual modules).

(provide 'prot-emacs-search)

5.6. The prot-emacs-dired.el module

[ Watch: https://protesilaos.com/codelog/2023-06-26-emacs-file-dired-basics/ (2023-06-26) ]

Dired is probably my favourite Emacs tool. It exemplifies how I see Emacs as a whole: a layer of interactivity on top of Unix. The dired interface wraps—and puts to synergy—standard commands like ls, cp, mv, rm, mkdir, chmod, and related. All while granting access to many other conveniences, such as (i) marking files to operate on (individually, with a regexp, etc.), (ii) bulk renaming files by making the buffer writable and editing it like a regular file, (iii) showing only files you want, (iv) listing the contents of any subdirectory, such as to benefit from the bulk-renaming capability, (v) running a keyboard macro that edits file contents while using Dired to navigate the file listing, (vi) open files in an external application, and more.

Dired lets us work with our files in a way that still feels close to the command-line, yet has more powerful interactive features than even fully fledged, graphical file managers.

5.6.1. The prot-emacs-dired.el settings for common operations

I add two settings which make all copy, rename/move, and delete operations more intuitive. I always want to perform those actions in a recursive manner, as this is the intent I have when I am targeting directories.

The delete-by-moving-to-trash is a deviation from the behaviour of the rm program, as it sends the file into the virtual trash folder. Depending on the system, files in the trash are either removed automatically after a few days, or we still have to permanently delete them manually. I prefer this extra layer of safety. Plus, we have the trashed package to navigate the trash folder in a Dired-like way (The prot-emacs-dired.el section about trashed.el).

;;; Dired file manager and prot-dired.el extras
(use-package dired
  :ensure nil
  :commands (dired)
  :config
  (setq dired-recursive-copies 'always)
  (setq dired-recursive-deletes 'always)
  (setq delete-by-moving-to-trash t))

5.6.2. The prot-emacs-dired.el switches for ls (how files are listed)

As I already explained, Dired is a layer of interactivity on top of standard Unix tools (The prot-emacs-dired.el module). We can see this in how Dired produces the file listing and how we can affect it. The ls program accepts an -l flag for a “long”, detailed list of files. This is what Dired uses. But we can pass more flags by setting the value of dired-listing-switches. Do M-x man and then search for the ls manpage to learn about what I have here. In short:

-A
Show hidden files (“dotfiles”), such as .bashrc, but omit the implied . and .. targets. The latter two refer to the present and parent directory, respectively.
-G
Do not show the group name in the long listing. Only show the owner of the file.
-F
Differentiate regular from special files by appending a character to them. The * is for executables, the / is for directories, the | is for a named pipe, the = is for a socket, the @ and the > are for stuff I have never seen.
-h
Make file sizes easier to read, such as 555k instead of 568024 (the size of prot-emacs.org as of this writing).
-l
Produce a long, detailed listing. Dired requires this.
-v
Sort files by version numbers, such that file1, file2, and file10 appear in this order instead of 1, 10, 2. The latter is called “lexicograhic” and I have not found a single case where it is useful to me.
--group-directories-first
Does what it says to place all directories before files in the listing. I prefer this over a strict sorting that does not differentiate between files and directories.
--time-style=long-iso
Uses the international standard for time representation in the file listing. So we have something like 2023-12-30 06:38 to show the last modified time.
(use-package dired
  :ensure nil
  :commands (dired)
  :config
  (setq dired-listing-switches
        "-AGFhlv --group-directories-first --time-style=long-iso"))

5.6.3. The prot-emacs-dired.el setting for dual-pane Dired

I often have two Dired buffers open side-by-side and want to move files between them. By setting dired-dwim-target to a t value, we get the other buffer as the default target of the current rename or copy operation. This is exactly what I want.

If there are more than two windows showing Dired buffers, the default target is the previously visited window.

Note that this only affects how quickly we can access the default value, as we can always type M-p (previous-history-element) and M-n (next-history-element) to cycle through the minibuffer history (The prot-emacs-completion.el settings for saving the history (savehist-mode)).

(use-package dired
  :ensure nil
  :commands (dired)
  :config
  (setq dired-dwim-target t))

5.6.4. The prot-emacs-dired.el settings to open files externally

From inside a Dired buffer, we can type ! (M-x dired-do-shell-command) or & (M-x dired-do-async-shell-command) to run an arbitrary command with the given file (or marked files) as an argument. These commands will produce a minibuffer prompt, which expects us to type in the name of the command. Emacs already tries to guess some relevant defaults, though we can make it do what we want by configuring the dired-guess-shell-alist-user user option.

This variable takes an alist value: a list of lists. Each element (each list) has the first item in the list as a regular expression to match file names. We normally want to have file type extensions here, though we can also target the full name of a file. The remaining entries in the list are strings that specify the name of the external program to use. We can have as many as we want and cycle between them using the familiar M-p and M-n keys inside the minibuffer (which call the commands previous-history-element and next-history-element, respectively).

On Linux, the generic “open with default app” call is xdg-open, so we always want that as a fallback.

Note that Emacs 30 (current development target as of this writing on 2023-12-30 16:12 +0200), we have the command dired-do-open, which is the equivalent of typing & and then specifying the xdg-open command.

(use-package dired
  :ensure nil
  :commands (dired)
  :config
  (setq dired-guess-shell-alist-user ; those are the suggestions for ! and & in Dired
        '(("\\.\\(png\\|jpe?g\\|tiff\\)" "feh" "xdg-open")
          ("\\.\\(mp[34]\\|m4a\\|ogg\\|flac\\|webm\\|mkv\\)" "mpv" "xdg-open")
          (".*" "xdg-open"))))

5.6.5. The prot-emacs-dired.el miscellaneous tweaks

These are some minor tweaks that I do not really care about. The only one which is really nice in my opinion is the hook that involves the dired-hide-details-mode. This is the command that hides the noisy output of the ls -l flag, leaving only the file names in the list (The prot-emacs-dired.el switches for ls (how files are listed)). We can toggle this effect at any time with the ( key, by default.

I disable the repetition of the j key as I do use repeat-mode (The prot-emacs-essentials.el settings for repeat-mode).

(use-package dired
  :ensure nil
  :commands (dired)
  :config
  (setq dired-auto-revert-buffer #'dired-directory-changed-p) ; also see `dired-do-revert-buffer'
  (setq dired-make-directory-clickable t) ; Emacs 29.1
  (setq dired-free-space nil) ; Emacs 29.1
  (setq dired-mouse-drag-files t) ; Emacs 29.1

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

  ;; In Emacs 29 there is a binding for `repeat-mode' which lets you
  ;; repeat C-x C-j just by following it up with j.  For me, this is a
  ;; problem as j calls `dired-goto-file', which I often use.
  (define-key dired-jump-map (kbd "j") nil))

5.6.6. The prot-emacs-dired.el section about various conveniences

The dired-aux.el and dired-x.el are two built-in libraries that provide useful extras for Dired. The highlights from what I have here are:

  • the user option dired-create-destination-dirs and dired-create-destination-dirs-on-trailing-dirsep, which offer to create the specified directory path if it is missing.
  • the user options dired-clean-up-buffers-too and dired-clean-confirm-killing-deleted-buffers which cover the deletion of buffers related to files that we delete from Dired.
  • the key binding for dired-do-open, which opens the file or directory externally (The prot-emacs-dired.el settings to open files externally).
(use-package dired-aux
  :ensure nil
  :after dired
  :bind
  ( :map dired-mode-map
    ("C-+" . dired-create-empty-file)
    ("M-s f" . nil)
    ("C-<return>" . dired-do-open) ; Emacs 30
    ("C-x v v" . dired-vc-next-action)) ; Emacs 28
  :config
  (setq dired-isearch-filenames 'dwim)
  (setq dired-create-destination-dirs 'ask) ; Emacs 27
  (setq dired-vc-rename-file t)             ; Emacs 27
  (setq dired-do-revert-buffer (lambda (dir) (not (file-remote-p dir)))) ; Emacs 28
  (setq dired-create-destination-dirs-on-trailing-dirsep t)) ; Emacs 29

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

5.6.7. The prot-emacs-dired.el section about my extras (prot-dired.el)

Dired is excellent out-of-the-box. What I provide are a few minor commands that make it more convenient for me to perform common actions (The prot-dired.el library). Chief among them is the prot-dired-limit-regexp (bound to C-c C-l), which is an easier way to do this in standard Dired:

  • Type % m (dired-mark-files-regexp) to mark files you want to keep seeing. Provide a regular expression or simply a common word.
  • Toggle the mark so that you now cover everything you do not want to see.
  • Invoke dired-do-kill-lines (bound to k by default) to remove the marked files from the view until the buffer is generated again (with g by default (revert-buffer)).

All this is fine, but with prot-dired-limit-regexp I simply provide the regexp I want to see.

Another common use-case for me is to create a flat listing of all files that match a regular expression, found recursively from the current directory. I do this with prot-dired-search-flat-list.

The other commands have situational uses. For example, the prot-dired-grep-marked-files is something I have only used a few times where prot-search-grep would produce too many results in a given directory (The prot-emacs-search.el extras provided by the prot-search.el library).

(use-package prot-dired
  :ensure nil
  :hook (dired-mode . prot-dired-setup-imenu)
  :bind
  ( :map dired-mode-map
    ("i" . prot-dired-insert-subdir) ; override `dired-maybe-insert-subdir'
    ("/" . prot-dired-limit-regexp)
    ("C-c C-l" . prot-dired-limit-regexp)
    ("M-n" . prot-dired-subdirectory-next)
    ("C-c C-s" . prot-dired-search-flat-list)
    ("C-c C-n" . prot-dired-subdirectory-next)
    ("C-c C-p" . prot-dired-subdirectory-previous)
    ("M-s G" . prot-dired-grep-marked-files) ; M-s g is `prot-search-grep'
    ("M-p" . prot-dired-subdirectory-previous)))

5.6.8. The prot-emacs-dired.el section about dired-subtree

The dired-subtree package by Matúš Goljer provides the convenience of quickly revealing the contents of the directory at point. We do not have to insert its contents below the current listing, as we would normally do in Dired, nor do we have to open it in another buffer just to check if we need to go further.

I do not use this feature frequently, though I appreciate it when I do need it.

(use-package dired-subtree
  :ensure t
  :after dired
  :bind
  ( :map dired-mode-map
    ("<tab>" . dired-subtree-toggle)
    ("TAB" . dired-subtree-toggle)
    ("<backtab>" . dired-subtree-remove)
    ("S-TAB" . dired-subtree-remove))
  :config
  (setq dired-subtree-use-backgrounds nil))

5.6.9. The prot-emacs-dired.el section about wdired (writable Dired)

As noted in the introduction, Dired can be made writable (The prot-emacs-dired.el module). This way, we can quickly rename multiple files using Emacs’ panoply of editing capabilities.

Both of the variables I configure here have situational usage. I cannot remember the last time I benefited from them.

Note that we have a variant of wdired for grep buffers (The prot-emacs-search.el setup for editable grep buffers (wgrep)).

(use-package wdired
  :ensure nil
  :commands (wdired-change-to-wdired-mode)
  :config
  (setq wdired-allow-to-change-permissions t)
  (setq wdired-create-parent-directories t))

5.6.10. The prot-emacs-dired.el section about image-dired

The built-in image-dired feature is one of those tools that I like when I use, but never remember exactly how to use because of how infrequent my need for it is. The gist is that we put thumbnails in a buffer and can browse through them in a file listing. It is okay, though if I really have to rely on image previewing, I might just open the graphical file manager. It is not a sin to use something outside of Emacs and, anyway, I don’t think Emacs’ editing capabilities are of any relevance when we are just previewing a picture.

(use-package image-dired
  :ensure nil
  :commands (image-dired)
  :bind
  ( :map image-dired-thumbnail-mode-map
    ("<return>" . image-dired-thumbnail-display-external))
  :config
  (setq image-dired-thumbnail-storage 'standard)
  (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))

5.6.11. The prot-emacs-dired.el section about dired-preview

The dired-preview is a package of mine that, unfortunately, I have not had the time to develop beyond the original release: it coincided with my relocation to the hut and I still am busy doing all sorts of work here. Once I get the opportunity, I will resume development of this package.

At any rate, dired-preview previews the file at point in an Emacs window. The goal is to make this work for large files, so that we never experience any lag. I will expand this section once I feel we are where we need to be.

;;; Automatically preview Dired file at point (dired-preview.el)
;; One of my packages: <https://protesilaos.com/emacs>
(use-package dired-preview
  :ensure t
  ;; :hook (dired-mode . (lambda ()
  ;;                       (when (string-match-p "Pictures" default-directory)
  ;;                         (dired-preview-mode 1))))
  :defer 1
  :hook (after-init . dired-preview-global-mode)
  :config
  (setq dired-preview-max-size (* (expt 2 20) 10))
  (setq dired-preview-delay 0.5)
  (setq dired-preview-ignored-extensions-regexp
        (concat "\\."
                "\\(gz\\|"
                "zst\\|"
                "tar\\|"
                "xz\\|"
                "rar\\|"
                "zip\\|"
                "iso\\|"
                "epub"
                "\\)"))

  ;; (setq dired-preview-display-action-alist
  ;;       '((display-buffer-in-side-window)
  ;;         (side . bottom)
  ;;         (window-height . 0.2)
  ;;         (preserve-size . (t . t))
  ;;         (window-parameters . ((mode-line-format . none)
  ;;                               (header-line-format . none)))))
  )

5.6.12. The prot-emacs-dired.el section about multimedia previews (ready-player)

This is a neat package by Alvaro Ramirez which produces a previewable representation of multimedia file when we try to visit them in an Emacs buffer. Without ready-player-major-mode, we get a bunch of gibberish from the binary data.

While this is a standalone package, it synergises with my dired-preview without the need to add any glue code between the two packages (The prot-emacs-dired.el section about dired-preview). When I move the cursor over a multimedia file, the Dired preview buffer tries to visit the file at point, which is in turn rendered using ready-player.

(use-package ready-player
  :ensure t
  :mode
  ("\\.\\(mp3\\|m4a\\|mp4\\|mkv\\|webm\\)\\'" . ready-player-major-mode)
  :config
  (setq ready-player-autoplay nil)
  (setq ready-player-repeat nil))

5.6.13. The prot-emacs-dired.el section about trashed.el

The trashed package by Shingo Tanaka provides a Dired-like interface to the system’s virtual trash directory. The few times I need to restore a file, I do M-x dired, then type r to mark the file to be restored (M-x trashed-flag-restore), and then type x (M-x trashed-do-execute) to apply the effect.

;;; dired-like mode for the trash (trashed.el)
(use-package trashed
  :ensure t
  :commands (trashed)
  :config
  (setq trashed-action-confirmer 'y-or-n-p)
  (setq trashed-use-header-line t)
  (setq trashed-sort-key '("Date deleted" . t))
  (setq trashed-date-format "%Y-%m-%d %H:%M:%S"))

5.6.14. The prot-emacs-dired.el section about mandoura (mpv media player)

The mandoura package is an experiment of mine to use Dired as a starting point to play back some media files (or directory containing such files) using the mpv program. I use it daily and it works fine for just playback, but I do need/want to develop it further as it really is nothing but a prototype right now. You will thus notice that the package declaration is actually installing the file from source: this is a feature built into Emacs 30, though it also is available as a package for Emacs 29.

;;; Play back media with Dired (mandoura.el)
;; This is yet another package of mine: <https://protesilaos.com/emacs>
(use-package mandoura
  ;; The :vc keyword is part of Emacs 30.  Read the manual for what keywords it reads: (info "(emacs) Fetching Package Sources")
  :vc ( :url "https://github.com/protesilaos/mandoura")
  :commands (mandoura-play-playlist)
  :after dired
  :bind
  ( :map global-map
    ("M-<AudioPlay>" . mandoura-return-track-title-and-time)
    ("M-<XF86AudioPlay>" . mandoura-return-track-title-and-time)
    :map dired-mode-map
    ("M-<return>" . mandoura-play-playlist)
    ("M-RET" . mandoura-play-playlist))
  :config
  (setq mandoura-saved-playlist-directory "~/Music/playlists/"))

5.6.15. The prot-emacs-dired.el call to provide

Finally, we provide the module. This is the mirror function of require (The init.el final part to load the individual modules).

(provide 'prot-emacs-dired)

5.7. The prot-emacs-window.el module

This module is all about buffers and windows. How they are managed and displayed.

5.7.1. The prot-emacs-window.el section about running commands in popup frames

Sometimes we want to call a command from outside Emacs. I have arranged for this to happen so that a new frame pops up, I do the command, and then the frame is deleted. This requires that we use the Emacs daemon process or server-mode (The prot-emacs-essentials.el arrangement to run Emacs as a server). We can thus call emacsclient from outside Emacs and instruct it to run any arbitrary Emacs Lisp code.

My prot-window.el provides the prot-window-define-with-popup-frame macro which streamlines the creation of commands that have the desired behaviour (The prot-window.el library). All we need is to call the macro and tell it which command should be empowered to run in such a popup frame. I did a video demonstrating this functionality: Emacs: commands in popup frames with emacsclient (2024-09-19).

All we need here is the hooks that close the frame after the commands run succesfully. The commands I configure are for org-capture and tmr:

(with-eval-after-load 'org-capture
  (add-hook 'org-capture-after-finalize-hook #'prot-window-delete-popup-frame))

(with-eval-after-load 'tmr
  (add-hook 'tmr-timer-created-functions #'prot-window-delete-popup-frame))

At my system level I also have keybindings bound to call the following (you can try those on the command line—remember they depend on the daemon or server-mode):

# Run `org-capture` in a popup frame that is deleted after you are done.
emacsclient -e '(prot-window-popup-org-capture)'
# Same idea for the `tmr` command.
emacsclient -e '(prot-window-popup-tmr)'

5.7.2. The prot-emacs-window.el section about uniquifying buffer names

When a buffer name is reserved, Emacs tries to produce the new buffer by finding a suitable variant of the original name. The doc string of the variable uniquify-buffer-name-style does a good job at explaining the various patterns:

For example, the files ‘/foo/bar/mumble/name’ and ‘/baz/quux/mumble/name’
would have the following buffer names in the various styles:

  forward                       bar/mumble/name    quux/mumble/name
  reverse                       name\mumble\bar    name\mumble\quux
  post-forward                  name|bar/mumble    name|quux/mumble
  post-forward-angle-brackets   name<bar/mumble>   name<quux/mumble>
  nil                           name               name<2>

I use the forward style, which is the closest to the actual file name.

;;; General window and buffer configurations
(use-package uniquify
  :ensure nil
  :config
;;;; `uniquify' (unique names for buffers)
  (setq uniquify-buffer-name-style 'forward)
  (setq uniquify-strip-common-suffix t)
  (setq uniquify-after-kill-buffer-p t))

5.7.3. The prot-emacs-window.el section about line highlighting (hl-line-mode)

The built-in hl-line-mode highlights the current line by adding a background colour to it. I normally do not use this functionality. I do it only when I need to draw attention to something I am demonstrating.

The nil value for hl-line-sticky-flag makes the line highlight not show up in unfocused windows. I prefer to keep highlights at a minimum, because I then find it harder to focus on where I am. The hl-line-overlay-priority is a more obscure aspect of how Emacs decides which background should take precedence. You most probably do not need this, though I had a case for it a while ago: the specifics do not matter.

;;;; Line highlight
(use-package hl-line
  :ensure nil
  :commands (hl-line-mode)
  :config
  (setq hl-line-sticky-flag nil)
  (setq hl-line-overlay-priority -50)) ; emacs28

5.7.4. The prot-emacs-window.el section about negative space highlighting (whitespace-mode)

Much like hl-line-mode, I normally do not use the whitespace-mode (The prot-emacs-window.el section about line highlighting (hl-line-mode)). I do it only when I have to demonstrate something, or to quickly check that my spacing is correct in some context where that matters.

The changes I have made to whitespace-style are experimental and I do not recommend you copy them. I basically tried to make it less busy, by highlighting fewer elements. For example, I do not highlight newline characters (represented as a $) because these are easy to infer in most cases.

;;;; Negative space highlight
(use-package whitespace
  :ensure nil
  :bind
  (("<f6>" . whitespace-mode)
   ("C-c z" . delete-trailing-whitespace))
  :config
  ;; NOTE 2023-08-14: This is experimental.  I am not sure I like it.
  (setq whitespace-style
        '(face
          tabs
          spaces
          tab-mark
          space-mark
          trailing
          missing-newline-at-eof
          space-after-tab::tab
          space-after-tab::space
          space-before-tab::tab
          space-before-tab::space)))

5.7.5. The prot-emacs-window.el section about line numbers (display-line-numbers-mode)

As with the two previous sections, I do not like to see line numbers by default and seldom use display-line-numbers-mode. They do not help me navigate a buffer, nor are they relevant in most cases. I enable the mode only when I need to compare buffers or to get a sense of how far apart two relevant sections are in a file.

;;; Line numbers on the side of the window
(use-package display-line-numbers
  :ensure nil
  :bind
  ("<f7>" . display-line-numbers-mode)
  :config
  (setq-default 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))

5.7.6. The prot-emacs-window.el rules for displaying buffers (display-buffer-alist)

[ Watch: control where buffers are displayed (the display-buffer-alist) (2024-02-08). ]

The display-buffer-alist is a powerful user option and somewhat hard to get started with. The reason for its difficulty comes from the knowledge required to understand the underlying display-buffer mechanism.

Here is the gist of what we do with it:

  • The alist is a list of lists.
  • Each element of the alist (i.e. one of the lists) is of the following form:

      (BUFFER-MATCHER
       FUNCTIONS-TO-DISPLAY-BUFFER
       OTHER-PARAMETERS)
    
  • The BUFFER-MATCHER is either a regular expression to match the buffer by its name or a method to get the buffer whose major mode is the one specified. In the latter case, you will see the use of cons cells (like (one . two)) involving the derived-mode symbol (remember that I build Emacs from source, so derived-mode may not exist in your version of Emacs).
  • The FUNCTIONS-TO-DISPLAY-BUFFER is a list of display-buffer functions that are tried in the order they appear in until one works. The list can be of one element, as you will notice with some of my entries.
  • The OTHER-PARAMETERS are enumerated in the Emacs Lisp Reference Manual. Evaluate:

      (info "(elisp) Buffer Display Action Alists")
    

In my prot-window.el library, I define functions that determine how a buffer should be displayed, given size considerations (The prot-window.el library). You will find the functions prot-window-shell-or-term-p to determine what a shell or terminal is, prot-window-display-buffer-below-or-pop to display the buffer below the current one or to its side depending on how much width is available, and prot-window-select-fit-size to perform the two-fold task of selecting a window and making it fit up to a certain height.

  ;;;; `window', `display-buffer-alist', and related
  (use-package prot-window
    :ensure nil
    :demand t
    :config
    ;; NOTE 2023-03-17: Remember that I am using development versions of
    ;; Emacs.  Some of my `display-buffer-alist' contents are for Emacs
    ;; 29+.
    (setq display-buffer-alist
          `(;; no window
            ("\\`\\*Async Shell Command\\*\\'"
             (display-buffer-no-window))
            ("\\`\\*\\(Warnings\\|Compile-Log\\|Org Links\\)\\*\\'"
             (display-buffer-no-window)
             (allow-no-window . t))
            ;; bottom side window
            ("\\*Org \\(Select\\|Note\\)\\*" ; the `org-capture' key selection and `org-add-log-note'
             (display-buffer-in-side-window)
             (dedicated . t)
             (side . bottom)
             (slot . 0)
             (window-parameters . ((mode-line-format . none))))
            ;; bottom buffer (NOT side window)
            ((or . ((derived-mode . flymake-diagnostics-buffer-mode)
                    (derived-mode . flymake-project-diagnostics-mode)
                    (derived-mode . messages-buffer-mode)
                    (derived-mode . backtrace-mode)))
             (display-buffer-reuse-mode-window display-buffer-at-bottom)
             (window-height . 0.3)
             (dedicated . t)
             (preserve-size . (t . t)))
            ("\\*Embark Actions\\*"
             (display-buffer-reuse-mode-window display-buffer-below-selected)
             (window-height . fit-window-to-buffer)
             (window-parameters . ((no-other-window . t)
                                   (mode-line-format . none))))
            ("\\*\\(Output\\|Register Preview\\).*"
             (display-buffer-reuse-mode-window display-buffer-at-bottom))
            ;; below current window
            ("\\(\\*Capture\\*\\|CAPTURE-.*\\)"
             (display-buffer-reuse-mode-window display-buffer-below-selected))
            ("\\*\\vc-\\(incoming\\|outgoing\\|git : \\).*"
             (display-buffer-reuse-mode-window display-buffer-below-selected)
             (window-height . 0.1)
             (dedicated . t)
             (preserve-size . (t . t)))
            ((derived-mode . reb-mode) ; M-x re-builder
             (display-buffer-reuse-mode-window display-buffer-below-selected)
             (window-height . 4) ; note this is literal lines, not relative
             (dedicated . t)
             (preserve-size . (t . t)))
            ((or . ((derived-mode . occur-mode)
                    (derived-mode . grep-mode)
                    (derived-mode . Buffer-menu-mode)
                    (derived-mode . log-view-mode)
                    (derived-mode . help-mode) ; See the hooks for `visual-line-mode'
                    "\\*\\(|Buffer List\\|Occur\\|vc-change-log\\|eldoc.*\\).*"
                    prot-window-shell-or-term-p
                    ;; ,world-clock-buffer-name
                    ))
             (prot-window-display-buffer-below-or-pop)
             (body-function . prot-window-select-fit-size))
            ("\\*\\(Calendar\\|Bookmark Annotation\\|ert\\).*"
             (display-buffer-reuse-mode-window display-buffer-below-selected)
             (dedicated . t)
             (window-height . fit-window-to-buffer))
            ;; NOTE 2022-09-10: The following is for `ispell-word', though
            ;; it only works because I override `ispell-display-buffer'
            ;; with `prot-spell-ispell-display-buffer' and change the
            ;; value of `ispell-choices-buffer'.
            ("\\*ispell-top-choices\\*.*"
             (display-buffer-reuse-mode-window display-buffer-below-selected)
             (window-height . fit-window-to-buffer))
            ;; same window

            ;; NOTE 2023-02-17: `man' does not fully obey the
            ;; `display-buffer-alist'.  It works for new frames and for
            ;; `display-buffer-below-selected', but otherwise is
            ;; unpredictable.  See `Man-notify-method'.
            ((or . ((derived-mode . Man-mode)
                    (derived-mode . woman-mode)
                    "\\*\\(Man\\|woman\\).*"))
             (display-buffer-same-window)))))

The following settings are relevant for the display-buffer-alist we saw right above. Notice, in particular, the split-height-threshold and split-width-threshold which determine when to split the frame by height or width. These are relevant for prot-window-display-buffer-below-or-pop and the other more basic functions I have defined for this purpose.

(use-package prot-window
  :ensure nil
  :demand t
  :config
  (setq window-combination-resize t)
  (setq even-window-sizes 'height-only)
  (setq window-sides-vertical nil)
  (setq switch-to-buffer-in-dedicated-window 'pop)
  (setq split-height-threshold 80)
  (setq split-width-threshold 125)
  (setq window-min-height 3)
  (setq window-min-width 30))

5.7.7. The prot-emacs-window.el setting to enable visual-line-mode in some contexts

I normally do not use visual-line-mode. What it does is to break long lines to span multiple lines without actually affecting the underlying text. In other words, we still have one long line only its visualisation is as a paragraph.

For the cases where I am fine with visual-line-mode, I enable the mode by adding it to these mode hooks.

(use-package prot-window
  :ensure nil
  :demand t
  :hook
  ((epa-info-mode help-mode custom-mode) . visual-line-mode))

5.7.8. The prot-emacs-window.el settings to truncate some buffers silently

This here is the opposite of what we saw above (The prot-emacs-window.el setting to enable visual-line-mode in some contexts). Unlike visual-line-mode where long lines are made to look like paragraphs, “truncation” means to let the line cover its natural length and simply cut it off screen.

I have a custom library that defines a function which performs line truncation without displaying a message about the fact (The prot-common.el library). Why do we need this? Check the output of M-x calendar in a tiny window and you will see the reason. In short, it is better to have lines not show their full contents than to have something that looks completely broken.

The whole point of using hooks is to make these decisions on a case-by-case basis.

(use-package prot-window
  :ensure nil
  :demand t
  :hook
  ((world-clock-mode calendar-mode) . prot-common-truncate-lines-silently))

5.7.9. The prot-emacs-window.el section key bindings

Here I simply define some key bindings. The commands I use the most in this list are delete-other-windows-vertically, previous-buffer, and next-buffer. I rarely resize windows, though I am happy to use the mouse for such a case.

(use-package prot-window
  :ensure nil
  :demand t
  :bind
  ( :map global-map
    ;; NOTE 2022-09-17: Also see `prot-simple-swap-window-buffers'.
    ("C-x <down>" . next-buffer)
    ("C-x <up>" . previous-buffer)
    ("C-x C-n" . next-buffer)     ; override `set-goal-column'
    ("C-x C-p" . previous-buffer) ; override `mark-page'
    ("C-x !" . delete-other-windows-vertically)
    ("C-x _" . balance-windows)      ; underscore
    ("C-x -" . fit-window-to-buffer) ; hyphen
    ("C-x +" . balance-windows-area)
    ("C-x }" . enlarge-window)
    ("C-x {" . shrink-window)
    ("C-x >" . enlarge-window-horizontally) ; override `scroll-right'
    ("C-x <" . shrink-window-horizontally) ; override `scroll-left'
    :map resize-window-repeat-map
    (">" . enlarge-window-horizontally)
    ("<" . shrink-window-horizontally)))

5.7.10. The prot-emacs-window.el section about beframe

[ Also see: The prot-emacs-git.el section about project.el. ]

My beframe package enables a frame-oriented Emacs workflow where each frame has access to the list of buffers visited therein. In the interest of brevity, we call buffers that belong to frames “beframed”. Check the video demo I did and note that I consider this one of the best changes I ever did to boost my productivity: https://protesilaos.com/codelog/2023-02-28-emacs-beframe-demo/.

;;; Frame-isolated buffers
;; Another package of mine.  Read the manual:
;; <https://protesilaos.com/emacs/beframe>.
(use-package beframe
  :ensure t
  :hook (after-init . beframe-mode)
  :config
  (setq beframe-functions-in-frames '(project-prompt-project-dir))

  ;; I use this instead of :bind because I am binding a keymap and the
  ;; way `use-package' does it is by wrapping a lambda around it that
  ;; then breaks `describe-key' for those keys.
  (prot-emacs-keybind global-map
    ;; Override the `set-fill-column' that I have no use for.
    "C-x f" #'other-frame-prefix
    ;; Bind Beframe commands to a prefix key. Notice the -map as I am
    ;; binding keymap here, not a command.
    "C-c b" #'beframe-prefix-map
    ;; Replace the generic `buffer-menu'.  With a prefix argument, this
    ;; commands prompts for a frame.  Call the `buffer-menu' via M-x if
    ;; you absolutely need the global list of buffers.
    "C-x C-b" #'beframe-buffer-menu
    ;; Not specific to Beframe, but since it renames frames (by means
    ;; of `beframe-mode') it is appropriate to have this here:
    "C-x B" #'select-frame-by-name))

5.7.11. The prot-emacs-window.el configuration of undelete-frame-mode and winner-mode

Since I am using my beframe package to isolate buffers per frame (The prot-emacs-window.el section about beframe), I appreciate the feature of Emacs 29 to undo the deletion of frames. Note the key binding I use for this purpose. It overrides one of the alternatives for the standard undo command, though I personally only ever use C-/: everything else is free to use as I see fit.

;;; Frame history (undelete-frame-mode)
(use-package frame
  :ensure nil
  :bind ("C-x u" . undelete-frame) ; I use only C-/ for `undo'
  :hook (after-init . undelete-frame-mode))

The winner-mode is basically the same idea as undelete-frame-mode but for window layouts. Or maybe I should phrase this the other way round, given that winner is the older package. But the point is that we can quickly go back to an earlier arrangement of windows in a frame.

;;; Window history (winner-mode)
(use-package winner
  :ensure nil
  :hook (after-init . winner-mode)
  :bind
  (("C-x <right>" . winner-redo)
   ("C-x <left>" . winner-undo)))

5.7.12. The prot-emacs-window.el keys for window motions (windmove)

I usually cycle between windows with C-x o (M-x other-window). This is because I use two or three windows at most. It is easier to use the same key, especially with the addition of repeat-mode (The prot-emacs-essentials.el settings for repeat-mode). Nevertheless, the built-in windmove package provides a set of useful commands to move focus directionally between windows but also to shift the placement of a window in the given direction.

;;; Directional window motions (windmove)
(use-package windmove
  :ensure nil
  :bind
  ;; Those override some commands that are already available with
  ;; C-M-u, C-M-f, C-M-b.
  (("C-M-<up>" . windmove-up)
   ("C-M-<right>" . windmove-right)
   ("C-M-<down>" . windmove-down)
   ("C-M-<left>" . windmove-left)
   ("C-M-S-<up>" . windmove-swap-states-up)
   ("C-M-S-<right>" . windmove-swap-states-right) ; conflicts with `org-increase-number-at-point'
   ("C-M-S-<down>" . windmove-swap-states-down)
   ("C-M-S-<left>" . windmove-swap-states-left))
  :config
  (setq windmove-create-window nil)) ; Emacs 27.1

5.7.13. The prot-emacs-window.el use of contextual header line (breadcrumb)

The breadcrumb package by João Távora lets us display contextual information about the current heading or code definition in the header line. The header line is displayed above the contents of each buffer in the given window. When we are editing an Org file, for example, we see the path to the file, followed by a reference to the tree that leads to the current heading. Same idea for programming modes. Neat!

;;; Header line context of symbol/heading (breadcrumb.el)
(use-package breadcrumb
  :ensure t
  :functions (prot/breadcrumb-local-mode)
  :hook ((text-mode prog-mode) . prot/breadcrumb-local-mode)
  :config
  (setq breadcrumb-project-max-length 0.5)
  (setq breadcrumb-project-crumb-separator "/")
  (setq breadcrumb-imenu-max-length 1.0)
  (setq breadcrumb-imenu-crumb-separator " > ")

  (defun prot/breadcrumb-local-mode ()
    "Enable `breadcrumb-local-mode' if the buffer is visiting a file."
    (when buffer-file-name
      (breadcrumb-local-mode 1))))

5.7.14. The prot-emacs-window.el provide form

As always, we provide the module. This is the mirror function of require (The init.el final part to load the individual modules).

(provide 'prot-emacs-window)

5.8. The prot-emacs-git.el module

[ Watch: Contribute to GNU Emacs core (2023-08-03). ]

Emacs can handle several version control programs, though I only ever use git. Version control is essential to any public-facing programming project: the history of changes is stored persistently, so the developer can retrace when a given snippet of code was introduced or modified. Plus, it is integral to collaboration, which is what free software (such as Emacs and all its packages) is all about.

This section covers my settings for version control per se, but more widely for tools related to checking different versions of files and working with so-called “projects”.

5.8.1. The prot-emacs-git.el section about ediff

[ Watch: Emacs: ediff basics (2023-12-30) ]

The built-in ediff feature provides several commands that let us compare files or buffers side-by-side. The defaults of ediff are bad, in my opinion: it puts buffers one on top of the other and places the “control panel” in a separate Emacs frame. The first time I tried to use it, I thought I broke my setup because it is unlike anything we normally interact with. As such, the settings I have for ediff-split-window-function and ediff-window-setup-function are what I would expect Emacs maintainers to adopt as the new default. I strongly encourage everyone to start with them.

In my workflow, the points of entry to the ediff feature are the commands ediff-files, ediff-buffers. Sometimes I use the 3-way variants with ediff-files3 and ediff-buffers3, though this is rare. Do watch the video I link to in the beginning of this section, as it covers the main functionality of this neat tool. I also show how it integrates with magit (The prot-emacs-git.el section about magit (great Git client)).

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

5.8.2. The prot-emacs-git.el section about project.el

In Emacs parlance, a “project” is a collection of files and/or directories that share the same root. The root of a project is identified by a special file or directory, with .git/ being one of the defaults as it is a version control system supported by the built-in vc.el (The prot-emacs-git.el section about vc.el and related).

We can specify more project roots as a list of strings in the user option project-vc-extra-root-markers. I work exclusively with Git repositories, so I just add there a .project file in case I ever need to register a project without it being controlled by git. In that case, the .project file is just an empty file in a directory that I want to treat as the root of this project.

The common way to switch to a project is to type C-x p p, which calls the command project-switch-project. It lists all registered projects and also includes a ... (choose a dir) option. By choosing a new directory, we register it in our project list if it has a recognisable root. Once we select a project, we are presented with a list of common actions to start working on the project. These are defined in the user option project-switch-commands and are activated by the final key that accesses them from the C-x p prefix. As such, do M-x describe-keymap and check the project-prefix-map. For example, I bind project-dired to C-x p RET, so RET accesses this command after C-x p p as well.

If any of the project.el commands is called from outside a project, it first prompts for a project and then carries out its action. For example, project-find-file will ask for a project to use, then switch to it, and then prompt for a file inside of the specified project.

While inside a project, we have many commands that operate on the project level. For example, C-x p f (project-find-file) searches for a file across the project, while C-x p b (project-switch-to-buffer) switches to a buffer that is specific to the project. Again, check the project-prefix-map for available commands.

If not inside a project, the project-related commands will first prompt to select a project (same as typing C-x p p) and then carry out their action.

I combine projects with my beframe package, so that when I switch to a project I get a new frame that limits the buffers I visit there limited to that frame (The prot-emacs-window.el section about beframe).

Note that the prot-project.el library defines functionality that is useful, but which I personally do not need. I am thus not including it here, but do take a look.

;;;; `project'
(use-package project
  :ensure nil
  :bind
  (("C-x p ." . project-dired)
   ("C-x p C-g" . keyboard-quit)
   ("C-x p <return>" . project-dired)
   ("C-x p <delete>" . project-forget-project))
  :config
  (setopt project-switch-commands
          '((project-find-file "Find file")
            (project-find-regexp "Find regexp")
            (project-find-dir "Find directory")
            (project-dired "Root dired")
            (project-vc-dir "VC-Dir")
            (project-shell "Shell")
            (keyboard-quit "Quit")))
  (setq project-vc-extra-root-markers '(".project")) ; Emacs 29
  (setq project-key-prompt-style t) ; Emacs 30

  (advice-add #'project-switch-project :after #'prot-common-clear-minibuffer-message))

(use-package prot-project
  :ensure nil
  ;; Also check the command `prot-project-in-tab'.  I do not use it
  ;; because I prefer to manage my buffers in frames, with my
  ;; `beframe' package.
  :bind
  ( :map project-prefix-map
    ("p" . prot-project-switch)))

5.8.3. The prot-emacs-git.el section about diff-mode

This built-in mode is an easy and effective way to interact with diffs. A “diff” is a Unix tradition of showing line-wise differences in a file. If you, say, edit a line to replace this with that, the diff output will show the original line prefixed with a minus sign and the new line prefixed with a plus sign.

Individual words can also be highlighted. This word-wise operation is known as “refining” the diff. In my setup, this is handled by the agitate package, which provides convenience functions for various version control operations. The command is agitate-diff-refine-cycle, bound to C-c C-b inside of diff buffers.

With diff-mode, we can also apply the “diff hunk” at point, if we have the corresponding files. The diff hunk is the section of the diff that pertains to a given region in the file and is delimited by a heading that enumerates the affected range, like @@ -6125,7 +6125,9 @@. Type C-c C-a (diff-apply-hunk).

The diff-mode buffers specify the outline-regexp, meaning that they can be used with the built-in outline-minor-mode to, for example, fold the invidual diff hunks and move between them (The prot-emacs-langs.el settings for outline-minor-mode). Personally, I combine this feature with my prot-search-outline command to quickly jump to an outline heading using minibuffer completion (The prot-emacs-search.el extras provided by the prot-search.el library).

Outside of Emacs, I have settings for git which produce more informative diff hunk headings in Elisp and Org buffers. I wrote about it here: https://protesilaos.com/codelog/2021-01-26-git-diff-hunk-elisp-org/. The configurations are part of my dotfiles (linked to at the opening section of this file).

;;;; `diff-mode'
(use-package diff-mode
  :ensure nil
  :defer t
  :config
  (setq diff-default-read-only t)
  (setq diff-advance-after-apply-hunk t)
  (setq diff-update-on-the-fly t)
  ;; The following are from Emacs 27.1
  (setq diff-refine nil) ; I do it on demand, with my `agitate' package (more below)
  (setq diff-font-lock-prettify t) ; I think nil is better for patches, but let me try this for a while
  (setq diff-font-lock-syntax 'hunk-also))

5.8.4. The prot-emacs-git.el section about vc.el and related

The concept of “version control” pertains to a system of versioning files, to track and visualise changes from record to record (The prot-emacs-git.el section about diff-mode). These version-controlled files may be part of a project (The prot-emacs-git.el section about project.el).

There are many programs that fall in the category of Version Control Software (VCS). I only use git, simply because it is ubiquitous though there are others which have technical merits as well.

VCSs have some common features, such as how they record a unit of history, and how they handle the synchronisation of their state across computers. Because of these commonalities, Emacs is able to provide a layer of abstraction, known as “Version Control”, else vc.el and its accoutrements.

[ Technically, the vc.el file is not the only one defining relevant functionality. There are VCS-specific variants, such as vc-git.el, as well as complementary features like vc-annotate.el. All these are hereinafter referred to as vc. ]

With vc, we can carry out all the common actions related to version control, such as to commit (to make a record of) changes and pull/push them from/to the remote (i.e. the server with which we sync our project). Whatever VCS we use, the workflow is the same:

  • Make changes to a file.
  • Type C-x v v (vc-next-action).
    • If the file is already under version control, vc will produce a “log edit” buffer to let you commit the changes.
    • If the file is not under version control, vc will use a minibuffer prompt to ask which VCS to use. These are also known as backends and are stored in the user option vc-handled-backends.
    • If the file is not under version control but is in a directory which itself is version controlled, then the file will be added to the list of tracked files.
  • Type C-x v v again and vc will proceed to the next action, which is to commit the changes to history. This is done in the new log-edit buffer.
  • By convention, the message of each commit is separated into a summary and the body of the message. An empty line divides them. The summary is the first line of the message and should, as a matter of best practices, be brief yet sufficiently descriptive. The rest is free form text. In the log-edit buffer, the empty separator line between the summary and the body is shown as a border, so there is no need to add another line there.
  • Once the message is ready, type C-c C-c (log-edit-done) to confirm it or C-c C-k (log-edit-kill-buffer) to cancel the operation.
  • From the log-edit buffer, it is possible to see the underlying changes in a diff buffer. Do it with C-c C-d (log-edit-show-diff).
  • The record of commits to the history of the entire project is accessed with the command vc-print-root-log, while that of individual files is handled by the command vc-print-log.
  • To pull from a remote, do vc-update. To push, invoke vc-push.
  • A Dired-like buffer is also available to perform these actions across many edited files. Check the commands, vc-dir, vc-dir-root, or even project-vc-dir.

Remember to use C-h m (describe-mode) to get help for the buffer you are in and to learn the relevant key bindings.

There is more functionality, though this should already give you an overview of what is on offer. The gist is that vc provides a fast and minimalist way to accomplish the basic tasks related to version control. For more demanding operations, there is either the command-line or the wonderful magit Emacs package (The prot-emacs-git.el section about magit (great Git client)).

Note that in the following code block I redefine lots of key bindings. They make more sense to me. Furthermore, my agitate package defines many extras on top of vc that I use daily in my workflow (The prot-emacs-git.el section about agitate).

  ;;; Version control framework (vc.el, vc-git.el, and more)
(use-package vc
  :ensure nil
  :bind
  (;; NOTE: I override lots of the defaults
   :map global-map
   ("C-x v B" . vc-annotate) ; Blame mnemonic
   ("C-x v e" . vc-ediff)
   ("C-x v k" . vc-delete-file) ; 'k' for kill==>delete is more common
   ("C-x v G" . vc-log-search)  ; git log --grep
   ("C-x v t" . vc-create-tag)
   ("C-x v c" . vc-clone) ; Emacs 31
   ("C-x v d" . vc-diff)
   ("C-x v ." . vc-dir-root) ; `vc-dir-root' is from Emacs 28
   ("C-x v <return>" . vc-dir-root)
   :map vc-dir-mode-map
   ("t" . vc-create-tag)
   ("O" . vc-log-outgoing)
   ("o" . vc-dir-find-file-other-window)
   ("d" . vc-diff)         ; parallel to D: `vc-root-diff'
   ("k" . vc-dir-delete-file)
   ("G" . vc-revert)
   :map vc-git-stash-shared-map
   ("a" . vc-git-stash-apply-at-point)
   ("c" . vc-git-stash) ; "create" named stash
   ("k" . vc-git-stash-delete-at-point) ; symmetry with `vc-dir-delete-file'
   ("p" . vc-git-stash-pop-at-point)
   ("s" . vc-git-stash-snapshot)
   :map vc-annotate-mode-map
   ("M-q" . vc-annotate-toggle-annotation-visibility)
   ("C-c C-c" . vc-annotate-goto-line)
   ("<return>" . vc-annotate-find-revision-at-line)
   :map log-edit-mode-map
   ("M-s" . nil) ; I use M-s for my search commands
   ("M-r" . nil) ; I use `consult-history'
   :map log-view-mode-map
   ("<tab>" . log-view-toggle-entry-display)
   ("<return>" . log-view-find-revision)
   ("s" . vc-log-search)
   ("o" . vc-log-outgoing)
   ("f" . vc-log-incoming)
   ("F" . vc-update)
   ("P" . vc-push))
  :init
  (setq vc-follow-symlinks t)
  :config
  ;; 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)

  ;; I only use Git.  If I ever need another, I will include it here.
  ;; This may have an effect on performance, as Emacs will not try to
  ;; check for a bunch of backends.
  (setq vc-handled-backends '(Git))

  ;; 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)
  ;; I can see the files from the Diff with C-c C-d
  (remove-hook 'log-edit-hook #'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-log-switches '("--stat"))
  (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 %ai %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.
          ,(concat "^\\(?:[*/\\|]+\\)\\(?:[*/\\| ]+\\)?"
                   "\\(?2: ([^)]+) \\)?\\(?1:[0-9a-z]+\\) "
                   "\\(?4:[0-9]\\{4\\}-[0-9-]\\{4\\}[0-9\s+:-]\\{16\\}\\) "
                   "\\(?3:.*?\\):")
          ((1 'log-view-message)
           (2 'change-log-list nil lax)
           (3 'change-log-name)
           (4 'change-log-date))))

  ;; These two are from Emacs 29
  (setq vc-git-log-edit-summary-target-len 50)
  (setq vc-git-log-edit-summary-max-len 70))

5.8.5. The prot-emacs-git.el section about agitate

This is an experimental package of mine whose role is to extend the functionality provided by the vc suite of tools (The prot-emacs-git.el section about vc.el and related), as well as that of diff-mode (The prot-emacs-git.el section about diff-mode). What agitate does is define several commands that are in the spirit of those built-in tools.

Because it is (i) an experimental package and (ii) vc is probably a niche feature, I will not document much here.

Agitate is a collection of commands or potentially useful functions that expand on the available version control features of Emacs. Those are meant to complement a workflow that relies on the built-in Version Control framework and its accoutrements (`diff-mode.el`, `log-view.el`, `log-edit.el`, `vc-git.el`, and potentially others).

;;; Agitate
;; A package of mine to complement VC and friends.  Read the manual
;; here: <https://protesilaos.com/emacs/agitate>.
(use-package agitate
  :ensure t
  :hook
  ((diff-mode . agitate-diff-enable-outline-minor-mode)
   (after-init . agitate-log-edit-informative-mode))
  :bind
  ( :map global-map
    ("C-x v =" . agitate-diff-buffer-or-file) ; replace `vc-diff'
    ("C-x v g" . agitate-vc-git-grep) ; replace `vc-annotate'
    ("C-x v f" . agitate-vc-git-find-revision)
    ("C-x v s" . agitate-vc-git-show)
    ("C-x v w" . agitate-vc-git-kill-commit-message)
    ("C-x v p p" . agitate-vc-git-format-patch-single)
    ("C-x v p n" . agitate-vc-git-format-patch-n-from-head)
    :map diff-mode-map
    ("C-c C-b" . agitate-diff-refine-cycle) ; replace `diff-refine-hunk'
    ("C-c C-n" . agitate-diff-narrow-dwim)
    ("L" . vc-print-root-log)
    ;; Emacs 29 can use C-x v v in diff buffers, which is great, but now I
    ;; need quick access to it...
    ("v" . vc-next-action)
    :map log-view-mode-map
    ("w" . agitate-log-view-kill-revision)
    ("W" . agitate-log-view-kill-revision-expanded)
    :map vc-git-log-view-mode-map
    ("c" . agitate-vc-git-format-patch-single)
    :map log-edit-mode-map
    ("C-c C-i C-n" . agitate-log-edit-insert-file-name)
    ;; See user options `agitate-log-edit-emoji-collection' and
    ;; `agitate-log-edit-conventional-commits-collection'.
    ("C-c C-i C-e" . agitate-log-edit-emoji-commit)
    ("C-c C-i C-c" . agitate-log-edit-conventional-commit))
  :config
  (advice-add #'vc-git-push :override #'agitate-vc-git-push-prompt-for-remote)

  (setq agitate-log-edit-informative-show-root-log nil
        agitate-log-edit-informative-show-files nil))

5.8.6. The prot-emacs-git.el section about magit (great Git client)

The magit package, maintained by Jonas Bernoulli, is the best front-end to git I have ever used. Not only is it excellent at getting the job done, it also helps you learn more about what git has to offer.

At the core of its interface is transient. This is a library that was originally developed as Magit-specific code that was then abstracted away and ultimately incorporated into Emacs version 29. With transient, we get a window pop up with keys and commands corresponding to them. The window is interactive, as the user can set a value or toggle an option and have it take effect when the relevant command is eventually invoked. For git, in particular, this interface is a genious way to surface the plethora of options.

To start, call the command magit-status. It brings up a buffer that shows information about the state of the repository. Sections include an overview of the current HEAD, untracked files, unstaged changes, staged changes, and recent commits. Each section’s visibility state can be cycled by pressing TAB (variations of this are available—remember to do C-h m (describe-mode) in an unfamiliar major mode to get information about its key bindings).

From the status buffer, we can perform all the usual version control operations. By typing ? (magit-dispatch), we bring up the main transient menu, with keys that then bring up their own submenus, such as for viewing commit logs, setting the remotes, switching branches, etc.

Before I used magit, I only knew the basics of adding files for a commit, writing a commit message inline with the -m flag on the command line, and pushing to the remote. Magit shows the staging area in the status buffer and makes “staging” a key part of the process of committing changes to history. To stage something, is to make it a candidate for the next commit action: only the staged parts are committed.

Magit has a refined understanding of context. We can target an individual line, a single diff hunk, a single file, or a range of files for staging or unstaging (among others). If the region is active, then only the selection is affected. If the cursor is on or somewhere inside a diff hunk, then that is targeted. If the cursor is over a file, then the file is the target. Same idea for the section heading, which then extends to everything under it.

This contextuality extends to every git command that accepts a commit hash as an argument. For example, if we are in a Magit commit log view and want to do a hard reset on the commit at point, Magit knows what commit hash to use (and presents it as an option when we choose where to reset to). Same principle for rebasing, cherry picking, and more.

Magit is good for newer users but also for those who have experience with git and the command-line in general. With it, I can easily maintain a project that needs to track separate remotes and push/pull between them in a fairly complicated manner. Partly supported by transient and partly by the Emacs completion user interface, we have all we need to select targets with ease (The prot-emacs-completion.el module).

The only downside of this wonderful package is that it is slow on Windows (based on what others have told me and showed me)… In those cases, a combination of vc and the command-line will suffice (The prot-emacs-git.el section about vc.el and related).

;;; Interactive and powerful git front-end (Magit)
(use-package transient
  :defer t
  :config
  (setq transient-show-popup 0.5))

(use-package magit
  :ensure t
  :bind ("C-c g" . magit-status)
  :init
  (setq magit-define-global-key-bindings nil)
  (setq magit-section-visibility-indicator '("⮧"))
  :config
  (setq git-commit-summary-max-length 50)
  ;; NOTE 2023-01-24: I used to also include `overlong-summary-line'
  ;; in this list, but I realised I do not need it.  My summaries are
  ;; always in check.  When I exceed the limit, it is for a good
  ;; reason.
  (setq git-commit-style-convention-checks '(non-empty-second-line))

  (setq magit-diff-refine-hunk t))

(use-package magit-repos
  :ensure nil ; part of `magit'
  :commands (magit-list-repositories)
  :init
  (setq magit-repository-directories
        '(("~/Git/Projects" . 1))))

5.8.7. The prot-emacs-git.el call to provide

Finally, we provide the module. This is the mirror function of require (The init.el final part to load the individual modules).

(provide 'prot-emacs-git)

5.9. The prot-emacs-org.el module

Watch:

At its core, Org is a plain text markup language. By “markup language”, we refer to the use of common characters to apply styling, such as how a word wrapped in asterisks acquires strong emphasis. Check the video I link to above on the basics of Org mode.

Though what makes Org powerful is not the markup per se, but the fact that it has a rich corpus of Emacs Lisp code that does a lot with this otherwise plain text notation. Some of the headline features:

  • Cycle the visibility of any heading and its subheadings. This lets you quickly fold a section you do not need to see (or reveal the one you care about).
  • Mix prose with code in a single document to either make the whole thing an actual program or to evaluate/demonstrate some snippets.
  • Convert (“export”) an Org file to a variety of formats, including HTML and PDF.
  • Use LaTeX inside of Org files to produce a scientific paper without all the markup of LaTeX.
  • Manage TODO lists and implement a concomitant methodology of labelling task states.
  • Quickly shift a “thing” (heading, list item, paragraph, …) further up or down in the file.
  • Use tables with formulas as a lightweight alternative to spreadsheet software.
  • Capture data or fleeting thoughts efficiently using templates.
  • Maintain an agenda for all your date-bound activities.
  • Clock in and out of tasks, to eventually track how you are spending your time.
  • Link to files regardless of file type. This includes special links such as to an Info manual or an email, if you also have that running locally and integrated with Emacs (The prot-emacs-email.el module).

In other words, Org is highly capable and widely considered one of the killer apps of Emacs.

This section covers the relevant configurations. You will notice that it is not limited to Org, as some other built-in features are also relevant here.

5.9.1. The prot-emacs-org.el section on the calendar

The calendar is technically independent of Org, though it tightly integrates with it. We witness this when we are setting timestamps, such as while setting a SCHEDULED or DEADLINE entry for a given heading. All I do here is set some stylistic preferences.

Note that Emacs also has a diary command. I used it for a while, but Org is far more capable, so I switched to it completely.

;;; Calendar
(use-package calendar
  :ensure nil
  :commands (calendar)
  :config
  (setq calendar-mark-diary-entries-flag nil)
  (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-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"))

5.9.2. The prot-emacs-org.el section about appointment reminders (appt.el)

The built in appt.el defines functionality for handling notifications about appointments. It is originally designed to work with the generic diary feature (the M-x diary one, I mean), which I do not use anymore, but also integrates nicely with the Org agenda (The prot-emacs-org.el Org agenda settings). I deepen this integration further, such that after adding a task or changing its state, the appointments mechanism re-reads my data to register new notifications. This is done via a series of hooks and with the use of the advice feature of Emacs Lisp.

Here I am setting some simple settings to keep appointment notifations minimal. I do not need them to inform me about the contents of my next entry on the agenda: just show text on the mode line telling me how many minutes are left until the event.

In Org files, every heading can have an APPT_WARNTIME property: it takes a numeric value representing minutes for a forewarning from appt.el. I use this in tandem with org-capture for tasks that need to be done at a specific time, such as coaching sessions (The prot-emacs-org.el Org capture templates (org-capture)).

;;; Appt (appointment reminders which also integrate with Org agenda)
(use-package appt
  :ensure nil
  :commands (appt-activate)
  :config
  (setq appt-display-diary nil
        appt-display-format nil
        appt-display-mode-line t
        appt-display-interval 3
        appt-audible nil ; TODO 2023-01-25: t does nothing because I disable `ring-bell-function'?
        appt-warning-time-regexp "appt \\([0-9]+\\)" ; This is for the diary
        appt-message-warning-time 6)

  (with-eval-after-load 'org-agenda
    (appt-activate 1)

    ;; NOTE 2021-12-07: In my `prot-org.el' (see further below), I add
    ;; `org-agenda-to-appt' to various relevant hooks.
    ;;
    ;; Create reminders for tasks with a due date when this file is read.
    (org-agenda-to-appt)))

5.9.3. The prot-emacs-org.el section with basic Org settings

Org, also known as “Org mode”, is one of the potentially most useful feature sets available to every Emacs user. At its core, Org is a lightweight markup language: you can have headings and paragraphs, mark a portion of text with emphasis, produce bullet lists, include code blocks, and the like. Though what really sets Org apart from other markup languages is the rich corpus of Emacs Lisp written around it to do all sorts of tasks with this otherwise plain text format.

With Org you can write technical documents (e.g. the manuals of all my Emacs packages), maintain a simple or highly sophisticated system for task management, organise your life using the agenda, write tables that can evaluate formulas to have spreadsheet functionality, have embedded LaTeX, evaluate code blocks in a wide range of programming languages and reuse their results for literate programming, include the contents of other files into a singular file, use one file to generate other files/directories with all their contents, and export the Org document to a variety of formats like .pdf and .odt. Furthermore, Org can be used as a lightweight, plain text database, as each heading can have its own metadata. This has practical applications in most of the aforementioned.

In short, if something can be done with plain text, Org probably does it already or has all the elements for piecing it together. This document, among many of my published works, is testament to Org’s sheer power, which I explained at greater length in a video demonstration: Advanced literate configuration with Org (2023-12-18).

This being Emacs, everything is customisable and Org is a good example of this. There are a lot of user options for us to tweak things to our liking. I do as much, though know that Org is perfectly usable without any configuration. The following sections contain further commentary on how I use Org.

;;; Org-mode (personal information manager)
(use-package org
  :ensure nil
  :init
  (setq org-directory (expand-file-name "~/Documents/org/"))
  (setq org-imenu-depth 7)

  (add-to-list 'safe-local-variable-values '(org-hide-leading-stars . t))
  (add-to-list 'safe-local-variable-values '(org-hide-macro-markers . t))
  :bind
  ( :map global-map
    ("C-c l" . org-store-link)
    ("C-c o" . org-open-at-point-global)
    :map org-mode-map
    ;; I don't like that Org binds one zillion keys, so if I want one
    ;; for something more important, I disable it from here.
    ("C-'" . nil)
    ("C-," . nil)
    ("M-;" . nil)
    ("<C-return>" . nil)
    ("<C-S-return>" . nil)
    ("C-M-S-<right>" . nil)
    ("C-M-S-<left>" . nil)
    ("C-c ;" . nil)
    ("C-c M-l" . org-insert-last-stored-link)
    ("C-c C-M-l" . org-toggle-link-display)
    ("M-." . org-edit-special) ; alias for C-c ' (mnenomic is global M-. that goes to source)
    :map org-src-mode-map
    ("M-," . org-edit-src-exit) ; see M-. above
    :map narrow-map
    ("b" . org-narrow-to-block)
    ("e" . org-narrow-to-element)
    ("s" . org-narrow-to-subtree)
    :map ctl-x-x-map
    ("i" . prot-org-id-headlines)
    ("h" . prot-org-ox-html))
  :config
  ;; My custom extras, which I use for the agenda and a few other Org features.
  (require 'prot-org)

;;;; general settings
  (setq org-ellipsis "⮧")
  (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 nil)
  (setq org-hide-macro-markers nil)
  (setq org-hide-leading-stars nil)
  (setq org-cycle-separator-lines 0)
  (setq org-structure-template-alist
        '(("s" . "src")
          ("e" . "src emacs-lisp")
          ("E" . "src emacs-lisp :results value code :lexical t")
          ("t" . "src emacs-lisp :tangle FILENAME")
          ("T" . "src emacs-lisp :tangle FILENAME :mkdirp yes")
          ("x" . "example")
          ("X" . "export")
          ("q" . "quote")))
  (setq org-fold-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)
  (setq org-read-date-prefer-future 'time)
  (setq org-highlight-latex-and-related nil) ; other options affect elisp regexp in src blocks
  (setq org-fontify-quote-and-verse-blocks t)
  (setq org-fontify-whole-block-delimiter-line t)
  (setq org-track-ordered-property-with-tag t)
  (setq org-highest-priority ?A)
  (setq org-lowest-priority ?C)
  (setq org-default-priority ?A)
  (setq org-priority-faces nil)

  ;; See my `pulsar' package, defined elsewhere in this setup.
  (with-eval-after-load 'pulsar
    (dolist (hook '(org-agenda-after-show-hook org-follow-link-hook))
      (add-hook hook #'pulsar-recenter-center)
      (add-hook hook #'pulsar-reveal-entry))))

5.9.4. The prot-emacs-org.el Org to-do and refile settings

One of the many use-cases for Org is to maintain a plain text to-do list. A heading that starts with a to-do keyword, such as TODO is treated as a task and its state is considered not completed.

We can switch between the task states with shift and the left or right arrow keys. Or we can select a keyword directly with C-c C-t, which calls org-todo by default. I personally prefer the latter approach, as it is more precise.

Whenever a task state changes, we can log that event in a special LOGBOOK drawer. This is automatically placed right below the heading, before any paragraph text. Logging data is an opt-in feature, which I consider helpful (The prot-emacs-org.el Org time/state logging).

Tasks can be associated with timestamps, typically a scheduled date+time or a deadline+time. This can be helpful when we are reviewing the source Org file, though it really shines in tandem with the agenda. Any heading that has a timestamp and which belongs to a file in the org-agenda-files will show up on the agenda in the given date (The prot-emacs-org.el Org agenda settings).

By default, the org-todo-keywords are TODO and DONE. We can write more keywords if we wish to implement a descriptive workflow. For example, we can have a WAIT keyword for something that is to be done but is not actionable yet. While the number of keywords is not limited, the binary model is the same: we have words that represent the incomplete state and those that count as the completion of the task. For instance, both CANCEL and DONE mean that the task is not actionable anymore and we move on to other things. As such, the extra keywords are a way for the user to make tasks more descriptive and easy to find. In the value of the org-todo-keywords, we use the bar character to separate the incomplete state to the left from the completed one to the right.

One of the agenda’s headiline features is the ability to produce a view that lists headings with the given keyword. So having the right terms can make search and retrieval of data more easy. On the flip-side, too many keywords add cognitive load and require more explicit search terms to yield the desired results. I used to work with a more descriptive set of keywords, but ultimately decided to keep things simple.

The refile mechanism is how we can reparent a heading, by moving it from one place to another. We do this with the command org-refile, bound to C-c C-w by default. A common workflow where refiling is essential is to have an “inbox” file or heading, where unprocessed information is stored at, and periodically process its contents to move the data where it belongs. Though it can also work fine without any such inbox, in those cases where a heading should be stored someplace else. The org-refile-targets specifies the files that are available when we try to refile the current heading. With how I set it up, all the agenda files plus the current file’s headings up to level 2 are included as possible targets.

In terms of workflow, I have not done a refile in a very long time, because my entries always stay in the same place as I had envisaged at the capture phase (The prot-emacs-org.el Org capture templates (org-capture)).

;;;; refile, todo
(use-package org
  :ensure nil
  :config
  (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)
  ;; ;; NOTE 2023-04-07: Leaving this here for demo purposes.
  ;; (setq org-todo-keywords
  ;;       '((sequence "TODO(t)" "MAYBE(m)" "WAIT(w@/!)" "|" "CANCEL(c@)" "DONE(d!)")
  ;;         (sequence "COACH(k)" "|" "COACHED(K!)")))
  (setq org-todo-keywords
        '((sequence "TODO(t)" "|" "CANCEL(c@)" "DONE(d!)")
          (sequence "COACH(k)" "|" "COACHED(K!)")))

  (defface prot/org-bold-done
    '((t :inherit (bold org-done)))
    "Face for bold DONE-type Org keywords.")

  (setq org-todo-keyword-faces
        '(("CANCEL" . prot/org-bold-done)))
  (setq org-use-fast-todo-selection 'expert)

  (setq org-fontify-done-headline nil)
  (setq org-fontify-todo-headline nil)
  (setq org-fontify-whole-heading-line nil)
  (setq org-enforce-todo-dependencies t)
  (setq org-enforce-todo-checkbox-dependencies t))

5.9.5. The prot-emacs-org.el Org heading tags

Each Org heading can have one or more tags associated with it, while all headings inherit any potential #+FILETAGS. We can add tags to a heading when the cursor is over it by typing the ever flexible C-c C-c. Though the more specific org-set-tags-command also gets the job done, plus it does not require that the cursor is positioned on the heading text.

Tagging is useful for searching and retrieving the data we store. The Org agenda, in particular, provides commands to filter tasks by tag:

The user option org-tag-alist lets us specify tags we always want to use, though we can write tags per file as well by using the #+TAGS keyword. I do the latter as a global list of tags is not useful in my case. For example, when I wan checking my coach.org file for the coaching sessions I provide, I do not need to see any of the tags that make sense in my general tasks.org.

Note that in the settings below I disable the auto-alignment that Org does where it shifts tags to the right of the heading. I do not like it.

;;;; tags
(use-package org
  :ensure nil
  :config
  (setq org-tag-alist nil)
  (setq org-auto-align-tags nil)
  (setq org-tags-column 0))

5.9.6. The prot-emacs-org.el Org time/state logging

Org can keep a record of state changes, such as when we set an entry marked with the TODO keyword as DONE or when we reschedule an appointment (The prot-emacs-org.el Org agenda settings). This data is stored in a LOGBOOK drawer right below the heading. I choose to keep track of this information, as it is sometimes useful to capture mistakes or figure out intent in the absence of further clarification (though I do tend to write why something happened).

;;;; log
(use-package org
  :ensure nil
  :config
  (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))

5.9.7. The prot-emacs-org.el Org link settings

One of the nice things about Org is its flexible linking mechanism. It can produce links to a variety of file types or buffers and even navigate to a section therein.

At its simplest form, we have the file link type, which points to a file system path, with an optional extension for a match inside the file, as documented in the manual. Evaluate this inside of Emacs:

(info "(org) Search Options")

Links to buffers are also common and valuable. For example, we can have a link to a page produced by the man command, which gives us quick access to the documentation of some program. When Org follows that link, it opens the buffer in the appropriate major mode. For me, the most common scenario is a link to an email, which I typically associate with a task that shows up in my agenda:

Org supports lots of link types out-of-the-box, though more can be added by packages. My Denote does this: it defines a denote link type which behaves the same way as the file type except that it uses the identifier of the file instead of its full path (so eve if the file is renamed, the link will work for as long as the identifier remains the same).

Links can be generated automatically as part of an org-capture template. The command org-store-link produces one manually, storing it to a special data structure from which it can be retrieved later for insertion with the command org-insert-link. The latter command can also create new links, simply by receiving data that is different from what was already stored.

I bind org-store-link in main section of the Org configuration: The prot-emacs-org.el section with basic Org settings.

;;;; links
(use-package org
  :ensure nil
  :config
  (require 'prot-org) ; for the above commands

  (setq org-link-context-for-files t)
  (setq org-link-keep-stored-after-insertion nil)
  (setq org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id))

5.9.8. The prot-emacs-org.el Org code block settings

This document benefits from Org’s ability to combine prose with code, by placing the latter inside of a block that is delimited by #+BEGIN_SRC and #+END_SRC lines.

Code blocks can use the syntax highlighting (“fontification” in Emacs parlance) of a given major mode. They can also have optional parameters passed to their header, which expand the capabilities of the block. For instance, the following code block with my actual configuration uses the fontification of the emacs-lisp-mode and has a :tangle parameter with a value of a file system path. When I invoke the command org-babel-tangle, the contents of this block will be added to that file, creating the file if necessary.

More generally, Org is capable of evaluating code blocks and passing their return value to other code blocks. It is thus possible to write a fully fledged program as an Org document. This paradigm is known as “literate programming”. In the case of an Emacs configuration, such as mine, it is called a “literate configuration” or variants thereof. I did a video about my setup: Advanced literate configuration with Org (2023-12-18).

Org can evaluate code blocks in many languages. This is known as “Org Babel” and the files which implement support for a given language are typically named ob-LANG.el where LANG is the name of the language. We can load the requisite code for the languages we care about with something like the following:

(require 'ob-python)

;; OR

(use-package ob-python)

;; OR for more control

(use-package ob-python
  :after org
  :config
  ;; Settings here
  )

I seldom need to work with Org Babel, so I do not load any language automatically. Note that Emacs Lisp is loaded by default.

To evaluate a code block, we type Org’s omnipotent C-c C-c. The results will be produced below the code block. There is an optional parameter that controls how—or even if—the results are displayed.

There are many other types of block apart from SRC. Those do different things, such as:

#+BEGIN_QUOTE
Treat the contents as a block quote or equivalent.
#+BEGIN_VERSE
Do not reflow any like breaks (for poetry and such).
#+BEGIN_EXPORT
Evaluate the code for the given export target (like html or latex), optionally replacing it with its results or keeping both of them (The prot-emacs-org.el Org export settings).

This is a wonderful world of possibilities!

;;;; code blocks
(use-package org
  :ensure nil
  :config
  (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))

5.9.9. The prot-emacs-org.el Org export settings

Org is a capable authoring tool in no small part because it can be converted to other file formats. A typical example is to write a technical document in Org and then export it to a PDF. Another use-case is what I commonly do with the Emacs packages I maintain, which I export to an Info manual (texinfo format) and an HTML web page.

The default set of export targets is specified in the value of the user option org-export-backends. It is one of those rare cases where it has to be evaluated before the package is loaded. Other than that, we can load an export backend by finding the correspond ox-FORMAT.el file and either require it or load it with use-package, like what I showed for Org Babel (The prot-emacs-org.el Org code block settings).

;;;; export
(use-package org
  :ensure nil
  :init
  ;; NOTE 2023-05-20: Must be evaluated before Org is loaded,
  ;; otherwise we have to use the Custom UI.  No thanks!
  (setq org-export-backends '(html texinfo md))
  :config
  (setq org-export-with-toc t)
  (setq org-export-headline-levels 8)
  (setq org-export-dispatch-use-expert-ui nil)
  (setq org-html-htmlize-output-type nil)
  (setq org-html-head-include-default-style nil)
  (setq org-html-head-include-scripts nil))

5.9.10. The prot-emacs-org.el Org capture templates (org-capture)

The org-capture command allows us to quickly store data in some structured way. This is done with the help of a templating system where we can, for example, record the date the entry was recorded, prompt for user input, automatically use the email’s subject as the title of the task, and the like. The documentation string of org-capture-templates covers the technicalities.

I use two Org files for my tasks. The one is tasks.org, which contains the bulk of my entries. The other is coach.org, which is specific to my coaching work: https://protesilaos.com/coach.

The tasks.org consists of several top-level headings. Each contains subheadings I need to review. You will notice how most of my entries in org-capture-templates involve this file. With Org, it is perfectly fine to work in a single file because we can fold headings or narrow to them with org-narrow-to-subtree. Furthermore, we can navigate directly to a heading using minibuffer completion, such as with the general purpose command prot-search-outline (The prot-emacs-search.el extras provided by the prot-search.el library).

Despite the fact that Org copes well with large files, I still choose to keep my coaching work in a separate file as a contingency plan. Because coach.org includes information about appointments, I need to be able to read it with ease from anywhere. This includes different types of hardware, but also any kind of generic text editor or terminal pager. I do not want to depend on features like folding, narrowing, and the like, in times when something has gone awry. Granted, this has never happened, though the idea makes sense. Besides, two files are not hard to manage in this case. The coach.org has a simple structure: each appointment is stored as a top-level heading.

As for my workflow, here is an overview:

  • When I want to capture data that I am not yet sure about, I add it to the tasks.org “Unprocessed” heading. I periodically review those to decide if I want to do something with them or not. If I do not want them, I delete them. Otherwise, I file them under another heading in the same file using the org-refile command (The prot-emacs-org.el Org to-do and refile settings). Not everything goes into the “Unprocessed” headings, as I often known in advance what an item is about. This is just a fallback for those cases when I need more information to decide on the appropriate action.
  • Tasks that have an inherent time component are given a SCHEDULED or DEADLINE timestamp (set those on demand with the commands org-schedule and org-deadline, respectively). These are the only tasks I want to see on my daily agenda (The prot-emacs-org.el Org agenda settings). The difference between SCHEDULED and DEADLINE is that the former has no strict start or end time and so is flexible, while the latter is more rigid. For example, “visit the vet today” does not have a strict time associated with it because the doctor often deals with emergency situations and thus their agenda is fluid. While a coaching session of mine like “work on Emacs with PERSON” has to start at the agreed upon time.
  • I do not arbitrarily assign timestamps to tasks. If something does not have a scheduled date or a deadline, then it does not belong on the agenda. Otherwise, those arbitrarily defined “events” accumulate in the agenda and crowd out the actual time-sensitive tasks. As a result, the cognitive load is heavier and things will not be done. So when I want to do something at some point, but have no specific plan for it, I add is to the tasks.org “Wishlist”. When I have free time, I review my wishlist and pick something to work on from there depending on my available time and mood. This keeps my workflow both focused and stress-free.
  • Finally, my coach.org only has time-sensitive appointments with a DEADLINE associated with them. I organise the rest of my activities in the given day based on those.
;;;; capture
(use-package org-capture
  :ensure nil
  :bind ("C-c c" . org-capture)
  :config
  (require 'prot-org)

  (setq org-capture-templates
        `(("u" "Unprocessed" entry
           (file+headline "tasks.org" "Unprocessed")
           ,(concat "* %^{Title}\n"
                    ":PROPERTIES:\n"
                    ":CAPTURED: %U\n"
                    ":CUSTOM_ID: h:%(format-time-string \"%Y%m%dT%H%M%S\")\n"
                    ":END:\n\n"
                    "%a\n%i%?")
           :empty-lines-after 1)
          ("w" "Wishlist" entry
           (file+olp "tasks.org" "All tasks" "Wishlist")
           ,(concat "* %^{Title} %^g\n"
                    ":PROPERTIES:\n"
                    ":CAPTURED: %U\n"
                    ":CUSTOM_ID: h:%(format-time-string \"%Y%m%dT%H%M%S\")\n"
                    ":END:\n\n"
                    "%a\n%?")
           :empty-lines-after 1)
          ("t" "Task to do" entry
           (file+headline "tasks.org" "All tasks")
           ,(concat "* TODO %^{Title} %^g\n"
                    ":PROPERTIES:\n"
                    ":CAPTURED: %U\n"
                    ":CUSTOM_ID: h:%(format-time-string \"%Y%m%dT%H%M%S\")\n"
                    ":END:\n\n"
                    "%a\n%?")
           :empty-lines-after 1)
          ("s" "Select file and heading to add to" entry
           (function prot-org-capture-select-project)
           ,(concat "* TODO %^{Title}%?\n"
                    ":PROPERTIES:\n"
                    ":CAPTURED: %U\n"
                    ":CUSTOM_ID: h:%(format-time-string \"%Y%m%dT%H%M%S\")\n"
                    ":END:\n\n")
           :empty-lines-after 1)

          ;; NOTE 2024-11-24: I am not using this, but am keeping it
          ;; here because the approach is good.
          
          ;; ("c" "Clock in and do immediately" entry
          ;;  (file+headline "tasks.org" "Clocked tasks")
          ;;  ,(concat "* TODO %^{Title}\n"
          ;;           ":PROPERTIES:\n"
          ;;           ":EFFORT: %^{Effort estimate in minutes|5|10|15|30|45|60|90|120}\n"
          ;;           ":END:\n\n"
          ;;           "%a\n")
          ;;  :prepend t
          ;;  :clock-in t
          ;;  :clock-keep t
          ;;  :immediate-finish t
          ;;  :empty-lines-after 1)
          ("p" "Private lesson or service" entry
           (file "coach.org")
           #'prot-org-capture-coach
           :prepend t
           :empty-lines 1)
          ("P" "Private service clocked" entry
           (file+headline "coach.org" "Clocked services")
           #'prot-org-capture-coach-clock
           :prepend t
           :clock-in t
           :clock-keep t
           :immediate-finish t
           :empty-lines 1)))

  ;; NOTE 2024-11-10: I realised that I was not using this enough, so
  ;; I decided to simplify my setup.  Keeping it here, in case I need
  ;; it again.

  ;; (setq org-capture-templates-contexts
  ;;       '(("e" ((in-mode . "notmuch-search-mode")
  ;;               (in-mode . "notmuch-show-mode")
  ;;               (in-mode . "notmuch-tree-mode")))))
  )

5.9.11. The prot-emacs-org.el Org agenda settings

[ Watch: Demo of my custom Org block agenda (2021-12-09). It has changed a bit since then, but the idea is the same. ]

With the Org agenda, we can visualise the tasks we have collected in our Org files or, more specifically, in the list of files specified in the user option org-agenda-files. In my workflow, only the files in the org-directory can feed data into the agenda. Though Org provides commands to add/remove the current file on demand: org-remove-file, and org-agenda-file-to-front. If I ever need to write a task that is specific to a certain file or buffer, then I use Org’s linking mechanism to point to the relevant context, but otherwise store my task in the usual place (The prot-emacs-org.el Org capture templates (org-capture)).

By default, Org provides many so-called “views” for the agenda. One of the them is the daily/weekly agenda. Others show only the headings with TODO keywords, or some other kind of search criteria. I personally never use those views. I have my own custom agenda view, which consolidates in a single buffer the following blocks on data, in this order (The prot-org.el library).:

Important tasks without a date
When I add a top priority to something, but there is no inherent deadline to it.
Pending scheduled tasks
Tasks with a SCHEDULED date may sometimes not be done when they ought to. So they need to be closer to the top for me to do them as soon as I can.
Today’s agenda
What I am actually working on. Because I only assign a timestamp to tasks that are indeed time-sensitive, this always reflects the commitments I have for the day.
Next three days
Like the above, but for the near future.
Upcoming deadlines (+14d)
These are the deadlines I need to be aware of for the 14 days after the next three days I am only informed about.

The Org agenda has lots of other extras, such as to filter the view. Though I never use them. My custom agenda does exactly what I need from it and thus keeps me focused.

;;;; agenda
(use-package org-agenda
  :ensure nil
  :bind
  ;; I bind `org-agenda' to C-c A, so this one puts me straight into my
  ;; custom block agenda.
  ( :map global-map
    ("C-c A" . org-agenda)
    ("C-c a" . (lambda ()
                 "Call Org agenda with `prot-org-custom-daily-agenda' configuration."
                 (interactive)
                 (org-agenda nil "A"))))
  :config
;;;;; Custom agenda blocks

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

  ;; Check the variable `prot-org-custom-daily-agenda' in prot-org.el
  (setq org-agenda-custom-commands
        `(("A" "Daily agenda and top priority tasks"
           ,prot-org-custom-daily-agenda
           ((org-agenda-fontify-priorities nil)
            (org-agenda-prefix-format "	 %t %s")
            (org-agenda-dim-blocked-tasks nil)))
          ("P" "Plain text daily agenda and top priorities"
           ,prot-org-custom-daily-agenda
           ((org-agenda-with-colors nil)
            (org-agenda-prefix-format "%t %s")
            (org-agenda-current-time-string ,(car (last org-agenda-time-grid)))
            (org-agenda-fontify-priorities nil)
            (org-agenda-remove-tags t))
           ("agenda.txt"))))

;;;;; Basic agenda setup
  (setq org-default-notes-file (make-temp-file "emacs-org-notes-")) ; send it to oblivion
  (setq org-agenda-files `(,org-directory))
  (setq org-agenda-span 'week)
  (setq org-agenda-start-on-weekday 1)  ; Monday
  (setq org-agenda-confirm-kill t)
  (setq org-agenda-show-all-dates t)
  (setq org-agenda-show-outline-path nil)
  (setq org-agenda-window-setup 'current-window)
  (setq org-agenda-skip-comment-trees t)
  (setq org-agenda-menu-show-matcher t)
  (setq org-agenda-menu-two-columns nil)
  (setq org-agenda-sticky nil)
  (setq org-agenda-custom-commands-contexts nil)
  (setq org-agenda-max-entries nil)
  (setq org-agenda-max-todos nil)
  (setq org-agenda-max-tags nil)
  (setq org-agenda-max-effort nil)

;;;;; General agenda view options
  ;; NOTE 2021-12-07: Check further below my `org-agenda-custom-commands'
  (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 nil)
  ;; I do not want the diary, but there is no way to disable it
  ;; altogether.  This creates a diary file in the /tmp directory.
  (setq diary-file (make-temp-file "emacs-diary-"))
  (setq org-agenda-diary-file 'diary-file) ; TODO 2023-05-20: review Org diary substitute

;;;;; 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 0)
  (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 nil)
  (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 70 ?.)))
  (setq org-agenda-time-grid
        '((daily today require-timed)
          ( 0500 0600 0700 0800 0900 1000
            1100 1200 1300 1400 1500 1600
            1700 1800 1900 2000 2100 2200)
          "" ""))
  (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)

  ;; ;;;;; Agenda habits
  ;;   (require 'org-habit)
  ;;   (setq org-habit-graph-column 50)
  ;;   (setq org-habit-preceding-days 9)
  ;;   ;; Always show the habit graph, even if there are no habits for
  ;;   ;; today.
  ;;   (setq org-habit-show-all-today t)
  )
(use-package prot-coach
  :ensure nil
  :commands (prot-coach-done-sessions-with-person))

5.9.12. The prot-emacs-org.el call to provide

Finally, we provide the module. This is the mirror function of require (The init.el final part to load the individual modules).

(provide 'prot-emacs-org)

5.10. The prot-emacs-langs.el module

This module encompasses a wide range of packages and built-in configurations that relate to “language” in the wider sense. Settings here include basic patterns of interaction, such as what the TAB key does, to spell checking, code linting, and writing.

5.10.1. The prot-emacs-langs.el settings for TAB

When I first switched to Emacs, I found the behaviour of the TAB key confusing. I was used to it inserting tabs or, at least, spaces. Whereas in Emacs, TAB tries to be smarter, depending on the major mode, as it tries to indent the current line to where it should be given the context. This works best when we mark a region of text and hit TAB there.

If we need to forcefully indent, we can use C-x C-i (indent-rigidly). This command allows us to shift the region left or right using the arrow keys. A common use-case for me is to paste some text I want to indent, and then do C-u C-x C-i, which indents by four spaces the implicit region. I have explained this in my video about mark and register basics: https://protesilaos.com/codelog/2023-06-28-emacs-mark-register-basics/.

To the settings I have here, the tab-always-indent makes the TAB key assume the dual role of indenting text as well as triggering completion at point (The prot-emacs-completion.el for in-buffer completion popup (corfu)). When it can perform indentation, it does that, otherwise it starts a completion loop. The tab-first-completion determines when not to complete. Read its doc string for the technicalities.

Finally, the tab-width and indent-tabs-mode are about the use of tabs. I never want them, as I only use spaces. Notice the setq-default here: we need this form when the buffer’s value automatically becomes buffer-local. This is very important, otherwise we will be setting the value only in the Emacs init file, which contradicts our intent. There are a few cases where setq-default is needed, so keep this in mind.

;;;; Tabs, indentation, and the TAB key
(use-package emacs
  :ensure nil
  :demand t
  :config
  (setq tab-always-indent 'complete)
  (setq tab-first-completion 'word-or-paren-or-punct) ; Emacs 27
  (setq-default tab-width 4
                indent-tabs-mode nil))

5.10.2. The prot-emacs-langs.el settings for “electric” behaviour

Emacs describes as “electric” any behaviour that tries to be smart about how to handle a given action. The electric-pair-mode, for example, automatically inserts a closing parenthesis when the user inputs an opening parenthesis. Same idea with quotes, performed by the electric-quote-mode. While the electric-indent-mode tries to be smart about how to indent a line, which is fine for programming purposes, it makes a mess of things in Org and related because you have to delete back to the beginning of a line if you want to “escape” from the indentation of a list or something.

If I need to manually wrap the symbol at point or a region in a pair of characters, I use my prot-pair-insert command (The prot-emacs-essentials.el section about prot-pair.el (insert character pairs)).

;;;; Disable "electric" behaviour
(use-package electric
  :ensure nil
  :hook
  (prog-mode . electric-indent-local-mode)
  :config
  ;; I don't like auto indents in Org and related.  They are okay for
  ;; programming.
  (electric-pair-mode -1)
  (electric-quote-mode -1)
  (electric-indent-mode -1))

5.10.3. The prot-emacs-langs.el settings show-paren-mode

The built-in show-paren-mode highlights the parenthesis on the opposite end of the current symbolic expression. It also highlights matching terms of control flow in programming languages that are not using parentheses like Lisp: for instance, in a bash shell script it highlights the if and fi keywords. This mode also works for prose and I use it globally. Simple and effective!

;;;; Parentheses (show-paren-mode)
(use-package paren
  :ensure nil
  :hook (prog-mode . show-paren-local-mode)
  :config
  (setq show-paren-style 'parenthesis)
  (setq show-paren-when-point-in-periphery nil)
  (setq show-paren-when-point-inside-paren nil)
  (setq show-paren-context-when-offscreen 'overlay)) ; Emacs 29

5.10.4. The prot-emacs-langs.el settings for plain text (no double spaces, auto-fill-mode)

These are some basic settings for plain text files but also for any major mode that inherits from text-mode (like Org and Markdown). For a long time, I was using double spaces after a sentence, as this is the Emacs default. I don’t have a strong preference for or against it, though I reverted to the single space convention as it is the norm nowadays.

The technical benefit of double spaces is that it makes sentence navigation less ambiguous as you do not get false positives like “Dr.”. Though I realised I seldom use such language so why type more spaces for a theoretical advantage?

I still need to use double spaces for Elisp programming, otherwise the byte compiler produces warnings. It is annoyingly pedantic, but here we are…

The other setting that matters here is the use of “auto fill”, else the auto-fill-mode. This is about the automatic break of long lines so that they wrap at the fill-column length: it happens as you type. This way, a paragraph is not a single long line, but several shorter lines with newline characters between them. I find this much more pleasant to work with than to have to rely on visual-line-mode to visually wrap long lines. I want my text to be readable even if I do not use Emacs (e.g. if I use cat or less on the command-line). Auto-filled text does not affect published prose, as the relevant programs strip away the newlines inside a paragraph.

To manually fill a region of text, mark it and type M-q. Or do M-q to operate on the current paragraph without marking it. Depending on the major mode you are in, this key binding calls a different command. The generic one is fill-paragraph. I use M-Q to “unfill” text, which corresponds to the prot-simple-unfill-region-or-paragraph command (The prot-emacs-essentials.el section about prot-simple.el (custom basic commands)).

Finally, notice how I am adding an association to the auto-mode-alist. The file names specified in that regular expression will be using text-mode when I visit them.

;;;; Plain text (text-mode)
(use-package text-mode
  :ensure nil
  :mode "\\`\\(README\\|CHANGELOG\\|COPYING\\|LICENSE\\)\\'"
  :hook
  ((text-mode . turn-on-auto-fill)
   (prog-mode . (lambda () (setq-local sentence-end-double-space t))))
  :config
  (setq sentence-end-double-space nil)
  (setq sentence-end-without-period nil)
  (setq colon-double-space nil)
  (setq use-hard-newlines nil)
  (setq adaptive-fill-mode t))

5.10.5. The prot-emacs-langs.el settings for common file types

As I explained above about auto-mode-alist (The prot-emacs-langs.el settings for plain text), this is how we tell Emacs what major mode to use for files that match the given regular expression. The PKGBUILD is for Arch Linux package recipes, by the way.

;;;; Arch Linux and AUR package scripts (sh-mode)
(use-package sh-script
  :ensure nil
  :mode ("PKGBUILD" . sh-mode))

;;;; SystemD and other configuration files (conf-mode)
(use-package conf-mode
  :ensure nil
  :mode ("\\`dircolors\\'" "\\.\\(service\\|timer\\)\\'"))

5.10.6. The prot-emacs-langs.el settings for eldoc

The built-in eldoc feature is especially useful in programming modes. While we are in a function call, it produces an indicator in the echo area (where the minibuffer appears upon invocation) that shows the name of the function, the arguments it takes, if any, and highlights the current argument we are positioned at. This way, we do not have to go back to review the signature of the function just to remember its arity. Same principle for variables, where eldoc-mode puts the first line of their documentation string in the echo area.

Of course, this works out-of-the-box for Emacs Lisp code. Other modes need to arrange how to use this feature.

;;;; Eldoc (Emacs live documentation feedback)
(use-package eldoc
  :ensure nil
  :hook (prog-mode . eldoc-mode)
  :config
  (setq eldoc-message-function #'message)) ; don't use mode line for M-x eval-expression, etc.

5.10.7. The prot-emacs-langs.el settings for eglot (LSP client)

The built-in eglot feature, developed and maintained by João Távora, is Emacs’ own client for the Language Server Protocol (LSP). The LSP technology is all about enhancing the ability of a text editor to work with a given programming language. This works by installing a so-called “language server” on your computer, which the “LSP client” (i.e. eglot) will plug into. A typical language server provides the following capabilities:

Code completion
This can be visualised for in-buffer automatic expansion of function calls, variables, and the like (The prot-emacs-completion.el for in-buffer completion popup (corfu)).
Code linting
To display suggestions, warnings, or errors. These are highlighted in the buffer, usually with an underline, and can also be displayed in a standalone buffer with the commands flymake-show-buffer-diagnostics, flymake-show-project-diagnostics (The prot-emacs-langs.el settings for code linting (flymake)).
Code navigation and cross-referencing

While over a symbol, use a command to jump directly to its definition. The default key bindings for going forth and then back are M-. (xref-find-definitions) and M-, (xref-go-back).

[ Features such as the definition of the outline should, in principle, be implemented by the major mode though I see no reason why a language server cannot also be involved in this task. You can use the built-in outline-minor-mode to provide Org-like folding capabilties for outline headings (The prot-emacs-langs.el settings for outline-minor-mode). I usually navigate the outline using minibuffer completion, with the help of my prot-search-outline command (The prot-emacs-search.el extras provided by the prot-search.el library). ]

Assuming the language server is installed, to start using the LSP client in a given file, do M-x eglot. To make this happen automatically for every newly visited file, add a hook like this:

(add-hook 'SOME-MAJOR-mode #'eglot-ensure)

I only code in Emacs Lisp, so I am actually not using eglot anywhere. Though I have tried it in Bash and JavaScript files and it worked fine.

;;;; Eglot (built-in client for the language server protocol)
(use-package eglot
  :ensure nil
  :functions (eglot-ensure)
  :commands (eglot)
  :config
  (setq eglot-sync-connect nil)
  (setq eglot-autoshutdown t))

5.10.8. The prot-emacs-langs.el settings for very long lines

Emacs is notoriously bad at performing well when parsing long lines. I believe this has to do with how syntax highlighting and code navigation are implemented. The global-so-long-mode tries to mitigate this problem by disabling such extras when we visit a file with really long lines, such as minified JavaScript. I have not used it enough to know how reliable this is, though it is nice to have just in case.

;;;; Handle performance for very long lines (so-long.el)
(use-package so-long
  :ensure nil
  :hook (after-init . global-so-long-mode))

5.10.9. The prot-emacs-langs.el settings for markdown-mode

The markdown-mode lets us edit Markdown files. We get syntax highlighting and several extras, such as the folding of headings and navigation between them. The mode actually provides lots of added functionality for GitHub-flavoured Markdown and to preview a Markdown file’s HTML representation on a web page. Though I only use it for basic text editing.

;;; Markdown (markdown-mode)
(use-package markdown-mode
  :ensure t
  :defer t
  :config
  (setq markdown-fontify-code-blocks-natively t))

5.10.10. The prot-emacs-langs.el settings for csv-mode

The package csv-mode provides support for .csv files. I do need this on occasion, even though my use-case is pretty basic. For me, the killer feature is the ability to create a virtual tabulated listing with the command csv-align-mode: it hides the field delimiter (comma or space) and shows a tab stop in its stead.

;;; csv-mode
(use-package csv-mode
  :ensure t
  :commands (csv-align-mode))

5.10.11. The prot-emacs-langs.el settings for sxhkdrc-mode

This is a major mode for editing sxhkdrc files. SXHKD is the Simple X Hot Key Daemon which is commonly used in minimalist desktop sessions on Xorg, such as with the Binary Space Partitioning Window Manager (BSPWM). The sxhkdrc file configures key chords, binding them to commands. For the technicalities, read the man page sxhkd(1).

;;; SXHKDRC mode (one of my many packages)
(use-package sxhkdrc-mode
  :ensure t
  ;; By default, it only applies to the sxhkdrc file, but I have other
  ;; relevant entries as well.  I separate my keys into different
  ;; modules and load only what I need.
  :mode "sxhkdrc_.*")

5.10.12. The prot-emacs-langs.el settings for spell checking

For spell checking on-demand, I rely on the built-in flyspell feature that I complement with some small extras (The prot-spell.el library).

In terms of workflow, I do not like to see any spell checking while I type. I prefer to write out the entire draft and then do a spell check at the end. Whatever typos do not bother me (and I have commented many times before about my “alla prima” approach to creativity). It is harder to set up multilingual spell checking with flyspell and I do not even bother trying as I seldom have that need.

;;; Flyspell and prot-spell.el (spell check)
(use-package flyspell
  :ensure nil
  :bind
  ( :map flyspell-mode-map
    ("C-;" . nil)
    :map flyspell-mouse-map
    ("<mouse-3>" . flyspell-correct-word)
    :map ctl-x-x-map
    ("s" . flyspell-mode)) ; C-x x s
  :config
  (setq flyspell-issue-message-flag nil)
  (setq flyspell-issue-welcome-flag nil)
  (setq ispell-program-name "aspell")
  (setq ispell-dictionary "en_GB"))

(use-package prot-spell
  :ensure nil
  :bind
  (("M-$" . prot-spell-spell-dwim)
   ("C-M-$" . prot-spell-change-dictionary)
   ("M-i" . prot-spell-spell-dwim) ; override `tab-to-tab-stop'
   ("C-M-i" . prot-spell-change-dictionary)) ; override `complete-symbol'
  :config
  (setq prot-spell-dictionaries
        '(("EN English" . "en")
          ("EL Ελληνικά" . "el")
          ("FR Français" . "fr")
          ("ES Espanõl" . "es")))

  ;; Also check prot-spell.el for what I am doing with
  ;; `prot-spell-ispell-display-buffer'.  Then refer to the
  ;; `display-buffer-alist' for the relevant entry.
  (setq ispell-choices-buffer "*ispell-top-choices*"))

5.10.13. The prot-emacs-langs.el settings for code linting (flymake)

The built-in flymake feature defines an interface for viewing the output of linter programs. A “linter” parses a file and reports possible notes/warnings/errors in it. With flymake we get these diagnostics in the form of a standalone buffer as well as inline highlights (typically underlines combined with fringe indicators) for the portion of text in question. The linter report is displayed with the command flymake-show-buffer-diagnostics, or flymake-show-project-diagnostics. Highlights are shown in the context of the file.

The built-in eglot feature uses flymake internally to handle the LSP linter output (The prot-emacs-langs.el settings for eglot).

As for what I have in this configuration block, the essentials for me are the user options flymake-start-on-save-buffer and flymake-start-on-flymake-mode as they make the linter update its report when the buffer is saved and when flymake-mode is started, respectively. Otherwise, we have to run it manually, which is cumbersome.

The package-lint-flymake package by Steve Purcell adds the glue code to make flymake report issues with Emacs Lisp files for the purposes of packaging. I use it whenever I work on my numerous Emacs packages.

;;; Flymake
(use-package flymake
  :ensure nil
  :preface
  (defvar prot/flymake-mode-projects-path
    (file-name-as-directory (expand-file-name "Projects" "~/Git/"))
    "Path to my Git projects.")

  (defun prot/flymake-mode-lexical-binding ()
    (when lexical-binding
      (flymake-mode 1)))

  (defun prot/flymake-mode-in-my-projects ()
    (when-let* ((file (buffer-file-name))
                ((string-prefix-p prot/flymake-mode-projects-path (expand-file-name file)))
                ((not (file-directory-p file)))
                ((file-regular-p file)))
      (add-hook 'find-file-hook #'prot/flymake-mode-lexical-binding nil t)))

  (add-hook 'emacs-lisp-mode-hook #'prot/flymake-mode-in-my-projects)
  :bind
  ( :map ctl-x-x-map
    ("m" . flymake-mode) ; C-x x m
    :map flymake-mode-map
    ("C-c ! s" . flymake-start)
    ("C-c ! d" . flymake-show-buffer-diagnostics) ; Emacs28
    ("C-c ! D" . flymake-show-project-diagnostics) ; Emacs28
    ("C-c ! n" . flymake-goto-next-error)
    ("C-c ! p" . flymake-goto-prev-error))
  :config
  (setq flymake-fringe-indicator-position 'left-fringe)
  (setq flymake-suppress-zero-counters t)
  (setq flymake-no-changes-timeout nil)
  (setq flymake-start-on-flymake-mode t)
  (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))
  ;; NOTE 2023-07-03: `prot-modeline.el' actually defines the counters
  ;; itself and ignores this.
  (setq flymake-mode-line-counter-format
        '("" flymake-mode-line-error-counter
          flymake-mode-line-warning-counter
          flymake-mode-line-note-counter ""))
  (setq flymake-show-diagnostics-at-end-of-line nil)) ; Emacs 30

;;; Elisp packaging requirements
(use-package package-lint-flymake
  :ensure t
  :after flymake
  :config
  (add-hook 'flymake-diagnostic-functions #'package-lint-flymake))

5.10.14. The prot-emacs-langs.el settings for outline-minor-mode

The built-in outline-minor-mode defines folding and navigation commands for the file’s outline. The “outline” is the document’s discernible structure, defined by the local value of the variable outline-regexp. Simply put, it is about the headings in the document.

Any file can have its own outline. For example, in Emacs Lisp any comment with three or more delimiters that starts at the beginning of the line counts as a heading. Three delimiters make it a level 1 heading; four delimiters for level 2, and so on. You will notice that I already use outlines in all my files. Sometimes I enable the outline-minor-mode, though I do not really need the folding capabilities. Plus, I can navigate the file using minibuffer completion among outline entries without enabling outline-minor-mode, courtesy of the command prot-search-outline (The prot-emacs-search.el extras provided by the prot-search.el library).

A common question/remark about my literate configuration with Org is why not use outline-minor-mode or the external outshine package to get “the same features” without Org. The feature sets are not really comparable. With Org we can comment at length without putting all that in the actual code, whereas with outline-minor-mode you would be reading all this commentary in the source code: now you only read it in this Org document and its website counterpart. Furthermore, we have links between headings, a convenient export mechanism to a website, and the ability to easily redirect where the code blocks are tangled to. On the latter point, if, for instance, I ever choose to consolidate all my Emacs setup in a monolithic init.el, I just run a quick replace for all the :tangle values in this document. Finally, we have to consider the use-case: if you have a private config, then outline-minor-mode may be enough. In my case, I maintain a massive document which, I think, can be helpful for others in the community as well.

[ Also read: Why use Org when you can have an outline in Elisp? ]

;;; General configurations for prose/writing

;;;; `outline' (`outline-mode' and `outline-minor-mode')
(use-package outline
  :ensure nil
  :bind
  ("<f10>" . outline-minor-mode)
  :config
  (setq outline-minor-mode-highlight nil) ; emacs28
  (setq outline-minor-mode-cycle t) ; emacs28
  (setq outline-minor-mode-use-buttons nil) ; emacs29---bless you for the nil option!
  (setq outline-minor-mode-use-margins nil)) ; as above

5.10.15. The prot-emacs-langs.el settings for dictionary

The built-in dictionary feature lets us access a webpage (or local server) to read dictionary entries. The command dictionary-search is also smart enough to use the word at point as the default value, so we can type RET at the minibuffer prompt to select it without typing it out. A neat package overall!

;;;; `dictionary'
(use-package dictionary
  :ensure nil
  :bind ("C-c d" . dictionary-search)
  :config
  (setq dictionary-server "dict.org"
        dictionary-default-popup-strategy "lev" ; read doc string
        dictionary-create-buttons nil
        dictionary-use-single-buffer t))

5.10.16. The prot-emacs-langs.el settings for altcaps (alternating letter casing)

What follows is another package of mine. I wrote it to practice some programming concepts, though I genuinely find it useful. How else are we supposed to mock people when they say wHy WoN’t YoU sPoOnFeEd Me AlL tHe AnSwErS?

Technically, the altcaps package is a small, focused-in-scope tool that helps users communicate mockery or sarcasm effectively. It does this by alternating the letter casing of characters in the words it affects.

;;; aLtCaPs
;; Read the manual: <https://protesilaos.com/emacs/altcaps>.
(use-package altcaps
  :ensure t
  :bind
  ("C-x C-a" . altcaps-dwim)
  :config
  ;; Force letter casing for certain characters (for legibility).
  (setq altcaps-force-character-casing
        '(;; Greek theta
          (?θ . downcase))))

5.10.17. The prot-emacs-langs.el settings for denote (notes and file-naming)

This is another one of my packages and is extended by my consult-denote package (The prot-emacs-langs.el integration between Consult and Denote (consult-denote)).

Denote is a simple note-taking tool for Emacs. It is based on the idea that notes should follow a predictable and descriptive file-naming scheme. The file name must offer a clear indication of what the note is about, without reference to any other metadata. Denote basically streamlines the creation of such files while providing facilities to link between them.

Denote’s file-naming scheme is not limited to “notes”. It can be used for all types of file, including those that are not editable in Emacs, such as videos. Naming files in a consistent way makes their filtering and retrieval considerably easier. Denote provides relevant facilities to rename files, regardless of file type.

;;; Denote (simple note-taking and file-naming)

;; Read the manual: <https://protesilaos.com/emacs/denote>.  This does
;; not include all the useful features of Denote.  I have a separate
;; private setup for those, as I need to test everything is in order.
(use-package denote
  :ensure t
  :hook
  ;; If you use Markdown or plain text files you want to fontify links
  ;; upon visiting the file (Org renders links as buttons right away).
  ((text-mode . denote-fontify-links-mode-maybe)

   ;; Highlight Denote file names in Dired buffers.  Below is the
   ;; generic approach, which is great if you rename files Denote-style
   ;; in lots of places as I do.
   ;;
   ;; If you only want the `denote-dired-mode' in select directories,
   ;; then modify the variable `denote-dired-directories' and use the
   ;; following instead:
   ;;
   ;;  (dired-mode . denote-dired-mode-in-directories)
   (dired-mode . denote-dired-mode))
  :bind
  ;; Denote DOES NOT define any key bindings.  This is for the user to
  ;; decide.  Here I only have a subset of what Denote offers.
  ( :map global-map
    ("C-c n n" . denote)
    ("C-c n N" . denote-type)
    ("C-c n o" . denote-sort-dired) ; "order" mnemonic
    ;; Note that `denote-rename-file' can work from any context, not
    ;; just Dired buffers.  That is why we bind it here to the
    ;; `global-map'.
    ;;
    ;; Also see `denote-rename-file-using-front-matter' further below.
    ("C-c n r" . denote-rename-file)
    ;; If you intend to use Denote with a variety of file types, it is
    ;; easier to bind the link-related commands to the `global-map', as
    ;; shown here.  Otherwise follow the same pattern for
    ;; `org-mode-map', `markdown-mode-map', and/or `text-mode-map'.
    :map text-mode-map
    ("C-c n i" . denote-link) ; "insert" mnemonic
    ("C-c n I" . denote-add-links)
    ("C-c n b" . denote-backlinks)
    ;; Also see `denote-rename-file' further above.
    ("C-c n R" . denote-rename-file-using-front-matter)
    :map org-mode-map
    ("C-c n d l" . denote-org-extras-dblock-insert-links)
    ("C-c n d b" . denote-org-extras-dblock-insert-backlinks)
    ;; Key bindings specifically for Dired.
    :map dired-mode-map
    ("C-c C-d C-i" . denote-dired-link-marked-notes)
    ("C-c C-d C-r" . denote-dired-rename-marked-files)
    ("C-c C-d C-k" . denote-dired-rename-marked-files-with-keywords)
    ("C-c C-d C-f" . denote-dired-rename-marked-files-using-front-matter))
  :config
  ;; Remember to check the doc strings of those variables.
  (setq denote-directory (expand-file-name "~/Documents/notes/"))
  (setq denote-file-type 'text) ; Org is the default file type

  ;; If you want to have a "controlled vocabulary" of keywords,
  ;; meaning that you only use a predefined set of them, then you want
  ;; `denote-infer-keywords' to be nil and `denote-known-keywords' to
  ;; have the keywords you need.
  (setq denote-known-keywords '("emacs" "philosophy" "politics" "economics"))
  (setq denote-infer-keywords t)
  (setq denote-sort-keywords t)

  (setq denote-excluded-directories-regexp nil)
  (setq denote-date-format nil) ; read its doc string
  (setq denote-rename-confirmations nil) ; CAREFUL with this if you are not familiar with Denote!

  (setq denote-backlinks-show-context nil)

  (setq denote-rename-buffer-format "[D] %t%b")
  (setq denote-buffer-has-backlinks-string " (<--->)")

  ;; Automatically rename Denote buffers when opening them so that
  ;; instead of their long file name they have a literal "[D]"
  ;; followed by the file's title.  Read the doc string of
  ;; `denote-rename-buffer-format' for how to modify this.
  (denote-rename-buffer-mode 1))
5.10.17.1. The prot-emacs-langs.el integration between Consult and Denote (consult-denote)

This is another package of mine which extends my denote package (The prot-emacs-langs.el settings for denote (notes and file-naming)).

This is glue code to integrate denote with Daniel Mendler’s consult (The prot-emacs-completion.el settings for consult). The idea is to enhance minibuffer interactions, such as by providing a preview of the file-to-linked/opened and by adding more sources to the consult-buffer command.

  • Package name (GNU ELPA): consult-denote
  • Official manual: not available yet.
  • Change log: not available yet.
  • Git repositories:
  • Backronym: Consult-Orchestrated Navigation and Selection of Unambiguous Targets…denote.
(when prot-emacs-completion-extras
  (use-package consult-denote
    :ensure t
    :bind
    (("C-c n f" . consult-denote-find)
     ("C-c n g" . consult-denote-grep))
    :config
    (consult-denote-mode 1)))

5.10.18. The prot-emacs-langs.el settings for logos (writing extras and buffer navigation)

This package provides a simple approach to setting up a “focus mode”. It uses the page-delimiter (typically ^L) or the outline together with some commands to move between pages whether narrowing is in effect or not. It also provides some optional aesthetic tweaks which come into effect when the buffer-local logos-focus-mode is enabled. The manual shows how to extend the code to achieve the desired result.

I use logos to do video presentations that involve “slides”. Each heading/section becomes its own “slide” simply by narrowing to it.

;;; Custom extensions for "focus mode" (logos.el)
;; Read the manual: <https://protesilaos.com/emacs/logos>.
(use-package olivetti
  :ensure t
  :commands (olivetti-mode)
  :config
  (setq olivetti-body-width 0.7)
  (setq olivetti-minimum-body-width 80)
  (setq olivetti-recall-visual-line-mode-entry-state t))

(use-package logos
  :ensure t
  :bind
  (("C-x n n" . logos-narrow-dwim)
   ("C-x ]" . logos-forward-page-dwim)
   ("C-x [" . logos-backward-page-dwim)
   ;; I don't think I ever saw a package bind M-] or M-[...
   ("M-]" . logos-forward-page-dwim)
   ("M-[" . logos-backward-page-dwim)
   ("<f9>" . logos-focus-mode))
  :config
  (setq logos-outlines-are-pages t)
  (setq logos-outline-regexp-alist
        `((emacs-lisp-mode . ,(format "\\(^;;;+ \\|%s\\)" logos-page-delimiter))
          (org-mode . ,(format "\\(^\\*+ +\\|^-\\{5\\}$\\|%s\\)" logos-page-delimiter))
          (markdown-mode . ,(format "\\(^\\#+ +\\|^[*-]\\{5\\}$\\|^\\* \\* \\*$\\|%s\\)" logos-page-delimiter))
          (conf-toml-mode . "^\\[")))

  ;; These apply when `logos-focus-mode' is enabled.  Their value is
  ;; buffer-local.
  (setq-default logos-hide-mode-line t)
  (setq-default logos-hide-header-line t)
  (setq-default logos-hide-buffer-boundaries t)
  (setq-default logos-hide-fringe t)
  (setq-default logos-variable-pitch t) ; see my `fontaine' configurations
  (setq-default logos-buffer-read-only nil)
  (setq-default logos-scroll-lock nil)
  (setq-default logos-olivetti t)

  (add-hook 'enable-theme-functions #'logos-update-fringe-in-buffers)

;;;; Extra tweaks
  ;; place point at the top when changing pages, but not in `prog-mode'
  (defun prot/logos--recenter-top ()
    "Use `recenter' to reposition the view at the top."
    (unless (derived-mode-p 'prog-mode)
      (recenter 1))) ; Use 0 for the absolute top

  (add-hook 'logos-page-motion-hook #'prot/logos--recenter-top))
(provide 'prot-emacs-langs)

5.11. The prot-emacs-email.el module

[ Also see: Overview of my email setup (mbsync, msmtp, mail indexer, and MUA). ]

Email inside of Emacs is one of the best changes I have ever made to my workflow. I consider it far better than the mutt setup I once had. The benefits are down to the interconnectedness of the Emacs environment (watch: Why Emacs itself is my “favourite Emacs package” (2020-10-21)). All text editing capabilities are available. Same for buffer navigation. Same for themes and fonts. Then we have integration with org-capture to quickly produce a task that shows up on the Org agenda and has a link back to the original email (The prot-emacs-org.el Org capture templates). And there is also the seamless connection between Emacs and GPG, so any encrypted file/email is decrypted behind the scenes with us experiencing it as every other regular file. Fantastic stuff!

5.11.1. The prot-emacs-email.el basic settings (including authinfo)

Emacs reads login credentials from the list of files specified in the user option auth-sources. I set it to a single GPG-encrypted entry. The contents of that file look like this:

machine pub port 993 login SOME-USER-NAME-HERE-1 password SOME-PASSWORD-HERE
machine inf port 993 login SOME-USER-NAME-HERE-2 password SOME-PASSWORD-HERE

machine smtp.some-server.com port 465 login SOME-USER-NAME-HERE-1 password SOME-PASSWORD-HERE
machine mail.other-server.com port 465 login SOME-USER-NAME-HERE-2 password SOME-PASSWORD-HERE

Each line can be read as a map of key-value pairs. Think of it like this:

machine pub
port 993
login SOME-USER-NAME-HERE-1
password SOME-PASSWORD-HERE-1

Depending on the settings and the applicable program, Emacs reads this file to establish a connection. This is not limited to emails, mind you, as we can have credentials such as for running sudo via TRAMP or logging in to IRC with the rcirc command (The prot-emacs-web.el module).

For my email purposes, Emacs does not read from the auth-sources, though I do keep this in place for other programs to read from. These programs include mbsync and msmtp (How I use email in Emacs).

Other than that, all I do in this snippet is set the default name and email, which are consulted by miscellaneous pieces Emacs functionality.

;;;; File with authentication credentials (`auth-source')
(use-package auth-source
  :ensure nil
  :defer t
  :config
  (setq auth-sources '("~/.authinfo.gpg")
        user-full-name "Protesilaos Stavrou"
        user-mail-address "public@protesilaos.com"))

5.11.2. The prot-emacs-email.el message composition and encryption settings (message.el)

Across all Emacs email clients that I have used (gnus, mu4e, and notmuch) message composition is handled by the same built-in library: message.el. It produces a buffer with email headers at the top, followed by a separator line, and then the body of the message where we write what we want. The body can have a signature automatically appended to it.

Message buffers can integrate with the system’s GPG agent to encrypt the current message. This is done by inserting a special MIME tag at the top of the message body. I do it by typing C-c C-m C-e, which invokes the command mml-secure-message-sign-encrypt. If the GPG agent is running and the password is already saved in the keyring, the message is sent encrypted without any further prompts (Emacs generally treats encrypted files transparently if everything is set up at the environment level).

When replying to emails I receive, I normally comment inline by citing the original message. Concretely, this looks like this:

> Some text I am commenting on.

My comment on it.

The settings I have for citing messages ensure that the top of the message includes the From and Date headers, so the original email I am replying to looks like this in my message composition buffer:

> From: Some Name <name@domain.tld>
> Date: Tue,  9 Jan 2024 06:58:38 +0200
>
> Some text I am commenting on.

My comment on it.

Check the documentation of the format-time-string function to understand how the date is defined in the user option message-citation-line-format.

When citing a portion of text, I do not need to include the entirety of it, but also the parts that are pertinent to the matter at hand. I thus frequently elide text by marking it and then typing C-c C-e (message-elide-region). The message-elide-ellipsis I specify here turns the region into a cited snippet that references the line count, like:

> From: Some Name <name@domain.tld>
> Date: Tue,  9 Jan 2024 06:58:38 +0200
>
> Some text I am commenting on.

My comment on it.

> Something that shows lots of details I do not need to keep in place:
>
> [... 20 lines elided]

Another comment of mine here.

To attach a file, type C-c C-a (mml-attach-file). This uses minibuffer completion to select a single file. An alternative is to use dired to produce a file listing, mark the relevant files, and attach them from there (The prot-emacs-email.el integration with Dired for email attachments (gnus-dired-mode))

Once the message is done, type C-c C-c to send it. The exact command depends on the mail user agent. In my case, it is notmuch-mua-send-and-exit. By default, Emacs prompts for confirmation before sending out the message. I disable that by modifying message-confirm-send. Similarly, I prefer to delete the buffer of a sent message, so I change the value of message-kill-buffer-on-exit.

Between frictionless encryption and the ease of editing text in a message composition buffer, the email setup I have in Emacs is the most potent I have ever used (Overview of my email setup (mbsync, msmtp, mail indexer, and MUA)).

;;;; Encryption settings (`mm-encode' and `mml-sec')

(use-package mm-encode
  :ensure nil
  :defer t
  :config
  (setq mm-encrypt-option nil ; use 'guided for both if you need more control
        mm-sign-option nil))

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

;;;; Message composition (`message')

(use-package message
  :ensure nil
  :defer t
  :hook
  (message-setup . message-sort-headers)
  :config
  (setq mail-user-agent 'message-user-agent
        message-mail-user-agent t) ; use `mail-user-agent'
  (setq mail-header-separator "--text follows this line--")
  (setq message-elide-ellipsis "\n> [... %l lines elided]\n")
  (setq compose-mail-user-agent-warnings nil)
  (setq message-signature "Protesilaos Stavrou\nhttps://protesilaos.com\n"
        mail-signature message-signature)
  (setq message-citation-line-function #'message-insert-formatted-citation-line)
  (setq message-citation-line-format (concat "> From: %f\n"
                                             "> Date: %a, %e %b %Y %T %z\n"
                                             ">")
        message-ignored-cited-headers "") ; default is "." for all headers
  (setq message-confirm-send nil)
  (setq message-kill-buffer-on-exit t)
  ;; (add-to-list 'mm-body-charset-encoding-alist '(utf-8 . base64))
  (setq message-wide-reply-confirm-recipients nil))

5.11.3. The prot-emacs-email.el integration with Dired for email attachments (gnus-dired-mode)

The whole point of using Emacs is to draw linkages between different specialised tools. One such case is to use the built-in file manager to select some files and attach them to the currently open message composition buffer (The prot-emacs-dired.el module). Do it by typing C-c C-m C-a (gnus-dired-attach). This also works without an open message composition buffer. In that case, it produces such a buffer, with the attachments in place. Though I usually have the message buffer in place before going to dired to find some attachments (The prot-emacs-email.el message composition and encryption settings (message.el)).

Note that the minor mode which sets up the relevant settings is called gnus-dired-mode, although it does not require gnus and its numerous accoutrements.

;;;; Add attachments from Dired (`gnus-dired' does not require `gnus')
(use-package gnus-dired
  :ensure nil
  :after message
  :hook
  (dired-mode . turn-on-gnus-dired-mode))

5.11.4. The prot-emacs-email.el settings for sendmail

As I explain in the Overview of my email setup (mbsync, msmtp, mail indexer, and MUA), I use the external msmtp program to handle email sending, else to set up the Mail Transfer Agent (MTA). With msmtp and a compatibility package installed (msmtp-mta on Arch Linux), the standard sendmail executable becomes a symlink to the msmtp program.

I use this method because it is more portable and flexible. With the standard built-in Emacs design to use the smtpmail-send-it function as the value of send-mail-function, I also need to declare in Elisp what the SMTP server is. I would rather encapsulate the login credentials in a separate configuration file, independent of Emacs, while also ensuring that different accounts can use their own SMTP server.

;;;; `sendmail' (mail transfer agent)
(use-package sendmail
  :ensure nil
  :after message
  :config
  (setq send-mail-function 'sendmail-send-it
        ;; ;; NOTE 2023-08-08: We do not need this if we have the Arch
        ;; ;; Linux `msmtp-mta' package installed: it replaces the
        ;; ;; generic sendmail executable with msmtp.
        ;;
        ;; sendmail-program (executable-find "msmtp")
        message-sendmail-envelope-from 'header))

5.11.5. The prot-emacs-email.el loading of the email client and call to provide

All the above are the client-agnostic settings. Now I have to put in place the specifics of my mail user agent. I could have a user option here to select one among notmuch, mu4e, or gnus, but it is not practical to have two indexers check the same files, as I will need to keep everything in sync for no apparent benefit. One MUA is more than enough (The prot-emacs-email.el submodule for notmuch (prot-emacs-notmuch.el)).

Finally, we provide the module. This is the mirror function of require (The init.el final part to load the individual modules).

(when (executable-find "notmuch")
  (require 'prot-emacs-notmuch))

(provide 'prot-emacs-email)

5.11.6. The prot-emacs-email.el submodule for notmuch (prot-emacs-notmuch.el)

When we refer to notmuch we cover three distinct concepts:

The command-line mail indexer
It produces a database out of our local mail directory. We can then query the database to get to the email we are looking for.
The Mail User Agent (MUA), else email client
It provides an Emacs interface to the aforementioned indexer. This MUA also draws linkages between other programs, to write and send messages. The user does not need to know that under the hood it is not one “thing” that gets the job done, though this is how it appears prima facie.
The system package
At least on Arch Linux and Debian, the notmuch package bundles together the above two. In fact, the indexer and the Emacs MUA are maintained in tandem by the same developers.

I install the system package to get the command-line utility. Since it ships with the Emacs files, I do not install the relevant Emacs package, but arrange for the notmuch system path to be added to the Emacs load-path. The load-path is where Emacs checks to find Elisp programs (when we require some feature, it works because of its directory being part of the load-path).

;;; 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).
(use-package notmuch
  :load-path "/usr/share/emacs/site-lisp/"
  :defer t
  :commands (notmuch notmuch-mua-new-mail))
5.11.6.1. The prot-emacs-notmuch.el section about the account settings

To actually use the notmuch mail user agent (The prot-emacs-email.el submodule for notmuch (prot-emacs-notmuch.el)), we need to specify (i) who we are and (ii) where should sent mail be directed to. This is what the user options notmuch-identities and notmuch-fcc-dirs are about.

Notice that I use the prot-common-auth-get-field function, which reads data from my encrypted ~/.authinfo.gpg file (The prot-emacs-email.el basic settings (including authinfo)). In prot-common.el, I define a few helper functions that are general in nature and may be used by other Elisp snippets of mine (The prot-common.el library). Doing things this way ensures that I do not share the details about the accounts I set.

At any rate, the format of notmuch-identities is like this:

(setq notmuch-identities '("Protesilaos Stavrou <someone@somewhere.com>" "Prot <just-my-nickname@somewhere.com>"))

While the corresponding notmuch-fcc-dirs map those accounts to the relative path of their local sent mail folder (it is relative to the root of the directory that notmuch indexes, so something like ~/.mail/pub/Sent needs to be written as pub/Sent). The data structure looks like this:

(setq notmuch-fcc-dirs
      '(("someone@somewhere.com" . "someone/Sent")
        ("nickname@somewhere.com" . "nickname/Sent")))

Now the actual code I use:

;;; Account settings
(use-package notmuch
  :defer t
  :config
  (let ((prv (prot-common-auth-get-field "prv-gandi" :user))
        (pub (prot-common-auth-get-field "pub-gandi" :user))
        (inf (prot-common-auth-get-field "inf-gandi" :user))
        (box (prot-common-auth-get-field "prot-gandi" :user)))
    (setq notmuch-identities
          (mapcar (lambda (str)
                    (format "%s <%s>" user-full-name str))
                  (list prv pub inf box))
          notmuch-fcc-dirs
          `((,prv . "gandi/Sent")
            (,inf . "gandi/Sent")
            (,pub . "gandi/Sent")
            (,box . "gandi/Sent")))))
5.11.6.2. The prot-emacs-notmuch.el section about the general user interface

When we use the notmuch Emacs command to get into the mail user agent (The prot-emacs-email.el submodule for notmuch (prot-emacs-notmuch.el)), we are presented in the “hello” buffer. It contains a set of pseudo-graphical widgets to check the available tags, view recent searches, perform a new search, and the like. I find it too busy and not useful, as all that functionality is already available directly with key bindings. Why nagivate all the way to the search box when s (notmuch-search) initiates a new search? As always, use M-x describe-mode (or type C-h m) to learn about the keys and commands of the current major mode (as well as to check all the minor modes that are effective therein).

My “hello” buffer is a single vertical listing of my saved searches. Those include the name of the search, such as inbox, followed by a total message count. Everything else is removed. As such, the values of notmuch-hello-recent-searches-max and notmuch-show-all-tags-list are not relevant.

;;;; General UI
(use-package notmuch
  :defer t
  :config
  (setq notmuch-show-logo nil
        notmuch-column-control 1.0
        notmuch-hello-auto-refresh t
        notmuch-hello-recent-searches-max 20
        notmuch-hello-thousands-separator ""
        notmuch-hello-sections '(notmuch-hello-insert-saved-searches)
        notmuch-show-all-tags-list t))
5.11.6.3. The prot-emacs-notmuch.el section about the presentation of search buffers

The search buffers are the interface I work with the most. They provide a listing of all message threads that match the given search terms.

Most of the configurations I have here are stylistic in nature. The one that defines necessary functionality is the value of the user option notmuch-saved-searches. The :query of those saved searches is what we would normally pass at the command line on the notmuch search invocation, or inside of the mail user agent by typing s (notmuch-search).

The emoji spotted here and elsewhere are purely cosmetic: they do not form part of the search terms or tags.

;;;; Search
(use-package notmuch
  :defer t
  :config
  (setq notmuch-search-oldest-first nil)
  (setq notmuch-search-result-format
        '(("date" . "%12s  ")
          ("count" . "%-7s  ")
          ("authors" . "%-20s  ")
          ("subject" . "%-80s  ")
          ("tags" . "(%s)")))
  (setq notmuch-tree-result-format
        '(("date" . "%12s  ")
          ("authors" . "%-20s  ")
          ((("tree" . "%s")
            ("subject" . "%s"))
           . " %-80s  ")
          ("tags" . "(%s)")))
  (setq notmuch-search-line-faces
        '(("unread" . notmuch-search-unread-face)
          ;; ;; NOTE 2022-09-19: I disable this because I add a cosmeic
          ;; ;; emoji via `notmuch-tag-formats'.  This way I do not get
          ;; ;; an intense style which is very distracting when I filter
          ;; ;; my mail to include this tag.
          ;;
          ;; ("flag" . notmuch-search-flagged-face)
          ;;
          ;; Using `italic' instead is just fine.  Though I also tried
          ;; it without any face and I was okay with it.  The upside of
          ;; having a face is that you can identify the message even
          ;; when the window is split and you don't see the tags.
          ("flag" . italic)))
  (setq notmuch-show-empty-saved-searches t)
  (setq notmuch-saved-searches
        `(( :name "📥 inbox"
            :query "tag:inbox"
            :sort-order newest-first
            :key ,(kbd "i"))
          ( :name "📚 all unread (inbox)"
            :query "tag:unread and tag:inbox"
            :sort-order newest-first
            :key ,(kbd "u"))
          ( :name "💬 unread other (inbox)"
            :query "tag:unread and tag:inbox and not tag:package and not tag:coach"
            :sort-order newest-first
            :key ,(kbd "o"))
          ( :name "🗂️ unread packages"
            :query "tag:unread and tag:package"
            :sort-order newest-first
            :key ,(kbd "p"))
          ;; My coaching job: <https://protesilaos.com/coach/>.
          ( :name "🏆 unread coaching"
            :query "tag:unread and tag:coach"
            :sort-order newest-first
            :key ,(kbd "c")))))
5.11.6.4. The prot-emacs-notmuch.el section about tag settings

Part of the value of using a mail indexer is the ability to tag messages. These help with data retrieval and filtering. For notmuch, tags are a purely indexing construct, meaning that they are not written to the underlying file. An exception to this are the standard IMAP tags for read/unread, seen, attachments, and deleted (I think that’s all, but please double check).

The + or - prefix indicates whether a tag is added or removed from the list. The same characters work as key bindings in all notmuch buffers to bring up a minibuffer interface for adding/removing tags. This interface accepts multiple entries, so even if we start with a - we can still continue with an addition.

Otherwise, tagging operations follow a predefined scheme, specified in the user option notmuch-tagging-keys. I do not specify its value here because I give some of my custom functions, hence its incorporation in the subsequent section about setting up prot-notmuch.el (The prot-emacs-notmuch.el custom extensions (per prot-notmuch.el)).

;;;; Tags
(use-package notmuch
  :defer t
  :config
  (setq notmuch-archive-tags nil ; I do not archive email
        notmuch-message-replied-tags '("+replied")
        notmuch-message-forwarded-tags '("+forwarded")
        notmuch-show-mark-read-tags '("-unread")
        notmuch-draft-tags '("+draft")
        notmuch-draft-folder "drafts"
        notmuch-draft-save-plaintext 'ask)

  ;; Also see `notmuch-tagging-keys' in the `prot-notmuch' section
  ;; further below.
  ;;
  ;; All emoji are cosmetic.  The tags are just the text.
  (setq notmuch-tag-formats
        '(("unread" (propertize tag 'face 'notmuch-tag-unread))
          ("flag" (propertize tag 'face 'notmuch-tag-flagged)
           (concat tag "🚩")))
        notmuch-tag-deleted-formats
        '(("unread" (notmuch-apply-face bare-tag 'notmuch-tag-deleted)
           (concat "👁️‍🗨️" tag))
          (".*" (notmuch-apply-face tag 'notmuch-tag-deleted)
           (concat "🚫" tag)))
        notmuch-tag-added-formats
        '(("del" (notmuch-apply-face tag 'notmuch-tag-added)
           (concat "💥" tag))
          (".*" (notmuch-apply-face tag 'notmuch-tag-added)
           (concat "🏷️" tag)))))
5.11.6.5. The prot-emacs-notmuch.el section about email composition settings

Most of the settings here are stylistic. I would not mind having them differently. They are complementary to those germane to the built-in message.el (The prot-emacs-email.el message composition and encryption settings (message.el)).

The notmuch-mua-attachment-regexp is a neat little helper to prevent me from sending out a message without its stipulated attachment. It can give false positives, as I may write something that is about “emotional attachment”, but on the balance I do like being asked for confirmation where needed.

;;;; Email composition
(use-package notmuch
  :defer t
  :config
  (setq notmuch-mua-compose-in 'current-window)
  (setq notmuch-mua-hidden-headers nil)
  (setq notmuch-address-command 'internal) ; NOTE 2024-01-09: I am not using this and must review it.
  (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 nil)
  (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")))
5.11.6.6. The prot-emacs-notmuch.el section about reading messages

Some simple tweaks here to get the presentation I like while reading messages. Everything here is valueble to me, though note the “wash” parts. These pertain to a behaviour where long quotes are hidden behind a button. The idea is to hide most of the text and reveal it on demand. I never want that: if there is a long section of text there, I need to see it.

Note that the presentation of HTML messages is affected by the state of the built-in Simple HTML Renderer (The prot-emacs-web.el settings about the Simple HTML Renderer (shr)). Concretely, there is the shr-use-colors option, which I disable because I do not want hardcoded values to mess up my theme. As such, newletters, receipts, etc., which are usually rendered on a white background will be dark while using a dark theme. This is considerably nicer.

;;;; Reading messages
(use-package notmuch
  :defer t
  :config
  (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-view-part)
  (setq notmuch-show-text/html-blocked-images ".") ; block everything
  (setq notmuch-wash-wrap-lines-length 120)
  (setq notmuch-unthreaded-show-out nil)
  (setq notmuch-message-headers '("To" "Cc" "Subject" "Date"))
  (setq notmuch-message-headers-visible t)

  (let ((count most-positive-fixnum)) ; I don't like the buttonisation of long quotes
    (setq notmuch-wash-citation-lines-prefix count
          notmuch-wash-citation-lines-suffix count)))
5.11.6.7. The prot-emacs-notmuch.el section about hooks and key bindings

Here I set up the following:

  • Remind me when I am mentioning an attachment but do not include one. This is done by reading the contents of the message in search for the notmuch-mua-attachment-regexp (The prot-emacs-notmuch.el section about email composition settings).
  • Do not use a header-line when showing a message. It adds visual clutter.
  • Do not activate the notmuch-hl-line-mode because I want the generic hl-line-mode to take effect instead. This is because I use my lin package to remap buffer-locally the line highlight to be a bit more noticeable in major modes where line selection is the main action (The prot-emacs-theme.el section for lin).
  • Define key bindings that make sense to me. The most important change here is the flipped meaning of the r and R keys, as I want to reply to all recipients by default. I define some more key bindings in the section about my custom extensions for Notmuch (The prot-emacs-notmuch.el custom extensions (per prot-notmuch.el)).
;;;; Hooks and key bindings
(use-package notmuch
  :hook
  (notmuch-mua-send . notmuch-mua-attachment-check) ; also see `notmuch-mua-attachment-regexp'
  (notmuch-show . (lambda () (setq-local header-line-format nil)))
  :config
  (remove-hook 'notmuch-show-hook #'notmuch-show-turn-on-visual-line-mode)
  (remove-hook 'notmuch-search-hook #'notmuch-hl-line-mode) ; Check my `lin' package
  :bind
  ( :map global-map
    ("C-c m" . notmuch)
    ("C-x m" . notmuch-mua-new-mail) ; override `compose-mail'
    :map notmuch-search-mode-map ; I normally don't use the tree view, otherwise check `notmuch-tree-mode-map'
    ("a" . nil) ; the default is too easy to hit accidentally and I do not archive stuff
    ("A" . nil)
    ("/" . notmuch-search-filter) ; alias for l
    ("r" . notmuch-search-reply-to-thread) ; easier to reply to all by default
    ("R" . notmuch-search-reply-to-thread-sender)
    :map notmuch-show-mode-map
    ("a" . nil) ; the default is too easy to hit accidentally and I do not archive stuff
    ("A" . nil)
    ("r" . notmuch-show-reply) ; easier to reply to all by default
    ("R" . notmuch-show-reply-sender)
    :map notmuch-hello-mode-map
    ("C-<tab>" . nil)))
5.11.6.8. The prot-emacs-notmuch.el custom extensions (per prot-notmuch.el)

My prot-notmuch.el extension defines a few useful extras for my email setup (The prot-notmuch.el library). These are commands to quickly perform a tagging operation, such as to mark a messge for deletion and remove it from the inbox. The key bindings here extend those that are for the base Notmuch package (The prot-emacs-notmuch.el section about hooks and key bindings).

;;; My own tweaks for notmuch (prot-notmuch.el)
(use-package prot-notmuch
  :ensure nil
  :after notmuch
  :bind
  ( :map notmuch-search-mode-map
    ("D" . prot-notmuch-search-delete-thread)
    ("S" . prot-notmuch-search-spam-thread)
    ("g" . prot-notmuch-refresh-buffer)
    :map notmuch-show-mode-map
    ("D" . prot-notmuch-show-delete-message)
    ("S" . prot-notmuch-show-spam-message)
    :map notmuch-show-stash-map
    ("S" . prot-notmuch-stash-sourcehut-link))
  :config
  ;; 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 "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 "r") ("-unread") "👁️‍🗨️ Mark as read")
          (,(kbd "u") ("+unread") "🗨️ Mark as unread")))

  ;; These emoji are purely cosmetic.  The tag remains the same: I
  ;; would not like to input emoji for searching.
  (add-to-list 'notmuch-tag-formats '("encrypted" (concat tag "🔒")))
  (add-to-list 'notmuch-tag-formats '("attachment" (concat tag "📎")))
  (add-to-list 'notmuch-tag-formats '("coach" (concat tag "🏆")))
  (add-to-list 'notmuch-tag-formats '("package" (concat tag "🗂️"))))
5.11.6.9. The prot-emacs-notmuch.el glue code for org-capture (ol-notmuch.el)

This package provides the glue code between Notmuch and Org capture:

In simple terms, I can create links that point to emails. When I follow the link, it opens in a fully fledged Notmuch buffer. This is how I build up my agenda of appointments. It highlights the power of Emacs’ interconnectedness, as I go from my email to the agenda, to editing, file management, and related.

;;; Glue code for notmuch and org-link (ol-notmuch.el)
(use-package ol-notmuch
  :ensure t
  :after notmuch)
5.11.6.10. The prot-emacs-notmuch.el section about the notmuch-indicator

This is a simple package of mine that renders an indicator with an email count of the notmuch index on the Emacs mode line. The underlying mechanism is that of notmuch-count(1), which is used to find the number of items that match the given search terms. In practice, the user can define one or more searches and display their counters. These form a listing which realistically is like: @50 😱1000 💕0 for unread messages, bills, and love letters, respectively.

;;; notmuch-indicator (another package of mine)
(use-package notmuch-indicator
  :ensure t
  :after notmuch
  :config
  (setq notmuch-indicator-args
        '(( :terms "tag:unread and tag:inbox"
            :label "[A] "
            :label-face prot-modeline-indicator-green
            :counter-face prot-modeline-indicator-green)
          ( :terms "tag:unread and tag:inbox and not tag:package and not tag:coach"
            :label "[U] "
            :label-face prot-modeline-indicator-cyan
            :counter-face prot-modeline-indicator-cyan)
          ( :terms "tag:unread and tag:package and tag:inbox"
            :label "[P] "
            :label-face prot-modeline-indicator-magenta
            :counter-face prot-modeline-indicator-magenta)
          ( :terms "tag:unread and tag:coach and tag:inbox"
            :label "[C] "
            :label-face prot-modeline-indicator-red
            :counter-face prot-modeline-indicator-red))

        notmuch-indicator-refresh-count (* 60 3)
        notmuch-indicator-hide-empty-counters t
        notmuch-indicator-force-refresh-commands '(notmuch-refresh-this-buffer))

  ;; I control its placement myself.  See prot-emacs-modeline.el where
  ;; I set the `mode-line-format'.
  (setq notmuch-indicator-add-to-mode-line-misc-info nil)

  (notmuch-indicator-mode 1))
5.11.6.11. The prot-emacs-notmuch.el call to provide

Finally, we provide the submodule. This is the mirror function of require (The prot-emacs-email.el loading of the email client and call to provide).

(provide 'prot-emacs-notmuch)

5.11.7. The deprecated prot-emacs-mail.el submodule for mu4e (prot-emacs-mu4e.el)

I did use Mu4e in the past as my email client. Its setup is similar to the one I have now with Notmuch (Overview of my email setup (mbsync, msmtp, mail indexer, and MUA)). The mu4e package is the Emacs interface, or “Mail User Agent”, for the user’s email: this is where one reads messages and responds to them, moves files to different folders, changes email tags, and so on. All email is indexed by the the external mu executable: it is an indexer of one’s local copy of their email. The indexer maintains a database that the user can query. This search facility can be accessed both from the command line and insider Emacs via mu4e.

I prefer Notmuch over Mu4e due to stylistic choices and the overall workflow. I keep this setup around for two reasons: (i) to be able to test it for my themes and (ii) help others who are using it.

;; This is deprecated code as I stopped using mu4e a long time ago.  I
;; keep it here for when I need to test it for my themes (`ef-themes',
;; `modus-themes', `standard-themes').

(use-package mu4e
  :ensure nil
  ;; This is an exception because I install it from the system
  ;; distribution's package archives (depends on non-Emacs code)
  :load-path "/usr/share/emacs/site-lisp/elpa/mu4e-1.8.14"
  :config
  (setq mu4e-maildir "~/.20240226-mail-mu4e")
  (setq mu4e-attachment-dir (expand-file-name "~/Downloads/mail-attachments/"))
  (setq mu4e-confirm-quit nil)
  (setq mu4e-mu-allow-temp-file t) ; mu 1.12.0

  (setq mu4e-use-fancy-chars t ; Cool idea, but they create misalignments
        mu4e-headers-draft-mark     '("D" . "⚒️")
        mu4e-headers-flagged-mark   '("F" . "🚩")
        mu4e-headers-new-mark       '("N" . "🔥")
        mu4e-headers-passed-mark    '("P" . "📨")
        mu4e-headers-replied-mark   '("R" . "✏️")
        mu4e-headers-seen-mark      '("S" . "👁️‍🗨️")
        mu4e-headers-trashed-mark   '("T" . "🚫")
        mu4e-headers-attach-mark    '("a" . "📎")
        mu4e-headers-encrypted-mark '("x" . "🔒")
        mu4e-headers-signed-mark    '("s" . "🔑")
        mu4e-headers-unread-mark    '("u" . "💬")
        mu4e-headers-list-mark      '("l" . "📬")
        mu4e-headers-personal-mark  '("p" . "🦚")
        mu4e-headers-calendar-mark  '("c" . "📅"))

  (setq mu4e-marks
        '((refile
           :char ("r" . "▶")
           :prompt "refile"
           :dyn-target (lambda (target msg) (mu4e-get-refile-folder msg))
           :action (lambda (docid msg target)
                     (mu4e--server-move docid (mu4e--mark-check-target target) "-N")))
          (delete
           :char ("D" . "🚫")
           :prompt "Delete"
           :show-target (lambda (target) "delete")
           :action (lambda (docid msg target) (mu4e--server-remove docid)))
          (flag
           :char ("+" . "🚩")
           :prompt "+flag"
           :show-target (lambda (target) "flag")
           :action (lambda (docid msg target)
                     (mu4e--server-move docid nil "+F-u-N")))
          (move
           :char ("m" . "▷")
           :prompt "move"
           :ask-target  mu4e--mark-get-move-target
           :action (lambda (docid msg target)
                     (mu4e--server-move docid (mu4e--mark-check-target target) "-N")))
          (read
           :char    ("!" . "👁️‍🗨️")
           :prompt "!read"
           :show-target (lambda (target) "read")
           :action (lambda (docid msg target) (mu4e--server-move docid nil "+S-u-N")))
          (trash
           :char ("d" . "🚫")
           :prompt "dtrash"
           :dyn-target (lambda (target msg) (mu4e-get-trash-folder msg))
           :action (lambda (docid msg target)
                     (mu4e--server-move docid
                                        (mu4e--mark-check-target target) "+T-N")))
          (unflag
           :char    ("-" . "➖")
           :prompt "-unflag"
           :show-target (lambda (target) "unflag")
           :action (lambda (docid msg target) (mu4e--server-move docid nil "-F-N")))
          (untrash
           :char   ("=" . "▲")
           :prompt "=untrash"
           :show-target (lambda (target) "untrash")
           :action (lambda (docid msg target) (mu4e--server-move docid nil "-T")))
          (unread
           :char    ("?" . "💬")
           :prompt "?unread"
           :show-target (lambda (target) "unread")
           :action (lambda (docid msg target) (mu4e--server-move docid nil "-S+u-N")))
          (unmark
           :char  " "
           :prompt "unmark"
           :action (mu4e-error "No action for unmarking"))
          (action
           :char ( "a" . "◯")
           :prompt "action"
           :ask-target  (lambda () (mu4e-read-option "Action: " mu4e-headers-actions))
           :action  (lambda (docid msg actionfunc)
                      (save-excursion
                        (when (mu4e~headers-goto-docid docid)
                          (mu4e-headers-action actionfunc)))))
          (something
           :char  ("*" . "✱")
           :prompt "*something"
           :action (mu4e-error "No action for deferred mark"))))

  (setq mu4e-modeline-support t
        mu4e-modeline-unread-items '("U:" . "[U]")
        mu4e-modeline-all-read '("R:" . "[R]")
        mu4e-modeline-all-clear '("C:" . "[C]")
        mu4e-modeline-max-width 42)

  (setq mu4e-notification-support t
        ;; TODO 2024-02-26: Write custom mu4e notification function.
        mu4e-notification-filter #'mu4e--default-notification-filter)

  (setq mu4e-headers-advance-after-mark nil)
  (setq mu4e-headers-auto-update t)
  (setq mu4e-headers-date-format "%F %a, %T")
  (setq mu4e-headers-time-format "%R")
  (setq mu4e-headers-long-date-format "%F, %R")
  (setq mu4e-headers-leave-behavior 'apply)

  (setq mu4e-headers-fields
        '((:date . 26)
          (:flags . 8)
          (:from . 20)
          (:subject)))

  (setq mu4e-get-mail-command "true" ; I auto-fetch with a systemd timer
        mu4e-update-interval nil)
  (setq mu4e-hide-index-messages t)

  (setq mu4e-read-option-use-builtin nil
        mu4e-completing-read-function 'completing-read)

  (setq mu4e-search-results-limit -1
        mu4e-search-sort-field :date
        mu4e-search-sort-direction 'descending)

  (setq mu4e-org-support t)

  (setq mu4e-sent-messages-behavior 'sent)
  (setq mu4e-view-show-addresses t)
  (setq mu4e-split-view 'horizontal)

  (setq mu4e-index-lazy-check t)
  (setq mu4e-change-filenames-when-moving t) ; better for `mbsync'?
  (setq mu4e-display-update-status-in-modeline nil)
  (setq mu4e-view-show-images nil)
  (setq mu4e-headers-include-related nil)
  (setq mu4e-view-auto-mark-as-read t)

  (setq mu4e-compose-complete-addresses nil
        mu4e-compose-complete-only-personal t)

  (setq mu4e-compose-signature "Protesilaos Stavrou\nprotesilaos.com\n")
  (setq mu4e-compose-signature-auto-include t)

  (setq mu4e-context-policy 'pick-first
        mu4e-compose-context-policy nil)

  (setq mu4e-contexts
        `(,@(mapcar
             (lambda (context)
               (let ((address (prot-common-auth-get-field context :user)))
                 (make-mu4e-context
                  :name context
                  :match-func `(lambda (msg)
                                 (when msg
                                   (mu4e-message-contact-field-matches msg :to ,address)))
                  :vars `((user-mail-address . ,address)
                          (mu4e-trash-folder . ,(format "/%s/Trash" context))
                          (mu4e-sent-folder . ,(format "/%s/Sent" context))))))
             '("pub" "inf" "prv"))))

  ;; 2024-02-26 10:34 +0200 WORK-IN-PROGRESS
  (setq mu4e-bookmarks
        `((:name "All unread messages" :query "g:unread AND NOT g:trashed" :key ?a)
          (:name "All messages" :query "*" :key ?A)
          ( :name "Personal unread"
            :query "contact:/@protesilaos/ or contact:protesilaos AND g:unread AND NOT contact:/@.*gnu/"
            :key ?u
            :favorite t)
          ( :name "Personal inbox"
            :query "contact:/@protesilaos/ or contact:protesilaos AND NOT contact:/@.*gnu/"
            :key ?U
            :favorite t)
          ( :name "Mailing list unread"
            :query "contact:/@.*gnu/ AND g:unread"
            :key ?m)
          ( :name "Mailing list inbox"
            :query "contact:/@.*gnu/"
            :key ?M)))

  (defun prot/mu4e (&rest args)
    (cl-letf (((symbol-function 'display-buffer-full-frame) #'display-buffer-same-window))
      (apply args)))

  (advice-add #'mu4e-display-buffer :around #'prot/mu4e)

  (defun prot/mu4e-no-header-line ()
    (setq-local header-line-format nil))

  (add-hook 'mu4e-headers-mode-hook #'prot/mu4e-no-header-line)

  (prot-emacs-keybind global-map
    "C-c m" #'mu4e
    "C-x m" #'mu4e-compose-new) ; override `compose-mail'

  (prot-emacs-keybind mu4e-headers-mode-map
    "!" #'mu4e-headers-mark-for-flag
    "?" #'mu4e-headers-mark-for-unflag
    "r" #'mu4e-headers-mark-for-read
    "d" #'mu4e-headers-mark-for-delete ; I do not use the trash
    "u" #'mu4e-headers-mark-for-unread
    "m" #'mu4e-headers-mark-for-unmark
    "M" #'mu4e-mark-unmark-all)

  (prot-emacs-keybind mu4e-headers-mode-map
    "!" #'mu4e-headers-mark-for-flag
    "?" #'mu4e-headers-mark-for-unflag
    "r" #'mu4e-headers-mark-for-read
    "u" #'mu4e-headers-mark-for-unread
    "m" #'mu4e-headers-mark-for-unmark
    "M" #'mu4e-mark-unmark-all)

  (prot-emacs-keybind mu4e-view-mode-map
    ;; NOTE 2024-02-26: with mu 1.12.0 there is a "wide" reply that I
    ;; would bind to r and the regular reply to R.
    "r" #'mu4e-compose-reply
    "w" #'mu4e-copy-thing-at-point
    "s" #'mu4e-view-save-attachments
    "S" #'mu4e-view-raw-message ; "source" mnemonic
    "u" #'mu4e-view-mark-for-unread
    "U" #'mu4e-view-mark-for-read
    "d" #'mu4e-view-mark-for-delete ; overwrite mu4e-view-mark-for-trash
    "!" #'mu4e-view-mark-for-flag
    "?" #'mu4e-view-mark-for-unflag))

(provide 'prot-emacs-mu4e)

5.11.8. The deprecated prot-emacs-mail.el submodule for Gnus (prot-emacs-gnus.el)

I have long now stopped using Gnus (Overview of my email setup (mbsync, msmtp, mail indexer, and MUA)). I keep this here in case I need to test the setup.

;; This is deprecated code as I stopped using Gnus a long time ago.  I
;; keep it here for when I need to test it for my themes (`ef-themes',
;; `modus-themes', `standard-themes').


(use-package gnus
  :ensure nil
  :config
  (require 'gnus-sum)
  (require 'gnus-dired)
  (require 'gnus-topic)

;;; accounts
  (setq gnus-select-method '(nnnil ""))
  (setq gnus-secondary-select-methods
        '((nntp "news.gwene.org")
          ;; ;;  NOTE 2021-05-13: Switched to notmuch.
          ;; (nnmaildir "prv" (directory "~/.mail/prv")
          ;;  (gnus-search-engine gnus-search-notmuch ; this feature is from Emacs 28
 		  ;;   		           (remove-prefix "~/.mail/prv")))
          ;; (nnmaildir "inf" (directory "~/.mail/inf")
          ;;  (gnus-search-engine gnus-search-notmuch
 		  ;;   		           (remove-prefix "~/.mail/inf")))
          ;; (nnmaildir "pub" (directory "~/.mail/pub")
          ;;  (gnus-search-engine gnus-search-notmuch
 		  ;;   		           (remove-prefix "~/.mail/pub")))
          ))

  (setq gnus-search-use-parsed-queries nil) ; Emacs 28

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

  (setq gnus-gcc-mark-as-read t)
  (setq gnus-agent t)
  (setq gnus-novice-user nil)           ; careful with this
  ;; checking sources
  (setq gnus-check-new-newsgroups 'ask-server)
  (setq gnus-read-active-file 'some)
  ;; dribble
  (setq gnus-use-dribble-file t)
  (setq gnus-always-read-dribble-file t)
;;; agent
  (setq gnus-agent-article-alist-save-format 1)  ; uncompressed
  (setq gnus-agent-cache t)
  (setq gnus-agent-confirmation-function 'y-or-n-p)
  (setq gnus-agent-consider-all-articles nil)
  (setq gnus-agent-directory "~/News/agent/")
  (setq gnus-agent-enable-expiration 'ENABLE)
  (setq gnus-agent-expire-all nil)
  (setq gnus-agent-expire-days 30)
  (setq gnus-agent-mark-unread-after-downloaded t)
  (setq gnus-agent-queue-mail t)        ; queue if unplugged
  (setq gnus-agent-synchronize-flags nil)
;;; article
  (setq gnus-article-browse-delete-temp 'ask)
  (setq gnus-article-over-scroll nil)
  (setq gnus-article-show-cursor t)
  (setq gnus-article-sort-functions
        '((not gnus-article-sort-by-number)
          (not gnus-article-sort-by-date)))
  (setq gnus-article-truncate-lines nil)
  (setq gnus-html-frame-width 80)
  (setq gnus-html-image-automatic-caching t)
  (setq gnus-inhibit-images t)
  (setq gnus-max-image-proportion 0.7)
  (setq gnus-treat-display-smileys nil)
  (setq gnus-article-mode-line-format "%G %S %m")
  (setq gnus-visible-headers
        '("^From:" "^To:" "^Cc:" "^Subject:" "^Newsgroups:" "^Date:"
          "Followup-To:" "Reply-To:" "^Organization:" "^X-Newsreader:"
          "^X-Mailer:"))
  (setq gnus-sorted-header-list gnus-visible-headers)
  (setq gnus-article-x-face-too-ugly ".*") ; all images in headers are outright annoying---disabled!
;;; async
  (setq gnus-asynchronous t)
  (setq gnus-use-article-prefetch 15)
;;; group
  (setq gnus-level-subscribed 6)
  (setq gnus-level-unsubscribed 7)
  (setq gnus-level-zombie 8)
  (setq gnus-activate-level 1)
  (setq gnus-list-groups-with-ticked-articles nil)
  (setq gnus-group-sort-function
        '((gnus-group-sort-by-unread)
          (gnus-group-sort-by-alphabet)
          (gnus-group-sort-by-rank)))
  (setq gnus-group-line-format "%M%p%P%5y:%B%(%g%)\n")
  (setq gnus-group-mode-line-format "%%b")
  (setq gnus-topic-display-empty-topics nil)
;;; summary
  (setq gnus-auto-select-first nil)
  (setq gnus-summary-ignore-duplicates t)
  (setq gnus-suppress-duplicates t)
  (setq gnus-save-duplicate-list t)
  (setq gnus-summary-goto-unread nil)
  (setq gnus-summary-make-false-root 'adopt)
  (setq gnus-summary-thread-gathering-function
        'gnus-gather-threads-by-subject)
  (setq gnus-summary-gather-subject-limit 'fuzzy)
  (setq gnus-thread-sort-functions
        '((not gnus-thread-sort-by-date)
          (not gnus-thread-sort-by-number)))
  (setq gnus-subthread-sort-functions
        'gnus-thread-sort-by-date)
  (setq gnus-thread-hide-subtree nil)
  (setq gnus-thread-ignore-subject nil)
  (setq gnus-user-date-format-alist
        '(((gnus-seconds-today) . "Today at %R")
          ((+ (* 60 60 24) (gnus-seconds-today)) . "Yesterday, %R")
          (t . "%Y-%m-%d %R")))

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

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

  (add-hook 'dired-mode-hook #'gnus-dired-mode) ; dired integration
  (add-hook 'gnus-group-mode-hook #'gnus-topic-mode)
  (add-hook 'gnus-select-group-hook #'gnus-group-set-timestamp)

  (dolist (mode '(gnus-group-mode-hook gnus-summary-mode-hook gnus-browse-mode-hook))
    (add-hook mode #'hl-line-mode))

  ;; ;;  NOTE 2021-05-13: Switched to notmuch.
  ;; (define-key global-map (kbd "C-c m") #'gnus)
  (let ((map gnus-article-mode-map))
    (define-key map (kbd "i") #'gnus-article-show-images)
    (define-key map (kbd "s") #'gnus-mime-save-part)
    (define-key map (kbd "o") #'gnus-mime-copy-part))
  (let ((map gnus-group-mode-map))       ; I always use `gnus-topic-mode'
    (define-key map (kbd "n") #'gnus-group-next-group)
    (define-key map (kbd "p") #'gnus-group-prev-group)
    (define-key map (kbd "M-n") #'gnus-topic-goto-next-topic)
    (define-key map (kbd "M-p") #'gnus-topic-goto-previous-topic))
  (let ((map gnus-summary-mode-map))
    (define-key map (kbd "<delete>") #'gnus-summary-delete-article)
    (define-key map (kbd "n") #'gnus-summary-next-article)
    (define-key map (kbd "p") #'gnus-summary-prev-article)
    (define-key map (kbd "N") #'gnus-summary-next-unread-article)
    (define-key map (kbd "P") #'gnus-summary-prev-unread-article)
    (define-key map (kbd "M-n") #'gnus-summary-next-thread)
    (define-key map (kbd "M-p") #'gnus-summary-prev-thread)
    (define-key map (kbd "C-M-n") #'gnus-summary-next-group)
    (define-key map (kbd "C-M-p") #'gnus-summary-prev-group)
    (define-key map (kbd "C-M-^") #'gnus-summary-refer-thread)))

(use-package nnmail
  :ensure nil
  :config
  (setq nnmail-expiry-wait 30))         ; careful with this

(provide 'prot-emacs-gnus)

5.11.9. Overview of my email setup (mbsync, msmtp, mail indexer, and MUA)

I have a regular IMAP email setup. This is the protocol that allows the server to retain messages, which is of practical benefit for accessing mail on multiple machines.

A copy of my server’s contents needs to be stored locally. This is valuable for backup purposes. It also provides the option to move to another email service provider by just syncing the local data to the new location while deleting the old one. More importantly though, local files can be indexed, granting us access to a powerful search mechanism (more below).

The program that stores email locally is called mbsync. For whatever reason, the system package is isync. This is true for Arch Linux and Debian. (I also heard it is the case with homebrew, but please check.)

With mbsync we synchronise the state from the server (known as the “far” side) to our computer (the “near” side). The directionality can be one-way, far to near or near to far. Or it can be two-way, as is the case on my end. Concretely, what I have on my computer is mirrored on the server.

The format mbsync uses is called maildir. This is in contrast to the Unix mbox. I do not know about the pros and cons of each format, other than the fact that maildir has been working flawlessly for me for years—and email is my primary medium of communication online (Contents of my .mbsyncrc).

The local files are indexed with a “mail indexer” system program. There are a few of those. The ones I have used are mu and notmuch. Both are built on top of the xapian search engine software and both offer the same features. The noticeable differences exist at the level of the email client, else the Mail User Agent (MUA), which I cover further below.

Mail indexers create a database out of a local email directory for the purposes of retrieving data from it. In practical terms, we can run a search like notmuch search from:protesilaos (with notmuch) or mu find from:protesilaos (with mu) to produce a list of all messages whose From mail header matches the given name. The search terms are quite sophisticated, accepting tags, date ranges, and even queries for message contents.

Indexation is an archiving process. We do not need a Mail User Agent (MUA) to handle all the interactivity involved. There are cases where we can have some old messages in such a read-only state. Though we most probably need to deal with email in present time, hence the need for a MUA.

The MUA is responsible for all the interactive aspects of dealing with email: show a list of messages, display an individual email, handle message composition, and others such as tagging, searching, downloading… In short, the mail user agent is what we normally understand as the “email app”.

The mu and notmuch indexers both come with their own Emacs MUA as part of their system package. For the former, the Emacs package is known as mu4e (The deprecated prot-emacs-mail.el submodule for mu4e (prot-emacs-mu4e.el)), while the latter’s is notmuch or notmuch.el to differentiate it from the system program (The prot-emacs-email.el submodule for notmuch (prot-emacs-notmuch.el)).

I have used both MUAs extensively and settled with notmuch.el simply because it has a threaded view: mu4e uses a tree view, which is fine if you are not dealing with lots of emails, but is harder to spot what you need when conversations start getting longer and you have plenty of them. I never use the tree view provided by notmuch.el, unless I am testing something.

Behind the scenes, the MUA needs some other program to actually send messages. For this purpose, I set up the msmtp system package, which defines a handler for the standard sendmail system utility. Not to bother you with the technicalities: msmtp is basically the same as mbsync but for sending instead of fetching.

This all sounds complex to anyone who expects email to be encapsulated in a single application. What we have here is the Unix paradigm in action. Need another program for mail indexing? Good, just swap out mbsync for offlineimap and continue using the rest of the tool kit. This is the sort of idea.

Overall, I am happy with my setup. Email is integral to my online communications: I need it to be reliable and efficient. Having the MUA in Emacs is extra nice because it integrates an essential part of my daily computing with the rest of what I do (The prot-emacs-email.el module).

5.11.9.1. Contents of my .mbsyncrc

This file is stored in the home directory at ~/.mbsyncrc (it can be placed elsewhere, but I do not mind it there). Notice that the user name and password are not written in the file directly but are instead retrieved by a system call that reads the encrypted ~/.authinfo.gpg file (The prot-emacs-email.el basic settings (including authinfo)).

What you see there is three separate email accounts.

# Gandi
####################################################################################################
IMAPAccount gandi
Host mail.gandi.net
UserCmd "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gandi/ { print $(NF-2); exit; }'"
PassCmd "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gandi/ { print $NF; exit; }'"
SSLType IMAPS

IMAPStore gandi-remote
Account gandi

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

Channel gandi
Far :gandi-remote:
Near :gandi-local:
# Include everything
Patterns *
Sync All
Create Both
Remove Both
Expunge Both
SyncState *

# # Gmail
# ####################################################################################################
# IMAPAccount gmail
# Host imap.gmail.com
# UserCmd "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gmail/ { print $(NF-2); exit; }'"
# PassCmd "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gmail/ { print $NF; exit; }'"
# SSLType IMAPS
#
# IMAPStore gmail-remote
# Account gmail
#
# MaildirStore gmail-local
# Subfolders Verbatim
# # The trailing "/" is important
# Path ~/.mail/gmail/
# Inbox ~/.mail/gmail/Inbox
#
# Channel gmail
# Far :gmail-remote:
# Near :gmail-local:
# # Include everything
# Patterns *
# Sync All
# Create Near
# Remove Near
# Expunge Near
# SyncState *

# # Mailbox
# ####################################################################################################
# IMAPAccount mailbox
# Host imap.mailbox.org
# UserCmd "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/mailbox/ { print $(NF-2); exit; }'"
# PassCmd "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/mailbox/ { print $NF; exit; }'"
# SSLType IMAPS
#
# IMAPStore mailbox-remote
# Account mailbox
#
# MaildirStore mailbox-local
# Subfolders Verbatim
# # The trailing "/" is important
# Path ~/.mail/mailbox/
# Inbox ~/.mail/mailbox/Inbox
#
# Channel mailbox
# Far :mailbox-remote:
# Near :mailbox-local:
# # Include everything
# Patterns *
# Sync All
# Create Near
# Remove Near
# Expunge Near
# SyncState *
5.11.9.2. Contents of my msmtp configuration file

This file is stored at ~/.config/msmtp/config (well, unless you change the XDG directory but then you know what you are doing). Just as with mbsync, I retrieve the user name and password via commands that read from the ~/.authinfo file (Contents of my .mbsyncrc).

# See my mbsync config, which is reflected here.
# https://github.com/protesilaos/dotfiles

defaults
auth on
protocol smtp
tls on
tls_starttls on

# Gandi
####################################################################################################
account gandi
host mail.gandi.net
port 587
eval echo from $(gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gandi/ { print $(NF-2); exit; }')
eval echo user $(gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gandi/ { print $(NF-2); exit; }')
passwordeval "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gandi/ { print $NF; exit; }'"

# # Gmail
# ####################################################################################################
# account gmail
# host smtp.gmail.com
# port 587
# eval echo from $(gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gmail/ { print $(NF-2); exit; }')
# eval echo user $(gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gmail/ { print $(NF-2); exit; }')
# passwordeval "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/gmail/ { print $NF; exit; }'"

# # Mailbox
# ####################################################################################################
# account mailbox
# host smtp.mailbox.org
# port 465
# eval echo from $(gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/mailbox/ { print $(NF-2); exit; }')
# eval echo user $(gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/mailbox/ { print $(NF-2); exit; }')
# passwordeval "gpg -q --for-your-eyes-only -d ~/.authinfo.gpg | awk -F ' ' '/mailbox/ { print $NF; exit; }'"


# Set a default account (I copied from man msmtp)
account default : gandi

5.12. TODO The prot-emacs-web.el module

5.12.1. TODO The prot-emacs-web.el settings about following links (browse-url)

;;;; `browse-url'
(use-package browse-url
  :ensure nil
  :defer t
  :config
  (setq browse-url-browser-function 'eww-browse-url)
  (setq browse-url-secondary-browser-function 'browse-url-default-browser))

5.12.2. TODO The prot-emacs-web.el settings about buttonising links (goto-addr)

;;;; `goto-addr'
(use-package goto-addr
  :ensure nil
  :defer t
  :config
  (setq goto-address-url-face 'link)
  (setq goto-address-url-mouse-face 'highlight)
  (setq goto-address-mail-face nil)
  (setq goto-address-mail-mouse-face 'highlight))

5.12.3. TODO The prot-emacs-web.el settings about the Simple HTML Renderer (shr)

;;;; `shr' (Simple HTML Renderer)
(use-package shr
  :ensure nil
  :defer t
  :config
  (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 fill-column)          ; check `prot-eww-readable'
  (setq shr-max-width fill-column)
  (setq shr-discard-aria-hidden t)
  (setq shr-fill-text nil)              ; Emacs 31
  (setq shr-cookie-policy nil))

5.12.4. TODO The prot-emacs-web.el settings about browser cookies

;;;; `url-cookie'
(use-package url-cookie
  :ensure nil
  :defer t
  :config
  (setq url-cookie-untrusted-urls '(".*")))

5.12.5. TODO The prot-emacs-web.el settings about the web browser (eww)

;;;; `eww' (Emacs Web Wowser)
(use-package eww
  :ensure nil
  :commands (eww)
  :bind
  ( :map eww-link-keymap
    ("v" . nil) ; stop overriding `eww-view-source'
    :map eww-mode-map
    ("L" . eww-list-bookmarks)
    :map dired-mode-map
    ("E" . eww-open-file) ; to render local HTML files
    :map eww-buffers-mode-map
    ("d" . eww-bookmark-kill)   ; it actually deletes
    :map eww-bookmark-mode-map
    ("d" . eww-bookmark-kill)) ; same
  :config
  (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))

5.12.6. TODO The prot-emacs-web.el extras for eww (prot-eww.el)

;;;; `prot-eww' extras
(use-package prot-eww
  :ensure nil
  :after eww
  :config
  (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)

  (prot-emacs-keybind prot-eww-map
    "b" #'prot-eww-visit-bookmark
    "e" #'prot-eww-browse-dwim
    "s" #'prot-eww-search-engine)
  (prot-emacs-keybind eww-mode-map
    "B" #'prot-eww-bookmark-page
    "D" #'prot-eww-download-html
    "F" #'prot-eww-find-feed
    "H" #'prot-eww-list-history
    "b" #'prot-eww-visit-bookmark
    "e" #'prot-eww-browse-dwim
    "o" #'prot-eww-open-in-other-window
    "E" #'prot-eww-visit-url-on-page
    "J" #'prot-eww-jump-to-url-on-page
    "R" #'prot-eww-readable
    "Q" #'prot-eww-quit))

5.12.7. TODO The prot-emacs-web.el RSS/Atom reader (elfeed)

;;; Elfeed feed/RSS reader
(use-package elfeed
  :ensure t
  :hook
  (elfeed-show-mode . visual-line-mode)
  :bind
  ("C-c e" . elfeed)
  :config
  (setq elfeed-use-curl nil)
  (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 "@2-weeks-ago +unread")
  (setq elfeed-sort-order 'descending)
  (setq elfeed-search-clipboard-type 'CLIPBOARD)
  (setq elfeed-search-title-max-width 100)
  (setq elfeed-search-title-min-width 30)
  (setq elfeed-search-trailing-width 25)
  (setq elfeed-show-truncate-long-urls t)
  (setq elfeed-show-unique-buffers t)
  (setq elfeed-search-date-format '("%F %R" 16 :left))

  (prot-emacs-keybind elfeed-search-mode-map
    "w" #'elfeed-search-yank
    "g" #'elfeed-update
    "G" #'elfeed-search-update--force)

  (define-key elfeed-show-mode-map (kbd "w") #'elfeed-show-yank))

(use-package prot-elfeed
  :ensure nil
  :after elfeed
  :bind
  ( :map elfeed-search-mode-map
    ("s" . prot-elfeed-search-tag-filter)
    ("+" . prot-elfeed-toggle-tag)
    :map elfeed-show-mode-map
    ("+" . prot-elfeed-toggle-tag))
  :hook
  (elfeed-search-mode . prot-elfeed-load-feeds)
  :config
  (setq prot-elfeed-tag-faces t)
  (prot-elfeed-fontify-tags))

5.12.8. TODO The prot-emacs-web.el settings for the IRC client

;;; Rcirc (IRC client)
(use-package rcirc
  :ensure nil
  :bind ("C-c i" . irc)
  :config
  (setq rcirc-server-alist
        `(("irc.libera.chat"
           :channels ("#emacs" "#rcirc")
           :port 6697
           :encryption tls
           :password ,(prot-common-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))
(provide 'prot-emacs-web)

5.13. The prot-emacs-which-key.el module

There is a user option in my setup to load this module (The init.el option to enable which-key).

When the which-key-mode is enabled, any incomplete key sequence will produce a popup at the lower part of the Emacs frame showing keys that complete the current sequence together with the name of the command they are invoking.

I personally do not use this, except when I need to do a demonstration.

Note that which-key is built into Emacs 30. If you are on a previous version, use :ensure t to install it from a package archive.

(use-package which-key
  :ensure nil ; built into Emacs 30
  :hook (after-init . which-key-mode)
  :config
  (setq which-key-separator "  ")
  (setq which-key-prefix-prefix "... ")
  (setq which-key-max-display-columns 3)
  (setq which-key-idle-delay 1.5)
  (setq which-key-idle-secondary-delay 0.25)
  (setq which-key-add-column-padding 1)
  (setq which-key-max-description-length 40))

(provide 'prot-emacs-which-key)

5.14. The prot-emacs-icons.el module (nerd-icons for various packages)

I define a user option to conditionally load icons in various parts of the Emacs interface (The init.el option to load icons (nerd-icons.el)). These are purely cosmetic. I normally work without them, though I do enable them for video demonstrations because they communicate more information, which some people may find helpful.

Remember that these packages do not automatically install any font files. The user must handle this step by invoking the command nerd-icons-install-fonts.

;;; Icons
(use-package nerd-icons
  :ensure t)

(use-package nerd-icons-completion
  :ensure t
  :after marginalia
  ;; FIXME 2024-09-01: For some reason this stopped working because it
  ;; macroexpands to `marginalia-mode' instead of
  ;; `marginalia-mode-hook'.  What is more puzzling is that this does
  ;; not happen in the next :hook...
  ;; :hook (marginalia-mode . nerd-icons-completion-marginalia-setup))
  :config
  (add-hook 'marginalia-mode-hook #'nerd-icons-completion-marginalia-setup))

(use-package nerd-icons-corfu
  :ensure t
  :after corfu
  :config
  (add-to-list 'corfu-margin-formatters #'nerd-icons-corfu-formatter))

(use-package nerd-icons-dired
  :ensure t
  :hook
  (dired-mode . nerd-icons-dired-mode))

(provide 'prot-emacs-icons)

6. The custom libraries of my configuration

Each of the following subsections is dedicated to an individual custom library. These are “packages” of mine that are only relevant to my Emacs configuration, even though they are designed in accordance with best practices for packaging Emacs Lisp code. Many of my public-facing packages for Emacs started out as custom libraries like these (The init.el arrangements for my own modules and custom libraries).

Please bear in mind that the code I write here is not necessarily as high quality as what I put in my public packages, meaning that I do not test it as much and do not try to make it perfect.

6.1. The prot-abbrev.el library

;;; prot-abbrev.el --- Functions for use with abbrev-mode -*- lexical-binding: t -*-

;; Copyright (C) 2024-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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:
;;
;; Functions for use with `abbrev-mode'.
;;
;; 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-abbrev ()
  "Functions for use with `abbrev-mode'."
  :group 'editing)

(defcustom prot-abbrev-time-specifier "%R"
  "Time specifier for `format-time-string'."
  :type 'string
  :group 'prot-abbrev)

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

(defun prot-abbrev-current-time ()
  "Insert the current time per `prot-abbrev-time-specifier'."
  (insert (format-time-string prot-abbrev-time-specifier)))

(defun prot-abbrev-current-date ()
  "Insert the current date per `prot-abbrev-date-specifier'."
  (insert (format-time-string prot-abbrev-date-specifier)))

(defun prot-abbrev-jitsi-link ()
  "Insert a Jitsi link."
  (insert (concat "https://meet.jit.si/" (format-time-string "%Y%m%dT%H%M%S"))))

(defvar prot-abbrev-update-html-history nil
  "Minibuffer history for `prot-abbrev-update-html-prompt'.")

(defun prot-abbrev-update-html-prompt ()
  "Minibuffer prompt for `prot-abbrev-update-html'.
Use completion among previous entries, retrieving their data from
`prot-abbrev-update-html-history'."
  (completing-read
   "Insert update for manual: "
   prot-abbrev-update-html-history
   nil nil nil 'prot-abbrev-update-html-history))

(defun prot-abbrev-update-html ()
  "Insert message to update NAME.html page, by prompting for NAME."
  (insert (format "Update %s.html" (prot-abbrev-update-html-prompt))))

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

6.2. The prot-coach.el library

;;; prot-coach.el --- Code for my personal coaching sessions -*- lexical-binding: t -*-

;; Copyright (C) 2023-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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:
;;
;; Code for my personal coaching sessions: <https://protesilaos.com/coach>.
;;
;; 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:

;;;; Jitsi link

(declare-function message-fetch-field "message" (header &optional first))
(declare-function notmuch-show-get-header "notmuch-show")

(defvar prot-coach--name-prompt-history nil
  "Minibuffer history of `prot-coach-name-prompt'.")

(defun prot-coach--name-prompt-default ()
  "Return default value for `prot-coach-name-prompt'."
  (when-let* ((from (cond
                     ((derived-mode-p 'message-mode)
                      (message-fetch-field "To"))
                     ((derived-mode-p 'notmuch-show-mode)
                      (notmuch-show-get-header :From)))))
    (string-clean-whitespace (car (split-string from "<")))))

(defun prot-coach-name-prompt ()
  "Prompt for student name."
  (let ((def (prot-coach--name-prompt-default)))
    (read-string
     (format-prompt "Name of student" def)
     nil 'prot-coach--name-prompt-history def)))

(defun prot-coach-get-identifier ()
  "Return identifier as YYYYmmddTHHMMSS.
This is the Denote identifier I use practically everywhere:
https://protesilaos.com/emacs/denote."
  (format-time-string "%Y%m%dT%H%M%S"))

;; I copied the "slug" functions from my denote.el.
(defconst prot-coach-excluded-punctuation-regexp "[][{}!@#$%^&*()=+'\"?,.\|;:~`‘’“”/]*"
  "Punctionation that is removed from file names.
We consider those characters illegal for our purposes.")

(defvar prot-coach-excluded-punctuation-extra-regexp nil
  "Additional punctuation that is removed from file names.
This variable is for advanced users who need to extend the
`prot-coach-excluded-punctuation-regexp'.  Once we have a better
understanding of what we should be omitting, we will update
things accordingly.")

(defun prot-coach--slug-no-punct (str)
  "Convert STR to a file name slug."
  (replace-regexp-in-string
   (concat prot-coach-excluded-punctuation-regexp
           prot-coach-excluded-punctuation-extra-regexp)
   "" str))

(defun prot-coach--slug-hyphenate (str)
  "Replace spaces and underscores with hyphens in STR.
Also replace multiple hyphens with a single one and remove any
leading and trailing hyphen."
  (replace-regexp-in-string
   "^-\\|-$" ""
   (replace-regexp-in-string
    "-\\{2,\\}" "-"
    (replace-regexp-in-string "_\\|\s+" "-" str))))

(defun prot-coach-sluggify (str)
  "Make STR an appropriate slug for file names and related."
  (downcase (prot-coach--slug-hyphenate (prot-coach--slug-no-punct str))))

(defun prot-coach--format-jitsi (name)
  "Format a Jitsi link with a unique identifier that includes NAME."
  (format "https://meet.jit.si/%s--%s"
          (prot-coach-get-identifier)
          (prot-coach-sluggify name)))

;;;###autoload
(defun prot-coach-jitsi-link (name)
  "Insert Jitsi link for NAME person."
  (interactive (list (prot-coach-name-prompt)))
  (insert (prot-coach--format-jitsi name)))

;;;; Time tables

(require 'org)

;; FIXME 2023-03-29: Can this work with logbooks for repeatable
;; entries?
(defun prot-coach--get-deadline-and-close (&optional name)
  "Get time stamps of deadline and closed for optional NAME.
Omit entries with a CANCEL state."
  (org-with-point-at (point)
    (when-let* ((case-fold-search t)
                (heading (org-no-properties
                          (org-get-heading :no-tags :no-todo :no-priority :no-comment)))
                ((string-match-p (or name ".*") heading))
                (deadline (org-entry-get nil "DEADLINE"))
                (closed (org-entry-get nil "CLOSED"))
                ((not (string= (org-entry-get nil "TODO") "CANCEL"))))
      (list heading (format "%s" deadline) (format "%s" closed)))))

(defun prot-coach--print-table-with-sessions (name sessions)
  "Return time table for NAME given SESSIONS."
  (let ((buf (get-buffer-create (format "*prot-coach with %s" name))))
    (with-current-buffer buf
      (org-mode)
      (erase-buffer)
      (goto-char (point-min))
      (insert "# Type C-c C-c in the TBLFM line to produce the table\n\n")
      (insert "* Cost without counting minutes\n")
      (insert (concat
               "\n"
               "| Description | Started | Closed | Days | Hours | Cost (EUR) |" "\n"
               "|-------------+---------+--------+------+-------+------------|" "\n"))
      (mapc
       (lambda (session)
         (insert (format "| %s | %s | %s |\n"
                         (nth 0 session)
                         (nth 1 session)
                         (nth 2 session))))
       sessions)
      (insert (concat "|-+" "\n"
                      "| " "\n"
                      "#+TBLFM: $4=date($3) - date($2) :: $5=86400 * $4;U"
                      ":: @>$5=vsum(@<<..@>>);U :: $6=$5*20;N :: @>$>=vsum(@<<..@>>)"
                      ":: @>$1='(length (org-lookup-all \".*\" '(@<<$1..@>>$1) nil 'string-match-p))"
                      "\n"))
      (org-table-recalculate-buffer-tables))
    (pop-to-buffer buf)))

;;;###autoload
(defun prot-coach-done-sessions-with-person ()
  "Produce buffer with time table for a given student."
  (declare (interactive-only t))
  (interactive)
  (when-let* ((file (buffer-file-name))
              (dir (file-name-directory file))
              ((string-match-p "coach" file))
              ((or (member file org-agenda-files)
                   ;; NOTE 2023-03-29: This assumes the file paths are
                   ;; absolute and end with a directory delimiter.
                   (member dir org-agenda-files)))
              (name (prot-coach-name-prompt)))
    (let (sessions)
      (org-map-entries
       (lambda ()
         (when-let* ((entry (prot-coach--get-deadline-and-close name)))
           (push entry sessions))))
      (prot-coach--print-table-with-sessions name sessions))))

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

6.3. The prot-comment.el library

;;; prot-comment.el --- Extensions newcomment.el for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2021-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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 'newcomment)
(require 'prot-common)

(defgroup prot-comment ()
  "Extensions for newcomment.el."
  :group 'comment)

(defcustom prot-comment-keywords
  '("TODO" "NOTE" "XXX" "REVIEW" "FIXME")
  "List of strings with keywords used by `prot-comment-timestamp-keyword'."
  :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 (n)
  "Comment N lines, defaulting to the current one.
When the region is active, comment its lines instead."
  (interactive "p")
  (if (use-region-p)
      (comment-or-uncomment-region (region-beginning) (region-end))
    (comment-line n)))

(make-obsolete 'prot-comment-comment-dwim 'prot-comment "2023-09-28")

(defvar prot-comment--keyword-hist '()
  "Minibuffer history of `prot-comment--keyword-prompt'.")

(defun prot-comment--keyword-prompt (keywords)
  "Prompt for candidate among KEYWORDS (per `prot-comment-timestamp-keyword')."
  (let ((def (car prot-comment--keyword-hist)))
    (completing-read
     (format "Select keyword [%s]: " def)
     keywords nil nil nil 'prot-comment--keyword-hist def)))

(defun prot-comment--format-date (verbose)
  "Format date using `format-time-string'.
VERBOSE has the same meaning as `prot-comment-timestamp-keyword'."
  (format-time-string
   (if verbose
       prot-comment-timestamp-format-verbose
     prot-comment-timestamp-format-concise)))

(defun prot-comment--timestamp (keyword &optional verbose)
  "Format string using current time and KEYWORD.
VERBOSE has the same meaning as `prot-comment-timestamp-keyword'."
  (format "%s %s: " keyword (prot-comment--format-date verbose)))

(defun prot-comment--format-comment (string)
  "Format comment STRING per `prot-comment-timestamp-keyword'.
STRING is a combination of a keyword and a time stamp."
  (concat comment-start
          (make-string comment-add (string-to-char comment-start))
          comment-padding
          string
          comment-end))

(defun prot-comment--maybe-newline ()
  "Call `newline' if current line is not empty.
Check `prot-comment-timestamp-keyword' for the rationale."
  (unless (prot-common-line-regexp-p 'empty 1)
    (save-excursion (newline))))

;;;###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-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 and the line is not empty,
the comment is appended to the line 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'.  DELIMITER is the value
of `comment-start', as defined by the current major mode.

With optional VERBOSE argument (such as a prefix argument), use
an alternative date format, as specified by
`prot-comment-timestamp-format-verbose'."
  (interactive
   (list
    (prot-comment--keyword-prompt prot-comment-keywords)
    current-prefix-arg))
  (let ((string (prot-comment--timestamp keyword verbose))
        (beg (point)))
    (cond
     ((prot-common-line-regexp-p 'empty)
      (insert (prot-comment--format-comment string)))
     ((eq beg (line-beginning-position))
      (insert (prot-comment--format-comment string))
      (indent-region beg (point))
      (prot-comment--maybe-newline))
     (t
      (comment-indent t)
      (insert (concat " " string))))))

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

6.4. The prot-common.el library

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

;; Copyright (C) 2020-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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)
  (require 'cl-lib))

(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-reverse-percentage (number percent change-p)
  "Determine the original value of NUMBER given PERCENT.

CHANGE-P should specify the increase or decrease.  For simplicity,
nil means decrease while non-nil stands for an increase.

NUMBER must satisfy `numberp', while PERCENT must be `natnump'."
  (unless (numberp number)
    (user-error "NUMBER must satisfy numberp"))
  (unless (natnump percent)
    (user-error "PERCENT must satisfy natnump"))
  (let* ((pc (/ (float percent) 100))
         (pc-change (if change-p (+ 1 pc) pc))
         (n (if change-p pc-change (float (- 1 pc-change)))))
    ;; FIXME 2021-12-21: If float, round to 4 decimal points.
    (/ number n)))

;;;###autoload
(defun prot-common-percentage-change (n-original n-final)
  "Find percentage change between N-ORIGINAL and N-FINAL numbers.

When the percentage is not an integer, it is rounded to 4
floating points: 16.666666666666664 => 16.667."
  (unless (numberp n-original)
    (user-error "N-ORIGINAL must satisfy numberp"))
  (unless (numberp n-final)
    (user-error "N-FINAL must satisfy numberp"))
  (let* ((difference (float (abs (- n-original n-final))))
         (n (* (/ difference n-original) 100))
         (round (floor n)))
    ;; FIXME 2021-12-21: Any way to avoid the `string-to-number'?
    (if (> n round) (string-to-number (format "%0.4f" n)) round)))

;; REVIEW 2023-04-07 07:43 +0300: I just wrote the conversions from
;; seconds.  Hopefully they are correct, but I need to double check.
(defun prot-common-seconds-to-minutes (seconds)
  "Convert a number representing SECONDS to MM:SS notation."
  (let ((minutes (/ seconds 60))
        (seconds (% seconds 60)))
    (format "%.2d:%.2d" minutes seconds)))

(defun prot-common-seconds-to-hours (seconds)
  "Convert a number representing SECONDS to HH:MM:SS notation."
  (let* ((hours (/ seconds 3600))
         (minutes (/ (% seconds 3600) 60))
         (seconds (% seconds 60)))
    (format "%.2d:%.2d:%.2d" hours minutes seconds)))

;;;###autoload
(defun prot-common-seconds-to-minutes-or-hours (seconds)
  "Convert SECONDS to either minutes or hours, depending on the value."
  (if (> seconds 3599)
      (prot-common-seconds-to-hours seconds)
    (prot-common-seconds-to-minutes seconds)))

;;;###autoload
(defun prot-common-rotate-list-of-symbol (symbol)
  "Rotate list value of SYMBOL by moving its car to the end.
Return the first element before performing the rotation.

This means that if `sample-list' has an initial value of `(one
two three)', this function will first return `one' and update the
value of `sample-list' to `(two three one)'.  Subsequent calls
will continue rotating accordingly."
  (unless (symbolp symbol)
    (user-error "%s is not a symbol" symbol))
  (when-let* ((value (symbol-value symbol))
              (list (and (listp value) value))
              (first (car list)))
    (set symbol (append (cdr list) (list first)))
    first))

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

;; NOTE 2023-08-12: I tried the `clear-message-function', but it did
;; not work.  What I need is very simple and this gets the job done.
;;;###autoload
(defun prot-common-clear-minibuffer-message (&rest _)
  "Print an empty message to clear the echo area.
Use this as advice :after a noisy function."
  (message ""))

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

;;;###autoload
(defun prot-common-window-bounds ()
  "Return start and end points in the window as a cons cell."
  (cons (window-start) (window-end)))

;;;###autoload
(defun prot-common-page-p ()
  "Return non-nil if there is a `page-delimiter' in the buffer."
  (or (save-excursion (re-search-forward page-delimiter nil t))
      (save-excursion (re-search-backward page-delimiter nil t))))

;;;###autoload
(defun prot-common-window-small-p ()
  "Return non-nil if window is small.
Check if the `window-width' or `window-height' is less than
`split-width-threshold' and `split-height-threshold',
respectively."
  (or (and (numberp split-width-threshold)
           (< (window-total-width) split-width-threshold))
      (and (numberp split-height-threshold)
           (> (window-total-height) split-height-threshold))))

(defun prot-common-window-narrow-p ()
  "Return non-nil if window is narrow.
Check if the `window-width' is less than `split-width-threshold'."
  (and (numberp split-width-threshold)
       (< (window-total-width) split-width-threshold)))

;;;###autoload
(defun prot-common-three-or-more-windows-p (&optional frame)
  "Return non-nil if three or more windows occupy FRAME.
If FRAME is non-nil, inspect the current frame."
  (>= (length (window-list frame :no-minibuffer)) 3))

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

;;;###autoload
(defun prot-common-completion-category ()
  "Return completion category."
  (when-let* ((window (active-minibuffer-window)))
    (with-current-buffer (window-buffer window)
      (completion-metadata-get
       (completion-metadata (buffer-substring-no-properties
                             (minibuffer-prompt-end)
                             (max (minibuffer-prompt-end) (point)))
                            minibuffer-completion-table
                            minibuffer-completion-predicate)
       'category))))

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

;;;###autoload
(defun prot-common-completion-table-no-sort (category candidates)
  "Pass appropriate metadata CATEGORY to completion CANDIDATES.
Like `prot-common-completion-table' but also disable sorting."
  (lambda (string pred action)
    (if (eq action 'metadata)
        `(metadata (category . ,category)
                   (display-sort-function . ,#'identity))
      (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 (line-beginning-position))
    (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
   "~?\\<\\([-a-zA-Z0-9+&@#/%?=~_|!:,.;]*\\)"
   "[.@]"
   "\\([-a-zA-Z0-9+&@#/%?=~_|!:,.;]+\\)\\>/?")
  "Regular expression to match (most?) URLs or email addresses.")

(autoload 'auth-source-search "auth-source")

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

;;;###autoload
(defun prot-common-parse-file-as-list (file)
  "Return the contents of FILE as a list of strings.
Strings are split at newline characters and are then trimmed for
negative space.

Use this function to provide a list of candidates for
completion (per `completing-read')."
  (split-string
   (with-temp-buffer
     (insert-file-contents file)
     (buffer-substring-no-properties (point-min) (point-max)))
   "\n" :omit-nulls "[\s\f\t\n\r\v]+"))

(defun prot-common-ignore (&rest _)
  "Use this as override advice to make a function do nothing."
  nil)

;; NOTE 2023-06-02: The `prot-common-wcag-formula' and
;; `prot-common-contrast' are taken verbatim from my `modus-themes'
;; and renamed to have the prefix `prot-common-' instead of
;; `modus-themes-'.  This is all my code, of course, but I do it this
;; way to ensure that this file is self-contained in case someone
;; copies it.

;; This is the WCAG formula: <https://www.w3.org/TR/WCAG20-TECHS/G18.html>.
(defun prot-common-wcag-formula (hex)
  "Get WCAG value of color value HEX.
The value is defined in hexadecimal RGB notation, such #123456."
  (cl-loop for k in '(0.2126 0.7152 0.0722)
           for x in (color-name-to-rgb hex)
           sum (* k (if (<= x 0.03928)
                        (/ x 12.92)
                      (expt (/ (+ x 0.055) 1.055) 2.4)))))

;;;###autoload
(defun prot-common-contrast (c1 c2)
  "Measure WCAG contrast ratio between C1 and C2.
C1 and C2 are color values written in hexadecimal RGB."
  (let ((ct (/ (+ (prot-common-wcag-formula c1) 0.05)
               (+ (prot-common-wcag-formula c2) 0.05))))
    (max ct (/ ct))))

;;;; EXPERIMENTAL macros (not meant to be used anywhere)

;; TODO 2023-09-30: Try the same with `cl-defmacro' and &key
(defmacro prot-common-if (condition &rest consequences)
  "Separate the CONSEQUENCES of CONDITION semantically.
Like `if', `when', `unless' but done by using `:then' and `:else'
keywords.  The forms under each keyword of `:then' and `:else'
belong to the given subset of CONSEQUENCES.

- The absence of `:else' means: (if CONDITION (progn CONSEQUENCES)).
- The absence of `:then' means: (if CONDITION nil CONSEQUENCES).
- Otherwise: (if CONDITION (progn then-CONSEQUENCES) else-CONSEQUENCES)."
  (declare (indent 1))
  (let (then-consequences else-consequences last-kw)
    (dolist (elt consequences)
      (let ((is-keyword (keywordp elt)))
        (cond
         ((and (not is-keyword) (eq last-kw :then))
          (push elt then-consequences))
         ((and (not is-keyword) (eq last-kw :else))
          (push elt else-consequences))
         ((and is-keyword (eq elt :then))
          (setq last-kw :then))
         ((and is-keyword (eq elt :else))
          (setq last-kw :else)))))
    `(if ,condition
         ,(if then-consequences
              `(progn ,@(nreverse then-consequences))
            nil)
       ,@(nreverse else-consequences))))

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

6.5. The prot-dired.el library

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

;; Copyright (C) 2020-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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:

(require 'prot-common)
(require 'dired)
(require 'dired-aux)

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

;;;; Flat Dired listing

(defvar prot-dired-regexp-history nil
  "Minibuffer history of `prot-dired-regexp-prompt'.")

(defun prot-dired-regexp-prompt ()
  (let ((default (car prot-dired-regexp-history)))
    (read-regexp
     (format-prompt "Files matching REGEXP" default)
     default 'prot-dired-regexp-history)))

(defun prot-dired--get-files (regexp)
  "Return files matching REGEXP, recursively from `default-directory'."
  (directory-files-recursively default-directory regexp nil))

;;;###autoload
(defun prot-dired-search-flat-list (regexp)
  "Return a Dired buffer for files matching REGEXP.
Perform the search recursively from the current directory."
  (interactive (list (prot-dired-regexp-prompt)))
  (if-let* ((files (prot-dired--get-files regexp))
            (relative-paths (mapcar #'file-relative-name files)))
      (dired (cons (format "prot-flat-dired for `%s'" regexp) relative-paths))
    (error "No files matching `%s'" regexp)))

;;;; General commands

;; NOTE 2023-06-27: This user option is quick-and-dirty.  I prefer not
;; to have an option at all and simply do the right thing based on
;; `dired-guess-shell-alist-user'.
(defcustom prot-dired-always-external-regexp
  "\\(mkv\\|mp4\\|mp4\\|ogg\\|m4a\\|webm\\)"
  "Regular expression of file extensions to open externally.
The test is performed by `prot-dired-open-dwim', which then
defers to the `dired-guess-shell-alist-user'."
  :group 'prot-dired
  :type 'string)

;; NOTE 2023-06-27: This is a proof-of-concept.  See the previous
;; note.
(defun prot-dired-open-dwim (files)
  "Open FILES using the appropriate program."
  (interactive (list (dired-get-marked-files)))
  (if-let* ((extension (file-name-extension (car files)))
            ((string-match-p extension prot-dired-always-external-regexp))
            (guess (dired-guess-default files))
            (program (if (listp guess) (car guess) guess)))
      (dired-do-async-shell-command program nil files)
    (find-file (car files))))

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

(defvar prot-dired--find-grep-hist '()
  "Minibuffer history for `prot-dired-grep-marked-files'.")

;; Also see `prot-search-grep' from prot-search.el.
;;;###autoload
(defun prot-dired-grep-marked-files (regexp &optional arg)
  "Run `find' with `grep' for REGEXP on marked files.
When no files are marked or when just a single one is marked,
search the entire directory instead.

With optional prefix ARG target a single marked file.

We assume that there is no point in marking a single file and
running find+grep on its contents.  Visit it and call `occur' or
run grep directly on it without the whole find part."
  (interactive
   (list
    (read-string "grep for PATTERN (marked files OR current directory): " nil 'prot-dired--find-grep-hist)
    current-prefix-arg)
   dired-mode)
  (when-let* ((marks (dired-get-marked-files 'no-dir))
              (files (mapconcat #'identity marks " "))
              (args (if (or arg (length> marks 1))
                        ;; Thanks to Sean Whitton for pointing out an
                        ;; earlier superfluity of mine: we do not need
                        ;; to call grep through find when we already
                        ;; know the files we want to search in.  Check
                        ;; Sean's dotfiles:
                        ;; <https://git.spwhitton.name/dotfiles>.
                        ;;
                        ;; Any other errors or omissions are my own.
                        (format "grep -nH --color=auto %s %s" (shell-quote-argument regexp) files)
                      (concat
                       "find . -not " (shell-quote-argument "(")
                       " -wholename " (shell-quote-argument "*/.git*")
                       " -prune " (shell-quote-argument ")")
                       " -type f"
                       " -exec grep -nHE --color=auto " regexp " "
                       (shell-quote-argument "{}")
                       " " (shell-quote-argument ";") " "))))
    (compilation-start
     args
     'grep-mode
     (lambda (mode) (format "*prot-dired-find-%s for '%s'" mode regexp))
     t)))

;;;; 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 (line-end-position))
    (if (re-search-forward subdir nil t (or arg nil))
        (progn
          (goto-char (match-beginning 1))
          (goto-char (line-beginning-position)))
      (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 (line-beginning-position))
    (if (re-search-backward subdir nil t (or arg nil))
        (goto-char (line-beginning-position))
      (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 (+ (line-beginning-position) 2)
                                   (1- (line-end-position)))))

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

6.6. The prot-elfeed.el library

;;; prot-elfeed.el --- Elfeed extensions for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2021-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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:

;; NOTE 2022-06-08: This is old code.  There are things I would like to
;; improve.

;;
;; 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
  (thread-last user-emacs-directory (expand-file-name "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-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 '((t :inherit font-lock-warning-face))
  "Face for Elfeed entries tagged with `critical'.")

(defface prot-elfeed-entry-important '((t :inherit font-lock-constant-face))
  "Face for Elfeed entries tagged with `important'.")

(defface prot-elfeed-entry-personal '((t :inherit font-lock-variable-name-face))
  "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")

;;;; General commands

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

(defun prot-elfeed--format-tags (tags sign)
  "Prefix SIGN to each tag in TAGS."
  (mapcar (lambda (tag)
            (format "%s%s" sign tag))
          tags))

;;;###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 (prot-elfeed--format-tags db-tags "+"))
           (minus-tags (prot-elfeed--format-tags 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)))

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

6.7. The prot-embark.el library

;;; prot-embark.el --- Custom Embark keymaps -*- lexical-binding: t -*-

;; Copyright (C) 2023-2024  Protesilaos Stavrou

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

;; 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:
;;
;; 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 'embark)

(defvar-keymap prot-embark-general-map
  :parent embark-general-map
  "i" #'embark-insert
  "w" #'embark-copy-as-kill
  "E" #'embark-export
  "S" #'embark-collect
  "A" #'embark-act-all
  "DEL" #'delete-region)

(defvar-keymap prot-embark-url-map
  :parent embark-general-map
  "b" #'browse-url
  "d" #'embark-download-url
  "e" #'eww)

(defvar-keymap prot-embark-buffer-map
  :parent embark-general-map
  "k" #'prot-simple-kill-buffer
  "o" #'switch-to-buffer-other-window
  "e" #'ediff-buffers)

(add-to-list 'embark-post-action-hooks (list 'prot-simple-kill-buffer 'embark--restart))

(defvar-keymap prot-embark-file-map
  :parent embark-general-map
  "f" #'find-file
  "j" #'embark-dired-jump
  "c" #'copy-file
  "e" #'ediff-files)

(defvar-keymap prot-embark-identifier-map
  :parent embark-general-map
  "h" #'display-local-help
  "." #'xref-find-definitions
  "o" #'occur)

(defvar-keymap prot-embark-command-map
  :parent embark-general-map
  "h" #'describe-command
  "." #'embark-find-definition)

(defvar-keymap prot-embark-expression-map
  :parent embark-general-map
  "e" #'pp-eval-expression
  "m" #'pp-macroexpand-expression)

(defvar-keymap prot-embark-function-map
  :parent embark-general-map
  "h" #'describe-function
  "." #'embark-find-definition)

(defvar-keymap prot-embark-package-map
  :parent embark-general-map
  "h" #'describe-package
  "i" #'package-install
  "d" #'package-delete
  "r" #'package-reinstall
  "u" #'embark-browse-package-url
  "w" #'embark-save-package-url)

(defvar-keymap prot-embark-symbol-map
  :parent embark-general-map
  "h" #'describe-symbol
  "." #'embark-find-definition)

(defvar-keymap prot-embark-variable-map
  :parent embark-general-map
  "h" #'describe-variable
  "." #'embark-find-definition)

(defvar-keymap prot-embark-region-map
  :parent embark-general-map
  "a" #'align-regexp
  "D" #'delete-duplicate-lines
  "f" #'flush-lines
  "i" #'epa-import-keys-region
  "d" #'epa-decrypt-armor-in-region
  "r" #'repunctuate-sentences
  "s" #'sort-lines
  "u" #'untabify)

;; The minimal indicator shows cycling options, but I have no use
;; for those.  I want it to be silent.
(defun prot-embark-no-minimal-indicator ())
(advice-add #'embark-minimal-indicator :override #'prot-embark-no-minimal-indicator)

(defun prot-embark-act-no-quit ()
  "Call `embark-act' but do not quit after the action."
  (interactive)
  (let ((embark-quit-after-action nil))
    (call-interactively #'embark-act)))

(defun prot-embark-act-quit ()
  "Call `embark-act' and quit after the action."
  (interactive)
  (let ((embark-quit-after-action t))
    (call-interactively #'embark-act))
  (when (and (> (minibuffer-depth) 0)
             (derived-mode-p 'completion-list-mode))
    (abort-recursive-edit)))

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

6.8. The prot-eww.el library

;;; prot-eww.el --- Extensions for EWW -*- lexical-binding: t -*-

;; Copyright (C) 2021-2024  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 "30.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.

;; XXX NOTE XXX 2023-05-19: Much of this code is severely out of date.
;; I plan to review it.  DO NOT USE!!!

;;; Code:

(require 'shr)
(require 'eww)
(require 'url-parse)
(require 'prot-common)

(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 (equal "" (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
                "EWW 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)))

(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)
;; 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."
  (when (eq major-mode 'eww-mode)
    (plist-get eww-data :url)))

;;;###autoload
(defun prot-eww (url &optional arg)
  "Pass URL to appropriate client.
With optional ARG, use a new buffer."
  (interactive (list (browse-url-interactive-arg "URL: ") current-prefix-arg))
  (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-max))
      ;; 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-backward '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))))

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

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

6.9. The prot-marginalia.el library

;;; prot-marginalia.el --- Code for my custom mode line -*- lexical-binding: t -*-

;; Copyright (C) 2023-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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:
;;
;; 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 'bookmark)
(require 'package)

(defun prot-marginalia-truncate (string)
  "Truncate STRING to `fill-column', if necessary."
  (if (> (length string) fill-column)
      (concat (substring string 0 fill-column) "...")
    string))

(defun prot-marginalia-display (string)
  "Propertize the display of STRING for completion annotation purposes."
  (when (stringp string)
    (format "%s%s"
            (propertize " " 'display `(space :align-to 40))
            (propertize (prot-marginalia-truncate string)
                        'face 'completions-annotations))))

(defun prot-marginalia-bookmark (bookmark)
  "Annotate BOOKMARK with its file path."
  (when-let* ((bm (assoc bookmark (bound-and-true-p bookmark-alist)))
              (path (bookmark-get-filename bookmark)))
    (prot-marginalia-display path)))

(defun prot-marginalia-buffer (buffer)
  "Annotate BUFFER with the return value of function `buffer-file-name'."
  (if-let* ((name (buffer-file-name (get-buffer buffer))))
      (prot-marginalia-display (abbreviate-file-name name))
    (prot-marginalia-display (format "%s" (buffer-local-value 'major-mode (get-buffer buffer))))))

(defun prot-marginalia-package (package)
  "Annotate PACKAGE with its summary."
  (when-let* ((pkg-alist (bound-and-true-p package-alist))
              (pkg (intern-soft package))
              (desc (or (when (package-desc-p pkg) pkg)
                        (car (alist-get pkg pkg-alist))
                        (if-let* ((built-in (assq pkg package--builtins)))
                            (package--from-builtin built-in)
                          (car (alist-get pkg package-archive-contents))))))
    (prot-marginalia-display (package-desc-summary desc))))

(defun prot-marginalia--get-symbol-doc (symbol)
  "Return documentation string according to SYMBOL type."
  (cond
   ((or (functionp symbol) (macrop symbol))
    (documentation symbol))
   (t
    (get symbol 'variable-documentation))))

(defun prot-marginalia--first-line-documentation (symbol)
  "Return first line of SYMBOL documentation string."
  (when-let* ((doc-string (prot-marginalia--get-symbol-doc symbol))
              ((stringp doc-string))
              ((not (string-empty-p doc-string))))
    (car (split-string doc-string "[?!.\n]"))))

(defun prot-marginalia-symbol (symbol)
  "Annotate SYMBOL with its documentation string."
  (when-let* ((sym (intern-soft symbol))
              (doc-string (prot-marginalia--first-line-documentation sym)))
    (prot-marginalia-display doc-string)))

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

6.10. The prot-modeline.el library

[ Watch: write a custom mode line (2023-07-29) and customise mode line colours (2024-01-13). ]

;;; prot-modeline.el --- Code for my custom mode line -*- lexical-binding: t -*-

;; Copyright (C) 2023-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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:
;;
;; 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-modeline nil
  "Custom modeline that is stylistically close to the default."
  :group 'mode-line)

(defgroup prot-modeline-faces nil
  "Faces for my custom modeline."
  :group 'prot-modeline)

(defcustom prot-modeline-string-truncate-length 9
  "String length after which truncation should be done in small windows."
  :type 'natnum)

;;;; Faces

(defface prot-modeline-indicator-button nil
  "Generic face used for indicators that have a background.
Modify this face to, for example, add a :box attribute to all
relevant indicators (combines nicely with my `spacious-padding'
package).")

(defface prot-modeline-indicator-red
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#880000")
    (((class color) (min-colors 88) (background dark))
     :foreground "#ff9f9f")
    (t :foreground "red"))
  "Face for modeline indicators (e.g. see my `notmuch-indicator')."
  :group 'prot-modeline-faces)

(defface prot-modeline-indicator-red-bg
  '((default :inherit (bold prot-modeline-indicator-button))
    (((class color) (min-colors 88) (background light))
     :background "#aa1111" :foreground "white")
    (((class color) (min-colors 88) (background dark))
     :background "#ff9090" :foreground "black")
    (t :background "red" :foreground "black"))
  "Face for modeline indicators with a background."
  :group 'prot-modeline-faces)

(defface prot-modeline-indicator-green
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#005f00")
    (((class color) (min-colors 88) (background dark))
     :foreground "#73fa7f")
    (t :foreground "green"))
  "Face for modeline indicators (e.g. see my `notmuch-indicator')."
  :group 'prot-modeline-faces)

(defface prot-modeline-indicator-green-bg
  '((default :inherit (bold prot-modeline-indicator-button))
    (((class color) (min-colors 88) (background light))
     :background "#207b20" :foreground "white")
    (((class color) (min-colors 88) (background dark))
     :background "#77d077" :foreground "black")
    (t :background "green" :foreground "black"))
  "Face for modeline indicators with a background."
  :group 'prot-modeline-faces)

(defface prot-modeline-indicator-yellow
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#6f4000")
    (((class color) (min-colors 88) (background dark))
     :foreground "#f0c526")
    (t :foreground "yellow"))
  "Face for modeline indicators (e.g. see my `notmuch-indicator')."
  :group 'prot-modeline-faces)

(defface prot-modeline-indicator-yellow-bg
  '((default :inherit (bold prot-modeline-indicator-button))
    (((class color) (min-colors 88) (background light))
     :background "#805000" :foreground "white")
    (((class color) (min-colors 88) (background dark))
     :background "#ffc800" :foreground "black")
    (t :background "yellow" :foreground "black"))
  "Face for modeline indicators with a background."
  :group 'prot-modeline-faces)

(defface prot-modeline-indicator-blue
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#00228a")
    (((class color) (min-colors 88) (background dark))
     :foreground "#88bfff")
    (t :foreground "blue"))
  "Face for modeline indicators (e.g. see my `notmuch-indicator')."
  :group 'prot-modeline-faces)

(defface prot-modeline-indicator-blue-bg
  '((default :inherit (bold prot-modeline-indicator-button))
    (((class color) (min-colors 88) (background light))
     :background "#0000aa" :foreground "white")
    (((class color) (min-colors 88) (background dark))
     :background "#77aaff" :foreground "black")
    (t :background "blue" :foreground "black"))
  "Face for modeline indicators with a background."
  :group 'prot-modeline-faces)

(defface prot-modeline-indicator-magenta
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#6a1aaf")
    (((class color) (min-colors 88) (background dark))
     :foreground "#e0a0ff")
    (t :foreground "magenta"))
  "Face for modeline indicators (e.g. see my `notmuch-indicator')."
  :group 'prot-modeline-faces)

(defface prot-modeline-indicator-magenta-bg
  '((default :inherit (bold prot-modeline-indicator-button))
    (((class color) (min-colors 88) (background light))
     :background "#6f0f9f" :foreground "white")
    (((class color) (min-colors 88) (background dark))
     :background "#e3a2ff" :foreground "black")
    (t :background "magenta" :foreground "black"))
  "Face for modeline indicators with a background."
  :group 'prot-modeline-faces)

(defface prot-modeline-indicator-cyan
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#004060")
    (((class color) (min-colors 88) (background dark))
     :foreground "#30b7cc")
    (t :foreground "cyan"))
  "Face for modeline indicators (e.g. see my `notmuch-indicator')."
  :group 'prot-modeline-faces)

(defface prot-modeline-indicator-cyan-bg
  '((default :inherit (bold prot-modeline-indicator-button))
    (((class color) (min-colors 88) (background light))
     :background "#006080" :foreground "white")
    (((class color) (min-colors 88) (background dark))
     :background "#40c0e0" :foreground "black")
    (t :background "cyan" :foreground "black"))
  "Face for modeline indicators with a background."
  :group 'prot-modeline-faces)

(defface prot-modeline-indicator-gray
  '((t :inherit shadow))
  "Face for modeline indicators (e.g. see my `notmuch-indicator')."
  :group 'prot-modeline-faces)

(defface prot-modeline-indicator-gray-bg
  '((default :inherit (bold prot-modeline-indicator-button))
    (((class color) (min-colors 88) (background light))
     :background "#808080" :foreground "white")
    (((class color) (min-colors 88) (background dark))
     :background "#a0a0a0" :foreground "black")
    (t :inverse-video t))
  "Face for modeline indicatovrs with a background."
  :group 'prot-modeline-faces)

;;;; Common helper functions

(defun prot-modeline--string-truncate-p (str)
  "Return non-nil if STR should be truncated."
  (if (string-empty-p str)
      str
    (and (prot-common-window-narrow-p)
         (> (length str) prot-modeline-string-truncate-length)
         (not (one-window-p :no-minibuffer)))))

(defun prot-modeline--truncate-p ()
  "Return non-nil if truncation should happen.
This is a more general and less stringent variant of
`prot-modeline--string-truncate-p'."
  (and (prot-common-window-narrow-p)
       (not (one-window-p :no-minibuffer))))

(defun prot-modeline-string-cut-end (str)
  "Return truncated STR, if appropriate, else return STR.
Cut off the end of STR by counting from its start up to
`prot-modeline-string-truncate-length'."
  (if (prot-modeline--string-truncate-p str)
      (concat (substring str 0 prot-modeline-string-truncate-length) "...")
    str))

(defun prot-modeline-string-cut-beginning (str)
  "Return truncated STR, if appropriate, else return STR.
Cut off the beginning of STR by counting from its end up to
`prot-modeline-string-truncate-length'."
  (if (prot-modeline--string-truncate-p str)
      (concat "..." (substring str (- prot-modeline-string-truncate-length)))
    str))

(defun prot-modeline-string-cut-middle (str)
  "Return truncated STR, if appropriate, else return STR.
Cut off the middle of STR by counting half of
`prot-modeline-string-truncate-length' both from its beginning
and end."
  (let ((half (floor prot-modeline-string-truncate-length 2)))
    (if (prot-modeline--string-truncate-p str)
        (concat (substring str 0 half) "..." (substring str (- half)))
      str)))

(defun prot-modeline--first-char (str)
  "Return first character from STR."
  (substring str 0 1))

(defun prot-modeline-string-abbreviate (str)
  "Abbreviate STR individual hyphen or underscore separated words.
Also see `prot-modeline-string-abbreviate-but-last'."
  (if (prot-modeline--string-truncate-p str)
      (mapconcat #'prot-modeline--first-char (split-string str "[_-]") "-")
    str))

(defun prot-modeline-string-abbreviate-but-last (str nthlast)
  "Abbreviate STR, keeping NTHLAST words intact.
Also see `prot-modeline-string-abbreviate'."
  (if (prot-modeline--string-truncate-p str)
      (let* ((all-strings (split-string str "[_-]"))
             (nbutlast-strings (nbutlast (copy-sequence all-strings) nthlast))
             (last-strings (nreverse (ntake nthlast (nreverse (copy-sequence all-strings)))))
             (first-component (mapconcat #'prot-modeline--first-char nbutlast-strings "-"))
             (last-component (mapconcat #'identity last-strings "-")))
        (if (string-empty-p first-component)
            last-component
          (concat first-component "-" last-component)))
    str))

;;;; Keyboard macro indicator

(defvar-local prot-modeline-kbd-macro
    '(:eval
      (when (and (mode-line-window-selected-p) defining-kbd-macro)
        (propertize " KMacro " 'face 'prot-modeline-indicator-blue-bg)))
  "Mode line construct displaying `mode-line-defining-kbd-macro'.
Specific to the current window's mode line.")

;;;; Narrow indicator

(defvar-local prot-modeline-narrow
    '(:eval
      (when (and (mode-line-window-selected-p)
                 (buffer-narrowed-p)
                 (not (derived-mode-p 'Info-mode 'help-mode 'special-mode 'message-mode)))
        (propertize " Narrow " 'face 'prot-modeline-indicator-cyan-bg)))
  "Mode line construct to report the narrowed state of the current buffer.")

;;;; Input method

(defvar-local prot-modeline-input-method
    '(:eval
      (when current-input-method-title
        (propertize (format " %s " current-input-method-title)
                    'face 'prot-modeline-indicator-green-bg
                    'mouse-face 'mode-line-highlight)))
  "Mode line construct to report the multilingual environment.")

;;;; Buffer status

;; TODO 2023-07-05: What else is there beside remote files?  If
;; nothing, this must be renamed accordingly.
(defvar-local prot-modeline-buffer-status
    '(:eval
      (when (file-remote-p default-directory)
        (propertize " @ "
                    'face 'prot-modeline-indicator-red-bg
                    'mouse-face 'mode-line-highlight)))
  "Mode line construct for showing remote file name.")

;;;; Dedicated window

(defvar-local prot-modeline-window-dedicated-status
    '(:eval
      (when (window-dedicated-p)
        (propertize " = "
                    'face 'prot-modeline-indicator-gray-bg
                    'mouse-face 'mode-line-highlight)))
  "Mode line construct for dedicated window indicator.")

;;;; Buffer name and modified status

(defun prot-modeline-buffer-identification-face ()
  "Return appropriate face or face list for `prot-modeline-buffer-identification'."
  (let ((file (buffer-file-name)))
    (cond
     ((and (mode-line-window-selected-p)
           file
           (buffer-modified-p))
      '(italic mode-line-buffer-id))
     ((and file (buffer-modified-p))
      'italic)
     ((mode-line-window-selected-p)
      'mode-line-buffer-id))))

(defun prot-modeline--buffer-name ()
  "Return `buffer-name', truncating it if necessary.
See `prot-modeline-string-cut-middle'."
  (when-let* ((name (buffer-name)))
    (prot-modeline-string-cut-middle name)))

(defun prot-modeline-buffer-name ()
  "Return buffer name, with read-only indicator if relevant."
  (let ((name (prot-modeline--buffer-name)))
    (if buffer-read-only
        (format "%s %s" (char-to-string #xE0A2) name)
      name)))

(defun prot-modeline-buffer-name-help-echo ()
  "Return `help-echo' value for `prot-modeline-buffer-identification'."
  (concat
   (propertize (buffer-name) 'face 'mode-line-buffer-id)
   "\n"
   (propertize
    (or (buffer-file-name)
        (format "No underlying file.\nDirectory is: %s" default-directory))
    'face 'font-lock-doc-face)))

(defvar-local prot-modeline-buffer-identification
    '(:eval
      (propertize (prot-modeline-buffer-name)
                  'face (prot-modeline-buffer-identification-face)
                  'mouse-face 'mode-line-highlight
                  'help-echo (prot-modeline-buffer-name-help-echo)))
  "Mode line construct for identifying the buffer being displayed.
Propertize the current buffer with the `mode-line-buffer-id'
face.  Let other buffers have no face.")

;;;; Major mode

(defun prot-modeline-major-mode-indicator ()
  "Return appropriate propertized mode line indicator for the major mode."
  (let ((indicator (cond
                    ((derived-mode-p 'text-mode) "§")
                    ((derived-mode-p 'prog-mode) "λ")
                    ((derived-mode-p 'comint-mode) ">_")
                    (t "◦"))))
    (propertize indicator 'face 'shadow)))

(defun prot-modeline-major-mode-name ()
  "Return capitalized `major-mode' without the -mode suffix."
  (capitalize (string-replace "-mode" "" (symbol-name major-mode))))

(defun prot-modeline-major-mode-help-echo ()
  "Return `help-echo' value for `prot-modeline-major-mode'."
  (if-let* ((parent (get major-mode 'derived-mode-parent)))
      (format "Symbol: `%s'.  Derived from: `%s'" major-mode parent)
    (format "Symbol: `%s'." major-mode)))

(defvar-local prot-modeline-major-mode
    (list
     (propertize "%[" 'face 'prot-modeline-indicator-red)
     '(:eval
       (concat
        (prot-modeline-major-mode-indicator)
        " "
        (propertize
         (prot-modeline-string-abbreviate-but-last
          (prot-modeline-major-mode-name)
          2)
         'mouse-face 'mode-line-highlight
         'help-echo (prot-modeline-major-mode-help-echo))))
     (propertize "%]" 'face 'prot-modeline-indicator-red))
  "Mode line construct for displaying major modes.")

(defvar-local prot-modeline-process
    (list '("" mode-line-process))
  "Mode line construct for the running process indicator.")

;;;; Git branch and diffstat

(declare-function vc-git--symbolic-ref "vc-git" (file))

(defun prot-modeline--vc-branch-name (file backend)
  "Return capitalized VC branch name for FILE with BACKEND."
  (when-let* ((rev (vc-working-revision file backend))
              (branch (or (vc-git--symbolic-ref file)
                          (substring rev 0 7))))
    (capitalize branch)))

;; NOTE 2023-07-27: This is a good idea, but it hardcodes Git, whereas
;; I want a generic VC method.  Granted, I only use Git but I still
;; want it to work as a VC extension.

;; (defun prot-modeline-diffstat (file)
;;   "Return shortened Git diff numstat for FILE."
;;   (when-let* ((output (shell-command-to-string (format "git diff --numstat %s" file)))
;;               (stats (split-string output "[\s\t]" :omit-nulls "[\s\f\t\n\r\v]+"))
;;               (added (nth 0 stats))
;;               (deleted (nth 1 stats)))
;;     (cond
;;      ((and (equal added "0") (equal deleted "0"))
;;       "")
;;      ((and (not (equal added "0")) (equal deleted "0"))
;;       (propertize (format "+%s" added) 'face 'shadow))
;;      ((and (equal added "0") (not (equal deleted "0")))
;;       (propertize (format "-%s" deleted) 'face 'shadow))
;;      (t
;;       (propertize (format "+%s -%s" added deleted) 'face 'shadow)))))

(declare-function vc-git-working-revision "vc-git" (file))

(defvar prot-modeline-vc-map
  (let ((map (make-sparse-keymap)))
    (define-key map [mode-line down-mouse-1] 'vc-diff)
    (define-key map [mode-line down-mouse-3] 'vc-root-diff)
    map)
  "Keymap to display on VC indicator.")

(defun prot-modeline--vc-help-echo (file)
  "Return `help-echo' message for FILE tracked by VC."
  (format "Revision: %s\nmouse-1: `vc-diff'\nmouse-3: `vc-root-diff'"
          (vc-working-revision file)))

(defun prot-modeline--vc-text (file branch &optional face)
  "Prepare text for Git controlled FILE, given BRANCH.
With optional FACE, use it to propertize the BRANCH."
  (concat
   (propertize (char-to-string #xE0A0) 'face 'shadow)
   " "
   (propertize branch
               'face face
               'mouse-face 'mode-line-highlight
               'help-echo (prot-modeline--vc-help-echo file)
               'local-map prot-modeline-vc-map)
   ;; " "
   ;; (prot-modeline-diffstat file)
   ))

(defun prot-modeline--vc-details (file branch &optional face)
  "Return Git BRANCH details for FILE, truncating it if necessary.
The string is truncated if the width of the window is smaller
than `split-width-threshold'."
  (prot-modeline-string-cut-end
   (prot-modeline--vc-text file branch face)))

(defvar prot-modeline--vc-faces
  '((added . vc-locally-added-state)
    (edited . vc-edited-state)
    (removed . vc-removed-state)
    (missing . vc-missing-state)
    (conflict . vc-conflict-state)
    (locked . vc-locked-state)
    (up-to-date . vc-up-to-date-state))
  "VC state faces.")

(defun prot-modeline--vc-get-face (key)
  "Get face from KEY in `prot-modeline--vc-faces'."
  (alist-get key prot-modeline--vc-faces 'up-to-date))

(defun prot-modeline--vc-face (file backend)
  "Return VC state face for FILE with BACKEND."
  (prot-modeline--vc-get-face (vc-state file backend)))

(defvar-local prot-modeline-vc-branch
    '(:eval
      (when-let* (((mode-line-window-selected-p))
                  (file (buffer-file-name))
                  (backend (vc-backend file))
                  ;; ((vc-git-registered file))
                  (branch (prot-modeline--vc-branch-name file backend))
                  (face (prot-modeline--vc-face file backend)))
        (prot-modeline--vc-details file branch face)))
  "Mode line construct to return propertized VC branch.")

;;;; Flymake errors, warnings, notes

(declare-function flymake--severity "flymake" (type))
(declare-function flymake-diagnostic-type "flymake" (diag))

;; Based on `flymake--mode-line-counter'.
(defun prot-modeline-flymake-counter (type)
  "Compute number of diagnostics in buffer with TYPE's severity.
TYPE is usually keyword `:error', `:warning' or `:note'."
  (let ((count 0))
    (dolist (d (flymake-diagnostics))
      (when (= (flymake--severity type)
               (flymake--severity (flymake-diagnostic-type d)))
        (cl-incf count)))
    (when (cl-plusp count)
      (number-to-string count))))

(defvar prot-modeline-flymake-map
  (let ((map (make-sparse-keymap)))
    (define-key map [mode-line down-mouse-1] 'flymake-show-buffer-diagnostics)
    (define-key map [mode-line down-mouse-3] 'flymake-show-project-diagnostics)
    map)
  "Keymap to display on Flymake indicator.")

(defmacro prot-modeline-flymake-type (type indicator &optional face)
  "Return function that handles Flymake TYPE with stylistic INDICATOR and FACE."
  `(defun ,(intern (format "prot-modeline-flymake-%s" type)) ()
     (when-let* ((count (prot-modeline-flymake-counter
                         ,(intern (format ":%s" type)))))
       (concat
        (propertize ,indicator 'face 'shadow)
        (propertize count
                    'face ',(or face type)
                    'mouse-face 'mode-line-highlight
                    ;; FIXME 2023-07-03: Clicking on the text with
                    ;; this buffer and a single warning present, the
                    ;; diagnostics take up the entire frame.  Why?
                    'local-map prot-modeline-flymake-map
                    'help-echo "mouse-1: buffer diagnostics\nmouse-3: project diagnostics")))))

(prot-modeline-flymake-type error "☣")
(prot-modeline-flymake-type warning "!")
(prot-modeline-flymake-type note "·" success)

(defvar-local prot-modeline-flymake
    `(:eval
      (when (and (bound-and-true-p flymake-mode)
                 (mode-line-window-selected-p))
        (list
         ;; See the calls to the macro `prot-modeline-flymake-type'
         '(:eval (prot-modeline-flymake-error))
         '(:eval (prot-modeline-flymake-warning))
         '(:eval (prot-modeline-flymake-note)))))
  "Mode line construct displaying `flymake-mode-line-format'.
Specific to the current window's mode line.")

;;;; Eglot

(with-eval-after-load 'eglot
  (setq mode-line-misc-info
        (delete '(eglot--managed-mode (" [" eglot--mode-line-format "] ")) mode-line-misc-info)))

(defvar-local prot-modeline-eglot
    `(:eval
      (when (and (featurep 'eglot) (mode-line-window-selected-p))
        '(eglot--managed-mode eglot--mode-line-format)))
  "Mode line construct displaying Eglot information.
Specific to the current window's mode line.")

;;;; Miscellaneous

(defvar-local prot-modeline-notmuch-indicator
    '(notmuch-indicator-mode
      (" "
       (:eval (when (mode-line-window-selected-p)
                notmuch-indicator--counters))))
  "The equivalent of `notmuch-indicator-mode-line-construct'.
Display the indicator only on the focused window's mode line.")

(defvar-local prot-modeline-misc-info
    '(:eval
      (when (mode-line-window-selected-p)
        mode-line-misc-info))
  "Mode line construct displaying `mode-line-misc-info'.
Specific to the current window's mode line.")

;;;; Risky local variables

;; NOTE 2023-04-28: The `risky-local-variable' is critical, as those
;; variables will not work without it.
(dolist (construct '(prot-modeline-kbd-macro
                     prot-modeline-narrow
                     prot-modeline-input-method
                     prot-modeline-buffer-status
                     prot-modeline-window-dedicated-status
                     prot-modeline-buffer-identification
                     prot-modeline-major-mode
                     prot-modeline-process
                     prot-modeline-vc-branch
                     prot-modeline-flymake
                     prot-modeline-eglot
                     ;; prot-modeline-align-right
                     prot-modeline-notmuch-indicator
                     prot-modeline-misc-info))
  (put construct 'risky-local-variable t))

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

6.11. The prot-notmuch.el library

;;; prot-notmuch.el --- Tweaks for my notmuch.el configurations -*- lexical-binding: t -*-

;; Copyright (C) 2021-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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)
(eval-when-compile (require 'cl-lib))

(defgroup prot-notmuch ()
  "Extensions for notmuch.el."
  :group '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-delete-tags
  `(,(format "+%s" prot-notmuch-delete-tag) "-inbox" "-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" "-inbox" "-unread")
  "List of tags to mark as spam."
  :type '(repeat string)
  :group 'prot-notmuch)

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

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

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

;;;; SourceHut-related setup

(defconst prot-notmuch-patch-control-codes
  '("PROPOSED" "NEEDS_REVISION" "SUPERSEDED" "APPROVED" "REJECTED" "APPLIED")
  "Control codes for SourceHut patches.
See `prot-notmuch-patch-add-email-control-code' for how to apply
them.")

(defun prot-notmuch--rx-in-sourcehut-mail (rx-group string)
  "Return RX-GROUP of SourceHut mail in STRING."
  (when (string-match-p "lists\\.sr\\.ht" string)
    (string-clean-whitespace
     (replace-regexp-in-string
      ".*?[<]?\\(\\([-a-zA-Z0-9=._+~/]+\\)@\\(lists\\.sr\\.ht\\)\\)[>]?.*?"
      (format "\\%s" rx-group) string))))

(declare-function notmuch-show-get-header "notmuch-show" (header &optional props))
(declare-function message-fetch-field "message" (header &optional first))

(defun prot-notmuch--get-to-or-cc-header ()
  "Get appropriate To or Cc header."
  (cond
   ((derived-mode-p 'notmuch-message-mode)
    (concat (message-fetch-field "To") " " (message-fetch-field "Cc")))
   ((derived-mode-p 'notmuch-show-mode)
    (concat (notmuch-show-get-header :To) " " (notmuch-show-get-header :Cc)))))

;; NOTE 2022-04-19: This assumes that we only have one list...  I think
;; that is okay, but it might cause problems.
(defun prot-notmuch--extract-sourcehut-mail (rx-group)
  "Extract RX-GROUP from SourceHut mailing list address.
1 is the full email address, 2 is the local part, while 3 is the
domain."
  (prot-notmuch--rx-in-sourcehut-mail
   rx-group (prot-notmuch--get-to-or-cc-header)))

(declare-function message-add-header "message" (&rest headers))

;; Read: <https://man.sr.ht/lists.sr.ht/#email-controls>.
;;;###autoload
(defun prot-notmuch-patch-add-email-control-code (control-code)
  "Add custom header for SourceHut email controls.
The CONTROL-CODE is among `prot-notmuch-patch-control-codes'."
  (interactive
   (list (completing-read "Select control code: " prot-notmuch-patch-control-codes nil t)))
  (if (member control-code prot-notmuch-patch-control-codes)
    (unless (message-fetch-field "X-Sourcehut-Patchset-Update")
      (message-add-header (format "X-Sourcehut-Patchset-Update: %s" control-code)))
    (user-error "%s is not specified in `prot-notmuch-patch-control-codes'" control-code)))

;;;###autoload
(defun prot-notmuch-ask-sourcehut-control-code ()
  "Use `prot-notmuch-patch-add-email-control-code' programmatically.
Add this to `notmuch-mua-send-hook'."
  (when-let* ((header (message-fetch-field "Subject"))
              (subject (when (>= (length header) 6) (substring header 0 6)))
              ((string= "[PATCH" subject)) ; Is [ always there?
              ((prot-notmuch--extract-sourcehut-mail 1))
              ((not (message-fetch-field "X-Sourcehut-Patchset-Update")))
              ((y-or-n-p "Add control code for SourceHut PATCH?")))
    (call-interactively #'prot-notmuch-patch-add-email-control-code)))

;; NOTE 2022-04-19: Ideally we should be able to use the
;; `notmuch-show-stash-mlarchive-link-alist' for
;; `prot-notmuch-stash-sourcehut-link', but it assumes that the base URL
;; is fixed for all message IDs, whereas those on SourceHut are not.

(declare-function notmuch-show-get-message-id "notmuch-show" (&optional bare))
(declare-function notmuch-show-message-top "notmuch-show")
(declare-function notmuch-common-do-stash "notmuch-lib" (text))

;;;###autoload
(defun prot-notmuch-stash-sourcehut-link (&optional current)
  "Stash web link to current SourceHut thread.
With optional CURRENT argument, produce a link to the current
message, else use the topmost message (start of the thread).

Note that the topmost message is assumed to hold the id of the
base URL, though this is not necessarily true."
  (interactive "P")
  (let* ((ml (prot-notmuch--extract-sourcehut-mail 2))
         (base-id (save-excursion (goto-char (point-min))
                                  (notmuch-show-message-top)
                                  (notmuch-show-get-message-id t)))
         (current-id (notmuch-show-get-message-id t)))
    (notmuch-common-do-stash
     (if current
         (format "https://lists.sr.ht/%s/<%s>#<%s>" ml base-id current-id)
       (format "https://lists.sr.ht/%s/<%s>" ml base-id)))))

;;;###autoload
(defun prot-notmuch-check-valid-sourcehut-email ()
  "Check if SourceHut address is correct.
Add this to `notmuch-mua-send-hook'."
  (when-let* ((ml (prot-notmuch--extract-sourcehut-mail 1))
              ((not (string-match-p "^\\(~\\|\\.\\)" ml)))
              ((not (y-or-n-p "SourceHut address looks wrong.  Send anyway?"))))
    (user-error "Incorrect SourceHut address")))

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

6.12. The prot-orderless.el library

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

;; Copyright (C) 2020-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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:

;;;; Style dispatchers

(defun prot-orderless-literal (word _index _total)
  "Read WORD= as a literal string."
  (when (string-suffix-p "=" word)
    ;; The `orderless-literal' is how this should be treated by
    ;; orderless.  The `substring' form omits the `=' from the
    ;; pattern.
    `(orderless-literal . ,(substring word 0 -1))))

(defun prot-orderless-file-ext (word _index _total)
  "Expand WORD. to a file suffix when completing file names."
  (when (and minibuffer-completing-file-name
             (string-suffix-p "." word))
    `(orderless-regexp . ,(format "\\.%s\\'" (substring word 0 -1)))))

(defun prot-orderless-beg-or-end (word _index _total)
  "Expand WORD~ to \\(^WORD\\|WORD$\\)."
  (when-let* (((string-suffix-p "~" word))
              (word (substring word 0 -1)))
    `(orderless-regexp . ,(format "\\(^%s\\|%s$\\)" word word))))

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

6.13. The prot-org.el library

;;; prot-org.el --- Tweaks for my org-mode configurations -*- lexical-binding: t -*-

;; Copyright (C) 2021-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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)
(require 'org)

(defgroup prot-org ()
  "Extensions for org.el."
  :group 'org)

;;;; org-capture

(defvar prot-org--capture-coach-person-history nil)

(declare-function message-fetch-field "message" (header &optional first))
(declare-function notmuch-show-get-header "notmuch-show")

(defun prot-org--capture-coach-person-message-from ()
  "Return default value for `prot-org--capture-coach-person-prompt'."
  (when-let* ((from (cond
                     ((derived-mode-p 'message-mode)
                      (message-fetch-field "To"))
                     ((derived-mode-p 'notmuch-show-mode)
                      (notmuch-show-get-header :From)))))
    (string-clean-whitespace (car (split-string from "<")))))

(defun prot-org--capture-coach-person-message-from-and-subject ()
  "Return default value for `prot-org--capture-coach-person-prompt'."
  (cond
   ((derived-mode-p 'message-mode)
    (message-fetch-field "Subject"))
   ((derived-mode-p 'notmuch-show-mode)
    (notmuch-show-get-header :Subject))))

(defun prot-org--capture-coach-person-prompt ()
  "Prompt for person for use in `prot-org-capture-coach'."
  (completing-read "Person to coach: "
                   prot-org--capture-coach-person-history
                   nil nil nil
                   'prot-org--capture-coach-person-history
                   (prot-org--capture-coach-person-message-from)))

(defvar prot-org--capture-coach-description-history nil)

(defun prot-org--capture-coach-description-prompt ()
  "Prompt for description in `prot-org-capture-coach'."
  (read-string "Description: "
               nil
               'prot-org--capture-coach-description-history
               (prot-org--capture-coach-person-message-from-and-subject)))

(defun prot-org--capture-coach-date-prompt-range ()
  "Prompt for Org date and return it as a +1h range.
For use in `prot-org-capture-coach'."
  (let ((date (org-read-date :with-time)))
    ;; We cannot use this here, unfortunately, as the Org agenda
    ;; interprets it both as a deadline and an event with the date
    ;; range.
    ;;
    ;; (format "DEADLINE: <%s>--<%s>\n" date
    (format "<%s>--<%s>\n" date
            (org-read-date
             :with-time nil "++1h" nil
             (org-encode-time (org-parse-time-string date))))))

(defun prot-org-capture-coach ()
  "Contents of an Org capture template for my coaching lessons."
  (let ((identifier (format-time-string "%Y%m%dT%H%M%S")))
    (format "* COACH %s %s :lesson:
DEADLINE: %%^T
:PROPERTIES:
:CAPTURED: %%U
:CUSTOM_ID: h:%s
:APPT_WARNTIME: 20
:END:

%%a%%?"
            (prot-org--capture-coach-person-prompt)
            (prot-org--capture-coach-description-prompt)
            identifier
            identifier)))

(defun prot-org-capture-coach-clock ()
  "Contents of an Org capture for my clocked coaching services."
  (format "* COACH %s %s :service:
:PROPERTIES:
:CAPTURED: %%U
:CUSTOM_ID: h:%s
:END:

%%a%%?"
          (prot-org--capture-coach-person-prompt)
          (prot-org--capture-coach-description-prompt)
          (format-time-string "%Y%m%dT%H%M%S")))

(declare-function cl-letf "cl-lib")

;; Adapted from source: <https://stackoverflow.com/a/54251825>.
;;
;; Thanks to Juanjo Presa (@uningan on GitHub) for discovering that the
;; original version was causing an error in `org-roam'.  I then figure
;; we were missing the `&rest':
;; <https://github.com/org-roam/org-roam/issues/2142#issuecomment-1100718373>.
(defun prot-org--capture-no-delete-windows (&rest args)
  (cl-letf (((symbol-function 'delete-other-windows) 'ignore))
    (apply args)))

;; Same source as above
(advice-add 'org-capture-place-template :around 'prot-org--capture-no-delete-windows)
(advice-add 'org-add-log-note :around 'prot-org--capture-no-delete-windows)

;;;;; Custom function to select a project to add to

(defun prot-org--get-outline (&optional file)
  "Return `outline-regexp' headings and line numbers of current file or FILE."
  (with-current-buffer (find-file-noselect file)
    (let ((outline-regexp (format "^\\(?:%s\\)" (or (bound-and-true-p outline-regexp) "[*\^L]+")))
          candidates)
      (save-excursion
        (goto-char (point-min))
        (while (if (bound-and-true-p outline-search-function)
                   (funcall outline-search-function)
                 (re-search-forward outline-regexp nil t))
          (push
           ;; NOTE 2024-11-24: The -5 (minimum width) is a sufficiently high number to keep the
           ;; alignment consistent in most cases.  Larger files will simply shift the heading text
           ;; in minibuffer, but this is not an issue anymore.
           (format "%-5s\t%s"
                   (line-number-at-pos (point))
                   (buffer-substring-no-properties (line-beginning-position) (line-end-position)))
           candidates)
          (goto-char (1+ (line-end-position)))))
      (if candidates
          (nreverse candidates)
        (user-error "No outline")))))

(defvar prot-org-outline-history nil
  "Minibuffer history for `prot-org-outline-prompt'.")

(defun prot-org-outline-prompt (&optional file)
  "Prompt for outline among headings retrieved by `prot-org--get-outline'.
With optional FILE use the outline of it, otherwise use that of
the current file."
  (let ((current-file (or file buffer-file-name))
        (default (car prot-org-outline-history)))
    (completing-read
     (format-prompt
      (format "Select heading inside `%s': "
              (propertize (file-name-nondirectory current-file) 'face 'error))
      default)
     (prot-common-completion-table-no-sort 'imenu (prot-org--get-outline current-file))
     nil :require-match nil 'prot-org-outline-history default)))

(defvar prot-org-file-history nil
  "Minibuffer history of `prot-org-file-prompt'.")

(defun prot-org--not-useful-p (file)
  "Return non-nil if FILE is not a useful Org file for `org-capture'."
  (or (string-match-p "\\.org_archive\\'" file)
      (backup-file-name-p file)
      (not (string-match-p "\\.org\\'" file))))

(defun prot-org-file-prompt ()
  "Select a file in the `org-directory'."
  (if-let* ((dir org-directory)
            (files (directory-files-recursively org-directory ".*" nil))
            (files (seq-remove #'prot-org--not-useful-p files)))
      (let ((default (car prot-org-file-history)))
        (completing-read
         (format-prompt "Select file" default)
         (prot-common-completion-table 'file files)
         nil :require-match nil 'prot-org-file-history default))
    (user-error "There are no files in the `org-directory'")))

;;;###autoload
(defun prot-org-capture-select-project ()
  "Like `prot-org-select-project' but specifically for `org-capture'."
  (declare (interactive-only t))
  (interactive)
  (pcase-let* ((file (prot-org-file-prompt))
               (line-with-heading (prot-org-outline-prompt file))
               (`(,line ,text) (split-string line-with-heading "\t"))
               (line (string-to-number line)))
    ;; NOTE 2024-11-24: `with-current-buffer' does not work with `org-capture'.
    (find-file file)
    (goto-char (point-min))
    (forward-line (1- line))))

(defalias 'prot-org-goto-heading-in-file 'prot-org-capture-select-project
 "Alias for `prot-org-capture-select-project'.")

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

(defvar org-priority-highest)

(defun prot-org-agenda-include-priority-no-timestamp ()
  "Return nil if heading has a priority but no timestamp.
Otherwise, return the buffer position from where the search should
continue, per `org-agenda-skip-function'."
  (let ((point (point)))
    (if (and (eq (nth 3 (org-heading-components)) ?A)
             (not (org-get-deadline-time point))
             (not (org-get-scheduled-time point)))
        nil
      (line-beginning-position 2))))

(defvar prot-org-custom-daily-agenda
  ;; NOTE 2021-12-08: Specifying a match like the following does not
  ;; work.
  ;;
  ;; tags-todo "+PRIORITY=\"A\""
  ;;
  ;; So we match everything and then skip entries with
  ;; `org-agenda-skip-function'.
  `((tags-todo "*"
               ((org-agenda-overriding-header "Important tasks without a date\n")
                ;; NOTE 2024-10-31: Those used to work, but now the
                ;; query for the timestamp is ignored.  I thus wrote
                ;; `prot-org-agenda-include-priority-no-timestamp'.
                ;;
                ;; (org-agenda-skip-function '(org-agenda-skip-subtree-if nil '(timestamp)))
                ;; (org-agenda-skip-function
                ;;  `(org-agenda-skip-entry-if
                ;;    'notregexp ,(format "\\[#%s\\]" (char-to-string org-priority-highest))))
                (org-agenda-skip-function #'prot-org-agenda-include-priority-no-timestamp)
                (org-agenda-block-separator nil)))
    (agenda "" ((org-agenda-overriding-header "\nPending scheduled tasks")
                (org-agenda-time-grid nil)
                (org-agenda-start-on-weekday nil)
                (org-agenda-span 1)
                (org-agenda-show-all-dates nil)
                (org-scheduled-past-days 365)
                ;; Excludes today's scheduled items
                (org-scheduled-delay-days 1)
                (org-agenda-block-separator nil)
                (org-agenda-entry-types '(:scheduled))
                (org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))
                (org-agenda-skip-function '(org-agenda-skip-entry-if 'regexp "ROUTINE"))
                (org-agenda-day-face-function (lambda (date) 'org-agenda-date))
                (org-agenda-format-date "")))
    (agenda "" ((org-agenda-overriding-header "\nToday's agenda\n")
                (org-agenda-span 1)
                (org-deadline-warning-days 0)
                (org-agenda-block-separator nil)
                (org-scheduled-past-days 0)
                (org-agenda-skip-function '(org-agenda-skip-entry-if 'regexp "ROUTINE"))
                ;; We don't need the `org-agenda-date-today'
                ;; highlight because that only has a practical
                ;; utility in multi-day views.
                (org-agenda-day-face-function (lambda (date) 'org-agenda-date))
                (org-agenda-format-date "%A %-e %B %Y")))
    ;; (agenda "" ((org-agenda-overriding-header "\nRoutine")
    ;;             (org-agenda-time-grid nil)
    ;;             (org-agenda-start-on-weekday nil)
    ;;             (org-agenda-span 1)
    ;;             (org-agenda-show-all-dates nil)
    ;;             (org-scheduled-past-days 365)
    ;;             ;; Excludes today's scheduled items
    ;;             (org-scheduled-delay-days 1)
    ;;             (org-agenda-block-separator nil)
    ;;             (org-agenda-entry-types '(:scheduled))
    ;;             (org-agenda-skip-function '(org-agenda-skip-entry-if 'notregexp "ROUTINE"))
    ;;             (org-agenda-day-face-function (lambda (date) 'org-agenda-date))
    ;;             (org-agenda-format-date "")))
    (agenda "" ((org-agenda-overriding-header "\nNext three days\n")
                (org-agenda-start-on-weekday nil)
                (org-agenda-start-day nil)
                (org-agenda-start-day "+1d")
                (org-agenda-span 3)
                (org-deadline-warning-days 0)
                (org-agenda-block-separator nil)
                (org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))))
    (agenda "" ((org-agenda-overriding-header "\nUpcoming deadlines (+14d)\n")
                (org-agenda-time-grid nil)
                (org-agenda-start-on-weekday nil)
                ;; We don't want to replicate the previous section's
                ;; three days, so we start counting from the day after.
                (org-agenda-start-day "+4d")
                (org-agenda-span 14)
                (org-agenda-show-all-dates nil)
                (org-deadline-warning-days 0)
                (org-agenda-block-separator nil)
                (org-agenda-entry-types '(:deadline))
                (org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done)))))
  "Custom agenda for use in `org-agenda-custom-commands'.")

;;;;; agenda appointments

(defvar prot-org-agenda-after-edit-hook nil
  "Hook that runs after select Org commands.
To be used with `advice-add'.")

(defun prot-org--agenda-after-edit (&rest _)
  "Run `prot-org-agenda-after-edit-hook'."
  (run-hooks 'prot-org-agenda-after-edit-hook))

(defvar prot-org-after-deadline-or-schedule-hook nil
  "Hook that runs after `org--deadline-or-schedule'.
To be used with `advice-add'.")

(defvar prot-org--appt-agenda-commands
  '( org-agenda-archive org-agenda-deadline org-agenda-schedule
     org-agenda-todo org-archive-subtree)
  "List of commands that run `prot-org-agenda-after-edit-hook'.")

(dolist (fn prot-org--appt-agenda-commands)
  (advice-add fn :after #'prot-org--agenda-after-edit))

(defun prot-org--after-deadline-or-schedule (&rest _)
  "Run `prot-org-after-deadline-or-schedule-hook'."
  (run-hooks 'prot-org-after-deadline-or-schedule-hook))

(defun prot-org-org-agenda-to-appt ()
  "Make `org-agenda-to-appt' always refresh appointment list."
  (org-agenda-to-appt :refresh))

(dolist (hook '(org-capture-after-finalize-hook
                org-after-todo-state-change-hook
                org-agenda-after-show-hook
                prot-org-agenda-after-edit-hook))
  (add-hook hook #'prot-org-org-agenda-to-appt))

(declare-function org--deadline-or-schedule "org" (arg type time))

(advice-add #'org--deadline-or-schedule :after #'prot-org--after-deadline-or-schedule)

(add-hook 'prot-org-after-deadline-or-schedule-hook #'prot-org-org-agenda-to-appt)

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

;; Original idea:
;; <https://writequit.org/articles/emacs-org-mode-generate-ids.html>.
(defun prot-org--id-get ()
  "Get the CUSTOM_ID of the current entry.
If the entry already has a CUSTOM_ID, return it as-is, else
create a new one."
  (let* ((pos (point))
         (id (org-entry-get pos "CUSTOM_ID")))
    (if (and id (stringp id) (string-match-p "\\S-" id))
        id
      (setq id (org-id-new "h"))
      (org-entry-put pos "CUSTOM_ID" id)
      id)))

(declare-function org-map-entries "org")

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

;;;###autoload
(defun prot-org-id-headline ()
  "Add missing CUSTOM_ID to headline at point."
  (interactive)
  (prot-org--id-get))

;;;###autoload
(defun prot-org-get-dotemacs-link ()
  "Get URL to current heading in my dotemacs file."
  (declare (interactive-only t))
  (interactive)
  (if (and (derived-mode-p 'org-mode)
           (string-match-p "prot-emacs\\.org\\'" buffer-file-name))
      (if-let* ((id (org-entry-get (point) "CUSTOM_ID"))
                (url (concat "https://protesilaos.com/emacs/dotemacs#" id)))
          (progn
            (kill-new url)
            (message "Copied %s" (propertize url 'face 'success)))
        (error "No CUSTOM_ID for the current entry"))
    (user-error "You are not in the right file")))

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

6.14. The prot-pair.el library

;;; prot-pair.el --- Insert character pair around symbol or region -*- lexical-binding: t -*-

;; Copyright (C) 2023-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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:
;;
;; Insert character pair around symbol or region using minibuffer
;; completion.
;;
;; 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-pair nil
  "Insert character pair around symbol or region."
  :group 'editing)

(defcustom prot-pair-pairs
  '((?'  :description "Single quotes"           :pair (?' . ?'))
    (?\" :description "Double quotes"           :pair (?\" . ?\"))
    (?‘  :description "Single curly quotes"     :pair (?‘ . ?’))
    (?“  :description "Double curly quotes"     :pair (?“ . ?”))
    (?\> :description "Natural language quotes" :pair prot-pair-insert-natural-language-quotes)
    (?\( :description "Parentheses"             :pair (?\( . ?\)))
    (?{  :description "Curly brackets"          :pair (?{ . ?}))
    (?\[ :description "Square brackets"         :pair (?\[ . ?\]))
    (?\< :description "Angled brackets"         :pair (?\< . ?\>))
    (?@  :description "At signs"                :pair (?@ . ?@))
    (?=  :description "Equals signs"            :pair (?= . ?=))
    (?+  :description "Plus signs"              :pair (?+ . ?+))
    (?`  :description "Backticks"               :pair prot-pair-insert-backticks)
    (?~  :description "Tildes"                  :pair (?~ . ?~))
    (?*  :description "Asterisks"               :pair (?* . ?*))
    (?/  :description "Forward slashes"         :pair (?/ . ?/))
    (?_  :description "Underscores"             :pair (?_ . ?_)))
  "Alist of pairs for use with `prot-pair-insert'.
Each element in the list is a list whose `car' is a character and
the `cdr' is a plist with a `:description' and `:pair' keys.  The
`:description' is a string used to describe the character/pair in
interactive use, while `:pair' is a cons cell referencing the
opening and closing characters.

The value of `:pair' can also be the unquoted symbol of a
function.  The function is called with no arguments and must
return a cons cell of two characters.  Examples of such functions
are `prot-pair-insert-natural-language-quotes' and
`prot-pair-insert-backticks'"
  :type '(alist
          :key-type character
          :value-type (plist :options (((const :tag "Pair description" :description) string)
                                       ((const :tag "Characters" :pair)
                                        (choice (cons character character) function)))))
  :group 'prot-pair)

(defun prot-pair-insert-backticks ()
  "Return pair of backticks for `prot-pair-pairs'.
When the major mode is derived from `lisp-mode', return a pair of
backtick and single quote, else two backticks."
  (if (derived-mode-p 'lisp-mode 'lisp-data-mode)
      (cons ?` ?')
    (cons ?` ?`)))

(defun prot-pair-insert-natural-language-quotes ()
  "Return pair of quotes for `prot-pair-pairs', per natural language."
  ;; There are more here: <https://en.wikipedia.org/wiki/Quotation_mark>.
  ;; I cover the languages I might type in.
  (cond
   ((and current-input-method
         (string-match-p "\\(greek\\|french\\|spanish\\)" current-input-method))
    (cons ?« ?»))
   (t (cons ?\" ?\"))))

(defvar prot-pair--insert-history nil
  "Minibuffer history of `prot-pair--insert-prompt'.")

(defun prot-pair--annotate (character)
  "Annotate CHARACTER with its description in `prot-pair-pairs'."
  (when-let* ((char (if (characterp character) character (string-to-char character)))
              (plist (alist-get char prot-pair-pairs))
              (description (plist-get plist :description)))
    (format "  %s" description)))

(defun prot-pair--get-pair (character)
  "Get the pair of corresponding to CHARACTER."
  (when-let* ((char (if (characterp character) character (string-to-char character)))
              (plist (alist-get char prot-pair-pairs))
              (pair (plist-get plist :pair)))
    pair))

(defun prot-pair--insert-prompt ()
  "Prompt for pair among `prot-pair-pairs'."
  (let ((default (car prot-pair--insert-history))
        (candidates (mapcar (lambda (char) (char-to-string (car char))) prot-pair-pairs))
        (completion-extra-properties `(:annotation-function ,#'prot-pair--annotate)))
    (completing-read
     (format-prompt "Select pair" default)
     candidates nil :require-match
     nil 'prot-pair--insert-history default)))

(defun prot-pair--insert-bounds ()
  "Return boundaries of symbol at point or active region."
  (if (region-active-p)
      (cons (region-beginning) (region-end))
    (bounds-of-thing-at-point 'symbol)))

;;;###autoload
(defun prot-pair-insert (pair n)
  "Insert N number of PAIR around object at point.
PAIR is one among `prot-pair-pairs'.  The object at point is
either a symbol or the boundaries of the active region.  N is a
numeric prefix argument, defaulting to 1 if none is provided in
interactive use."
  (interactive
   (list
    (prot-pair--get-pair (prot-pair--insert-prompt))
    (prefix-numeric-value current-prefix-arg)))
  (let* ((bounds (prot-pair--insert-bounds))
         (beg (car bounds))
         (end (1+ (cdr bounds))) ; 1+ because we want the character after it
         (characters (if (functionp pair) (funcall pair) pair)))
    (dotimes (_ n)
      (save-excursion
        (goto-char beg)
        (insert (car characters))
        (goto-char end)
        (setq end (1+ end))
        (insert (cdr characters))))
    (goto-char (+ end (1- n)))))

;;;###autoload
(defun prot-pair-delete ()
  "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)))

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

6.15. The prot-prefix.el library

[ Watch: define prefix/leader key (nested key maps) (2024-01-29). ]

;;; prot-prefix.el --- Prefix keymap for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2023-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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:
;;
;; Prefix keymap for my custom keymaps.
;;
;; 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:

(declare-function prot-simple-kill-buffer-current "prot-simple" (&optional arg))
(declare-function prot-simple-rename-file-and-buffer "prot-simple" (name))
(declare-function prot-simple-buffers-major-mode "prot-simple")
(declare-function prot-simple-buffers-vc-root "prot-simple")
(declare-function beframe-buffer-menu "beframe" (&optional frame &key sort))

(defvar-keymap prot-prefix-buffer-map
  :doc "Prefix keymap for buffers."
  :name "Buffer"
  :prefix 'prot-prefix-buffer
  "m" #'beframe-buffer-menu
  "b" #'switch-to-buffer
  "B" #'prot-simple-buffers-major-mode
  "c" #'clone-indirect-buffer-other-window
  "f" #'fit-window-to-buffer
  "k" #'prot-simple-kill-buffer-current
  "g" #'revert-buffer-quick
  "r" #'prot-simple-rename-file-and-buffer
  "n" #'next-buffer
  "p" #'previous-buffer
  "v" #'prot-simple-buffers-vc-root)

(defvar-keymap prot-prefix-file-map
  :doc "Prefix keymaps for files."
  :name "File"
  :prefix 'prot-prefix-file
  "f" #'find-file
  "F" #'find-file-other-window
  "b" #'bookmark-jump
  "d" #'dired
  "l" #'find-library
  "m" #'man)

(defvar-keymap prot-prefix-insert-map
  :doc "Prefix keymap for character insertion."
  :name "Insert"
  :prefix 'prot-prefix-insert
  "i" #'insert-char
  "e" #'emoji-search
  "q" #'quoted-insert
  "s" #'emoji-search
  "l" #'emoji-list)

(declare-function logos-focus-mode "logos")
(declare-function keycast-mode-line-mode "keycast")
(declare-function rainbow-mode "rainbow")
(declare-function spacious-padding-mode "spacious-padding")

(defvar-keymap prot-prefix-mode-map
  :doc "Prefix keymap for minor mode toggles."
  :name "Toggle"
  :prefix 'prot-prefix-mode
  "f" #'flymake-mode
  "h" #'hl-line-mode
  "k" #'keycast-mode-line-mode
  "l" #'logos-focus-mode
  "m" #'menu-bar-mode
  "n" #'display-line-numbers-mode
  "t" #'toggle-truncate-lines
  "s" #'spacious-padding-mode
  "r" #'rainbow-mode
  "v" #'variable-pitch-mode)

(defvar-keymap prot-prefix-window-map
  :doc "Prefix keymap for windows."
  :name "Window"
  :prefix 'prot-prefix-window
  "u" #'winner-undo
  "r" #'winner-redo
  "b" #'balance-windows-area
  "d" #'toggle-window-dedicated
  "0" #'delete-window
  "1" #'delete-other-windows
  "!" #'delete-other-windows-vertically
  "2" #'split-window-below
  "@" #'split-root-window-below
  "3" #'split-window-right
  "#" #'split-root-window-right
  "o" #'other-window
  "^" #'tear-off-window
  "h" #'windmove-left
  "j" #'windmove-down
  "k" #'windmove-up
  "l" #'windmove-right
  "H" #'windmove-swap-states-left
  "J" #'windmove-swap-states-down
  "K" #'windmove-swap-states-up
  "L" #'windmove-swap-states-right)

(declare-function consult-find "consult" (&optional dir initial))
(declare-function consult-ripgrep "consult" (&optional dir initial))
(declare-function prot-search-grep "prot-search" (regexp &optional recursive))
(declare-function prot-search-grep-todo-keywords "prot-search" (&optional arg))
(declare-function prot-search-occur-browse-url "prot-search")
(declare-function prot-search-occur-outline "prot-search" (&optional arg))
(declare-function prot-simple-flush-and-diff "prot-simple" (regexp beg end))

(defvar-keymap prot-prefix-search-map
  :doc "Prefix keymap for search (and replace) commands."
  :name "Search"
  :prefix 'prot-prefix-search
  "f" #'consult-find
  "d" #'prot-simple-flush-and-diff
  "g" #'prot-search-grep
  "o" #'prot-search-occur-outline
  "r" #'consult-ripgrep
  "t" #'prot-search-grep-todo-keywords
  "u" #'prot-search-occur-browse-url)

(declare-function prot-simple-transpose-chars "prot-simple")
(declare-function prot-simple-transpose-lines "prot-simple" (arg))
(declare-function prot-simple-transpose-paragraphs "prot-simple" (arg))
(declare-function prot-simple-transpose-sentences "prot-simple" (arg))
(declare-function prot-simple-transpose-words "prot-simple" (arg))
(declare-function prot-simple-transpose-sexps "prot-simple" (arg))

(defvar-keymap prot-prefix-transpose-map
  :doc "Prefix keymap for object transposition."
  :name "Transpose"
  :prefix 'prot-prefix-transpose
  "c" #'prot-simple-transpose-chars
  "l" #'prot-simple-transpose-lines
  "p" #'prot-simple-transpose-paragraphs
  "s" #'prot-simple-transpose-sentences
  "w" #'prot-simple-transpose-words
  "x" #'prot-simple-transpose-sexps)

(defvar-keymap prot-prefix-expression-map
  :doc "Prefix keymap for s-expression motions."
  :name "S-EXP"
  :prefix 'prot-prefix-expression
  "a" #'beginning-of-defun
  "e" #'end-of-defun
  "f" #'forward-sexp
  "b" #'backward-sexp
  "n" #'forward-list
  "p" #'backward-list
  "d" #'up-list ; confusing name for what looks "out and down" to me
  "t" #'transpose-sexps
  "u" #'backward-up-list ; the actual "up"
  "k" #'kill-sexp
  "DEL" #'backward-kill-sexp)

(declare-function winner-undo "winner")
(declare-function winner-redo "winner")
(declare-function magit-status "magit" (&optional directory cache))
(declare-function prot-simple-other-windor-or-frame "prot-simple")

;; NOTE 2024-02-17: Some cons cells here have a symbol as a `cdr' and
;; some do not.  The former are those which define a prefix command
;; (per `define-prefix-command').  This is a symbol that references
;; the keymaps, thus making our binding an indirection: if we update
;; the key map, we automatically get the new key bindings.  Whereas
;; when we bind a key to the value of a variable, we have to update
;; the key map and then the binding for changes to propagate.
(defvar-keymap prot-prefix-map
  :doc "Prefix keymap with multiple subkeymaps."
  :name "Prot Prefix"
  :prefix 'prot-prefix
  "0" #'delete-window
  "1" #'delete-other-windows
  "!" #'delete-other-windows-vertically
  "^" #'tear-off-window
  "2" #'split-window-below
  "@" #'split-root-window-below
  "3" #'split-window-right
  "#" #'split-root-window-right
  "o" #'other-window
  "O" #'prot-simple-other-windor-or-frame
  "Q" #'save-buffers-kill-emacs
  "b" (cons "Buffer" 'prot-prefix-buffer)
  "c" #'world-clock
  "f" (cons "File" 'prot-prefix-file)
  "g" #'magit-status
  "h" (cons "Help" help-map)
  "i" (cons "Insert" 'prot-prefix-insert)
  "j" #'dired-jump
  "m" (cons "Minor modes" 'prot-prefix-mode)
  "n" (cons "Narrow" narrow-map)
  "p" (cons "Project" project-prefix-map)
  "r" (cons "Rect/Registers" ctl-x-r-map)
  "s" (cons "Search" 'prot-prefix-search)
  "t" (cons "Transpose" 'prot-prefix-transpose)
  "u" #'universal-argument
  "v" (cons "Version Control" 'vc-prefix-map)
  "w" (cons "Window" 'prot-prefix-window)
  "x" (cons "S-EXP" 'prot-prefix-expression))

;; ;; NOTE 2024-02-17: This is not needed anymore, because I bind a cons
;; ;; cell to the key.  The `car' of it is the description, which
;; ;; `which-key-mode' understands.
;;
;; (with-eval-after-load 'which-key
;;   (which-key-add-keymap-based-replacements prot-prefix-map
;;     "b" `("Buffer" . ,prot-prefix-buffer-map)
;;     "f" `("File" . ,prot-prefix-file-map)
;;     "h" `("Help" . ,help-map)
;;     "i" `("Insert" . ,prot-prefix-insert-map)
;;     "m" `("Mode" . ,prot-prefix-mode-map)
;;     "n" `("Narrow" . ,narrow-map)
;;     "p" `("Project" . ,project-prefix-map)
;;     "r" `("C-x r" . ,ctl-x-r-map)
;;     "s" `("Search" . ,prot-prefix-search-map)
;;     "t" `("Transpose" . ,prot-prefix-transpose-map)
;;     "v" `("C-x v" . ,vc-prefix-map)
;;     "w" `("Window" . ,prot-prefix-window-map)
;;     "x" `("S-EXP" . ,prot-prefix-expression-map)))

;; What follows is an older experiment with transient.  I like its
;; visuals, though find it hard to extend.  Keymaps are easier for me,
;; as I can add commands to one of the subkeymaps and they are readily
;; available without evaluating anything else.  Probably transient can
;; do this, though it is not obvious to me as to how.

;; (require 'transient)
;;
;; (transient-define-prefix prot-prefix-file nil
;;   "Transient with file commands."
;;   [["File or directory"
;;     ("f" "find-file" find-file)
;;     ("F" "find-file-other-window" find-file-other-window)]
;;    ["Directory only"
;;     ("d" "dired" dired)
;;     ("D" "dired-other-window" dired-other-window)]
;;    ["Documentation"
;;     ("l" "find-library" find-library)
;;     ("m" "man" man)]])
;;
;; (transient-define-prefix prot-prefix-buffer nil
;;   "Transient with buffer commands."
;;   [["Switch"
;;     ("b" "switch buffer" switch-to-buffer)
;;     ("B" "switch buf other window" switch-to-buffer-other-window)
;;     ("n" "next-buffer" next-buffer)
;;     ("p" "previous-buffer" previous-buffer)
;;     ("m" "buffer-menu" buffer-menu)
;;     ("q" "bury-buffer" bury-buffer)]
;;    ["Persist"
;;     ("c" "clone buffer" clone-indirect-buffer)
;;     ("C" "clone buf other window" clone-indirect-buffer-other-window)
;;     ("r" "rename-buffer" rename-buffer)
;;     ("R" "rename-uniquely" rename-uniquely)
;;     ("s" "save-buffer" save-buffer)
;;     ("w" "write-file" write-file)]
;;    ["Destroy"
;;     ("k" "kill-current-buffer" kill-current-buffer)
;;     ("K" "kill-buffer-and-window" kill-buffer-and-window)
;;     ("r" "revert-buffer" revert-buffer)]])
;;
;; (transient-define-prefix prot-prefix-search nil
;;   "Transient with search commands."
;;   [["Search"
;;     ("s" "isearch-forward" isearch-forward)
;;     ("S" "isearch-forward-regexp" isearch-forward-regexp)
;;     ("r" "isearch-backward" isearch-backward)
;;     ("R" "isearch-backward-regexp" isearch-backward-regexp)
;;     ("o" "occur" occur)]
;;    ["Edit"
;;     ("f" "flush-lines" flush-lines)
;;     ("k" "keep-lines" keep-lines)
;;     ("q" "query-replace" query-replace)
;;     ("Q" "query-replace-regexp" query-replace-regexp)]])
;;
;; (transient-define-prefix prot-prefix-window nil
;;   "Transient with window commands."
;;   [["Manage"
;;     ("b" "balance-windows" balance-windows)
;;     ("f" "fit-window-to-buffer" fit-window-to-buffer)
;;     ("t" "tear-off-window" tear-off-window)]
;;    ["Popup"
;;     ("c" "calc" calc)
;;     ("f" "list-faces-display" list-faces-display)
;;     ("r" "re-builder" re-builder)
;;     ("w" "world-clock" world-clock)]])
;;
;; ;; This is independent of the transient, though still useful.
;; (defvar-keymap prot-prefix-repeat-map
;;   :doc "Global prefix map for repeatable keybindings (per `repeat-mode')."
;;   :name "Repeat"
;;   :repeat t
;;   "n" #'next-buffer
;;   "p" #'previous-buffer
;;   "<down>" #'enlarge-window
;;   "<right>" #'enlarge-window-horizontally
;;   "<up>" #'shrink-window
;;   "<left>" #'shrink-window-horizontally)
;;
;; (transient-define-prefix prot-prefix-toggle nil
;;   "Transient with minor mode toggles."
;;   [["Interface"
;;     ("c" "context-menu-mode" context-menu-mode)
;;     ("m" "menu-bar-mode" menu-bar-mode)
;;     ("s" "scroll-bar-mode" scroll-bar-mode)
;;     ("C-t" "tool-bar-mode" tool-bar-mode)]
;;    ["Tools"
;;     ("d" "toggle-debug-on-error" toggle-debug-on-error)
;;     ("f" "follow-mode" follow-mode)
;;     ("l" "visual-line-mode" visual-line-mode)
;;     ("v" "variable-pitch-mode" variable-pitch-mode)
;;     ("t" "toggle-truncate-lines" toggle-truncate-lines)
;;     ("C-s" "window-toggle-side-windows" window-toggle-side-windows)]])
;;
;; (transient-define-prefix prot-prefix nil
;;   "Transient with common commands.
;; Commands that bring up transients have ... in their description."
;;   [["Common"
;;     ("b" "Buffer..." prot-prefix-buffer)
;;     ("f" "File..." prot-prefix-file)
;;     ("s" "Search..." prot-prefix-search)
;;     ("w" "Window..." prot-prefix-window)
;;     ("t" "Toggle..." prot-prefix-toggle)]
;;    ["Resize"
;;     ("   <up>" "Shrink vertically" shrink-window)
;;     (" <down>" "Enlarge vertically" enlarge-window)
;;     (" <left>" "Shrink horizontally" shrink-window-horizontally)
;;     ("<right>" "Enlarge horizontally" enlarge-window-horizontally)]
;;    ["Misc"
;;     ("e" "Emoji transient..." emoji-insert)
;;     ("E" "Emoji search" emoji-search)
;;     ("C-e" "Emoji buffer" emoji-list)
;;     ("RET" "Insert unicode" insert-char)
;;     ("\\" "toggle-input-method" toggle-input-method)]])

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

6.16. The prot-project.el library

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

;; Copyright (C) 2024-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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 project.el.
;;
;; 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 'project)
(require 'tab-bar)

;;;; Switch to a project root Dired outright

(defun prot-project--switch (directory &optional command)
  "Do the work of `project-switch-project' in the given DIRECTORY.
With optional COMMAND, run it in DIRECTORY."
  (let ((command (or (when (functionp command) command)
                     (if (symbolp project-switch-commands)
                         project-switch-commands
                       (project--switch-project-command))))
        (buffer (current-buffer)))
    (unwind-protect
        (progn
          (setq-local project-current-directory-override directory)
          (call-interactively command))
      (with-current-buffer buffer
        (kill-local-variable 'project-current-directory-override)))))

(defun prot-project--frame-names ()
  "Return a list of frame names."
  (mapcar #'car (make-frame-names-alist)))

;;;###autoload
(defun prot-project-switch (directory)
  "Switch to project DIRECTORY.
If DIRECTORY exists in a frame, select it.  Otherwise switch to
the project in DIRECTORY using `project-dired'."
  (interactive (list (funcall project-prompter)))
  (project--remember-dir directory)
  (let ((name (file-name-nondirectory (directory-file-name directory))))
    (if (member name (prot-project--frame-names))
        (select-frame-by-name name)
      (prot-project--switch directory 'project-dired))))

;;;; Produce a VC root log for the project

(defun prot-project-rename-vc-root-log (&rest _)
  "Rename the buffer of `vc-print-root-log' to mention the project."
  (when-let* ((root (vc-root-dir))
              ((consp project--list))
              ((member root (mapcar #'car project--list))))
    (rename-buffer (format "*vc-root-log: %s*" root))))

(advice-add #'vc-print-root-log :after #'prot-project-rename-vc-root-log)

;;;; One tab per project

;; NOTE 2024-01-15 07:07:52 +0200: I define the "in tab" functions as
;; a coding exercise.  I don't have a use for it, as I prefer to use
;; the approach of my `beframe' package instead.
(defun prot-project-in-tab--get-tab-names (&optional frame)
  "Return list of tab names associated with FRAME.
If FRAME is nil, use the current frame."
  (mapcar
   (lambda (tab)
     (alist-get 'name tab))
   (frame-parameter frame 'tabs)))

(defun prot-project-in-tab--create-tab (directory name)
  "Create new tab visiting DIRECTORY and named NAME."
  (tab-new)
  (find-file directory)
  (unwind-protect
      (prot-project--switch directory)
    (tab-rename name)
    ;; NOTE 2024-01-15 06:52 +0200: I am adding this because
    ;; `tab-rename' is not persistent for some reason. Probably a bug...
    (let* ((tabs (funcall tab-bar-tabs-function))
           (tab-to-rename (nth (tab-bar--current-tab-index) tabs)))
      (setf (alist-get 'explicit-name tab-to-rename) name))))

;;;###autoload
(defun prot-project-in-tab (directory)
  "Switch to project DIRECTORY in a tab.
If a tab is named after the non-directory component of DIRECTORY,
switch to it.  Otherwise, create a new tab and name it after the
non-directory component of DIRECTORY.

Use this as an alternative to `project-switch-project'."
  (interactive (list (funcall project-prompter)))
  (project--remember-dir directory)
  (let ((name (file-name-nondirectory (directory-file-name directory))))
    (if (member name (prot-project-in-tab--get-tab-names))
        (tab-switch name)
      (prot-project-in-tab--create-tab directory name))))

;;;; Set up a project root

;; I don't actually have a use-case for `prot-project-find-root',
;; but I wrote it once so I keep it here in case I ever need it.
;; Use it like this: (prot-project-find-root c-mode "Makefile")
(defmacro prot-project-find-root (mode file)
  "Define project root check for MODE given FILE.
MODE must be the symbol of the major mode, without a quote.  FILE
is a string."
  (let ((project-find-fn (intern (format "project-find-%s-root" mode)))
        (major-mode-fn (intern (format "prot-%s-project-find-function" mode)))
        (file-symbol (intern file)))
    `(progn
       (defun ,project-find-fn (dir)
         (when-let* ((root (locate-dominating-file dir ,file)))
           (cons ',file-symbol root)))

       (cl-defmethod project-root ((project (head ,file-symbol)))
         (cdr project))

       (defun ,(intern (format "prot-%s-project-find-function" mode)) ()
         (add-hook 'project-find-functions #',project-find-fn :depth :local))

       (add-hook ',(intern (format "%s-hook" mode)) #',major-mode-fn))))

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

6.17. The prot-scratch.el library

;;; prot-scratch.el --- Scratch buffers for editable major mode of choice -*- lexical-binding: t -*-

;; Copyright (C) 2023-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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:
;;
;; Set up a scratch buffer for an editable major mode of choice.  The
;; idea is based on the `scratch.el' package by Ian Eure:
;; <https://github.com/ieure/scratch-el>.
;;
;; 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-scratch ()
  "Scratch buffers for editable major mode of choice."
  :group 'editing)

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

(defun prot-scratch--scratch-list-modes ()
  "List known major modes."
  (let (symbols)
    (mapatoms
     (lambda (symbol)
       (when (and (functionp symbol)
                  (or (provided-mode-derived-p symbol 'text-mode)
                      (provided-mode-derived-p symbol 'prog-mode)))
         (push symbol symbols))))
    symbols))

(defun prot-scratch--insert-comment ()
  "Insert comment for major mode, if appropriate.
Insert a comment if `comment-start' is non-nil and the buffer is
empty."
  (when (and (prot-common-empty-buffer-p) comment-start)
    (insert (format "Scratch buffer for: %s\n\n" major-mode))
    (goto-char (point-min))
    (comment-region (line-beginning-position) (line-end-position))))

(defun prot-scratch--prepare-buffer (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)))
    (with-current-buffer (pop-to-buffer (format "*%s scratch*" major))
      (funcall major)
      (prot-scratch--insert-comment)
      (goto-char (point-max))
      (unless (string-empty-p region)
        (when (prot-common-line-regexp-p 'non-empty)
          (insert "\n\n"))
        (insert region)))))

(defvar prot-scratch--major-mode-history nil
  "Minibuffer history of `prot-scratch--major-mode-prompt'.")

(defun prot-scratch--major-mode-prompt ()
  "Prompt for major mode and return the choice as a symbol."
  (intern
   (completing-read "Select major mode: "
                    (prot-scratch--scratch-list-modes)
                    nil
                    :require-match
                    nil
                    'prot-scratch--major-mode-history)))

(defun prot-scratch--capture-region ()
  "Capture active region, else return empty string."
  (if (region-active-p)
      (buffer-substring-no-properties (region-beginning) (region-end))
    ""))

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

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

With ARG as a double prefix argument, prompt for a major mode
with completion.  Candidates are derivatives of `text-mode' or
`prog-mode'.

If region is active, copy its contents to the new scratch
buffer.

Buffers are named as *MAJOR-MODE scratch*.  If one already exists
for the given MAJOR-MODE, any text is appended to it."
  (interactive "P")
  (let ((region (prot-scratch--capture-region)))
    (pcase (prefix-numeric-value arg)
      (16 (prot-scratch--prepare-buffer region (prot-scratch--major-mode-prompt)))
      (4 (prot-scratch--prepare-buffer region prot-scratch-default-mode))
      (_ (prot-scratch--prepare-buffer region)))))

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

6.18. The prot-search.el library

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

;; Copyright (C) 2020-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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)

(defcustom prot-search-todo-keywords
  (concat "TODO\\|FIXME\\|NOTE\\|REVIEW\\|XXX\\|KLUDGE"
          "\\|HACK\\|WARN\\|WARNING\\|DEPRECATED\\|BUG")
  "Regexp with search to-do keywords."
  :type 'string
  :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

(defvar prot-search-markup-replacements
  '((elisp-to-org-code "`\\(.*?\\)'" "~\\1~")
    (elisp-to-org-verbatim "`\\(.*?\\)'" "=\\1=")
    (org-to-elisp-quote "[=~]\\(.*?\\)[=~]" "`\\1'")
    (org-to-markdown-code "[=~]\\(.*?\\)[=~]" "`\\1`"))
  "Common markup replacement patterns.")

(defvar prot-search--replace-markup-history '()
  "Minibuffer history of `prot-search-replace-markup'.")

(defun prot-search--replace-markup-prompt ()
  "Prompt for `prot-search-replace-markup'."
  (let* ((def (nth 0 prot-search--replace-markup-history))
         (prompt (if def
                     (format "Replace markup TYPE [%s]: " def)
                   "Replace markup TYPE: ")))
    (intern
     (completing-read
      prompt
      ;; TODO 2022-05-01: maybe older Emacs versions need to explicitly
      ;; map through the car of each list?
      prot-search-markup-replacements
      nil t nil 'prot-search--replace-markup-history def))))

(defun prot-search-replace-markup (type)
  "Perform TYPE of markup replacement.
TYPE is the car of a list in `prot-search-markup-replacements'.

When used interactively, prompt for completion among the
available types.

When the region is active, only perform replacements within its
boundaries, else start from point to the end of the buffer."
  (interactive (list (prot-search--replace-markup-prompt)))
  (if-let* ((types prot-search-markup-replacements)
            ((memq type (mapcar #'car types)))
            (association (alist-get type types))
            (search (nth 0 association))
            (replace (nth 1 association)))
      (if (use-region-p)
          (replace-regexp-in-region search replace (region-beginning) (region-end))
        (while (re-search-forward search nil t)
          (replace-match replace)))
    (user-error "`%s' is not part of `prot-search-markup-replacements'" type)))

;; NOTE 2023-01-14: See my `substitute' package instead of the
;; following: <https://github.com/protesilaos/substitute>.

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

(defvar-local prot-search--remap-cookie nil
  "Current local value of `prot-search--remap-match-face'.")

(defface prot-search-match '((t :inherit default))
  "Face intended to override `match' buffer-locally.")

(defun prot-search--remap-match-face (buf)
  "Remap `match' to `prot-search-match' in BUF."
  (with-current-buffer buf
    (setq prot-search--remap-cookie
          (face-remap-add-relative 'match 'prot-search-match))))

;;;###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))
               (alist-get regexp prot-search-outline-regexp-alist))
              ((assoc major-mode prot-search-outline-regexp-alist)
               (alist-get 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)
    ;; Because we are producing an outline, we do not need to know what
    ;; the exact matches are.
    (prot-search--remap-match-face buf-name)
    (add-to-history 'prot-search--occur-outline-hist regexp)))

;;;###autoload
(defun prot-search-occur-todo-keywords (&optional context)
  "Produce Occur buffer with `prot-search-todo-keywords'.
With optional numeric prefix argument for CONTEXT, show as many
lines before and after each match.

When called from Lisp CONTEXT must satisfy `natnump'.  A faulty
value is read as 0.

Also see `prot-search-grep-todo-keywords'."
  (interactive "P")
  (let* ((case-fold-search nil)
         (num (cond
               (current-prefix-arg
	            (prefix-numeric-value current-prefix-arg))
               (t (if (natnump context) context 0))))
         (buf-name (format "*keywords in <%s>*" (buffer-name))))
    (occur-1 prot-search-todo-keywords num (list (current-buffer)) buf-name)))

;;;; Outline

(defun prot-search--get-outline ()
  "Return alist of outline outline-regexp and positions."
  (let* ((outline-regexp (format "^\\(?:%s\\)" (or (bound-and-true-p outline-regexp) "[*\^L]+")))
         (heading-alist (bound-and-true-p outline-heading-alist))
         (level-fun (or (bound-and-true-p outline-level)
                        (lambda () ;; as in the default from outline.el
                          (or (cdr (assoc (match-string 0) heading-alist))
                              (- (match-end 0) (match-beginning 0))))))
         candidates)
    (save-excursion
      (goto-char (point-min))
      (while (if (bound-and-true-p outline-search-function)
                 (funcall outline-search-function)
               (re-search-forward outline-regexp nil t))
        (push
         (format "%-5s %s"
                 (line-number-at-pos (point))
                 (buffer-substring-no-properties (line-beginning-position) (line-end-position)))
         candidates)
        (goto-char (1+ (line-end-position)))))
    (if candidates
        (nreverse candidates)
      (user-error "No outline"))))

(defun prot-search--outline-prompt ()
  "Prompt for outline among headings retrieved by `prot-search--get-outline'."
  (completing-read
   "Go to outline: "
   (prot-common-completion-table-no-sort 'imenu (prot-search--get-outline))
   nil :require-match))

(defvar prot-search-outline-hook nil
  "Normal hook to run at the end of `prot-search-outline'.")

;;;###autoload
(defun prot-search-outline ()
  "Go to the line of the given outline using completion."
  (interactive)
  (when-let* ((selection (prot-search--outline-prompt))
              (line (string-to-number (car (split-string selection "\t")))))
    (goto-line line)
    (run-hooks 'prot-search-outline-hook)))

;;;; Grep

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

(defun prot-search-grep-prompt (&optional recursive)
  "Prompt for grep pattern.
With optional RECURSIVE, indicate that the search will be called
recursively."
  (read-regexp
   (concat (if recursive
               (propertize "Recursive" 'face 'warning)
             "Local")
           " grep for PATTERN: ")
   nil 'prot-search--grep-hist))

;;;###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
    (prot-search-grep-prompt current-prefix-arg)
    current-prefix-arg))
  (unless grep-command
    (grep-compute-defaults))
  (if recursive
      (rgrep regexp "*" default-directory)
    (lgrep regexp "*" default-directory)))

;;;###autoload
(defun prot-search-grep-todo-keywords (&optional arg)
  "Use `prot-search-grep' to find `prot-search-todo-keywords'.

With optional prefix ARG use git-grep instead for the entire
repository (runs `prot-search-git-grep-todo-keywords').  If Git
is not available on the system, run `prot-search-grep'
recursively, starting from the current directory.

Also see `prot-search-occur-todo-keywords'."
  (interactive "P")
  (cond
   (arg
    (if (executable-find "git")
        (prot-search-git-grep-todo-keywords)
      (prot-search-grep prot-search-todo-keywords t)))
   (t
    (prot-search-grep prot-search-todo-keywords))))

;; NOTE 2022-01-30: We could use `project-find-regexp' but I prefer
;; grep's editable buffers.  Besides, where is the fun in that when we
;; can use `compilation-start' instead?
;;;###autoload
(defun prot-search-git-grep-todo-keywords ()
  "Use the git-grep mechanism for `prot-search-todo-keywords'."
  (interactive)
  (let ((regexp prot-search-todo-keywords)
        (default-directory (or (vc-root-dir)
                               (locate-dominating-file "." ".git")
                               default-directory)))
    (compilation-start
     (format "git --no-pager grep -n --color=auto -r -I -E -e %s" regexp)
     'grep-mode
     (lambda (mode) (format "*prot-search-git-%s for '%s'" mode regexp))
     t)))

(defun prot-search--add-revert-function (buffer mode fn regexp)
  "Append `revert-buffer-function' for FN with REGEXP to MODE BUFFER variables.
See `prot-search-find-grep-buffer' (or related) for the kind of
BUFFER this works with."
  (with-current-buffer buffer
    (setq-local revert-buffer-function
                (lambda (_ignore-auto _noconfirm)
                  (funcall fn regexp))
                ;; FIXME 2023-04-04: The `compile-command' does not
                ;; feel right here.  We do it because in grep-mode the
                ;; g key runs `recompile' which falls back to the
                ;; `compile-command'.  We want it to do the same thing
                ;; as `revert-buffer'.
                compile-command `(funcall ',fn ,regexp))
    (let ((inhibit-read-only t))
      (goto-char (point-min))
      (when (re-search-forward (format "-*- mode: %s;" mode) (line-end-position) :no-error 1)
        (insert
         (format " revert-buffer-function: %S; compile-command %S;"
                 `(lambda (_ignore-auto _noconfirm)
                    (,fn ,regexp))
                 `(funcall ,fn ,regexp)))))))

(defun prot-search--start-compilation (args mode buffer command query)
  "Run compilation with ARGS for MODE in BUFFER given COMMAND running QUERY."
  (compilation-start
   args
   (intern (format "%s-mode" mode))
   (lambda (_mode) buffer)
   :highlight-regexp)
  (prot-search--add-revert-function buffer mode command query))

(defvar prot-search--find-grep-hist '()
  "Minibuffer history for `prot-search-find-grep-buffer' and related.")

(defmacro prot-search-make-search (command docstring prompt function mode)
  "Produce COMMAND with DOCSTRING given PROMPT, FUNCTION, and MODE."
  `(defun ,command (query)
     ,(format
       "%s.

Place the output in a buffer that runs `%s'.  Store the
invocation of this command with REGEXP in a buffer-local
variable.  When the buffer is written to a file, per
`write-file', the `revert-buffer' command (typically bound to
`g') can be used to re-run the search.  The buffer contains
information about the search results, including the exact command
line flags that were used, the time the results were produced,
and the number of matches.  All matching entries are buttonized
and function as links to the context they reference."
       docstring mode)
     (interactive
      (list
       (read-regexp ,prompt nil 'prot-search--find-grep-hist)))
     (let ((args (,function query))
           (buffer-name (format "*prot-search-find for '%s'*" query)))
       (prot-search--start-compilation args ,mode buffer-name ',command query))))

(defun prot-search--find-grep-args (regexp)
  "Return find args to produce grep results for REGEXP."
  (concat
   "find " default-directory
   " -not " (shell-quote-argument "(")
   " -path " (shell-quote-argument "*/.git*")
   " -prune " (shell-quote-argument ")")
   " -type f"
   " -exec grep -nHE --color=auto " regexp " "
   (shell-quote-argument "{}")
   " " (shell-quote-argument ";") " "))

;;;###autoload (autoload 'prot-search-find-grep-buffer "prot-search")
(prot-search-make-search
 prot-search-find-grep-buffer
 "Combine find with grep to produce a buffer for REGEXP matches"
 "Find files matching REGEXP and show a grep buffer: "
 prot-search--find-grep-args
 "grep")

(defun prot-search--find-grep-files-args (regexp)
  "Return find args to produce file listing with contents matching REGEXP."
  (concat
   "find " default-directory
   " -not " (shell-quote-argument "(")
   " -path " (shell-quote-argument "*/.git*")
   " -prune " (shell-quote-argument ")")
   " -type f"
   " -exec grep -qo --color=auto " regexp " "
   (shell-quote-argument "{}")
   " "
   (shell-quote-argument ";") " "
   "-ls"))

;;;###autoload (autoload 'prot-search-find-grep-files-buffer "prot-search")
(prot-search-make-search
 prot-search-find-grep-files-buffer
 "Combine find with grep to produce a buffer for files matching REGEXP"
 "Find files with contents matching REGEXP and show a file listing: "
 prot-search--find-grep-files-args
 "dired")

(defun prot-search--find-file-names-args (regexp)
  "Return find args to produce file listing with file names matching REGEXP."
  (concat
   "find " default-directory
   " -not " (shell-quote-argument "(")
   " -path " (shell-quote-argument "*/.git*")
   " -prune " (shell-quote-argument ")")
   " -type f"
   " -iname '*" regexp "*'"
   " -exec ls -AFhldvN --group-directories-first --time-style=long-iso --color=auto --hyperlink=never "
   (shell-quote-argument "{}")
   " "
   (shell-quote-argument ";")))

;;;###autoload (autoload 'prot-search-find-files-buffer "prot-search")
(prot-search-make-search
 prot-search-find-files-buffer
 "Use find to produce a buffer for file names matching REGEXP"
 "Find files with name matching REGEXP and show a file listing: "
 prot-search--find-file-names-args
 "dired")

;; (defun prot-search-find-grep-file (regexp)
;;   "Use find to produce list of files that include REGEXP."
;;   (interactive
;;    (list
;;     (read-regexp "Find and grep for REGEXP: " nil 'prot-search--find-grep-hist)))
;;   (let ((files (process-lines "find"
;;                               "-type" "f"
;;                               "-exec" "grep" "-nHE" "--color=auto" (format "'%s" regexp) " "
;;                               "-ls" " "
;;                               "{};")
;;                ))
;;     (find-file (completing-read "Find file: "files))))

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

6.19. The prot-shell.el library

;;; prot-shell.el --- M-x shell extensions for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2023-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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 shell.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 'shell)

;;;; Helper functions

(defun prot-shell--beginning-of-prompt-p ()
  "Return non-nil if point is at the beginning of a shell prompt."
  (if comint-use-prompt-regexp
      (looking-back comint-prompt-regexp (line-beginning-position))
    (eq (point) (comint-line-beginning-position))))

(defun prot-shell--insert-and-send (&rest args)
  "Insert and execute ARGS in the last shell prompt.
ARGS is a list of strings."
  (if (prot-shell--beginning-of-prompt-p)
      (progn
        (insert (mapconcat #'identity args " "))
        (comint-send-input))
    (user-error "Not at the beginning of prompt; won't insert: %s" args)))

(defun prot-shell--last-input ()
  "Return last input as a string."
  (buffer-substring-no-properties
   comint-last-input-start
   comint-last-input-end))

;;;; Input from shell command history using completion

(defun prot-shell--build-input-history ()
  "Return `comint-input-ring' as a list."
  (when (and (ring-p comint-input-ring)
	         (not (ring-empty-p comint-input-ring)))
    (let (history)
      ;; We have to build up a list ourselves from the ring vector.
      (dotimes (index (ring-length comint-input-ring))
        (push (ring-ref comint-input-ring index) history))
      (delete-dups history))))

(defvar prot-shell--input-history-completion-history nil
  "Minibuffer history of `prot-shell--input-history-prompt'.
Not to be confused with the shell input history, which is stored
in the `comint-input-ring' (see `prot-shell--build-input-history').")

(defun prot-shell--input-history-prompt ()
  "Prompt for completion against `prot-shell--build-input-history'."
  (let* ((history (prot-shell--build-input-history))
         (default (car history)))
    (completing-read
     (format-prompt "Insert input from history" default)
     history nil :require-match nil
     'prot-shell--input-history-completion-history
     default)))

;;;###autoload
(defun prot-shell-input-from-history ()
  "Insert command from shell input history.
Only account for the history Emacs knows about, ignoring
`comint-input-ring-file-name' (e.g. ~/.bash_history)."
  (declare (interactive-only t))
  (interactive)
  (prot-shell--insert-and-send
   (prot-shell--input-history-prompt)))

;;;; Directory navigation

;;;;; Directory tracking

(defvar prot-shell-cd-directories nil
  "List of accumulated `shell-last-dir'.")

(with-eval-after-load 'savehist
  (add-to-list 'savehist-additional-variables 'prot-shell-cd-directories))

(defun prot-shell-track-cd (&rest _)
  "Track shell input of cd commands.
Push `shell-last-dir' to `prot-shell-cd-directories'."
  (when-let* ((input (prot-shell--last-input))
              ((string-match-p "cd " input)))
    (push shell-last-dir prot-shell-cd-directories)))

(defun prot-shell-update-name-on-cd (&rest _)
  "Update the shell buffer name after a cd for use in `prot-shell'."
  (when-let* ((input (prot-shell--last-input))
              ((string-match-p "cd " input)))
    (rename-buffer (format "*prot-shell in %s*" default-directory) :make-unique)))

(defvar prot-shell--cd-history nil
  "Minibuffer history for `prot-shell-cd'.")

(defun prot-shell--cd-prompt ()
  "Prompt for a directory among `prot-shell-cd-directories'."
  (if-let* ((history prot-shell-cd-directories)
            (dirs (cons default-directory history))
            (def (if (listp dirs) (car dirs) shell-last-dir)))
      (completing-read
       (format-prompt "Select directory" def)
       dirs nil :require-match nil 'prot-shell--cd-history def)
    (user-error "No directories have been tracked")))

;;;###autoload
(defun prot-shell-cd ()
  "Switch to `prot-shell-cd-directories' using minibuffer completion."
  (declare (interactive-only t))
  (interactive)
  (prot-shell--insert-and-send
   "cd"
   (prot-shell--cd-prompt)))

;;;;; VC root directory

(defun prot-shell--get-vc-root-dir ()
  "Return `vc-root-dir' or root of present Git repository."
  (or (vc-root-dir)
      (locate-dominating-file "." ".git")))

;;;###autoload
(defun prot-shell-cd-vc-root-dir ()
  "Change into the `vc-root-dir'."
  (interactive)
  (if-let* ((root (prot-shell--get-vc-root-dir)))
      (prot-shell--insert-and-send "cd" root)
    (user-error "Cannot find the VC root of `%s'" default-directory)))

;;;; Bookmark support

;; NOTE 2023-08-18: I sent this to the Emacs maintainers as a patch
;; (bug#65039).  I received approval to proceed with the change, but I
;; did not do it because a user reported an issue with SSH (TRAMP).  I
;; do not have access to SSH and am not familiar with such workflows.
;; If/when that changes, I will try again.  In the meantime, this is
;; good code and it works for me.

;; Adapted from esh-mode.el
(declare-function bookmark-prop-get "bookmark" (bookmark prop))

(defun prot-shell-bookmark-name ()
  "Return name of bookmark based on currect directory."
  (format "prot-shell-%s"
          (file-name-nondirectory
           (directory-file-name
            (file-name-directory default-directory)))))

(defvar sh-shell-file)

(defun prot-shell-bookmark-make-record ()
  "Create a bookmark for the current Shell buffer."
  `(,(prot-shell-bookmark-name)
    (location . ,default-directory)
    (shell-file-name . ,sh-shell-file)
    (handler . prot-shell-bookmark-jump)))

;;;###autoload
(defun prot-shell-bookmark-jump (bookmark)
  "Default BOOKMARK handler for Shell buffers."
  (let ((default-directory (bookmark-prop-get bookmark 'location))
        (explicit-shell-file-name (bookmark-prop-get bookmark 'shell-file-name)))
    (shell (get-buffer-create (car bookmark)))))

(put 'prot-shell-bookmark-jump 'bookmark-handler-type "Shell")

;; ;;;; Convert YouTube links to Invidious
;;
;; (defvar prot-shell-invidious-domains
;;   '("invidious.io.lol"
;;     "invidious.lunar.icu"
;;     "iv.nboeck.de"
;;     "vid.priv.au"
;;     "invidious.tiekoetter.com"
;;     "inv.in.projectsegfau.lt"
;;     "onion.tube"
;;     "yt.artemislena.eu"
;;     "invidious.no-logs.com"
;;     "yewtu.be"
;;     "invidious.projectsegfau.lt"
;;     "yt.oelrichsgarcia.de"
;;     "invidious.0011.lt"
;;     "inv.zzls.xyz"
;;     "inv.bp.projectsegfau.lt"
;;     "invidious.flokinet.to"
;;     "iv.ggtyler.dev"
;;     "invidious.slipfox.xyz"
;;     "vid.puffyan.us"
;;     "inv.pistasjis.net"
;;     "inv.citw.lgbt"
;;     "invidious.protokolla.fi"
;;     "inv.makerlab.tech"
;;     "inv.tux.pizza"
;;     "invidious.privacydev.net")
;;   "List of Invidious domains.")
;;
;; (defvar prot-shell-youtube-domains
;;   '("www.youtube.com"
;;     "youtu.be")
;;   "List of YouTube domains.")
;;
;; (defvar prot-shell-yt-invidious-domains
;;   (append prot-shell-youtube-domains
;;           prot-shell-invidious-domains)
;;   "List of YouTube and Invidious domains.")
;;
;; (defun prot-shell--get-random-invidious-instance ()
;;   "Return `random' index from `prot-shell-invidious-domains'."
;;   (nth
;;    (random (length prot-shell-invidious-domains))
;;    prot-shell-invidious-domains))
;;
;; ;;;###autoload
;; (defun prot-shell-invidious ()
;;   "Convert `prot-shell-yt-invidious-domains' into a random Invidious instance."
;;   (interactive)
;;   (save-excursion
;;     (goto-char (line-beginning-position))
;;     (while (re-search-forward (regexp-opt prot-shell-yt-invidious-domains) (line-end-position) :no-error)
;;       (replace-match (prot-shell--get-random-invidious-instance)))))

;;;; Built-in Emacs commands

;; ;; `comint-input-filter-functions'
;; (defun prot-shell--intercept-input (input)
;;   (when (string-match-p "man " input)
;;     (comint-interrupt-subjob)
;;     ;; TODO 2023-08-18: The idea is to interrupt the input, and split
;;     ;; it such that, say, "man echo" becomes (man "echo")
;;     ;;
;;     ;; (let ((proc (get-buffer-process (current-buffer)))
;; 	;;       (inhibit-read-only t)
;; 	;;       replacement)
;;     ;;   (save-excursion
;;     ;;     (let ((pmark (progn (goto-char (process-mark proc))
;; 	;; 		                (forward-line 0)
;; 	;; 		                (point-marker))))
;; 	;;       (delete-region comint-last-input-end pmark)
;; 	;;       (goto-char (process-mark proc))
;; 	;;       (setq replacement (concat "*** Called command externally ***\n"
;; 	;; 			                    (buffer-substring pmark (point))))
;; 	;;       (delete-region pmark (point))))
;;     ;;   (comint-output-filter proc replacement))
;;     ))
;;
;; (add-hook 'comint-input-filter-functions #'prot-shell--intercept-input)

;;;; General commands

(defun prot-shell--history-or-motion (history-fn motion-fn arg)
  "Call HISTORY-FN or MOTION-FN with ARG depending on where point is.
If `prot-shell--beginning-of-prompt-p' returns non-nil call
HISTORY-FN, else MOTION-FN."
  (let ((fn (if (or (prot-shell--beginning-of-prompt-p)
                    (eq last-command 'comint-next-input)
                    (eq last-command 'comint-previous-input))
                history-fn
              motion-fn)))
    (funcall-interactively fn arg)
    (setq this-command fn)))

;;;###autoload
(defun prot-shell-up-dwim (arg)
  "Return previous ARGth history input or go ARGth lines up.
If point is at the beginning of a shell prompt, return previous
input, otherwise perform buffer motion."
  (interactive "^p")
  (prot-shell--history-or-motion 'comint-previous-input 'previous-line arg))

;;;###autoload
(defun prot-shell-down-dwim (arg)
  "Return next ARGth history input or or go ARGth lines down.
If point is at the beginning of a shell prompt, return previous
input, otherwise perform buffer motion."
  (interactive "^p")
  (prot-shell--history-or-motion 'comint-next-input 'next-line arg))

;;;###autoload
(defun prot-shell ()
  "Like `shell' but always start a new shell.
Name the shell buffer after the `default-directory'.  If the name of
that buffer already exists, then reuse it."
  (interactive)
  (with-current-buffer (shell (format "*prot-shell in %s*" default-directory))
    (add-hook 'comint-output-filter-functions #'prot-shell-update-name-on-cd nil :local)))

;;;; Minor mode setup

(defvar-keymap prot-shell-mode-map
  :doc "Key map for `prot-shell-mode'."
  "<up>" #'prot-shell-up-dwim
  "<down>" #'prot-shell-down-dwim
  "C-c C-d" #'prot-shell-cd
  ;; "C-c C-i" #'prot-shell-invidious
  "C-c C-j" #'prot-shell-input-from-history
  "C-c C-." #'prot-shell-cd-vc-root-dir
  "C-c C-r" #'prot-shell-cd-vc-root-dir)

(define-minor-mode prot-shell-mode
  "Provide extra functionality for the Emacs `shell'.
Add a bookmark handler for shell buffer and activate the
`prot-shell-mode-map':
\\{prot-shell-mode-map}"
  :init-value nil
  :global nil
  (if prot-shell-mode
      (progn
        (add-hook 'comint-output-filter-functions #'prot-shell-track-cd nil :local)
        (setq-local bookmark-make-record-function #'prot-shell-bookmark-make-record))
    (remove-hook 'comint-output-filter-functions #'prot-shell-track-cd :local)
    (setq-local bookmark-make-record-function nil)))

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

6.20. The prot-simple.el library

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

;; Copyright (C) 2020-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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)

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

;;; Commands

;;;; General commands

(defun prot-simple--mark (bounds)
  "Mark between BOUNDS as a cons cell of beginning and end positions."
  (push-mark (car bounds))
  (goto-char (cdr bounds))
  (activate-mark))

;;;###autoload
(defun prot-simple-mark-sexp ()
  "Mark symbolic expression at or near point.
Repeat to extend the region forward to the next symbolic
expression."
  (interactive)
  (if (and (region-active-p)
           (eq last-command this-command))
      (ignore-errors (forward-sexp 1))
    (when-let* ((thing (cond
                        ((thing-at-point 'url) 'url)
                        ((thing-at-point 'sexp) 'sexp)
                        ((thing-at-point 'string) 'string)
                        ((thing-at-point 'word) 'word))))
      (prot-simple--mark (bounds-of-thing-at-point thing)))))

;;;###autoload
(defun prot-simple-keyboard-quit-dwim ()
  "Do-What-I-Mean behaviour for a general `keyboard-quit'.

The generic `keyboard-quit' does not do the expected thing when
the minibuffer is open.  Whereas we want it to close the
minibuffer, even without explicitly focusing it.

The DWIM behaviour of this command is as follows:

- When the region is active, disable it.
- When a minibuffer is open, but not focused, close the minibuffer.
- When the Completions buffer is selected, close it.
- In every other case use the regular `keyboard-quit'."
  (interactive)
  (cond
   ((region-active-p)
    (keyboard-quit))
   ((derived-mode-p 'completion-list-mode)
    (delete-completion-window))
   ((> (minibuffer-depth) 0)
    (abort-recursive-edit))
   (t
    (keyboard-quit))))

;; DEPRECATED 2023-12-26: I have not used `prot-simple-describe-symbol'
;; since a very long time.  The idea is fine, but having a key binding
;; to provide a shortcut for C-h o RET is wasteful.

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

;; DEPRECATED 2023-12-26: The `prot-simple-goto-definition' is a good
;; idea but it needs more work.  Ultimately though, it is easier to
;; just produce a Help buffer and just go to the source from there by
;; typing 's'.

;; (declare-function help--symbol-completion-table "help-fns" (string pred action))
;;
;; ;;;###autoload
;; (defun prot-simple-goto-definition (symbol)
;;   "Prompt for SYMBOL and go to its source.
;; When called from Lisp, SYMBOL is a string."
;;   (interactive
;;    (list
;;     (completing-read "Go to source of SYMBOL: "
;;                      #'help--symbol-completion-table
;;                      nil :require-match)))
;;   (xref-find-definitions symbol))

;; DEPRECATED 2023-12-26: I have no need for these commands.  I was
;; just experimenting with a simple implementation.  It is not robust.
;; I can fix it, but I will still not use it, so I am deprecating it
;; instead.

;; (autoload 'number-at-point "thingatpt")
;;
;; (defun prot-simple--number-operate (number amount operation)
;;   "Perform OPERATION on NUMBER given AMOUNT and return the result.
;; OPERATION is the keyword `:increment' or `:decrement' to perform
;; `1+' or `1-', respectively."
;;   (when (and (numberp number) (numberp amount))
;;     (let ((fn (pcase operation
;;                 (:increment #'+)
;;                 (:decrement #'-)
;;                 (_ (user-error "Unknown operation `%s' for number `%s'" operation number)))))
;;       (funcall fn number amount))))
;;
;; (defun prot-simple--number-replace (number amount operation)
;;   "Perform OPERATION on NUMBER at point by AMOUNT."
;;   (when-let* ((bounds (bounds-of-thing-at-point 'number))
;;               (replacement (prot-simple--number-operate number amount operation)))
;;     (delete-region (car bounds) (cdr bounds))
;;     (save-excursion
;;       (insert (number-to-string replacement)))))
;;
;; ;;;###autoload
;; (defun prot-simple-number-increment (number amount)
;;   "Increment NUMBER by AMOUNT.
;; When called interactively, NUMBER is the one at point, while
;; AMOUNT is either 1 or that of a number prefix argument."
;;   (interactive
;;    (list
;;     (number-at-point)
;;     (prefix-numeric-value current-prefix-arg)))
;;   (prot-simple--number-replace number amount :increment))
;;
;; ;;;###autoload
;; (defun prot-simple-number-decrement (number amount)
;;   "Decrement NUMBER by AMOUNT.
;; When called interactively, NUMBER is the one at point, while
;; AMOUNT is either 1 or that of a number prefix argument."
;;   (interactive
;;    (list
;;     (number-at-point)
;;     (prefix-numeric-value current-prefix-arg)))
;;   (prot-simple--number-replace number amount :decrement))

;;;; Commands for lines

;;;###autoload
(defun prot-simple-new-line-below (n)
  "Create N empty lines below the current one.
When called interactively without a prefix numeric argument, N is
1."
  (interactive "p")
  (goto-char (line-end-position))
  (dotimes (_ n) (insert "\n")))

;;;###autoload
(defun prot-simple-new-line-above (n)
  "Create N empty lines above the current one.
When called interactively without a prefix numeric argument, N is
1."
  (interactive "p")
  (let ((point-min (point-min)))
    (if (or (bobp)
            (eq (point) point-min)
            (eq (line-number-at-pos point-min) 1))
        (progn
          (goto-char (line-beginning-position))
          (dotimes (_ n) (insert "\n"))
          (forward-line (- n)))
      (forward-line (- n))
      (prot-simple-new-line-below n))))

;;;###autoload
(defun prot-simple-copy-line ()
  "Copy the current line to the `kill-ring'."
  (interactive)
  (copy-region-as-kill (line-beginning-position) (line-end-position)))

(make-obsolete 'prot-simple-copy-line-or-region 'prot-simple-copy-line "2023-09-26")

;;;###autoload
(defun prot-simple-kill-ring-save (beg end)
  "Copy the current region or line.
When the region is active, use `kill-ring-save' between the BEG and END
positions.  Otherwise, copy the current line."
  (interactive "r")
  (if (region-active-p)
      (kill-ring-save beg end)
    (prot-simple-copy-line)))

(defun prot-simple--duplicate-buffer-substring (boundaries)
  "Duplicate buffer substring between BOUNDARIES.
BOUNDARIES is a cons cell representing buffer positions."
  (unless (consp boundaries)
    (error "`%s' is not a cons cell" boundaries))
  (let ((beg (car boundaries))
        (end (cdr boundaries)))
    (goto-char end)
    (newline)
    (insert (buffer-substring-no-properties beg end))))

;;;###autoload
(defun prot-simple-duplicate-line-or-region ()
  "Duplicate the current line or active region."
  (interactive)
  (unless mark-ring                  ; needed when entering a new buffer
    (push-mark (point) t nil))
  (prot-simple--duplicate-buffer-substring
   (if (region-active-p)
       (cons (region-beginning) (region-end))
     (cons (line-beginning-position) (line-end-position)))))

;;;###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 (line-beginning-position) (line-end-position)))
  (yank))

;;;###autoload
(defun prot-simple-multi-line-below ()
  "Move half a screen below."
  (interactive)
  (forward-line (floor (window-height) 2))
  (setq this-command 'scroll-up-command))

;;;###autoload
(defun prot-simple-multi-line-above ()
  "Move half a screen above."
  (interactive)
  (forward-line (- (floor (window-height) 2)))
  (setq this-command 'scroll-down-command))

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

;;;###autoload
(define-minor-mode prot-simple-auto-fill-visual-line-mode
  "Enable `visual-line-mode' and disable `auto-fill-mode' in the current buffer."
  :global nil
  (if prot-simple-auto-fill-visual-line-mode
      (progn
        (auto-fill-mode -1)
        (visual-line-mode 1))
    (auto-fill-mode 1)
    (visual-line-mode -1)))

;;;; Commands for text insertion or manipulation

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

(defun prot-simple--pos-url-on-line (char)
  "Return position of `prot-common-url-regexp' at CHAR."
  (when (integer-or-marker-p char)
    (save-excursion
      (goto-char char)
      (re-search-forward prot-common-url-regexp (line-end-position) :noerror))))

;;;###autoload
(defun prot-simple-escape-url-line (char)
  "Escape all URLs or email addresses on the current line.
When called from Lisp CHAR is a buffer position to operate from
until the end of the line.  In interactive use, CHAR corresponds
to `line-beginning-position'."
  (interactive
   (list
    (if current-prefix-arg
        (re-search-forward
         prot-common-url-regexp
         (line-end-position) :no-error
         (prefix-numeric-value current-prefix-arg))
      (line-beginning-position))))
  (when-let* ((regexp-end (prot-simple--pos-url-on-line char)))
    (goto-char regexp-end)
    (unless (looking-at ">")
      (insert ">")
      (when (search-backward "\s" (line-beginning-position) :noerror)
        (forward-char 1))
      (insert "<"))
    (prot-simple-escape-url-line (1+ regexp-end)))
  (goto-char (line-end-position)))

;; Thanks to Bruno Boal for the original `prot-simple-escape-url-region'.
;; Check Bruno's Emacs config: <https://github.com/BBoal/emacs-config>.

;;;###autoload
(defun prot-simple-escape-url-region (&optional beg end)
  "Apply `prot-simple-escape-url-line' on region lines between BEG and END."
  (interactive
   (if (region-active-p)
       (list (region-beginning) (region-end))
     (error "There is no region!")))
  (let ((beg (min beg end))
        (end (max beg end)))
    (save-excursion
      (goto-char beg)
      (setq beg (line-beginning-position))
      (while (<= beg end)
        (prot-simple-escape-url-line beg)
        (beginning-of-line 2)
        (setq beg (point))))))

;;;###autoload
(defun prot-simple-escape-url-dwim ()
  "Escape URL on the current line or lines implied by the active region.
Call the commands `prot-simple-escape-url-line' and
`prot-simple-escape-url-region' ."
  (interactive)
  (if (region-active-p)
      (prot-simple-escape-url-region (region-beginning) (region-end))
    (prot-simple-escape-url-line (line-beginning-position))))

;;;###autoload
(defun prot-simple-zap-to-char-backward (char &optional arg)
  "Backward `zap-to-char' for CHAR.
Optional ARG is a numeric prefix to match ARGth occurance of
CHAR."
  (interactive
   (list
    (read-char-from-minibuffer "Zap to char: " nil 'read-char-history)
    (prefix-numeric-value current-prefix-arg)))
  (zap-to-char (- arg) char t))

(defvar prot-simple-flush-and-diff-history nil
  "Minibuffer history for `prot-simple-flush-and-diff'.")

;;;###autoload
(defun prot-simple-flush-and-diff (regexp beg end)
  "Call `flush-lines' for REGEXP and produce diff if file is modified.
When region is active, operate between the region boundaries
demarcated by BEG and END."
  (interactive
   (let ((regionp (region-active-p)))
     (list
      (read-regexp "Flush lines using REGEXP: " nil 'prot-simple-flush-and-diff-history)
      (and regionp (region-beginning))
      (and regionp (region-end)))))
  (flush-lines regexp (or beg (point-min)) (or end (point-max)) :no-message)
  (when (and (buffer-modified-p) buffer-file-name)
    (diff-buffer-with-file (current-buffer))))

;; FIXME 2023-09-28: The line prefix is problematic.  I plan to rewrite it.

;; (defcustom prot-simple-line-prefix-strings '(">" "+" "-")
;;   "List of strings used as line prefixes.
;; The command which serves as the point of entry is
;; `prot-simple-insert-line-prefix'."
;;   :type '(repeat string)
;;   :group 'prot-simple)
;;
;; (defun prot-simple--line-prefix-regexp (&optional string)
;;   "Format regular expression for `prot-simple--line-prefix-p'.
;; With optional STRING use it directly.  Else format the regexp by
;; concatenating `prot-simple-line-prefix-strings'."
;;   (if string
;;       (format "^%s " string)
;;     (format "^[%s] " (apply #'concat prot-simple-line-prefix-strings))))
;;
;; (defun prot-simple--line-prefix-p (&optional string)
;;   "Return non-nil if line beginning has an appropriate string prefix.
;; With optional STRING test that it is at the beginning of the line."
;;   (save-excursion
;;     (goto-char (line-beginning-position))
;;     (looking-at (prot-simple--line-prefix-regexp string))))
;;
;; (defun prot-simple--line-prefix-insert (string)
;;   "Insert STRING at the beginning of the line, followed by a space."
;;   (save-excursion
;;     (goto-char (line-beginning-position))
;;     (insert string)
;;     (insert " ")))
;;
;; (defun prot-simple--line-prefix-infer-string ()
;;   "Return line prefix string if it matches `prot-simple--line-prefix-p'."
;;   (when (prot-simple--line-prefix-p)
;;     (string-trim
;;      (buffer-substring-no-properties (match-beginning 0) (match-end 0)))))
;;
;; (defun prot-simple--line-prefix-toggle (string)
;;   "Insert or remove STRING at the beginning of the line."
;;   (if (prot-simple--line-prefix-p string)
;;       (delete-region (match-beginning 0) (match-end 0))
;;     (prot-simple--line-prefix-insert string)))
;;
;; (defvar prot-simple--line-prefix-history nil
;;   "Minibuffer history of `prot-simple--line-prefix-prompt'.")
;;
;; (defun prot-simple--line-prefix-prompt ()
;;   "Prompt for string to use as line prefix.
;; Provide `prot-simple-line-prefix-strings' as completion
;; candidates, though accept arbitrary input."
;;   (let ((default (car prot-simple--line-prefix-history)))
;;     (completing-read
;;      (format-prompt "Select line prefix" default)
;;      prot-simple-line-prefix-strings
;;      nil nil nil
;;      'prot-simple--line-prefix-history default)))
;;
;; (defun prot-simple-line-prefix-infer-or-prompt ()
;;   "Infer string for line prefix or prompt for one."
;;   (or (prot-simple--line-prefix-infer-string)
;;       (prot-simple--line-prefix-prompt)))
;;
;; ;;;###autoload
;; (defun prot-simple-insert-line-prefix-dwim (string)
;;   "Toggle presence of STRING at the beginning of the line.
;;
;; When called interactively try to infer STRING based on the line
;; prefix.  If one is found among `prot-simple-line-prefix-strings',
;; perform a removal outright.
;;
;; If no string can be inferred, prompt for STRING among
;; `prot-simple-line-prefix-strings'.  Accept arbitrary strings at
;; the prompt.
;;
;; When the region is active, toggle the presence of STRING for each
;; line in the region."
;;   (interactive (list (prot-simple-line-prefix-infer-or-prompt)))
;;   (if-let* ((region-p (region-active-p))
;;             (beg (region-beginning))
;;             (end (line-number-at-pos (region-end))))
;;       (progn
;;         (goto-char beg)
;;         (push-mark (point))
;;         (while (<= (line-number-at-pos (point)) end)
;;           (prot-simple--line-prefix-toggle string)
;;           (forward-line 1)))
;;     (prot-simple--line-prefix-toggle string)))

;;;; Commands for object transposition

;; The "move" functions all the way to `prot-simple-move-below-dwim'
;; are courtesy of Bruno Boal: <https://git.sr.ht/~bboal>.  With minor
;; tweaks by me.
(defun prot-simple--move-line (count dir)
  "Move line or region COUNTth times in DIR direction."
  (let* ((start (pos-bol))
         (end (pos-eol))
         diff-eol-point
         diff-eol-mark)
    (when-let* (((use-region-p))
                (pos (point))
                (mrk (mark))
                (line-diff-mark-point (1+ (- (line-number-at-pos mrk)
                                             (line-number-at-pos pos)))))
      (if (> pos mrk)
          (setq start (pos-bol line-diff-mark-point)) ; pos-bol of where the mark is
        (setq end (pos-eol line-diff-mark-point)))    ; pos-eol of the line where the mark is
      (setq diff-eol-mark (1+ (- end mrk))))          ; 1+ to get the \n
    ;; this is valid for region or a single line
    (setq diff-eol-point (1+ (- end (point))))
    (let* ((max (point-max))
           (end (1+ end))
           (end (if (> end max) max end))
           (deactivate-mark)
           (lines (delete-and-extract-region start end)))
      (forward-line (* count dir))
      ;; Handle the special case when there isn't a newline as the eob.
      (when (and (eq (point) max)
                 (/= (current-column) 0))
        (insert "\n"))
      (insert lines)
      ;; if user provided a region
      (when diff-eol-mark
        (set-mark (- (point) diff-eol-mark)))
      ;; either way go to same point location reference initial motion
      (goto-char (- (point) diff-eol-point)))))

(defun prot-simple--move-line-user-error (boundary)
  "Return `user-error' with message accounting for BOUNDARY.
BOUNDARY is a buffer position, expected to be `point-min' or `point-max'."
  (when-let* ((bound (line-number-at-pos boundary))
              (scope (cond
                      ((and (use-region-p)
                            (or (= (line-number-at-pos (point)) bound)
                                (= (line-number-at-pos (mark)) bound)))
                       "region is ")
                      ((= (line-number-at-pos (point)) bound)
                       "")
                      (t nil))))
    (user-error (format "Warning: %salready in the last line!" scope))))

(defun prot-simple-move-above-dwim (arg)
  "Move line or region ARGth times up.
If ARG is nil, do it one time."
  (interactive "p")
  (unless (prot-simple--move-line-user-error (point-min))
    (prot-simple--move-line arg -1)))

(defun prot-simple-move-below-dwim (arg)
  "Move line or region ARGth times down.
If ARG is nil, do it one time."
  (interactive "p")
  (unless (prot-simple--move-line-user-error (point-max))
    (prot-simple--move-line arg 1)))

(defmacro prot-simple-define-transpose (scope)
  "Define transposition command for SCOPE.
SCOPE is the text object to operate on.  The command's name is
prot-simple-transpose-SCOPE."
  `(defun ,(intern (format "prot-simple-transpose-%s" scope)) (arg)
     ,(format "Transpose %s.
Transposition over an active region will swap the object at
the region beginning with the one at the region end." scope)
     (interactive "p")
     (let ((fn (intern (format "%s-%s" "transpose" ,scope))))
       (if (use-region-p)
           (funcall fn 0)
         (funcall fn arg)))))

;;;###autoload (autoload 'prot-simple-transpose-lines "prot-simple")
;;;###autoload (autoload 'prot-simple-transpose-paragraphs "prot-simple")
;;;###autoload (autoload 'prot-simple-transpose-sentences "prot-simple")
;;;###autoload (autoload 'prot-simple-transpose-sexps "prot-simple")
;;;###autoload (autoload 'prot-simple-transpose-words "prot-simple")
(prot-simple-define-transpose "lines")
(prot-simple-define-transpose "paragraphs")
(prot-simple-define-transpose "sentences")
(prot-simple-define-transpose "sexps")
(prot-simple-define-transpose "words")

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

;;;; Commands for paragraphs

;;;###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 and pages

;;;###autoload
(defun prot-simple-other-window ()
  "Wrapper for `other-window' and `next-multiframe-window'.
If there is only one window and multiple frames, call
`next-multiframe-window'.  Otherwise, call `other-window'."
  (interactive)
  (if (and (one-window-p) (length> (frame-list) 1))
      (progn
        (call-interactively #'next-multiframe-window)
        (setq this-command #'next-multiframe-window))
    (call-interactively #'other-window)
    (setq this-command #'other-window)))

;;;###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 (- (cdr bounds) (car bounds)))
         (buffer-area (- (point-max) (point-min))))
    (if (/= buffer-area window-area)
        (narrow-to-region (car bounds) (cdr 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 pages are defined by virtue of `prot-common-page-p', narrow to
the current page boundaries.

If no region is active and no pages exist, 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)))
    (narrow-to-region (region-beginning) (region-end)))
   ((prot-common-page-p)
    (narrow-to-page))
   ((null (buffer-narrowed-p))
    (prot-simple-narrow-visible-window))
   ((widen))))

(defun prot-simple--narrow-to-page (count &optional back)
  "Narrow to COUNTth page with optional BACK motion."
  (if back
      (narrow-to-page (or (- count) -1))
    (narrow-to-page (or (abs count) 1)))
  ;; Avoids the problem of skipping pages while cycling back and forth.
  (goto-char (point-min)))

;;;###autoload
(defun prot-simple-forward-page-dwim (&optional count)
  "Move to next or COUNTth page forward.
If buffer is narrowed to the page, keep the effect while
performing the motion.  Always move point to the beginning of the
narrowed page."
  (interactive "p")
  (if (buffer-narrowed-p)
      (prot-simple--narrow-to-page count)
    (forward-page count)
    (setq this-command 'forward-page)))

;;;###autoload
(defun prot-simple-backward-page-dwim (&optional count)
  "Move to previous or COUNTth page backward.
If buffer is narrowed to the page, keep the effect while
performing the motion.  Always move point to the beginning of the
narrowed page."
  (interactive "p")
  (if (buffer-narrowed-p)
      (prot-simple--narrow-to-page count t)
    (backward-page count)
    (setq this-command 'backward-page)))

;;;###autoload
(defun prot-simple-delete-page-delimiters (&optional beg end)
  "Delete lines with just page delimiters in the current buffer.
When region is active, only operate on the region between BEG and
END, representing the point and mark."
  (interactive "r")
  (let (b e)
    (if (use-region-p)
        (setq b beg
              e end)
      (setq b (point-min)
            e (point-max)))
    (widen)
    (flush-lines (format "%s$" page-delimiter) b e)
    (setq this-command 'flush-lines)))

;; NOTE 2023-06-18: The idea of narrowing to a defun in an indirect
;; buffer is still experimental.
(defun prot-simple-narrow--guess-defun-symbol ()
  "Try to return symbol of current defun as a string."
  (save-excursion
    (beginning-of-defun)
    (search-forward " ")
    (thing-at-point 'symbol :no-properties)))

;;;###autoload
(defun prot-simple-narrow-to-cloned-buffer ()
  "Narrow to defun in cloned buffer.
Name the buffer after the defun's symbol."
  (interactive)
  (clone-indirect-buffer-other-window
   (format "%s -- %s"
           (buffer-name)
           (prot-simple-narrow--guess-defun-symbol))
   :display)
  (narrow-to-defun))

;;;; Commands for buffers

(defun prot-simple--display-unsaved-buffers (buffers buffer-menu-name)
  "Produce buffer menu listing BUFFERS called BUFFER-MENU-NAME."
  (let ((old-buf (current-buffer))
        (buf (get-buffer-create buffer-menu-name)))
    (with-current-buffer buf
      (Buffer-menu-mode)
      (setq-local Buffer-menu-files-only nil
                  Buffer-menu-buffer-list buffers
                  Buffer-menu-filter-predicate nil)
      (list-buffers--refresh buffers old-buf)
      (tabulated-list-print))
    (display-buffer buf)))

(defun prot-simple--get-unsaved-buffers ()
  "Get list of unsaved buffers."
  (seq-filter
   (lambda (buffer)
     (and (buffer-file-name buffer)
          (buffer-modified-p buffer)))
   (buffer-list)))

;;;###autoload
(defun prot-simple-display-unsaved-buffers ()
  "Produce buffer menu listing unsaved file-visiting buffers."
  (interactive)
  (if-let* ((unsaved-buffers (prot-simple--get-unsaved-buffers)))
      (prot-simple--display-unsaved-buffers unsaved-buffers "*Unsaved buffers*")
    (message "No unsaved buffers")))

(defun prot-simple-display-unsaved-buffers-on-exit (&rest _)
  "Produce buffer menu listing unsaved file-visiting buffers.
Add this as :before advice to `save-buffers-kill-emacs'."
  (when-let* ((unsaved-buffers (prot-simple--get-unsaved-buffers)))
    (prot-simple--display-unsaved-buffers unsaved-buffers "*Unsaved buffers*")))

;;;###autoload
(defun prot-simple-copy-current-buffer-name ()
  "Add the current buffer's name to the `kill-ring'."
  (declare (interactive-only t))
  (interactive)
  (kill-new (buffer-name (current-buffer))))

;;;###autoload
(defun prot-simple-copy-current-buffer-file ()
  "Add the current buffer's file path to the `kill-ring'."
  (declare (interactive-only t))
  (interactive)
  (if buffer-file-name
      (kill-new buffer-file-name)
    (user-error "%s is not associated with a file" (buffer-name (current-buffer)))))

;;;###autoload
(defun prot-simple-kill-buffer (buffer)
  "Kill current BUFFER without confirmation.
When called interactively, prompt for BUFFER."
  (interactive (list (read-buffer "Select buffer: ")))
  (let ((kill-buffer-query-functions nil))
    (kill-buffer (or buffer (current-buffer)))))

;;;###autoload
(defun prot-simple-kill-buffer-current (&optional arg)
  "Kill current buffer.
With optional prefix ARG (\\[universal-argument]) delete the
buffer's window as well.  Kill the window regardless of ARG if it
satisfies `prot-common-window-small-p' and it has no previous
buffers in its history."
  (interactive "P")
  (let ((kill-buffer-query-functions nil))
    (if (or (and (prot-common-window-small-p)
                 (null (window-prev-buffers)))
            (and arg (not (one-window-p))))
        (kill-buffer-and-window)
      (kill-buffer))))

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

(defun prot-simple--buffer-major-mode-prompt ()
  "Prompt of `prot-simple-buffers-major-mode'.
Limit list of buffers to those matching the current
`major-mode' or its derivatives."
  (let ((read-buffer-function nil)
        (current-major-mode major-mode))
    (read-buffer
     (format "Buffer for %s: " major-mode)
     nil
     :require-match
     (lambda (pair) ; pair is (name-string . buffer-object)
       (with-current-buffer (cdr pair)
         (derived-mode-p current-major-mode))))))

;;;###autoload
(defun prot-simple-buffers-major-mode ()
  "Select BUFFER matching the current one's major mode."
  (interactive)
  (switch-to-buffer (prot-simple--buffer-major-mode-prompt)))

(defun prot-simple--buffer-vc-root-prompt ()
  "Prompt of `prot-simple-buffers-vc-root'."
  (let ((root (or (vc-root-dir)
                  (locate-dominating-file "." ".git")))
        (read-buffer-function nil))
    (read-buffer
     (format "Buffers in %s: " root)
     nil t
     (lambda (pair) ; pair is (name-string . buffer-object)
       (with-current-buffer (cdr pair) (string-match-p root default-directory))))))

;;;###autoload
(defun prot-simple-buffers-vc-root ()
  "Select buffer matching the current one's VC root."
  (interactive)
  (switch-to-buffer (prot-simple--buffer-vc-root-prompt)))

;;;###autoload
(defun prot-simple-swap-window-buffers (counter)
  "Swap states of live buffers.
With two windows, transpose their buffers.  With more windows,
perform a clockwise rotation.  Do not alter the window layout.
Just move the buffers around.

With COUNTER as a prefix argument, do the rotation
counter-clockwise."
  (interactive "P")
  (when-let* ((winlist (if counter (reverse (window-list)) (window-list)))
              (wincount (count-windows))
              ((> wincount 1)))
    (dotimes (i (- wincount 1))
      (window-swap-states (elt winlist i) (elt winlist (+ i 1))))))

;;;; Commands for files

(cl-defmethod register--type ((_regval vector)) 'vector)

(cl-defmethod register-val-describe ((val vector) _verbose)
  (if-let* ((pos (aref val 2))
            (file (aref val 1)))
      (princ (format "%s at position %s" file pos))
    (princ "Garbage data")))

;;;###autoload
(defun prot-simple-file-to-register (register)
  "Store current location of file's point in REGISTER."
  (interactive (list (register-read-with-preview "File with point to register: ")))
  (set-register register (vector 'file-with-point (buffer-file-name) (point))))

(defvar prot-simple-file-to-register-jump-hook nil
  "Normal hook called after jumping to a file register.
See `prot-simple-file-to-register'.")

;;;###autoload
(cl-defmethod register-val-jump-to ((val vector) delete)
  "Handle how to jump to a location register.
This is like the default, but does not ask to visit a file: it does it
outright."
  (cond
   ((eq (aref val 0) 'file-with-point)
    (find-file (aref val 1))
    (goto-char (aref val 2))
    (run-hooks 'prot-simple-file-to-register-jump-hook))
   (t (cl-call-next-method val delete))))

;;;; Commands of a general nature

(autoload 'color-rgb-to-hex "color")
(autoload 'color-name-to-rgb "color")

(defun prot-simple-accessible-colors (variant)
  "Return list of accessible `defined-colors'.
VARIANT is either `dark' or `light'."
  (let ((variant-color (if (eq variant 'black) "#000000" "#ffffff")))
    (seq-filter
     (lambda (c)
       (let* ((rgb (color-name-to-rgb c))
              (r (nth 0 rgb))
              (g (nth 1 rgb))
              (b (nth 2 rgb))
              (hex (color-rgb-to-hex r g b 2)))
         (when (>= (prot-common-contrast variant-color hex) 4.5)
           c)))
     (defined-colors))))

(defun prot-simple--list-accessible-colors-prompt ()
  "Use `read-multiple-choice' to return white or black background."
  (intern
   (cadr
    (read-multiple-choice
     "Variant"
     '((?b "black" "Black background")
       (?w "white" "White background"))
     "Choose between white or black background."))))

;;;###autoload
(defun prot-simple-list-accessible-colors (variant)
  "Return buffer with list of accessible `defined-colors'.
VARIANT is either `dark' or `light'."
  (interactive (list (prot-simple--list-accessible-colors-prompt)))
  (list-colors-display (prot-simple-accessible-colors variant)))

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

6.21. The prot-spell.el library

;;; prot-spell.el --- Spelling-related extensions for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2021-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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:

(require 'ispell)

(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
(defun prot-spell-spell-dwim (beg end)
  "Spell check between BEG END, current word, or select a dictionary.

Use `flyspell-region' on the active region and deactivate the
mark.

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)
    (deactivate-mark))
   ((thing-at-point 'word)
    (call-interactively 'ispell-word))
   (t
    (call-interactively 'prot-spell-change-dictionary))))

(defun prot-spell-ispell-display-buffer (buffer)
  "Function to override `ispell-display-buffer' for BUFFER.
Use this as `advice-add' to override the aforementioned Ispell
function.  Then you can control the buffer's specifics via
`display-buffer-alist' (how it ought to be!)."
  (pop-to-buffer buffer)
  (set-window-point (get-buffer-window buffer) (point-min)))

(advice-add #'ispell-display-buffer :override #'prot-spell-ispell-display-buffer)

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

6.22. The prot-vertico.el library

;;; prot-vertico.el --- Custom Vertico extras -*- lexical-binding: t -*-

;; Copyright (C) 2023-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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:
;;
;; 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 'vertico)

(defvar prot-vertico-multiform-minimal
  '(unobtrusive
    (vertico-flat-format . ( :multiple  ""
                             :single    ""
                             :prompt    ""
                             :separator ""
                             :ellipsis  ""
                             :no-match  "")))
  "List of configurations for minimal Vertico multiform.
The minimal view is intended to be more private or less
revealing.  This is important when, for example, a prompt shows
names of people.  Of course, such a view also provides a minimal
style for general usage.

Toggle the vertical view with the `vertico-multiform-vertical'
command or use the commands `prot-vertico-private-next' and
`prot-vertico-private-previous', which toggle the vertical view
automatically.")

(defvar prot-vertico-multiform-maximal
  '((vertico-count . 10)
    (vertico-resize . t))
  "List of configurations for maximal Vertico multiform.")

(defun prot-vertico--match-directory (str)
  "Match directory delimiter in STR."
  (string-suffix-p "/" str))

;; From the Vertico documentation.
(defun prot-vertico-sort-directories-first (files)
  "Sort directories before FILES."
  (setq files (vertico-sort-alpha files))
  (nconc (seq-filter #'prot-vertico--match-directory files)
         (seq-remove #'prot-vertico--match-directory files)))

(defun prot-vertico-private-next ()
  "Like `vertico-next' but toggle vertical view if needed.
This is done to accommodate `prot-vertico-multiform-minimal'."
  (interactive)
  (if vertico-unobtrusive-mode
      (let ((vertico--index 0))
        (vertico-multiform-vertical)
        (vertico-next 1))
    (vertico-next 1)))

(defun prot-vertico-private-previous ()
  "Like `vertico-previous' but toggle vertical view if needed.
This is done to accommodate `prot-vertico-multiform-minimal'."
  (interactive)
  (if vertico-unobtrusive-mode
      (progn
        (vertico-multiform-vertical)
        (vertico-previous 1))
    (vertico-previous 1)))

(defun prot-vertico-private-complete ()
  "Expand contents and show remaining candidates, if needed.
This is done to accommodate `prot-vertico-multiform-minimal'."
  (interactive)
  (if (and vertico-unobtrusive-mode (> vertico--total 1))
      (progn
        (minibuffer-complete)
        (vertico-multiform-vertical))
    (vertico-insert)))

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

6.23. The prot-window.el library

;;; prot-window.el --- Display-buffer and window-related extensions for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2023-2024  Protesilaos Stavrou

;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; URL: https://protesilaos.com/emacs/dotemacs
;; Version: 0.1.0
;; Package-Requires: ((emacs "30.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 window and display-buffer 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)

(defvar prot-window-window-sizes
  '( :max-height (lambda () (floor (frame-height) 3))
     :min-height 10
     :max-width (lambda () (floor (frame-width) 4))
     :min-width 20)
  "Property list of maximum and minimum window sizes.
The property keys are `:max-height', `:min-height', `:max-width',
and `:min-width'.  They all accept a value of either a
number (integer or floating point) or a function.")

(defun prot-window--get-window-size (key)
  "Extract the value of KEY from `prot-window-window-sizes'."
  (when-let* ((value (plist-get prot-window-window-sizes key)))
    (cond
     ((functionp value)
      (funcall value))
     ((numberp value)
      value)
     (t
      (error "The value of `%s' is neither a number nor a function" key)))))

(defun prot-window-select-fit-size (window)
  "Select WINDOW and resize it.
The resize pertains to the maximum and minimum values for height
and width, per `prot-window-window-sizes'.

Use this as the `body-function' in a `display-buffer-alist' entry."
  (select-window window)
  (fit-window-to-buffer
   window
   (prot-window--get-window-size :max-height)
   (prot-window--get-window-size :min-height)
   (prot-window--get-window-size :max-width)
   (prot-window--get-window-size :min-width))
  ;; If we did not use `display-buffer-below-selected', then we must
  ;; be in a lateral window, which has more space.  Then we do not
  ;; want to dedicate the window to this buffer, because we will be
  ;; running out of space.
  (when (or (window-in-direction 'above) (window-in-direction 'below))
    (set-window-dedicated-p window t)))

(defun prot-window--get-display-buffer-below-or-pop ()
  "Return list of functions for `prot-window-display-buffer-below-or-pop'."
  (list
   #'display-buffer-reuse-mode-window
   (if (or (prot-common-window-small-p)
           (prot-common-three-or-more-windows-p))
       #'display-buffer-below-selected
     #'display-buffer-pop-up-window)))

(defun prot-window-display-buffer-below-or-pop (&rest args)
  "Display buffer below current window or pop a new window.
The criterion for choosing to display the buffer below the
current one is a non-nil return value for
`prot-common-window-small-p'.

Apply ARGS expected by the underlying `display-buffer' functions.

This as the action function in a `display-buffer-alist' entry."
  (let ((functions (prot-window--get-display-buffer-below-or-pop)))
    (catch 'success
      (dolist (fn functions)
        (when (apply fn args)
          (throw 'success fn))))))

(defun prot-window-shell-or-term-p (buffer &rest _)
  "Check if BUFFER is a shell or terminal.
This is a predicate function for `buffer-match-p', intended for
use in `display-buffer-alist'."
  (when (string-match-p "\\*.*\\(e?shell\\|v?term\\).*" (buffer-name (get-buffer buffer)))
    (with-current-buffer buffer
      ;; REVIEW 2022-07-14: Is this robust?
      (and (not (derived-mode-p 'message-mode 'text-mode))
           (derived-mode-p 'eshell-mode 'shell-mode 'comint-mode 'fundamental-mode)))))

(defun prot-window-remove-dedicated (&rest _)
  "Remove dedicated window parameter.
Use this as :after advice to `delete-other-windows' and
`delete-window'."
  (when (one-window-p :no-mini)
    (set-window-dedicated-p nil nil)))

(mapc
 (lambda (fn)
   (advice-add fn :after #'prot-window-remove-dedicated))
 '(delete-other-windows delete-window))

(defmacro prot-window-define-full-frame (name &rest args)
  "Define command to call ARGS in new frame with `display-buffer-full-frame' bound.
Name the function prot-window- followed by NAME.  If ARGS is nil,
call NAME as a function."
  (declare (indent 1))
  `(defun ,(intern (format "prot-window-%s" name)) ()
     ,(format "Call `prot-window-%s' in accordance with `prot-window-define-full-frame'." name)
     (interactive)
     (let ((display-buffer-alist '((".*" (display-buffer-full-frame)))))
       (with-selected-frame (make-frame)
         ,(if args
              `(progn ,@args)
            `(funcall ',name))
         (modify-frame-parameters nil '((buffer-list . nil)))))))

(defun prot-window--get-shell-buffers ()
  "Return list of `shell' buffers."
  (seq-filter
   (lambda (buffer)
     (with-current-buffer buffer
       (derived-mode-p 'shell-mode)))
   (buffer-list)))

(defun prot-window--get-new-shell-buffer ()
  "Return buffer name for `shell' buffers."
  (if-let* ((buffers (prot-window--get-shell-buffers))
            (buffers-length (length buffers))
            ((>= buffers-length 1)))
      (format "*shell*<%s>" (1+ buffers-length))
    "*shell*"))

;;;###autoload (autoload 'prot-window-shell "prot-window")
(prot-window-define-full-frame shell
  (let ((name (prot-window--get-new-shell-buffer)))
    (shell name)
    (set-frame-name name)
    (when-let* ((buffer (get-buffer name)))
      (with-current-buffer buffer
        (add-hook
         'delete-frame-functions
         (lambda (_)
           ;; FIXME 2023-09-09: Works for multiple frames (per
           ;; `make-frame-command'), but not if the buffer is in two
           ;; windows in the same frame.
           (unless (> (safe-length (get-buffer-window-list buffer nil t)) 1)
             (let ((kill-buffer-query-functions nil))
               (kill-buffer buffer))))
         nil
         :local)))))

;;;###autoload (autoload 'prot-window-coach "prot-window")
(prot-window-define-full-frame coach
  (let ((buffer (get-buffer-create "*scratch for coach*")))
    (with-current-buffer buffer
      (funcall initial-major-mode))
    (display-buffer buffer)
    (set-frame-name "Coach")))

;; REVIEW 2023-06-25: Does this merit a user option?  I don't think I
;; will ever set it to the left.  It feels awkward there.
(defun prot-window-scroll-bar-placement ()
  "Control the placement of scroll bars."
  (when scroll-bar-mode
    (setq default-frame-scroll-bars 'right)
    (set-scroll-bar-mode 'right)))

(add-hook 'scroll-bar-mode-hook #'prot-window-scroll-bar-placement)

(defun prot-window-no-minibuffer-scroll-bar (frame)
  "Remove the minibuffer scroll bars from FRAME."
  (set-window-scroll-bars (minibuffer-window frame) nil nil nil nil :persistent))

(add-hook 'after-make-frame-functions 'prot-window-no-minibuffer-scroll-bar)

;;;; Run commands in a popup frame (via emacsclient)

(defun prot-window-delete-popup-frame (&rest _)
  "Kill selected selected frame if it has parameter `prot-window-popup-frame'.
Use this function via a hook."
  (when (frame-parameter nil 'prot-window-popup-frame)
    (delete-frame)))

(defmacro prot-window-define-with-popup-frame (command)
  "Define function which calls COMMAND in a new frame.
Make the new frame have the `prot-window-popup-frame' parameter."
  `(defun ,(intern (format "prot-window-popup-%s" command)) ()
     ,(format "Run `%s' in a popup frame with `prot-window-popup-frame' parameter.
Also see `prot-window-delete-popup-frame'." command)
     (interactive)
     (let ((frame (make-frame '((prot-window-popup-frame . t)))))
       (select-frame frame)
       (switch-to-buffer " prot-window-hidden-buffer-for-popup-frame")
       (condition-case nil
           (call-interactively ',command)
         ((quit error user-error)
          (delete-frame frame))))))

(declare-function org-capture "org-capture" (&optional goto keys))
(defvar org-capture-after-finalize-hook)

;;;###autoload (autoload 'prot-window-popup-org-capture "prot-window")
(prot-window-define-with-popup-frame org-capture)

(declare-function tmr "tmr" (time &optional description acknowledgep))
(defvar tmr-timer-created-functions)

;;;###autoload (autoload 'prot-window-popup-tmr "prot-window")
(prot-window-define-with-popup-frame tmr)

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

7. Frequently Asked Questions (FAQ)

7.1. Why many modules instead of one init.el?

[ This question is in relation to the the anatomy of my Emacs configuration. ]

I prefer to keep things separate by splitting them into modules and custom libraries. Smaller files are easier to work with, including from the command line or with a generic text editor. They are also easier to share with others, which I do frequently.

A large file is fully dependent on the capabilities of Emacs. This is fine if you know your way around. But it does not work for people of different skill levels. Remember that I am sharing my configuration and keeping everything well documented to help others learn. The target audience is not Emacs veterans. I cannot expect an inexperienced user to already be familiar with how to navigate the outline, narrow to a defun, jump back to a mark, and so on, just to navigate one massive file.

Furthermore, I am not convinced by the argument that loading one large init.el is more efficient than loading many smaller modules. The reason is that the init file will anyway have multiple require calls or use autoloaded functions which themselves load all those other files. If, say, you are anyway loading a ~100 files through your init.el, do a few extra files really affect performance at startup? And if yes, is this enough to forgo the aforementioned usability and accessibility considerations?

Thoe granted, my current setup does not make me dependent on Org because I can at any moment stop editing this prot-emacs.org file and continue my work in the many small files I already have.

7.2. Why use Org when you can have an outline in Elisp?

[ Also read: Why many modules instead of one init.el? ]

This question is about the use of the built-in outline-minor-mode or the outshine package (The prot-emacs-langs.el settings for outline-minor-mode). They provide folding capabilities like those of Org mode and can, in principle, be enabled in any buffer (provided it has a recognisable outline).

I am, in fact, using outlines in all my Elisp files. Not just here, but also in all my public-facing Emacs packages. You can spot the entries to the outline as comments that have three or more delimiters. I can thus visit the .el file I am interested in and enable outline-minor-mode to get the folding capabilities and extras. Furthermore, I can use the consult-outline or prot-search-outline command to navigate to a heading with minibuffer completion.

What the prot-emacs.org provides is an optional single point of entry to my Emacs configuration. I use this to produce all the individual files (Anatomy of my Emacs configuration). It allows me to document my comprehensive corpus of work for the benefit of the Emacs community at-large:

  • I can establish links between sections and use richer typography, something that outline-minor-mode does not provide.
  • The extensive commentary included herein is not added to the source code, thus making it easier for me or others to focus on the programming parts when we want to.
  • This document is exported to my website as a standalone web page. People can find what they need there and share it with friends, regardless of their skill level.
  • Anyone may read the source of this file to learn more about the technicalities of how this is done. And they can still use only the .el files, if they do not want to deal with Org. Everybody is covered.

The prot-emacs.org does not introduce a dependency on Org mode. This file is not loaded at startup. I can remove prot-emacs.org at any moment and my setup will continue to work. This arrangement is so effective that I might actually convert the entirety of my dotfiles to it (long-term though, as it is a lot of work to document everything).

7.3. Why do you use multiple setq instead of one?

This question is about the following pattern:

;; I usually have this:
(setq var-1 val-1)
(setq var-2 val-2)
(setq var-3 val-3)

;; But why not this for everything?
(setq var-1 val-1
      var-2 val-2
      var-3 val-3)

I actually use both approaches. To me, the latter is better when I want to say “these go together”, while the former makes it easier for me to copy-paste what I need when communicating with someone.

Furthermore, the single setq call is harder to read when (i) there are lots of variables involved and/or (ii) the values are longer lists which themselves take some effort to figure out.

7.4. Why don’t you remap keys?

This is about the following pattern:

(define-key some-map [remap old-command] #'new-command)

This is a good way to replace in situ a command you don’t use for the one you want. The problem is that it is not didactic for new users. The person reading my code will not know which key binding I am referencing. Sure, they can type C-h w (where-is command) to search for the command in the current major mode, but this too requires some experience. Whereas an explicit key binding is self explanatory.

7.5. Why not use Org block arguments in the properties drawer?

The code blocks I define in this document have directives to tangle their contents to files. I do this for each code block, though it is possible to set the same settings inside the parent heading’s PROPERTIES drawer and achieve the same result.

The problem I have with that approach is that it is less discoverable in a massive file like this one. People are not expected to read this document from start to end. Instead, they will jump directly to the section that is of interest to them (perhaps by performing a search). They will thus skip past whatever parent heading declares the arguments. Whereas the code block encapsulates its own data and thus is self-documenting.

7.6. What hardware and software do you use?

I have a Lenovo ThinkPad T470. It is a second-hand laptop that I got from Ebay courtesy of a generous donation by Anush V. I run Debian on this laptop. I do not mind having older versions of system packages. The only program I need to run an up-to-date version of is Emacs—and I compile that from source.

During the summer of 2023, I used the laptop to experiment with the GNU Guix system but I eventually abandoned that project. Guix solves problems that I do not have, while it does things very differently to how other Unix-like systems are doing them. I cannot justify spending a considerable amount of time for something I do not really need, especially with the busy schedule I have.

I also use a desktop computer that I built in 2021 with donations I got from the Emacs community. At the time, I had a Lenovo ThinkPad X220 laptop as my sole computer: it broke and I posted an announcement to the effect that I would not be available for a while. Then donations started coming in and I was able to be back online after a month or so. It was a special moment.

The desktop computer is more powerful: it cost me a bit less than 600 EUR to build it. Back then I did not know enough about hardware and was able to assemble it with the help of a local who knows more about computer hardware than I do (I have learnt since then and I can do it myself now). I installed Arch when I first got this computer and it has been stable ever since (Arch is reliable if you know what you are doing).

In early 2024 I migrated my desktop to Debian. I did it because the Internet connection at my hut is metered (I moved to the hut at the end of summer 2023) and I did not want to spend a lot of data on system maintenance. Arch never caused me any issues. It is a top tier distro, as is Debian. Those are the two I trust the most.

On the laptop, I use the GNOME desktop environment. It is good for keyboard usage, plus it has excellent support for the trackpad. On the desktop, I normally use a tiling window manager. Wayland still does not work 100% for me, so I prefer to stay on Xorg until further notice.

For many years, the keyboard I had on my desktop was a generic, toy-grade, Qwerty model (I literally bought it from a toy store, together with the mouse for ~10 EUR). In autumn 2023, Arialdo Martini bought me the Keychron K5 Pro, which is a programmable keyboard with a traditional, full-key form factor. While in early spring 2024 “Andreas” got me the Iris keyboards from Keebio, which is a more ergonomic split design with a columnar stagger for how the keys are arranged. I discuss this at greater length here: https://protesilaos.com/news/2024-04-11-my-rsi-keyboard-ergonomics/. I also write about mechanical keyboards and ergonomics on this section of my website: https://protesilaos.com/keeb/.

Finally, I have a mobile phone that Dmitry Matveyev bought for me. It is a Samsung A53 model. I run the factory operating system on it. I have removed all apps that can be removed and use it only for its camera and phone capabilities. I know I could run Emacs on android (Emacs 30 has an Android build), but that is the sort of rabbit hole I need to avoid.

7.7. What is your desktop setup?

You can check my dotfiles to find all about the tiling window managers and other programs I use:

I have been using tiling window managers for most of my time on Linux (I switched to Linux in the summer of 2016 and I think I started with i3wm in 2017).