Buffer-associated window layouts in Emacs

I’ve recently spent some time configuring Emacs. It is a pastime of sorts in which I occasionally get really invested and then eventually lose interest again. Each time, though, I start my configuration afresh, and I always learn something new.

This time, my interest has been split between two packages. On the one hand EXWM, a fully working X window manager within Emacs, and on the other Wanderlust, an e-mail client for Emacs with built-in IMAP support. The two packages are obviously quite different from one another, but they do have one thing in common: they require a lot of manual window management.

In Emacs, there are two common window layouts: the single-window layout, which is what it normally starts in, and the two-window layout, with a file in the upper window and a help buffer in the lower window. Usually, when I’m editing a file, I use a two-window layout.

However, when using EXWM, I’ve often found myself wanting to go from editing a file in a two-window layout to viewing an X program in a one-window layout. The obvious solution is to close the second window and switch to the buffer containing the X program. The problem with this solution is that I lose the original two-window layout. When I switch back to the file I was editing, it will be displayed in a single-window layout.

This annoyed me to such a degree that I spent a lot of time thinking about how best to solve it. Eventually, I come up with the following solution, which is both very simple and very useful.

I call it buffer-associated layouts. The idea is that each buffer is associated with a certain window layout, which is restored whenever that buffer is switched to. The implementation looks like this:

(defvar *buffer-layouts* (list) "Buffer-layout associations")
(defvar *protect-buffer-layouts* nil "Temporarily protect buffer layouts")

(defun restore-buffer-layout ()
  "Restore the layout associated with the current buffer."
  (interactive)
  (let ((conf (alist-get (current-buffer) *buffer-layouts*)))
    (if conf
	(progn
	  (set-window-configuration conf)
	  (message "Restored buffer layout"))
      (setf (alist-get (current-buffer) *buffer-layouts*)
	    (current-window-configuration))
      (message "Set buffer layout"))))

(defun switch-to-buffer-with-layout ()
  "Switch to the window layout associated with a buffer. At the
same time, associate the original buffer with the original
layout.

If the new buffer has no associated layout, it is displayed as
the only window in the frame."
  (interactive)
  (let ((*protect-buffer-layouts* t))
    (dolist (window (window-list))
      (setf (alist-get (window-buffer window) *buffer-layouts*)
	    (current-window-configuration)))
    (call-interactively #'helm-multi-files)
    (delete-other-windows)
    (let* ((buf (current-buffer))
	   (conf (alist-get buf *buffer-layouts*)))
      (when conf
	(set-window-configuration conf)
	(select-window (get-buffer-window buf))))))

(advice-add #'delete-other-windows :before
	    (lambda (&optional window)
	      (when (not *protect-buffer-layouts*)
		(dolist (window (window-list))
		  (setf (alist-get (window-buffer window) *buffer-layouts*) nil)))))
(advice-add #'delete-window :before
	    (lambda (&optional window)
	      (when (not window)
		(setq window (get-buffer-window)))
	      (when (not *protect-buffer-layouts*)
		(setf (alist-get (window-buffer window) *buffer-layouts*) nil))))
(advice-add #'quit-window :before
	    (lambda (&optional kill window)
	      (when (not window)
		(setq window (get-buffer-window)))
	      (when (not *protect-buffer-layouts*)
		(setf (alist-get (window-buffer window) *buffer-layouts*) nil))))

(set-keys "C-c b" switch-to-buffer-with-layout
	  "C-c n" restore-buffer-layout)

Basically, a global variable holds a list of all buffer–layout associations, which is updated and referred to whenever the switch-to-buffer-with-layout function is called. I’ve personally chosen to bind it to C-c b.

(Note that switch-to-buffer-with-layout manually calls helm-multi-files. If you don’t use Helm, you should replace this with switch-to-buffer.)

Here is a short demonstration of switching between a three-window file editing layout, a single-window EXWM layout and a two-window Wanderlust layout:

Video demonstration of switching between layouts by switching buffers.

As you can see, whenever I switch buffers (with C-c b), the window layout is updated too. The benefit with this approach is that you don’t need to learn and remember a separate system for keeping track of layouts. They’re automatically associated with the buffers, which you already know how to manage.

2 Responses to “Buffer-associated window layouts in Emacs”

  1. eric says:

    Note that Emacs comes with registers that allow you to do something similar out of the box: C-x r w B (where B is any letter) saves the current window configuration and C-x r j B (where B is the same letter) will restore that window configuration.

    What I don’t know is whether you can preload specific window configurations. I don’t think you can but nevertheless those two bindings are very helpful.

    • eric says:

      Funnily enough, just after posting this, I found the announcement of bookmark-view, a new package that adds window configurations to things you can bookmark. Check it out on MELPA.

Leave a Reply