Problem: doc/pending.txt was written before the undo stack, folds, :Pending due, D mapping, and BufEnter reload were added. Several entries were factually wrong (single-level undo, d vs D key, :Pending undo listed as non-existent) and highlight group defaults referenced stale hex colours. Solution: correct all factual errors and add missing entries — :Pending due command, :Pending undo command, zc/zo fold mappings, PendingOverdue highlight group, semantic link defaults for all groups, category fold docs, BufEnter auto-reload note, and multi-level undo description.
432 lines
19 KiB
Text
432 lines
19 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:` and `cat:` tokens parsed on `:w`
|
|
- Relative date input: `today`, `tomorrow`, `+Nd`, weekday names
|
|
- Two views: category (default) and priority flat list
|
|
- Multi-level undo (up to 20 `:w` saves, session-only)
|
|
- Quick-add from the command line with `:Pending add`
|
|
- Quickfix list of overdue/due-today tasks via `:Pending due`
|
|
- Foldable category sections (`zc`/`zo`) in category view
|
|
- Google Calendar one-way push via OAuth PKCE
|
|
|
|
==============================================================================
|
|
REQUIREMENTS *pending-requirements*
|
|
|
|
- Neovim 0.10+
|
|
- No external dependencies for local use
|
|
- `curl` and `openssl` are required for Google Calendar sync
|
|
|
|
==============================================================================
|
|
INSTALL *pending-install*
|
|
|
|
Install with lazy.nvim: >lua
|
|
{ 'barrettruth/pending.nvim' }
|
|
<
|
|
|
|
Install with luarocks: >vim
|
|
luarocks install pending.nvim
|
|
<
|
|
|
|
No `setup()` call is needed. The plugin loads automatically and works with
|
|
defaults. To customize behavior, set |vim.g.pending| before the plugin loads.
|
|
See |pending-config|.
|
|
|
|
==============================================================================
|
|
USAGE *pending-usage*
|
|
|
|
Open the task buffer: >vim
|
|
:Pending
|
|
<
|
|
|
|
The buffer named `pending://` opens in the current window. From there, use
|
|
standard Vim editing:
|
|
|
|
- `o` / `O` to add a new task line under or above the cursor
|
|
- `dd` to remove a task (deletion is applied on `:w`)
|
|
- `dd` + `p` to reorder tasks (pasted tasks receive new IDs)
|
|
- `:w` to save — all additions, deletions, and edits are diffed against the
|
|
store and committed atomically
|
|
|
|
Buffer-local keys are set automatically when the buffer opens. See
|
|
|pending-mappings| for the full list.
|
|
|
|
The buffer uses `buftype=acwrite` so `:w` always routes through pending.nvim's
|
|
write handler rather than writing to disk directly. The `pending://` buffer
|
|
persists across window switches; reopening with `:Pending` focuses the
|
|
existing window if one is open. The buffer is automatically reloaded from
|
|
disk when entered unmodified.
|
|
|
|
==============================================================================
|
|
INLINE METADATA *pending-metadata*
|
|
|
|
Metadata tokens may be appended to any task line before saving. Tokens are
|
|
parsed from the right and consumed until a non-metadata token is reached.
|
|
|
|
Supported tokens: ~
|
|
|
|
`due:YYYY-MM-DD` Set a due date using an absolute date.
|
|
`due:today` Resolve to today's date.
|
|
`due:tomorrow` Resolve to tomorrow's date.
|
|
`due:+Nd` Resolve to N days from today (e.g. `due:+3d`).
|
|
`due:mon` Resolve to the next occurrence of that weekday.
|
|
Supported: `sun` `mon` `tue` `wed` `thu` `fri` `sat`
|
|
`cat:Name` Move the task to the named category on save.
|
|
|
|
The token name for due dates defaults to `due` and is configurable via
|
|
`date_syntax` in |pending-config|. If `date_syntax` is set to `by`, write
|
|
`by:2026-03-15` instead.
|
|
|
|
Example: >
|
|
|
|
Buy milk due:2026-03-15 cat:Errands
|
|
<
|
|
|
|
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:` and one
|
|
`cat:` per task line are consumed.
|
|
|
|
==============================================================================
|
|
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
|
|
<
|
|
If the buffer is currently open it is re-rendered after the add.
|
|
|
|
*:Pending-archive*
|
|
:Pending archive [{days}]
|
|
Permanently remove done and deleted tasks whose completion timestamp is
|
|
older than {days} days. {days} defaults to 30 if not provided. >vim
|
|
:Pending archive " remove tasks completed more than 30 days ago
|
|
:Pending archive 7 " remove tasks completed more than 7 days ago
|
|
<
|
|
|
|
*:Pending-due*
|
|
:Pending due
|
|
Populate the quickfix list with all tasks that are overdue or due today.
|
|
Open the list with |:copen| to navigate to each task's category.
|
|
|
|
*:Pending-sync*
|
|
:Pending sync
|
|
Push pending tasks that have a due date to Google Calendar as all-day
|
|
events. Requires |pending-gcal| to be configured. See |pending-gcal| for
|
|
full details on what gets created, updated, and deleted.
|
|
|
|
*:Pending-undo*
|
|
:Pending undo
|
|
Undo the last `:w` save, restoring the task store to its previous state.
|
|
Equivalent to the `U` buffer-local key (see |pending-mappings|). Up to 20
|
|
levels of undo are retained per session.
|
|
|
|
==============================================================================
|
|
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: ~
|
|
|
|
Key Action ~
|
|
------- ------------------------------------------------
|
|
`<CR>` Toggle complete / uncomplete the task at cursor
|
|
`!` Toggle the priority flag on the task at cursor
|
|
`D` Prompt for a due date on the task at cursor
|
|
`<Tab>` Switch between category view and priority view
|
|
`U` Undo the last `:w` save
|
|
`g?` Show a help popup with available keys
|
|
`zc` Fold the current category section (category view only)
|
|
`zo` Unfold the current category section (category view only)
|
|
|
|
`o` and `O` are overridden to insert a correctly-formatted blank task line
|
|
at the position below or above the cursor rather than using standard Vim
|
|
indentation. `dd`, `p`, `P`, and `:w` work as expected.
|
|
|
|
*<Plug>(pending-open)*
|
|
<Plug>(pending-open)
|
|
Open the task buffer. Maps to |:Pending| with no arguments.
|
|
|
|
*<Plug>(pending-toggle)*
|
|
<Plug>(pending-toggle)
|
|
Toggle complete / uncomplete for the task under the cursor.
|
|
|
|
*<Plug>(pending-priority)*
|
|
<Plug>(pending-priority)
|
|
Toggle the priority flag for the task under the cursor.
|
|
|
|
*<Plug>(pending-date)*
|
|
<Plug>(pending-date)
|
|
Prompt for a due date for the task under the cursor.
|
|
|
|
*<Plug>(pending-view)*
|
|
<Plug>(pending-view)
|
|
Switch between category view and priority view.
|
|
|
|
Example configuration: >lua
|
|
vim.keymap.set('n', '<leader>t', '<Plug>(pending-open)')
|
|
vim.keymap.set('n', '<leader>T', '<Plug>(pending-toggle)')
|
|
<
|
|
|
|
==============================================================================
|
|
VIEWS *pending-views*
|
|
|
|
Two views are available. Switch with `<Tab>` or |<Plug>(pending-view)|.
|
|
|
|
Category view (default): ~ *pending-view-category*
|
|
Tasks are grouped under their category header. Categories appear in the
|
|
order tasks were added unless `category_order` is set (see
|
|
|pending-config|). Blank lines separate categories. Within each category,
|
|
pending tasks appear before done tasks. Priority tasks (`!`) are sorted
|
|
first within each group. Category sections are foldable with `zc` and
|
|
`zo`.
|
|
|
|
Priority view: ~ *pending-view-priority*
|
|
A flat list of all tasks sorted by priority, then by due date (tasks
|
|
without a due date sort last), then by internal order. Done tasks appear
|
|
after all pending tasks. Category names are shown as right-aligned virtual
|
|
text alongside the due date virtual text so tasks remain identifiable
|
|
across categories.
|
|
|
|
==============================================================================
|
|
CONFIGURATION *pending-config*
|
|
|
|
Configuration is done via `vim.g.pending`. Set this before the plugin
|
|
loads: >lua
|
|
vim.g.pending = {
|
|
data_path = vim.fn.stdpath('data') .. '/pending/tasks.json',
|
|
default_view = 'category',
|
|
default_category = 'Inbox',
|
|
date_format = '%b %d',
|
|
date_syntax = 'due',
|
|
category_order = {},
|
|
gcal = {
|
|
calendar = 'Tasks',
|
|
credentials_path = '/path/to/client_secret.json',
|
|
},
|
|
}
|
|
<
|
|
|
|
All fields are optional. Unset fields use the defaults shown above.
|
|
|
|
*pending.Config*
|
|
Fields: ~
|
|
{data_path} (string)
|
|
Path to the JSON file where tasks are stored.
|
|
Default: `stdpath('data') .. '/pending/tasks.json'`.
|
|
The directory is created automatically on first save.
|
|
|
|
{default_view} ('category'|'priority', default: 'category')
|
|
The view to use when the buffer is opened for the
|
|
first time in a session.
|
|
|
|
{default_category} (string, default: 'Inbox')
|
|
Category assigned to new tasks when no `cat:` token
|
|
is present and no `Category: ` prefix is used with
|
|
`:Pending add`.
|
|
|
|
{date_format} (string, default: '%b %d')
|
|
strftime format string used to render due dates as
|
|
virtual text in the buffer. Examples: `'%Y-%m-%d'`
|
|
for ISO dates, `'%d %b'` for day-first.
|
|
|
|
{date_syntax} (string, default: 'due')
|
|
The token name for inline due-date metadata. Change
|
|
this to use a different keyword, for example `'by'`
|
|
to write `by:2026-03-15` instead of `due:2026-03-15`.
|
|
|
|
{category_order} (string[], default: {})
|
|
Ordered list of category names. In category view,
|
|
categories that appear in this list are shown in the
|
|
given order. Categories not in the list are appended
|
|
after the ordered ones in their natural order.
|
|
|
|
{gcal} (table, default: nil)
|
|
Google Calendar sync configuration. See
|
|
|pending.GcalConfig|. Omit this field entirely to
|
|
disable Google Calendar sync.
|
|
|
|
==============================================================================
|
|
GOOGLE CALENDAR *pending-gcal*
|
|
|
|
pending.nvim can push tasks with due dates to a dedicated Google Calendar as
|
|
all-day events. This is a one-way push; changes made in Google Calendar are
|
|
not pulled back into pending.nvim.
|
|
|
|
Configuration: >lua
|
|
vim.g.pending = {
|
|
gcal = {
|
|
calendar = 'Tasks',
|
|
credentials_path = '/path/to/client_secret.json',
|
|
},
|
|
}
|
|
<
|
|
|
|
*pending.GcalConfig*
|
|
Fields: ~
|
|
{calendar} (string, default: 'Pendings')
|
|
Name of the Google Calendar to sync to. If a calendar
|
|
with this name does not exist it is created
|
|
automatically on the first sync.
|
|
|
|
{credentials_path} (string)
|
|
Path to the OAuth client secret JSON file downloaded
|
|
from the Google Cloud Console. Default:
|
|
`stdpath('data')..'/pending/gcal_credentials.json'`.
|
|
The file may be in the `installed` wrapper format
|
|
that Google provides or as a bare credentials object.
|
|
|
|
OAuth flow: ~
|
|
On the first `:Pending sync` call the plugin detects that no refresh token
|
|
exists and opens the Google authorization URL in the browser using
|
|
|vim.ui.open()|. A temporary local HTTP server listens on port 18392 for the
|
|
OAuth redirect. The PKCE (Proof Key for Code Exchange) flow is used —
|
|
`openssl` generates the code challenge. After the user grants consent, the
|
|
authorization code is exchanged for tokens and the refresh token is stored at
|
|
`stdpath('data')/pending/gcal_tokens.json` with mode `600`. Subsequent syncs
|
|
use the stored refresh token and refresh the access token automatically when
|
|
it is about to expire.
|
|
|
|
`:Pending sync` behavior: ~
|
|
For each task in the store:
|
|
- A pending task with a due date and no existing event: a new all-day event is
|
|
created and the event ID is stored in the task's `_extra` table.
|
|
- A pending task with a due date and an existing event: the event summary and
|
|
date are updated in place.
|
|
- A done or deleted task with an existing event: the event is deleted.
|
|
- A pending task with no due date that had an existing event: the event is
|
|
deleted.
|
|
|
|
A summary notification is shown after sync: `created: N, updated: N,
|
|
deleted: N`.
|
|
|
|
==============================================================================
|
|
HIGHLIGHT GROUPS *pending-highlights*
|
|
|
|
pending.nvim defines the following highlight groups. All groups are set with
|
|
`default`, so colorschemes can override them by defining the group without
|
|
`default` before or after the plugin loads.
|
|
|
|
*PendingHeader*
|
|
PendingHeader Applied to category header lines (text at column 0).
|
|
Default: links to `Title`.
|
|
|
|
*PendingDue*
|
|
PendingDue Applied to the due date virtual text shown at the right
|
|
margin of each task line.
|
|
Default: links to `DiagnosticHint`.
|
|
|
|
*PendingOverdue*
|
|
PendingOverdue Applied to the due date virtual text of overdue tasks.
|
|
Default: links to `DiagnosticError`.
|
|
|
|
*PendingDone*
|
|
PendingDone Applied to the text of completed tasks.
|
|
Default: links to `Comment`.
|
|
|
|
*PendingPriority*
|
|
PendingPriority Applied to the `! ` priority marker on priority tasks.
|
|
Default: links to `DiagnosticWarn`.
|
|
|
|
To override a group in your colorscheme or config: >lua
|
|
vim.api.nvim_set_hl(0, 'PendingDue', { fg = '#aaaaaa', italic = true })
|
|
<
|
|
|
|
==============================================================================
|
|
HEALTH CHECK *pending-health*
|
|
|
|
Run |:checkhealth| pending to verify your setup: >vim
|
|
:checkhealth pending
|
|
<
|
|
|
|
Checks performed: ~
|
|
- Config loads without error
|
|
- Reports active configuration values (data path, default view, default
|
|
category, date format, date syntax)
|
|
- Whether the data directory exists (warning if not yet created)
|
|
- Whether the data file exists and can be parsed; reports total task count
|
|
- Whether `curl` is available (required for Google Calendar sync)
|
|
- Whether `openssl` is available (required for OAuth PKCE)
|
|
|
|
==============================================================================
|
|
DATA FORMAT *pending-data*
|
|
|
|
Tasks are stored as JSON at `data_path`. The file is safe to edit by hand and
|
|
is forward-compatible — unknown fields are preserved on every read/write cycle
|
|
via the `_extra` table.
|
|
|
|
Schema: >
|
|
|
|
{
|
|
"version": 1,
|
|
"next_id": 42,
|
|
"tasks": [ ... ]
|
|
}
|
|
<
|
|
|
|
Task fields: ~
|
|
{id} (integer) Unique, auto-incrementing task identifier.
|
|
{description} (string) Task text as shown in the buffer.
|
|
{status} (string) `'pending'`, `'done'`, or `'deleted'`.
|
|
{category} (string) Category name. Defaults to `default_category`.
|
|
{priority} (integer) `1` for priority tasks, `0` otherwise.
|
|
{due} (string) ISO date string `YYYY-MM-DD`, or absent.
|
|
{entry} (string) ISO 8601 UTC timestamp of creation.
|
|
{modified} (string) ISO 8601 UTC timestamp of last modification.
|
|
{end} (string) ISO 8601 UTC timestamp of completion or deletion.
|
|
{order} (integer) Relative ordering within a category.
|
|
|
|
Any field not in the list above is preserved in `_extra` and written back on
|
|
save. This is used internally to store the Google Calendar event ID
|
|
(`_gcal_event_id`) and allows third-party tooling to annotate tasks without
|
|
data loss.
|
|
|
|
The `version` field is checked on load. If the file version is newer than the
|
|
version the plugin supports, loading is aborted with an error message asking
|
|
you to update the plugin.
|
|
|
|
==============================================================================
|
|
vim:tw=78:ts=8:ft=help:norl:
|