refactor(sync): extract backend interface, adapt gcal module
Problem: :Pending sync hardcodes Google Calendar — M.sync() does
pcall(require, 'pending.sync.gcal') and calls gcal.sync() directly.
The config has a flat gcal field. This prevents adding new sync backends
without modifying init.lua.
Solution: Define a backend interface contract (name, auth, sync, health
fields), refactor :Pending sync to dispatch via require('pending.sync.'
.. backend_name), add sync table to config with legacy gcal migration,
rename gcal.authorize to gcal.auth, add gcal.health for checkhealth,
and add tab completion for backend names and actions.
This commit is contained in:
parent
8d3d21b330
commit
7de9c23ec1
5 changed files with 85 additions and 18 deletions
|
|
@ -2,6 +2,9 @@
|
||||||
---@field calendar? string
|
---@field calendar? string
|
||||||
---@field credentials_path? string
|
---@field credentials_path? string
|
||||||
|
|
||||||
|
---@class pending.SyncConfig
|
||||||
|
---@field gcal? pending.GcalConfig
|
||||||
|
|
||||||
---@class pending.Keymaps
|
---@class pending.Keymaps
|
||||||
---@field close? string|false
|
---@field close? string|false
|
||||||
---@field toggle? string|false
|
---@field toggle? string|false
|
||||||
|
|
@ -32,6 +35,7 @@
|
||||||
---@field drawer_height? integer
|
---@field drawer_height? integer
|
||||||
---@field debug? boolean
|
---@field debug? boolean
|
||||||
---@field keymaps pending.Keymaps
|
---@field keymaps pending.Keymaps
|
||||||
|
---@field sync? pending.SyncConfig
|
||||||
---@field gcal? pending.GcalConfig
|
---@field gcal? pending.GcalConfig
|
||||||
|
|
||||||
---@class pending.config
|
---@class pending.config
|
||||||
|
|
@ -65,6 +69,7 @@ local defaults = {
|
||||||
next_task = ']t',
|
next_task = ']t',
|
||||||
prev_task = '[t',
|
prev_task = '[t',
|
||||||
},
|
},
|
||||||
|
sync = {},
|
||||||
}
|
}
|
||||||
|
|
||||||
---@type pending.Config?
|
---@type pending.Config?
|
||||||
|
|
@ -77,6 +82,10 @@ function M.get()
|
||||||
end
|
end
|
||||||
local user = vim.g.pending or {}
|
local user = vim.g.pending or {}
|
||||||
_resolved = vim.tbl_deep_extend('force', defaults, user)
|
_resolved = vim.tbl_deep_extend('force', defaults, user)
|
||||||
|
if _resolved.gcal and not (_resolved.sync and _resolved.sync.gcal) then
|
||||||
|
_resolved.sync = _resolved.sync or {}
|
||||||
|
_resolved.sync.gcal = _resolved.gcal
|
||||||
|
end
|
||||||
return _resolved
|
return _resolved
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,16 +47,18 @@ function M.check()
|
||||||
vim.health.info('No data file yet (will be created on first save)')
|
vim.health.info('No data file yet (will be created on first save)')
|
||||||
end
|
end
|
||||||
|
|
||||||
if vim.fn.executable('curl') == 1 then
|
local sync_paths = vim.fn.globpath(vim.o.runtimepath, 'lua/pending/sync/*.lua', false, true)
|
||||||
vim.health.ok('curl found (required for Google Calendar sync)')
|
if #sync_paths == 0 then
|
||||||
|
vim.health.info('No sync backends found')
|
||||||
else
|
else
|
||||||
vim.health.warn('curl not found (needed for Google Calendar sync)')
|
for _, path in ipairs(sync_paths) do
|
||||||
|
local name = vim.fn.fnamemodify(path, ':t:r')
|
||||||
|
local bok, backend = pcall(require, 'pending.sync.' .. name)
|
||||||
|
if bok and type(backend.health) == 'function' then
|
||||||
|
vim.health.start('pending.nvim: sync/' .. name)
|
||||||
|
backend.health()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if vim.fn.executable('openssl') == 1 then
|
|
||||||
vim.health.ok('openssl found (required for OAuth PKCE)')
|
|
||||||
else
|
|
||||||
vim.health.warn('openssl not found (needed for Google Calendar OAuth)')
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -414,14 +414,25 @@ function M.add(text)
|
||||||
vim.notify('Pending added: ' .. description)
|
vim.notify('Pending added: ' .. description)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param backend_name string
|
||||||
|
---@param action? string
|
||||||
---@return nil
|
---@return nil
|
||||||
function M.sync()
|
function M.sync(backend_name, action)
|
||||||
local ok, gcal = pcall(require, 'pending.sync.gcal')
|
if not backend_name or backend_name == '' then
|
||||||
if not ok then
|
vim.notify('Usage: :Pending sync <backend> [action]', vim.log.levels.ERROR)
|
||||||
vim.notify('Google Calendar sync module not available.', vim.log.levels.ERROR)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
gcal.sync()
|
action = (action and action ~= '') and action or 'sync'
|
||||||
|
local ok, backend = pcall(require, 'pending.sync.' .. backend_name)
|
||||||
|
if not ok then
|
||||||
|
vim.notify('Unknown sync backend: ' .. backend_name, vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if type(backend[action]) ~= 'function' then
|
||||||
|
vim.notify(backend_name .. " backend has no '" .. action .. "' action", vim.log.levels.ERROR)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
backend[action]()
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param days? integer
|
---@param days? integer
|
||||||
|
|
@ -700,7 +711,8 @@ function M.command(args)
|
||||||
local id_str, edit_rest = rest:match('^(%S+)%s*(.*)')
|
local id_str, edit_rest = rest:match('^(%S+)%s*(.*)')
|
||||||
M.edit(id_str, edit_rest)
|
M.edit(id_str, edit_rest)
|
||||||
elseif cmd == 'sync' then
|
elseif cmd == 'sync' then
|
||||||
M.sync()
|
local backend, action = rest:match('^(%S+)%s*(.*)')
|
||||||
|
M.sync(backend, action)
|
||||||
elseif cmd == 'archive' then
|
elseif cmd == 'archive' then
|
||||||
local d = rest ~= '' and tonumber(rest) or nil
|
local d = rest ~= '' and tonumber(rest) or nil
|
||||||
M.archive(d)
|
M.archive(d)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ local store = require('pending.store')
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
M.name = 'gcal'
|
||||||
|
|
||||||
local BASE_URL = 'https://www.googleapis.com/calendar/v3'
|
local BASE_URL = 'https://www.googleapis.com/calendar/v3'
|
||||||
local TOKEN_URL = 'https://oauth2.googleapis.com/token'
|
local TOKEN_URL = 'https://oauth2.googleapis.com/token'
|
||||||
local AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
|
local AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
|
||||||
|
|
@ -22,7 +24,7 @@ local SCOPE = 'https://www.googleapis.com/auth/calendar'
|
||||||
---@return table<string, any>
|
---@return table<string, any>
|
||||||
local function gcal_config()
|
local function gcal_config()
|
||||||
local cfg = config.get()
|
local cfg = config.get()
|
||||||
return cfg.gcal or {}
|
return (cfg.sync and cfg.sync.gcal) or cfg.gcal or {}
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return string
|
---@return string
|
||||||
|
|
@ -199,7 +201,7 @@ local function get_access_token()
|
||||||
end
|
end
|
||||||
local tokens = load_tokens()
|
local tokens = load_tokens()
|
||||||
if not tokens or not tokens.refresh_token then
|
if not tokens or not tokens.refresh_token then
|
||||||
M.authorize()
|
M.auth()
|
||||||
tokens = load_tokens()
|
tokens = load_tokens()
|
||||||
if not tokens then
|
if not tokens then
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -218,7 +220,7 @@ local function get_access_token()
|
||||||
return tokens.access_token
|
return tokens.access_token
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.authorize()
|
function M.auth()
|
||||||
local creds = load_credentials()
|
local creds = load_credentials()
|
||||||
if not creds then
|
if not creds then
|
||||||
vim.notify(
|
vim.notify(
|
||||||
|
|
@ -514,4 +516,18 @@ function M.sync()
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return nil
|
||||||
|
function M.health()
|
||||||
|
if vim.fn.executable('curl') == 1 then
|
||||||
|
vim.health.ok('curl found (required for gcal sync)')
|
||||||
|
else
|
||||||
|
vim.health.warn('curl not found (needed for gcal sync)')
|
||||||
|
end
|
||||||
|
if vim.fn.executable('openssl') == 1 then
|
||||||
|
vim.health.ok('openssl found (required for gcal OAuth PKCE)')
|
||||||
|
else
|
||||||
|
vim.health.warn('openssl not found (needed for gcal OAuth)')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,34 @@ end, {
|
||||||
if cmd_line:match('^Pending%s+edit') then
|
if cmd_line:match('^Pending%s+edit') then
|
||||||
return complete_edit(arg_lead, cmd_line)
|
return complete_edit(arg_lead, cmd_line)
|
||||||
end
|
end
|
||||||
|
if cmd_line:match('^Pending%s+sync') then
|
||||||
|
local after_sync = cmd_line:match('^Pending%s+sync%s+(.*)')
|
||||||
|
if not after_sync then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
local parts = {}
|
||||||
|
for part in after_sync:gmatch('%S+') do
|
||||||
|
table.insert(parts, part)
|
||||||
|
end
|
||||||
|
local trailing_space = after_sync:match('%s$')
|
||||||
|
if #parts == 0 or (#parts == 1 and not trailing_space) then
|
||||||
|
local backends = {}
|
||||||
|
local pattern = vim.fn.globpath(vim.o.runtimepath, 'lua/pending/sync/*.lua', false, true)
|
||||||
|
for _, path in ipairs(pattern) do
|
||||||
|
local name = vim.fn.fnamemodify(path, ':t:r')
|
||||||
|
table.insert(backends, name)
|
||||||
|
end
|
||||||
|
table.sort(backends)
|
||||||
|
return filter_candidates(arg_lead, backends)
|
||||||
|
end
|
||||||
|
if #parts == 1 and trailing_space then
|
||||||
|
return filter_candidates(arg_lead, { 'auth', 'sync' })
|
||||||
|
end
|
||||||
|
if #parts >= 2 and not trailing_space then
|
||||||
|
return filter_candidates(arg_lead, { 'auth', 'sync' })
|
||||||
|
end
|
||||||
|
return {}
|
||||||
|
end
|
||||||
return {}
|
return {}
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue