fix(app): pdfcraft --help prints usage instead of opening the app (#4076) (#4093)

The desktop app's argument loop took `--help` as a view setting named
`help` (consuming the next argument as its value) and `-h` as a file to
open, so both started the window and a scripted call never returned.

`-h` and `--help` now print a short usage, which points scripts to
pdfcraft-cli, and exit 0 before any window or log file is created.
`--version` and `--help` write through a plain `writeln!` instead of
`println!`, which panicked (exit 101) when standard output was full or
closed, e.g. `pdfcraft --version > /dev/full`.

Release builds on Windows are GUI-subsystem programs, so, as with
`--version` already, the text shows when the output is redirected or
piped but not in a plain console window.

Fixes #4076
This commit is contained in:
Ridwan
2026-10-23 10:43:14 -04:00
committed by GitHub
parent 70704ff260
commit 857c491360
2 changed files with 85 additions and 5 deletions
+30 -5
View File
@@ -1,6 +1,6 @@
//! PdfCraft desktop app. //! PdfCraft desktop app.
//! //!
//! Usage: `pdfcraft [options] [files…]` //! Usage: `pdfcraft [options] [files…]` (`-h` / `--help` prints a short version of this, [`USAGE`]).
//! `--create-images [images…]` stages the images in one PDF and asks for the page DPI. //! `--create-images [images…]` stages the images in one PDF and asks for the page DPI.
//! //!
//! View options (applied after the files open; also the seed of the UI control channel): //! View options (applied after the files open; also the seed of the UI control channel):
@@ -56,6 +56,32 @@ const APP_ICON_PNG: &[u8] = include_bytes!("../../../assets/app-icon/pdfcraft-10
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
const APP_ICON_PNG: &[u8] = include_bytes!("../../../assets/app-icon/hicolor/256x256/apps/ai.storyteller.pdfcraft.png"); const APP_ICON_PNG: &[u8] = include_bytes!("../../../assets/app-icon/hicolor/256x256/apps/ai.storyteller.pdfcraft.png");
/// What `-h` / `--help` prints (#4076): a hint, not the full option list (that is this file's
/// doc comment and the README). Scripts want `pdfcraft-cli`, which never opens a window.
const USAGE: &str = "\
Usage: pdfcraft [options] [files…]
Opens the files in the PdfCraft desktop app.
Options:
--new-window open a window of its own instead of passing the files to a running PdfCraft
--create-images [images…] put the images in one PDF and ask for the page DPI
--control <file> enable the UI control channel (pdfcraft-cli ui --control <file> …)
--page N, --zoom 150, --layout continuous, --theme dark, …
view settings applied once the files are open
-h, --help print this and exit
--version print the version and exit
To inspect, convert or edit PDFs from a script, use pdfcraft-cli (pdfcraft-cli --help).";
/// Print `text` for `--help` and `--version`. `println!` would panic on a full or closed
/// standard output; this exits normally instead.
fn print_and_exit(text: &str) -> eframe::Result {
use std::io::Write;
let _ = writeln!(std::io::stdout().lock(), "{text}");
Ok(())
}
/// The app was called PrintCraft before; settings saved then are under this key. /// The app was called PrintCraft before; settings saved then are under this key.
const LEGACY_STORAGE_KEY: &str = "printcraft"; const LEGACY_STORAGE_KEY: &str = "printcraft";
@@ -196,10 +222,9 @@ fn main() -> eframe::Result {
let mut args = std::env::args().skip(1); let mut args = std::env::args().skip(1);
while let Some(a) = args.next() { while let Some(a) = args.next() {
match a.as_str() { match a.as_str() {
"--version" => { "--version" => return print_and_exit(&format!("pdfcraft {}", pdfcraft_engine::build_info::version())),
println!("pdfcraft {}", pdfcraft_engine::build_info::version()); // Before any window or log file, so a scripted call returns (#4076).
return Ok(()); "-h" | "--help" => return print_and_exit(USAGE),
}
"--control" => control_file = args.next(), "--control" => control_file = args.next(),
"--create-images" => create_images = true, "--create-images" => create_images = true,
"--new-window" => new_window = true, "--new-window" => new_window = true,
+55
View File
@@ -0,0 +1,55 @@
//! `--help`, `-h` and `--version` print and exit without opening a window (#4076), so a script that
//! calls them returns.
use std::process::{Command, Output, Stdio};
use std::time::{Duration, Instant};
const BIN: &str = env!("CARGO_BIN_EXE_pdfcraft");
/// Run the app with `args`, failing if it is still running after 60 s (it opened its window).
fn run(args: &[&str], stdout: Stdio) -> Output {
let mut child = Command::new(BIN).args(args).stdin(Stdio::null()).stdout(stdout).stderr(Stdio::piped()).spawn().unwrap();
let deadline = Instant::now() + Duration::from_secs(60);
while child.try_wait().unwrap().is_none() {
if Instant::now() > deadline {
let _ = child.kill();
panic!("pdfcraft {args:?} did not exit: it started the app instead");
}
std::thread::sleep(Duration::from_millis(50));
}
child.wait_with_output().unwrap()
}
#[test]
fn help_prints_usage_and_exits() {
for flag in ["--help", "-h"] {
let out = run(&[flag], Stdio::piped());
assert!(out.status.success(), "{flag}: {:?} {}", out.status, String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.starts_with("Usage: pdfcraft [options] [files…]"), "{flag}: {stdout}");
assert!(stdout.contains("pdfcraft-cli"), "{flag}: points scripts to the CLI: {stdout}");
}
// Wherever it is on the command line, and with files named.
let out = run(&["missing.pdf", "--help"], Stdio::piped());
assert!(out.status.success());
assert!(String::from_utf8_lossy(&out.stdout).starts_with("Usage:"));
}
#[test]
fn version_prints_the_version_and_exits() {
let out = run(&["--version"], Stdio::piped());
assert!(out.status.success(), "{:?} {}", out.status, String::from_utf8_lossy(&out.stderr));
assert!(String::from_utf8_lossy(&out.stdout).starts_with("pdfcraft "), "{}", String::from_utf8_lossy(&out.stdout));
}
/// A standard output that can't be written (here a full device) ends the run normally: `println!`
/// would panic and exit 101.
#[cfg(target_os = "linux")]
#[test]
fn help_and_version_survive_an_unwritable_stdout() {
for flag in ["--help", "--version"] {
let full = std::fs::OpenOptions::new().write(true).open("/dev/full").unwrap();
let out = run(&[flag], Stdio::from(full));
assert!(out.status.success(), "{flag}: {:?} {}", out.status, String::from_utf8_lossy(&out.stderr));
}
}