doom-config/config.org

132 KiB
Raw Blame History

Doom Emacs Config

Introduction

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)

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.

/toki/doom-config/media/commit/8798d4c41d242bff0018b6181ffe0d4f32fa80b4/assets/vanilla_emacs.png

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: org-mode, calfw, calc, mu4e. I started putting more and more time into tweaking these applications 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 had been using org-mode for several months at this point and was very comfortable with it, so utilizing it to better organize my Emacs code seemed like a good idea.

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 a compiler or interpreter to be installed.)

Tangle, Weave, Export, Publish

Since Org's ability to execute and code process its output is so robust, it's only natural that one might consider using Org to annotate existing code. This could be done by maintaining both the Org document and an actual source file separately, but that would require duplicating edits in both files, which is not ideal.

The solution is literate programming, the practice of embedding an active codebase into a document. Literate programming can be done in a few different ways; some languages support directly processing literate programs, including Julia and Agda. However, the way Org implements it is closer to the traditional method, which involves two processing systems:

  1. Tangling, converting the file into source code that can be executed. Tangling must be performed before the code is used, and in Org is typically done into a separate generated file.
  2. 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 of them require running code, since as demonstrated above, the exported documentation can contain program execution results.

From Code into 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 and tangling configuration, 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.

confpkg

As part of their 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 the source block mechanism.

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 the code from being exported
  • Reorganize to get rid of superfluous noweb references
  • Change the package template to contain my information

Current Issues

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 Mode

A lot of my current Org mode configuration consists of relics of previous organizational systems, including the TODO states and capture templates. I don't currently use these, as most of my Org mode use has shifted towards Org-roam, so it might be a good idea to replace these with something more useful.

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! <<doom-input>>

       <<doom-completion>>

       <<doom-ui>>

       <<doom-editor>>

       <<doom-emacs>>

       <<doom-term>>

       <<doom-checkers>>

       <<doom-tools>>

       <<doom-os>>

       <<doom-lang>>

       <<doom-email>>

       <<doom-app>>

       <<doom-config>>
       )

Config Modules

Considering 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 the icons flag because why not.

:completion
(vertico +icons)
(company +childframe)

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 looks
  • (window-select +numbers) because multiple windows are too inconvenient without an easy way to switch between them
  • file-templates and snippets because typing is hard
  • (format +onsave) because I don't want to have to remember to run a formatter
  • direnv 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 +diff-hl +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
make
pass
pdf
;;prodigy
;;rgb
;;terraform
tree-sitter
;;tmux
;;upload
:emacs
(dired +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)
;;javascript
;;julia
;;kotlin
(latex +lsp)
;;lean
;;ledger
;;lua
markdown
;;nim
(nix +tree-sitter)
;;ocaml
(org +pretty +roam2
    +gnuplot +jupyter
    +pandoc +journal
    +present)
;;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 tab-width 2                      ; 2 width tabs
              delete-by-moving-to-trash t      ; Delete files to trash
              window-combination-resize t      ; Resize windows more evenly
              )

(setq compile-command "nix build"
      truncate-string-ellipsis "…"             ; Unicode!
      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)

Thanks once again to Tecosaur for some of these settings.

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

I don't want my cache files to get deleted whenever I mess up my Doom install, so let's move them to somewhere more safe.

(require 'auth-source-pass)
(setq auth-sources '(password-store "~/.authinfo.gpg")
      auth-source-cache-expiry nil)

Bindings

Windows & Workspaces

I like using window numbers to navigate between splitscreen windows, but having to type SPC w <#> every time is annoying. Let's shorten that key sequence by 67%, and also throw in a convenient binding for switching to treemacs.

(map! :leader
      ;; Bind "SPC 0" to treemacs
      ;; Map window bindings to "SPC 1" through "SPC 9"
      "w 0" #'treemacs-select-window
      :desc "Select project tree window" "0" #'treemacs-select-window
      :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)

Now SPC 1 will work equivalently to SPC w 1. Efficiency!

I like to reorganize my workspaces, so we can also 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)

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 "Open URL"
      "s u" #'goto-address-at-point)

… This is Also Here

I'm not even going to bother explaining this one. Emacs is just janky sometimes lol

(defadvice! ~/projectile-find-file (invalidate-cache &optional ff-variant)
  :override #'projectile--find-file
  (projectile-maybe-invalidate-cache invalidate-cache)
  (let* ((project-root (projectile-acquire-root))
         (file (read-file-name "Find file: " project-root project-root
                               (confirm-nonexistent-file-or-buffer) nil
                               ))
         (ff (or ff-variant #'find-file)))
    (when file
      (funcall ff (expand-file-name file project-root))
      (run-hooks 'projectile-find-file-hook))))

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
  (setq-local visual-fill-column-extra-text-width '(0 . 6))
  (visual-fill-column--adjust-window))

Automated Nix Builds

Some packages in this config such as treemacs, 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:

  1. 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.
  2. 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:
  3. Build the tool and point Emacs directly to the store path. This is the simplest 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"
                     ,@(if 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 well enough if we just want to build something, but there's a problem: we haven't indicated to Nix that we want this output to stick around, 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))

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 out of obligation 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 this font, so let's make more things italicized! While we're here, we'll also set doom's modified buffer font to be red 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 :weight bold :inherit (doom-modeline error)))

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)

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 with a focus on minimalism, 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:

/toki/doom-config/media/commit/8798d4c41d242bff0018b6181ffe0d4f32fa80b4/assets/splash.svg

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.

Our package! declarations go in packages.el, which must not be byte-compiled:

;; -*- no-byte-compile: t; -*-

Everything else goes in config.el, which is managed by confpkg as outlined earlier.

Company

TODO Optimization

Bindings

When Company is active, its keybindings overshadow the default ones, meaning keys like RET no longer work. To prevent this from happening, let's rebind company-complete-selection to TAB (less useful in the middle of typing), and only allow RET to be used if Company has been explicitly interacted with.

(after! company
  (let ((item `(menu-item nil company-complete-selection
                :filter ,(lambda (cmd)
                           (when (company-explicit-action-p)
                             cmd)))))
    (map! :map company-active-map
          "RET" item
          "<return>" item
          "TAB" #'company-complete-selection
          "<tab>" #'company-complete-selection
          "S-TAB" #'company-complete-common)))

Spell Correction

I've been having problems with company-ispell, mainly due to Ispell requiring a text-based dictionary (unlike Aspell, which uses a binary dictionary). So let's switch to company-spell:

(package! company-spell)
(after! company-spell
  (map! :map evil-insert-state-map
        "C-x s" #'company-spell))

We should make sure that company-spell uses Ispell's personal dictionary too:

(after! (company-spell ispell)
  (setq company-spell-args
        (concat company-spell-args " -p " ispell-personal-dictionary)))

Icons

The company-box front-end adds support for icons, but there aren't many providers for them, especially in text. We'll add two new icon providers:

  • ~/company-box-icons--text, which directly targets the output of company-spell
  • ~/company-box-icons--spell, which is a fallback for all text completions
;; Mark candidates from `company-spell' using a text property
(defadvice! ~/company-spell-text-property (words)
  :filter-return #'company-spell-lookup-words
  (dolist (word words)
    (put-text-property 0 1 'spell-completion-item t word))
  words)

(defun ~/company-box-icons--spell (candidate)
  (when (get-text-property 0 'spell-completion-item candidate)
    'Text))

(defun ~/company-box-icons--text (candidate)
  (when (derived-mode-p 'text-mode) 'Text))

(after! company-box
  (pushnew! company-box-icons-functions #'~/company-box-icons--text)
  ;; `~/company-box-icons--text' is a fallback, so it has to go at the end of
  ;; the list
  (setq company-box-icons-functions
        (append company-box-icons-functions '(~/company-box-icons--text))))

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.

(defun ~/embark-target-prog-mode (old-fn)
  "Advise an embark target to only activate in `prog-mode'."
  (when (derived-mode-p 'prog-mode) (funcall old-fn)))

(defun ~/embark-target-identifier (old-fn)
  "Advise an embark target to only activate in `prog-mode' and not in `lsp-mode'."
  (when (and (derived-mode-p 'prog-mode) (not (bound-and-true-p lsp-mode))) (funcall old-fn)))

(advice-add #'embark-target-expression-at-point :around #'~/embark-target-prog-mode)
(advice-add #'embark-target-identifier-at-point :around #'~/embark-target-identifier)

(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 override it to flip the order.

(after! 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 (car bounds))
        (goto-char (cdr bounds))
        (apply run :bounds bounds rest)))
     (bounds ;; used as pre- or post-action hook
      (set-mark (car bounds))
      (goto-char (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

(after! flymake
  (set-popup-rule! "^\\*Flymake" :vslot 1 :side 'bottom))

Indent Guides

I've found that character-based indent guides work best.

(after! highlight-indent-guides
  (setq highlight-indent-guides-method 'character
        highlight-indent-guides-character 9615
        highlight-indent-guides-responsive 'top
        highlight-indent-guides-auto-character-face-perc 90
        highlight-indent-guides-auto-top-character-face-perc 200))

Language Servers

lsp-mode requires avy, but doesn't load it for some reason.

;; (advice-add #'lsp-avy-lens :before (cmd! (require 'avy)))

Here's a convenient leader key binding as well:

(map! :leader
      :desc "Select LSP code lens"
      "c L" #'lsp-avy-lens)

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.

(after! git-commit
  (setq git-commit-post-finish-hook-timeout 8))

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))

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 hints that show how large the editing operation I just performed was, 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 an extremely versatile way of avoiding unnecessary typing, especially when writing code.

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-mode (&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, so we'll override this function to use that.

(defadvice! ~/snippets-new (&optional all-modes)
  "Use standard file template when creating a new snippet."
  :override #'+snippets/new
  (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)
      (switch-to-buffer snippet-key)
      (snippet-mode)
      (erase-buffer)
      (set-visited-file-name snippet-file-name)
      (+file-templates--expand t))))

Since the snippet is expanded in an environment including the variable snippet-key, our template can use that to automatically fill in the key we specified.

Treemacs

Treemacs is a really useful package, but it also has a lot of defaults I don't like. Let's add a use-package! declaration to fix some of them:

(use-package! treemacs
  :defer t
  :init
  ; More accurate git status
  (setq +treemacs-git-mode 'deferred
        treemacs-python-executable
        (if-let ((path (nix-build-out-path-gcroot
                        "treemacs-python" "nixpkgs#python3")))
            (concat path "/bin/python")
          (error "Building python for treemacs failed")))
  :config
  (setq ; Child-frame reading is broken (and sucks anyways)
        treemacs-read-string-input 'from-minibuffer
        ; Make "SPC 0" work like other window select commands
        treemacs-select-when-already-in-treemacs 'stay)

  ; Better font styling
  (custom-set-faces!
    ; Variable pitch fonts
    '((treemacs-root-face
       treemacs-file-face) :inherit variable-pitch)
    '(treemacs-tags-face :height 0.95 :inherit variable-pitch)
    '(treemacs-directory-face :inherit treemacs-file-face)
    '((treemacs-git-added-face
       treemacs-git-modified-face
       treemacs-git-renamed-face
       treemacs-git-conflict-face) :inherit treemacs-file-face)
    ; Better colors
    `(treemacs-git-ignored-face
      :foreground ,(doom-color 'base1) :slant italic :inherit treemacs-file-face)
    `(treemacs-git-untracked-face
      :foreground ,(doom-color 'base1) :inherit treemacs-file-face)
    '(treemacs-async-loading-face
      :height 0.8 :inherit (font-lock-comment-face treemacs-file-face)))

  (treemacs-hide-gitignored-files-mode) ; Hide git-ignored files by default
  (treemacs-fringe-indicator-mode -1)   ; No fringe indicator
  (treemacs-resize-icons 16)            ; Make icons smaller
  )

Project Integration

I often accidentally open the project tree before I've even selected a project, which I don't want because it messes up treemacs-projectile. Let's fix that problem:

(defun ~/treemacs-restrict (&rest _)
  (unless (doom-project-p)
    (user-error "Must be in a project to open project tree")))

(advice-add #'treemacs-select-window :before #'~/treemacs-restrict)
(advice-add #'+treemacs/toggle :before #'~/treemacs-restrict)

When I do have a project open, Treemacs is flexible and allows you to open directories other than that project. This would be great and convenient if it weren't for the fact that it doesn't do so very well, often opening the wrong directories entirely. This convenience function ensures that only the project directory is open.

(defun ~/treemacs-fix-project ()
  "Modify the current `treemacs' workspace to only include the current project."
  (interactive)
  (require 'treemacs)
  (let* ((name (concat "Perspective " (doom-project-name)))
         (project (treemacs-project->create!
                   :name (doom-project-name)
                   :path (directory-file-name (doom-project-root))
                   :path-status 'local-readable :is-disabled? nil))
         (workspace (treemacs-workspace->create!
                     :name name :projects (list project) :is-disabled? nil)))
    ;; Only rebuild workspace if it doesn't have the structure we expect
    (unless (equal (treemacs-current-workspace) workspace)
      (setq treemacs--workspaces
            (append (remove-if (lambda (w)
                                 (string= (treemacs-workspace->name w) name)
                               treemacs--workspaces)
                    (list workspace))))
      (treemacs-do-switch-workspace workspace)
      (treemacs--invalidate-buffer-project-cache)
      (treemacs--rerender-after-workspace-change))))

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")))

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. I haven't found a better way to do this than to edit the relevant variable in init.el:

;; Enable evil-collection-calc
(delq 'calc +evil-collection-disabled-list)

Let's also rebind some keys. Preserving evil's [ and ] bindings doesn't make sense to me, 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
  ))

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
(defadvice! ~/emacs-everywhere-app-info-hyprland ()
  "Return information on the active window, in Hyprland."
  :override #'emacs-everywhere--app-info-linux
  (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)))

TODO Mail

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! password-store
  (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
  (set-popup-rule! "^\\*Password-Store" :side 'right :size 0.25 :quit nil))

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

(setq org-directory "~/org/")

(after! org
  (setq org-archive-location               ; Global archive file
        (concat org-directory ".org_archive::")
        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
        org-startup-with-inline-images t   ; Do more stuff on startup
        org-startup-with-latex-preview t
        +org-startup-with-animated-gifs t
        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))

        ;; Customize appearance
        org-hide-emphasis-markers t
        org-hide-leading-stars nil
        org-superstar-item-bullet-alist
        '((?* . 8226)
          (?+ . 8226)
          (?- . 8226))))

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
      "p" #'org-priority ; Remove extraneous commands
      "g j" #'org-goto
      "c D" #'org-clock-display
      "m b f" #'org-table-eval-formula
      "m b F" #'org-table-edit-formulas

      ;; 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)

This also means we don't need org-cycle to emulate indentation, which is nice.

Appearance

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))

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))

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!

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.")

(defadvice! ~/org-override-time (old-fn)
  "Use `org-todo-time' as the current time if it is specified."
  :around #'org-current-effective-time
  (or org-todo-time (funcall old-fn)))

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)))

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))

(defadvice! ~/org-checklist-reset-inherit ()
  "Override checkbox resetting to use property inheritance."
  :override #'org-reset-checkbox-state-maybe
  (if (org-entry-get (point) "RESET_CHECK_BOXES" 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))

Attachment Inline Previews

Doom enhances Org mode's attachment system to show inline previews of attached images. However, this appears to be broken due to using outdated APIs, so we have to patch the link parameter responsible.

(defadvice! ~/org-image-file-data-fn (protocol link _description)
  "Properly handle attachment links."
  :override #'+org-image-file-data-fn
  (setq link
        (pcase protocol
          ("download"
           (expand-file-name
            link
            (or (if (require 'org-download nil t) org-download-image-dir)
                (if (require 'org-attach)         org-attach-id-dir)
                default-directory)))
          ("attachment"
           (require 'org-attach)
           (org-attach-expand link))
          (_ (expand-file-name link default-directory))))
  (when (and (file-exists-p link)
             (image-type-from-file-name link))
    (with-temp-buffer
      (set-buffer-multibyte nil)
      (setq buffer-file-coding-system 'binary)
      (insert-file-contents-literally link)
      (buffer-substring-no-properties (point-min) (point-max)))))

(defadvice! ~/org-init-attach-link ()
  "Properly set attachment link's parameters."
  :after #'+org-init-attachments-h
  (org-link-set-parameters "attachment" :image-data-fun #'+org-image-file-data-fn))

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 '(org-yt-image-data-fun
            +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.

Annoyingly, the only good way to fix these issues is to completely override the extremely long function.

(defadvice! ~/org-dwim (old-fn &optional arg)
  "Only toggle overlays in headlines if there is nothing else to do."
  :override #'+org/dwim-at-point
  (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
        ((or `citation `citation-reference)
         (org-cite-follow context arg))
        (`headline
         (cond ((memq (bound-and-true-p org-goto-map)
                      (current-active-maps))
                (org-goto-ret))
               ((and (fboundp 'toc-org-insert-toc)
                     (member "TOC" (org-get-tags)))
                (toc-org-insert-toc)
                (message "Updating table of contents"))
               ((string= "ARCHIVE" (car-safe (org-get-tags)))
                (org-force-cycle-archived))
               ((or (org-element-property :todo-type context)
                    (org-element-property :scheduled context))
                (org-todo
                 (if (eq (org-element-property :todo-type context) 'done)
                     (or (car (+org-get-todo-keywords-for (org-element-property :todo-keyword context)))
                         'todo)
                   'done)))
               (t
                (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)))))
         (org-update-checkbox-count)
         (org-update-parent-todo-statistics)
         (when (and (fboundp 'toc-org-insert-toc)
                    (member "TOC" (org-get-tags)))
           (toc-org-insert-toc)
           (message "Updating table of contents"))
         )
        (`clock (org-clock-update-time-maybe))
        (`footnote-reference
         (org-footnote-goto-definition
          (org-element-property :label context)))
        (`footnote-definition
         (org-footnote-goto-previous-reference
          (org-element-property :label context)))
        ((or `planning `timestamp)
         (org-follow-timestamp-link))
        ((or `table `table-row)
         (if (org-at-TBLFM-p)
             (org-table-calc-current-TBLFM)
           (ignore-errors
             (save-excursion
               (goto-char (org-element-property :contents-begin context))
               (org-call-with-arg 'org-table-recalculate (or arg t))))))
        (`table-cell
         (org-table-blank-field)
         (org-table-recalculate arg)
         (when (and (string-empty-p (string-trim (org-table-get-field)))
                    (bound-and-true-p evil-local-mode))
           (evil-change-state 'insert)))
        (`babel-call
         (org-babel-lob-execute-maybe))
        (`statistics-cookie
         (save-excursion (org-update-statistics-cookies arg)))
        ((or `src-block `inline-src-block)
         (org-babel-execute-src-block arg))
        ((or `latex-fragment `latex-environment)
         (org-latex-preview arg))
        (`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-supported-file-p path)
                        (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))))
        ((guard (org-element-property :checkbox (org-element-lineage context '(item) t)))
         (org-toggle-checkbox))
        (`paragraph
         (+org--toggle-inline-images-in-subtree))
        (_
         (if (or (org-in-regexp org-ts-regexp-both nil t)
                 (org-in-regexp org-tsr-regexp-both nil  t)
                 (org-in-regexp org-link-any-re nil t))
             (call-interactively #'org-open-at-point)
           (+org--toggle-inline-images-in-subtree
            (org-element-property :begin context)
            (org-element-property :end context))))))))

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 category names. This is especially bad for Org-roam, where filenames are automatically generated and can be very large and hard to read.

To fix this issue, it is thankfully rather simple to patch Org-mode's category system2. 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-refresh-category-properties org-element--get-category)
  (let ((org-category (or org-category (org-get-title))))
    (funcall old-fn)))

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 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    '(("HIST1111" . ?1))
        classes-tr     '(("MATH2203" . ?2))
        classes-online '(("HIST2111" . ?3)))

  (setq org-tag-persistent-alist
        `(("area" . ?A) ("goal" . ?G) ("project" . ?P) ("meta" . ?M)

          ;; Topics
          (:startgrouptag) ("college")
          (:grouptags) ("assign") ("notes") (:endgrouptag)

          ("economics") ("polsci") ("math") ("history")
          (:startgrouptag) ("math")
          (:grouptags) ("calculus") ("algebra") (:endgrouptag)

          ;; Classes
          (: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))))

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:

  1. Areas, which represent continual areas of your life to organize and plan;
  2. Goals, short- or long-term, things that can be completed;
  3. Tasks, which are 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."
  (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)]
                                     (org-roam-node-id node) tag)))
    (mapcar (lambda (id) (org-roam-populate
                          (org-roam-node-create :id (car id))))
            response)))

TODO Bindings

(map! :mode org-mode
      :after org
      :localleader
      "m l" #'org-roam-link-replace-all
      "m R" #'org-roam-extract-subtree)

Roam Buffer

The unlinked references section is turned off by default for performance reasons, but I've never had any serious issues with it. Let's turn that on, and also make sure backlinks are unique.

(after! org-roam
  (setq org-roam-mode-sections
        '((org-roam-backlinks-section :unique t)
          org-roam-reflinks-section
          org-roam-unlinked-references-section)))

Roam Capture

Creating new nodes should be quick and easy, so we should stick to one template to avoid the hassle of choosing.

(defun org-roam-node-file-maybe (node &optional dir)
  "Get file name from NODE, or return a default filename in directory DIR."
  (unless dir (setq dir org-roam-directory))
  (or (org-roam-node-file node)
      (expand-file-name (concat "%<%Y%m%d%H%M%S>-" (org-roam-node-slug node) ".org")
                        dir)))

(defun org-roam-node-file-maybe-pick-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 (concat "%<%Y%m%d%H%M%S>-" (org-roam-node-slug node) ".org")
                        (read-directory-name "Directory: " org-roam-directory))))


(after! org-roam
  (setq org-roam-capture-templates
        '(("d" "Default" plain "%?"
           :target (file+head "${file-maybe-pick-dir}"
                              "#+title: ${title}")
           :unnarrowed t))
        org-roam-dailies-capture-templates
        '(("d" "Default" entry "* %?"
           :target (file+head "%<%Y-%m-%d>.org"
                              "#+title: %<%Y-%m-%d>")))))

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 Templates

I don't use these very often currently, and am reconsidering whether I should rework them entirely. These might change soon!

(defun ~/org-project-find-heading ()
  "Find heading in org project file."
  (beginning-of-buffer)
  ;; (unless (string-match-p "\\`\\s-*$" (thing-at-point 'line))
  ;;   (insert "\n")
  ;;   (beginning-of-buffer))
  (when (y-or-n-p "Insert project at heading? ")
    (require 'consult-org)
    ;; Prevent consult from trying to recenter the window
    ;; after capture has already hidden the buffer
    (let (consult-after-jump-hook)
      (consult--read
       (consult--slow-operation "Collecting headings..."
         (or (consult-org--headings nil "-project" nil)
             (user-error "No headings")))
       :prompt "Heading: "
       :category 'consult-org-heading
       :sort nil
       :require-match t
       :history '(:input consult-org--history)
       :narrow (consult-org--narrow)
       :state (consult--jump-state)
       :group nil
       :lookup #'consult--lookup-candidate))))

(after! org
  (setq org-capture-templates
        '(("t" "Task")
          ("tt" "Task" entry (file+headline "events.org" "Tasks")
           "* TODO %?" :empty-lines 1)
          ("td" "Task with Deadline" entry (file+headline "events.org" "Tasks")
           "* TODO %?\nDEADLINE: %^{Deadline}T" :empty-lines 1)
          ("tD" "Task with Deadline (date only)" entry (file+headline "events.org" "Tasks")
           "* TODO %?\nDEADLINE: %^{Deadline}t" :empty-lines 1)
          ("ts" "Scheduled Task" entry (file+headline "events.org" "Tasks")
           "* TODO %?\nSCHEDULED: %^{Time}T" :empty-lines 1)
          ("tS" "Scheduled Task (date only)" entry (file+headline "events.org" "Tasks")
           "* TODO %?\nSCHEDULED: %^{Date}t" :empty-lines 1)
          ("e" "Event" entry (file+headline "events.org" "Events")
           "* %?\n%^T" :empty-lines 1)
          ("E" "Event (date only)" entry (file+headline "events.org" "Events")
           "* %?\n$^t" :empty-lines 1)
          ("p" "Project" entry (file+function "projects.org" ~/org-project-find-heading)
           "* PROJ %? :project:\n:PROPERTIES:\n:VISIBILITY: folded\n:END:
:LOGBOOK:\n- Created                              %U\n:END:"
           :empty-lines 1))))

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
  (setq org-agenda-span 'day
        org-agenda-start-day nil
        org-agenda-start-on-weekday 1 ; 1 = Monday

        org-agenda-sorting-strategy
        '((agenda time-up habit-down urgency-down category-up)
          (todo urgency-down time-up category-up)
          (tags urgency-down time-up category-up)
          (search category-up))

        ;; 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 %-18:c%?-12t% s")
          (todo . " %i %-18:c")
          (tags . " %i %-18:c")
          (search . " %i %-18: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 parsing 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))

The org-agenda dispatcher is occasionally useful, but most of the time when I want to open my agenda, it's to see my "preferred" view.

(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))
              (node (org-super-agenda--when-with-marker-buffer marker
                      (org-roam-node-at-point)))
              (links (~/org-roam-get-linked-nodes node tag)))
    (->> links
         (mapcar #'org-roam-node-title)
         (-interpose ", ")
         (apply #'concat prefix))))

(after! org
  (setq org-agenda-custom-commands
        '(("o" "Overview"
           ((agenda "")
            (alltodo ""
                     ((org-super-agenda-groups
                       '((:name "Next/In Progress"
                          :todo ("NEXT" "STRT" "WAIT" "HOLD"))
                         (:name "Important"
                          :priority "A"
                          :order 1)
                         (: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))))))))))


(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
      :desc "Org agenda dispatcher" ; Use shift to access full dispatcher
      "o A" #'org-agenda)

Agenda Files

I have a lot of different subdirectories and groupings in my org directory, but unfortunately directories listed in org-agenda-files aren't checked recursively! We can solve this by creating a function org-agenda-files-function to return the agenda files, then advising Org to call that function before getting the agenda files.

(defun org-agenda-files-function ()
  (require 'find-lisp)
  (find-lisp-find-files org-directory "\.org$"))

(defvar org-agenda-files-function #'org-agenda-files-function
  "The function to determine the org agenda files.")

(defadvice! ~/org-agenda-files (&rest _)
  "Set `org-agenda-files' before Org fetches them."
  :before #'org-agenda-files
  (setq org-agenda-files (funcall org-agenda-files-function)))

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"
        org-cite-csl--fallback-style-file "/home/kiana/Zotero/styles/modern-language-styles.csl"
        org-cite-global-bibliography (list (expand-file-name "library.json" org-directory))
        citar-bibliography org-cite-global-bibliography))

And we should also make it look a little prettier:

;; Make faces conform to theme
(after! org
  (custom-set-faces!
    `(org-cite :foreground ,(doom-color 'green))
    `(org-cite-key :slant italic :foreground ,(doom-color 'green))))

;; 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

Despite Emacs being my editor of choice for programming, I don't actually have a lot of configuration for programming languages. I suppose that this is because language packages tend to not need much personal configuration, as the bounds of what a language mode needs to do are typically defined by the language itself.

Dired

Dired by default spawns a new buffer for every directory, which clutters up your buffer list very quickly.

(after! dired
  (setq dired-kill-when-opening-new-dired-buffer t))

Haskell

Operators being in italics looks ugly, so let's fix that.

(after! haskell-mode
  (custom-set-faces! '(haskell-operator-face :slant normal)))

TODO 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"
        idris-repl-banner-functions
          '(idris-repl-text-banner) ; No fun allowed!!
        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 unfortunately 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.

(after! idris-mode
  (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))))

Scratch

This section is for code with little or no associated documentation. This could be because the code is:

  1. Temporary
  2. Unimportant
  3. Self-explanatory
  4. Just not really worth the time it takes to write these explanations

Org

Automate Problem List

(defvar ~/org-problem-spec-alist nil
  "An alist of regexps matching problem specs.")

(setq ~/org-problem-spec-alist
      `((,(rx (group (+ digit))
              (* space) "-" (* space)
              (group (+ digit))
              (* space) "odd")
         . ,(lambda (beg end)
              (when (cl-evenp beg) (cl-incf beg))
              (number-sequence beg end 2)))
        (,(rx (group (+ digit))
              (* space) "-" (* space)
              (group (+ digit))
              (* space) "even")
         . ,(lambda (beg end)
              (when (cl-oddp beg) (cl-incf beg))
              (number-sequence beg end 2)))
        (,(rx (group (+ digit))
              (* space) "-" (* space)
              (group (+ digit))) . number-sequence)
        (,(rx (group (+ digit))) . list)))


(defun ~/org-generate-problem-list (&rest specs)
  (interactive (s-split "," (read-from-minibuffer "Problems: ")))
  (let* ((alist ~/org-problem-spec-alist)
         (problems
          (mapcan
           (lambda (spec)
             (let* ((match
                     (or (--some (-some-> (s-match (car it) spec)
                                   cdr (cons (cdr it)))
                                 alist)
                         (user-error "Invalid problem spec \"%s\"" spec))))
               (apply (cdr match) (mapcar #'string-to-number (car match)))))
           specs)))
    (move-to-left-margin)
    (dolist (num problems)
      (insert (format "- [ ] %d\n" num)))))

1

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.

2

Where by "simple" I mean that it took me multiple hours of combing through Org's source code in order to find the multiple places(???) where this behavior was implemented and to figure out how to modify it. At least the final code is short!