An Emacs Keyboard Shortcut for Changing the Font Size

Regardless of which editor I’m using, it’s common for me to change the font size depending on whether I’m using my MacBook Pro’s Retina display or the non-retina 27” Thunderbolt Display at my desk. It’s a small thing, but, when I started using Emacs, one of my biggest annoyances was the amount of friction involved in changing its font size. Here’s how I fixed that.

First, the Code

If you open the macOS font panel in Emacs by pressing cmd - t, you can select a new font or font size and have it applied to the entire window. Then, you must close the font panel.

What I actually wanted was to simply press cmd - = or cmd - -, but that functionality is built-in. Since Emacs is easily extended, I wrote the following code to make it happen. If you’d like to use it as well, all you need to do is copy and paste this into your ~/.emacs configuration file.


(defvar chris-font-size 11)
(defvar chris-font "SF Mono")

(add-variable-watcher 'chris-font #'chris--font-watcher)
(add-variable-watcher 'chris-font-size #'chris--font-watcher)

(defun chris--font-watcher (&optional sym newval _ _)
  (let ((face (if (eq 'chris-font sym) newval chris-font))
    (size (if (eq 'chris-font-size sym) newval chris-font-size)))
    (set-frame-font (concat face "-" (number-to-string size)) t)))

(chris--font-watcher)

(defun chris-set-font-size (arg)
  (interactive "P")
  (let ((size (if (eq nil arg) 11
        (prefix-numeric-value arg))))
    (setq chris-font-size size)
    (message "Font size is now %s" size)))

(require 'cl)
(defun chris-font-adjuster (delta)
  (lexical-let ((delta delta))
           (lambda ()
         (interactive)
         (let ((current-prefix-arg (list (+ delta chris-font-size))))
           (call-interactively #'chris-set-font-size)))))

(bind-key "s-=" (chris-font-adjuster 2))
(bind-key "s--" (chris-font-adjuster -2))
(bind-key "s-0" #'chris-set-font-size)

For the Curious: How It Works

Emacs has a built-in function called set-frame-font. You can call it with a string like “SF Mono-11,” and it will set both the font and font size. This function effectively does the same thing as configuring the font via the font panel (cmd - t).

In order to facilitate increasing or decreasing the font size, I needed to keep track of some state: specifically, the current font size.

After some perusing of the Emacs info pages, I figured out that you can attach watcher functions to variables. When the value changes, your function will be invoked. My strategy was to simply define one variable each for the font and font size, and, whenever either changes, invoke set-frame-font. Then, I bound some handlers to the appropriate keys to change the value of these variables.

lexical-let ?

As far as Lisp languages go, I have a strong background in Clojure and a passing familiarity with Scheme (mostly from reading SICP). Both of these languages are lexically bound–at least by default.

I was surprised to find that this is not the case for Emacs Lisp.

My original definition for chris-font-adjuster was throwing errors at runtime because the value for delta was unbound.

There are a couple of ways of attaining the more sensible (and familiar) lexical binding. One is to add a specially formatted comment to the top of your file, but this caused problems when I attempted to use it from my top-level .emacs configuration. It’s a better fit when you are writing a more complicated library or extension for Emacs.

In this case, I opted to use the lexical-let macro from the cl (Common Lisp) library that ships with Emacs. I presume it emulates lexical binding by generating a random symbol and then rewriting your code to reference that symbol, but I didn’t see any need to seek a deeper understanding of the code.

Conclusion

Even though I disagree with many of the design choices of Emacs Lisp, I have a lot of respect for how extensible and flexible the system is as a whole.

I hope you found this interesting, if not outright useful. Do you see any mistakes or ways to improve this? If so, let me know in the comments.