From b9dfccaa43dac8ef231d560eccf94c65d5303d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=BCdecke=20Sascha?= Date: Mon, 13 Jul 2026 10:57:20 +0200 Subject: [PATCH] Added simple stopwatch as with S prefix --- init.lua | 3 + lua/keybindings.lua | 13 +++ lua/stopwatch.lua | 231 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 247 insertions(+) create mode 100644 lua/stopwatch.lua diff --git a/init.lua b/init.lua index 5c56239..af09188 100644 --- a/init.lua +++ b/init.lua @@ -14,6 +14,9 @@ require('colorscheme') require('lsp') require('keybindings') require('layout') + +require('stopwatch').setup({}) + if vim.loop.os_gethostname() == 'ccflow' then require('diary') end diff --git a/lua/keybindings.lua b/lua/keybindings.lua index e6dd77f..e695238 100644 --- a/lua/keybindings.lua +++ b/lua/keybindings.lua @@ -80,6 +80,19 @@ local workspaces_fzf_picker = function(opts) end vim.keymap.set('n', 'w', workspaces_fzf_picker, { desc = "Workspaces" }) +------------------------------------------ +-- +-- Stopwatch +-- +------------------------------------------ + +vim.keymap.set('n', 'Ss', ":StopwatchStart", { desc = "Stopwatch: Start" }) +vim.keymap.set('n', 'St', ":StopwatchToggle", { desc = "Stopwatch: Toggle" }) +vim.keymap.set('n', 'Sr', ":StopwatchReset", { desc = "Stopwatch: Reset" }) +vim.keymap.set('n', 'SS', ":StopwatchShow", { desc = "Stopwatch: Show" }) +vim.keymap.set('n', 'SH', ":StopwatchHide", { desc = "Stopwatch: Hide" }) + + ------------------------------------------ -- -- Window layout save and restore diff --git a/lua/stopwatch.lua b/lua/stopwatch.lua new file mode 100644 index 0000000..6cfba49 --- /dev/null +++ b/lua/stopwatch.lua @@ -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 +