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