cashmere

cashmere

Messing around with Nix, Emacs and EAF-Framework

DISCLAIMER

Please do not continue on reading if you are against:

Takeaways

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:

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.

Tags: #emacs #nix #eaf