diff --git a/Cargo.lock b/Cargo.lock index 02be4689..735fd68c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3679,15 +3679,16 @@ dependencies = [ [[package]] name = "ship-shape" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d8d8a8c8427c40bf6c501aa42af704d1be34ffa35bd3f87dd569a4fff81899" +checksum = "4509036deff18ba59490ea407c32a5af270578d3243b69365cdc73a829af4a3e" dependencies = [ "homeport", "minisign-verify", "patois", "pulldown-cmark", "serde", + "tempfile", "ureq", "wxdragon", ] diff --git a/crates/paperback/Cargo.toml b/crates/paperback/Cargo.toml index 940dd74b..5c18a098 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 diff --git a/crates/paperback/src/ui.rs b/crates/paperback/src/ui.rs index 1aecbc8a..4869ec64 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 d8beed69..21760081 100644 --- a/crates/paperback/src/ui/app.rs +++ b/crates/paperback/src/ui/app.rs @@ -362,3 +362,30 @@ 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().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); + } + if let Some(app) = wxdragon::app::get_app_instance() { + app.exit_main_loop(); + } +} diff --git a/crates/paperback/src/ui/main_window.rs b/crates/paperback/src/ui/main_window.rs index 6bf5bb4b..4cc6554f 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 de605a0d..c0e54028 100644 --- a/crates/paperback/src/updater.rs +++ b/crates/paperback/src/updater.rs @@ -98,25 +98,39 @@ 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), - ); + 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, 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); }