Using ibuffer for custom buffer list commands

Published 2026-11-05

tag(s): #overblown-minor-annoyances #emacs

At work I have instances of Claude running in different repositories, many times for the same overarching task. For example: "load info from flat file in DB1, run transformation and store in DB2, update output step to include new data, enable output feed for a client". These happen in 3 or 4 different repos, depending on specifics. But the branch names are all the same, since they tie to the same user story.

I have a keymap for agent-shell commands, in which I included a little command to pick another agent buffer using completing-read:[1]


;; init.el
(defun hoagie-switch-to-agent-shell ()
  "Switch to an agent-shell-mode buffer."
  (interactive)
  (switch-to-buffer
   (completing-read
    "Switch to Agent: "
    (mapcar #'buffer-name
            (seq-filter (lambda (b)
                          (eq (buffer-local-value 'major-mode b)
                              'agent-shell-mode))
                        (buffer-list)))
    nil t)))

;; user-lisp/bindings.el
(defvar-keymap hoagie-agent-shell-keymap
  :doc "Keymap for agent-shell."
  :name "Agent Shell"
  "t" '("transient" . agent-shell-help-menu)
  "b" '("switch to buffer" . hoagie-switch-to-agent-shell)
  "p" '("previous-permission" . agent-shell-previous-permission-button)
  "a" '("DWIM" . agent-shell))
(keymap-set hoagie-shortcut-keymap "a" hoagie-agent-shell-keymap)
(keymap-global-set "C-c a" hoagie-agent-shell-keymap)
    

This worked OK, except that the buffer names are all too similar: I ended up always requesting completion just to pop the list to tell them apart. Which made me think I had to adjust something, the point of having a command for quick switching, over just listing all buffers, was to make things quicker.
I thought of two ways to solve this: uniquify buffer names more, or give up and display the buffer list, and then filter by mode or name prefix.
An iterating on the second idea, what if I found a way to filter the buffer list before popping it? 👀

I started checking out list-buffers, which happens to be a simple wrapper over list-buffers-noselect. It is easy to filter the list, but it is impossible to change the list format[2].
Then I remembered that a while back I switched to ibuffer because it is easier to sort the buffer list by mode, and filter by name, with it. And looking at the function signature for the command, everything I needed was right there! And it was lot simpler than I expected:


(defun hoagie-list-buffers-agent-shell ()
  "List agent-shell-mode buffers."
  (interactive)
  (ibuffer nil "*agent-shell-buffers*" '((mode . agent-shell-mode))
           nil nil nil '((" " (name 50) " "))))
    

The first parameter is whether to pop the list in another window (nope!), then the buffer name, a list of filters (in this case, only major mode), and bunch of other parameters not relevant for this case (so, no to all). Finally the last parameter, very important, is the format of the buffer list. Which lets me trivially show only the buffer name, and make the column very wide.
Now this command is assigned to b in the agent-shell keymap. Early usage tells me this isn't slower than what I used to do, and I switch to the wrong agent less often. Win-win. Well, more like, even-win, right?
And because whenever you find a new toy, you inevitably think that it might be good solution in other cases, I added a similar command to list all open chats in rcirc:


(defun hoagie-rcirc-list-buffers ()
  "List rcirc-mode buffers."
  (interactive)
  (ibuffer nil "*rcirc-buffers*" '((mode . rcirc-mode)) nil nil nil
           '((" " (name 50) " "))))
;; clobbers rcirc-browse-url
(keymap-set rcirc-mode-map "C-c C-b" #'hoagie-rcirc-list-buffers)
    
Footnotes
  1. For more details on using simple keymaps as menus, see this older post.
    Wow, that post is old enough that I was still on use-package!
  2. Well, nothing in Emacs is impossible. I could use advice or hooks or something to change the list format, I bet.

Share your thoughts (via email)

Back to top

Back to homepage