From 0ceefdb040d9487686f8ca4e9b0266b3e4c376e4 Mon Sep 17 00:00:00 2001 From: Johann Date: Fri, 2 Oct 2026 22:14:20 +0800 Subject: [PATCH 1/3] Save state before macOS update installation Use ship-shape's installer-ready callback to force-close the main window through Paperback's normal persistence and cleanup handler, then leave the application event loop. This saves settings, document positions and window geometry, stops audio and timers, and bypasses macOS's hide-on-close veto so the update helper can replace and relaunch the app. Pass the resolved settings directory through Launch Services when the new app is launched, preserving custom and isolated configurations. Pin the companion ship-shape PR commit so this draft builds without a local checkout. Replace the temporary Git patch with the published crate version after trypsynth/ship-shape#6 is merged and released. Co-Authored-By: Codex --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++++ crates/paperback/src/ui.rs | 2 ++ crates/paperback/src/ui/app.rs | 12 ++++++++++++ crates/paperback/src/updater.rs | 12 +++++++++++- 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 02be46894..2e046fd0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3680,14 +3680,14 @@ dependencies = [ [[package]] name = "ship-shape" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d8d8a8c8427c40bf6c501aa42af704d1be34ffa35bd3f87dd569a4fff81899" +source = "git+https://github.com/johanntan/ship-shape?rev=41969f2e79010ef5e372710009af46b3baa78ac4#41969f2e79010ef5e372710009af46b3baa78ac4" dependencies = [ "homeport", "minisign-verify", "patois", "pulldown-cmark", "serde", + "tempfile", "ureq", "wxdragon", ] diff --git a/Cargo.toml b/Cargo.toml index 139d8a85d..06ce8afe4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,10 @@ codegen-units = 1 # Fixed upstream in zip-rs/zip2#962, merged but not yet published: 9.0.0-pre3 is still the newest # version on crates.io. Drop this once the fix reaches a release. [patch.crates-io] +# Automatic macOS installation and its shutdown callback are not released yet. +# Companion PR: https://github.com/trypsynth/ship-shape/pull/6 +# Replace this pin with a crates.io dependency after that change is released. +ship-shape = { git = "https://github.com/johanntan/ship-shape", rev = "41969f2e79010ef5e372710009af46b3baa78ac4" } # Pinned to wxDragon main for the binary-size work (its #224 and #225) and the tree item data fix # (#241), none of it released yet. Swap back to a plain version requirement once a release carries them. wxdragon = { git = "https://github.com/AllenDang/wxDragon", rev = "82356530e28062b0909de96b6831a585cae7256c" } diff --git a/crates/paperback/src/ui.rs b/crates/paperback/src/ui.rs index 1aecbc8a9..4869ec64e 100644 --- a/crates/paperback/src/ui.rs +++ b/crates/paperback/src/ui.rs @@ -26,6 +26,8 @@ mod tray; mod window_geometry; pub use app::PaperbackApp; +#[cfg(target_os = "macos")] +pub use app::quit_for_update; #[cfg(target_os = "linux")] pub use dialogs::{AssociationChoice, ChoiceAction, show_linux_setup_dialog}; pub use main_window::MainWindow; diff --git a/crates/paperback/src/ui/app.rs b/crates/paperback/src/ui/app.rs index d8beed69b..adeacd1b6 100644 --- a/crates/paperback/src/ui/app.rs +++ b/crates/paperback/src/ui/app.rs @@ -362,3 +362,15 @@ fn send_ipc_command(command: &IpcCommand) { #[cfg(target_os = "linux")] pipe_unix::send(&payload); } + +/// The updater calls this on the UI thread only once its helper is ready. Force-close runs the +/// normal persistence and audio cleanup without macOS's hide-on-close veto. +#[cfg(target_os = "macos")] +pub fn quit_for_update() { + if let Some(window) = main_window_from_ptr() { + window.frame().close(true); + } + if let Some(app) = wxdragon::app::get_app_instance() { + app.exit_main_loop(); + } +} diff --git a/crates/paperback/src/updater.rs b/crates/paperback/src/updater.rs index de605a0d4..35a805531 100644 --- a/crates/paperback/src/updater.rs +++ b/crates/paperback/src/updater.rs @@ -109,7 +109,8 @@ pub fn run_update_check(parent: &Frame, silent: bool, channel: UpdateChannel) { .with_commit(version::COMMIT_HASH) .with_install_kind(install_kind) .with_user_agent(version::user_agent()) - .with_asset_suffix(UPDATE_ASSET_SUFFIX), + .with_asset_suffix(UPDATE_ASSET_SUFFIX) + .with_macos_relaunch_env("PAPERBACK_CONFIG_DIR", crate::config_ext::config_dir().to_string_lossy()), ); beat_foreground_grant(); let ship_channel = match channel { @@ -117,6 +118,15 @@ pub fn run_update_check(parent: &Frame, silent: bool, channel: UpdateChannel) { UpdateChannel::Dev => ShipChannel::Dev, }; let trigger = if silent { CheckTrigger::Automatic } else { CheckTrigger::Manual }; + #[cfg(target_os = "macos")] + ship_shape::ui::run_update_check_with_exit_handler( + config, + parent, + ship_channel, + trigger, + crate::ui::quit_for_update, + ); + #[cfg(not(target_os = "macos"))] ship_shape::ui::run_update_check(config, parent, ship_channel, trigger); } From 5b54c1cd4c238e5c323b13d7dc866bc37e53b142 Mon Sep 17 00:00:00 2001 From: Johann Date: Sat, 3 Oct 2026 10:25:03 +0800 Subject: [PATCH 2/3] Announce update restarts and preserve explicit overrides Forward PAPERBACK_CONFIG_DIR only when it is present in this process's environment. Preserve its native value so relaunch does not synthesize an override for the default settings directory or lose non-UTF-8 path bytes. Post a translated high-priority live-region announcement before updater shutdown. Disable the frame and keep the UI event loop running for a brief grace period so VoiceOver can receive the asynchronous notification, then run the existing forced-close persistence and cleanup path. Update the pinned ship-shape revision and lockfile to the reviewed signer, stale-stage cleanup and log-retention fixes. Keep the integration draft pending the upstream crate release. Co-Authored-By: Codex --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/paperback/src/ui/app.rs | 15 ++++++++++++ crates/paperback/src/ui/main_window.rs | 13 +++++++++-- crates/paperback/src/updater.rs | 32 +++++++++++++++----------- 5 files changed, 46 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e046fd0b..abe7288e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3680,7 +3680,7 @@ dependencies = [ [[package]] name = "ship-shape" version = "0.3.0" -source = "git+https://github.com/johanntan/ship-shape?rev=41969f2e79010ef5e372710009af46b3baa78ac4#41969f2e79010ef5e372710009af46b3baa78ac4" +source = "git+https://github.com/johanntan/ship-shape?rev=bbbdc5761a54a9b0b742b0f393e28ae0548e895b#bbbdc5761a54a9b0b742b0f393e28ae0548e895b" dependencies = [ "homeport", "minisign-verify", diff --git a/Cargo.toml b/Cargo.toml index 06ce8afe4..6dcda1427 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ codegen-units = 1 # Automatic macOS installation and its shutdown callback are not released yet. # Companion PR: https://github.com/trypsynth/ship-shape/pull/6 # Replace this pin with a crates.io dependency after that change is released. -ship-shape = { git = "https://github.com/johanntan/ship-shape", rev = "41969f2e79010ef5e372710009af46b3baa78ac4" } +ship-shape = { git = "https://github.com/johanntan/ship-shape", rev = "bbbdc5761a54a9b0b742b0f393e28ae0548e895b" } # Pinned to wxDragon main for the binary-size work (its #224 and #225) and the tree item data fix # (#241), none of it released yet. Swap back to a plain version requirement once a release carries them. wxdragon = { git = "https://github.com/AllenDang/wxDragon", rev = "82356530e28062b0909de96b6831a585cae7256c" } diff --git a/crates/paperback/src/ui/app.rs b/crates/paperback/src/ui/app.rs index adeacd1b6..217600814 100644 --- a/crates/paperback/src/ui/app.rs +++ b/crates/paperback/src/ui/app.rs @@ -367,6 +367,21 @@ fn send_ipc_command(command: &IpcCommand) { /// normal persistence and audio cleanup without macOS's hide-on-close veto. #[cfg(target_os = "macos")] pub fn quit_for_update() { + if let Some(window) = main_window_from_ptr() { + window.frame().enable(false); + window.announce_update_restart(); + } + // macOS posts live-region announcements on the next main run-loop iteration. Keep the + // window alive briefly so VoiceOver can receive it; never block the UI thread. + std::thread::spawn(|| { + std::thread::sleep(std::time::Duration::from_secs(2)); + wxdragon::call_after(Box::new(finish_quit_for_update)); + wxdragon::wake_up_idle(); + }); +} + +#[cfg(target_os = "macos")] +fn finish_quit_for_update() { if let Some(window) = main_window_from_ptr() { window.frame().close(true); } diff --git a/crates/paperback/src/ui/main_window.rs b/crates/paperback/src/ui/main_window.rs index 6bf5bb4ba..4cc6554f2 100644 --- a/crates/paperback/src/ui/main_window.rs +++ b/crates/paperback/src/ui/main_window.rs @@ -54,7 +54,11 @@ pub struct MainWindow { config: Rc>, #[cfg(target_os = "windows")] tray_state: Rc>>, - _live_region_label: StaticText, + #[cfg_attr( + not(target_os = "macos"), + allow(dead_code, reason = "keeps the announcement label with the main window") + )] + live_region_label: StaticText, _find_dialog: Rc>>, #[cfg(target_os = "windows")] _hotkey_handle: Rc>>, @@ -336,7 +340,7 @@ impl MainWindow { config, #[cfg(target_os = "windows")] tray_state, - _live_region_label: live_region_label, + live_region_label, _find_dialog: find_dialog, #[cfg(target_os = "windows")] _hotkey_handle: hotkey_handle, @@ -514,6 +518,11 @@ impl MainWindow { &self.frame } + #[cfg(target_os = "macos")] + pub(super) fn announce_update_restart(&self) { + live_region::announce(self.live_region_label, &t("Installing the update. Paperback will restart.")); + } + fn ensure_parser_ready(&self, path: &Path) -> bool { ensure_parser_ready_for_path(&self.frame, path, &self.config) } diff --git a/crates/paperback/src/updater.rs b/crates/paperback/src/updater.rs index 35a805531..c0e540288 100644 --- a/crates/paperback/src/updater.rs +++ b/crates/paperback/src/updater.rs @@ -98,20 +98,24 @@ fn beat_foreground_grant() {} pub fn run_update_check(parent: &Frame, silent: bool, channel: UpdateChannel) { tracing::info!(channel = %channel, silent, "checking for updates"); let install_kind = if is_installer_distribution() { InstallKind::Installer } else { InstallKind::Portable }; - let config = Arc::new( - UpdaterConfig::new( - PAPERBACK_GITHUB_REPO, - "paperback", - "Paperback", - PAPERBACK_MINISIGN_KEY, - env!("CARGO_PKG_VERSION"), - ) - .with_commit(version::COMMIT_HASH) - .with_install_kind(install_kind) - .with_user_agent(version::user_agent()) - .with_asset_suffix(UPDATE_ASSET_SUFFIX) - .with_macos_relaunch_env("PAPERBACK_CONFIG_DIR", crate::config_ext::config_dir().to_string_lossy()), - ); + let config = UpdaterConfig::new( + PAPERBACK_GITHUB_REPO, + "paperback", + "Paperback", + PAPERBACK_MINISIGN_KEY, + env!("CARGO_PKG_VERSION"), + ) + .with_commit(version::COMMIT_HASH) + .with_install_kind(install_kind) + .with_user_agent(version::user_agent()) + .with_asset_suffix(UPDATE_ASSET_SUFFIX); + #[cfg(target_os = "macos")] + let config = if let Some(directory) = env::var_os("PAPERBACK_CONFIG_DIR") { + config.with_macos_relaunch_env("PAPERBACK_CONFIG_DIR", directory) + } else { + config + }; + let config = Arc::new(config); beat_foreground_grant(); let ship_channel = match channel { UpdateChannel::Stable => ShipChannel::Stable, From cc5a40fab9bf2d5836cbafcdc0a69a90ef4c6bf7 Mon Sep 17 00:00:00 2001 From: Quin Gillespie Date: Sat, 3 Oct 2026 16:28:28 -0600 Subject: [PATCH 3/3] Use ship-shape 0.4.0 from crates.io instead of the pinned PR commit --- Cargo.lock | 5 +++-- Cargo.toml | 4 ---- crates/paperback/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abe7288e2..735fd68c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3679,8 +3679,9 @@ dependencies = [ [[package]] name = "ship-shape" -version = "0.3.0" -source = "git+https://github.com/johanntan/ship-shape?rev=bbbdc5761a54a9b0b742b0f393e28ae0548e895b#bbbdc5761a54a9b0b742b0f393e28ae0548e895b" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4509036deff18ba59490ea407c32a5af270578d3243b69365cdc73a829af4a3e" dependencies = [ "homeport", "minisign-verify", diff --git a/Cargo.toml b/Cargo.toml index 6dcda1427..139d8a85d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,10 +60,6 @@ codegen-units = 1 # Fixed upstream in zip-rs/zip2#962, merged but not yet published: 9.0.0-pre3 is still the newest # version on crates.io. Drop this once the fix reaches a release. [patch.crates-io] -# Automatic macOS installation and its shutdown callback are not released yet. -# Companion PR: https://github.com/trypsynth/ship-shape/pull/6 -# Replace this pin with a crates.io dependency after that change is released. -ship-shape = { git = "https://github.com/johanntan/ship-shape", rev = "bbbdc5761a54a9b0b742b0f393e28ae0548e895b" } # Pinned to wxDragon main for the binary-size work (its #224 and #225) and the tree item data fix # (#241), none of it released yet. Swap back to a plain version requirement once a release carries them. wxdragon = { git = "https://github.com/AllenDang/wxDragon", rev = "82356530e28062b0909de96b6831a585cae7256c" } diff --git a/crates/paperback/Cargo.toml b/crates/paperback/Cargo.toml index 940dd74be..5c18a098f 100644 --- a/crates/paperback/Cargo.toml +++ b/crates/paperback/Cargo.toml @@ -35,7 +35,7 @@ rodio-wsola = "0.2.0" tracing = "0.1.44" tracing-appender = "0.2.5" tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } -ship-shape = "0.3.0" +ship-shape = "0.4.0" toml = { workspace = true } wx-utils = { version = "0.4.1", features = ["shortcuts"] } # `default-features = false` turns off the seven size knobs wxdragon enables by default, all of