Giving (emacs-)purpose to i3wm

One fine day, imagine you're modifying a ruby file in Emacs. Suddenly, you wanna open a REPL, run some tests or make some magit operations. The problem is that all those buffers tend to compete with each other to a space in your window; they keep overlapping each other. This is truly a problem that Emacs users face: organizing the window layout.

Whit this post, I'm going to show how the awesome tool Purpose can help you overcome this difficulty. The approach is to have multiple responsibilities (git, code, shell, tests, notes) each one with their own GUI window. Purpose will not let those windows overstep each other and, with the addition of a tiling window manager, you won't have the overhead of switching among them.

What is Purpose?

Managing the window layout with Purpose is easy but may take a little time to be tamed. Basically, it gives us two ways:

  • you designate a window to only accept buffers with a certain major mode; any other major mode will be opened on another window
  • you register a single buffer to a window regardless of the mode and that buffer won't ever leave that window.

To explain better what we're trying to solve, several animations will be presented. Let's use the example of trying to manage a regular ruby code and a pry REPL session at the same time.

Just some contextualization of the elements of the pictures:

  • lower left: name of the buffer
  • lower center: major mode
  • lower right: name of purpose
  • black rectangle: the cursor

without-purpose.gif

Figure 1: Notice how, by default, Emacs doesn't really care about the meaning of a buffer and will overwrite it without any consideration. Where the cursor is, Emacs will open the new buffer.

Our buffers have the general name in the beginning. To give some context to them, we have to declare the desired behavior in the configuration:

(add-to-list 'purpose-user-mode-purposes '(ruby-mode . ruby))
(add-to-list 'purpose-user-name-purposes '(comint-mode . terminal))
;; Populate Purpose data structure
(purpose-compile-user-configuration)

Surprisingly, with only this customization, we can see Purpose changing the behavior of window management.

without-configuration.gif

Figure 2: When opening a new ruby code buffer, Purpose will try to reuse a window if they share the same purpose.

Now, imagine that you wanna only open the ruby code in the left window no matter the circumstance. You have to be more explicit and assign the window to the ruby purpose.

with-window-purpose.gif

Figure 3: Only ruby purpose is allowed in the left window. In the second step you dedicate the window to the purpose indicated as !

There's also the restrictive option to dedicate the buffer to the window.

with-buffer-purpose.gif

Figure 4: The buffer a.rb will be always in the left window. The buffer dedication is is indicated with #

These animations were the foundation of what Purpose brings to the table. But, since the idea is to use a tiling window manager and multiple GUI windows, we want to have multiple frames each one with their own purpose.

two-frames-problem.gif

Figure 5: That's not what we were expecting. The b.rb should be opened inside Frame 1.

That went wrong because we have to add the corresponding regex in the display-buffer-alist. If we don't do that, Purpose won't consider opening the buffer in another frame.

(add-to-list 'display-buffer-alist
             `("\\.rb\\'"
               nil
               (reusable-frames . t)))

two-frames-okay.gif

Figure 6: Now we're talking. The b.rb is opened in frame 1 without removing the REPL buffer from the frame.

If you're curious about more options and its internals, you could see the awesome wiki of the project.

Improving your workflow

Now that we have a notion of how Purpose works, we can extend it to bring multiple frames to the table.

three-frames.gif

Figure 7: Look how it automatically jumps to the window without interfering with each other. We can have multiple buffers with the same purpose opening inside the same frame.

This setup demands that you open a lot of GUI windows and this can be a disadvantage when you're using a floating window manager, because the single way to move among windows is with ~alt-tab.

When you introduce a tiling window manager to your life, you realize that alt-tab was, in fact, your enemy the whole time. With a clean and flexible way to organize your windows, you can be really productive assigning each different GUI window to a new keybinding.

I can't really say for other window managers, but i3wm separates the GUI windows in workspaces. You can have as many as you want and it's common to assign workspaces as numbers. So, if you press Super-x, it'll show you the x window.

Let me suggest a possible setup with these workspaces:

  • 1st: free (without dedication)
  • 2nd: ruby editor
  • 3rd: browser
  • 4th: terminal
  • 5th: email
  • 6th: notes
  • 7th: magit
  • 8th: compilation or test status
  • 9th: elfeed

For each one of them, we dedicate the corresponding purpose to each frame. Every buffer that doesn't fit into any purpose will be opened inside the first frame since it's not dedicated to any purpose.

I wish that by now you learned the principle and will be able to choose the setup that's most suitable for you.

Open all frames automatically

Opening all these Emacs frames and assign each one to the correct workspace is a manual and repetitive task. We need an automatic mechanism to launch and position all these frames in each correct workspace within i3wm.

Defining the frames

I'll use only the Magit workspace, but it can be extended later easily with your preferences. This snippet basically creates all desired frames and configures them with Purpose. This is omitted for brevity but you can find the complete implementation here.

(setq zezin-frames
         ;; title of GUI window
      '(((title . "Emacs - Primary"))

        ((title . "Emacs - Git") 
         ;; function that will be executed when this frame starts
         (start-fn . zezin-start-magit-frame))))

(use-package window-purpose
  :config
  (progn
    (purpose-mode)

    ;; some context to Purpose
    (add-to-list 'purpose-user-mode-purposes '(ruby-mode . ruby))
    (add-to-list 'purpose-user-regexp-purposes '("^\\*magit\\*" . magit))

    ;; make magit buffers frame-aware
    (add-to-list 'display-buffer-alist
                 `("\\*magit*"
                   nil
                   (reusable-frames . t)))

    (purpose-compile-user-configuration)))

(defun zezin-start-magit-frame (frame)
  ;; this buffer will have the magit purpose 
  ;; because we use regex to identify the purpose
  (switch-to-buffer (get-buffer-create "*magit: purpose"))
  ;; dedicate this purpose to this window
  (purpose-toggle-window-purpose-dedicated))

(defun zezin-start-frames ()
  (interactive)
;; zezin-make-new-frame checks if there's an existing frame
;; if there isn't, it creates a new one from zezin-frames elements
  (-each zezin-frames 'zezin-make-new-frame))

;; hook that's executed every time there's a new frame
(add-hook 'after-make-frame-functions
          (lambda (frame)
            (let* ((title (zezin-frame-title frame))
                   (start-fn (zezin-find-start-fn title)))
              (when start-fn
                (select-frame frame)
                (funcall start-fn frame)))))

Call it from anywhere

We have the function zezin-start-frames, but we can only invoke it inside Emacs. To open all these frames in any place, we can create a .desktop file that can be called by any launcher. I use Albert by the way, but it could really be any launcher. In the end, it won't make any difference.

# Save it in ~/.local/share/applications/emacssetup.desktop
[Desktop Entry]
Name=Emacs Setup
Comment=Spawn specific Emacs instances
# Emacs daemon is required for this
Exec=emacsclient -c -e "(zezin-start-frames)"
Icon=emacs

Position the frames automatically

Now we are opening all the frames specified in our list, but i3wm positions all of them in the same workspace. We can use title of the frame to position each frame in the desired workspace.

assign [title="Emacs - Primary"] 1
assign [title="Emacs - Git"] 2

Now, after the Emacs Setup desktop entry is called from our launcher application, we can press Super+2 and always find the Magit frame in this workspace. If we call magit-status for example, it'll automatically always open the new buffer in 2nd workspace.

Final thoughts

I tried to present here a different way to position your buffers with Emacs. I'm using it for a few months and it's been great so far. Also, if you think having multiple frames is troublesome, give at least Purpose a try. It's really worth it.

Thanks for reading.