local config = require('pending.config') ---@class pending.Task ---@field id integer ---@field description string ---@field status 'pending'|'done'|'deleted' ---@field category? string ---@field priority integer ---@field due? string ---@field recur? string ---@field recur_mode? 'scheduled'|'completion' ---@field entry string ---@field modified string ---@field end? string ---@field order integer ---@field _extra? table ---@class pending.Data ---@field version integer ---@field next_id integer ---@field tasks pending.Task[] ---@field undo pending.Task[][] ---@class pending.store local M = {} local SUPPORTED_VERSION = 1 ---@type pending.Data? local _data = nil ---@return pending.Data local function empty_data() return { version = SUPPORTED_VERSION, next_id = 1, tasks = {}, undo = {}, } end ---@param path string local function ensure_dir(path) local dir = vim.fn.fnamemodify(path, ':h') if vim.fn.isdirectory(dir) == 0 then vim.fn.mkdir(dir, 'p') end end ---@return string local function timestamp() return os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]] end ---@type table local known_fields = { id = true, description = true, status = true, category = true, priority = true, due = true, recur = true, recur_mode = true, entry = true, modified = true, ['end'] = true, order = true, } ---@param task pending.Task ---@return table local function task_to_table(task) local t = { id = task.id, description = task.description, status = task.status, entry = task.entry, modified = task.modified, } if task.category then t.category = task.category end if task.priority and task.priority ~= 0 then t.priority = task.priority end if task.due then t.due = task.due end if task.recur then t.recur = task.recur end if task.recur_mode then t.recur_mode = task.recur_mode end if task['end'] then t['end'] = task['end'] end if task.order and task.order ~= 0 then t.order = task.order end if task._extra then for k, v in pairs(task._extra) do t[k] = v end end return t end ---@param t table ---@return pending.Task local function table_to_task(t) local task = { id = t.id, description = t.description, status = t.status or 'pending', category = t.category, priority = t.priority or 0, due = t.due, recur = t.recur, recur_mode = t.recur_mode, entry = t.entry, modified = t.modified, ['end'] = t['end'], order = t.order or 0, _extra = {}, } for k, v in pairs(t) do if not known_fields[k] then task._extra[k] = v end end if next(task._extra) == nil then task._extra = nil end return task end ---@return pending.Data function M.load() local path = config.get().data_path local f = io.open(path, 'r') if not f then _data = empty_data() return _data end local content = f:read('*a') f:close() if content == '' then _data = empty_data() return _data end local ok, decoded = pcall(vim.json.decode, content) if not ok then error('pending.nvim: failed to parse ' .. path .. ': ' .. tostring(decoded)) end if decoded.version and decoded.version > SUPPORTED_VERSION then error( 'pending.nvim: data file version ' .. decoded.version .. ' is newer than supported version ' .. SUPPORTED_VERSION .. '. Please update the plugin.' ) end _data = { version = decoded.version or SUPPORTED_VERSION, next_id = decoded.next_id or 1, tasks = {}, undo = {}, } for _, t in ipairs(decoded.tasks or {}) do table.insert(_data.tasks, table_to_task(t)) end for _, snapshot in ipairs(decoded.undo or {}) do if type(snapshot) == 'table' then local tasks = {} for _, raw in ipairs(snapshot) do table.insert(tasks, table_to_task(raw)) end table.insert(_data.undo, tasks) end end return _data end ---@return nil function M.save() if not _data then return end local path = config.get().data_path ensure_dir(path) local out = { version = _data.version, next_id = _data.next_id, tasks = {}, undo = {}, } for _, task in ipairs(_data.tasks) do table.insert(out.tasks, task_to_table(task)) end for _, snapshot in ipairs(_data.undo) do local serialized = {} for _, task in ipairs(snapshot) do table.insert(serialized, task_to_table(task)) end table.insert(out.undo, serialized) end local encoded = vim.json.encode(out) local tmp = path .. '.tmp' local f = io.open(tmp, 'w') if not f then error('pending.nvim: cannot write to ' .. tmp) end f:write(encoded) f:close() local ok, rename_err = os.rename(tmp, path) if not ok then os.remove(tmp) error('pending.nvim: cannot rename ' .. tmp .. ' to ' .. path .. ': ' .. tostring(rename_err)) end end ---@return pending.Data function M.data() if not _data then M.load() end return _data --[[@as pending.Data]] end ---@return pending.Task[] function M.tasks() return M.data().tasks end ---@return pending.Task[] function M.active_tasks() local result = {} for _, task in ipairs(M.tasks()) do if task.status ~= 'deleted' then table.insert(result, task) end end return result end ---@param id integer ---@return pending.Task? function M.get(id) for _, task in ipairs(M.tasks()) do if task.id == id then return task end end return nil end ---@param fields { description: string, status?: string, category?: string, priority?: integer, due?: string, recur?: string, recur_mode?: string, order?: integer, _extra?: table } ---@return pending.Task function M.add(fields) local data = M.data() local now = timestamp() local task = { id = data.next_id, description = fields.description, status = fields.status or 'pending', category = fields.category or config.get().default_category, priority = fields.priority or 0, due = fields.due, recur = fields.recur, recur_mode = fields.recur_mode, entry = now, modified = now, ['end'] = nil, order = fields.order or 0, _extra = fields._extra, } data.next_id = data.next_id + 1 table.insert(data.tasks, task) return task end ---@param id integer ---@param fields table ---@return pending.Task? function M.update(id, fields) local task = M.get(id) if not task then return nil end local now = timestamp() for k, v in pairs(fields) do if k ~= 'id' and k ~= 'entry' then if v == vim.NIL then task[k] = nil else task[k] = v end end end task.modified = now if fields.status == 'done' or fields.status == 'deleted' then task['end'] = task['end'] or now end return task end ---@param id integer ---@return pending.Task? function M.delete(id) return M.update(id, { status = 'deleted', ['end'] = timestamp() }) end ---@param id integer ---@return integer? function M.find_index(id) for i, task in ipairs(M.tasks()) do if task.id == id then return i end end return nil end ---@param tasks pending.Task[] ---@return nil function M.replace_tasks(tasks) M.data().tasks = tasks end ---@return pending.Task[] function M.snapshot() local result = {} for _, task in ipairs(M.active_tasks()) do local copy = {} for k, v in pairs(task) do if k ~= '_extra' then copy[k] = v end end if task._extra then copy._extra = {} for k, v in pairs(task._extra) do copy._extra[k] = v end end table.insert(result, copy --[[@as pending.Task]]) end return result end ---@return pending.Task[][] function M.undo_stack() return M.data().undo end ---@param stack pending.Task[][] ---@return nil function M.set_undo_stack(stack) M.data().undo = stack end ---@param id integer ---@return nil function M.set_next_id(id) M.data().next_id = id end ---@return nil function M.unload() _data = nil end return M