Your tasks, documented¶
Footman ships a first-party plugin that renders a project's task tree as
markdown: the same names, params, docstring help, defaults, and examples
that fm --help shows, as pages you can publish. Everything on this page is
dogfooded: the task reference in this site's nav is
generated output, and the sample further down is embedded live.
Write plain prose¶
Footman surfaces a docstring, and a doc()
string, as plain text: verbatim in fm --help, so it reads in any terminal
and survives a pipe (fm --help build | less) byte for byte, and as plain
paragraphs in the exported markdown. It renders no rich markup in the terminal:
no bold, no headings, no reflowed tables. That is deliberate: footman is
zero-dependency, and plain text is the one format every
terminal, pager, and CI log agrees on.
So write your help as prose. A sentence or two that reads straight is worth more than markup footman won't paint, and it exports cleanly either way. The one styling footman does apply is colour, to what footman itself prints (names, receipts, the progress line) and, through the tools it spawns, to their output, never injected into your text. Your task's own stdout is yours; footman routes it untouched.
(A future opt-in could render markdown in the terminal too, see the roadmap, but it stays off by default and never becomes a dependency.)
Mount it¶
The plugin mounts like any other, in one line in your tasks file:
That's also the one-line demo of the plugin system: the
entry point is the identity, into= is your placement, and after the mount
fm --list shows docs.page and docs.site. (Cherry-pick
with only=, or pass into="footman" to nest the group deeper, as
footman.docs.page.)
One page: fm docs.page¶
fm docs.page > TASKS.md # the whole tree, one document
fm docs.page --target=docs # just one group…
fm docs.page --target=docs.build # …or one task
fm docs.page --out=TASKS.md # write the file directly
The page goes to stdout (stdout is the answer; footman's summary is stderr commentary), so it pipes:
--heading=2 (up to 6) makes the headings start deeper, so the output nests
under a host page's own title, which is exactly how the sample below is
embedded, via a pymdownx.snippets
include of a file the docs build regenerates:
### docs { #docs }
Documentation site (Zensical)
#### docs.serve { #docs-serve }
Build and serve the docs with live reload.
```text
fm docs.serve
docs.coverage¶
Generate the coverage HTML report into docs/htmlcov (embedded in the site).
Returns: The measured total and where the HTML report landed. — CoverageReport {percent, statements, missing, report}
| Field | Type |
|---|---|
percent |
a number |
statements |
an integer |
missing |
an integer |
report |
a path |
docs.api¶
Generate the API reference from the export table; refuse on drift.
| Parameter | Type | Default | Description |
|---|---|---|---|
--out=PATH |
path | "docs/api.md" |
docs.build¶
Build the docs site into ./site; regenerates llms.txt and docs/tasks/.
Not unit-tested, and deliberately: the body is orchestration over zensical, a pty screenshotter and five real shells, so a test could only stub twenty collaborators and assert the call order back — a change detector that passes while the site breaks. Its real test is CI's strict docs build, which runs the whole thing against the actual tools. The pieces with logic of their own — the llms.txt generator, the cast guard, the scratch projects — are tested separately, where a test can say something true.
| Parameter | Type | Default | Description |
|---|---|---|---|
--check |
flag | false |
build strictly (what CI runs) |
`--flavor=plain` (the default) is pure CommonMark and pipe tables, safe for
pandoc and any renderer. `--flavor=material` opts into what a
zensical/mkdocs-material site already understands: heading anchors for
stable deep links and an `!!! example` admonition for the synthesized
invocation.
## A linked site: `fm docs.site`
```console
$ fm docs.site docs/tasks
wrote 20 pages under docs/tasks
One file per task, an index.md per group with relative links, directories
mirroring your group tree: drop it into your docs source and put the index
in your nav. This site's Task reference section is exactly that, wired
into zensical.toml's
nav. site defaults to --flavor=material because a docs site is where it
lands; pass --flavor=plain for anything else.
The runner itself: fm docs.globals¶
Your tasks aren't the only thing worth documenting; the runner's global
options deserve a page too. globals renders them as a markdown table
straight from the CLI grammar: the same rows, in the same order, with the
same words --help prints. This site's CLI reference table
is exactly that, regenerated on every docs build, so it cannot drift,
because it was never written by hand.
Terminal screenshots: fm docs.shots¶
Prose about colours drifts the moment the palette changes; a screenshot
generated from the CLI cannot. shots runs a command on a real
pseudo-terminal (colours, receipts, taught errors, exactly as a terminal
shows them), collapses the live rewrites to their final frame, and renders
the capture as an SVG in a macOS-style window:
$ fm docs.shots --out=docs/_generated/shots/run.svg -- format lint
wrote docs/_generated/shots/run.svg
Everything after -- is the command line to capture; --width sets the
terminal columns, --title the window title, and --cmd swaps the
executable (default: the CLI that invoked it, so a branded CLI screenshots
itself; App is a library, and the screenshotter simply runs your CLI).
The command really executes, so don't screenshot tasks whose side
effects you don't want. Every terminal image on this site is one of these,
regenerated by the docs build.
This task needs rich and a POSIX
pseudo-terminal. Neither is a footman dependency: the task is gated with
footman's own @requires_dep("rich"), so without rich it lists as
shots … (unavailable: requires rich) and refuses to run with that
message; add rich to your docs dependency group and it comes alive.
Footman documenting itself with its own availability machinery is exactly
the use @requires_dep was built for.
Animated sessions: fm docs.cast¶
A static frame can't show Tab completion. cast boots a real
interactive shell (zsh, bash, fish, pwsh, or nushell) from a scratch config
with footman's hook loaded, types a keystroke script, and replays the
captured bytes through a terminal emulator into an animated SVG: CSS
keyframes with the session's own timing, no JavaScript, plays anywhere an
image does. The session even answers its shell's terminal interrogations
(capability, cursor, and colour queries) the way a plain xterm would, since
modern shells refuse to paint a prompt into silence:
$ fm docs.cast --out=docs/_generated/shots/zsh-cast.svg \
--shell=zsh -- "fm " "<TAB>" "<WAIT>" "che" "<TAB>" "<ENTER>" "<WAIT:2500>"
wrote docs/_generated/shots/zsh-cast.svg (55 frames)
Everything after -- is the script: plain text is typed at a human-ish
cadence; <TAB>, <ENTER>, <SPACE>, <BACKSPACE>, <CTRL-C>,
<WAIT>, and <WAIT:ms> are keys. The shell really runs what you type:
the recording on the zsh completion page ends with a
real fm check. Needs rich and pyte
(the terminal emulator), gated the same way: without them the task lists
as unavailable and says which package to add.
Keep it fresh¶
Generated pages drift unless a build regenerates them. The tasks are plain functions, so footman's own docs task calls them directly. Copy the shape:
from pathlib import Path
from footman import group
docs = group("docs", help="Documentation")
@docs.task(name="build")
def docs_build(check: bool = False):
"Build the docs site; regenerates the task reference first."
from footman.tasks.docs import globals_, page, site
from toolroom import zensical
site(Path("docs/tasks"))
page(target="docs", heading=3, out=Path("_generated/tasks-page.md"))
globals_(out=Path("_generated/globals.md"))
zensical.build(clean=True, strict=check)
Add the generated paths to .gitignore: they're build output, not source.
Under --json, both tasks return the list of files they wrote,
so returned carries it for CI to verify.
Two flags to know. Usage lines and examples carry the CLI you invoked, so
a branded CLI documents itself as acme with no flag at
all, and --prog overrides the name when you need to. --all includes the
mounted footman group itself (excluded by default, since the documenter
doesn't document itself unless asked).
Link the terminal back to the pages¶
Once the pages are published, close the loop: a docs_url template under
[tool.footman] turns every task and group name in --list, --tree and
the --help pages into a terminal hyperlink to its own docs, and puts a
docs_url field on each task's --json row for readers that
cannot click.
[tool.footman]
docs_url = "https://docs.example.dev/tasks/{path}/" # docs.site: page per task
# docs_url = "https://docs.example.dev/reference/#{slug}" # docs.page: anchors
{path} is the task's slash-joined address (docs.build → docs/build),
matching the per-task pages docs.site writes; {slug} is the dash-joined
one (docs-build), matching the heading anchors the one-page docs.page
export carries. An unknown placeholder is refused by name. The links ride
the colour switch — piped output stays plain, and a terminal without
hyperlink support simply ignores them — and each --help page also prints
the URL as visible text, so there is always something to copy.
The live sample¶
Everything below this line is fm docs.page --target=docs
--heading=3 --flavor=material, regenerated on every docs build:
docs¶
Documentation site (Zensical)
docs.serve¶
Build and serve the docs with live reload.
docs.coverage¶
Generate the coverage HTML report into docs/htmlcov (embedded in the site).
Returns: The measured total and where the HTML report landed. — CoverageReport {percent, statements, missing, report}
| Field | Type |
|---|---|
percent |
a number |
statements |
an integer |
missing |
an integer |
report |
a path |
docs.api¶
Generate the API reference from the export table; refuse on drift.
| Parameter | Type | Default | Description |
|---|---|---|---|
--out=PATH |
path | "docs/api.md" |
docs.build¶
Build the docs site into ./site; regenerates llms.txt and docs/tasks/.
Not unit-tested, and deliberately: the body is orchestration over zensical, a pty screenshotter and five real shells, so a test could only stub twenty collaborators and assert the call order back — a change detector that passes while the site breaks. Its real test is CI's strict docs build, which runs the whole thing against the actual tools. The pieces with logic of their own — the llms.txt generator, the cast guard, the scratch projects — are tested separately, where a test can say something true.
| Parameter | Type | Default | Description |
|---|---|---|---|
--check |
flag | false |
build strictly (what CI runs) |