- Introduction
- Basic Setup
- Behavior
- Appearance
- Package Management
- Webpage Publishing
- Miscellaneous Hacks
This document contains my entire GNU Emacs configuration file in Literate Programming style. Literate Programming strives to invert the normal programming paradigm; Instead of source code scattered with comments, Literate Programming encourages code to be written as a human readable document, in prose, with source code blocks embedded inside of it. The source code is later interpreted, while the human readable prose is ignored by the interpreter or compiler.
The magic here is provided by org-babel
, which provides a method
for extracting and evaluating Emacs Lisp expressions inside an
org-mode
file.
There is one more thing: Emacs comes with a built-in version of
Org Mode that is usually out of date. Because I like to live on the
bleeding edge of Org Mode, I provide my own checkout as a git
submodule. The first two lines of my init.el
file then add this
submodule to the load path. This is required to use the submodule
version of org mode. My init.el
looks like this, in its entirety:
(add-to-list 'load-path "~/.emacs.d/org-mode/lisp") (add-to-list 'load-path "~/.emacs.d/org-mode/contrib/lisp") (org-babel-load-file "~/.emacs.d/configuration.org")
To finish the bootstrap, all you need to do is:
$ cd ~/.emacs.d $ git submodule init $ git submodule update $ cd ~/.emacs.d/org-mode $ make
Some of the packages I use still do (require 'cl)
, which is now
deprecated in favor of (require 'cl-lib)
. I don’t use this
deprecated library myself, so this hack simply silences those
deprecation warnings.
(setq byte-compile-warnings '(cl-functions))
If (and only if) Emacs has native compilation available, set it up to do its thing.
(when (and (fboundp 'native-comp-available-p)
(native-comp-available-p)
(functionp 'json-serialize))
(setq comp-deferred-compilation t))
First things first. Because I use org-babel
and literate
programming style for my config, I want my init.el
file to be as
small as possible. For that reason, I want Emacs to put all of its
generated fluff into a separate file that is actually outside
the Emacs directory.
(let ((local-custom-file "~/.emacs-custom.el"))
(when (not (file-exists-p local-custom-file))
(write-region "" nil local-custom-file))
(setq custom-file local-custom-file)
(load custom-file))
Some builds of emacs on exotic operating systems don’t automatically know that I want my home directory to be the default find-file directory.
(setq default-directory (expand-file-name "~/"))
Next, I like to immediately reduce what I consider to be visual clutter. Your milage may vary, but for me this means turning off startup messages, the splash screen, tool bar, and tooltips.
(setq warning-minimum-level :emergency)
(setq inhibit-startup-message t)
(setq inhibit-splash-screen t)
(tool-bar-mode -1)
(tooltip-mode -1)
(scroll-bar-mode -1)
(if (display-graphic-p)
(menu-bar-mode t)
(menu-bar-mode -1))
A few minor tweaks that I enjoy. First, I like to take space from all windows whenever I split a window
(setq-default window-combination-resize t)
Next, stretch the cursor to fill a full glyph cell
(setq-default x-stretch-cursor t)
Emacs, by default, keeps backup files in the current working
directory. I much prefer to keep all backup files together in one
place. This will put them all into the directory
~/.emacs.d/backups/
, creating the directory if it does not exist.
(if (not (file-exists-p "~/.emacs.d/backups/"))
(make-directory "~/.emacs.d/backups/" t))
(setq backup-directory-alist
'(("." . "~/.emacs.d/backups/")))
(setq auto-save-file-name-transforms
'((".*" "~/.emacs.d/backups/" t)))
(setq backup-by-copying t)
(setq auto-save-default t)
Next, these settings control how many backup versions to keep, and specify that older versions should be silently deleted (don’t warn me).
(setq kept-old-versions 2)
(setq kept-new-versions 5)
(setq delete-old-versions t)
Spelling is important (I’m terrible at spelling).
(cond
((executable-find "aspell")
(setq ispell-program-name "aspell"))
((executable-find "hunspell")
(setq ispell-program-name "hunspell")
(setq ispell-local-dictionary "en_US")
(setq ispell-local-dictionary-alist
'(("en_US" "[[:alpha]]" "[^[:alpha:]]" "[']" nil ("-d" "en_US") nil utf-8))))
(t (setq ispell-program-name nil)))
On macOS, I turn off --dired
(because ls
does not support it).
(when (string= system-type "darwin")
(setq dired-use-ls-dired nil))
I completely disable lockfiles, which I don’t need, and which only cause trouble.
(setq create-lockfiles nil)
Lastly, I disable the default “Control-Z” behavior of suspending emacs, because I find that I accidentally hit this key combo way too often when my clumsy fingers are trying to hit “Control-X”
(global-unset-key [(control z)])
(global-unset-key [(control x)(control z)])
scroll-step
controls the number of lines that the window will
scroll automatically when the cursor moves off the screen. By default,
it will jump you so that the cursor is centered (vertically) after
scrolling. I really don’t like this behavior, so I set it to 1
so
the window will only move by a single line.
(setq scroll-step 1)
Next, setting scroll-conservatively
to a very large number will
further prevent automatic centering. The value 10,000
comes from
a suggestion on the Emacs Wiki.
(setq scroll-conservatively 10000)
I always prefer 4 spaces for indents.
(setq-default c-basic-offset 4)
(setq-default sh-basic-offset 4)
(setq-default tab-width 4)
(setq-default indent-tabs-mode nil)
And next, I want to fix how multi-line initialization in C-like
languages is handled (for example, when initializing an array or a
struct). By default, elements after the brace-list-intro
character get lined up directly below it, like this:
int array[3] = { 0, 1, 2, };
By setting the correct value for c-set-offset 'brace-list-intro
,
I can get what I consider to be a much better offset that
looks like this:
int array[3] = { 0, 1, 2, };
Here’s the setting:
(c-set-offset 'brace-list-intro '+)
Tramp is a useful mode that allows editing files remotely.
The first thing I like to do is set the default connection method.
(setq tramp-default-method "ssh")
Then, I up some default values to make editing large directories happy.
(setq max-lisp-eval-depth 4000) ; default is 400
(setq max-specpdl-size 5000) ; default is 1000
Keep a list of recently opened files
(recentf-mode 1)
(setq-default recent-save-file "~/.emacs.d/recentf")
If certain directories exist, they should be added to the exec-path.
(when (file-exists-p "/usr/local/bin")
(setq exec-path (append exec-path '("/usr/local/bin")))
(setenv "PATH" (concat (getenv "PATH") ":/usr/local/bin")))
(when (file-exists-p (expand-file-name "~/bin"))
(setq exec-path (append exec-path '("~/bin")))
(setenv "PATH" (concat (getenv "PATH") ":$HOME/bin")))
(when (file-exists-p "/Library/TeX/texbin")
(setq exec-path (append exec-path '("/Library/TeX/texbin")))
(setenv "PATH" (concat (getenv "PATH") ":/Library/TeX/texbin")))
(when (file-exists-p "~/.cargo/bin")
;; Add to front of the list
(add-to-list 'exec-path "~/.cargo/bin")
(setenv "PATH" (concat (getenv "PATH") ":~/.cargo/bin")))
Enable integration between Emacs and GPG.
(setenv "GPG_AGENT_INFO" nil)
(require 'epa-file)
(require 'password-cache)
(setq epg-pgp-program "gpg")
(setq password-cache-expiry (* 15 60))
(setq epa-file-cache-passphrase-for-symmetric-encryption t)
(setq epa-pinentry-mode 'loopback)
I frequently split my Emacs windows both horizontally and
vertically. Navigation between windows with C-x o
is tedious, so
I use C-<arrow>
to navigate between windows. (N.B.: This
overrides the default behavior of moving forward or backward by
word using C-<right>
nad C-<left>
, so keep that in mind)
The typical way of doing this would be just to set the following in your config:
(windmove-default-keybindings 'ctrl)
However, there’s one downside here: If you accidentally try to
navigate to a window that doesn’t exist, it raises an error and/or
traps into the debugger (if debug-on-error
is enabled). No good!
So instead, I wrap in a lambda that ignores errors (Inspired by:
EmacsWiki WindMove)
(global-set-key (kbd "C-<left>")
'(lambda ()
(interactive)
(ignore-errors (windmove-left))))
(global-set-key (kbd "C-<right>")
'(lambda ()
(interactive)
(ignore-errors (windmove-right))))
(global-set-key (kbd "C-<up>")
'(lambda ()
(interactive)
(ignore-errors (windmove-up))))
(global-set-key (kbd "C-<down>")
'(lambda ()
(interactive)
(ignore-errors (windmove-down))))
(global-set-key "\C-xl" 'goto-line)
Turn off the infernal bell, both visual and audible.
(setq ring-bell-function 'ignore)
Enable the upcase-region
function. I still have no idea
why this is disabled by default.
(put 'upcase-region 'disabled nil)
Whenever we visit a buffer that has no active edits, but the file has changed on disk, automatically reload it.
(global-auto-revert-mode t)
I’m really not smart sometimes, so I need emacs to warn me when I try to quit it.
(setq confirm-kill-emacs 'yes-or-no-p)
Remote X11 seems to have problems with delete for me (mostly XQuartz, I believe), so I force erase to be backspace.
(when (eq window-system 'x)
(normal-erase-is-backspace-mode 1))
When functions are redefined with defadvice
, a warning is
emitted. This is annoying, so I disable these warnings.
(setq ad-redefinition-action 'accept)
Tell Python mode to use Python 3
(setq python-shell-interpreter "python3")
Not all fonts are installed on all systems where I use Emacs. This code will iterate over a list of fonts, in order of my personal preference, and set the default face to the first one available. Of course, if Emacs is not running in a windowing system, this is ignored.
(when window-system
(let* ((families '("Hack"
"Input Mono"
"Inconsolata"
"Dejavu"
"Menlo"
"Monaco"
"Courier New"
"Courier"
"fixed"))
(selected-family (cl-dolist (fam families)
(when (member fam (font-family-list))
(cl-return fam)))))
(set-face-attribute 'default nil
:family selected-family
:weight 'medium
:height 160)))
Beginning in Emacs 27, a new attribute, :extend
, was added to
faces. It determines whether the background of a face will extend
to the right margin or not. It defaults to nil
, but I prefer it
to be set for some things.
(when (>= emacs-major-version 27)
(set-face-attribute 'org-block nil :extend t)
(set-face-attribute 'org-block-begin-line nil :extend t)
(set-face-attribute 'org-block-end-line nil :extend t))
By default, the Emacs frame (what you or I would call a window) title is user@host. I much prefer the frame title to show the actual name of the currently selected buffer.
(setq-default frame-title-format "%b")
(setq frame-title-format "%b")
When uncommented, this will disable fringe-mode (“fringe” is a small blank margin space by default on the right and left of the buffer). I’m leaving this default for now, but may uncomment it later.
; (set-fringe-mode 0)
By default, you can increase or decrease the font face size in a
single window with C-x C-+
or C-x C--
, respectively. This is
fine, but it applies to the current window only (note: In
Emacs, a window is what you or I would probably call a frame or a
pane… yes, I know, just work with it). I like to map C-+
and
C--
to functions that will change the height of the default face
in ALL windows.
First, I create a base function to do the change by a certain amount in a certain direction.
(defun change-face-size (dir-func &optional delta)
"Increase or decrease font size in all frames and windows.
* DIR-FUNC is a direction function (embiggen-default-face) or
(ensmallen-default-face)
* DELTA is an amount to increase. By default, the value is 10."
(progn
(set-face-attribute
'default nil :height
(funcall dir-func (face-attribute 'default :height) delta))))
Then, I create two little helper functions to bump the size up or down.
(defun embiggen-default-face (&optional delta)
"Increase the default font.
* DELTA is the amount (in point units) to increase the font size.
If not specified, the dfault is 10."
(interactive)
(let ((incr (or delta 10)))
(change-face-size '+ incr)))
(defun ensmallen-default-face (&optional delta)
"Decrease the default font.
* DELTA is the amount (in point units) to decrease the font size.
If not specified, the default is 10."
(interactive)
(let ((incr (or delta 10)))
(change-face-size '- incr)))
And, finally, bind those functions to the right keys.
(global-set-key (kbd "C-+") 'embiggen-default-face)
(global-set-key (kbd "C--") 'ensmallen-default-face)
Turn on ANSI colors in the shell.
(autoload 'ansi-color-for-comint-mode-on "ansi-color" nil t)
(add-hook 'shell-mode-hook 'ansi-color-for-comint-mode-on)
Display line numbers in all programming-modes.
(add-hook 'prog-mode-hook #'display-line-numbers-mode)
Furthermore, I like to see (Line,Column) displayed in the modeline.
(setq line-number-mode t)
(setq column-number-mode t)
I like having the day, date, and time displayed in my modeline. (Note that it’s pointless to display seconds here, since the modeline does not automatically update every second, for efficiency purposes)
(setq display-time-day-and-date t)
(display-time-mode 1)
By default, if a frame has been split horizontally, partial windows will not wrap.
(setq truncate-partial-width-windows nil)
Whenever the cursor is on a paren, highlight the matching paren.
(show-paren-mode t)
GNU Emacs running on recent versions of MacOS in particular exhibit
some pretty ugly UI elements. Further, I don’t like having to use
the Option key for Meta, so I switch things around on the
keyboard. Note, though, that this block is only evaluated when the
windowing system is 'ns
, so this won’t do anything at all on
Linux.
(when (eq window-system 'ns)
(add-to-list 'frameset-filter-alist
'(ns-transparent-titlebar . :never))
(add-to-list 'frameset-filter-alist
'(ns-appearance . :never))
(setq mac-option-modifier 'super
mac-command-modifier 'meta
mac-function-modifier 'hyper
mac-right-option-modifier 'super))
Before we begin, add some special folders to the load-path. We’ll need these for packages that are not installed from ELPA and MELPA.
(add-to-list 'load-path "~/.emacs.d/lisp")
(add-to-list 'load-path "~/.emacs.d/local")
Next, require the package
mode and set up URLs to the package
archives.
(require 'package)
(setq package-enable-at-startup t)
(setq package-archives '(("org" . "https://orgmode.org/elpa/")
("gnu" . "https://elpa.gnu.org/packages/")
("melpa" . "https://melpa.org/packages/")))
Then, actually initialize things.
(package-initialize)
And then, if the use-package
package is not installed, install it
immediately.
(unless (package-installed-p 'use-package)
(package-refresh-contents)
(package-install 'use-package))
(require 'use-package)
(use-package dracula-theme
:ensure t)
(when window-system
(load-theme 'dracula t))
Our first package is a little bit of an exception. I don’t load Slime as a package. Instead, I prefer to load it from Quicklisp, if and only if Quicklisp is installed.
(when (file-exists-p (expand-file-name "~/quicklisp/slime-helper.el"))
(load (expand-file-name "~/quicklisp/slime-helper.el"))
(setq inferior-lisp-program "sbcl")
(setq slime-contribs '(slime-fancy)))
Next is org-mode
, which I use constantly, day in and day out.
(defun my-org-agenda-format-date-aligned (date)
"Format a DATE string for display in the daily/weekly agenda, or timeline.
This function makes sure that dates are aligned for easy reading."
(require 'cal-iso)
(let* ((dayname (calendar-day-name date 1 nil))
(day (cadr date))
(day-of-week (calendar-day-of-week date))
(month (car date))
(monthname (calendar-month-name month 1))
(year (nth 2 date))
(iso-week (org-days-to-iso-week
(calendar-absolute-from-gregorian date)))
(weekyear (cond ((and (= month 1) (>= iso-week 52))
(1- year))
((and (= month 12) (<= iso-week 1))
(1+ year))
(t year)))
(weekstring (if (= day-of-week 1)
(format " W%02d" iso-week)
"")))
(format "%-2s. %2d %s"
dayname day monthname)))
(eval-and-compile
(setq org-load-paths '("~/.emacs.d/org-mode/lisp"
"~/.emacs.d/org-mode/contrib/lisp")))
(use-package org
:load-path org-load-paths
:ensure t
:config
(use-package org-drill
:ensure t)
(require 'ox-latex)
(setq org-tags-column -65
org-export-default-language "en"
org-export-with-smart-quotes t
org-agenda-tags-column -65
org-deadline-warning-days 14
org-table-shrunk-column-indicator ""
org-agenda-block-separator (string-to-char " ")
org-adapt-indentation t
org-fontify-whole-heading-line t
org-agenda-format-date 'my-org-agenda-format-date-aligned
;; Use CSS for htmlizing HTML output
org-html-htmlize-output-type 'css
;; Open up org-mode links in the same buffer
org-link-frame-setup '((file . find-file)))
(set-face-attribute 'org-level-1 nil
:height 1.4)
(set-face-attribute 'org-level-2 nil
:height 1.2))
I have a lot of custom configuration for org-mode
.
Org-Roam is a Zettelkasten note taking system for org-mode. I’ve just started using it, and this is my first attempt at a configuration.
(use-package org-roam
:ensure t
:hook (after-init . org-roam-mode)
:config
(require 'org-roam-protocol)
(when (file-exists-p (expand-file-name "~/Nextcloud/org-roam"))
(setq
org-roam-directory "~/Nextcloud/org-roam"
org-roam-index-file "~/Nextcloud/org-roam/index.org"))
(setq
org-roam-capture-templates
'(("d" "default" plain (function org-roam--capture-get-point)
"%?"
:file-name "%<%Y%m%d%H%M%S>-${slug}"
:head "#+AUTHOR: %n\n#+DATE: %<%Y-%m-%d>\n#+TITLE: ${title}\n#+STARTUP: showall inlineimages\n#+OPTIONS: toc:nil num:nil\n\n- tags :: \n\n")))
:bind (:map org-roam-mode-map
(("C-c n l" . org-roam)
("C-c n f" . org-roam-find-file)
("C-c n j" . org-roam-jump-to-index)
("C-c n b" . org-roam-switch-to-buffer)
("C-c n g" . org-roam-graph)
("C-c n t" . org-roam-dailies-today)
("C-c n c" . org-roam-capture))
:map org-mode-map
(("C-c n i" . org-roam-insert))))
(use-package org-roam-server
:ensure t
:config
(setq org-roam-server-host "127.0.0.1"
org-roam-server-port 8080
org-roam-server-export-inline-images t
org-roam-server-authenticate nil
org-roam-server-network-arrows nil
org-roam-server-label-truncate t
org-roam-server-label-truncate-length 60
org-roam-server-label-wrap-length 20))
(use-package company-org-roam
:ensure t
:config
(push 'company-org-roam company-backends))
Org Agenda is a great way of tracking time and progress on various projects and repeatable tasks. It’s built into org-mode.
I add a quick and easy way to get into org-agenda
from any
org-mode
buffer by pressing C-c a
.
(global-set-key (kbd "C-c a") 'org-agenda)
Next, I add a custom org-agenda
command to show the next three
weeks.
(setq org-agenda-custom-commands
'(("n" "Agenda / INTR / PROG / NEXT"
((agenda "" nil)
(todo "INTR" nil)
(todo "PROG" nil)
(todo "NEXT" nil)))
("W" "Next Week" agenda ""
((org-agenda-span 7)
(org-agenda-start-on-weekday 0)))
("N" "Next Three Weeks" agenda ""
((org-agenda-span 21)
(org-agenda-start-on-weekday 0)))))
Then, I define some faces and use them for deadlines in
org-agenda
.
(defface deadline-soon-face
'((t (:foreground "#ff0000"
:weight bold
:slant italic
:underline t))) t)
(defface deadline-near-face
'((t (:foreground "#ffa500"
:weight bold
:slant italic))) t)
(defface deadline-distant-face
'((t (:foreground "#ffff00"
:weight bold
:slant italic))) t)
(setq org-agenda-deadline-faces
'((0.75 . deadline-soon-face)
(0.5 . deadline-near-face)
(0.25 . deadline-distant-face)
(0.0 . deadline-distant-face)))
Then I set my org-todo-keywords
so that I can manage my workflow
states the way I like to. Although my own list is very linear and
simple, they can become quite complex if need be!
(setq org-todo-keywords
'((sequence
"TODO(t)"
"NEXT(n)"
"PROG(p)"
"INTR(i)"
"DONE(d)")))
And finally, I set some file locations. This is a bit convoluted
because I use Agenda both for work and for home. At work, I keep a
file called ~/.org-agenda-setup.el
that contains my agenda files
and archive location information. At home, I just use what’s baked
into this file.
Also note that I like to keep archived Agenda items in a separate
directory, rather than the default behavior of renaming them to
<original-file-name>.org_archive
.
(if (file-exists-p "~/.org-agenda-setup.el")
(load "~/.org-agenda-setup.el")
(progn
(global-set-key (kbd "C-c o")
(lambda ()
(interactive)
(find-file "~/Nextcloud/agenda/agenda.org")))
(setq org-habit-show-habits-only-for-today nil
org-agenda-files (file-expand-wildcards "~/Nextcloud/agenda/*.org")
org-archive-location (concat "~/Nextcloud/agenda/Archive/%s::")
org-default-notes-file "~/Nextcloud/agenda/agenda.org")))
(use-package org-super-agenda
:ensure t
:after org-agenda
:init
(setq org-super-agenda-groups
'((:name "Next"
:time-grid t
:todo "NEXT"
:order 1)
(:name "Language"
:time-grid t
:tag "language"
:order 2)
(:name "Study"
:time-grid t
:tag "study"
:order 3)
(:discard (:not (:todo "TODO")))))
:config
(org-super-agenda-mode)
(setq org-agenda-compact-blocks nil
org-agenda-span 'day
org-agenda-todo-ignore-scheduled 'future
org-agenda-skip-deadline-prewarning-if-scheduled 'pre-scheduled
org-super-agenda-header-separator ""
org-columns-default-format "%35ITEM %TODO %3PRIORITY %TAGS")
(set-face-attribute 'org-super-agenda-header nil
:weight 'bold))
To capture new notes, I configure Org Capture with a quick
key binding of C-c c
.
(global-set-key (kbd "C-c c") 'org-capture)
I want to be able to support C, Emacs Lisp, and GraphViz blocks in org-babel.
(org-babel-do-load-languages
'org-babel-load-languages '((C . t)
(emacs-lisp . t)
(dot . t)))
This block adds a link handler for YouTube links in org-mode
buffers.
(defvar youtube-iframe-format
(concat "<iframe width=\"440\""
" height=\"335\""
" src=\"https://www.youtube.com/embed/%s\""
" frameborder=\"0\""
" allowfullscreen>%s</iframe>"))
(org-link-set-parameters
"youtube"
:follow (lambda (id)
(browse-url
(concat "https://www.youtube.com/embed/" id)))
:export (lambda (path desc backend)
(cl-case backend
(html (format youtube-iframe-format
path (or desc "")))
(latex (format "\href{%s}{%s}"
path (or desc "video"))))))
I prefer to insert periods after section numbers when exporting
org-mode
files to HTML. This tweak enables that.
(defun my-html-filter-headline-yesdot (text backend info)
"Ensure dots in headlines.
* TEXT is the text being exported.
* BACKEND is the backend (e.g. 'html).
* INFO is ignored."
(when (org-export-derived-backend-p backend 'html)
(save-match-data
(when (let ((case-fold-search t))
(string-match
(rx (group "<span class=\"section-number-" (+ (char digit)) "\">"
(+ (char digit ".")))
(group "</span>"))
text))
(replace-match "\\1.\\2"
t nil text)))))
(eval-after-load 'ox
'(progn
(add-to-list 'org-export-filter-headline-functions
'my-html-filter-headline-yesdot)))
I turn on Pretty Entities, which allows Emacs, in graphics mode, to render unicode symbols, math symbols, and so on. I also set a custom ellipsis character that will be shown when sections or blocks are collapsed.
(setq org-pretty-entities t
org-ellipsis "⬇")
This adds support the LaTeX class koma-article
on LaTeX export.
(add-to-list 'org-latex-classes
'("koma-article"
"\\documentclass{scrartcl}"
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")
("\\subparagraph{%s}" . "\\subparagraph*{%s}")))
(use-package org-superstar
:ensure t
:init
(add-hook 'org-mode-hook (lambda () (org-superstar-mode 1)))
:config
(setq org-hide-leading-stars nil
org-superstar-prettify-item-bullets nil
org-superstar-leading-bullet ?\s))
(use-package auth-source
:ensure t
:config
(setq auth-sources '("~/.authinfo.gpg")))
(use-package epresent
:ensure t)
(use-package ledger-mode
:ensure t)
(use-package graphviz-dot-mode
:ensure t)
(use-package magit
:ensure t
:init
(global-set-key (kbd "C-x g") 'magit-status)
(add-hook 'prog-mode-hook #'git-gutter-mode))
(use-package git-gutter
:ensure t)
YAML mode is useful for editing Docker files.
(use-package yaml-mode
:ensure t)
This is just a bit of fun. See: “Let It Snow” on GitHub.
(use-package snow
:ensure t)
Snippets build in support for typing a few keys, pressing tab, and
getting a complete template inserted into your buffer. I use these
heavily. In addition to the built-in snippets that come from the
yasnippet-snippets
package, I have some custom snippets defined
in the snippets
directory.
(use-package yasnippet
:ensure t
:diminish yas-minor-mode
:config
(add-to-list 'auto-mode-alist '("~/.emacs.d/snippets"))
(yas-global-mode))
(use-package yasnippet-snippets
:ensure t
:defer t
:after yasnippet
:config (yasnippet-snippets-initialize))
I really like paredit, especially for Lisp, but I don’t like the
default key bindings, so I tweak them heavily. Primarily, the
problem is that I use C-<left>
and C-<right>
to navigate
between windows in Emacs, so I don’t want to use them for
Paredit. Instead, I remap these to C-S-<left>
and C-S-<right>
,
respectively. One issue is that <left>
and <right>
may differ
depending on the platform I’m on, so there are several alternate
definitions here to make sure it works on all platforms. What a
pain.
(use-package paredit
:ensure t
:defer t
:init
(autoload 'enable-paredit-mode "paredit" "Structural editing of Lisp")
(add-hook 'emacs-lisp-mode-hook #'enable-paredit-mode)
(add-hook 'eval-expression-minibuffer-setup-hook #'enable-paredit-mode)
(add-hook 'ielm-mode-hook #'enable-paredit-mode)
(add-hook 'lisp-mode-hook #'enable-paredit-mode)
(add-hook 'lisp-interaction-mode-hook #'enable-paredit-mode)
(add-hook 'scheme-mode-hook #'enable-paredit-mode)
:config
(define-key paredit-mode-map (kbd "C-<left>") nil)
(define-key paredit-mode-map (kbd "C-<right>") nil)
(define-key paredit-mode-map (kbd "C-S-<left>")
'paredit-forward-barf-sexp)
(define-key paredit-mode-map (kbd "C-S-<right>")
'paredit-forward-slurp-sexp)
(define-key paredit-mode-map (read-kbd-macro "S-M-[ 5 D")
'paredit-forward-barf-sexp)
(define-key paredit-mode-map (read-kbd-macro "S-M-[ 5 C")
'paredit-forward-slurp-sexp)
(define-key paredit-mode-map (read-kbd-macro "M-[ 1 ; 6 d")
'paredit-forward-barf-sexp)
(define-key paredit-mode-map (read-kbd-macro "M-[ 1 ; 6 c")
'paredit-forward-slurp-sexp)
(define-key paredit-mode-map (read-kbd-macro "S-M-[ 1 ; 5 D")
'paredit-forward-barf-sexp)
(define-key paredit-mode-map (read-kbd-macro "S-M-[ 1 ; 5 C")
'paredit-forward-slurp-sexp))
CEDET provides a lot of nice support for C and C++ development.
(use-package cedet
:ensure t
:bind (:map semantic-mode-map
("C-c , >" . semantic-ia-fast-jump)))
Language Server Protocol support is pretty essential to my workflow. It makes Emacs act almost like an IDE.
(use-package lsp-mode
:ensure t
:defer t
:commands lsp
:config
(setq lsp-clients-clangd-args
'("-j=4"
"-background-index"
"-log=error")
;; Disable automatic formatting-as-you-type
lsp-enable-indentation nil
lsp-enable-on-type-formatting nil)
(when (string= system-type "darwin")
(setq lsp-clients-clangd-executable "/usr/local/opt/llvm/bin/clangd"))
(add-hook 'c-mode-hook #'lsp)
(add-hook 'c++-mode-hook #'lsp)
(add-hook 'python-mode-hook #'lsp)
(add-hook 'rust-mode-hook #'lsp))
(use-package flycheck
:ensure t
:hook (prog-mode . flycheck-mode))
(use-package flycheck-rust
:ensure t
:config
(setenv "PATH" (concat (getenv "PATH") ":~/.cargo/bin"))
(setq exec-path (append exec-path '("~/.cargo/bin")))
(add-hook 'flycheck-mode-hook #'flycheck-rust-setup))
(use-package lsp-ui
:ensure t
:requires lsp-mode flycheck
:config
(setq lsp-ui-doc-enable t
lsp-ui-doc-use-childframe t
lsp-ui-doc-position 'top
lsp-ui-doc-include-signature t
lsp-ui-doc-delay 2.5
lsp-ui-sideline-enable nil
lsp-ui-flycheck-enable t
lsp-ui-flycheck-list-position 'right
lsp-ui-flycheck-live-reporting t
lsp-ui-peek-enable t
lsp-ui-peek-list-width 60
lsp-ui-peek-peek-height 25)
(add-hook 'lsp-mode-hook 'lsp-ui-mode))
(use-package lsp-java
:ensure t
:config
(add-hook 'java-mode-hook #'lsp))
(use-package company
:ensure t
:hook (prog-mode . company-mode)
:config
(setq company-idle-delay 0.5
company-show-numbers t
company-minimum-prefix-length 0))
Web Mode offers a very nice integrated experience for editing HTML, JavaScript, and CSS.
(use-package web-mode
:ensure t
:config
(setq web-mode-markup-indent-offset 4
web-mode-css-indent-offset 4
web-mode-code-indent-offset 4))
I almost never use PHP, but it’s nice to have when you really, really need it.
(use-package php-mode
:ensure t
:defer t)
I detest JavaScript, and yet… sometimes you need to interact with the modern web.
(use-package typescript-mode
:ensure t)
(defun setup-tide-mode ()
(interactive)
(defun tide-imenu-index () nil)
(tide-setup)
(tide-hl-identifier-mode +1))
(use-package tide
:ensure t
:config
(progn
(add-hook 'typescript-mode-hook #'setup-tide-mode)
(add-hook 'js-mode-hook #'setup-tide-mode)
(add-hook 'js2-mode-hook #'setup-tide-mode)
(add-hook 'rjsx-mode-hook #'setup-tide-mode)))
(use-package htmlize
:ensure t)
One of my projects uses the Tera template language, so I add
support here. Unfortunately, tera-mode
is not in ELPA or MELPA,
so I have it checked out as a sub-module. I also just lazily enable
tera-mode
by hand when I need it.
(add-to-list 'load-path "~/.emacs.d/tera-mode")
(require 'tera-mode)
;; Set offset for HTML/XML-like things
(setq sgml-basic-offset 4)
Org-mode is clearly superior [;-)] but sometimes you need to use Markdown.
(use-package markdown-mode
:ensure t
:commands (markdown-mode gfm-mode)
:mode (("README\\.md\\'" . gfm-mode)
("\\.md\\'" . markdown-mode)
("\\.markdown\\'" . markdown-mode))
:init (setq markdown-command "multimarkdown"))
(use-package haskell-mode
:ensure t
:defer t)
Rust’s Cargo configuration files use TOML.
(use-package toml-mode
:ensure t)
Next, configure Rust Mode. Note that the hooks are set up in the
init:
block intentionally. There is a dependency load order
problem that prevents them from being hook:
calls.
(use-package rust-mode
:ensure t
:defer t
:bind (("C-c TAB" . rust-format-buffer))
:config
(use-package racer
:ensure t
:defer t)
(setq lsp-rust-server 'rust-analyzer
lsp-rust-analyzer-server-display-inline-hints t
lsp-rust-analyzer-display-chaining-hints t
lsp-rust-analyzer-display-parameter-hints t
lsp-rust-show-warnings t
lsp-rust-racer-completion t
lsp-rust-full-docs t
lsp-rust-build-lib t))
(use-package cargo
:ensure t
:config
(add-hook 'rust-mode-hook 'cargo-minor-mode))
Counsel, Swiper, and Ivy are used for autocompletion.
(use-package counsel
:ensure t
:config
(ivy-mode 1)
(setq ivy-use-virtual-buffers t
enable-recursive-minibuffers t
ivy-height 10
ivy-count-format ""
ivy-initial-inputs-alist nil
ivy-re-builders-alist '((t . ivy--regex-ignore-order)))
(setq search-default-mode #'char-fold-to-regexp)
(global-set-key (kbd "C-s") 'swiper-isearch)
(global-set-key (kbd "C-c C-r") 'ivy-resume)
(global-set-key (kbd "M-x") 'counsel-M-x)
(global-set-key (kbd "C-x C-f") 'counsel-find-file)
(global-set-key (kbd "M-y") 'counsel-yank-pop)
(global-set-key (kbd "C-x b") 'ivy-switch-buffer)
(global-set-key (kbd "C-c v") 'ivy-push-view)
(global-set-key (kbd "C-c V") 'ivy-pop-view))
Email configuration is all in an external, optional file.
(let ((mail-conf (expand-file-name "~/.emacs-mail.el")))
(when (file-exists-p mail-conf)
(load-file mail-conf)))
I keep my main homepage (https://loomcom.com/) entirely in
org-mode
. This section details how org-publish
is used to
transform a mass of Org files into a website.
First I define a few paths and a pointer to the header file, for conveninience.
(setq loomcom-project-dir "~/Projects/loomcom/")
(setq loomcom-org-dir (concat loomcom-project-dir "org/"))
(setq loomcom-www-dir (concat loomcom-project-dir "www/"))
(setq loomcom-blog-org-dir (concat loomcom-org-dir "blog/"))
(setq loomcom-blog-www-dir (concat loomcom-www-dir "blog/"))
(setq loomcom-blog-pattern "^\\([0-9]\\{4\\}\\)")
(setq loomcom-header-file
(concat loomcom-project-dir "org/header.html"))
(setq loomcom-posts-per-page 12)
Next, I define some additional tags to be used in headers and footers.
(setq loomcom-head
(concat
"<meta name=\"twitter:site\" content=\"@twylo\" />\n"
"<meta name=\"twitter:creator\" content=\"@twylo\" />\n"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n"
"<link rel=\"icon\" type=\"image/png\" href=\"/images/icon/favicon-32x32.png\" />\n"
"<link rel=\"apple-touch-icon-precomposed\" href=\"/images/icon/apple-touch-icon.png\" />\n"
"<link rel=\"stylesheet\" type=\"text/css\" href=\"/res/faces.css\">\n"
"<link rel=\"stylesheet\" type=\"text/css\" href=\"/res/style.css\">\n"))
(setq loomcom-footer
(concat
"<div id=\"footer\">\n"
"<p>Proudly "
"<a href=\"https://loomcom.com/blog/0110_emacs_blogging_for_fun_and_profit.html\">published</a> with "
"<a href=\"https://www.gnu.org/software/emacs/\">Emacs</a> and "
"<a href=\"https://orgmode.org/\">Org Mode</a>"
"</div>"))
When I publish a post to my blog, I want the ability to publish a summary of the post to the main blog index page, followed by a “Read More…” link that will take you to the full article.
This helper function builds the preview string by returning
anything in the post up to the first line that reads
#+BEGIN_more
.
(defun loomcom--get-preview (filename)
"Get a preview string for a file.
This function returns a list, '(<needs-more> <preview-string>),
where <needs-more> is nil or non-nil, and indicates whether
a \"Read More →\" link is needed.
FILENAME The file to get a preview for."
(with-temp-buffer
(insert-file-contents (concat loomcom-blog-org-dir filename))
(goto-char (point-min))
(let ((content-start (or
;; Look for the first non-keyword line
(and (re-search-forward "^[^#]" nil t)
(match-beginning 0))
;; Failing that, assume we're malformed and
;; have no content
(buffer-size)))
(marker (or
(and (re-search-forward "^#\\+BEGIN_more$" nil t)
(match-beginning 0))
(buffer-size))))
;; Return a pair of '(needs-more preview-string)
(list (not (= marker (buffer-size)))
(buffer-substring content-start marker)))))
These functions allow me to easily add a new blog entry by calling
M-x loomcom-blog-new
, which will prompt for a title and create
the appropriate file.
(defun loomcom--blog-entry-p (fname)
"Return true if `fname' is a blog entry"
(string-match loomcom-blog-pattern fname))
(defun loomcom--make-file-name (number title)
(let ((stub
(replace-regexp-in-string "[^a-z]+" "_" (downcase title) nil 'literal)))
(format
"%s_%s.org"
number
(replace-regexp-in-string "^_\\|_$" "" stub nil 'literal))))
(defun loomcom-blog-new ()
"Create a new blog entry."
(interactive)
(if (file-exists-p loomcom-blog-org-dir)
(progn
(org-mode)
(let* ((blog-files (sort
(seq-filter 'loomcom--blog-entry-p
(directory-files loomcom-blog-org-dir)) 'string>))
(match (string-match loomcom-blog-pattern (car blog-files)))
(last-num (match-string 1 (car blog-files)))
(next-num (format "%04d" (+ 1 (string-to-number last-num))))
(human-title (read-from-minibuffer "New Entry Title: "))
(new-file (loomcom--make-file-name next-num human-title))
(snippet (cl-find "New Blog File" (yas--all-templates
(yas--get-snippet-tables 'org-mode))
:key #'yas--template-name :test #'string=)))
(find-file (concat loomcom-blog-org-dir new-file))
(yas-expand-snippet snippet)
(insert human-title)
(yas-next-field)
(yas-next-field)
(yas-next-field)))
(error "Blog directory does not exist.")))
(defun loomcom--header (_)
"Insert the header of the page."
(with-temp-buffer
(insert-file-contents loomcom-header-file)
(buffer-string)))
My blog uses a paginated index, which is actually not supported by default
in org-publish
, so I do a lot of work to tweak it here.
The first thing I do is define a function that will return a sitemap for a single page.
(defun loomcom--sitemap-for-group (title previous-page next-page list)
"Generate the sitemap for one group of pages.
TITLE The title of the page
PREVIOUS-PAGE The previous index page to link to.
NEXT-PAGE The next index page to link to.
LIST The group of pages."
(let ((previous-link (if previous-page
(format "[[%s][← Previous Page]]" previous-page)
""))
(next-link (if next-page
(format "[[%s][Next Page →]]" next-page)
"")))
(concat "#+TITLE: " title "\n\n"
"#+BEGIN_pagination\n"
(format "- %s\n" previous-link)
(format "- %s\n" next-link)
"#+END_pagination\n\n"
(string-join (mapcar #'car (cdr list)) "\n\n"))))
Next, a function that will return a single entry in the sitemap. This is the actual entry that shows up on the index page!
(defun loomcom--sitemap-entry (entry project)
"Sitemap (Blog Main Page) Entry Formatter.
ENTRY The sitemap entry to format.
PROJECT The project structure."
(when (not (directory-name-p entry))
(format (string-join
'("* [[file:%s][%s]]\n"
" :PROPERTIES:\n"
" :PUBDATE: %s\n"
" :END:\n"
"#+BEGIN_published\n"
"%s\n"
"#+END_published\n"
"%s"))
entry
(org-publish-find-title entry project)
(format-time-string (cdr org-time-stamp-formats) (org-publish-find-date entry project))
(format-time-string "%A, %B %_d %Y at %l:%M %p %Z" (org-publish-find-date entry project))
(let* ((preview (loomcom--get-preview entry))
(needs-more (car preview))
(preview-text (cadr preview)))
(if needs-more
(format
(concat
"%s\n\n"
"#+BEGIN_morelink\n"
"[[file:%s][Read More →]]\n"
"#+END_morelink\n")
preview-text entry)
(format "%s" preview-text))))))
Then we define a function that will take a subset of all the blog posts that are to be published, and turn them into a list.
(defun loomcom--sitemap-files-to-lisp (files project)
"Convert a group of entries into a list.
FILES The group of entries to list-ify.
PROJECT The project structure."
(let ((root (expand-file-name
(file-name-as-directory
(org-publish-property :base-directory project)))))
(cons 'unordered
(mapcar
(lambda (f)
(list (loomcom--sitemap-entry (file-relative-name f root) project)))
files))))
And here is the function that takes the entire set of articles to
be published, and turns them into groups of n
elements.
(defun loomcom--group (source n)
"Group a list by 'n' elements.
SOURCE The list.
N The number to group the list by."
(if (not (cl-endp (nthcdr n source)))
(cons (cl-subseq source 0 n)
(loomcom--group (nthcdr n source) n))
(list source)))
Next, there’s a helper function to find the date of an entry. This mainly exists to help performance, because the sorting algorithm used to sort all the blog entries is very expensive and gets called n^2 times. Without this little helper and date cache, things would be a lot slower.
(setq loomcom-sitemap-file-dates (make-hash-table))
(defun loomcom--find-date (file-name project)
"Find the date for a file and cache it.
FILE-NAME The file in which to find a date.
PROJECT The project structure."
(let ((maybe-date (gethash file-name loomcom-sitemap-file-dates nil)))
(if maybe-date
maybe-date
(let ((new-date (org-publish-find-date file-name project)))
(puthash file-name new-date loomcom-sitemap-file-dates)
new-date))))
I override the entire org-html-template
function because I want to
wrap the HTML body in a wrapper div, and also want to add the
document date under the title and subtitle, if available.
(fmakunbound 'org-html-template)
(defun org-html-template (contents info)
"Return complete document string after HTML conversion.
CONTENTS is the transcoded contents string. INFO is a plist
holding export options."
(concat
(when (and (not (org-html-html5-p info)) (org-html-xhtml-p info))
(let* ((xml-declaration (plist-get info :html-xml-declaration))
(decl (or (and (stringp xml-declaration) xml-declaration)
(cdr (assoc (plist-get info :html-extension)
xml-declaration))
(cdr (assoc "html" xml-declaration))
"")))
(when (not (or (not decl) (string= "" decl)))
(format "%s\n"
(format decl
(or (and org-html-coding-system
(fboundp 'coding-system-get)
(coding-system-get org-html-coding-system 'mime-charset))
"iso-8859-1"))))))
(org-html-doctype info)
"\n"
(concat "<html"
(when (org-html-xhtml-p info)
(format
" xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"%s\" xml:lang=\"%s\""
(plist-get info :language) (plist-get info :language)))
">\n")
"<head>\n"
(org-html--build-meta-info info)
(org-html--build-head info)
(org-html--build-mathjax-config info)
"</head>\n"
"<body lang=\"en-US\">\n"
"<div id=\"wrapper\">\n"
(let ((link-up (org-trim (plist-get info :html-link-up)))
(link-home (org-trim (plist-get info :html-link-home))))
(unless (and (string= link-up "") (string= link-home ""))
(format (plist-get info :html-home/up-format)
(or link-up link-home)
(or link-home link-up))))
;; Preamble.
(org-html--build-pre/postamble 'preamble info)
;; Document contents.
(let ((div (assq 'content (plist-get info :html-divs))))
(format "<%s id=\"%s\">\n" (nth 1 div) (nth 2 div)))
;; Document title.
(when (plist-get info :with-title)
(let* ((title (plist-get info :title))
(subtitle (plist-get info :subtitle))
(with-date (plist-get info :with-date))
(date-fmt (plist-get info :html-metadata-timestamp-format))
(date (org-export-get-date info date-fmt)))
(when title
(format
(if (plist-get info :html-html5-fancy)
"<header>\n<h1 class=\"title\">%s</h1>\n%s%s</header>"
"<h1 class=\"title\">%s%s%s</h1>\n")
(org-export-data title info)
(if subtitle
(format
(if (plist-get info :html-html5-fancy)
"<p class=\"subtitle\">%s</p>\n"
"\n<br>\n<span class=\"subtitle\">%s</span>\n")
(org-export-data subtitle info))
"")
(if (and with-date date)
(format "\n<h2 class=\"date\">%s</h2>" date)
"")))))
contents
(format "</%s>\n" (nth 1 (assq 'content (plist-get info :html-divs))))
;; Postamble.
(org-html--build-pre/postamble 'postamble info)
;; Closing document.
"</div>\n</body>\n</html>"))
Then, the meat of the matter. This is a complete rewrite of the
default org-publish-sitemap
function that comes built into Org Mode.
It redefines the behavior to add support for publishing a multi-page
sitemap.
;; Un-define the original version of 'org-publish-sitemap'
(fmakunbound 'org-publish-sitemap)
;; Define our own version.
(defun org-publish-sitemap (project &optional sitemap-filename)
"Publish the blog.
This is actually a heavily modified and customized version of the
function by the same name in ox-publish.el. It allows the
generation of a sitemap with multiple pages.
PROJECT The project structure.
SITEMAP-FILENAME The filename to use as the default index."
(let* ((base (file-name-sans-extension (or sitemap-filename "index.org")))
(root (file-name-as-directory (expand-file-name
(concat loomcom-org-dir "blog/"))))
(title (or (org-publish-property :sitemap-title project)
(concat "Sitemap for project " (car project))))
(sort-predicate
(lambda (a b)
(let* ((adate (loomcom--find-date a project))
(bdate (loomcom--find-date b project))
(A (+ (lsh (car adate) 16) (cadr adate)))
(B (+ (lsh (car bdate) 16) (cadr bdate))))
(>= A B))))
(file-filter (lambda (f) (not (string-match (format "%s.*\\.org" base) f))))
(files (seq-filter file-filter (org-publish-get-base-files project))))
(message (format "Generating blog indexes for %s" title))
(let* ((pages (sort files sort-predicate))
(page-groups (loomcom--group pages loomcom-posts-per-page))
(page-number 0))
(dolist (group page-groups page-number)
(let ((fname (if (eq 0 page-number)
(concat root (format "%s.org" base))
(concat root (format "%s_%d.org" base page-number))))
(previous-page (cond ((eq 0 page-number) nil)
((eq 1 page-number) (concat root (format "%s.org" base)))
(t (concat root (format "%s_%d.org" base (- page-number 1))))))
(next-page (if (eq (- (length page-groups) 1) page-number)
nil
(concat root (format "%s_%d.org" base (+ page-number 1))))))
(setq page-number (+ 1 page-number))
(with-temp-file fname
(insert
(loomcom--sitemap-for-group
title
previous-page
next-page
(loomcom--sitemap-files-to-lisp group project)))))))))
And finally, at long last, the actual configuration for Org Publish that defines the project.
(setq org-publish-timestamp-directory (concat loomcom-project-dir "cache/"))
(setq org-publish-project-alist
`(("loomcom"
:components ("blog" "pages" "res" "images"))
("blog"
:base-directory ,loomcom-blog-org-dir
:base-extension "org"
:publishing-directory ,loomcom-blog-www-dir
:publishing-function org-html-publish-to-html
:with-author t
:author "Seth Morabito"
:email "web@loomcom.com"
:with-creator nil
:with-date t
:section-numbers nil
:with-title t
:with-toc nil
:with-drawers t
:with-sub-superscript nil
:html-html5-fancy t
:html-metadata-timestamp-format "%A, %B %_d %Y at %l:%M %p"
:html-doctype "html5"
:html-link-home "https://loomcom.com/"
:html-link-use-abs-url t
:html-head ,loomcom-head
:html-head-extra nil
:html-head-include-default-style nil
:html-head-include-scripts nil
:html-viewport nil
:html-link-up ""
:html-link-home ""
:html-preamble loomcom--header
:html-postamble ,loomcom-footer
:auto-sitemap t
:sitemap-filename "index.org"
:sitemap-title "Seth Morabito ∴ A Weblog"
:sitemap-sort-files anti-chronologically)
("pages"
:base-directory ,loomcom-org-dir
:base-extension "org"
:exclude ".*blog/.*"
:publishing-directory ,loomcom-www-dir
:publishing-function org-html-publish-to-html
:section-numbers nil
:recursive t
:with-title t
:with-toc nil
:with-drawers t
:with-sub-superscript nil
:with-author t
:author "Seth Morabito"
:email "web@loomcom.com"
:html-html5-fancy t
:with-creator nil
:with-date nil
:html-link-home "/"
:html-head nil
:html-doctype "html5"
:html-head ,loomcom-head
:html-head-extra nil
:html-head-include-default-style nil
:html-head-include-scripts nil
:html-link-up ""
:html-link-home ""
:html-preamble loomcom--header
:html-postamble ,loomcom-footer
:html-viewport nil)
("res"
:base-directory ,loomcom-org-dir
:base-extension "css\\|js\\|woff2\\|woff\\|ttf"
:recursive t
:publishing-directory ,loomcom-www-dir
:publishing-function org-publish-attachment)
("images"
:base-directory ,loomcom-org-dir
:base-extension "png\\|jpg\\|gif\\|pdf\\|svg"
:recursive t
:publishing-directory ,loomcom-www-dir
:publishing-function org-publish-attachment)))
Some time in Emacs 27’s development lifetime, the default value of
the variable truncate-string-ellipsis
became unbound. It’s
supposed to be a string that’s used when truncating a string to
width with the truncate-string-to-width
function. I think this
is a bug, but to work around it, we just define it here.
It might also be a bug with mu4e. Maybe mu4e is unbinding the variable?
Hopefully we can remove this hack after the bug is fixed, whoever’s fault it is.
(setq truncate-string-ellipsis "...")