pub mod models;
pub mod reminder;
use models::BrewInfo;
use reminder::UpdateReminder;
use semver::Version;
use std::process::Command;
use crate::config::constants::REPOSITORY;
use crate::utils::output;
use gim_config::config::update_config_value;
use toml::Value;
const VERSION: &str = env!("CARGO_PKG_VERSION");
pub fn check_update_reminder() -> Result<(), Box<dyn std::error::Error>> {
let mut reminder = UpdateReminder::load();
output::print_verbose(&format!("Checking new version on config: {}", reminder));
let to_reminder = reminder.should_show_reminder();
output::print_verbose(&format!(
"Should reminder update according to config: {}",
to_reminder
));
if to_reminder {
let check_result = new_version_available()?;
if check_result.0 {
output::print_normal(&format!(
"ℹ️ A new version is available: {}. Run 'gim update' to update.",
check_result.2
));
if let Err(e) = reminder.increment_reminder_count() {
eprintln!("Warning: Failed to update reminder status: {}", e);
}
}
}
output::print_verbose(&format!("[background] End checking new version"));
Ok(())
}
pub async fn check_update_reminder_async() {
tokio::task::spawn_blocking(|| {
if let Err(e) = check_update_reminder() {
output::print_warning(&format!("Warning: {}", e));
}
})
.await
.unwrap_or_else(|e| {
output::print_warning(&format!("Warning: Failed to check for updates: {}", e));
});
}
fn new_version_available() -> Result<(bool, Version, Version), Box<dyn std::error::Error>> {
let current_version = VERSION;
let current = semver::Version::parse(current_version)
.map_err(|_| format!("Invalid current version format: {}", current_version))?;
let latest = if cfg!(target_os = "windows") {
get_latest_version_by_scoop()?
} else {
get_latest_version_by_homebrew()?
};
output::print_verbose(&format!(
"[background] Local version: {}; Remote Version: {}",
current, latest
));
Ok((&latest > ¤t, current, latest))
}
fn get_latest_version_by_homebrew() -> Result<Version, Box<dyn std::error::Error>> {
if let Ok(repo) = String::from_utf8(Command::new("brew").arg("--repository").output()?.stdout) {
let fetch_head = format!("{}/.git/FETCH_HEAD", repo.trim());
let should_update = match std::fs::metadata(&fetch_head) {
Ok(meta) => {
let modified = meta.modified()?;
std::time::SystemTime::now().duration_since(modified)?
> std::time::Duration::from_secs(86400)
}
Err(_) => true,
};
if should_update {
output::print_verbose("Homebrew index is outdated, running 'brew update'...");
let _ = Command::new("brew").arg("update").status();
} else {
output::print_verbose("Homebrew index was updated recently.");
}
}
let output = Command::new("brew")
.args(["info", "--json=v2", REPOSITORY])
.output()?;
output::print_verbose(&format!(
"[background] run 'brew info --json=v2 {}'",
REPOSITORY
));
if !output.status.success() {
return Err("Failed to fetch version information from Homebrew".into());
}
let brew_info: BrewInfo = serde_json::from_slice(&output.stdout)
.map_err(|e| format!("Failed to parse Homebrew info: {}", e))?;
let formulae = brew_info.formulae;
let latest_version = formulae
.first()
.ok_or("No version information found in Homebrew response")?
.versions
.stable
.trim_start_matches('v');
let latest = semver::Version::parse(latest_version)
.map_err(|_| format!("Invalid version format in release: {}", latest_version))?;
Ok(latest)
}
fn get_scoop_exe() -> Result<String, Box<dyn std::error::Error>> {
let home = dirs::home_dir().ok_or("Home directory not found")?;
let scoop_exe = home.join("scoop\\shims\\scoop.cmd");
Ok(scoop_exe.to_string_lossy().to_string())
}
fn get_latest_version_by_scoop() -> Result<Version, Box<dyn std::error::Error>> {
let scoop_exe = &get_scoop_exe()?;
let update_output = Command::new(scoop_exe).args(["update"]).output();
output::print_verbose(&format!("[background] run '{} update'", scoop_exe));
if let Err(e) = update_output {
output::print_normal(&format!("Warning: Failed to update Scoop bucket: {}", e));
return Err("Skip new version checking. You may have to add 'scoop' to your PATH.".into());
}
let update_output = update_output?;
if !update_output.status.success() {
return Err("Error when running 'scoop update'".into());
}
let status_output = Command::new(scoop_exe)
.args(["status", REPOSITORY])
.output()?;
output::print_normal(&format!("run '{} status {}'", scoop_exe, REPOSITORY));
if !status_output.status.success() {
let info_output = Command::new(scoop_exe)
.args(["info", REPOSITORY])
.output()?;
output::print_normal(&format!(
"[I'm TRYing] run '{} info {}'",
scoop_exe, REPOSITORY
));
if !info_output.status.success() {
return Err("Failed to fetch version information from Scoop".into());
}
let output_str = String::from_utf8_lossy(&info_output.stdout);
let version_line = output_str
.lines()
.find(|line| line.trim().starts_with("Version:"))
.ok_or("No version information found in Scoop response")?;
let latest_version = version_line
.split(':')
.nth(1)
.ok_or("Invalid version format in Scoop response")?
.trim()
.trim_start_matches('v');
let latest = semver::Version::parse(latest_version)
.map_err(|_| format!("Invalid version format in release: {}", latest_version))?;
return Ok(latest);
}
let status_str = String::from_utf8_lossy(&status_output.stdout);
for line in status_str.lines() {
if line.contains(REPOSITORY) {
let latest_version = if let Some(arrow_pos) = line.find(" -> ") {
line[arrow_pos + 4..].trim().trim_start_matches('v')
} else {
line.split_whitespace()
.nth(2)
.ok_or(format!("Unknown version format in status: '{}'", line))?
};
let latest = semver::Version::parse(latest_version)
.map_err(|_| format!("Invalid version format in status: {}", latest_version))?;
return Ok(latest);
}
}
let info_output = Command::new(scoop_exe)
.args(["info", REPOSITORY])
.output()?;
output::print_normal(&format!(
"[CHECK DONE] run '{} info {}'",
scoop_exe, REPOSITORY
));
if !info_output.status.success() {
return Err("Failed to fetch version information from Scoop".into());
}
let output_str = String::from_utf8_lossy(&info_output.stdout);
let version_line = output_str
.lines()
.find(|line| line.trim().starts_with("Version:"))
.ok_or("No version information found in Scoop response")?;
let latest_version = version_line
.split(':')
.nth(1)
.ok_or("Invalid version format in Scoop response")?
.trim()
.trim_start_matches('v');
let latest = semver::Version::parse(latest_version)
.map_err(|_| format!("Invalid version format in release: {}", latest_version))?;
Ok(latest)
}
pub async fn check_and_install_update(force: bool) -> Result<(), Box<dyn std::error::Error>> {
let scoop_exe = &get_scoop_exe()?;
let package_manager = if cfg!(target_os = "windows") {
scoop_exe
} else {
"Homebrew"
};
output::print_normal(&format!("Checking for updates via {}...", package_manager));
let (new, current, latest) = new_version_available()?;
if !new && !force {
output::print_normal(&format!(
"You're already on the latest version: {}. Run with '--force' to update me anyway.",
current
));
if let Err(e) = UpdateReminder::load().reset_reminder() {
eprintln!("Failed to reset update reminder: {}", e);
}
return Ok(());
} else if new {
output::print_normal(&format!(
"New version available: {} (current: {})",
latest, current
));
}
output::print_normal(&format!("Upgrading via {}...", package_manager));
let status = if cfg!(target_os = "windows") {
let status = Command::new(scoop_exe)
.args(["update", REPOSITORY])
.status()?;
output::print_verbose(&format!("{} update {}", package_manager, REPOSITORY));
status
} else {
let status = Command::new("brew")
.args(["upgrade", REPOSITORY])
.status()?;
output::print_verbose(&format!("brew upgrade {}", REPOSITORY));
status
};
if !status.success() {
return Err(format!("Failed to upgrade via {}", package_manager).into());
}
output::print_normal(&format!("✅ Successfully upgraded to version: {}", latest));
if let Err(e) = UpdateReminder::load().reset_reminder() {
eprintln!("Warning: Failed to reset reminder: {}", e);
}
Ok(())
}
pub fn set_max_try(max_try: u32) -> Result<(), Box<dyn std::error::Error>> {
update_config_value("update", "max_try", Value::Integer(max_try as i64))?;
output::print_verbose(&format!("Successfully set max try count to: {}", max_try));
Ok(())
}
pub fn set_try_interval(interval: u32) -> Result<(), Box<dyn std::error::Error>> {
update_config_value(
"update",
"try_interval_days",
Value::Integer(interval as i64),
)?;
output::print_verbose(&format!(
"Successfully set try interval to: {} days",
interval
));
Ok(())
}