Add isolated window screenshot targets with native selection

This commit is contained in:
Samuel Onoja committed 2026-09-29 23:54:32 +01:00
1 parent c8ed8e6c30
commit 0ab5f2e05b
12 files changed
+681 -95

No files matched your search

+122 -1
View File
@@ -125,7 +125,9 @@ pub(crate) fn init(
return;
}
};
if call.request.command == "screenshot" {
if call.request.command == "screenshot-window" {
state.start_window_screenshot(call.request, call.response);
} else if call.request.command == "screenshot" {
// Deferred: this path answers the caller itself, exactly
// once, whenever the request finishes or is terminated.
state.start_screenshot(call.request, call.response);
@@ -309,6 +311,8 @@ fn serve_connection(
};
let (response, receiver) = sync_channel(1);
let exit_requested = request.command == "exit";
let screenshot_requested =
matches!(request.command.as_str(), "screenshot" | "screenshot-window");
let call = IpcEvent::Call(IpcCall {
owner,
request,
@@ -323,6 +327,9 @@ fn serve_connection(
};
let exit_accepted = exit_requested && response.error.is_none();
if let Err(error) = write_frame(&mut stream, &response) {
if screenshot_requested {
discard_undelivered_screenshot(&response);
}
tracing::debug!(%error, "IPC client disconnected before receiving its response");
return;
}
@@ -335,6 +342,19 @@ fn serve_connection(
}
}
fn discard_undelivered_screenshot(response: &Response) {
if response.error.is_none()
&& let Some(path) = response
.result
.as_ref()
.and_then(|result| result["path"].as_str())
&& let Err(error) = fs::remove_file(path)
&& error.kind() != io::ErrorKind::NotFound
{
tracing::debug!(%error, "failed to remove an undelivered screenshot");
}
}
impl Ferese {
fn handle_ipc_request(&mut self, owner: u64, request: Request) -> Response {
if self.session_lock.active && request.command != "portal-shortcuts-poll" {
@@ -520,6 +540,93 @@ impl Ferese {
crate::backends::direct::render_all(self);
}
fn start_window_screenshot(&mut self, request: Request, response: SyncSender<Response>) {
let reject = |message: String| {
let _ = response.try_send(Response::error(
request.id,
"window_capture_failed",
message,
));
};
if self.session_lock.active || !screencopy::capture_allowed() {
reject("Screen capture is unavailable".into());
return;
}
if let Err(error) = validate_request(&request) {
reject(error.message);
return;
}
let Some(id) = request
.args
.get("window")
.and_then(Value::as_u64)
.map(ferese_layout::WindowId)
else {
reject("Expected a window ID".into());
return;
};
let Some(window) = self
.window_ids
.iter()
.find(|(_, candidate)| **candidate == id)
.map(|(window, _)| window.clone())
else {
reject("Window no longer exists".into());
return;
};
let Some(output) = self
.space
.outputs()
.find(|output| self.window_belongs_to_output(id, output))
.cloned()
else {
reject("Window output is unavailable".into());
return;
};
let geometry = window.geometry();
let scale = output.current_scale().fractional_scale();
let size = geometry.size.to_physical_precise_round(scale);
let spec = crate::handlers::screenshot::PartSpec {
preserve_alpha: true,
transform: Transform::Normal,
scale,
location: (0, 0),
logical_width: geometry.size.w,
logical_height: geometry.size.h,
buffer_width: size.w,
buffer_height: size.h,
};
let fallback = response.clone();
let capture = match self.screenshot.admit(request.id, response, vec![spec]) {
Ok(capture) => capture,
Err(error) => {
let _ =
fallback.try_send(Response::error(request.id, "screenshot_rejected", error));
return;
}
};
let result = if let Some(backend) = &self.nested_backend {
match backend.try_borrow_mut() {
Ok(mut backend) => crate::winit::capture_window_buffer(
backend.renderer(),
&window,
geometry,
scale,
),
Err(_) => Err("Window renderer is busy".into()),
}
} else if let Some(backend) = &mut self.direct_backend {
backend.capture_window_buffer(&window, geometry, &output)
} else {
Err("Window renderer is unavailable".into())
};
self.on_screenshot_part(PartOutcome {
request: capture,
part: 0,
result,
});
}
pub(crate) fn on_screenshot_part(&mut self, outcome: PartOutcome) {
let action = self
.screenshot
@@ -1030,6 +1137,20 @@ mod tests {
worker.join().unwrap();
}
#[test]
fn failed_screenshot_delivery_removes_only_its_staging_file() {
let root = unique_test_directory("undelivered-screenshot");
fs::create_dir(&root).unwrap();
let path = root.join("capture.png");
fs::write(&path, b"capture").unwrap();
let response = Response::success(1, serde_json::json!({"path": path}));
discard_undelivered_screenshot(&response);
assert!(!path.exists());
discard_undelivered_screenshot(&response);
fs::remove_dir(root).unwrap();
}
#[test]
fn runtime_socket_directory_is_private() {
let root = unique_test_directory("private-parent");