--- jasper/timer.lua --- Timer state machine + inactivity auto-pause. local M = {} local api = require("jasper.api") local auth = require("jasper.auth") --- Internal state (one instance per Neovim session). local state = { token = nil, ---@type string|nil timer_id = nil, ---@type number|nil attivita_id = nil, ---@type number|nil running = false, -- inactivity_ms: milliseconds of silence before the timer is auto-paused inactivity_ms = 10 * 60 * 1000, uv_timer = nil, ---@type uv_timer_t|nil augroup = nil, ---@type number|nil } -- --------------------------------------------------------------------------- -- Helpers -- --------------------------------------------------------------------------- local function notify(msg, level) vim.notify("[Jasper] " .. msg, level or vim.log.levels.INFO) end --- Stop the uv inactivity countdown (without pausing the Jasper timer). local function cancel_inactivity_watchdog() if state.uv_timer then state.uv_timer:stop() end end --- (Re)start the inactivity countdown. --- When it fires the Jasper timer is paused. local function arm_inactivity_watchdog() if not state.uv_timer or not state.running then return end state.uv_timer:stop() state.uv_timer:start( state.inactivity_ms, 0, vim.schedule_wrap(function() if not state.running then return end local ok, err = api.stop_timer(state.token, state.timer_id) if ok then state.running = false notify("Timer paused (inactivity)") else notify("Could not pause timer: " .. (err or ""), vim.log.levels.WARN) end end) ) end --- Find the smallest non-negative integer not already used as a timer_id. --- @param timers table[] --- @return number local function next_free_timer_id(timers) local used = {} for _, t in ipairs(timers) do used[t.timer_id] = true end local id = 0 while used[id] do id = id + 1 end return id end --- Find an existing timer for `attivita_id` or create a new one. --- @param token string --- @param attivita_id number --- @param timers table[] --- @return number|nil timer_id --- @return string|nil error local function find_or_create_timer(token, attivita_id, timers) for _, t in ipairs(timers) do if t["attivita__id"] == attivita_id then return t.timer_id, nil end end -- No existing timer: create one in the smallest free slot local new_id = next_free_timer_id(timers) local ok, err = api.create_timer(token, new_id, attivita_id) if not ok then return nil, "Cannot create timer: " .. (err or "unknown error") end notify("New timer created (ID " .. new_id .. ")") return new_id, nil end -- --------------------------------------------------------------------------- -- Activity callback (called from autocmds) -- --------------------------------------------------------------------------- function M.on_activity() if not state.timer_id then return end if not state.running then -- Resume after an inactivity pause local ok, err = api.start_timer(state.token, state.timer_id) if ok then state.running = true notify("Timer resumed") else notify("Could not resume timer: " .. (err or ""), vim.log.levels.WARN) return end end arm_inactivity_watchdog() end -- --------------------------------------------------------------------------- -- Lifecycle -- --------------------------------------------------------------------------- --- Start tracking: called after token + timer_id are known. --- @param token string --- @param timer_id number local function begin_tracking(token, timer_id) state.token = token state.timer_id = timer_id -- Create the libuv timer used for inactivity detection state.uv_timer = vim.uv.new_timer() -- Register activity autocmds state.augroup = vim.api.nvim_create_augroup("JasperActivity", { clear = true }) vim.api.nvim_create_autocmd( { "CursorMoved", "CursorMovedI", "InsertCharPre", "BufEnter", "FocusGained" }, { group = state.augroup, callback = M.on_activity } ) -- Start the timer immediately local ok, err = api.start_timer(token, timer_id) if not ok then notify("Could not start timer: " .. (err or ""), vim.log.levels.ERROR) return end state.running = true notify("Timer started (ID " .. timer_id .. ")") arm_inactivity_watchdog() end --- Called after a valid token is obtained (either from cache or fresh login). --- Fetches timers, creates one if needed, then begins tracking. --- @param token string local function on_token_ready(token) local timers, err = api.get_timers(token) if err == "unauthorized" then -- Token is expired: delete it and ask the user to login again auth.delete_token() notify("Session expired. Please re-open Neovim to login again.", vim.log.levels.WARN) return end if err then notify("Cannot fetch timers: " .. err, vim.log.levels.ERROR) return end local timer_id, terr = find_or_create_timer(token, state.attivita_id, timers) if not timer_id then notify(terr or "Cannot get timer", vim.log.levels.ERROR) return end begin_tracking(token, timer_id) end --- Entry point called on VimEnter. --- @param opts table { attivita_id, inactivity_timeout } function M.setup(opts) state.attivita_id = opts.attivita_id state.inactivity_ms = (opts.inactivity_timeout or 10) * 60 * 1000 local token = auth.get_token() if token then on_token_ready(token) else auth.prompt_login(function(new_token) if new_token then on_token_ready(new_token) end end) end end --- Stop tracking: called on VimLeave. function M.teardown() cancel_inactivity_watchdog() if state.uv_timer then state.uv_timer:close() state.uv_timer = nil end if state.running and state.timer_id then local ok, err = api.stop_timer(state.token, state.timer_id) if ok then state.running = false else -- Best-effort; we're exiting anyway vim.notify("[Jasper] Could not stop timer on exit: " .. (err or ""), vim.log.levels.WARN) end end end return M