diff --git a/Cargo.lock b/Cargo.lock index 5ff27fdd2..62d15529a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -847,12 +847,12 @@ dependencies = [ [[package]] name = "craft-fonts-build" 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" [[package]] name = "craft-i18n" 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 = [ "fixed_decimal", "icu_decimal", @@ -875,7 +875,7 @@ dependencies = [ [[package]] name = "craft-media" 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 = [ "thiserror 2.0.21", ] @@ -883,7 +883,7 @@ dependencies = [ [[package]] name = "craft-pdf" 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 = [ "krilla", "thiserror 2.0.21", @@ -892,7 +892,7 @@ dependencies = [ [[package]] name = "craft-ui" 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 = [ "craft-fonts-build", "craft-i18n", @@ -912,7 +912,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", @@ -2791,7 +2791,7 @@ dependencies = [ [[package]] name = "krilla" version = "0.8.2" -source = "git+https://github.com/storytold/craft-libs?rev=c5a1cbfb5f9049f9270955caffb5bee5997aebf7#c5a1cbfb5f9049f9270955caffb5bee5997aebf7" +source = "git+https://github.com/dotCooCoo/craft-libs?rev=c7c84230631cbc01ee928f0a0e0d838b1ed612ae#c7c84230631cbc01ee928f0a0e0d838b1ed612ae" dependencies = [ "base64 0.22.1", "bumpalo", @@ -6571,7 +6571,6 @@ dependencies = [ "nix", "typed-path", "windows-sys 0.61.2", - "zopfli", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ab2b74291..a919e0504 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,11 +59,11 @@ harfrust = "0.13" vello_cpu = "0.2" vello_common = { version = "0.2", default-features = false } image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif", "tiff", "bmp"] } -craft-media = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", version = "=0.1.0", default-features = false } -craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", version = "=0.1.0", default-features = false, features = ["write", "write-simple-text"] } +craft-media = { git = "https://github.com/dotCooCoo/craft-libs", rev = "c7c84230631cbc01ee928f0a0e0d838b1ed612ae", version = "=0.1.0", default-features = false } +craft-pdf = { git = "https://github.com/dotCooCoo/craft-libs", rev = "c7c84230631cbc01ee928f0a0e0d838b1ed612ae", version = "=0.1.0", default-features = false, features = ["write", "write-simple-text"] } # 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-ui = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", version = "=0.1.0" } +craft-update = { git = "https://github.com/dotCooCoo/craft-libs", version = "=0.1.0", rev = "c7c84230631cbc01ee928f0a0e0d838b1ed612ae", features = ["egui"] } +craft-ui = { git = "https://github.com/dotCooCoo/craft-libs", rev = "c7c84230631cbc01ee928f0a0e0d838b1ed612ae", version = "=0.1.0" } i_overlay = "9" quick-xml = "0.41" zip = { version = "3", default-features = false, features = ["deflate"] } @@ -109,7 +109,7 @@ codegen-units = 16 debug = 0 [patch.crates-io] -krilla = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", version = "=0.8.2" } +krilla = { git = "https://github.com/dotCooCoo/craft-libs", rev = "c7c84230631cbc01ee928f0a0e0d838b1ed612ae", version = "=0.8.2" } ecolor = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" } emath = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" } epaint = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" } diff --git a/apps/deckcraft/src/main.rs b/apps/deckcraft/src/main.rs index 3f872c214..d71817ceb 100644 --- a/apps/deckcraft/src/main.rs +++ b/apps/deckcraft/src/main.rs @@ -51,6 +51,9 @@ impl eframe::App for App { } fn on_exit(&mut self) { save_prefs(&self.0); + // The window is closing for good: a ready update's helper starts now and installs it once + // the process has exited. + self.0.start_pending_update(); // macOS (#279): end the process here, before eframe tears the window down. AccessKit's // macOS adapter subclasses the window's content view and restores its class on teardown; // AppKit's Touch Bar support observes that view (KVO), so in the next display cycle diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index 178ab24ba..8cc53258e 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -137,6 +137,14 @@ impl SlideApp { updates_ui::enable(self); } + /// Start the helper of an update that is ready, so that it installs once the process has + /// exited. The desktop shell calls it in `on_exit`, which ends the process on macOS before the + /// app is dropped. Later calls do nothing. + #[cfg(not(target_arch = "wasm32"))] + pub fn start_pending_update(&mut self) { + updates_ui::start_pending_update(self); + } + /// A message that must be seen wherever the user is: a toast, which also shows over the start /// screen and the File tab, where there is no status bar. pub fn notify(&mut self, msg: impl Into) { diff --git a/crates/ui-egui/src/dialogs/updates_ui.rs b/crates/ui-egui/src/dialogs/updates_ui.rs index cdf2bf207..3c62a254d 100644 --- a/crates/ui-egui/src/dialogs/updates_ui.rs +++ b/crates/ui-egui/src/dialogs/updates_ui.rs @@ -13,13 +13,11 @@ pub(crate) struct Runtime { /// The desktop host turns the check at start on; tests and the web build leave it off. enabled: bool, check_requested: bool, - /// A manual check's result dialog is showing (checking, up to date or failed). - result_open: bool, } impl Default for Runtime { fn default() -> Self { - Self { updater: Updater::new(&crate::UPDATE_APP, VERSION), enabled: false, check_requested: false, result_open: false } + Self { updater: Updater::new(&crate::UPDATE_APP, VERSION), enabled: false, check_requested: false } } } @@ -31,10 +29,15 @@ pub(crate) fn enable(app: &mut SlideApp) { pub(crate) fn request_check(app: &mut SlideApp) { if !matches!(app.updates_rt.updater.status(), Status::Checking) { app.updates_rt.check_requested = true; - app.updates_rt.result_open = true; } } +/// Starts the helper of an update that is ready, so that it installs once the process has exited. +/// Later calls do nothing. +pub(crate) fn start_pending_update(app: &mut SlideApp) { + app.updates_rt.updater.start_pending_update(); +} + pub(crate) fn tick(app: &mut SlideApp, ctx: &egui::Context) { let rt = &mut app.updates_rt; if std::mem::take(&mut rt.check_requested) { @@ -46,21 +49,31 @@ pub(crate) fn tick(app: &mut SlideApp, ctx: &egui::Context) { } pub(crate) fn show(app: &mut SlideApp, ctx: &egui::Context) { - // The restart helper is scheduled before the app closes, so no update starts over unsaved work. + // An installation starts, and a ready update asks the app to close, only while nothing is unsaved. let clean = !app.session.documents().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)), - // The app's close path asks about unsaved work; the restart helper relaunches once the process exits. + // The app's close path asks about unsaved work; `on_exit` starts the restart helper. Action::Close => ctx.send_viewport_cmd(egui::ViewportCommand::Close), Action::None => {} } + // The result window reports a requested check, and at the first start after an update what + // that update came to (installed, or why it failed). let rt = &mut app.updates_rt; - if rt.result_open { - match CheckResultDialog::new(&crate::UPDATE_APP, VERSION).show(ctx, rt.updater.status()) { - CheckResultResponse::Close => rt.result_open = false, - CheckResultResponse::CheckAgain => rt.check_requested = true, - CheckResultResponse::None => {} - } + if !matches!(rt.updater.status(), Status::Idle) { + let response = CheckResultDialog::new(&crate::UPDATE_APP, VERSION).show(ctx, rt.updater.status()); + answer(rt, response); + } +} + +/// Acts on the result window's answer. +fn answer(rt: &mut Runtime, response: CheckResultResponse) { + match response { + CheckResultResponse::Close => rt.updater.dismiss_status(), + CheckResultResponse::CheckAgain => rt.check_requested = true, + // The next frame's `show_if` asks the window to close once nothing is unsaved. + CheckResultResponse::RestartToUpdate => rt.updater.restart_to_update(), + CheckResultResponse::None => {} } } @@ -77,4 +90,62 @@ mod tests { ctx.run_ui(egui::RawInput::default(), |ui| show(&mut app, ui.ctx())).textures_delta.clear(); assert!(app.updates_rt.updater.prompt_open()); } + + /// An app whose updater keeps its outcome records under `root`, reads no launcher choice and + /// asks no server, with the check at start turned on. + fn app_with_records(root: &std::path::Path) -> SlideApp { + let mut app = SlideApp::new(deckcraft_engine::Session::new(), crate::Services::default()); + app.updates_rt.updater = Updater::new(&crate::UPDATE_APP, VERSION) + .with_paths(craft_update::Paths::under(root)) + .with_pins_path(root.join("pins.json")) + .with_source(std::sync::Arc::new(|| Ok(None))); + app.updates_rt.enabled = true; + app + } + + fn records_dir(name: &str) -> std::path::PathBuf { + let dir = std::env::temp_dir().join(format!("deckcraft-updates-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + dir + } + + /// The first start after an update reports it; OK dismisses the report. + #[test] + fn the_start_after_an_update_reports_it_until_dismissed() { + let root = records_dir("installed"); + std::fs::create_dir_all(root.join("data")).unwrap(); + std::fs::write(root.join("data").join("self-update-done.txt"), VERSION).unwrap(); + let mut app = app_with_records(&root); + let ctx = egui::Context::default(); + ctx.run_ui(egui::RawInput::default(), |ui| { + tick(&mut app, ui.ctx()); + show(&mut app, ui.ctx()); + }) + .textures_delta + .clear(); + assert!(matches!(app.updates_rt.updater.status(), Status::Updated(_)), "{:?}", app.updates_rt.updater.status()); + answer(&mut app.updates_rt, CheckResultResponse::Close); + assert_eq!(app.updates_rt.updater.status(), &Status::Idle); + let _ = std::fs::remove_dir_all(&root); + } + + /// The first start after a failed update says why; Check Again asks for a new check. + #[test] + fn the_start_after_a_failed_update_reports_why_and_offers_a_new_check() { + let root = records_dir("failed"); + std::fs::create_dir_all(root.join("data").join("logs")).unwrap(); + std::fs::write(root.join("data").join("logs").join("self-update-error.txt"), "the disk is full").unwrap(); + let mut app = app_with_records(&root); + let ctx = egui::Context::default(); + ctx.run_ui(egui::RawInput::default(), |ui| { + tick(&mut app, ui.ctx()); + show(&mut app, ui.ctx()); + }) + .textures_delta + .clear(); + assert_eq!(app.updates_rt.updater.status(), &Status::InstallFailed("the disk is full".into())); + answer(&mut app.updates_rt, CheckResultResponse::CheckAgain); + assert!(app.updates_rt.check_requested); + let _ = std::fs::remove_dir_all(&root); + } } diff --git a/xtask/src/pdf_deps.rs b/xtask/src/pdf_deps.rs index ad8e8725c..65b32ee8d 100644 --- a/xtask/src/pdf_deps.rs +++ b/xtask/src/pdf_deps.rs @@ -6,7 +6,8 @@ use std::process::Command; use serde_json::Value; -const SHARED_REV: &str = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7"; +const SHARED_REPO: &str = "https://github.com/dotCooCoo/craft-libs"; +const SHARED_REV: &str = "c7c84230631cbc01ee928f0a0e0d838b1ed612ae"; const REGISTRY: &str = "registry+https://github.com/rust-lang/crates.io-index"; pub fn run(root: &Path) -> Result<(), String> { @@ -70,7 +71,7 @@ fn validate(metadata: &Value) -> Result<(), String> { } matches.first().copied().ok_or_else(|| format!("missing {name}")) }; - let source = format!("git+https://github.com/storytold/craft-libs?rev={SHARED_REV}#{SHARED_REV}"); + let source = format!("git+{SHARED_REPO}?rev={SHARED_REV}#{SHARED_REV}"); for (name, version, expected_source, expected_features) in [ ("craft-pdf", "0.1.0", source.as_str(), vec!["write", "write-simple-text"]), ("krilla", "0.8.2", source.as_str(), vec!["raster-images", "rayon", "simple-text"]), @@ -143,7 +144,7 @@ mod tests { use serde_json::json; fn metadata() -> Value { - let source = format!("git+https://github.com/storytold/craft-libs?rev={SHARED_REV}#{SHARED_REV}"); + let source = format!("git+{SHARED_REPO}?rev={SHARED_REV}#{SHARED_REV}"); json!({ "packages": [ {"id":"app", "name":"deckcraft-pdf"},