Expand description
A framework for command-line programs built on clap. A library, not a command.
A program declares its arguments with clap’s derive macros and writes a handler that returns a typed result. ctl-core parses the arguments, prints help, and writes the result as terminal text or as JSON. It also supplies the shared flags, colour policy, error output, and exit codes. One serializable result feeds the pretty, colourless, and JSON output.
§Quick start
The derive macros expand to paths in clap and serde, so a program
depends on both itself, each with its derive feature:
cargo add ctl-core
cargo add clap --no-default-features --features std,derive
cargo add serde --features deriveuse ctl_core::prelude::*;
use serde::Serialize;
#[derive(Parser)]
#[command(version, about = "example")]
struct Cli {
#[command(flatten)]
output: OutputArgs,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Show status.
Status,
}
#[derive(Serialize)]
struct Status {
pending: usize,
}
impl Present for Status {
fn present(&self) -> Document {
Document::new().fields(Fields::new().row("pending", self.pending.to_string()))
}
}
fn main() -> ExitCode {
App::<Cli>::new("example")
.view(|cli| cli.output.view())
.run(|cli| match cli.command {
Command::Status => Ok(Status { pending: 0 }),
})
}The default app feature covers this example.
§Where to start
Appruns a CLI: it parses argv, calls your handler, and writes the result or the error.preludeis the one ctl-core import a binary needs.Presentmaps a serializable result to aDocument, and itsOutcomesays whether the result succeeded.Viewwrites it as pretty text, colourless text, or JSON.Documentbuilds human output from headings, paragraphs,Fields,Tables,Sections, andNotices. It has no layout in it.OutputArgs,DryRunArgs, andEnvPrefixare the shared flags and the environment binding.ColorModeandOutputFormatare the values the flags parse to. AFlagWarningreports aFlagClashbetween two of them, where the last one given wins.RenderOptionsandThemeset how a document renders: the width policy, the record and table framing, and the style of each role.EnvelopeandErrorBodyare the JSON error shape.input::Inputloads a CLI’s declared config from YAML or TOML, and reports every problem on its file and line.input::FieldPathandinput::Input::placeplace a problem of the CLI’s own the same way.
§How output flows
Clap types ──→ App ──→ typed domain result ──┬──→ JSON
└──→ Document ──→ pretty/colourless- Domain handlers return data. They do not print, read terminal state, or choose the output format.
Presentmaps a serializable result to a semanticDocument.Viewserializes the result for JSON and renders its document for pretty and colourless output.- JSON always goes to stdout and never contains ANSI. Human failures go to stderr.
- A reader that closes stdout early, such as
head -1, does not change the exit code. A command that succeeded exits 0, and a failure keeps its code.App::broken_pipecan make every closed pipe a failure.
prelude is the one ctl-core import a program’s main.rs needs.
§The rendering engine
A Document renders through three companion crates, which a CLI can also
use directly through the re-exports text, layout, and
term (term with view or help):
ctl-text: styled spans, lines, and text, with widths counted in terminal cells, wrapping, and truncation. Text from outside the program loses its control characters when a span is built.ctl-layout: the widgets a document becomes, such as tables, records, and rules. They lay out plain lines of styled spans and never see an escape sequence.ctl-term: whether to write colour and how wide to lay out, decided for the stream being written. ItsEncoderis the one place an escape sequence is written.
A Theme maps each Role to a style on the way from the document to
the widgets. document::Text holds roles and text::Text holds
styles.
§Cargo features
Features are additive. Unused ones do not compile clap, schemars, or
the rendering crates. document builds documents with no engine.
render adds ctl-text, ctl-layout, and ctl-term’s encoder, and
renders to a String without reading any terminal. help and view turn
on ctl-term’s detect feature, which reads the stream’s width and
decides colour for it.
Declared input takes one feature per format. input-yaml reads YAML and
input-toml reads TOML; neither links the other’s parser. validate
adds garde validation, and input is input-yaml with validate.
app(enabled by default) — Complete typed CLI lifecycle over the shared help and view layers.cli(enabled by default) — Clap derive helpers (OutputArgs,DryRunArgs) and parser defaults. Turns on only the Clap features ctl-core calls:std,derive,env, andstring. ctl-core declares-h/--helpand thehelpsubcommand itself and renders the help, so Clap’shelpfeature stays off. A plainCli::parse()outside ctl-core’s entry point gets Clap’s minimal output and nohelpsubcommand. Clap’s ownerror-context,suggestions,help, andcolorfeatures stay the CLI’s choice.document(enabled by default) — Semantic presentation documents without a terminal engine.json(enabled by default) — Serde models and the JSON view.render(enabled by default) — Rendering semantic documents to aStringthroughctl-textandctl-layout, with a replaceableTheme, written by thectl-termencoder. No terminal is asked: the width is explicit or the fallback, and nothing is detected.unsafe-chars(enabled by default) — Which characters are unsafe to print as they are (unsafe_chars::is_unsafe), re-exported fromctl-text.schema— JSON Schema for the envelope and enums (schemars).help(enabled by default) — Styled-h/--helpthrough the shared document renderer. Adds Clap’susagefeature for the usage line, andctl-term’sdetectfeature, which decides colour and width for the stream help goes to.view(enabled by default) — Pretty, colourless, and JSON output from serializable results. Addsctl-term’sdetectfeature, which decides colour and width for the stream each result goes to.surface-model— A CLI’s commands and flags as data (surface::Surface), without serialization or a template engine.surface-serde—Surfacewith serialization, without a template engine.surface-templates— Shared MiniJinja fragments for generated documentation files.surface— The same assurface-templates.served— Files a CLI generates into a repository from templates, each template declaring its own target path and mode (served::render,Served::write).validate— Garde rules every declared schema shares (validate).input— Declared input from YAML, validated once with garde:input-yamlandvalidate.input-yaml— Declared input from YAML: parse with serde-saphyr and place every problem on its file and line through yamled. Adds the YAML-onlyLimitsfields and the repository’s.ctl/input.yaml. Needs no garde withoutvalidate.input-toml— Declared input from TOML:Input::openreads.tomlfiles, and thetomlcrate’s span-carrying tree places each problem on its line and column. Needs no YAML crate, and no garde withoutvalidate.fixture— Temporary file trees for integration tests:Suite, its scenarios, and one call per file.shell— The shells a CLI writes scripts for (Shell): bash, zsh, and fish.completion— Thecompletion <SHELL>verb and theCOMPLETErequest it registers (CompletionCommand,App::completion), throughclap_complete’s dynamic engine. Enablesapp, whichApp::completionlives on.shell-init— Theinit <SHELL>verb: a script that runs one command of the CLI once, before the shell’s first prompt (InitCommand,Hook).usage— Mise Usage spec from clap (--usage-specand the#USAGE mountline). Adds Clap’susagefeature, which renders the spec’s usage line.
Re-exports§
pub use ctl_layout as layout;renderpub use ctl_term as term;helporviewpub use ctl_text as text;render
Modules§
- app
app - The typed CLI lifecycle:
Appparses argv, runs a handler, and writes the result through aView. - color
- The colour policy for pretty output:
auto,always, ornever. - completion
completion - Shell completion: one
completion <SHELL>verb for a CLI. - document
document - The semantic document a command’s human output is built from.
- fixture
fixture - Files for integration tests, written through one call instead of
fs::create_dir_all,fs::write, and anexpectper file. - flags
cli - The flags every CLI built on ctl-core shares, as Clap structs to flatten into a parser.
- format
- The output representation:
prettyfor people, orjsonfor machines. - help
help - Styled help extracted from Clap and rendered as a semantic document.
- input
input-tomlorinput-yaml - Declared input: read a file, parse it, validate it once, and report every problem on its file and line.
- model
- The JSON envelope types. A view serializes a result as is. The envelope
wraps a success or a failure for a reader that wants a status and a schema
version:
Envelope::Okholds a result, andEnvelope::Errholds anErrorBody. - parser
cli - The parser rules every CLI built on ctl-core keeps:
-h/--help, thehelpsubcommand, and-V/--versionstay on, and a repeated flag takes its last value. - prelude
- One import for a program’s
main.rs. - render
render - Render semantic documents through the ctl-layout widgets.
- served
served - Files a CLI generates into a repository from templates, each template declaring its own target path and mode.
- shell
shell - The shells a CLI writes scripts for, and the script a shell verb prints.
- shell_
init shell-init - Shell integration: one
init <SHELL>verb that hooks a CLI into the user’s shell. - surface
surface-model - A CLI’s commands and flags as plain data, taken from its Clap types.
- unsafe_
chars unsafe-chars - Which characters are unsafe to print to a terminal as they are, and the function that removes them.
- usage
usage - Mise Usage spec from a clap
Command. - validate
validate - Garde rules every declared schema shares. A repository’s config and a template’s own exports are held to the same rules and report the same messages.
- view
view - Pretty, colourless, and JSON output from one typed model.
Structs§
- Theme
render - Maps each
Role, and the label of eachNoticeLevel, to ananstyle::Style.