the Emacs Application Framework

The Emacs Application Framework is an extension for Emacs that (supposedly) integrates the capability for full embedded GUI programming, and comes with several apps that serve as proof-of-concept as well as being practically usable. Stuff like:

  • Web browser
  • Video player
  • PDF viewer
  • 2048 game
  • etc, etc

This is a Cool Idea.

too bad it’s shit!

Conceptual yellow flags

  1. EAF is built in Python, JS, and Elisp. That’s 3 languages, all of which have notorious idiosyncrasies. That’s not to say such an architecture can’t work - only that it’s rare and has a natural tendency for fiddly kludgy interop.

  2. EAF works by embedding a GUI window inside an Emacs frame. While this has been done before, it’s just a rather fraught concept. EXWM works because Emacs gets sole claim to all the X windows. Xwidget-webkit works because it’s been integrated into the C core. EAF, having neither of these luxuries, must work overtime to constantly reaffirm its supposed integration.

OK, but does it work tho?

Well no, but give it a year and try tweaking everything mercilessly for hours and maybe eventually it will.

I never got it working on GNOME - there was an extension you need to install but it wouldn’t, then it just won’t work with EmacsClient… i said oh well, and went about my day.

But now I’ve got it working on Hyprland, so I thought I’d best share my setup. Countless blogs and articles exist on this topic, and none of them got me here, but maybe this one helps you just the tiniest bit - after all, this app is a Cool Idea and it’s a shame it usually doesn’t work.

Here’s my Use-Package declaration on the elisp side:

(use-package eaf
  :elpaca (:host github :repo "emacs-eaf/emacs-application-framework"
               :files ("*.el" "*.py" "core" "app" "*.json")
               :pre-build (("python" ""
                            "--install" "pdf-viewer" "browser"
  :config (add-to-list 'load-path (expand-file-name "app/" eaf-source-dir)))
(use-package eaf-browser
  :elpaca nil
  :after eaf
  (eaf-browser-continue-where-left-off t)
  (eaf-browser-enable-adblocker t))

That long :elpaca declaration could also be :straight if you’re still on that package manager - everything’s mostly the same.

Now, despite that pre-build including the installation command, you need to run M-x eaf-install-and-update manually once the package has been loaded to make sure your system dependencies are installed (it needs sudo).

If that’s all you do, the EAF window will still display as a separate phantom window on Hyprland. To get it to overlay properly we need some lines in the Hyprland config file:

windowrule = float,*
windowrule = noanim,*
windowrule = noborder,*

There may be a better way to do this, but I don’t know what it is. Ideally we could set hyprland window rules on the shell/elisp side, but my experimentation has thus far failed to provide a reliable way to do so.

Interaction with other UI extensions

If you use the vertico-posframe package, the posframes will appear underneath the EAF window.

I wrote hypop.el to make a true minibuffer frame that pops up on Hyprland and it overcomes this problem. There’s another issue I encountered (while writing this very article, actually) where leaving the EAF window puts you on the workspace that hypop’s frame gets banished to. I suspect it’s because I’m using window Title rather than Class - will hopefully fix that soon, or find another workaround.

This was not the issue. Here is the actual chain of what was happening:

  • Emacs starts up. It shares its PID with the desktop in its window properties.
  • Hypop forces Emacs to make a new frame. This frame has the same PID since it’s the same Emacs instance.
  • EAF gets its coordinates based on polling Hyprland’s list of clients for Emacs' PID
    • Thus, if it happens to find the hypop window first it will (erroneously) try to base itself around _that_​.

So the issue was the PID, not the WM_CLASS. That’s something basically impossible to work around (I am NOT going to re-invent the Emacs Client).

Instead I decided to modify the function in which EAF makes this oversight - here’s the new one, paste it into the :config block of that use-package eaf above.

(if (and (string= (getenv "XDG_CURRENT_DESKTOP") "Hyprland")
       (featurep 'hypop))
    (defun eaf--get-frame-coordinate ()
      "We need fetch Emacs coordinate to adjust coordinate of EAF if it running on system not support cross-process reparent technology.

Such as, wayland native, macOS etc.

This is redefined for use with `hypop' library because multiple Emacs frames may be open."
      (cond ((string-equal (getenv "XDG_CURRENT_DESKTOP") "sway")
           (eaf--split-number (shell-command-to-string (concat eaf-build-dir "swaymsg-treefetch/ emacs"))))
          ((string-equal (getenv "XDG_CURRENT_DESKTOP") "Hyprland")
           (let ((clients (json-parse-string (shell-command-to-string "hyprctl -j clients")))
             (dotimes (i (length clients))
               ;; the below condition was modified for compatibility w/ `hypop'
               (when (and (equal (gethash "pid" (aref clients i)) (emacs-pid))
                          (not (string= (gethash "title" (aref clients i)) hypop--frame-name)))
                 (setq coordinate (gethash "at" (aref clients i)))))
             (list (aref coordinate 0) (aref coordinate 1))))
           (require 'dbus)
           (let* ((coordinate (eaf--split-number
                               (dbus-call-method :session "org.gnome.Shell" "/org/eaf/wayland" "org.eaf.wayland" "get_emacs_window_coordinate" :timeout 1000)
                  ;; HiDPI need except by `frame-scale-factor'.
                  (frame-x (truncate (/ (car coordinate) (frame-scale-factor))))
                  (frame-y (truncate (/ (cadr coordinate) (frame-scale-factor)))))
             (list frame-x frame-y)))
           (list 0 0)))))

Note that because of the surronding if statement, this redefinition only runs when hypop is loaded and we’re running under Hyprland.

Some people may say, “use advice​”. But amen amen I say to you, this modification is so many conditions deep inside the function that advice wouldst not be practical.

Now, I realize that they may change how things work in the near future. In that case, I’ll fix my config and hopefully update this article. But for the moment, it’s a decent workaround.