diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs index 4400c2d9c..5517e9ecc 100644 --- a/apps/vectorcraft/src/main.rs +++ b/apps/vectorcraft/src/main.rs @@ -1,7 +1,7 @@ //! VectorCraft desktop app. //! //! Usage: `vectorcraft [--control [--automation-read-root ] [--automation-write-root ]] -//! [--in-window-menus] [files…]` +//! [--in-window-menus] [files…]`, `vectorcraft --version`, `vectorcraft --help` (or `-h`) //! //! `--in-window-menus` (or `VECTORCRAFT_IN_WINDOW_MENUS=1`) keeps the menus inside the window on //! macOS instead of the macOS menu bar (`mac_menu`). @@ -625,6 +625,26 @@ fn native_options(custom_titlebar: bool) -> eframe::NativeOptions { } } +/// `vectorcraft --help`. Headless work (convert, run, MCP) is `vectorcraft-cli`'s. +const USAGE: &str = "\ +Usage: vectorcraft [options] [files…] + +Opens the VectorCraft desktop app with the given files. + +Options: + --control Start the localhost JSON-lines control server on + (or VECTORCRAFT_CONTROL_PORT) + --automation-read-root Confine the files control requests read to + --automation-write-root Confine the files control requests write to + --in-window-menus Keep the menus inside the window on macOS + (or VECTORCRAFT_IN_WINDOW_MENUS=1) + --version Print the version and exit + -h, --help Print this help and exit + +Without a window (converting, scripting, the MCP server), use vectorcraft-cli: + vectorcraft-cli --help +"; + fn main() -> std::process::ExitCode { // Validate roots before logging, font scans, preferences or the window: a missing value // must not clear an environment root and start the control channel without confinement. @@ -650,6 +670,14 @@ fn main() -> std::process::ExitCode { println!("vectorcraft {}", env!("CARGO_PKG_VERSION")); return std::process::ExitCode::SUCCESS; } + // Before anything starts, like `--version`: from a terminal or an agent's shell the app + // used to open (or hang without a display) instead (#2110). + "--help" | "-h" => { + // Not `print!`, which panics when standard output is closed (`| head -0`): + // there is nothing left to tell then. + let _ = std::io::Write::write_all(&mut std::io::stdout(), USAGE.as_bytes()); + return std::process::ExitCode::SUCCESS; + } _ => files.push(a), } } diff --git a/apps/vectorcraft/tests/help.rs b/apps/vectorcraft/tests/help.rs new file mode 100644 index 000000000..5c77ecdbb --- /dev/null +++ b/apps/vectorcraft/tests/help.rs @@ -0,0 +1,63 @@ +// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness). +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +//! `vectorcraft --help` prints the usage and exits before the app starts (#2110): run from a +//! terminal, SSH or an agent's shell it used to open the window (or hang without a display). + +use std::process::{Command, Output, Stdio}; +use std::time::{Duration, Instant}; + +const BIN: &str = env!("CARGO_BIN_EXE_vectorcraft"); + +/// Runs the desktop binary without a display, so a regression fails instead of opening a window. +fn run(args: &[&str], env: &[(&str, &str)]) -> Output { + let mut child = Command::new(BIN) + .args(args) + .env_remove("DISPLAY") + .env_remove("WAYLAND_DISPLAY") + .env_remove("WAYLAND_SOCKET") + .env_remove("VECTORCRAFT_CONTROL_PORT") + .env("VECTORCRAFT_NO_PREFS", "1") + .envs(env.iter().copied()) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let deadline = Instant::now() + Duration::from_secs(5); + while child.try_wait().unwrap().is_none() { + if Instant::now() >= deadline { + child.kill().unwrap(); + panic!("{args:?}: the app started instead of printing its usage"); + } + std::thread::sleep(Duration::from_millis(10)); + } + child.wait_with_output().unwrap() +} + +fn prints_usage(args: &[&str], env: &[(&str, &str)]) { + let out = run(args, env); + let stdout = String::from_utf8(out.stdout).unwrap(); + assert!(out.status.success(), "{args:?}: {}", String::from_utf8_lossy(&out.stderr)); + assert!(out.stderr.is_empty(), "{args:?}: {}", String::from_utf8_lossy(&out.stderr)); + assert!(stdout.starts_with("Usage: vectorcraft "), "{args:?}: {stdout}"); + for flag in ["--control", "--automation-read-root", "--automation-write-root", "--in-window-menus", "--version", "--help"] { + assert!(stdout.contains(flag), "{args:?}: usage doesn't mention {flag}: {stdout}"); + } + // Headless work is the CLI's: the usage says where to go. + assert!(stdout.contains("vectorcraft-cli"), "{args:?}: {stdout}"); +} + +#[test] +fn help_prints_usage_and_exits_before_startup() { + prints_usage(&["--help"], &[]); + prints_usage(&["-h"], &[]); + // After a file or a flag, too: nothing is opened. + prints_usage(&["drawing.svg", "--help"], &[]); + prints_usage(&["--in-window-menus", "-h"], &[]); +} + +#[test] +fn help_wins_over_a_bad_control_port_variable() { + prints_usage(&["--help"], &[("VECTORCRAFT_CONTROL_PORT", "abc")]); +}