--- Task list views (tech-spec §8): Tactical `next` (the "what is next?" ranking) --- and Organizational `list` (the whole outstanding set). Both render the same --- titled rows the daemon returns into a scratch buffer, and are interactive: --- open the task's canonical-context doc --- a add a new task (prompt title + attention) from the list --- d mark the task under the cursor done --- r refresh local rpc = require("heph.rpc") local M = {} -- buf -> { tasks = , refresh = fn }; line N maps to tasks[N]. M._views = {} local hint_ns = vim.api.nvim_create_namespace("heph_view_hint") local HINT = " open a add d done r refresh" local ATTENTIONS = { "white", "orange", "red", "blue" } -- Compact relative date for a do/late epoch-ms value (mirrors heph-tui's fmt): -- today / tomorrow / yesterday, MM-DD within the year, else YYYY-MM-DD. local function fmt_date(ms) local d = os.date("*t", math.floor(ms / 1000)) local n = os.date("*t") local d_noon = os.time({ year = d.year, month = d.month, day = d.day, hour = 12 }) local n_noon = os.time({ year = n.year, month = n.month, day = n.day, hour = 12 }) local days = math.floor((d_noon - n_noon) / 86400 + 0.5) if days == 0 then return "today" elseif days == 1 then return "tomorrow" elseif days == -1 then return "yesterday" elseif d.year == n.year then return string.format("%02d-%02d", d.month, d.day) else return string.format("%04d-%02d-%02d", d.year, d.month, d.day) end end -- The right-side date chip: a late marker once past due, else the do-date. local function date_chip(t) if t.late_on and os.time() * 1000 > t.late_on then return "late:" .. fmt_date(t.late_on) elseif t.do_date then return "do:" .. fmt_date(t.do_date) end return "" end local function row(t) local tag = t.attention and ("[" .. t.attention .. "]") or "[ ]" local recur = t.recurrence and " ↻" or "" local left = string.format("%s %s%s", tag, t.title, recur) local chip = date_chip(t) if chip ~= "" then return string.format("%-50s %s", left, chip) end return left end local function task_on_line(buf) local view = M._views[buf] if not view then return nil end -- line 1 is the key-hint header; task rows start at line 2. return view.tasks[vim.api.nvim_win_get_cursor(0)[1] - 1] end -- Find or create the named scratch buffer, fill it, and (re)bind its keymaps. -- `refresh` re-runs the query+render so actions can reflect their changes. local function render(name, tasks, refresh) local buf for _, b in ipairs(vim.api.nvim_list_bufs()) do if vim.api.nvim_buf_get_name(b) == name then buf = b break end end if not buf then buf = vim.api.nvim_create_buf(false, true) -- unlisted scratch vim.api.nvim_buf_set_name(buf, name) end vim.bo[buf].buftype = "nofile" vim.bo[buf].bufhidden = "hide" vim.bo[buf].swapfile = false -- Line 1 is a dimmed key hint; task rows follow. local lines = { HINT } for _, t in ipairs(tasks) do lines[#lines + 1] = row(t) end if #tasks == 0 then lines[#lines + 1] = "(nothing here — press 'a' to add a task)" end vim.bo[buf].modifiable = true vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) vim.bo[buf].modifiable = false vim.api.nvim_buf_clear_namespace(buf, hint_ns, 0, -1) vim.api.nvim_buf_set_extmark(buf, hint_ns, 0, 0, { end_row = 0, end_col = #HINT, hl_group = "Comment", }) M._views[buf] = { tasks = tasks, refresh = refresh } local function map(lhs, fn, desc) vim.keymap.set("n", lhs, fn, { buffer = buf, desc = desc }) end map("", function() M.open_under_cursor(buf) end, "heph: open task context") map("a", function() M.add_from(buf) end, "heph: add a task") map("d", function() M.done_under_cursor(buf) end, "heph: mark task done") map("r", function() M.refresh(buf) end, "heph: refresh") vim.api.nvim_set_current_buf(buf) if #tasks > 0 then pcall(vim.api.nvim_win_set_cursor, 0, { 2, 0 }) -- land on the first task row end return buf end --- Open the canonical-context doc of the task on the cursor line. function M.open_under_cursor(buf) buf = buf or vim.api.nvim_get_current_buf() local t = task_on_line(buf) if t then require("heph.node").open(t.canonical_context_id or t.node_id) end end --- Re-run the view's query and re-render in place. function M.refresh(buf) local view = M._views[buf or vim.api.nvim_get_current_buf()] if view and view.refresh then view.refresh() end end --- Add a task from the list: prompt a title, pick an attention, capture, refresh. function M.add_from(buf) vim.ui.input({ prompt = "New task: " }, function(title) if not title or #title == 0 then return end require("heph.picker").select(ATTENTIONS, { prompt = "attention for: " .. title }, function(attention) require("heph.task").capture(title, { attention = attention }) require("heph.util").notify("captured: " .. title) M.refresh(buf) end) end) end --- Mark the task on the cursor line done, then refresh. function M.done_under_cursor(buf) local t = task_on_line(buf) if not t then return end rpc.call("task.set_state", { id = t.node_id, state = "done" }) require("heph.util").notify("done: " .. t.title) M.refresh(buf) end --- Tactical "what is next?" — render the ranking, return the rows. function M.next(opts) opts = opts or {} local tasks = rpc.call("next", { scope = opts.scope, limit = opts.limit or 5 }) render("heph://next", tasks, function() M.next(opts) end) return tasks end --- Organizational survey — render the outstanding set, return the rows. --- `list` takes a ListFilter (tech-spec §8.2); an empty table is the whole --- outstanding set. Legacy opts map onto the filter fields. function M.list(opts) opts = opts or {} local filter = {} if opts.scope then filter.scope = { opts.scope } end if opts.attention then filter.attention_in = { opts.attention } end if opts.include_blue == false then filter.attention_not = { "blue" } end local tasks = rpc.call("list", filter) render("heph://list", tasks, function() M.list(opts) end) return tasks end --- A built-in filter view (tech-spec §8.2) — render its rows like `list`. function M.view(name, opts) opts = opts or {} local tasks = rpc.call("view", { name = name }) render("heph://view/" .. name, tasks, function() M.view(name, opts) end) return tasks end return M