feat: overdue highlighting, relative dates, undo write, buffer mappings (#1)
* feat(config): add category_order field Problem: category display order was always insertion order with no way to configure it. Solution: add category_order to config defaults so users can declare a preferred category ordering; unspecified categories append after. * feat(parse): add relative date resolution Problem: due dates required full YYYY-MM-DD input, adding friction for common cases like "today" or "next monday". Solution: add resolve_date() supporting today, tomorrow, +Nd, and weekday abbreviations; extend inline token parsing to resolve relative values before falling back to strict date validation. * feat(views): overdue flag, category in priority view, category ordering Problem: overdue tasks were visually indistinct from upcoming ones; priority view had no category context; category display order was not configurable. Solution: compute overdue meta flag for pending tasks past their due date; set show_category on priority view task meta; reorder categories according to config.category_order when present. * feat(buffer): overdue highlight, category virt text in priority view Problem: overdue tasks had no visual distinction; priority view showed no category context alongside due dates. Solution: add PendingOverdue highlight group; render category name as right-aligned virtual text in priority view, composited with the due date when both are present. * feat(init): undo write and buffer-local default mappings Problem: _undo_state was captured on every save but never consumed; toggle_priority and prompt_date had no buffer-local defaults, requiring manual <Plug> configuration. Solution: implement undo_write() to restore pre-save task state; add !, d, and U as buffer-local defaults following fugitive's philosophy of owning the buffer; expose :Pending undo as a command alias. * test(views): add views spec Problem: views.lua had no test coverage. Solution: add 26 tests covering category_view and priority_view including sort order, line format, overdue detection, show_category meta, and category_order config behavior. * test(archive): add archive spec Problem: archive had no test coverage. Solution: add 9 tests covering cutoff logic, custom day counts, pending task preservation, deleted task cleanup, and notify output. * docs: add vimdoc Problem: no :help documentation existed. Solution: add doc/pending.txt covering all features — commands, mappings, views, configuration, Google Calendar sync, highlight groups, data format, and health check — following standard vimdoc conventions. * ci: format * fix: resolve lint and type check errors Problem: selene flagged unused variables in new spec files; LuaLS flagged os.date/os.time return type mismatches, integer? assignments, and stale task.Task/task.GcalConfig type references. Solution: prefix unused spec variables with _ or drop unnecessary assignments; add --[[@as string/integer]] casts for os.date and os.time calls; add category_order field to pending.Config annotation; fix task.GcalConfig -> pending.GcalConfig and task.Task[] -> pending.Task[]; add nil guards on meta[row].id before store calls; cast store.data() return to non-optional. * ci: format * fix: sync * ci: format
This commit is contained in:
parent
0727a03d41
commit
f21658f138
11 changed files with 1137 additions and 30 deletions
415
doc/pending.txt
Normal file
415
doc/pending.txt
Normal file
|
|
@ -0,0 +1,415 @@
|
|||
*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
|
||||
- Single-level undo for the last `:w` save
|
||||
- Quick-add from the command line with `:Pending add`
|
||||
- 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.
|
||||
|
||||
==============================================================================
|
||||
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-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
|
||||
There is no `:Pending undo` subcommand. Use the `U` buffer-local key
|
||||
(see |pending-mappings|) to undo the last `:w` save while the task buffer
|
||||
is open.
|
||||
|
||||
==============================================================================
|
||||
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
|
||||
|
||||
Standard Vim keys `o`, `O`, `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.
|
||||
|
||||
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: bold.
|
||||
|
||||
*PendingDue*
|
||||
PendingDue Applied to the due date virtual text shown at the right
|
||||
margin of each task line.
|
||||
Default: fg=#888888, italic.
|
||||
|
||||
*PendingDone*
|
||||
PendingDone Applied to the text of completed tasks.
|
||||
Default: strikethrough, fg=#666666.
|
||||
|
||||
*PendingPriority*
|
||||
PendingPriority Applied to the `! ` priority marker on priority tasks.
|
||||
Default: fg=#e06c75, bold.
|
||||
|
||||
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:
|
||||
Loading…
Add table
Add a link
Reference in a new issue