bread-ecosystem/bread-capture/src/main.rs
Breadway bcd57b7b54 bread-capture: isolate captures in a throwaway nested Hyprland instance
Captures now run inside a dedicated nested Hyprland session by default
(--no-isolate to opt out), so nothing on the operator's live desktop can
leak into a screenshot and the capture never flashes across their screen
either. The nested instance nests as a Wayland client of the outer session
(true headless was ruled out empirically: this machine's real GPU/output is
already claimed by the live session, and only one process can hold logind's
seat at a time), gets floated/exact-resized/focused via one-shot outer-session
hyprctl dispatches targeted by pid, and has Hyprland's default background/
logo and startup warning overlays disabled via config so captures come out
clean. Focusing turned out to be load-bearing, not cosmetic: an occluded
nested window never gets frame callbacks from the outer compositor, so grim
run inside it hangs forever waiting on a ready event that never comes.
2026-07-23 16:22:24 +08:00

87 lines
2.9 KiB
Rust

//! Orchestrator for the bread ecosystem's UI screenshot tooling.
//!
//! Drives each target app's `--screenshot <view> --output <path>` mode (see
//! `bread-screenshots` for what that mode does inside the app) and reports
//! pass/fail per view. Foundation-phase scope: one target (breadbar), a
//! hardcoded view list, and a flat output directory — no versioned
//! `screenshots/vX.Y.Z/latest` structure or manifest file yet, since those
//! only earn their complexity once more apps are wired up.
//!
//! By default every capture runs inside a throwaway nested Hyprland instance
//! (see [`isolation`]) rather than the operator's live desktop, so another
//! window (or their own differently-themed real bar) can't leak into a
//! capture. `--no-isolate` skips that and captures directly against whatever
//! session bread-capture itself is running in — useful for debugging the
//! capture sequence itself, since you can then actually watch it happen.
mod isolation;
use anyhow::Result;
use clap::Parser;
use std::path::PathBuf;
use std::time::Duration;
const CAPTURE_TIMEOUT: Duration = Duration::from_secs(10);
/// (view name, output filename)
const BREADBAR_TARGETS: &[(&str, &str)] = &[
("bar", "breadbar-bar.png"),
("control-panel", "breadbar-control-panel.png"),
];
#[derive(Parser)]
struct Cli {
/// Path to the breadbar binary (resolved via $PATH if not a path).
#[arg(long, default_value = "breadbar")]
app_path: String,
/// Directory to write captured PNGs into.
#[arg(long, default_value = "./screenshots")]
out_dir: PathBuf,
/// Capture directly against the current session instead of a nested,
/// throwaway Hyprland instance. Off by default so captures can't pick up
/// whatever else is on the operator's desktop.
#[arg(long)]
no_isolate: bool,
/// Width of the isolated session's capture canvas.
#[arg(long, default_value_t = 1920)]
isolate_width: u32,
/// Height of the isolated session's capture canvas.
#[arg(long, default_value_t = 1080)]
isolate_height: u32,
}
fn main() -> Result<()> {
let cli = Cli::parse();
let _isolation = if cli.no_isolate {
None
} else {
Some(isolation::Isolation::start(cli.isolate_width, cli.isolate_height)?)
};
let mut failed = false;
for (view, filename) in BREADBAR_TARGETS {
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],
CAPTURE_TIMEOUT,
);
if result.success {
println!("ok breadbar/{view} -> {}", out_path.display());
} else {
failed = true;
println!("FAIL breadbar/{view}: {}", result.stderr.trim());
}
}
if failed {
std::process::exit(1);
}
Ok(())
}