1use crate::config::schema::CommandsConfig;
32use crate::context::ExecutionContext;
33use crate::error::{display_error, DynamicCliError, ExecutionError, Result};
34use crate::help::HelpFormatter;
35use crate::parser::ReplParser;
36use crate::registry::CommandRegistry;
37use rustyline::error::ReadlineError;
38use rustyline::DefaultEditor;
39use std::path::PathBuf;
40
41pub struct ReplInterface {
70 registry: CommandRegistry,
72
73 context: Box<dyn ExecutionContext>,
75
76 prompt: String,
78
79 editor: DefaultEditor,
81
82 history_path: Option<PathBuf>,
84
85 config: Option<CommandsConfig>,
88
89 help_formatter: Option<Box<dyn HelpFormatter>>,
92}
93
94impl ReplInterface {
95 pub fn new(
128 registry: CommandRegistry,
129 context: Box<dyn ExecutionContext>,
130 prompt: String,
131 ) -> Result<Self> {
132 let editor = DefaultEditor::new().map_err(|e| {
134 ExecutionError::CommandFailed(anyhow::anyhow!("Failed to initialize REPL: {}", e))
135 })?;
136
137 let history_path = Self::get_history_path(&prompt);
139
140 let mut repl = Self {
141 registry,
142 context,
143 prompt: format!("{} > ", prompt),
144 editor,
145 history_path,
146 config: None,
147 help_formatter: None,
148 };
149
150 repl.load_history();
152
153 Ok(repl)
154 }
155
156 pub fn with_help(mut self, config: CommandsConfig, formatter: Box<dyn HelpFormatter>) -> Self {
195 self.config = Some(config);
196 self.help_formatter = Some(formatter);
197 self
198 }
199
200 fn try_handle_help(&self, line: &str) -> Option<String> {
216 let (config, formatter) = match (&self.config, &self.help_formatter) {
217 (Some(c), Some(f)) => (c, f.as_ref()),
218 _ => return None,
219 };
220
221 let trimmed = line.trim();
222
223 if trimmed == "--help" || trimmed == "-h" {
225 return Some(formatter.format_app(config));
226 }
227
228 if let Some(rest) = trimmed
230 .strip_prefix("--help ")
231 .or_else(|| trimmed.strip_prefix("-h "))
232 {
233 let cmd = rest.trim();
234 if !cmd.is_empty() {
235 return Some(formatter.format_command(config, cmd));
236 }
237 }
238
239 let parts: Vec<&str> = trimmed.split_whitespace().collect();
241 if parts.len() >= 2 {
242 let last = *parts.last().unwrap();
243 if last == "--help" || last == "-h" {
244 return Some(formatter.format_command(config, parts[0]));
245 }
246 }
247
248 None
249 }
250
251 fn get_history_path(app_name: &str) -> Option<PathBuf> {
255 dirs::config_dir().map(|config_dir| {
256 let app_dir = config_dir.join(app_name);
257 app_dir.join("history.txt")
258 })
259 }
260
261 fn load_history(&mut self) {
263 if let Some(ref path) = self.history_path {
264 if let Some(parent) = path.parent() {
266 let _ = std::fs::create_dir_all(parent);
267 }
268
269 let _ = self.editor.load_history(path);
271 }
272 }
273
274 fn save_history(&mut self) {
276 if let Some(ref path) = self.history_path {
277 if let Err(e) = self.editor.save_history(path) {
278 eprintln!("Warning: Failed to save command history: {}", e);
279 }
280 }
281 }
282
283 pub fn run(mut self) -> Result<()> {
319 loop {
320 let readline = self.editor.readline(&self.prompt);
322
323 match readline {
324 Ok(line) => {
325 let line = line.trim();
327 if line.is_empty() {
328 continue;
329 }
330
331 let _ = self.editor.add_history_entry(line);
333
334 if line == "exit" || line == "quit" {
336 println!("Goodbye!");
337 break;
338 }
339
340 match self.execute_line(line) {
342 Ok(()) => {
343 }
345 Err(e) => {
346 display_error(&e);
348 }
349 }
350 }
351
352 Err(ReadlineError::Interrupted) => {
353 println!("^C");
355 continue;
356 }
357
358 Err(ReadlineError::Eof) => {
359 println!("exit");
361 break;
362 }
363
364 Err(err) => {
365 eprintln!("Error reading input: {}", err);
367 break;
368 }
369 }
370 }
371
372 self.save_history();
374
375 Ok(())
376 }
377
378 fn execute_line(&mut self, line: &str) -> Result<()> {
384 if let Some(output) = self.try_handle_help(line) {
387 print!("{}", output);
388 return Ok(());
389 }
390
391 let parser = ReplParser::new(&self.registry);
393
394 let parsed = parser.parse_line(line)?;
396
397 let handler = self
399 .registry
400 .get_handler(&parsed.command_name)
401 .ok_or_else(|| {
402 DynamicCliError::Execution(ExecutionError::handler_not_found(
403 &parsed.command_name,
404 "unknown",
405 ))
406 })?;
407
408 handler.execute(&mut *self.context, &parsed.arguments)?;
410
411 Ok(())
412 }
413}
414
415impl Drop for ReplInterface {
417 fn drop(&mut self) {
418 self.save_history();
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425 use crate::config::schema::{ArgumentDefinition, ArgumentType, CommandDefinition};
426 use std::collections::HashMap;
427
428 #[derive(Default)]
430 struct TestContext {
431 executed_commands: Vec<String>,
432 }
433
434 impl ExecutionContext for TestContext {
435 fn as_any(&self) -> &dyn std::any::Any {
436 self
437 }
438
439 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
440 self
441 }
442 }
443
444 struct TestHandler {
446 name: String,
447 }
448
449 impl crate::executor::CommandHandler for TestHandler {
450 fn execute(
451 &self,
452 context: &mut dyn ExecutionContext,
453 _args: &HashMap<String, String>,
454 ) -> Result<()> {
455 let ctx = crate::context::downcast_mut::<TestContext>(context)
456 .expect("Failed to downcast context");
457 ctx.executed_commands.push(self.name.clone());
458 Ok(())
459 }
460 }
461
462 fn create_test_registry() -> CommandRegistry {
463 let mut registry = CommandRegistry::new();
464
465 let cmd_def = CommandDefinition {
466 name: "test".to_string(),
467 aliases: vec!["t".to_string()],
468 description: "Test command".to_string(),
469 required: false,
470 arguments: vec![],
471 options: vec![],
472 implementation: "test_handler".to_string(),
473 };
474
475 let handler = Box::new(TestHandler {
476 name: "test".to_string(),
477 });
478
479 registry.register(cmd_def, handler).unwrap();
480
481 registry
482 }
483
484 #[test]
485 fn test_repl_interface_creation() {
486 let registry = create_test_registry();
487 let context = Box::new(TestContext::default());
488
489 let repl = ReplInterface::new(registry, context, "test".to_string());
490 assert!(repl.is_ok());
491 }
492
493 #[test]
494 fn test_repl_execute_line() {
495 let registry = create_test_registry();
496 let context = Box::new(TestContext::default());
497
498 let mut repl = ReplInterface::new(registry, context, "test".to_string()).unwrap();
499
500 let result = repl.execute_line("test");
501 assert!(result.is_ok());
502
503 let ctx = crate::context::downcast_ref::<TestContext>(&*repl.context).unwrap();
505 assert_eq!(ctx.executed_commands, vec!["test".to_string()]);
506 }
507
508 #[test]
509 fn test_repl_execute_with_alias() {
510 let registry = create_test_registry();
511 let context = Box::new(TestContext::default());
512
513 let mut repl = ReplInterface::new(registry, context, "test".to_string()).unwrap();
514
515 let result = repl.execute_line("t");
516 assert!(result.is_ok());
517 }
518
519 #[test]
520 fn test_repl_execute_unknown_command() {
521 let registry = create_test_registry();
522 let context = Box::new(TestContext::default());
523
524 let mut repl = ReplInterface::new(registry, context, "test".to_string()).unwrap();
525
526 let result = repl.execute_line("unknown");
527 assert!(result.is_err());
528
529 match result.unwrap_err() {
530 DynamicCliError::Parse(_) => {}
531 other => panic!("Expected Parse error, got: {:?}", other),
532 }
533 }
534
535 #[test]
536 fn test_repl_history_path() {
537 let path = ReplInterface::get_history_path("myapp");
538
539 if let Some(p) = path {
541 assert!(p.to_str().unwrap().contains("myapp"));
542 assert!(p.to_str().unwrap().contains("history.txt"));
543 }
544 }
545
546 #[test]
547 fn test_repl_command_with_args() {
548 let mut registry = CommandRegistry::new();
549
550 let cmd_def = CommandDefinition {
551 name: "greet".to_string(),
552 aliases: vec![],
553 description: "Greet someone".to_string(),
554 required: false,
555 arguments: vec![ArgumentDefinition {
556 name: "name".to_string(),
557 arg_type: ArgumentType::String,
558 required: true,
559 description: "Name".to_string(),
560 validation: vec![],
561 }],
562 options: vec![],
563 implementation: "greet_handler".to_string(),
564 };
565
566 struct GreetHandler;
567 impl crate::executor::CommandHandler for GreetHandler {
568 fn execute(
569 &self,
570 _context: &mut dyn ExecutionContext,
571 args: &HashMap<String, String>,
572 ) -> Result<()> {
573 assert_eq!(args.get("name"), Some(&"Alice".to_string()));
574 Ok(())
575 }
576 }
577
578 registry.register(cmd_def, Box::new(GreetHandler)).unwrap();
579
580 let context = Box::new(TestContext::default());
581 let mut repl = ReplInterface::new(registry, context, "test".to_string()).unwrap();
582
583 let result = repl.execute_line("greet Alice");
584 assert!(result.is_ok());
585 }
586
587 #[test]
588 fn test_repl_empty_line() {
589 let registry = create_test_registry();
590 let context = Box::new(TestContext::default());
591
592 let mut repl = ReplInterface::new(registry, context, "test".to_string()).unwrap();
593
594 let result = repl.execute_line("");
596 assert!(result.is_err());
597 }
598
599 fn make_help_config() -> crate::config::schema::CommandsConfig {
605 use crate::config::schema::{CommandDefinition, CommandsConfig, Metadata};
606 CommandsConfig {
607 metadata: Metadata {
608 version: "1.0.0".to_string(),
609 prompt: "testapp".to_string(),
610 prompt_suffix: " > ".to_string(),
611 },
612 commands: vec![CommandDefinition {
613 name: "hello".to_string(),
614 aliases: vec!["hi".to_string()],
615 description: "Say hello".to_string(),
616 required: false,
617 arguments: vec![],
618 options: vec![],
619 implementation: "hello_handler".to_string(),
620 }],
621 global_options: vec![],
622 }
623 }
624
625 #[test]
626 fn test_try_handle_help_without_formatter_returns_none() {
627 let registry = create_test_registry();
629 let context = Box::new(TestContext::default());
630 let repl = ReplInterface::new(registry, context, "test".to_string()).unwrap();
631
632 assert!(repl.try_handle_help("--help").is_none());
633 assert!(repl.try_handle_help("-h").is_none());
634 }
635
636 #[test]
637 fn test_try_handle_help_global() {
638 use crate::help::DefaultHelpFormatter;
639 colored::control::set_override(false);
640
641 let registry = create_test_registry();
642 let context = Box::new(TestContext::default());
643 let config = make_help_config();
644
645 let repl = ReplInterface::new(registry, context, "test".to_string())
646 .unwrap()
647 .with_help(config, Box::new(DefaultHelpFormatter::new()));
648
649 let out = repl.try_handle_help("--help");
650 assert!(out.is_some());
651 let out = out.unwrap();
652 assert!(out.contains("testapp"), "should contain app prompt");
653 assert!(out.contains("hello"), "should list commands");
654 }
655
656 #[test]
657 fn test_try_handle_help_short_flag() {
658 use crate::help::DefaultHelpFormatter;
659 colored::control::set_override(false);
660
661 let registry = create_test_registry();
662 let context = Box::new(TestContext::default());
663 let config = make_help_config();
664
665 let repl = ReplInterface::new(registry, context, "test".to_string())
666 .unwrap()
667 .with_help(config, Box::new(DefaultHelpFormatter::new()));
668
669 let out = repl.try_handle_help("-h");
671 assert!(out.is_some());
672 assert!(out.unwrap().contains("testapp"));
673 }
674
675 #[test]
676 fn test_try_handle_help_with_command_prefix() {
677 use crate::help::DefaultHelpFormatter;
678 colored::control::set_override(false);
679
680 let registry = create_test_registry();
681 let context = Box::new(TestContext::default());
682 let config = make_help_config();
683
684 let repl = ReplInterface::new(registry, context, "test".to_string())
685 .unwrap()
686 .with_help(config, Box::new(DefaultHelpFormatter::new()));
687
688 let out = repl.try_handle_help("--help hello");
690 assert!(out.is_some());
691 assert!(out.unwrap().contains("hello"));
692
693 let out2 = repl.try_handle_help("-h hello");
695 assert!(out2.is_some());
696 }
697
698 #[test]
699 fn test_try_handle_help_command_suffix() {
700 use crate::help::DefaultHelpFormatter;
701 colored::control::set_override(false);
702
703 let registry = create_test_registry();
704 let context = Box::new(TestContext::default());
705 let config = make_help_config();
706
707 let repl = ReplInterface::new(registry, context, "test".to_string())
708 .unwrap()
709 .with_help(config, Box::new(DefaultHelpFormatter::new()));
710
711 let out = repl.try_handle_help("hello --help");
713 assert!(out.is_some());
714 assert!(out.unwrap().contains("hello"));
715
716 let out2 = repl.try_handle_help("hello -h");
718 assert!(out2.is_some());
719 }
720
721 #[test]
722 fn test_try_handle_help_alias() {
723 use crate::help::DefaultHelpFormatter;
724 colored::control::set_override(false);
725
726 let registry = create_test_registry();
727 let context = Box::new(TestContext::default());
728 let config = make_help_config();
729
730 let repl = ReplInterface::new(registry, context, "test".to_string())
731 .unwrap()
732 .with_help(config, Box::new(DefaultHelpFormatter::new()));
733
734 let out = repl.try_handle_help("--help hi");
736 assert!(out.is_some());
737 assert!(out.unwrap().contains("hello"));
739 }
740
741 #[test]
742 fn test_execute_line_help_intercepted() {
743 use crate::help::DefaultHelpFormatter;
744 colored::control::set_override(false);
745
746 let registry = create_test_registry();
747 let context = Box::new(TestContext::default());
748 let config = make_help_config();
749
750 let mut repl = ReplInterface::new(registry, context, "test".to_string())
751 .unwrap()
752 .with_help(config, Box::new(DefaultHelpFormatter::new()));
753
754 let result = repl.execute_line("--help");
756 assert!(result.is_ok(), "help request must not return an error");
757 }
758
759 #[test]
760 fn test_execute_line_normal_command_still_works_with_formatter() {
761 use crate::help::DefaultHelpFormatter;
762
763 let registry = create_test_registry();
764 let context = Box::new(TestContext::default());
765 let config = make_help_config();
766
767 let mut repl = ReplInterface::new(registry, context, "test".to_string())
768 .unwrap()
769 .with_help(config, Box::new(DefaultHelpFormatter::new()));
770
771 let result = repl.execute_line("test");
773 assert!(result.is_ok());
774 }
775}