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:
parent
686af0d3dc
commit
d059d99437
4 changed files with 124 additions and 341 deletions
|
|
@ -1,13 +1,11 @@
|
|||
//! Capture primitives for the bread ecosystem's UI screenshot tooling (see
|
||||
//! Capture primitive for the bread ecosystem's UI screenshot tooling (see
|
||||
//! `bread-capture`, the orchestrator that drives this crate's consumers).
|
||||
//!
|
||||
//! A "view" being screenshotted is either:
|
||||
//! - its own layer-shell surface (a bar, launcher, ...) — captured tightly via
|
||||
//! [`capture_layer`], geometry from `hyprctl layers`.
|
||||
//! - a transient popover/popup — not a separate layer surface under
|
||||
//! gtk4-layer-shell (it's an xdg_popup Hyprland doesn't list individually),
|
||||
//! so the only reliable capture is [`capture_output`]: whatever's currently
|
||||
//! on the focused monitor.
|
||||
//! 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;
|
||||
|
|
@ -15,36 +13,18 @@ use std::time::Duration;
|
|||
|
||||
const GRIM_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
/// Capture a named layer-shell surface belonging to *this* process
|
||||
/// (`std::process::id()`), identified by its `namespace` (e.g. `"breadbar"`).
|
||||
/// Namespace alone can't identify "our own" surface when another instance
|
||||
/// under the same namespace is already running (breadbar commonly is), so
|
||||
/// this matches by pid too — see `bread_utils::hypr::find_layer`.
|
||||
pub fn capture_layer(namespace: &str, out: &Path) -> Result<()> {
|
||||
let layer = bread_utils::hypr::find_layer(namespace, std::process::id())
|
||||
.with_context(|| format!("no layer surface found for namespace={namespace}"))?;
|
||||
let geometry = format!("{},{} {}x{}", layer.x, layer.y, layer.w, layer.h);
|
||||
run_grim(&geometry, out)
|
||||
}
|
||||
|
||||
/// Capture the entire focused output (monitor). Used for views whose
|
||||
/// interesting content isn't its own layer surface — see the module doc.
|
||||
pub fn capture_output(out: &Path) -> Result<()> {
|
||||
let monitor = bread_utils::hypr::focused_monitor().context("no focused monitor found")?;
|
||||
let (w, h) = monitor.logical_size();
|
||||
let geometry = format!("{},{} {}x{}", monitor.x, monitor.y, w, h);
|
||||
run_grim(&geometry, out)
|
||||
}
|
||||
|
||||
fn run_grim(geometry: &str, out: &Path) -> Result<()> {
|
||||
/// 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 result = bread_utils::proc::run("grim", &["-g", geometry, out_str], GRIM_TIMEOUT);
|
||||
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);
|
||||
bail!("grim failed for geometry {geometry}: {}", result.stderr.trim());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue