— a living document —

This is my Emacs config, expressed in literate programming. That is, my Emacs config and the documentation for my Emacs config are the same file, and it's the one you're reading (source). Winter has rendered it from Org to HTML.

  1. Traditional Configuration
  2. Small Efficiencies
  3. Visuals
  4. Packages
    1. Formatting
    2. Integrations
    3. Autocompletion
    4. Splash Screen
    5. Language Support
    6. Display
    7. System Tweaks
    8. Org Mode
    9. Project management
    10. Newbie Helpers
    11. Efficiencies
    12. Modern Niceties
  5. The End

Traditional Configuration

Get the boring stuff out of the way first:

	(setq-default
	 user-full-name "Benjamin Carlsson"
	 user-mail-address "[email protected]")
	(setq-default tab-width 2)
	(tool-bar-mode -1) ; Don't show the GUI toolbar

	;; Modifier keys
	(setq-default mac-command-modifier 'meta)       ; Make Command act as Meta
	(setq-default mac-option-modifier 'super)       ; Make Option act as Super
	(setq-default mac-control-modifier 'control)    ; Make Control act as Control
	(setq-default mac-right-option-modifier 'hyper) ; Make Right Option act as Hyper
	(setq-default ns-function-modifier 'hyper)      ; Make Fn act as Hyper

	(setq-default ring-bell-function 'ignore) ; Disable the audible bell
	(setq-default sentence-end-double-space nil) ; End sentences with single spaces
	(setq-default display-line-numbers t) ; Show line numbers
	(delete-selection-mode 1) ; Delete selected text when starting to type
	(setq-default fill-column 80) ; Wrap at 80 characters
	(add-hook 'prog-mode-hook #'auto-fill-mode) ; Autowrap when coding
	(add-hook 'text-mode-hook #'auto-fill-mode) ; Autowrap when writing
	(global-hl-line-mode 1) ; Highlight cursor line

	;; Unicode everywhere
	(set-charset-priority 'unicode)
	(set-default-coding-systems 'utf-8)
	(set-terminal-coding-system 'utf-8)
	(set-keyboard-coding-system 'utf-8)
	(set-selection-coding-system 'utf-8)
	(prefer-coding-system 'utf-8)
	(setq-default buffer-file-coding-system 'utf-8)
	(setq-default default-process-coding-system '(utf-8-unix . utf-8-unix))

	(when (and (eq system-type 'darwin) (display-graphic-p)))

Small Efficiencies

Now we'll start with some of the less obvious stuff.

  (ido-mode 1) ; Autocomplete M-x among other things
  (setq-default ido-enable-flex-matching t) ; Don't require exact matches in ido-mode
  (defalias 'yes-or-no-p 'y-or-n-p) ; Allow y/n in yes/no prompts
  ;; Make C-x k kill focused buffer instead of promptingexpand-region
  (global-set-key (kbd "C-x k") #'kill-this-buffer)

The following block makes dired mouseclicks open the in same window, not a new one.

For details on why this binds mouse-2 even though we're trying to affect mouse-1, see this link.

  (eval-after-load "dired"
    '(progn
       (define-key
         dired-mode-map
         [mouse-2]
         'dired-mouse-find-file)))

Some keybinds:

  (global-set-key (kbd "C-c a") 'org-agenda)

Some keybinds that work in macOS but are overridden by Emacs's use of modifiers:

  (global-set-key (kbd "s--") (lambda () (interactive) (insert ""))) ; En dash
  (global-set-key (kbd "s-_") (lambda () (interactive) (insert ""))) ; Em dash

The last small thing: open this config file with C-c i:

  (global-set-key
   (kbd "C-c i")
   (lambda () (interactive)
     (find-file "~/.config/emacs/config.org")))

Visuals

This section deals with minimizing visual noise and making things pretty.

  (add-to-list 'default-frame-alist '(font . "JetBrains Mono" ))
  (set-face-attribute 'default t :font "JetBrains Mono" )
  ;; Delete-auto-save-files
  (setq-default delete-auto-save-files t)

  ;; Don't pollute project directories; save backup files in a central location.
  (when (not (file-exists-p "~/.cache/emacs"))
    (make-directory "~/.cache/emacs"))

  (setq-default backup-directory-alist
                '((".*" . "~/.cache/emacs")))
  (setq-default auto-save-file-name-transforms
        '((".*" "~/.cache/emacs/" t)))

  ;; Delete old backups silently.
  (setq-default delete-old-versions t)

  ;; Ligatures, to be provided by major modes.
  (global-prettify-symbols-mode +1)

  ;; Custom ligatures
  (setq-default prettify-symbols-alist
        '(("TODO" . "")
          ("BLKD" . "")        
          ("CNCL" . "")
          ("DONE" . "")
          ("->>"  . "↠")
          ("->"   . "→")
          ("<-"   . "←")
          ("<-"   . "←")
          ("=>"   . "⇒")
          ("<="   . "≤")
          (">="   . "≥")
          ;; Below are commented until/unless I start using them frequently.
  ;				("[#A]" . "")
  ;				("[#B]" . "")
  ;				("[#C]" . "")
  ;				("[ ]" . "")
  ;				("[X]" . "")
  ;				("[-]" . "")
  ;				("#+BEGIN_SRC" . "")
  ;				("#+END_SRC" . "")
  ;				(":PROPERTIES:" . "")
  ;				(":END:" . "")
  ;				("#+STARTUP:" . "")
  ;				("#+TITLE: " . "")
  ;				("#+RESULTS:" . "")
  ;				("#+NAME:" . "")
  ;				("#+ROAM_TAGS:" . "")
  ;				("#+FILETAGS:" . "")
  ;				("#+HTML_HEAD:" . "")
  ;				("#+SUBTITLE:" . "")
  ;				("#+AUTHOR:" . "")
  ;				(":Effort:" . "")
  ;				("SCHEDULED:" . "")
  ;				("DEADLINE:" . "")
          ))

Packages

The meat of it.

Formatting

The following package and associated settings deal with indentation and formatting on a language-agnostic basis.

  (straight-use-package 'ws-butler)
  (require 'ws-butler)
  (add-hook 'prog-mode-hook #'ws-butler-mode)

Some automatic indentation:

  (straight-use-package 'aggressive-indent-mode)

Integrations

Magit is the de facto Git interface in Emacs. This package surprised me with its convenience; having come from Vim I was expecting something along the lines of fugitive.vim, but Magit is far more efficient than that. It's even more efficient to open Emacs and use Magit than it is to use Git at the command line.

Once you're in a Magit buffer, you can do things like stage files with s, commit with c c, push with p p, and all of it with way more nuance than that happy path. I'm a born git add -p user, and a Magit diff buffer is basically the better version of that.

Note that Magit must be loaded before chezmoi, as chezmoi needs to load chezmoi-magit which depends on magit.

  (straight-use-package 'magit)

Chezmoi is a CLI tool unrelated to Emacs that helps manage dotfiles. It takes the rigmarole of managing them with a Git repository and adds some quality-of-life improvements to it, like chezmoi edit --apply ~/path/to/dotfile to open your file in $EDITOR, then (once closed) immediately add, commit, and push it without any further interaction.

The chezmoi Emacs package adds some of its feature set to Emacs, but don't use it unless you're already a chezmoi user.

  (straight-use-package 'chezmoi)  ; Dotfiles management
  (require 'chezmoi)

Autocompletion

The next code block sets up autocompletion with company, the most popular generic autocompletion package for Emacs. Generally, LSP servers will call out to company to display autocompletions in a minibuffer near point.

  (straight-use-package 'company)
  (setq-default company-idle-delay 0)
  (setq-default company-minimum-prefix-length 1)

GitHub Copilot has its issues, but there's nothing like autocompleting an if err != nil { return fmt.Sprintf("helpful context: %w", err) } when writing Go. There's no official Copilot package for GitHub, but zerolfx has a pretty good unofficial one.

  (straight-use-package
   '(copilot
     :type git
     :host github
     :repo "zerolfx/copilot.el"
     :files ("dist" "*.el")))
  (add-hook 'prog-mode-hook 'copilot-mode)
  (defun my/copilot-tab ()
    (interactive)
    (or (copilot-accept-completion) (indent-for-tab-command)))
  (with-eval-after-load 'copilot
    (define-key copilot-mode-map
      (kbd "<tab>")
      #'my/copilot-tab))

Splash Screen

The following code sets up the splash screen that shows when Emacs boots, which is usually an empty buffer. It pulls some info from history like recent files and projects opened, and some info from org-mode like upcoming agenda. I also configure mine here to shell out to fortune to render a random quote from my dotfiles repo.

   (straight-use-package 'dashboard)
   (setq-default dashboard-items '((recents . 5)
                           (agenda . 5)
                           (bookmarks . 5)
                           (projects . 5)
                           (registers . 5)))
   (setq-default dashboard-banner-logo-title
         (shell-command-to-string "fortune ~/.config/fortune"))
   (setq-default dashboard-startup-banner 'logo)
   (require 'dashboard)
   (dashboard-setup-startup-hook)

Language Support

This section loads various types of support for programming languages, markups, and similar.

And how can we do any of that without the miracle of LSP and an appropriate UI for it?

  (straight-use-package 'lsp-mode)
  (straight-use-package 'lsp-ui)
  (straight-use-package 'flycheck)

For Go, we need to do some work to automatically run gofmt and friends:

    (straight-use-package 'go-mode)
    (defun lsp-go-install-save-hooks ()
      (add-hook 'before-save-hook #'lsp-format-buffer t t)
      (add-hook 'before-save-hook #'lsp-organize-imports t t))
    (add-hook 'go-mode-hook #'lsp-go-install-save-hooks)
    (add-hook 'go-mode-hook #'lsp-deferred)

For YAML:

  (straight-use-package 'yaml-mode)
  (require 'yaml-mode)
  (add-to-list 'auto-mode-alist '("\\.yml\\'" . yaml-mode))
  (add-hook 'yaml-mode-hook
            '(lambda ()
               (define-key yaml-mode-map "\C-m" 'newline-and-indent)))

Finally, some support for miscellaneous languages whose packages have good enough defaults that I don't need to configure anything.

  (straight-use-package 'dockerfile-mode)
  (straight-use-package 'git-modes)
  (straight-use-package 'hcl-mode)
  (straight-use-package 'terraform-mode)

Display

A beautiful editor is important to me whether it was made in 2015 or 1976.

I started my Emacs journey with Doom and eventually migrated to vanilla, but missed the Doom themeset. Thankfully, Doom is very modular and much of their custom code is available as individual packages. I import and use doom-monokai-pro; in some way shape or form I've been using Monokai on and off for well over a decade.

  (straight-use-package 'doom-themes)
  (setq-default doom-themes-enable-bold t doom-themes-enable-italic t)
  (load-theme 'doom-monokai-pro t)

Another great Doom feature is the modeline, which brings the default Emacs mode line up to date with the modern world in terms of design and showing information in a considerate manner.

  (straight-use-package 'doom-modeline)
  (require 'doom-modeline)
  (doom-modeline-mode 1)

Marginalia adds Emacs function docstrings to the live suggestions in the M-x menu; a must-have for any beginner.

  (straight-use-package 'marginalia)
  (marginalia-mode)

And some final small tweaks:

  ;; Color-coordinate each pair of parentheses
  (straight-use-package 'rainbow-delimiters)
  (add-hook 'prog-mode-hook #'rainbow-delimiters-mode)

  ;; Colorize mentions of colors in files
  (straight-use-package 'rainbow-mode)

  ;; Show Git changes in the gutter
  (straight-use-package 'diff-hl)
  (global-diff-hl-mode)

  ;; Show trailing whitespace
  (straight-use-package  'whitespace)

System Tweaks

When Emacs is booted it inherits a copy of the environment it was created in. This is fine when you run emacs from the command line, but when you start Emacs from somewhere like the macOS dock, it's missing a lot.

The most important missing variable is $PATH, as it means Emacs can't access any CLI tools installed with Homebrew, Go, or the like. This includes Chezmoi, gopls, a modern version of Git, etc.

The following code block installs exec-path-from-shell, which fetches environment info from the shell and copies it into Emacs explicitly.

  (straight-use-package 'exec-path-from-shell)
  (when (memq window-system '(mac ns x)) (exec-path-from-shell-initialize))

highlight-indent-guides makes indentation levels visually distinct columns down down the left side of the file, making it easy to tell at a glance whether something 50 lines away is at a given indentation level.

  (straight-use-package 'highlight-indent-guides)
  (add-hook 'prog-mode-hook 'highlight-indent-guides-mode)

The following code starts the Emacs server. This allows future invocations of emacs to open in the existing instance rather than starting a new one.

  (load "server")
  (unless (server-running-p) (server-start))

mac-pseudo-daaemon goes one step further by refusing to stop the server even after the application quits. This prevents an error when invoking Emacs from the command line without the application already running, as the Emacs command line isn't integrated with macOS well enough to boot the app bundle in that case.

I have this disabled because it does this by pretending to quit the application without actually doing so, so if you actually want to quit Emacs you have to do a weird song and dance. While starting out on Emacs I'm finding that I want to completely quit it often to make sure my startup configs still work as intended, so the benefit wasn't worth the sacrifice. This may change later.

  ;; (straight-use-package 'mac-pseudo-daemon)
  ;; (mac-psuedo-daemon-mode)

Org Mode

Org Mode has already been loaded by the straight.el package in init.el (that's how the Org file you're reading was tangled into an .el file), so we don't need to do that here. Let's set up the rest of Org.

First, we'll set up some basic configuration.

  (setq-default org-directory "~/org")
  (setq-default org-default-notes-file (concat org-directory "/notes.org"))
  (setq-default org-agenda-files '("~/org/notes.org" "~/sync/org-roam"))

And clean it up visually a bit:

  ; Hide the first n-1 asterisks in level n headings
  (setq-default org-startup-indented t)

  ; Don't wrap lines in plaintext exports of Org files
  (setq-default org-export-preserve-breaks nil)
  (setq-default org-ascii-text-width 99999)

Now, for some shortcuts to skip around Org Mode.

  ;; Access org-mode index with C-c o
  (global-set-key
   (kbd "C-c o")
   (lambda ()
     (interactive)
     (find-file "~/org/notes.org")))

  ;; Drag and drop images into Org mode
  (straight-use-package 'org-download)
  (require 'org-download)
  (add-hook 'dired-mode-hook 'org-download-enable)

Capture is a feature built into Org Mode that allows quick insertion to your notes no matter what file you're currently editing. This code block sets C-c c as a capture shortcut and defines a couple of capture templates to choose from. Starting here, you'll see that my preferred way of using Org to take notes is to have a giant date tree in my main Org file that looks like this:

  * Daily log
  ** 2023
  *** 2023-02 February
  **** 2023-02-01 Wednesday
  ***** Here lie notes for this day
  ***** These notes might stay top-level
  ****** Or be nested very deeply
  ***** TODO And I'll probably have some tasks as well
  ***** DONE Including finished ones

Here's how we'll set up the templates. C-c c t to create a new TODO entry, or C-c c h to create a new generic note.

  (global-set-key (kbd "C-c c") 'org-capture)
  (setq-default org-capture-templates
        '(("t"
           "Task"
           entry
           (file+olp+datetree
            "~/org/notes.org"
            "Daily log")
           "* TODO %?")
          ("h"
           "Headline"
           entry
           (file+olp+datetree
            "~/org/notes.org"
            "Daily log")
           "* %?")
          ("m"
           "Meeting"
           entry
           (file+olp+datetree
            "~/org/notes.org"
            "Daily log")
           "* %t %? :meeting:")))

By default, the Org refile command (C-c C-w) can only refile to shallow headlines. I currently use headlines for just about every line of notes I take from small jots to tasks to actual headlines, so it's important for me to be able to refile to any depth.

(I'm trying to break this habit. I don't have the foresight to know when a note will need subnotes inside it. I'm also confused about why Org only supports tasks in headlines by default. Let me know if you can help me with either of these things.)

   (setq-default org-refile-targets
         '((nil :maxlevel . 99) (org-agenda-files :maxlevel . 99)))

Speaking of headlines, let's clean up the display of a collapsed headline a bit:

  (setq-default org-ellipsis "⤵")

We'll also set up our preferred TODO keywords, and have Org autosave our Org file whenever we update a TODO item.

  (advice-add 'org-todo :after 'org-save-all-org-buffers)
  (setq-default org-todo-keywords
        '(
          (sequence "TODO(t)" "STRT(s)" "BLKD(b)" "|" "DONE(d)" "CNCL(c)")
          (sequence "[ ](T)" "[-](S)" "[?](B)" "|" "[X](D)" "[C](C)")
          ))

We'll also add Org-roam, which is like wiki mode for Org mode:

  (straight-use-package 'org-roam)
  (straight-use-package 'org-roam-ui)
  (straight-use-package 'emacsql)
  (straight-use-package 'emacsql-sqlite)
  (setq-default org-roam-directory "~/sync/org-roam")
  (org-roam-db-autosync-mode)
  (setq-default org-roam-completion-everywhere t)

And add some more Org packages:

  ;; Various visual improvements to Org
  (straight-use-package 'org-modern)
  (add-hook 'org-mode-hook #'org-modern-mode)
  (add-hook 'org-agenda-finalize-hook #'org-modern-agenda)

It's time to get serious about the date tree. Because I'm always logging notes in a nested headline for today, I want it to be easy to get there. Capture helps us put things there, but I often want to see the whole day's notes with context, edit previous entries, etc.

First, we'll define a function datetree-dates to generate the title for today's date tree headline.

  (defun datetree-dates ()
    (let (dates
          (day (string-to-number (format-time-string "%d")))
          (month (string-to-number (format-time-string "%m")))
          (year (string-to-number (format-time-string "%Y"))))
      (dotimes (i 365)
        (push
         (format-time-string
          "%F %A"
          (encode-time 1 1 0 (- day i) month year))
         dates))
      (nreverse dates)))

Then we'll define datetree-jump to jump to that item in the current buffer's date tree.

  (defun datetree-jump ()
    (interactive)
    (let ((point (point)))
      (catch 'found (goto-char (point-max))
             (while (outline-previous-heading)
               (let* ((hl (org-element-at-point))
                      (title (org-element-property :raw-value hl)))
                 (when (member title (datetree-dates))
                   (org-show-context)
                   (setq-default point (point))
                   (throw 'found t)))))
      (goto-char point)))

We'll wrap datetree-jump with a new function we'll call open-today to open the default Org file which has that date tree in it, then call datetree-jump.

  (defun open-today () ; Open org file to today
    (interactive)
    (find-file org-default-notes-file)
    (datetree-jump))

Finally, we'll bring it all together with C-c t to open the default Org file, generate a heading for today's log if needed, and jump to it.

  ;; Jump to today in the date tree with C-c t
  (global-set-key (kbd "C-c t") 'open-today)

This last section of my Org config is still in progress, but the goal is to get my calendar and email readable and writable inside Emacs.

  ;; Required to not get prompted for Touch ID every boot
  (setq-default plstore-cache-passphrase-for-symmetric-encryption t)

  (straight-use-package 'org-gcal)
  (setq
   org-gcal-client-id
   (string-trim
    (shell-command-to-string
     "op item get 'Emacs Google Client' --fields username"))
   org-gcal-client-secret
   (string-trim
    (shell-command-to-string
     "op item get 'Emacs Google Client' --fields password"))
   org-gcal-fetch-file-alist '(("[email protected]" .  "~/org/schedule.org")))
  (require 'org-gcal)

Project management

Because Emacs runs as a daemon with any number of frames connected to it, it doesn't place the same emphasis on a working directory as editors like Vim.

This makes things a bit sticky when e.g. trying to open a new file while looking at ~/myproject/config/dev.yml; you'd generally expect the starting directory for the search to be ~/myproject, but Emacs instead starts at ~/myproject/config, not knowing the difference in significance between the two and being unable to lean on a stable working directory given that you also have ~/anotherproject/main.go open in another buffer.

projectile is a fantastic Emacs package that fixes this. At its most basic level it brings a hidden Emacs feature (project.el) into the limelight and attaches a bunch of modern quality-of-life improvements to it. It uses a combination of autodetection and prompts to establish what project a given file belongs to.

With that new relationship comes project-scoped commands and actions, like fuzzy jump-to-file, jumping between a file and its counterpart test file, closing every buffer for a project, etc.

  (straight-use-package 'projectile)
  (require 'projectile)
  (define-key projectile-mode-map (kbd "s-p") 'projectile-command-map)
  (projectile-mode +1)

Newbie Helpers

These packages help me out as a new Emacs user.

  ;; Try out packages without installing them
  (straight-use-package 'try)

  ;; Show available key sequence paths forward in minibuffer
  (straight-use-package 'which-key)
  (which-key-mode)

Efficiencies

Use C-= to smartly select based on semantics of the language being selected.

  (straight-use-package 'expand-region)
  (global-set-key (kbd "C-=") 'er/expand-region)

Use god-mode, which is like normal mode in Vim but using traditional Emacs bindings. It has these effects:

  • All C- are removed from commands (e.g. x s performs C-x C-s)
  • g modifies the next keystroke with M- (e.g. g x performs M-x)
  • G modifies the next keystroke with C-M- (e.g. G x performs C-M-x)
  • SPC prevents all of the above for the remainder of the key sequence (e.g. x SPC s performs C-x s)
  • Starting a key sequence with C- stops God mode from affecting that sequence (e.g. C-x C-s performs itself: C-x C-s)
  (straight-use-package 'god-mode)
  (require 'god-mode)
  (define-key god-local-mode-map (kbd "i") #'god-local-mode)
  (define-key god-local-mode-map (kbd ".") #'repeat)
  (global-set-key (kbd "<escape>") #'god-local-mode)
  (setq-default god-mode-enable-function-key-translation nil) ; Except function keys

  ;; Change cursor to box outside god mode, bar inside it
  (defun my-god-mode-update-cursor-type ()
    (setq-default cursor-type (if (or god-local-mode buffer-read-only) 'box 'bar)))
  (add-hook 'post-command-hook #'my-god-mode-update-cursor-type)

Modern Niceties

Emacs was written in 1976, and it takes a bit of configuration to get up to speed with modern standard practices.

savehist generically allows saving minibuffer histories (e.g. frecency data) across restarts:

  (straight-use-package 'savehist) ; Save minibuffer histories; pairs with frecency of vertico

undo-fu makes Emacs's undo feature more modern:

  (straight-use-package 'undo-fu)
  (global-unset-key (kbd "C-z"))
  (global-set-key (kbd "C-z")   'undo-fu-only-undo)
  (global-set-key (kbd "C-s-z") 'undo-fu-only-redo)
  (straight-use-package 'undo-fu-session)
  (undo-fu-session-global-mode)

We'll install orderless for completion:

  (straight-use-package 'orderless)

And vertico for more completion improvements:

(straight-use-package 'vertico)
(vertico-mode)

The End

Thanks for reading my Emacs config! Please let me know if you have any questions or improvement suggestions! [email protected]