diff --git a/lua/pending/init.lua b/lua/pending/init.lua index 446d375..36f5282 100644 --- a/lua/pending/init.lua +++ b/lua/pending/init.lua @@ -430,6 +430,48 @@ function M.toggle_complete() end end +---@param id_str string +---@return nil +function M.done(id_str) + local id = tonumber(id_str) + if not id then + log.error('Invalid task ID: ' .. tostring(id_str)) + return + end + local s = get_store() + s:load() + local task = s:get(id) + if not task then + log.error('No task with ID ' .. id .. '.') + return + end + local was_done = task.status == 'done' + if was_done then + s:update(id, { status = 'pending', ['end'] = vim.NIL }) + else + if task.recur and task.due then + local recur = require('pending.recur') + local mode = task.recur_mode or 'scheduled' + local next_date = recur.next_due(task.due, task.recur, mode) + s:add({ + description = task.description, + category = task.category, + priority = task.priority, + due = next_date, + recur = task.recur, + recur_mode = task.recur_mode, + }) + end + s:update(id, { status = 'done' }) + end + _save_and_notify() + local bufnr = buffer.bufnr() + if bufnr and vim.api.nvim_buf_is_valid(bufnr) then + buffer.render(bufnr) + end + log.info('Task #' .. id .. ' marked ' .. (was_done and 'pending' or 'done')) +end + ---@return nil function M.toggle_priority() local bufnr = buffer.bufnr() @@ -550,7 +592,7 @@ local function run_sync(backend_name, action) if not action or action == '' then local actions = {} for k, v in pairs(backend) do - if type(v) == 'function' and k:sub(1, 1) ~= '_' then + if type(v) == 'function' and k:sub(1, 1) ~= '_' and k ~= 'health' then table.insert(actions, k) end end @@ -558,7 +600,7 @@ local function run_sync(backend_name, action) log.info(backend_name .. ' actions: ' .. table.concat(actions, ', ')) return end - if type(backend[action]) ~= 'function' then + if action == 'health' or type(backend[action]) ~= 'function' then log.error(backend_name .. " backend has no '" .. action .. "' action") return end @@ -831,8 +873,8 @@ end ---@return nil function M.auth() local oauth = require('pending.sync.oauth') - vim.ui.select({ 'gtasks', 'gcal', 'both' }, { - prompt = 'Authenticate:', + vim.ui.select({ 'Google Tasks', 'Google Calendar', 'Google Tasks and Google Calendar' }, { + prompt = 'Authenticate with:', }, function(choice) if not choice then return @@ -856,6 +898,8 @@ function M.command(args) local cmd, rest = args:match('^(%S+)%s*(.*)') if cmd == 'add' then M.add(rest) + elseif cmd == 'done' then + M.done(rest:match('^(%S+)')) elseif cmd == 'edit' then local id_str, edit_rest = rest:match('^(%S+)%s*(.*)') M.edit(id_str, edit_rest) diff --git a/lua/pending/sync/gcal.lua b/lua/pending/sync/gcal.lua index f90d7c1..942fbec 100644 --- a/lua/pending/sync/gcal.lua +++ b/lua/pending/sync/gcal.lua @@ -234,6 +234,12 @@ end ---@return nil function M.health() oauth.health(M.name) + local tokens = oauth.google_client:load_tokens() + if tokens and tokens.refresh_token then + vim.health.ok('gcal tokens found') + else + vim.health.info('no gcal tokens — run :Pending auth') + end end return M diff --git a/lua/pending/sync/oauth.lua b/lua/pending/sync/oauth.lua index 887769c..224476b 100644 --- a/lua/pending/sync/oauth.lua +++ b/lua/pending/sync/oauth.lua @@ -350,7 +350,7 @@ end function OAuthClient:auth(on_complete) local creds = self:resolve_credentials() if creds.client_id == BUNDLED_CLIENT_ID then - log.error(self.name .. ': no credentials configured — run :Pending ' .. self.name .. ' setup') + log.error(self.name .. ': no credentials configured — run :Pending auth') return end local port = self.port @@ -470,12 +470,14 @@ function OAuthClient:_exchange_code(creds, code, code_verifier, port, on_complet }, { text = true }) if result.code ~= 0 then + self:_wipe() log.error('Token exchange failed.') return end local ok, decoded = pcall(vim.json.decode, result.stdout or '') if not ok or not decoded.access_token then + self:_wipe() log.error('Invalid token response.') return end @@ -488,6 +490,12 @@ function OAuthClient:_exchange_code(creds, code, code_verifier, port, on_complet end end +---@return nil +function OAuthClient:_wipe() + os.remove(self:token_path()) + os.remove(vim.fn.stdpath('data') .. '/pending/google_credentials.json') +end + ---@param opts { name: string, scope: string, port: integer, config_key: string } ---@return pending.OAuthClient function M.new(opts) diff --git a/plugin/pending.lua b/plugin/pending.lua index cba4916..d246fba 100644 --- a/plugin/pending.lua +++ b/plugin/pending.lua @@ -167,7 +167,7 @@ end, { nargs = '*', complete = function(arg_lead, cmd_line) local pending = require('pending') - local subcmds = { 'add', 'archive', 'auth', 'due', 'edit', 'filter', 'undo' } + local subcmds = { 'add', 'archive', 'auth', 'done', 'due', 'edit', 'filter', 'undo' } for _, b in ipairs(pending.sync_backends()) do table.insert(subcmds, b) end @@ -200,6 +200,16 @@ end, { end return filtered end + if cmd_line:match('^Pending%s+done%s') then + local store = require('pending.store') + local s = store.new(store.resolve_path()) + s:load() + local ids = {} + for _, task in ipairs(s:active_tasks()) do + table.insert(ids, tostring(task.id)) + end + return filter_candidates(arg_lead, ids) + end if cmd_line:match('^Pending%s+edit') then return complete_edit(arg_lead, cmd_line) end @@ -216,7 +226,7 @@ end, { end local actions = {} for k, v in pairs(mod) do - if type(v) == 'function' and k:sub(1, 1) ~= '_' then + if type(v) == 'function' and k:sub(1, 1) ~= '_' and k ~= 'health' then table.insert(actions, k) end end