1pub mod completer;
2pub mod parser;
3pub mod shell;
4
5use crate::command::voice::do_voice_record_for_interactive;
6use crate::config::YamlConfig;
7use crate::constants::{self, cmd};
8use crate::{error, info};
9use colored::Colorize;
10use completer::CopilotHelper;
11use parser::execute_interactive_command;
12use rustyline::error::ReadlineError;
13use rustyline::history::DefaultHistory;
14use rustyline::{
15 Cmd, CompletionType, Config, EditMode, Editor, EventHandler, KeyCode, KeyEvent, Modifiers,
16};
17use shell::{
18 enter_interactive_shell, execute_shell_command, expand_env_vars, inject_envs_to_process,
19};
20use std::sync::atomic::{AtomicBool, Ordering};
21use std::sync::{Arc, Mutex};
22
23struct VoiceState {
27 triggered: AtomicBool,
29 saved_line: Mutex<String>,
31 saved_pos: Mutex<usize>,
33}
34
35impl VoiceState {
36 fn new() -> Self {
37 Self {
38 triggered: AtomicBool::new(false),
39 saved_line: Mutex::new(String::new()),
40 saved_pos: Mutex::new(0),
41 }
42 }
43
44 fn reset(&self) {
45 self.triggered.store(false, Ordering::SeqCst);
46 *self.saved_line.lock().unwrap() = String::new();
47 *self.saved_pos.lock().unwrap() = 0;
48 }
49}
50
51struct VoiceKeyHandler {
53 state: Arc<VoiceState>,
54}
55
56impl rustyline::ConditionalEventHandler for VoiceKeyHandler {
57 fn handle(
58 &self,
59 _evt: &rustyline::Event,
60 _n: rustyline::RepeatCount,
61 _positive: bool,
62 ctx: &rustyline::EventContext,
63 ) -> Option<Cmd> {
64 *self.state.saved_line.lock().unwrap() = ctx.line().to_string();
66 *self.state.saved_pos.lock().unwrap() = ctx.pos();
67 self.state.triggered.store(true, Ordering::SeqCst);
68 Some(Cmd::Interrupt)
70 }
71}
72
73pub fn run_interactive(config: &mut YamlConfig) {
77 let rl_config = Config::builder()
78 .completion_type(CompletionType::Circular)
79 .edit_mode(EditMode::Emacs)
80 .auto_add_history(false) .build();
82
83 let helper = CopilotHelper::new(config);
84
85 let mut rl: Editor<CopilotHelper, DefaultHistory> =
86 Editor::with_config(rl_config).expect("无法初始化编辑器");
87 rl.set_helper(Some(helper));
88
89 rl.bind_sequence(
90 KeyEvent(KeyCode::Tab, Modifiers::NONE),
91 EventHandler::Simple(Cmd::Complete),
92 );
93
94 let voice_state = Arc::new(VoiceState::new());
96 let handler = VoiceKeyHandler {
97 state: voice_state.clone(),
98 };
99 rl.bind_sequence(
100 KeyEvent(KeyCode::Char('v'), Modifiers::CTRL),
101 EventHandler::Conditional(Box::new(handler)),
102 );
103
104 let history_path = history_file_path();
105 let _ = rl.load_history(&history_path);
106
107 info!("{}", constants::WELCOME_MESSAGE);
108
109 inject_envs_to_process(config);
110
111 let prompt = format!("{} ", constants::INTERACTIVE_PROMPT.yellow());
112
113 loop {
114 voice_state.reset();
116
117 match rl.readline(&prompt) {
118 Ok(line) => {
119 let input = line.trim();
120
121 if input.is_empty() {
122 continue;
123 }
124
125 if input.starts_with(constants::SHELL_PREFIX) {
126 let shell_cmd = &input[1..].trim();
127 if shell_cmd.is_empty() {
128 enter_interactive_shell(config);
129 } else {
130 execute_shell_command(shell_cmd, config);
131 }
132 let _ = rl.add_history_entry(input);
133 println!();
134 continue;
135 }
136
137 let args = parse_input(input);
138 if args.is_empty() {
139 continue;
140 }
141
142 let args: Vec<String> = args.iter().map(|a| expand_env_vars(a)).collect();
143
144 let verbose = config.is_verbose();
145 let start = if verbose {
146 Some(std::time::Instant::now())
147 } else {
148 None
149 };
150
151 let is_report_cmd = !args.is_empty() && cmd::REPORT.contains(&args[0].as_str());
152 if !is_report_cmd {
153 let _ = rl.add_history_entry(input);
154 }
155
156 execute_interactive_command(&args, config);
157
158 if let Some(start) = start {
159 let elapsed = start.elapsed();
160 crate::debug_log!(config, "duration: {} ms", elapsed.as_millis());
161 }
162
163 if let Some(helper) = rl.helper_mut() {
164 helper.refresh(config);
165 }
166 inject_envs_to_process(config);
167
168 println!();
169 }
170 Err(ReadlineError::Interrupted) => {
171 if voice_state.triggered.load(Ordering::SeqCst) {
172 let saved_line = voice_state.saved_line.lock().unwrap().clone();
174 let saved_pos = voice_state.saved_pos.lock().unwrap().clone();
175
176 println!();
177 let text = do_voice_record_for_interactive();
178
179 if !text.is_empty() {
180 let left = &saved_line[..saved_pos];
182 let right = &saved_line[saved_pos..];
183 let new_left = format!("{}{}", left, text);
184
185 match rl.readline_with_initial(&prompt, (&new_left, right)) {
187 Ok(line) => {
188 let input = line.trim();
189 if !input.is_empty() {
190 let args = parse_input(input);
191 if !args.is_empty() {
192 let args: Vec<String> =
193 args.iter().map(|a| expand_env_vars(a)).collect();
194 let is_report_cmd = !args.is_empty()
195 && cmd::REPORT.contains(&args[0].as_str());
196 if !is_report_cmd {
197 let _ = rl.add_history_entry(input);
198 }
199 execute_interactive_command(&args, config);
200 if let Some(helper) = rl.helper_mut() {
201 helper.refresh(config);
202 }
203 inject_envs_to_process(config);
204 }
205 }
206 println!();
207 }
208 Err(ReadlineError::Interrupted) => {
209 if voice_state.triggered.load(Ordering::SeqCst) {
212 }
214 info!("\nProgram interrupted. Use 'exit' to quit.");
215 }
216 Err(ReadlineError::Eof) => {
217 info!("\nGoodbye! 👋");
218 break;
219 }
220 Err(err) => {
221 error!("读取输入失败: {:?}", err);
222 break;
223 }
224 }
225 } else {
226 if !saved_line.is_empty() {
228 match rl.readline_with_initial(&prompt, (&saved_line, "")) {
229 Ok(line) => {
230 let input = line.trim();
231 if !input.is_empty() {
232 let args = parse_input(input);
233 if !args.is_empty() {
234 let args: Vec<String> =
235 args.iter().map(|a| expand_env_vars(a)).collect();
236 let is_report_cmd = !args.is_empty()
237 && cmd::REPORT.contains(&args[0].as_str());
238 if !is_report_cmd {
239 let _ = rl.add_history_entry(input);
240 }
241 execute_interactive_command(&args, config);
242 if let Some(helper) = rl.helper_mut() {
243 helper.refresh(config);
244 }
245 inject_envs_to_process(config);
246 }
247 }
248 println!();
249 }
250 Err(ReadlineError::Interrupted) => {
251 info!("\nProgram interrupted. Use 'exit' to quit.");
252 }
253 Err(ReadlineError::Eof) => {
254 info!("\nGoodbye! 👋");
255 break;
256 }
257 Err(err) => {
258 error!("读取输入失败: {:?}", err);
259 break;
260 }
261 }
262 }
263 }
264 } else {
265 info!("\nProgram interrupted. Use 'exit' to quit.");
266 }
267 }
268 Err(ReadlineError::Eof) => {
269 info!("\nGoodbye! 👋");
270 break;
271 }
272 Err(err) => {
273 error!("读取输入失败: {:?}", err);
274 break;
275 }
276 }
277 }
278
279 let _ = rl.save_history(&history_path);
280}
281
282fn history_file_path() -> std::path::PathBuf {
284 let data_dir = crate::config::YamlConfig::data_dir();
285 let _ = std::fs::create_dir_all(&data_dir);
286 data_dir.join(constants::HISTORY_FILE)
287}
288
289fn parse_input(input: &str) -> Vec<String> {
291 let mut args = Vec::new();
292 let mut current = String::new();
293 let mut in_quotes = false;
294
295 for ch in input.chars() {
296 match ch {
297 '"' => {
298 in_quotes = !in_quotes;
299 }
300 ' ' if !in_quotes => {
301 if !current.is_empty() {
302 args.push(current.clone());
303 current.clear();
304 }
305 }
306 _ => {
307 current.push(ch);
308 }
309 }
310 }
311
312 if !current.is_empty() {
313 args.push(current);
314 }
315
316 args
317}