emacs-dot-d/unravel-emacs.org
Vedang Manerikar 2b7409bc77 Copy my org-capture settings from org-mode-crate
At the moment, I am not committing my capture templates to git
history, as I want a way to moove this code into the personal settings
part of this configuration
2024-11-28 16:10:57 +05:30

281 KiB
Raw Blame History

GNU Emacs configuration

This configuration is inspired from the work of my hero Prot. I've copied straight from his config file, because I think the explanations he has created are worthwhile and should be read by everyone. Prot's files are all prefixed with prot-emacs-*. I have changed this to unravel-*. The reason for this is that I have picked up only what I need and changed it where needed. As such, any issues you face with this configuration are likely introduced by me.

I use emacs on Mac OSX as my primary development environment. I install the latest available pre-compiled Emacs (with native compilation), as provided by Jimeh here: jimeh/emacs-builds.

Any quote without attribution is directly taken from Prot's org file. You should read through it if you want detailed explanations of things you find here.

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

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

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

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

(org-babel-tangle)

Here is what the generated directory structure should look like:

  fd -e el -e org -E elpa | tree --fromfile

. ├── early-init.el ├── init.el ├── unravel-emacs.org └── unravel-modules ├── unravel-completion.el ├── unravel-essentials.el ├── unravel-langs.el └── unravel-theme.el

2 directories, 7 files

To make a change to this Emacs configuration, edit this file and then type C-c C-v C-t (M-x org-babel-tangle) to republish all the relevant files.

The early-init.el file

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

The early-init.el basic frame settings

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

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

  ;; A big contributor to startup times is garbage collection.

  ;; We up the gc threshold to temporarily prevent it from running, then
  ;; reset it later after startup is complete. Not resetting it will
  ;; cause stuttering/freezes.

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

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

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

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

If you have both .el and .elc files, load the newer one

  ;; When both .el and .elc / .eln files are available,
  ;; load the latest one.

  (setq load-prefer-newer t)

The early-init.el initialises the package cache

  ;; Ensure that `describe-package' does not require a
  ;; `package-refresh-contents'.
  (setq package-enable-at-startup t)

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

Naming frames allows you to select them using completion (M-x select-frame-by-name).

  ;; Name the default frame
  ;; You can select a frame with M-x select-frame-by-name
  (add-hook 'after-init-hook (lambda () (set-frame-name "unravel/emacs")))

The init.el file

This is the main initialisation file of Emacs. Everything loads from here, even if it has been split into multiple files for convenience.

The init.el tweaks to make native compilation silent

These warnings are unnecessarily scary.

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

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

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

There is no need to use the M-x customize infrastructure. It's easier to just rely on the init file instead.

I would prefer to just have an option to avoid the Custom infrastructure altogether, but this is not possible. So here we are…

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

The init.el settings to enable commands disabled by default

These commands are actually useful, especially in org-mode.

;; Enable these commands which have been disabled by default
(mapc
 (lambda (command)
   (put command 'disabled nil))
 '(list-timers narrow-to-region narrow-to-page upcase-region downcase-region))

The init.el settings to disable unnecessary commands enabled by default

These commands are "unsafe", in that we should be using the alternatives (like vterm and org)

;; Disable these commands which have been enabled by default
(mapc
 (lambda (command)
   (put command 'disabled t))
 '(eshell project-eshell overwrite-mode iconify-frame diary))

Add the modules folder to the load-path

This is where all the custom configuration sits for all the packages we use. We write configuration on a per-file basis instead of in a giant file, because these smaller files are more readable, approachable and shareable.

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

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

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

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

  ;;;; Packages

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

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

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

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

  ;; Let `package-install' suggest upgrades for built-in packages too.
  (setq package-install-upgrade-built-in t)

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

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

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

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

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

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

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

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

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

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

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

The init.el final part to load the individual modules

Now we are ready to load our per-module configuration files:

  (require 'unravel-theme)
  (require 'unravel-essentials)
  (require 'unravel-completion)
  (require 'unravel-search)
  (require 'unravel-dired)
  (require 'unravel-window)
  (require 'unravel-git)
  (require 'unravel-org)
  (require 'unravel-shell)
  (require 'unravel-langs)

The unravel-theme.el module

This module defines everything related to the aesthetics of Emacs.

  ;;; Everything related to the look of Emacs

The unravel-theme.el section for cool, modern themes (ef-themes)

I use themes from the ef-themes package exclusively.

Prot is the lead developer and maintainer of this package.

  ;;; The Ef (εὖ) themes

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

The unravel-theme.el section for lin

lin is an improvement on hl-line-mode.

Prot is the lead developer and maintainer of this package.

  ;;;; Lin
  ;; Read the lin manual: <https://protesilaos.com/emacs/lin>.
  (use-package lin
    :ensure t
    :hook (after-init . lin-global-mode) ; applies to all `lin-mode-hooks'
    :config
    (setopt lin-face 'lin-cyan))

The unravel-theme.el section for spacious-padding

spacious-padding gives us a comfortable reading experience.

Prot is the lead developer and maintainer of this package.

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

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

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

The unravel-theme.el section for rainbow-mode

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

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

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

The unravel-theme.el section for cursory

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

Prot is the lead developer and maintainer.

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

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

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

  (cursory-mode 1))

The unravel-theme.el section for theme-buffet

The theme-buffet package automatically changes the theme based on time of day.

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

  ;;;; Theme buffet
  ;; <https://git.sr.ht/~bboal/theme-buffet>
  (use-package theme-buffet
    :ensure t
    :after (:any modus-themes ef-themes)
    :defer 1
    :config
    (let ((modus-themes-p (featurep 'modus-themes))
          (ef-themes-p (featurep 'ef-themes)))
      (setq theme-buffet-menu 'end-user)
      (setq theme-buffet-time-offset 0)
      (setq theme-buffet-end-user
            '(:night     (ef-dark ef-winter ef-autumn ef-night ef-duo-dark ef-symbiosis ef-owl)
              :morning   (ef-light ef-cyprus ef-spring ef-frost ef-duo-light ef-eagle)
              :afternoon (ef-arbutus ef-day ef-kassio ef-summer ef-elea-light ef-maris-light ef-melissa-light ef-trio-light ef-reverie)
              :evening   (ef-rosa ef-elea-dark ef-maris-dark ef-melissa-dark ef-trio-dark ef-dream)))

      (when (or modus-themes-p ef-themes-p)
        (theme-buffet-timer-hours 2)
        (theme-buffet-a-la-carte))))

The unravel-theme.el section about fontaine

[ Watch Prot's video: Customise Emacs fonts (2024-01-16) ]

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

Prot is the lead developer and maintainer.

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

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

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

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

    (setq fontaine-presets
          '((small
             :default-family "Iosevka"
             :default-height 130)
            (regular
             :default-height 150)
            (medium
             :default-weight semilight
             :default-height 170
             :bold-weight extrabold)
            (large
             :inherit medium
             :default-height 190)
            (presentation
             :inherit medium
             :default-height 250)
            (jumbo
             :inherit medium
             :default-height 330)
            (t
             ;; I keep all properties for didactic purposes, but most can be
             ;; omitted.  See the fontaine manual for the technicalities:
             ;; <https://protesilaos.com/emacs/fontaine>.
             :default-family "Iosevka"
             :default-weight regular
             :default-slant normal
             :default-width normal
             :default-height 150

             :fixed-pitch-family nil ; falls back to :default-family
             :fixed-pitch-weight nil ; falls back to :default-weight
             :fixed-pitch-slant nil
             :fixed-pitch-width nil
             :fixed-pitch-height 1.0

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

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

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

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

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

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

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

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

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

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

             :line-spacing nil))))

The unravel-theme.el section about show-font

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

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

Prot is the developer and maintainer of this package.

To actually set fonts, use the fontaine package (The unravel-theme.el section about fontaine).

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

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

The unravel-theme.el section about variable-pitch-mode and font resizing

[ Watch Prot's video: Customise Emacs fonts (2024-01-16) ]

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

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

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

Information about the Iosevka Comfy fonts

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

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

Finally, we provide the unravel-theme.el module

(provide 'unravel-theme)

The unravel-essentials.el module

The unravel-essentials.el block with basic configurations

  ;;; Essential configurations
  (use-package emacs
    :ensure nil
    :demand t
    :config
  ;;;; General settings and common custom functions
    (setq help-window-select t)
    (setq next-error-recenter '(4)) ; center of the window
    (setq find-library-include-other-files nil) ; Emacs 29
    (setq tramp-connection-timeout (* 60 10)) ; seconds
    (setq save-interprogram-paste-before-kill t)
    (setq mode-require-final-newline t)
    (setq-default truncate-partial-width-windows nil)
    (setq eval-expression-print-length nil)
    (setq kill-do-not-save-duplicates t)
    (setq scroll-error-top-bottom t)
    (setq echo-keystrokes-help t) ; Emacs 30
    (setq epa-keys-select-method 'minibuffer)) ; Emacs 30

The unravel-essentials.el section for fixing PATH on OSX (exec-path-from-shell)

  (use-package exec-path-from-shell
    :ensure t
    :demand t
    :config
    (exec-path-from-shell-initialize))

The unravel-essentials.el configuration to track recently visited files (recentf)

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

The unravel-essentials.el settings for bookmarks

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

Also see the unravel-essentials.el settings for registers.

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

The unravel-essentials.el settings for registers

[ Watch Prot's video: Mark and register basics (2023-06-28). ]

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

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

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

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

The unravel-essentials.el settings for files

  (use-package files
    :ensure nil
    :config
    (setq confirm-kill-emacs #'y-or-n-p)
    (setq require-final-newline t))

The unravel-essentials.el section for delete-selection-mode

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

The unravel-essentials.el settings for tooltips

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

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

The unravel-essentials.el arrangement to run Emacs as a server

The "server" is functionally like the daemon, except it is run by the first Emacs frame we launch. With a running server, we can connect to it through a new emacsclient call. This is useful if we want to launch new frames that share resources with the existing running process.

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

The unravel-essentials.el section about easy-kill

easy-kill is a drop-in replacement for kill-ring-save, letting me easily and quickly copy / kill anything I want.

  (use-package easy-kill
    :ensure t
    :bind
    ("M-w" . easy-kill)) ; re-map kill-ring-save

The unravel-essentials.el section about expreg (tree-sitter mark syntactically)

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

The package offers the expreg-expand and expreg-contract commands.

I expect expreg to eventually completely replace easy-kill ()

  ;;; Mark syntactic constructs efficiently if tree-sitter is available (expreg)
  (when (treesit-available-p)
    (use-package expreg
      :ensure t
      :functions (prot/expreg-expand prot/expreg-expand-dwim)
      :bind ("C-M-SPC" . prot/expreg-expand-dwim) ; overrides `mark-sexp'
      :config
      (defun prot/expreg-expand (n)
        "Expand to N syntactic units, defaulting to 1 if none is provided interactively."
        (interactive "p")
        (dotimes (_ n)
          (expreg-expand)))

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

The unravel-essentials.el section for Battery display

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

The unravel-essentials.el section for OSX changes

These are modifications to basic configuration I use on my Mac OSX machine.

  ;;;; Configuration on Mac OS X machine
  (when (eq system-type 'darwin)
    (use-package ns-win
      :ensure nil
      :config
      (defun copy-from-osx ()
        "Make cut and paste work with the OS X clipboard"
        (shell-command-to-string "pbpaste"))

      (defun paste-to-osx (text &optional push)
        "Make cut and paste work with the OS X clipboard"
        (let ((process-connection-type nil))
          (let ((proc (start-process "pbcopy" "*Messages*" "pbcopy")))
            (process-send-string proc text)
            (process-send-eof proc))))

      (setq mac-command-modifier 'meta)
      (setq mac-option-modifier 'alt)
      (setq interprogram-cut-function #'paste-to-osx)
      (setq interprogram-paste-function #'copy-from-osx)
      ;; Work around a bug on OS X where system-name is a fully qualified
      ;; domain name
      (setq system-name (car (split-string system-name "\\.")))
  ;;; Binaries
      (setq vc-git-program (or (executable-find "git") "/usr/local/bin/git"))
      (setq epg-gpg-program (or (executable-find "gpg") "/usr/local/bin/gpg"))
  ;;; Source dirs
      ;; Note: These are hard-coded to my machine.
      (setq source-directory (expand-file-name "~/src/emacs/src/"))
      (setq find-function-C-source-directory (expand-file-name "~/src/emacs/src/"))))

The unravel-essentials.el section for simple.el changes

  (defun vedang/backward-kill-word-or-kill-region (&optional arg)
    "Rebind `C-w' to work differently based on whether a region is active.

  If the region is selected, retain the original behaviour, otherwise call
  `backward-kill-word' instead.  ARG is passed to `backward-kill-word'."
    (interactive "p")
    (if (region-active-p)
        (kill-region (region-beginning) (region-end))
      (backward-kill-word arg)))

  (use-package simple
    :ensure nil
    :after vertico ;; so that we can bind to vertico-map
    :bind
    ;; Rebind `C-w' to work differently based on whether a region is
    ;; active.
    ( :map global-map
      ("C-w" . vedang/backward-kill-word-or-kill-region)
      :map vertico-map
      ("C-l" . vedang/backward-kill-word-or-kill-region))
    :hook
    ((before-save . delete-trailing-whitespace)
     (text-mode . turn-on-visual-line-mode))
    :config
    (setq column-number-mode t))

Finally, we provide the unravel-essentials.el module

(provide 'unravel-essentials)

The unravel-completion.el module

The unravel-completion.el settings for completion styles

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

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

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

(There are more details in Prot's file, for the interested reader)

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

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

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

The unravel-completion.el for the orderless completion style

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

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

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

The unravel-completion.el settings to ignore letter casing

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

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

The unravel-completion.el settings for recursive minibuffers

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

The unravel-completion.el settings for default values

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

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

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

The unravel-completion.el settings for common interactions

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

  • The need to resize-mini-windows arises on some occasions where Emacs has to show text spanning multiple lines in the "mini windows". A common scenario for me is in Org mode buffers where I set the TODO keyword of a task with C-c C-t (M-x org-todo) and have this as my setting: (setq org-use-fast-todo-selection 'expert) Otherwise, this is not an issue anyway and I may also like other options for org-use-fast-todo-selection.
  • The read-answer-short is complementary to use-short-answers. This is about providing the shorter version to some confirmation prompt, such as y instead of yes.
  • The echo-keystrokes is set to a low value to show in the echo area the incomplete key sequence I have just typed. This is especially helpful for demonstration purposes but also to double check that I did not mistype something (I cannot touch-type, so this happens a lot).
  • The minibuffer-prompt-properties and advice to completing-read-multiple make it so that (i) the minibuffer prompt is not accessible with regular motions to avoid mistakes and (ii) prompts that complete multiple targets show an indicator about this fact. With regard to the latter in particular, we have prompts like that of Org to set tags for a heading (with C-c C-q else M-x org-set-tags-command) where more than one candidate can be provided using completion, provided each candidate is separated by the crm-separator (a comma by default, though Org uses : in that scenario). Remember that when using completion in the minibuffer, you can hit TAB to expand the selected choice without exiting with it. For cases when multiple candidates can be selected, you select the candidate, TAB, then input the crm-separator, and repeat until you are done selecting at which point you type RET.
  • Finally the file-name-shadow-mode is a neat little feature to remove the "shadowed" part of a file prompt while using something like C-x C-f (M-x find-file). File name shadowing happens when we invoke find-file and instead of first deleting the contents of the minibuffer, we start typing out the file system path we wish to visit. For example, I am in ~/Git/Projects/ and type directly after it something like ~/.local/share/fonts/, so Emacs displays ~/Git/Projects/~/.local/share/fonts/ with the original part greyed out. With file-name-shadow-mode the "shadowed" part is removed altogether. This is especially nice when combined with the completion style called partial-completion (The unravel-completion.el settings for completion styles).
  (use-package rfn-eshadow
    :ensure nil
    :hook (minibuffer-setup . cursor-intangible-mode)
    :config
    ;; Not everything here comes from rfn-eshadow.el, but this is fine.

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

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

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

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

    (file-name-shadow-mode 1))

The unravel-completion.el generic minibuffer UI settings

These are some settings for the default completion user interface.

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

The unravel-completion.el settings for saving the history (savehist-mode)

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

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

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

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

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

The unravel-completion.el settings for dynamic text expansion (dabbrev)

The built-in dabbrev package provides a text completion method that reads the contents of a buffer and expands the text before the cursor to match possible candidates…

The term "dabbrev" stands for "dynamic abbreviation". Emacs also has static, user-defined abbreviations (Settings for static text expansion (abbrev)).

  (use-package dabbrev
    :ensure nil
    :commands (dabbrev-expand dabbrev-completion)
    :config
  ;;;; `dabbrev' (dynamic word completion (dynamic abbreviations))
    (setq dabbrev-abbrev-char-regexp "\\sw\\|\\s_")
    (setq dabbrev-abbrev-skip-leading-regexp "[$*/=~']")
    (setq dabbrev-backward-only nil)
    (setq dabbrev-case-distinction 'case-replace)
    (setq dabbrev-case-fold-search nil)
    (setq dabbrev-case-replace 'case-replace)
    (setq dabbrev-check-other-buffers t)
    (setq dabbrev-eliminate-newlines t)
    (setq dabbrev-upcase-means-case-search t)
    (setq dabbrev-ignored-buffer-modes
          '(archive-mode image-mode docview-mode pdf-view-mode)))

The unravel-completion.el settings for dynamic text expansion (hippie)

Hippie is a built-in expansion mechanism that competes with dabbrev. I prefer hippie because of the simpler configuration and detailed expansion options it provides.

Hippie uses Dabbrev as one of the expansion sources, so all the dabbrev settings above are still important.

  (use-package hippie-ext
    :ensure nil
    :bind
    ;; Replace the default dabbrev
    ("M-/" . hippie-expand))

The unravel-completion.el for in-buffer completion popup (corfu)

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

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

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

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

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

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

The unravel-completion.el settings for consult

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

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

Also check: The unravel-search.el module.

  ;;; Enhanced minibuffer commands (consult.el)
  (use-package consult
    :ensure t
    :hook (completion-list-mode . consult-preview-at-point-mode)
    :bind
    (:map global-map
          ;; Prot's bindings
          ("M-K" . consult-keep-lines) ; M-S-k is similar to M-S-5 (M-%)
          ("M-F" . consult-focus-lines)   ; same principle
          ("M-s M-b" . consult-buffer) ; Start opening anything from here
          ("M-s M-f" . consult-fd)
          ("M-s M-g" . consult-ripgrep)
          ("M-s M-h" . consult-history)
          ("M-s M-i" . consult-imenu)
          ("M-s M-l" . consult-line)
          ("M-s M-m" . consult-mark)
          ("M-s M-y" . consult-yank-pop)
          ("M-s M-s" . consult-outline)
          ;; Overriding defaults: C-x bindings in `ctl-x-map'
          ("C-x M-:" . consult-complex-command) ;; orig. repeat-complex-command
          ("C-x 4 b" . consult-buffer-other-window) ;; orig. switch-to-buffer-other-window
          ("C-x 5 b" . consult-buffer-other-frame) ;; orig. switch-to-buffer-other-frame
          ("C-x t b" . consult-buffer-other-tab) ;; orig. switch-to-buffer-other-tab
          ("C-x r b" . consult-bookmark)         ;; orig. bookmark-jump
          ("C-x p b" . consult-project-buffer) ;; orig. project-switch-to-buffer
          ;; Custom M-# bindings for fast register access
          ("M-#" . consult-register-load)
          ("M-'" . consult-register-store) ;; orig. abbrev-prefix-mark (unrelated)
          ("C-M-#" . consult-register)
          ;; Other custom bindings
          ("M-y" . consult-yank-pop) ;; orig. yank-pop
          ;; M-g bindings in `goto-map'
          ("M-g e" . consult-compile-error)
          ("M-g f" . consult-flymake)   ;; Alternative: consult-flycheck
          ("M-g g" . consult-goto-line) ;; orig. goto-line
          ("M-g M-g" . consult-goto-line) ;; orig. goto-line
          ("M-g o" . consult-outline) ;; Alternative: consult-org-heading
          ;; My bindings from my Helm workflow
          ("C-x c i" . consult-imenu)
          ("C-c s" . consult-ripgrep)
          :map consult-narrow-map
          ("?" . consult-narrow-help))
    :config
    (setq consult-line-numbers-widen t)
    ;; (setq completion-in-region-function #'consult-completion-in-region)
    (setq consult-async-min-input 3)
    (setq consult-async-input-debounce 0.5)
    (setq consult-async-input-throttle 0.8)
    (setq consult-narrow-key nil)
    (setq consult-find-args
          (concat "find . -not ( "
                  "-path */.git* -prune "
                  "-or -path */.cache* -prune )"))
    (setq consult-preview-key 'any)
    (setq consult-project-function nil) ; always work from the current directory (use `cd' to switch directory)

    (add-to-list 'consult-mode-histories '(vc-git-log-edit-mode . log-edit-comment-ring))
    ;; the `imenu' extension is in its own file
    (require 'consult-imenu))

The unravel-completion.el section about embark

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

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

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

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

Depending on the configurations about the "indicator", the embark-act command will display an informative buffer with keys and their corresponding commands.

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

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

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

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

Here are the main keybindings you should be aware of:

  • C-. to enter embark with prot-embark-act-quit
  • E to embark-export (eg: when you want to see grep results in the grep-mode window
  • S to embark-collect (eg: when you just want to save the consult results for inspection)
  • A to embark-act (eg: to choose a new action in the middle of an existing action)
  ;;; Extended minibuffer actions and more (embark.el and prot-embark.el)
  (use-package embark
    :ensure t
    :defer 1
    :config
    (setq embark-confirm-act-all nil)
    (setq embark-mixed-indicator-both nil)
    (setq embark-mixed-indicator-delay 1.0)
    (setq embark-indicators '(embark-mixed-indicator embark-highlight-indicator))
    (setq embark-verbose-indicator-nested nil) ; I think I don't have them, but I do not want them either
    (setq embark-verbose-indicator-buffer-sections '(bindings))
    (setq embark-verbose-indicator-excluded-actions
          '(embark-cycle embark-act-all embark-collect embark-export embark-insert))

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

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

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

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

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

The unravel-completion.el section to configure completion annotations (marginalia)

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

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

The unravel-completion.el section for vertico

The vertico package by Daniel Mendler displays the minibuffer in a vertical layout. Under the hood, it takes care to be responsive and to handle even massive completion tables gracefully.

I use vertico-repeat to mimic the functionality that helm-resume would provide. The configuration for that is also part of this section.

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

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

  (use-package vertico-repeat
    :after vertico
    :bind ( :map global-map
            ("M-R" . vertico-repeat)
            :map vertico-map
            ("M-N" . vertico-repeat-next)
            ("M-P" . vertico-repeat-previous))
    :hook (minibuffer-setup . vertico-repeat-save))

  (use-package vertico-suspend
    :after vertico
    ;; Note: `enable-recursive-minibuffers' must be t
    :bind ( :map global-map
            ("M-S" . vertico-suspend)
            ("C-x c b" . vertico-suspend)))

Finally, we provide the unravel-completion.el module

(provide 'unravel-completion)

The unravel-search.el module

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

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

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

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

For starters, just learn:

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

Now on to the configurations.

The unravel-search.el section on imenu

imenu is amazing and I use it as my primary tool for navigating any file. Here, we make a small tweak to ensure that consult-imenu returns good imenu results to us.

  (use-package imenu
    :ensure nil
    :config
    ;; Don't limit item length, this makes imenu less useful in
    ;; consult-imenu
    (setq imenu-max-item-length 'unlimited))

The unravel-search.el section on isearch lax space

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

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

The unravel-search.el settings for isearch highlighting

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

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

The unravel-search.el section on isearch match counter

I think the following options should be enabled by default. They produce a counter next to the isearch prompt that shows the position of the current match relative to the total count (like 5/20). As we move to the next/previous match, the counter is updated accordingly.

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

The unravel-search.el tweaks for the occur buffer

Here I am making some minor tweaks to *occur* buffer (remember to read the introduction to this section (The unravel-search.el module)). I always want (i) the cursor to be at the top of the buffer, (ii) the current line to be highlighted, as it is easier for selection purposes …

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

The unravel-search.el modified isearch and occur key bindings

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

The unravel-search.el tweaks to xref, re-builder and grep

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

  (use-package xref
    :ensure nil
    :commands (xref-find-definitions xref-go-back)
    :config
    ;; All those have been changed for Emacs 28
    (setq xref-show-definitions-function #'xref-show-definitions-completing-read) ; for M-.
    (setq xref-show-xrefs-function #'xref-show-definitions-buffer) ; for grep and the like
    (setq xref-file-name-display 'project-relative)
    (setq xref-search-program (if (or (executable-find "rg")
                                      (executable-find "ripgrep"))
                                  'ripgrep
                                'grep)))

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

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

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

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

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

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

Finally, we provide the unravel-search.el module

(provide 'unravel-search)

The unravel-dired.el module

[ Watch Prot's talk: <https://protesilaos.com/codelog/2023-06-26-emacs-file-dired-basics/> (2023-06-26) ]

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

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

The unravel-dired.el settings for common operations

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

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

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

The unravel-dired.el switches for ls (how files are listed)

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

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

The unravel-dired.el setting for dual-pane Dired

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

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

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

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

The unravel-dired.el miscellaneous tweaks

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

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

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

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

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

The unravel-dired.el section about various conveniences

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

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

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

The unravel-dired.el section about dired-subtree

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

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

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

The unravel-dired.el section about wdired (writable Dired)

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

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

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

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

The unravel-dired.el section about trashed

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

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

Finally, we provide the unravel-dired.el module

(provide 'unravel-dired)

The unravel-window.el module

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

The unravel-window.el section about uniquifying buffer names

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

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

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

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

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

The unravel-window.el rules for displaying buffers (display-buffer-alist)

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

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

Here is the gist of what we do with it:

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

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

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

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

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

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

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

The unravel-window.el section about beframe

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

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

Some notes on how I use beframe:

  • I add beframe as a source for consult-buffers.

  • I replace the default C-x b (switch-to-buffer) with the beframe'd version of the same command.

    • This lets me focus on the buffers in the current frame, which is what I generally want. When I need to access files or buffers which are not open yet, I use either:

      • The consult version of the command (M-s M-b)
      • project.el to find a file in the current project (C-x p f)
  • When I am done with the frame, I delete all the buffers in the frame using beframe-kill-buffers-matching-regexp (C-c b k) with regex *. This automatically kills the frame.
  ;;; Frame-isolated buffers
  ;; Another package of mine.  Read the manual:
  ;; <https://protesilaos.com/emacs/beframe>.
  (use-package beframe
    :ensure t
    :hook (after-init . beframe-mode)
    :bind
    ("C-x f" . other-frame-prefix)
    ("C-c b" . beframe-prefix-map)
    ;; Replace the generic `buffer-menu'.  With a prefix argument, this
    ;; commands prompts for a frame.  Call the `buffer-menu' via M-x if
    ;; you absolutely need the global list of buffers.
    ("C-x C-b" . beframe-buffer-menu)
    ("C-x b" . beframe-switch-buffer)
    ("C-x B" . select-frame-by-name)
    :config
    (setq beframe-functions-in-frames '(project-prompt-project-dir))

    (defvar consult-buffer-sources)
    (declare-function consult--buffer-state "consult")

    (with-eval-after-load 'consult
      (defface beframe-buffer
        '((t :inherit font-lock-string-face))
        "Face for `consult' framed buffers.")

      (defun my-beframe-buffer-names-sorted (&optional frame)
        "Return the list of buffers from `beframe-buffer-names' sorted by visibility.
  With optional argument FRAME, return the list of buffers of FRAME."
        (beframe-buffer-names frame :sort #'beframe-buffer-sort-visibility))

      (defvar beframe-consult-source
        `( :name     "Frame-specific buffers (current frame)"
           :narrow   ?F
           :category buffer
           :face     beframe-buffer
           :history  beframe-history
           :items    ,#'my-beframe-buffer-names-sorted
           :action   ,#'switch-to-buffer
           :state    ,#'consult--buffer-state))

      (add-to-list 'consult-buffer-sources 'beframe-consult-source)))

The unravel-window.el configuration of undelete-frame-mode and winner-mode

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

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

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

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

COMMENT The unravel-window.el use of contextual header line (breadcrumb)

breadcrumb is an interesting package when demo'ing something, but not useful to me in day-to-day use. Commenting it out until I need it.

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

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

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

The unravel-window.el section for Zone (zone)

  (use-package zone
    :ensure nil
    :config
    (zone-when-idle 300))

The unravel-window.el section for displaying time

I like being able to see the time in the modeline.

  (use-package time
    :ensure nil
    :config
    (setq display-time-day-and-date t)
    (display-time))

Finally, we provide the unravel-window.el module

(provide 'unravel-window)

The unravel-git.el module

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

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

The unravel-git.el section about ediff

[ Watch Prot's talk: Emacs: ediff basics (2023-12-30) ]

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

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

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

The unravel-git.el section about project.el

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

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

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

If any of the project.el commands is called from outside a project, it first prompts for a project and then carries out its action. For example, project-find-file will ask for a project to use, then switch to it, and then prompt for a file inside of the specified project.

While inside a project, we have many commands that operate on the project level. For example, C-x p f (project-find-file) searches for a file across the project, while C-x p b (project-switch-to-buffer) switches to a buffer that is specific to the project. Again, check the project-prefix-map for available commands.

If not inside a project, the project-related commands will first prompt to select a project (same as typing C-x p p) and then carry out their action.

I combine projects with my beframe package, so that when I switch to a project I get a new frame that limits the buffers I visit there limited to that frame (The unravel-window.el section about beframe).

  ;;;; `project'
  (use-package project
    :ensure nil
    :bind
    (("C-x p ." . project-dired)
     ("C-x p C-g" . keyboard-quit)
     ("C-x p <return>" . project-dired)
     ("C-x p <delete>" . project-forget-project))
    :config
    (setopt project-switch-commands
            '((project-find-file "Find file")
              (project-find-regexp "Find regexp")
              (project-find-dir "Find directory")
              (project-dired "Root dired")
              (project-vc-dir "VC-Dir")
              (project-shell "Shell")
              (keyboard-quit "Quit")))
    (setq project-vc-extra-root-markers '(".project")) ; Emacs 29
    (setq project-key-prompt-style t)) ; Emacs 30

The unravel-git.el section about diff-mode

;;;; `diff-mode'
(use-package diff-mode
  :ensure nil
  :defer t
  :config
  (setq diff-default-read-only t)
  (setq diff-advance-after-apply-hunk t)
  (setq diff-update-on-the-fly t)
  ;; The following are from Emacs 27.1
  (setq diff-refine nil) ; I do it on demand, with my `agitate' package (more below)
  (setq diff-font-lock-prettify t) ; I think nil is better for patches, but let me try this for a while
  (setq diff-font-lock-syntax 'hunk-also))

The unravel-git.el section about magit (great Git client)

The magit package, maintained by Jonas Bernoulli, is the best front-end to git I have ever used. Not only is it excellent at getting the job done, it also helps you learn more about what git has to offer.

At the core of its interface is transient. This is a library that was originally developed as Magit-specific code that was then abstracted away and ultimately incorporated into Emacs version 29. With transient, we get a window pop up with keys and commands corresponding to them. The window is interactive, as the user can set a value or toggle an option and have it take effect when the relevant command is eventually invoked. For git, in particular, this interface is a genious way to surface the plethora of options.

To start, call the command magit-status. It brings up a buffer that shows information about the state of the repository. Sections include an overview of the current HEAD, untracked files, unstaged changes, staged changes, and recent commits. Each section's visibility state can be cycled by pressing TAB (variations of this are available—remember to do C-h m (describe-mode) in an unfamiliar major mode to get information about its key bindings).

From the status buffer, we can perform all the usual version control operations. By typing ? (magit-dispatch), we bring up the main transient menu, with keys that then bring up their own submenus, such as for viewing commit logs, setting the remotes, switching branches, etc.

  ;;; Interactive and powerful git front-end (Magit)
  (use-package transient
    :defer t
    :config
    (setq transient-show-popup 0.5))

  (use-package magit
    :ensure t
    :bind ("C-x g" . magit-status)
    :init
    (setq magit-define-global-key-bindings nil)
    ;; (setq magit-section-visibility-indicator '("⮧"))
    :config
    (setq git-commit-summary-max-length 50)
    ;; NOTE 2023-01-24: I used to also include `overlong-summary-line'
    ;; in this list, but I realised I do not need it.  My summaries are
    ;; always in check.  When I exceed the limit, it is for a good
    ;; reason.
    ;; (setq git-commit-style-convention-checks '(non-empty-second-line))

    (setq magit-diff-refine-hunk t))

  (use-package magit-repos
    :ensure nil ; part of `magit'
    :commands (magit-list-repositories)
    :init
    (setq magit-repository-directories
          '(("~/src/prototypes" . 1))))

The unravel-git.el call to provide

Finally, we provide the module.

(provide 'unravel-git)

The unravel-org.el module

Watch these talks that I've given about Org:

Watch these talks by Prot:

At its core, Org is a plain text markup language. By "markup language", we refer to the use of common characters to apply styling, such as how a word wrapped in asterisks acquires strong emphasis. Check the video I link to above on the basics of Org mode.

Though what makes Org powerful is not the markup per se, but the fact that it has a rich corpus of Emacs Lisp code that does a lot with this otherwise plain text notation. Some of the headline features:

  • Cycle the visibility of any heading and its subheadings. This lets you quickly fold a section you do not need to see (or reveal the one you care about).
  • Mix prose with code in a single document to either make the whole thing an actual program or to evaluate/demonstrate some snippets.
  • Convert ("export") an Org file to a variety of formats, including HTML and PDF.
  • Use LaTeX inside of Org files to produce a scientific paper without all the markup of LaTeX.
  • Manage TODO lists and implement a concomitant methodology of labelling task states.
  • Quickly shift a "thing" (heading, list item, paragraph, …) further up or down in the file.
  • Use tables with formulas as a lightweight alternative to spreadsheet software.
  • Capture data or fleeting thoughts efficiently using templates.
  • Maintain an agenda for all your date-bound activities.
  • Clock in and out of tasks, to eventually track how you are spending your time.
  • Link to files regardless of file type. This includes special links such as to an Info manual or an email, if you also have that running locally and integrated with Emacs (The unravel-email.el module).

In other words, Org is highly capable and widely considered one of the killer apps of Emacs.

This section covers the relevant configurations. You will notice that it is not limited to Org, as some other built-in features are also relevant here.

The unravel-org.el section on the calendar

The calendar is technically independent of Org, though it tightly integrates with it. We witness this when we are setting timestamps, such as while setting a SCHEDULED or DEADLINE entry for a given heading. All I do here is set some stylistic preferences.

Note that Emacs also has a diary command. I used it for a while, but Org is far more capable, so I switched to it completely.

;;; Calendar
(use-package calendar
  :ensure nil
  :commands (calendar)
  :config
  (setq calendar-mark-diary-entries-flag nil)
  (setq calendar-mark-holidays-flag t)
  (setq calendar-mode-line-format nil)
  (setq calendar-time-display-form
        '( 24-hours ":" minutes
           (when time-zone (format "(%s)" time-zone))))
  (setq calendar-week-start-day 1)      ; Monday
  (setq calendar-date-style 'iso)
  (setq calendar-time-zone-style 'numeric) ; Emacs 28.1

  (require 'solar)
  (setq calendar-latitude 35.17         ; Not my actual coordinates
        calendar-longitude 33.36)

  (require 'cal-dst)
  (setq calendar-standard-time-zone-name "+0200")
  (setq calendar-daylight-time-zone-name "+0300"))

The unravel-org.el section about appointment reminders (appt.el)

The built in appt.el defines functionality for handling notifications about appointments. It is originally designed to work with the generic diary feature (the M-x diary one, I mean), which I do not use anymore, but also integrates nicely with the Org agenda (The unravel-org.el Org agenda settings). I deepen this integration further, such that after adding a task or changing its state, the appointments mechanism re-reads my data to register new notifications. This is done via a series of hooks and with the use of the advice feature of Emacs Lisp.

Here I am setting some simple settings to keep appointment notifations minimal. I do not need them to inform me about the contents of my next entry on the agenda: just show text on the mode line telling me how many minutes are left until the event.

In Org files, every heading can have an APPT_WARNTIME property: it takes a numeric value representing minutes for a forewarning from appt.el. I use this in tandem with org-capture for tasks that need to be done at a specific time, such as coaching sessions (The unravel-org.el Org capture templates (org-capture)).

;;; Appt (appointment reminders which also integrate with Org agenda)
(use-package appt
  :ensure nil
  :commands (appt-activate)
  :config
  (setq appt-display-diary nil
        appt-display-format nil
        appt-display-mode-line t
        appt-display-interval 3
        appt-audible nil ; TODO 2023-01-25: t does nothing because I disable `ring-bell-function'?
        appt-warning-time-regexp "appt \\([0-9]+\\)" ; This is for the diary
        appt-message-warning-time 6)

  (with-eval-after-load 'org-agenda
    (appt-activate 1)

    ;; Create reminders for tasks with a due date when this file is read.
    (org-agenda-to-appt)))

The unravel-org.el section on paragraphs

  (use-package paragraphs
    :ensure nil
    :config
    (setq sentence-end-double-space nil))

The unravel-org.el section with basic Org settings

Org, also known as "Org mode", is one of the potentially most useful feature sets available to every Emacs user. At its core, Org is a lightweight markup language: you can have headings and paragraphs, mark a portion of text with emphasis, produce bullet lists, include code blocks, and the like. Though what really sets Org apart from other markup languages is the rich corpus of Emacs Lisp written around it to do all sorts of tasks with this otherwise plain text format.

With Org you can write technical documents (e.g. the manuals of all my Emacs packages), maintain a simple or highly sophisticated system for task management, organise your life using the agenda, write tables that can evaluate formulas to have spreadsheet functionality, have embedded LaTeX, evaluate code blocks in a wide range of programming languages and reuse their results for literate programming, include the contents of other files into a singular file, use one file to generate other files/directories with all their contents, and export the Org document to a variety of formats like .pdf and .odt. Furthermore, Org can be used as a lightweight, plain text database, as each heading can have its own metadata. This has practical applications in most of the aforementioned.

In short, if something can be done with plain text, Org probably does it already or has all the elements for piecing it together. This document, among many of my published works, is testament to Org's sheer power, which I explained at greater length in a video demonstration: Advanced literate configuration with Org (2023-12-18).

This being Emacs, everything is customisable and Org is a good example of this. There are a lot of user options for us to tweak things to our liking. I do as much, though know that Org is perfectly usable without any configuration. The following sections contain further commentary on how I use Org.

  ;;; Org-mode (personal information manager)
  (use-package org
    :ensure nil
    :init
    (setq org-directory (expand-file-name "~/Tresors/Documents/diary"))
    (setq org-default-notes-file (expand-file-name "brain/daily.org" org-directory))
    (setq org-imenu-depth 7)

    (add-to-list 'safe-local-variable-values '(org-hide-leading-stars . t))
    (add-to-list 'safe-local-variable-values '(org-hide-macro-markers . t))
    :bind
    ( :map global-map
      ("C-c l" . org-store-link)
      ("C-c o" . org-open-at-point-global)
      :map org-mode-map
      ;; I don't like that Org binds one zillion keys, so if I want one
      ;; for something more important, I disable it from here.
      ("C-'" . nil)
      ("C-," . nil)
      ("M-;" . nil)
      ("C-c M-l" . org-insert-last-stored-link)
      ("C-c C-M-l" . org-toggle-link-display)
      ("M-." . org-edit-special) ; alias for C-c ' (mnenomic is global M-. that goes to source)
      :map org-src-mode-map
      ("M-," . org-edit-src-exit) ; see M-. above
      )
    :config
  ;;;; general settings
    (setq org-ellipsis "...")
    (setq org-adapt-indentation nil)      ; No, non, nein, όχι!
    (setq org-special-ctrl-a/e nil)
    (setq org-special-ctrl-k nil)
    (setq org-use-speed-commands t)
    (setq org-M-RET-may-split-line '((default . nil)))
    (setq org-hide-emphasis-markers nil)
    (setq org-hide-macro-markers nil)
    (setq org-hide-leading-stars nil)
    (setq org-cycle-separator-lines 0)
    (setq org-structure-template-alist
          '(("s" . "src")
            ("e" . "src emacs-lisp")
            ("E" . "src emacs-lisp :results value code :lexical t")
            ("t" . "src emacs-lisp :tangle FILENAME")
            ("T" . "src emacs-lisp :tangle FILENAME :mkdirp yes")
            ("x" . "example")
            ("X" . "export")
            ("q" . "quote")))
    (setq org-fold-catch-invisible-edits 'show)
    (setq org-return-follows-link nil)
    (setq org-loop-over-headlines-in-active-region 'start-level)
    (setq org-modules '(ol-info ol-eww))
    (setq org-use-sub-superscripts '{})
    (setq org-insert-heading-respect-content t)
    (setq org-read-date-prefer-future 'time)
    (setq org-highlight-latex-and-related nil) ; other options affect elisp regexp in src blocks
    (setq org-fontify-quote-and-verse-blocks t)
    (setq org-fontify-whole-block-delimiter-line t)
    (setq org-track-ordered-property-with-tag t)
    (setq org-highest-priority ?A)
    (setq org-lowest-priority ?C)
    (setq org-default-priority ?A)
    (setq org-priority-faces nil))

The unravel-org.el section for archival settings

  ;;;; archival, org-archive
  (use-package org
    :ensure nil
    :config
    ;; Setup directory and file paths for org
    (defvar org-archive-directory (concat org-directory "/archive")
      "Directory under which all archived content is stored.")
    (setq org-archive-location (concat org-archive-directory "/%s_archive::")))

The unravel-org.el section for narrowing and folding

Narrowing and folding are really powerful ways of working with org-mode, since we tend to create so many nodes and subtrees when we write documents in Org

  ;;; Narrowing and Folding
  (use-package org
    :ensure nil
    :bind
    ( :map narrow-map
      ("b" . org-narrow-to-block)
      ("e" . org-narrow-to-element)
      ("s" . org-narrow-to-subtree)))

  (use-package org-fold
    :ensure nil
    :config
    (setq org-fold-catch-invisible-edits 'show-and-error))

The unravel-org.el Org to-do and refile settings

One of the many use-cases for Org is to maintain a plain text to-do list. A heading that starts with a to-do keyword, such as TODO is treated as a task and its state is considered not completed.

We can switch between the task states with shift and the left or right arrow keys. Or we can select a keyword directly with C-c C-t, which calls org-todo by default. I personally prefer the latter approach, as it is more precise.

Whenever a task state changes, we can log that event in a special LOGBOOK drawer. This is automatically placed right below the heading, before any paragraph text. Logging data is an opt-in feature, which I consider helpful (The unravel-org.el Org time/state logging).

Tasks can be associated with timestamps, typically a scheduled date+time or a deadline+time. This can be helpful when we are reviewing the source Org file, though it really shines in tandem with the agenda. Any heading that has a timestamp and which belongs to a file in the org-agenda-files will show up on the agenda in the given date (The unravel-org.el Org agenda settings).

By default, the org-todo-keywords are TODO and DONE. We can write more keywords if we wish to implement a descriptive workflow. For example, we can have a WAIT keyword for something that is to be done but is not actionable yet. While the number of keywords is not limited, the binary model is the same: we have words that represent the incomplete state and those that count as the completion of the task. For instance, both CANCEL and DONE mean that the task is not actionable anymore and we move on to other things. As such, the extra keywords are a way for the user to make tasks more descriptive and easy to find. In the value of the org-todo-keywords, we use the bar character to separate the incomplete state to the left from the completed one to the right.

One of the agenda's headiline features is the ability to produce a view that lists headings with the given keyword. So having the right terms can make search and retrieval of data more easy. On the flip-side, too many keywords add cognitive load and require more explicit search terms to yield the desired results. I used to work with a more descriptive set of keywords, but ultimately decided to keep things simple.

The refile mechanism is how we can reparent a heading, by moving it from one place to another. We do this with the command org-refile, bound to C-c C-w by default. A common workflow where refiling is essential is to have an "inbox" file or heading, where unprocessed information is stored at, and periodically process its contents to move the data where it belongs. Though it can also work fine without any such inbox, in those cases where a heading should be stored someplace else. The org-refile-targets specifies the files that are available when we try to refile the current heading. With how I set it up, all the agenda files plus the current file's headings up to level 2 are included as possible targets.

In terms of workflow, I have not done a refile in a very long time, because my entries always stay in the same place as I had envisaged at the capture phase (The unravel-org.el Org capture templates (org-capture)).

  ;;;; refile, todo
  (use-package org
    :ensure nil
    :config
    (setq org-refile-targets
          '((org-agenda-files . (:maxlevel . 2))
            (nil . (:maxlevel . 2))))
    (setq org-refile-use-outline-path t)
    (setq org-refile-allow-creating-parent-nodes 'confirm)
    (setq org-refile-use-cache t)
    (setq org-reverse-note-order nil)
    (setq org-todo-keywords
          '((sequence "TODO(t)" "|" "CANCEL(c@)" "DONE(d!)")))

    (defface prot/org-bold-done
      '((t :inherit (bold org-done)))
      "Face for bold DONE-type Org keywords.")

    (setq org-todo-keyword-faces
          '(("CANCEL" . prot/org-bold-done)))
    (setq org-use-fast-todo-selection 'expert)

    (setq org-fontify-done-headline nil)
    (setq org-fontify-todo-headline nil)
    (setq org-fontify-whole-heading-line nil)
    (setq org-enforce-todo-dependencies t)
    (setq org-enforce-todo-checkbox-dependencies t))

The unravel-org.el Org heading tags

Each Org heading can have one or more tags associated with it, while all headings inherit any potential #+FILETAGS. We can add tags to a heading when the cursor is over it by typing the ever flexible C-c C-c. Though the more specific org-set-tags-command also gets the job done, plus it does not require that the cursor is positioned on the heading text.

Tagging is useful for searching and retrieving the data we store. The Org agenda, in particular, provides commands to filter tasks by tag:

  ;;;; tags
  (use-package org
    :ensure nil
    :config
    (setq org-tag-alist nil)
    (setq org-auto-align-tags nil)
    (setq org-tags-column 0))

The unravel-org.el Org time/state logging

Org can keep a record of state changes, such as when we set an entry marked with the TODO keyword as DONE or when we reschedule an appointment (The unravel-org.el Org agenda settings). This data is stored in a LOGBOOK drawer right below the heading. I choose to keep track of this information, as it is sometimes useful to capture mistakes or figure out intent in the absence of further clarification (though I do tend to write why something happened).

  ;;;; log
  (use-package org
    :ensure nil
    :config
    (setq org-log-done 'time)
    (setq org-log-into-drawer t)
    (setq org-log-note-clock-out nil)
    (setq org-log-redeadline 'time)
    (setq org-log-reschedule 'time))

The unravel-org.el Org link settings

One of the nice things about Org is its flexible linking mechanism. It can produce links to a variety of file types or buffers and even navigate to a section therein.

At its simplest form, we have the file link type, which points to a file system path, with an optional extension for a match inside the file, as documented in the manual. Evaluate this inside of Emacs:

(info "(org) Search Options")

I bind org-store-link in main section of the Org configuration: The unravel-org.el section with basic Org settings.

;;;; links
(use-package org
  :ensure nil
  :config
  (setq org-link-context-for-files t)
  (setq org-link-keep-stored-after-insertion nil)
  (setq org-id-link-to-org-use-id 'create-if-interactive-and-no-custom-id))

The unravel-org.el Org code block settings

More generally, Org is capable of evaluating code blocks and passing their return value to other code blocks. It is thus possible to write a fully fledged program as an Org document. This paradigm is known as "literate programming". In the case of an Emacs configuration, such as mine, it is called a "literate configuration" or variants thereof. I did a video about my setup: Advanced literate configuration with Org (2023-12-18).

Org can evaluate code blocks in many languages. This is known as "Org Babel" and the files which implement support for a given language are typically named ob-LANG.el where LANG is the name of the language. We can load the requisite code for the languages we care about with something like the following:

(require 'ob-python)

;; OR

(use-package ob-python)

;; OR for more control

(use-package ob-python
  :after org
  :config
  ;; Settings here
  )
;;;; code blocks
(use-package org
  :ensure nil
  :config
  (setq org-confirm-babel-evaluate nil)
  (setq org-src-window-setup 'current-window)
  (setq org-edit-src-persistent-message nil)
  (setq org-src-fontify-natively t)
  (setq org-src-preserve-indentation nil)
  (setq org-src-tab-acts-natively t)
  (setq org-edit-src-content-indentation 2))

The unravel-org.el Org export settings

;;;; export
(use-package org
  :ensure nil
  :init
  ;; NOTE 2023-05-20: Must be evaluated before Org is loaded,
  ;; otherwise we have to use the Custom UI.  No thanks!
  (setq org-export-backends '(html texinfo md))
  :config
  (setq org-export-with-toc t)
  (setq org-export-headline-levels 8)
  (setq org-export-dispatch-use-expert-ui nil)
  (setq org-html-htmlize-output-type nil)
  (setq org-html-head-include-default-style nil)
  (setq org-html-head-include-scripts nil))

The unravel-org.el Org capture templates (org-capture)

The org-capture command allows us to quickly store data in some structured way. This is done with the help of a templating system where we can, for example, record the date the entry was recorded, prompt for user input, automatically use the email's subject as the title of the task, and the like. The documentation string of org-capture-templates covers the technicalities.

  ;;;; org-capture
  (use-package org-capture
    :ensure nil
    :bind ("C-c c" . org-capture))

The unravel-org.el section on YASnippet

yasnippet is the primary tool I use when I need templates of any kind. Since most of my templating is in org-mode, it makes sense to keep YAS in this section.

  (use-package yasnippet
    :ensure t
    :config
    (yas-global-mode 1)
    (add-to-list 'hippie-expand-try-functions-list
                 'yas-hippie-try-expand))

  (use-package yasnippet-snippets
    :ensure t
    :after yasnippet)

The unravel-org.el Org agenda settings

[ Watch Prot's talk: Demo of my custom Org block agenda (2021-12-09). It has changed a bit since then, but the idea is the same. ]

  ;;;; agenda
  (use-package org-agenda
    :ensure nil
    :bind
    ("C-c a" . org-agenda)
    :config

  ;;;;; Basic agenda setup
    (setq org-agenda-files `(,org-directory))
    (setq org-agenda-span 'week)
    (setq org-agenda-start-on-weekday 1)  ; Monday
    (setq org-agenda-confirm-kill t)
    (setq org-agenda-show-all-dates t)
    (setq org-agenda-show-outline-path nil)
    (setq org-agenda-window-setup 'current-window)
    (setq org-agenda-skip-comment-trees t)
    (setq org-agenda-menu-show-matcher t)
    (setq org-agenda-menu-two-columns nil)
    (setq org-agenda-sticky nil)
    (setq org-agenda-custom-commands-contexts nil)
    (setq org-agenda-max-entries nil)
    (setq org-agenda-max-todos nil)
    (setq org-agenda-max-tags nil)
    (setq org-agenda-max-effort nil)

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

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

  ;;;;; Agenda diary entries
    (setq org-agenda-insert-diary-strategy 'date-tree)
    (setq org-agenda-insert-diary-extract-time nil)
    (setq org-agenda-include-diary nil)
    ;; I do not want the diary, but there is no way to disable it
    ;; altogether.  This creates a diary file in the /tmp directory.
    (setq diary-file (make-temp-file "emacs-diary-"))
    (setq org-agenda-diary-file 'diary-file) ; TODO 2023-05-20: review Org diary substitute

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

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

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

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

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

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

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

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

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

    ;; ;;;;; Agenda habits
    ;;   (require 'org-habit)
    ;;   (setq org-habit-graph-column 50)
    ;;   (setq org-habit-preceding-days 9)
    ;;   ;; Always show the habit graph, even if there are no habits for
    ;;   ;; today.
    ;;   (setq org-habit-show-all-today t)
    )

Finally, we provide the unravel-org.el module

(provide 'unravel-org)

The unravel-shell.el module

I use vterm for my shell inside Emacs, and at the moment, this section is about vterm configuration only.

The unravel-shell.el section for vterm

  ;;;; Vterm
  (use-package vterm
    :ensure t
    :bind
    ("C-x m" . vterm)
    :config
    (setq vterm-shell (or (executable-find "fish") "/opt/homebrew/bin/fish")))

Finally, we provide the unravel-shell.el module

(provide 'unravel-shell)

The unravel-langs.el module

The unravel-langs.el settings for TAB

  ;;;; Tabs, indentation, and the TAB key
  (use-package emacs
    :ensure nil
    :demand t
    :config
    (setq tab-always-indent 'complete)
    (setq tab-first-completion 'word-or-paren-or-punct) ; Emacs 27
    (setq-default tab-width 4
                  indent-tabs-mode nil))

The unravel-langs.el settings show-paren-mode

The built-in show-paren-mode highlights the parenthesis on the opposite end of the current symbolic expression. It also highlights matching terms of control flow in programming languages that are not using parentheses like Lisp: for instance, in a bash shell script it highlights the if and fi keywords. This mode also works for prose and I use it globally. Simple and effective!

  ;;;; Parentheses (show-paren-mode)
  (use-package paren
    :ensure nil
    :hook (prog-mode . show-paren-local-mode)
    :config
    (setq show-paren-style 'mixed)
    (setq show-paren-when-point-in-periphery nil)
    (setq show-paren-when-point-inside-paren nil)
    (setq show-paren-context-when-offscreen 'overlay)) ; Emacs 29

The unravel-langs.el settings for eldoc

The built-in eldoc feature is especially useful in programming modes. While we are in a function call, it produces an indicator in the echo area (where the minibuffer appears upon invocation) that shows the name of the function, the arguments it takes, if any, and highlights the current argument we are positioned at. This way, we do not have to go back to review the signature of the function just to remember its arity. Same principle for variables, where eldoc-mode puts the first line of their documentation string in the echo area.

;;;; Eldoc (Emacs live documentation feedback)
(use-package eldoc
  :ensure nil
  :hook (prog-mode . eldoc-mode)
  :config
  (setq eldoc-message-function #'message)) ; don't use mode line for M-x eval-expression, etc.

The unravel-langs.el settings for eglot (LSP client)

The built-in eglot feature, developed and maintained by João Távora, is Emacs' own client for the Language Server Protocol (LSP). The LSP technology is all about enhancing the ability of a text editor to work with a given programming language. This works by installing a so-called "language server" on your computer, which the "LSP client" (i.e. eglot) will plug into. A typical language server provides the following capabilities:

Code completion
This can be visualised for in-buffer automatic expansion of function calls, variables, and the like (The unravel-completion.el for in-buffer completion popup (corfu)).
Code linting
To display suggestions, warnings, or errors. These are highlighted in the buffer, usually with an underline, and can also be displayed in a standalone buffer with the commands flymake-show-buffer-diagnostics, flymake-show-project-diagnostics (The unravel-langs.el settings for code linting (flymake)).
Code navigation and cross-referencing
While over a symbol, use a command to jump directly to its definition. The default key bindings for going forth and then back are M-. (xref-find-definitions) and M-, (xref-go-back).

Assuming the language server is installed, to start using the LSP client in a given file, do M-x eglot. To make this happen automatically for every newly visited file, add a hook like this:

(add-hook 'SOME-MAJOR-mode #'eglot-ensure)

I use eglot as the main LSP entry point, and as such, I have key-bindings for the common functionality implemented by eglot.

Note that demanding eglot is not a mistake. I want it loaded so that I can advice functions for different programming langauges. For example, Python uses emacs-pet for tracking the environment of each project, and this needs to load after eglot so that it can advice the necessary functions.

  ;;;; Eglot (built-in client for the language server protocol)
  (use-package eglot
    :ensure nil
    :demand t ; Not a mistake, we need to load Eglot elisp code before
              ; we open any Python file.
    :functions (eglot-ensure)
    :commands (eglot)
    :bind
    ( :map eglot-mode-map
      ("C-c e r" . eglot-rename)
      ("C-c e o" . eglot-code-action-organize-imports)
      ("C-c e d" . eldoc)
      ("C-c e c" . eglot-code-actions)
      ("C-c e f" . eglot-format)
      ;; Since eglot plugs into flymake anyway
      ("C-c e l" . flymake-show-buffer-diagnostics))
    :config
    (setq eglot-sync-connect nil)
    (setq eglot-autoshutdown t)
    (setq eglot-extend-to-xref t))

The unravel-langs.el settings for markdown-mode

The markdown-mode lets us edit Markdown files. We get syntax highlighting and several extras, such as the folding of headings and navigation between them. The mode actually provides lots of added functionality for GitHub-flavoured Markdown and to preview a Markdown file's HTML representation on a web page. Though I only use it for basic text editing.

;;; Markdown (markdown-mode)
(use-package markdown-mode
  :ensure t
  :defer t
  :config
  (setq markdown-fontify-code-blocks-natively t))

The unravel-langs.el settings for csv-mode

The package csv-mode provides support for .csv files. I do need this on occasion, even though my use-case is pretty basic. For me, the killer feature is the ability to create a virtual tabulated listing with the command csv-align-mode: it hides the field delimiter (comma or space) and shows a tab stop in its stead.

;;; csv-mode
(use-package csv-mode
  :ensure t
  :commands (csv-align-mode))

The unravel-langs.el settings for spell checking (flyspell)

  ;;; Flyspell
  (use-package flyspell
    :ensure nil
    :bind
    ( :map flyspell-mode-map
      ("C-;" . nil)
      :map flyspell-mouse-map
      ("<mouse-3>" . flyspell-correct-word))
    :config
    (setq flyspell-issue-message-flag nil)
    (setq flyspell-issue-welcome-flag nil)
    (setq ispell-program-name "aspell")
    (setq ispell-dictionary "en_GB"))

The unravel-langs.el settings for code linting (flymake)

The built-in flymake feature defines an interface for viewing the output of linter programs. A "linter" parses a file and reports possible notes/warnings/errors in it. With flymake we get these diagnostics in the form of a standalone buffer as well as inline highlights (typically underlines combined with fringe indicators) for the portion of text in question. The linter report is displayed with the command flymake-show-buffer-diagnostics, or flymake-show-project-diagnostics. Highlights are shown in the context of the file.

The built-in eglot feature uses flymake internally to handle the LSP linter output (The unravel-langs.el settings for eglot).

As for what I have in this configuration block, the essentials for me are the user options flymake-start-on-save-buffer and flymake-start-on-flymake-mode as they make the linter update its report when the buffer is saved and when flymake-mode is started, respectively. Otherwise, we have to run it manually, which is cumbersome.

The package-lint-flymake package by Steve Purcell adds the glue code to make flymake report issues with Emacs Lisp files for the purposes of packaging. I use it whenever I work on my numerous Emacs packages.

  ;;; Flymake
  (use-package flymake
    :ensure nil
    :bind
    ( :map flymake-mode-map
      ("C-c ! s" . flymake-start)
      ("C-c ! l" . flymake-show-buffer-diagnostics)  ; Emacs28
      ("C-c ! L" . flymake-show-project-diagnostics) ; Emacs28
      ("C-c ! n" . flymake-goto-next-error)
      ("C-c ! p" . flymake-goto-prev-error))
    :config
    (setq flymake-fringe-indicator-position 'left-fringe)
    (setq flymake-suppress-zero-counters t)
    (setq flymake-no-changes-timeout nil)
    (setq flymake-start-on-flymake-mode t)
    (setq flymake-start-on-save-buffer t)
    (setq flymake-proc-compilation-prevents-syntax-check t)
    (setq flymake-wrap-around nil)
    (setq flymake-mode-line-format
          '("" flymake-mode-line-exception flymake-mode-line-counters))
    (setq flymake-mode-line-counter-format
          '("" flymake-mode-line-error-counter
            flymake-mode-line-warning-counter
            flymake-mode-line-note-counter ""))
    (setq flymake-show-diagnostics-at-end-of-line nil)) ; Emacs 30

  ;;; Elisp packaging requirements
  (use-package package-lint-flymake
    :ensure t
    :after flymake
    :config
    (add-hook 'flymake-diagnostic-functions #'package-lint-flymake))

The unravel-langs.el settings for outline-minor-mode

;;; General configurations for prose/writing

;;;; `outline' (`outline-mode' and `outline-minor-mode')
(use-package outline
  :ensure nil
  :bind
  ("<f10>" . outline-minor-mode)
  :config
  (setq outline-minor-mode-highlight nil) ; emacs28
  (setq outline-minor-mode-cycle t) ; emacs28
  (setq outline-minor-mode-use-buttons nil) ; emacs29---bless you for the nil option!
  (setq outline-minor-mode-use-margins nil)) ; as above

The unravel-langs.el settings for dictionary

Use the entry point M-x dictionary-search

;;;; `dictionary'
(use-package dictionary
  :ensure nil
  :config
  (setq dictionary-server "dict.org"
        dictionary-default-popup-strategy "lev" ; read doc string
        dictionary-create-buttons nil
        dictionary-use-single-buffer t))

The unravel-langs.el settings for denote (notes and file-naming)

This is another one of my packages and is extended by my consult-denote package (The unravel-langs.el integration between Consult and Denote (consult-denote)).

Denote is a simple note-taking tool for Emacs. It is based on the idea that notes should follow a predictable and descriptive file-naming scheme. The file name must offer a clear indication of what the note is about, without reference to any other metadata. Denote basically streamlines the creation of such files while providing facilities to link between them.

Denote's file-naming scheme is not limited to "notes". It can be used for all types of file, including those that are not editable in Emacs, such as videos. Naming files in a consistent way makes their filtering and retrieval considerably easier. Denote provides relevant facilities to rename files, regardless of file type.

Prot is the developer and maintainer of this package.

  ;;; Denote (simple note-taking and file-naming)

  ;; Read the manual: <https://protesilaos.com/emacs/denote>.  This does
  ;; not include all the useful features of Denote.  I have a separate
  ;; private setup for those, as I need to test everything is in order.
  (use-package denote
    :ensure t
    :hook
    ;; If you use Markdown or plain text files you want to fontify links
    ;; upon visiting the file (Org renders links as buttons right away).
    ((text-mode . denote-fontify-links-mode-maybe)

     ;; Highlight Denote file names in Dired buffers.  Below is the
     ;; generic approach, which is great if you rename files Denote-style
     ;; in lots of places as I do.
     ;;
     ;; If you only want the `denote-dired-mode' in select directories,
     ;; then modify the variable `denote-dired-directories' and use the
     ;; following instead:
     ;;
     ;;  (dired-mode . denote-dired-mode-in-directories)
     (dired-mode . denote-dired-mode))
    :bind
    ;; Denote DOES NOT define any key bindings.  This is for the user to
    ;; decide.  Here I only have a subset of what Denote offers.
    ( :map global-map
      ("C-c d n" . denote-create-note)
      ("C-c d N" . denote-silo-extras-select-silo-then-command)
      ("C-c d o" . denote-open-or-create)
      ("C-c d O" . denote-silo-extras-open-or-create)
      ("C-c d l" . denote-link-or-create)
      ("C-c d L" . denote-link-after-creating-with-command)
      ;; Note that `denote-rename-file' can work from any context, not
      ;; just Dired buffers.  That is why we bind it here to the
      ;; `global-map'.
      ;;
      ;; Also see `denote-rename-file-using-front-matter' further below.
      ("C-c d r" . denote-rename-file)
      ;; If you intend to use Denote with a variety of file types, it is
      ;; easier to bind the link-related commands to the `global-map', as
      ;; shown here.  Otherwise follow the same pattern for
      ;; `org-mode-map', `markdown-mode-map', and/or `text-mode-map'.
      ("C-c d j" . denote-journal-extras-new-entry)
      ("C-c d s" . denote-sort-dired)
      :map text-mode-map
      ("C-c d B" . denote-backlinks)
      ("C-c d b" . denote-find-backlink)
      ;; Also see `denote-rename-file' further above.
      ("C-c d R" . denote-rename-file-using-front-matter)
      ("C-c d k" . denote-rename-file-keywords)
      :map org-mode-map
      ("C-c d h" . denote-org-extras-link-to-heading)
      ("C-c d d l" . denote-org-extras-dblock-insert-links)
      ("C-c d d b" . denote-org-extras-dblock-insert-backlinks)
      ("C-c d d m" . denote-org-extras-dblock-insert-missing-links)
      ;; Key bindings specifically for Dired.
      :map dired-mode-map
      ("C-c C-d C-i" . denote-dired-link-marked-notes)
      ("C-c C-d C-r" . denote-dired-rename-marked-files)
      ("C-c C-d C-k" . denote-dired-rename-marked-files-with-keywords)
      ("C-c C-d C-A" . denote-dired-rename-marked-files-add-keywords)
      ("C-c C-d C-K" . denote-dired-rename-marked-files-remove-keywords)
      ("C-c C-d C-f" . denote-dired-rename-marked-files-using-front-matter))
    :config
    (require 'denote-silo-extras)
    (require 'denote-journal-extras)
    (require 'denote-org-extras)

    ;; Remember to check the doc strings of those variables.
    (setq denote-directory (expand-file-name "~/Tresors/Documents/diary/notes"))
    (setq denote-infer-keywords t)
    (setq denote-sort-keywords t)
    (setq denote-excluded-directories-regexp "data") ; external data related to headings is stored in these directories (web archives)
    (setq denote-date-format nil)         ; read its doc string
    (setq denote-date-prompt-use-org-read-date t)
    (setq denote-prompts '(title keywords subdirectory signature))

    (setq denote-rename-confirmations nil) ; CAREFUL with this if you are not familiar with Denote!
    (setq denote-save-buffers t)
    (setq denote-rename-buffer-format "[D] %s %t%b")
    ;; Automatically rename Denote buffers when opening them so that
    ;; instead of their long file name they have a literal "[D]"
    ;; followed by the file's title.  Read the doc string of
    ;; `denote-rename-buffer-format' for how to modify this.
    (denote-rename-buffer-mode 1)

    (setq denote-buffer-has-backlinks-string " (<--->)")
    (setq denote-backlinks-show-context t)
    (setq denote-org-store-link-to-heading t)

    ;; Journal settings
    (setq denote-journal-extras-keyword "")

    ;; I use Yasnippet to expand these into a better template.
    (add-to-list 'denote-templates '(reference-note . "reference"))
    (add-to-list 'denote-templates '(morning . "morningpage"))
    (add-to-list 'denote-templates '(emotion . "emotion"))
    (add-to-list 'denote-templates '(insight . "insight"))
    (add-to-list 'denote-templates '(weekly_intentions . "weekint"))
    (add-to-list 'denote-templates '(weekly_report . "weekrpt"))
    (add-to-list 'denote-templates '(sketch . "sketch"))

    ;; Front-matter for Org files
    (setq denote-org-front-matter
          ":PROPERTIES:
    :ID: %4$s
    :CREATED: %2$s
    :END:
    ,#+title:      %1$s
    ,#+filetags:   %3$s
    ,#+date:       %2$s
    ,#+identifier: %4$s
    \n"))

  (defun vedang/denote-publishing-extras-new-blog-entry (&optional date)
    "Create a new blog entry.

    With optional DATE as a prefix argument, prompt for a date.  If
    `denote-date-prompt-use-org-read-date' is non-nil, use the Org
    date selection module.

    When called from Lisp DATE is a string and has the same format as
    that covered in the documentation of the `denote' function.  It
    is internally processed by `denote-parse-date'."
    (interactive (list (when current-prefix-arg (denote-date-prompt))))
    (let ((internal-date (denote-parse-date date))
          (denote-directory (file-name-as-directory (expand-file-name "published" denote-directory))))
      (denote
       (denote-title-prompt)
       '("draft")
       nil nil date
       ;; See YASnippet
       "fullblog")))

  (defun vedang/denote-publishing-extras-new-microblog-entry (&optional date)
    "Create a new microblog entry.
    Set the title of the new entry according to the value of the user option
    `denote-journal-extras-title-format'.

    With optional DATE as a prefix argument, prompt for a date.  If
    `denote-date-prompt-use-org-read-date' is non-nil, use the Org
    date selection module.

    When called from Lisp DATE is a string and has the same format as
    that covered in the documentation of the `denote' function.  It
    is internally processed by `denote-parse-date'."
    (interactive (list (when current-prefix-arg (denote-date-prompt))))
    (let ((internal-date (denote-parse-date date))
          (denote-directory (file-name-as-directory (expand-file-name "published" denote-directory))))
      (denote
       (denote-journal-extras-daily--title-format internal-date)
       '("draft" "microblog")
       nil nil date
       ;; See YASnippet
       "microblog")))

  (defun vedang/denote-link-ol-get-id ()
    "Get the CUSTOM_ID of the current entry.

  If the entry already has a CUSTOM_ID, return it as-is, else create a new
  one.

  If we are creating a new ID, add a CREATED property with the current
  timestamp as well.

  This function is based on `denote-link-ol-get-id', with minor
  modifications."
    (interactive)
    (let* ((pos (point))
           (id (org-entry-get pos "CUSTOM_ID"))
           (created (org-entry-get pos "CREATED")))
      (if (and (stringp id) (string-match-p "\\S-" id))
          id
        (setq id (org-id-new "h"))
        (org-entry-put pos "CUSTOM_ID" id))
      (when (not created)
        (setq created (format-time-string (org-time-stamp-format t t) (current-time)))
        (org-entry-put pos "CREATED" created))
      id))

  (defun vedang/denote--split-luhman-sig (signature)
    "Split numbers and letters in Luhmann-style SIGNATURE string."
    (replace-regexp-in-string
     "\\([a-zA-Z]+?\\)\\([0-9]\\)" "\\1=\\2"
     (replace-regexp-in-string
      "\\([0-9]+?\\)\\([a-zA-Z]\\)" "\\1=\\2"
      signature)))

  (defun vedang/denote--pad-sig (signature)
    "Create a new signature with padded spaces for all components"
    (combine-and-quote-strings
     (mapcar
      (lambda (x)
        (string-pad x 5 32 t))
      (split-string (vedang/denote--split-luhman-sig signature) "=" t))
     "="))

  (defun vedang/denote-sort-for-signatures (sig1 sig2)
    "Return non-nil if SIG1 is smaller that SIG2.

    Perform the comparison with `string<'."
    (string< (vedang/denote--pad-sig sig1) (vedang/denote--pad-sig sig2)))

  (use-package denote
    :ensure t
    :bind
    ( :map global-map
      ;; Bindings to personal functions (defined above)
      ("C-c d p m" . vedang/denote-publishing-extras-new-microblog-entry)
      ("C-c d p b" . vedang/denote-publishing-extras-new-blog-entry))
    :config
    (setq denote-sort-signature-comparison-function #'vedang/denote-sort-for-signatures))

The unravel-langs.el integration between Consult and Denote (consult-denote)

This is another package of mine which extends my denote package (The unravel-langs.el settings for denote (notes and file-naming)).

This is glue code to integrate denote with Daniel Mendler's consult (The unravel-completion.el settings for consult). The idea is to enhance minibuffer interactions, such as by providing a preview of the file-to-linked/opened and by adding more sources to the consult-buffer command.

Prot is the developer of this package.

  (use-package consult-denote
    :ensure t
    :bind
    (("C-c d f" . consult-denote-find)
     ("C-c d g" . consult-denote-grep))
    :config
    (consult-denote-mode 1))

The unravel-langs.el settings for paredit (paren matching)

  (use-package paredit
    :ensure t
    :bind ( :map paredit-mode-map
            ("C-o" . paredit-open-round)
            ("M-D" . paredit-splice-sexp)
            ("C-A-d" . paredit-forward-down)
            ("C-A-u" . paredit-backward-up)
            ;; Unbind things that I don't need
            ("M-s" . nil) ; used for search related keybindings
            ("M-?" . nil)) ; `xref-find-references` uses it.
    :hook ((lisp-data-mode lisp-mode clojure-mode clojure-ts-mode cider-repl-mode inferior-emacs-lisp-mode) . paredit-mode))

The unravel-langs.el settings for apheleia (code formatting)

  (use-package apheleia
    :ensure t
    :demand t
    :config
    (apheleia-global-mode +1)
    (with-eval-after-load 'apheleia-formatters
      (push '(zprint . ("zprint")) apheleia-formatters)))

The unravel-langs.el section for Python

The built-in Python mode for Emacs goes a long way. I use the following stack when programming Python:

  • poetry for package and venv management
  • pylsp as the language server
  • ruff as the linting and formatting tool

Run the following commands in every virtualenv environment to setup the necessary developer tooling:

  • poetry add ruff python-lsp-server python-lsp-ruff --group dev

    • Ruff is an extremely fast Python linter, and code formatter, written in Rust. Ruff is also a langauge server, but it only provides functionality related to formating and linting. As it adds more over time (like go-to definition), I may make it my primary language server
    • Python LSP Server (provides the binary pylsp) is the defacto language server for Python.
    • python-lsp-ruff provides tight integration between pylsp and ruff, enabling the language server to use ruff for it's linting and formatting capabilities.
  • poetry add pytest --group test

Poetry takes care of setting up the venv properly, so if you replace the default commands with poetry versions, you are good to go. In practice, this means:

  • Use C-u C-c C-p command (run-python, with an argument) to start the Inferior Python Shell, instead of C-c C-p.

    • This will prompt you for a command, with the default value being python3 -i. Change it to poetry run python3 -i.
  • Modify the C-c C-v command (python-check) to poetry run ruff check <filename>

I run eglot on demand, that is, I do not start a language server automatically when I open a Python file. This ensures that emacs-pet can setup the venv for the project and use executables only from the virtualenv. (Note: We also setup the emacs-pet hook to be the first thing that runs when python mode is activated, so automatically starting eglot by uncommenting the eglot-ensure hook below should also work. But running manually is just how I prefer it.)

If you want to start the language server automatically you need to:

  • Install python-language-server and ruff globally, so that it's always available to Emacs.

    • brew install pipx
    • pipx install ruff python-language-server
  • Uncomment the :hook in the Python use-package form below
  ;;;; Configuration for Python Programming

  (use-package python
    :ensure nil
    :ensure-system-package (dasel sqlite3)
  ;;; Uncomment this if you want Eglot to start automatically. I don't
  ;;; recommend it because it does not give you time to activate the
  ;;; appropriate VirtualEnv and get the best of the situation.
    ;; :hook ((python-base-mode . eglot-ensure))
    :config
    (setq python-shell-dedicated 'project)
    ;; Apheleia is an Emacs package for formatting code as you save
    ;; it. Here we are asking Apheleia to use Ruff for formatting our
    ;; Python code.
    (with-eval-after-load 'apheleia
      (setf (alist-get 'python-mode apheleia-mode-alist)
            '(ruff-isort ruff))
      (setf (alist-get 'python-ts-mode apheleia-mode-alist)
            '(ruff-isort ruff)))
    (with-eval-after-load 'eglot
      (require 'vedang-pet)
      ;; The -10 here is a way to define the priority of the function in
      ;; the list of hook functions. We want `pet-mode' to run before
      ;; any other configured hook function.
      (add-hook 'python-base-mode-hook #'pet-mode -10)))

Tooling I have tried and rejected or failed to setup correctly

  • Auto-virtualenv: For virtualenv setup and detection
  • Pyvenv's WORKON_HOME: WORKON_HOME is useful if you keep all your venvs in the same directory. This is not generally what most people do.

    • Config: (setenv "WORKON_HOME" "~/.cache/venvs/")
  (use-package pyvenv
    :ensure t
    :after python
    :commands (pyvenv-create pyvenv-workon pyvenv-activate pyvenv-deactivate)
    :hook
    ((python-base-mode . pyvenv-mode)))

  (use-package pet ;; Python Environment Tracker
    :ensure t
    :after (python eglot)
    :ensure-system-package (dasel sqlite3)
    :config
    ;; We use `add-hook' instead of :hook to be able to specify the -10
    ;; (call as early as possible)
    (add-hook 'python-base-mode-hook #'pet-mode -10))

  (use-package auto-virtualenv
    :ensure t
    :after python
    :config
    (setq auto-virtualenv-verbose t)
    (auto-virtualenv-setup))

The unravel-langs.el section for Ziglang

I use the Emacs major mode zig-mode, along with the Ziglang langauge server zls (via eglot) for all my Zig programming requirements

To install zig and zls on MacOS:

brew install zig zls

  ;;;; Configuration for Zig Programming

  (use-package zig-mode
    :ensure t
  ;;; Uncomment this if you want Eglot to start automatically. I don't
  ;;; recommend it, but that's just me.
    ;; :hook ((zig-mode . eglot-ensure))
    )

Finally, we provide the unravel-langs.el module

(provide 'unravel-langs)

The unravel-personal.el module

I want this section to contain all the personal configuration that anyone using this repo should modify. But I don't know how to do this properly at the moment, so just noting things here until then.

  (use-package startup
    :ensure nil
    :config
    (setq user-mail-address "vedang@unravel.tech"))

My personal settings for org-capture

  ;;;; org-capture
  (use-package org-capture
    :ensure nil
    :config
  ;;; Default definitions for variables used in capture templates
    (when (not (boundp 'org-blogpost-file))
      (defvar org-blogpost-file org-default-notes-file
        "File in which blogposts and microblogposts are stored."))
    (when (not (boundp 'org-company-file))
      (defvar org-company-file org-default-notes-file
        "File in which company documentation is stored."))

  ;;; *CRITICAL NOTE* Read before modifying the push stack below:
    ;; Pushing to capture templates is a stack. What goes in first shows
    ;; up at the bottom of the capture templates list.

  ;;; Templates for thinking tools
    (push '("T" "Templates for Helping Me Think") org-capture-templates)
    ;; Capture a decision that you've taken, for review and reflection later.
    (push `("Td" "Decision Journal" entry
            (file+headline org-default-notes-file "Helping Me Think")
            (file ,(expand-file-name "capture-templates/thinking.decision.org"))
            :prepend nil
            :clock-in t
            :clock-resume t
            :empty-lines 1)
          org-capture-templates)

    ;; Create a Current Reality Tree for a problem
    (push `("Tc" "Current Reality Tree" entry
            (file+headline org-default-notes-file "Helping Me Think")
            (file ,(expand-file-name "capture-templates/thinking.crt.org"))
            :prepend nil
            :clock-in t
            :clock-resume t
            :empty-lines 1)
          org-capture-templates)

    ;; Create an Evaporating Cloud for a problem
    (push `("Te" "Evaporating Cloud" entry
            (file+headline org-default-notes-file "Helping Me Think")
            (file ,(expand-file-name "capture-templates/thinking.ec.org"))
            :prepend nil
            :clock-in t
            :clock-resume t
            :empty-lines 1)
          org-capture-templates)

    ;; Create a Future Reality Tree for a problem
    (push `("Tf" "Future Reality Tree" entry
            (file+headline org-default-notes-file "Helping Me Think")
            (file ,(expand-file-name "capture-templates/thinking.frt.org"))
            :prepend nil
            :clock-in t
            :clock-resume t
            :empty-lines 1)
          org-capture-templates)

    ;; Create a Prerequisite Tree for a problem
    (push `("Tp" "Prerequisite Tree" entry
            (file+headline org-default-notes-file "Helping Me Think")
            (file ,(expand-file-name "capture-templates/thinking.prt.org"))
            :prepend nil
            :clock-in t
            :clock-resume t
            :empty-lines 1)
          org-capture-templates)

    ;; Create a Transition Tree for a problem
    (push `("Tt" "Transition Tree" entry
            (file+headline org-default-notes-file "Helping Me Think")
            (file ,(expand-file-name "capture-templates/thinking.trt.org"))
            :prepend nil
            :clock-in t
            :clock-resume t
            :empty-lines 1)
          org-capture-templates)

    ;; Capture a new Business idea for sketching out / thinking through
    (push `("Tb" "Business Canvas" entry
            (file+headline org-default-notes-file "Helping Me Think")
            (file ,(expand-file-name "capture-templates/business.canvas.org"))
            :prepend nil
            :clock-in t
            :clock-resume t
            :empty-lines 1)
          org-capture-templates)

    ;; Capture a customer persona, note that this is always captured in
    ;; the current clocking task, and is something I should do under the
    ;; business canvas.
    (push `("TP" "Customer Persona (under Business Canvas)" entry
            (clock)
            (file ,(expand-file-name "capture-templates/business.customer.persona.org"))
            :prepend nil
            :clock-in t
            :clock-resume t
            :empty-lines 1)
          org-capture-templates)

    ;; Capture a customer journey through your product, note that this is
    ;; always captured in the current clocking task
    (push `("Tj" "Customer Journey (under Business Canvas)" entry
            (clock)
            (file ,(expand-file-name "capture-templates/business.customer.journey.org"))
            :prepend nil
            :clock-in t
            :clock-resume t
            :empty-lines 1)
          org-capture-templates)

  ;;; Templates for capturing data about myself on a day-to-day basis
    (push '("d" "Templates for Capturing Data (personal)") org-capture-templates)

    ;; Capture weight / food. This seems hard to get into a laptop habit.
    ;; This is the kind of quantitative life that a mobile solution would
    ;; have helped with.

    (push `("dw" "Weight Tracking" entry
            (file+olp+datetree org-default-notes-file)
            (file ,(expand-file-name "capture-templates/bodylog.weight.org"))
            :clock-in t
            :clock-resume t
            :immediate-finish t
            :empty-lines 1)
          org-capture-templates)

    (push `("df" "Food Tracking" entry
            (file+olp+datetree org-default-notes-file)
            (file ,(expand-file-name "capture-templates/bodylog.food.org"))
            :clock-in t
            :clock-resume t
            :immediate-finish t
            :empty-lines 1)
          org-capture-templates)

    (push `("dd" "Downtime Tracking" entry
            (file+olp+datetree org-default-notes-file)
            (file ,(expand-file-name "capture-templates/bodylog.dt.org"))
            :clock-in t
            :clock-resume t
            :immediate-finish t
            :empty-lines 1)
          org-capture-templates)

  ;;; Templates for capturing build in public ideas
    (push '("b" "Templates for Capturing Build in Public") org-capture-templates)

    ;; Capture Micro-blogging
    (push `("bm" "New Microblogging entry" entry
            (file+olp+datetree org-blogpost-file "Microblogging")
            (file ,(expand-file-name "capture-templates/microblog.org"))
            :prepend nil
            :clock-in t
            :clock-resume t
            :empty-lines 1)
          org-capture-templates)

    ;; New blogpost idea
    (push `("bb" "New Blogpost entry" entry
            (file+headline org-blogpost-file "Meta: Blogposts to write")
            (file ,(expand-file-name "capture-templates/todo.org"))
            :prepend nil
            :clock-in t
            :clock-resume t
            :empty-lines 1)
          org-capture-templates)

  ;;; Templates for when I want to capture specific feedback about something
    (push '("f" "Templates for Feedback, Reflection, Journaling") org-capture-templates)

    ;; Capture feedback for people I am working with
    (push `("fp" "Feedback for People I'm working with" item
            (file+headline org-company-file "Feedback")
            (file ,(expand-file-name "capture-templates/feedback.others.org"))
            :clock-in t
            :clock-resume t
            :empty-lines 1)
          org-capture-templates)

    ;; The monthly newsletter to send to investors, friends and mentors
    (push `("fn" "Company Newsletters" entry
            (file+headline org-company-file "Company Newsletters")
            (file ,(expand-file-name "capture-templates/business.updates.org"))
            :prepend nil
            :clock-in t
            :clock-resume t
            :empty-lines 1)
          org-capture-templates)

    ;; Capture suggestions / ideas from other people, which can be
    ;; expanded into actual projects later.
    (push `("fs" "Ideas and Suggestions" entry
            (file+headline org-company-file "Ideas and Suggestions")
            (file ,(expand-file-name "capture-templates/suggestion.org"))
            :prepend t
            :clock-in t
            :clock-resume t
            :empty-lines 1)
          org-capture-templates)

  ;;; Templates for planning on a day-to-day basis
    (push '("p" "Templates for Planning") org-capture-templates)

    ;; Deliberately plan out and make a routine out of start of day and
    ;; end of day activities.

    (push `("ps" "The Start of Day Planning Routine" entry
            (file+olp+datetree org-default-notes-file)
            (file ,(expand-file-name "capture-templates/workday.start.org"))
            :prepend nil
            :clock-in t
            :clock-resume t
            :empty-lines 1)
          org-capture-templates)

    (push `("pe" "The End of Day Reflection Routine" entry
            (file+olp+datetree org-default-notes-file)
            (file ,(expand-file-name "capture-templates/workday.end.org"))
            :prepend nil
            :clock-in t
            :clock-resume t
            :empty-lines 1)
          org-capture-templates)

    (push `("pn" "The Next Day Intentions Routine" entry
            (file+olp+datetree org-default-notes-file)
            (file ,(expand-file-name "capture-templates/workday.next.org"))
            :prepend nil
            :clock-in t
            :clock-resume t
            :empty-lines 1)
          org-capture-templates)

  ;;; Templates for capturing meetings, events, something happening at this time
    (push '("m" "Templates for Capturing Meetings or Events") org-capture-templates)

    ;; Capture an upcoming meeting or one that has already happened
    (push `("mp" "Meeting some other day" entry
            (file+olp+datetree org-default-notes-file)
            (file ,(expand-file-name "capture-templates/meeting.org"))
            :prepend t
            :clock-in t
            :clock-resume t
            :time-prompt t)
          org-capture-templates)

    ;; Capture notes for an ongoing meeting or a meeting that's already
    ;; happened.
    (push `("mn" "Meeting today" entry
            (file+olp+datetree org-default-notes-file)
            (file ,(expand-file-name "capture-templates/meeting.org"))
            :prepend t
            :clock-in t
            :clock-resume t)
          org-capture-templates)

  ;;; Templates for Capturing Tasks
    (push '("t" "Templates for Capturing Tasks") org-capture-templates)

    ;; Set up a new habit for tracking. This should be refiled to the
    ;; correct location later.
    (push `("th" "Habit" entry
            (file+headline org-default-notes-file "My Habit Tracker")
            (file ,(expand-file-name "capture-templates/habit.org")))
          org-capture-templates)

    ;; One-click Capture for replying to emails from notmuch. Creates a
    ;; task to remind you that you need to reply to this email.
    (push `("tr" "Respond to email" entry
            (file+olp+datetree org-default-notes-file)
            (file ,(expand-file-name "capture-templates/reply.org"))
            :clock-in t
            :clock-resume t
            :immediate-finish t)
          org-capture-templates)

    ;; One-click capture of links from the clipboard. Used in conjunction
    ;; with `org-protocol', or as a stand-alone to capture links.
    (push `("tw" "Website Link Immediate Capture" entry
            (file+olp org-default-notes-file "Links Captured from the Browser")
            (file ,(expand-file-name "capture-templates/website.org"))
            :immediate-finish t)
          org-capture-templates)

    ;; A more nuanced capture for browser links, which I use for cleaning
    ;; out my browser 2/3 times a week.
    (push `("tl" "Website Link Pinboard Capture" entry
            (file+olp org-default-notes-file "Links Captured from the Browser")
            (file ,(expand-file-name "capture-templates/pinboard.capture.org"))
            :clock-in t
            :clock-resume t
            :immediate-finish t)
          org-capture-templates)

    ;; Capture a task where someone expects me to communicate when it's done
    (push `("tj" "Jira or External-facing Task" entry
            (file+olp+datetree org-default-notes-file)
            (file ,(expand-file-name "capture-templates/jira.org"))
            :clock-in t
            :clock-resume t)
          org-capture-templates)

    ;; One-click Capture for Tasks. Captures the task immediately and gets
    ;; out of your way.
    (push `("ti" "Simple Task Immediate Finish" entry
            (file+olp+datetree org-default-notes-file)
            (file ,(expand-file-name "capture-templates/todo.org"))
            :clock-in t
            :clock-resume t
            :immediate-finish t)
          org-capture-templates))

Custom libraries

The prot-common.el library

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

  ;; Copyright (C) 2020-2024  Protesilaos Stavrou

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

  ;; This file is NOT part of GNU Emacs.

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

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

  ;;; Code:

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

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

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

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

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

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

  ;;;###autoload
  (defun prot-common-reverse-percentage (number percent change-p)
    "Determine the original value of NUMBER given PERCENT.

  CHANGE-P should specify the increase or decrease.  For simplicity,
  nil means decrease while non-nil stands for an increase.

  NUMBER must satisfy `numberp', while PERCENT must be `natnump'."
    (unless (numberp number)
      (user-error "NUMBER must satisfy numberp"))
    (unless (natnump percent)
      (user-error "PERCENT must satisfy natnump"))
    (let* ((pc (/ (float percent) 100))
           (pc-change (if change-p (+ 1 pc) pc))
           (n (if change-p pc-change (float (- 1 pc-change)))))
      ;; FIXME 2021-12-21: If float, round to 4 decimal points.
      (/ number n)))

  ;;;###autoload
  (defun prot-common-percentage-change (n-original n-final)
    "Find percentage change between N-ORIGINAL and N-FINAL numbers.

  When the percentage is not an integer, it is rounded to 4
  floating points: 16.666666666666664 => 16.667."
    (unless (numberp n-original)
      (user-error "N-ORIGINAL must satisfy numberp"))
    (unless (numberp n-final)
      (user-error "N-FINAL must satisfy numberp"))
    (let* ((difference (float (abs (- n-original n-final))))
           (n (* (/ difference n-original) 100))
           (round (floor n)))
      ;; FIXME 2021-12-21: Any way to avoid the `string-to-number'?
      (if (> n round) (string-to-number (format "%0.4f" n)) round)))

  ;; REVIEW 2023-04-07 07:43 +0300: I just wrote the conversions from
  ;; seconds.  Hopefully they are correct, but I need to double check.
  (defun prot-common-seconds-to-minutes (seconds)
    "Convert a number representing SECONDS to MM:SS notation."
    (let ((minutes (/ seconds 60))
          (seconds (% seconds 60)))
      (format "%.2d:%.2d" minutes seconds)))

  (defun prot-common-seconds-to-hours (seconds)
    "Convert a number representing SECONDS to HH:MM:SS notation."
    (let* ((hours (/ seconds 3600))
           (minutes (/ (% seconds 3600) 60))
           (seconds (% seconds 60)))
      (format "%.2d:%.2d:%.2d" hours minutes seconds)))

  ;;;###autoload
  (defun prot-common-seconds-to-minutes-or-hours (seconds)
    "Convert SECONDS to either minutes or hours, depending on the value."
    (if (> seconds 3599)
        (prot-common-seconds-to-hours seconds)
      (prot-common-seconds-to-minutes seconds)))

  ;;;###autoload
  (defun prot-common-rotate-list-of-symbol (symbol)
    "Rotate list value of SYMBOL by moving its car to the end.
  Return the first element before performing the rotation.

  This means that if `sample-list' has an initial value of `(one
  two three)', this function will first return `one' and update the
  value of `sample-list' to `(two three one)'.  Subsequent calls
  will continue rotating accordingly."
    (unless (symbolp symbol)
      (user-error "%s is not a symbol" symbol))
    (when-let* ((value (symbol-value symbol))
                (list (and (listp value) value))
                (first (car list)))
      (set symbol (append (cdr list) (list first)))
      first))

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

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

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

  ;; NOTE 2023-08-12: I tried the `clear-message-function', but it did
  ;; not work.  What I need is very simple and this gets the job done.
  ;;;###autoload
  (defun prot-common-clear-minibuffer-message (&rest _)
    "Print an empty message to clear the echo area.
  Use this as advice :after a noisy function."
    (message ""))

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

  ;;;###autoload
  (defun prot-common-window-bounds ()
    "Return start and end points in the window as a cons cell."
    (cons (window-start) (window-end)))

  ;;;###autoload
  (defun prot-common-page-p ()
    "Return non-nil if there is a `page-delimiter' in the buffer."
    (or (save-excursion (re-search-forward page-delimiter nil t))
        (save-excursion (re-search-backward page-delimiter nil t))))

  ;;;###autoload
  (defun prot-common-window-small-p ()
    "Return non-nil if window is small.
  Check if the `window-width' or `window-height' is less than
  `split-width-threshold' and `split-height-threshold',
  respectively."
    (or (and (numberp split-width-threshold)
             (< (window-total-width) split-width-threshold))
        (and (numberp split-height-threshold)
             (> (window-total-height) split-height-threshold))))

  (defun prot-common-window-narrow-p ()
    "Return non-nil if window is narrow.
  Check if the `window-width' is less than `split-width-threshold'."
    (and (numberp split-width-threshold)
         (< (window-total-width) split-width-threshold)))

  ;;;###autoload
  (defun prot-common-three-or-more-windows-p (&optional frame)
    "Return non-nil if three or more windows occupy FRAME.
  If FRAME is non-nil, inspect the current frame."
    (>= (length (window-list frame :no-minibuffer)) 3))

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

  ;;;###autoload
  (defun prot-common-completion-category ()
    "Return completion category."
    (when-let* ((window (active-minibuffer-window)))
      (with-current-buffer (window-buffer window)
        (completion-metadata-get
         (completion-metadata (buffer-substring-no-properties
                               (minibuffer-prompt-end)
                               (max (minibuffer-prompt-end) (point)))
                              minibuffer-completion-table
                              minibuffer-completion-predicate)
         'category))))

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

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

  ;;;###autoload
  (defun prot-common-completion-table-no-sort (category candidates)
    "Pass appropriate metadata CATEGORY to completion CANDIDATES.
  Like `prot-common-completion-table' but also disable sorting."
    (lambda (string pred action)
      (if (eq action 'metadata)
          `(metadata (category . ,category)
                     (display-sort-function . ,#'identity))
        (complete-with-action action candidates string pred))))

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

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

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

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

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

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

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

  (defvar prot-common-url-regexp
    (concat
     "~?\\<\\([-a-zA-Z0-9+&@#/%?=~_|!:,.;]*\\)"
     "[.@]"
     "\\([-a-zA-Z0-9+&@#/%?=~_|!:,.;]+\\)\\>/?")
    "Regular expression to match (most?) URLs or email addresses.")

  (autoload 'auth-source-search "auth-source")

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

  ;;;###autoload
  (defun prot-common-parse-file-as-list (file)
    "Return the contents of FILE as a list of strings.
  Strings are split at newline characters and are then trimmed for
  negative space.

  Use this function to provide a list of candidates for
  completion (per `completing-read')."
    (split-string
     (with-temp-buffer
       (insert-file-contents file)
       (buffer-substring-no-properties (point-min) (point-max)))
     "\n" :omit-nulls "[\s\f\t\n\r\v]+"))

  (defun prot-common-ignore (&rest _)
    "Use this as override advice to make a function do nothing."
    nil)

  ;; NOTE 2023-06-02: The `prot-common-wcag-formula' and
  ;; `prot-common-contrast' are taken verbatim from my `modus-themes'
  ;; and renamed to have the prefix `prot-common-' instead of
  ;; `modus-themes-'.  This is all my code, of course, but I do it this
  ;; way to ensure that this file is self-contained in case someone
  ;; copies it.

  ;; This is the WCAG formula: <https://www.w3.org/TR/WCAG20-TECHS/G18.html>.
  (defun prot-common-wcag-formula (hex)
    "Get WCAG value of color value HEX.
  The value is defined in hexadecimal RGB notation, such #123456."
    (cl-loop for k in '(0.2126 0.7152 0.0722)
             for x in (color-name-to-rgb hex)
             sum (* k (if (<= x 0.03928)
                          (/ x 12.92)
                        (expt (/ (+ x 0.055) 1.055) 2.4)))))

  ;;;###autoload
  (defun prot-common-contrast (c1 c2)
    "Measure WCAG contrast ratio between C1 and C2.
  C1 and C2 are color values written in hexadecimal RGB."
    (let ((ct (/ (+ (prot-common-wcag-formula c1) 0.05)
                 (+ (prot-common-wcag-formula c2) 0.05))))
      (max ct (/ ct))))

  ;;;; EXPERIMENTAL macros (not meant to be used anywhere)

  ;; TODO 2023-09-30: Try the same with `cl-defmacro' and &key
  (defmacro prot-common-if (condition &rest consequences)
    "Separate the CONSEQUENCES of CONDITION semantically.
  Like `if', `when', `unless' but done by using `:then' and `:else'
  keywords.  The forms under each keyword of `:then' and `:else'
  belong to the given subset of CONSEQUENCES.

  - The absence of `:else' means: (if CONDITION (progn CONSEQUENCES)).
  - The absence of `:then' means: (if CONDITION nil CONSEQUENCES).
  - Otherwise: (if CONDITION (progn then-CONSEQUENCES) else-CONSEQUENCES)."
    (declare (indent 1))
    (let (then-consequences else-consequences last-kw)
      (dolist (elt consequences)
        (let ((is-keyword (keywordp elt)))
          (cond
           ((and (not is-keyword) (eq last-kw :then))
            (push elt then-consequences))
           ((and (not is-keyword) (eq last-kw :else))
            (push elt else-consequences))
           ((and is-keyword (eq elt :then))
            (setq last-kw :then))
           ((and is-keyword (eq elt :else))
            (setq last-kw :else)))))
      `(if ,condition
           ,(if then-consequences
                `(progn ,@(nreverse then-consequences))
              nil)
         ,@(nreverse else-consequences))))

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

The prot-embark.el library

;;; prot-embark.el --- Custom Embark keymaps -*- lexical-binding: t -*-

;; Copyright (C) 2023-2024  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'embark)

(defvar-keymap prot-embark-general-map
  :parent embark-general-map
  "i" #'embark-insert
  "w" #'embark-copy-as-kill
  "E" #'embark-export
  "S" #'embark-collect
  "A" #'embark-act-all
  "DEL" #'delete-region)

(defvar-keymap prot-embark-url-map
  :parent embark-general-map
  "b" #'browse-url
  "d" #'embark-download-url
  "e" #'eww)

(defvar-keymap prot-embark-buffer-map
  :parent embark-general-map
  "k" #'prot-simple-kill-buffer
  "o" #'switch-to-buffer-other-window
  "e" #'ediff-buffers)

(add-to-list 'embark-post-action-hooks (list 'prot-simple-kill-buffer 'embark--restart))

(defvar-keymap prot-embark-file-map
  :parent embark-general-map
  "f" #'find-file
  "j" #'embark-dired-jump
  "c" #'copy-file
  "e" #'ediff-files)

(defvar-keymap prot-embark-identifier-map
  :parent embark-general-map
  "h" #'display-local-help
  "." #'xref-find-definitions
  "o" #'occur)

(defvar-keymap prot-embark-command-map
  :parent embark-general-map
  "h" #'describe-command
  "." #'embark-find-definition)

(defvar-keymap prot-embark-expression-map
  :parent embark-general-map
  "e" #'pp-eval-expression
  "m" #'pp-macroexpand-expression)

(defvar-keymap prot-embark-function-map
  :parent embark-general-map
  "h" #'describe-function
  "." #'embark-find-definition)

(defvar-keymap prot-embark-package-map
  :parent embark-general-map
  "h" #'describe-package
  "i" #'package-install
  "d" #'package-delete
  "r" #'package-reinstall
  "u" #'embark-browse-package-url
  "w" #'embark-save-package-url)

(defvar-keymap prot-embark-symbol-map
  :parent embark-general-map
  "h" #'describe-symbol
  "." #'embark-find-definition)

(defvar-keymap prot-embark-variable-map
  :parent embark-general-map
  "h" #'describe-variable
  "." #'embark-find-definition)

(defvar-keymap prot-embark-region-map
  :parent embark-general-map
  "a" #'align-regexp
  "D" #'delete-duplicate-lines
  "f" #'flush-lines
  "i" #'epa-import-keys-region
  "d" #'epa-decrypt-armor-in-region
  "r" #'repunctuate-sentences
  "s" #'sort-lines
  "u" #'untabify)

;; The minimal indicator shows cycling options, but I have no use
;; for those.  I want it to be silent.
(defun prot-embark-no-minimal-indicator ())
(advice-add #'embark-minimal-indicator :override #'prot-embark-no-minimal-indicator)

(defun prot-embark-act-no-quit ()
  "Call `embark-act' but do not quit after the action."
  (interactive)
  (let ((embark-quit-after-action nil))
    (call-interactively #'embark-act)))

(defun prot-embark-act-quit ()
  "Call `embark-act' and quit after the action."
  (interactive)
  (let ((embark-quit-after-action t))
    (call-interactively #'embark-act))
  (when (and (> (minibuffer-depth) 0)
             (derived-mode-p 'completion-list-mode))
    (abort-recursive-edit)))

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

The prot-window.el library

;;; prot-window.el --- Display-buffer and window-related extensions for my dotemacs -*- lexical-binding: t -*-

;; Copyright (C) 2023-2024  Protesilaos Stavrou

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

;; This file is NOT part of GNU Emacs.

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

;;; Commentary:
;;
;; This covers my window and display-buffer extensions, for use in my
;; Emacs setup: https://protesilaos.com/emacs/dotemacs.
;;
;; Remember that every piece of Elisp that I write is for my own
;; educational and recreational purposes.  I am not a programmer and I
;; do not recommend that you copy any of this if you are not certain of
;; what it does.

;;; Code:

(require 'prot-common)

(defvar prot-window-window-sizes
  '( :max-height (lambda () (floor (frame-height) 3))
     :min-height 10
     :max-width (lambda () (floor (frame-width) 4))
     :min-width 20)
  "Property list of maximum and minimum window sizes.
The property keys are `:max-height', `:min-height', `:max-width',
and `:min-width'.  They all accept a value of either a
number (integer or floating point) or a function.")

(defun prot-window--get-window-size (key)
  "Extract the value of KEY from `prot-window-window-sizes'."
  (when-let* ((value (plist-get prot-window-window-sizes key)))
    (cond
     ((functionp value)
      (funcall value))
     ((numberp value)
      value)
     (t
      (error "The value of `%s' is neither a number nor a function" key)))))

(defun prot-window-select-fit-size (window)
  "Select WINDOW and resize it.
The resize pertains to the maximum and minimum values for height
and width, per `prot-window-window-sizes'.

Use this as the `body-function' in a `display-buffer-alist' entry."
  (select-window window)
  (fit-window-to-buffer
   window
   (prot-window--get-window-size :max-height)
   (prot-window--get-window-size :min-height)
   (prot-window--get-window-size :max-width)
   (prot-window--get-window-size :min-width))
  ;; If we did not use `display-buffer-below-selected', then we must
  ;; be in a lateral window, which has more space.  Then we do not
  ;; want to dedicate the window to this buffer, because we will be
  ;; running out of space.
  (when (or (window-in-direction 'above) (window-in-direction 'below))
    (set-window-dedicated-p window t)))

(defun prot-window--get-display-buffer-below-or-pop ()
  "Return list of functions for `prot-window-display-buffer-below-or-pop'."
  (list
   #'display-buffer-reuse-mode-window
   (if (or (prot-common-window-small-p)
           (prot-common-three-or-more-windows-p))
       #'display-buffer-below-selected
     #'display-buffer-pop-up-window)))

(defun prot-window-display-buffer-below-or-pop (&rest args)
  "Display buffer below current window or pop a new window.
The criterion for choosing to display the buffer below the
current one is a non-nil return value for
`prot-common-window-small-p'.

Apply ARGS expected by the underlying `display-buffer' functions.

This as the action function in a `display-buffer-alist' entry."
  (let ((functions (prot-window--get-display-buffer-below-or-pop)))
    (catch 'success
      (dolist (fn functions)
        (when (apply fn args)
          (throw 'success fn))))))

(defun prot-window-shell-or-term-p (buffer &rest _)
  "Check if BUFFER is a shell or terminal.
This is a predicate function for `buffer-match-p', intended for
use in `display-buffer-alist'."
  (when (string-match-p "\\*.*\\(e?shell\\|v?term\\).*" (buffer-name (get-buffer buffer)))
    (with-current-buffer buffer
      ;; REVIEW 2022-07-14: Is this robust?
      (and (not (derived-mode-p 'message-mode 'text-mode))
           (derived-mode-p 'eshell-mode 'shell-mode 'comint-mode 'fundamental-mode)))))

(defun prot-window-remove-dedicated (&rest _)
  "Remove dedicated window parameter.
Use this as :after advice to `delete-other-windows' and
`delete-window'."
  (when (one-window-p :no-mini)
    (set-window-dedicated-p nil nil)))

(mapc
 (lambda (fn)
   (advice-add fn :after #'prot-window-remove-dedicated))
 '(delete-other-windows delete-window))

(defmacro prot-window-define-full-frame (name &rest args)
  "Define command to call ARGS in new frame with `display-buffer-full-frame' bound.
Name the function prot-window- followed by NAME.  If ARGS is nil,
call NAME as a function."
  (declare (indent 1))
  `(defun ,(intern (format "prot-window-%s" name)) ()
     ,(format "Call `prot-window-%s' in accordance with `prot-window-define-full-frame'." name)
     (interactive)
     (let ((display-buffer-alist '((".*" (display-buffer-full-frame)))))
       (with-selected-frame (make-frame)
         ,(if args
              `(progn ,@args)
            `(funcall ',name))
         (modify-frame-parameters nil '((buffer-list . nil)))))))

(defun prot-window--get-shell-buffers ()
  "Return list of `shell' buffers."
  (seq-filter
   (lambda (buffer)
     (with-current-buffer buffer
       (derived-mode-p 'shell-mode)))
   (buffer-list)))

(defun prot-window--get-new-shell-buffer ()
  "Return buffer name for `shell' buffers."
  (if-let* ((buffers (prot-window--get-shell-buffers))
            (buffers-length (length buffers))
            ((>= buffers-length 1)))
      (format "*shell*<%s>" (1+ buffers-length))
    "*shell*"))

;;;###autoload (autoload 'prot-window-shell "prot-window")
(prot-window-define-full-frame shell
  (let ((name (prot-window--get-new-shell-buffer)))
    (shell name)
    (set-frame-name name)
    (when-let* ((buffer (get-buffer name)))
      (with-current-buffer buffer
        (add-hook
         'delete-frame-functions
         (lambda (_)
           ;; FIXME 2023-09-09: Works for multiple frames (per
           ;; `make-frame-command'), but not if the buffer is in two
           ;; windows in the same frame.
           (unless (> (safe-length (get-buffer-window-list buffer nil t)) 1)
             (let ((kill-buffer-query-functions nil))
               (kill-buffer buffer))))
         nil
         :local)))))

;;;###autoload (autoload 'prot-window-coach "prot-window")
(prot-window-define-full-frame coach
  (let ((buffer (get-buffer-create "*scratch for coach*")))
    (with-current-buffer buffer
      (funcall initial-major-mode))
    (display-buffer buffer)
    (set-frame-name "Coach")))

;; REVIEW 2023-06-25: Does this merit a user option?  I don't think I
;; will ever set it to the left.  It feels awkward there.
(defun prot-window-scroll-bar-placement ()
  "Control the placement of scroll bars."
  (when scroll-bar-mode
    (setq default-frame-scroll-bars 'right)
    (set-scroll-bar-mode 'right)))

(add-hook 'scroll-bar-mode-hook #'prot-window-scroll-bar-placement)

(defun prot-window-no-minibuffer-scroll-bar (frame)
  "Remove the minibuffer scroll bars from FRAME."
  (set-window-scroll-bars (minibuffer-window frame) nil nil nil nil :persistent))

(add-hook 'after-make-frame-functions 'prot-window-no-minibuffer-scroll-bar)

;;;; Run commands in a popup frame (via emacsclient)

(defun prot-window-delete-popup-frame (&rest _)
  "Kill selected selected frame if it has parameter `prot-window-popup-frame'.
Use this function via a hook."
  (when (frame-parameter nil 'prot-window-popup-frame)
    (delete-frame)))

(defmacro prot-window-define-with-popup-frame (command)
  "Define function which calls COMMAND in a new frame.
Make the new frame have the `prot-window-popup-frame' parameter."
  `(defun ,(intern (format "prot-window-popup-%s" command)) ()
     ,(format "Run `%s' in a popup frame with `prot-window-popup-frame' parameter.
Also see `prot-window-delete-popup-frame'." command)
     (interactive)
     (let ((frame (make-frame '((prot-window-popup-frame . t)))))
       (select-frame frame)
       (switch-to-buffer " prot-window-hidden-buffer-for-popup-frame")
       (condition-case nil
           (call-interactively ',command)
         ((quit error user-error)
          (delete-frame frame))))))

(declare-function org-capture "org-capture" (&optional goto keys))
(defvar org-capture-after-finalize-hook)

;;;###autoload (autoload 'prot-window-popup-org-capture "prot-window")
(prot-window-define-with-popup-frame org-capture)

(declare-function tmr "tmr" (time &optional description acknowledgep))
(defvar tmr-timer-created-functions)

;;;###autoload (autoload 'prot-window-popup-tmr "prot-window")
(prot-window-define-with-popup-frame tmr)

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

The vedang-pet.el library

  ;;; pet.el --- Executable and virtualenv tracker for python-mode -*- lexical-binding: t -*-

  ;; Author: Jimmy Yuen Ho Wong <wyuenho@gmail.com>
  ;; Maintainer: Jimmy Yuen Ho Wong <wyuenho@gmail.com>
  ;; Version: 3.1.0
  ;; Package-Requires: ((emacs "26.1") (f "0.6.0") (map "3.3.1") (seq "2.24"))
  ;; Homepage: https://github.com/wyuenho/emacs-pet/
  ;; Keywords: tools


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

  ;; __P__ython __E__xecutable __T__racker.  Tracks downs the correct Python
  ;; executables from the various virtualenv management tools and assign them to
  ;; buffer local variables.  The package to end all Emacs virtualenv packages.

  ;;; Code:


  (require 'cl-lib)
  (require 'f)
  (require 'filenotify)
  (require 'let-alist)
  (require 'map)
  (require 'pcase)
  (require 'project)
  (require 'python)
  (require 'seq)
  (require 'subr-x)
  (require 'tramp)

  (when (< emacs-major-version 27)
    (require 'json))

  (defgroup pet nil
    "Customization group for `pet'."
    :group 'python
    :prefix "pet-")

  (defcustom pet-debug nil
    "Whether to turn on debug messages."
    :group 'pet
    :type 'boolean)

  (defcustom pet-toml-to-json-program "dasel"
    "Name of the program to convert TOML to JSON.

  The program must accept input from STDIN and output a JSON to
  STDOUT.

  You can customize the arguments that will be passed to the
  program by adjusting `pet-toml-to-json-program-arguments'"
    :group 'pet
    :type '(choice (const "dasel")
                   (const "tomljson")
                   (string :tag "Other")))

  (defcustom pet-toml-to-json-program-arguments '("-f" "-" "-r" "toml" "-w" "json")
    "Arguments for `pet-toml-to-json-program'."
    :group 'pet
    :type '(repeat string))

  (defcustom pet-yaml-to-json-program "dasel"
    "Name of the program to convert YAML to JSON.

  The program must accept input from STDIN and output a JSON to
  STDOUT.

  You can customize the arguments that will be passed to the
  program by adjusting `pet-yaml-to-json-program-arguments'"
    :group 'pet
    :type '(choice (const "dasel")
                   (const "yq")
                   (string :tag "Other")))

  (defcustom pet-yaml-to-json-program-arguments '("-f" "-" "-r" "yaml" "-w" "json")
    "Arguments for `pet-yaml-to-json-program'."
    :group 'pet
    :type '(repeat string))

  (defcustom pet-find-file-functions '(pet-find-file-from-project-root
                                       pet-locate-dominating-file
                                       pet-find-file-from-project-root-recursively)
    "Order in which `pet-find-file-from-project' should search for a config file.

  Each function should take a file name as its sole argument and
  return an absolute path to the file found in the current project
  and nil otherwise."
    :group 'pet
    :type '(repeat (choice (const pet-find-file-from-project-root)
                           (const pet-locate-dominating-file)
                           (const pet-find-file-from-project-root-recursively)
                           function)))

  (defcustom pet-venv-dir-names '(".venv" "venv" "env")
    "Directory names to search for when looking for a virtualenv at the project root."
    :group 'pet
    :type '(repeat string))

  

  (defun pet--executable-find (command &optional remote)
    "Like Emacs 27's `executable-find', ignore REMOTE on Emacs 26.

  See `executable-find' for the meaning of COMMAND and REMOTE."
    (if (>= emacs-major-version 27)
        (executable-find command remote)
      (executable-find command)))

  (defun pet-system-bin-dir ()
    "Determine the correct script directory based on `system-type'."
    (if (eq (if (file-remote-p default-directory)
                (tramp-get-connection-property
                 (tramp-dissect-file-name default-directory)
                 "uname"
                 'windows-nt)
              system-type)
            'windows-nt)
        "Scripts" "bin"))

  (defun pet-report-error (err)
    "Report ERR to the minibuffer.

  Only reports to the minibuffer if `pet-debug' is non-nil."
    (when pet-debug
      (minibuffer-message (error-message-string err)))
    nil)

  (defun pet-project-root ()
    "Return the path of root of the project.

  If `projectile' is available, the function
  `projectile-project-root' is used to find the project root.
  Otherwise, `project-root' is used."
    (or (and (functionp 'projectile-project-root)
             (projectile-project-root))
        (when-let ((project (project-current)))
          (or (and (functionp 'project-root)
                   (expand-file-name (project-root project)))
              (and (functionp 'project-roots)
                   (when-let ((root (car (project-roots project))))
                     (expand-file-name root)))))))

  (defun pet-find-file-from-project-root (file)
    "Find FILE from the current project's root.

  FILE is a file name or a wildcard.

  Return absolute path to FILE if found in the project root, nil
  otherwise."
    (when-let ((root (pet-project-root)))
      (car (file-expand-wildcards (concat (file-name-as-directory root) file) t))))

  (defun pet-locate-dominating-file (file)
    "Find FILE by walking up `default-directory' until the current project's root.

  FILE is a file name or a wildcard.

  Return absolute path to FILE if found, nil otherwise."
    (when-let* ((root (pet-project-root))
                (dir (locate-dominating-file
                      default-directory
                      (lambda (dir)
                        (car
                         (file-expand-wildcards
                          (concat (file-name-as-directory dir) file))))))
                (dir (expand-file-name dir)))
      (when (string-prefix-p root dir)
        (car (file-expand-wildcards (concat (file-name-as-directory dir) file) t)))))

  (defun pet-find-file-from-project-root-recursively (file)
    "Find FILE by recursively searching down from the current project's root.

  FILE is a file name or a wildcard.

  Return absolute path to FILE if found, nil otherwise."
    (condition-case err
        (when-let ((root (pet-project-root))
                   (fileset
                    (cond ((functionp 'projectile-dir-files)
                           (mapcar (apply-partially #'concat root)
                                   (projectile-dir-files (pet-project-root))))
                          ((functionp 'project-files)
                           (project-files (project-current)))
                          (t (directory-files-recursively
                              (pet-project-root)
                              (wildcard-to-regexp file))))))
          (seq-find (lambda (f)
                      (string-match-p
                       (wildcard-to-regexp file)
                       (file-name-nondirectory f)))
                    (sort fileset 'string<)))
      (error (pet-report-error err))))

  (defun pet-find-file-from-project (file)
    "Find FILE from the current project.

  Try each function in `pet-find-file-functions' in order and
  return the absolute path found by the first function, nil
  otherwise."
    (seq-some (lambda (fn) (funcall fn file)) pet-find-file-functions))

  (defun pet-parse-json (str)
    "Parse JSON STR to an alist.  Arrays are converted to lists."
    (if (functionp 'json-parse-string)
        (json-parse-string str :object-type 'alist :array-type 'list)
      (let ((json-array-type 'list))
        (json-read-from-string str))))

  (defun pet-parse-config-file (file-path)
    "Parse a configuration file at FILE-PATH into JSON alist."
    (condition-case err
        (let* ((ext (downcase (or (file-name-extension file-path) "")))
               (auto-mode-alist-matcher (lambda (entry)
                                          (pcase-let ((`(,pat . ,mode) entry))
                                            (when (string-match-p pat file-path)
                                              mode))))
               (mode (seq-some auto-mode-alist-matcher auto-mode-alist))
               (json-p (or (equal ext "json")
                           (eq 'json-mode mode)
                           (eq 'json-ts-mode mode)
                           (eq 'jsonian-mode mode)))
               (toml-p (or (equal ext "toml")
                           (eq 'conf-toml-mode mode)
                           (eq 'toml-ts-mode mode)))
               (yaml-p (or (string-match-p "ya?ml" ext)
                           (eq 'yaml-mode mode)
                           (eq 'yaml-ts-mode mode))))

          (let ((output (get-buffer-create " *pet parser output*")))
            (unwind-protect
                (let ((exit-code
                       (when (or toml-p yaml-p)
                         (condition-case err
                             (apply #'process-file
                                    (cond (toml-p pet-toml-to-json-program)
                                          (yaml-p pet-yaml-to-json-program))
                                    file-path
                                    output
                                    nil
                                    (cond (toml-p pet-toml-to-json-program-arguments)
                                          (yaml-p pet-yaml-to-json-program-arguments)))
                           (error (error-message-string err))))))

                  (cond ((and (integerp exit-code) (zerop exit-code))
                         (with-current-buffer output
                           (pet-parse-json (buffer-string))))
                        (json-p
                         (with-temp-buffer
                           (insert-file-contents file-path)
                           (pet-parse-json (buffer-string))))
                        (t
                         (error (if (stringp exit-code)
                                    exit-code
                                  (with-current-buffer output
                                    (buffer-string)))))))
              (kill-buffer output))))

      (error (pet-report-error err))))

  (defvar pet-watched-config-files nil)

  (defun pet-make-config-file-change-callback (cache-var parser)
    "Make callback for `file-notify-add-watch'.

  Return a callback with CACHE-VAR and PARSER captured in
  itsenvironment.  CACHE-VAR is the symbol to the cache variable to
  update.  PARSER is the symbol to the parser to parse the file.

  When invoked, the callback returned will parse the file with
  PARSER and cache the result in CACHE-VAR if the file was changed.
  If the file was deleted or renamed, remove the file's watcher,
  and delete the file entry from CACHE-VAR and
  `pet-watched-config-files'."
    (lambda (event)
      (pcase-let ((`(,_ ,action ,file . ,_) event))
        (pcase action
          ((or 'deleted 'renamed)
           (file-notify-rm-watch (assoc-default file pet-watched-config-files))
           (setf (alist-get file (symbol-value cache-var) nil t 'equal) nil)
           (setf (alist-get file pet-watched-config-files nil t 'equal) nil))
          ('changed
           (setf (alist-get file (symbol-value cache-var) nil nil 'equal)
                 (funcall parser file)))))))

  (defun pet-watch-config-file (config-file cache-var parser)
    "Keep cache fresh by watching for change in the config file.

  CONFIG-FILE is the path to the configuration file to watch for
  changes.  CACHE-VAR is the symbol to the variable where the
  parsed configuration file content is stored.  PARSER is the
  symbol to a function that takes a file path and parses its
  content into an alist."
    (unless (assoc-default config-file pet-watched-config-files)
      (push (cons config-file
                  (file-notify-add-watch
                   config-file
                   '(change)
                   (pet-make-config-file-change-callback cache-var parser)))
            pet-watched-config-files)))

  (cl-defmacro pet-def-config-accessor (name &key file-name parser)
    "Create a function for reading the content of a config file.

  NAME will be used to create a memorized funcion named `pet-NAME'
  to return the content of the configuration file FILE-NAME.
  FILE-NAME is the name or glob pattern of the configuration file
  that will be searched in the project.  The content of the file
  will be parsed by PARSER and then cached in a variable called
  `pet-NAME-cache'.

  Changes to the file will automatically update the cached content
  See `pet-watch-config-file' for details."
    (let* ((accessor-name (concat "pet-" (symbol-name name)))
           (path-accessor-name (concat accessor-name "-path"))
           (cache-var (intern (concat accessor-name "-cache")))
           (accessor-docstring
            (format "Accessor for `%s' in the current Python project.

  If the file is found in the current Python project, cache its
  content in `%s' and return it.

  If the file content change, it is parsed again and the cache is
  refreshed automatically.  If it is renamed or deleted, the cache
  entry is deleted.
  "
                    name (symbol-name cache-var)))
           (path-accessor-docstring (format "Path of `%s' in the current Python project.

  Return nil if the file is not found." file-name))
           (cache-var-docstring
            (format "Cache for `%s'.

  This variable is an alist where the key is the absolute path to a
  `%s' in some Python project and the value is the parsed content.
  " name name)))
      `(progn
         (defvar ,cache-var nil ,cache-var-docstring)

         (defun ,(intern path-accessor-name) ()
           ,path-accessor-docstring
           (pet-find-file-from-project ,file-name))

         (defun ,(intern accessor-name) ()
           ,accessor-docstring
           (when-let ((config-file (,(intern path-accessor-name))))
             (if-let ((cached-content (assoc-default config-file ,cache-var)))
                 cached-content
               (pet-watch-config-file config-file ',cache-var #',parser)
               (when-let ((content (funcall #',parser config-file)))
                 (push (cons config-file content) ,cache-var)
                 content)))))))

  (pet-def-config-accessor pre-commit-config
                           :file-name ".pre-commit-config.yaml"
                           :parser pet-parse-config-file)

  (pet-def-config-accessor pyproject
                           :file-name "pyproject.toml"
                           :parser pet-parse-config-file)

  (pet-def-config-accessor python-version
                           :file-name ".python-version"
                           :parser f-read-text)

  (pet-def-config-accessor pipfile
                           :file-name "Pipfile"
                           :parser pet-parse-config-file)

  ;; So `pet-parse-config-file' knows Pipfile can be parsed with `pet-toml-to-json-program'.
  (add-to-list 'auto-mode-alist '("/Pipfile\\'" . conf-toml-mode))

  (pet-def-config-accessor environment
                           :file-name "environment*.y*ml"
                           :parser pet-parse-config-file)

  (defun pet-use-pre-commit-p ()
    "Whether the current project is using `pre-commit'.

  Returns the path to the `pre-commit' executable."
    (and (pet-pre-commit-config)
         (or (pet--executable-find "pre-commit" t)
             (and (when-let* ((venv (pet-virtualenv-root))
                              (exec-path (list (concat (file-name-as-directory venv) (pet-system-bin-dir))))
                              (process-environment (copy-sequence process-environment)))
                    (setenv "PATH" (string-join exec-path path-separator))
                    (pet--executable-find "pre-commit" t))))))

  (defun pet-use-conda-p ()
    "Whether the current project is using `conda'.

  Returns the path to the `conda' executable variant found."
    (and (pet-environment)
         (or (pet--executable-find "conda" t)
             (pet--executable-find "mamba" t)
             (pet--executable-find "micromamba" t))))

  (defun pet-use-poetry-p ()
    "Whether the current project is using `poetry'.

  Returns the path to the `poetry' executable."
    (and (string-match-p
          "poetry"
          (or (let-alist (pet-pyproject)
                .build-system.build-backend)
              ""))
         (pet--executable-find "poetry" t)))

  (defun pet-use-pyenv-p ()
    "Whether the current project is using `pyenv'.

  Returns the path to the `pyenv' executable."
    (and (pet-python-version)
         (pet--executable-find "pyenv" t)))

  (defun pet-use-pipenv-p ()
    "Whether the current project is using `pipenv'.

  Returns the path to the `pipenv' executable."
    (and (pet-pipfile)
         (pet--executable-find "pipenv" t)))

  (defun pet-pre-commit-config-has-hook-p (id)
    "Determine if the `pre-commit' configuration has a hook.

  Return non-nil if the `pre-commit' configuration for the current
  project has hook ID set up."
    (member id (cl-loop for repo in (let-alist (pet-pre-commit-config) .repos)
                        append (cl-loop for hook in (let-alist repo .hooks)
                                        collect (let-alist hook .id)))))

  (defun pet-parse-pre-commit-db (db-file)
    "Parse `pre-commit' database.

  Read the pre-commit SQLite database located at DB-FILE into an alist."
    (if (and (functionp 'sqlite-available-p)
             (sqlite-available-p))
        (let ((db (sqlite-open db-file)))
          (unwind-protect
              (let* ((result-set (sqlite-select db "select * from repos" nil 'set))
                     result
                     row)
                (while (setq row (sqlite-next result-set))
                  (setq result (cons (seq-mapn (lambda (a b) (cons (intern a) b))
                                               (sqlite-columns result-set)
                                               row)
                                     result)))
                (sqlite-finalize result-set)
                result)
            (sqlite-close db)))

      (condition-case err
          (with-temp-buffer
            (process-file "sqlite3" nil t nil "-json" db-file "select * from repos")
            (pet-parse-json (buffer-string)))
        (error (pet-report-error err)))))

  (defvar pet-pre-commit-database-cache nil)

  (defun pet-pre-commit-virtualenv-path (hook-id)
    "Find the virtualenv location from the `pre-commit' database.

  If the `pre-commit' hook HOOK-ID is found in the current Python
  project's `.pre-commit-config.yaml' file, the hook ID and its
  additional dependencies are used to construct a key for looking
  up a virtualenv for the hook from the pre-commit database.

  In order to find the hook virtualenv, `pre-commit' and the hooks
  must both be installed into the current project first."
    (when-let* ((db-file
                 (concat
                  (expand-file-name
                   (file-name-as-directory
                    (or (getenv "PRE_COMMIT_HOME")
                        (getenv "XDG_CACHE_HOME")
                        "~/.cache/")))
                  (unless (getenv "PRE_COMMIT_HOME") "pre-commit/")
                  "db.db"))

                (db
                 (or (assoc-default db-file pet-pre-commit-database-cache)
                     (when (file-exists-p db-file)
                       (pet-watch-config-file db-file 'pet-pre-commit-database-cache 'pet-parse-pre-commit-db)
                       (when-let ((content (pet-parse-pre-commit-db db-file)))
                         (push (cons db-file content) pet-pre-commit-database-cache)
                         content))))

                (repo-config
                 (seq-find
                  (lambda (repo)
                    (seq-find
                     (lambda (hook)
                       (equal (let-alist hook .id) hook-id))
                     (let-alist repo .hooks)))
                  (let-alist (pet-pre-commit-config) .repos)))

                (repo-url
                 (let-alist repo-config .repo))

                (repo-dir
                 (let* ((additional-deps
                         (let-alist repo-config
                           (let-alist (seq-find (lambda (hook) (let-alist hook (equal .id hook-id))) .hooks)
                             .additional_dependencies)))
                        (unsorted-repo-url (concat repo-url ":" (string-join additional-deps ",")))
                        (sorted-repo-url (concat repo-url ":" (string-join (sort (copy-sequence additional-deps) 'string<) ","))))
                   (let-alist (seq-find
                               (lambda (row)
                                 (let-alist row
                                   (and (if additional-deps
                                            (or (equal .repo unsorted-repo-url)
                                                (equal .repo sorted-repo-url))
                                          (equal .repo repo-url))
                                        (equal .ref (let-alist repo-config .rev)))))
                               db)
                     .path))))

      (car
       (last
        (file-expand-wildcards
         (concat (file-name-as-directory repo-dir) "py_env-*")
         t)))))

  

  ;;;###autoload
  (defun pet-executable-find (executable)
    "Find the correct EXECUTABLE for the current Python project.

  Search for EXECUTABLE first in the `pre-commit' virtualenv, then
  whatever environment if found by `pet-virtualenv-root', then
  `pyenv', then finally from the variable `exec-path'.

  The executable will only be searched in an environment created by
  a Python virtualenv management tool if the project is set up to
  use it."
    (cond ((and (pet-use-pre-commit-p)
                (not (string-prefix-p "python" executable))
                (pet-pre-commit-config-has-hook-p executable))
           (condition-case err
               (let* ((venv (or (pet-pre-commit-virtualenv-path executable)
                                (user-error "`pre-commit' is configured but the hook `%s' does not appear to be installed" executable)))
                      (bin-dir (concat (file-name-as-directory venv) (pet-system-bin-dir)))
                      (bin-path (concat bin-dir "/" executable)))
                 (if (file-exists-p bin-path)
                     bin-path
                   (user-error "`pre-commit' is configured but `%s' is not found in %s" executable bin-dir)))
             (error (pet-report-error err))))
          ((when-let* ((venv (pet-virtualenv-root))
                       (path (list (concat (file-name-as-directory venv) (pet-system-bin-dir))))
                       (exec-path path)
                       (tramp-remote-path path)
                       (process-environment (copy-sequence process-environment)))
             (setenv "PATH" (string-join exec-path path-separator))
             (pet--executable-find executable t)))
          ((when (pet--executable-find "pyenv" t)
             (condition-case err
                 (car (process-lines "pyenv" "which" executable))
               (error (pet-report-error err)))))
          (t (or (pet--executable-find executable t)
                 (pet--executable-find (concat executable "3") t)))))

  (defvar pet-project-virtualenv-cache nil)

  ;;;###autoload
  (defun pet-virtualenv-root ()
    "Find the path to the virtualenv for the current Python project.

  Selects a virtualenv in the follow order:

  1. The value of the environment variable `VIRTUAL_ENV' if defined.
  2. If the current project is using any `conda' variant, return the absolute path
     to the virtualenv directory for the current project.
  3. Ditta for `poetry'.
  4. Ditto for `pipenv'.
  5. A directory in `pet-venv-dir-names' in the project root if found.
  6. If the current project is using `pyenv', return the path to the virtualenv
     directory by looking up the prefix from `.python-version'."
    (let ((root (pet-project-root)))
      (or (assoc-default root pet-project-virtualenv-cache)
          (when-let ((ev (getenv "VIRTUAL_ENV")))
            (expand-file-name ev))
          (let ((venv-path
                 (cond ((when-let* ((program (pet-use-conda-p))
                                    (default-directory (file-name-directory (pet-environment-path))))
                          (condition-case err
                              (with-temp-buffer
                                (let ((exit-code (process-file program nil t nil "info" "--json"))
                                      (output (string-trim (buffer-string))))
                                  (if (zerop exit-code)
                                      (let* ((json-output (pet-parse-json output))
                                             (env-dirs (or (let-alist json-output .envs_dirs)
                                                           (let-alist json-output .envs\ directories)))
                                             (env-name (alist-get 'name (pet-environment)))
                                             (env (seq-find 'file-directory-p
                                                            (seq-map (lambda (dir)
                                                                       (file-name-as-directory
                                                                        (concat
                                                                         (file-name-as-directory dir)
                                                                         env-name)))
                                                                     env-dirs))))
                                        (or env
                                            (user-error "Please create the environment with `$ %s create --file %s' first" program (pet-environment-path))))
                                    (user-error (buffer-string)))))
                            (error (pet-report-error err)))))
                       ((when-let ((program (pet-use-poetry-p))
                                   (default-directory (file-name-directory (pet-pyproject-path))))
                          (condition-case err
                              (with-temp-buffer
                                (let ((exit-code (process-file program nil t nil "env" "info" "--no-ansi" "--path"))
                                      (output (string-trim (buffer-string))))
                                  (if (zerop exit-code)
                                      output
                                    (user-error (buffer-string)))))
                            (error (pet-report-error err)))))
                       ((when-let ((program (pet-use-pipenv-p))
                                   (default-directory (file-name-directory (pet-pipfile-path))))
                          (condition-case err
                              (with-temp-buffer
                                (let ((exit-code (process-file program nil '(t nil) nil "--quiet" "--venv"))
                                      (output (string-trim (buffer-string))))
                                  (if (zerop exit-code)
                                      output
                                    (user-error (buffer-string)))))
                            (error (pet-report-error err)))))
                       ((when-let ((dir (cl-loop for name in pet-venv-dir-names
                                                 with dir = nil
                                                 if (setq dir (locate-dominating-file default-directory name))
                                                 return (file-name-as-directory (concat dir name)))))
                          (expand-file-name dir)))
                       ((when-let ((program (pet-use-pyenv-p))
                                   (default-directory (file-name-directory (pet-python-version-path))))
                          (condition-case err
                              (with-temp-buffer
                                (let ((exit-code (process-file program nil t nil "prefix"))
                                      (output (string-trim (buffer-string))))
                                  (if (zerop exit-code)
                                      (file-truename output)
                                    (user-error (buffer-string)))))
                            (error (pet-report-error err))))))))
            ;; root maybe nil when not in a project, this avoids caching a nil
            (when root
              (setf (alist-get root pet-project-virtualenv-cache nil nil 'equal) venv-path))
            venv-path))))

  

  (defvar flycheck-mode)
  (defvar flycheck-python-mypy-config)
  (defvar flycheck-pylintrc)
  (defvar flycheck-python-flake8-executable)
  (defvar flycheck-python-pylint-executable)
  (defvar flycheck-python-mypy-executable)
  (defvar flycheck-python-pyright-executable)
  (defvar flycheck-python-pycompile-executable)
  (defvar flycheck-python-ruff-executable)

  (defun pet-flycheck-python-pylint-find-pylintrc ()
    "Polyfill `flycheck-pylintrc'.

  Find the correct `pylint' configuration file according to the
  algorithm described at
  `https://pylint.pycqa.org/en/latest/user_guide/usage/run.html'."
    (let* ((pylintrc '("pylintrc" ".pylintrc" "pyproject.toml" "setup.cfg"))
           (found     (cond ((cl-loop for f in pylintrc
                                      with path = nil
                                      do (setq path (concat default-directory f))
                                      if (file-exists-p path)
                                      return (expand-file-name path)))
                            ((and (buffer-file-name)
                                  (file-exists-p (concat (file-name-directory (buffer-file-name)) "__init__.py")))
                             (when-let ((path (cl-loop for f in pylintrc
                                                       with dir = nil
                                                       do (setq dir (locate-dominating-file default-directory f))
                                                       if dir
                                                       return (concat dir f))))
                               (expand-file-name path))))))
      (if found
          found
        (cond ((when-let* ((ev (getenv "PYLINTRC"))
                           (path (expand-file-name ev)))
                 (and (file-exists-p path) path)))
              ((let* ((ev (getenv "XDG_CONFIG_HOME"))
                      (config-dir
                       (or (and ev (file-name-as-directory ev))
                           "~/.config/"))
                      (xdg-file-path (expand-file-name (concat config-dir "pylintrc"))))
                 (and (file-exists-p xdg-file-path) xdg-file-path)))
              ((let ((home-dir-pylintrc (expand-file-name "~/.pylintrc")))
                 (and (file-exists-p home-dir-pylintrc) home-dir-pylintrc)))
              (t "/etc/pylintrc")))))

  (defun pet-flycheck-toggle-local-vars ()
    "Toggle buffer local variables for `flycheck' Python checkers.

  When `flycheck-mode' is non-nil, set up all supported Python
  checker executable variables buffer-locally.  Reset them to
  default otherwise."
    (if (bound-and-true-p flycheck-mode)
        (progn
          (when (derived-mode-p (if (functionp 'python-base-mode) 'python-base-mode 'python-mode))
            (setq-local flycheck-python-mypy-config `("mypy.ini" ".mypy.ini" "pyproject.toml" "setup.cfg"
                                                      ,(expand-file-name
                                                        (concat
                                                         (or (when-let ((xdg-config-home (getenv "XDG_CONFIG_HOME")))
                                                               (file-name-as-directory xdg-config-home))
                                                             "~/.config/")
                                                         "mypy/config"))
                                                      ,(expand-file-name "~/.mypy.ini")))
            (setq-local flycheck-pylintrc (pet-flycheck-python-pylint-find-pylintrc))
            (setq-local flycheck-python-flake8-executable (pet-executable-find "flake8"))
            (setq-local flycheck-python-pylint-executable (pet-executable-find "pylint"))
            (setq-local flycheck-python-mypy-executable (pet-executable-find "mypy"))
            (setq-local flycheck-python-mypy-python-executable (pet-executable-find "python"))
            (setq-local flycheck-python-pyright-executable (pet-executable-find "pyright"))
            (setq-local flycheck-python-pycompile-executable python-shell-interpreter)
            (setq-local flycheck-python-ruff-executable (pet-executable-find "ruff"))))
      (kill-local-variable 'flycheck-python-mypy-config)
      (kill-local-variable 'flycheck-pylintrc)
      (kill-local-variable 'flycheck-python-flake8-executable)
      (kill-local-variable 'flycheck-python-pylint-executable)
      (kill-local-variable 'flycheck-python-mypy-executable)
      (kill-local-variable 'flycheck-python-mypy-python-executable)
      (kill-local-variable 'flycheck-python-pyright-executable)
      (kill-local-variable 'flycheck-python-pycompile-executable)
      (kill-local-variable 'flycheck-python-ruff-executable)))

  (defun pet-flycheck-python-find-project-root-advice (_)
    "Delegate `flycheck-python-find-project-root' to `pet-virtualenv-root'."
    (pet-virtualenv-root))

  ;;;###autoload
  (defun pet-flycheck-setup ()
    "Set up all `flycheck' Python checker configuration."
    (advice-add 'flycheck-python-find-project-root :override #'pet-flycheck-python-find-project-root-advice)
    (add-hook 'flycheck-mode-hook #'pet-flycheck-toggle-local-vars))

  ;;;###autoload
  (defun pet-flycheck-teardown ()
    "Reset all `flycheck' Python checker configuration to default."
    (advice-remove 'flycheck-python-find-project-root #'pet-flycheck-python-find-project-root-advice)
    (remove-hook 'flycheck-mode-hook #'pet-flycheck-toggle-local-vars)
    (kill-local-variable 'flycheck-python-mypy-config)
    (kill-local-variable 'flycheck-pylintrc)
    (kill-local-variable 'flycheck-python-flake8-executable)
    (kill-local-variable 'flycheck-python-pylint-executable)
    (kill-local-variable 'flycheck-python-mypy-executable)
    (kill-local-variable 'flycheck-python-mypy-python-executable)
    (kill-local-variable 'flycheck-python-pyright-executable)
    (kill-local-variable 'flycheck-python-pycompile-executable)
    (kill-local-variable 'flycheck-python-ruff-executable))

  

  (defvar eglot-workspace-configuration)
  (declare-function jsonrpc--process "ext:jsonrpc")
  (declare-function eglot--executable-find "ext:eglot")
  (declare-function eglot--workspace-configuration-plist "ext:eglot")
  (declare-function eglot--guess-contact "ext:eglot")

  (defun pet-eglot--executable-find-advice (fn &rest args)
    "Look up Python language servers using `pet-executable-find'.

  FN is `eglot--executable-find', or `executable-find' (1.17+), depending
  on the version of `eglot' being used. ARGS is the arguments to FN."
    (pcase-let ((`(,command . ,_) args))
      (if (member command '("pylsp" "pyls" "basedpyright-langserver" "pyright-langserver" "jedi-language-server" "ruff-lsp"))
          (pet-executable-find command)
        (apply fn args))))

  (defun pet-lookup-eglot-server-initialization-options (command)
    "Return LSP initializationOptions for Eglot.

  COMMAND is the name of the Python language server command."
    (cond ((string-match-p "pylsp" command)
           `(:pylsp
             (:plugins
              (:jedi
               (:environment
                ,(pet-virtualenv-root))
               :ruff
               (:executable
                ,(pet-executable-find "ruff"))
               :pylsp_mypy
               (:overrides
                ["--python-executable" ,(pet-executable-find "python") t])
               :flake8
               (:executable
                ,(pet-executable-find "flake8"))
               :pylint
               (:executable
                ,(pet-executable-find "pylint"))))))
          ((string-match-p "pyls" command)
           `(:pyls
             (:plugins
              (:jedi
               (:environment
                ,(pet-virtualenv-root))
               :pylint
               (:executable
                ,(pet-executable-find "pylint"))))))
          ((string-match-p "pyright-langserver" command)
           `(:python
             (:pythonPath
              ,(pet-executable-find "python")
              :venvPath
              ,(pet-virtualenv-root))))
          ((string-match-p "jedi-language-server" command)
           `(:jedi
             (:executable
              (:command
               ,(pet-executable-find "jedi-language-server"))
              :workspace
              (:environmentPath
               ,(pet-executable-find "python")))))
          ((string-match-p "ruff-lsp" command)
           `(:settings
             (:interpreter
              ,(pet-executable-find "python")
              :path
              ,(pet-executable-find "ruff"))))
          (t nil)))

  (defalias 'pet--proper-list-p 'proper-list-p)
  (eval-when-compile
    (when (and (not (functionp 'proper-list-p))
               (functionp 'format-proper-list-p))
      (defun pet--proper-list-p (l)
        (and (format-proper-list-p l)
             (length l)))))

  (defun pet--plistp (object)
    "Non-nil if and only if OBJECT is a valid plist."
    (let ((len (pet--proper-list-p object)))
      (and len
           (zerop (% len 2))
           (seq-every-p
            (lambda (kvp)
              (keywordp (car kvp)))
            (seq-split object 2)))))

  (defun pet-merge-eglot-initialization-options (a b)
    "Deep merge plists A and B."
    (map-merge-with 'plist
                    (lambda (c d)
                      (cond ((and (pet--plistp c) (pet--plistp d))
                             (pet-merge-eglot-initialization-options c d))
                            ((and (vectorp c) (vectorp d))
                             (vconcat (seq-union c d)))
                            (t d)))
                    (copy-tree a t)
                    (copy-tree b t)))

  (defun pet-eglot--workspace-configuration-plist-advice (fn &rest args)
    "Enrich `eglot-workspace-configuration' with paths found by `pet'.

  FN is `eglot--workspace-configuration-plist', ARGS is the
  arguments to `eglot--workspace-configuration-plist'."
    (let* ((path (cadr args))
           (canonical-path (if (and path (file-directory-p path))
                               (file-name-as-directory path)
                             path))
           (server (car args))
           (command (process-command (jsonrpc--process server)))
           (program (and (listp command) (car command)))
           (pet-config (pet-lookup-eglot-server-initialization-options program))
           (user-config (apply fn server (and canonical-path (cons canonical-path (cddr args))))))
      (pet-merge-eglot-initialization-options user-config pet-config)))

  (defun pet-eglot--guess-contact-advice (fn &rest args)
    "Enrich `eglot--guess-contact' with paths found by `pet'.

  FN is `eglot--guess-contact', ARGS is the arguments to
  `eglot--guess-contact'."
    (let* ((result (apply fn args))
           (contact (nth 3 result))
           (probe (seq-position contact :initializationOptions))
           (program-with-args (seq-subseq contact 0 (or probe (length contact))))
           (program (car program-with-args))
           (init-opts (plist-get (seq-subseq contact (or probe 0)) :initializationOptions)))
      (if init-opts
          (append (seq-subseq result 0 3)
                  (list
                   (append
                    program-with-args
                    (list
                     :initializationOptions
                     (pet-merge-eglot-initialization-options
                      init-opts
                      (pet-lookup-eglot-server-initialization-options
                       program)))))
                  (seq-subseq result 4))
        result)))

  (defun pet-eglot-setup ()
    "Set up Eglot to use server executables and virtualenvs found by PET."
    (if (fboundp 'eglot--executable-find)
        ;; Eglot version 1.16 or below
        (advice-add 'eglot--executable-find :around #'pet-eglot--executable-find-advice)
      ;; Eglot version 1.17 and above
      (advice-add 'executable-find :around #'pet-eglot--executable-find-advice))
    (advice-add 'eglot--workspace-configuration-plist :around #'pet-eglot--workspace-configuration-plist-advice)
    (advice-add 'eglot--guess-contact :around #'pet-eglot--guess-contact-advice))

  (defun pet-eglot-teardown ()
    "Tear down PET advices to Eglot."
    (if (fboundp 'eglot--executable-find)
        ;; Eglot version 1.16 or below
        (advice-remove 'eglot--executable-find #'pet-eglot--executable-find-advice)
      ;; Eglot version 1.17 and above
      (advice-remove 'executable-find #'pet-eglot--executable-find-advice))
    (advice-remove 'eglot--workspace-configuration-plist #'pet-eglot--workspace-configuration-plist-advice)
    (advice-remove 'eglot--guess-contact #'pet-eglot--guess-contact-advice))

  
  (defvar dape-command)
  (defvar dape-cwd-fn)

  (defun pet-dape-setup ()
    "Set up the buffer local variables for `dape'."
    (if-let* ((main (pet-find-file-from-project-root-recursively "__main__.py"))
              (module (let* ((dir (file-name-directory main))
                             (dir-file-name (directory-file-name dir))
                             (module))
                        (while (file-exists-p (concat dir "__init__.py"))
                          (push (file-name-nondirectory dir-file-name) module)
                          (setq dir (file-name-directory dir-file-name))
                          (setq dir-file-name (directory-file-name dir)))
                        (string-join module "."))))
        (setq-local dape-command `(debugpy-module command ,(pet-executable-find "python") :module ,module))
      (setq-local dape-command `(debugpy command ,(pet-executable-find "python"))))
    (setq-local dape-cwd-fn #'pet-project-root))

  (defun pet-dape-teardown ()
    "Tear down the buffer local variables for `dape'."
    (kill-local-variable 'dape-command)
    (kill-local-variable 'dape-cwd-fn))

  

  (defvar lsp-jedi-executable-command)
  (defvar lsp-pyls-plugins-jedi-environment)
  (defvar lsp-pylsp-plugins-jedi-environment)
  (defvar lsp-pyright-python-executable-cmd)
  (defvar lsp-pyright-venv-path)
  (defvar lsp-ruff-server-command)
  (defvar lsp-ruff-python-path)
  (defvar dap-python-executable)
  (defvar dap-variables-project-root-function)
  (defvar python-pytest-executable)
  (defvar python-black-command)
  (defvar python-isort-command)
  (defvar ruff-format-command)
  (defvar blacken-executable)
  (defvar yapfify-executable)
  (defvar py-autopep8-command)

  (defun pet-buffer-local-vars-setup ()
    "Set up the buffer local variables for Python tools.

  Assign all supported Python tooling executable variables to
  buffer local values."
    (setq-local python-shell-interpreter (pet-executable-find "python"))
    (setq-local python-shell-virtualenv-root (pet-virtualenv-root))

    (pet-flycheck-setup)

    (setq-local lsp-jedi-executable-command
                (pet-executable-find "jedi-language-server"))
    (setq-local lsp-pyls-plugins-jedi-environment python-shell-virtualenv-root)
    (setq-local lsp-pylsp-plugins-jedi-environment python-shell-virtualenv-root)
    (setq-local lsp-pyright-venv-path python-shell-virtualenv-root)
    (setq-local lsp-pyright-python-executable-cmd python-shell-interpreter)
    (setq-local lsp-ruff-server-command (list (pet-executable-find "ruff") "server"))
    (setq-local lsp-ruff-python-path python-shell-interpreter)
    (setq-local dap-python-executable python-shell-interpreter)
    (setq-local dap-variables-project-root-function #'pet-project-root)
    (setq-local python-pytest-executable (pet-executable-find "pytest"))
    (setq-local python-black-command (pet-executable-find "black"))
    (setq-local python-isort-command (pet-executable-find "isort"))
    (setq-local ruff-format-command (pet-executable-find "ruff"))
    (setq-local blacken-executable python-black-command)
    (setq-local yapfify-executable (pet-executable-find "yapf"))
    (setq-local py-autopep8-command (pet-executable-find "autopep8"))

    (pet-eglot-setup)
    (pet-dape-setup))

  (defun pet-buffer-local-vars-teardown ()
    "Reset all supported buffer local variable values to default."

    (kill-local-variable 'python-shell-interpreter)
    (kill-local-variable 'python-shell-virtualenv-root)

    (pet-flycheck-teardown)

    (kill-local-variable 'lsp-jedi-executable-command)
    (kill-local-variable 'lsp-pyls-plugins-jedi-environment)
    (kill-local-variable 'lsp-pylsp-plugins-jedi-environment)
    (kill-local-variable 'lsp-pyright-venv-path)
    (kill-local-variable 'lsp-pyright-python-executable-cmd)
    (kill-local-variable 'lsp-ruff-python-path)
    (kill-local-variable 'lsp-ruff-server-command)
    (kill-local-variable 'dap-python-executable)
    (kill-local-variable 'dap-variables-project-root-function)
    (kill-local-variable 'python-pytest-executable)
    (kill-local-variable 'python-black-command)
    (kill-local-variable 'python-isort-command)
    (kill-local-variable 'ruff-format-command)
    (kill-local-variable 'blacken-executable)
    (kill-local-variable 'yapfify-executable)
    (kill-local-variable 'py-autopep8-command)

    (pet-eglot-teardown)
    (pet-dape-teardown))

  (defun pet-verify-setup ()
    "Verify the values of buffer local variables visually.

  Print all of the buffer local variable values `pet-mode'
  has assigned to."
    (interactive)

    (unless (derived-mode-p 'python-base-mode 'python-mode)
      (user-error "You are not in python-mode!"))

    (let ((kvp (mapcar (lambda (sym)
                         (cons sym
                               (if (boundp sym)
                                   (let ((val (symbol-value sym)))
                                     (if (consp val)
                                         (apply #'string-join
                                                (mapcar (apply-partially #'abbreviate-file-name)
                                                        (mapcar (apply-partially #'format "%s") val))
                                                (list ", "))
                                       (abbreviate-file-name (format "%s" val))))
                                 'unbound)))
                       '(python-shell-interpreter
                         python-shell-virtualenv-root
                         flycheck-python-flake8-executable
                         flycheck-pylintrc
                         flycheck-python-pylint-executable
                         flycheck-python-mypy-executable
                         flycheck-python-mypy-config
                         flycheck-python-mypy-python-executable
                         flycheck-python-pyright-executable
                         flycheck-python-pycompile-executable
                         flycheck-python-ruff-executable
                         lsp-jedi-executable-command
                         lsp-pyls-plugins-jedi-environment
                         lsp-pylsp-plugins-jedi-environment
                         lsp-pyright-python-executable-cmd
                         lsp-pyright-venv-path
                         lsp-ruff-server-command
                         lsp-ruff-python-path
                         dap-python-executable
                         dap-variables-project-root-function
                         dape-command
                         dape-cwd-fn
                         python-pytest-executable
                         python-black-command
                         blacken-executable
                         python-isort-command
                         ruff-format-command
                         yapfify-executable
                         py-autopep8-command))))

      (with-current-buffer-window "*pet info*" nil nil
        (mapc (pcase-lambda (`(,key . ,value))
                (insert (propertize (format "%-40s" (concat (symbol-name key) ":")) 'face 'font-lock-variable-name-face))
                (insert (format "%s" value))
                (insert "\n"))
              kvp)
        (insert (propertize (format "%-40s"
                                    (concat (symbol-name (if (file-remote-p default-directory)
                                                             'tramp-remote-path
                                                           'exec-path))
                                            ":"))
                            'face 'font-lock-variable-name-face) "\n")
        (mapc (lambda (dir)
                (insert (abbreviate-file-name (format "%s" dir)) "\n"))
              (if (file-remote-p default-directory)
                  tramp-remote-path
                exec-path))
        (special-mode))))

  ;;;###autoload
  (define-minor-mode pet-mode
    "Minor mode to set up buffer local variables for Python tools."
    :lighter " Pet"
    :group 'pet
    (if pet-mode
        (progn
          (pet-buffer-local-vars-setup)
          (add-hook 'kill-buffer-hook #'pet-cleanup-watchers-and-caches t))
      (pet-buffer-local-vars-teardown)
      (remove-hook 'kill-buffer-hook #'pet-cleanup-watchers-and-caches t)))

  (defun pet-cleanup-watchers-and-caches ()
    "Clean up configuration file caches and watchers.

  Delete configuration file caches and watchers when all
  `python-mode' buffers of a project have been closed."
    (when (and (buffer-file-name)
               (derived-mode-p 'python-base-mode 'python-mode))
      (when-let ((root (pet-project-root)))
        (when (null (cl-loop for buf in (buffer-list)
                             if (and (not (equal buf (current-buffer)))
                                     (string-prefix-p root (buffer-file-name buf)))
                             return buf))

          (setf (alist-get root pet-project-virtualenv-cache nil t 'equal) nil)

          (pcase-dolist (`(,config-file . ,watcher) pet-watched-config-files)
            (when (string-prefix-p root config-file)
              (file-notify-rm-watch watcher)
              (setf (alist-get config-file pet-watched-config-files nil t 'equal) nil)))

          (dolist (cache '(pet-pre-commit-config-cache
                           pet-pyproject-cache
                           pet-python-version-cache
                           pet-pipfile-cache
                           pet-environment-cache))
            (pcase-dolist (`(,key . ,_) (symbol-value cache))
              (when (string-prefix-p root key)
                (setf (alist-get key (symbol-value cache) nil t 'equal) nil))))))))

  (provide 'vedang-pet)

  ;;; pet.el ends here