Compare commits
No commits in common. "87679e98571a8b125d7108201d436bcf7e97fb0a" and "e0e3af6787c948d879ed069c7e7e582bd72e591c" have entirely different histories.
87679e9857
...
e0e3af6787
15 changed files with 306 additions and 349 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,6 +1,5 @@
|
||||||
doc/tags
|
doc/tags
|
||||||
*.log
|
*.log
|
||||||
minimal_init.lua
|
|
||||||
|
|
||||||
.*cache*
|
.*cache*
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ Edit tasks like text. `:w` saves them.
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Neovim 0.10+
|
- Neovim 0.10+
|
||||||
- (Optionally) `curl` for Google Calendar and Google Task sync
|
- (Optionally) `curl` and `openssl` for Google Calendar and Google Task sync
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ REQUIREMENTS *pending-requirements*
|
||||||
|
|
||||||
- Neovim 0.10+
|
- Neovim 0.10+
|
||||||
- No external dependencies for local use
|
- No external dependencies for local use
|
||||||
- `curl` is required for the `gcal` and `gtasks` sync backends
|
- `curl` and `openssl` are required for the `gcal` and `gtasks` sync backends
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
INSTALL *pending-install*
|
INSTALL *pending-install*
|
||||||
|
|
@ -149,17 +149,17 @@ COMMANDS *pending-commands*
|
||||||
Open the list with |:copen| to navigate to each task's category.
|
Open the list with |:copen| to navigate to each task's category.
|
||||||
|
|
||||||
*:Pending-gtasks*
|
*:Pending-gtasks*
|
||||||
:Pending gtasks {action}
|
:Pending gtasks [{action}]
|
||||||
Run a Google Tasks action. An explicit action is required.
|
Run a Google Tasks sync action. {action} defaults to `sync` when omitted.
|
||||||
|
|
||||||
Actions: ~
|
Actions: ~
|
||||||
`sync` Push local changes then pull remote changes.
|
`sync` Push local changes then pull remote changes (default).
|
||||||
`push` Push local changes to Google Tasks only.
|
`push` Push local changes to Google Tasks only.
|
||||||
`pull` Pull remote changes from Google Tasks only.
|
`pull` Pull remote changes from Google Tasks only.
|
||||||
`auth` Run the OAuth authorization flow.
|
`auth` Run the OAuth authorization flow.
|
||||||
|
|
||||||
Examples: >vim
|
Examples: >vim
|
||||||
:Pending gtasks sync " push then pull
|
:Pending gtasks " push then pull (default)
|
||||||
:Pending gtasks push " push local → Google Tasks
|
:Pending gtasks push " push local → Google Tasks
|
||||||
:Pending gtasks pull " pull Google Tasks → local
|
:Pending gtasks pull " pull Google Tasks → local
|
||||||
:Pending gtasks auth " authorize
|
:Pending gtasks auth " authorize
|
||||||
|
|
@ -169,15 +169,16 @@ COMMANDS *pending-commands*
|
||||||
See |pending-gtasks| for full details.
|
See |pending-gtasks| for full details.
|
||||||
|
|
||||||
*:Pending-gcal*
|
*:Pending-gcal*
|
||||||
:Pending gcal {action}
|
:Pending gcal [{action}]
|
||||||
Run a Google Calendar action. An explicit action is required.
|
Run a Google Calendar sync action. {action} defaults to `sync` when
|
||||||
|
omitted.
|
||||||
|
|
||||||
Actions: ~
|
Actions: ~
|
||||||
`push` Push tasks with due dates to Google Calendar.
|
`sync` Push tasks with due dates to Google Calendar (default).
|
||||||
`auth` Run the OAuth authorization flow.
|
`auth` Run the OAuth authorization flow.
|
||||||
|
|
||||||
Examples: >vim
|
Examples: >vim
|
||||||
:Pending gcal push " push to Google Calendar
|
:Pending gcal " push to Google Calendar (default)
|
||||||
:Pending gcal auth " authorize
|
:Pending gcal auth " authorize
|
||||||
<
|
<
|
||||||
|
|
||||||
|
|
@ -605,7 +606,9 @@ loads: >lua
|
||||||
prev_task = '[t',
|
prev_task = '[t',
|
||||||
},
|
},
|
||||||
sync = {
|
sync = {
|
||||||
gcal = {},
|
gcal = {
|
||||||
|
calendar = 'Pendings',
|
||||||
|
},
|
||||||
gtasks = {},
|
gtasks = {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -890,8 +893,8 @@ SYNC BACKENDS *pending-sync-backend*
|
||||||
|
|
||||||
Sync backends are Lua modules under `lua/pending/sync/<name>.lua`. Each
|
Sync backends are Lua modules under `lua/pending/sync/<name>.lua`. Each
|
||||||
backend is exposed as a top-level `:Pending` subcommand: >vim
|
backend is exposed as a top-level `:Pending` subcommand: >vim
|
||||||
:Pending gtasks {action}
|
:Pending gtasks [action]
|
||||||
:Pending gcal {action}
|
:Pending gcal [action]
|
||||||
<
|
<
|
||||||
|
|
||||||
Each module returns a table conforming to the backend interface: >lua
|
Each module returns a table conforming to the backend interface: >lua
|
||||||
|
|
@ -899,9 +902,9 @@ Each module returns a table conforming to the backend interface: >lua
|
||||||
---@class pending.SyncBackend
|
---@class pending.SyncBackend
|
||||||
---@field name string
|
---@field name string
|
||||||
---@field auth fun(): nil
|
---@field auth fun(): nil
|
||||||
|
---@field sync fun(): nil
|
||||||
---@field push? fun(): nil
|
---@field push? fun(): nil
|
||||||
---@field pull? fun(): nil
|
---@field pull? fun(): nil
|
||||||
---@field sync? fun(): nil
|
|
||||||
---@field health? fun(): nil
|
---@field health? fun(): nil
|
||||||
<
|
<
|
||||||
|
|
||||||
|
|
@ -921,15 +924,16 @@ Backend-specific configuration goes under `sync.<name>` in |pending-config|.
|
||||||
==============================================================================
|
==============================================================================
|
||||||
GOOGLE CALENDAR *pending-gcal*
|
GOOGLE CALENDAR *pending-gcal*
|
||||||
|
|
||||||
pending.nvim can push tasks with due dates to Google Calendar as all-day
|
pending.nvim can push tasks with due dates to a dedicated Google Calendar as
|
||||||
events. Each pending.nvim category maps to a Google Calendar of the same
|
all-day events. This is a one-way push; changes made in Google Calendar are
|
||||||
name. Calendars are created automatically on first push. This is a one-way
|
not pulled back into pending.nvim.
|
||||||
push; changes made in Google Calendar are not pulled back.
|
|
||||||
|
|
||||||
Configuration: >lua
|
Configuration: >lua
|
||||||
vim.g.pending = {
|
vim.g.pending = {
|
||||||
sync = {
|
sync = {
|
||||||
gcal = {},
|
gcal = {
|
||||||
|
calendar = 'Pendings',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
<
|
<
|
||||||
|
|
@ -939,6 +943,11 @@ used by default. Run `:Pending gcal auth` and the browser opens immediately.
|
||||||
|
|
||||||
*pending.GcalConfig*
|
*pending.GcalConfig*
|
||||||
Fields: ~
|
Fields: ~
|
||||||
|
{calendar} (string, default: 'Pendings')
|
||||||
|
Name of the Google Calendar to sync to. If a calendar
|
||||||
|
with this name does not exist it is created
|
||||||
|
automatically on the first sync.
|
||||||
|
|
||||||
{client_id} (string, optional)
|
{client_id} (string, optional)
|
||||||
OAuth client ID. When set together with
|
OAuth client ID. When set together with
|
||||||
{client_secret}, these take priority over the
|
{client_secret}, these take priority over the
|
||||||
|
|
@ -964,24 +973,26 @@ OAuth flow: ~
|
||||||
On the first `:Pending 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
|
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
|
|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. After
|
OAuth redirect. The PKCE (Proof Key for Code Exchange) flow is used —
|
||||||
the user grants consent, the
|
`openssl` generates the code challenge. After the user grants consent, the
|
||||||
authorization code is exchanged for tokens and the refresh token is stored at
|
authorization code is exchanged for tokens and the refresh token is stored at
|
||||||
`stdpath('data')/pending/gcal_tokens.json` with mode `600`. Subsequent syncs
|
`stdpath('data')/pending/gcal_tokens.json` with mode `600`. Subsequent syncs
|
||||||
use the stored refresh token and refresh the access token automatically when
|
use the stored refresh token and refresh the access token automatically when
|
||||||
it is about to expire.
|
it is about to expire.
|
||||||
|
|
||||||
`:Pending gcal push` behavior: ~
|
`:Pending gcal` behavior: ~
|
||||||
For each task in the store:
|
For each task in the store:
|
||||||
- A pending task with a due date and no existing event: a new all-day event is
|
- A pending task with a due date and no existing event: a new all-day event is
|
||||||
created in the calendar matching the task's category. The event ID and
|
created and the event ID is stored in the task's `_extra` table.
|
||||||
calendar ID are stored in the task's `_extra` table.
|
|
||||||
- A pending task with a due date and an existing event: the event summary and
|
- A pending task with a due date and an existing event: the event summary and
|
||||||
date are updated in place.
|
date are updated in place.
|
||||||
- A done or deleted task with an existing event: the event is deleted.
|
- A done or deleted task with an existing event: the event is deleted.
|
||||||
- A pending task with no due date that had an existing event: the event is
|
- A pending task with no due date that had an existing event: the event is
|
||||||
deleted.
|
deleted.
|
||||||
|
|
||||||
|
A summary notification is shown after sync: `created: N, updated: N,
|
||||||
|
deleted: N`.
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
GOOGLE TASKS *pending-gtasks*
|
GOOGLE TASKS *pending-gtasks*
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
---@field category string
|
---@field category string
|
||||||
|
|
||||||
---@class pending.GcalConfig
|
---@class pending.GcalConfig
|
||||||
|
---@field calendar? string
|
||||||
---@field credentials_path? string
|
---@field credentials_path? string
|
||||||
---@field client_id? string
|
---@field client_id? string
|
||||||
---@field client_secret? string
|
---@field client_secret? string
|
||||||
|
|
|
||||||
|
|
@ -121,19 +121,17 @@ function M.apply(lines, s, hidden_ids)
|
||||||
task.priority = entry.priority
|
task.priority = entry.priority
|
||||||
changed = true
|
changed = true
|
||||||
end
|
end
|
||||||
if entry.due ~= nil and task.due ~= entry.due then
|
if task.due ~= entry.due then
|
||||||
task.due = entry.due
|
task.due = entry.due
|
||||||
changed = true
|
changed = true
|
||||||
end
|
end
|
||||||
if entry.rec ~= nil then
|
if task.recur ~= entry.rec then
|
||||||
if task.recur ~= entry.rec then
|
task.recur = entry.rec
|
||||||
task.recur = entry.rec
|
changed = true
|
||||||
changed = true
|
end
|
||||||
end
|
if task.recur_mode ~= entry.rec_mode then
|
||||||
if task.recur_mode ~= entry.rec_mode then
|
task.recur_mode = entry.rec_mode
|
||||||
task.recur_mode = entry.rec_mode
|
changed = true
|
||||||
changed = true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
if entry.status and task.status ~= entry.status then
|
if entry.status and task.status ~= entry.status then
|
||||||
task.status = entry.status
|
task.status = entry.status
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,13 @@ function M.check()
|
||||||
vim.health.info('(project-local store; global path: ' .. cfg.data_path .. ')')
|
vim.health.info('(project-local store; global path: ' .. cfg.data_path .. ')')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local data_dir = vim.fn.fnamemodify(resolved_path, ':h')
|
||||||
|
if vim.fn.isdirectory(data_dir) == 1 then
|
||||||
|
vim.health.ok('Data directory exists: ' .. data_dir)
|
||||||
|
else
|
||||||
|
vim.health.warn('Data directory does not exist yet: ' .. data_dir)
|
||||||
|
end
|
||||||
|
|
||||||
if vim.fn.filereadable(resolved_path) == 1 then
|
if vim.fn.filereadable(resolved_path) == 1 then
|
||||||
local s = store.new(resolved_path)
|
local s = store.new(resolved_path)
|
||||||
local load_ok, err = pcall(function()
|
local load_ok, err = pcall(function()
|
||||||
|
|
@ -47,6 +54,8 @@ function M.check()
|
||||||
else
|
else
|
||||||
vim.health.error('Failed to load data file: ' .. tostring(err))
|
vim.health.error('Failed to load data file: ' .. tostring(err))
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
vim.health.info('No data file yet (will be created on first save)')
|
||||||
end
|
end
|
||||||
|
|
||||||
local sync_paths = vim.fn.globpath(vim.o.runtimepath, 'lua/pending/sync/*.lua', false, true)
|
local sync_paths = vim.fn.globpath(vim.o.runtimepath, 'lua/pending/sync/*.lua', false, true)
|
||||||
|
|
|
||||||
|
|
@ -142,16 +142,6 @@ local function compute_hidden_ids(tasks, predicates)
|
||||||
visible = false
|
visible = false
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
elseif pred == 'done' then
|
|
||||||
if task.status ~= 'done' then
|
|
||||||
visible = false
|
|
||||||
break
|
|
||||||
end
|
|
||||||
elseif pred == 'pending' then
|
|
||||||
if task.status ~= 'pending' then
|
|
||||||
visible = false
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if not visible then
|
if not visible then
|
||||||
|
|
@ -546,22 +536,12 @@ end
|
||||||
---@param action? string
|
---@param action? string
|
||||||
---@return nil
|
---@return nil
|
||||||
local function run_sync(backend_name, action)
|
local function run_sync(backend_name, action)
|
||||||
|
action = (action and action ~= '') and action or 'sync'
|
||||||
local ok, backend = pcall(require, 'pending.sync.' .. backend_name)
|
local ok, backend = pcall(require, 'pending.sync.' .. backend_name)
|
||||||
if not ok then
|
if not ok then
|
||||||
vim.notify('Unknown sync backend: ' .. backend_name, vim.log.levels.ERROR)
|
vim.notify('Unknown sync backend: ' .. backend_name, vim.log.levels.ERROR)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
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
|
|
||||||
table.insert(actions, k)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.sort(actions)
|
|
||||||
vim.notify(backend_name .. ' actions: ' .. table.concat(actions, ', '), vim.log.levels.INFO)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if type(backend[action]) ~= 'function' then
|
if type(backend[action]) ~= 'function' then
|
||||||
vim.notify(backend_name .. " backend has no '" .. action .. "' action", vim.log.levels.ERROR)
|
vim.notify(backend_name .. " backend has no '" .. action .. "' action", vim.log.levels.ERROR)
|
||||||
return
|
return
|
||||||
|
|
@ -824,7 +804,7 @@ function M.edit(id_str, rest)
|
||||||
|
|
||||||
s:update(id, updates)
|
s:update(id, updates)
|
||||||
|
|
||||||
_save_and_notify()
|
s:save()
|
||||||
|
|
||||||
local bufnr = buffer.bufnr()
|
local bufnr = buffer.bufnr()
|
||||||
if bufnr and vim.api.nvim_buf_is_valid(bufnr) then
|
if bufnr and vim.api.nvim_buf_is_valid(bufnr) then
|
||||||
|
|
@ -861,7 +841,7 @@ 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 SYNC_BACKEND_SET[cmd] then
|
elseif SYNC_BACKEND_SET[cmd] then
|
||||||
local action = rest:match('^(%S+)')
|
local action = rest:match('^(%S+)') or 'sync'
|
||||||
run_sync(cmd, action)
|
run_sync(cmd, 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
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,13 @@ local client = oauth.new({
|
||||||
config_key = 'gcal',
|
config_key = 'gcal',
|
||||||
})
|
})
|
||||||
|
|
||||||
---@param access_token string
|
---@return string? calendar_id
|
||||||
---@return table<string, string>? name_to_id
|
|
||||||
---@return string? err
|
---@return string? err
|
||||||
local function get_all_calendars(access_token)
|
local function find_or_create_calendar(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',
|
||||||
|
|
@ -27,41 +30,27 @@ local function get_all_calendars(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 then
|
if item.summary == cal_name then
|
||||||
result[item.summary] = item.id
|
return item.id, nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return result, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param access_token string
|
local body = vim.json.encode({ summary = cal_name })
|
||||||
---@param name string
|
local created, create_err =
|
||||||
---@param existing table<string, string>
|
|
||||||
---@return string? calendar_id
|
|
||||||
---@return string? err
|
|
||||||
local function find_or_create_calendar(access_token, name, existing)
|
|
||||||
if existing[name] then
|
|
||||||
return existing[name], nil
|
|
||||||
end
|
|
||||||
local body = vim.json.encode({ summary = name })
|
|
||||||
local created, err =
|
|
||||||
oauth.curl_request('POST', BASE_URL .. '/calendars', oauth.auth_headers(access_token), body)
|
oauth.curl_request('POST', BASE_URL .. '/calendars', oauth.auth_headers(access_token), body)
|
||||||
if err then
|
if create_err then
|
||||||
return nil, err
|
return nil, create_err
|
||||||
end
|
end
|
||||||
local id = created and created.id
|
|
||||||
if id then
|
return created and created.id, nil
|
||||||
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]]
|
||||||
|
|
@ -139,100 +128,74 @@ function M.auth()
|
||||||
client:auth()
|
client:auth()
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.push()
|
function M.sync()
|
||||||
oauth.async(function()
|
local access_token = client:get_access_token()
|
||||||
local access_token = client:get_access_token()
|
if not access_token then
|
||||||
if not access_token then
|
return
|
||||||
return
|
end
|
||||||
end
|
|
||||||
|
|
||||||
local calendars, cal_err = get_all_calendars(access_token)
|
local calendar_id, err = find_or_create_calendar(access_token)
|
||||||
if cal_err or not calendars then
|
if err or not calendar_id then
|
||||||
vim.notify('pending.nvim: ' .. (cal_err or 'failed to fetch calendars'), vim.log.levels.ERROR)
|
vim.notify('pending.nvim: ' .. (err or 'calendar not found'), vim.log.levels.ERROR)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local s = require('pending').store()
|
local tasks = require('pending').store():tasks()
|
||||||
local created, updated, deleted = 0, 0, 0
|
local created, updated, deleted = 0, 0, 0
|
||||||
|
|
||||||
for _, task in ipairs(s:tasks()) do
|
for _, task in ipairs(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 cal_id ~= nil
|
and (
|
||||||
and (
|
task.status == 'done'
|
||||||
task.status == 'done'
|
or task.status == 'deleted'
|
||||||
or task.status == 'deleted'
|
or (task.status == 'pending' and not task.due)
|
||||||
or (task.status == 'pending' and not task.due)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if should_delete then
|
if should_delete and event_id then
|
||||||
local del_err =
|
local del_err = delete_event(access_token, calendar_id, event_id) --[[@as string]]
|
||||||
delete_event(access_token, cal_id --[[@as string]], event_id --[[@as string]])
|
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
|
||||||
extra['_gcal_event_id'] = nil
|
task._extra = extra
|
||||||
extra['_gcal_calendar_id'] = nil
|
|
||||||
if next(extra) == nil then
|
|
||||||
task._extra = nil
|
|
||||||
else
|
|
||||||
task._extra = extra
|
|
||||||
end
|
|
||||||
task.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
|
|
||||||
deleted = deleted + 1
|
|
||||||
end
|
end
|
||||||
elseif task.status == 'pending' and task.due then
|
task.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
|
||||||
local cat = task.category or config.get().default_category
|
deleted = deleted + 1
|
||||||
if event_id and cal_id then
|
end
|
||||||
local upd_err = update_event(access_token, cal_id, event_id, task)
|
elseif task.status == 'pending' and task.due then
|
||||||
if upd_err then
|
if event_id then
|
||||||
vim.notify('pending.nvim: gcal update failed: ' .. upd_err, vim.log.levels.WARN)
|
local upd_err = update_event(access_token, calendar_id, event_id, task)
|
||||||
else
|
if not upd_err then
|
||||||
updated = updated + 1
|
updated = updated + 1
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
local lid, lid_err = find_or_create_calendar(access_token, cat, calendars)
|
local new_id, create_err = create_event(access_token, calendar_id, task)
|
||||||
if lid_err or not lid then
|
if not create_err and new_id then
|
||||||
vim.notify(
|
if not task._extra then
|
||||||
'pending.nvim: gcal calendar failed: ' .. (lid_err or 'unknown'),
|
task._extra = {}
|
||||||
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
|
||||||
|
task._extra['_gcal_event_id'] = new_id
|
||||||
|
task.modified = os.date('!%Y-%m-%dT%H:%M:%SZ') --[[@as string]]
|
||||||
|
created = created + 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
s:save()
|
require('pending').store():save()
|
||||||
require('pending')._recompute_counts()
|
require('pending')._recompute_counts()
|
||||||
local buffer = require('pending.buffer')
|
vim.notify(
|
||||||
if buffer.bufnr() and vim.api.nvim_buf_is_valid(buffer.bufnr()) then
|
string.format(
|
||||||
buffer.render(buffer.bufnr())
|
'pending.nvim: Synced to Google Calendar (created: %d, updated: %d, deleted: %d)',
|
||||||
end
|
created,
|
||||||
vim.notify(
|
updated,
|
||||||
string.format(
|
deleted
|
||||||
'pending.nvim: Google Calendar pushed — +%d ~%d -%d',
|
|
||||||
created,
|
|
||||||
updated,
|
|
||||||
deleted
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
end)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return nil
|
---@return nil
|
||||||
|
|
|
||||||
|
|
@ -247,9 +247,7 @@ local function push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
|
||||||
|
|
||||||
if task.status == 'deleted' and gtid and list_id then
|
if task.status == 'deleted' and gtid and list_id then
|
||||||
local err = delete_gtask(access_token, list_id, gtid)
|
local err = delete_gtask(access_token, list_id, gtid)
|
||||||
if err then
|
if not err then
|
||||||
vim.notify('pending.nvim: gtasks delete failed: ' .. err, vim.log.levels.WARN)
|
|
||||||
else
|
|
||||||
if not task._extra then
|
if not task._extra then
|
||||||
task._extra = {}
|
task._extra = {}
|
||||||
end
|
end
|
||||||
|
|
@ -264,9 +262,7 @@ local function push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
|
||||||
elseif task.status ~= 'deleted' then
|
elseif task.status ~= 'deleted' then
|
||||||
if gtid and list_id then
|
if gtid and list_id then
|
||||||
local err = update_gtask(access_token, list_id, gtid, task_to_gtask(task))
|
local err = update_gtask(access_token, list_id, gtid, task_to_gtask(task))
|
||||||
if err then
|
if not err then
|
||||||
vim.notify('pending.nvim: gtasks update failed: ' .. err, vim.log.levels.WARN)
|
|
||||||
else
|
|
||||||
updated = updated + 1
|
updated = updated + 1
|
||||||
end
|
end
|
||||||
elseif task.status == 'pending' then
|
elseif task.status == 'pending' then
|
||||||
|
|
@ -274,9 +270,7 @@ local function push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
|
||||||
local lid, err = find_or_create_tasklist(access_token, cat, tasklists)
|
local lid, err = find_or_create_tasklist(access_token, cat, tasklists)
|
||||||
if not err and lid then
|
if not err and lid then
|
||||||
local new_id, create_err = create_gtask(access_token, lid, task_to_gtask(task))
|
local new_id, create_err = create_gtask(access_token, lid, task_to_gtask(task))
|
||||||
if create_err then
|
if not create_err and new_id then
|
||||||
vim.notify('pending.nvim: gtasks create failed: ' .. create_err, vim.log.levels.WARN)
|
|
||||||
elseif new_id then
|
|
||||||
if not task._extra then
|
if not task._extra then
|
||||||
task._extra = {}
|
task._extra = {}
|
||||||
end
|
end
|
||||||
|
|
@ -363,79 +357,61 @@ function M.auth()
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.push()
|
function M.push()
|
||||||
oauth.async(function()
|
local access_token, tasklists, s, now_ts = sync_setup()
|
||||||
local access_token, tasklists, s, now_ts = sync_setup()
|
if not access_token then
|
||||||
if not access_token then
|
return
|
||||||
return
|
end
|
||||||
end
|
---@cast tasklists table<string, string>
|
||||||
---@cast tasklists table<string, string>
|
---@cast s pending.Store
|
||||||
---@cast s pending.Store
|
---@cast now_ts string
|
||||||
---@cast now_ts string
|
local by_gtasks_id = build_id_index(s)
|
||||||
local by_gtasks_id = build_id_index(s)
|
local created, updated, deleted = push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
|
||||||
local created, updated, deleted = push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
|
s:save()
|
||||||
s:save()
|
require('pending')._recompute_counts()
|
||||||
require('pending')._recompute_counts()
|
vim.notify(
|
||||||
local buffer = require('pending.buffer')
|
string.format('pending.nvim: Google Tasks pushed — +%d ~%d -%d', created, updated, deleted)
|
||||||
if buffer.bufnr() and vim.api.nvim_buf_is_valid(buffer.bufnr()) then
|
)
|
||||||
buffer.render(buffer.bufnr())
|
|
||||||
end
|
|
||||||
vim.notify(
|
|
||||||
string.format('pending.nvim: Google Tasks pushed — +%d ~%d -%d', created, updated, deleted)
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.pull()
|
function M.pull()
|
||||||
oauth.async(function()
|
local access_token, tasklists, s, now_ts = sync_setup()
|
||||||
local access_token, tasklists, s, now_ts = sync_setup()
|
if not access_token then
|
||||||
if not access_token then
|
return
|
||||||
return
|
end
|
||||||
end
|
---@cast tasklists table<string, string>
|
||||||
---@cast tasklists table<string, string>
|
---@cast s pending.Store
|
||||||
---@cast s pending.Store
|
---@cast now_ts string
|
||||||
---@cast now_ts string
|
local by_gtasks_id = build_id_index(s)
|
||||||
local by_gtasks_id = build_id_index(s)
|
local created, updated = pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
|
||||||
local created, updated = pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
|
s:save()
|
||||||
s:save()
|
require('pending')._recompute_counts()
|
||||||
require('pending')._recompute_counts()
|
vim.notify(string.format('pending.nvim: Google Tasks pulled — +%d ~%d', created, updated))
|
||||||
local buffer = require('pending.buffer')
|
|
||||||
if buffer.bufnr() and vim.api.nvim_buf_is_valid(buffer.bufnr()) then
|
|
||||||
buffer.render(buffer.bufnr())
|
|
||||||
end
|
|
||||||
vim.notify(string.format('pending.nvim: Google Tasks pulled — +%d ~%d', created, updated))
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.sync()
|
function M.sync()
|
||||||
oauth.async(function()
|
local access_token, tasklists, s, now_ts = sync_setup()
|
||||||
local access_token, tasklists, s, now_ts = sync_setup()
|
if not access_token then
|
||||||
if not access_token then
|
return
|
||||||
return
|
end
|
||||||
end
|
---@cast tasklists table<string, string>
|
||||||
---@cast tasklists table<string, string>
|
---@cast s pending.Store
|
||||||
---@cast s pending.Store
|
---@cast now_ts string
|
||||||
---@cast now_ts string
|
local by_gtasks_id = build_id_index(s)
|
||||||
local by_gtasks_id = build_id_index(s)
|
local pushed_create, pushed_update, pushed_delete =
|
||||||
local pushed_create, pushed_update, pushed_delete =
|
push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
|
||||||
push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
|
local pulled_create, pulled_update = pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
|
||||||
local pulled_create, pulled_update = pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
|
s: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: Google Tasks synced — push: +%d ~%d -%d, pull: +%d ~%d',
|
||||||
buffer.render(buffer.bufnr())
|
pushed_create,
|
||||||
end
|
pushed_update,
|
||||||
vim.notify(
|
pushed_delete,
|
||||||
string.format(
|
pulled_create,
|
||||||
'pending.nvim: Google Tasks synced — push: +%d ~%d -%d, pull: +%d ~%d',
|
pulled_update
|
||||||
pushed_create,
|
|
||||||
pushed_update,
|
|
||||||
pushed_delete,
|
|
||||||
pulled_create,
|
|
||||||
pulled_update
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
end)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
M._due_to_rfc3339 = due_to_rfc3339
|
M._due_to_rfc3339 = due_to_rfc3339
|
||||||
|
|
|
||||||
|
|
@ -27,27 +27,6 @@ OAuthClient.__index = OAuthClient
|
||||||
---@class pending.oauth
|
---@class pending.oauth
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
---@param args string[]
|
|
||||||
---@param opts? table
|
|
||||||
---@return { code: integer, stdout: string, stderr: string }
|
|
||||||
function M.system(args, opts)
|
|
||||||
local co = coroutine.running()
|
|
||||||
if not co then
|
|
||||||
return vim.system(args, opts or {}):wait() --[[@as { code: integer, stdout: string, stderr: string }]]
|
|
||||||
end
|
|
||||||
vim.system(args, opts or {}, function(result)
|
|
||||||
vim.schedule(function()
|
|
||||||
coroutine.resume(co, result)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
return coroutine.yield() --[[@as { code: integer, stdout: string, stderr: string }]]
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param fn fun(): nil
|
|
||||||
function M.async(fn)
|
|
||||||
coroutine.resume(coroutine.create(fn))
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param str string
|
---@param str string
|
||||||
---@return string
|
---@return string
|
||||||
function M.url_encode(str)
|
function M.url_encode(str)
|
||||||
|
|
@ -112,7 +91,7 @@ function M.curl_request(method, url, headers, body)
|
||||||
table.insert(args, body)
|
table.insert(args, body)
|
||||||
end
|
end
|
||||||
table.insert(args, url)
|
table.insert(args, url)
|
||||||
local result = M.system(args, { text = true })
|
local result = vim.system(args, { text = true }):wait()
|
||||||
if result.code ~= 0 then
|
if result.code ~= 0 then
|
||||||
return nil, 'curl failed: ' .. (result.stderr or '')
|
return nil, 'curl failed: ' .. (result.stderr or '')
|
||||||
end
|
end
|
||||||
|
|
@ -146,6 +125,11 @@ function M.health(backend_name)
|
||||||
else
|
else
|
||||||
vim.health.warn('curl not found (needed for ' .. backend_name .. ' sync)')
|
vim.health.warn('curl not found (needed for ' .. backend_name .. ' sync)')
|
||||||
end
|
end
|
||||||
|
if vim.fn.executable('openssl') == 1 then
|
||||||
|
vim.health.ok('openssl found (required for ' .. backend_name .. ' OAuth PKCE)')
|
||||||
|
else
|
||||||
|
vim.health.warn('openssl not found (needed for ' .. backend_name .. ' OAuth)')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return string
|
---@return string
|
||||||
|
|
@ -205,17 +189,19 @@ function OAuthClient:refresh_access_token(creds, tokens)
|
||||||
.. '&grant_type=refresh_token'
|
.. '&grant_type=refresh_token'
|
||||||
.. '&refresh_token='
|
.. '&refresh_token='
|
||||||
.. M.url_encode(tokens.refresh_token)
|
.. M.url_encode(tokens.refresh_token)
|
||||||
local result = M.system({
|
local result = vim
|
||||||
'curl',
|
.system({
|
||||||
'-s',
|
'curl',
|
||||||
'-X',
|
'-s',
|
||||||
'POST',
|
'-X',
|
||||||
'-H',
|
'POST',
|
||||||
'Content-Type: application/x-www-form-urlencoded',
|
'-H',
|
||||||
'-d',
|
'Content-Type: application/x-www-form-urlencoded',
|
||||||
body,
|
'-d',
|
||||||
TOKEN_URL,
|
body,
|
||||||
}, { text = true })
|
TOKEN_URL,
|
||||||
|
}, { text = true })
|
||||||
|
:wait()
|
||||||
if result.code ~= 0 then
|
if result.code ~= 0 then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
@ -261,18 +247,23 @@ function OAuthClient:auth()
|
||||||
|
|
||||||
local verifier_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'
|
local verifier_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'
|
||||||
local verifier = {}
|
local verifier = {}
|
||||||
math.randomseed(vim.uv.hrtime())
|
math.randomseed(os.time())
|
||||||
for _ = 1, 64 do
|
for _ = 1, 64 do
|
||||||
local idx = math.random(1, #verifier_chars)
|
local idx = math.random(1, #verifier_chars)
|
||||||
table.insert(verifier, verifier_chars:sub(idx, idx))
|
table.insert(verifier, verifier_chars:sub(idx, idx))
|
||||||
end
|
end
|
||||||
local code_verifier = table.concat(verifier)
|
local code_verifier = table.concat(verifier)
|
||||||
|
|
||||||
local hex = vim.fn.sha256(code_verifier)
|
local sha_pipe = vim
|
||||||
local binary = hex:gsub('..', function(h)
|
.system({
|
||||||
return string.char(tonumber(h, 16))
|
'sh',
|
||||||
end)
|
'-c',
|
||||||
local code_challenge = vim.base64.encode(binary):gsub('+', '-'):gsub('/', '_'):gsub('=', '')
|
'printf "%s" "'
|
||||||
|
.. code_verifier
|
||||||
|
.. '" | openssl dgst -sha256 -binary | openssl base64 -A | tr "+/" "-_" | tr -d "="',
|
||||||
|
}, { text = true })
|
||||||
|
:wait()
|
||||||
|
local code_challenge = sha_pipe.stdout or ''
|
||||||
|
|
||||||
local auth_url = AUTH_URL
|
local auth_url = AUTH_URL
|
||||||
.. '?client_id='
|
.. '?client_id='
|
||||||
|
|
@ -292,15 +283,6 @@ function OAuthClient:auth()
|
||||||
vim.notify('pending.nvim: Opening browser for Google authorization...')
|
vim.notify('pending.nvim: Opening browser for Google authorization...')
|
||||||
|
|
||||||
local server = vim.uv.new_tcp()
|
local server = vim.uv.new_tcp()
|
||||||
local server_closed = false
|
|
||||||
local function close_server()
|
|
||||||
if server_closed then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
server_closed = true
|
|
||||||
server:close()
|
|
||||||
end
|
|
||||||
|
|
||||||
server:bind('127.0.0.1', port)
|
server:bind('127.0.0.1', port)
|
||||||
server:listen(1, function(err)
|
server:listen(1, function(err)
|
||||||
if err then
|
if err then
|
||||||
|
|
@ -310,8 +292,6 @@ function OAuthClient:auth()
|
||||||
server:accept(conn)
|
server:accept(conn)
|
||||||
conn:read_start(function(read_err, data)
|
conn:read_start(function(read_err, data)
|
||||||
if read_err or not data then
|
if read_err or not data then
|
||||||
conn:close()
|
|
||||||
close_server()
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local code = data:match('[?&]code=([^&%s]+)')
|
local code = data:match('[?&]code=([^&%s]+)')
|
||||||
|
|
@ -325,7 +305,7 @@ function OAuthClient:auth()
|
||||||
conn:close()
|
conn:close()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
close_server()
|
server:close()
|
||||||
if code then
|
if code then
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
self:_exchange_code(creds, code, code_verifier, port)
|
self:_exchange_code(creds, code, code_verifier, port)
|
||||||
|
|
@ -333,13 +313,6 @@ function OAuthClient:auth()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
vim.defer_fn(function()
|
|
||||||
if not server_closed then
|
|
||||||
close_server()
|
|
||||||
vim.notify('pending.nvim: OAuth callback timed out (120s).', vim.log.levels.WARN)
|
|
||||||
end
|
|
||||||
end, 120000)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param creds pending.OAuthCredentials
|
---@param creds pending.OAuthCredentials
|
||||||
|
|
@ -360,17 +333,19 @@ function OAuthClient:_exchange_code(creds, code, code_verifier, port)
|
||||||
.. '&redirect_uri='
|
.. '&redirect_uri='
|
||||||
.. M.url_encode('http://127.0.0.1:' .. port)
|
.. M.url_encode('http://127.0.0.1:' .. port)
|
||||||
|
|
||||||
local result = M.system({
|
local result = vim
|
||||||
'curl',
|
.system({
|
||||||
'-s',
|
'curl',
|
||||||
'-X',
|
'-s',
|
||||||
'POST',
|
'-X',
|
||||||
'-H',
|
'POST',
|
||||||
'Content-Type: application/x-www-form-urlencoded',
|
'-H',
|
||||||
'-d',
|
'Content-Type: application/x-www-form-urlencoded',
|
||||||
body,
|
'-d',
|
||||||
TOKEN_URL,
|
body,
|
||||||
}, { text = true })
|
TOKEN_URL,
|
||||||
|
}, { text = true })
|
||||||
|
:wait()
|
||||||
|
|
||||||
if result.code ~= 0 then
|
if result.code ~= 0 then
|
||||||
vim.notify('pending.nvim: Token exchange failed.', vim.log.levels.ERROR)
|
vim.notify('pending.nvim: Token exchange failed.', vim.log.levels.ERROR)
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ end, {
|
||||||
for word in after_filter:gmatch('%S+') do
|
for word in after_filter:gmatch('%S+') do
|
||||||
used[word] = true
|
used[word] = true
|
||||||
end
|
end
|
||||||
local candidates = { 'clear', 'overdue', 'today', 'priority', 'done', 'pending' }
|
local candidates = { 'clear', 'overdue', 'today', 'priority' }
|
||||||
local store = require('pending.store')
|
local store = require('pending.store')
|
||||||
local s = store.new(store.resolve_path())
|
local s = store.new(store.resolve_path())
|
||||||
s:load()
|
s:load()
|
||||||
|
|
|
||||||
30
scripts/demo-init.lua
Normal file
30
scripts/demo-init.lua
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
vim.opt.runtimepath:prepend(vim.fn.getcwd())
|
||||||
|
local tmpdir = vim.fn.tempname()
|
||||||
|
vim.fn.mkdir(tmpdir, 'p')
|
||||||
|
|
||||||
|
vim.g.pending = {
|
||||||
|
data_path = tmpdir .. '/tasks.json',
|
||||||
|
}
|
||||||
|
|
||||||
|
local store = require('pending.store')
|
||||||
|
store.load()
|
||||||
|
|
||||||
|
local today = os.date('%Y-%m-%d')
|
||||||
|
local yesterday = os.date('%Y-%m-%d', os.time() - 86400)
|
||||||
|
local tomorrow = os.date('%Y-%m-%d', os.time() + 86400)
|
||||||
|
|
||||||
|
store.add({
|
||||||
|
description = 'Finish quarterly report',
|
||||||
|
category = 'Work',
|
||||||
|
due = tomorrow,
|
||||||
|
recur = 'monthly',
|
||||||
|
priority = 1,
|
||||||
|
})
|
||||||
|
store.add({ description = 'Review pull requests', category = 'Work' })
|
||||||
|
store.add({ description = 'Update deployment docs', category = 'Work', status = 'done' })
|
||||||
|
store.add({ description = 'Buy groceries', category = 'Personal', due = today })
|
||||||
|
store.add({ description = 'Call dentist', category = 'Personal', due = yesterday, priority = 1 })
|
||||||
|
store.add({ description = 'Read chapter 5', category = 'Personal' })
|
||||||
|
store.add({ description = 'Learn a new language', category = 'Someday' })
|
||||||
|
store.add({ description = 'Plan hiking trip', category = 'Someday' })
|
||||||
|
store.save()
|
||||||
28
scripts/demo.tape
Normal file
28
scripts/demo.tape
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
Output assets/demo.gif
|
||||||
|
|
||||||
|
Require nvim
|
||||||
|
|
||||||
|
Set Shell "bash"
|
||||||
|
Set FontSize 14
|
||||||
|
Set Width 900
|
||||||
|
Set Height 450
|
||||||
|
|
||||||
|
Type "nvim -u scripts/demo-init.lua -c 'autocmd VimEnter * Pending'"
|
||||||
|
Enter
|
||||||
|
|
||||||
|
Sleep 2s
|
||||||
|
|
||||||
|
Down
|
||||||
|
Down
|
||||||
|
Sleep 300ms
|
||||||
|
Down
|
||||||
|
Sleep 300ms
|
||||||
|
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
|
||||||
|
Tab
|
||||||
|
Sleep 1s
|
||||||
|
|
||||||
|
Type "q"
|
||||||
|
Sleep 200ms
|
||||||
|
|
@ -199,7 +199,7 @@ describe('diff', function()
|
||||||
assert.are.equal(modified_after_first, task.modified)
|
assert.are.equal(modified_after_first, task.modified)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('preserves due when not present in buffer line', function()
|
it('clears due when removed from buffer line', function()
|
||||||
s:add({ description = 'Pay bill', due = '2026-03-15' })
|
s:add({ description = 'Pay bill', due = '2026-03-15' })
|
||||||
s:save()
|
s:save()
|
||||||
local lines = {
|
local lines = {
|
||||||
|
|
@ -209,20 +209,7 @@ describe('diff', function()
|
||||||
diff.apply(lines, s)
|
diff.apply(lines, s)
|
||||||
s:load()
|
s:load()
|
||||||
local task = s:get(1)
|
local task = s:get(1)
|
||||||
assert.are.equal('2026-03-15', task.due)
|
assert.is_nil(task.due)
|
||||||
end)
|
|
||||||
|
|
||||||
it('updates due when inline token is present', function()
|
|
||||||
s:add({ description = 'Pay bill', due = '2026-03-15' })
|
|
||||||
s:save()
|
|
||||||
local lines = {
|
|
||||||
'# Inbox',
|
|
||||||
'/1/- [ ] Pay bill due:2026-04-01',
|
|
||||||
}
|
|
||||||
diff.apply(lines, s)
|
|
||||||
s:load()
|
|
||||||
local task = s:get(1)
|
|
||||||
assert.are.equal('2026-04-01', task.due)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('stores recur field on new tasks from buffer', function()
|
it('stores recur field on new tasks from buffer', function()
|
||||||
|
|
@ -250,7 +237,7 @@ describe('diff', function()
|
||||||
assert.are.equal('weekly', task.recur)
|
assert.are.equal('weekly', task.recur)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('preserves recur when not present in buffer line', function()
|
it('clears recur when token removed from line', function()
|
||||||
s:add({ description = 'Task', recur = 'daily' })
|
s:add({ description = 'Task', recur = 'daily' })
|
||||||
s:save()
|
s:save()
|
||||||
local lines = {
|
local lines = {
|
||||||
|
|
@ -260,7 +247,7 @@ describe('diff', function()
|
||||||
diff.apply(lines, s)
|
diff.apply(lines, s)
|
||||||
s:load()
|
s:load()
|
||||||
local task = s:get(1)
|
local task = s:get(1)
|
||||||
assert.are.equal('daily', task.recur)
|
assert.is_nil(task.recur)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('parses rec: with completion mode prefix', function()
|
it('parses rec: with completion mode prefix', function()
|
||||||
|
|
|
||||||
|
|
@ -49,27 +49,27 @@ describe('sync', function()
|
||||||
assert.are.equal("gcal backend has no 'notreal' action", msg)
|
assert.are.equal("gcal backend has no 'notreal' action", msg)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('lists actions when action is omitted', function()
|
it('defaults to sync action when action is omitted', function()
|
||||||
local msg = nil
|
|
||||||
local orig = vim.notify
|
|
||||||
vim.notify = function(m)
|
|
||||||
msg = m
|
|
||||||
end
|
|
||||||
pending.command('gcal')
|
|
||||||
vim.notify = orig
|
|
||||||
assert.is_not_nil(msg)
|
|
||||||
assert.is_truthy(msg:find('push'))
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('routes explicit push action', function()
|
|
||||||
local called = false
|
local called = false
|
||||||
local gcal = require('pending.sync.gcal')
|
local gcal = require('pending.sync.gcal')
|
||||||
local orig_push = gcal.push
|
local orig_sync = gcal.sync
|
||||||
gcal.push = function()
|
gcal.sync = function()
|
||||||
called = true
|
called = true
|
||||||
end
|
end
|
||||||
pending.command('gcal push')
|
pending.command('gcal')
|
||||||
gcal.push = orig_push
|
gcal.sync = orig_sync
|
||||||
|
assert.is_true(called)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('routes explicit sync action', function()
|
||||||
|
local called = false
|
||||||
|
local gcal = require('pending.sync.gcal')
|
||||||
|
local orig_sync = gcal.sync
|
||||||
|
gcal.sync = function()
|
||||||
|
called = true
|
||||||
|
end
|
||||||
|
pending.command('gcal sync')
|
||||||
|
gcal.sync = orig_sync
|
||||||
assert.is_true(called)
|
assert.is_true(called)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -90,10 +90,10 @@ describe('sync', function()
|
||||||
config.reset()
|
config.reset()
|
||||||
vim.g.pending = {
|
vim.g.pending = {
|
||||||
data_path = tmpdir .. '/tasks.json',
|
data_path = tmpdir .. '/tasks.json',
|
||||||
sync = { gcal = { client_id = 'test-id' } },
|
sync = { gcal = { calendar = 'NewStyle' } },
|
||||||
}
|
}
|
||||||
local cfg = config.get()
|
local cfg = config.get()
|
||||||
assert.are.equal('test-id', cfg.sync.gcal.client_id)
|
assert.are.equal('NewStyle', cfg.sync.gcal.calendar)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe('gcal module', function()
|
describe('gcal module', function()
|
||||||
|
|
@ -107,9 +107,9 @@ describe('sync', function()
|
||||||
assert.are.equal('function', type(gcal.auth))
|
assert.are.equal('function', type(gcal.auth))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('has push function', function()
|
it('has sync function', function()
|
||||||
local gcal = require('pending.sync.gcal')
|
local gcal = require('pending.sync.gcal')
|
||||||
assert.are.equal('function', type(gcal.push))
|
assert.are.equal('function', type(gcal.sync))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('has health function', function()
|
it('has health function', function()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue