dynamic_cli/interface/mod.rs
1//! User interface module
2//!
3//! This module provides two main interfaces for interacting with the CLI framework:
4//!
5//! - [`CliInterface`]: One-shot command execution from command-line arguments
6//! - [`ReplInterface`]: Interactive REPL (Read-Eval-Print Loop) with history
7//!
8//! # Overview
9//!
10//! The `interface` module is the user-facing layer of the framework. It handles:
11//! - Parsing user input (CLI args or REPL lines)
12//! - Executing commands through the registry
13//! - Displaying results and errors
14//! - Managing command history (REPL only)
15//!
16//! # Choosing an Interface
17//!
18//! ## CLI Interface
19//!
20//! Use [`CliInterface`] when:
21//! - Running single commands from scripts
22//! - Building traditional CLI tools
23//! - No interaction is needed
24//! - Each invocation is independent
25//!
26//! ```no_run
27//! use dynamic_cli::interface::CliInterface;
28//! use dynamic_cli::prelude::*;
29//!
30//! # #[derive(Default)]
31//! # struct MyContext;
32//! # impl ExecutionContext for MyContext {
33//! # fn as_any(&self) -> &dyn std::any::Any { self }
34//! # fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
35//! # }
36//! # fn main() -> dynamic_cli::Result<()> {
37//! let registry = CommandRegistry::new();
38//! let context = Box::new(MyContext::default());
39//!
40//! let cli = CliInterface::new(registry, context);
41//! cli.run(std::env::args().skip(1).collect())?;
42//! # Ok(())
43//! # }
44//! ```
45//!
46//! ## REPL Interface
47//!
48//! Use [`ReplInterface`] when:
49//! - Building interactive tools
50//! - Users need to run multiple commands
51//! - Context/state is preserved between commands
52//! - Command history and line editing are desired
53//!
54//! ```no_run
55//! use dynamic_cli::interface::ReplInterface;
56//! use dynamic_cli::prelude::*;
57//!
58//! # #[derive(Default)]
59//! # struct MyContext;
60//! # impl ExecutionContext for MyContext {
61//! # fn as_any(&self) -> &dyn std::any::Any { self }
62//! # fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
63//! # }
64//! # fn main() -> dynamic_cli::Result<()> {
65//! let registry = CommandRegistry::new();
66//! let context = Box::new(MyContext::default());
67//!
68//! let repl = ReplInterface::new(registry, context, "myapp".to_string(), None, None)?;
69//! repl.run()?; // Enters interactive loop
70//! # Ok(())
71//! # }
72//! ```
73//!
74//! # Architecture
75//!
76//! Both interfaces follow the same flow:
77//!
78//! ```text
79//! User Input → Parser → Validator → Executor → Handler
80//! ↓
81//! ExecutionContext
82//! ```
83//!
84//! **Key differences**:
85//!
86//! | Aspect | CLI | REPL |
87//! |--------|-----|------|
88//! | Input | Command-line args | Interactive lines |
89//! | Parser | [`CliParser`] | [`ReplParser`] |
90//! | History | None | Written to disk by `:quit` and Ctrl-D |
91//! | Errors | Exit process | Display and continue |
92//! | Lifecycle | One command, exit | Loop until `:quit`, `:exit` or Ctrl-D |
93//!
94//! # Error Handling
95//!
96//! ## CLI Interface
97//!
98//! Errors cause the process to exit with specific codes:
99//! - `0`: Success
100//! - `1`: Execution error
101//! - `2`: Parse/validation error
102//! - `3`: Other errors
103//!
104//! ## REPL Interface
105//!
106//! Errors are displayed but the REPL continues:
107//! - Parse errors → show suggestions, continue
108//! - Validation errors → explain issue, continue
109//! - Execution errors → display error, continue
110//! - Directive errors → list the available directives (unknown directive)
111//! or show the expected usage (wrong arguments), continue
112//! - Input failure (the terminal cannot be read) → reported, the REPL exits
113//! without writing the history
114//!
115//! # REPL Directives
116//!
117//! In the REPL, a line starting with `:` is a framework directive; any
118//! other line is an application command. The two never compete for a name:
119//! `:quit` is always the directive, `quit` is always looked up among the
120//! application's commands.
121//!
122//! | Directive | Aliases | Action |
123//! |--------------------|------------|---------------------------------------------|
124//! | `:help [command]` | `:h`, `:?` | Application help, or the help of `command` |
125//! | `:load <path>` | | Run every line of a script file |
126//! | `:quit` | `:q` | Write the session history, then leave |
127//! | `:exit` | | Leave without writing the session history |
128//!
129//! These are the default names. An application can rename a directive,
130//! replace its aliases or its description through the `directives:` section
131//! of its configuration, for instance to translate them; it can neither add
132//! nor disable one. See [`crate::config::directive`].
133//!
134//! Leaving the REPL:
135//!
136//! - `:quit` and end of input (Ctrl-D) write the session history, then
137//! leave.
138//! - `:exit` leaves without writing it. Any other termination (a handler
139//! calling `std::process::exit`, a panic, the process being killed) also
140//! leaves the history file as it was when the session started.
141//! - `exit` or `quit` typed without `:` is an ordinary command name. When
142//! the application has no command of that name, the error suggests the
143//! matching directive (`:quit`).
144//!
145//! Inside a `:load` script, `:quit` and `:exit` are reported and skipped;
146//! the other lines run.
147//!
148//! `:help` lists the directives after the application help, under their
149//! effective names (see
150//! [`HelpFormatter::format_directives`](crate::help::HelpFormatter::format_directives)).
151//! Tab completion after a leading `:` offers the directive names and
152//! aliases. `--help` and `-h` work in the REPL as in CLI mode.
153//!
154//! # Examples
155//!
156//! ## Complete CLI Application
157//!
158//! ```no_run
159//! use dynamic_cli::prelude::*;
160//! use dynamic_cli::config::loader::load_config;
161//! use dynamic_cli::interface::CliInterface;
162//!
163//! // Define context
164//! #[derive(Default)]
165//! struct AppContext {
166//! data: Vec<String>,
167//! }
168//!
169//! impl ExecutionContext for AppContext {
170//! fn as_any(&self) -> &dyn std::any::Any { self }
171//! fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
172//! }
173//!
174//! // Define handler
175//! struct AddCommand;
176//!
177//! impl CommandHandler for AddCommand {
178//! fn execute(
179//! &self,
180//! context: &mut dyn ExecutionContext,
181//! args: &ParsedArgs,
182//! ) -> dynamic_cli::Result<()> {
183//! let ctx = dynamic_cli::context::downcast_mut::<AppContext>(context).unwrap();
184//! let item = args.get_scalar("item").unwrap();
185//! ctx.data.push(item.to_string());
186//! println!("Added: {}", item);
187//! Ok(())
188//! }
189//! }
190//!
191//! fn main() -> dynamic_cli::Result<()> {
192//! // Load configuration
193//! let config = load_config("commands.yaml")?;
194//!
195//! // Build registry
196//! let mut registry = CommandRegistry::new();
197//! registry.register_sync(
198//! config.commands[0].clone(),
199//! Box::new(AddCommand),
200//! )?;
201//!
202//! // Create and run CLI
203//! let context = Box::new(AppContext::default());
204//! let cli = CliInterface::new(registry, context);
205//! cli.run(std::env::args().skip(1).collect())
206//! }
207//! ```
208//!
209//! ## Complete REPL Application
210//!
211//! ```no_run
212//! use dynamic_cli::prelude::*;
213//! use dynamic_cli::config::loader::load_config;
214//! use dynamic_cli::interface::ReplInterface;
215//!
216//! // Same context and handler as above
217//! # #[derive(Default)]
218//! # struct AppContext { data: Vec<String> }
219//! # impl ExecutionContext for AppContext {
220//! # fn as_any(&self) -> &dyn std::any::Any { self }
221//! # fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
222//! # }
223//! # struct AddCommand;
224//! # impl CommandHandler for AddCommand {
225//! # fn execute(&self, context: &mut dyn ExecutionContext, args: &ParsedArgs) -> dynamic_cli::Result<()> {
226//! # let ctx = dynamic_cli::context::downcast_mut::<AppContext>(context).unwrap();
227//! # let item = args.get_scalar("item").unwrap();
228//! # ctx.data.push(item.to_string());
229//! # println!("Added: {}", item);
230//! # Ok(())
231//! # }
232//! # }
233//!
234//! fn main() -> dynamic_cli::Result<()> {
235//! // Load configuration
236//! let config = load_config("commands.yaml")?;
237//!
238//! // Build registry
239//! let mut registry = CommandRegistry::new();
240//! registry.register_sync(
241//! config.commands[0].clone(),
242//! Box::new(AddCommand),
243//! )?;
244//!
245//! // Create and run REPL
246//! let context = Box::new(AppContext::default());
247//! let repl = ReplInterface::new(registry, context, "myapp".to_string(), None, None)?;
248//! repl.run() // Interactive loop
249//! }
250//! ```
251//!
252//! # Module Structure
253//!
254//! - [`cli`]: CLI interface implementation
255//! - [`repl`]: REPL interface implementation
256//!
257//! [`CliParser`]: crate::parser::CliParser
258//! [`ReplParser`]: crate::parser::ReplParser
259
260pub mod cli;
261pub mod repl;
262
263// Re-export main types for convenience
264pub use cli::{CliInterface, ScriptErrorPolicy, ScriptOutcome};
265pub use repl::ReplInterface;
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use crate::config::schema::CommandDefinition;
271 use crate::prelude::*;
272
273 // Test context
274 struct TestContext;
275
276 impl ExecutionContext for TestContext {
277 fn as_any(&self) -> &dyn std::any::Any {
278 self
279 }
280
281 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
282 self
283 }
284 }
285
286 // Test handler
287 struct TestHandler;
288
289 impl CommandHandler for TestHandler {
290 fn execute(
291 &self,
292 _context: &mut dyn ExecutionContext,
293 _args: &ParsedArgs,
294 ) -> crate::Result<()> {
295 Ok(())
296 }
297 }
298
299 #[test]
300 fn test_module_imports() {
301 // Verify that types are re-exported
302 let _: Option<CliInterface> = None;
303 let _: Option<ReplInterface> = None;
304 }
305
306 #[test]
307 fn test_cli_interface_accessible() {
308 let mut registry = CommandRegistry::new();
309
310 let cmd_def = CommandDefinition {
311 name: "test".to_string(),
312 aliases: vec![],
313 description: "Test".to_string(),
314 required: false,
315 arguments: vec![],
316 options: vec![],
317 implementation: "test".to_string(),
318 continue_on_failure: false,
319 requires_success: false,
320 };
321
322 registry
323 .register_sync(cmd_def, Box::new(TestHandler))
324 .unwrap();
325
326 let context = Box::new(TestContext);
327 let _cli = CliInterface::new(registry, context);
328 }
329
330 #[test]
331 fn test_repl_interface_accessible() {
332 let mut registry = CommandRegistry::new();
333
334 let cmd_def = CommandDefinition {
335 name: "test".to_string(),
336 aliases: vec![],
337 description: "Test".to_string(),
338 required: false,
339 arguments: vec![],
340 options: vec![],
341 implementation: "test".to_string(),
342 continue_on_failure: false,
343 requires_success: false,
344 };
345
346 registry
347 .register_sync(cmd_def, Box::new(TestHandler))
348 .unwrap();
349
350 let context = Box::new(TestContext);
351 let _repl = ReplInterface::new(registry, context, "test".to_string(), None, None);
352 }
353}