Skip to content

Latest commit

 

History

History
3302 lines (2555 loc) · 107 KB

configuration.org

File metadata and controls

3302 lines (2555 loc) · 107 KB

Emacs Configuration

Configure use-package

I use use-package to install and configure my packages. My init.el includes the initial setup for package.el and ensures that use-package is installed, since I wanna do that right away.

This makes sure that use-package will install the package if it’s not already available. It also means that I should be able to open Emacs for the first time on a fresh Debian box and have my whole environment automatically installed. I’m not totally sure about that, but we’re gettin’ close.

(require 'use-package-ensure)
(setq use-package-always-ensure t)

Defer loading packages unless explicitly demanded.

(setq use-package-always-defer t)

Always compile packages, and use the newest version available.

(use-package auto-compile
  :demand t
  :config (auto-compile-on-load-mode))

(setq load-prefer-newer t)

Disable deprecation warnings about cl. The cl library has been deprecated, but lots of packages still use it. I can’t control that, but I can disable the warnings.

(setq byte-compile-warnings '(cl-functions))

If an Emacs package relies on the installation of a system package, install that package (for example, deadgrep doesn’t work if ripgrep isn’t installed). This uses the system package manager (Debian’s apt, in my case).

(use-package use-package-ensure-system-package
  :demand t
  :custom
  (system-packages-package-manager 'apt))

Don’t pop up a buffer to warn me about deprecations and other minor issues.

(setq warning-minimum-level :emergency)

Use sensible-defaults.el

Use sensible-defaults.el for some basic settings.

(use-package sensible-defaults
  :load-path "~/code/personal/sensible-defaults.el"
  :demand t

  :config
  (sensible-defaults/use-all-settings)
  (sensible-defaults/use-all-keybindings)
  (sensible-defaults/backup-to-temp-directory))

Trigger garbage collection when I’ve been idle for five seconds and memory usage is over 16 MB.

(use-package gcmh
  :demand t

  :init
  (setq gcmh-idle-delay 5
        gcmh-high-cons-threshold (* 16 1024 1024))
  :config
  (gcmh-mode))

evil-mode

Load and configure evil-mode.

  • I’d prefer not to expand abbrevs when I hit escape. That’s always jarring and usually not what I want. In particular, it makes working with Coq really frustrating.
  • Don’t automatically load Evil bindings in different modes.
  • Bind C-p to fuzzy-finding files in the current project. We also need to explicitly set that in a few other modes.
(use-package evil
  :demand t

  :init
  (setq evil-respect-visual-line-mode t
        evil-want-abbrev-expand-on-insert-exit nil
        evil-want-keybinding nil)

  :config
  (evil-mode 1)

  (evil-define-key '(normal insert) 'global (kbd "C-p") 'project-find-file)

  (evil-define-key 'normal org-mode-map (kbd "TAB") 'org-cycle)
  (evil-define-key 'insert org-mode-map (kbd "S-<right>") 'org-shiftright)
  (evil-define-key 'insert org-mode-map (kbd "S-<left>") 'org-shiftleft)

  (fset 'evil-visual-update-x-selection 'ignore))

Install evil-collection, which provides evil-friendly bindings for many modes.

(use-package evil-collection
  :after evil
  :demand t

  :config
  (setq evil-collection-mode-list
        '(comint
          deadgrep
          dired
          ediff
          elfeed
          eww
          ibuffer
          info
          magit
          mu4e
          package-menu
          pdf-view
          proced
          replace
          vterm
          which-key))

  (evil-collection-init))

Enable surround everywhere.

(use-package evil-surround
  :after evil
  :demand t
  :config
  (global-evil-surround-mode 1))

Use evil with Org agendas.

(use-package evil-org
  :after (evil org)
  :demand t

  :config
  (require 'evil-org-agenda)
  (evil-org-agenda-set-keys))

Extend the exec-path and PATH as needed

(defun +append-to-path (path)
  "Add a path both to the $PATH variable and to Emacs' exec-path."
  (let ((full-path (expand-file-name path)))
    (setenv "PATH" (concat (getenv "PATH") ":" full-path))
    (add-to-list 'exec-path full-path)))

(setq +local-bin-paths
      '("/usr/local/bin"
        "~/.bin"
        "~/bin"
        "~/.cargo/bin"
        "~/.cabal/bin"
        "~/.opam/default/bin"
        "~/.local/bin"))

(dolist (path +local-bin-paths)
  (+append-to-path path))

Utility functions

Define a big ol’ bunch of handy utility functions.

(defun +visit-last-migration ()
  "Open the most recent Rails migration."
  (interactive)
  (let ((migrations
         (directory-files
          (expand-file-name "db/migrate" (project-root (project-current))) t)))
    (find-file (car (last migrations)))))

(defun +image-path-p (path)
  "Return true if the path corresponds to an image file."
  (member (downcase (or (file-name-extension path) ""))
          '("bmp" "gif" "jpeg" "jpg" "png" "tiff")))

(defun +quit-window-and-kill ()
  "Quit the current window and kill the buffer. Handy for pop-ups."
  (interactive)
  (quit-window t))

(defun +maphash (f hash)
  "Call function `f' on each (key, value) pair in `hash', returning the results in a list."
  (let ((acc '()))
    (maphash (lambda (k v) (setq acc (cons (funcall f k v) acc)))
             hash)
    acc))

UI preferences

Don’t resize the frame

By default, Emacs attempts to resize each frame to maintain a certain number of characters in each line, so this width varies depending on the font used. This operation is a bit expensive, and since I use i3, a tiling window manager, it’s also completely unnecessary, since the frame will be resized differently regardless. Just skip the whole thing instead.

(setq frame-inhibit-implied-resize t)

Tweak window chrome

I don’t usually use the menu or scroll bar, and they take up useful space.

(tool-bar-mode 0)
(menu-bar-mode 0)
(scroll-bar-mode -1)

There’s a tiny scroll bar that appears in the minibuffer window. This disables that:

(set-window-scroll-bars (minibuffer-window) nil nil)

Enable smooth scrolling

This is especially nice when I’ve got a document with embedded images or rendered equations. Or on the rare occasions I use a mouse.

In certain read-only modes I like to bind J and K to scrolling. That’s nice for reading mail or RSS items. This provides a +bind-scroll-keys function I can use to enable that for a given key map.

(pixel-scroll-precision-mode 1)

(defvar +scroll-delta 180)

(defun +scroll-up-some ()
  (interactive)
  (pixel-scroll-precision-scroll-up +scroll-delta))

(defun +scroll-down-some ()
  (interactive)
  (pixel-scroll-precision-scroll-down +scroll-delta))

(defun +bind-scroll-keys (mode-map)
  (evil-define-key '(motion normal) mode-map (kbd "K") '+scroll-up-some)
  (evil-define-key '(motion normal) mode-map (kbd "J") '+scroll-down-some))

Configure a pretty modeline

(use-package moody
  :demand t

  :custom
  (x-underline-at-descent-line t)

  :config
  (moody-replace-mode-line-buffer-identification)
  (moody-replace-vc-mode)
  (moody-replace-eldoc-minibuffer-message-function))

Load up a theme

This sets up the current theme.

(setq custom-theme-directory
             (concat user-emacs-directory "themes"))
(load-theme 'witchhazel t)

Maintain window configurations with winner-mode

This binds C-c <left> and C-c <right> to undo and redo window configuration changes.

(winner-mode 1)

Reopen the same files with restarting

(desktop-save-mode 1)

Use minions to hide all minor modes

I never want to see a minor mode, and manually adding :diminish to every use-package declaration is a hassle. This uses minions to hide all the minor modes in the modeline. Nice!

(use-package minions
  :demand t

  :custom
  (minions-mode-line-delimiters (cons "" ""))

  :config
  (defun +set-minions-mode-line-lighter ()
    (setq minions-mode-line-lighter
          (if (display-graphic-p) "" "#")))

  (add-hook 'server-after-make-frame-hook #'+set-minions-mode-line-lighter)

  (minions-mode 1))

Scroll conservatively

When point goes outside the window, Emacs usually recenters the buffer point. I’m not crazy about that. This changes scrolling behavior to only scroll as far as point goes.

(setq scroll-conservatively 100)

Set font and configure font resizing

(set-face-attribute 'default nil
                    :family "Fantasque Sans Mono"
                    :height 80)

(set-face-attribute 'fixed-pitch nil
                    :family "Fantasque Sans Mono"
                    :height 80)

(set-face-attribute 'variable-pitch nil
                    :family "ETBembo"
                    :height 80)

(use-package default-text-scale
  :bind
  (("C-)" . default-text-scale-reset)
   ("C-=" . default-text-scale-increase)
   ("C--" . default-text-scale-decrease)))

Highlight the current line

global-hl-line-mode softly highlights the background color of the line containing point. It makes it a bit easier to find point, and it’s useful when pairing or presenting code.

(when (display-graphic-p)
  (global-hl-line-mode))

Highlight uncommitted changes

Use the diff-hl package to highlight changed-and-uncommitted lines when programming.

(use-package diff-hl
  :config
  :hook ((text-mode prog-mode vc-dir-mode) . turn-on-diff-hl-mode))

Project management

I use a few packages in virtually every programming or writing environment to manage the project, handle auto-completion, search for terms, and deal with version control. That’s all in here.

ripgrep

Install ripgrep to provide search within projects. Search even “hidden” dotfiles, but not .git repos.

(use-package deadgrep
  :ensure-system-package (rg . ripgrep)
  :commands (deadgrep deadgrep--read-search-term)

  :config
  (evil-define-key 'motion deadgrep-mode-map (kbd "C-p") 'project-find-file)

  (defun deadgrep--include-args (rg-args)
    (push "--hidden" rg-args)
    (push "--glob=!.git/" rg-args))
  (advice-add 'deadgrep--arguments
              :filter-return #'deadgrep--include-args))

comint

Treat comint sessions more like a shell.

(use-package comint-mode
  :ensure nil
  :bind ("C-l" . comint-clear-buffer)

  :config
  (evil-define-key '(normal insert) comint-mode-map (kbd "C-d") '+kill-current-buffer))

completion-at-point

Use corfu for a pop-up completions menu.

(use-package corfu
  :bind
  (:map corfu-map
        ("TAB" . corfu-next)
        ([tab] . corfu-next)
        ("S-TAB" . corfu-previous)
        ([backtab] . corfu-previous))

  :custom
  (tab-always-indent 'complete)
  (corfu-auto t)
  (corfu-cycle t)
  (corfu-preselect 'prompt)

  :init
  (global-corfu-mode))

docker

I use Docker less often than you might expect for a person who mostly does Web development, but when I do =docker.el= provides a convenient, magit-like interface for managing containers.

(use-package docker
  :ensure-system-package docker)

dumb-jump

The dumb-jump package works well enough in a ton of environments, and it doesn’t require any additional setup. I’ve bound its most useful command to M-..

(use-package dumb-jump
  :demand t

  :init
  (setq xref-show-definitions-function #'xref-show-definitions-completing-read)

  :custom
  (xref-search-program 'ripgrep)

  :config
  (add-hook 'xref-backend-functions #'dumb-jump-xref-activate)
  (define-key evil-normal-state-map (kbd "M-.") 'xref-find-definitions))

ediff

When using ediff to compare file, show files side by side and don’t split the control panel into a separate frame.

(use-package ediff
  :ensure nil

  :config
  (setq ediff-window-setup-function 'ediff-setup-windows-plain)
  (setq ediff-split-window-function 'split-window-horizontally))

flycheck

I’d like to enable flycheck all kinds of places, but I don’t really need the keybindings (and they conflict with, for example, the default C-c ! binding for org-time-stamp-inactive).

(use-package flycheck
  :demand t

  :config
  (unbind-key "C-c !" flycheck-mode-map)
  (global-flycheck-mode))

magit

I use magit to handle version control. It’s lovely, but I tweak a few things:

  • I bring up the status menu with C-x g.
  • The default behavior of magit is to ask before pushing. I haven’t had any problems with accidentally pushing, so I’d rather not confirm that every time.
  • Per tpope’s suggestions, highlight commit text in the summary line that goes beyond 50 characters.
  • I’d like to start in the insert state when writing a commit message.
  • Always take up the whole frame. I’m often on a laptop, where this is especially convenient, but generally I want additional git-related space more than I want more windows.
(use-package magit
  :ensure-system-package git
  :hook (with-editor-mode . evil-insert-state)
  :bind ("C-x g" . magit-status)

  :config
  (use-package git-commit)
  (use-package magit-section)
  (use-package with-editor)

  (require 'git-rebase)

  (defun +get-author-parse-line (key value domain)
    (let* ((values (mapcar #'s-trim (s-split ";" value)))
           (name (car values))
           (email (or (cadr values) key)))
      (format "%s <%s@%s>" name email domain)))

  (defun +git-authors ()
    (let* ((config (yaml-parse-string (f-read-text "~/.git-authors")))
           (domain (gethash 'domain (gethash 'email config)))
           (authors '()))
      (+maphash (lambda (k v) (+git-author-parse-line k v domain))
                (gethash 'authors config))))

  (defun +insert-git-coauthor ()
    "Prompt for co-author and insert a co-authored-by block."
    (interactive)
    (insert (format "Co-authored-by: %s\n"
                    (completing-read "Co-authored by:" (+git-authors)))))

  (setq git-commit-summary-max-length 50
        magit-bury-buffer-function 'magit-restore-window-configuration
        magit-display-buffer-function 'magit-display-buffer-fullframe-status-topleft-v1
        magit-push-always-verify nil))

I’m also partial to git-timemachine, which lets you quickly page through the history of a file.

(use-package git-timemachine)

occur

You’d think evil-collection would include bindings like this, but seemingly not!

(use-package occur
  :ensure nil

  :config
  (evil-define-key 'normal occur-mode-map (kbd "g r") 'revert-buffer)
  (evil-define-key 'normal occur-mode-map (kbd "q") '+quit-window-and-kill))

project.el

  • Bind searching within the project to C-c v.
  • Treat a directory containing .dir-locals.el as a project root (useful for projects not under version control).
  • When I switch projects, just open a dired buffer at the project root rather than asking whether I want to open a file, search, etc.
(use-package project
  :bind (("C-c v" . deadgrep)
         ("C-x p p" . +project-switch-project))

  :custom
  (project-vc-extra-root-markers '(".dir-locals.el"))

  :config
  (defun +project-switch-project (dir)
    (interactive (list (project-prompt-project-dir)))
    (dired dir)))

codespaces

I use GitHub Codespaces for a few projects.

(use-package codespaces
  :ensure-system-package gh
  :demand t
  :config
  (setq vc-handled-backends '(Git))
  (codespaces-setup))

undo-tree

I like tree-based undo management. I only rarely need it, but when I do, oh boy.

This configuration:

  • Stores all undo files under the undo-tree directory in my Emacs config directory.
  • Registers undo-tree for use in evil-mode.
  • Suppresses warnings about being unable to load undo history when an underlying file is changed outside Emacs (as often happens when I, say, append a task to my todo lists through a script).
(use-package undo-tree
  :demand t

  :config
  (setq undo-tree-history-directory-alist `(("." . ,(concat user-emacs-directory "undo-tree"))))
  (global-undo-tree-mode)
  (evil-set-undo-system 'undo-tree)

  (defun +undo-tree-suppress-undo-history-saved-message (undo-tree-save-history &rest args)
    "Suppress the message saying that the undo history file was saved (because this happens every single time you save a file)."
    (let ((inhibit-message t))
      (apply undo-tree-save-history args)))

  (defun +undo-tree-suppress-buffer-modified-message (undo-tree-load-history &rest args)
    "Suppress the message saying that the undo history could not be loaded because the file changed outside of Emacs."
    (let ((inhibit-message t))
      (apply undo-tree-load-history args)))

  (advice-add #'undo-tree-load-history :around
              #'+undo-tree-suppress-undo-history-saved-message)

  (advice-add #'undo-tree-load-history :around
              #'+undo-tree-suppress-buffer-modified-message))

ChatGPT integration

I’ve been toying around with integrating ChatGPT into my work. org-ai enables that by providing begin_ai blocks in Org which interact with ChatGPT sessions.

Authentication is handled by an auth key for api.openai.com in my .netrc, which org-ai knows to read. I’ve also got a pair of snippets to create conversations and images.

(use-package org-ai
  :commands (org-ai-mode)
  :hook (org-mode . org-ai-mode)

  :custom
  (org-ai-image-directory (expand-file-name "~/media/pictures/ai")))

An interactive shell session is often even more useful.

(use-package chatgpt-shell
  :commands (chatgpt-shell)

  :custom
  (chatgpt-shell-openai-key (auth-source-pick-first-password :host "api.openai.com")))

World clock

Collaborating with different timezones counts as project management, right?

(use-package world-clock
  :ensure nil

  :custom
  (world-clock-time-format "%a %d %b %l:%M %p %Z")
  (world-clock-list
   '(("America/Vancouver" "Vancouver")
     ("America/New_York" "New York")
     ("Europe/Paris" "Paris")
     ("Africa/Nairobi" "Nairobi")))

  :config
  (evil-define-key 'normal world-clock-mode-map (kbd "q") '+quit-window-and-kill))

Programming environments

I like shallow indentation, but tabs are displayed as 8 characters by default. This reduces that.

(setq-default tab-width 2)

Treating terms in CamelCase symbols as separate words makes editing a little easier for me, so I like to use subword-mode everywhere.

(use-package subword
  :config (global-subword-mode 1))

Compilation output goes to the *compilation* buffer. I rarely have that window selected, so the compilation output disappears past the bottom of the window. This automatically scrolls the compilation window so I can always see the output.

(setq compilation-scroll-output t)

LSP

I use LSP for some languages. This hooks LSP to run in those modes and ensures that it displays all available documentation on hover.

(use-package lsp-mode
  :commands (lsp lsp-deferred)
  :hook ((go-mode
          ruby-base-mode
          ruby-mode
          ruby-ts-mode
          rust-mode
          rust-ts-mode) . lsp-deferred)

  :init
  (setq lsp-enabled-clients '(gopls
                              sorbet-ls
                              rust-analyzer))

  :custom
  (lsp-completion-provider :none))

This integrates LSP into my UI in various useful ways. Specifically, it writes documentation and type annotations and suchlike all over my damn screen, which I enjoy, personally.

(use-package lsp-ui
  :commands lsp-ui-mode
  :hook (lsp-mode . lsp-ui-mode)

  :custom
  (lsp-ui-peek-always-show t)
  (lsp-ui-sideline-show-hover t)
  (lsp-ui-doc-enable nil))

Toggle code folding

I don’t use TAB to indent when I’m in normal-mode, so instead I use it to toggle hiding blocks of code.

Similarly, I bind BACKTAB to toggle hiding all the top-level code blocks. This parallels the use of those keys in Org.

(use-package hs-minor-mode
  :ensure nil
  :hook prog-mode

  :init
  (defvar-local +maybe-hidden-blocks nil)
  (add-hook 'hs-hide-hook (lambda () (setq-local +maybe-hidden-blocks t)))

  (defun +toggle-all-folds ()
    "If any block are hidden, show them all. Otherwise, hide all top-level blocks."
    (interactive)
    (if +maybe-hidden-blocks
        (progn
          (setq-local +maybe-hidden-blocks nil)
          (hs-show-all))
      (hs-hide-all)))

  (evil-define-key 'normal prog-mode-map (kbd "<tab>") 'hs-toggle-hiding)
  (evil-define-key 'normal prog-mode-map (kbd "<backtab>") '+toggle-all-folds))

Coq

I use Proof General as my Coq IDE.

  • I like to disable abbrev-mode; it has a ton of abbreviations for Coq, but they’ve always been unpleasant surprises for me.
  • Similarly, flycheck-mode seems to do more harm than good.
  • The Proof General splash screen’s pretty cute, but I don’t need to see it every time.
  • The default Proof General layout stacks the code, goal, and response buffers on top of each other. I like to keep my code on one side and my goal and response buffers on the other.
  • Have point follow the end of the locked region when asserting and undoing proof commands, but don’t lock it to the end.
  • Proof General usually evaluates each comment individually. In literate programs, this can result in evaluating a ton of comments. This evaluates a series of consecutive comments as a single comment.
  • I bind the up and down arrow keys in Coq to evaluating and retracting the next and previous statements. This is more convenient for me than the default bindings of C-c C-n and C-c C-u.
(use-package proof-general
  :ensure-system-package (coqc . coq)
  :hook (coq-mode . (lambda ()
                      (undo-tree-mode 1)
                      (abbrev-mode 0)
                      (flycheck-mode 0)))
  :bind ("C-c v" . deadgrep)

  :custom
  (proof-splash-enable nil)
  (proof-three-window-mode-policy 'hybrid)
  (proof-follow-mode 'follow)
  (proof-script-fly-past-comments t)

  :config
  (evil-define-key 'normal coq-mode-map (kbd "<down>") 'proof-assert-next-command-interactive)
  (evil-define-key 'insert coq-mode-map (kbd "<down>") 'proof-assert-next-command-interactive)

  (evil-define-key 'normal coq-mode-map (kbd "<up>") 'proof-undo-last-successful-command)
  (evil-define-key 'insert coq-mode-map (kbd "<up>") 'proof-undo-last-successful-command))

cron

Add syntax highlighting to cron files.

(use-package crontab-mode)

CSS & Sass

Indent by 2 spaces.

(use-package css-mode
  :config
  (setq css-indent-offset 2))

Don’t compile the current SCSS file every time I save.

(use-package scss-mode
  :config
  (setq scss-compile-at-save nil))

Go

Install go-mode, plus protobuf-mode.

(use-package go-mode
  :ensure-system-package ((go . golang)
                          (gopls . "go install golang.org/x/tools/gopls@latest")))

(use-package protobuf-mode)

Define my $GOPATH and tell Emacs where to find the Go binaries.

(setenv "GOPATH" (expand-file-name "~/code/go"))
(+append-to-path (concat (getenv "GOPATH") "/bin"))

When I save a Go file, reformat the buffer (per gofmt) and organize the imports (per goimports).

(defun +install-go-save-hooks ()
  (add-hook 'before-save-hook #'lsp-format-buffer t t)
  (add-hook 'before-save-hook #'lsp-organize-imports t t))

(add-hook 'go-mode-hook #'+install-go-save-hooks)

Haskell

Enable haskell-doc-mode, which displays the type signature of a function, and use smart indentation.

(use-package haskell-mode
  :hook (haskell-mode . (lambda ()
                          (haskell-doc-mode)
                          (turn-on-haskell-indent))))

JavaScript

Indent everything by 2 spaces.

(setq js-indent-level 2)

Browse JSON documents hierarchically with json-navigator-navigate-after-point.

(use-package json-navigator
  :commands (json-navigator-navigate-after-point))

Lisps

rainbow-delimiters is convenient for coloring matching parentheses.

(use-package rainbow-delimiters
  :hook ((emacs-lisp-mode lisp-mode racket-mode) . rainbow-delimiters-mode))

Common Lisp

Set up SLIME to interactively hack on Common Lisp.

(use-package slime
  :ensure-system-package sbcl
  :commands (slime)

  :config
  (setq inferior-lisp-program "sbcl")
  (load (expand-file-name "~/.quicklisp/slime-helper.el"))
  (add-to-list 'slime-contribs 'slime-autodoc))

Emacs Lisp

If I’m writing in Emacs Lisp I’d like to use eldoc-mode to display documentation.

(use-package eldoc
  :hook (emacs-lisp-mode . eldoc-mode))

Bind running tests to C-c , v, like in rspec-mode.

(use-package ert
  :ensure nil
  :bind (:map emacs-lisp-mode-map ("C-c , v" . +ert-verify))

  :config
  (evil-define-key '(motion normal) ert-results-mode-map (kbd "C-p") 'project-find-file)

  (defun +ert-verify ()
    "Delete all loaded tests from the runtime, evaluate the
 current buffer and run all loaded tests with ert."
    (interactive)
    (ert-delete-all-tests)
    (eval-buffer)
    (ert 't)))

Buttercup offers BDD-style testing. I’ve been using that instead of ERT for my packages’ tests, and I find myself preferring it (especially for features like spy-on).

(use-package buttercup)

I use package-lint to verify that my packages are, y’know, linted.

(use-package package-lint)

Racket

(use-package racket-mode
  :ensure-system-package racket
  :hook (racket-mode . racket-xp-mode)
  :mode "\\.rkt\\'")

(use-package geiser
  :after racket-mode
  :config
  (setq geiser-active-implementations '(racket)))

Quit documentation buffers.

(evil-define-key 'normal racket-describe-mode (kbd "q") 'quit-window)

OCaml

Use tuareg-mode for editing OCaml.

(use-package tuareg
  :ensure-system-package opam

  :config
  (electric-indent-mode 0))

Configure Merlin. This also requires installing the Merlin package through OPAM with opam install merlin.

(use-package merlin
  :after tuareg-mode
  :hook (tuareg-mode . merlin-mode))

Python

(use-package python-mode)

Enable elpy. This provides automatic indentation, auto-completion, syntax checking, etc. Use the python3 interpreter for eldoc.

(use-package elpy
  :after python-mode

  :custom
  (elpy-rpc-python-command "python3")

  :config
  (elpy-enable))

Format code according to PEP8 on save:

(use-package py-autopep8
  :after python-mode
  :hook (elpy-mode-hook . py-autopep8-enable-on-save))

Ruby

This defines a default Ruby version to use within Emacs (for things like xmp or rspec).

(setq +ruby-version "3.2.2")

Ruby executables are installed in ~/.gem/ruby/<version>/bin. This ensures that that’s included in the path. In particular, we want that directory to be included because it contains the xmpfilter executable.

(setenv "GEM_HOME" (concat (file-name-as-directory (expand-file-name "~/.gem/ruby"))
                           +ruby-version))
(+append-to-path (concat (file-name-as-directory (getenv "GEM_HOME"))
                         "bin"))

I associate ruby-mode with Gemfiles, gemspecs, Rakefiles, and Vagrantfiles.

There are a bunch of things I’d like to do when I open a Ruby buffer:

  • C-c C-c should run xmp, to do that nifty “eval into comments” trick.
  • Hitting “enter” should indent to the current level.
  • Disable reek, which I don’t find helpful.
  • When assigning the result of a conditional, I like to align the expression to match the beginning of the statement instead of indenting it all the way to the if.
(use-package ruby-mode
  :ensure-system-package (xmpfilter . "gem install rcodetools")

  :mode ("\\.rake$"
         "\\.gemspec$"
         "\\Guardfile$"
         "\\Rakefile$"
         "\\Vagrantfile$"
         "\\Vagrantfile.local$")

  :bind ("\r" . newline-and-indent)

  :custom
  (ruby-align-to-stmt-keywords t)

  :config
  (setq-default flycheck-disabled-checkers '(ruby-reek)))

I use chruby to switch between versions of Ruby.

(use-package chruby
  :after ruby-mode
  :hook (ruby . chruby-use-corresponding)
  :config
  (chruby +ruby-version))

Running tests from within Emacs is awfully convenient. I enable rspec-mode basically everywhere, since working with a Rails project involves a ton of modes.

I’d like my rspec tests to be run in a random order, and I’d like the output to be colored.

(use-package rspec-mode
  :after ruby-mode
  :ensure-system-package (rspec . "gem install rspec")
  :bind (:map rspec-verifiable-mode-keymap  ("o" . +rspec-outline))

  :hook (css-mode
         deadgrep-mode
         js-mode
         magit-status-mode
         ruby-mode
         scss-mode
         web-mode
         yaml-mode
         yard-mode)

  :custom
  (compilation-scroll-output nil)
  (rspec-command-options "--color --order random")
  (rspec-use-chruby t)

  :config
  (defvar +rspec-outline-blocks
    '("context"
      "describe"
      "include_examples"
      "it"
      "it_behaves_like"
      "it_should_behave_like"
      "shared_examples_for"
      "specify"))

  (defun +rspec-outline ()
    "Use `occur' to create a linked outline of the spec associated with the current file, which may be either a spec or a target."
    (interactive)
    (let ((list-matching-lines-face nil)
          (spec-buffer (if (rspec-buffer-is-spec-p)
                           (current-buffer)
                         (find-file-noselect (rspec-spec-file-for (buffer-file-name))))))
      (with-current-buffer spec-buffer
        (occur (rx-to-string `(seq line-start
                                   (zero-or-more whitespace)
                                   (optional "RSpec.")
                                   (or ,@+rspec-outline-blocks)
                                   (one-or-more whitespace)
                                   (or "\"" "'" "A-Z" "{ ")))
               0)))
    (occur-rename-buffer))

  (evil-define-key 'motion rspec-mode-map (kbd "C-p") 'project-find-file)
  (evil-define-key 'motion rspec-compilation-mode-map (kbd "C-p") 'project-find-file)
  (evil-define-key 'motion rspec-compilation-mode-map (kbd "g r") 'rspec-rerun))

I’d like inf-ruby to automatically steal focus if a breakpoint triggers.

(use-package inf-ruby
  :config
  (add-hook 'ruby-base-mode 'inf-ruby-minor-mode)
  (inf-ruby-enable-auto-breakpoint))

Some Ruby projects use minitest instead of rspec.

(use-package minitest
  :after ruby-mode
  :custom
  (compilation-scroll-output nil))

rspec-mode and minitest-mode use the same keybindings for running tests. That’s great for muscle memory, but it means that it’s better to only have one or the other active at any given time. This checks the root of the current project for a tests directory. If it finds one it activates minitest-mode, and if it doesn’t (or if we’re not in a project) it uses rspec-mode. Kinda hacky, but seems to do the job.

(defvar +ruby-testable-mode-hooks
  '(css-mode-hook
    deadgrep-mode-hook
    js-mode-hook
    magit-status-mode-hook
    ruby-mode-hook
    scss-mode-hook
    web-mode-hook
    yard-mode-hook))

(defun +current-project-uses-minitest-p ()
  (and (project-current)
       (file-directory-p (expand-file-name "test" (project-root (project-current))))))

(defun +activate-ruby-tests-mode ()
  (if (+current-project-uses-minitest-p)
      (progn
        (minitest-mode 1)
        (rspec-mode 0)
        (rspec-verifiable-mode 0))
      (progn
        (minitest-mode 0)
        (rspec-mode 1)
        (rspec-verifiable-mode 1))))

(dolist (hook +ruby-testable-mode-hooks)
  (add-hook hook #'+activate-ruby-tests-mode))

rcodetools provides xmp, which lets me evaluate a Ruby buffer and display the results in “magic” (# ==>) comments.

I disable warnings when running code through xmp because I disagree with a few of them (complaining about private attr_reader, especially) and they gunk up my buffer.

(use-package rcodetools
  :after ruby-mode
  :load-path "resources"
  :commands (xmp)
  :bind (:map ruby-mode-map ("C-c C-c" . xmp))

  :config
  (setq xmpfilter-command-name
        "ruby -S xmpfilter --no-warnings --dev --fork --detect-rbtest"))

Ruby method comments are often formatted with Yard.

(use-package yard-mode
  :after ruby-mode
  :hook ruby-mode)

Insert end keywords automatically when I start to define a method, class, module, or block.

(use-package ruby-end
  :after ruby-mode)

Rust

Use rustic to edit Rust code.

(use-package rustic
  :bind (:map rustic-mode-map
              ("M-j" . lsp-ui-imenu)
              ("M-?" . lsp-find-references)
              ("C-c C-c l" . flycheck-list-errors)
              ("C-c C-c a" . lsp-execute-code-action)
              ("C-c C-c r" . lsp-rename)
              ("C-c C-c q" . lsp-workspace-restart)
              ("C-c C-c Q" . lsp-workspace-shutdown)
              ("C-c C-c s" . lsp-rust-analyzer-status))

  :config
  (setq lsp-rust-analyzer-cargo-watch-command "clippy")
  (setq lsp-rust-analyzer-server-display-inlay-hints t)

  (setq rustic-format-on-save t)
  (add-hook 'rustic-mode-hook '+rustic-mode-hook))

(defun +rustic-mode-hook ()
  "Don't prompt for confirmation before running `rustfmt'."
  (setq-local buffer-save-without-query t))

sh

Indent with 2 spaces.

(add-hook 'sh-mode-hook
          (lambda ()
            (setq sh-basic-offset 2
                  sh-indentation 2)))

Scala

Ensure that scala-mode and sbt-mode are installed.

(use-package scala-mode
  :interpreter ("scala" . scala-mode))

(use-package sbt-mode
  :after scala-mode
  :commands sbt-start sbt-command

  :config
  (substitute-key-definition 'minibuffer-complete-word
                             'self-insert-command
                             minibuffer-local-completion-map))

(use-package hydra)

Don’t show the startup message with launching ENSIME:

(setq ensime-startup-notification nil)

Bind a few keys to common operations:

(evil-define-key 'normal ensime-mode-map (kbd "C-t") 'ensime-type-at-point)
(evil-define-key 'normal ensime-mode-map (kbd "M-.") 'ensime-edit-definition)

SQL

Support syntax-based indentation when editing SQL files.

(use-package sql-indent
  :hook (sql-mode . sqlind-minor-mode))

Terraform

Install terraform-mode.

(use-package terraform-mode
  :ensure-system-package terraform
  :custom
  (terraform-format-on-save t))

web-mode

Use web-mode with embedded Ruby files, regular HTML, and PHP.

(use-package web-mode
  :mode ("\\.erb$"
         "\\.html$"
         "\\.php$"
         "\\.rhtml$")

  :config
  (setq web-mode-markup-indent-offset 2
        web-mode-css-indent-offset 2
        web-mode-code-indent-offset 2
        web-mode-indent-style 2))

I’d like to see colors with rainbow-mode, so we’ll need to install that, too:

(use-package rainbow-mode
  :hook web-mode)

YAML

This is not a place of honor.

(use-package yaml-mode)

Terminal

I’m trying vterm. This disables global-hl-line-mode locally and lets me open up a new terminal instance with C-c t.

(use-package multi-vterm
  :ensure-system-package (cmake
                          ("/usr/share/doc/libvterm-dev" . libvterm-dev))
  :commands (multi-vterm)
  :hook (vterm-mode-hook . (lambda () (setq-local global-hl-line-mode nil))))

(global-set-key (kbd "C-c t") 'multi-vterm)

Password management

I manage my passwords with =pass=, a nifty command-line utility that’s accessible through Emacs.

I’ll also occasionally use pwgen to generate and insert a secure password.

(use-package password-store
  :ensure-system-package pass)

(use-package password-store-otp)

(defun +insert-password ()
  (interactive)
  (shell-command "pwgen 30 --num-passwords=1 --secure | tr --delete '\n'" t))

Publishing and task management with Org-mode

  • I’d like the initial scratch buffer to be in Org.
  • Put tags directly after the associated header rather than trying to align them.
  • When hitting C-<return> to create a new heading, don’t insert the heading between the current heading and its content, but instead append it after the content.

I’d like to open file: links in Org with the applications defined in my mailcap. This clears the existing MIME mapping, parses my personal mailcap, and tells Org to open those links with the mailcap-defined applications.

(use-package org
  :custom
  (initial-major-mode 'org-mode)

  (org-auto-align-tags nil)
  (org-footnote-auto-label nil)
  (org-footnote-section nil)
  (org-insert-heading-respect-content t)
  (org-tags-column 0)

  :config
  (add-hook 'org-mode-hook
            (lambda ()
              (setq mailcap-mime-data '())
              (mailcap-parse-mailcap "~/.mailcap")
              (setq org-file-apps
                    '((auto-mode . emacs)
                      ("jpg" . "~/.bin/s %s")
                      ("mobi" . "foliate %s")
                      ("\\.x?html?\\'" . mailcap)
                      ("pdf" . mailcap)
                      (system . mailcap)
                      (t . mailcap))))))

Including org-tempo restores the <s-style easy templates that were deprecated in Org 9.2. This also adds a <el template to quickly insert a block of Emacs lisp.

(use-package org-tempo
  :demand t
  :ensure nil

  :config
  (add-to-list 'org-structure-template-alist
               '("el" . "src emacs-lisp")))

Task management and agenda views

Store my org files in ~/documents/org and define the location of an index file (my main todo list).

(setq org-directory "~/documents/org")

(defun +org-file-path (filename)
  "Return the absolute address of an org file, given its relative name."
  (concat (file-name-as-directory org-directory) filename))

(setq org-index-file (+org-file-path "index.org"))

Archive finished tasks in ~/documents/org/archive/archive-YYYY-MM-DD.org. Since I often leave Emacs running overnight, I reset the update location at midnight.

(defun +set-org-archive-location ()
  "Set the `org-archive-location' variable according to the current date."
  (setq org-archive-location
        (concat
         (+org-file-path (format-time-string "archive/archive-%Y-%m-%d.org"))
         "::* From %s")))

(+set-org-archive-location)

(use-package midnight
  :demand t
  :custom
  (midnight-delay 0)
  :config
  (midnight-mode 1)
  (add-hook 'midnight-hook #'+set-org-archive-location))
  • Record the time that a task was archived.
  • Ensure that a task can’t be marked as done if it contains unfinished subtasks. This is handy for organizing “blocking” tasks hierarchically.
  • By default Org will dim any tasks that contain blocking subtasks. That’s good, but I’ve got enough of those that I’d rather not see them at all. By making blocked tasks invisible I ensure that everything in my agenda is currently actionable. Or, in GTD lingo, I’m only seeing “next steps.”
  • Org treats unprioritized entries as if they had a priority of [#B]. I’d prefer to treat them as the lowest priority, which I’m leaving as [#C]. That ensures that prioritized entries always come before unprioritized ones in my agenda.
  • Hide the category prefix from tasks. I categorize my tasks with tags, including using filetags, so prefixing tasks with the file they’re stored in is noisy and redundant.
  • Begin weeks today, not on the last Monday.
  • Don’t show deadline warnings under today’s entry. If something’s due in two days, I’ll see it in my agenda as a deadline on that day; I don’t also need it listed under today’s tasks, prefixed with In 2 d:.
  • Hide blocks in the agenda that don’t contain any tasks. From this email thread.
(use-package org-agenda
  :demand t
  :ensure nil

  :custom
  (org-agenda-files (list org-directory
                          (+org-file-path "calendars")))
  (org-log-done 'time)
  (org-enforce-todo-dependencies t)
  (org-agenda-dim-blocked-tasks 'invisible)
  (org-default-priority ?C)
  (org-agenda-prefix-format '((agenda . " %i %?-12t% s")
                              (todo . " %i ")
                              (tags . " %i ")
                              (search . " %i ")))
  (org-agenda-start-on-weekday nil)
  (org-deadline-warning-days 0)

  :config
  (defun +org-agenda-delete-empty-blocks ()
    "Remove empty agenda blocks.
  A block is identified as empty if there are fewer than 2
  non-empty lines in the block (excluding the line with
  `org-agenda-block-separator' characters)."
    (when org-agenda-compact-blocks
      (user-error "Cannot delete empty compact blocks"))
    (setq buffer-read-only nil)
    (save-excursion
      (goto-char (point-min))
      (let* ((blank-line-re "^\\s-*$")
             (content-line-count (if (looking-at-p blank-line-re) 0 1))
             (start-pos (point))
             (block-re (format "%c\\{10,\\}" org-agenda-block-separator)))
        (while (and (not (eobp)) (forward-line))
          (cond
           ((looking-at-p block-re)
            (when (< content-line-count 2)
              (delete-region start-pos (1+ (point-at-bol))))
            (setq start-pos (point))
            (forward-line)
            (setq content-line-count (if (looking-at-p blank-line-re) 0 1)))
           ((not (looking-at-p blank-line-re))
            (setq content-line-count (1+ content-line-count)))))
        (when (< content-line-count 2)
          (delete-region start-pos (point-max)))
        (goto-char (point-min))
        ;; The above strategy can leave a separator line at the beginning
        ;; of the buffer.
        (when (looking-at-p block-re)
          (delete-region (point) (1+ (point-at-eol))))))
    (setq buffer-read-only t))

  (add-hook 'org-agenda-finalize-hook #'+org-agenda-delete-empty-blocks))

Separate org blocks with nearly complete lines, not rows of ===.

(setq org-agenda-block-separator ?─
      org-agenda-time-grid
      '((daily today require-timed)
        (800 1000 1200 1400 1600 1800 2000)
        " ┄┄┄┄┄ " "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄")
      org-agenda-current-time-string
      "⭠ now ─────────────────────────────────────────────────")

The “Personal agenda” view is simpler than it seems. I’m mostly sorting tasks by the inbox, habit, and project tags. Here are the sections:

Inbox
Newly captured notes or ideas that haven’t yet been turned into “real” tasks or projects, or tasks that have been deferred until today and require reexamination.
Next
Next unblocked steps in projects (or stand-alone tasks).
Habit
Automatically generated tasks appended to a file with a custom script. This includes stuff like, y’know, exercising, feeding the sourdough starter, or resetting my watch for daylight savings time. I could probably replace this script with org-habit, but I don’t for mostly historical reasons.
Calendar
I have a cron job that pulls down my calendars into an Org file, so my day’s meetings, pending deliveries, and so on are displayed in my agenda. This also shows tasks with deadlines and so on that I might not have tagged.
Projects
The list of GTD-style projects that I’m currently working on.
(setq org-agenda-custom-commands '())

(add-to-list 'org-agenda-custom-commands
             '("p" "Personal agenda"
               ((tags-todo "inbox|tickler+SCHEDULED=\"<today>\"|tickler+DEADLINE=\"<today>\""
                           ((org-agenda-overriding-header "Inbox")))

                (tags-todo "next"
                           ((org-agenda-overriding-header "Next")))

                (tags-todo "habit-daily"
                           ((org-agenda-overriding-header "Habits")))

                (agenda ""
                        ((org-agenda-overriding-header "Calendar")
                         (org-agenda-tag-filter-preset '("-next" "-habit"))))

                (tags-todo "project"
                           ((org-agenda-overriding-header "Projects"))))

               ((org-agenda-skip-deadline-if-done t)
                (org-agenda-skip-scheduled-if-done t)
                (org-agenda-skip-timestamp-if-done t)
                (org-agenda-hide-tags-regexp "calendar\\|habit\\|inbox\\|next\\|project")
                (org-agenda-tag-filter-preset '("-duplicate" "-news" "-writing")))))

I consult my agenda pretty often, so I bind C-c d to open it a bit faster.

(defun +dashboard ()
  (interactive)
  (call-process-shell-command "daily-checklist")
  (find-file org-index-file)
  (with-current-buffer (get-file-buffer org-index-file)
    (revert-buffer nil t))
  (delete-other-windows)
  (org-agenda nil "p"))

(global-set-key (kbd "C-c d") '+dashboard)

Shorten the default (lengthy) org-agenda modeline.

(defadvice org-agenda-set-mode-name (after truncate-org-agenda-mode-name activate)
  (setq mode-name '("Org-agenda")))

I have a number of standing weekly video calls with friends and family, and I like keeping track of what happened in the last week to share on those calls. Because my memories only exist in text files, I have a custom view to list news items from the last couple weeks.

(add-to-list 'org-agenda-custom-commands
             '("n" "News from this week"
               ((agenda ""))
               ((org-agenda-overriding-header "News from this week")
                (org-agenda-start-day "-14d")
                (org-agenda-span 21)
                (org-agenda-files '("~/documents/org/news.org"
                                    "~/documents/org/recurring-events.org"
                                    "~/documents/notes/bird-log.org"
                                    "~/documents/notes/books-read.org"
                                    "~/documents/notes/papers-read.org")))))

I do a ton of journaling! I maintain a list of topics I’d like to think through and pop it open when I’m ready to write.

(add-to-list 'org-agenda-custom-commands
             '("w" "Writing prompts"
               ((tags "+writing"))
               ((org-agenda-overriding-header "Writing prompts")
                (org-agenda-sorting-strategy '((agenda ts-down))))))

Capturing tasks

Define a few common tasks as capture templates.

  • Creating a new capture item also adds a bookmark, which includes a marker in the fringe. I don’t need to see that.
  • When I’m starting an Org capture template I’d like to begin in insert mode. I’m opening it up in order to start typing something, so this skips a step.
(use-package org-capture
  :demand t
  :ensure nil

  :custom
  (bookmark-set-fringe-mark nil)

  :config
  (add-hook 'org-capture-mode-hook 'evil-insert-state)

  (setq org-capture-templates
        '(("b" "Blog idea" entry
           (file "~/documents/notes/blog-ideas.org")
           "* %?\n")
          ("d" "Delivery" entry
           (file+headline "~/documents/org/deliveries.org" "Deliveries")
           "** %?\nSCHEDULED: %^t\n")
          ("e" "Email" entry
           (file+headline org-index-file "Inbox")
           "* TODO %?\n%a\n")
          ("f" "Finished book"
           entry
           (file+headline "~/documents/notes/books-read.org" "Books")
           "* %^{Title} -- %^{Author}\n%^t\n"
           :immediate-finish t)
          ("m" "Media queue"
           item
           (file+headline "~/documents/notes/media.org" "Inbox")
           "- [ ] %^{Media}\n"
           :immediate-finish t)
          ("n" "News item"
           entry
           (file "~/documents/org/news.org")
           "* %?\n%t\n")
          ("s" "Subscribe to an RSS feed"
           plain
           (file "~/documents/rss-feeds.org")
           "*** [[%^{Feed URL}][%^{Feed name}]]"
           :immediate-finish t)
          ("t" "Task"
           entry
           (file+headline org-index-file "Inbox")
           "* TODO %?\n")
          ("w" "Writing prompt"
           entry
           (file+headline "~/documents/org/writing.org" "Writing")
           "* %?\n%t\n")))

  (defun +org-capture-todo ()
    (interactive)
    (org-capture :keys "t")))

Refiling headings

(use-package org-refile
  :ensure nil
  :custom
  (org-refile-use-outline-path t)
  (org-outline-path-complete-in-steps nil)
  (org-refile-targets `((,org-index-file :level . 1)
                        (,(+org-file-path "deliveries.org") :level . 1)
                        (,(+org-file-path "environment.org") :level . 1)
                        (,(+org-file-path "someday-maybe.org") :level . 1)
                        (,(+org-file-path "work.org") :level . 1)
                        (,(+org-file-path "writing.org") :level . 1))))

Keybindings

Bind a few handy keys.

(define-key global-map "\C-cl" 'org-store-link)
(define-key global-map "\C-ca" 'org-agenda)
(define-key global-map "\C-cc" 'org-capture)

Hit C-c i to quickly open up my todo list.

(defun +open-index-file ()
  "Open the master org TODO list."
  (interactive)
  (find-file org-index-file)
  (flycheck-mode -1)
  (end-of-buffer))

(global-set-key (kbd "C-c i") '+open-index-file)

Hit M-n to quickly open up a capture template for a new todo.

(global-set-key (kbd "M-n") '+org-capture-todo)

(setq +org-capture-todo-hooks
      '(gfm-mode-hook
        haskell-mode-hook
        magit-mode-hook
        makefile-mode-hook
        vterm-mode-hook))

(dolist (hook +org-capture-todo-hooks)
  (add-hook hook
            (lambda ()
              (local-set-key (kbd "M-n") '+org-capture-todo))))

Rebind C-c C-l to DWIM:

(defun +org-insert-link-dwim ()
  "Like `org-insert-link' but with personal dwim preferences."
  (interactive)
  (let* ((point-in-link (org-in-regexp org-link-any-re 1))
         (clipboard-url (when (and kill-ring
                                   (string-match-p "^http" (current-kill 0)))
                          (current-kill 0)))
         (region-content (when (region-active-p)
                           (buffer-substring-no-properties (region-beginning)
                                                           (region-end)))))
    (cond ((and region-content clipboard-url (not point-in-link))
           (delete-region (region-beginning) (region-end))
           (insert (org-make-link-string clipboard-url region-content))
           (message clipboard-url))
          ((and clipboard-url (not point-in-link))
           (insert (org-make-link-string
                    clipboard-url
                    (read-string "title: "
                                 (with-current-buffer (url-retrieve-synchronously clipboard-url)
                                   (dom-text (car
                                              (dom-by-tag (libxml-parse-html-region
                                                           (point-min)
                                                           (point-max))
                                                          'title))))))))
          (t
           (call-interactively 'org-insert-link)))))

(define-key org-mode-map (kbd "C-c C-l") '+org-insert-link-dwim)

Add a function to link the selected text to its associated Wikipedia article.

(defun +apply-to-region (fn)
  (interactive "XFunction to apply to region: ")
  (save-excursion
    (let* ((beg (region-beginning))
           (end (region-end))
           (resulting-text
            (funcall
             fn
             (buffer-substring-no-properties beg end))))
      (kill-region beg end)
      (insert resulting-text))))

(defun +org-insert-wikipedia-link ()
  (interactive)
  (+apply-to-region (lambda (string)
                      "Convert a string to a link to English Wikipedia"
                      (concat "[[https://en.wikipedia.org/wiki/" (subst-char-in-string ?  ?_ string) "]"
                              "[" string "]]"))))

Exporting

Allow babel to evaluate code blocks in a handful of languages.

(use-package gnuplot
  :ensure-system-package gnuplot)

(org-babel-do-load-languages
 'org-babel-load-languages
 '((ditaa . t)
   (dot . t)
   (emacs-lisp . t)
   (gnuplot . t)
   (ruby . t)
   (shell . t)))

Don’t ask before evaluating code blocks.

(setq org-confirm-babel-evaluate nil)

Associate the “dot” language with the graphviz-dot major mode.

This also sets up some logic in snippets to ensure that snippet-created graphs are saved in the correct location.

(use-package graphviz-dot-mode
  :ensure-system-package (dot . graphviz)

  :config
  (defvar +note-diagram-path-alist
    '(("~/documents/journal/entries" . "../diagrams")
      ("~/documents/notes" . "./diagrams"))
    "Alist mapping between where Org files are saved and where diagrams generated by snippets should be stored.")

  (defun +diagram-snippet-directory ()
    (cdr (seq-find (lambda (pair) (file-in-directory-p buffer-file-name (car pair)))
                   +note-diagram-path-alist
                   '("" . "."))))

  (defun +diagram-snippet-path ()
    (if buffer-file-name
        (format "%s/%s.png"
                (+diagram-snippet-directory)
                (file-name-base buffer-file-name))
      (message "%s" "Can't name new diagram (did you save the buffer?)")))

  (add-to-list 'org-src-lang-modes '("dot" . graphviz-dot)))

Translate regular ol’ straight quotes to typographically correct curly quotes when exporting.

(setq org-export-with-smart-quotes t)

Exporting to HTML

Don’t include a footer with my contact and publishing information at the bottom of every exported HTML document.

(setq org-html-postamble nil)

Use htmlize to ensure that exported code blocks use syntax highlighting.

(use-package htmlize)

Markdown

(use-package ox-md
  :ensure nil
  :after org
  :commands (org-export-dispatch))

EPUB

(use-package ox-epub
  :after org
  :commands (org-export-dispatch))

Exporting to PDF

  • I want to produce PDFs with syntax highlighting in the code. The best way to do that seems to be with the minted package, but that package shells out to pygments to do the actual work. xelatex usually disallows shell commands; this enables that.
  • Include the listings package in all of my LaTeX exports.
  • Remove the intermediate TeX file when exporting to PDF.
(use-package ox-latex
  :ensure-system-package latexmk
  :ensure nil
  :after org
  :commands (org-export-dispatch)

  :custom
  (org-latex-pdf-process '("latexmk -xelatex -shell-escape -quiet -f %f"))

  (org-latex-src-block-backend 'listings)
  (org-latex-listings-options
   '(("basicstyle" "\\ttfamily")
     ("showstringspaces" "false")
     ("keywordstyle" "\\color{blue}\\textbf")
     ("commentstyle" "\\color{gray}")
     ("stringstyle" "\\color{green!70!black}")
     ("stringstyle" "\\color{red}")
     ("frame" "single")
     ("numbers" "left")
     ("numberstyle" "\\ttfamily")
     ("columns" "fullflexible")))

  (org-latex-packages-alist '(("" "listings")
                              ("" "booktabs")
                              ("AUTO" "polyglossia" t ("xelatex" "lualatex"))
                              ("" "grffile")
                              ("" "unicode-math")
                              ("" "xcolor")))

  :config
  (add-to-list 'org-latex-logfiles-extensions "tex"))

Allow exporting presentations to beamer.

(use-package ox-beamer
  :ensure nil
  :after ox-latex)

TeX configuration

I rarely write LaTeX directly any more, but I often export through it with Org, so I’m keeping them together.

  • Automatically parse the file after loading it.
  • Always use pdflatex when compiling LaTeX documents. I don’t really have any use for DVIs.
  • Enable a minor mode for dealing with math (it adds a few useful keybindings), and always treat the current file as the “main” file. That’s intentional, since I’m usually actually in an org document.
(use-package auctex
  :custom
  (TeX-parse-self t)

  :config
  (TeX-global-PDF-mode 1)

  (add-hook 'LaTeX-mode-hook
            (lambda ()
              (LaTeX-math-mode)
              (setq TeX-master t))))

Blogging

I maintain a blog written in Jekyll. There are plenty of command-line tools to automate creating a new post, but staying in my editor minimizes friction and encourages me to write.

This defines a +new-blog-post function, which prompts the user for a title and creates a new draft (with a slugged file name) in the blog’s _drafts/ directory. The new post includes appropriate YAML header information.

This also defines +publish-post and +unpublish-post, which adjust the date in the YAML front matter and rename the file appropriately.

(defvar +jekyll-drafts-directory (expand-file-name "~/documents/blog/_drafts/"))
(defvar +jekyll-posts-directory (expand-file-name "~/documents/blog/_posts/"))
(defvar +jekyll-post-extension ".md")

(defun +timestamp ()
  (format-time-string "%Y-%m-%d"))

(defun +replace-whitespace-with-hyphens (s)
  (replace-regexp-in-string " " "-" s))

(defun +replace-nonalphanumeric-with-whitespace (s)
  (replace-regexp-in-string "[^A-Za-z0-9 ]" " " s))

(defun +remove-quotes (s)
  (replace-regexp-in-string "[\'\"]" "" s))

(defun +replace-unusual-characters (title)
  "Remove quotes, downcase everything, and replace characters
that aren't alphanumeric with hyphens."
  (+replace-whitespace-with-hyphens
   (s-trim
    (downcase
     (+replace-nonalphanumeric-with-whitespace
      (+remove-quotes title))))))

(defun +slug-for (title)
  "Given a blog post title, return a convenient URL slug.
   Downcase letters and remove special characters."
  (let ((slug (+replace-unusual-characters title)))
    (while (string-match "--" slug)
      (setq slug (replace-regexp-in-string "--" "-" slug)))
    slug))

(defun +jekyll-yaml-template (title)
  "Return the YAML header information appropriate for a blog
   post. Include the title, the current date, the post layout,
   and an empty list of tags."
  (concat
   "---\n"
   "title: " title "\n"
   "date:\n"
   "layout: post\n"
   "# mathjax: true\n"
   "# pdf_file: " (+slug-for title) ".pdf\n"
   "tags: []\n"
   "---\n\n"))

(defun +new-blog-post (title)
  "Create a new blog draft in Jekyll."
  (interactive "sPost title: ")
  (let ((post (concat +jekyll-drafts-directory
                      (+slug-for title)
                      +jekyll-post-extension)))
    (if (file-exists-p post)
        (find-file post)
      (find-file post)
      (insert (+jekyll-yaml-template title)))))

(defun +jekyll-draft-p ()
  "Return true if the current buffer is a draft."
  (equal
   (file-name-directory (buffer-file-name (current-buffer)))
   +jekyll-drafts-directory))

(defun +jekyll-published-p ()
  "Return true if the current buffer is a published post."
  (equal
   (file-name-directory (buffer-file-name (current-buffer)))
   +jekyll-posts-directory))

(defun +publish-post ()
  "Move a draft post to the posts directory, rename it to include
the date, reopen the new file, and insert the date in the YAML
front matter."
  (interactive)
  (cond ((not (+jekyll-draft-p))
         (message "This is not a draft post."))
        ((buffer-modified-p)
         (message "Can't publish post; buffer has modifications."))
        (t
         (let ((filename
                (concat +jekyll-posts-directory
                        (+timestamp) "-"
                        (file-name-nondirectory
                         (buffer-file-name (current-buffer)))))
               (old-point (point)))
           (rename-file (buffer-file-name (current-buffer))
                        filename)
           (kill-buffer nil)
           (find-file filename)
           (set-window-point (selected-window) old-point)
           (save-excursion
             (beginning-of-buffer)
             (replace-regexp "^date:$" (concat "date: " (+timestamp))))
           (save-buffer)
           (message "Published post!")))))

(defun +unpublish-post ()
  "Move a published post to the drafts directory, rename it to
exclude the date, reopen the new file, and remove the date in the
YAML front matter."
  (interactive)
  (cond ((not (+jekyll-published-p))
         (message "This is not a published post."))
        ((buffer-modified-p)
         (message "Can't publish post; buffer has modifications."))
        (t
         (let ((filename
                (concat +jekyll-drafts-directory
                        (substring
                         (file-name-nondirectory
                          (buffer-file-name (current-buffer)))
                         11 nil)))
               (old-point (point)))
           (rename-file (buffer-file-name (current-buffer))
                        filename)
           (kill-buffer nil)
           (find-file filename)
           (set-window-point (selected-window) old-point)
           (save-excursion
             (beginning-of-buffer)
             (replace-regexp "^date: [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$" "date:"))
           (save-buffer)
           (message "Returned post to drafts!")))))

This selects and inserts a tag:

(defun +tags-from-tag-line (line)
  "Given a line of tags from a blog post (like \"tags: [animals, design, cephalopods]\") return a sorted list of the tags (like '(\"animals\" \"cephalopods\" \"design\"))."
  (sort (mapcar #'string-trim
                (-> (string-trim line)
                    (substring 7 -1)
                    (split-string ",")))
        #'string<))

(defun +tag-lines ()
  "Return all the lines of tags from all existing blog posts."
  (seq-remove #'string-empty-p
              (split-string
               (shell-command-to-string
                (format "grep --no-filename \"^tags: \\[.*\\]$\" %s"
                        (concat (file-name-as-directory +jekyll-posts-directory) "*")))
               "\n")))

(defun +existing-blog-tags ()
  "Return a sorted list of all the tags used in my blog posts."
  (-> (mapcar #'+tags-from-tag-line (+tag-lines))
      (flatten-list)
      (seq-uniq)
      (sort #'string<)))

(defun +insert-blog-tag ()
  "Prompt for one of the existing tags used in the blog and insert
it in the YAML front matter appropriately."
  (interactive)
  (save-excursion
    (beginning-of-buffer)
    (search-forward-regexp "^tags: \\[")
    (insert
     (completing-read "Insert tag: " (+existing-blog-tags))
     (if (looking-at "\\]") "" ", ")))
  (message "Tagged!"))

Email with mu4e

Inconveniently, mu4e is ordinarily distributed along with mu in my system’s package manager instead of as a package on MELPA. That package also seems to have some trouble inter-operating with my (more recent and locally built) version of Emacs. As a last resort, this loads up mu4e from a local repo.

Also, rather than quitting mu4e, just bury the buffer when I hit q.

(use-package mu4e
  :commands mu4e
  :defer 2
  :load-path "~/media/code/mu/build/mu4e"

  :custom
  (mu4e-trash-folder "/personal/archive")
  (mu4e-refile-folder "/personal/archive")
  (mu4e-sent-folder "/personal/sent")
  (mu4e-drafts-folder "/personal/drafts")

  (mu4e-modeline-support nil)

  (mu4e-index-update-error-warning nil)
  (mu4e-hide-index-messages t)

  :config
  (evil-define-key 'normal mu4e-main-mode-map (kbd "q") 'bury-buffer)
  (mu4e t))

I use multiple contexts for personal and work email.

(use-package mu4e-context
  :after mu4e
  :load-path "~/media/code/mu/build/mu4e"

  :config
  (defun +context-matches-p (msg context-name context-email)
    (if msg
        (mu4e-message-contact-field-matches msg '(:bcc :cc :to) context-email)
      (when (mu4e-context-current)
        (string= context-name (mu4e-context-name (mu4e-context-current))))))

  (setq mu4e-contexts
        `(,(make-mu4e-context
            :name "consulting"
            :match-func (lambda (msg) (+context-matches-p msg "consulting" "consulting@harryrschwartz.com"))
            :vars '((user-mail-address . "consulting@harryrschwartz.com")
                    (user-full-name . "Harry R. Schwartz")
                    (mu4e-trash-folder . "/personal/archive")
                    (mu4e-refile-folder . "/personal/archive")
                    (mu4e-sent-folder . "/personal/sent")
                    (mu4e-drafts-folder . "/personal/drafts")))

          ,(make-mu4e-context
            :name "test-double"
            :match-func (lambda (msg) (+context-matches-p msg "test-double" "harry.schwartz@testdouble.com"))
            :vars '((user-mail-address . "harry.schwartz@testdouble.com")
                    (user-full-name . "Harry R. Schwartz")
                    (mu4e-trash-folder . "/testdouble/archive")
                    (mu4e-refile-folder . "/testdouble/archive")
                    (mu4e-sent-folder . "/testdouble/sent")
                    (mu4e-drafts-folder . "/testdouble/drafts")))

          ,(make-mu4e-context
            :name "hrs"
            :match-func (lambda (msg) t)
            :vars '((user-mail-address . "hello@harryrschwartz.com")
                    (user-full-name . "Harry R. Schwartz")
                    (mu4e-trash-folder . "/personal/archive")
                    (mu4e-refile-folder . "/personal/archive")
                    (mu4e-sent-folder . "/personal/sent")
                    (mu4e-drafts-folder . "/personal/drafts"))))))

Fetching new mail

  • I fetch my email with mbsync every two minutes.
  • Rename files when moving them between directories. mbsync supposedly prefers this.
(use-package mu4e-bookmarks
  :after mu4e
  :load-path "~/media/code/mu/build/mu4e"

  :custom
  (mu4e-get-mail-command "mbsync --all")
  (mu4e-update-interval 120)
  (mu4e-change-filenames-when-moving t))

Listing mail

Hit C-c m to quickly visit my inbox.

(defun +visit-inbox ()
  (interactive)
  (mu4e t)
  (mu4e-search "maildir:/personal/inbox OR maildir:/testdouble/inbox"))

(global-set-key (kbd "C-c m") '+visit-inbox)

Configure the main pane with some reasonable bookmarks. Don’t show my (many) email addresses, though, since they’re just noise.

(use-package mu4e-bookmarks
  :after mu4e
  :load-path "~/media/code/mu/build/mu4e"

  :custom
  (mu4e-main-hide-personal-addresses t)
  (mu4e-bookmarks
        '((:name "unified - inbox" :query "maildir:/personal/inbox OR maildir:/testdouble/inbox" :key ?u :favorite t)
          (:name "personal - inbox" :query "maildir:/personal/inbox" :key ?i)
          (:name "personal - drafts" :query "maildir:/personal/drafts" :key ?d)
          (:name "personal - sent" :query "maildir:/personal/sent" :key ?s)
          (:name "personal - archive" :query "maildir:/personal/archive" :key ?a)
          (:name "work - inbox" :query "maildir:/testdouble/inbox" :key ?w)
          (:name "today's messages" :query "date:today..now" :key ?t)
          (:name "last 7 days" :query "date:7d..now" :key ?7)))
  (mu4e-maildir-shortcuts
        '(("/personal/inbox" . ?i)
          ("/personal/drafts" . ?d)
          ("/personal/sent" . ?s)
          ("/personal/archive" . ?a))))
  • I don’t need to see the context of a thread (with all the deleted messages) in my inbox.
  • Hit q to quit the headers buffer without returning to the main view. This is essentially a reimplementation of mu4e~headers-quit-buffer.
  • Marking a message for deletion applies the “Trashed” flag. This is unfortunate, since Fastmail will automatically delete any messages with that flag (as is the IMAP standard). I want to archive my messages, not delete them, so I’ve rebound d to move email to my “Archive” folder without applying that flag.
(use-package mu4e-headers
  :after mu4e
  :load-path "~/media/code/mu/build/mu4e"

  :custom
  (mu4e-search-include-related nil)

  :config
  (fset '+mu4e-move-to-archive "ma")
  (evil-define-key 'normal mu4e-headers-mode-map (kbd "d") '+mu4e-move-to-archive)

  (defun +mu4e-headers-quit-buffer ()
    "Quit the mu4e-headers buffer without returning to the main view."
    (interactive)
    (mu4e-mark-handle-when-leaving)
    (quit-window t)
    (mu4e--query-items-refresh 'reset-baseline))

  (evil-define-key 'normal mu4e-headers-mode-map (kbd "q") '+mu4e-headers-quit-buffer))

Composing mail

  • When I’m composing a new email, default to using the current context.
  • Compose new messages (as with C-x m) using mu4e-user-agent.
  • Once I’ve sent an email, kill the associated buffer instead of just burying it.
  • If a message is encrypted, my reply should always be encrypted, too.
(use-package mu4e-compose
  :after mu4e
  :load-path "~/media/code/mu/build/mu4e"
  :hook (mu4e-compose-mode . +encrypt-responses)

  :custom
  (mu4e-compose-context-policy nil)
  (mail-user-agent 'mu4e-user-agent)
  (message-kill-buffer-on-exit t)

  :config
  (defun +encrypt-responses ()
    "Encrypt the current message if it's a reply to another encrypted message."
    (let ((msg mu4e-compose-parent-message))
      (when (and msg (member 'encrypted (mu4e-message-field msg :flags)))
        (mml-secure-message-encrypt-pgpmime)))))

Write HTML emails in Org by toggling org-msg-mode.

I don’t enable this by default because I usually prefer plain-text email, but every now and then it’s nice to be able to send a message with syntax highlighting and LaTeX snippets (as PNGs) and all that fancy nonsense.

(use-package org-msg
  :after (mu4e org)
  :config
  (setq org-msg-options "html-postamble:nil H:5 num:nil ^:{} toc:nil author:nil email:nil tex:dvipng \\n:t"
        org-msg-startup "inlineimages"
        org-msg-greeting-fmt "\nHello, %s,\n\n"
        org-msg-greeting-name-limit 3
        org-msg-text-plain-alternative t
        org-msg-signature "

Cheers,
Harry Schwartz

Viewing mail

  • I’d rather word-wrap long lines when viewing mail.
  • Hit C-c C-o to open a URL in the browser.
  • Display the sender’s email address along with their name.
  • Save attachments in my ~/downloads directory, not my home directory.
  • While HTML emails are just fundamentally awful, we usually still need to read them. Tweaking shr settings ensures that their formatting in Emacs isn’t too hideous.
  • Some HTML emails are just too messy to display in Emacs. This binds a h to open the current email in my default Web browser.
  • As in the header view, I want mail to be archived, not deleted.
(use-package mu4e-view
  :after mu4e
  :load-path "~/media/code/mu/build/mu4e"
  :hook (mu4e-view-mode . visual-line-mode)
  :bind (:map mu4e-view-mode-map ("C-c C-o" . shr-browse-url))

  :custom
  (mu4e-view-show-addresses t)
  (mu4e-attachment-dir "~/downloads")

  (mu4e-html2text-command 'mu4e-shr2text)
  (shr-color-visible-luminance-min 60)
  (shr-color-visible-distance-min 5)
  (shr-use-fonts nil)
  (shr-use-colors nil)

  :config
  (advice-add #'shr-colorize-region
              :around (defun shr-no-colorize-region (&rest ignore)))
  (add-to-list 'mu4e-view-actions
               '("html in browser" . mu4e-action-view-in-browser)
               t)

  (evil-define-key 'normal mu4e-view-mode-map (kbd "d") '+mu4e-move-to-archive)
  (+bind-scroll-keys mu4e-view-mode-map))

Sending mail over SMTP

I send my email through msmtp. These settings describe how to send a message:

  • Use a sendmail program instead of sending directly from Emacs,
  • Tell msmtp to infer the correct account from the From: address,
  • Don’t add a ”-f username” flag to the msmtp command, and
  • Use /usr/bin/msmtp!
(use-package sendmail
  :after mu4e
  :custom
  (message-send-mail-function 'message-send-mail-with-sendmail)
  (message-sendmail-extra-arguments '("--read-envelope-from"))
  (message-sendmail-f-is-evil 't)
  (sendmail-program "msmtp"))

Agenda integration

mu4e-org lets me store Org links to emails. I use this to reference emails in my TODO list while keeping my inbox empty.

When storing a link to a message in the headers view, link to the message instead of the search that resulted in that view.

(use-package mu4e-org
  :after (mu4e org)
  :load-path "~/media/code/mu/build/mu4e"
  :custom
  (mu4e-org-link-query-in-headers-mode nil))

Configure org-contacts with mu4e

Use an org-contacts file to manage my address book.

(use-package org-contacts
  :load-path "resources"
  :after (mu4e org)
  :custom
  (org-contacts-files '("~/documents/contacts.org"))

  :config
  (setq mu4e-org-contacts-file (car org-contacts-files))
  (add-to-list 'mu4e-headers-actions
               '("org-contact-add" . mu4e-action-add-org-contact) t)
  (add-to-list 'mu4e-view-actions
               '("org-contact-add" . mu4e-action-add-org-contact) t))

RSS with elfeed

I use elfeed to read my (300-odd!) feeds.

  • Sort RSS feeds first by tag (comics come before haskell, for example), then by name of the feed, and finally by publication date.
  • Increase the max number of simultaneous connections to 32.
(use-package elfeed
  :after writing-mode
  :commands (elfeed elfeed-update)
  :custom
  (elfeed-search-title-max-width 120)

  :config
  (evil-define-key 'normal elfeed-show-mode-map (kbd "U") 'elfeed-show-tag--unread)

  (defun +custom-elfeed-sort (a b)
    (let* ((a-tags (format "%s" (elfeed-entry-tags a)))
           (b-tags (format "%s" (elfeed-entry-tags b)))
           (a-title (elfeed-feed-title (elfeed-entry-feed a)))
           (b-title (elfeed-feed-title (elfeed-entry-feed b))))
      (if (string= a-tags b-tags)
          (if (string= a-title b-title)
              (< (elfeed-entry-date b) (elfeed-entry-date a))
            (string< b-title a-title))
        (string< a-tags b-tags))))
  (setf elfeed-search-sort-function #'+custom-elfeed-sort)

  (defun +elfeed-entry-reformat (buff)
    (switch-to-buffer buff)
    (writing-mode 1)
    (elfeed-show-refresh))
  (setq elfeed-show-entry-switch '+elfeed-entry-reformat)

  (+bind-scroll-keys elfeed-show-mode-map)

  (elfeed-set-max-connections 32))

Open elfeed with C-c r.

(global-set-key (kbd "C-c r") 'elfeed)

I store my feeds in an Org file, of course. This parses them into something elfeed understands.

(use-package elfeed-org
  :after (elfeed org)
  :init
  (elfeed-org)
  (setq rmh-elfeed-org-files (list "~/documents/rss-feeds.org")))

I don’t subscribe to many YouTube channels, but I use elfeed-tube to load items with some associated metadata (descriptions, transcripts, etc).

(use-package elfeed-tube
  :after elfeed
  :demand t
  :config
  (elfeed-tube-setup))

Browsing the Web

I use Firefox to browse the Web, but I’d like to open Gemini links in elpher. This checks the prefix of each URL and uses the appropriate program to open it.

(use-package elpher
  :commands (elpher-go)
  :config
  (+bind-scroll-keys elpher-mode-map))

(use-package eww
  :config
  (+bind-scroll-keys eww-mode-map))

(setq +gemini-browser 'elpher-go)

(defun +browse-url (url &rest args)
  (if (s-prefix? "gemini:" url)
      (funcall +gemini-browser url)
    (browse-url-default-browser url args)))

(setq browse-url-browser-function '+browse-url)

Exporting Org files to HTML and opening the result triggers /usr/bin/sensible-browser, which checks the $BROWSER environment variable to choose the right browser. I’d like to always use Firefox for that, so:

(setenv "BROWSER" "firefox")

engine-mode

I sometimes use =engine-mode= to (mostly) look up error messages.

(use-package engine-mode
  :config
  (engine-mode t)

  (defengine duckduckgo
    "https://duckduckgo.com/?q=%s"
    :keybinding "/")

  (defengine wikipedia
    "http://www.wikipedia.org/search-redirect.php?search=%s&language=en&go=Go"
    :keybinding "w"))

Writing prose

I write prose in several modes: I might be editing an Org document, or a commit message, or an email. These are the main ones, with sub-items being derived from their parents:

  • git-commit-mode
  • text-mode
    • markdown-mode
      • gfm-mode
    • message-mode
      • mu4e-compose-mode
    • org-mode

Recall that derived modes “inherit” their parent’s hooks, so a hook added onto e.g. text-mode will also be executed by mu4e-compose-mode.

There are some exceptions, but I can usually associate a hook with every prose-related mode, so I store those in a list:

(defvar prose-modes
  '(gfm-mode
    git-commit-mode
    markdown-mode
    message-mode
    mu4e-compose-mode
    org-mode
    text-mode))

(defvar prose-mode-hooks
  (mapcar (lambda (mode) (intern (format "%s-hook" mode)))
          prose-modes))

Enable spell-checking in the usual places

I want to make sure that I’ve enabled spell-checking if I’m editing text, composing an email, or authoring a Git commit.

(use-package flyspell
  :ensure-system-package ispell
  :config
  (setq ispell-personal-dictionary "~/.ispell_words")
  (dolist (hook prose-mode-hooks)
    (add-hook hook 'flyspell-mode)))

Use Org-style lists and tables everywhere

Enable Org-style tables.

(add-hook 'markdown-mode-hook 'orgtbl-mode)
(add-hook 'message-mode-hook 'orgtbl-mode)

Use the =orgalist= package for more convenient list manipulation.

(use-package orgalist
  :hook ((git-commit-mode markdown-mode message-mode) . orgalist-mode))

Linting prose

I’ve been using vale as a prose linter, and it’s not been bad so far. There’s a package that integrates it with flycheck, but it doesn’t seem to work, so I’ve got some code here to do it manually.

(flycheck-define-checker vale
  "A checker for prose"
  :command ("vale" "--output" "line"
            source)
  :standard-input nil
  :error-patterns
  ((error line-start (file-name) ":" line ":" column ":" (id (one-or-more (not (any ":")))) ":" (message) line-end))
  :modes prose-modes)

(add-to-list 'flycheck-checkers 'vale 'append)

Activate prose-assistant-mode

I wrote this global minor mode to let me quickly trigger a handful of common tools I reach for while writing prose (dictionaries, word counting, etymologies, spellchecking, translation, that sort of thing).

(use-package synosaurus
  :ensure-system-package wordnet
  :custom
  (synosaurus-choose-method 'default))

(use-package prose-assistant-mode
  :load-path "resources"
  :bind (([f10] . prose-assistant-mode)
         ("<XF86Go>" . prose-assistant-mode))
  :config
  (prose-assistant-mode t))

Activate writing-mode

This minor mode enables a distraction-free writing environment. It enables a whole bunch of pretty modes, switches fonts, enables inline images, and even displays the word count in the mode-line. Toggle it with <f9>.

(use-package mixed-pitch)
(use-package olivetti)
(use-package org-appear :after org)
(use-package org-modern :after org)
(use-package org-superstar :after org)

(use-package wc-mode
  :custom
  (wc-modeline-format "[%tw words]")

  :config
  (unbind-key "C-c C-w" wc-mode-map)
  (add-to-list 'minions-prominent-modes 'wc-mode))

(use-package writing-mode
  :load-path "resources"
  :defer 1
  :hook (org-mode . writing-mode-repo)
  :bind (([f9] . writing-mode)
         ("<XF86Messenger>" . writing-mode)
         ("<XF86Tools>" . writing-mode))

  :config
  (require 'mixed-pitch)
  (require 'olivetti)
  (require 'org-appear)
  (require 'org-indent)
  (require 'org-modern)
  (require 'org-superstar)
  (require 'wc-mode)

  (defun writing-mode-repo ()
    (when (or (s-starts-with? "/home/hrs/documents/journal" buffer-file-name)
              (s-starts-with? "/home/hrs/documents/notes" buffer-file-name)
              (s-starts-with? "/home/hrs/documents/org" buffer-file-name))
      (writing-mode 1)))

  (setq writing-enabled-modes
        '((org-mode . (org-appear-mode
                       org-indent-mode
                       org-modern-mode
                       org-superstar-mode))
          (elfeed-show-mode . (mixed-pitch-mode
                               olivetti-mode))
          (special-mode . (mixed-pitch-mode
                           olivetti-mode))
          (text-mode . (flycheck-mode
                        mixed-pitch-mode
                        olivetti-mode
                        prettify-symbols-mode
                        visual-line-mode
                        wc-mode)))))

(use-package publish-mode
  :load-path "resources"
  :bind ([f8] . publish-build-and-view-pdf))

Editing with Markdown

Because I can’t always use org.

  • Associate .md files with GitHub-flavored Markdown.
  • Use pandoc to render the results.
  • Apply syntax highlighting in code blocks.
(use-package markdown-mode
  :ensure-system-package pandoc
  :commands gfm-mode
  :mode (("\\.md$" . gfm-mode))
  :config
  (custom-set-faces
   '(markdown-pre-face ((t nil))))

  (setq markdown-command "pandoc --standalone --mathjax --from=gfm"
        markdown-disable-tooltip-prompt t
        markdown-fontify-code-blocks-natively t))

Cycle between spacing alternatives

Successive calls to cycle-spacing rotate between changing the whitespace around point to:

  • A single space,
  • No spaces, or
  • The original spacing.

Binding this to M-SPC is strictly better than the original binding of just-one-space.

(global-set-key (kbd "M-SPC") 'cycle-spacing)

Enable region case modification

(put 'downcase-region 'disabled nil)
(put 'upcase-region 'disabled nil)

Note-taking and citation management

I’m using denote for note-taking, plus citar and citar-denote to manage citations.

Configure a few packages to handle bibliographies and citations with denote and bind keys:

C-c n b
List all the notes that link to this note.
C-c n c
Insert a citation to an existing reference in this file.
C-c n d
Suggest unlinked notes that are textually similar to this one.
C-c n e
Create a new bibliography entry.
C-c n f
Fuzzy-find a note by its filename.
C-c n g
grep through notes.
C-c n l
Insert a link to another note.
C-c n n
Create a new note.
C-c n r
Prompt for an existing reference and visit or create a literature note associated with it.
C-c n s
Search the contents of my notes.
(use-package denote
  :hook (dired-mode . denote-dired-mode)
  :bind (("C-c n b" . denote-link-find-backlink)
         ("C-c n f" . +denote-find-file)
         ("C-c n g" . +denote-grep)
         ("C-c n l" . denote-link)
         ("C-c n n" . denote))

  :custom
  (denote-directory "~/documents/notes")

  :config
  (defun +denote-find-file ()
    (interactive)
    (let ((project-current-directory-override denote-directory))
      (project-find-file)))

  (defun +denote-grep (term)
    (interactive (list (deadgrep--read-search-term)))
    (deadgrep term denote-directory)))

Citations

I’ve got a single big bibtex file that contains all the references in my notes. That’s mostly books, but also some papers and Web sites.

I’m not in love with bibtex-entry as a tool for adding new references to my database, so I wrote a few functions to do that.

(use-package citar
  :custom
  (org-cite-csl-styles-dir denote-directory)
  (setq org-cite-export-processors '((md csl "chicago-fullnote-bibliography.csl")
                                     (latex csl "chicago-fullnote-bibliography.csl")
                                     (t csl "modern-language-association.csl")))

  (org-cite-global-bibliography '("~/documents/notes/references.bib"))

  (org-cite-insert-processor 'citar)
  (org-cite-follow-processor 'citar)
  (org-cite-activate-processor 'citar)
  (citar-bibliography org-cite-global-bibliography)

  :bind (("C-c n c" . org-cite-insert)
         ("C-c n e" . +bibliography-create-reference))

  :hook
  (LaTeX-mode . citar-capf-setup)
  (org-mode . citar-capf-setup)

  :config
  (defun +bibliography-create-reference (type)
    "Add a bibliographic reference of TYPE to the first entry in `org-cite-global-bibliography'."
    (interactive
     (list (intern
            (completing-read "Type of reference: "
                             '(article book website)))))
    (cl-case type
      (article (call-interactively '+bibliography-create-reference-article))
      (book (call-interactively '+bibliography-create-reference-book))
      (website (call-interactively '+bibliography-create-reference-www))
      (t (message "unknown reference type!"))))

  (defun +insert-bibliography-entry (type slug-components props)
    (let ((entry (concat "\n@" (prin1-to-string type) "{"
                         (+slug-for (string-join slug-components "-")) ",\n  "
                         (string-join (mapcar (lambda (p)
                                                (concat (prin1-to-string (car p)) " = {" (cdr p) "}"))
                                              props) ",\n  ") "\n"
                         "}\n")))
      (append-to-file entry nil (car org-cite-global-bibliography))))

  (defun +bibliography-create-reference-book (title author publisher year)
    (interactive
     (list
      (read-string "Title: ")
      (read-string "Author: ")
      (read-string "Publisher: ")
      (read-string "Year: ")))

    (+insert-bibliography-entry 'book (list author year)
                                `((title . ,title)
                                  (author . ,author)
                                  (publisher . ,publisher)
                                  (year . ,year))))

  (defun +bibliography-create-reference-article (title author year journal)
    (interactive
     (list
      (read-string "Title: ")
      (read-string "Author: ")
      (read-string "Year: ")
      (read-string "Journal: ")))

    (+insert-bibliography-entry 'article (list author year)
                                `((title . ,title)
                                  (author . ,author)
                                  (year . ,year)
                                  (journal . ,journal))))

  (defun +bibliography-create-reference-www (url title author)
    (interactive
     (list
      (read-string "URL: " (when (string-match-p "^http" (current-kill 0))
                             (current-kill 0)))
      (read-string "Title: ")
      (read-string "Author: ")))

    (+insert-bibliography-entry 'www (list author title)
                                `((title . ,title)
                                  (author . ,author)
                                  (url . ,url)))))

citar-denote makes it a bit easier to create and manage literature notes, which correspond to specific bibtex entries.

(use-package citar-denote
  :hook (org-mode . citar-denote-mode)

  :bind ("C-c n r" . citar-create-note)

  :custom
  (citar-notes-paths (list denote-directory))
  (citar-denote-title-format "title"))

Searching notes

I wrote docsim.el to provide richer search functionality in my notes. I’m biased, but I think it works pretty well.

Calling docsim-search-buffer opens a buffer of links to nodes that are (1) textually similar to this note, and (2) not yet linked from it.

(use-package docsim
  :ensure t
  :ensure-system-package ((go . golang)
                          (docsim . "go install github.org/hrs/docsim/docsim@latest"))

  :commands (docsim-search docsim-search-buffer)
  :bind (("C-c n s" . docsim-search)
         ("C-c n d" . docsim-search-buffer))

  :custom
  (docsim-search-paths `(,denote-directory
                         "~/documents/journal/entries"
                         "~/documents/logistics"
                         "~/documents/blog/_drafts"
                         "~/documents/blog/_posts"))
  (docsim-omit-denote-links t)

  :init
  (evil-define-key '(insert normal) docsim-mode-map (kbd "RET") 'docsim--visit-link)
  (evil-define-key '(insert normal) docsim-mode-map (kbd "q") 'docsim--quit-sidebuffer))

File management with dired

I’ve started using dired as my primary file manager. About time, huh?

  • Set some specific ls switches:
    • Use the long listing format.
    • Sort numbers naturally.
    • Don’t include the owner or group names.
    • Use human-readable sizes.
    • Format timestamps as YYYY-MM-DD.
    • Include hidden files, but don’t include ”.” or ”..”.
  • Kill buffers of files/directories that are deleted in dired.
  • When I’ve got two dired windows side-by-side, and I move or copy files in one window, set the default location to the other window.
  • Always copy directories recursively instead of asking every time.
  • Do please ask before recursively deleting a directory, though.
  • Enable auto-revert-mode in dired buffers (so when a directory’s contents are modified the results are reflected in the buffer automatically).

I’m often browsing directories of photos and images, so this also binds ”v” to view a slideshow of the current directory with s (a custom feh wrapper defined elsewhere in this repo).

(use-package dired
  :demand t
  :ensure nil
  :hook (dired-mode . (lambda () (undo-tree-mode 1)))

  :config
  (defun +dired-slideshow ()
    (interactive)
    (start-process "dired-slideshow" nil "s" (dired-current-directory)))

  (evil-define-key 'normal dired-mode-map (kbd "o") 'dired-find-file-other-window)
  (evil-define-key 'normal dired-mode-map (kbd "p") 'transient-extras-lp-menu)
  (evil-define-key 'normal dired-mode-map (kbd "v") '+dired-slideshow)

  (setq-default dired-listing-switches
                (combine-and-quote-strings '("-l"
                                             "-v"
                                             "-g"
                                             "--no-group"
                                             "--human-readable"
                                             "--time-style=+%Y-%m-%d"
                                             "--almost-all")))

  (setq dired-clean-up-buffers-too t
        dired-dwim-target t
        dired-recursive-copies 'always
        dired-recursive-deletes 'top
        global-auto-revert-non-file-buffers t
        auto-revert-verbose nil))

Hide dotfiles by default, but toggle their visibility with ”.”. This conflicts with evil-repeat, but in practice I never use that with dired, so the mnemonic is worth it for me.

(use-package dired-hide-dotfiles
  :demand t
  :config
  (dired-hide-dotfiles-mode 1)
  (evil-define-key 'normal dired-mode-map "." 'dired-hide-dotfiles-mode))

Open media with the appropriate programs.

(use-package dired-open
  :demand t
  :ensure-system-package (abiword
                          feh
                          (ffplay . ffmpeg)
                          gnumeric
                          mpv
                          zathura)
  :config
  (setq dired-open-extensions
        `(("avi" . "mpv")
          ("cbr" . "zathura")
          ("cbz" . "zathura")
          ("doc" . "abiword")
          ("docx" . "abiword")
          ("epub" . "foliate")
          ("flac" . "mpv")
          ("gif" . "ffplay")
          ("gnumeric" . "gnumeric")
          ("jpeg" . ,(executable-find "s"))
          ("jpg" . ,(executable-find "s"))
          ("m3u8" . "mpv")
          ("m4a" . "mpv")
          ("mkv" . "mpv")
          ("mobi" . "foliate")
          ("mov" . "mpv")
          ("mp3" . "mpv")
          ("mp4" . "mpv")
          ("mpg" . "mpv")
          ("pdf" . "zathura")
          ("png" . ,(executable-find "s"))
          ("webm" . "mpv")
          ("webp" . ,(executable-find "s"))
          ("wmv" . "mpv")
          ("xcf" . "gimp")
          ("xls" . "gnumeric")
          ("xlsx" . "gnumeric"))))

Files are normally moved and copied synchronously. This is fine for small or local files, but copying a large file or moving a file across a mounted network drive blocks Emacs until the process is completed. Unacceptable!

This uses emacs-async to make dired perform actions asynchronously.

(use-package async
  :demand t

  :config
  (dired-async-mode 1))

Convert images

I sometimes need to convert images with imagemagick by changing the format and/or size. This provides a wrapper around that in dired.

(defun +image-dimensions (filename)
  "Given an image file `filename' readable by `identify', return a cons pair of integers denoting the width and height of the image, respectively."
  (->> (shell-command-to-string (format "identify %s" filename))
       (s-split " ")
       (nth 2)
       (s-split "x")
       (mapcar #'string-to-number)))

(defun +dired-convert-image (source-file target-width target-height target-file)
  (interactive
   (let* ((source-file (dired-file-name-at-point))
          (source-dimensions (+image-dimensions source-file))
          (source-width (nth 0 source-dimensions))
          (source-height (nth 1 source-dimensions))
          (target-width (read-number "Width: " source-width))
          (target-height (read-number "Height: "
                                      (if (= source-width target-width)
                                          source-height
                                        (round (* source-height
                                                  (/ (float target-width)
                                                     source-width))))))
          (target-file (read-file-name "Target: " nil nil nil
                                       (file-name-nondirectory source-file))))
     (list source-file target-width target-height target-file)))

  (call-process "convert" nil nil nil
                (expand-file-name source-file)
                "-resize" (format "%sx%s"
                                  target-width
                                  target-height)
                (expand-file-name target-file)))

Editing settings

Quickly visit Emacs configuration

I futz around with my dotfiles a lot. This binds C-c e to quickly open my Emacs configuration file.

(defun +visit-emacs-config ()
  (interactive)
  (find-file (concat user-emacs-directory "configuration.org")))

(global-set-key (kbd "C-c e") '+visit-emacs-config)

Always kill current buffer

Assume that I always want to kill the current buffer when hitting C-x k.

(defun +kill-current-buffer ()
  "Kill the current buffer without prompting."
  (interactive)
  (kill-buffer (current-buffer)))

(global-set-key (kbd "C-x k") '+kill-current-buffer)

Set up helpful

The helpful package provides, among other things, more context in Help buffers.

(use-package helpful
  :commands (helpful-callable helpful-variable helpful-key)
  :bind
  ("C-h f" . helpful-callable)
  ("C-h v" . helpful-variable)
  ("C-h k" . helpful-key)

  :config
  (evil-define-key 'normal helpful-mode-map (kbd "q") 'quit-window))

Save my location within a file

Using save-place-mode saves the location of point for every file I visit. If I close the file or close the editor, then later re-open it, point will be at the last place I visited.

(setq save-place-forget-unreadable-files nil)
(save-place-mode 1)

Always indent with spaces

Never use tabs. Tabs are the devil’s whitespace.

(setq-default indent-tabs-mode nil)

Install and configure which-key

which-key displays the possible completions for a long keybinding. That’s really helpful for some modes (like project.el, for example).

(use-package which-key
  :demand t
  :config (which-key-mode))

Configure yasnippet

I always want yasnippet enabled.

I don’t want yas to always indent the snippets it inserts. Sometimes this looks pretty bad (when indenting org-mode, for example, or trying to guess at the correct indentation for Python).

(use-package yasnippet
  :demand t
  :config
  (setq yas-indent-line 'auto)
  (yas-global-mode 1))

Configure minibuffer completion

I’m trying vertico, orderless, consult, and marginalia as my completion framework.

(use-package vertico
  :bind (:map vertico-map
              ("RET" . vertico-directory-enter)
              ("DEL" . vertico-directory-delete-char)
              ("M-DEL" . vertico-directory-delete-word))

  :init
  (vertico-mode))

(use-package savehist
  :demand t
  :init
  (savehist-mode))

(use-package orderless
  :demand t
  :custom
  (completion-styles '(orderless basic))
  (completion-category-defaults nil)
  (completion-category-overrides '((file (styles basic partial-completion)))))

(use-package consult
  :bind
  (("M-i" . consult-imenu)
   ("C-x b" . consult-buffer)
   ("C-x r b" . consult-bookmark)
   ("C-s" . consult-line))

  :config
  (setq completion-in-region-function #'consult-completion-in-region))

(use-package marginalia
  :bind (:map minibuffer-local-map
              ("M-A" . marginalia-cycle))

  :init
  (marginalia-mode))

Switch and rebalance windows when splitting

When splitting a window, I invariably want to switch to the new window. This makes that automatic. Similarly, when closing a window I’d like to rebalance the remaining windows.

(advice-add #'delete-window
            :after #'(lambda (&rest _)
                       (balance-windows)))

(advice-add #'split-window
            :after #'(lambda (&rest _)
                       (balance-windows)
                       (other-window 1)))

Mass editing of grep results

I like the idea of mass editing grep results the same way I can edit filenames in dired. These keybindings allow me to use C-x C-q to start editing grep results and C-c C-c to stop, just like in dired.

(use-package wgrep)

(eval-after-load 'grep
  '(define-key grep-mode-map
    (kbd "C-x C-q") 'wgrep-change-to-wgrep-mode))

(eval-after-load 'wgrep
  '(define-key grep-mode-map
    (kbd "C-c C-c") 'wgrep-finish-edit))

(setq wgrep-auto-save-buffer t)

Reformat typographic symbols and HTML entities to plain text

I sometimes need to convert some copied text containing typographic symbols like curly quotes and em-dashes into ASCII text. Similarly, I’ll sometimes need to do that with HTML entities, too. This provides functions to do that within a specified region.

(defvar +typographic-replacements
  '(("" . "...")
    ("" . "'")
    ("" . "'")
    ("" . "\"")
    ("" . "\"")
    ("" . "--")
    ("" . "---")))

(defvar +html-entity-replacements
  '(("&amp;" . "&")
    ("&nbsp;" . " ")
    ("&lsquo;" . "'")
    ("&rsquo;" . "'")
    ("&apos;" . "'")
    ("&ldquo;" . "\"")
    ("&rdquo;" . "\"")
    ("&quot;" . "\"")
    ("&lt;" . "<")
    ("&gt;" . ">")))

(defun +replace-symbols (replacements)
  (save-restriction
    (when (region-active-p)
      (narrow-to-region (region-beginning) (region-end)))
    (dolist (pair replacements)
      (goto-char (point-min))
      (while (search-forward (car pair) nil t)
        (replace-match (cdr pair))))))

(defun +replace-typographic-symbols ()
  "Replace common typographic symbols in the region or buffer with their ASCII equivalents."
  (interactive)
  (+replace-symbols +typographic-replacements))

(defun +replace-html-entities ()
  "Replace common HTML entities in the region or buffer with their ASCII equivalents."
  (interactive)
  (+replace-symbols +html-entity-replacements))

Edit files as sudo

I always forget the TRAMP syntax, and this provides the easier-to-remember sudo-edit function.

(use-package sudo-edit
  :commands (sudo-edit))

calc

Start calc in insert mode.

(use-package calc
  :ensure nil

  :config
  (add-hook 'calc-trail-mode-hook 'evil-insert-state))

Printing

Sometimes I like reading dead trees.

(use-package transient-extras-lp
  :commands (transient-extras-lp-menu)

  :custom
  (transient-extras-lp-saved-options '("-dBrother_HL_L2340D_series"
                                       "-osides=two-sided-long-edge"
                                       "-omedia=letter")))

Set custom keybindings

Just a few handy functions.

(global-set-key (kbd "C-w") 'backward-kill-word)
(global-set-key (kbd "M-o") 'other-window)
(global-set-key (kbd "C-x C-b") 'ibuffer)

Remap when working in terminal Emacs.

(define-key input-decode-map "\e[1;2A" [S-up])

Extra

Load any extra bits and bobs.

(when (file-exists-p (concat user-emacs-directory "private.el"))
  (load-file (concat user-emacs-directory "private.el")))