Theo's ʕ•ᴥ•ʔ Park

Wrote my own Tabline in Lua (with clickable buttons + buffers in the current tab)

tabline-demo

I could not find a tabline plugin that I liked (closest I could find was tabby.nvim), so I wrote my own tabline to:

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

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

#neovim #lua