neovim implementation
Highlights
I had to figure out a few things for this:
- Dealing with timers (
vim.uv.timer_*) - Setting the statusline (
vim.o.statusline = ...) - Some squirrely config injection solved using closures
- Calling
vim.cmdfrom inside of a timer (hint, usevim.scheduleto politely ask neovim to process a function inside of the main event loop)
Implementation
This uses the lua interface for neovim, it's contained in three files:
- lua/bespoke/pomodoro.lua
- lua/bespoke/statusline.lua
- init.lua
-- lua/bespoke/pomodoro.lua
local M = {}
M.want = function(name)
local out; if xpcall(
function() out = require(name) end,
function(e) out = e end)
then return out -- success
else return nil, out end -- error
end
local STOPPED = 0
local RUNNING = 1
M.data = {
notifier = "notify-send",
status = STOPPED,
seconds_left = 0,
timer = nil,
timer_type = "MAIN" -- or "BREAK"
}
M.finish = function()
M.notify("timer finished")
M.data.status = STOPPED
M.data.seconds_left = 0
end
M.start = function(seconds)
if M.data.status == RUNNING then
vim.notify("timer already running")
else
M.data.status = RUNNING
M.data.seconds_left = seconds
M.data.timer = vim.uv.new_timer()
M.notify("timer started")
M.data.timer:start(1000, 0, function()
if M.data.seconds_left < 0 then
M.data.timer:stop()
M.data.status = STOPPED
M.notify("timer finished")
elseif M.data.status == RUNNING then
M.data.seconds_left = M.data.seconds_left - 1
end
end)
M.data.timer:set_repeat(1000)
end
end
M.notify = function(msg)
vim.system({M.data.notifier, msg})
end
M.pause = function()
M.data.status = STOPPED
M.notify("timer paused")
end
M.string = function()
local sec = M.data.seconds_left
local stat = ""
if M.data.status == RUNNING then
stat = "running: "
else
stat = "stopped: "
end
return stat .. string.format("%02.0f:%02.0f", sec / 60, sec % 60)
end
M.toggle = function()
if M.data.status == RUNNING then
M.data.status = STOPPED
else
M.start(25 * 60)
end
end
M.configure = function()
local statusline = M.want("bespoke.statusline")
if statusline then
vim.notify("registering statusline callback")
statusline.add_callback(M.string)
end
local wk = M.want("which-key")
if wk then
wk.add({
{"<leader>p", group = "pomo" },
{"<leader>ps", function() M.start(25 * 60) end, desc = "start main timer"},
{"<leader>pb", function() M.start(5 * 60) end, desc = "start break timer"},
{"<leader>pt", function() M.pause() end, desc = "pause timer"},
{"<leader>pk", function() M.finish() end, desc = "kill timer"},
})
end
end
return M
I also ended up writing some statusline logic
-- lua/bespoke/statusline.lua
local M = {}
M.data = {
left = "%<%f %h%w%m%r",
right = "%-14.(%l,%c%V%) %P",
callbacks = {}, -- callbacks which evaluate to strings. Appends to right-side.
timer = nil,
computed = nil
}
M.eval = function()
local result = ""
for _, v in ipairs(M.data.callbacks) do
result = result .. " " .. v()
end
return result
end
M.configure = function()
M.data.computed = table.concat({
M.data.left,
"%=",
"%{%v:lua.statusline.eval()%} :: ",
M.data.right
},'')
M.data.timer = vim.uv.new_timer()
M.data.timer:start(1000, 0, function()
vim.schedule(function() vim.cmd("redrawstatus") end)
end)
M.data.timer:set_repeat(1000)
vim.o.statusline = M.data.computed
end
M.add_callback = function(cb)
 table.insert(M.data.callbacks, cb)
end
return M
And to bring it all together, here's the snippet of my init.lua
-- init.lua statusline = require "bespoke.statusline" pomodoro = require "bespoke.pomodoro" -- ... snip, configure whichkey plugin here ... -- pomodoro.configure() -- registers callback with statusline, sets up keybinds statusline.configure() -- registers statusline refresh loop