feat(customization): icons config, PendingTab, and demo infrastructure (#46)

* 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 <Plug>(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
This commit is contained in:
Barrett Ruth 2026-02-26 19:20:29 -05:00
parent a2e0e296ac
commit dbd76d6759
9 changed files with 248 additions and 4 deletions

View file

@ -2,7 +2,7 @@
Edit tasks like text. `:w` saves them.
<!-- insert preview -->
![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)

0
assets/.gitkeep Normal file
View file

View file

@ -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|.
<Plug>(pending-tab) *<Plug>(pending-tab)*
Open the task buffer in a new tab. See |:PendingTab|.
Example configuration: >lua
vim.keymap.set('n', '<leader>t', '<Plug>(pending-open)')
vim.keymap.set('n', '<leader>T', '<Plug>(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*

View file

@ -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

View file

@ -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?

View file

@ -304,3 +304,13 @@ end)
vim.keymap.set('n', '<Plug>(pending-add-here)', function()
require('pending').add_here()
end)
vim.keymap.set('n', '<Plug>(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, {})

39
scripts/demo-init.lua Normal file
View file

@ -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()

28
scripts/demo.tape Normal file
View file

@ -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

59
spec/icons_spec.lua Normal file
View file

@ -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)