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}