From 64b19360b1d2954ef76c8c9ce97dba152dca4796 Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Thu, 26 Feb 2026 19:20:29 -0500 Subject: [PATCH] feat(customization): icons config, PendingTab, and demo infrastructure (#46) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(config): add icons table with unicode defaults * feat(buffer): render icon overlays from config.icons Problem: status characters ([ ], [x], [!]) and metadata prefixes are hardcoded literals with no user customization. Solution: read config.icons in apply_extmarks and apply overlay extmarks for checkboxes/headers, replace hardcoded recur ↺ with icons.recur, and prefix due/category virt_text with configurable icon characters. * feat(plugin): add PendingTab command and (pending-tab) * docs: add icons config, PendingTab recipes, and demo infrastructure Problem: icon customization and auto-start workflow are undocumented; no demo asset exists for the README. Solution: document pending.Icons in vimdoc with nerd font and ASCII recipes, add PendingTab to commands and mappings, add open-on-startup recipe, add demo-init.lua and demo.tape for VHS screenshot generation, add assets/ directory, add README icons section and demo placeholder. * ci: format --- README.md | 17 +++++++++++- assets/.gitkeep | 0 doc/pending.txt | 52 +++++++++++++++++++++++++++++++++++++ lua/pending/buffer.lua | 28 +++++++++++++++++--- lua/pending/config.lua | 19 ++++++++++++++ plugin/pending.lua | 10 +++++++ scripts/demo-init.lua | 39 ++++++++++++++++++++++++++++ scripts/demo.tape | 28 ++++++++++++++++++++ spec/icons_spec.lua | 59 ++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 assets/.gitkeep create mode 100644 scripts/demo-init.lua create mode 100644 scripts/demo.tape create mode 100644 spec/icons_spec.lua diff --git a/README.md b/README.md index df7f3dd..f6add96 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Edit tasks like text. `:w` saves them. - +![demo](assets/demo.gif) ## Requirements @@ -24,6 +24,21 @@ luarocks install pending.nvim :help pending.nvim ``` +## Icons + +pending.nvim renders task status and metadata using configurable icon characters. The defaults use plain unicode (no nerd font required): + +```lua +vim.g.pending = { + icons = { + pending = '○', done = '✓', priority = '●', + header = '▸', due = '·', recur = '↺', category = '#', + }, +} +``` + +See `:help pending.Icons` for nerd font examples. + ## Acknowledgements - [dooing](https://github.com/atiladefreitas/dooing) diff --git a/assets/.gitkeep b/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/doc/pending.txt b/doc/pending.txt index 08c63f9..486ea32 100644 --- a/doc/pending.txt +++ b/doc/pending.txt @@ -356,6 +356,10 @@ COMMANDS *pending-commands* Equivalent to the `U` buffer-local key (see |pending-mappings|). Up to 20 levels of undo are persisted across sessions. + *:PendingTab* +:PendingTab + Open the task buffer in a new tab. + ============================================================================== MAPPINGS *pending-mappings* @@ -494,6 +498,9 @@ All motions support count: `3]]` jumps three headers forward. `]]` and to the current buffer's file and the cursor's line number. See |pending-file-token|. +(pending-tab) *(pending-tab)* + Open the task buffer in a new tab. See |:PendingTab|. + Example configuration: >lua vim.keymap.set('n', 't', '(pending-open)') vim.keymap.set('n', 'T', '(pending-toggle)') @@ -679,6 +686,16 @@ Fields: ~ automatically. New configs should use `sync.gcal` instead. See |pending.GcalConfig|. + {icons} (table) *pending.Icons* + Icon characters displayed in the buffer. Fields: + {pending} Uncompleted task icon. Default: '○' + {done} Completed task icon. Default: '✓' + {priority} Priority task icon. Default: '●' + {header} Category header prefix. Default: '▸' + {due} Due date prefix. Default: '·' + {recur} Recurrence prefix. Default: '↺' + {category} Category label prefix. Default: '#' + ============================================================================== LUA API *pending-api* @@ -860,6 +877,41 @@ directly, or disable `a_category` in `keymaps` and handle it via a `vim.b.miniai_config` entry that returns a linewise region if mini.ai's spec allows it in your version. +Nerd font icons: >lua + vim.g.pending = { + icons = { + pending = '', + done = '', + priority = '', + header = '', + due = '', + recur = '󰁯', + category = '', + }, + } +< + +ASCII fallback icons: >lua + vim.g.pending = { + icons = { + pending = '-', + done = 'x', + priority = '!', + header = '>', + due = '@', + recur = '~', + category = '+', + }, + } +< + +Open tasks in a new tab on startup: >lua + vim.api.nvim_create_autocmd('VimEnter', { + callback = function() + vim.cmd.PendingTab() + end, + }) +< ============================================================================== GOOGLE CALENDAR *pending-gcal* diff --git a/lua/pending/buffer.lua b/lua/pending/buffer.lua index 0aa78bb..09412f3 100644 --- a/lua/pending/buffer.lua +++ b/lua/pending/buffer.lua @@ -143,6 +143,7 @@ end ---@param bufnr integer ---@param line_meta pending.LineMeta[] local function apply_extmarks(bufnr, line_meta) + local icons = config.get().icons vim.api.nvim_buf_clear_namespace(bufnr, task_ns, 0, -1) for i, m in ipairs(line_meta) do local row = i - 1 @@ -156,13 +157,13 @@ local function apply_extmarks(bufnr, line_meta) local due_hl = m.overdue and 'PendingOverdue' or 'PendingDue' local virt_parts = {} if m.show_category and m.category then - table.insert(virt_parts, { m.category, 'PendingHeader' }) + table.insert(virt_parts, { icons.category .. ' ' .. m.category, 'PendingHeader' }) end if m.recur then - table.insert(virt_parts, { '\u{21bb} ' .. m.recur, 'PendingRecur' }) + table.insert(virt_parts, { icons.recur .. ' ' .. m.recur, 'PendingRecur' }) end if m.due then - table.insert(virt_parts, { m.due, due_hl }) + table.insert(virt_parts, { icons.due .. ' ' .. m.due, due_hl }) end if m.file then local display = m.file:match('([^/]+:%d+)$') or m.file @@ -185,12 +186,33 @@ local function apply_extmarks(bufnr, line_meta) hl_group = 'PendingDone', }) end + local line = vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] or '' + local bracket_col = (line:find('%[') or 1) - 1 + local icon, icon_hl + if m.status == 'done' then + icon, icon_hl = icons.done, 'PendingDone' + elseif m.priority and m.priority > 0 then + icon, icon_hl = icons.priority, 'PendingPriority' + else + icon, icon_hl = icons.pending, 'Normal' + end + local icon_padded = icon .. ' ' + vim.api.nvim_buf_set_extmark(bufnr, task_ns, row, bracket_col, { + virt_text = { { icon_padded, icon_hl } }, + virt_text_pos = 'overlay', + priority = 100, + }) elseif m.type == 'header' then local line = vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false)[1] or '' vim.api.nvim_buf_set_extmark(bufnr, task_ns, row, 0, { end_col = #line, hl_group = 'PendingHeader', }) + vim.api.nvim_buf_set_extmark(bufnr, task_ns, row, 0, { + virt_text = { { icons.header .. ' ', 'PendingHeader' } }, + virt_text_pos = 'overlay', + priority = 100, + }) end end end diff --git a/lua/pending/config.lua b/lua/pending/config.lua index 000ac2b..6adf1c3 100644 --- a/lua/pending/config.lua +++ b/lua/pending/config.lua @@ -1,3 +1,12 @@ +---@class pending.Icons +---@field pending string +---@field done string +---@field priority string +---@field header string +---@field due string +---@field recur string +---@field category string + ---@class pending.GcalConfig ---@field calendar? string ---@field credentials_path? string @@ -38,6 +47,7 @@ ---@field keymaps pending.Keymaps ---@field sync? pending.SyncConfig ---@field gcal? pending.GcalConfig +---@field icons pending.Icons ---@class pending.config local M = {} @@ -71,6 +81,15 @@ local defaults = { prev_task = '[t', }, sync = {}, + icons = { + pending = '○', + done = '✓', + priority = '●', + header = '▸', + due = '·', + recur = '↺', + category = '#', + }, } ---@type pending.Config? diff --git a/plugin/pending.lua b/plugin/pending.lua index 5cd94d0..ce62d1b 100644 --- a/plugin/pending.lua +++ b/plugin/pending.lua @@ -304,3 +304,13 @@ end) vim.keymap.set('n', '(pending-add-here)', function() require('pending').add_here() end) + +vim.keymap.set('n', '(pending-tab)', function() + vim.cmd.tabnew() + require('pending').open() +end) + +vim.api.nvim_create_user_command('PendingTab', function() + vim.cmd.tabnew() + require('pending').open() +end, {}) diff --git a/scripts/demo-init.lua b/scripts/demo-init.lua new file mode 100644 index 0000000..f2a6213 --- /dev/null +++ b/scripts/demo-init.lua @@ -0,0 +1,39 @@ +vim.opt.runtimepath:prepend(vim.fn.getcwd()) +local tmpdir = vim.fn.tempname() +vim.fn.mkdir(tmpdir, 'p') + +vim.g.pending = { + data_path = tmpdir .. '/tasks.json', + icons = { + pending = '○', + done = '✓', + priority = '●', + header = '▸', + due = '·', + recur = '↺', + category = '#', + }, +} + +local store = require('pending.store') +store.load() + +local today = os.date('%Y-%m-%d') +local yesterday = os.date('%Y-%m-%d', os.time() - 86400) +local tomorrow = os.date('%Y-%m-%d', os.time() + 86400) + +store.add({ + description = 'Finish quarterly report', + category = 'Work', + due = tomorrow, + recur = 'monthly', + priority = 1, +}) +store.add({ description = 'Review pull requests', category = 'Work' }) +store.add({ description = 'Update deployment docs', category = 'Work', status = 'done' }) +store.add({ description = 'Buy groceries', category = 'Personal', due = today }) +store.add({ description = 'Call dentist', category = 'Personal', due = yesterday, priority = 1 }) +store.add({ description = 'Read chapter 5', category = 'Personal' }) +store.add({ description = 'Learn a new language', category = 'Someday' }) +store.add({ description = 'Plan hiking trip', category = 'Someday' }) +store.save() diff --git a/scripts/demo.tape b/scripts/demo.tape new file mode 100644 index 0000000..3a1eee5 --- /dev/null +++ b/scripts/demo.tape @@ -0,0 +1,28 @@ +Output assets/demo.gif + +Require nvim + +Set Shell "bash" +Set FontSize 14 +Set Width 900 +Set Height 450 + +Type "nvim -u scripts/demo-init.lua -c 'autocmd VimEnter * Pending'" +Enter + +Sleep 2s + +Down +Down +Sleep 300ms +Down +Sleep 300ms + +Enter +Sleep 500ms + +Tab +Sleep 1s + +Type "q" +Sleep 200ms diff --git a/spec/icons_spec.lua b/spec/icons_spec.lua new file mode 100644 index 0000000..fe3288f --- /dev/null +++ b/spec/icons_spec.lua @@ -0,0 +1,59 @@ +require('spec.helpers') + +local config = require('pending.config') + +describe('icons', function() + before_each(function() + vim.g.pending = nil + config.reset() + end) + + after_each(function() + vim.g.pending = nil + config.reset() + end) + + it('has default icon values', function() + local icons = config.get().icons + assert.equals('○', icons.pending) + assert.equals('✓', icons.done) + assert.equals('●', icons.priority) + assert.equals('▸', icons.header) + assert.equals('·', icons.due) + assert.equals('↺', icons.recur) + assert.equals('#', icons.category) + end) + + it('allows overriding individual icons', function() + vim.g.pending = { icons = { pending = '-', done = 'x' } } + config.reset() + local icons = config.get().icons + assert.equals('-', icons.pending) + assert.equals('x', icons.done) + assert.equals('●', icons.priority) + assert.equals('▸', icons.header) + end) + + it('allows overriding all icons', function() + vim.g.pending = { + icons = { + pending = '-', + done = 'x', + priority = '!', + header = '>', + due = '@', + recur = '~', + category = '+', + }, + } + config.reset() + local icons = config.get().icons + assert.equals('-', icons.pending) + assert.equals('x', icons.done) + assert.equals('!', icons.priority) + assert.equals('>', icons.header) + assert.equals('@', icons.due) + assert.equals('~', icons.recur) + assert.equals('+', icons.category) + end) +end)