Skip to main content

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}