Use tab-bar for Emacs 27.1+ workspace management


As stated on Discord, I am toying with the idea of using tab-bar-mode and Bufler to handle the workspaces instead of persp-mode.

Target feature list :

  • bufler to handle buffer grouping
  • desktop for session persistence
  • bookmark+ to have multiple desktop configurations to jump to and autoload them on project entry

The short snippet (just for bufler) is :

(use-package bufler
  :hook ((after-init . bufler-tabs-mode) ; Use tab-line and tab-bar to display bufler info
         (after-init . bufler-workspace-mode)) ; Set the frame name to the workspace name
  ;; TODO: disable tab-{bar,line}-mode in Dashboard
  ;; TODO: disable tab-{bar,line}-mode in most popup buffers and Dashboard
  ;; TODO: disable tab-{bar,line}-mode in Company childframes
  (setq tab-bar-show 1)
  ;; Remove the tab line it is confusing
  (global-tab-line-mode -1)
  (tab-line-mode -1)

  (setf bufler-groups

My main issue for now seems to be that tab-bar-mode applies to all frames

To toggle the use of tab bars, type M-x tab-bar-mode. This command applies to all frames, including frames yet to be created. To control the use of tab bars at startup, customize the variable tab-bar-mode .

It means that the frame that company-box opens has a tab-bar. Since ivy doesn’t do that, it’s probably a frame-parameter to set


I like the idea (I especially like any excuse to get rid of persp-mode), though I despise how it automatically groups buffers. I much prefer buffers be added to the current group when they are interactively switched to (on doom-switch-buffer-hook). Otherwise it defeats the original purpose of our workspaces; to act like desktop manager workspaces.

Perhaps burly.el would be a better option. Desktop.el can be brittle, and burly handles a couple edge cases (with indirect buffers) out of the box.

Perhaps this will work around it:

(add-hook! 'after-make-frame-functions
  (defun remove-tab-bar-from-childframe (frame)
    (when (frame-parameter frame 'parent-frame)
      (set-frame-parameter frame 'tab-bar-lines 0))))

company-box persists its child frame, so this has to take effect before it is created.

1 Like

I didn’t find tab-bar-lines in the manual. Indeed that is what Posframe does

So fixing it for company-box is just a matter of customizing company-box-frame-parameters and the fix can eventually make it upstream in the default value.

Bufler both handles buffer grouping, and managing the tabs in a different way (selecting a tab doesn’t change the window configuration, but instead sets the default filters for finding buffers basically); since both are not really aligned with what you see for :ui workspace it might be a hard sell.

There is support for arbitrary grouping as far as I understand but I couldn’t make it work yet

It took me longer than I thought, but the crude version of using tabs as workspace can be seen there

Basically, bufler is still used to group the buffers, but the automatic categories are only used for the special buffers (to avoid special buffers moving around workspaces). Otherwise, the grouping is done by doom-project-name and the tab gets the same name as well.

It should be possible to get rid of persp-mode in 27.

There are a lot of things to add still (handling “main” in a smart way, not opening duplicate tabs if we open the same project, and per-project window configuration persistence mostly), but hopefully redoing this almost from scratch will also reduce the number of hacks to use

I fixed the 2 blocking issues I had with Bufler and Burly in my forks (and I’ll try to get the changes merged once I’m confident I can avoid wasting alphapapa’s time like I’ve already done).

  • Burly : give a predicate list to ignore frames on save, like childframes
  • Bufler: allow a buffer to be in multiple named workspaces

Now I’m thinking about implementing a tab-based version of all the +workspaces/ API, do you still think that the best way to do this would be to use generic functions ? I’m not really sure what I should aim for.

Currently I’m thinking of trying this kind of thing, as mentioned in Using advices for core lookup and completion functions? conversation:

(cl-defgeneric doom--ui-workspaces--rename (_impl _name _new-name)
  "Rename the current workspace named NAME to NEW-NAME. Returns old name on
success, nil otherwise."

  (:method ((impl (eql 'persp)) name new-name)
   (when (doom--ui-workspaces--protected-p impl name)
     (error "Can't rename '%s' workspace" name))
   (persp-rename new-name (doom--ui-workspaces--get impl name)))

  (:method ((impl (eql 'tabs)) name new-name)
   (when (doom--ui-workspaces--protected-p impl name)
     (error "Can't rename '%s' workspace" name))
   (tab-rename new-name (tab-bar--tab-index-by-name name))))

(defvar doom-workspaces-implementation 'tabs
  "Implementation to use for the :ui workspaces")

(defun +workspace-rename (name new-name)
  "Rename workspace NAME to NEW-NAME."
  (interactive ...)
  (doom--ui-workspaces--rename doom-workspaces-implementation name new-name))

(map! :leader :prefix "[TAB]" "R" #'+workspace-rename)

The main issue with this approach is that methods cannot be interactive, so a wrapper is necessary to inject the correct (interactive) call.


Functions defined using cl-defmethod cannot be made interactive, i.e. commands (see Defining Commands), by adding the interactive form to them. If you need a polymorphic command, we recommend defining a normal command that calls a polymorphic function defined via cl-defgeneric and cl-defmethod.

Do you still think about going generic functions to reduce “code adherence” in core ?

1 Like

There are news on that front. Tl;dr : Emacs 27 is probably not going to have enough features, and 28 probably won’t either.

Adam “alphapapa” Porter has been taking a deeper look on using tab-bar.el to show the state of his packages Burly and Bufler, and it looks like setting arbitrary data to tabs (which is basically mandatory to associate a workspace with a native tab) is not well supported as of 2021-09-20. We are really close to emacs 28 feature freeze and branch cut, so I’m not sure it’s going to make it to 28.

I’m really looking forward to him finishing Taxy and its integration in Bufler and/or Burly to start testing it and giving Adam feedback


A new candidate is in town. tabspaces is a workspace implementation using project.el and tab-bar.

It would probably struggle with adding outsider files to a workspace when opened with find-file, but I suspect it does most of what I want

(emacs-workroom)[] seems to be in the same vein at first glance and also pretty new.

1 Like