Tabspaces leverages tab-bar.el and project.el (both built into emacs 27+) to create buffer-isolated workspaces (or “tabspaces”) that also integrate with your version-controlled projects. It should work with emacs 27+. It is tested to work with a single frame workflow, but should work with multiple frames as well.
While other great packages exist for managing workspaces, such as bufler, perspective and persp-mode, this package is less complex than those alternatives, and works entirely based on the built-in (to emacs 27+) tab-bar and project packages. If you like simple, this may be the workspace package for you. That said, bufler, perspective or persp-mode, etc. may better fit your needs.
NOTE: version 1.2 renames several functions and streamlines tab and project creation. Apologies if this breaks your workflow. Please update your configuration accordingly.
Calling the minor-mode tabspaces-mode
sets up newly created tabs as
buffer-isolated workspaces using tab.el
in the background. Calling
tabspaces-mode
does not itself create a new tabbed workspace.
Switch or create workspace via tabspaces-switch-or-create-workspace
. Close a
workspace by invoking tabspaces-close-workspace
. Note that these two functions
are simply wrappers around native tab-bar
commands. You can close a workspace
and kill all buffers associated with it using
tabspaces-kill-buffers-close-workspace
.
Open an existing version-controlled project in its own workspace using
tabspaces-open-or-create-project-and-workspace
. If no such project exists it
will then create one in its own workspace for you.
See workspace buffers using tabspaces-switch-buffer
(for consult
integration see
below), which will only show buffers in the workspace (but list-buffers,
ibuffer, etc. will show all buffers). Setting
tabspaces-use-filtered-buffers-as-default
to t
remaps switch-to-buffer
to
tabspaces-switch-to-buffer
.
Adding buffers to a workspace is as simple as opening the buffer in
the workspace. Delete buffers from a workspace either by killing them or using
one of either tabspaces-remove-selected-buffer
or
tabspaces-remove-current-buffer
. Removed buffers are still available from the
default tabspace unless the variable tabspaces-remove-to-default
is set to nil
.
NOTE that other than tabbed buffer isolation for all created window tabs this
package does not modify tab-bar
, tab-line
, or project
in any way. It simply adds
convenience functions for use with those packages. So it is still up to the user
to configure tabs, etc., however they like.
Here are some screenshots of tabspaces (with my lambda-themes) and using consult-buffer
(see below for instructions on that setup). You can see the workspace isolated buffers in each and the tabs at top:
You may install this package either from Melpa (M-x package-install tabspaces
RET
) or by cloning this repo and adding it to your load-path.
Here’s one possible way of setting up the package using use-package (and straight, if you use that).
(use-package tabspaces
;; use this next line only if you also use straight, otherwise ignore it.
:straight (:type git :host github :repo "mclear-tools/tabspaces")
:hook (after-init . tabspaces-mode) ;; use this only if you want the minor-mode loaded at startup.
:commands (tabspaces-switch-or-create-workspace
tabspaces-open-or-create-project-and-workspace)
:custom
(tabspaces-use-filtered-buffers-as-default t)
(tabspaces-default-tab "Default")
(tabspaces-remove-to-default t)
(tabspaces-include-buffers '("*scratch*"))
(tabspaces-initialize-project-with-todo t)
(tabspaces-todo-file-name "project-todo.org")
;; sessions
(tabspaces-session t)
(tabspaces-session-auto-restore t)
(tab-bar-new-tab-choice "*scratch*"))
Note the inclusion of the `tab-bar` setting, which is built-in to Emacs and allows a number of different options for what buffer to set for a newly created tab.
Workspace Keybindings are defined in the following variable:
(defvar tabspaces-command-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "C") 'tabspaces-clear-buffers)
(define-key map (kbd "b") 'tabspaces-switch-to-buffer)
(define-key map (kbd "d") 'tabspaces-close-workspace)
(define-key map (kbd "k") 'tabspaces-kill-buffers-close-workspace)
(define-key map (kbd "o") 'tabspaces-open-or-create-project-and-workspace)
(define-key map (kbd "r") 'tabspaces-remove-current-buffer)
(define-key map (kbd "R") 'tabspaces-remove-selected-buffer)
(define-key map (kbd "s") 'tabspaces-switch-or-create-workspace)
(define-key map (kbd "t") 'tabspaces-switch-buffer-and-tab)
map)
"Keymap for tabspace/workspace commands after `tabspaces-keymap-prefix'.")
The variable tabspaces-keymap-prefix
sets a key prefix (default is C-c TAB
) for
the keymap, but this can be changed to anything the user prefers.
When tabspaces-mode
is enabled use tabspaces-switch-to-buffer
to choose from a
filtered list of only those buffers in the current tab/workspace. Though nil
by
default, when tabspaces-use-filtered-buffers-as-default
is set to t
and
tabspaces-mode
is enabled, switch-to-buffer
is globally remapped to
tabspaces-switch-to-buffer
, and thus only shows those buffers in the current
workspace. For use with consult-buffer
, see below.
Sometimes the user may wish to switch to some open buffer in a tabspace and switch to that tab as well. Use (=tabspaces-switch-buffer-and-tab
) to achieve this. If the buffer is open in more than one tabspace the user will be prompted to choose which tab to switch to. If there is no such buffer user will be prompted on whether to create it in a new tabspace or the current one.
The tabspaces-open-or-create-project-and-workspace
function provides a
versatile way to manage projects and their associated workspaces in
Emacs. Here’s what you can do with it:
- Open Existing Projects: Open an existing version-controlled project in its own workspace. The function will switch to the project’s tab if it already exists.
- Create New Projects: If no such project exists at the specified path, it will create one in its own workspace for you, initializing version control (git or other VCS) in the process.
- Descriptive Tab Naming:
- Tabs are named descriptively based on the project structure.
- In case of naming conflicts, it intelligently renames tabs to avoid confusion.
- Multiple Tabs for the Same Project:
- By using a universal argument (C-u) before calling the function, you can force the creation of a new tab even for already open project tabs.
- The first tab will have the original project name.
- Subsequent tabs will be automatically named with incrementing numbers (e.g., “ProjectName<2>”, “ProjectName<3>”).
- This is useful when you want to work on different aspects of the same project in separate workspaces.
Rudimentary support for saving tabspaces across sessions has been implemented.
Setting tabspaces-session
to t
ensures that all open tabspaces and file-visiting
buffers are saved. These may either be restored interactively via
(tabspaces-restore-session)
, non-interactively via
(tabspaces--restore-session-on-startup)
, or they can be automatically opened
when (tabspaces-mode)
is activated if tabspaces-session-auto-restore
is set to
t
. In addition, a particular project tabspace may be saved via
(tabspaces-save-current-project-session)
, and restored when the project is opened via (tabspaces-open-or-create-project-and-workspace)
.
If you have consult installed you might want to implement the following in your
config to have workspace buffers in consult-buffer
:
;; Filter Buffers for Consult-Buffer
(with-eval-after-load 'consult
;; hide full buffer list (still available with "b" prefix)
(consult-customize consult--source-buffer :hidden t :default nil)
;; set consult-workspace buffer list
(defvar consult--source-workspace
(list :name "Workspace Buffers"
:narrow ?w
:history 'buffer-name-history
:category 'buffer
:state #'consult--buffer-state
:default t
:items (lambda () (consult--buffer-query
:predicate #'tabspaces--local-buffer-p
:sort 'visibility
:as #'buffer-name)))
"Set workspace buffer list for consult-buffer.")
(add-to-list 'consult-buffer-sources 'consult--source-workspace))
This should seamlessly integrate workspace buffers into consult-buffer
,
displaying workspace buffers by default and all buffers when narrowing using
“b”. Note that you can also see all project related buffers and files just by
narrowing with “p” in a default consult setup.
NOTE: If you typically toggle between having tabspaces-mode
active and inactive,
you may want to also include a hook function to turn off the
consult--source-workspace
above and modify the visibility of
consult--source-buffer
. You can do that with something like the following:
(defun my--consult-tabspaces ()
"Deactivate isolated buffers when not using tabspaces."
(require 'consult)
(cond (tabspaces-mode
;; hide full buffer list (still available with "b")
(consult-customize consult--source-buffer :hidden t :default nil)
(add-to-list 'consult-buffer-sources 'consult--source-workspace))
(t
;; reset consult-buffer to show all buffers
(consult-customize consult--source-buffer :hidden nil :default t)
(setq consult-buffer-sources (remove #'consult--source-workspace consult-buffer-sources)))))
(add-hook 'tabspaces-mode-hook #'my--consult-tabspaces)
If you use ivy you can use this function to limit your buffer search to only those in the tabspace.
(defun tabspaces-ivy-switch-buffer (buffer)
"Display the local buffer BUFFER in the selected window.
This is the frame/tab-local equivilant to `switch-to-buffer'."
(interactive
(list
(let ((blst (mapcar #'buffer-name (tabspaces-buffer-list))))
(read-buffer
"Switch to local buffer: " blst nil
(lambda (b) (member (if (stringp b) b (car b)) blst))))))
(ivy-switch-buffer buffer))
Alternatively, you may use the following function, which is basically a clone of ivy-switch-buffer
(and thus uses ivy’s own implementation framework), but with an additional predicate that only allows showing buffers from the current tabspace.
(defun tabspaces-ivy-switch-buffer ()
"Switch to another buffer in the current tabspace."
(interactive)
(ivy-read "Switch to buffer: " #'internal-complete-buffer
:predicate (when (tabspaces--current-tab-name)
(let ((local-buffers (tabspaces--buffer-list)))
(lambda (name-and-buffer)
(member (cdr name-and-buffer) local-buffers))))
:keymap ivy-switch-buffer-map
:preselect (buffer-name (other-buffer (current-buffer)))
:action #'ivy--switch-buffer-action
:matcher #'ivy--switch-buffer-matcher
:caller 'ivy-switch-buffer))
By default the *scratch*
buffer is included in all workspaces. You can modify
which buffers are included by default by changing the value of
tabspaces-include-buffers
.
If you want emacs to startup with a set of initial buffers in a workspace (something I find works well) you could do something like the following:
(defun my--tabspace-setup ()
"Set up tabspace at startup."
;; Add *Messages* and *splash* to Tab \`Home\'
(tabspaces-mode 1)
(progn
(tab-bar-rename-tab "Home")
(when (get-buffer "*Messages*")
(set-frame-parameter nil
'buffer-list
(cons (get-buffer "*Messages*")
(frame-parameter nil 'buffer-list))))
(when (get-buffer "*splash*")
(set-frame-parameter nil
'buffer-list
(cons (get-buffer "*splash*")
(frame-parameter nil 'buffer-list))))))
(add-hook 'after-init-hook #'my--tabspace-setup)
By default Tabspaces will create a project-todo.org
file at the root of the project
when creating a new workspace using tabspaces-open-or-create-project-and-workspace
.
Use tabspaces-todo-file-name
to change the name of that file, or tabspaces-initialize-project-with-todo
to disable this feature completely.
Code for this package is derived from, or inspired by, a variety of sources. These include:
- The original buffer filter function
- Buffer filtering and removal
- Consult integration