GNU Emacs integrated computing environment

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

  • Created: 2019-08-15
  • Updated: 2021-01-20 18:38 +0200.

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

Note that I am running GNU Emacs 28.0.50.

Table of Contents

1 Overview

1.1 Canonical links to this document

1.2 What is this

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

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

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

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

1.2.1 Contents of my init.el (for Emacs 27+)

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

(setq straight-use-package-by-default nil)

(defvar bootstrap-version)
(let ((bootstrap-file
       (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
      (bootstrap-version 5))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
      (goto-char (point-max))
      (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))

(straight-use-package 'use-package)

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

;; provides `straight-x-clean-unused-repos' (part of `straight.el')
(use-package straight-x)

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

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

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

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

;; Do not initialise installed packages (I use `straight.el')
(setq package-enable-at-startup nil)

;; Do not allow loading from the package cache (same reason).
(setq package-quickstart nil)

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

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

1.2.2 About use-package

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

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

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

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

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

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

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

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

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

(straight-use-package 'use-package)

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

1.2.3 About the source code version of this document

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

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

As for the various settings included herein, you can learn even more about them by using Emacs' built-in documentation facilities (also read my note on How do you learn Emacs?).

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

This metadata also makes it possible to create immutable internal links, whenever a reference is needed. To create such links, you can use C-c l to capture the unique ID of the current section and then C-c C-l to create a link.

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

1.3 COPYING

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

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

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

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

2 Base settings

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

2.1 Common auxiliary functions (prot-common.el)

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

(use-package prot-common
  :straight (:local-repo "prot-lisp")
  :demand)

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

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

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; Common functions for my Emacs: <https://protesilaos.com/dotemacs/>.

;;; Code:

(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-negative (n)
  "Make N negative."
  (if (numberp n)
      (string-to-number (format "-%d" n)) ; TODO: better way?
    (error "%s is not a number" n)))

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

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

(declare-function auth-source-search "auth-source")

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

(declare-function org-babel-tangle-file "ob-tangle")

;; TODO defcustom for the emacs-init file
;;;###autoload
(defun prot-common-rebuild-emacs-init ()
  "Produce Elisp init from my Org dotemacs.
Add this to `kill-emacs-hook', to use the newest file in the next
session.  The idea is to reduce startup time, though just by
rolling it over to the end of a session rather than the beginning
of it."
  (let ((init-el (concat user-emacs-directory "emacs-init.el"))
        (init-org (concat user-emacs-directory "emacs-init.org")))
    (when (file-exists-p init-el)
      (delete-file init-el))
    (org-babel-tangle-file init-org init-el)))

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

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

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

(prot-common--line-p
 prot-common-non-empty-line-p
 "^.+$")

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

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

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

2.2 Common custom functions (prot-simple.el)

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

Given that this is a foundational piece of my Emacs setup, its package declaration serves as the appropriate place to re-bind or free up some common key combinations.

(use-package prot-simple
  :straight (:type built-in)
  :demand
  :config
  (setq prot-simple-insert-pair-alist
	    '(("' Single quote" . (39 39))           ; ' '
	      ("\" Double quotes" . (34 34))         ; " "
	      ("` Elisp quote" . (96 39))            ; ` '
	      ("‘ Single apostrophe" . (8216 8217))  ; ‘ ’
	      ("“ Double apostrophes" . (8220 8221)) ; “ ”
	      ("( Parentheses" . (40 41))            ; ( )
	      ("{ Curly brackets" . (123 125))       ; { }
	      ("[ Square brackets" . (91 93))        ; [ ]
	      ("< Angled brackets" . (60 62))        ; < >
	      ("« Εισαγωγικά Gr quote" . (171 187))  ; « »
	      ("= Equals signs" . (61 61))           ; = =
	      ("* Asterisks" . (42 42))              ; * *
	      ("_ underscores" . (95 95))))          ; _ _
  :bind (;; General commands
         ("<insert>" . nil)
	     ("C-z" . nil)
	     ("C-x C-z" . nil)
	     ("C-h h" . nil)
	     ("M-`" . nil)
	     ("s-h" . prot-simple-describe-symbol)
	     ("s-H" . (lambda ()
		            (interactive)
		            (prot-simple-describe-symbol '(4))))
         ("C-h K" . describe-keymap) ; overrides `Info-goto-emacs-key-command-node'
	     ;; Commands for lines
	     ("C-S-w" . prot-simple-copy-line-or-region)
	     ("C-S-y" . prot-simple-yank-replace-line-or-region)
	     ("M-SPC" . cycle-spacing)
	     ("M-o" . delete-blank-lines)   ; alias for C-x C-o
	     ("M-k" . prot-simple-kill-line-backward)
	     ("C-S-n" . prot-simple-multi-line-next)
	     ("C-S-p" . prot-simple-multi-line-prev)
	     ("<C-return>" . prot-simple-new-line-below)
	     ("<C-S-return>" . prot-simple-new-line-above)
	     ;; Commands for text insertion or manipulation
	     ("C-'" . prot-simple-insert-pair-completion)
	     ("M-'" . prot-simple-insert-pair-completion)
	     ("<C-M-backspace>" . backward-kill-sexp)
	     ("M-c" . capitalize-dwim)
	     ("M-l" . downcase-dwim)        ; "lower" case
	     ("M-u" . upcase-dwim)
	     ;; Commands for object transposition
	     ("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 marking objects
	     ("M-@" . prot-simple-mark-word)       ; replaces `mark-word'
	     ("C-M-SPC" . prot-simple-mark-construct-dwim)
	     ("C-M-d" . prot-simple-downward-list)
         ;; Commands for paragraphs
         ("M-Q" . prot-simple-unfill-region-or-paragraph)
         ;; Commands for windows
         ("s-m" . prot-simple-monocle)
         ;; Commands for buffers
         ("M-=" . count-words)
         ("<C-f2>" . prot-simple-rename-file-and-buffer)
         ("s-k" . prot-simple-kill-buffer-current))
  :hook (kill-emacs-hook . prot-common-rebuild-emacs-init))

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

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

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; Common commands for my Emacs: <https://protesilaos.com/dotemacs/>.

;;; Code:

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

;;; Commands

;;;; General commands

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

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

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

;;;; Comands for lines

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

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

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

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

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

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

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

;;;; Commands for text insertion or manipulation

;; Got those numbers from `string-to-char'
(defcustom prot-simple-insert-pair-alist
  '(("' Single quote" . (39 39))           ; ' '
    ("\" Double quotes" . (34 34))         ; " "
    ("` Elisp quote" . (96 39))            ; ` '
    ("‘ Single apostrophe" . (8216 8217))  ; ‘ ’
    ("“ Double apostrophes" . (8220 8221)) ; “ ”
    ("( Parentheses" . (40 41))            ; ( )
    ("{ Curly brackets" . (123 125))       ; { }
    ("[ Square brackets" . (91 93))        ; [ ]
    ("< Angled brackets" . (60 62))        ; < >
    ("« Εισαγωγικά Gr quote" . (171 187))  ; « »
    ("= Equals signs" . (61 61))           ; = =
    ("* Asterisks" . (42 42))              ; * *
    ("_ underscores" . (95 95)))           ; _ _
  "Alist of pairs for use with `prot-simple-insert-pair-completion'."
  :type 'alist
  :group 'prot-simple)

;;;###autoload
(defun prot-simple-insert-pair-completion (&optional num)
  "Insert pair from `prot-simple-insert-pair-alist'.
With optional NUM numeric argument, insert pair to NUMth
constructs.  A negative number counts backwards."
  (interactive "p")
  (let* ((data prot-simple-insert-pair-alist)
         (chars (mapcar #'car data))
         (choice (completing-read "Select character: " chars nil t))
         (left (cadr (assoc choice data)))
         (right (caddr (assoc choice data)))
         (n (or num 1)))
    (insert-pair n left right)))

;;;; Commands for object transposition

(defmacro prot-simple-transpose (name scope &optional doc)
  "Macro to produce transposition functions.
NAME is the function's symbol.  SCOPE is the text object to
operate on.  Optional DOC is the function's docstring.

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

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

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

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

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

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

;;;###autoload
(defun prot-simple-transpose-words (arg)
  "Transpose ARG words.

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

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

;;;; Commands for marking syntactic constructs

(defmacro prot-simple-mark (name object &optional docstring)
  "Produce function for marking small syntactic constructs.
NAME is how the function should be called.  OBJECT is its scope.
Optional DOCSTRING describes the resulting function.

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

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

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

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

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

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

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

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

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

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

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

;;;; Commands for paragraphs

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

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

;;;###autoload
(defun prot-simple-unfill-region-or-paragraph (&optional beg end)
  "Unfill paragraph or, when active, the region.
Join all lines in region delimited by BEG and END, if active,
while respecting any empty lines (so multiple paragraphs are not
joined, just unfilled).  If no region is active, operate on the
paragraph.  The idea is to produce the opposite effect of both
`fill-paragraph' and `fill-region'."
  (interactive "r")
  (let ((fill-column most-positive-fixnum))
    (if (use-region-p)
        (fill-region beg end)
      (fill-paragraph))))

;;;; Commands for windows

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

;;;###autoload
(define-minor-mode prot-simple-monocle
  "Toggle between multiple windows and single window.
This is the equivalent of maximising a window.  Tiling window
managers such as DWM, BSPWM refer to this state as 'monocle'."
  :lighter " -M-"
  :global nil
  (let ((win prot-simple--windows-current))
    (if (one-window-p)
        (when win
          (set-window-configuration win))
      (setq prot-simple--windows-current (current-window-configuration))
      (delete-other-windows))))

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

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

;;;; Commands for buffers

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

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

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

2.3 Remove modeline "lighters"

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

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

(use-package diminish
  :straight t
  :after use-package)

2.4 Put customisation settings in a "custom.el"

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

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

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

  (setq custom-file prot/custom-file)

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

2.5 Modus themes (my highly accessible themes)

This is a project I started as soon as I switched to Emacs in July 2019. About a year later the themes became part of upstream Emacs, available for Emacs version 28 (as of version 0.12.0 of the themes). I have benefited a lot from community contributions, of which I am most thankful of, as discussed in My Modus themes are now shipped with Emacs (2020-08-27).

The Modus themes are designed for accessible readability. They conform with the highest standard for colour contrast between foreground and background values. This stands for a minimum contrast ratio of 7:1, also known as the WCAG AAA standard (the highest of its kind).

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

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

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

Lastly, if you are curious about the underlying methodology, read my essay on the design of the Modus themes (2020-03-17). And here are some more resources from my website for those who are really into the minutia and wish to get a glimpse of how much work goes into this project:

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

(use-package modus-themes
  :straight (:local-repo "modus-themes" ; Just use :straight t OR :ensure t
             :no-byte-compile t)        ; Omit this---only for my dev needs
  :init
  ;; Add all your customizations prior to loading the themes
  ;;
  ;; NOTE: these are not my preferences!  I am always testing various
  ;; configurations.  Though I still like what I have here.
  (setq modus-themes-slanted-constructs t
        modus-themes-bold-constructs t
        modus-themes-fringes 'subtle ; {nil,'subtle,'intense}
        ;; Options for `modus-themes-lang-checkers': nil,
        ;; 'straight-underline, 'subtle-foreground,
        ;; 'subtle-foreground-straight-underline, 'intense-foreground,
        ;; 'intense-foreground-straight-underline, 'colored-background
        modus-themes-lang-checkers nil
        modus-themes-mode-line nil ; {nil,'3d,'moody}
        ;; Options for `modus-themes-syntax': nil, 'faint,
        ;; 'yellow-comments, 'green-strings,
        ;; 'yellow-comments-green-strings, 'alt-syntax,
        ;; 'alt-syntax-yellow-comments
        modus-themes-syntax nil
        modus-themes-intense-hl-line nil
        modus-themes-paren-match 'subtle-bold ; {nil,'subtle-bold,'intense,'intense-bold}
        ;; Options for `modus-themes-links': nil, 'faint,
        ;; 'neutral-underline, 'faint-neutral-underline, 'no-underline,
        ;; 'underline-only
        modus-themes-links 'underline-only
        modus-themes-no-mixed-fonts nil
        modus-themes-prompts 'subtle ; {nil,'subtle,'intense}
        modus-themes-completions 'moderate ; {nil,'moderate,'opinionated}
        modus-themes-region 'bg-only-no-extend ; {nil,'no-extend,'bg-only,'bg-only-no-extend}
        modus-themes-diffs 'fg-only ; {nil,'desaturated,'fg-only,'bg-only}
        modus-themes-org-blocks 'grayscale ; {nil,'grayscale,'rainbow}
        modus-themes-org-habit nil ; {nil,'simplified,'traffic-light}
        modus-themes-headings ; Read the manual for this one
        '((t . no-color-no-bold))
        modus-themes-variable-pitch-ui nil
        modus-themes-variable-pitch-headings nil
        modus-themes-scale-headings t
        modus-themes-scale-1 1.1
        modus-themes-scale-2 1.15
        modus-themes-scale-3 1.21
        modus-themes-scale-4 1.27
        modus-themes-scale-5 1.33)
  ;; Load the theme files before enabling a theme (I do this via the
  ;; `after-init-hook', though you could also add `(enable-theme
  ;; 'modus-operandi)' after the `:config' keyword.
  (modus-themes-load-themes)
  :config
  ;; Don't forget to read the manual.  This is explained there.  And
  ;; please don't read too much into those: I am testing various
  ;; combinations and scenaria.
  (defun prot/modus-themes-custom-faces ()
    "Tweak faces after `modus-themes-after-load-theme-hook'."
    (modus-themes-with-colors
      (custom-set-faces
       `(dired-flagged ((,class :strike-through t)))
       `(fill-column-indicator ((,class :background ,bg-inactive :foreground ,bg-inactive)))
       `(gnus-summary-cancelled ((,class :strike-through t)))
       `(line-number ((,class :background unspecified :foreground ,fg-unfocused)))
       `(line-number-current-line
         ((,class :background ,bg-special-cold :foreground ,fg-special-cold))))))

  ;; Have I told you about the manual?  This is just meant to showcase
  ;; the option of overriding individual colours from each theme's
  ;; palette.
  (define-minor-mode prot/modus-themes-tinted
    "Tweak key Modus themes colors."
    :init-value nil
    :global t
    (if prot/modus-themes-tinted
        (setq modus-themes-operandi-color-overrides
              '((bg-main . "#fefcf4")
                (bg-dim . "#faf6ef")
                (bg-alt . "#f7efe5")
                (bg-hl-line . "#f4f0e3")
                (bg-active . "#e8dfd1")
                (bg-inactive . "#f6ece5")
                (bg-region . "#c6bab1")
                (bg-header . "#ede3e0")
                (bg-tab-bar . "#dcd3d3")
                (bg-tab-active . "#fdf6eb")
                (bg-tab-inactive . "#c8bab8")
                (fg-unfocused . "#55556f"))
              modus-themes-vivendi-color-overrides
              '((bg-main . "#100b17")
                (bg-dim . "#161129")
                (bg-alt . "#181732")
                (bg-hl-line . "#191628")
                (bg-active . "#282e46")
                (bg-inactive . "#1a1e39")
                (bg-region . "#393a53")
                (bg-header . "#202037")
                (bg-tab-bar . "#262b41")
                (bg-tab-active . "#120f18")
                (bg-tab-inactive . "#3a3a5a")
                (fg-unfocused . "#9a9aab")))
      (setq modus-themes-operandi-color-overrides nil
            modus-themes-vivendi-color-overrides nil)))

  ;; Toggle the minor mode and switch between the themes to see the
  ;; effect.
  (prot/modus-themes-tinted -1)

  ;; Also check my package declaration for `prot-fonts' because I use
  ;; the `modus-themes-after-load-theme-hook' for some typeface-related
  ;; tweaks (as those are made at the "face" level).
  :hook ((after-init-hook . modus-themes-load-operandi)
         (modus-themes-after-load-theme-hook . prot/modus-themes-custom-faces))
  :bind ("<f5>" . modus-themes-toggle))

2.6 Typeface configurations

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

While there are many good free/libre options available, only a handful of them cover my fairly demanding needs. Some look good at large point sizes. Others lack Greek characters. While a few of them are virtually unreadable when cast on a light background (bitmap fonts in particular). The section on Font configurations (prot-fonts.el) defines typefaces that I consider suitable for my needs.

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

2.6.1 Font configurations (prot-fonts.el)

Moving on to my configurations, prot-fonts.el is a library I have written which contains lots of extras pertaining to my typeface configurations and preferences.

Some highlights:

  • prot-fonts-set-fonts is a command that lets me select with completion a predetermined set of font configurations depending on the display context (more on completion in Completion framework and extras). Such a "context" is configurable: I define them as "laptop", "desktop", etc. When the function is executed non-interactively, it can be given an arbitrary font size as well as family names for the {mono,proportionately}-spaced typefaces.
  • prot-fonts-fonts-per-monitor sets the appropriate font family and size depending on whether I am only on my laptop or have connected to it an external monitor. In the latter case we use the "desktop" context.
  • prot-fonts-bold-face lets me associate a list of typefaces with desired weights for their "bold" variation. This practically means that if my font family has lots of weights, such as "light", "extrabold", "semibold", I can control what constitutes the normal one and what should be used for heavy emphasis. Note that this only works if your theme of choice use the bold face to assign such emphasis instead of hard-wiring the :weight bold property. My Modus themes are designed to account for such a requirement (the default remains the bold weight property—no need to specify that).

Now a few notes about setting fonts in Emacs.

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

To appreciate this point, consider that in Emacs parlance a "face" signifies a construct that groups together several display attributes, such as a foreground and a background colour, as well as all typography-related values. Multiple assignments of value can expand the face's specifications, unless one explicitly overrules a given property.

With regard to fonts, there are three faces that are of immediate interest: the default, variable-pitch, and fixed-pitch. The first is the session's main typeface, the second specifies a proportionately spaced font, and the third does the same for a monospaced family.

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

Relevant blog posts of mine:

(use-package prot-fonts
  :straight (:type built-in)
  :after modus-themes                   ; Because I use its hook
  :init
  ;; Note that the light weight I pass to Iosevka Comfy is thicker than
  ;; the equivalent for standard Iosevka.  In my build instructions, I
  ;; set that to 350, while normal light is at 300 and regular is at
  ;; 400.  Source: <https://gitlab.com/protesilaos/iosevka-comfy>.
  (setq prot-fonts-typeface-sets-alist
        '((laptop 90 "Hack" normal "DejaVu Sans Condensed" normal)
          (desktop 130 "Iosevka Comfy" light "Roboto Condensed" normal)
          (reader 150 "Iosevka Comfy" light "FiraGO" normal)
          (presentation 180 "Iosevka Comfy" light "Source Sans Pro" normal)))
  (setq prot-fonts-monospaced-list
        '("Hack" "DejaVu Sans Mono" "Iosevka Comfy" "Source Code Pro"
          "Ubuntu Mono" "Fantasque Sans Mono" "Fira Code" "Monoid"))
  (setq prot-fonts-heights-list
        '(100 105 110 120 130 140 150 160 170 180 190))
  (setq prot-fonts-line-spacing-alist
        '(("Ubuntu Mono" . 2)))
  (setq prot-fonts-laptop-desktop-keys-list '(laptop desktop))
  (setq prot-fonts-max-small-resolution-width 1366)
  (setq prot-fonts-bold-weight-alist
        '(("Iosevka Comfy" . semibold)
          ("Fira Code" . semibold)
          ("Source Code Pro" . semibold)))
  :config
  ;; This is defined in Emacs' C code, though I feel this is a good
  ;; place to put it.
  (setq x-underline-at-descent-line t)
  ;; And this just sets the right font depending on whether my laptop is
  ;; connected to an external monitor or not.
  (prot-fonts-fonts-per-monitor)
  :hook ((prot-fonts-set-typeface-hook . prot-fonts-line-spacing)
         (prot-fonts-set-typeface-hook . prot-fonts-bold-face)
         ;; See theme section for this hook
         (modus-themes-after-load-theme-hook . prot-fonts-bold-face))
  :bind ("C-c f" . prot-fonts-set-fonts-dwim))

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

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

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; This set of configurations pertains to my font settings, for use in
;; my Emacs setup: https://protesilaos.com/dotemacs.
;;
;; Note that this package "requires" Emacs 24.3 or higher, though I only
;; tested it with versions 27 and 28.

;;; Code:

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

;; NOTE: "Hack" and "Iosevka Comfy" are personal builds of Hack and
;; Iosevka respectively:
;;
;; 1. https://gitlab.com/protesilaos/hack-font-mod
;; 2. https://gitlab.com/protesilaos/iosevka-comfy
(defcustom prot-fonts-typeface-sets-alist
  '((laptop 90 "Hack" normal "DejaVu Sans Condensed" normal)
    (desktop 130 "Iosevka Comfy" light "Roboto Condensed" normal)
    (reader 150 "Iosevka Comfy" light "FiraGO" normal)
    (presentation 180 "Iosevka Comfy" light "Source Sans Pro" normal))
  "Alist of desired typefaces and their particularities.

The list specifies, in this order:

0. Display type of context, used to recognise the association.

1. Font height as an integer that is 10x the point size.

2. The family name (as a string) of the monospaced typeface that
will be assigned to the `default' and `fixed-pitch' faces.

3. The main weight of the monospaced family.

4. The family name of the proportionately spaced typeface that
will be assigned to the `variable-pitch' face.

5. The weight of the proportionately spaced family.

It is assumed that all those typefaces already exist on the
system and we make no effort whatsoever to run relevant tests."
  :group 'prot-fonts
  :type 'alist)

(defcustom prot-fonts-monospaced-list
  '("Hack" "DejaVu Sans Mono" "Iosevka Comfy" "Source Code Pro"
    "Ubuntu Mono" "Fantasque Sans Mono" "Fira Code" "Monoid")
  "List of typefaces for coding.

It is assumed that those already exist on the system, otherwise
an error will be displayed when trying to set one of them."
  :group 'prot-fonts
  :type 'list)

(defcustom prot-fonts-heights-list
  '(100 105 110 120 130 140 150 160 170 180 190)
  "List of font heights for `prot-fonts-set-font-size-family'."
  :group 'prot-fonts
  :type 'list)

(defcustom prot-fonts-line-spacing-alist
  '(("Source Code Pro" . 1)
    ("Ubuntu Mono" . 2))
  "Font families in need of extra line spacing.

The alist defines a font family as a string and the desired
integer to pass to the `line-spacing' variable."
  :group 'prot-fonts
  :type 'alist)

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

(defcustom prot-fonts-max-small-resolution-width 1366
  "Maximum width for use in `prot-fonts-fonts-per-monitor'."
  :group 'prot-fonts
  :type 'integer)

(defcustom prot-fonts-bold-weight-alist
  '(("Iosevka Comfy" . semibold)
    ("Fira Code" . semibold)
    ("Source Code Pro" . semibold))
  "Font families in need of a different weight for `bold'.

The alist defines a font family as a string and the desired style
to pass to the `bold' face's weight property."
  :group 'prot-fonts
  :type 'alist)

;;; Variables

(defvar prot-fonts-set-typeface-hook nil
  "Hook that is called after setting fonts.")

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

(defvar prot-fonts-font-family-hist '()
  "History of inputs for font families.")

(defvar prot-fonts-font-height-hist '()
  "History of inputs for font heights.")

;;; Functions

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

(defun prot-fonts--return-nth (choice displays data n)
  "Check if CHOICE maps to DISPLAYS from DATA; return N."
  (if (member choice displays)
      (nth n (assoc choice data))
    (error "'%s' not a member of %s" choice displays)))

;;;###autoload
(defun prot-fonts-set-fonts (&optional height font-mono font-var weight-mono weight-var)
  "Set default font size using presets.

HEIGHT is the font's height as 10x its point size.  FONT-MONO
should be a monospaced typeface, due to the alignment
requirements of the `fixed-pitch' face.  FONT-VAR could be a
proportionately spaced typeface or even a monospaced one, since
the `variable-pitch' it applies to is not supposed to be
spacing-sensitive.  Both families must be represented as a string
holding the family's name.

WEIGHT-MONO is the weight property of FONT-MONO, while WEIGHT-VAR
is that of FONT-VAR."
  (interactive)
  (if window-system
      (let* ((data prot-fonts-typeface-sets-alist)
             (displays (mapcar #'car prot-fonts-typeface-sets-alist))
             (display-strings (mapcar (lambda (x)
                                        (format "%s" (car x)))
                                      prot-fonts-typeface-sets-alist))
             (prompt (unless height
                       (completing-read "Pick display size: "
                                        display-strings nil t
                                        nil 'prot-fonts-font-display-hist)))
             (choice (or height (intern prompt)))
             (size (or height (prot-fonts--return-nth choice displays data 1)))
             (mono (or font-mono (prot-fonts--return-nth choice displays data 2)))
             (weight-m (or weight-mono (prot-fonts--return-nth choice displays data 3)))
             (var (or font-var (prot-fonts--return-nth choice displays data 4)))
             (weight-v (or weight-var (prot-fonts--return-nth choice displays data 5))))
        (prot-fonts--set-face-attribute 'default mono weight-m size)
        (prot-fonts--set-face-attribute 'fixed-pitch mono weight-m)
        (prot-fonts--set-face-attribute 'variable-pitch var weight-v)
        (run-hooks 'prot-fonts-set-typeface-hook)
        (add-to-history 'prot-fonts-font-display-hist prompt))
    (error "Not running a graphical Emacs; cannot set fonts")))

;;;###autoload
(defun prot-fonts-set-font-size-family ()
  "Set point size and main typeface.
This command is mostly intended for testing typefaces defined in
`prot-fonts-monospaced-list' at common heights specified in
`prot-fonts-heights-list'."
  (interactive)
  (if window-system
      (let* ((fonts prot-fonts-monospaced-list)
             (font (completing-read "Select main font: " fonts nil nil
                                    nil 'prot-fonts-font-family-hist))
             (nums prot-fonts-heights-list)
             (sizes (mapcar 'number-to-string nums))
             (size (completing-read "Select or insert number: " sizes nil nil
                                    nil 'prot-fonts-font-height-hist))
             (var (face-attribute 'variable-pitch :family)))
        (prot-fonts--set-face-attribute 'default font 'normal (string-to-number size))
        (prot-fonts--set-face-attribute 'fixed-pitch font)
        (prot-fonts--set-face-attribute 'variable-pitch var)
        (run-hooks 'prot-fonts-set-typeface-hook)
        (add-to-history 'prot-fonts-font-family-hist font)
        (add-to-history 'prot-fonts-font-height-hist size))
    (error "Not running a graphical Emacs; cannot set fonts")))

;;;###autoload
(defun prot-fonts-set-fonts-dwim (&optional arg)
  "Set fonts interactively.
With optional prefix ARG (\\[universal-argument]) call
`prot-fonts-set-font-size-family' else default to
`prot-fonts-set-fonts'.

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

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

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

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

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

;;;###autoload
(defun prot-fonts-fonts-per-monitor ()
  "Use font settings based on screen size."
  (when window-system
    (let* ((display (prot-fonts--display-type-for-monitor))
           (data prot-fonts-typeface-sets-alist)
           (size (cadr (assoc `,display data)))
           (mono (nth 2 (assoc `,display data)))
           (weight-m (nth 3 (assoc `,display data)))
           (var (nth 4 (assoc `,display data)))
           (weight-v (nth 5 (assoc `,display data))))
      (prot-fonts--set-face-attribute 'default mono weight-m size)
      (prot-fonts--set-face-attribute 'fixed-pitch mono weight-m)
      (prot-fonts--set-face-attribute 'variable-pitch var weight-v)
    (run-hooks 'prot-fonts-set-typeface-hook))))

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

2.6.2 Simple font suitability test

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

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

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

Sample character set
Check for monospacing and Greek glyphs

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

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

2.6.3 Bidirectional writing and so-long.el

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

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

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

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

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

2.7 Key chord hints (which-key.el)

This library provides hints on the possible tails of a key chord. So if you type C-x r and wait for which-key-idle-delay, a pop-up window will appear showing you the keys you can use and the actions bound to them.

I do not use which-key as I have already memorised all the key sequences I type. Plus, I prefer the default way of following up a key chord with C-h, which produces a help buffer with the relevant key bindings (a regular buffer can be renamed, written to a file, and generally acted upon, whereas some auto-disappearing pop-up does not lend itself to such workflows).

The only reason I keep this package here is to make things easier when I record videos of my setup in which I invoke some Embark action or employ Consult's narrowing facility. Please refer to the relevant sections:

(use-package which-key
  :straight t
  :diminish
  :config
  ;; NOTE: I only use this for `embark' and `consult' and for the sake
  ;; of producing more user-friendly video demonstrations.
  (setq which-key-dont-use-unicode t)
  (setq which-key-add-column-padding 2)
  (setq which-key-show-early-on-C-h nil)
  (setq which-key-idle-delay most-positive-fixnum) ; set this to something like 0.8
  (setq which-key-idle-secondary-delay 0.05)
  (setq which-key-popup-type 'side-window)
  (setq which-key-show-prefix 'echo)
  (setq which-key-max-display-columns 3)
  (setq which-key-separator "  ")
  (setq which-key-special-keys nil)
  (setq which-key-paging-key "<next>")
  (which-key-mode -1))       ; and turn this on, if you want to use this

3 Selection candidates and search methods

3.1 Completion framework and extras

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

To get a sense of my current completion framework, watch my presentation on Default Emacs completion and extras (2021-01-06).

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

The, dare I say, sublime “orderless” package is developed by Omar Antolín Camarena. It provides the orderless completion style for efficient, out-of-order grouped pattern matching. The components can be determined using several styles, such as regexp, flex, prefix, initialism (check its README because there are lots of variations). Delimiters are literal spaces by default, but can be configured to match other characters, with hyphens and slashes being likely choices. As such, Orderless can supersede—and for most part improve upon—the completion styles that come built into Emacs, adding to them the powerful out-of-order capability.

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

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

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

(use-package orderless
  :straight t
  :demand
  :after prot-orderless
  :config
  (setq orderless-component-separator " +")
  (setq orderless-matching-styles prot-orderless-default-styles)
  (setq orderless-style-dispatchers
        '(prot-orderless-literal-dispatcher
          prot-orderless-initialism-dispatcher))
  ;; SPC should never complete: use it for `orderless' groups.
  :bind (:map minibuffer-local-completion-map
              ("SPC" . nil)
              ("?" . nil)))

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

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

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; Extensions for the Orderless completion style for use in my Emacs
;; setup: <https://protesilaos.com/dotemacs/>.

;;; Code:

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

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

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

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

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

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

(defvar orderless-matching-styles)

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

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

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

3.1.2 Completion annotations (marginalia)

This is a utility jointly developed by Daniel Mendler and Omar Antolín Camarena that provides annotations to completion candidates. It is meant to be framework-agnostic, so it works with Selectrum, Icomplete vertical, and Embark (since 2020-12-20, the latter has become my choice for visualising the standard completion framework's output—see Extended minibuffer actions and more (embark.el and prot-embark.el)).

(use-package marginalia
  :straight (:host github :repo "minad/marginalia" :branch "main")
  :demand
  :config
  (setq marginalia-annotators
        '(marginalia-annotators-heavy
          marginalia-annotators-light))
  (marginalia-mode 1))

3.1.3 Minibuffer configurations and extras (prot-minibuffer.el)

The code block below is specifically about the minibuffer setup. Emacs has built-in capabilities to narrow a list of candidates using various pattern-matching styles. Note that the task of "narrowing" does not encompass the visualisation of completion candidates. That is handled by some other tool which could be icomplete-mode or, in my current setup, Embark (as of 2020-12-20). For what "visualisation" entails, see Extended minibuffer actions and more (embark.el and prot-embark.el).

Here is an overview of the settings covered herein:

Completion styles
I mostly rely on the Orderless completion style. An exception is a niche functionality of the partial-completion style (built-in): with it you can navigate to a filesystem path like ~/.l/s/fo for ~/.local/share/fonts. So my recommendation is to use those two styles to cover every case.
Recursive minibuffers

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

The minibuffer-depth-indicate-mode will show a depth indicator, represented as a number, next to the minibuffer prompt, if a recursive edit is in progress (also check Mode line recursion indicators).

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

Also check my setup for the Minibuffer history (savehist-mode). After several months of full time usage, I am confident in the built-in mechanism's ability to sort things well enough and to surface the results I am most likely interested in, based on previous selections.

Finally note that prot-minibuffer.el contains a few extensions that help me focus the minibuffer or the completions' window. It also provides three de facto deprecated commands that are pertinent to the *Completions* buffer: kill-save the symbol at point, insert it at point in the most recently used window, insert and then exit all recirsive minibuffers. Those are not part of my day-to-day workflow, because I normally rely on Embark for extended minibuffer actions. At any rate, the prot-minibuffer.el is reproduced after this set of use-package declarations. For its :hook also check Cursor appearance and tweaks.

(use-package prot-minibuffer
  :straight (:type built-in)
  :demand
  :bind (("s-v" . prot-minibuffer-focus-mini-or-completions)
         :map completion-list-mode-map
         ("M-v" . prot-minibuffer-focus-mini)
         ("h" . prot-simple-describe-symbol) ; from `prot-simple.el'
         ;; Those are DE FACTO DEPRECATED generic actions for the
         ;; "*Completions*" buffer.  I normally use `embark' and its own
         ;; buffers.
         ("w" . prot-minibuffer-completions-kill-symbol-at-point)
         ("i" . prot-minibuffer-completions-insert-symbol-at-point)
         ("j" . prot-minibuffer-completions-insert-symbol-at-point-exit))
  :hook (minibuffer-setup-hook . prot-minibuffer-mini-cursor))

(use-package minibuffer
  :demand
  :after prot-minibuffer
  :config
  (setq completion-styles '(partial-completion substring flex orderless))
  (setq completion-category-defaults nil)
  (setq completion-cycle-threshold 3)
  (setq completion-flex-nospace nil)
  (setq completion-pcm-complete-word-inserts-delimiters t)
  (setq completion-pcm-word-delimiters "-_./:| ")
  (setq completion-show-help nil)
  (setq completion-auto-help nil)
  (setq completion-ignore-case t)
  (setq-default case-fold-search t)   ; For general regexp

  ;; The following two are updated in Emacs 28.  They concern the
  ;; *Completions* buffer.  Note that I actually do not use that buffer,
  ;; because I rely on Embark's version of it.
  (setq completions-format 'one-column)
  (setq completions-detailed t)

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

  (setq enable-recursive-minibuffers t)
  (setq read-answer-short t)
  (setq resize-mini-windows t)
  (setq minibuffer-eldef-shorten-default t)

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

  ;; Defines, among others, aliases for common minibuffer commands to
  ;; Super-KEY.  Normally these should go in individual package
  ;; declarations, but their grouping here makes things easier to
  ;; understand.  Besides, they are related to the minibuffer.
  :bind (("s-b" . switch-to-buffer)
         ("s-B" . switch-to-buffer-other-window)
         ("s-f" . find-file)
         ("s-F" . find-file-other-window)
         ("s-d" . dired)
         ("s-D" . dired-other-window)
         :map minibuffer-local-completion-map
         ("C-j" . exit-minibuffer)
         ("<tab>" . minibuffer-force-complete)
         ;; De facto deprecated as I use Embark and its own completions'
         ;; buffer.
         :map completion-list-mode-map
         ("n" . next-line)
         ("p" . previous-line)
         ("f" . next-completion)
         ("b" . previous-completion)))

And here is prot-minibuffer.el (from my dotfiles' repo):

;;; prot-minibuffer.el --- Extensions for the minibuffer -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; Extensions for the minibuffer, intended for my Emacs setup:
;; <https://protesilaos.com/dotemacs/>.

;;; Code:

;;;; General utilities

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

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

;;;; Minibuffer behaviour

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

(dolist (fn '(minibuffer-force-complete-and-exit
              minibuffer-complete-and-exit
              exit-minibuffer))
  (advice-add fn :around #'prot-minibuffer--messageless))

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

(advice-add 'minibuf-eldef-setup-minibuffer :around #'prot-minibuffer--stealthily)

;;;; Cursor appearance

(defun prot-minibuffer--cursor-type ()
  "Determine whether `cursor-type' is a list and return value.
If it is a list, this actually returns its car."
  (if (listp cursor-type)
      (car cursor-type)
    cursor-type))

;;;###autoload
(defun prot-minibuffer-mini-cursor ()
  "Local value of `cursor-type' for `minibuffer-setup-hook'."
  (pcase (prot-minibuffer--cursor-type)
    ('hbar (setq-local cursor-type '(hbar . 8)))
    ('bar (setq-local cursor-type '(hbar . 3)))
    (_  (setq-local cursor-type '(bar . 2)))))

;;;###autoload
(defun prot-minibuffer-completions-cursor ()
  "Local value of `cursor-type' for `completion-setup-hook'."
  (pcase (prot-minibuffer--cursor-type)
    ('hbar (setq-local cursor-type 'box))
    ('bar (setq-local cursor-type '(hbar . 8)))
    (_  (setq-local cursor-type '(bar . 3)))))

;;;; Minibuffer interactions

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

(defun prot-minibuffer--get-completion-window ()
  "Find a live window showing completion candidates."
  (get-window-with-predicate
   (lambda (window)
     (string-match-p
      prot-minibuffer-completion-windows-regexp
      (format "%s" window)))))

(defun prot-minibuffer-focus-mini-or-completions ()
  "Focus the active minibuffer or the completions' window.

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

The continuous switch is essentially the same as running
`prot-minibuffer-focus-minibuffer' and `switch-to-completions' in
succession.

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

;;;; M-X utility (M-x limited to buffer's major and minor modes)

;; UPDATE 2020-12-23: A better version of this is now part of Consult.
;; I am using that one instead, but keeping the code here:
;; <https://github.com/minad/consult>.

;; Adapted from the smex.el library of Cornelius Mika:
;; <https://github.com/nonsequitur/smex>.

(defun prot-minibuffer--extract-commands (mode)
  "Extract commands from MODE."
  (let ((commands)
        (library-path (symbol-file mode))
        (mode-name (substring (symbol-name major-mode) 0 -5)))
    (dolist (feature load-history)
      (let ((feature-path (car feature)))
        (when (and feature-path
                   (or (equal feature-path library-path)
                       (string-match mode-name (file-name-nondirectory
                                                feature-path))))
          (dolist (item (cdr feature))
            (when (and (listp item) (eq 'defun (car item)))
              (let ((function (cdr item)))
                (when (commandp function)
                  (setq commands (append commands (list function))))))))))
    commands))

(autoload 'prot-common-minor-modes-active "prot-common")

(defun prot-minibuffer--extract-commands-minor ()
  "Extract commands from active minor modes."
  (let ((modes))
    (dolist (mode (prot-common-minor-modes-active))
      (push (prot-minibuffer--extract-commands mode) modes))
    modes))

(defun prot-minibuffer--commands ()
  "Merge and clean list of commands."
  (delete-dups
   (append (prot-minibuffer--extract-commands major-mode)
           (prot-minibuffer--extract-commands-minor))))

;;;###autoload
(defun prot-minibuffer-mode-commands ()
  "Run commands from current major mode and active minor modes."
  (interactive)
  (let ((commands (prot-minibuffer--commands)))
    (command-execute (intern (completing-read "M-X: " commands)))))

;;;; Simple actions for the "*Completions*" buffer

;; DEPRECATED: I just use Embark for such tasks, but am keeping this
;; around in case I ever need it.

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

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

(defmacro prot-minibuffer-completions-buffer-act (name doc &rest body)
  "Produce NAME function with DOC and rest BODY.
This is meant to define some basic commands for use in the
Completions' buffer."
  `(defun ,name ()
     ,doc
     (interactive)
     (let ((completions-window (get-buffer-window "*Completions*"))
           (completions-buffer (get-buffer "*Completions*"))
           (symbol (thing-at-point 'symbol)))
       (if (window-live-p completions-window)
           (with-current-buffer completions-buffer
             ,@body)
         (user-error "No live window with Completions")))))

(prot-minibuffer-completions-buffer-act
 prot-minibuffer-completions-kill-symbol-at-point
 "Append `symbol-at-point' to the `kill-ring'.
Intended to be used from inside the Completions' buffer."
 (kill-new `,symbol)
 (message "Copied %s to kill-ring"
          (propertize `,symbol 'face 'success)))

(prot-minibuffer-completions-buffer-act
 prot-minibuffer-completions-insert-symbol-at-point
 "Add `symbol-at-point' to last active window.
Intended to be used from inside the Completions' buffer."
 (let ((window (window-buffer (get-mru-window))))
   (with-current-buffer window
     (insert `,symbol)
     (message "Inserted %s"
              (propertize `,symbol 'face 'success)))))

(prot-minibuffer-completions-buffer-act
 prot-minibuffer-completions-insert-symbol-at-point-exit
 "Add `symbol-at-point' to last window and exit all minibuffers.
Intended to be used from inside the Completions' buffer."
 (let ((window (window-buffer (get-mru-window))))
   (with-current-buffer window
     (insert `,symbol)
     (message "Inserted %s"
              (propertize `,symbol 'face 'success))))
 (top-level))

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

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

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

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

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

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

This "filtering" mechanism, which is internally known as "narrowing", can be accessed via a key binding for all commands that support it. In my case, that key is the right angled bracket, or greater than sign (>) from inside the minibuffer (configure consult-narrow-key). So you type the narrow key and follow it up with another key that matches the relevant targets, such as > f to narrow the consult-imenu candidates to functions. Hit backspace to remove the narrowing (users may wish to set up Key chord hints (which-key.el), though I just memorise what I need, or call consult-narrow-help).

An all-in-one command that supports this kind of narrowing while also expanding on the features of its default equivalent is consult-buffer. Unlike the built-in switch-to-buffer, it grants direct access to recent files that no longer have a buffer visiting them, as well as to bookmarks. While I like this idea, I prefer to not mix my buffers with anything else, as then I get too many false positives that slow me down. However, you may prefer to economise on key bindings and/or like quick, seamless access to your "points of interest", regardless of whether they are internally stored as buffers, recent files, bookmarks.

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

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

As already suggested, Consult provides previews for its commands. This feature should work without any further intervention for users of Icomplete or Selectrum. For my case, however, where I rely on the default minibuffer plus Embark's ability to show and live-update completion candidates, this can be achieved with the embark-consult package: embark.el, emacs-consult.el, prot-embark.el.

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

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

(use-package consult
  :straight t
  :demand
  :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 ">")
  ;; NOTE: check `embark-consult' for previews that can be used with the
  ;; default minibuffer and Embark collections.
  :bind (("C-x M-:" . consult-complex-command)
         ("C-x M-m" . consult-minor-mode-menu)
         ("C-x M-k" . consult-kmacro)
         ("M-g g" . consult-goto-line)
         ("M-g M-g" . consult-goto-line)
         ("M-X" . consult-mode-command)
         ("M-K" . consult-keep-lines)  ; M-S-k is similar to M-S-5 (M-%)
         ("M-s g" . consult-grep)
         ("M-s m" . consult-mark)
         :map consult-narrow-map
         ("?" . consult-narrow-help)))

(use-package prot-consult
  :straight (:type built-in)
  :after (consult prot-pulse)
  :config
  (setq consult-project-root-function #'prot-consult-project-root)
  (setq prot-consult-command-centre-list
        '(consult-line
          prot-consult-line
          consult-mark))
  (setq prot-consult-command-top-list
        '(consult-outline
          consult-imenu
          prot-consult-outline
          prot-consult-imenu))
  (prot-consult-set-up-hooks-mode 1)
  :bind (("M-s i" . prot-consult-imenu)
         ("M-s s" . prot-consult-outline)    ; M-s o is `occur'
         ("M-s y" . prot-consult-yank)
         ("M-s l" . prot-consult-line)))

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

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

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; Tweaks for `consult.el' intended for my Emacs configuration:
;; <https://protesilaos.com/dotemacs/>.

;;; Code:

(when (featurep 'consult)
  (require 'consult))
(require 'prot-pulse)

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

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

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

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

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

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

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

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

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

;;;; Commands

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

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

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

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

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

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

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

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

Video introduction: Embark and my extras (2021-01-09).

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

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

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

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

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

Learn more about the available keymaps with M-x describe-keymap and then search for embark.

Now a few things about actions that you can gain access to by invoking either of embark-act (most common case), embark-act-noexit, embark-become:

  • To learn which keymap's contents get enabled in the present context, either set up which-key.el, or follow up the embark-act call with C-h (remember that this conforms with the Emacs convention of using C-h as a suffix to display help about possible key chords that complete what has already been typed in—if you are new to Emacs, consult my note on How do you learn Emacs?). For the sake of user-friendliness, I do set up which-key in this document (Key chord hints (which-key.el)) and apply the necessary tweaks in the following package declaration.
  • You will often be targeting individual items, such as the current completion candidate in the minibuffer, or the symbol at point. You can, however, collect the entire set of targets and store it in a buffer, which you can then re-use at your convenience or save it on disk (with write-file bound to C-x C-w by default). This is done by the embark-collect-snapshot command, which you can always access through embark-act.
    • The "Embark Collect" buffer can be presented as a grid or a list, with the possibility to manually switch between the two by means of the embark-collect-toggle-view command. The list view offers more room to the side of each candidate. It can be used to display annotations (see Completion annotations (marginalia)), such as the first line of a variable's doc string and current value, a command's key binding, the buffer's underlying file system path if it is visiting a file, and so on.
    • Embark's "collect" buffer also has a live-updating version, which can be use to filter the list of targets. This particular feature can, in fact, be used as a medium for visualising the list of candidates in the active minibuffer session. I do use it together with the default minibuffer as part my completion framework (also watch my presentation on Default Emacs completion and extras (2021-01-06)).
    • Other than producing a snapshot, Embark can also collect the targets and present them in a buffer whose major-mode is specialised to work on the category those targets belong to. This is done with the embark-export command. If you are targeting files, then the export takes you to a dired-mode buffer (also refer to this document's section on Dired (directory editor, file manager)); buffers go to ibuffer-mode (check Ibuffer and extras); grep results in a grep-mode buffer.

Finally, a few words about the following package declaration:

  • I use Embark's "live collection" feature as my front-end to the default minibuffer. This is done with the minibuffer-setup-hook. The "live completions" buffer pops up only after I have already typed something into the minibuffer, which helps minimise distractions (and which often means that I complete against candidates without ever seeing the completions' buffer—e.g. M-x M-n RET is precise and does not need to alter the window layout).
  • The embark-action-indicator sets up which-key. I do this to help users who may want to get started with Embark.
  • My prot-embark.el that is reproduced after the following block with the package configurations contains the following:
    • prot-embark--collect-fit-window ensures that Embark's live occur buffer shrinks and expands to match the window's contents.
    • prot-embark-completions-toggle toggles the display of the live completions' buffer.
    • Several extensions for cycling through the list of candidates and/or performing the default action while moving in a given direction.
    • Commands to clear lines in an Embark collect buffer. This can be done on a line-wise basis with prot-embark-collection-kill-line, or by matching a regular expression (search for the "flush lines" and "keep lines" wrappers).
    • The convenience command prot-embark-keyboard-quit which makes C-g abort a completions' buffer instead of just cancelling the active minibuffer's input.
    • The prot-embark-consult-preview-toggle to preview Consult matches. This leverages the embark-consult package (also check my configurations for consult.el and prot-consult.el).
  • NOTE: I also define a prot-embark-extras.el library which unifies Embark with all of my other libraries. It is mostly meant to define actions for embark-become (please check this section first and then visit Cross-package integration for Embark (prot-embark-extras.el)).
(use-package embark
  ;; Note that this gets only the main library.  That repo contains
  ;; other packages as well (which are small *.el files that are
  ;; distributed separately).
  :straight (embark :host github
                    :repo "oantolin/embark"
                    :branch "master"
                    :files ("embark.el"))
  :demand
  :diminish embark-collect-zebra-minor-mode
  :after prot-minibuffer
  :config
  (setq embark-collect-initial-view-alist
        '((file . list)
          (buffer . list)
          (symbol . list)
          (line . list)
          (xref-location . list)
          (kill-ring . zebra)
          (t . list)))
  (setq embark-collect-live-update-delay 0.5)
  (setq embark-collect-live-initial-delay 0.8)

  ;; Please don't read too much into the names of those faces.  Just
  ;; green and yellow.
  (setq embark-action-indicator (propertize "Act" 'face 'success))
  (setq embark-become-indicator (propertize "Become" 'face 'warning))

  ;; ;; NOTE: I keep this around for when I do videos, otherwise I do not
  ;; ;; use it.  It requires `which-key' to display key hints.
  ;; (setq embark-action-indicator
  ;;       (lambda (map)
  ;;         (which-key--show-keymap "Embark" map nil nil 'no-paging)
  ;;         #'which-key--hide-popup-ignore-command)
  ;;       embark-become-indicator embark-action-indicator)
  :hook ((minibuffer-setup-hook . embark-collect-completions-after-input)
         (embark-post-action-hook . embark-collect--update-linked)
         (embark-collect-mode-hook . prot-embark-completions-cursor))
  :bind (("C-," . embark-act)
         :map minibuffer-local-completion-map
         ("C-," . embark-act)
         ("C-." . embark-act-noexit)
         ("C->" . embark-become)
         ("M-q" . embark-collect-toggle-view) ; parallel of `fill-paragraph'
         :map embark-collect-mode-map
         ("C-," . embark-act)
         ("C-." . embark-act-noexit)
         ("," . embark-act)
         ("." . embark-act-noexit)
         ("M-q" . embark-collect-toggle-view)
         :map embark-symbol-map
         ("." . embark-find-definition)
         ("k" . describe-keymap)))

;; Integration with Consult.  Note that the package is `embark-consult',
;; but because it comes from the same repo as Embark I prefer to use
;; this straight.el directive (check the main embark package above).
(use-package embark-consult
  :straight (embark-consult :host github
                            :repo "oantolin/embark"
                            :branch "master"
                            :files ("embark-consult.el"))
  :demand
  :after (embark consult)
  ;; ;; Use the hook, or check `prot-embark-consult-preview-toggle'.
  ;; :hook (embark-collect-mode-hook . embark-consult-preview-minor-mode)
  :bind (:map embark-collect-mode-map
         ("C-j" . embark-consult-preview-at-point)))

(use-package prot-embark
  :straight (:type built-in)
  :demand
  :after embark
  :hook ((minibuffer-exit-hook . prot-embark-clear-live-buffers)
         (embark-collect-post-revert-hook . prot-embark-collect-fit-window)
         (embark-collect-mode-hook . prot-embark-hl-line)
         (embark-collect-mode-hook . prot-embark-display-line-numbers))
  ;; NOTE: to switch to the live collection buffer, I also use
  ;; `prot-minibuffer-focus-mini-or-completions' which is bound to
  ;; "s-v".
  :bind (:map embark-collect-mode-map
         ("h" . prot-simple-describe-symbol)  ; from `prot-simple.el'
         ("C-g" . prot-embark-keyboard-quit)
         ("C-k" . prot-embark-collection-kill-line)
         ("C-M-n" . prot-embark-completions-act-next)
         ("C-M-p" . prot-embark-completions-act-previous)
         ("C-M-j" . prot-embark-completions-act-current)
         ("C-M-v" . prot-embark-consult-preview-toggle) ; "view", "visualise" mnemonic
         ("C-n" . prot-embark-next-line-or-mini)
         ("C-p" . prot-embark-previous-line-or-mini)
         ("M-F" . prot-embark-collection-flush-lines) ; M-S-f like M-S-5 (M-%)
         ("M-K" . prot-embark-collection-keep-lines)  ; same principle as right above
         :map minibuffer-local-completion-map
         ("C-n" . prot-embark-switch-to-completions-top)
         ("C-p" . prot-embark-switch-to-completions-bottom)
         ("C-l" . prot-embark-completions-toggle)))

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

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

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; Extensions to `embark.el' for my Emacs configuration:
;; <https://protesilaos.com/dotemacs/>.

;;; Code:

(require 'cl-lib)
(when (featurep 'embark)
  (require 'embark))
(require 'prot-common)
(require 'prot-minibuffer)

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

;;;###autoload
(defun prot-embark-clear-live-buffers ()
  "Remove lingering Embark Collect Completions' buffers.
Add this to `minibuffer-exit-hook'."
  (let* ((buffers (buffer-list))
         (case-fold-search nil)
         (completions
          (cl-remove-if-not (lambda (buf)
                              (string-match "\\*Embark.*Completions.*"
                                            (format "%s" buf)))
                            buffers)))
    (mapc #'kill-buffer completions)))

;; Thanks to Omar Antolín Camarena for providing a variant of this!
;; (mistakes are always my own).
;;;###autoload
(defun prot-embark-collect-fit-window (&rest _)
  "Fit Embark's live occur window to its buffer.
To be added to `embark-occur-post-revert-hook'."
  (when (derived-mode-p 'embark-collect-mode)
    (fit-window-to-buffer (get-buffer-window)
                          (floor (frame-height) 2) 1)))

(defvar embark-collect-linked-buffer)

(defun prot-embark--live-buffer-p ()
  "Determine presence of a linked live occur buffer."
  (let ((buf embark-collect-linked-buffer))
    (when buf
      (window-live-p (get-buffer-window buf)))))

(defvar embark-collect--kind)

;; Thanks to Omar Antolín Camarena for sharing this:
;; <https://github.com/oantolin/embark/issues/114#issuecomment-761583395>
(defun prot-embark--live-completions-p ()
  "Determine whether current collection is for live completions."
  (and (derived-mode-p 'embark-collect-mode)
       (eq embark-collect--kind :completions)))

(defface prot-embark-hl-line
  '((default :extend t)
    (((class color) (min-colors 88) (background light))
     :background "#b0d8ff" :foreground "#000000")
    (((class color) (min-colors 88) (background dark))
     :background "#103265" :foreground "#ffffff")
    (t :inherit (font-lock-string-face elfeed-search-title-face)))
  "Face for current line in Embark completions."
  :group 'prot-embark)

(defface prot-embark-line-number
  '((default :inherit default)
    (((class color) (min-colors 88) (background light))
     :background "#f2eff3" :foreground "#252525")
    (((class color) (min-colors 88) (background dark))
     :background "#151823" :foreground "#dddddd")
    (t :inverse-video t))
  "Face for line numbers in Embark completions."
  :group 'prot-embark)

(defface prot-embark-line-number-current-line
  '((default :inherit default)
    (((class color) (min-colors 88) (background light))
     :background "#8ac7ff" :foreground "#000000")
    (((class color) (min-colors 88) (background dark))
     :background "#142a79" :foreground "#ffffff")
    (t :inverse-video t))
  "Face for current line number in Embark completions."
  :group 'prot-embark)

(autoload 'display-line-numbers-mode "display-line-numbers")
(autoload 'face-remap-remove-relative "face-remap")

;;;###autoload
(defun prot-embark-display-line-numbers ()
  "Set up line numbers for live Embark collect buffers.
Add this to `embark-collect-mode-hook'."
  (if (prot-embark--live-completions-p)
      (progn
        (face-remap-add-relative 'line-number 'prot-embark-line-number)
        (face-remap-add-relative 'line-number-current-line
                                 'prot-embark-line-number-current-line)
        (display-line-numbers-mode 1))
    (display-line-numbers-mode -1)
    ;; TODO: can we avoid `face-remap-add-relative' and just use the
    ;; value it previously returned?
    (face-remap-remove-relative
     (face-remap-add-relative 'line-number
                              'prot-embark-line-number))
    (face-remap-remove-relative
     (face-remap-add-relative 'line-number-current-line
                              'prot-embark-line-number-current-line))))

;;;###autoload
(defun prot-embark-hl-line ()
  "Set up line numbers for live Embark collect buffers.
Add this to `embark-collect-mode-hook'."
  (if (prot-embark--live-completions-p)
      (progn
        (face-remap-add-relative 'hl-line 'prot-embark-hl-line)
        (hl-line-mode 1))
    (hl-line-mode -1)
    ;; TODO: same as above with regard to `face-remap-add-relative'.
    (face-remap-remove-relative
     (face-remap-add-relative 'hl-line 'prot-embark-hl-line))))

;;;###autoload
(defun prot-embark-completions-cursor ()
  "`prot-minibuffer-completions-cursor' for Embark completions.
Add this to `embark-collect-mode-hook'."
  (if (prot-embark--live-completions-p)
      (prot-minibuffer-completions-cursor) ; from `prot-minibuffer.el'
    (kill-local-variable 'cursor-type)))

;; Thanks to Karthik Chikmagalur for providing an earlier version of
;; `prot-embark-keyboard-quit' command!  Sources to Karthik's work:
;;
;; + https://karthinks.com/
;; + https://github.com/karthink/.emacs.d/tree/clean
;;
;;;###autoload
(defun prot-embark-keyboard-quit ()
  "Control the exit behaviour for Embark collect buffers.

If in a live Embark collect/completions buffer, run
`abort-recursive-edit'.  Otherwise run `keyboard-quit'.

This is meant to be bound in `embark-collect-mode-map'."
  (interactive)
  (if (prot-embark--live-completions-p)
      (progn
        (kill-buffer)
        (abort-recursive-edit))
    (keyboard-quit)))

(declare-function embark-collect-completions "embark")

;;;###autoload
(defun prot-embark-completions-toggle ()
  "Toggle `embark-collect-completions'."
  (interactive)
  (if (prot-embark--live-buffer-p)
      (kill-buffer embark-collect-linked-buffer)
    (embark-collect-completions)))

(declare-function embark--act "embark")
(declare-function embark-default-action "embark")
(declare-function embark--target "embark")

(defun prot-embark--completions-act (arg)
  "Move ARG lines and perform `embark-default-action'."
  (forward-line arg)
  (embark--act #'embark-default-action (cdr (embark--target))))

;;;###autoload
(defun prot-embark-completions-act-next (&optional arg)
  "Run default action on next or ARGth Embark target.
This calls `prot-embark--completions-act' and is meant to be
assigned to a key in `embark-collect-mode-map'."
  (interactive "p")
  (prot-embark--completions-act (or arg 1)))

;;;###autoload
(defun prot-embark-completions-act-previous (&optional arg)
  "Run default action on previous or ARGth Embark target.
This calls `prot-embark--completions-act' and is meant to be
assigned to a key in `embark-collect-mode-map'."
  (interactive "p")
  (let ((num (prot-common-number-negative arg))) ; from `prot-common.el'
    (prot-embark--completions-act (or num -1))))

;;;###autoload
(defun prot-embark-completions-act-current ()
  "Run default action on Embark target without exiting.
Meant to be assigned to a key in `embark-collect-mode-map'."
  (interactive)
  (embark--act #'embark-default-action (cdr (embark--target))))

(defun prot-embark--switch-to-completions ()
  "Subroutine for switching to the Embark completions buffer."
  (unless (prot-embark--live-buffer-p)
    (prot-embark-completions-toggle))
  (let ((win (get-buffer-window embark-collect-linked-buffer)))
    (select-window win)))

;;;###autoload
(defun prot-embark-switch-to-completions-top ()
  "Switch to the top of Embark's completions buffer.
Meant to be bound in `minibuffer-local-completion-map'."
  (interactive)
  (prot-embark--switch-to-completions)
  (goto-char (point-min)))

;;;###autoload
(defun prot-embark-switch-to-completions-bottom ()
  "Switch to the bottom of Embark's completions buffer.
Meant to be bound in `minibuffer-local-completion-map'."
  (interactive)
  (prot-embark--switch-to-completions)
  (goto-char (point-max))
  (forward-line -1)
  (goto-char (point-at-bol))
  (recenter
   (- -1
      (min (max 0 scroll-margin)
           (truncate (/ (window-body-height) 4.0))))
      t))

;;;###autoload
(defun prot-embark-next-line-or-mini (&optional arg)
  "Move to the next line or switch to the minibuffer.
This performs a regular motion for optional ARG lines, but when
point can no longer move in that direction, then it switches to
the minibuffer."
  (interactive "p")
  (if (or (eobp) (eq (point-max) (save-excursion (forward-line 1) (point))))
      (prot-minibuffer-focus-mini)    ; from `prot-minibuffer.el'
    (forward-line (or arg 1)))
  (setq this-command 'next-line))

;;;###autoload
(defun prot-embark-previous-line-or-mini (&optional arg)
  "Move to the next line or switch to the minibuffer.
This performs a regular motion for optional ARG lines, but when
point can no longer move in that direction, then it switches to
the minibuffer."
  (interactive "p")
  (let ((num (prot-common-number-negative arg))) ; from `prot-common.el'
    (if (bobp)
        (prot-minibuffer-focus-mini)    ; from `prot-minibuffer.el'
      (forward-line (or num 1)))))

;;;###autoload
(defun prot-embark-collection-kill-line ()
  "Delete line from Embark collect buffer."
  (interactive)
  (let* ((inhibit-read-only t)
         (eol (point-at-eol))
         (eol-dwim (if (= eol (point-max)) eol (1+ eol))))
    (save-excursion
      (goto-char (point-at-bol))
      (delete-region (point) eol-dwim))))

;;;###autoload
(defun prot-embark-collection-flush-lines (regexp)
  "`flush-lines' matching REGEXP in Embark collect buffers."
  (interactive
   (list (read-regexp "Flush lines matching regexp: ")))
  (let ((inhibit-read-only t))
    (if (derived-mode-p 'embark-collect-mode)
        (with-current-buffer (current-buffer)
            (save-excursion
              (goto-char (point-min))
              (flush-lines regexp)))
      (user-error "Not in an Embark collect buffer"))))

;;;###autoload
(defun prot-embark-collection-keep-lines (regexp)
  "`keep-lines' matching REGEXP in Embark collect buffers."
  (interactive
   (list (read-regexp "Keep lines matching regexp: ")))
  (let ((inhibit-read-only t))
    (if (derived-mode-p 'embark-collect-mode)
        (with-current-buffer (current-buffer)
            (save-excursion
              (goto-char (point-min))
              (keep-lines regexp)))
      (user-error "Not in an Embark collect buffer"))))

(declare-function embark-consult-preview-minor-mode "embark-consult")
(defvar embark-consult-preview-minor-mode)

;;;###autoload
(defun prot-embark-consult-preview-toggle ()
  "Toggle preview mode for Embark's Consult collections."
  (interactive)
  (when (featurep 'embark-consult)
    (require 'embark-consult)
    (if (and (bound-and-true-p embark-consult-preview-minor-mode)
             (derived-mode-p 'embark-collect-mode))
        (progn
          (remove-hook 'embark-collect-mode-hook #'embark-consult-preview-minor-mode)
          (embark-consult-preview-minor-mode -1))
      (add-hook 'embark-collect-mode-hook #'embark-consult-preview-minor-mode)
      (embark-consult-preview-minor-mode 1))))

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

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

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

A "project" is, in our case, a directory whose contents are related to each other in terms of the end product they can provide. Think, for example, how Emacs' source code is a single "project" that delivers the program we use. In practical terms, a project is a version controlled directory (or directory tree). For my case that means git though other backends are also supported (same principle as with VC—see section on Generic version control framework).

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

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

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

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

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

(use-package project
  :demand
  :init
  (setq project-switch-commands
        '((?f "File" project-find-file)
          (?s "Subdir" prot-project-find-subdir)
          (?g "Grep" project-find-regexp)
          (?d "Dired" project-dired)
          (?b "Buffer" project-switch-to-buffer)
          (?q "Query replace" project-query-replace-regexp)
          (?t "Tag switch" prot-project-retrieve-tag)
          (?m "Magit" prot-project-magit-status)
          (?v "VC dir" project-vc-dir)
          (?l "Log VC" prot-project-commit-log)
          (?e "Eshell" project-eshell)))
  :bind ("C-x p q" . project-query-replace-regexp)) ; C-x p is `project-prefix-map'

(use-package prot-project
  :straight (:type built-in)
  :demand
  :init
  (setq prot-project-project-roots '("~/Git/Projects/" "~/Git/build/"))
  (setq prot-project-commit-log-limit 25)
  (setq prot-project-large-file-lines 1000)
  :bind (("C-x p <delete>" . prot-project-remove-project)
         ("C-x p l" . prot-project-commit-log)
         ("C-x p m" . prot-project-magit-status)
         ("C-x p s" . prot-project-find-subdir)
         ("C-x p t" . prot-project-retrieve-tag)))

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

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

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; This covers my project.el extensions, for use in my Emacs setup:
;; https://protesilaos.com/dotemacs.

;;; Code:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

(autoload 'magit-status "magit")

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

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

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

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

(defvar org-src-mode-hook)

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

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

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

This is a built-in minor mode that keeps track of the files you have opened, allowing you to revisit them faster. Its true power consists in the fact that its data, maintained in recentf-list, is a simple variable. This means that we can access it through any relevant piece of Elisp functionality.

To that end, the functions I define in prot-recentf.el are meant to either control the contents of the list or allow me to access them through my completion framework or a dedicated file listing.

Note that there exists a built-in recentf-open-files function for accessing the recent files through a bespoke buffer. I find that I have no use for it.

(use-package recentf
  :demand
  :config
  (setq recentf-save-file "~/.emacs.d/recentf")
  (setq recentf-max-saved-items 200)
  (setq recentf-exclude '(".gz" ".xz" ".zip" "/elpa/" "/ssh:" "/sudo:"))
  :hook (after-init-hook . recentf-mode))

(use-package prot-recentf
  :demand
  :config
  (add-to-list 'recentf-keep 'prot-recentf-keep-predicate)
  :bind (("s-r" . prot-recentf-recent-files)
         ("C-x C-r" . prot-recentf-recent-dirs)))

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

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

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; Extensions to `recentf.el' for my Emacs configuration:
;; <https://protesilaos.com/dotemacs/>.

;;; Code:

(require 'recentf)

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

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

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

;;;###autoload
(defun prot-recentf-recent-files (&optional input)
  "Select item from `recentf-list' using completion.
Use INPUT as an initial, yet editable, filter.

The user's $HOME directory is abbreviated as a tilde."
  (interactive)
  (let* ((files (mapcar 'abbreviate-file-name recentf-list))
         (f (completing-read "Open recentf entry: " files nil t
                             (or input nil) 'prot-recentf--history-files)))
    (find-file f)
    (add-to-history 'prot-recentf--history-files f)))

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

;;;###autoload
(defun prot-recentf-recent-dirs (&optional arg)
  "Select directory from `recentf-list' using completion.
With optional prefix ARG (\\[universal-argument]) present the
list in a `dired' buffer.  This buffer is meant to be reused by
subsequent invocations of this command (otherwise you need to
remove the `when' expression.

Without \\[universal-argument], the user's $HOME directory is
abbreviated as a tilde.  In the Dired buffer paths are absolute."
  (interactive "P")
  (let* ((dirs (prot-recentf--dirs))
         (buf "*Recentf Dired*")
         (default-directory "~")
         (f (unless arg (completing-read
                         "Recent dirs: " dirs nil t nil
                         'prot-recentf--history-dirs))))
    (when (get-buffer buf)
      (kill-buffer buf))
    (if arg
        (dired (cons (generate-new-buffer-name buf) dirs))
      (find-file f)
      (add-to-history 'prot-recentf--history-dirs f))))

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

3.1.8 Cross-package integration for Embark (prot-embark-extras.el)

NOTE: This section extends Embark so that it works with the rest of my packages and custom code. I keep it separate from prot-embark.el so that there is a clear distinction between my generic setup and the somewhat more opinionated extentions that may be unique to my use-case: Extended minibuffer actions and more (embark.el and prot-embark.el).

The way to extend Embark is to follow its own design: define keymaps and add them to the list of Embark's known keymaps.

(use-package prot-embark-extras
  :straight (:type built-in)
  :demand
  :config
  (prot-embark-extras-keymaps 1)
  (prot-embark-extras-setup-packages 1))

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

;;; prot-embark-extras.el --- Complementary extensions to embark.el for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; 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:
;;
;; Complementary extensions to `embark.el' for my Emacs configuration:;
; <https://protesilaos.com/dotemacs/>.

;;; Code:

(defgroup prot-embark-extras ()
  "Custom cross-package extensions for `embark'."
  :group 'editing)

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

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

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

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

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

(defvar embark-become-keymaps)

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

;;;; Keycast integration

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

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

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

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

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

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

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

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

3.1.9 In-buffer completions

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

What I have in this section is a few simple tweaks and built-in ways to complete terms while typing text in a buffer. I think that, for most cases, the minibuffer can be used effectively to perform in-buffer completion.

3.1.9.1 Dabbrev (dynamic word completion)

This is Emacs' own approach to dynamic/arbitrary text completion inside the buffer: "dynamic abbreviation" or else dabbrev. This mechanism works by reading all text before point to find a suitable match. Different scenaria determine whether it should also look forward and in other buffers. In essence, Dabbrev can help you type again what you already have. It will not draw candidates from some knowledge bank.

With dabbrev-expand we make an attempt to complete the text at point. Repeated invocations will cycle through the candidates. No feedback is provided, much in the same way yanking from the kill-ring works. To complete a phrase, matching the last succesful dabbrev-expand, you need to supply an empty space and call the command again. This will match the next word, and so on for N words.

Whereas dabbrev-completion benefits from minibuffer interactivity and the pattern matching styles in effect (Completion framework and extras). If you configure completion-in-region-function to display a list of candidates, such as how I do with Consult, then you can use that to pick the candidate you want (Enhanced minibuffer commands (consult.el)).

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

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

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

As for hippie-exp, this is another built-in library that builds on top of dabbrev. I like what it does, but feel that its lack of visual feedback prevents it from realising its potential. Maybe we will one day have a consult-hippie alternative

(use-package dabbrev
  :config
  (setq dabbrev-abbrev-char-regexp "\\sw\\|\\s_")
  (setq dabbrev-abbrev-skip-leading-regexp "[$*/=~']")
  (setq dabbrev-backward-only nil)
  (setq dabbrev-case-distinction 'case-replace)
  (setq dabbrev-case-fold-search 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)
  :bind (("M-/" . dabbrev-expand)
         ("s-/" . dabbrev-completion)))

(use-package hippie-exp
  :config
  (setq hippie-expand-try-functions-list
        '(try-expand-dabbrev
          try-expand-dabbrev-all-buffers
          try-expand-dabbrev-from-kill
          try-expand-all-abbrevs
          try-expand-list
          try-expand-line
          try-complete-lisp-symbol-partially
          try-complete-lisp-symbol
          try-complete-file-name-partially
          try-complete-file-name))
  (setq hippie-expand-verbose t)
  (setq hippie-expand-dabbrev-skip-space nil)
  (setq hippie-expand-dabbrev-as-symbol t)
  (setq hippie-expand-no-restriction t)
  :bind ("C-M-/" . hippie-expand))
3.1.9.2 Skeletons and abbreviations

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

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

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

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

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

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

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

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

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

3.2.1 Isearch, occur, and extras (prot-search.el)

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

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

The beauty of the Occur and Replace commands is that they can be initiated from within an active Isearch session, using the same keys. So C-s INPUT M-s o will search for input and then run occur on it. Try C-h k C-s to get a help menu with all the extra keys you can use with isearch. These are the ones I use the most:

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

Every one of the above, except the first item, can be executed on their own, or as extensions of C-s (and variants).

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

Now here is a neat trick I discovered a while ago that makes Isearch even better for most tasks: the ability to interpret a space as a wildcard. This is due to the combined effect of the values assigned to the variables search-whitespace-regexp, isearch-lax-whitespace, isearch-regexp-lax-whitespace. So you can now search for something like se di bu al and it will return setq display-buffer-alist. And you can still combine it with all of the aforementioned! Note that this affects regular searches (the standard C-s and C-r). The regexp-sensitive functions C-M-s and C-M-r remain in tact. You can always toggle whitespace matching behaviour while performing a search, with M-s SPC (revert back to just literal spaces).

Now on to my prot-search.el library which provides some extensions to an already well-designed architecture (the code is reproduced after the package declarations).

  • prot-search-isearch-other-end simply places point at the opposite end of the current match. Particularly helpful while recording keyboard macros. This is to work around the default behaviour of Isearch which puts the point at either the beginning or the end of the match, depending on the direction it is moving in. For single words or balanced expressions this is not an issue because you can always confirm+exit a search by using a motion key (so, for example, move to the end of the matching word with M-f). There are, however, matches that are not limited to such boundaries, especially with the wildcard hack mentioned above. For those cases moving to the opposite end might require multiple key presses, which is bad when trying to record an efficient keyboard macro. Note though that you can achieve the same result by changing the direction the search is moving towards with C-s or C-r (though I still prefer my minor addition).
  • prot-search-isearch-abort-dwim deletes the entirety of the non-matching input while leaving the valid parts in place. Otherwise it behaves like a standard backward character deletion. The built-in method to remove the entirety of a mismatched input is to hit C-g following a failed search. However, I find that the choice of key binding can prove problematic, since C-g also exits a successful search, while I also prefer a "do-what-I-mean" behaviour.
  • prot-search-isearch-replace-symbol runs a forward-looking query-replace for the symbol at point. Simple and effective for quickly refactoring a given function/variable name (and one of the reasons why I have never needed an extra package for such tasks).
  • prot-search-isearch-beginning-of-buffer and its counterpart prot-search-isearch-end-of-buffer move to the first or last instance of the symbol at point. They also accept a numeric argument, which they interpret as an offset. In practice, this is the same as running M-s . M-s < or M-s . M-s >.
  • prot-search-occur-urls gathers all URLs in the current buffer and places them in an Occur buffer without their context while also making them clickable (we say that it "buttonises" them).
  • prot-search-occur-browse-url gathers all URLs in the buffer and prompts you to select one with completion. It then browses that item using whatever browser you have for browse-url-browser-function.
(use-package isearch
  :diminish
  :config
  (setq search-highlight t)
  (setq search-whitespace-regexp ".*?")
  (setq isearch-lax-whitespace t)
  (setq isearch-regexp-lax-whitespace nil)
  (setq isearch-lazy-highlight t)
  ;; All of the following variables were introduced in Emacs 27.1.
  (setq isearch-lazy-count t)
  (setq lazy-count-prefix-format nil)
  (setq lazy-count-suffix-format " (%s/%s)")
  (setq isearch-yank-on-move 'shift)
  (setq isearch-allow-scroll 'unlimited)
  :bind (:map minibuffer-local-isearch-map
         ("M-/" . isearch-complete-edit)
         :map isearch-mode-map
         ("C-g" . isearch-cancel)       ; instead of `isearch-abort'
         ("M-/" . isearch-complete)))

(use-package replace
  :config
  (setq list-matching-lines-jump-to-current-line t)
  :hook ((occur-mode-hook . hl-line-mode)
         (occur-mode-hook . (lambda ()
                              (toggle-truncate-lines t))))
  :bind (("M-s M-o" . multi-occur)
         :map occur-mode-map
         ("t" . toggle-truncate-lines)))

(use-package prot-search
  :straight (:type built-in)
  :bind (("M-s %" . prot-search-isearch-replace-symbol)
         ("M-s M-<" . prot-search-isearch-beginning-of-buffer)
         ("M-s M->" . prot-search-isearch-end-of-buffer)
         ("M-s u" . prot-search-occur-urls)
         ("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)))

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

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

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; This covers my isearch.el and replace.el extensions, for use in my
;; Emacs setup: https://protesilaos.com/dotemacs.

;;; Code:

(require 'isearch)
(require 'replace)

;;;; Isearch

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

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

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

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

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

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

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

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

;;;; Replace/Occur

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

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

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

;;;###autoload
(defun prot-search-occur-urls ()
  "Produce buttonised list of all URLs in the current buffer."
  (interactive)
  (add-hook 'occur-hook #'goto-address-mode)
  (occur prot-search-url-regexp "\\&")
  (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-url'."
  (interactive)
  (let ((matches nil))
    (save-excursion
      (goto-char (point-min))
      (while (search-forward-regexp prot-search-url-regexp nil t)
        (push (match-string-no-properties 0) matches)))
    (funcall browse-url-browser-function
             (completing-read "Browse URL: " matches nil t))))

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

3.2.2 Regular expressions: re-builder and visual-regexp

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

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

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

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

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

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

3.2.3 wgrep (writable grep)

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

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

3.2.4 Cross-references (xref.el)

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

This package declaration contains only the basics. I might add more in the future.

(use-package xref
  :config
  ;; All those have been changed for Emacs 28
  (setq xref-show-definitions-function #'xref-show-definitions-completing-read)
  (setq xref-show-xrefs-function #'xref-show-definitions-completing-read)
  (setq xref-file-name-display 'project-relative)
  (setq xref-search-program 'ripgrep))

4 Directory, buffer, window management

4.1 Dired (directory editor, file manager)

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

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

Check some of my videos:

4.1.1 Base settings for Dired

The options here are meant to do the following:

  • Copy and delete recursively. No need to be prompted about each action.
  • While in detailed view, search only file names while point is on one of them, else apply the query to all the rest.
  • Deletion sends items to the system's Trash, making it safer than the standard rm.
  • Reformat output. Sort directories first. Show dotfiles and place them before anything else. Omit implicit directories (the single and double dots). Use human-readable size units. To learn everything about these switches, you need to read the manpage of ls. You can do so with M-x man RET ls.
    • Note that dired-listing-switches and find-ls-option are configured to show hidden directories and files before their non-hidden counterparts. If you want to reverse this order, you must include the -X option (such as -AFXhlv --group-directories-first).
  • Hide all the details by default (permissions, size, etc.). Those can easily be toggled on using the left parenthesis (. Also enable highlighting of the current line, which makes it even easier to spot the current item (I do not enable this globally, because I only want it for line-oriented interfaces, such as Dired's, but not for text editing).
  • While having two dired buffers open, the rename and copy operations will place the path of the inactive one as the target destination. When multiple dired buffers are present, this works between the current and most recently used ones.
  • For Emacs 27.1, Dired can automatically create destination directories for its copy and rename operations. So you can, for example, move file to /non-existent-path/file and you will get what you want right away.
  • For Emacs 27.1, renaming a file of a version-controlled repository (git) will be done using the appropriate VC mechanism.

The commands with the contrib/ prefix in dired-aux are copied from the Emacs configurations of Omar Antolín Camarena. They let you insert the path of a bookmarked directory while performing an action such as copying and renaming/moving a file.

While prot-dired.el (reproduced after the package configurations) contains a set of commands that are conceptually related to those present in Projects (project.el and prot-project.el), as well as the ones in the Ibuffer section. In short, they leverage the fd executable to recursively search for directories or directories+files from the root of the current version-controlled directory tree, if inside one, or just the present working directory.

Finally, to see how I get commits for marked files that belong to a given version-controlled (git) repo, see the section of this document on Version control framework (vc.el and prot-vc.el).

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

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

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

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

  :bind ( :map dired-mode-map
          ("C-+" . dired-create-empty-file)
          ("M-s f" . nil)
          ("C-x v v" . dired-vc-next-action) ; Emacs 28
          :map minibuffer-local-filename-completion-map
          ("C-c d" . contrib/cd-bookmark)))

(use-package prot-dired
  :disabled                            ; superseded by `consult.el'
  :straight (:type built-in)
  :bind (("M-s d" .  prot-dired-fd-dirs)
         ("M-s z" . prot-dired-fd-files-and-dirs)))

;; NOTE 2021-01-04: deprecated first in favour of `prot-dired.el' and
;; eventually by `consult.el'
(use-package find-dired
  :disabled
  :after dired
  :config
  (setq find-ls-option
        '("-ls" . "-AGFhlv --group-directories-first --time-style=long-iso"))
  (setq find-name-arg "-iname"))

(use-package async :straight t)

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

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

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

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; This covers my dired.el extensions, for use in my Emacs setup:
;; https://protesilaos.com/dotemacs.

;;; Code:

(require 'prot-common)

;; NOTE 2021-01-04: This library is deprecated and superseded by
;; `prot-consult.el'.

(defun prot-dired--expand-root-dir ()
  "Expand file name of project or current directory."
  (expand-file-name (or (vc-root-dir)
                        (locate-dominating-file "." ".git")
                        default-directory)))

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

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

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

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

4.1.2 wdired (writable dired)

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

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

4.1.3 image-dired (image thumbnails and previews)

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

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

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

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

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

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

What I have here:

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

4.1.5 dired-x (extra Dired functions)

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

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

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

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

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

4.1.6 dired-like view for the trash directory

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

Basically, its interaction model is as follows:

  • m to mark for some deferred action, such as D to delete, R to restore.
  • t to toggle the status of all items as marked. Use this without marks to m (mark) all items, then call a deferred action to operate on them.
  • d to mark for permanent deletion.
  • r to mark for restoration.
  • x to execute these special marks.
(use-package trashed
  :straight t
  :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"))

4.2 Working with buffers

4.2.1 Unique names for buffers

These settings make it easier to work with multiple buffers. When two buffers have the same name, Emacs will try to disambiguate them by displaying their element of differentiation in accordance with the style of uniquify-buffer-name-style. While uniquify-strip-common-suffix will remove the part of the file system path they have in common.

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

(use-package uniquify
  :config
  (setq uniquify-buffer-name-style 'forward)
  (setq uniquify-strip-common-suffix t)
  (setq uniquify-after-kill-buffer-p t))

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

ibuffer.el ships with Emacs and it provides a drop-in replacement for list-buffers. Compared to its counterpart, it allows for granular control over the buffer list and is more powerful overall. For this reason I bind it to C-x C-b.

Overview of its features:

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

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

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

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

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

Now some extras that I introduced after I published that video, which pertain to my prot-ibuffer.el library (copied in its entirety below the package declarations):

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

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

(use-package ibuffer
  :config
  (setq ibuffer-expert t)
  (setq ibuffer-display-summary nil)
  (setq ibuffer-use-other-window nil)
  (setq ibuffer-show-empty-filter-groups nil)
  (setq ibuffer-movement-cycle nil)
  (setq ibuffer-default-sorting-mode 'filename/process)
  (setq ibuffer-use-header-line t)
  (setq ibuffer-default-shrink-to-minimum-size nil)
  (setq ibuffer-formats
        '((mark modified read-only locked " "
                (name 30 30 :left :elide)
                " "
                (size 9 -1 :right)
                " "
                (mode 16 16 :left :elide)
                " " filename-and-process)
          (mark " "
                (name 16 -1)
                " " filename)))
  (setq ibuffer-saved-filter-groups nil)
  (setq ibuffer-old-time 48)
  :hook (ibuffer-mode-hook . hl-line-mode)
  :bind (("C-x C-b" . ibuffer)
         :map ibuffer-mode-map
         ("* f" . ibuffer-mark-by-file-name-regexp)
         ("* g" . ibuffer-mark-by-content-regexp) ; "g" is for "grep"
         ("* n" . ibuffer-mark-by-name-regexp)
         ("s n" . ibuffer-do-sort-by-alphabetic)  ; "sort name" mnemonic
         ("/ g" . ibuffer-filter-by-content)))

(use-package prot-ibuffer
  :straight (:type built-in)
  :bind (("M-s b" . prot-ibuffer-buffers-major-mode)
         ("M-s v" . prot-ibuffer-buffers-vc-root)))

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

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

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; This covers my ibuffer.el extensions, for use in my Emacs setup:
;; https://protesilaos.com/dotemacs.

;;; Code:

(require 'ibuffer)

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

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

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

4.2.3 Scratch buffers per major-mode

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

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

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

4.3 Window configuration

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

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

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

4.3.1 Window rules and basic tweaks

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

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

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

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

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

(use-package window
  :init
  (setq display-buffer-alist
        '(;; top side window
          ("\\**prot-elfeed-bongo-queue.*"
           (display-buffer-reuse-window display-buffer-in-side-window)
           (window-height . 0.16)
           (side . top)
           (slot . -2))
          ("\\*\\(prot-elfeed-mpv-output\\|world-clock\\).*"
           (display-buffer-in-side-window)
           (window-height . 0.16)
           (side . top)
           (slot . -1))
          ("\\*\\(Flymake\\|Package-Lint\\|vc-git :\\).*"
           (display-buffer-in-side-window)
           (window-height . 0.16)
           (side . top)
           (slot . 0)
           (window-parameters . ((no-other-window . t))))
          ("\\*Messages.*"
           (display-buffer-in-side-window)
           (window-height . 0.16)
           (side . top)
           (slot . 1)
           (window-parameters . ((no-other-window . t))))
          ("\\*\\(Backtrace\\|Warnings\\|Compile-Log\\)\\*"
           (display-buffer-in-side-window)
           (window-height . 0.16)
           (side . top)
           (slot . 2)
           (window-parameters . ((no-other-window . t))))
          ;; bottom side window
          ("\\*\\(Embark\\)?.*Completions.*"
           (display-buffer-in-side-window)
           (side . bottom)
           (slot . 0)
           (window-parameters . ((no-other-window . t)
                                 (mode-line-format . none))))
          ;; left side window
          ("\\*Help.*"
           (display-buffer-in-side-window)
           (window-width . 0.20)       ; See the :hook
           (side . left)
           (slot . 0)
           (window-parameters . ((no-other-window . t))))
          ;; right side window
          ("\\*Faces\\*"
           (display-buffer-in-side-window)
           (window-width . 0.25)
           (side . right)
           (slot . 0)
           (window-parameters
            . ((mode-line-format
                . (" "
                   mode-line-buffer-identification)))))
          ("\\*Custom.*"
           (display-buffer-in-side-window)
           (window-width . 0.25)
           (side . right)
           (slot . 1)
           (window-parameters . ((no-other-window . t))))
          ;; bottom buffer (NOT side window)
          ("\\*\\vc-\\(incoming\\|outgoing\\).*"
           (display-buffer-at-bottom))
          ("\\*\\(Output\\|Register Preview\\).*"
           (display-buffer-at-bottom)
           (window-parameters . ((no-other-window . t))))
          ("\\*.*\\([^E]eshell\\|shell\\|v?term\\).*"
           (display-buffer-reuse-mode-window display-buffer-at-bottom)
           (window-height . 0.2)
           ;; (mode . '(eshell-mode shell-mode))
           )))
  (setq window-combination-resize t)
  (setq even-window-sizes 'height-only)
  (setq window-sides-vertical nil)
  (setq switch-to-buffer-in-dedicated-window 'pop)
  ;; Hooks' syntax is controlled by the `use-package-hook-name-suffix'
  ;; variable.  The "-hook" suffix is intentional.
  :hook ((help-mode-hook . visual-line-mode)
         (custom-mode-hook . visual-line-mode))
  :bind (("s-n" . next-buffer)
         ("s-p" . previous-buffer)
         ("s-o" . other-window)
         ("s-2" . split-window-below)
         ("s-3" . split-window-right)
         ("s-0" . delete-window)
         ("s-1" . delete-other-windows)
         ("s-!" . delete-other-windows-vertically) ; s-S-1
         ("s-5" . delete-frame)
         ("C-x _" . balance-windows)
         ("C-x +" . balance-windows-area)
         ("s-q" . window-toggle-side-windows)))

4.3.2 Window history (winner-mode)

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

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

4.3.3 Directional window motions (windmove)

Windmove is also built into Emacs. It provides functions for selecting a window in any of the cardinal directions. A decent addition to the simpler other-window command (C-x o by default).

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

(use-package windmove
  :config
  (setq windmove-create-window nil)     ; Emacs 27.1
  :bind (("<C-M-up>" . windmove-up)
         ("<C-M-right>" . windmove-right)
         ("<C-M-down>" . windmove-down)
         ("<C-M-left>" . windmove-left)))

4.3.4 Tabs for window layouts (and prot-tab.el)

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

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

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

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

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

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

For me tabs are useful as groups of buffers in a given window configuration. I do not want a persistent bar with buttons that introduces extra visual clutter. Switching to tabs is done through completion, specifically prot-tab-select-tab-dwim.

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

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

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

Here my settings, followed by the entirety of prot-tab.el.

(use-package tab-bar
  :init
  (setq tab-bar-close-button-show nil)
  (setq tab-bar-close-last-tab-choice 'tab-bar-mode-disable)
  (setq tab-bar-close-tab-select 'recent)
  (setq tab-bar-new-tab-choice t)
  (setq tab-bar-new-tab-to 'right)
  (setq tab-bar-position nil)
  (setq tab-bar-show nil)
  (setq tab-bar-tab-hints nil)
  (setq tab-bar-tab-name-function 'tab-bar-tab-name-all)
  :config
  (tab-bar-mode -1)
  (tab-bar-history-mode -1)
  :bind (("<s-tab>" . tab-next)
         ("<S-s-iso-lefttab>" . tab-previous)))

(use-package prot-tab
  :straight (:type built-in)
  :after tab-bar
  :bind (("<f8>" . prot-tab-tab-bar-toggle)
         ("C-x t t" . prot-tab-select-tab-dwim)
         ("s-t" . prot-tab-select-tab-dwim)))

;; This is only included as a reference.
(use-package tab-line
  :disabled
  :commands (tab-line-mode global-tab-line-mode)
  :config
  (global-tab-line-mode -1))

The prot-tab.el code, which is in my dotfiles' repo:

;;; prot-tab.el --- Tab bar (tab-bar.el) extras for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; This set of configurations pertains to my tab-bar.el extensions, for
;; use in my Emacs setup: https://protesilaos.com/dotemacs.

;;; Code:

(require 'tab-bar)

(defun prot-tab--tab-bar-tabs ()
  "Return a list of `tab-bar' tabs, minus the current one."
  (mapcar (lambda (tab)
            (alist-get 'name tab))
          (tab-bar--tabs-recent)))

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

;;;###autoload
(defun prot-tab-tab-bar-toggle ()
  "Toggle `tab-bar' presentation."
  (interactive)
  (if (bound-and-true-p tab-bar-mode)
      (progn
        (setq tab-bar-show nil)
        (tab-bar-mode -1))
    (setq tab-bar-show t)
    (tab-bar-mode 1)))

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

4.3.5 Transposition and rotation of windows

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

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

5 Applications and utilities

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

5.1 Custom extensions for "focus mode" (prot-logos.el)

My prot-logos.el (copied verbatim after the package declaration) provides the necessary infrastructure for my preferred "focus mode" aesthetic. Everything is controlled by prot-logos-focus-mode. Bind that to a key and you are good to go. An overview of its components, which are contingent on other features:

Olivetti (centred buffer content)
I spend much of my time in Emacs reading and writing long form texts. It is nice to be able to easily toggle a mode that centres the buffer, allowing for greater comfort. Olivetti fulfils that niche very nicely. It is not aggressive in its interface requirements, respects my existing line settings and my preference for auto-filling text, while it does not introduce any kind of functionality beyond the scope of bringing the current window's buffer to the centre of the view. This is exactly what I need. Any other enhancement, such as a larger font size can be delegated to a specialised instrument. Thanks to Paul W. Rankin for providing such a nimble tool! For prot-logos Olivetti always gets activated.
variable-pitch-mode (mixed fonts)
This is a built-in mode that remaps the default face's font family to a proportionately spaced one (also see Font configurations (prot-fonts.el)). It can produce a prose-friendly presentation, especially if the variable-pitch face is set to some nice font family. As the effect is not particularly good in prog-mode buffers, due to misalignments in spacing and indentation, prot-logos only applies variable width fonts in text-mode buffers. The activation is further controlled by prot-logos-variable-pitch (off by default). Bear in mind that variable-pitch-mode is quite aggressive in its application, as it affects all other faces, unless the active theme (or some minor mode) makes provisions to retain fixed typographic spacing for those elements that require it, such as code blocks and inline code elements, tables, and indentation (refer to Modus themes (my highly accessible themes)).
org-tree-slide and org-indent
The former is a third-party package and the latter is part of the Org distribution. What the first does is convert headings into pseudo slides. While the other indents content visually, without actually affecting the underlying initial spacing, to match the heading's depth. Those two are disabled by default and the prot-logos-org-presentation toggle determines whether they should be activated.
Scroll lock
Sometimes you want the cursor to remain centred on the screen while your focus is on writing or reading. This is controlled by the variable prot-logos-scroll-lock (off by default), which controls the activation of the built-in scroll-lock-mode.
Modeline
The variable prot-logos-hidden-modeline (off by default) can be set to t to hide the modeline while entering the focused state. For me this is mostly useful for presentations.
Fringes
There is also a function that unconditionally disables fringes on the edge of the window. It ensures that we do not see that area and any indicators that may be placed on it while entering the focus state (refer to Fringe mode for the relevant configurations, while their overall presentation is controlled by the active theme).

All those combined contribute to an outcome that is appropriate for long reading or writing sessions, as well as presentations. I intentionally do not introduce any font-resizing effect, as my needs vary in that regard depending on the context (though do refer to the prot-fonts.el I linked to earlier).

For video demonstrations, albeit with earlier versions of my code, watch these:

(use-package face-remap :diminish buffer-face-mode)

(use-package olivetti
  :straight t
  :diminish
  :config
  (setq olivetti-body-width 0.7)
  (setq olivetti-minimum-body-width 80)
  (setq olivetti-recall-visual-line-mode-entry-state t))

(use-package org-tree-slide
  :straight t
  :diminish
  :config
  (setq org-tree-slide-breadcrumbs nil)
  (setq org-tree-slide-header nil)
  (setq org-tree-slide-slide-in-effect nil)
  (setq org-tree-slide-heading-emphasis nil)
  (setq org-tree-slide-cursor-init t)
  (setq org-tree-slide-modeline-display nil)
  (setq org-tree-slide-skip-done nil)
  (setq org-tree-slide-skip-comments t)
  (setq org-tree-slide-fold-subtrees-skipped t)
  (setq org-tree-slide-skip-outline-level 8)
  (setq org-tree-slide-never-touch-face t)
  (setq org-tree-slide-activate-message
        (format "Presentation %s" (propertize "ON" 'face 'success)))
  (setq org-tree-slide-deactivate-message
        (format "Presentation %s" (propertize "OFF" 'face 'error)))
  :bind (:map org-tree-slide-mode-map
         ("<C-down>" . org-tree-slide-display-header-toggle)
         ("<C-right>" . org-tree-slide-move-next-tree)
         ("<C-left>" . org-tree-slide-move-previous-tree)))

(use-package prot-logos
  :straight (:type built-in)
  :diminish visual-line-mode
  :config
  (setq prot-logos-org-presentation nil)
  (setq prot-logos-variable-pitch nil)
  (setq prot-logos-scroll-lock nil)
  (setq prot-logos-hidden-modeline nil)
  :bind ("<f9>" . prot-logos-focus-mode))

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

;;; prot-logos.el --- Extensions for my dotemacs to help read, write, present -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; Extensions to help me read, write, present.  For use in my Emacs
;; setup: https://protesilaos.com/dotemacs.

;;; Code:

(when (featurep 'org-tree-slide)
  (require 'org-tree-slide))
(when (featurep 'org-indent)
  (require 'org-indent))
(when (featurep 'olivetti)
  (require 'olivetti))
(require 'face-remap)
(require 'org)

(defgroup prot-logos ()
  "Setup for reading and presenting text-heavy buffers."
  :group 'files)

(defcustom prot-logos-org-presentation nil
  "Org files should switch to presentation view.
This concerns cases where variable `prot-logos-focus-mode' is set
to non-nil and determines whether headings should be converted
into pseudo slides and indentation be adjusted accordingly."
  :type 'boolean
  :group 'prot-logos)

(defcustom prot-logos-variable-pitch nil
  "Non-programming buffers should switch to `variable-pitch-mode'.
In programming modes the default font is always used, as that is
assumed to be a monospaced typeface."
  :type 'boolean
  :group 'prot-logos)

(defcustom prot-logos-scroll-lock nil
  "Use centred scrolling while in focused view."
  :type 'boolean
  :group 'prot-logos)

(defcustom prot-logos-hidden-modeline nil
  "Hide the modeline."
  :type 'boolean
  :group 'prot-logos)

(defvar prot-logos--focus-mode-hook nil
  "Hook that runs from function `prot-logos-focus-mode'.")

;;;###autoload
(define-minor-mode prot-logos-focus-mode
  "Buffer-local wrapper mode for presentations.
Other tools should hook into `prot-logos--focus-mode-hook' to
introduce their effects.  Otherwise this minor mode has no effect
on its own."
  :init-value nil
  :global nil
  :lighter " -Λ-"			; greek lambda majuscule
  (run-hooks 'prot-logos--focus-mode-hook))

(autoload 'buffer-face-mode "face-remap")
(autoload 'variable-pitch-mode "face-remap")

(defun prot-logos--variable-pitch-toggle ()
  "Make text use `variable-pitch' face, except for programming."
  (when (and prot-logos-variable-pitch
             (derived-mode-p 'text-mode))
    (if (or (bound-and-true-p buffer-face-mode)
	        (not (bound-and-true-p prot-logos-focus-mode)))
	    (variable-pitch-mode -1)
      (variable-pitch-mode 1))))

(add-hook 'prot-logos--focus-mode-hook #'prot-logos--variable-pitch-toggle)

(autoload 'olivetti-mode "olivetti")

(defun prot-logos--olivetti-toggle ()
  "Toggle the variable `olivetti-mode', if available."
  (if (or (bound-and-true-p olivetti-mode)
          (not (bound-and-true-p prot-logos-focus-mode)))
	  (olivetti-mode -1)
	(olivetti-mode 1)))

(add-hook 'prot-logos--focus-mode-hook #'prot-logos--olivetti-toggle)

(defun prot-logos--fringe-toggle ()
  "Toggle fringe width."
  (if (or (= (car (window-fringes)) 0)
          (not (bound-and-true-p prot-logos-focus-mode)))
      (set-window-fringes (selected-window) nil)
    (set-window-fringes (selected-window) 0 0)))

(add-hook 'prot-logos--focus-mode-hook #'prot-logos--fringe-toggle)

(autoload 'org-tree-slide-mode "org-tree-slide")

(defun prot-logos--org-tree-slide-mode ()
  "Toggle variable `org-tree-slide-mode' if loaded and needed."
  (let* ((buf (window-buffer (get-mru-window)))
         (mode (with-current-buffer buf major-mode)))
    (when (and prot-logos-org-presentation
               (eq mode 'org-mode))
	  (if (or (bound-and-true-p org-tree-slide-mode)
		      (not (bound-and-true-p prot-logos-focus-mode)))
	      (org-tree-slide-mode -1)
	    (org-tree-slide-mode 1)))))

(add-hook 'prot-logos--focus-mode-hook #'prot-logos--org-tree-slide-mode)

(autoload 'org-indent-mode "org")

(defun prot-logos--org-indent-mode ()
  "Toggle variable `org-tree-slide-mode' if loaded and needed."
  (let* ((buf (window-buffer (get-mru-window)))
         (mode (with-current-buffer buf major-mode)))
    (when (and prot-logos-org-presentation
               (eq mode 'org-mode))
	  (if (or (bound-and-true-p org-indent-mode)
		      (not (bound-and-true-p prot-logos-focus-mode)))
	      (org-indent-mode -1)
	    (org-indent-mode 1)))))

(add-hook 'prot-logos--focus-mode-hook #'prot-logos--org-indent-mode)

(defun prot-logos--scroll-lock ()
  "Keep the point at the centre."
  (when prot-logos-scroll-lock
    (if (or (bound-and-true-p scroll-lock-mode)
		    (not (bound-and-true-p prot-logos-focus-mode)))
        (scroll-lock-mode -1)
      (recenter nil)
      (scroll-lock-mode 1))))

(add-hook 'prot-logos--focus-mode-hook #'prot-logos--scroll-lock)

;; Based on Paul W. Rankin's code:
;; https://gist.github.com/rnkn/a522429ed7e784ae091b8760f416ecf8
(defun prot-logos--hidden-modeline ()
  "Toggle mode line visibility."
  (when prot-logos-hidden-modeline
    (if (or (eq mode-line-format nil)
		    (not (bound-and-true-p prot-logos-focus-mode)))
        (kill-local-variable 'mode-line-format)
      (setq-local mode-line-format nil)
      (force-mode-line-update))))

(add-hook 'prot-logos--focus-mode-hook #'prot-logos--hidden-modeline)

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

5.2 USLS — Unassuming Sidenotes of Little Significance

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

Because this is standard Emacs stuff, I can always benefit from the rest of my setup, such as to search for file contents in the current directory. Study the entirety of my Completion framework and extras.

In the usls.el code I wanted to respect key binding conventions, but on my own setup I can do whatever I want. So let us make sure we use a more convenient set of key combinations. The other options I have here are for the sake of visibility and are left to their default values.

The code for this project is on the USLS Gitlab repo and reproduced in the subsequent code block.

(use-package usls
  :straight (:type built-in
             :no-byte-compile t)      ; Omit this: only for my dev needs
  :config
  (setq usls-directory "~/Documents/notes/")
  (setq usls-known-categories '(economics philosophy politics))
  (setq usls-file-type-extension ".txt") ; {.txt,.org,.md}
  (setq usls-subdir-support nil)
  (setq usls-file-region-separator 'line) ; {'line,'heading, OR string of your choice}
  (setq usls-file-region-separator-heading-level 1)
  :bind (("C-c n d" . usls-dired)
         ("C-c n f" . usls-find-file)
         ("C-c n n" . usls-new-note)
         ("C-c n a" . usls-append-region-buffer-or-file)
         :map usls-mode-map
         ("C-c n i" . usls-id-insert)
         ("C-c n l" . usls-follow-link)))

Here is the usls.el code, which is also part of my dotfiles' repo (as with all my Elisp code):

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

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

;; This program is free software; you can redistribute it and/or 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:
;;
;; usls.el -- Unassuming Sidenotes of Little Significance
;; ------------------------------------------------------
;;
;; WARNING: This software is pre-alpha quality.  There will be bugs,
;; errors, cases where improvements could be made.  Please do not try it
;; with sensitive data that you have not safely backed up.  If you do use
;; it, I encourage you to send me feedback about anything you feel could be
;; improved or otherwise made different.
;;
;;
;; USLS or usls, which may be pronounced as a series of letters or just
;; "useless", is a set of utilities for fleshing out a note-taking workflow
;; that revolves around a strict file naming convention and relies
;; exclusively on core Emacs tools.
;;
;; usls.el is meant to be a simple tool for streamlining the process of
;; creating notes.  It does not provide utilities that already exist in the
;; Emacs milieu or standard Unix tools, such as dired and grep
;; respectively.  The focus is on the main points of interaction: (i)
;; creating notes, (ii) adding forward/backward references to other notes,
;; (iii) browsing such references for the current file, (iv) visiting the
;; `usls-directory', (v) finding a file that belongs to said directory.
;;
;;
;; The file name convention
;; ------------------------
;;
;; All files created with usls have a file name that follows this pattern:
;;
;;     DATE--CATEGORY--TITLE.EXTENSION
;;
;; All fields are separated by two hyphens.
;;
;; The DATE field represents the date in year-month-day followed by an
;; underscore and the current time in hour-minute-second notation.  The
;; presentation is compact, with only the underscore separating the two
;; components.  Like this: 20201108_091625.  The DATE serves as the unique
;; identifier of each note.
;;
;; CATEGORY is one or more entries separated by a hyphen.  Items that need
;; to be more than one word long must be written with an underscore.  So
;; "emacs_library" is one category, while "emacs-library" are two.
;;
;; The TITLE is the title of the note that gets extracted and hyphenated.
;; An entry about "This is a test" produces a "this-is-a-test" TITLE.
;;
;; Some complete examples:
;;
;; 20201007_124941--economics--plentiful-and-predictable-liquidity.txt
;; 20201007_104945--emacs-git--git-patch-formatting.txt
;; 20201105_113805--monetary_policy--asset-bubbles-macroprudential-policy.txt
;;
;; EXTENSION is one of ".txt", ".md", ".org" and is subject to a
;; user-facing customisation option.
;;
;;
;; Main points of entry
;; --------------------
;;
;; The aforementioned are handled automatically by the `usls-new-note'
;; command.  Invoking it brings up a minibuffer prompt for entering the
;; note's title.  Once that is done, it opens a second prompt, with
;; completion, for inputting the category.  The date is always derived
;; automatically.
;;
;; Completion for categories presents a list that combines two sources: (1)
;; a customisable list of "known categories", (2) a dynamic list of
;; inferred categories from existing file names.  The latter is possible
;; due to the assumption that the file name convention is fully respected.
;;
;; To create a new category, just enter text that does not match any of
;; the existing items.  To input multiple categories, separate them with
;; a comma or whatever matches your `crm-separator'.  If your completion
;; framework does not support such actions, then it should be considered
;; undesirable behaviour and reported upstream.
;;
;; `usls-new-note' accepts an optional prefix argument, with C-u.  Doing so
;; will start the command with a completion prompt for the subdirectory to
;; be used for the new note.  Subdirectories must already exist in the
;; filesystem, else an error message is displayed.
;;
;; A key feature of `usls-new-note' is the ability to extract the current
;; region, if active, and place it below the area where the point will be
;; in.  This is useful for quickly capturing some text you wish to comment
;; on and keep it in context.
;;
;; The note's text and the captured region are demarcated by a horizontal
;; rule, denoted by three space-separated asterisks for plain text and
;; markdown (* * *), or five consecutive hyphens for org (-----), plus
;; empty lines before and after the separator.  Though there also exists
;; `usls-file-region-separator' that can be configured to introduce a
;; heading instead of a dividing line.  The heading's text and level are
;; customisable, the latter via `usls-file-region-separator-heading-level'.
;;
;;
;; References to other notes
;; -------------------------
;;
;; In the interest of simplicity, usls.el does not maintain a database and
;; does not try to be too smart about linking between notes in the
;; `usls-directory'.  A "link" is, for our purposes, a plain text reference
;; to the unique identifier of a file (the DATE mentioned above).
;;
;; Inserting such text is made simple with the use of `usls-id-insert'.  It
;; will produce a minibuffer completion prompt with a list of all notes
;; except the current one.  Selecting an item will place its ID at point,
;; preceded by an uptick.  Like this:
;;
;;     A reference here.^20201108_141930
;;
;; An endnote is also included, now with two successive upticks (^^) which
;; points to the full file name of the referenced entry.  If support for
;; subdirectories is enabled (via `usls-subdir-support'), such endnotes
;; will include a complete filesystem path.  Otherwise they are assumed as
;; relative to the `usls-directory'.
;;
;; In the background, the referenced file will get a backward reference as
;; an endnote, now denoted by two adjacent at-signs (@@) followed by the
;; file name of the item where the reference was created.
;;
;; USLS makes sure to remove duplicate backward references whenever a new
;; one is created, but it does not try to update them in case things
;; change.  This is where general-purpose tools come in handy, such as the
;; ability to edit a grep buffer with the wgrep package.  Combine that with
;; your completion framework's directory-wide search or something like the
;; rg.el library to edit references in bulk (e.g. when renaming a file
;; name).
;;
;; To visit the reference at point, one can rely on Emacs' ability to
;; identify a file name contextually (among others).  Type C-x C-f or M-x
;; find-file and follow it up with M-n.  You will get the file-at-point
;; (i.e. the referenced entry) as the selected item.  Or call the command
;; `usls-follow-link' which uses minibuffer completion, with candidates
;; being the file references documented in the endnotes.
;;
;;
;; Accessing notes
;; ---------------
;;
;; Three commands allow you to quickly visit your notes with usls.el:
;;
;; 1. `usls-dired' will produce a dired buffer with the contents of the
;;    `usls-directory'.
;;
;; 2. `usls-find-file' uses minibuffer completion to run `find-file' on the
;;    selected entry, with options in the list being all the files in the
;;    `usls-directory'.
;;
;; 3. `usls-append-region-buffer-or-file' places the active region to the
;;    very end of a USLS buffer or file.  A "buffer" is, for our purposes,
;;    a live window holding a buffer that visits a file present in the
;;    `usls-directory'.  When multiple such windows are available, a
;;    minibuffer prompt asks for a choice between them, otherwise goes with
;;    the one present.  When no live windows of the sort exist, a
;;    minibuffer prompt will ask for a file.
;;
;;
;; Standard Emacs commands for extending usls.el
;; ---------------------------------------------
;;
;; As we do not have any intent to reproduce general purpose tools for
;; usls-specific cases, we encourage the usage of existing solutions within
;; the Emacs milieu.  Some ideas:
;;
;; + Use a completion framework, such as Icomplete (built-in), Ivy, Helm,
;;   Selectrum.  Packages such as the Orderless completion style can
;;   further improve your experience, depending on your choice and needs.
;;
;; + Learn how to run directory-wide searches and how to refactor entries
;;   in bulk.  A common workflow involves some grep command and the wgrep
;;   package.  Though you could also use `ibuffer-do-query-replace',
;;   `dired-do-find-regexp-and-replace', `multi-occur'.
;;
;; + If you are running Emacs 28 (current development target) make sure you
;;   give a fair chance to project.el, as it contains lots of commands that
;;   can operate on a per-project basis (find a file, grep, query and
;;   replace...).  Just make the `usls-directory' a "project" and the rest
;;   follows from there.  To do so, either run any of the commands listed
;;   in 'C-x p C-h' while inside the `usls-directory' or choose that
;;   directory from the 'C-x p p' prompt.
;;
;; + Benefit from dired's numerous capabilities (which can be combined).
;;
;;   * For example, the key sequence '% m' (dired-mark-files-regexp) lets
;;     you mark files based on a regular expression or just a string.  Say
;;     you wish to only see notes about "politics".  Do '% m politics',
;;     then toggle the mark with 't' and complete the process with 'k'.
;;     What you just did is to remove from view all entries that do no
;;     match the pattern you searched for.  Bring everything back to the
;;     standard view with 'g'.
;;
;;   * Another neat feature of dired is `dired-goto-file' which is bound to
;;     'j' by default.  It lets you jump to the line of a given file using
;;     minibuffer completion.  So if your completion framework supports
;;     fuzzy search or out-of-order matching of regular expression groups,
;;     you can interactively find virtually any file with only a few key
;;     strokes.
;;
;;   * To work with dired subdirectories, you can produce a recursive list
;;     of the current buffer.  Place the point over the file system path
;;     that is at the top of the dired buffer (it shows the directory you
;;     are in).  Then use 'C-u l' to modify the 'ls' flags that are active.
;;     You want to pass the '-R' switch to those already in effect.  Doing
;;     so will populate the buffer with listings from the current directory
;;     and all its subdirectories.
;;
;;     (Note: 'C-u l' is for any directory path at point.  If you only ever
;;     want for the directory shown by dired, use 'C-u s' instead, which is
;;     not sensitive to the location of the cursor.)
;;
;; The principle is to learn how to use Emacs' existing capabilities or
;; extensions to your advantage---not just for usls but for your day-to-day
;; operations.
;;
;; To that end, this video tutorial offers a primer on regexp:
;; <https://protesilaos.com/codelog/2020-01-23-emacs-regexp-primer/>.
;; Other videos in that list may also be of help.
;;
;;
;; General principles of usls.el
;; -----------------------------
;;
;; This blog post from 2020-10-08 describes the core ideas of usls:
;; <https://protesilaos.com/codelog/2020-10-08-intro-usls-emacs-notes/>.
;; Some references are out-of-date, since the library is expanded to
;; support Org and Markdown file types, while it can be configured to
;; access subdirectories inside the `usls-directory'.
;;
;; The gist is that usls should keep your notes as close to plain text as
;; possible.  You should always be able to can access them from outside
;; Emacs, such as a Unix shell prompt operated via a TTY.
;;
;;
;; Free software license
;; ---------------------
;;
;; usls.el is distributed under the terms of the GNU General Public
;; License, Version 3 or, at your convenience, a later version.
;;
;; Refer to the COPYING document, distributed as part of this project, for
;; the legal text.

;;; Code:

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

;;; User-facing options

(defgroup usls ()
  "Simple tool for plain text notes."
  :group 'files
  :prefix "usls-")

(defcustom usls-directory "~/Documents/notes/"
  "Directory for storing personal notes."
  :group 'usls
  :type 'directory)

(defcustom usls-known-categories '(economics philosophy politics)
  "List of predefined categories for `usls-new-note'.

The implicit assumption is that a category is a single word.  If
you need a category to be multiple words long, use underscores to
separate them.  Do not use hyphens, as those are assumed to
demarcate distinct categories, per `usls--inferred-categories'.

Also see `usls-categories' for a dynamically generated list that
gets combined with this one in relevant prompts."
  :group 'usls
  :type 'list)

(defcustom usls-subdir-support nil
  "Enable support for subdirectories in `usls-directory'.

The default workflow of USLS is to maintain a flat directory
where all the notes are stored in.  This allows us to omit the
common filesystem path and only show file names.

When set to non-nil, the usls workflow can handle subdirectories
at the expense of making all file names more verbose, as it needs
to include the complete path.

NOTE: such subdirectories must be created manually to make sure
that no destructive filesystem operations are performed by
accident."
  :group 'usls
  :type 'boolean)

(defcustom usls-file-type-extension ".txt"
  "File type extension for new USLS notes.

Available options cover plain text (.txt), Markdown (.md), and
Org (.org) formats."
  :group 'usls
  :type '(choice
          (const :tag "Plain text format" ".txt")
          (const :tag "Markdown format" ".md")
          (const :tag "Org format" ".org")))

(defcustom usls-file-region-separator 'line
  "Separator for `usls-new-note' delimiting the captured region.

The default value of 'line' produces a horizontal rule depending
on the `usls-file-type-extension'.

* For plain text and Markdown this results in the following
  string (without the quotes): '\\n\\n* * *\\n\\n'.  It means to put
  two new lines before and two after the three space-separated
  asterisks.  In practice, that means an empty line before and
  after.  This notation is a common way to denote a horizontal
  rule or page/section break and is a standard in Markdown.

* For Org files it produces five consecutive hyphens with
  newlines before and after ('\\n\\n-----\\n\\n').  This is the valid
  syntax for a horizontal rule in `org-mode'.

Option 'heading' produces a heading that is formatted according
to `usls-file-type-extension'.  Its text is 'Reference':

* For plain text, the formatting of the heading involves a series
  of hyphens below the heading's text, followed by an empty line.
  The length of the hyphens is equal to that of the heading's
  text.

* For Markdown and Org the heading is formatted per the
  respective major mode's syntax, plus an empty line before and
  after.

It is also possible to provide a string of your own.  This should
contain just the text that you wish to turn into a heading.  For
example, you want to use the word 'Captured region' instead of
'Reference', so provide only that.  Your input will be processed
according to `usls-file-type-extension' to offer the correct
heading format.  The result will mimic that of the aforementioned
options.

The level of the heading is controlled by the customisation
option `usls-file-region-separator-heading-level' and defaults to
1 (one # for Markdown or one * for Org)."
  :group 'usls
  :type '(choice
          (const :tag "Line with surrounding space (default)" line)
          (const :tag "A 'Reference' heading" heading)
          (string :tag "A heading with text of your choice")))

(defcustom usls-file-region-separator-heading-level 1
  "Heading level for `usls-file-region-separator'.
Has effect when `usls-file-type-extension' is either that for
Markdown or Org types."
  :group 'usls
  :type 'integer)

;;; Main variables

(defconst usls-id "%Y%m%d_%H%M%S"
  "Format of ID prefix of a note's filename.")

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

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

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

(defvar usls--file-link-regexp "^\\(@@\\|\\^^\\) \\(.*\\.\\)\\(txt\\|md\\|org\\)"
  "Regexp for file links.")

;;;; Input history lists

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

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

(defvar usls--file-history '()
  "Used internally by `usls-find-file' to record file names.")

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

(defvar usls--subdirectory-history '()
  "Used internally by `usls-new-note' to record subdirectories.")

;;; Basic utilities

(defun usls--completion-table (category candidates)
  "Pass appropriate metadata CATEGORY to completion CANDIDATES."
  (lambda (string pred action)
    (if (eq action 'metadata)
        `(metadata (category . ,category))
      (complete-with-action action candidates string pred))))

;;;; File name helpers

(defun usls--directory ()
  "Valid name format for `usls-directory'."
  (file-name-as-directory usls-directory))

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

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

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

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

;;;; Files in directory

(defun usls--directory-files-flat ()
  "List `usls-directory' files, assuming flat directory."
  (let ((dotless directory-files-no-dot-files-regexp))
    (cl-remove-if
     (lambda (x)
       ;; TODO: generalise this for all VC backends?  Which ones?
       (string-match-p "\\.git" x))
     (directory-files (usls--directory) nil dotless t))))

(defun usls--directory-files-recursive ()
  "List `usls-directory' files, assuming directory tree."
    (cl-remove-if
     (lambda (x)
       ;; TODO: generalise this for all VC backends?  Which ones?
       (string-match-p "\\.git" x))
     (directory-files-recursively (usls--directory) ".*" nil t)))

(defun usls--directory-files ()
  "List directory files."
  (let ((path (usls--directory)))
    (unless (file-directory-p path)
      (make-directory path t))
    (if usls-subdir-support
        (usls--directory-files-recursive)
      (usls--directory-files-flat))))

(defun usls--directory-subdirs ()
  "Return list of subdirectories in `usls-directory'."
  (cl-remove-if-not
   (lambda (x)
     (file-directory-p x))
   (directory-files-recursively (usls--directory) ".*" t t)))

;; TODO: generalise this for all VC backends?  Which ones?
(defun usls--directory-subdirs-no-git ()
  "Remove .git directories from `usls--directory-subdirs'."
  (cl-remove-if
   (lambda (x)
     (string-match-p "\\.git" x))
   (usls--directory-subdirs)))

(defun usls--directory-subdirs-prompt ()
  "Handle user input on choice of subdirectory."
  (let* ((subdirs
          (if (eq (usls--directory-subdirs-no-git) nil)
              (user-error "No subdirs in `%s'; create them manually"
                          (usls--directory))
            (usls--directory-subdirs-no-git)))
         (choice (completing-read "Subdirectory of new note: " subdirs
                                  nil t nil 'usls--subdirectory-history))
         (subdir (file-truename choice)))
    (add-to-history 'usls--subdirectory-history choice)
    subdir))

;;;; Categories

(defun usls--categories-in-files ()
  "Produce list of categories in `usls--directory-files'."
  (cl-remove-if nil
   (mapcar (lambda (x)
             (usls--extract usls-category-regexp x))
           (usls--directory-files))))

(defun usls--inferred-categories ()
  "Extract categories from `usls--directory-files'."
  (let ((sequence (usls--categories-in-files)))
    (mapcan (lambda (s)
              (split-string s "-" t))
            sequence)))

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

(defun usls--categories-prompt ()
  "Prompt for one or more categories (comma/space separated)."
  (let* ((categories (usls-categories))
         (choice (completing-read-multiple "File category: " categories
                                           nil nil nil 'usls--category-history)))
    (if (= (length choice) 1)
        (car choice)
      choice)))

(defun usls--categories-hyphenate (categories)
  "Format CATEGORIES output of `usls--categories-prompt'."
  (if (and (> (length categories) 1)
           (not (stringp categories)))
      (mapconcat #'downcase categories "-")
    categories))

(defun usls--categories-capitalize (categories)
  "`capitalize' CATEGORIES output of `usls--categories-prompt'."
  (if (and (> (length categories) 1)
           (not (stringp categories)))
      (mapconcat #'capitalize categories ", ")
    (capitalize categories)))

(defun usls--categories-add-to-history (categories)
  "Append CATEGORIES to `usls--category-history'."
  (if (and (> (length categories) 1)
           (not (stringp categories)))
      (dolist (x categories)
        (add-to-history 'usls--category-history x))
    (add-to-history 'usls--category-history categories)))

;;; Templates

(defun usls--file-meta-header (title date categories filename id)
  "Front matter template based on `usls-file-type-extension'.

This helper function is meant to integrate with `usls-new-note'.
As such TITLE, DATE, CATEGORIES, FILENAME, ID are all retrieved
from there."
  (let ((cat (usls--categories-capitalize `,categories)))
    (pcase usls-file-type-extension
      ;; TODO: make those templates somewhat customisable.  We need to
      ;; determine what should be parametrised.
      (".md" `(concat "---" "\n"
                      "title: " ,title "\n"
                      "date: " ,date "\n"
                      "category: " ,cat "\n"
                      "orig_name: " ,filename "\n"
                      "orig_id: " ,id "\n"
                      "---" "\n\n"))
      (".org" `(concat "#+title: " ,title "\n"
                       "#+date: " ,date "\n"
                       "#+category: " ,cat "\n"
                       "#+orig_name: " ,filename "\n"
                       "#+orig_id: " ,id "\n\n"))
      (_ `(concat "title: " ,title "\n"
                  "date: " ,date "\n"
                  "category: " ,cat "\n"
                  "orig_name: " ,filename "\n"
                  "orig_id: " ,id "\n"
                  (make-string 24 ?-) "\n\n")))))

(defun usls--file-region-separator-heading-level (mark str)
  "Format MARK and STR for `usls--file-region-separator-str'.
MARK must be a single character string.  For multiple character
strings only the first one is used."
  (let ((num usls-file-region-separator-heading-level)
        (char (when (stringp mark)
                (string-to-char (substring mark 0 1)))))
    (format "\n\n%s %s\n\n" (make-string num char) str)))

(defun usls--file-region-separator-str ()
  "Produce region delimiter string for use in `usls-new-note'."
  (let* ((str (format "%s" usls-file-region-separator))
         (num (length str)))
    (pcase usls-file-region-separator
      ('line (pcase usls-file-type-extension
               (".org" (format "\n\n%s\n\n" (make-string 5 ?-)))
               (_ "\n\n* * *\n\n")))
      ('heading (pcase usls-file-type-extension
                  (".md" (usls--file-region-separator-heading-level "#" "Reference"))
                  (".org" (usls--file-region-separator-heading-level "*" "Reference"))
                  (_ (format "\n\nReference\n%s\n\n" (make-string 9 ?-)))))
      (_ (pcase usls-file-type-extension
           (".md" (usls--file-region-separator-heading-level "#" str))
           (".org" (usls--file-region-separator-heading-level "*" str))
           (_ (format "\n\n%s\n%s\n\n" str (make-string num ?-))))))))

;; This just silences the compiler for the subsequent function
(defvar eww-data)

;; TODO: get some link for gnus, mu4e?  What else?
(defun usls--file-region-source ()
  "Capture path to file or URL for `usls--file-region'."
  (cond
   ((derived-mode-p 'eww-mode)
    (concat (plist-get eww-data :url) "\n\n"))
   ((buffer-file-name)
    (concat (buffer-file-name) "\n\n"))
   (t
    "")))

(defun usls--file-region-separator (region)
  "`usls--file-region-separator-str' and `usls-new-note' REGION."
  `(concat
    (usls--file-region-separator-str)
    (usls--file-region-source)
    ,region))

(defun usls--file-region ()
  "Capture active region for use in `usls-new-note'."
  (if (use-region-p)
      (eval (usls--file-region-separator
             (buffer-substring-no-properties
              (region-beginning)
              (region-end))))
    ""))

(defun usls--file-region-append ()
  "Capture active region for use in `usls-append-region-buffer-or-file'."
  (if (use-region-p)
      (eval (buffer-substring-no-properties
             (region-beginning)
             (region-end)))
    ""))

;;; Commands and their helper functions

;;;; New note

;;;###autoload
(defun usls-new-note (&optional arg)
  "Create new note with the appropriate metadata and file name.
If the region is active, append it to the newly created file.

This command first prompts for a file title and then for a
category.  The latter supports completion.  To input multiple
categories, separate them with a space or a comma.

With prefix key (\\[universal-argument]) as optional ARG also
prompt for a subdirectory of `usls-directory' to place the new
note in."
  (interactive "P")
  (let* ((subdir (when arg (usls--directory-subdirs-prompt)))
         (title (read-string "File title: " nil 'usls--title-history))
         (categories (usls--categories-prompt))
         (slug (usls--sluggify title))
         (path (file-name-as-directory (or subdir usls-directory)))
         (id (format-time-string usls-id))
         (filename
          (format "%s%s--%s--%s%s"
                  path
                  id
                  (usls--categories-hyphenate categories)
                  slug
                  usls-file-type-extension))
         (date (format-time-string "%F"))
         (region (usls--file-region)))
    (with-current-buffer (find-file filename)
      (insert (eval (usls--file-meta-header title date categories filename id)))
      (save-excursion (insert region)))
    (add-to-history 'usls--title-history title)
    (usls--categories-add-to-history categories)))

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

;;;; Insert reference

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

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

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


;;;; Follow links

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

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

;;;; Find file

(defun usls--file-name (file)
  "Return properly formatted name of FILE."
  (if usls-subdir-support
     (file-truename file)
    (file-truename (concat (usls--directory) file))))

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

;;;; Append to file

;; REVIEW: Maybe all those filtered lists can be simplified into maybe
;; one or two.  This feels needlessly complex.

(defun usls--window-buffer-list ()
  "Return list of windows."
  (mapcar (lambda (x)
            (window-buffer x))
          (window-list)))

(defun usls--window-buffer-file-names-list ()
  "Return file names in `usls--window-buffer-list'."
  (cl-remove-if nil
   (mapcar (lambda (x)
             (buffer-file-name x))
           (usls--window-buffer-list))))

(defun usls--window-usls-file-buffers ()
  "Return USLS files in `usls--window-buffer-file-names-list'."
  (let ((files (usls--directory-files-recursive))
        (buf-files (mapcar #'abbreviate-file-name (usls--window-buffer-file-names-list))))
    (cl-remove-if nil
     (mapcar (lambda (x)
               (when (member x files)
                 x))
             buf-files))))

(defun usls--window-usls-buffers ()
  "Return buffer names from `usls--window-usls-file-buffers'."
  (mapcar (lambda (x)
            (get-file-buffer x))
          (usls--window-usls-file-buffers)))

(defun usls--window-buffers-live ()
  "Return live windows matching `usls--window-usls-buffers'."
  (cl-remove-if-not (lambda (x)
                      (window-live-p x))
                    (mapcar (lambda (y)
                              (get-buffer-window y))
                            (usls--window-usls-buffers))))

(defun usls--window-buffers ()
  "Return buffer names in `usls--window-buffers-live'."
  (mapcar (lambda (x)
            (window-buffer x))
          (usls--window-buffers-live)))

(defun usls--window-single-buffer-or-prompt ()
  "Return buffer name if one, else prompt with completion."
  (let* ((buffers
          (delete-dups
           (mapcar (lambda (x)
                     (format "%s" x))
                   (usls--window-buffers))))
         (bufs (usls--completion-table 'buffer buffers))
         (buf (if (> (length buffers) 1)
                  (completing-read "Pick buffer: "
                                   bufs nil t)
                (if (listp buffers) (car buffers) buffers))))
    (unless (eq buf nil)
      (get-buffer-window buf))))

(defun usls--window-buffer-or-file ()
  "Return window with a USLS buffer or prompt for a file."
  (let ((files (usls--directory-files)))
    (or (usls--window-single-buffer-or-prompt)
        (completing-read "Visit file: " files nil t nil 'usls--file-history))))

(defun usls--append-region (buf region arg)
  "Routines to append active region.
All of BUF, REGION, ARG are intended to be passed by another
function, such as with `usls-append-region-buffer-or-file'."
  (let ((window (get-buffer-window buf)))
    (with-current-buffer `,buf
      (goto-char (if (not (eq arg nil)) (point-max) (window-point window)))
      (set-mark (point))
      (insert `,region)
      ;; REVIEW: is this the correct way to go to the last mark?  Are we
      ;; polluting the mark-ring?
      (goto-char (car mark-ring)))))

;;;###autoload
(defun usls-append-region-buffer-or-file (&optional arg)
  "Append active region to buffer or file.

To 'append' is to insert at point.  To insert at the end of text
instead, pass a \\[universal-argument] prefix argument ARG.

If there exist one or more windows whose buffers visit a file
found in `usls-directory', then they are used as targets for
appending the active region.  When multiple windows are
available, a minibuffer prompt with completion is provided to
select one among them.

When no such windows are live, the minibuffer prompt asks for a
file to visit.

The appended region is not preceded by a delimiter, as is the
case with `usls-new-note'."
  (interactive "P")
  (let* ((object (usls--window-buffer-or-file))
         (buf (when (windowp object) (window-buffer object)))
         (region (usls--file-region-append))
         (append (if arg t nil)))
    (if (bufferp buf)
        (usls--append-region buf region append)
      (usls--append-region (find-file (usls--file-name object)) region append)
      ;; Only add to history when we are dealing with a file
      (add-to-history 'usls--file-history object))))

;;;; Dired

;;;###autoload
(defun usls-dired (&optional arg)
  "Switch to `usls-directory' using `dired'.
With optional \\[universal-argument] prefix ARG prompt for a usls
subdirectory to switch to.  If none is available, the main
directory will be directly displayed instead."
  (interactive "P")
  (let ((path usls-directory)
        (subdirs (usls--directory-subdirs-no-git)))
    (unless (file-directory-p path)
      (user-error "`usls-directory' not found at %s" usls-directory))
    (if (and arg subdirs)
        (dired (usls--directory-subdirs-prompt))
      (dired path))))

;;; User-facing setup

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

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

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

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

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

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

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

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

(defface usls-header-data-date
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#2544bb")
    (((class color) (min-colors 88) (background dark))
     :foreground "#79a8ff")
    (t :inherit font-lock-string-face))
  "Face for header date entry.")

(defface usls-header-data-category
  '((default :inherit bold)
    (((class color) (min-colors 88) (background light))
     :foreground "#1f0f6f")
    (((class color) (min-colors 88) (background dark))
     :foreground "#92baff")
    (t :inherit font-lock-builtin-face))
  "Face for header category entry.")

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

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

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

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

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

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

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

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

;; TODO: re-use regular expressions as is already done for
;; `usls-file-regexp'.
(defconst usls-font-lock-keywords
  `(("\\(title:\\) \\(.*\\)"
     (1 'usls-header-data-key)
     (2 'usls-header-data-title))
    ("\\(date:\\) \\(.*\\)"
     (1 'usls-header-data-key)
     (2 'usls-header-data-date))
    ("\\(category:\\) \\(.*\\)"
     (1 'usls-header-data-key)
     (2 'usls-header-data-category))
    ("\\(orig_\\(name\\|id\\):\\) \\(.*\\)"
     (1 'usls-header-data-key)
     (2 'usls-header-data-key)
     (3 'usls-header-data-secondary))
    ("^\\(-\\{24\\}\\|[*\s]\\{5\\}\\)$"
     (1 'usls-section-delimiter))
    ("\\(\\^\\)\\([0-9_]\\{15\\}\\)"
     (1 'escape-glyph)
     (2 'font-lock-variable-name-face))
    (,usls--file-link-regexp
     (1 'escape-glyph)
     (2 'font-lock-constant-face)
     (3 'font-lock-constant-face))
    (,usls-file-regexp
     (1 'usls-dired-field-date)
     (2 'usls-dired-field-category)
     (3 'usls-dired-field-name)
     (4 'usls-dired-field-delimiter)))
  "Rules to apply font-lock highlighting with `usls--fontify'.")

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

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

(provide 'usls)

;;; usls.el ends here

5.3 TMR Must Recur (just my simplistic timer)

Sometimes I need to set off a timer with a notification. I used to rely on a homegrown shell script for such a task, but where is the fun in that?

tmr.el satisfies my curiosity to experiment with Elisp, while it also provides the tmr function that I only ever use from Eshell or through eval-expression (M-:).

tmr works by accepting a number, which it interprets as a count of minutes. It can also read strings, such as "1h" for 1 hour and "30s" for 30 seconds. Once the time elapses, it produces a system notification as well as a message in the echo area with information about the start and end times (review the echo area log with C-h e). Then it plays back some otherwise annoying sound, just to be sure that you feel an urge to quit whatever caught your attention in the meantime.

(use-package tmr
  :straight (:type built-in)
  :config
  (setq tmr-sound-file
        "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga"))

This is its code (from my dotfiles' repo):

;;; tmr.el --- TMR Must Recur -*- lexical-binding: t -*-

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; TMR Must Recur.  Else a super simple timer for my Emacs setup:
;; https://protesilaos.com/dotemacs.
;;
;; THIS IS EXPERIMENTAL and I still plan to iterate on it.

;;; Code:

(require 'notifications)

(defgroup tmr ()
  "TMR Must Recur (super simple timer for my private use)."
  :group 'data)

(defcustom tmr-sound-file
  "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga"
  "Path to sound file used by `tmr--play-sound'."
  :type 'string
  :group 'tmr)

(defun tmr--unit (time)
  "Determine common time unit for TIME."
  (if (numberp time) ; TODO: how to recognise a positive/negative number?
      (* time 60)
    (let* ((unit (substring time -1))
           (str (substring time 0 -1))
           (num (string-to-number str)))
      (pcase unit
        ("s" num)
        ("h" (* num 60 60))
        ;; This is not needed, of course, but we should not miss a good
        ;; chance to make some fun of ourselves.
        ("w" (user-error "TMR Made Ridiculous; use minutes, hours, seconds"))
        (_ (* num 60))))))

(defun tmr--play-sound ()
  "Play `tmr-sound-file' using the 'ffplay' executable (ffmpeg)."
  (let ((sound tmr-sound-file))
    (when (and (file-exists-p sound)
               (executable-find "ffplay"))
      (call-process-shell-command
       (format "ffplay -nodisp -autoexit %s >/dev/null 2>&1" sound) nil 0))))

(defun tmr--notify-send (start)
  "Send system notification for timer with START time."
  (let ((end (format-time-string "%R")))
    (notifications-notify
     :title "TMR Must Recur"
     :body (format "Time is up!\nStarted: %s\nEnded: %s" start end)
     :app-name "GNU Emacs")
    (message
     "TMR %s %s ; %s %s"
     (propertize "Start:" 'face 'success) start
     (propertize "End:" 'face 'warning) end)
    (tmr--play-sound)))

;;;###autoload
(defun tmr (time)
  "Set timer to TIME duration and notify after it elapses.

When TIME is a number, it is interpreted as a count of minutes.
Otherwise TIME must be a string that consists of a number and a
special final character denoting a unit of time: 'h' for 'hours',
's' for 'seconds'.

This command also plays back `tmr-sound-file'."
  (let ((start (format-time-string "%R"))
        (unit (tmr--unit time)))
    (run-with-timer
     unit nil
     'tmr--notify-send start)))

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

5.4 Version control tools

5.4.1 Diff-mode (and prot-diff.el extensions)

This covers the standard diff-mode.el, which I use quite often, such as while interfacing with the built-in Version Control framework (see the section on VC), or while browsing various code-related newsgroups through Gnus (refer to the email settings).

Overview of my preferences for how diffs should look:

  • Always start the buffer in a read-only state. A typo will mess things up when trying to apply a patch.
  • After applying a diff hunk (diff-apply-hunk with C-c C-a) move on to the next one.
  • Update hunk headers automatically following an edit to the diff.
  • Do not show word-wise ("refined") changes upon activation. I prefer to do so manually. All such highlights are removed if you generate the buffer again (with g as expected) and the default is to not show word-wise changes.
  • Do not prettify headers. I like the standard "patch" looks. It also makes it easier to copy the diff elsewhere.

Now some notes on my prot-diff.el extensions, combined with a description of the basics of diff-mode (as always you can get documentation about the current buffer's major mode with C-h m—read How do you learn Emacs? in the FAQ section appended to this document):

  • prot-diff-buffer-dwim will produce a diff that compares the current buffer to the last saved state of the underlying file. If the buffer has no unsaved edits, the command will produce a diff that compares the file to its last registered version-controlled state. Calling the command with an optional prefix argument (C-u) will enable word-wise highlighting across the diff.
  • prot-diff-refine-dwim is how I manually control word-wise diff highlights. By default, the command will turn on refined changes throughout the buffer. If called with an optional prefix argument, it will operate only on the diff hunk at point. If the region is active, it will instead apply fontification to the diff hunks encompassed by the region. And if word-wise highlights are already present, the command will remove everything and leave point back where it was.
  • prot-diff-narrow-dwim narrows to the diff hunk at point. If narrowing is already present, it widens the buffer. When invoked with an optional prefix argument, it narrows to the current file.
  • C-c C-c or M-o takes you to the point of the changes in the source file. If you run this of the diff hunk's heading, you go to the beginning of the context. But if you place the point somewhere inside of the diff's added changes or context, you will visit that exact position in the original file (does not work for removed text because technically it does not exist).
  • When working with patches to source code, which are distributed e.g. through email, you can apply the current hunk with C-c C-a or test for compatibility with C-c C-t. This is a nice way to easily merge contributions from others, without having to go through the workflow of some proprietary Git/Version-Control forge.
  • With M-n and M-p you move between hunks. With M-} and M-{ or M-N, M-P do the same between files.

The prot-diff-* commands are part of my prot-diff.el library, reproduced in its entirety after this set of package declarations.

Pro tip: enable outline-minor-mode to make diff sections foldable. Check Outline mode, outline minor mode, and extras (prot-outline.el).

Also read these sections:

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

(use-package diff-mode
  :config
  (setq diff-default-read-only t)
  (setq diff-advance-after-apply-hunk t)
  (setq diff-update-on-the-fly t)
  ;; The following are from Emacs 27.1
  (setq diff-refine nil)                ; I do it on demand
  (setq diff-font-lock-prettify nil)    ; better for patches
  ;; The following is further controlled by
  ;; `prot-diff-modus-themes-diffs'
  (setq diff-font-lock-syntax 'hunk-also))

(use-package prot-diff
  :straight (:type built-in)
  ;; `prot-diff-buffer-dwim' replaces the default for `vc-diff' (which I
  ;; bind to another key---see VC section).
  :hook (modus-themes-after-load-theme-hook . prot-diff-modus-themes-diffs)
  :bind (("C-x v =" . prot-diff-buffer-dwim)
         :map diff-mode-map
         ("C-c C-b" . prot-diff-refine-dwim) ; replace `diff-refine-hunk'
         ("C-c C-n" . prot-diff-narrow-dwim)))

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

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

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; This covers my diff-mode.el extensions, for use in my Emacs setup:
;; https://protesilaos.com/dotemacs.

;;; Code:

(require 'diff-mode)

;;;###autoload
(defun prot-diff-buffer-dwim (&optional arg)
  "Diff buffer with its file's last saved state, or run `vc-diff'.
With optional prefix ARG (\\[universal-argument]) enable
highlighting of word-wise changes (local to the current buffer)."
  (interactive "P")
  (let ((buf))
    (if (buffer-modified-p)
        (progn
          (diff-buffer-with-file (current-buffer))
          (setq buf "*Diff*"))
      (vc-diff)
      (setq buf "*vc-diff*"))
    (when arg
      (with-current-buffer (get-buffer buf)
        (unless diff-refine
          (setq-local diff-refine 'font-lock))))))

;;;###autoload
(defun prot-diff-refine-dwim (&optional arg)
  "Produce word-wise, 'refined' diffs in `diff-mode' buffer.

Operate on the entire buffer by default.  With optional prefix
ARG (\\[universal-argument]), act on the hunk at point.  When the
region is active, fontify the diff hunks encompassed by it.

If any such fontification is already present, revert the buffer
and place point back where it was."
  (interactive "P")
  (let ((position (point))
        (beg (or (when (mark) (region-beginning)) (point-min)))
        (end (or (when (mark) (region-end)) (point-max))))
    (when (derived-mode-p 'diff-mode)
      (cond
       ((and arg (not (region-active-p)))
        (diff-refine-hunk)
        (setq-local diff-refine 'font-lock))
       ((eq (buffer-local-value 'diff-refine (current-buffer)) 'font-lock)
        (revert-buffer)
        (goto-char position)
        (recenter))
       (t
        (setq-local diff-refine 'font-lock)
        (when (region-active-p) (deactivate-mark))
        (font-lock-flush beg end)
        (goto-char position))))))

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

(defvar modus-themes-diffs)

;;;###autoload
(defun prot-diff-modus-themes-diffs ()
  "Configure `diff-font-lock-syntax' for accessibility.

A non-nil value for that variable will apply fontification to the
text while also trying to add the familiar diff styles.  This can
easily result in inaccessible colour combinations.

My Modus themes, which are designed for the highest accessibility
standard in legibility, provide an option that can work well with
such non-nil values.  Otherwise `diff-font-lock-syntax' should be
set to nil.

Run this function at the post theme load phase, such as with the
hook `modus-themes-after-load-theme-hook'."
  (if (eq modus-themes-diffs 'bg-only)
      (setq diff-font-lock-syntax 'hunk-also)
    (setq diff-font-lock-syntax nil)))

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

5.4.2 Version control framework (vc.el and prot-vc.el)

VC consists of set of libraries that provide the means for working with several version control systems, else "backends". It is built into Emacs. Compared with magit (see section on Magit configs), vc offers a more abstract, buffer-oriented paradigm that does a fine job at covering all basic versioning needs. It however never stands as Magit's peer when it comes to the sheer coverage of Git features.

To my mind, VC and Magit can be used as part of the same setup. Employ the former for common tasks such as viewing diffs and logs, committing changes in bulk, pushing and pulling from a remote. And let Magit handle the more involved and specialised cases of staging a partial diff, rebasing commits interactively, writing a commit fixup, and so on.

Also refer to the section on Diff-mode (and prot-diff.el extensions). And watch my Introduction to the Emacs Version Control framework (2020-03-30).

Here is an overview of the keys I define, with only a few of them being left to their default values (note that prot-diff-buffer-dwim is part of the prot-diff.el that I linked to above):

Command C-x v prefix Mnemonic
vc-annotate a  
vc-update F Fetch and merge
vc-push P  
vc-log-incoming f Fetch only
vc-log-outgoing O  
vc-create-tag t  
vc-retrieve-tag b Branch/tag switch
vc-diff d Diff current file
vc-root-diff D Diff project
prot-diff-buffer-dwim =  
prot-vc-project-or-dir p Project status
prot-vc-custom-log SPC  
prot-vc-patch-dwim c Create patch

My prot-vc.el library (reproduced after the package declarations) defines a few commands that extend VC to suit my needs. In short:

  • prot-vc-patch-dwim produce a properly formatted patch for a given commit. When browsing a log-view buffer, the commit is the one around point, otherwise the commit is selected with completion from a log of recent entries (the length of the log is controlled by the variable prot-vc-log-limit). Prompting for a commit can also be done by calling this command with a universal prefix argument (C-u). The patch is saved as a file in a directory that can be selected with another completion prompt: default candidates are stored in the list prot-vc-patch-output-dirs.
  • prot-vc-custom-log prints a log of commits that matches a custom file set. This is of great value when you need to inspect the history of only some files rather than that of the entire repository. What files to choose is determined in two ways: (1) the file-at-point in Dired buffers, or all marked files, and (2) files in the current directory selected with completion. The latter is invoked by passing a double prefix argument (C-u C-u). While a single prefix arg (C-u), starts with a prompts for a number to limit the log to, otherwise it falls back to the number in prot-vc-log-limit.
  • prot-vc-project-or-dir produces a vc-dir buffer for the current project (also see Projects (project.el and prot-project.el)). With a C-u prefix argument the command limits the matches to the present directory.
  • prot-vc-log-kill-hash appends to the kill-ring the hash of the commit around point. It is meant to be used in log-view buffers. Simple and effective.
  • prot-vc-git-setup-mode is a minor mode that tweaks the log view of vc-git so that when we expand commits it uses a different format than the default. Basically this is an elaborate workaround to make it show add/remove stats and affected file names. It is set up as a minor mode to make it easy to disable.
(use-package vc
  :demand
  :config
  (use-package vc-annotate)
  (use-package vc-dir)
  (use-package vc-git)
  (use-package add-log)
  (use-package log-view)

  (setq vc-find-revision-no-save t)
  (setq vc-annotate-display-mode 'scale) ; scale to oldest
  ;; I use a different account for git commits
  (setq add-log-mailing-address "info@protesilaos.com")
  (setq add-log-keep-changes-together t)
  (setq vc-git-diff-switches '("--patch-with-stat" "--histogram"))
  (setq vc-git-print-log-follow t)
  (setq vc-git-revision-complete-only-branches nil) ; Emacs 28
  (setq vc-git-root-log-format
        '("%d%h %ad %an: %s"
          ;; The first shy group matches the characters drawn by --graph.
          ;; We use numbered groups because `log-view-message-re' wants the
          ;; revision number to be group 1.
          "^\\(?:[*/\\| ]+ \\)?\
\\(?2: ([^)]+)\\)?\\(?1:[0-9a-z]+\\) \
\\(?4:[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\) \
\\(?3:.*?\\):"
          ((1 'log-view-message)
           (2 'change-log-list nil lax)
           (3 'change-log-name)
           (4 'change-log-date))))
  :bind (("C-x v a" . vc-annotate) ; `vc-update-change-log' is not in git
         ("C-x v b" . vc-retrieve-tag)  ; "branch" switch
         ("C-x v t" . vc-create-tag)
         ("C-x v f" . vc-log-incoming)  ; the actual git fetch
         ("C-x v o" . vc-log-outgoing)
         ("C-x v F" . vc-update)        ; "F" because "P" is push
         ("C-x v d" . vc-diff)
         :map vc-dir-mode-map
         ("a" . vc-annotate)
         ("b" . vc-retrieve-tag)
         ("t" . vc-create-tag)
         ("o" . vc-log-outgoing)
         ("f" . vc-log-incoming) ; replaces `vc-dir-find-file' (use RET)
         ("F" . vc-update)       ; symmetric with P: `vc-push'
         ("d" . vc-diff)         ; parallel to D: `vc-root-diff'
         ("k" . vc-dir-clean-files)
         :map vc-annotate-mode-map
         ("M-q" . vc-annotate-toggle-annotation-visibility)
         :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)))

(use-package prot-vc
  :straight (:type built-in)
  :after vc
  :init
  (setq prot-vc-log-limit 20)
  (setq prot-vc-shell-output "*prot-vc-shell-output*")
  (setq prot-vc-patch-output-dirs (list "~/" "~/Desktop/"))

  (prot-vc-git-setup-mode 1)
  :bind (("C-x v p" . prot-vc-project-or-dir)
         ("C-x v SPC" . prot-vc-custom-log)
         ("C-x v c" . prot-vc-patch-dwim)
         :map log-view-mode-map
         ("c" . prot-vc-patch-dwim)
         ("w" . prot-vc-log-kill-hash)))

And here is prot-vc.el (part of my dotfiles' repo):

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

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; This covers my vc.el extensions, for use in my Emacs setup:
;; https://protesilaos.com/dotemacs.

;;; Code:

(require 'vc)
(require 'prot-common)

;;;; Customisation options

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

(defcustom prot-vc-log-limit 20
  "Limit commits in `prot-vc-custom-log' and others."
  :type 'integer
  :group 'prot-vc)

(defcustom prot-vc-shell-output "*prot-vc-shell-output*"
  "Name of buffer for VC-related shell output."
  :type 'string
  :group 'prot-vc)

(defcustom prot-vc-patch-output-dirs (list "~/" "~/Desktop/")
  "List of directories to save `prot-vc-patch-dwim' output."
  :type 'list
  :group 'prot-vc)

;;;; Commands and helper functions

(defun prot-vc--current-project ()
  "Return root directory of current project."
  (or (vc-root-dir)
      (locate-dominating-file "." ".git")))

;;;###autoload
(defun prot-vc-project-or-dir (&optional arg)
  "Run `vc-dir' for the current project root.
With optional prefix ARG (\\[universal-argument]), use the
`default-directory' instead."
  (interactive "P")
  (let* ((root (prot-vc--current-project))
         (dir (if arg default-directory root)))
    (vc-dir dir)))

(autoload 'log-view-current-entry "log-view")
(autoload 'dired-get-marked-files "dired")

(defun prot-vc--commit-num ()
  "Determime whether NUM is a positive integer."
  (let ((num prot-vc-log-limit))
    (if (and (integerp num)
             (> num 0))
        num
      (error "'%s' is not a valid number" num))))

;;;###autoload
(defun prot-vc-custom-log (&optional arg)
  "Like `vc-print-log' but for a custom fileset.

With optional prefix ARG (\\[universal-argument]), prompt for a
number to limit the log to.  Then prompt the user for matching
files in the `default-directory' with `completing-read-multiple'.

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

With a double prefix ARG, prompt for a limit and produce a log
that covers all files in the present directory."
  (interactive "P")
  (let* ((lim (if arg
                  (read-number "Limit log to N entries: " 5)
                (prot-vc--commit-num)))
         (dir default-directory)
         (dotless directory-files-no-dot-files-regexp)
         (files (directory-files dir nil dotless t))
         (set (cond                     ; REVIEW: this is confusing
               ((equal arg '(16))
                files)
               ((eq major-mode 'dired-mode) ; REVIEW: any downside over `derived-mode-p'?
                (dired-get-marked-files t nil))
               (t
                (completing-read-multiple
                 "Select files in current dir: " files nil t))))
         (backend (vc-backend set)))
    (vc-print-log-internal backend set nil nil lim 'with-diff)))

;;;###autoload
(defun prot-vc-log-kill-hash ()
  "Save to `kill-ring' contextual commit hash in `vc-print-log'."
  (interactive)
  (let ((commit (cadr (log-view-current-entry (point) t))))
    (kill-new (format "%s" commit))
    (message "Copied: %s" commit)))

(defvar prot-vc--patch-commit-hist '()
  "Minibuffer history for `prot-vc-patch-dwim' commits.")

(defvar prot-vc--patch-output-hist '()
  "Minibuffer history for `prot-vc-patch-dwim' output.")

;; TODO: abstract `process-lines' and make format customisable
;; REVIEW: using __ %h __ works but is a quick and dirty hack
(defun prot-vc--log-commit-prompt (&optional prompt)
  "Select git log commit with completion using optional PROMPT."
  (let ((text (or prompt "Select a commit: "))
        (vc (prot-vc--current-project))
        (num (format "%s" prot-vc-log-limit)))
    (if vc
        (completing-read
         text
         (prot-common-completion-table
          'line
          (process-lines "git" "log" "--pretty=format:%d __ %h __ %ad %an: %s" "-n" num))
         nil t nil 'prot-vc--patch-commit-hist)
      (error "'%s' is not under version control" default-directory))))

(defun prot-vc--log-commit-hash ()
  "Extract commit hash from `prot-vc--log-commit-prompt'."
  (let ((commit (prot-vc--log-commit-prompt)))
    (string-match "__ \\([a-z0-9]*\\) __" commit) ; see above "review" comment
    (match-string-no-properties 1 commit)))

;;;###autoload
(defun prot-vc-patch-dwim (&optional arg)
  "Create patch for commit at point in `log-view'.
With optional prefix ARG (\\[universal-argument]), or if no
commit at or around point is available, prompt for commit with
completion."
  (interactive "P")
  (let* ((commit-at-point (cadr (log-view-current-entry (point) t)))
         (commit (if (or arg (not commit-at-point))
                     (prot-vc--log-commit-hash)
                   commit-at-point))
         (vc-dir (or (prot-vc--current-project)
                     default-directory))
         (dirs (append (list vc-dir) prot-vc-patch-output-dirs))
         (out-dir
          (completing-read
           "Output directory: "
           (prot-common-completion-table 'file dirs)
           nil t nil 'prot-vc--patch-output-hist))
         (buf (get-buffer-create prot-vc-shell-output)))
    (shell-command
     (format "git format-patch -1 %s -o %s --" commit out-dir) buf)
    (message "Prepared patch for `%s' and sent it to %s"
             (propertize commit 'face 'bold)
             (propertize out-dir 'face 'success))
    (add-to-history 'prot-vc--patch-commit-hist commit)
    (add-to-history 'prot-vc--patch-output-hist out-dir)))

;; This is a tweaked variant of `vc-git-expanded-log-entry'
(defun prot-vc-git-expanded-log-entry (revision)
  "Expand git commit message for REVISION."
  (with-temp-buffer
    (apply 'vc-git-command t nil nil (list "log" revision "--stat" "-1" "--"))
    (goto-char (point-min))
    (unless (eobp)
      ;; Indent the expanded log entry.
      (while (re-search-forward "^  " nil t)
        (replace-match "")
        (forward-line))
      (buffer-string))))

(defun prot-vc-git-expand-function ()
  "Set `log-view-expanded-log-entry-function' for `vc-git'."
  (setq-local log-view-expanded-log-entry-function
              #'prot-vc-git-expanded-log-entry))

(defvar prot-vc-git-log-view-mode-hook nil
  "Hook that runs after `vc-git-log-view-mode'.")

(defun prot-vc-git-log-view-add-hook (&rest _)
  "Run `prot-vc-git-log-view-mode-hook'."
  (run-hooks 'prot-vc-git-log-view-mode-hook))

(autoload 'vc-git-log-view-mode "vc-git")

;;;###autoload
(define-minor-mode prot-vc-git-setup-mode
  "Extend `vc-git'."
  :init-value nil
  :global t
  (if prot-vc-git-setup-mode
      (progn
        (advice-add #'vc-git-log-view-mode :after #'prot-vc-git-log-view-add-hook)
        (add-hook 'prot-vc-git-log-view-mode-hook #'prot-vc-git-expand-function))
    (advice-remove #'vc-git-log-view-mode #'prot-vc-git-log-view-add-hook)
    (remove-hook 'prot-vc-git-log-view-mode-hook #'prot-vc-git-expand-function)))

(provide 'prot-vc)
;;; prot-vc.el ends here
5.4.2.1 Commit log editing

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

Overview of the following tweaks:

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

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

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

5.4.3 Interactive git front-end (Magit)

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

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

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

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

5.4.3.1 Base Magit settings

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

;; TODO 2021-01-19: merge all magit settings
(use-package magit
  :straight t
  :bind ("C-c g" . magit-status))
5.4.3.2 Magit commits

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

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

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

;; TODO 2021-01-19: merge all magit settings
(use-package magit-diff
  :after magit
  :config
  (setq magit-diff-refine-hunk t))
5.4.3.4 Magit repo list

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

;; TODO 2021-01-19: merge all magit settings
(use-package magit-repos
  :after magit
  :commands magit-list-repositories
  :config
  (setq magit-repository-directories
        '(("~/Git/Projects" . 1))))

5.4.4 Smerge and Ediff

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

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

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

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

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

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

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

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

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

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

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

git config --global merge.conflictStyle diff3

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

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

There actually is nothing in terms of Smerge-related configurations. The package is small and does one thing well.

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

(use-package smerge-mode)

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

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

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

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

5.5 Command-line shells

It should come to no surprise that Emacs can operate as both a terminal emulator for command line shells and toolkit for terminal emulators. The present section covers only the former category as I never use the likes of ansi-term or vterm.

5.5.0.1 Eshell and prot-eshell.el

Eshell is a strictly line-oriented command prompt written in Emacs Lisp. This comes with its pros and cons: it can understand Elisp but does not behave exactly like Unix shells, say, Bash. Eshell cannot display the kind of pseudo graphics a terminal emulator can, such as those you find in mutt, htop, ncmpcpp, newsboat and so on. As each user's needs are different, you will have to determine whether Eshell can fit into your workflow. Start by reading its fairly short, yet insightful, manual.

For me this tool is one of the most promising in the Emacs milieu because while it is a competent shell it can seamlessly integrate with the rest of Emacs' capabilities. This is best exemplified by its extensibility, such as what I am doing with prot-eshell.el. More on that below.

The fact that Eshell cannot reproduce the artefacts of the ncurses library does not pose a hindrance to my workflow, as I have replacements for all such "graphical" programs within Emacs. Gnus handles my email, M-x proced lets me interact with system processes, Bongo deals with media playback, while Elfeed fills the niche of following RSS/Atom feed.

Read relevant sections:

Now an overview of prot-eshell.el, with the full code reproduced right after the package configurations:

  • There are several prot-eshell-ffap-* commands that operate on the file at point. Say you have called ls and wish to expand the contents of a file at the command prompt. With point over the file name of interest, use prot-eshell-ffap-insert. Wish to visit the file instead, so that you may edit it? Try prot-eshell-ffap-find-file. The command prot-eshell-ffap-kill-save copies the file's full file system path, while prot-eshell-ffap-dired-jump opens a Dired buffer in that file's directory (see Dired (directory editor, file manager)).
  • prot-eshell-export takes the prompt and output of the last command and places it in a bespoke buffer. The name of the buffer is controlled by the variable prot-eshell-output-buffer. If that buffer does not exist, it is created. Otherwise subsequent invocations of this "export" command will append their contents to the existing ones. This is good for keeping a record of something you are working on. And because this is a standard buffer, you can edit it a will as well as call write-file (C-x C-w) to save it permanently to a file.
  • prot-eshell-redirect-to-buffer provides a completion prompt to help you redirect the output of a command to a given buffer. Simple and effective.
  • prot-eshell-narrow-output-highlight-regexp prompts for a regexp to highlight in the output of the last command. It then narrows the Eshell buffer to the contents of that output and emphasises the matches of the regexp. Very useful when you need to inspect some logs or other terse output. Remember that to widen the view you use the standard widen command, bound to C-x n w by default.
  • prot-eshell-complete-history lets you pick a command from your history using minibuffer completion. Forget about a non-interactive regexp search or, worse, consecutive calls to M-p and M-n to cycle through your recent inputs one at a time.
  • prot-eshell-complete-recent-dir provides a minibuffer prompt with completion that queries through all paths in your cd input history. This is much more convenient that standard actions like cd - or cd -N where N is the position of the item in the history of entries (retrieved with cd =).
  • prot-eshell-find-subdirectory-recursive uses completion to help you pick a subdirectory that extends the present working directory. It does so recursively, which makes it powerful, but can cause problems when called from the root of some massive directory tree. Exercise restraint.
  • prot-eshell-root-dir switches the present working directory to that of the current project's root directory, if one is found.

Here is a video on Eshell and my extras (2020-05-08) which, however, showcases older code than what I have here.

Also check these valuable resources:

(use-package eshell)
(use-package esh-mode)
(use-package esh-module
  :config
  (setq eshell-modules-list             ; It works but may need review
        '(eshell-alias
          eshell-basic
          eshell-cmpl
          eshell-dirs
          eshell-glob
          eshell-hist
          eshell-ls
          eshell-pred
          eshell-prompt
          eshell-script
          eshell-term
          eshell-tramp
          eshell-unix)))

(use-package em-dirs
  :after esh-mode
  :config
  (setq eshell-cd-on-directory t))

(use-package em-tramp
  :after esh-mode
  :config
  (setq password-cache t)
  (setq password-cache-expiry 600))

(use-package em-hist
  :after esh-mode
  :config
  (setq eshell-hist-ignoredups t)
  (setq eshell-save-history-on-exit t))

(use-package prot-eshell
  :straight (:type built-in)
  :after (eshell esh-mode em-dirs em-hist)
  :config
  (setq prot-eshell-output-buffer "*Exported Eshell output*")
  (setq prot-eshell-output-delimiter "* * *")
  :bind (("<s-return>" . eshell)
         :map eshell-mode-map
         ("M-k" . eshell-kill-input)
         ("C-c C-f" . prot-eshell-ffap-find-file)
         ("C-c C-j" . prot-eshell-ffap-dired-jump)
         ("C-c C-w" . prot-eshell-ffap-kill-save)
         ("C-c C->" . prot-eshell-redirect-to-buffer)
         ("C-c C-e" . prot-eshell-export)
         ("C-c C-r" . prot-eshell-root-dir)
         :map eshell-cmpl-mode-map
         ("C-c TAB" . prot-eshell-ffap-insert) ; C-c C-i
         ("C-c M-h" . prot-eshell-narrow-output-highlight-regexp)
         :map eshell-hist-mode-map
         ("M-s" . nil) ; I use this prefix for lots of more useful commands
         ("M-r" . prot-eshell-complete-history)
         ("C-c C-d" . prot-eshell-complete-recent-dir)
         ("C-c C-s" . prot-eshell-find-subdirectory-recursive)))

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

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

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; This covers my Eshell extensions, for use in my Emacs setup:
;; https://protesilaos.com/dotemacs.

;;; Code:

(require 'eshell)
(require 'esh-mode)
(require 'em-dirs)
(require 'em-hist)

;;;; Customisation options

(defgroup prot-eshell ()
  "Extensions for Eshell and related libraries."
  :group 'shell)

(defcustom prot-eshell-output-buffer "*Exported Eshell output*"
  "Name of buffer with the last output of Eshell command.
Used by `prot-eshell-export'."
  :type 'string
  :group 'prot-eshell)

(defcustom prot-eshell-output-delimiter "* * *"
  "Delimiter for successive `prot-eshell-export' outputs.
This is formatted internally to have newline characters before
and after it."
  :type 'string
  :group 'prot-eshell)

;;;; Commands

(autoload 'ffap-file-at-point "ffap.el")

(defmacro prot-eshell-ffap (name doc &rest body)
  "Make `find-file-at-point' commands for Eshell.
NAME is how the function is called.  DOC is the function's
documentation string.  BODY is the set of arguments passed to the
`if' statement to be evaluated when a file at point is present."
  `(defun ,name ()
     ,doc
     (interactive)
     (let ((file (ffap-file-at-point)))
       (if file
           ,@body
         (user-error "No file at point")))))

(prot-eshell-ffap
 prot-eshell-ffap-insert
 "Insert (cat) contents of file at point."
 (progn
   (goto-char (point-max))
   (insert (format "cat %s" file))
   (eshell-send-input)))

(prot-eshell-ffap
 prot-eshell-ffap-kill-save
 "Add to kill-ring the absolute path of file at point."
 (progn
   (kill-new (format "%s/%s" (eshell/pwd) file))
   (message "Copied full path of %s" file)))

(prot-eshell-ffap
 prot-eshell-ffap-find-file
 "Run `find-file' for file at point (ordinary file or dir).
Recall that this will produce a `dired' buffer if the file is a
directory."
 (find-file file))

(prot-eshell-ffap
 prot-eshell-ffap-dired-jump
 "Jump to the parent directory of the file at point."
 (dired (file-name-directory file)))

(defun prot-eshell--command-prompt-output ()
  "Capture last command prompt and its output."
  (let ((beg (save-excursion
               (goto-char (eshell-beginning-of-input))
               (goto-char (point-at-bol)))))
  (when (derived-mode-p 'eshell-mode)
    (buffer-substring-no-properties beg (eshell-end-of-output)))))

;;;###autoload
(defun prot-eshell-export ()
  "Produce a buffer with output of the last Eshell command.
If `prot-eshell-output-buffer' does not exist, create it.  Else
append to it, while separating multiple outputs with
`prot-eshell-output-delimiter'."
  (interactive)
  (let ((eshell-output (prot-eshell--command-prompt-output)))
    (with-current-buffer (get-buffer-create prot-eshell-output-buffer)
      (goto-char (point-max))
      (unless (eq (point-min) (point-max))
        (insert (format "\n%s\n\n" prot-eshell-output-delimiter)))
      (goto-char (point-at-bol))
      (insert eshell-output)
      (switch-to-buffer-other-window (current-buffer)))))

;;;###autoload
(defun prot-eshell-redirect-to-buffer (buffer)
  "Complete the syntax for appending Eshell output to BUFFER."
  (interactive
   (list (read-buffer "Redirect to buffer: ")))
  (insert
   (format " >>> #<%s>" buffer)))

;;;###autoload
(defun prot-eshell-narrow-output-highlight-regexp (regexp)
  "Narrow to last command output and highlight REGEXP."
  (interactive
   (list (read-regexp "Regexp to highlight")))
  (narrow-to-region (eshell-beginning-of-output)
                    (eshell-end-of-output))
  (goto-char (point-min))
  (highlight-regexp regexp 'hi-yellow)
  (message "Narrowed to last output and highlighted < %s >" regexp))

;;;###autoload
(defun prot-eshell-complete-recent-dir (&optional arg)
  "Switch to a recent Eshell directory using completion.
With optional ARG prefix argument (\\[universal-argument]) also
open the directory in a `dired' buffer."
  (interactive "P")
  (let* ((dirs (ring-elements eshell-last-dir-ring))
         (dir (completing-read "Switch to recent dir: " dirs nil t)))
    (insert dir)
    (eshell-send-input)
    (when arg
      (dired dir))))

;;;###autoload
(defun prot-eshell-complete-history ()
  "Insert element from Eshell history using completion."
  (interactive)
  (let ((hist (ring-elements eshell-history-ring)))
    (insert
     (completing-read "Input from history: " hist nil t))))

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

;;;###autoload
(defun prot-eshell-find-subdirectory-recursive ()
  "Recursive `eshell/cd' to subdirectory.
This command has the potential for infinite recursion: use it
wisely or prepare to call `eshell-interrupt-process'."
  (interactive)
  (let* ((dir (abbreviate-file-name (eshell/pwd)))
         (contents (directory-files-recursively dir ".*" t nil nil))
         (dirs (cl-remove-if-not (lambda (x)
                                   (or (file-directory-p x)
                                       (string-match-p "\\.git" x)))
                                 contents))
         (selection (completing-read
                     (format "Find sub-dir from %s: "
                             (propertize dir 'face 'success))
                     dirs nil t)))
    (insert selection)
    (eshell-send-input)))

;;;###autoload
(defun prot-eshell-root-dir ()
  "Switch to the root directory of the present project."
  (interactive)
  (let ((root (or (vc-root-dir)
                  (locate-dominating-file "." ".git"))))
    (if root
        (progn
          (insert root)
          (eshell-send-input))
      (user-error "Cannot find a project root here"))))

(provide 'prot-eshell)
;;; prot-eshell.el ends here
5.5.0.2 Shell (M-x shell)

NOTE: I normally use Eshell. Refer to the Eshell and prot-eshell.el section.

This is a shell (Bash, in my case) that runs inside of Emacs. Unlike terminal emulators, this one can use standard Emacs keys and behaves much like an ordinary buffer. It also integrates nicely with the built-in completion tools, which makes it particularly nice to work with.

The one area where this Shell differs substantially from ordinary buffers is with regard to the command prompt: you can re-run a command on the scroll-back buffer by just hitting RET while point is on its line (no need to go back to the end and cycle the command history with M-p or M-n).

Another peculiarity relative to the standard commands in the terminal is to search backward through your history with M-r (whereas in a terminal emulator you use C-r).

Run C-h m inside of a shell buffer to learn about all the key bindings and corresponding functions.

(use-package shell
  :commands shell-command
  :config
  (setq ansi-color-for-comint-mode t)
  (setq shell-command-prompt-show-cwd t) ; Emacs 27.1
  :bind (("<s-S-return>" . shell)))

5.6 Calendar

Some basic settings for calendar.el. It is used by Org-mode facilities that require date/time input (see following sections).

(use-package calendar
  :config
  (setq calendar-mark-diary-entries-flag nil)
  (setq calendar-time-display-form
        '(24-hours ":" minutes
                   (when time-zone
                     (concat " (" time-zone ")"))))
  (setq calendar-week-start-day 1)      ; Monday
  (setq calendar-date-style 'iso)
  (setq calendar-mark-holidays-flag nil)
  (setq calendar-time-zone-style 'numeric) ; Emacs 28.1

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

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

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

5.7 Org-mode (personal information manager)

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

What lends Org its super powers though is everything else built around it: a rich corpus of Elisp functions that automate, link, combine, enhance, structure, or otherwise enrich the process of using this rather straightforward system of plain text notation.

Couched in those terms, Org is at once a distribution of well integrated libraries and a vibrant ecosystem that keeps producing new ideas and workflows on how to organise one's life with plain text.

The present document is written in org-mode while its website version is outputted by a tool (also part of Org) that exports Org notation to its HTML equivalent.

Regarding the following code block, I strongly encourage you to make liberal use of Emacs' documentation facilities to learn more about functions, variables, symbols provided herein. And do not forget to read Org's manual.

What follows in an exposition about each of the subsesctions of this package declaration:

Org links

The org-store-link is one of the commands I use the most, as it allows me to, inter alia, connect the various sections of this document. Use it to store a direct link to the heading you are currently under. Or to produce a properly formatted link to supported buffers you are visiting (e.g. another file).

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

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

Org capture

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

Each template is accessed via a key. These are listed in a temporary buffer when you call org-capture. Unique keys give direct access to their template, whereas templates that share a common initial key will produce a second selection list with the remaining options. In the latter case, the initial key entry has no call to an actual function, but is just written as a heading.

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

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

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

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

The %? specifier determines where the point shall end in once the template is parsed. While %i will insert the contents of the active region, if any.

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

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

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

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

Org agenda

The org-agenda is not just a single interface. It rather is your conduit to a set of utilities for reading timestamped tasks. From there you can keep track of all the relevant entries you have inserted in the files declared as part of org-agenda-files list.

Running org-agenda will present you with a list of possible options: the "dispatcher" as it called. Here is a primer (there are many more functions documented in the manual):

  • From the dispatcher, the a is where you keep track of all the items that have a date assigned to them, be it SCHEDULED or DEADLINE. To assign such a value to a heading use C-c C-s or C-c C-d respectively. Run those commands with a universal prefix (C-u) to remove the timestamp. Hit / to filter this view to match particular tags.
  • In the dispatcher's menu, the t will list all your tasks, regardless of whether they have a date assigned to them. You can then filter by keyword, regular expression, etc. Check the top of the buffer for information on how to do that.
  • And the n in the dispatcher will offer you a combined view of the above.
Org export
I do not have much to offer here, apart from the setup that handles consistent heading IDs and anchor tags (the latter concerns the HTML output). Everything in that segment, minus some minor tweaks from my part, is copied from this detailed tutorial on Org header IDs. Basically, the problem is that exported HTML does not have reliable anchor tags for the various sections of the document. This fixes the issue (read the article for more).

Finally, note that I sometimes deliver simple presentations using Org. Refer to Custom extensions for "focus mode" (prot-logos.el).

;; Pro tip: If you are reading the source code, use C-c '
;; (`org-edit-special') to put the code block in a dedicated buffer and
;; then activate `prot-outline-minor-mode-safe' to conveniently browse
;; this massive package declaration.
(use-package org-mode
  ;; :straight (:host nil :type git
  ;;            :repo "https://code.orgmode.org/bzg/org-mode.git") ; Get Org from its source
  ;; ;; Use this instead of the above if you want the built-in version.
  :straight (:type built-in)
  :init
  (setq org-directory "~/Org")
  (setq org-imenu-depth 7)
;;;; general settings
  (setq org-adapt-indentation nil)      ; No, non, nein, όχι!
  (setq org-special-ctrl-a/e nil)
  (setq org-special-ctrl-k nil)
  (setq org-M-RET-may-split-line '((default . nil)))
  (setq org-hide-emphasis-markers t)
  (setq org-hide-macro-markers t)
  (setq org-hide-leading-stars nil)
  (setq org-structure-template-alist    ; CHANGED in Org 9.3, Emacs 27.1
        '(("s" . "src")
          ("E" . "src emacs-lisp")
          ("e" . "example")
          ("q" . "quote")
          ("v" . "verse")
          ("V" . "verbatim")
          ("c" . "center")
          ("C" . "comment")))
  (setq org-catch-invisible-edits 'show)
  (setq org-return-follows-link nil)
  (setq org-loop-over-headlines-in-active-region 'start-level)
  (setq org-modules '(ol-gnus ol-info ol-eww))

;;;; refile, todo
  (setq org-refile-targets
        '((org-agenda-files . (:maxlevel . 2))
          (nil . (:maxlevel . 2))))
  (setq org-refile-use-outline-path t)
  (setq org-refile-allow-creating-parent-nodes 'confirm)
  (setq org-refile-use-cache t)
  (setq org-reverse-note-order nil)
  (setq org-todo-keywords
        '((sequence "TODO(t)" "|" "DONE(D)" "CANCEL(C)")
          (sequence "MEET(m)" "|" "MET(M)")
          (sequence "STUDY(s)" "|" "STUDIED(S)")
          (sequence "WRITE(w)" "|" "WROTE(W)")))
  (setq org-todo-keyword-faces
        '(("MEET" . '(font-lock-preprocessor-face org-todo))
          ("STUDY" . '(font-lock-variable-name-face org-todo))
          ("WRITE" . '(font-lock-type-face org-todo))))
  (setq org-priority-faces
        '((?A . '(org-scheduled-today org-priority))
          (?B . org-priority)
          (?C . '(shadow org-priority))))
  (setq org-fontify-done-headline nil)
  (setq org-fontify-quote-and-verse-blocks t)
  (setq org-fontify-whole-heading-line nil)
  (setq org-fontify-whole-block-delimiter-line t)
  (setq org-enforce-todo-dependencies t)
  (setq org-enforce-todo-checkbox-dependencies t)
  (setq org-track-ordered-property-with-tag t)
  (setq org-highest-priority ?A)
  (setq org-lowest-priority ?C)
  (setq org-default-priority ?A)

;;;; tags
  (setq org-tag-alist                   ; TODO review org tag list
        '((:startgroup)
          ("@work")
          ("@priv")
          (:endgroup)
          ("emacs")
          ("modus")
          ("politics")
          ("economics")
          ("philosophy")
          ("paper")
          ("book")
          ("essay")
          ("article")
          ("mail")
          ("website")))

;;;; log
  (setq org-log-done 'time)
  (setq org-log-note-clock-out nil)
  (setq org-log-redeadline nil)
  (setq org-log-reschedule nil)
  (setq org-read-date-prefer-future 'time)

;;;; links
  (setq org-link-keep-stored-after-insertion t)

;;;; capture
  (setq org-capture-templates
        `(("a" "Article to write" entry
           (file+headline "tasks.org" "Writing list")
           ,(concat "* WRITE %^{Title} %^g\n"
                    "SCHEDULED: %^t\n"
                    ":PROPERTIES:\n"
                    ":CAPTURED: %U\n:END:\n\n"
                    "%i%?"))
          ("b" "Basic task for future review" entry
           (file+headline "tasks.org" "Basic tasks that need to be reviewed")
           ,(concat "* %^{Title}\n"
                    ":PROPERTIES:\n"
                    ":CAPTURED: %U\n"
                    ":END:\n\n"
                    "%i%l"))
          ("t" "Task with a due date" entry
           (file+headline "tasks.org" "Task list with a date")
           ,(concat "* %^{Scope of task||TODO|STUDY|MEET} %^{Title} %^g\n"
                    "SCHEDULED: %^t\n"
                    ":PROPERTIES:\n:CAPTURED: %U\n:END:\n\n"
                    "%i%?"))
          ("r" "Reply to an email" entry
           (file+headline "tasks.org" "Mail correspondence")
           ,(concat "* TODO [#B] %:subject :mail:\n"
                    "SCHEDULED: %t\n:"
                    "PROPERTIES:\n:CONTEXT: %a\n:END:\n\n"
                    "%i%?"))))

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

  ;; Source: https://stackoverflow.com/a/54251825
  (defun contrib/org-capture-no-delete-windows (oldfun args)
    (cl-letf (((symbol-function 'delete-other-windows) 'ignore))
      (apply oldfun args)))

  ;; Same source as above
  (advice-add 'org-capture-place-template
              :around 'contrib/org-capture-no-delete-windows)

;;;; agenda
;;;;; Basic agenda setup
  (setq org-default-notes-file "~/Org/notes.org")
  (setq org-agenda-files '("~/Org" "~/.emacs.d" "~/Documents"))
  (setq org-agenda-span 14)
  (setq org-agenda-start-on-weekday 1)  ; Monday
  (setq org-agenda-confirm-kill t)
  (setq org-agenda-show-all-dates t)
  (setq org-agenda-show-outline-path nil)
  (setq org-agenda-window-setup 'current-window)
  (setq org-agenda-skip-comment-trees t)
  (setq org-agenda-menu-show-matcher t)
  (setq org-agenda-menu-two-columns nil)
  (setq org-agenda-sticky nil)
  (setq org-agenda-custom-commands-contexts nil)
  (setq org-agenda-max-entries nil)
  (setq org-agenda-max-todos nil)
  (setq org-agenda-max-tags nil)
  (setq org-agenda-max-effort nil)

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

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

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

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

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

;;;;; Agenda diary entries
  ;; NOTE: I don't use the diary, but here it is anyway
  (setq org-agenda-insert-diary-strategy 'date-tree)
  (setq org-agenda-insert-diary-extract-time nil)
  (setq org-agenda-include-diary nil)

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

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

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

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

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

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

;;;;; 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-sort-agenda-notime-is-late t)
  (setq org-sort-agenda-noeffort-is-high t)

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

;;;; code blocks
  (setq org-confirm-babel-evaluate nil)
  (setq org-src-window-setup 'current-window)
  (setq org-edit-src-persistent-message nil)
  (setq org-src-fontify-natively t)
  (setq org-src-preserve-indentation t)
  (setq org-src-tab-acts-natively t)
  (setq org-edit-src-content-indentation 0)

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

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

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

;;;; IDs
  (setq org-id-link-to-org-use-id
        'create-if-interactive-and-no-custom-id)

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

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

  :hook (org-follow-link-hook . prot-pulse-recentre-top)
  :bind (("C-c a" . org-agenda)
         ("C-c c" . org-capture)
         ("C-c l" . org-store-link)
         :map org-mode-map
         ("C-'" . nil)
         ("C-," . nil)
         ("<C-return>" . nil)
         ("<C-S-return>" . nil)
         ("C-c S-l" . org-toggle-link-display)
         ("C-c C-S-l" . org-insert-last-stored-link)))

5.8 Email settings

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

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

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

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

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

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

Then we set up the interface for composing emails:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

IMAPStore pub-remote
Account pub

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

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

Create Both
# Expunge Both
SyncState *

##########

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

IMAPStore inf-remote
Account inf

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

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

Create Both
# Expunge Both
SyncState *

##########

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

IMAPStore prv-remote
Account prv

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

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

Create Both
# Expunge Both
SyncState *

5.8.3 Gnus for reading email, mailing lists, and more

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

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

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

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

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

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

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

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

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

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

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

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

5.8.3.1 Gnus account settings and essential configurations

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

;; TODO 2021-01-19: merge all gnus settings
(use-package gnus-async
  :after gnus
  :config
  (setq gnus-asynchronous t)
  (setq gnus-use-article-prefetch 15))
5.8.3.5 Gnus group (main interface)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  :hook (gnus-summary-mode-hook . hl-line-mode)
  :bind (:map gnus-summary-mode-map
              ("<delete>" . gnus-summary-delete-article)
              ("n" . gnus-summary-next-article)
              ("p" . gnus-summary-prev-article)
              ("N" . gnus-summary-next-unread-article)
              ("P" . gnus-summary-prev-unread-article)
              ("M-n" . gnus-summary-next-thread)
              ("M-p" . gnus-summary-prev-thread)
              ("C-M-n" . gnus-summary-next-group)
              ("C-M-p" . gnus-summary-prev-group)
              ("C-M-^" . gnus-summary-refer-thread)))
5.8.3.7 Gnus server

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

;; TODO 2021-01-19: merge all gnus settings
(use-package gnus-srvr
  :after gnus
  :hook ((gnus-browse-mode-hook gnus-server-mode-hook) . hl-line-mode))
5.8.3.8 Gnus window layout

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

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

;; TODO 2021-01-19: merge all gnus settings.  THIS WILL BE REMOVED!
(use-package gnus-win
  :config
  (gnus-add-configuration
   '(article
     (horizontal 1.0
                 (vertical 40 (group 1.0))
                 (vertical 1.0
                           (summary 0.16 point)
                           (article 1.0)))))

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

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

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

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

;; TODO 2021-01-19: merge all gnus settings
(use-package gnus-dired
  :after (gnus dired)
  :hook (dired-mode-hook . gnus-dired-mode))

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

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

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

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

Further notes:

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

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

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

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

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

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

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

(use-package org-mu4e                   ; no need for `:straight t'
  :after (org mu4e)
  :config
  (setq org-mu4e-link-query-in-headers-mode nil))

5.8.5 Sending email (SMTP)

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

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

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

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

5.9 Bongo music or media manager (and prot-bongo.el)

Bongo is a buffer oriented media manager. It provides an interface to external players, such as VLC or MPV. Those are known as "back-ends" (prot-bongo-enabled-backends sets up my preferred ones). A "library" buffer contains the media collection, which consists of music or video files, or even links to online streams. While a "playlist" buffer holds the items that wait in the queue to be played by the back-end program. We normally use the Library to browse our multimedia collection and to pick the items we wish to add to the playlist. In my case, the Library is just a Dired buffer, so I also benefit from everything this powerful tool provides (for configurations and commentary, check Dired (directory editor, file manager)).

I mostly use Bongo for my local Music collection, but also as an interface to the various video or audio streams I access via their RSS feed (the latter is done through the integration with Elfeed—as such, consult Elfeed feed reader, prot-elfeed.el and prot-elfeed-bongo.el).

My music directories inside ~/Music are, in principle, organised in file system paths that follow the pattern Artist/Album/Tracks. Each track inside of them is named in the style of Artist - Album - Title. As part of this collection comes from physical discs, I never bothered writing metadata for all them and, consequently, do not rely on it to either play back or organise my files. The directory structure and its concomitant naming conventions are sufficient and, in my opinion, more precise and easier to predict. My methodology aside, Bongo will read the media's file name and interpret each hyphen as a field delimiter that it can then use to better present the information in the playlist queue.

I prefer this manual approach to organising my music collection over all metadata-centric alternatives. Reading metadata requires more than just looking at a plain text name: it is opaque or at least not immediately obvious. Besides, how many times have you bought an album that has one track with a guest author and that one track gets auto-filtered in some random place because of its unique meta datum for the "artist" field? So you need to supply a track "artist", then an "album artist" to avoid the pains of unpredictability… This is the kind of busy work I want to eschew by organising my files in a way that I understand intuitively. As for whether Bongo can handle metadata, I do not know.

My way of listening to music is straightforward: load up a directory or directory tree, randomise the playback order, and let it play in the background. To ease this workflow, I make my ~/Music directory a valid Bongo library. As already noted in the introduction, this practically means that I can access it with dired, while still benefiting from the Bongo-specific extensions (the technical implementation is handled by my comprehensive prot-bongo.el library, which is produced after the following package declarations).

Two main uses of the Dired+Bongo combination:

Enqueue items for immediate playback
Mark some directories or files the way you always do with Dired, and hit a key to insert them to the Bongo playlist (the command is prot-bongo-dired-insert). This will do a number of things, specifically, (i) enqueue those Dired marks to the playlist buffer, (ii) perform playback randomisation where appropriate, (iii) mark this inserted group by bespoke section delimiters for easier future retrieval, and (iv) start playing an item unless one is already playing. When there are no marked items in the Dired buffer, the file/directory at point will be used instead.
Add items to plain playlist files
Add the absolute filesystem path of marked items (typically directories) to either an existing playlist file or a new one that is created on demand. This is done using minibuffer completion (check prot-bongo-dired-make-playlist-file). Those files can at any time be inserted in a Bongo playlist buffer to start playing their contents, following the same conditional patterns of behaviour described in the previous point (see, in particular, prot-bongo-playlist-insert-playlist-file). Put simply, you have a "rock" playlist file that includes file paths to "~/Music/Scorpions" and "~/Music/Queen", so inserting that file plays all songs that are found in those two directories (files are found recursively, so don't worry if you have multiple albums inside each directory).

Now a few words about my custom delimiters that the likes of prot-bongo-dired-insert will add to the Bongo playlist buffer:

  • The "section delimiters" (prot-bongo-playlist-section-delimiter) demarcate sets of inserted media. For example, if you mark three items in Dired and proceed to enqueue them, then the section delimiter goes after those three. Such delimiters provide visual feedback, but can also be used to either navigate between them and/or remove all of their contents in one go (with prot-bongo-playlist-kill-section).
  • Then there is my concept of "headings" that complements those section delimiters (prot-bongo-playlist-heading-delimiter). Headings are comments in the Bongo playlist buffer that contain the name of the directory or file that includes the tracks diretly below them. Other than being informative, they function as anchors for navigation (e.g. with prot-bongo-playlist-heading-next), while they double as pointers in an M-x imenu index (by virtue of prot-bongo-imenu-setup). So we can use key bindings to go to the next or previous heading or employ minibuffer completion to jump directly to the heading of interest. The beauty of this is that we can then use either the built-in Imenu, or the excellent consult-imenu to navigate to a heading using minibuffer completion. For more on the latter, refer to the mega-section on Completion framework and extras. It covers everything about the minibuffer, Consult, Embark, and more.

For the video demo of some of the aforementioned, you may want to watch the recording on Bongo media manager and my extras (2020-08-06). Though note that it showcases code that is considerably older than what I currently have with prot-bongo.el (as of 2021-01-18).

By default, all the Bongo buffers have a prominent header that provides some basic information about the program. As I have no use for that, I run the function prot-bongo-remove-headers: it takes care of clearing the buffers while setting them up. The idea for this is derived from the Emacs configuration file of Nicolas De Jaeghere.

Finally, note that I combine Bongo with Elfeed to keep track of video or audio streams that I follow. The code, shared as prot-elfeed-bongo.el, is included in the section on Elfeed (RSS/Atom feed reader). Thanks to Madhavan Krishnan who helped me flesh out this project by sharing code and ideas in a private exchange (disclosed with permission).

Also watch: Manage podcasts in Emacs with Elfeed and Bongo (2020-09-11), though please bear in my that my current code is not exactly what was demonstrated back then (as of 2021-01-18).

(use-package bongo
  :straight t
  :demand
  :init
  (setq bongo-default-directory "~/Music")
  (setq bongo-prefer-library-buffers nil)
  (setq bongo-insert-whole-directory-trees t)
  (setq bongo-logo nil)
  (setq bongo-display-track-icons nil)
  (setq bongo-display-track-lengths nil)
  (setq bongo-display-header-icons nil)
  (setq bongo-display-playback-mode-indicator t)
  (setq bongo-display-inline-playback-progress nil) ; t slows down the playlist buffer
  (setq bongo-join-inserted-tracks nil)
  (setq bongo-field-separator (propertize " · " 'face 'shadow))
  (setq bongo-mark-played-tracks t)
  (setq bongo-vlc-program-name "cvlc")
  :config
  (bongo-mode-line-indicator-mode -1)
  (bongo-header-line-mode -1)
  :bind (("C-c b" . bongo)
         ("<C-XF86AudioPlay>" . bongo-pause/resume)
         ("<C-XF86AudioNext>" . bongo-next)
         ("<C-XF86AudioPrev>" . bongo-previous)
         ("<M-XF86AudioPlay>" . bongo-show)
         ("<S-XF86AudioNext>" . bongo-seek-forward-10)
         ("<S-XF86AudioPrev>" . bongo-seek-backward-10)
         :map bongo-playlist-mode-map
         ("n" . bongo-next-object)
         ("p" . bongo-previous-object)
         ("R" . bongo-rename-line)
         ("j" . bongo-dired-line)       ; Jump to dir of file at point
         ("J" . dired-jump)             ; Jump to library buffer
         ("I" . bongo-insert-special)))

(use-package prot-bongo
  :straight (:type built-in)
  :init
  (setq prot-bongo-enabled-backends '(mpv vlc))
  (setq prot-bongo-playlist-section-delimiter (make-string 30 ?*))
  (setq prot-bongo-playlist-heading-delimiter "§")
  (setq prot-bongo-playlist-directory
        (concat
         (file-name-as-directory bongo-default-directory)
         (file-name-as-directory "playlists")))
  ;; Those set up a few extras: read each function's doc string.  Pass
  ;; an argument to undo their effects.
  :config
  (prot-bongo-enabled-backends)
  (prot-bongo-remove-headers)
  (prot-bongo-imenu-setup)
  :hook ((dired-mode-hook . prot-bongo-dired-library-enable)
         (wdired-mode-hook . prot-bongo-dired-library-disable)
         (prot-bongo-playlist-change-track-hook . prot-bongo-playlist-recenter))
  :bind (:map bongo-playlist-mode-map
              ("C-c C-n" . prot-bongo-playlist-heading-next)
              ("C-c C-p" . prot-bongo-playlist-heading-previous)
              ("M-n" . prot-bongo-playlist-section-next)
              ("M-p" . prot-bongo-playlist-section-previous)
              ("M-h" . prot-bongo-playlist-mark-section)
              ("M-d" . prot-bongo-playlist-kill-section)
              ("g" . prot-bongo-playlist-reset)
              ("D" . prot-bongo-playlist-terminate)
              ("r" . prot-bongo-playlist-random-toggle)
              ("i" . prot-bongo-playlist-insert-playlist-file)
              :map bongo-dired-library-mode-map
              ("<C-return>" . prot-bongo-dired-insert)
              ("C-c SPC" . prot-bongo-dired-insert)
              ("C-c +" . prot-bongo-dired-make-playlist-file)))

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

;;; prot-bongo.el --- Bongo extensions for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; Extensions for Bongo, intended for use in my Emacs setup:
;; https://protesilaos.com/dotemacs.

;;; Code:

;; XXX Written on 2021-01-18.  Remains to be reviewed.

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

(defgroup prot-bongo ()
  "Personal extensions for Bongo."
  :group 'bongo)

(defcustom prot-bongo-enabled-backends '(mpv vlc)
  "List of enabled backends.
See `bongo-backends' for a list of available backends."
  :type 'list
  :group 'prot-bongo)

(defcustom prot-bongo-playlist-section-delimiter (make-string 30 ?*)
  "Delimiter for inserted groups in Bongo playlist buffers.
It is recommended to set this to a few character length, as it
should be placed on its own line to demacrate groups of enqueued
media."
  :type 'string
  :group 'prot-bongo)

(defcustom prot-bongo-playlist-heading-delimiter "§"
  "Delimiter for custom headings in Bongo playlist buffers.
It is recommended to set this to a single character, as it will
be complemented with the name of the enqueued item."
  :type 'string
  :group 'prot-bongo)

(defvar bongo-default-directory)

(defcustom prot-bongo-playlist-directory
  (concat
   (file-name-as-directory bongo-default-directory)
   (file-name-as-directory "playlists"))
  "Path to playlist files.
Such files are plain text documents that contain a filesystem
path on each line which points to a multimedia item (e.g. a
directory with music files).

Make sure this is a valid path, as we will not make any attempt
at creating it or running any other kind of check."
  :type 'string
  :group 'prot-bongo)

;;;; Basic setup

(defvar bongo-enabled-backends)

;;;###autoload
(defun prot-bongo-enabled-backends (&optional negation)
  "Assign variable `prot-bongo-enabled-backends' to Bongo.
With optional NEGATION, undo this assignment."
  (if negation
      (progn
        (setq bongo-enabled-backends nil)
        (remove-hook 'bongo-mode-hook #'prot-bongo-enabled-backends))
    (setq bongo-enabled-backends prot-bongo-enabled-backends)
    (add-hook 'bongo-mode-hook #'prot-bongo-enabled-backends)))

;; The original idea for the advice setup to hide the Bongo comment
;; headers comes from the Emacs configuration of Nicolas De Jaeghere:
;; <https://github.com/Koekelas/dotfiles/blob/master/emacs.org>.

(defvar bongo-default-playlist-buffer-name)
(defvar bongo-default-library-buffer-name)
(declare-function bongo-playlist-mode "bongo")
(declare-function bongo-library-mode "bongo")

(defun prot-bongo-playlist-buffer-no-banner ()
  "Set up a Bongo playlist buffer without its header commentary.
To be advised as override for `bongo-default-playlist-buffer'.

To actually enable this, evaluate `prot-bongo-remove-headers'."
  (with-current-buffer (get-buffer-create bongo-default-playlist-buffer-name)
    (unless (derived-mode-p 'bongo-playlist-mode)
      (bongo-playlist-mode))
    (current-buffer)))

(defun prot-bongo-library-buffer-no-banner ()
  "Set up a Bongo library buffer without its header commentary.
To be advised as override for `bongo-default-library-buffer'.

To actually enable this, evaluate `prot-bongo-remove-headers'."
  (with-current-buffer (get-buffer-create bongo-default-library-buffer-name)
    (unless (derived-mode-p 'bongo-library-mode)
      (bongo-library-mode))
    (current-buffer)))

;;;###autoload
(defun prot-bongo-remove-headers (&optional negation)
  "Remove comment headers from Bongo buffers.
With optional NEGATION undo the changes."
  (if negation
      (progn
        (advice-remove 'bongo-default-playlist-buffer #'prot-bongo-playlist-buffer-no-banner)
        (advice-remove 'bongo-default-library-buffer #'prot-bongo-library-buffer-no-banner))
    (advice-add 'bongo-default-playlist-buffer :override #'prot-bongo-playlist-buffer-no-banner)
    (advice-add 'bongo-default-library-buffer :override #'prot-bongo-library-buffer-no-banner)))

;;;; Custom delimiters for headings and sections

(declare-function bongo-insert-comment-text "bongo")

(defun prot-bongo-playlist-heading (title &optional description)
  "Insert `bongo' comment with TITLE and DESCRIPTION.
Use this to add a custom heading for the enqueued media items."
  (bongo-insert-comment-text
   (format "%s %s%s\n"
           prot-bongo-playlist-heading-delimiter
           title
           (if description (concat " " description) ""))))

(defun prot-bongo-playlist-section ()
  "Make `prot-bongo-playlist-section-delimiter' comment."
  (bongo-insert-comment-text
   (format "\n%s\n\n" prot-bongo-playlist-section-delimiter)))

;;;; Motions and actions for custom sections

;; REVIEW: there probably is a better way to parametrise move-buf and
;; move-point so that one key checks for appropriate forward or backward
;; motions, but this is okay right now.
(defmacro prot-bongo-playlist-motion (fn desc rx move-buf move-point)
  "Produce interactive commands to navigate custom bongo delimiters.

FN is the resulting interactive function's name.  DESC is its doc
string.  RX is the regular expression that matches the custom
bongo playlist delimiter (see `prot-bongo-playlist-delimiter' and
`prot-bongo-playlist-heading').

MOVE-BUF is a motion across an arbitrary number of lines.
Currently it assumes (though does test) either
`re-search-forward' or `re-search-backward'.  Likewise,
MOVE-POINT expects `point-at-eol' or `point-at-bol'.  These
motions should go in pairs, in the order they are presented here."
  (declare (indent defun))
  `(defun ,fn ()
     ,desc
     (interactive)
     (let ((section ,rx))
       (when (save-excursion (funcall ,move-buf section nil t))
         (goto-char (funcall ,move-point))
         (funcall ,move-buf section nil t)))))

(prot-bongo-playlist-motion
  prot-bongo-playlist-heading-next
  "Move to next `bongo' playlist custom heading."
  (format "^.*%s.*$" prot-bongo-playlist-heading-delimiter)
  're-search-forward
  'point-at-eol)

(prot-bongo-playlist-motion
  prot-bongo-playlist-heading-previous
  "Move to previous `bongo' playlist custom heading."
  (format "^.*%s.*$" prot-bongo-playlist-heading-delimiter)
  're-search-backward
  'point-at-bol)

(defun prot-bongo--section-delimiter-string ()
  "Format regexp for `prot-bongo-playlist-section-delimiter'."
  (let* ((string prot-bongo-playlist-section-delimiter)
         (char (regexp-quote (substring string 0 1))))
    (format "^%s+$" char)))

(prot-bongo-playlist-motion
  prot-bongo-playlist-section-next
  "Move to next `bongo' playlist custom section delimiter."
  (prot-bongo--section-delimiter-string)
  're-search-forward
  'point-at-eol)

(prot-bongo-playlist-motion
  prot-bongo-playlist-section-previous
  "Move to previous `bongo' playlist custom section delimiter."
  (prot-bongo--section-delimiter-string)
  're-search-backward
  'point-at-bol)

;;;###autoload
(defun prot-bongo-playlist-mark-section ()
  "Mark `bongo' playlist section, delimited by custom markers.
The marker is `prot-bongo-playlist-delimiter'."
  (interactive)
  (let ((section (prot-bongo--section-delimiter-string)))
    (search-forward-regexp section nil t)
    (push-mark nil t)
    (forward-line -1)
    ;; REVIEW any predicate to replace this `save-excursion'?
    (if (save-excursion (re-search-backward section nil t))
        (progn
          (search-backward-regexp section nil t)
          (forward-line 1))
      (goto-char (point-min)))
    (activate-mark)))

(declare-function bongo-kill "bongo")

;;;###autoload
(defun prot-bongo-playlist-kill-section ()
  "Kill `bongo' playlist-section at point.
This operates on a custom delimited section of the buffer.  See
`prot-bongo-playlist-kill-section'."
  (interactive)
  (prot-bongo-playlist-mark-section)
  (bongo-kill))

;;;; Imenu setup for custom sections

(defvar prot-bongo-playlist-setup-hook nil
  "Hook that runs after inserting items to the Bongo playlist.
See, for example, `prot/bongo-playlist-insert-playlist-file' or
`prot/bongo-dired-insert-files'.")

(defun prot-bongo--playlist-imenu-heading ()
  "Return the text of the custom `bongo' playlist heading."
  (let* ((string prot-bongo-playlist-heading-delimiter)
         (char (substring string 0 1)))
    (nth 1
         (split-string
          (buffer-substring-no-properties (point-at-bol) (point-at-eol))
          (concat char " ")))))

;;;###autoload
(defun prot-bongo-imenu-setup (&optional negation)
  "Set up `imenu' bindings for the Bongo playlist buffer.
With optional NEGATION, remove them."
  (if negation
      (progn
        (dolist (local '(imenu-prev-index-position-function
                         imenu-extract-index-name-function))
          (kill-local-variable local))
        (remove-hook 'prot-bongo-playlist-setup-hook #'prot-bongo-imenu-setup))
    (add-hook 'prot-bongo-playlist-setup-hook #'prot-bongo-imenu-setup)
    (setq-local imenu-prev-index-position-function
                'prot-bongo-playlist-heading-previous)
    (setq-local imenu-extract-index-name-function
                'prot-bongo--playlist-imenu-heading)))

;;;; Commands

(declare-function bongo-erase-buffer "bongo")
(declare-function bongo-library-buffer-p "bongo")
(declare-function bongo-play-random "bongo")
(declare-function bongo-playing-p "bongo")
(declare-function bongo-playlist-buffer "bongo")
(declare-function bongo-playlist-buffer-p "bongo")
(declare-function bongo-progressive-playback-mode "bongo")
(declare-function bongo-random-playback-mode "bongo")
(declare-function bongo-recenter "bongo")
(declare-function bongo-reset-playlist "bongo")
(declare-function bongo-stop "bongo")

;;;###autoload
(defun prot-bongo-playlist-play-random ()
  "Play random `bongo' track and determine further conditions."
  (interactive)
  (unless (bongo-playlist-buffer)
    (bongo-playlist-buffer))
  (when (or (bongo-playlist-buffer-p)
            (bongo-library-buffer-p))
    (unless (bongo-playing-p)
      (with-current-buffer (bongo-playlist-buffer)
        (bongo-play-random)
        (bongo-random-playback-mode)
        (bongo-recenter)))))

(defvar bongo-next-action)

;;;###autoload
(defun prot-bongo-playlist-random-toggle ()
  "Toggle `bongo-random-playback-mode' in playlist buffers."
  (interactive)
  (if (eq bongo-next-action 'bongo-play-random-or-stop)
      (bongo-progressive-playback-mode)
    (bongo-random-playback-mode)))

;;;###autoload
(defun prot-bongo-playlist-reset ()
  "Stop playback and reset Bongo playlist.
To reset the playlist is to undo the marks produced by non-nil
`bongo-mark-played-tracks'."
  (interactive)
  (when (bongo-playlist-buffer-p)
    (bongo-stop)
    (bongo-reset-playlist)))

;;;###autoload
(defun prot-bongo-playlist-terminate ()
  "Stop playback and clear the entire `bongo' playlist buffer.
Contrary to the standard `bongo-erase-buffer', this also removes
the currently playing track."
  (interactive)
  (when (bongo-playlist-buffer-p)
    (bongo-stop)
    (bongo-erase-buffer)))

(defvar prot-bongo--playlist-history '()
  "Input history of `prot-bongo-playlist-insert-playlist-file'.")

(defun prot-bongo--playlist-prompt ()
  "Prompt for a file in `prot-bongo-playlist-directory'.
Helper function for `prot-bongo-playlist-insert-playlist-file'."
  (let* ((path prot-bongo-playlist-directory)
         (dotless directory-files-no-dot-files-regexp)
         (playlists (mapc
                       'abbreviate-file-name
                       (directory-files path nil dotless))))
    (completing-read-multiple
     "Add playlist: " playlists nil t nil 'prot-bongo--playlist-history)))

(declare-function bongo-insert-playlist-contents "bongo")

;;;###autoload
(defun prot-bongo-playlist-insert-playlist-file ()
  "Insert contents of playlist file to a `bongo' playlist.
Upon insertion, playback starts immediately, in accordance with
`prot-bongo-play-random'.

The available options at the completion prompt are pre-configured
files that contain absolute filesystem paths of directories or
media items one per line.  Think of them as meta-directories that
mix manually selected media items (yes, I never liked 'smart'
playlists).

To insert multiple playlists complete the first, then type a
character that matches `crm-separator' to complete the second,
and so on.

Also see `prot-bongo-dired-make-playlist-file'."
  (interactive)
  (let ((path prot-bongo-playlist-directory))
    (unless (file-directory-p path)
      (error "'%s' is not an existing directory" path))
    (let ((choice
           (if (not (bongo-playlist-buffer-p (current-buffer)))
               (user-error "Not in a `bongo' playlist buffer")
             (prot-bongo--playlist-prompt))))
      (mapc (lambda (x)
              (save-excursion
                (goto-char (point-max))
                (prot-bongo-playlist-heading x "playlist file")
                (bongo-insert-playlist-contents
                 (format "%s%s" path x))
                (prot-bongo-playlist-section)))
            choice)
      (prot-bongo-playlist-play-random)
      (run-hooks 'prot-bongo-playlist-setup-hook))))

;;;; Setup for track changing

(defvar prot-bongo-playlist-change-track-hook nil
  "Hook that runs after `bongo' switches to a new track.")

(defun prot-bongo-playlist-run-hook-change-track (&rest _)
  "Run `prot-bongo-playlist-run-hook-change-track'.
This is meant to be loaded after the relevant `bongo' functions
that change tracks, such as `bongo-play-next-or-stop' and
`bongo-play-random-or-stop'."
  (run-hooks 'prot-bongo-playlist-change-track-hook))

(dolist (fn '(bongo-play-next-or-stop bongo-play-random-or-stop))
  (advice-add fn :after #'prot-bongo-playlist-run-hook-change-track))

;;;###autoload
(defun prot-bongo-playlist-recenter ()
  "Recenter `bongo' playlist buffer while in a live window.
Add to `prot-bongo-playlist-change-track-hook'."
  (with-current-buffer (bongo-playlist-buffer)
    (bongo-recenter)))

;;;; Bongo + Dired (bongo library buffer)

(declare-function bongo-dired-library-mode "bongo")
(declare-function bongo-insert-directory-tree "bongo")
(declare-function bongo-insert-file "bongo")
(declare-function bongo-library-buffer "bongo")
(autoload 'dired-filename-at-point "bongo")
(autoload 'dired-get-marked-files "bongo")
(autoload 'dired-next-line "bongo")

(defmacro prot-bongo-dired-library (name doc val)
  "Create Bongo library function NAME with DOC and VAL."
  (declare (indent defun))
  `(defun ,name ()
     ,doc
     (when (string-match-p (file-truename bongo-default-directory)
                           (file-truename default-directory))
       (bongo-dired-library-mode ,val))))

(prot-bongo-dired-library
  prot-bongo-dired-library-enable
  "Set `bongo-dired-library-mode' when accessing ~/Music.

Add this to `dired-mode-hook'.  Upon activation, the directory
and all its sub-directories become a valid library buffer for
Bongo, from where we can, among others, add tracks to playlists.
The added benefit is that Dired will continue to behave as
normal, making this a superior alternative to a purpose-specific
library buffer.

Note, though, that this will interfere with `wdired-mode'.  See
`prot-bongo-dired-library-disable'."
  1)

(prot-bongo-dired-library
  prot-bongo-dired-library-disable
  "Disable `bongo-dired-library-mode' when accessing ~/Music.
This should be added `wdired-mode-hook'.  For more, refer to
`prot-bongo-dired-library-enable'."
  -1)

(advice-add 'wdired-finish-edit :after #'prot-bongo-dired-library-enable)

(defun prot-bongo--dired-insert-files ()
  "Add files in a `dired' buffer to the `bongo' playlist."
  (let ((media (or (dired-get-marked-files) (dired-filename-at-point))))
    (with-current-buffer (bongo-playlist-buffer)
      (goto-char (point-max))
      (mapc (lambda (x)
              (if (file-directory-p x)
                  (progn
                    (prot-bongo-playlist-heading (file-name-base x))
                    (bongo-insert-directory-tree x))
                (bongo-insert-file x)))
            media)
      (prot-bongo-playlist-section)
      (run-hooks 'prot-bongo-playlist-setup-hook))
    (with-current-buffer (bongo-library-buffer)
      (dired-next-line 1))))

;;;###autoload
(defun prot-bongo-dired-insert ()
  "Add `dired' item at point or marked ones to Bongo playlist.

The playlist buffer is created, if necessary, while some other
tweaks are introduced.  See `prot-bongo--dired-insert-files' as
well as `prot-bongo-playlist-play-random'.

Meant to work while inside a `dired' buffer that doubles as a
library buffer (see `prot-bongo-dired-library-enable')."
  (interactive)
  (when (bongo-library-buffer-p)
    (unless (bongo-playlist-buffer-p)
      (bongo-playlist-buffer))
    (prot-bongo--dired-insert-files)
    (prot-bongo-playlist-play-random)))

;;;###autoload
(defun prot-bongo-dired-make-playlist-file ()
  "Add `dired' marked items to playlist file using completion.

Files are stored in `prot-bongo-playlist-directory'.  These are
meant to reference filesystem paths: one path per line.  They
ease the task of playing media from closely related directory
trees, without having to interfere with the user's directory
structure (e.g. a playlist file 'rock' can include the paths of
~/Music/Scorpions and ~/Music/Queen).

This works by appending the absolute filesystem path of each item
to the selected playlist file.  If no Dired marked items are
available, the item at point will be used instead.

Forcibly selecting a non-existent file at the prompt will create
a new entry whose name matches the minibuffer input.

Also see `prot-bongo-playlist-insert-playlist-file'."
  (interactive)
  (let* ((dotless directory-files-no-dot-files-regexp)
         (pldir prot-bongo-playlist-directory)
         (playlists (mapcar
                     'abbreviate-file-name
                     (directory-files pldir nil dotless)))
         (plname (completing-read "Select playlist: " playlists nil t))
         (plfile (concat pldir plname))
         (media-paths
          (if (derived-mode-p 'dired-mode)
              ;; TODO more efficient way to do ensure newline ending?
              ;;
              ;; The issue is that we need to have a newline at the
              ;; end of the file, so that when we append again we
              ;; start on an empty line.
              (concat
               (mapconcat #'identity
                          (dired-get-marked-files)
                          "\n")
               "\n")
            (user-error "Not in a `dired' buffer"))))
    ;; The following `when' just checks for an empty string.  If we
    ;; wanted to make this more robust we should also check for names
    ;; that contain only spaces and/or invalid characters…  This is
    ;; good enough for me.
    (when (string-empty-p plname)
      (user-error "No playlist file has been specified"))
    (unless (file-directory-p pldir)
      (make-directory pldir))
    (unless (and (file-exists-p plfile)
                 (file-readable-p plfile)
                 (not (file-directory-p plfile)))
      (make-empty-file plfile))
    (append-to-file media-paths nil plfile)
    (with-current-buffer (find-file-noselect plfile)
      (delete-duplicate-lines (point-min) (point-max))
      (sort-lines nil (point-min) (point-max))
      (save-buffer)
      (kill-buffer))))

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

5.10 Elfeed feed reader, prot-elfeed.el and prot-elfeed-bongo.el

This is a standalone feed reader by Christopher Wellons that comes with good defaults and is very well designed overall. It treats the stream of updates as a flat list that can be narrowed incrementally using an efficient tagging system.

In terms of feed format specifications, i.e. Atom vs RSS, things should just work without any kind of configuration from your side (a huge plus compared to, say, Gnus' nnrss backend that only supports the latter).

Elfeed stores feed sources in a single list that associates a valid URL to one or more tags. These are then written to a database. The Elisp part users should care about looks like this:

(setq elfeed-feeds
      '(("https://www.archlinux.org/feeds/news/" linux distro)
        ("https://planet.emacslife.com/atom.xml" emacs community)
        ("https://www.ecb.europa.eu/rss/press.html" economics eu)
        ("http://feed.pippa.io/public/shows/teamhuman" podcast culture)
        ("https://www.youtube.com/feeds/videos.xml?channel_id=UC0uTPqBCFIpZxlz_Lv1tk_g" personal video)
        ("https://protesilaos.com/codelog.xml" personal)))

I keep the actual list in a GPG-encrypted file (defined in the variable prot-elfeed-feeds-file which is part of my prot-elfeed.el library that is reproduced after the following package declarations). Emacs can transparently decrypt and read gpg-protected files, making it a great way to safely store sensitive data while still keeping everything perfectly functional.

A good tagging system for your feeds will offer a strong foundation for catching up with the news. I generally use 2-3 tags per feed, while I make sure that conceptually similar items will share at least one tag. My tags are not particularly sophisticated, though they are not random either: for example, I have a particular "EU" entry for all institutions, bodies, agencies, etc. of the European Union and then I have other more general ones, such as "politics" and "economics". So, in this case, the European Central Bank gets tagged with eu, economics, while the European Parliament is eu, politics.

The user interface consists of two distinct types of buffers:

  1. The *elfeed-search* buffer that holds the list with all the news items.
  2. The individual item entries.

By default, hitting s (elfeed-search-live-filter) in the search buffer will place the point inside the minibuffer, where you can then edit the applicable filters. The Elfeed README offers a detailed explanation of how to apply such filters. The ones I use the most:

  • Prepend a + to the name of a tag to only show items that include that tag. With - show items that do not include it.
  • Type in a regular string with the equals sign in front of it, say "=TITLE", to show feeds whose name contains it.

Other common cases are regular expressions and date ranges, though I have found that I never use those. Probably because the tagging system is sufficiently powerful for my particular needs.

My only inconvenience with elfeed-search-live-filter is that it does not support completion out-of-the-box. Instead it expects full user input, which ins understandable given the types of searches it can conduct. Since I only need this facility to filter by tag, I rebind s to my prot-elfeed-search-tag-filter. While S-s (shift and s) can still be used to access the original command, whenever we need more precise control over the search filters.

The MPV-related functions require the external mpv program. They will play a video in a new app window at a resolution that matches the current setup's display width or, in the case of an enclosure (presumably a podcast), play just the audio file without popping up a new app window. The process runs in a dedicated buffer, so it can be terminated by killing the buffer. In the future I might make this cleaner, so that it understands input from, e.g., playerctl, though it is not a priority as the current simplistic design is "good enough" for my case.

For an older, albeit still relevant, demonstration of what I have here, watch my Elfeed video (2020-06-09).

As I also am a user of Bongo, and because Emacs lets one handle things with precision, there are a few functions here that are meant to make the Elfeed search buffer a bongo-aware media library, from where we can enqueue online multimedia sources (video links, or podcast enclosures). The key is to not interfere with the primary Bongo playlist and library tandem, which is dedicated to my local music collection, but to maintain a separate playlist which can be controlled independently. The entirety of my prot-elfeed-bongo.el is shared after prot-elfeed.el below the following package declarations (for my other configurations, refer to Bongo music or media manager (and prot-bongo.el)).

The placement of my custom buffers for Elfeed's multimedia output is controlled by display-buffer-alist (see Window rules and basic tweaks).

I benefited in this particular Elfeed+Bongo workflow from an email exchange I had with Madhavan Krishnan: we shared code and ideas that helped establish the modalities of interaction between Elfeed and Bongo (this information is made public with permission). Video demo with older code: Manage podcasts in Emacs with Elfeed and Bongo (2020-09-11).

Also see: Sample configuration for MPV (Elfeed+Bongo extension).

(use-package elfeed
  :straight t
  :config
  (setq elfeed-use-curl t)
  (setq elfeed-curl-max-connections 10)
  (setq elfeed-db-directory "~/.emacs.d/elfeed/")
  (setq elfeed-enclosure-default-dir "~/Downloads/")
  (setq elfeed-search-filter "@4-months-ago +unread")
  (setq elfeed-sort-order 'descending)
  (setq elfeed-search-clipboard-type 'CLIPBOARD)
  (setq elfeed-search-title-max-width 100)
  (setq elfeed-search-title-min-width 30)
  (setq elfeed-search-trailing-width 25)
  (setq elfeed-show-truncate-long-urls t)
  (setq elfeed-show-unique-buffers t)
  (setq elfeed-search-date-format '("%F %R" 16 :left))
  :bind (("C-c e" . elfeed)
         :map elfeed-search-mode-map
         ("w" . elfeed-search-yank)
         ("g" . elfeed-update)
         ("G" . elfeed-search-update--force)
         :map elfeed-show-mode-map
         ("w" . elfeed-show-yank)))

(use-package prot-elfeed
  :straight (:type built-in)
  :init
  (setq prot-elfeed-tag-faces t)
  :config
  (prot-elfeed-fontify-tags)
  :hook (elfeed-search-mode-hook . prot-elfeed-load-feeds)
  :bind (:map elfeed-search-mode-map
         ("s" . prot-elfeed-search-tag-filter)
         ("o" . prot-elfeed-search-open-other-window)
         ("q" . prot-elfeed-kill-buffer-close-window-dwim)
         ("v" . prot-elfeed-mpv-dwim)
         ("+" . prot-elfeed-toggle-tag)
         ("!" . (lambda ()
                  (interactive)
                  (prot-elfeed-toggle-tag 'important)))
         :map elfeed-show-mode-map
         ("a" . prot-elfeed-show-archive-entry)
         ("e" . prot-elfeed-show-eww)
         ("q" . prot-elfeed-kill-buffer-close-window-dwim)
         ("v" . prot-elfeed-mpv-dwim)))

(use-package prot-elfeed-bongo
  :straight (:type built-in)
  :bind (:map elfeed-search-mode-map
         ("b" . prot-elfeed-bongo-insert-item)
         ("h" . prot-elfeed-bongo-switch-to-playlist) ; "hop" mnemonic
         :map elfeed-show-mode-map
         ("b" . prot-elfeed-bongo-insert-item)))

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

;;; prot-elfeed.el --- Elfeed extensions for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; Extensions for Elfeed, intended for use in my Emacs setup:
;; https://protesilaos.com/dotemacs.

;;; Code:

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

(defgroup prot-elfeed ()
  "Personal extensions for Elfeed."
  :group 'elfeed)

(defcustom prot-elfeed-feeds-file (concat user-emacs-directory "feeds.el.gpg")
  "Path to file with `elfeed-feeds'."
  :type 'string
  :group 'prot-elfeed)

(defcustom prot-elfeed-archives-directory "~/Documents/feeds/"
  "Path to directory for storing Elfeed entries."
  :type 'string
  :group 'prot-elfeed)

(defcustom prot-elfeed-tag-faces nil
  "Add faces for certain tags.
The tags are: critical, important, personal."
  :type 'boolean
  :group 'prot-elfeed)

(defcustom prot-elfeed-laptop-resolution-breakpoint 1366
  "Determine video resolution based on this display width.
This is used to check whether I am on the laptop or whether an
external display is attached to it.  In the latter case, a
`prot-elfeed-video-resolution-large' video resolution will be
used, else `prot-elfeed-video-resolution-small'."
  :type 'integer
  :group 'prot-elfeed)

(defcustom prot-elfeed-video-resolution-small 720
  "Set video resolution width for smaller displays."
  :type 'integer
  :group 'prot-elfeed)

(defcustom prot-elfeed-video-resolution-large 1080
  "Set video resolution width for larger displays."
  :type 'integer
  :group 'prot-elfeed)

(defface prot-elfeed-entry-critical
  '((((class color) (min-colors 88) (background light))
     :inherit elfeed-search-title-face :foreground "#972500")
    (((class color) (min-colors 88) (background dark))
     :inherit elfeed-search-title-face :foreground "#f4923b")
    (t :inherit (font-lock-builtin-face elfeed-search-title-face)))
  "Face for Elfeed entries tagged 'critical'.")

(defface prot-elfeed-entry-important
  '((((class color) (min-colors 88) (background light))
     :inherit elfeed-search-title-face :foreground "#315b00")
    (((class color) (min-colors 88) (background dark))
     :inherit elfeed-search-title-face :foreground "#70c900")
    (t :inherit (font-lock-string-face elfeed-search-title-face)))
  "Face for Elfeed entries tagged 'important'.")

(defface prot-elfeed-entry-personal
    '((((class color) (min-colors 88) (background light))
     :inherit elfeed-search-title-face :foreground "#8f0075")
    (((class color) (min-colors 88) (background dark))
     :inherit elfeed-search-title-face :foreground "#f78fe7")
    (t :inherit (font-lock-keyword-face elfeed-search-title-face)))
  "Face for Elfeed entries tagged '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-search-tags
  '(critical essential important)
  "List of tags used by `prot-elfeed-toggle-tag'.")

(declare-function elfeed-search-toggle-all "elfeed")

;;;###autoload
(defun prot-elfeed-toggle-tag (&optional tag)
  "Toggle tag on current item.

A list of tags is provided by `prot-elfeed-search-tags'.
Otherwise an optional TAG symbol will suffice."
  (interactive)
  (let* ((tags prot-elfeed-search-tags)
         (input (or tag (intern (completing-read "Set tag: " tags nil t)))))
    (elfeed-search-toggle-all input)))

(defvar elfeed-show-entry)
(defvar elfeed-show-truncate-long-urls)
(declare-function elfeed-entry-title "elfeed")
(declare-function elfeed-show-refresh "elfeed")

;;;###autoload
(defun prot-elfeed-show-archive-entry ()
  "Store a plain text copy of the current `elfeed' entry.

The destination is defined in `prot-elfeed-archives-directory'
and will be created if it does not exist."
  (interactive)
  (let* ((entry (if (eq major-mode 'elfeed-show-mode)
                    elfeed-show-entry
                  (elfeed-search-selected :ignore-region)))
         (title (elfeed-entry-title entry))
         (elfeed-show-truncate-long-urls nil)
         (archives (file-name-as-directory prot-elfeed-archives-directory))
         (file (format "%s%s.txt" archives title)))
    (unless (file-exists-p archives)
      (make-directory archives t))
    (when (derived-mode-p 'elfeed-show-mode)
      ;; Refresh to expand truncated URLs
      (elfeed-show-refresh)
      (write-file file t)
      (message "Saved buffer at %s" file))))

;;;; General commands

(defvar elfeed-show-entry)
(declare-function elfeed-search-selected "elfeed")
(declare-function elfeed-entry-link "elfeed")

;;;###autoload
(defun prot-elfeed-show-eww (&optional link)
  "Browse current entry's link or optional LINK in `eww'.

Only show the readable part once the website loads.  This can
fail on poorly-designed websites."
  (interactive)
  (let* ((entry (if (eq major-mode 'elfeed-show-mode)
                    elfeed-show-entry
                  (elfeed-search-selected :ignore-region)))
         (link (or link (elfeed-entry-link entry))))
    (eww link)
    (add-hook 'eww-after-render-hook 'eww-readable nil t)))

(declare-function elfeed-search-untag-all-unread "elfeed")
(declare-function elfeed-search-show-entry "elfeed")

;;;###autoload
(defun prot-elfeed-search-open-other-window (&optional arg)
  "Browse `elfeed' entry in the other window.
With optional prefix ARG (\\[universal-argument]) browse the
entry in `eww' using the `prot-elfeed-show-eww' wrapper."
  (interactive "P")
  (let* ((entry (if (eq major-mode 'elfeed-show-mode)
                    elfeed-show-entry
                  (elfeed-search-selected :ignore-region)))
         (link (elfeed-entry-link entry))
         (win (selected-window)))
    (with-current-buffer (get-buffer "*elfeed-search*")
      (unless (one-window-p)              ; experimental
        (delete-other-windows win))
      (split-window win (/ (frame-height) 5) 'below)
      (other-window 1)
      (if arg
          (progn
            (when (eq major-mode 'elfeed-search-mode)
              (elfeed-search-untag-all-unread))
            (prot-elfeed-show-eww link))
        (elfeed-search-show-entry entry)))))

(declare-function elfeed-kill-buffer "elfeed")
(declare-function elfeed-search-quit-window "elfeed")

;;;###autoload
(defun prot-elfeed-kill-buffer-close-window-dwim ()
  "Do-what-I-mean way to handle `elfeed' windows and buffers.

When in an entry buffer, kill the buffer and return to the Elfeed
Search view.  If the entry is in its own window, delete it as
well.

When in the search view, close all other windows.  Else just kill
the buffer."
  (interactive)
  (let ((win (selected-window)))
    (cond ((eq major-mode 'elfeed-show-mode)
           (elfeed-kill-buffer)
           (unless (one-window-p) (delete-window win))
           (switch-to-buffer "*elfeed-search*"))
          ((eq major-mode 'elfeed-search-mode)
           (if (one-window-p)
               (elfeed-search-quit-window)
             (delete-other-windows win))))))

(defvar elfeed-search-filter-active)
(defvar elfeed-search-filter)
(declare-function elfeed-db-get-all-tags "elfeed")
(declare-function elfeed-search-update "elfeed")
(declare-function elfeed-search-clear-filter "elfeed")

;;;###autoload
(defun prot-elfeed-search-tag-filter ()
  "Filter Elfeed search buffer by tags using completion.

Completion accepts multiple inputs, delimited by `crm-separator'.
Arbitrary input is also possible, but you may have to exit the
minibuffer with something like `exit-minibuffer'."
  (interactive)
  (unwind-protect
      (elfeed-search-clear-filter)
    (let* ((elfeed-search-filter-active :live)
           (db-tags (elfeed-db-get-all-tags))
           (plus-tags (mapcar (lambda (tag)
                                (format "+%s" tag))
                              db-tags))
           (minus-tags (mapcar (lambda (tag)
                                 (format "-%s" tag))
                               db-tags))
           (all-tags (delete-dups (append plus-tags minus-tags)))
           (tags (completing-read-multiple "Apply one or more tags: " all-tags nil t))
           (input (string-join `(,elfeed-search-filter ,@tags) " ")))
      (setq elfeed-search-filter input))
    (elfeed-search-update :force)))

;;;; Elfeed multimedia extras

(defvar prot-elfeed-mpv-buffer-name "*prot-elfeed-mpv-output*"
  "Name of buffer holding Elfeed MPV output.")

(defun prot-elfeed--video-resolution ()
  "Determine display resolution.
This checks `prot-elfeed-laptop-resolution-breakpoint'."
  (if (<= (display-pixel-width) prot-elfeed-laptop-resolution-breakpoint)
      prot-elfeed-video-resolution-small
    prot-elfeed-video-resolution-large))

(defun prot-elfeed--get-mpv-buffer ()
  "Prepare `prot-elfeed-mpv-buffer-name' buffer."
  (let ((buf (get-buffer prot-elfeed-mpv-buffer-name))
        (inhibit-read-only t))
    (with-current-buffer buf
      (erase-buffer))))

(declare-function elfeed-entry-enclosures "elfeed")

;;;###autoload
(defun prot-elfeed-mpv-dwim ()
  "Play entry link with the external MPV program.
When there is an audio enclosure (assumed to be a podcast), play
just the audio.  Else spawn a video player at a resolution that
accounts for the current monitor's width."
  (interactive)
  (let* ((entry (if (eq major-mode 'elfeed-show-mode)
                    elfeed-show-entry
                  (elfeed-search-selected :ignore-region)))
         (link (elfeed-entry-link entry))
         (enclosure (elt (car (elfeed-entry-enclosures entry)) 0)) ; fragile?
         (audio "--no-video")
         ;; Here the display width checks if I am on the laptop
         (height (prot-elfeed--video-resolution))
         (video                       ; this assumes mpv+youtube-dl
          (format "--ytdl-format=bestvideo[height\\<=?%s]+bestaudio/best" height))
         (buf (pop-to-buffer prot-elfeed-mpv-buffer-name)))
    (prot-elfeed--get-mpv-buffer)
    (if enclosure
        (progn
          (async-shell-command (format "mpv %s %s" audio enclosure) buf)
          (message "Launching MPV for %s" enclosure))
      (async-shell-command (format "mpv %s %s" video link) buf)
      (message "Launching MPV for %s" link))))

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

And here is prot-elfeed-bongo.el (part of my dotfiles' repo):

;;; prot-elfeed-bongo.el --- Bongo+Elfeed integration for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; Extensions for integrating Elfeed with Bongo, intended for use in my
;; Emacs setup: https://protesilaos.com/dotemacs.

;;; Code:

;; XXX Written on 2021-01-18.  Remains to be reviewed.

(eval-when-compile (require 'subr-x))
(when (featurep 'bongo)
  (require 'bongo))
(when (featurep 'elfeed)
  (require 'elfeed))

(defgroup prot-elfeed-bongo ()
  "Personal extensions for Bongo."
  :group 'bongo)

(defcustom prot-elfeed-bongo-playlist "*prot-elfeed-bongo-queue*"
  "Name of the Elfeed+Bongo multimedia playlist."
  :type 'string
  :group 'prot-elfeed-bongo)

(autoload 'bongo-insert-comment-text "bongo")
(autoload 'bongo-insert-uri "bongo")
(autoload 'bongo-playlist-buffer "bongo")
(autoload 'bongo-playlist-buffer-p "bongo")
(autoload 'bongo-playlist-mode "bongo")
(autoload 'bongo-progressive-playback-mode "bongo")
(autoload 'bongo-recenter "bongo")
(autoload 'define-bongo-backend "bongo")
(autoload 'elfeed-entry-enclosures "elfeed")
(autoload 'elfeed-entry-link "elfeed")
(autoload 'elfeed-entry-title "elfeed")
(autoload 'elfeed-search-selected "elfeed")
(defvar elfeed-show-entry)
(defvar prot-elfeed-bongo-mpv)

;; Here we define a slightly tweaked variant of the standard mpv
;; backend.  We will be using this to play back audio and video URLs.
;; The latter will spawn a new MPV player window.  Refer to my Elfeed
;; section for the implementation details.
(define-bongo-backend prot-elfeed-bongo-mpv
  ;; :constructor 'bongo-start-mpv-player
  :program-name 'mpv
  :extra-program-arguments nil
  :matcher '((local-file "file:" "http:" "ftp:")
             "ogg" "flac" "mp3" "mka" "wav" "wma"
             "mpg" "mpeg" "vob" "avi" "ogm" "mp4" "mkv"
             "mov" "asf" "wmv" "rm" "rmvb" "ts")
  :matcher '(("mms:" "mmst:" "rtp:" "rtsp:" "udp:" "unsv:"
              "dvd:" "vcd:" "tv:" "dvb:" "mf:" "cdda:" "cddb:"
              "cue:" "sdp:" "mpst:" "tivo:") . t)
  :matcher '(("http:" "https:") . t))

;;;###autoload
(defun prot-elfeed-bongo-insert-item ()
  "Insert `elfeed' multimedia links in `bongo' playlist buffer.

The playlist buffer has a unique name so that it will never
interfere with the default variable `bongo-playlist-buffer'."
  (interactive)
  (let* ((entry (if (eq major-mode 'elfeed-show-mode)
                    elfeed-show-entry
                  (elfeed-search-selected :ignore-region)))
         (link (elfeed-entry-link entry))
         (enclosure (elt (car (elfeed-entry-enclosures entry)) 0))
         (url (or enclosure link))
         (title (elfeed-entry-title entry))
         (bongo-pl prot-elfeed-bongo-playlist)
         (buffer (get-buffer-create bongo-pl)))
    (unless (bongo-playlist-buffer)
      (bongo-playlist-buffer))
    (display-buffer buffer)
    (with-current-buffer buffer
 	  (when (not (bongo-playlist-buffer-p))
 	    (bongo-playlist-mode)
        (setq-local bongo-library-buffer (get-buffer "*elfeed-search*"))
        (setq-local bongo-enabled-backends '(prot-elfeed-bongo-mpv))
        (bongo-progressive-playback-mode))
 	  (goto-char (point-max))
      (bongo-insert-uri url title)
      (bongo-insert-comment-text (format "     ==> %s\n" url))
      (let ((inhibit-read-only t))
        (delete-duplicate-lines (point-min) (point-max)))
      (bongo-recenter))
    (message "Enqueued %s “%s” in %s"
             (if enclosure "podcast" "video")
             (propertize title 'face 'italic)
             (propertize bongo-pl 'face 'bold))))

(defun prot-elfeed-bongo-switch-to-playlist ()
  "Switch to `prot-elfeed-bongo-playlist'."
  (interactive)
  (let* ((bongo-pl prot-elfeed-bongo-playlist)
         (buffer (get-buffer bongo-pl)))
    (if buffer
        (switch-to-buffer buffer)
      (message "No `bongo' playlist is associated with `elfeed'."))))

(provide 'prot-elfeed-bongo)
;;; prot-elfeed-bongo.el ends here

5.10.1 Sample configuration for MPV (Elfeed+Bongo extension)

In the previous section I configure Elfeed to integrate with the Bongo media manager. The external mpv executable is used to play back audio and video links. Instead of passing command-line arguments to control the settings of the player, I just add the following to my local configuration files, specifically ~/.config/mpv/mpv.conf:

hwdec=auto-safe
ytdl-format="bestvideo[height<=?1080]+bestaudio/best"

5.11 Proced (process monitor, similar to `top')

This is a built-in tool that allows you to monitor running processes and act on them accordingly. These are the basic settings I have right now. Would need to experiment with it a bit more. It works fine though.

(use-package proced
  :commands proced
  :config
  (setq proced-auto-update-flag t)
  (setq proced-auto-update-interval 1)
  (setq proced-descend t)
  (setq proced-filter 'user))

And with this nimble tool we get live narrowing of the list, based on the terms of our search.

(use-package proced-narrow
  :straight t
  :after proced
  :diminish
  :bind (:map proced-mode-map
              ("/" . proced-narrow)))

5.12 Pass interface (password-store)

The external pass program, aka "password-store", is a password manager that uses GPG and standard UNIX tools to handle passwords. Encrypted files are stored in a plain directory structure. Very simple, very nice: now all data is available with a variety of interfaces, such as standard CLI, a dmenu interface, a graphical front-end like qtpass, etc.

The package below provides an Emacs interface to some of the most common actions, in the form of a list of candidates that can be narrowed down using completion methods (study Completion framework and extras). I use it to quickly store a password to the kill ring.

(use-package password-store
  :straight t
  :commands (password-store-copy
             password-store-edit
             password-store-insert)
  :config
  (setq password-store-time-before-clipboard-restore 30))

And this one adds a major mode for browsing the pass keychain. Call it with M-x pass. There is a helpful section at the top with key bindings and their functions.

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

5.13 Emacs Simple HTML Renderer (shr)

NOTE 2020-08-16: This section is subject to major changes contingent on the eventual review of eww (which is documented in the next section: Emacs Web Wowser (EWW)).

As far as I can tell, the following shr-* variables concern an HTML parser that is used by a variety of tools, including Elfeed (refer to Elfeed feed reader and my prot-elfeed.el extras). I guess we could scope them by using hooks, but I see no need for different settings.

What these do:

  • Open links in a new Emacs window, instead of the system's browser. This Emacs web browser is called eww.
  • Use monospaced fonts, since that is what I want to have everywhere in Emacs.
  • Do not preserve colours from websites, as they may be inaccessible (see my Modus theme).
  • Keep images to 70% of the window. This number is arbitrary. It just feels like a good upper limit (not a fan of decorative images inside of blog posts).
  • Line length at same number of characters as fill-column (defined in the section about Line length (column count) at 72).
(use-package shr
  :config
  (setq shr-use-fonts nil)
  (setq shr-use-colors nil)
  (setq shr-max-image-proportion 0.7)
  (setq shr-image-animate nil)
  (setq shr-width (current-fill-column)))

5.14 Emacs Web Wowser (EWW)

;; TODO 2021-01-19: Everything about eww is subject to review.  It is
;; not in a good state.
(use-package eww
  :config
  (setq eww-restore-desktop nil)
  (setq eww-desktop-remove-duplicates t)
  (setq eww-header-line-format "%u")
  (setq eww-search-prefix "https://duckduckgo.com/html/?q=")
  (setq eww-download-directory "~/Downloads/")
  (setq eww-suggest-uris
        '(eww-links-at-point
          thing-at-point-url-at-point))
  (setq eww-bookmarks-directory "~/.emacs.d/eww-bookmarks/")
  (setq eww-history-limit 150)
  (setq eww-use-external-browser-for-content-type
        "\\`\\(video/\\|audio/\\|application/pdf\\)")
  (setq eww-browse-url-new-window-is-tab nil)
  (setq eww-form-checkbox-selected-symbol "[X]")
  (setq eww-form-checkbox-symbol "[ ]")

  (defun prot/eww-visit-history (&optional arg)
    "Revisit a URL from `eww-prompt-history' using completion.
With \\[universal-argument] produce a new buffer."
    (interactive "P")
    (let ((history eww-prompt-history)  ; eww-bookmarks
          (new (if arg t nil)))
      (eww
       (completing-read "Visit website from history: " history nil t)
       new)))

  ;; eww-view-source

  (defvar prot/eww-mode-global-map
    (let ((map (make-sparse-keymap)))
      (define-key map "s" 'eww-search-words)
      (define-key map "o" 'eww-open-in-new-buffer)
      (define-key map "f" 'eww-open-file)
      (define-key map "w" 'prot/eww-visit-history)
      map)
    "Key map to scope `eww' bindings for global usage.
The idea is to bind this to a prefix sequence, so that its
defined keys follow the pattern of <PREFIX> <KEY>.")
  :bind-keymap ("C-c w" . prot/eww-mode-global-map)
  :bind (:map eww-mode-map
              ("n" . next-line)
              ("p" . previous-line)
              ("f" . forward-char)
              ("b" . backward-char)
              ("a" . prot/eww-org-archive-current-url)
              ("B" . eww-back-url)
              ("N" . eww-next-url)
              ("P" . eww-previous-url)))

(use-package browse-url
  :after eww
  :config
  (setq browse-url-browser-function 'eww-browse-url))

6 General interface and interactions

This section contains configurations for all aspects of the Emacs user interface, as well lots of small or self-contained tweaks that cover a wide range of built-in libraries.

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

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

Check the package upstream for information on the supported modes and on how to contribute your own extensions.

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

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

6.2 Go to last change

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

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

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

6.3 Mode line

The mode line is an integral part of the Emacs interface. While there are lots of third party packages that style it in a variety of ways, I find the default to be "good enough".

In the code snippet right below I reshuffle some of the mode line indicators. Nothing too fancy. The mode-line-defining-kbd-macro is tweaked to use a more appropriate string for its indicator and to apply colours that are designed specifically for the mode line (the default uses the generic font-lock warning face).

Note that in Custom extensions for "focus mode" (prot-logos.el) I provide a snippet that optionally toggles the visibility of the modeline while entering a bespoke "focus mode" for writing/reading.

(use-package emacs
  :init
  (setq mode-line-percent-position '(-3 "%p"))
  (setq mode-line-position-column-line-format '(" %l,%c")) ; Emacs 28
  (setq mode-line-defining-kbd-macro
        (propertize " Macro" 'face 'mode-line-emphasis))

  ;; Thanks to Daniel Mendler for this!  It removes the square brackets
  ;; that denote recursive edits in the modeline.  I do not need them
  ;; because I am using Daniel's `recursion-indicator':
  ;; <https://github.com/minad/recursion-indicator>.
  (setq-default mode-line-modes
                (seq-filter (lambda (s)
                              (not (and (stringp s)
                                        (string-match-p
                                         "^\\(%\\[\\|%\\]\\)$" s))))
                            mode-line-modes))

  (setq mode-line-compact t)            ; Emacs 28
  (setq-default mode-line-format
                '("%e"
                  mode-line-front-space
                  mode-line-mule-info
                  mode-line-client
                  mode-line-modified
                  mode-line-remote
                  mode-line-frame-identification
                  mode-line-buffer-identification
                  "  "
                  mode-line-position
                  mode-line-modes
                  "  "
                  (vc-mode vc-mode)
                  " "
                  mode-line-misc-info
                  mode-line-end-spaces)))

In the following sub-sections I provide customisations for some tools that place information on the mode line. Again, nothing flamboyant.

6.3.1 Moody.el (simple mode line configuration utility)

moody.el is a lightweight library for adding some flair to the mode line. I have been using it on and off to make sure that it works well with my themes (see Modus themes (my highly accessible themes)).

My fairly minor tweaks in prot-moody.el (reproduced further below) align Moody with my Font configurations (prot-fonts.el). What I basically want is to make the mode line gracefully adapt to changes in font size.

(use-package moody :straight t)

(use-package prot-moody
  :straight (:type built-in)
  :after (moody
          prot-fonts              ; we use its hook
          prot-common)            ; provides `prot-common-number-even-p'
  :config
  (prot-moody-set-height -1))

Here are my tweaks (from my dotfiles' repo):

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

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; This covers my moody.el extensions, for use in my Emacs setup:
;; https://protesilaos.com/dotemacs.

;;; Code:

(require 'prot-common)
(require 'prot-fonts)
(when (featurep 'moody)
  (require 'moody))

(defgroup prot-moody ()
  "Tweaks for moody.el."
  :group 'mode-line)

(defun prot-moody--height ()
  "Set Moody height to an even number.
Bind this to a hook that gets called after loading/changing the
mode line's typeface (or the default one if they are the same)."
  (let* ((font (face-font 'mode-line))
         (height (truncate (* 1.65 (aref (font-info font) 2))))
         (height-even (if (prot-common-number-even-p height) height (+ height 1))))
    (if font
        height-even
      24)))

(defvar moody-mode-line-height)

(defun prot-moody--mode-line-height ()
  "Set Moody height to the value of `prot-moody--height'."
  (let ((height (prot-moody--height)))
    (setq moody-mode-line-height height)))

(autoload 'moody-replace-mode-line-buffer-identification "moody")
(autoload 'moody-replace-vc-mode "moody")

;;;###autoload
(define-minor-mode prot-moody-set-height
  "Toggle Moody for the mode line and configure its fonts."
  :init-value nil
  :global t
  (if prot-moody-set-height
      (progn
        (moody-replace-mode-line-buffer-identification)
        (moody-replace-vc-mode)
        (add-hook 'prot-fonts-set-typeface-hook #'prot-moody--mode-line-height)
        (run-hooks 'prot-fonts-set-typeface-hook))
    (let ((format (default-value 'mode-line-format)))
      (when (member 'moody-mode-line-buffer-identification format)
        (moody-replace-mode-line-buffer-identification 'reverse))
      (when (member '(vc-mode moody-vc-mode) format)
        (moody-replace-vc-mode 'reverse)))
    (remove-hook 'prot-fonts-set-typeface-hook #'prot-moody--mode-line-height)))

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

6.3.2 Mode line recursion indicators

This utility by Daniel Mendler provides a couple of indicators for denoting minibuffer recursion. They offer a reminder that we are in the midst of a recursive editing session when we should, perhaps, not be in one. I consider recursion-indicator complementary to what is already built into Emacs in the form of minibuffer-depth-indicate-mode which shows the level of recursion at the current minibuffer prompt (refer to Minibuffer configurations and extras (prot-minibuffer.el)).

(use-package recursion-indicator
  :straight (:host github :repo "minad/recursion-indicator" :branch "master")
  :config
  (setq recursion-indicator-general "&")
  (setq recursion-indicator-minibuffer "@")
  (recursion-indicator-mode 1))

6.3.3 Battery status

Emacs offers a built-in library for presenting information about the status of the laptop's battery. Using it allows me to eliminate my reliance on the system panel and thus keep Emacs in full screen view without any interruptions.

The default update interval is set to a single minute (in seconds), which is generally fine though I find that a slightly higher value works just as well. As for the format, it is designed to show a context-dependent, single character indicator about the current status, as well as the battery's overall percentage.

Variable battery-mode-line-limit will hide the indicator if the value is above the declared threshold. 95 basically means "full" for me. I use that instead of a 100 because sometimes the battery only ever fills up to a lower threshold, meaning that the indicator remains present at all times.

(use-package battery
  :config
  (setq battery-mode-line-format " [%b%p%%]")
  (setq battery-mode-line-limit 95)
  (setq battery-update-interval 180)
  (setq battery-load-low 20)
  (setq battery-load-critical 10)
  :hook (after-init-hook . display-battery-mode))

6.3.4 Display current time

I normally use Emacs in fullscreen view. No system panels, no window decorations, no icons and blinking indicators. Nothing to distract me. While I really like this environment, sometimes I need to take a look at the time… Thankfully Emacs offers a convenient, built-in way of displaying such information in the mode line.

The display-time-format can be configured to show the current date and time in all the various formats we would expect, using a string of specifiers (find the docs with C-h v format-time-string). Setting its value to nil means that the information on display will be the combined result of display-time-24hr-format and display-time-day-and-date. I prefer to just write a string directly, keeping those two inactive.

The display-time-mode can output more than just the current time. It also shows the load average and an email indicator. I only need the time and date. The rest is noise.

Sometimes I need to check the current time on various timezones. This library's world-clock command gets the job done.

(use-package time
  :commands world-clock
  :config
  (setq display-time-format "%H:%M  %Y-%m-%d")
  ;;;; Covered by `display-time-format'
  ;; (setq display-time-24hr-format t)
  ;; (setq display-time-day-and-date t)
  (setq display-time-interval 60)
  (setq display-time-mail-directory nil)
  (setq display-time-default-load-average nil)

;;; World clock
  (setq zoneinfo-style-world-list
        '(("America/Los_Angeles" "Los Angeles")
          ("America/New_York" "New York")
          ("Europe/Brussels" "Brussels")
          ("Asia/Shanghai" "Shanghai")
          ("Asia/Tokyo" "Tokyo")))

  ;; All of the following variables are for Emacs 28
  (setq world-clock-list t)
  (setq world-clock-time-format "%R %z  %A %d %B")
  (setq world-clock-buffer-name "*world-clock*") ; Placement handled by
                                                 ; `display-buffer-alist'
  (setq world-clock-timer-enable t)
  (setq world-clock-timer-second 60)

  :hook (after-init-hook . display-time-mode))

6.3.5 Keycast mode

Once enabled, this package uses the mode line to show the keys being pressed and the command they call. It is quite useful for screen casting.

The placement of the indicator is controlled by keycast-window-predicate which I set to the current window. The moody.el library offers that specific piece of functionality (though I also configure Moody for its primary purpose of styling the mode line).

The tweaks to the keycast-substitute-alist prevent the display of self-inserting characters and some other commands that are not particularly useful while screen casting. Now the indicator will only show commands, which looks cleaner. I got the idea and original piece of Elisp from the dotfiles of André Alexandre Gomes and then added a few tweaks of my own.

(use-package keycast
  :straight t
  :after moody
  :commands keycast-mode
  :config
  (setq keycast-window-predicate 'moody-window-active-p)
  (setq keycast-separator-width 1)
  (setq keycast-insert-after
        (if prot-moody-set-height
            'moody-mode-line-buffer-identification
          'mode-line-buffer-identification))
  (setq keycast-remove-tail-elements nil)

  (dolist (input '(self-insert-command
                   org-self-insert-command))
    (add-to-list 'keycast-substitute-alist `(,input "." "Typing…")))

  ;;; TODO identify all events that should be excluded
  ;; mouse-set-point
  ;; mouse-set-region
  ;; mouse-drag-secondary
  ;; mouse-drag-line
  ;; mouse-drag-drag
  ;; mouse-start-end
  ;; mouse-drag-region nil
  ;; mouse-drag-track nil
  ;; mouse-drag-region-rectangle
  ;; mouse-drag-and-drop-region
  ;; mwheel-event-button
  ;; dframe-mouse-event-p
  ;; mouse-drag-events-are-point-events-p

  (dolist (event '(mouse-event-p
                   mouse-movement-p
                   mwheel-scroll))
    (add-to-list 'keycast-substitute-alist `(,event nil))))

6.4 Window divider mode

This is a built-in mode that draws vertical window borders in a slightly different way than the default, which I find more consistent. Only using it because of that, though it can also adjust the size of the borders as well as their placement.

(use-package emacs
  :config
  (setq window-divider-default-right-width 1)
  (setq window-divider-default-bottom-width 1)
  (setq window-divider-default-places 'right-only)
  :hook (after-init-hook . window-divider-mode))

6.5 Fringe mode

The fringes are areas to the right and left side of an Emacs frame. They can be used to show status-related or contextual feedback such as line truncation indicators, continuation lines, code linting markers, etc.

The default fringe width (nil) is 8 pixels on either side, which I approve of. It is possible to set the value of the fringe-mode to something like '(10 . 5) which applies the varied width to the left and right side respectively. Otherwise, we can use a single integer that controls both sides.

The use of setq-default is necessary, otherwise these values become buffer-local.

(use-package fringe
  :config
  (fringe-mode nil)
  (setq-default fringes-outside-margins nil)
  (setq-default indicate-buffer-boundaries nil)
  (setq-default indicate-empty-lines nil)
  (setq-default overflow-newline-into-fringe t))

6.5.1 Diff highlights in the fringe (diff-hl)

The diff-hl package uses the left or right fringe to highlight changes in the current buffer. The indicators are colour-coded to denote whether a change is an addition, removal, or change that includes a bit of both.

The package offers some more features, such as the ability to move between diff hunks while editing the buffer. I still need to experiment with those before customising them to my liking.

At any rate, this package is meant as a general tool for version control systems, rather than a git-specific one. Much like the built-in VC (see section on Version control framework (vc.el and prot-vc.el)).

(use-package diff-hl
  :straight t
  :after vc
  :config
  (setq diff-hl-draw-borders nil)
  (setq diff-hl-side 'left)
  ;; TODO 2021-01-19: write toggle for diff-hl-mode
  :hook (after-init-hook . global-diff-hl-mode))

6.6 Highlight comment keywords (hl-todo)

This tool by Jonas Bernoulli will apply highlighting to keywords that are normally used in code comments. Simple and effective!

(use-package hl-todo
  :straight t
  :hook (prog-mode-hook . hl-todo-mode))

6.7 Prism.el (highlight code by depth level)

This library by Adam Porter, aka "alphapapa" or "github-alphapapa", implements an alternative to the typical colouration of code. Instead of highligting the syntactic constructs, it applies colour to different levels of depth in the code. I find this particularly helpful for code that has deep levels of indentation or which I am not familiar with. M-x prism-mode is all we need.

prism.el is highly configurable. Users can specify the exact colours to use and the sequence they should appear in. This is controlled by the function prism-set-colors. While the total number is handled by the variable prism-num-faces.

In this package declaration I leverage the modus-themes-with-colors macro to pull in named colour values from my Modus themes. I also specify presets for different stylistic alternatives with 16, 8, and 4 colours respectively.

(use-package prism
  :straight (:host github :repo "alphapapa/prism.el" :branch "master")
  :defer
  :config
  (setq prism-comments nil) ; non-nil distorts colours
  (setq prism-num-faces 8)

  ;; ;; NOTE: read the manual of the `modus-themes' for prism.el

  ;; ;; for 4 colours (the closest to the default)
  ;; (prism-set-colors
  ;;   :desaturations '(0) ; may lower the contrast ratio
  ;;   :lightens '(0)      ; same
  ;;   :colors (modus-themes-with-colors
  ;;             (list fg-main
  ;;                   cyan-alt-other
  ;;                   magenta-alt-other
  ;;                   magenta)))
  ;;
  ;; ;; for 16 colours
  ;; (prism-set-colors
  ;;   :desaturations '(0) ; may lower the contrast ratio
  ;;   :lightens '(0)      ; same
  ;;   :colors (modus-themes-with-colors
  ;;             (list fg-main
  ;;                   magenta
  ;;                   cyan-alt-other
  ;;                   magenta-alt-other
  ;;                   blue
  ;;                   magenta-alt
  ;;                   cyan-alt
  ;;                   red-alt-other
  ;;                   green
  ;;                   fg-main
  ;;                   cyan
  ;;                   yellow
  ;;                   blue-alt
  ;;                   red-alt
  ;;                   green-alt-other
  ;;                   fg-special-warm)))
  ;;
  ;; for 8 colours
  (prism-set-colors
    :desaturations '(0) ; may lower the contrast ratio
    :lightens '(0)      ; same
    :colors (modus-themes-with-colors
              (list fg-special-cold
                    magenta
                    magenta-alt-other
                    cyan-alt-other
                    fg-main
                    blue-alt
                    red-alt-other
                    cyan))))

6.8 Color tools (ct.el)

The ct.el library by Github user "neeasade" provides various utilities for testing colour values across several spaces. The developer also has an interesting article on the matter: Reasoning about Colors. I may need some of those tools while developing my Modus themes.

(use-package ct
  :straight (:host github :repo "neeasade/ct.el" :branch "master"))

6.9 Rainbow mode for colour previewing (rainbow-mode.el)

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

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

6.10 Optional visual indicators or layout elements

This is a collection of modes or interfaces I seldom use or, rather, I use under special circumstances. They are useful, but there is not need for them to be available at all times.

6.10.1 Current line highlight (hl-line-mode)

This is a mode that I only activate via hooks for certain buffers where the current line itself is more important that the actual column (e.g. in Dired buffers). Here I configure it so that the highlight applies only to the current window. There is also a "global" variant, for when the equivalent mode is used (I have no plan to use that).

(use-package hl-line
  :config
  (setq hl-line-sticky-flag nil))

6.10.2 Toggles for line numbers and whitespace indicators

Display line numbers (buffer-local)
I seldom use line numbers, but here it is. This toggles the setting for the local buffer and also activates hl-line-mode.
Display invisible characters (whitespace)
Viewing invisible characters (whitespace) can be very helpful under certain circumstances. Generally though, I do not keep it active.

As for delete-trailing-whitespace, I prefer to call it manually because sometimes it causes problems, such as with diffs.

(use-package display-line-numbers
  :config
  ;; Set absolute line numbers.  A value of "relative" is also useful.
  (setq display-line-numbers-type t)
  ;; Those two variables were introduced in Emacs 27.1
  (setq display-line-numbers-major-tick 0)
  (setq display-line-numbers-minor-tick 0)
  ;; Use absolute numbers in narrowed buffers
  (setq-default display-line-numbers-widen t)

  ;; TODO 2021-01-19: this is subject to review, as it is fragile in its
  ;; current state.
  (define-minor-mode prot/display-line-numbers-mode
    "Toggle `display-line-numbers-mode' and `hl-line-mode'."
    :init-value nil
    :global nil
    (if prot/display-line-numbers-mode
        (progn
          (display-line-numbers-mode 1)
          (hl-line-mode 1)
          (setq-local truncate-lines t))
      (display-line-numbers-mode -1)
      (hl-line-mode -1)
      (setq-local truncate-lines nil)))
  :bind ("<f7>" . prot/display-line-numbers-mode))

(use-package whitespace
  :config
  (defun prot/toggle-invisibles ()
    "Toggles the display of indentation and space characters."
    (interactive)
    (if (bound-and-true-p whitespace-mode)
        (whitespace-mode -1)
      (whitespace-mode)))
  :bind (("<f6>" . prot/toggle-invisibles)
         ("C-c z" . delete-trailing-whitespace)))

6.10.3 Outline mode, outline minor mode, and extras (prot-outline.el)

The outline.el library defines a major mode (outline-mode) that is similar to org-mode in that it consists of headings which can be expanded or contracted individually or as a group. It is meant to work on plain text files, or be leveraged by other packages, that need to have some structure and the accompanying benefits of this mode. In practice, however, I never found a dedicated use for this major mode that would justify it over the more featureful Org.

Where outline.el truly shines is in the minor mode it defines (outline-minor-mode), which provides the familiar structured, heading-folding facilities in other major modes, such as emacs-lisp-mode (note: it may also work with other programming modes, though I am not a programmer so I cannot really test it). With some careful tweaks you can continue to work on your code while also benefitting from a more effective means of organising and reviewing what you have.

In practice, to make an outline for Elisp buffers, you need to start a comment line without leading spaces and make it at least three comment delimiters long (;;;). That is considered a heading level 1. Every extra delimiter will increase heading levels accordingly. Markdown headings should be recognised right away.

Now on to my custom library, prot-outline.el which builds on those concepts:

  • Provide some new commands for working with outlines. The main point of entry is, for me at least, prot-outline-cycle-dwim.
  • Define a new minor mode, prot-outline-minor-mode, which sets up a bespoke keymap as well as hooks that get fired when activating and disabling the mode. Combine it with prot-outline-minor-mode-safe that checks whether the current buffer's major mode is not a member of a blocklist. The idea is not to run this minor mode with major modes that already provide its functionality, namely, org-mode, outline-mode, markdown-mode.
  • Establish bindings with imenu.el (though this practically works with Enhanced minibuffer commands (consult.el and prot-consult.el)). This is done via the aforementioned hooks and, in my experience, yields more accurate results than the defaults. A quick reminder of why this matters: you can now navigate the outline using minibuffer completion, which is my favourite way to navigate a file I am familiar with.

Finally, an element of improved design: the outline-minor-faces package, by Jonas Bernoulli, will apply colouration to the headings produced by outline-minor-mode. These inherit from outline-mode (they are configured to look the same as the ones of Org in my Modus themes—see relevant section).

Watch my video demo of outline-minor-mode and imenu (2020-07-20), though note that it was recorder long before I wrote prot-outline.el.

(use-package prot-outline
  :straight (:type built-in)
  :diminish outline-minor-mode
  ;; key replaces `menu-bar-open', which I never use
  :bind ("<f10>" . prot-outline-minor-mode-safe))

(use-package outline-minor-faces
  :straight t
  :after prot-outline
  :hook
  (prot-outline-minor-mode-enter-hook . outline-minor-faces-add-font-lock-keywords))

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

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

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; Extensions to the built-in `outline.el' library for my Emacs
;; configuration: <https://protesilaos.com/dotemacs/>.

;;; Code:

(require 'outline)
(require 'imenu)
(require 'prot-common)

;;; Commands for outline visibility

;;;###autoload
(defun prot-outline-hide-all ()
  "Hide all `outline-mode' subtrees."
  (interactive)
  (outline-map-region 'outline-hide-subtree (point-min) (point-max)))

;;;###autoload
(defun prot-outline-down-heading ()
  "Move to the next `outline-mode' subtree."
  (interactive)
  ;; Hacky, but it kinda works.
  (outline-up-heading 1 t)
  (outline-forward-same-level 1))

;;;###autoload
(defun prot-outline-cycle-dwim ()
  "Convenience wrapper around common `outline-mode' commands.
When constructs are hidden, show everything.  While on a
headline, or an invisible part of the overlay, cycle the item's
visibility.  Else call `indent-for-tab-command'."
  (interactive)
  (cond
   ((eq (outline--cycle-state) (or 'hide-all 'headings-only))
    (outline-show-all))
   ((or (outline-on-heading-p) (outline-invisible-p))
    (outline-cycle))
   (t
    (indent-for-tab-command))))

;;; Minor mode setup

(autoload 'org-src-mode "org-src")
(defvar outline-minor-faces--font-lock-keywords)

;;;###autoload
(defun prot-outline-refontify-buffer ()
  "Re-enable the current buffer's major mode.
Add this to `prot-outline-minor-mode-exit-hook'."
  (let ((minor-modes (prot-common-minor-modes-active)))
    (when (bound-and-true-p outline-minor-faces)
      (font-lock-remove-keywords nil outline-minor-faces--font-lock-keywords))
    (when (or (derived-mode-p 'text-mode)
              (derived-mode-p 'prog-mode))
      (funcall major-mode)
      ;; REVIEW: Are there any other minor modes we need to account for?
      ;; If so, create a defvar and check it here.
      (when (member 'org-src-mode minor-modes)
        (org-src-mode))
      (message "Re-enabled %s" major-mode))))

(defvar prot-outline-minor-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "M-n") 'outline-next-visible-heading)
    (define-key map (kbd "M-p") 'outline-previous-visible-heading)
    (define-key map (kbd "C-c C-n") 'outline-next-visible-heading)
    (define-key map (kbd "C-c C-p") 'outline-previous-visible-heading)
    (define-key map (kbd "C-c C-f") 'outline-forward-same-level)
    (define-key map (kbd "C-c C-b") 'outline-backward-same-level)
    (define-key map (kbd "C-c C-a") 'outline-show-all)
    (define-key map (kbd "C-c C-q") 'prot-outline-hide-all)
    (define-key map (kbd "C-c C-u") 'outline-up-heading)
    (define-key map (kbd "C-c C-d") 'prot-outline-down-heading)
    (define-key map (kbd "<tab>") 'prot-outline-cycle-dwim)
    map)
  "Custom keymap for working with Outlines.")

(defvar prot-outline-minor-mode-enter-hook nil
  "Hook used if variable `prot-outline-minor-mode' is non-nil.")

(defvar prot-outline-minor-mode-exit-hook nil
  "Hook called when variable `prot-outline-minor-mode' is nil.")

;;;###autoload
(define-minor-mode prot-outline-minor-mode
  "Toggles `outline-minor-mode' and extras.

\\{prot-outline-minor-mode-map}"
  :init-value nil
  :lighter " =~"
  :keymap prot-outline-minor-mode-map
  :global nil
  (if prot-outline-minor-mode
      (progn
        (outline-minor-mode 1)
        (run-hooks 'prot-outline-minor-mode-enter-hook))
    (outline-minor-mode -1)
    (run-hooks 'prot-outline-minor-mode-exit-hook)))

(add-hook 'prot-outline-minor-mode-exit-hook #'prot-outline-refontify-buffer)

;; TODO: which other modes could prove problematic?
(defvar prot-outline-major-modes-blocklist '(org-mode outline-mode markdown-mode))

;;;###autoload
(defun prot-outline-minor-mode-safe ()
  "Test to set variable `prot-outline-minor-mode' to non-nil."
  (interactive)
  (let ((blocklist prot-outline-major-modes-blocklist)
        (mode major-mode))
    (when (derived-mode-p (car (member mode blocklist)))
      (error "Don't use `prot-outline-minor-mode' with `%s'" mode))
    (if (eq prot-outline-minor-mode nil)
        (prot-outline-minor-mode 1)
      (prot-outline-minor-mode -1))))

;;; Imenu bindings

(defun prot-outline-imenu-heading ()
  "Move to the previous `outline-mode' heading.
This is because `imenu' produces its index by moving backward
from the bottom."
  (outline-previous-heading))

;; FIXME: breaks `flymake-mode' (because it returns a string?)
(defun prot-outline-imenu-title ()
  "Return current line and text of the `outline-mode' heading.
To be used by `imenu-extract-index-name-function'."
  (format "%d %s"
          (line-number-at-pos nil t)
          ;; NOTE: I actually prefer the output of `buffer-substring'
          ;; over `buffer-substring-no-properties'.  It is not related
          ;; to the above "fixme", though it might cause problems in
          ;; some cases (none that I know of).
          (buffer-substring (line-beginning-position)
                            (line-end-position))))

(defun prot-outline-imenu-setup ()
  "`imenu' bindings for the local `outline-mode' buffer.
To be used in tandem with `prot-outline-minor-mode-enter-hook'."
  (setq-local imenu-prev-index-position-function
              'prot-outline-imenu-heading)
  (setq-local imenu-extract-index-name-function
              'prot-outline-imenu-title))

(defun prot-outline-imenu-restore ()
  "Restore `imenu' list when variable `outline-minor-mode' is nil.
The new index should be the same as the one you would get in a
standard invocation of `imenu'.

To be used in `prot-outline-minor-mode-exit-hook'."
  (dolist (var '(imenu-prev-index-position-function
                 imenu-extract-index-name-function))
    (kill-local-variable var))
  (save-excursion
    (imenu-default-create-index-function))
  (message "Refreshed `imenu' index"))

(add-hook 'prot-outline-minor-mode-enter-hook #'prot-outline-imenu-setup)
(add-hook 'prot-outline-minor-mode-exit-hook #'prot-outline-imenu-restore)

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

6.11 Registers

Watch: Primer on Emacs “registers” (2020-03-08). The rest is just an overview of the topic, meaning that I have no custom code to share.

Registers are compartments that hold data of various sorts. They offer the means for advanced, highly efficient workflows, especially when combined with keyboard macros.

Registers are called by a single character, which can be a letter (case-sensitive), number, or symbol. Each character can only contain a single register at a time.

To define a register, you call the appropriate command (see table below) and then specify the character you want to store that data at.

Key chord Command
C-x r n number-to-register
C-x r s copy-to-register (think "save string")
C-x r r copy-rectangle-to-register
C-x r SPC point-to-register
C-x r w window-configuration-to-register
C-x r f frameset-to-register (frames and their windows)
C-x r + increment-register (better used with numbers)
C-x r i insert-register (text, number, rectangle)
C-x r j jump-to-register (to point or window/frameset config)

Notes about some of the above:

  • Using point-to-register allows you to revisit a specific location in a buffer, but also reopen the file visited by that buffer in case the buffer is deleted.
  • Calling number-to-register without an argument will just store the number 0 to the register you specify. Whereas C-u 100 C-x r n will store 100. In practice, you often want to use the latter method.
  • Use increment-register to increment a number by one. Pass a numeric argument to increment by that amount instead. For example, to increment by five do C-u 5 C-x r + and then select the register you want to operate on. This only affects the value stored in the register. It does not also insert it in the buffer.
  • Number registers are particularly useful when you want to increment several numbers through a keyboard macro. You can record the motions you need, run increment-register as noted above followed by the standard insert-register. If, however, you just want to increment a single number through a keyboard macro, then just use the counter provided by that facility (refer to my video on easier kmacro counter from 2019-10-14).
  • The registers that store text as a string or a rectangle rely on the active region to capture the data. They are also great for keyboard macros where you cannot rely on yanking from the head of the kill ring (because, say, you are killing other things which push that value further down). Note though that increment-register has a different behaviour when applied to them, where it will append to the register instead (with an option to override the previous value).
  • While on the topic of appending to registers, I define several functions that change how the accumulation of text is supposed to happen. They introduce a space or line separator between the entries you {ap,pre}-pend to the register.
  • In my testing, I could never make window-configuration-to-register persist between sessions (see section on Emacs server and desktop). Whereas all other registers retain their values. So use this command to store window configurations that are otherwise transient in nature. For more permanent setups, rely on frameset-to-register which will produce a new frame (or more if you had) with all the windows in place. Remember that you can always go back to your previous window configuration without using registers, such as with the built-in winner-mode (see section on window history and directional motions).
(use-package register)

6.12 Cursor and mouse settings

6.12.1 Cursor appearance and tweaks

Nothing special here. Just some tweaks for the cursor's appearance. Do C-h f cursor-type to learn everything about the available options.

Note that in Minibuffer configurations and extras (prot-minibuffer.el) I also add a local variable for the minibuffer's cursor.

(use-package frame
  :config
  (setq-default cursor-type '(hbar . 3))
  (setq-default cursor-in-non-selected-windows 'hollow)
  (setq-default blink-cursor-blinks 50)
  (setq-default blink-cursor-interval 0.2)
  (setq-default blink-cursor-delay 0.2)
  (blink-cursor-mode 1))

6.12.2 prot-pulse.el (highlight cursor position)

pulse.el is a library that provides utilities for highlighting the region or area around point. It is meant to be used by other packages as a means of offering visual feedback, as is the case with, for example, M-. (xref-find-definitions).

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

(use-package prot-pulse
  :straight (:type built-in)
  :demand
  :config
  (setq prot-pulse-advice-commands t)
  (setq prot-pulse-pulse-command-list
        '(recenter-top-bottom
          reposition-window))
  (prot-pulse-advice-commands-mode 1)
  :bind ("<s-escape>" . prot-pulse-pulse-line))

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

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

;; Copyright (C) 2020  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; Extensions to the built-in `pulse.el' library for my Emacs
;; configuration: <https://protesilaos.com/dotemacs/>.

;;; Code:

(require 'pulse)

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

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

(defcustom prot-pulse-advice-commands t
  "Add advice to `prot-pulse-pulse-command-list' items."
  :type 'boolean
  :group 'prot-pulse)

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

;;;###autoload
(defun prot-pulse-pulse-line (&optional face)
  "Temporarily highlight the current line with optional FACE."
  (interactive)
  (let ((start (if (eobp)
                   (line-beginning-position 0)
                 (line-beginning-position)))
        (end (line-beginning-position 2))
        (pulse-delay .04)
        (face (or face 'prot-pulse-line)))
    (pulse-momentary-highlight-region start end face)))

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

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

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

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

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

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

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

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

6.12.3 Mouse wheel behaviour

The value of mouse-wheel-scroll-amount means the following:

  • By default scroll by one line.
  • Hold down Shift to do so by five lines.
  • Hold down Meta to scroll half a screen.
  • Hold down Control to adjust the size of the text. This was added in Emacs 27.

The other options in short:

  • Hide mouse pointer while typing.
  • Enable mouse scroll.
  • Faster wheel movement means faster scroll.
  • Scroll window under mouse pointer regardless of whether it is the current one or not.

Note that if we enable mouse-drag-copy-region we automatically place the mouse selection to the kill ring. This is the same behaviour as terminal emulators that place the selection to the clipboard (or the primary selection). I choose not to use this here.

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

6.12.4 Scrolling behaviour

By default, page scrolling should keep the point at the same visual position, rather than force it to the top or bottom of the viewport. This eliminates the friction of guessing where the point has warped to.

As for per-line scrolling, I dislike the default behaviour of visually re-centring the point: it is too aggressive as a standard mode of interaction. With the following setq-default, the point will stay at the top/bottom of the screen while moving in that direction (use C-l to reposition it).

(use-package emacs
  :config
  (setq-default scroll-preserve-screen-position t)
  (setq-default scroll-conservatively 1) ; affects `scroll-step'
  (setq-default scroll-margin 0))

6.12.5 Delete selection

This is a very helpful mode. It kills the marked region when inserting directly to it. It also has checks to ensure that yanking over a selected region will not insert itself when mouse-drag-copy-region is in effect (see the section on the mouse wheel behaviour).

(use-package delsel
  :hook (after-init-hook . delete-selection-mode))

Pro tip: On Emacs 27.1 or higher you can create a rectangular region by holding down Ctrl and Meta while dragging the mouse with the left click pressed.

6.12.6 Tooltips (tooltip-mode)

These settings control how tool tips are to be handled when hovering the mouse over an actionable item:

  • I just want to make sure that the GTK theme is not used for those: I prefer the generic display which follows my current theme's styles.
  • The delay is slightly reduced for the initial pop-up, while it has been increased for immediate pop-ups thereafter.
(use-package tooltip
  :config
  (setq tooltip-delay 0.5)
  (setq tooltip-short-delay 0.5)
  (setq x-gtk-use-system-tooltips nil)
  (setq tooltip-frame-parameters
        '((name . "tooltip")
          (internal-border-width . 6)
          (border-width . 0)
          (no-special-glyphs . t)))
  :hook (after-init-hook . tooltip-mode))

6.13 Conveniences and minor extras

6.13.1 Auto revert mode

This mode ensures that the buffer is updated whenever the file changes. A change can happen externally or by some other tool inside of Emacs (e.g. kill a Magit diff).

(use-package autorevert
  :diminish
  :config
  (setq auto-revert-verbose t)
  :hook (after-init-hook . global-auto-revert-mode))

6.13.2 Preserve contents of system clipboard

Say you copied a link from your web browser, then switched to Emacs to paste it somewhere. Before you do that, you notice something you want to kill. Doing that will place the last kill to the clipboard, thus overriding the thing you copied earlier. We can have a kill ring solution to this with the following:

(use-package emacs
  :config
  (setq save-interprogram-paste-before-kill t))

Now the contents of the clipboard are stored in the kill ring and can be retrieved from there (e.g. with M-y).

6.13.3 Generic feedback

The common thread of these options is the feedback they provide us with or simplify common tasks so that their feedback does not cause friction:

  • Show the current buffer's name as the frame's title. This only affects window managers that have window decorations. If you do not know what that means, then you are most likely using an environment where frame titles are already visible.
  • Faster feedback for key chords (keys appear in the echo area).
  • Allow inputting Greek while preserving Emacs keys. Toggle with C-\.
  • Ignore visual or audible bells. Emacs has more appropriate ways of providing error/warning messages or hints that something is not producing the desired results (e.g. a failed isearch will return no results, while the failed match will be styled accordingly in the echo area). By the way, use C-h e to bring up the log with the echo area's messages.
  • Answer with just the initials when dealing with "yes/no" questions.
  • Enable actions for narrowing the buffer, region {up,down}casing (all caps or no caps), dired single-buffer navigation (bound to a). Disable overwrite-mode.
(use-package emacs
  :config
  (setq frame-title-format '("%b"))
  (setq echo-keystrokes 0.25)
  (setq default-input-method "greek")
  (setq ring-bell-function 'ignore)

  (defalias 'yes-or-no-p 'y-or-n-p)
  (put 'narrow-to-region 'disabled nil)
  (put 'upcase-region 'disabled nil)
  (put 'downcase-region 'disabled nil)
  (put 'dired-find-alternate-file 'disabled nil)
  (put 'overwrite-mode 'disabled t))

6.13.4 Pulse highlight changes (goggles.el)

This is another package by Daniel Mendler which pulses the area where some change took effect. It is a subtle, yet effective, method to visually capture the affected region of text and, thus, be confident that no mistakes were made in the process (or notice those as they occur).

(use-package goggles
  :straight (:host github :repo "minad/goggles" :branch "master")
  :diminish
  :config
  (setq-default goggles-pulse t)
  (goggles-mode 1))

6.13.5 Newline characters for file ending

For some major modes, such as diff-mode, a final newline is of paramount importance. Without it you cannot, for instance, apply a patch cleanly. As such, the mode-require-final-newline will add a newline character when visiting or saving a buffer of relevance.

(use-package emacs
  :config
  (setq mode-require-final-newline 'visit-save))

6.13.6 Altered zap and easier repeats

I seldom use the functionality related to this section, but when I do I prefer it to work the way I expect. zap-up-to-char will delete everything from point up to the character you provide it with. Think of how you may want to delete a file name but keep its file type extension.

The repeat command is bound by default to C-x z. I make it so that subsequent repetitions require only hitting another z. In practice though, you should not bother with this. Let keyboard macros handle that task.

Pro tip: to make a keyboard macro out of your most recent commands, use C-x C-k l which calls kmacro-edit-lossage. The list is editable, so remove anything that is not required and then save what is left. The result is stored as the latest keyboard macro (and you also have the power to cycle through kmacros, store them in specific keys, etc.).

Moving on to the mark, practically every Emacs motion that operates on a portion of text will set the mark automatically. You can also do it manually with C-SPC (hit it twice if you do not wish to activate the region). It is then possible to cycle through the marks in reverse order by passing a prefix argument C-u C-SPC. With the evaluation of set-mark-command-repeat-pop as t we can continue cycling by repeated presses of C-SPC. Again though, this is not the type of functionality I rely on: for more deliberate actions of this sort, consider Emacs' notion of "registers".

(use-package emacs
  :config
  (setq repeat-on-final-keystroke t)
  (setq set-mark-command-repeat-pop t)
  :bind ("M-z" . zap-up-to-char))

6.13.7 Package lists

With this I just want to enable line highlighting when browsing the list of packages. I generally use hl-line-mode on all interfaces where the current line is more important than the exact column of the point.

;; TODO 2021-01-19: review package.el, given that I no longer use it.
(use-package package
  :commands (list-packages
             package-refresh-contents
             package-list-packages)
  :config
  ;; All variables are for Emacs 28+
  (setq package-name-column-width 40)
  (setq package-version-column-width 14)
  (setq package-status-column-width 12)
  (setq package-archive-column-width 8)
  :hook (package-menu-mode-hook . hl-line-mode))

7 Language settings for prose and code

This section is all about configurations and/or packages that deal with natural or programming language enhancements.

7.1 Support for various major modes

These provide syntax highlighting and additional features for environments that are not already supported by Emacs.

7.1.1 Plain text (text-mode with prot-text.el)

My prot-text.el (copied verbatim further below), provides a set of extensions for the built-in text-mode.el. Currently there are only two commands, though I plan to add more of them.

  • prot-text-insert-heading lets you add a heading delimiter to the line at point. The length of the delimiter is equal to that of the line. By default, the delimiter consists of hyphens, but with a C-u prefix argument those are substituted for equals signs.
  • prot-text-cite-region reformats the active region to look like a quoted block. It is meant to be simple and so does not test for indentation (remember, this is strictly about plain text, not structured program code). When called with an optional C-u prefix argument, it prompts for a description, which it places at the top of the formatted text inside square brackets. Instead of trying to visualise the effect for you, check this:
This is some text
we would like to quote

+----
| This is some text
| we would like to quote
+----

+----[ Description added (called command with C-u) ]
| This is some text
| we would like to quote
+----

Here is the use-package declaration. Remember that text-mode is a dependency of prot-text, so no need to include it here.

(use-package prot-text
  :straight (:type built-in)
  :mode ("\\(README\\|CHANGELOG\\|COPYING\\|LICENSE\\)$")
  :bind (:map text-mode-map
              ("<M-return>" . prot-text-insert-heading)
              ("M-;" . prot-text-cite-region)
              :map org-mode-map
              ("M-;" . nil)))

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

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

;; Copyright (C) 2020-2021  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; This covers my text-mode.el extensions, for use in my Emacs setup:
;; https://protesilaos.com/dotemacs.

;;; Code:

(require 'text-mode)
(require 'prot-common)
(require 'prot-simple)

;; TODO: make this more robust
;;;###autoload
(defun prot-text-insert-heading (&optional arg)
  "Insert equal length heading delimiter below current line.

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

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

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

;;;###autoload
(defun prot-text-cite-region (beg end &optional arg)
  "Cite text in region between BEG and END.
With optional prefix ARG (\\[universal-argument]) prompt for a
description that will be placed on a new line at the top of the
newly formatted text."
  (interactive "*r\nP")
  (let* ((text (buffer-substring-no-properties beg end))
         (text-new (replace-regexp-in-string "^.*?" "| " text))
         (description (if arg
                          (format "+----[ %s ]\n"
                                  (read-string "Add description: "))
                        "+----\n")))
    (delete-region beg end)
    (insert (concat description text-new "\n+----"))))

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

7.1.2 Markdown (markdown-mode)

I edit lots of Markdown files. This makes things easier.

(use-package markdown-mode
  :straight t
  :config
  ;; Allows for fenced block focus with C-c ' (same as Org blocks).
  (use-package edit-indirect :straight t)
  (setq markdown-fontify-code-blocks-natively t)
  :mode ("\\.md$" . markdown-mode))

7.1.3 YAML (yaml-mode)

This adds support for YAML files.

(use-package yaml-mode
  :straight t
  :mode (("\\.yml$" . yaml-mode)
         ("\\.yaml$" . yaml-mode)))

7.1.4 CSS (css-mode)

This is the built-in mode for working with CSS and SCSS. I just want it to not apply previews to colour references. If I ever need that, there is rainbow-mode (see relevant section).

(use-package css-mode
  :mode (("\\.css$" . css-mode)
         ("\\.scss$" . scss-mode))
  :config
  (setq css-fontify-colors nil))

7.1.5 Shell scripts (sh-mode)

Another built-in mode that targets shell scripts. I think it works well out-of-the-box, even though it provides lots of configuration options to further control its behaviour.

All I want here is to enable sh-mode in various files that are not obvious shell scripts, like Arch Linux's package recipes. As such, the :mode list will be expanded over time.

(use-package sh-script
  :mode (("PKGBUILD" . sh-mode)))

7.2 Line length (column count)

The column count is set to 72. The standard line length is 80 characters, so having it at something less allows for such things as quoting plain text, indenting, etc. git commit messages also make good use of this method. The column count is used by auto-fill-mode and similar tools (or when manually invoking text formatting with fill-paragraph or equivalent—normally bound to M-q).

;; TODO 2021-01-19: fill-column for text-mode should be 72, 80 for
;; prog-mode; check for rest.

;; TODO 2021-01-19: this should be merged with configurations for
;; paragraphs and fill-mode
(use-package emacs
  :config
  (setq-default fill-column 72)
  :hook (after-init-hook . column-number-mode))

7.3 Paragraphs and fill-mode

Let us first configure how paragraphs should be treated. I find that a double space is the best way to delimit sentences in source form. There is no worry that this will be shown on a website or rendered version of a document, because processors know how to handle spacing. We do this to make things easier to tell apart, but also to render unambiguous commands like forward-sentence.

;; TODO 2021-01-19: this should be merged with configurations for line
;; length.
(use-package emacs
  :config
  (setq sentence-end-double-space t)
  (setq colon-double-space nil)
  (setq use-hard-newlines nil)
  (setq sentence-end-without-period nil))

Now we need to make sure we run the minor mode that keeps paragraphs within the column limit I prefer: you can always do it manually for the current paragraph or the active region with fill-paragraph, bound by default to M-q.

By targeting text-mode we also affect every major mode derived from it, which means that we correctly do not get this utility in programming-related modes (in principle, those come from prog-mode). The adaptive mode improves the handling of things like bulleted and numbered lists, where it recognises the text's prefix and tries to align to it rather than the absolute beginning of the line.

Note that Common custom functions (prot-simple.el) contains some commands related to auto-fill.

;; TODO 2021-01-19: this should be merged with configurations for line
;; length.
(use-package emacs
  :diminish auto-fill-function
  :config
  (setq adaptive-fill-mode t)
  :hook (text-mode-hook . turn-on-auto-fill))

7.4 Recognise subwords

It is better you do C-h f subword-mode. Basically, this alters the way Emacs understands word boundaries. So, camelCaseWords are exposed as their constituents rather than one long word, meaning that motions will behave accordingly.

;; TODO 2021-01-19: subword-mode is SUBJECT TO REMOVAL!  I never read
;; code that has subwords.
(use-package subword
  :diminish
  :hook (prog-mode-hook . subword-mode))

7.5 Comment lines, regions, boxes, etc.

Just some basic configurations for commenting structured text. This is mostly a placeholder for potentially more targeted and detailed settings that would involve per-mode hooks.

The purpose of my reviewed key bindings is to make them more consistent. Helps with mnemonics. They also are more ergonomic. To this end, I have the following:

  • The standard commenting function is now bound to the simple C-;. This runs a "do what I meant" function I have defined, whose detailed documentation can be read below.
  • C-: (C-S-;) will kill the comment on the current line. This is particularly helpful when the comment follows text you would like to keep. The operation can be performed regardless of where the point is on the line. Some modes disable this behaviour (e.g. trying it on source code inside of org-mode—for those cases, focus the block with C-c ').
  • The M-; will just append a comment to the line, rather than the default comment-dwim.

Note that C-; is occupied by some flyspell command that I have no use for (disabled in the relevant package declaration).

Lastly, use M-j (alias C-M-j) when you want to continue an existing comment on a new line with respect for the current indentation. If you are not inside of a comment, this will just create an indentation-aware new line.

(use-package newcomment
  :config
  (setq comment-empty-lines t)
  (setq comment-fill-column nil)
  (setq comment-multi-line t)
  (setq comment-style 'multi-line)

  ;; TODO 2021-01-19: this is subject to review and any custom code must
  ;; be placed in a new prot-comment.el
  (defun prot/comment-dwim (&optional arg)
    "Alternative to `comment-dwim': offers a simple wrapper
around `comment-line' and `comment-dwim'.

If the region is active, then toggle the comment status of the
region or, if the major mode defines as much, of all the lines
implied by the region boundaries.

Else toggle the comment status of the line at point."
    (interactive "*P")
    (if (use-region-p)
        (comment-dwim arg)
      (save-excursion
        (comment-line arg))))

  :bind (("C-;" . prot/comment-dwim)
         ("C-:" . comment-kill)
         ("M-;" . comment-indent)
         ("C-x C-;" . comment-box)))

7.6 Configure 'electric' behaviour

Emacs labels as "electric" any behaviour that involves contextual auto-insertion of characters. This is a summary of my settings:

  • Indent automatically.
  • If electric-pair-mode is enabled (which I might do manually), insert quotes and brackets in pairs. Only do so if there is no alphabetic character after the cursor.
  • The cryptic numbers in the pairs set, correspond to curly single and double quotes and these «». The contents of this set are always inserted in pairs, regardless of major mode.
    • To get those numbers, evaluate (string-to-char CHAR) where CHAR is the one you are interested in. For example, get the literal tab's character with (string-to-char "\t").
  • While inputting a pair, inserting the closing character will just skip over the existing one, rather than add a new one. So typing ( will insert () and then typing ) will just be the same as moving forward one character C-f.
  • Do not skip over whitespace when operating on pairs. Combined with the above point, this means that a new character will be inserted, rather than be skipped over. I find this better, because it prevents the point from jumping forward, plus it allows for more natural editing.
  • The whitespace characters are space (\s), tab (\t), and newline (\n).
  • The rest concern the conditions for transforming quotes into their curly equivalents. I keep this disabled, because curly quotes are distinct characters. It is difficult to search for them. Just note that on GNU/Linux you can type them directly by hitting the "compose" key and then an angled bracket (< or >) followed by a quote mark.
(use-package electric
  :config
  (setq electric-pair-inhibit-predicate'electric-pair-conservative-inhibit)
  (setq electric-pair-preserve-balance t)
  (setq electric-pair-pairs
        '((8216 . 8217)
          (8220 . 8221)
          (171 . 187)))
  (setq electric-pair-skip-self 'electric-pair-default-skip-self)
  (setq electric-pair-skip-whitespace nil)
  (setq electric-pair-skip-whitespace-chars
        '(9
          10
          32))
  (setq electric-quote-context-sensitive t)
  (setq electric-quote-paragraph t)
  (setq electric-quote-string nil)
  (setq electric-quote-replace-double t)
  :hook (after-init-hook . (lambda ()
                             (electric-indent-mode 1)
                             (electric-pair-mode -1)
                             (electric-quote-mode -1))))

7.7 Parentheses (show-paren-mode)

Configure the mode that highlights matching delimiters or parentheses. I consider this of utmost importance when working with languages such as elisp.

Summary of what these do:

  • Activate the mode upon startup.
  • Show the matching delimiter/parenthesis if on screen, else show nothing. It is possible to highlight the expression enclosed by the delimiters, by using either mixed or expression. The latter always highlights the entire balanced expression, while the former will only do so if the matching delimiter is off screen.
  • show-paren-when-point-in-periphery lets you highlight parentheses even if the point is in their vicinity. This means the beginning or end of the line, with space in between. I used that for a long while and it server me well. Now that I have a better understanding of Elisp, I disable it.
  • Do not highlight a match when the point is on the inside of the parenthesis.
(use-package paren
  :config
  (setq show-paren-style 'parenthesis)
  (setq show-paren-when-point-in-periphery nil)
  (setq show-paren-when-point-inside-paren nil)
  :hook (after-init-hook . show-paren-mode))

7.8 Tabs, indentation, and the TAB key

I believe tabs, in the sense of inserting the tab character, are best suited for indentation. While spaces are superior at precisely aligning text. However, I understand that elisp uses its own approach, which I do not want to interfere with. Also, Emacs tends to perform alignments by mixing tabs with spaces, which can actually lead to misalignments depending on certain variables such as the size of the tab. As such, I am disabling tabs by default.

If there ever is a need to use different settings in other modes, we can customise them via hooks. This is not an issue I have encountered yet and am therefore refraining from solving a problem that does not affect me.

Note that tab-always-indent will first do indentation and then try to complete whatever you have typed in. I control how completion works for that particular function in my in-buffer completions section.

(use-package emacs
  :config
  (setq-default tab-always-indent 'complete)
  (setq-default tab-width 4)
  (setq-default indent-tabs-mode nil))

7.9 Flyspell (spell check)

I need spell checking mostly for English, though I also install dictionaries for Greek, French, and Spanish. These are external to Emacs and are provided by the aspell library.

In previous versions of this section I had configurations that would automate spell checking. It worked but was rather slow. Upon further inspection, I realised that I seldom need to work in mixed language circumstances. Moreover, I now understand that I do not need to have spell checking always on. It can be activated manually, with the flyspell functions defined in the :commands segment below or with my prot/flyspell-dwim for the active region.

Also bear in mind that the key binding C-; that Flyspell uses by default is disabled because I re-purpose that for a faster version of C-x C-; (much more useful for my work—see the section on comments).

Note that my use-case for Flyspell is to mark a region and test for errors in it. If I need to review the entire buffer, especially if it is about some long-form text, I use a flymake interface instead (refer to the section on flymake-aspell).

(use-package flyspell
  :config
  (setq flyspell-issue-message-flag nil)
  (setq flyspell-issue-welcome-flag nil)
  (setq ispell-program-name "aspell")
  (setq ispell-dictionary "en_GB")

  ;; TODO 2021-01-19: this is subject to review and any custom code must
  ;; be placed in a new prot-flyspell.el
  (defvar prot/ispell-dictionaries
    '(("English" . "en")
      ("Ελληνικά" . "el")
      ("Français" . "fr")
      ("Espanõl" . "es"))
    "Alist of dictionaries I may use.
It is used by `prot/ispell-dictionaries-complete'.")

  (defun prot/ispell-dictionaries-complete ()
    "Select an item from `prot/ispell-dictionaries'."
    (interactive)
    (let* ((dicts (mapcar #'car prot/ispell-dictionaries))
           (choice (completing-read "Select dictionary: " dicts nil t))
           (key (cdr (assoc `,choice prot/ispell-dictionaries))))
      (ispell-change-dictionary key)
      (message "Switched to %s" key)))

  (defun prot/flyspell-dwim ()
    "Spell check region or select dictionary.

Use `flyspell-region' on the active region, else invoke
`prot/ispell-dictionaries-complete'."
    (interactive)
    (let ((beg (region-beginning))
          (end (region-end)))
      (if (use-region-p)
          (flyspell-region beg end)
        (prot/ispell-dictionaries-complete))))

  :bind (("M-$" . prot/flyspell-dwim)
         ("C-M-$" . prot/ispell-dictionaries-complete)
         :map flyspell-mode-map
         ("C-;" . nil)))

7.10 Code and text linters

7.10.1 Flymake

This is a built-in linter interface. It visualises in a buffer what you would otherwise get on the command-line prompt (or compilation log), while it also marks the line[s] where the note, warning, or error occurs. In short, it is quite a nice tool to have.

Several extensions to Flymake are already available, mostly targeted at programmers. For my case, there is no need for any of those, while Flymake can lint Elisp without any further configuration.

(use-package flymake
  :after prot-project
  :commands flymake-mode
  :init
  (setq flymake-fringe-indicator-position 'left-fringe)
  (setq flymake-suppress-zero-counters t)
  (setq flymake-start-on-flymake-mode t)
  (setq flymake-no-changes-timeout nil)
  (setq flymake-start-on-save-buffer t)
  (setq flymake-proc-compilation-prevents-syntax-check t)
  (setq flymake-wrap-around nil)
  :bind (:map flymake-mode-map
              ("C-c ! s" . flymake-start)
              ("C-c ! d" . flymake-show-diagnostics-buffer)
              ("C-c ! n" . flymake-goto-next-error)
              ("C-c ! p" . flymake-goto-prev-error)))

(use-package flymake-diagnostic-at-point
  :straight t
  :after flymake
  :config
  (setq flymake-diagnostic-at-point-display-diagnostic-function
        'flymake-diagnostic-at-point-display-minibuffer))
7.10.1.1 Flymake + Proselint

Manuel Uberti has published flymake-proselint on Github and MELPA. It offers a Flymake interface to the external proselint executable (see Proselint configuration).

This comes in handy when I need to review some long-form text for common inconsistencies and stylistic irregularities. Errors will be marked on the fringe, while you can quickly get an overview with pointers to the precise line number by invoking flymake-show-diagnostics-buffer (also check my configurations for Flymake).

To run the program, you just need to hook it to whatever major-mode you use for prose. The following dolist handles this nicely. Then you need to enable flymake-mode to start using it. I prefer to do the final step manually, as I normally do not run a linter while writing: it is too distracting.

(use-package flymake-proselint
  :straight t
  :after flymake
  :init
  (dolist (mode '("markdown-mode" "org-mode" "text-mode"))
    (add-hook (intern (concat mode "-hook")) #'flymake-proselint-setup)))
7.10.1.1.1 Proselint configuration

This is my configuration for the external proselint executable (check that project's README). The following should be made available at ~/.config/proselint/config.

See Flymake + Proselint for how I use this tool to review my long-form writing.

{
    "max_errors": 200,
    "checks": {
        "airlinese.misc"                : false,
        "annotations.misc"              : true,
        "archaism.misc"                 : true,
        "cliches.hell"                  : true,
        "cliches.misc"                  : true,
        "consistency.spacing"           : true,
        "consistency.spelling"          : true,
        "corporate_speak.misc"          : false,
        "cursing.filth"                 : false,
        "cursing.nfl"                   : false,
        "cursing.nword"                 : false,
        "dates_times.am_pm"             : false,
        "dates_times.dates"             : false,
        "hedging.misc"                  : true,
        "hyperbole.misc"                : true,
        "jargon.misc"                   : true,
        "lexical_illusions.misc"        : true,
        "lgbtq.offensive_terms"         : true,
        "lgbtq.terms"                   : true,
        "links.broken"                  : false,
        "malapropisms.misc"             : true,
        "misc.apologizing"              : true,
        "misc.back_formations"          : true,
        "misc.bureaucratese"            : true,
        "misc.but"                      : true,
        "misc.capitalization"           : true,
        "misc.chatspeak"                : false,
        "misc.commercialese"            : true,
        "misc.composition"              : true,
        "misc.currency"                 : false,
        "misc.debased"                  : true,
        "misc.false_plurals"            : true,
        "misc.illogic"                  : true,
        "misc.inferior_superior"        : true,
        "misc.institution_name"	        : true,
        "misc.latin"                    : true,
        "misc.many_a"                   : false,
        "misc.metaconcepts"             : true,
        "misc.metadiscourse"            : true,
        "misc.narcissism"               : true,
        "misc.not_guilty"               : true,
        "misc.phrasal_adjectives"       : true,
        "misc.preferred_forms"          : true,
        "misc.pretension"               : true,
        "misc.professions"              : true,
        "misc.punctuation"              : true,
        "misc.scare_quotes"             : true,
        "misc.suddenly"                 : false,
        "misc.tense_present"            : true,
        "misc.waxed"                    : true,
        "misc.whence"                   : false,
        "mixed_metaphors.misc"          : true,
        "mondegreens.misc"              : true,
        "needless_variants.misc"        : true,
        "nonwords.misc"                 : true,
        "oxymorons.misc"                : true,
        "psychology.misc"               : true,
        "redundancy.misc"               : true,
        "redundancy.ras_syndrome"       : true,
        "skunked_terms.misc"            : true,
        "spelling.able_atable"          : true,
        "spelling.able_ible"            : true,
        "spelling.athletes"             : false,
        "spelling.em_im_en_in"          : true,
        "spelling.er_or"                : true,
        "spelling.in_un"                : true,
        "spelling.misc"                 : true,
        "security.credit_card"          : false,
        "security.password"             : false,
        "sexism.misc"                   : true,
        "terms.animal_adjectives"       : true,
        "terms.denizen_labels"          : true,
        "terms.eponymous_adjectives"    : true,
        "terms.venery"                  : true,
        "typography.diacritical_marks"  : false,
        "typography.exclamation"        : true,
        "typography.symbols"            : true,
        "uncomparables.misc"            : true,
        "weasel_words.misc"             : true,
        "weasel_words.very"             : false
    }
}
7.10.1.2 Flymake + Aspell

I normally review a region's spelling with Flyspell, using my own extensions for it (see Flyspell (spell check)). I do, however, prefer to have an overview of all spelling warnings/errors as a final test before publishing a long-form text (e.g. an article on my website).

This is where flymake-aspell by Leo Gaskin comes in handy. It uses the familiar Flymake interface (see Flymake configuration) to highlight all spelling issues. Simple and effective!

Note that flyspell is not a dependency, but I still list it in the :after directive because that is where the ispell dictionary is specified (as it is what I normally use). This would normally be a bad practice, though I do not run into problems since flyspell is always invoked before I ever have any need for flymake-aspell, so the dependency is satisfied.

For a related package, consult my configurations for flymake-proselint.

(use-package flymake-aspell
  :after (flyspell flymake)
  :straight t
  :init
  (dolist (mode '("markdown-mode" "org-mode" "text-mode"))
    (add-hook (intern (concat mode "-hook")) #'flymake-aspell-setup)))

7.10.2 Elisp packaging requirements

With this in place we can perform checks that pertain to Emacs lisp packaging. I use it for my themes but also for any elisp library I may want to send patches to.

(use-package package-lint-flymake
  :straight t
  :after flymake
  :hook (flymake-diagnostic-functions . package-lint-flymake))

7.11 Eldoc (elisp live documentation feedback)

When editing elisp, this mode will display useful information about the construct at point in the echo area. For functions it will display the list of arguments they accept. While it will show the the first sentence of a variable's documentation string.

At first, I dismissed this package. Upon closer inspection, it does offer a lightweight complementary facility to that of the standard help commands: C-h f FUNCTION, C-h v VARIABLE.

(use-package eldoc
  :diminish
  :config
  (global-eldoc-mode 1))

7.12 Tools for manual pages (manpages)

Emacs offers a couple of commands for reading manual pages: man and woman. The former relies on the standard Unix tools, while the latter is an elisp implementation of the same idea. As I only ever run a GNU/Linux system, I am okay with just man.

Why bother?

  • All the goodies of consistency: fonts, themes, operating on text with your familiar Emacs functionality, handling buffers…
  • Each manpage provides direct links to other items it references.

What you can do inside such a buffer (with minor tweaks by me):

  • Hit i to go to the information node you want using completion (same principle as with the Info pages of C-h i and the like).
  • g will generate the buffer anew. Do it to reformat the text manually, though this should also happen automatically when adjusting a window's size.
  • n and p move between section headings.
  • Hit RET while over a referenced manpage to produce a new buffer with its contents.
  • s takes you directly to the familiar "See Also" section.
  • Use m to search for another manpage using your completion framework. If you invoke this command while point is over a referenced manpage, it becomes the default choice (same concept as with common help commands, C-h f, C-h v, and with many others like find-library).

Need to filter out those man buffers? Check my Ibuffer entry.

While there are customisation options for this tool, I find the defaults to work as expected. Note that the capitalisation of those symbols is canonical.

(use-package man
  :bind (:map Man-mode-map
              ("i" . Man-goto-section)
              ("g" . Man-update-manpage)))

8 History and state

This section contains configurations for packages that are dedicated to the task of recording the state of various Emacs tools, such as the history of the minibuffer or the list of recently visited files.

In practice, these are some of the most useful configurations one can make, as lots of functions depend on them. For example, a record of the minibuffer's history of inputs allows the completion framework to guess the most likely course of action. Typing M-x g gives me gnus as the first possible option, which is exactly what I want.

8.1 Emacs server and desktop

The following uses the first running process of Emacs as the one others may connect to. This means that calling emacsclient (with or without --create-frame), will share the same buffer list and data as the original running process, aka "the server". The server persists for as long as there is an Emacs frame attached to it.

(use-package server
  :hook (after-init-hook . server-start))

With some exceptions aside, I only ever use Emacs in a single frame. What I find more useful is the ability to save the state I was in: the name the of buffers, the cursor's position in each of them, the recent file list, the minibuffer history, my stored registers.

The state of the available buffers and the values of each register are called the "desktop" (for the other items see the following sections on recording various types of history).

Preserving the "desktop" saves me from any possible crash or when I need to close Emacs and re-launch it later (my hardware is limited, so I do not keep it running while I am away).

Overview of my settings:

  • Enable the mode that saves the "desktop", instructing it to load a small number of buffers at launch (desktop-restore-eager). The remainder of the buffer list will be loaded lazily.
  • Now we must tell it where to store the files it generates and how often it should save. Concerning the latter, the default is to store the state every time it changes. I find that a bit too much, so I set a timeout of five minutes of idleness.
  • Note the desktop-load-locked-desktop. By default, Emacs locks the desktop file while it runs. The lock is removed upon exiting. This is a safety mechanism. There are two cases where the lock can create issues:
    • Emacs has crashed, meaning that it exited abruptly and was not able to unlock the desktop. Upon re-launch Emacs will prompt you whether to load the locked file. You normally want to answer affirmatively.
    • Emacs runs in daemon mode, where it does not ask questions upon loading. In this case the lock is ignored.
    • Because I am only affected by the former, I choose to disable the prompt and just load the thing directly. Otherwise, I would set it to nil.
  • Do not restore frame configurations. If I need to store one of those, I use registers, specifically C-x r f (see section on Registers).
  • Ask what to do in case the session has a newer file that the one it initially started out with (e.g. when a new frame runs in parallel to the older one).
(use-package desktop
  :config
  (setq desktop-auto-save-timeout 300)
  (setq desktop-path '("~/.emacs.d/"))
  (setq desktop-base-file-name "desktop")
  (setq desktop-files-not-to-save nil)
  (setq desktop-globals-to-clear nil)
  (setq desktop-load-locked-desktop t)
  (setq desktop-missing-file-warning nil)
  (setq desktop-restore-eager 0)
  (setq desktop-restore-frames nil)
  (setq desktop-save 'ask-if-new)
  (desktop-save-mode 1))

And here we make sure that Emacs starts in the *scratch* buffer, no matter what.

(use-package emacs
  :init
  (setq initial-buffer-choice t)
  (setq inhibit-startup-echo-area-message "prot") ; read the docstring
  (setq inhibit-startup-screen t)
  (setq inhibit-startup-buffer-menu t))

8.2 Record various types of history

8.2.1 Minibuffer history (savehist-mode)

Keeps a record of actions involving the minibuffer. This is of paramount importance to a fast and efficient workflow involving any completion framework that leverages the built-in mechanisms.

Emacs will remember your input and choices and will surface the desired results towards the top as the most likely candidates. Make sure to also read the Minibuffer configurations and extras (prot-minibuffer.el).

(use-package savehist
  :config
  (setq savehist-file "~/.emacs.d/savehist")
  (setq history-length 1000)
  (setq history-delete-duplicates t)
  (setq savehist-save-minibuffer-history t)
  :hook (after-init-hook . savehist-mode))

8.2.2 Record cursor position

Just remember where the point is in any given file. This can often be a subtle reminder of what you were doing the last time you visited that file, allowing you to pick up from there.

(use-package saveplace
  :config
  (setq save-place-file "~/.emacs.d/saveplace")
  (setq save-place-forget-unreadable-files t)
  (save-place-mode 1))

8.2.3 Backups

And here are some settings pertaining to backups. I rarely need those, but I prefer to be safe in the knowledge that if something goes awry there is something to fall back to.

(use-package emacs
  :config
  (setq backup-directory-alist
        '(("." . "~/.emacs.d/backup/")))
  (setq backup-by-copying t)
  (setq version-control t)
  (setq delete-old-versions t)
  (setq kept-new-versions 6)
  (setq kept-old-versions 2)
  (setq create-lockfiles nil))

9 Frequently Asked Questions about this document

There are some persistent questions that pop up in my email exchange, so I thought I would cover them all in this section.

9.1 How do you learn Emacs?

There is no one-size-fits-all approach to learning. What one finds satisfactory, another may consider insufficient. In my opinion, the best way to learn Emacs is to start small, be patient, and brace yourself for a lot of reading and experimentation.

The best skill you can master, the one that will always help you in your Emacs journey, is the built-in help system. Learn to ask Emacs about things you do not know about. This section documents the essentials of Emacs' introspectable nature.

Know that C-h is the universal key for help commands (broadly understood). It works both as a prefix and as a suffix. Some common help commands:

  • C-h f allows you to search for documentation about functions.
  • C-h v is the same for variables.
  • C-h o is a wrapper of the above two, so you are searching for functions or variables. The proper name for any of these items is called a "symbol" (think of name-of-thing as symbolising a definition in the source code).
  • C-h k will prompt you for a key binding. Once you type it in you will get help about what command corresponds to that key (if any). Note that this depends on the major mode you are in. For example C-c C-c does something different in Org and Eshell buffers. Try C-h k C-c C-c to find about the different functions these will invoke in their respective major mode.
  • C-h l produces a log with your most recent key presses and the commands they call. Emacs calls this the "lossage". Ever mistyped something and got to the wrong place? Use this as an opportunity to learn and, perhaps, a way to identify key sequences you would like to modify. (pro tip: you can edit/convert your lossage into a keyboard macro with C-x C-k l—also watch Edit keyboard macros (2020-03-14)).

In the above scenaria we see how C-h is used as a prefix. You are starting a key sequence with it. So here are some cases of using it as a suffix:

  • For every incomplete key sequence if you follow it up with C-h you will get a help buffer about all possible key combinations that complete that sequence. For example, if you type C-c C-h inside of an Org buffer you will get all possible commands for Org mode and for all other minor modes you have active.
  • The C-h suffix can be appended to longer key sequences. Indeed the length is irrelevant. Suppose you want to learn more about some of the advanced features of registers. C-x r is the common prefix for those commands, so you just do C-x r C-h and you get a buffer with more information.
  • And, as you may imagine, you can even append the C-h suffix to the C-h prefix. This is a fancy way of saying that C-h C-h will show you help about help commands themselves. But because this is a special case, it comes with some extras. Try it!

All help buffers include links to other parts of Emacs, from where you can learn ever more information. For example, the help for C-c C-h includes links to the commands that correspond to each key chord. Follow the link to get documentation about that symbol.

More generally, you will find that a symbol is linked to its source. Look carefully at the top of the buffer that display symbol documentation and you will find a link to the source code (library) from where the function/variable (symbol) comes from.

Also know that the source code can be accessed at any time by means of M-x find-library.

Help commands that ask you for a symbol to input can also be aware of the context of the point (the cursor). So if you are over the name of a function and you type C-h f, that function will be the default match. Hitting RET (Enter) will take you to its documentation. This is a great way to study source code, because it will guide you to other libraries or other parts of the same library from where you can understand how things are pieced together. And it also works with the find-library command.

Finally, you should practice C-h m as much as possible. This is the help command for getting information about the major mode you are in and all the minor modes you have active. It will show you some valuable documentation as well as the main key bindings and their commands. Try it whenever you use something you have not mastered yet. For example, do it in a dired buffer to see the main operations you can perform inside of it.

9.2 Why maintain all those 'prot-*.el' files?

Those contain my custom Elisp code. Several of them provide extensions to existing tools, while others introduce some new functionality. They are written in accordance with the best coding practices and are, for all intents and purposes, regular packages.

The main benefits of packaging my code are thus:

Lexical scope
The code in written in a way that does not introduce implicit dependencies on the environment. Everything within the file has to be known so that the compiler can properly interpret it.
Byte compilation
Because of the above, the code compiles cleanly. This allows me to execute my code a bit faster than it would otherwise be possible. The more I write, the greater the otherwise marginal performance gains.
Transparency
Users who copy my code are made aware of its dependencies, which saves me time answering emails why X or Y from my dotemacs does not work on another's setup.
Modularity
Since my files render their dependencies and bindings explicit, it is easier to catch errors and debug them.

While my prot-*.el are distributed as packages, please understand that I consider this an exercise in programming. I develop them because I believe they will be useful to me. Please do not unilaterally put them in some package archive as I cannot promise that I will keep them around forever (distributing a package implies a commitment to its users).

9.3 What is the purpose of "prot/" and "contrib/"?

[Also read: Why maintain all those 'prot-*.el' files?]

The prot/ prefix in some unpackaged symbols works as a namespace that captures all my custom, yet-to-be-reviewed code. These can be functions or variables. Its utility is two-fold:

  1. It informs others that this symbol is not part of core Emacs or some other package. Otherwise it can be difficult to understand why something you copied did not work. Say, for example, I have a function that accepts an argument: (prot/function prot/variable). If none of these had the namespace you could be misled into thinking that your Emacs setup already includes those symbols (and then you will get an error message).
  2. It makes it easier for me (and others) to quickly discover what additions I have made, for whatever reason that may. For instance, M-x prot/ will give me matches for all my interactive functions (depending on the completion framework, one can access those with just M-x p/). This also means that I can do M-x occur prot/ to produce an Occur buffer with all my symbols (pass a numeric argument for displaying N lines of context C-u 5 M-s o). From there I can, say, browse them all easily or even edit them using the full array of Emacs' relevant powers (occur's results buffer is made editable with e, but you should be able to find that by using the information documented in [[*How do you learn

Emacs?][How do you learn Emacs?]]).

The same principles apply for the contrib/ namespace. Whenever I copy something from another user, I use that namespace to tell others that this is not part of Emacs or any other package (and I always link to the source).

Adding contrib/ also has another longer-term benefit. It tells my future self that some bespoke configuration was needed to solve a particular problem and, maybe, this has since been solved by a good package or even a newer version of Emacs.

Again, occur or similar tools will filter those out when necessary. Imagine having to do that without any namespaces… You would need to check each symbol one by one to determine its origin.

The convention of separating namespaces with a forward slash is not particularly important, though the linter for Elisp packaging will complain about it, if you ever go down that path. It could be something like prot- or my- or whatever. What matters is to keep things consistent and fairly easy to identify.

9.4 Why not `:custom' and why do you use so many `setq'?

I normally do not use the :custom keyword that use-package provides, opting instead for :config or :init. This is for a couple of closely related reasons:

  • It makes each variable more difficult to copy elsewhere, say, when sharing it online or to an emacs -Q scratch buffer. You either need to edit the balanced expression, or hope that users will know the syntax that the :custom keyword expects.
  • It is impossible to run C-x C-e (eval-last-sexp) on each balanced expression because it lacks the setq special form. Again, this introduces an impediment to quick copy/paste operations.

Now you may be wondering why I define each variable in its own balanced expression. To be clear, these are equivalent:

;; Style A
(setq var-1 'symbol)
(setq var-2 '(a b c))
(setq var-3 '((a . b) (c . d)))

;; Style B
(setq var-1 'symbol
      var-2 '(a b c)
      var-3 '((a . b) (c . d)))

You will notice that most of my configurations follow Style A. The reasons are the same as above: (i) copying, (ii) evaluating.

Style B used to give me more problems with copy-pasting, while it did not solve any real issues (besides, I finalise style A using a keyboard macro, so there is no real difference in typing).

I find that Style A consists of balanced expressions that are easier to keep consistent. This is especially true when you have a mixture of values: boolean types, property lists, association lists…

Of course, experience is key to understanding that all these styles are valid.

9.5 Why do you explicitly set variables the same as default?

You may have realised that many of my configurations will use a setq that declares a value that is the same as its original in the source code. I do this for two reasons:

  1. To raise awareness of its existence. If someone does not like how the defaults work, they know where to look.
  2. I guard against future versions that could be changing the defaults.

Obviously point 2 is not particularly strong for built-in libraries that are already very stable, though I find that, on the balance, nothing bad comes out of it.

At any rate, one must always read the NEWS (C-h n for view-emacs-news) whenever they upgrade to a new version of Emacs. Though there is no equivalent mechanism for individual packages… So here we are.

9.6 What is your distro and/or window manager?

I have been on GNU/Linux since the summer of 2016. For the most time I have used Debian and Arch Linux, switching between the two. As of 2020-05-03, I am back on Arch.

My criterion for picking a distro is that it is community-driven and has a strong following that ensures its longer-term continuity and overall stability. This happens to be consistent with my current focus on Emacs: I need things to work so that I may not be bothered by too much admin work (and yes, Arch is super-stable once you know what you are doing—and, well, Debian is designed for that).

Given that I mostly live inside of Emacs, the desktop environment is no longer important to me, provided it does not impede my usage of Emacs, which practically means that it does not bind any keys system-wide (with the exception of some standard ones like those for accessing TTYs).

Prior to my Emacs days, I used to have a highly customised session centred around BSPWM (the Binary Space Partitioning Window Manager), while I also spent time with i3WM, DWM, Herbestluftwm. The tiling window manager paradigm offers little to no value now that practically my entire computing experience happens inside of a single application, which is why I have no interest whatsoever in EXWM.

My Emacs is built from source, directly from trunk (i.e. the master branch).

9.7 Did you know XYZ package is better than the defaults?

As a rule of thumb, I choose external packages only after I give a fair chance to the defaults. The idea is to take things slowly and learn along the way, while consulting the official manual and relevant documentation (I strongly encourage you to study the information I provide in How do you learn Emacs?).

Without exposure to the built-in tools it is impossible to make an informed decision about what is actually missing and what could be improved further. Additionally, it is difficult to appreciate the underlying rationale that led to a given element of design without actually trying to put yourself in that mindset or workflow.

Put differently, keep an open mind about the alternatives before deciding to reproduce the thing you had before, else you are assuming your prior knowledge to be true in advance of any possible evidence to the contrary (a sign of dogmatism).

The process of learning the internals of Emacs means that I write my own Elisp functions when I feel that a standard tool could be tweaked on the margin of its core utility in order to do what I want (read my answer to the question Why maintain all those 'prot-*.el' files? as well as the one on What is the purpose of "prot/" and "contrib/"?). External packages that I do use are either a clear upgrade over the defaults or otherwise extend the functionality of what is already available.

You will not find any superficialities herein: no rainbow-coloured mode lines, no icons, no tool bars with bells and spinning wheels, nothing. I respect the fact that they exist, but find that they do not contribute to my productivity.

Though a former Vim user for ~3 years, I decided not to use evil-mode or any kind of Vi emulation (remember the point about keeping an open mind?). I wanted to do things differently in order to ultimately set on the best approach going forward. I have eventually settled on a system that builds on top of the "Emacs way" to key bindings, which I discuss in my hour-long presentation about Switching to Emacs (2019-12-20). I believe that a mnemonics-based set of keys is easier to get started with. It expands organically as you familiarise yourself with the multitude of Emacs' functionalities: there is an entire world of applications outside the narrow confines of editing code.

Since you read this (and the rest of my dotemacs, I presume), also consider two highly valuable blog posts by Karthik Chikmagalur:

9.8 What is the meaning of the `s-KEY' bindings?

Some sections of this document assign functions to key combinations that involve the "Super" key (also known as the "Windows key" on some commercial keyboards). This is represented as a lower case s.

In most cases, those key bindings are alternative ways of invoking common commands that are by default bound to longer key chords. The original keys will continue to function as intended (for example, C-x o is also s-o). Otherwise they bind my own custom functions.

To find all my keybindings of this sort in the source code version of this document from inside of Emacs, do M-s o (or M-x occur) followed by the pattern "[a-zA-Z<]?-?s-.+?" (please contact me if you know how to improve this).

Note that your choice of desktop environment (or window manager) might render some of those useless. The DE/WM will simply intercept the key chord before it is ever sent to Emacs. For example, GNOME has a hidden key mapping to s-p, which does something with monitors (last time I checked on GNOME 3.30). Such bindings are scattered throughout the config database that is normally accessed with gsettings on the command line or the graphical dconf-editor (not pretty either way).

9.9 What hardware do you use?

I am using a Lenovo Thinkpad X220 that I got in 2018 for ~80 EUR. This is the first computer I ever bought: before that I had a Macbook that was offered to me as part of an office job—but do not ask me about it because at the time I only knew how to copy/paste using right click and that sort of thing, while I only ever bothered with the hardware side of things once I got better at using the computer (my switch to GNU/Linux was about freedom and consumer sovereignty, i.e. politics, not tech-only considerations).

The laptop is mostly deployed as part of a desktop setup, attached to an external monitor, mouse, and keyboard. The monitor is 1080p and I got free of charge from a clearance. As for the mouse and keyboard, I bought those from a toy store for a grand total of 7 EUR combined. The keyboard's layout is US Qwerty.

For my videos I use the built-in camera and microphone (sorry if the production quality is sub-standard!). Since we are here: the recording software is OBS Studio, while I do no video editing whatsoever.