Google Tasks sync + shared OAuth module (#60)

* feat(gtasks): add Google Tasks bidirectional sync

Problem: pending.nvim only supported one-way push to Google Calendar.
Users who use Google Tasks had no way to sync tasks bidirectionally.

Solution: add `lua/pending/sync/gtasks.lua` backend with OAuth PKCE
auth, push/pull/sync actions, and field mapping between pending tasks
and Google Tasks (category↔tasklist, `priority`/`recur` via notes).

* refactor(cli): promote sync backends to top-level subcommands

Problem: `:Pending sync gtasks auth` required an extra `sync` keyword
that added no value and made the command unnecessarily verbose.

Solution: route `gtasks` and `gcal` as top-level `:Pending` subcommands
via `SYNC_BACKEND_SET` lookup. Tab completion introspects backend
modules for available actions instead of hardcoding `{ 'auth', 'sync' }`.

* docs(gtasks): document Google Tasks backend and CLI changes

Problem: vimdoc had no coverage for the gtasks backend and still
referenced the old `:Pending sync <backend>` command form.

Solution: add `:Pending-gtasks` and `:Pending-gcal` command sections
with per-action docs, update sync backend interface, and add gtasks
config example.

* ci: format

* refactor(sync): extract shared OAuth into `oauth.lua`

Problem: `gcal.lua` and `gtasks.lua` duplicated ~250 lines of identical
OAuth code (token management, PKCE flow, credential loading, curl
helpers, url encoding).

Solution: Extract a shared `OAuthClient` metatable in `oauth.lua` with
module-level utilities and instance methods. Both backends now delegate
all OAuth to `oauth.new()`. Skip `oauth` in `health.lua` backend
discovery by checking for a `name` field.

* feat(sync): ship bundled OAuth credentials

Problem: Users must manually create a Google Cloud project and place a
credentials JSON file before sync works — terrible onboarding.

Solution: Add `client_id`/`client_secret` fields to `GcalConfig` and
`GtasksConfig`. `oauth.lua` resolves credentials in three tiers: config
fields, credentials file, then bundled defaults (placeholders for now).

* docs(sync): document bundled credentials and config fields

* ci: format
This commit is contained in:
Barrett Ruth 2026-03-05 01:21:18 -05:00
parent a6be248cbf
commit 6910bdb1be
7 changed files with 726 additions and 743 deletions

View file

@ -608,11 +608,8 @@ loads: >lua
sync = {
gcal = {
calendar = 'Pendings',
credentials_path = '/path/to/client_secret.json',
},
gtasks = {
credentials_path = '/path/to/client_secret.json',
},
gtasks = {},
},
}
<
@ -683,7 +680,9 @@ Fields: ~
{sync} (table, default: {}) *pending.SyncConfig*
Sync backend configuration. Each key is a backend
name and the value is the backend-specific config
table. Currently only `gcal` is built-in.
table. Built-in backends: `gcal`, `gtasks`. Both
ship bundled OAuth credentials so no setup is
needed beyond `:Pending <backend> auth`.
{icons} (table) *pending.Icons*
Icon characters displayed in the buffer. The
@ -934,12 +933,14 @@ Configuration: >lua
sync = {
gcal = {
calendar = 'Pendings',
credentials_path = '/path/to/client_secret.json',
},
},
}
<
No configuration is required to get started — bundled OAuth credentials are
used by default. Run `:Pending gcal auth` and the browser opens immediately.
*pending.GcalConfig*
Fields: ~
{calendar} (string, default: 'Pendings')
@ -947,13 +948,27 @@ Fields: ~
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
{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: ~
On the first `:Pending gcal` call the plugin detects that no refresh token
exists and opens the Google authorization URL in the browser using
@ -988,22 +1003,34 @@ created automatically on first sync.
Configuration: >lua
vim.g.pending = {
sync = {
gtasks = {
credentials_path = '/path/to/client_secret.json',
},
gtasks = {},
},
}
<
No configuration is required to get started — bundled OAuth credentials are
used by default. Run `:Pending gtasks auth` and the browser opens immediately.
*pending.GtasksConfig*
Fields: ~
{credentials_path} (string)
Path to the OAuth client secret JSON file downloaded
{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: ~
Same PKCE flow as the gcal backend; listens on port 18393. Tokens are stored
at `stdpath('data')/pending/gtasks_tokens.json`. Run `:Pending gtasks auth`