1use crate::config::YamlConfig;
2use crate::constants::{section, config_key, search_engine, shell, DEFAULT_SEARCH_ENGINE};
3use crate::{error, info};
4use std::path::Path;
5use std::process::Command;
6
7pub fn handle_open(args: &[String], config: &YamlConfig) {
10 if args.is_empty() {
11 error!("❌ 请指定要打开的别名");
12 return;
13 }
14
15 let alias = &args[0];
16
17 if !config.alias_exists(alias) {
19 error!("❌ 无法找到别名对应的路径或网址 {{{}}}。请检查配置文件。", alias);
20 return;
21 }
22
23 if config.contains(section::BROWSER, alias) {
25 handle_open_browser(args, config);
26 return;
27 }
28
29 if config.contains(section::EDITOR, alias) {
31 if args.len() == 2 {
32 let file_path = &args[1];
33 open_with_path(alias, Some(file_path), config);
34 } else {
35 open_alias(alias, config);
36 }
37 return;
38 }
39
40 if config.contains(section::VPN, alias) {
42 open_alias(alias, config);
43 return;
44 }
45
46 if config.contains(section::SCRIPT, alias) {
48 run_script(args, config);
49 return;
50 }
51
52 open_alias_with_args(alias, &args[1..], config);
54}
55
56fn handle_open_browser(args: &[String], config: &YamlConfig) {
58 let alias = &args[0];
59 if args.len() == 1 {
60 open_alias(alias, config);
62 } else {
63 let url_alias_or_text = &args[1];
65
66 let url = if let Some(u) = config.get_property(section::INNER_URL, url_alias_or_text) {
68 u.clone()
69 } else if let Some(u) = config.get_property(section::OUTER_URL, url_alias_or_text) {
70 if let Some(vpn_map) = config.get_section(section::VPN) {
72 if let Some(vpn_alias) = vpn_map.keys().next() {
73 open_alias(vpn_alias, config);
74 }
75 }
76 u.clone()
77 } else if is_url_like(url_alias_or_text) {
78 url_alias_or_text.clone()
80 } else {
81 let engine = if args.len() >= 3 {
83 args[2].as_str()
84 } else {
85 config
86 .get_property(section::SETTING, config_key::SEARCH_ENGINE)
87 .map(|s| s.as_str())
88 .unwrap_or(DEFAULT_SEARCH_ENGINE)
89 };
90 get_search_url(url_alias_or_text, engine)
91 };
92
93 open_with_path(alias, Some(&url), config);
94 }
95}
96
97const NEW_WINDOW_FLAG: &str = "-w";
99const NEW_WINDOW_FLAG_LONG: &str = "--new-window";
100
101fn run_script(args: &[String], config: &YamlConfig) {
105 let alias = &args[0];
106 if let Some(script_path) = config.get_property(section::SCRIPT, alias) {
107 let script_path = clean_path(script_path);
109
110 let new_window = args[1..].iter().any(|s| s == NEW_WINDOW_FLAG || s == NEW_WINDOW_FLAG_LONG);
112 let script_args: Vec<String> = args[1..]
113 .iter()
114 .filter(|s| s.as_str() != NEW_WINDOW_FLAG && s.as_str() != NEW_WINDOW_FLAG_LONG)
115 .map(|s| clean_path(s))
116 .collect();
117 let script_arg_refs: Vec<&str> = script_args.iter().map(|s| s.as_str()).collect();
118
119 if new_window {
120 info!("⚙️ 即将在新窗口执行脚本,路径: {}", script_path);
121 run_script_in_new_window(&script_path, &script_arg_refs, config);
122 } else {
123 info!("⚙️ 即将执行脚本,路径: {}", script_path);
124 run_script_in_current_terminal(&script_path, &script_arg_refs, config);
125 }
126 }
127}
128
129fn inject_alias_envs(cmd: &mut Command, config: &YamlConfig) {
131 for (key, value) in config.collect_alias_envs() {
132 cmd.env(&key, &value);
133 }
134}
135
136fn run_script_in_current_terminal(script_path: &str, script_args: &[&str], config: &YamlConfig) {
138 let result = if cfg!(target_os = "windows") {
139 let mut cmd = Command::new("cmd.exe");
140 cmd.arg("/c").arg(script_path).args(script_args);
141 inject_alias_envs(&mut cmd, config);
142 cmd.status()
143 } else {
144 let mut cmd = Command::new("sh");
146 cmd.arg(script_path).args(script_args);
147 inject_alias_envs(&mut cmd, config);
148 cmd.status()
149 };
150
151 match result {
152 Ok(status) => {
153 if status.success() {
154 info!("✅ 脚本执行完成");
155 } else {
156 error!("❌ 脚本执行失败,退出码: {}", status);
157 }
158 }
159 Err(e) => error!("💥 执行脚本失败: {}", e),
160 }
161}
162
163fn run_script_in_new_window(script_path: &str, script_args: &[&str], config: &YamlConfig) {
167 let os = std::env::consts::OS;
168
169 let env_exports = build_env_export_string(config);
171
172 if os == shell::MACOS_OS {
173 let script_cmd = if script_args.is_empty() {
176 format!("sh {}", shell_escape(script_path))
177 } else {
178 let args_str = script_args
179 .iter()
180 .map(|a| shell_escape(a))
181 .collect::<Vec<_>>()
182 .join(" ");
183 format!("sh {} {}", shell_escape(script_path), args_str)
184 };
185
186 let full_cmd = if env_exports.is_empty() {
187 format!("{}; exit", script_cmd)
188 } else {
189 format!("{} {}; exit", env_exports, script_cmd)
190 };
191
192 let apple_script = format!(
194 "tell application \"Terminal\"\n\
195 activate\n\
196 do script \"{}\"\n\
197 end tell",
198 full_cmd.replace('\\', "\\\\").replace('"', "\\\"")
199 );
200
201 let result = Command::new("osascript")
202 .arg("-e")
203 .arg(&apple_script)
204 .status();
205
206 match result {
207 Ok(status) => {
208 if status.success() {
209 info!("✅ 已在新终端窗口中启动脚本");
210 } else {
211 error!("❌ 启动新终端窗口失败,退出码: {}", status);
212 }
213 }
214 Err(e) => error!("💥 调用 osascript 失败: {}", e),
215 }
216 } else if os == shell::WINDOWS_OS {
217 let script_cmd = if script_args.is_empty() {
219 script_path.to_string()
220 } else {
221 format!("{} {}", script_path, script_args.join(" "))
222 };
223
224 let full_cmd = if env_exports.is_empty() {
226 script_cmd
227 } else {
228 format!("{} && {}", env_exports, script_cmd)
229 };
230
231 let result = Command::new("cmd")
232 .args(["/c", "start", "cmd", "/c", &full_cmd])
233 .status();
234
235 match result {
236 Ok(status) => {
237 if status.success() {
238 info!("✅ 已在新终端窗口中启动脚本");
239 } else {
240 error!("❌ 启动新终端窗口失败,退出码: {}", status);
241 }
242 }
243 Err(e) => error!("💥 启动新窗口失败: {}", e),
244 }
245 } else {
246 let script_cmd = if script_args.is_empty() {
249 format!("sh {}", script_path)
250 } else {
251 format!("sh {} {}", script_path, script_args.join(" "))
252 };
253
254 let full_cmd = if env_exports.is_empty() {
255 format!("{}; exit", script_cmd)
256 } else {
257 format!("{} {}; exit", env_exports, script_cmd)
258 };
259
260 let terminals = [
262 ("gnome-terminal", vec!["--", "sh", "-c", &full_cmd]),
263 ("xterm", vec!["-e", &full_cmd]),
264 ("konsole", vec!["-e", &full_cmd]),
265 ];
266
267 for (term, term_args) in &terminals {
268 if let Ok(status) = Command::new(term).args(term_args).status() {
269 if status.success() {
270 info!("✅ 已在新终端窗口中启动脚本");
271 return;
272 }
273 }
274 }
275
276 info!("⚠️ 未找到可用的终端模拟器,降级到当前终端执行");
278 run_script_in_current_terminal(script_path, script_args, config);
279 }
280}
281
282fn build_env_export_string(config: &YamlConfig) -> String {
286 let envs = config.collect_alias_envs();
287 if envs.is_empty() {
288 return String::new();
289 }
290
291 let os = std::env::consts::OS;
292 if os == shell::WINDOWS_OS {
293 envs.iter()
294 .map(|(k, v)| format!("set \"{}={}\"", k, v))
295 .collect::<Vec<_>>()
296 .join(" && ")
297 } else {
298 envs.iter()
301 .map(|(k, v)| {
302 let escaped_value = v.replace('\'', "'\\''");
304 format!("export {}='{}';", k, escaped_value)
305 })
306 .collect::<Vec<_>>()
307 .join(" ")
308 }
309}
310
311fn shell_escape(s: &str) -> String {
313 if s.contains(' ') || s.contains('"') || s.contains('\'') || s.contains('\\') {
314 format!("'{}'", s.replace('\'', "'\\''")
316 )
317 } else {
318 s.to_string()
319 }
320}
321
322fn open_alias(alias: &str, config: &YamlConfig) {
324 open_alias_with_args(alias, &[], config);
325}
326
327fn open_alias_with_args(alias: &str, extra_args: &[String], config: &YamlConfig) {
332 if let Some(path) = config.get_path_by_alias(alias) {
333 let path = clean_path(path);
334 let expanded_args: Vec<String> = extra_args.iter().map(|s| clean_path(s)).collect();
336 if is_cli_executable(&path) {
337 let result = Command::new(&path)
339 .args(&expanded_args)
340 .status();
341 match result {
342 Ok(status) => {
343 if !status.success() {
344 error!("❌ 执行 {{{}}} 失败,退出码: {}", alias, status);
345 }
346 }
347 Err(e) => error!("💥 执行 {{{}}} 失败: {}", alias, e),
348 }
349 } else {
350 if extra_args.is_empty() {
352 do_open(&path);
353 } else {
354 let os = std::env::consts::OS;
356 let result = if os == shell::MACOS_OS {
357 Command::new("open")
358 .args(["-a", &path])
359 .args(&expanded_args)
360 .status()
361 } else if os == shell::WINDOWS_OS {
362 Command::new(shell::WINDOWS_CMD)
363 .args([shell::WINDOWS_CMD_FLAG, "start", "", &path])
364 .args(&expanded_args)
365 .status()
366 } else {
367 Command::new("xdg-open").arg(&path).status()
368 };
369 if let Err(e) = result {
370 error!("💥 启动 {{{}}} 失败: {}", alias, e);
371 return;
372 }
373 }
374 info!("✅ 启动 {{{}}} : {{{}}}", alias, path);
375 }
376 } else {
377 error!("❌ 未找到别名对应的路径或网址: {}。请检查配置文件。", alias);
378 }
379}
380
381fn is_cli_executable(path: &str) -> bool {
387 if path.starts_with("http://") || path.starts_with("https://") {
389 return false;
390 }
391
392 if path.ends_with(".app") || path.contains(".app/") {
394 return false;
395 }
396
397 let p = Path::new(path);
398
399 if !p.is_file() {
401 return false;
402 }
403
404 #[cfg(unix)]
406 {
407 use std::os::unix::fs::PermissionsExt;
408 if let Ok(metadata) = p.metadata() {
409 return metadata.permissions().mode() & 0o111 != 0;
410 }
411 }
412
413 #[cfg(windows)]
415 {
416 if let Some(ext) = p.extension() {
417 let ext = ext.to_string_lossy().to_lowercase();
418 return matches!(ext.as_str(), "exe" | "cmd" | "bat" | "com");
419 }
420 }
421
422 false
423}
424
425fn open_with_path(alias: &str, file_path: Option<&str>, config: &YamlConfig) {
427 if let Some(app_path) = config.get_property(section::PATH, alias) {
428 let app_path = clean_path(app_path);
429 let os = std::env::consts::OS;
430 let file_path_expanded = file_path.map(|fp| clean_path(fp));
432 let file_path = file_path_expanded.as_deref();
433
434 let result = if os == shell::MACOS_OS {
435 match file_path {
436 Some(fp) => Command::new("open").args(["-a", &app_path, fp]).status(),
437 None => Command::new("open").arg(&app_path).status(),
438 }
439 } else if os == shell::WINDOWS_OS {
440 match file_path {
441 Some(fp) => Command::new(shell::WINDOWS_CMD)
442 .args([shell::WINDOWS_CMD_FLAG, "start", "", &app_path, fp])
443 .status(),
444 None => Command::new(shell::WINDOWS_CMD)
445 .args([shell::WINDOWS_CMD_FLAG, "start", "", &app_path])
446 .status(),
447 }
448 } else {
449 error!("💥 当前操作系统不支持此功能: {}", os);
450 return;
451 };
452
453 match result {
454 Ok(_) => {
455 let target = file_path.unwrap_or("");
456 info!("✅ 启动 {{{}}} {} : {{{}}}", alias, target, app_path);
457 }
458 Err(e) => error!("💥 启动 {} 失败: {}", alias, e),
459 }
460 } else {
461 error!("❌ 未找到别名对应的路径: {}。", alias);
462 }
463}
464
465fn do_open(path: &str) {
467 let os = std::env::consts::OS;
468 let result = if os == shell::MACOS_OS {
469 Command::new("open").arg(path).status()
470 } else if os == shell::WINDOWS_OS {
471 Command::new(shell::WINDOWS_CMD).args([shell::WINDOWS_CMD_FLAG, "start", "", path]).status()
472 } else {
473 Command::new("xdg-open").arg(path).status()
475 };
476
477 if let Err(e) = result {
478 crate::error!("💥 打开 {} 失败: {}", path, e);
479 }
480}
481
482fn clean_path(path: &str) -> String {
484 let mut path = path.trim().to_string();
485
486 if path.len() >= 2 {
488 if (path.starts_with('\'') && path.ends_with('\''))
489 || (path.starts_with('"') && path.ends_with('"'))
490 {
491 path = path[1..path.len() - 1].to_string();
492 }
493 }
494
495 path = path.replace("\\ ", " ");
497
498 if path.starts_with('~') {
500 if let Some(home) = dirs::home_dir() {
501 if path == "~" {
502 path = home.to_string_lossy().to_string();
503 } else if path.starts_with("~/") {
504 path = format!("{}{}", home.to_string_lossy(), &path[1..]);
505 }
506 }
507 }
508
509 path
510}
511
512fn is_url_like(s: &str) -> bool {
514 s.starts_with("http://") || s.starts_with("https://")
515}
516
517fn get_search_url(query: &str, engine: &str) -> String {
519 let pattern = match engine.to_lowercase().as_str() {
520 "google" => search_engine::GOOGLE,
521 "bing" => search_engine::BING,
522 "baidu" => search_engine::BAIDU,
523 _ => {
524 info!("未指定搜索引擎,使用默认搜索引擎:{}", DEFAULT_SEARCH_ENGINE);
525 search_engine::BING
526 }
527 };
528 pattern.replace("{}", query)
529}