From 892cda8e34f55e05d141dcb8c44c08735dacb938 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 8 Nov 2026 13:32:01 -0800 Subject: [PATCH] Updates: the updater reports installing, ready, installed and failed, and installs at exit craft-update moves to a craft-libs branch that merges storytold/craft-libs#687 and #696. - The check result window shows for every status but idle and stays closed while the startup prompt is open, so the first start after an update reports that it installed or why it failed. Restart to Update calls Updater::restart_to_update. - on_exit calls Updater::start_pending_update before the process ends, which starts the install of an update waiting for the app to close. --- Cargo.lock | 3 +- Cargo.toml | 2 +- apps/figcraft/src/main.rs | 7 ++- crates/ui-egui/src/dialogs/updates_ui.rs | 55 ++++++++++++++++++++---- 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5fdb9e27b..672a5f8e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1134,7 +1134,7 @@ dependencies = [ [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7" +source = "git+https://github.com/dotCooCoo/craft-libs?rev=c7c84230631cbc01ee928f0a0e0d838b1ed612ae#c7c84230631cbc01ee928f0a0e0d838b1ed612ae" dependencies = [ "chrono", "dunce", @@ -7020,7 +7020,6 @@ dependencies = [ "nix", "typed-path", "windows-sys 0.61.2", - "zopfli", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c9b915a69..cf6ca6fea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-l # pull a second egui family version). craft-window = { git = "https://github.com/storytold/craft-libs", rev = "e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545" } # craft-update: app-specific pretend versions apply to startup and manual checks. -craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", features = ["egui"] } +craft-update = { git = "https://github.com/dotCooCoo/craft-libs", version = "=0.1.0", rev = "c7c84230631cbc01ee928f0a0e0d838b1ed612ae", features = ["egui"] } # The suite's font store: exact lookup of a missing font in the pinned Google Fonts and Noto # indexes, and checksum-verified installs into the one folder every Craft app reads (craft-libs). craft-font-store = { git = "https://github.com/storytold/craft-libs", rev = "e11f9d9aebea5f6b8e74643c1ea1ed172ba5f545" } diff --git a/apps/figcraft/src/main.rs b/apps/figcraft/src/main.rs index 148165f6b..66b5f73ee 100644 --- a/apps/figcraft/src/main.rs +++ b/apps/figcraft/src/main.rs @@ -126,7 +126,12 @@ impl eframe::App for App { fn on_exit(&mut self) { // With the app out of reach (busy), its recovery copies stay for the next start. let copies = recovery::lock(&self.recovery).take(); - with_app(|fig| services::shutdown(fig, services::config_dir().as_deref(), copies)); + with_app(|fig| { + services::shutdown(fig, services::config_dir().as_deref(), copies); + // An update that waits for the app to close installs now: the app lives in a + // thread-local, and the process can end without dropping it. + fig.start_pending_update(); + }); // Everything is kept: end before the windows' teardown, where AppKit's Touch Bar support // throws on the content view AccessKit restores (craft-libs#3, #781). #[cfg(target_os = "macos")] diff --git a/crates/ui-egui/src/dialogs/updates_ui.rs b/crates/ui-egui/src/dialogs/updates_ui.rs index f4481d064..0cd8dab79 100644 --- a/crates/ui-egui/src/dialogs/updates_ui.rs +++ b/crates/ui-egui/src/dialogs/updates_ui.rs @@ -5,7 +5,7 @@ use crate::FigApp; #[cfg(target_arch = "wasm32")] use crate::i18n::tr; #[cfg(not(target_arch = "wasm32"))] -use craft_update::updater::{Action, Updater}; +use craft_update::updater::{Action, Status, Updater}; #[cfg(not(target_arch = "wasm32"))] use craft_update::{CheckResultDialog, CheckResultResponse}; @@ -21,6 +21,7 @@ pub(crate) struct Runtime { #[cfg(not(target_arch = "wasm32"))] repaint: Option, /// The result dialog of a manual check is open. + #[cfg(target_arch = "wasm32")] pub(crate) manual_window_open: bool, /// The web build has no updater; its only result is that checks are unsupported. #[cfg(target_arch = "wasm32")] @@ -38,6 +39,7 @@ impl Default for Runtime { check_requested: false, #[cfg(not(target_arch = "wasm32"))] repaint: None, + #[cfg(target_arch = "wasm32")] manual_window_open: false, #[cfg(target_arch = "wasm32")] status_message: None, @@ -51,13 +53,19 @@ impl FigApp { pub fn set_startup_update_check(&mut self, on: bool) { self.updates_rt.startup_check = on; } + + /// Start the install of an update that waits for the app to close. The desktop app calls this + /// when it exits, before it ends the process; later calls do nothing. + #[cfg(not(target_arch = "wasm32"))] + pub fn start_pending_update(&mut self) { + self.updates_rt.updater.start_pending_update(); + } } pub(crate) fn check(app: &mut FigApp) { #[cfg(not(target_arch = "wasm32"))] { app.updates_rt.check_requested = true; - app.updates_rt.manual_window_open = true; if let Some(ctx) = &app.updates_rt.repaint { ctx.request_repaint(); } @@ -85,10 +93,18 @@ pub(crate) fn tick(app: &mut FigApp, ctx: &egui::Context) { let _ = (app, ctx); } +/// Whether the check result window shows: for every status but idle (a requested check, and the +/// first start after an update, which reports that it installed or why it failed), and never over +/// the startup prompt. +#[cfg(not(target_arch = "wasm32"))] +fn result_window_shows(status: &Status, prompt_open: bool) -> bool { + !prompt_open && !matches!(status, Status::Idle) +} + pub(crate) fn show(app: &mut FigApp, ctx: &egui::Context) { #[cfg(not(target_arch = "wasm32"))] { - // The restart helper is scheduled before the app closes, so no update starts over unsaved work. + // The updater asks to close only when no document has unsaved changes; the install starts when the app exits. let clean = !app.session.docs.iter().any(|d| d.is_dirty()); match app.updates_rt.updater.show_if(ctx, clean) { Action::OpenUrl(url) => ctx.open_url(egui::OpenUrl::new_tab(url)), @@ -97,14 +113,13 @@ pub(crate) fn show(app: &mut FigApp, ctx: &egui::Context) { Action::None => {} } let rt = &mut app.updates_rt; - if rt.manual_window_open { + if result_window_shows(rt.updater.status(), rt.updater.prompt_open()) { match CheckResultDialog::new(&crate::UPDATE_APP, env!("CARGO_PKG_VERSION")).show(ctx, rt.updater.status()) { CheckResultResponse::None => {} - CheckResultResponse::Close => { - rt.manual_window_open = false; - rt.updater.dismiss_status(); - } + CheckResultResponse::Close => rt.updater.dismiss_status(), CheckResultResponse::CheckAgain => rt.check_requested = true, + // The next `show_if` asks the window to close once no document has unsaved changes. + CheckResultResponse::RestartToUpdate => rt.updater.restart_to_update(), } } } @@ -135,3 +150,27 @@ pub(crate) fn show(app: &mut FigApp, ctx: &egui::Context) { } } } + +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use super::*; + + #[test] + fn result_window_shows_every_status_but_idle() { + let shown = [ + Status::Checking, + Status::UpToDate, + Status::Failed("no network".into()), + Status::Installing, + Status::Ready, + Status::Managed, + Status::Updated("0.2.0".into()), + Status::InstallFailed("disk full".into()), + ]; + for status in &shown { + assert!(result_window_shows(status, false), "{status:?}"); + assert!(!result_window_shows(status, true), "{status:?} over the prompt"); + } + assert!(!result_window_shows(&Status::Idle, false)); + } +}