Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions core/xvm/src/cmdprocessor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
18 changes: 17 additions & 1 deletion core/xvm/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub fn xvm_add(matches: &ArgMatches) -> Result<()> {
let version = matches.get_one::<String>("version").context("Version is required")?;
let path = matches.get_one::<String>("path");
let alias = matches.get_one::<String>("alias");
let icon = matches.get_one::<String>("icon");

let env_vars: Vec<String> = matches
.get_many::<String>("env")
.unwrap_or_default()
Expand All @@ -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(
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions core/xvm/xvm.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[Desktop Entry]
Version=1.0
Name=xvm
Exec=xvm
Icon=
Type=Application
Terminal=false
7 changes: 7 additions & 0 deletions core/xvm/xvm.xvm.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[Desktop Entry]
Version=1.0
Name=xvm
Exec=xvm
Icon=
Type=Application
Terminal=false
150 changes: 150 additions & 0 deletions core/xvm/xvmlib/desktop.rs
Original file line number Diff line number Diff line change
@@ -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<String>, // 快捷方式的描述
pub icon_path: Option<PathBuf>, // 图标路径
pub working_dir: Option<PathBuf>, // 工作目录(仅 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(())
}
53 changes: 53 additions & 0 deletions core/xvm/xvmlib/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
}
27 changes: 26 additions & 1 deletion core/xvm/xvmlib/shims.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ pub enum Type {
pub struct Program {
name: String,
version: String,
args: Vec<String>,
// vdata
alias: Option<String>,
path: String,
icon: Option<String>,
envs: Vec<(String, String)>,
args: Vec<String>,
}

impl Program {
Expand All @@ -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(),
Expand All @@ -41,10 +44,30 @@ impl Program {
&self.version
}

pub fn bin_path(&self) -> Option<String> {
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()));
}
Expand Down Expand Up @@ -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 {
Expand All @@ -87,6 +111,7 @@ impl Program {
self.add_envs(envs.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect::<Vec<_>>().as_slice());
}
self.alias = vdata.alias.clone();
self.icon = vdata.icon.clone();
}

pub fn run(&self) {
Expand Down
2 changes: 2 additions & 0 deletions core/xvm/xvmlib/versiondb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ pub struct VData {
pub alias: Option<String>,
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub envs: Option<IndexMap<String, String>>,
}

Expand Down
Loading