Wai Hon's Blog

Another Emacs i3 Integration

2021-10-15 #emacs #i3wm

Pavel Korytov wrote an inspiring blog post to get a consistent set of keybindings between i3 and Emacs. It caught my attention because I use exactly these two tools heavily. Before the integration, I have to define a different keybinding for switching window inside Emacs and i3. After the integration, I can use the same keybinding everywhere. Check out the video in the original blog post to see how cool it is!

I followed the blog post and implemented it immediately. It worked and I loved that! However, the proposed (i3 -> Emacs -> i3) solution introduced a coupling between i3 and Emacs. i3 has to know Emacs, and Emacs has to know i3. It does not fit into the Law of Demeter. It makes the solution harder to scale to other tiling window managers or applications.

Hence, I implemented another (i3 -> Emacs) solution with less coupling (only the focus command for now).

1: Add an Elisp function my/wm-integration

(require 'windmove)
(defun my/wm-integration (command)
  (pcase command
    ((rx bos "focus")
     (windmove-do-window-select
      (intern (elt (split-string command) 1))))
    (- (error command))))

my/wm-integration handles the command from the window manager.

Emacs does not need to know what the window manager is, and do not need to call i3-msg. It return a non-zero code if the command is not handled, or a zero code if handled.

nit. Emacs still use the protocol defined by i3, like the focus command. Well… we have to use a protocol anyway. Let’s pick the i3 protocol in this post.

2: Create a Script i3-msg-proxy

#!/bin/sh
#
# Proxy the `i3-msg` command to the focused window.

# Proxy to Emacs if it is the active window
if [[ "$(xdotool getactivewindow getwindowclassname)" == "Emacs" ]]; then
    command="(my/wm-integration \"$@\")"
    if emacsclient -e "$command"; then
        exit
    fi
fi

# fallback to i3
i3-msg $@

This script proxies the i3-msg command to the focused window. If the focused window has handled it (return zero), then exit. Otherwise (return non-zero), fallback to i3.

This mechanism (using the return code) can work with other applications, like Tmux.

3: Update the i3 config

bindsym $mod+Left  exec i3-msg-proxy focus left
bindsym $mod+Down  exec i3-msg-proxy focus down
bindsym $mod+Up    exec i3-msg-proxy focus up
bindsym $mod+Right exec i3-msg-proxy focus right

Finally, update the i3 config to proxy the command to the focused window. Now, I can move my focus between i3 and Emacs!

Conclusion

The key difference between this and the original solution is how Emacs calls i3 back when it failed to handle the command. The original solution calls i3-msg inside Emacs. This solution returns non-zero from Emacs.