Homelast changed: 2021-02-09

Configuring the Emacs display system

TL;DR

A topic of long and outstanding troubles for me has been the automatic selection of the other window in emacs. As soon as an info buffer is shown, the current window is split or another, already existing window is selected to show the buffer. This is no problem when there is only one window in the current frame, but if you split your frame into a carefully crafted set of windows, the next info buffer will most often annoy by appearing in one of those windows. So I tried to solve the question on how to change this behaviour.

Handling of other window

First, what is this other window? When you are working in emacs, typically all your navigation or modification commands are executed in the current window (the one the cursor mark is in). Besides that, there can be another special window, which you can also modify indirectly (e.g. with C-M-v to scroll). Often, this other window pops up to show info buffers activated with functions like describe-key or describe-function.

At the beginning, I had three ideas to handle this: pushing an annoying buffer into its own frame, explicitly selecting a window where the next buffer should appear and modifying the default selection strategy. I implemented all three to see which one works best.

Now, as it seems, the selection mechanism of the other window is actually a quite deterministic operation. Its frontend is pop-to-buffer and documentation is in display-buffer. Its behaviour is controlled by some variables (display-buffer-fallback-action, display-buffer-base-action etc), which define a priorized list of actions. There, a range of display-buffer-functions can be combined, generating this behaviour. When displaying a new buffer, each of these functions will be called. Each one now uses its own strategy to decide if there is an existing window or if a new window can be split to display the buffer. Each one also can step back and leave it to the following one to take the decision.

So the most promising idea mitigating the problem probably is to sensibly change the action list defined in display-buffer-fallback-action, overriding it in display-buffer-base-action. I did this, just replacing the display-buffer-use-some-window function, which obviously is the source of this erratic behaviour. My replacement is a combination of display-buffer-below-selected and display-buffer-at-bottom. After a while of testing this, it seems to be not such a bad selection, at least for frames using a large screen space. This way, e.g. windows for occur or magit, which you want to pop up on the fly, are put below the window you are working in currently. Other windows further down or at the sides are left untouched.

But you also need to make sure windows are not split if too small. When this happens the last action comes in, display-buffer-pop-up-frame, which will open a new frame to display the buffer. In my opinion, this is still better as a last resort than to change my window layout.

The configuration looks loke this.

(custom-set-variables
 '(display-buffer-base-action
   '((display-buffer--maybe-same-window display-buffer-reuse-window display-buffer--maybe-pop-up-frame-or-window display-buffer-in-previous-window display-buffer-below-selected display-buffer-at-bottom display-buffer-pop-up-frame)))
 '(window-min-height 8)
 '(window-min-width 40))

The actions in this list do things like checking if the buffer already had been displayed; if that window is still existing, then re-using it. Otherwise, they try to split a window below the current one or, if there is not enough place, at the frame's bottom.

To avoid opening a new frame, for example if your current one is already cluttered with small windows, I've added a function to temporarily edit with original behaviour.

(defun edit-with-fallback-display ()
  "reset `display-buffer-base-action' and edit recursively, to
  have the original buffer sharing behaviour."
  (interactive)
  (let ((display-buffer-base-action nil))
    (recursive-edit)))

Help and Apropos buffers

Help and apropos buffers should not go into a magit or occur window probably visible, but always into their own. So, I've added another configuration for them. Recognized by a regex, they receive a special list of display actions and also an attribute modifying the window size. I want to put those buffers into a window at bottom, where they are unobtrusive and can stay as long as I want. If such a window already exists, it should be reused. The given display-height attribute makes sure the window (also if reused) is shrunk to the size of it's new contents. If it needs to be widened instead, this is done at max to half of the frame size. This way, describing functions or variables can be repeated as often as wished and the description always goes into the same window, resizing it as needed.

(custom-set-variables
 '(display-buffer-alist
   '(("\\*Help\\*\\|\\*Apropos\\*"
      (display-buffer-reuse-help display-buffer-at-bottom)
      (window-height . fit-window-to-buffer-max-half-frame)))))

The function display-buffer-reuse-help uses a configurable regex to see if there is already a help window visible. If one is found, that one is used to show the new help buffer. Also, if a window height function is given, it is applied to adopt the size of the re-used window.

Note this configurable regex looks the same as the selector regex in the display-buffer-alist. But the selector triggers an action when a new buffer with this name should be displayed, while the second one is used to recognize and re-use a window already existing.

(defcustom help-buffer-regex "\\*Help\\*\\|\\*Apropos\\*"
  "regular expression defining which buffers should be recognized as help
buffers in `display-buffer-reuse-help'."
  :type '(string)
  :group 'my-cust)

(defun display-buffer-reuse-help (buffer alist)
  "If a *Help* or *Apropos* window is live, try to reuse that."
  (let ((wnd (car (remove-if-not
                   (lambda (w)
                     (string-match help-buffer-regex
                                   (buffer-name (window-buffer w))))
                   (window-list)))))
    (if (and wnd (window-live-p wnd))
        (window-update-buffer buffer wnd 'reuse alist))))

(defun window-update-buffer (buffer window type alist)
  "display buffer in window, recording type then apply height"
  (if (window--display-buffer buffer window type alist)
      (window-apply-height-fnc window alist)))

(defun window-apply-height-fnc (window alist)
  "rudimentary alist parser just accepting a height fnc for now.."
  (let ((height (cdr (assq 'window-height alist))))
    (if (functionp height)
        (ignore-errors (funcall height window)))
    window))

Finally, the window-height action attribute specified in the configuration above uses a custom function fit-window-to-buffer-max-half-frame. It calls fit-window-to-buffer to narrow the window to its new contents or, if widening it instead, uses half of the frame height as max height.

(defun fit-window-to-buffer-max-half-frame (&optional window)
  "fit window to buffer size, but use max half of current frame height."
  (interactive)
  (let ((wnd (or window (selected-window)))
        (max-height (/ (frame-height) 2)))
    (fit-window-to-buffer window max-height)))

Now try this and repeatedly call describe-function for different functions. The first call opens a help window, each following call changes its size and updates it. For me, this works nicely.

Explicitly selecting a window

As a second way to solve my problem, I implemented a function to dedicate a selected window for the next buffer to appear in. This way, you can create a window outside of your working environment, dedicate it and then from your main window call magit, occur or describe to appear there.

(defvar dedicated-other-window nil)
(defvar dedication-count)

(defun dedicate-window (arg)
  "Dedicate the currently selected window as 'other' window. When
called with a =C-0= prefix arg, releases the previously set
window and reverts to the default window selection behaviour. Default is a
one-time dedication, use =C-u= for unlimited."
  (interactive "p")
  (setq dedicated-other-window
        (if (eq arg 0)
            nil
          (selected-window)))
  (setq dedication-count
        (if (eq 4 arg) nil arg))
  (if dedicated-other-window
      (message "window dedicated")
    (message "dedication removed")))

To activate this, put display-buffer-dedicated-window at the beginning of your display-buffer-base-action (and also into display-buffer-alist or whereever you need it).

(defun display-buffer-dedicated-window (buffer alist)
  "Display pop-up-buffer in the dedicated other window, if one is
selected. If none is selected, revert to the default behaviour."
  (if (and dedicated-other-window
           (window-live-p dedicated-other-window))
      (prog1
          (window-update-buffer buffer dedicated-other-window 'reuse alist)
        (if (and dedication-count (> dedication-count 0))
            (setq dedication-count (- dedication-count 1)))
        (if (eq 0 dedication-count)
            (setq dedicated-other-window nil)))))

The prog1 makes sure the result of window-update-buffer (the window actually used or nil) is returned, indicating if this action has selected a window or if the next one should do it.

If you bind dedicate-window to a convenient key, this way of selecting which window to use as the next other window is not too clumsy.

Push buffer to own frame

Finally, if it is too late and a window already did pop up inside of your working environment, you can select it and use this function to push it out into a new frame. As it seems, this works with an emacs running on mac or windows, but not on X11. I'll have another look at this..

(defun push-buffer-to-frame ()
  "push current window buffer to own frame and revert current window to previous frame."
  (interactive)
  (let ((current (current-buffer))
        (selected (selected-window)))
    (if (display-buffer-other-frame current)
        (quit-restore-window selected))))

to be continued..

Now I'll see how these three ways are helping. So far, the most promising one seems to be the way of modifying the automatic display strategies. Of course, it is also the most expensive one. The two changes shown above (general change and specific change for info/apropos buffers) can be continued to apply special behavious for example also to dired, occur, deadgrep, magit windows. And the more you customize this, the more you also want a more fine grained "remote" control of the other window, i.e. by augmenting the set of C-x 4 keys. In the next post I'm continuing to show what I've done so far..

Feel free to comment on Reddit.

My current .emacs (always a work in progress) on github

Author: Jörg Kollmann (Reddit: u/e17i)

Made on emacs org-mode with Rethink