summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorAlan Pearce2019-10-08 21:40:33 +0200
committerAlan Pearce2019-10-08 21:40:33 +0200
commit40fd4964068d0b7628b6f699dab4aec200eadb0a (patch)
treea81e6967ac8f40590a7d657c20f04ec5e2f5990a
parentee00a9bddc0a5614a00198317b55437e9b6e7fdd (diff)
downloadnixfiles-40fd4964068d0b7628b6f699dab4aec200eadb0a.tar.lz
nixfiles-40fd4964068d0b7628b6f699dab4aec200eadb0a.tar.zst
nixfiles-40fd4964068d0b7628b6f699dab4aec200eadb0a.zip
Emacs: use single configuration file, compiled by home-manager
-rw-r--r--user/emacs/.emacs.d/init.el848
-rw-r--r--user/emacs/.emacs.d/main.el839
-rw-r--r--user/modules/emacs.nix8
3 files changed, 837 insertions, 858 deletions
diff --git a/user/emacs/.emacs.d/init.el b/user/emacs/.emacs.d/init.el
index 48fc30a3..d4470242 100644
--- a/user/emacs/.emacs.d/init.el
+++ b/user/emacs/.emacs.d/init.el
@@ -1,26 +1,842 @@
-;;; init --- user init file -*- no-byte-compile: t -*-
-;;; Commentary:
-;; Entry point for Emacs init.
-;; Ensures that main init code is up-to-date by loading the newest version.
-;;
-;;; Code:
+;;; init --- user init file
 (setq inhibit-startup-echo-area-message "alan")
-(setq load-prefer-newer t
-      package-user-dir (concat "~/.emacs.d/packages/" emacs-version "/elpa")
-      package-enable-at-startup nil
-      package-archives nil)
+(setq inhibit-startup-screen t
+      initial-scratch-message ""
+      initial-major-mode 'text-mode
+      package-enable-at-startup nil)
 
 (package-initialize)
-(require 'auto-compile nil :noerror)
 
 (defun reload-user-init-file ()
   "Reload init file."
   (interactive)
   (load-file user-init-file))
 
-(when (featurep 'auto-compile)
-  (auto-compile-on-load-mode))
+(eval-when-compile
+  (require 'use-package)
+  (setq use-package-expand-minimally t))
+(setq use-package-always-demand (daemonp))
 
-(let ((gc-cons-threshold most-positive-fixnum))
-  (load (expand-file-name "main.el" user-emacs-directory) :nomessage t))
-;;; init ends here
+;;; Customize
+
+(setq custom-file "~/.emacs.d/custom.el")
+(load custom-file :noerror :nomessage)
+
+(use-package crux
+  :defer 1
+  :custom ((crux-reopen-as-root-mode nil)))
+
+;;; Styles
+
+(custom-set-variables
+ ;; I prefer an always-visible cursor.  Feels less distracting.
+ '(blink-cursor-mode nil)
+ ;; Disable all the bars, unless on OSX, in which case, keep the menu bar.
+ '(menu-bar-mode nil)
+ '(scroll-bar-mode nil)
+ '(tool-bar-mode nil))
+(set-fringe-mode '(4 . 4))
+
+;; Ring the bell sometimes, but not so often
+(setq ring-bell-function
+      (lambda ()
+        (unless (memq this-command
+                      '(isearch-abort abort-recursive-edit exit-minibuffer keyboard-quit minibuffer-keyboard-quit undo-tree-undo))
+          (ding))))
+
+(when (or (daemonp)
+           window-system)
+  (load-theme 'almost-mono-white t)
+  (if (eq window-system 'x)
+      (setq-default line-spacing 0.2))
+  (setq frame-background-mode 'light)
+  (mapc 'frame-set-background-mode (frame-list)))
+  (let ((line (face-attribute 'mode-line :underline)))
+    (set-face-attribute 'mode-line          nil :overline   line)
+    (set-face-attribute 'mode-line-inactive nil :overline   line)
+    (set-face-attribute 'mode-line-inactive nil :underline  line)
+    (set-face-attribute 'mode-line          nil :box        nil)
+    (set-face-attribute 'mode-line-inactive nil :box        nil))
+
+;;; Chrome
+(use-package minions
+  :custom ((minions-mode-line-lighter "#")
+           (minions-mode-line-delimiters nil)
+           (minions-mode t)))
+
+(setq-default mode-line-position nil
+              mode-line-mule-info nil
+              mode-line-client nil
+              mode-line-remote nil)
+
+(use-package moody
+  :config (progn
+            (setq x-underline-at-descent-line t)
+            (moody-replace-mode-line-buffer-identification)
+            (moody-replace-vc-mode)
+            (with-eval-after-load 'eyebrowse
+              (defvar eyebrowse-lighter-default
+                '(:eval (eyebrowse-mode-line-indicator)))
+              (defvar moody-replace-eyebrowse-lighter
+                '(:eval (moody-tab (eyebrowse-mode-line-indicator) 1 'up)))
+              (defun moody-replace-eyebrowse (&optional reverse)
+                (interactive "P")
+                (if (assoc 'eyebrowse-mode mode-line-misc-info)
+                (setcdr (assoc 'eyebrowse-mode mode-line-misc-info)
+                        (list (if reverse
+                                  eyebrowse-lighter-default
+                                moody-replace-eyebrowse-lighter)))
+                (push (list 'eyebrowse-mode moody-replace-eyebrowse-lighter)
+                      (cdr (last mode-line-misc-info)))))
+              (moody-replace-eyebrowse nil))))
+
+(when (eq system-type 'darwin)
+  (add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))
+  (add-to-list 'default-frame-alist '(ns-appearance . 'light)))
+
+(add-to-list 'default-frame-alist '(width . 100))
+(add-to-list 'default-frame-alist '(height . 40))
+
+;;; Dates & Times
+
+(custom-set-variables
+ '(calendar-week-start-day 1)
+ '(calendar-date-style 'iso))
+
+(defun insert-date (prefix)
+  "Insert the current date.
+With PREFIX, use British format.
+With two prefix arguments, write out the day and month name."
+  (interactive "P")
+  (let ((format (cond
+                 ((not prefix) "%Y-%m-%d")
+                 ((equal prefix '(4)) "%d/%m/%Y")
+                 ((equal prefix '(16)) "%A, %d %B %Y"))))
+    (insert (format-time-string format))))
+
+(defun insert-datetime (prefix)
+  "Insert current date and time.  With PREFIX, use ISO8601 format."
+  (interactive "P")
+  (let ((format (cond
+                 ((not prefix) "%Y-%m-%d %H:%M:%S")
+                 ((equal prefix '(4)) "%Y-%m-%dT%H:%M:%SZ"))))
+    (insert (format-time-string format))))
+
+;;; Keybindings
+
+;; I think =set-keyboard-coding-system= stops OS X from doing something
+;; annoying to add accents.  The modifier setup is to match my
+;; re-arrangement of modifiers on OSX: Cmd on the outside, then
+;; Option/alt, then Control.
+
+(when (eq system-type 'darwin)
+  (custom-set-variables
+   '(mac-option-modifier 'meta)
+   '(mac-right-option-modifier 'none)
+   '(mac-control-modifier 'control)
+   '(mac-right-control-modifier 'left)
+   '(mac-command-modifier 'super)
+   '(mac-right-command-modifier 'left)
+   '(mac-function-modifier 'hyper)))
+
+(use-package general
+  :functions (general-unbind general-define-key)
+  :config (progn
+	    (general-override-mode +1)
+            (when (eq system-type 'darwin)
+              (general-unbind "s-x"))))
+
+(use-package avy
+  :custom ((avy-all-windows nil)))
+(use-package ace-link
+  :after avy
+  :commands (ace-link-setup-default)
+  :config (ace-link-setup-default))
+
+;; Popup keybindings following a prefix automatically.
+
+(use-package which-key
+  :defer 5
+  :custom ((which-key-mode +1))
+  :config (progn
+            (which-key-setup-side-window-right-bottom)))
+
+;;; Modeline
+
+(use-package relative-buffers
+  :defer 2
+  :custom ((global-relative-buffers-mode t)))
+
+;;; Minibuffer
+
+(setq enable-recursive-minibuffers t)
+(minibuffer-depth-indicate-mode t)
+
+(use-package hydra
+  :defer 2)
+(use-package ivy
+  :config (progn
+            (ivy-mode +1)))
+(use-package ivy-hydra
+  :defer 2)
+
+(use-package smerge-mode
+  :after magit
+  :config
+  (defhydra unpackaged/smerge-hydra
+    (:color pink :hint nil :post (smerge-auto-leave))
+    "
+^Move^       ^Keep^               ^Diff^                 ^Other^
+^^-----------^^-------------------^^---------------------^^-------
+_n_ext       _b_ase               _<_: upper/base        _C_ombine
+_p_rev       _u_pper              _=_: upper/lower       _r_esolve
+^^           _l_ower              _>_: base/lower        _k_ill current
+^^           _a_ll                _R_efine
+^^           _RET_: current       _E_diff
+"
+    ("n" smerge-next)
+    ("p" smerge-prev)
+    ("b" smerge-keep-base)
+    ("u" smerge-keep-upper)
+    ("l" smerge-keep-lower)
+    ("a" smerge-keep-all)
+    ("RET" smerge-keep-current)
+    ("\C-m" smerge-keep-current)
+    ("<" smerge-diff-base-upper)
+    ("=" smerge-diff-upper-lower)
+    (">" smerge-diff-base-lower)
+    ("R" smerge-refine)
+    ("E" smerge-ediff)
+    ("C" smerge-combine-with-next)
+    ("r" smerge-resolve)
+    ("k" smerge-kill-current)
+    ("w" (lambda ()
+            (interactive)
+            (save-buffer)
+            (bury-buffer))
+     "Save and bury buffer" :color blue)
+    ("q" nil "cancel" :color blue))
+  :hook (magit-diff-visit-file . (lambda ()
+                                   (when smerge-mode
+                                     (unpackaged/smerge-hydra/body)))))
+
+(use-package swiper
+  :general ([remap isearch-forward] #'swiper-isearch))
+
+;; transition smex history to amx
+(let ((smex-save-file (concat user-emacs-directory "smex-items")))
+  (use-package amx
+    :custom ((amx-history-length 100))))
+
+(use-package counsel
+  :commands (counsel-unicode-char)
+  :general ("M-x" #'counsel-M-x))
+
+;;; Windows
+
+(defun split-window-properly (&optional window)
+    (let ((window (or window (selected-window))))
+    (or (and (window-splittable-p window)
+	     ;; Split window vertically.
+	     (with-selected-window window
+	       (split-window-below)))
+	(and (window-splittable-p window t)
+	     ;; Split window horizontally.
+	     (with-selected-window window
+	       (split-window-right))))))
+
+(setq split-window-preferred-function #'split-window-properly
+      split-height-threshold nil
+      split-width-threshold 160)
+
+(use-package eyebrowse
+  :after (evil)
+  :custom ((eyebrowse-new-workspace t)
+           (eyebrowse-mode t)
+           (eyebrowse-mode-line-left-delimiter "")
+           (eyebrowse-mode-line-right-delimiter "")
+           (eyebrowse-mode-line-separator " ")
+           (eyebrowse-mode-line-style 'always))
+  :general (:keymaps 'evil-window-map
+	    "0" #'eyebrowse-switch-to-window-config-0
+            "1" #'eyebrowse-switch-to-window-config-1
+            "2" #'eyebrowse-switch-to-window-config-2
+            "3" #'eyebrowse-switch-to-window-config-3
+            "4" #'eyebrowse-switch-to-window-config-4
+            "5" #'eyebrowse-switch-to-window-config-5
+            "6" #'eyebrowse-switch-to-window-config-6
+            "7" #'eyebrowse-switch-to-window-config-7
+            "8" #'eyebrowse-switch-to-window-config-8
+            "9" #'eyebrowse-switch-to-window-config-9)
+  :ghook ('evil-after-load-hook #'eyebrowse-setup-evil-keys))
+
+(use-package winner
+  :after evil
+  :defer 8
+  :custom ((winner-mode t)
+           (winner-boring-buffers '("*Completions*" "*Help*" "*Apropos*" "*Buffer List*" "*info*" "*Compile-Log*")))
+  :general (:keymaps 'evil-window-map
+                    "u" #'winner-undo
+                    "r" #'winner-redo
+                    "C-r" #'winner-redo))
+;;; Evil
+
+(use-package evil
+  :demand t
+  :commands (evil-mode evil-delete-buffer evil-ex-define-cmd)
+  :init (progn
+	  (defvar evil-want-integration)
+	  (defvar evil-want-keybinding)
+	  (setq evil-want-integration t
+		evil-want-keybinding nil))
+  :custom ((evil-shift-width 2)
+	   (evil-mode-line-format nil))
+  :config (evil-mode +1)
+  :general
+  (:states 'motion
+	   "C-;" #'evil-avy-goto-line)
+  (:states 'normal
+	   ";" #'evil-ex)
+  (:states '(normal motion)
+           "g s" #'evil-avy-goto-symbol-1))
+
+(use-package evil-collection
+  :after (evil)
+  :defer 3
+  :demand t
+  :commands (evil-collection-init)
+  :custom ((evil-collection-company-use-tng nil))
+  :config (progn
+	    (evil-collection-init)))
+
+(general-create-definer my-leader-def
+  :keymaps 'override
+  :states '(normal motion)
+  :prefix ",")
+
+(use-package evil-space
+  :defer 1
+  :after evil
+  :custom ((evil-space-mode t)))
+
+(use-package evil-surround
+  :after evil
+  :defer 2
+  :custom ((global-evil-surround-mode t)))
+
+(use-package evil-commentary
+  :after evil
+  :defer 2
+  :custom ((evil-commentary-mode t)))
+
+(use-package evil-magit
+  :after magit
+  :custom ((evil-magit-use-y-for-yank nil)))
+
+(use-package evil-quickscope
+  :after evil
+  :commands (evil-quickscope-mode)
+  :ghook ('(magit-mode-hook git-rebase-mode-hook) #'turn-off-evil-quickscope-mode)
+  :custom ((global-evil-quickscope-mode t)))
+
+(use-package evil-org
+  :after org
+  :commands (evil-org-set-key-theme)
+  :ghook ('org-mode-hook #'evil-org-mode)
+  :gfhook #'evil-org-set-key-theme)
+
+(use-package evil-org-agenda
+  :after org
+  :ghook ('org-agenda-mode-hook #'evil-org-agenda-set-keys))
+
+;;; Projects
+
+(use-package projectile
+  :defer 1
+  :defines projectile-command-map
+  :custom ((projectile-mode +1)
+           (projectile-completion-system 'ivy))
+  :config (progn
+            (add-to-list 'projectile-globally-ignored-files "package-lock.json")
+            (add-to-list 'projectile-globally-ignored-files "pnpm-lock.yaml")
+            (add-to-list 'projectile-project-root-files "package.json")
+            (add-to-list 'projectile-project-root-files-bottom-up "pnpm-workspace.yaml")
+            (setq projectile-project-root-files-functions '(projectile-root-local
+                                                            projectile-root-top-down
+                                                            projectile-root-bottom-up
+                                                            projectile-root-top-down-recurring))
+	    (with-eval-after-load 'evil-ex
+	      (evil-ex-define-cmd "prg" #'projectile-ripgrep)
+              (evil-ex-define-cmd "pesh[ell]" #'projectile-run-eshell))))
+
+(use-package counsel-projectile
+  :defer 1
+  :commands (counsel-projectile-switch-project
+	     counsel-projectile-rg
+	     counsel-projectile-switch-to-buffer
+             counsel-projectile-mode)
+  :general (:keymaps 'projectile-command-map
+                     "s s" #'counsel-projectile-rg
+                     "s r" #'counsel-projectile-rg)
+  :config (progn
+            (assq-delete-all #'projectile-ripgrep counsel-projectile-key-bindings)
+            (counsel-projectile-mode +1)
+	    (with-eval-after-load 'evil-ex
+	      (evil-ex-define-cmd "cprg" #'counsel-projectile-rg)
+	      (evil-ex-define-cmd "pb" #'counsel-projectile-switch-to-buffer)
+	      (evil-ex-define-cmd "psw[itch]" #'counsel-projectile-switch-project))))
+
+(use-package magit
+  :defer 10
+  :commands (magit-status magit-dispatch)
+  :custom ((magit-auto-revert-mode nil)
+           (magit-section-visibility-indicator nil)
+           (magit-diff-refine-hunk 'all)
+           (magit-display-buffer-function #'display-buffer)
+           (magit-completing-read-function #'ivy-completing-read))
+  :config (progn
+            (global-magit-file-mode +1)
+            (remove-hook 'magit-section-highlight-hook 'magit-section-highlight)
+            (remove-hook 'magit-section-highlight-hook 'magit-section-highlight-selection)
+            (remove-hook 'magit-section-highlight-hook 'magit-diff-highlight)))
+
+(eval-when-compile (require 'fringe-helper))
+(use-package git-gutter
+  :defer t)
+(use-package git-gutter-fringe
+  :defer 5
+  :config (progn
+            (global-git-gutter-mode 1)
+            ;; places the git gutter outside the margins.
+            (setq-default fringes-outside-margins nil)
+            ;; thin fringe bitmaps
+            (fringe-helper-define 'git-gutter-fr:added '(center repeated)
+              ".XXX....")
+            (fringe-helper-define 'git-gutter-fr:modified '(center repeated)
+              ".XXX....")
+            (fringe-helper-define 'git-gutter-fr:deleted '(center repeated)
+              ".XXX....")
+            (setq git-gutter-fr:side 'right-fringe)))
+
+(use-package git-messenger
+  :commands (git-messenger:popup-message)
+  :defer 10
+  :custom ((git-messenger:use-magit-popup t)))
+
+(use-package git-timemachine
+  :commands (git-timemachine))
+
+(use-package editorconfig
+  :defer 2
+  :init (progn
+          (unless (executable-find "editorconfig")
+            (warn "Missing `editorconfig' executable.")))
+  :config (editorconfig-mode +1))
+
+;;; Completion
+
+(use-package company
+  :defer 2
+  :commands (company-explicit-action-p)
+  :custom ((global-company-mode +1)
+           (company-idle-delay 0)
+           (company-show-numbers t)
+           (company-dabbrev-downcase nil)
+           (company-dabbrev-ignore-case nil)
+           (company-begin-commands '(self-insert-command))
+           (company-auto-complete #'company-explicit-action-p)
+           (company-auto-complete-chars '(?\ ?\( ?\) ?.)))
+  :general (:states 'insert
+                    "TAB" #'company-indent-or-complete-common))
+
+;; (use-package all-the-icons)
+
+(eval-when-compile (require 'subr-x))
+(eval-and-compile
+  (defun company-tabnine-load-path ()
+    (string-trim-right (shell-command-to-string "ghq list -p company-tabnine"))))
+
+(use-package company-tabnine
+  :commands (company-tabnine)
+  :after (company)
+  :load-path (lambda () (list (company-tabnine-load-path)))
+  :custom ((company-tabnine-binaries-folder "~/.local/tabnine"))
+  :general ("<M-tab>" #'company-tabnine-call-other-backends
+            "<C-tab>" #'company-tabnine-call-other-backends)
+  :init (progn
+          (add-to-list 'company-backends #'company-tabnine)))
+
+;;; Documentation
+
+(use-package eldoc
+  :defer 5
+  :custom ((global-eldoc-mode +1)
+           (eldoc-idle-delay 0.5)))
+
+(use-package eldoc-box
+  :after (eldoc eglot)
+  :custom ((eldoc-box-hover-mode +1)
+           (eldoc-box-hover-at-point-mode +1)))
+
+(use-package ehelp
+  :defer 15)
+
+(use-package helpful
+  :after ehelp
+  :general (ehelp-map
+            "k" #'helpful-key
+            "v" #'helpful-variable
+            "f" #'helpful-callable))
+
+;;; Files
+
+;;;; Auto-saving
+
+;; Auto-save everything to a temporary directory, instead of cluttering
+;; the filesystem.  I don’t want emacs-specific lockfiles, either.
+
+(setq auto-save-file-name-transforms `((".*" ,temporary-file-directory t))
+      create-lockfiles nil)
+
+;;;; Auto-reloading
+
+(use-package autorevert
+  :custom ((global-auto-revert-mode t)
+           (auto-revert-verbose t)))
+
+;;;; Backups
+
+;; I like to keep my backups out of regular folders.  I tell emacs to use
+;; a subfolder of its configuration directory for that.  Also, use the
+;; trash for deleting on OS X.
+(let ((backup-dir (expand-file-name "~/.emacs.d/backups/")))
+  (unless (file-directory-p backup-dir)
+    (make-directory backup-dir))
+  (setq backup-directory-alist `((".*" . ,backup-dir))
+        backup-by-copying-when-linked t
+        backup-by-copying-when-mismatch t))
+
+(setq delete-by-moving-to-trash t)
+
+(use-package goto-chg
+  :defer 1)
+
+;;; Directories
+
+(custom-set-variables
+ '(dired-dwim-target t)
+ '(dired-recursive-copies 'top)
+ '(dired-listing-switches "-alh")
+ '(dired-recursive-deleted (if delete-by-moving-to-trash
+                              'always
+                            'top)))
+
+(add-hook 'dired-mode-hook
+          (lambda ()
+            (dired-hide-details-mode)))
+
+(use-package dired-git-info
+  :general (:keymaps 'dired-mode-map
+                     :states 'normal
+                     ")" #'dired-git-info-mode))
+
+;;; Shells
+
+(use-package eshell
+  :defer 5
+  :commands (eshell)
+  :functions (eshell/pwd)
+  :init (progn
+          (with-eval-after-load 'evil-ex
+            (evil-ex-define-cmd "esh[ell]" #'eshell)))
+  :custom ((eshell-prompt-function (lambda ()
+                                     (concat (eshell/pwd) "\n$ ")))
+           (eshell-prompt-regexp "^[$][[:blank:]]")
+           (eshell-cmpl-cycle-completions nil)))
+
+(use-package eshell-toggle
+  :after projectile
+  :commands (eshell-toggle)
+  :custom ((eshell-toggle-use-projectile-root t)))
+
+(use-package esh-autosuggest
+  :after eshell
+  :ghook ('eshell-mode-hook))
+
+(declare-function eshell-push-command "esh-buf-stack" (CMD))
+(defun my-bind-esh-push ()
+  (general-define-key
+   :states '(normal insert)
+   :keymaps 'local
+   "M-q" #'eshell-push-command))
+
+(use-package esh-buf-stack
+  :after (eshell)
+  :ghook ('eshell-mode-hook #'my-bind-esh-push)
+  :config (setup-eshell-buf-stack))
+
+(use-package bash-completion
+  :after (eshell))
+
+(use-package fish-completion
+  :when (executable-find "fish")
+  :after (bash-completion)
+  :custom ((fish-completion-fallback-on-bash-p t))
+  :commands (global-fish-completion-mode)
+  :config (global-fish-completion-mode))
+
+(use-package esh-help
+  :after (eshell)
+  :config (setup-esh-help-eldoc))
+
+(use-package eshell-fringe-status
+  :after eshell
+  :ghook '(eshell-mode-hook))
+
+(use-package eshell-up
+  :after (eshell))
+
+(use-package shell
+  :defer t
+  :general (:keymaps 'shell-mode-map
+                     "C-d" #'comint-delchar-or-maybe-eof))
+
+(use-package comint
+  :defer t
+  :general (:keymaps 'comint-mode-map
+                     "C-c C-l" #'counsel-shell-history))
+
+;;; Editing
+
+(setq-default indent-tabs-mode nil
+              tab-always-indent 'complete)
+
+(use-package ws-butler
+  :ghook ('prog-mode-hook))
+
+;;; Major modes
+
+;;;; js
+(custom-set-variables '(js-indent-level 2)
+                      '(js-enabled-frameworks '(javascript)))
+
+;;;; typescript
+(custom-set-variables '(typescript-indent-level 2))
+(autoload 'ansi-color-apply-on-region "ansi-color")
+(defun colorise-compilation-buffer ()
+  (ansi-color-apply-on-region compilation-filter-start (point-max)))
+(add-hook 'compilation-filter-hook #'colorise-compilation-buffer)
+
+;;;; shell
+(general-add-hook 'sh-mode-hook
+                  (lambda ()
+                    (general-add-hook 'after-save-hook
+                                      #'executable-make-buffer-file-executable-if-script-p :append :local)))
+
+(add-to-list 'auto-mode-alist '("\\.env\\'" . conf-unix-mode))
+(use-package sh-script
+  :mode (("\\.zsh\\'" . shell-script-mode)
+         ("zshenv\\'" . shell-script-mode)
+         ("zshrc\\'"  . shell-script-mode))
+  :config (setq sh-shell-file "/usr/bin/env zsh"
+                sh-basic-offset 2))
+
+(add-hook 'shell-mode-hook 'ansi-color-for-comint-mode-on)
+
+;;;; make
+(general-add-hook 'makefile-mode-hook
+                  (lambda ()
+                    (setq-local indent-tabs-mode t)))
+
+;;;; nix
+(custom-set-variables '(nix-indent-function #'nix-indent-line))
+
+(use-package nix-update
+  :commands (nix-update-fetch))
+
+;;;; gitlab-ci.yml
+(with-eval-after-load 'git-gutter-fringe
+              (fringe-helper-define 'flycheck-fringe-bitmap-double-arrow '(center repeated)
+                "XXX....."))
+
+(custom-set-variables '(gitlab-ci-url "https://gitlab.satoshipay.tech"))
+(use-package gitlab-ci-mode-flycheck
+  :ghook ('gitlab-ci-mode-hook (list #'gitlab-ci-mode-flycheck-enable
+                                     #'flycheck-mode)))
+
+;;;; *ignore
+(use-package gitignore-mode
+  :mode ((".dockerignore\\'" . gitignore-mode)))
+
+;;;; kubernetes
+(custom-set-variables '(k8s-site-docs-version "v1.13"))
+
+;;;; beancount
+
+(use-package beancount
+  :load-path "~/projects/bitbucket.org/blais/beancount/editors/emacs")
+
+;;;; org
+
+(custom-set-variables '(org-ellipsis "…")
+                      `(org-directory "~/Documents/org"))
+
+(use-package org-journal
+  :commands (org-journal-new-date-entry
+	     org-journal-new-entry
+	     org-journal-new-scheduled-entry)
+  :gfhook (#'variable-pitch-mode)
+  :custom ((org-journal-date-format "%A, %d %B %Y")
+           (org-journal-dir "~/Documents/journal")))
+
+;;;; web modes (tsx, html)
+
+(use-package web-mode
+  :mode (("\\.tsx" . web-mode))
+  :custom ((web-mode-enable-auto-pairing nil)
+           (web-mode-code-indent-offset 2)
+           (web-mode-markup-indent-offset 2)
+           (web-mode-css-indent-offset 2)
+           (web-mode-style-padding 0)
+           (web-mode-script-padding 0)))
+
+;;; IDE features
+
+(fringe-helper-define 'left-vertical-bar '(center repeated)
+  "XXX.....")
+(use-package flymake
+  :custom ((flymake-error-bitmap '(left-vertical-bar compilation-error))
+           (flymake-warning-bitmap '(left-vertical-bar compilation-warning))))
+(use-package flymake-diagnostic-at-point
+  :custom ((flymake-diagnostic-at-point-diagnostic-function #'flymake-diagnostic-at-point-display-minibuffer))
+  :ghook '(flymake-mode-hook))
+
+(use-package lsp-mode
+  :ghook ('(typescript-mode-hook
+            dockerfile-mode-hook
+            yaml-mode-hook
+            js-mode-hook
+            css-mode-hook
+            scss-mode-hook
+            html-mode-hook
+            haskell-mode-hook)
+          #'lsp)
+  :functions (lsp--flymake-setup)
+  :gfhook 'lsp--flymake-setup
+  :custom ((lsp-auto-guess-root t)
+           (lsp-auto-configure nil)
+           (lsp-prefer-flymake t)
+           (lsp-enable-symbol-highlighting nil))
+  :config (progn
+            (add-to-list 'lsp-language-id-configuration '(js-mode . "javascript"))))
+
+(use-package lsp-clients
+  :after (lsp-mode))
+
+(use-package lsp-ui
+  :after lsp-mode
+  :ghook ('lsp-mode-hook)
+  :custom ((lsp-ui-sideline-enable nil)
+           (lsp-ui-doc-mode nil)
+           (lsp-enable-snippet nil)))
+
+(use-package lsp-haskell
+  :after lsp-mode)
+
+;; Inside a javascript project, it's common to install tools locally to
+;; the project.  This will allows emacs to find their executables.
+
+(use-package add-node-modules-path
+  :ghook ('(js2-mode-hook typescript-mode-hook) #'add-node-modules-path))
+
+;;;; Reformat on save
+
+(use-package prettier-js
+  :custom ((prettier-js-show-errors 'echo))
+  :ghook ('(typescript-mode-hook) #'prettier-js-mode t)
+  :config (progn
+            (if-let ((prettier_d (executable-find "prettier_d")))
+                (setq prettier-js-command prettier_d
+                      prettier-js-args '("--pkg-conf" "--parser" "typescript"))
+              (message "prettier_d is not available"))))
+
+;;; E-mail
+
+(declare-function sendmail-send-it "sendmail")
+(setq send-mail-function #'sendmail-send-it)
+
+(use-package mu4e
+  :if (executable-find "mu")
+  :commands (mu4e)
+  :init (setq mail-user-agent #'mu4e-user-agent)
+  :custom ((mu4e-completing-read-function #'ivy-completing-read)
+           (mu4e-maildir "~/mail")
+           (mu4e-context-policy 'pick-first)
+           (mu4e-update-interval 600)
+           (mu4e-change-filenames-when-moving t)
+           (mu4e-index-lazy-check t)
+           (mu4e-hide-index-messages t)
+           (mu4e-compose-format-flowed t))
+  :init (progn
+         (with-eval-after-load 'evil-ex
+           (evil-ex-define-cmd "mu[4e]" #'mu4e)))
+  :config (progn
+            (setq mu4e-contexts (list
+                                 (make-mu4e-context
+                                  :name "SatoshiPay"
+                                  :match-func (lambda (msg)
+                                                (if msg
+                                                    (mu4e-message-contact-field-matches
+                                                     msg :to ".*@satoshipay.io")
+                                                  (string-equal (system-name) "satoshipad")))
+                                  :vars '((user-mail-address . "alan@satoshipay.io")
+                                          (mu4e-sent-messages-behavior . delete)
+                                          (mu4e-drafts-folder . "/satoshipay/Drafts")
+                                          (mu4e-sent-folder . "/satoshipay/Sent Mail")
+                                          (mu4e-refile-folder . "/satoshipay/All Mail")
+                                          (mu4e-trash-folder . "/satoshipay/Bin")
+                                          (mu4e-maildir-shortcuts . (("/satoshipay/INBOX" . ?i)
+                                                                     ("/satoshipay/All Mail" . ?a)
+                                                                     ("/satoshipay/Sent Mail" . ?s)
+                                                                     ("/satoshipay/Spam" . ?p)))))))))
+
+;;; Take me to my leader
+
+(my-leader-def
+ "" nil
+ "`" #'eshell-toggle
+ "h" '(:keymap ehelp-map :package ehelp)
+ "w" '(:keymap evil-window-map :package evil)
+ "x" '(:keymap ctl-x-map)
+ "q" #'evil-delete-buffer
+ "p" projectile-command-map
+ "v" #'split-window-right
+ "o" #'other-window
+ "u" #'universal-argument
+ ";" #'counsel-M-x
+ "bb" #'ivy-switch-buffer
+ "bx" #'kill-this-buffer
+ "br" #'revert-buffer
+ "bk" #'kill-buffer
+ "dd" #'dired
+ "fs" #'save-buffer
+ "ff" #'find-file
+ "fw" #'write-file
+ "fd" #'crux-delete-file-and-buffer
+ "fr" #'crux-rename-file-and-buffer
+ "gs" #'magit-status
+ "gm" #'git-messenger:popup-message
+ "gg" #'magit-dispatch
+ "gn" #'git-gutter:next-hunk
+ "gp" #'git-gutter:previous-hunk
+ "gi" #'git-gutter:popup-hunk
+ "gs" #'git-gutter:stage-hunk
+ "go" #'git-gutter:revert-hunk
+ "gt" #'git-timemachine
+ "bi" #'ibuffer
+ "bz" #'bury-buffer
+ "iu" #'counsel-unicode-char)
+
+;; # Local Variables:
+;; # flycheck-disabled-checkers: 'emacs-lisp-checkdoc
+;; # End:
diff --git a/user/emacs/.emacs.d/main.el b/user/emacs/.emacs.d/main.el
deleted file mode 100644
index 4a30df6a..00000000
--- a/user/emacs/.emacs.d/main.el
+++ /dev/null
@@ -1,839 +0,0 @@
-(setq inhibit-startup-screen t
-      initial-scratch-message ""
-      initial-major-mode 'text-mode
-      package-enable-at-startup nil)
-
-(eval-when-compile
-  (require 'use-package)
-  (setq use-package-expand-minimally t))
-(setq use-package-always-demand (daemonp))
-
-;;; Customize
-
-(setq custom-file "~/.emacs.d/custom.el")
-(load custom-file :noerror :nomessage)
-
-(use-package crux
-  :defer 1
-  :custom ((crux-reopen-as-root-mode nil)))
-
-;;; Styles
-
-(custom-set-variables
- ;; I prefer an always-visible cursor.  Feels less distracting.
- '(blink-cursor-mode nil)
- ;; Disable all the bars, unless on OSX, in which case, keep the menu bar.
- '(menu-bar-mode nil)
- '(scroll-bar-mode nil)
- '(tool-bar-mode nil))
-(set-fringe-mode '(4 . 4))
-
-;; Ring the bell sometimes, but not so often
-(setq ring-bell-function
-      (lambda ()
-        (unless (memq this-command
-                      '(isearch-abort abort-recursive-edit exit-minibuffer keyboard-quit minibuffer-keyboard-quit undo-tree-undo))
-          (ding))))
-
-(when (or (daemonp)
-           window-system)
-  (load-theme 'almost-mono-white t)
-  (if (eq window-system 'x)
-      (setq-default line-spacing 0.2))
-  (setq frame-background-mode 'light)
-  (mapc 'frame-set-background-mode (frame-list)))
-  (let ((line (face-attribute 'mode-line :underline)))
-    (set-face-attribute 'mode-line          nil :overline   line)
-    (set-face-attribute 'mode-line-inactive nil :overline   line)
-    (set-face-attribute 'mode-line-inactive nil :underline  line)
-    (set-face-attribute 'mode-line          nil :box        nil)
-    (set-face-attribute 'mode-line-inactive nil :box        nil))
-
-;;; Chrome
-(use-package minions
-  :custom ((minions-mode-line-lighter "#")
-           (minions-mode-line-delimiters nil)
-           (minions-mode t)))
-
-(setq-default mode-line-position nil
-              mode-line-mule-info nil
-              mode-line-client nil
-              mode-line-remote nil)
-
-(use-package moody
-  :config (progn
-            (setq x-underline-at-descent-line t)
-            (moody-replace-mode-line-buffer-identification)
-            (moody-replace-vc-mode)
-            (with-eval-after-load 'eyebrowse
-              (defvar eyebrowse-lighter-default
-                '(:eval (eyebrowse-mode-line-indicator)))
-              (defvar moody-replace-eyebrowse-lighter
-                '(:eval (moody-tab (eyebrowse-mode-line-indicator) 1 'up)))
-              (defun moody-replace-eyebrowse (&optional reverse)
-                (interactive "P")
-                (if (assoc 'eyebrowse-mode mode-line-misc-info)
-                (setcdr (assoc 'eyebrowse-mode mode-line-misc-info)
-                        (list (if reverse
-                                  eyebrowse-lighter-default
-                                moody-replace-eyebrowse-lighter)))
-                (push (list 'eyebrowse-mode moody-replace-eyebrowse-lighter)
-                      (cdr (last mode-line-misc-info)))))
-              (moody-replace-eyebrowse nil))))
-
-(when (eq system-type 'darwin)
-  (add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))
-  (add-to-list 'default-frame-alist '(ns-appearance . 'light)))
-
-(add-to-list 'default-frame-alist '(width . 100))
-(add-to-list 'default-frame-alist '(height . 40))
-
-;;; Dates & Times
-
-(custom-set-variables
- '(calendar-week-start-day 1)
- '(calendar-date-style 'iso))
-
-(defun insert-date (prefix)
-  "Insert the current date.
-With PREFIX, use British format.
-With two prefix arguments, write out the day and month name."
-  (interactive "P")
-  (let ((format (cond
-                 ((not prefix) "%Y-%m-%d")
-                 ((equal prefix '(4)) "%d/%m/%Y")
-                 ((equal prefix '(16)) "%A, %d %B %Y"))))
-    (insert (format-time-string format))))
-
-(defun insert-datetime (prefix)
-  "Insert current date and time.  With PREFIX, use ISO8601 format."
-  (interactive "P")
-  (let ((format (cond
-                 ((not prefix) "%Y-%m-%d %H:%M:%S")
-                 ((equal prefix '(4)) "%Y-%m-%dT%H:%M:%SZ"))))
-    (insert (format-time-string format))))
-
-;;; Keybindings
-
-;; I think =set-keyboard-coding-system= stops OS X from doing something
-;; annoying to add accents.  The modifier setup is to match my
-;; re-arrangement of modifiers on OSX: Cmd on the outside, then
-;; Option/alt, then Control.
-
-(when (eq system-type 'darwin)
-  (custom-set-variables
-   '(mac-option-modifier 'meta)
-   '(mac-right-option-modifier 'none)
-   '(mac-control-modifier 'control)
-   '(mac-right-control-modifier 'left)
-   '(mac-command-modifier 'super)
-   '(mac-right-command-modifier 'left)
-   '(mac-function-modifier 'hyper)))
-
-(use-package general
-  :functions (general-unbind general-define-key)
-  :config (progn
-	    (general-override-mode +1)
-            (when (eq system-type 'darwin)
-              (general-unbind "s-x"))))
-
-(use-package avy
-  :custom ((avy-all-windows nil)))
-(use-package ace-link
-  :after avy
-  :commands (ace-link-setup-default)
-  :config (ace-link-setup-default))
-
-;; Popup keybindings following a prefix automatically.
-
-(use-package which-key
-  :defer 5
-  :custom ((which-key-mode +1))
-  :config (progn
-            (which-key-setup-side-window-right-bottom)))
-
-;;; Modeline
-
-(use-package relative-buffers
-  :defer 2
-  :custom ((global-relative-buffers-mode t)))
-
-;;; Minibuffer
-
-(setq enable-recursive-minibuffers t)
-(minibuffer-depth-indicate-mode t)
-
-(use-package hydra
-  :defer 2)
-(use-package ivy
-  :config (progn
-            (ivy-mode +1)))
-(use-package ivy-hydra
-  :defer 2)
-
-(use-package smerge-mode
-  :after magit
-  :config
-  (defhydra unpackaged/smerge-hydra
-    (:color pink :hint nil :post (smerge-auto-leave))
-    "
-^Move^       ^Keep^               ^Diff^                 ^Other^
-^^-----------^^-------------------^^---------------------^^-------
-_n_ext       _b_ase               _<_: upper/base        _C_ombine
-_p_rev       _u_pper              _=_: upper/lower       _r_esolve
-^^           _l_ower              _>_: base/lower        _k_ill current
-^^           _a_ll                _R_efine
-^^           _RET_: current       _E_diff
-"
-    ("n" smerge-next)
-    ("p" smerge-prev)
-    ("b" smerge-keep-base)
-    ("u" smerge-keep-upper)
-    ("l" smerge-keep-lower)
-    ("a" smerge-keep-all)
-    ("RET" smerge-keep-current)
-    ("\C-m" smerge-keep-current)
-    ("<" smerge-diff-base-upper)
-    ("=" smerge-diff-upper-lower)
-    (">" smerge-diff-base-lower)
-    ("R" smerge-refine)
-    ("E" smerge-ediff)
-    ("C" smerge-combine-with-next)
-    ("r" smerge-resolve)
-    ("k" smerge-kill-current)
-    ("w" (lambda ()
-            (interactive)
-            (save-buffer)
-            (bury-buffer))
-     "Save and bury buffer" :color blue)
-    ("q" nil "cancel" :color blue))
-  :hook (magit-diff-visit-file . (lambda ()
-                                   (when smerge-mode
-                                     (unpackaged/smerge-hydra/body)))))
-
-(use-package swiper
-  :general ([remap isearch-forward] #'swiper-isearch))
-
-;; transition smex history to amx
-(let ((smex-save-file (concat user-emacs-directory "smex-items")))
-  (use-package amx
-    :custom ((amx-history-length 100))))
-
-(use-package counsel
-  :commands (counsel-unicode-char)
-  :general ("M-x" #'counsel-M-x))
-
-;;; Windows
-
-(defun split-window-properly (&optional window)
-    (let ((window (or window (selected-window))))
-    (or (and (window-splittable-p window)
-	     ;; Split window vertically.
-	     (with-selected-window window
-	       (split-window-below)))
-	(and (window-splittable-p window t)
-	     ;; Split window horizontally.
-	     (with-selected-window window
-	       (split-window-right))))))
-
-(setq split-window-preferred-function #'split-window-properly
-      split-height-threshold nil
-      split-width-threshold 160)
-
-(use-package eyebrowse
-  :after (evil)
-  :custom ((eyebrowse-new-workspace t)
-           (eyebrowse-mode t)
-           (eyebrowse-mode-line-left-delimiter "")
-           (eyebrowse-mode-line-right-delimiter "")
-           (eyebrowse-mode-line-separator " ")
-           (eyebrowse-mode-line-style 'always))
-  :general (:keymaps 'evil-window-map
-	    "0" #'eyebrowse-switch-to-window-config-0
-            "1" #'eyebrowse-switch-to-window-config-1
-            "2" #'eyebrowse-switch-to-window-config-2
-            "3" #'eyebrowse-switch-to-window-config-3
-            "4" #'eyebrowse-switch-to-window-config-4
-            "5" #'eyebrowse-switch-to-window-config-5
-            "6" #'eyebrowse-switch-to-window-config-6
-            "7" #'eyebrowse-switch-to-window-config-7
-            "8" #'eyebrowse-switch-to-window-config-8
-            "9" #'eyebrowse-switch-to-window-config-9)
-  :ghook ('evil-after-load-hook #'eyebrowse-setup-evil-keys))
-
-(use-package winner
-  :after evil
-  :defer 8
-  :custom ((winner-mode t)
-           (winner-boring-buffers '("*Completions*" "*Help*" "*Apropos*" "*Buffer List*" "*info*" "*Compile-Log*")))
-  :general (:keymaps 'evil-window-map
-                    "u" #'winner-undo
-                    "r" #'winner-redo
-                    "C-r" #'winner-redo))
-;;; Evil
-
-(use-package evil
-  :demand t
-  :commands (evil-mode evil-delete-buffer evil-ex-define-cmd)
-  :init (progn
-	  (defvar evil-want-integration)
-	  (defvar evil-want-keybinding)
-	  (setq evil-want-integration t
-		evil-want-keybinding nil))
-  :custom ((evil-shift-width 2)
-	   (evil-mode-line-format nil))
-  :config (evil-mode +1)
-  :general
-  (:states 'motion
-	   "C-;" #'evil-avy-goto-line)
-  (:states 'normal
-	   ";" #'evil-ex)
-  (:states '(normal motion)
-           "g s" #'evil-avy-goto-symbol-1))
-
-(use-package evil-collection
-  :after (evil)
-  :defer 3
-  :demand t
-  :commands (evil-collection-init)
-  :custom ((evil-collection-company-use-tng nil))
-  :config (progn
-	    (evil-collection-init)))
-
-(general-create-definer my-leader-def
-  :keymaps 'override
-  :states '(normal motion)
-  :prefix ",")
-
-(use-package evil-space
-  :defer 1
-  :after evil
-  :custom ((evil-space-mode t)))
-
-(use-package evil-surround
-  :after evil
-  :defer 2
-  :custom ((global-evil-surround-mode t)))
-
-(use-package evil-commentary
-  :after evil
-  :defer 2
-  :custom ((evil-commentary-mode t)))
-
-(use-package evil-magit
-  :after magit
-  :custom ((evil-magit-use-y-for-yank nil)))
-
-(use-package evil-quickscope
-  :after evil
-  :commands (evil-quickscope-mode)
-  :ghook ('(magit-mode-hook git-rebase-mode-hook) #'turn-off-evil-quickscope-mode)
-  :custom ((global-evil-quickscope-mode t)))
-
-(use-package evil-org
-  :after org
-  :commands (evil-org-set-key-theme)
-  :ghook ('org-mode-hook #'evil-org-mode)
-  :gfhook #'evil-org-set-key-theme)
-
-(use-package evil-org-agenda
-  :after org
-  :ghook ('org-agenda-mode-hook #'evil-org-agenda-set-keys))
-
-;;; Projects
-
-(use-package projectile
-  :defer 1
-  :defines projectile-command-map
-  :custom ((projectile-mode +1)
-           (projectile-completion-system 'ivy))
-  :config (progn
-            (add-to-list 'projectile-globally-ignored-files "package-lock.json")
-            (add-to-list 'projectile-globally-ignored-files "pnpm-lock.yaml")
-            (add-to-list 'projectile-project-root-files "package.json")
-            (add-to-list 'projectile-project-root-files-bottom-up "pnpm-workspace.yaml")
-            (setq projectile-project-root-files-functions '(projectile-root-local
-                                                            projectile-root-top-down
-                                                            projectile-root-bottom-up
-                                                            projectile-root-top-down-recurring))
-	    (with-eval-after-load 'evil-ex
-	      (evil-ex-define-cmd "prg" #'projectile-ripgrep)
-              (evil-ex-define-cmd "pesh[ell]" #'projectile-run-eshell))))
-
-(use-package counsel-projectile
-  :defer 1
-  :commands (counsel-projectile-switch-project
-	     counsel-projectile-rg
-	     counsel-projectile-switch-to-buffer
-             counsel-projectile-mode)
-  :general (:keymaps 'projectile-command-map
-                     "s s" #'counsel-projectile-rg
-                     "s r" #'counsel-projectile-rg)
-  :config (progn
-            (assq-delete-all #'projectile-ripgrep counsel-projectile-key-bindings)
-            (counsel-projectile-mode +1)
-	    (with-eval-after-load 'evil-ex
-	      (evil-ex-define-cmd "cprg" #'counsel-projectile-rg)
-	      (evil-ex-define-cmd "pb" #'counsel-projectile-switch-to-buffer)
-	      (evil-ex-define-cmd "psw[itch]" #'counsel-projectile-switch-project))))
-
-(use-package magit
-  :defer 10
-  :commands (magit-status magit-dispatch)
-  :custom ((magit-auto-revert-mode nil)
-           (magit-section-visibility-indicator nil)
-           (magit-diff-refine-hunk 'all)
-           (magit-display-buffer-function #'display-buffer)
-           (magit-completing-read-function #'ivy-completing-read))
-  :config (progn
-            (global-magit-file-mode +1)
-            (remove-hook 'magit-section-highlight-hook 'magit-section-highlight)
-            (remove-hook 'magit-section-highlight-hook 'magit-section-highlight-selection)
-            (remove-hook 'magit-section-highlight-hook 'magit-diff-highlight)))
-
-(eval-when-compile (require 'fringe-helper))
-(use-package git-gutter
-  :defer t)
-(use-package git-gutter-fringe
-  :defer 5
-  :config (progn
-            (global-git-gutter-mode 1)
-            ;; places the git gutter outside the margins.
-            (setq-default fringes-outside-margins nil)
-            ;; thin fringe bitmaps
-            (fringe-helper-define 'git-gutter-fr:added '(center repeated)
-              ".XXX....")
-            (fringe-helper-define 'git-gutter-fr:modified '(center repeated)
-              ".XXX....")
-            (fringe-helper-define 'git-gutter-fr:deleted '(center repeated)
-              ".XXX....")
-            (setq git-gutter-fr:side 'right-fringe)))
-
-(use-package git-messenger
-  :commands (git-messenger:popup-message)
-  :defer 10
-  :custom ((git-messenger:use-magit-popup t)))
-
-(use-package git-timemachine
-  :commands (git-timemachine))
-
-(use-package editorconfig
-  :defer 2
-  :init (progn
-          (unless (executable-find "editorconfig")
-            (warn "Missing `editorconfig' executable.")))
-  :config (editorconfig-mode +1))
-
-;;; Completion
-
-(use-package company
-  :defer 2
-  :commands (company-explicit-action-p)
-  :custom ((global-company-mode +1)
-           (company-idle-delay 0)
-           (company-show-numbers t)
-           (company-dabbrev-downcase nil)
-           (company-dabbrev-ignore-case nil)
-           (company-begin-commands '(self-insert-command))
-           (company-auto-complete #'company-explicit-action-p)
-           (company-auto-complete-chars '(?\ ?\( ?\) ?.)))
-  :general (:states 'insert
-                    "TAB" #'company-indent-or-complete-common))
-
-;; (use-package all-the-icons)
-
-(eval-when-compile (require 'subr-x))
-(eval-and-compile
-  (defun company-tabnine-load-path ()
-    (string-trim-right (shell-command-to-string "ghq list -p company-tabnine"))))
-
-(use-package company-tabnine
-  :commands (company-tabnine)
-  :after (company)
-  :load-path (lambda () (list (company-tabnine-load-path)))
-  :custom ((company-tabnine-binaries-folder "~/.local/tabnine"))
-  :general ("<M-tab>" #'company-tabnine-call-other-backends
-            "<C-tab>" #'company-tabnine-call-other-backends)
-  :init (progn
-          (add-to-list 'company-backends #'company-tabnine)))
-
-;;; Documentation
-
-(use-package eldoc
-  :defer 5
-  :custom ((global-eldoc-mode +1)
-           (eldoc-idle-delay 0.5)))
-
-(use-package eldoc-box
-  :after (eldoc eglot)
-  :custom ((eldoc-box-hover-mode +1)
-           (eldoc-box-hover-at-point-mode +1)))
-
-(use-package ehelp
-  :defer 15)
-
-(use-package helpful
-  :after ehelp
-  :general (ehelp-map
-            "k" #'helpful-key
-            "v" #'helpful-variable
-            "f" #'helpful-callable))
-
-;;; Files
-
-;;;; Auto-saving
-
-;; Auto-save everything to a temporary directory, instead of cluttering
-;; the filesystem.  I don’t want emacs-specific lockfiles, either.
-
-(setq auto-save-file-name-transforms `((".*" ,temporary-file-directory t))
-      create-lockfiles nil)
-
-;;;; Auto-reloading
-
-(use-package autorevert
-  :custom ((global-auto-revert-mode t)
-           (auto-revert-verbose t)))
-
-;;;; Backups
-
-;; I like to keep my backups out of regular folders.  I tell emacs to use
-;; a subfolder of its configuration directory for that.  Also, use the
-;; trash for deleting on OS X.
-(let ((backup-dir (expand-file-name "~/.emacs.d/backups/")))
-  (unless (file-directory-p backup-dir)
-    (make-directory backup-dir))
-  (setq backup-directory-alist `((".*" . ,backup-dir))
-        backup-by-copying-when-linked t
-        backup-by-copying-when-mismatch t))
-
-(setq delete-by-moving-to-trash t)
-
-(use-package goto-chg
-  :defer 1)
-
-;;; Directories
-
-(custom-set-variables
- '(dired-dwim-target t)
- '(dired-recursive-copies 'top)
- '(dired-listing-switches "-alh")
- '(dired-recursive-deleted (if delete-by-moving-to-trash
-                              'always
-                            'top)))
-
-(add-hook 'dired-mode-hook
-          (lambda ()
-            (dired-hide-details-mode)))
-
-(use-package dired-git-info
-  :general (:keymaps 'dired-mode-map
-                     :states 'normal
-                     ")" #'dired-git-info-mode))
-
-;;; Shells
-
-(use-package eshell
-  :defer 5
-  :commands (eshell)
-  :functions (eshell/pwd)
-  :init (progn
-          (with-eval-after-load 'evil-ex
-            (evil-ex-define-cmd "esh[ell]" #'eshell)))
-  :custom ((eshell-prompt-function (lambda ()
-                                     (concat (eshell/pwd) "\n$ ")))
-           (eshell-prompt-regexp "^[$][[:blank:]]")
-           (eshell-cmpl-cycle-completions nil)))
-
-(use-package eshell-toggle
-  :after projectile
-  :commands (eshell-toggle)
-  :custom ((eshell-toggle-use-projectile-root t)))
-
-(use-package esh-autosuggest
-  :after eshell
-  :ghook ('eshell-mode-hook))
-
-(declare-function eshell-push-command "esh-buf-stack" (CMD))
-(defun my-bind-esh-push ()
-  (general-define-key
-   :states '(normal insert)
-   :keymaps 'local
-   "M-q" #'eshell-push-command))
-
-(use-package esh-buf-stack
-  :after (eshell)
-  :ghook ('eshell-mode-hook #'my-bind-esh-push)
-  :config (setup-eshell-buf-stack))
-
-(use-package bash-completion
-  :after (eshell))
-
-(use-package fish-completion
-  :when (executable-find "fish")
-  :after (bash-completion)
-  :custom ((fish-completion-fallback-on-bash-p t))
-  :commands (global-fish-completion-mode)
-  :config (global-fish-completion-mode))
-
-(use-package esh-help
-  :after (eshell)
-  :config (setup-esh-help-eldoc))
-
-(use-package eshell-fringe-status
-  :after eshell
-  :ghook '(eshell-mode-hook))
-
-(use-package eshell-up
-  :after (eshell))
-
-(use-package shell
-  :defer t
-  :general (:keymaps 'shell-mode-map
-                     "C-d" #'comint-delchar-or-maybe-eof))
-
-(use-package comint
-  :defer t
-  :general (:keymaps 'comint-mode-map
-                     "C-c C-l" #'counsel-shell-history))
-
-;;; Editing
-
-(setq-default indent-tabs-mode nil
-              tab-always-indent 'complete)
-
-(use-package ws-butler
-  :ghook ('prog-mode-hook))
-
-;;; Major modes
-
-;;;; js
-(custom-set-variables '(js-indent-level 2)
-                      '(js-enabled-frameworks '(javascript)))
-
-;;;; typescript
-(custom-set-variables '(typescript-indent-level 2))
-(autoload 'ansi-color-apply-on-region "ansi-color")
-(defun colorise-compilation-buffer ()
-  (ansi-color-apply-on-region compilation-filter-start (point-max)))
-(add-hook 'compilation-filter-hook #'colorise-compilation-buffer)
-
-;;;; shell
-(general-add-hook 'sh-mode-hook
-                  (lambda ()
-                    (general-add-hook 'after-save-hook
-                                      #'executable-make-buffer-file-executable-if-script-p :append :local)))
-
-(add-to-list 'auto-mode-alist '("\\.env\\'" . conf-unix-mode))
-(use-package sh-script
-  :mode (("\\.zsh\\'" . shell-script-mode)
-         ("zshenv\\'" . shell-script-mode)
-         ("zshrc\\'"  . shell-script-mode))
-  :config (setq sh-shell-file "/usr/bin/env zsh"
-                sh-basic-offset 2))
-
-(add-hook 'shell-mode-hook 'ansi-color-for-comint-mode-on)
-
-;;;; make
-(general-add-hook 'makefile-mode-hook
-                  (lambda ()
-                    (setq-local indent-tabs-mode t)))
-
-;;;; nix
-(custom-set-variables '(nix-indent-function #'nix-indent-line))
-
-(use-package nix-update
-  :commands (nix-update-fetch))
-
-;;;; gitlab-ci.yml
-(with-eval-after-load 'git-gutter-fringe
-              (fringe-helper-define 'flycheck-fringe-bitmap-double-arrow '(center repeated)
-                "XXX....."))
-
-(custom-set-variables '(gitlab-ci-url "https://gitlab.satoshipay.tech"))
-(use-package gitlab-ci-mode-flycheck
-  :ghook ('gitlab-ci-mode-hook (list #'gitlab-ci-mode-flycheck-enable
-                                     #'flycheck-mode)))
-
-;;;; *ignore
-(use-package gitignore-mode
-  :mode ((".dockerignore\\'" . gitignore-mode)))
-
-;;;; kubernetes
-(custom-set-variables '(k8s-site-docs-version "v1.13"))
-
-;;;; beancount
-
-(use-package beancount
-  :load-path "~/projects/bitbucket.org/blais/beancount/editors/emacs")
-
-;;;; org
-
-(custom-set-variables '(org-ellipsis "…")
-                      `(org-directory "~/Documents/org"))
-
-(use-package org-journal
-  :commands (org-journal-new-date-entry
-	     org-journal-new-entry
-	     org-journal-new-scheduled-entry)
-  :gfhook (#'variable-pitch-mode)
-  :custom ((org-journal-date-format "%A, %d %B %Y")
-           (org-journal-dir "~/Documents/journal")))
-
-;;;; emacs-lisp
-
-(use-package auto-async-byte-compile
-  :custom ((auto-async-byte-compile-exclude-files-regexp "custom\\.el"))
-  :ghook ('emacs-lisp-mode-hook #'enable-auto-async-byte-compile-mode))
-
-;;;; web modes (tsx, html)
-
-(use-package web-mode
-  :mode (("\\.tsx" . web-mode))
-  :custom ((web-mode-enable-auto-pairing nil)
-           (web-mode-code-indent-offset 2)
-           (web-mode-markup-indent-offset 2)
-           (web-mode-css-indent-offset 2)
-           (web-mode-style-padding 0)
-           (web-mode-script-padding 0)))
-
-;;; IDE features
-
-(fringe-helper-define 'left-vertical-bar '(center repeated)
-  "XXX.....")
-(use-package flymake
-  :custom ((flymake-error-bitmap '(left-vertical-bar compilation-error))
-           (flymake-warning-bitmap '(left-vertical-bar compilation-warning))))
-(use-package flymake-diagnostic-at-point
-  :custom ((flymake-diagnostic-at-point-diagnostic-function #'flymake-diagnostic-at-point-display-minibuffer))
-  :ghook '(flymake-mode-hook))
-
-(use-package lsp-mode
-  :ghook ('(typescript-mode-hook
-            dockerfile-mode-hook
-            yaml-mode-hook
-            js-mode-hook
-            css-mode-hook
-            scss-mode-hook
-            html-mode-hook
-            haskell-mode-hook)
-          #'lsp)
-  :functions (lsp--flymake-setup)
-  :gfhook 'lsp--flymake-setup
-  :custom ((lsp-auto-guess-root t)
-           (lsp-auto-configure nil)
-           (lsp-prefer-flymake t)
-           (lsp-enable-symbol-highlighting nil))
-  :config (progn
-            (add-to-list 'lsp-language-id-configuration '(js-mode . "javascript"))))
-
-(use-package lsp-clients
-  :after (lsp-mode))
-
-(use-package lsp-ui
-  :after lsp-mode
-  :ghook ('lsp-mode-hook)
-  :custom ((lsp-ui-sideline-enable nil)
-           (lsp-ui-doc-mode nil)
-           (lsp-enable-snippet nil)))
-
-(use-package lsp-haskell
-  :after lsp-mode)
-
-;; Inside a javascript project, it's common to install tools locally to
-;; the project.  This will allows emacs to find their executables.
-
-(use-package add-node-modules-path
-  :ghook ('(js2-mode-hook typescript-mode-hook) #'add-node-modules-path))
-
-;;;; Reformat on save
-
-(use-package prettier-js
-  :custom ((prettier-js-show-errors 'echo))
-  :ghook ('(typescript-mode-hook) #'prettier-js-mode t)
-  :config (progn
-            (if-let ((prettier_d (executable-find "prettier_d")))
-                (setq prettier-js-command prettier_d
-                      prettier-js-args '("--pkg-conf" "--parser" "typescript"))
-              (message "prettier_d is not available"))))
-
-;;; E-mail
-
-(declare-function sendmail-send-it "sendmail")
-(setq send-mail-function #'sendmail-send-it)
-
-(use-package mu4e
-  :if (executable-find "mu")
-  :commands (mu4e)
-  :init (setq mail-user-agent #'mu4e-user-agent)
-  :custom ((mu4e-completing-read-function #'ivy-completing-read)
-           (mu4e-maildir "~/mail")
-           (mu4e-context-policy 'pick-first)
-           (mu4e-update-interval 600)
-           (mu4e-change-filenames-when-moving t)
-           (mu4e-index-lazy-check t)
-           (mu4e-hide-index-messages t)
-           (mu4e-compose-format-flowed t))
-  :init (progn
-         (with-eval-after-load 'evil-ex
-           (evil-ex-define-cmd "mu[4e]" #'mu4e)))
-  :config (progn
-            (setq mu4e-contexts (list
-                                 (make-mu4e-context
-                                  :name "SatoshiPay"
-                                  :match-func (lambda (msg)
-                                                (if msg
-                                                    (mu4e-message-contact-field-matches
-                                                     msg :to ".*@satoshipay.io")
-                                                  (string-equal (system-name) "satoshipad")))
-                                  :vars '((user-mail-address . "alan@satoshipay.io")
-                                          (mu4e-sent-messages-behavior . delete)
-                                          (mu4e-drafts-folder . "/satoshipay/Drafts")
-                                          (mu4e-sent-folder . "/satoshipay/Sent Mail")
-                                          (mu4e-refile-folder . "/satoshipay/All Mail")
-                                          (mu4e-trash-folder . "/satoshipay/Bin")
-                                          (mu4e-maildir-shortcuts . (("/satoshipay/INBOX" . ?i)
-                                                                     ("/satoshipay/All Mail" . ?a)
-                                                                     ("/satoshipay/Sent Mail" . ?s)
-                                                                     ("/satoshipay/Spam" . ?p)))))))))
-
-;;; Take me to my leader
-
-(my-leader-def
- "" nil
- "`" #'eshell-toggle
- "h" '(:keymap ehelp-map :package ehelp)
- "w" '(:keymap evil-window-map :package evil)
- "x" '(:keymap ctl-x-map)
- "q" #'evil-delete-buffer
- "p" projectile-command-map
- "v" #'split-window-right
- "o" #'other-window
- "u" #'universal-argument
- ";" #'counsel-M-x
- "bb" #'ivy-switch-buffer
- "bx" #'kill-this-buffer
- "br" #'revert-buffer
- "bk" #'kill-buffer
- "dd" #'dired
- "fs" #'save-buffer
- "ff" #'find-file
- "fw" #'write-file
- "fd" #'crux-delete-file-and-buffer
- "fr" #'crux-rename-file-and-buffer
- "gs" #'magit-status
- "gm" #'git-messenger:popup-message
- "gg" #'magit-dispatch
- "gn" #'git-gutter:next-hunk
- "gp" #'git-gutter:previous-hunk
- "gi" #'git-gutter:popup-hunk
- "gs" #'git-gutter:stage-hunk
- "go" #'git-gutter:revert-hunk
- "gt" #'git-timemachine
- "bi" #'ibuffer
- "bz" #'bury-buffer
- "iu" #'counsel-unicode-char)
-
-;; # Local Variables:
-;; # flycheck-disabled-checkers: 'emacs-lisp-checkdoc
-;; # End:
diff --git a/user/modules/emacs.nix b/user/modules/emacs.nix
index b6ac0395..9f542391 100644
--- a/user/modules/emacs.nix
+++ b/user/modules/emacs.nix
@@ -154,9 +154,11 @@ in
   home.sessionVariables = {
     EDITOR = "${editorScript}/bin/edit";
   };
-  home.file.".emacs.d/init.el".source = ../emacs/.emacs.d/init.el;
-  home.file.".emacs.d/main.el" = {
-    source = ../emacs/.emacs.d/main.el;
+  home.file.".emacs.d/init.el" = {
+    source = ../emacs/.emacs.d/init.el;
+    onChange = ''
+      ${config.programs.emacs.finalPackage}/bin/emacs -batch -f batch-byte-compile .emacs.d/init.el
+    '';
   };
   home.file.".emacs.d/eshell/" = {
     recursive = true;