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;