bread-ecosystem/bread-screenshots/src/lib.rs
Breadway d059d99437 bread-capture: switch capture isolation to headless Sway
Nested Hyprland worked but had real limits: the outer compositor decided
the nested window's pixel size (needing an outer-session float+resize
dispatch per capture), occluded surfaces got no frame callbacks (so grim
hung unless the nested window was also focused/raised), and there was no
way to fully suppress a brief real, visible flash of that window on the
operator's desktop.

wlroots' WLR_BACKENDS=headless (Sway, not Hyprland, is built on wlroots
directly) has a genuine headless backend: no seat/DRM-master claim, no
window anywhere, ever. Confirmed empirically: zero visible footprint,
both zwlr_layer_shell_v1 and zwlr_screencopy_manager_v1 present, grim
completes instantly with no focus dance needed.

This drops the Hyprland-specific plumbing that no longer applies:
- bread-screenshots now exposes one compositor-agnostic capture_region
  primitive instead of capture_layer/capture_output, since the isolated
  canvas size is always known up front rather than queried via hyprctl.
- bread-utils::hypr loses the Monitor scale/transform/logical_size and
  Layer/find_layer additions that only existed to support that querying.
- bread-capture's isolation module spawns headless Sway instead of a
  nested Hyprland instance, and passes --width/--height through to the
  target app so it knows the canvas size without asking anyone.

Also fixes a socket leak in isolation teardown: killing the compositor
(Hyprland or Sway) doesn't unlink the wayland-N/.lock files it created,
so every capture run was orphaning a socket pair in the runtime dir.
Drop now removes them explicitly.
2026-07-29 11:15:41 +08:00

30 lines
1.3 KiB
Rust

//! Capture primitive for the bread ecosystem's UI screenshot tooling (see
//! `bread-capture`, the orchestrator that drives this crate's consumers).
//!
//! Deliberately compositor-agnostic: no Hyprland IPC, no layer/output
//! lookup. `bread-capture` runs every target app inside an isolated,
//! headless compositor instance of a known, fixed size (see its
//! `isolation` module), so the caller already knows exactly what region to
//! grab — there's nothing to query.
use anyhow::{bail, Context, Result};
use std::path::Path;
use std::time::Duration;
const GRIM_TIMEOUT: Duration = Duration::from_secs(5);
/// Capture a `w`x`h` region at `(x, y)` (compositor-global coordinates) to
/// `out` via `grim -g`.
pub fn capture_region(x: i32, y: i32, w: i32, h: i32, out: &Path) -> Result<()> {
if let Some(parent) = out.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating {}", parent.display()))?;
}
let out_str = out.to_str().context("output path is not valid UTF-8")?;
let geometry = format!("{x},{y} {w}x{h}");
let result = bread_utils::proc::run("grim", &["-g", &geometry, out_str], GRIM_TIMEOUT);
if !result.success {
bail!("grim failed for geometry {geometry}: {}", result.stderr.trim());
}
Ok(())
}