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. [`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;