feat(archive): duration syntax and confirmation prompt

Problem: `:Pending archive` accepted only a bare integer for days and
silently deleted tasks with no confirmation, risking accidental data loss.

Solution: Accept duration strings (`7d`, `3w`, `2m`) via
`parse.parse_duration_to_days()`, show a `vim.ui.input` confirmation
prompt before removing tasks, and skip the prompt when zero tasks match.
This commit is contained in:
Barrett Ruth 2026-03-08 20:25:33 -04:00
parent 8cfdafe464
commit cfcaaca28b
4 changed files with 220 additions and 25 deletions

View file

@ -992,35 +992,67 @@ local function run_sync(backend_name, action)
backend[action]()
end
---@param days? integer
---@param msg string
---@param callback fun()
local function confirm(msg, callback)
vim.ui.input({ prompt = msg .. ' [y/N]: ' }, function(input)
if input and input:lower() == 'y' then
callback()
end
end)
end
---@param arg? string
---@return nil
function M.archive(days)
if days == nil then
function M.archive(arg)
local days
if arg and arg ~= '' then
days = parse.parse_duration_to_days(arg)
if not days then
log.error('Invalid duration: ' .. arg .. '. Use e.g. 7d, 2w, 3m, or a bare number.')
return
end
else
days = 30
end
local cutoff = os.date('!%Y-%m-%dT%H:%M:%SZ', os.time() - (days * 86400)) --[[@as string]]
local s = get_store()
local tasks = s:tasks()
log.debug(('archive: days=%d cutoff=%s total_tasks=%d'):format(days, cutoff, #tasks))
local archived = 0
local kept = {}
local count = 0
for _, task in ipairs(tasks) do
if (task.status == 'done' or task.status == 'deleted') and task['end'] then
if task['end'] < cutoff then
archived = archived + 1
goto skip
count = count + 1
end
end
table.insert(kept, task)
::skip::
end
s:replace_tasks(kept)
_save_and_notify()
log.info('Archived ' .. archived .. ' task' .. (archived == 1 and '' or 's') .. '.')
local bufnr = buffer.bufnr()
if bufnr and vim.api.nvim_buf_is_valid(bufnr) then
buffer.render(bufnr)
if count == 0 then
log.info('No tasks to archive.')
return
end
confirm(
'Archive ' .. count .. ' task' .. (count == 1 and '' or 's') .. ' completed/deleted more than ' .. days .. 'd ago?',
function()
local kept = {}
for _, task in ipairs(tasks) do
if (task.status == 'done' or task.status == 'deleted') and task['end'] then
if task['end'] < cutoff then
goto skip
end
end
table.insert(kept, task)
::skip::
end
s:replace_tasks(kept)
_save_and_notify()
log.info('Archived ' .. count .. ' task' .. (count == 1 and '' or 's') .. '.')
local bufnr = buffer.bufnr()
if bufnr and vim.api.nvim_buf_is_valid(bufnr) then
buffer.render(bufnr)
end
end
)
end
---@return nil
@ -1342,7 +1374,7 @@ function M.command(args)
local action = rest:match('^(%S+)')
run_sync(cmd, action)
elseif cmd == 'archive' then
M.archive(tonumber(rest))
M.archive(rest ~= '' and rest or nil)
elseif cmd == 'due' then
M.due()
elseif cmd == 'filter' then