Added simple stopwatch as with <leader>S prefix

This commit is contained in:
Lüdecke Sascha
2026-07-13 10:57:20 +02:00
parent da729abfc5
commit b9dfccaa43
3 changed files with 247 additions and 0 deletions

231
lua/stopwatch.lua Normal file
View File

@@ -0,0 +1,231 @@
-- ~/.config/nvim/lua/stopwatch.lua
-- Simple visible stopwatch for Neovim
local M = {}
local uv = vim.loop
local state = {
running = false,
start_hr = 0, -- hrtime at start (nanoseconds)
elapsed_ns = 0, -- accumulated nanoseconds when stopped
name = nil,
timer = nil,
bufnr = nil,
win = nil,
opts = nil,
}
local function ns_to_time(ns)
local s = ns / 1e9
local h = math.floor(s / 3600)
s = s - h * 3600
local m = math.floor(s / 60)
local sec = s - m * 60
local secs = math.floor(sec)
local ms = math.floor((sec - secs) * 1000)
if h > 0 then
return string.format("%02d:%02d:%02d.%03d", h, m, secs, ms)
else
return string.format("%02d:%02d.%03d", m, secs, ms)
end
end
local function get_elapsed_ns()
if state.running then
return state.elapsed_ns + (uv.hrtime() - state.start_hr)
else
return state.elapsed_ns
end
end
local function ensure_win()
if state.win and vim.api.nvim_win_is_valid(state.win) then return end
if not state.bufnr or not vim.api.nvim_buf_is_valid(state.bufnr) then
state.bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_option(state.bufnr, "bufhidden", "wipe")
vim.api.nvim_buf_set_option(state.bufnr, "modifiable", false)
vim.api.nvim_buf_set_option(state.bufnr, "filetype", "stopwatch")
end
local width = state.opts.width or 24
local height = 1
local col = (vim.o.columns - width) - (state.opts.margin_right or 2)
local row = state.opts.row or 1
local win_opts = {
relative = "editor",
anchor = "NW",
row = row,
col = col,
width = width,
height = height,
style = "minimal",
focusable = false,
noautocmd = true,
}
state.win = vim.api.nvim_open_win(state.bufnr, false, win_opts)
vim.api.nvim_win_set_option(state.win, "winblend", state.opts.winblend or 5)
end
local function close_win()
if state.win and vim.api.nvim_win_is_valid(state.win) then
pcall(vim.api.nvim_win_close, state.win, true)
end
state.win = nil
-- keep buf so it can be reused
end
local function update_win()
if not state.opts.visible then return end
ensure_win()
if not state.bufnr or not vim.api.nvim_buf_is_valid(state.bufnr) then return end
local label = state.name and ("[" .. state.name .. "] ") or ""
local status_sym = state.running and "" or ""
local txt = status_sym .. " " .. label .. ns_to_time(get_elapsed_ns())
vim.api.nvim_buf_set_option(state.bufnr, "modifiable", true)
vim.api.nvim_buf_set_lines(state.bufnr, 0, -1, false, {txt})
vim.api.nvim_buf_set_option(state.bufnr, "modifiable", false)
end
local function start_timer_loop()
if state.timer then return end
state.timer = uv.new_timer()
state.timer:start(0, state.opts.update_ms or 250, vim.schedule_wrap(function()
if state.win and not vim.api.nvim_win_is_valid(state.win) then
state.win = nil
end
update_win()
end))
end
local function stop_timer_loop()
if state.timer then
pcall(function() state.timer:stop() end)
state.timer:close()
state.timer = nil
end
end
-- Public API
function M.start(name)
if state.running then return end
state.name = name or state.name
state.start_hr = uv.hrtime() - state.elapsed_ns
state.running = true
if state.opts.visible then
ensure_win()
end
start_timer_loop()
update_win()
end
function M.stop()
if not state.running then return end
state.elapsed_ns = get_elapsed_ns()
state.running = false
stop_timer_loop()
update_win()
if state.opts.log_on_stop then
local log_path = state.opts.log_path or (vim.fn.stdpath("data") .. "/stopwatch_log.md")
local time_str = os.date("%Y-%m-%d %H:%M:%S")
local name = state.name or "-"
local duration = ns_to_time(state.elapsed_ns)
local line = string.format("- %s | %s | %s\n", time_str, name, duration)
local f = io.open(log_path, "a")
if f then
f:write(line)
f:close()
end
end
end
function M.toggle(name)
if state.running then
M.stop()
else
M.start(name)
end
end
function M.reset()
state.running = false
state.elapsed_ns = 0
state.start_hr = 0
stop_timer_loop()
update_win()
end
function M.show()
state.opts.visible = true
ensure_win()
start_timer_loop()
update_win()
end
function M.hide()
state.opts.visible = false
close_win()
stop_timer_loop()
end
function M.log_now()
local log_path = state.opts.log_path or (vim.fn.stdpath("data") .. "/stopwatch_log.md")
local time_str = os.date("%Y-%m-%d %H:%M:%S")
local name = state.name or "-"
local duration = ns_to_time(get_elapsed_ns())
local line = string.format("- %s | %s | %s\n", time_str, name, duration)
local f = io.open(log_path, "a")
if f then
f:write(line)
f:close()
end
end
function M.status()
-- returns a short string for statusline
local elapsed = get_elapsed_ns()
local prefix = state.running and "" or ""
local name = state.name and (" " .. state.name) or ""
return prefix .. name .. " " .. ns_to_time(elapsed)
end
function M.setup(opts)
opts = opts or {}
state.opts = vim.tbl_extend("keep", opts, {
visible = true,
update_ms = 250,
width = 28,
row = 1,
margin_right = 2,
winblend = 5,
log_on_stop = false,
log_path = nil,
})
-- Create user commands
vim.api.nvim_create_user_command("StopwatchStart", function(opts)
M.start(opts.args ~= "" and opts.args or nil)
end, { nargs = "?" })
vim.api.nvim_create_user_command("StopwatchStop", function()
M.stop()
end, {})
vim.api.nvim_create_user_command("StopwatchToggle", function(opts)
M.toggle(opts.args ~= "" and opts.args or nil)
end, { nargs = "?" })
vim.api.nvim_create_user_command("StopwatchReset", function() M.reset() end, {})
vim.api.nvim_create_user_command("StopwatchShow", function() M.show() end, {})
vim.api.nvim_create_user_command("StopwatchHide", function() M.hide() end, {})
vim.api.nvim_create_user_command("StopwatchLog", function() M.log_now() end, {})
-- optionally show initially
if state.opts.visible then
ensure_win()
update_win()
end
end
return M