My Emacs configuration
Package management
Load straight
straight is a purely functional package manager for Emacs. It enables 100% reproducible package management and makes editing packages very easy!
Initialization code taken directly from https://github.com/radian-software/straight.el#getting-started
(defvar bootstrap-version)
(let ((bootstrap-file
(expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
(bootstrap-version 6))
(unless (file-exists-p bootstrap-file)
(with-current-buffer
(url-retrieve-synchronously
"https://raw.githubusercontent.com/radian-software/straight.el/develop/install.el"
'silent 'inhibit-cookies)
(goto-char (point-max))
(eval-print-last-sexp)))
(load bootstrap-file nil 'nomessage))
Load use-package
use-package is a tool to declaratively specify package configuration. This increases performance (not everything is loaded at once) and tidiness. use-package integrates with straight to fetch packages.
(straight-use-package 'use-package)
(use-package straight
:custom (straight-use-package-by-default t))
Custom Functions
split-window fix
The default behaviour of the split-window functions is to just split, but not to select the new window. These functions are a fix for that. They also balance the window layout.
(defun my/split-switch-below ()
"Split and switch to window below."
(interactive)
(split-window-below)
(balance-windows)
(other-window 1))
(defun my/split-switch-right ()
"Split and switch to window on the right."
(interactive)
(split-window-right)
(balance-windows)
(other-window 1))
bspwm-like window splitting
Automatically selects the next split direction. Inspired by this post: https://www.reddit.com/r/tmux/comments/j7fcr7/tiling_in_tmux_as_in_bspwm
(defun my/autosplit ()
(interactive)
(if (greaterthan 0 (- (* 8 (window-total-width)) (* 20 (window-total-height))))
(my/split-switch-below)
(my/split-switch-right)))
Repeatably join a line
This wraps the standard function join-line
by moving to the start of the next line,
where it can be reapplied immediately to quickly join multiple lines.
(defun my/join-line ()
(interactive)
(join-line)
(forward-line 1)
(back-to-indentation))
A smarter C-a
This function allows jumping to the start of the line or the first non-whitespace character just by calling it repeatedly.
(defun my/smart-home ()
"Jump to beginning of line or first non-whitespace."
(interactive)
(let ((oldpos (point)))
(back-to-indentation)
(and (= oldpos (point)) (beginning-of-line))))
Terminal functions
Some functions for terminal interaction. The first one just opens a terminal in the current buffer. The second one checks if we are already in the terminal buffer, then it does nothing. Otherwise, it opens a terminal buffer on the right. The third function resets the terminal.
(defun my/terminal ()
"Open the terminal."
(interactive)
(eat "bash"))
(defun my/switch-to-terminal ()
"Create or switch to the terminal buffer."
(interactive)
(let ((term-win (get-buffer-window "*eat*")))
(if
(eq term-win nil)
(progn
(my/split-switch-right)
(my/terminal))
(select-window term-win))))
(defun my/eat-reset ()
"Reset eat and input newline."
(interactive)
(eat-reset)
(eat-self-input 1 ?\x15)
(eat-self-input 1 ?\n))
Dashboard
My dashboard is very minimalistic: a logo and some package load statistics. The logo should be centered both vertically and horizontally.
(defun my/dashboard ()
"Switch to a custom dashboard buffer."
(interactive)
(switch-to-buffer (get-buffer-create "*my-dashboard*"))
(read-only-mode 0)
(centaur-tabs-local-mode 1) ; this *disables* the tab bar
(setq-local mode-line-format nil
cursor-type nil)
(erase-buffer)
(dashboard-insert-banner)
(call-interactively #'beginning-of-buffer)
(newline
(/
(-
(window-height)
(count-lines (point-min) (point-max))
5)
2))
(cd "~")
(read-only-mode 1)
(message nil))
Haskell process reload
This function reloads the REPL of haskell-mode.
(defun my/haskell-reload ()
(interactive)
(haskell-process-file-loadish
"reload" t
(or haskell-interactive-previous-buffer (current-buffer))))
Open today’s Org file
This function lets the user select a project folder and opens the Org file with the current ISO 8601 date as the name.
(defun my/todays-org-file (directory)
"Opens the Org file for today in DIRECTORY.
It has the filename year-month-day.org"
(interactive "Ddirectory: ")
(let* ((date (calendar-current-date))
(month (car date))
(day (cadr date))
(year (caddr date))
(file (format "%04d-%02d-%02d.org" year month day)))
(find-file (expand-file-name file directory))))
General configuration
Some modes
We don’t want:
- a blinking cursor
- a menu, scroll, and tool bar
(blink-cursor-mode 0)
(menu-bar-mode 0)
(scroll-bar-mode 0)
(tool-bar-mode 0)
We want:
- to automatically reload a buffer when its corresponding file changes
- the current line to be highlighted
- pretty symbols
(global-auto-revert-mode 1)
(global-hl-line-mode 1)
(global-prettify-symbols-mode 1)
More recentf items
(setq recentf-max-saved-items 100)
Spell checking
Aspell is a modern replacement for ispell with full UTF-8 support.
(setq ispell-program-name "aspell"
ispell-dictionary "de_DE")
More miscellaneous settings
No startup screen (we have our own dashboard). No initial message in the scratch buffer. No bell, dialogs or long yes-or-no questions. And finally, no “when done with this frame…” message in emacsclient frames.
(setq inhibit-startup-screen t
initial-scratch-message ""
ring-bell-function 'ignore
use-dialog-box nil)
(defalias 'yes-or-no-p 'y-or-n-p)
(use-package server :custom (server-client-instructions nil))
Appearance
Theme
Gruvbox medium dark is the supreme colorscheme and I will fight anyone who dare says otherwise. Link to repo
(use-package gruvbox-theme
:custom (custom-safe-themes '("046a2b81d13afddae309930ef85d458c4f5d278a69448e5a5261a5c78598e012" default))
:config (load-theme 'gruvbox-dark-medium))
Font
We use Iosevka as a basis for Nerd Fonts
(defvar my/default-font "Iosevka NFM")
(set-face-attribute 'default nil :font my/default-font)
Transparency
Since I use emacs-pgtk-29, this works perfectly!
(push '(alpha-background . 50) default-frame-alist)
Tab width
4 spaces by default.
(setq-default tab-width 4)
Line numbers
We use relative line numbers because they make relative jumps easier (no need to type the full line number, two digits are always enough).
(use-package display-line-numbers
:custom (display-line-numbers-type 'relative)
:config
(set-face-foreground 'line-number "#ebdbb2")
(set-face-background 'line-number nil)
(global-display-line-numbers-mode 1))
Tab bar
centaur-tabs creates a nice tab bar at the top of a window. It groups buffers by type and project, has a “modified” indicator and other goodies.
(use-package centaur-tabs
:custom
(centaur-tabs-cycle-scope 'tabs)
(centaur-tabs-modified-marker "●")
(centaur-tabs-set-bar 'under)
(centaur-tabs-show-new-tab-button nil)
(centaur-tabs-set-close-button nil)
(centaur-tabs-set-icons t)
(centaur-tabs-set-modified-marker t)
(centaur-tabs-style "bar")
(x-underline-at-descent-line 1)
:config
(centaur-tabs-mode 1)
(centaur-tabs-change-fonts my/default-font 100)
(centaur-tabs-headline-match))
Icons
All the icons for our tab bar!
(use-package all-the-icons
:custom
(all-the-icons-fonts-subdirectory "all-the-icons"))
Modeline
We use telephone-line, a pretty simple custom modeline.
(use-package telephone-line
:custom
(telephone-line-lhs
'((accent . (telephone-line-vc-segment
telephone-line-process-segment))
(nil . (telephone-line-project-segment
telephone-line-buffer-segment))))
:config (telephone-line-mode 1))
More dashboard setup
While the my/dashboard
function sets up the buffer,
this configuration describes the actual contents of the dashboard.
This uses the dashboard package.
(use-package dashboard
:custom
(dashboard-banner-logo-title "Welcome to Emacs!")
(dashboard-startup-banner (expand-file-name "splash.png" user-emacs-directory))
:config
(set-face-attribute 'dashboard-banner-logo-title nil :height 200))
(add-hook 'after-init-hook #'my/dashboard)
Selection and completion interface
vertico is a modern and minimalistic completion UI.
(use-package vertico
:custom
(vertico-count 30)
(vertico-cycle t)
:config (vertico-mode 1))
Better syntax highlighting
With tree-sitter, much more complex syntax highlighting is possible, even when we don’t have a language-specific mode installed!
;; better syntax highlighting
(use-package tree-sitter
:config (global-tree-sitter-mode 1)
:hook (tree-sitter-after-on . tree-sitter-hl-mode))
(use-package tree-sitter-langs)
Indent guides
A visual representation of where we are in an indented structure. highlight-indent-guides is very adaptive and thus a perfect fit for languages with weird, dynamic indentation (looking at you, Haskell).
;; indent guides
(use-package highlight-indent-guides
:custom (highlight-indent-guides-responsive 'stack)
:hook (prog-mode . highlight-indent-guides-mode))
Visible whitespace
I want to see tabs and trailing whitespace.
;; show whitespace
(use-package whitespace
:config (global-whitespace-mode 1)
:custom (whitespace-style '(face tab-mark trailing missig-newline-at-eof)))
Colored strings
With rainbow-mode, color strings like #bb77ff get a background of their color.
(use-package rainbow-mode
:config
(define-globalized-minor-mode my/global-rainbow-mode rainbow-mode
(lambda () (rainbow-mode 1)))
(my/global-rainbow-mode))
Colored parentheses
We need more rainbows. Or, in this case, gruv-bows? Link to repo
(use-package rainbow-delimiters
:custom (rainbow-delimiters-max-face-count 6)
:config
(set-face-foreground 'rainbow-delimiters-depth-1-face "#cc241d")
(set-face-foreground 'rainbow-delimiters-depth-2-face "#98971a")
(set-face-foreground 'rainbow-delimiters-depth-3-face "#d79921")
(set-face-foreground 'rainbow-delimiters-depth-4-face "#458588")
(set-face-foreground 'rainbow-delimiters-depth-5-face "#b16286")
(set-face-foreground 'rainbow-delimiters-depth-6-face "#689d6a")
(define-globalized-minor-mode my/global-raindow-delims-mode rainbow-delimiters-mode
(lambda () (rainbow-delimiters-mode 1)))
(my/global-raindow-delims-mode 1))
Smooth scrolling
Default emacs “scrolling” behaviour sucks tbh.
(use-package smooth-scrolling
:config (smooth-scrolling-mode 1))
Popup control
Popup windows can quickly become annoying. The popwin package allows closing them with just C-g.
(use-package popwin
:config
;;(push "*undo-tree*" popwin:special-display-config)
;;(push "*Help*" popwin:special-display-config)
(push "*Backtrace*" popwin:special-display-config)
(push "*hoogle*" popwin:special-display-config)
(push '("^[*]" :regex t) popwin:special-display-config)
(popwin-mode 1))
Temporary files
Emacs leaves a lot of temporary files lying around, such as backups and autosaves. We shove all of them in a single directory next to the Emacs configuration.
(defvar my/temp-dir (concat user-emacs-directory "temp/"))
(setq backup-directory-alist `(("." . ,my/temp-dir))
auto-save-file-name-transforms `((".*" ,my/temp-dir t))
auto-save-list-file-prefix my/temp-dir)
Helpers
Fill column
For a long time, terminals were only 80 columns wide. Today, such tight space constrains no longer exist, but it is still nice to not write overly long lines. The fill column shows up as a thin bar on the 80th column.
(add-hook 'display-fill-column-indicator-mode-hook
(lambda () (set-fill-column 80)))
(global-display-fill-column-indicator-mode)
Show composite keybindings
which-key shows possible continuations of a multi-part keybind.
(use-package which-key
:custom
(which-key-idle-delay 0.5)
(which-key-idle-secondary-delay 0)
:config
(which-key-mode 1)
(which-key-setup-side-window-bottom))
Frecency-based sorting
prescient sorts possible completions by frequency and recency (“frecency”).
(use-package prescient
:config (prescient-persist-mode 1)
:custom (prescient-save-file (concat my/temp-dir "prescient-save.el")))
(use-package vertico-prescient :config (vertico-prescient-mode 1))
More selection functions
consult offers lots of search and navigation functions, such as
- selecting buffers
- grepping for text
- jumping to lines, headings or bookmarks
and many more.
(use-package consult
:init (recentf-mode 1)
:custom (completion-in-region-function #'consult-completion-in-region))
More completion information
Marginalia are annotations at the margin of page. Here, they show e.g. file permissions, function names or buffer types in the respective selection menus.
(use-package marginalia :config (marginalia-mode 1))
Git line status
git-gutter shows the modification status of lines (added, changed, removed) in the “gutter” (left side of the window).
(use-package git-gutter
:custom
(git-gutter:added-sign "+")
(git-gutter:modified-sign "~")
(git-gutter:deleted-sign "-")
(git-gutter:update-interval 2)
:config
(set-face-background 'git-gutter:added nil)
(set-face-background 'git-gutter:modified nil)
(set-face-background 'git-gutter:deleted nil)
(global-git-gutter-mode 1))
Editing
Multiple cursors
For when you need to edit EVEN MORE! Pure magic
(use-package multiple-cursors)
Direct jumps
Another pretty crazy feature: With avy you can jump to any visible text with just a few keystrokes!
(use-package avy
:custom
(avy-keys
(nconc
(number-sequence ?a ?z)
;; (number-sequence ?A ?Z)
(number-sequence ?0 ?9))))
Undo tree
Is this how timelords think? undo-tree can visualize the entire undo/redo tree of a buffer and even lets us move around in it!
(use-package undo-tree
:custom (undo-tree-history-directory-alist `(("." . ,my/temp-dir)))
:config (global-undo-tree-mode 1))
Terminal
eat: Emulate A Terminal, is by far the best terminal emulator for emacs.
It’s faster than term
, doesn’t flicker, has more features…
(use-package eat
:custom (eat-term-inside-emacs "vterm")
:bind (:map eat-semi-char-mode-map
("M-DEL" . #'eat-self-input)
("C-a" . #'eat-self-input)
("C-u" . #'eat-self-input)
("C-l" . #'my/eat-reset)))
Programming basics
Trailing whitespace cleanup
We don’t like junk on our lines.
(add-hook 'before-save-hook #'delete-trailing-whitespace)
Projects
The builtin project package is enough for my requirements.
(use-package project)
Autocompletion
company-mode adds powerful autocompletion. We want to ignore casing and show it as soon as a word is typed.
(use-package company
:hook (after-init . global-company-mode)
:custom
(company-dabbrev-downcase nil)
(company-dabbrev-ignore-case t)
(company-idle-delay 0)
(company-minimum-prefix-length 1)
(company-show-numbers t))
Language server support
lsp-mode integrates into installed language servers. We start them deferred, this reduces peak load.
(use-package lsp-mode
:custom
(eldoc-idle-delay 0)
(lsp-headerline-breadcrumb-enable nil)
(lsp-idle-delay 0)
(lsp-inlay-hint-enable t)
(lsp-log-io nil)
(read-process-output-max (* 1024 1024))
:hook
(c-mode . lsp-deferred)
(elixir-mode . lsp-deferred)
(gleam-mode . lsp-deferred)
(go-mode . lsp-deferred)
(haskell-mode . lsp-deferred)
(javascript-mode . lsp-deferred)
(nix-mode . lsp-deferred)
(python-mode . lsp-deferred)
(typescript-mode . lsp-deferred))
(use-package lsp-ui
:custom
(lsp-ui-sideline-show-code-actions t)
(lsp-ui-sideline-show-diagnostics t)
(lsp-ui-sideline-show-hover nil)
(lsp-ui-sideline-delay 0)
(lsp-ui-doc-delay 0)
(lsp-ui-doc-show-with-cursor t))
xref setup
Consult provices a selection function for xref. We also disable the symbol selection in xref-find-references.
(setq xref-show-xrefs-function #'consult-xref
xref-show-definitions-function #'consult-xref
xref-prompt-for-identifier nil)
Formatting
Format all the code! Automatic formatting on save. For Haskell, I am currently using stylish-haskell, which is not the default setting.
(use-package format-all
:hook (prog-mode . format-all-mode)
(format-all-mode . format-all-ensure-formatter)
:config
(setq-default format-all-formatters '(("Haskell" stylish-haskell)
("HTML" prettier))))
EditorConfig
EditorConfig automatically loads basic code formatting rules from a project’s rule file. The Emacs plugin is here.
(use-package editorconfig :config (editorconfig-mode 1))
Error checking
Flycheck provides on-the-fly syntax & error checking.
(use-package flycheck
:custom (flycheck-display-errors-delay 0)
:config (global-flycheck-mode 1))
Snippets
Yasnippet is a template/snippet system for emacs. It is required by some language’s autocompletion to correctly fill in function arguments and such things.
(use-package yasnippet :config (yas-global-mode 1))
highlighting
hl-todo highlights TODO and some other keywords.
(use-package hl-todo :config (global-hl-todo-mode 1))
Electricity
Automatic indentation and completion of pair characters (brackets, quotation marks, …). Emacs calls this behaviour Electricity.
(electric-indent-mode 1)
(electric-pair-mode 1)
Direnv integration
direnv automatically loads project environments. Together with my nix-direnv setup on NixOS (dotfiles here), this loads entire Nix flakes and enables Emacs to use the packages declared within.
(use-package direnv
:config (direnv-mode 1)
:custom (direnv-always-show-summary nil))
Languages
Lisp
We use two packages for lisp:
- lisp-extra-font-lock highlights local bindings and quoted expressions
- parinfer makes writing Lisp easier by automatically adjusting parentheses and indentation
(put 'if 'lisp-indent-function 'defun) ; indent if normally
(use-package lisp-extra-font-lock :config (lisp-extra-font-lock-global-mode 1))
(use-package parinfer-rust-mode
:hook emacs-lisp-mode
:custom
(parinfer-rust-library-directory my/temp-dir)
(parinfer-rust-auto-download t))
Some problems due to parinfer
The magic of parinfer clashes with some other automatic adjustment modes, such as format-all-mode and the electric modes. Therefore, they need to be disabled.
(add-hook
'emacs-lisp-mode-hook
#'(lambda ()
(format-all-mode 0)
(indent-tabs-mode 0)
(electric-indent-local-mode 0)
(electric-pair-local-mode 0)))
C
Indents are 4 spaces wide.
(setq c-basic-offset 4)
Haskell
Define hotkeys for Haskell and its REPL and enable automatic reload on save. Link to repo
(use-package haskell-mode
:bind (:map haskell-mode-map
("C-c C-h" . #'hoogle)
("C-c C-p" . #'haskell-interactive-switch))
:hook
(haskell-mode . (lambda () (add-hook 'after-save-hook #'my/haskell-reload)))
(haskell-interactive-mode
. (lambda ()
(bind-key "C-a" #'haskell-interactive-mode-beginning 'haskell-interactive-mode-map)
(bind-key "C-l" #'haskell-interactive-mode-clear 'haskell-interactive-mode-map)
(bind-key "C-n" #'haskell-interactive-mode-history-next 'haskell-interactive-mode-map)
(bind-key "C-p" #'haskell-interactive-mode-history-previous 'haskell-interactive-mode-map)
(bind-key "C-r" #'my/haskell-reload 'haskell-interactive-mode-map))))
(use-package lsp-haskell)
Go
Nothing fancy here. Link to repo
(use-package go-mode)
Rust
Instead of the official rust-mode, we use rustic. It wraps rust-mode with more features and provides automatic lsp-mode integration.
(use-package rustic
:custom (lsp-rust-analyzer-cargo-watch-command "clippy"))
Elixir
(use-package elixir-mode)
Idris 2
(use-package idris2-mode
:straight (:type git :host github :repo "idris-community/idris2-mode"))
Gleam
(use-package tree-sitter-indent)
(use-package gleam-mode
:straight (:type git :host github :repo "gleam-lang/gleam-mode"
:files ("*.el" "tree-sitter-gleam")))
HTML
We need to explicitly set the indentation here again, since it uses a custom variable. sgml-mode is a builtin mode.
(use-package sgml-mode
:custom (sgml-basic-offset 4))
Typescript
(use-package typescript-mode)
Org
The language this document is written in! We enable indentation of text under headers and syntax highlighting in the HTML export with htmlize.
(add-hook 'org-mode-hook #'org-indent-mode)
(use-package htmlize)
Typst
A modern typesetting language.
(use-package typst-mode
:straight (:type git :host github :repo "Ziqi-Yang/typst-mode.el"))
Nix
Nothing fancy here too. Link to repo
(use-package nix-mode)
Structured data
JSON and YAML are data serialization languages (they describe data, not code).
(use-package json-mode)
(use-package yaml-mode)
Keybindings
A helper
To always override existing keybinds in some modes with my own,
I have designed this little helper macro.
It allows me to write my keybinds as one huge expression
instead of many separate calls to bind-key*
.
(defmacro my/bind-keys* (&rest body)
"Globally bind all keys.
BODY: a list of alternating key-function arguments."
`(progn
,@(cl-loop
while body collecting
`(bind-key* ,(pop body) ,(pop body)))))
Principles
- When a modifier key is pressed, it is held for the rest of the keybind
- C-x is for general actions
- C-c is for code actions.
- Very important actions have no prefix, they are a single hotkey
- Meta (Alt) roughly corresponds to a “bigger” version of the same hotkey with Control
Menus
(my/bind-keys*
"C-x C-b" #'consult-bookmark
"C-x C-f" #'find-file
"C-x C-r" #'consult-ripgrep
"C-x C-i" #'consult-imenu
"C-x C-m" #'consult-minor-mode-menu
"C-x C-o" #'consult-outline
"C-x C-s" #'consult-buffer
"C-x C-u" #'undo-tree-visualize)
Window controls
(my/bind-keys*
"C-<next>" #'centaur-tabs-forward
"C-<prior>" #'centaur-tabs-backward
"C-M-<return>" #'my/autosplit
"C-x C-0" #'delete-window
"C-x C-1" #'delete-other-windows
"C-x C-2" #'my/split-switch-below
"C-x C-3" #'my/split-switch-right
"C-x C-4" #'kill-buffer-and-window)
Movement
(bind-key "C-a" #'my/smart-home)
(my/bind-keys*
"C-#" (lambda () (interactive) (select-window (next-window)))
"C-M-#" (lambda () (interactive) (select-window (previous-window)))
"M-c" #'avy-goto-char
"M-e" #'forward-word
"M-f" #'forward-to-word
"M-l" #'consult-goto-line
"M-n" #'scroll-up-command
"M-p" #'scroll-down-command
"M-s" #'consult-line)
Editing
(my/bind-keys*
"C-," #'mc/mark-previous-like-this
"C-." #'mc/mark-next-like-this
"C-<tab>" #'format-all-buffer
"C-M-<backspace>" #'my/join-line
"C-s" #'save-buffer
"C-y" #'undo-tree-redo
"C-z" #'undo-tree-undo
"M-v" #'consult-yank-from-kill-ring)
Language server actions
(my/bind-keys*
"C-c C-a" #'lsp-execute-code-action
"C-c C-d" #'lsp-ui-doc-focus-frame
"C-c C-f C-d" #'xref-find-definitions
"C-c C-f C-i" #'lsp-find-implementation
"C-c C-f C-r" #'xref-find-references
"C-c C-o" #'lsp-organize-imports
"C-c C-r" #'lsp-rename)
Text scale adjustment
(my/bind-keys*
"C-+" #'text-scale-increase
"C--" #'text-scale-decrease
"C-=" #'text-scale-mode)
Other utilities
(my/bind-keys*
"C-M-i" #'ispell-buffer
"C-x C-a" #'mark-whole-buffer
"C-x C-k" (lambda () (interactive) (kill-buffer (current-buffer)))
"C-x C-t" #'my/switch-to-terminal)
Help
(my/bind-keys*
"C-h C-b" #'describe-personal-keybindings
"C-h C-f" #'describe-function
"C-h C-k" #'describe-key
"C-h C-m" #'consult-man
"C-h C-v" #'describe-variable)
cua-mode
The Common User Access system (CUA) enables some keybindings found in standard text editors, such as
- C-c for copying a region
- C-x for cutting a region
These keybindings are only active when a region is selected, otherwise they are just prefixes to other keybindings. But for that to work, cua-mode must be enabled last. We also don’t want CUA do touch C-v, since we define it ourselves.
(setq cua-remap-control-v nil)
(cua-mode 1)
Paste
We want to use cua-paste everywhere except in the terminal.
(bind-key "C-v" #'cua-paste)
(bind-key "C-v" #'eat-yank 'eat-semi-char-mode-map)
Startup message
Send a notification when Emacs has started up.
(start-process
"startup-notify" nil
"notify-send" "emacs"
(format "Startup took %s!" (emacs-init-time)))