Skip to main content

j_cli/interactive/
mod.rs

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
23// ========== Voice 快捷键状态 ==========
24
25/// Ctrl+V 语音输入的共享状态
26struct VoiceState {
27    /// 是否由 Ctrl+V 触发(区分 Ctrl+C)
28    triggered: AtomicBool,
29    /// 触发时保存的行内容
30    saved_line: Mutex<String>,
31    /// 触发时保存的光标位置
32    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
51/// Ctrl+V 按键处理器
52struct 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        // 保存当前行内容和光标位置
65        *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        // 返回 Interrupt 跳出 readline
69        Some(Cmd::Interrupt)
70    }
71}
72
73// ========== 交互模式主循环 ==========
74
75/// 启动交互模式
76pub 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) // 手动控制历史记录,report 内容不入历史(隐私保护)
81        .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    // 绑定 Ctrl+V 到语音输入处理器
95    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 状态
115        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                    // Ctrl+V 触发的语音输入
173                    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                        // 将转写文字插入到光标位置
181                        let left = &saved_line[..saved_pos];
182                        let right = &saved_line[saved_pos..];
183                        let new_left = format!("{}{}", left, text);
184
185                        // 用 readline_with_initial 回填
186                        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                                // readline_with_initial 被 Ctrl+C 中断
210                                // 检查是否又触发了 Ctrl+V
211                                if voice_state.triggered.load(Ordering::SeqCst) {
212                                    // 嵌套 voice 不处理,简单忽略
213                                }
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                        // 录音无结果,恢复之前的输入
227                        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
282/// 获取历史文件路径: ~/.jdata/history.txt
283fn 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
289/// 解析用户输入为参数列表(支持双引号包裹带空格的参数)
290fn 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}