pending.nvim/doc/pending.txt
Barrett Ruth ec1d4c6092 feat: add <C-a> / <C-x> keymaps for priority increment/decrement
Problem: Priority could only be cycled with `g!` (0→1→2→3→0), with no
way to directly increment or decrement.

Solution: Add `adjust_priority()` with clamping at 0 and `max_priority`,
exposed as `increment_priority()` / `decrement_priority()` on `<C-a>` /
`<C-x>`. Includes `<Plug>` mappings and vimdoc.
2026-03-08 20:30:51 -04:00

1428 lines
62 KiB
Text
Raw Permalink 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:`, and `rec:` 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`
- Configurable category folds (`zc`/`zo`) with custom foldtext
- Omnifunc completion for `cat:`, `due:`, and `rec:` tokens (`<C-x><C-o>`)
- Google Calendar one-way push via OAuth PKCE
- Google Tasks bidirectional sync via OAuth PKCE
==============================================================================
CONTENTS *pending-contents*
1. Introduction ............................................. |pending.nvim|
2. Requirements ..................................... |pending-requirements|
3. Install ............................................... |pending-install|
4. Usage ................................................... |pending-usage|
5. Commands .............................................. |pending-commands|
6. Mappings .............................................. |pending-mappings|
7. Views ................................................... |pending-views|
8. Filters ............................................... |pending-filters|
9. Inline Metadata ....................................... |pending-metadata|
10. Date Input .............................................. |pending-dates|
11. Recurrence ......................................... |pending-recurrence|
12. Configuration ........................................... |pending-config|
13. Store Resolution .......................... |pending-store-resolution|
14. Highlight Groups .................................... |pending-highlights|
15. Lua API ................................................... |pending-api|
16. Recipes ............................................... |pending-recipes|
17. Sync Backends ................................... |pending-sync-backend|
18. Google Calendar .......................................... |pending-gcal|
19. Google Tasks ............................................ |pending-gtasks|
20. Google Authentication ......................... |pending-google-auth|
21. S3 Sync ................................................... |pending-s3|
22. Data Format .............................................. |pending-data|
23. Health Check ........................................... |pending-health|
==============================================================================
REQUIREMENTS *pending-requirements*
- Neovim 0.10+
- No external dependencies for local use
- `curl` is required for the `gcal` and `gtasks` sync backends
==============================================================================
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.
==============================================================================
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
:Pending add Buy milk due:fri +!!
<
Trailing `+!`, `+!!`, or `+!!!` tokens set the priority level (capped
at `max_priority`). If the buffer is currently open it is re-rendered
after the add.
*:Pending-archive*
:Pending archive [{duration}]
Permanently remove done and deleted tasks whose completion timestamp is
older than {duration}. {duration} defaults to 30 days if not provided.
Supported duration formats:
`Nd` N days (e.g. `7d`)
`Nw` N weeks (e.g. `3w` → 21 days)
`Nm` N months (e.g. `2m` → 60 days, approximated as N×30)
`N` bare integer, treated as days (backwards-compatible)
A confirmation prompt is shown before any tasks are removed. If no
tasks match the cutoff, a message is displayed and no prompt appears.
>vim
:Pending archive " 30-day default, with confirmation
:Pending archive 7d " tasks completed more than 7 days ago
:Pending archive 3w " tasks completed more than 21 days ago
:Pending archive 2m " tasks completed more than 60 days ago
:Pending archive 30 " bare integer, same as 30d
<
*: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-auth*
:Pending auth
Authorize pending.nvim to access Google services (Tasks and Calendar).
Prompts with |vim.ui.select| to choose gtasks, gcal, or both — all
options run the same combined OAuth flow and produce a single shared
token file. If no credentials are configured, the setup wizard runs
first to collect a client ID and secret.
See |pending-google-auth| for full details.
*:Pending-gtasks*
:Pending gtasks {action}
Run a Google Tasks action. An explicit action is required.
Actions: ~
`sync` Push local changes then pull remote changes.
`push` Push local changes to Google Tasks only.
`pull` Pull remote changes from Google Tasks only.
Examples: >vim
:Pending gtasks sync " push then pull
:Pending gtasks push " push local → Google Tasks
:Pending gtasks pull " pull Google Tasks → local
<
Tab completion after `:Pending gtasks ` lists available actions.
See |pending-gtasks| for full details.
*:Pending-gcal*
:Pending gcal {action}
Run a Google Calendar action. An explicit action is required.
Actions: ~
`push` Push tasks with due dates to Google Calendar.
Examples: >vim
:Pending gcal push " push to Google Calendar
<
Tab completion after `:Pending gcal ` lists available actions.
See |pending-gcal| for full details.
*: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. {id} is the numeric task ID. When
{id} is omitted and the task buffer is open, the task under the cursor
is used. This makes `:Pending edit +!` work without knowing the ID.
One or more operations follow: >vim
:Pending edit 5 due:tomorrow cat:Work +!
:Pending edit 5 -due -cat -rec
:Pending edit +!!
<
Operations: ~
`due:<date>` Set due date (accepts all |pending-dates| vocabulary).
`cat:<name>` Set category.
`rec:<pattern>` Set recurrence (prefix `!` for completion-based).
`+!` Set priority to 1.
`+!!` Set priority to 2.
`+!!!` Set priority to 3 (capped at `max_priority`).
`-!` Remove priority flag.
`-due` Clear due date.
`-cat` Clear category.
`-rec` Clear recurrence.
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 `gz` buffer-local key (see |pending-mappings|). Up to 20
levels of undo are persisted across sessions.
*:Pending-init*
:Pending init
Create a project-local `.pending.json` file in the current working
directory. After creation, `:Pending` will use this file instead of the
global store (see |pending-store-resolution|). Errors if `.pending.json`
already exists in the current directory.
*:PendingTab*
:PendingTab
Open the task buffer in a new tab.
==============================================================================
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`)
`g!` Cycle priority: 0→1→2→3→0 (`priority`)
`gd` Prompt for a due date (`date`)
`gc` Select a category from existing categories (`category`)
`gr` Prompt for a recurrence pattern (`recur`)
`gw` Toggle work-in-progress status (`wip`)
`gb` Toggle blocked status (`blocked`)
`gf` Prompt for filter predicates (`filter`)
`<Tab>` Switch between category / queue view (`view`)
`gz` 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`)
`<C-a>` Increment priority (clamped at `max_priority`) (`priority_up`)
`<C-x>` Decrement priority (clamped at 0) (`priority_down`)
`J` Move task down within its category (`move_down`)
`K` Move task up within its category (`move_up`)
`zc` Fold the current category section (requires `folding`)
`zo` Unfold the current category section (requires `folding`)
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.
Deprecated keys: ~ *pending-deprecated-keys*
The following keys were renamed to avoid shadowing Vim builtins. The old
keys still work but emit a deprecation warning and will be removed in a
future release:
Old New Action ~
------- ------- ------------------------------------------------
`!` `g!` Toggle the priority flag
`D` `gd` Prompt for a due date
`F` `gf` Prompt for filter predicates
`U` `gz` Undo the last `:w` save
To silence warnings, set the new keys explicitly in your config or set the
old keys to `false`: >lua
vim.g.pending = { keymaps = { priority = 'g!' } }
<
*<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)
Cycle the priority level for the task under the cursor (0→1→2→3→0).
The maximum level is controlled by `max_priority` in |pending-config|.
*<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-filter)*
<Plug>(pending-filter)
Prompt for filter predicates via |vim.ui.input|.
*<Plug>(pending-category)*
<Plug>(pending-category)
Select a category for the task under the cursor via |vim.ui.select|.
*<Plug>(pending-recur)*
<Plug>(pending-recur)
Prompt for a recurrence pattern for the task under the cursor.
Prefix with `!` for completion mode (e.g. `!weekly`). Empty input
removes recurrence.
*<Plug>(pending-move-down)*
<Plug>(pending-move-down)
Swap the task under the cursor with the one below it. In category
view, movement is limited to tasks within the same category.
*<Plug>(pending-move-up)*
<Plug>(pending-move-up)
Swap the task under the cursor with the one above it.
*<Plug>(pending-wip)*
<Plug>(pending-wip)
Toggle work-in-progress status for the task under the cursor.
If the task is already `wip`, reverts to `pending`.
*<Plug>(pending-blocked)*
<Plug>(pending-blocked)
Toggle blocked status for the task under the cursor.
If the task is already `blocked`, reverts to `pending`.
*<Plug>(pending-priority-up)*
<Plug>(pending-priority-up)
Increment the priority level for the task under the cursor, clamped
at `max_priority`. Default key: `<C-a>`.
*<Plug>(pending-priority-down)*
<Plug>(pending-priority-down)
Decrement the priority level for the task under the cursor, clamped
at 0. Default key: `<C-x>`.
*<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-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)')
<
==============================================================================
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,
tasks are sorted by status (wip → pending → blocked → done), then by
priority, then by insertion order. Category sections are foldable with
`zc` and `zo`.
Queue view: ~ *pending-view-queue*
A flat list of all tasks sorted by status (wip → pending → blocked →
done), then by priority, then by due date (tasks without a due date sort
last), then by internal order. 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|, the `F` buffer key, 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).
`wip` Show only tasks with status `wip` (work in progress).
`blocked` Show only tasks with status `blocked`.
`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.
==============================================================================
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|).
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:`, and one `rec:` 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.
==============================================================================
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`)
Custom formats: ~ *pending-dates-custom*
Additional input formats can be configured via `input_date_formats` in
|pending-config|. They are tried in order after all built-in keywords fail.
See |pending-input-formats| for supported specifiers and examples.
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).
==============================================================================
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_category = 'Todo',
date_format = '%b %d',
date_syntax = 'due',
recur_syntax = 'rec',
someday_date = '9999-12-30',
max_priority = 3,
view = {
default = 'category',
eol_format = '%c %r %d',
category = {
order = {},
folding = true,
},
queue = {},
},
keymaps = {
close = 'q',
toggle = '<CR>',
view = '<Tab>',
priority = 'g!',
date = 'gd',
undo = 'gz',
filter = 'gf',
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',
category = 'gc',
recur = 'gr',
move_down = 'J',
move_up = 'K',
wip = 'gw',
blocked = 'gb',
},
sync = {
gcal = {},
gtasks = {},
},
}
<
All fields are optional. Unset fields use the defaults shown above.
*pending.Config*
Fields: ~
{data_path} (string)
Path to the global JSON file where tasks are stored.
Default: `stdpath('data') .. '/pending/tasks.json'`.
The directory is created automatically on first save.
See |pending-store-resolution| for how the active
store is chosen at runtime.
{default_category} (string, default: 'Todo')
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.
{input_date_formats} (string[], default: {}) *pending-input-formats*
List of strftime-like format strings tried in order
when parsing a `due:` token that does not match the
built-in keywords or ISO `YYYY-MM-DD` format.
Specifiers supported: `%Y` (4-digit year), `%y`
(2-digit year, 0069 → 2000s, 7099 → 1900s), `%m`
(numeric month), `%d` / `%e` (day), `%b` / `%B`
(abbreviated or full month name, case-insensitive).
When no year specifier is present the current year is
used, advancing to next year if the date has already
passed. Examples: >lua
input_date_formats = {
'%m/%d/%Y', -- 03/15/2026
'%d-%b-%Y', -- 15-Mar-2026
'%m/%d', -- 03/15 (year inferred)
}
<
{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.
{view} (table) *pending.ViewConfig*
View rendering configuration. Groups all settings
that affect how the buffer displays tasks.
{default} ('category'|'priority', default: 'category')
The view to use when the buffer is opened
for the first time in a session.
{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:
{foldtext} (string|false) Format string
with `%c` (category) and `%n` (count).
`false` uses Vim's built-in foldtext.
Folds only apply to category view.
{queue} (table) *pending.QueueViewConfig*
Queue (priority) view settings.
Examples: >lua
vim.g.pending = {
view = {
default = 'priority',
eol_format = '%d | %r',
category = {
order = { 'Work', 'Personal' },
folding = { foldtext = '%c: %n items' },
},
},
}
<
{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.
{max_priority} (integer, default: 3)
Maximum priority level. The `g!` keymap cycles
through `0 → 1 → … → max_priority → 0`. Priority
levels map to highlight groups: `PendingPriority`
(1), `PendingPriority2` (2), `PendingPriority3`
(3+). `:Pending edit +!!` and `:Pending add +!!!`
accept multi-bang syntax capped at this value.
Set to `1` for the old binary on/off behavior.
{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. Built-in backends: `gcal`, `gtasks`. Both
ship bundled OAuth credentials so no setup is
needed beyond `:Pending <backend> auth`.
{icons} (table) *pending.Icons*
Icon characters displayed in the buffer. The
{pending}, {done}, {priority}, {wip}, and
{blocked} characters appear inside brackets
(`[icon]`) as an overlay on the checkbox. The
{category} character prefixes both header lines
and EOL category labels. Fields:
{pending} Pending task character. Default: ' '
{done} Done task character. Default: 'x'
{priority} Priority task character. Default: '!'
{wip} Work-in-progress character. Default: '>'
{blocked} Blocked task character. Default: '='
{due} Due date prefix. Default: '.'
{recur} Recurrence prefix. Default: '~'
{category} Category prefix. Default: '#'
==============================================================================
STORE RESOLUTION *pending-store-resolution*
When pending.nvim opens the task buffer it resolves which store file to use:
1. Search upward from `vim.fn.getcwd()` for a file named `.pending.json`.
2. If found, use that file as the active store (project-local store).
3. If not found, fall back to `data_path` from |pending-config| (global
store).
This means placing a `.pending.json` file in a project root makes that
project use an isolated task list. Tasks in the project store are completely
separate from tasks in the global store; there is no aggregation.
To create a project-local store in the current directory: >vim
:Pending init
<
The `:checkhealth pending` report shows which store file is currently active.
==============================================================================
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`.
*PendingWip*
PendingWip Applied to the checkbox icon of work-in-progress tasks.
Default: links to `DiagnosticInfo`.
*PendingBlocked*
PendingBlocked Applied to the checkbox icon and text of blocked tasks.
Default: links to `DiagnosticError`.
*PendingPriority*
PendingPriority Applied to the checkbox icon of priority 1 tasks.
Default: links to `DiagnosticWarn`.
*PendingPriority2*
PendingPriority2 Applied to the checkbox icon of priority 2 tasks.
Default: links to `DiagnosticError`.
*PendingPriority3*
PendingPriority3 Applied to the checkbox icon of priority 3+ tasks.
Default: links to `DiagnosticError`.
*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`.
To override a group in your colorscheme or config: >lua
vim.api.nvim_set_hl(0, 'PendingDue', { fg = '#aaaaaa', italic = true })
<
==============================================================================
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,
})
<
Nerd font icons: >lua
vim.g.pending = {
icons = {
due = '',
recur = '󰁯',
category = '',
},
}
<
Open tasks in a new tab on startup: >lua
vim.api.nvim_create_autocmd('VimEnter', {
callback = function()
vim.cmd.PendingTab()
end,
})
<
==============================================================================
SYNC BACKENDS *pending-sync-backend*
Sync backends are Lua modules under `lua/pending/sync/<name>.lua`. Backends
are auto-discovered at runtime — any module that exports a `name` field is
registered automatically. No hardcoded list or manual registration step is
required. Adding a backend is as simple as creating a new file.
Each backend is exposed as a top-level `:Pending` subcommand: >vim
:Pending gtasks {action}
:Pending gcal {action}
:Pending s3 {action}
<
Each module returns a table conforming to the backend interface: >lua
---@class pending.SyncBackend
---@field name string
---@field auth? fun(args?: string): nil
---@field auth_complete? fun(arg_lead: string): string[]
---@field push? fun(): nil
---@field pull? fun(): nil
---@field sync? fun(): nil
---@field health? fun(): nil
<
Required fields: ~
{name} Backend identifier (matches the filename).
Optional fields: ~
{auth} Per-backend authentication. Called by `:Pending auth <name>`.
Receives an optional sub-action string (e.g. `"clear"`).
{auth_complete} Returns valid sub-action completions for tab completion
(e.g. `{ "clear", "reset" }`).
{push} Push-only action. Called by `:Pending <name> push`.
{pull} Pull-only action. Called by `:Pending <name> pull`.
{sync} Main sync action. Called by `:Pending <name> sync`.
{health} Called by `:checkhealth pending` to report backend-specific
diagnostics (e.g. checking for external tools).
Modules without a `name` field (e.g. `oauth.lua`, `util.lua`) are ignored
by discovery and do not appear as backends.
Shared utilities for backend authors are provided by `sync/util.lua`:
`util.async(fn)` Coroutine wrapper for async operations.
`util.system(args)` Coroutine-aware `vim.system` wrapper.
`util.with_guard(name, fn)` Concurrency guard — prevents overlapping
sync operations. Clears on return or error.
`util.finish(s)` Persist store, recompute counts, re-render
the buffer. Typical sync epilogue.
`util.fmt_counts(parts)` Format `{ {n, label}, ... }` into a
human-readable summary string.
Backend-specific configuration goes under `sync.<name>` in |pending-config|.
==============================================================================
GOOGLE CALENDAR *pending-gcal*
pending.nvim can push tasks with due dates to Google Calendar as all-day
events. Each pending.nvim category maps to a Google Calendar of the same
name. Calendars are created automatically on first push. This is a one-way
push; changes made in Google Calendar are not pulled back.
Configuration: >lua
vim.g.pending = {
sync = {
gcal = {},
},
}
<
No configuration is required to get started — bundled OAuth credentials are
used by default. Run `:Pending auth` and the browser opens immediately.
*pending.GcalConfig*
Fields: ~
{client_id} (string, optional)
OAuth client ID. When set together with
{client_secret}, these take priority over the
credentials file and bundled defaults.
{client_secret} (string, optional)
OAuth client secret. See {client_id}.
{credentials_path} (string, optional)
Path to an 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.
Credential resolution: ~
Credentials are resolved in order:
1. `client_id` + `client_secret` config fields (highest priority).
2. JSON file at `credentials_path` (or the default path).
3. Bundled credentials shipped with the plugin (always available).
OAuth flow: ~
See |pending-google-auth|. Tokens are shared with the gtasks backend and
stored at `stdpath('data')/pending/google_tokens.json`.
`:Pending gcal push` 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 in the calendar matching the task's category. The event ID and
calendar ID are 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.
==============================================================================
GOOGLE TASKS *pending-gtasks*
pending.nvim can sync tasks bidirectionally with Google Tasks. Each
pending.nvim category maps to a Google Tasks list of the same name. Lists are
created automatically on first sync.
Configuration: >lua
vim.g.pending = {
sync = {
gtasks = {},
},
}
<
No configuration is required to get started — bundled OAuth credentials are
used by default. Run `:Pending auth` and the browser opens immediately.
*pending.GtasksConfig*
Fields: ~
{client_id} (string, optional)
OAuth client ID. When set together with
{client_secret}, these take priority over the
credentials file and bundled defaults.
{client_secret} (string, optional)
OAuth client secret. See {client_id}.
{credentials_path} (string, optional)
Path to an OAuth client secret JSON file downloaded
from the Google Cloud Console. Default:
`stdpath('data')..'/pending/gtasks_credentials.json'`.
Accepts the `installed` wrapper format or a bare
credentials object.
Credential resolution: ~
Same three-tier resolution as the gcal backend (see |pending-gcal|).
OAuth flow: ~
See |pending-google-auth|. Tokens are shared with the gcal backend and
stored at `stdpath('data')/pending/google_tokens.json`.
`:Pending gtasks` actions: ~
`:Pending gtasks` (or `:Pending gtasks sync`) runs push then pull. Use
`:Pending gtasks push` or `:Pending gtasks pull` to run only one direction.
Push (local → Google Tasks, `:Pending gtasks push`):
- Pending task with no `_gtasks_task_id`: created in the matching list.
- Pending task with an existing ID: updated in Google Tasks.
- Done task with an existing ID: marked `completed` in Google Tasks.
- Deleted task with an existing ID: deleted from Google Tasks.
Pull (Google Tasks → local, `:Pending gtasks pull`):
- GTasks task already known (matched by `_gtasks_task_id`): updated locally
if `gtasks.updated` timestamp is newer than `task.modified`.
- GTasks task not known locally: created as a new pending.nvim task in the
category matching the list name.
Field mapping: ~
{title} ↔ task description
{status} `needsAction` ↔ `pending`, `completed` ↔ `done`
{due} date-only; time component ignored (GTasks limitation)
{notes} serializes extra fields: `pri:1 rec:weekly`
The `notes` field is used exclusively for pending.nvim metadata. Any existing
notes on tasks created outside pending.nvim are parsed for known tokens and
the remainder is ignored.
Recurrence (`rec:`) is stored in notes for round-tripping but is not
expanded by Google Tasks (GTasks has no recurrence API).
==============================================================================
GOOGLE AUTHENTICATION *pending-google-auth*
Both the gcal and gtasks backends share a single OAuth client with combined
scopes (`tasks` + `calendar`). One authorization flow covers both services
and produces one token file.
:Pending auth ~
`:Pending auth` dispatches to per-backend `auth()` methods. When called
without arguments, if multiple backends have auth methods, a
|vim.ui.select| prompt lets you choose. With an explicit backend name,
the call goes directly: >vim
:Pending auth gcal
:Pending auth gtasks
:Pending auth gcal clear
:Pending auth gtasks reset
<
Sub-actions are backend-specific. Google backends support `clear` (remove
tokens) and `reset` (remove tokens and credentials). If no real credentials
are configured (i.e. bundled placeholders are in use), the setup wizard runs
first to collect a client ID and client secret before opening the browser.
OAuth flow: ~
A PKCE (Proof Key for Code Exchange) flow is used:
1. A random 64-character `code_verifier` is generated.
2. Its SHA-256 hash is base64url-encoded as the `code_challenge`.
3. The Google authorization URL is opened in the browser via |vim.ui.open()|.
4. A temporary TCP server on port 18392 waits up to 120 seconds for the
OAuth redirect.
5. The authorization code is exchanged for tokens via `curl`.
6. The refresh token is written to
`stdpath('data')/pending/google_tokens.json` with mode `600`.
7. Subsequent syncs refresh the access token automatically when it is about
to expire (within 60 seconds of the `expires_in` window).
Credential resolution: ~
Credentials are resolved in order for the `google` config key:
1. `client_id` + `client_secret` under `sync.google` (highest priority).
2. JSON file at `sync.google.credentials_path` or the default path
`stdpath('data')/pending/google_credentials.json`.
3. Bundled placeholder credentials (always available; trigger setup wizard).
The `installed` wrapper format from the Google Cloud Console is accepted.
==============================================================================
S3 SYNC *pending-s3*
pending.nvim can sync the task store to an S3 bucket. This enables
whole-store synchronization between machines via the AWS CLI.
Configuration: >lua
vim.g.pending = {
sync = {
s3 = {
bucket = 'my-tasks-bucket',
key = 'pending.json', -- optional, default "pending.json"
profile = 'personal', -- optional AWS CLI profile
region = 'us-east-1', -- optional region override
},
},
}
<
*pending.S3Config*
Fields: ~
{bucket} (string, required)
S3 bucket name.
{key} (string, optional, default `"pending.json"`)
S3 object key (path within the bucket).
{profile} (string, optional)
AWS CLI profile name. Maps to `--profile`.
{region} (string, optional)
AWS region override. Maps to `--region`.
Credential resolution: ~
Delegates entirely to the AWS CLI credential chain (environment variables,
~/.aws/credentials, IAM roles, SSO, etc.). No credentials are stored by
pending.nvim.
Auth flow: ~
`:Pending auth s3` runs `aws sts get-caller-identity` to verify credentials.
If the profile uses SSO and the session has expired, it automatically runs
`aws sso login`. Sub-action `profile` prompts for a profile name.
`:Pending s3 push` behavior: ~
Assigns a `_s3_sync_id` (UUID) to each task that lacks one, serializes the
store to a temp file, and uploads it to S3 via `aws s3 cp`.
`:Pending s3 pull` behavior: ~
Downloads the remote store from S3, then merges per-task by `_s3_sync_id`:
- Remote task with a matching local task: the version with the newer
`modified` timestamp wins.
- Remote task with no local match: added to the local store.
- Local tasks not present in the remote: kept (local-only tasks are never
deleted by pull).
`:Pending s3 sync` behavior: ~
Pulls first (merge), then pushes the merged result.
==============================================================================
DATA FORMAT *pending-data*
Tasks are stored as JSON at the active store path (see
|pending-store-resolution|). 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'`, `'wip'`, `'blocked'`, `'done'`,
or `'deleted'`.
{category} (string) Category name. Defaults to `default_category`.
{priority} (integer) Priority level: `0` (none), `1``3` (or up to
`max_priority`). Higher values sort first.
{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 Google Tasks IDs (`_gtasks_task_id`,
`_gtasks_list_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.
==============================================================================
HEALTH CHECK *pending-health*
Run |:checkhealth| pending to verify your setup: >vim
:checkhealth pending
<
==============================================================================