From 24cb6018b6645f962d958546b77e3405a001b5c5 Mon Sep 17 00:00:00 2001 From: Tristan von Kries Date: Sun, 11 Oct 2026 19:11:51 +0200 Subject: [PATCH] macOS: the reopen-last-session preference starts from NSQuitAlwaysKeepsWindows (#871) (#1053) System Settings > Desktop & Dock > "Close windows when quitting an application" is the system's own version of Preferences > "Reopen the files that were open when PdfCraft last closed". Read it once at launch (defaults read -g, 500 ms guard, as text_scale.rs does for gsettings) and use it as the preference's starting value, before the saved preferences are restored. A choice made in Preferences is persisted and still wins; other systems keep starting off. Co-authored-by: Tristan von Kries Co-authored-by: Claude Fable 5.1 --- apps/pdfcraft/src/main.rs | 3 ++ apps/pdfcraft/src/reopen.rs | 72 +++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 apps/pdfcraft/src/reopen.rs diff --git a/apps/pdfcraft/src/main.rs b/apps/pdfcraft/src/main.rs index be123fcef..fca781e5c 100644 --- a/apps/pdfcraft/src/main.rs +++ b/apps/pdfcraft/src/main.rs @@ -33,6 +33,7 @@ use pdfcraft_ui_egui::PdfCraftApp; mod apple_events; mod compositor_gpu; mod logging; +mod reopen; mod single_instance; mod text_scale; mod updates; @@ -300,6 +301,8 @@ fn app_creator<'a>( Box::new(move |cc| { started.set(true); let mut app = PdfCraftApp::new(); + // The system's reopen-windows choice (macOS) starts the preference; a saved choice replaces it. + app.reopen_last_session = reopen::system_default(); if let Some(json) = cc.storage.and_then(|s| s.get_string("pdfcraft").or_else(|| s.get_string(LEGACY_STORAGE_KEY))) { app.restore(&json); } diff --git a/apps/pdfcraft/src/reopen.rs b/apps/pdfcraft/src/reopen.rs new file mode 100644 index 000000000..1280b4579 --- /dev/null +++ b/apps/pdfcraft/src/reopen.rs @@ -0,0 +1,72 @@ +//! The system's choice for Preferences ▸ "Reopen the files that were open when PdfCraft last closed". +//! +//! On macOS, System Settings ▸ Desktop & Dock ▸ Windows ▸ "Close windows when quitting an +//! application" is on by default; turning it off stores `NSQuitAlwaysKeepsWindows = 1`, and apps +//! then reopen their windows on the next launch. A user who turned it off expects PdfCraft to +//! bring its documents back too (#871), so that setting is the preference's starting value. It +//! is read once at launch through `defaults`; the user's own choice in Preferences, once saved, +//! wins over it. Other systems have no such setting, and the preference starts off there. + +/// How long `defaults` may take before launch goes on without it. +#[cfg(target_os = "macos")] +const TIMEOUT: std::time::Duration = std::time::Duration::from_millis(500); + +/// Whether reopening the last session starts on, before the saved preferences are applied. +pub fn system_default() -> bool { + #[cfg(target_os = "macos")] + if let Some(output) = defaults(&["read", "-g", "NSQuitAlwaysKeepsWindows"]) { + return keeps_windows(&output); + } + false +} + +/// `defaults read` output for a boolean (`1`, `0`, `true`, `false`): true only when it says so. +#[cfg_attr(not(target_os = "macos"), allow(dead_code))] +fn keeps_windows(output: &str) -> bool { + matches!(output.trim(), "1" | "true" | "yes" | "YES") +} + +/// Run `defaults` and return what it printed, or `None` if it fails or hangs. Fails when the key +/// was never set, which means the system default: close windows when quitting. +#[cfg(target_os = "macos")] +fn defaults(args: &[&str]) -> Option { + use std::io::Read; + use std::process::{Command, Stdio}; + // The absolute path keeps a `defaults` earlier on PATH from running. + let mut child = Command::new("/usr/bin/defaults").args(args).stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::null()).spawn().ok()?; + let start = std::time::Instant::now(); + loop { + match child.try_wait() { + Ok(Some(status)) if status.success() => break, + Ok(None) if start.elapsed() < TIMEOUT => std::thread::sleep(std::time::Duration::from_millis(5)), + _ => { + let _ = child.kill(); + let _ = child.wait(); + return None; + } + } + } + let mut out = String::new(); + // A boolean fits in far less; a misbehaving `defaults` can't make launch read without end. + child.stdout.take()?.take(256).read_to_string(&mut out).ok()?; + Some(out) +} + +#[cfg(test)] +mod tests { + #[test] + fn only_a_set_flag_keeps_windows() { + for on in ["1\n", "1", "true", "YES"] { + assert!(super::keeps_windows(on), "{on:?}"); + } + for off in ["0\n", "0", "false", "NO", "", "garbage"] { + assert!(!super::keeps_windows(off), "{off:?}"); + } + } + + #[test] + fn reading_the_system_default_never_fails() { + // Whatever this machine is set to, the read returns a value within the timeout. + let _ = super::system_default(); + } +}