feat: statusline API, counts, and PendingStatusChanged event (#40)
Problem: no way to know about overdue or due-today tasks without opening :Pending. No ambient awareness for statusline plugins. Solution: add counts(), statusline(), and has_due() public API functions backed by a module-local cache that recomputes after every store.save() and store.load(). Fire a User PendingStatusChanged event on every recompute. Extract is_overdue() and is_today() from duplicate locals into parse.lua as public functions. Refactor views.lua and init.lua to use the shared date logic. Add vimdoc API section and integration recipes for lualine, heirline, manual statusline, startup notification, and event-driven refresh.
This commit is contained in:
parent
302bf8126f
commit
e62e09f609
6 changed files with 507 additions and 69 deletions
264
spec/status_spec.lua
Normal file
264
spec/status_spec.lua
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
require('spec.helpers')
|
||||
|
||||
local config = require('pending.config')
|
||||
local parse = require('pending.parse')
|
||||
local store = require('pending.store')
|
||||
|
||||
describe('status', function()
|
||||
local tmpdir
|
||||
local pending
|
||||
|
||||
before_each(function()
|
||||
tmpdir = vim.fn.tempname()
|
||||
vim.fn.mkdir(tmpdir, 'p')
|
||||
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||
config.reset()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
pending = require('pending')
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.fn.delete(tmpdir, 'rf')
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
end)
|
||||
|
||||
describe('counts', function()
|
||||
it('returns zeroes for empty store', function()
|
||||
store.load()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(0, c.overdue)
|
||||
assert.are.equal(0, c.today)
|
||||
assert.are.equal(0, c.pending)
|
||||
assert.are.equal(0, c.priority)
|
||||
assert.is_nil(c.next_due)
|
||||
end)
|
||||
|
||||
it('counts pending tasks', function()
|
||||
store.load()
|
||||
store.add({ description = 'One' })
|
||||
store.add({ description = 'Two' })
|
||||
store.save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(2, c.pending)
|
||||
end)
|
||||
|
||||
it('counts priority tasks', function()
|
||||
store.load()
|
||||
store.add({ description = 'Urgent', priority = 1 })
|
||||
store.add({ description = 'Normal' })
|
||||
store.save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(1, c.priority)
|
||||
end)
|
||||
|
||||
it('counts overdue tasks with date-only', function()
|
||||
store.load()
|
||||
store.add({ description = 'Old task', due = '2020-01-01' })
|
||||
store.save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(1, c.overdue)
|
||||
end)
|
||||
|
||||
it('counts overdue tasks with datetime', function()
|
||||
store.load()
|
||||
store.add({ description = 'Old task', due = '2020-01-01T08:00' })
|
||||
store.save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(1, c.overdue)
|
||||
end)
|
||||
|
||||
it('counts today tasks', function()
|
||||
store.load()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Today task', due = today })
|
||||
store.save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(1, c.today)
|
||||
assert.are.equal(0, c.overdue)
|
||||
end)
|
||||
|
||||
it('counts mixed overdue and today', function()
|
||||
store.load()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Overdue', due = '2020-01-01' })
|
||||
store.add({ description = 'Today', due = today })
|
||||
store.save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(1, c.overdue)
|
||||
assert.are.equal(1, c.today)
|
||||
end)
|
||||
|
||||
it('excludes done tasks', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Done', due = '2020-01-01' })
|
||||
store.update(t.id, { status = 'done' })
|
||||
store.save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(0, c.overdue)
|
||||
assert.are.equal(0, c.pending)
|
||||
end)
|
||||
|
||||
it('excludes deleted tasks', function()
|
||||
store.load()
|
||||
local t = store.add({ description = 'Deleted', due = '2020-01-01' })
|
||||
store.delete(t.id)
|
||||
store.save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(0, c.overdue)
|
||||
assert.are.equal(0, c.pending)
|
||||
end)
|
||||
|
||||
it('excludes someday sentinel', function()
|
||||
store.load()
|
||||
store.add({ description = 'Someday', due = '9999-12-30' })
|
||||
store.save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(0, c.overdue)
|
||||
assert.are.equal(0, c.today)
|
||||
assert.are.equal(1, c.pending)
|
||||
end)
|
||||
|
||||
it('picks earliest future date as next_due', function()
|
||||
store.load()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Soon', due = '2099-06-01' })
|
||||
store.add({ description = 'Sooner', due = '2099-03-01' })
|
||||
store.add({ description = 'Today', due = today })
|
||||
store.save()
|
||||
pending._recompute_counts()
|
||||
local c = pending.counts()
|
||||
assert.are.equal(today, c.next_due)
|
||||
end)
|
||||
|
||||
it('lazy loads on first counts() call', function()
|
||||
local path = config.get().data_path
|
||||
local f = io.open(path, 'w')
|
||||
f:write(vim.json.encode({
|
||||
version = 1,
|
||||
next_id = 2,
|
||||
tasks = {
|
||||
{
|
||||
id = 1,
|
||||
description = 'Overdue',
|
||||
status = 'pending',
|
||||
due = '2020-01-01',
|
||||
entry = '2020-01-01T00:00:00Z',
|
||||
modified = '2020-01-01T00:00:00Z',
|
||||
},
|
||||
},
|
||||
}))
|
||||
f:close()
|
||||
store.unload()
|
||||
package.loaded['pending'] = nil
|
||||
pending = require('pending')
|
||||
local c = pending.counts()
|
||||
assert.are.equal(1, c.overdue)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('statusline', function()
|
||||
it('returns empty string when nothing actionable', function()
|
||||
store.load()
|
||||
store.save()
|
||||
pending._recompute_counts()
|
||||
assert.are.equal('', pending.statusline())
|
||||
end)
|
||||
|
||||
it('formats overdue only', function()
|
||||
store.load()
|
||||
store.add({ description = 'Old', due = '2020-01-01' })
|
||||
store.save()
|
||||
pending._recompute_counts()
|
||||
assert.are.equal('1 overdue', pending.statusline())
|
||||
end)
|
||||
|
||||
it('formats today only', function()
|
||||
store.load()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Today', due = today })
|
||||
store.save()
|
||||
pending._recompute_counts()
|
||||
assert.are.equal('1 today', pending.statusline())
|
||||
end)
|
||||
|
||||
it('formats overdue and today', function()
|
||||
store.load()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Old', due = '2020-01-01' })
|
||||
store.add({ description = 'Today', due = today })
|
||||
store.save()
|
||||
pending._recompute_counts()
|
||||
assert.are.equal('1 overdue, 1 today', pending.statusline())
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('has_due', function()
|
||||
it('returns false when nothing due', function()
|
||||
store.load()
|
||||
store.add({ description = 'Future', due = '2099-01-01' })
|
||||
store.save()
|
||||
pending._recompute_counts()
|
||||
assert.is_false(pending.has_due())
|
||||
end)
|
||||
|
||||
it('returns true when overdue', function()
|
||||
store.load()
|
||||
store.add({ description = 'Old', due = '2020-01-01' })
|
||||
store.save()
|
||||
pending._recompute_counts()
|
||||
assert.is_true(pending.has_due())
|
||||
end)
|
||||
|
||||
it('returns true when today', function()
|
||||
store.load()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
store.add({ description = 'Now', due = today })
|
||||
store.save()
|
||||
pending._recompute_counts()
|
||||
assert.is_true(pending.has_due())
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('parse.is_overdue', function()
|
||||
it('date before today is overdue', function()
|
||||
assert.is_true(parse.is_overdue('2020-01-01'))
|
||||
end)
|
||||
|
||||
it('date after today is not overdue', function()
|
||||
assert.is_false(parse.is_overdue('2099-01-01'))
|
||||
end)
|
||||
|
||||
it('today date-only is not overdue', function()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
assert.is_false(parse.is_overdue(today))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('parse.is_today', function()
|
||||
it('today date-only is today', function()
|
||||
local today = os.date('%Y-%m-%d') --[[@as string]]
|
||||
assert.is_true(parse.is_today(today))
|
||||
end)
|
||||
|
||||
it('yesterday is not today', function()
|
||||
assert.is_false(parse.is_today('2020-01-01'))
|
||||
end)
|
||||
|
||||
it('tomorrow is not today', function()
|
||||
assert.is_false(parse.is_today('2099-01-01'))
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
Loading…
Add table
Add a link
Reference in a new issue