feat: add cancelled task status with configurable state chars (#158)

Problem: the task lifecycle only has `pending`, `wip`, `blocked`, and
`done`. There is no way to mark a task as abandoned. Additionally,
state characters (`>`, `=`) are hardcoded rather than reading from
`config.icons`, so customizing them has no effect on rendering or
parsing.

Solution: add a `cancelled` status with default state char `c`, `g/`
keymap, `PendingCancelled` highlight, filter predicate, and archive
support. Unify state chars by making `state_char()`, `parse_buffer()`,
and `infer_status()` read from `config.icons`. Change defaults to
mnemonic chars: `w` (wip), `b` (blocked), `c` (cancelled).
This commit is contained in:
Barrett Ruth 2026-03-12 20:55:21 -04:00
parent e9f21c0f0b
commit 2fd95e6dde
9 changed files with 109 additions and 45 deletions

View file

@ -71,21 +71,24 @@ local function compute_forge_spans(task, prefix_len)
end
---@type table<string, integer>
local status_rank = { wip = 0, pending = 1, blocked = 2, done = 3 }
local status_rank = { wip = 0, pending = 1, blocked = 2, done = 3, cancelled = 4 }
---@param task pending.Task
---@return string
local function state_char(task)
local icons = config.get().icons
if task.status == 'done' then
return 'x'
return icons.done
elseif task.status == 'cancelled' then
return icons.cancelled
elseif task.status == 'wip' then
return '>'
return icons.wip
elseif task.status == 'blocked' then
return '='
return icons.blocked
elseif task.priority > 0 then
return '!'
return icons.priority
end
return ' '
return icons.pending
end
---@param tasks pending.Task[]
@ -208,7 +211,7 @@ function M.category_view(tasks)
by_cat[cat] = {}
done_by_cat[cat] = {}
end
if task.status == 'done' or task.status == 'deleted' then
if task.status == 'done' or task.status == 'deleted' or task.status == 'cancelled' then
table.insert(done_by_cat[cat], task)
else
table.insert(by_cat[cat], task)
@ -271,7 +274,11 @@ function M.category_view(tasks)
status = task.status,
category = cat,
priority = task.priority,
overdue = task.status ~= 'done' and task.due ~= nil and parse.is_overdue(task.due) or nil,
overdue = task.status ~= 'done'
and task.status ~= 'cancelled'
and task.due ~= nil
and parse.is_overdue(task.due)
or nil,
recur = task.recur,
forge_spans = compute_forge_spans(task, prefix_len),
})
@ -289,7 +296,7 @@ function M.priority_view(tasks)
local done = {}
for _, task in ipairs(tasks) do
if task.status == 'done' then
if task.status == 'done' or task.status == 'cancelled' then
table.insert(done, task)
else
table.insert(pending, task)
@ -312,7 +319,7 @@ function M.priority_view(tasks)
for _, task in ipairs(all) do
local prefix = '/' .. task.id .. '/'
local state = task.status == 'done' and 'x' or (task.priority > 0 and '!' or ' ')
local state = state_char(task)
local line = prefix .. '- [' .. state .. '] ' .. task.description
local prefix_len = #prefix + #('- [' .. state .. '] ')
table.insert(lines, line)
@ -324,7 +331,11 @@ function M.priority_view(tasks)
status = task.status,
category = task.category,
priority = task.priority,
overdue = task.status ~= 'done' and task.due ~= nil and parse.is_overdue(task.due) or nil,
overdue = task.status ~= 'done'
and task.status ~= 'cancelled'
and task.due ~= nil
and parse.is_overdue(task.due)
or nil,
show_category = true,
recur = task.recur,
forge_ref = task._extra and task._extra._forge_ref or nil,