Skip to content

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 its own chrome (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 — two lines of config, no tasks-file edit:

# pyproject.toml (or footman.toml)
[tool.footman]
plugins = ["footman.docs"]

That's also the two-line demo of the plugin system: a plugin's name is its command path, so footman.docs mounts under footman, and after it fm --list shows footman docs page and footman docs site. (Cherry-pick or remount with include(plugin("footman.docs"), …) if you'd rather not take the whole group. The maintainer-facing stub toolkit is a separate plugin, footman.tools.)

One page: fm footman docs page

fm footman docs page > TASKS.md          # the whole tree, one document
fm footman docs page --target docs       # just one group…
fm footman docs page --target docs.build # …or one task
fm footman 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:

fm footman docs page | pandoc -o tasks.pdf     # or .html, .docx, …

--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

Example

$ fm docs serve

docs coverage

Generate the coverage HTML report into docs/htmlcov (embedded in the site).

fm docs coverage

Example

$ fm docs coverage

docs build

Build the docs site into ./site; regenerates llms.txt and docs/tasks/.

fm docs build [--check]
Parameter Type Default Description
--check flag false build strictly (what CI runs)

Example

$ fm docs build --check
`--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 footman docs site`

```console
$ fm footman docs site docs/tasks
wrote 19 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 footman 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 — it cannot drift, because it was never written by hand.

$ fm footman docs globals --out docs/_generated/globals.md
wrote docs/_generated/globals.md

Terminal screenshots: fm footman 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 footman 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). The command really executes — 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 simply 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 footman 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 — modern shells refuse to paint a prompt into silence:

$ fm footman 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:

@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 footman.tools import zensical

    site(Path("docs/tasks"))
    page(target="docs", heading=3, out=Path("docs/_generated/tasks-page.md"))
    globals_(out=Path("docs/_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 — 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 — the documenter doesn't document itself unless asked).

The live sample

Everything below this line is fm footman 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.

fm docs serve

Example

$ fm docs serve

docs coverage

Generate the coverage HTML report into docs/htmlcov (embedded in the site).

fm docs coverage

Example

$ fm docs coverage

docs build

Build the docs site into ./site; regenerates llms.txt and docs/tasks/.

fm docs build [--check]
Parameter Type Default Description
--check flag false build strictly (what CI runs)

Example

$ fm docs build --check