From 7ec232b86d8e1dd3e9bb993314c1a08b220d84e2 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 29 Jul 2026 21:54:46 +0800 Subject: [PATCH] bread-capture: one command for every app, flags for a single one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plain `bread-capture` with no flags now captures every known app's every view in one run — each binary resolved by its own bare name via $PATH, same as invoking it directly by name would (so an installed bread ecosystem needs nothing but `bread-capture` to regenerate every screenshot). Previously --app-path was required, so there was no way to run more than one app per invocation. --app restricts to a single app (resolved via $PATH, no path needed); --app-path still works alone too, inferring which app by its file stem exactly as before. --view further restricts to one view — apps without a matching view are silently skipped rather than treated as an error, since view names naturally don't overlap across apps in a multi-app run, but an unmatched --view in a single-app run (or one that matches nothing across every selected app) is still a real error. --- bread-capture/src/main.rs | 127 ++++++++++++++++++++++++++------------ 1 file changed, 88 insertions(+), 39 deletions(-) diff --git a/bread-capture/src/main.rs b/bread-capture/src/main.rs index 2f26756..5af8be9 100644 --- a/bread-capture/src/main.rs +++ b/bread-capture/src/main.rs @@ -2,10 +2,14 @@ //! //! Drives each target app's `--screenshot --output ` mode (see //! `bread-screenshots` for what that mode does inside the app) and reports -//! pass/fail per view. One app per invocation, selected by `--app-name` -//! (defaults to `--app-path`'s file stem, so `--app-path -//! ./target/release/breadbox` needs no separate `--app-name`) — the view -//! list for each app is looked up from [`TARGETS`] below. Flat output +//! pass/fail per view/app. Plain `bread-capture` with no flags captures +//! every known app's every view in one run — each app's binary is resolved +//! by its own bare name via `$PATH`, same as running it directly by name +//! would. `--app ` restricts to one app; `--app-path ` +//! overrides where its binary is found (and, without `--app`, also selects +//! which app by its file stem — so `--app-path ./target/release/breadbox` +//! alone still works); `--view ` further restricts to one view. The +//! view list for each app is looked up from [`TARGETS`] below. Flat output //! directory for now — no versioned `screenshots/vX.Y.Z/latest` structure //! or manifest file yet, since that's still not earning its complexity over //! a handful of apps. @@ -115,14 +119,26 @@ const TARGETS: &[(&str, &[(&str, &str)])] = &[ #[derive(Parser)] struct Cli { - /// Path to the target app's binary (resolved via $PATH if not a path). + /// Restrict to one app (see `TARGETS` for known names). Omit to capture + /// every known app's every view in one run. #[arg(long)] - app_path: String, + app: Option, - /// Which app's view list to use (see `TARGETS`). Defaults to - /// `--app-path`'s file stem, e.g. `./target/release/breadbox` -> `breadbox`. + /// Path to that app's binary (resolved via $PATH if not a path). + /// Without `--app`, this also selects *which* app by its file stem + /// (e.g. `./target/release/breadbox` -> `breadbox`) — so a single-app + /// run never needs both flags. Ignored (with a warning) if given + /// together with a multi-app run (no `--app`, and the path isn't + /// resolvable to exactly one app). #[arg(long)] - app_name: Option, + app_path: Option, + + /// Restrict to one view within the selected app(s) (see each app's + /// entry in `TARGETS` for known view names). Apps that don't have a + /// view by this name are skipped, not treated as an error, since a + /// multi-app run's view names naturally don't all overlap. + #[arg(long)] + view: Option, /// Directory to write captured PNGs into. #[arg(long, default_value = "./screenshots")] @@ -143,21 +159,49 @@ struct Cli { isolate_height: u32, } -fn main() -> Result { - let cli = Cli::parse(); +fn known_app_names() -> String { + TARGETS.iter().map(|(n, _)| *n).collect::>().join(", ") +} - let app_name = cli.app_name.clone().unwrap_or_else(|| { - PathBuf::from(&cli.app_path) +/// (app_name, binary_path, views) per selected app. +type SelectedTarget = (&'static str, String, &'static [(&'static str, &'static str)]); + +/// Resolves which `TARGETS` entries this run covers, and the binary path +/// to use for each. +fn selected_targets(cli: &Cli) -> Result> { + if let Some(app) = &cli.app { + let Some((name, views)) = TARGETS.iter().find(|(n, _)| n == app) else { + bail!("no known view list for app '{app}' (known: {})", known_app_names()); + }; + let path = cli.app_path.clone().unwrap_or_else(|| name.to_string()); + return Ok(vec![(name, path, views)]); + } + + if let Some(path) = &cli.app_path { + let stem = PathBuf::from(path) .file_stem() .map(|s| s.to_string_lossy().into_owned()) - .unwrap_or_else(|| cli.app_path.clone()) - }); - let Some((_, views)) = TARGETS.iter().find(|(name, _)| *name == app_name) else { - bail!( - "no known view list for app '{app_name}' (known: {})", - TARGETS.iter().map(|(n, _)| *n).collect::>().join(", ") - ); - }; + .unwrap_or_else(|| path.clone()); + let Some((name, views)) = TARGETS.iter().find(|(n, _)| *n == stem) else { + bail!("no known view list for app '{stem}' (known: {})", known_app_names()); + }; + return Ok(vec![(name, path.clone(), views)]); + } + + // No --app / --app-path at all: every known app, resolved by its own + // bare name via $PATH. + Ok(TARGETS.iter().map(|(name, views)| (*name, name.to_string(), *views)).collect()) +} + +fn main() -> Result { + let cli = Cli::parse(); + let targets = selected_targets(&cli)?; + + if let Some(view) = &cli.view { + if !targets.iter().any(|(_, _, views)| views.iter().any(|(v, _)| v == view)) { + bail!("view '{view}' doesn't match any selected app's views"); + } + } // Bound, not dropped-and-discarded: `_isolation`'s teardown (kill the // compositor, remove its socket/config) must run via Drop regardless of @@ -174,24 +218,29 @@ fn main() -> Result { let height_str = cli.isolate_height.to_string(); let mut failed = false; - for (view, filename) in *views { - let out_path = cli.out_dir.join(filename); - let out_str = out_path.to_string_lossy(); - let result = bread_utils::proc::run( - &cli.app_path, - &[ - "--screenshot", view, - "--output", &out_str, - "--width", &width_str, - "--height", &height_str, - ], - CAPTURE_TIMEOUT, - ); - if result.success { - println!("ok {app_name}/{view} -> {}", out_path.display()); - } else { - failed = true; - println!("FAIL {app_name}/{view}: {}", result.stderr.trim()); + for (app_name, app_path, views) in &targets { + for (view, filename) in *views { + if cli.view.as_deref().is_some_and(|v| v != *view) { + continue; + } + let out_path = cli.out_dir.join(filename); + let out_str = out_path.to_string_lossy(); + let result = bread_utils::proc::run( + app_path, + &[ + "--screenshot", view, + "--output", &out_str, + "--width", &width_str, + "--height", &height_str, + ], + CAPTURE_TIMEOUT, + ); + if result.success { + println!("ok {app_name}/{view} -> {}", out_path.display()); + } else { + failed = true; + println!("FAIL {app_name}/{view}: {}", result.stderr.trim()); + } } }