Project-local Eat terminals with display-buffer-alist

:emacs: :eat:
Jun 03, 2026

Emacs gives you something new every day. A couple of weeks ago I stumbled across tab-bar-mode, which turned out to be a great fit for my workflow: one frame, with one tab per project. The only thing missing was a convenient terminal. I often need one for complex build steps or deployments, so I wanted a quick way to pop up a terminal for the current project and hide it again afterwards.

I found shell-pop, which seemed like a good fit and even works with eat. Unfortunately, it only manages one terminal at a time. That made it unsuitable for a tab-per-project workflow. But luckily, this is Emacs, so we can build the behavior ourselves.

Let me show you a small built-in solution using display-buffer-alist. Emacs provides display-buffer-alist, which controls how buffers are displayed: which window they use, where that window appears, and whether an existing window can be reused. The mechanism is very powerful, but also quite complex. For a deeper explanation, recommend Prot's article on display-buffer-alist.

For this use case, we only need two pieces: a display rule for eat-mode buffers and a small toggle function.

;; Show Eat terminals in a bottom side window, similar to in many modern IDEs
  (add-to-list 'display-buffer-alist
               '((major-mode . eat-mode)
                 (display-buffer-reuse-mode-window display-buffer-in-side-window)
                 (inhibit-same-window . nil)
                 (side . bottom)
                 (window-height . 0.3)
                 (mode . eat-mode)))

This rule makes eat buffers appear in a bottom side window, similar to many modern editors. The trade-off is that every eat buffer is now opened this way. The custom function below handles multiple project-local eat instances.

(defun my/toggle-eat ()
  "Toggle an Eat terminal using the display rules in `display-buffer-alist'."
  (interactive)
  (let* ((buf (get-buffer (project-prefixed-buffer-name "eat")))
         (win (and buf (get-buffer-window buf))))
    (if win
        (quit-window nil win)
      (let* ((buf (get-buffer (project-prefixed-buffer-name "eat"))))
        (if buf
            (pop-to-buffer buf)
          (eat-project))))))

Since each tab represents one project, the function looks for the project-local eat buffer name generated by project-prefixed-buffer-name. If that buffer is already visible, the function hides its window and buries the buffer. If the buffer exists but is not visible, it displays it again using the rules from display-buffer-alist. If no project-local terminal exists yet, it creates one with eat-project.

There are a few trade-offs:

  1. The display rule applies to all eat buffers, which may be annoying if you sometimes open terminals outside this workflow.
  2. It relies on eat and project agreeing on the same project-prefixed buffer name. This can break when the current buffer does not belong to a project.

Even with those drawbacks, this improves my current workflow: one frame, one tab per project, and a toggleable terminal for each project.