ctl-core
Shared Rust CLI chassis and presentation kernel for forkctl, qctl,
verctl, and state-sync. It is a library, not a command.
A consumer owns typed domain requests and results. ctl-core owns parsing, help, Usage mounts, color policy, terminal layout, streams, errors, and JSON emission. The same serializable result feeds pretty, colorless, and JSON output.
Comfy Table, Anstyle, and Anstream are private rendering engines. Consumer crates compose ctl-core semantic documents and never import terminal engines.
Model
Clap types ──→ App ──→ typed domain result ──┬──→ JSON
└──→ Document ──→ pretty/colorless
Document is a fluent semantic tree: headings, prose, verbatim blocks, fields,
grids, sections, notices, and rules. It carries meaning, not ANSI or table
borders. The renderer chooses style, wrapping, width, and color; verbatim blocks
preserve preformatted Markdown and protocol lines without wrapping, remove
terminal controls, bidi controls, and Unicode line separators, preserve other
Unicode formatting, and normalize trailing newlines to the document contract.
Use
use *;
use Serialize;
Enable app plus usage for that shape. A CLI that already owns -f composes
FormatLong with ColorLong; one that owns -c composes FormatArgs with
ColorLong. Features remain additive and explicit: document has no terminal
engine, render adds terminal layout, view adds JSON emission, help adds
Clap help, app composes the runtime lifecycle, and surface adds the
Clap-derived operator model plus shared MiniJinja fragments.
Operator surface
Surface::new::<Cli>("q") extracts the binary and mounted names, version,
descriptions, visible and hidden command trees, aliases, locally declared
arguments and flags, and mounted Usage KDL from Clap. Global flags stay on their
declaring ancestor, while each nested command exposes them separately through
inherited_arguments. Templates can render effective flags without mixing
provenance into local declarations. Surface::note adds audience-specific prose when
Clap's about belongs to a different reader. The ctl/version.md.jinja,
ctl/invocation.md.jinja, and ctl/commands.md.jinja fragments render repeated
operator text without copying command lists or mise's no--- rule.
Consumer-owned templates keep domain prose. Their tests render through
surface::render and byte-compare the result with committed
skills/<tool>/SKILL.md and src/instructions.md. Verctl can call
surface::add_fragments on its existing MiniJinja environment so Version PRs
render the same fragments.
Contract
- Domain handlers return data. They do not print, inspect terminal state, choose output format, or construct engine tables.
Presentmaps a serializable result to a semanticDocument.Viewserializes the result directly for JSON and renders its document for pretty/colorless output.- JSON always goes to stdout and never contains ANSI.
- Human failures go to stderr. Quiet suppresses successful human output only.
- Help comes from the Clap graph, uses the same document renderer, and wraps long Usage lines within the detected width.
Appruns Usage, pre-parse hooks, help, Clap, execution, and presentation in that order.App::usage_speclets a consumer enrich the mounted Usage document without taking back stream or short-circuit ownership.parser::apply_defaultskeeps-h/--helpand-V/--versionenabled.
Boolean pairs that are domain-specific (--pr / --no-pr) remain in the
consumer. Use Clap overrides_with both ways and warn_opposites; the last
flag wins without silence.
See docs/presentation.md for the architecture and
migration boundary.