162 KiB
Toki's Doom Emacs Configuration!
- Introduction
- Configuration Support
- Doom Modules
- Basic Configuration
- Aesthetics
- Packages
- Applications
- Org
- Languages and Modes
Emacs outshines all other editing software in approximately the same way that the noonday sun does the stars. It is not just bigger and brighter; it simply makes everything else vanish.
– Neil Stephenson, In the Beginning was the Command Line (1998)
Introduction
Hello!
This is a literate configuration for Doom Emacs.
Background; or, My Emacs Story
Given that you are currently reading an Emacs config, I will assume that you already have a moderate understanding of what Emacs is and the ideas behind its configuration system. If you do not, then this section of the official Doom Emacs documentation makes for a decent introduction.
Rather than use this space to explain Emacs, I will instead use it to chronicle my history with Emacs, how I got here, and what lessons should be taken away from this experience. Don't worry, I promise it won't be long.
In The Beginning
My first brush with Emacs was in around 2019, when I installed it for use with the proof assistant language Agda. I had vaguely heard tales about its beauty and power, but I was nowhere near comfortable enough with config files and programming in general to fully appreciate its capabilities (not to mention that I was using Windows at the time). I bounced off of it pretty quickly because… well, vanilla Emacs is just kinda terrible.
A few years later in 2022, after I had moved to the more sensible OS of Arch Linux, I discovered that my preferred text editor Atom was in the process of being discontinued and began to look for a replacement. I tried Visual Studio Code for a little while, but after some serious use I became dissatisfied with how few options there were to customize it to fit my workflow.
It was at this point that I started thinking about Emacs again. By chance, I happened to stumble upon Doom Emacs, and it turned out to be exactly what I was looking for:
- Extreme flexibility
- Robust modular configuration system
- Sensible defaults
- Extensive ecosystem
As I became more comfortable with configuration via scripting, I immersed myself into the many utilities that make up the Emacs ecosystem, especially org-mode
, the gold standard of Emacs tools. I started putting more and more time into tweaking this environment to fit my needs, my files kept getting longer and longer, and eventually I fully fell off the deep end and now we're here.
Literate Programming
My first Doom Emacs config was hacked together directly from the generated example config: no comments, no organization, nothing. after!
and use-package!
blocks were scattered about the file without rhyme or reason, making it very difficult to remember what any particular line of code was actually doing. I was able to mitigate some of this issue by categorizing my config into multiple files, but at the end of the day it was a losing battle. The config directory was at around 1200 lines of code before I decided that something needed to be done.
I was considering what to do about this problem of organizational decay when I came across Tecosaur's config and learned about org-mode
's literate programming support. I was very comfortable with org-mode
at this point, so it seemed like a good solution and a natural next step.
Org and Source Code
This file is written in Org, a plain-text markup language similar to Markdown in basic structure. Like Markdown, it has support for embedding blocks of source code in between paragraphs.
Unlike a standard markup language, however, Org mode has built-in capabilities for executing code and inserting the output into the document, somewhat similar to a Jupyter notebook.
;; This is Emacs Lisp code in a src block!
(format "Result: %s" (+ 1 2))
Result: 3
The second block above was automatically generated by org-mode
.
(This feature does not exclusively work with Emacs Lisp, but other languages require the corresponding build tools to be installed.)
Tangle, Weave, Export, Publish
Since Org's ability to execute code and process its output is so robust, it's only natural that one might consider using Org to annotate a codebase, storing the code inside source blocks in the document. This would provide the space for much more thorough commentary than regular comments, but also prevent the file from being interpreted as a program, which is the important part! You could maintain both the Org file and the actual source files separately, but that would require manually duplicating edits in both files, which is not ideal.
The solution is literate programming, the practice of embedding usable code into a markup document. Literate programming can be handled in a few different ways. A handful of languages support directly processing these markup documents, such as Haskell and Julia; however, the way Org implements it is closer to the traditional method, which involves two parallel processes:
- Tangling, converting the file into raw source code. Tangling must be performed before the code is used; in Org, code is tangled into a new file, and this can be done to generate many source files from a single Org file.
- Weaving, formatting the file into a human-readable document. Org conceptually divides this further into "exporting" and "publishing," where the latter is intended specifically for converting into web-ready HTML.
You are currently reading the published version of this literate program1. If you were to download this repository and use it as your config, Emacs would be running the tangled version. These versions are generated in separate processes, but both are ultimately derived from the content and metadata inside of the Org file.
Code and Comprehension
The simultaneous handling of documentation and code inherent to literate programming is reminiscent of documentation generation (doc comments) in traditional programming. Both systems involve superimposing code and documentation into one file, but the literate style takes the concept one step further; the document isn't embedded in the code, the code is embedded in the document.
Instead of documentation having to be bent around the restrictions of source code, the source code can be written and organized with all the freedoms of prose. If written well, the literate program can be structured in a manner closer to how the human mind understands code, rather than how a computer processes it. This is assisted by features such as literate macros, features intended to break one's code out of the restrictions of standard programming.
It's not the right tool for every codebase, but proper use of literate programming can make a program much, much easier to comprehend and maintain. This is especially true for configuration languages like Emacs Lisp, where much of the code is conceptually disconnected and can easily be split into categories.
Current Issues
TODO Nix
Creating garbage collection roots currently doesn't work.
TODO Idris
The configuration for Idris is a bit light, and could use some touching up.
TODO Mail
My mail client currently requires GPG access to sync emails, which doesn't properly work. Using the mail client requires running mbsync -a
externally instead.
Org
TODO Configure Org popups
Configuration Support
A proper Emacs configuration is often so complex that it requires its own support code to ease the process of writing and maintaining it. Before the configuration itself, here's all the support systems it uses (aside from Doom Emacs itself, which is also configuration support).
confpkg
As part of their own literate config, Tecosaur implemented confpkg
, an embedded Emacs Lisp library that manages multiple aspects of config tangling:
- Controlling what generated files each code block is tangled to
- Creating package files from templates
- Automatically detecting cross-section dependencies
- Reporting profiling information on config load times
It's an incredibly impressive utility, and I highly recommend reading the section in their config on its design. I tried to read through it myself, but I don't understand half of it; it's a bizarre mixture of exploits to hook into org-mode
's tangling process, self-modifying buffer shenanigans, and abuse of various features of org-babel
, the package that manages source blocks.
Luckily, I don't need to be able to understand code in order to do what I do best: press Ctrl+C
and Ctrl+V
in that order. Programming!
If you're reading the raw org file instead of the published version, the code for confpkg
is below. It is mostly unchanged, aside from these tweaks:
- Prevent
confpkg
's code from being exported - Change the package template to contain my information
- Reorganize to get rid of superfluous noweb references
- Hook only into babel calls that contain
confpkg
as a substring - Allow package statements anywhere in subconfig files, rather than only at the beginning
Source Code Patching
Emacs's runtime and configuration systems are designed to be as flexible as possible, so flexible that it even lets you directly modify the source code of your dependencies.
… Unfortunately, this means that you are now directly modifying the source code of your dependencies. This can get very messy without proper precautions, and while Emacs provides limited tools like the advising system, they don't always have enough flexibility to get the job done. The package el-patch
exists as a more powerful alternative.
(package! el-patch)
The package allows you to define patches which modify the definitions of functions in systematic ways. The suite of tools provided by the package is easily comprehensive enough for my needs, but we can make the API a bit nicer with some macros and configure options:
(defmacro defpatch! (feature type-name &rest args)
"Define a patch over something defined in FEATURE."
(declare (doc-string 4) (indent 3))
(let* ((el-patch (intern (format "el-patch-%s" (car type-name))))
(patch `(,el-patch ,(cadr type-name) ,@args)))
(if feature
`(progn
(el-patch-feature ,feature)
(after! ,feature
,patch))
patch)))
(defmacro deftemplate! (feature type-name &rest args)
"Define a template over something defined in FEATURE."
(declare (doc-string 3) (indent 2))
(let* ((template `(el-patch-define-and-eval-template ,type-name ,@args)))
(if feature
`(progn
(el-patch-feature ,feature)
(after! ,feature
,template))
template)))
(after! el-patch
(setq el-patch-warn-on-eval-template nil))
Automated Nix Builds
Some packages in this config such as dirvish
, org-roam
, etc. require certain tools to be in the environment. On a Nix-based system, there are a few different ways to handle this:
- Put that tool in the actual environment, e.g. in a profile. This makes sense for simple things (
ripgrep
,sqlite
, etc) but for more opinionated things like an instance of Python it becomes less desirable. - Build the tool and put a symlink to the output somewhere, e.g. in the HOME directory. This avoids polluting the environment, but you still have to deal with an unwieldy symlink that breaks Emacs if you accidentally delete it. This was my approach before coming up with the third option:
- Build the tool and point Emacs directly to the store path. This is the most automatic solution, but requires the most complex Emacs configuration.
This section is an implementation of that third solution.
We first need a function to build a flake reference:
(defun nix-build-out-path (out &optional impure)
"Build the given flake output OUT and return the output path. Return
nil if the build fails.
If IMPURE is t, then allow impure builds."
(require 'nix) (require 's)
(with-temp-message (format "Building \"%s\" ..." out)
(with-temp-buffer
(let* ((args `("build" "--no-link" "--print-out-paths"
,@(when impure '("--impure")) ,out))
(status (apply #'call-process nix-executable nil
(list (current-buffer) nil) nil args)))
(when (eql status 0)
(s-trim (buffer-string)))))))
This works if we just want to start a build, but there's a problem: we haven't indicated to Nix that we're using this output for something, so it will be deleted the next time we garbage collect. To fix this, we can write a wrapper function that also makes the output path a garbage collection root.
(defun nix-build-out-path-gcroot (name out &optional impure)
"Build the given flake output OUT, register its output path as
a garbage collection root under NAME, and return the output path.
Return nil if the build fails.
The GC root is placed under \"/nix/var/nix/gcroots/emacs/NAME\". If
a call to this function reuses the same NAME argument, then the
symlink is overwritten.
If IMPURE is t, then allow impure builds."
(when-let* ((path (nix-build-out-path out impure))
(gcdir "/nix/var/nix/gcroots/emacs")
(sym (expand-file-name name gcdir)))
(unless (equal path (file-symlink-p sym))
(require 'tramp)
(make-directory (concat "/sudo::" gcdir) t)
(make-symbolic-link path (concat "/sudo::" sym) t))
path))
Doom Modules
One of Doom Emacs's most useful features is its modular configuration system, allowing configuration code to be sectioned into modules that can be enabled or customized individually. Doom provides a full suite of prewritten modules to enable.
;;; init.el -*- lexical-binding: t; -*-
;; This file controls what Doom modules are enabled and what order they load in.
(doom!
...
)
Config Modules
Since this is a literate config, the corresponding :config literate
module is necessary. We'll also turn on some of the default config options too.
:config
literate
(default +bindings +smartparens)
Completion
I'm a big fan of the Vertico ecosystem, as it's lightweight and easy to use. Let's turn on that module, along with Corfu for a nice in-buffer completion popup and some icons because why not.
:completion
(vertico +icons)
(corfu +icons +orderless)
Checkers
The two most common syntax checking engines seem to be flymake
and flycheck
. flymake
is built in to Emacs, is generally faster and currently has better support in the ecosystem, so let's use that one.
We'll also enable a dedicated spell checking module using aspell
, as that seems to be the recommended option.
:checkers
(syntax +flymake +childframe)
(spell +aspell)
;;grammar
UI
Most of these are either defaults that come with Doom Emacs or just recommended, but here are the highlights:
vi-tilde-fringe
because I like how it looksfile-templates
andsnippets
because typing is hard(format +onsave)
because I don't want to have to remember to run a formatterdirenv
because I'm a nix user- Icons!
:ui
deft
doom
doom-dashboard
;;doom-quit
;;(emoji +unicode)
hl-todo
;;hydra
indent-guides
;;ligatures
;;minimap
modeline
;;nav-flash
;;neotree
ophints
(popup +defaults)
;;tabs
;;(treemacs +lsp)
unicode
(vc-gutter +pretty)
vi-tilde-fringe
;;(window-select +numbers)
workspaces
;;zen
:editor
(evil +everywhere)
file-templates
fold
(format +onsave)
;;god
;;lispy
;;multiple-cursors
;;objed
;;parinfer
;;rotate-text
snippets
word-wrap
:tools
;;ansible
biblio
;;collab
;;debugger
direnv
;;docker
;;editorconfig
;;ein
(eval +overlay)
;;gist
(lookup +docsets)
lsp
(magit +forge)
make
pass
pdf
;;prodigy
;;rgb
;;terraform
tree-sitter
;;tmux
;;upload
:emacs
(dired +dirvish +icons)
electric
(ibuffer +icons)
(undo +tree)
vc
:os
tty
Apps
Who doesn't love doing everything in Emacs?
:term
vterm
:email
(mu4e +org +gmail)
:app
calendar
;;emms
everywhere
;;irc
;;(rss +org) ; One day...
;;twitter
Language Modules
Doom Emacs provides a large collection of modules for different languages. Which is good, because setting up language mode packages is kind of annoying.
:lang
(agda +tree-sitter +local)
;;beancount
;;(cc +lsp)
;;clojure
;;common-lisp
;;coq
;;crystal
;;csharp
data
;;(dart +flutter)
dhall
;;elixir
;;elm
emacs-lisp
;;erlang
;;ess
;;factor
;;faust
;;fortran
;;fsharp
;;fstar
;;gdscript
;;(go +lsp)
;;(graphql +lsp)
(haskell +lsp)
;;hy
idris
;;json
(java +lsp +tree-sitter)
;;javascript
;;julia
;;kotlin
(latex +lsp)
;;lean
;;ledger
;;lua
markdown
;;nim
(nix +lsp +tree-sitter)
;;ocaml
(org +roam2 +present
+gnuplot +jupyter
+pandoc +journal)
;;php
;;plantuml
;;purescript
(python +lsp +tree-sitter)
;;qt
;;racket
;;raku
;;rest
;;rst
;;(ruby +rails)
(rust +lsp +tree-sitter)
(scala +lsp +tree-sitter)
;;(scheme +guile)
(sh +fish +lsp +tree-sitter)
;;sml
;;solidity
;;swift
;;terra
(web +lsp +tree-sitter)
yaml
;;zig
Basic Configuration
This is mostly config settings that don't belong to any particular package and aren't important enough to get their own major section.
Sensible Settings
It wouldn't be Emacs if there wasn't an endless list of config variables to change every aspect of its function!
(setq-default delete-by-moving-to-trash t ; Delete files to trash
window-combination-resize t ; Resize windows more evenly
)
(setq compile-command "nix build"
shell-file-name (executable-find "bash") ; Use bash instead of fish for default shell
disabled-command-function nil ; Disabled commands are a stupid idea
password-cache-expiry nil ; Security? Never heard of it
scroll-margin 2 ; A few extra lines on each end of the window
)
(global-subword-mode +1)
Personal Information
Emacs uses this basic personal information for a few different things, mostly applications.
(setq user-full-name "Kiana Sheibani"
user-mail-address "kiana.a.sheibani@gmail.com")
Authentication
The auth-source-pass
module lets you use pass as a source for authentication. Let's turn that on.
(require 'auth-source-pass)
(setq auth-sources '(password-store "~/.authinfo.gpg")
auth-source-cache-expiry nil)
Bindings
Workspaces
I like to reorganize my workspaces, so we can add bindings to change the workspace order.
(map! :leader
:desc "Move workspace left"
"TAB h" #'+workspace/swap-left
:desc "Move workspace right"
"TAB l" #'+workspace/swap-right)
Leader Key
It's sometimes useful to have a universal-argument
binding that doesn't go through the leader key.
(map! :map global-map
"M-u" #'universal-argument
:map universal-argument-map
"M-u" #'universal-argument-more)
It's also sometimes useful to have an evil-ex
binding that does go through the leader key.
(map! :leader
"w :" nil
":" #'evil-ex)
Evil Macros
Seeing as it's practically the Evil Emacs version of C-g
, I often end up accidentally pressing q
in a non-popup buffer, which starts recording a macro. That's very annoying, and I don't use macros enough to justify that annoyance.
(map! :map evil-normal-state-map
"q" nil
"C-q" #'evil-record-macro)
Creating New Projects
Whenever I want to make a new project, having to create a new directory, initialize Git, and register it with Projectile is cumbersome. A new command to do all of those steps in one go sounds like a good idea.
(defun create-new-project (dir type &optional parents)
"Create a new directory DIR and add it to the list of known projects.
TYPE specifies the type of project to create. It can take the following values:
- `git', which creates a new Git repository.
- `projectile', which creates a .projectile file in the project root.
- A string, which is used as a filename to create in the project root.
- A function, which is called with no arguments inside the root of the project.
When called interactively, this defaults to `git' unless a prefix arg is given.
If PARENTS is non-nil, the parents of the specified directory will also be created."
(interactive (list (read-directory-name "Create new project: ")
(if current-prefix-arg
(intern (completing-read "Project type: "
'("git" "projectile") nil t))
'git) t))
(make-directory dir parents)
(let ((default-directory dir))
(pcase type
('git
(shell-command "git init"))
('projectile
(make-empty-file ".projectile"))
((pred stringp)
(make-empty-file type))
((pred functionp)
(funcall type))))
(projectile-add-known-project dir))
(map! :leader
:desc "Create new project"
"p n" #'create-new-project)
Misc.
(map! :leader
:desc "Undo Tree"
"o u" #'undo-tree-visualize
:desc "Open URL"
"s u" #'goto-address-at-point
:desc "Nerd Icons"
"i i" #'nerd-icons-insert)
Windows
Prefer Vertical Split
Emacs has a sophisticated system for controlling how windows and buffers are arranged on the screen, called display-buffer
. This works out of the box for almost every case I've thrown at it, except for one issue: it prefers horizontal splitting of windows to vertical splitting, despite the fact that the latter is far better in most cases.
(defpatch! nil
(defun split-window-sensibly) (&optional window)
"Split WINDOW in a way suitable for `display-buffer'.
WINDOW defaults to the currently selected window.
If `split-height-threshold' specifies an integer, WINDOW is at
least `split-height-threshold' lines tall and can be split
vertically, split WINDOW into two windows one above the other and
return the lower window. Otherwise, if `split-width-threshold'
specifies an integer, WINDOW is at least `split-width-threshold'
columns wide and can be split horizontally, split WINDOW into two
windows side by side and return the window on the right. If this
can't be done either and WINDOW is the only window on its frame,
try to split WINDOW vertically disregarding any value specified
by `split-height-threshold'. If that succeeds, return the lower
window. Return nil otherwise.
By default `display-buffer' routines call this function to split
the largest or least recently used window. To change the default
customize the option `split-window-preferred-function'.
You can enforce this function to not split WINDOW horizontally,
by setting (or binding) the variable `split-width-threshold' to
nil. If, in addition, you set `split-height-threshold' to zero,
chances increase that this function does split WINDOW vertically.
In order to not split WINDOW vertically, set (or bind) the
variable `split-height-threshold' to nil. Additionally, you can
set `split-width-threshold' to zero to make a horizontal split
more likely to occur.
Have a look at the function `window-splittable-p' if you want to
know how `split-window-sensibly' determines whether WINDOW can be
split."
(let ((window (or window (selected-window))))
(or (el-patch-let
(($fst (and (window-splittable-p window)
;; Split window vertically.
(with-selected-window window
(split-window-below))))
($snd (and (window-splittable-p window t)
;; Split window horizontally.
(with-selected-window window
(split-window-right)))))
(el-patch-swap $fst $snd)
(el-patch-swap $snd $fst))
(and
(let ((frame (window-frame window)))
(or
(eq window (frame-root-window frame))
(catch 'done
(walk-window-tree (lambda (w)
(unless (or (eq w window)
(window-dedicated-p w))
(throw 'done nil)))
frame nil 'nomini)
t)))
(not (window-minibuffer-p window))
(let (((el-patch-swap split-height-threshold
split-width-threshold)
0))
(when (window-splittable-p window)
(with-selected-window window
((el-patch-swap split-window-below split-window-right)))))))))
;; Minimum window height and width
(setq split-height-threshold 10
split-width-threshold 60)
Window Split Prompt
When a window is split in Emacs, the result is two windows that show the same buffer. This is an odd state for Emacs to be in, and it's annoying to deal with afterwards, so let's fix it so that a buffer prompt is brought up for the new window.
;; Select new window instead of old window
(setq evil-vsplit-window-right t
evil-split-window-below t)
(defadvice! ~/evil-window-split-prompt (&rest _)
"Prompt for a buffer to show on a new split window."
:after (list #'evil-window-split #'evil-window-vsplit)
(condition-case error
(+vertico/switch-workspace-buffer t)
(quit (evil-window-delete)
(signal 'quit (cdr error)))))
Aesthetics
If you're going to be staring at your screen for hours a day, you might as well make the thing you're staring at look nice.
Theme
My favorite color theme has always been Tokyo Night. I use it literally everywhere I can, and Doom Emacs is no exception.
(setq doom-theme 'doom-tokyo-night)
Fonts
Victor Mono is my preferred coding font. I also use Source Sans Pro as my sans-serif font, though that is more an arbitrary pick than actually liking how it looks.
(setq doom-font (font-spec :family "VictorMono" :size 13)
doom-variable-pitch-font (font-spec :family "Source Sans Pro" :size 16))
I'm a very big fan of how italics look in Victor Mono, so let's make more things italicized! While we're here, we'll also set doom's modified buffer font to be orange instead of yellow (I like how it looks better).
(custom-set-faces!
'(font-lock-comment-face :slant italic)
'(font-lock-variable-name-face :slant italic)
`(doom-modeline-buffer-modified
:foreground ,(doom-color 'orange)
:weight bold
:inherit doom-modeline))
Some other small aesthetic changes:
(setq nerd-icons-scale-factor 1.1 ; Make icons slightly larger
doom-modeline-height 24 ; Make Doom's modeline taller
display-line-numbers-type t) ; Line numbers (absolute)
Line Wrapping
I have rather specific tastes when it comes to line wrapping. I like soft line wrapping (visual-line-mode
), but I want it to be as seamless as possible.
(setq +word-wrap-fill-style 'soft ; Soft line wrapping
evil-respect-visual-line-mode t ; Respect visual line mode
)
(setq-default fill-column 90) ; More space before wrap
Hacks
Evil
For some reason, telling Evil to respect soft line wrapping doesn't change the behavior of j
and k
, so I'll do that myself.
(after! evil
(evil-define-motion evil-next-line (count)
"Move the cursor COUNT lines down."
:type line
(let ((line-move-visual evil-respect-visual-line-mode))
(evil-line-move (or count 1))))
(evil-define-motion evil-previous-line (count)
"Move the cursor COUNT lines up."
:type line
(let ((line-move-visual evil-respect-visual-line-mode))
(evil-line-move (- (or count 1))))))
Modeline
The visual-fill-column
package works by expanding the window's right margin. This causes the right edge of the modeline to follow the margin as well, which looks a bit strange. As a hacky fix, I've found that configuring the the modeline to align itself to the right fringe instead of the right window edge seems to fix the issue.
(setq mode-line-right-align-edge 'right-fringe)
Line Numbers
When a buffer has line numbers, they can interfere with the margins and make the line smaller than it should be. We can mitigate this issue by adding extra columns to the window.
(add-hook! display-line-numbers-mode
(require 'visual-fill-column)
(when visual-fill-column-mode
(setq-local visual-fill-column-extra-text-width '(0 . 6))
(visual-fill-column--adjust-window)))
Dashboard
There's a lot of reasons why I don't like Spacemacs and why I left it for Doom Emacs (mainly the fact that it's slow and often opaque to the user), but there's one thing that Spacemacs undoubtedly has Doom beat in:
Spacemacs's dashboard has impeccable style.
Doom Emacs tends to favor practicality over aesthetics, and its dashboard is no exception. If we want something that looks visually appealing, we're going to need a serious overhaul.
Splash Banner
The Doom dashboard allows the use of an image for its banner, which supports any image type Emacs can display, including SVG. I have procured an SVG image to use for my dashboard, the classic Emacs E:
The obvious choice for the fill color of the image would have been purple, the standard highlight color of my theme, but I wanted the banner to pop out a bit more.
The image can be set like thus:
(setq fancy-splash-image
(expand-file-name "assets/splash.svg" doom-private-dir))
Title
Since our banner no longer includes a title, we should add one after the splash image. This title format is inspired by Spacemacs!
(defface doom-dashboard-title
'((t (:weight bold :inherit warning)))
"Face used for the Doom Emacs title on the dashboard."
:group 'doom-dashboard)
(setq +doom-dashboard-banner-padding '(0 . 3))
(defvar +doom-dashboard-title-padding 3)
(defun doom-dashboard-widget-title ()
(when (display-graphic-p)
(insert (propertize
(+doom-dashboard--center
+doom-dashboard--width
"[D O O M E M A C S]")
'face 'doom-dashboard-title)
(make-string +doom-dashboard-title-padding ?\n))))
To add the title to the dashboard, we create a new widget that inserts the title string with some padding. We only do this on graphical displays, as non-graphical ones fall back on the default ASCII banner, which includes a title.
Other Tweaks
We'll put our title widget into the +doom-dashboard-functions
hook, and while we're at it we'll also get rid of the footer widget, which I don't see much use for.
(setq +doom-dashboard-functions
'(doom-dashboard-widget-banner
doom-dashboard-widget-title
doom-dashboard-widget-shortmenu
doom-dashboard-widget-loaded))
We should also declutter some other aspects of the dashboard. Since the dashboard has load information built into it, I don't see much purpose in printing it to the minibuffer on startup.
(remove-hook 'doom-after-init-hook #'doom-display-benchmark-h)
Packages
Now that we've enabled our preferred modules and done some basic configuration, we can install and configure our packages.
Corfu
(after! corfu
;; I don't really use TAB very often, so prefer other actions
(setq +corfu-want-tab-prefer-navigating-snippets t
+corfu-want-tab-prefer-expand-snippets t
+corfu-want-tab-prefer-navigating-org-tables t
corfu-min-width 25))
Omnicomplete Bindings
Before switching to Corfu, I got really used to Doom's omnicomplete bindings for various company backends. Doom unfortunately doesn't do this for CAPFs, so we'll have to do the work ourselves.
(map! :prefix "C-x"
:i "C-e" #'cape-elisp-symbol
:i "C-f" #'cape-file
:i "C-l" #'cape-line
:i "C-]" #'complete-tag
:i "s" #'cape-dict
:i "C-s" #'yasnippet-capf
:i "C-d" #'cape-dabbrev
:i "\\" #'cape-tex)
Window Numbers
Corfu's posframe buffer shouldn't be numbered by winum
:
(after! (corfu winum)
(push " *corfu*" winum-ignored-buffers))
Eldoc
We'll switch the default docstring handler to eldoc-documentation-compose
, since that provides the most information and I don't mind the space it takes up.
(after! eldoc
(setq eldoc-documentation-strategy 'eldoc-documentation-compose))
Embark
When I first learned about Embark and began to use it, I was a bit disappointed by its defaults, especially since Doom Emacs is normally great when it comes to ensuring good defaults. I eventually went ahead and looked through every aspect of Embark to see what needed to change.
Targets
Some of the targeting functions are a bit too general in what they accept. We'll adjust the expression and identifier targeters to only work in prog-mode
and the "defun" targeter to only work in Emacs Lisp code.
(defadvice! ~/embark-target-prog-mode (old-fn)
"Advise an embark target to only activate in `prog-mode'."
:around #'embark-target-expression-at-point
(when (derived-mode-p 'prog-mode)
(funcall old-fn)))
(defadvice! ~/embark-target-identifier (old-fn)
"Advise an embark target to only activate in `prog-mode' and not in `lsp-mode'."
:around #'embark-target-identifier-at-point
(when (and (derived-mode-p 'prog-mode)
(not (bound-and-true-p lsp-mode)))
(funcall old-fn)))
(after! embark
(embark-define-thingatpt-target defun emacs-lisp-mode))
We'll also define a word targeter, since that case was previously handled by the identifier one.
(defun embark-target-word-at-point ()
"Target word at point."
(when (or (derived-mode-p 'text-mode 'help-mode 'Info-mode 'man-common)
(doom-point-in-comment-p))
(when-let ((bounds (bounds-of-thing-at-point 'word)))
(cons 'word (cons (buffer-substring (car bounds) (cdr bounds)) bounds)))))
(after! embark
(pushnew! embark-target-finders #'embark-target-word-at-point))
LSP Integration
The provided action types related to programming only apply to Emacs Lisp code, so we'll add a new one that integrates with LSP.
(defun embark-target-lsp-symbol-at-point ()
"Target the LSP symbol at point."
(when (bound-and-true-p lsp-mode)
(require 'lsp-ui-doc)
;; Use hover request (meant for highlighting) to get the current symbol
(when-let ((bounds (lsp-ui-doc--extract-bounds
(lsp-request "textDocument/hover"
(lsp--text-document-position-params)))))
(cons 'lsp-symbol
(cons (buffer-substring (car bounds) (cdr bounds))
bounds)))))
(after! embark
(pushnew! embark-target-finders #'embark-target-lsp-symbol-at-point))
Hooks
The hook embark--mark-target
normally sets the mark to the end and puts the point at the beginning. This is the opposite of the usual order, so let's patch it to flip the order.
(defpatch! embark (cl-defun embark--mark-target) (&rest rest &key run bounds &allow-other-keys)
"Mark the target if its BOUNDS are known.
After marking the target, call RUN with the REST of its arguments."
(cond
((and bounds run)
(save-mark-and-excursion
(set-mark ((el-patch-swap cdr car) bounds))
(goto-char ((el-patch-swap car cdr) bounds))
(apply run :bounds bounds rest)))
(bounds
(set-mark ((el-patch-swap cdr car) bounds))
(goto-char ((el-patch-swap car cdr) bounds)))
(run (apply run rest))))
Actions
We'll be using a lot of new actions, so let's set their hooks.
(after! embark
(cl-pushnew #'embark--mark-target
(alist-get #'evil-change embark-around-action-hooks))
(cl-pushnew #'embark--mark-target
(alist-get #'+eval/region embark-around-action-hooks))
(cl-pushnew #'embark--mark-target
(alist-get #'+eval/region-and-replace embark-around-action-hooks))
(cl-pushnew #'embark--beginning-of-target
(alist-get #'backward-word embark-pre-action-hooks))
(cl-pushnew #'embark--end-of-target
(alist-get #'forward-word embark-pre-action-hooks))
(cl-pushnew #'embark--ignore-target
(alist-get #'lsp-rename embark-target-injection-hooks))
(cl-pushnew #'embark--ignore-target
(alist-get #'+spell/correct embark-target-injection-hooks))
(cl-pushnew #'embark--universal-argument
(alist-get #'+workspace/delete embark-pre-action-hooks))
(cl-pushnew #'embark--restart
(alist-get #'+workspace/delete embark-post-action-hooks))
(cl-pushnew #'embark--restart
(alist-get #'projectile-remove-known-project embark-post-action-hooks))
; Actions that retrigger Embark
(pushnew! embark-repeat-actions
#'lsp-ui-find-next-reference
#'lsp-ui-find-prev-reference
#'forward-word
#'backward-word
#'org-table-next-row
#'+org/table-previous-row
#'org-table-next-field
#'org-table-previous-field)
; Don't require confirmation on these actions
(setf (alist-get #'kill-buffer embark-pre-action-hooks nil t) nil
(alist-get #'embark-kill-buffer-and-window embark-pre-action-hooks nil t) nil
(alist-get #'bookmark-delete embark-pre-action-hooks nil t) nil
(alist-get #'tab-bar-close-tab-by-name embark-pre-action-hooks nil t) nil))
Keymaps
Here's the big one.
(defmacro ~/embark-target-wrapper (fn prompt)
"Wrap the command FN to take its argument interactively."
(let ((fsym (make-symbol (symbol-name fn))))
;;; Love me some uninterned symbols
`(progn
(defun ,fsym (ident &optional arg)
,(documentation fn)
(interactive (list (read-from-minibuffer ,prompt) current-prefix-arg))
(,fn ident arg))
#',fsym)))
(after! embark
(defvar-keymap embark-word-map
:doc "Keymap for Embark word actions."
:parent embark-general-map
"j" #'forward-word
"k" #'backward-word
"$" #'+spell/correct)
(defvar-keymap embark-lsp-symbol-map
:doc "Keymap for Embark LSP symbol actions."
:parent embark-identifier-map
"j" #'lsp-ui-find-next-reference
"k" #'lsp-ui-find-prev-reference
"r" #'lsp-rename)
(defvar-keymap embark-workspace-map
:doc "Keymap for Embark workspace actions."
:parent embark-general-map
"RET" #'+workspace/switch-to
"d" #'+workspace/delete)
(defvar-keymap embark-known-project-map
:doc "Keymap for Embark known project actions."
:parent embark-file-map
"RET" #'projectile-switch-project
"d" #'projectile-remove-known-project)
(pushnew! embark-keymap-alist
'(word . embark-word-map)
'(lsp-symbol . embark-lsp-symbol-map)
'(workspace . embark-workspace-map)
'(known-project . embark-known-project-map))
(map! (:map embark-general-map
"SPC" #'doom/leader
"C-SPC" #'embark-select
"X" #'embark-export
"W" #'+vertico/embark-export-write
"y" #'embark-copy-as-kill
"v" #'mark
"C-q" #'embark-toggle-quit
"d" #'kill-region
"c" #'evil-change
"/" #'evil-ex-search-forward
"?" #'evil-ex-search-backward
"E" nil "w" nil "q" nil "C-s" nil "C-r" nil)
(:map embark-heading-map
"v" #'mark
"V" #'outline-mark-subtree
"j" #'outline-next-visible-heading
"k" #'outline-previous-visible-heading
"J" #'outline-forward-same-level
"K" #'outline-backward-same-level
"h" #'outline-up-heading
"M-j" #'outline-move-subtree-down
"M-k" #'outline-move-subtree-up
"M-l" #'outline-demote
"M-h" #'outline-promote
"n" nil "p" nil "f" nil "b" nil "^" nil
"u" nil "C-SPC" nil)
(:map embark-prose-map
"c" #'evil-change
"u" #'downcase-region
"U" #'upcase-region
"q" #'fill-region
"C" #'capitalize-region
"l" nil "f" nil)
(:map embark-sentence-map
"j" #'forward-sentence
"k" #'backward-sentence
"n" nil "p" nil)
(:map embark-paragraph-map
"j" #'forward-paragraph
"k" #'backward-paragraph
"n" nil "p" nil)
(:map embark-identifier-map
"j" #'embark-next-symbol
"k" #'embark-previous-symbol
"d" #'kill-region
"RET" (~/embark-target-wrapper +lookup/definition "Identifier: ")
"K" (~/embark-target-wrapper +lookup/documentation "Identifier: ")
"D" (~/embark-target-wrapper +lookup/definition "Identifier: ")
"R" (~/embark-target-wrapper +lookup/references "Identifier: ")
"n" nil "p" nil "r" nil "a" nil "o" nil "H" nil "$" nil)
(:map embark-expression-map
"j" #'forward-list
"k" #'backward-list
"h" #'backward-up-list
"=" #'indent-region
"RET" #'+eval/region
"e" #'+eval/region
"E" #'+eval/region-and-replace
"TAB" nil "<" nil "u" nil "n" nil "p" nil)
(:map embark-defun-map
"c" #'evil-change
"C" #'compile-defun
"RET" nil "e" nil)
(:map embark-symbol-map
"s" nil "h" nil "d" nil "e" nil)
(:map embark-variable-map
"Y" #'embark-save-variable-value
"K" #'helpful-variable
"RET" #'+eval/region
"e" #'+eval/region
"E" #'+eval/region-and-replace
"i" #'embark-insert-variable-value
"v" #'mark
"c" #'evil-change
"<" nil)
(:map embark-function-map
"e" #'debug-on-entry
"E" #'cancel-debug-on-entry
"j" #'embark-next-symbol
"k" #'embark-previous-symbol
"K" #'helpful-callable)
(:map embark-command-map
"w" #'where-is
"b" nil "g" nil "l" nil)
(:map embark-package-map
"Y" #'embark-save-package-url
"i" #'embark-insert
"a" nil "I" nil "d" nil "r" nil "W" nil)
(:map embark-unicode-name-map
"Y" #'embark-save-unicode-character
"W" nil)
(:map embark-flymake-map
"j" #'flymake-goto-next-error
"k" #'flymake-goto-prev-error
"n" nil "p" nil)
(:map embark-tab-map
"d" #'tab-bar-close-tab-by-name)
(:map embark-region-map
"u" #'downcase-region
"U" #'upcase-region
"C" #'capitalize-region
"w" #'write-region
"W" #'count-words-region
"q" #'fill-region
"Q" #'fill-region-as-paragraph
"N" #'narrow-to-region
"D" #'delete-duplicate-lines
"=" #'indent-region
"g" #'vc-region-history
"d" #'kill-region
"c" #'evil-change
"TAB" nil "n" nil "l" nil "f" nil "p" nil
"*" nil ":" nil "_" nil)
(:map embark-file-map
"g" 'embark-vc-file-map
"w" #'embark-save-relative-path
"W" #'+vertico/embark-export-write
"Y" #'copy-file
"v" #'mark
"c" #'evil-change)
(:map embark-become-file+buffer-map
"." #'find-file
"b" #'+vertico/switch-workspace-buffer
"B" #'consult-buffer
"p" #'projectile--find-file)
(:map embark-become-help-map
"b" #'embark-bindings
"v" #'helpful-variable
"f" #'helpful-callable
"F" #'describe-face
"o" #'helpful-symbol
"s" #'helpful-symbol
"p" #'doom/help-packages)))
(after! embark-org
(map! (:map embark-org-table-cell-map
"RET" #'+org/dwim-at-point
"v" #'mark
"-" #'org-table-insert-hline
"l" #'org-table-next-field
"h" #'org-table-previous-field
"j" #'org-table-next-row
"k" #'+org/table-previous-row
"H" #'org-table-move-column-left
"L" #'org-table-move-column-right
"J" #'org-table-move-row-down
"K" #'org-table-move-row-up
(:prefix ("i" . "insert")
"h" #'+org/table-insert-column-left
"l" #'org-table-insert-column
"j" #'+org/table-insert-row-below
"k" #'org-table-insert-row
"-" #'org-table-insert-hline)
"^" nil "<" nil ">" nil "o" nil "O" nil)
(:map embark-org-table-map
"p" #'org-table-paste-rectangle
"C" #'org-table-convert
"D" #'org-table-toggle-formula-debugger
"y" #'embark-copy-as-kill
"d" #'kill-region
"c" #'evil-change)
(:map embark-org-link-copy-map
"y" #'embark-org-copy-link-in-full
"w" nil)
(:map embark-org-link-map
"e" #'org-insert-link
"y" 'embark-org-link-copy-map
"w" nil)
(:map embark-org-heading-map
">" #'org-do-demote
"<" #'org-do-promote
"j" #'org-next-visible-heading
"k" #'org-previous-visible-heading
"J" #'org-forward-heading-same-level
"K" #'org-backward-heading-same-level
"q" #'org-set-tags-command
"o" #'org-set-property
"D" #'org-cut-subtree
"s" #'org-sort
"S" #'embark-collect
"i" #'embark-insert
"d" #'kill-region
"I" #'org-insert-heading-respect-content
"l" #'org-store-link
"L" #'embark-live
(:prefix ("t" . "time")
"d" #'org-deadline
"s" #'org-schedule)
(:prefix ("c" . "clock")
"i" #'org-clock-in
"o" #'org-clock-out))
(:map embark-org-src-block-map
"v" #'org-babel-mark-block
"y" #'embark-org-copy-block-contents
"Y" #'embark-copy-as-kill
"D" #'org-babel-remove-result-one-or-many
"j" #'org-babel-next-src-block
"k" #'org-babel-previous-src-block
"e" #'org-edit-special
"=" #'org-indent-block
"c" #'evil-change)
(:map embark-org-inline-src-block-map
"e" #'org-edit-inline-src-code
"D" #'org-babel-remove-inline-result
"k" nil)
(:map embark-org-babel-call-map
"D" #'org-babel-remove-result
"k" nil)
(:map embark-org-item-map
"j" #'org-next-item
"k" #'org-previous-item
"M-j" #'org-move-item-down
"M-k" #'org-move-item-up
"c" #'evil-change
"n" nil "p" nil)
(:map embark-org-plain-list-map
"c" #'evil-change
"C" #'org-toggle-checkbox)
(:map embark-org-agenda-item-map
"RET" #'org-agenda-switch-to
"TAB" #'org-agenda-goto
"j" #'org-agenda-next-item
"k" #'org-agenda-previous-item
"d" #'org-agenda-kill
"q" #'org-agenda-set-tags
"o" #'org-agenda-set-property
(:prefix ("t" . "time")
"d" #'org-agenda-deadline
"s" #'org-agenda-schedule)
(:prefix ("c" . "clock")
"i" #'org-agenda-clock-in
"o" #'org-agenda-clock-out)
"u" nil "i" nil ":" nil "s" nil "P" nil)))
Evil
(after! evil
(setq evil-shift-width 2 ; 2 width tabs (again)
evil-want-fine-undo t ; More fine-grained undos
evil-ex-substitute-global t ; s/../../ is global by default
evil-kill-on-visual-paste nil ; Don't copy text overwritten on paste
))
While we're here, we'll also set my preferred evil-escape
keys:
(after! evil-escape
(setq evil-escape-key-sequence "fd"))
Flymake
I really like Flycheck's double-arrow fringe indicator, so let's quickly steal that:
(after! flymake
(define-fringe-bitmap 'flymake-double-left-arrow
[#b00011011
#b00110110
#b01101100
#b11011000
#b01101100
#b00110110
#b00011011])
(setf (car flymake-error-bitmap) 'flymake-double-left-arrow
(car flymake-warning-bitmap) 'flymake-double-left-arrow
(car flymake-note-bitmap) 'flymake-double-left-arrow))
Flymake normally uses italics for warnings, but my italics font being cursive makes that a bit too visually noisy.
(custom-set-faces!
'(compilation-warning :slant normal :weight bold)
'(flymake-note-echo :underline nil :inherit compilation-info))
And just to make sure nothing else accidentally starts running:
(package! flycheck :disable t)
(package! flyspell :disable t)
Bindings
(map! :leader
:desc "Open errors buffer"
"c X" #'flymake-show-project-diagnostics)
Tooltips
Having an IDE-style tooltip pop up is nice, but flymake-popon
is a bit ugly by default.
(after! flymake-popon
; Widen popon
(setq flymake-popon-width 120)
; Add visible border
(set-face-foreground 'flymake-popon-posframe-border (doom-color 'selection)))
Popups
(set-popup-rule! "^\\*Flymake" :vslot 1 :side 'bottom)
Git
I use GPG signing for commits, which means that committing often takes longer than the single second timeout. Eight seconds seems like a reasonable amount of time to type in a password.
Let's also increase the maximum length of commit summaries past the default of 50 characters, because that's a somewhat restrictive limit.
(after! git-commit
(setq git-commit-post-finish-hook-timeout 8
git-commit-summary-max-length 60))
Magit Syntax Highlighting
Magit already looks great, but it could use some proper syntax highlighting!
(package! magit-delta)
(use-package! magit-delta
:hook (magit-mode . magit-delta-mode))
Forge
Forge is a convenient package for working with remote code forges like GitHub, GitLab, etc. These days, I've mostly switched over to my own personal forge instead of something public like GitHub, so I'll need to let Forge know about that.
(after! forge
(push '("git.tokinanpa.dev"
"git.tokinanpa.dev/api/v1"
"git.tokinanpa.dev"
forge-gitea-repository)
forge-alist))
Marginalia
Marginalia mostly works fine on its own, but we should add a few more Doom-specific prompt categories to its registry.
(after! marginalia
;; Workspace and project categories
(pushnew! marginalia-prompt-categories
'("\\<workspace\\>" . workspace)
'("\\<projects?\\>" . known-project))
;; Annotate equivalently to files
(pushnew! marginalia-annotator-registry
'(known-project marginalia-annotate-file builtin none))
;; Remove special case for projectile-switch-project
;; (now covered by known-project category)
(setf (alist-get #'projectile-switch-project marginalia-command-categories nil t) nil))
These new categories can then be used to define Embark keymaps for minibuffer completion.
Operation Hints
I like having ophints for vim editing so that I don't get lost when making large edits, but the ophints
module in Doom doesn't look very good to me (it gets rid of pulses and color), so I'll override it.
;; -*- no-byte-compile: t; -*-
;;; ui/ophints/packages.el
(package! evil-goggles)
;;; -*- lexical-binding: t; -*-
;;; ui/ophints/config.el
(use-package! evil-goggles
:hook (doom-first-input . evil-goggles-mode)
:init
(setq evil-goggles-duration 0.15
evil-goggles-blocking-duration 0.12
evil-goggles-async-duration 0.2)
:config
(pushnew! evil-goggles--commands
'(evil-magit-yank-whole-line
:face evil-goggles-yank-face
:switch evil-goggles-enable-yank
:advice evil-goggles--generic-async-advice)
'(+evil:yank-unindented
:face evil-goggles-yank-face
:switch evil-goggles-enable-yank
:advice evil-goggles--generic-async-advice)
'(+eval:region
:face evil-goggles-yank-face
:switch evil-goggles-enable-yank
:advice evil-goggles--generic-async-advice)
'(evil-fill
:face evil-goggles-fill-and-move-face
:switch evil-goggles-enable-fill-and-move
:advice evil-goggles--generic-async-advice)
'(evil-fill-and-move
:face evil-goggles-fill-and-move-face
:switch evil-goggles-enable-fill-and-move
:advice evil-goggles--generic-async-advice))
(custom-set-faces! '(evil-goggles-default-face :background "#2b3a7f")
'(evil-goggles-delete-face :inherit magit-diff-removed-highlight)
'(evil-goggles-paste-face :inherit magit-diff-added-highlight)
'(evil-goggles-change-face :inherit evil-goggles-delete-face)))
Snippets
Snippets are a sophisticated method of warding off the scourge of unnecessary keystrokes. They were a bit hard to get used to, but I've warmed up to them over time.
The snippets themselves are stored in separate files, each of which can be found in the snippets/
subdirectory of this repo. As the yasnippet
package expects, they are grouped by major mode.
Tweaks
Allow nested snippets:
(after! yasnippet
(setq yas-triggers-in-field t))
Editing Snippets
Snippets are edited by visiting their file in snippet-mode
, which defines some useful keybindings for working with the snippet.
Trailing Newlines
If there are any trailing newlines in the snippet file, they will be inserted when the snippet is expanded. This may not always be desirable, so we should prevent Emacs from automatically inserting trailing newlines in these buffers.
(add-hook! snippet-mode
(setq-local require-final-newline nil))
Testing
When editing a snippet, the binding C-c C-t
can be used to test it in a fresh buffer. This is very useful, but with Evil it has the issue of leaving the editor in normal state, when snippets are designed to be expanded in insert state.
(defadvice! ~/yas-tryout-insert-state (&rest _)
"Switch to Insert state when trying out a snippet."
:after #'yas-tryout-snippet
(evil-insert-state))
Creating New Snippets
Doom's command to create a new snippet, +snippets/new
, defines a template inside of itself purely for when creating a snippet through this command. This doesn't make much sense to me when file templates already exist as a standard system in Doom, and snippets are stored inside files!
(defpatch! nil
(defun +snippets/new) (&optional all-modes)
"Create a new snippet in `+snippets-dir'.
If there are extra yasnippet modes active, or if ALL-MODES is non-nil, you will
be prompted for the mode for which to create the snippet."
(interactive "P")
(let* ((mode (+snippets--snippet-mode-name-completing-read all-modes))
(default-directory (+snippet--ensure-dir (expand-file-name mode +snippets-dir)))
(snippet-key (read-string "Enter a key for the snippet: "))
(snippet-file-name (expand-file-name snippet-key)))
(when (+snippets--use-snippet-file-name-p snippet-file-name)
(el-patch-swap
(with-current-buffer (switch-to-buffer snippet-key)
(snippet-mode)
(erase-buffer)
(set-visited-file-name snippet-file-name)
(yas-expand-snippet (concat "# -*- mode: snippet -*-\n"
"# name: $1\n"
"# uuid: $2\n"
"# key: ${3:" snippet-key "}${4:\n"
"# condition: t}\n"
"# --\n"
"$0"))
(when (bound-and-true-p evil-local-mode)
(evil-insert-state)))
(find-file snippet-file-name)))))
File Templates
Doom's file-templates
module extends yasnippet
to provide a nice file template system. The idea is simple: the variable +file-templates-alist
maps file predicates to snippets. If a file that matches a predicate is created, the corresponding snippet is automatically expanded inside of it.
This system works well for the most part, but there's a serious issue with its UI: the function that registers file templates, set-file-templates!
, takes a plist to configure the template. If this list is empty, an existing template is removed instead. This is not only unintuitive, but it prevents you from having an empty property list, which is often necessary! We'll patch the function to remove this issue with a :remove
key, as well as to have templates appended to the alist instead of prepended to make the order of templates more clear.
(defpatch! nil
(defun +file-templates--set) (pred plist)
(if (el-patch-swap (null (car-safe plist))
(plist-member plist :remove))
(setq +file-templates-alist
(el-patch-swap (delq (assoc pred +file-templates-alist) +file-templates-alist)
(assoc-delete-all pred +file-templates-alist)))
(el-patch-swap
(push `(,pred ,@plist) +file-templates-alist)
(setq +file-templates-alist
(nconc +file-templates-alist `((,pred ,@plist)))))))
Now that we have our new-and-improved template registry system, we can add new file templates as we please.
Nix
The default Nix file template is for a NixOS module. This is nice when you're writing a module, but most Nix files aren't modules. We'll restrict the default template to only trigger for files inside /etc/nixos
, and for there to be no template in other cases. We'll also add a template for flakes.
(set-file-templates!
'(nix-mode :remove t)
'("/etc/nixos/.*\\.nix$" :mode nix-mode)
'("/flake\\.nix$" :trigger "__flake.nix" :mode nix-mode))
Spell Checking
Doom Emacs sets up spell-checking with ispell
(Emacs-internal tool) and aspell
(external tool) for us, which is nice. We just need to set which dictionary to use:
(after! ispell
(setq ispell-dictionary "en_US"))
We also need to generate a plain-text dictionary for some ispell
functionality, which is annoying, but I haven't figured out a way around it. I could use my automated nix-build system for this, but in this case it's easier to just put it in the usual location.
Now that we have our new-and-improved template registry system, we can add new file templates as we please.
(defvar ~/plaintext-dict (expand-file-name "ispell/dict.plain" doom-data-dir)
"File location of a plaintext wordlist for spellchecking.")
(unless (file-readable-p ~/plaintext-dict)
(shell-command-to-string
(concat
"aspell -l en_US dump master > " ~/plaintext-dict ";"
"aspell -d en-computers.rws dump master >> " ~/plaintext-dict ";"
"aspell -d en_US-science.rws dump master >> " ~/plaintext-dict ";")))
(after! ispell
(setq ispell-alternate-dictionary ~/plaintext-dict))
Now that we have this word list, we can also plug it into cape-dict
and get proper spelling completion!
(after! cape
(setq cape-dict-file ~/plaintext-dict))
(add-hook! text-mode
(add-hook! 'completion-at-point-functions :local :depth 40 #'cape-dict))
Undo Tree
(after! undo-tree
(setq undo-tree-visualizer-diff nil))
(set-popup-rule! "^ \\*undo-tree\\*" :slot 2 :side 'right :size 30 :select t :quit nil)
VTerm
I've set my default Emacs shell to bash
, since pointing Emacs to a non-POSIX shell like fish
(my usual default) can cause incompatibility issues. I still want to use fish
for my own purposes, though, so we'll set it as the shell in vterm
:
(after! vterm
(setq-default vterm-shell (executable-find "fish")))
Window Numbering
Emacs comes with a few standard commands for selecting different windows. These are mostly directional, allowing you to move your selection up, right, left or down from the current window. This is rather inconvenient when working with large amounts of windows, so the winum
package allows you to assign a number to each window to make navigation easier.
There's just one problem with winum
for my purposes: Doom Emacs's popup management system. In Doom Emacs, popups are treated as separate from "real" windows, usually displayed on the edge of the frame and without a modeline. winum
has no knowledge of popup windows, and assigns them numbers exactly the same as any other window, which is very confusing when popups are otherwise treated as separate.
To solve this issue, I've written my own fork of winum
:
(package! winum :recipe (:local-repo "pkgs/winum"))
This fork generalizes the window numbering system to support indexing windows with any Lisp object, instead of just integers. We can then index regular windows with integers N
, and popup windows with cons cells (popup . N)
, allowing us to number them separately.
Configuration
(use-package! winum
:after-call doom-switch-window-hook
:config
(setq winum-auto-setup-mode-line nil)
(winum-mode +1)
(map! :map evil-window-map
"0" #'winum-select-window-0-or-10
"1" #'winum-select-window-1
"2" #'winum-select-window-2
"3" #'winum-select-window-3
"4" #'winum-select-window-4
"5" #'winum-select-window-5
"6" #'winum-select-window-6
"7" #'winum-select-window-7
"8" #'winum-select-window-8
"9" #'winum-select-window-9)
(map! :leader
:desc "Select window 1" "1" #'winum-select-window-1
:desc "Select window 2" "2" #'winum-select-window-2
:desc "Select window 3" "3" #'winum-select-window-3
:desc "Select window 4" "4" #'winum-select-window-4
:desc "Select window 5" "5" #'winum-select-window-5
:desc "Select window 6" "6" #'winum-select-window-6
:desc "Select window 7" "7" #'winum-select-window-7
:desc "Select window 8" "8" #'winum-select-window-8
:desc "Select window 9" "9" #'winum-select-window-9))
Popup Management
With the basic config out of the way, we can implement popup-aware numbering by changing the winum-auto-assign-function
.
(after! winum
(defun ~/winum-auto-assign (windows)
"Assign indices to windows, handling popup windows separately."
(let ((index 1) (popup-index '(popup . 1)))
(dolist (w windows)
(if (+popup-window-p w)
(winum--assign-unique
w popup-index t
(cons 'popup-index (1+ (cdr popup-index))))
(winum--assign-unique w index t)))))
(setq winum-auto-assign-function #'~/winum-auto-assign))
For convenience, we can add functions to select a particular popup window:
(map! :leader
"~" nil
:prefix ("~" . "popup")
:desc "Toggle last popup"
"~" #'+popup/toggle)
(dolist (n (number-sequence 1 9))
(let ((function (intern (format "+winum-select-popup-%s" n))))
(eval
`(progn
(defun ,function (&optional arg)
,(format "Jump to popup window %s.
If prefix ARG is given, delete the window instead of selecting it." n)
(interactive "P")
(if arg
(winum-delete-window-by-index '(popup . ,n))
(winum-select-window-by-index '(popup . ,n))))
(map! :leader
:desc ,(format "Select popup %s" n)
,(format "M-%s" n) #',function
:desc ,(format "Select popup %s" n)
,(format "~ %s" n) #',function)))))
(defun +winum-select-popup-0-or-10 (&optional arg)
"Jump to popup window 0, or popup 10 if 0 is not assigned.
If prefix ARG is given, delete the window instead of selecting it."
(interactive "P")
(let ((n (if (winum-get-window-by-index '(popup . 0))
'(popup . 0) '(popup . 10))))
(if arg
(winum-delete-window-by-index n)
(winum-select-window-by-index n))))
(map! :leader
:desc "Select popup 0 or 10"
"M-0" #'+winum-select-popup-0-or-10
:desc "Select popup 0 or 10"
"~ 0" #'+winum-select-popup-0-or-10)
Applications
Calculator
Emacs Calc is the best calculator I've ever used, and given the fact that it's an RPN calculator, that's saying something.
Leader Key Bindings
Typing C-x *
every time I want to use Calc (very often) is annoying.
(map! :leader
:prefix ("#" . "calc")
:desc "Emacs Calc"
"#" #'calc
:desc "Emacs Calc"
"c" #'calc
:desc "Emacs Calc (full window)"
"C" #'full-calc
:desc "Quick Calc"
"q" #'quick-calc
:desc "Keypad"
"k" #'calc-keypad
:desc "Grab region into Calc"
"g" #'~/calc-grab-region
:desc "Paste from stack"
"y" #'calc-copy-to-buffer
:desc "Read keyboard macro"
"m" #'read-kbd-macro
(:prefix ("e" . "embedded")
:desc "Embedded mode"
"e" #'calc-embedded
:desc "Embedded mode (select)"
"s" #'calc-embedded-select
:desc "Embedded mode (word)"
"w" #'calc-embedded-word
:desc "Activate special operators"
"a" #'calc-embedded-activate
:desc "Duplicate formula at point"
"d" #'calc-embedded-duplicate
:desc "New formula"
"f" #'calc-embedded-new-formula
:desc "Next formula"
"j" #'calc-embedded-next
:desc "Previous formula"
"k" #'calc-embedded-previous
:desc "Refresh formula at point"
"r" #'calc-embedded-update-formula
:desc "Edit formula at point"
"`" #'calc-embedded-edit))
For the grab-region command, I think it makes sense to have it check whether your selection is a rectangle (C-v
):
(defun ~/calc-grab-region (top bot &optional arg)
"Perform either `calc-grab-region' or `calc-grab-rectangle' depending on
what type of visual state is currently active."
(interactive "r\nP")
(if (eq (evil-visual-type) 'block)
(calc-grab-rectangle top bot arg)
(calc-grab-region top bot arg)))
Evil Bindings
I want to have vim keybindings in Calc, so let's enable the evil-collection
module for it by removing it from +evil-collection-disabled-list
.
;; Enable evil-collection-calc
(delq 'calc +evil-collection-disabled-list)
Let's also rebind some keys. We'll set [
and ]
to directly begin and end vectors like it did originally, and C-r
makes more sense as a redo binding than D D
.
(defadvice! ~/evil-collection-calc-bindings ()
:after #'evil-collection-calc-setup
(map! :map calc-mode-map
:n "C-r" #'calc-redo
:n "[" #'calc-begin-vector
:n "]" #'calc-end-vector))
Appearance
Calc doesn't use faces to show selections by default, which I think is rather strange.
(after! calc
(setq calc-highlight-selections-with-faces t
calc-show-selections nil)
(custom-set-faces!
`(calc-selected-face :weight extra-bold :foreground ,(doom-color 'highlight))
`(calc-nonselected-face :weight semi-light :foreground ,(doom-color 'comments))))
Other Defaults
(after! calc
(setq calc-window-height 13 ; Make window taller
calc-angle-mode 'rad ; Default to radians
calc-symbolic-mode t ; Symbolic evaluation
calc-kill-line-numbering nil ; Don't copy stack position numbers
))
Calendar
The calendar's main purpose for me is to give a better view of the Org agenda.
(after! calendar
;; Start week on Monday
(setq calendar-week-start-day 1)
;; ISO date style
(calendar-set-date-style 'iso))
(after! calfw
(setq cfw:org-face-agenda-item-foreground-color (doom-color 'yellow)))
(map! :leader
:desc "Calendar"
"o c" #'cfw:open-org-calendar)
Emacs Everywhere
Emacs Everywhere is a great idea. Unfortunately, the default package on MELPA uses X-based window commands, while I use Hyprland, which is Wayland-based. To fix this issue, we need to override some of the package's variables and functions.
(after! emacs-everywhere
;; Shell commands for interacting with window system
(setq emacs-everywhere-paste-command
'("wtype" "-M" "shift" "-P" "Insert")
emacs-everywhere-copy-command
'("sh" "-c" "wl-copy < %f")
emacs-everywhere-window-focus-command
'("hyprctl" "dispatch" "focuswindow" "address:%w")))
;; Function for accessing current window
(defun emacs-everywhere--app-info-linux-hyprland ()
"Return information on the active window, on Hyprland."
(pcase-let*
((`(,window-id ,window-class ,window-title . ,window-dims-)
(split-string (shell-command-to-string
"hyprctl activewindow -j | jaq -r \
'.address, .class, .title, .at[], .size[]'")
"\n"))
(window-dims (mapcar #'string-to-number (butlast window-dims-))))
(make-emacs-everywhere-app
:id window-id
:class window-class
:title window-title
:geometry window-dims)))
(defpatch! emacs-everywhere
(defun emacs-everywhere--app-info-linux) ()
"Return information on the active window, on Linux."
(pcase emacs-everywhere--display-server
(`(x11 . ,_) (emacs-everywhere--app-info-linux-x11))
(`(wayland . KDE) (emacs-everywhere--app-info-linux-kde))
(el-patch-add
(`(wayland . Hyprland) (emacs-everywhere--app-info-linux-hyprland)))
(_ (user-error "Unable to fetch app info with display server %S"
emacs-everywhere--display-server))))
I use isync
, msmtp
and mu
as Doom Emacs recommends.
(after! mu4e
(setq sendmail-program (executable-find "msmtp")
send-mail-function #'smtpmail-send-it
message-sendmail-f-is-evil t
message-sendmail-extra-arguments '("--read-envelope-from")
message-send-mail-function #'message-send-mail-with-sendmail))
Accounts
(set-email-account! "gmail"
'((mu4e-sent-folder . "/gmail/[Gmail]/Sent Mail")
(mu4e-drafts-folder . "/gmail/[Gmail]/Drafts")
(mu4e-trash-folder . "/gmail/[Gmail]/Trash")
(mu4e-refile-folder . "/gmail/[Gmail]/All Mail")
(smtpmail-smtp-user . "kiana.a.sheibani@gmail.com"))
t)
Password Management
I use the standard Unix-style password management system, pass.
(map! :leader
:desc "Password Store"
"o s" #'pass)
(after! pass
(setq pass-show-keybindings nil ; Keybindings take up too much space
pass-suppress-confirmations t ; Quit shouldn't need a confirm step
))
;; Move to right side
(after! pass
(set-popup-rule! "^\\*Password-Store" :side 'right :size 0.25 :quit nil))
Tweaks
For some unknown reason, the creators of the original pass
package decided that when showing the pass buffer, the main dispatch function would call pop-to-buffer
when it needs to be created, but switch-to-buffer
when it already exists. These are different functions! Let's fix that.
(defpatch! pass
(defun pass) ()
"Open the password-store buffer."
(interactive)
((el-patch-swap if if-let)
(el-patch-swap (get-buffer pass-buffer-name)
((window (get-buffer-window pass-buffer-name))))
(progn
(el-patch-swap (switch-to-buffer pass-buffer-name)
(select-window window))
(pass-update-buffer))
(el-patch-add (pop-to-buffer pass-buffer-name))
(el-patch-splice 3
(let ((buf (get-buffer-create pass-buffer-name)))
(pop-to-buffer buf)
(pass-setup-buffer)))))
When visiting a password file, the file's buffer replaces the pass buffer, which isn't very good UX. To fix this, we can patch it to use pop-to-buffer
, folding it into the popup system.
(defpatch! pass
(defun pass-view) ()
"Visit the entry at point."
(interactive)
(pass--with-closest-entry entry
(el-patch-let (($file (concat (f-join (password-store-dir) entry) ".gpg")))
(el-patch-swap
(find-file $file)
(pop-to-buffer
(find-file-noselect $file)
(cdr (+popup-make-rule nil '(:side bottom :size 8 :quit t :modeline t))))))))
Org
I love org-mode
. In fact, I love it so much that I'm willing to give it its own top-level section in this config! Its power and flexibility are unmatched by any other productivity/organization tool I've ever used. Much like Emacs itself, all alternatives simply vanish.
Unfortunately, with that power comes a lot of configuration work up-front. It was completely worth it for me when I made it out the other end, but that doesn't mean everyone would have the time or patience to make it work.
Basic Configuration
;; Org Directory - location of main org repo
(setq org-directory "~/org/")
(after! org
(setq org-cycle-emulate-tab nil ; We don't need this with evil
org-attach-dir-relative t
org-log-into-drawer t ; Write logs into :LOGBOOK:
org-footnote-auto-label 'confirm ; Allow editing of footnote names
;; Startup
org-startup-with-inline-images t ; Do more stuff on startup
org-startup-with-latex-preview t
+org-startup-with-animated-gifs t
;; Customize appearance
org-indent-indentation-per-level 0 ; No heading indentation
org-cycle-separator-lines 1 ; Keep 1-line padding between folded headings
org-hide-emphasis-markers t ; Hide *emphasis*
org-image-actual-width '(550) ; Default images to 550px
org-format-latex-options ; Make latex preview smaller
(plist-put org-format-latex-options :scale 0.55)
;; Todo Keywords
org-todo-keywords
'((sequence
"TODO(t)" "PROJ(p)"
"NEXT(n)"
"STRT(s!)"
"WAIT(w@/!)" "HOLD(h@/!)"
"|"
"DONE(d!/@)" "PART(r@/@)"
"KILL(k@/@)"))
org-todo-keyword-faces
'(("PROJ" . +org-todo-project)
("STRT" . +org-todo-active)
("WAIT" . +org-todo-onhold)
("HOLD" . +org-todo-onhold)
("KILL" . +org-todo-cancel))
))
Tags
Org mode offers a useful tag hierarchy system, configured via org-tag-alist
. We'll be using org-tag-persistent-alist
instead so that our tag hierarchy can't be overwritten.
(defvar subject-alist '()
"An alist where the car is the name of a class subject (typically
four capitalized letters), and the cdr is a list of sub-tags.")
(defvar classes-mwf '()
"Classes that belong under the :MWF: tag.")
(defvar classes-tr '()
"Classes that belong under the :TR: tag.")
(defvar classes-online '()
"Classes that belong under the :Online: tag.")
(after! org
(setq classes-mwf '(("PHIL2010" . ?1) ("CSE1322" . ?2))
classes-tr '(("MATH2345" . ?3) ("CSE1322L" . ?4))
classes-online '()
subject-alist '(("MATH" ("calculus") ("algebra"))
("HIST") ("POLS") ("ECON") ("PHIL")))
(setq org-tag-persistent-alist
`(("area" . ?A) ("goal" . ?G) ("project" . ?P) ("meta" . ?M)
(:startgrouptag) ("college")
(:grouptags) ("assign") ("notes") (:endgrouptag)
,@(-mapcat
(lambda (subject)
`((:startgrouptag) (,(car subject))
(:grouptags)
(,(format "{%s[[:digit:]]+L?}" (car subject)))
,@(cdr subject)
(:endgrouptag)))
subject-alist)
(:startgroup) ("college")
(:grouptags) ("TR") ("MWF") ("Online") (:endgroup)
(:startgroup) ("MWF")
(:grouptags) ,@classes-mwf (:endgroup)
(:startgroup) ("TR")
(:grouptags) ,@classes-tr (:endgroup)
(:startgroup) ("Online")
(:grouptags) ,@classes-online (:endgroup))))
Bindings
Convenience
There are a few useful functions Doom doesn't bind by default, so let's add them for convenience.
(map! :after org
:map org-mode-map
:localleader
"n" #'org-num-mode
"C" #'org-columns
"D" #'org-insert-drawer
"g j" #'org-goto
"c D" #'org-clock-display
"m b f" #'org-table-eval-formula
"m b F" #'org-table-edit-formulas
;; Remove extraneous commands
"a" #'org-attach
"p" #'org-priority
;; Map babel commands into localleader
:desc "babel"
"v" (lookup-key org-mode-map (kbd "C-c C-v")))
YASnippet
By default, snippet expansion doesn't work in Org mode; when TAB
is pressed to move to the next placeholder, headline visibility cycling is triggered instead. This is because in org-mode-map
TAB
is unconditionally bound to org-cycle
, and for some reason this has a higher precedence than YAS's keymaps.
While this is a complex problem, the solution is actually rather simple: just remove the org-mode-map
binding. The org-cycle
command will still be triggered on TAB
when in normal mode, as it is bound in evil-org-mode-map
, but when in insert mode (as one generally is during snippet expansion) the binding will fall through both maps and be handled by YASnippet.
(map! :after org
:map org-mode-map
"TAB" nil
"<tab>" nil
:i "TAB" nil
:i "<tab>" nil)
This also means we don't need org-cycle
to emulate indentation, which is nice. Unfortunately, this breaks something else: org-cycle
not only handles visibility cycling, but also navigating inside tables. If org-cycle
isn't being called when in insert mode, then we can't use TAB
to move fields. This means that we need a little extra binding configuration:
(let ((item
`(menu-item nil org-table-next-field
:filter ,(lambda (cmd)
(when (org-table-p)
cmd)))))
(map! :after org
:map evil-org-mode-map
:i "TAB" item
:i "<tab>" item))
This makes it so that we only let tab keypresses fall through if we aren't in a table. (We'll just assume that we aren't going to be expanding any snippets while we're in a table.)
Project Links
It's sometimes nice to be able to click a link in an Org file that takes me to one of my projects.
(defun org-projectile-follow (path _)
"Open a projectile link to PATH."
(projectile-switch-project-by-name path))
(defun org-projectile-completion (&optional arg)
(let ((project (completing-read "Project: " projectile-known-projects nil 'confirm)))
(concat "projectile:" project)))
(after! org
(org-link-set-parameters "projectile"
:follow #'org-projectile-follow
:complete #'org-projectile-completion))
Appearance
Entities
(after! org
(setq org-pretty-entities t
org-entities-user
'(("top" "\\top" t "⊤" "" "22A4" "⊤")
("bot" "\\bot" t "⊥" "" "22A5" "⊥")
("bbN" "\\mathbb{N}" t "" "" "2115" "ℕ")
("bbZ" "\\mathbb{Z}" t "" "" "2124" "ℤ")
("bbQ" "\\mathbb{Q}" t "" "" "211A" "ℚ")
("bbR" "\\mathbb{R}" t "" "" "211D" "ℝ")
("qed" "\\square" t "" "" "2B1C" "⬜")
("sqrt" "\\sqrt" t "" "" "221A" "√"))))
Modern
Doom Emacs's +pretty
flag by default uses the package org-superstar
to prettify Org files. This package is decently nice looking, but it has a much nicer and more comprehensive alternative in the form of org-modern
.
(package! org-modern)
(use-package! org-modern
:hook (org-mode . org-modern-mode)
:config
(setq org-modern-star 'replace
org-modern-replace-stars '("◉" "○" "✸" "✿" "✤" "✜" "◆" "▶")
org-modern-label-border 0.3
org-modern-table-vertical 1
org-modern-table-horizontal 0.2
org-modern-list '((?- . "•")
(?+ . "•")
(?* . "•"))
org-modern-todo nil
org-modern-todo-faces
'(("TODO" :inverse-video t :inherit org-todo)
("PROJ" :inverse-video t :inherit +org-todo-project)
("STRT" :inverse-video t :inherit +org-todo-active)
("WAIT" :inverse-video t :inherit +org-todo-onhold)
("HOLD" :inverse-video t :inherit +org-todo-onhold)
("KILL" :inverse-video t :inherit +org-todo-cancel))
org-modern-block-fringe nil
org-modern-block-name
'(("src" "»" "«")
("example" "»–" "–«")
("quote" "❝" "❞")
("export" "⏩" "⏪")
(t . t))
org-modern-priority nil
org-modern-horizontal-rule (make-string 36 ?─)
org-modern-keyword
'(("title" . "")
("subtitle" . "")
("author" . "")
("email" . "")
("date" . "")
("property" . "")
("startup" . "")
("filetags" . "")
("bind" . "")
("bibliography" . "")
("print_bibliography" . #("" 0 1 (display (raise -0.1))))
("cite_export" . "")
("print_glossary" . #("ᴬᶻ" 0 1 (display (raise -0.1))))
("glossary_sources" . #("" 0 1 (display (raise -0.14))))
("include" . "⇤")
("setupfile" . "⇚")
("html_head" . "🅷")
("html" . "🅗")
("latex_class" . "🄻")
("latex_class_options" . #("🄻" 1 2 (display (raise -0.14))))
("latex_header" . "🅻")
("latex_header_extra" . "🅻⁺")
("latex" . "🅛")
("beamer_theme" . "🄱")
("beamer_color_theme" . #("🄱" 1 2 (display (raise -0.12))))
("beamer_font_theme" . "🄱𝐀")
("beamer_header" . "🅱")
("beamer" . "🅑")
("attr_latex" . "🄛")
("attr_html" . "🄗")
("attr_org" . "⒪")
("call" . "")
("name" . "")
("header" . "›")
("caption" . "")
("results" . "› result")
(t . "› "))
org-modern-checkbox
'((88 . "")
(45 . #("" 0 2 (composition ((2)))))
(32 . ""))))
The default colors for various elements of org-modern
don't match with our theme colors, so we need to modify them ourselves. We'll also modify a few aspects of label appearance here.
(after! org-modern
(set-face-attribute 'org-modern-label nil :height 0.85)
(custom-set-faces!
'(org-checkbox :weight normal :inherit org-todo)
`(org-modern-tag
:foreground ,(doom-color 'fg-alt)
:background ,(doom-color 'base0)
:inherit (secondary-selection org-modern-label))
`(org-modern-horizontal-rule
:strike-through ,(doom-color 'grey) :inherit org-hide)
`(org-modern-done
:height 1.1
:foreground ,(doom-color 'fg-alt)
:background ,(doom-color 'modeline-bg)
:inherit org-modern-label)
`(org-modern-date-active
:foreground ,(doom-color 'brown)
:inherit org-modern-done)
`(org-modern-date-inactive
:foreground ,(doom-color 'doc-comments)
:inherit org-modern-date-active)
`(org-modern-time-active
:foreground ,(doom-color 'fg-alt)
:background ,(doom-color 'base0)
:inherit org-modern-done)
`(org-modern-time-inactive
:foreground ,(doom-color 'grey)
:background ,(doom-color 'modeline-bg-l)
:inherit org-modern-time-active)
`(org-modern-progress-incomplete
:background ,(doom-color 'bg-alt)
:foreground ,(doom-color 'fg-alt))
`(org-modern-progress-complete
:background ,(doom-color 'base5)
:foreground ,(doom-color 'bg-alt))))
Appear
Since we've disabled +pretty
, we need to add the packages we do want from it, namely org-appear
to automatically handle emphasis markers and other nice things like that.
(package! org-appear)
(use-package! org-appear
:hook (org-mode . org-appear-mode)
:config
(setq org-appear-autoentities t
org-appear-autokeywords t
org-appear-autosubmarkers t
org-appear-autolinks 'just-brackets
org-appear-inside-latex t))
(after! org
(setq org-highlight-latex-and-related '(native script entities)))
Emphasis
When marking text for *emphasis*
, Org mode normally only allows emphasized sections to span 2 lines. This strikes me as needlessly limited, so let's bump up that number to 20 lines.
(after! org
(setf (nth 4 org-emphasis-regexp-components) 20))
Popups
Org uses many popup windows for various things, and we can use Doom to make them look a little nicer.
Enhancements
While Org-mode provides a very comprehensive set of tools and systems, there are a few small things that are missing or that would make the overall UX smoother. Luckily, Org-mode being implemented as an Emacs package gives us more than enough control over its function to crowbar in a few new features!
Archiving
Archive Restore
Org offers a wonderfully useful trash system called archiving, which lets you move subtrees into a file where they can be saved without permanently deleting them. However, there's no system for automatically restoring these subtrees once they're archived!
Thankfully, Org already stores context data about archived subtrees, so we can use that. First, let's create a function for restoring an archived subtree at point.
(defun org-restore-from-archive ()
"Restore an archived subtree to its original file and location.
This function works on any subtree with the ARCHIVE_FILE and optionally
the ARCHIVE_OLPATH and ARCHIVE_TODO properties, returning the subtree
to the specified state. It will also remove any other ARCHIVE_* properties.
WARNING: If this subtree had an attachment that was deleted during archive,
the attachment cannot be restored!"
(interactive)
(let* ((archived-p
(lambda (element)
(and (eq (org-element-type element) 'headline)
(org-element-property :ARCHIVE_FILE element))))
(lineage (org-element-lineage (org-element-at-point) nil t))
(heading (or (-first archived-p lineage)
(user-error "Archived heading could not be found")))
(title (org-element-property :title heading))
(file (org-element-property :ARCHIVE_FILE heading))
(olp (-some--> (org-element-property :ARCHIVE_OLPATH heading)
(s-split "/" it t)))
(todo (org-element-property :ARCHIVE_TODO heading))
(target (condition-case nil
(org-find-olp (cons file olp))
((t (error "Invalid outline path %S" olp))))))
(goto-char (org-element-begin heading))
;; Restore todo state
(when todo (org-todo todo))
;; Delete properties (code hacked from `org-entry-delete')
(save-excursion
(pcase (org-get-property-block)
(`(,begin . ,origin)
(let ((end (copy-marker origin))
(re (org-re-property "ARCHIVE_[^:]+" t t)))
(goto-char begin)
(while (re-search-forward re end t)
(delete-region (match-beginning 0)
(line-beginning-position 2)))
(when (= begin end)
(delete-region (line-beginning-position 0)
(line-beginning-position 2)))))))
;; Move back to original location
(org-refile nil nil (list "" file nil target))
(org-refile-goto-last-stored)
(message "Restored \"%s\" to file %s" title file)))
File Archive
Currently, there only exists capabilities to archive a subtree, not an entire file. This matters for me because I often want to remove Org Roam files so that they don't clutter up the database and bog down my agenda. As a quick hacky way to implement this, we can just rename the file to make it look like an archive file.
(defun org-archive-file (file)
"Archive the entire Org file FILE by renaming it to an org_archive file.
Interactively, this operates on the current buffer. With a prefix arg,
prompt for a file instead."
(interactive (list (if current-prefix-arg
(read-file-name "Archive file: " nil nil t)
(buffer-file-name (buffer-base-buffer)))))
(find-file file)
(widen)
(let ((archive (car (org-archive--compute-location org-archive-location))))
(if (file-exists-p archive)
(progn
(goto-char (point-min))
(insert "\n# Final contents:\n\n")
(append-to-file (point-min) (point-max) archive))
(add-file-local-variable-prop-line 'mode 'org)
(write-file archive))
(doom/delete-this-file nil t)))
Todo Date Overriding
My attention span being what it is, I often forget to update TODO entries in my Org files until long after the task has been completed. I rely heavily on tracking TODOs through timestamps, so it would be nice to have a command to specify the time to log the task as being completed at. To do this, we can create a new variable org-todo-time
that will override the time when set.
(defvar org-todo-time nil
"The time to use when updating TODO entries.
If nil, then use the current time.")
;; HACK: Why does Emacs have so many independently defined time functions
(defadvice! ~/org-override-time (old-fn &rest args)
"Use `org-todo-time' as the current time if it is specified."
:around (list #'org-current-time #'org-auto-repeat-maybe)
(if org-todo-time
(cl-letf (((symbol-function #'current-time)
(lambda (&rest _) org-todo-time))
(float-time-old (symbol-function #'float-time))
((symbol-function #'float-time)
(lambda (&optional time) (funcall float-time-old (or time org-todo-time))))
(time-subtract-old (symbol-function #'time-subtract))
((symbol-function #'time-subtract)
(lambda (a b) (funcall time-subtract-old
(or a org-todo-time)
(or b org-todo-time))))
(decode-time-old (symbol-function #'decode-time))
((symbol-function #'decode-time)
(lambda (&optional time &rest args)
(apply decode-time-old (or time org-todo-time) args))))
(apply old-fn args))
(apply old-fn args)))
We can then define and bind alternate versions of org-todo
and org-agenda-todo
that allow us to pick the time to set.
(defmacro ~/org-wrap-todo (fn)
"Wrap a command to set `org-todo-time'."
(let ((new-fn (intern (format "%s-date" fn))))
`(defun ,new-fn (&optional arg)
,(format "Call `%s', but allow the user to pick a time first." fn)
(interactive "P")
(let ((org-todo-time (org-read-date t t)))
(,fn arg)))))
(after! org
(~/org-wrap-todo org-todo)
(~/org-wrap-todo org-agenda-todo))
(map! :mode org-mode
:after org
:localleader
"T" #'org-todo-date)
(map! :mode org-agenda-mode
:after org
"T" #'org-agenda-todo-date
:localleader
"T" #'org-agenda-todo-date)
(map! :mode evil-org-agenda-mode
:after org
:m "T" #'org-agenda-todo-date)
Header Argument Snippets
Writing header arguments for source code blocks is confusing, so let's automate the process somewhat by adding snippets to fill in keywords. To do this, we'll need to write some utility functions for checking if we're in a source block header.
(This is based on the similar code in Tecosaur's config, though I've removed some of the features I don't care much for.)
(defun ~/yas-org-src-header-p ()
"Determine whether `point' is within a src-block header or header-args."
(let ((context (org-element-context)))
(pcase (org-element-type context)
('src-block (< (point) ; before code part of the src-block
(save-excursion
(goto-char (org-element-property :begin context))
(forward-line 1)
(point))))
('inline-src-block (< (point) ; before code part of the inline-src-block
(save-excursion
(goto-char (org-element-property :begin context))
(search-forward "]{")
(point))))
('keyword (string-match-p "^header-args"
(org-element-property :value context))))))
(defun ~/yas-org-src-lang ()
"Try to find the current language of the src/header at `point'.
Return nil otherwise."
(let ((context (org-element-context)))
(pcase (org-element-type context)
('src-block (org-element-property :language context))
('inline-src-block (org-element-property :language context))
('keyword
(when (string-match "^header-args:\\([^ ]+\\)"
(org-element-property :value context))
(match-string 1 (org-element-property :value context)))))))
(defun ~/yas-org-prompt-header-arg (arg question values)
"Prompt the user to set ARG header property to one of VALUES with QUESTION.
The default value is identified and indicated. If either default is selected,
or no selection is made: nil is returned."
(let* ((src-block-p (not (looking-back
"^#\\+property:[ \t]+header-args:.*"
(line-beginning-position))))
(default
(or
(cdr (assoc arg
(if src-block-p
(nth 2 (org-babel-get-src-block-info t))
(org-babel-merge-params
org-babel-default-header-args
(let ((lang-headers
(intern (concat "org-babel-default-header-args:"
(~/yas-org-src-lang)))))
(when (boundp lang-headers) (eval lang-headers t)))))))
""))
default-value)
(setq values (mapcar
(lambda (value)
(if (string-match-p (concat "^" (regexp-quote value) "$")
default)
(setq default-value
(concat value " "
(propertize "(default)" 'face
'font-lock-doc-face)))
value))
values))
(let ((selection
(consult--read values :prompt question :default default-value)))
(unless (or (string-match-p "(default)$" selection)
(string= "" selection))
selection))))
Org Checklist
The simple package org-checklist
from org-contrib
makes it so that checkboxes inside of cyclic tasks are reset when the task is repeated. Sounds good to me!
(when (modulep! :lang org)
(use-package! org-checklist
:commands (org-reset-checkbox-state-maybe
org-make-checklist-export
org-checklist)
:init
(add-hook 'org-after-todo-state-change-hook #'org-checklist)
:config
(remove-hook 'org-after-todo-state-change-hook #'org-checklist)
(add-hook 'org-after-todo-state-change-hook #'org-checklist)))
The org-checklist
function will reset the checkboxes on any task, but I only want them reset when the task repeats.
(advice-add #'org-checklist :before-while #'org-get-repeat)
I don't want to have to specify the RESET_CHECK_BOXES
property for every TODO I write, though. I would much prefer if it was on by default, and the system allowed me to turn it off if I wanted to. Luckily, the fine control Org gives you over property inheritance nicely fixes this problem.
(after! org-checklist
(push '("RESET_CHECK_BOXES" . "t") org-global-properties))
(defpatch! org-checklist
(defun org-reset-checkbox-state-maybe) ()
"Reset all checkboxes in an entry if the `RESET_CHECK_BOXES' property is set"
(interactive "*")
(if (org-entry-get (point) "RESET_CHECK_BOXES" (el-patch-add t))
(org-reset-checkbox-state-subtree)))
Bug Fixes and Tweaks
Export Directory
Org mode by default exports to the same directory the org-mode file is in. This is inconvenient for me, as I use a lot of subdirectories. To fix this, we can advise the function org-export-output-file-name
.
(defvar org-export-dir (expand-file-name "export/" org-directory)
"The directory to export Org mode files to.
If nil, then `default-directory' for the org buffer is used.")
(defadvice! ~/modify-org-export-dir (orig-fn extension &optional subtreep pub-dir)
:around #'org-export-output-file-name
(unless pub-dir
(setq pub-dir org-export-dir))
(unless (file-directory-p pub-dir)
(make-directory pub-dir t))
(funcall orig-fn extension subtreep pub-dir))
Inline Image Previews
Inline images in Org are file links pointing to images without a description. User-defined inline images can have a description, however, which makes for a disparity between file links and other links that is very counter-intuitive. We can advise the image data provider functions to fix this, and I'll add a variable to restore the default behavior if I ever want to.
(defvar +org-inline-image-desc nil
"Whether to allow inline images to contain descriptions.")
(defadvice! +org-inline-desc (old-fn protocol link description)
"Disable inline images with descriptions when `+org-inline-image-desc'
is non-nil."
:around (list #'+org-inline-image-data-fn
#'+org-http-image-data-fn)
(when (or +org-inline-image-desc (null description))
(funcall old-fn protocol link description)))
This works fine for explicitly displaying inline images. However, toggling using RET
is now broken, as the command does not properly test for inline images. This will be fixed in the next section.
DWIM Command
The command +org/dwim-at-point
will toggle all overlays in a subtree even if there are other actions that are more likely to be what the user meant (such as marking as DONE). We also need to change the interaction with links to properly account for whether the link has a description.
(deftemplate! org (defun +org/dwim-at-point)
"Do-what-I-mean at point.
If on a:
- checkbox list item or todo heading: toggle it.
- citation: follow it
- headline: cycle ARCHIVE subtrees, toggle latex fragments and inline images in
subtree; update statistics cookies/checkboxes and ToCs.
- clock: update its time.
- footnote reference: jump to the footnote's definition
- footnote definition: jump to the first reference of this footnote
- timestamp: open an agenda view for the time-stamp date/range at point.
- table-row or a TBLFM: recalculate the table's formulas
- table-cell: clear it and go into insert mode. If this is a formula cell,
recaluclate it instead.
- babel-call: execute the source block
- statistics-cookie: update it.
- src block: execute it
- latex fragment: toggle it.
- link: follow it
- otherwise, refresh all inline images in current tree."
(interactive "P")
(if (button-at (point))
(call-interactively #'push-button)
(let* ((context (org-element-context))
(type (org-element-type context)))
(while (and context (memq type '(verbatim code bold italic underline
strike-through subscript superscript)))
(setq context (org-element-property :parent context)
type (org-element-type context)))
(pcase type
...
(el-patch-let (($latex (let* ((beg (if (org-before-first-heading-p)
(line-beginning-position)
(save-excursion (org-back-to-heading) (point))))
(end (if (org-before-first-heading-p)
(line-end-position)
(save-excursion (org-end-of-subtree) (point))))
(overlays (ignore-errors (overlays-in beg end)))
(latex-overlays
(cl-find-if (lambda (o)
(eq (overlay-get o 'org-overlay-type)
'org-latex-overlay))
overlays))
(image-overlays
(cl-find-if (lambda (o) (overlay-get o 'org-image-overlay))
overlays)))
(+org--toggle-inline-images-in-subtree beg end)
(if (or image-overlays latex-overlays)
(org-clear-latex-preview beg end)
(org--latex-preview-region beg end)))))
(`headline
(cond ... (el-patch-add (t $latex)))
...
(el-patch-remove $latex)))
...
(`link
(let* ((lineage (org-element-lineage context '(link) t))
(path (org-element-property :path lineage)))
(if (or (equal (org-element-property :type lineage) "img")
(and path (image-type-from-file-name path)
(el-patch-add
(or +org-inline-image-desc
(not (org-element-property :contents-begin lineage))))))
(+org--toggle-inline-images-in-subtree
(org-element-property :begin lineage)
(org-element-property :end lineage))
(org-open-at-point arg))))
...))))
Default Categories
When an explicit category is not specified, Org mode typically defaults to the filename (sans extension). This … sort of makes sense? I guess? It doesn't really, because filename conventions don't make for good agenda category names. I want my category names to be in title case, whereas a file name is typically going to be all lowercase and without spaces. This is especially bad for Org-roam, where filenames are automatically generated and way too long to be a UI element.
To fix this issue, it's thankfully rather simple to patch Org-mode's metadata system. The following code sets things up so that the file's #+title
metadata is used as the default category, falling back on the default behavior if a title is not given.
(defadvice! ~/org-default-category (old-fn)
"Modify how Org resolves the default category through the
`org-category' variable."
:around #'org-element--get-category
(let ((org-category (or org-category (org-get-title))))
(funcall old-fn)))
Org Roam
When I started out using Org mode, I just used vanilla Org files to manage my notes. This worked, but as my notes grew more and more I've begun to increasingly rely on Org-roam to more systematically manage my organization.
Concept
Org-roam is inspired by Roam Research, and like that tool it is based on the Zettelkasten (slip-box) note-taking method. In the Zettelkasten method, notes and concepts are separated into small chunks (called "nodes" in Org-roam terminology). These notes can live freely in any file on your system, and are linked to each other through ID links, leading to a freer note system that isn't tied to any particular organizational structure or hierarchy.
Task Management
In my use of Org-roam for task management, I divide nodes into a few different categories:
- Areas, which represent continual areas of your life to organize and plan;
- Goals, short- or long-term that can be completed;
- Tasks, which are small, one-time and contribute to goals or areas.
Areas are stored as subnodes of the Areas
file node, and likewise for goals. They also have the :area:
and :goal:
tags respectively. A task is a node that is a TODO entry that links to an area or a goal. We can thus check for if a node is a task by checking if the node links to a :area:
or :goal:
tagged node.
(defun ~/org-roam-get-linked-nodes (node tag)
"Return the nodes that NODE links to that are tagged with TAG."
(unless (stringp node)
(setq node (org-roam-node-id node)))
(let ((response (org-roam-db-query [:select :distinct [dest]
:from links
:inner-join tags :on (= dest node_id)
:where (= source $s1)
:and (= type "id")
:and (= tag $s2)]
node tag)))
(mapcar (lambda (id) (org-roam-populate
(org-roam-node-create :id (car id))))
response)))
Bindings
Here's a handful of bindings that streamline common operations I use:
(map! :mode org-mode
:after org
:localleader
"m l" #'org-roam-link-replace-all
"m R" #'org-roam-extract-subtree)
Roam File Names
By default, Roam generates file names containing both a timestamp and the node title. This makes it annoying if I want to change the title of that node later, so I'll change it to just the timestamp.
(after! org-roam
(setq org-roam-extract-new-file-path "%<%Y-%m-%d_%H-%M-%S>.org"))
Roam Buffer
The unlinked references section is turned off by default for performance reasons, but sometimes I want to see that information when writing notes. I also don't want it to show up for every node, because some of my nodes have quite a large reference list. Let's add a predicate to control when unlinked references are shown.
(defadvice! ~/org-roam-unlinked-references-p (node)
"A predicate to control when unlinked references are shown in the `org-roam' buffer."
:before-while #'org-roam-unlinked-references-section
(assoc "UNLINKED_REFS" (org-roam-node-properties node)))
(after! org-roam
(setq org-roam-mode-sections
'((org-roam-backlinks-section :unique t)
org-roam-reflinks-section
org-roam-unlinked-references-section)))
Roam Links
Making links to Roam nodes is a bit finicky. This helps fix some of that.
(defun org-roam-completion (&optional arg)
(let ((node (org-roam-node-read nil nil nil t)))
(concat "id:" (org-roam-node-id node))))
(defun org-roam-insert-description (idstr)
(org-roam-node-title (org-roam-node-from-id (substring idstr 3))))
(after! org
(org-link-set-parameters "roam"
:complete #'org-roam-completion
:insert-description #'org-roam-insert-description))
Capture
Org capture is a very useful mechanism for shortening the time it takes to "capture" information in your notes. The default method for configuring capture is the variable org-capture-templates
, which specifies templates to generate new entries with.
YASnippet Integration
These templates are all well and good, but there's a crucial issue with them: we already have a perfectly good template system! Snippets are highly versatile and useful, and it would be nice if we could leverage their features in Org capture templates.
The goal is to hook into Org's capturing system to get it to feed its template into YASnippet, instead of printing it directly.
(defadvice! org-capture-yasnippet (old-fn &optional arg)
"Modify Org's capture system to expand a YASnippet template."
:around #'org-capture-place-template
(let* ((template (org-capture-get :template))
(org-capture-plist (plist-put org-capture-plist :template "")))
(letf! ((#'org-paste-subtree #'ignore)
(#'org-kill-is-subtree-p #'always))
(funcall old-fn arg)
(yas-expand-snippet template))))
This advice overrides org-capture-place-template
, the function that:
- Moves to the location of capture inside the file
- Modifies the template to fit its context
- Places the template in the file
The advice temporarily sets Org's capture template to an empty string, lets Org do its thing, and then expands the template itself. This does mean that Org can no longer perform point 2, but YASnippet is powerful enough that we can simply do that ourselves in the template.
Roam Capture
Since Org-roam is currently my primary method of using Org, I only use its templating system. We'll start with defining some useful node accessor functions.
(defun org-roam-node-file-maybe (node &optional dir)
"Get file name from NODE, or return a default filename in directory DIR."
(setq dir (expand-file-name (or dir "") org-roam-directory))
(or (org-roam-node-file node)
(expand-file-name "%<%Y-%m-%d_%H-%M-%S>.org" dir)))
(defun org-roam-node-file-maybe-cite (node)
(org-roam-node-file-maybe node "cite"))
(defun org-roam-node-file-maybe-dir (node)
"Get file name from NODE, or ask for directory and return a default filename."
(or (org-roam-node-file node)
(expand-file-name
"%<%Y-%m-%d_%H-%M-%S>.org"
(read-directory-name "Directory: " org-roam-directory))))
I want there to only be one capture template when I'm making new links, so that I don't get distracted by a prompt. However, when I'm explicitly telling Roam to use node capture, it would be a shame if there was only one option. To fix this, I can split my capture templates into three contexts: a default template for when not explicitly capturing, a list of templates for capturing on a node that already exists, and the regular list for when the node is new.
(defvar org-roam-capture-default-template nil
"The default capture template to use when not explicitly capturing.")
(defvar org-roam-capture-existing-templates nil
"Capture templates to use when capturing on an existing node.")
(defvar org-roam-dailies-capture-default-template nil
"The default daily capture template to use when not explicitly capturing.")
(We don't distinguish between new and existing dailies, since dailies are meant to follow a single format, which can just be covered by the default template.)
With those variables created, we can define our templates.
(after! org-roam
;; Default templates
(setq org-roam-capture-default-template
'("d" "Default" plain ""
:target (file+head "${file-maybe-dir}"
"#+title: ${title}")
:unnarrowed t
:immediate-finish t)
org-roam-dailies-capture-default-template
'("d" "Default" plain ""
:target (file+head "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>")
:unnarrowed t
:immediate-finish t))
;; new-file templates
(setq org-roam-capture-templates
'(("f" "Standalone file" plain ""
:target (file+head "${file-maybe-dir}"
"#+title: ${title}")
:unnarrowed t))
org-roam-dailies-capture-templates
'(("e" "Event" entry "* $1\n%^t\n\n$0"
:target (file+head "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>")
:empty-lines 1)
("t" "Task" entry "* TODO $1\n\n$0"
:target (file+head "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>")
:empty-lines 1)
("n" "Notes" entry "** $0"
:target (file+head+olp "%<%Y-%m-%d>.org"
"#+title: %<%Y-%m-%d>"
("Course Notes :notes:"))
:unnarrowed t))))
Now we just have to advise Org-roam with the proper logic!
(defadvice! ~/org-roam-default-capture (old-fn &rest args)
"Use default capture template when not explicitly capturing."
:around #'org-roam-node-insert
(let ((org-roam-capture-templates (list org-roam-capture-default-template)))
(apply old-fn args)))
(defpatch! org-roam
(cl-defun org-roam-capture) (&optional goto keys &key filter-fn templates info)
"Launches an `org-capture' process for a new or existing node.
This uses the templates defined at `org-roam-capture-templates'.
Arguments GOTO and KEYS see `org-capture'.
FILTER-FN is a function to filter out nodes: it takes an `org-roam-node',
and when nil is returned the node will be filtered out.
The TEMPLATES, if provided, override the list of capture templates (see
`org-roam-capture-'.)
The INFO, if provided, is passed along to the underlying `org-roam-capture-'."
(interactive "P")
(let ((node (org-roam-node-read nil filter-fn)))
(org-roam-capture- :goto goto
:info info
:keys keys
:templates (el-patch-wrap 1 1
(or templates
(when (org-roam-node-file node)
org-roam-capture-existing-templates)))
:node node
:props '(:immediate-finish nil))))
(defadvice! ~/org-roam-dailies-default-capture (old-fn time &optional goto keys)
"Use default capture template when not explicitly capturing."
:around #'org-roam-dailies--capture
(let ((org-roam-dailies-capture-templates
(if goto (list org-roam-dailies-capture-default-template)
org-roam-dailies-capture-templates)))
(funcall old-fn time goto keys)))
Agenda
Configuration
A full week-long agenda is usually too cluttered for me to read, so I'll narrow it down to a single day. I also like the week to start on Monday.
(after! org
(require 'org-roam-dailies) ; Load along with `org' to make this code work
(setq org-agenda-span 'day
org-agenda-start-day nil
org-agenda-start-on-weekday 1 ; 1 = Monday
org-deadline-warning-days 7
org-agenda-sorting-strategy
'((agenda time-up habit-down timestamp-up priority-down category-up)
(todo habit-down priority-down category-up)
(tags habit-down priority-down category-up)
(search category-up))
;; Agenda files
org-agenda-files
(list org-directory
org-roam-directory
(f-join org-roam-directory "school/courses")
(f-join org-roam-directory org-roam-dailies-directory))
;; Make sure agenda is the only window
org-agenda-window-setup 'only-window
org-agenda-restore-windows-after-quit t
;; Agenda prefix
org-agenda-prefix-format
'((agenda . " %i %-28:c %?-12t% s")
(todo . " %i %-28:c ")
(tags . " %i %-28:c ")
(search . " %i %-28:c "))))
Agenda View
The Org agenda is a very nice feature, but by default it doesn't really provide enough customization to fit my needs. I like to have nice categories to make reading my todos easier, so we'll use org-super-agenda
:
(package! org-super-agenda)
(use-package! org-super-agenda
:commands org-super-agenda-mode)
(after! org-agenda
(let ((inhibit-message t))
(org-super-agenda-mode))
;; This map is unnecessary and causes evil bindings to not work
;; while on super agenda headers
(setq org-super-agenda-header-map nil))
By customizing org-super-agenda-groups
via a let-binding in my custom agenda view, I can set up a sophisticated multi-category agenda system.
(defun ~/org-agenda-section-by-link (prefix tag item)
"Org super-agenda function to categorize agenda entries by linked node with TAG."
(when-let* ((marker (org-super-agenda--get-marker item))
(nodes (org-super-agenda--when-with-marker-buffer marker
(--keep (org-element-property :ID it)
(org-element-lineage (org-element-context) nil t))))
(-compare-fn (lambda (a b)
(equal (org-roam-node-id a) (org-roam-node-id b))))
(links (-distinct (--mapcat (~/org-roam-get-linked-nodes it tag) nodes))))
(->> links
(mapcar #'org-roam-node-title)
(-interpose ", ")
(apply #'concat prefix))))
(defvar ~/org-todo-groups nil
"`org-super-agenda' groups for TODO entries of agenda.")
(after! org
(setq ~/org-todo-groups
'((:name "Next/In Progress"
:todo ("NEXT" "STRT" "WAIT" "HOLD"))
(:name "Important"
:priority "A"
:order 1)
(:name "Goals"
:tag "goal"
:order 9)
(:name "Notes to Intake"
:tag "notes"
:order 10)
(:name "Assignments"
:tag "assign"
:order 2)
(:auto-map (lambda (item)
(~/org-agenda-section-by-link
"Goal: " "goal" item))
:order 3)
(:auto-map (lambda (item)
(~/org-agenda-section-by-link
"Area: " "area" item))
:order 4))
org-agenda-custom-commands
'(("o" "Overview"
((agenda "")
(alltodo "" ((org-super-agenda-groups
(-insert-at 1 '(:discard
(:date t
:deadline t
:scheduled t))
~/org-todo-groups))))))
("g" "Grouped list of TODO entries"
alltodo "" ((org-super-agenda-groups ~/org-todo-groups))))))
This "overview" agenda command is very nice. It's so nice, in fact, that it's almost always the only agenda view I want to use! Having to go through the dispatcher every time I want to access it is annoying and unnecessary.
(defun ~/org-agenda (&optional arg)
"Wrapper around preferred agenda view."
(interactive "P")
(org-agenda arg "o"))
(map! :leader
:desc "Org agenda"
"o a" #'~/org-agenda
;; Use shift to access full dispatcher
:desc "Org agenda dispatcher"
"o A" #'org-agenda)
TODO Tweaks
When finding a node with org-roam-node-find
, the universal argument to open it in another window doesn't work when the node is new. We can fix this with a patch:
(defpatch! org-roam
(cl-defun org-roam-node-find) (&optional other-window initial-input filter-fn pred
&key templates)
"Find and open an Org-roam node by its title or alias.
INITIAL-INPUT is the initial input for the prompt.
FILTER-FN is a function to filter out nodes: it takes an `org-roam-node',
and when nil is returned the node will be filtered out.
If OTHER-WINDOW, visit the NODE in another window.
The TEMPLATES, if provided, override the list of capture templates (see
`org-roam-capture-'.)"
(interactive current-prefix-arg)
(let ((node (org-roam-node-read initial-input filter-fn pred)))
(if (org-roam-node-file node)
(org-roam-node-visit node other-window)
(org-roam-capture-
:node node
:templates (el-patch-wrap 1 1
(or templates (list org-roam-capture-default-template)))
:props (el-patch-wrap 3
(if other-window
'(:after-finalize find-file-other-window)
'(:finalize find-file)))))))
Citations
Org mode has a very nice notation for specifying citations in square brackets, and the package citar
uses this to implement a useful bibliography system.
Let's start with some configuration. I use Zotero to manage my citations, and when I want to use them in Org mode I export them to a file library.json
(CSL JSON) in my org directory.
(after! org
(setq org-cite-csl-styles-dir "~/Zotero/styles"
;; MLA style by default
org-cite-csl--fallback-style-file
"/home/kiana/Zotero/styles/modern-language-association.csl"
org-cite-global-bibliography
(list (expand-file-name "library.json" org-directory))
citar-bibliography org-cite-global-bibliography))
Citation Settings
I primarily use the CSL export processor to create MLA-style citations, so let's configure that to make its citations more standard.
(after! org
(setq org-cite-csl-link-cites nil ; This is not recommended by MLA
))
Roam Citation Notes
We can use the package citar-org-roam
(bundled with Doom module :tools biblio
) to create citation notes in Roam.
(after! citar-org-roam
(setq citar-org-roam-subdir "cite"
citar-org-roam-capture-template-key "n"))
Aesthetics
We should also make Org citations look a little prettier:
;; Make faces conform to theme
(after! org
(custom-set-faces!
`(org-cite :foreground ,(doom-color 'green))
'(org-cite-key :foreground nil :slant italic :underline t :inherit org-cite)))
;; Citar icons
(after! citar
(setq citar-indicators
(list
(citar-indicator-create
:symbol (nerd-icons-mdicon "nf-md-link"
:face 'nerd-icons-lblue)
:padding " "
:function #'citar-has-links
:tag "has:links")
(citar-indicator-create
:symbol (nerd-icons-mdicon "nf-md-file"
:face 'nerd-icons-lred)
:padding " "
:function #'citar-has-files
:tag "has:files")
(citar-indicator-create
:symbol (nerd-icons-mdicon "nf-md-note_text"
:face 'nerd-icons-blue)
:padding " "
:function #'citar-has-notes
:tag "has:notes")
(citar-indicator-create
:symbol (nerd-icons-mdicon "nf-md-check"
:face 'nerd-icons-lgreen)
:padding " "
:function #'citar-is-cited
:tag "is:cited"))))
Journal
I don't use org-journal
anymore, but I'm keeping my old configuration for it in case I want to go back.
(after! org-journal
;; One entry per day, no separation
(setq org-journal-file-format "%Y-%m-%d"
org-journal-hide-entries-p nil))
To make opening the journal more convenient, here's a command to open the latest entry:
(defun +org/org-journal-open-latest ()
(interactive)
(require 'org-journal)
(funcall org-journal-find-file
(car (last (seq-filter #'file-regular-p
(directory-files org-journal-dir t))))))
(map! :leader
:desc "Journal"
"o j" #'+org/org-journal-open-latest)
Languages and Modes
This is the configuration for all the mode-related packages, including programming language packages. Most of this is configuring packages to work with my preferred Nix-based development environment.
Dirvish
dirvish
is a drop-in replacement to Emacs's native dired
that adds a bunch of useful features and customization, inspired by vim's ranger
. The package is provided by Doom Emacs's :emacs dired
module, so we just need to configure it.
Switching Projects
The variable +workspaces-switch-project-function
contains a function that is run whenever a project is opened. A file explorer seems like a natural place to open at this point.
(after! doom-modules
(setq +workspaces-switch-project-function #'dirvish))
Bindings
The Doom module's bindings for Evil users are currently a bit unpolished, so let's fix them up here.
(map! :mode dirvish-mode
:after dirvish
:m "C-o" #'dirvish-history-go-backward
:m "C-i" #'dirvish-history-go-forward)
Layout
(after! dirvish
;; Allocate more space to center window
(setq dirvish-default-layout '(1 0.15 0.45))
;; Show nested directories
(pushnew! dirvish-attributes 'collapse))
Faces
The faces that dirvish
uses to indicate git changes are showing up as gray by default, and the face that it uses to highlight the current line is a bit bright.
To fix the line highlight we can just configure the relevant face (dirvish-hl-line
), but the faces used by dirvish-vc
actually inherit from faces for a built-in emacs library vc
, so it makes more sense to customize those instead.
(after! dirvish
(set-face-attribute 'dirvish-hl-line nil :inherit 'hl-line))
(after! vc
(set-face-attribute 'vc-edited-state nil
:foreground (doom-color 'orange))
(set-face-attribute 'vc-locally-added-state nil
:foreground (doom-color 'green))
(set-face-attribute 'vc-conflict-state nil
:foreground (doom-color 'red))
(set-face-attribute 'vc-needs-update-state nil
:foreground (doom-color 'red))
(set-face-attribute 'vc-missing-state nil
:foreground (doom-color 'comments)))
(after! dirvish-vc
(set-face-attribute 'dirvish-vc-unregistered-face nil
:foreground (doom-color 'grey)))
dirvish-fd
(after! dirvish-fd
(setq dirvish-fd-program
(if-let ((path (nix-build-out-path-gcroot "fd" "nixpkgs#fd")))
(concat path "/bin/fd")
(error "Building fd for dirvish-fd failed"))))
Sidebar Project Tree
I used to use treemacs
as my dedicated project tree, but dirvish
is much more polished and works better with the Emacs ecosystem. The provided dirvish-side
extension adds support for using it in a sidebar.
(defun ~/dirvish-side-open (&optional path)
"Select the Dirvish side session, creating one if it does not exist.
If called with \\[universal-argument], prompt for PATH, otherwise
it defaults to `project-current'."
(interactive (list (and current-prefix-arg
(read-directory-name "Open sidetree: "))))
(require 'dirvish-side)
(let ((fullframep (when-let ((dv (dirvish-curr))) (car (dv-layout dv))))
(visible (dirvish-side--session-visible-p))
(path (or path (dirvish--get-project-root) default-directory)))
(cond (fullframep (user-error "Can not create side session here"))
(visible (select-window visible))
(t (dirvish-side--new path)))))
(map! :map evil-window-map
"0" #'~/dirvish-side-open)
(map! :leader
:desc "Select project tree window"
"0" #'~/dirvish-side-open
"o p" #'dirvish-side)
Window Numbering
If a side session is open, we'll configure winum
to record it as having the index (popup . 0)
, i.e. the 0th popup window. It's not actually a popup, but this is the easiest way to separate it from the "main" windows.
(after! (winum dirvish-side)
(defun ~/winum-dirvish-side ()
(when (eq (selected-window) (dirvish-side--session-visible-p))
'(popup . 0)))
(push #'~/winum-dirvish-side winum-assign-functions))
Haskell
Operators being in italics looks ugly, so let's fix that.
(custom-set-faces! '(haskell-operator-face :slant normal))
Idris
Idris's support for Emacs is still experimental and has a few kinks to work out, especially since I'm using Idris 2, which the recommended idris-mode
needs to be configured to use.
The Doom module is very outdated, so I'll be overriding it.
;; -*- no-byte-compile: t; -*-
;;; lang/idris/packages.el
(package! idris-mode)
Config
(after! idris-mode
(setq idris-interpreter-path "idris2"
;; No fun allowed!!
;; (The animated banner was messing with the popup system)
idris-repl-banner-functions '(idris-repl-text-banner)
idris-repl-show-repl-on-startup nil ; Don't show repl on startup
)
;; (add-hook! idris-mode :append #'lsp!)
(set-repl-handler! 'idris-mode (cmd! (idris-repl-buffer)))
(set-lookup-handlers! 'idris-mode
:documentation #'idris-docs-at-point)
(map! :localleader
:mode idris-mode
"q" #'idris-quit
"l" #'idris-load-file
"t" #'idris-type-at-point
"a" #'idris-add-clause
"e" #'idris-make-lemma
"c" #'idris-case-dwim
"w" #'idris-make-with-block
"m" #'idris-add-missing
"p" #'idris-proof-search
"h" #'idris-docs-at-point
(:prefix ("i" . "ipkg")
"f" #'idris-open-package-file
"b" #'idris-ipkg-build
"c" #'idris-ipkg-clean
"i" #'idris-ipkg-install))
(map! :localleader
:mode idris-ipkg-mode
"b" #'idris-ipkg-build
"c" #'idris-ipkg-clean
"i" #'idris-ipkg-install
"f" #'idris-ipkg-insert-field))
Appearance
Operators being in italics looks ugly in this mode too, but due to Idris's more complicated syntax highlighting system we have to do a bit more work than for Haskell. There's also the issue of the semantic highlighting faces, which don't match our theme colors.
(custom-set-faces!
'((idris-operator-face idris-colon-face idris-equals-face) :slant normal)
;; Semantic highlighting
`(idris-semantic-type-face :foreground ,(doom-color 'blue))
`(idris-semantic-data-face :foreground ,(doom-color 'red))
`(idris-semantic-bound-face :foreground ,(doom-color 'magenta) :slant italic)
`(idris-semantic-function-face :foreground ,(doom-color 'dark-green))
`(idris-semantic-postulate-face :foreground ,(doom-color 'yellow)))
Indentation
I prefer 2-space indentation in all circumstances. Unfortunately, Emacs's indentation is mode-specific, so I have to configure every mode's indentation separately.
(after! css-mode
(setq css-indent-offset 2))
Java
The lsp-java
package provides LSP support using the standard Java language server, the Eclipse-based jdtls
. Unfortunately, this package isn't designed for Nix, so it fails to find the server script in our Nix store. We need to do a bit of legwork to patch in this support, as well as some related considerations, such as per-project config directories.
(defun +lsp-java-server-store-path ()
"Return the Nix store derivation path to the Java language server."
(require 'lsp-java)
(string-remove-suffix
(concat "/bin/" lsp-java-jdt-ls-command)
(or
(executable-find lsp-java-jdt-ls-command)
(user-error "Could not find Java language server"))))
(after! lsp-java
(setq lsp-java-jdt-ls-prefer-native-command t))
(add-hook! java-mode
(setq-local lsp-java-server-install-dir
(concat (+lsp-java-server-store-path) "/share/java/jdtls/")))
(defadvice! ~/lsp-java-ls-command ()
"Configure `lsp-java' to work with Nix."
:override #'lsp-java--ls-command
(let ((root (or (doom-project-root) (f-join (getenv "XDG_DATA_HOME") "jdtls"))))
(list lsp-java-jdt-ls-command
"-configuration" (f-join root "config-linux")
"-data" (f-join root "java-workspace"))))
Language Servers
The emacs package lsp-mode
is the package of choice for general LSP integration in Emacs. The Doom Emacs module :tools lsp
handles most of the basic configuration for it, but there's one minor annoyance it doesn't cover: when a server isn't found for a particular language, lsp-mode
tries to install the server itself to a local directory, which is a completely useless space-filler for my purposes since that I use NixOS.
There is luckily a configuration variable to disable this suggestion:
(after! lsp-mode
(setq lsp-enable-suggest-server-download nil))
Bindings
Some more convenient leader key bindings would be nice to prevent having to trawl through the lsp-command-map
.
(map! :leader
:desc "Select LSP code lens"
"c L" #'lsp-avy-lens)
Yuck
Since I've started using EWW (Elkowar's Wacky Widgets) for my linux setup, it would be nice to have an editing mode for its configuration language.
(package! yuck-mode)
Slint
(package! slint-mode)
Unless you're reading the raw file on Github, in which case you are probably already decently familiar with org-mode
to be able to read its markup.