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