Disabling emacs window management

How to force emacs to cede some control to the window manager.
April 6, 2018 (programming, emacs, i3)

I use Emacs, which is mostly great. However I also use i3, a tiling window manager, and there’s a little bit of overlap. Emacs can split frames into multiple windows and shuffle things around - but so can i3, and in fact that’s specifically what i3 is built for.

I resolve this tension pretty heavy-handedly. I use only i3 for placing windows. Emacs is stripped of all power to split frames.

Here’s how I get emacs to behave (annotated from my init.el.

The main part is configuring display-buffer-alist, which controls what happens when emacs wants to display a buffer.

(setq display-buffer-alist
      '(("shell.*" (display-buffer-same-window) ())
        (".*" (display-buffer-reuse-window
               display-buffer-same-window
               display-buffer-pop-up-frame)
         (reusable-frames . t))))

This tells emacs to always raise an existing window that’s already displaying the buffer, if one exists, and otherwise to display it in the current window. There’s a slight exception for shells - when I switch to them I want them to always display in the current window, even if there’s another frame somewhere already displaying them.

I override display-buffer-pop-up-window, which is used by some code that really wants to split the window (for example displaying grep results), to instead use the whole thing.

(defun anders/same-window-instead
    (orig-fun buffer alist)
  (display-buffer-same-window buffer nil))
(advice-add 'display-buffer-pop-up-window :around 'anders/same-window-instead)

I’ve forgotten exactly what this does - but I think it tells emacs to switch to the frame that’s already displaying whatever I want to look at, if such a frame exists.

(defun anders/do-select-frame (orig-fun buffer &rest args)
  (let* ((old-frame (selected-frame))
         (window (apply orig-fun buffer args))
         (frame (window-frame window)))
    (unless (eq frame old-frame)
      (select-frame-set-input-focus frame))
    (select-window window)
    window))
(advice-add 'display-buffer :around 'anders/do-select-frame)

I make transient frames (I have certain utilities that pop up a temporary frame) go away entirely when they’re done.

(setq frame-auto-hide-function 'delete-frame)

I completely neuter dedicated window functionality. I don’t need it, since I’m explicitly controlling all window placement anyways, and dedicated windows override a bunch of behavior and break all the other customizations.

(advice-add 'set-window-dedicated-p :around
            (lambda (orig-fun &rest args) nil))

There are also some package-specific fixes.

Org mode

  (setq org-agenda-window-setup 'current-window)

Gdb

(setq gdb-display-io-nopopup t)

Compile. compilation-goto-locus (for jumping to compile errors or grep results) is a huge offender here. It overrides display-buffer-alist with a let binding deep inside it’s internals. There’s nothing for it but to override the whole function with a stripped-down version (still pretty long but look at the original - it’s a real beast).

  (defun compilation-goto-locus (msg mk end-mk)
    "Jump to an error corresponding to MSG at MK.
All arguments are markers.  If END-MK is non-nil, mark is set there
and overlay is highlighted between MK and END-MK."

    (display-buffer (marker-buffer msg) '(nil (allow-no-window . t)))
    (with-current-buffer (marker-buffer msg)
      (goto-char (marker-position msg))
      (and w (compilation-set-window w msg)))

    (display-buffer (marker-buffer mk))

    (with-current-buffer (marker-buffer mk)
      (unless (eq (goto-char mk) (point))
        ;; If narrowing gets in the way of going to the right place, widen.
        (widen)
        (if next-error-move-function
            (funcall next-error-move-function msg mk)
          (goto-char mk)))
      (if end-mk
          (push-mark end-mk t)
        (if mark-active (setq mark-active)))
      ;; If hideshow got in the way of
      ;; seeing the right place, open permanently.
      (dolist (ov (overlays-at (point)))
        (when (eq 'hs (overlay-get ov 'invisible))
          (delete-overlay ov)
          (goto-char mk))))))

And as a complement to that, rust-mode tries to get fancy with jumping to compile errors. Undo that too.

  (eval-after-load 'compile
    (remove-hook 'next-error-hook 'rustc-scroll-down-after-next-error)))

There’s probably more interactions I’ll discover with various packages, but on the whole this has been working for me quite well.