Permanently display workspaces *in the tab-bar*

We have Permanently display workspaces in minibuffer already, but we can also hijack the tab-bar to have a truly permanent display of workspaces without encumbering the minibuffer. Note that this removes any display of Emacs’ native tabs. Personally, I find them unnecessary given that we have workspaces already.

(after! persp-mode
  ;; alternative, non-fancy version which only centers the output of +workspace--tabline
  (defun workspaces-formatted ()
    (+doom-dashboard--center (frame-width) (+workspace--tabline)))

  (defun hy/invisible-current-workspace ()
    "The tab bar doesn't update when only faces change (i.e. the
current workspace), so we invisibly print the current workspace
name as well to trigger updates"
    (propertize (safe-persp-name (get-current-persp)) 'invisible t))

  (customize-set-variable 'tab-bar-format '(workspaces-formatted tab-bar-format-align-right hy/invisible-current-workspace))

  ;; don't show current workspaces when we switch, since we always see them
  (advice-add #'+workspace/display :override #'ignore)
  ;; same for renaming and deleting (and saving, but oh well)
  (advice-add #'+workspace-message :override #'ignore))

;; need to run this later for it to not break frame size for some reason
(run-at-time nil nil (cmd! (tab-bar-mode +1)))

We can also get a bit fancier and change faces and the workspace format, yielding a tab-bar like this:

(custom-set-faces!
  '(+workspace-tab-face :inherit default :family "Jost" :height 135)
  '(+workspace-tab-selected-face :inherit (highlight +workspace-tab-face)))

(after! persp-mode
  (defun workspaces-formatted ()
    ;; fancy version as in screenshot
    (+doom-dashboard--center (frame-width)
                             (let ((names (or persp-names-cache nil))
                                   (current-name (safe-persp-name (get-current-persp))))
                               (mapconcat
                                #'identity
                                (cl-loop for name in names
                                         for i to (length names)
                                         collect
                                         (concat (propertize (format " %d" (1+ i)) 'face
                                                             `(:inherit ,(if (equal current-name name)
                                                                             '+workspace-tab-selected-face
                                                                           '+workspace-tab-face)
                                                               :weight bold))
                                                 (propertize (format " %s " name) 'face
                                                             (if (equal current-name name)
                                                                 '+workspace-tab-selected-face
                                                               '+workspace-tab-face))))
                                " "))))
;; other persp-mode related configuration
)

1 Like

This looks good, adapted for my private config & using perspectives.el rather than persp-mode (extremely minimal changes)

(declare-function "persp-names" 'perspective)

(defgroup lkn-tab-bar nil
  "Options related to my custom tab-bar."
  :group 'tab-bar)

(defgroup lkn-tab-bar-faces nil
  "Faces for the tab-bar."
  :group 'lkn-tab-bar)

(defface lkn-tab-bar-workspace-tab
  '((t :inherit default))
  "Face for a workspace tab."
  :group 'lkn-tab-bar-faces)

(defface lkn-tab-bar-selected-workspace-tab
  '((t :inherit (highlight lkn-tab-bar-workspace-tab)))
  "Face for a selected workspace tab."
  :group 'lkn-tab-bar-faces)

(defun lkn-tab-bar--workspaces ()
  "Return a list of the current workspaces."
  (let ((persps (persp-names))
	(persp (persp-current-name)))
    (seq-reduce
     (lambda (acc elm)
       (let ((face (if (equal persp elm)
		       'lkn-tab-bar-selected-workspace-tab
		     'lkn-tab-bar-workspace-tab)))
	 (concat
	  acc
	  (concat
	   (propertize (format " %d" (1+ (cl-position elm persps)))
		       'face
		       `(:inherit ,face
			 :weight bold))
	   (propertize (format " %s " elm)
		       'face `,face)))))
     persps
     `(,(propertize (persp-current-name) 'invisible t))))))

(customize-set-variable 'global-mode-string '((:eval (lkn-tab-bar--workspaces)) " "))
(customize-set-variable 'tab-bar-format '(tab-bar-format-global))
(customize-set-variable 'tab-bar-mode t)

This is lovely, and I have been using it for a while. One issue: while I don’t usually use the mouse for anything, it’s occasionally a nice option for a quick, arbitrary navigation action like “click on a tab bar tab” (especially when I’m using a mac, with their lovely trackpads), and unlike the default tab bar contents, neither of these tab bar definitions do anything when a tab is clicked.

Unfortunately, the relevant APIs and their documentation feel pretty opaque to me, so I haven’t been able to sort this out in the time that I am willing to allocate for such a niche (to me) workflow. But it does seem like a pretty central tab bar use case, so worth calling out (plus, there’s a small chance some kind soul has already figured this out). Meanwhile, I’ll keep poking at it occasionally; here’s hoping I find my way to some small breakthrough.

Saw this very late (notifications eh…) but I have a solution for this. An incomplete solution but one nonetheless.

So the TL;DR here is tab-bar-mode functions slightly different to how other things work. It’s only expected to work with tabs, so all the click logic is hard-coded to be for tabs.

In order to combat this, we have to write a generic handler (which to me screams for a defgeneric solution) to handle the “click event” on whatever you clicked. Using the below snippet like emacs -Q -l tabs.el, you get another item in the tab-bar which when clicked, gives a different message in the modeline vs clicking the tab

(defun test-tab-click-handler (evt)
  "Function to handle clicks on the custom tab."
  (interactive "e")
  (message "Hi" evt))

(keymap-set tab-bar-map "<mouse-1>" 'test-tab-click-handler)

(defun test-tab-item ()
  (propertize "Click Me"
              'mouse-face 'highlight))

(setq tab-bar-format (cons 'test-tab-item tab-bar-format))

(tab-bar-mode 1)

The same would also have to be done for the other click events and things like touchscreens. I’ll reply back here when I have a complete snippet, but if either of you do so in the meantime feel free to do the same :smile:

Okay, we got there…

To save me constantly updating this, the config can be found here

Pasting the whole module config to show how I did it, but because each of these “tabs” are rendered as a string, we have to compute the pixel width of each tab and set that as a property on the tab.

Once we have that, the click handler becomes a simple case of given the x position of the click; filter out any “tabs” that have a right edge less than that and we take the car of that. This gives us the tab we clicked on; or nil if we clicked elsewhere (thanks to when-let* we get no-op for free).

For the other click events, you would look at tab-bar--event-to-item to figure out which click event it was then dispatch with pcase/cond/etc

1 Like