modcli/
lib.rs

1//! ModCLI — a lightweight, modular CLI framework for Rust.
2//!
3//! # Quick Start
4//! ```no_run
5//! use modcli::ModCli;
6//! let args: Vec<String> = std::env::args().skip(1).collect();
7//! let mut cli = ModCli::new();
8//! cli.run(args);
9//! ```
10//!
11//! # Features
12//! - Custom commands via the `Command` trait
13//! - Styled output, gradients, progress, tables
14//! - Optional internal helper commands
15//!
16//! Note: Runtime plugins and JSON/config loaders have been removed from core for
17//! security and performance. Configure your CLI directly in code.
18
19use std::sync::{
20    atomic::{AtomicBool, Ordering},
21    OnceLock,
22};
23
24pub mod command;
25pub mod error;
26pub mod input;
27pub mod loader;
28pub mod output;
29pub mod parser;
30
31pub use crate::command::Command as CliCustom;
32use crate::loader::CommandRegistry;
33
34#[cfg(feature = "internal-commands")]
35pub mod commands;
36
37#[cfg(feature = "custom-commands")]
38pub mod custom;
39
40/// Represents a CLI application and provides command registration and dispatch.
41///
42/// Typical usage:
43/// ```no_run
44/// use modcli::ModCli;
45/// let args: Vec<String> = std::env::args().skip(1).collect();
46/// let mut cli = ModCli::new();
47/// cli.run(args);
48/// ```
49pub struct ModCli {
50    pub registry: CommandRegistry,
51}
52
53/// Registers a startup banner from a UTF-8 text file. The contents are read immediately
54/// and stored; at runtime the stored text is printed when the banner runs.
55/// Returns Err if the file cannot be read.
56pub fn set_startup_banner_from_file(path: &str) -> Result<(), String> {
57    let data = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
58    let owned = data.clone();
59    set_startup_banner(move || {
60        println!("{owned}\n");
61    });
62    Ok(())
63}
64
65// --- Macros ------------------------------------------------------------------
66
67/// Register a simple text banner that prints a single line and a newline.
68#[macro_export]
69macro_rules! banner_text {
70    ($text:expr) => {{
71        $crate::set_startup_banner(|| {
72            $crate::output::print::line($text);
73            println!();
74        });
75    }};
76}
77
78/// Register a banner from a file path (evaluated at runtime).
79#[macro_export]
80macro_rules! banner_file {
81    ($path:expr) => {{
82        let _ = $crate::set_startup_banner_from_file($path);
83    }};
84}
85
86/// Register a banner with custom code using a block. Example:
87/// banner!({ println!("Hello"); })
88#[macro_export]
89macro_rules! banner {
90    ($body:block) => {{
91        $crate::set_startup_banner(|| $body);
92    }};
93}
94
95impl Default for ModCli {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101impl ModCli {
102    /// Creates a new ModCli instance.
103    ///
104    /// # Example
105    /// ```
106    /// use modcli::ModCli;
107    /// let cli = ModCli::new();
108    /// ```
109    ///
110    /// # Arguments
111    /// * `args` - A vector of command-line arguments
112    ///
113    /// # Returns
114    /// A new instance of `ModCli`
115    pub fn new() -> Self {
116        Self {
117            registry: CommandRegistry::new(),
118        }
119    }
120
121    /// Sets the command prefix used for prefix routing (e.g., `tool:hello`).
122    pub fn set_prefix(&mut self, prefix: &str) {
123        self.registry.set_prefix(prefix);
124    }
125
126    /// Gets the current command prefix.
127    pub fn get_prefix(&self) -> &str {
128        self.registry.get_prefix()
129    }
130
131    /// Runs the CLI by dispatching the first arg as the command and the rest as arguments.
132    /// Prints an error if no command is provided.
133    pub fn run(&mut self, args: Vec<String>) {
134        run_startup_banner_if_enabled();
135        if args.is_empty() {
136            crate::output::hook::status("No command provided. Try `help`.");
137            return;
138        }
139
140        let command = &args[0];
141        let rest = &args[1..];
142
143        self.registry.execute(command, rest);
144    }
145}
146
147/// Returns the version of the ModCLI framework (from `modcli/Cargo.toml`).
148///
149/// Useful for surfacing framework version from applications.
150pub fn modcli_version() -> &'static str {
151    env!("CARGO_PKG_VERSION")
152}
153
154// --- Startup banner hook -----------------------------------------------------
155
156static STARTUP_BANNER: OnceLock<Box<dyn Fn() + Send + Sync>> = OnceLock::new();
157static BANNER_RAN: AtomicBool = AtomicBool::new(false);
158
159/// Registers a startup banner callback that will be invoked once, the first time
160/// `ModCli::run()` is called in this process. If the environment variable
161/// `MODCLI_DISABLE_BANNER` is set to "1" or "true" (case-insensitive), the
162/// banner will be suppressed.
163///
164/// Note: This can only be set once per process.
165pub fn set_startup_banner<F>(f: F)
166where
167    F: Fn() + Send + Sync + 'static,
168{
169    let _ = STARTUP_BANNER.set(Box::new(f));
170}
171
172fn run_startup_banner_if_enabled() {
173    // Ensure one-time run per process
174    if BANNER_RAN.swap(true, Ordering::SeqCst) {
175        return;
176    }
177    // Allow disabling via env var
178    if let Ok(val) = std::env::var("MODCLI_DISABLE_BANNER") {
179        if val == "1" || val.eq_ignore_ascii_case("true") {
180            return;
181        }
182    }
183    if let Some(cb) = STARTUP_BANNER.get() {
184        cb();
185    }
186}