diff --git a/README.md b/README.md index d139295..f6fa5af 100644 --- a/README.md +++ b/README.md @@ -57,3 +57,9 @@ require'heirline'.setup(statusline) ``` Calling `setup` will load your statusline. To learn how to write a StatusLine, see the [docs](cookbook.md). + +### Donate +Buy me coffee and support my work ;) + +[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/donate/?business=VNQPHGW4JEM3S&no_recurring=0&item_name=Buy+me+coffee+and+support+my+work+%3B%29¤cy_code=EUR) + diff --git a/cookbook.md b/cookbook.md index 0add98f..65a14c1 100644 --- a/cookbook.md +++ b/cookbook.md @@ -5,7 +5,7 @@ - [Main concepts](#main-concepts) - [Component fields](#component-fields) - [The StatusLine life cycle](#the-statusline-life-cycle) - - [StatusLine Base Methods](#statusline-base-methods) + - [StatusLine Base Methods and Attributes](#statusline-base-methods-and-attributes) - [Builtin conditions and utilities](#builtin-conditions-and-utilities) - [Recipes](#recipes) - [Getting started](#getting-started) @@ -24,6 +24,8 @@ - [Terminal Name](#terminal-name) - [Help FileName](#help-filename) - [Snippets Indicator](#snippets-indicator) + - [Spell](#spell) +- [Flexible Components](#flexible-components) :new: - [Putting it all together: Conditional Statuslines](#putting-it-all-together-conditional-statuslines) - [Theming](#theming) @@ -131,13 +133,12 @@ Each component may contain _any_ of the following fields: **Advanced fields** -- `stop_at_first`: - - Type: `bool` - - Description: If a component has any child, their evaluation will stop at - the first child in the succession line who does not return an empty string. - This field is not inherited by the component's progeny. Use this in - combination with children conditions to create buffer-specific statuslines! - (Or do whatever you can think of!) +- `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 within `init` function using the self + parameter to dynamically pick the children to evaluate. - `init`: - Type: `function(self) -> any` - Description: This function is called whenever a component is evaluated @@ -160,7 +161,7 @@ Each component may contain _any_ of the following fields: - 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: `stop_at_first`, `init`, `provider`, `condition` and `restrict`. + component: `pick_child`, `init`, `provider`, `condition` and `restrict`. Attention: modifying the defaults could dramatically affect the behavior of the component! (eg: `restrict = { my_private_var = true, provider = false }`) @@ -171,15 +172,15 @@ _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`, `init`, `hl`, `provider` and -`stop_at_first` are evaluated (in this order) every time the statusline is +`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](#recipes) examples. -### StatusLine Base Methods +### StatusLine Base Methods and Attributes You'll probably never need those, however, for completeness, it's worth -explaining the `StatusLine` object base methods: +explaining the `StatusLine` object base methods and attributes: - `new(self, child)`: This is the constructor that takes in the `child` "blueprint" and returns a new `StatusLine` object. This function is @@ -195,6 +196,16 @@ explaining the `StatusLine` object base methods: `__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 of `attr` only if it is defined for + the component itself, do not look in the parent's metatables. +- `broadcast(self, func)`: Execute `func(component)` on every component of the + statusline. +- `get(self, id)`: Get a handle of the component with the given `id` +- `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 a `default` value. +- `stl`: the last output value of the component's evaluation. ## Builtin conditions and utilities @@ -235,6 +246,18 @@ These functions are accessible via `require'heirline.conditions'` and - `component`: the component to be surrounded. - `insert(parent, ...)`: return a copy of `parent` component where each `child` 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 the `components` + 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](#flexible-components) for more! +- `pick_child_on_condition(component)`: This function should be passed as the `init` + field while defining a new component. It will dynamically set the `pick_child` + field to the index of the first child whose condition evaluates to `true`. + This is useful for branching conditional statuslines + (see [Putting it all together: Conditional Statuslines](#putting-it-all-together-conditional-statuslines)). +- `count_chars(str)`: Returns the character length of `str`. Handles multibyte + characters (icons) and statusline syntax like `%f`, `%3.10%(...%)`, etc. ## Recipes @@ -552,7 +575,8 @@ local ScrollBar ={ local lines = vim.api.nvim_buf_line_count(0) local i = math.floor(curr_line / lines * (#self.sbar - 1)) + 1 return string.rep(self.sbar[i], 2) - end + end, + hl = { fg = colors.blue, bg = colors.bright_bg }, } ``` @@ -586,6 +610,8 @@ local LSPActive = { ```lua -- 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 }, @@ -764,6 +790,7 @@ local DAPMessages = { return filename == progname end end + return false end, provider = function() return " " .. require("dap").status() @@ -894,6 +921,147 @@ local Snippets = { } ``` +### Spell + +Add indicator when spell is set! + +```lua +local Spell = { + condition = function() + return vim.wo.spell + end, + provider = 'SPELL ', + hl = { style = 'bold', fg = colors.orange} +} +``` + +## Flexible Components + +Yes, Heirline has flexible components! And, like any other component, they are +nestable and 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 are +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: + +```lua +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_elastic_component(1, + a, + utils.make_elastic_component(nil, -- nested components priority is ignored! + b, + utils.make_elastic_component(nil, c, d), + e + ), + f + ), + { provider = "%=" }, + utils.make_elastic_component(4, -- 1 + 1 * 2 levels of nesting + a, + utils.make_elastic_component(nil, + b, + utils.make_elastic_component(nil, c, d), + e + ), + f + ), +} +require("heirline").setup(nest_madness) +``` + +And now some more useful examples! + +**Flexible WorkDir** compare to [Working Directory](#working-directory) + +```lua +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, style = "bold" }, + + 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](#crash-course-part-ii-filename-and-friends) + +```lua +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 + +```lua +local Gps = utils.make_flexible_component(3, Gps, { provider = "" }) +``` + ## Putting it all together: Conditional Statuslines With heirline you can setup custom statuslines depending on some condition. @@ -946,8 +1114,8 @@ local InactiveStatusline = { local SpecialStatusline = { condition = function() return conditions.buffer_matches({ - buftype = {"nofile", "help", "quickfix"}, - filetype = {"^git.*", "fugitive"} + buftype = { "nofile", "prompt", "help", "quickfix" }, + filetype = { "^git.*", "fugitive" }, }) end, @@ -970,12 +1138,16 @@ local TerminalStatusline = { ``` That's it! We now sparkle a bit of conditional default colors to affect all the -statuslines at once and set the flag `stop_at_first` to stop the evaluation at -the first component that returns something printable! +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`! **IMPORTANT**: Statuslines conditions are evaluate sequentially, so make sure that their order makes sense! Ideally, you should order them from stricter to -looser conditions. +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`](lua/heirline/utils.lua#L236) to have a +sense of what's going on. ```lua local StatusLines = { @@ -994,7 +1166,7 @@ local StatusLines = { end end, - stop_at_first = true, + init = utils.pick_child_on_condition, SpecialStatusline, TerminalStatusline, InactiveStatusline, DefaultStatusline, } diff --git a/lua/heirline/init.lua b/lua/heirline/init.lua index 398c7cf..0bcb544 100644 --- a/lua/heirline/init.lua +++ b/lua/heirline/init.lua @@ -1,10 +1,9 @@ local M = {} -local StatusLine = require'heirline.statusline' - -M.statusline = {} +local StatusLine = require("heirline.statusline") +local utils = require("heirline.utils") function M.reset_highlights() - return require'heirline.highlights'.reset_highlights() + return require("heirline.highlights").reset_highlights() end function M.load() @@ -14,11 +13,24 @@ end function M.setup(statusline) M.statusline = StatusLine:new(statusline) + M.statusline:make_ids() M.load() end function M.eval() - return M.statusline:eval() + M.statusline.winnr = vim.api.nvim_win_get_number(0) + M.statusline.flexible_components = {} + local out = M.statusline:eval() + utils.expand_or_contract_flexible_components(M.statusline, out) + return out +end + +-- test [[ +function M.timeit() + local start = os.clock() + M.eval() + return os.clock() - start end +--]] return M diff --git a/lua/heirline/statusline.lua b/lua/heirline/statusline.lua index e78e236..e1d09c1 100644 --- a/lua/heirline/statusline.lua +++ b/lua/heirline/statusline.lua @@ -2,16 +2,17 @@ local utils = require("heirline.utils") local hi = require("heirline.highlights") local default_restrict = { - stop_at_first = true, + stop_when = true, init = true, provider = true, condition = true, restrict = true, + pick_child = true, } local StatusLine = { hl = {}, - cur_hl = {}, + merged_hl = {}, } function StatusLine:new(child) @@ -25,9 +26,10 @@ function StatusLine:new(child) end new.condition = child.condition + new.pick_child = child.pick_child and vim.tbl_exend("keep", child.pick_child, {}) new.init = child.init new.provider = child.provider - new.stop_at_first = child.stop_at_first + new.stop_when = child.stop_when new.restrict = child.restrict and vim.tbl_extend("keep", child.restrict, {}) if child.static then @@ -53,10 +55,56 @@ function StatusLine:new(child) return new end +function StatusLine:broadcast(func) + for i, c in ipairs(self) do + func(c) + c:broadcast(func) + end +end + +function StatusLine:make_ids(index) + local parent_id = self:nonlocal("id") or {} + + self.id = vim.tbl_extend("force", parent_id, { [#parent_id + 1] = index }) + + for i, c in ipairs(self) do + c:make_ids(i) + end +end + +function StatusLine:get(id) + id = id or {} + local curr = self + for _, i in ipairs(id) do + curr = curr[i] + end + return curr +end + function StatusLine:nonlocal(attr) return getmetatable(self).__index(self, attr) end +function StatusLine:local_(attr) + local orig_mt = getmetatable(self) + setmetatable(self, {}) + local result = self[attr] + setmetatable(self, orig_mt) + return result +end +function StatusLine:set_win_attr(attr, val, default) + local winnr = self.winnr + self[attr] = self[attr] or {} + self[attr][winnr] = val or (self[attr][winnr] or default) +end + +function StatusLine:get_win_attr(attr, default) + local winnr = self.winnr + self[attr] = self[attr] or {} + self[attr][winnr] = self[attr][winnr] or default + return self[attr][winnr] +end + function StatusLine:eval() if self.condition and not self:condition() then return "" @@ -69,29 +117,39 @@ function StatusLine:eval() local stl = {} local hl = type(self.hl) == "function" and (self:hl() or {}) or self.hl -- self raw hl - local prev_hl = self:nonlocal("cur_hl") -- the parent hl + local parent_hl = self:nonlocal("merged_hl") - if prev_hl.force then - self.cur_hl = vim.tbl_extend("keep", prev_hl, hl) -- merged hl + if parent_hl.force then + self.merged_hl = vim.tbl_extend("keep", parent_hl, hl) else - self.cur_hl = vim.tbl_extend("force", prev_hl, hl) -- merged hl + self.merged_hl = vim.tbl_extend("force", parent_hl, hl) end if self.provider then local provider_str = type(self.provider) == "function" and (self:provider() or "") or (self.provider or "") - local hl_str_start, hl_str_end = hi.eval_hl(self.cur_hl) + local hl_str_start, hl_str_end = hi.eval_hl(self.merged_hl) table.insert(stl, hl_str_start .. provider_str .. hl_str_end) end - for _, child in ipairs(self) do + local children_i + if self.pick_child then + children_i = self.pick_child + else + children_i = {} + for i, _ in ipairs(self) do + table.insert(children_i, i) + end + end + + for _, i in ipairs(children_i) do + local child = self[i] local out = child:eval() table.insert(stl, out) - if self.stop_at_first and out ~= "" then - break - end end - return table.concat(stl, "") + self.stl = table.concat(stl, "") + + return self.stl end return StatusLine diff --git a/lua/heirline/utils.lua b/lua/heirline/utils.lua index b3a418b..8997182 100644 --- a/lua/heirline/utils.lua +++ b/lua/heirline/utils.lua @@ -82,11 +82,165 @@ end function M.insert(destination, ...) local children = { ... } local new = M.clone(destination) - for i, child in ipairs(children) do + for _, child in ipairs(children) do local new_child = M.clone(child) table.insert(new, new_child) end return new end +function M.count_chars(str) + return vim.api.nvim_eval_statusline(str, { winid = 0, maxwidth = 0 }).width +end + +function M.make_flexible_component(priority, ...) + local new = M.insert({}, ...) + + new.static = { + priority = priority, + } + new.init = function(self) + if not vim.tbl_contains(self.flexible_components, self) then + table.insert(self.flexible_components, self) + end + self:set_win_attr("win_child_index", nil, 1) + self.pick_child = { self:get_win_attr("win_child_index") } + end + new.restrict = { win_child_index = true } + + return new +end + +local function next_child(self) + local pi = self:get_win_attr("win_child_index") + 1 + if pi > #self then + return false + end + self:set_win_attr("win_child_index", pi) + return true +end + +local function prev_child(self) + local pi = self:get_win_attr("win_child_index") - 1 + if pi < 1 then + return false + end + self:set_win_attr("win_child_index", pi) + return true +end + +local function is_child(child, parent) + if not (child and parent) then + return false + end + if #child.id <= #parent.id then + return false + end + for i, v in ipairs(parent.id) do + if child.id[i] ~= v then + return false + end + end + return true +end + +local function group_flexible_components(statusline, mode) + local priority_groups = {} + local priorities = {} + local cur_priority + local prev_component + + for _, component in ipairs(statusline.flexible_components) do + local priority + if prev_component and is_child(component, prev_component) then + priority = cur_priority + mode + -- if mode == -1 then + -- priority = ec.priority < cur_priority + mode and ec.priority or cur_priority + mode + -- elseif mode == 1 then + -- priority = ec.priority > cur_priority + mode and ec.priority or cur_priority + mode + -- end + else + priority = component.priority + end + + prev_component = component + cur_priority = priority + + priority_groups[priority] = priority_groups[priority] or {} + table.insert(priority_groups[priority], component) + if not priorities[priority] then + table.insert(priorities, priority) + end + end + return priority_groups, priorities +end + +function M.expand_or_contract_flexible_components(statusline, out) + if not statusline.flexible_components or not next(statusline.flexible_components) then + return + end + + local winw = vim.api.nvim_win_get_width(0) + + local stl_len = M.count_chars(out) + + if stl_len > winw then + local priority_groups, priorities = group_flexible_components(statusline, -1) + table.sort(priorities, function(a, b) + return a < b + end) + + local saved_chars = 0 + + for _, p in ipairs(priorities) do + for _, component in ipairs(priority_groups[p]) do + -- try increasing the child index and return success + if next_child(component) then + local prev_len = M.count_chars(component.stl) + local cur_len = M.count_chars(component:eval()) + saved_chars = saved_chars + (prev_len - cur_len) + end + end + + if stl_len - saved_chars <= winw then + break + end + end + elseif stl_len < winw then + local gained_chars = 0 + + local priority_groups, priorities = group_flexible_components(statusline, 1) + table.sort(priorities, function(a, b) + return a > b + end) + + for _, p in ipairs(priorities) do + for _, component in ipairs(priority_groups[p]) do + if prev_child(component) then + local prev_len = M.count_chars(component.stl) + local cur_len = M.count_chars(component:eval()) + gained_chars = gained_chars + (cur_len - prev_len) + end + end + + if stl_len + gained_chars > winw then + for _, component in ipairs(priority_groups[p]) do + next_child(component) + end + break + end + end + end +end + +function M.pick_child_on_condition(self) + self.pick_child = {} + for i, child in ipairs(self) do + if not child.condition or child:condition() then + table.insert(self.pick_child, i) + break + end + end +end + return M