mod install;
use std::sync::{Arc, Mutex};
use std::thread;
use self_update::backends::github::ReleaseList;
pub use install::Relaunch;
const OWNER: &str = "manimovassagh";
const REPO: &str = "Ferrodock";
pub const CURRENT: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Available {
pub version: String,
pub notes: String,
asset_name: String,
asset_url: String,
sums_url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum Status {
#[default]
Idle,
Checking,
UpToDate,
Available(Available),
Downloading {
version: String,
done: u64,
total: Option<u64>,
},
Ready {
version: String,
relaunch: Relaunch,
},
Failed(String),
}
#[derive(Clone)]
pub struct Updater {
status: Arc<Mutex<Status>>,
repaint: Arc<dyn Fn() + Send + Sync>,
}
impl Updater {
pub fn new(repaint: impl Fn() + Send + Sync + 'static) -> Self {
Self {
status: Arc::new(Mutex::new(Status::Idle)),
repaint: Arc::new(repaint),
}
}
#[cfg(test)]
pub fn pretend(&self, status: Status) {
self.set(status);
}
pub fn status(&self) -> Status {
self.status.lock().map(|s| s.clone()).unwrap_or_default()
}
fn set(&self, status: Status) {
if let Ok(mut current) = self.status.lock() {
*current = status;
}
(self.repaint)();
}
fn busy(&self) -> bool {
matches!(
self.status(),
Status::Checking | Status::Downloading { .. } | Status::Ready { .. }
)
}
pub fn check(&self) {
if self.busy() {
return;
}
self.set(Status::Checking);
let this = self.clone();
thread::spawn(move || {
let status = match latest() {
Ok(Some(available)) => Status::Available(available),
Ok(None) => Status::UpToDate,
Err(e) => Status::Failed(format!("Couldn't check for updates: {e}")),
};
this.set(status);
});
}
pub fn install(&self, available: Available) {
if self.busy() {
return;
}
let this = self.clone();
thread::spawn(move || {
let version = available.version.clone();
let progress = {
let this = this.clone();
let version = version.clone();
move |done, total| {
this.set(Status::Downloading {
version: version.clone(),
done,
total,
})
}
};
progress(0, None);
let status = match install::install(&available, progress) {
Ok(relaunch) => Status::Ready { version, relaunch },
Err(e) => Status::Failed(format!("Couldn't update: {e}")),
};
this.set(status);
});
}
}
#[cfg(test)]
impl Available {
pub fn example(version: &str) -> Self {
Self {
version: version.into(),
notes: "### Added\n- In-app updates".into(),
asset_name: String::new(),
asset_url: String::new(),
sums_url: None,
}
}
}
fn asset_suffix() -> Option<&'static str> {
if cfg!(target_os = "macos") {
Some("-macos-universal.zip")
} else if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
Some("-windows-x64.zip")
} else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
Some("-linux-x64.tar.gz")
} else {
None
}
}
fn latest() -> Result<Option<Available>, String> {
let suffix = asset_suffix().ok_or("there are no prebuilt updates for this platform")?;
let releases = ReleaseList::configure()
.repo_owner(OWNER)
.repo_name(REPO)
.build()
.and_then(|list| list.fetch())
.map_err(|e| e.to_string())?
.into_vec();
let newest = releases
.into_iter()
.filter(|r| !r.version().contains('-'))
.find(|r| r.assets().iter().any(|a| a.name().ends_with(suffix)));
let Some(release) = newest else {
return Ok(None);
};
if !is_newer(CURRENT, release.version()) {
return Ok(None);
}
let asset = release
.assets()
.iter()
.find(|a| a.name().ends_with(suffix))
.expect("filtered above");
let sums_url = release
.assets()
.iter()
.find(|a| a.name() == "SHA256SUMS.txt")
.map(|a| a.download_url().to_owned());
Ok(Some(Available {
version: release.version().to_owned(),
notes: release.body().unwrap_or_default().to_owned(),
asset_name: asset.name().to_owned(),
asset_url: asset.download_url().to_owned(),
sums_url,
}))
}
fn is_newer(current: &str, other: &str) -> bool {
self_update::version::bump_is_greater(current, other).unwrap_or(false)
}
pub fn installed_with_homebrew() -> bool {
cfg!(target_os = "macos")
&& [
"/opt/homebrew/Caskroom/ferrodock",
"/usr/local/Caskroom/ferrodock",
]
.iter()
.any(|p| std::path::Path::new(p).exists())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compares_versions() {
assert!(is_newer("0.5.0", "1.0.0"));
assert!(is_newer("0.5.0", "0.5.1"));
assert!(!is_newer("1.0.0", "1.0.0"));
assert!(!is_newer("1.0.0", "0.9.9"));
}
#[test]
fn has_an_asset_for_supported_platforms() {
if cfg!(any(
target_os = "macos",
target_os = "linux",
target_os = "windows"
)) {
assert!(asset_suffix().is_some());
}
}
#[test]
#[ignore]
fn live_finds_releases() {
let releases = ReleaseList::configure()
.repo_owner(OWNER)
.repo_name(REPO)
.build()
.unwrap()
.fetch()
.unwrap()
.into_vec();
assert!(!releases.is_empty());
let suffix = asset_suffix().unwrap();
assert!(
releases
.iter()
.any(|r| r.assets().iter().any(|a| a.name().ends_with(suffix)))
);
}
}