Wrote my own Tabline in Lua (with clickable buttons + buffers in the current tab)
I could not find a tabline plugin that I liked (closest I could find was tabby.nvim), so I wrote my own tabline to:
- display the list of tabs and number of windows for each tab (if more than one, excluding floating windows) on the left side
- display the list of buffers opened in the current tab (excluding
unlisted
buffers), highlighting the focused one, on the right side
One surprising thing was that it does not say anywhere in the help documentation that the tabline buttons (%nT
where n
is the tab number) are draggable, but I was surprised that they in fact are in both Neovide and Neovim running under Wezterm and Kitty.
Overall, configuring tabline (+ winbar and statusline) was a good learning experience for me, learning about
- tab number v.s. tab ID
- various tab related Neovim API (
nvim_tabpage_list_wins
,nvim_tabpage_is_valid
, etc.) and Vimscript functions (tabpage_buflist
) - verifying if a window is floating by checking
relative
field of the window config in the return result ofnvim_win_get_config
- various tabline/statusline components (
%T
,%X
, etc.) and using evaluation of Lua function as tabline/statusline (%!
,%{%..%}
,v:lua
,luaeval()
)
I highly recommend you to configure your own Tabline if you want to utilize the built-in tabs and have 3 hours to burn.
Here is a link to the tabline.lua
in my dotfiles repository
I am including the Lua code as well, call tabline.setup()
in your init.lua
.
1--- *tabline.lua* Theovim Tabline
2--- $ figlet -f tinker-joy theovim
3--- o o
4--- | | o
5--- -o- O--o o-o o-o o o o-O-o
6--- | | | |-' | | \ / | | | |
7--- o o o o-o o-o o | o o o
8---
9--- Initialize tabline with:
10--- - Clickable tab list
11--- - Number of window iff there is more than one
12--- - List of buffers in the current tab
13
14Tabline = {}
15
16local logo = vim.g.have_nerd_font and " " or "Theovim"
17
18
19---Given a list of |window-ID|, filters out abnormal (i.e., float) windows.
20---The mechanism relies on checking the `relative` field of the window config (|api-win_config|),
21---as |api-floatwin| states "To check whether a window is floating, check whether `relative` option ... is non-empty."
22---Example:
23---
24---```lua
25---local currTabNonFloats = filterFloatWins(vim.api.nvim_tabpage_list_wins(0))
26---```
27---@param winids table List of window-iD
28---@return table nonFloats List of window-ID of non-floating windows
29local function filterFloatWins(winids)
30 local nonFloats = {}
31 for _, winid in pairs(winids) do
32 if vim.api.nvim_win_get_config(winid).relative == "" then
33 nonFloats[#nonFloats + 1] = winid
34 end
35 end
36 return nonFloats
37end
38
39
40---Given a list of buffer numbers (e.g., return value of |tabpagebuflist()|),
41---filters out duplicates AND unlisted buffers.
42---Example:
43---
44---```lua
45---local currTabListedBuf = filterUnlistedBuffers(vim.fn.tabpagebuflist())
46---````
47---@param bufnums table List of buffer numbers
48---@return table listed List of listed buffers
49local function filterUnlistedBuffers(bufnums)
50 local listed = {}
51 local hash = {}
52 for _, buf in pairs(bufnums) do
53 if vim.fn.buflisted(buf) == 1 and (not hash[buf]) then
54 listed[#listed + 1] = buf
55 hash[buf] = true
56 end
57 end
58 return listed
59end
60
61
62---Format a string for Vim tabline based on tabs and buffers on the current tab
63---@return string s Formatted string to be used as a Vim tabline
64Tabline.build = function()
65 local s = "%#TabLineFill#" .. logo
66
67 -- ========== Left ==========
68 -- List of tabs
69 -- ==========================
70
71 local currTabID = vim.api.nvim_get_current_tabpage()
72 for _, tabID in pairs(vim.api.nvim_list_tabpages()) do
73 -- This should not happen
74 if not vim.api.nvim_tabpage_is_valid(tabID) then break end
75
76 local tabNum = vim.api.nvim_tabpage_get_number(tabID)
77 local winids = filterFloatWins(vim.api.nvim_tabpage_list_wins(tabID))
78
79 -- Basic setup
80 s = s .. ((tabID == currTabID) and "%#TabLineSel#" or "%#TabLine#") --> diff hl for active and inactive tabs
81 s = s .. " " --> Left margin/separator
82 s = s .. "%" .. tabNum .. "T" --> make tab clickable (%nT)
83 s = s .. tabNum .. " " --> Tab index
84
85 -- Add a number of windows in the tab, if applicable
86 if #winids > 1 then
87 if vim.g.have_nerd_font then
88 s = s .. "[ " .. (#winids) .. "]"
89 else
90 s = s .. "[" .. (#winids) .. " WINS]"
91 end
92 end
93
94 -- Make close button clickable ("%nX", %999X closes the current tab)
95 s = s .. "%" .. tabNum .. "X"
96 s = s .. (vim.g.have_nerd_font and "" or "X")
97
98 -- Add a reset button (%T)
99 s = s .. "%T"
100 -- Add a BG highlight and left spacing
101 s = s .. " %#TabLineFill# "
102 end
103
104 -- ========== Middle ==========
105 -- Empty space
106 -- ============================
107 s = s .. "%=" --> spacer
108 s = s .. "%#TabLineSel#" --> highlight
109
110 -- ========== Right ==========
111 -- List of LISTED buffers in the current tab
112 -- ===========================
113 local bufnums = filterUnlistedBuffers(vim.fn.tabpagebuflist(vim.api.nvim_tabpage_get_number(currTabID)))
114 for _, bufnum in pairs(bufnums) do
115 local bufname = vim.fn.fnamemodify(vim.fn.bufname(bufnum), ":t")
116
117 -- Give a name to an empty buffer
118 if bufname == "" then
119 bufname = "[No Name:" .. bufnum .. "]"
120 end
121
122 -- Limit bufname to n character + 3 (accounting for "..." to be appended)
123 local bufnameLimits = 18
124 if string.len(bufname) > bufnameLimits + 3 then
125 bufname = string.sub(bufname, 1, bufnameLimits) .. "..."
126 end
127
128 -- Add a flag to a modified buffer
129 if vim.fn.getbufvar(bufnum, "&modified") == 1 then
130 bufname = bufname .. "[+]"
131 end
132
133 -- Determine highlight to use
134 local hl = "%#TabLine#"
135 if vim.fn.bufnr() == bufnum then
136 hl = "%#TabLineSel#"
137 end
138
139 -- Append formatted bufname
140 s = s .. hl .. " " .. bufname .. " "
141 end
142
143 -- Add a truncation starting point: truncate buffer information first
144 s = s .. "%<"
145 return s
146end
147
148
149-- Set tabline. The Lua function called must be globally accessible
150Tabline.setup = function()
151 vim.go.tabline = "%!v:lua.Tabline.build()"
152end
153
154return Tabline