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
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
(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
long, and so on. This is great, but if we have 70 tabs, each would be
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:
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
3, because we need to add single space padding before name, and
add ellipsis symbol followed by space at the end.
name-width is less than
3, we can produce left
padding, by computing difference between
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
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:
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
(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.