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.
87 lines
2.9 KiB
Rust
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(())
|
|
}
|