From c37cf7cc3af5d56ac5a0da29825986eb0b383b51 Mon Sep 17 00:00:00 2001 From: Barrett Ruth <62671086+barrettruth@users.noreply.github.com> Date: Sun, 8 Mar 2026 20:56:22 -0400 Subject: [PATCH] fix(sync): normalize log prefixes and S3 prompt UX (#115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(s3): create bucket interactively during auth when unconfigured Problem: when a user runs `:Pending s3 auth` with no bucket configured, auth succeeds but offers no way to create the bucket. The user must manually run `aws s3api create-bucket` and update their config. Solution: add `util.input()` coroutine-aware prompt wrapper and a `create_bucket()` flow in `s3.lua` that prompts for bucket name and region, handles the `us-east-1` LocationConstraint quirk, and logs a config snippet on success. Called automatically from `auth()` when `sync.s3.bucket` is absent. * ci: typing * feat(parse): add `parse_duration_to_days` for duration string conversion Problem: The archive command accepted only a bare integer for days, inconsistent with the `+Nd`/`+Nw`/`+Nm` duration syntax used elsewhere. Solution: Add `parse_duration_to_days()` supporting `Nd`, `Nw`, `Nm`, and bare integers. Returns nil on invalid input for caller error handling. * feat(archive): duration syntax and confirmation prompt Problem: `:Pending archive` accepted only a bare integer for days and silently deleted tasks with no confirmation, risking accidental data loss. Solution: Accept duration strings (`7d`, `3w`, `2m`) via `parse.parse_duration_to_days()`, show a `vim.ui.input` confirmation prompt before removing tasks, and skip the prompt when zero tasks match. * feat: add `` / `` keymaps for priority increment/decrement Problem: Priority could only be cycled with `g!` (0→1→2→3→0), with no way to directly increment or decrement. Solution: Add `adjust_priority()` with clamping at 0 and `max_priority`, exposed as `increment_priority()` / `decrement_priority()` on `` / ``. Includes `` mappings and vimdoc. * fix(s3): use parenthetical defaults in bucket creation prompts Problem: `util.input` with `default` pre-filled the input field, and the success message said "Add to your config" ambiguously. Solution: Show defaults in prompt text as `(default)` instead of pre-filling, and clarify the message to "Add to your pending.nvim config". * ci: format * ci(sync): normalize log prefix to `backend:` across all sync backends Problem: Sync log messages used inconsistent prefixes like `s3 push:`, `gtasks pull:`, `gtasks sync —` instead of the `backend: action` pattern used by auth messages. Solution: Normalize all sync backend logs to `backend: action ...` format across `s3.lua`, `gcal.lua`, and `gtasks.lua`. * ci: fix linter warnings in archive spec and s3 bucket creation --- lua/pending/sync/gcal.lua | 2 +- lua/pending/sync/gtasks.lua | 8 ++++---- lua/pending/sync/s3.lua | 22 ++++++++++------------ spec/archive_spec.lua | 6 +++--- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/lua/pending/sync/gcal.lua b/lua/pending/sync/gcal.lua index 2e50fd5..d316375 100644 --- a/lua/pending/sync/gcal.lua +++ b/lua/pending/sync/gcal.lua @@ -233,7 +233,7 @@ function M.push() end util.finish(s) - log.info('gcal push: ' .. util.fmt_counts({ + log.info('gcal: push ' .. util.fmt_counts({ { created, 'added' }, { updated, 'updated' }, { deleted, 'removed' }, diff --git a/lua/pending/sync/gtasks.lua b/lua/pending/sync/gtasks.lua index 9eade7d..272bed6 100644 --- a/lua/pending/sync/gtasks.lua +++ b/lua/pending/sync/gtasks.lua @@ -434,7 +434,7 @@ function M.push() local created, updated, deleted, failed = push_pass(access_token, tasklists, s, now_ts, by_gtasks_id) util.finish(s) - log.info('gtasks push: ' .. util.fmt_counts({ + log.info('gtasks: push ' .. util.fmt_counts({ { created, 'added' }, { updated, 'updated' }, { deleted, 'deleted' }, @@ -456,7 +456,7 @@ function M.pull() pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id) local unlinked = detect_remote_deletions(s, seen_remote_ids, fetched_list_ids, now_ts) util.finish(s) - log.info('gtasks pull: ' .. util.fmt_counts({ + log.info('gtasks: pull ' .. util.fmt_counts({ { created, 'added' }, { updated, 'updated' }, { unlinked, 'unlinked' }, @@ -480,12 +480,12 @@ function M.sync() pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id) local unlinked = detect_remote_deletions(s, seen_remote_ids, fetched_list_ids, now_ts) util.finish(s) - log.info('gtasks sync — push: ' .. util.fmt_counts({ + log.info('gtasks: sync push ' .. util.fmt_counts({ { pushed_create, 'added' }, { pushed_update, 'updated' }, { pushed_delete, 'deleted' }, { pushed_failed, 'failed' }, - }) .. ' pull: ' .. util.fmt_counts({ + }) .. ' | pull ' .. util.fmt_counts({ { pulled_create, 'added' }, { pulled_update, 'updated' }, { unlinked, 'unlinked' }, diff --git a/lua/pending/sync/s3.lua b/lua/pending/sync/s3.lua index 373052a..64c4348 100644 --- a/lua/pending/sync/s3.lua +++ b/lua/pending/sync/s3.lua @@ -88,9 +88,7 @@ local function create_bucket() end local region = util.input({ prompt = 'AWS region (' .. default_region .. '): ' }) - if not region then - region = default_region - elseif region == '' then + if not region or region == '' then region = default_region end @@ -220,12 +218,12 @@ function M.push() os.remove(tmpfile) if result.code ~= 0 then - log.error('s3 push: ' .. (result.stderr or 'unknown error')) + log.error('s3: push failed — ' .. (result.stderr or 'unknown error')) return end util.finish(s) - log.info('s3 push: uploaded to s3://' .. s3cfg.bucket .. '/' .. key) + log.info('s3: push uploaded to s3://' .. s3cfg.bucket .. '/' .. key) end) end) end @@ -247,7 +245,7 @@ function M.pull() if result.code ~= 0 then os.remove(tmpfile) - log.error('s3 pull: ' .. (result.stderr or 'unknown error')) + log.error('s3: pull failed — ' .. (result.stderr or 'unknown error')) return end @@ -258,7 +256,7 @@ function M.pull() end) if not load_ok then os.remove(tmpfile) - log.error('s3 pull: failed to parse remote store') + log.error('s3: pull failed — could not parse remote store') return end @@ -320,7 +318,7 @@ function M.pull() os.remove(tmpfile) util.finish(s) - log.info('s3 pull: ' .. util.fmt_counts({ + log.info('s3: pull ' .. util.fmt_counts({ { created, 'added' }, { updated, 'updated' }, { unchanged, 'unchanged' }, @@ -416,7 +414,7 @@ function M.sync() local f = io.open(s.path, 'r') if not f then - log.error('s3 sync: failed to read store file') + log.error('s3: sync failed — could not read store file') return end local content = f:read('*a') @@ -425,7 +423,7 @@ function M.sync() local push_tmpfile = vim.fn.tempname() .. '.json' local tf = io.open(push_tmpfile, 'w') if not tf then - log.error('s3 sync: failed to create temp file') + log.error('s3: sync failed — could not create temp file') return end tf:write(content) @@ -437,13 +435,13 @@ function M.sync() os.remove(push_tmpfile) if push_result.code ~= 0 then - log.error('s3 sync push: ' .. (push_result.stderr or 'unknown error')) + log.error('s3: sync push failed — ' .. (push_result.stderr or 'unknown error')) util.finish(s) return end util.finish(s) - log.info('s3 sync: pull ' .. util.fmt_counts({ + log.info('s3: sync ' .. util.fmt_counts({ { created, 'added' }, { updated, 'updated' }, }) .. ' | push uploaded') diff --git a/spec/archive_spec.lua b/spec/archive_spec.lua index 94331b6..525b12e 100644 --- a/spec/archive_spec.lua +++ b/spec/archive_spec.lua @@ -27,13 +27,13 @@ describe('archive', function() end) local function auto_confirm_y() - vim.ui.input = function(opts, on_confirm) + vim.ui.input = function(_, on_confirm) on_confirm('y') end end local function auto_confirm_n() - vim.ui.input = function(opts, on_confirm) + vim.ui.input = function(_, on_confirm) on_confirm('n') end end @@ -206,7 +206,7 @@ describe('archive', function() end) it('does not archive when user cancels confirmation (nil)', function() - vim.ui.input = function(opts, on_confirm) + vim.ui.input = function(_, on_confirm) on_confirm(nil) end local s = pending.store()