diff --git a/src/localization/locales/de.toml b/src/localization/locales/de.toml index 3c18af66..5583a39e 100644 --- a/src/localization/locales/de.toml +++ b/src/localization/locales/de.toml @@ -34,6 +34,8 @@ applying_update = "Update wird installiert..." update_to = "Aktualisieren auf" update_available = "Update verfügbar" update_prompt_now = "Version {version} ist verfügbar. Möchten Sie jetzt aktualisieren?" +update_pending_winget = "Version {version} wurde veröffentlicht, ist aber noch nicht über WinGet verfügbar. Bitte versuchen Sie es später erneut." +update_prompt_winget_behind = "Version {release} wurde veröffentlicht, ist aber noch nicht über WinGet verfügbar. Version {version} ist verfügbar. Möchten Sie jetzt aktualisieren?" exit = "Beenden" session_window = "5h" weekly_window = "7d" diff --git a/src/localization/locales/en.toml b/src/localization/locales/en.toml index b809ef30..8dddb461 100644 --- a/src/localization/locales/en.toml +++ b/src/localization/locales/en.toml @@ -34,6 +34,8 @@ applying_update = "Applying update..." update_to = "Update to" update_available = "Update available" update_prompt_now = "Version {version} is available. Do you want to update now?" +update_pending_winget = "Version {version} has been released but is not yet available on WinGet. Please try again later." +update_prompt_winget_behind = "Version {release} has been released but is not yet available on WinGet. Version {version} is available. Do you want to update now?" exit = "Exit" session_window = "5h" weekly_window = "7d" diff --git a/src/localization/locales/es.toml b/src/localization/locales/es.toml index 2153ca01..238e97de 100644 --- a/src/localization/locales/es.toml +++ b/src/localization/locales/es.toml @@ -34,6 +34,8 @@ applying_update = "Aplicando actualización..." update_to = "Actualizar a" update_available = "Actualización disponible" update_prompt_now = "La versión {version} está disponible. ¿Quieres actualizar ahora?" +update_pending_winget = "La versión {version} se ha publicado, pero aún no está disponible en WinGet. Inténtalo de nuevo más tarde." +update_prompt_winget_behind = "La versión {release} se ha publicado, pero aún no está disponible en WinGet. La versión {version} está disponible. ¿Quieres actualizar ahora?" exit = "Salir" session_window = "5h" weekly_window = "7d" diff --git a/src/localization/locales/fr.toml b/src/localization/locales/fr.toml index a060ff1e..ff7bfd31 100644 --- a/src/localization/locales/fr.toml +++ b/src/localization/locales/fr.toml @@ -34,6 +34,8 @@ applying_update = "Application de la mise à jour..." update_to = "Mettre à jour vers" update_available = "Mise à jour disponible" update_prompt_now = "La version {version} est disponible. Voulez-vous mettre à jour maintenant ?" +update_pending_winget = "La version {version} a été publiée mais n’est pas encore disponible sur WinGet. Veuillez réessayer plus tard." +update_prompt_winget_behind = "La version {release} a été publiée mais n’est pas encore disponible sur WinGet. La version {version} est disponible. Voulez-vous mettre à jour maintenant ?" exit = "Quitter" session_window = "5h" weekly_window = "7d" diff --git a/src/localization/locales/ja.toml b/src/localization/locales/ja.toml index 343c55ec..a6e5d969 100644 --- a/src/localization/locales/ja.toml +++ b/src/localization/locales/ja.toml @@ -36,6 +36,8 @@ applying_update = "更新を適用しています..." update_to = "更新先" update_available = "更新が利用可能です" update_prompt_now = "バージョン {version} が利用可能です。今すぐ更新しますか?" +update_pending_winget = "バージョン {version} はリリースされましたが、WinGet ではまだ利用できません。しばらくしてから再度お試しください。" +update_prompt_winget_behind = "バージョン {release} はリリースされましたが、WinGet ではまだ利用できません。バージョン {version} が利用可能です。今すぐ更新しますか?" exit = "終了" session_window = "5h" weekly_window = "7d" diff --git a/src/localization/locales/ko.toml b/src/localization/locales/ko.toml index a40d431d..cee44f4e 100644 --- a/src/localization/locales/ko.toml +++ b/src/localization/locales/ko.toml @@ -36,6 +36,8 @@ applying_update = "업데이트 적용 중..." update_to = "업데이트 대상" update_available = "업데이트 사용 가능" update_prompt_now = "버전 {version}을 사용할 수 있습니다. 지금 업데이트하시겠습니까?" +update_pending_winget = "버전 {version}이 출시되었지만 아직 WinGet에서 사용할 수 없습니다. 나중에 다시 시도하세요." +update_prompt_winget_behind = "버전 {release}이 출시되었지만 아직 WinGet에서 사용할 수 없습니다. 버전 {version}을 사용할 수 있습니다. 지금 업데이트하시겠습니까?" exit = "종료" session_window = "5시간" weekly_window = "7일" diff --git a/src/localization/locales/nl.toml b/src/localization/locales/nl.toml index c4275a3a..cf3a8cfd 100644 --- a/src/localization/locales/nl.toml +++ b/src/localization/locales/nl.toml @@ -34,6 +34,8 @@ applying_update = "Update wordt toegepast..." update_to = "Bijwerken naar" update_available = "Update beschikbaar" update_prompt_now = "Versie {version} is beschikbaar. Wil je nu bijwerken?" +update_pending_winget = "Versie {version} is uitgebracht, maar is nog niet beschikbaar via WinGet. Probeer het later opnieuw." +update_prompt_winget_behind = "Versie {release} is uitgebracht, maar is nog niet beschikbaar via WinGet. Versie {version} is beschikbaar. Wil je nu bijwerken?" exit = "Afsluiten" session_window = "5u" weekly_window = "7d" diff --git a/src/localization/locales/pl.toml b/src/localization/locales/pl.toml index 3148ac61..e043b46a 100644 --- a/src/localization/locales/pl.toml +++ b/src/localization/locales/pl.toml @@ -34,6 +34,8 @@ applying_update = "Instalowanie aktualizacji..." update_to = "Zaktualizuj do" update_available = "Dostępna aktualizacja" update_prompt_now = "Wersja {version} jest dostępna. Czy chcesz ją zainstalować teraz?" +update_pending_winget = "Wersja {version} została wydana, ale nie jest jeszcze dostępna w WinGet. Spróbuj ponownie później." +update_prompt_winget_behind = "Wersja {release} została wydana, ale nie jest jeszcze dostępna w WinGet. Wersja {version} jest dostępna. Czy chcesz ją zainstalować teraz?" exit = "Zakończ" session_window = "5h" weekly_window = "7d" diff --git a/src/localization/locales/pt-BR.toml b/src/localization/locales/pt-BR.toml index bb2b6dd5..8af21274 100644 --- a/src/localization/locales/pt-BR.toml +++ b/src/localization/locales/pt-BR.toml @@ -34,6 +34,8 @@ applying_update = "Aplicando atualização..." update_to = "Atualizar para" update_available = "Atualização disponível" update_prompt_now = "Versão {version} está disponível. Deseja atualizar agora?" +update_pending_winget = "A versão {version} foi lançada, mas ainda não está disponível no WinGet. Tente novamente mais tarde." +update_prompt_winget_behind = "A versão {release} foi lançada, mas ainda não está disponível no WinGet. A versão {version} está disponível. Deseja atualizar agora?" exit = "Sair" session_window = "5h" weekly_window = "7d" diff --git a/src/localization/locales/ru.toml b/src/localization/locales/ru.toml index 6c89dee7..848a7673 100644 --- a/src/localization/locales/ru.toml +++ b/src/localization/locales/ru.toml @@ -34,6 +34,8 @@ applying_update = "Применение обновления..." update_to = "Обновить до" update_available = "Доступно обновление" update_prompt_now = "Доступна версия {version}. Обновить сейчас?" +update_pending_winget = "Версия {version} выпущена, но пока недоступна в WinGet. Повторите попытку позже." +update_prompt_winget_behind = "Версия {release} выпущена, но пока недоступна в WinGet. Доступна версия {version}. Обновить сейчас?" exit = "Выход" session_window = "5ч" weekly_window = "7д" diff --git a/src/localization/locales/th.toml b/src/localization/locales/th.toml index 9c49a4b9..589641f1 100644 --- a/src/localization/locales/th.toml +++ b/src/localization/locales/th.toml @@ -36,6 +36,8 @@ applying_update = "กำลังติดตั้งการอัปเด update_to = "อัปเดตเป็น" update_available = "มีการอัปเดตใหม่" update_prompt_now = "มีเวอร์ชัน {version} ให้ใช้งานแล้ว ต้องการอัปเดตตอนนี้หรือไม่?" +update_pending_winget = "เวอร์ชัน {version} เผยแพร่แล้ว แต่ยังไม่พร้อมใช้งานใน WinGet โปรดลองอีกครั้งในภายหลัง" +update_prompt_winget_behind = "เวอร์ชัน {release} เผยแพร่แล้ว แต่ยังไม่พร้อมใช้งานใน WinGet มีเวอร์ชัน {version} ให้ใช้งานแล้ว ต้องการอัปเดตตอนนี้หรือไม่?" exit = "ออกจากโปรแกรม" session_window = "5h" weekly_window = "7d" diff --git a/src/localization/locales/tr.toml b/src/localization/locales/tr.toml index 53fe411c..c4ae8c94 100644 --- a/src/localization/locales/tr.toml +++ b/src/localization/locales/tr.toml @@ -34,6 +34,8 @@ applying_update = "Güncelleme uygulanıyor..." update_to = "Şu sürüme güncelle" update_available = "Güncelleme mevcut" update_prompt_now = "{version} sürümü mevcut. Şimdi güncellemek ister misiniz?" +update_pending_winget = "{version} sürümü yayımlandı ancak henüz WinGet üzerinde mevcut değil. Lütfen daha sonra tekrar deneyin." +update_prompt_winget_behind = "{release} sürümü yayımlandı ancak henüz WinGet üzerinde mevcut değil. {version} sürümü mevcut. Şimdi güncellemek ister misiniz?" exit = "Çıkış" session_window = "5 sa" weekly_window = "7 g" diff --git a/src/localization/locales/zh-CN.toml b/src/localization/locales/zh-CN.toml index b8b27351..de64f186 100644 --- a/src/localization/locales/zh-CN.toml +++ b/src/localization/locales/zh-CN.toml @@ -36,6 +36,8 @@ applying_update = "正在应用更新..." update_to = "更新至" update_available = "有可用更新" update_prompt_now = "版本 {version} 已可用。是否立即更新?" +update_pending_winget = "版本 {version} 已发布,但尚未在 WinGet 上提供。请稍后再试。" +update_prompt_winget_behind = "版本 {release} 已发布,但尚未在 WinGet 上提供。版本 {version} 已可用。是否立即更新?" exit = "退出" session_window = "5h" weekly_window = "7d" diff --git a/src/localization/locales/zh-TW.toml b/src/localization/locales/zh-TW.toml index 01b36265..098d9b2e 100644 --- a/src/localization/locales/zh-TW.toml +++ b/src/localization/locales/zh-TW.toml @@ -36,6 +36,8 @@ applying_update = "正在套用更新..." update_to = "更新至" update_available = "有可用更新" update_prompt_now = "版本 {version} 已可用。是否立即更新?" +update_pending_winget = "版本 {version} 已發布,但尚未在 WinGet 上提供。請稍後再試。" +update_prompt_winget_behind = "版本 {release} 已發布,但尚未在 WinGet 上提供。版本 {version} 已可用。是否立即更新?" exit = "結束" session_window = "5h" weekly_window = "7d" diff --git a/src/localization/mod.rs b/src/localization/mod.rs index 58fb1b1a..136c3554 100644 --- a/src/localization/mod.rs +++ b/src/localization/mod.rs @@ -159,6 +159,8 @@ pub struct Strings { pub update_to: &'static str, pub update_available: &'static str, pub update_prompt_now: &'static str, + pub update_pending_winget: &'static str, + pub update_prompt_winget_behind: &'static str, pub exit: &'static str, pub session_window: &'static str, pub weekly_window: &'static str, diff --git a/src/updater.rs b/src/updater.rs index 1d54236a..4fa28b11 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -1,5 +1,5 @@ use std::fs::File; -use std::io; +use std::io::{self, Read}; use std::os::windows::process::CommandExt; use std::path::{Path, PathBuf}; use std::process::Command; @@ -18,13 +18,28 @@ const CREATE_NO_WINDOW: u32 = 0x08000000; const CREATE_NEW_CONSOLE: u32 = 0x00000010; // Keep this aligned with the package identifier used in winget-pkgs. const WINGET_PACKAGE_ID: &str = "CodeZeno.ClaudeCodeUsageMonitor"; +// `winget show` exit code when the package is not in the source. +const WINGET_NO_APPLICATIONS_FOUND: u32 = 0x8A15_0014; +const WINGET_SHOW_TIMEOUT: Duration = Duration::from_secs(60); +const WINGET_SHOW_VERSIONS_ARGS: [&str; 9] = [ + "show", + "--id", + WINGET_PACKAGE_ID, + "--exact", + "--versions", + "--source", + "winget", + "--accept-source-agreements", + "--disable-interactivity", +]; mod download; mod release; use download::{download_release_asset, open_verified_source, AssetIntegrity}; pub use release::ReleaseDescriptor; -use release::{release_descriptor, GitHubRelease}; +use release::{parse_version, release_descriptor, GitHubRelease}; +use semver::Version; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum InstallChannel { @@ -35,7 +50,32 @@ pub enum InstallChannel { #[derive(Debug)] pub enum UpdateCheckResult { UpToDate, - Available(ReleaseDescriptor), + Available(AvailableUpdate), + /// Released on GitHub, but the WinGet source has nothing newer than the current version. + Pending(String), +} + +#[derive(Clone, Debug)] +pub enum AvailableUpdate { + /// Portable installs download this GitHub release asset. + Release(ReleaseDescriptor), + /// WinGet installs upgrade to the newest version in the WinGet source, + /// which can trail GitHub. `unlisted_release` names the newer GitHub + /// release that WinGet does not list yet. + Winget { + version: String, + unlisted_release: Option, + }, +} + +impl AvailableUpdate { + /// The version this update installs. + pub fn version(&self) -> &str { + match self { + Self::Release(release) => &release.latest_version, + Self::Winget { version, .. } => version, + } + } } pub fn handle_cli_mode(args: &[String]) -> Option { @@ -89,10 +129,44 @@ pub fn current_install_channel() -> InstallChannel { } } -pub fn check_for_updates() -> Result { - match fetch_latest_release()? { - Some(release) => Ok(UpdateCheckResult::Available(release)), - None => Ok(UpdateCheckResult::UpToDate), +pub fn check_for_updates(channel: InstallChannel) -> Result { + let Some(release) = fetch_latest_release()? else { + return Ok(UpdateCheckResult::UpToDate); + }; + match channel { + InstallChannel::Portable => Ok(UpdateCheckResult::Available(AvailableUpdate::Release( + release, + ))), + // GitHub releases publish before the winget-pkgs manifest PR merges, so + // offer the newest version `winget upgrade` can actually install. + InstallChannel::Winget => Ok(winget_update_result( + &parse_version(env!("CARGO_PKG_VERSION"))?, + &parse_version(&release.latest_version)?, + &winget_listed_versions()?, + )), + } +} + +fn winget_update_result( + current: &Version, + released: &Version, + listed: &[Version], +) -> UpdateCheckResult { + let newest_listed = listed + .iter() + .filter(|version| version.pre.is_empty()) + .max_by(|a, b| a.cmp_precedence(b)); + match newest_listed { + Some(listed) if listed.cmp_precedence(current).is_gt() => { + UpdateCheckResult::Available(AvailableUpdate::Winget { + version: listed.to_string(), + unlisted_release: released + .cmp_precedence(listed) + .is_gt() + .then(|| released.to_string()), + }) + } + _ => UpdateCheckResult::Pending(released.to_string()), } } @@ -320,6 +394,61 @@ fn updates_dir() -> Result { .ok_or_else(|| "Unable to resolve a writable local updates directory.".to_string()) } +fn winget_listed_versions() -> Result, String> { + let mut child = Command::new("winget.exe") + .args(WINGET_SHOW_VERSIONS_ARGS) + .creation_flags(CREATE_NO_WINDOW) + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::null()) + .spawn() + .map_err(|e| format!("Unable to run WinGet: {e}"))?; + + // Drain stdout on another thread so a full pipe cannot stall WinGet. + let mut stdout = child.stdout.take().expect("WinGet stdout is piped"); + let reader = std::thread::spawn(move || { + let mut output = Vec::new(); + stdout.read_to_end(&mut output).map(|_| output) + }); + + let started = std::time::Instant::now(); + let status = loop { + match child.try_wait() { + Ok(Some(status)) => break status, + Ok(None) if started.elapsed() < WINGET_SHOW_TIMEOUT => { + std::thread::sleep(Duration::from_millis(250)); + } + Ok(None) => { + let _ = child.kill(); + let _ = child.wait(); + return Err("Timed out waiting for WinGet to list available versions.".into()); + } + Err(error) => return Err(format!("Unable to wait for WinGet: {error}")), + } + }; + let output = reader + .join() + .map_err(|_| "Unable to read WinGet output.".to_string())? + .map_err(|e| format!("Unable to read WinGet output: {e}"))?; + winget_versions_outcome(status.code(), &String::from_utf8_lossy(&output)) +} + +fn winget_versions_outcome(exit_code: Option, stdout: &str) -> Result, String> { + // Windows exit codes are HRESULTs; Rust reports them as signed values. + match exit_code.map(|code| code as u32) { + // Headers are localized, so keep only the lines that are versions. + Some(0) => Ok(stdout + .lines() + .filter_map(|line| Version::parse(line.trim()).ok()) + .collect()), + Some(WINGET_NO_APPLICATIONS_FOUND) => Ok(Vec::new()), + Some(code) => Err(format!( + "WinGet could not list available versions (exit code 0x{code:08X})." + )), + None => Err("WinGet exited without a status code.".into()), + } +} + fn winget_upgrade_command(pid: u32, target: &str, working_dir: &str) -> String { let target = powershell_single_quoted(target); let working_dir = powershell_single_quoted(working_dir); diff --git a/src/updater/release.rs b/src/updater/release.rs index 4bd158b4..eadd8b7f 100644 --- a/src/updater/release.rs +++ b/src/updater/release.rs @@ -73,7 +73,7 @@ pub(super) fn release_descriptor( })) } -fn parse_version(version: &str) -> Result { +pub(super) fn parse_version(version: &str) -> Result { Version::parse(version.strip_prefix('v').unwrap_or(version)) .map_err(|e| format!("Invalid release version {version:?}: {e}")) } diff --git a/src/updater/tests.rs b/src/updater/tests.rs index 73d6941e..abb56ae3 100644 --- a/src/updater/tests.rs +++ b/src/updater/tests.rs @@ -198,3 +198,93 @@ fn winget_upgrade_command_quotes_each_path_as_a_powershell_literal() { ); } } + +#[test] +fn winget_show_args_list_versions_non_interactively() { + assert_eq!( + WINGET_SHOW_VERSIONS_ARGS.join(" "), + "show --id CodeZeno.ClaudeCodeUsageMonitor --exact --versions --source winget --accept-source-agreements --disable-interactivity" + ); +} + +#[test] +fn winget_versions_outcome_parses_versions_under_localized_headers() { + let stdout = concat!( + "Trouvé Claude Code Usage Monitor [CodeZeno.ClaudeCodeUsageMonitor] +", + "Version +------- +2.15.14 +2.15.0 + 2.14.55 +1.3 +", + ); + assert_eq!( + winget_versions_outcome(Some(0), stdout), + Ok(vec![v("2.15.14"), v("2.15.0"), v("2.14.55")]) + ); + assert_eq!( + winget_versions_outcome(Some(WINGET_NO_APPLICATIONS_FOUND as i32), ""), + Ok(Vec::new()) + ); + assert_eq!( + winget_versions_outcome(Some(0x8A15_0001_u32 as i32), ""), + Err("WinGet could not list available versions (exit code 0x8A150001).".into()) + ); + assert!(winget_versions_outcome(Some(1), "2.15.14").is_err()); + assert!(winget_versions_outcome(None, "2.15.14").is_err()); +} + +fn v(version: &str) -> Version { + Version::parse(version).unwrap() +} + +fn winget_result(current: &str, released: &str, listed: &[&str]) -> String { + let listed = listed.iter().map(|version| v(version)).collect::>(); + match winget_update_result(&v(current), &v(released), &listed) { + UpdateCheckResult::UpToDate => "up to date".into(), + UpdateCheckResult::Pending(released) => format!("pending {released}"), + UpdateCheckResult::Available(AvailableUpdate::Winget { + version, + unlisted_release, + }) => format!("winget {version} unlisted {unlisted_release:?}"), + UpdateCheckResult::Available(AvailableUpdate::Release(_)) => "release".into(), + } +} + +#[test] +fn winget_offers_the_released_version_once_listed() { + assert_eq!( + winget_result("2.15.14", "2.15.19", &["2.15.0", "2.15.19", "2.15.14"]), + "winget 2.15.19 unlisted None" + ); +} + +#[test] +fn winget_offers_the_newest_listed_version_while_the_release_is_unlisted() { + assert_eq!( + winget_result("2.15.14", "2.15.19", &["2.15.14", "2.15.18", "2.15.17"]), + "winget 2.15.18 unlisted Some(\"2.15.19\")" + ); +} + +#[test] +fn winget_is_pending_when_nothing_newer_is_listed() { + assert_eq!( + winget_result("2.15.14", "2.15.19", &["2.15.0", "2.15.14"]), + "pending 2.15.19" + ); + assert_eq!(winget_result("2.15.14", "2.15.19", &[]), "pending 2.15.19"); + assert_eq!( + winget_result("2.15.14", "2.15.19", &["2.15.18-beta1"]), + "pending 2.15.19" + ); +} + +#[test] +#[ignore = "runs winget.exe against the live WinGet source"] +fn winget_lists_published_versions_from_the_live_source() { + let versions = winget_listed_versions().unwrap(); + assert!(versions.contains(&v("2.15.0")), "{versions:?}"); +} diff --git a/src/window.rs b/src/window.rs index f85c6d68..ff8e21a8 100644 --- a/src/window.rs +++ b/src/window.rs @@ -43,7 +43,7 @@ use crate::theme_engine::{ MouseEventKind, ReferenceRegion, SurfaceNest, ThemeDocument, ThemeRuntime, VerticalAnchor, }; use crate::tray_icon; -use crate::updater::{self, InstallChannel, ReleaseDescriptor, UpdateCheckResult}; +use crate::updater::{self, AvailableUpdate, InstallChannel, ReleaseDescriptor, UpdateCheckResult}; /// Copyable HWND value used by the watchdog after the UI thread publishes it. #[derive(Clone, Copy, PartialEq, Eq)] @@ -151,7 +151,7 @@ enum UpdateStatus { Checking, Applying, UpToDate, - Available(ReleaseDescriptor), + Available(AvailableUpdate), } fn publish_update_status(state: &AppState) { @@ -160,15 +160,13 @@ fn publish_update_status(state: &AppState) { UpdateStatus::Idle | UpdateStatus::UpToDate => DashboardStatus::Idle, UpdateStatus::Checking => DashboardStatus::Checking, UpdateStatus::Applying => DashboardStatus::Applying, - UpdateStatus::Available(release) => { - DashboardStatus::Available(release.latest_version.clone()) - } + UpdateStatus::Available(update) => DashboardStatus::Available(update.version().to_owned()), }; crate::dashboard::publish_update_status(state.hwnd.to_hwnd(), status); } fn perform_update_action(hwnd: HWND) { - let (install_channel, release) = { + let update = { let state = lock_state(); let Some(state) = state.as_ref() else { return; @@ -179,18 +177,14 @@ fn perform_update_action(hwnd: HWND) { ) { return; } - ( - state.install_channel, - match &state.update_status { - UpdateStatus::Available(release) => Some(release.clone()), - _ => None, - }, - ) + match &state.update_status { + UpdateStatus::Available(update) => Some(update.clone()), + _ => None, + } }; - match (install_channel, release) { - (InstallChannel::Portable, Some(release)) => begin_update_apply(hwnd, release), - (InstallChannel::Winget, Some(_)) => begin_winget_update(hwnd), - (_, None) => begin_update_check(hwnd, true), + match update { + Some(update) => begin_update_install(hwnd, update), + None => begin_update_check(hwnd, true), } } @@ -1125,10 +1119,24 @@ fn show_error_message(hwnd: HWND, title: &str, message: &str) { } } -fn show_update_prompt(hwnd: HWND, strings: Strings, release: &ReleaseDescriptor) -> bool { - let message = strings - .update_prompt_now - .replace("{version}", &release.latest_version); +fn begin_update_install(hwnd: HWND, update: AvailableUpdate) { + match update { + AvailableUpdate::Release(release) => begin_update_apply(hwnd, release), + AvailableUpdate::Winget { .. } => begin_winget_update(hwnd), + } +} + +fn show_update_prompt(hwnd: HWND, strings: Strings, update: &AvailableUpdate) -> bool { + let message = match update { + AvailableUpdate::Winget { + unlisted_release: Some(release), + .. + } => strings + .update_prompt_winget_behind + .replace("{release}", release), + _ => strings.update_prompt_now.to_owned(), + } + .replace("{version}", update.version()); unsafe { let title_wide = native_interop::wide_str(strings.update_available); @@ -1197,8 +1205,8 @@ fn begin_update_check(hwnd: HWND, interactive: bool) { std::thread::spawn(move || { let hwnd = send_hwnd.to_hwnd(); let checked_at = now_unix_secs(); - match updater::check_for_updates() { - Ok(UpdateCheckResult::UpToDate) => { + match updater::check_for_updates(install_channel) { + Ok(result @ (UpdateCheckResult::UpToDate | UpdateCheckResult::Pending(_))) => { { let mut state = lock_state(); if let Some(s) = state.as_mut() { @@ -1209,7 +1217,13 @@ fn begin_update_check(hwnd: HWND, interactive: bool) { } save_state_settings(); if interactive { - show_info_message(hwnd, strings.updates, strings.up_to_date); + let message = match result { + UpdateCheckResult::Pending(version) => { + strings.update_pending_winget.replace("{version}", &version) + } + _ => strings.up_to_date.to_string(), + }; + show_info_message(hwnd, strings.updates, &message); } unsafe { let _ = PostMessageW( @@ -1220,20 +1234,17 @@ fn begin_update_check(hwnd: HWND, interactive: bool) { ); } } - Ok(UpdateCheckResult::Available(release)) => { + Ok(UpdateCheckResult::Available(update)) => { { let mut state = lock_state(); if let Some(s) = state.as_mut() { - s.update_status = UpdateStatus::Available(release.clone()); + s.update_status = UpdateStatus::Available(update.clone()); s.last_update_check_unix = Some(checked_at); } } save_state_settings(); - if interactive && show_update_prompt(hwnd, strings, &release) { - match install_channel { - InstallChannel::Portable => begin_update_apply(hwnd, release), - InstallChannel::Winget => begin_winget_update(hwnd), - } + if interactive && show_update_prompt(hwnd, strings, &update) { + begin_update_install(hwnd, update); } // Keep the dashboard busy until the install prompt is dismissed. if let Some(state) = lock_state().as_ref() { @@ -1310,7 +1321,8 @@ fn begin_update_apply(hwnd: HWND, release: ReleaseDescriptor) { { let mut state = lock_state(); if let Some(s) = state.as_mut() { - s.update_status = UpdateStatus::Available(release); + s.update_status = + UpdateStatus::Available(AvailableUpdate::Release(release)); publish_update_status(s); } }