- Main concepts
- Component fields
- Builtin conditions and utilities
- Recipes
- Getting started
- Colors, colors, more colors!
- Crash course: the ViMode
- Crash course part II: FileName and friends
- FileType, FileEncoding and FileFormat
- FileSize and FileLastModified
- Cursor position: Ruler and ScrollBar
- LSP
- Diagnostics
- Git
- Debugger
- Tests
- Working Directory
- Terminal Name
- Help FileName
- Snippets Indicator
- Spell
- Flexible Components 🆕
- Putting it all together: Conditional Statuslines
- A classic: Change multiple background colors based on Vi Mode
- Click it! 🆕
- Theming
In heirline, everything is a StatusLine
object. There is no distinction in the way one defines the final statusline
from any of its components.
You don't need to explicitly create a StatusLine
object, the setup
function
will handle that. What you should do, is to create a lua table that will serve as
a blueprint for the creation of such objects.
That's it, your statusline(s) are just some nested tables.
The nested tables will be referred to as components
. Components may contain
other components, each of which may contain others. A component within another
component is called a child
, and will inherit the fields of its parent
.
There is no limit in how many components can be nested into each other.
local statusline = {
{...}, {...}, {..., {...}, {...}, {..., {...}, {..., {...}}}}
}
local winbar = {{...}, {{...}, {...}}}
-- the winbar parameter is optional!
require'heirline'.setup(statusline, winbar)
Writing nested tables can be tiresome, so the best approach is to define simple components and then assemble them. For example:
local Component1 = { ... }
local Sub1 = { ... }
local Component2 = { ... }
local statusline = {
...,
{Component1, Sub1},
Component2,
}
require'heirline'.setup(statusline)
After calling require'heirline'.setup(statusline[, winbar])
, your StatusLine
object
will be created, and you can find its handle at require'heirline'.statusline
(and require'heirline'.winbar
).
Any modification to the object itself will reflect in real time on your statusline!
Note that no reference is shared between the table objects used as blueprints (the
ones you pass to setup()
) and the final object, as all data is deep-copied.
So, what should be the content of a component table? Well it's fairly simple,
don't let the detailed description discourage you! Just keep one thing in mind:
whenever you see a function, know that the function is executed in the context
of the buffer and window the statusline belongs to. (The indices of the actual
buffer and window you're in are stored in the default vim global variables
vim.g.actual_curbuf
and vim.g.acutal_curwin
.)
Each component may contain any of the following fields:
Note that all functions described below are actual methods of the component itself, which can be accessed via the
self
parameter. Because of inheritance, children will look for unknown attributes within their own parent fields.
Basic fields:
provider
:- Type:
string|number
orfunction(self) -> string|number|nil
- Description: This is the string that gets printed in the statusline. No
escaping is performed, so it may contain sequences that have a special
meaning within the statusline, such as
%f
(filename),%p
(percentage through file),%-05.10(
%)
(to control text alignment and padding), etc. For more, see:h 'statusline'
. To print an actual%
, use%%
.
- Type:
hl
:- Type:
table|string
orfunction(self) -> table|string|nil
. Ifhl
is a string, it will be interpreted as the name of an already defined highlight group. Ifhl
is a table, it may contain any of:fg
: The foreground color. Type:string
to hex color code or vim standard color name (e.g.:"#FFFFFF"
,"red"
).bg
: The background color. Type: as above.sp
: The underline/undercurl color, if any. Type: as above.- Style fields supported by
synIDattrstyle()
: Example:{ bold = true, underline = true }
force
: Control whether the parent'shl
fields will override child's hl. Type:bool
.
- Description:
hl
controls the colors of what is printed by the component'sprovider
, or by any of its descendants. At evaluation time, thehl
of any component gets merged with thehl
of its parent (whether it is a function or table), so that, when specified, the fields in the childhl
will always take precedence unlessforce
istrue
.
- Type:
condition
:- Type:
function(self) -> any
- Description: This function controls whether the component should be
evaluated or not. It is the first function to be executed at evaluation
time. The truthy of the return value is tested, so any value besides
nil
andfalse
will evaluate totrue
. Of course, this will affect all of the component's progeny.
- Type:
on_click
:- Type:
table
with the following fields:callback
: (vim/)lua function to be called on mouse click(s). The function has the signaturefunction(self, winid, minwid, nclicks, button)
(see:h 'statusline'
description for@
). If astring
is provided, it is interpreted as the raw function name (v:lua.
is not prepended) of an already defined function accessible from vim global scope. Type:function
orstring
.name
: the global name the function will be registered with. It is not required whencallback
is astring
. Type:string
orfunction -> string
.update
: whether the function should be registered even if it already exists in the global namespace. This is useful for dynamically registering different callbacks. Omit this field if you are registering only one function. Type:boolean
(optional).
- Description: Specify a function to be called when clicking on the component (including its progeny);
Lua functions are automatically registered in the global scope with the name provided
by the
name
field. Arguments passed to the function are the same described for the@
statusline field, with the addition of the component reference (self
) as the first parameter and the currentwindow-ID
as the second parameter. The self parameter andwinid
are not passed ifcallback
is astring
. By default, the callback is registered only once: the first time it's encountered during components evaluation. Ifupdate
istrue
, the callback will be (re-)registered at each evaluation cycle. Note 1: be careful of the arguments passed to the callback, you may often prefer wrapping a 'third-party' functions rather than passing their reference as is. Note 2: the callback is not executed in the context of the window/buffer the component belongs to, but in the context of the actual current window and buffer. Usewinid
parameter to retrieve information about the current buffer from a callback. Be careful when accessingself
attributes that were set depending on the local buffer/window the component is displayed into from within the callback, as they are shared between all representation of the same component. Please see the recipes to learn how to propagate information about the window/buffer the clicked component belongs to.
- Type:
update
:- Type:
function(self) -> boolean
orstring
ortable<string>
. - Description: Control when the component should be updated or return a per-window
cached value.
If
update
is a function, the component will be updated whenever the function return value istrue
; else, astring
or atable
of strings will be interpreted as autocommand event names that should trigger the component evaluation.
- Type:
{...}
:- Type:
list
- Description: The component progeny. Each item of the list is a component itself and may contain any of the basic and advanced fields.
- Type:
Advanced fields
pick_child
:- Type:
table[int]
- Description: Specify which children and in which order they should be
evaluated by indicating their indexes (eg:
{1, 3, 2}
). It makes most sense to modify this attribute from withininit
function using theself
parameter to dynamically pick the children to evaluate.
- Type:
init
:- Type:
function(self) -> any
- Description: This function is called whenever a component is evaluated
(right after
condition
but beforehl
andprovider
), and can be used to modify the state of the component itself via theself
parameter. For example, you can compute some values that will be accessed from other functions within the component genealogy (even "global" statusline variables).
- Type:
after
:- Type:
function(self) -> any
- Description: This function is called after the component has evaluated all of its
children and can be used to alter the state of the component before it returns
the output string
self.stl
.
- Type:
static
:- Type:
table
- Description: This is a container for static variables, that is, variables
that are computed only once at component definition. This is useful to
organize data that should be shared among children, like icons or
dictionaries. Any keyword defined within this table can be accessed by the
component and its children methods as a direct attribute using the
self
parameter. (eg:static = { x = ... }
can be accessed asself.x
somewhere else).
- Type:
restrict
:- Type:
table[keyword = bool]
- Description: Set-like table to control which component fields can be
inherited by the component's progeny. The supplied table gets merged with
the defaults. By default, the following fields are private to the
component:
pick_child
,init
,provider
,condition
andrestrict
. Attention: modifying the defaults could dramatically affect the behavior of the component! (eg:restrict = { my_private_var = true, provider = false }
)
- Type:
There are two distinct phases in the life of a StatusLine object component: its
creation (instantiation) and its evaluation. When creating the "blueprint"
tables, the user instructs the actual constructor on the attributes and methods
of the component. The fields static
and restrict
will have a meaning only
during the instantiation phase, while condition
, update
, init
, hl
,
on_click
, provider
and pick_child
are evaluated (in this order) every time the statusline is
refreshed.
Confused yet? Don't worry, everything will come together in the Recipes examples.
You'll probably never need those, however, for completeness, it's worth
explaining the StatusLine
object base methods and attributes:
new(self, child)
: This is the constructor that takes in thechild
"blueprint" and returns a newStatusLine
object. This function is recursive, so ifchild
has children, those will be instantiated aschild
subclasses. Also note that all tables inchild
are deep-copied in the returned object.eval(self)
: Evaluates theStatusLine
recursively to figure out, for every component and {sub{,sub{,sub{,...}}}} components what's their printable value and color. This function will executecondition
,init
,hl
andprovider
and merges the object's evaluatedhl
with the parent's (depending on the value of itshl.force
).nonlocal(self, attr)
: Searches for the keywordattr
in the parent's__index
, ignoring any value defined for that keyword in the component itself (self
). This is useful for children that want to look for their parent's attributes, ignoring what was passed to them by inheritance.local_(self, attr)
: Return the value ofattr
only if it is defined for the component itself, do not look in the parent's metatables.broadcast(self, func)
: Executefunc(component)
on every component of the statusline.get(self, id)
: Get a handle of the component with the givenid
id
: Table containing the indices required to index the component from the root.{set,get}_win_attr(self, attr, default)
: Set or get a window-local component attribute. If the attribute is not defined, sets adefault
value.stl
: the last output value of the component's evaluation.winnr
: window number of the last window the component was evaluated into.
While heirline does not provide any default component, it defines a few useful
test and utility functions to aid in writing components and their conditions.
These functions are accessible via require'heirline.conditions'
and
require'heirline.utils'
Built-in conditions:
is_active()
: returns true if the statusline's window is the active window.buffer_matches(patterns)
: Returns true whenever a buffer attribute (filetype
,buftype
orbufname
) matches any of the lua patterns in the corresponding list.patterns
: table of the form{filetype = {...}, buftype = {...}, bufname = {...}}
where each field is a list of lua patterns.
width_percent_below(N, threshold, is_winbar)
: returns true if(N / current_window_width) <= threshold
(eg.:width_percent_below(#mystring, 0.33)
). This function checks the value ofvim.o.laststatus
to determine the statusline draw space, ifis_winbar == true
only the current window width will be considered.is_git_repo()
: returns true if the file is within a git repo (uses gitsigns)has_diagnostics()
: returns true if there is any diagnostic for the buffer.lsp_attached():
returns true if an LSP is attached to the buffer.
Utility functions:
get_highlight(hl_name)
: returns a table of the attributes of the provided highlight name. The returned table contains the same fields as returned bynvim_get_hl_by_name
. The returned table can be indexed using the following abbreviations:fg
→foreground
,bg
→background
,sp
→special
.clone(component[, with])
: returns a new component which is a copy of the supplied one, updated with the fields in the optionalwith
table.surround(delimiters, color, component)
: returns a new component, which contains a copy of the supplied one, surrounded by the left and right delimiters given by thedelimiters
table.delimiters
: table of the form{left_delimiter, right_delimiter}
. Because they are actually just providers, delimiters could also be functions!color
:string|nil
orfunction -> string|nil
. String should refer to RGB hex code or builtin color name. This color will be the foreground color of the delimiters and the background color of the component.component
: the component to be surrounded.
insert(parent, ...)
: return a copy ofparent
component where eachchild
in...
(variable arguments) is appended to its children (if any).make_flexible_component(priority, ...)
: Returns a flexible component with the given priority (int
). This component will cycle between all thecomponents
passed as...
arguments until they fit in the available space for the statusline. The components passed as variable arguments should evaluate to decreasing lengths. See Flexible Components for more!pick_child_on_condition(component)
: This function should be passed as theinit
field while defining a new component. It will dynamically set thepick_child
field to the index of the first child whose condition evaluates totrue
. This is useful for branching conditional statuslines (see Putting it all together: Conditional Statuslines).count_chars(str)
: Returns the character length ofstr
. Handles multibyte characters (icons) and statusline syntax like%f
,%3.10%(...%)
, etc.
Ideally, the following code snippets should go within a configuration file, say
~/.config/nvim/lua/plugins/heirline.lua
, that can be required in your
init.lua
(or from packer config
) using require'plugins.heirline'
.
Your configuration file will start like this:
local conditions = require("heirline.conditions")
local utils = require("heirline.utils")
You will probably want to define some colors. This is not required, you don't even have to use them if you don't like it, but let's say you like colors.
Colors can be specified directly in components, but it is probably more
convenient to organize them in some kind of table. If you want your statusline
to blend nicely with your colorscheme, the utility function get_highlight()
is your friend. To create themes and have your colors updated on-demand, see
Theming.
local colors = {
red = utils.get_highlight("DiagnosticError").fg,
green = utils.get_highlight("String").fg,
blue = utils.get_highlight("Function").fg,
gray = utils.get_highlight("NonText").fg,
orange = utils.get_highlight("DiagnosticWarn").fg,
purple = utils.get_highlight("Statement").fg,
cyan = utils.get_highlight("Special").fg,
diag = {
warn = utils.get_highlight("DiagnosticWarn").fg,
error = utils.get_highlight("DiagnosticError").fg,
hint = utils.get_highlight("DiagnosticHint").fg,
info = utils.get_highlight("DiagnosticInfo").fg,
},
git = {
del = utils.get_highlight("diffDeleted").fg,
add = utils.get_highlight("diffAdded").fg,
change = utils.get_highlight("diffChanged").fg,
},
}
Perhaps, your favorite colorscheme already provides a way to get the theme colors.
local colors = require'kanagawa.colors'.setup() -- wink
No statusline is worth its weight in fanciness 🌟 without an appropriate mode indicator. So let's cook ours! Also, this snippet will introduce you to a lot of heirline advanced capabilities.
local ViMode = {
-- get vim current mode, this information will be required by the provider
-- and the highlight functions, so we compute it only once per component
-- evaluation and store it as a component attribute
init = function(self)
self.mode = vim.fn.mode(1) -- :h mode()
end,
-- Now we define some dictionaries to map the output of mode() to the
-- corresponding string and color. We can put these into `static` to compute
-- them at initialisation time.
static = {
mode_names = { -- change the strings if you like it vvvvverbose!
n = "N",
no = "N?",
nov = "N?",
noV = "N?",
["no\22"] = "N?",
niI = "Ni",
niR = "Nr",
niV = "Nv",
nt = "Nt",
v = "V",
vs = "Vs",
V = "V_",
Vs = "Vs",
["\22"] = "^V",
["\22s"] = "^V",
s = "S",
S = "S_",
["\19"] = "^S",
i = "I",
ic = "Ic",
ix = "Ix",
R = "R",
Rc = "Rc",
Rx = "Rx",
Rv = "Rv",
Rvc = "Rv",
Rvx = "Rv",
c = "C",
cv = "Ex",
r = "...",
rm = "M",
["r?"] = "?",
["!"] = "!",
t = "T",
},
mode_colors = {
n = colors.red ,
i = colors.green,
v = colors.cyan,
V = colors.cyan,
["\22"] = colors.cyan,
c = colors.orange,
s = colors.purple,
S = colors.purple,
["\19"] = colors.purple,
R = colors.orange,
r = colors.orange,
["!"] = colors.red,
t = colors.red,
}
},
-- We can now access the value of mode() that, by now, would have been
-- computed by `init()` and use it to index our strings dictionary.
-- note how `static` fields become just regular attributes once the
-- component is instantiated.
-- To be extra meticulous, we can also add some vim statusline syntax to
-- control the padding and make sure our string is always at least 2
-- characters long. Plus a nice Icon.
provider = function(self)
return " %2("..self.mode_names[self.mode].."%)"
end,
-- Same goes for the highlight. Now the foreground will change according to the current mode.
hl = function(self)
local mode = self.mode:sub(1, 1) -- get only the first mode character
return { fg = self.mode_colors[mode], bold = true, }
end,
-- Re-evaluate the component only on ModeChanged event!
-- This is not required in any way, but it's there, and it's a small
-- performance improvement.
update = 'ModeChanged'
}
Perhaps one of the most important components is the one that shows which file you are editing. In this second recipe, we will revisit some heirline concepts and explore new ways to assemble components. We will also learn a few useful vim lua API functions. Because we are all crazy about icons, we'll require nvim-web-devicons, but you are absolutely free to omit that if you're not an icon person.
local FileNameBlock = {
-- let's first set up some attributes needed by this component and it's children
init = function(self)
self.filename = vim.api.nvim_buf_get_name(0)
end,
}
-- We can now define some children separately and add them later
local FileIcon = {
init = function(self)
local filename = self.filename
local extension = vim.fn.fnamemodify(filename, ":e")
self.icon, self.icon_color = require("nvim-web-devicons").get_icon_color(filename, extension, { default = true })
end,
provider = function(self)
return self.icon and (self.icon .. " ")
end,
hl = function(self)
return { fg = self.icon_color }
end
}
local FileName = {
provider = function(self)
-- first, trim the pattern relative to the current directory. For other
-- options, see :h filename-modifers
local filename = vim.fn.fnamemodify(self.filename, ":.")
if filename == "" then return "[No Name]" end
-- now, if the filename would occupy more than 1/4th of the available
-- space, we trim the file path to its initials
-- See Flexible Components section below for dynamic truncation
if not conditions.width_percent_below(#filename, 0.25) then
filename = vim.fn.pathshorten(filename)
end
return filename
end,
hl = { fg = utils.get_highlight("Directory").fg },
}
local FileFlags = {
{
provider = function() if vim.bo.modified then return "[+]" end end,
hl = { fg = colors.green }
}, {
provider = function() if (not vim.bo.modifiable) or vim.bo.readonly then return "" end end,
hl = { fg = colors.orange }
}
}
-- Now, let's say that we want the filename color to change if the buffer is
-- modified. Of course, we could do that directly using the FileName.hl field,
-- but we'll see how easy it is to alter existing components using a "modifier"
-- component
local FileNameModifer = {
hl = function()
if vim.bo.modified then
-- use `force` because we need to override the child's hl foreground
return { fg = colors.cyan, bold = true, force=true }
end
end,
}
-- let's add the children to our FileNameBlock component
FileNameBlock = utils.insert(FileNameBlock,
FileIcon,
utils.insert(FileNameModifer, FileName), -- a new table where FileName is a child of FileNameModifier
unpack(FileFlags), -- A small optimisation, since their parent does nothing
{ provider = '%<'} -- this means that the statusline is cut here when there's not enough space
)
These ones are pretty straightforward.
local FileType = {
provider = function()
return string.upper(vim.bo.filetype)
end,
hl = { fg = utils.get_highlight("Type").fg, bold = true },
}
local FileEncoding = {
provider = function()
local enc = (vim.bo.fenc ~= '' and vim.bo.fenc) or vim.o.enc -- :h 'enc'
return enc ~= 'utf-8' and enc:upper()
end
}
local FileFormat = {
provider = function()
local fmt = vim.bo.fileformat
return fmt ~= 'unix' and fmt:upper()
end
}
Now let's get a little exotic!
local FileSize = {
provider = function()
-- stackoverflow, compute human readable file size
local suffix = { 'b', 'k', 'M', 'G', 'T', 'P', 'E' }
local fsize = vim.fn.getfsize(vim.api.nvim_buf_get_name(0))
fsize = (fsize < 0 and 0) or fsize
if fsize <= 0 then
return "0"..suffix[1]
end
local i = math.floor((math.log(fsize) / math.log(1024)))
return string.format("%.2g%s", fsize / math.pow(1024, i), suffix[i])
end
}
local FileLastModified = {
-- did you know? Vim is full of functions!
provider = function()
local ftime = vim.fn.getftime(vim.api.nvim_buf_gett_name(0))
return (ftime > 0) and os.date("%c", ftime)
end
}
Here's some classics!
-- We're getting minimalists here!
local Ruler = {
-- %l = current line number
-- %L = number of lines in the buffer
-- %c = column number
-- %P = percentage through file of displayed window
provider = "%7(%l/%3L%):%2c %P",
}
-- I take no credits for this! :lion:
local ScrollBar ={
static = {
sbar = { '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█' }
-- Another variant, because the more choice the better.
-- sbar = { '🭶', '🭷', '🭸', '🭹', '🭺', '🭻' }
},
provider = function(self)
local curr_line = vim.api.nvim_win_get_cursor(0)[1]
local lines = vim.api.nvim_buf_line_count(0)
local i = math.floor((curr_line - 1) / lines * #self.sbar) + 1
return string.rep(self.sbar[i], 2)
end,
hl = { fg = colors.blue, bg = colors.bright_bg },
}
Nice work! You made it jumped right to the main courses! The finest rice is
here.
local LSPActive = {
condition = conditions.lsp_attached,
update = {'LspAttach', 'LspDetach'},
-- You can keep it simple,
-- provider = " [LSP]",
-- Or complicate things a bit and get the servers names
provider = function()
local names = {}
for i, server in pairs(vim.lsp.buf_get_clients(0)) do
table.insert(names, server.name)
end
return " [" .. table.concat(names, " ") .. "]"
end,
hl = { fg = colors.green, bold = true },
}
-- I personally use it only to display progress messages!
-- See lsp-status/README.md for configuration options.
-- Note: check "j-hui/fidget.nvim" for a nice statusline-free alternative.
local LSPMessages = {
provider = require("lsp-status").status,
hl = { fg = colors.gray },
}
-- Awesome plugin
local Gps = {
condition = require("nvim-gps").is_available,
provider = require("nvim-gps").get_location,
hl = { fg = colors.gray },
}
See how much you've messed up...
local Diagnostics = {
condition = conditions.has_diagnostics,
static = {
error_icon = vim.fn.sign_getdefined("DiagnosticSignError")[1].text,
warn_icon = vim.fn.sign_getdefined("DiagnosticSignWarn")[1].text,
info_icon = vim.fn.sign_getdefined("DiagnosticSignInfo")[1].text,
hint_icon = vim.fn.sign_getdefined("DiagnosticSignHint")[1].text,
},
init = function(self)
self.errors = #vim.diagnostic.get(0, { severity = vim.diagnostic.severity.ERROR })
self.warnings = #vim.diagnostic.get(0, { severity = vim.diagnostic.severity.WARN })
self.hints = #vim.diagnostic.get(0, { severity = vim.diagnostic.severity.HINT })
self.info = #vim.diagnostic.get(0, { severity = vim.diagnostic.severity.INFO })
end,
update = { "DiagnosticChanged", "BuEnter" },
{
provider = "![",
},
{
provider = function(self)
-- 0 is just another output, we can decide to print it or not!
return self.errors > 0 and (self.error_icon .. self.errors .. " ")
end,
hl = { fg = colors.diag.error },
},
{
provider = function(self)
return self.warnings > 0 and (self.warn_icon .. self.warnings .. " ")
end,
hl = { fg = colors.diag.warn },
},
{
provider = function(self)
return self.info > 0 and (self.info_icon .. self.info .. " ")
end,
hl = { fg = colors.diag.info },
},
{
provider = function(self)
return self.hints > 0 and (self.hint_icon .. self.hints)
end,
hl = { fg = colors.diag.hint },
},
{
provider = "]",
},
}
Let's say that you'd like to have only the diagnostic icon colored, not the actual count. Just replace the children with something like this.
...
{
condition = function(self) return self.errors > 0 end,
{
provider = function(self) return self.error_icon end,
hl = { fg = colors.diag.error },
},
{
provider = function(self) return self.errors .. " " end,
}
},
...
Delimiters are subject to the Diagnostics component condition
; if you'd like
them to be always visible, just wrap 'em around the component or use the surround()
utility!
Diagnostics = { { provider = "![" }, Diagnostics, { provider = "]" } }
-- or
Diagnostics = utils.surround({"![", "]"}, nil, Diagnostics)
For the ones who're not (too) afraid of changes! Uses gitsigns.
local Git = {
condition = conditions.is_git_repo,
init = function(self)
self.status_dict = vim.b.gitsigns_status_dict
self.has_changes = self.status_dict.added ~= 0 or self.status_dict.removed ~= 0 or self.status_dict.changed ~= 0
end,
hl = { fg = colors.orange },
{ -- git branch name
provider = function(self)
return " " .. self.status_dict.head
end,
hl = { bold = true }
},
-- You could handle delimiters, icons and counts similar to Diagnostics
{
condition = function(self)
return self.has_changes
end,
provider = "("
},
{
provider = function(self)
local count = self.status_dict.added or 0
return count > 0 and ("+" .. count)
end,
hl = { fg = colors.git.add },
},
{
provider = function(self)
local count = self.status_dict.removed or 0
return count > 0 and ("-" .. count)
end,
hl = { fg = colors.git.del },
},
{
provider = function(self)
local count = self.status_dict.changed or 0
return count > 0 and ("~" .. count)
end,
hl = { fg = colors.git.change },
},
{
condition = function(self)
return self.has_changes
end,
provider = ")",
},
}
Display informations from nvim-dap!
local DAPMessages = {
-- display the dap messages only on the debugged file
condition = function()
local session = require("dap").session()
if session then
local filename = vim.api.nvim_buf_get_name(0)
if session.config then
local progname = session.config.program
return filename == progname
end
end
return false
end,
provider = function()
return " " .. require("dap").status()
end,
hl = { fg = utils.get_highlight('Debug').fg },
}
This requires the great ultest.
local UltTest = {
condition = function()
return vim .api.nvim_call_function("ultest#is_test_file", {}) ~= 0
end,
static = {
passed_icon = vim.fn.sign_getdefined("test_pass")[1].text,
failed_icon = vim.fn.sign_getdefined("test_fail")[1].text,
passed_hl = { fg = utils.get_highlight("UltestPass").fg },
failed_hl = { fg = utils.get_highlight("UltestFail").fg },
},
init = function(self)
self.status = vim.api.nvim_call_function("ultest#status", {})
end,
-- again, if you'd like icons and numbers to be colored differently,
-- just split the component in two
{
provider = function(self)
return self.passed_icon .. self.status.passed .. " "
end,
hl = function(self)
return self.passed_hl
end,
},
{
provider = function(self)
return self.failed_icon .. self.status.failed .. " "
end,
hl = function(self)
return self.failed_hl
end,
},
{
provider = function(self)
return "of " .. self.status.tests - 1
end,
},
}
Always know your global or local working directory. This component, together with FileName, will provide the full path to the edited file.
local WorkDir = {
provider = function()
local icon = (vim.fn.haslocaldir(0) == 1 and "l" or "g") .. " " .. " "
local cwd = vim.fn.getcwd(0)
cwd = vim.fn.fnamemodify(cwd, ":~")
if not conditions.width_percent_below(#cwd, 0.25) then
cwd = vim.fn.pathshorten(cwd)
end
local trail = cwd:sub(-1) == '/' and '' or "/"
return icon .. cwd .. trail
end,
hl = { fg = colors.blue, bold = true },
}
Special handling of the built-in terminal bufname. See conditional statuslines below to see an example of dedicated statusline for terminals!
local TerminalName = {
-- we could add a condition to check that buftype == 'terminal'
-- or we could do that later (see #conditional-statuslines below)
provider = function()
local tname, _ = vim.api.nvim_buf_get_name(0):gsub(".*:", "")
return " " .. tname
end,
hl = { fg = colors.blue, bold = true },
}
See the name of the helpfile you're viewing.
local HelpFileName = {
condition = function()
return vim.bo.filetype == "help"
end,
provider = function()
local filename = vim.api.nvim_buf_get_name(0)
return vim.fn.fnamemodify(filename, ":t")
end,
hl = { fg = colors.blue },
}
This requires ultisnips, but the same logic could be applied to many other snippet plugins! Get an indicator of when you're inside a snippet and can jump to the previous and/or forward tag.
local Snippets = {
-- check that we are in insert or select mode
condition = function()
return vim.tbl_contains({'s', 'i'}, vim.fn.mode())
end,
provider = function()
local forward = (vim.fn["UltiSnips#CanJumpForwards"]() == 1) and "" or ""
local backward = (vim.fn["UltiSnips#CanJumpBackwards"]() == 1) and " " or ""
return backward .. forward
end,
hl = { fg = "red", bold = true },
}
Add indicator when spell is set!
local Spell = {
condition = function()
return vim.wo.spell
end,
provider = 'SPELL ',
hl = { bold = true, fg = colors.orange}
}
Yes, Heirline has flexible components! And, like any other component, they can be nested and are context-aware!
Flexible components are components that will adjust their output depending on the visible space for that window statusline.
Setting them up is as easy as calling utils.make_flexible_component(priority, ...)
replacing the variable ...
with a series of components that will evaluate to
decreasing lengths.
The priority
will determine the order at which multiple flexible components will be
expanded or contracted:
- higher priority: last to contract, first to expand
- lower priority: first to contract, last to expand
- same priority: will contract or expand simultaneously
Flexible components can be nested at will, however, when doing so, the
priority
of the nested components will be ignored and only the
outermost priority will be used to determine the order of
expansion/contraction. If you'd like nested components to have different
priorities, make sure there is enough difference between the priorities of the
outermost ("root") flexible components (at least 1 + (1 for each level of nesting)
),
unless you are after some very complicated behavior.
You don't need to do the math though, you can just use large numbers! If nesting seems complex, it is because it is! Remember that you can suit most of your needs without nesting flexible components.
Here's a wild example:
local a = { provider = string.rep("A", 40) }
local b = { provider = string.rep("B", 30) }
local c = { provider = string.rep("C", 20) }
local d = { provider = string.rep("D", 10) }
local e = { provider = string.rep("E", 8) }
local f = { provider = string.rep("F", 4) }
local nest_madness = {
utils.make_flexible_component(1,
a,
utils.make_flexible_component(nil, -- nested components priority is ignored!
b,
utils.make_flexible_component(nil, c, d),
e
),
f
),
{ provider = "%=" },
utils.make_flexible_component(4, -- 1 + 1 * 2 levels of nesting
a,
utils.make_flexible_component(nil,
b,
utils.make_flexible_component(nil, c, d),
e
),
f
),
}
require("heirline").setup(nest_madness)
And now some more useful examples!
Flexible WorkDir compare to Working Directory
local WorkDir = {
provider = function(self)
self.icon = (vim.fn.haslocaldir(0) == 1 and "l" or "g") .. " " .. " "
local cwd = vim.fn.getcwd(0)
self.cwd = vim.fn.fnamemodify(cwd, ":~")
end,
hl = { fg = colors.blue, bold = true },
utils.make_flexible_component(1, {
-- evaluates to the full-lenth path
provider = function(self)
local trail = self.cwd:sub(-1) == "/" and "" or "/"
return self.icon .. self.cwd .. trail .." "
end,
}, {
-- evaluates to the shortened path
provider = function(self)
local cwd = vim.fn.pathshorten(self.cwd)
local trail = self.cwd:sub(-1) == "/" and "" or "/"
return self.icon .. cwd .. trail .. " "
end,
}, {
-- evaluates to "", hiding the component
provider = "",
}),
}
Flexible FileName Use this in the same context of Crash course part II: FileName and friends
local FileName = {
init = function(self)
self.lfilename = vim.fn.fnamemodify(self.filename, ":.")
if self.lfilename == "" then self.lfilename = "[No Name]" end
end,
hl = { fg = utils.get_highlight("Directory").fg },
utils.make_flexible_component(2, {
provider = function(self)
return self.lfilename
end,
}, {
provider = function(self)
return vim.fn.pathshorten(self.lfilename)
end,
}),
}
Flexible Gps a.k.a. make it disappear
local Gps = utils.make_flexible_component(3, Gps, { provider = "" })
With heirline you can setup custom statuslines depending on some condition. Let's say you'd like to have something like this:
- a default statusline to be shown whenever you edit a regular file,
- a statusline for regular inactive buffers
- a statusline for special buffers, like the quickfix, helpfiles, nvim-tree, or other windowed plugins.
- a dedicated statuslines for terminals.
Because there's no actual distinction between a statusline and any of its
components, we can just use the condition
field to affect a whole series of
components (our statusline).
Let's first define some trivial components to readily create aligned sections and spacing where we want.
local Align = { provider = "%=" }
local Space = { provider = " " }
Assembling your favorite components and doing last-minute adjustments is easy!
ViMode = utils.surround({ "", "" }, colors.bright_bg, { ViMode, Snippets })
local DefaultStatusline = {
ViMode, Space, FileName, Space, Git, Space, Diagnostics, Align,
Gps, DAPMessages, Align,
LSPActive, Space, LSPMessages, Space, UltTest, Space, FileType, Space, Ruler, Space, ScrollBar
}
Pro-tip: Always end a short statusline with %=
(the Align component) to
fill the whole statusline with the same color!
local InactiveStatusline = {
condition = function()
return not conditions.is_active()
end,
FileType, Space, FileName, Align,
}
local SpecialStatusline = {
condition = function()
return conditions.buffer_matches({
buftype = { "nofile", "prompt", "help", "quickfix" },
filetype = { "^git.*", "fugitive" },
})
end,
FileType, Space, HelpFileName, Align
}
local TerminalStatusline = {
condition = function()
return conditions.buffer_matches({ buftype = { "terminal" } })
end,
hl = { bg = colors.dark_red },
-- Quickly add a condition to the ViMode to only show it when buffer is active!
{ condition = conditions.is_active, ViMode, Space }, FileType, Space, TerminalName, Align,
}
That's it! We now sparkle a bit of conditional default colors to affect all the
statuslines at once and set the flag pick_child
via
utils.pick_child_on_condition
to stop the evaluation at the first component
whose condition evaluates to true
!
Note that no condition equals to true
, so make sure that all your statuslines
but the last one have a condition set at their top-level.
IMPORTANT: Statuslines conditions are evaluated sequentially, so make sure
that their order makes sense! Ideally, you should order them from stricter to
looser conditions. You can always write the init
function yourself and
leverage the pick_child
table to have full control. See the implementation
of utils.pick_child_on_condition
to have a
sense of what's going on.
local StatusLines = {
hl = function()
if conditions.is_active() then
return {
fg = utils.get_highlight("StatusLine").fg,
bg = utils.get_highlight("StatusLine").bg
}
else
return {
fg = utils.get_highlight("StatusLineNC").fg,
bg = utils.get_highlight("StatusLineNC").bg
}
end
end,
init = utils.pick_child_on_condition,
SpecialStatusline, TerminalStatusline, InactiveStatusline, DefaultStatusline,
}
Just a bunch of nested tables with trivial fields, yet, such complex behavior!
You have learned how to define components avoiding a lot of redundancy, how you can reutilize components, group them and tweak them easily. You are ready to build your own dream StatusLine(s)!
require("heirline").setup(StatusLines)
-- we're done.
If you want extra fine control over buftype/filetype/bufname and active/inactive buffers, you can use the following style to define your statusline:
- First, buftype/filetype/bufname are matched against the buffer the statusline is displayed into to choose an appropriate branch of the genealogical tree.
- Then, only one component will be picked depending on if the window of the statusline is the current one or not.
local FelineStyle = {
-- stop at child where buftype/filetype/bufname matches
init = utils.pick_child_on_condition,
{ -- Identify the buftype/filetype/bufname first
condtion = function()
return conditions.buffer_matches({...})
end,
-- Evaluate only the "active" or "inactive" child
init = utils.pick_child_on_condition,
{ -- If it's the current window, display some components
condition = conditions.is_active
{...} --
},
{ -- Otherwise, display some other components
{...} --
}
},
{ -- this block can be exactly as the one above for a different kind of
-- buffer
...
}
}
Everything we talked about for the statusline can be seamlessly applied
to the new Neovim winbar
!
local WinBars = {
init = utils.pick_child_on_condition,
{ -- Hide the winbar for special buffers
condition = function()
return conditions.buffer_matches({
buftype = { "nofile", "prompt", "help", "quickfix" },
filetype = { "^git.*", "fugitive" },
})
end,
provider = "",
},
{ -- A special winbar for terminals
condition = function()
return conditions.buffer_matches({ buftype = { "terminal" } })
end,
utils.surround({ "", "" }, colors.dark_red, {
FileType,
Space,
TerminalName,
}),
},
{ -- An inactive winbar for regular files
condition = function()
return not conditions.is_active()
end,
utils.surround({ "", "" }, colors.bright_bg, { hl = { fg = "gray", force = true }, FileNameBlock }),
},
-- A winbar for regular files
utils.surround({ "", "" }, colors.bright_bg, FileNameBlock),
}
require("heirline").setup(StatusLines, WinBars)
You may feel nostalgic about the good ol' Airline Style, where multiple sections used to change background color based on the current mode. Fear not! We can conveniently do that by making the mode-dominant color visible to all components in one go.
local StatusLines = {
hl = function()
if conditions.is_active() then
return {
fg = utils.get_highlight("StatusLine").fg,
bg = utils.get_highlight("StatusLine").bg,
}
else
return {
fg = utils.get_highlight("StatusLineNC").fg,
bg = utils.get_highlight("StatusLineNC").bg,
}
end
end,
static = {
mode_colors = {
n = colors.red,
i = colors.green,
v = colors.cyan,
V = colors.cyan,
["\22"] = colors.cyan,
c = colors.orange,
s = colors.purple,
S = colors.purple,
["\19"] = colors.purple,
R = colors.orange,
r = colors.orange,
["!"] = colors.red,
t = colors.green,
},
mode_color = function(self)
local mode = conditions.is_active() and vim.fn.mode() or "n"
return self.mode_colors[mode]
end,
},
}
Now the ViMode simply becomes:
local ViMode = {
static = {
mode_names = { ... }
},
provider = function(self)
return " %2(" .. self.mode_names[vim.fn.mode(1)] .. "%)"
end,
hl = function(self)
local color = self:mode_color() -- here!
return { fg = color, bold = true }
end,
}
And you can go crazy surrounding all the blocks you want with a little help from utils.surround
.
utils.surround({ "", "" }, function(self) return self:mode_color() end, {Ruler, hl = {fg = 'black'}} ),
-- we are surrounding the component and adjusting the foreground in one go!
You can specify a function callback to be executed when clicking the component! Here are some examples referring to the abovementioned components:
Diagnostics on_click
local Diagnostics = {
on_click = {
callback = function()
require("trouble").toggle({ mode = "document_diagnostics" })
-- or
-- vim.diagnostic.setqflist()
end,
name = "heirline_diagnostics",
},
...
}
Git on_click
local Git = {
on_click = {
callback = function()
-- If you want to use Fugitive:
-- vim.cmd("G")
-- If you prefer Lazygit
-- use vim.defer_fn() if the callback requires
-- opening of a floating window
-- (this also applies to telescope)
vim.defer_fn(function()
vim.cmd("Lazygit")
end, 100)
end,
name = "heirline_git",
},
...
}
Window Close button: Let the callback know from which window it was clicked from!
The following is the recommended way of achieving that:
on_click = {
callback = function(_, winid)
-- winid is the window id of the window the component was clicked from
end,
-- A dynamic name + update are required whenever
-- we need to register a closure for each instance of the
-- component displayed in the current tab.
name = function(self)
return "heirline_button_name" .. self.winnr
end,
update = true,
}
local CloseButton = {
condition = function(self)
return not vim.bo.modified
end,
-- a small performance improvement:
-- re register the component callback only on layout/buffer changes.
update = {'WinNew', 'WinClosed', 'BufEnter'},
{ provider = " " },
{
provider = "",
hl = { fg = "gray" },
on_click = {
callback = function(_, winid)
vim.api.nvim_win_close(winid, true)
end,
name = function(self)
return "heirline_close_button_" .. self.winnr
end,
update = true,
},
},
}
-- Use it anywhere!
local WinBarFileName = utils.surround({ "", "" }, colors.bright_bg, {
hl = function()
if not conditions.is_active() then
return { fg = "gray", force = true }
end
end,
FileNameBlock,
Space,
CloseButton,
})
Debugger on_click: step-over, step-into, next, previous, stop buttons
-- coming soon!
You can change the colors of the statusline automatically whenever you change
your colorscheme. To do so, just setup a ColorScheme
event autocommand that
will reset heirline highlights and re-source your config!
You can achieve that in two ways:
- Wrapping the generation of the statusline blueprints (components) into a function
-- beginning of your heirline config file
local M = {}
function M.setup()
... -- wrap everything into a function
require("heirline").setup(StatusLines)
end
-- setup the autocommand
vim.cmd[[
augroup heirline
autocmd!
autocmd ColorScheme * lua require'heirline'.reset_highlights(); require'plugins.heirline'.setup()
augroup END
]]
M.setup() -- call setup when the file is required the first time
return M
-- end of your heirline config file
- Using
:luafile
to reload your config.
-- beginning of your heirline config file, no need to wrap anything
...
vim.cmd[[
augroup heirline
autocmd!
autocmd ColorScheme * lua require'heirline'.reset_highlights(); vim.cmd('luafile <path-to-this-file>')
augroup END
]]
require("heirline").setup(statuslines)
-- end of your heirline config file
Config for heirline.nvim took 0.000786ms