From e9a080991101f42190d22a3b3d0a25b9df2103f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=BCdecke=20Sascha?= Date: Thu, 23 Jul 2026 10:18:27 +0200 Subject: [PATCH] stopwatch update --- init.lua | 3 +- lua/stopwatch.lua | 257 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 214 insertions(+), 46 deletions(-) diff --git a/init.lua b/init.lua index 58f4752..f71b138 100644 --- a/init.lua +++ b/init.lua @@ -14,7 +14,8 @@ require('colorscheme') require('lsp') require('keybindings') require('layout') -require('stopwatch').setup({}) + +require('stopwatch').setup({ big_display = true, update_ms = 1000 }) if vim.loop.os_gethostname() == 'slaptop' then require('diary') diff --git a/lua/stopwatch.lua b/lua/stopwatch.lua index 6cfba49..291b218 100644 --- a/lua/stopwatch.lua +++ b/lua/stopwatch.lua @@ -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", "", "lua require('stopwatch')._on_click()", {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 -