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