Who Framed Emacs?
To be honest, I don’t really like frames[1] in Emacs. One reason is how bad they play with i3, my preferred tiling window manager. Any call of raise-frame fails drawing my attention to it unless the frame has been on the same workspace already which makes it rather useless for me. The other reason is that unlike windows[2] frames do not have first-class support. It is considerably hard convincing Emacs to prefer frames over windows, so hard in fact that there is a package for it with an optional dependency list from hell. Supporting frames programmatically isn’t simple for me either. I try very hard to do so in my own packages, but given my aforementioned setup, I simply won’t run into more subtle bugs involving them.
That being said, there are situations where I’m OK with frames. Emacs does always create an initial frame, no matter whether you run a normal instance, use --batch or go for the Emacs daemon. This can be easily verified with (> (length (frame-list)) 0). Additionally to the initial frame, extra frames can be created. I usually don’t do this by hand, so this only happens to me when emacsclient requests a new one.
The distinction between the initial frame and every other frame becomes quite important as soon as you wish to run code after a frame was created. I do this for three things, adjusting the default fontset to display Emoji properly, resetting the ansi-color-names-vector variable for my theme and handling C-i independently from TAB. My first attempt is making use of after-make-frame-functions:
(defun my-modify-frame () ...) (add-hook 'after-make-frame-functions 'my-modify-frame)
This will not work because after-make-frame-functions is a so-called abnormal hook, but will nevertheless not error out. Unlike other hooks every function in it is run with an argument, in this case, the created frame. If you rely on the frame in question to be selected (like, for the theme case), you must select it explicitly:
(defun my-modify-frame (frame) (with-selected-frame frame ...)) (add-hook 'after-make-frame-functions 'my-modify-frame)
Much better. For some reason though, it only seems to work in emacsclient and on creation of subsequent frames in normal Emacs instances. A quick search of the Emacs Lisp sources reveals that make-frame ends up running the hook, but is make-frame actually used for the initial frame? command-line in startup.el suggests it does use frame-initialize which uses make-frame internally, but any attempts at getting a replacement function displaying debug information fails for me, so I’m just going to assume the initial frame is very special and requires a stupid workaround: Unconditionally executing the function and adding it to the hook. To make this a bit more convenient, I’ll make the frame parameter optional.
(defun my-modify-frame (&optional frame) (with-selected-frame (or frame (selected-frame)) ...)) (my-modify-frame) (add-hook 'after-make-frame-functions 'my-modify-frame)
And there you go. This time with the actual code I’m running:
(defun my-filter-C-i (map) (if (and (bound-and-true-p evil-mode) (eq evil-state 'normal)) (kbd "<C-i>") map)) (with-eval-after-load 'evil-maps (define-key evil-motion-state-map (kbd "<C-i>") 'evil-jump-forward)) (defun my-modify-frame (&optional frame) (with-selected-frame (or frame (selected-frame)) (set-fontset-font "fontset-default" nil "Symbola" nil 'append)) (load-theme 'my-solarized t) (define-key input-decode-map [?\C-i] `(menu-item "" ,(kbd "TAB") :filter my-filter-C-i))) (my-modify-frame) (add-hook 'after-make-frame-functions 'my-modify-frame)
Next up: Explaining what the hell I’m doing there with Evil.
edit: Turns out frame-initialize was a false lead. Earlier in the text I’ve suggested a possible way to verify that there’s always an initial frame. Here’s the output of the batch mode for the frame list:
$ emacs --batch --eval "(princ (frame-list))" # (#<frame F1 0xbd29e8>)
It’s interesting that this initial frame isn’t named after a buffer, but always gets a hardcoded name. Searching the sources for "F1" leads to make_initial_frame called from init_window_once called from the main function. I assume that for a normal Emacs session this initial frame is kept and turned into a fully-featured graphical one while it is kept as is for both daemon and batch mode. This explains of course why the hook function wasn’t called: That piece of code didn’t contain anything to run it. Worse even, the bare frame probably couldn’t be altered by hook functions given how little of it is initialized.
[1] | This unfortunate naming choice is the result of Emacs predating more common naming systems. The rest of the world refers to them as windows. |
[2] | Another unfortunate naming choice, even RMS agrees they’d better be called panes. |