Modular GNU Emacs Configuration

Table of Contents

1. Goals

This is a new attempt at writing a depersonalized, copyable, and modular Emacs config:

  • All Emacs config (elisp) is written in a single Org Mode file:
  • Your configuration is displayed (exported) as a single HTML page:
  • All Org-Babel code blocks are "tangled" into normal elisp files and published to the git repository:
    • emacs.org remains the single "source of truth" of this config.
    • However, it is the tangled elisp files (init.el, modules/*) that Emacs loads on startup, not emacs.org.
    • All of the other files in the repository will be overwritten each time emacs.org is re-tangled.
  • Code blocks should be of medium size, grouping related functions:
    • There should be more code here than prose. Avoid having lots of little code blocks and excessive explanations.
  • All custom elisp functions and variables should have the namespace prefix my/ :
    • This configuration is non-proprietary to promote code adoption by others.
    • Meme: "I made this".
  • This config should support multiple platforms: Linux CLI, Wayland, Android…
  • Most configuration should be customizable by the user without needing to edit this file:
    • Run M-x my/custom-settings to open the configuration menu to save settings on a per-installation basis in ~/.emacs.d/custom.el (and it is .gitignore 'd).
  • The base config should work without needing to download any third party libraries:
    • The custom variable my/machine-labels is used to set per-machine labels that opt-in to loading additional modules.

2. LICENSE

See LICENSE.txt

See LICENSE_GPLv3.txt

This distribution has a mixed license (0BSD and GPLv3). Everything here that has not been identified in the comments as having been taken from somewhere else, is licensed/dedicated to you as public domain (0BSD). Some code has been copied from other sources that are licensed GPLv3 and so these have been identified as such in the code block comments. As long as these additional sources are included in this distribution, this repository is effectively (infectively) licensed as GPLv3.

3. Dependencies

3.1. Emacs

You will need a recent version of Emacs.

Run this in your shell ::
#e.g., on Fedora:
sudo dnf install emacs
(untangled) emacs-lisp
(message (emacs-version))
GNU Emacs 29.4 (build 1, x86_64-redhat-linux-gnu, GTK+ Version 3.24.43, cairo version 1.18.2)
 of 2025-01-03

3.2. Rustup and Cargo

Rustup is a tool that installs the latest versions of Rust and Cargo in your home directory. Cargo is a user-space source-package manager for Rust programs and libraries. This config uses Cargo to install the optional helper programs used by its modules. Once setup, any user-space packages installed by Cargo are placed in ~/.cargo/bin.

You may be able to find the rustup tool in your system package manager and install it that way:

Run this in your shell ::
# e.g., on Fedora:
sudo dnf install rustup
rustup-init
rustup component add rust-analyzer

But you can also install it the "curl bomb" way on most other systems:

Run this in your shell ::
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup component add rust-analyzer

Cargo packages are declared by Emacs modules themselves:

(untangled) emacs-lisp
;; this example declares the "just" crate needs to be installed.
;; (the actual install is deferred until later):
(my/cargo-dependency "just")

All of the crates are installed together after all of the modules have been loaded.

3.3. Git

Install git via your package manager:

Run this in your shell ::
#e.g., Fedora:
sudo dnf install git

Clone this git repository:

Run this in your shell ::
git clone https://github.com/EnigmaCurry/emacs.git \
    ~/git/vendor/enigmacurry/emacs

Switch to the right branch:

git checkout literate-new

3.4. Tree-sitter

Tree-sitter is an optional dependency. It adds advanced syntax tree parser to language modes.

Run this in your shell ::
#on e.g., Fedora
sudo dnf install libtree-sitter

3.5. Create your Emacs config directory

You can try this config out without modifying your existing config. The following command tells Emacs to load config directly (ignoring your existing config):

Run this in your shell ::
emacs --init-directory ~/git/vendor/enigmacurry/emacs

If you omit --init-directory, the default location Emacs looks for your config is ~/.emacs.d

To set this config as the default, symlink the git repository from ~/.emacs.d :

Run this in your shell ::
ln -s ~/git/vendor/enigmacurry/emacs ~/.emacs.d

Now when you run emacs with no arguments it will load the init.el found in the git repository.

4. Early Init

The early-init.el is the first config file that is loaded on Emacs startup. It is used to set an initial visual look and feel before the GUI is initialized, including an initial dark theme. Additionally, early debug options are set here.

:tangle early-init.el
;; Load an initial theme (will be overriden later in modules/theme/theme.el)
(load-theme 'modus-vivendi)
;; Set a larger default font size (150 = 15 pt size):
(setq my/default-text-height 150)
(set-face-attribute 'default nil :height my/default-text-height)
;; Turn off GUI distractions:
(menu-bar-mode -1) ; Press F10 to bring up the menu if you still need it.
(tool-bar-mode -1)
(scroll-bar-mode -1)
(setq inhibit-startup-screen t)
;; Don't resize the frame when adjusting the font size:
(setq window-resize-pixelwise t)
(setq frame-resize-pixelwise t)
;; Disable package.el in favor of straight.el
(setq package-enable-at-startup nil)

;; Debug options:
;;; Start Emacs with the `--debug-init` argument, to debug errors during startup.
;;; Enter debugger on specific logger regex (see *Messages* buffer):
;; (setq debug-on-message "Example log message to trace")
;;; M-x toggle-debug-on-error
(setq debug-on-error t)

5. Base config

This is the base config of init.el which is loaded in all environments. Only Emacs builtins will be allowed here:

5.1. Core libraries

:tangle init.el
;; core libraries
(require 'cl-lib)

5.2. Nice defaults

:tangle init.el
;; Nice defaults
(setq-default confirm-kill-emacs #'yes-or-no-p)
(setq-default vc-follow-symlinks t)
(setq-default indicate-empty-lines t)
(setq-default indicate-buffer-boundaries 'left)
(setq-default sentence-end-double-space nil)
(setq-default indent-tabs-mode nil)
(setq-default tab-width 4)
(setq-default visible-bell t)
(setq-default dired-listing-switches "-al --group-directories-first")
(setq-default tramp-default-method "ssh")
(setq-default native-comp-deferred-compilation-deny-list nil)
(setq-default browse-url-browser-function 'browse-url-firefox)
(put 'narrow-to-region 'disabled nil)
(put 'downcase-region 'disabled nil)
(put 'upcase-region 'disabled nil)

5.3. Configure file backups and auto-saves

  • Store file backups in ~/.emacs.d/backup rather than being littered everywhere else. Backups should store a file snapshot before every save.
  • Store auto-saves in ~/.emacs.d/auto-save. auto-saves helps you to not to lose your work before you save, by automatically saving idle buffers into the auto-save directory.
:tangle init.el
;; Backups and auto-save
;; Reference: https://www.emacswiki.org/emacs/BackupDirectory
;; Reference: https://www.emacswiki.org/emacs/ForceBackups
(setq backup-by-copying t)
(setq backup-directory-alist
      `(("." . ,(expand-file-name "backup" user-emacs-directory))))
(setq delete-old-versions t)
(setq kept-new-versions 6)
(setq kept-old-versions 2)
(setq version-control t)
(setq vc-make-backup-files t)
(add-hook 'before-save-hook
          (lambda () (setq buffer-backed-up nil)))
;; autosaves go in a separate directory
(let ((auto-save-dir (expand-file-name "auto-save" user-emacs-directory)))
  (make-directory auto-save-dir t)
  (setq auto-save-file-name-transforms
        `((".*" ,auto-save-dir t))))

5.4. Customization

Emacs has a powerful customization feature and this configuration uses it for all the user and machine specific settings.

Enter the interactive customization menu for this config: M-x my/custom-settings

There are several sub-menus of settings for different modules. You can change the values for any of the settings and then press C-x C-s (custom-save), and it will save the custom values in ~/.emacs.d/custom.el. This file is ignored by the git repository, and it is used to store persistent custom values for a specific machine only.

:tangle init.el
;; Store customizations in custom.el
(setq custom-file (locate-user-emacs-file "custom.el"))
(when (file-exists-p custom-file)
  (load custom-file))
;; Store all customizations under my/custom-settings group
(defgroup my/custom-settings nil
  "My custom Emacs settings"
  :group 'emacs)
;; Shortcut to open custom settings:
(defun my/custom-settings ()
  "Open the Emacs customization interface for my custom settings."
  (interactive)
  (customize-group 'my/custom-settings))
(defalias 'my/settings 'my/custom-settings)

5.5. Builtin minor modes to activate everywhere

:tangle init.el
;; Global minor modes
(column-number-mode)
(save-place-mode t)
(savehist-mode t)
(recentf-mode t)
(electric-pair-mode t)

5.6. PATH setup

:tangle init.el
;; Function to add a directory to PATH and exec-path
(defun my/add-exec-path (dir)
  "Add DIR to the environment PATH and exec-path if not already present."
  (unless (member dir exec-path)
    (setenv "PATH" (concat dir path-separator (getenv "PATH")))
    (add-to-list 'exec-path dir)))

5.7. Programming mode

:tangle modules/programming.el
;; Basic programming mode settings
(setq-default display-fill-column-indicator-column 80)
(add-hook 'prog-mode-hook (lambda ()
                            (setq show-trailing-whitespace t)
                            (display-fill-column-indicator-mode 1)
                            (display-line-numbers-mode 1)))

5.8. Shell processes

:tangle init.el
(defun my/shell-execute (command &optional interactive)
  "Run a given shell COMMAND in a new buffer and display its output."
  (interactive)
  (let* ((command-name (car (split-string command)))
         (buffer-name (generate-new-buffer-name (concat "*" command-name " Output*")))
         (buffer (get-buffer-create buffer-name)))
    (with-current-buffer buffer
      (erase-buffer)
      (insert "## Running shell process ...\n"))
    (start-process-shell-command
     command
     buffer
     command)
    (pop-to-buffer buffer)))

6. Cargo

Cargo is used to install some system programs automatically.

6.1. Set cargo binary PATH

Cargo and all of the helper programs you install go in ~/.cargo/bin. Ensure that this path is set (because Emacs might not have read ~/.bashrc):

:tangle modules/cargo.el
(my/add-exec-path "~/.cargo/bin")
(unless (executable-find "cargo")
  (message "cargo binary NOT found."))

6.2. Cargo functions

:tangle modules/cargo.el
 (require 'seq)
 (require 'comint)

 (defun my/cargo-package-installed-p (package)
   "Check if a given cargo PACKAGE is installed.
       PACKAGE can be either a string or a cons cell (CRATE . GIT-URL).
       In either case, this function uses the crate name for the check."
   (let ((pkg (if (consp package) (car package) package)))
     (with-temp-buffer
       (when (eq 0 (call-process "cargo" nil t nil "install" "--list"))
         (goto-char (point-min))
         (search-forward pkg nil t)))))
(defun my/cargo-install (programs)
   "Install a list of PROGRAMS via `cargo install`, skipping those already installed.
 PROGRAMS can be:
 - a single string (with space-separated program names),
 - a list of strings,
 - or a list where some elements are cons cells (CRATE . GIT-REPO).

 For cons cell entries, Cargo is invoked with the --git flag."
   (unless programs
     (user-error "No programs specified for cargo install"))
   ;; If PROGRAMS is a string, split it into a list.
   (setq programs (if (stringp programs)
                      (split-string programs)
                    programs))
   ;; Remove already installed programs.
   (setq programs (seq-remove #'my/cargo-package-installed-p programs))
   ;; Separate into two groups:
   ;;   - crates from crates.io (plain strings or cons cells treated as a name only)
   ;;   - git dependencies (explicit cons cells)
   (let ((crate-names (mapcar (lambda (dep)
                                (if (consp dep)
                                    (car dep)
                                  dep))
                              (seq-filter (lambda (dep)
                                            (not (consp dep)))
                                          programs)))
         (git-deps (seq-filter #'consp programs)))
     ;; Install crates from crates.io in one command, if any.
     (when crate-names
       (let ((command (concat "cargo install " (string-join crate-names " "))))
         (my/shell-execute command)))
     ;; Install each git dependency separately.
     (dolist (dep git-deps)
       (let* ((crate (car dep))
              (git-repo (cdr dep))
              (command (format "cargo install %s --git %s" crate git-repo)))
         (my/shell-execute command)))))
 (defvar my/cargo-dependencies nil
   "A list of Rust crate dependencies to be installed.
         Each dependency is either a crate name (a symbol or string) or a cons cell
         of the form (CRATE . GIT-REPO).")

 (defun my/cargo-dependency (crate &optional git-repo)
   "Add a Rust CRATE to the list of dependencies.
         If GIT-REPO is provided, the dependency is stored as (CRATE . GIT-REPO).
         Otherwise, only CRATE is stored.
         If the dependency (by crate name) already exists, do nothing."
   (unless (cl-find crate my/cargo-dependencies
                    :test (lambda (d c)
                            (if (consp d)
                                (equal (car d) c)
                              (equal d c))))
     (push (if git-repo (cons crate git-repo) crate)
           my/cargo-dependencies)))

6.3. Justfile

Just is better than Make.

:tangle modules/just/just.el
;; depend on the just crate so its installed automatically
;; you must add "just" to the list of my/machine-labels for this to work:
(my/cargo-dependency "just")

This Justfile consolidates commands that operate on this git repository:

:tangle Justfile
set export
current_dir := `pwd`

# Print this help message for the Justfile targets
help:
    @just -l

# Clean pacage cache
clean:
    rm -rf straight eln-cache
Clean package cache (shell command)
just clean

7. straight.el and use-package

straight.el replaces the builtin package manager of Emacs (package.el). In return, you can install packages directly from git, and lock dependencies for reproducible installs. use-package is an Emacs builtin macro that enables declarative package configuration.

7.1. Lazy-load straight.el

:tangle init.el
(defun my/bootstrap-straight (&rest _)
  "Bootstrap straight.el only if it's not already installed."
  (unless (bound-and-true-p straight--build-dir)
    (let ((bootstrap-file
           (expand-file-name "straight/repos/straight.el/bootstrap.el"
                             user-emacs-directory))
          (bootstrap-version 5))
      (unless (file-exists-p bootstrap-file)
        (with-current-buffer
            (url-retrieve-synchronously
             "https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
             'silent
             'inhibit-cookies)
          (goto-char (point-max))
          (eval-print-last-sexp)
          ))
      (load bootstrap-file nil 'nomessage)
      (setq straight-use-package-by-default t)
      (straight-use-package 'use-package))))
;; Advise `use-package` to initialize straight.el when first called
(advice-add 'use-package :before #'my/bootstrap-straight)

7.2. Track packages added via use-package

:tangle init.el
(defvar my/use-package-tracked-list nil
  "A list to track the names of packages declared via `use-package`.")
(defun my/use-package-tracked-list ()
  "Display the tracked `use-package` packages in a new buffer, one per line.
If the buffer already exists, delete it and recreate it."
  (interactive)
  (let ((buffer-name "*Tracked Packages*"))
    (when (get-buffer buffer-name)
      (kill-buffer buffer-name))
    (let ((buffer (get-buffer-create buffer-name)))
      (with-current-buffer buffer
        (erase-buffer)
        (insert "# Packages tracked via use-package:\n")
        (if my/use-package-tracked-list
            (dolist (pkg (sort my/use-package-tracked-list #'string<))
              (insert (format "%s\n" pkg)))
          (insert "No packages tracked.\n"))
        (read-only-mode 1))
      (pop-to-buffer buffer))))
(defun my/use-package-advice (orig-fun &rest args)
  "Advice around `use-package' to track package names."
  (when (symbolp (car args))
    (push (symbol-name (car args)) my/use-package-tracked-list)
    (setq my/use-package-tracked-list (delete-dups my/use-package-tracked-list)))
  (apply orig-fun args))

(advice-add 'use-package :around #'my/use-package-advice)

8. Configure which modules to load

A modular Emacs configuration is designed to serve various roles running in several different environments. The base config is kept lean, without any third party modules loaded. Each machine may be labeled with the names of additional modules to load.

:tangle init.el
;; Customize which emacs config modules to load on a per-machine basis:
(defcustom my/machine-labels '()
  "List of machine-specific labels to configure which modules to load."
  :type '(repeat string)
  :group 'my/custom-settings)
(defun my/machine-labels ()
  "Return the list of machine-specific labels."
  (interactive)
  my/machine-labels)
(defun my/machine-has-label (label)
  "Check if the current machine is labeled with LABEL."
  (if (member label my/machine-labels)
      t
    nil))
(defun my/machine-labels-available ()
  "List all available machine labels"
  (let ((modules-dir (expand-file-name "modules" user-emacs-directory)))
    (cl-sort (mapcar #'file-name-nondirectory
                     (seq-filter #'file-directory-p
                                 (directory-files modules-dir t "^[^.]" t))) #'string<)))
(defun my/machine-labels-enable-all nil
  "Adds ALL existing machine labels to the custom my/machine-labels"
  (interactive)
  (let ((modules-dir (expand-file-name "modules" user-emacs-directory)))
    (when (y-or-n-p (format "Do you want to enable ALL Emacs modules from %s? " modules-dir))
      (progn
        (customize-set-variable 'my/machine-labels (my/machine-labels-available))
        (customize-save-customized)))))

Here are the available modules:

- avy
- completion
- dashboard
- fonts
- general
- gptel
- just
- latin-words
- lsp
- magit
- markdown
- org
- python
- registers
- rust
- s3-publish
- ssh-agent
- text-scale
- theme
- treesit
- vterm
- which-key

All modules are disabled by default! You must customize my/machine-labels on each machine you configure.

M-x my/custom-settings

This will open the customization interface allowing you to set the list named My Machine Labels. For example, add each of the labels you want to enable for the current machine: general, org, markdown, etc:

Example ::
↓ My Machine Labels:
Repeat:
INS DEL String: general
INS DEL String: org
INS DEL String: markdown
INS
    State : SAVED and set.
   List of machine-specific labels to configure which modules to load.

When done, press C-x C-s to save the settings.

If you want to enable ALL existing modules, you can run M-x my/machine-labels-enable-all (the result will be saved in custom.el). (If new modules are added to the git repository, they will not be enabled until you run this again, or by customizing my/machine-labels.)

You can then remove any of the modules you don't want, the same way as before: M-x my/custom-settings.

You should restart Emacs to reload the configured modules from custom.el.

9. Modules

9.1. Registers (label registers)

Registers are storage compartments in Emacs, which can hold several different types of things: strings, numbers, buffer positions, frame configs, etc.

9.1.1. Position registers

Position Registers store the point position of buffers, so you can quickly come back to it from anywhere.

  • C-c j e jump to the Emacs config file (emacs.org)
  • C-c j g open the user's git directory (You must customize my/git-user-directory)
  • C-c j v open the vendor git directory (You may customize my/git-vendor-directory)
  • C-c j d open the Docker projects directory (d.rymcg.tech)

You should change the following values to suit your own environment by running M-x my/custom-settings. Enter the My Path Settings sub-menu, and change the following settings:

  • my/git-vendor-directory Set this to where you clone git repositories (not necessarily your own. e.g., ~/git/vendor).
  • my/git-user-directory Set this to the directory where you store your personal git repositories (e.g., ~/git/vendor/YOUR_USERNAME).
:tangle modules/registers/registers.el
(defgroup my/path-settings nil
  "My custom path settings"
  :group 'my/custom-settings)
(defcustom my/git-vendor-directory "~/git/vendor"
  "My git vendor directory"
  :type 'string
  :group 'my/path-settings)
(defcustom my/git-user-directory "~/git/vendor/enigmacurry"
  "My personal git repositories directory"
  :type 'string
  :group 'my/path-settings)

(set-register ?e `(file . ,(expand-file-name "emacs.org" user-emacs-directory)))
(set-register ?g `(file . ,my/git-user-directory))
(set-register ?d `(file . ,(expand-file-name "enigmacurry/d.rymcg.tech/" my/git-vendor-directory)))
(set-register ?v `(file . ,my/git-vendor-directory))

The F1, F2, F3, and F4 keys store temporary positions:

  • C-<f1> jump to register stored in the F1 register.
  • C-<f2> jump to register stored in the F2 register.
  • C-<f3> jump to register stored in the F3 register.
  • C-<f4> jump to register stored in the F4 register.
  • M-<f1> store the current point into the F1 register.
  • M-<f2> store the current point into the F2 register.
  • M-<f3> store the current point into the F3 register.
  • M-<f4> store the current point into the F4 register.
:tangle modules/registers/registers.el
(defun my/register-save-f1 () (interactive) (point-to-register 'my/register-f1))
(defun my/register-jump-f1 () (interactive) (jump-to-register 'my/register-f1))
(defun my/register-save-f2 () (interactive) (point-to-register 'my/register-f2))
(defun my/register-jump-f2 () (interactive) (jump-to-register 'my/register-f2))
(defun my/register-save-f3 () (interactive) (point-to-register 'my/register-f3))
(defun my/register-jump-f3 () (interactive) (jump-to-register 'my/register-f3))
(defun my/register-save-f4 () (interactive) (point-to-register 'my/register-f4))
(defun my/register-jump-f4 () (interactive) (jump-to-register 'my/register-f4))

9.2. Theme (label theme)

You can cycle the current theme via C-<f11> and C-<f12>. As you do this, the theme will be made persistent by automatically customizing my/theme which is loaded on startup.

:tangle modules/theme/theme.el
(defgroup my/theme-settings nil
  "My custom theme settings"
  :group 'my/custom-settings)
(defcustom my/theme 'deeper-blue
  "Emacs Theme"
  :type 'symbol
  :group 'my/theme-settings)

(defun my/theme-update (theme-fn)
  "Update the `my/theme` variable with the new theme and call THEME-FN."
  (let ((current-theme (car custom-enabled-themes)))
    (funcall theme-fn)
    (customize-set-variable 'my/theme (car custom-enabled-themes))
    (customize-save-customized)
    (message "Theme changed to: %s" my/theme)))

;; Install themes directly from a git repository:
;;;NOTE: don't use deep-thought-theme it crashes Emacs 29.4!!
;;;Keeping this here as an example for loading a theme from git:
;; (use-package
;;   deep-thought-theme
;;   :straight
;;   (deep-thought-theme :type git :repo "https://github.com/emacsfodder/emacs-deep-thought-theme.git"))
(use-package solaire-mode :init (solaire-global-mode +1))
(use-package theme-looper
  :general
  ("C-<f11>" (lambda () (interactive) (my/theme-update 'theme-looper-enable-previous-theme)))
  ("C-<f12>"  (lambda () (interactive) (my/theme-update 'theme-looper-enable-next-theme))))

(load-theme my/theme t)

9.3. Fonts (label fonts)

9.3.1. List installed font families

(untangled) emacs-lisp
(mapconcat (lambda (x) (format "- %s" x))
           (delete-dups (sort (font-family-list) #'string<)) "\n")

9.3.2. Install font: JetBrains Mono Nerdfont

JetBrains Mono typeface Nerd Fonts

:tangle modules/fonts/fonts.el
;;; Download JetBrains Mono typeface
(let* ((url "https://github.com/ryanoasis/nerd-fonts/releases/download/v3.3.0/JetBrainsMono.zip")
       (zip-file (expand-file-name "JetBrainsMono.zip" temporary-file-directory))
       (font-dir (expand-file-name "~/.local/share/fonts/JetBrainsMonoNerdFont/"))
       (default-directory temporary-file-directory))
  (unless (file-directory-p font-dir)
    (url-copy-file url zip-file t)
    (make-directory font-dir t)
    (let ((output-buffer (generate-new-buffer "*unzip-output*")))
      (unwind-protect
          (call-process "unzip"
                        nil output-buffer nil "-j" zip-file "-d" font-dir)
        (kill-buffer output-buffer)))
    (call-process "fc-cache" nil nil nil "-fv")
    (delete-file zip-file)
    (message "JetBrains Mono Nerd Font installed successfully.")))

9.3.3. Set font faces

:tangle modules/fonts/fonts.el
;;; show list of installed fonts:
;;(font-family-list)
;;; show default font:
;;(face-attribute 'default :font)
;;; Set default font faces:
;; M-x my/custom-settings    :: font-settings group:
(defgroup my/font-settings nil
  "My custom font settings"
    :group 'my/custom-settings)
(defcustom my/font-family-default "JetBrainsMono Nerd Font"
  "Default font family"
  :type 'string
  :group 'my/font-settings)
(defcustom my/font-size-default 120
  "Default font size"
  :type 'string
  :group 'my/font-settings)
(defun my/font-settings-apply ()
  "Set the default font based on
    `my/font-family-default` and `my/font-size-default`."
  (set-face-attribute 'default nil
                      :family my/font-family-default
                      :height my/font-size-default)
  t)
(my/font-settings-apply)
(add-hook 'after-init-hook #'my/font-settings-apply)
(advice-add 'custom-save-all :after (lambda ()
            "Re-apply custom settings after saving customizations."
            (my/font-settings-apply)))

;; Use nerd icons
(use-package nerd-icons
  :custom
  (nerd-icons-font-family "JetBrainsMono Nerd Font"))

Run M-x my/custom-settings and go to the font customize sub-menu. Choose your default font family and font size for your machine. Use M-x describe-char and M-x describe-font to help debug font selection.

9.3.4. Preview fonts

:tangle modules/fonts/fonts.el
(use-package show-font)
(untangled) emacs-lisp
(setq show-font-pangram
      (concat "A wizard’s job is to vex chumps quickly in fog."
              "\n\t٩(-̮̮̃-̃)۶ ٩(●̮̮̃•̃)۶ ٩(͡๏̯͡๏)۶ ٩(-̮̮̃•̃)."
              "\n\tΣὲ γνωρίζω ἀπὸ τὴν κόψη"))
(show-font-list)

9.4. General keybindings manager (label general)

:tangle modules/general/general.el
(use-package
  general
  :init
  ;; Switch between two most recent buffers:
  (fset 'quick-switch-buffer [?\C-x ?b return])
  :config
   ;;; Custom global bindings:
  (general-define-key
   "C-h B"     'general-describe-keybindings
   "s-b"       'quick-switch-buffer
   "s-B"       'buffer-menu-other-window
   "C-x B"     'buffer-menu-other-window
   "s-o"       'browse-url
   "C-;"       'comment-region ; C-u C-; to uncomment
   "s-<down-mouse-1>"       'mouse-drag-region-rectangle
   "C-x j"     'jump-to-register
   "C-c j"     'jump-to-register
   "C-<f1>"    'my/register-jump-f1
   "M-<f1>"    'my/register-save-f1
   "C-<f2>"    'my/register-jump-f2
   "M-<f2>"    'my/register-save-f2
   "C-<f3>"    'my/register-jump-f3
   "M-<f3>"    'my/register-save-f3
   "C-<f4>"    'my/register-jump-f4
   "M-<f4>"    'my/register-save-f4
   )
  ;;; Put the Emacs default keybindings you want included
  ;;;   in general-describe-keybindings here:
  ;;; Its useful to duplicate these simply as a way of documentation:
  (general-define-key
   "M-SPC"     'cycle-spacing ; If you document it, you will use it.
   "M-h"       'mark-paragraph ; C-h B is like your personal cheat sheet.
   "C-h b"     'describe-bindings ;; default binding for documentation purpose
   "C-x 4 c"   'clone-indirect-buffer-other-window ;; default binding
   )
  ;;; Define bindings for specific builtin (non use-package) modes:
  ;; Emacs Lisp mode bindings:
  (general-define-key
   :keymaps 'emacs-lisp-mode-map
   "s-e" 'eval-defun ;eval top-level form
   "M-;" 'paredit-comment-dwim)
  ;; Dired mode bindings:
  (general-define-key
   :keymaps 'dired-mode-map "C-c C-q" 'dired-toggle-read-only))

9.5. Which key (label which-key)

:tangle modules/which-key/which-key.el
;; which-key (shows keyboard shortcut completions) :: https://github.com/justbur/emacs-which-key
(use-package which-key :config (which-key-mode))

9.6. Org mode (label org)

:tangle modules/org/org.el
;; hydra (rapid fire mnemonic keybindings) :: https://github.com/abo-abo/hydra
(use-package hydra)
(use-package org
  :after hydra
  :hook ((org-mode . flyspell-mode)
         (org-mode . unpackaged/org-export-html-with-useful-ids-mode))
  :custom
  (org-html-validation-link nil)
  (org-html-use-infojs nil)
  (org-html-postamble 'auto)
  (org-export-with-author t)
  (org-export-with-date t)
  (org-export-with-creator nil)
  (org-export-with-email nil)
  (org-export-timestamp-file t)
  (org-export-allow-bind-keywords t)
  (org-directory "~/Org")
  :general
  ("s-<up>" 'org-previous-visible-heading)
  ("s-<down>" 'org-next-visible-heading)
  ("C-c o k" 'org-babel-remove-result)
  :config
  (setq org-startup-folded t)
  (defun my-org-html--translate (original-function keyword info)
    "Custom advice to translate the keyword 'Created' to 'Last Modified'."
    (if (string-equal keyword "Created")
        "Last Modified"
      (funcall original-function keyword info)))
  (advice-add 'org-html--translate :around #'my-org-html--translate)
  (org-babel-do-load-languages
   'org-babel-load-languages
   '((python . t) (scheme . t) (shell . t) (ditaa . t)))
  :init
  (defcustom my/org-notes-directory "~/Org/notes" "My org notes directory")
  (defcustom my/org-notes-export-directory "~/Org/export/notes" "My org notes HTML export directory")
  ;; Hydra for commonly used org commands:
  (defhydra
    hydra-org
    (global-map "C-c o" :exit t color pink :hint nil)
    "Org commands:"
    ("o" my/org-open-file)
    ("l" org-store-link "store link")
    ("i" org-insert-link "insert link")
    ("a" org-agenda "agenda")
    ("c" org-capture "capture")
    ("m" org-info "read info manual")
    ("e" org-export-dispatch "export")
    ("p" org-preview-html-mode "toggle preview mode")
    ("s" org-insert-source-code-block "insert source code block"))
  )
(require 'org-tempo) ; required for Structure Templates
                                        ; See https://orgmode.org/manual/Structure-Templates.html
(use-package htmlize) ; required for colorized HTML code blocks
(use-package org-preview-html :after org)
(use-package ob-async)
(progn ; electic pairs for org-mode
  (modify-syntax-entry ?/ "\"" org-mode-syntax-table)
  (modify-syntax-entry ?* "\"" org-mode-syntax-table)
  (modify-syntax-entry ?= "\"" org-mode-syntax-table)
  (modify-syntax-entry ?+ "\"" org-mode-syntax-table)
  (modify-syntax-entry ?_ "\"" org-mode-syntax-table)
  (modify-syntax-entry ?~ "\"" org-mode-syntax-table))
:tangle ~/Org/notes/.dir-locals.el
((org-mode
  . ((org-confirm-babel-evaluate . nil)
     (eval .
           (add-hook 'after-save-hook
                     (lambda ()
                       (when (and buffer-file-name
                                  (string= "org" (file-name-extension buffer-file-name)))
                         (my/org-babel-tangle buffer-file-name)))
                     nil t))))))

9.6.1. Tangle Emacs config and write HTML document

This function tangles this file (emacs.org) and exports an HTML page (emacs.html):

:tangle modules/org/org.el
(defcustom my/org-html-theme "simple_dark" "the name of my custom org theme (CSS)")
(defun my/emacs-org-tangle ()
  "Tangle all code blocks in 'emacs.org' and export this document to HTML."
  (let* ((org-file (expand-file-name "emacs.org" user-emacs-directory))
         (modules-dir (expand-file-name "modules" user-emacs-directory))
         (export-dir (expand-file-name my/org-notes-export-directory))
         (export-emacs-dir (expand-file-name "emacs" export-dir))
         (export-file (expand-file-name "index.html" user-emacs-directory)))
    (when (file-exists-p org-file)
      (with-current-buffer (find-file-noselect org-file)
        (delete-directory modules-dir t)
        (org-babel-tangle)
        (org-export-to-file 'html export-file)
        (unless (file-directory-p export-dir)
          (make-directory export-dir t))
        (unless (file-directory-p export-emacs-dir)
          (make-directory export-emacs-dir t))
        (my/org-create-theme-file)
        (make-symbolic-link "index.html" "emacs.html" t)
        (make-symbolic-link (expand-file-name "index.html" user-emacs-directory) (expand-file-name "index.html" export-emacs-dir) t)
        (make-symbolic-link (expand-file-name "index.html" user-emacs-directory) (expand-file-name "emacs.html" export-emacs-dir) t)
        (make-symbolic-link (expand-file-name "early-init.el" user-emacs-directory) (expand-file-name "early-init.el" export-emacs-dir) t)
        (make-symbolic-link (expand-file-name "init.el" user-emacs-directory) (expand-file-name "init.el" export-emacs-dir) t)
        (make-symbolic-link (expand-file-name "modules" user-emacs-directory) (expand-file-name "modules" export-emacs-dir) t)
        (make-symbolic-link (expand-file-name "LICENSE.txt" user-emacs-directory) (expand-file-name "LICENSE.txt" export-emacs-dir) t)
        (make-symbolic-link (expand-file-name "LICENSE_GPLv3.txt" user-emacs-directory) (expand-file-name "LICENSE_GPLv3.txt" export-emacs-dir) t)
        (make-symbolic-link (expand-file-name "theme" user-emacs-directory) (expand-file-name "theme" export-dir) t)
        (make-symbolic-link (expand-file-name "theme" user-emacs-directory) (expand-file-name "theme" my/org-notes-directory) t)
        ))))
(with-eval-after-load 'ox-html
  (defun my/org-html-src-block (orig-fun src-block contents info)
    "Advice for `org-html-src-block' to add a header.
If a :tangle header is specified (and not \"no\"), it shows the tangle file.
If the block is a shell block, it prints 'Run in Bash shell:'.
Otherwise, it prints the code block's language.
ORIG-FUN is the original function; SRC-BLOCK is the source block;
INFO is the export options plist."
    (let* ((parameters (org-element-property :parameters src-block))
           (header-args (org-babel-parse-header-arguments parameters))
           (tangle (cdr (assoc :tangle header-args)))
           (lang (org-element-property :language src-block))
           (header (cond
                    ((and tangle (not (string= tangle "no")))
                     (format "<div class=\"code-block-header tangle\"><span class=\"org-parameter\">:tangle</span> <span class=\"filename\">%s</span></div>\n"
                             (org-html-encode-plain-text tangle)))
                    ((string= lang "shell")
                     "<div class=\"code-block-header shell\">Run this in your shell ::</div>\n")
                    ((string= lang "example")
                     "<div class=\"code-block-header example\">Example ::</div>\n")
                    (lang
                     (format "<div class=\"code-block-header lang\">(untangled) %s</div>\n"
                             (org-html-encode-plain-text lang)))
                    (t "")))
           (code (funcall orig-fun src-block contents info)))
      (if (not (string= header ""))
          (format "<div class=\"code-block-container\">%s%s</div>" header code)
        code)))
  (advice-add 'org-html-src-block :around #'my/org-html-src-block))

;; Tell Emacs to trust this code in all buffer local vars:
(add-to-list 'safe-local-variable-values
             '(eval add-hook 'after-save-hook 'my/emacs-org-tangle nil t))
(add-to-list 'safe-local-variable-values
             '(org-confirm-babel-evaluate))

9.6.2. Local HTTP server for displaying exported HTML

:tangle modules/org/org.el
(my/cargo-dependency "live-server") ; defers install of live-server Rust crate
(defvar my/org-html-server-host "127.0.0.1") ; Set to 0.0.0.0 to serve publicly
(defvar my/org-html-server-port "7776")
(defun my/org-html-server (&optional redirect)
  "Start a local live-server for my org notes.
  If the server is already running, open the URL."
  (interactive)
  (let* ((host my/org-html-server-host)
         (port my/org-html-server-port)
         (redirect (or redirect ""))
         (url (format "http://%s:%s/%s" host port redirect))
         (live-server-proc (get-process "live-server")))
    (if (and live-server-proc (process-live-p live-server-proc))
        (progn
          (message "live-server already running; opening %s" url)
          (browse-url url))
      (let ((live-server-path (executable-find "live-server"))
            (log-buffer-name "*my/org-html-server*")
            (export-dir (expand-file-name my/org-notes-export-directory)))
        (with-current-buffer (get-buffer-create log-buffer-name)
          (if live-server-path
              (progn
                (message "live-server found at: %s" live-server-path)
                (start-process "live-server" log-buffer-name "live-server"
                               "-H" host "-p" port "-o" redirect export-dir)
                (message "Started live-server on %s" url))
            (message "live-server NOT found – please run: cargo install live-server")))))))
(defun my/emacs-org-html-server ()
  "Start a local live-server and redirect to the emacs page"
  (interactive)
  (my/org-html-server "emacs"))
(defun my/org-notes-html-server ()
  "Start a local live-server and redirect to the current Org note file.
If the current buffer is an Org file in `my/org-notes-directory`, tangle
it and export to HTML before serving it."
  (interactive)
  (let ((org-file (buffer-file-name)))
    (if (and org-file
             (string= "org" (file-name-extension org-file))
             (string-prefix-p (expand-file-name my/org-notes-directory) (expand-file-name org-file)))
        (let ((html-file (concat (file-name-sans-extension org-file) ".html")))
          (my/org-babel-tangle org-file)
          (my/org-html-server (file-name-nondirectory html-file)))
      (message "Current buffer is not an Org file in %s" my/org-notes-directory))))
Start the html server
M-x my/org-html-server
M-x my/emacs-org-html-server

9.6.3. Use org headings as HTML anchors

unpackaged.el: Export to HTML with useful anchors

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

(require 'cl-lib)
(define-minor-mode unpackaged/org-export-html-with-useful-ids-mode
  "Attempt to export Org as HTML with useful link IDs.
  Instead of random IDs like \"#orga1b2c3\", use heading titles,
  made unique when necessary."
  :global t
  (if unpackaged/org-export-html-with-useful-ids-mode
      (advice-add #'org-export-get-reference :override
                  #'unpackaged/org-export-get-reference)
    (advice-remove #'org-export-get-reference
                   #'unpackaged/org-export-get-reference)))
(defun unpackaged/org-export-get-reference (datum info)
  "Like `org-export-get-reference',
        except uses heading titles instead of random numbers."
  (let ((cache (plist-get info :internal-references)))
    (or (car (rassq datum cache))
        (let* ((crossrefs (plist-get info :crossrefs))
               (cells (org-export-search-cells datum))
               ;; Preserve any pre-existing association between
               ;; a search cell and a reference.
               (new (or (cl-some
                         (lambda (cell)
                           (let ((stored (cdr (assoc cell crossrefs))))
                             (when stored
                               (let ((old (org-export-format-reference stored)))
                                 (and (not (assoc old cache)) stored)))))
                         cells)
                        (when (org-element-property :raw-value datum)
                          ;; Heading with a title
                          (unpackaged/org-export-new-title-reference datum cache))
                        ;; Generate new reference
                        (org-export-format-reference
                         (org-export-new-reference cache))))
               (reference-string (replace-regexp-in-string "%20" "_" new)))
          ;; Cache contains both data already associated to
          ;; a reference and in-use internal references, so as to make
          ;; unique references.
          (dolist (cell cells) (push (cons cell new) cache))
          ;; Retain a direct association between reference string and DATUM.
          (push (cons reference-string datum) cache)
          (plist-put info :internal-references cache)
          reference-string))))
(defun unpackaged/org-export-new-title-reference (datum cache)
  "Return new reference for DATUM that is unique in CACHE."
  (cl-macrolet ((inc-suffixf (place)
                  `(progn
                     (string-match (rx bos
                                       (minimal-match (group (1+ anything)))
                                       (optional "--" (group (1+ digit)))
                                       eos)
                                   ,place)
                     ;; HACK: `s1' instead of a gensym.
                     (-let* (((s1 suffix) (list (match-string 1 ,place)
                                                (match-string 2 ,place)))
                             (suffix (if suffix
                                         (string-to-number suffix)
                                       0)))
                            (setf ,place (format "%s--%s" s1 (cl-incf suffix)))))))
    (let* ((title (org-element-property :raw-value datum))
           (ref (url-hexify-string (substring-no-properties title)))
           (parent (org-element-property :parent datum)))
      (while (cl-some (lambda (it) (equal ref (car it))) cache)
        ;; Title not unique: make it so.
        (if parent
            ;; Append ancestor title.
            (setf title (concat (org-element-property :raw-value parent)
                                "--" title)
                  ref (url-hexify-string (substring-no-properties title))
                  parent (org-element-property :parent parent))
          ;; No more ancestors: add and increment a number.
          (inc-suffixf ref)))
      ref)))

9.6.4. Template for new org files

:tangle modules/org/org.el
(defcustom my/org-template
  (concat "#+TITLE: {{title}}\n"
          "#+PROPERTY: header-args :exports both :results both :eval never-export\n"
          "#+OPTIONS: noweb:t\n")
  "My default Org template.")

(defun my/org-create-theme-file ()
  "Create the theme file in the 'theme' directory under `user-emacs-directory`.
  This file wraps the contents of the theme CSS (also in the theme directory)
  with HTML <style> tags for use with Org export."
  (let* ((theme-dir (expand-file-name "theme" user-emacs-directory))
         (theme-file (expand-file-name (concat my/org-html-theme ".theme") theme-dir))
         (css-file (expand-file-name (concat my/org-html-theme ".css") theme-dir)))
    (unless (file-directory-p theme-dir)
      (make-directory theme-dir t))
    (with-temp-file theme-file
      (insert "#+HTML_HEAD: <style>\n")
      (if (file-exists-p css-file)
          (let ((css-content (with-temp-buffer
                               (insert-file-contents css-file)
                               (buffer-string))))
            (dolist (line (split-string css-content "\n" t))
              (insert (format "#+HTML_HEAD: %s\n" line))))
        (insert "#+HTML_HEAD: /* CSS file not found */\n"))
      (insert "#+HTML_HEAD: </style>\n")
      theme-file)))

(defun my/org-open-file ()
  "Open a new Org file with the default notes template and include the theme file.
    This function does the following:
    1. Opens a new Org file in `org-directory/notes` and inserts the template,
       replacing placeholders like {{title}}, {{date}}, etc.
    2. If the directory `org-directory/notes` does not exist, it is created.
    3. It creates (or updates) the theme file via `my/org-create-theme-file`, which is
       stored in `user-emacs-directory/theme/{{theme}}.theme`.
    4. The Org file then includes a line: \"#+SETUPFILE: {{theme}}.theme\" so that
       when you export, Org loads the theme file."
  (interactive)
  (let* ((formatted-date (format-time-string "%Y-%m-%d"))
         (user-title (read-string "Title for new note: "))
         (safe-title (replace-regexp-in-string " " "-" (downcase user-title)))
         ;; Define the notes directory and ensure it exists.
         (notes-dir (file-name-as-directory (concat org-directory "/notes")))
         (_ (unless (file-directory-p notes-dir)
              (make-directory notes-dir t)))
         (filename (format "%s/%s-%s.org"
                           notes-dir
                           (format-time-string "%Y-%m-%d-%H-%M-%S")
                           safe-title))
         (section (read-string "Section: " formatted-date))
         (title-section (mapconcat #'capitalize (split-string section " ") " ")))
    (unless (file-directory-p my/org-notes-directory)
      (make-directory my/org-notes-directory t))
    (find-file filename)
    (when (zerop (buffer-size))
      ;; Insert the Org template with placeholders replaced.
      (insert
       (replace-regexp-in-string
        "{{title}}"
        user-title
        (replace-regexp-in-string
         "{{date}}"
         formatted-date
         (replace-regexp-in-string
          "{{section}}"
          section
          (replace-regexp-in-string
           "{{safe-title}}"
           safe-title
           (replace-regexp-in-string
            "{{title-section}}"
            title-section
            my/org-template))))))
      ;; Create the theme file and insert the setup line.
      (let ((theme-file (my/org-create-theme-file)))
        (insert (format "#+SETUPFILE: theme/%s\n\n"
                        (file-name-nondirectory theme-file)))))))

(defun my/org-babel-tangle (org-file)
  "Tangle and export an Org file to HTML."
  (when (file-exists-p org-file)
    (with-current-buffer (find-file-noselect org-file)
      (org-babel-tangle)
      (my/org-create-theme-file)
      (org-export-to-file 'html
          (expand-file-name (org-export-output-file-name ".html" nil)
                            my/org-notes-export-directory)))))

9.6.5. Org-babel evaluation

:tangle modules/org/org.el
(defun my/save-buffer-after-code-execution (orig-fun &rest args)
  "Execute ORIG-FUN with ARGS and save the buffer afterward.
If the code block is executed asynchronously (i.e. returns a process),
attach a sentinel so that `save-buffer` is called when the process finishes.
Otherwise, call `save-buffer` immediately."
  (let ((result (apply orig-fun args)))
    (if (processp result)
        (set-process-sentinel
         result
         (lambda (_proc event)
           ;; Adjust this if your process returns a different finished event.
           (when (string-match-p "finished" event)
             (save-buffer))))
      (save-buffer))
    result))
(advice-add 'org-babel-execute-src-block :around #'my/save-buffer-after-code-execution)
(advice-add 'rustic-babel-run-update-result-block :around #'my/save-buffer-after-code-execution)

9.7. Completion (label completion)

:tangle modules/completion/completion.el
  ;; Allow minibuffers to stack:
  (setq-default enable-recursive-minibuffers t)
  ;; Filter command completions to only include commands
  ;; applicable to the current major mode:
  (setq-default read-extended-command-predicate
                #'command-completion-default-include-p)
  ;; Add custom prompt when asking for multiple values as (comma) separated list:
  (advice-add #'completing-read-multiple :filter-args
              (lambda (args)
                (cons (format "[CRM%s] %s"
                              (replace-regexp-in-string
                               "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" ""
                               crm-separator)
                              (car args))
                      (cdr args))))
  ;; Do not allow the cursor in the minibuffer prompt
  (setq minibuffer-prompt-properties
        '(read-only t cursor-intangible t face minibuffer-prompt))
  (add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)
(use-package
  consult
  :general ("C-x b" 'consult-buffer)
    (setq consult-buffer-sources
      '(consult--source-buffers
        consult--source-recent-file
        consult--source-project-buffer)))

(use-package vertico
    :custom
    (vertico-scroll-margin 2)
    (vertico-count 10)
    (vertico-resize 'grow-only)
    (vertico-cycle nil)
    :init
    ;;(keymap-set vertico-map "?" #'minibuffer-completion-help)
    ;;(keymap-set vertico-map "M-TAB" #'vertico-insert)
    ;;(keymap-set vertico-map "TAB" #'minibuffer-complete)
    (vertico-mode))

  (use-package orderless
    :custom
    ;; Configure a custom style dispatcher (see the Consult wiki)
    ;; (orderless-style-dispatchers
    ;;   '(+orderless-consult-dispatch orderless-affix-dispatch))
    ;; (orderless-component-separator #'orderless-escapable-split-on-space)
    (completion-styles '(orderless basic))
    (completion-category-defaults nil)
    (completion-category-overrides '((file (styles partial-completion)))))

  (use-package marginalia
    :hook
    ((marginalia-mode . all-the-icons-completion-marginalia-setup))
    :bind (:map minibuffer-local-map
                ("M-A" . marginalia-cycle))
    :init
    (marginalia-mode))

  (use-package nerd-icons-completion
    :init
    (nerd-icons-completion-mode))

(use-package company)

9.8. Vterm (label vterm)

:tangle modules/vterm/vterm.el
;; vterm (terminal emulator) ::
;;  https://github.com/akermu/emacs-libvterm
;; Configure BASH to work with vterm:
;;  https://github.com/akermu/emacs-libvterm#vterm-clear-scrollback
(use-package
  vterm
  :custom (vterm-always-compile-module t)
  :general ("C-c t" 'my/vterm-toggle)
  :config (define-key vterm-mode-map (kbd "<f5>") nil)
  :hook ((vterm-mode . (lambda () (setq-local show-trailing-whitespace nil))))
  :init
  ;; shell-pop for vterm :: https://github.com/jixiuf/vterm-toggle
  (use-package vterm-toggle)
  (defun my/vterm-toggle (&optional args)
    "Customized vterm-toggle wrapper- this fixes the universal
    argument (C-u) to always create a new terminal"
    (interactive "P")
    (if (not
         (or (derived-mode-p 'vterm-mode)
             (and (vterm-toggle--get-window)
                  vterm-toggle-hide-method)))
        (if (equal current-prefix-arg '(4))
            (vterm-toggle--new args)
          (vterm-toggle args))
      (vterm-toggle args))))

9.9. Markdown mode (label markdown)

:tangle modules/markdown/markdown.el
(use-package markdown-mode)

9.10. SSH agent (label ssh-agent)

:tangle modules/ssh-agent/ssh-agent.el
;; Load SSH / GPG keys from keychain agent
(use-package
  keychain-environment
  :straight
  (keychain-environment
   :type git
   :files (:defaults "keychain-environment")
   :host github
   :repo "tarsius/keychain-environment")
  :init (keychain-refresh-environment))

9.11. Magit (label magit)

:tangle modules/magit/magit.el
;; Magit (git version control system) :: https://magit.vc/
(use-package
  magit
  :general ("C-c g" 'magit-status)
  :config
  ;; open magit in a full frame always:
  (setq magit-display-buffer-function
        #'magit-display-buffer-fullframe-status-v1))

9.12. Scale text uniformly in all buffers (label text-scale)

:tangle modules/text-scale/text-scale.el
(defun my/default-text-scale-reset nil
  (setq default-text-scale--complement 0)
  (set-face-attribute 'default
                      nil
                      :height my/default-text-height)
  (message "Text height reset: %d" my/default-text-height)
  )
;; Scale text sizes in all buffers :: https://github.com/purcell/default-text-scale
(use-package
  default-text-scale
  :general
  ("C-="
   'default-text-scale-increase
   "C--"
   (lambda ()
     "Reset text scale if C-u is used, otherwise decrease it."
     (interactive)
     (let ((prefix current-prefix-arg))
       ;; Intercept and clear the prefix argument before calling the function
       (setq current-prefix-arg nil)
       (if prefix
           (my/default-text-scale-reset)
         (default-text-scale-decrease)))))
  :init (setq default-text-scale-amount 5))

9.13. Justfile mode (label just)

:tangle modules/just/just.el
;; just-mode
(use-package just-mode)

9.14. Latin words dictionary and word of the day (label latin-words)

This package requires the system dependency jq.

:tangle modules/latin-words/latin-words.el
(use-package
  latin-words
  :straight
  (latin-words :type git :host github :repo "enigmacurry/latin-words")
  :custom
  (latin-words-directory
   (expand-file-name "straight/repos/latin-words/data" user-emacs-directory)))

9.15. Dashboard (label dashboard)

:tangle modules/dashboard/dashboard.el
(use-package
  dashboard
  :init
  (defun my-dashboard-insert-vocabulary (list-size)
    (when (fboundp 'latin-word-of-the-day)
      (dashboard-insert-heading "Word of the day:" nil)
      (insert "\n")
      (let* ((char-limit 100000)
             (word (latin-word-of-the-day))
             (description (latin-word-get-description word)))
        (insert
         (substring description
                    0
                    (min char-limit (length description)))))))
  (dashboard-setup-startup-hook)
  :hook
  ((dashboard-mode . (lambda () (setq-local show-trailing-whitespace nil))))
  :custom
  (dashboard-center-content t)
  (dashboard-set-heading-icons nil)
  (dashboard-set-file-icons nil)
  (dashboard-icon-type nil)
  (dashboard-footer-messages (list "    "))
  (dashboard-items '((recents . 5) (bookmarks . 5) (vocabulary)))
  (dashboard-startup-banner (+ 1 (random 3)))
  (dashboard-item-generators
   '((vocabulary . my-dashboard-insert-vocabulary)
     (recents . dashboard-insert-recents)
     (bookmarks . dashboard-insert-bookmarks))))

9.16. GPTel (label gptel)

:tangle modules/gptel/gptel.el
(use-package
  gptel
  :general
  ("C-c C-g" 'gptel-menu)
  (:keymaps 'gptel-mode-map "C-c C-c" 'gptel-send)
  :config (add-hook 'gptel-post-response-functions 'gptel-end-of-response)
                                        ;(add-hook 'gptel-post-stream-hook 'gptel-auto-scroll)
  (setq gptel-org-branching-context t)
  :init
  ;; LM-studio offers an OpenAI compatible API
  (setq
   gptel-model 'test
   gptel-backend
   (gptel-make-openai
    "lm-studio"
    :stream t
    :protocol "http"
    :host "localhost:1234"
    :models '(test)))
  (defun my-gptel-deepseek-wrap-think-block (beg end)
    "Wrap '<think>' blocks in an Org-mode drawer if not already wrapped."
    (when (derived-mode-p 'org-mode)
      (save-excursion
        (goto-char beg)
        ;; Find all occurrences of <think> blocks
        (while (re-search-forward "^<think>" end t)
          (let ((start (line-beginning-position)))
            ;; Check if the block is already wrapped
            (unless (save-excursion
                      (forward-line -4)
                      (looking-at "^:THINKING:$"))
              ;; Insert Org-mode drawer start
              (goto-char start)
              (kill-region (point) (line-end-position))
              (insert-and-inherit ":THINKING:\n")
              (insert-and-inherit
               "#+attr_shortcode: :title Thinking ...\n")
              (insert-and-inherit "#+begin_expand\n")
              (insert-and-inherit "<pre>")
              (forward-line 4)
              ;; Find the closing tag again after insertion
              (when (re-search-forward "</think>" nil t)
                (end-of-line)
                (kill-region (line-beginning-position) (point))
                (insert-and-inherit "</pre>")
                (end-of-line)
                ;; Ensure we don't add duplicate :END:
                (unless (looking-at "\n#+end_expand")
                  (insert-and-inherit "\n#+end_expand\n")
                  (insert-and-inherit ":END:\n"))
                ;; Move back to the start of the drawer for org-cycle
                (goto-char start)
                (org-cycle)))))))
    (message "Think blocks wrapped and folded."))

  (add-hook
   'gptel-post-response-functions
   #'my-gptel-deepseek-wrap-think-block))

9.17. LSP (label lsp)

:tangle modules/lsp/lsp.el
;; LSP mode :: https://emacs-lsp.github.io/lsp-mode/
(use-package
  lsp-mode
  :init
  ;; set prefix for lsp-command-keymap (few alternatives - "C-l", "C-c l")
  (setq lsp-keymap-prefix "C-c l")
  (setq lsp-modeline-diagnostics-scope :workspace)
 ;;; extra verbose logging of lsp json messages:
  ;;(setq lsp-log-io t)
  :hook
  ((web-mode . lsp)
   ;(lsp-mode . lsp-enable-which-key-integration)
   (python-mode . lsp-deferred))
  :commands lsp
  :config)
(use-package lsp-ui :commands lsp-ui-mode)
(use-package lsp-ivy :commands lsp-ivy-workspace-symbol)
(use-package lsp-treemacs :commands lsp-treemacs-errors-list)
(use-package flycheck)

;; LSP debuggers
(use-package dap-mode)
;; (use-package dap-LANGUAGE) to load the dap adapter for your language

9.18. Avy (label avy)

  • avy lets you jump to any word you can see in your Emacs frame.
:tangle modules/avy/avy.el
;; Avy (like ace-jump) :: https://github.com/abo-abo/avy
(use-package
 avy
 :general
 ;;; These are if you want to use avy by itself,
 ;;; Otherwise these keys will be defined by treesit-jump instead.
 ("s-s" 'avy-goto-word-1)
 ("C-c s" 'avy-goto-char)
 ("C-c S" 'avy-goto-word-1))

9.19. Treesit (label treesit)

Treesit is an Emacs builtin language syntax parser.

There are several third party modules to configure and enhance it:

:tangle modules/treesit/treesit.el
(use-package treesit-auto
  :custom
  (treesit-auto-langs '(awk bash bibtex blueprint c c-sharp
                            clojure cmake commonlisp cpp css dart dockerfile elixir glsl go
                            gomod heex html janet java javascript json julia kotlin latex
                            lua magik make markdown nix nu org perl proto python r ruby
                            scala sql surface toml tsx typescript typst verilog vhdl vue
                            wast wat wgsl yaml))
  (treesit-auto-install 'prompt)
  :config
  (treesit-auto-add-to-auto-mode-alist 'all)
  (global-treesit-auto-mode))

(use-package treesit-jump
  :straight (:host github :repo "dmille56/treesit-jump" :files ("*.el" "treesit-queries"))
  :config
  ;; Optional: add some queries to filter out of results (since they can be too cluttered sometimes)
  ;;(setq treesit-jump-queries-filter-list '("inner" "test" "param"))

9.20. S3-publish (label s3-publish)

s3-publish.el is a quick/temporary way to publish Emacs buffers, regions, and files to S3 storage and the web.

:tangle modules/s3-publish/s3-publish.el
(use-package
  s3-publish
  :straight
  (s3-publish
   :type
   git
   :repo
   "https://github.com/EnigmaCurry/s3-publish.el.git")
  :general
  ("C-c p" 's3-publish))

9.21. Python (label python)

  • uv - The uv package manager is installed from source when requested. Please be patient while it builds during its initial load.
:tangle modules/python/python.el
(my/add-exec-path "~/.local/bin")
(defun my/python-install-uv-package-manager ()
  "Install uv package manager via cargo"
  (my/cargo-install '(("uv" . "https://github.com/astral-sh/uv"))))
(defun my/python-uv-execute (command)
  (my/python-install-uv-package-manager)
  (my/shell-execute command))
(defun my/python-install-ruff ()
  (unless (executable-find "ruff")
    (my/python-uv-execute "uv tool install ruff@latest")))
(use-package
  python-mode
  :general ("s-a" 'lsp-execute-code-action)
  :init (my/python-install-ruff)
  :hook
  (python-mode . pyvenv-mode)
  (python-mode . flycheck-mode)
  (python-mode . company-mode)
  (python-mode . python-black-on-save-mode)
  :custom (python-shell-interpreter "python3")
  :config
  ;; Activate python virtualenv BEFORE opening a python buffer and/or starting pyright server:
  ;; M-x pyvenv-activate     (~/.virtualenvs/XXX)
  (use-package
    pyvenv
    :init (setenv "WORKON_HOME" "~/.virtualenvs/")
    :config
    ;; (pyvenv-mode t)
    ;; Set correct Python interpreter
    (setq pyvenv-post-activate-hooks
          (list
           (lambda ()
             (setq python-shell-interpreter
                   (concat pyvenv-virtual-env "bin/python")))))
    (setq pyvenv-post-deactivate-hooks
          (list (lambda () (setq python-shell-interpreter "python3")))))
  ;; Black (Python code formatter) :: https://github.com/wbolster/emacs-python-black
  ;; Note: this depends on black being installed in the project virtualenv as a dev dependency
  (use-package python-black :demand t :after python)
  ;; Python dev dependencies need to be installed in your project's virtualenv:
  ;; ruff
  ;; ruff-lsp
  ;; black
 ;;; Add the following to a .dir-locals.el to activate virtualenv automatically:
  ;; ((python-mode . ((eval . (let ((project-root (locate-dominating-file
  ;;                              (or (buffer-file-name) default-directory)
  ;;                                ".dir-locals.el")))
  ;;               (pyvenv-activate (expand-file-name "virtualenv" project-root)))))))
  )

9.22. Rust (label rust)

:tangle modules/rust/rust.el
;; Rust
;; must manually install cargo-watch, wasm-pack, wasm-bindgen, cargo-generate
(my/cargo-dependency "cargo-watch")
(my/cargo-dependency "wasm-pack")
(my/cargo-dependency "wasm-bindgen")
(my/cargo-dependency "cargo-generate")
(use-package
  rustic
  :mode ("\\.rs\\'" . rustic-mode)
  ;; :hook (rustic-mode . yas-minor-mode)
  :init
  (setq rustic-format-on-save t)
  (setq rustic-rustfmt-args "--edition 2021")
  (add-to-list 'exec-path "~/.cargo/bin")
  (defalias 'org-babel-execute:rust 'org-babel-execute:rustic)
  (describe-function 'org-babel-execute:rust)
  (add-hook
   'rustic-mode-hook
   (lambda ()
     (define-key
      rustic-mode-map
      (kbd "C-c M-.")
      'lsp-rust-analyzer-open-external-docs))))

10. Load modules

The code-blocks in this file are tangled to separate categorized files under the modules sub-directory. These are loaded on a per-machine basis based on my/machine-has-label:

10.1. Prioritize and load requested modules

Here is the list of prioritized modules that must be loaded first (if enabled):

- general
- fonts
:tangle init.el
(defvar my/modules-dir (expand-file-name "modules/" user-emacs-directory))
(defvar my/module-priority-list '("general" "fonts")
  "List of prioritized modules to install first.")
(defun my/load-modules (requested-modules)
  "Load user-requested modules in a priority order.
  REQUESTED-MODULES is a list of module names to load."
  ;; Find and load all single file modules and load them regardless of any config
  ;; (these onesshouldn't have any extra dependencies)
  (when (file-directory-p my/modules-dir)
    (let ((files (directory-files my/modules-dir t "\\.el\\'")))
      (dolist (file (sort files #'string<))
        (message "Loading module: %s" file)
        (condition-case err
            (load file nil 'nomessage)
          (error (message "Error loading %s: %s" file err))))))
  ;; Prioritize and install the requested third party / optional modules:
  (let ((prioritized-modules
         (seq-filter (lambda (mod) (member mod my/module-priority-list))
                     requested-modules))
        (remaining-modules
         (seq-remove (lambda (mod) (member mod my/module-priority-list))
                     requested-modules)))
    ;; Sort prioritized modules based on `my/module-priority-list`
    (setq prioritized-modules
          (sort prioritized-modules
                (lambda (a b)
                  (< (or (cl-position a my/module-priority-list)
                         most-positive-fixnum)
                     (or (cl-position b my/module-priority-list)
                         most-positive-fixnum)))))
    ;; Combine prioritized and remaining modules
    (let ((ordered-modules (append prioritized-modules
                                   remaining-modules)))
      (dolist (mod ordered-modules)
        (let ((mod-path (expand-file-name mod my/modules-dir)))
          (if (file-directory-p mod-path)
              (progn
                (message "Loading module: %s" mod)
                (let ((files (directory-files mod-path t "\\.el\\'")))
                  (dolist (file (sort files #'string<))
                    (condition-case err
                        (load file nil 'nomessage)
                      (error (message "Error loading %s: %s" file err))))))
            (message "Skipping module: %s (not found)" mod)))))))

;; load the modules configured for this macchine
(my/load-modules my/machine-labels)

10.2. Install rust helper programs declared by module

:tangle init.el
;; Install rust dependencies that were declared by modules
(when my/cargo-dependencies
  (my/cargo-install my/cargo-dependencies))

Author: EnigmaCurry

Last Modified: 2025-02-12 Wed 23:35