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 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))
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))
(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))
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))
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)
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)
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))
(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))
This sets up the current theme.
(setq custom-theme-directory
(concat user-emacs-directory "themes"))
(load-theme 'witchhazel t)
This binds C-c <left>
and C-c <right>
to undo and redo window configuration changes.
(winner-mode 1)
(desktop-save-mode 1)
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))
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-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)))
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))
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))
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.
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))
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))
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))
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)
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))
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))
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))
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)
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))
- 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)))
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))
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 inevil-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))
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")))
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))
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)
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))
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))
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
andC-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))
Add syntax highlighting to cron
files.
(use-package crontab-mode)
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))
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)
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))))
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))
rainbow-delimiters
is convenient for coloring matching parentheses.
(use-package rainbow-delimiters
:hook ((emacs-lisp-mode lisp-mode racket-mode) . rainbow-delimiters-mode))
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))
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)
(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)
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))
(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))
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 runxmp
, 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)
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))
Indent with 2 spaces.
(add-hook 'sh-mode-hook
(lambda ()
(setq sh-basic-offset 2
sh-indentation 2)))
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)
Support syntax-based indentation when editing SQL files.
(use-package sql-indent
:hook (sql-mode . sqlind-minor-mode))
Install terraform-mode
.
(use-package terraform-mode
:ensure-system-package terraform
:custom
(terraform-format-on-save t))
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)
This is not a place of honor.
(use-package yaml-mode)
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)
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))
- 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")))
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))))))
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")))
(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))))
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 "]]"))))
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)
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)
(use-package ox-md
:ensure nil
:after org
:commands (org-export-dispatch))
(use-package ox-epub
:after org
:commands (org-export-dispatch))
- 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 topygments
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)
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))))
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!"))
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"))))))
- 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))
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 ofmu4e~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))
- When I’m composing a new email, default to using the current context.
- Compose new messages (as with
C-x m
) usingmu4e-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
- 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))
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 theFrom:
address, - Don’t add a ”
-f username
” flag to themsmtp
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"))
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))
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))
I use elfeed
to read my (300-odd!) feeds.
- Sort RSS feeds first by tag (
comics
come beforehaskell
, 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))
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")
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"))
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))
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)))
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))
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)
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))
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))
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))
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)
(put 'downcase-region 'disabled nil)
(put 'upcase-region 'disabled nil)
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)))
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"))
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))
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
indired
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))
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)))
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)
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)
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))
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)
Never use tabs. Tabs are the devil’s whitespace.
(setq-default indent-tabs-mode nil)
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))
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))
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))
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)))
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)
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
'(("&" . "&")
(" " . " ")
("‘" . "'")
("’" . "'")
("'" . "'")
("“" . "\"")
("”" . "\"")
(""" . "\"")
("<" . "<")
(">" . ">")))
(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))
I always forget the TRAMP syntax, and this provides the easier-to-remember sudo-edit
function.
(use-package sudo-edit
:commands (sudo-edit))
Start calc
in insert
mode.
(use-package calc
:ensure nil
:config
(add-hook 'calc-trail-mode-hook 'evil-insert-state))
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")))
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])
Load any extra bits and bobs.
(when (file-exists-p (concat user-emacs-directory "private.el"))
(load-file (concat user-emacs-directory "private.el")))