[emacs] further evil-related enhancements
[~bandali/configs] / init.org
index b5e32d3..c7b2c83 100644 (file)
--- a/init.org
+++ b/init.org
@@ -44,7 +44,6 @@ byte-compiled the packages. Something along these lines should work:
 git clone https://github.com/aminb/dotfiles ~/.emacs.d
 cd ~/.emacs.d
 make bootstrap-borg
-make tangle-init
 make bootstrap
 make build
 #+end_src
@@ -106,20 +105,19 @@ file.
 
 ** Naming conventions
 
-The conventions below were inspired by [[https://github.com/hlissner/doom-emacs][Doom]]'s conventions, found
-[[https://github.com/hlissner/doom-emacs/blob/5dacbb7cb1c6ac246a9ccd15e6c4290def67757c/core/core.el#L3-L17][here]]. Naturally, I use my initials, =ab=, instead of =doom=.
+The conventions below were inspired by [[https://github.com/hlissner/doom-emacs][Doom]]'s, found [[https://github.com/hlissner/doom-emacs/blob/5dacbb7cb1c6ac246a9ccd15e6c4290def67757c/core/core.el#L3-L17][here]].
 
 #+begin_src emacs-lisp :comments none
 ;; Naming conventions:
 ;;
-;;   ab-...   public variables or non-interactive functions
-;;   ab--...  private anything (non-interactive), not safe for direct use
-;;   ab/...   an interactive function; safe for M-x or keybinding
-;;   ab:...   an evil operator, motion, or command
-;;   ab|...   a hook function
-;;   ab*...   an advising function
-;;   ab@...   a hydra command
-;;   ...!     a macro
+;;   amin-...   public variables or non-interactive functions
+;;   amin--...  private anything (non-interactive), not safe for direct use
+;;   amin/...   an interactive function; safe for M-x or keybinding
+;;   amin:...   an evil operator, motion, or command
+;;   amin|...   a hook function
+;;   amin*...   an advising function
+;;   amin@...   a hydra command
+;;   ...!       a macro
 #+end_src
 
 * Initial setup
@@ -138,10 +136,10 @@ let's see how long Emacs takes to start up, before even loading
 =init.el=, i.e. =user-init-file=:
 
 #+begin_src emacs-lisp
-(defvar ab--before-user-init-time (current-time)
+(defvar amin--before-user-init-time (current-time)
   "Value of `current-time' when Emacs begins loading `user-init-file'.")
 (message "Loading Emacs...done (%.3fs)"
-         (float-time (time-subtract ab--before-user-init-time
+         (float-time (time-subtract amin--before-user-init-time
                                     before-init-time)))
 #+end_src
 
@@ -151,9 +149,9 @@ frequency. Clearing the ~file-name-handler-alist~ seems to help reduce
 startup time as well.
 
 #+begin_src emacs-lisp
-(defvar ab--gc-cons-threshold gc-cons-threshold)
-(defvar ab--gc-cons-percentage gc-cons-percentage)
-(defvar ab--file-name-handler-alist file-name-handler-alist)
+(defvar amin--gc-cons-threshold gc-cons-threshold)
+(defvar amin--gc-cons-percentage gc-cons-percentage)
+(defvar amin--file-name-handler-alist file-name-handler-alist)
 (setq gc-cons-threshold (* 400 1024 1024)  ; 400 MiB
       gc-cons-percentage 0.6
       file-name-handler-alist nil
@@ -168,13 +166,9 @@ done initializing.
 (add-hook
  'after-init-hook
  (lambda ()
-   (let ((elapsed (float-time (time-subtract (current-time)
-                                             ab--before-user-init-time))))
-     (message "Loading %s...done (%.3fs) [after-init]"
-              user-init-file elapsed))
-   (setq gc-cons-threshold ab--gc-cons-threshold
-         gc-cons-percentage ab--gc-cons-percentage
-         file-name-handler-alist ab--file-name-handler-alist)))
+   (setq gc-cons-threshold amin--gc-cons-threshold
+         gc-cons-percentage amin--gc-cons-percentage
+         file-name-handler-alist amin--file-name-handler-alist)))
 #+end_src
 
 Increase the number of lines kept in message logs (the =*Messages*=
@@ -193,6 +187,13 @@ but for now I've decided to keep them enabled. See documentation for
 ;;       '(not free-vars unresolved noruntime lexical make-local))
 #+end_src
 
+** whoami
+
+#+begin_src emacs-lisp
+(setq user-full-name "Amin Bandali"
+      user-mail-address "amin@aminb.org")
+#+end_src
+
 ** Package management
 
 *** No =package.el=
@@ -246,7 +247,7 @@ and without compromising on performance.
 
 #+begin_src emacs-lisp
 (require 'use-package)
-(if nil                                    ; set to t when need to debug init
+(if nil  ; set to t when need to debug init
     (setq use-package-verbose t
           use-package-expand-minimally nil
           use-package-compute-statistics t
@@ -324,6 +325,14 @@ in my shell.
   (exec-path-from-shell-copy-env "SSH_AUTH_SOCK"))
 #+end_src
 
+** Only one custom theme at a time
+
+#+begin_src emacs-lisp
+(defadvice load-theme (before clear-previous-themes activate)
+  "Clear existing theme settings instead of layering them"
+  (mapc #'disable-theme custom-enabled-themes))
+#+end_src
+
 ** Server
 
 Start server if not already running. Alternatively, can be done by
@@ -353,17 +362,70 @@ Font stack with better unicode support, around =Ubuntu Mono= and
   (set-fontset-font
    ft
    'unicode
-   (font-spec
-    :name "Hack")
+   (font-spec :name "DejaVu Sans Mono")
    nil
    'append)
+  ;; (set-fontset-font
+  ;;  ft
+  ;;  'unicode
+  ;;  (font-spec
+  ;;   :name "Symbola monospacified for DejaVu Sans Mono")
+  ;;  nil
+  ;;  'append)
+  ;; (set-fontset-font
+  ;;  ft
+  ;;  #x2115  ; ℕ
+  ;;  (font-spec :name "DejaVu Sans Mono")
+  ;;  nil
+  ;;  'append)
   (set-fontset-font
    ft
-   'unicode
-   (font-spec
-    :name "Symbola monospacified for DejaVu Sans Mono")
+   (cons ?Α ?ω)
+   (font-spec :name "DejaVu Sans Mono" :size 14)
    nil
-   'append))
+   'prepend))
+#+end_src
+
+** Libraries
+
+#+begin_src emacs-lisp
+(require 'cl-lib)
+(require 'subr-x)
+#+end_src
+
+** Useful utilities
+
+#+begin_src emacs-lisp
+(defun amin-enlist (exp)
+  "Return EXP wrapped in a list, or as-is if already a list."
+(if (listp exp) exp (list exp)))
+
+; from https://github.com/hlissner/doom-emacs/commit/589108fdb270f24a98ba6209f6955fe41530b3ef
+(defmacro after! (features &rest body)
+  "A smart wrapper around `with-eval-after-load'. Supresses warnings during
+compilation."
+  (declare (indent defun) (debug t))
+  (list (if (or (not (bound-and-true-p byte-compile-current-file))
+                (dolist (next (amin-enlist features))
+                  (if (symbolp next)
+                      (require next nil :no-error)
+                    (load next :no-message :no-error))))
+            #'progn
+          #'with-no-warnings)
+        (cond ((symbolp features)
+               `(eval-after-load ',features '(progn ,@body)))
+              ((and (consp features)
+                    (memq (car features) '(:or :any)))
+               `(progn
+                  ,@(cl-loop for next in (cdr features)
+                             collect `(after! ,next ,@body))))
+              ((and (consp features)
+                    (memq (car features) '(:and :all)))
+               (dolist (next (cdr features))
+                 (setq body `(after! ,next ,@body)))
+               body)
+              ((listp features)
+               `(after! (:all ,@features) ,@body)))))
 #+end_src
 
 * Core
@@ -379,7 +441,9 @@ Enable displaying time and battery in the mode-line, since I'm not
 using the Xfce panel anymore. Also, I don't need to see the load
 average on a regular basis, so disable that.
 
-#+begin_src emacs-lisp
+Note: using =i3status= on sway at the moment, so disabling this.
+
+#+begin_src emacs-lisp :tangle no
 (use-package time
   :ensure nil
   :init
@@ -502,6 +566,25 @@ variable.
       version-control t)
 #+end_src
 
+*** Auto revert
+
+Enable automatic reloading of changed buffers and files.
+
+#+begin_src emacs-lisp
+(global-auto-revert-mode 1)
+(setq auto-revert-verbose nil
+      global-auto-revert-non-file-buffers t)
+#+end_src
+
+*** Always use space for indentation
+
+#+begin_src emacs-lisp
+(setq-default
+ indent-tabs-mode nil
+ require-final-newline t
+ tab-width 4)
+#+end_src
+
 ** Packages
 
 The packages in this section are absolutely essential to my everyday
@@ -518,244 +601,145 @@ customizing it.
   (auto-compile-on-load-mode)
   (auto-compile-on-save-mode)
   (setq auto-compile-display-buffer               nil
-       auto-compile-mode-line-counter            t
-       auto-compile-source-recreate-deletes-dest t
-       auto-compile-toggle-deletes-nonlib-dest   t
-       auto-compile-update-autoloads             t)
+        auto-compile-mode-line-counter            t
+        auto-compile-source-recreate-deletes-dest t
+        auto-compile-toggle-deletes-nonlib-dest   t
+        auto-compile-update-autoloads             t)
   (add-hook 'auto-compile-inhibit-compile-hook
             'auto-compile-inhibit-compile-detached-git-head))
 #+end_src
 
-*** TODO [[https://github.com/Kungsgeten/ryo-modal][ryo-modal]]
+*** [[https://github.com/noctuid/general.el][general]]
 
-#+begin_quote
-Roll your own modal mode
-#+end_quote
-
-*** [[https://github.com/ch11ng/exwm][EXWM]] (window manager)
-
-#+begin_src emacs-lisp
-(use-package exwm
+ #+begin_src emacs-lisp
+(use-package general
   :demand t
   :config
-  (require 'exwm-config)
-
-  ;; Set the initial workspace number.
-  (setq exwm-workspace-number 4)
-
-  ;; Make class name the buffer name, truncating beyond 50 characters
-  (defun exwm-rename-buffer ()
-    (interactive)
-    (exwm-workspace-rename-buffer
-     (concat exwm-class-name ":"
-             (if (<= (length exwm-title) 50) exwm-title
-               (concat (substring exwm-title 0 49) "...")))))
-  (add-hook 'exwm-update-class-hook 'exwm-rename-buffer)
-  (add-hook 'exwm-update-title-hook 'exwm-rename-buffer)
-
-  ;; 's-R': Reset
-  (exwm-input-set-key (kbd "s-R") #'exwm-reset)
-  ;; 's-\': Switch workspace
-  (exwm-input-set-key (kbd "s-\\") #'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))))
-  ;; 's-SPC': Launch application
-  ;; (exwm-input-set-key
-  ;;  (kbd "s-SPC")
-  ;;  (lambda (command)
-  ;;    (interactive (list (read-shell-command "➜ ")))
-  ;;    (start-process-shell-command command nil command)))
-
-  (exwm-input-set-key (kbd "M-s-SPC") #'counsel-linux-app)
-
-  ;; Shorten 'C-c C-q' to 'C-q'
-  (define-key exwm-mode-map [?\C-q] #'exwm-input-send-next-key)
-
-  ;; Line-editing shortcuts
-  (setq exwm-input-simulation-keys
-       '(;; movement
-          ([?\C-b] . [left])
-          ([?\M-b] . [C-left])
-          ([?\C-f] . [right])
-          ([?\M-f] . [C-right])
-          ([?\C-p] . [up])
-          ([?\C-n] . [down])
-          ([?\C-a] . [home])
-          ([?\C-e] . [end])
-          ([?\M-v] . [prior])
-          ([?\C-v] . [next])
-          ([?\C-d] . [delete])
-          ([?\C-k] . [S-end delete])
-          ;; cut/copy/paste
-          ;; ([?\C-w] . [?\C-x])
-          ([?\M-w] . [?\C-c])
-          ([?\C-y] . [?\C-v])
-          ;; search
-          ([?\C-s] . [?\C-f])))
-
-  ;; Enable EXWM
-  (exwm-enable)
-
-  (add-hook 'exwm-init-hook #'exwm-config--fix/ido-buffer-window-other-frame)
-
-  (require 'exwm-systemtray)
-  (exwm-systemtray-enable)
-
-  (require 'exwm-randr)
-  (exwm-randr-enable)
-
-  ;; (exwm-input-set-key
-  ;;  (kbd "s-<return>")
-  ;;  (lambda ()
-  ;;    (interactive)
-  ;;    (start-process "urxvt" nil "urxvt")))
-
-  ;; (exwm-input-set-key
-  ;;  (kbd "s-SPC")  ;; rofi doesn't properly launch programs when started from emacs
-  ;;  (lambda ()
-  ;;    (interactive)
-  ;;    (start-process-shell-command "rofi-run" nil "rofi -show run -display-run '> ' -display-window ' 🗔 '")))
-
-  ;; (exwm-input-set-key
-  ;;  (kbd "s-/")
-  ;;  (lambda ()
-  ;;    (interactive)
-  ;;    (start-process-shell-command "rofi-win" nil "rofi -show window  -display-run '> ' -display-window ' 🗔 '")))
-
-  ;; (exwm-input-set-key
-  ;;  (kbd "M-SPC")
-  ;;  (lambda ()
-  ;;    (interactive)
-  ;;    (start-process "rofi-pass" nil "rofi-pass")))
-
-  ;; (exwm-input-set-key
-  ;;  (kbd "<XF86AudioMute>")
-  ;;  (lambda ()
-  ;;    (interactive)
-  ;;    (start-process-shell-command "pamixer" nil "pamixer --toggle-mute")))
-
-  ;; (exwm-input-set-key
-  ;;  (kbd "<XF86AudioLowerVolume>")
-  ;;  (lambda ()
-  ;;    (interactive)
-  ;;    (start-process-shell-command "pamixer" nil "pamixer --allow-boost --decrease 5")))
-
-  ;; (exwm-input-set-key
-  ;;  (kbd "<XF86AudioRaiseVolume>")
-  ;;  (lambda ()
-  ;;    (interactive)
-  ;;    (start-process-shell-command "pamixer" nil "pamixer --allow-boost --increase 5")))
-
-  ;; (exwm-input-set-key
-  ;;  (kbd "<XF86AudioPlay>")
-  ;;  (lambda ()
-  ;;    (interactive)
-  ;;    (start-process-shell-command "mpc" nil "mpc toggle")))
-
-  ;; (exwm-input-set-key
-  ;;  (kbd "<XF86AudioPrev>")
-  ;;  (lambda ()
-  ;;    (interactive)
-  ;;    (start-process-shell-command "mpc" nil "mpc prev")))
-
-  ;; (exwm-input-set-key
-  ;;  (kbd "<XF86AudioNext>")
-  ;;  (lambda ()
-  ;;    (interactive)
-  ;;    (start-process-shell-command "mpc" nil "mpv next")))
-
-  (defun ab--exwm-pasystray ()
-    "A command used to start pasystray."
-    (interactive)
-    (if (executable-find "pasystray")
-       (progn
-          (message "EXWM: starting pasystray ...")
-          (start-process-shell-command "pasystray" nil "pasystray --notify=all"))
-      (message "EXWM: pasystray is not installed, abort!")))
-
-  (add-hook 'exwm-init-hook #'ab--exwm-pasystray)
-
-  (exwm-input-set-key
-   (kbd "s-t")
-   (lambda ()
-     (interactive)
-     (exwm-floating-toggle-floating)))
-
-  (exwm-input-set-key
-   (kbd "s-f")
-   (lambda ()
-     (interactive)
-     (exwm-layout-toggle-fullscreen)))
-
-  (exwm-input-set-key
-   (kbd "s-w")
-   (lambda ()
-     (interactive)
-     (kill-buffer (current-buffer))))
-
-  (exwm-input-set-key
-   (kbd "s-q")
-   (lambda ()
-     (interactive)
-     (exwm-manage--kill-client))))
-#+end_src
-
-**** sxhkdrc
-:PROPERTIES:
-:header-args+: :tangle ~/.config/sxhkd/sxhkdrc :mkdirp yes
-:END:
-
-#+begin_src conf
-# terminal emulator
-super + Return
-       urxvt
-
-# program launcher
-super + space
-       rofi -show run -display-run '> ' -display-window ' 🗔 '
-
-# window finder
-super + slash
-       rofi -show window  -display-run '> ' -display-window ' 🗔 '
-
-# password manager
-alt + space
-       rofi-pass
-
-# make sxhkd reload its configuration files:
-super + Escape
-       pkill -USR1 -x sxhkd
+  (general-evil-setup t)
+
+  (general-override-mode)
+
+  (general-create-definer
+    amin--mode-leader-keys
+    :keymaps 'override
+    :states '(emacs normal visual motion insert)
+    :non-normal-prefix "C-,"
+    :prefix ",")
+
+  (general-create-definer
+    amin--leader-keys
+    :keymaps 'override
+    :states '(emacs normal visual motion insert)
+    :non-normal-prefix "M-m"
+    :prefix "SPC"))
+#+end_src
 
-# volume {up,down}
-XF86Audio{Raise,Lower}Volume
-       pamixer --allow-boost --{in,de}crease 5
+*** evil
 
-# mute
-XF86AudioMute
-       pamixer --toggle-mute
+#+begin_src emacs-lisp
+(use-package evil
+  :demand t
+  :hook (view-mode . evil-motion-state)
+  :init
+  (setq evil-want-integration nil)
+  :config (evil-mode 1)
+  (general-swap-key nil '(normal motion) ";" ":")
+  (setq evil-want-visual-char-semi-exclusive t))
+#+end_src
 
-# playback control
-XF86Audio{Play,Prev,Next}
-       mpc {toggle,prev,next}
+#+begin_src emacs-lisp
+(use-package evil-collection
+  :after evil
+  :config
+  (evil-collection-init))
+#+end_src
 
-# Toggle keyboard layout
-# super + F7
-#      toggle-layout
+#+begin_src emacs-lisp
+(use-package evil-escape
+  :demand t
+  :init
+  (setq evil-escape-excluded-states '(normal visual multiedit emacs motion)
+        evil-escape-excluded-major-modes '(neotree-mode)
+        evil-escape-key-sequence "jk"
+        evil-escape-delay 0.25)
+  :general
+  (:states '(insert replace visual operator)
+    "C-g" #'evil-escape)
+   :config
+  (evil-escape-mode 1)
+  ;; no `evil-escape' in minibuffer
+  (push #'minibufferp evil-escape-inhibit-functions))
+#+end_src
 
-# Toggle Xfce presentation mode
-# XF86LaunchB
-#      toggle-presentation-mode
+#+begin_src emacs-lisp
+(use-package evil-magit
+  :after magit
+  :config
+  (evil-set-initial-state 'git-commit-mode 'insert))
+#+end_src
 
-# monitor brightness
-XF86MonBrightness{Up,Down}
-       light -{A,U} 5
+#+begin_src emacs-lisp
+(use-package evil-org
+  :hook (org-mode . evil-org-mode)
+  :general
+  (nvmap evil-org-mode-map
+    "TAB" 'org-cycle
+    "S-TAB" 'org-cycle)
+  :config
+  (evil-org-set-key-theme)
+  (require 'evil-org-agenda)
+  (evil-org-agenda-set-keys)
+  (nmap evil-org-mode-map
+    [backtab] 'org-shifttab)
+  (after! org-src
+    (define-key org-src-mode-map [remap evil-write] 'org-edit-src-save)
+    (define-key org-src-mode-map [remap evil-save-and-close]
+      (lambda () (interactive)
+        (org-edit-src-save)
+        (org-edit-src-exit)))
+    (define-key org-src-mode-map [remap evil-save-modified-and-close]
+      (lambda () (interactive)
+        (org-edit-src-save)
+        (org-edit-src-exit)))))
+#+end_src
 
-super + apostrophe
-       rofi-light
+#+begin_src emacs-lisp
+(amin--leader-keys
+ "/"  '(:ignore t :wk "search")
+
+ "B"  '(:ignore t :wk "Borg")
+ "BA" 'borg-activate
+ "Ba" 'borg-assimilate
+ "Bb" 'borg-build
+ "Bc" 'borg-clone
+ "Bd" 'epkg-describe-package
+ "Bp" 'epkg-list-packages
+ "Br" 'borg-remove
+ "Bu" 'epkg-update
+
+ "b"  '(:ignore t :wk "buffers")
+ "bb" 'ibuffer-list-buffers
+ "bk" 'kill-this-buffer
+ "bs" 'save-buffer
+
+ "f"  '(:ignore t :wk "files")
+
+ "h"  '(:ignore t :wk "help")
+ "hc" 'describe-char
+ "hf" 'describe-function
+ "hF" 'describe-face
+ "hi" 'info
+ "hk" 'describe-key
+ "hl" 'view-lossage
+ "hv" 'describe-variable
+
+ "o" 'other-window
+
+ "w"  '(:ignore t :wk "window")
+ "wo" 'other-window
+
+ "q"  '(:ignore t :wk "quit")
+ "qq" 'save-buffers-kill-terminal)
 #+end_src
 
 *** [[https://orgmode.org/][Org mode]]
@@ -769,9 +753,18 @@ system.
 In short, my favourite way of life.
 
 #+begin_src emacs-lisp
-(setq org-src-tab-acts-natively t
-      org-src-preserve-indentation nil
-      org-edit-src-content-indentation 0)
+(use-package org
+  :general
+  (amin--leader-keys "bt" 'org-babel-tangle)
+  (amin--leader-keys org-mode-map "i" 'org-edit-special)
+  :config
+  (setq org-src-tab-acts-natively t
+        org-src-preserve-indentation nil
+        org-edit-src-content-indentation 0)
+  :hook (org-mode . org-indent-mode))
+
+(use-package org-notmuch
+  :after (:any org notmuch))
 #+end_src
 
 *** [[https://magit.vc/][Magit]]
@@ -784,9 +777,24 @@ Not just how I do git, but /the/ way to do git.
 
 #+begin_src emacs-lisp
 (use-package magit
+  :general
+  (amin--leader-keys
+   "g"     '(:ignore t :wk "magit")
+   "g SPC" 'magit-status
+   "gs"    'magit-status
+   "gS"    'magit-status-prefix
+   "gB"    'magit-blame
+   "gC"    'magit-clone
+   "gf"    'magit-fetch
+   "gF"    'magit-pull
+   "gP"    'magit-push
+   "gcc"   'magit-commit
+   "gca"   'magit-commit-amend
+   "gbb"   'magit-checkout
+   "gbc"   'magit-branch)
   :defer t
   :bind (("s-g"     . magit-status)
-        ("C-x g"   . magit-status)
+         ("C-x g"   . magit-status)
          ("C-x M-g" . magit-dispatch-popup))
   :config
   (magit-add-section-hook 'magit-status-sections-hook
@@ -808,6 +816,7 @@ There's no way I could top that, so I won't attempt to.
 
 #+begin_src emacs-lisp
 (use-package ivy
+  :defer 1
   :bind
   (:map ivy-minibuffer-map
         ([escape] . keyboard-escape-quit)
@@ -816,6 +825,7 @@ There's no way I could top that, so I won't attempt to.
         ([S-up]   . ivy-previous-history-element)
         ([S-down] . ivy-next-history-element)
         ("DEL"    . ivy-backward-delete-char))
+  :general (amin--leader-keys "," 'ivy-switch-buffer)
   :config
   (setq ivy-wrap t)
   (ivy-mode 1))
@@ -825,20 +835,27 @@ There's no way I could top that, so I won't attempt to.
 
 #+begin_src emacs-lisp
 (use-package swiper
+  :general (:states '(normal motion) "/" 'swiper)
+  (amin--leader-keys "//" 'swiper)
   :bind (([remap isearch-forward]  . swiper)
-        ([remap isearch-backward] . swiper)))
+         ([remap isearch-backward] . swiper)))
 #+end_src
 
 **** Counsel
 
 #+begin_src emacs-lisp
 (use-package counsel
-  :defer 1.5
+  :defer 1
+  :general
+  (amin--leader-keys
+   "fr" 'counsel-recentf
+   "SPC" 'counsel-M-x
+   "."   'counsel-find-file)
   :bind (([remap execute-extended-command] . counsel-M-x)
          ([remap find-file] . counsel-find-file)
          ("s-r"     . counsel-recentf)
          :map minibuffer-local-map
-        ("C-r" . counsel-minibuffer-history))
+         ("C-r" . counsel-minibuffer-history))
   :config
   (counsel-mode 1)
   (defalias 'locate #'counsel-locate))
@@ -920,19 +937,530 @@ TODO: break this giant source block down into individual org sections.
                (list (regexp-quote (system-name)) nil nil)))
 
 (use-package undo-tree
+  :bind (("C-?" . undo-tree-undo)
+         ("M-_" . undo-tree-redo))
   :config
   (global-undo-tree-mode)
-  (setq undo-tree-mode-lighter ""))
+  (setq undo-tree-mode-lighter ""
+        undo-tree-auto-save-history t))
+#+end_src
+
+* Editing
+
+** Company
+
+#+begin_src emacs-lisp
+(use-package company
+  :defer 5
+  :bind
+  (:map company-active-map
+        ([tab] . company-complete-common-or-cycle))
+  :custom
+  (company-idle-delay 0.3)
+  (company-minimum-prefix-length 1)
+  (company-selection-wrap-around t)
+  (company-dabbrev-char-regexp "\\sw\\|\\s_\\|[-_]")
+  :config
+  (global-company-mode t))
 #+end_src
 
+* Syntax and spell checking
+#+begin_src emacs-lisp
+(use-package flycheck
+  :hook (prog-mode . flycheck-mode)
+  :config
+  ;; Use the load-path from running Emacs when checking elisp files
+  (setq flycheck-emacs-lisp-load-path 'inherit)
+
+  ;; Only flycheck when I actually save the buffer
+  (setq flycheck-check-syntax-automatically '(mode-enabled save)))
+#+end_src
 * Programming modes
 
-** Lean mode
+** [[http://alloytools.org][Alloy]] (with [[https://github.com/dwwmmn/alloy-mode][alloy-mode]])
+
+#+begin_src emacs-lisp
+(use-package alloy-mode
+  :config (setq alloy-basic-offset 2))
+#+end_src
+
+** [[https://coq.inria.fr][Coq]] (with [[https://github.com/ProofGeneral/PG][Proof General]])
+
+#+begin_src emacs-lisp
+(use-package proof-site  ; Proof General
+  :load-path "lib/proof-site/generic/")
+#+end_src
+
+** [[https://leanprover.github.io][Lean]] (with [[https://github.com/leanprover/lean-mode][lean-mode]])
 
 #+begin_src emacs-lisp
 (use-package lean-mode
   :bind (:map lean-mode-map
-             ("S-SPC" . company-complete)))
+              ("S-SPC" . company-complete)))
+#+end_src
+
+** Haskell
+
+*** [[https://github.com/haskell/haskell-mode][haskell-mode]]
+
+#+begin_src emacs-lisp
+(use-package haskell-mode
+  :config
+  (setq haskell-indentation-layout-offset 4
+        haskell-indentation-left-offset 4
+        flycheck-checker 'haskell-hlint
+        flycheck-disabled-checkers '(haskell-stack-ghc haskell-ghc)))
+#+end_src
+
+*** [[https://github.com/jyp/dante][dante]]
+
+#+begin_src emacs-lisp
+(use-package dante
+  :after haskell-mode
+  :commands dante-mode
+  :hook (haskell-mode . dante-mode))
+#+end_src
+
+*** [[https://github.com/mpickering/hlint-refactor-mode][hlint-refactor]]
+
+Emacs bindings for [[https://github.com/ndmitchell/hlint][hlint]]'s refactor option. This requires the refact
+executable from [[https://github.com/mpickering/apply-refact][apply-refact]].
+
+#+begin_src emacs-lisp
+(use-package hlint-refactor
+  :bind (:map hlint-refactor-mode-map
+              ("C-c l b" . hlint-refactor-refactor-buffer)
+              ("C-c l r" . hlint-refactor-refactor-at-point))
+  :hook (haskell-mode . hlint-refactor-mode))
+#+end_src
+
+*** [[https://github.com/flycheck/flycheck-haskell][flycheck-haskell]]
+
+#+begin_src emacs-lisp
+(use-package flycheck-haskell)
+#+end_src
+
+*** [[https://github.com/ndmitchell/hlint/blob/20e116a043f2073c57b17b24ae6364b5e433ba7e/data/hs-lint.el][hs-lint.el]]
+:PROPERTIES:
+:header-args+: :tangle lisp/hs-lint.el :mkdirp yes
+:END:
+
+Currently using =flycheck-haskell= with the =haskell-hlint= checker
+instead.
+
+#+begin_src emacs-lisp :tangle no
+;;; hs-lint.el --- minor mode for HLint code checking
+
+;; Copyright 2009 (C) Alex Ott
+;;
+;; Author: Alex Ott <alexott@gmail.com>
+;; Keywords: haskell, lint, HLint
+;; Requirements:
+;; Status: distributed under terms of GPL2 or above
+
+;; Typical message from HLint looks like:
+;;
+;; /Users/ott/projects/lang-exp/haskell/test.hs:52:1: Eta reduce
+;; Found:
+;;   count1 p l = length (filter p l)
+;; Why not:
+;;   count1 p = length . filter p
+
+
+(require 'compile)
+
+(defgroup hs-lint nil
+  "Run HLint as inferior of Emacs, parse error messages."
+  :group 'tools
+  :group 'haskell)
+
+(defcustom hs-lint-command "hlint"
+  "The default hs-lint command for \\[hlint]."
+  :type 'string
+  :group 'hs-lint)
+
+(defcustom hs-lint-save-files t
+  "Save modified files when run HLint or no (ask user)"
+  :type 'boolean
+  :group 'hs-lint)
+
+(defcustom hs-lint-replace-with-suggestions nil
+  "Replace user's code with suggested replacements"
+  :type 'boolean
+  :group 'hs-lint)
+
+(defcustom hs-lint-replace-without-ask nil
+  "Replace user's code with suggested replacements automatically"
+  :type 'boolean
+  :group 'hs-lint)
+
+(defun hs-lint-process-setup ()
+  "Setup compilation variables and buffer for `hlint'."
+  (run-hooks 'hs-lint-setup-hook))
+
+;; regex for replace suggestions
+;;
+;; ^\(.*?\):\([0-9]+\):\([0-9]+\): .*
+;; Found:
+;; \s +\(.*\)
+;; Why not:
+;; \s +\(.*\)
+
+(defvar hs-lint-regex
+  "^\\(.*?\\):\\([0-9]+\\):\\([0-9]+\\): .*[\n\C-m]Found:[\n\C-m]\\s +\\(.*\\)[\n\C-m]Why not:[\n\C-m]\\s +\\(.*\\)[\n\C-m]"
+  "Regex for HLint messages")
+
+(defun make-short-string (str maxlen)
+  (if (< (length str) maxlen)
+      str
+    (concat (substring str 0 (- maxlen 3)) "...")))
+
+(defun hs-lint-replace-suggestions ()
+  "Perform actual replacement of suggestions"
+  (goto-char (point-min))
+  (while (re-search-forward hs-lint-regex nil t)
+    (let* ((fname (match-string 1))
+          (fline (string-to-number (match-string 2)))
+          (old-code (match-string 4))
+          (new-code (match-string 5))
+          (msg (concat "Replace '" (make-short-string old-code 30)
+                       "' with '" (make-short-string new-code 30) "'"))
+          (bline 0)
+          (eline 0)
+          (spos 0)
+          (new-old-code ""))
+      (save-excursion
+        (switch-to-buffer (get-file-buffer fname))
+        (goto-char (point-min))
+        (forward-line (1- fline))
+        (beginning-of-line)
+        (setf bline (point))
+        (when (or hs-lint-replace-without-ask
+                  (yes-or-no-p msg))
+          (end-of-line)
+          (setf eline (point))
+          (beginning-of-line)
+          (setf old-code (regexp-quote old-code))
+          (while (string-match "\\\\ " old-code spos)
+            (setf new-old-code (concat new-old-code
+                                 (substring old-code spos (match-beginning 0))
+                                 "\\ *"))
+            (setf spos (match-end 0)))
+          (setf new-old-code (concat new-old-code (substring old-code spos)))
+          (remove-text-properties bline eline '(composition nil))
+          (when (re-search-forward new-old-code eline t)
+            (replace-match new-code nil t)))))))
+
+(defun hs-lint-finish-hook (buf msg)
+  "Function, that is executed at the end of HLint execution"
+  (if hs-lint-replace-with-suggestions
+      (hs-lint-replace-suggestions)
+      (next-error 1 t)))
+
+(define-compilation-mode hs-lint-mode "HLint"
+  "Mode for check Haskell source code."
+  (set (make-local-variable 'compilation-process-setup-function)
+       'hs-lint-process-setup)
+  (set (make-local-variable 'compilation-disable-input) t)
+  (set (make-local-variable 'compilation-scroll-output) nil)
+  (set (make-local-variable 'compilation-finish-functions)
+       (list 'hs-lint-finish-hook))
+  )
+
+(defun hs-lint ()
+  "Run HLint for current buffer with haskell source"
+  (interactive)
+  (save-some-buffers hs-lint-save-files)
+  (compilation-start (concat hs-lint-command " \"" buffer-file-name "\"")
+                     'hs-lint-mode))
+
+(provide 'hs-lint)
+;;; hs-lint.el ends here
+#+end_src
+
+#+begin_src emacs-lisp :tangle no
+(use-package hs-lint
+  :load-path "lisp/"
+  :bind (:map haskell-mode-map
+              ("C-c l l" . hs-lint)))
+#+end_src
+* Emacs Enhancements
+
+** [[https://github.com/justbur/emacs-which-key][which-key]]
+
+#+begin_quote
+Emacs package that displays available keybindings in popup
+#+end_quote
+
+#+begin_src emacs-lisp
+(use-package which-key
+  :defer 1
+  :config (which-key-mode))
+#+end_src
+
+** [[https://github.com/seagle0128/doom-modeline][doom-modeline]]
+
+#+begin_src emacs-lisp
+(use-package doom-modeline
+  :demand t
+  :config (setq doom-modeline-height 32)
+  :hook (after-init . doom-modeline-init))
+#+end_src
+
+** [[https://github.com/11111000000/tao-theme-emacs][tao-theme]]
+
+#+begin_src emacs-lisp :tangle no
+(use-package tao-theme
+  :demand t
+  :config (load-theme 'tao-yang t))
+#+end_src
+
+** [[https://github.com/maio/eink-emacs][eink-theme]]
+
+#+begin_src emacs-lisp
+(load-theme 'eink t)
+#+end_src
+
+** [[https://github.com/bbatsov/crux][crux]]
+
+#+begin_src emacs-lisp
+(use-package crux
+  :bind (("C-c d"    . crux-duplicate-current-line-or-region)
+         ("C-c M-d"  . crux-duplicate-and-comment-current-line-or-region))
+  :general
+  (amin--leader-keys
+   "bK" 'crux-kill-other-buffers
+   "fc" 'crux-copy-file-preserve-attributes
+   "fD" 'crux-delete-file-and-buffer
+   "fR" 'crux-rename-file-and-buffer))
+#+end_src
+
+** [[https://github.com/alezost/mwim.el][mwim]]
+
+#+begin_src emacs-lisp
+(use-package mwim
+  :bind (("C-a"    . mwim-beginning-of-code-or-line)
+         ("C-e"    . mwim-end-of-code-or-line)
+         ("<home>" . mwim-beginning-of-line-or-code)
+         ("<end>"  . mwim-end-of-line-or-code))
+  :general (:states '(normal visual)
+    "0" 'mwim-beginning-of-code-or-line
+    "$" 'mwim-end-of-code-or-line))
+#+end_src
+
+* Email
+** [[https://notmuchmail.org][notmuch]]
+
+See [[notmuch:id:87muuqsvci.fsf@fencepost.gnu.org][bug follow-up]].
+
+#+begin_src emacs-lisp
+(defvar amin-maildir "~/mail")
+
+(use-package sendmail
+  ;; :ensure nil
+  :config
+  (setq sendmail-program "/usr/bin/msmtp"
+        ; message-sendmail-extra-arguments '("-v" "-d")
+        mail-specify-envelope-from t
+        mail-envelope-from 'header))
+
+(use-package message
+  ;; :ensure nil
+  :config
+  (setq message-kill-buffer-on-exit t
+        message-send-mail-function 'message-send-mail-with-sendmail
+        message-sendmail-envelope-from 'header
+        message-directory "drafts"
+        message-user-fqdn "aminb.org")
+  (add-hook 'message-mode-hook
+            (lambda () (setq fill-column 65
+                        message-fill-column 65)))
+  (add-hook 'message-mode-hook
+            #'flyspell-mode)
+  ;; (add-hook 'notmuch-message-mode-hook #'+doom-modeline|set-special-modeline)
+  ;; TODO: is there a way to only run this when replying and not composing?
+  ;; (add-hook 'notmuch-message-mode-hook
+  ;;           (lambda () (progn
+  ;;                   (newline)
+  ;;                   (newline)
+  ;;                   (forward-line -1)
+  ;;                   (forward-line -1))))
+  ;; (add-hook 'message-setup-hook
+  ;;           #'mml-secure-message-sign-pgpmime)
+  )
+
+(after! mml-sec
+  (setq mml-secure-openpgp-encrypt-to-self t
+        mml-secure-openpgp-sign-with-sender t))
+
+(defun amin/notmuch ()
+  "Delete other windows, then launch `notmuch'."
+  (interactive)
+  (delete-other-windows)
+  (notmuch))
+
+(use-package notmuch
+  :general (amin--leader-keys "m" 'amin/notmuch)
+  :commands notmuch
+  :config
+  (setq notmuch-hello-sections
+        '(notmuch-hello-insert-header
+          notmuch-hello-insert-saved-searches
+          ;; notmuch-hello-insert-search
+          notmuch-hello-insert-alltags)
+        notmuch-search-oldest-first nil
+        notmuch-show-all-tags-list t
+        notmuch-message-headers  ; see bug follow-up above
+        '("Subject" "To" "Cc" "Date" "List-Id" "X-RT-Originator")
+        notmuch-hello-thousands-separator ","
+        notmuch-fcc-dirs
+        '(("amin@aminb.org"            . "amin/Sent")
+          ("amin@gnu.org"              . "gnu/Sent")
+          ("abandali@uwaterloo.ca"     . "\"uwaterloo/Sent Items\"")
+          ("mab@gnu.org"               . "gnu/Sent")
+          ("aminb@gnu.org"             . "gnu/Sent")
+          (".*"                        . "sent"))
+        notmuch-search-result-format
+        '(("date" . "%12s ")
+          ("count" . "%-7s ")
+          ("authors" . "%-40s ")
+          ("subject" . "%s ")
+          ("tags" . "(%s)")))
+  ;; (add-hook 'visual-fill-column-mode-hook
+  ;;           (lambda ()
+  ;;             (when (string= major-mode 'notmuch-message-mode)
+  ;;               (setq visual-fill-column-width 70))))
+  ;; (set! :evil-state 'notmuch-message-mode 'insert)
+  ;; (advice-add #'notmuch-bury-or-kill-this-buffer
+  ;;             :override #'kill-this-buffer)
+  :bind
+  (:map notmuch-hello-mode-map
+        ("g" . notmuch-poll-and-refresh-this-buffer)
+        ("u" . (lambda ()
+                 "Search for `unread' tagged messages"
+                 (interactive)
+                 (notmuch-hello-search "tag:unread")))
+        ("i" . (lambda ()
+                 "Search for `inbox' tagged messages"
+                 (interactive)
+                 (notmuch-hello-search "tag:inbox")))
+        ("l" . (lambda ()
+                 "Search for `latest' tagged messages"
+                 (interactive)
+                 (notmuch-hello-search "tag:latest")))
+        ("e" . (lambda ()
+                 "Search for `encrypted' tagged messages"
+                 (interactive)
+                 (notmuch-hello-search "tag:encrypted")))
+        ("M" . (lambda ()
+                 "Compose new mail and prompt for sender"
+                 (interactive)
+                 (let ((current-prefix-arg t))
+                   (call-interactively #'notmuch-mua-new-mail)))))
+  (:map notmuch-search-mode-map
+        ("g" . notmuch-poll-and-refresh-this-buffer)
+        ("k" . (lambda ()
+                 "Mark message read"
+                 (interactive)
+                 (notmuch-search-tag '("-unread"))
+                 ;; (notmuch-search-archive-thread)
+                 (notmuch-search-next-thread)))
+        ("u" . (lambda ()
+                 "Mark message unread"
+                 (interactive)
+                 (notmuch-search-tag '("+unread"))
+                 (notmuch-search-next-thread)))
+        ("K" . (lambda ()
+                 "Mark message deleted"
+                 (interactive)
+                 (notmuch-search-tag '("-unread" "-inbox" "+deleted"))
+                 (notmuch-search-archive-thread)))
+        ("S" . (lambda ()
+                 "Mark message as spam"
+                 (interactive)
+                 (notmuch-search-tag '("-unread" "-inbox" "-webmasters" "+spam"))
+                 (notmuch-search-archive-thread))))
+  (:map notmuch-tree-mode-map  ; TODO: additional bindings
+        ("S" . (lambda ()
+                 "Mark message as spam"
+                 (interactive)
+                 (notmuch-tree-tag '("-unread" "-inbox" "-webmasters" "+spam"))
+                 (notmuch-tree-archive-thread))))
+)
+
+(use-package counsel-notmuch
+  :general (amin--leader-keys "/m" 'counsel-notmuch))
+
+(after! notmuch-crypto
+  (setq notmuch-crypto-process-mime t))
+
+;; (after! evil
+;;   (mapc (lambda (str) (evil-set-initial-state (car str) (cdr str)))
+;;         '((notmuch-hello-mode . emacs)
+;;           (notmuch-search-mode . emacs)
+;;           (notmuch-tree-mode . emacs))))
+
+(after! recentf
+  (add-to-list 'recentf-exclude (expand-file-name amin-maildir)))
+#+end_src
+
+** supercite
+
+#+begin_src emacs-lisp :tangle no
+(use-package supercite
+  :commands sc-cite-original
+  :init
+  (add-hook 'mail-citation-hook 'sc-cite-original)
+
+  (defun sc-remove-existing-signature ()
+    (save-excursion
+      (goto-char (region-beginning))
+      (when (re-search-forward message-signature-separator (region-end) t)
+        (delete-region (match-beginning 0) (region-end)))))
+
+  (add-hook 'mail-citation-hook 'sc-remove-existing-signature)
+
+  (defun sc-remove-if-not-mailing-list ()
+    (unless (assoc "list-id" sc-mail-info)
+      (setq attribution sc-default-attribution
+            citation (concat sc-citation-delimiter
+                             sc-citation-separator))))
+
+  (add-hook 'sc-attribs-postselect-hook 'sc-remove-if-not-mailing-list)
+
+  :config
+  (defun sc-fill-if-different (&optional prefix)
+    "Fill the region bounded by `sc-fill-begin' and point.
+Only fill if optional PREFIX is different than
+`sc-fill-line-prefix'.  If `sc-auto-fill-region-p' is nil, do not
+fill region.  If PREFIX is not supplied, initialize fill
+variables.  This is useful for a regi `begin' frame-entry."
+    (if (not prefix)
+        (setq sc-fill-line-prefix ""
+              sc-fill-begin (line-beginning-position))
+      (if (and sc-auto-fill-region-p
+               (not (string= prefix sc-fill-line-prefix)))
+          (let ((fill-prefix sc-fill-line-prefix))
+            (unless (or (string= fill-prefix "")
+                        (save-excursion
+                          (goto-char sc-fill-begin)
+                          (or (looking-at ">+  +")
+                              (< (length
+                                  (buffer-substring (point)
+                                                    (line-end-position)))
+                                 65))))
+              (fill-region sc-fill-begin (line-beginning-position)))
+            (setq sc-fill-line-prefix prefix
+                  sc-fill-begin (line-beginning-position)))))
+nil))
+#+end_src
+
+* Blogging
+** [[https://ox-hugo.scripter.co][ox-hugo]]
+
+#+begin_src emacs-lisp
+(use-package ox-hugo
+  :after ox)
 #+end_src
 
 * Post initialization
@@ -945,7 +1473,7 @@ Display how long it took to load the init file.
 #+begin_src emacs-lisp
 (message "Loading %s...done (%.3fs)" user-init-file
          (float-time (time-subtract (current-time)
-                                    ab--before-user-init-time)))
+                                    amin--before-user-init-time)))
 #+end_src
 
 * Footer