Retired stopwatch
This commit is contained in:
2
init.lua
2
init.lua
@@ -15,8 +15,6 @@ require('lsp')
|
||||
require('keybindings')
|
||||
require('layout')
|
||||
|
||||
require('stopwatch').setup({ big_display = true, update_ms = 1000 })
|
||||
|
||||
if vim.loop.os_gethostname() == 'slaptop' then
|
||||
require('diary')
|
||||
end
|
||||
|
||||
@@ -1,398 +0,0 @@
|
||||
-- ~/.config/nvim/lua/stopwatch.lua
|
||||
-- Visible stopwatch for Neovim with centered big display and clickable buttons
|
||||
local M = {}
|
||||
local uv = vim.loop
|
||||
|
||||
local state = {
|
||||
running = false,
|
||||
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)
|
||||
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 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)
|
||||
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 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
|
||||
|
||||
local win_opts = {
|
||||
relative = "editor",
|
||||
anchor = "NW",
|
||||
row = row,
|
||||
col = math.max(0, col),
|
||||
width = math.min(width, vim.o.columns - 1),
|
||||
height = height,
|
||||
style = "minimal",
|
||||
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()
|
||||
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
|
||||
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()
|
||||
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 lines = {}
|
||||
local label = state.name and ("[" .. state.name .. "] ") or ""
|
||||
|
||||
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, lines)
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
-- 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.pause()
|
||||
else
|
||||
-- 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
|
||||
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()
|
||||
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, {
|
||||
visible = true,
|
||||
update_ms = 250,
|
||||
width = 28,
|
||||
row = 1,
|
||||
margin_right = 2,
|
||||
winblend = 5,
|
||||
log_on_stop = false,
|
||||
log_path = nil,
|
||||
big_display = false,
|
||||
big_chars = 8,
|
||||
})
|
||||
state.big_mode = state.opts.big_display or false
|
||||
|
||||
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, {})
|
||||
|
||||
if state.opts.visible then ensure_win() update_win() end
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user