Merge pull request #2130 from dotCooCoo/chore/craft-update-updater-fixes

Updates: the updater reports installing, ready, installed and failed, and installs at exit
This commit is contained in:
Dylan M. Taylor
2026-11-08 22:12:41 -05:00
committed by GitHub
4 changed files with 55 additions and 12 deletions
Generated
+1 -2
View File
@@ -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]]
+1 -1
View File
@@ -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" }
+6 -1
View File
@@ -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")]
+47 -8
View File
@@ -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<egui::Context>,
/// 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));
}
}