notes

My Emacs config

First published: Last updated: 8196 words · 6971 lines of code
Table of contents

I am not a programmer, let alone an Elisp hacker. My background is in the humanities. It is only a slight exaggeration to say that, before I started using Emacs in 2020, I didn’t know the difference between a function and a variable. You have been forewarned.

This configuration is an Org document that tangles to an Emacs Lisp init file. It is organized into thematic sections—package management, version control, display, text manipulation, and so on—each of which groups related package declarations together. In addition to built-in features and external packages, it loads dozens of “extras” files: personal Elisp libraries stored in the emacs/extras/ directory and named after the package they extend (e.g., org-extras, elfeed-extras, gptel-extras). These files are loaded via the use-personal-package macro, a thin wrapper around use-package that fetches the corresponding file from this dotfiles repository. Such a setup allows me to extend the functionality of various packages and features without cluttering the main configuration section. For example, instead of piling dozens of custom org-mode functions into the org section of this file, I place them in emacs/extras/org-extras.el and load that file with a single (use-personal-package org-extras) declaration here. This structure also allows anyone to try out my configuration selectively and straightforwardly. Thus, if you’d like to install my org extensions, you can just add one of the following recipes to your own config (depending on which package manager or Emacs version you use):

;; with elpaca
(use-package org-extras
  :ensure (:host github :repo "benthamite/dotfiles"
                 :files ("emacs/extras/org-extras.el"
                         "emacs/extras/doc/org-extras.texi")))

;; with straight
(use-package org-extras
  :straight (:host github :repo "benthamite/dotfiles"
                   :files ("emacs/extras/org-extras.el"
                           "emacs/extras/doc/org-extras.texi")))

;; with vc (requires Emacs 30.1 or higher; no Info manual)
(use-package org-extras
  :vc (:url "https://github.com/benthamite/dotfiles"
            :lisp-dir "emacs/extras"
            :rev :newest))

The extras come with their own manuals and user options: everything is documented and customizable. When installed via elpaca or straight, each package’s Info manual is built and registered automatically. To read a manual, type M-x info-display-manual RET (or C-h R) and enter the package name (e.g. org-extras). You can also browse all available manuals with M-x info RET (C-h i). (The :vc recipe does not currently build Info manuals due to a limitation in package-vc.)

early-init

The contents of this code block are tangled to the early-init.el file.

First, I check the system appearance and blacken the screen if it’s set to `dark`. This is done to prevent a flash of white during startup on macOS when using a dark theme. I use frame parameters to set the background and foreground colors instead of set-face-attribute to avoid interfering with face-spec-recalc during theme switches.

(defun macos-get-system-appearance ()
  "Return the current macOS system appearance."
  (intern (downcase (string-trim (shell-command-to-string
                                  "defaults read -g AppleInterfaceStyle 2>/dev/null || echo 'Light'")))))

(defun early-init-blacken-screen ()
  "Blacken screen as soon as Emacs starts, if the system theme is `dark'.
Use frame parameters instead of `set-face-attribute' to avoid
interfering with `face-spec-recalc' during theme switches."
  (when (eq (macos-get-system-appearance) 'dark)
    (setopt mode-line-format nil)
    (push '(background-color . "#000000") default-frame-alist)
    (push '(foreground-color . "#ffffff") default-frame-alist)))

(early-init-blacken-screen)

I also disable package initialization at startup (recommended for elpaca) and set load-prefer-newer to t to ensure that Emacs always loads the latest version of a package (useful during development when packages are frequently updated).

(setq package-enable-at-startup nil)
(setq load-prefer-newer t)

Then, I set some frame parameters to remove the title bar and maximize the frame on startup.

(add-to-list 'default-frame-alist '(undecorated-round . t)) ; remove title bar
(add-to-list 'initial-frame-alist '(fullscreen . maximized)) ; maximize frame on startup

Finally, I redirect the native compilation cache to a directory within my Emacs profile and define a function for debugging feature loading.

;; github.com/emacscollective/no-littering#native-compilation-cache
(when (fboundp 'startup-redirect-eln-cache)
  (startup-redirect-eln-cache
   (file-name-concat (getenv "HOME")
                     ".config/emacs-profiles/var/eln-cache/")))

;; for debugging
(defun early-init-trace-feature-load (feature)
  "Print a backtrace immediately after FEATURE is loaded."
  (eval-after-load feature
    `(message "Feature '%s' loaded by:\n%s"
              ',feature
              (with-output-to-string
                (backtrace)))))

package management

elpaca

elpaca is a package manager that supports asynchronous installation of packages.

When experiencing issues, follow these steps.

  • By default, elpaca makes shallow copies of all the repos it clones. You can specify the repo depth with the :depth keyword. What if, however, you want to turn a shallow repo into a full repo after it has been cloned? There is a relatively obscure command in Magit that lets you do this: magit-remote-unshallow. (Note that this not only passes the --unshallow flag but also restores access to all branches in addition to the main one.)
;;; init.el --- Init File -*- lexical-binding: t -*-
(defvar elpaca-installer-version 0.12)
(defvar elpaca-directory (expand-file-name "elpaca/" user-emacs-directory))
(defvar elpaca-builds-directory (expand-file-name "builds/" elpaca-directory))
(defvar elpaca-sources-directory (expand-file-name "sources/" elpaca-directory))
;; Using benthamite fork until progfolio/elpaca#513 is merged (mono-repo deadlock fix).
(defvar elpaca-order '(elpaca :repo "https://github.com/benthamite/elpaca.git"
                              :ref "6503e6c19931dc42bf16e9af980d2e69921f7b6a" :depth 1 :inherit ignore
                              :files (:defaults "elpaca-test.el" (:exclude "extensions"))
                              :build (:not elpaca-activate)))
(let* ((repo  (expand-file-name "elpaca/" elpaca-sources-directory))
       (build (expand-file-name "elpaca/" elpaca-builds-directory))
       (order (cdr elpaca-order))
       (default-directory repo))
  (add-to-list 'load-path (if (file-exists-p build) build repo))
  (unless (file-exists-p repo)
    (make-directory repo t)
    (when (<= emacs-major-version 28) (require 'subr-x))
    (condition-case-unless-debug err
        (if-let* ((buffer (pop-to-buffer-same-window "*elpaca-bootstrap*"))
                  ((zerop (apply #'call-process `("git" nil ,buffer t "clone"
                                                  ,@(when-let* ((depth (plist-get order :depth)))
                                                      (list (format "--depth=%d" depth) "--no-single-branch"))
                                                  ,(plist-get order :repo) ,repo))))
                  ((zerop (call-process "git" nil buffer t "checkout"
                                        (or (plist-get order :ref) "--"))))
                  (emacs (concat invocation-directory invocation-name))
                  ((zerop (call-process emacs nil buffer nil "-Q" "-L" "." "--batch"
                                        "--eval" "(byte-recompile-directory \".\" 0 'force)")))
                  ((require 'elpaca))
                  ((elpaca-generate-autoloads "elpaca" repo)))
            (progn (message "%s" (buffer-string)) (kill-buffer buffer))
          (error "%s" (with-current-buffer buffer (buffer-string))))
      ((error) (warn "%s" err) (delete-directory repo 'recursive))))
  (unless (require 'elpaca-autoloads nil t)
    (require 'elpaca)
    (elpaca-generate-autoloads "elpaca" repo)
    (let ((load-source-file-function nil)) (load "./elpaca-autoloads"))))
(add-hook 'after-init-hook #'elpaca-process-queues)
(elpaca `(,@elpaca-order))
(elpaca-wait)

;; NOTE: Do not set elpaca-queue-limit. It counts *all* queued packages (not just
;; actively cloning ones) as "active", causing a false deadlock on fresh installs
;; where hundreds of packages are enqueued simultaneously. See elpaca--continue-build.

(require 'elpaca-menu-elpa)
(setf (alist-get 'packages-url (alist-get 'gnu elpaca-menu-elpas))
      "https://raw.githubusercontent.com/emacsmirror/gnu_elpa/refs/heads/main/elpa-packages"
      (alist-get 'remote (alist-get 'gnu elpaca-menu-elpas))
      "https://github.com/emacsmirror/gnu_elpa"
      (alist-get 'packages-url (alist-get 'nongnu elpaca-menu-elpas))
      "https://raw.githubusercontent.com/emacsmirror/nongnu_elpa/refs/heads/main/elpa-packages"
      (alist-get 'remote (alist-get 'nongnu elpaca-menu-elpas))
      "https://github.com/emacsmirror/nongnu_elpa")

(toggle-debug-on-error) ; uncomment when debugging
(setq elpaca-lock-file
      (file-name-concat (file-name-directory (directory-file-name elpaca-directory)) "lockfile.el"))

use-package

use-package is a package organizer.

(elpaca elpaca-use-package
  (elpaca-use-package-mode))

(use-package use-package
  :demand t
  :custom
  (use-package-always-ensure t)
  (use-package-verbose t)
  (use-package-compute-statistics t)
  (use-package-hook-name-suffix nil) ; use real name for hooks, i.e. do not omit the `-hook' bit
  (use-package-minimum-reported-time 0.1)

  :config
  (defmacro use-personal-package (name &rest args)
    "Like `use-package' but to load personal packages.
NAME and ARGS as in `use-package'."
    (declare (indent defun))
    (let ((name-str (symbol-name (eval `(quote ,name)))))
      `(use-package ,name
         :ensure (:host github
                        :repo "benthamite/dotfiles"
                        :files ,(list (file-name-concat
                                       "emacs/extras"
                                       (file-name-with-extension name-str "el"))
                                      (file-name-concat
                                       "emacs/extras/doc"
                                       (file-name-with-extension name-str "texi")))
                        :depth nil)
         ,@args))))

(elpaca-wait)

Because use-personal-package declares each extras file with an :ensure recipe pointing at this GitHub repository, elpaca treats them like any other package: it clones the repo into its own elpaca/repos/dotfiles/ directory and builds the relevant files from there. This means the dotfiles end up in two separate local clones—the primary one in my main dotfiles location (where all edits are made) and the elpaca-managed one under elpaca/repos/dotfiles/ (which Emacs loads from). To keep them in sync, three git hooks in the primary clone’s gitdir automatically propagate changes to the elpaca clone. The post-commit hook handles normal commits, the post-rewrite hook handles rebases and amends, and both delegate to a shared sync-elpaca-clone.sh script. All three files live in the gitdir’s hooks/ directory and must be executable. The script reads the active profile name from a cache file (~/.config/emacs-profiles/.current-profile) that Emacs writes at startup, rather than calling emacsclient, to avoid deadlocking when the hook is triggered by a synchronous git process inside Emacs (e.g., magit-commit-squash).

hooks/sync-elpaca-clone.sh:

#!/bin/sh
# Propagate the current state of master to the elpaca dotfiles clone.
# Reads the active profile from a cache file written by Emacs at startup,
# avoiding emacsclient calls that deadlock when git is spawned synchronously
# by Emacs.
# Called by post-commit and post-rewrite hooks.
PROFILE=$(cat "$HOME/.config/emacs-profiles/.current-profile" 2>/dev/null)
ELPACA_BASE="$HOME/.config/emacs-profiles/$PROFILE/elpaca"

# Newer elpaca versions use sources/ instead of repos/
if [ -d "$ELPACA_BASE/sources/dotfiles/.git" ]; then
    ELPACA_DOTFILES="$ELPACA_BASE/sources/dotfiles"
elif [ -d "$ELPACA_BASE/repos/dotfiles/.git" ]; then
    ELPACA_DOTFILES="$ELPACA_BASE/repos/dotfiles"
fi

if [ -n "$ELPACA_DOTFILES" ]; then
    GITDIR="$GIT_DIR"
    # The parent git process sets GIT_DIR, GIT_INDEX_FILE, and
    # GIT_WORK_TREE pointing at the Google Drive clone.  Unsetting them
    # is essential so that git commands target the elpaca clone.
    unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE
    git -C "$ELPACA_DOTFILES" fetch "$GITDIR" master 2>&1 &&
    git -C "$ELPACA_DOTFILES" reset --hard FETCH_HEAD 2>&1
fi

hooks/post-commit:

#!/bin/sh
exec "$(dirname "$0")/sync-elpaca-clone.sh"

hooks/post-rewrite:

#!/bin/sh
exec "$(dirname "$0")/sync-elpaca-clone.sh"

use-package-extras

use-package-extras collects my extensions for use-package.

(use-personal-package use-package-extras
  :demand t
  :hook
  (init-post-init-hook . use-package-extras-display-startup-time))

elpaca-extras

elpaca-extras collects my extensions for elpaca.

(use-personal-package elpaca-extras
  :ensure (:wait t)
  :after use-package-extras
  :custom
  (elpaca-extras-write-lock-file-excluded '(tlon)))

foundational

gcmh

GCMH enforces a sneaky Garbage Collection strategy to minimize GC interference with user activity.

(use-package gcmh
  :config
  (gcmh-mode))

seq

seq provides sequence-manipulation functions that complement basic functions provided by subr.el.

;; https://github.com/progfolio/elpaca/issues/216#issuecomment-1868747372
(defun elpaca-unload-seq (e)
  (and (featurep 'seq) (unload-feature 'seq t))
  (elpaca--continue-build e))

(use-package seq
  :ensure `(seq :build (:before elpaca-activate elpaca-unload-seq)))

paths

paths defines various paths used in this configuration.

(use-personal-package paths)

transient

transient is a library for creating keyboard-driven menus.

(use-package transient
  :ensure (:host github
                 :repo "magit/transient"
                 :branch "main" ; github.com/progfolio/elpaca/issues/342
                 :build (:not elpaca-check-version))
  :after seq
  :custom
  (transient-default-level 7) ; magit.vc/manual/transient/Enabling-and-Disabling-Suffixes.html
  (transient-save-history nil) ; the history file was throwing an error on startup

  :bind
  (:map transient-base-map
        ("M-q" . transient-quit-one)))

init

init is a private package that I use to manage my config files and profiles.

(use-package init
  :ensure (:host github
                 :repo "benthamite/init"
                 :depth nil ; clone entire repo, not just last commit
                 :wait t)
  :after paths
  :demand t
  :config
  (init-startup)

  ;; Cache the profile name to a file so that git hooks can read it
  ;; without calling emacsclient (which deadlocks when the hook is
  ;; spawned by a synchronous git process inside Emacs).
  (with-temp-file (file-name-concat
                   (file-name-directory
                    (directory-file-name user-emacs-directory))
                   ".current-profile")
    (insert init-current-profile))

  :bind
  ("A-n" . init-menu))

no-littering

no-littering keeps .emacs.d clean.

(use-package no-littering
  :ensure (:wait t)
  :demand t
  :init
  ;; these directories should be shared across profiles, so there should
  ;; be only one `var' and one `etc' directory in `emacs-profiles'
  ;; rather than a pair of such directories for each profile
  (setq no-littering-etc-directory (file-name-concat paths-dir-emacs-profiles "etc/"))
  (setq no-littering-var-directory (file-name-concat paths-dir-emacs-profiles "var/"))

  :config
  ;; github.com/emacscollective/no-littering#auto-save-settings
  ;; should not be set via :custom
  (setq auto-save-file-name-transforms
        `((".*" ,(no-littering-expand-var-file-name "auto-save/") t))))

ns-win

ns-win provides various Nexstep convenience functions.

(use-feature ns-win
  :custom
  (mac-option-modifier 'meta)
  (mac-control-modifier 'control)
  (mac-command-modifier 'hyper)
  (mac-function-modifier 'none)
  (mac-right-option-modifier 'none)
  (mac-right-control-modifier 'super)
  (mac-right-command-modifier 'alt)
  ;; ns-use-proxy-icon set to t causes Emacs to freeze
  (ns-use-proxy-icon nil))

iso-transl

iso-transl defines ways of entering the non-ASCII printable characters with codes above 127.

(use-feature iso-transl
  :config
  (setq iso-transl-char-map nil) ; emacs.stackexchange.com/questions/17508/

  ;; unset all `Super' key bindings
  (dolist (char (number-sequence ?a ?z))
    (keymap-global-unset (concat "s-" (char-to-string char))))

  ;; unset some `Alt' key bindings in `key-translation-map'
  (dolist (char '("SPC" "!" "$" "+" "-" "<" ">" "?" "a" "c" "m" "o" "u"
                  "x" "C" "L" "P" "R" "S" "T" "Y" "[" "]" "{" "|" "}"))
    (keymap-unset key-translation-map (concat "A-" char))))

el-patch

el-patch customizes the behavior of Emacs Lisp functions and notifies the user when a function so customized changes.

(use-package el-patch)

casual

casual is a collection of Transient menus for various Emacs modes.

(use-package casual
  :defer t
  :init
  (with-eval-after-load 'calc-mode
    (bind-keys :map calc-mode-map
               ("C-o" . casual-calc-tmenu)
               :map calc-alg-map
               ("C-o" . casual-calc-tmenu))))

warnings

warnings provides support for logging and displaying warnings.

(use-feature warnings
  :custom
  (warning-suppress-types '((copilot copilot-exceeds-max-char)
                            (flycheck syntax-checker)
                            (org-roam)
                            (tramp)
                            (aidermacs)
                            (org-element-cache)
                            (yasnippet backquote-change))))

comp

comp compiles Lisp code into native code.

(use-feature comp
  :custom
  (native-comp-async-report-warnings-errors nil))

bytecomp

bytecomp compiles Lisp code into byte code.

(use-feature bytecomp
  :custom
  (byte-compile-warnings '(cl-functions)))

startup

startup processes Emacs shell arguments and controls startup behavior.

(use-feature emacs
  :custom
  (user-full-name "Pablo Stafforini")
  (user-mail-address (getenv "PERSONAL_GMAIL"))
  (initial-scratch-message nil)
  (inhibit-startup-screen t)
  (inhibit-startup-echo-area-message user-login-name)
  (inhibit-startup-buffer-menu t)
  (frame-resize-pixelwise t))

server

server starts a server for external clients to connect to.

(use-feature server
  :demand t
  :config
  (unless (server-running-p)
    (server-start)))

async

async is a simple library for asynchronous processing in Emacs.

(use-package async
  :defer t)

prot-common

[[https://github.com/protesilaos/dotfiles/blob/master/emacs.emacs.d/prot-lisp/prot-common.el][prot-common]] is a set of functions used by Protesilaos Stavrou’s unreleased “packages”._

Note Prot’s clarification:

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.

(use-package prot-common
  :ensure (:host github
           :repo "protesilaos/dotfiles"
           :local-repo "prot-common"
           :main "emacs/.emacs.d/prot-lisp/prot-common.el"
           :build (:not elpaca-check-version)
           :files ("emacs/.emacs.d/prot-lisp/prot-common.el")))

prot-simple

[[https://github.com/protesilaos/dotfiles/blob/master/emacs.emacs.d/prot-lisp/prot-simple.el][prot-simple]] is a set of common commands used by Protesilaos Stavrou’s unreleased “packages”._

Note Prot’s clarification:

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.

(use-package prot-simple
  :ensure (:host github
           :repo "protesilaos/dotfiles"
           :local-repo "prot-simple"
           :main "emacs/.emacs.d/prot-lisp/prot-simple.el"
           :build (:not elpaca-check-version)
           :files ("emacs/.emacs.d/prot-lisp/prot-simple.el"))
  :after prot-common
  :custom
  (prot-simple-date-specifier "%F")
  (prot-simple-time-specifier "%R %z")

  :bind
  (("M-s-=" . prot-simple-insert-date)
   ("A-C-H-j" . prot-simple-mark-sexp)))

bug-hunter

bug-hunter interactively bisects and debugs your init file.

(use-package bug-hunter
  :defer t)

inheritenv

inheritenv allows temp buffers to inherit buffer-local environment variables.

(use-package inheritenv
  :ensure (:host github :repo "purcell/inheritenv")
  :defer t)

misc

Miscellaneous settings: default directory, short answers, message log, bell, cursor width, and UTF-8 encoding.

(use-feature emacs
  :custom
  (default-directory paths-dir-dropbox)
  (use-short-answers t)
  (message-log-max t)
  (ring-bell-function 'ignore) ; silence bell when mistake is made
  (x-stretch-cursor t) ; make curor the width of the character under it
  ;; emacs.stackexchange.com/questions/14509/kill-process-buffer-without-confirmation
  ;; UTF8 stuff.

  :init
  (prefer-coding-system 'utf-8)
  (set-default-coding-systems 'utf-8)
  (set-terminal-coding-system 'utf-8)
  (set-keyboard-coding-system 'utf-8)

  :bind
  (:map input-decode-map
        ("M-8" . "•")))

performance

profiler

profiler provides UI and helper functions for the Emacs profiler.

(use-feature profiler
  :defer t)

profiler-extras

profiler-extras collects my extensions for profiler.

(use-personal-package profiler-extras
  :bind
  (("A-H-p" . profiler-extras-profiler-toggle)
   :map profiler-report-mode-map
   ("<backtab>" . profiler-extras-profiler-report-toggle-entry-global)))

so-long

so-long optimizes performance with minified code.

(use-feature so-long
  :custom
  (so-long-threshold 500000)

  :hook
  (find-file-hook . global-so-long-mode))

misc.

Performance-related settings: bidirectional display, font caches, fontification skipping, and process output buffer size.

Partly borrowed from Prot.

(use-feature emacs
  :custom
  (bidi-display-reordering nil)
  (bidi-inhibit-bpa t)
  (inhibit-compacting-font-caches t)
  (redisplay-skip-fontification-on-input t)
  ;; emacs-lsp.github.io/lsp-mode/page/performance/
  (read-process-output-max (expt 1024 2))
  (bidi-paragraph-direction 'left-to-right))

secrets

plstore

plstore is a plist based data store providing search and partial encryption.

This feature is required by org-gcal. We create a new GPG key to use with org-gcal and add its public ID to plstore-encrypt-to , following these instructions. (This method is superior to using symmetric encryption because it does not prompt the user for authentication with every new Emacs session.)

(use-feature plstore
  :after pass
  :config
  (add-to-list 'plstore-encrypt-to "A7C6A908CD1254A8B4051D3DCDBBB523C9627A26"))

epg-config

epg-config provides configuration for the Easy Privacy Guard library.

(use-feature epg-config
  :custom
  (epg-pinentry-mode 'loopback) ; use minibuffer for password entry
  (epg-gpg-program "/opt/homebrew/bin/gpg"))

auth-source

auth-source supports authentication sources for Gnus and Emacs.

(use-feature auth-source
  :preface
  (eval-when-compile
    (defvar auth-sources))

  :custom
  (auth-source-debug nil) ; set to t for debugging
  (auth-source-do-cache t)
  (auth-sources '(macos-keychain-internet macos-keychain-generic)))

oauth2-auto

emacs-oauth2-auto supports authentication to an OAuth2 provider from within Emacs.

(use-package oauth2-auto
  :ensure (:host github
                 :repo "telotortium/emacs-oauth2-auto"
                 :protocol ssh)
  :after org-gcal
  :custom
  (oauth2-auto-plstore (no-littering-expand-var-file-name "oauth2-auto.plist")))

pass

pass is a major mode for pass, the standard Unix password manager

(use-package pass
  :custom
  (pass-suppress-confirmations t)
  (pass-show-keybindings nil)

  :config
  (run-with-timer (* 5 60) t (lambda () (magit-extras-warn-if-repo-is-dirty paths-dir-dropbox-tlon-pass)))

  :bind
  (("A-H-o" . pass)
   :map pass-mode-map
   ("RET" . pass-edit)
   ("c" . pass-copy)
   ("D" . pass-kill)
   :map pass-view-mode-map
   ("s-p" . pass-view-toggle-password)
   ("H-q" . pass-quit)
   ("s-s" . server-edit)))

pass-extras

pass-extras collects my extensions for pass.

(use-personal-package pass-extras
  :bind
  (:map pass-mode-map
        ("SPC" . pass-extras-open-at-point)
        ("e" . pass-extras-edit)
        ("G" . pass-extras-generate-password)
        ("I" . pass-extras-insert-generated-no-symbols)))

password-store-otp

password-store-otp provides integration with the pass-otp extension for pass.

(use-package password-store-otp
  :ensure (:version (lambda (_) "0.1.5"))  ; github.com/progfolio/elpaca/issues/229
  :after pass)

auth-source-pass

auth-source-pass integrates auth-source with password-store.

(use-feature auth-source-pass
  :demand t
  :after auth-source pass
  :config
  (auth-source-pass-enable)

  :hook
  (doom-modeline-before-github-fetch-notification-hook . auth-source-pass-enable))

password-generator

password-generator [sic] generates various types of passwords.

(use-package password-generator
  :ensure (:host github
                 :repo "vandrlexay/emacs-password-genarator") ; sic
  :defer t)

version control

vc

vc provides support for various version control systems.

(use-feature vc
  :defer t
  :custom
  (vc-handled-backends '(Git))
  (vc-follow-symlinks t) ; don't ask for confirmation when opening symlinked file
  (vc-make-backup-files nil) ; do not backup version controlled files
  ;; Disable VC in Dropbox cloud storage directories.  `vc-git' runs
  ;; synchronous `git' subprocesses via `call-process', which can hang
  ;; indefinitely when Dropbox's virtual filesystem stalls on I/O
  ;; (e.g. smart sync resolving a cloud-only file).  This blocks the
  ;; main thread since there is no timeout on the read.
  (vc-ignore-dir-regexp
   (format "%s\\|%s\\|%s"
           vc-ignore-dir-regexp
           "Library/CloudStorage/Dropbox/"
           "My Drive/")))

vc-extras

vc-extras collects my extensions for vc.

(use-personal-package vc-extras
  :after vc
  :custom
  (vc-extras-split-repo t)

  :bind
  ("A-v" . vc-extras-menu))

log-edit

log-edit is a major mode for editing CVS commit messages.

(use-feature log-edit
  :defer t
  :config
  (with-eval-after-load 'savehist
    (add-to-list 'savehist-additional-variables 'log-edit-comment-ring)))

diff-mode

diff-mode is a mode for viewing and editing context diffs.

(use-feature diff-mode
  :defer t
  :bind
  (:map diff-mode-map
   ("M-o" . nil)))

ediff

ediff is a comprehensive visual interface to diff and patch.

(use-feature ediff
  :custom
  (ediff-window-setup-function 'ediff-setup-windows-plain)
  (ediff-split-window-function 'split-window-horizontally)

  :config
  (defun ediff-toggle-word-mode ()
    "Toggle between linewise and wordwise comparisons."
    (interactive)
    (setq ediff-word-mode (not ediff-word-mode))
    (message "Word mode %s"
             (if ediff-word-mode "disabled" "enabled"))
    (ediff-update-diffs))

  :bind
  (("A-d" . ediff)))

ediff-extras

ediff-extras collects my extensions for ediff.

(use-personal-package ediff-extras
  :after ediff
  :demand t)

smerge

smerge-mode is a minor mode for resolving diff3 conflicts.

(use-feature smerge-mode
  :bind
  (:map smerge-mode-map
        ("s-n" . smerge-next)
        ("s-SPC" . smerge-next)
        ("s-p" . smerge-prev)
        ("s-l" . smerge-keep-lower)
        ("s-k" . smerge-keep-upper)
        ("s-a" . smerge-keep-all)
        ("s-b" . smerge-keep-base)
        ("s-c" . smerge-keep-current)))

gh

gh is a GitHub API library for Emacs.

(use-package gh
  :ensure (:version (lambda (_) "2.29"))
  :defer t) ; github.com/progfolio/elpaca/issues/229

closql

closql stores EIEIO objects using EmacSQL.

(use-package closql
  :ensure (:host github
                 :repo "magit/closql")
  :defer t)

magit

magit is a complete text-based user interface to Git.

(use-package magit
  :ensure (:host github
                 :repo "magit/magit"
                 :branch "main"
                 :build (:not elpaca-check-version))
  :custom
  (magit-commit-ask-to-stage 'stage)
  (magit-clone-set-remote.pushDefault t)
  (magit-diff-refine-hunk 'all) ; show word-granularity differences in all diff hunks

  :config
  (with-eval-after-load 'savehist
    (add-to-list 'savehist-additional-variables 'magit-read-rev-history))

  (add-to-list 'magit-no-confirm 'stage-all-changes)

  :hook
  ((magit-status-mode-hook magit-diff-mode-hook) .
   (lambda ()
     "Disable line truncation in Magit buffers."
     (setq truncate-lines nil)))

  :bind
  (("A-g" . magit)
   ("A-M-g" . magit-clone)
   :map magit-log-mode-map
   ("k" . magit-section-backward-sibling)
   ("l" . magit-section-forward-sibling)
   :map magit-mode-map
   ("p" . magit-pull)
   ("." . magit-push)
   :map magit-diff-mode-map
   ("A-C-s-r" . magit-section-backward-sibling)
   ("A-C-s-f" . magit-section-forward-sibling)
   :map magit-hunk-section-map
   ("s-l" . magit-smerge-keep-lower)
   ("s-k" . magit-smerge-keep-upper)
   ("s-a" . magit-smerge-keep-all)
   ("s-b" . magit-smerge-keep-base)
   ("s-c" . magit-smerge-keep-current)
   :map magit-hunk-section-smerge-map
   ("s-l" . magit-smerge-keep-lower)
   ("s-k" . magit-smerge-keep-upper)
   ("s-a" . magit-smerge-keep-all)
   ("s-b" . magit-smerge-keep-base)
   ("s-c" . magit-smerge-keep-current)
   :map magit-status-mode-map
   ("s-l" . magit-smerge-keep-lower)
   ("s-k" . magit-smerge-keep-upper)
   ("s-a" . magit-smerge-keep-all)
   ("s-b" . magit-smerge-keep-base)
   ("s-c" . magit-smerge-keep-current)
   ("s-r" . tlon-commit-when-slug-at-point)
   ("s-u" . magit-remote-unshallow)
   ("A-C-s-r" . magit-section-backward-sibling)
   ("A-C-s-f" . magit-section-forward-sibling)
   :map magit-revision-mode-map
   ("A-C-s-r" . magit-section-backward-sibling)
   ("A-C-s-f" . magit-section-forward-sibling)))

magit-extra

magit-extra collects my extensions for magit.

Note that this is called magit-extra (with no ‘s’ at the end) because Magit already provides a feature called magit-extras.

(use-personal-package magit-extra
  :after magit
  :demand t
  :hook
  (git-commit-setup-hook . magit-extras-move-point-to-start)

  :bind
  ("s-p" . magit-extras-with-editor-finish-and-push))

magit-todos

magit-todos displays TODOs present in project files in the Magit status buffer.

(use-package magit-todos
  :ensure (:host github
                 :repo "alphapapa/magit-todos"
                 :build (:not elpaca-check-version))
  :after magit hl-todo
  :custom
  (magit-todos-branch-list nil)

  :config
  (magit-todos-mode))

with-editor

with-editor allows the use of Emacsclient as the $EDITOR for external programs.

(use-package with-editor
  :bind (("s-c" . with-editor-finish)
         ("s-k" . with-editor-cancel)
         ("C-c C-c" . with-editor-finish)))

ghub

ghub provides basic support for using the APIs of various Git forges from Emacs packages.

(use-package ghub
  :ensure (:host github
                 :build (:not elpaca-check-version)
                 :repo "magit/ghub"
                 :branch "main")
  :defer t
  :config
  (require 'pass))

forge

forge let’s one work with git forges directly from Magit.

(use-package forge
  :ensure (:host github
                 :repo "magit/forge"
                 :branch "main" ; github.com/progfolio/elpaca/issues/342
                 :build (:not elpaca-check-version))
  :after magit ghub emacsql auth-source-pass
  :init
  (with-eval-after-load 'magit-status
    (bind-keys :map 'magit-status-mode-map
               ("s-a" . forge-topic-set-assignees)
               ("s-d" . forge-delete-comment)
               ("s-e" . forge-edit-post)
               ("s-i" . forge-browse-issue)
               ("s-I" . forge-browse-issues)
               ("s-l" . forge-topic-set-labels)
               ("s-o" . forge-topic-status-set-done)
               ("s-p" . forge-create-post)
               ("s-r" . forge-create-post)
               ("s-t" . forge-topic-set-title)))
  (with-eval-after-load 'magit
    (bind-keys :map 'magit-mode-map
               ("n" . forge-dispatch)))
  :custom
  (forge-owned-accounts '(("benthamite")))
  (forge-topic-list-limit '(500 . -500)) ; show closed topics only via `forge-toggle-closed-visibility'
  ;; do not show inactive topics by default; keep other settings unchanged
  (forge-status-buffer-default-topic-filters
   (forge--topics-spec :type 'topic :active nil :state 'open :order 'newest))

  :config
  ;; why is this turned on by default!?
  (remove-hook 'forge-post-mode-hook 'turn-on-flyspell)

  ;; temporarily overwrite function until idiotic error message is removed
  (defun forge--ghub-massage-notification (data githost)
    (let-alist data
      (let* ((type (intern (downcase .subject.type)))
             (type (if (eq type 'pullrequest) 'pullreq type))
             (_ (unless (memq type '( discussion issue pullreq
                                      commit release checksuite)) ; Added checksuite
                  (message "Forge: Ignoring unknown notification type: %s" type))) ; Changed error to message
             (number-or-commit (and .subject.url
                                    (string-match "[^/]*\\'" .subject.url)
                                    (match-string 0 .subject.url)))
             (number (and (memq type '(discussion issue pullreq))
                          (string-to-number number-or-commit)))
             (repo   (forge-get-repository
                      (list githost
                            .repository.owner.login
                            .repository.name)
                      nil :insert!))
             (repoid (oref repo id))
             (owner  (oref repo owner))
             (name   (oref repo name))
             (id     (forge--object-id repoid (string-to-number .id)))
             (alias  (intern (concat "_" (string-replace "=" "_" id)))))
        (and number
             (list alias id
                   `((,alias repository)
                     [(name ,name)
                      (owner ,owner)]
                     ,@(cddr
                        (caddr
                         (ghub--graphql-prepare-query
                          ghub-fetch-repository
                          (pcase type
                            ('discussion `(repository
                                           discussions
                                           (discussion . ,number)))
                            ('issue      `(repository
                                           issues
                                           (issue . ,number)))
                            ('pullreq    `(repository
                                           pullRequest
                                           (pullRequest . ,number))))))))
                   repo type data)))))

  :hook
  (forge-issue-mode-hook . simple-extras-visual-line-mode-enhanced)

  :bind
  (:map forge-post-mode-map
        ("s-c" . forge-post-submit)
        :map forge-issue-mode-map
        ("s-a" . forge-topic-set-assignees)
        ("s-d" . forge-delete-comment)
        ("s-e" . forge-edit-post)
        ("s-i" . forge-browse-issue)
        ("s-I" . forge-browse-issues)
        ("s-l" . forge-topic-set-labels)
        ("s-o" . forge-topic-status-set-done)
        ("s-p" . forge-create-post)
        ("s-r" . forge-create-post)
        ("s-t" . forge-topic-set-title)
        :map forge-notifications-mode-map
        ("s-a" . forge-topic-set-assignees)
        ("s-d" . forge-delete-comment)
        ("s-e" . forge-edit-post)
        ("s-i" . forge-browse-issue)
        ("s-I" . forge-browse-issues)
        ("s-l" . forge-topic-set-labels)
        ("s-o" . forge-topic-status-set-done)
        ("s-p" . forge-create-post)
        ("s-r" . forge-create-post)
        ("s-t" . forge-topic-set-title)
        :map forge-topic-mode-map
        ("s-a" . forge-topic-set-assignees)
        ("s-d" . forge-delete-comment)
        ("s-e" . forge-edit-post)
        ("s-i" . forge-browse-issue)
        ("s-I" . forge-browse-issues)
        ("s-l" . forge-topic-set-labels)
        ("s-o" . forge-topic-status-set-done)
        ("s-p" . forge-create-post)
        ("s-r" . forge-create-post)
        ("s-t" . forge-topic-set-title)))

orgit

orgit provides support for Org links to Magit buffers.

(use-package orgit
  :ensure (:build (:not elpaca-check-version))
  :defer t)

orgit-forge

orgit-forge supports org-mode links to forge buffers.

(use-package orgit-forge
  :after org forge
  :ensure (:build (:not elpaca-check-version)))

forge-search supports searching through issues and pull requests within forge.

(use-package forge-search
  :ensure (:host github
                 :repo "benthamite/forge-search.el"
                 :branch "fix/forge-get-repository")
  :after forge
  :bind
  (:map forge-search-mode-map
        ("A-C-s-r" . magit-section-backward-sibling)
        ("A-C-s-f" . magit-section-forward-sibling)))

forge-extras

forge-extras collects my extensions for forge.

(use-personal-package forge-extras
  :after forge
  :demand t
  :init
  (with-eval-after-load 'magit-status
    (bind-keys :map 'magit-status-mode-map
               ("s-x" . forge-extras-state-set-dwim)))

  :custom
  (forge-extras-project-owner "tlon-team")
  (forge-extras-project-number 9)
  (forge-extras-project-node-id "PVT_kwDOBtGWf84A5jZf")
  (forge-extras-status-field-node-id "PVTSSF_lADOBtGWf84A5jZfzguVNY8")
  (forge-extras-estimate-field-node-id "PVTF_lADOBtGWf84A5jZfzguVNc0")
  (forge-extras-status-option-ids-alist
   '(("Doing" . "47fc9ee4")
     ("Next" . "8607328f")
     ("Later" . "13e22f63")
     ("Someday" . "4bf0f00e")
     ("Waiting" . "28097d1b")
     ("Done" . "98236657")))

  :config
  (advice-add 'orgit-store-link :override #'forge-extras-orgit-store-link)
  (advice-add 'forge-visit-this-topic :before #'forge-extras-sync-read-status)

  (run-with-idle-timer 30 t #'forge-extras-pull-notifications)

  :bind
  (:map forge-issue-mode-map
        ("A-C-s-d" . forge-previous-message)
        ("A-C-s-f" . forge-next-message)
        ("s-s" . forge-extras-set-project-status)
        ("s-w" . forge-extras-copy-message-at-point-as-kill))
  (:map forge-notifications-mode-map
        ("x" . forge-extras-browse-github-inbox)
        ("s-x" . forge-extras-state-set-dwim))
  (:map forge-topic-mode-map
        ("s-x" . forge-extras-state-set-dwim)))

emacs-pr-review

emacs-pr-review provides support for reviewing pull requests in Emacs.

See this config for ideas.

(use-package pr-review
  :after forge)

git-auto-commit-mode

git-auto-commit-mode allows for committing and pushing automatically after each save.

(use-package git-auto-commit-mode
  :after recentf
  :config
  (setq-default gac-automatically-push-p nil)
  (setq-default gac-debounce-interval 30)
  (setq-default gac-silent-message-p t)
  (setq-default gac-automatically-add-new-files-p t))

display

(setq-default line-spacing 2)

fringe

fringe controls the thin strips at the edges of windows used for indicators.

(use-feature fringe
  :config
  (setq-default fringe-indicator-alist
                '((truncation nil nil)
                  (continuation nil nil)
                  (overlay-arrow . right-triangle)
                  (up . up-arrow)
                  (down . down-arrow)
                  (top top-left-angle top-right-angle)
                  (bottom bottom-left-angle bottom-right-angle top-right-angle top-left-angle)
                  (top-bottom left-bracket right-bracket top-right-angle top-left-angle)
                  (empty-line . empty-line)
                  (unknown . question-mark))))

faces

faces provides face definition and manipulation.

(use-feature faces
  :config
  (setq ns-use-thin-smoothing t)
  ;; to prevent misalignment in vtable
  (set-face-attribute 'header-line nil :box nil))

faces-extras

faces-extras collects my extensions for faces.

(use-personal-package faces-extras
  :demand t
  :config
  (faces-extras-set-and-store-face-attributes
   '((default :family faces-extras-fixed-pitch-font :height faces-extras-fixed-pitch-size)
     (fixed-pitch :family faces-extras-fixed-pitch-font :height faces-extras-fixed-pitch-height)
     (variable-pitch :family faces-extras-variable-pitch-font :height faces-extras-variable-pitch-height)
     (window-divider :foreground (face-attribute 'mode-line-inactive :background))))

  :hook
  (init-post-init-hook . faces-extras-set-custom-face-attributes)

  :bind
  ("C-h C-f" . faces-extras-describe-face))

org-modern

org-modern prettifies org mode.

(use-package org-modern
  :after org faces-extras
  :custom
  (org-modern-table nil) ; doesn’t work well with variable-pitch: github.com/minad/org-modern/issues/99
  (org-modern-statistics nil)
  (org-modern-star 'fold)
  (org-modern-fold-stars
   '(("▸" . "▾")
     ("▸" . "▾")
     ("▸" . "▾")
     ("▸" . "▾")
     ("▸" . "▾")))
  (org-modern-replace-stars '("◉" "◉" "◉" "◉" "◉"))
  (org-modern-list
   '((42 . "○")
     (43 . "○")
     (45 . "○")))

  :config
  (faces-extras-set-and-store-face-attributes
   '((org-modern-date-active :family faces-extras-fixed-pitch-font :height faces-extras-org-date-height)
     (org-modern-date-inactive :family faces-extras-fixed-pitch-font :height faces-extras-org-date-height)
     (org-modern-tag :family faces-extras-fixed-pitch-font :height faces-extras-org-tag-height)
     (org-modern-label :family faces-extras-fixed-pitch-font :height faces-extras-org-tag-height)))

  (global-org-modern-mode))

org-modern-indent

org-modern-indent extends org-modern stylistic improvements to contexts involving indentation.

(use-package org-modern-indent
  :ensure (:host github
                 :repo "jdtsmith/org-modern-indent")
  :after org-modern
  :hook org-mode-hook
  :config
  ;; Remove the ╭│╰ bracket decoration; it renders with gaps in
  ;; variable-pitch buffers because the line height exceeds the glyph height.
  (setq org-modern-indent-begin " "
        org-modern-indent-guide " "
        org-modern-indent-end   " "))

org-indent-pixel

org-indent-pixel fixes misaligned wrapped lines in variable-pitch-mode.

(use-package org-indent-pixel
  :ensure (:host github :repo "benthamite/org-indent-pixel")
  :after org-indent
  :init
  (org-indent-pixel-setup))

org-tidy

org-tidy hides org-mode property drawers.

(use-package org-tidy
  :after org
  :custom
  (org-tidy-properties-inline-symbol "")
  (org-tidy-protect-overlay nil) ; github.com/jxq0/org-tidy/issues/11

  :hook org-mode-hook)

org-appear

org-appear toggles the visibility of hidden org mode element parts upon entering and leaving those elements.

(use-package org-appear
  :after org
  :hook org-mode-hook)

face-remap

face-remap defines simple operations for face remapping.

(use-feature face-remap
  :after eww
  :hook
  ((elfeed-show-mode-hook
    telega-webpage-mode-hook
    eww-mode-hook
    mu4e-view-mode-hook
    outline-mode-hook) . variable-pitch-mode)

  :bind
  (:map eww-mode-map
        ("+" . text-scale-increase)
        ("-" . text-scale-decrease)))

modus-themes

modus-themes are a pair of accessible white/dark themes for Emacs.

(use-package modus-themes
  :ensure (:host github
                 :repo "protesilaos/modus-themes")
  :after faces faces-extras simple-extras
  :demand t
  :custom
  (modus-themes-mixed-fonts t)

  :config
  (setopt modus-themes-common-palette-overrides
          `((fringe unspecified) ; hide the fringe
            (bg-prose-block-delimiter bg-inactive)
            (fg-prose-block-delimiter gray)
            ;; for the rest, use the predefined intense values
            ,@modus-themes-preset-overrides-intense))

  :hook
  (modus-themes-after-load-theme-hook . faces-extras-set-custom-face-attributes)
  (modus-themes-after-load-theme-hook . frame-extras-restore-window-divider))

modus-themes-extras

modus-themes-extras collects my extensions for modus-themes.

(use-personal-package modus-themes-extras
  :after modus-themes
  :demand t
  :custom
  (modus-themes-extras-light-theme 'modus-operandi-tinted)
  (modus-themes-extras-dark-theme 'modus-vivendi-tinted)

  :config
  (init-override-code
   :modus-themes-load
   '((modus-themes-extras-load-theme-conditionally)))

  :hook
  (modus-themes-after-load-theme-hook . modus-themes-extras-highlight-parentheses)
  (modus-themes-after-load-theme-hook . modus-themes-extras-set-faces)

  :bind
  ("A-u" . modus-themes-extras-toggle))

highlight-parentheses

highlight-parentheses dynamically highlights the parentheses surrounding point based on nesting-level using configurable lists of colors, background colors, and other properties.

(use-package highlight-parentheses
  :custom
  (highlight-parentheses-delay 0)

  :config
  (global-highlight-parentheses-mode)

  :hook
  (minibuffer-setup-hook . highlight-parentheses-minibuffer-setup))

spacious-padding

spacious-padding increases the spacing of frames and windows.

(use-package spacious-padding
  :ensure (:tag "0.3.0") ; using tagged version to avoid error on 2024-02-21
  :custom
  (spacious-padding-widths '())

  :config
  (spacious-padding-mode))

emoji

emoji provides commands for emoji insertion.

(use-feature emoji
  :bind
  ("H-:" . emoji-search))

color

color is a color manipulation library.

(use-feature color)

color-extras

color-extras collects my extensions for color.

Note that the loading of color cannot be deferred, since it is required by pulse. So we defer-load this package.

(use-personal-package color-extras
  :after color
  :defer 30)

rainbow-mode

rainbow-mode colorizes strings that match color names.

(use-package rainbow-mode
  :after color-extras
  :custom
  (rainbow-ansi-colors nil)
  (rainbow-x-colors nil))

ct

ct is color library meant for making changes to individual colors in various color spaces.

(use-package ct
  :after color-extras)

hsluv

hsluv is a HSLuv implementation for Emacs Lisp.

(use-package hsluv
  :after color-extras)

image

image provides image-manipulation functions.

(use-feature image
  :after image-mode
  :init
  ;; Use imagemagick, if available.
  ;; djcbsoftware.nl/code/mu/mu4e/Viewing-images-inline.html
  (when (fboundp 'imagemagick-register-types)
    (imagemagick-register-types))

  :bind
  (:map image-mode-map
        ("+" . image-increase-size)
        ("-" . image-decrease-size)))

image-mode

image-mode is a major mode for viewing images.

(use-feature image-mode
  :bind
  (:map image-mode-map
        ("c" . dired-extras-copy-image)))

paren

paren highlights matching parens.

(use-feature paren
  :custom
  (show-paren-delay 0)

  :config
  (show-paren-mode))

doom-modeline

doom-modeline is a tidier and more aesthetically pleasing modeline.

I combine the modeline with the tab bar to display various types of information. Specifically, I use the modeline to display buffer-local information (such as the buffer major mode, line number, or word count), and the tab bar to display global information (such as the time and date, the weather, the computer’s battery status, and various notifications). This functionality is provided by a combination of the =doom-modeline package, the tab-bar feature, and my corresponding extensions (doom-modeline-extras and tab-bar-extras). In short, I move to the tab bar some of the elements that would normally be displayed in the modeline by (1) enabling those elements via the relevant doom-modeline user options, (2) hiding those elements via the doom-modeline-def-modeline macro, and (3) adding equivalent elements to the tab bar via the tab-bar-format user option.

Here’s a screenshot illustrating the modeline and tab bar in action (click to enlarge):

(use-package doom-modeline
  :ensure (:build (:not elpaca-check-version))
  :demand t
  :custom
  (doom-modeline-time nil) ; we display time (and date) in the tab bar
  (doom-modeline-buffer-name t)
  ;; we display the full path in the header line via `breadcrumb'
  (doom-modeline-buffer-file-name-style 'file-name)
  (doom-modeline-check-simple-format t)
  (doom-modeline-total-line-number t)
  (doom-modeline-position-column-line-format '(" %c %l"))
  (doom-modeline-enable-word-count t)
  (doom-modeline-indent-info nil)
  (doom-modeline-github t)
  (doom-modeline-github-interval 60)
  (doom-modeline-irc nil)

  :config
  (dolist (cons  '((display-time-mode-hook . doom-modeline-override-time)
                   (doom-modeline-mode-hook . doom-modeline-override-time)))
    (remove-hook (car cons) (cdr cons))))

doom-modeline-extras

doom-modeline-extras collects my extensions for doom-modeline.

(use-personal-package doom-modeline-extras
  :after doom-modeline
  :demand t
  :config
  (doom-modeline-def-modeline 'main
    '(bar workspace-name parrot buffer-info modals matches follow remote-host buffer-position tlon-paragraph word-count selection-info org-roam-backlinks)
    '(tlon-split compilation objed-state misc-info persp-name grip irc mu4e gnus lsp minor-modes input-method indent-info buffer-encoding major-mode process vcs check time))

  (doom-modeline-def-modeline 'vcs
    '(bar window-number modals matches buffer-info remote-host buffer-position parrot selection-info)
    '(compilation misc-info irc mu4e gnus minor-modes buffer-encoding major-mode process time))

  (doom-modeline-def-modeline 'dashboard
    '(bar window-number modals buffer-default-directory-simple remote-host)
    '(compilation misc-info irc mu4e gnus minor-modes input-method major-mode process time))

  (doom-modeline-def-modeline 'project
    '(bar window-number modals buffer-default-directory remote-host buffer-position)
    '(compilation misc-info irc mu4e gnus github minor-modes input-method major-mode process time))

  (doom-modeline-mode))

tab-bar

tab-bar displays a tab bar at the top of the frame, just below the tool bar.

(use-feature tab-bar
  :after faces-extras
  :custom
  (auto-resize-tab-bars t)
  (tab-bar-format '(tab-bar-format-global))

  :config
  (setf mode-line-misc-info
        ;; When the tab-bar is active, don't show `global-mode-string'
        ;; in `mode-line-misc-info', because we now show that in the
        ;; tab-bar using `tab-bar-format-global'.
        (remove '(global-mode-string ("" global-mode-string))
                mode-line-misc-info))

  :hook
  (init-post-init-hook
   . (lambda ()
       "Set and store the tab bar attributes, then activate the tab bar."
       (faces-extras-set-and-store-face-attributes
        '((tab-bar :background (face-background 'mode-line-active)
                   :box `(:line-width 6 :color ,(face-attribute 'mode-line-active :background) :style nil))))
       (tab-bar-mode))))

tab-bar-extras

tab-bar-extras collects my extensions for tab-bar.

(use-personal-package tab-bar-extras
  :config
  (setq tab-bar-extras-global-mode-string
        `(,tab-bar-extras-prefix-element
          ,tab-bar-extras-notification-status-element
          ;; ,tab-bar-extras-time-element
          ;; ,tab-bar-extras-separator-element
          ,tab-bar-extras-emacs-profile-element
          ;; ,tab-bar-extras-separator-element
          ;; ,tab-bar-extras-battery-element
          ,tab-bar-extras-telega-element
          ,tab-bar-extras-github-element
          ,tab-bar-extras-pomodoro-element
          ,tab-bar-extras-debug-element
          ;; we add a separator at the end because `wttr' appends itself after it
          ,tab-bar-extras-separator-element))

  :hook
  (init-post-init-hook
   . (lambda ()
       "Reset the tab shortly after startup to show all its elements correctly."
       (run-with-timer 1 nil #'tab-bar-extras-quick-reset))))

breadcrumb displays project information in the header line.

(use-package breadcrumb
  :custom
  (breadcrumb-project-max-length 0.5)
  (breadcrumb-project-crumb-separator "/")
  (breadcrumb-imenu-max-length 1.0)
  (breadcrumb-imenu-crumb-separator " > ")

  :config
  (breadcrumb-mode))

battery

battery displays battery status information.

(use-feature battery
  :defer t
  :config
  (display-battery-mode))

nerd-icons

nerd-icons is a library for Nerd Font icons inside Emacs.

Note that the icons need to be installed via nerd-icons-install-fonts. If you want to install the icons with brew on macOS, run brew tap homebrew/cask-fonts && brew install --cask font-symbols-only-nerd-font.

(use-package nerd-icons
  :defer t)

menu-bar defines the menu bar.

(use-feature menu-bar
  :config
  (menu-bar-mode -1))

tool-bar

tool-bar provides the tool bar.

(use-feature tool-bar
  :config
  (tool-bar-mode -1))

scroll-bar

scroll-bar handles window scroll bars.

(use-feature scroll-bar
  :config
  (scroll-bar-mode -1))

pixel-scroll

pixel-scroll supports smooth scrolling.

(use-feature pixel-scroll
  :config
  (pixel-scroll-precision-mode))

delsel

delsel deletes the selection when the user start typing.

(use-feature delsel
  :config
  (delete-selection-mode))

hl-line

hl-line highlights the current line.

(use-feature hl-line
  :hook
  (dired-mode-hook . hl-line-mode)
  (ledger-reconcile-mode-hook . hl-line-mode))

lin

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

(use-package lin
  :custom
  (lin-face 'lin-blue)
  (lin-mode-hooks
   '(dired-mode-hook
     elfeed-search-mode-hook
     git-rebase-mode-hook
     grep-mode-hook
     ibuffer-mode-hook
     ilist-mode-hook
     ledger-report-mode-hook
     log-view-mode-hook
     magit-log-mode-hook
     mu4e-headers-mode
     occur-mode-hook
     org-agenda-mode-hook
     pdf-outline-buffer-mode-hook
     proced-mode-hook
     tabulated-list-mode-hook))

  :hook
  (init-post-init-hook . lin-global-mode))

jit-lock

jit-lock provides just-in-time fontification.

I have noticed that Emacs will sometimes fail to fontify parts of a buffer. This problem is solved, in my experience, by increasing the value of the user option jit-lock-chunk-size. Its docstring says that “The optimum value is a little over the typical number of buffer characters which fit in a typical window”, so we set its value dynamically by multiplying the number of lines per window by the number of characters per line, doubling that for safety.

(use-feature jit-lock
  :custom
  (jit-lock-chunk-size
        (* (window-max-chars-per-line) (window-body-height) 2)))

text movement

words

Keybindings for word-level movement.

(use-feature simple
  :bind
  (("A-C-s-p" . forward-word)
   ("A-C-s-u" . backward-word)))

lines

Keybindings for line-level movement.

(use-feature simple
  :commands next-line previous-line
  :init
  (with-eval-after-load 'em-hist
    (bind-keys :map eshell-hist-mode-map
               ("<up>" . previous-line)
               ("<down>" . next-line)))
  (with-eval-after-load 'cus-edit
    (bind-keys :map custom-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'ebib
    (bind-keys :map ebib-entry-mode-map
               ("k" . previous-line)
               ("l" . next-line))
    (bind-keys :map ebib-index-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'elfeed
    (bind-keys :map elfeed-show-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'elisp-refs
    (bind-keys :map elisp-refs-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'eww
    (bind-keys :map eww-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'forge-notify
    (bind-keys :map forge-notifications-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'help
    (bind-keys :map help-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'helpful
    (bind-keys :map helpful-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'info
    (bind-keys :map Info-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'johnson
    (bind-keys :map 'johnson-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'ledger-reconcile
    (bind-keys :map ledger-reconcile-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'Man
    (bind-keys :map Man-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'mu4e
    (bind-keys :map mu4e-view-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'org-lint
    (bind-keys :map org-lint--report-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'osa-chrome
    (bind-keys :map osa-chrome-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'pass
    (bind-keys :map pass-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'simple
    (bind-keys :map special-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'slack
    (bind-keys :map slack-message-buffer-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'slack
    (bind-keys :map slack-activity-feed-buffer-mode-map
               ("k" . previous-line)
               ("l" . next-line)))
  (with-eval-after-load 'wasabi
    (bind-keys :map wasabi-mode-map
               ("k" . previous-line)
               ("l" . next-line)))

  :bind
  (("A-C-s-m" . move-beginning-of-line)
   ("A-C-s-/" . move-end-of-line)))

sentences

Keybindings for sentence-level movement.

(use-feature emacs
  :bind
  (("A-C-s-i" . backward-sentence)
   ("A-C-s-o" . forward-sentence)))

paragraphs

Keybindings for paragraph-level movement.

(use-feature emacs
  :bind
  (("A-C-s-," . backward-paragraph)
   ("A-C-s-." . forward-paragraph)))

sexps

Keybindings for sexp-level movement.

(use-feature emacs
  :bind
  (("A-C-s-e" . backward-sexp)
   ("A-H-M-s-d" . forward-sexp) ; nonstandard binding because otherwise intercepted by OSX
   ))

defuns

Keybindings for defun-level movement.

(use-feature emacs
  :bind
  (("A-C-s-w" . beginning-of-defun)
   ("A-C-s-s" . end-of-defun)))

buffers

Keybindings for buffer-level movement.

(use-feature simple
  :bind
  (("A-C-s-SPC" . beginning-of-buffer)
   ("A-C-s-<tab>" . end-of-buffer)))

text manipulation

simple

simple configures the kill ring, transposing, and text manipulation commands.

(use-feature simple
  :custom
  (kill-ring-max 99999)
  (save-interprogram-paste-before-kill t) ; add system clipboard to kill ring
  (auto-save-interval 5)

  :bind
  (("A-H-M-d" . transpose-chars)
   ("A-H-M-e" . transpose-sentences)
   ("A-H-M-f" . transpose-sexps)
   ("A-H-M-r" . transpose-words)
   ("A-H-M-v" . transpose-lines)
   ("C-k" . nil)
   ("C-<delete>" . nil)
   ("C-H-M-=" . overwrite-mode)
   ("C-H-M-a" . backward-kill-sexp)
   ("C-H-M-d" . delete-forward-char)
   ("C-H-M-e" . kill-sentence)
   ("C-H-M-f" . kill-sexp)
   ("C-H-M-f" . zap-to-char)
   ("C-H-M-g" . append-next-kill)
   ("C-H-M-q" . backward-kill-word)
   ("C-H-M-r" . kill-word)
   ("C-H-M-s" . delete-backward-char)
   ("M-SPC" . cycle-spacing)
   ("C-H-M-v" . kill-line)
   ("C-H-M-w" . backward-kill-sentence)
   ("C-H-M-z" . crux-kill-line-backwards)
   ("C-M-<backspace>" . nil)
   ("C-M-k" . nil)
   ("H-v" . yank)
   ("M-DEL" . nil)
   ("s-C" . nil)))

simple-extras

simple-extras collects my extensions for simple.

(use-personal-package simple-extras
  :demand t
  :bind
  (("A-C-H-a" . simple-extras-copy-whole-sexp)
   ("A-C-H-f" . simple-extras-delete-whole-sexp)
   ("A-C-H-M-S-s-a" . simple-extras-backward-delete-sexp)
   ("A-C-H-M-S-s-a" . simple-extras-backward-zap-delete-to-char)
   ("A-C-H-M-S-s-e" . simple-extras-delete-sentence)
   ("A-C-H-M-S-s-f" . simple-extras-delete-sexp)
   ("A-C-H-M-S-s-f" . simple-extras-zap-delete-to-char)
   ("A-C-H-M-S-s-q" . simple-extras-backward-delete-word)
   ("A-C-H-M-S-s-r" . simple-extras-delete-word)
   ("A-C-H-M-S-s-v" . simple-extras-delete-line)
   ("A-C-H-M-S-s-w" . simple-extras-backward-delete-sentence)
   ("A-C-H-M-S-s-z" . simple-extras-backward-delete-line)
   ("A-H-c" . simple-extras-count-words-dwim)
   ("A-C-H-e" . simple-extras-delete-whole-sentence)
   ("A-C-H-i" . simple-extras-kill-whole-sentence)
   ("A-C-H-m" . simple-extras-kill-whole-line)
   ("A-C-H-r" . simple-extras-delete-whole-word)
   ("A-C-H-u" . simple-extras-kill-whole-word)
   ("A-C-H-v" . simple-extras-delete-whole-line)
   ("A-C-H-w" . simple-extras-copy-whole-sentence)
   ("A-C-H-z" . simple-extras-copy-whole-line)
   ("A-H-M-a" . simple-extras-transpose-sexps-backward)
   ("A-H-M-q" . simple-extras-transpose-words-backward)
   ("A-H-M-s" . simple-extras-transpose-chars-backward)
   ("A-H-M-s-9" . simple-extras-copy-whole-word) ; `.-q'
   ("A-H-M-w" . simple-extras-transpose-sentences-backward)
   ("A-H-M-z" . simple-extras-transpose-lines-backward)
   ("A-M-f" . simple-extras-fill-or-unfill-paragraph)
   ("C-g" . simple-extras-keyboard-quit-dwim)
   ("C-H-M-a" . simple-extras-backward-zap-to-char)
   ("C-H-M-b" . simple-extras-strip-thing-at-point)
   ("C-H-M-s-A-a" . simple-extras-backward-copy-sexp)
   ("C-H-M-s-A-a" . simple-extras-backward-zap-copy-to-char)
   ("C-H-M-s-A-e" . simple-extras-copy-sentence)
   ("C-H-M-s-A-f" . simple-extras-copy-sexp)
   ("C-H-M-s-A-f" . simple-extras-zap-copy-to-char)
   ("C-H-M-s-A-q" . simple-extras-backward-copy-word)
   ("C-H-M-s-A-r" . simple-extras-copy-word)
   ("C-H-M-s-A-v" . simple-extras-copy-line)
   ("C-H-M-s-A-w" . simple-extras-backward-copy-sentence)
   ("C-H-M-s-A-z" . simple-extras-backward-copy-line)
   ("C-v" . simple-extras-paste-no-properties)
   ("C-w" . simple-extras-narrow-or-widen-dwim)
   ("H-A-v" . simple-extras-yank-and-pop)
   ("H-c" . simple-extras-smart-copy-region)
   ("H-M"  . simple-extras-exchange-point-and-mark)
   ("H-X" . simple-extras-smart-delete-region)
   ("H-x" . simple-extras-smart-kill-region)
   ("M-A-i" . simple-extras-visual-line-mode-enhanced)
   ("M-i" . simple-extras-indent-dwim)
   ("M-q" . simple-extras-keyboard-quit-dwim)
   ("M-v" . simple-extras-visible-mode-enhanced)
   :map isearch-mode-map
   ("C-w" . simple-extras-narrow-or-widen-dwim)))

paragraphs

paragraphs configures paragraph manipulation, including sentence-end and paragraph keybindings.

(use-feature emacs
  :demand t
  :commands kill-paragraph transpose-paragraphs backward-kill-paragraph
  :custom
  (sentence-end-double-space nil)

  :init
  (with-eval-after-load 'text-mode
    (bind-keys :map text-mode-map
               ("C-H-M-c" . kill-paragraph)
               ("C-H-M-x" . backward-kill-paragraph)
               ("A-C-H-M-S-s-c" . simple-extras-delete-paragraph)
               ("A-C-H-M-S-s-x" . simple-extras-backward-delete-paragraph)
               ("C-H-M-s-A-c" . simple-extras-copy-paragraph)
               ("C-H-M-s-A-x" . simple-extras-backward-copy-paragraph)
               ("A-C-H-c" . simple-extras-delete-whole-paragraph)
               ("A-C-H-x" . simple-extras-copy-whole-paragraph)
               ("A-C-H-," . simple-extras-kill-whole-paragraph)
               ("A-H-M-c" . transpose-paragraphs)
               ("A-H-M-x" . simple-extras-transpose-paragraphs-backward)))
  (with-eval-after-load 'org
    (bind-keys :map org-mode-map
               ("C-H-M-c" . kill-paragraph)
               ("C-H-M-x" . backward-kill-paragraph)
               ("A-C-H-M-S-s-c" . simple-extras-delete-paragraph)
               ("A-C-H-M-S-s-x" . simple-extras-backward-delete-paragraph)
               ("C-H-M-s-A-c" . simple-extras-copy-paragraph)
               ("C-H-M-s-A-x" . simple-extras-backward-copy-paragraph)
               ("A-C-H-c" . simple-extras-delete-whole-paragraph)
               ("A-C-H-x" . simple-extras-copy-whole-paragraph)
               ("A-C-H-," . simple-extras-kill-whole-paragraph)
               ("A-H-M-c" . transpose-paragraphs)
               ("A-H-M-x" . simple-extras-transpose-paragraphs-backward)))
  (with-eval-after-load 'outline
    (bind-keys :map outline-mode-map
               ("C-H-M-c" . kill-paragraph)
               ("C-H-M-x" . backward-kill-paragraph)
               ("A-C-H-M-S-s-c" . simple-extras-delete-paragraph)
               ("A-C-H-M-S-s-x" . simple-extras-backward-delete-paragraph)
               ("C-H-M-s-A-c" . simple-extras-copy-paragraph)
               ("C-H-M-s-A-x" . simple-extras-backward-copy-paragraph)
               ("A-C-H-c" . simple-extras-delete-whole-paragraph)
               ("A-C-H-x" . simple-extras-copy-whole-paragraph)
               ("A-C-H-," . simple-extras-kill-whole-paragraph)
               ("A-H-M-c" . transpose-paragraphs)
               ("A-H-M-x" . simple-extras-transpose-paragraphs-backward)))
  (with-eval-after-load 'telega
    (bind-keys :map telega-chat-mode-map
               ("C-H-M-c" . kill-paragraph)
               ("C-H-M-x" . backward-kill-paragraph)
               ("A-C-H-M-S-s-c" . simple-extras-delete-paragraph)
               ("A-C-H-M-S-s-x" . simple-extras-backward-delete-paragraph)
               ("C-H-M-s-A-c" . simple-extras-copy-paragraph)
               ("C-H-M-s-A-x" . simple-extras-backward-copy-paragraph)
               ("A-C-H-c" . simple-extras-delete-whole-paragraph)
               ("A-C-H-x" . simple-extras-copy-whole-paragraph)
               ("A-C-H-," . simple-extras-kill-whole-paragraph)
               ("A-H-M-c" . transpose-paragraphs)
               ("A-H-M-x" . simple-extras-transpose-paragraphs-backward))))

editing

simple

simple configures general editing behavior, including selection, indentation, and case conversion.

(use-feature simple
  :custom
  (shift-select-mode nil) ; Shift keys do not activate the mark momentarily.
  ;; hide commands in M-x which do not apply to the current mode.
  (read-extended-command-predicate #'command-completion-default-include-p)
  (eval-expression-print-level nil)
  (eval-expression-print-length nil)
  (print-level nil)
  (print-length nil)
  (truncate-partial-width-windows nil)
  (tab-always-indent 'complete)

  :config
  (setq-default fill-column 80)

  :init
  (column-number-mode)

  :bind
  (("C-e" . eval-last-sexp)
   ("H-C" . kill-ring-save)
   ("H-m" . set-mark-command)
   ("H-Z" . undo-redo)
   ("M-o" . downcase-dwim)
   ("M-u" . capitalize-dwim)
   ("A-M-u" . upcase-dwim)
   ("M-w" . count-words-region)
   ("H-z" . undo-only)))

rect

rect provides commands for operating on rectangular regions of text.

(use-feature rect
  :bind ("C-x r w" . copy-rectangle-as-kill))

repeat

repeat provides commands for repeating the previous command.

(use-feature repeat
  :bind
  (("M-r" . repeat)
   ("A-M-r" . repeat-complex-command)))

view

view provides a minor mode for viewing files without editing them.

(use-feature view
  :bind
  ("M-A-v" . view-mode))

sort

sort provides commands for sorting text in a buffer.

(use-feature sort
  :custom
  (sort-fold-case t)

  :bind
  ("C-t" . sort-lines))

vundo

vundo displays the undo history as a tree.

(use-package vundo
  :custom
  (undo-limit (* 100 1000 1000))
  (undo-strong-limit undo-limit)
  (undo-outer-limit undo-limit)

  :bind
  (("A-z" . vundo)
   :map vundo-mode-map
   ("j" . vundo-backward)
   (";" . vundo-forward)
   ("k" . vundo-previous)
   ("l" . vundo-next)))

outline

outline provides selective display of portions of a buffer.

(use-feature outline
  :hook
  (prog-mode-hook . outline-minor-mode)

  :bind
  (:map outline-mode-map
        ("TAB" . outline-cycle)
        ("<backtab>" . outline-cycle-buffer)
        ("A-C-s-r" . outline-previous-heading)
        ("A-C-s-f" . outline-next-heading)
        ("C-H-M-s-a" . outline-extras-promote-heading)
        ("C-H-M-s-s" . outline-move-subtree-up)
        ("C-H-M-s-d" . outline-move-subtree-down)
        ("C-H-M-s-f" . outline-extras-demote-heading)
        ("C-H-M-s-q" . outline-promote)
        ("C-H-M-s-r" . outline-demote)
        :map outline-minor-mode-map
        ("TAB" . outline-cycle)
        ("<backtab>" . outline-cycle-buffer)
        ("A-C-s-r" . outline-previous-heading)
        ("A-C-s-f" . outline-next-heading)
        ("C-H-M-s-a" . outline-extras-promote-heading)
        ("C-H-M-s-s" . outline-move-subtree-up)
        ("C-H-M-s-d" . outline-move-subtree-down)
        ("C-H-M-s-f" . outline-extras-demote-heading)
        ("C-H-M-s-q" . outline-promote)
        ("C-H-M-s-r" . outline-demote)))

outline-extras

outline-extras collects my extensions for outline.

(use-personal-package outline-extras
  :after outline)

outli

outli is a simple comment-based outliner for Emacs.

(use-package outli
  :ensure (:host github
                 :repo "jdtsmith/outli")
  :after outline
  :custom
  (outli-speed-commands
   '(("Outline navigation")
     ("k" . outline-previous-visible-heading)
     ("." . outline-forward-same-level)
     ("," . outline-backward-same-level)
     ("l" . outline-next-visible-heading)
     ("m" . outline-up-heading)
     ("j" . consult-imenu)
     ("Outline structure editing")
     ("q" . outline-promote)
     ("a" . outline-extras-promote-heading)
     ("d" . outline-move-subtree-down)
     ("s" . outline-move-subtree-up)
     ("f" . outline-extras-demote-heading)
     ("r" . outline-demote)
     ("Outline visibility")
     ("<tab>" . outline-cycle)
     ("C" . outline-cycle-buffer)
     ("w" . outli-toggle-narrow-to-subtree)
     ("Regular editing")
     ("z" . undo-only)
     ("v" . yank)
     ("Other")
     ("?" . outli-speed-command-help)))

  :hook emacs-lisp-mode-hook)

thingatpt

thingatpt gets the “thing” at point.

(use-feature thingatpt
  :init
  ;; we redefine `thing-at-point-url-path-regexp' to support Japanese URLs
  ;; *after* `goto-addr' is loaded, so that `goto-address-url-regexp', which is
  ;; defined in reference to that user option, inherits the redefinition
  (with-eval-after-load 'goto-addr
    (setq thing-at-point-url-path-regexp "[^]\t\n \"'<>[^`{}、。!?]*[^]\t\n \"'<>[^`{}.,;、。!?]+")))

abbrev

abbrev provides automatic expansion of abbreviations as you type.

(use-feature abbrev
  :custom
  (save-abbrevs 'silently)
  (abbrev-file-name (file-name-concat paths-dir-abbrev "abbrev_defs"))

  :config
  (setq-default abbrev-mode t)
  ;; do not look up abbrevs with case folding; e.g. `EA' will not expand an `ea' abbrev
  (abbrev-table-put global-abbrev-table :case-fixed t)
  (abbrev-table-put text-mode-abbrev-table :case-fixed t))

abbrev-extras

abbrev-extras collects my extensions for abbrev.

(use-personal-package abbrev-extras
  :after abbrev)

yasnippet

yasnippet is a template system for Emacs.

(use-package yasnippet
  :custom
  (yas-snippet-dirs (list paths-dir-yasnippets
                          paths-dir-yasnippets-private
                          (file-name-concat elpaca-builds-directory "yasnippet-snippets/snippets/")))
  (yas-triggers-in-field t) ; allow stacked expansions
  (yas-new-snippet-default
   (format "# -*- mode: snippet -*-\n# name: $1\n# key: $2\n# contributor: %s\n# --\n$0" user-full-name))

  :config
  ;; Dropbox's file provider can leave snippet files as online-only
  ;; placeholders, causing `file-error' "Operation canceled" when
  ;; yasnippet tries to read them.  Catch the error so that loading
  ;; one unavailable file does not prevent the mode from activating.
  (advice-add 'yas--load-directory-2 :around
              (lambda (fn &rest args)
                (condition-case err
                    (apply fn args)
                  (file-error
                   (message "yasnippet: skipping unreadable directory %s: %s"
                            (car args) (error-message-string err))))))

  (yas-global-mode)

  :hook
  (minibuffer-setup-hook . yas-minor-mode)

  :bind
  ("C-y" . yas-new-snippet))

yasnippet-snippets

yasnippet-snippets is a public repository of yasnippet snippets.

(use-package yasnippet-snippets
  :after yasnippet)

expand-region

expand-region incrementally selects regions by semantic units.

(use-package expand-region
  :bind
  (("C-H-s-n" . er/expand-region)
   ("C-H-s-h" . er/contract-region)))

newcomment

newcomment provides commands for commenting and uncommenting code.

(use-feature newcomment
  :bind
  ("M-/" . comment-line))

skeleton

skeleton provides a concise language extension for writing structured statement skeleton insertion commands for programming modes.

The code block below specifies how certain characters should be paired either globally or in specific modes.

(use-feature skeleton
  :init
  (setq skeleton-pair t)
  (with-eval-after-load 'telega
    (bind-keys :map telega-chat-mode-map
               ("~" . skeleton-pair-insert-maybe)
               ("=" . skeleton-pair-insert-maybe)))
  (with-eval-after-load 'markdown-mode
    (bind-keys :map markdown-mode-map
               ("*" . skeleton-pair-insert-maybe)
               ("`" . skeleton-pair-insert-maybe)))
  (with-eval-after-load 'forge
    (bind-keys  :map forge-post-mode-map
                ("*" . skeleton-pair-insert-maybe)
                ("`" . skeleton-pair-insert-maybe)))

  :hook
  ((markdown-mode-hook forge-post-mode-hook) .
   (lambda ()
     "Use two backticks, rather than ` and ', in selected modes."
     (setq-local skeleton-pair-alist '((?` _ ?`)))))

  :bind
  (("[" . skeleton-pair-insert-maybe)
   ("{" . skeleton-pair-insert-maybe)
   ("(" . skeleton-pair-insert-maybe)
   ("\"" . skeleton-pair-insert-maybe)
   ("«" . skeleton-pair-insert-maybe)
   :map org-mode-map
   ("~" . skeleton-pair-insert-maybe)
   ("=" . skeleton-pair-insert-maybe)
   :map emacs-lisp-mode-map
   ("`" . skeleton-pair-insert-maybe)
   :map lisp-interaction-mode-map
   ("`" . skeleton-pair-insert-maybe)))

crux

crux is a “collection of ridiculously useful extensions”.

(use-package crux
  :init
  (defun crux-smart-open-line-before ()
    "Insert an empty line before the current line."
    (interactive)
    (crux-smart-open-line t))

  :bind
  (("M-l" . crux-smart-open-line)
   ("M-A-l" . crux-smart-open-line-before)
   ("A-H-l" . crux-duplicate-current-line-or-region)))

button

button defines functions for inserting and manipulating clickable buttons in Emacs buffers.

(use-feature button
  :after telega
  :bind
  (("A-C-M-s-j" . backward-button)
   ("A-C-M-s-;" . forward-button)
   :map telega-chat-mode-map
   ("M-RET" . push-button)))

back-button

back-button supports navigating the mark ring forward and backward.

(use-package back-button
  :config
  (back-button-mode)

  :bind
  (("H-," . back-button-local-backward)
   ("H-." . back-button-local-forward)
   ("H-<" . back-button-global-backward)
   ("H->" . back-button-global-forward)))

goto-last-change

goto-last-change moves point through buffer-undo-list positions.

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

goto-addr

goto-addr activates URLs and e-mail addresses in buffers.

(use-feature goto-addr
  :config
  (global-goto-address-mode))

registers & bookmarks

register

register saves text, rectangles, positions, and other things for later use.

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

register-extras

register-extras collects my extensions for register.

(use-personal-package register-extras
  :bind
  ("C-r" . register-extras-dispatch))

bookmark

bookmark provides a system for recording and jumping to named positions in files.

(use-feature bookmark
  :defer t
  :custom
  (bookmark-default-file paths-file-bookmarks) ; Set location of bookmarks file
  (bookmark-save-flag 1)) ; Save bookmarks after each entry

files & buffers

files

files provides core commands for visiting, saving, and managing files.

(use-feature files
  :after savehist
  :custom
  (confirm-kill-processes nil) ; do not prompt to kill running processes when quitting Emacs
  (delete-by-moving-to-trash t)
  (trash-directory (expand-file-name (file-name-concat "~" ".Trash"))) ; fallback for `move-file-to-trash'
  (remote-file-name-inhibit-delete-by-moving-to-trash t)
  (remote-file-name-inhibit-auto-save t)
  (find-file-visit-truename t); emacs.stackexchange.com/questions/14509/kill-process-buffer-without-confirmation
  (create-lockfiles nil) ; lockfiles are indexed by `org-roam', which causes problems with `org-agenda'
  (large-file-warning-threshold (* 200 1000 1000))
  (enable-local-variables :all)
  (insert-directory-program "/opt/homebrew/bin/gls") ; use coreutils to avoid 'listing directory failed' error
  (auto-save-no-message t)
  (delete-old-versions t)
  (make-backup-files nil)
  (version-control 'never)
  (auto-save-visited-interval 1)
  (require-final-newline t)
  (revert-without-query '(".*"))

  :config
  (setq kill-buffer-query-functions nil) ; not a customizable variable
  ;; we enable `auto-save-visited-mode' globally...
  (auto-save-visited-mode)
  ;; ...but then we disable it for all buffers
  (setq-default auto-save-visited-mode nil)
  ;; so that we can then re-enable it for specific buffers via a file-local variable:
  ;; # Local Variables:
  ;; # eval: (setq-local auto-save-visited-mode t)
  ;; # End:

  (advice-add 'recover-session
              :after (lambda ()
                       "Disable `dired-hide-details-mode' to show dates in `recover-session'."
                       (dired-hide-details-mode -1)))

  (with-eval-after-load 'savehist
    (add-to-list 'savehist-additional-variables 'file-name-history))

  (add-to-list 'auto-mode-alist '("\\.mdx\\'" . markdown-mode))

  (add-to-list 'safe-local-eval-forms
               '(add-hook 'after-save-hook
                          (lambda ()
                            (require 'ox-texinfo)
                            (let ((inhibit-message t))
                              (org-texinfo-export-to-texinfo)))
                          nil t))

  :bind
  (("M--" . not-modified)
   ("H-a" . mark-whole-buffer)
   ("H-s" . save-buffer)
   ("C-b" . clone-indirect-buffer-other-window)
   ("H-C-g" . abort-recursive-edit)
   ("H-C-S-g" . top-level)
   ("H-C-A-g" . keyboard-escape-quit))) ; ESC ESC ESC

files-extras

files-extras collects my extensions for files.

(use-personal-package files-extras
  :bind
  (("M-;" . files-extras-copy-current-path)
   ("H-f" . files-extras-dispatch)
   ("M-b" . files-extras-save-and-revert-buffer)
   ("M-e" . files-extras-eval-region-or-buffer)
   ("H-q" . files-extras-kill-this-buffer)
   ("A-H-M-s-q" . files-extras-kill-this-buffer-switch-to-other-window)
   ("A-H-q" . files-extras-kill-other-buffer)
   ("H-n" . files-extras-new-empty-buffer)
   ("A-H-n" . files-extras-new-buffer-in-current-mode)
   ("H-S" . files-extras-save-all-buffers)
   ("A-H-M-s-SPC" . files-extras-switch-to-alternate-buffer)
   ("A-H-v" . files-extras-internet-archive-dwim))

  :init
  (with-eval-after-load 'Info
    (bind-keys :map Info-mode-map
               ("q" . files-extras-kill-this-buffer)))
  (with-eval-after-load 'apropos
    (bind-keys :map apropos-mode-map
               ("q" . files-extras-kill-this-buffer)))
  (with-eval-after-load 'calendar
    (bind-keys :map calendar-mode-map
               ("q" . files-extras-kill-this-buffer)))
  (with-eval-after-load 'completion-list
    (bind-keys :map completion-list-mode-map
               ("q" . files-extras-kill-this-buffer)))
  (with-eval-after-load 'dired
    (bind-keys :map dired-mode-map
               ("q" . files-extras-kill-this-buffer)
               ("s-o" . files-extras-ocr-pdf)))
  (with-eval-after-load 'ebib
    (bind-keys :map ebib-entry-mode-map
               ("v" . files-extras-internet-archive-dwim)
               ("q" . files-extras-bury-buffer-switch-to-other-window)
               :map ebib-index-mode-map
               ("q" . files-extras-bury-buffer-switch-to-other-window)))
  (with-eval-after-load 'elfeed
    (bind-keys :map elfeed-show-mode-map
               ("q" . files-extras-kill-this-buffer)))
  (with-eval-after-load 'finder
    (bind-keys :map finder-mode-map
               ("q" . files-extras-kill-this-buffer)))
  ;; We typically enter these modes to lookup some information and
  ;; then return to the previous buffer, so we set `q' to switch to
  ;; the other window, and reserve `Q' for the normal behavior
  (with-eval-after-load 'help
    (bind-keys :map help-mode-map
               ("Q" . files-extras-kill-this-buffer)
               ("q" . files-extras-kill-this-buffer-switch-to-other-window)))
  (with-eval-after-load 'helpful
    (bind-keys :map helpful-mode-map
               ("Q" . files-extras-kill-this-buffer)
               ("q" . files-extras-kill-this-buffer-switch-to-other-window)))
  (with-eval-after-load 'ledger-reconcile
    (bind-keys :map ledger-reconcile-mode-map
               ("q" . files-extras-kill-this-buffer)))
  (with-eval-after-load 'markdown-mode
    (bind-keys :map markdown-mode-map
               ("s-g" . files-extras-grammarly-open-in-external-editor)
               :map gfm-mode-map
               ("s-g" . files-extras-grammarly-open-in-external-editor)))
  (with-eval-after-load 'mu4e
    (bind-keys :map mu4e-headers-mode-map
               ("q" . files-extras-kill-this-buffer)))
  (with-eval-after-load 'pass
    (bind-keys :map pass-mode-map
               ("q" . files-extras-kill-this-buffer)))
  (with-eval-after-load 'pdf-view
    (bind-keys :map pdf-view-mode-map
               ("s-o" . files-extras-ocr-pdf)))
  (with-eval-after-load 'simple
    (bind-keys :map messages-buffer-mode-map
               ("q" . files-extras-bury-buffer-switch-to-other-window)))
  (with-eval-after-load 'slack
    (bind-keys :map slack-activity-feed-buffer-mode-map
               ("q" . files-extras-kill-this-buffer))
    (bind-keys :map slack-message-buffer-mode-map
               ("q" . files-extras-kill-this-buffer))
    (bind-keys :map slack-thread-message-buffer-mode-map
               ("q" . files-extras-kill-this-buffer)))
  (with-eval-after-load 'special
    (bind-keys :map special-mode-map
               ("q" . files-extras-kill-this-buffer)))
  (with-eval-after-load 'telega
    (bind-keys :map telega-root-mode-map
               ("q" . files-extras-bury-buffer-switch-to-other-window)))
  (with-eval-after-load 'tetris
    (bind-keys :map tetris-mode-map
               ("q" . files-extras-kill-this-buffer)))
  (with-eval-after-load 'view
    (bind-keys :map view-mode-map
               ("q" . files-extras-kill-this-buffer))))

locate

locate provides an interface for finding files using a locate database.

(use-feature locate
  :after consult
  :custom
  (locate-command "mdfind")) ; use the OSX Spotlight backend

autorevert

autorevert automatically reverts buffers when their files change on disk.

(use-feature autorevert
  :custom
  (auto-revert-use-notify nil) ; reddit.com/r/emacs/comments/mq2znn/comment/gugo0n4/
  (auto-revert-verbose nil)

  :hook
  (find-file-hook . global-auto-revert-mode))

dired

dired is the Emacs directory editor.

(use-feature dired
  :init
  (with-eval-after-load 'pdf-annot
    (bind-keys
     :map pdf-annot-minor-mode-map
     ("x" . dired-jump)))

  :custom
  (dired-listing-switches "-AGFhlv --group-directories-first --time-style=long-iso")
  (dired-auto-revert-buffer t)
  (dired-recursive-copies 'always)
  (dired-recursive-deletes 'always)
  (dired-no-confirm t) ; never ask for confirmation
  (dired-dwim-target t) ; if Dired buffer in other window, use that buffer's current directory as target
  (dired-vc-rename-file t)
  (dired-do-revert-buffer t)
  (dired-create-destination-dirs 'ask)
  (dired-guess-shell-alist-user '(("" "open")))

  :config
  (setq dired-deletion-confirmer (lambda (x) t)) ; not a customizable variable
  (put 'dired-find-alternate-file 'disabled nil) ; do not disable dired-find-alternate-file!

  :hook
  (dired-mode-hook . dired-hide-details-mode)
  (dired-mode-hook . (lambda () (visual-line-mode -1)))

  :bind
  (:map dired-mode-map
        ("<tab>" . dired-extras-subtree-toggle)
        (";" . dired-do-rename)
        ("." . dired-find-alternate-file)
        ("C" . dired-do-copy)
        ("s-s" . dired-isearch-filenames)
        ("J" . dired-jump-other-window)
        ("e" . browse-url-extras-of-dired-file-externally)
        ("f" . avy-extras-dired-find-file)
        ("k" . dired-previous-line)
        ("l" . dired-next-line)
        ("r" . dired-toggle-read-only)
        ("H-z" . dired-undo)
        ("A-C-s-r" . dired-prev-dirline)
        ("A-C-s-f" . dired-next-dirline)
        ("A-C-s-," . dired-prev-marked-file)
        ("A-C-s-." . dired-next-marked-file)
        ("C-o" . nil)))

dired-x

dired-x provides extra Dired functionality.

(use-feature dired-x
  :custom
  (dired-omit-verbose nil) ; shut up
  (dired-omit-size-limit nil)  ; always omit, regardless of directory size

  :config
  (setopt dired-omit-files
   (concat dired-omit-files "\\|^.localized$\\|^\\.DS_Store$\\|^\\.pdf-view-restore\\|^Icon\\\015"))

  :hook
  (dired-mode-hook . dired-omit-mode))

dired-extras

dired-extras collects my extensions for dired.

(use-personal-package dired-extras
  :hook
  (dired-mode-hook . (lambda () (require 'dired-extras)))

  :bind
  (("H-d" . dired-extras-dispatch)
   :map  dired-mode-map
   ("," . dired-extras-up-directory-reuse)
   ("-" . dired-extras-hide-details-mode-enhanced)
   ("H-." . dired-extras-dotfiles-toggle)
   ("c" . dired-extras-copy-filename-as-kill-absolute)
   ("w" . dired-extras-copy-filename-as-kill-dwim)
   ("W" . dired-extras-copy-filename-as-kill-sans-extension)
   ("z" . dired-extras-mark-screenshots)
   ("s" . dired-extras-sort-toggle-dwim)
   ("s-d" . dired-extras-do-delete-fast)
   ("s-r" . dired-extras-copy-to-remote-docs-directory)))

dired-aux

dired-aux provides auxiliary Dired functions for file operations like compression and diffing.

(use-feature dired-aux
  :after dired-x
  :config
  ;; with `unar' installed, `Z' uncompresses `rar' files
  (push '("\\.rar\\'" "" "unar") dired-compress-file-suffixes))

dired-git-info

dired-git-info displays information about Git files in Dired.

(use-package dired-git-info
  :after dired
  :custom
  (dgi-commit-message-format "%s	%cr	%an")

  :bind
  (:map dired-mode-map
        ("b" . dired-git-info-mode)))

dired-du

dired-du displays the recursive size of directories in Dired.

(use-package dired-du
  :after dired
  :custom
  (dired-du-size-format 'comma)

  :bind
  (:map dired-mode-map
        ("'" . dired-du-mode)))

image-dired

image-dired provides image viewing and thumbnail management in Dired.

(use-feature image-dired
  :after dired
  :custom
  (image-dired-main-image-directory (expand-file-name "~/Pictures/"))
  (image-dired-external-viewer "open")

  :bind
  (:map image-dired-thumbnail-mode-map
        ("c" . dired-extras-image-copy-filename-as-kill-absolute)
        ("e" . image-dired-thumbnail-display-external)
        ("k" . image-dired-display-previous)
        ("l" . image-dired-display-next)
        :map dired-mode-map
        ("I" . dired-extras-image-dired-current-directory)))

nerd-icons-dired

nerd-icons-dired adds Dired support to nerd-icons.

(use-package nerd-icons-dired
  :after dired
  :hook
  (dired-mode-hook . nerd-icons-dired-mode))

wdired

wdired makes Dired buffers editable for renaming files and changing permissions.

(use-feature wdired
  :custom
  (wdired-allow-to-change-permissions t)

  :bind
  (:map wdired-mode-map
   ("s-c" . wdired-finish-edit)
   ("<return>" . wdired-finish-edit)))

gnus-dired

gnus-dired provides utility functions for intersections of gnus and dired.

I use mu4e (see below) rather than gnus to handle email. However, a specific functionality in this feature also works with mu4e, allowing me to attach a file to an email directly from a Dired buffer.

(use-feature gnus-dired
  :after dired
  :custom
  ;; enable `mu4e' attachments from `dired'
  ;; djcbsoftware.nl/code/mu/mu4e/Dired.html
  (gnus-dired-mail-mode 'mu4e-user-agent)

  :hook
  (dired-mode-hook . turn-on-gnus-dired-mode)

  :bind
  (:map dired-mode-map
        ("s-a" . gnus-dired-attach)))

dired-hacks

dired-hacks is a collection of useful dired additions.

(use-package dired-hacks
  :ensure (:host github
                    :repo "Fuco1/dired-hacks")
  :after dired
  :init
  (advice-add 'dired-subtree-toggle :after (lambda () (dired-omit-mode) (dired-omit-mode)))
  (advice-add 'dired-subtree-cycle :after (lambda () (dired-omit-mode) (dired-omit-mode)))

  :bind
  (:map dired-mode-map
   ("C-w" . dired-narrow-regexp)
   ("<tab>" . dired-subtree-toggle)
   ("<backtab>" . dired-subtree-cycle)))

dired-quick-sort

dired-quick-sort provides persistent quick sorting of Dired buffers in various ways.

(use-package dired-quick-sort
  :after dired
  :bind
  (:map dired-mode-map
        ("T" . dired-quick-sort-transient)))

peep-dired

peep-dired supports browsing file contents in other window while browsing directory in dired.

(use-package peep-dired
  :after dired
  :bind
  (:map dired-mode-map
        ("F" . peep-dired)))

minibuffer

minibuffer provides minibuffer completion and input facilities.

(use-feature minibuffer
  :custom
  (enable-recursive-minibuffers t)
  (resize-mini-windows t)
  (completion-cycle-threshold 3) ; TAB cycle if there are only few candidates
  (minibuffer-default-prompt-format " [%s]")
  (minibuffer-electric-default-mode)

  :bind
  (:map minibuffer-mode-map
        ("M-k" . previous-history-element)
        ("M-l" . next-history-element)
        ("C-e" . search-query-replace)
        ("TAB" . yas-maybe-expand)
        ("s-i" . org-roam-node-insert)
        ("M-n" . nil)
        ("M-p" . nil)))

ibuffer

ibuffer provides an advanced, interactive buffer list.

(use-feature ibuffer
  :bind
  (:map ibuffer-mode-map
   ("k" . ibuffer-do-delete)))

prot-scratch

[[https://github.com/protesilaos/dotfiles/blob/master/emacs.emacs.d/prot-lisp/prot-scratch.el][prot-scratch]] supports scratch buffers for an editable major mode of choice._

(use-package prot-scratch
  :ensure (:host github
                 :repo "protesilaos/dotfiles"
                 :local-repo "prot-scratch"
                 :main "emacs/.emacs.d/prot-lisp/prot-scratch.el"
                 :build (:not elpaca-check-version)
                 :files ("emacs/.emacs.d/prot-lisp/prot-scratch.el"))
  :custom
  (prot-scratch-default-mode 'org-mode)

  :bind
  ("C-n" . prot-scratch-buffer))

persistent-scratch

persistent-scratch preserves the scratch buffer across Emacs sessions.

I use this package in combination with prot-scratch (see above) to persist scratch buffers in different modes. This way, I am able to open a scratch buffer in any mode for temporary notes, without running the risk of losing them.

(use-package persistent-scratch
  :custom
  (persistent-scratch-autosave-interval 10)
  (persistent-scratch-backup-directory
   (no-littering-expand-var-file-name "auto-save/scratch-buffers/"))

  :config
  (persistent-scratch-setup-default))

executable

executable provides base functionality for executable interpreter scripts.

(use-feature executable
  :hook
  ;; masteringemacs.org/article/script-files-executable-automatically
  (after-save-hook . executable-make-buffer-file-executable-if-script-p))

uniquify

uniquify provides unique buffer names.

(use-feature uniquify
  :custom
  (uniquify-buffer-name-style 'forward))

reveal-in-osx-finder

reveal-in-osx-finder lets you open the file at point or the current file-visiting buffer in OS X Finder.

(use-package reveal-in-osx-finder
  :after dired
  :demand t
  :bind (:map dired-mode-map
              ("/" . reveal-in-osx-finder)))

tramp

tramp is a remote file editing package for Emacs.

(use-feature tramp
  :after dired-x
  :custom
  (tramp-auto-save-directory no-littering-var-directory)
  ;; Disable version control on tramp buffers to avoid freezes.
  (vc-ignore-dir-regexp
   (format "\\(%s\\)\\|\\(%s\\)"
           vc-ignore-dir-regexp
           tramp-file-name-regexp))

  ;; Don't clean up recentf tramp buffers.
  (recentf-auto-cleanup 'never)

  ;; This is supposedly [[https://www.emacswiki.org/emacs/TrampMode][faster than the default]], `scp'.
  (tramp-default-method "sshx")

  ;; Store TRAMP auto-save files locally.
  (tramp-auto-save-directory paths-dir-emacs-var)

  ;; A more representative name for this file.
  (tramp-persistency-file-name (file-name-concat tramp-auto-save-directory "tramp-connection-history"))

  ;; Cache SSH passwords during the whole Emacs session.
  (password-cache-expiry nil)

  ;; emacs.stackexchange.com/a/37855/32089
  (remote-file-name-inhibit-cache nil)

  :config
  ;; Reuse SSH connections. Taken from the TRAMP FAQ.
  (customize-set-variable 'tramp-ssh-controlmaster-options
                          (concat
                           "-o ControlPath=/tmp/ssh-tramp-%%r@%%h:%%p "
                           "-o ControlMaster=auto -o ControlPersist=yes"))

  ;; This will put in effect PATH changes in the remote ~/.profile.
  (add-to-list 'tramp-remote-path 'tramp-own-remote-path)

  (advice-add 'projectile-project-root
              :around (lambda ()
                        "Ignore remote files."
                        (unless (file-remote-p default-directory 'no-identification)
                          (apply orig-fun args))))

  (add-to-list 'tramp-connection-properties
               (list (regexp-quote "/ssh:fede@tlon.team:")
                     "direct-async-process" t)))

pandoc-mode

pandoc-mode is a minor mode for interacting with Pandoc.

(use-package pandoc-mode
  :defer t)

track-changes

track-changes provides an API to react to buffer modifications.

I load this explicitly because the version bundled with Emacs 30.2 (1.2) has a known bug (debbugs#73041), where track-changes–before-beg/end can become inconsistent with the buffer size, triggering a cl-assertion-failed error on buffer modifications. The GNU ELPA version fixes this bug.

(use-package track-changes
  ;; The default GNU ELPA recipe clones the full emacs-mirror/emacs repo
  ;; and uses a :files spec matching that layout.  Use emacs-straight's
  ;; lightweight mirror instead, with a flat :files glob.
  :ensure (:repo "https://github.com/emacs-straight/track-changes.git"
           :files ("*" (:exclude ".git")))
  :demand t)

windows & frames

window

window provides commands for displaying buffers, scrolling, and managing window layout.

(use-feature window
  :init
  (with-eval-after-load 'ebib
    (bind-keys :map ebib-index-mode-map
               ("H-q" . bury-buffer)
               :map ebib-entry-mode-map
               ("H-q" . bury-buffer)))
  (with-eval-after-load 'simple
    (bind-keys :map messages-buffer-mode-map
               ("H-q" . bury-buffer)))
  (with-eval-after-load 'telega
    (bind-keys :map 'telega-root-mode-map
               ("H-q" . bury-buffer)))
  (with-eval-after-load 'elfeed
    (bind-keys :map elfeed-show-mode-map
               ("y" . scroll-down-command)
               ("h" . scroll-up-command)))
  (with-eval-after-load 'helpful
    (bind-keys :map helpful-mode-map
               ("y" . scroll-down-command)
               ("h" . scroll-up-command)))
  (with-eval-after-load 'mu4e
    (bind-keys :map mu4e-view-mode-map
               ("y" . scroll-down-command)
               ("h" . scroll-up-command)))
  (with-eval-after-load 'telega
    (bind-keys :map telega-msg-button-map
               ("y" . scroll-down-command)
               ("h" . scroll-up-command)))

  :custom
  (split-height-threshold nil)
  ;; move point to top of buffer if `scroll-down-command' invoked when screen can scroll no further
  (scroll-error-top-bottom t)
  (split-width-threshold 200)

  :config
  ;; we add `*ocr-pdf' buffer to list of buffers not to be displayed,
  ;; so that the process runs in the background`
  (push '("*ocr-pdf*" display-buffer-no-window) display-buffer-alist)

  ;; The following prevents Emacs from splitting windows indefinitely when the monitor config changes
  ;; stackoverflow.com/questions/23207958/how-to-prevent-emacs-dired-from-splitting-frame-into-more-than-two-windows
  (add-to-list 'display-buffer-alist `(,shell-command-buffer-name-async display-buffer-no-window))

  (init-override-code
   :window-split
   '((add-hook 'elpaca-after-init-hook #'window-extras-split-if-unsplit)))

  :bind
  (("H-w" . delete-window)
   ("A-C-s-y" . scroll-down-command)
   ("A-C-s-h" . scroll-up-command)
   ("A-C-s-g" . scroll-other-window)
   ("A-C-s-t" . scroll-other-window-down)))

window-extras

window-extras collects my extensions for window.

(use-personal-package window-extras
  :bind
  (("C-H-0" . window-extras-switch-to-last-window)
   ("A-C-H-0" . window-extras-switch-to-minibuffer-window)
   ("M-," . window-extras-buffer-move-left)
   ("M-." . window-extras-buffer-move-right)
   ("A-M--" . window-extras-buffer-swap) ; `emacs-mac'
   ("A-M-–" . window-extras-buffer-swap))) ; `emacs-plus'

frame

frame provides multi-frame management independent of window systems.

(use-feature frame
  :demand t
  :custom
  (window-divider-default-right-width 1)

  :config
  (blink-cursor-mode)
  (window-divider-mode)

  :bind
  (("H-M-<tab>" . other-frame) ; M-S-TAB
   ("M-N" . make-frame)
   ("M-W" . delete-frame)))

frame-extras

frame-extras collects my extensions for frame.

(use-personal-package frame-extras
  :hook
  (elpaca-after-init-hook . frame-extras-maximize-frame)
  (spacious-padding-mode-hook . frame-extras-restore-window-divider)

  :bind
  ;; the key bindings below are triggered via Karabiner
  (("C-H-I" . frame-extras-maximize-frame)
   ("C-H-U" . frame-extras-left-half)
   ("C-H-P" . frame-extras-right-half)))

posframe

posframe supports displaying small popup frames.

(use-package posframe)

winum

winum-mode supports navigation of windows and frames using numbers.

(use-package winum
  :custom
  (winum-scope 'frame-local)

  :config
  (winum-mode)

  :bind
  (("<C-m>" . winum-select-window-1)
   ("C-," . winum-select-window-2)
   ("C-." . winum-select-window-3)
   ("C-/" . winum-select-window-4)))

winner

winner-mode is a global minor mode that records the changes in the window configuration (i.e., how the frames are partitioned into windows), so that you can undo them.

(use-feature winner
  :config
  (remove-hook 'minibuffer-setup-hook 'winner-save-unconditionally)
  (winner-mode)

  :bind
  ("H-W" . winner-undo))

popper

popper is a minor-mode to summon and dismiss buffers easily.

(use-package popper
  :init
  (setopt popper-reference-buffers
        '("\\*Warnings\\*"
          "Output\\*$"
          help-mode
          helpful-mode
          compilation-mode))
  (popper-mode)
  (popper-echo-mode)

  :custom
  (popper-display-control 'user) ; assumes buffer-specific behavior customized via `display-buffer-alist'
  (popper-echo-dispatch-keys '(?a ?s ?d ?f ?j ?l ?r ?q ?w ?e ?r ?u ?i ?o ?p ?z ?x ?c ?v ?m ?, ?. ?/ ? ))
  (popper-window-height (lambda (window)
                          "Set WINDOW to a size up to 33% of the frame height."
                          (fit-window-to-buffer
                           window
                           (floor (frame-height) 3))))

  :bind
  (("C-o" . popper-toggle)
   ("A-C-o" . popper-toggle-type)
   ("C-H-o" . popper-cycle)))

avy

avy lets you jump to any visible text using a char-based decision tree.

(use-package avy
  :init
  (with-eval-after-load 'ebib
    (bind-keys :map ebib-entry-mode-map
               ("f" . avy-goto-line)))
  (with-eval-after-load 'isearch
    (bind-keys :map isearch-mode-map
               ("M-f" . avy-isearch)))

  :custom
  (avy-case-fold-search nil)
  (avy-timeout-seconds 0.2)
  (avy-all-windows nil)
  (avy-keys (append '(?k) popper-echo-dispatch-keys))

  :config
  (setf (alist-get ?r avy-dispatch-alist) 'avy-extras-action-mark-to-char)

  :bind
  (("C-H-s-m" . avy-goto-line-above)
   ("C-H-s-." . avy-goto-line-below)
   ("C-H-s-k" . avy-goto-word-1-above)
   ("C-H-s-l" . avy-goto-word-1-below)))

avy-extras

avy-extras collects my extensions for avy.

(use-personal-package avy-extras
  :bind
  (("C-H-s-u" . avy-extras-goto-word-in-line-behind)
   ("C-H-s-p" . avy-extras-goto-word-in-line-ahead)
   ("C-H-s-," . avy-extras-goto-end-of-line-above)
   ("C-H-s-/" . avy-extras-goto-end-of-line-below)
   ("C-H-s-j" . avy-extras-goto-char-backward)
   ("C-H-s-;" . avy-extras-goto-char-forward)))

writeroom-mode

writeroom-mode provides distraction-free writing for Emacs.

(use-package writeroom-mode
  :custom
  (writeroom-global-effects '(writeroom-set-fullscreen
                              writeroom-set-alpha
                              writeroom-set-menu-bar-lines
                              writeroom-set-tool-bar-lines
                              writeroom-set-vertical-scroll-bars
                              writeroom-set-bottom-divider-width
                              (lambda (arg) (tab-bar-mode (* -1 arg)))))
  (writeroom-restore-window-config t) ; upon leaving `writeroom mode', restore pre-existing number of windows
  (writeroom-major-modes '(org-mode
                           elfeed-search-mode
                           elfeed-show-mode
                           eww-mode
                           eww-buffers-mode)) ; major modes activated in global-writeroom-mode
  (writeroom-fullscreen-effect 'maximized) ; disables annoying fullscreen transition effect on macos
  (writeroom-maximize-window t)

  :config
  (advice-add 'writeroom-mode
              :before (lambda (&rest args)
                        "Set `writeroom-width' to the width of the window in which it is invoked."
                        (setopt writeroom-width (window-total-width))))

  :bind
  ("H-u" . writeroom-mode))

logos

logos provides a simple focus mode for reading, writing, and presentation.

(use-package logos
  :demand t
  :config
  ;; Treat outline headings (org *, elisp ;;;, markdown #) as pages
  (setq logos-outlines-are-pages t)

  ;; Aesthetic tweaks for `logos-focus-mode' (all buffer-local)
  (setq-default logos-hide-cursor nil
                logos-hide-mode-line t
                logos-hide-header-line t
                logos-hide-buffer-boundaries t
                logos-hide-fringe t
                logos-variable-pitch nil
                logos-buffer-read-only nil
                logos-scroll-lock nil
                logos-olivetti nil)

  ;; Recenter at top on page motion so each "slide" starts at the top
  (defun ps/logos-recenter-top ()
    "Use `recenter' to reposition the view at the top."
    (unless (derived-mode-p 'prog-mode)
      (recenter 0)))
  (add-hook 'logos-page-motion-hook #'ps/logos-recenter-top)

  :bind
  (([remap narrow-to-region] . logos-narrow-dwim)
   ([remap forward-page] . logos-forward-page-dwim)
   ([remap backward-page] . logos-backward-page-dwim)
   ("<f9>" . logos-focus-mode)))

ace-link lets you quickly follow links in Emacs, Vimium-style.

(use-package ace-link
  :defer t)

ace-link-extras collects my extensions for ace-link.

(use-personal-package ace-link-extras
  :after ace-link
  :config
  (dolist (mode '(slack-message-buffer-mode
                  slack-thread-message-buffer-mode
                  slack-activity-feed-buffer-mode))
    (push (list 'ace-link-extras-slack mode) ace-link-major-mode-actions)))

date & time

calendar

calendar provides a collection of calendar-related functions.

(use-feature calendar
  :custom
  (calendar-week-start-day 1)    ; week starts on Monday
  (calendar-set-date-style 'iso) ; this isn't the default?
  (calendar-time-display-form
   '(24-hours ":" minutes
              (when time-zone
                (concat " (" time-zone ")"))))
  (calendar-mark-holidays-flag nil)
  (calendar-time-zone-style 'numeric)
  (holiday-bahai-holidays nil)

  :bind
  (("C-d" . calendar)
   ("s-=" . "C-u A-s-=")
   :map calendar-mode-map
   ("H-m" . calendar-set-mark)
   ("A-C-s-u" . calendar-backward-day)
   ("A-C-s-i" . calendar-backward-week)
   ("A-C-s-o" . calendar-forward-week)
   ("A-C-s-p" . calendar-forward-day)
   ("A-C-s-m" . calendar-backward-month)
   ("A-C-s-," . calendar-backward-year)
   ("A-C-s-." . calendar-forward-year)
   ("A-C-s-/" . calendar-forward-month)
   ("C-f" . nil)
   ("C-b" . nil)
   ("C-n" . nil)
   ("C-p" . nil)
   ("=" . calendar-count-days-region)))

calendar-extras

calendar-extras collects my extensions for calendar.

(use-personal-package calendar-extras
  :after org-agenda
  :custom
  (calendar-extras-location-name "Buenos Aires")
  (calendar-extras-use-geolocation t))

holidays

holidays provides holiday functions for calendar.

(use-feature holidays
  :after org-agenda
  :config
  (dolist (holiday '((holiday-float 6 0 3 "Father's Day")
                     (holiday-float 5 0 2 "Mother's Day")))
    (delete holiday holiday-general-holidays)))

institution-calendar

institution-calendar augments the calendar buffer to include Oxford/Calendar term indicators.

(use-package institution-calendar
  :ensure (:host github
                 :repo "protesilaos/institution-calendar")
  :defer t
  :config
  (institution-calendar-mode))

org-gcal

org-gcal integrates org-mode with Google Calendar.

(That’s the actively maintained fork; the official repository is no longer maintained.)

(use-package org-gcal
  :ensure (:host github
                 :repo "benthamite/org-gcal.el"
                 :branch "fix/strip-html-descriptions"
                 :build (:not elpaca-check-version)) ; https://github.com/kidd/org-gcal.el/pull/276
  :after auth-source-pass org-agenda
  :custom
  (org-gcal-client-id (auth-source-pass-get "host" "auth-sources/org-gcal"))
  (org-gcal-client-secret (auth-source-pass-get 'secret "auth-sources/org-gcal"))
  (org-gcal-fetch-file-alist `((,(getenv "PERSONAL_GMAIL") . ,paths-file-calendar)
                               (,(getenv "EPOCH_EMAIL") . ,paths-file-calendar)))
  (org-gcal-recurring-events-mode 'top-level)
  (org-gcal-remove-api-cancelled-events nil) ; never remove cancelled events
  (org-gcal-notify-p nil)
  (org-gcal-up-days 1)
  (org-gcal-down-days 7)
  (org-gcal-auto-archive nil)

  :config
  ;; see the relevant section in this config file for more details on how to set
  ;; up `org-gcal' with asymmetric encryption
  (require 'plstore))

org-gcal-extras

org-gcal-extras collects my extensions for org-gcal.

(use-personal-package org-gcal-extras
  :after org-gcal
  :demand t)

calfw

calf is a calendar framework for Emacs.

The original package is no longer maintained. A fork by Abdul-Lateef Haji-Ali below added a few improvements. But that fork itself ceased to be maintained so I am now using my own fork.

(use-package calfw
  ;; :ensure (:host github
                 ;; :repo "benthamite/emacs-calfw")
  :after org-agenda)

calfw-org

calfw-org display org-agenda items in the calfw buffer.

(use-package calfw-org
  :after calfw org)

calfw-blocks

calfw-blocks provides visual enhancements for calfw.

The original package appears to no longer be maintained, so I have created my own fork.

(use-package calfw-blocks
  :ensure (calfw-blocks
             :host github
             :repo "benthamite/calfw-blocks")
  :after calfw)

time

time provides facilities to display the current date and time, and a new-mail indicator mode line.

(use-feature time
  :demand t
  :custom
  (zoneinfo-style-world-list '(("America/Buenos_Aires" "Buenos Aires")
                               ("Europe/London" "London")
                               ("Europe/Madrid" "Madrid")
                               ("America/New_York" "New York")
                               ("America/Los_Angeles" "San Francisco")
                               ("Europe/Stockholm" "Stockholm")))
  (world-clock-list t)
  (world-clock-time-format "%R %z (%Z)  %A %d %B")
  (world-clock-buffer-name "*world-clock*")
  (world-clock-timer-enable t)
  (world-clock-timer-second 60)

  (display-time-interval 1)
  (display-time-format "%a %e %b %T %z")
  (display-time-default-load-average nil)

  :config
  (display-time-mode)

  :bind
  ("M-A-t" . world-clock))

timer-list

timer-list lists active timers in a tabulated buffer.

(use-feature timer-list
  :config
  ;; disable warning
  (put 'list-timers 'disabled nil))

tmr

tmr set timers using a convenient notation.

(use-package tmr
  :defer t
  :config
  (when (eq system-type 'darwin)
    (setopt tmr-sound-file "/System/Library/Sounds/Blow.aiff")))

display-wttr

display-wttr displays weather information in the mode line.

(use-package display-wttr
  :disabled ; triggering lots of errors
  :after calendar-extras
  :custom
  (display-wttr-interval (* 15 60))
  (display-wttr-locations `(,calendar-extras-location-name))

  :config
  (display-wttr-mode))

history

savehist

savehist makes Emacs remember completion history across sessions.

(use-feature savehist
  :custom
  (history-length t) ; unlimited history
  (savehist-save-minibuffer-history t)

  :config
  (savehist-mode))

simple

simple registers additional variables for persistence across sessions via savehist.

(use-feature simple
  :defer t
  :config
  (with-eval-after-load 'savehist
    (dolist (var '(command-history
                   extended-command-history
                   kill-ring
                   mark-ring
                   shell-command-history
                   read-expression-history))
      (add-to-list 'savehist-additional-variables var))))

saveplace

saveplace makes Emacs remember point position in file across sessions.

(use-feature saveplace
  :config
  (save-place-mode))

session

session lets you use variables, registers and buffer places across sessions.

(use-package session
  :custom
  (session-globals-include '((kill-ring 100)
                             (session-file-alist 100 t)
                             (file-name-history 100)
                             search-ring regexp-search-ring))

  :hook
  (elpaca-after-init-hook . session-initialize))

recentf

recentf makes Emacs remember the most recently visited files.

(use-feature recentf
  :custom
  (recentf-max-saved-items 100)

  :config
  ;; github.com/emacscollective/no-littering#suggested-settings
  (add-to-list 'recentf-exclude no-littering-var-directory)
  (add-to-list 'recentf-exclude no-littering-etc-directory)

  :hook find-file-hook)

search & replace

elgrep

elgrep is an Emacs implementation of grep that requires no external dependencies.

(use-package elgrep
  :bind
  (:map elgrep-mode-map
        ("r" . elgrep-edit-mode)
        ("s-c" . elgrep-save)))

isearch

isearch provides incremental search.

(use-feature isearch
  :custom
  (search-default-mode #'char-fold-to-regexp)
  (isearch-yank-on-move t)
  (isearch-lazy-count t)
  (lazy-count-prefix-format nil)
  (lazy-count-suffix-format " (%s/%s)")
  (isearch-allow-scroll 'unlimited)
  (search-upper-case t)
  (search-exit-option t) ; `t' is the default, but some alternative value may be more sensible

  :config
  (with-eval-after-load 'savehist
    (dolist (var '(regexp-search-ring search-ring))
      (add-to-list 'savehist-additional-variables var)))

  :hook
  (isearch-mode-end-hook . recenter-top-bottom)

  :bind
  (:map isearch-mode-map
        ("C-H-M-s" . isearch-delete-char)
        ("C-H-M-d" . "C-- C-H-M-s") ; delete forward char
        ("C-g" . isearch-abort) ; "quit once"
        ("C-H-g" . isearch-exit) ; "quit twice"
        ("C-. " . isearch-toggle-char-fold)
        ("C-," . isearch-forward-symbol-at-point)
        ("C-." . isearch-forward-thing-at-point)
        ("C-/" . isearch-complete)
        ("H-m" . isearch-toggle-lax-whitespace)
        ("C-a" . isearch-toggle-regexp)
        ("C-b" . isearch-beginning-of-buffer)
        ("C-d" . isearch-toggle-word)
        ("C-f" . isearch-highlight-lines-matching-regexp)
        ("C-i" . isearch-toggle-invisible)
        ("C-l" . isearch-yank-line)
        ("C-m" . isearch-toggle-symbol)
        ("C-n" . isearch-end-of-buffer)
        ("C-o" . isearch-occur)
        ("C-p" . isearch-highlight-regexp)
        ("C-v" . isearch-yank-kill)
        ("C-y" . isearch-forward-symbol-at-point)
        ("M-k" . isearch-ring-retreat)
        ("M-l" . isearch-ring-advance)
        ("C-e" . isearch-query-replace)))

To check: Bridging Islands in Emacs: re-builder and query-replace-regexp | Karthinks

isearch-extras

isearch-extras collects my extensions for isearch.

(use-personal-package isearch-extras
  :config
  (advice-add 'isearch-mode :around #'isearch-extras-use-selection)

  :bind
  (:map isearch-mode-map
        ("C-<return>" . isearch-extras-exit-other-end)
        ("H-c" . isearch-extras-copy-match)
        ("C-p" . isearch-extras-project-search)
        ("C-l" . isearch-extras-consult-line)
        ("C-H-v" . isearch-extras-yank-kill-literally)))

replace

replace provides search-and-replace commands.

(use-feature replace
  :custom
  ;; emacs.stackexchange.com/a/12318/32089
  (query-replace-from-history-variable 'regexp-search-ring)
  (case-replace nil)

  :bind
  (("C-H-a" . query-replace)
   ("C-H-s" . query-replace-regexp)))

substitute

substitute efficiently replaces targets in the buffer or context.

(use-package substitute
  :ensure (:host github
                 :repo "protesilaos/substitute")
  :hook
  (substitute-post-replace-functions . substitute-report-operation)

  :bind
  (("A-H-b" . substitute-target-in-buffer)
   :map prog-mode-map
   ("A-H-d" . substitute-target-in-defun)))

imenu

imenu is a framework for mode-specific buffer indexes.

(use-feature imenu
  :defer t
  :custom
  (org-imenu-depth 3))

pcre2el

pcre2el supports conversion between PCRE, Emacs and rx regexp syntax.

(use-package pcre2el
  :defer t)

wgrep

wgrep lets you create a writable grep buffer and apply the changes to files.

(use-package wgrep
  :custom
  (wgrep-auto-save-buffer t)
  (wgrep-enable-key "r")

  :bind
  (:map wgrep-mode-map
   ("s-c" . wgrep-finish-edit)))

minibuffer completion

packagewhat it does
verticominibuffer completion UI
consultminibuffer completion backend
orderlessminibuffer completion styles
marginaliaminibuffer completion annotations
embarkminibuffer completion actions

For an introduction to minibuffer completion, I recommend this video by Protesilaos Stavrou. For a comprehensive overview of both minibuffer completion and completion at point, I recommend this video by Andrew Tropin.

bindings

bindings defines standard key bindings and some variables.

(use-feature bindings
  :bind
  ("<C-i>" . complete-symbol))

vertico

vertico is a vertical completion UI based on the default completion system.

(use-package vertico
  :ensure (:files (:defaults "extensions/*")
                  :includes (vertico-indexed
                             vertico-flat
                             vertico-grid
                             vertico-mouse
                             vertico-quick
                             vertico-buffer
                             vertico-repeat
                             vertico-reverse
                             vertico-directory
                             vertico-multiform
                             vertico-unobtrusive))
  :init
  (vertico-mode)

  :custom
  (vertico-multiform-commands
   '((consult-line buffer)
     (consult-imenu buffer)
     (consult-grep buffer)
     (isearch-extras-consult-line buffer)))
  (vertico-multiform-categories
   '((grid)))
  (vertico-cycle t)
  (vertico-count 16)

  :config
  (vertico-multiform-mode)

  :hook
  ;; youtu.be/L_4pLN0gXGI?t=779
  (rfn-eshadow-update-overlay-hook . vertico-directory-tidy)

  :bind
  (:map vertico-map
        ("<C-i>" . vertico-exit)
        ("M-f" . vertico-quick-exit)
        ("C-k" . vertico-previous-group)
        ("C-l" . vertico-next-group)
        ("C-H-M-w" . vertico-directory-up)))

embark

embark provides contextually relevant actions in completion menus and in normal buffers.

(use-package embark
  :custom
  (embark-confirm-act-all nil)

  :config
  (defvar-keymap embark-yasnippet-completion-actions
    :doc "Keymap for actions on yasnippet completions."
    :parent embark-general-map
    "d" #'consult-yasnippet-visit-snippet-file)
  (add-to-list 'embark-keymap-alist '(yasnippet . embark-yasnippet-completion-actions))

  (keymap-set embark-general-map "?" #'gptel-quick)
  (keymap-set embark-defun-map "R" #'gptel-extras-rewrite-defun)

  :bind
  (("C-;" . embark-act)
   ("C-H-;" . embark-act-all)
   ("C-h B" . embark-bindings)
   :map embark-general-map
   ("DEL" . nil)
   ("D" . delete-region)
   ("f" . helpful-symbol)
   :map embark-file-map
   ("D" . delete-region)
   :map embark-general-map
   ("I" . embark-insert)
   :map embark-identifier-map
   ("i" . citar-extras-open-in-ebib)
   :map embark-file-map
   ("H-c" . files-extras-copy-contents)))

consult

consult provides practical commands based on the Emacs completion function completing-read.

(use-package consult
  :init
  (with-eval-after-load 'helpful
    (bind-keys :map helpful-mode-map
               ("s-j" . consult-outline)))
  (with-eval-after-load 'markdown-mode
    (bind-keys :map markdown-mode-map
               ("s-j" . consult-outline)))
  (with-eval-after-load 'gfm-mode
    (bind-keys :map gfm-mode-map
               ("s-j" . consult-outline)))
  (with-eval-after-load 'outline
    (bind-keys :map outline-mode-map
               ("s-j" . consult-outline)))

  :custom
  ;; we call this wrapper to silence the annoying two lines of debug info that
  ;; `mdfind' outputs, which show briefly in the echo area and pollute the
  ;; `consult' search field. the file is in the `bin' directory of this repo.
  (consult-locate-args "mdfind-wrapper")
  (consult-narrow-key "<")
  (consult-widen-key ">")
  (consult-grep-max-columns nil)

  :config
  (setopt consult-ripgrep-args (concat consult-ripgrep-args " --hidden")) ; include hidden files

  :bind
  (("C-H-l" . consult-line)
   ("C-f" . consult-find)
   ("s-j" . consult-imenu)
   ("H-b" . consult-buffer)
   ("H-B" . consult-project-buffer)
   ("A-H-i" . consult-info)
   ("H-R" . consult-history)
   ("H-V" . consult-yank-pop)))

consult-extras

consult-extras collects my extensions for consult.

(use-personal-package consult-extras
  :bind
  (("H-F" . consult-extras-locate-file-current)
   ("H-k" . consult-extras-locate-current)
   ("H-p" . consult-extras-ripgrep-current)))

consult-dir

consult-dir enables insertion of paths into the minibuffer prompt.

(use-package consult-dir
  :after consult
  :custom
  (consult-dir-default-command 'consult-dir-dired)

  :bind
  (:map minibuffer-mode-map
        ("H-d" . consult-dir)))

consult-git-log-grep

consult-git-log-grep provides an interactive way to search the git log using consult.

(use-package consult-git-log-grep
  :after consult
  :defer t)

consult-yasnippet

consult-yasnippet provides consult functionality to yasnippet.

(use-package consult-yasnippet
  :config
  ;; we delay previews to avoid accidentally triggering snippets that execute elisp code
  (consult-customize consult-yasnippet :preview-key nil)

  (add-to-list 'vertico-multiform-commands
               '(consult-yasnippet grid))

  :bind
  ("C-H-y" . consult-yasnippet))

embark-consult

embark-consult provides integration between embark and consult.

(use-package embark-consult
  :after embark consult)

marginalia

marginalia displays annotations (such as docstrings) next to completion candidates.

(use-package marginalia
  :init
  (marginalia-mode))

orderless

orderless is an completion style that matches multiple regexps in any order.

(use-package orderless
  :custom
  (completion-styles '(orderless basic partial-completion))
  (completion-category-overrides '((file (styles basic partial-completion))))
  (orderless-matching-styles '(orderless-regexp)))

orderless-extras

orderless-extras collects my extensions for orderless.

I define the following style dispatchers to extend orderless functionality:

SuffixMatching StyleExampleDescription
~orderless-flexabc~Flex/fuzzy matching
,orderless-initialismabc,Match initials (e.g., “abc” → “a-b-c”)
;orderless-prefixesabc;Match word prefixes
!orderless-without-literal!abcExclude matches containing pattern
(use-personal-package orderless-extras
  :after orderless
  :custom
  (orderless-style-dispatchers '(orderless-extras-flex-dispatcher
                                 orderless-extras-initialism-dispatcher
                                 orderless-extras-prefixes-dispatcher
                                 orderless-extras-exclusion-dispatcher)))

affe

affe is an Asynchronous Fuzzy Finder for Emacs.

(use-package affe
  :custom
  ;; https://github.com/minad/affe?tab=readme-ov-file#installation-and-configuration
  (affe-regexp-compiler #'affe-orderless-regexp-compiler)
  (affe-count 100)

  :config
  (defun affe-orderless-regexp-compiler (input _type _ignorecase)
    (setq input (cdr (orderless-compile input)))
    (cons input (apply-partially #'orderless--highlight input t)))

  :bind
  (("H-P" . affe-grep)
   ("H-K" . affe-find)))

nerd-icons-completion

nerd-icons-completion displays nerd icons in completion candidates.

(use-package nerd-icons-completion
  :after marginalia
  :config
  (nerd-icons-completion-mode)

  :hook
  (marginalia-mode-hook . nerd-icons-completion-marginalia-setup))

ido

ido is a completion package for Emacs.

(use-feature ido
  :after dired
  :config
  (with-eval-after-load 'savehist
    (add-to-list 'savehist-additional-variables 'ido-file-history))

  :bind
  (:map dired-mode-map
        ("i" . ido-find-file)))

which-key

which-key displays available keybindings in a popup.

(use-feature which-key
  :custom
  (which-key-idle-delay 0)

  :config
  (which-key-mode))

completion at point

packagewhat it does
corfucompletion at point UI
capecompletion at point backend

corfu

corfu enhances completion at point with a small completion popup.

(use-package corfu
  :ensure (:files (:defaults "extensions/*")
                  :includes (corfu-info
                             corfu-echo
                             corfu-history
                             corfu-popupinfo
                             corfu-quick))
  :after faces-extras
  :custom
  (corfu-auto t)                 ;; Enable auto completion
  (corfu-quit-no-match t)        ;; Automatically quit if there is no match
  (corfu-cycle vertico-cycle)
  (corfu-count vertico-count)
  (corfu-auto-prefix 3)
  (corfu-auto-delay 0.5)
  (corfu-popupinfo-delay 0.1)

  :config
  (faces-extras-set-and-store-face-attributes
   '((corfu-default :family faces-extras-fixed-pitch-font :height faces-extras-fixed-pitch-size)))

  (global-corfu-mode)

  (with-eval-after-load 'savehist
    (add-to-list 'corfu-history 'savehist-additional-variables))

  :hook
  (prog-mode-hook . corfu-popupinfo-mode)
  (prog-mode-hook . corfu-echo-mode)
  (corfu-mode-hook . corfu-history-mode)

  :bind
  (:map corfu-map
        ("M-f" . corfu-quick-complete)
        ("TAB" . nil)
        ("<tab>" . nil)
        ("<return>" . corfu-complete)
        ("RET" . corfu-complete)))

corfu-extras

corfu-extras collects my extensions for corfu.

(use-personal-package corfu-extras
  :hook
  (minibuffer-setup-hook . corfu-extras-enable-always-in-minibuffer)

  :bind
  (:map corfu-map
        ("M-m" . corfu-extras-move-to-minibuffer)))

cape

cape provides completion-at-point extensions

(use-package cape
  :after corfu
  :custom
  (cape-dabbrev-min-length 4)

  :config
  (defun cape-enable-completions ()
    "Enable file and emoji completion in the current buffer."
    (setq-local completion-at-point-functions
                (cons #'cape-file completion-at-point-functions)
                completion-at-point-functions
                (cons #'cape-emoji completion-at-point-functions)))

  :hook
  ((text-mode-hook prog-mode-hook) . cape-enable-completions))

corg

corg provides provides completion-at-point for org-mode source block and dynamic block headers.

(use-package corg
  :ensure (:host github
                 :repo "isamert/corg.el")
  :hook
  (org-mode-hook . corg-setup))

help

help

help is the built-in help system.

(use-feature help
  :custom
  (help-window-select t)

  :config
  (lossage-size 10000)

  :bind
  (("C-h C-k" . describe-keymap)
   ("C-h C-." . display-local-help)
   :map help-mode-map
   ("f" . ace-link-help)
   :map input-decode-map
   ([?\C-m] . [C-m])
   ([?\C-i] . [C-i])))

help-at-pt

help-at-pt displays local help based on text properties at point.

(use-feature help-at-pt
  :custom
  (help-at-pt-display-when-idle 'never)
  (help-at-pt-timer-delay 1) ; show help immediately when enabled

  :init
  (help-at-pt-set-timer)) ; set timer, thus enabling local help

helpful

helpful enhances the Emacs help buffer.

(use-package helpful
  :config
  ;; always use `helpful', even when `describe-function' is called by a program
  ;; (e.g. `transient')
  (advice-add 'describe-function :override #'helpful-function)

  :hook
  (minibuffer-setup-hook . (lambda () (require 'helpful)))

  :bind
  (("C-h k" . helpful-key)
   ("C-h f" . helpful-function)
   ("C-h c" . helpful-command)
   ("C-h o" . helpful-symbol)
   ("C-h v" . helpful-variable)
   ("C-h ." . helpful-at-point)
   :map helpful-mode-map
   ("f" . ace-link-help)
   ("w" . files-extras-copy-as-kill-dwim)))

info

info is the Info documentation browser.

(use-feature info
  :config
  (with-eval-after-load 'savehist
    (add-to-list 'savehist-additional-variables 'Info-history-list))

  :bind
  (:map Info-mode-map
        ("f" . ace-link-info)
        ("m" . Info-prev)
        ("/" . Info-next)
        ("," . Info-up)
        ("j" . Info-backward-node)
        (";" . Info-forward-node)
        ("s-j" . Info-menu)))

man

man is a manual page viewer.

(use-feature man
  :bind
  (:map Man-mode-map
        ("f" . ace-link-man)))

woman

woman browses manual pages without the man command.

(use-feature woman
  :bind
  (:map woman-mode-map
        ("f" . ace-link-woman)))

shortdoc

shortdoc provides short function summaries.

(use-feature shortdoc
  :bind
  ("C-h u" . shortdoc-display-group))

find-func

find-func finds the definition of the Emacs Lisp function near point.

(use-feature find-func
  :bind
  ("M-L" . find-library))

elisp-refs

elisp-refs finds references to functions, macros and variables in Elisp files.

(use-feature elisp-refs
  :bind (:map elisp-refs-mode-map
              ("f" . ace-link-help)))

elisp-demos

elisp-demos displays examples for many Elisp functions.

(use-package elisp-demos
  :after helpful
  :init
  (advice-add 'helpful-update :after 'elisp-demos-advice-helpful-update))

keyboard macros

kmacro

kmacro provides a simplified interface for keyboard macros.

(use-feature kmacro
  :config
  (kmacro-set-counter 1)
  (with-eval-after-load 'savehist
    (dolist (var '(kmacro-ring last-kbd-macro))
      (add-to-list 'savehist-additional-variables var)))

  :bind
  (("A-H-M-s-h" . kmacro-end-or-call-macro) ; = H-h, to circumvent OSX mapping
   ("H-H" . kmacro-start-macro-or-insert-counter)
   ("A-C-H-s-h" . kmacro-set-counter)
   ("M-h" . kmacro-edit-macro)
   ("M-A-h" . kmacro-bind-to-key)))

kmacro-extras

kmacro-extras collects my extensions for kmacro.

(use-personal-package kmacro-extras
  :bind
  ("C-A-h" . kmacro-extras-counter-toggle-alpha-number))

shell

simple

simple configures shell command behaviour for interactive use.

(use-feature simple
  :custom
  (shell-command-switch "-ic") ; https://stackoverflow.com/a/12229404/4479455
  (async-shell-command-buffer 'new-buffer)) ; don't ask for confirmation before running command in a new buffer

shell

shell provides a shell-mode interface for inferior shell processes.

(use-feature shell
  :init
  ;; remove maddening "saving session" messages in non-interactive shells
  (let ((filtered-env
       (seq-filter
        (lambda (var)
          (let ((var-name (car (split-string var "="))))
            (not (member var-name '("TERM_PROGRAM" "TERM_SESSION_ID")))))
        process-environment)))
  (setq process-environment filtered-env
        shell-command-environment filtered-env))

  :bind
  (("A-s" . shell)
  :map shell-mode-map
   ("M-p" . nil)
   ("M-n" . nil)
   ("M-k" . comint-previous-input)
   ("M-l" . comint-next-input)))

eshell

eshell is the Emacs shell, a shell implemented entirely in Emacs Lisp.

(use-feature eshell
  :after simple
  :custom
  (eshell-banner-message "")
  (eshell-save-history-on-exit t)
  (eshell-hist-ignoredups t)
  (eshell-history-size 100000)
  (eshell-last-dir-ring-size 1000)

  :config
  (require 'esh-mode)

  :bind
  (("A-e" . eshell)
   :map eshell-mode-map
   ("<tab>" . yas-next-field-or-maybe-expand)
   ("TAB" . yas-next-field-or-maybe-expand) ; why is this necessary for eshell only?
   ("C-H-M-z" . eshell-kill-input)
   ("A-C-s-m" . beginning-of-line)
   ("M-k" . eshell-previous-matching-input-from-input)
   ("M-l" . eshell-next-matching-input-from-input)
   ("s-l" . eshell/clear)
   ("s-d" . eshell-send-eof-to-process)
   ("M-p" . nil)
   ("M-n" . nil)))

em-hist

em-hist provides history management for eshell.

(use-feature em-hist
  :defer t
  :custom
  (eshell-hist-ignoredups t)
  (eshell-save-history-on-exit t))

eshell-syntax-highlighting

eshell-syntax-highlighting provides syntax highlighting for eshell-mode.

(use-package eshell-syntax-highlighting
  :after eshell
  :hook
  (eshell-mode-hook . eshell-syntax-highlighting-global-mode))

dwim-shell-command

dwim-shell-command supports Emacs shell commands with dwim behaviour.

(use-package dwim-shell-command
  :ensure (:host github
           :repo "xenodium/dwim-shell-command")
  :defer t)

eat

eat is a terminal emulator.

(use-package eat
  :ensure (:host codeberg
                 :repo "akib/emacs-eat"
                 :files ("*.el" ("term" "term/*.el") "*.texi"
                         "*.ti" ("terminfo/e" "terminfo/e/*")
                         ("terminfo/65" "terminfo/65/*")
                         ("integration" "integration/*")
                         (:exclude ".dir-locals.el" "*-tests.el")))
  :custom
  (eat-term-name "xterm-256color")

  :hook
  (eshell-load-hook . eat-eshell-mode)
  (eshell-load-hook . eat-eshell-visual-command-mode))

eat-extras

eat-extras collects my extensions for eat.

(use-personal-package eat-extras
  :after eat
  :demand t
  :hook
  (eat-mode-hook . eat-extras-use-fixed-pitch-font)
  :config
  (eat-extras-setup-semi-char-mode-map))

vterm

vterm is another terminal emulator.

(use-package vterm
  :defer t
  :custom
  (vterm-always-compile-module t))

vterm-extras

vterm-extras collects my extensions for vterm.

(use-personal-package vterm-extras
  :after vterm
  :demand t
  :config
  (vterm-extras-setup-keymap))

spelling & grammar

jinx

jinx is a highly performant spell-checker for Emacs.

(use-package jinx
  :after faces-extras
  :custom
  (jinx-languages "en")

  :config
  (faces-extras-set-and-store-face-attributes
   '((jinx-misspelled :underline '(:color "#008000" :style wave))))

  (add-to-list 'vertico-multiform-categories
               '(jinx grid (vertico-grid-annotate . 20)))

  :hook
  ((text-mode-hook prog-mode-hook conf-mode-hook) . jinx-mode)

  :bind
  (("M-p" . jinx-correct)))

jinx-extras

jinx-extras collects my extensions for jinx.

(use-personal-package jinx-extras
  :after jinx

  :bind
  (("A-M-p" . jinx-extras-toggle-languages)))

flycheck

flycheck is a syntax-checker for Emacs.

(use-package flycheck
  :after faces-extras
  :custom
  ;; move temporary flycheck files to a temporary directory
  (flycheck-temp-prefix (concat temporary-file-directory "flycheck-"))
  (flycheck-emacs-lisp-load-path 'inherit)
  (flycheck-indication-mode nil)
  (flycheck-display-errors-delay 0.5)
  (flycheck-checker-error-threshold 10000)
  ;; org-lint runs synchronously in-process; it freezes Emacs on large org files
  (flycheck-disabled-checkers '(org-lint))
  ;; https://github.com/skeeto/elfeed/pull/448#issuecomment-1120336279
  (flycheck-global-modes '(not . (elfeed-search-mode)))

  :config
  (faces-extras-set-and-store-face-attributes
   '((flycheck-error :underline '(:color "#ff0000" :style wave))
     (flycheck-warning :underline '(:color "#0000ff" :style wave))))

  :hook
  (find-file-hook . global-flycheck-mode)
  (org-src-mode-hook . (lambda ()
                         "Disable `emacs-lisp-checkdoc' in `org-src' blocks."
                         (setq-local flycheck-disabled-checkers '(emacs-lisp-checkdoc))))

  (after-change-major-mode-hook . (lambda ()
                                    "Disable flycheck in selected buffers."
                                    (when (member (buffer-name) '("*scratch*" "notes"))
                                      (flycheck-mode -1))))

  :bind
  ("M-k" . flycheck-next-error))

consult-flycheck

consult-flycheck integrates flycheck with consult.

(use-package consult-flycheck
  :after consult flyckeck)

flycheck-ledger

flycheck-ledger provides flycheck support for ledger-mode.

(use-package flycheck-ledger
  :after flycheck ledger-mode)

flycheck-languagetool

flycheck-languagetool provides flycheck support for LanguageTool.

(use-package flycheck-languagetool
  :ensure (:host github
                 :repo "benthamite/flycheck-languagetool"
                 :branch "fix/guard-stale-buffer-positions") ; https://github.com/emacs-languagetool/flycheck-languagetool/pull/40
  :after flycheck
  :init
  (setopt flycheck-languagetool-server-jar
        (expand-file-name (file-name-concat paths-dir-external-repos "LanguageTool/languagetool-server.jar")))

  :custom
  (flycheck-languagetool-check-params
   '(("level" . "picky")
     ("disabledRules" . "ARROWS,DASH_RULE,DATE_NEW_YEAR,EN_QUOTES,GITHUB,WHITESPACE_RULE")))

  :config
  (defun flycheck-languagetool-enable ()
    "Enable `flycheck-languagetool' in selected buffers."
    (unless (or (derived-mode-p 'forge-post-mode
                                'gfm-mode
                                'mhtml-mode
                                'flycheck-error-message-mode
                                'mu4e-compose-mode
                                'mu4e-view-mode
                                'org-journal-mode
                                'org-msg-edit-mode)
                (not (file-directory-p default-directory)))
      (flycheck-select-checker 'languagetool)))

  (defun flycheck-languagetool-toggle ()
    "Toggle the LanguageTool checker in the current buffer."
    (interactive)
    (if (eq flycheck-checker 'languagetool)
        (progn
          (setq-local flycheck-checker nil)
          (flycheck-buffer)
          (message "LanguageTool disabled"))
      (flycheck-select-checker 'languagetool)
      (message "LanguageTool enabled")))

  :hook
  ((markdown-mode-hook
    org-mode-hook
    org-msg-edit-mode-hook) . flycheck-languagetool-enable))

flymake-mdl

flymake-mdl provides a flymake backend for markdownlint.

(use-package flymake-mdl
  :ensure (:host github
                 :repo "MicahElliott/flymake-mdl")
  :after markdown-mode
  :demand t)

prose

text-mode

text-mode is the major mode for editing plain text.

(use-feature text-mode
  :hook
  (text-mode-hook . simple-extras-visual-line-mode-enhanced)
  (text-mode-hook . (lambda ()
                      "Disable ispell completion in text mode."
                      (remove-hook 'completion-at-point-functions #'ispell-completion-at-point t))))

atomic-chrome

atomic chrome enables editing of browser input fields in Emacs.

I use a fork that is better maintained, together with the associated Chrome Emacs browser extension.

(use-package atomic-chrome
  :ensure (:repo "KarimAziev/atomic-chrome"
           :host github)
  :defer 30
  :custom
  (atomic-chrome-default-major-mode 'markdown-mode)
  (atomic-chrome-create-file-strategy 'buffer) ; needed for proper recognition of modes
  (atomic-chrome-url-major-mode-alist
   '(("github\\.com" . gfm-mode)
     ("wikipedia\\.org" . mediawiki-mode)
     ("timelines\\.issarice\\.com" . mediawiki-mode)))

  :config
  (setq-default atomic-chrome-extension-type-list '(atomic-chrome))
  (atomic-chrome-start-server)

  :bind
  (:map atomic-chrome-edit-mode-map
        ("s-c" . atomic-chrome-close-current-buffer)))

markdown-mode

markdown-mode is a major mode for editing Markdown-formatted text.

(use-package markdown-mode
  :custom
  (markdown-fontify-code-blocks-natively t)
  (markdown-command "pandoc --from markdown --to html")
  (markdown-disable-tooltip-prompt t)
  (markdown-italic-underscore t)

  :config
  ;; pop code block indirect buffers in the same window, mirroring the org behavior
  (add-to-list 'display-buffer-alist
             '("\\*edit-indirect.*\\*"
               (display-buffer-same-window)))

  :bind
  (:map gfm-mode-map
        ("s-a" . markdown-insert-gfm-code-block)
        ("s-z" . markdown-edit-code-block)
        ("A-C-H-t" . markdown-mode-extras-copy-section)
        ("A-C-s-r" . markdown-outline-previous)
        ("A-C-s-f" . markdown-outline-next)
        ("M-p" . nil)
        ("A-s-f" . markdown-footnote-goto-text)
        ("A-s-r" . markdown-footnote-return)
        ("s-b" . markdown-insert-bold)
        ("s-e" . markdown-insert-code)
        ("s-f" . markdown-insert-footnote)
        ("s-i" . markdown-insert-italic)
        ("s-k" . markdown-insert-link)
        ("s-p" . markdown-preview)
        :map markdown-mode-map
        ("s-a" . markdown-insert-gfm-code-block)
        ("s-z" . markdown-edit-code-block)
        ("A-C-H-t" . markdown-mode-extras-copy-section)
        ("A-C-s-r" . markdown-outline-previous)
        ("A-C-s-f" . markdown-outline-next)
        ("M-p" . nil)
        ("A-s-f" . markdown-footnote-goto-text)
        ("A-s-r" . markdown-footnote-return)
        ("s-b" . markdown-insert-bold)
        ("s-e" . markdown-insert-code)
        ("s-f" . markdown-insert-footnote)
        ("s-i" . markdown-insert-italic)
        ("s-k" . markdown-insert-link)
        ("s-p" . markdown-preview)))

markdown-mode-extras

markdown-mode-extras collects my extensions for markdown-mode.

(use-personal-package markdown-mode-extras
  :bind
  (:map gfm-mode-map
        ("A-C-H-t" . markdown-mode-extras-copy-section)
        ("s-l" . markdown-mode-extras-insert-locator)
        ("s-r" . markdown-mode-extras-remove-url-in-link)
        ("s-v" . markdown-mode-extras-paste-with-conversion)
        ("H-s-v" . markdown-mode-extras-org-paste-dwim)
        :map markdown-mode-map
        ("A-C-H-t" . markdown-mode-extras-copy-section)
        ("s-l" . markdown-mode-extras-insert-locator)
        ("s-r" . markdown-mode-extras-remove-url-in-link)
        ("s-v" . markdown-mode-extras-paste-with-conversion)
        ("H-s-v" . markdown-mode-extras-org-paste-dwim)
        :map org-mode-map
        ("H-s-v" . markdown-mode-extras-org-paste-dwim)))

grip-mode

grip-mode provides org-mode and Github-flavored Markdown preview using grip.

(use-package grip-mode
  :defer t
  :init
  (with-eval-after-load 'markdown-mode
    (bind-keys :map markdown-mode-map
               ("s-w" . grip-mode)))

  :custom
  (grip-github-user (auth-source-pass-get "user" "tlon/core/api.github.com/grip-mode"))
  (grip-github-password (auth-source-pass-get 'secret "tlon/core/api.github.com/grip-mode"))

  :config
  (require 'xwidget))

xwidget

xwidget provides API functions for xwidgets.

(use-feature xwidget
  :config
  ;; do not prompt user when attempting to kill xwidget buffer
  (remove-hook 'kill-buffer-query-functions #'xwidget-kill-buffer-query-function)

  :bind
  (:map xwidget-webkit-mode-map
        ("," . xwidget-webkit-scroll-down)
        ("." . xwidget-webkit-scroll-up)
        ("j" . xwidget-webkit-scroll-top)
        (";" . xwidget-webkit-scroll-bottom)))

edit-indirect

edit-indirect supports editing regions in separate buffers.

This package is required by the markdown-mode command markdown-edit-code-block.

(use-package edit-indirect
  :after markdown-mode
  :bind (:map edit-indirect-mode-map
              ("s-z" . edit-indirect-commit)))

mediawiki

mediawiki is an Emacs interface to editing mediawiki sites.

(use-package mediawiki
  ;; :ensure (:tag "2.4.3") ; otherwise can't authenticate; https://github.com/hexmode/mediawiki-el/issues/48
  :custom
  (mediawiki-site-alist `(("Wikipedia"
                           "https://en.wikipedia.org/w/"
                           "Sir Paul"
                           ,(auth-source-pass-get 'secret "chrome/auth.wikimedia.org/Sir_Paul") ""
                           :description "English Wikipedia" :first-page "Main Page")))
  (mediawiki-draft-data-file (file-name-concat paths-dir-notes "drafts.wiki"))

  :bind
  (:map mediawiki-mode-map
        ("s-k" . mediawiki-insert-link)
        ("A-C-s-r" . mediawiki-prev-header)
        ("A-C-s-f" . mediawiki-next-header)))

wikipedia

wikipedia is an Emacs interface for Wikipedia, with a focus on fast editing workflows, review tools, and optional local/offline browsing of watched pages.

(use-package wikipedia
  :ensure (:host github
                 :repo "benthamite/wikipedia"
                 :depth nil)
  :defer t
  :custom
  (wikipedia-draft-directory (file-name-concat paths-dir-google-drive "wikipedia"))
  (wikipedia-ai-model 'gemini-flash-lite-latest)
  (wikipedia-auto-update-mode 1)
  (wikipedia-ai-review-auto t)
  (wikipedia-ai-summarize-auto t)
  (wikipedia-watchlist-score-reason-auto t)
  (wikipedia-watchlist-sort-by-score t)

  :bind
  (("A-p" . wikipedia-transient)))

gdocs

gdocs provides Emacs integration with Google Docs

(use-package gdocs
  :ensure (:host github
                 :repo "benthamite/gdocs")
  :defer t
  :custom
  (gdocs-accounts
   `(("personal" . ((client-id . ,(auth-source-pass-get "gdocs-client-id" "chrome/cloud.google.com"))
                    (client-secret . ,(auth-source-pass-get "gdocs-client-secret" "chrome/cloud.google.com")))))))

gdrive

gdrive is an interface to Google Drive.

(use-package gdrive
  :ensure (:host github
                 :repo "benthamite/gdrive"
                 :depth nil)
  :defer t
  :init
  (load-file (file-name-concat paths-dir-dotemacs "etc/gdrive-users.el"))
  (defun gdrive-extras-mark-multiple-and-share (&optional file)
    "Search for each line in FILE and share the selected results.
The file should contain one search query per line."
    (interactive)
    (require 'gdrive)
    (gdrive-mark-clear)
    (if-let ((file (or file (when (y-or-n-p "Read file? ")
                              (read-file-name "File: "))))
             (lines (files-extras-lines-to-list file)))
        (dolist (line lines)
          (gdrive-extras--mark-matching-results line))
      (gdrive-extras--mark-matching-results (read-string "ID: ")))
    (gdrive-share-results gdrive-marked-files))

  (defun gdrive-extras--mark-matching-results (string)
    "Mark the files that match STRING."
    (let ((results (gdrive-act-on-selected-search-results string)))
      (gdrive-mark-results results))))

ledger-mode

ledger-mode is a major mode for interacting with the Ledger accounting system.

To populate the database of historical prices:

(use-package ledger-mode
  :custom
  (ledger-default-date-format ledger-iso-date-format)
  (ledger-reconcile-default-commodity "ARS")
  (ledger-mode-extras-currencies '("USD" "EUR" "GBP" "ARS"))
  (ledger-schedule-file paths-file-tlon-ledger-schedule-file)
  (ledger-schedule-look-forward 0)
  (ledger-schedule-look-backward 30)

  :config
  (dolist (report
           '(("net worth"
              "%(binary) --date-format '%Y-%m-%d' -f %(ledger-file) bal --strict")
             ("net worth (USD)"
              "%(binary) --date-format '%Y-%m-%d' -f %(ledger-file) --price-db .pricedb --exchange USD bal ^assets ^liabilities --strict")
             ("account"
              "%(binary) --date-format '%Y-%m-%d' -f %(ledger-file) reg %(account) --price-db .pricedb")
             ("account (USD)"
              "%(binary) --date-format '%Y-%m-%d' -f %(ledger-file) reg %(account) --price-db .pricedb --exchange USD --limit \"commodity == 'USD'\"")
             ("cost basis"
              "%(binary) --date-format '%Y-%m-%d' -f %(ledger-file) --basis bal %(account) --strict")
             ("account (unrounded)"
              "%(binary) --date-format '%Y-%m-%d' --unround -f %(ledger-file) reg %(account)")))
    (add-to-list 'ledger-reports report))

  :hook
  (ledger-reconcile-mode-hook . (lambda () (mouse-wheel-mode -1)))

  :bind
  (:map ledger-mode-map
        ("A-C-s-f" . ledger-navigate-next-xact-or-directive)
        ("A-C-s-r" . ledger-navigate-prev-xact-or-directive)
        ("A-s-e" . ledger-toggle-current-transaction)
        ("M-n" . nil)
        ("M-p" . nil)
        ("s-=" . ledger-reconcile)
        ("s-a" . ledger-add-transaction)
        ("s-b" . ledger-post-edit-amount)
        ("s-d" . ledger-delete-current-transaction)
        ("s-f" . ledger-occur)
        ("s-g" . ledger-report-goto)
        ("s-i" . ledger-insert-effective-date)
        ("s-k" . ledger-report-quit)
        ("s-l" . ledger-display-ledger-stats)
        ("s-o" . ledger-report-edit-report)
        ("s-p" . ledger-display-balance-at-point)
        ("s-q" . ledger-post-align-dwim)
        ("s-r" . ledger-report)
        ("s-s" . ledger-report-save)
        ("s-u" . ledger-schedule-upcoming)
        ("s-v" . ledger-copy-transaction-at-point)
        ("s-x" . ledger-fully-complete-xact)
        ("s-y" . ledger-copy-transaction-at-point)
        ("s-z" . ledger-report-redo)
        :map ledger-report-mode-map
        ("A-C-s-f" . ledger-navigate-next-xact-or-directive)
        ("A-C-s-r" . ledger-navigate-prev-xact-or-directive)
        :map ledger-reconcile-mode-map
        ("q" . ledger-reconcile-quit)))

ledger-mode-extras

ledger-mode-extras collects my extensions for ledger-mode.

(use-personal-package ledger-mode-extras
  :after ledger-mode
  :demand t
  :bind
  (:map ledger-mode-map
        ("s-SPC" . ledger-mode-extras-new-entry-below)
        ("s-t" . ledger-mode-extras-sort-region-or-buffer)
        ("s-e" . ledger-mode-extras-extras-sort-region-or-buffer-reversed)
        ("A-s-a" . ledger-mode-extras-report-account)
        ("A-s-b" . ledger-mode-extras-decrease-date-by-one-day)
        ("A-s-c" . ledger-mode-extras-copy-transaction-at-point)
        ("A-s-f" . ledger-mode-extras-increase-date-by-one-day)
        ("A-s-t" . ledger-mode-extras-sort-region-or-buffer-reversed)
        ("A-s-u" . ledger-mode-extras-report-net-worth-USD)
        ("A-s-w" . ledger-mode-extras-report-net-worth)
        ("s-c" . ledger-mode-extras-align-and-next)
        ("A-s-c" . ledger-mode-extras-copy-transaction-at-point)
        ("s-x" . ledger-mode-extras-kill-transaction-at-point)))

translation

tlon

tlon is a set of Emacs commands that my team uses in various contexts.

(use-package tlon
  :ensure (:host github
                 :repo "tlon-team/tlon.el"
                 :depth nil) ; clone entire repo, not just last commit
  :after paths
  :init
  (with-eval-after-load 'forge
    (bind-keys :map forge-topic-mode-map
               ("," . tlon-visit-counterpart-or-capture)
               ("'" . tlon-open-forge-file)))
  (with-eval-after-load 'magit
    (bind-keys :map magit-status-mode-map
               ("," . tlon-visit-counterpart-or-capture)
               ("'" . tlon-open-forge-file)))
  (with-eval-after-load 'markdown-mode
    (bind-keys :map markdown-mode-map
               ("H-;" . tlon-md-menu)
               ("A-s-e" . tlon-yaml-edit-field)
               ("s-c" . tlon-copy-dwim)
               ("s-l" . tlon-md-insert-locator)
               ("s-u" . tlon-md-insert-entity)
               ("A-C-s-SPC" . tlon-md-beginning-of-buffer-dwim)
               ("A-C-s-<tab>" . tlon-md-end-of-buffer-dwim)
               :map gfm-mode-map
               ("H-;" . tlon-md-menu)
               ("A-s-e" . tlon-yaml-edit-field)
               ("s-c" . tlon-copy-dwim)
               ("s-l" . tlon-md-insert-locator)
               ("s-u" . tlon-md-insert-entity)
               ("A-C-s-SPC" . tlon-md-beginning-of-buffer-dwim)
               ("A-C-s-<tab>" . tlon-md-end-of-buffer-dwim)))

  :custom
  (tlon-forg-archive-todo-on-close t)
  (tlon-forg-sort-after-sync-or-capture t)

  :config
  (run-with-idle-timer (* 60 60) t #'tlon-pull-issues-in-all-repos)

  (with-eval-after-load 'tlon-db
    (defun tlon-db-extras--suppress-debugger (orig-fun &rest args)
      "Call ORIG-FUN with ARGS with `debug-on-error' bound to nil.
When `tlon-db-get-entries-no-confirm' runs from its idle timer,
`url-retrieve-synchronously' calls `accept-process-output', which
can fire sentinels for unrelated connections (e.g. ghub/Forge).
If those sentinels signal an error while `debug-on-error' is t,
the debugger's `recursive-edit' freezes Emacs inside the timer."
      (let ((debug-on-error nil))
        (condition-case err
            (apply orig-fun args)
          (error
           (message "tlon-db: periodic refresh failed: %s"
                    (error-message-string err))))))
    (advice-add 'tlon-db-get-entries-no-confirm :around
                #'tlon-db-extras--suppress-debugger))

  :hook
  (init-post-init-hook . tlon-initialize)

  :bind
  (("M-j" . tlon-node-find)
   ("H-r" . tlon-dispatch)
   ("H-?" . tlon-mdx-insert-cite)
   ("A-C-p" . tlon-grep)))

johnson

johnson is a multi-format dictionary UI for Emacs, providing the functionality of programs such as GoldenDict.

(use-package johnson
  :ensure (:host github
                 :repo "benthamite/johnson")
  :custom
  (johnson-dictionary-directories '("/Users/pablostafforini/My Drive/Dictionaries/"))

  :bind
  (("A-o" . johnson-lookup)
   :map johnson-mode-map
   ("," . johnson-prev-section)
   ("." . johnson-next-section)
   ("f" . johnson-ace-link)))

go-translate

go-translate is an Emacs translator that supports multiple translation engines.

(use-package go-translate
  :disabled
  :custom
  (gts-translate-list '(("en" "es")))
  (gts-default-translator
   (gts-translator

    :picker ; used to pick source text, from, to. choose one.

    ;;(gts-noprompt-picker)
    ;;(gts-noprompt-picker :texter (gts-whole-buffer-texter))
    (gts-prompt-picker)
    ;;(gts-prompt-picker :single t)
    ;;(gts-prompt-picker :texter (gts-current-or-selection-texter) :single t)

    :engines ; engines, one or more. Provide a parser to give different output.

    (list
     ;; (gts-bing-engine)
     ;;(gts-google-engine)
     ;;(gts-google-rpc-engine)
     (gts-deepl-engine :auth-key (auth-source-pass-get "key" (concat "tlon/babel/deepl.com/" tlon-email-shared)) :pro nil)
     ;; (gts-google-engine :parser (gts-google-summary-parser))
     ;;(gts-google-engine :parser (gts-google-parser))
     ;;(gts-google-rpc-engine :parser (gts-google-rpc-summary-parser) :url "https://translate.google.com")
     ;; (gts-google-rpc-engine :parser (gts-google-rpc-parser) :url "https://translate.google.com")
     )

    :render ; render, only one, used to consumer the output result. Install posframe yourself when use gts-posframe-xxx

    ;; (gts-buffer-render)
    ;;(gts-posframe-pop-render)
    ;;(gts-posframe-pop-render :backcolor "#333333" :forecolor "#ffffff")
    ;; (gts-posframe-pin-render)
    ;;(gts-posframe-pin-render :position (cons 1200 20))
    ;;(gts-posframe-pin-render :width 80 :height 25 :position (cons 1000 20) :forecolor "#ffffff" :backcolor "#111111")
    (gts-kill-ring-render)

    :splitter ; optional, used to split text into several parts, and the translation result will be a list.

    (gts-paragraph-splitter))))

powerthesaurus

powerthesaurus is an Emacs client for power thesaurus.

(use-package powerthesaurus
  :bind
  ("H-y" . powerthesaurus-transient))

reverso

reverso is an Emacs client for reverso.

(use-package reverso
  :ensure (:host github
           :repo "SqrtMinusOne/reverso.el")
  :custom
  (reverso-languages '(spanish english french german italian portuguese))

  :bind
  ("H-Y" . reverso))

dictionary

dictionary is a client for rfc2229 dictionary servers.

(use-feature dictionary
  :defer t
  :custom
  (dictionary-server "dict.org")
  (dictionary-use-single-buffer t))

docs

pdf-tools

pdf-tools is a support library for PDF files.

(use-package pdf-tools
  :mode ("\\.pdf\\'" . pdf-view-mode)
  :after (:any dired-extras ebib)
  :init
  (pdf-tools-install t)

  (with-eval-after-load 'pdf-annot
    (bind-keys :map pdf-annot-minor-mode-map
               ("e" . pdf-annot-add-highlight-markup-annotation)
               ("j" . pdf-view-goto-page)
               ("k" . pdf-view-previous-line-or-previous-page)
               ("l" . pdf-view-next-line-or-next-page)
               ("H-c" . pdf-view-kill-ring-save)
               ("A-u" . pdf-view-midnight-minor-mode)))

  (with-eval-after-load 'pdf-history
    (bind-keys :map pdf-history-minor-mode-map
               ("e" . pdf-annot-add-highlight-markup-annotation)
               ("j" . pdf-view-goto-page)
               ("k" . pdf-view-previous-line-or-previous-page)
               ("l" . pdf-view-next-line-or-next-page)
               ("H-c" . pdf-view-kill-ring-save)
               ("A-u" . pdf-view-midnight-minor-mode)))

  :custom
  (pdf-view-use-scaling t)
  (pdf-view-use-imagemagick nil)
  (pdf-view-resize-factor 1.01)
  (pdf-annot-default-annotation-properties
   '((t
      (label . user-full-name))
     (text
      (color . "#ff0000")
      (icon . "Note"))
     (highlight
      (color . "LightBlue2"))
     (underline
      (color . "blue"))
     (squiggly
      (color . "orange"))
     (strike-out
      (color . "red"))))

  :config
  (pdf-cache-prefetch-minor-mode -1) ; https://github.com/vedang/pdf-tools/issues/278#issuecomment-2096894629

  :hook
  (pdf-view-mode-hook . pdf-view-fit-page-to-window)

  :bind
  (:map pdf-view-mode-map
        ("s" . save-buffer)
        ("e" . pdf-annot-add-highlight-markup-annotation)
        ("j" . pdf-view-goto-page)
        ("k" . pdf-view-previous-line-or-previous-page)
        ("l" . pdf-view-next-line-or-next-page)
        ("H-c" . pdf-view-kill-ring-save)
        ("A-u" . pdf-view-midnight-minor-mode)))

pdf-tools-extras

pdf-tools-extras collects my extensions for pdf-tools.

(use-personal-package pdf-tools-extras
  :init
  (with-eval-after-load 'pdf-annot
    (bind-keys :map pdf-annot-minor-mode-map
               ("c" . pdf-tools-extras-copy-dwim)
               ("x" . pdf-tools-extras-count-words)
               ("e" . pdf-tools-extras-open-in-ebib)
               ("h" . pdf-annot-extras-add-highlight-markup-annotation)
               ("t" . pdf-tools-extras-toggle-writeroom)
               ("x" . pdf-tools-extras-open-externally)))
  (with-eval-after-load 'pdf-history
    (bind-keys
     :map pdf-history-minor-mode-map
     ("c" . pdf-tools-extras-copy-dwim)
     ("x" . pdf-tools-extras-count-words)
     ("e" . pdf-tools-extras-open-in-ebib)
     ("h" . pdf-annot-extras-add-highlight-markup-annotation)
     ("t" . pdf-tools-extras-toggle-writeroom)
     ("x" . pdf-tools-extras-open-externally)))

  :hook
  (pdf-tools-enabled-hook . pdf-tools-extras-apply-theme)
  (pdf-view-mode-hook . pdf-tools-extras-sel-mode)

  :bind (:map pdf-view-mode-map
              ("c" . pdf-tools-extras-copy-dwim)
              ("x" . pdf-tools-extras-count-words)
              ("e" . pdf-tools-extras-open-in-ebib)
              ("h" . pdf-annot-extras-add-highlight-markup-annotation)
              ("t" . pdf-tools-extras-toggle-writeroom)
              ("x" . pdf-tools-extras-open-externally)))

pdf-tools-pages

pdf-tools-pages is a simple pdf-tools extension I created to delete and extract pages from PDF files.

(use-package pdf-tools-pages
  :ensure (:host github
                 :repo "benthamite/pdf-tools-pages")
  :after pdf-tools
  :init
  (with-eval-after-load 'pdf-annot
    (bind-keys :map pdf-annot-minor-mode-map
               ("C" . pdf-tools-pages-clear-page-selection)
               ("D" . pdf-tools-pages-delete-selected-pages)
               ("S" . pdf-tools-pages-select-dwim)
               ("X" . pdf-tools-pages-extract-selected-pages)))
  (with-eval-after-load 'pdf-history
    (bind-keys
     :map pdf-history-minor-mode-map
     ("C" . pdf-tools-pages-clear-page-selection)
     ("D" . pdf-tools-pages-delete-selected-pages)
     ("S" . pdf-tools-pages-select-dwim)
     ("X" . pdf-tools-pages-extract-selected-pages)))

  :bind (:map pdf-view-mode-map
              ("C" . pdf-tools-pages-clear-page-selection)
              ("D" . pdf-tools-pages-delete-selected-pages)
              ("S" . pdf-tools-pages-select-dwim)
              ("X" . pdf-tools-pages-extract-selected-pages)))

scroll-other-window

scroll-other-window enables scrolling of the other window in pdf-tools.

(use-package scroll-other-window
  :ensure (:host github
                 :repo "benthamite/scroll-other-window")
  :after pdf-tools
  :hook
  (pdf-tools-enabled-hook . sow-mode)

  :bind
  (:map sow-mode-map
        ("A-C-s-g" . sow-scroll-other-window-down)
        ("A-C-s-t" . sow-scroll-other-window)))

pdf-view-restore

pdf-view-restore adds support to saving and reopening the last known PDF position.

(use-package pdf-view-restore
  :after pdf-tools
  :init
  ;; https://github.com/007kevin/pdf-view-restore/issues/6
  (defun pdf-view-restore-mode-conditionally ()
    "Enable `pdf-view-restore-mode' iff the current buffer is visiting a PDF."
    (when (buffer-file-name)
      (pdf-view-restore-mode)))

  :hook
  (pdf-view-mode-hook . pdf-view-restore-mode-conditionally))

moon-reader

moon-reader synchronizes page position between pdf-tools and Moon+ Reader.

(use-package moon-reader
  :ensure (:host github
           :repo "benthamite/moon-reader")
  :after pdf-view-restore
  :custom
  (moon-reader-cache-directory (file-name-concat paths-dir-google-drive "Apps/Books/.Moon+/Cache/")))

org-pdftools

org-pdftools adds org link support for pdf-tools.

(use-package org-pdftools
  :ensure (:build (:not elpaca-check-version))
  :after org pdf-tools
  :hook
  (org-mode-hook . org-pdftools-setup-link))

nov

nov is a major mode for reading EPUBs in Emacs.

(use-package nov
  :defer t)

djvu

djvu is a major mode for viewing and editing Djvu files in Emacs.

(use-package djvu
  :defer t)

programming

prog-mode

prog-mode is the base major mode for programming language modes.

(use-feature prog-mode
  :init
  (with-eval-after-load 'shell
    (bind-keys :map shell-mode-map
               ("s-c" . exit-recursive-edit)))

  :config
  (global-prettify-symbols-mode)

  :bind
  (("A-H-v" . set-variable)
   ("M-d" . toggle-debug-on-error)
   ("A-M-d" . toggle-debug-on-quit)
   :map prog-mode-map
   ("A-C-H-i" . mark-defun)
   ("s-e" . xref-find-definitions)
   ("s-f" . consult-flycheck)
   ("M-q" . nil)
   ("s-q" . prog-fill-reindent-defun)
   :map emacs-lisp-mode-map
   ("s-c" . exit-recursive-edit)))

treesit

treesit provides Tree-sitter-based syntax highlighting in Emacs.

(use-feature treesit
  :defer 30
  :config
  (setq treesit-language-source-alist
        '((typescript "https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src")
          (tsx "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src")))

  (unless (treesit-language-available-p 'typescript)
    (treesit-install-language-grammar 'typescript))

  (unless (treesit-language-available-p 'tsx)
    (treesit-install-language-grammar 'tsx)))

elisp-mode

elisp-mode is the major mode for editing Emacs Lisp.

(use-feature elisp-mode
  :init
  (defun instrument-defun ()
    "Instrument the current defun."
    (interactive)
    (eval-defun t))

  :bind (:map emacs-lisp-mode-map
              ("s-b" . eval-buffer)
              ("s-d" . eval-defun)
              ("s-i" . instrument-defun)
              :map lisp-interaction-mode-map
              ("s-b" . eval-buffer)
              ("s-d" . eval-defun)
              ("s-i" . instrument-defun)))

lisp-mode

lisp-mode is the major mode for editing Lisp code.

(use-feature lisp-mode
  :custom
  ;; default is 65, which overrides the value of `fill-column'
  (emacs-lisp-docstring-fill-column nil))

curl-to-elisp

curl-to-elisp converts cURL command to Emacs Lisp code.

(use-package curl-to-elisp
  :defer t)

f

f is a modern API for working with files and directories in Emacs.

(use-package f
  :defer t)

s

s is a string manipulation library.

(use-package s
  :defer t)

backtrace

backtrace provides generic facilities for displaying backtraces.

(use-feature backtrace
  :defer t
  :custom
  (backtrace-line-length nil))

debug

debug is the Emacs Lisp debugger.

(use-feature debug
  :config
  (defun debug-save-backtrace ()
    "Save the backtrace at point and copy its path to the kill-ring."
    (interactive)
    (when (string-match-p "\\*Backtrace\\*" (buffer-name))
      (let* ((contents (buffer-string))
             (file (file-name-concat paths-dir-downloads "backtrace.el"))
             message)
        (with-temp-buffer
          (insert contents)
          (write-region (point-min) (point-max) file)
          (setq message (format "Backtrace saved to \"%s\" (%s)"
                                (abbreviate-file-name file)
                                (file-size-human-readable (file-attribute-size (file-attributes file)))))
          (kill-new file))
        (kill-buffer)
        (message "%s" message))))

  :bind
  (:map debugger-mode-map
        ("s" . debug-save-backtrace)))

edebug

edebug is a source-level debugger for Emacs Lisp.

(use-feature edebug
  :custom
  (edebug-sit-for-seconds 10)
  (edebug-sit-on-break nil)
  ;; do not truncate print results
  (print-level nil)
  (print-length nil)
  (print-circle nil)
  (edebug-print-level nil)
  (edebug-print-length nil)
  (edebug-print-circle nil) ; disable confusing #N= and #N# print syntax

  :bind
  (:map emacs-lisp-mode-map
        ("M-s-d" . edebug-defun)))

macrostep

macrostep is an interactive macro-expander.

See this video (starting at 7:30) for an introduction to this package.

(use-package macrostep
  :defer t)

js

js is a major mode for editing JavaScript.

(use-feature js
  :custom
  (js-indent-level 4)

  :bind
  (:map js-mode-map
        ("s-w" . eww-extras-browse-file)
        ("M-," . window-extras-buffer-move-left)
        ("M-." . window-extras-buffer-move-right)))

js2-mode

js2-mode is a Javascript editing mode for Emacs.

(use-package js2-mode
  :defer t)

clojure

clojure-mode provides support for the Clojure(Script) programming language.

(use-package clojure-mode
  :defer t)

haskell-mode

haskell-mode is a major mode for Haskell.

(use-package haskell-mode
  :defer t)

python

python is the major mode for editing Python.

(use-feature python
  :custom
  (python-shell-interpreter "/Users/pablostafforini/.pyenv/shims/python3")
  (org-babel-python-command "/Users/pablostafforini/.pyenv/shims/python3")
  (flycheck-python-pycompile-executable "/Users/pablostafforini/.pyenv/shims/python3")
  (python-indent-offset 4)  ; Set default to suppress warning message
  (python-indent-guess-indent-offset nil)  ; Don't try to guess indent

  :config
  (org-babel-do-load-languages
   'org-babel-load-languages
   '((python . t)))

  :bind (:map python-mode-map
              ("C-c p" . run-python)
              ("s-l" . python-shell-send-file)
              ("s-d" . python-shell-send-defun)
              ("s-c" . python-shell-send-buffer)
              ("s-s" . python-shell-send-string)
              ("s-r" . python-shell-send-region)
              ("s-e" . python-shell-send-statement)
              :map python-ts-mode-map
              ("C-c p" . run-python)
              ("s-l" . python-shell-send-file)
              ("s-d" . python-shell-send-defun)
              ("s-c" . python-shell-send-buffer)
              ("s-s" . python-shell-send-string)
              ("s-r" . python-shell-send-region)
              ("s-e" . python-shell-send-statement)))

pyenv-mode

pyenv-mode integrates pyenv with python-mode.

(use-package pyenv-mode
  :after python
  :init
  (add-to-list 'exec-path "~/.pyenv/bin"))

pet

pet tracks down the correct Python tooling executables from Python virtual environments.

(use-package pet
  :after python
  :defer t
  ;; Disabled by default because it causes slowdown when opening Python files
  ;; Uncomment the line below to enable pet-mode automatically
  ;; :config
  ;; (add-hook 'python-base-mode-hook 'pet-mode -10)
  )

emacs-ipython-notebook

emacs-ipython-notebook is a Jupyter notebook client in Emacs.

This needs to be configured.

(use-package ein
  :after python
  :defer t)

go

go-mode provides support for the Go programming language.

(use-package go-mode
  :defer t)

applescript-mode

applescript-mode is a major mode for editing AppleScript.

(use-package applescript-mode
  :defer t)

json-mode

json-mode is a major mode for editing JSON files.

(use-package json-mode
  :defer t
  :bind
  (:map json-mode-map
        ("RET" . nil)))

csv-mode

csv-mode is a major mode for editing comma-separated values.

(use-package csv-mode
  :defer t)

yaml

yaml is YAML parser written in Emacs List without any external dependencies.

(use-package yaml
  :ensure (:host github
                 :repo "zkry/yaml.el")
  :defer t)

yaml-mode

yaml-mode is a major mode for editing YAML files.

(use-package yaml-mode)

shut-up

shut-up provides a macro to silence function calls.

(use-package shut-up)

puni

puni supports structured editing for many major modes.

(use-package puni
  :bind
  (:map prog-mode-map
   ("C-H-M-r" . puni-forward-kill-word)
   ("C-H-M-q" . puni-backward-kill-word)
   ("C-H-M-v" . puni-kill-line)
   ("C-H-M-z" . puni-backward-kill-line)
   ("A-C-s-d" . puni-forward-sexp)
   ("A-C-s-e" . puni-backward-sexp)
   ("A-C-s-r" . puni-beginning-of-sexp)
   ("A-C-s-f" . puni-end-of-sexp)
   ("A-C-H-j" . puni-mark-sexp-at-point)
   ("A-C-H-k" . puni-mark-sexp-around-point)))

hl-todo

hl-todo highlights TODO and similar keywords in comments and strings.

(use-package hl-todo
  :ensure (:build (:not elpaca-check-version))
  :defer 30
  :config
  (setopt hl-todo-keyword-faces
          (append '(("WAITING" . "blue")
                    ("LATER" . "violet")
                    ("SOMEDAY" . "brown")
                    ("DELEGATED" . "gray"))))

  (global-hl-todo-mode))

consult-todo

consult-todo uses consult to navigate hl-todo keywords.

(use-package consult-todo
  :after hl-todo
  :bind
  (:map prog-mode-map
        ("s-t" . consult-todo)))

project

project provides various functions for dealing with projects.

(use-feature project
  :bind
  (:map emacs-lisp-mode-map
   ("s-r" . project-query-replace-regexp)))

hideshow

hideshow is a minor mode for block hiding and showing.

(use-feature hideshow
  :hook
  (prog-mode-hook . hs-minor-mode))

aggressive-indent

aggressive-indent keeps code always indented.

(use-package aggressive-indent
  :config
  (global-aggressive-indent-mode 1)
  (add-to-list 'aggressive-indent-excluded-modes 'snippet-mode))

elpy

elpy is an Emacs Python development environment.

(use-package elpy
  :defer t
  :custom
  (elpy-rpc-python-command "python3")
  (elpy-rpc-virtualenv-path 'current)

  ;; Disabled by default because it causes slowdown when opening Python files
  ;; To enable elpy features, run M-x elpy-enable or uncomment the lines below
  ;; :config
  ;; (elpy-enable)
  )

eldoc

eldoc show function arglist or variable docstring in echo area.

(use-feature eldoc
  :config
  ;; emacs.stackexchange.com/a/55914/32089
  (define-advice elisp-get-fnsym-args-string (:around (orig-fun sym &rest r) docstring)
    "If SYM is a function, append its docstring."
    (concat
     (apply orig-fun sym r)
     (let* ((doc (and (fboundp sym) (documentation sym 'raw)))
            (oneline (and doc (substring doc 0 (string-match "\n" doc)))))
       (and oneline
            (stringp oneline)
            (not (string= "" oneline))
            (concat "  |  " (propertize oneline 'face 'italic))))))

  ;; reddit.com/r/emacs/comments/1l9y7de/showing_org_mode_link_at_point_in_echo_area/
  ;; this is natively supported in Emacs 31
  (defun org-display-link-info-at-point ()
    "Display the link info in the echo area when the cursor is on an Org mode link."
    (when-let* ((is-face-at-point 'org-link)
                (link-info (get-text-property (point) 'help-echo)))
      ;; This will show the link in the echo area without it being logged in
      ;; the Messages buffer.
      (let ((message-log-max nil)) (message "%s" link-info))))

  (dolist (h '(org-mode-hook org-agenda-mode-hook))
    (add-hook h (lambda () (add-hook 'post-command-hook #'org-display-link-info-at-point nil 'local))))

  (defun is-face-at-point (face)
    "Returns non-nil if given FACE is applied at text at the current point."
    (let ((face-at-point (get-text-property (point) 'face)))
      (or (eq face-at-point face) (and (listp face-at-point) (memq face face-at-point)))))

  (global-eldoc-mode))

org-mode

org

org-mode is a major mode for keeping notes, authoring documents, computational notebooks, literate programming, maintaining to-do lists, planning projects, and more.

(use-feature org
  :custom
  (org-directory paths-dir-org) ; set org directory
  (org-todo-keywords
   '((sequence "TODO(t)"
               "DOING(g)"
               "IMPORTANT(i)"
               "URGENT(u)"
               "SOMEDAY(s)"
               "MAYBE(m)"
               "WAITING(w)"
               "PROJECT(p)"
               "NEXT(n)"
               "LATER(l)"
               "|"
               "DELEGATED(e)"
               "DONE(d)"
               "CANCELLED(c)")))
  (org-priority-highest 1)
  (org-priority-default 7)
  (org-priority-lowest 9) ; set priorities
  (org-deadline-warning-days 0)              ; show due tasks only on the day the tasks are due
  (org-hide-emphasis-markers t)
  (org-hide-leading-stars t) ; indent every heading and hide all but the last leading star
  (org-startup-indented t)
  (org-log-into-drawer "STATES")
  (org-log-done 'time) ; add timestamp when task is marked as DONE
  (org-log-repeat nil) ; do not log TODO status changes for repeating tasks
  (org-M-RET-may-split-line nil) ; irreal.org/blog/?p=6297
  (org-loop-over-headlines-in-active-region t) ; Allow simultaneous modification of multiple task statuses.
  (org-ctrl-k-protect-subtree t)
  (org-special-ctrl-a/e t) ; `org-beginning-of-line' goes to beginning of first word
  (org-mark-ring-length 4)
  (org-pretty-entities nil)
  (org-image-actual-width '(800))
  (org-link-elisp-confirm-function nil)
  (org-file-apps '((auto-mode . emacs)
                   (directory . emacs)
                   ("\\.mm\\'" . default)
                   ("\\.x?html?\\'" . default)
                   ("\\.pdf\\'" . emacs)))
  (org-use-tag-inheritance t)
  (org-yank-dnd-method 'attach)
  (org-yank-image-save-method paths-dir-org-images)
  (org-structure-template-alist
   '(("a" . "export ascii")
     ("c" . "center")
     ("C" . "comment")
     ("e" . "example")
     ("E" . "export")
     ("h" . "export html")
     ("l" . "export latex")
     ("q" . "quote")
     ("s" . "src")
     ("se" . "src emacs-lisp")
     ("sE" . "src emacs-lisp :tangle (init-tangle-conditionally)")
     ("sc" . "src clojure")
     ("sj" . "src javascript")
     ("sm" . "src markdown")
     ("sp" . "src python")
     ("sq" . "src sql")
     ("ss" . "src shell")
     ("v" . "verse")
     ("w" . "WP")))

  ;; refile
  (org-reverse-note-order t) ; store notes at the beginning of header

  ;; export
  (org-export-backends '(ascii html icalendar latex md odt texinfo)) ; set export backends
  (org-preview-latex-default-process 'dvisvgm)

  ;; org-src
  (org-src-fontify-natively t)

  ;; org-crypt
  (org-tags-exclude-from-inheritance '("crypt"))

  :config
  (plist-put org-format-latex-options :scale 2)
  (plist-put org-format-latex-options :background "Transparent")
  (dolist (module '(org-habit org-tempo))
    (add-to-list 'org-modules module))

  ;; force reloading of first file opened so the buffer is correctly formatted
  (with-eval-after-load 'org
    (when (and (buffer-file-name)
               (string-match "\\.org$" (buffer-file-name)))
      (revert-buffer nil t)))

  :bind
  (:map org-mode-map
        ("C-H-M-s-z" . org-shiftleft)
        ("C-H-M-s-x" . org-shiftup)
        ("C-H-M-s-c" . org-shiftdown)
        ("C-H-M-s-v" . org-shiftright)
        ("C-H-M-s-a" . org-metaleft)
        ("C-H-M-s-s" . org-metaup)
        ("C-H-M-s-d" . org-metadown)
        ("C-H-M-s-f" . org-metaright)
        ("C-H-M-s-q" . org-shiftmetaleft)
        ("C-H-M-s-w" . org-shiftmetaup)
        ("C-H-M-s-e" . org-shiftmetadown)
        ("C-H-M-s-r" . org-shiftmetaright)
        ("s-j" . consult-extras-org-heading)
        ("s-A-k" . org-web-tools-insert-link-for-url)
        ("s-l" . org-transclusion-add-all)
        ("s-c" . ox-clip-formatted-copy)
        ("s-w" . org-refile)
        ("s-A-i" . org-id-copy)
        ("<S-left>" . nil)
        ("<S-right>" . nil)
        ("<S-up>" . nil)
        ("<S-down>" . nil)
        ("<M-left>" . nil)
        ("<M-right>" . nil)
        ("<M-S-left>" . nil)
        ("<M-S-right>" . nil)
        ("<M-up>" . nil)
        ("<M-down>" . nil)
        ("C-j" . nil)
        ("<backtab>" . org-shifttab)
        ("C-k" . nil)
        ("C-," . nil)
        ("A-C-s-i" . org-backward-sentence)
        ("A-C-s-o" . org-forward-sentence)
        ("A-C-s-," . org-backward-paragraph)
        ("A-C-s-." . org-forward-paragraph) ; org element?
        ("A-C-s-m" . org-beginning-of-line)
        ("A-C-s-z" . org-end-of-line) ; karabiner maps `/' to `z'; otherwise I can't trigger the command while holding `shift'
        ("A-C-s-r" . org-previous-visible-heading)
        ("A-C-s-f" . org-next-visible-heading)
        ("A-C-s-M-m" . org-previous-block)
        ("A-C-s-M-/" . org-next-block)
        ("A-C-H-t" . org-extras-copy-dwim)
        ("A-C-M-s-j" . org-previous-link)
        ("A-C-M-s-;" . org-next-link)
        ("A-H-M-t" . org-transpose-element)
        ("s-d" . org-deadline)
        ("s-e" . org-set-effort)
        ("s-f" . org-footnote-action)
        ("s-h" . org-insert-todo-subheading)
        ("s-p" . org-time-stamp-inactive)
        ("s-A-p" . org-time-stamp)
        ("s-g" . org-agenda)
        ("A-s-g" . org-gcal-extras-menu)
        ("s-k" . org-insert-link)
        ("s-q" . org-set-tags-command)
        ("s-r" . org-roam-buffer-toggle)
        ("s-s" . org-schedule)
        ("s-t" . org-todo)
        ("s-A-t" . org-sort)
        ("s-u" . org-clock-split)
        ("s-y" . org-evaluate-time-range)
        ("s-z" . org-edit-special)
        ("s-," . org-priority)
        ("s-A-e" . org-export-dispatch)
        ("A-<return>" . "C-u M-<return>")
        ("A-M-<return>" . org-insert-todo-heading)
        ;; bindings with matching commands in Fundamental mode
        ("H-v" . org-yank)
        ("M-f" . ace-link-org)))

org-extras

org-extras collects my extensions for org.

(use-personal-package org-extras
  :init
  (setq org-extras-agenda-switch-to-agenda-current-day-timer
        (run-with-idle-timer (* 10 60) nil #'org-extras-agenda-switch-to-agenda-current-day))

  :custom
  (org-extras-id-auto-add-excluded-directories (list paths-dir-anki
                                                     paths-dir-dropbox-tlon-leo
                                                     paths-dir-dropbox-tlon-fede
                                                     paths-dir-android
                                                     (file-name-concat paths-dir-dropbox-tlon-leo "gptel/")
                                                     (file-name-concat paths-dir-dropbox-tlon-fede "archive/")
                                                     (file-name-concat paths-dir-dropbox-tlon-core "legal/contracts/")))

  (org-extras-agenda-files-excluded (list paths-file-tlon-tareas-leo
                                          paths-file-tlon-tareas-fede))

  (org-extras-clock-in-add-participants-exclude
   "Leo<>Pablo\\|Fede<>Pablo\\|Tlön: group meeting")

  :config
  (setopt org-extras-agenda-files-excluded
          (append org-extras-agenda-files-excluded
                  ;; files in `paths-dir-inactive' sans ., .., hidden files and subdirectories
                  (seq-filter (lambda (f) (not (file-directory-p f)))
                              (directory-files paths-dir-inactive t "^[^.][^/]*$"))))

  (quote (:link t :maxlevel 5 :fileskip0 t :narrow 70 :formula % :indent t :formatter org-extras-clocktable-sorter))

  :hook
  (org-mode-hook . org-extras-enable-nested-src-block-fontification)
  (before-save-hook . org-extras-id-auto-add-ids-to-headings-in-file)

  :bind
  (("H-;" . org-extras-personal-menu)
   ("A-H-w" . org-extras-refile-goto-latest)
   :map org-mode-map
   ("s-<return>" . org-extras-super-return)
   ("s-v" . org-extras-paste-with-conversion)
   ("A-C-s-n" . org-extras-jump-to-first-heading)
   ("A-s-b" . org-extras-set-todo-properties)
   ("A-s-h" . org-extras-insert-todo-subheading-after-body)
   ("A-s-v" . org-extras-paste-image)
   ("A-s-z" . org-extras-export-to-ea-wiki)
   ("M-w" . org-extras-count-words)
   ("A-s-n" . org-extras-new-clock-entry-today)
   ("s-." . org-extras-time-stamp-active-current-time)
   ("A-s-." . org-extras-time-stamp-active-current-date)
   ("s-/" . org-extras-time-stamp-inactive-current-time)
   ("A-s-/" . org-extras-time-stamp-inactive-current-date)
   ("A-s-u" . org-extras-id-update-id-locations)
   ("A-s-c" . org-extras-mark-checkbox-complete-and-move-to-next-item)
   ("A-s-o" . org-extras-reset-checkbox-state-subtree)
   ("H-s-w" . org-extras-refile-and-archive)
   ("s-A-l" . org-extras-url-dwim)))

org-agenda

org-agenda provides agenda views for org tasks and appointments.

(use-feature org-agenda
  :after org
  :init
  (setopt org-agenda-hide-tags-regexp "project")

  :custom
  (org-agenda-window-setup 'current-window)
  (org-agenda-use-time-grid nil)
  (org-agenda-ignore-properties '(effort appt category))
  (org-agenda-dim-blocked-tasks nil)
  (org-agenda-sticky t)
  (org-agenda-todo-ignore-with-date t)       ; exclude tasks with a date.
  (org-agenda-todo-ignore-scheduled 'future) ; exclude scheduled tasks.
  (org-agenda-restore-windows-after-quit t)  ; don't destroy window splits
  (org-agenda-span 1)                        ; show daily view by default
  (org-agenda-clock-consistency-checks       ; highlight gaps of five or more minutes in agenda log mode
   '(:max-duration "5:00" :min-duration "0:01" :max-gap 5 :gap-ok-around ("2:00")))
  (org-agenda-skip-scheduled-if-done t)
  (org-agenda-skip-deadline-if-done t)
  (org-agenda-log-mode-items '(clock))
  (org-agenda-custom-commands
   '(("E" "TODOs without effort"
      ((org-ql-block '(and (todo)
                           (not (property "effort")))
                     ((org-ql-block-header "TODOs without effort")))))
     ("w" "Weekly review"
      agenda ""
      ((org-agenda-clockreport-mode t)
       (org-agenda-archives-mode t)
       (org-agenda-start-day "-7d")
       (org-agenda-span 7)
       (org-agenda-start-on-weekday 0)))
     ("p" "Appointments" agenda* "Today's appointments"
      ((org-agenda-span 1)
       (org-agenda-max-entries 3)))
     ("r"
      "Reading list"
      tags
      "PRIORITY=\"1\"|PRIORITY=\"2\"|PRIORITY=\"3\"|PRIORITY=\"4\"|PRIORITY=\"5\"|PRIORITY=\"6\"|PRIORITY=\"7\"|PRIORITY=\"8\"|PRIORITY=\"9\""
      ((org-agenda-files (list paths-dir-bibliographic-notes))))
     ("g" "All TODOs"
      todo "TODO")
     ("G" "All Tlön TODOs"
      todo "TODO"
      ((org-agenda-files (list paths-dir-tlon-todos))))
     ("," "All tasks with no priority"
      tags-todo "-PRIORITY=\"1\"-PRIORITY=\"2\"-PRIORITY=\"3\"-PRIORITY=\"4\"-PRIORITY=\"5\"-PRIORITY=\"6\"-PRIORITY=\"7\"-PRIORITY=\"8\"-PRIORITY=\"9\"")))
  (org-agenda-files (list paths-file-calendar))
  (org-agenda-archives-mode 'trees)

  :config
  (advice-add 'org-agenda-goto :after
              (lambda (&rest _)
                "Narrow to the entry and its children after jumping to it."
                (org-extras-narrow-to-entry-and-children)))

  (advice-add 'org-agenda-prepare-buffers :around
              (lambda (fn files)
                "Skip agenda files that can't be opened (e.g. Dropbox online-only placeholders).
`org-agenda-prepare-buffers' passes each file to `org-get-agenda-file-buffer'
and hands the result to `with-current-buffer'.  When a file cannot be read
\(e.g. a Dropbox online-only placeholder), the call errors out.  Pre-filter
the file list so that only openable files reach the original function."
                (let (readable)
                  (dolist (f files)
                    (if (bufferp f)
                        (push f readable)
                      (condition-case err
                          (when (org-get-agenda-file-buffer f)
                            (push f readable))
                        (error
                         (message "org-agenda: skipping unreadable file %s: %s"
                                  f (error-message-string err))))))
                  (funcall fn (nreverse readable)))))

  (advice-add 'org-habit-toggle-display-in-agenda :around
              (lambda (orig-fun &rest args)
                "Prevent `org-modern-mode' interference with org habits."
                (if org-habit-show-habits
                    (progn
                      (global-org-modern-mode)
                      (apply orig-fun args)
                      (org-agenda-redo)
                      (global-org-modern-mode))
                  (global-org-modern-mode -1)
                  (apply orig-fun args)
                  (org-agenda-redo)
                  (global-org-modern-mode -1))))

  :hook
  (org-agenda-mode-hook . (lambda ()
                            "Disable `visual-line-mode' and `toggle-truncate-lines' in `org-agenda'."
                            (visual-line-mode -1)
                            (toggle-truncate-lines)))

  :bind
  (("s-g" . org-agenda)
   :map org-agenda-mode-map
   ("A-s-g" . org-gcal-extras-menu)
   ("s-s" . org-save-all-org-buffers)
   ("I" . org-pomodoro)
   ("h" . org-habit-toggle-display-in-agenda)
   ("M-k" . org-clock-convenience-timestamp-up)
   ("M-l" . org-clock-convenience-timestamp-down)
   ("s-b" . calendar-extras-calfw-block-agenda)
   ("f" . ace-link-org-agenda)
   ("?" . org-agenda-filter)
   (";" . org-agenda-later)
   ("C-b" . org-agenda-tree-to-indirect-buffer)
   ("C-k" . nil)
   ("d" . org-agenda-deadline)
   ("M-t" . nil)
   ("H-n" . nil)
   ("s-k" . nil)
   ("s-f" . ace-link-extras-org-agenda-clock-in)
   ("i" . org-agenda-clock-in)
   ("I" . org-agenda-diary-entry)
   ("j" . org-agenda-earlier)
   ("J" . org-agenda-goto-date)
   ("k" . org-agenda-previous-line)
   ("l" . org-agenda-next-line)
   ("n" . org-agenda-date-later)
   ("o" . org-agenda-open-link)
   ("p" . org-agenda-date-earlier)
   ("q" . org-agenda-kill-all-agenda-buffers)
   ("RET" . org-extras-agenda-switch-to-dwim)
   ("/" . org-extras-agenda-done-and-next)
   ("\"" . org-extras-agenda-postpone-and-next)
   ("b" . org-extras-agenda-toggle-anniversaries)
   ("SPC" . org-extras-agenda-goto-and-start-clock)
   ("x" . org-extras-agenda-toggle-log-mode)
   ("s" . org-agenda-schedule)
   ("w" . org-agenda-refile)
   ("W" . org-agenda-week-view)
   ("X" . org-agenda-exit)
   ("y" . org-agenda-day-view)
   ("z" . org-agenda-undo)))

org-capture

org-capture provides rapid note-taking and task capture.

(use-feature org-capture
  :custom
  (org-default-notes-file paths-file-inbox-desktop)
  (org-capture-templates
   `(("." "Todo" entry
      (id "B3C12507-6A83-42F6-9FFA-9A45F5C8F278")
      "** TODO %?\n" :empty-lines 1)
     ;; djcbsoftware.nl/code/mu/mu4e/Org_002dmode-links.html
     ("e" "Email" entry
      (id "B3C12507-6A83-42F6-9FFA-9A45F5C8F278")
      "** TODO Follow up with %:fromname on %a\nSCHEDULED: %t\n\n%i" :immediate-finish t :empty-lines 1 :prepend t)
     ("n" "Telegram" entry
      (id "B3C12507-6A83-42F6-9FFA-9A45F5C8F278")
      "** TODO Follow up with %a\nSCHEDULED: %t\n\n%i" :immediate-finish t :empty-lines 1 :prepend t)
     ("r" "Calendar" entry
      (file ,paths-file-calendar)
      "* TODO [#5] %^ \nDEADLINE: %^T" :empty-lines 1 :immediate-finish t)
     ("s" "Slack" entry
      (id "B3C12507-6A83-42F6-9FFA-9A45F5C8F278")
      "** TODO Follow up %a\nSCHEDULED: %t\n\n%i" :immediate-finish t :empty-lines 1 :prepend t)
     ("E" "Epoch inbox" entry
      (file paths-file-epoch-inbox)
      "** TODO %?\n" :empty-lines 1 :prepend t)
     ("S" "Slack to Epoch inbox" entry
      (file paths-file-epoch-inbox)
      "** TODO Follow up %a\nSCHEDULED: %t\n\n%i" :immediate-finish t :empty-lines 1 :prepend t)
     ("t" "Tlön inbox " entry
      (id "E9C77367-DED8-4D59-B08C-E6E1CCDDEC3A")
      "** TODO %? \n" :empty-lines 1 :prepend t)
     ("y" "YouTube playlist" entry
      (id "319B1611-A5A6-42C8-923F-884A354333F9")
      "* %(org-web-tools-extras-youtube-dl (current-kill 0))\n[[%c][YouTube link]]" :empty-lines 1 :prepend t :immediate-finish t)
     ;; github.com/alphapapa/org-protocol-capture-html#org-capture-template
     ("f" "Feed" entry
      (id "D70DFEBE-A5FD-4BA6-A054-49E7C8F6448A")
      "*** %(org-capture-feed-heading)\n%?" :empty-lines 1)
     ("w" "Web site" entry
      (file paths-file-downloads)
      "* %a :website:\n\n%U %?\n\n%:initial")))

  :config
  (defun org-capture-feed-heading ()
    "Prompt for feed URL, name, and tags, returning a formatted Org heading."
    (let* ((url (read-string "Feed URL: "))
           (name (read-string "Name: "))
           (id "D70DFEBE-A5FD-4BA6-A054-49E7C8F6448A")
           (file (org-id-find-id-file id))
           (all-tags (when file
                       (with-current-buffer (find-file-noselect file)
                         (mapcar #'car (org-get-buffer-tags)))))
           (selected (completing-read-multiple "Tags: " all-tags))
           (tag-str (if selected
                        (concat " :" (string-join selected ":") ":")
                      "")))
      (format "[[%s][%s]]%s" url name tag-str)))

  (defun org-capture-feed-sort ()
    "Sort feed entries under the Feeds heading alphabetically after capture."
    (when (string= (plist-get org-capture-plist :key) "f")
      (let ((marker (org-id-find "D70DFEBE-A5FD-4BA6-A054-49E7C8F6448A" 'marker)))
        (when marker
          (with-current-buffer (marker-buffer marker)
            (save-excursion
              (goto-char marker)
              (org-sort-entries nil ?a))
            (save-buffer))
          (set-marker marker nil)))))

  :hook
  (org-capture-before-finalize-hook . org-extras-capture-before-finalize-hook-function)
  (org-capture-after-finalize-hook . org-capture-feed-sort)

  :bind
  (("H-t" . org-capture)
   ("A-H-t" . org-capture-goto-last-stored)
   :map org-capture-mode-map
   ("s-c" . org-capture-finalize)
   ("s-w" . org-capture-refile)))

org-clock

org-clock implements clocking time spent on tasks.

(use-feature org-clock
  :after org
  :custom
  (org-clock-out-when-done t)
  (org-clock-persist t)
  (org-clock-persist-query-resume nil)
  (org-clock-in-resume t)
  (org-clock-report-include-clocking-task t)
  (org-clock-ask-before-exiting nil)
  (org-clock-history-length 30)
  (org-clock-into-drawer "LOGBOOK") ; file task state changes in STATES drawer

  :config
  (org-clock-persistence-insinuate)

  :bind
  (("A-H-j" . org-clock-goto)
   ("A-H-x" . org-clock-cancel)
   ("H-i" . org-clock-in)
   ("H-o" . org-clock-out)))

org-clock-convenience

org-clock-convenience provides convenience functions to work with org-mode clocking.

(use-package org-clock-convenience
  :after org-clock org-agenda
  :defer t)

org-clock-split

org-clock-split allows splitting of one clock entry into two contiguous entries.

I’m using a fork that fixes some functionality that broke when org changed org-time-stamp-formats.

(use-package org-clock-split
  :ensure (:host github
                 :repo "0robustus1/org-clock-split"
                 :branch "support-emacs-29.1")
  :defer t)

org-cycle

org-cycle controls visibility cycling of org outlines.

(use-feature org-cycle
  :after org
  :custom
  (org-cycle-emulate-tab nil)) ; TAB always cycles, even if point not on a heading

org-archive

org-archive archives org subtrees.

(use-feature org-archive
  :after org
  :custom
  (org-archive-default-command 'org-archive-to-archive-sibling)
  (org-archive-location (expand-file-name "%s_archive.org::" paths-dir-archive))

  :bind
  (:map org-mode-map
        ("s-a" . org-archive-subtree-default)))

org-archive-hierarchically

org-archive-hierarchically archives org subtrees in a way that preserves the original heading structure.

I normally archive subtrees with org-archive-to-archive-sibling, but use org-archive-hierarchically for files in public repositories (such as this one). org-archive-to-archive-sibling moves archived tasks to a heading, which is by default collapsed in org, but in Github archived tasks are always fully visible, creating a lot of clutter.

(use-package org-archive-hierarchically
  :ensure (:host gitlab
             :repo "andersjohansson/org-archive-hierarchically")
  :defer t)

org-fold

org-fold manages visibility and folding of org outlines.

(use-feature org-fold
  :after org
  :custom
  (org-fold-catch-invisible-edits 'smart))

org-faces

org-faces defines faces for org-mode.

(use-feature org-faces
  :after org faces-extras
  :custom
  (org-fontify-quote-and-verse-blocks t)

  :config
  (faces-extras-set-and-store-face-attributes
   '((org-drawer :family faces-extras-fixed-pitch-font :height faces-extras-org-property-value-height
                 :foreground "LightSkyBlue")
     (org-property-value :family faces-extras-fixed-pitch-font :height faces-extras-org-property-value-height)
     (org-special-keyword :family faces-extras-fixed-pitch-font :height faces-extras-org-property-value-height)
     (org-meta-line :family faces-extras-fixed-pitch-font :height faces-extras-org-property-value-height)
     (org-tag :family faces-extras-fixed-pitch-font :height faces-extras-org-tag-height)
     (org-document-title :family faces-extras-fixed-pitch-font :height faces-extras-fixed-pitch-height)
     (org-code :family faces-extras-fixed-pitch-font :height faces-extras-org-code-height)
     (org-todo :family faces-extras-fixed-pitch-font :height faces-extras-org-level-height)
     (org-archived :family faces-extras-fixed-pitch-font :height faces-extras-org-level-height)
     (org-level-1 :family faces-extras-fixed-pitch-font :height faces-extras-org-level-height)
     (org-level-2 :family faces-extras-fixed-pitch-font :height faces-extras-org-level-height)
     (org-level-3 :family faces-extras-fixed-pitch-font :height faces-extras-org-level-height)
     (org-level-4 :family faces-extras-fixed-pitch-font :height faces-extras-org-level-height)
     (org-level-5 :family faces-extras-fixed-pitch-font :height faces-extras-org-level-height)
     (org-level-6 :family faces-extras-fixed-pitch-font :height faces-extras-org-level-height)
     (org-level-7 :family faces-extras-fixed-pitch-font :height faces-extras-org-level-height)
     (org-level-8 :family faces-extras-fixed-pitch-font :height faces-extras-org-level-height)
     (org-date :family faces-extras-fixed-pitch-font :height faces-extras-org-date-height)
     (org-block :family faces-extras-fixed-pitch-font :height faces-extras-org-block-height)
     (org-block-begin-line :family faces-extras-fixed-pitch-font :height faces-extras-org-block-height)
     (org-quote :family faces-extras-variable-pitch-font :height 1.0))))

org-id

org-id manages globally unique IDs for org entries.

(use-feature org-id
  :after org
  :defer t
  :custom
  (org-id-link-to-org-use-id t)
  ;; I want these files to be searched for IDs, so that I can use
  ;; org-capture templates with them, but do not want them to be part
  ;; of org-agenda or org-roam.
  (org-id-extra-files (list
                       paths-file-tlon-tareas-leo
                       paths-file-tlon-tareas-fede))
  :config
  (defun org-id-ensure-locations-hash-table (&rest _)
    "Ensure `org-id-locations' is a hash table.
If `org-id-update-id-locations' is interrupted between building the
alist and converting it to a hash table, `org-id-locations' can be
left as an alist, causing `puthash' errors in `org-id-add-location'."
    (when (and org-id-locations (not (hash-table-p org-id-locations)))
      (setq org-id-locations (org-id-alist-to-hash org-id-locations))))

  (advice-add 'org-id-add-location :before #'org-id-ensure-locations-hash-table))

org-list

org-list handles plain lists in org-mode.

(use-feature org-list
  :after org
  :custom
  (org-plain-list-ordered-item-terminator ?.)
  (org-list-indent-offset 2))

org-refile

org-refile refiles subtrees to various locations.

(use-feature org-refile
  :after org
  :defer t
  :custom
  (org-refile-targets '((org-agenda-files :maxlevel . 9)
                        (files-extras-open-buffer-files :maxlevel . 9)
                        (nil :maxlevel . 9)))
  (org-refile-use-outline-path 'level3)
  (org-outline-path-complete-in-steps nil)
  (org-refile-allow-creating-parent-nodes nil)
  (org-refile-use-cache t) ;  build cache at startup

  :config
  ;; Regenerate cache every half hour
  (run-with-idle-timer (* 60 30) t #'org-extras-refile-regenerate-cache))

org-keys

org-keys manages speed keys and key bindings for org-mode.

Enable speed keys. To trigger a speed key, point must be at the very beginning of an org headline. Type ‘?’ for a list of keys.

(use-feature org-keys
  :after org
  :custom
  (org-return-follows-link t)
  (org-use-speed-commands t)
  (org-speed-commands
   '(("Outline navigation")
     ("k" . (org-speed-move-safe 'org-previous-visible-heading))
     ("." . (org-speed-move-safe 'org-forward-heading-same-level))
     ("," . (org-speed-move-safe 'org-backward-heading-same-level))
     ("l" . (org-speed-move-safe 'org-next-visible-heading))
     ("m" . (org-speed-move-safe 'outline-up-heading))
     ("j" . (consult-extras-org-heading))
     ("Outline structure editing")
     ("a" . (org-metaleft))
     ("d" . (org-metadown))
     ("s" . (org-metaup))
     ("f" . (org-metaright))
     ("q" . (org-shiftmetaleft))
     ("e" . (org-shiftmetadown))
     ("w" . (org-shiftmetaup))
     ("r" . (org-shiftmetaright))
     ("Archiving")
     ("A" . (org-archive-subtree-default))
     ("'" . (org-force-cycle-archived))
     ("Meta data editing")
     ("t" . (org-todo))
     ("Clock")
     ("h" . (org-extras-jump-to-latest-clock-entry))
     ("H" . (lambda () (org-extras-jump-to-latest-clock-entry) (org-extras-clone-clock-entry)))
     ("i" . (org-clock-in))
     ("o" . (org-clock-out))
     ("Regular editing")
     ("z" . (undo-only))
     ("X" . (org-cut-subtree)) ; capital 'X' to prevent accidents
     ("c" . (org-copy-subtree))
     ("v" . (org-yank))
     ("Other")
     ("I" . (org-id-copy))
     ("p" . (org-priority))
     ("u" . (org-speed-command-help))
     ("g" . (org-agenda)))))

ol

ol implements the org link framework.

(use-feature ol
  :after org
  :custom
  (org-link-search-must-match-exact-headline nil)
  (org-ellipsis " ")

  :bind
  ("H-L" . org-store-link))

ol-bbdb

ol-bbdb provides support for links to BBDB records in org-mode.

(use-feature ol-bbdb
  :after org bbdb ol
  :custom
  (org-bbdb-anniversary-field 'birthday))

org-protocol

org-protocol intercepts calls from emacsclient to trigger custom actions.

This section of the org-roam manual describes how to set up org-protocol on macOS. Note that emacs-mac supports org-protocol out of the box and doesn’t require turning on the Emacs server.

(use-feature org-protocol
  :after org)

ox

ox is the generic export engine for org-mode.

(use-feature ox
  :after org
  :defer t
  :custom
  (org-export-exclude-tags '("noexport" "ARCHIVE"))
  (org-export-with-broken-links 'mark) ; allow export with broken links
  (org-export-with-section-numbers nil) ; do not add numbers to section headings
  (org-export-with-toc nil) ; do not include table of contents
  (org-export-with-title nil) ; do not include title
  (org-export-headline-levels 4) ; include up to level 4 headlines
  (org-export-with-tasks nil) ; exclude headings with TODO keywords
  (org-export-with-todo-keywords nil) ; do not render TODO keywords in headings
  (org-export-preserve-breaks t) ; respect single breaks when exporting
  ;; (org-export-with-author nil "do not include author")
  ;; (org-export-with-date nil "do not include export date")
  ;; (org-html-validation-link nil "do not include validation link")
  (org-export-show-temporary-export-buffer nil)) ; bury temporary export buffers generated by `org-msg'

ox-html

ox-html is the HTML back-end for the org export engine.

(use-feature ox-html
  :after org ox
  :custom
  (org-html-postamble nil)) ; the three lines above unnecessary when this set to nil

ox-latex

ox-latex is the LaTeX back-end for the org export engine.

(use-feature ox-latex
  :after org ox
  :custom
  ;; get rid of temporary LaTeX files upon export
  (org-latex-logfiles-extensions (quote
                                  ("lof" "lot" "tex" "aux" "idx" "log" "out" "toc" "nav" "snm" "vrb" "dvi" "fdb_latexmk" "blg" "brf" "fls" "entoc" "ps" "spl" "bbl" "pygtex" "pygstyle"))))

ox-hugo

ox-hugo is an org-mode exporter back-end for Hugo.

Hugo should be able to export org-cite citations.

(use-package ox-hugo
  :after org ox
  :demand t
  :custom
  (org-hugo-default-section-directory "posts"))

ox-hugo-extras

ox-hugo-extras collects my extensions for ox-hugo.

(use-personal-package ox-hugo-extras
  :after ox-hugo
  :demand t)

stafforini

stafforini is a private package that provides Emacs commands for building and previewing my personal site, built with Hugo.

(use-package stafforini
  :ensure (:host github :repo "benthamite/stafforini.el")
  :defer t
  :bind (("A-H-s" . stafforini-menu)))

ox-pandoc

ox-pandoc is an org-mode exporter that uses Pandoc.

(use-package ox-pandoc
  :after org ox
  :defer t)

ox-gfm

ox-gfm is a Github Flavored Markdown org-mode exporter.

(use-package ox-gfm
  :after org ox
  :defer t)

ob

ob provides support for code blocks in org-mode.

Note to self: Typescript syntax highlighting works fine, but calling org-edit-special triggers an error; see the gptel conversation named typescript-syntax-highlighting.

(use-feature ob
  :after org
  :defer t
  :custom
  (org-confirm-babel-evaluate 'org-extras-confirm-babel-evaluate)
  (org-export-use-babel t)

  :config
  ;; enable lexical binding in code blocks
  (setcdr (assq :lexical org-babel-default-header-args:emacs-lisp) "no")
  ;; Prevent code-block evaluation during export while preserving noweb expansion
  (setf (alist-get :eval org-babel-default-header-args) "never-export")
  (setq org-babel-default-header-args:python
        '((:exports . "both")
          (:results . "replace output")
          (:session . "none")
          (:cache . "no")
          (:noweb . "no")
          (:hlines . "no")
          (:tangle . "no")))

  (org-babel-do-load-languages
   'org-babel-load-languages
   '((emacs-lisp . t)
     (shell . t)
     (python . t)
     (R . t)))

  (dolist (cons (list (cons "j" 'org-babel-next-src-block)
                      (cons "k" 'org-babel-previous-src-block)
                      (cons "n" 'org-babel-insert-header-arg)
                      (cons "p" 'org-babel-remove-result-one-or-many)))
    (add-to-list 'org-babel-key-bindings cons)))

ob-typescript

ob-typescript enables the execution of typescript code blocks.

(use-package ob-typescript
  :after ob
  :config
  (setq org-babel-default-header-args:typescript
        '((:mode . typescript-ts-mode)))
  (add-to-list 'org-babel-load-languages '(typescript . t))
  (add-to-list 'major-mode-remap-alist '(typescript-mode . typescript-ts-mode))
  (defalias 'typescript-mode 'typescript-ts-mode))

org-tempo

org-tempo provides completion templates for org-mode.

(use-feature org-tempo
  :after org)

org-src

org-src manages source code blocks in org-mode.

(use-feature org-src
  :after org
  :custom
  (org-edit-src-content-indentation 0)
  (org-src-preserve-indentation nil)
  (org-src-window-setup 'current-window)
  (org-src-tab-acts-natively nil) ; When set to `nil', newlines will be properly indented

  :bind
  (:map org-src-mode-map
   ("s-z" . org-edit-src-exit)))

org-table

org-table is the plain-text table editor for org-mode.

(use-feature org-table
  :after org
  :bind
  (:map org-table-fedit-map
   ("s-c" . org-table-fedit-finish)))

org-table-wrap

org-table-wrap provides visual word-wrapping for org mode tables.

(use-package org-table-wrap
  :ensure (:host github
                 :repo "benthamite/org-table-wrap")
  :after org-table
  :demand t
  :custom
  (org-table-wrap-width-fraction 0.8)

  :hook
  (org-mode-hook . org-table-wrap-mode))

orgtbl-edit

orgtbl-edit allows editing a spreadsheet or text-delimited file as an org table.

(use-package orgtbl-edit
  :ensure (:host github
                 :repo "shankar2k/orgtbl-edit")
  :after org-table
  :defer t)

orgtbl-join

orgtbl-join joins two org tables on a common column.

(use-package orgtbl-join
  :after org-table
  :defer t)

org-crypt

org-crypt encrypts the text under all headlines with a designated tag.

(use-feature org-crypt
  :after org
  :defer t
  :custom
  (org-crypt-key (getenv "PERSONAL_GMAIL"))
  (org-crypt-disable-auto-save t)

  :config
  (org-crypt-use-before-save-magic))

org-element

org-element implements a parser and an API for org syntax.

(use-feature org-element
  :after org
  :custom
  ;; set to nil to temporarily disable cache and avoid `org-element-cache' errors
  (org-element-use-cache t)

  :config
  ;; `org-capture' creates an indirect buffer with `clone', which copies
  ;; the base buffer's cache.  The cloned cache is immediately stale
  ;; because the capture buffer is narrowed and has the template inserted.
  ;; Reset it so the cache is rebuilt from scratch in the capture buffer.
  (add-hook 'org-capture-mode-hook #'org-element-cache-reset)

  ;; `org-element-at-point' wraps its cache recovery code in
  ;; `condition-case-unless-debug', which is disabled when
  ;; `debug-on-error' is non-nil.  This causes every cache error—stale
  ;; entries, invalid search bounds, missing parents—to enter the
  ;; debugger instead of being handled by Org's built-in reset-and-retry
  ;; logic.  Temporarily binding `debug-on-error' to nil lets the
  ;; existing recovery work while preserving the debugger for all other
  ;; code.
  (define-advice org-element-at-point (:around (fn &rest args) let-cache-recovery-work)
    (let ((debug-on-error nil))
      (apply fn args))))

org-lint

org-lint checks org files for common errors.

(use-feature org-lint
  :after org
  :defer t)

org-habit

org-habit tracks habits and displays consistency graphs in the agenda.

(use-feature org-habit
  :after org org-agenda
  :custom
  (org-habit-today-glyph #x1f4c5)
  (org-habit-completed-glyph #x2713)
  (org-habit-preceding-days 25)
  (org-habit-following-days 1)
  (org-habit-graph-column 85)
  (org-habit-show-habits nil)
  (org-habit-show-habits-only-for-today nil))

org-contrib

org-contrib features add-ons to org-mode.

(use-package org-contrib
  :after org)

org-checklist

org-checklist resets checkboxes in repeating org entries.

Allows reset of checkboxes in recurring tasks. This works only on headings that have the property RESET_CHECK_BOXES set to t. You can set the property of a heading by invoking the command org-set-property with point on that heading or immediately under it.

(use-feature org-checklist
  :after org-contrib
  :defer 30)

org-make-toc

org-make-toc generates automatic tables of contents for org files.

(use-package org-make-toc
  :after org
  :defer 30)

org-journal

org-journal is an org-mode based journaling mode.

(use-package org-journal
  :after org
  :custom
  (org-journal-dir paths-dir-journal)
  (org-journal-date-format "%A, %d %B %Y")
  (org-journal-file-format "%Y-%m-%d.org")

  :config
  (defun org-journal-new-entry-in-journal ()
    "Create a new journal entry in the selected journal."
    (interactive)
    (let* ((journal-dirs (list paths-dir-tlon-todos paths-dir-journal))
           (cons (mapcar (lambda (dir)
                           (cons (file-name-nondirectory (directory-file-name dir)) dir))
                         journal-dirs))
           (choice (completing-read "Journal: " cons))
           (org-journal-dir (alist-get choice cons nil nil 'string=)))
      (org-journal-new-entry nil)))

  :bind
  ("A-j" . org-journal-new-entry-in-journal))

org-contacts

org-contacts is a contacts management system for Org Mode.

(use-package org-contacts
  :ensure (:build (:not elpaca-check-version))
  :after org tlon
  :defer t
  :custom
  (org-contacts-files `(,(file-name-concat paths-dir-tlon-repos "babel-core/contacts.org"))))

org-vcard

org-vcard imports and exports vCards from within org-mode.

(use-package org-vcard
  :defer t
  :custom
  ;; optimized for macOS Contacts.app
  (org-vcard-styles-languages-mappings
   '(("flat"
      (("en"
        (("3.0"
          (("ADDRESS_HOME" . "ADR;TYPE=\"home\";PREF=1")
           ("ADDRESS_HOME" . "ADR;TYPE=\"home\"")
           ("BIRTHDAY" . "BDAY")
           ("EMAIL" . "EMAIL;PREF=1")
           ("FN" . "FN")
           ("PHOTO" . "PHOTO;TYPE=JPEG")
           ("URL" . "item1.URL;type=pref")))
         ("4.0"
          (("ADDRESS_HOME" . "ADR;TYPE=\"home\";PREF=1")
           ("ADDRESS_HOME" . "ADR;TYPE=\"home\"")
           ("BIRTHDAY" . "BDAY")
           ("EMAIL" . "EMAIL;PREF=1")
           ("FN" . "FN")
           ("PHOTO" . "PHOTO;TYPE=JPEG")
           ("URL" . "URL;PREF=1")))
         ("2.1"
          (("ADDRESS_HOME" . "ADR;HOME;PREF")
           ("ADDRESS_HOME" . "ADR;HOME")
           ("BIRTHDAY" . "BDAY")
           ("EMAIL" . "EMAIL;PREF")
           ("FN" . "FN")
           ("PHOTO" . "PHOTO;TYPE=JPEG")
           ("URL" . "URL;PREF")))))))))

  :config
  (defun org-vcard--save-photo-and-make-link (base64-data contact-name)
    "Save base64 photo data to file and return org link."
    (let* ((photo-dir (expand-file-name "contact-photos" user-emacs-directory))
           (safe-name (replace-regexp-in-string "[^a-zA-Z0-9]" "_" contact-name))
           (filename (expand-file-name (concat safe-name ".jpg") photo-dir)))
      (unless (file-directory-p photo-dir)
        (make-directory photo-dir t))
      (with-temp-file filename
        (set-buffer-multibyte nil)  ; Use unibyte mode for binary data
        (let ((coding-system-for-write 'binary))
          ;; Remove potential MIME type header if present
          (when (string-match "^data:image/[^;]+;base64," base64-data)
            (setq base64-data (substring base64-data (match-end 0))))
          ;; Process base64 data in chunks
          (let ((chunk-size 4096)
                (start 0)
                (total-length (length base64-data)))
            (while (< start total-length)
              (let* ((end (min (+ start chunk-size) total-length))
                     (chunk (substring base64-data start end)))
                (insert (base64-decode-string chunk))
                (setq start end))))))
      (format "\n#+ATTR_ORG: :width 300\n[[file:%s]]\n" filename)))

  (advice-add 'org-vcard--transfer-write :around
              (lambda (orig-fun direction content destination)
                "Convert PHOTO properties to image links before writing."
                (if (eq direction 'import)
                    (let ((modified-content
                           (with-temp-buffer
                             (insert content)
                             (goto-char (point-min))
                             (while (re-search-forward "^:PHOTO: \\(.+\\)$" nil t)
                               (let* ((base64-data (match-string 1))
                                      (heading (save-excursion
                                                 (re-search-backward "^\\* \\(.+\\)$" nil t)
                                                 (match-string 1)))
                                      (photo-link (condition-case err
                                                      (org-vcard--save-photo-and-make-link
                                                       base64-data heading)
                                                    (error
                                                     (message "Error processing photo for %s: %s"
                                                              heading (error-message-string err))
                                                     ":PHOTO: [Error processing photo]\n"))))
                                 (delete-region (line-beginning-position) (line-end-position))
                                 ;; Delete any following empty line
                                 (when (looking-at "\n")
                                   (delete-char 1))
                                 (save-excursion
                                   (re-search-forward ":END:\n" nil t)
                                   (insert photo-link))))
                             (buffer-string))))
                      (funcall orig-fun direction modified-content destination))
                  (funcall orig-fun direction content destination)))))

org-autosort

org-autosort sorts entries in org files automatically.

(use-package org-autosort
  :ensure (:host github
                 :repo "yantar92/org-autosort")
  :after org
  :defer 30)

ox-clip

ox-clip copies selected regions in org-mode as formatted text on the clipboard.

(use-package ox-clip
  :after org
  :defer t
  :custom
  ;; github.com/jkitchin/ox-clip/issues/13
  (ox-clip-osx-cmd
   '(("HTML" . "hexdump -ve '1/1 \"%.2x\"' | xargs printf \"set the clipboard to {text:\\\" \\\", «class HTML»:«data HTML%s»}\" | osascript -")
     ("Markdown" . "pandoc --wrap=none -f html -t \"markdown-smart+hard_line_breaks\" - | grep -v \"^:::\" | sed 's/{#.*}//g' | sed 's/\\\\`/`/g' | pbcopy"))))

elgantt

elgantt is a gantt chart for org mode.

(use-package elgantt
  :ensure (:host github
           :repo "legalnonsense/elgantt")
  :after org
  :defer t)

org-pomodoro

org-pomodoro provides org-mode support for the Pomodoro technique.

(use-package org-pomodoro
  :after org org-agenda
  :custom
  (org-pomodoro-length 25)
  (org-pomodoro-short-break-length 5)
  (org-pomodoro-long-break-length org-pomodoro-short-break-length)
  (org-pomodoro-finished-sound "/System/Library/Sounds/Blow.aiff")
  (org-pomodoro-long-break-sound org-pomodoro-finished-sound)
  (org-pomodoro-short-break-sound org-pomodoro-finished-sound)

  :hook
  (org-pomodoro-started-hook . org-extras-pomodoro-format-timer)
  (org-pomodoro-started-hook . tab-bar-extras-disable-all-notifications)
  (org-pomodoro-started-hook . org-extras-narrow-to-entry-and-children)
  (org-pomodoro-finished-hook . tab-bar-extras-enable-all-notifications)
  (org-pomodoro-finished-hook . widen)

  :bind
  (("H-I" . org-pomodoro)
   ("M-s-e" . org-pomodoro-extend-last-clock)))

org-pomodoro-extras

org-pomodoro-extras collects my extensions for org-pomodoro.

(use-personal-package org-pomodoro-extras
  :after org-pomodoro)

org-percentile

org-percentile is a productivity tool that let’s you compete with your past self.

(use-package org-percentile
  :disabled
  :ensure (:host github
                 :repo "benthamite/org-percentile")
  :after org-clock
  :custom
  (org-percentile-data-file (file-name-concat paths-dir-dropbox "misc/org-percentile-data.el"))

  :config
  (org-percentile-mode))

note-taking

org-roam

org-roam is a Roam replica with org-mode.

(use-package org-roam
  :ensure (:host github
                 :repo "benthamite/org-roam"
                 :branch "fix/handle-nil-db-version") ; https://github.com/org-roam/org-roam/pull/2609
  :after org
  :custom
  (org-roam-directory paths-dir-org-roam)
  (org-roam-node-display-template
   (concat "${title:*} "
           (propertize "${tags:10}" 'face 'org-tag)))
  (org-roam-node-display-template
   (concat "${hierarchy:160} "
           (propertize "${tags:20}" 'face 'org-tag)))
  ;; exclude selected headings based on other criteria
  (org-roam-db-node-include-function #'org-roam-extras-node-include-function)

  :config
  (with-eval-after-load 'org-roam-bibtex
    (setopt org-roam-mode-sections (append org-roam-mode-sections '(orb-section-reference orb-section-abstract))))

  ;; include transcluded links in `org-roam' backlinks
  (delete '(keyword "transclude") org-roam-db-extra-links-exclude-keys)

  ;; https://github.com/org-roam/org-roam/issues/2550#issuecomment-3451456331
  (with-eval-after-load 'org-roam-capture
    (setopt org-roam-capture-new-node-hook nil))

  :bind
  (:map org-mode-map
        ("s-i" . org-roam-node-insert)
        :map org-roam-mode-map
        ("f" . ace-link-org)))

org-roam-extras

org-roam-extras collects my extensions for org-roam.

(use-personal-package org-roam-extras
  :custom
  (org-roam-extras-auto-show-backlink-buffer t)

  :config
  ;; exclude headings in specific files and directories
  ;; Set here rather than in :custom so that `org-roam-extras-excluded-dirs'
  ;; and `org-roam-extras-excluded-files' are guaranteed to be defined (they
  ;; are void if org-roam loads first, e.g. via a citar idle timer).
  (setopt org-roam-file-exclude-regexp
          (let (result)
            (dolist (dir-or-file
                     (append
                      org-roam-extras-excluded-dirs
                      org-roam-extras-excluded-files)
                     (regexp-opt result))
              (push (if (file-directory-p dir-or-file)
                        (file-relative-name dir-or-file paths-dir-org-roam)
                      dir-or-file)
                    result))))
  (org-roam-extras-setup-db-sync)

  :hook
  (org-capture-prepare-finalize-hook . org-roam-extras-remove-file-level-properties)

  :bind
  (("H-N" . org-roam-extras-new-note)
   ("H-j" . org-roam-extras-node-find)
   ("H-J" . org-roam-extras-node-find-special)))

org-roam-ui

org-roam-ui is a graphical frontend for exploring org-roam.

(use-package org-roam-ui
  :ensure (:host github
                 :repo "org-roam/org-roam-ui"
                 :branch "main"
                 :files ("*.el" "out"))
  :after org-roam
  :defer t
  :custom
  (org-roam-ui-sync-theme t)
  (org-roam-ui-follow t)
  (org-roam-ui-update-on-save nil)
  (org-roam-ui-open-on-start nil))

org-transclusion

org-transclusion supports transclusion with org-mode.

(use-package org-transclusion
  :after org
  :defer t
  :config
  (dolist (element '(headline drawer property-drawer))
    (push element org-transclusion-exclude-elements))

  (face-spec-set 'org-transclusion-fringe
                 '((((background light))
                    :foreground "black")
                   (t
                    :foreground "white"))
                 'face-override-spec)
  (face-spec-set 'org-transclusion-source-fringe
                 '((((background light))
                    :foreground "black")
                   (t
                    :foreground "white"))
                 'face-override-spec))

vulpea

vulpea is a collection of functions for note-taking based on org and org-roam.

I use this package to define a dynamic agenda, as explained and illustrated here. I’ve made some changes to the system in that link, specifically to exclude files and directories at various stages:

  1. At the broadest level, I exclude files and directories from the function (org-extras-id-auto-add-ids-to-headings-in-file) that otherwise automatically adds an ID to every org heading in a file-visiting buffer. Headings so excluded are not indexed by org-roam, because a heading requires an ID to be indexed. For details, see that function’s docstring. For examples of how this is used in my config, see the variables org-extras-id-auto-add-excluded-files and org-extras-id-auto-add-excluded-directories under the org-id section of this file.
  2. I then exclude some headings with IDs from the org-roam database. For examples of how this is used in my config, see the variables org-roam-file-exclude-regexp and org-roam-db-node-include-function under the org-roam section of this file.
  3. Finally, I selectively include in org-agenda-files files that satisfy certain conditions (as defined by vulpea-extras-project-p) and files modified recently (as specified by org-roam-extras-recent), and exclude from org-agenda-files files listed in org-extras-agenda-files-excluded.
(use-package vulpea
  :after org org-roam)

vulpea-extras

vulpea-extras collects my extensions for vulpea.

(use-personal-package vulpea-extras
  :hook
  ((find-file-hook before-save-hook) . vulpea-extras-project-update-tag))

org-noter

org-noter is an org-mode document annotator.

(use-package org-noter
  :ensure (:host github
                 :repo "org-noter/org-noter")
  :after org-extras
  :init
  (with-eval-after-load 'pdf-annot
    (bind-keys :map pdf-annot-minor-mode-map
               ("s-s" . org-noter-create-skeleton)))

  :custom
  (org-noter-notes-search-path `(,paths-dir-bibliographic-notes))
  (org-noter-auto-save-last-location t)
  (org-noter-always-create-frame nil)
  (org-noter-separate-notes-from-heading t)
  (org-noter-kill-frame-at-session-end nil)
  (org-noter-use-indirect-buffer nil)

  :config
  (push paths-file-orb-noter-template org-extras-id-auto-add-excluded-files)

  :bind
  (:map org-noter-notes-mode-map
        ("s-n" . org-noter-sync-current-note)))

org-noter-extras

org-noter-extras collects my extensions for org-noter.

(use-personal-package org-noter-extras
  :after org-noter
  :demand t
  :bind
  (:map org-noter-notes-mode-map
   ("s-a" . org-noter-extras-cleanup-annotation)
   ("s-d" . org-noter-extras-dehyphenate)
   ("s-k" . org-noter-extras-sync-prev-note)
   ("s-l" . org-noter-extras-sync-next-note)
   ("s-o" . org-noter-extras-highlight-offset)))

spaced-repetition

anki-editor

anki-editor is a minor mode for making Anki cards with Org Mode.

The original package is abandoned, but there is an actively maintained fork.

(use-package anki-editor
  :ensure (:host github
                 :repo "anki-editor/anki-editor")
  :defer t
  :custom
  ;; https://github.com/anki-editor/anki-editor/issues/116
  (anki-editor-export-note-fields-on-push nil)
  (anki-editor-org-tags-as-anki-tags nil))

anki-editor-extras

anki-editor-extras collects my extensions for anki-editor.

(use-personal-package anki-editor-extras
  :after anki-editor
  :demand t)

ankiorg

ankiorg is an anki-editor add-on which pulls Anki notes to Org.

(use-package ankiorg
  :ensure (:host github :repo "orgtre/ankiorg")
  :demand t
  :after anki-editor
  :custom
  (ankiorg-sql-database
   (file-name-concat no-littering-var-directory "ankiorg/collection.anki2"))
  (ankiorg-media-directory
   no-littering-var-directory "ankiorg/img")
   (ankiorg-pick-deck-all-directly t))

anki-noter

anki-noter AI-powered Anki notes generator.

(use-package anki-noter
  :ensure (:host github
                 :repo "benthamite/anki-noter")
  :defer t)

reference & citation

See this section of citar’s manual for a handy summary of the main bibliographic and citation Emacs packages.

I split my bibliographies into two categories: personal and work. The files providing my personal bibliography are defined in paths-files-bibliography-personal. The files providing my work bibliography are defined in tlon-bibliography-files. I then define paths-files-bibliography-all as the concatenation of these two lists. Finally, this master variable is used to set the values of the user options for all package that define bibliographies:

  • bibtex-files (for bibtext)
  • bibtex-completion-bibliography (for bibtex-completion)
  • citar-bibliography (for citar)
  • ebib-preload-bib-files (for ebib)

Each of these packages requires tlon, since the latter must load for paths-files-bibliography-all to be set.

oc

oc is Org mode’s built-in citation handling library.

(use-feature oc
  :after org el-patch
  :defer t
  :custom
  (org-cite-insert-processor 'citar)
  (org-cite-follow-processor 'citar) ; `org-open-at-point' integration
  (org-cite-activate-processor 'citar) ;
  (org-cite-export-processors
   '((t . (csl "long-template.csl")))))

oc-csl

oc-csl is a CSL citation processor for Org mode’s citation system.

(use-feature oc-csl
  :after oc
  :defer t
  :custom
  (org-cite-csl-styles-dir paths-dir-tlon-csl-styles)
  (org-cite-csl-locales-dir paths-dir-tlon-csl-locales))

org-footnote

org-footnote provides footnote support in org-mode.

(use-feature org-footnote
  :defer t
  :custom
  (org-footnote-auto-adjust t))

citeproc

citeproc is a CSL 1.0.2 Citation Processor for Emacs.

(use-package citeproc
  :after oc
  :defer t)

bibtex

bibtex is major mode for editing and validating BibTeX .bib files.

(use-feature bibtex
  :after ebib
  :custom
  (bibtex-files paths-files-bibliography-all)
  ;; This corresponds (roughly?) to `auth+year+shorttitle(3,3)' on Better BibTeX
  ;; retorque.re/zotero-better-bibtex/citing/
  (bibtex-search-entry-globally t)
  (bibtex-autokey-names 1)
  (bibtex-autokey-name-case-convert-function 'capitalize)
  (bibtex-autokey-year-length 4)
  (bibtex-autokey-year-title-separator "")
  (bibtex-autokey-title-terminators "[.!?;]\\|--")
  (bibtex-autokey-titlewords 3)
  (bibtex-autokey-titlewords-stretch 0)
  (bibtex-autokey-titleword-case-convert-function 'capitalize)
  (bibtex-autokey-titleword-length nil)
  (bibtex-autokey-titleword-separator "")
  (bibtex-autokey-titleword-ignore '("A" "a" "An" "an" "On" "on" "The" "the" "Eine?" "Der" "Die" "Das" "El" "La" "Lo" "Los" "Las" "Un" "Una" "Unos" "Unas" "el" "la" "lo" "los" "las" "un" "una" "unos" "unas" "y" "o" "Le" "La" "L'" "Les" "Un" "Une" "Des" "Du" "De la" "De l'" "Des" "le" "la" "l'" "les" "un" "une" "des" "du" "de la" "de l'" "des" "Lo" "Il" "La" "L'" "Gli" "I" "Le" "Uno" "lo" "il" "la" "l'" "gli" "i" "le" "uno"))
  ;; Remove accents
  (bibtex-autokey-before-presentation-function 'simple-extras-asciify-string)
  ;; check tweaked version of `bibtex-format-entry' above
  (bibtex-entry-format '(opts-or-alts-fields last-comma delimiters page-dashes))
  (bibtex-field-indentation 8) ; match ebib value

  :config
  (require 'tlon) ; see explanatory note under ‘reference & citation’
  (push '("\\." . "") bibtex-autokey-name-change-strings)
  ;; add extra entry types
  (dolist (entry '(("Video" . "Video file")
                   ("Movie" . "Film")
                   ("tvepisode" . "TV episode")))
    (push `(,(car entry) ,(cdr entry)
            (("author" nil nil 0)
             ("title")
             ("date" nil nil 1)
             ("year" nil nil -1)
             ("url" nil nil 2))
            nil
            (("abstract")
             ("keywords")
             ("language")
             ("version")
             ("rating")
             ("letterboxd")
             ("note")
             ("organization")
             ("eprintclass" nil nil 4)
             ("primaryclass" nil nil -4)
             ("eprinttype" nil nil 5)
             ("archiveprefix" nil nil -5)
             ("urldate")))
          bibtex-biblatex-entry-alist))

  :bind
  (:map bibtex-mode-map
        ("s-f" . ebib-extras-open-file-dwim)
        ("s-/" . ebib-extras-attach-most-recent-file)
        ("s-a" . bibtex-set-field)
        ("s-c" . bibtex-copy-entry-as-kill)
        ("s-v" . bibtex-yank)
        ("s-x" . bibtex-kill-entry)
        ("A-C-H-x" . bibtex-copy-entry-as-kill)
        ("A-C-H-c" . bibtex-kill-entry)
        ("A-C-H-a" . bibtex-copy-field-as-kill)
        ("A-C-H-f" . bibtex-kill-field)
        ("A-C-s-r" . bibtex-previous-entry)
        ("A-C-s-f" . bibtex-next-entry)))

bibtex-extras

bibtex-extras collects my extensions for bibtex.

(use-personal-package bibtex-extras
  :after bibtex
  :demand t
  :custom
  (bibtex-maintain-sorted-entries
   '(bibtex-extras-entry-sorter bibtex-extras-lessp))

  :config
  ;; Replace 'online' entry type
  (bibtex-extras-replace-element-by-name
   bibtex-biblatex-entry-alist
   "Online" '("Online" "Online Resource"
              (("author" nil nil 0) ("title") ("journaltitle" nil nil 3)
               ("date" nil nil 1) ("year" nil nil -1)
               ("doi" nil nil 2) ("url" nil nil 2))
              nil
              (("subtitle") ("language") ("version") ("note")
               ("organization") ("month")
               ("pubstate") ("eprintclass" nil nil 4) ("primaryclass" nil nil -4)
               ("eprinttype" nil nil 5) ("archiveprefix" nil nil -5) ("urldate"))))

  (add-to-list 'bibtex-biblatex-entry-alist
               '("Performance" "A performance entry"
                 (("author") ("title") ("date")) ; Required fields
                 nil ; Crossref fields
                 (("venue") ("location") ("note"))))
  :bind
  (:map bibtex-mode-map
        ("s-a" . bibtex-extras-set-field)
        ("s-d" . bibtex-extras-url-to-pdf-attach)
        ("s-h" . bibtex-extras-url-to-html-attach)
        ("s-i" . bibtex-extras-open-in-ebib)
        ("s-t" . bibtex-extras-move-entry-to-tlon)))

bibtex-completion

bibtex-completion is a backend for searching and managing bibliographies in Emacs.

The package is required by org-roam-bibtex.

(use-package bibtex-completion
  :ensure (:version (lambda (_) "2.0.0")) ; github.com/progfolio/elpaca/issues/229
  :after bibtex
  :custom
  (bibtex-completion-bibliography paths-files-bibliography-all)
  (bibtex-completion-pdf-open-function 'find-file)
  (bibtex-completion-notes-path paths-dir-bibliographic-notes)
  (bibtex-completion-pdf-field "file")
  (bibtex-dialect 'biblatex)
  (bibtex-completion-library-path paths-dir-pdf-library)

  :config
  (require 'tlon)) ; see explanatory note under ‘reference & citation’

bibtex-completion-extras

bibtex-completion-extras collects my extensions for bibtex-completion.

(use-personal-package bibtex-completion-extras
  :after bibtex-completion)

org-roam-bibtex

org-roam-bibtex integrates org-roam and bibtex.

(use-package org-roam-bibtex
  :after bibtex-completion org-roam
  :custom
  (orb-roam-ref-format 'org-cite)
  (orb-insert-interface 'citar-open-notes)
  (orb-note-actions-interface 'default)
  (orb-attached-file-extensions '("pdf"))
  (org-roam-capture-templates
   `(("r" "bibliography reference" plain
      (file ,paths-file-orb-noter-template)
      :if-new
      (file ,paths-file-orb-capture-template)
      :unnarrowed t :immediate-finish t :jump-to-captured t)))

  :config
  (dolist (keyword '("year" "title" "url" "keywords"))
    (add-to-list 'orb-preformat-keywords keyword))
  (org-roam-bibtex-mode)
  ;; https://github.com/org-roam/org-roam/issues/2550#issuecomment-3451456331
  (setq org-roam-capture-new-node-hook nil))

citar

citar is a package to quickly find and act on bibliographic references, and edit org, markdown, and latex academic documents.

We defer-load the package to activate the timer that in turn updates the bibliography files when Emacs is idle, like we do with ebib below.

(use-package citar
  :ensure (:host github
                 :repo "emacs-citar/citar"
                 :includes (citar-org))
  :defer 30
  :custom
  (citar-bibliography paths-files-bibliography-all)
  (citar-notes-paths `(,paths-dir-bibliographic-notes))
  (citar-at-point-function 'embark-act)
  (citar-symbol-separator "  ")
  (citar-format-reference-function 'citar-citeproc-format-reference)
  (citar-templates '((main . "${author editor:30%sn}   ${date year issued:4}   ${title:60}   ${database:10}")
                     (suffix . "   ${=key= id:25}    ${=type=:12}")
                     (preview . "${author editor:%etal} (${year issued date}) ${title}, ${journal journaltitle publisher container-title collection-title}.\n")
                     (note . "Notes on ${author editor:%etal}, ${title}")))
  (citar-notes-source 'orb-citar-source)

  :config
  (require 'tlon) ; see explanatory note under ‘reference & citation’
  (with-eval-after-load 'savehist
    (add-to-list 'savehist-additional-variables 'citar-history))

  (require 'citar-org-roam)
  (citar-register-notes-source
   'orb-citar-source (list :name "Org-Roam Notes"
                           :category 'org-roam-node
                           :items #'citar-org-roam--get-candidates
                           :hasitems #'citar-org-roam-has-notes
                           :open #'citar-org-roam-open-note
                           :create #'orb-citar-edit-note
                           :annotate #'citar-org-roam--annotate))

  ;; allow invocation of `citar-insert-citation' in any buffer. Although it is
  ;; not possible to insert citations in some modes, it is still useful to be
  ;; able to run this command because of the `embark' integration
  (setf (alist-get 't citar-major-mode-functions)
        (cons '(insert-citation . citar-org-insert-citation)
              (alist-get 't citar-major-mode-functions)))

  :bind
  (("H-/" . citar-insert-citation)
   :map citar-map
   ("c" . embark-copy-as-kill)
   ("u" . citar-open-links)
   ("s" . ebib-extras-search-dwim)
   ("t" . citar-extras-move-entry-to-tlon)
   ("b" . citar-extras-goto-bibtex-entry)
   ("i" . citar-extras-open-in-ebib)
   :map citar-citation-map
   ("c" . embark-copy-as-kill)
   ("u" . citar-open-links)
   ("s" . ebib-extras-search-dwim)
   ("t" . citar-extras-move-entry-to-tlon)
   ("b" . citar-extras-goto-bibtex-entry)
   ("i" . citar-extras-open-in-ebib)))

citar-extras

citar-extras collects my extensions for citar.

(use-personal-package citar-extras
  :after citar
  :config
  ;; https://github.com/emacs-citar/citar/wiki/Indicators
  (setq citar-indicators
        (list citar-extras-indicator-links-icons
              citar-extras-indicator-files-icons
              citar-extras-indicator-notes-icons
              citar-extras-indicator-cited-icons)))

citar-citeproc

citar-citeproc provides Citeproc reference support for citar.

(use-feature citar-citeproc
  :after citar citeproc citar-extras tlon
  :custom
  (citar-citeproc-csl-styles-dir paths-dir-tlon-csl-styles)
  (citar-citeproc-csl-locales-dir paths-dir-tlon-csl-locales))

citar-embark

citar-embark adds contextual access actions in the minibuffer and at-point via the citar-embark-mode minor mode.

(use-package citar-embark
  :after citar embark
  :config
  (citar-embark-mode))

citar-org-roam

citar-org-roam provides integration between citar and org-roam.

(use-package citar-org-roam
  :ensure (:host github
           :repo "emacs-citar/citar-org-roam")
  :after citar org-roam)

org-ref

org-ref supports citations, cross-references, bibliographies in org-mode and useful bibtex tools.

I use this package only to run the cleanup function org-ref-clean-bibtex-entry after adding new entries to my bibliography and to occasionally call a few miscellaneous commands. I do not use any of its citation-related functionality, since I use org-cite for that.

(use-package org-ref
  :after zotra
  :custom
  (org-ref-bibtex-pdf-download-dir paths-dir-downloads)
  (org-ref-insert-cite-function
   (lambda ()
     (org-cite-insert nil)))

  :config
  (dolist (fun '(org-ref-replace-nonascii
                 orcb-check-journal
                 orcb-download-pdf))
    (delete fun org-ref-clean-bibtex-entry-hook)))

org-ref-extras

org-ref-extras collects my extensions for org-ref.

(use-personal-package org-ref-extras
  :after org-ref)

ebib

ebib (homepage) is a BibTeX database manager for Emacs.

We defer-load the package to activate the timer that in turn updates the bibliography files when Emacs is idle, like we do with citar above.

(use-package ebib
  :custom
  (ebib-preload-bib-files paths-files-bibliography-all)
  (ebib-notes-directory paths-dir-bibliographic-notes)
  (ebib-notes-use-org-capture t)
  (ebib-notes-display-max-lines 9999)
  (ebib-filename-separator ";")
  (ebib-file-associations nil) ; do not open any file types externally
  (ebib-layout 'index-only)
  (ebib-bibtex-dialect 'biblatex)
  (ebib-use-timestamp t)
  (ebib-timestamp-format "%Y-%m-%d %T (%Z)")
  (ebib-default-entry-type "online")
  (ebib-uniquify-keys t)
  (ebib-index-columns '(("Entry Key" 30 t)
                        ("Author/Editor" 25 t)
                        ("Year" 4 t)
                        ("Title" 50 t)))
  (ebib-extra-fields
   '((biblatex "abstract" "keywords" "origdate" "langid" "translation" "narrator" "file" "timestamp" "wordcount" "rating" "crossref" "=key=")
     (bibtex "crossref" "annote" "abstract" "keywords" "file" "timestamp" "url" "doi")))

  :config
  (require 'tlon) ; see explanatory note under ‘reference & citation’

  :hook
  (ebib-entry-mode-hook . visual-line-mode)

  :bind
  (:map ebib-multiline-mode-map
        ("s-c" . ebib-quit-multiline-buffer-and-save)
        :map ebib-index-mode-map
        ("<return>" . ebib-edit-entry)
        ("A" . ebib-add-entry)
        ("D" . ebib-delete-entry)
        ("f" . avy-extras-ebib-view-entry)
        ("k" . ebib-prev-entry)
        ("l" . ebib-next-entry)
        ("H-s" . ebib-save-current-database)
        ("K" . ebib-copy-key-as-kill)
        ("Q" . ebib-quit)
        ("W" . zotra-download-attachment)
        :map ebib-entry-mode-map
        ("TAB" . ebib-goto-next-set)
        ("<backtab>" . ebib-goto-prev-set)
        ("H-s" . ebib-save-current-database)
        ("H-S" . ebib-save-all-databases)
        ("!" . ebib-generate-autokey)
        ("A" . ebib-add-field)
        ("c" . ebib-copy-current-field-contents)
        ("D" . ebib-delete-current-field-contents)
        ("E" . ebib-edit-keyname)
        ("H-s" . ebib-save-current-database)
        ("K" . ebib-copy-key-as-kill)
        ("Q" . ebib-quit)
        ("W" . zotra-download-attachment)))

The macro below generates the commands correctly. But attempting to define key bindings results in duplicate commands. I’m not sure what’s on; it seems to be related to use-package.

ebib-utils

ebib-utils provides internal utility functions for ebib.

(use-feature ebib-utils
  :after ebib
  :custom
  (ebib-hidden-fields ; unhide some fields
        (cl-remove-if
         (lambda (el)
           (member el '("edition" "isbn" "timestamp" "titleaddon" "translator")))
         ebib-hidden-fields))

  :config
  (add-to-list 'ebib-hidden-fields "year")) ; hide others

ebib-extras

ebib-extras collects my extensions for ebib.

(use-personal-package ebib-extras
  :init
  (advice-add 'ebib-init :after #'ebib-extras-auto-reload-databases)

  :custom
  (ebib-extras-attach-existing-file-action 'overwrite)

  :hook
  (ebib-add-entry . ebib-extras-create-list-of-existing-authors)

  :bind
  (("A-i" . ebib-extras-open-or-switch)
   :map ebib-index-mode-map
   ("," . ebib-extras-prev-entry)
   ("." . ebib-extras-next-entry)
   ("d" . ebib-extras-duplicate-entry)
   ("n" . ebib-extras-citar-open-notes)
   ("A-C-s-<tab>" . ebib-extras-end-of-index-buffer)
   ("s" . ebib-extras-sort)
   :map ebib-entry-mode-map
   ("s-f" . ebib-extras-open-file-dwim)
   ("," . ebib-extras-prev-entry)
   ("." . ebib-extras-next-entry)
   ("d" . ebib-extras-duplicate-entry)
   ("n" . ebib-extras-citar-open-notes)
   ("SPC" . ebib-extras-open-file-dwim)
   ("/" . ebib-extras-attach-most-recent-file)
   ("?" . ebib-extras-attach-file)
   (";" . ebib-extras-process-entry)
   ("a" . ebib-extras-search-amazon)
   ("b" . ebib-extras-get-or-open-entry)
   ("g" . ebib-extras-search-library-genesis)
   ("G" . ebib-extras-search-goodreads)
   ("h" . ebib-extras-open-html-file)
   ("H" . ebib-extras-open-html-file-externally)
   ("I" . ebib-extras-set-id)
   ("o" . ebib-extras-search-connected-papers)
   ("p" . ebib-extras-open-pdf-file)
   ("P" . ebib-extras-open-pdf-file-externally)
   ("R" . ebib-extras-set-rating)
   ("s" . ebib-extras-search-dwim)
   ("T" . ebib-extras-no-translation-found)
   ("u" . ebib-extras-browse-url-or-doi)
   ("V" . ebib-extras-search-internet-archive)
   ("x" . ebib-extras-search-university-of-toronto)
   ("y" . ebib-extras-search-hathitrust)
   ("z" . ebib-extras-search-google-scholar)
   ("s-d" . ebib-extras-url-to-pdf-attach)
   ("s-k" . ebib-extras-fetch-keywords)
   ("s-h" . ebib-extras-url-to-html-attach)
   ("s-r" . ebib-extras-rename-files)))

bib

bib fetches bibliographic metadata from various APIs.

(use-package bib
  :ensure (:host github
                 :repo "benthamite/bib"
                 :depth nil) ; clone entire repo, not just last commit
  :after ebib
  :defer t
  :custom
  (bib-isbndb-key
   (auth-source-pass-get "key" (concat "tlon/babel/isbndb.com/" tlon-email-shared)))
  (bib-omdb-key
   (auth-source-pass-get 'secret "chrome/omdbapi.com"))
  (bib-tmdb-key
   (auth-source-pass-get "key" "chrome/themoviedb.org/stafforini"))

  :bind
  (:map ebib-index-mode-map
        ("t" . bib-zotra-add-entry-from-title)))

zotra

zotra provides functions to get bibliographic information from a URL via Zotero translators, but without relying on the Zotero client.

(use-package zotra
  :ensure (:host github
                 :repo "mpedramfar/zotra")
  :defer t
  :custom
  (zotra-use-curl nil)
  (zotra-url-retrieve-timeout 15)
  (zotra-default-entry-format "biblatex")
  (zotra-download-attachment-default-directory paths-dir-downloads)
  (zotra-backend 'zotra-server)
  (zotra-local-server-directory (file-name-concat paths-dir-external-repos "zotra-server/")))

zotra-extras

zotra-extras collects my extensions for zotra.

(use-personal-package zotra-extras
  :after ebib eww
  :custom
  (zotra-extras-use-mullvad-p t)

  :hook
  (zotra-after-get-bibtex-entry-hook . zotra-extras-after-add-process-bibtex)

  :bind
  (:map ebib-index-mode-map
        ("a" . zotra-extras-add-entry))
  (:map eww-mode-map
        ("a" . zotra-extras-add-entry)))

annas-archive

annas-archive provides rudimentary integration for Anna’s Archive, the largest existing search engine for shadow libraries.

(use-package annas-archive
  :ensure (:host github
                 :repo "benthamite/annas-archive")
  :defer t
  :init
  (with-eval-after-load 'ebib
    (bind-keys :map ebib-entry-mode-map
               ("s-a" . annas-archive-download)))
  (with-eval-after-load 'eww
    (bind-keys :map 'eww-mode-map
               ("s-a" . annas-archive-download-file)))

  :custom
  (annas-archive-secret-key (auth-source-pass-get 'secret "tlon/core/annas-archive"))
  (annas-archive-included-file-types '("pdf"))
  (annas-archive-title-column-width 130))

email

simple

simple configures the mail user agent.

(use-feature simple
  :custom
  (mail-user-agent 'mu4e-user-agent)
  (read-mail-command 'mu4e))

sendmail

sendmail is a mode that provides mail-sending facilities from within Emacs.

(use-feature sendmail
  :after (:any mu4e org-msg)
  :custom
  (send-mail-function 'smtpmail-send-it))

smtpmail

smtpmail provides SMTP mail sending support.

(use-feature smtpmail
  :after (:any mu4e org-msg)
  :custom
  (smtpmail-smtp-user (getenv "PERSONAL_GMAIL"))
  (smtpmail-local-domain "gmail.com")
  (smtpmail-default-smtp-server "smtp.gmail.com")
  (smtpmail-smtp-server "smtp.gmail.com")
  (smtpmail-smtp-service 465)
  (smtpmail-stream-type 'ssl))

message

message is a message composition mode.

(use-feature message
  :after (:any mu4e org-msg)
  :demand t
  :custom
  (message-kill-buffer-on-exit t) ; make `message-send-and-exit' kill buffer, not bury it
  (message-send-mail-function 'smtpmail-send-it)
  (message-elide-ellipsis "\n> [... %l lines omitted]\n")
  (message-citation-line-function 'message-insert-formatted-citation-line)
  (message-citation-line-format (concat "> From: %f\n"
                                        "> Date: %a, %e %b %Y %T %z\n"
                                        ">")
                                message-ignored-cited-headers "")

  :config
  (faces-extras-set-and-store-face-attributes
   '((message-header-name :family faces-extras-fixed-pitch-font :height faces-extras-fixed-pitch-size)
     (message-header-subject :family faces-extras-fixed-pitch-font :height faces-extras-fixed-pitch-size)
     (message-header-to :family faces-extras-fixed-pitch-font :height faces-extras-fixed-pitch-size)
     (message-header-other :family faces-extras-fixed-pitch-font :height faces-extras-fixed-pitch-size)
     (message-header-cc :family faces-extras-fixed-pitch-font :height faces-extras-fixed-pitch-size)))

  :hook
  (message-send-hook . buffer-disable-undo) ; required to avoid an error

  :bind
  (:map message-mode-map
        ("s-a" . ml-attach-file)
        ("s-b" . message-goto-body)
        ("s-c" . message-send-and-exit)
        ("s-f" . message-goto-from)
        ("s-s" . message-goto-subject)
        ("s-t" . message-goto-to)
        ("s-A-b" . message-goto-bcc)
        ("s-A-c" . message-goto-cc)
        ("s-A-s" . message-send)))

mml

mml is a library that parses a MML (MIME Meta Language) and generates MIME messages.

(use-feature mml
  :defer t)

mu4e

mu4e is an an Emacs-based e-mail client.

(use-package mu4e
  :ensure (:host github
                 :files ("mu4e/*.el" "build/mu4e/mu4e-meta.el" "build/mu4e/mu4e-config.el" "build/mu4e/mu4e.info")
                 :repo "djcb/mu"
                 :main "mu4e/mu4e.el"
                 :pre-build (("./autogen.sh") ("ninja" "-C" "build"))
                 :build (:not elpaca-build-docs)
                 :ref "1a501281" ; v.1.12.13
                 :depth nil)
  :defer 30
  :custom
  ;; uncomment thw two user options below when debugging
  ;; (mu4e-debug t)
  ;; (mu4e-index-update-error-warning )
  (mu4e-split-view 'single-window)
  (mu4e-headers-show-target nil)
  (mu4e-get-mail-command "sh $HOME/bin/mbsync-parallel")
  (mu4e-update-interval (* 5 60))
  (mu4e-drafts-folder "/Drafts")
  (mu4e-sent-folder "/Sent")
  (mu4e-refile-folder "/Refiled")
  (mu4e-trash-folder "/Trash")
  (mu4e-attachment-dir paths-dir-downloads)
  (mu4e-change-filenames-when-moving t)
  ;; see also `mu4e-extras-set-shortcuts'
  (mu4e-maildir-shortcuts
   `((:maildir ,mu4e-drafts-folder :key ?d)
     (:maildir ,mu4e-sent-folder :key ?t)
     (:maildir ,mu4e-refile-folder :key ?r)
     (:maildir ,mu4e-trash-folder :key ?x)))
  (mu4e-compose-format-flowed t)
  (mu4e-confirm-quit nil)
  (mu4e-headers-date-format "%Y-%m-%d %H:%M")
  (mu4e-search-include-related nil)
  (mu4e-search-results-limit 1000)
  (mu4e-headers-visible-lines 25)
  (mu4e-hide-index-messages t)
  (mu4e-sent-messages-behavior 'delete) ; Gmail already keeps a copy

  ;; performance improvements (but with downsides)
  ;; groups.google.com/g/mu-discuss/c/hRRNhM5mwr0
  ;; djcbsoftware.nl/code/mu/mu4e/Retrieval-and-indexing.html
  (mu4e-index-cleanup t) ; nil improves performance but causes stale index errors
  (mu4e-index-lazy-check t) ; t improves performance

  (mu4e-compose-context-policy 'ask)
  (mu4e-context-policy nil)
  (mu4e-modeline-support nil)
  (mu4e-headers-fields '((:human-date . 16)
                         (:from . 30)
                         (:subject)))


  :config
  (require 'mu4e-contrib)
  (setf (alist-get 'trash mu4e-marks)
        '(:char ("d" . "▼")
                :prompt "dtrash"
                :dyn-target (lambda (target msg) (mu4e-get-trash-folder msg))
                ;; Here's the main difference to the regular trash mark, no +T
                ;; before -N so the message is not marked as IMAP-deleted:
                :action (lambda (docid msg target)
                          (mu4e--server-move docid (mu4e--mark-check-target target) "+S-u-N"))))

  (with-eval-after-load 'savehist
    (add-to-list 'savehist-additional-variables 'mu4e--search-hist))

  ;; do not override native `mu4e' completion with `org-contacts' completion
  (remove-hook 'mu4e-compose-mode-hook 'org-contacts-setup-completion-at-point)
  (faces-extras-set-and-store-face-attributes
   '((mu4e-compose-separator-face :family faces-extras-fixed-pitch-font :height faces-extras-fixed-pitch-size)))

  :bind
  (("A-m" . mu4e)
   :map mu4e-main-mode-map
   ("c" . mu4e-compose-new)
   ("h" . mu4e-display-manual)
   ("j" . mu4e-search-maildir)
   ("u" . mu4e-update-mail-and-index)
   :map mu4e-headers-mode-map
   (";" . mu4e-copy-message-path)
   ("<" . mu4e-headers-split-view-shrink)
   (">" . mu4e-headers-split-view-grow)
   ("s-f" . mu4e-compose-forward)
   ("i" . mu4e-select-other-view)
   ("c" . mu4e-compose-new)
   ("*" . mu4e-headers-mark-all)
   ("A" . mu4e-headers-mark-all-unread-read)
   ("d" . mu4e-headers-mark-for-delete)
   ("f" . avy-extras-headers-view-message)
   ("k" . mu4e-headers-prev)
   ("l" . mu4e-headers-next)
   ("m" . mu4e-headers-mark-for-something)
   ("R" . mu4e-headers-mark-for-refile)
   ("V" . mu4e-headers-mark-for-move)
   :map mu4e-view-mode-map
   ("," . shr-heading-previous)
   ("." . shr-heading-next)
   (";" . mu4e-copy-message-path)
   ("<" . mu4e-headers-split-view-shrink)
   (">" . mu4e-headers-split-view-grow)
   ("s-f" . mu4e-compose-forward)
   ("i" . mu4e-select-other-view)
   ("c" . mu4e-compose-new)
   ("," . mu4e-view-headers-next)
   ("." . mu4e-view-headers-prev)
   ("d" . mu4e-view-mark-for-delete)
   ("f" . ace-link-extras-mu4e)
   ("L" . mu4e-view-save-attachments)
   ("m" . mu4e-view-mark-for-something)
   ("A-C-s-u" . nil)
   ("A-C-s-p" . nil)
   ("s-c" . org-extras-eww-copy-for-org-mode)
   :map mu4e-compose-minor-mode-map
   ("E" . nil)
   :map mu4e-minibuffer-search-query-map
   ("M-k" . previous-history-element)
   ("M-l" . next-history-element)
   :map mu4e-search-minor-mode-map
   ("c" . nil)))

mu4e-extras

mu4e-extras collects my extensions for mu4e.

(use-personal-package mu4e-extras
  :after mu4e
  :demand t
  :custom
  (mu4e-extras-inbox-folder "/Inbox")
  (mu4e-extras-daily-folder "/Daily")
  (mu4e-extras-epoch-inbox-folder "/epoch/Inbox")
  (mu4e-extras-epoch-sent-folder "/epoch/Sent")
  (mu4e-extras-epoch-drafts-folder "/epoch/Drafts")
  (mu4e-extras-epoch-refiled-folder "/epoch/Refiled")
  (mu4e-extras-epoch-trash-folder "/epoch/Trash")
  (mu4e-extras-wide-reply t)

  :config
  (mu4e-extras-set-shortcuts)
  (mu4e-extras-set-bookmarks)
  (mu4e-extras-set-contexts)
  (mu4e-extras-set-account-folders)

  :hook
  (mu4e-mark-execute-pre-hook . mu4e-extras-gmail-fix-flags)
  (mu4e-view-mode-hook . mu4e-extras-set-face-locally)
  (mu4e-update-pre-hook . mu4e-extras-set-index-params)
  (mu4e-index-updated-hook . mu4e-extras-reapply-read-status)
  (message-sent-hook . mu4e-extras-add-sent-to-mark-as-read-queue)

  :bind
  (:map mu4e-main-mode-map
        ("g" . mu4e-extras-compose-new-externally)
        ("r" . mu4e-extras-reindex-db)
        :map mu4e-headers-mode-map
        ("$" . mu4e-extras-copy-sum)
        ("D" . mu4e-extras-headers-trash)
        ("E" . mu4e-extras-headers-mark-read-and-refile)
        ("X" . mu4e-extras-mark-execute-all-no-confirm)
        ("e" . mu4e-extras-headers-refile)
        ("o" . mu4e-extras-view-org-capture)
        ("r" . mu4e-extras-compose-reply)
        ("v" . mu4e-extras-headers-move)
        ("x" . mu4e-extras-open-gmail)
        :map mu4e-view-mode-map
        ("$" . mu4e-extras-copy-sum)
        (";" . mu4e-copy-message-path)
        ("D" . mu4e-extras-view-trash)
        ("e" . mu4e-extras-view-refile)
        ("o" . mu4e-extras-view-org-capture)
        ("r" . mu4e-extras-compose-reply)
        ("v" . mu4e-extras-view-move)
        ("x" . mu4e-extras-view-in-gmail)))

org-msg

org-msg is a global minor mode mixing up Org mode and Message mode to compose and reply to emails in a HTML-friendly style.

I use this package to compose messages and to reply to messages composed in HTML For plain-text messages, I use message (of which see above).

(use-package org-msg
  :after mu4e-extras
  :demand t
  :custom
  (org-msg-options "html-postamble:nil H:5 num:nil ^:{} toc:nil author:nil email:nil \\n:t tex:imagemagick")
  (org-msg-startup "hidestars indent inlineimages")
  (org-msg-recipient-names `((,(getenv "PERSONAL_GMAIL") . "Pablo")))
  (org-msg-greeting-name-limit 3)
  (org-msg-default-alternatives '((new		. (text html))
                                  (reply-to-html	. (text html))))
  (org-msg-convert-citation t)
  (org-msg-enforce-css '((del nil
                              ((font-family . "\"Georgia\"")
                               (font-size . "11pt")
                               (color . "grey")
                               (border-left . "none")
                               (text-decoration . "line-through")
                               (margin-bottom . "0px")
                               (margin-top . "10px")
                               (line-height . "1.3")))
                         (a nil
                            ((color . "#4078F2")))
                         (a reply-header
                            ((color . "black")
                             (text-decoration . "none")))
                         (div reply-header
                              ((padding . "3.0pt 0in 0in 0in")
                               (border-top . "solid #d5d5d5 1.0pt")
                               (margin-bottom . "20px")))
                         (span underline
                               ((text-decoration . "underline")))
                         (li nil
                             ((font-family . "\"Georgia\"")
                              (font-size . "11pt")
                              (line-height . "1.3")
                              (margin-bottom . "0px")
                              (margin-top . "2px")))
                         (nil org-ul
                              ((list-style-type . "square")))
                         (nil org-ol
                              ((font-family . "\"Georgia\"")
                               (font-size . "11pt")
                               (line-height . "1.3")
                               (margin-bottom . "0px")
                               (margin-top . "0px")
                               (margin-left . "30px")
                               (padding-top . "0px")
                               (padding-left . "5px")))
                         (nil signature
                              ((font-family . "\"Arial\", \"Helvetica\", sans-serif")
                               (font-size . "11pt")
                               (margin-bottom . "20px")))
                         (blockquote quote0
                                     ((padding-left . "5px")
                                      (font-size . "0.9")
                                      (margin-left . "10px")
                                      (margin-top . "10px")
                                      (margin-bottom . "0")
                                      (background . "#f9f9f9")
                                      (border-left . "3px solid #d5d5d5")))
                         (blockquote quote1
                                     ((padding-left . "5px")
                                      (font-size . "0.9")
                                      (margin-left . "10px")
                                      (margin-top . "10px")
                                      (margin-bottom . "0")
                                      (background . "#f9f9f9")
                                      (color . "#324e72")
                                      (border-left . "3px solid #3c5d88")))
                         (pre nil
                              ((line-height . "1.3")
                               (color . "#000000")
                               (background-color . "#f0f0f0")
                               (margin . "0px")
                               (font-size . "9pt")
                               (font-family . "monospace")))
                         (p nil
                            ((text-decoration . "none")
                             (margin-bottom . "0px")
                             (margin-top . "10px")
                             (line-height . "1.3")
                             (font-size . "11pt")
                             (font-family . "\"Georgia\"")))
                         (div nil
                              ((font-family . "\"Georgia\"")
                               (font-size . "11pt")
                               (line-height . "11pt")))))

  :config
  (org-msg-mode)
  (require 'pass)

  :hook
  (org-msg-mode-hook . (lambda () (auto-fill-mode -1)))

  :bind
  (:map org-msg-edit-mode-map
        ("s-A-b" . message-goto-bcc)
        ("s-A-c" . message-goto-cc)
        ("s-A-s" . message-send)
        ("s-a" . org-msg-attach)
        ("s-b" . org-msg-extras-begin-compose)
        ("s-c" . message-send-and-exit)
        ("s-f" . message-goto-from)
        ("s-k" . org-insert-link)
        ("s-s" . message-goto-subject)
        ("s-t" . message-goto-to)
        ("s-A-l" . org-extras-url-dwim)))

org-msg-extras

org-msg-extras collects my extensions for org-msg.

(use-personal-package org-msg-extras
  :after org-msg
  :demand t
  :hook
  (org-msg-edit-mode-hook . org-msg-extras-fold-signature-blocks)
  :bind
  (:map org-msg-edit-mode-map
        ("s-g" . org-msg-extras-open-in-grammarly)
        ("s-x" . org-msg-extras-kill-message)))

messaging

telega

telega is an unofficial Emacs Telegram client.

To upgrade TDLib with homebrew, run brew upgrade tdlib --fetch-HEAD in a terminal, then M-x telega-server-build.

If you need to install an earlier version than HEAD, you’ll need to build from source:

  1. Clone the repo: git clone https://github.com/tdlib/td.git
  2. Navigate to the td directory: cd td
  3. Checkout the desired version: git checkout <version_tag>
  4. Create a build directory: mkdir build && cd build && cmake ../
  5. Build the library: make -jN, replacing N by the number of cores to be used for the compilation.
  6. Install the library: make install

Then change the value of telega-server-libs-prefix from /opt/homebrew to /usr/local/lib.

(use-package telega
  :init
  (setopt telega-server-libs-prefix "/opt/homebrew")

  :custom
  (telega-chat-input-markups '("org"))
  (telega-use-images t)
  (telega-emoji-font-family 'noto-emoji)
  (telega-emoji-use-images nil)
  (telega-filters-custom '(("Main" . main)
                           ("Important" or mention
                            (and unread unmuted))
                           ("Archive" . archive)
                           ("Online" and
                            (not saved-messages) (user is-online))
                           ("Groups" type basicgroup supergroup)
                           ("Channels" type channel)))
  (telega-completing-read-function 'completing-read)
  (telega-webpage-history-max most-positive-fixnum)
  (telega-root-fill-column 110)
  (telega-chat-fill-column 90)
  (telega-webpage-fill-column 110)
  (telega-photo-size-limits `(8 3 ,(* 55 1.5) ,(* 12 1.5)))
  (telega-webpage-photo-size-limits `(55 10 ,(* 110 1.5) ,(* 20 1.5)))
  (telega-mode-line-format nil)
  (telega-vvnote-play-speeds '(1 1.5 2))

  :config
  (with-eval-after-load 'savehist
    (add-to-list 'savehist-additional-variables 'telega-search-history))

  (telega-mode-line-mode)

  (faces-extras-set-and-store-face-attributes
   '((telega-entity-type-code :family faces-extras-fixed-pitch-font :height
                              (face-attribute 'default :height))))

  :hook
  (telega-chat-mode-hook . (lambda () (setq default-directory paths-dir-downloads)))

  :bind
  (:map telega-chat-mode-map
        ("M-p" . nil)
        ("S-<return>" . newline)
        ("A-s-r" . telega-chatbuf-next-unread-reaction)
        ("A-C-s-f" . telega-msg-next)
        ("A-C-s-r" . telega-msg-previous)
        ("<return>" . telega-extras-smart-enter)
        ;; if point is on a URL, `telega-msg-button-map' ceases to be
        ;; active and `<return>' triggers `newline' rather than
        ;; `push-button'. this seems to be a bug. as a workaround, we also
        ;; bind `push-button' to `s-<return>' in `telega-chat-mode-map'.
        ("s-," . telega-chatbuf-goto-pinned-message)
        ("s-a" . telega-chatbuf-attach)
        ("s-c" . telega-mnz-chatbuf-attach-code)
        ("s-d" . telega-chatbuf-goto-date)
        ("s-f" . telega-chatbuf-filter)
        ("s-k" . org-insert-link)
        ("s-m" . telega-chatbuf-attach-media)
        ("s-o" . telega-chatbuf-filter-by-topic)
        ("s-r" . telega-msg-add-reaction)
        ("s-s" . telega-chatbuf-filter-search)
        ("s-t" . telega-sticker-choose-favorite-or-recent)
        ("s-v" . org-extras-paste-with-conversion)
        ("M-s-v" . telega-chatbuf-attach-clipboard)
        ("s-z" . telega-mnz-chatbuf-attach-code)
        ("M-s-e" . telega-chatbuf-edit-prev)
        ("M-s-v" . telega-chatbuf-attach-clipboard)
        ("M-f" . ace-link-org)
        :map telega-msg-button-map
        ("k" . telega-button-backward)
        ("l" . telega-button-forward)
        ("<return>" . telega-extras-smart-enter)
        ("," . telega-chatbuf-goto-pinned-message)
        ("C" . telega-msg-copy-link)
        ("D" . telega-msg-delete-dwim)
        ("F" . telega-msg-forward-dwim)
        ("f" . ace-link-org)
        ("s" . telega-chatbuf-filter-search)
        ("w" . telega-browse-url)
        ("W" . telega-chatbuf-filter-cancel)
        :map telega-chat-button-map
        ("a" . nil)
        ("o" . nil)
        :map telega-root-mode-map
        ("k" . telega-button-backward)
        ("<up>" . telega-button-backward)
        ("l" . telega-button-forward)
        ("<down>" . telega-button-forward)
        ("SPC" . telega-root-next-unread)
        ("." . telega-chat-with)
        ("a" . telega-chat-toggle-archive)
        ("f" . avy-extras-telega-view-message)
        ("m" . telega-chat-toggle-muted)
        :map telega-webpage-mode-map
        ("x" . telega-webpage-browse-url)))

telega-mnz

telega-mnz displays syntax highlighting in Telega code blocks.

(use-feature telega-mnz
  :after telega
  :demand t
  :custom
  (telega-mnz-use-language-detection nil)

  :hook
  (telega-load-hook . global-telega-mnz-mode))

telega-dired-dwim

telega-dired-dwim enables Dired file attachments in Telega chat buffers.

(use-feature telega-dired-dwim
  :after telega dired)

telega-extras

telega-extras collects my extensions for telega.

(use-personal-package telega-extras
  :hook
  (telega-load-hook . telega-extras-reset-tab-bar)
  (telega-chat-post-message-hook . telega-extras-transcribe-audio)

  :bind
  (("A-l" . telega-extras-switch-to)
   :map telega-msg-button-map
   ("o" . telega-extras-chat-org-capture)
   ("." . telega-extras-docs-change-open)
   ("b" . telega-extras-transcribe-audio)
   ("d" . telega-extras-download-file)
   ("L" . telega-extras-chat-org-capture-leo)
   :map telega-chat-mode-map
   ("M-s-t" . telega-extras-chatbuf-attach-most-recent-file)
   :map dired-mode-map
   ("M-s-a" . telega-extras-dired-attach-send)
   :map telega-root-view-map
   ("a" . telega-extras-view-archive)
   ("m" . telega-extras-view-main)
   :map telega-root-mode-map
   ("o" . telega-extras-chat-org-capture)))

ol-telega

ol-telega enables Org mode links to Telega chats and messages.

(use-feature ol-telega
  :after telega)

sgn

sgn is an Emacs interface for Signal.

(use-package sgn
  :ensure (:host github
           :repo "benthamite/sgn")
  :custom
  (sgn-account "+14246668293"))

wasabi

wasabi is a WhatsApp Emacs client powered by wuzapi and whatsmeow.

(use-package wasabi
  :ensure (:host github
                 :repo "xenodium/wasabi")
  :defer t)

ement

ement is a Matrix client for Emacs.

(use-package ement
  :disabled)

I installed ement in the hopes that I would be able to send Signal and WhatsApp messages from Emacs. I tried the two way verification method but calling panctl results in a dbus-related error, and I was unable to make dubs work. Some discussion here (see also this comment).

erc

erc is an IRC client for Emacs.

(use-feature erc
  :after auth-source-pass
  :defer t
  :custom
  (erc-server "irc.libera.chat")
  (erc-user-full-name user-full-name)
  (erc-nick (auth-source-pass-get "user" "auth-sources/erc/libera"))
  (erc-password (auth-source-pass-get 'secret "auth-sources/erc/libera"))
  (erc-prompt-for-nickserv-password nil)
  ;; erc-track-shorten-start 8 ; characters to display in modeline
  (erc-autojoin-channels-alist '(("irc.libera.chat")))
  (erc-kill-buffer-on-part nil)
  (erc-auto-query t)

  :config
  (add-to-list 'erc-modules 'notifications)
  (add-to-list 'erc-modules 'spelling))

circe

circe is another IRC client for Emacs.

(use-package circe
  :defer t)

slack

slack is a Slack client for Emacs.

(use-package slack
  :ensure (:host github
                 :repo "benthamite/emacs-slack")
  :custom
  (slack-file-dir paths-dir-downloads)
  (slack-prefer-current-team t)
  (slack-buffer-emojify t)
  (slack-message-custom-notifier 'ignore)

  :config
  (require 'pass)
  (slack-register-team
   :name "Epoch AI"
   :token (auth-source-pass-get "token" "epoch/slack.com/epochai")
   :cookie (auth-source-pass-get "cookie" "epoch/slack.com/epochai"))

  :hook
  (slack-buffer-mode-hook . (lambda () (setopt line-spacing nil)))

  :bind
  (("A-k" . slack-menu)
   :map slack-mode-map
   ("s-a" . slack-all-threads)
   ("s-c" . slack-channel-select)
   ("s-g" . slack-group-select)
   ("s-m" . slack-im-select)
   ("H-s-t" . slack-change-current-team)
   ("s-u" . slack-select-rooms)
   ("H-s-u" . slack-select-unread-rooms)
   :map slack-buffer-mode-map
   ("s-a" . slack-all-threads)
   ("s-c" . slack-channel-select)
   ("s-g" . slack-group-select)
   ("s-m" . slack-im-select)
   ("H-s-t" . slack-change-current-team)
   ("s-u" . slack-select-rooms)
   ("H-s-u" . slack-select-unread-rooms) ; `slack-all-unreads' not working
   :map slack-thread-message-buffer-mode-map
   ("d" . slack-thread-show-or-create)
   ("e" . slack-message-edit)
   ("o" . slack-chat-org-capture)
   ("q" . files-extras-kill-other-buffer)
   ("r" . slack-thread-reply)
   ("R" . slack-message-add-reaction)
   ("z" . slack-message-write-another-buffer)
   ("s-z" . slack-message-write-another-buffer)
   :map slack-activity-feed-buffer-mode-map
   ("," . slack-activity-feed-goto-prev)
   ("." . slack-activity-feed-goto-next)
   ("d" . slack-thread-show-or-create)
   ("e" . slack-message-edit)
   ("k" . slack-feed-goto-prev)
   ("l" . slack-feed-goto-next)
   ("r" . slack-thread-reply)
   ("R" . slack-message-add-reaction)
   ("z" . slack-message-write-another-buffer)
   ("s-z" . slack-message-write-another-buffer)
   :map slack-message-buffer-mode-map
   ("," . slack-buffer-goto-prev-message)
   ("." . slack-buffer-goto-next-message)
   ("d" . slack-thread-show-or-create)
   ("e" . slack-message-edit)
   ("k" . slack-buffer-goto-prev-message)
   ("l" . slack-buffer-goto-next-message)
   ("o" . slack-chat-org-capture)
   ("r" . slack-thread-reply)
   ("R" . slack-message-add-reaction)
   ("z" . slack-message-write-another-buffer)
   ("s-z" . slack-message-write-another-buffer)
   :map slack-message-compose-buffer-mode-map
   ("s-c" . slack-message-send-from-buffer)
   ("s-f" . slack-message-select-file)
   ("s-m" . slack-message-embed-mention)))

slack-extras

slack-extras collects my extensions for slack.

(use-personal-package slack-extras
  :after slack
  :demand t
  :bind
  (:map slack-activity-feed-buffer-mode-map
        ("E" . slack-extras-work-capture)
        ("o" . slack-extras-personal-capture)
        :map slack-thread-message-buffer-mode-map
        ("E" . slack-extras-work-capture)
        ("o" . slack-extras-personal-capture)
        :map slack-message-buffer-mode-map
        ("E" . slack-extras-work-capture)
        ("o" . slack-extras-personal-capture)))

ol-emacs-slack

ol-emacs-slack provides org-store-link support for slack.

(use-package ol-emacs-slack
  :ensure (:host github
             :repo "benthamite/ol-emacs-slack")
  :after slack)

web

browse-url

browse-url provides functions for browsing URLs.

(use-feature browse-url
  :defer 30
  :custom
  (browse-url-browser-function 'eww-browse-url)
  (browse-url-firefox-program "/Applications/Firefox.app/Contents/MacOS/firefox")
  (browse-url-chrome-program "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"))

browse-url-extras

browse-url-extras collects my extensions for browse-url.

(use-personal-package browse-url-extras
  :init
  (with-eval-after-load 'xwidget
    (bind-keys :map 'xwidget-webkit-mode-map
               ("X" . browse-extras-browse-url-externally)))
  :after browse-url)

shr

shr is a simple HTML renderer.

(use-feature shr
  :after faces-extras
  :defer t
  :custom
  (shr-bullet "• ")
  (shr-use-colors nil)
  (shr-use-fonts t)
  (shr-image-animate nil)
  (shr-width nil)
  (shr-max-width 100)
  (shr-discard-aria-hidden t)
  (shr-cookie-policy t)

  :config
  (faces-extras-set-and-store-face-attributes
   '((shr-text :height 0.65)
     ;; doesn’t seem to be working?
     (shr-h1 :family faces-extras-fixed-pitch-font :height faces-extras-org-level-height)
     (shr-h2 :family faces-extras-fixed-pitch-font :height faces-extras-org-level-height))))

html

html provides a major mode for editing HTML files.

(use-feature html
  :bind (:map html-mode-map
              ("s-w" . eww-extras-browse-file)))

mhtml

mhtml is an editing mode that handles CSS and JavaScript.

(use-feature mhtml
  :bind
  (:map mhtml-mode
        ("s-x" . browse-url-of-buffer)
        ("s-w" . eww-extras-browse-file)))

shr-tag-pre-highlight

shr-tag-pre-highlight adds syntax highlighting for code blocks in HTML rendered by shr.

(use-package shr-tag-pre-highlight
  :after (:any eww elfeed)
  :config
  (add-to-list 'shr-external-rendering-functions
               '(pre . shr-tag-pre-highlight)))

shr-heading

shr-heading supports heading navigation for shr-rendered buffers.

Discussion here.

(use-package shr-heading
  :ensure (:host github
             :repo "oantolin/emacs-config"
             :files ("my-lisp/shr-heading.el"))
  :after shr
  :hook
  (eww-mode-hook . shr-heading-setup-imenu))

eww

eww is a text-based web browser.

(use-feature eww
  :after simple-extras
  :custom
  (eww-search-prefix "https://duckduckgo.com/?t=h_&q=")
  (eww-restore-desktop t)
  (eww-desktop-remove-duplicates t)
  (eww-header-line-format nil)
  (eww-download-directory paths-dir-downloads)
  (eww-auto-rename-buffer 'title)
  (eww-suggest-uris
   '(eww-links-at-point
     thing-at-point-url-at-point))
  (eww-history-limit most-positive-fixnum)
  (eww-browse-url-new-window-is-tab nil)
  ;; make eww respect url handlers when following links in webpages

  :config
  (dolist (cons browse-url-handlers)
    (setopt eww-use-browse-url
          (concat eww-use-browse-url "\\|" (car cons))))

  (with-eval-after-load 'savehist
    (dolist (var '(eww-history eww-prompt-history))
      (add-to-list 'savehist-additional-variables var)))

  :bind
  (("A-w" . eww)
   :map eww-mode-map
   ("<return>" . eww-follow-link)
   ("S-<return>" . eww-follow-link)
   ("," . shr-heading-previous)
   ("." . shr-heading-next)
   ("[" . eww-previous-url)
   ("]" . eww-next-url)
   ("j" . eww-back-url)
   (";" . eww-forward-url)
   ("e" . browse-url-extras-add-domain-to-open-externally)
   ("f" . ace-link-eww)
   ("F" . ace-link-extras-eww-new-buffer)
   ("s-f" . ace-link-extras-eww-externally)
   ("g" . nil)
   ("o" . eww-toggle-fonts)
   ("r" . eww-reload)
   ;; ":" (lambda! (eww-follow-link '(4)))
   ("X" . eww-browse-with-external-browser)
   ("s-c" . org-extras-eww-copy-for-org-mode)))

eww-extras

eww-extras collects my extensions for eww.

(use-personal-package eww-extras
  :after eww
  :demand t
  :config
  (require 'xwidget)
  (advice-add 'eww :before #'eww-extras-browse-youtube)

  :bind
  (:map eww-mode-map
        ("g e" . eww-extras-edit-current-url)
        ("g u" . eww-extras-go-up-url-hierarchy)
        ("g U" . eww-extras-go-to-root-url-hierarchy)
        ;; "p" 'eww-extras-open-with-recent-kill-ring
        ("h" . eww-extras-url-to-html)
        ("p" . eww-extras-url-to-pdf)
        ("x" . eww-extras-open-with-xwidget)
        ("s-d" . eww-extras-url-to-pdf)
        ("s-h" . eww-extras-url-to-html)
        :map xwidget-webkit-mode-map
        ("x" . eww-extras-open-with-eww)))

prot-eww

[[https://github.com/protesilaos/dotfiles/blob/master/emacs.emacs.d/prot-lisp/prot-eww.el][prot-eww]] is a set of eww extensions from Protesilaos Stavrou’s personal configuration._

Note Prot’s clarification:

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.

(use-package prot-eww
  :ensure (:host github
                 :repo "protesilaos/dotfiles"
                 :local-repo "prot-eww"
                 :main "emacs/.emacs.d/prot-lisp/prot-eww.el"
                 :build (:not elpaca-check-version)
                 :files ("emacs/.emacs.d/prot-lisp/prot-eww.el"))
  :after eww prot-common
  :bind
  (:map eww-mode-map
        ("M-f" . prot-eww-visit-url-on-page)
        ("A-M-f" . prot-eww-jump-to-url-on-page)))

w3m

w3m is an Emacs interface to w3m.

I only use w3m to browse HTML email messages with mu4e. For web browsing, I use eww.

(use-package w3m
  :after simple mu4e
  :bind
  (:map w3m-minor-mode-map
        ("<left>" . left-char)
        ("<right>" . right-char)
        ("<up>" . previous-line)
        ("<down>" . next-line)
        :map w3m-mode-map
        ("s-<return>" . w3m-view-url-with-browse-url)
        :map mu4e-view-mode-map
        ("<return>" . w3m-view-url-with-browse-url)))

elfeed

elfeed is a web feeds client.

If the lines are breaking at the wrong places, set shr-width to the right value.

(use-package elfeed
  :ensure (:host github
                 :repo "benthamite/elfeed"
                 :branch "debounce-search-update") ; https://github.com/skeeto/elfeed/pull/558
  :init
  (run-with-idle-timer (* 5 60) t #'elfeed-extras-update)

  :custom
  (elfeed-curl-timeout 5)
  (elfeed-curl-max-connections 4)
  (elfeed-search-remain-on-entry t)

  :config
  (setq-default elfeed-search-filter "@15-days-ago +unread")

  :hook
  (elfeed-show-mode-hook . visual-line-mode)
  (elfeed-search-mode-hook . (lambda ()
                               "Disable undo in ‘*elfeed-search*’ buffer to avoid warnings."
                               (buffer-disable-undo)))

  :bind
  (("A-f" . elfeed)
   :map eww-mode-map
   ("c" . elfeed-kill-link-url-at-point)
   :map elfeed-search-mode-map
   ("U" . elfeed-search-tag-all-unread)
   ("d" . elfeed-update)
   ("f" . avy-extras-elfeed-search-show-entry)
   ("j" . elfeed-unjam)
   ("o" . elfeed-org)
   ("s" . elfeed-search-live-filter)
   :map elfeed-show-mode-map
   (";" . elfeed-show-next)
   ("<return>" . eww-follow-link)
   ("," . shr-heading-previous)
   ("." . shr-heading-next)
   ("F" . ace-link-extras-eww-new-buffer)
   ("S-<return>" . eww-follow-link)
   ("a" . zotra-extras-add-entry)
   ("b" . nil)
   ("f" . ace-link-eww)
   ("j" . elfeed-show-prev)
   ("q" . files-extras-kill-this-buffer)
   ("q" . nil)
   ("s-f" . ace-link-extras-eww-externally)
   ("x" . elfeed-show-visit)))

elfeed-extras

elfeed-extras collects my extensions for elfeed.

(use-personal-package elfeed-extras
  :after elfeed
  :demand t
  :custom
  (elfeed-show-entry-switch #'elfeed-extras-display-buffer)

  :hook
  (elfeed-search-mode-hook . elfeed-extras-disable-undo)

  :bind
  (:map elfeed-search-mode-map
        ("A" . elfeed-extras-mark-all-as-read)
        ("e" . elfeed-extras-toggle-read-entries)
        ("k" . elfeed-extras-follow-previous)
        ("l" . elfeed-extras-follow-next)
        ("w" . elfeed-extras-toggle-wiki-entries)
        :map elfeed-show-mode-map
        ("<tab>" . elfeed-extras-jump-to-next-link)
        ("i" . elfeed-extras-toggle-fixed-pitch)
        ("w" . elfeed-extras-kill-link-url-of-entry)))

elfeed-org

elfeed-org supports defining the feeds used by elfeed in an org-mode file.

(use-package elfeed-org
  :after elfeed
  :custom
  (rmh-elfeed-org-files (list paths-file-feeds-pablo))

  :config
  (elfeed-org))

elfeed-tube

elfeed-tube integrates elfeed with YouTube.

(use-package elfeed-tube
  :after elfeed
  :demand t
  :custom
  (elfeed-tube-auto-save-p t)

  :config
  (push '(text . shr-text) elfeed-tube-captions-faces)
  (elfeed-tube-setup)

  :bind
  (:map elfeed-show-mode-map
   ("v" . elfeed-tube-mpv)
   ("F" . elfeed-tube-mpv-follow-mode)
   ("." . elfeed-tube-mpv-where)))

elfeed-tube-mpv

elfeed-tube-mpv integrates elfeed-tube with mpv.

(use-package elfeed-tube-mpv
  :after elfeed-tube
  :demand t
  :custom
  (elfeed-tube-save-indicator t))

elfeed-ai

elfeed-ai provides AI-powered content curation for elfeed.

(use-package elfeed-ai
  :ensure (:host github
                 :repo "benthamite/elfeed-ai")
  :after elfeed
  :demand t
  :custom
  (elfeed-ai-model 'gemini-flash-lite-latest)
  (elfeed-ai-interest-profile (file-name-concat paths-dir-notes "my-reading-taste-profile-summary.org"))

  :config
  (elfeed-ai-mode)

  :bind
  (:map elfeed-search-mode-map
        ("a" . elfeed-ai-menu)
        ("t" . elfeed-ai-toggle-sort)))

engine-mode

engine-mode is a minor mode for defining and querying search engines through Emacs.

(use-package engine-mode
  :defer t
  :custom
  (engine/browser-function browse-url-browser-function)

  :config
  (engine/set-keymap-prefix (kbd "H-g"))
  (defengine AllMusic
    "http://www.allmusic.com/search/all/%s"
    :keybinding "a m")
  (defengine Alignment-Forum
    "https://www.alignmentforum.org/search?query=%s"
    :keybinding "a f")
  (defengine AlternativeTo
    "http://alternativeto.net/SearchResult.aspx?profile=all&search=%s"
    :keybinding "a t")
  (defengine Amazon-DE
    "http://www.amazon.de/s?k=%s"
    :keybinding "a d")
  (defengine Amazon-ES
    "http://www.amazon.es/s?k=%s"
    :keybinding "a e")
  (defengine Amazon-FR
    "https://www.amazon.fr/s?k=%s"
    :keybinding "a f")
  (defengine TheresAnAIForThat
    "https://theresanaiforthat.com/s/%s/"
    :keybinding "a i")
  (defengine Amazon-MX
    "https://www.amazon.com.mx/s?k=%s"
    :keybinding "a x")
  (defengine Amazon-UK
    "http://www.amazon.co.uk/s?k=%s"
    :keybinding "a k")
  (defengine Amazon-US
    "http://www.amazon.com/s?k=%s"
    :keybinding "a a")
  (defengine AnkiWeb
    "https://ankiweb.net/shared/decks/%s"
    :keybinding "a w")
  (defengine AstralCodexTen
    "https://substack.com/search/%s?focusedPublicationId=89120"
    :keybinding "a c"
    ;; individual Substack posts render nicely in eww, but for other pages we need a modern browser
    :browser 'browse-url-default-browser)
  (defengine Audible
    "https://www.audible.com/search/ref=a_hp_tseft?advsearchKeywords=%s&filterby=field-keywords&x=13&y=11"
    :keybinding "a u")
  (defengine AudioBookBay
    "https://audiobookbay.lu/?s=%s&tt=1"
    :keybinding "a b")
  (defengine EABlogs
    "https://cse.google.com/cse?cx=013594344773078830993:k3igzr2se6y&q=%s"
    :keybinding "b b")
  (defengine BookFinder
    "http://www.bookfinder.com/search/?keywords=%s&st=xl&ac=qr&src=opensearch"
    :keybinding "b f")
  (defengine Bing
    "https://www.bing.com/search?q=%s&PC=U316&FORM=CHROMN"
    :keybinding "b i")
  (defengine UCBerkeleyLibrary    "https://search.library.berkeley.edu/discovery/search?query=any,contains,%s&tab=Default_UCLibrarySearch&search_scope=DN_and_CI&vid=01UCS_BER:UCB&offset=0"
             :keybinding "b l")
  (defengine MercadoLibre
    "https://listado.mercadolibre.com.ar/%s#D[A:qwer]"
    :keybinding "c c")
  (defengine CRSocietyForums
    "https://www.crsociety.org/search/?q=%s"
    :keybinding "c r")
  (defengine Calendly
    "https://calendly.com/app/login?email=%s&lang=en"
    :keybinding "c l")
  (defengine ChromeExtensions
    "https://chrome.google.com/webstore/search/%s?_category=extensions"
    :keybinding "c e")
  (defengine Crossref
    "https://search.crossref.org/?q=%s"
    :keybinding "c r")
  (defengine MercadoLibreUsed
    "https://listado.mercadolibre.com.ar/%s_ITEM*CONDITION_2230581_NoIndex_True#applied_filter_id%3DITEM_CONDITION%26applied_filter_name%3DCondici%C3%B3n%26applied_filter_order%3D10%26applied_value_id%3D2230581%26applied_value_name%3DUsado%26applied_value_order%3D2%26applied_value_results%3D9%26is_custom%3Dfalse"
    :keybinding "c r")
  (defengine DOI
    "https://doi.org/%s"
    :keybinding "d o")
  (defengine DuckDuckGo
    "https://duckduckgo.com/?q=%s"
    :keybinding "d d")
  (defengine Diccionario-Panhispánico-de-Dudas
    "https://www.rae.es/dpd/%s"
    :keybinding "d p")
  (defengine EAForum
    "https://www.google.com/search?q=%s+site:forum.effectivealtruism.org"
    :keybinding "f f")
  (defengine Ebay-UK
    "https://www.ebay.co.uk/sch/i.html?_from=R40&_trksid=p2380057.m570.l1313&_nkw=%s&_sacat=0"
    :keybinding "e k")
  (defengine Ebay-US
    "https://www.ebay.com/sch/i.html?_from=R40&_trksid=p2380057.m570.l1313&_nkw=%s&_sacat=0"
    :keybinding "e b")
  (defengine Ebay-DE
    "https://www.ebay.de/sch/i.html?_from=R40&_trksid=p2380057.m570.l1313&_nkw=%s&_sacat=0"
    :keybinding "e d")
  (defengine Fundeu
    "https://cse.google.com/cse?cx=005053095451413799011:alg8dd3pluq&q=%s"
    :keybinding "f f")
  (defengine Flickr
    "http://www.flickr.com/search/?q=%s"
    :keybinding "f l")
  (defengine Financial-Times
    "https://www.ft.com/search?q=%s"
    :keybinding "f t")
  (defengine GitHub
    "https://github.com/search?q=%s&type=code"
    :keybinding "g h")
  (defengine Goodreads
    "http://www.goodreads.com/search/search?search_type=books&search[query]=%s"
    :keybinding "g r")
  (defengine Google
    "https://www.google.com/search?q=%s"
    :keybinding "g g")
  (defengine Google-Books
    "https://www.google.com/search?q=%s&btnG=Search+Books&tbm=bks&tbo=1&gws_rd=ssl"
    :keybinding "g k")
  (defengine Google-Custom-Search
    "https://cse.google.com/cse?cx=013594344773078830993:bg9mrnfwe30&q=%s"
    :keybinding "g c")
  (defengine Google-Domains
    "https://domains.google.com/registrar?s=%s&hl=en"
    :keybinding "g d")
  (defengine Google-Drive
    "https://drive.google.com/drive/u/0/search?q=%s"
    :keybinding "g d")
  (defengine Google-Trends
    "http://www.google.com/trends/explore#q=%s"
    :keybinding "g e")
  (defengine Google-Images
    "https://www.google.com/search?tbm=isch&source=hp&biw=1920&bih=1006&ei=2PlgWp_OEcHF6QTo2b2ACQ&q=%s"
    :keybinding "g i")
  (defengine Google-Maps
    "https://www.google.com/maps/search/%s"
    :keybinding "g m")
  (defengine Google-News
    "https://news.google.com/search?q=%s"
    :keybinding "g n")
  (defengine Google-Podcasts
    "https://podcasts.google.com/?q=%s"
    :keybinding "g o")
  (defengine Google-Photos
    "https://photos.google.com/search/%s"
    :keybinding "g p")
  (defengine Google-Scholar
    "https://scholar.google.com/scholar?hl=en&as_sdt=1%2C5&q=%s&btnG=&lr="
    :keybinding "s s")
  (defengine Google-Translate
    "https://translate.google.com/#auto/en/%s"
    :keybinding "g t")
  (defengine Google-Video
    "https://www.google.com/search?q=%s&tbm=vid"
    :keybinding "g v")
  (defengine GiveWell
    "https://www.givewell.org/search/ss360/%s"
    :keybinding "g w")
  (defengine Google-Play
    "https://play.google.com/store/search?q=%s"
    :keybinding "g y")
  (defengine Google-Scholar-Spanish
    "https://scholar.google.com/scholar?hl=en&as_sdt=1%2C5&q=%s&btnG="
    :keybinding "s x")
  (defengine Gwern
    "https://www.google.com/search?q=%s+site:gwern.net"
    :keybinding "g w")
  (defengine IMDb
    "https://www.imdb.com/find/?q=%s"
    :keybinding "i i")
  (defengine IMDb-Actor
    "http://www.imdb.com/filmosearch?explore=title_type&role=%s&ref_=filmo_ref_job_typ&sort=user_rating"
    :keybinding "i a")
  (defengine IMDb-Director
    "http://www.imdb.com/filmosearch?explore=title_type&role=%s&ref_=filmo_ref_job_typ&sort=user_rating"
    :keybinding "i d")
  (defengine IMDb-Composer
    "http://www.imdb.com/filmosearch?explore=title_type&role=%s&ref_=filmo_ref_job_typ&sort=user_rating"
    :keybinding "i c")
  (defengine IberLibroArgentina
    "https://www.iberlibro.com/servlet/SearchResults?bi=0&bx=off&cm_sp=SearchF-_-Advs-_-Result&cty=ar&ds=20&kn=%s&prc=USD&recentlyadded=all&rgn=ww&rollup=on&sortby=20&xdesc=off&xpod=off"
    :keybinding "i r")
  (defengine Internet-Archive
    "https://archive.org/search.php?query=%s"
    :keybinding "v v")
  (defengine Internet-Archive-Scholar
    "https://scholar.archive.org/search?q=%s"
    :keybinding "v s")
  (defengine JustWatch
    "https://www.justwatch.com/us/search?q=%s"
    :keybinding "j w")
  (defengine KAYAK
    "https://www.kayak.co.uk/sherlock/opensearch/search?q=%s"
    :keybinding "k k")
  (defengine Keyboard-Maestro
    "https://forum.keyboardmaestro.com/search?q=%s"
    :keybinding "k m")
  (defengine Lastfm
    "http://www.last.fm/search?q=%s"
    :keybinding "f m")
  (defengine LessWrong
    "https://www.google.com/search?q=%s+site:lesswrong.com"
    :keybinding "l w")
  (defengine LessWrongWiki
    "https://wiki.lesswrong.com/index.php?title=Special:Search&search=%s"
    :keybinding "l i")
  (defengine LibraryGenesis
    "http://libgen.li/index.php?req=%s"
    :keybinding "l l")
  (defengine Librivox
    "https://librivox.org/search?q=%s&search_form=advanced"
    :keybinding "l v")
  (defengine LinkedIn
    "http://www.linkedin.com/vsearch/f?type=all&keywords=%s&orig=GLHD&rsid=&pageKey=member-home&search=Search"
    :keybinding "i n")
  (defengine Linguee
    "https://www.linguee.com/english-spanish/search?source=auto&query=%s"
    :keybinding "l i")
  (defengine Marginal-Revolution
    "https://marginalrevolution.com/?s=%s"
    :keybinding "m r")
  (defengine MediaCenter
    "https://www.google.com/search?q=%s+site:yabb.jriver.com"
    :keybinding "m c")
  (defengine Medium
    "https://medium.com/search?q=%s&ref=opensearch"
    :keybinding "m d")
  (defengine Melpa
    "https://melpa.org/#/?q=%s"
    :keybinding "m p")
  (defengine MetaFilter
    "https://www.metafilter.com/contribute/search.mefi?site=mefi&q=%s"
    :keybinding "m f")
  (defengine Metaculus
    "https://www.metaculus.com/questions/?order_by=-activity&search=%s"
    :keybinding "m e")
  (defengine Metaforecast
    "https://metaforecast.org/?query=%s"
    :keybinding "m m")
  (defengine Movielens
    "https://movielens.org/explore?q=%s"
    :keybinding "m l")
  (defengine Netflix
    "https://www.netflix.com/search?q=%s"
    :keybinding "n n")
  (defengine New-York-Times
    "https://www.nytimes.com/search?query=%s"
    :keybinding "n y")
  (defengine Notatu-Dignum
    "http://www.stafforini.com/quotes/index.php?s=%s"
    :keybinding "q q")
  (defengine OddsChecker
    "https://www.oddschecker.com/search?query=%s"
    :keybinding "o c")
  (defengine Open-Philanthropy
    "https://www.google.com/search?q=%s+site:openphilanthropy.org"
    :keybinding "o p")
  (defengine Overcoming-Bias
    "https://substack.com/search/%s?focusedPublicationId=1245641"
    :keybinding "o b"
    :browser 'browse-url-default-browser)
  (defengine OxfordReference
    "https://www-oxfordreference-com.myaccess.library.utoronto.ca/search?btog=chap&q0=%22%s%22"
    :keybinding "o r")
  (defengine OxfordReferenceDOI
    "https://www-oxfordreference-com.myaccess.library.utoronto.ca/view/%s"
    :keybinding "o d")
  (defengine PhilPapers
    "http://philpapers.org/s/%s"
    :keybinding "p p")
  (defengine AnnasArchive
    (progn
      (require 'annas-archive)
      (concat annas-archive-home-url "search?index=&page=1&q=%s&ext=pdf&sort="))
    :keybinding "r r")
  (defengine ReducingSuffering
    "http://reducing-suffering.org/?s=%s"
    :keybinding "r s")
  (defengine Reference
    "https://cse.google.com/cse?cx=013594344773078830993:bg9mrnfwe30&q=%s"
    :keybinding "r f")
  (defengine sci-hub
    "https://sci-hub.se/%s"
    :keybinding "u u")
  (defengine ScienceDirectencyclopedias
    "https://www.sciencedirect.com/search?qs=%s&articleTypes=EN"
    :keybinding "s e")
  (defengine SlateStarCodex
    "http://slatestarcodex.com/?s=%s"
    :keybinding "s c")
  (defengine StackSnippet
    "http://www.stacksnippet.com/#gsc.tab=0&gsc.q=%s"
    :keybinding "s n")
  (defengine Stanford-Encyclopedia-of-Philosophy
    "https://plato.stanford.edu/search/searcher.py?query=%s"
    :keybinding "s p")
  (defengine Tango-DJ
    "http://www.tango-dj.at/database/?tango-db-search=%s&search=Search"
    :keybinding "d j")
  (defengine TangoDJ-Yahoo-Group
    "http://groups.yahoo.com/group/TangoDJ/msearch?query=%s&submit=Search&charset=ISO-8859-1"
    :keybinding "t y")
  (defengine TasteDive
    "https://tastedive.com/like/%s"
    :keybinding "t d")
  (defengine ThreadReader
    "https://threadreaderapp.com/search?q=%s"
    :keybinding "t r")
  (defengine Twitter
    "https://twitter.com/search?q=%s&src=typed_query"
    :keybinding "t w")
  (defengine Vimeo
    "http://vimeo.com/search?q=%s"
    :keybinding "v m")
  (defengine WaybackMachine
    "http://web.archive.org/web/*/%s"
    :keybinding "w b")
  (defengine Wikipedia-Deutsch
    "https://de.wikipedia.org/w/index.php?title=Spezial:Suche&search=%s"
    :keybinding "w d")
  (defengine Wikipedia-English
    "http://en.wikipedia.org/w/index.php?title=Special:Search&profile=default&search=%s&fulltext=Search"
    :keybinding "w w")
  (defengine Wikipedia-French
    "http://fr.wikipedia.org/w/index.php?title=Spécial:Recherche&search=%s"
    :keybinding "w f")
  (defengine Wikipedia-Italiano
    "http://it.wikipedia.org/w/index.php?title=Speciale:Ricerca&search=%s"
    :keybinding "w i")
  (defengine Wikipedia-Spanish
    "https://es.wikipedia.org/w/index.php?search=%s&title=Especial:Buscar&ns0=1&ns11=1&ns100=1"
    :keybinding "w e")
  (defengine Wikipedia-Swedish
    "http://sv.wikipedia.org/w/index.php?title=Special:S%C3%B6k&search=%s"
    :keybinding "w s")
  (defengine Wirecutter
    "https://thewirecutter.com/search/?s=%s"
    :keybinding "w t")
  (defengine WorldCat
    "http://www.worldcat.org/search?q=%s&qt=results_page"
    :keybinding "w c")
  (defengine YahooFinance
    "https://finance.yahoo.com/company/%s"
    :keybinding "y f")
  (defengine YouTube
    "https://www.youtube.com/results?search_query=%s"
    :keybinding "y t")
  (defengine YouTubemovies
    "https://www.youtube.com/results?lclk=long&filters=hd%2Clong&search_query=%s"
    :keybinding "y m")

  :hook minibuffer-setup-hook)

org-download

org-download supports drag and drop images to org-mode.

(use-package org-download
  :after org
  :bind
  ("H-s-v" . org-download-clipboard))

org-web-tools

org-web-tools supports viewing, capturing, and archiving web pages in org-mode.

(use-package org-web-tools
  :defer t)

org-web-tools-extras

org-web-tools-extras collects my extensions for org-web-tools.

(use-personal-package org-web-tools-extras
  :after org-web-tools)

request

request provides HTTP request for Emacs Lisp.

(use-package request
  :defer t)

deferred

deferred provides simple asynchronous functions for emacs lisp.

(use-package deferred
  :defer t)

graphql-mode

graphql-mode is a major mode for GraphQL.

(use-package graphql-mode
  :defer t)

mullvad

mullvad provides a few functions for interfacing with Mullvad, a VPN service.

(use-package mullvad
  :ensure (mullvad
           :host github
           :repo "benthamite/mullvad")
  :demand t
  :custom
  (mullvad-durations '(1 5 10 30 60 120))

  (mullvad-cities-and-servers
        '(("London" . "gb-lon-ovpn-005")
          ("Madrid" . "es-mad-ovpn-202")
          ("Malmö" . "se-sto-wg-005")
          ("Frankfurt" . "de-fra-wg-005")
          ("New York" . "us-nyc-wg-504")
          ("San José" . "us-sjc-wg-101")
          ("São Paulo" . "br-sao-wg-202")))

  (mullvad-websites-and-cities
        '(("Betfair" . "London")
          ("Criterion Channel" . "New York")
          ("Gemini" . "New York")
          ("HathiTrust" . "San José")
          ("IMDb" . "New York")
          ("Library Genesis" . "Malmö")
          ("Pirate Bay" . "Malmö")
          ("UC Berkeley" . "San José")
          ("Wise" . "Madrid")))

  :bind
  ("A-a" . mullvad))

multimedia

EMMS

EMMS (Emacs MultiMedia System) is media player software for Emacs.

EMMS is not powerful enough for my use case (tango DJ with a collection of over 70,000 tracks). But I’m exploring whether I can use it for specific purposes, such as batch-tagging.

(use-package emms
  :defer t
  :disabled t ; temporarily because server is down
  :custom
  (emms-player-list '(emms-player-mpv))
  (emms-source-file-default-directory paths-dir-music-tango)
  (emms-playlist-buffer-name "*Music*")
  (emms-info-functions '(emms-info-libtag)) ; make sure libtag is the only thing delivering metadata
  ;; ~1 order of magnitude fzaster; requires GNU find: `brew install findutils'
  (emms-source-file-directory-tree-function 'emms-source-file-directory-tree-find)

  :config
  (require 'emms-setup)
  (require 'emms-player-simple)
  (require 'emms-source-file)
  (require 'emms-source-playlist)
  (require 'emms-info-native)
  ;; emms-print-metadata binary must be present; see emacs.stackexchange.com/a/22431/32089
  (require 'emms-info-libtag)
  (require 'emms-mode-line)
  (require 'emms-mode-line-icon)
  (require 'emms-playing-time)

  (emms-all)
  (emms-default-players)
  (add-to-list 'emms-info-functions 'emms-info-libtag)
  (emms-mode-line-mode)
  (emms-playing-time 1))

empv

empv is a media player based on mpv.

(use-package empv
  :ensure (:host github
                 :repo "isamert/empv.el")
  :defer t
  :custom
  (empv-audio-dir paths-dir-music-tango)
  (empv-invidious-instance "https://invidious.fdn.fr/api/v1")

  :config
  (add-to-list 'empv-mpv-args "--ytdl-format=best")) ; github.com/isamert/empv.el#viewing-youtube-videos

tangodb

tangodb is a package for browsing and editing a tango encyclopaedia.

(use-package tangodb
  :ensure (:host github
                 :repo "benthamite/tangodb.el")
  :defer t)

trx

trx is an Emacs interface for the Transmission BitTorrent client.

(use-package trx
  :ensure (:host github
                 :repo "benthamite/trx")
  :defer t)

ytdl

ytdl is an Emacs interface for youtube-dl.

Note that this package also works with yt-dlp, a youtube-dl fork.

(use-package ytdl
  :custom
  (ytdl-command "yt-dlp")
  (ytdl-video-folder paths-dir-downloads
                     ytdl-music-folder paths-dir-downloads
                     ytdl-download-folder paths-dir-downloads)
  (ytdl-video-extra-args . ("--write-sub" "--write-auto-sub" "--sub-lang" "en,es,it,fr,pt"))

  :bind
  (("A-M-y" . ytdl-download)
   :map ytdl--dl-list-mode-map
   ("RET" . ytdl--open-item-at-point)
   ("D" . ytdl--delete-item-at-point)))

esi-dictate

esi-dictate is a set of packages for speech and voice inputs in Emacs.

Setup:

  1. Install Python dependencies: pip install 'deepgram-sdk>=3.0,<4.0' pyaudio. Note: the dg.py script requires SDK v3.x; v5.x has a completely different API and will not work.
  2. Download dg.py from the repo and place it in your PATH (e.g., ~/.local/bin/dg.py), then make it executable: chmod +x ~/.local/bin/dg.py

Usage: M-x esi-dictate-start to begin dictation, C-g to stop.

(use-package esi-dictate
  :ensure (:host sourcehut
                 :repo "lepisma/emacs-speech-input")
  :after llm
  :defer t
  :custom
  (esi-dictate-llm-provider (make-llm-openai
                             :key (auth-source-pass-get "gptel" (concat "tlon/core/openai.com/" tlon-email-shared))
                             :chat-model "gpt-4o-mini"))

  (esi-dictate-dg-api-key (auth-source-pass-get "key" (concat "chrome/deepgram.com/" (getenv "PERSONAL_EMAIL"))))

  :bind (("A-h" . esi-dictate-start)
         :map esi-dictate-mode-map
         ("C-g" . esi-dictate-stop))
  :hook
  (esi-dictate-speech-final . esi-dictate-fix-context))

read-aloud

read-aloud is an Emacs interface to TTS (text-to-speech) engines.

  • To give Emacs access to the microphone on MacOS, clone https://github.com/DocSystem/tccutil and from the cloned repo, run sudo python3 tccutil.py -p /opt/homebrew/Cellar/emacs-plus@30/30.0.60/Emacs.app/ --microphone -e (some discussion here).
  • To read with macOS directly, b-n. In turn, b-h starts dictation. (These are system-wide shortcuts defined with Karabiner rather than bindings specific to Emacs. See the “b-mode” section in my Karabiner configuration.)
(use-package read-aloud
  :custom
  (read-aloud-engine "say")

  :bind
  ("C-H-r" . read-aloud-this))

read-aloud-extras

read-aloud-extras collects my extensions for read-aloud.

(use-personal-package read-aloud-extras
  :after read-aloud)

subed

subed is a subtitle editor for Emacs.

(use-package subed
  :ensure (:host github
                 :repo "sachac/subed"
                 :files ("subed/*.el"))
  :defer t
  :config
  (defun subed-export-transcript ()
    "Export a clean transcript of the current subtitle buffer to a file.
This function retrieves all subtitle text, strips any HTML-like tags (such
as WebVTT timing or style tags within the text lines), and then saves the
result to a user-specified file."
    (interactive)
    (let* ((subtitles (subed-subtitle-list))
           (raw-text (if subtitles
                         (subed-subtitle-list-text subtitles nil) ; nil = do not include comments
                       ""))
           (cleaned-text (if (string-empty-p raw-text)
                             ""
                           (let* ((lines (split-string raw-text "\n" t)) ; OMIT-NULLS is t
                                  (stripped-lines (mapcar #'subed--strip-tags lines))
                                  (unique-lines (seq-uniq stripped-lines)))
                             (mapconcat #'identity unique-lines "\n"))))
           (buffer-filename (buffer-file-name))
           (default-output-name (if buffer-filename
                                    (concat (file-name-sans-extension buffer-filename) ".txt")
                                  "transcript.txt"))
           (output-file (read-file-name "Export clean transcript to: " nil default-output-name t)))
      (if output-file
          (progn
            (with-temp-file output-file
              (insert cleaned-text))
            (message "Transcript exported to %s" output-file))
        (message "Transcript export cancelled")))))

spofy

spofy is a Spotify player for Emacs.

(use-package spofy
  :ensure (:host github
                 :repo "benthamite/spofy")
  :defer t
  :custom
  (spofy-client-id (auth-source-pass-get "spofy-id" "chrome/accounts.spotify.com"))
  (spofy-client-secret (auth-source-pass-get "spofy-secret" "chrome/accounts.spotify.com"))
  (spofy-tab-bar-max-length nil)
  (spofy-tab-bar-alignment 'right)
  (spofy-enable-tab-bar t)
  (spofy-consult-columns '((track 50 40 40) (album 50 40 6) (artist 50 45) (playlist 50 35) (device 45)))

  :bind (("A-y" . spofy-menu)))

misc

epoch

(use-package epoch
  :ensure (:host github
                 :repo "benthamite/epoch.el")
  :demand t
  :bind
  (("H-l" . epoch-menu)))

calc

calc is the Emacs calculator.

(use-feature calc
  :config
  (with-eval-after-load 'savehist
    (add-to-list 'savehist-additional-variables 'calc-quick-calc-history))

  :bind
  (("A-c" . calc)
   ("A-M-c" . quick-calc)
   :map calc-mode-map
   ("C-k" . nil)))

calc-ext

calc-ext provides various extension functions for calc.

(use-feature calc-ext
  :after cal
  :bind (:map calc-alg-map
              ("C-k" . nil)))

alert

alert is a Growl-like alerts notifier for Emacs.

(use-package alert
  :defer t
  :custom
  ;; the settings below are not working; is it because `alert-default-style' is set to `notifier'?
  (alert-fade-time 2)
  (alert-persist-idle-time 60)
  (alert-default-style 'osx-notifier)

  :config
  ;; This function has to be loaded manually, for some reason.
  (defun alert-osx-notifier-notify (info)
    (apply #'call-process "osascript" nil nil nil "-e"
           (list (format "display notification %S with title %S"
                         (alert-encode-string (plist-get info :message))
                         (alert-encode-string (plist-get info :title)))))
    (alert-message-notify info)))

midnight

midnight runs custom processes every night.

(use-feature midnight
  :defer 30
  :custom
  (clean-buffer-list-kill-never-buffer-names
   '("*mu4e-headers*"
     " *mu4e-update*"))
  (clean-buffer-list-kill-never-regexps
   '("\\` \\*Minibuf-.*\\*\\'"
     "^untitled.*"))
  (clean-buffer-list-delay-general 2) ; kill buffers unused for more than three days

  :config
  (midnight-mode)
  ;; setting the delay causes midnight to run immediately, so we set it via a timer
  (run-with-idle-timer (* 3 60 60) nil (lambda () (midnight-delay-set 'midnight-delay "5:00am")))

  (dolist (fun (nreverse '(files-extras-save-all-buffers
                           clean-buffer-list
                           ledger-mode-extras-update-coin-prices
                           ledger-mode-extras-update-commodities
                           magit-extras-stage-commit-and-push-all-repos
                           org-roam-db-sync
                           org-extras-id-update-id-locations
                           el-patch-validate-all
                           org-extras-agenda-switch-to-agenda-current-day
                           mu4e-extras-update-all-mail-and-index
                           org-gcal-sync
                           elfeed-update)))
    (add-hook 'midnight-hook
              (apply-partially #'simple-extras-call-verbosely
                               fun "Midnight hook now calling `%s'..."))))

bbdb

bbdb is a contact management package.

A tutorial for this undocumented package may be found here.

(use-package bbdb
  :ensure (:host github
                 :repo "benthamite/bbdb"
                 :files (:defaults "lisp/*.el")
                 :depth nil
                 :pre-build (("./autogen.sh")
                             ("./configure")
                             ("make"))
                 :build (:not elpaca-build-docs))

  :custom
  (bbdb-file (file-name-concat paths-dir-bbdb "bbdn.el"))
  (bbdb-image-path (file-name-concat paths-dir-bbdb "media/"))

  :config
  (bbdb-initialize 'anniv)

  :bind
  (("A-b" . bbdb)
   :map bbdb-mode-map
   ("A-C-s-r" . bbdb-prev-record)
   ("A-C-s-f" . bbdb-next-record)
   ("c" . bbdb-copy-fields-as-kill)
   ("C-k" . nil)
   ("M-d" . nil)))

bbdb-extras

bbdb-extras collects my extensions for bbdb.

(use-personal-package bbdb-extras
  :bind (:map bbdb-mode-map
              ("D" . bbdb-extras-delete-field-or-record-no-confirm)
              ("E" . bbdb-extras-export-vcard)
              ("n" . bbdb-extras-create-quick)))

bbdb-vcard

bbdb-vcard supports import and export for BBDB.

(use-package bbdb-vcard
  :after bbdb
  :custom
  (bbdb-vcard-directory paths-dir-bbdb)
  (bbdb-vcard-media-directory "media"))

macos

macos is a simple package I developed that provides a few macOS-specific functions.

(use-package macos
  :ensure (:host github
           :repo "benthamite/macos")
  :custom
  (macos-bluetooth-device-list
        '(("Sonny WH-1000XM5" . "ac-80-0a-37-41-1e")))

  :bind
  (("C-M-s-c" . macos-bluetooth-device-dwim)
   ("C-M-s-g" . macos-set-dication-language)))

keycast

keycast shows the current command and its key in the mode line.

(use-package keycast
  :defer t
  :config
  ;; support for doom modeline (github.com/tarsius/keycast/issues/7)
  (with-eval-after-load 'keycast
    (define-minor-mode keycast-mode
      "Show current command and its key binding in the mode line."
      :global t
      (if keycast-mode
          (add-hook 'pre-command-hook 'keycast--update t)
        (remove-hook 'pre-command-hook 'keycast--update)))
    (add-to-list 'global-mode-string '("" keycast-mode-line))))

activity-watch-mode

activity-watch-mode is an Emacs watcher for ActivityWatch.

(use-package activity-watch-mode
  :defer 30
  :config
  (require 'magit)
  (global-activity-watch-mode)

  (advice-add 'activity-watch--save :around
              (lambda (fn &rest args)
                "Ignore errors from activity-watch (e.g. missing directories)."
                (condition-case nil
                    (apply fn args)
                  (error nil)))))

custom

custom provides the Customize interface for user options and themes.

(use-feature custom
  :custom
  (custom-safe-themes t)
  (custom-file (make-temp-file "gone-baby-gone"))  ; move unintended customizations to a garbage file

  :bind
  (:map custom-mode-map
        ("f" . ace-link-custom)))

mercado-libre

mercado-libre is a package for querying MercadoLibre, a popular Latin American e-commerce platform.

(use-package mercado-libre
  :ensure (:host github
                 :repo "benthamite/mercado-libre")
  :defer t
  :custom
  (mercado-libre-client-id (auth-source-pass-get "app-id" "chrome/mercadolibre.com/benthamite"))
  (mercado-libre-client-key (auth-source-pass-get "app-key" "chrome/mercadolibre.com/benthamite"))
  (mercado-libre-new-results-limit nil)
  (mercado-libre-listings-db-file
   (file-name-concat paths-dir-dropbox "Apps/Mercado Libre/mercado-libre-listings.el")))

polymarket

polymarket is a package to fetch and place trades on Polymarket, a popular prediction market.

(This package is not currently public.)

(use-package polymarket
  :ensure (:host github
                 :repo "benthamite/polymarket")
  :defer t)

kelly

kelly is a Kelly criterion calculator.

(use-package kelly
  :ensure (:host github
                 :repo "benthamite/kelly")
  :defer t

  :custom
  (kelly-b-parameter-type 'probability))

fatebook

fatebook is an Emacs package to create predictions on Fatebook.

(use-package fatebook
  :ensure (:repo "sonofhypnos/fatebook.el"
                 :host github
                 :files ("fatebook.el"))
  :commands fatebook-create-question
  :custom
  (fatebook-api-key-function (lambda () (auth-source-pass-get "api" "chrome/fatebook.io"))))

keyboard-maestro

Keybindings for triggering Emacs commands from Keyboard Maestro.

These bindings allow Keyboard Maestro to trigger various Emacs processes.

Note to self: The pattern for KM shortcuts is C-H-<capital-letter>. This corresponds to ⇧⌘⌃<letter> in macOS.

(global-set-key (kbd "C-H-Z") 'zotra-extras-add-entry)

tetris

tetris is an implementation of the classic Tetris game for Emacs.

And finally, the section you’ve all been waiting for.

(use-feature tetris
  :bind
  (:map tetris-mode-map
   ("k" . tetris-rotate-prev)
   ("l" . tetris-move-down)
   ("j" . tetris-move-left)
   (";" . tetris-move-right)))

appendices

key bindings

Emacs has five native modifier keys: Control (C), Meta (M), Super (s), Hyper (H), and Alt (A). (The letter abbreviation for the Super modifier is s because S is assigned to the Shift key.) I use Karabiner-Elements, in combination with a Moonlander keyboard, to generate several additional “pseudo modifiers”, or mappings between individual keys and combinations of two or more Emacs modifiers:

So when you see a monstrous key binding such as C-H-M-s-d, remember that everything that precedes the final key (in this case, d) represents a single key press (in this case, l). For details, see my Karabiner config file, specifically the “Key associations” section.

I set key bindings in the following ways:

  • With the :bind keyword of use-package.
    • For commands provided by the package or feature being loaded in that block.
    • For commands provided by other packages or features, when these are being set in a keymap provided by the feature being loaded in that block. This approach is appropriate when one wants to bind a key to a command in a keymap and wants this binding to be active even before the feature providing the command is loaded. Example:
  • With bind-keys within the :init section of use-package.
    • For commands provided by the feature being loaded in that block that are bound in a keymap provided by another feature. This is appropriate when the feature providing the keymap may load after the feature providing the command. In these cases, using :bind is not possible since Emacs will throw an error. Example: binding the command scroll-down-command, which is provided by window, to the key y in elfeed-show-mode-map, which is provided by elfeed. Note that if the command is not natively autoloaded, an autoload must be set, e.g. (autoload #'=scroll-down-command "window" nil t). [confirm that this is so: it’s possible bind-keys automatically creates autoloads, just like the :bind keyword does]
    • For commands provided by the feature being loaded in that block that are bound globally and should be available even before the feature is configured. (Note that I distinguish between the loading of a feature and its configuration. A key binding specified via the :bind keyword of use-package will be available before the package is loaded but only after it is configured. If, for example, the block includes the :after keyword, the package will only be configured after that condition is satisfied.) Example: ebib-extras-open-or-switch, which is provided by ebib-extras and will be configured after ebib is loaded, yet we want it to be available even before ebib is loaded.

profiling

  • If you use use-package, the command use-package-report displays a table showing the impact of each package on load times.

installation

For personal reference, these are the most recent Emacs installations (in reverse chronological order).

(After installing, you may need to create a symlink to the Emacs.app folder in /opt/homebrew/Cellar/emacs-plus@30/30.1/Emacs.app, replacing 30.1 with the actual version number.)

[2025-06-27 Fri]:

brew tap d12frosted/emacs-plus
brew install emacs-plus@30 --with-dbus --with-debug --with-xwidgets --with-imagemagick --with-spacemacs-icon

[2024-10-09 Wed]:

brew tap d12frosted/emacs-plus
brew install emacs-plus@30 --with-dbus --with-debug --with-native-comp --with-xwidgets --with-imagemagick --with-spacemacs-icon

[2024-03-18 Mon]:

brew tap d12frosted/emacs-plus
brew install emacs-plus@29 --with-dbus --with-debug --with-native-comp --with-xwidgets --with-imagemagick --with-spacemacs-icon

???:

brew tap d12frosted/emacs-plus
brew install emacs-plus@30 --with-dbus --with-debug --with-native-comp --with-xwidgets --with-imagemagick --with-spacemacs-icon

[2023-02-23 Thu 02:10]

brew tap d12frosted/emacs-plus
brew install emacs-plus@28 --with-dbus --with-no-titlebar --with-native-comp --with-xwidgets --with-imagemagick --with-spacemacs-icon
  • Very slow.
  • Theme broke for some reason.
  • Some functions (e.g. keymap-unset) not available).
  • Telega doesn’t show profile pics

[2023-02-14 Tue 20:07]:

brew tap d12frosted/emacs-plus
brew install emacs-plus@30 --with-dbus --with-debug --with-native-comp --with-xwidgets --with-imagemagick --with-spacemacs-icon

[2023-02-07 Tue 21:52]:

brew install emacs-mac --with-dbus --with-starter --with-natural-title-bar --with-native-comp --with-mac-metal --with-xwidgets --with-imagemagick  --with-librsvg  --with-spacemacs-icon

other config files

The below is a link dump for config files and other related links I have found useful in the past or may want to check out for ideas at some point in the future.

Literate configuration

Some useful config files: