馃 a tiny, customizable statusline for neovim
1local log = require("lylla.log")
2local utils = require("lylla.utils")
3
4---@class lylla.proto
5---@field wins table<integer, table>
6---@field win integer
7---@field modules any[]
8---@field winbar any[]
9---@field timer uv.uv_timer_t
10---@field refreshau integer
11local statusline = {}
12
13statusline.wins = {}
14
15---@class lylla.proto
16---@field new fun(self, win): lylla.proto
17function statusline:new(win)
18 if statusline.wins[win] then
19 statusline.wins[win]:close()
20 end
21 local stl = setmetatable({
22 win = win,
23 modules = vim.deepcopy(require("lylla.config").get().modules, true),
24 winbar = vim.deepcopy(require("lylla.config").get().winbar, true),
25 }, { __index = statusline })
26 statusline.wins[win] = stl
27 return stl
28end
29
30---@class lylla.proto
31---@field init fun(self)
32function statusline:init()
33 local err, err_kind
34 ---@diagnostic disable-next-line: assign-type-mismatch
35 self.timer, err, err_kind = vim.uv.new_timer()
36 if not self.timer or err then
37 vim.api.nvim_echo({ { err_kind }, { "\n\t" }, { err } }, true, { err = true })
38 return
39 end
40
41 local refresh = require("lylla.config").get().refresh_rate
42 self.timer:start(0, refresh, function()
43 self:refresh()
44 end)
45
46 self.refreshau = vim.api.nvim_create_autocmd(require("lylla.config").get().events, {
47 group = vim.api.nvim_create_augroup("lylla:refresh", { clear = false }),
48 callback = function(ev)
49 self:refresh(ev)
50 end,
51 })
52end
53
54---@class lylla.proto
55---@field close fun(self)
56function statusline:close()
57 self.timer:stop()
58 self.timer:close()
59 vim.api.nvim_del_autocmd(self.refreshau)
60 statusline.wins[self.win] = nil
61end
62
63local function refreshcomponent(self, fn, ev)
64 do
65 local ok, result = pcall(fn, self, ev)
66 if not ok then
67 log.error("[lylla] error occured on refresh:\n\t" .. result)
68 end
69 end
70end
71
72---@class lylla.proto
73---@field refresh fun(self, ev?: vim.api.keyset.create_autocmd.callback_args)
74function statusline:refresh(ev)
75 vim.schedule(function()
76 if not vim.api.nvim_win_is_valid(self.win) then
77 return
78 end
79
80 refreshcomponent(self, statusline.set, ev)
81 refreshcomponent(self, statusline.setwinbar, ev)
82 end)
83end
84
85---@class lylla.proto
86---@field fold fun(self, ev?: vim.api.keyset.create_autocmd.callback_args, modules: any[]): string
87function statusline:fold(ev, modules)
88 if type(modules) ~= "table" or modules == nil then
89 return ""
90 end
91
92 local lst = vim
93 .iter(ipairs(modules))
94 :map(function(_, module)
95 if type(module) == "table" and module.fn and type(module.fn) == "function" then
96 if module.opts and module.opts.events then
97 -- refresh from timer
98 if not ev and module.prev then
99 return module.prev
100 end
101 -- refresh from non-match event
102 if ev and not vim.tbl_contains(module.opts.events, ev.event) and module.prev then
103 return module.prev
104 end
105 end
106 do
107 local ok, result = pcall(module.fn)
108 if not ok then
109 error(result)
110 end
111 module.prev = result
112 end
113 return module.prev
114 end
115 if type(module) == "function" then
116 local ok, result = pcall(module)
117 if not ok then
118 error(result)
119 end
120 return result
121 end
122 return module
123 end)
124 :totable()
125 lst = utils.flatten(lst, 1)
126 return vim.iter(lst):fold("", function(str, module)
127 if type(module) == "string" and #module > 0 then
128 return str .. module
129 end
130 if type(module) ~= "table" or #module == 0 then
131 return str
132 end
133 local text = module[1]
134 if text == nil or type(text) ~= "string" or #text == 0 then
135 return str
136 end
137 local hl = module[2]
138 if hl and type(hl) == "string" and #hl > 0 then
139 return str .. "%#" .. hl .. "#" .. text .. "%*"
140 end
141 return str .. "%*" .. text
142 end)
143end
144
145---@class lylla.proto
146---@field get fun(self, ev?: vim.api.keyset.create_autocmd.callback_args)
147function statusline:get(ev)
148 return self:fold(ev, self.modules)
149end
150
151---@class lylla.proto
152---@field getwinbar fun(self, ev?: vim.api.keyset.create_autocmd.callback_args)
153function statusline:getwinbar(ev)
154 return self:fold(ev, self.winbar)
155end
156
157---@class lylla.proto
158---@field setwinbar fun(self, ev?: vim.api.keyset.create_autocmd.callback_args)
159function statusline:setwinbar(ev)
160 local buf = vim.api.nvim_win_get_buf(self.win)
161 if vim.bo[buf].buftype ~= "" then
162 return
163 end
164
165 local ok, result = pcall(vim.api.nvim_win_call, self.win, function()
166 return self:getwinbar(ev)
167 end)
168 assert(ok, string.format("error occured while trying to evaluate winbar:\n\t%s", result))
169
170 vim.wo[self.win].winbar = result
171end
172
173---@class lylla.proto
174---@field set fun(self, ev?: vim.api.keyset.create_autocmd.callback_args)
175function statusline:set(ev)
176 local ok, result = pcall(vim.api.nvim_win_call, self.win, function()
177 return self:get(ev)
178 end)
179 assert(ok, string.format("error occured while trying to evaluate statusline:\n\t%s", result))
180
181 vim.wo[self.win].statusline = result
182end
183
184return statusline