dynamic_cli/interface/
repl.rs

1//! REPL (Read-Eval-Print Loop) implementation
2//!
3//! This module provides an interactive REPL interface with:
4//! - Line editing (arrow keys, history navigation)
5//! - Command history (persistent across sessions)
6//! - Tab completion (future enhancement)
7//! - Colored prompts and error display
8//!
9//! # Example
10//!
11//! ```no_run
12//! use dynamic_cli::interface::ReplInterface;
13//! use dynamic_cli::prelude::*;
14//!
15//! # #[derive(Default)]
16//! # struct MyContext;
17//! # impl ExecutionContext for MyContext {
18//! #     fn as_any(&self) -> &dyn std::any::Any { self }
19//! #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
20//! # }
21//! # fn main() -> dynamic_cli::Result<()> {
22//! let registry = CommandRegistry::new();
23//! let context = Box::new(MyContext::default());
24//!
25//! let repl = ReplInterface::new(registry, context, "myapp".to_string())?;
26//! repl.run()?;
27//! # Ok(())
28//! # }
29//! ```
30
31use crate::context::ExecutionContext;
32use crate::error::{display_error, DynamicCliError, ExecutionError, Result};
33use crate::parser::ReplParser;
34use crate::registry::CommandRegistry;
35use rustyline::error::ReadlineError;
36use rustyline::DefaultEditor;
37use std::path::PathBuf;
38
39/// REPL (Read-Eval-Print Loop) interface
40///
41/// Provides an interactive command-line interface with:
42/// - Line editing and history
43/// - Persistent command history
44/// - Graceful error handling
45/// - Special commands (exit, quit, help)
46///
47/// # Architecture
48///
49/// ```text
50/// User input → rustyline → ReplParser → CommandExecutor → Handler
51///                                             ↓
52///                                       ExecutionContext
53/// ```
54///
55/// # Special Commands
56///
57/// The REPL recognizes these built-in commands:
58/// - `exit`, `quit` - Exit the REPL
59/// - `help` - Show available commands (if registered)
60///
61/// # History
62///
63/// Command history is stored in the user's config directory:
64/// - Linux: `~/.config/<app_name>/history.txt`
65/// - macOS: `~/Library/Application Support/<app_name>/history.txt`
66/// - Windows: `%APPDATA%\<app_name>\history.txt`
67pub struct ReplInterface {
68    /// Command registry
69    registry: CommandRegistry,
70
71    /// Execution context
72    context: Box<dyn ExecutionContext>,
73
74    /// Prompt string (e.g., "myapp > ")
75    prompt: String,
76
77    /// Rustyline editor for input
78    editor: DefaultEditor,
79
80    /// History file path
81    history_path: Option<PathBuf>,
82}
83
84impl ReplInterface {
85    /// Create a new REPL interface
86    ///
87    /// # Arguments
88    ///
89    /// * `registry` - Command registry with all registered commands
90    /// * `context` - Execution context
91    /// * `prompt` - Prompt prefix (e.g., "myapp" will display as "myapp > ")
92    ///
93    /// # Errors
94    ///
95    /// Returns an error if rustyline initialization fails (rare).
96    ///
97    /// # Example
98    ///
99    /// ```no_run
100    /// use dynamic_cli::interface::ReplInterface;
101    /// use dynamic_cli::prelude::*;
102    ///
103    /// # #[derive(Default)]
104    /// # struct MyContext;
105    /// # impl ExecutionContext for MyContext {
106    /// #     fn as_any(&self) -> &dyn std::any::Any { self }
107    /// #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
108    /// # }
109    /// # fn main() -> dynamic_cli::Result<()> {
110    /// let registry = CommandRegistry::new();
111    /// let context = Box::new(MyContext::default());
112    ///
113    /// let repl = ReplInterface::new(registry, context, "myapp".to_string())?;
114    /// # Ok(())
115    /// # }
116    /// ```
117    pub fn new(
118        registry: CommandRegistry,
119        context: Box<dyn ExecutionContext>,
120        prompt: String,
121    ) -> Result<Self> {
122        // Create rustyline editor
123        let editor = DefaultEditor::new().map_err(|e| {
124            ExecutionError::CommandFailed(anyhow::anyhow!("Failed to initialize REPL: {}", e))
125        })?;
126
127        // Determine history file path
128        let history_path = Self::get_history_path(&prompt);
129
130        let mut repl = Self {
131            registry,
132            context,
133            prompt: format!("{} > ", prompt),
134            editor,
135            history_path,
136        };
137
138        // Load history if available
139        repl.load_history();
140
141        Ok(repl)
142    }
143
144    /// Get the history file path
145    ///
146    /// Uses the user's config directory to store command history.
147    fn get_history_path(app_name: &str) -> Option<PathBuf> {
148        dirs::config_dir().map(|config_dir| {
149            let app_dir = config_dir.join(app_name);
150            app_dir.join("history.txt")
151        })
152    }
153
154    /// Load command history from file
155    fn load_history(&mut self) {
156        if let Some(ref path) = self.history_path {
157            // Create parent directory if it doesn't exist
158            if let Some(parent) = path.parent() {
159                let _ = std::fs::create_dir_all(parent);
160            }
161
162            // Load history (ignore errors if file doesn't exist yet)
163            let _ = self.editor.load_history(path);
164        }
165    }
166
167    /// Save command history to file
168    fn save_history(&mut self) {
169        if let Some(ref path) = self.history_path {
170            if let Err(e) = self.editor.save_history(path) {
171                eprintln!("Warning: Failed to save command history: {}", e);
172            }
173        }
174    }
175
176    /// Run the REPL loop
177    ///
178    /// Enters an interactive loop that:
179    /// 1. Displays the prompt
180    /// 2. Reads user input
181    /// 3. Parses and executes the command
182    /// 4. Displays results or errors
183    /// 5. Repeats until user exits
184    ///
185    /// # Returns
186    ///
187    /// - `Ok(())` when user exits normally (via `exit` or `quit`)
188    /// - `Err(_)` on critical errors (I/O failures, etc.)
189    ///
190    /// # Example
191    ///
192    /// ```no_run
193    /// use dynamic_cli::interface::ReplInterface;
194    /// use dynamic_cli::prelude::*;
195    ///
196    /// # #[derive(Default)]
197    /// # struct MyContext;
198    /// # impl ExecutionContext for MyContext {
199    /// #     fn as_any(&self) -> &dyn std::any::Any { self }
200    /// #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
201    /// # }
202    /// # fn main() -> dynamic_cli::Result<()> {
203    /// let registry = CommandRegistry::new();
204    /// let context = Box::new(MyContext::default());
205    ///
206    /// let repl = ReplInterface::new(registry, context, "myapp".to_string())?;
207    /// repl.run()?; // Starts the REPL loop
208    /// # Ok(())
209    /// # }
210    /// ```
211    pub fn run(mut self) -> Result<()> {
212        loop {
213            // Read line from user
214            let readline = self.editor.readline(&self.prompt);
215
216            match readline {
217                Ok(line) => {
218                    // Skip empty lines
219                    let line = line.trim();
220                    if line.is_empty() {
221                        continue;
222                    }
223
224                    // Add to history
225                    let _ = self.editor.add_history_entry(line);
226
227                    // Check for built-in exit commands
228                    if line == "exit" || line == "quit" {
229                        println!("Goodbye!");
230                        break;
231                    }
232
233                    // Parse and execute command
234                    match self.execute_line(line) {
235                        Ok(()) => {
236                            // Command executed successfully
237                        }
238                        Err(e) => {
239                            // Display error but continue REPL
240                            display_error(&e);
241                        }
242                    }
243                }
244
245                Err(ReadlineError::Interrupted) => {
246                    // Ctrl-C pressed
247                    println!("^C");
248                    continue;
249                }
250
251                Err(ReadlineError::Eof) => {
252                    // Ctrl-D pressed
253                    println!("exit");
254                    break;
255                }
256
257                Err(err) => {
258                    // Other readline errors (rare)
259                    eprintln!("Error reading input: {}", err);
260                    break;
261                }
262            }
263        }
264
265        // Save history before exiting
266        self.save_history();
267
268        Ok(())
269    }
270
271    /// Execute a single line of input
272    ///
273    /// Parses the line and executes the corresponding command.
274    fn execute_line(&mut self, line: &str) -> Result<()> {
275        // Create parser (borrows registry immutably)
276        let parser = ReplParser::new(&self.registry);
277
278        // Parse command (parser is dropped after this, releasing the borrow)
279        let parsed = parser.parse_line(line)?;
280
281        // Now we can borrow registry again to get the handler
282        let handler = self
283            .registry
284            .get_handler(&parsed.command_name)
285            .ok_or_else(|| {
286                DynamicCliError::Execution(ExecutionError::HandlerNotFound {
287                    command: parsed.command_name.clone(),
288                    implementation: "unknown".to_string(),
289                })
290            })?;
291
292        // Execute (handler references registry, context is borrowed mutably)
293        handler.execute(&mut *self.context, &parsed.arguments)?;
294
295        Ok(())
296    }
297}
298
299// Implement Drop to ensure history is saved even if run() is not called
300impl Drop for ReplInterface {
301    fn drop(&mut self) {
302        self.save_history();
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use crate::config::schema::{ArgumentDefinition, ArgumentType, CommandDefinition};
310    use std::collections::HashMap;
311
312    // Test context
313    #[derive(Default)]
314    struct TestContext {
315        executed_commands: Vec<String>,
316    }
317
318    impl ExecutionContext for TestContext {
319        fn as_any(&self) -> &dyn std::any::Any {
320            self
321        }
322
323        fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
324            self
325        }
326    }
327
328    // Test handler
329    struct TestHandler {
330        name: String,
331    }
332
333    impl crate::executor::CommandHandler for TestHandler {
334        fn execute(
335            &self,
336            context: &mut dyn ExecutionContext,
337            _args: &HashMap<String, String>,
338        ) -> Result<()> {
339            let ctx = crate::context::downcast_mut::<TestContext>(context)
340                .expect("Failed to downcast context");
341            ctx.executed_commands.push(self.name.clone());
342            Ok(())
343        }
344    }
345
346    fn create_test_registry() -> CommandRegistry {
347        let mut registry = CommandRegistry::new();
348
349        let cmd_def = CommandDefinition {
350            name: "test".to_string(),
351            aliases: vec!["t".to_string()],
352            description: "Test command".to_string(),
353            required: false,
354            arguments: vec![],
355            options: vec![],
356            implementation: "test_handler".to_string(),
357        };
358
359        let handler = Box::new(TestHandler {
360            name: "test".to_string(),
361        });
362
363        registry.register(cmd_def, handler).unwrap();
364
365        registry
366    }
367
368    #[test]
369    fn test_repl_interface_creation() {
370        let registry = create_test_registry();
371        let context = Box::new(TestContext::default());
372
373        let repl = ReplInterface::new(registry, context, "test".to_string());
374        assert!(repl.is_ok());
375    }
376
377    #[test]
378    fn test_repl_execute_line() {
379        let registry = create_test_registry();
380        let context = Box::new(TestContext::default());
381
382        let mut repl = ReplInterface::new(registry, context, "test".to_string()).unwrap();
383
384        let result = repl.execute_line("test");
385        assert!(result.is_ok());
386
387        // Verify command was executed
388        let ctx = crate::context::downcast_ref::<TestContext>(&*repl.context).unwrap();
389        assert_eq!(ctx.executed_commands, vec!["test".to_string()]);
390    }
391
392    #[test]
393    fn test_repl_execute_with_alias() {
394        let registry = create_test_registry();
395        let context = Box::new(TestContext::default());
396
397        let mut repl = ReplInterface::new(registry, context, "test".to_string()).unwrap();
398
399        let result = repl.execute_line("t");
400        assert!(result.is_ok());
401    }
402
403    #[test]
404    fn test_repl_execute_unknown_command() {
405        let registry = create_test_registry();
406        let context = Box::new(TestContext::default());
407
408        let mut repl = ReplInterface::new(registry, context, "test".to_string()).unwrap();
409
410        let result = repl.execute_line("unknown");
411        assert!(result.is_err());
412
413        match result.unwrap_err() {
414            DynamicCliError::Parse(_) => {}
415            other => panic!("Expected Parse error, got: {:?}", other),
416        }
417    }
418
419    #[test]
420    fn test_repl_history_path() {
421        let path = ReplInterface::get_history_path("myapp");
422
423        // Path should exist (unless we're in a very restricted environment)
424        if let Some(p) = path {
425            assert!(p.to_str().unwrap().contains("myapp"));
426            assert!(p.to_str().unwrap().contains("history.txt"));
427        }
428    }
429
430    #[test]
431    fn test_repl_command_with_args() {
432        let mut registry = CommandRegistry::new();
433
434        let cmd_def = CommandDefinition {
435            name: "greet".to_string(),
436            aliases: vec![],
437            description: "Greet someone".to_string(),
438            required: false,
439            arguments: vec![ArgumentDefinition {
440                name: "name".to_string(),
441                arg_type: ArgumentType::String,
442                required: true,
443                description: "Name".to_string(),
444                validation: vec![],
445            }],
446            options: vec![],
447            implementation: "greet_handler".to_string(),
448        };
449
450        struct GreetHandler;
451        impl crate::executor::CommandHandler for GreetHandler {
452            fn execute(
453                &self,
454                _context: &mut dyn ExecutionContext,
455                args: &HashMap<String, String>,
456            ) -> Result<()> {
457                assert_eq!(args.get("name"), Some(&"Alice".to_string()));
458                Ok(())
459            }
460        }
461
462        registry.register(cmd_def, Box::new(GreetHandler)).unwrap();
463
464        let context = Box::new(TestContext::default());
465        let mut repl = ReplInterface::new(registry, context, "test".to_string()).unwrap();
466
467        let result = repl.execute_line("greet Alice");
468        assert!(result.is_ok());
469    }
470
471    #[test]
472    fn test_repl_empty_line() {
473        let registry = create_test_registry();
474        let context = Box::new(TestContext::default());
475
476        let mut repl = ReplInterface::new(registry, context, "test".to_string()).unwrap();
477
478        // Empty line should return an error from parser
479        let result = repl.execute_line("");
480        assert!(result.is_err());
481    }
482}