local config = require('pending.config') local forge = require('pending.forge') local parse = require('pending.parse') ---@class pending.ForgeLineMeta ---@field ref pending.ForgeRef ---@field cache? pending.ForgeCache ---@field col_start integer ---@field col_end integer ---@class pending.LineMeta ---@field type 'task'|'header'|'blank'|'filter' ---@field id? integer ---@field due? string ---@field raw_due? string ---@field status? pending.TaskStatus ---@field category? string ---@field overdue? boolean ---@field show_category? boolean ---@field priority? integer ---@field recur? string ---@field forge_spans? pending.ForgeLineMeta[] ---@class pending.views local M = {} ---@param due? string ---@return string? local function format_due(due) if not due then return nil end local y, m, d, hh, mm = due:match('^(%d%d%d%d)-(%d%d)-(%d%d)T(%d%d):(%d%d)$') if not y then y, m, d = due:match('^(%d%d%d%d)-(%d%d)-(%d%d)$') end if not y then return due end local t = os.time({ year = tonumber(y) --[[@as integer]], month = tonumber(m) --[[@as integer]], day = tonumber(d) --[[@as integer]], }) local formatted = os.date(config.get().date_format, t) --[[@as string]] if hh then formatted = formatted .. ' ' .. hh .. ':' .. mm end return formatted end ---@param task pending.Task ---@param prefix_len integer ---@return pending.ForgeLineMeta[]? local function compute_forge_spans(task, prefix_len) local refs = forge.find_refs(task.description) if #refs == 0 then return nil end local cache = task._extra and task._extra._forge_cache or nil local spans = {} for _, r in ipairs(refs) do table.insert(spans, { ref = r.ref, cache = cache, col_start = prefix_len + r.start_byte, col_end = prefix_len + r.end_byte, }) end return spans end ---@type table local status_rank = { wip = 0, pending = 1, blocked = 2, done = 3 } ---@param task pending.Task ---@return string local function state_char(task) if task.status == 'done' then return 'x' elseif task.status == 'wip' then return '>' elseif task.status == 'blocked' then return '=' elseif task.priority > 0 then return '!' end return ' ' end ---@param tasks pending.Task[] local function sort_tasks(tasks) table.sort(tasks, function(a, b) local ra = status_rank[a.status] or 1 local rb = status_rank[b.status] or 1 if ra ~= rb then return ra < rb end if a.priority ~= b.priority then return a.priority > b.priority end if a.order ~= b.order then return a.order < b.order end return a.id < b.id end) end ---@type table local sort_key_comparators = { status = function(a, b) local ra = status_rank[a.status] or 1 local rb = status_rank[b.status] or 1 if ra ~= rb then return ra < rb end return nil end, priority = function(a, b) if a.priority ~= b.priority then return a.priority > b.priority end return nil end, due = function(a, b) local a_due = a.due or '' local b_due = b.due or '' if a_due ~= b_due then if a_due == '' then return false end if b_due == '' then return true end return a_due < b_due end return nil end, order = function(a, b) if a.order ~= b.order then return a.order < b.order end return nil end, id = function(a, b) if a.id ~= b.id then return a.id < b.id end return nil end, age = function(a, b) if a.id ~= b.id then return a.id < b.id end return nil end, } ---@return fun(a: pending.Task, b: pending.Task): boolean local function build_queue_comparator() local log = require('pending.log') local keys = config.get().view.queue.sort or { 'status', 'priority', 'due', 'order', 'id' } local comparators = {} local unknown = {} for _, key in ipairs(keys) do local cmp = sort_key_comparators[key] if cmp then table.insert(comparators, cmp) else table.insert(unknown, key) end end if #unknown > 0 then local label = #unknown == 1 and 'unknown queue sort key: ' or 'unknown queue sort keys: ' log.warn(label .. table.concat(unknown, ', ')) end return function(a, b) for _, cmp in ipairs(comparators) do local result = cmp(a, b) if result ~= nil then return result end end return false end end ---@param tasks pending.Task[] local function sort_tasks_priority(tasks) local cmp = build_queue_comparator() table.sort(tasks, cmp) end ---@param tasks pending.Task[] ---@return string[] lines ---@return pending.LineMeta[] meta function M.category_view(tasks) local by_cat = {} local cat_order = {} local cat_seen = {} local done_by_cat = {} for _, task in ipairs(tasks) do local cat = task.category or config.get().default_category if not cat_seen[cat] then cat_seen[cat] = true table.insert(cat_order, cat) by_cat[cat] = {} done_by_cat[cat] = {} end if task.status == 'done' or task.status == 'deleted' then table.insert(done_by_cat[cat], task) else table.insert(by_cat[cat], task) end end local cfg_order = config.get().view.category.order if cfg_order and #cfg_order > 0 then local ordered = {} local seen = {} for _, name in ipairs(cfg_order) do if cat_seen[name] then table.insert(ordered, name) seen[name] = true end end for _, name in ipairs(cat_order) do if not seen[name] then table.insert(ordered, name) end end cat_order = ordered end for _, cat in ipairs(cat_order) do sort_tasks(by_cat[cat]) sort_tasks(done_by_cat[cat]) end local lines = {} local meta = {} for i, cat in ipairs(cat_order) do if i > 1 then table.insert(lines, '') table.insert(meta, { type = 'blank' }) end table.insert(lines, '# ' .. cat) table.insert(meta, { type = 'header', category = cat }) local all = {} for _, t in ipairs(by_cat[cat]) do table.insert(all, t) end for _, t in ipairs(done_by_cat[cat]) do table.insert(all, t) end for _, task in ipairs(all) do local prefix = '/' .. task.id .. '/' local state = state_char(task) local line = prefix .. '- [' .. state .. '] ' .. task.description local prefix_len = #prefix + #('- [' .. state .. '] ') table.insert(lines, line) table.insert(meta, { type = 'task', id = task.id, due = format_due(task.due), raw_due = task.due, status = task.status, category = cat, priority = task.priority, overdue = task.status ~= 'done' and task.due ~= nil and parse.is_overdue(task.due) or nil, recur = task.recur, forge_spans = compute_forge_spans(task, prefix_len), }) end end return lines, meta end ---@param tasks pending.Task[] ---@return string[] lines ---@return pending.LineMeta[] meta function M.priority_view(tasks) local pending = {} local done = {} for _, task in ipairs(tasks) do if task.status == 'done' then table.insert(done, task) else table.insert(pending, task) end end sort_tasks_priority(pending) sort_tasks_priority(done) local lines = {} local meta = {} local all = {} for _, t in ipairs(pending) do table.insert(all, t) end for _, t in ipairs(done) do table.insert(all, t) end for _, task in ipairs(all) do local prefix = '/' .. task.id .. '/' local state = task.status == 'done' and 'x' or (task.priority > 0 and '!' or ' ') local line = prefix .. '- [' .. state .. '] ' .. task.description local prefix_len = #prefix + #('- [' .. state .. '] ') table.insert(lines, line) table.insert(meta, { type = 'task', id = task.id, due = format_due(task.due), raw_due = task.due, 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, show_category = true, recur = task.recur, forge_ref = task._extra and task._extra._forge_ref or nil, forge_cache = task._extra and task._extra._forge_cache or nil, forge_spans = compute_forge_spans(task, prefix_len), }) end return lines, meta end return M