Skip to main content

j_cli/interactive/
mod.rs

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
20/// 启动交互模式
21pub 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) // 手动控制历史记录,report 内容不入历史(隐私保护)
26        .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
119/// 获取历史文件路径: ~/.jdata/history.txt
120fn 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
126/// 解析用户输入为参数列表(支持双引号包裹带空格的参数)
127fn 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}