use colored::Colorize;
use self_update::backends::github::ReleaseList;
use self_update::cargo_crate_version;
use semver::Version;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
const REPO_OWNER: &str = "iyulab";
const REPO_NAME: &str = "unhwp";
const BIN_NAME: &str = "unhwp";
const CLI_CRATE_NAME: &str = "unhwp-cli";
struct PlatformInfo {
os_name: &'static str,
arch_name: &'static str,
target_triple: &'static str,
archive_ext: &'static str,
}
fn get_platform_info() -> PlatformInfo {
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
return PlatformInfo {
os_name: "windows",
arch_name: "x86_64",
target_triple: "x86_64-pc-windows-msvc",
archive_ext: "zip",
};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
return PlatformInfo {
os_name: "linux",
arch_name: "x86_64",
target_triple: "x86_64-unknown-linux-gnu",
archive_ext: "tar.gz",
};
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
return PlatformInfo {
os_name: "macos",
arch_name: "x86_64",
target_triple: "x86_64-apple-darwin",
archive_ext: "tar.gz",
};
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
return PlatformInfo {
os_name: "macos",
arch_name: "aarch64",
target_triple: "aarch64-apple-darwin",
archive_ext: "tar.gz",
};
#[cfg(not(any(
all(target_os = "windows", target_arch = "x86_64"),
all(target_os = "linux", target_arch = "x86_64"),
all(target_os = "macos", target_arch = "x86_64"),
all(target_os = "macos", target_arch = "aarch64"),
)))]
{
PlatformInfo {
os_name: std::env::consts::OS,
arch_name: std::env::consts::ARCH,
target_triple: "unknown",
archive_ext: "tar.gz",
}
}
}
fn get_asset_patterns(platform: &PlatformInfo, version: &str) -> Vec<String> {
let v = version.trim_start_matches('v');
vec![
format!(
"unhwp-{}-{}-v{}.{}",
platform.os_name, platform.arch_name, v, platform.archive_ext
),
format!(
"unhwp-{}-{}-{}.{}",
platform.os_name, platform.arch_name, v, platform.archive_ext
),
format!(
"unhwp-{}-v{}.{}",
platform.target_triple, v, platform.archive_ext
),
format!(
"unhwp-{}-{}.{}",
platform.target_triple, v, platform.archive_ext
),
]
}
fn find_matching_asset(asset_names: &[String], patterns: &[String]) -> Option<String> {
for pattern in patterns {
if asset_names.iter().any(|name| name == pattern) {
return Some(pattern.clone());
}
}
None
}
fn is_cargo_install() -> bool {
if let Ok(exe_path) = std::env::current_exe() {
let path_str = exe_path.to_string_lossy();
path_str.contains(".cargo") && path_str.contains("bin")
} else {
false
}
}
pub struct UpdateCheckResult {
pub has_update: bool,
pub latest_version: String,
pub current_version: String,
}
pub fn check_update_async() -> mpsc::Receiver<Option<UpdateCheckResult>> {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let result = check_latest_version();
let _ = tx.send(result);
});
rx
}
fn check_latest_version() -> Option<UpdateCheckResult> {
let current_version = cargo_crate_version!();
let releases = ReleaseList::configure()
.repo_owner(REPO_OWNER)
.repo_name(REPO_NAME)
.build()
.ok()?
.fetch()
.ok()?;
if releases.is_empty() {
return None;
}
let latest = &releases[0];
let latest_version = latest.version.trim_start_matches('v');
let current = Version::parse(current_version).ok()?;
let latest_ver = Version::parse(latest_version).ok()?;
Some(UpdateCheckResult {
has_update: latest_ver > current,
latest_version: latest_version.to_string(),
current_version: current_version.to_string(),
})
}
pub fn try_get_update_result(
rx: &mpsc::Receiver<Option<UpdateCheckResult>>,
) -> Option<UpdateCheckResult> {
rx.recv_timeout(Duration::from_millis(500)).ok().flatten()
}
pub fn print_update_notification(result: &UpdateCheckResult) {
if result.has_update {
println!();
println!(
"{} {} → {} available! Run '{}' to update.",
"Update:".yellow().bold(),
result.current_version,
result.latest_version.green(),
"unhwp update".cyan()
);
}
}
pub fn run_update(check_only: bool, force: bool) -> Result<(), Box<dyn std::error::Error>> {
let current_version = cargo_crate_version!();
println!("{} {}", "Current version:".cyan().bold(), current_version);
println!("{}", "Checking for updates...".cyan());
let releases = ReleaseList::configure()
.repo_owner(REPO_OWNER)
.repo_name(REPO_NAME)
.build()?
.fetch()?;
if releases.is_empty() {
println!("{}", "No releases found on GitHub.".yellow());
return Ok(());
}
let latest = &releases[0];
let latest_version = latest.version.trim_start_matches('v');
println!("{} {}", "Latest version:".cyan().bold(), latest_version);
let current = Version::parse(current_version)?;
let latest_ver = Version::parse(latest_version)?;
if current >= latest_ver && !force {
println!();
println!("{} You are running the latest version!", "✓".green().bold());
return Ok(());
}
if current < latest_ver {
println!();
println!(
"{} New version available: {} → {}",
"↑".yellow().bold(),
current_version.yellow(),
latest_version.green().bold()
);
}
if check_only {
println!();
if is_cargo_install() {
println!(
"Run '{}' to update.",
format!("cargo install {}", CLI_CRATE_NAME).cyan()
);
} else {
println!("Run '{}' to update.", "unhwp update".cyan());
}
return Ok(());
}
if is_cargo_install() {
println!();
println!(
"{} Installed via cargo. Please run:",
"Note:".yellow().bold()
);
println!(
" {}",
format!("cargo install {}", CLI_CRATE_NAME).cyan().bold()
);
println!();
println!(
"{}",
"This ensures proper integration with your Rust toolchain.".dimmed()
);
return Ok(());
}
println!();
println!("{}", "Downloading update...".cyan());
let platform = get_platform_info();
let patterns = get_asset_patterns(&platform, latest_version);
let asset_names: Vec<String> = latest.assets.iter().map(|a| a.name.clone()).collect();
let asset_name = find_matching_asset(&asset_names, &patterns);
if asset_name.is_none() {
println!("{}", "No matching asset found.".red());
println!("{}", "Searched for:".dimmed());
for p in &patterns {
println!(" - {}", p.dimmed());
}
println!();
println!(
"{} {}",
"Available assets:".dimmed(),
latest
.assets
.iter()
.map(|a| a.name.as_str())
.collect::<Vec<_>>()
.join(", ")
);
return Err("No compatible binary found for this platform".into());
}
let asset_name = asset_name.unwrap();
println!("{} {}", "Found asset:".dimmed(), asset_name.dimmed());
let target_asset = latest
.assets
.iter()
.find(|a| a.name == asset_name)
.ok_or("Matched asset not found in release")?;
let tmp_dir = self_update::TempDir::new()?;
let tmp_archive_path = tmp_dir.path().join(&asset_name);
let mut tmp_archive = std::fs::File::create(&tmp_archive_path)?;
let mut download = self_update::Download::from_url(&target_asset.download_url);
download.set_header(
reqwest::header::ACCEPT,
"application/octet-stream".parse().unwrap(),
);
download.show_progress(true);
download.download_to(&mut tmp_archive)?;
println!("{}", "Extracting archive...".dimmed());
let bin_name_with_ext = format!("{}{}", BIN_NAME, std::env::consts::EXE_SUFFIX);
self_update::Extract::from_source(&tmp_archive_path)
.extract_file(tmp_dir.path(), &bin_name_with_ext)?;
let new_exe = tmp_dir.path().join(&bin_name_with_ext);
self_update::self_replace::self_replace(&new_exe)?;
println!();
println!(
"{} Successfully updated to v{}!",
"✓".green().bold(),
latest_version
);
println!();
println!("Restart unhwp to use the new version.");
Ok(())
}