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