Reverse Video Tabline in Neovim

I am not a big user of the tabline in Neovim, but when i occasionally reach for it, I want it to look like the user interfaces of the rest of my tools, namely a (dark) foreground color on a (light) background color with reverse video for highlight. For my Neovim setup, this means that the windows show the text in the buffers using whatever dark color my terminal has as its foreground color on whatever light color my terminal has as its background color. The same goes for the command line. The status lines, on the other hand, show reverse video.

To stay in line with this look and to make the tabline stand out from the editing window, I want my whole tabline, save for the active tab label, to be displayed in reverse video. To make the active tab stand out from the rest of the tabline, in turn, I want it to be displayed in normal video.

Furthermore, since I find that the default tab labels can be a bit messy, I want the labels to be a single number, where the label of the first tab is simply 1, the label of the second tab is 2 and so on. According to the Neovim docs this isn't easy. However, the hard part about defining my preferred way to tab pages is not in the tabline option. The provided example is clear an easy to follow. Rather, the hard part is in getting the highlights to work. Let us have a look!

But first, here is what Neovim looks like when using an empty configuration file and running the command vi -p foo bar baz /usr/local/share/nvim/runtime/filetype.lua , where vi is my alias for nvim. I added the last file to show what a label with a long path name looks like in the default tabline.

A terminal showing neovim with four tabs
Neovim with an empty configuration file.

First naive attempts at highlighting

Let me start with configuring the highlight groups:


        highlight TabLine cterm=reverse
        highlight clear TabLineSel
        highlight TabLineFill cterm=reverse
        

The second line is not strictly necessary, but I include it to be explicit. It could also be set to cterm=NONE. As far as highlighting goes, the result is exactly what I want:

A terminal showing neovim with four tabs
Neovim with highlight groups for the default tabline.

Let me then use the provided example functions from the documentation as a starting point for my desired tab labels.


        highlight TabLine cterm=reverse
        highlight clear TabLineSel
        highlight TabLineFill cterm=reverse

        set tabline=%!MyTabLine()

        function MyTabLine()
          let s = ''
          for i in range(tabpagenr('$'))
            " select the highlighting
            if i + 1 == tabpagenr()
              let s ..= '%#TabLineSel#'
            else
              let s ..= '%#TabLine#'
            endif

            " set the tab page number (for mouse clicks)
            let s ..= '%' .. (i + 1) .. 'T'

            " the label is made by MyTabLabel()
            let s ..= ' %{MyTabLabel(' .. (i + 1) .. ')} '
          endfor

          " after the last tab page fill with TabLineFill and reset tab page nr
          let s ..= '%#TabLineFill#%T'

          " right-align the label to close the current tab page
          if tabpagenr('$') > 1
            let s ..= '%=%#TabLine#%999Xclose'
          endif

          return s
        endfunction

        function MyTabLabel(n)
          let buflist = tabpagebuflist(a:n)
          let winnr = tabpagewinnr(a:n)
          return bufname(buflist[winnr - 1])
        endfunction
        

Strangely, adding these functions and setting the tabline causes the whole tabline to become reversed:

A terminal showing nvim with four tabs
Neovim with highlight groups for the tabline of the example tabline configuration.

The problem

Having experimented a bit with this, trying out different configurations for both cterm and gui written in both Vimscript and Lua, I have come to the conclusion that the attributes of TabLineFill are established before the tabline, that they will remain in effect for all elements of the tabline, and that the they cannot be changed in the tabline.

This, however, does only apply when using a custom tabline. When using the default tabline (by letting the tabline option be empty), the attributes are working as expected. To see this in action, let me change the setting for the inactive labels from reversed to blue text on yellow background, still using the tabline example from the Neovim documentation and still having TabLineFill set to reverse.


        highlight TabLine ctermbg=yellow ctermfg=blue
        
A terminal showing neovim with four tabs
Neovim displaying inactive tab labels as yellow text on blue background using the colors of TabLine, with the reverse attribute of TabLineFill.

Since TabLine has no reverse attribute, one would expect blue text one yellow background for the inactive tabs, and one would certainly expect normal video for the active tab. This is indeed how it works when the same highlight settings are used with the default (empty) tabline option:

A terminal showing neovim with four tabs
Neovim displaying inactive tab labels as blue text on yellow background.

Workarounds

To get around this apparent limitation, one can imagine a few workarounds. One workaround would be to not put reverse among the attributes of TabLineFill.


        highlight TabLine cterm=reverse
        highlight TabLineFill cterm=NONE
        

Obviously, this wont give me the desired tabline, since the part containing no tabs will be in normal video:

A terminal showing neovim with four tabs
Neovim with the fill part in normal video.

Another possible workaround would be to use explicit colors instead of attributes for normal and reverse video.


        highlight TabLine ctermfg=white ctermbg=black
        highlight TabLineSel ctermfg=black ctermbg=white
        highlight TabLineFill ctermfg=white ctermbg=black
        

This looks somewhat like my desired highlight, but wont quite cut it since the active tab now is displayed with a white background, whereas I want it to be displayed using whatever color my terminal program has as its background color. This background color is most likely some very, very light gray rather than totally white:

A terminal showing neovim with four tabs
Neovim with the tabline highlighted by colors black and white.

The Solution

The solution that I have found and that will highlight the tabline properly while still allowing a custom tabline to be used is a variation of the first workaround. By leaving attributes out of TabLineFill altogether and instead putting them in a custom highlight group, one can get the inactive tabs and the fill part to be displayed in reverse video, and the active tab to be displayed in normal video. Again, using the example tabline from the Neovim documentation:


        highlight TabLine cterm=reverse
        highlight TabLineFill cterm=None
        highlight MyTabLineFill cterm=reverse

        set tabline=%!MyTabLine()

        function MyTabLine()
          let s = ''
          for i in range(tabpagenr('$'))
            " select the highlighting
            if i + 1 == tabpagenr()
              let s ..= '%#TabLineSel#'
            else
              let s ..= '%#TabLine#'
            endif

            " set the tab page number (for mouse clicks)
            let s ..= '%' .. (i + 1) .. 'T'

            " the label is made by MyTabLabel()
            let s ..= ' %{MyTabLabel(' .. (i + 1) .. ')} '
          endfor

          " after the last tab page fill with TabLineFill and reset tab page nr
          let s ..= '%#MyTabLineFill#%T'

          " right-align the label to close the current tab page
          if tabpagenr('$') > 1
            let s ..= '%=%#TabLine#%999Xclose'
          endif

          return s
        endfunction

        function MyTabLabel(n)
          let buflist = tabpagebuflist(a:n)
          let winnr = tabpagewinnr(a:n)
          return bufname(buflist[winnr - 1])
        endfunction
        
A terminal showing neovim with four tabs
Neovim with a custom tabline highlighted properly.

Wrapping up

With a way of properly highlighting a custom tabline, it is now time to rewrite the tabline function and make it display numbers instead of filenames or long path names:


        highlight TabLine cterm=reverse
        highlight TabLineFill cterm=None
        highlight MyTabLineFill cterm=reverse

        set tabline=%!MyTabLine()

        function MyTabLine()
          let s = ''
          for i in range(tabpagenr('$'))
            if i + 1 == tabpagenr()
              let s ..= '%#TabLineSel#'
            else
              let s ..= '%#TabLine#'
            endif
            let s ..= ' ' .. (i + 1) .. ' '
          endfor
          let s ..= '%#MyTabLineFill#'
          return s
        endfunction
        

While changing the tab line labels, I also removed the mouse related %T and %X:

A terminal showing neovim with four tabs
Numbered tabs.

From here on, it is trivial to convert everything to Lua and to make it work for termguicolors. That is actually how I have it setup in my init.lua, but since the example in the Neovim documentation is given in Vimscript, all the configuration above is also in Vimscript.

While working on my tabline configuration, it struck me that the documentation is silent regarding this limitation, and also that not much is said about it online. As for the latter, I can only speculate, and I would guess that most users who configure their own tabline will do so by using colors and hence be unaware of the difficulties of using video attributes.