Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,10 @@ Run `:checkhealth shepherd` to verify the binary is found.

`require("shepherd").status()` returns a short string — the open count, plus an
overdue suffix — or `""` when there's nothing open or the count hasn't loaded
yet. Counts refresh after any add/done/rm you make and on `FocusGained`; call
`require("shepherd").refresh()` to force it.
yet. Counts refresh live on every board change — yours, the board TUI's, or
another process's — plus on `FocusGained`; call
`require("shepherd").refresh()` to force it. Set `watch = false` (see
[configuration](#configuration)) to drop back to `FocusGained` only.

lualine:

Expand Down Expand Up @@ -140,6 +142,7 @@ require("shepherd").setup({
board = nil, -- string | fun():string | nil — passed as --board
float = { width = 0.8, height = 0.8, border = "rounded" },
status = { icon = "" }, -- prefix for status(); e.g. a nerd-font glyph
watch = true, -- stream `shepherd watch` to keep the statusline live
})
```

Expand All @@ -153,6 +156,12 @@ require("shepherd").setup({
- `float` — fractions of the editor size, and the window border.
- `status.icon` — prefix for `status()`. With an icon it renders `<icon> 3`;
empty renders `3 todo`.
- `watch` — streams `shepherd watch` in the background and refreshes the
statusline counts on every board change (from the TUI or another process), not
just on `FocusGained`. Re-scopes to the active board on switch, and stops on
exit. One extra process per Neovim, polling the board file once a second; set
it to `false` to skip that. Needs shepherd ≥ 0.16 (the `watch` verb); with no
`shepherd` on `$PATH` it simply doesn't start.

## development

Expand Down
67 changes: 66 additions & 1 deletion lua/shepherd/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ local defaults = {
board = nil, -- string | fun():string | nil
float = { width = 0.8, height = 0.8, border = "rounded" },
status = { icon = "" }, -- prefix for M.status(), e.g. a nerd-font glyph
watch = true, -- stream `shepherd watch` to keep the statusline live
}

local config = vim.tbl_deep_extend("force", {}, defaults)
Expand All @@ -14,6 +15,10 @@ local config = vim.tbl_deep_extend("force", {}, defaults)
-- it wins over config.board; "default" means the unscoped default board.
local active_board = nil

-- watch state, forward-declared so set_active_board (below) can restart the
-- stream; defined near M.status.
local watch_job, watch_start, watch_stop

-- binary returns the configured shepherd command (used by the health check).
function M.binary()
return config.cmd
Expand All @@ -23,6 +28,9 @@ end
-- config.board until changed again.
function M.set_active_board(name)
active_board = name
if watch_job then
watch_start() -- re-scope the stream to the new board
end
end

-- with_board appends "--board <name>" to cmd when one is in effect: the
Expand Down Expand Up @@ -300,6 +308,57 @@ function M.refresh()
end)
end

-- watch streams `shepherd watch` (NDJSON, board-scoped) and refreshes counts on
-- every change, so edits from the board TUI or another process update the
-- statusline without waiting for FocusGained. Opt-in via config.watch.
-- watch_job/watch_start/watch_stop are forward-declared near the top.
function watch_stop()
if watch_job then
pcall(function()
watch_job:kill(15)
end)
watch_job = nil
end
end

function watch_start()
watch_stop()
-- on by default, so a missing binary must stay a checkhealth problem rather
-- than an ENOENT thrown out of setup() on every startup
if vim.fn.executable(config.cmd) ~= 1 then
return
end
local cmd = with_board({ config.cmd, "watch" })
local job
job = vim.system(cmd, {
text = true,
stdout = function(_, data)
if data and data ~= "" then
vim.schedule(M.refresh)
end
end,
}, function(r)
-- a restart's kill lands after the replacement is live, so only the
-- current job may clear the handle — and only its death is worth
-- reporting: a killed predecessor is expected, a live stream dying
-- (no `watch` verb on an older binary) would otherwise fail silently
if watch_job ~= job then
return
end
watch_job = nil
if r.code ~= 0 then
local why = (r.stderr or ""):gsub("%s+$", "")
vim.schedule(function()
vim.notify(
"shepherd: watch stopped — " .. (why ~= "" and why or "exit " .. r.code),
vim.log.levels.ERROR
)
end)
end
end)
watch_job = job
end

-- status returns a statusline string: "" when empty/loading, else the open
-- count with the configured icon and an overdue suffix.
function M.status()
Expand Down Expand Up @@ -440,12 +499,18 @@ function M.setup(opts)
end, { desc = "unarchive an archived board" })

-- keep statusline counts fresh across external edits / other tabs
local group = vim.api.nvim_create_augroup("shepherd", { clear = true })
vim.api.nvim_create_autocmd("FocusGained", {
group = vim.api.nvim_create_augroup("shepherd", { clear = true }),
group = group,
callback = function()
M.refresh()
end,
})

if config.watch then
watch_start()
vim.api.nvim_create_autocmd("VimLeavePre", { group = group, callback = watch_stop })
end
end

-- _run exposes the CLI runner to the board-management submodule
Expand Down
Loading