From a0cbca6db5793d6f9c0a929a0a0512c86e036fa0 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 24 Feb 2026 15:09:22 -0500 Subject: [PATCH] feat(views): add category and priority view renderers Problem: need to render task lists grouped by category or sorted by priority with concealed IDs and metadata. Solution: add category_view and priority_view functions that produce buffer lines with hidden /id/ prefixes, priority flags, and line metadata for extmark placement. --- lua/todo/views.lua | 181 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 lua/todo/views.lua diff --git a/lua/todo/views.lua b/lua/todo/views.lua new file mode 100644 index 0000000..5330d86 --- /dev/null +++ b/lua/todo/views.lua @@ -0,0 +1,181 @@ +local config = require('todo.config') + +---@class todo.LineMeta +---@field type 'task'|'header'|'blank' +---@field id? integer +---@field due? string +---@field raw_due? string +---@field status? string +---@field category? string + +---@class todo.views +local M = {} + +---@param due? string +---@return string? +local function format_due(due) + if not due then + return nil + end + local y, m, d = due:match('^(%d%d%d%d)-(%d%d)-(%d%d)$') + if not y then + return due + end + local t = os.time({ year = tonumber(y), month = tonumber(m), day = tonumber(d) }) + return os.date(config.get().date_format, t) +end + +---@param tasks todo.Task[] +local function sort_tasks(tasks) + table.sort(tasks, function(a, b) + 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 + +---@param tasks todo.Task[] +local function sort_tasks_priority(tasks) + table.sort(tasks, function(a, b) + if a.priority ~= b.priority then + return a.priority > b.priority + end + 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 + if a.order ~= b.order then + return a.order < b.order + end + return a.id < b.id + end) +end + +---@param tasks todo.Task[] +---@return string[] lines +---@return todo.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' then + table.insert(done_by_cat[cat], task) + else + table.insert(by_cat[cat], task) + end + 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 indent = ' ' + local prio = task.priority == 1 and '! ' or '' + local line = prefix .. indent .. prio .. task.description + 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, + }) + end + end + + return lines, meta +end + +---@param tasks todo.Task[] +---@return string[] lines +---@return todo.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 indent = ' ' + local prio = task.priority == 1 and '! ' or '' + local line = prefix .. indent .. prio .. task.description + 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, + }) + end + + return lines, meta +end + +return M