Multi-Monitor Compatible Code to Center Emacs Frames on Screen
When centering my Emacs windows (aka ‘frames’) on my monitors, I noticed that with 2 monitors active, the computation doesn’t work: It doesn’t center in the main monitor; it centers in the area of both monitors combined. That’s not what I want.
Here’s a fix and an explanation of the problem.
Update 2022-04-22: There’s a much shorter version that is likely even more robust shared by Louis Brauer. See the new solution.
Why 2 monitors caused trouble
Looking at the documentation for display-pixel-width
, I learned that this is intended.
There’s a distinction between “monitor” and “display” in Emacs that was absolutely not what I expected. “Display” is the total available space, while “monitor” is a single device.
To get per-monitor geometry information, the documentation advices the reader to look at display-monitor-attributes-list
. That shows information for all monitors known to the system, though, so you’d have to find the right onw; looking at the docs there accidentally brought up a related function, frame-monitor-attributes
, that limits the information to the monitor of the current Emacs frame. Since I want to center a newly created frame on screen, this works perfectly.
Using the monitor workarea
The result of (frame-monitor-attributes)
for the main monitor is a list of attributes and value like this:
((geometry 0 0 3440 1440)
(workarea 0 25 3440 1415)
(mm-size 801 335)
(frames #<frame *Minibuf-1* 0x7f9c773b3418>)
(source . "NS"))
This has the added benefit of defining a ‘workarea’ next to ‘geometry’ that excludes the space taken up by main menu. So I don’t need to compute this area myself.
I never worked with an attribute list like that, so I had to look up a couple of functions for hash maps, propertly lists, dictionaries etc. until one eventually worked. In the process I noticed there also is the function (frame-monitor-workarea)
that does the job, so I’ll be using that. In case you’re curious how to unpack an attribute, though, try: (alist-get 'workarea (frame-monitor-attributes))
.
Now the result of this is the list of values:
(0 25 3440 1415)
To get to the 4th item in this list, i.e. the usable height, cadddr
is used. That’s a shorthand:
- 3x
cdr
calls, which would becdddr
. In modern languages, we’d be calling this “drop first”. It returns rest of the list sans the first element. - That on its own would produce a list with 1 item,
(1415)
. If you’re new to Lisp, thats basically an array with 1 element. - So we add a
car
call that fetches the tip of the list. For example(car '(foo bar fizz buzz))
returnsfoo
. This gives us the number.
To unpack the 3rd item in this list, i.e. the usable width, we use caddr
– note it does one less cdr
call, so the temporary list result is (3440 1415)
, and then the car
fetches the number, 3440
.
Update 2021-06-11: You can also use (nth INDEX LIST)
. I prefer that because even though it looks like I’m a Lisp noob, it’s easier to figure out which element you get.
Armed with this knowledge, I’ve added these two functions to get the width and height, including proper documentation:
(defun ct/frame-monitor-usable-height (&optional frame)
"Return the usable height in pixels of the monitor of FRAME.
FRAME can be a frame name, a terminal name, or a frame.
If FRAME is omitted or nil, use currently selected frame.
Uses the monitor's workarea. See `display-monitor-attributes-list'."
(cadddr (frame-monitor-workarea frame)))
(defun ct/frame-monitor-usable-width (&optional frame)
"Return the usable width in pixels of the monitor of FRAME.
FRAME can be a frame name, a terminal name, or a frame.
If FRAME is omitted or nil, use currently selected frame.
Uses the monitor's workarea. See `display-monitor-attributes-list'."
(caddr (frame-monitor-workarea frame)))
If you never ever use these functions anywhere else, the following would do the trick, too, as local variables in a function:
;; ...
(let* ((workarea (frame-monitor-workarea frame))
(width (caddr workarea))
(height (cadddr workarea)))
;; use width and height here
)
;; ...
I guess with more Elisp experience, I’d be using these directly, but at the moment I benefit from dedicated functions with documentation to encapsulate concepts like “unpack the width from a monitor workarea value list”.
Update 2021-06-11: Göktuğ Kayaalp shared a condensed single-function approach that I tweaked a bit and shared in another post.
Resulting code to center a frame on screen
Here is the complete, fully updated version:
(defun ct/frame-monitor-usable-height (&optional frame)
"Return the usable height in pixels of the monitor of FRAME.
FRAME can be a frame name, a terminal name, or a frame.
If FRAME is omitted or nil, use currently selected frame.
Uses the monitor's workarea. See `display-monitor-attributes-list'."
(cadddr (frame-monitor-workarea frame)))
(defun ct/frame-monitor-usable-width (&optional frame)
"Return the usable width in pixels of the monitor of FRAME.
FRAME can be a frame name, a terminal name, or a frame.
If FRAME is omitted or nil, use currently selected frame.
Uses the monitor's workarea. See `display-monitor-attributes-list'."
(caddr (frame-monitor-workarea frame)))
(defun ct/center-box (w h cw ch)
"Center a box inside another box.
Returns a list of `(TOP LEFT)' representing the centered position
of the box `(w h)' inside the box `(cw ch)'."
(list (/ (- cw w) 2) (/ (- ch h) 2)))
(defun ct/frame-get-center (frame)
"Return the center position of FRAME on it's display."
(ct/center-box (frame-pixel-width frame) (frame-pixel-height frame)
(ct/frame-monitor-usable-width frame) (ct/frame-monitor-usable-height frame)))
(defun ct/frame-center (&optional frame)
"Center a frame on the screen."
(interactive)
(let* ((frame (or (and (boundp 'frame) frame) (selected-frame)))
(center (ct/frame-get-center frame)))
(apply 'set-frame-position (flatten-list (list frame center)))))
Update 2021-11-06: I noticed that some time after I began using Emacs 28, (and (boundp 'frame) frame)
caused trouble and would fall-back to selected-frame
too often. A working solution to auto-center a frame after making it is to omit the boundp
check and just write this: (let* ((frame (or frame (selected-frame))) ...
Now it works, without any hacks and manual macOS menu offsetting.
Also see the shorter single-function variant.