🪴 a tiny, customizable statusline for neovim
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: init

robin fa624f55

+628
+21
.nvim.lua
··· 1 + R = function(m, ...) 2 + require("plenary.reload").reload_module(m, ...) 3 + return require(m) 4 + end 5 + 6 + vim.opt.rtp:prepend "." 7 + 8 + -- 9 + 10 + pcall(function() 11 + require("lylla").setup() 12 + end) 13 + 14 + -- 15 + 16 + vim.api.nvim_create_autocmd("BufWritePost", { 17 + pattern = "*/lylla/*", 18 + callback = function() 19 + vim.schedule(function() R "lylla".init() end) 20 + end, 21 + })
+157
lua/lylla/config.lua
··· 1 + ---@module 'lylla.config' 2 + 3 + ---@class lylla.config 4 + ---@field refresh_rate integer 5 + ---@field events string[] 6 + ---@field prefix string 7 + ---@field hls table<'normal'|'visual'|'command'|'insert', vim.api.keyset.highlight> 8 + ---@field modules any[] 9 + 10 + local utils = require("lylla.utils") 11 + 12 + local M = {} 13 + 14 + ---@param fn fun(): string[] 15 + ---@param opts? { events: string[] } 16 + ---@return table 17 + local function component(fn, opts) 18 + local t = { _type = "component" } 19 + t.fn = fn 20 + t.opts = opts 21 + return t 22 + end 23 + 24 + ---@type lylla.config 25 + ---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) 26 + ---@text # Default ~ 27 + M.default = { 28 + refresh_rate = 300, 29 + events = { 30 + "WinEnter", 31 + "BufEnter", 32 + "BufWritePost", 33 + "SessionLoadPost", 34 + "FileChangedShellPost", 35 + "VimResized", 36 + "Filetype", 37 + "CursorMoved", 38 + "CursorMovedI", 39 + "ModeChanged", 40 + "CmdlineEnter", 41 + }, 42 + prefix = "▌", 43 + hls = {}, 44 + modules = { 45 + component(function() 46 + local prefix = require('lylla.config').get().prefix 47 + local modehl = utils.get_modehl() 48 + return { 49 + { prefix, modehl }, 50 + { "[" .. vim.api.nvim_get_mode().mode .. "]", modehl }, 51 + } 52 + end, { 53 + events = { "ModeChanged", "CmdlineEnter" }, 54 + }), 55 + { " " }, 56 + component(function() 57 + return { 58 + utils.getfilepath(), 59 + utils.getfilename(), 60 + { " " }, 61 + } 62 + end, { 63 + events = { 64 + "WinEnter", 65 + "BufEnter", 66 + "BufWritePost", 67 + "FileChangedShellPost", 68 + "Filetype", 69 + }, 70 + }), 71 + { " " }, 72 + component(function() 73 + return { utils.get_searchcount() } 74 + end), 75 + { "%=" }, 76 + {}, 77 + { "%=" }, 78 + component(function() 79 + return { 80 + { { "lsp :: " }, { utils.get_client() or "none" } }, 81 + { " | ", "NonText" }, 82 + { { "fmt :: " }, { utils.get_fmt() or "none" } }, 83 + { " | ", "NonText" }, 84 + } 85 + end, { events = { "FileType" } }), 86 + { "%p%%" }, 87 + { " | ", "NonText" }, 88 + { "%L lines" }, 89 + { " | ", "NonText" }, 90 + { "%l:%c" }, 91 + { " " }, 92 + }, 93 + } 94 + 95 + ---@type lylla.config 96 + ---@diagnostic disable-next-line: missing-fields 97 + M.config = {} 98 + 99 + ---@private 100 + ---@generic T: table|any[] 101 + ---@param tdefault T 102 + ---@param toverride T 103 + ---@return T 104 + local function tmerge(tdefault, toverride) 105 + if toverride == nil then 106 + return tdefault 107 + end 108 + 109 + if vim.islist(tdefault) then 110 + return toverride 111 + end 112 + if vim.tbl_isempty(tdefault) then 113 + return toverride 114 + end 115 + 116 + return vim.iter(pairs(tdefault)):fold({}, function(tnew, k, v) 117 + if toverride[k] == nil or type(v) ~= type(toverride[k]) then 118 + tnew[k] = v 119 + return tnew 120 + end 121 + if type(v) == 'table' then 122 + tnew[k] = tmerge(v, toverride[k]) 123 + return tnew 124 + end 125 + 126 + tnew[k] = toverride[k] 127 + return tnew 128 + end) 129 + end 130 + 131 + ---@param tdefault lylla.config 132 + ---@param toverride lylla.config 133 + ---@return lylla.config 134 + function M.merge(tdefault, toverride) 135 + if vim.fn.has('nvim-0.11.0') == 1 then 136 + toverride = vim.tbl_deep_extend('keep', toverride, { editor = { float = { solid_border = vim.o.winborder == 'solid' } } }) 137 + end 138 + return tmerge(tdefault, toverride) 139 + end 140 + 141 + ---@return lylla.config 142 + function M.get() 143 + return M.merge(M.default, M.config) 144 + end 145 + 146 + ---@param cfg lylla.config 147 + ---@return lylla.config 148 + function M.override(cfg) 149 + return M.merge( M.default, cfg) 150 + end 151 + 152 + ---@param cfg lylla.config 153 + function M.set(cfg) 154 + M.config = cfg 155 + end 156 + 157 + return M
+72
lua/lylla/init.lua
··· 1 + local M = {} 2 + 3 + ---@type table<'normal'|'visual'|'command'|'insert', vim.api.keyset.highlight> 4 + local default_hls = { 5 + normal = { link = "MiniIconsAzure" }, 6 + visual = { link = "MiniIconsPurple" }, 7 + command = { link = "MiniIconsOrange" }, 8 + insert = { link = "MiniIconsGrey" }, 9 + } 10 + 11 + function M.inithls() 12 + local utils = require("lylla.utils") 13 + vim.iter(pairs(default_hls)):each(function(mode, defaulthl) 14 + local name = utils.get_modehl_name(mode) 15 + 16 + local hl = require("lylla.config").get().hls[mode] 17 + if hl then 18 + vim.api.nvim_set_hl(0, name, hl) 19 + return 20 + end 21 + 22 + if vim.tbl_isempty(vim.api.nvim_get_hl(0, { name = name })) then 23 + vim.api.nvim_set_hl(0, name, defaulthl) 24 + end 25 + end) 26 + end 27 + 28 + function M.resethl() 29 + local utils = require("lylla.utils") 30 + vim.iter(pairs(default_hls)):each(function(mode, _) 31 + local name = utils.get_modehl_name(mode) 32 + vim.api.nvim_set_hl(0, name, {}) 33 + end) 34 + end 35 + 36 + function M.init() 37 + vim.api.nvim_create_autocmd("WinNew", { 38 + group = vim.api.nvim_create_augroup("lylla:statusline:new", { clear = true }), 39 + callback = function() 40 + local win = vim.api.nvim_get_current_win() 41 + require("lylla.statusline"):new(win):init() 42 + end, 43 + }) 44 + local win = vim.api.nvim_get_current_win() 45 + require("lylla.statusline"):new(win):init() 46 + 47 + vim.api.nvim_create_autocmd("WinClosed", { 48 + group = vim.api.nvim_create_augroup("lylla:close", { clear = true }), 49 + callback = function(ev) 50 + local stl = require("lylla.statusline").wins[ev.match] 51 + if stl then 52 + stl:close() 53 + end 54 + end, 55 + }) 56 + 57 + vim.api.nvim_create_autocmd("ColorSchemePre", { 58 + group = vim.api.nvim_create_augroup("lylla:resethl", { clear = true }), 59 + callback = function() 60 + M.resethl() 61 + end, 62 + }) 63 + vim.api.nvim_create_autocmd("ColorScheme", { 64 + group = vim.api.nvim_create_augroup("lylla:inithls", { clear = true }), 65 + callback = function() 66 + M.inithls() 67 + end, 68 + }) 69 + M.inithls() 70 + end 71 + 72 + return M
+128
lua/lylla/statusline.lua
··· 1 + local utils = require("lylla.utils") 2 + 3 + ---@class lylla.proto 4 + ---@field wins table<integer, table> 5 + ---@field win integer 6 + ---@field modules any[] 7 + ---@field timer uv.uv_timer_t 8 + ---@field refreshau integer 9 + local statusline = {} 10 + 11 + statusline.wins = {} 12 + 13 + ---@class lylla.proto 14 + ---@field new fun(self, win): lylla.proto 15 + function statusline:new(win) 16 + if statusline.wins[win] then 17 + statusline.wins[win]:close() 18 + end 19 + local stl = setmetatable({ 20 + win = win, 21 + modules = vim.deepcopy(require("lylla.config").get().modules, true), 22 + }, { __index = statusline }) 23 + statusline.wins[win] = stl 24 + return stl 25 + end 26 + 27 + ---@class lylla.proto 28 + ---@field init fun(self) 29 + function statusline:init() 30 + local err, err_kind 31 + ---@diagnostic disable-next-line: assign-type-mismatch 32 + self.timer, err, err_kind = vim.uv.new_timer() 33 + if not self.timer or err then 34 + vim.api.nvim_echo({ { err_kind }, { "\n\t" }, { err } }, true, { err = true }) 35 + return 36 + end 37 + 38 + local refresh = require("lylla.config").get().refresh_rate 39 + self.timer:start(0, refresh, function() 40 + self:refresh() 41 + end) 42 + 43 + self.refreshau = vim.api.nvim_create_autocmd(require("lylla.config").get().events, { 44 + group = vim.api.nvim_create_augroup("lylla:refresh", { clear = false }), 45 + callback = function(ev) 46 + self:refresh(ev) 47 + end, 48 + }) 49 + end 50 + 51 + ---@class lylla.proto 52 + ---@field close fun(self) 53 + function statusline:close() 54 + self.timer:stop() 55 + self.timer:close() 56 + vim.api.nvim_del_autocmd(self.refreshau) 57 + statusline.wins[self.win] = nil 58 + end 59 + 60 + ---@class lylla.proto 61 + ---@field refresh fun(self, ev?: vim.api.keyset.create_autocmd.callback_args) 62 + function statusline:refresh(ev) 63 + vim.schedule(function() 64 + if vim.o.cmdheight == 0 and vim.api.nvim_get_mode().mode == "c" then 65 + return 66 + end 67 + 68 + if not vim.api.nvim_win_is_valid(self.win) then 69 + return 70 + end 71 + 72 + local ok, result = pcall(vim.api.nvim_win_call, self.win, function() 73 + return self:get(ev) 74 + end) 75 + if not ok then 76 + return 77 + end 78 + 79 + vim.wo[self.win].statusline = result 80 + end) 81 + end 82 + 83 + ---@class lylla.proto 84 + ---@field fold fun(self, ev?: vim.api.keyset.create_autocmd.callback_args): string 85 + function statusline:fold(ev) 86 + local modules = vim 87 + .iter(ipairs(self.modules)) 88 + :map(function(i, module) 89 + if type(module) == "table" and module._type == "component" then 90 + if module.opts and module.opts.events then 91 + -- refresh from timer 92 + if not ev and module.prev then 93 + return module.prev 94 + end 95 + -- refresh from non-match event 96 + if ev and not vim.tbl_contains(module.opts.events, ev.event) and module.prev then 97 + return module.prev 98 + end 99 + end 100 + module.prev = module.fn() 101 + return module.prev 102 + end 103 + if type(module) == "function" then 104 + module = module() 105 + end 106 + return module 107 + end) 108 + :totable() 109 + modules = utils.flatten(modules, 1) 110 + return vim.iter(modules):fold("", function(str, module) 111 + if type(module) ~= "table" or #module == 0 then 112 + return str 113 + end 114 + local text = module[1] 115 + if #module > 1 then 116 + return str .. "%#" .. module[2] .. "#" .. text .. "%*" 117 + end 118 + return str .. "%*" .. text 119 + end) 120 + end 121 + 122 + ---@class lylla.proto 123 + ---@field get fun(self, ev?: vim.api.keyset.create_autocmd.callback_args) 124 + function statusline:get(ev) 125 + return self:fold(ev) 126 + end 127 + 128 + return statusline
+166
lua/lylla/utils.lua
··· 1 + local utils = {} 2 + 3 + function utils.get_client() 4 + local buf_ft = vim.api.nvim_get_option_value("filetype", { buf = 0 }) 5 + local result = vim.iter(vim.lsp.get_clients({ bufnr = 0 })):find(function( 6 + client --[[@as vim.lsp.Client]] 7 + ) 8 + return vim.iter(ipairs(client.config.filetypes)):any(function(_, ft) 9 + return ft == buf_ft 10 + end) 11 + end) 12 + if result then 13 + return result.config.name 14 + end 15 + end 16 + 17 + --- flatten list so all children have level of depth 18 + ---@param lst table 19 + ---@param maxdepth integer 20 + function utils.flatten(lst, maxdepth) 21 + ---@param _t any[] 22 + ---@return integer 23 + local function _depth(_t) 24 + return vim.iter(_t):fold(1, function(maxd, v) 25 + if type(v) == "table" then 26 + local d = 1 + _depth(v) 27 + if d > maxd then 28 + return d 29 + end 30 + end 31 + return maxd 32 + end) 33 + end 34 + 35 + local result = {} 36 + ---@param _t any[] 37 + local function _flatten(_t) 38 + local n = #_t 39 + for i = 1, n do 40 + local v = _t[i] 41 + if type(v) ~= "table" or _depth(v) <= maxdepth then 42 + table.insert(result, v) 43 + else 44 + _flatten(v) 45 + end 46 + end 47 + end 48 + _flatten(lst) 49 + return result 50 + end 51 + 52 + function utils.getfilename() 53 + local _, default_file_hl = require("mini.icons").get("default", "file") 54 + 55 + local name = vim.fn.expand("%:t") 56 + 57 + local file_icon_raw, file_icon_hl 58 + 59 + if vim.bo.buftype ~= "" then 60 + local filetype = vim.bo.filetype 61 + file_icon_raw, file_icon_hl = require("mini.icons").get("filetype", filetype) 62 + else 63 + file_icon_raw, file_icon_hl = require("mini.icons").get("file", name) 64 + end 65 + 66 + return { { name, default_file_hl }, { " " }, { file_icon_raw, file_icon_hl } } 67 + end 68 + 69 + function utils.getfilepath() 70 + local path = vim.fn.expand("%:p:~:.") 71 + 72 + local file_path_list = {} 73 + local _ = string.gsub(path, "[^/]+", function(w) 74 + table.insert(file_path_list, w) 75 + end) 76 + 77 + local filepath = vim.iter(ipairs(file_path_list)):fold("", function(acc, i, fragment) 78 + if i == #file_path_list then 79 + return acc 80 + end 81 + acc = acc .. fragment .. "/" 82 + return acc 83 + end) 84 + 85 + return { filepath, "Directory" } 86 + end 87 + 88 + function utils.get_searchcount() 89 + local result = vim.fn.searchcount({ recompute = 1 }) 90 + if vim.v.hlsearch ~= 1 then 91 + return "" 92 + end 93 + if vim.tbl_isempty(result) then 94 + return "" 95 + end 96 + local term = vim.fn.getreg("/") 97 + local display 98 + if result.incomplete == 1 then 99 + -- timed out 100 + display = "[?/??]" 101 + elseif result.incomplete == 2 then 102 + -- max count exceeded 103 + if result.total > result.maxcount and result.current > result.maxcount then 104 + display = string.format("[>%d/>%d]", result.current, result.total) 105 + elseif result.total > result.maxcount then 106 + display = string.format("[%d/>%d]", result.current, result.total) 107 + end 108 + end 109 + display = display or string.format("[%d/%d]", result.current, result.total) 110 + 111 + return { { string.format("/%s", term), "IncSearch" }, { " " }, { display, "MsgSeparator" } } 112 + end 113 + 114 + function utils.get_fmt() 115 + local filetype = vim.bo.filetype 116 + if not filetype then 117 + return 118 + end 119 + local formatters = require("mossy.filetype").get(filetype, "formatting") 120 + if #formatters == 0 then 121 + return 122 + end 123 + 124 + local fmt = vim.iter(formatters):find(function(formatter) 125 + if formatter.cond and not formatter.cond({ buf = 0 }) then 126 + return false 127 + end 128 + 129 + if formatter.cmd and vim.fn.executable(formatter.cmd) == 0 then 130 + return false 131 + end 132 + return true 133 + end) 134 + return fmt and fmt.name or nil 135 + end 136 + 137 + ---@param mode string 138 + ---@return string 139 + function utils.get_modehl_name(mode) 140 + return "@lylla." .. mode 141 + end 142 + 143 + ---@return string 144 + function utils.get_modehl() 145 + local mode = vim.api.nvim_get_mode().mode 146 + 147 + if string.match(mode, "n") then 148 + return utils.get_modehl_name("normal") 149 + end 150 + 151 + if string.match(mode, "[vVs]") then 152 + return utils.get_modehl_name("visual") 153 + end 154 + 155 + if string.match(mode, "c") then 156 + return utils.get_modehl_name("command") 157 + end 158 + 159 + if string.match(mode, "[irRt]") then 160 + return utils.get_modehl_name("insert") 161 + end 162 + 163 + return mode 164 + end 165 + 166 + return utils
+16
plugin/lylla.lua
··· 1 + if vim.g.loaded_statusline then 2 + return 3 + end 4 + 5 + vim.g.loaded_statusline = true 6 + 7 + if vim.v.vim_did_enter > 0 then 8 + require("lylla").init() 9 + else 10 + vim.api.nvim_create_autocmd("VimEnter", { 11 + group = vim.api.nvim_create_augroup("lylla:init", { clear = true }), 12 + callback = function() 13 + require("lylla").init() 14 + end, 15 + }) 16 + end
+4
selene.toml
··· 1 + std="vim" 2 + 3 + [rules] 4 + mixed_table = "allow"
+9
stylua.toml
··· 1 + indent_type = "Spaces" 2 + indent_width = 2 3 + column_width = 120 4 + quote_style = "AutoPreferDouble" 5 + call_parentheses = "Always" 6 + line_endings = "Unix" 7 + 8 + [sort_requires] 9 + enabled = true
+55
vim.toml
··· 1 + [selene] 2 + base = "lua51" 3 + name = "vim" 4 + 5 + [vim] 6 + any = true 7 + 8 + [[describe.args]] 9 + type = "string" 10 + [[describe.args]] 11 + type = "function" 12 + 13 + [[it.args]] 14 + type = "string" 15 + [[it.args]] 16 + type = "function" 17 + 18 + [[before_each.args]] 19 + type = "function" 20 + [[after_each.args]] 21 + type = "function" 22 + 23 + [assert.is_not] 24 + any = true 25 + 26 + [assert.matches] 27 + any = true 28 + 29 + [assert.has_error] 30 + any = true 31 + 32 + [[assert.equals.args]] 33 + type = "any" 34 + [[assert.equals.args]] 35 + type = "any" 36 + [[assert.equals.args]] 37 + type = "any" 38 + required = false 39 + 40 + [[assert.same.args]] 41 + type = "any" 42 + [[assert.same.args]] 43 + type = "any" 44 + 45 + [[assert.truthy.args]] 46 + type = "any" 47 + 48 + [[assert.falsy.args]] 49 + type = "any" 50 + 51 + [[assert.spy.args]] 52 + type = "any" 53 + 54 + [[assert.stub.args]] 55 + type = "any"