1pub mod completer;
2pub mod parser;
3pub mod shell;
4
5use crate::config::YamlConfig;
6use crate::constants::{self, cmd};
7use crate::{error, info};
8use colored::Colorize;
9use completer::CopilotHelper;
10use parser::execute_interactive_command;
11use rustyline::error::ReadlineError;
12use rustyline::history::DefaultHistory;
13use rustyline::{
14 Cmd, CompletionType, Config, EditMode, Editor, EventHandler, KeyCode, KeyEvent, Modifiers,
15};
16use shell::{
17 enter_interactive_shell, execute_shell_command, expand_env_vars, inject_envs_to_process,
18};
19
20pub fn run_interactive(config: &mut YamlConfig) {
22 let rl_config = Config::builder()
23 .completion_type(CompletionType::Circular)
24 .edit_mode(EditMode::Emacs)
25 .auto_add_history(false) .build();
27
28 let helper = CopilotHelper::new(config);
29
30 let mut rl: Editor<CopilotHelper, DefaultHistory> =
31 Editor::with_config(rl_config).expect("无法初始化编辑器");
32 rl.set_helper(Some(helper));
33
34 rl.bind_sequence(
35 KeyEvent(KeyCode::Tab, Modifiers::NONE),
36 EventHandler::Simple(Cmd::Complete),
37 );
38
39 let history_path = history_file_path();
40 let _ = rl.load_history(&history_path);
41
42 info!("{}", constants::WELCOME_MESSAGE);
43
44 inject_envs_to_process(config);
45
46 let prompt = format!("{} ", constants::INTERACTIVE_PROMPT.yellow());
47
48 loop {
49 match rl.readline(&prompt) {
50 Ok(line) => {
51 let input = line.trim();
52
53 if input.is_empty() {
54 continue;
55 }
56
57 if input.starts_with(constants::SHELL_PREFIX) {
58 let shell_cmd = &input[1..].trim();
59 if shell_cmd.is_empty() {
60 enter_interactive_shell(config);
61 } else {
62 execute_shell_command(shell_cmd, config);
63 }
64 let _ = rl.add_history_entry(input);
65 println!();
66 continue;
67 }
68
69 let args = parse_input(input);
70 if args.is_empty() {
71 continue;
72 }
73
74 let args: Vec<String> = args.iter().map(|a| expand_env_vars(a)).collect();
75
76 let verbose = config.is_verbose();
77 let start = if verbose {
78 Some(std::time::Instant::now())
79 } else {
80 None
81 };
82
83 let is_report_cmd = !args.is_empty() && cmd::REPORT.contains(&args[0].as_str());
84 if !is_report_cmd {
85 let _ = rl.add_history_entry(input);
86 }
87
88 execute_interactive_command(&args, config);
89
90 if let Some(start) = start {
91 let elapsed = start.elapsed();
92 crate::debug_log!(config, "duration: {} ms", elapsed.as_millis());
93 }
94
95 if let Some(helper) = rl.helper_mut() {
96 helper.refresh(config);
97 }
98 inject_envs_to_process(config);
99
100 println!();
101 }
102 Err(ReadlineError::Interrupted) => {
103 info!("\nProgram interrupted. Use 'exit' to quit.");
104 }
105 Err(ReadlineError::Eof) => {
106 info!("\nGoodbye! 👋");
107 break;
108 }
109 Err(err) => {
110 error!("读取输入失败: {:?}", err);
111 break;
112 }
113 }
114 }
115
116 let _ = rl.save_history(&history_path);
117}
118
119fn history_file_path() -> std::path::PathBuf {
121 let data_dir = crate::config::YamlConfig::data_dir();
122 let _ = std::fs::create_dir_all(&data_dir);
123 data_dir.join(constants::HISTORY_FILE)
124}
125
126fn parse_input(input: &str) -> Vec<String> {
128 let mut args = Vec::new();
129 let mut current = String::new();
130 let mut in_quotes = false;
131
132 for ch in input.chars() {
133 match ch {
134 '"' => {
135 in_quotes = !in_quotes;
136 }
137 ' ' if !in_quotes => {
138 if !current.is_empty() {
139 args.push(current.clone());
140 current.clear();
141 }
142 }
143 _ => {
144 current.push(ch);
145 }
146 }
147 }
148
149 if !current.is_empty() {
150 args.push(current);
151 }
152
153 args
154}