Compare commits

...
Author SHA1 Message Date
Dylan M. Taylor 2f02324b6a Release 0.17.0 2026-11-09 00:29:33 -05:00
Dylan M. Taylor 31d9ba484b Merge main into release 2026-11-09 00:29:27 -05:00
Dylan M. Taylor 7b8c0ba5eb Merge pull request #6070 from dotCooCoo/chore/craft-update-1b34eba41
Updates: take craft-update from craft-libs main at 1b34eba41
2026-11-08 23:01:20 -05:00
dotCooCoo 0b61f8b426 Updates: take craft-update from craft-libs main at 1b34eba41 2026-11-08 19:43:58 -08:00
Dylan M. Taylor 0a5d5d29f8 Merge pull request #6038 from dotCooCoo/chore/craft-update-updater-fixes
Updates: the updater reports installing, ready, installed and failed, and installs at exit
2026-11-08 22:12:29 -05:00
dotCooCoo 642f5dd6bf Updates: the updater reports installing, ready, installed and failed, and installs at exit
Moves craft-update, craft-pdf and the krilla patch from storytold/craft-libs
c5a1cbfb to dotCooCoo/craft-libs c7c84230, which is craft-libs main at
0a9d1f48 with storytold/craft-libs#687 and #696 merged in.

- The result window opens whenever the updater's status is not idle: at
  the first start after an update it says the version that was installed
  or why the update failed, and Help > Check for Updates shows Installing,
  Ready and the launcher's choice. A check asked for from the ribbon now
  shows the window on the next frame.
- Restart to Update closes the window and asks the app to quit. An update
  that asks the app to close quits every window, after asking about each
  document with unsaved changes.
- on_exit and the macOS terminate path start the waiting update's helper.
- The update prompt and the result window draw their words through the
  interface catalog.
2026-11-08 10:56:53 -08:00
FredAppsandClaude Opus 5.5 02a802cbaf Release 0.16.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 20:32:38 +01:00
FredAppsandClaude Opus 5.5 6e266e8525 Merge main into release
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 20:32:38 +01:00
Dylan M. Taylor cfcc8c2aec Merge main into release 2026-11-04 21:12:48 -05:00
Dylan M. Taylor 64a3badfe3 Merge main into release 2026-11-04 21:07:43 -05:00
Dylan M. Taylor edfea68101 Release 0.15.2 2026-11-04 19:49:42 -05:00
Dylan M. Taylor 74d3701c74 Release 0.15.2 2026-11-04 19:46:26 -05:00
Dylan M. Taylor 8512ac6f36 Revert the release environment on the workflow call 2026-11-04 19:39:27 -05:00
Dylan M. Taylor 421700e890 Run the signed release from the release environment 2026-11-04 19:36:14 -05:00
Dylan M. Taylor d69cbec87f Release 0.15.2 2026-11-04 18:49:09 -05:00
4 changed files with 139 additions and 32 deletions
Generated
+22 -23
View File
@@ -1081,7 +1081,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/storytold/craft-libs?rev=1b34eba41e62138fc2438d2734523b302840b45d#1b34eba41e62138fc2438d2734523b302840b45d"
dependencies = [
"krilla",
"thiserror 2.0.21",
@@ -1135,7 +1135,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/storytold/craft-libs?rev=1b34eba41e62138fc2438d2734523b302840b45d#1b34eba41e62138fc2438d2734523b302840b45d"
dependencies = [
"chrono",
"dunce",
@@ -2981,7 +2981,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/storytold/craft-libs?rev=1b34eba41e62138fc2438d2734523b302840b45d#1b34eba41e62138fc2438d2734523b302840b45d"
dependencies = [
"base64 0.22.1",
"bumpalo",
@@ -6806,7 +6806,7 @@ dependencies = [
[[package]]
name = "wordcraft"
version = "0.15.1"
version = "0.17.0"
dependencies = [
"arboard",
"chrono",
@@ -6844,7 +6844,7 @@ dependencies = [
[[package]]
name = "wordcraft-cli"
version = "0.15.1"
version = "0.17.0"
dependencies = [
"serde_json",
"sys-locale",
@@ -6857,11 +6857,11 @@ dependencies = [
[[package]]
name = "wordcraft-control-key"
version = "0.15.1"
version = "0.17.0"
[[package]]
name = "wordcraft-doc"
version = "0.15.1"
version = "0.17.0"
dependencies = [
"base64 0.23.1",
"log",
@@ -6875,7 +6875,7 @@ dependencies = [
[[package]]
name = "wordcraft-docbin"
version = "0.15.1"
version = "0.17.0"
dependencies = [
"cfb",
"chrono",
@@ -6892,7 +6892,7 @@ dependencies = [
[[package]]
name = "wordcraft-docx"
version = "0.15.1"
version = "0.17.0"
dependencies = [
"aes",
"base64 0.23.1",
@@ -6917,7 +6917,7 @@ dependencies = [
[[package]]
name = "wordcraft-engine"
version = "0.15.1"
version = "0.17.0"
dependencies = [
"cfb",
"chrono",
@@ -6950,7 +6950,7 @@ dependencies = [
[[package]]
name = "wordcraft-fonts"
version = "0.15.1"
version = "0.17.0"
dependencies = [
"brotli-decompressor",
"harfrust 0.13.3",
@@ -6964,7 +6964,7 @@ dependencies = [
[[package]]
name = "wordcraft-formats"
version = "0.15.1"
version = "0.17.0"
dependencies = [
"aes",
"base64 0.23.1",
@@ -6991,7 +6991,7 @@ dependencies = [
[[package]]
name = "wordcraft-geom"
version = "0.15.1"
version = "0.17.0"
dependencies = [
"image",
"serde",
@@ -6999,7 +6999,7 @@ dependencies = [
[[package]]
name = "wordcraft-layout"
version = "0.15.1"
version = "0.17.0"
dependencies = [
"craft-proof",
"image",
@@ -7019,7 +7019,7 @@ dependencies = [
[[package]]
name = "wordcraft-mcp"
version = "0.15.1"
version = "0.17.0"
dependencies = [
"log",
"serde_json",
@@ -7031,7 +7031,7 @@ dependencies = [
[[package]]
name = "wordcraft-metafile"
version = "0.15.1"
version = "0.17.0"
dependencies = [
"kurbo",
"thiserror 2.0.21",
@@ -7039,7 +7039,7 @@ dependencies = [
[[package]]
name = "wordcraft-pdf"
version = "0.15.1"
version = "0.17.0"
dependencies = [
"chrono",
"craft-pdf",
@@ -7060,7 +7060,7 @@ dependencies = [
[[package]]
name = "wordcraft-render"
version = "0.15.1"
version = "0.17.0"
dependencies = [
"image",
"log",
@@ -7074,7 +7074,7 @@ dependencies = [
[[package]]
name = "wordcraft-ui-egui"
version = "0.15.1"
version = "0.17.0"
dependencies = [
"accesskit_consumer",
"cfb",
@@ -7107,7 +7107,7 @@ dependencies = [
[[package]]
name = "wordcraft-web"
version = "0.15.1"
version = "0.17.0"
dependencies = [
"craft-web-api",
"craft-web-host",
@@ -7128,7 +7128,7 @@ dependencies = [
[[package]]
name = "wordcraft-zotero"
version = "0.15.1"
version = "0.17.0"
dependencies = [
"log",
"proptest",
@@ -7291,7 +7291,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee"
[[package]]
name = "xtask"
version = "0.15.1"
version = "0.17.0"
dependencies = [
"serde_json",
]
@@ -7562,7 +7562,6 @@ dependencies = [
"nix",
"typed-path",
"windows-sys 0.61.2",
"zopfli",
]
[[package]]
+4 -4
View File
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
default-members = ["crates/*", "apps/wordcraft", "apps/wordcraft-cli", "xtask"]
[workspace.package]
version = "0.15.1"
version = "0.17.0"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
@@ -67,9 +67,9 @@ wuff = "0.2.9"
brotli-decompressor = "5"
vello_cpu = "0.2"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif", "bmp", "tiff"] }
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", version = "=0.1.0", default-features = false, features = ["write"] }
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "1b34eba41e62138fc2438d2734523b302840b45d", version = "=0.1.0", default-features = false, features = ["write"] }
# 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/storytold/craft-libs", version = "=0.1.0", rev = "1b34eba41e62138fc2438d2734523b302840b45d", features = ["egui"] }
quick-xml = "0.41"
zip = { version = "3", default-features = false, features = ["deflate"] }
# zlib streams (compressed metafile pictures in `.doc` drawings); already in the lock as a dependency of other crates.
@@ -134,7 +134,7 @@ debug = 0
# Required by craft-pdf: the backend rejects writers that failed during unwinding.
[patch.crates-io]
krilla = { git = "https://github.com/storytold/craft-libs", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7" }
krilla = { git = "https://github.com/storytold/craft-libs", rev = "1b34eba41e62138fc2438d2734523b302840b45d" }
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" }
+3
View File
@@ -133,6 +133,7 @@ impl eframe::App for App {
if let Some(key_file) = &self.2 {
key_file.remove();
}
self.0.start_pending_update();
}
}
@@ -192,6 +193,8 @@ fn schedule_macos_terminate(app: &mut WordApp, key_file: Option<&control_server:
if let Some(key_file) = key_file {
key_file.remove();
}
// `terminate:` ends the process without dropping the app, so a ready update's helper starts here.
app.start_pending_update();
dispatch2::DispatchQueue::main().exec_async(|| {
if let Some(mtm) = objc2::MainThreadMarker::new() {
objc2_app_kit::NSApplication::sharedApplication(mtm).terminate(None);
+110 -5
View File
@@ -29,9 +29,9 @@ impl Default for Runtime {
fn default() -> Self {
Self {
#[cfg(not(target_arch = "wasm32"))]
updater: Updater::new(&crate::UPDATE_APP, env!("CARGO_PKG_VERSION")),
updater: Updater::new(&crate::UPDATE_APP, env!("CARGO_PKG_VERSION")).with_translator(translate),
#[cfg(not(target_arch = "wasm32"))]
result_dialog: CheckResultDialog::new(&crate::UPDATE_APP, env!("CARGO_PKG_VERSION")),
result_dialog: CheckResultDialog::new(&crate::UPDATE_APP, env!("CARGO_PKG_VERSION")).with_translator(translate),
#[cfg(not(target_arch = "wasm32"))]
startup_check: false,
#[cfg(not(target_arch = "wasm32"))]
@@ -45,6 +45,13 @@ impl Default for Runtime {
}
}
/// The update prompt's and the result window's words in the interface language: craft-update
/// passes its English text and the values of its `{name}` placeholders each time it draws.
#[cfg(not(target_arch = "wasm32"))]
fn translate(text: &str, args: &[(&str, &str)]) -> String {
crate::i18n::fmt(crate::i18n::t(text), args)
}
impl WordApp {
/// Whether the app asks for a newer release when it starts (off until the desktop app turns it on).
#[cfg(not(target_arch = "wasm32"))]
@@ -59,7 +66,15 @@ impl WordApp {
pub fn disable_self_update(&mut self) {
let rt = &mut self.updates_rt;
rt.startup_check = false;
rt.updater = Updater::new(&crate::UPDATE_APP, env!("CARGO_PKG_VERSION")).with_source(refused_check());
rt.updater = Updater::new(&crate::UPDATE_APP, env!("CARGO_PKG_VERSION")).with_translator(translate).with_source(refused_check());
}
/// Starts the helper of an update that is ready, so that it installs once the process has
/// ended. Call it once the exit is certain, before a path that ends the process without
/// dropping the app. Later calls do nothing.
#[cfg(not(target_arch = "wasm32"))]
pub fn start_pending_update(&mut self) {
self.updates_rt.updater.start_pending_update();
}
}
@@ -107,15 +122,23 @@ pub(crate) fn tick(app: &mut WordApp, ctx: &egui::Context) {
pub(crate) fn show(app: &mut WordApp, ctx: &egui::Context) {
#[cfg(not(target_arch = "wasm32"))]
{
// The restart helper is scheduled before the app closes, so no update starts over unsaved work.
// An update starts, and a ready one asks the app to close, only while no work is unsaved.
let clean = !(app.session.dirty || app.save_unconfirmed() || app.printing.busy() || crate::panes::has_comment_drafts(app));
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),
// The whole app quits, every window included, and first asks about each document with
// unsaved changes.
Action::Close => {
let _ = app.ask_before_quit();
}
Action::None => {}
}
// The dialog answers Help ▸ Check for Updates; a found update leaves the status idle and the prompt above takes over.
// It also reports what the last update came to, which the check at start leaves as the status.
let rt = &mut app.updates_rt;
if !matches!(rt.updater.status(), Status::Idle) {
rt.manual_window_open = true;
}
if rt.manual_window_open {
match rt.result_dialog.show(ctx, rt.updater.status()) {
CheckResultResponse::None => {}
@@ -124,6 +147,13 @@ pub(crate) fn show(app: &mut WordApp, ctx: &egui::Context) {
rt.updater.dismiss_status();
}
CheckResultResponse::CheckAgain => rt.updater.check_now(ctx),
// The window closes, and the next frame's `show_if` asks the app to quit once the
// shown document has no unsaved work.
CheckResultResponse::RestartToUpdate => {
rt.updater.restart_to_update();
rt.manual_window_open = false;
rt.updater.dismiss_status();
}
}
// An update was found: the prompt replaces the dialog.
if matches!(rt.updater.status(), Status::Idle) {
@@ -178,4 +208,79 @@ mod tests {
assert!(matches!(status, Status::Failed(why) if why.contains("another program's AppImage")), "{status:?}");
assert!(!app.updates_rt.updater.prompt_open(), "no update offered");
}
/// Help ▸ Check for Updates runs while the ribbon draws, after the frame's `tick`: the result
/// window shows the check from the next frame on.
#[test]
fn a_check_asked_for_while_drawing_opens_the_result_window() {
let mut app = crate::WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), crate::Services::default());
let pins = std::env::temp_dir().join(format!("wordcraft-no-pins-{}.toml", std::process::id()));
app.updates_rt.updater = craft_update::updater::Updater::new(&crate::UPDATE_APP, env!("CARGO_PKG_VERSION"))
.with_source(std::sync::Arc::new(|| Err("offline".into())))
.with_pins_path(pins);
let ctx = egui::Context::default();
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
super::tick(&mut app, ui.ctx());
super::check(&mut app);
super::show(&mut app, ui.ctx());
});
out.textures_delta.clear();
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
super::tick(&mut app, ui.ctx());
super::show(&mut app, ui.ctx());
});
out.textures_delta.clear();
let status = app.updates_rt.updater.status();
assert!(matches!(status, Status::Checking | Status::Failed(_)), "{status:?}");
assert!(app.updates_rt.manual_window_open, "the result window shows the check ({status:?})");
}
/// The first start after an update reports what it came to without a menu click: the check at
/// start reads the record, the result window opens, and Esc closes it and clears the status.
#[test]
fn the_last_update_opens_the_result_window_until_it_is_closed() {
let version = env!("CARGO_PKG_VERSION");
// The files craft-update writes for an installed update and for a failed one.
let cases: [(&str, &str, fn(&Status) -> bool); 2] = [
("data/self-update-done.txt", version, |s| matches!(s, Status::Updated(v) if v == env!("CARGO_PKG_VERSION"))),
("data/logs/self-update-error.txt", "disk full", |s| matches!(s, Status::InstallFailed(why) if why == "disk full")),
];
for (n, (file, text, expected)) in cases.into_iter().enumerate() {
let root = std::env::temp_dir().join(format!("wordcraft-last-update-{n}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let record = root.join(file);
std::fs::create_dir_all(record.parent().unwrap()).unwrap();
std::fs::write(&record, text).unwrap();
let mut app = crate::WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), crate::Services::default());
app.updates_rt.updater = craft_update::updater::Updater::new(&crate::UPDATE_APP, version)
.with_source(std::sync::Arc::new(|| Ok(None)))
.with_paths(craft_update::Paths::under(&root))
.with_pins_path(root.join("no-pins.toml"));
app.set_startup_update_check(true);
let ctx = egui::Context::default();
let frame = |app: &mut crate::WordApp, events: Vec<egui::Event>| {
let input = egui::RawInput { events, ..Default::default() };
let mut out = ctx.run_ui(input, |ui| {
super::tick(app, ui.ctx());
super::show(app, ui.ctx());
});
out.textures_delta.clear();
};
frame(&mut app, Vec::new());
let status = app.updates_rt.updater.status();
assert!(expected(status), "{file}: {status:?}");
assert!(app.updates_rt.manual_window_open, "{file}: the result window opens by itself");
assert!(!record.exists(), "{file}: the record is read once");
frame(&mut app, Vec::new());
assert!(app.updates_rt.manual_window_open, "{file}: the window stays open");
let escape =
egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE };
frame(&mut app, vec![escape]);
assert!(!app.updates_rt.manual_window_open, "{file}: Esc closes the window");
assert_eq!(app.updates_rt.updater.status(), &Status::Idle, "{file}: closing clears the status");
frame(&mut app, Vec::new());
assert!(!app.updates_rt.manual_window_open, "{file}: the window stays closed");
let _ = std::fs::remove_dir_all(&root);
}
}
}