feat: Google Tasks bidirectional sync and CLI refactor (#59)
* 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
This commit is contained in:
parent
3e8fd0a6a3
commit
21628abe53
7 changed files with 1114 additions and 85 deletions
136
doc/pending.txt
136
doc/pending.txt
|
|
@ -41,6 +41,7 @@ Features: ~
|
|||
- Foldable category sections (`zc`/`zo`) in category view
|
||||
- Omnifunc completion for `cat:`, `due:`, and `rec:` tokens (`<C-x><C-o>`)
|
||||
- Google Calendar one-way push via OAuth PKCE
|
||||
- Google Tasks bidirectional sync via OAuth PKCE
|
||||
|
||||
==============================================================================
|
||||
CONTENTS *pending-contents*
|
||||
|
|
@ -63,15 +64,16 @@ CONTENTS *pending-contents*
|
|||
16. Recipes ............................................... |pending-recipes|
|
||||
17. Sync Backends ................................... |pending-sync-backend|
|
||||
18. Google Calendar .......................................... |pending-gcal|
|
||||
19. Data Format .............................................. |pending-data|
|
||||
20. Health Check ........................................... |pending-health|
|
||||
19. Google Tasks ............................................ |pending-gtasks|
|
||||
20. Data Format .............................................. |pending-data|
|
||||
21. Health Check ........................................... |pending-health|
|
||||
|
||||
==============================================================================
|
||||
REQUIREMENTS *pending-requirements*
|
||||
|
||||
- Neovim 0.10+
|
||||
- No external dependencies for local use
|
||||
- `curl` and `openssl` are required for the `gcal` sync backend
|
||||
- `curl` and `openssl` are required for the `gcal` and `gtasks` sync backends
|
||||
|
||||
==============================================================================
|
||||
INSTALL *pending-install*
|
||||
|
|
@ -146,24 +148,42 @@ COMMANDS *pending-commands*
|
|||
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 {backend} [{action}]
|
||||
Run a sync action against a named backend. {backend} is required — bare
|
||||
`:Pending sync` prints a usage message. {action} defaults to `sync`
|
||||
when omitted. Each backend lives at `lua/pending/sync/<name>.lua`.
|
||||
*:Pending-gtasks*
|
||||
:Pending gtasks [{action}]
|
||||
Run a Google Tasks sync action. {action} defaults to `sync` when omitted.
|
||||
|
||||
Actions: ~
|
||||
`sync` Push local changes then pull remote changes (default).
|
||||
`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 sync gcal " runs gcal.sync()
|
||||
:Pending sync gcal auth " runs gcal.auth()
|
||||
:Pending sync gcal sync " explicit sync (same as bare)
|
||||
:Pending gtasks " push then pull (default)
|
||||
:Pending gtasks push " push local → Google Tasks
|
||||
:Pending gtasks pull " pull Google Tasks → local
|
||||
:Pending gtasks auth " authorize
|
||||
<
|
||||
|
||||
Tab completion after `:Pending sync ` lists discovered backends.
|
||||
Tab completion after `:Pending sync gcal ` lists available actions.
|
||||
Tab completion after `:Pending gtasks ` lists available actions.
|
||||
See |pending-gtasks| for full details.
|
||||
|
||||
Built-in backends: ~
|
||||
*:Pending-gcal*
|
||||
:Pending gcal [{action}]
|
||||
Run a Google Calendar sync action. {action} defaults to `sync` when
|
||||
omitted.
|
||||
|
||||
`gcal` Google Calendar one-way push. See |pending-gcal|.
|
||||
Actions: ~
|
||||
`sync` Push tasks with due dates to Google Calendar (default).
|
||||
`auth` Run the OAuth authorization flow.
|
||||
|
||||
Examples: >vim
|
||||
:Pending gcal " push to Google Calendar (default)
|
||||
:Pending gcal auth " authorize
|
||||
<
|
||||
|
||||
Tab completion after `:Pending gcal ` lists available actions.
|
||||
See |pending-gcal| for full details.
|
||||
|
||||
*:Pending-filter*
|
||||
:Pending filter {predicates}
|
||||
|
|
@ -590,6 +610,9 @@ loads: >lua
|
|||
calendar = 'Pendings',
|
||||
credentials_path = '/path/to/client_secret.json',
|
||||
},
|
||||
gtasks = {
|
||||
credentials_path = '/path/to/client_secret.json',
|
||||
},
|
||||
},
|
||||
}
|
||||
<
|
||||
|
|
@ -870,21 +893,30 @@ Open tasks in a new tab on startup: >lua
|
|||
SYNC BACKENDS *pending-sync-backend*
|
||||
|
||||
Sync backends are Lua modules under `lua/pending/sync/<name>.lua`. Each
|
||||
module returns a table conforming to the backend interface: >lua
|
||||
backend is exposed as a top-level `:Pending` subcommand: >vim
|
||||
:Pending gtasks [action]
|
||||
:Pending gcal [action]
|
||||
<
|
||||
|
||||
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 health? fun(): nil
|
||||
<
|
||||
|
||||
Required fields: ~
|
||||
{name} Backend identifier (matches the filename).
|
||||
{sync} Main sync action. Called by `:Pending sync <name>`.
|
||||
{auth} Authorization flow. Called by `:Pending sync <name> auth`.
|
||||
{sync} Main sync action. Called by `:Pending <name>`.
|
||||
{auth} Authorization flow. Called by `:Pending <name> auth`.
|
||||
|
||||
Optional fields: ~
|
||||
{push} Push-only action. Called by `:Pending <name> push`.
|
||||
{pull} Pull-only action. Called by `:Pending <name> pull`.
|
||||
{health} Called by `:checkhealth pending` to report backend-specific
|
||||
diagnostics (e.g. checking for external tools).
|
||||
|
||||
|
|
@ -923,7 +955,7 @@ Fields: ~
|
|||
that Google provides or as a bare credentials object.
|
||||
|
||||
OAuth flow: ~
|
||||
On the first `:Pending sync gcal` call the plugin detects that no refresh token
|
||||
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 —
|
||||
|
|
@ -933,7 +965,7 @@ authorization code is exchanged for tokens and the refresh token is stored at
|
|||
use the stored refresh token and refresh the access token automatically when
|
||||
it is about to expire.
|
||||
|
||||
`:Pending sync gcal` behavior: ~
|
||||
`:Pending gcal` 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.
|
||||
|
|
@ -946,6 +978,67 @@ For each task in the store:
|
|||
A summary notification is shown after sync: `created: N, updated: N,
|
||||
deleted: N`.
|
||||
|
||||
==============================================================================
|
||||
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 = {
|
||||
credentials_path = '/path/to/client_secret.json',
|
||||
},
|
||||
},
|
||||
}
|
||||
<
|
||||
|
||||
*pending.GtasksConfig*
|
||||
Fields: ~
|
||||
{credentials_path} (string)
|
||||
Path to the 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.
|
||||
|
||||
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`
|
||||
to authorize; subsequent syncs refresh the token automatically.
|
||||
|
||||
`: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).
|
||||
|
||||
==============================================================================
|
||||
DATA FORMAT *pending-data*
|
||||
|
||||
|
|
@ -979,7 +1072,8 @@ Task fields: ~
|
|||
|
||||
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
|
||||
(`_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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue