DISCLAIMER
Please do not continue on reading if you are against:
- Integration of third-party languages into Emacs
- Bloat/Unnecessary integrations into Emacs
- FUN!!
Takeaways
- Let nixpkgs do the heavy lifting and handle the correct way of packaging complex packages such as emacs-eaf
- Small emacs-lisp snippet to integrate and handle eaf buffers in a more flexible way
- Small showcase
If you could not care less about my personal why, then feel free to move on to the .
Backstory
I think the vast majority of us do agree, that Emacs is one if not the most extensible playground, as I like to call it, somebody can experience.
In my case, it was exwm and emacs-eaf showcased in this video. Now after integrating Emacs not only into my developer workflow, but more as a tool I just love to use from day to day, I made the learning that it's less of how much can this editor handle until it breaks, but more of a how can this environment benefit me in my day to day challenges. I still think it's a fun way to interact with Emacs, maybe extend it in ways you cannot with emacs-lisp alone or to just experience more possibilities of Emacs extensibility beyond the ones you currently know.
Nonetheless, I love to mess, try and configure Emacs. If something does break, doesn't matter as everything is version controlled. This statement is true until the point where third-party dependencies come my way. That's the moment, where I utilize Nix to profit from declarative packaging and then configure it on my side.
Now as cool and fun as emacs-eaf does look, the installation process is the complete opposite.
Digging into nixpkgs, you may realize that eaf is also packaged.
Implementation
Nix, letting nixpkgs do the heavy lifting
The whole point of using Nix here is to never think about EAF's dependency hell yourself. Python, Qt, DBus wiring, nixpkgs takes care of all of it. You just declare what you want.
Inside my Emacs derivation I override emacs-application-framework with the specific apps I care about:
myEmacs = pkgs.emacs-gtk.pkgs.withPackages (epkgs: [
epkgs.mu4e
epkgs.offlineimap
epkgs.pdf-tools
epkgs.nushell-ts-mode
(epkgs.emacs-application-framework.override {
enabledApps = [
epkgs.eaf-browser
epkgs.eaf-pyqterminal
eaf-auralis
];
})
]);
That's it. No install-eaf.py, no manual pip install, no chasing Qt versions. The override mechanism lets you pick exactly which EAF apps get built and bundled. Everything else, Python environment, shared libraries, wrapper scripts, is handled by the package expression upstream.
Rebuild, switch, done. If it breaks, roll back one generation.
Emacs, loading EAF and wiring up C-SPC
Now on the Emacs side, the packages are on the load-path thanks to Nix, but we still need to require them at the right time. EAF needs a graphical display, so loading it during a headless daemon start will crash. The solution is a small init function that defers loading until a frame actually exists:
(setq eaf-pyqterminal-font-size 24
eaf-pyqterminal-refresh-ms 17
eaf-pyqterminal-font-family "Maple Mono NF"
eaf-pyqterminal-cursor-type "bar"
eaf-pyqterminal-cursor-size 1)
(defvar my/eaf-loaded nil
"Non-nil after EAF has been successfully loaded.")
(defun my/eaf-init ()
"Load EAF and its apps, but only when a graphical display is available.
Errors are caught so a broken EAF never kills the Emacs session."
(when (and (display-graphic-p) (not my/eaf-loaded))
(condition-case err
(progn
(require 'eaf)
(require 'eaf-browser)
(require 'eaf-pyqterminal)
(require 'eaf-auralis)
;; Evil leader integration
(when (featurep 'evil)
(require 'eaf-evil)
(setq eaf-evil-leader-key "C-SPC")
(let ((leader-km (when (boundp 'general-override-mode-map)
(lookup-key
(evil-get-auxiliary-keymap
general-override-mode-map 'normal)
(kbd "SPC")))))
(when (keymapp leader-km)
(setq eaf-evil-leader-keymap leader-km))))
(setq my/eaf-loaded t)
(message "EAF loaded successfully."))
(error
(message "EAF failed to load: %s" (error-message-string err))))))
(if (daemonp)
(add-hook 'server-after-make-frame-hook #'my/eaf-init)
(add-hook 'after-init-hook #'my/eaf-init))
The key bit is the C-SPC binding. EAF buffers live in their own Qt world, normal Evil keybindings don't reach them. By setting eaf-evil-leader-key to C-SPC and pointing eaf-evil-leader-keymap at your existing leader map, you get your full SPC menu inside EAF buffers via C-SPC. Buffer switching, perspectives, whatever you bound under your leader, it all just works.
The condition-case wrapper is not optional paranoia. EAF depends on a living Python process. If something in the Nix build changed or Qt decides to be Qt, you don't want your entire init to bail. This way Emacs starts fine and you get a message telling you what went wrong.
Crash cleanup
One thing that annoyed me: when the EAF Python process dies (and it will, eventually), it leaves behind dead buffers that just sit there doing nothing. A small process sentinel fixes that:
(advice-add 'eaf--start-process :after
(lambda (&rest _)
(when-let* ((proc (get-process "eaf")))
(set-process-sentinel
proc
(lambda (process event)
(when (memq (process-status process) '(exit signal))
(message "EAF process terminated: %s" (string-trim event))
(dolist (buf (buffer-list))
(when (and (buffer-live-p buf)
(with-current-buffer buf
(derived-mode-p 'eaf-mode)))
(with-current-buffer buf
(let ((inhibit-read-only t))
(erase-buffer)
(insert (format "EAF process crashed: %s\nRe-open this buffer to restart."
(string-trim event)))))))))))))
Instead of stale frozen buffers, you get a clear message and can just reopen.
Showcase
With all of that in place, you end up with:
eaf-open-browsera full Chromium-based browser inside an Emacs buffereaf-open-pyqterminala fast Qt terminal emulator, also inside a bufferC-SPCinside any EAF buffer drops you back into your leader keymap
Nothing here is something you need. But if you enjoy pushing Emacs into weird corners and letting Nix clean up after you, it's a fun ride.