I used to use Emacs frames as workspaces. But reading through Emacs 28 NEWS reminded me that tab bars could be probably better used for the purpose. And indeed, using tab bars is simpler and looks like a very good fit.
First some initial settings:
(tab-bar-mode 1) (tab-bar-history-mode 1) (custom-set-variables '(tab-bar-show nil)) (tab-bar-rename-tab "emacs")
They enable the tab bar mode, tab bar history mode (not needed but useful) and hide the tab bar (not to waste screen space for no good purpose). We also want to give a static name to the initial tab.
Now we need to retrieve the tab names. I don’t know whether there is an official way to do it but the following functions serve the purpose:
(defun my-workspace-name () (alist-get 'name (assq 'current-tab (funcall tab-bar-tabs-function)))) (defun my-workspace-all-names () (mapcar #'(lambda (tab) (alist-get 'name tab)) (funcall tab-bar-tabs-function)))
Workspace switching is much simpler with tabs:
(defun my-workspace-switch (name) (interactive "sWorkspace: ") (if (member name (my-workspace-all-names)) (progn (tab-bar-switch-to-tab name) nil) (tab-bar-new-tab) (tab-bar-rename-tab name) t))
There is no need to track and handle frames anymore. Switching to the last workspace can be done simply with
The rest remains the same as previously with the frames:
(defvar my-workspaces ;; name ;; new-fn ;; refresh-fn '(("emacs" nil (lambda () (require 'bookmark) (unless bookmark-alist (bookmark-maybe-load-default-file)) (bookmark-jump "init.d"))) ("gnus" gnus (lambda () (let ((group-buffer (get-buffer "*Group*"))) (if group-buffer (switch-to-buffer group-buffer) (gnus) (cd "~"))))) ("irc" my-erc-connect nil) ("org" my-org-agenda my-org-agenda) ("roam" org-roam-find-file org-roam-find-file) ("system" my-run-eshell (lambda () (call-interactively (lambda () (interactive) (ido-buffer-internal ido-default-buffer-method nil nil nil "eshell:"))))))) (defun my-switch-to-workspace (&optional key) (interactive) (unless key (setq key (logand last-command-event 255))) (let* ((prefix (char-to-string key)) (current-name (my-workspace-name)) ;; This will be explained later: (predefined (cl-assoc prefix my-workspaces :test #'string-prefix-p))) (if predefined (cl-destructuring-bind (name new-fn refresh-fn) predefined (if (equal current-name name) (when refresh-fn (funcall refresh-fn)) (when (and (my-workspace-switch name) new-fn) (funcall new-fn)))) (let* ((candidates (remove current-name (cl-remove-if-not #'(lambda (n) (string-prefix-p prefix n)) (my-all-workspace-names)))) (n (length candidates))) (cond ((= n 0) (my-workspace-switch (read-from-minibuffer "Switch to workspace: "))) ((= n 1) (my-workspace-switch (car candidates))) (t (my-workspace-switch (completing-read "Switch to workspace: " candidates nil t prefix)))))))) (global-set-key (kbd "<s-return>") 'my-last-workspace) (dotimes (i 26) (global-set-key (kbd (format "s-%c" (+ ?a i))) 'my-switch-to-workspace)) (defun my-switch-to-workspace-key (key) (interactive "c") (my-switch-to-workspace key)) (global-set-key (kbd "C-c z") 'my-switch-to-workspace-key)
Of course, it’s possible to use tabs as workspaces directly, without these add-ons and with taking advantage of
tab-bar-tab-post-open-functions variable. Especially people who like using mouse for tab switching may like using the tab bar mode as it is. But I still like the bits of extra functionality for a bit more comfortable tab/workspace switching.