From 2b9317519cfda78367a9a2735b9706023b886c4b Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 14:19:39 -0400 Subject: [PATCH 1/5] feat: startup update prompt modal and direct self-updating The update flow (check at start or on request, the startup prompt, the self-update worker) is craft-update's Updater; the app only supplies the suite, its close path and its status window. The check at start is opt-in for the desktop app, and the Windows update helper runs before argument parsing. Ref: storytold/craft-libs#319 --- Cargo.lock | 373 ++++++++++++++++++++++++++++++- Cargo.toml | 1 + apps/lightcraft/Cargo.toml | 3 + apps/lightcraft/src/main.rs | 6 + crates/ui-egui/Cargo.toml | 3 + crates/ui-egui/src/lib.rs | 6 + crates/ui-egui/src/menubar.rs | 1 + crates/ui-egui/src/menus.rs | 2 + crates/ui-egui/src/updates_ui.rs | 159 +++++++++++++ 9 files changed, 546 insertions(+), 8 deletions(-) create mode 100644 crates/ui-egui/src/updates_ui.rs diff --git a/Cargo.lock b/Cargo.lock index 6e932cba8..ebe47ab97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -580,6 +580,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -829,7 +835,7 @@ dependencies = [ "safetensors 0.7.0", "thiserror 2.0.21", "yoke 0.8.3", - "zip", + "zip 7.2.0", ] [[package]] @@ -1038,6 +1044,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1051,7 +1067,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "core-graphics-types", "foreign-types", "libc", @@ -1064,7 +1080,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "libc", ] @@ -1091,6 +1107,31 @@ dependencies = [ "serde", ] +[[package]] +name = "craft-update" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=13ceaf2f31620531f39a54f6b994d581a6082a93#13ceaf2f31620531f39a54f6b994d581a6082a93" +dependencies = [ + "chrono", + "dunce", + "egui", + "flate2", + "getrandom 0.3.4", + "log", + "plist", + "semver", + "serde", + "serde_json", + "sha2", + "tar", + "tempfile", + "ureq", + "verifysign", + "winreg", + "winsafe", + "zip 9.1.0", +] + [[package]] name = "crc32fast" version = "1.5.2" @@ -1215,6 +1256,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" + [[package]] name = "dhat" version = "0.3.3" @@ -1304,6 +1351,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-stack" version = "0.10.0" @@ -1784,6 +1837,16 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.14" @@ -2342,8 +2405,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", + "wasm-bindgen", ] [[package]] @@ -2614,6 +2679,22 @@ dependencies = [ "digest", ] +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "image" version = "0.25.10" @@ -3105,6 +3186,7 @@ name = "lightcraft" version = "0.14.0" dependencies = [ "chrono", + "craft-update", "eframe", "egui", "lightcraft-codecs", @@ -3371,7 +3453,7 @@ version = "0.14.0" dependencies = [ "lightcraft-develop", "lightcraft-geom", - "quick-xml", + "quick-xml 0.41.0", ] [[package]] @@ -3409,7 +3491,7 @@ dependencies = [ "lightcraft-geom", "lightcraft-tiff", "proptest", - "quick-xml", + "quick-xml 0.41.0", "serde", "serde_json", ] @@ -3537,6 +3619,7 @@ name = "lightcraft-ui-egui" version = "0.14.0" dependencies = [ "craft-ui", + "craft-update", "egui", "egui_kittest", "lightcraft-catalog", @@ -3859,6 +3942,18 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "no_std_io2" version = "0.9.4" @@ -3939,6 +4034,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-derive" version = "0.4.2" @@ -4376,6 +4477,12 @@ dependencies = [ "libc", ] +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "orbclient" version = "0.3.55" @@ -4652,6 +4759,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "plist" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327a4cb2ad49dc48adfdc9da89e22c64d488b508c67a044d71ba729e4521a285" +dependencies = [ + "base64", + "indexmap", + "quick-xml 0.42.0", + "serde", + "time", +] + [[package]] name = "png" version = "0.18.1" @@ -4738,6 +4858,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -4894,6 +5020,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.47" @@ -5303,13 +5438,27 @@ version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ + "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki 0.103.15", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.15.1" @@ -5319,6 +5468,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.15", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" + [[package]] name = "rustls-rustcrypto" version = "0.0.2-alpha" @@ -5419,6 +5595,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -5445,6 +5630,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "self_cell" version = "1.3.0" @@ -5456,6 +5664,10 @@ name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "send_wrapper" @@ -5824,6 +6036,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -5906,6 +6128,36 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tokio" version = "1.53.1" @@ -6030,6 +6282,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" +[[package]] +name = "typed-path" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e" + [[package]] name = "typenum" version = "1.20.1" @@ -6140,6 +6398,41 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc" +dependencies = [ + "base64", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "ureq-proto", + "utf8-zero", +] + +[[package]] +name = "ureq-proto" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841583fc36089d7d59419568ae157f43a5a03a2e5a81ba87c77d1596ef46e922" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "uuid" version = "1.26.1" @@ -6189,6 +6482,16 @@ dependencies = [ "vello_common", ] +[[package]] +name = "verifysign" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6acfcc42a8d44f340d001193d97ef56bf33210b51ad760ae8308c97d5241ca51" +dependencies = [ + "core-foundation 0.10.1", + "windows-sys 0.61.2", +] + [[package]] name = "version_check" version = "0.9.5" @@ -6404,7 +6707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" dependencies = [ "proc-macro2", - "quick-xml", + "quick-xml 0.41.0", "quote", ] @@ -6440,6 +6743,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49d0e65abc00c142e8f9428da991da9f1a250c39be97459901eb12b9b49f62a" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "1.0.9" @@ -6936,7 +7248,7 @@ dependencies = [ "calloop 0.13.0", "cfg_aliases", "concurrent-queue", - "core-foundation", + "core-foundation 0.9.4", "core-graphics", "cursor-icon", "dpi", @@ -6981,6 +7293,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + [[package]] name = "winresource" version = "0.1.31" @@ -6991,6 +7313,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "winsafe" +version = "0.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef0ffc427f045c0cc9ebffd6f4f91153dcd2a1547b5c3c29ee3f541e16e95c6" + [[package]] name = "wit-bindgen" version = "0.57.1" @@ -7328,7 +7656,24 @@ dependencies = [ "crc32fast", "indexmap", "memchr", - "typed-path", + "typed-path 0.12.3", +] + +[[package]] +name = "zip" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886b00003a72c4ccee3f42cfc34908d33b2508a74d827dc2a4244706475c704" +dependencies = [ + "crc32fast", + "flate2", + "getrandom 0.4.3", + "indexmap", + "memchr", + "nix", + "typed-path 0.13.0", + "windows-sys 0.61.2", + "zopfli", ] [[package]] @@ -7343,6 +7688,18 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +[[package]] +name = "zopfli" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf7fc5d30c28483d93805c4a5e12b05bbb52407fa67c5f8bd552374cd01fb11" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + [[package]] name = "zune-core" version = "0.5.3" diff --git a/Cargo.toml b/Cargo.toml index 0dc85dfe2..a8c96ad2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ repository = "https://github.com/storytold/lightcraft" [workspace.dependencies] craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "25da1314d443171b4adeaad2cd9454b273f35c6f", features = ["serde"] } craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "82a24b006eefa036b43c09a67fb9a3fbd17ffe63" } +craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "5b0f3c572a3b5c1e51bdb90d7ff788e0212ec1cc", features = ["egui"] } # internal lightcraft-denoise-core = { path = "crates/denoise-core" } lightcraft-denoise = { path = "crates/denoise", default-features = false } diff --git a/apps/lightcraft/Cargo.toml b/apps/lightcraft/Cargo.toml index f93bd2ccd..d00bc41e9 100644 --- a/apps/lightcraft/Cargo.toml +++ b/apps/lightcraft/Cargo.toml @@ -55,3 +55,6 @@ objc2-foundation = { workspace = true } [dev-dependencies] # tests record why they skip (`cargo xtask ci` lists the skips) lightcraft-testkit = { workspace = true } + +[target.'cfg(windows)'.dependencies] +craft-update = { workspace = true } diff --git a/apps/lightcraft/src/main.rs b/apps/lightcraft/src/main.rs index b666a0cdc..815fbd70f 100644 --- a/apps/lightcraft/src/main.rs +++ b/apps/lightcraft/src/main.rs @@ -881,6 +881,11 @@ fn panic_log_path_with(env: impl Fn(&str) -> Option, temp: s } fn main() -> eframe::Result { + // Windows: the update helper is a copy of this program started with `--finish-update`. + #[cfg(windows)] + if let Some(result) = craft_update::self_update::finish(&craft_update::suite::LIGHTCRAFT, &std::env::args_os().skip(1).collect::>()) { + std::process::exit(i32::from(result.is_err())); + } // First, so every start-up record is kept for the log file (`logging`). let logger = logging::install(); lightcraft_engine::guard::install_hook(panic_log_path_with(|name| std::env::var_os(name), std::env::temp_dir())); @@ -1009,6 +1014,7 @@ fn main() -> eframe::Result { // HDR display: SDR unless the surface, platform and display all allow HDR hdr_display::report(cc.wgpu_render_state.as_ref().map(|r| r.target_format)); let mut app = LightcraftApp::new(session, services(cc, app_log_file.as_deref())); + app.set_startup_update_check(true); if let Some(ui) = prefs { app.ui = ui; } diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index 5ee1c0233..8dab2b967 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -44,3 +44,6 @@ lightcraft-merge = { workspace = true } [build-dependencies] serde_json = { workspace = true } toml = "1" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +craft-update = { workspace = true } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 1b5fea8f1..71c2cef13 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -50,6 +50,7 @@ pub mod tasks; pub mod text_field; pub mod theme; pub mod titlebar; +pub mod updates_ui; pub mod widgets; pub mod window_place; @@ -409,6 +410,7 @@ pub struct LightcraftApp { pub headless_host: bool, /// Warnings to show one at a time (damaged settings files…, issue #103). pub notices: Vec, + pub(crate) updates_rt: updates_ui::Runtime, /// Quitting was stopped to ask first: changes couldn't be saved, or tasks are running. pub quit_prompt: Option, /// Keyboard/focus/input state for the prompt above, including its one-frame creation barrier. @@ -615,6 +617,7 @@ impl LightcraftApp { text_focus: false, headless_host: false, notices: vec![], + updates_rt: Default::default(), quit_prompt: None, notice_input: Default::default(), quit_confirmed: false, @@ -1966,6 +1969,7 @@ impl LightcraftApp { lightroom_import::tick(self, ctx); recheck::tick(self); tasks::poll(self, ctx); + updates_ui::tick(self, ctx); self.preview_build_status(ctx); self.save_status(ctx); self.ai_mask_detail(ctx, now); @@ -2626,6 +2630,7 @@ impl LightcraftApp { panels::notices::hide_background_pointer(&ctx); } panels::dialogs::show(self, &ctx, notice_blocks); + updates_ui::show(self, &ctx); panels::library_problem::show(self, &ctx, notice_blocks); panels::toast(self, &ctx); drop_overlay::show(self, &ctx); @@ -2656,6 +2661,7 @@ impl LightcraftApp { panels::notices::hide_background_pointer(&ctx); } panels::dialogs::show(self, &ctx, notice_blocks); + updates_ui::show(self, &ctx); panels::library_problem::show(self, &ctx, notice_blocks); export_task::poll(self, &ctx); slideshow_export::poll(self, &ctx); diff --git a/crates/ui-egui/src/menubar.rs b/crates/ui-egui/src/menubar.rs index 9b3d88036..ba4a8a7c2 100644 --- a/crates/ui-egui/src/menubar.rs +++ b/crates/ui-egui/src/menubar.rs @@ -322,6 +322,7 @@ const LAYOUT: &[(&str, &[&str])] = &[ "app.systemInfo", "app.openLogFolder", "---", + "app.checkForUpdates", "app.about", ], ), diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 03841d14a..84ec8ae0c 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -263,6 +263,7 @@ pub const UI_COMMANDS: &[UiCommand] = &[ // Settings ▸ Display ("app.displayProfile", "Display Profile", None, ""), ("app.about", "About LightCraft", None, "Help"), + ("app.checkForUpdates", "Check for Updates…", None, "Help"), ("app.systemInfo", "System Info…", None, "Help"), ("app.openLogFolder", "Open Log Folder", None, "Help"), // the frame-rate display (`frame_rate`): a debugging aid, never saved @@ -1587,6 +1588,7 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option crate::updates_ui::check(app), "photo.editInPhotoCraft" => edit_in(app, &ctx, p, true), "photo.editInExternal" => edit_in(app, &ctx, p, false), "photo.openOriginalInExternal" => open_original_in(app, &ctx, p), diff --git a/crates/ui-egui/src/updates_ui.rs b/crates/ui-egui/src/updates_ui.rs new file mode 100644 index 000000000..423f9a7c7 --- /dev/null +++ b/crates/ui-egui/src/updates_ui.rs @@ -0,0 +1,159 @@ +//! Software updates, background check at startup, and startup prompt modal. + +use serde_json::Value; + +use crate::LightcraftApp; + +#[cfg(not(target_arch = "wasm32"))] +use craft_update::updater::{Action, Status, Updater}; + +pub(crate) struct Runtime { + #[cfg(not(target_arch = "wasm32"))] + updater: Updater, + /// The desktop app turns the check at start on; tests and the web build leave it off. + #[cfg(not(target_arch = "wasm32"))] + startup_check: bool, + #[cfg(not(target_arch = "wasm32"))] + check_requested: bool, + pub(crate) manual_window_open: bool, + pub(crate) status_message: Option, +} + +impl Default for Runtime { + fn default() -> Self { + Self { + #[cfg(not(target_arch = "wasm32"))] + updater: Updater::new(&craft_update::suite::LIGHTCRAFT, env!("CARGO_PKG_VERSION")), + #[cfg(not(target_arch = "wasm32"))] + startup_check: false, + #[cfg(not(target_arch = "wasm32"))] + check_requested: false, + manual_window_open: false, + status_message: None, + } + } +} + +impl LightcraftApp { + /// Whether the app asks for a newer release when it starts (off until the desktop app turns it on). + #[cfg(not(target_arch = "wasm32"))] + pub fn set_startup_update_check(&mut self, on: bool) { + self.updates_rt.startup_check = on; + } +} + +pub(crate) fn check(app: &mut LightcraftApp) -> Result { + #[cfg(not(target_arch = "wasm32"))] + { + app.updates_rt.check_requested = true; + Ok(Value::Null) + } + #[cfg(target_arch = "wasm32")] + { + app.updates_rt.manual_window_open = true; + app.updates_rt.status_message = Some("Update check is not supported in the web browser.".into()); + Ok(Value::Null) + } +} + +pub(crate) fn tick(app: &mut LightcraftApp, ctx: &egui::Context) { + #[cfg(not(target_arch = "wasm32"))] + { + let rt = &mut app.updates_rt; + if std::mem::take(&mut rt.check_requested) { + rt.updater.check_now(ctx); + } else if rt.startup_check { + rt.updater.check_at_start(ctx); + } + rt.updater.tick(ctx); + } + #[cfg(target_arch = "wasm32")] + let _ = (app, ctx); +} + +pub(crate) fn show(app: &mut LightcraftApp, ctx: &egui::Context) { + #[cfg(not(target_arch = "wasm32"))] + { + match app.updates_rt.updater.show(ctx) { + Action::OpenUrl(url) => ctx.open_url(egui::OpenUrl::new_tab(url)), + Action::Close => ctx.send_viewport_cmd(egui::ViewportCommand::Close), + Action::None => {} + } + // The window answers Help ▸ Check for Updates, so it follows the updater's status. + let rt = &mut app.updates_rt; + (rt.manual_window_open, rt.status_message) = match rt.updater.status() { + Status::Idle => (false, None), + Status::Checking => (true, Some("Checking for updates…".to_string())), + Status::UpToDate => (true, Some(format!("LightCraft {} is currently the newest version available.", env!("CARGO_PKG_VERSION")))), + Status::Failed(e) => (true, Some(format!("Could not check for updates:\n{e}"))), + }; + } + + if app.updates_rt.manual_window_open { + let mut open = true; + egui::Window::new(crate::i18n::tr("LightCraft Updates")) + .id(egui::Id::new("lightcraft_manual_updates")) + .open(&mut open) + .collapsible(false) + .resizable(false) + .pivot(egui::Align2::CENTER_CENTER) + .default_pos(ctx.content_rect().center()) + .show(ctx, |ui| { + if let Some(msg) = &app.updates_rt.status_message { + ui.label(msg); + } + ui.add_space(8.0); + if ui.button(crate::i18n::tr("OK")).clicked() { + app.updates_rt.manual_window_open = false; + } + }); + if !open { + app.updates_rt.manual_window_open = false; + } + } + + // Closing the window dismisses the outcome it showed. + #[cfg(not(target_arch = "wasm32"))] + if !app.updates_rt.manual_window_open { + app.updates_rt.updater.dismiss_status(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use lightcraft_engine::Session; + + #[test] + fn default_runtime_is_idle() { + let rt = Runtime::default(); + assert!(!rt.manual_window_open); + #[cfg(not(target_arch = "wasm32"))] + { + assert!(!rt.startup_check); + assert!(!rt.updater.prompt_open()); + assert_eq!(rt.updater.status(), &Status::Idle); + } + } + + #[test] + #[cfg(not(target_arch = "wasm32"))] + fn startup_prompt_shows_and_closes() { + let mut app = LightcraftApp::new(Session::new(), Default::default()); + let ctx = egui::Context::default(); + let release = craft_update::Release { + tag: "v99.0.0".into(), + version: craft_update::parse_version("99.0.0"), + name: "LightCraft 99.0.0".into(), + html_url: "https://github.com/storytold/lightcraft/releases/tag/v99.0.0".into(), + ..Default::default() + }; + app.updates_rt.updater.offer(release); + + let mut output = ctx.run_ui(egui::RawInput::default(), |ui| { + show(&mut app, ui.ctx()); + }); + output.textures_delta.clear(); + assert!(app.updates_rt.updater.prompt_open()); + } +} From 8e5a6fc2e77376b55caa40c08c7faad16597a175 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 14:53:33 -0400 Subject: [PATCH 2/5] refactor(updates): hold the install back while documents are unsaved --- Cargo.lock | 18 +++++++++--------- crates/ui-egui/src/updates_ui.rs | 4 +++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ebe47ab97..d85066e8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1110,7 +1110,7 @@ dependencies = [ [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=13ceaf2f31620531f39a54f6b994d581a6082a93#13ceaf2f31620531f39a54f6b994d581a6082a93" +source = "git+https://github.com/storytold/craft-libs?rev=5b0f3c572a3b5c1e51bdb90d7ff788e0212ec1cc#5b0f3c572a3b5c1e51bdb90d7ff788e0212ec1cc" dependencies = [ "chrono", "dunce", @@ -1720,7 +1720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3850,7 +3850,7 @@ dependencies = [ "once_cell", "png", "thiserror 2.0.21", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4519,7 +4519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5429,7 +5429,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5486,7 +5486,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6056,7 +6056,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.5", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6302,7 +6302,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6956,7 +6956,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] diff --git a/crates/ui-egui/src/updates_ui.rs b/crates/ui-egui/src/updates_ui.rs index 423f9a7c7..9d09ff09e 100644 --- a/crates/ui-egui/src/updates_ui.rs +++ b/crates/ui-egui/src/updates_ui.rs @@ -74,7 +74,9 @@ pub(crate) fn tick(app: &mut LightcraftApp, ctx: &egui::Context) { pub(crate) fn show(app: &mut LightcraftApp, ctx: &egui::Context) { #[cfg(not(target_arch = "wasm32"))] { - match app.updates_rt.updater.show(ctx) { + // The restart helper is scheduled before the app closes, so no update starts over unsaved work. + let clean = app.session.unsaved().is_none(); + match app.updates_rt.updater.show_if(ctx, clean) { Action::OpenUrl(url) => ctx.open_url(egui::OpenUrl::new_tab(url)), Action::Close => ctx.send_viewport_cmd(egui::ViewportCommand::Close), Action::None => {} From 2d3c7275b2ace1c1548b5e5b6910434af61dcf7a Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 15:07:57 -0400 Subject: [PATCH 3/5] fix(updates): drop the manual window's custom id; pin craft-update to craft-libs main (db19c37) --- Cargo.lock | 18 +++++++++--------- Cargo.toml | 2 +- crates/ui-egui/src/updates_ui.rs | 1 - 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d85066e8b..11c7b1d73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1110,7 +1110,7 @@ dependencies = [ [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=5b0f3c572a3b5c1e51bdb90d7ff788e0212ec1cc#5b0f3c572a3b5c1e51bdb90d7ff788e0212ec1cc" +source = "git+https://github.com/storytold/craft-libs?rev=db19c373c8d59a545972ca340987aecfb3611a4b#db19c373c8d59a545972ca340987aecfb3611a4b" dependencies = [ "chrono", "dunce", @@ -1720,7 +1720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3850,7 +3850,7 @@ dependencies = [ "once_cell", "png", "thiserror 2.0.21", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4519,7 +4519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5429,7 +5429,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5486,7 +5486,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6056,7 +6056,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.5", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6302,7 +6302,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6956,7 +6956,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a8c96ad2f..2bb309f91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ repository = "https://github.com/storytold/lightcraft" [workspace.dependencies] craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "25da1314d443171b4adeaad2cd9454b273f35c6f", features = ["serde"] } craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "82a24b006eefa036b43c09a67fb9a3fbd17ffe63" } -craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "5b0f3c572a3b5c1e51bdb90d7ff788e0212ec1cc", features = ["egui"] } +craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "db19c373c8d59a545972ca340987aecfb3611a4b", features = ["egui"] } # internal lightcraft-denoise-core = { path = "crates/denoise-core" } lightcraft-denoise = { path = "crates/denoise", default-features = false } diff --git a/crates/ui-egui/src/updates_ui.rs b/crates/ui-egui/src/updates_ui.rs index 9d09ff09e..6ef3a97bc 100644 --- a/crates/ui-egui/src/updates_ui.rs +++ b/crates/ui-egui/src/updates_ui.rs @@ -94,7 +94,6 @@ pub(crate) fn show(app: &mut LightcraftApp, ctx: &egui::Context) { if app.updates_rt.manual_window_open { let mut open = true; egui::Window::new(crate::i18n::tr("LightCraft Updates")) - .id(egui::Id::new("lightcraft_manual_updates")) .open(&mut open) .collapsible(false) .resizable(false) From ee487850909ab73e3334bd5ef0c19a981c03f842 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 15:17:03 -0400 Subject: [PATCH 4/5] fix(updates): address review feedback Wake the UI when a manual check is queued from a menu callback; the update prompt owns the keyboard while it is open (LightCraft); Download opens through the desktop URL hook (FilmCraft); the Help menu dispatches through menus::invoke (FigCraft); print jobs and comment drafts hold an install back (WordCraft); Check for Updates is a registered command (GridCraft); an install holds back while documents are unsaved. --- crates/ui-egui/src/lib.rs | 2 +- crates/ui-egui/src/updates_ui.rs | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 71c2cef13..be1a9e879 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -2127,7 +2127,7 @@ impl LightcraftApp { // Recent-library checks and launch-list keys read focus/time/keys: they belong to the UI // phase, not the hidden-capable `logic` tick (plan §4.5 / migration input contract). panels::library_problem::logic(self, ctx); - if self.fonts_ready { + if self.fonts_ready && !updates_ui::blocks_input(self) { shortcuts::handle(self, ctx); } } diff --git a/crates/ui-egui/src/updates_ui.rs b/crates/ui-egui/src/updates_ui.rs index 6ef3a97bc..92f421e45 100644 --- a/crates/ui-egui/src/updates_ui.rs +++ b/crates/ui-egui/src/updates_ui.rs @@ -15,6 +15,9 @@ pub(crate) struct Runtime { startup_check: bool, #[cfg(not(target_arch = "wasm32"))] check_requested: bool, + /// The context of the last frame, to wake the UI when a check is queued from a menu callback. + #[cfg(not(target_arch = "wasm32"))] + repaint: Option, pub(crate) manual_window_open: bool, pub(crate) status_message: Option, } @@ -28,6 +31,8 @@ impl Default for Runtime { startup_check: false, #[cfg(not(target_arch = "wasm32"))] check_requested: false, + #[cfg(not(target_arch = "wasm32"))] + repaint: None, manual_window_open: false, status_message: None, } @@ -42,10 +47,26 @@ impl LightcraftApp { } } +/// The update prompt owns the keyboard while it is open: shortcuts must not reach the library behind it. +pub(crate) fn blocks_input(app: &LightcraftApp) -> bool { + #[cfg(not(target_arch = "wasm32"))] + { + app.updates_rt.updater.prompt_open() + } + #[cfg(target_arch = "wasm32")] + { + let _ = app; + false + } +} + pub(crate) fn check(app: &mut LightcraftApp) -> Result { #[cfg(not(target_arch = "wasm32"))] { app.updates_rt.check_requested = true; + if let Some(ctx) = &app.updates_rt.repaint { + ctx.request_repaint(); + } Ok(Value::Null) } #[cfg(target_arch = "wasm32")] @@ -60,6 +81,7 @@ pub(crate) fn tick(app: &mut LightcraftApp, ctx: &egui::Context) { #[cfg(not(target_arch = "wasm32"))] { let rt = &mut app.updates_rt; + rt.repaint = Some(ctx.clone()); if std::mem::take(&mut rt.check_requested) { rt.updater.check_now(ctx); } else if rt.startup_check { From 015ca9eebae768d45149c8e32d1c4c2d60993ad8 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Sun, 25 Oct 2026 15:34:06 -0400 Subject: [PATCH 5/5] fix(updates): localize the update status messages and format the sources --- crates/ui-egui/src/updates_ui.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/updates_ui.rs b/crates/ui-egui/src/updates_ui.rs index 92f421e45..5e159a0f4 100644 --- a/crates/ui-egui/src/updates_ui.rs +++ b/crates/ui-egui/src/updates_ui.rs @@ -107,9 +107,15 @@ pub(crate) fn show(app: &mut LightcraftApp, ctx: &egui::Context) { let rt = &mut app.updates_rt; (rt.manual_window_open, rt.status_message) = match rt.updater.status() { Status::Idle => (false, None), - Status::Checking => (true, Some("Checking for updates…".to_string())), - Status::UpToDate => (true, Some(format!("LightCraft {} is currently the newest version available.", env!("CARGO_PKG_VERSION")))), - Status::Failed(e) => (true, Some(format!("Could not check for updates:\n{e}"))), + Status::Checking => (true, Some(crate::i18n::tr("Checking for updates…").to_string())), + Status::UpToDate => ( + true, + Some( + crate::i18n::tr("LightCraft {version} is currently the newest version available.") + .replace("{version}", env!("CARGO_PKG_VERSION")), + ), + ), + Status::Failed(e) => (true, Some(crate::i18n::tr("Could not check for updates:\n{error}").replace("{error}", e))), }; }