Skip to main content

ctl_core/
lib.rs

1//! A framework for command-line programs built on clap. A library, not a
2//! command.
3//!
4//! A program declares its arguments with clap's derive macros and writes a
5//! handler that returns a typed result. ctl-core parses the arguments, prints
6//! help, and writes the result as terminal text or as JSON. It also supplies
7//! the shared flags, colour policy, error output, and exit codes. One
8//! serializable result feeds the pretty, colourless, and JSON output.
9//!
10//! ## Quick start
11//!
12//! The derive macros expand to paths in `clap` and `serde`, so a program
13//! depends on both itself, each with its `derive` feature:
14//!
15//! ```text
16//! cargo add ctl-core
17//! cargo add clap --no-default-features --features std,derive
18//! cargo add serde --features derive
19//! ```
20//!
21//! ```no_run
22//! use ctl_core::prelude::*;
23//! use serde::Serialize;
24//!
25//! #[derive(Parser)]
26//! #[command(version, about = "example")]
27//! struct Cli {
28//!     #[command(flatten)]
29//!     output: OutputArgs,
30//!     #[command(subcommand)]
31//!     command: Command,
32//! }
33//!
34//! #[derive(Subcommand)]
35//! enum Command {
36//!     /// Show status.
37//!     Status,
38//! }
39//!
40//! #[derive(Serialize)]
41//! struct Status {
42//!     pending: usize,
43//! }
44//!
45//! impl Present for Status {
46//!     fn present(&self) -> Document {
47//!         Document::new().fields(Fields::new().row("pending", self.pending.to_string()))
48//!     }
49//! }
50//!
51//! fn main() -> ExitCode {
52//!     App::<Cli>::new("example")
53//!         .view(|cli| cli.output.view())
54//!         .run(|cli| match cli.command {
55//!             Command::Status => Ok(Status { pending: 0 }),
56//!         })
57//! }
58//! ```
59//!
60//! The default `app` feature covers this example.
61//!
62//! ## Where to start
63//!
64//! - [`App`] runs a CLI: it parses argv, calls your handler, and writes the
65//!   result or the error. [`prelude`] is the one ctl-core import a binary
66//!   needs.
67//! - [`Present`] maps a serializable result to a [`Document`], and its
68//!   [`Outcome`] says whether the result succeeded. [`View`] writes it as
69//!   pretty text, colourless text, or JSON.
70//! - [`Document`] builds human output from headings, paragraphs, [`Fields`],
71//!   [`Table`]s, [`Section`]s, and [`Notice`]s. It has no layout in it.
72//! - [`OutputArgs`], [`DryRunArgs`], and [`EnvPrefix`] are the shared flags and
73//!   the environment binding. [`ColorMode`] and [`OutputFormat`] are the values
74//!   the flags parse to. A [`FlagWarning`] reports a [`FlagClash`] between two
75//!   of them, where the last one given wins.
76//! - [`RenderOptions`] and [`Theme`] set how a document renders: the width
77//!   policy, the record and table framing, and the style of each role.
78//! - [`Envelope`] and [`ErrorBody`] are the JSON error shape.
79//! - [`input::Input`] loads a CLI's declared config from YAML or TOML, and
80//!   reports every problem on its file and line.
81//!
82//! ## How output flows
83//!
84//! ```text
85//! Clap types ──→ App ──→ typed domain result ──┬──→ JSON
86//!                                              └──→ Document ──→ pretty/colourless
87//! ```
88//!
89//! - Domain handlers return data. They do not print, read terminal state, or
90//!   choose the output format.
91//! - [`Present`] maps a serializable result to a semantic [`Document`].
92//! - [`View`] serializes the result for JSON and renders its document for
93//!   pretty and colourless output.
94//! - JSON always goes to stdout and never contains ANSI. Human failures go to
95//!   stderr.
96//! - A reader that closes stdout early, such as `head -1`, does not change the
97//!   exit code. A command that succeeded exits 0, and a failure keeps its code.
98//!   [`App::broken_pipe`] can make every closed pipe a failure.
99//!
100//! [`prelude`] is the one ctl-core import a program's `main.rs` needs.
101//!
102//! ## The rendering engine
103//!
104//! A [`Document`] renders through three companion crates, which a CLI can also
105//! use directly through the re-exports [`text`], [`layout`], and
106//! [`term`] (`term` with `view` or `help`):
107//!
108//! - `ctl-text`: styled spans, lines, and text, with widths counted in terminal
109//!   cells, wrapping, and truncation. Text from outside the program loses its
110//!   control characters when a span is built.
111//! - `ctl-layout`: the widgets a document becomes, such as tables, records, and
112//!   rules. They lay out plain lines of styled spans and never see an escape
113//!   sequence.
114//! - `ctl-term`: whether to write colour and how wide to lay out, decided for
115//!   the stream being written. Its `Encoder` is the one place an escape
116//!   sequence is written.
117//!
118//! A [`Theme`] maps each [`Role`] to a style on the way from the document to
119//! the widgets. [`document::Text`] holds roles and [`text::Text`] holds
120//! styles.
121//!
122//! ## Cargo features
123//!
124//! Features are additive. Unused ones do not compile `clap`, `schemars`, or
125//! the rendering crates. `document` builds documents with no engine.
126//! `render` adds `ctl-text`, `ctl-layout`, and `ctl-term`'s encoder, and
127//! renders to a `String` without reading any terminal. `help` and `view` turn
128//! on `ctl-term`'s `detect` feature, which reads the stream's width and
129//! decides colour for it.
130//!
131//! Declared input takes one feature per format. `input-yaml` reads YAML and
132//! `input-toml` reads TOML; neither links the other's parser. `validate`
133//! adds garde validation, and `input` is `input-yaml` with `validate`.
134#![cfg_attr(
135    not(feature = "app"),
136    doc = "",
137    doc = "[`App`]: crate#cargo-features",
138    doc = "[`App::broken_pipe`]: crate#cargo-features"
139)]
140#![cfg_attr(
141    not(feature = "view"),
142    doc = "",
143    doc = "[`Present`]: crate#cargo-features",
144    doc = "[`Outcome`]: crate#cargo-features",
145    doc = "[`View`]: crate#cargo-features"
146)]
147#![cfg_attr(
148    not(feature = "document"),
149    doc = "",
150    doc = "[`Document`]: crate#cargo-features",
151    doc = "[`Fields`]: crate#cargo-features",
152    doc = "[`Table`]: crate#cargo-features",
153    doc = "[`Section`]: crate#cargo-features",
154    doc = "[`Notice`]: crate#cargo-features",
155    doc = "[`Role`]: crate#cargo-features",
156    doc = "[`document::Text`]: crate#cargo-features"
157)]
158#![cfg_attr(
159    not(feature = "cli"),
160    doc = "",
161    doc = "[`OutputArgs`]: crate#cargo-features",
162    doc = "[`DryRunArgs`]: crate#cargo-features",
163    doc = "[`EnvPrefix`]: crate#cargo-features",
164    doc = "[`FlagWarning`]: crate#cargo-features",
165    doc = "[`FlagClash`]: crate#cargo-features"
166)]
167#![cfg_attr(
168    not(feature = "render"),
169    doc = "",
170    doc = "[`RenderOptions`]: crate#cargo-features",
171    doc = "[`Theme`]: crate#cargo-features",
172    doc = "[`text`]: crate#cargo-features",
173    doc = "[`layout`]: crate#cargo-features",
174    doc = "[`text::Text`]: crate#cargo-features"
175)]
176#![cfg_attr(
177    not(any(feature = "view", feature = "help")),
178    doc = "",
179    doc = "[`term`]: crate#cargo-features"
180)]
181#![cfg_attr(
182    not(any(feature = "input-yaml", feature = "input-toml")),
183    doc = "",
184    doc = "[`input::Input`]: crate#cargo-features"
185)]
186#![doc = document_features::document_features!()]
187#![cfg_attr(docsrs, feature(doc_cfg))]
188
189#[cfg(feature = "app")]
190pub mod app;
191pub mod color;
192#[cfg(feature = "completion")]
193pub mod completion;
194#[cfg(feature = "document")]
195pub mod document;
196pub mod format;
197pub mod model;
198
199#[cfg(feature = "fixture")]
200pub mod fixture;
201#[cfg(feature = "cli")]
202pub mod flags;
203#[cfg(feature = "help")]
204pub mod help;
205#[cfg(any(feature = "input-yaml", feature = "input-toml"))]
206pub mod input;
207#[cfg(feature = "cli")]
208pub mod parser;
209pub mod prelude;
210#[cfg(feature = "render")]
211pub mod render;
212#[cfg(feature = "served")]
213pub mod served;
214#[cfg(feature = "shell")]
215pub mod shell;
216#[cfg(feature = "shell-init")]
217pub mod shell_init;
218#[cfg(feature = "surface-model")]
219pub mod surface;
220#[cfg(feature = "render")]
221mod theme;
222#[cfg(feature = "usage")]
223pub mod usage;
224#[cfg(feature = "validate")]
225pub mod validate;
226#[cfg(feature = "render")]
227pub use ctl_layout as layout;
228#[cfg(any(feature = "view", feature = "help"))]
229pub use ctl_term as term;
230#[cfg(feature = "render")]
231pub use ctl_text as text;
232#[cfg(feature = "unsafe-chars")]
233pub use ctl_text::unsafe_chars;
234#[cfg(feature = "view")]
235pub mod view;
236
237#[cfg(feature = "app")]
238#[doc(hidden)]
239pub use app::{App, BrokenPipe};
240#[doc(hidden)]
241pub use color::ColorMode;
242#[cfg(feature = "document")]
243#[doc(hidden)]
244pub use document::Document;
245#[cfg(feature = "document")]
246#[doc(hidden)]
247pub use document::{Block, Fields, Notice, NoticeLevel, Role, Rule, Section, Span, Table, Text};
248#[cfg(feature = "cli")]
249#[doc(hidden)]
250pub use flags::EnvPrefix;
251#[cfg(feature = "cli")]
252#[doc(hidden)]
253pub use flags::{
254    ColorLong, DryRunArgs, FlagClash, FlagWarning, FormatArgs, FormatLong, OutputArgs,
255    chassis_warnings, emit_warnings, resolve_color, switch, warn_opposites,
256};
257#[doc(hidden)]
258pub use format::OutputFormat;
259#[doc(hidden)]
260pub use indoc::{concatdoc, eprintdoc, formatdoc, indoc, printdoc, writedoc};
261#[doc(hidden)]
262pub use model::{Envelope, ErrorBody, SCHEMA_VERSION};
263#[cfg(any(feature = "view", feature = "help"))]
264#[doc(hidden)]
265pub use render::{
266    DEFAULT_COLUMN_BUFFER, DEFAULT_COLUMN_BUFFER_ENV, DEFAULT_COLUMN_BUFFER_ENVS,
267    DEFAULT_MINIMUM_AUTOMATIC_WIDTH,
268};
269#[cfg(feature = "render")]
270#[doc(hidden)]
271pub use render::{
272    DEFAULT_FALLBACK_WIDTH, DEFAULT_LIST_STYLE_ENV, DEFAULT_LIST_STYLE_ENVS,
273    DEFAULT_RECORD_STYLE_ENV, DEFAULT_RECORD_STYLE_ENVS, DEFAULT_ROW_SEPARATION_ENV,
274    DEFAULT_ROW_SEPARATION_ENVS, Frame, RecordStyle, RenderOptions, RowSeparation,
275};
276#[cfg(feature = "surface-model")]
277#[doc(hidden)]
278pub use surface::{Surface, SurfaceArgument, SurfaceCommand, SurfaceRequirement, SurfaceScope};
279#[cfg(feature = "render")]
280pub use theme::Theme;
281#[cfg(feature = "usage")]
282#[doc(hidden)]
283pub use usage::{mount_directives, mount_line, spec, spec_bin, take};
284#[cfg(feature = "view")]
285#[doc(hidden)]
286pub use view::Present;
287#[cfg(feature = "view")]
288#[doc(hidden)]
289pub use view::{
290    Captured, DEFAULT_JSON_LAYOUT_ENV, DEFAULT_JSON_LAYOUT_ENVS, Destination, JsonLayout, Outcome,
291    View,
292};
293
294/// The README's example, compiled as a doctest. It needs the default `app`
295/// feature.
296#[doc = include_str!("../README.md")]
297#[cfg(all(doctest, feature = "app"))]
298pub struct ReadmeDoctests;