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.
This commit is contained in:
Breadway 2026-07-29 11:15:41 +08:00
parent 686af0d3dc
commit d059d99437
4 changed files with 124 additions and 341 deletions

View file

@ -7,7 +7,7 @@
//! `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
//! By default every capture runs inside a throwaway headless Sway 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
@ -39,8 +39,8 @@ struct Cli {
#[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
/// Capture directly against the current session instead of a headless,
/// throwaway Sway instance. Off by default so captures can't pick up
/// whatever else is on the operator's desktop.
#[arg(long)]
no_isolate: bool,
@ -63,13 +63,21 @@ fn main() -> Result<()> {
Some(isolation::Isolation::start(cli.isolate_width, cli.isolate_height)?)
};
let width_str = cli.isolate_width.to_string();
let height_str = cli.isolate_height.to_string();
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],
&[
"--screenshot", view,
"--output", &out_str,
"--width", &width_str,
"--height", &height_str,
],
CAPTURE_TIMEOUT,
);
if result.success {