use crate::config::YamlConfig;
use crate::constants::{
DEFAULT_SEARCH_ENGINE, NEW_WINDOW_FLAG, NEW_WINDOW_FLAG_LONG, config_key, search_engine,
section, shell,
};
use crate::{error, info};
use std::path::Path;
use std::process::Command;
pub fn handle_open(args: &[String], config: &YamlConfig) {
if args.is_empty() {
error!("✖️ 请指定要打开的别名");
return;
}
let alias = &args[0];
if !config.alias_exists(alias) {
error!(
"✖️ 无法找到别名对应的路径或网址 {{{}}}。请检查配置文件。",
alias
);
return;
}
if config.contains(section::BROWSER, alias) {
handle_open_browser(args, config);
return;
}
if config.contains(section::EDITOR, alias) {
if args.len() == 2 {
let file_path = &args[1];
open_with_path(alias, Some(file_path), config);
} else {
open_alias(alias, config);
}
return;
}
if config.contains(section::VPN, alias) {
open_alias(alias, config);
return;
}
if config.contains(section::SCRIPT, alias) {
run_script(args, config);
return;
}
open_alias_with_args(alias, &args[1..], config);
}
fn handle_open_browser(args: &[String], config: &YamlConfig) {
let alias = &args[0];
if args.len() == 1 {
open_alias(alias, config);
} else {
let url_alias_or_text = &args[1];
let url = if let Some(u) = config.get_property(section::INNER_URL, url_alias_or_text) {
u.clone()
} else if let Some(u) = config.get_property(section::OUTER_URL, url_alias_or_text) {
if let Some(vpn_map) = config.get_section(section::VPN)
&& let Some(vpn_alias) = vpn_map.keys().next()
{
open_alias(vpn_alias, config);
}
u.clone()
} else if is_url_like(url_alias_or_text) {
url_alias_or_text.clone()
} else {
let engine = if args.len() >= 3 {
args[2].as_str()
} else {
config
.get_property(section::SETTING, config_key::SEARCH_ENGINE)
.map(|s| s.as_str())
.unwrap_or(DEFAULT_SEARCH_ENGINE)
};
get_search_url(url_alias_or_text, engine)
};
open_with_path(alias, Some(&url), config);
}
}
fn run_script(args: &[String], config: &YamlConfig) {
let alias = &args[0];
if let Some(script_path) = config.get_property(section::SCRIPT, alias) {
let script_path = clean_path(script_path);
let new_window = args[1..]
.iter()
.any(|s| s == NEW_WINDOW_FLAG || s == NEW_WINDOW_FLAG_LONG);
let script_args: Vec<String> = args[1..]
.iter()
.filter(|s| s.as_str() != NEW_WINDOW_FLAG && s.as_str() != NEW_WINDOW_FLAG_LONG)
.map(|s| clean_path(s))
.collect();
let script_arg_refs: Vec<&str> = script_args.iter().map(|s| s.as_str()).collect();
if new_window {
info!("⚙️ 即将在新窗口执行脚本,路径: {}", script_path);
run_script_in_new_window(&script_path, &script_arg_refs, config);
} else {
info!("⚙️ 即将执行脚本,路径: {}", script_path);
run_script_in_current_terminal(&script_path, &script_arg_refs, config);
}
}
}
fn inject_alias_envs(cmd: &mut Command, config: &YamlConfig) {
for (key, value) in config.collect_alias_envs() {
cmd.env(&key, &value);
}
}
fn run_script_in_current_terminal(script_path: &str, script_args: &[&str], config: &YamlConfig) {
let result = if cfg!(target_os = "windows") {
let mut cmd = Command::new("cmd.exe");
cmd.arg("/c").arg(script_path).args(script_args);
inject_alias_envs(&mut cmd, config);
cmd.status()
} else {
let mut cmd = Command::new("sh");
cmd.arg(script_path).args(script_args);
inject_alias_envs(&mut cmd, config);
cmd.status()
};
match result {
Ok(status) => {
if status.success() {
info!("☑️ 脚本执行完成");
} else {
error!("✖️ 脚本执行失败,退出码: {}", status);
}
}
Err(e) => error!("💥 执行脚本失败: {}", e),
}
}
fn run_script_in_new_window(script_path: &str, script_args: &[&str], config: &YamlConfig) {
let os = std::env::consts::OS;
let env_exports = build_env_export_string(config);
if os == shell::MACOS_OS {
let script_cmd = if script_args.is_empty() {
format!("sh {}", shell_escape(script_path))
} else {
let args_str = script_args
.iter()
.map(|a| shell_escape(a))
.collect::<Vec<_>>()
.join(" ");
format!("sh {} {}", shell_escape(script_path), args_str)
};
let full_cmd = if env_exports.is_empty() {
format!("{}; exit", script_cmd)
} else {
format!("{} {}; exit", env_exports, script_cmd)
};
let apple_script = format!(
"tell application \"Terminal\"\n\
activate\n\
do script \"{}\"\n\
end tell",
full_cmd.replace('\\', "\\\\").replace('"', "\\\"")
);
let result = Command::new("osascript")
.arg("-e")
.arg(&apple_script)
.status();
match result {
Ok(status) => {
if status.success() {
info!("☑️ 已在新终端窗口中启动脚本");
} else {
error!("✖️ 启动新终端窗口失败,退出码: {}", status);
}
}
Err(e) => error!("💥 调用 osascript 失败: {}", e),
}
} else if os == shell::WINDOWS_OS {
let script_cmd = if script_args.is_empty() {
script_path.to_string()
} else {
format!("{} {}", script_path, script_args.join(" "))
};
let full_cmd = if env_exports.is_empty() {
script_cmd
} else {
format!("{} && {}", env_exports, script_cmd)
};
let result = Command::new("cmd")
.args(["/c", "start", "cmd", "/c", &full_cmd])
.status();
match result {
Ok(status) => {
if status.success() {
info!("☑️ 已在新终端窗口中启动脚本");
} else {
error!("✖️ 启动新终端窗口失败,退出码: {}", status);
}
}
Err(e) => error!("💥 启动新窗口失败: {}", e),
}
} else {
let script_cmd = if script_args.is_empty() {
format!("sh {}", script_path)
} else {
format!("sh {} {}", script_path, script_args.join(" "))
};
let full_cmd = if env_exports.is_empty() {
format!("{}; exit", script_cmd)
} else {
format!("{} {}; exit", env_exports, script_cmd)
};
let terminals = [
("gnome-terminal", vec!["--", "sh", "-c", &full_cmd]),
("xterm", vec!["-e", &full_cmd]),
("konsole", vec!["-e", &full_cmd]),
];
for (term, term_args) in &terminals {
if let Ok(status) = Command::new(term).args(term_args).status()
&& status.success()
{
info!("☑️ 已在新终端窗口中启动脚本");
return;
}
}
info!("⚠️ 未找到可用的终端模拟器,降级到当前终端执行");
run_script_in_current_terminal(script_path, script_args, config);
}
}
fn build_env_export_string(config: &YamlConfig) -> String {
let envs = config.collect_alias_envs();
if envs.is_empty() {
return String::new();
}
let os = std::env::consts::OS;
if os == shell::WINDOWS_OS {
envs.iter()
.map(|(k, v)| format!("set \"{}={}\"", k, v))
.collect::<Vec<_>>()
.join(" && ")
} else {
envs.iter()
.map(|(k, v)| {
let escaped_value = v.replace('\'', "'\\''");
format!("export {}='{}';", k, escaped_value)
})
.collect::<Vec<_>>()
.join(" ")
}
}
fn shell_escape(s: &str) -> String {
if s.contains(' ') || s.contains('"') || s.contains('\'') || s.contains('\\') {
format!("'{}'", s.replace('\'', "'\\''"))
} else {
s.to_string()
}
}
fn open_alias(alias: &str, config: &YamlConfig) {
open_alias_with_args(alias, &[], config);
}
fn open_alias_with_args(alias: &str, extra_args: &[String], config: &YamlConfig) {
if let Some(path) = config.get_path_by_alias(alias) {
let path = clean_path(path);
let expanded_args: Vec<String> = extra_args.iter().map(|s| clean_path(s)).collect();
if is_cli_executable(&path) {
let result = Command::new(&path).args(&expanded_args).status();
match result {
Ok(status) => {
if !status.success() {
error!("✖️ 执行 {{{}}} 失败,退出码: {}", alias, status);
}
}
Err(e) => error!("💥 执行 {{{}}} 失败: {}", alias, e),
}
} else {
if extra_args.is_empty() {
do_open(&path);
} else {
let os = std::env::consts::OS;
let result = if os == shell::MACOS_OS {
Command::new("open")
.args(["-a", &path])
.args(&expanded_args)
.status()
} else if os == shell::WINDOWS_OS {
Command::new(shell::WINDOWS_CMD)
.args([shell::WINDOWS_CMD_FLAG, "start", "", &path])
.args(&expanded_args)
.status()
} else {
Command::new("xdg-open").arg(&path).status()
};
if let Err(e) = result {
error!("💥 启动 {{{}}} 失败: {}", alias, e);
return;
}
}
info!("☑️ 启动 {{{}}} : {{{}}}", alias, path);
}
} else {
error!("✖️ 未找到别名对应的路径或网址: {}。请检查配置文件。", alias);
}
}
fn is_cli_executable(path: &str) -> bool {
if path.starts_with("http://") || path.starts_with("https://") {
return false;
}
if path.ends_with(".app") || path.contains(".app/") {
return false;
}
let p = Path::new(path);
if !p.is_file() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = p.metadata() {
return metadata.permissions().mode() & 0o111 != 0;
}
}
#[cfg(windows)]
{
if let Some(ext) = p.extension() {
let ext = ext.to_string_lossy().to_lowercase();
return matches!(ext.as_str(), "exe" | "cmd" | "bat" | "com");
}
}
false
}
fn open_with_path(alias: &str, file_path: Option<&str>, config: &YamlConfig) {
if let Some(app_path) = config.get_property(section::PATH, alias) {
let app_path = clean_path(app_path);
let os = std::env::consts::OS;
let file_path_expanded = file_path.map(clean_path);
let file_path = file_path_expanded.as_deref();
let result = if os == shell::MACOS_OS {
match file_path {
Some(fp) => Command::new("open").args(["-a", &app_path, fp]).status(),
None => Command::new("open").arg(&app_path).status(),
}
} else if os == shell::WINDOWS_OS {
match file_path {
Some(fp) => Command::new(shell::WINDOWS_CMD)
.args([shell::WINDOWS_CMD_FLAG, "start", "", &app_path, fp])
.status(),
None => Command::new(shell::WINDOWS_CMD)
.args([shell::WINDOWS_CMD_FLAG, "start", "", &app_path])
.status(),
}
} else {
error!("💥 当前操作系统不支持此功能: {}", os);
return;
};
match result {
Ok(_) => {
let target = file_path.unwrap_or("");
info!("☑️ 启动 {{{}}} {} : {{{}}}", alias, target, app_path);
}
Err(e) => error!("💥 启动 {} 失败: {}", alias, e),
}
} else {
error!("✖️ 未找到别名对应的路径: {}。", alias);
}
}
fn do_open(path: &str) {
let os = std::env::consts::OS;
let result = if os == shell::MACOS_OS {
Command::new("open").arg(path).status()
} else if os == shell::WINDOWS_OS {
Command::new(shell::WINDOWS_CMD)
.args([shell::WINDOWS_CMD_FLAG, "start", "", path])
.status()
} else {
Command::new("xdg-open").arg(path).status()
};
if let Err(e) = result {
crate::error!("💥 打开 {} 失败: {}", path, e);
}
}
fn clean_path(path: &str) -> String {
let mut path = path.trim().to_string();
if path.len() >= 2
&& ((path.starts_with('\'') && path.ends_with('\''))
|| (path.starts_with('"') && path.ends_with('"')))
{
path = path[1..path.len() - 1].to_string();
}
path = path.replace("\\ ", " ");
if path.starts_with('~')
&& let Some(home) = dirs::home_dir()
{
if path == "~" {
path = home.to_string_lossy().to_string();
} else if path.starts_with("~/") {
path = format!("{}{}", home.to_string_lossy(), &path[1..]);
}
}
path
}
fn is_url_like(s: &str) -> bool {
s.starts_with("http://") || s.starts_with("https://")
}
fn get_search_url(query: &str, engine: &str) -> String {
let pattern = match engine.to_lowercase().as_str() {
"google" => search_engine::GOOGLE,
"bing" => search_engine::BING,
"baidu" => search_engine::BAIDU,
_ => {
info!(
"未指定搜索引擎,使用默认搜索引擎:{}",
DEFAULT_SEARCH_ENGINE
);
search_engine::BING
}
};
pattern.replace("{}", query)
}