219 lines
6.0 KiB
Lua
219 lines
6.0 KiB
Lua
--- 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
|