From f78f8e42fad2bca13a043c4f14fecedb0b7c87ed Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:29:32 -0500 Subject: [PATCH] feat(sync): interactive setup, auth continuation, and credential resolution fixes (#70) * feat(sync): add `setup` command to configure credentials interactively Problem: users had to manually create a JSON credentials file at the correct path before authenticating, with no guidance from the plugin. Solution: add `OAuthClient:setup()` that prompts for client ID and secret via `vim.ui.input`, writes to the shared `google_credentials.json`, then immediately starts the OAuth flow. Expose as `:Pending {gtasks,gcal} setup`. Also extend `resolve_credentials()` to fall back to a shared `google_credentials.json` so one file covers both backends. * fix(sync): improve `setup` input loop with validation and masking Problem: `setup()` used async `vim.ui.input` for both prompts, causing newline and re-prompt issues when validation failed. The secret was also echoed in plain text. Solution: switch to synchronous `vim.fn.input` / `vim.fn.inputsecret` loops with `vim.cmd.redraw()` + `nvim_echo` for inline error display and re-prompting. Validate client ID format and `GOCSPX-` secret prefix before saving. * fix(oauth): fix `ipairs` nil truncation in `resolve_credentials` and add file-path setup option Problem: `resolve_credentials` built `cred_paths` with a potentially nil first element (`credentials_path`), causing `ipairs` to stop immediately and always fall through to bundled placeholder credentials. Solution: build `cred_paths` without nil entries using `table.insert`. Also add a `2. Load from JSON file path` option to `setup()` via `vim.fn.inputlist`, with `vim.fn.expand` for `~`/`$HOME` support and the `installed` wrapper unwrap. * doc: cleanup * ci: format --- README.md | 19 +----- lua/pending/sync/gcal.lua | 4 ++ lua/pending/sync/gtasks.lua | 4 ++ lua/pending/sync/oauth.lua | 114 ++++++++++++++++++++++++++++++++---- 4 files changed, 115 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 43c8447..356096a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # pending.nvim -Edit tasks like text. `:w` saves them. +Edit tasks like text. Inspired by +[oil.nvim](https://github.com/stevearc/oil.nvim), +[vim-fugitive](https://github.com/tpope/vim-fugitive) ![demo](assets/demo.gif) @@ -24,21 +26,6 @@ luarocks install pending.nvim :help pending.nvim ``` -## Icons - -All display characters are configurable. Defaults produce markdown-style checkboxes (`[ ]`, `[x]`, `[!]`): - -```lua -vim.g.pending = { - icons = { - pending = ' ', done = 'x', priority = '!', - due = '.', recur = '~', category = '#', - }, -} -``` - -See `:help pending.Icons` for nerd font examples. - ## Acknowledgements - [dooing](https://github.com/atiladefreitas/dooing) diff --git a/lua/pending/sync/gcal.lua b/lua/pending/sync/gcal.lua index 69c175d..53a9111 100644 --- a/lua/pending/sync/gcal.lua +++ b/lua/pending/sync/gcal.lua @@ -155,6 +155,10 @@ local function with_token(callback) end) end +function M.setup() + client:setup() +end + function M.auth() client:auth() end diff --git a/lua/pending/sync/gtasks.lua b/lua/pending/sync/gtasks.lua index d383c77..1bf3848 100644 --- a/lua/pending/sync/gtasks.lua +++ b/lua/pending/sync/gtasks.lua @@ -371,6 +371,10 @@ local function with_token(callback) end) end +function M.setup() + client:setup() +end + function M.auth() client:auth() end diff --git a/lua/pending/sync/oauth.lua b/lua/pending/sync/oauth.lua index 88eaf35..9e13870 100644 --- a/lua/pending/sync/oauth.lua +++ b/lua/pending/sync/oauth.lua @@ -166,18 +166,26 @@ function OAuthClient:resolve_credentials() } end - local cred_path = backend_cfg.credentials_path - or (vim.fn.stdpath('data') .. '/pending/' .. self.name .. '_credentials.json') - local creds = M.load_json_file(cred_path) - if creds then - if creds.installed then - creds = creds.installed - end - if creds.client_id and creds.client_secret then - return creds --[[@as pending.OAuthCredentials]] + local data_dir = vim.fn.stdpath('data') .. '/pending/' + local cred_paths = {} + if backend_cfg.credentials_path then + table.insert(cred_paths, backend_cfg.credentials_path) + end + table.insert(cred_paths, data_dir .. self.name .. '_credentials.json') + table.insert(cred_paths, data_dir .. 'google_credentials.json') + for _, cred_path in ipairs(cred_paths) do + if cred_path then + local creds = M.load_json_file(cred_path) + if creds then + if creds.installed then + creds = creds.installed + end + if creds.client_id and creds.client_secret then + return creds --[[@as pending.OAuthCredentials]] + end + end end end - return { client_id = BUNDLED_CLIENT_ID, client_secret = BUNDLED_CLIENT_SECRET, @@ -251,6 +259,92 @@ function OAuthClient:get_access_token() return tokens.access_token end +---@return nil +function OAuthClient:setup() + local choice = vim.fn.inputlist({ + self.name .. ' setup:', + '1. Enter client ID and secret', + '2. Load from JSON file path', + }) + vim.cmd.redraw() + + local id, secret + + if choice == 1 then + while true do + id = vim.trim(vim.fn.input(self.name .. ' client ID: ')) + if id == '' then + return + end + if id:match('^%d+%-[%w_]+%.apps%.googleusercontent%.com$') then + break + end + vim.cmd.redraw() + vim.api.nvim_echo({ + { + 'invalid client ID — expected -.apps.googleusercontent.com', + 'ErrorMsg', + }, + }, false, {}) + end + + while true do + secret = vim.trim(vim.fn.inputsecret(self.name .. ' client secret: ')) + if secret == '' then + return + end + if secret:match('^GOCSPX%-') then + break + end + vim.cmd.redraw() + vim.api.nvim_echo( + { { 'invalid client secret — expected GOCSPX-...', 'ErrorMsg' } }, + false, + {} + ) + end + elseif choice == 2 then + local fpath + while true do + fpath = vim.trim(vim.fn.input(self.name .. ' credentials file: ', '', 'file')) + if fpath == '' then + return + end + fpath = vim.fn.expand(fpath) + local creds = M.load_json_file(fpath) + if creds then + if creds.installed then + creds = creds.installed + end + if creds.client_id and creds.client_secret then + id = creds.client_id + secret = creds.client_secret + break + end + end + vim.cmd.redraw() + vim.api.nvim_echo( + { { 'could not read client_id/client_secret from ' .. fpath, 'ErrorMsg' } }, + false, + {} + ) + end + else + return + end + + vim.schedule(function() + local path = vim.fn.stdpath('data') .. '/pending/google_credentials.json' + local ok = M.save_json_file(path, { client_id = id, client_secret = secret }) + if not ok then + log.error(self.name .. ': failed to save credentials') + return + end + log.info(self.name .. ': credentials saved, starting authorization...') + self:auth() + end) +end + ---@param on_complete? fun(): nil ---@return nil function OAuthClient:auth(on_complete)