11 Finch D. River’s Website
void love_emacs() {

💟 Emacs & Me

I’ve used Emacs since 2014. I have dipped my toes into other editors, namely:

And they’re great, but such pastures were still browner than first expected, and I danced right back to Emacs.

Emacs ain’t perfect, but the malleability (Emacs is ~10 editors for me daily, not 1), introspection (I change Emacs on-the-fly with Emacs), and documentation (features are easily discover-able) make it close to my ideal.

In other words, Emacs has a pretty great programming language, comes out-of-box with good provisions, and is backed by an outstanding community. It’s a killer combination that, for now, for me, can’t be beat.


Emacs Config

Find the whole Emacs config in a <details>…</details> block at the very end, but I’ll mostly use this space to point out parts I think are interesting or personally-important.

I won’t explain as much as I perhaps should; if you use Emacs and want more detail, then use the help functions. Upon further confusion (or you aren’t a member of the Church of Emacs) then let me know, and I’ll update.

N.B. I use Emacs within the terminal. I know: it’s sacrilege; it’s how I like it. Some parts of my config reflect this and I will try to point them out.

Simple Bindings

Every good config starts with some rebinding!

          (let ((map global-map))
            (define-key map (kbd "<C-tab>") #'hs-toggle-hiding)
            (define-key map (kbd "<mouse-4>") #'scroll-down-line)
            (define-key map (kbd "<mouse-5>") #'scroll-up-line)
            (define-key map (kbd "<mouse-6>") #'ignore)
            (define-key map (kbd "<mouse-7>") #'ignore)
            (define-key map (kbd "C-'") #'comment-line)
            (define-key map (kbd "C-x C-b") #'bs-show)
            (define-key map (kbd "M-R") #'query-replace-regexp)
            (define-key map (kbd "M-n") #'forward-paragraph)
            (define-key map (kbd "M-o") #'other-window)
            (define-key map (kbd "M-p") #'backward-paragraph)
            (define-key map (kbd "M-r") #'query-replace))

          (defvar 11fd/map (let ((map (make-sparse-keymap)))
                             (define-key map "c" 'switch-to-completions)
                             (define-key map "h" 'hs-hide-all)
                             (define-key map "r" 'recompile)
                             (define-key map "s" 'hs-show-all)
                             (global-set-key "\e\e" map)) "My key-map.")

          (define-key query-replace-map "a" 'automatic) ; mnemonic: "all"
          (define-key query-replace-map "p" 'backup)    ; mnemonic: "prev"
          (windmove-default-keybindings)
        

Using M-n for forward-paragraph and M-p for backward-paragraph are great for moving around a file quickly.

M-r for query-replace is nicer than the awful M-%.

ESC ESC is a good location for occasionally-used bindings (from EmacsWiki: Choosing Keys To Bind). Setting up a custom keymap is an idea stolen from Lars Tveito’s config and goes well with the later modal-editing section.

a and p in query replace are easier to remember. automatic replaces all remaining matches without prompt, and backup moves point back to previous match.

Extra Term Keys

You may find that some of the above combos, such as C-', don’t work. Emacs can interpret xterm-style keyboard escape sequences as long as the termcap fits.

          key_bindings:
            # …
            # Use with xterm-256color
            - { key: Apostrophe, mods: Control, chars: "\e[27;5;39~" }
            - { key: Comma, mods: Control, chars: "\e[27;5;44~" }
            - { key: Minus, mods: Control, chars: "\e[27;5;45~" }
            - { key: Period, mods: Control, chars: "\e[27;5;46~" }
            - { key: Semicolon, mods: Control, chars: "\e[27;5;59~" }
            - { key: Tab, mods: Control, chars: "\e[27;5;9~" }
            - { key: Return, mods: Control, chars: "\e[27;5;13~" }
        

Theme

wombat is pretty, as is tsdh-dark. I include the line that uses the terminal background-color, but this should be removed for GUI editing.

          (load-theme 'wombat)
          (set-face-background 'default "unspecified-bg") ;; Terminal only
          (set-face-italic 'font-lock-comment-face t)
        

Folding

hideshow, often abbreviated to just hs- in prefixes, is Emacs’ built-in code-folding package.

          (setq-default hs-allow-nesting t
                        hs-isearch-open t)

          (add-hook 'hs-minor-mode-hook #'reveal-mode)
          (add-hook 'prog-mode-hook #'hs-minor-mode)
        

Auto-unfolding

I like to have my code temporarily unfold when I pass over it, which in Emacs is easily performed via reveal-mode.
Setting hs-isearch-open means that I can still search within folded blocks, and they are opened automatically.

Mouse

This one isn’t all that amazing, and I don’t use it that often, but it’s something I’ve been consistently asked about.

          (xterm-mouse-mode 1)
        

xterm-mouse-mode requires $TERM to be xterm-compatible. I use Alacritty, which can use xterm-256color.

          env:
            TERM: xterm-256color
        

Minibuffer Completion

There are various installable packages like smex that enhance M-x, but icomplete is built-in and works well enough for me.

          (icomplete-mode 1)
          (ido-mode 1)

          (require 'icomplete)
          (let ((map icomplete-minibuffer-map))
            (define-key map [left] #'icomplete-backward-completions)
            (define-key map [right] #'icomplete-forward-completions)
            (define-key map [?\r] #'icomplete-force-complete-and-exit))
        

Package Config

          ;;; Workaround for https://debbugs.gnu.org/34341 in GNU Emacs <= 26.3.
          (when (and (version< emacs-version "26.3") (>= libgnutls-version 30603))
            (setq-default gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3"))
          (require 'package)
          (add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/"))

          (setq package-selected-packages nil)

          (defun with-package/install-packages ()
            "Install not-installed packages found by `with-package'."
            (package-refresh-contents)
            (package-install-selected-packages)
            (package-autoremove)
            (package-quickstart-refresh)
            (byte-recompile-file user-init-file 'force 'ask 'reload))

          (defmacro with-package (package &rest body)
            "Package-specific configuration macro.
          Add PACKAGE to `package-selected-packages', attempt to
          `require' PACKAGE, evaluate BODY if successful, else add
          `with-package/install-packages' to `after-init-hook'."
            (declare (indent 1))
            `(and (push ,package package-selected-packages)
                  (if (require ,package nil 'noerror)
                      (progn ,@body)
                    (add-hook 'after-init-hook #'with-package/install-packages) nil)))
        

I’ve tried use-package and leaf, both advanced, featureful, and convenient package-configuration macros. In my opinion, they abstract too much: what might usually be Emacs Lisp errors are now package-config-macro errors, which can be more difficult to diagnose, especially for beginners.

It’s a worthy tradeoff in a language with powerful, introspective macros, but I prefer to get my hands dirty.

I 5-min’d a method that checked & installed a list of packages, which can then be configured as if built-in. It had downsides, and I kept meaning to make it into a simple macro.

Lo and behold, someone beats me to it, Declarative Package Configuration in 5 Lines of Emacs Lisp. I made some tweaks, there’s a few more lines involved, but Emacs now automatically installs new packages, and then reloads the config.

Examples

          ;; Just install
          (with-package 'fish-mode)
        
          ;; Install and run command
          (with-package 'flycheck
            (global-flycheck-mode))
        
Extra example…
            ;; Slightly-more complex example
            (with-package 'd-mode
              (add-hook 'd-mode-hook #'auto-fill-mode)
              (add-hook 'd-mode-hook #'display-line-numbers-mode)
              (add-hook 'd-mode-hook #'follow-mode)
              (add-hook 'd-mode-hook #'subword-mode)
              (add-hook 'd-mode-hook #'11fd/d-mode-setup 50)
              (defun 11fd/d-mode-setup ()
                "My `d-mode' preferences."
                (add-hook 'before-save-hook #'whitespace-cleanup 0 'local)
                (c-set-style "linux")
                (c-set-offset 'inline-open '0)
                (c-toggle-auto-newline 1)
                (setq-local c-basic-offset 4
                            tab-width 4)))
          

Attention, Vimmers! Emacs is not anti-modal!
The way that Emacs handles ‘plugin’ keybindings is already (sorta) modal.
Viper is included OOTB, there are installable binding schemes (e.g. Boon, Meow, Xah-Fly-Keys, &c.), plus DIY-modality packages. Making your own modal scheme without packages is itself cheap and easy.
PSA over :)

Most of the time, I admit to using the default Emacs bindings. I’m pretty quick with them, but — especially if I’m using my laptop’s keyboard and can’t palm-press ctrl — I sometimes like to have some basic modality.

I’ve used viper, which is a built-in Vi-like editing scheme, but I’m too set in my Emacs-y ways.

Modalka is a simple (< 100 loc) package which works nicely for me, but if you want to build up your own Vim-like grammars, then I hear RYO‑Modal is better.

          (with-package 'modalka
            (setq modalka-excluded-modes '(bs-mode dired-mode package-menu-mode
                                                   mu4e-headers-mode))
            (modalka-remove-kbd "q")
            (dolist (pair '(("'" "C-'") ("," "C-,") ("." "C-.") ("/" "C-/")
                            ("0" "C-x )") ("1" "C-x 1") ("2" "C-x 2") ("3" "C-x 3")
                            ("4" "C-x C-k 4") ("5" "C-x C-k 5") ("6" "C-x C-k 6")
                            ("7" "C-x C-k 7") ("8" "C-x 8 RET") ("9" "C-x (")
                            ("SPC" "C-SPC")
                            ("a" "C-a") ("b" "C-b") ("d" "C-d") ("e" "C-e")
                            ("f" "C-f") ("g" "C-g") ("h" "DEL") ("i" "C-i")
                            ("j" "M-j") ("k" "C-k") ("l" "C-l") ("m" "M-m")
                            ("n" "C-n") ("o" "C-o") ("p" "C-p") ("r" "C-r")
                            ("s" "C-s") ("t" "C-t") ("v" "C-v") ("w" "C-w")
                            ("y" "C-y") ("z" "C-z")))
              (modalka-define-kbd (car pair) (nth 1 pair)))
            (let ((map modalka-mode-map))
              (define-key map "x" ctl-x-map)
              (define-key map "u" #'modalka-global-mode)
              (define-key map "c" 11fd/map)
              (define-key map "K" kmacro-keymap)
              (define-key map "?" help-map))
            (global-set-key [select] #'modalka-global-mode)
            (global-set-key [end] #'modalka-global-mode))
        

Unbound Keys?

By default, unbound top-level keys still insert the character they represent, but they can be remapped.

          (define-key modalka-mode-map [remap self-insert-command] #'undefined)
          ;; Or
          (define-key modalka-mode-map [remap self-insert-command] #'ignore)
        

Non-Emacs .el

Certain programs include their respective Emacs mode as part of the main installation, such as Erlang and Agda. I find an autoload statement is the best way to manage this.

          (autoload 'agda2-mode (shell-command-to-string "agda-mode locate")
            "Load agda2-mode from cabal install" 'interactive)
          (add-to-list 'auto-mode-alist '("\\.l?agda\\'" . agda2-mode))
        

Entire Config

Here it is, the whole shebang. There are parts that need to be cleaned up, and some package-specific configuration has been removed for brevity.

Probably, I should link to a git forge with this in it, but I’m lazy, and this allows for quick-referencing.

~/.config/emacs/init.el:
            ;;; init.el --- 11fdriver's simple-ish Emacs config.
            ;;; Commentary:
            ;;; A comfortable Emacs config with help from the with-package macro
            ;;; (https://cosine.blue/emacs-with-package.html).
            ;;; Code:
            (defalias 'yes-or-no-p 'y-or-n-p)

            (load-theme 'wombat)
            (set-face-background 'default "unspecified-bg")
            (set-face-italic 'font-lock-comment-face t)

            (setq-default
             backward-delete-char-untabify-method 'hungry
             desktop-save t
             desktop-load-locked-desktop t
             disabled-command-function nil
             fill-column 68
             hs-allow-nesting t
             hs-isearch-open t
             ido-enable-flex-matching t
             ispell-extra-args '("--sug-mode=ultra" "--lang=en_US" "--keyboard=dvorak")
             sentence-end-double-space nil
             show-paren-delay 0
             whitespace-style '(face trailing empty space-after-tab space-before-tab))

            (add-to-list 'desktop-locals-to-save 'buffer-undo-list)
            (add-to-list 'auto-mode-alist '("\\.do\\'" . sh-mode))

            (require 'webjump)
            (add-to-list 'webjump-sites '("11fdriver" . "11fdriver.neocities.org/"))
            (add-to-list 'webjump-sites '("Neocities" . "neocities.org/"))

            (gpm-mouse-mode 0)
            (menu-bar-mode 0)
            (tool-bar-mode 0)
            (tooltip-mode 0)
            (when (fboundp 'scroll-bar-mode)
              (scroll-bar-mode 0))
            (desktop-save-mode 1)
            (electric-indent-mode 1)
            (electric-pair-mode 1)
            (icomplete-mode 1)
            (ido-mode 1)
            (save-place-mode 1)
            (savehist-mode 1)
            (show-paren-mode 1)
            (winner-mode 1)
            (xterm-mouse-mode 1)

            (add-hook 'hs-minor-mode-hook #'reveal-mode)
            (add-hook 'prog-mode-hook #'flyspell-prog-mode)
            (add-hook 'prog-mode-hook #'hs-minor-mode)
            (add-hook 'prog-mode-hook #'whitespace-mode)
            (add-hook 'text-mode-hook #'flyspell-mode)
            (add-hook 'text-mode-hook #'visual-line-mode)

            (defconst custom-file (expand-file-name "custom.el" user-emacs-directory))
            (load custom-file 'noerror 'nomessage 'nosuffix 'must-suffix)
            (setq backup-by-copying t
                  backup-directory-alist `(("." . ,(locate-user-emacs-file "backups")))
                  delete-old-versions t
                  vc-make-backup-files t
                  version-control t)

            (require 'icomplete)
            (let ((map icomplete-minibuffer-map))
              (define-key map [left] #'icomplete-backward-completions)
              (define-key map [right] #'icomplete-forward-completions)
              (define-key map [?\r] #'icomplete-force-complete-and-exit))

            (let ((map global-map))
              (define-key map (kbd "<C-tab>") #'hs-toggle-hiding)
              (define-key map (kbd "<mouse-4>") #'scroll-down-line)
              (define-key map (kbd "<mouse-5>") #'scroll-up-line)
              (define-key map (kbd "<mouse-6>") #'ignore)
              (define-key map (kbd "<mouse-7>") #'ignore)
              (define-key map (kbd "C-'") #'comment-line)
              (define-key map (kbd "C-x C-b") #'bs-show)
              (define-key map (kbd "C-x c") #'save-buffers-kill-terminal)
              (define-key map (kbd "C-x f") #'find-file)
              (define-key map (kbd "C-x w") #'write-file)
              (define-key map (kbd "M-/") #'hippie-expand)
              (define-key map (kbd "M-R") #'query-replace-regexp)
              (define-key map (kbd "M-n") #'forward-paragraph)
              (define-key map (kbd "M-o") #'other-window)
              (define-key map (kbd "M-p") #'backward-paragraph)
              (define-key map (kbd "M-r") #'query-replace))

            (defvar 11fd/map (let ((map (make-sparse-keymap)))
            (define-key map "c" 'switch-to-completions)
            (define-key map "h" 'hs-hide-all)
            (define-key map "r" 'recompile)
            (define-key map "s" 'hs-show-all)
            (define-key map "t" 'hs-toggle-hiding)
            (global-set-key "\e\e" map)) "My key-map.")

            (define-key emacs-lisp-mode-map (kbd "C-c C-c") #'emacs-lisp-byte-compile)
            (define-key query-replace-map "a" 'automatic) ; mnemonic: "all"
            (define-key query-replace-map "p" 'backup)    ; mnemonic: "prev"
            (windmove-default-keybindings)

            ;;; Workaround for https://debbugs.gnu.org/34341 in GNU Emacs <= 26.3.
            (when (and (version< emacs-version "26.3") (>= libgnutls-version 30603))
              (setq-default gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3"))
            (require 'package)
            (add-to-list 'package-archives '("melpa" . "http://melpa.org/packages/"))
            (setq package-selected-packages nil)

            (defun with-package/install-packages ()
              "Install not-installed packages found by `with-package'."
              (package-refresh-contents)
              (package-install-selected-packages)
              (package-autoremove)
              (package-quickstart-refresh)
              (byte-recompile-file user-init-file 'force 'ask 'reload))

            (defmacro with-package (package &rest body)
              "Package-specific configuration macro.
            Add PACKAGE to `package-selected-packages', attempt to
            `require' PACKAGE, evaluate BODY if successful, else add
            `with-package/install-packages' to `after-init-hook'."
              (declare (indent 1))
              `(and (push ,package package-selected-packages)
                    (if (require ,package nil 'noerror)
            (progn ,@body)
            (add-hook 'after-init-hook #'with-package/install-packages) nil)))

            (with-package 'avy
              (setq-default avy-keys '(?a ?o ?e ?u ?i ?d ?h ?t ?n ?s)) ; Dvorak keys
              (global-set-key (kbd "C-z") #'avy-goto-char-timer)
              (define-key isearch-mode-map (kbd "C-z") #'avy-isearch)
              (modify-face 'avy-lead-face "bright white" "bright red")
              (modify-face 'avy-lead-face-0 "bright white" "bright blue")
              (modify-face 'avy-lead-face-1 "bright black" "bright white")
              (modify-face 'avy-lead-face-2 "bright white" "bright magenta"))

            (with-package 'company
              (add-hook 'prog-mode-hook #'company-mode))

            (with-package 'd-mode
              (add-hook 'd-mode-hook #'auto-fill-mode)
              (add-hook 'd-mode-hook #'display-line-numbers-mode)
              (add-hook 'd-mode-hook #'follow-mode)
              (add-hook 'd-mode-hook #'subword-mode)
              (add-hook 'd-mode-hook #'11fd/d-mode-setup 50)
              (defun 11fd/d-mode-setup ()
                "My `d-mode' preferences."
                (add-hook 'before-save-hook #'whitespace-cleanup 0 'local)
                (c-set-style "linux")
                (c-set-offset 'inline-open '0)
                (c-toggle-auto-newline 1)
                (setq-local c-basic-offset 4
            tab-width 4)))

            (with-package 'fish-mode)

            (with-package 'flycheck
              (global-flycheck-mode))

            (with-package 'mhtml-mode
              (define-skeleton html-tag-wrap-skel
                "Skeleton to wrap selection with tag.
            Goes well with `sgml-electric-tag-pair-mode'."
                > "<p" - ">" \n
                > _ \n
                > "</p>" > \n )

              (define-skeleton html-inline-tag-wrap-skel
                "Like `html-tag-wrap-skel' but on one line."
                > "<em" - ">" _ "</em>")

              (define-skeleton html-date-skel
                "Skeleton to insert <time datetime=…>…</time>."
                "Date: "
                > "<time datetime=\""
                str | (setq date (format-time-string "%Y-%m-%d")) "\">"
                str | date "</time>" > \n \n)

              (defun refresh-firefox ()
                "Quick 'n' dirty func to refresh current FF page."
                (interactive)
                (async-shell-command
                 "xdotool search --name 'Mozilla Firefox' key F5"
                 "*Refresh Firefox*")
                (quit-window nil (get-buffer-window "*Refresh Firefox*")))

              (let ((map mhtml-mode-map))
                (define-key map (kbd "C-c d") #'html-date-skel)
                (define-key map (kbd "C-c i") #'html-inline-tag-wrap-skel)
                (define-key map (kbd "<f5>") #'refresh-firefox)
                (define-key map (kbd "C-c t") #'html-tag-wrap-skel))

              (add-hook 'mhtml-mode-hook #'sgml-electric-tag-pair-mode)
              (add-hook 'mhtml-mode-hook #'electric-quote-local-mode)
              (add-hook 'mhtml-mode-hook #'11fd/mhtml-mode-setup 50)
              (defun 11fd/mhtml-mode-setup ()
                "My `mhtml-mode' preferences."
                (add-hook 'before-save-hook #'whitespace-cleanup 0 'local)
                (setq indent-tabs-mode nil)))

            (with-package 'modalka
              (setq modalka-excluded-modes '(bs-mode dired-mode package-menu-mode
            mu4e-headers-mode))
              (modalka-remove-kbd "q")
              (dolist (pair '(("'" "C-'") ("," "C-,") ("." "C-.") ("/" "C-/")
            ("0" "C-x )") ("1" "C-x 1") ("2" "C-x 2") ("3" "C-x 3")
            ("4" "C-x C-k 4") ("5" "C-x C-k 5") ("6" "C-x C-k 6")
            ("7" "C-x C-k 7") ("8" "C-x 8 RET") ("9" "C-x (")
            ("SPC" "C-SPC")
            ("a" "C-a") ("b" "C-b") ("d" "C-d") ("e" "C-e")
            ("f" "C-f") ("g" "C-g") ("h" "DEL") ("i" "C-i")
            ("j" "M-j") ("k" "C-k") ("l" "C-l") ("m" "M-m")
            ("n" "C-n") ("o" "C-o") ("p" "C-p") ("r" "C-r")
            ("s" "C-s") ("t" "C-t") ("v" "C-v") ("w" "C-w")
            ("y" "C-y") ("z" "C-z")))
                (modalka-define-kbd (car pair) (nth 1 pair)))
              (let ((map modalka-mode-map))
                ;; (define-key map [remap self-insert-command] 'undefined)
                (define-key map "x" ctl-x-map)
                (define-key map "u" #'modalka-global-mode)
                (define-key map "c" 11fd/map)
                (define-key map "K" kmacro-keymap)
                (define-key map "?" help-map))
              (global-set-key [select] #'modalka-global-mode)
              (global-set-key [end] #'modalka-global-mode))

            (with-package 'rainbow-delimiters
              (add-hook 'prog-mode-hook #'rainbow-delimiters-mode))

            (with-package 'xclip
              (xclip-mode))

            (with-package 'yaml-mode)

            (with-package 'zzz-to-char
                (global-set-key (kbd "M-z") #'zzz-up-to-char))

            (autoload 'agda2-mode (shell-command-to-string "agda-mode locate")
              "Load agda2-mode from cabal install" 'interactive)
            (add-to-list 'auto-mode-alist '("\\.l?agda\\'" . agda2-mode))
            ;;; init.el ends here