diff --git a/Cargo.lock b/Cargo.lock index 6bcd2d3d28..0fded78442 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1243,7 +1243,7 @@ dependencies = [ [[package]] name = "craft-print" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=1146cdc4f92e6c5c4fc3d67efbf0e1cec436a10c#1146cdc4f92e6c5c4fc3d67efbf0e1cec436a10c" +source = "git+https://github.com/storytold/craft-libs?rev=1ee01a4c58729d781638b8afbd8982726dbdefcb#1ee01a4c58729d781638b8afbd8982726dbdefcb" dependencies = [ "craft-winprint-gdi", "log", diff --git a/Cargo.toml b/Cargo.toml index e6f29521ca..a967af58c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ craft-keymap = { git = "https://github.com/storytold/craft-libs", version = "0.1 # the commit in `craft-i18n.rev`). Already in the tree through craft-ui. craft-i18n = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "787a7a0f972fd1d7a7618fdbeca3a5267a0407ff" } # Shared system spooler; document composition remains in pdfcraft-print. -craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "1146cdc4f92e6c5c4fc3d67efbf0e1cec436a10c" } +craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "1ee01a4c58729d781638b8afbd8982726dbdefcb" } # Whether a path stored in a document may be opened (GoToR link targets). craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "787a7a0f972fd1d7a7618fdbeca3a5267a0407ff" } # craft-update: app-specific pretend versions apply to startup and manual checks. diff --git a/crates/print/src/spool.rs b/crates/print/src/spool.rs index f80b349916..73a267daa5 100644 --- a/crates/print/src/spool.rs +++ b/crates/print/src/spool.rs @@ -2,8 +2,8 @@ //! stay in PdfCraft; these adapters retain the application's print-error surface. pub use craft_print::{ - Duplex, HANDLED_OPTIONS, HAS_PRINTER_PREFERENCES, Job, Printer, PrinterOption, Scaling, lp_args, lpstat_command, parse_lpoptions, parse_lpstat, - parse_lpstat_e, parse_ppd, parse_windows_printers, ppd_text, printers_parsed, + AUTO_SELECT, Duplex, HANDLED_OPTIONS, HAS_PRINTER_PREFERENCES, Job, Printer, PrinterOption, Scaling, apply_queue_defaults, lp_args, + lpstat_command, parse_lpoptions, parse_lpstat, parse_lpstat_e, parse_ppd, parse_windows_printers, ppd_text, printers_parsed, }; #[cfg(any(windows, test))] @@ -243,13 +243,10 @@ pub(crate) fn printer_options_via_within( if let Some(b) = &ppd { localize(&mut options, b, lang); } - for (key, value) in current { - if let Some(o) = options.iter_mut().find(|o| o.key == key) - && o.choices.iter().any(|(c, _)| *c == value) - { - o.default = value; - } - } + // The queue's current choices become the defaults (names matched as CUPS matches them). On + // macOS the paper tray also starts on Auto Select, which is what a job naming no tray asks + // for, so the printer isn't held on an empty default tray (#3752, storytold/craft-libs#232). + apply_queue_defaults(&mut options, ¤t); options } diff --git a/crates/print/src/tests.rs b/crates/print/src/tests.rs index 9bcb7e1ab4..98a37a98cc 100644 --- a/crates/print/src/tests.rs +++ b/crates/print/src/tests.rs @@ -3,6 +3,12 @@ use std::sync::Arc; use super::*; use crate::spool::{Duplex, Job, Printer, lp_args, parse_lpstat, parse_windows_printers}; +/// What `lp_args` adds on macOS for a job that names no paper tray: Auto Select, as the system +/// Print dialog asks (#3752, storytold/craft-libs#232). Nothing elsewhere. +fn macos_auto_tray() -> &'static str { + if cfg!(target_os = "macos") { " -o AP_D_InputSlot=" } else { "" } +} + /// `n` pages of 200×300 (page 3 is landscape 300×200 when n ≥ 3); page i shows "(Page i+1)". /// Page 1 carries a printable square comment, a non-printing note, and a stamp. fn fixture(n: usize) -> Document { @@ -423,7 +429,10 @@ fn spooler_arguments_and_printer_list() { }; assert_eq!( lp_args(&job).join(" "), - "-d Office_Laser -n 3 -t memo.pdf -o collate=false -o sides=two-sided-long-edge -o print-color-mode=monochrome -o fit-to-page=false" + format!( + "-d Office_Laser -n 3 -t memo.pdf -o collate=false -o sides=two-sided-long-edge -o print-color-mode=monochrome -o fit-to-page=false{}", + macos_auto_tray() + ) ); assert_eq!(lp_args(&Job::default())[0], "-n", "no -d: the default printer"); } @@ -766,7 +775,7 @@ fn landscape_sheets_ask_cups_to_turn_the_page() { assert_eq!(cups_orientation(&[]), None); // `lp` is given it, among the job's other options. let job = Job { options: ask.into_iter().collect(), ..Job::default() }; - assert!(lp_args(&job).join(" ").ends_with("-o fit-to-page=false -o orientation-requested=4")); + assert!(lp_args(&job).join(" ").ends_with(&format!("-o fit-to-page=false -o orientation-requested=4{}", macos_auto_tray()))); } /// Both submission paths add the orientation through `add_cups_orientation`: after the job's @@ -796,6 +805,13 @@ fn a_job_that_names_its_orientation_keeps_it() { mod cups_options { use crate::spool; + /// The paper tray a CUPS option list shows as its default: on macOS, a queue left on the driver's + /// own default tray (`driver_default`) shows Auto Select, which is what a job naming no tray asks + /// for (#3752, storytold/craft-libs#232); elsewhere, and for any other tray, `expected` as it is. + fn macos_tray<'a>(expected: &'a str, driver_default: &str) -> &'a str { + if cfg!(target_os = "macos") && expected == driver_default { spool::AUTO_SELECT } else { expected } + } + const RECORD: &str = "PDFCRAFT_LPOPTIONS_TEST_RECORD"; const ANSWER: &str = "PDFCRAFT_LPOPTIONS_TEST_ANSWER"; /// Generous: these tests are about what is read, not how long it takes. @@ -832,7 +848,8 @@ mod cups_options { let options = spool::printer_options_via_within("Office", &dir, command(&dir, "HANG"), std::time::Duration::from_millis(500), "en"); assert!(started.elapsed() < std::time::Duration::from_secs(20), "given up at the deadline, not when the command does"); assert_eq!(options.len(), 1, "the PPD is still read"); - assert_eq!(options[0].default, "Auto", "the PPD default stands when the queue does not answer"); + // On macOS the driver's default tray is shown as Auto Select, what the job then sends (#3752). + assert_eq!(options[0].default, macos_tray("Auto", "Auto"), "the PPD default stands when the queue does not answer"); std::fs::remove_dir_all(dir).unwrap(); } @@ -886,8 +903,13 @@ mod cups_options { let options = spool::printer_options_via_within(destination, &dir, command(&dir, &format!("InputSlot/Paper tray: *{current}")), LIMIT, "en"); assert_eq!(options.len(), 1, "{destination}: the queue's PPD must be read"); - assert_eq!(options[0].default, expected, "{destination}: only valid instance choices override the PPD default"); - assert_eq!(options[0].choices, [("Auto".into(), "Automatic".into()), ("Tray2".into(), "Tray 2".into())]); + assert_eq!(options[0].default, macos_tray(expected, "Auto"), "{destination}: only valid instance choices override the PPD default"); + // On macOS, a queue left on the driver's default tray also offers Auto Select first (#3752). + let mut choices: Vec<(String, String)> = vec![("Auto".into(), "Automatic".into()), ("Tray2".into(), "Tray 2".into())]; + if macos_tray(expected, "Auto") != expected { + choices.insert(0, (spool::AUTO_SELECT.into(), "Auto Select".into())); + } + assert_eq!(options[0].choices, choices); assert_eq!(std::fs::read_to_string(dir.join("args")).unwrap(), format!("-p\n{destination}\n-l")); } let job = spool::Job { printer: Some("Office/duplex".into()), ..Default::default() };