Andrey Listopadov

Making Emacs tabs look like in Atom

This is yet another follow-up post in the Emacs configuration series, which is also about Tabs. The 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 names take a lot of horizontal space, which can be a problem when dealing with buffers that were created by some package, like CIDER:

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

Tabs in Atom

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

By default tabs in Atom occupy more space than 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 a 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 built-in packages via defcustom because if someone else will use my config they will be able to customize such things with the 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 the 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 the window to fit full-sized tabs. In Emacs, we can access the width of a window with the 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 the number of tabs, we will calculate the width of the tab that we will need for all tabs that we currently have:

\[\text{window-tab-width}=\frac{\text{window-width}}{\text{amount-of-tabs}}\]

So, if our window is 300 chars wide, and we have one tab, it will occupy the 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 the left padding, one char to display the close button, and one char for the 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:

\[
\text{tab-width}=\left(\begin{cases}
\text{tab-line-tab-max-width}&\text{if window-tab-width}\geq\text{tab-line-tab-max-width}\\
\text{tab-line-tab-min-width}&\text{if window-tab-width}<\text{tab-line-tab-min-width}\\
\text{window-tab-width}&\text{otherwise}
\end{cases}\right)-\text{close-button-width}
\]

This way if we have a tab width larger than our maximum width, we will use maximum width. If we have a 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 the width that was calculated in the previous formula. Note that we’re subtracting the size of the close button from our width which is represented as × with spaces at the front and the end.

The second thing we need is to produce a valid name for the tab, but right now we will simply string-trim buffer name, and calculate its 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 the name for the tab and its paddings. First, we need to check if the trimmed buffer name exceeds the tab width. If it is, we truncate the name to the width of the tab minus 3, because we need to add single space padding before a name and add an ellipsis symbol followed by a 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 is needed when a 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 the small unused space on the right side of the window. Unfortunately, we can compute width only in terms 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 the 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 the close button if we want:

Rationale

Before:

After:

In the beginning, I’ve said that there are a lot of complaints 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 the 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. An 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 a 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 an Emacs user and is unfamiliar with the concept of buffer list, sits near me, since the concept of tabs is well known and obvious to most other people, because of tabs in browsers or other editors.

One thing that I have 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 the 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 the 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.