Skip to main content

j_cli/interactive/
shell.rs

1use crate::config::YamlConfig;
2use crate::constants::shell;
3use crate::{error, info};
4use colored::Colorize;
5
6/// 进入交互式 shell 子进程
7pub fn enter_interactive_shell(config: &YamlConfig) {
8    let os = std::env::consts::OS;
9
10    let shell_path = if os == shell::WINDOWS_OS {
11        shell::WINDOWS_CMD.to_string()
12    } else {
13        std::env::var("SHELL").unwrap_or_else(|_| shell::BASH_PATH.to_string())
14    };
15
16    info!("进入 shell 模式 ({}), 输入 exit 返回 copilot", shell_path);
17
18    let mut command = std::process::Command::new(&shell_path);
19
20    for (key, value) in config.collect_alias_envs() {
21        command.env(&key, &value);
22    }
23
24    let mut cleanup_path: Option<std::path::PathBuf> = None;
25
26    if os != shell::WINDOWS_OS {
27        let is_zsh = shell_path.contains("zsh");
28        let is_bash = shell_path.contains("bash");
29
30        if is_zsh {
31            let pid = std::process::id();
32            let tmp_dir = std::path::PathBuf::from(format!("/tmp/j_shell_zsh_{}", pid));
33            let _ = std::fs::create_dir_all(&tmp_dir);
34
35            let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
36            let zshrc_content = format!(
37                "# j shell 临时配置 - 自动生成,退出后自动清理\n\
38                 export ZDOTDIR=\"{home}\"\n\
39                 if [ -f \"{home}/.zshenv\" ]; then\n\
40                   source \"{home}/.zshenv\"\n\
41                 fi\n\
42                 if [ -f \"{home}/.zprofile\" ]; then\n\
43                   source \"{home}/.zprofile\"\n\
44                 fi\n\
45                 if [ -f \"{home}/.zshrc\" ]; then\n\
46                   source \"{home}/.zshrc\"\n\
47                 fi\n\
48                 PROMPT='%F{{green}}shell%f (%F{{cyan}}%~%f) %F{{green}}>%f '\n",
49                home = home,
50            );
51
52            let zshrc_path = tmp_dir.join(".zshrc");
53            if let Err(e) = std::fs::write(&zshrc_path, &zshrc_content) {
54                error!("创建临时 .zshrc 失败: {}", e);
55                command.env("PROMPT", "%F{green}shell%f (%F{cyan}%~%f) %F{green}>%f ");
56            } else {
57                command.env("ZDOTDIR", tmp_dir.to_str().unwrap_or("/tmp"));
58                cleanup_path = Some(tmp_dir);
59            }
60        } else if is_bash {
61            let pid = std::process::id();
62            let tmp_rc = std::path::PathBuf::from(format!("/tmp/j_shell_bashrc_{}", pid));
63
64            let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
65            let bashrc_content = format!(
66                "# j shell 临时配置 - 自动生成,退出后自动清理\n\
67                 if [ -f \"{home}/.bashrc\" ]; then\n\
68                   source \"{home}/.bashrc\"\n\
69                 fi\n\
70                 PS1='\\[\\033[32m\\]shell\\[\\033[0m\\] (\\[\\033[36m\\]\\w\\[\\033[0m\\]) \\[\\033[32m\\]>\\[\\033[0m\\] '\n",
71                home = home,
72            );
73
74            if let Err(e) = std::fs::write(&tmp_rc, &bashrc_content) {
75                error!("创建临时 bashrc 失败: {}", e);
76                command.env("PS1", "\\[\\033[32m\\]shell\\[\\033[0m\\] (\\[\\033[36m\\]\\w\\[\\033[0m\\]) \\[\\033[32m\\]>\\[\\033[0m\\] ");
77            } else {
78                command.arg("--rcfile");
79                command.arg(tmp_rc.to_str().unwrap_or("/tmp/j_shell_bashrc"));
80                cleanup_path = Some(tmp_rc);
81            }
82        } else {
83            command.env(
84                "PS1",
85                "\x1b[32mshell\x1b[0m (\x1b[36m\\w\x1b[0m) \x1b[32m>\x1b[0m ",
86            );
87            command.env(
88                "PROMPT",
89                "\x1b[32mshell\x1b[0m (\x1b[36m%~\x1b[0m) \x1b[32m>\x1b[0m ",
90            );
91        }
92    }
93
94    command
95        .stdin(std::process::Stdio::inherit())
96        .stdout(std::process::Stdio::inherit())
97        .stderr(std::process::Stdio::inherit());
98
99    match command.status() {
100        Ok(status) => {
101            if !status.success() {
102                if let Some(code) = status.code() {
103                    error!("shell 退出码: {}", code);
104                }
105            }
106        }
107        Err(e) => {
108            error!("启动 shell 失败: {}", e);
109        }
110    }
111
112    if let Some(path) = cleanup_path {
113        if path.is_dir() {
114            let _ = std::fs::remove_dir_all(&path);
115        } else {
116            let _ = std::fs::remove_file(&path);
117        }
118    }
119
120    info!("{}", "已返回 copilot 交互模式 🚀".green());
121}
122
123/// 执行 shell 命令(交互模式下 ! 前缀触发)
124pub fn execute_shell_command(cmd: &str, config: &YamlConfig) {
125    if cmd.is_empty() {
126        return;
127    }
128
129    let os = std::env::consts::OS;
130    let mut command = if os == shell::WINDOWS_OS {
131        let mut c = std::process::Command::new(shell::WINDOWS_CMD);
132        c.args([shell::WINDOWS_CMD_FLAG, cmd]);
133        c
134    } else {
135        let mut c = std::process::Command::new(shell::BASH_PATH);
136        c.args([shell::BASH_CMD_FLAG, cmd]);
137        c
138    };
139
140    for (key, value) in config.collect_alias_envs() {
141        command.env(&key, &value);
142    }
143
144    match command.status() {
145        Ok(status) => {
146            if !status.success() {
147                if let Some(code) = status.code() {
148                    error!("命令退出码: {}", code);
149                }
150            }
151        }
152        Err(e) => {
153            error!("执行命令失败: {}", e);
154        }
155    }
156}
157
158/// 将所有别名路径注入为当前进程的环境变量
159pub fn inject_envs_to_process(config: &YamlConfig) {
160    for (key, value) in config.collect_alias_envs() {
161        // SAFETY: 交互模式为单线程,set_var 不会引起数据竞争
162        unsafe {
163            std::env::set_var(&key, &value);
164        }
165    }
166}
167
168/// 展开字符串中的环境变量引用(支持 $VAR 和 ${VAR} 格式)
169pub fn expand_env_vars(input: &str) -> String {
170    let mut result = String::with_capacity(input.len());
171    let chars: Vec<char> = input.chars().collect();
172    let len = chars.len();
173    let mut i = 0;
174
175    while i < len {
176        if chars[i] == '$' && i + 1 < len {
177            if chars[i + 1] == '{' {
178                if let Some(end) = chars[i + 2..].iter().position(|&c| c == '}') {
179                    let var_name: String = chars[i + 2..i + 2 + end].iter().collect();
180                    if let Ok(val) = std::env::var(&var_name) {
181                        result.push_str(&val);
182                    } else {
183                        result.push_str(&input[i..i + 3 + end]);
184                    }
185                    i = i + 3 + end;
186                    continue;
187                }
188            }
189            let start = i + 1;
190            let mut end = start;
191            while end < len && (chars[end].is_alphanumeric() || chars[end] == '_') {
192                end += 1;
193            }
194            if end > start {
195                let var_name: String = chars[start..end].iter().collect();
196                if let Ok(val) = std::env::var(&var_name) {
197                    result.push_str(&val);
198                } else {
199                    let original: String = chars[i..end].iter().collect();
200                    result.push_str(&original);
201                }
202                i = end;
203                continue;
204            }
205        }
206        result.push(chars[i]);
207        i += 1;
208    }
209
210    result
211}