Problem: Incrementing or decrementing priority required operating on one task at a time with `<C-a>`/`<C-x>`, which is tedious when adjusting multiple tasks. Solution: Add `adjust_priority_visual(delta)` that iterates the visual selection range, updates every task line's priority in one pass, then re-renders once. Exposed as `increment_priority_visual()` / `decrement_priority_visual()` with `g<C-a>` / `g<C-x>` defaults, new `<Plug>` mappings, and config keys `priority_up_visual` / `priority_down_visual`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1631 lines
70 KiB
Text
1631 lines
70 KiB
Text
*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
|
||
- S3 whole-store sync via AWS CLI
|
||
- Forge links: reference GitHub/GitLab/Codeberg issues and PRs inline
|
||
|
||
==============================================================================
|
||
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. Forge Links ........................................... |pending-forge|
|
||
23. Data Format .............................................. |pending-data|
|
||
24. 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
|
||
- `aws` CLI is required for the `s3` 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.
|
||
|
||
==============================================================================
|
||
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 [{backend} [{sub-action}]]
|
||
Authenticate a sync backend. Without arguments, prompts with
|
||
|vim.ui.select| when multiple backends have auth methods. With an
|
||
explicit {backend} name, dispatches directly: >vim
|
||
:Pending auth gcal
|
||
:Pending auth gtasks
|
||
:Pending auth s3
|
||
:Pending auth gcal clear
|
||
:Pending auth gtasks reset
|
||
:Pending auth s3 profile
|
||
<
|
||
Google backends (gcal, gtasks): ~
|
||
Both share a single OAuth flow with combined scopes. If no credentials
|
||
are configured (bundled placeholders in use), the setup wizard runs
|
||
first to collect a client ID and secret. Sub-actions:
|
||
`clear` Remove OAuth tokens (forces re-authentication).
|
||
`reset` Remove tokens and credentials (full reset).
|
||
See |pending-google-auth| for details.
|
||
|
||
S3 backend: ~
|
||
Runs `aws sts get-caller-identity` to verify AWS credentials. If the
|
||
profile uses SSO and the session has expired, runs `aws sso login`
|
||
automatically. Sub-actions:
|
||
`profile` Prompt for an AWS profile name.
|
||
See |pending-s3| for details.
|
||
|
||
Auth is triggered automatically when running sync actions without valid
|
||
credentials. See |pending-sync-auto-auth|.
|
||
|
||
*: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-s3*
|
||
:Pending s3 {action}
|
||
Run an S3 sync action. An explicit action is required.
|
||
|
||
Actions: ~
|
||
`sync` Pull remote changes then push merged result.
|
||
`push` Upload the local store to S3.
|
||
`pull` Download the remote store and merge into local.
|
||
|
||
Examples: >vim
|
||
:Pending s3 sync " pull then push
|
||
:Pending s3 push " upload local → S3
|
||
:Pending s3 pull " download S3 → local
|
||
<
|
||
|
||
Tab completion after `:Pending s3 ` lists available actions.
|
||
See |pending-s3| for full details.
|
||
|
||
*:Pending-done*
|
||
:Pending done [{id}]
|
||
Mark a task as done without opening the buffer. {id} is the numeric task
|
||
ID. When {id} is omitted and the task buffer is open, the task under the
|
||
cursor is used. Recurring tasks spawn a new pending instance with the
|
||
next due date. >vim
|
||
:Pending done 5
|
||
:Pending done
|
||
<
|
||
|
||
*: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`)
|
||
`g<C-a>` Increment priority for visual selection (`priority_up_visual`)
|
||
`g<C-x>` Decrement priority for visual selection (`priority_down_visual`)
|
||
`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 categories defaults to `cat` and is configurable via
|
||
`category_syntax` in |pending-config|. The token name for due dates defaults
|
||
to `due` and is configurable via `date_syntax`. 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',
|
||
category_syntax = 'cat',
|
||
date_syntax = 'due',
|
||
recur_syntax = 'rec',
|
||
someday_date = '9999-12-30',
|
||
max_priority = 3,
|
||
view = {
|
||
default = 'category',
|
||
eol_format = '%l %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, 00–69 → 2000s, 70–99 → 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)
|
||
}
|
||
<
|
||
{category_syntax} (string, default: 'cat')
|
||
The token name for inline category metadata. Change
|
||
this to use a different keyword, for example
|
||
`'category'` to write `category:Work` instead of
|
||
`cat:Work`.
|
||
|
||
{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`, `s3`.
|
||
Google backends ship bundled OAuth credentials so
|
||
no setup is needed beyond `:Pending auth`. The S3
|
||
backend delegates to the AWS CLI credential chain.
|
||
See |pending-gcal|, |pending-gtasks|, |pending-s3|.
|
||
|
||
{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`.
|
||
|
||
*PendingForge*
|
||
PendingForge Applied to forge link virtual text (issue/PR reference).
|
||
Default: links to `DiagnosticInfo`.
|
||
|
||
*PendingForgeClosed*
|
||
PendingForgeClosed Applied to forge link virtual text when the remote
|
||
issue/PR is closed or merged.
|
||
Default: links to `Comment`.
|
||
|
||
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|.
|
||
|
||
Auto-auth: ~
|
||
*pending-sync-auto-auth*
|
||
Running a sync action (`:Pending <name> push/pull/sync`) without valid
|
||
credentials automatically triggers authentication before proceeding:
|
||
|
||
- OAuth backends (gcal, gtasks): if real credentials are configured but no
|
||
token exists, the browser-based auth flow starts automatically. On
|
||
success, the original action continues. Bundled placeholder credentials
|
||
cannot auto-auth and require the setup wizard via `:Pending auth`.
|
||
- S3: `aws sts get-caller-identity` runs before every sync action. If SSO
|
||
is expired, `aws sso login` is triggered automatically. Missing
|
||
credentials abort with an error pointing to |pending-s3|.
|
||
|
||
On auth failure, the sync action is aborted with an error message.
|
||
|
||
==============================================================================
|
||
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.
|
||
|
||
==============================================================================
|
||
FORGE LINKS *pending-forge*
|
||
|
||
Tasks can reference remote issues, pull requests, and merge requests from
|
||
GitHub, GitLab, and Codeberg (or Gitea). References are parsed from inline
|
||
tokens, concealed in the buffer, and rendered as configurable virtual text.
|
||
|
||
Inline syntax: ~
|
||
|
||
Two input forms, both parsed on `:w`:
|
||
|
||
Shorthand: ~
|
||
`gh:user/repo#42` GitHub issue or PR
|
||
`gl:group/project#15` GitLab issue or MR
|
||
`cb:user/repo#3` Codeberg issue or PR
|
||
|
||
Full URL: ~
|
||
`https://github.com/user/repo/issues/42`
|
||
`https://gitlab.com/group/project/-/merge_requests/15`
|
||
`https://codeberg.org/user/repo/issues/3`
|
||
|
||
Example: >
|
||
Fix login bug gh:user/repo#42 due:friday
|
||
<
|
||
|
||
On `:w`, the forge reference stays in the description and is also stored in
|
||
the task's `_extra._forge_ref` field. The raw token is visually replaced
|
||
inline with a formatted label using overlay extmarks (same technique as
|
||
checkbox icons). Multiple forge references in one line are each overlaid
|
||
independently.
|
||
|
||
Format string: ~
|
||
*pending-forge-format*
|
||
Each forge has a configurable `issue_format` string with these placeholders:
|
||
`%i` Forge icon (nerd font)
|
||
`%o` Repository owner
|
||
`%r` Repository name
|
||
`%n` Issue/PR number
|
||
|
||
Default: `'%i %o/%r#%n'` (e.g. ` user/repo#42`).
|
||
|
||
Configuration: ~
|
||
*pending.ForgeConfig*
|
||
>lua
|
||
vim.g.pending = {
|
||
forge = {
|
||
close = false,
|
||
validate = false,
|
||
warn_missing_cli = true,
|
||
github = {
|
||
icon = '',
|
||
issue_format = '%i %o/%r#%n',
|
||
instances = {},
|
||
},
|
||
gitlab = {
|
||
icon = '',
|
||
issue_format = '%i %o/%r#%n',
|
||
instances = {},
|
||
},
|
||
codeberg = {
|
||
icon = '',
|
||
issue_format = '%i %o/%r#%n',
|
||
instances = {},
|
||
},
|
||
},
|
||
}
|
||
<
|
||
|
||
Top-level fields: ~
|
||
{close} (boolean, default: false) When true, tasks linked to
|
||
closed/merged remote issues are automatically marked
|
||
done on buffer open. Only forges with an explicit
|
||
per-forge key (e.g. `github = {}`) are checked;
|
||
unconfigured forges are skipped entirely.
|
||
{validate} (boolean, default: false) When true, new or changed
|
||
forge refs are validated on `:w` by fetching metadata.
|
||
Logs a warning if the ref is not found, auth fails, or
|
||
the CLI is missing.
|
||
{warn_missing_cli} (boolean, default: true) When true, warns once per
|
||
forge per session if the CLI is missing or fails.
|
||
|
||
Fields (per forge): ~
|
||
{icon} (string) Nerd font icon used in virtual text.
|
||
{issue_format} (string) Format string for the inline overlay label.
|
||
{instances} (string[]) Additional hostnames for self-hosted instances
|
||
(e.g. `{ 'github.company.com' }`).
|
||
|
||
Authentication: ~
|
||
Forge metadata fetching uses each forge's native CLI. No tokens are
|
||
configured in pending.nvim — authenticate once in your shell:
|
||
1. GitHub: `gh auth login`
|
||
2. GitLab: `glab auth login`
|
||
3. Codeberg: `tea login add`
|
||
|
||
Public repositories work without authentication. Private repositories
|
||
require a logged-in CLI session.
|
||
|
||
Metadata fetching: ~
|
||
On buffer open, tasks with a `_forge_ref` whose cached metadata is older
|
||
than 5 minutes are re-fetched asynchronously. The buffer renders immediately
|
||
with cached data and updates extmarks when the fetch completes.
|
||
|
||
State pull: ~
|
||
Requires `forge.close = true`. After fetching, if the remote issue/PR
|
||
is closed or merged and the local task is pending/wip/blocked, the task is
|
||
automatically marked as done. Disabled by default. One-way: local status
|
||
changes do not push back to the forge.
|
||
|
||
Highlight groups: ~
|
||
|PendingForge| Open issue/PR link label
|
||
|PendingForgeClosed| Closed/merged issue/PR link label
|
||
|
||
==============================================================================
|
||
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 sync backend metadata:
|
||
- Google Calendar: `_gcal_event_id`, `_gcal_calendar_id`
|
||
- Google Tasks: `_gtasks_task_id`, `_gtasks_list_id`
|
||
- S3: `_s3_sync_id` (UUID for cross-device merge)
|
||
- Forge links: `_forge_ref` (parsed reference), `_forge_cache` (fetched state)
|
||
Third-party tooling can annotate tasks via `_extra` 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
|
||
<
|
||
|
||
==============================================================================
|