refactor(config): nest view settings under view key

Problem: View-related config fields (`default_view`, `eol_format`,
`category_order`, `folding`) are scattered as top-level siblings
alongside unrelated fields like `data_path` and `date_syntax`.

Solution: Group them under a `view` table with per-view sub-tables:
`view.default`, `view.eol_format`, `view.category.order`,
`view.category.folding`, and `view.queue` (empty, ready for #100).
Update all call sites, tests, and vimdoc.
This commit is contained in:
Barrett Ruth 2026-03-08 19:10:01 -04:00
parent 91cce0a82e
commit 740849588e
5 changed files with 82 additions and 73 deletions

View file

@ -589,14 +589,20 @@ Configuration is done via `vim.g.pending`. Set this before the plugin
loads: >lua loads: >lua
vim.g.pending = { vim.g.pending = {
data_path = vim.fn.stdpath('data') .. '/pending/tasks.json', data_path = vim.fn.stdpath('data') .. '/pending/tasks.json',
default_view = 'category',
default_category = 'Todo', default_category = 'Todo',
date_format = '%b %d', date_format = '%b %d',
date_syntax = 'due', date_syntax = 'due',
recur_syntax = 'rec', recur_syntax = 'rec',
someday_date = '9999-12-30', someday_date = '9999-12-30',
view = {
default = 'category',
eol_format = '%c %r %d',
category = {
order = {},
folding = true, folding = true,
category_order = {}, },
queue = {},
},
keymaps = { keymaps = {
close = 'q', close = 'q',
toggle = '<CR>', toggle = '<CR>',
@ -634,10 +640,6 @@ Fields: ~
See |pending-store-resolution| for how the active See |pending-store-resolution| for how the active
store is chosen at runtime. store is chosen at runtime.
{default_view} ('category'|'priority', default: 'category')
The view to use when the buffer is opened for the
first time in a session.
{default_category} (string, default: 'Todo') {default_category} (string, default: 'Todo')
Category assigned to new tasks when no `cat:` token Category assigned to new tasks when no `cat:` token
is present and no `Category: ` prefix is used with is present and no `Category: ` prefix is used with
@ -648,32 +650,6 @@ Fields: ~
virtual text in the buffer. Examples: `'%Y-%m-%d'` virtual text in the buffer. Examples: `'%Y-%m-%d'`
for ISO dates, `'%d %b'` for day-first. for ISO dates, `'%d %b'` for day-first.
{eol_format} (string, default: '%c %r %d')
Format string controlling the order, content, and
separators of end-of-line virtual text on task lines.
Three specifiers are available:
`%c` category icon + name (`PendingHeader`)
`%r` recurrence icon + pattern (`PendingRecur`)
`%d` due icon + date (`PendingDue` / `PendingOverdue`)
Literal text between specifiers is rendered with the
`Normal` highlight group and acts as a separator.
When a specifier's data is absent (e.g. `%d` on a
task with no due date), the specifier and any
surrounding literal text up to the next specifier
are omitted — missing fields never leave gaps.
`%c` only renders in priority view (where
`show_category` is true). In category view it is
always omitted regardless of the format string.
Examples: >lua
vim.g.pending = { eol_format = '%d %r' }
vim.g.pending = { eol_format = '%d | %r' }
vim.g.pending = { eol_format = '%c %d %r' }
<
{input_date_formats} (string[], default: {}) *pending-input-formats* {input_date_formats} (string[], default: {}) *pending-input-formats*
List of strftime-like format strings tried in order List of strftime-like format strings tried in order
when parsing a `due:` token that does not match the when parsing a `due:` token that does not match the
@ -705,38 +681,57 @@ Fields: ~
The date that `later` and `someday` resolve to. This The date that `later` and `someday` resolve to. This
acts as a "no date" sentinel for GTD-style workflows. acts as a "no date" sentinel for GTD-style workflows.
{category_order} (string[], default: {}) {view} (table) *pending.ViewConfig*
Ordered list of category names. In category view, View rendering configuration. Groups all settings
categories that appear in this list are shown in the that affect how the buffer displays tasks.
given order. Categories not in the list are appended
after the ordered ones in their natural order.
{folding} (boolean|table, default: true) *pending.FoldingConfig* {default} ('category'|'priority', default: 'category')
Controls category-level folds in category view. When The view to use when the buffer is opened
`true`, folds are enabled with the default foldtext for the first time in a session.
`'%c (%n tasks)'`. When `false`, folds are disabled
entirely. When a table, folds are enabled and the {eol_format} (string, default: '%c %r %d')
Format string for end-of-line virtual text.
Specifiers:
`%c` category icon + name (`PendingHeader`)
`%r` recurrence icon + pattern (`PendingRecur`)
`%d` due icon + date (`PendingDue`/`PendingOverdue`)
Literal text between specifiers acts as a
separator. Absent fields and surrounding
literals are collapsed automatically. `%c`
only renders in priority view.
{category} (table) *pending.CategoryViewConfig*
Category view settings.
{order} (string[], default: {})
Ordered list of category names. Categories
in this list appear in the given order;
others are appended after.
{folding} (boolean|table, default: true)
*pending.FoldingConfig*
Controls category-level folds. `true`
enables with default foldtext `'%c (%n
tasks)'`. `false` disables entirely. A
table may contain: table may contain:
{foldtext} (string|false) Format string
with `%c` (category) and `%n` (count).
`false` uses Vim's built-in foldtext.
Folds only apply to category view.
{foldtext} (string|false, default: '%c (%n tasks)') {queue} (table) *pending.QueueViewConfig*
Custom foldtext format string. Set to Queue (priority) view settings.
`false` to use Vim's built-in
foldtext. Two specifiers are
available:
`%c` category name
`%n` number of tasks in the fold
The category icon is prepended
automatically. When `false`, the
default Vim foldtext is used.
Folds only apply to category view; priority view
is always fold-free regardless of this setting.
Examples: >lua Examples: >lua
vim.g.pending = { folding = true }
vim.g.pending = { folding = false }
vim.g.pending = { vim.g.pending = {
folding = { foldtext = '%c (%n tasks)' }, view = {
default = 'priority',
eol_format = '%d | %r',
category = {
order = { 'Work', 'Personal' },
folding = { foldtext = '%c: %n items' },
},
},
} }
< <

View file

@ -440,7 +440,7 @@ end
local function apply_extmarks(bufnr, line_meta) local function apply_extmarks(bufnr, line_meta)
local cfg = config.get() local cfg = config.get()
local icons = cfg.icons local icons = cfg.icons
local eol_segments = parse_eol_format(cfg.eol_format or '%c %r %d') local eol_segments = parse_eol_format(cfg.view.eol_format or '%c %r %d')
vim.api.nvim_buf_clear_namespace(bufnr, ns_eol, 0, -1) vim.api.nvim_buf_clear_namespace(bufnr, ns_eol, 0, -1)
vim.api.nvim_buf_clear_namespace(bufnr, ns_inline, 0, -1) vim.api.nvim_buf_clear_namespace(bufnr, ns_inline, 0, -1)
for i, m in ipairs(line_meta) do for i, m in ipairs(line_meta) do
@ -578,7 +578,7 @@ function M.render(bufnr)
return return
end end
current_view = current_view or config.get().default_view current_view = current_view or config.get().view.default
local view_label = current_view == 'priority' and 'queue' or current_view local view_label = current_view == 'priority' and 'queue' or current_view
vim.api.nvim_buf_set_name(bufnr, 'pending://' .. view_label) vim.api.nvim_buf_set_name(bufnr, 'pending://' .. view_label)
local all_tasks = _store and _store:active_tasks() or {} local all_tasks = _store and _store:active_tasks() or {}

View file

@ -49,22 +49,31 @@
---@field next_task? string|false ---@field next_task? string|false
---@field prev_task? string|false ---@field prev_task? string|false
---@class pending.CategoryViewConfig
---@field order? string[]
---@field folding? boolean|pending.FoldingConfig
---@class pending.QueueViewConfig
---@class pending.ViewConfig
---@field default? 'category'|'priority'
---@field eol_format? string
---@field category? pending.CategoryViewConfig
---@field queue? pending.QueueViewConfig
---@class pending.Config ---@class pending.Config
---@field data_path string ---@field data_path string
---@field default_view 'category'|'priority'
---@field default_category string ---@field default_category string
---@field date_format string ---@field date_format string
---@field date_syntax string ---@field date_syntax string
---@field recur_syntax string ---@field recur_syntax string
---@field someday_date string ---@field someday_date string
---@field input_date_formats? string[] ---@field input_date_formats? string[]
---@field category_order? string[]
---@field drawer_height? integer ---@field drawer_height? integer
---@field debug? boolean ---@field debug? boolean
---@field keymaps pending.Keymaps ---@field keymaps pending.Keymaps
---@field folding? boolean|pending.FoldingConfig ---@field view pending.ViewConfig
---@field sync? pending.SyncConfig ---@field sync? pending.SyncConfig
---@field eol_format? string
---@field icons pending.Icons ---@field icons pending.Icons
---@class pending.config ---@class pending.config
@ -73,15 +82,20 @@ local M = {}
---@type pending.Config ---@type pending.Config
local defaults = { local defaults = {
data_path = vim.fn.stdpath('data') .. '/pending/tasks.json', data_path = vim.fn.stdpath('data') .. '/pending/tasks.json',
default_view = 'category',
default_category = 'Todo', default_category = 'Todo',
date_format = '%b %d', date_format = '%b %d',
date_syntax = 'due', date_syntax = 'due',
recur_syntax = 'rec', recur_syntax = 'rec',
someday_date = '9999-12-30', someday_date = '9999-12-30',
view = {
default = 'category',
eol_format = '%c %r %d', eol_format = '%c %r %d',
category = {
order = {},
folding = true, folding = true,
category_order = {}, },
queue = {},
},
keymaps = { keymaps = {
close = 'q', close = 'q',
toggle = '<CR>', toggle = '<CR>',
@ -132,7 +146,7 @@ end
---@return pending.ResolvedFolding ---@return pending.ResolvedFolding
function M.resolve_folding() function M.resolve_folding()
local raw = M.get().folding local raw = M.get().view.category.folding
if raw == false then if raw == false then
return { enabled = false, foldtext = false } return { enabled = false, foldtext = false }
elseif raw == true or raw == nil then elseif raw == true or raw == nil then

View file

@ -102,7 +102,7 @@ function M.category_view(tasks)
end end
end end
local cfg_order = config.get().category_order local cfg_order = config.get().view.category.order
if cfg_order and #cfg_order > 0 then if cfg_order and #cfg_order > 0 then
local ordered = {} local ordered = {}
local seen = {} local seen = {}

View file

@ -228,7 +228,7 @@ describe('views', function()
end) end)
it('respects category_order when set', function() it('respects category_order when set', function()
vim.g.pending = { data_path = tmpdir .. '/tasks.json', category_order = { 'Work', 'Inbox' } } vim.g.pending = { data_path = tmpdir .. '/tasks.json', view = { category = { order = { 'Work', 'Inbox' } } } }
config.reset() config.reset()
s:add({ description = 'Inbox task', category = 'Inbox' }) s:add({ description = 'Inbox task', category = 'Inbox' })
s:add({ description = 'Work task', category = 'Work' }) s:add({ description = 'Work task', category = 'Work' })
@ -248,7 +248,7 @@ describe('views', function()
end) end)
it('appends categories not in category_order after ordered ones', function() it('appends categories not in category_order after ordered ones', function()
vim.g.pending = { data_path = tmpdir .. '/tasks.json', category_order = { 'Work' } } vim.g.pending = { data_path = tmpdir .. '/tasks.json', view = { category = { order = { 'Work' } } } }
config.reset() config.reset()
s:add({ description = 'Errand', category = 'Errands' }) s:add({ description = 'Errand', category = 'Errands' })
s:add({ description = 'Work task', category = 'Work' }) s:add({ description = 'Work task', category = 'Work' })