Skip to main content

cabin/
lib.rs

1//! Library half of the `cabin` CLI binary.
2//!
3//! The bin (`src/main.rs`) is intentionally a thin shim that
4//! calls [`run`]; the typed parser ([`Cli`]), the command
5//! dispatcher, and the per-command glue modules under `cli/`
6//! live here so integration tests can re-use the same surface
7//! the binary does - `Cli::command()` is the single source of
8//! truth for which subcommands exist and which are hidden.
9
10#![allow(
11    clippy::struct_excessive_bools,
12    clippy::too_many_lines,
13    clippy::unused_self,
14    // Remaining hits are `root_settings: Default::default()` in test
15    // graph fixtures, where the typed-default suggestion is MaybeIncorrect.
16    clippy::default_trait_access
17)]
18
19use std::process::ExitCode;
20
21use clap::FromArgMatches;
22
23pub use crate::cli::Cli;
24use crate::term_setup::{EarlyTerminalState, resolve_early_terminal_state};
25
26// The clap parser stays reachable for this crate's glue modules,
27// but `Cli` is re-exported at the crate root for integration
28// tests and downstream command-tree generation.
29mod cli;
30mod command_list;
31mod completions;
32mod diagnostic_registry;
33mod error_rendering;
34mod help_rendering;
35mod manpages;
36mod port_subcommand;
37mod stamp;
38mod term_setup;
39mod version_info;
40
41/// Return the English plural suffix for a count: empty for one,
42/// `s` for everything else.  Used across the reporter glue files
43/// to keep `"<n> file"` / `"<n> files"` consistent.
44pub(crate) fn plural(n: usize) -> &'static str {
45    if n == 1 { "" } else { "s" }
46}
47
48/// Serialize `value` as pretty-printed JSON and write it to stdout
49/// followed by a newline. `error_context` is attached to any
50/// serialization failure, matching `anyhow::Context::context`.
51pub(crate) fn print_pretty_json<T>(value: &T, error_context: &'static str) -> anyhow::Result<()>
52where
53    T: serde::Serialize + ?Sized,
54{
55    use anyhow::Context;
56    let body = serde_json::to_string_pretty(value).context(error_context)?;
57    println!("{body}");
58    Ok(())
59}
60
61/// One top-level subcommand row: the canonical name first,
62/// followed by each visible alias, paired with the short about
63/// text.  Shared by the `--list` renderer ([`crate::command_list`])
64/// and the `--help` Commands block ([`crate::help_rendering`]);
65/// each keeps its own render loop (color sink vs.  ANSI string,
66/// sorted vs. declaration order, all subcommands vs. visible-only),
67/// but the row extraction and column-width computation are
68/// identical, so they live here.
69#[derive(Debug, Clone)]
70pub(crate) struct SubcommandRow {
71    /// The canonical name first, followed by each visible alias.
72    /// Rendered joined by `, ` to match cargo's `cargo --list` style.
73    pub(crate) tokens: Vec<String>,
74    pub(crate) about: String,
75}
76
77/// Extract the [`SubcommandRow`] for a single clap subcommand: its
78/// name plus any visible aliases, and the first line of its about
79/// block (the short summary clap uses in `--help`).
80pub(crate) fn row_from_subcommand(sub: &clap::Command) -> SubcommandRow {
81    let mut tokens = vec![sub.get_name().to_owned()];
82    for alias in sub.get_visible_aliases() {
83        tokens.push(alias.to_string());
84    }
85    let about = sub
86        .get_about()
87        .map(|s| s.to_string().lines().next().unwrap_or("").trim().to_owned())
88        .unwrap_or_default();
89    SubcommandRow { tokens, about }
90}
91
92/// Display width of the widest row's `<name>[, <alias>…]` cell,
93/// used to align the about column.  Computed from the plain-text
94/// `, ` join so embedded ANSI escapes (which do not advance the
95/// cursor) never inflate the width.
96pub(crate) fn rows_display_width(rows: &[SubcommandRow]) -> usize {
97    rows.iter()
98        .map(|row| row.tokens.join(", ").len())
99        .max()
100        .unwrap_or(0)
101}
102
103/// Run the `cabin` CLI to completion using the given argv
104/// iterator.  Owns parsing, color/verbosity resolution,
105/// dispatch, and top-level error rendering.  The binary
106/// `main` calls this with the process's own arguments.
107pub fn run<I, T>(args: I) -> ExitCode
108where
109    I: IntoIterator<Item = T>,
110    T: Into<std::ffi::OsString> + Clone,
111{
112    // `cabin stamp <FILE> -- <CMD>` is internal build plumbing (the Ninja
113    // syntax-check rule's witness writer); dispatch it before clap so it
114    // never enters the user-facing command surface - no `--help`,
115    // `--list`, completions, or man pages, and no special-case filtering.
116    let arguments: Vec<std::ffi::OsString> = args.into_iter().map(Into::into).collect();
117    if let Some(code) = stamp::dispatch(&arguments) {
118        return code;
119    }
120
121    let cmd = help_rendering::prepare_top_level_command();
122    let matches = match cmd.try_get_matches_from(arguments) {
123        Ok(m) => m,
124        Err(err) => {
125            // `clap::Error` already routes `--help` /
126            // `--version` to stdout and real errors to stderr
127            // with the correct ANSI handling.  Hand it
128            // through.
129            err.exit();
130        }
131    };
132    // `cabin ...` is a help-row affordance that doubles as a
133    // shortcut for `cabin --list`.  The unmapped subcommand
134    // produces `cmd: None` after `from_arg_matches`; promote
135    // it to `list = true` so the downstream dispatcher renders
136    // the listing with the same color-aware code path as the
137    // real flag.
138    let dots_shortcut = matches.subcommand_name() == Some(help_rendering::DOTS_HINT);
139    let mut parsed = match Cli::from_arg_matches(&matches) {
140        Ok(cli) => cli,
141        Err(err) => err.exit(),
142    };
143    if dots_shortcut {
144        parsed.list = true;
145    }
146
147    let EarlyTerminalState { color, reporter } =
148        match resolve_early_terminal_state(parsed.color, parsed.verbose, parsed.quiet) {
149            Ok(state) => state,
150            Err(exit_code) => return exit_code,
151        };
152
153    match cli::run(parsed, reporter, color) {
154        Ok(code) => code,
155        Err(error) => {
156            error_rendering::render_error(&error, color);
157            ExitCode::FAILURE
158        }
159    }
160}