mod completion;
mod config;
mod docs;
mod error;
mod help;
mod precedence;
mod spec;
#[cfg(feature = "clap")]
pub mod clap;
pub use completion::{generate, CompletionCatalog, CompletionItem, CompletionKind, Shell};
pub use config::{ConfigEntry, ConfigReference, ConfigSource};
#[cfg(feature = "markdown")]
pub use docs::markdown_view;
pub use docs::{to_man, to_man_pages, to_markdown};
pub use error::{suggest, CliError, CliErrorKind};
pub use help::{HelpView, STACK_BELOW};
pub use precedence::{Explanation, Layer, Precedence, PrecedenceView, Resolved};
pub use spec::{ArgSpec, Choice, CommandSpec, Example, HeadingNote, Section, ValueHint};
use rich::{Console, Segment, Style, Text};
pub const STYLES: &[(&str, &str)] = &[
("help.usage", "bold"),
("help.heading", "bold yellow"),
("help.option", "bold cyan"),
("help.metavar", "cyan"),
("help.command", "bold green"),
("help.hint", "dim"),
("help.example", "green"),
("help.description", "none"),
("config.key", "bold"),
("config.winner", "bold green"),
("config.shadowed", "dim strike"),
("config.origin", "dim"),
];
pub(crate) fn style(console: &Console, key: &str) -> Style {
let fallback = STYLES
.iter()
.find(|(name, _)| *name == key)
.map_or("none", |(_, spec)| spec);
crate::event::theme_style(console, key, fallback)
}
pub(crate) fn paragraphs(text: &str) -> Vec<String> {
let mut out = Vec::new();
let mut current: Vec<&str> = Vec::new();
for line in text.lines() {
if line.trim().is_empty() {
if !current.is_empty() {
out.push(current.join("\n"));
current.clear();
}
} else {
current.push(line.trim_end());
}
}
if !current.is_empty() {
out.push(current.join("\n"));
}
out
}
pub(crate) fn wrap_indented(
console: &Console,
text: &Text,
indent: usize,
width: usize,
) -> Vec<Vec<Segment>> {
let inner = width.saturating_sub(indent).max(1);
text.render_lines(console.theme(), &Style::new(), Some(inner))
.into_iter()
.map(|line| {
let mut row = Vec::with_capacity(line.len() + 1);
if indent > 0 {
row.push(Segment::new(" ".repeat(indent), None));
}
row.extend(line);
row
})
.collect()
}
pub(crate) fn join_lines(lines: Vec<Vec<Segment>>) -> Vec<Segment> {
let trimmed = lines.into_iter().map(|line| {
let mut line = Segment::simplify(&line);
while let Some(last) = line.last_mut() {
if last.control || last.style.as_ref().is_some_and(|s| s.bgcolor().is_some()) {
break;
}
let kept = last.text.trim_end_matches(' ').len();
last.text.truncate(kept);
if !last.text.is_empty() {
break;
}
line.pop();
}
line
});
crate::event::flatten(trimmed.collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_default_style_parses() {
for (name, spec) in STYLES {
assert!(Style::parse(spec).is_ok(), "{name}: {spec}");
}
}
#[test]
fn paragraphs_split_on_blank_lines() {
assert_eq!(
paragraphs("a\nb\n\n \nc\n"),
vec!["a\nb".to_string(), "c".to_string()]
);
}
}