Making Emacs tabs look like in Atom
emacs emacs-lisp ~7 minutes read

This is yet another follow up post in Emacs configuration series, that is also about Tabs. Previous post was about how tabs behave when you close them, and how I think the algorithm can be improved. This post is more about visuals and horizontal space management.

By default tabs in Emacs are named after their respective buffers. This is how tabs look with my configurations and default naming:

Looks fine, but there’s a catch. Tabs with really long name take a lot of horizontal space, which can be problem wen dealing with buffers that were created by some package, like CIDER:

With tab of this size no wonder we get complains that tabs are not usable. And on this screenshot you can see that all tabs have different width, which doesn’t really look good in my opinion. Both problems can be fixed with some code, but first, let’s check how Atom editor handles tab sizes.

Tabs in Atom

This is default look of tabs in Atom editor with One Dark theme:

By default tabs in Atom occupy more space that is actually needed for a tab, and I think that this is beautiful. Someone may be concerned that it wastes horizontal space, but it’s actually not - if we open more tabs, their width will shrink to make those fit in the window:

But this not going to happen all the time, and eventually, once some minimum width is reached, tabs will no longer shrink, and we’ll have to scroll those. We still can get the idea of what file is opened in the tab, and there are a lot of them in such a tiny window. And if some tab has really long name that doesn’t fit into tab, it gets truncated:

This is the behavior we’re going to replicate in Emacs.

Making Emacs tabs resize automatically

First we will need two variables - one for minimum width, and one for maximum width. I like to define variables that extend some builtin packages via defcustom because if someone else will use my config they will be able to customize such things with custom interface or via code. Also Custom makes persistent configurations a bit easier as well.

(defcustom tab-line-tab-min-width 10
  "Minimum width of a tab in characters."
  :type 'integer
  :group 'tab-line)

(defcustom tab-line-tab-max-width 30
  "Maximum width of a tab in characters."
  :type 'integer
  :group 'tab-line)

Good. This will later be used in a function that creates a name for the tab. Now we need to think about general algorithm. Note that we’re defining size in terms of monospace characters. This algorithm will not work for variable pitch fonts.

In Atom tabs use fixed width up until there is no more horizontal space in window to fit full-sized tabs. In Emacs we can access width of a window with window-width function. So our first step will be to obtain such width, and check how many tabs will fit into it. However, instead of checking amount of tabs, we will calculate width of the tab that we will need to all tabs that we currently have:

So, if our window is 300 chars wide, and we have one tab, it will occupy whole width. If we have two tabs, each should be 150 chars long, and so on. This is great, but if we have 70 tabs, each would be only 4 chars. Given that we need at least one char to display left padding, one char to display close button, and one char for right padding, we’re left with 1 char for the name which is not good at all. However we don’t want to use such wide or such narrow tabs, so we need to adjust their length based on these conditions:

This way if we have tab width larger than our maximum width, we will use maximum width. If we have width that is less than our minimum width, we will use minimal width, and this will enable scrolling. If we’re in between, we’re using width that was calculated in previous formula. Note that we’re subtracting the size of close button from our width that is represented as × with spaces at the front and the end.

Second thing we need is to produce valid name for the tab, but right now we will simply string-trim buffer name, and calculate it’s width. This should leave us with these computations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
(defun aorst/tab-line-name-buffer (buffer &rest _buffers)
  (with-current-buffer buffer
    (let* ((window-width (window-width (get-buffer-window)))
           (close-button-size (if tab-line-close-button-show
                                  (length (substring-no-properties tab-line-close-button))
                                0))
           (tab-amount (length (tab-line-tabs-window-buffers)))
           (window-max-tab-width (/ window-width tab-amount))
           (tab-width (- (cond ((>= window-max-tab-width tab-line-tab-max-width)
                                tab-line-tab-max-width)
                               ((< window-max-tab-width tab-line-tab-min-width)
                                tab-line-tab-min-width)
                               (t window-max-tab-width))
                         close-button-size))
           (buffer-name (string-trim (buffer-name)))
           (name-width (length buffer-name)))

Now, when we have all this information, we can compute name for the tab and it’s paddings. First we need to check if trimmed buffer name exceeds tab width. If it is, we truncate name to the width of the tab minus 3, because we need to add single space padding before name, and add ellipsis symbol followed by space at the end.

17
18
(if (>= name-width (- tab-width 3))
    (concat  " " (truncate-string-to-width buffer-name (- tab-width 3)) "… ")

If name-width is less than tab-width minus 3, we can produce left padding, by computing difference between tab-width and name-width, and dividing it by 2. Then we concatenate this padding, and calculate the right padding needed for the name. This extra calculation needed when name is odd or even, so we produce equally sized tabs for any buffer name:

19
20
21
22
(let* ((padding (make-string (/ (- tab-width name-width) 2) ?\s))
       (buffer-name (concat padding buffer-name))
       (name-width (length buffer-name)))
  (concat buffer-name (make-string (- tab-width name-width) ?\s)))))))

This gives us such results. With the same window width as in Atom examples, two tabs will occupy this amount of space:

If we will open four tabs, the size of each tab will shrink accordingly:

Though this is not as nice as in atom, due to small unused space at the right side of the window. Unfortunately we can compute width only in term of characters, which can’t really have variable width, unless we use variable pitch font, but as I’ve mentioned earlier, this algorithm will not work with variable sized fonts, and perhaps such computation will be quite expensive. Computing variable width in terms of pixels may be possible, but I couldn’t find any information on this topic. But character based solution should also work fine in terminal Emacs.

When we open too many tabs, we can see scroll buttons, and tabs no longer shrink in size beyond tab-line-tab-min-width value. We also can disable close button if we want:

Rationale

Before:

After:

At the beginning I’ve said that there are a lot of complains that tabs unnecessarily waste horizontal space. Even though we’ve fixed this, now you may think, that this is still impractical, because you can’t see whole name of a file in the tab, and tabs are not usable as a concept overall. And I can agree to a certain point. Interactive buffer list, that we can fuzzy match through, has no issues with space because we pop it up when needed, and can show you full buffer names any time. But I find myself using tabs very often simply as a visual indicator of what buffers are next and previous in the list, and switch between those via C-x right and C-x left. You can’t really do this with buffer list, unless you display it somewhere all the time, but then it takes much more space than tabs.

Tabs also help a lot when someone, who isn’t Emacs user, and is unfamiliar with concept of buffer list, sits near me, since concept of tabs is well known and obvious for most other people, because of tabs in browsers or other editors.

One thing that I not yet succeeded to achieve is to automatically recalculate tab width on frame or window resize events. It seems that force-mode-line-update does not initiate the process of name building, but for some reason it happens on focus events, so I have yet to investigate this part.

Turns out there’s a way to update tab width on resize events by clearing cache of tab-line in each window. As a quite dirty hack, we can use this hook:

(add-hook 'window-configuration-change-hook
          #'(lambda ()
              (dolist (window (window-list))
                (set-window-parameter window 'tab-line-cache nil))))

This makes tabs resize whenever you change window or frame width with your mouse, or when you split windows via C-x 3 and other windowing commands that change window layout.

Though this post only mentions naming function, I’ve set up a bit more things to make tabs look as represented on screenshots. This configuration can be found in full form in my .dotfiles.