Customizing the Emacs Modeline

There are a few phrases in the popular lexicon that I’ve developed something of an aversion to; phrases whose use has come to make me do an immediate mental wince & start to re-evaluate my opinion of the author.

One of those phrases is “bloated” (as in, “this piece of software is totally bloated”). It seems like it’s used to mean, at best, “this has features I don’t use” and at worst is the hallmark of a certain sort of denizen of the online programming community that seems to spend most of its time railing against codes of conduct and insisting that you can’t be a “real programmer” if you’ve ever written in anything other than C.

That being said, I’ve been on a bit of a kick to clean some of the cruft out of my Emacs config. I wouldn’t say that things had gotten “bloated”, nor am I going for some sort of minimalist setup (I am using Emacs, after all; I have no interest in using a statically-linked version of ed, or whatever it is that the commentariat has anointed as the current best practice), but I really like being able to understand how as much as possible of my stack works.

Not only is it empowering to be able to debug and customize anything I feel the desire to, but I’ve learned to be leery of adding too many fancy bells and whistles, as the more complicated configurations seem to eventually collapse under their own weight, and one ends up exerting a great deal of effort just to make all the pieces play nicely which each other.

The most recent manifestation of this effort has been to change the modeline I’m using.

When I switched back to Emacs from (Neo)Vim a few years ago, I was using airline (which I’d switched to from powerline) for a fancy modeline, so it seemed the natural thing to do was to use was Spaceline. Spaceline is a port of powerline to Emacs for the Spacemacs project, so my Emacs modeline could look largely the same as my vim status line, without having to go all the way to Spacemacs. It had been serving me fine since then, but I was starting to get a little sick of it and I realized both that I didn’t really want most of the stuff it was trying to do and that I’d like to be able to shake up the look a little bit.

With Vim, I never really tried to customize the status line beyond just using a plugin, because it seemed so byzantine: It’s a big ol’ format string, with various special things for changing the colour or calling functions and seems a lot like defining shell prompts – in a word, not very fun.

With Emacs though, I was pleasantly surprised to learn how easy it was to have a pretty nice modeline. Instead of a string, one sets mode-line-format to be a list, which can contain both format strings (e.g. "%m" will show the current major mode – see the documentation for mode-line-format for more), but also forms like '(:eval (some code)), where (some code) will get eval’d every time the modeline refreshes! In conjunction with the propertize function that lets one apply a face to a string (e.g. (propertize "hello!" 'face some-face) will return the string "hello!", but with some special annotations that will make Emacs display it with the given face).

One bit of behaviour of Spacemacs that I wanted to immitate was showing some things differently in the modeline for the active versus inactive windows. While there are separate faces for mode-line and mode-line-inactive, those can only really be used to make the default colour & backgrounds different – if I want something to, say, be gray-on-orange in the active window but orange-on-gray in the inactive one, that doesn’t help me much. Reading through the code that Spaceline uses (which actually comes from Powerline, so I guess I’ve gone full circle now), I was able to set up some simple hooks and advice that let me easily determine if a given window is active:

;; Keep track of selected window, so we can render the modeline differently
(defvar cogent-line-selected-window (frame-selected-window))
(defun cogent-line-set-selected-window (&rest _args)
  (when (not (minibuffer-window-active-p (frame-selected-window)))
    (setq cogent-line-selected-window (frame-selected-window))
(defun cogent-line-unset-selected-window ()
  (setq cogent-line-selected-window nil)
(add-hook 'window-configuration-change-hook #'cogent-line-set-selected-window)
(add-hook 'focus-in-hook #'cogent-line-set-selected-window)
(add-hook 'focus-out-hook #'cogent-line-unset-selected-window)
(advice-add 'handle-switch-frame :after #'cogent-line-set-selected-window)
(advice-add 'select-window :after #'cogent-line-set-selected-window)
(defun cogent-line-selected-window-active-p ()
  (eq cogent-line-selected-window (selected-window)))

With this (and the suitable definition of faces & related helper functions) I was able to put together this nice, simple little modeline:

(setq-default mode-line-format

               '(:eval (propertize (if (eq 'emacs evil-state) "  " "  ")
                                   'face (cogent/evil-state-face)))

               " "
               mode-line-misc-info ; for eyebrowse

               '(:eval (when-let (vc vc-mode)
                         (list " "
                               (propertize (substring vc 5)
                                           'face 'font-lock-comment-face)
                               " ")))

               '(:eval (list
                        ;; the buffer name; the file name as a tool tip
                        (propertize " %b" 'face 'font-lock-type-face
                                    'help-echo (buffer-file-name))
                        (when (buffer-modified-p)
                           " "
                           'face (if (cogent-line-selected-window-active-p)
                        (when buffer-read-only
                           'face (if (cogent-line-selected-window-active-p)
                        " "))

               ;; relative position in file
               '(:eval (list (nyan-create))) ;; from the nyan-mode package
               (propertize "%p" 'face 'font-lock-constant-face)

               ;; spaces to align right
               '(:eval (propertize
                        " " 'display
                        `((space :align-to (- (+ right right-fringe right-margin)
                                              ,(+ 3 (string-width mode-name)))))))

               ;; the current major mode
               (propertize " %m " 'face 'font-lock-string-face)))

(if you can’t see some of the characters above, I’m using some of the icon characters in the private use area of my go-to font Pragmata Pro).

Here’s what it looks like in action:

Figure 1: Modeline on the active window
Figure 2: Modeline on an inactive window

(and yes, I kept the nyan-mode package for just a little bit of fun & flair in my modeline)

The little Vim icon shows I’m in evil-mode and changes colours based on the state (normal, insert, visual, operator-pending, etc); in Emacs state it shows a little gnu head, like below.

Figure 3: Modeline on active window in Emacs state

The full source for my modeline config is here.