https://github.com/42LoCo42/.dotfiles/blob/nixos/rice/emacs.nix
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))
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))
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))
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)))
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))
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))))
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))
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))
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))))
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))))
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)
(setq recentf-max-saved-items 100)
Aspell is a modern replacement for ispell with full UTF-8 support.
(setq ispell-program-name "aspell"
ispell-dictionary "de_DE")
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))
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))
We use Iosevka as a basis for Nerd Fonts
(defvar my/default-font "Iosevka NFM")
(set-face-attribute 'default nil :font my/default-font)
Since I use emacs-pgtk-29, this works perfectly!
(push '(alpha-background . 50) default-frame-alist)
4 spaces by default.
(setq-default tab-width 4)
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))
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))
All the icons for our tab bar!
(use-package all-the-icons
:custom
(all-the-icons-fonts-subdirectory "all-the-icons"))
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))
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)
vertico is a modern and minimalistic completion UI.
(use-package vertico
:custom
(vertico-count 30)
(vertico-cycle t)
:config (vertico-mode 1))
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)
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))
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)))
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))
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))
Default emacs “scrolling” behaviour sucks tbh.
(use-package smooth-scrolling
:config (smooth-scrolling-mode 1))
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))
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)
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)
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))
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))
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))
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-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))
For when you need to edit EVEN MORE! Pure magic
(use-package multiple-cursors)
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))))
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))
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)))
We don’t like junk on our lines.
(add-hook 'before-save-hook #'delete-trailing-whitespace)
The builtin project package is enough for my requirements.
(use-package project)
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))
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))
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)
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 automatically loads basic code formatting rules from a project’s rule file. The Emacs plugin is here.
(use-package editorconfig :config (editorconfig-mode 1))
Flycheck provides on-the-fly syntax & error checking.
(use-package flycheck
:custom (flycheck-display-errors-delay 0)
:config (global-flycheck-mode 1))
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))
hl-todo highlights TODO and some other keywords.
(use-package hl-todo :config (global-hl-todo-mode 1))
Automatic indentation and completion of pair characters (brackets, quotation marks, …). Emacs calls this behaviour Electricity.
(electric-indent-mode 1)
(electric-pair-mode 1)
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))
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))
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)))
Indents are 4 spaces wide.
(setq c-basic-offset 4)
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)
Nothing fancy here. Link to repo
(use-package go-mode)
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"))
(use-package elixir-mode)
(use-package idris2-mode
:straight (:type git :host github :repo "idris-community/idris2-mode"))
(use-package tree-sitter-indent)
(use-package gleam-mode
:straight (:type git :host github :repo "gleam-lang/gleam-mode"
:files ("*.el" "tree-sitter-gleam")))
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))
(use-package typescript-mode)
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)
A modern typesetting language.
(use-package typst-mode
:straight (:type git :host github :repo "Ziqi-Yang/typst-mode.el"))
Nothing fancy here too. Link to repo
(use-package nix-mode)
JSON and YAML are data serialization languages (they describe data, not code).
(use-package json-mode)
(use-package yaml-mode)
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)))))
- 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
(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)
(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)
(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)
(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)
(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)
(my/bind-keys*
"C-+" #'text-scale-increase
"C--" #'text-scale-decrease
"C-=" #'text-scale-mode)
(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)
(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)
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)
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)
Send a notification when Emacs has started up.
(start-process
"startup-notify" nil
"notify-send" "emacs"
(format "Startup took %s!" (emacs-init-time)))