馃 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(win: integer): 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 try_new fun(win: integer?): lylla.proto
32function statusline.try_new(win)
33 win = win or vim.api.nvim_get_current_win()
34
35 if statusline.wins[win] then
36 return statusline.wins[win]
37 end
38 return statusline.new(win)
39end
40
41---@class lylla.proto
42---@field init fun(self)
43function statusline:init()
44 if self.initialized then
45 return
46 end
47
48 local err, err_kind
49 ---@diagnostic disable-next-line: assign-type-mismatch
50 self.timer, err, err_kind = vim.uv.new_timer()
51 if not self.timer or err then
52 vim.api.nvim_echo({ { err_kind }, { "\n\t" }, { err } }, true, { err = true })
53 return
54 end
55
56 local refresh = require("lylla.config").get().refresh_rate
57 self.timer:start(0, refresh, function()
58 self:refresh()
59 end)
60
61 self.refreshau = vim.api.nvim_create_augroup(("@lylla.refresh.%d"):format(self.win), { clear = true })
62
63 local events = self:getevents()
64
65 for i = 1, #events do
66 local event = events[i]
67 local eventname, eventpattern = unpack(vim.split(event, " "), 1, 2)
68 vim.api.nvim_create_autocmd(eventname, {
69 group = self.refreshau,
70 pattern = eventpattern,
71 callback = function(ev)
72 self:refresh(ev)
73 end,
74 })
75 end
76
77 self.initialized = true
78end
79
80---@class lylla.proto
81---@field close fun(self)
82function statusline:close()
83 self.timer:stop()
84 self.timer:close()
85 pcall(vim.api.nvim_del_augroup_by_id, self.refreshau)
86 statusline.wins[self.win] = nil
87end
88
89---@class lylla.proto
90---@field getevents fun(self): string[]
91function statusline:getevents()
92 local t = vim.iter(ipairs(self.modules)):fold({}, function(acc, _, module)
93 if type(module) == "table" and module.fn and type(module.fn) == "function" then
94 if module.opts and module.opts.events then
95 return vim.iter(module.opts.events):fold(acc, function(a, event)
96 a[event] = true
97 return a
98 end)
99 end
100 end
101 return acc
102 end)
103
104 return vim.tbl_keys(t)
105end
106
107local function refreshcomponent(self, fn, ev)
108 do
109 local ok, result = pcall(fn, self, ev)
110 if not ok then
111 log.error("[lylla] error occured on refresh:\n\t" .. result)
112 end
113 end
114end
115
116---@class lylla.proto
117---@field refresh fun(self, ev?: vim.api.keyset.create_autocmd.callback_args)
118function statusline:refresh(ev)
119 vim.schedule(function()
120 if not vim.api.nvim_win_is_valid(self.win) then
121 return
122 end
123
124 refreshcomponent(self, statusline.set, ev)
125 refreshcomponent(self, statusline.setwinbar, ev)
126 end)
127end
128
129---@class lylla.proto
130---@field fold fun(self, ev?: vim.api.keyset.create_autocmd.callback_args, modules: any[]): string
131function statusline:fold(ev, modules)
132 if type(modules) ~= "table" or modules == nil then
133 return ""
134 end
135
136 local lst = vim
137 .iter(ipairs(modules))
138 :map(function(_, module)
139 if type(module) == "table" and module.fn and type(module.fn) == "function" then
140 if module.opts and module.opts.events then
141 -- refresh from timer
142 if not ev and module.prev then
143 return module.prev
144 end
145 -- refresh from non-match event
146 if ev and not vim.tbl_contains(module.opts.events, ev.event) and module.prev then
147 return module.prev
148 end
149 end
150 do
151 local ok, result = pcall(module.fn)
152 if not ok then
153 error(result)
154 end
155 module.prev = result
156 end
157 return module.prev
158 end
159 if type(module) == "function" then
160 local ok, result = pcall(module)
161 if not ok then
162 error(result)
163 end
164 return result
165 end
166 return module
167 end)
168 :totable()
169 return utils.fold(lst)
170end
171
172---@class lylla.proto
173---@field get fun(self, ev?: vim.api.keyset.create_autocmd.callback_args)
174function statusline:get(ev)
175 return self:fold(ev, self.modules)
176end
177
178---@class lylla.proto
179---@field getwinbar fun(self, ev?: vim.api.keyset.create_autocmd.callback_args)
180function statusline:getwinbar(ev)
181 return self:fold(ev, self.winbar)
182end
183
184---@class lylla.proto
185---@field setwinbar fun(self, ev?: vim.api.keyset.create_autocmd.callback_args)
186function statusline:setwinbar(ev)
187 local buf = vim.api.nvim_win_get_buf(self.win)
188 if vim.bo[buf].buftype ~= "" then
189 return
190 end
191
192 local ok, result = pcall(vim.api.nvim_win_call, self.win, function()
193 return self:getwinbar(ev)
194 end)
195 assert(ok, string.format("error occured while trying to evaluate winbar:\n\t%s", result))
196
197 vim.wo[self.win].winbar = result
198end
199
200---@class lylla.proto
201---@field set fun(self, ev?: vim.api.keyset.create_autocmd.callback_args)
202function statusline:set(ev)
203 local ok, result = pcall(vim.api.nvim_win_call, self.win, function()
204 return self:get(ev)
205 end)
206 assert(ok, string.format("error occured while trying to evaluate statusline:\n\t%s", result))
207
208 vim.wo[self.win].statusline = result
209end
210
211return statusline