Welcome to the latest iteration of my Emacs configuration! I’m not sure how many times I’ve rewritten my Emacs configuration over the years (it’s probably less than you think), but I’m approaching this one with some specific goals and use cases in mind.
I have specific goals for this configuration which I hope will serve some personal use cases and also inspire other people to experiment with their own ideas:
- The base configuration is terminal-first, meaning that everything I add there must provide a good experience for Emacs in the terminal.
init.elitself should have no external package dependencies (if possible) and I should be able to drop it on any machine usingcurland have a comfortable environment ready to go immediately.- Additional functionality will be introduced using optional configuration modules which are only loaded on specific machines.
- Use built-in Emacs functionality as much as I can, only pull in community packages for critical workflows.
- Avoid special convenience key bindings; stay as close to “vanilla” as I can.
This particular set of goals may seem surprising, so I’d like to provide some rationale. I’m not attempting this because I think it’s somehow better to be minimalistic. I am trying to address specific use cases that I have instead of trying to come up with a generic configuration structure. What I do here may not be right for you!
I’ve described in a few previous live streams how I feel that it’s important I understand fundamental Emacs functionality and key bindings so that I can teach them effectively. I really want to give the best advice to someone on what options they have available for their workflow and I can’t do that if I don’t understand the tools that Emacs provides by default.
Also, I think there is some value in writing your own Emacs Lisp code for certain things that may be provided by community packages. Why depend on a small package with configuration options to suit many users when you could just write something similar for yourself that is tailored specifically to your needs?
There is probably more to say here that I haven’t thought of yet; please feel free to ask me any questions you have on IRC, the forum, or via email (my first name at systemcrafters.net).
A short list of features I’m trying to stick with this time before reaching for community packages:
- Use
icomplete-vertical-modeorfido-vertical-modeinstead of Vertico. Vertico is amazing, but I want to know what the actual pain points of the built-in solutions are before I resort to using something that is not built in. - Get used to the built-in
completion-stylesbefore I try pulling inorderless.orderlessis excellent but I honestly have not experimented enough with the built-in styles to see if I can get around well enough with them.
p
- Use
vc-modeinstead of Magit as long as I can.vc-modeis nowhere near as polished as Magit, by any means. However, I should be able to do many of the same operations I accomplish with Magit (aside from interactive rebase?). I want to test the limits ofvc-modebefore I pull in Magit, and even then, I may only use Magit for specific tasks thatvc-modeis not good at. - Try to avoid using
consultfor as long as I can.consult-lineis pretty useful, butisearchis mostly fine and I haven’t even tried to configure it to be nicer yet. The one thing I will miss fromconsultisconsult-ripgrep, but I need to see if there’s a way to useripgrepdirectly with Emacs’ existinggrepfunctionality. - Try to avoid using “workspace” packages like
beframeortabspaces. There must be other useful ways to manage buffers that don’t require forcing per-project isolation. There’s alreadyC-x p b(project-switch-to-buffer) that could take care of 80% of my needs. I also haven’t experimented enough withibufferfor mass buffer management to clean up those that I don’t need any more in my current session. - No configuration helpers other than
use-packagesince it’s now built in with Emacs 30. There are also new keymap macros like define-keymap which remove the need for packages likegeneral.el, etc.
You may have realized that I am back to using a literate Emacs configuration with Org Babel. I’ve actually been planning to do this for a while but never got around to it until now.
I had a couple of reasons for abandoning my old literate configuration:
- Tangling config changes after syncing from my dotfiles can be a pain
- I wanted to treat my configuration as real code instead of code blocks in an Org file
After spending over a year with my Emacs config as plain .el files, I feel like I’m missing the convenience of one big file that contains everything in one place. Sure, I can move around pretty quickly in my dotfiles repo using C-x p f (project-find-file), but half the time I forget which file some aspect of my Emacs config lives in.
Also, as many have noticed, the website where I publish my configuration has been down for almost as long as I haven’t been using a literate config due to DNS changes at SourceHut. I’ve been thinking about how I can get a website back up for my config again and tried a few different options, but going back to literate just feels like the most appropriate solution for now.
Lastly, I think a literate config is superior because it enables one to write real prose to explain their configuration and the rationale for why they do things a certain way. I wouldn’t have bothered to write this many paragraphs as a comment in an Emacs Lisp file. People can learn from reading about how you use Emacs, it’s a benefit to the community!
To set up this configuration on a new machine, there are two options:
- Download
emacs/init.elinto your home folder as.emacs - OR Clone the whole
dotfilesrepo to~/.dotfilesand run the following commands:
mkdir ~/emacs.d
ln -sf ~/.dotfiles/emacs/init.el ~/.emacs.d/
We don’t actually symlink the whole ~/.dotfiles/emacs folder to ~/.emacs.d to prevent Emacs from dumping runtime files and ELPA packages into the dotfiles repository!
The standard init.el file is shared between all machines that use this configuration. It is intended that file can be copied directly onto a new machine to get a convenient working environment up and running quickly.
Additional functionality is loaded from module files which are defined in sections later in this document.
I’ve structured all of this in a very concise way because much of it doesn’t need explanation if you’ve been using Emacs for a while.
;; -*- lexical-binding: t; -*-
;;; This file is generated from the Emacs.org file in my dotfiles repository!
;;; ----- Basic Configuration -----
;; Core settings
(setq ;; Yes, this is Emacs
inhibit-startup-message t
;; Instruct auto-save-mode to save to the current file, not a backup file
auto-save-default nil
;; No backup files, please
make-backup-files nil
;; Make it easy to cycle through previous items in the mark ring
set-mark-command-repeat-pop t
;; Don't warn on large files
large-file-warning-threshold nil
;; Follow symlinks to VC-controlled files without warning
vc-follow-symlinks t
;; Don't warn on advice
ad-redefinition-action 'accept
;; Revert Dired and other buffers
global-auto-revert-non-file-buffers t
;; Silence compiler warnings as they can be pretty disruptive
native-comp-async-report-warnings-errors nil)
;; Core modes
(repeat-mode 1) ;; Enable repeating key maps
(menu-bar-mode 0) ;; Hide the menu bar
(tool-bar-mode 0) ;; Hide the tool bar
(savehist-mode 1) ;; Save minibuffer history
(scroll-bar-mode 0) ;; Hide the scroll bar
(xterm-mouse-mode 1) ;; Enable mouse events in terminal Emacs
(display-time-mode 1) ;; Display time in mode line / tab bar
(column-number-mode 1) ;; Show column number on mode line
(tab-bar-history-mode 1) ;; Remember previous tab window configurations
(auto-save-visited-mode 1) ;; Auto-save files at an interval
(global-visual-line-mode 1) ;; Visually wrap long lines in all buffers
(global-auto-revert-mode 1) ;; Refresh buffers with changed local files
;; Tabs to spaces
(setq-default indent-tabs-mode nil
tab-width 2)
;; Display line numbers in programming modes
(add-hook 'prog-mode-hook #'display-line-numbers-mode)
;; Delete trailing whitespace before saving buffers
(add-hook 'before-save-hook 'delete-trailing-whitespace)
;; Move customization settings out of init.el
(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(when (file-exists-p custom-file)
(load custom-file t))
;; Match completion substrings that may be out of order
(defun dw/override-fido-completion-styles ()
(setq-local completion-styles '(basic substring partial-completion emacs22)))
(setopt tab-always-indent 'complete
read-buffer-completion-ignore-case t
read-file-name-completion-ignore-case t
;; This *may* need to be set to 'always just so that you don't
;; miss other possible good completions that match the input
;; string.
completion-auto-help t
;; Include more information with completion listings
completions-detailed t
;; Move focus to the completions window after hitting tab
;; twice.
completion-auto-select 'second-tab
;; If there are 3 or less completion candidates, don't pop up
;; a window, just cycle through them.
completion-cycle-threshold 3
;; Cycle through completion options vertically, not
;; horizontally.
completions-format 'vertical
;; Sort recently used completions first.
completions-sort 'historical
;; Only show up to 10 lines in the completions window.
completions-max-height 10
;; Don't show the unneeded help string at the top of the
;; completions buffer.
completion-show-help nil
;; Add more `completion-styles' to improve candidate selection.
completion-styles '(basic partial-completion substring initials))
(keymap-set minibuffer-local-map "C-p" #'minibuffer-previous-completion)
(keymap-set minibuffer-local-map "C-n" #'minibuffer-next-completion)
I often need to modify behavior based on the type of system Emacs is running on, especially if it’s running on Guix.
;;; ----- System Identification -----
(defvar dw/is-termux
(string-suffix-p "Android" (string-trim (shell-command-to-string "uname -a"))))
(defvar dw/current-distro (or (and (eq system-type 'gnu/linux)
(file-exists-p "/etc/os-release")
(with-temp-buffer
(insert-file-contents "/etc/os-release")
(search-forward-regexp "^ID=\"?\\(.*\\)\"?$")
(intern (or (match-string 1)
"unknown"))))
'unknown))
(defvar dw/is-guix-system (eql dw/current-distro 'guix))
Customize package management based on the type of system Emacs is running on.
;;; ----- Package Management -----
;; Automatically install packages (when not on Guix) but don't load
;; them until requested
(setq use-package-always-ensure (not dw/is-guix-system)
use-package-always-defer t)
This section is currently under development as I figure out the best pattern to use for providing customization “knobs” on the base configuration.
For now, the idea is that I provide variables and functions that can be called in machine-specific configuration files (named with the system-name) to customize basic configuration details and load extension modules that are needed for each machine.
;;; ----- Configuration Management -----
(defvar dw/use-config-modules '()
"A list of module symbols to load once init.el is finished.")
(defvar dw/common-config-modules '(dw-auth
dw-present
dw-0x0
dw-writing
dw-workflow)
"Configuration modules most commonly used across my machines.")
;; Add configuration modules to load path
(add-to-list 'load-path '"~/.dotfiles/emacs/modules")
;; Load system-specific configuration
(let ((config-path
(format "~/.dotfiles/emacs/systems/%s.el" system-name)))
(if (file-exists-p config-path)
(load-file config-path)
(message "No per-system configuration found for %s!" system-name)))
I prefer to use terminals that support the full range of colors so that themes like ef-themes can have an equivalent appearance to graphical Emacs.
However, setting a background color in an Emacs theme generally defeats any transparency settings of the terminals I’ve used so I’ve added the dw/clear-background-color function to clear the background color after a theme gets applied.
I may switch to using a highly-customized modus-vivendi here if I can figure out the right combination of colors to approximate the usual doom-palenight theme that I use on System Crafters videos and streams.
;;; ----- Appearance -----
(defun dw/set-terminal-title (title)
(send-string-to-terminal (format "\e]0;%s\a" title)))
(defun dw/clear-background-color (&optional frame)
(interactive)
(or frame (setq frame (selected-frame)))
"unsets the background color in terminal mode"
(unless (display-graphic-p frame)
;; Set the terminal to a transparent version of the background color
(send-string-to-terminal
(format "\033]11;[90]%s\033\\"
(face-attribute 'default :background)))
(set-face-background 'default "unspecified-bg" frame)))
;; Clear the background color for transparent terminals
(unless (display-graphic-p)
(add-hook 'after-make-frame-functions 'dw/clear-background-color)
(add-hook 'window-setup-hook 'dw/clear-background-color)
(add-hook 'ef-themes-post-load-hook 'dw/clear-background-color))
(when (display-graphic-p)
(set-face-attribute 'default nil
:font "JetBrains Mono"
:weight 'normal
:height 140)
;; Set the fixed pitch face
(set-face-attribute 'fixed-pitch nil
:font "JetBrains Mono"
:weight 'normal
:height 140)
;; Set the variable pitch face
(set-face-attribute 'variable-pitch nil
:font "Iosevka Aile"
:height 120
:weight 'normal)
;; Make frames transparent
(set-frame-parameter (selected-frame) 'alpha-background 93)
(add-to-list 'default-frame-alist '(alpha-background . 93))
(set-frame-parameter (selected-frame) 'fullscreen 'maximized)
(add-to-list 'default-frame-alist '(fullscreen . maximized)))
(defun dw/apply-ayu-dark-style ()
(interactive)
(setopt modus-themes-italic-constructs t
modus-themes-bold-constructs t
modus-themes-common-palette-overrides
`((bg-main "#0F111B")
(bg-active bg-main)
(fg-main "#C3CCDF")
(fg-active fg-main)
(fringe unspecified)
(border-mode-line-active unspecified)
(border-mode-line-inactive unspecified)
(fg-mode-line-active "#B3B1AD")
(bg-mode-line-active "#171B27")
(fg-mode-line-inactive "#65737E")
(bg-mode-line-inactive "#1C1F29")
(bg-tab-bar "#1C1F29")
(bg-tab-current bg-main)
(bg-tab-other "#171B27")
(fg-prompt "#F6C177")
(bg-prompt unspecified)
(bg-hover-secondary "#65737E")
(bg-completion "#2f447f")
(fg-completion "#ffffff")
(bg-region "#2B2E36")
(fg-region "#ffffff")
;; Heading colors
(fg-heading-0 "#81A1C1")
(fg-heading-1 "#81A1C1")
(fg-heading-2 "#F6C177")
(fg-heading-3 "#FFB974")
(fg-heading-4 "#C792EA")
(fg-prose-verbatim "#A3BE8C")
(bg-prose-block-contents "#171B27")
(fg-prose-block-delimiter "#65737E")
(bg-prose-block-delimiter "#171B27")
(accent-1 "#7FDBCA")
(keyword "#F6C177")
(builtin "#81A1C1")
(comment "#65737E")
(string "#A3BE8C")
(fnname "#7FDBCA")
(type "#C792EA")
(variable "#FFB974")
(docstring "#8996A2")
(constant "#F07178"))))
(defun dw/apply-palenight-style ()
(interactive)
(setopt modus-themes-italic-constructs t
modus-themes-bold-constructs t
modus-themes-common-palette-overrides
`((bg-main "#292D3E")
(bg-active bg-main)
(fg-main "#EEFFFF")
(fg-active fg-main)
(fringe unspecified)
(border-mode-line-active unspecified)
(border-mode-line-inactive unspecified)
(fg-mode-line-active "#A6Accd")
(bg-mode-line-active "#232635")
(fg-mode-line-inactive "#676E95")
(bg-mode-line-inactive "#282c3d")
(bg-tab-bar "#242837")
(bg-tab-current bg-main)
(bg-tab-other bg-active)
(fg-prompt "#c792ea")
(bg-prompt unspecified)
(bg-hover-secondary "#676E95")
(bg-completion "#2f447f")
(fg-completion white)
(bg-region "#3C435E")
(fg-region white)
(fg-heading-0 "#82aaff")
(fg-heading-1 "#82aaff")
(fg-heading-2 "#c792ea")
(fg-heading-3 "#bb80b3")
(fg-heading-4 "#a1bfff")
(fg-prose-verbatim "#c3e88d")
(bg-prose-block-contents "#232635")
(fg-prose-block-delimiter "#676E95")
(bg-prose-block-delimiter bg-prose-block-contents)
(accent-1 "#79a8ff")
(keyword "#89DDFF")
(builtin "#82aaff")
(comment "#676E95")
(string "#c3e88d")
(fnname "#82aaff")
(type "#c792ea")
(variable "#ffcb6b")
(docstring "#8d92af")
(constant "#f78c6c"))))
(use-package modus-themes
:ensure nil
:demand t
:init
(load-theme 'modus-vivendi-tinted t)
(dw/apply-ayu-dark-style)
(add-hook 'modus-themes-after-load-theme-hook #'dw/clear-background-color))
;; Make vertical window separators look nicer in terminal Emacs
(set-display-table-slot standard-display-table 'vertical-border (make-glyph-code ?│))
;; Clean up the mode line
(setq-default mode-line-format
'("%e" " "
(:propertize
("" mode-line-mule-info mode-line-client mode-line-modified mode-line-remote))
mode-line-frame-identification
mode-line-buffer-identification
" "
mode-line-position
mode-line-format-right-align
" "
(project-mode-line project-mode-line-format)
" "
(vc-mode vc-mode)
" "
mode-line-modes
mode-line-misc-info
" ")
project-mode-line t
mode-line-buffer-identification '(" %b")
mode-line-position-column-line-format '(" %l:%c"))
(use-package emacs-solo-rainbow-delimiters
:ensure nil
:no-require t
:defer t
:init
(defun emacs-solo/rainbow-delimiters ()
"Apply simple rainbow coloring to parentheses, brackets, and braces in the current buffer.
Opening and closing delimiters will have matching colors."
(interactive)
(let ((colors '(font-lock-keyword-face
font-lock-type-face
font-lock-function-name-face
font-lock-variable-name-face
font-lock-constant-face
font-lock-builtin-face
font-lock-string-face
)))
(font-lock-add-keywords
nil
`((,(rx (or "(" ")" "[" "]" "{" "}"))
(0 (let* ((char (char-after (match-beginning 0)))
(depth (save-excursion
;; Move to the correct position based on opening/closing delimiter
(if (member char '(?\) ?\] ?\}))
(progn
(backward-char) ;; Move to the opening delimiter
(car (syntax-ppss)))
(car (syntax-ppss)))))
(face (nth (mod depth ,(length colors)) ',colors)))
(list 'face face)))))))
(font-lock-flush)
(font-lock-ensure))
(add-hook 'prog-mode-hook #'emacs-solo/rainbow-delimiters))
Tweak the tab bar to remove some unnecessary elements and shift the global-mode-string there.
;; Move global mode string to the tab-bar and hide tab close buttons
(setq tab-bar-close-button-show nil
tab-bar-separator " "
tab-bar-format '(tab-bar-format-menu-bar
tab-bar-format-tabs-groups
tab-bar-separator
tab-bar-format-align-right
tab-bar-format-global))
;; Turn on the tab-bar
(tab-bar-mode 1)
Time is relative, OK?
;; Customize time display
(setq display-time-load-average nil
display-time-format "%l:%M %p %b %d W%U"
display-time-world-time-format "%a, %d %b %I:%M %p %Z"
display-time-world-list
'(("Etc/UTC" "UTC")
("Europe/Athens" "Athens")
("America/Los_Angeles" "Seattle")
("America/Denver" "Denver")
("America/New_York" "New York")
("Pacific/Auckland" "Auckland")
("Asia/Shanghai" "Shanghai")
("Asia/Kolkata" "Hyderabad")))
I find it easier to just run the main Emacs instance as a daemon using server-start. This enables external programs to drive my Emacs session with emacsclient.
(server-start)
Here’s another minimal implementation of a package I commonly use called popper.el. The goal here is to automatically place a specific set of buffers into a popup window at the bottom of the frame and make that window togglable with a key binding.
I don’t currently support buffer cycling like popper does but I don’t think I used it that much to begin with.
;; ----- Special Buffers as Popup Window -----
(setq display-buffer-alist
'(("\\*\\(.*shell\\|.*term\\|.*eshell\\|help\\|compilation\\|Async Shell Command\\|Occur\\|xref\\).*\\*"
(display-buffer-reuse-window display-buffer-in-side-window)
(side . bottom) ; Popups go at the bottom
(slot . 0) ; Use the first slot at the bottom
(post-command-select-window . t) ; Select the window upon display
(window-height . 0.3)))) ; 30% of the frame height
(defun dw/toggle-popup-window ()
(interactive)
(if-let ((popup-window
(get-window-with-predicate
(lambda (window)
(eq (window-parameter window 'window-side)
'bottom)))))
;; Focus the window if it is not selected, otherwise close it
(if (eq popup-window (selected-window))
(delete-window popup-window)
(select-window popup-window))
;; Find the most recent buffer that matches the rule and show it
;; NOTE: This logic is somewhat risky because it makes the assumption
;; that the popup rule comes first in `display-buffer-alist'.
;; I chose to do this because maintaining a separate variable
;; for this rule meant I had to re-evaluate 2 different forms
;; to update my rule list.
(if-let ((popup-buffer
(seq-find (lambda (buffer)
(buffer-match-p (caar display-buffer-alist)
(buffer-name buffer)))
(if (project-current)
(project-buffers (project-current))
(buffer-list (selected-frame))))))
(display-buffer popup-buffer (cdar display-buffer-alist))
(message "No popup buffers found."))))
;; TODO: This binding may need to change
(keymap-global-set "C-c p" #'dw/toggle-popup-window)
(with-eval-after-load 'term
(keymap-set term-raw-map "C-c p" #'dw/toggle-popup-window))
Here are the most important Org Mode settings that enable me to edit files comfortably, especially my literate configuration files.
;;; ----- Essential Org Mode Configuration -----
(setq org-ellipsis " ▾"
org-startup-folded 'content
org-cycle-separator-lines 2
org-fontify-quote-and-verse-blocks t)
;; Indent org-mode buffers for readability
(add-hook 'org-mode-hook #'org-indent-mode)
;; Set up Org Babel languages
(org-babel-do-load-languages
'org-babel-load-languages
'((emacs-lisp . t)
(shell . t)))
;; Use org-tempo
(use-package org-tempo
:ensure nil
:demand t
:config
(dolist (item '(("sh" . "src sh")
("el" . "src emacs-lisp")
("li" . "src lisp")
("sc" . "src scheme")
("ts" . "src typescript")
("py" . "src python")
("yaml" . "src yaml")
("json" . "src json")
("einit" . "src emacs-lisp :tangle emacs/init.el")
("emodule" . "src emacs-lisp :tangle emacs/modules/dw-MODULE.el")))
(add-to-list 'org-structure-template-alist item)))
I previously used visual-fill-column-mode for this functionality but decided to write my own minimal implementation so that I could avoid installing a MELPA package.
Works pretty well, but I’m not fully convinced this needs to be in init.el. It certainly does make the writing experience more pleasant but may not be critical for minimal config deployments.
;;; ----- Document Centering -----
(defvar center-document-desired-width 90
"The desired width of a document centered in the window.")
(defun center-document--adjust-margins ()
;; Reset margins first before recalculating
(set-window-parameter nil 'min-margins nil)
(set-window-margins nil nil)
;; Adjust margins if the mode is on
(when center-document-mode
(let ((margin-width (max 0
(truncate
(/ (- (window-width)
center-document-desired-width)
2.0)))))
(when (> margin-width 0)
(set-window-parameter nil 'min-margins '(0 . 0))
(set-window-margins nil margin-width margin-width)))))
(define-minor-mode center-document-mode
"Toggle centered text layout in the current buffer."
:lighter " Centered"
:group 'editing
(if center-document-mode
(add-hook 'window-configuration-change-hook #'center-document--adjust-margins 'append 'local)
(remove-hook 'window-configuration-change-hook #'center-document--adjust-margins 'local))
(center-document--adjust-margins))
(add-hook 'org-mode-hook #'center-document-mode)
(add-hook 'markdown-mode-hook #'center-document-mode)
(add-hook 'text-mode-hook #'center-document-mode)
This is a configuration for Emacs’ built in shells, term / ansi-term and eshell.
;; Coming soon.
Dired doesn’t need much configuration, but the following just ensures that Dired buffers are organized in a way that makes sense to me, are free from unneeded information (at first), and doesn’t leave a ton of buffers open as I move around.
I also add a binding for b to dired-up-directory because it seems very strange to me that they have f bound to dired-find-file without b moving in the “opposite” direction.
;;; ----- Dired -----
(defun dw/dired-mode-hook ()
(interactive)
(dired-hide-details-mode 1)
(hl-line-mode 1))
(use-package dired
:ensure nil
:bind (:map dired-mode-map
("b" . dired-up-directory))
:config
(setq dired-listing-switches "-alv --group-directories-first"
dired-omit-files "^\\.[^.].*"
dired-omit-verbose nil
dired-dwim-target 'dired-dwim-target-next
dired-hide-details-hide-symlink-targets nil
dired-kill-when-opening-new-dired-buffer t
delete-by-moving-to-trash t)
(add-hook 'dired-mode-hook #'dw/dired-mode-hook))
The built-in proced package is quite useful for producing a list of all running processes. It’s basically like having top built in to Emacs.
This configuration makes the proced buffer update automatically on an interval so that you can watch process activity as it changes.
(use-package proced
:ensure nil
:config
(setq proced-auto-update-interval 1)
(add-hook 'proced-mode-hook
(lambda ()
(proced-toggle-auto-update 1))))
I search through files a lot. Here are some tweaks to make that experience a bit faster and more convenient:
;; Make sure ripgrep is used everywhere
(setq xref-search-program 'ripgrep
grep-command "rg -nS --noheading")
Sometimes you just want to focus on what you’re writing. This is a simple mode to help accomplish that.
(defvar focus-mode--saved-mode-line-format nil
"Stores the `mode-line-format' value before enabling focus mode.")
(define-minor-mode focus-mode
"Hide unnecessary UI elements to increase focus."
:global t
:group 'editing
:lighter " Focus"
(if focus-mode
(progn
(setq-default focus-mode--saved-mode-line-format (default-value 'mode-line-format))
(setq-default mode-line-format nil)
(tab-bar-mode 0))
(setq-default mode-line-format focus-mode--saved-mode-line-format)
(tab-bar-mode 1)))
At the end of init.el, we finalize any configuration settings that may have been applied at the per-system level. This includes loading any configuration modules that are requested.
;;; ----- Finalization
;; Load requested configuration modules
(dolist (module dw/use-config-modules)
(require module))
The following sections contain optional modules that will be loaded on a per-system basis depending on whether the specified features are needed.
I use Denote as my primary tool for writing in Emacs. It enables me to write whatever I need very efficiently without thinking too much about where the information belongs.
I also use it as the core for my personal productivity system which revolves largely around Denote-managed Org Mode notes with special file-level tags.
;; -*- lexical-binding: t; -*-
(use-package denote
:demand t
:bind (("C-c n l" . denote-link-or-create)
("C-c n o" . denote-open-or-create)
("C-c n r" . denote-rename-file-using-front-matter))
:custom
(denote-directory "~/Notes/Log")
(denote-rename-buffer-format "Denote: %t (%k)")
(denote-infer-keywords nil)
(denote-known-keywords
'("pra" "prb" "prc"
"ply" "plm" "plw"
"kt" "ke" "kp" "kl" "ka" "kap"
"kcp" "kca" "kcc"
"kra" "krb" "krv"
"rn"))
:config
;; Rename buffers with the note name
(denote-rename-buffer-mode 1)
;; Buttonize all denote links in text buffers
(add-hook 'text-mode-hook #'denote-fontify-links-mode-maybe))
(use-package consult-notes
:ensure nil
:commands (consult-notes)
:config
(consult-notes-denote-mode 1))
(defun dw/setup-markdown-mode ()
(center-document-mode 1)
(display-line-numbers-mode 0))
(use-package markdown-mode
:config
(setq markdown-command "marked")
(add-hook 'markdown-mode-hook #'dw/setup-markdown-mode)
(dolist (face '((markdown-header-face-1 . 1.2)
(markdown-header-face-2 . 1.1)
(markdown-header-face-3 . 1.0)
(markdown-header-face-4 . 1.0)
(markdown-header-face-5 . 1.0)))
(set-face-attribute (car face) nil :weight 'normal :height (cdr face))))
(defun dw/orgalist-text-mode-hook ()
(unless (derived-mode-p 'org-mode)
(orgalist-mode 1)))
(use-package orgalist
:init
(add-hook 'text-mode-hook 'dw/orgalist-text-mode-hook))
(defun dw/howm-set-buffer-name ()
(when (and buffer-file-name
(howm-subdirectory-p howm-directory buffer-file-name))
(howm-mode-set-buffer-name)))
(use-package howm
:ensure nil
:bind* ("C-c ; ;" . howm-menu)
:init
(setq howm-prefix (kbd "C-c ;")
howm-view-use-grep t
howm-buffer-name-format "howm: %s"
howm-buffer-name-limit 100
howm-buffer-name-total-limit 100
howm-directory "~/Notes"
howm-keyword-file (expand-file-name ".howm-keys" howm-directory)
howm-history-file (expand-file-name ".howm-history" howm-directory)
howm-file-name-format "%Y/%m/%Y-%m-%d-%H%M%S.org"
howm-view-title-header "*"
howm-dtime-format "<%Y-%m-%d %a %H:%M>"
howm-template "* %title%cursor\n\n%date %file\n\n")
:config
(add-hook 'org-mode-hook 'howm-mode)
(add-hook 'howm-mode-hook 'dw/howm-set-buffer-name)
(add-hook 'after-save-hook 'dw/howm-set-buffer-name)
;; Adapted from this GitHub issue:
;; https://github.com/protesilaos/modus-themes/issues/117#issuecomment-2337993946
(modus-themes-with-colors
(custom-set-faces
`(howm-menu-key-face ((,c :inherit help-key-binding)))
`(howm-mode-keyword-face (( )))
`(howm-mode-ref-face ((,c :inherit link)))
`(howm-mode-title-face ((,c :inherit modus-themes-heading-0)))
`(howm-mode-wiki-face ((,c :inherit link)))
`(howm-reminder-deadline-face ((,c :foreground ,red-warmer :underline nil)))
`(howm-reminder-late-deadline-face ((,c :inherit bold :foreground ,date-deadline :underline nil)))
`(howm-reminder-defer-face ((,c :foreground ,date-scheduled :underline nil)))
`(howm-reminder-schedule-face ((,c :foreground ,date-scheduled :underline nil)))
`(howm-reminder-done-face ((,c :foreground ,prose-done :underline nil)))
`(howm-reminder-todo-face ((,c :foreground ,prose-todo :underline nil)))
`(howm-reminder-normal-face ((,c :foreground ,date-common :underline nil)))
`(howm-reminder-today-face ((,c :inherit bold :foreground ,bg-main :background ,yellow-warmer :underline nil)))
`(howm-reminder-tomorrow-face ((,c :inherit bold :foreground ,date-scheduled :underline nil)))
`(howm-simulate-todo-mode-line-face ((,c :inherit bold)))
`(howm-view-empty-face (( )))
`(howm-view-hilit-face ((,c :inherit match)))
`(howm-view-name-face ((,c :inherit bold)))
`(iigrep-counts-face1 ((,c :foreground ,rainbow-1)))
`(iigrep-counts-face2 ((,c :foreground ,rainbow-2)))
`(iigrep-counts-face3 ((,c :foreground ,rainbow-3)))
`(iigrep-counts-face4 ((,c :foreground ,rainbow-4)))
`(iigrep-counts-face5 ((,c :foreground ,rainbow-5))))))
(provide 'dw-writing)
;; -*- lexical-binding: t; -*-
;;; ----- TODO Configuration -----
(setq org-todo-keywords
'((sequence "TODO(t)" "WAIT(w)" "|" "DONE(d!)")))
(setq org-todo-keyword-faces
'(("GOAL" . (:foreground "orange red" :weight bold))
("WAIT" . (:foreground "HotPink2" :weight bold))
("BACK" . (:foreground "MediumPurple3" :weight bold))))
;;; ----- Context Tags -----
(setq-default org-tag-alist
'((:startgroup)
("Areas")
(:grouptags)
("@home" . ?H)
("@work" . ?W)
(:endgroup)
(:startgrouptag . nil)
("Contexts")
(:grouptags)
("@computer" . ?C)
("@mobile" . ?M)
("@calls" . ?A)
("@errands" . ?E)
(:endgrouptag)
;; Task Types
(:startgrouptag . nil)
("Types")
(:grouptags)
("@easy" . ?e)
("@hacking" . ?h)
("@writing" . ?w)
("@creative" . ?v)
("@accounting" . ?a)
("@email" . ?m)
("@system" . ?s)
(:endgrouptag)
;; Workflow states
(:startgroup . nil)
("States")
(:grouptags)
("@plan" . ?p)
("@review" . ?r)
("@followup" . ?f)
(:endgroup)))
;; Only make context tags inheritable (what about noexport?)
(setq org-use-tag-inheritance "^@")
;;; ----- Time Tracking -----
;; Clock in on the current task when setting a timer
(add-hook 'org-timer-set-hook #'org-clock-in)
;; Clock out of the current task when the timer is complete
(add-hook 'org-timer-done-hook #'org-clock-out)
;;; ----- Agenda Configuration -----
(defvar dw/base-agenda-files '("Inbox.org" "Schedule.org")
"The base agenda files that will always be included.")
(setq org-directory "~/Notes"
org-agenda-span 'day
org-agenda-start-with-log-mode t
org-agenda-files dw/base-agenda-files
org-agenda-window-setup 'current-window)
;; Make done tasks show up in the agenda log
(setq org-log-done 'time
org-log-into-drawer t)
(defvar-keymap dw/org-key-map
:doc "Personal Org Mode key bindings"
"a" #'org-agenda)
(keymap-set global-map "C-c o" dw/org-key-map)
;;; ----- Capture Templates -----
(setq org-capture-templates
'(("t" "Task" entry (file+datetree+olp "~/Notes/2025-Notes.org" "Tasks")
"* TODO %?\n %i\n %a")
("e" "Entry" entry (file+olp+datetree "~/Notes/2025-Notes.org" "Entries")
"* %?\nEntered on %U\n %i\n %a")))
;;; ----- Denote Integration -----
(defun dw/refresh-agenda-files ()
(interactive)
(setq org-agenda-files
(append (denote-directory-files "_pra")
dw/base-agenda-files)))
(defun dw/goto-weekly-note ()
(interactive)
(let* ((note-title (format-time-string "%Y-W%V"))
(existing-notes
(denote-directory-files (format "-%s" note-title) nil t)))
(if existing-notes
(find-file (car existing-notes))
(denote note-title '("plw")))))
(defun dw/goto-daily-note ()
(interactive)
(let* ((default-directory denote-directory)
(existing-file (denote-directory-files (format-time-string "^%Y%m%d.*_pld") nil t)))
(if existing-file
(find-file (expand-file-name (car existing-file)))
(progn
(denote (format-time-string "%A, %B %e, %Y") '("pld"))
(insert-file-contents "~/Notes/templates/daily-plan.org")))))
(with-eval-after-load 'denote
;; Quick access commands
(keymap-set global-map "C-c n w" #'dw/goto-weekly-note)
;; Refresh agenda files the first time
(dw/refresh-agenda-files)
;; Update agenda files after notes are created or renamed
(add-hook 'denote-after-rename-file-hook #'dw/refresh-agenda-files)
(add-hook 'denote-after-new-note-hook #'dw/refresh-agenda-files))
(provide 'dw-workflow)
I’m leveraging the built-in project.el package to manage the tasks I perform around the projects I’m working on.
(use-package project
:ensure nil
:bind (:map project-prefix-map
("t" . eat-project))
:custom
(project-switch-use-entire-map t))
(defun dw/project-compilation-buffer-name-function (name-of-mode)
(format "*compilation: %s*" (project-name (project-current))))
(setq project-compilation-buffer-name-function 'dw/project-compilation-buffer-name-function)
(defun dw/ensure-project-in-tab (project-dir)
"Ensure we're in a tab named after the project."
(let* ((project-name (file-name-nondirectory (directory-file-name project-dir)))
(tab-names (mapcar (lambda (tab) (alist-get 'name tab)) (tab-bar-tabs)))
(existing-tab (seq-find (lambda (name) (equal name project-name)) tab-names)))
(unless existing-tab
(tab-new)
(tab-rename project-name))
(unless (equal (alist-get 'name (tab-bar--current-tab)) project-name)
(tab-bar-select-tab-by-name project-name))))
(defun dw/project-prompter ()
"Prompt for project and ensure it opens in the correct tab."
(let ((project-dir (project-prompt-project-dir)))
(dw/ensure-project-in-tab project-dir)
project-dir))
;; Use our custom prompter for all project commands
(setq project-prompter #'dw/project-prompter)
(use-package apheleia
:vc (:url "https://github.com/radian-software/apheleia"
:rev "v4.4.1")
:init
(apheleia-global-mode 1))
Eglot is the way.
(use-package eglot
:ensure nil
:hook ((js-mode
typescript-mode) . eglot-ensure))
(use-package sigil
:ensure nil
:demand t
:load-path "/home/daviwil/Projects/Code/sigil-worktrees/sigil-emacs/editor/emacs"
:custom
(sigil-repl-program "/home/daviwil/Projects/Code/sigil-worktrees/sigil-emacs/build/bin/sigil"))
(use-package js-mode
:ensure nil
:mode ("\\.jsx?\\'")
:config
(setq-default js-indent-level 2))
(use-package typescript-mode
:mode ("\\.tsx?\\'")
:config
(setq-default typescript-indent-level 2))
Janet is probably my second-favorite language for project hacking right now.
(use-package janet-mode
:ensure nil)
Finish up by providing the feature.
(provide 'dw-develop)
(use-package dockerfile-mode)
I’m planning to develop a new presentation configuration using Prot’s Logos package because it’s a bit more flexible and will enable me to treat code files as presentations in addition to Org files and pretty much any other type of file that has syntax that can be interpreted as a page marker (like a comment string).
;; -*- lexical-binding: t; -*-
(defun dw/present-prepare-slide ()
(when (and logos-focus-mode
(derived-mode-p 'org-mode))
(org-overview)
(org-show-entry)
(org-show-children)))
(defun dw/present-toggle ()
"Configures the buffer for a presentation."
(interactive)
(if logos-focus-mode
(progn
(setq-local face-remapping-alist nil)
(widen)
(logos-focus-mode 0))
(setq-local face-remapping-alist '((default (:height 1.5) default)
(org-document-title (:height 1.75) org-document-title)
(org-block-begin-line (:height 0.7) org-block)))
;; Narrow the buffer and start focus mode
(logos-narrow-dwim)
(logos-focus-mode 1)
;; Prepare the slide
(dw/present-prepare-slide)))
(use-package logos
:bind (([remap narrow-to-region] . logos-narrow-dwim)
([remap forward-page] . logos-forward-page-dwim)
([remap backward-page] . logos-backward-page-dwim))
:custom
(logos-outlines-are-pages t)
(logos-scroll-lock t)
:config
(setf (alist-get 'org-mode logos-outline-regexp-alist) "^\\*\\{1,2\\} +")
(add-hook 'logos-page-motion-hook #'dw/present-prepare-slide))
(provide 'dw-present)
I prefer rcirc as my default IRC client, it’s simple and gets the job done. I’ve mainly moved to using an XMPP server with biboumi as an IRC bridge, though.
;; -*- lexical-binding: t; -*-
(use-package rcirc
:ensure nil
:custom
(rcirc-default-nick "daviwil")
(rcirc-default-user-name "daviwil")
(rcirc-default-full-name "David Wilson")
(rcirc-server-alist `(("chat.sr.ht"
:port 6697
:encryption tls
:user-name "daviwil/irc.libera.chat@emacs")))
(rcirc-reconnect-delay 5)
(rcirc-fill-column 90)
(rcirc-track-ignore-server-buffer-flag t)
:config
;; Annoy me, please
(rcirc-track-minor-mode 1)
;; See: https://idiomdrottning.org/rcirc-soju
(defun-rcirc-command detach (channel)
"Detach channel to soju."
(interactive "sPart channel: ")
(let ((channel (if (> (length channel) 0) channel target)))
(rcirc-send-privmsg
process "BouncerServ"
(format
"channel update %s -detached true -reattach-on highlight" channel)))))
(use-package jabber
:custom
(jabber-chat-buffer-format "Jabber: %n")
:config
;; Unset the global key binding
(global-set-key (kbd "C-x C-j") #'dired-jump))
(provide 'dw-chat)
The 0x0.el package is really useful and I used it for a long time. However, it’s currently only on MELPA and I really only use a couple of the functions it provides.
Here’s my own minimal implementation from scratch, please feel free to copy into your own config!
;; -*- lexical-binding: t; -*-
(defun dw/0x0-upload-text ()
(interactive)
(let* ((contents (if (use-region-p)
(buffer-substring-no-properties (region-beginning) (region-end))
(buffer-string)))
(temp-file (make-temp-file "0x0" nil ".txt" contents)))
(message "Sending %s to 0x0.st..." temp-file)
(let ((url (string-trim-right
(shell-command-to-string
(format "curl -s -F'file=@%s' https://0x0.st" temp-file)))))
(message "The URL is %s" url)
(kill-new url)
(delete-file temp-file))))
(defun dw/0x0-upload-file (file-path)
(interactive "fSelect a file to upload: ")
(message "Sending %s to 0x0.st..." file-path)
(let ((url (string-trim-right
(shell-command-to-string
(format "curl -s -F'file=@%s' https://0x0.st" (expand-file-name file-path))))))
(message "The URL is %s" url)
(kill-new url)))
(provide 'dw-0x0)
;; -*- lexical-binding: t; -*-
(use-package tracking
:demand t)
(use-package telega
:commands telega
:config
(setq telega-use-tracking-for '(or unmuted mention)
telega-completing-read-function #'completing-read
telega-msg-rainbow-title t
telega-chat-fill-column 75)
;; Show notifications in the mode line
(add-hook 'telega-load-hook #'telega-mode-line-hook)
;; Disable chat buffer auto-fill
(add-hook 'telega-chat-mode-hook #'telega-chat-auto-fill-mode)
(when (eq dw/current-distro 'void)
(setq telega-server-libs-prefix "/usr")))
(provide 'dw-telegram)
When used as part of a desktop environment, here are some packages that will make it easier for Emacs to drive most of the experience.
;; -*- lexical-binding: t; -*-
;; Integrate with the system clipboard
(unless (display-graphic-p)
(use-package xclip
:demand t
:config
(xclip-mode 1)))
;; Pull in $PATH from the shell environment
(use-package exec-path-from-shell
:init
(exec-path-from-shell-initialize))
(use-package bluetooth)
;; :commands (bluetooth-list-devices))
;; Control NetworkManager via nmcli
(use-package nm
:vc (:url "https://github.com/Kodkollektivet/emacs-nm"
:rev :newest))
(use-package inf-mongo
:vc (:url "https://github.com/endofunky/inf-mongo"
:rev :newest))
(provide 'dw-desktop)
I use GPG primarily when authenticating with Git remotes or opening pass entries. pinentry is the tool that enables me to prompt for my GPG passphrase inside of Emacs.
auth-source-pass enables the use of pass entries for automatically loading credentials for certain things like my IRC bouncer password.
;; -*- lexical-binding: t; -*-
;; Use `pass` as an auth-source
(when (file-exists-p "~/.password-store")
(auth-source-pass-enable))
;; Enable GPG passphrase entry
(use-package pinentry
:demand t
:config
(pinentry-start))
(provide 'dw-auth)
(use-package gnus
:ensure nil
:config
(setq gnus-select-method '(nnnil ""))
(setq gnus-secondary-select-methods
'((nnimap "fastmail"
(nnimap-address "imap.fastmail.com")
(nnir-search-engine imap)
;; Move expired mail items to the trash immediately
(nnmail-expiry-target "nnimap+fastmail:Trash")
(nnmail-expiry-wait immediate))
(nntp "gwene" (nntp-address "news.gwene.org"))
(nntp "news.yhetil.org")))
(add-hook 'gnus-after-getting-new-news-hook 'gnus-notifications)
(require 'gnus-demon)
(setq gnus-demon-handlers
'((gnus-demon-scan-news t 60)
(gnus-demon-scan-mail t 60)))
(gnus-demon-init))
(provide 'dw-mail)
The emacs-guix package is quite convenient for searching Guix packages from within Emacs. I’ve started running my config application scripts using async-shell-command so I’ve included a command to make that more convenient.
;; -*- lexical-binding: t; -*-
(defun dw/guix-update-channels ()
(interactive)
(async-shell-command "~/.dotfiles/.files/.bin/update-channels" "*guix-channel-update*"))
(defun dw/guix-update-system ()
(interactive)
(async-shell-command "~/.dotfiles/.files/.bin/update-system" "*guix-system-update*"))
(use-package guix
:ensure nil
:if dw/is-guix-system
:bind (("C-c G p" . guix-packages-by-name)
("C-c G u c" . dw/guix-update-channels)
("C-c G u s" . dw/guix-update-system)))
(provide 'dw-system)
This module uses xterm-color to improve ANSI escape handling in eshell and M-x compile so that colors and other terminal behavior comes through correctly.
;; -*- lexical-binding: t; -*-
(use-package xterm-color
:ensure nil ; Not available on ELPA repos
:config
;; Enable ANSI escape handling in eshell
(with-eval-after-load 'eshell
(add-hook 'eshell-before-prompt-hook
(lambda ()
(setq xterm-color-preserve-properties t)))
(add-to-list 'eshell-preoutput-filter-functions 'xterm-color-filter)
(setq eshell-output-filter-functions (remove 'eshell-handle-ansi-color eshell-output-filter-functions))
(setenv "TERM" "xterm-256color"))
;; Enable ANSI escape handling in M-x compile
(setq compilation-environment '("TERM=xterm-256color"))
(defun dw/advice-compilation-filter (f proc string)
(funcall f proc (xterm-color-filter string)))
(advice-add 'compilation-filter :around #'dw/advice-compilation-filter))
(use-package eat
:config
(add-hook 'eshell-load-hook #'eat-eshell-mode))
I’ve been using Claude Code lately to assist with writing tools to enhance my workflow in certain areas. These packages make it easier to interact with Claude Code in Emacs.
(provide 'dw-ai)
This section contains settings that are specific to various systems, particularly to choose extension modules to be loaded on top of the base configuration.
Guix 🐃
(setq dw/use-config-modules
(append dw/common-config-modules
'(dw-desktop
dw-system
dw-develop
dw-telegram
dw-chat
dw-mail
dw-ai)))
Guix 🐃
(setq dw/use-config-modules
(append dw/common-config-modules
'(dw-desktop
dw-system)))
Windows 🤮
(setq dw/use-config-modules dw/common-config-modules)
(setq xclip-method 'powershell)
macOS 🤮
(setq dw/use-config-modules
(append dw/common-config-modules
'(dw-desktop
dw-system
dw-develop
dw-ai)))
;; Treat Command as Ctrl inside of Emacs (because I remap Ctrl to Command)
(setq ns-command-modifier 'meta)
;; Use the traditional approach to setting alpha on macOS
(set-frame-parameter (selected-frame) 'alpha 93)
(add-to-list 'default-frame-alist '(alpha . 93))
;; Use ls from coreutils in Dired
(setq insert-directory-program "gls"
dired-use-ls-dired t)
;; Adjust font sizes
;; TODO: Use defvars instead
(add-hook 'after-init-hook
(lambda ()
(set-face-attribute 'default nil :height 166)
(set-face-attribute 'fixed-pitch nil :height 166)
(set-face-attribute 'variable-pitch nil :height 146)))