diff --git a/core/xvm/src/cmdprocessor.rs b/core/xvm/src/cmdprocessor.rs index 353b94d..f3a0316 100644 --- a/core/xvm/src/cmdprocessor.rs +++ b/core/xvm/src/cmdprocessor.rs @@ -62,6 +62,13 @@ fn build_command() -> Command { .action(ArgAction::Set) .help("Specify a alias for the target"), ) + .arg( + Arg::new("icon") + .long("icon") + .value_name("ICON") + .action(ArgAction::Set) + .help("Specify the icon path for the target"), + ) .arg( Arg::new("env") .long("env") diff --git a/core/xvm/src/handler.rs b/core/xvm/src/handler.rs index 68e88f4..8fc8263 100644 --- a/core/xvm/src/handler.rs +++ b/core/xvm/src/handler.rs @@ -16,6 +16,8 @@ pub fn xvm_add(matches: &ArgMatches) -> Result<()> { let version = matches.get_one::("version").context("Version is required")?; let path = matches.get_one::("path"); let alias = matches.get_one::("alias"); + let icon = matches.get_one::("icon"); + let env_vars: Vec = matches .get_many::("env") .unwrap_or_default() @@ -34,6 +36,12 @@ pub fn xvm_add(matches: &ArgMatches) -> Result<()> { program.set_alias(c); } + if let Some(i) = icon { + program.set_icon_path(i); + // create desktop shortcut + xvmlib::update_desktop_shortcut(&program); + } + if !env_vars.is_empty() { //println!("Environment variables: {:?}", env_vars); program.add_envs( @@ -77,7 +85,12 @@ pub fn xvm_remove(matches: &ArgMatches) -> Result<()> { if version.is_none() { // 检查 version 是否为 None helper::prompt(&format!("remove all versions for [{}]? (y/n): ", target.green().bold()), "y"); println!("removing..."); - vdb.remove_all_vdata(target); + let versions = vdb.get_all_version(target).unwrap_or_default(); + for version in versions { + vdb.remove_vdata(target, &version); + // remove desktop shortcut + xvmlib::remove_desktop_shortcut(target, &version); + } } else { let version = version.unwrap(); if !vdb.has_version(target, &version) { @@ -89,6 +102,9 @@ pub fn xvm_remove(matches: &ArgMatches) -> Result<()> { } println!("removing target: {}, version: {}", target.green().bold(), version.cyan()); vdb.remove_vdata(target, version); + // remove desktop shortcut + xvmlib::remove_desktop_shortcut(target, &version); + // if removed version is current version, set update flag if workspace_version == Some(version) { global_version_removed = true; diff --git a/core/xvm/xvm.desktop b/core/xvm/xvm.desktop new file mode 100755 index 0000000..ea66983 --- /dev/null +++ b/core/xvm/xvm.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Version=1.0 +Name=xvm +Exec=xvm +Icon= +Type=Application +Terminal=false diff --git a/core/xvm/xvm.xvm.desktop b/core/xvm/xvm.xvm.desktop new file mode 100755 index 0000000..ea66983 --- /dev/null +++ b/core/xvm/xvm.xvm.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Version=1.0 +Name=xvm +Exec=xvm +Icon= +Type=Application +Terminal=false diff --git a/core/xvm/xvmlib/desktop.rs b/core/xvm/xvmlib/desktop.rs new file mode 100644 index 0000000..fc7663b --- /dev/null +++ b/core/xvm/xvmlib/desktop.rs @@ -0,0 +1,150 @@ +use colored::Colorize; + +use std::fs; +use std::path::PathBuf; + +/// 描述快捷方式的选项 +pub struct ShortcutOptions { + pub name: String, // 快捷方式的显示名称 + pub exec_path: PathBuf, // 要执行的程序路径/目标路径 + pub description: Option, // 快捷方式的描述 + pub icon_path: Option, // 图标路径 + pub working_dir: Option, // 工作目录(仅 Windows 有效) + pub terminal: bool, // 是否需要终端运行(仅 Linux 有效) +} + +#[cfg(windows)] +mod windows_desktop { + use super::ShortcutOptions; + use std::path::PathBuf; + use std::process::Command; + + pub static SHORTCUT_ROOT_DIR: &str = r"C:\ProgramData\Microsoft\Windows\Start Menu\Programs"; + + pub fn shortcut_userdir() -> PathBuf { + PathBuf::from(r"C:\Users\Public\Desktop") + } + + pub fn shortcut_path_format(dir: &PathBuf, name: &str) -> PathBuf { + dir.join(format!("{}.xvm.lnk", name)) + } + + pub fn create_shortcut(options: ShortcutOptions, shortcut_dir: &PathBuf) -> Result<(), String> { + let shortcut_path = shortcut_path_format(shortcut_dir, &options.name); + + // 构建 PowerShell 脚本命令 + let mut command = format!( + r#" + $WshShell = New-Object -ComObject WScript.Shell; + $Shortcut = $WshShell.CreateShortcut('{}'); + $Shortcut.TargetPath = '{}'; + "#, + shortcut_path.display(), + options.exec_path.display() + ); + + // 设置可选字段 + if let Some(description) = &options.description { + command.push_str(&format!(r"$Shortcut.Description = '{}';", description)); + } + if let Some(working_dir) = &options.working_dir { + command.push_str(&format!(r"$Shortcut.WorkingDirectory = '{}';", working_dir.display())); + } + if let Some(icon_path) = &options.icon_path { + command.push_str(&format!(r"$Shortcut.IconLocation = '{}';", icon_path.display())); + } + + // 保存快捷方式 + command.push_str(r"$Shortcut.Save();"); + + // 调用 PowerShell 执行命令 + let status = Command::new("powershell") + .arg("-Command") + .arg(command) + .status() + .map_err(|e| format!("Failed to execute PowerShell command: {:?}", e))?; + + if status.success() { + Ok(()) + } else { + Err(format!("PowerShell returned non-zero exit code: {:?}", status)) + } + } +} + +#[cfg(unix)] +mod linux_desktop { + use super::ShortcutOptions; + use std::fs::File; + use std::io::Write; + use std::os::unix::fs::PermissionsExt; + use std::path::PathBuf; + + pub static SHORTCUT_ROOT_DIR: &str = "/usr/share/applications"; + + pub fn shortcut_userdir() -> PathBuf { + let homedir = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); + PathBuf::from(homedir).join(".local/share/applications") + } + + pub fn shortcut_path_format(dir: &PathBuf, name: &str) -> PathBuf { + dir.join(format!("{}.xvm.desktop", name)) + } + + pub fn create_shortcut(options: ShortcutOptions, shortcut_dir: &PathBuf) -> Result<(), String> { + // Create .desktop file content + let desktop_entry = format!( + r#"[Desktop Entry] +Version=1.0 +Name={} +Exec={} +Icon={} +Type=Application +Terminal={} +"#, + options.name, + options.exec_path.display(), + options.icon_path + .as_ref() + .map(|p| p.display().to_string()) + .unwrap_or_else(|| "".to_string()), + if options.terminal { "true" } else { "false" } + ); + + let path = shortcut_path_format(shortcut_dir, &options.name); + + // Write to the .desktop file + let mut file = File::create(&path).map_err(|e| format!("Failed to create file: {:?}", e))?; + file.write_all(desktop_entry.as_bytes()) + .map_err(|e| format!("Failed to write to file: {:?}", e))?; + + // Set executable permissions + let permissions = std::fs::Permissions::from_mode(0o755); + std::fs::set_permissions(&path, permissions).map_err(|e| format!("Failed to set permissions: {:?}", e))?; + + // Refresh desktop database + let _ = std::process::Command::new("update-desktop-database") + .arg(shortcut_dir) + .output() + .map_err(|e| format!("Failed to update desktop database: {:?}", e))?; + + Ok(()) + } +} + +#[cfg(windows)] +pub use windows_desktop::{create_shortcut, SHORTCUT_ROOT_DIR, shortcut_path_format, shortcut_userdir}; + +#[cfg(unix)] +pub use linux_desktop::{create_shortcut, SHORTCUT_ROOT_DIR, shortcut_path_format, shortcut_userdir}; + +pub fn delete_shortcut(dir: &PathBuf, name: &str) -> Result<(), String> { + let path = shortcut_path_format(dir, name); + if path.exists() { + println!("try to remove [{}] desktop shortcut...", name.green()); + fs::remove_file(&path).map_err(|e| format!("Failed to delete shortcut: {:?}", e))?; + println!("shortcut deleted: {}", path.display()); + } + + Ok(()) +} \ No newline at end of file diff --git a/core/xvm/xvmlib/lib.rs b/core/xvm/xvmlib/lib.rs index c284cbc..21d2d72 100644 --- a/core/xvm/xvmlib/lib.rs +++ b/core/xvm/xvmlib/lib.rs @@ -4,12 +4,14 @@ mod workspace; use std::fs; use std::sync::OnceLock; +use std::path::PathBuf; use colored::*; // public api pub mod shims; +pub mod desktop; pub use versiondb::VersionDB; pub use workspace::Workspace; @@ -66,4 +68,55 @@ pub fn get_versiondb() -> &'static VersionDB { pub fn get_global_workspace() -> &'static Workspace { GLOBAL_WORKSPACE.get().expect("Global Workspace not initialized") +} + +pub fn update_desktop_shortcut(program: &shims::Program) { + + let shortcut_name = format!("{} {}", program.name(), program.version()); + + println!("update desktop shortcut for [{}]...", shortcut_name.green()); + + let icon_path = if let Some(icon) = program.icon_path() { + PathBuf::from(icon) + } else { + return; + }; + + if icon_path.exists() { + let desktop_dir = desktop::shortcut_userdir(); + + if !desktop_dir.exists() { + println!("create desktop shortcut directory: {}", desktop_dir.display()); + fs::create_dir_all(&desktop_dir).expect("Failed to create desktop shortcut directory"); + } + + let exec_path = if let Some(epath) = program.bin_path() { + PathBuf::from(epath) + } else { + // maybe is a alias + return; + }; + + // check exec_path exists + if exec_path.exists() { + let options = desktop::ShortcutOptions { + name: shortcut_name, + exec_path: exec_path.clone(), + icon_path: Some(icon_path), + terminal: false, + working_dir: Some(exec_path.parent().unwrap().to_path_buf()), + description: None, + }; + desktop::create_shortcut(options, &desktop_dir).expect("Failed to create desktop shortcut"); + } else { + println!("Program not found: {}", exec_path.display()); + } + } else { + println!("Icon not found: {}", icon_path.display()); + } +} + +pub fn remove_desktop_shortcut(target: &str, version: &str) { + let desktop_dir = desktop::shortcut_userdir(); + desktop::delete_shortcut(&desktop_dir, &format!("{} {}", target, version)).expect("Failed to remove desktop shortcut"); } \ No newline at end of file diff --git a/core/xvm/xvmlib/shims.rs b/core/xvm/xvmlib/shims.rs index e887810..234d61c 100644 --- a/core/xvm/xvmlib/shims.rs +++ b/core/xvm/xvmlib/shims.rs @@ -15,10 +15,12 @@ pub enum Type { pub struct Program { name: String, version: String, + args: Vec, + // vdata alias: Option, path: String, + icon: Option, envs: Vec<(String, String)>, - args: Vec, } impl Program { @@ -27,6 +29,7 @@ impl Program { name: name.to_string(), version: version.to_string(), alias: None, + icon: None, path: String::new(), envs: Vec::new(), args: Vec::new(), @@ -41,10 +44,30 @@ impl Program { &self.version } + pub fn bin_path(&self) -> Option { + if !self.path.is_empty() { + if let Some(alias) = &self.alias { + Some(format!("{}/{}", self.path, alias)) + } else { + Some(format!("{}/{}", self.path, self.name)) + } + } else { + None + } + } + + pub fn icon_path(&self) -> Option<&String> { + self.icon.as_ref() + } + pub fn set_alias(&mut self, alias: &str) { self.alias = Some(alias.to_string()); } + pub fn set_icon_path(&mut self, icon: &str) { + self.icon = Some(icon.to_string()); + } + pub fn add_env(&mut self, key: &str, value: &str) { self.envs.push((key.to_string(), value.to_string())); } @@ -73,6 +96,7 @@ impl Program { VData { alias: self.alias.clone(), path: self.path.clone(), + icon: self.icon.clone(), envs: if self.envs.is_empty() { None } else { @@ -87,6 +111,7 @@ impl Program { self.add_envs(envs.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect::>().as_slice()); } self.alias = vdata.alias.clone(); + self.icon = vdata.icon.clone(); } pub fn run(&self) { diff --git a/core/xvm/xvmlib/versiondb.rs b/core/xvm/xvmlib/versiondb.rs index 1152273..50c5975 100644 --- a/core/xvm/xvmlib/versiondb.rs +++ b/core/xvm/xvmlib/versiondb.rs @@ -8,6 +8,8 @@ pub struct VData { pub alias: Option, pub path: String, #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub envs: Option>, }