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
78 changes: 78 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# contributing

A Neovim plugin with no dependencies, three files under `lua/shepherd/`:

| File | What |
|------|------|
| `init.lua` | commands, the picker, quick-add/capture, statusline counts |
| `board.lua` | the board switcher and board CRUD, over `boards --json` |
| `health.lua` | `:checkhealth shepherd` — finds the binary |

Every read and write goes through the installed `shepherd` binary. The plugin
never parses or writes a board file itself — the binary owns that format, so
keep new features behind a CLI verb rather than touching markdown here.

```sh
make fmt # stylua
make lint # stylua --check + luacheck
make test # busted
make check # lint + test
```

Test deps: [busted](https://lunarmodules.github.io/busted/) and `nlua` (the specs
run under Neovim) — `luarocks install busted nlua`. Formatting and linting need
[stylua](https://github.com/JohnnyMorganz/StyLua) and
[luacheck](https://github.com/lunarmodules/luacheck).

## tests

`tests/shepherd_spec.lua` drives the pure helpers — `tally`, `format_status`,
`label`, `flatten`, `edit_seed`, `clean`, and `build_cmd`/filter resolution —
exposed on the `_internal` table. Keep new logic in a pure helper where you can:
anything that only reshapes data is testable without a running UI, and the
command wiring around it stays thin.

CI (`.github/workflows/pull-request.yml`) runs the lint job plus the suite on
Neovim stable and nightly. Both must be green.

## conventions

- stylua: tabs, 120 columns (`.stylua.toml`). Run `make fmt` before committing.
- luacheck: `vim` is the only permitted global (`.luacheckrc`).
- [Conventional Commits](https://www.conventionalcommits.org) — `feat:`, `fix:`,
`docs:`, `chore:`.
- Branch for the change; don't commit to `master`.
- The plugin targets the Neovim version in [requirements](README.md#requirements)
— no compatibility shims for older releases.

## demo gifs

The README's `assets/demo.gif` (the commands) and `assets/stats.gif`
(`:ShepherdStats`) are scripted with
[vhs](https://github.com/charmbracelet/vhs), not screen-captured — regenerate
them after any change to the commands, the picker, or the statusline:

```sh
brew install vhs # pulls in ttyd + ffmpeg
vhs assets/demo.tape
vhs assets/stats.tape
```

Both tapes run nvim with `assets/demo-init.lua` — this checkout and nothing else,
so the recording never shows another plugin's UI — against a board seeded by
`assets/demo-seed.sh` under `mktemp -d`, with `$SHEPHERD_TODO_FILE` and
`$SHEPHERD_CONFIG` pointed at it. A real board or config is never touched.

Two things worth knowing when you edit a tape:

- The seed writes its dates relative to today, so due dates and the aging numbers
stay meaningful whenever a gif is re-recorded.
- The window has to clear the tallest screen it shows. `shepherd stats` is 56
rows and the float only gets 85% of the window, which is why `stats.tape`
records at a smaller font in a taller window than `demo.tape`.

Named boards live under `$XDG_CONFIG_HOME/shepherd/boards/`, *not* wherever
`$SHEPHERD_TODO_FILE` points — that variable only overrides the default board's
file. Set `XDG_CONFIG_HOME` as well when you want a scratch tree for board
commands (`:ShepherdBoards` and friends), or they will read and write your real
boards.
33 changes: 21 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ buffer. Zero dependencies; drives the installed `shepherd` binary.
- [requirements](#requirements)
- [install](#install)
- [usage](#usage)
- [stats](#stats)
- [statusline](#statusline)
- [configuration](#configuration)
- [development](#development)

![nvim-shepherd in action](assets/demo.gif)

## requirements

- Neovim ≥ 0.11 (`vim.fn.jobstart({ term = true })`, `vim.system`).
Expand Down Expand Up @@ -79,8 +82,8 @@ return {
- `:ShepherdList work` — pick within a filtered view. `:ShepherdList!` picks
across all boards (read-only — items show their `[board]`; only open-link is
offered).
- `:ShepherdStats` — open shepherd's stats charts in the floating terminal.
`:ShepherdStats!` aggregates every board (`shepherd stats --all`).
- `:ShepherdStats` — open shepherd's stats charts in the floating terminal (see
[stats](#stats)). `:ShepherdStats!` aggregates every board.
- `:ShepherdBoards` — pick a board (shows `open/total`, `*` marks current, and
its working **dir** if set), then **switch** the session to it, **rename**,
set its **dir** (working directory; empty clears), **archive**, or **delete**
Expand All @@ -94,6 +97,19 @@ return {

Run `:checkhealth shepherd` to verify the binary is found.

## stats

`:ShepherdStats` runs `shepherd stats` in the floating terminal — completion, due
and urgency, priority load, aging, status mix, throughput and backlog trend for
the active board. `:ShepherdStats!` aggregates every board and adds a by-board
breakdown.

![:ShepherdStats](assets/stats.gif)

The charts are one-shot output, so the float holds until you dismiss it with `q`,
`<esc>` or `<cr>` — unlike `:Shepherd`, which closes when the board exits.
Done-based counts include the archive.

## statusline

`require("shepherd").status()` returns a short string — the open count, plus an
Expand Down Expand Up @@ -156,19 +172,12 @@ require("shepherd").setup({

## development

Tooling: [stylua](https://github.com/JohnnyMorganz/StyLua),
[luacheck](https://github.com/lunarmodules/luacheck), and
[busted](https://lunarmodules.github.io/busted/) + `nlua` (tests run under
Neovim). Install the test deps with `luarocks install busted nlua`.

```sh
make fmt # format
make fmt # stylua
make lint # stylua --check + luacheck
make test # busted
make check # lint + test
```

CI (`.github/workflows/pull-request.yml`) runs lint plus the suite on Neovim
stable and nightly. Tests cover the pure helpers (`tally`, `format_status`, `label`,
`flatten`, `edit_seed`, `clean`, `build_cmd`/filter resolution) via the
`_internal` table.
See [CONTRIBUTING.md](CONTRIBUTING.md) for the layout, the test approach, the
conventions, and how to re-record the demo gif.
33 changes: 33 additions & 0 deletions assets/demo-init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
-- Minimal init for the vhs recording (assets/demo.tape): loads this checkout,
-- nothing else. No plugin manager, no colourscheme, no other plugins, so what
-- the gif shows is only ever nvim-shepherd.
--
-- nvim -u assets/demo-init.lua
--
-- The board comes from $SHEPHERD_TODO_FILE (the tape points it at a scratch
-- file), so `board` stays nil here.

local root = vim.fn.fnamemodify(debug.getinfo(1, "S").source:sub(2), ":p:h:h")
vim.opt.runtimepath:prepend(root)

vim.o.swapfile = false
vim.o.shadafile = "NONE"
vim.o.termguicolors = true
vim.o.number = true
vim.o.showmode = false
vim.o.laststatus = 3
vim.o.statusline = " %t%=%{v:lua.require'shepherd'.status()} "

require("shepherd").setup({
status = { icon = "todo" },
float = { width = 0.85, height = 0.85, border = "rounded" },
})

-- The counts land asynchronously, so redraw when they do — same hook the README
-- suggests for a statusline that doesn't refresh itself.
vim.api.nvim_create_autocmd("User", {
pattern = "ShepherdStatusUpdate",
callback = function()
vim.cmd.redrawstatus()
end,
})
80 changes: 80 additions & 0 deletions assets/demo-seed.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/bin/sh
# Seeds a throwaway board plus its archive for the vhs recording
# (assets/demo.tape). $SHEPHERD_TODO_FILE must already point at a scratch
# todo.md — the tape sets it under `mktemp -d`.
#
# Dates are written relative to today, so the board and `shepherd stats` still
# look lived-in whenever the gif gets re-recorded.
set -e

board=$SHEPHERD_TODO_FILE
archive=$(dirname "$board")/archive.md

ago() { date -v-"$1"d +%d-%m-%Y 2>/dev/null || date -d "$1 days ago" +%d-%m-%Y; }
in_days() { date -v+"$1"d +%Y-%m-%d 2>/dev/null || date -d "$1 days" +%Y-%m-%d; }

cat >"$board" <<EOF
- [ ] (H) float the picker over the buffer
created: $(ago 2) 09:14
due: $(in_days 1)
category: plugin
- [ ] map the actions
created: $(ago 2) 09:15
- [ ] wire the statusline count
created: $(ago 3) 16:02
category: plugin
status: in-progress
- [ ] document the capture command
created: $(ago 6) 11:30
due: $(in_days 7)
category: docs
- [ ] (L) tidy the health check
created: $(ago 11) 14:47
category: docs
- [ ] (H) fix the flaky spec on nightly
created: $(ago 38) 08:21
category: ci
EOF

cat >"$archive" <<EOF
- [x] (H) open the board in a float
created: $(ago 26) 09:00
completed: $(ago 24) 17:40
category: plugin
- [x] quick-add without leaving the buffer
created: $(ago 25) 10:15
completed: $(ago 22) 11:05
category: plugin
- [x] (H) board-scoped commands
created: $(ago 22) 09:30
completed: $(ago 21) 15:20
category: plugin
- [x] pick an item with vim.ui.select
created: $(ago 20) 13:00
completed: $(ago 18) 09:45
category: plugin
- [x] (L) checkhealth for the binary
created: $(ago 19) 08:10
completed: $(ago 18) 08:55
category: docs
- [x] run the specs on nightly
created: $(ago 17) 14:20
completed: $(ago 12) 16:30
category: ci
- [x] (H) archive an item from the picker
created: $(ago 15) 09:05
completed: $(ago 9) 18:10
category: plugin
- [x] board working directories
created: $(ago 12) 11:40
completed: $(ago 7) 10:25
category: plugin
- [x] aggregate stats across boards
created: $(ago 9) 15:15
completed: $(ago 4) 12:00
category: plugin
- [x] (M) capture the visual selection
created: $(ago 5) 09:50
completed: $(ago 2) 14:35
category: plugin
EOF
Binary file added assets/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
94 changes: 94 additions & 0 deletions assets/demo.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Regenerate assets/demo.gif:
#
# brew install vhs # pulls in ttyd + ffmpeg
# vhs assets/demo.tape
#
# Runs nvim with assets/demo-init.lua (this checkout, no other plugins) against a
# throwaway board seeded by assets/demo-seed.sh, so recording never touches a
# real board or config.

Require nvim
Require shepherd

Output assets/demo.gif

Set Shell zsh
Set FontSize 15
Set Width 1320
Set Height 800
Set Padding 20
Set Theme "Catppuccin Mocha"
Set Framerate 24
Set TypingSpeed 55ms

Hide
Type 'd=$(mktemp -d) && export SHEPHERD_TODO_FILE=$d/todo.md SHEPHERD_CONFIG=$d/config.toml'
Enter
Type 'sh assets/demo-seed.sh'
Enter
Type 'printf "%s\n" "local M = {}" "" "-- TODO: retry the board fetch on a transient failure" "function M.fetch() end" "" "return M" > $d/board.lua'
Enter
Type 'clear'
Enter
Show

Type "nvim -u assets/demo-init.lua $d/board.lua"
Enter
Sleep 3s

# the statusline carries the open count for the board
Sleep 1.5s

# capture the TODO comment on line 3 as a todo
Type "3G"
Sleep 1s
Type ":ShepherdCapture"
Enter
Sleep 1.5s
Type " @plugin !h"
Sleep 800ms
Enter
Sleep 2.5s

# pick an item, then edit it with a pre-filled quick-add line
Type ":ShepherdList"
Enter
Sleep 2.5s
Type "4"
Enter
Sleep 2s
Type "2"
Enter
Sleep 2s
Type " due:tomorrow"
Sleep 800ms
Enter
Sleep 2.5s

# pick again, this time tick it off
Type ":ShepherdList"
Enter
Sleep 2s
Type "4"
Enter
Sleep 1.5s
Type "1"
Enter
Sleep 2.5s

# the whole board, in a floating terminal
Type ":Shepherd"
Enter
Sleep 3s
Down@450ms 2
Sleep 1s
Space
Sleep 1.5s
Type "v"
Sleep 2s
Type "q"
Sleep 2s

Type ":q!"
Enter
Sleep 1.5s
Binary file added assets/stats.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading