about summary refs log tree commit diff
path: root/tools/emacs
diff options
context:
space:
mode:
authorVincent Ambo <tazjin@google.com>2019-12-14T11·30+0000
committerVincent Ambo <tazjin@google.com>2019-12-14T11·30+0000
commit15c61c0beebeb2d9645ac7cd3736d21fe286dd3a (patch)
tree1eb5d77db796bfb38ed17877ca77d94445f92d06 /tools/emacs
parent9b35db823f5947d289a9d8b18c5ca415e3be814d (diff)
chore(emacs): Move emacs config to tools/emacs
Diffstat (limited to 'tools/emacs')
-rw-r--r--tools/emacs/.gitignore11
-rw-r--r--tools/emacs/README.md6
-rw-r--r--tools/emacs/init.el168
-rw-r--r--tools/emacs/init/bindings.el54
-rw-r--r--tools/emacs/init/custom.el52
-rw-r--r--tools/emacs/init/eshell-setup.el68
-rw-r--r--tools/emacs/init/functions.el266
-rw-r--r--tools/emacs/init/look-and-feel.el115
-rw-r--r--tools/emacs/init/mail-setup.el98
-rw-r--r--tools/emacs/init/modes.el36
-rw-r--r--tools/emacs/init/nixos.el103
-rw-r--r--tools/emacs/init/settings.el65
-rw-r--r--tools/emacs/init/term-setup.el37
13 files changed, 1079 insertions, 0 deletions
diff --git a/tools/emacs/.gitignore b/tools/emacs/.gitignore
new file mode 100644
index 000000000000..7b666905f847
--- /dev/null
+++ b/tools/emacs/.gitignore
@@ -0,0 +1,11 @@
+.smex-items
+*token*
+auto-save-list/
+clones/
+elpa/
+irc.el
+local.el
+other/
+scripts/
+themes/
+*.elc
diff --git a/tools/emacs/README.md b/tools/emacs/README.md
new file mode 100644
index 000000000000..2dd067a9101f
--- /dev/null
+++ b/tools/emacs/README.md
@@ -0,0 +1,6 @@
+emacs.d
+========
+
+This contains my emacs.d folder.
+
+I use emacs for many things.
diff --git a/tools/emacs/init.el b/tools/emacs/init.el
new file mode 100644
index 000000000000..66d38cd9fcde
--- /dev/null
+++ b/tools/emacs/init.el
@@ -0,0 +1,168 @@
+;;; init.el --- Package bootstrapping. -*- lexical-binding: t; -*-
+
+;; Packages are installed via Nix configuration, this file only
+;; initialises the newly loaded packages.
+
+(require 'use-package)
+(require 'seq)
+
+(package-initialize)
+
+;; Add 'init' folder that contains other settings to load.
+(add-to-list 'load-path (concat user-emacs-directory "init"))
+
+;; Initialise all packages installed via Nix.
+;;
+;; TODO: Generate this section in Nix for all packages that do not
+;; require special configuration.
+
+;;
+;; Packages providing generic functionality.
+;;
+
+(use-package ace-window
+  :bind (("C-x o" . ace-window))
+  :init
+  (setq aw-keys '(?f ?j ?d ?k ?s ?l ?a)
+        aw-scope 'frame))
+
+(use-package auth-source-pass :init (auth-source-pass-enable))
+
+(use-package avy
+  :bind (("M-j" . avy-goto-char)
+         ("M-p" . avy-pop-mark)
+         ("M-g g" . avy-goto-line)))
+
+(use-package browse-kill-ring)
+
+(use-package company
+  :hook ((prog-mode . company-mode))
+  :bind (:map rust-mode-map ("<tab>" . company-indent-or-complete-common)
+         :map lisp-mode-map ("<tab>" . company-indent-or-complete-common))
+  :init (setq company-tooltip-align-annotations t))
+
+(use-package dash)
+(use-package dash-functional)
+(use-package edit-server :init (edit-server-start))
+(use-package gruber-darker-theme)
+(use-package ht)
+(use-package hydra)
+(use-package idle-highlight-mode :hook ((prog-mode . idle-highlight-mode)))
+(use-package paredit :hook ((lisp-mode . paredit-mode)
+                            (emacs-lisp-mode . paredit-mode)))
+(use-package multiple-cursors)
+(use-package pinentry
+  :init
+  (setq epa-pinentry-mode 'loopback)
+  (pinentry-start))
+
+(use-package rainbow-delimiters :hook (prog-mode . rainbow-delimiters-mode))
+(use-package rainbow-mode)
+(use-package s)
+(use-package smartparens :init (smartparens-global-mode))
+(use-package string-edit)
+(use-package telephone-line) ;; configuration happens outside of use-package
+(use-package undo-tree :init (global-undo-tree-mode))
+(use-package uuidgen)
+(use-package which-key :init (which-key-mode t))
+
+;;
+;; Applications in emacs
+;;
+
+(use-package magit
+  :bind ("C-c g" . magit-status)
+  :init (setq magit-repository-directories '(("/home/vincent/projects" . 2))))
+
+(use-package password-store)
+(use-package pg)
+(use-package restclient)
+
+;;
+;; Packages providing language-specific functionality
+;;
+
+(use-package cargo
+  :hook ((rust-mode . cargo-minor-mode)
+         (cargo-process-mode . visual-line-mode))
+  :bind (:map cargo-minor-mode-map ("C-c C-c C-l" . ignore)))
+
+(use-package dockerfile-mode)
+
+(use-package eglot
+  :init (defvar rust-eglot-initialized nil)
+  :hook ((rust-mode . (lambda ()
+                        (unless rust-eglot-initialized
+                          (call-interactively #'eglot)
+                          (setq rust-eglot-initialized t))))))
+
+(use-package erlang
+  :hook ((erlang-mode . (lambda ()
+                          ;; Don't indent after '>' while I'm writing
+                          (local-set-key ">" 'self-insert-command)))))
+
+(use-package go-mode)
+(use-package haskell-mode)
+
+(use-package jq-mode
+  :init (add-to-list 'auto-mode-alist '("\\.jq\\'" . jq-mode)))
+
+(use-package kotlin-mode
+  :bind (:map kotlin-mode-map ("<tab>" . indent-relative)))
+
+(use-package markdown-mode
+  :init
+  (add-to-list 'auto-mode-alist '("\\.txt\\'" . markdown-mode))
+  (add-to-list 'auto-mode-alist '("\\.markdown\\'" . markdown-mode))
+  (add-to-list 'auto-mode-alist '("\\.md\\'" . markdown-mode)))
+
+(use-package markdown-toc)
+
+(use-package nix-mode
+  :bind (:map nix-mode-map ("<tab>" . nix-indent-line)))
+
+(use-package nginx-mode)
+(use-package rust-mode)
+(use-package terraform-mode)
+(use-package toml-mode)
+(use-package web-mode)
+(use-package yaml-mode)
+
+;;
+;; EXWM / NixOS related packages
+;;
+
+;; Configure a few basics before moving on to package-specific initialisation.
+(setq custom-file (concat user-emacs-directory "init/custom.el"))
+(load custom-file)
+
+(defvar home-dir (expand-file-name "~"))
+
+;; Seed RNG
+(random t)
+
+(defun load-other-settings ()
+  (mapc 'require '(nixos
+		   mail-setup
+                   look-and-feel
+                   functions
+                   settings
+                   modes
+                   bindings
+                   term-setup
+                   eshell-setup))
+  (telephone-line-setup)
+  (ace-window-display-mode)
+
+  (use-package sly
+    :init (setq inferior-lisp-program (concat (nix-store-path "sbcl") "/bin/sbcl"))
+    ;;(add-to-list 'company-backends 'sly-company)
+    ))
+
+
+;; Some packages can only be initialised after the rest of the
+;; settings has been applied:
+
+(add-hook 'after-init-hook 'load-other-settings)
+(put 'narrow-to-region 'disabled nil)
+(put 'upcase-region 'disabled nil)
diff --git a/tools/emacs/init/bindings.el b/tools/emacs/init/bindings.el
new file mode 100644
index 000000000000..f10869a5325f
--- /dev/null
+++ b/tools/emacs/init/bindings.el
@@ -0,0 +1,54 @@
+;; Various keybindings, most of them taken from starter-kit-bindings
+
+;; Font size
+(define-key global-map (kbd "C-+") 'text-scale-increase)
+(define-key global-map (kbd "C--") 'text-scale-decrease)
+
+;; Use regex searches by default.
+(global-set-key (kbd "\C-r") 'isearch-backward-regexp)
+(global-set-key (kbd "M-%") 'query-replace-regexp)
+(global-set-key (kbd "C-M-s") 'isearch-forward)
+(global-set-key (kbd "C-M-r") 'isearch-backward)
+(global-set-key (kbd "C-M-%") 'query-replace)
+
+;; Counsel stuff:
+(global-set-key (kbd "C-c r g") 'counsel-rg)
+
+;; imenu instead of insert-file
+(global-set-key (kbd "C-x i") 'imenu)
+
+;; Window switching. (C-x o goes to the next window)
+(windmove-default-keybindings) ;; Shift+direction
+
+;; Start eshell or switch to it if it's active.
+(global-set-key (kbd "C-x m") 'eshell)
+
+;; Start a new eshell even if one is active.
+(global-set-key (kbd "C-x M") (lambda () (interactive) (eshell t)))
+
+(global-set-key (kbd "C-x p") 'ivy-browse-repositories)
+(global-set-key (kbd "M-g M-g") 'goto-line-with-feedback)
+
+(global-set-key (kbd "C-c w") 'whitespace-cleanup)
+(global-set-key (kbd "C-c a") 'align-regexp)
+
+;; Browse URLs (very useful for Gitlab's SSH output!)
+(global-set-key (kbd "C-c b p") 'browse-url-at-point)
+(global-set-key (kbd "C-c b b") 'browse-url)
+
+;; Goodness from @magnars
+;; I don't need to kill emacs that easily
+;; the mnemonic is C-x REALLY QUIT
+(global-set-key (kbd "C-x r q") 'save-buffers-kill-terminal)
+(global-set-key (kbd "C-x C-c") 'delete-frame)
+
+;; Open Fefes Blog
+(global-set-key (kbd "C-c C-f") 'fefes-blog)
+
+;; Open a file in project:
+(global-set-key (kbd "C-c f") 'project-find-file)
+
+;; Use swiper instead of isearch
+(global-set-key "\C-s" 'swiper)
+
+(provide 'bindings)
diff --git a/tools/emacs/init/custom.el b/tools/emacs/init/custom.el
new file mode 100644
index 000000000000..4c92f0d32fc4
--- /dev/null
+++ b/tools/emacs/init/custom.el
@@ -0,0 +1,52 @@
+(custom-set-variables
+ ;; custom-set-variables was added by Custom.
+ ;; If you edit it by hand, you could mess it up, so be careful.
+ ;; Your init file should contain only one such instance.
+ ;; If there is more than one, they won't work right.
+ '(ac-auto-show-menu 0.8)
+ '(ac-delay 0.2)
+ '(aprila-nixops-path "/home/vincent/projects/langler/nixops")
+ '(aprila-release-author "Vincent Ambo <vincent@aprila.no>")
+ '(aprila-releases-path "/home/vincent/projects/langler/docs/releases")
+ '(avy-background t)
+ '(cargo-process--custom-path-to-bin "env CARGO_INCREMENTAL=1 cargo")
+ '(cargo-process--enable-rust-backtrace 1)
+ '(custom-enabled-themes (quote (gruber-darker)))
+ '(custom-safe-themes
+   (quote
+    ("d61fc0e6409f0c2a22e97162d7d151dee9e192a90fa623f8d6a071dbf49229c6" "3c83b3676d796422704082049fc38b6966bcad960f896669dfc21a7a37a748fa" "89336ca71dae5068c165d932418a368a394848c3b8881b2f96807405d8c6b5b6" default)))
+ '(elnode-send-file-program "/run/current-system/sw/bin/cat")
+ '(frame-brackground-mode (quote dark))
+ '(global-auto-complete-mode t)
+ '(intero-debug nil)
+ '(intero-global-mode t nil (intero))
+ '(intero-package-version "0.1.31")
+ '(kubernetes-commands-display-buffer-function (quote display-buffer))
+ '(magit-log-show-gpg-status t)
+ '(ns-alternate-modifier (quote none))
+ '(ns-command-modifier (quote control))
+ '(ns-right-command-modifier (quote meta))
+ '(require-final-newline (quote visit-save)))
+(custom-set-faces
+ ;; custom-set-faces was added by Custom.
+ ;; If you edit it by hand, you could mess it up, so be careful.
+ ;; Your init file should contain only one such instance.
+ ;; If there is more than one, they won't work right.
+ '(default ((t (:foreground "#e4e4ef" :background "#181818"))))
+ '(rainbow-delimiters-depth-1-face ((t (:foreground "#2aa198"))))
+ '(rainbow-delimiters-depth-2-face ((t (:foreground "#b58900"))))
+ '(rainbow-delimiters-depth-3-face ((t (:foreground "#268bd2"))))
+ '(rainbow-delimiters-depth-4-face ((t (:foreground "#dc322f"))))
+ '(rainbow-delimiters-depth-5-face ((t (:foreground "#859900"))))
+ '(rainbow-delimiters-depth-6-face ((t (:foreground "#268bd2"))))
+ '(rainbow-delimiters-depth-7-face ((t (:foreground "#cb4b16"))))
+ '(rainbow-delimiters-depth-8-face ((t (:foreground "#d33682"))))
+ '(rainbow-delimiters-depth-9-face ((t (:foreground "#839496"))))
+ '(term-color-black ((t (:background "#282828" :foreground "#282828"))))
+ '(term-color-blue ((t (:background "#96a6c8" :foreground "#96a6c8"))))
+ '(term-color-cyan ((t (:background "#1fad83" :foreground "#1fad83"))))
+ '(term-color-green ((t (:background "#73c936" :foreground "#73c936"))))
+ '(term-color-magenta ((t (:background "#9e95c7" :foreground "#9e95c7"))))
+ '(term-color-red ((t (:background "#f43841" :foreground "#f43841"))))
+ '(term-color-white ((t (:background "#f5f5f5" :foreground "#f5f5f5"))))
+ '(term-color-yellow ((t (:background "#ffdd33" :foreground "#ffdd33")))))
diff --git a/tools/emacs/init/eshell-setup.el b/tools/emacs/init/eshell-setup.el
new file mode 100644
index 000000000000..0b23c5a2d1bc
--- /dev/null
+++ b/tools/emacs/init/eshell-setup.el
@@ -0,0 +1,68 @@
+;; EShell configuration
+
+(require 'eshell)
+
+;; Generic settings
+;; Hide banner message ...
+(setq eshell-banner-message "")
+
+;; Prompt configuration
+(defun clean-pwd (path)
+  "Turns a path of the form /foo/bar/baz into /f/b/baz
+   (inspired by fish shell)"
+  (let* ((hpath (replace-regexp-in-string home-dir
+                                          "~"
+                                          path))
+         (current-dir (split-string hpath "/"))
+	 (cdir (last current-dir))
+	 (head (butlast current-dir)))
+    (concat (mapconcat (lambda (s)
+			 (if (string= "" s) nil
+			   (substring s 0 1)))
+		       head
+		       "/")
+	    (if head "/" nil)
+	    (car cdir))))
+
+(defun vcprompt (&optional args)
+  "Call the external vcprompt command with optional arguments.
+   VCPrompt"
+  (replace-regexp-in-string
+   "\n" ""
+   (shell-command-to-string (concat  "vcprompt" args))))
+
+(defmacro with-face (str &rest properties)
+  `(propertize ,str 'face (list ,@properties)))
+
+(defun prompt-f ()
+  "EShell prompt displaying VC info and such"
+  (concat
+   (with-face (concat (clean-pwd (eshell/pwd)) " ") :foreground  "#96a6c8")
+   (if (= 0 (user-uid))
+       (with-face "#" :foreground "#f43841")
+     (with-face "$" :foreground "#73c936"))
+   (with-face " " :foreground "#95a99f")))
+
+
+(setq eshell-prompt-function 'prompt-f)
+(setq eshell-highlight-prompt nil)
+(setq eshell-prompt-regexp "^.+? \\((\\(git\\|svn\\|hg\\|darcs\\|cvs\\|bzr\\):.+?) \\)?[$#] ")
+
+;; Ignore version control folders in autocompletion
+(setq eshell-cmpl-cycle-completions nil
+      eshell-save-history-on-exit t
+      eshell-cmpl-dir-ignore "\\`\\(\\.\\.?\\|CVS\\|\\.svn\\|\\.git\\)/\\'")
+
+;; Load some EShell extensions
+(eval-after-load 'esh-opt
+  '(progn
+     (require 'em-term)
+     (require 'em-cmpl)
+     ;; More visual commands!
+     (add-to-list 'eshell-visual-commands "ssh")
+     (add-to-list 'eshell-visual-commands "tail")
+     (add-to-list 'eshell-visual-commands "sl")))
+
+(setq eshell-directory-name "~/.config/eshell/")
+
+(provide 'eshell-setup)
diff --git a/tools/emacs/init/functions.el b/tools/emacs/init/functions.el
new file mode 100644
index 000000000000..8b96a0e737df
--- /dev/null
+++ b/tools/emacs/init/functions.el
@@ -0,0 +1,266 @@
+(require 's)
+;; A few handy functions I use in init.el (or not, but they're nice to
+;; have)
+
+(defun custom-download-theme (url filename)
+  "Downloads a theme through HTTP and places it in ~/.emacs.d/themes"
+
+  ;; Ensure the directory exists
+  (unless (file-exists-p "~/.emacs.d/themes")
+    (make-directory "~/.emacs.d/themes"))
+
+  ;; Adds the themes folder to the theme load path (if not already
+  ;; there)
+  (unless (member "~/.emacs.d/themes" custom-theme-load-path)
+    (add-to-list 'custom-theme-load-path "~/.emacs.d/themes"))
+
+  ;; Download file if it doesn't exist.
+
+  (let ((file
+         (concat "~/.emacs.d/themes/" filename)))
+    (unless (file-exists-p file)
+      (url-copy-file url file))))
+
+(defun custom-download-script (url filename)
+  "Downloads an Elisp script, places it in ~/.emacs/other and then loads it"
+
+  ;; Ensure the directory exists
+  (unless (file-exists-p "~/.emacs.d/other")
+    (make-directory "~/.emacs.d/other"))
+
+  ;; Download file if it doesn't exist.
+  (let ((file
+         (concat "~/.emacs.d/other/" filename)))
+    (unless (file-exists-p file)
+      (url-copy-file url file))
+
+    (load file)))
+
+(defun keychain-password (account &optional keychain)
+  "Returns the password for the account, by default it's looked up in the Login.keychain but a
+   different keychain can be specified."
+  (let ((k (if keychain keychain "Login.keychain")))
+    (replace-regexp-in-string
+     "\n" ""
+     (shell-command-to-string (concat  "security find-generic-password -w -a "
+                                       account
+                                       " "
+                                       k)))))
+
+;; This clones a git repository to 'foldername in .emacs.d
+;; if there isn't already a folder with that name
+(defun custom-clone-git (url foldername)
+  "Clones a git repository to .emacs.d/foldername"
+  (let ((fullpath (concat "~/.emacs.d/" foldername)))
+    (unless (file-exists-p fullpath)
+      (async-shell-command (concat "git clone " url " " fullpath)))))
+
+(defun load-file-if-exists (filename)
+  (if (file-exists-p filename)
+      (load filename)))
+
+(defun goto-line-with-feedback ()
+  "Show line numbers temporarily, while prompting for the line number input"
+  (interactive)
+  (unwind-protect
+      (progn
+        (setq-local display-line-numbers t)
+        (let ((target (read-number "Goto line: ")))
+          (avy-push-mark)
+          (goto-line target)))
+    (setq-local display-line-numbers nil)))
+
+
+(defun untabify-buffer ()
+  (interactive)
+  (untabify (point-min) (point-max)))
+
+(defun indent-buffer ()
+  (interactive)
+  (indent-region (point-min) (point-max)))
+
+(defun cleanup-buffer ()
+  "Perform a bunch of operations on the whitespace content of a buffer.
+Including indent-buffer, which should not be called automatically on save."
+  (interactive)
+  (untabify-buffer)
+  (delete-trailing-whitespace)
+  (indent-buffer))
+
+;; These come from the emacs starter kit
+
+(defun esk-add-watchwords ()
+  (font-lock-add-keywords
+   nil '(("\\<\\(FIX\\(ME\\)?\\|TODO\\|DEBUG\\|HACK\\|REFACTOR\\|NOCOMMIT\\)"
+          1 font-lock-warning-face t))))
+
+(defun esk-sudo-edit (&optional arg)
+  (interactive "p")
+  (if (or arg (not buffer-file-name))
+      (find-file (concat "/sudo:root@localhost:" (read-file-name "File: ")))
+    (find-alternate-file (concat "/sudo:root@localhost:" buffer-file-name))))
+
+;; Open Fefes blog
+(defun fefes-blog ()
+  (interactive)
+  (eww "https://blog.fefe.de/"))
+
+;; Open this machines NixOS config
+(defun nix-config ()
+  (interactive)
+  (find-file "/etc/nixos/configuration.nix"))
+
+;; Open the NixOS man page
+(defun nixos-man ()
+  (interactive)
+  (man "configuration.nix"))
+
+;; Open local emacs configuration
+(defun emacs-config ()
+  (interactive)
+  (dired "~/.emacs.d/"))
+
+;; Get the nix store path for a given derivation.
+;; If the derivation has not been built before, this will trigger a build.
+(defun nix-store-path (derivation)
+  (let ((expr (concat "with import <nixos> {}; " derivation)))
+    (s-chomp (shell-command-to-string (concat "nix-build -E '" expr "'")))))
+
+(defun insert-nix-store-path ()
+  (interactive)
+  (let ((derivation (read-string "Derivation name (in <nixos>): ")))
+    (insert (nix-store-path derivation))))
+
+(defun toggle-force-newline ()
+  "Buffer-local toggle for enforcing final newline on save."
+  (interactive)
+  (setq-local require-final-newline (not require-final-newline))
+  (message "require-final-newline in buffer %s is now %s"
+           (buffer-name)
+           require-final-newline))
+
+;; Helm includes a command to run external applications, which does
+;; not seem to exist in ivy. This implementation uses some of the
+;; logic from Helm to provide similar functionality using ivy.
+(defun list-external-commands ()
+  "Creates a list of all external commands available on $PATH
+  while filtering NixOS wrappers."
+  (cl-loop
+   for dir in (split-string (getenv "PATH") path-separator)
+   when (and (file-exists-p dir) (file-accessible-directory-p dir))
+   for lsdir = (cl-loop for i in (directory-files dir t)
+                        for bn = (file-name-nondirectory i)
+                        when (and (not (s-contains? "-wrapped" i))
+                                  (not (member bn completions))
+                                  (not (file-directory-p i))
+                                  (file-executable-p i))
+                        collect bn)
+   append lsdir into completions
+   finally return (sort completions 'string-lessp)))
+
+(defun run-external-command (cmd)
+    "Execute the specified command and notify the user when it
+  finishes."
+    (message "Starting %s..." cmd)
+    (set-process-sentinel
+     (start-process-shell-command cmd nil cmd)
+     (lambda (process event)
+       (when (string= event "finished\n")
+         (message "%s process finished." process)))))
+
+(defun ivy-run-external-command ()
+  "Prompts the user with a list of all installed applications and
+  lets them select one to launch."
+
+  (interactive)
+  (let ((external-commands-list (list-external-commands)))
+    (ivy-read "Command:" external-commands-list
+              :require-match t
+              :history 'external-commands-history
+              :action #'run-external-command)))
+
+(defun ivy-password-store (&optional password-store-dir)
+  "Custom version of password-store integration with ivy that
+  actually uses the GPG agent correctly."
+
+  (interactive)
+  (ivy-read "Copy password of entry: "
+            (password-store-list (or password-store-dir (password-store-dir)))
+            :require-match t
+            :keymap ivy-pass-map
+            :action (lambda (entry)
+                      (let ((password (auth-source-pass-get 'secret entry)))
+                        (password-store-clear)
+                        (kill-new password)
+                        (setq password-store-kill-ring-pointer kill-ring-yank-pointer)
+                        (message "Copied %s to the kill ring. Will clear in %s seconds."
+                                 entry (password-store-timeout))
+                        (setq password-store-timeout-timer
+                              (run-at-time (password-store-timeout)
+                                           nil 'password-store-clear))))))
+
+(defun ivy-browse-repositories ()
+  "Select a git repository and open its associated magit buffer."
+
+  (interactive)
+  (ivy-read "Repository: "
+            (magit-list-repos)
+            :require-match t
+            :sort t
+            :action #'magit-status))
+
+(defun warmup-gpg-agent (arg &optional exit)
+  "Function used to warm up the GPG agent before use. This is
+   useful in cases where there is no easy way to make pinentry run
+   in the correct context (such as when sending email)."
+  (interactive)
+  (message "Warming up GPG agent")
+  (epg-sign-string (epg-make-context) "dummy")
+  nil)
+
+(defun bottom-right-window-p ()
+  "Determines whether the last (i.e. bottom-right) window of the
+  active frame is showing the buffer in which this function is
+  executed."
+  (let* ((frame (selected-frame))
+         (right-windows (window-at-side-list frame 'right))
+         (bottom-windows (window-at-side-list frame 'bottom))
+         (last-window (car (seq-intersection right-windows bottom-windows))))
+    (eq (current-buffer) (window-buffer last-window))))
+
+(defun inferior-erlang-nix-shell ()
+  "Start an inferior Erlang process from the root of the current
+  project."
+  (interactive)
+  (inferior-erlang
+   (format "nix-shell --command erl %s" (cdr (project-current)))))
+
+(defun intero-fix-ghci-panic ()
+  "Disable deferring of out of scope variable errors, which
+  triggers a bug in the interactive Emacs REPL printing a panic
+  under certain conditions."
+
+  (interactive)
+  (let* ((root (intero-project-root))
+         (package-name (intero-package-name))
+         (backend-buffer (intero-buffer 'backend))
+         (name (format "*intero:%s:%s:repl*"
+                       (file-name-nondirectory root)
+                       package-name))
+         (setting ":set -fno-defer-out-of-scope-variables\n"))
+    (when (get-buffer name)
+      (with-current-buffer (get-buffer name)
+        (goto-char (point-max))
+        (let ((process (get-buffer-process (current-buffer))))
+          (when process (process-send-string process setting)))))))
+
+;; Brute-force fix: Ensure the setting is injected every time the REPL
+;; is selected.
+;;
+;; Upstream issue: https://github.com/commercialhaskell/intero/issues/569
+(advice-add 'intero-repl :after (lambda (&rest r) (intero-fix-ghci-panic))
+            '((name . intero-panic-fix)))
+(advice-add 'intero-repl-load :after (lambda (&rest r) (intero-fix-ghci-panic))
+            '((name . intero-panic-fix)))
+
+(provide 'functions)
diff --git a/tools/emacs/init/look-and-feel.el b/tools/emacs/init/look-and-feel.el
new file mode 100644
index 000000000000..3d480bd5f43e
--- /dev/null
+++ b/tools/emacs/init/look-and-feel.el
@@ -0,0 +1,115 @@
+;;; -*- lexical-binding: t; -*-
+
+;; Hide those ugly tool bars:
+(tool-bar-mode 0)
+(scroll-bar-mode 0)
+(menu-bar-mode 0)
+(add-hook 'after-make-frame-functions
+          (lambda (frame) (scroll-bar-mode 0)))
+
+;; Don't do any annoying things:
+(setq ring-bell-function 'ignore)
+(setq initial-scratch-message "")
+
+;; Remember layout changes
+(winner-mode 1)
+
+;; Usually emacs will run as a proper GUI application, in which case a few
+;; extra settings are nice-to-have:
+(when window-system
+  (setq frame-title-format '(buffer-file-name "%f" ("%b")))
+  (mouse-wheel-mode t)
+  (blink-cursor-mode -1))
+
+;; Configure editor fonts
+(let ((font (format "Input Mono-%d" 12)))
+  (setq default-frame-alist `((font-backend . "xft")
+                              (font . ,font)))
+  (set-frame-font font t t))
+
+;; Display battery in mode-line's misc section on adho:
+(when (equal "adho" (system-name))
+  (setq battery-mode-line-format " %b%p%%")
+  (display-battery-mode))
+
+;; Configure telephone-line
+(defun telephone-misc-if-last-window ()
+  "Renders the mode-line-misc-info string for display in the
+  mode-line if the currently active window is the last one in the
+  frame.
+
+  The idea is to not display information like the current time,
+  load, battery levels in all buffers."
+
+  (when (bottom-right-window-p)
+      (telephone-line-raw mode-line-misc-info t)))
+
+(defun telephone-line-setup ()
+  (telephone-line-defsegment telephone-line-last-window-segment ()
+    (telephone-misc-if-last-window))
+
+  ;; Display the current EXWM workspace index in the mode-line
+  (telephone-line-defsegment telephone-line-exwm-workspace-index ()
+    (when (bottom-right-window-p)
+      (format "[%s]" exwm-workspace-current-index)))
+
+  ;; Define a highlight font for ~ important ~ information in the last
+  ;; window.
+  (defface special-highlight '((t (:foreground "white" :background "#5f627f"))) "")
+  (add-to-list 'telephone-line-faces
+               '(highlight . (special-highlight . special-highlight)))
+
+  (setq telephone-line-lhs
+        '((nil . (telephone-line-position-segment))
+          (accent . (telephone-line-buffer-segment))))
+
+  (setq telephone-line-rhs
+        '((accent . (telephone-line-major-mode-segment))
+          (nil . (telephone-line-last-window-segment
+                  telephone-line-exwm-workspace-index))
+          (highlight . (telephone-line-notmuch-counts))))
+
+  (setq telephone-line-primary-left-separator 'telephone-line-tan-left
+        telephone-line-primary-right-separator 'telephone-line-tan-right
+        telephone-line-secondary-left-separator 'telephone-line-tan-hollow-left
+        telephone-line-secondary-right-separator 'telephone-line-tan-hollow-right)
+
+  (telephone-line-mode 1))
+
+;; Auto refresh buffers
+(global-auto-revert-mode 1)
+
+;; Use clipboard properly
+(setq select-enable-clipboard t)
+
+;; Show in-progress chords in minibuffer
+(setq echo-keystrokes 0.1)
+
+;; Show column numbers in all buffers
+(column-number-mode t)
+
+;; Highlight currently active line
+(global-hl-line-mode t)
+
+(defalias 'yes-or-no-p 'y-or-n-p)
+(defalias 'auto-tail-revert-mode 'tail-mode)
+
+;; Style line numbers (shown with M-g g)
+(setq linum-format
+      (lambda (line)
+        (propertize
+         (format (concat " %"
+                         (number-to-string
+                          (length (number-to-string
+                                   (line-number-at-pos (point-max)))))
+                         "d ")
+                 line)
+         'face 'linum)))
+
+;; Display tabs as 2 spaces
+(setq tab-width 2)
+
+;; Don't wrap around when moving between buffers
+(setq windmove-wrap-around nil)
+
+(provide 'look-and-feel)
diff --git a/tools/emacs/init/mail-setup.el b/tools/emacs/init/mail-setup.el
new file mode 100644
index 000000000000..1700ccddd37d
--- /dev/null
+++ b/tools/emacs/init/mail-setup.el
@@ -0,0 +1,98 @@
+(require 'notmuch)
+(require 'counsel-notmuch)
+
+(global-set-key (kbd "C-c m") 'notmuch-hello)
+(global-set-key (kbd "C-c C-m") 'counsel-notmuch)
+(global-set-key (kbd "C-c C-e n") 'notmuch-mua-new-mail)
+
+(setq notmuch-cache-dir (format "%s/.cache/notmuch" (getenv "HOME")))
+(make-directory notmuch-cache-dir t)
+
+;; Cache addresses for completion:
+(setq notmuch-address-save-filename (concat notmuch-cache-dir "/addresses"))
+
+;; Don't spam my home folder with drafts:
+(setq notmuch-draft-folder "drafts") ;; relative to notmuch database
+
+;; Mark things as read when archiving them:
+(setq notmuch-archive-tags '("-inbox" "-unread" "+archive"))
+
+;; Show me saved searches that I care about:
+(setq notmuch-saved-searches
+      '((:name "inbox" :query "tag:inbox" :count-query "tag:inbox AND tag:unread" :key "i")
+        (:name "aprila-dev" :query "tag:aprila-dev" :count-query "tag:aprila-dev AND tag:unread" :key "d")
+        (:name "gitlab" :query "tag:gitlab" :key "g")
+        (:name "sent" :query "tag:sent" :key "t")
+        (:name "drafts" :query "tag:draft")))
+(setq notmuch-show-empty-saved-searches t)
+
+;; Mail sending configuration
+(setq send-mail-function 'sendmail-send-it) ;; sendmail provided by MSMTP
+(setq notmuch-always-prompt-for-sender t)
+(setq notmuch-mua-user-agent-function
+      (lambda () (format "Emacs %s; notmuch.el %s" emacs-version notmuch-emacs-version)))
+(setq mail-host-address (system-name))
+(setq notmuch-mua-cite-function #'message-cite-original-without-signature)
+
+;; Close mail buffers after sending mail
+(setq message-kill-buffer-on-exit t)
+
+;; Ensure sender is correctly passed to msmtp
+(setq mail-specify-envelope-from t
+      message-sendmail-envelope-from 'header
+      mail-envelope-from 'header)
+
+;; Store sent mail in the correct folder per account
+(setq notmuch-maildir-use-notmuch-insert nil)
+(setq notmuch-fcc-dirs '(("mail@tazj.in" . "tazjin/Sent")
+                         ;; Not a mistake, Office365 apparently
+                         ;; renames IMAP folders (!) to your local
+                         ;; language instead of providing translations
+                         ;; in the UI m(
+                         ("vincent@aprila.no" . "aprila/Sende element")))
+
+;; I don't use drafts but I instinctively hit C-x C-s constantly, lets
+;; handle that gracefully.
+(define-key notmuch-message-mode-map (kbd "C-x C-s") #'ignore)
+
+;; MSMTP decrypts passwords using pass, but pinentry doesn't work
+;; correctly in that setup. This forces a warmup of the GPG agent
+;; before sending the message.
+;;
+;; Note that the sending function is advised because the provided hook
+;; for this seems to run at the wrong time.
+(advice-add 'notmuch-mua-send-common :before 'warmup-gpg-agent)
+
+;; Define a telephone-line segment for displaying the count of unread,
+;; important mails in the last window's mode-line:
+(defvar *last-notmuch-count-redraw* 0)
+(defvar *current-notmuch-count* nil)
+
+(defun update-display-notmuch-counts ()
+  "Update and render the current state of the notmuch unread
+  count for display in the mode-line.
+
+  The offlineimap-timer runs every 2 minutes, so it does not make
+  sense to refresh this much more often than that."
+
+  (when (> (- (float-time) *last-notmuch-count-redraw*) 30)
+    (setq *last-notmuch-count-redraw* (float-time))
+    (let* ((inbox-unread (notmuch-saved-search-count "tag:inbox and tag:unread"))
+           (devel-unread (notmuch-saved-search-count "tag:aprila-dev and tag:unread"))
+           (notmuch-count (format "I: %s; D: %s" inbox-unread devel-unread)))
+      (setq *current-notmuch-count* notmuch-count)))
+
+  (when (and (bottom-right-window-p)
+             ;; Only render if the initial update is done and there
+             ;; are unread mails:
+             *current-notmuch-count*
+             (not (equal *current-notmuch-count* "I: 0; D: 0")))
+    *current-notmuch-count*))
+
+(telephone-line-defsegment telephone-line-notmuch-counts ()
+  "This segment displays the count of unread notmuch messages in
+  the last window's mode-line (if unread messages are present)."
+
+  (update-display-notmuch-counts))
+
+(provide 'mail-setup)
diff --git a/tools/emacs/init/modes.el b/tools/emacs/init/modes.el
new file mode 100644
index 000000000000..19ed2a684349
--- /dev/null
+++ b/tools/emacs/init/modes.el
@@ -0,0 +1,36 @@
+;; Initializes modes I use.
+
+(add-hook 'prog-mode-hook 'esk-add-watchwords)
+
+;; Use auto-complete as completion at point
+(defun set-auto-complete-as-completion-at-point-function ()
+  (setq completion-at-point-functions '(auto-complete)))
+
+(add-hook 'auto-complete-mode-hook
+          'set-auto-complete-as-completion-at-point-function)
+
+;; Enable rainbow-delimiters for all things programming
+(add-hook 'prog-mode-hook 'rainbow-delimiters-mode)
+
+;; Enable Paredit & Company in Emacs Lisp mode
+(add-hook 'emacs-lisp-mode-hook 'company-mode)
+
+;; Always highlight matching brackets
+(show-paren-mode 1)
+
+;; Always auto-close parantheses and other pairs
+;; (replaced by smartparens)
+;; (electric-pair-mode)
+
+;; Keep track of recent files
+(recentf-mode)
+
+;; Easily navigate sillycased words
+(global-subword-mode 1)
+
+;; Transparently open compressed files
+(auto-compression-mode t)
+
+;; Show available key chord completions
+
+(provide 'modes)
diff --git a/tools/emacs/init/nixos.el b/tools/emacs/init/nixos.el
new file mode 100644
index 000000000000..e384e9b77db8
--- /dev/null
+++ b/tools/emacs/init/nixos.el
@@ -0,0 +1,103 @@
+;; Configure additional settings if this is one of my NixOS machines
+;; (i.e. if ExWM is required)
+;; -*- lexical-binding: t; -*-
+
+(require 's)
+(require 'f)
+(require 'dash)
+
+(defun pulseaudio-ctl (cmd)
+  (shell-command (concat "pulseaudio-ctl " cmd))
+  (message "Volume command: %s" cmd))
+
+(defun volume-mute () (interactive) (pulseaudio-ctl "mute"))
+(defun volume-up () (interactive) (pulseaudio-ctl "up"))
+(defun volume-down () (interactive) (pulseaudio-ctl "down"))
+
+(defun brightness-up ()
+  (interactive)
+  (shell-command "exec light -A 10")
+  (message "Brightness increased"))
+
+(defun brightness-down ()
+  (interactive)
+  (shell-command "exec light -U 10")
+  (message "Brightness decreased"))
+
+(defun lock-screen ()
+  (interactive)
+  (shell-command "screen-lock"))
+
+(defun generate-randr-config ()
+  (-flatten `(,(-map (lambda (n) (list n "DP2")) (number-sequence 1 7))
+              (0 "eDP1")
+              ,(-map (lambda (n) (list n "eDP1")) (number-sequence 8 9)))))
+
+(use-package exwm
+  :hook ((exwm-update-class . (lambda ()
+                                ;; Make class name the buffer name
+                                (exwm-workspace-rename-buffer exwm-class-name))))
+  :init
+  (progn
+    (require 'exwm-config)
+
+    (fringe-mode 3)
+
+    (setq exwm-workspace-number 10)
+
+    ;; 's-r': Reset
+    (exwm-input-set-key (kbd "s-r") #'exwm-reset)
+    ;; 's-w': Switch workspace
+    (exwm-input-set-key (kbd "s-w") #'exwm-workspace-switch)
+    ;; 's-N': Switch to certain workspace
+    (dotimes (i 10)
+      (exwm-input-set-key (kbd (format "s-%d" i))
+                          `(lambda ()
+                             (interactive)
+                             (exwm-workspace-switch-create ,i))))
+
+    ;; Launch applications with completion (dmenu style!)
+    (exwm-input-set-key (kbd "s-d") #'ivy-run-external-command)
+    (exwm-input-set-key (kbd "s-p") #'ivy-password-store)
+    (exwm-input-set-key (kbd "C-s-p") '(lambda ()
+                                         (interactive)
+                                         (ivy-password-store "~/.aprila-secrets")))
+
+    ;; Add Alacritty selector to a key
+    (exwm-input-set-key (kbd "C-x t") #'counsel-switch-to-alacritty)
+
+    ;; Toggle between line-mode / char-mode
+    (exwm-input-set-key (kbd "C-c C-t C-t") #'exwm-input-toggle-keyboard)
+
+    ;; Volume keys
+    (exwm-input-set-key (kbd "<XF86AudioMute>") #'volume-mute)
+    (exwm-input-set-key (kbd "<XF86AudioRaiseVolume>") #'volume-up)
+    (exwm-input-set-key (kbd "<XF86AudioLowerVolume>") #'volume-down)
+
+    ;; Brightness keys
+    (exwm-input-set-key (kbd "<XF86MonBrightnessDown>") #'brightness-down)
+    (exwm-input-set-key (kbd "<XF86MonBrightnessUp>") #'brightness-up)
+    (exwm-input-set-key (kbd "<XF86Display>") #'lock-screen)
+
+    ;; Line-editing shortcuts
+    (exwm-input-set-simulation-keys
+     '(([?\C-d] . delete)
+       ([?\C-w] . ?\C-c)))
+
+    ;; Enable EXWM
+    (exwm-enable)
+
+    ;; Show time in the mode line
+    (display-time-mode)
+
+    ;; Configure xrandr when running on laptop
+    (when (equal (shell-command-to-string "hostname") "adho\n")
+      (require 'exwm-randr)
+      (setq exwm-randr-workspace-output-plist (generate-randr-config))
+      (exwm-randr-enable))
+
+    ;; Let buffers move seamlessly between workspaces
+    (setq exwm-workspace-show-all-buffers t)
+    (setq exwm-layout-show-all-buffers t)))
+
+(provide 'nixos)
diff --git a/tools/emacs/init/settings.el b/tools/emacs/init/settings.el
new file mode 100644
index 000000000000..2e4dedc0a535
--- /dev/null
+++ b/tools/emacs/init/settings.el
@@ -0,0 +1,65 @@
+(require 'prescient)
+(require 'ivy-prescient)
+(require 'uniquify)
+(require 'ivy-pass)
+
+;; Make ivy go!
+(ivy-mode 1)
+(counsel-mode 1)
+
+(setq ivy-use-virtual-buffers t)
+(setq enable-recursive-minibuffers t)
+
+;; Enable support for prescient in ivy & configure it
+(ivy-prescient-mode)
+(prescient-persist-mode)
+(add-to-list 'ivy-prescient-excluded-commands 'counsel-rg)
+
+;; Move files to trash when deleting
+(setq delete-by-moving-to-trash t)
+
+;; We don't live in the 80s, but we're also not a shitty web app.
+(setq gc-cons-threshold 20000000)
+
+(setq uniquify-buffer-name-style 'forward)
+
+; Fix some defaults
+(setq visible-bell nil
+      inhibit-startup-message t
+      color-theme-is-global t
+      sentence-end-double-space nil
+      shift-select-mode nil
+      uniquify-buffer-name-style 'forward
+      whitespace-style '(face trailing lines-tail tabs)
+      whitespace-line-column 80
+      default-directory "~"
+      fill-column 80
+      ediff-split-window-function 'split-window-horizontally)
+
+(add-to-list 'safe-local-variable-values '(lexical-binding . t))
+(add-to-list 'safe-local-variable-values '(whitespace-line-column . 80))
+
+(set-default 'indent-tabs-mode nil)
+
+;; UTF-8 please
+(setq locale-coding-system 'utf-8) ; pretty
+(set-terminal-coding-system 'utf-8) ; pretty
+(set-keyboard-coding-system 'utf-8) ; pretty
+(set-selection-coding-system 'utf-8) ; please
+(prefer-coding-system 'utf-8) ; with sugar on top
+
+;; Make emacs behave sanely (overwrite selected text)
+(delete-selection-mode 1)
+
+;; Keep your temporary files in tmp, emacs!
+(setq auto-save-file-name-transforms
+      `((".*" ,temporary-file-directory t)))
+(setq backup-directory-alist
+      `((".*" . ,temporary-file-directory)))
+
+(remove-hook 'kill-buffer-query-functions 'server-kill-buffer-query-function)
+
+;; Show time in 24h format
+(setq display-time-24hr-format t)
+
+(provide 'settings)
diff --git a/tools/emacs/init/term-setup.el b/tools/emacs/init/term-setup.el
new file mode 100644
index 000000000000..a2a71be9eeba
--- /dev/null
+++ b/tools/emacs/init/term-setup.el
@@ -0,0 +1,37 @@
+;; Utilities for Alacritty buffers.
+
+(defun open-or-create-alacritty-buffer (buffer-name)
+  "Switch to the buffer with BUFFER-NAME or create a
+  new buffer running Alacritty."
+  (let ((buffer (get-buffer buffer-name)))
+    (if (not buffer)
+        (run-external-command "alacritty")
+      (switch-to-buffer buffer))))
+
+(defun is-alacritty-buffer (buffer)
+  "Determine whether BUFFER runs Alacritty."
+  (and (equal 'exwm-mode (buffer-local-value 'major-mode buffer))
+       (s-starts-with? "Alacritty" (buffer-name buffer))))
+
+(defun counsel-switch-to-alacritty ()
+  "Switch to a (multi-)term buffer or create one."
+  (interactive)
+  (let ((terms (-map #'buffer-name
+                     (-filter #'is-alacritty-buffer (buffer-list)))))
+    (if terms
+        (ivy-read "Switch to Alacritty buffer: "
+                  (cons "New terminal" terms)
+                  :caller 'counsel-switch-to-alacritty
+                  :require-match t
+                  :action #'open-or-create-alacritty-buffer)
+      (run-external-command "alacritty"))))
+
+(defun alacritty-rename ()
+  "Rename the current terminal buffer."
+  (interactive)
+  (let* ((buffer (get-buffer (buffer-name))))
+    (if (is-alacritty-buffer buffer)
+        (rename-buffer (format "Alacritty<%s>" (read-string "New terminal name: ")))
+      (error "This function is only intended to rename Alacritty buffers."))))
+
+(provide 'term-setup)