use std::path::{Path, PathBuf};
use crate::targets::install::shortcut_stem;
pub fn apply_shortcut_choices(
aumid: &str,
product: &str,
main_exe: &str,
desktop: Option<bool>,
menu: Option<bool>,
dir: &str,
) -> Result<(), String> {
let dir = dir.trim().trim_end_matches('\\').to_string();
if dir.is_empty() {
return Err("the install directory must not be empty".into());
}
let exe = Path::new(&dir).join(main_exe);
if !exe.is_file() {
return Err(format!("{main_exe} not found in the install directory"));
}
let stem = shortcut_stem(product);
let primary_name = format!("{stem}.lnk");
let legacy_name = link_name(main_exe);
let legacy = (!legacy_name.eq_ignore_ascii_case(&primary_name)).then_some(legacy_name);
let mut changed = Vec::new();
let mut failures = Vec::new();
if let Some(want) = menu {
let link = start_menu_link(&stem);
if let Err(e) = apply_shortcut(
&link,
legacy.as_deref().map(start_menu_link_stem),
&exe,
want,
aumid,
&mut changed,
) {
failures.push(format!("start-menu shortcut: {e}"));
}
}
if let Some(want) = desktop {
let link = desktop_link(&stem);
if let Err(e) = apply_shortcut(
&link,
legacy.as_deref().map(desktop_link_stem),
&exe,
want,
aumid,
&mut changed,
) {
eprintln!("shun: desktop-shortcut-blocked: {e}");
}
}
notify_shell_change(&changed);
if failures.is_empty() {
Ok(())
} else {
Err(failures.join("; "))
}
}
fn link_name(main_exe: &str) -> String {
let file = Path::new(main_exe)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| main_exe.to_string());
let stem = file.strip_suffix(".exe").unwrap_or(&file);
format!("{stem}.lnk")
}
fn start_menu_link_stem(link: &str) -> PathBuf {
start_menu_dir().join(link)
}
fn start_menu_link(stem: &str) -> PathBuf {
start_menu_link_stem(&format!("{stem}.lnk"))
}
fn start_menu_dir() -> PathBuf {
std::env::var_os("APPDATA")
.map(PathBuf::from)
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default())
.join(r"Microsoft\Windows\Start Menu\Programs")
}
fn desktop_link_stem(link: &str) -> PathBuf {
desktop_dir().join(link)
}
fn desktop_link(stem: &str) -> PathBuf {
desktop_link_stem(&format!("{stem}.lnk"))
}
fn desktop_dir() -> PathBuf {
use windows_sys::Win32::UI::Shell::{FOLDERID_Desktop, SHGetKnownFolderPath};
unsafe {
let mut path = std::ptr::null_mut();
let hr = SHGetKnownFolderPath(&FOLDERID_Desktop, 0, std::ptr::null_mut(), &mut path);
if hr == 0 && !path.is_null() {
let mut len = 0usize;
while *path.add(len) != 0 {
len += 1;
}
let wide = std::slice::from_raw_parts(path, len);
let s = String::from_utf16_lossy(wide);
windows_sys::Win32::System::Com::CoTaskMemFree(path.cast());
return PathBuf::from(s);
}
}
std::env::var_os("USERPROFILE")
.map(PathBuf::from)
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default())
.join("Desktop")
}
fn apply_shortcut(
link: &Path,
legacy: Option<PathBuf>,
exe: &Path,
want: bool,
aumid: &str,
changed: &mut Vec<PathBuf>,
) -> Result<(), String> {
if want {
if !link.is_file() {
if let Some(parent) = link.parent() {
std::fs::create_dir_all(parent).map_err(|e| format!("create dir: {e}"))?;
}
mslnk::ShellLink::new(exe)
.and_then(|l| l.create_lnk(link))
.map_err(|e| format!("create shortcut: {e}"))?;
if let Err(e) = crate::targets::aumid::stamp(link, aumid) {
eprintln!("shun: AUMID stamp on {}: {e}", link.display());
}
changed.push(link.to_path_buf());
}
if let Some(legacy) = legacy
&& std::fs::remove_file(&legacy).is_ok()
{
changed.push(legacy);
}
} else {
match std::fs::remove_file(link) {
Ok(()) => changed.push(link.to_path_buf()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(format!("remove shortcut: {e}")),
}
if let Some(legacy) = legacy {
if std::fs::remove_file(&legacy).is_ok() {
changed.push(legacy);
}
}
}
Ok(())
}
fn notify_shell_change(paths: &[PathBuf]) {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::UI::Shell::{
SHCNE_ASSOCCHANGED, SHCNE_UPDATEITEM, SHCNF_PATH, SHChangeNotify,
};
unsafe {
for path in paths {
let wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
SHChangeNotify(
SHCNE_UPDATEITEM as i32,
SHCNF_PATH,
wide.as_ptr().cast(),
std::ptr::null(),
);
}
SHChangeNotify(
SHCNE_ASSOCCHANGED as i32,
0,
std::ptr::null(),
std::ptr::null(),
);
}
}