pending.nvim/doc/pending.txt
Barrett Ruth 1748e5caa1
feat(file-token): file: inline metadata token with gf navigation (#45)
* feat(file-token): add file: inline metadata token with gf navigation

Problem: there was no way to link a task to a specific location in a
source file, or to quickly jump from a task to the relevant code.

Solution: add a file:<path>:<line> inline token that stores a relative
file reference in task._extra.file. Virtual text renders basename:line
in a new PendingFile highlight group. A buffer-local gf mapping
(configurable via keymaps.goto_file) opens the file at the given line.
M.add_here() lets users attach the current cursor position to any task
via vim.ui.select(). M.edit() gains -file support to clear the
reference. <Plug>(pending-goto-file) and <Plug>(pending-add-here) are
exposed for custom mappings.

* test(file-token): add parse, diff, views, edit, and navigation tests

Problem: the file: token implementation had no test coverage.

Solution: add spec/file_spec.lua covering parse.body extraction,
malformed token handling, duplicate token stop-parsing, diff
reconciliation (store/update/clear/round-trip), LineMeta population
in both views, :Pending edit -file, and goto_file notify paths for
no-file and unreadable-file cases. All 292 tests pass.

* style: apply stylua formatting

* fix(types): remove empty elseif block, fix file? annotation nullability
2026-02-26 19:12:48 -05:00

1051 lines
44 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

*pending.txt* Buffer-centric task management for Neovim
Author: Barrett Ruth <br.barrettruth@gmail.com>
License: MIT
==============================================================================
INTRODUCTION *pending.nvim*
pending.nvim is a buffer-centric task manager for Neovim. Tasks live in a
plain, editable buffer — add with `o`, delete with `dd`, reorder with
`dd`/`p`, rename by typing. Writing the buffer with `:w` computes a diff
against the JSON store and applies only the changes. No floating windows,
no special UI, no abstraction between you and your tasks.
The buffer looks like this: >
School
! Read chapter 5 Feb 28
Submit homework Feb 25
Errands
Buy groceries Mar 01
Clean apartment
<
Category headers sit at column 0. Tasks are indented two spaces below them.
`!` marks a priority task. Due dates appear as right-aligned virtual text.
Completed tasks are rendered with strikethrough. Task IDs are embedded as
concealed tokens and are never visible during editing.
Features: ~
- Oil-style buffer editing: standard Vim motions for all task operations
- Inline metadata syntax: `due:`, `cat:`, `rec:`, and `file:` tokens parsed on `:w`
- Relative date input: `today`, `tomorrow`, `+Nd`, `+Nw`, `+Nm`, weekday
names, month names, ordinals, and more
- Recurring tasks with automatic next-date spawning on completion
- Two views: category (default) and queue (priority-sorted flat list)
- Multi-level undo (up to 20 `:w` saves, persisted across sessions)
- Quick-add from the command line with `:Pending add`
- Quickfix list of overdue/due-today tasks via `:Pending due`
- Foldable category sections (`zc`/`zo`) in category view
- Omnifunc completion for `cat:`, `due:`, and `rec:` tokens (`<C-x><C-o>`)
- Google Calendar one-way push via OAuth PKCE
==============================================================================
REQUIREMENTS *pending-requirements*
- Neovim 0.10+
- No external dependencies for local use
- `curl` and `openssl` are required for the `gcal` sync backend
==============================================================================
INSTALL *pending-install*
Install with lazy.nvim: >lua
{ 'barrettruth/pending.nvim' }
<
Install with luarocks: >vim
luarocks install pending.nvim
<
No `setup()` call is needed. The plugin loads automatically and works with
defaults. To customize behavior, set |vim.g.pending| before the plugin loads.
See |pending-config|.
==============================================================================
USAGE *pending-usage*
Open the task buffer: >vim
:Pending
<
The buffer named `pending://` opens in the current window. From there, use
standard Vim editing:
- `o` / `O` to add a new task line under or above the cursor
- `dd` to remove a task (deletion is applied on `:w`)
- `dd` + `p` to reorder tasks (pasted tasks receive new IDs)
- `:w` to save — all additions, deletions, and edits are diffed against the
store and committed atomically
Buffer-local keys are set automatically when the buffer opens. See
|pending-mappings| for the full list.
The buffer uses `buftype=acwrite` so `:w` always routes through pending.nvim's
write handler rather than writing to disk directly. The `pending://` buffer
persists across window switches; reopening with `:Pending` focuses the
existing window if one is open. The buffer is automatically reloaded from
disk when entered unmodified.
==============================================================================
INLINE METADATA *pending-metadata*
Metadata tokens may be appended to any task line before saving. Tokens are
parsed from the right and consumed until a non-metadata token is reached.
Supported tokens: ~
`due:YYYY-MM-DD` Set a due date using an absolute date.
`due:<name>` Resolve a named date (see |pending-dates| below).
`cat:Name` Move the task to the named category on save.
`rec:<pattern>` Set a recurrence rule (see |pending-recurrence|).
`file:<path>:<n>` Attach a file reference (see |pending-file-token|).
The token name for due dates defaults to `due` and is configurable via
`date_syntax` in |pending-config|. The token name for recurrence defaults to
`rec` and is configurable via `recur_syntax`.
Example: >
Buy milk due:2026-03-15 cat:Errands
Take out trash due:monday rec:weekly
<
On `:w`, the description becomes `Buy milk`, the due date is stored as
`2026-03-15` and rendered as right-aligned virtual text, and the task is
placed under the `Errands` category header.
Parsing stops at the first token that is not a recognised metadata token.
Repeated tokens of the same type also stop parsing — only one `due:`, one
`cat:`, one `rec:`, and one `file:` per task line are consumed.
Omnifunc completion is available for `due:`, `cat:`, and `rec:` token types.
In insert mode, type the token prefix and press `<C-x><C-o>` to see
suggestions.
==============================================================================
FILE TOKEN *pending-file-token*
The `file:` inline token attaches a source file reference to a task. The
syntax is: >
file:<relative-path>:<line-number>
<
The path is stored relative to the directory containing the data file. The
token is rendered as virtual text at the end of the task line, showing only
the basename and line number (e.g. `auth.lua:42`) using the |PendingFile|
highlight group.
Example: >
Fix null pointer file:src/auth.lua:42
Update tests file:spec/parse_spec.lua:100
<
`gf` in normal mode in the task buffer follows the file reference, opening
the file and jumping to the specified line. The default key is `gf` and can
be changed via the `goto_file` keymap in |pending-config|. Set it to `false`
to disable.
To attach the current file and cursor position to an existing task, invoke
|<Plug>(pending-add-here)| from any source file. A `vim.ui.select()` picker
lists all active tasks; selecting one records the current file and line.
To clear a file reference with `:Pending edit`: >vim
:Pending edit 5 -file
<
==============================================================================
DATE INPUT *pending-dates*
Named dates can be used anywhere a date is accepted: the `due:` inline
token, the `D` prompt, and `:Pending add`.
Token Resolves to ~
----- -----------
`today` Today's date
`tomorrow` Tomorrow's date
`yesterday` Yesterday's date
`eod` Today (end of day semantics)
`+Nd` N days from today (e.g. `+3d`)
`+Nw` N weeks from today (e.g. `+2w`)
`+Nm` N months from today (e.g. `+1m`)
`-Nd` N days ago (e.g. `-2d`)
`-Nw` N weeks ago (e.g. `-1w`)
`mon``sun` Next occurrence of that weekday
`jan``dec` 1st of next occurrence of that month
`1st``31st` Next occurrence of that day-of-month
`sow` / `eow` Monday / Sunday of current week
`som` / `eom` First / last day of current month
`soq` / `eoq` First / last day of current quarter
`soy` / `eoy` January 1 / December 31 of current year
`later` / `someday` Sentinel date (default: `9999-12-30`)
Time suffix: ~ *pending-dates-time*
Any named date or absolute date accepts an `@` time suffix. Supported
formats: `HH:MM` (24h), `H:MM`, bare hour (`9`, `14`), and am/pm
(`2pm`, `9:30am`, `12am`). All forms are normalized to `HH:MM` on save. >
due:tomorrow@2pm " tomorrow at 14:00
due:fri@9 " next Friday at 09:00
due:+1w@17:00 " one week from today at 17:00
due:tomorrow@9:30am " tomorrow at 09:30
due:2026-03-15@08:00 " absolute date with time
due:2026-03-15T14:30 " ISO 8601 datetime (also accepted)
<
Tasks with a time component are not considered overdue until after the
specified time. The time is displayed alongside the date in virtual text
and preserved across recurrence advances.
==============================================================================
RECURRENCE *pending-recurrence*
Tasks can recur on a schedule. Add a `rec:` token to set recurrence: >
- [ ] Take out trash due:monday rec:weekly
- [ ] Pay rent due:2026-03-01 rec:monthly
- [ ] Standup due:tomorrow rec:weekdays
<
When a recurring task is marked done with `<CR>`:
1. The current task stays as done (preserving history).
2. A new pending task is created with the same description, category,
priority, and recurrence — with the due date advanced to the next
occurrence.
Shorthand patterns: ~
Pattern Meaning ~
------- -------
`daily` Every day
`weekdays` Monday through Friday
`weekly` Every week
`biweekly` Every 2 weeks (alias: `2w`)
`monthly` Every month
`quarterly` Every 3 months (alias: `3m`)
`yearly` Every year (alias: `annual`)
`Nd` Every N days (e.g. `3d`)
`Nw` Every N weeks (e.g. `2w`)
`Nm` Every N months (e.g. `6m`)
`Ny` Every N years (e.g. `2y`)
For patterns the shorthand cannot express, use a raw RRULE fragment: >
rec:FREQ=MONTHLY;BYDAY=1MO
<
Completion-based recurrence: ~ *pending-recur-completion*
By default, recurrence is schedule-based: the next due date advances from the
original schedule, skipping to the next future occurrence. Prefix the pattern
with `!` for completion-based mode, where the next due date advances from the
completion date: >
rec:!weekly
<
Schedule-based is like org-mode `++`; completion-based is like `.+`.
Google Calendar: ~
Recurrence patterns map directly to iCalendar RRULE strings for future GCal
sync support. Completion-based recurrence cannot be synced (it is inherently
local).
==============================================================================
COMMANDS *pending-commands*
*:Pending*
:Pending
Open the task buffer. If the buffer is already displayed in a window,
focus that window. Equivalent to |<Plug>(pending-open)|.
*:Pending-add*
:Pending add {text}
Quick-add a task without opening the buffer. Inline metadata tokens in
{text} are parsed exactly as they are in the buffer. A `Category: ` prefix
(uppercase first letter, colon, space) assigns the category directly: >vim
:Pending add Buy groceries due:2026-03-15
:Pending add School: Submit homework
:Pending add Errands: Pick up dry cleaning due:fri
:Pending add Work: standup due:tomorrow rec:weekdays
<
If the buffer is currently open it is re-rendered after the add.
*:Pending-archive*
:Pending archive [{days}]
Permanently remove done and deleted tasks whose completion timestamp is
older than {days} days. {days} defaults to 30 if not provided. >vim
:Pending archive " remove tasks completed more than 30 days ago
:Pending archive 7 " remove tasks completed more than 7 days ago
<
*:Pending-due*
:Pending due
Populate the quickfix list with all tasks that are overdue or due today.
Open the list with |:copen| to navigate to each task's category.
*:Pending-sync*
:Pending sync {backend} [{action}]
Run a sync action against a named backend. {backend} is required — bare
`:Pending sync` prints a usage message. {action} defaults to `sync`
when omitted. Each backend lives at `lua/pending/sync/<name>.lua`.
Examples: >vim
:Pending sync gcal " runs gcal.sync()
:Pending sync gcal auth " runs gcal.auth()
:Pending sync gcal sync " explicit sync (same as bare)
<
Tab completion after `:Pending sync ` lists discovered backends.
Tab completion after `:Pending sync gcal ` lists available actions.
Built-in backends: ~
`gcal` Google Calendar one-way push. See |pending-gcal|.
*:Pending-filter*
:Pending filter {predicates}
Apply a filter to the task buffer. {predicates} is a space-separated list
of one or more predicate tokens. Only tasks matching all predicates (AND
semantics) are shown. Hidden tasks are not deleted — they are preserved in
the store and reappear when the filter is cleared. >vim
:Pending filter cat:Work
:Pending filter overdue
:Pending filter cat:Work overdue
:Pending filter priority
:Pending filter clear
<
When a filter is active the buffer's first line shows: >
FILTER: cat:Work overdue
<
The user can edit this line inline and `:w` to change the active filter.
Deleting the `FILTER:` line entirely and saving clears the filter.
`:Pending filter clear` also clears the filter programmatically.
Tab completion after `:Pending filter ` lists available predicates and
category values. Already-used predicates are excluded from completions.
See |pending-filters| for the full list of supported predicates.
*:Pending-edit*
:Pending edit {id} [{operations}]
Edit metadata on an existing task without opening the buffer. {id} is the
numeric task ID. One or more operations follow: >vim
:Pending edit 5 due:tomorrow cat:Work +!
:Pending edit 5 -due -cat -rec
:Pending edit 5 rec:!weekly due:fri
:Pending edit 5 -file
<
Operations: ~
`due:<date>` Set due date (accepts all |pending-dates| vocabulary).
`cat:<name>` Set category.
`rec:<pattern>` Set recurrence (prefix `!` for completion-based).
`+!` Add priority flag.
`-!` Remove priority flag.
`-due` Clear due date.
`-cat` Clear category.
`-rec` Clear recurrence.
`-file` Clear the attached file reference (see |pending-file-token|).
Tab completion is available for IDs, field names, date values, categories,
and recurrence patterns.
*:Pending-undo*
:Pending undo
Undo the last `:w` save, restoring the task store to its previous state.
Equivalent to the `U` buffer-local key (see |pending-mappings|). Up to 20
levels of undo are persisted across sessions.
==============================================================================
MAPPINGS *pending-mappings*
The following keys are set buffer-locally when the task buffer opens. They
are active only in the `pending://` buffer.
Buffer-local keys are configured via the `keymaps` table in |pending-config|.
The defaults are shown below. Set any key to `false` to disable it.
Default buffer-local keys: ~
Key Action ~
------- ------------------------------------------------
`q` Close the task buffer (`close`)
`<CR>` Toggle complete / uncomplete (`toggle`)
`!` Toggle the priority flag (`priority`)
`D` Prompt for a due date (`date`)
`<Tab>` Switch between category / queue view (`view`)
`U` Undo the last `:w` save (`undo`)
`o` Insert a new task line below (`open_line`)
`O` Insert a new task line above (`open_line_above`)
`gf` Open the file attached to the task under the cursor (`goto_file`)
`zc` Fold the current category section (category view only)
`zo` Unfold the current category section (category view only)
Text objects (operator-pending and visual): ~
Key Action ~
------- ------------------------------------------------
`at` Select the current task line (`a_task`)
`it` Select the task description only (`i_task`)
`aC` Select a category: header + tasks + blanks (`a_category`)
`iC` Select inner category: tasks only (`i_category`)
`at` supports count: `d3at` deletes three consecutive tasks. `it` selects
the description text between the checkbox prefix and trailing metadata
tokens (`due:`, `cat:`, `rec:`), making `cit` the natural way to retype a
task description without touching its metadata.
`aC` and `iC` are no-ops in the queue view (no headers to delimit).
Motions (normal, visual, operator-pending): ~
Key Action ~
------- ------------------------------------------------
`]]` Jump to the next category header (`next_header`)
`[[` Jump to the previous category header (`prev_header`)
`]t` Jump to the next task line (`next_task`)
`[t` Jump to the previous task line (`prev_task`)
All motions support count: `3]]` jumps three headers forward. `]]` and
`[[` are no-ops in the queue view. `]t` and `[t` work in both views.
`dd`, `p`, `P`, and `:w` work as standard Vim operations.
*<Plug>(pending-open)*
<Plug>(pending-open)
Open the task buffer. Maps to |:Pending| with no arguments.
*<Plug>(pending-close)*
<Plug>(pending-close)
Close the task buffer window.
*<Plug>(pending-toggle)*
<Plug>(pending-toggle)
Toggle complete / uncomplete for the task under the cursor.
*<Plug>(pending-priority)*
<Plug>(pending-priority)
Toggle the priority flag for the task under the cursor.
*<Plug>(pending-date)*
<Plug>(pending-date)
Prompt for a due date for the task under the cursor.
*<Plug>(pending-view)*
<Plug>(pending-view)
Switch between category view and priority view.
*<Plug>(pending-undo)*
<Plug>(pending-undo)
Undo the last `:w` save.
*<Plug>(pending-open-line)*
<Plug>(pending-open-line)
Insert a correctly-formatted blank task line below the cursor.
*<Plug>(pending-open-line-above)*
<Plug>(pending-open-line-above)
Insert a correctly-formatted blank task line above the cursor.
*<Plug>(pending-a-task)*
<Plug>(pending-a-task)
Select the current task line (linewise). Supports count.
*<Plug>(pending-i-task)*
<Plug>(pending-i-task)
Select the task description text (characterwise).
*<Plug>(pending-a-category)*
<Plug>(pending-a-category)
Select a full category section: header, tasks, and surrounding blanks.
*<Plug>(pending-i-category)*
<Plug>(pending-i-category)
Select tasks within a category, excluding the header and blanks.
*<Plug>(pending-next-header)*
<Plug>(pending-next-header)
Jump to the next category header. Supports count.
*<Plug>(pending-prev-header)*
<Plug>(pending-prev-header)
Jump to the previous category header. Supports count.
*<Plug>(pending-next-task)*
<Plug>(pending-next-task)
Jump to the next task line, skipping headers and blanks.
*<Plug>(pending-prev-task)*
<Plug>(pending-prev-task)
Jump to the previous task line, skipping headers and blanks.
*<Plug>(pending-goto-file)*
<Plug>(pending-goto-file)
Open the file attached to the task under the cursor. If the cursor is not
on a task line, or the task has no file reference, a warning is shown. If
the referenced file cannot be read, an error is shown.
See |pending-file-token|.
*<Plug>(pending-add-here)*
<Plug>(pending-add-here)
Attach the current file and cursor line to an existing task. Invoke from
any source file (not the pending buffer itself) to open a picker listing
all active tasks. The selected task receives a `file:` reference pointing
to the current buffer's file and the cursor's line number.
See |pending-file-token|.
Example configuration: >lua
vim.keymap.set('n', '<leader>t', '<Plug>(pending-open)')
vim.keymap.set('n', '<leader>T', '<Plug>(pending-toggle)')
<
==============================================================================
VIEWS *pending-views*
Two views are available. Switch with `<Tab>` or |<Plug>(pending-view)|.
Category view (default): ~ *pending-view-category*
Tasks are grouped under their category header. Categories appear in the
order tasks were added unless `category_order` is set (see
|pending-config|). Blank lines separate categories. Within each category,
pending tasks appear before done tasks. Priority tasks (`!`) are sorted
first within each group. Category sections are foldable with `zc` and
`zo`.
Queue view: ~ *pending-view-queue*
A flat list of all tasks sorted by priority, then by due date (tasks
without a due date sort last), then by internal order. Done tasks appear
after all pending tasks. Category names are shown as right-aligned virtual
text alongside the due date virtual text so tasks remain identifiable
across categories. The buffer is named `pending://queue`.
==============================================================================
FILTERS *pending-filters*
Filters narrow the task buffer to a subset of tasks without deleting any data.
Hidden tasks are preserved in the store and reappear when the filter is
cleared. Filter state is session-local — it does not persist across Neovim
restarts.
Set a filter with |:Pending-filter| or by editing the `FILTER:` line: >vim
:Pending filter cat:Work overdue
<
Multiple predicates are separated by spaces and combined with AND logic — a
task must match every predicate to be shown.
Available predicates: ~
`cat:X` Show only tasks whose category is exactly `X`. Tasks with no
category (assigned to `default_category`) are hidden unless
`default_category` matches `X`.
`overdue` Show only pending tasks with a due date strictly before today.
`today` Show only pending tasks with a due date equal to today.
`priority` Show only tasks with priority > 0 (the `!` marker).
`clear` Special value for |:Pending-filter| — clears the active filter
and shows all tasks.
FILTER: line: ~ *pending-filter-line*
When a filter is active, the first line of the task buffer is: >
FILTER: cat:Work overdue
<
This line is editable. Write the buffer with `:w` to apply the updated
predicates. Deleting the `FILTER:` line and saving clears the filter. The
line is highlighted with |PendingFilter| and does not appear in the stored
task data.
==============================================================================
CONFIGURATION *pending-config*
Configuration is done via `vim.g.pending`. Set this before the plugin
loads: >lua
vim.g.pending = {
data_path = vim.fn.stdpath('data') .. '/pending/tasks.json',
default_view = 'category',
default_category = 'Inbox',
date_format = '%b %d',
date_syntax = 'due',
recur_syntax = 'rec',
someday_date = '9999-12-30',
category_order = {},
keymaps = {
close = 'q',
toggle = '<CR>',
view = '<Tab>',
priority = '!',
date = 'D',
undo = 'U',
open_line = 'o',
open_line_above = 'O',
a_task = 'at',
i_task = 'it',
a_category = 'aC',
i_category = 'iC',
next_header = ']]',
prev_header = '[[',
next_task = ']t',
prev_task = '[t',
goto_file = 'gf',
},
sync = {
gcal = {
calendar = 'Tasks',
credentials_path = '/path/to/client_secret.json',
},
},
}
<
All fields are optional. Unset fields use the defaults shown above.
*pending.Config*
Fields: ~
{data_path} (string)
Path to the JSON file where tasks are stored.
Default: `stdpath('data') .. '/pending/tasks.json'`.
The directory is created automatically on first save.
{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: 'Inbox')
Category assigned to new tasks when no `cat:` token
is present and no `Category: ` prefix is used with
`:Pending add`.
{date_format} (string, default: '%b %d')
strftime format string used to render due dates as
virtual text in the buffer. Examples: `'%Y-%m-%d'`
for ISO dates, `'%d %b'` for day-first.
{date_syntax} (string, default: 'due')
The token name for inline due-date metadata. Change
this to use a different keyword, for example `'by'`
to write `by:2026-03-15` instead of `due:2026-03-15`.
{recur_syntax} (string, default: 'rec')
The token name for inline recurrence metadata. Change
this to use a different keyword, for example
`'repeat'` to write `repeat:weekly`.
{someday_date} (string, default: '9999-12-30')
The date that `later` and `someday` resolve to. This
acts as a "no date" sentinel for GTD-style workflows.
{category_order} (string[], default: {})
Ordered list of category names. In category view,
categories that appear in this list are shown in the
given order. Categories not in the list are appended
after the ordered ones in their natural order.
{keymaps} (table, default: see below) *pending.Keymaps*
Buffer-local key bindings. Each field maps an action
name to a key string. Set a field to `false` to
disable that binding. Unset fields use the default.
See |pending-mappings| for the full list of actions
and their default keys.
{goto_file} (string|false, default: 'gf')
Open the file attached to the task under the
cursor. Set to `false` to disable. See
|pending-file-token|.
{debug} (boolean, default: false)
Enable diagnostic logging. When `true`, textobj
motions, mapping registration, and cursor jumps
emit messages at `vim.log.levels.DEBUG`. Use
|:messages| to inspect the output. Useful for
diagnosing keymap conflicts (e.g. `]t` colliding
with Neovim defaults) or motion misbehavior.
Example: >lua
vim.g.pending = { debug = true }
<
{sync} (table, default: {}) *pending.SyncConfig*
Sync backend configuration. Each key is a backend
name and the value is the backend-specific config
table. Currently only `gcal` is built-in.
{gcal} (table, default: nil)
Legacy shorthand for `sync.gcal`. If `gcal` is set
but `sync.gcal` is not, the value is migrated
automatically. New configs should use `sync.gcal`
instead. See |pending.GcalConfig|.
==============================================================================
LUA API *pending-api*
The following functions are available on `require('pending')` for use in
statuslines, autocmds, and other integrations.
*pending.counts()*
pending.counts()
Returns a table of current task counts: >lua
{
overdue = 2, -- pending tasks past their due date/time
today = 1, -- pending tasks due today (not yet overdue)
pending = 10, -- total pending tasks (all statuses)
priority = 3, -- pending tasks with priority > 0
next_due = "2026-03-01", -- earliest future due date, or nil
}
<
The counts are read from a module-local cache that is invalidated on every
`:w`, toggle, date change, archive, undo, and sync. The first call triggers
a lazy `store.load()` if the store has not been loaded yet.
Done, deleted, and `someday` sentinel-dated tasks are excluded from the
`overdue` and `today` counts. The `someday` sentinel is the value of
`someday_date` in |pending-config| (default `9999-12-30`).
*pending.statusline()*
pending.statusline()
Returns a pre-formatted string suitable for embedding in a statusline:
- `"2 overdue, 1 today"` when both overdue and today counts are non-zero
- `"2 overdue"` when only overdue
- `"1 today"` when only today
- `""` (empty string) when nothing is actionable
*pending.has_due()*
pending.has_due()
Returns `true` when `overdue > 0` or `today > 0`. Useful as a conditional
for statusline components that should only render when tasks need attention.
*PendingStatusChanged*
PendingStatusChanged
A |User| autocmd event fired after every count recomputation. Use this to
trigger statusline refreshes or notifications: >lua
vim.api.nvim_create_autocmd('User', {
pattern = 'PendingStatusChanged',
callback = function()
vim.cmd.redrawstatus()
end,
})
<
==============================================================================
RECIPES *pending-recipes*
Configure blink.cmp to use pending.nvim's omnifunc as a completion source: >lua
require('blink.cmp').setup({
sources = {
per_filetype = {
pending = { 'omni', 'buffer' },
},
},
})
<
Lualine integration: >lua
require('lualine').setup({
sections = {
lualine_x = {
{
function() return require('pending').statusline() end,
cond = function() return require('pending').has_due() end,
},
},
},
})
<
Heirline integration: >lua
local Pending = {
condition = function() return require('pending').has_due() end,
provider = function() return require('pending').statusline() end,
}
<
Manual statusline: >vim
set statusline+=%{%v:lua.require('pending').statusline()%}
<
Startup notification: >lua
vim.api.nvim_create_autocmd('User', {
pattern = 'PendingStatusChanged',
once = true,
callback = function()
local c = require('pending').counts()
if c.overdue > 0 then
vim.notify(c.overdue .. ' overdue task(s)')
end
end,
})
<
Event-driven statusline refresh: >lua
vim.api.nvim_create_autocmd('User', {
pattern = 'PendingStatusChanged',
callback = function()
vim.cmd.redrawstatus()
end,
})
<
mini.ai integration: ~ *pending-mini-ai*
mini.ai (from mini.nvim) maps `a` and `i` as single-key handlers in
operator-pending and visual modes. It captures the next keystroke internally
rather than routing it through Neovim's mapping system, which means the
buffer-local `at`, `it`, `aC`, and `iC` maps never fire for users who have
mini.ai installed.
The fix is to register pending.nvim's text objects as mini.ai custom
textobjects via `vim.b.miniai_config` in a `FileType` autocmd. mini.ai's
`custom_textobjects` spec expects each entry to be a function returning
`{ from = { line, col }, to = { line, col } }` (1-indexed, col is
byte-offset from 1).
pending.nvim's `textobj.inner_task_range(line)` returns the start and end
column offsets within the current line. Combine it with the cursor row and
the buffer line to build the region tables mini.ai expects: >lua
vim.api.nvim_create_autocmd('FileType', {
pattern = 'pending',
callback = function()
local function task_inner()
local textobj = require('pending.textobj')
local row = vim.api.nvim_win_get_cursor(0)[1]
local line = vim.api.nvim_buf_get_lines(0, row - 1, row, false)[1]
if not line then return end
local s, e = textobj.inner_task_range(line)
if s > e then return end
return { from = { line = row, col = s }, to = { line = row, col = e } }
end
local function category_inner()
local textobj = require('pending.textobj')
local buffer = require('pending.buffer')
local meta = buffer.meta()
if not meta then return end
local row = vim.api.nvim_win_get_cursor(0)[1]
local header_row, last_row = textobj.category_bounds(row, meta)
if not header_row then return end
local first_task, last_task
for r = header_row + 1, last_row do
if meta[r] and meta[r].type == 'task' then
if not first_task then first_task = r end
last_task = r
end
end
if not first_task then return end
local first_line = vim.api.nvim_buf_get_lines(0, first_task - 1, first_task, false)[1] or ''
local last_line = vim.api.nvim_buf_get_lines(0, last_task - 1, last_task, false)[1] or ''
return {
from = { line = first_task, col = 1 },
to = { line = last_task, col = #last_line },
}
end
vim.b.miniai_config = {
custom_textobjects = { t = task_inner, C = category_inner },
}
end,
})
<
Note that the default `keymaps.a_task = 'at'` and friends still work in
standard Neovim operator-pending mode for users who do not have mini.ai. The
`vim.b.miniai_config` block is only needed when mini.ai is active.
`aC` (outer category) is not exposed here because mini.ai does not support
the linewise selection that `aC` requires. Use the buffer-local `aC` key
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.
==============================================================================
GOOGLE CALENDAR *pending-gcal*
pending.nvim can push tasks with due dates to a dedicated Google Calendar as
all-day events. This is a one-way push; changes made in Google Calendar are
not pulled back into pending.nvim.
Configuration: >lua
vim.g.pending = {
sync = {
gcal = {
calendar = 'Tasks',
credentials_path = '/path/to/client_secret.json',
},
},
}
<
The legacy `gcal` top-level key is still accepted and migrated automatically.
New configurations should use `sync.gcal`.
*pending.GcalConfig*
Fields: ~
{calendar} (string, default: 'Pendings')
Name of the Google Calendar to sync to. If a calendar
with this name does not exist it is created
automatically on the first sync.
{credentials_path} (string)
Path to the OAuth client secret JSON file downloaded
from the Google Cloud Console. Default:
`stdpath('data')..'/pending/gcal_credentials.json'`.
The file may be in the `installed` wrapper format
that Google provides or as a bare credentials object.
OAuth flow: ~
On the first `:Pending sync gcal` call the plugin detects that no refresh token
exists and opens the Google authorization URL in the browser using
|vim.ui.open()|. A temporary local HTTP server listens on port 18392 for the
OAuth redirect. The PKCE (Proof Key for Code Exchange) flow is used —
`openssl` generates the code challenge. After the user grants consent, the
authorization code is exchanged for tokens and the refresh token is stored at
`stdpath('data')/pending/gcal_tokens.json` with mode `600`. Subsequent syncs
use the stored refresh token and refresh the access token automatically when
it is about to expire.
`:Pending sync gcal` behavior: ~
For each task in the store:
- A pending task with a due date and no existing event: a new all-day event is
created and the event ID is stored in the task's `_extra` table.
- A pending task with a due date and an existing event: the event summary and
date are updated in place.
- A done or deleted task with an existing event: the event is deleted.
- A pending task with no due date that had an existing event: the event is
deleted.
A summary notification is shown after sync: `created: N, updated: N,
deleted: N`.
==============================================================================
SYNC BACKENDS *pending-sync-backend*
Sync backends are Lua modules under `lua/pending/sync/<name>.lua`. Each
module returns a table conforming to the backend interface: >lua
---@class pending.SyncBackend
---@field name string
---@field auth fun(): nil
---@field sync fun(): nil
---@field health? fun(): nil
<
Required fields: ~
{name} Backend identifier (matches the filename).
{sync} Main sync action. Called by `:Pending sync <name>`.
{auth} Authorization flow. Called by `:Pending sync <name> auth`.
Optional fields: ~
{health} Called by `:checkhealth pending` to report backend-specific
diagnostics (e.g. checking for external tools).
Backend-specific configuration goes under `sync.<name>` in |pending-config|.
==============================================================================
HIGHLIGHT GROUPS *pending-highlights*
pending.nvim defines the following highlight groups. All groups are set with
`default`, so colorschemes can override them by defining the group without
`default` before or after the plugin loads.
*PendingHeader*
PendingHeader Applied to category header lines (text at column 0).
Default: links to `Title`.
*PendingDue*
PendingDue Applied to the due date virtual text shown at the right
margin of each task line.
Default: links to `DiagnosticHint`.
*PendingOverdue*
PendingOverdue Applied to the due date virtual text of overdue tasks.
Default: links to `DiagnosticError`.
*PendingDone*
PendingDone Applied to the text of completed tasks.
Default: links to `Comment`.
*PendingPriority*
PendingPriority Applied to the `! ` priority marker on priority tasks.
Default: links to `DiagnosticWarn`.
*PendingRecur*
PendingRecur Applied to the recurrence indicator virtual text shown
alongside due dates for recurring tasks.
Default: links to `DiagnosticInfo`.
*PendingFilter*
PendingFilter Applied to the `FILTER:` header line shown at the top of
the buffer when a filter is active.
Default: links to `DiagnosticWarn`.
*PendingFile*
PendingFile Applied to the file reference virtual text shown for tasks
that have a `file:` token attached (see |pending-file-token|).
Displays the basename and line number (e.g. `auth.lua:42`).
Default: links to `Directory`.
To override a group in your colorscheme or config: >lua
vim.api.nvim_set_hl(0, 'PendingDue', { fg = '#aaaaaa', italic = true })
<
==============================================================================
HEALTH CHECK *pending-health*
Run |:checkhealth| pending to verify your setup: >vim
:checkhealth pending
<
Checks performed: ~
- Config loads without error
- Reports active configuration values (data path, default view, default
category, date format, date syntax)
- Whether the data directory exists (warning if not yet created)
- Whether the data file exists and can be parsed; reports total task count
- Validates recurrence specs on stored tasks
- Discovers sync backends under `lua/pending/sync/` and runs each backend's
`health()` function if it exists (e.g. gcal checks for `curl` and `openssl`)
==============================================================================
DATA FORMAT *pending-data*
Tasks are stored as JSON at `data_path`. The file is safe to edit by hand and
is forward-compatible — unknown fields are preserved on every read/write cycle
via the `_extra` table.
Schema: >
{
"version": 1,
"next_id": 42,
"tasks": [ ... ]
}
<
Task fields: ~
{id} (integer) Unique, auto-incrementing task identifier.
{description} (string) Task text as shown in the buffer.
{status} (string) `'pending'`, `'done'`, or `'deleted'`.
{category} (string) Category name. Defaults to `default_category`.
{priority} (integer) `1` for priority tasks, `0` otherwise.
{due} (string) ISO date string `YYYY-MM-DD`, or absent.
{recur} (string) Recurrence shorthand (e.g. `weekly`), or absent.
{recur_mode} (string) `'scheduled'` or `'completion'`, or absent.
{entry} (string) ISO 8601 UTC timestamp of creation.
{modified} (string) ISO 8601 UTC timestamp of last modification.
{end} (string) ISO 8601 UTC timestamp of completion or deletion.
{order} (integer) Relative ordering within a category.
Any field not in the list above is preserved in `_extra` and written back on
save. This is used internally to store the Google Calendar event ID
(`_gcal_event_id`) and allows third-party tooling to annotate tasks without
data loss.
The `version` field is checked on load. If the file version is newer than the
version the plugin supports, loading is aborted with an error message asking
you to update the plugin.
==============================================================================
vim:tw=78:ts=8:ft=help:norl: