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. [`input::FieldPath`] and
81//! [`input::Input::place`] place a problem of the CLI's own the same way.
82//!
83//! ## How output flows
84//!
85//! ```text
86//! Clap types ──→ App ──→ typed domain result ──┬──→ JSON
87//! └──→ Document ──→ pretty/colourless
88//! ```
89//!
90//! - Domain handlers return data. They do not print, read terminal state, or
91//! choose the output format.
92//! - [`Present`] maps a serializable result to a semantic [`Document`].
93//! - [`View`] serializes the result for JSON and renders its document for
94//! pretty and colourless output.
95//! - JSON always goes to stdout and never contains ANSI. Human failures go to
96//! stderr.
97//! - A reader that closes stdout early, such as `head -1`, does not change the
98//! exit code. A command that succeeded exits 0, and a failure keeps its code.
99//! [`App::broken_pipe`] can make every closed pipe a failure.
100//!
101//! [`prelude`] is the one ctl-core import a program's `main.rs` needs.
102//!
103//! ## The rendering engine
104//!
105//! A [`Document`] renders through three companion crates, which a CLI can also
106//! use directly through the re-exports [`text`], [`layout`], and
107//! [`term`] (`term` with `view` or `help`):
108//!
109//! - `ctl-text`: styled spans, lines, and text, with widths counted in terminal
110//! cells, wrapping, and truncation. Text from outside the program loses its
111//! control characters when a span is built.
112//! - `ctl-layout`: the widgets a document becomes, such as tables, records, and
113//! rules. They lay out plain lines of styled spans and never see an escape
114//! sequence.
115//! - `ctl-term`: whether to write colour and how wide to lay out, decided for
116//! the stream being written. Its `Encoder` is the one place an escape
117//! sequence is written.
118//!
119//! A [`Theme`] maps each [`Role`] to a style on the way from the document to
120//! the widgets. [`document::Text`] holds roles and [`text::Text`] holds
121//! styles.
122//!
123//! ## Cargo features
124//!
125//! Features are additive. Unused ones do not compile `clap`, `schemars`, or
126//! the rendering crates. `document` builds documents with no engine.
127//! `render` adds `ctl-text`, `ctl-layout`, and `ctl-term`'s encoder, and
128//! renders to a `String` without reading any terminal. `help` and `view` turn
129//! on `ctl-term`'s `detect` feature, which reads the stream's width and
130//! decides colour for it.
131//!
132//! Declared input takes one feature per format. `input-yaml` reads YAML and
133//! `input-toml` reads TOML; neither links the other's parser. `validate`
134//! adds garde validation, and `input` is `input-yaml` with `validate`.
135#![cfg_attr(
136 not(feature = "app"),
137 doc = "",
138 doc = "[`App`]: crate#cargo-features",
139 doc = "[`App::broken_pipe`]: crate#cargo-features"
140)]
141#![cfg_attr(
142 not(feature = "view"),
143 doc = "",
144 doc = "[`Present`]: crate#cargo-features",
145 doc = "[`Outcome`]: crate#cargo-features",
146 doc = "[`View`]: crate#cargo-features"
147)]
148#![cfg_attr(
149 not(feature = "document"),
150 doc = "",
151 doc = "[`Document`]: crate#cargo-features",
152 doc = "[`Fields`]: crate#cargo-features",
153 doc = "[`Table`]: crate#cargo-features",
154 doc = "[`Section`]: crate#cargo-features",
155 doc = "[`Notice`]: crate#cargo-features",
156 doc = "[`Role`]: crate#cargo-features",
157 doc = "[`document::Text`]: crate#cargo-features"
158)]
159#![cfg_attr(
160 not(feature = "cli"),
161 doc = "",
162 doc = "[`OutputArgs`]: crate#cargo-features",
163 doc = "[`DryRunArgs`]: crate#cargo-features",
164 doc = "[`EnvPrefix`]: crate#cargo-features",
165 doc = "[`FlagWarning`]: crate#cargo-features",
166 doc = "[`FlagClash`]: crate#cargo-features"
167)]
168#![cfg_attr(
169 not(feature = "render"),
170 doc = "",
171 doc = "[`RenderOptions`]: crate#cargo-features",
172 doc = "[`Theme`]: crate#cargo-features",
173 doc = "[`text`]: crate#cargo-features",
174 doc = "[`layout`]: crate#cargo-features",
175 doc = "[`text::Text`]: crate#cargo-features"
176)]
177#![cfg_attr(
178 not(any(feature = "view", feature = "help")),
179 doc = "",
180 doc = "[`term`]: crate#cargo-features"
181)]
182#![cfg_attr(
183 not(any(feature = "input-yaml", feature = "input-toml")),
184 doc = "",
185 doc = "[`input::Input`]: crate#cargo-features",
186 doc = "[`input::FieldPath`]: crate#cargo-features",
187 doc = "[`input::Input::place`]: crate#cargo-features"
188)]
189#![doc = document_features::document_features!()]
190#![cfg_attr(docsrs, feature(doc_cfg))]
191
192#[cfg(feature = "app")]
193pub mod app;
194pub mod color;
195#[cfg(feature = "completion")]
196pub mod completion;
197#[cfg(feature = "document")]
198pub mod document;
199pub mod format;
200pub mod model;
201
202#[cfg(feature = "fixture")]
203pub mod fixture;
204#[cfg(feature = "cli")]
205pub mod flags;
206#[cfg(feature = "help")]
207pub mod help;
208#[cfg(any(feature = "input-yaml", feature = "input-toml"))]
209pub mod input;
210#[cfg(feature = "cli")]
211pub mod parser;
212pub mod prelude;
213#[cfg(feature = "render")]
214pub mod render;
215#[cfg(feature = "served")]
216pub mod served;
217#[cfg(feature = "shell")]
218pub mod shell;
219#[cfg(feature = "shell-init")]
220pub mod shell_init;
221#[cfg(feature = "surface-model")]
222pub mod surface;
223#[cfg(feature = "render")]
224mod theme;
225#[cfg(feature = "usage")]
226pub mod usage;
227#[cfg(feature = "validate")]
228pub mod validate;
229#[cfg(feature = "render")]
230pub use ctl_layout as layout;
231#[cfg(any(feature = "view", feature = "help"))]
232pub use ctl_term as term;
233#[cfg(feature = "render")]
234pub use ctl_text as text;
235#[cfg(feature = "unsafe-chars")]
236pub use ctl_text::unsafe_chars;
237#[cfg(feature = "view")]
238pub mod view;
239
240#[cfg(feature = "app")]
241#[doc(hidden)]
242pub use app::{App, BrokenPipe};
243#[doc(hidden)]
244pub use color::ColorMode;
245#[cfg(feature = "document")]
246#[doc(hidden)]
247pub use document::Document;
248#[cfg(feature = "document")]
249#[doc(hidden)]
250pub use document::{Block, Fields, Notice, NoticeLevel, Role, Rule, Section, Span, Table, Text};
251#[cfg(feature = "cli")]
252#[doc(hidden)]
253pub use flags::EnvPrefix;
254#[cfg(feature = "cli")]
255#[doc(hidden)]
256pub use flags::{
257 ColorLong, DryRunArgs, FlagClash, FlagWarning, FormatArgs, FormatLong, OutputArgs,
258 chassis_warnings, emit_warnings, resolve_color, switch, warn_opposites,
259};
260#[doc(hidden)]
261pub use format::OutputFormat;
262#[doc(hidden)]
263pub use indoc::{concatdoc, eprintdoc, formatdoc, indoc, printdoc, writedoc};
264#[doc(hidden)]
265pub use model::{Envelope, ErrorBody, SCHEMA_VERSION};
266#[cfg(any(feature = "view", feature = "help"))]
267#[doc(hidden)]
268pub use render::{
269 DEFAULT_COLUMN_BUFFER, DEFAULT_COLUMN_BUFFER_ENV, DEFAULT_COLUMN_BUFFER_ENVS,
270 DEFAULT_MINIMUM_AUTOMATIC_WIDTH,
271};
272#[cfg(feature = "render")]
273#[doc(hidden)]
274pub use render::{
275 DEFAULT_FALLBACK_WIDTH, DEFAULT_LIST_STYLE_ENV, DEFAULT_LIST_STYLE_ENVS,
276 DEFAULT_RECORD_STYLE_ENV, DEFAULT_RECORD_STYLE_ENVS, DEFAULT_ROW_SEPARATION_ENV,
277 DEFAULT_ROW_SEPARATION_ENVS, Frame, RecordStyle, RenderOptions, RowSeparation,
278};
279#[cfg(feature = "surface-model")]
280#[doc(hidden)]
281pub use surface::{Surface, SurfaceArgument, SurfaceCommand, SurfaceRequirement, SurfaceScope};
282#[cfg(feature = "render")]
283pub use theme::Theme;
284#[cfg(feature = "usage")]
285#[doc(hidden)]
286pub use usage::{mount_directives, mount_line, spec, spec_bin, take};
287#[cfg(feature = "view")]
288#[doc(hidden)]
289pub use view::Present;
290#[cfg(feature = "view")]
291#[doc(hidden)]
292pub use view::{
293 Captured, DEFAULT_JSON_LAYOUT_ENV, DEFAULT_JSON_LAYOUT_ENVS, Destination, JsonLayout, Outcome,
294 View,
295};
296
297/// The README's example, compiled as a doctest. It needs the default `app`
298/// feature.
299#[doc = include_str!("../README.md")]
300#[cfg(all(doctest, feature = "app"))]
301pub struct ReadmeDoctests;