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
|
|
@ -152,81 +152,10 @@ pub struct Monitor {
|
|||
pub y: i32,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
#[serde(default = "one")]
|
||||
pub scale: f64,
|
||||
#[serde(default)]
|
||||
pub transform: i64,
|
||||
#[serde(default)]
|
||||
pub focused: bool,
|
||||
}
|
||||
|
||||
fn one() -> f64 {
|
||||
1.0
|
||||
}
|
||||
|
||||
impl Monitor {
|
||||
/// Logical (scaled, transform-aware) output size, matching what `grim -g`
|
||||
/// expects — `width`/`height` above are physical pixels. A 90/270-degree
|
||||
/// transform swaps the axes before the scale divide, same as breadshot's
|
||||
/// `monitor_geometry`, which this mirrors.
|
||||
pub fn logical_size(&self) -> (i32, i32) {
|
||||
if self.transform % 2 == 0 {
|
||||
(
|
||||
(self.width as f64 / self.scale).round() as i32,
|
||||
(self.height as f64 / self.scale).round() as i32,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
(self.height as f64 / self.scale).round() as i32,
|
||||
(self.width as f64 / self.scale).round() as i32,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One entry from `hyprctl layers -j` — a layer-shell surface (bar, launcher,
|
||||
/// lock screen, ...), keyed by the `namespace` its client registered.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Layer {
|
||||
pub namespace: String,
|
||||
pub pid: u32,
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub w: i32,
|
||||
pub h: i32,
|
||||
}
|
||||
|
||||
/// Find a layer-shell surface by namespace *and* pid. Namespace alone isn't
|
||||
/// enough to identify "our own" surface — several bread apps (breadbar
|
||||
/// notably) are typically already running under the same namespace when a
|
||||
/// second instance starts up for some other purpose (e.g. screenshot mode),
|
||||
/// so callers pass their own `std::process::id()` to disambiguate.
|
||||
pub fn find_layer(namespace: &str, pid: u32) -> Option<Layer> {
|
||||
find_layer_in(&request_json("j/layers")?, namespace, pid)
|
||||
}
|
||||
|
||||
/// Parsing half of [`find_layer`], split out so it's testable without a live
|
||||
/// Hyprland socket.
|
||||
fn find_layer_in(root: &serde_json::Value, namespace: &str, pid: u32) -> Option<Layer> {
|
||||
let monitors = root.as_object()?;
|
||||
for monitor in monitors.values() {
|
||||
let Some(levels) = monitor.get("levels").and_then(|v| v.as_object()) else {
|
||||
continue;
|
||||
};
|
||||
for layers in levels.values() {
|
||||
for layer in layers.as_array().into_iter().flatten() {
|
||||
let Ok(l) = serde_json::from_value::<Layer>(layer.clone()) else {
|
||||
continue;
|
||||
};
|
||||
if l.namespace == namespace && l.pid == pid {
|
||||
return Some(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Query the currently active (focused) window. Returns `None` if the
|
||||
/// window is fullscreen or no window is focused — same "centre the popup
|
||||
/// instead" contract `breadclip`'s original `get_active_window` had.
|
||||
|
|
@ -261,77 +190,6 @@ pub fn active_workspace_name() -> Option<String> {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn logical_size_divides_by_scale() {
|
||||
let m = Monitor {
|
||||
name: "eDP-1".into(),
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 3840,
|
||||
height: 2400,
|
||||
scale: 2.0,
|
||||
transform: 0,
|
||||
focused: true,
|
||||
};
|
||||
assert_eq!(m.logical_size(), (1920, 1200));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logical_size_swaps_axes_on_rotated_transform() {
|
||||
let m = Monitor {
|
||||
name: "eDP-1".into(),
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1920,
|
||||
height: 1200,
|
||||
scale: 1.0,
|
||||
transform: 1,
|
||||
focused: true,
|
||||
};
|
||||
assert_eq!(m.logical_size(), (1200, 1920));
|
||||
}
|
||||
|
||||
// Real shape confirmed live via `hyprctl layers -j`: per-monitor map ->
|
||||
// "levels" map (layer index "0".."3") -> array of layer objects.
|
||||
const LAYERS_JSON: &str = r#"{
|
||||
"eDP-1": {
|
||||
"levels": {
|
||||
"0": [
|
||||
{"address":"0x1","x":0,"y":0,"w":1920,"h":1200,"namespace":"awww-daemon","pid":2481}
|
||||
],
|
||||
"1": [],
|
||||
"2": [
|
||||
{"address":"0x2","x":0,"y":0,"w":1920,"h":32,"namespace":"breadbar","pid":1601316},
|
||||
{"address":"0x3","x":0,"y":0,"w":1920,"h":32,"namespace":"breadbar","pid":9999}
|
||||
],
|
||||
"3": []
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
||||
#[test]
|
||||
fn find_layer_in_matches_namespace_and_pid() {
|
||||
let root: serde_json::Value = serde_json::from_str(LAYERS_JSON).unwrap();
|
||||
let layer = find_layer_in(&root, "breadbar", 9999).unwrap();
|
||||
assert_eq!(layer.pid, 9999);
|
||||
assert_eq!(layer.w, 1920);
|
||||
assert_eq!(layer.h, 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_layer_in_ignores_same_namespace_wrong_pid() {
|
||||
let root: serde_json::Value = serde_json::from_str(LAYERS_JSON).unwrap();
|
||||
// pid 1601316 is a real breadbar layer in the fixture, but not the one
|
||||
// we're asking for — must not fall back to it.
|
||||
assert!(find_layer_in(&root, "breadbar", 42).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_layer_in_returns_none_for_missing_namespace() {
|
||||
let root: serde_json::Value = serde_json::from_str(LAYERS_JSON).unwrap();
|
||||
assert!(find_layer_in(&root, "breadbox", 1601316).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fullscreen_state_deserializes_from_bool() {
|
||||
let s: FullscreenState = serde_json::from_str("true").unwrap();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue