fix: harden sync backends and fix edit recompute (#66)

* refactor(oauth): async coroutine support, pure-Lua PKCE, server hardening

Problem: OAuth module shelled out to openssl for PKCE, used blocking
`vim.system():wait()`, had a weak `os.time()` PRNG seed, and the TCP
callback server leaked on read errors with no timeout.

Solution: Add `M.system()` coroutine wrapper and `M.async()` helper,
replace openssl with `vim.fn.sha256` + `vim.base64.encode`, seed from
`vim.uv.hrtime()`, add `close_server()` guard with 120s timeout, and
close the server on read errors.

* fix(gtasks): async operations, error notifications, buffer refresh

Problem: Sync operations blocked the editor, `push_pass` silently
dropped delete/update/create API errors, and the buffer was not
re-rendered after push/pull/sync.

Solution: Wrap `push`, `pull`, `sync` in `oauth.async()`, add
`vim.notify` for all `push_pass` failure paths, and re-render the
pending buffer after each operation.

* fix(init): edit recompute, filter predicates, sync action listing

Problem: `M.edit()` skipped `_recompute_counts()` after saving,
`compute_hidden_ids` lacked `done`/`pending` predicates, and
`run_sync` defaulted to `sync` instead of listing available actions.

Solution: Replace `s:save()` with `_save_and_notify()` in `M.edit()`,
add `done` and `pending` filter predicates, and list backend actions
when no action is specified.

* refactor(gcal): per-category calendars, async push, error notifications

Problem: gcal used a single hardcoded calendar name, ran synchronously
blocking the editor, and silently dropped some API errors.

Solution: Fetch all calendars and map categories to calendars (creating
on demand), wrap push in `oauth.async()`, notify on individual API
failures, track `_gcal_calendar_id` in `_extra`, and remove the `$`
anchor from `next_day` pattern.

* refactor: formatting fixes, config cleanup, health simplification

Problem: Formatter disagreements in `init.lua` and `gtasks.lua`,
stale `calendar` field in gcal config, and redundant health checks
for data directory existence.

Solution: Apply stylua formatting, remove `calendar` field from
`pending.GcalConfig`, drop data-dir and no-file health messages,
add `done`/`pending` to filter tab-completion candidates.

* docs: update vimdoc for sync refactor, remove demo scripts

Problem: Docs still referenced openssl dependency, defaulting to `sync`
action, and the `calendar` config field. Demo scripts used the old
singleton `store` API.

Solution: Update vimdoc and README to reflect explicit actions, per-
category calendars, and pure-Lua PKCE. Remove stale demo scripts and
update sync specs to match new behavior.

* fix(types): correct LuaLS annotations in oauth and gcal
This commit is contained in:
Barrett Ruth 2026-03-05 11:50:13 -05:00
parent 6910bdb1be
commit ee362f7785
13 changed files with 319 additions and 291 deletions

View file

@ -73,7 +73,7 @@ REQUIREMENTS *pending-requirements*
- Neovim 0.10+
- No external dependencies for local use
- `curl` and `openssl` are required for the `gcal` and `gtasks` sync backends
- `curl` is required for the `gcal` and `gtasks` sync backends
==============================================================================
INSTALL *pending-install*
@ -149,17 +149,17 @@ COMMANDS *pending-commands*
Open the list with |:copen| to navigate to each task's category.
*:Pending-gtasks*
:Pending gtasks [{action}]
Run a Google Tasks sync action. {action} defaults to `sync` when omitted.
:Pending gtasks {action}
Run a Google Tasks action. An explicit action is required.
Actions: ~
`sync` Push local changes then pull remote changes (default).
`sync` Push local changes then pull remote changes.
`push` Push local changes to Google Tasks only.
`pull` Pull remote changes from Google Tasks only.
`auth` Run the OAuth authorization flow.
Examples: >vim
:Pending gtasks " push then pull (default)
:Pending gtasks sync " push then pull
:Pending gtasks push " push local → Google Tasks
:Pending gtasks pull " pull Google Tasks → local
:Pending gtasks auth " authorize
@ -169,16 +169,15 @@ COMMANDS *pending-commands*
See |pending-gtasks| for full details.
*:Pending-gcal*
:Pending gcal [{action}]
Run a Google Calendar sync action. {action} defaults to `sync` when
omitted.
:Pending gcal {action}
Run a Google Calendar action. An explicit action is required.
Actions: ~
`sync` Push tasks with due dates to Google Calendar (default).
`push` Push tasks with due dates to Google Calendar.
`auth` Run the OAuth authorization flow.
Examples: >vim
:Pending gcal " push to Google Calendar (default)
:Pending gcal push " push to Google Calendar
:Pending gcal auth " authorize
<
@ -606,9 +605,7 @@ loads: >lua
prev_task = '[t',
},
sync = {
gcal = {
calendar = 'Pendings',
},
gcal = {},
gtasks = {},
},
}
@ -893,8 +890,8 @@ SYNC BACKENDS *pending-sync-backend*
Sync backends are Lua modules under `lua/pending/sync/<name>.lua`. Each
backend is exposed as a top-level `:Pending` subcommand: >vim
:Pending gtasks [action]
:Pending gcal [action]
:Pending gtasks {action}
:Pending gcal {action}
<
Each module returns a table conforming to the backend interface: >lua
@ -902,9 +899,9 @@ Each module returns a table conforming to the backend interface: >lua
---@class pending.SyncBackend
---@field name string
---@field auth fun(): nil
---@field sync fun(): nil
---@field push? fun(): nil
---@field pull? fun(): nil
---@field sync? fun(): nil
---@field health? fun(): nil
<
@ -924,16 +921,15 @@ Backend-specific configuration goes under `sync.<name>` in |pending-config|.
==============================================================================
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.
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 = {
calendar = 'Pendings',
},
gcal = {},
},
}
<
@ -943,11 +939,6 @@ used by default. Run `:Pending gcal auth` and the browser opens immediately.
*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.
{client_id} (string, optional)
OAuth client ID. When set together with
{client_secret}, these take priority over the
@ -973,26 +964,24 @@ OAuth flow: ~
On the first `:Pending gcal` 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
OAuth redirect. The PKCE (Proof Key for Code Exchange) flow is used. 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 gcal` behavior: ~
`: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 and the event ID is stored in the task's `_extra` table.
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.
A summary notification is shown after sync: `created: N, updated: N,
deleted: N`.
==============================================================================
GOOGLE TASKS *pending-gtasks*