refactor(gcal): per-category calendars, async push, error notifications

Problem: gcal used a single hardcoded calendar name, ran synchronously
blocking the editor, and silently dropped some API errors.

Solution: Fetch all calendars and map categories to calendars (creating
on demand), wrap push in `oauth.async()`, notify on individual API
failures, track `_gcal_calendar_id` in `_extra`, and remove the `$`
anchor from `next_day` pattern.
This commit is contained in:
Barrett Ruth 2026-03-05 11:24:29 -05:00
parent ca61db7127
commit 765d7fa0b5

View file

@ -15,13 +15,10 @@ local client = oauth.new({
config_key = 'gcal', config_key = 'gcal',
}) })
---@return string? calendar_id ---@param access_token string
---@return table<string, string>? name_to_id
---@return string? err ---@return string? err
local function find_or_create_calendar(access_token) local function get_all_calendars(access_token)
local cfg = config.get()
local gc = (cfg.sync and cfg.sync.gcal) or {}
local cal_name = gc.calendar or 'Pendings'
local data, err = oauth.curl_request( local data, err = oauth.curl_request(
'GET', 'GET',
BASE_URL .. '/users/me/calendarList', BASE_URL .. '/users/me/calendarList',
@ -30,27 +27,41 @@ local function find_or_create_calendar(access_token)
if err then if err then
return nil, err return nil, err
end end
local result = {}
for _, item in ipairs(data and data.items or {}) do for _, item in ipairs(data and data.items or {}) do
if item.summary == cal_name then if item.summary then
return item.id, nil result[item.summary] = item.id
end end
end end
return result, nil
end
local body = vim.json.encode({ summary = cal_name }) ---@param access_token string
local created, create_err = ---@param name string
oauth.curl_request('POST', BASE_URL .. '/calendars', oauth.auth_headers(access_token), body) ---@param existing table<string, string>
if create_err then ---@return string? calendar_id
return nil, create_err ---@return string? err
local function find_or_create_calendar(access_token, name, existing)
if existing[name] then
return existing[name], nil
end end
local body = vim.json.encode({ summary = name })
return created and created.id, nil local created, err =
oauth.curl_request('POST', BASE_URL .. '/calendars', oauth.auth_headers(access_token), body)
if err then
return nil, err
end
local id = created and created.id
if id then
existing[name] = id
end
return id, nil
end end
---@param date_str string ---@param date_str string
---@return string ---@return string
local function next_day(date_str) local function next_day(date_str)
local y, m, d = date_str:match('^(%d%d%d%d)-(%d%d)-(%d%d)$') local y, m, d = date_str:match('^(%d%d%d%d)-(%d%d)-(%d%d)')
local t = os.time({ year = tonumber(y) or 0, month = tonumber(m) or 0, day = tonumber(d) or 0 }) local t = os.time({ year = tonumber(y) or 0, month = tonumber(m) or 0, day = tonumber(d) or 0 })
+ 86400 + 86400
return os.date('%Y-%m-%d', t) --[[@as string]] return os.date('%Y-%m-%d', t) --[[@as string]]
@ -128,74 +139,99 @@ function M.auth()
client:auth() client:auth()
end end
function M.sync() function M.push()
local access_token = client:get_access_token() oauth.async(function()
if not access_token then local access_token = client:get_access_token()
return if not access_token then
end return
end
local calendar_id, err = find_or_create_calendar(access_token) local calendars, cal_err = get_all_calendars(access_token)
if err or not calendar_id then if cal_err or not calendars then
vim.notify('pending.nvim: ' .. (err or 'calendar not found'), vim.log.levels.ERROR) vim.notify('pending.nvim: ' .. (cal_err or 'failed to fetch calendars'), vim.log.levels.ERROR)
return return
end end
local tasks = require('pending').store():tasks() local s = require('pending').store()
local created, updated, deleted = 0, 0, 0 local created, updated, deleted = 0, 0, 0
for _, task in ipairs(tasks) do for _, task in ipairs(s:tasks()) do
local extra = task._extra or {} local extra = task._extra or {}
local event_id = extra['_gcal_event_id'] --[[@as string?]] local event_id = extra['_gcal_event_id'] --[[@as string?]]
local cal_id = extra['_gcal_calendar_id'] --[[@as string?]]
local should_delete = event_id ~= nil local should_delete = event_id ~= nil
and ( and cal_id ~= nil
task.status == 'done' and (
or task.status == 'deleted' task.status == 'done'
or (task.status == 'pending' and not task.due) or task.status == 'deleted'
) or (task.status == 'pending' and not task.due)
)
if should_delete and event_id then if should_delete then
local del_err = delete_event(access_token, calendar_id, event_id) --[[@as string]] local del_err = delete_event(access_token, cal_id, event_id)
if not del_err then if del_err then
extra['_gcal_event_id'] = nil vim.notify('pending.nvim: gcal delete failed: ' .. del_err, vim.log.levels.WARN)
if next(extra) == nil then
task._extra = nil
else else
task._extra = extra extra['_gcal_event_id'] = nil
end extra['_gcal_calendar_id'] = nil
task.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]] if next(extra) == nil then
deleted = deleted + 1 task._extra = nil
end else
elseif task.status == 'pending' and task.due then task._extra = extra
if event_id then
local upd_err = update_event(access_token, calendar_id, event_id, task)
if not upd_err then
updated = updated + 1
end
else
local new_id, create_err = create_event(access_token, calendar_id, task)
if not create_err and new_id then
if not task._extra then
task._extra = {}
end end
task._extra['_gcal_event_id'] = new_id
task.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]] task.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
created = created + 1 deleted = deleted + 1
end
elseif task.status == 'pending' and task.due then
local cat = task.category or config.get().default_category
if event_id and cal_id then
local upd_err = update_event(access_token, cal_id, event_id, task)
if upd_err then
vim.notify('pending.nvim: gcal update failed: ' .. upd_err, vim.log.levels.WARN)
else
updated = updated + 1
end
else
local lid, lid_err = find_or_create_calendar(access_token, cat, calendars)
if lid_err or not lid then
vim.notify(
'pending.nvim: gcal calendar failed: ' .. (lid_err or 'unknown'),
vim.log.levels.WARN
)
else
local new_id, create_err = create_event(access_token, lid, task)
if create_err then
vim.notify('pending.nvim: gcal create failed: ' .. create_err, vim.log.levels.WARN)
elseif new_id then
if not task._extra then
task._extra = {}
end
task._extra['_gcal_event_id'] = new_id
task._extra['_gcal_calendar_id'] = lid
task.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
created = created + 1
end
end
end end
end end
end end
end
require('pending').store():save() s:save()
require('pending')._recompute_counts() require('pending')._recompute_counts()
vim.notify( local buffer = require('pending.buffer')
string.format( if buffer.bufnr() and vim.api.nvim_buf_is_valid(buffer.bufnr()) then
'pending.nvim: Synced to Google Calendar (created: %d, updated: %d, deleted: %d)', buffer.render(buffer.bufnr())
created, end
updated, vim.notify(
deleted string.format(
'pending.nvim: Google Calendar pushed — +%d ~%d -%d',
created,
updated,
deleted
)
) )
) end)
end end
---@return nil ---@return nil