stopwatch update

This commit is contained in:
Lüdecke Sascha
2026-07-23 10:18:27 +02:00
parent 8202d991df
commit e9a0809911
2 changed files with 214 additions and 46 deletions

View File

@@ -1,17 +1,19 @@
-- ~/.config/nvim/lua/stopwatch.lua
-- Simple visible stopwatch for Neovim
-- Visible stopwatch for Neovim with centered big display and clickable buttons
local M = {}
local uv = vim.loop
local state = {
running = false,
start_hr = 0, -- hrtime at start (nanoseconds)
elapsed_ns = 0, -- accumulated nanoseconds when stopped
start_hr = 0,
elapsed_ns = 0,
name = nil,
timer = nil,
bufnr = nil,
win = nil,
opts = nil,
big_mode = false,
button_spans = nil,
}
local function ns_to_time(ns)
@@ -29,6 +31,35 @@ local function ns_to_time(ns)
end
end
local BIG_DIGITS = {
["0"] = {" ███ ","█ █","█ █","█ █"," ███ "},
["1"] = {""," ██ ","",""," ███ "},
["2"] = {" ███ ","█ █"," ██ "," ██ ","█████"},
["3"] = {" ███ ",""," ██ ",""," ███ "},
["4"] = {"█ █ ","█ █ ","█████","",""},
["5"] = {"█████","","████ ","","████ "},
["6"] = {" ███ ","","████ ","█ █"," ███ "},
["7"] = {"█████","","","",""},
["8"] = {" ███ ","█ █"," ███ ","█ █"," ███ "},
["9"] = {" ███ ","█ █"," ████",""," ███ "},
[":"] = {" ",""," ",""," "},
[" "] = {" "," "," "," "," "},
["-"] = {" "," ","█████"," "," "},
["."] = {" "," "," "," ",""},
}
local function big_time_lines(timestr)
local rows = {"","","","",""}
for i = 1, #timestr do
local ch = timestr:sub(i,i)
local pat = BIG_DIGITS[ch] or BIG_DIGITS[" "]
for r = 1,5 do
rows[r] = rows[r] .. pat[r] .. " "
end
end
return rows
end
local function get_elapsed_ns()
if state.running then
return state.elapsed_ns + (uv.hrtime() - state.start_hr)
@@ -46,8 +77,18 @@ local function ensure_win()
vim.api.nvim_buf_set_option(state.bufnr, "filetype", "stopwatch")
end
local width = state.opts.width or 24
local width = state.opts.width or 28
local height = 1
if state.opts.big_display or state.big_mode then
local chars = state.opts.big_chars or 8
local char_width = 6
width = chars * char_width
height = 8
height = height + 1 -- plus button line
else
height = 1 + 1 -- one line time + button line
end
local col = (vim.o.columns - width) - (state.opts.margin_right or 2)
local row = state.opts.row or 1
@@ -55,15 +96,26 @@ local function ensure_win()
relative = "editor",
anchor = "NW",
row = row,
col = col,
width = width,
col = math.max(0, col),
width = math.min(width, vim.o.columns - 1),
height = height,
style = "minimal",
focusable = false,
focusable = true, -- <-- must be focusable for clicks to work reliably
noautocmd = true,
}
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 = vim.api.nvim_open_win(state.bufnr, false, win_opts)
vim.api.nvim_win_set_option(state.win, "winblend", state.opts.winblend or 5)
vim.api.nvim_buf_set_option(state.bufnr, "buftype", "nofile")
vim.api.nvim_buf_set_option(state.bufnr, "wrap", false)
-- buffer-local mouse mapping: LeftMouse triggers click handler
pcall(function()
vim.api.nvim_buf_set_keymap(state.bufnr, "n", "<LeftMouse>", "<Cmd>lua require('stopwatch')._on_click()<CR>", {nowait=true, noremap=true, silent=true})
end)
end
local function close_win()
@@ -71,7 +123,23 @@ local function close_win()
pcall(vim.api.nvim_win_close, state.win, true)
end
state.win = nil
-- keep buf so it can be reused
end
local function build_button_line()
local btns = { "> Toggle", "O Reset", "X Hide" }
local sep = " "
local line = table.concat(btns, sep)
local spans = {}
local cur = 1
for i, b in ipairs(btns) do
local s = line:find(b, cur, true)
if s then
local e = s + #b - 1
table.insert(spans, {name = b, s = s, e = e})
cur = e + #sep + 1
end
end
return line, spans
end
local function update_win()
@@ -79,11 +147,68 @@ local function update_win()
ensure_win()
if not state.bufnr or not vim.api.nvim_buf_is_valid(state.bufnr) then return end
local lines = {}
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())
if state.opts.big_display or state.big_mode then
-- compute time string
local total_s = math.floor(get_elapsed_ns() / 1e9)
local h = math.floor(total_s / 3600)
local m = math.floor((total_s % 3600) / 60)
local s = total_s % 60
local timestr
if h > 0 then
timestr = string.format("%02d:%02d:%02d", h, m, s)
else
timestr = string.format("%02d:%02d", m, s)
end
local b_lines = big_time_lines(timestr)
-- center horizontally inside window by adding left padding
local win_w = vim.api.nvim_win_get_width(state.win)
local char_width = 6 -- matches BIG_DIGITS width+gap
local displayed_w = #timestr * char_width
local leftpad = math.max(0, math.floor((win_w - displayed_w) / 2))
local pad = string.rep(" ", leftpad)
table.insert(lines, "") -- top blank line
for _, l in ipairs(b_lines) do table.insert(lines, pad .. l) end
table.insert(lines, "") -- blank between time and buttons
local btn_line, spans = build_button_line()
btn_line = pad .. btn_line
-- adjust spans by leftpad
for _, sp in ipairs(spans) do
sp.s = sp.s + leftpad
sp.e = sp.e + leftpad
end
table.insert(lines, btn_line)
table.insert(lines, "") -- bottom blank
state.button_spans = spans
else
local status_sym = state.running and "" or ""
local txt = status_sym .. label .. ns_to_time(get_elapsed_ns())
local win_w = vim.api.nvim_win_get_width(state.win)
local strw = vim.fn.strwidth(txt)
local leftpad = math.max(0, math.floor((win_w - strw) / 2))
local pad = string.rep(" ", leftpad)
table.insert(lines, "") -- top blank
table.insert(lines, pad .. txt)
table.insert(lines, "") -- blank between time and buttons
local btn_line, spans = build_button_line()
btn_line = pad .. btn_line
for _, sp in ipairs(spans) do
sp.s = sp.s + leftpad
sp.e = sp.e + leftpad
end
table.insert(lines, btn_line)
table.insert(lines, "") -- bottom blank
state.button_spans = spans
end
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_lines(state.bufnr, 0, -1, false, lines)
vim.api.nvim_buf_set_option(state.bufnr, "modifiable", false)
end
@@ -106,15 +231,12 @@ local function stop_timer_loop()
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
if state.opts.visible then ensure_win() end
start_timer_loop()
update_win()
end
@@ -132,21 +254,41 @@ function M.stop()
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
if f then f:write(line) f:close() end
end
end
-- Pause (anhalten, aber nicht als "Stop"/Log)
function M.pause()
if not state.running then return end
state.elapsed_ns = get_elapsed_ns()
state.running = false
stop_timer_loop()
update_win()
end
-- Resume (fortsetzen)
function M.resume()
if state.running then return end
state.start_hr = uv.hrtime() - state.elapsed_ns
state.running = true
start_timer_loop()
update_win()
end
-- Toggle zwischen Pause und Resume (für Button/Command)
function M.toggle(name)
if state.running then
M.stop()
M.pause()
else
M.start(name)
-- optional: wenn name übergeben wurde, setze neuen Namen
if name and name ~= "" then state.name = name end
M.resume()
end
end
function M.reset()
state.running = false
state.elapsed_ns = 0
@@ -175,20 +317,56 @@ function M.log_now()
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
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.toggle_big()
state.big_mode = not state.big_mode
update_win()
end
-- Mouse click handler
function M._on_click()
-- only proceed when our floating window is focused
local cur = vim.api.nvim_get_current_win()
if cur ~= state.win then return end
local pos = vim.fn.getmousepos()
if not pos then return end
local click_line = pos.line
local col = pos.column
-- button line is the second-to-last line because we add a bottom blank line
local total = vim.api.nvim_buf_line_count(state.bufnr)
local btn_line_index = total - 1
if click_line ~= btn_line_index then return end
if not state.button_spans then return end
for _, sp in ipairs(state.button_spans) do
if col >= sp.s and col <= sp.e then
local name = sp.name
if name:find("Toggle",1,true) then M.toggle()
elseif name:find("Hide",1,true) then M.hide()
elseif name:find("Reset",1,true) then M.reset() end
-- visual feedback
pcall(function()
local ns = vim.api.nvim_create_namespace("stopwatch_click")
vim.api.nvim_buf_add_highlight(state.bufnr, ns, "Visual", btn_line_index-1, sp.s-1, sp.e)
vim.defer_fn(function() vim.api.nvim_buf_clear_namespace(state.bufnr, ns, 0, -1) end, 120)
end)
break
end
end
end
function M.setup(opts)
opts = opts or {}
state.opts = vim.tbl_extend("keep", opts, {
@@ -200,32 +378,21 @@ function M.setup(opts)
winblend = 5,
log_on_stop = false,
log_path = nil,
big_display = false,
big_chars = 8,
})
state.big_mode = state.opts.big_display or false
-- 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("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, {})
vim.api.nvim_create_user_command("StopwatchToggleBig", function() M.toggle_big() end, {})
-- optionally show initially
if state.opts.visible then
ensure_win()
update_win()
end
if state.opts.visible then ensure_win() update_win() end
end
return M