*pending.txt* Buffer-centric task management for Neovim Author: Barrett Ruth 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 (``) - 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 |(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:` Set due date (accepts all |pending-dates| vocabulary). `cat:` Set category. `rec:` 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`) `` 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`) `` 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`) `` Increment priority (clamped at `max_priority`) (`priority_up`) `` 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!' } } < *(pending-open)* (pending-open) Open the task buffer. Maps to |:Pending| with no arguments. *(pending-close)* (pending-close) Close the task buffer window. *(pending-toggle)* (pending-toggle) Toggle complete / uncomplete for the task under the cursor. *(pending-priority)* (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|. *(pending-date)* (pending-date) Prompt for a due date for the task under the cursor. *(pending-view)* (pending-view) Switch between category view and priority view. *(pending-undo)* (pending-undo) Undo the last `:w` save. *(pending-filter)* (pending-filter) Prompt for filter predicates via |vim.ui.input|. *(pending-category)* (pending-category) Select a category for the task under the cursor via |vim.ui.select|. *(pending-recur)* (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. *(pending-move-down)* (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. *(pending-move-up)* (pending-move-up) Swap the task under the cursor with the one above it. *(pending-wip)* (pending-wip) Toggle work-in-progress status for the task under the cursor. If the task is already `wip`, reverts to `pending`. *(pending-blocked)* (pending-blocked) Toggle blocked status for the task under the cursor. If the task is already `blocked`, reverts to `pending`. *(pending-priority-up)* (pending-priority-up) Increment the priority level for the task under the cursor, clamped at `max_priority`. Default key: ``. *(pending-priority-down)* (pending-priority-down) Decrement the priority level for the task under the cursor, clamped at 0. Default key: ``. *(pending-open-line)* (pending-open-line) Insert a correctly-formatted blank task line below the cursor. *(pending-open-line-above)* (pending-open-line-above) Insert a correctly-formatted blank task line above the cursor. *(pending-a-task)* (pending-a-task) Select the current task line (linewise). Supports count. *(pending-i-task)* (pending-i-task) Select the task description text (characterwise). *(pending-a-category)* (pending-a-category) Select a full category section: header, tasks, and surrounding blanks. *(pending-i-category)* (pending-i-category) Select tasks within a category, excluding the header and blanks. *(pending-next-header)* (pending-next-header) Jump to the next category header. Supports count. *(pending-prev-header)* (pending-prev-header) Jump to the previous category header. Supports count. *(pending-next-task)* (pending-next-task) Jump to the next task line, skipping headers and blanks. *(pending-prev-task)* (pending-prev-task) Jump to the previous task line, skipping headers and blanks. (pending-tab) *(pending-tab)* Open the task buffer in a new tab. See |:PendingTab|. Example configuration: >lua vim.keymap.set('n', 't', '(pending-open)') vim.keymap.set('n', 'T', '(pending-toggle)') < ============================================================================== VIEWS *pending-views* Two views are available. Switch with `` or |(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:` Resolve a named date (see |pending-dates| below). `cat:Name` Move the task to the named category on save. `rec:` 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 `` 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 ``: 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 = '', view = '', 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) } < {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 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/.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 `. 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 push`. {pull} Pull-only action. Called by `:Pending pull`. {sync} Main sync action. Called by `:Pending 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.` 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 < ==============================================================================