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 <[email protected]>
Co-authored-by: Claude Fable 5.1 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-11 13:11:51 -04:00
1 parent 96538312df
commit 24cb6018b6
2 files changed
+75

No files matched your search

+3
View File
@@ -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);
}
+72
View File
@@ -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<String> {
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();
}
}