GNU Emacs Configuration

Ano, What is This?

Published on 12 March 2025 by Jing Huang.

For reasons that felt arbitrary at the time, I decided to document my Emacs configuration, written specifically for the NeXTStep Cocoa implementation. Refactored in May 2025, with major update in November 2025.

1. Early Initial Stage

Somewhat trivial, basically about correcting Emacs’s behavior before init.el is loaded. This toggles off the defaults that might cause clutter or affect performance, and disables unnecessary components for the interface.

   |(setq frame-inhibit-implied-resize t
   |      frame-resize-pixelwise t
   |      window-resize-pixelwise t
   |      inhibit-startup-screen t
 5 |      use-dialog-box nil)
   |
   |(setopt menu-bar-mode nil
   |        scroll-bar-mode nil
   |        tool-bar-mode nil
10 |        tooltip-mode nil)

There are no redundant GC tweaks, as I compile Emacs with the incremental garbage collector.

2. Initial Stage

Set the load-path explicitly when portable dumped. Portable dump related details are documented in Chapter 5.

  |(defvar dumped-load-path)
  |
  |(when (boundp 'dumped-load-path)
  |  (setq load-path dumped-load-path))

The handlers for file names are stripped locally during the very early stages.

  |(setq-local file-name-handler-alist nil)

Then add core to the load path. The dolist is intended for more fine-grained division of load paths in the future.

  |(dolist (site '("core"))
  |  (add-to-list 'load-path
  |               (expand-file-name site user-emacs-directory)))

Tell Emacs not to litter on the file system, prevent custom.el from modifying init.el, unlock disabled commands, use short answers, and enable refined CJK line-break which respects kinsoku.

  |(setq auto-save-file-name-transforms `((".*" ,temporary-file-directory t))
  |      backup-directory-alist `((".*" . ,temporary-file-directory))
  |      custom-file (make-temp-file "custom" nil ".el")
  |      disabled-command-function nil
5 |      use-short-answers t
  |      word-wrap-by-category t)

Font configuration without mixing fonts, as they may lead to unexpected line height related issues. Since we don’t actually use any sans serif fonts, we should’t scale the variable-pitch-text face as well.

  |(when (display-graphic-p)
  |  (dolist (face '(default fixed-pitch fixed-pitch-serif variable-pitch))
  |    (set-face-attribute face nil :family "SF Mono")))
  |
5 |(set-face-attribute 'variable-pitch-text nil :height 1.0)

Optimizations for large files, based on LdBeth’s approach.

  |(setq bidi-display-reordering nil
  |      bidi-inhibit-bpa t
  |      large-hscroll-threshold 1000
  |      long-line-threshold 1000
5 |      syntax-wholeline-max 1000)

Unbind the annoying key binding to mouse-wheel-text-scale, to prevent accidental unintended text scaling.

  |(keymap-global-set "C-<wheel-up>" 'ignore)
  |(keymap-global-set "C-<wheel-down>" 'ignore)

Supply our own mode line, which is by design robust, with only necessary elements displayed. The lighter for minor modes are replaced with condensed Unicode glyphs.

   |(setq mode-line-right-align-edge 'right-margin
   |      mode-line-space (propertize " " 'display '(space :height 1.4))
   |      mode-line-minor-mode '("ⓐ")
   |      mode-line-minor-mode-lighter '((eldoc-mode . "ⓔ")
 5 |                                     (flymake-mode . "ⓜ")
   |                                     (flyspell-mode . "ⓢ")
   |                                     (visual-line-mode . "ⓥ")
   |                                     (whitespace-mode . "ⓦ")
   |                                     (yas-minor-mode . "ⓨ")))
10 |
   |(dolist (mapping mode-line-minor-mode-lighter)
   |  (let ((mode (intern (symbol-name (car mapping))))
   |        (lighter (cdr mapping)))
   |    (push `(,mode ,lighter) mode-line-minor-mode)))
15 |
   |(setq-default mode-line-format `(,mode-line-space
   |                                 ,mode-line-mule-info
   |                                 ,mode-line-modified
   |                                 ,mode-line-space
20 |                                 (:propertize "%b" face bold)
   |                                 mode-line-format-right-align
   |                                 (:propertize mode-name face bold)
   |                                 ,mode-line-space
   |                                 ,mode-line-minor-mode
25 |                                 ,mode-line-space))

Activate some handy builtin modes. We use setopt for modes simply because they look better than numbers preceded by mode name as functions. Note that indent-tabs-mode is disabled, meaning that spaces will be used instead.

   |(setq pixel-scroll-precision-use-momentum t
   |      pixel-scroll-precision-interpolate-page t
   |      window-divider-default-right-width 1)
   |
 5 |(setopt indent-tabs-mode nil
   |        delete-selection-mode t
   |        electric-pair-mode t
   |        global-auto-revert-mode t
   |        pixel-scroll-precision-mode t
10 |        repeat-mode t
   |        save-place-mode t
   |        window-divider-mode t)

Way to insert backslashs on JIS keyboard.

  |(define-key key-translation-map (kbd "¥") (kbd "\\"))

3. External World

Spawn a shell process to update environment variables inside Emacs. Without this, our wrapper around use-package which checks for appropriate executables won’t work right.

   |(defun environment-flush ()
   |  (with-temp-buffer
   |    (call-process-shell-command
   |     (format "%s -l -c 'env'" (getenv "SHELL")) nil t)
 5 |    (goto-char (point-min))
   |    (while (re-search-forward "^\\([^=]+\\)=\\(.*\\)$" nil t)
   |      (when-let* ((name (match-string 1))
   |                  (value (match-string 2)))
   |        (when (string= name "PATH")
10 |          (setenv name value)
   |          (setq exec-path (split-string value path-separator)))))))

Macro for revealing my deepest secrets.

  |(defmacro secret-get (host)
  |  `(funcall (plist-get (car (auth-source-search :host ,host)) :secret)))

We then gather the cookies in third-party extension packages that are not managed by package.el and process them. The heavy lifting of generating autoloads is taken care of by loaddefs-generate.

  |(let* ((site (expand-file-name "core" user-emacs-directory))
  |       (cookie (expand-file-name "core-autoloads.el" site)))
  |  (loaddefs-generate site cookie))
  |
5 |(require 'core-autoloads)

Set up Milkypostman’s Emacs Lisp package archive. I’m happy with the bleeding-edge repository.

  |(with-eval-after-load 'package
  |  (add-to-list 'package-archives
  |               '("melpa" . "https://melpa.org/packages/")))

Finally we setup use-package, an abstraction layer for configuration and loading of packages. It is further extended with the ability to conceal package declarations unless specific binary exists.

   |(setq use-package-always-defer t
   |      use-package-always-ensure t)
   |
   |(eval-when-compile
 5 |  (require 'use-package))
   |
   |(define-advice use-package
   |    (:around (orig package &rest body) use-with-binary)
   |  (let ((executable (plist-get body :with)))
10 |    (when executable
   |      (setq body (seq-difference body `(:with ,executable))))
   |    (if (or (not executable) (executable-find executable))
   |        (apply orig package body))))
   |
15 |(with-eval-after-load 'use-package
   |  (environment-flush))

4. Package Management

Here contains the configuration and loading of extension packages, which are either shipped with Emacs or available in any of the repositories. They are declared in alphabetical order, which is not generally recommended considering loading priorities, etc. However it’s applicable here since I dump all of them.

    |(use-package auctex
    |  :with "luatex"
    |  :init
    |  (setq-default TeX-engine 'luatex)
  5 |  (setq TeX-check-TeX nil
    |        TeX-parse-self t
    |        TeX-view-program-list '(("Preview" "open -a Preview %o"))))
    |
    |(use-package avy
 10 |  :bind (("C-: c" . avy-goto-char)
    |         ("C-: x" . avy-goto-char-timer)
    |         ("C-: l" . avy-goto-line)))
    |
    |(use-package corfu
 15 |  :hook (after-init . global-corfu-mode)
    |  :init (setq corfu-auto t
    |              corfu-cycle t
    |              corfu-preselect 'prompt
    |              corfu-quit-no-match 'separator)
 20 |  :bind (:map corfu-map
    |              ([tab] . corfu-next)
    |              ([backtab] . corfu-previous)
    |              ([return] . corfu-send)
    |              ([escape] . corfu-quit)))
 25 |
    |(use-package dired
    |  :ensure nil
    |  :init (setq dired-use-ls-dired nil))
    |
 30 |(use-package ef-themes
    |  :defer nil
    |  :config (ef-themes-select 'ef-kassio))
    |
    |(use-package eglot
 35 |  :ensure nil
    |  :init
    |  (setq eglot-code-action-indications '(eldoc-hint))
    |  (defun eglot-for-tab-command ()
    |    (interactive)
 40 |    (cond ((yas-active-snippets)
    |           (yas-next-field-or-maybe-expand))
    |          ((save-excursion
    |             (beginning-of-line)
    |             (looking-at-p "[ \t]*$"))
 45 |           (indent-for-tab-command))
    |          (t (eglot-format)
    |             (when (use-region-p)
    |               (deactivate-mark)))))
    |  :bind (:map eglot-mode-map
 50 |              ([tab] . eglot-for-tab-command)))
    |
    |(use-package eldoc
    |  :ensure nil
    |  :init (setq eldoc-echo-area-display-truncation-message nil
 55 |              eldoc-echo-area-use-multiline-p nil
    |              eldoc-echo-area-prefer-doc-buffer 'maybe))
    |
    |(use-package eww
    |  :ensure nil
 60 |  :hook (eww-after-render . eww-render-xslt)
    |  :init
    |  (defun eww-extract-xslt ()
    |    (save-excursion
    |      (goto-char (point-min))
 65 |      (when (re-search-forward "<\\?xml-stylesheet [^>]*href=['\"]\\([^'\"]+\\)['\"]" nil t)
    |        (let ((xslt (match-string 1))
    |              (link (url-generic-parse-url (eww-current-url))))
    |          (if (file-name-absolute-p xslt)
    |              (progn
 70 |                (setf (url-filename link) xslt)
    |                (url-recreate-url link))
    |            (let* ((path (file-name-directory (url-filename link)))
    |                   (xslt (expand-file-name xslt path)))
    |              (setf (url-filename link) xslt)
 75 |              (url-recreate-url link)))))))
    |  (defun eww-render-xslt ()
    |    (when (or (string-match "\\.xml$" (eww-current-url))
    |              (save-excursion
    |                (goto-char (point-min))
 80 |                (re-search-forward "<\\?xml" nil t)))
    |      (when-let* ((link (eww-extract-xslt))
    |                  (xslt (make-temp-file "eww" nil ".xsl"))
    |                  (xml (make-temp-file "eww" nil ".xml"))
    |                  (html (make-temp-file "eww" nil ".html"))
 85 |                  (command (format "xsltproc '%s' '%s' > '%s'" xslt xml html)))
    |        (url-copy-file link xslt t)
    |        (append-to-file nil nil xml)
    |        (call-process-shell-command command nil nil)
    |        (eww-open-file html)))))
 90 |
    |(use-package flymake
    |  :ensure nil
    |  :init (define-fringe-bitmap 'flymake-fringe-indicator
    |          (vector #b00000000
 95 |                  #b00000000
    |                  #b00000000
    |                  #b00000000
    |                  #b00000000
    |                  #b00000000
100 |                  #b00011100
    |                  #b00111110
    |                  #b00111110
    |                  #b00111110
    |                  #b00011100
105 |                  #b00000000
    |                  #b00000000
    |                  #b00000000
    |                  #b00000000
    |                  #b00000000
110 |                  #b00000000))
    |  :config (setq flymake-indicator-type 'fringes
    |                flymake-note-bitmap '(flymake-fringe-indicator compilation-info)
    |                flymake-warning-bitmap '(flymake-fringe-indicator compilation-warning)
    |                flymake-error-bitmap '(flymake-fringe-indicator compilation-error)))
115 |
    |(use-package flyspell
    |  :ensure nil
    |  :init (setq ispell-program-name "aspell"))
    |
120 |(use-package gptel
    |  :config (setq gptel-backend (gptel-make-anthropic "claude"
    |                                :stream t
    |                                :key (secret-get "console.anthropic.com"))))
    |
125 |(use-package magit)
    |
    |(use-package marginalia
    |  :hook (after-init . marginalia-mode))
    |
130 |(use-package markdown-mode
    |  :init (setq markdown-enable-math t
    |              markdown-fontify-code-blocks-natively t
    |              markdown-hide-urls t)
    |  :config
135 |  (set-face-background 'markdown-code-face nil)
    |  (set-face-underline 'markdown-line-break-face nil))
    |
    |(use-package nxml-mode
    |  :ensure nil
140 |  :config (add-to-list 'rng-schema-locating-files
    |                       (expand-file-name "schema/schemas.xml" user-emacs-directory)))
    |
    |(use-package orderless
    |  :init (setq completion-styles '(orderless basic)
145 |              orderless-matching-styles '(orderless-literal
    |                                          orderless-prefixes
    |                                          orderless-regexp)))
    |
    |(use-package proof-general
150 |  :with "coqc"
    |  :init (setq proof-splash-enable nil
    |              proof-delete-empty-windows t))
    |
    |(use-package sly
155 |  :with "sbcl"
    |  :init (setq inferior-lisp-program "sbcl"))
    |
    |(use-package swift-mode
    |  :with "swift")
160 |
    |(use-package treesit
    |  :ensure nil
    |  :defer nil
    |  :init (setq treesit-language-unmask-alist '((c++ . cpp))
165 |              treesit-language-fallback-alist '((html-ts-mode . mhtml-mode))
    |              treesit-language-source-alist '((bash . ("https://github.com/tree-sitter/tree-sitter-bash"))
    |                                              (c . ("https://github.com/tree-sitter/tree-sitter-c"))
    |                                              (cpp . ("https://github.com/tree-sitter/tree-sitter-cpp"))
    |                                              (css . ("https://github.com/tree-sitter/tree-sitter-css"))
170 |                                              (html . ("https://github.com/tree-sitter/tree-sitter-html"))
    |                                              (rnc . ("https://github.com/ldbeth/tree-sitter-rnc"))))
    |  :config (dolist (grammar treesit-language-source-alist)
    |            (let* ((language (or (car (rassq (car grammar) treesit-language-unmask-alist))
    |                                 (car grammar)))
175 |                   (derived (intern (concat (symbol-name language) "-ts-mode")))
    |                   (fallback (assq derived treesit-language-fallback-alist))
    |                   (default (or (cdr fallback)
    |                                (intern (concat (symbol-name language) "-mode")))))
    |              (and (not (and fallback (not (cdr fallback))))
180 |                   (fboundp derived)
    |                   (if (treesit-ready-p (car grammar) t)
    |                       (add-to-list 'major-mode-remap-alist
    |                                    `(,default . ,derived))
    |                     (when (fboundp default)
185 |                       (add-to-list 'major-mode-remap-alist
    |                                    `(,derived . ,default))))))))
    |
    |(use-package tuareg
    |  :with "ocaml")
190 |
    |(use-package vertico
    |  :hook (after-init . vertico-mode))
    |
    |(use-package wanderlust
195 |  :init
    |  (define-mail-user-agent
    |    'wl-user-agent
    |    'wl-user-agent-compose
    |    'wl-draft-send
200 |    'wl-draft-kill
    |    'mail-send-hook)
    |  (setq mail-user-agent 'wl-user-agent)
    |  (with-eval-after-load 'wl-demo
    |    (set-face-background 'wl-highlight-demo-face nil)))
205 |
    |(use-package yasnippet
    |  :hook (prog-mode . yas-minor-mode))

5. Mail Management

Wanderlust-specific settings are isolated in ~/.wl, which is automatically loaded during its start up. The behavior of Mail.app is replicated, and the default summary interface is refined. To get X-Face display working, you need to have x-face-e21.el installed and patched so it works on recent Emacs versions.

   |(setq user-mail-address "[email protected]"
   |      user-full-name "Huang Jing")
   |
   |(setq elmo-imap4-default-user user-mail-address
 5 |      elmo-imap4-default-authenticate-type 'clear
   |      elmo-imap4-default-server "imap.mail.me.com"
   |      elmo-imap4-default-port 993
   |      elmo-imap4-default-stream-type 'ssl
   |      elmo-passwd-storage-type 'auth-source)
10 |
   |(setq wl-expire-alist '(("^\\+trash$" (date 7) remove))
   |      wl-from "Huang Jing <[email protected]>"
   |      wl-fcc "%Sent Messages"
   |      wl-fcc-force-as-read t
15 |      wl-local-domain "icloud.com"
   |      wl-smtp-authenticate-type "plain"
   |      wl-smtp-connection-type 'starttls
   |      wl-smtp-posting-user user-mail-address
   |      wl-smtp-posting-server "smtp.mail.me.com"
20 |      wl-smtp-posting-port 587
   |      wl-temporary-file-directory "~/.wlt"
   |      wl-summary-width nil
   |      wl-summary-line-format "%n%T%P %W:%M/%D %h:%m %36(%t%[%c %f %]%) %s"
   |      wl-thread-indent-level 2
25 |      wl-thread-have-younger-brother-str "+"
   |      wl-thread-youngest-child-str "+"
   |      wl-thread-vertical-str " "
   |      wl-thread-horizontal-str "-"
   |      wl-thread-space-str " "
30 |      wl-message-id-domain "smtp.mail.me.com"
   |      wl-message-ignored-field-list '(".")
   |      wl-message-visible-field-list
   |      '("^Subject:"
   |        "^\\(To\\|Cc\\):"
35 |        "^\\(From\\|Reply-To\\):"
   |        "^\\(Posted\\|Date\\):"
   |        "^Organization:"
   |        "^X-Face\\(-[0-9]+\\)?:")
   |      wl-message-sort-field-list
40 |      '("^Subject"
   |        "^\\(To\\|Cc\\)"
   |        "^\\(From\\|Reply-To\\)"
   |        "^\\(Posted\\|Date\\)"
   |        "^Organization"
45 |        "^X-Face\\(-[0-9]+\\)?:")
   |      wl-highlight-x-face-function 'x-face-decode-message-header)

6. Portable Dumper

The portable dumper is a subsystem that replaces the traditional unexec method of creating an Emacs pre-loaded with Lisp code and data. This significantly improves Emacs startup and response time.

   |(package-initialize)
   |
   |(defconst dumped-load-mask nil)
   |(defconst dumped-load-path load-path)
 5 |
   |(with-temp-buffer
   |  (insert-file-contents (concat user-emacs-directory "init.el"))
   |  (goto-char (point-min))
   |  (condition-case error
10 |      (while-let ((form (read (current-buffer))))
   |        (pcase form
   |          (`(use-package ,package . ,rest)
   |           (unless (memq package dumped-load-mask)
   |             (require package nil t)))))
15 |    (end-of-file nil)))
   |
   |(load (concat user-emacs-directory "init.el"))
   |
   |(defun dumped-init ()
20 |  (global-font-lock-mode t)
   |  (transient-mark-mode t))
   |
   |(add-hook 'emacs-startup-hook 'dumped-init)
   |
25 |(dump-emacs-portable "Emacs.pdmp")