This is my Emacs configuration. Org Babel, a native feature of Emacs that can run code snippets embedded in rich text documents, spurred a fad in the Emacs community where users define their personal configs in README documents like this one.
In that spirit, I tried to contain my actual lisp code in this document, save for one function that you need in init.el
to kick off the process.
(defun recker/load-config () "Tangle configuration and load it." (let ((config (concat (file-name-as-directory user-emacs-directory) "README.org"))) (if (file-exists-p config) (org-babel-load-file config) (warn (concat config " not found - not loading"))))) (recker/load-config)
From here on, all lisp code snippets with syntax highlighting are run on startup in the order they appear.
One more note before we get going. You are free to use this as a starting place for your own configuration. All of my custom functions begin with the recker/
prefix. To change it, just perform a string replace on (defun recker/
within this document. Beyond that, just update these global variables with your own info.
(setq user-full-name "Alex Recker")
(setq user-mail-address "alex@reckerfamily.com")
(setq recker/work-mail-address "arecker@zendesk.com")
First, we setup the Emacs package manager. This occurs at the top of the file so we can install packages as we need them.
The Emacs package manager does a nice job. With the help of John Wiegley’s use-package, we can simultaneously declare a package we need and configure the package after it installs and loads.
As far as package maintenance, I sometimes run M-x package-list-packages
to bring up the packages screen. You can press U
then x
at this screen to pull down any updates that are ready, and it’s also a good idea to restart Emacs to make sure everything is still working.
(require 'package)
(defun recker/configure-packages ()
"Set up the package manager."
;; set and load custom file first (this is gitignored)
(setq custom-file (concat user-emacs-directory "custom.el"))
(if (file-exists-p custom-file) (load custom-file)
(warn "couldn't find custom file %s" custom-file))
;; set the repo URLs
(setq package-archives '(("gnu" . "http://elpa.gnu.org/packages/")
("melpa" . "https://melpa.org/packages/")))
;; initialize the package manager
(package-initialize)
;; install use-package
(unless (package-installed-p 'use-package)
(package-refresh-contents)
(package-install 'use-package)))
(recker/configure-packages)
If you’re using Emacs through a CLI, I’d recommend you close it out and download the real thing instead (if you’re running a mac, Emacs for Mac OS X is great no-frills package). Flipping between emacs and the terminal is still fine, though. Especially since with this config, running the command emacsclient
will connect to the running Emacs process seamlessly. You can even export EDITOR=emacsclient
in your environment so other tools can get on the same page.
(defun recker/configure-application ()
;; prompt when quitting (protects from fat fingers)
(setq-default confirm-kill-emacs #'yes-or-no-p)
;; start the server
(server-start))
(recker/configure-application)
Default Emacs gets a lot of crap for its “ugly” UI. I find it cozy and nostalgic. In an effort to maintain that unmistakable aesthetic, I make just a few modest tweaks to the appearance.
(defun recker/configure-appearance ()
"Configure the Emacs UI."
;; Suppress widgets
(menu-bar-mode 0)
(tool-bar-mode 0)
(scroll-bar-mode 0)
;; Set font
(when (string= system-type "darwin")
(set-frame-font "Monaco 18" nil t))
(when (string= system-type "gnu/linux")
(set-frame-font "Monospace 13" nil t))
;; Full screen by default
(unless (eq (frame-parameter (selected-frame) 'fullscreen) 'maximized)
(toggle-frame-maximized))
;; Use this package to hide minor modes. I like to know which major
;; mode I'm editing, but the minor mode list gets a little too
;; cluttered trying to list all the plugins I have running.
(unless (bound-and-true-p rich-minority-mode) ;it breaks if it runs twice?
(use-package rich-minority
:ensure t
:init (rich-minority-mode 't)
:config (setq rm-blacklist ""))))
(recker/configure-appearance)
Automatically clean-up whitespace on save. Trailing whitespace is annoying, and it shouldn’t be there in the first place. Also, don’t insert tabs unless the major mode really wants to (golang, for example, will do its own thing).
(add-hook 'before-save-hook 'whitespace-cleanup)
(setq-default indent-tabs-mode nil)
Bind the build in function replace-string
to C-c r
. By default, delete the selected text when you hit “backspace”. Also, use upcase-region
without Emacs bothering you about some nuance that I’ve never bothered to read closely - the function works just fine for me.
(global-set-key (kbd "C-c r") 'replace-string)
(setq delete-selection-mode 't)
(put 'upcase-region 'disabled nil)
Bind the handy expand-region
tool to C-\=
. This tool can highlight incrementally larger portions of text like quotes, parentheses, and function definitions.
(use-package expand-region
:ensure t
:bind (("C-=" . 'er/expand-region)))
Use yasnippet for managing snippets of text. To create a new snippet, run M-x yas-new-snippet
. This will open a buffer where, following some simple syntax rules, you can create dynamic snippets for any editing mode in Emacs. These are saved within the snippets/
directory of your emacs configuration.
(use-package yasnippet
:ensure t
:init (yas-global-mode))
The buffer would have to be the most common form of transportation in the Emacs world. Suppressing the more boisterous default splash screen, I’ve made the *scratch*
buffer my home. With these configs, I’ve made it so that this buffer can never be deleted. I wrote a good amount of custom code to print output from the infamous fortune
command (or another command if you want) on every launch. It’s also a great place to quickly test lisp expressions or paste random text.
;; don't show the splash screen
(setq inhibit-splash-screen 't)
;; never kill the scratch buffer
(defun recker/not-scratch-p ()
"Return NIL if the current buffer is the *scratch* buffer."
(not (equal (buffer-name (current-buffer)) "*scratch*")))
(add-hook 'kill-buffer-query-functions 'recker/not-scratch-p)
;; display the output of "fortune" as the scratch message
(setq recker/scratch-message-command "fortune --wrap 72 --comment ';; '")
(defun recker/scratch-message ()
"Return a scratch message from fortune-blog."
(concat "\n"
(recker/scratch-lisp-comment (format-time-string "%A, %B %-d %Y"))
"\n\n"
(shell-command-to-string recker/scratch-message-command)))
(defun recker/scratch-lisp-comment (text)
"Turn text into a lisp comment."
(with-temp-buffer
(insert text)
(let ((comment-start ";; "))
(comment-region (point-min) (point-max)))
(concat "\n" (buffer-string) "\n")
(buffer-string)))
(defun recker/refresh-scratch-buffer ()
"Redraw the *scratch* buffer."
(interactive)
(save-excursion
(switch-to-buffer "*scratch*")
(erase-buffer)
(insert (recker/scratch-message))))
(setq initial-scratch-message (recker/scratch-message))
Where C-x p
deletes the current buffer, I added my own function that deletes all buffers which you can call by C-x P
. Just like my browser tabs, sometimes I get a little overwhelmed and I need a clean slate to focus.
(global-set-key (kbd "C-x k") 'kill-this-buffer)
(defun recker/purge-buffers ()
"Delete all buffers, except for *scratch*."
(interactive)
(mapc #'(lambda (b) (unless (string= (buffer-name b) "*scratch*") (kill-buffer b))) (buffer-list)))
(global-set-key (kbd "C-x P") 'recker/purge-buffers)
In Emacs, you spend much of your time selecting things in the minibuffer. “Interactive Do” (IDO for short) can enhance this experience. IDO comes with Emacs, but I install some packages to display options vertically instead of horizontally, and also to plug the interface in to Imenu.
(defun recker/configure-ido ()
(setq ido-enable-flex-matching t)
(setq ido-everywhere t)
(ido-mode t)
(use-package ido-vertical-mode
:ensure t
:config (setq ido-vertical-define-keys 'C-n-and-C-p-only)
:init (ido-vertical-mode))
(use-package idomenu
:ensure t
:bind ("C-c i" . idomenu)))
(recker/configure-ido)
The M-x
menu also carries a lot of weight in the Emacs workflow. Transparently swapping out this command with the smex package adds value to this interface without changing the intuitive experience.
(use-package smex
:ensure t
:init (smex-initialize)
:bind (("M-x" . 'smex)
("M-X" . 'smex-major-mode-commands)))
For quickly jumping around a buffer, standard isearch
can’t be beat. But as a small luxury, sometimes I use the swiper package to quickly fuzzy search a buffer. I bind this to a similar keystroke as isearch so it’s easy to remember.
(use-package swiper
:ensure t
:bind ("C-c s" . swiper))
Use company mode for autocomplete. Without a direct way to call company mode, this plugin feels more magical to me. But other language modes seem to know where to find it without any needed interference, so that’s good.
(use-package company
:ensure t
:init (add-hook 'after-init-hook 'global-company-mode))
Use projectile for moving around git repos. From the outside, this plugin feels huge and robust. Compared to everything it can do, I barely use it. I’m content to leverage the project wide file search with C-c p f
and the compile interface with C-c p P
(all the projectile commands fall under the same C-c p
prefix).
(use-package projectile
:ensure t
:config
(define-key projectile-mode-map (kbd "C-c p") 'projectile-command-map)
(setq projectile-completion-system 'ido)
:init (projectile-mode t))
Unfortunately, Emacs litters the filesystem with “backup” files. I can appreciate that it’s trying to be helpful, but it drives me nuts so I turn it off. Another edge case - if a file changes while I’m looking at it, I make Emacs re-render the buffer live.
(setq make-backup-files nil)
(setq auto-save-default nil)
(global-auto-revert-mode t)
Speaking of backing up files, tell emacs version control to follow symlinks if the file is under version control.
(setq vc-follow-symlinks 't)
And while we’re at it, install magit for working with git. This is not an understatement - magit is truly a beautiful piece of software. The way I have it configured, you can open the interface with C-x g
(it will open git for the current file or prompt you for a git project).
(use-package magit
:ensure t
:bind ("C-x g" . magit-status))
Emacs has a great file manager called dired. To activate it, visit a directory just as you would open a file. Not wanting to interfere with greatness, I make just a few changes to the default behavior. I like to hide hidden files by default (you can see these by pressing C-x M-o
), hide the .
and ..
pointers that you see by default, and blow through extra confirmations when you delete a file with a visiting buffer.
(require 'dired-x)
(setq dired-use-ls-dired nil)
(setq dired-clean-confirm-killing-deleted-buffers nil)
(setq-default dired-omit-files-p t)
(setq dired-omit-files (concat dired-omit-files "\\|^\\..+$"))
(add-hook 'dired-mode-hook 'dired-omit-mode)
I make good use of the Emacs bookmarks system. To create a bookmark for a file, just press C-x r m
. To visit a bookmark, press C-x r b
(with my customization in place, you will have the chance to choose a bookmark with IDO fuzzy search, so it’s very convenient). On top of that, every file/directory created in the following paths automatically gets a bookmark entry. These entries blend seamlessly with your existing bookmarks.
~/org"
~/src/
~/src/work
(require 'bookmark)
(require 'cl-lib)
(setq bookmark-save-flag 1)
(defun recker/list-bookmarks ()
"List all bookmarks in alphabetical order, and filter out the junk entries I don't care about."
(let ((junk-entries '("org-capture-last-stored")))
(sort (cl-remove-if #'(lambda (b) (member b junk-entries))
(append (bookmark-all-names) ; actual saved bookmarks
;; then all the dynamic ones
(recker/list-files-as-bookmarks "org/" ".org")
(recker/list-entries-as-bookmarks "src/")
(recker/list-entries-as-bookmarks "src/work/")))
#'string<)))
(defun recker/list-entries-as-bookmarks (parent)
"List all the entries in the PARENT directory as if they were bookmarks."
(mapcar #'(lambda (n) (concat parent n))
(cl-remove-if #'(lambda (f) (string-prefix-p "." f))
(directory-files (expand-file-name (concat "~/" parent))))))
(defun recker/list-files-as-bookmarks (parent pattern)
"List all the files matching pattern as if they were bookmarks."
(mapcar #'(lambda (s) (string-remove-prefix (expand-file-name "~/") s))
(directory-files-recursively (expand-file-name (concat "~/" parent)) pattern nil)))
(defun recker/ido-bookmark-jump (bookmark)
"Switch to bookmark BOOKMARK interactively using `ido'."
(interactive (list (ido-completing-read "Bookmark: " (recker/list-bookmarks) nil t)))
(if (member bookmark (bookmark-all-names))
(bookmark-jump bookmark)
;; If it's not in the actual bookmark file, just treat the key
;; like a relative path (ex. src/work/azdrain => ~/src/work/azdrain)
(find-file (expand-file-name (concat "~/" bookmark)))))
(global-set-key (kbd "C-x r b") 'recker/ido-bookmark-jump)
Emacs has the built-in capability to send email. From anywhere, press C-x m
to open the compose mail screen, do your business, then hit the ubiquitous C-c C-c
to let it rip. Rather than sending the email directly, I configure Emacs to instead shell out to the CLI program msmtp, which is smart enough to use different settings based on the “From” address. I’ve tried a lot of solutions over the years, and I’ve settled on this solution as my favorite. If msmtp
isn’t installed or if it’s configured wrongly, Emacs will throw a pretty obvious error message.
(setq smtpmail-smtp-service 587
smtpmail-smtp-user user-mail-address
smtpmail-smtp-server "smtp.gmail.com"
send-mail-function 'smtpmail-send-it)
(setq message-send-mail-function 'message-send-mail-with-sendmail)
(setq sendmail-program "msmtp")
(setq mail-host-address "smtp.gmail.com")
(setq message-sendmail-f-is-evil 't)
(setq message-sendmail-extra-arguments '("--read-envelope-from"))
I read email with Gnus. It is both the worst and best tool for the job. My own journey into Gnus, from what I hear, is typical. I opened it once, felt disgusted, closed it out for a few months and tried other things, then occasionally retreated to Gnus to see if it was really as bad as I remember. Curiosity (or Stockholm syndrome) eventually got the best of me, and I concluded it was both the worst and best option for what I needed to do.
Without further ado, here are my very minimal settings for Gnus. Let’s get all the tuning stuff out of the way.
;; keep passwords in ~/.password-store/authinfo.gpg (and work)
(setq auth-sources
(list
(concat (expand-file-name "~/.password-store/") "authinfo.gpg")
(concat (expand-file-name "~/.password-store-work/") "authinfo.gpg")))
;; hide startup files in .emacs.d/gnus/
;; little known fact, there are an infinite number of gnus directories
;; and they WILL make their way to your home directory whether you
;; want it or not
(let ((gnus-dir (concat user-emacs-directory "gnus/")))
(setq gnus-startup-file (concat gnus-dir "newsrc"))
(setq gnus-home-directory (concat gnus-dir "gnus")
nnfolder-directory (concat gnus-dir "gnus/Mail/archive")
message-directory (concat gnus-dir "gnus/Mail")
nndraft-directory (concat gnus-dir "gnus/Drafts")
gnus-cache-directory (concat gnus-dir "gnus/cache")))
;; If you experience dribble, talk to your doctor.
(setq gnus-use-dribble-file nil)
;; set primary method to empty so the program doesn't absolutely
;; EXPLODE when you open it
(setq gnus-select-method '(nnml ""))
;; set topic mode (the only readable mode) as the default
(add-hook 'gnus-group-mode-hook 'gnus-topic-mode)
;; Don't move archived messages anywhere
(setq gnus-message-archive-group nil)
;; powerful placebo settings for faster perceived speed
(setq gnus-asynchronous t)
(setq gnus-use-cache t)
(setq gnus-check-new-newsgroups nil
gnus-check-bogus-newsgroups nil)
(setq gnus-show-threads nil
gnus-use-cross-reference nil
gnus-nov-is-evil nil)
(setq gnus-check-new-newsgroups nil
gnus-use-adaptive-scoring nil)
;; Look at this fucking variable lol
(setq gnus-summary-line-format "%U%R%z%I%(%[%4L: %-23,23f%]%) %s
")
;; Use this nerdy bullshit to save email addresseses for autocompletion
(use-package bbdb
:ensure t
:config (setq bbdb-file (concat user-emacs-directory "bbdb.el"))
:init
(bbdb-mua-auto-update-init 'message)
(setq bbdb-mua-auto-update-p 'query)
(add-hook 'gnus-startup-hook 'bbdb-insinuate-gnus))
;; auto filled messages look like shit on most normal mail clients, so
;; just turn it off to appease all the filthy casuals I email
(add-hook 'message-mode-hook #'turn-off-auto-fill)
Finally we’ve arrived at the backends. If you desire, you can hook it up to RSS and other fun backends. I just use mail, but I occasionally revisit this list when I want to play with something new.
(setq gnus-secondary-select-methods '())
;; personal email
(add-to-list 'gnus-secondary-select-methods
`(nnimap ,user-mail-address
(nnimap-address "imap.gmail.com")
(nnimap-server-port "imaps")
(nnimap-stream ssl)
(nnmail-expiry-target ,(format "nnimap+%s:[Gmail]/Trash" user-mail-address))
(nnmail-expiry-wait immediate)))
;; work email
(add-to-list 'gnus-secondary-select-methods
`(nnimap ,recker/work-mail-address
(nnimap-user ,recker/work-mail-address)
(nnimap-address "imap.gmail.com")
(nnimap-server-port "imaps")
(nnimap-stream ssl)
(nnmail-expiry-target ,(format "nnimap+%s:[Gmail]/Trash" recker/work-mail-address))
(nnmail-expiry-wait immediate)))
If all went according to plan, Gnus should be ready to use. Just run M-x gnus
, and if all went according to plan, you should see something resembling Email folders. There are only a few remaining things that regrettably have to be done manually.
- Make your topics. From the screen, I struggle through the topic commands to separate the IMAP folders into personal and work. I then use
U
to “unsubscribe” from the ones I don’t care about (which really just hides them). - Fix posting styles. With your cursor hovering on a topic or folder, press
G c
to open the customize menu (this is a useful interface, have a look around). From there I add “address” to personal and work topics as a posting style, this is needed for msmtp to correctly route to the right settings.
Some basic usability tips.
- Open folders by hitting
RET
over the folder, open messages by hittingRET
over the message - Trash mail by pressing
E
(expire) to mark it, thenq
to exit the folder. Expiring is done in batches - Archive mail by moving the message to the IMAP folder (
B m
, then choose the folder interactively). - Compose a new message by pressing
m
at the topic screen. Depending on where your cursor is, the corresponding styles and settings will apply.