From 68a5c6ac1ce6373f43abe9703e8c515e840ab4d7 Mon Sep 17 00:00:00 2001 From: Access Date: Fri, 2 Feb 2024 19:18:37 +0000 Subject: [PATCH 01/26] Refactor: introduce FrameGuard, ScreenCapturer, and Logical/EmbeddedRegion --- libwayshot/src/dispatch.rs | 29 ++-- libwayshot/src/image_util.rs | 8 + libwayshot/src/lib.rs | 325 ++++++++++++++++++----------------- libwayshot/src/region.rs | 219 +++++++++++++++++++++++ libwayshot/src/screencopy.rs | 26 ++- wayshot/src/utils.rs | 16 +- 6 files changed, 435 insertions(+), 188 deletions(-) create mode 100644 libwayshot/src/region.rs diff --git a/libwayshot/src/dispatch.rs b/libwayshot/src/dispatch.rs index 9b760c21..170e1026 100644 --- a/libwayshot/src/dispatch.rs +++ b/libwayshot/src/dispatch.rs @@ -25,11 +25,13 @@ use crate::{ screencopy::FrameFormat, }; +#[derive(Debug)] pub struct OutputCaptureState { pub outputs: Vec, } impl Dispatch for OutputCaptureState { + #[tracing::instrument(skip(wl_registry, qh), ret, level = "trace")] fn event( state: &mut Self, wl_registry: &WlRegistry, @@ -76,6 +78,7 @@ impl Dispatch for OutputCaptureState { } impl Dispatch for OutputCaptureState { + #[tracing::instrument(skip(wl_output), ret, level = "trace")] fn event( state: &mut Self, wl_output: &WlOutput, @@ -114,6 +117,7 @@ impl Dispatch for OutputCaptureState { delegate_noop!(OutputCaptureState: ignore ZxdgOutputManagerV1); impl Dispatch for OutputCaptureState { + #[tracing::instrument(ret, level = "trace")] fn event( state: &mut Self, _: &ZxdgOutputV1, @@ -128,19 +132,20 @@ impl Dispatch for OutputCaptureState { zxdg_output_v1::Event::LogicalPosition { x, y } => { output_info.dimensions.x = x; output_info.dimensions.y = y; - tracing::debug!("Logical position event fired!"); } zxdg_output_v1::Event::LogicalSize { width, height } => { output_info.dimensions.width = width; output_info.dimensions.height = height; - tracing::debug!("Logical size event fired!"); } + zxdg_output_v1::Event::Done => {} + zxdg_output_v1::Event::Name { .. } => {} + zxdg_output_v1::Event::Description { .. } => {} _ => {} }; } } -/// State of the frame after attemting to copy it's data to a wl_buffer. +/// State of the frame after attempting to copy it's data to a wl_buffer. #[derive(Debug, Copy, Clone, PartialEq)] pub enum FrameState { /// Compositor returned a failed event on calling `frame.copy`. @@ -156,6 +161,7 @@ pub struct CaptureFrameState { } impl Dispatch for CaptureFrameState { + #[tracing::instrument(skip(frame), ret, level = "trace")] fn event( frame: &mut Self, _: &ZwlrScreencopyFrameV1, @@ -171,7 +177,6 @@ impl Dispatch for CaptureFrameState { height, stride, } => { - tracing::debug!("Received Buffer event"); if let Value(f) = format { frame.formats.push(FrameFormat { format: f, @@ -184,30 +189,20 @@ impl Dispatch for CaptureFrameState { exit(1); } } - zwlr_screencopy_frame_v1::Event::Flags { .. } => { - tracing::debug!("Received Flags event"); - } zwlr_screencopy_frame_v1::Event::Ready { .. } => { // If the frame is successfully copied, a “flags” and a “ready” events are sent. Otherwise, a “failed” event is sent. // This is useful when we call .copy on the frame object. - tracing::debug!("Received Ready event"); frame.state.replace(FrameState::Finished); } zwlr_screencopy_frame_v1::Event::Failed => { - tracing::debug!("Received Failed event"); frame.state.replace(FrameState::Failed); } - zwlr_screencopy_frame_v1::Event::Damage { .. } => { - tracing::debug!("Received Damage event"); - } - zwlr_screencopy_frame_v1::Event::LinuxDmabuf { .. } => { - tracing::debug!("Received LinuxDmaBuf event"); - } + zwlr_screencopy_frame_v1::Event::Damage { .. } => {} + zwlr_screencopy_frame_v1::Event::LinuxDmabuf { .. } => {} zwlr_screencopy_frame_v1::Event::BufferDone => { - tracing::debug!("Received bufferdone event"); frame.buffer_done.store(true, Ordering::SeqCst); } - _ => unreachable!(), + _ => {} }; } } diff --git a/libwayshot/src/image_util.rs b/libwayshot/src/image_util.rs index 92eb3967..197e8f11 100644 --- a/libwayshot/src/image_util.rs +++ b/libwayshot/src/image_util.rs @@ -7,6 +7,14 @@ pub(crate) fn rotate_image_buffer( width: u32, height: u32, ) -> DynamicImage { + // TODO Better document whether width and height are before or after the transform. + // Perhaps this should be part of a cleanup of the FrameCopy struct. + let (width, height) = match transform { + Transform::_90 | Transform::_270 | Transform::Flipped90 | Transform::Flipped270 => { + (height, width) + } + _ => (width, height), + }; let final_image = match transform { Transform::_90 => image::imageops::rotate90(&image).into(), Transform::_180 => image::imageops::rotate180(&image).into(), diff --git a/libwayshot/src/lib.rs b/libwayshot/src/lib.rs index 6c003320..c93cb335 100644 --- a/libwayshot/src/lib.rs +++ b/libwayshot/src/lib.rs @@ -8,10 +8,10 @@ mod dispatch; mod error; mod image_util; pub mod output; +pub mod region; mod screencopy; use std::{ - cmp, fs::File, os::fd::AsFd, process::exit, @@ -19,12 +19,15 @@ use std::{ thread, }; -use image::{imageops::overlay, DynamicImage}; +use image::{imageops::replace, DynamicImage}; use memmap2::MmapMut; +use region::{EmbeddedRegion, RegionCapturer}; +use screencopy::FrameGuard; +use tracing::debug; use wayland_client::{ globals::{registry_queue_init, GlobalList}, protocol::{ - wl_output::{Transform, WlOutput}, + wl_output::WlOutput, wl_shm::{self, WlShm}, }, Connection, EventQueue, @@ -41,6 +44,7 @@ use crate::{ convert::create_converter, dispatch::{CaptureFrameState, FrameState, OutputCaptureState, WayshotState}, output::OutputInfo, + region::LogicalRegion, screencopy::{create_shm_fd, FrameCopy, FrameFormat}, }; @@ -51,28 +55,6 @@ pub mod reexport { pub use wl_output::{Transform, WlOutput}; } -type Frame = (Vec, (i32, i32)); - -/// Struct to store region capture details. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct CaptureRegion { - /// X coordinate of the area to capture. - pub x_coordinate: i32, - /// y coordinate of the area to capture. - pub y_coordinate: i32, - /// Width of the capture area. - pub width: i32, - /// Height of the capture area. - pub height: i32, -} - -#[derive(Debug)] -struct IntersectingOutput { - output: WlOutput, - region: CaptureRegion, - transform: Transform, -} - /// Struct to store wayland connection and globals list. /// # Example usage /// @@ -161,7 +143,7 @@ impl WayshotConnection { tracing::error!("Compositor did not advertise any wl_output devices!"); exit(1); } - tracing::debug!("Outputs detected: {:#?}", state.outputs); + tracing::trace!("Outputs detected: {:#?}", state.outputs); self.output_infos = state.outputs; Ok(()) @@ -174,18 +156,21 @@ impl WayshotConnection { cursor_overlay: i32, output: &WlOutput, fd: T, - capture_region: Option, - ) -> Result { + capture_region: Option, + ) -> Result<(FrameFormat, FrameGuard)> { let (state, event_queue, frame, frame_format) = self.capture_output_frame_get_state(cursor_overlay, output, capture_region)?; - self.capture_output_frame_inner(state, event_queue, frame, frame_format, fd) + let frame_guard = + self.capture_output_frame_inner(state, event_queue, frame, frame_format, fd)?; + + Ok((frame_format, frame_guard)) } fn capture_output_frame_get_state( &self, cursor_overlay: i32, output: &WlOutput, - capture_region: Option, + capture_region: Option, ) -> Result<( CaptureFrameState, EventQueue, @@ -216,15 +201,15 @@ impl WayshotConnection { } }; - // Capture output. - let frame: ZwlrScreencopyFrameV1 = if let Some(region) = capture_region { + debug!("Capturing output..."); + let frame = if let Some(embedded_region) = capture_region { screencopy_manager.capture_output_region( cursor_overlay, output, - region.x_coordinate, - region.y_coordinate, - region.width, - region.height, + embedded_region.inner.x, + embedded_region.inner.y, + embedded_region.inner.width, + embedded_region.inner.height, &qh, (), ) @@ -238,7 +223,7 @@ impl WayshotConnection { event_queue.blocking_dispatch(&mut state)?; } - tracing::debug!( + tracing::trace!( "Received compositor frame buffer formats: {:#?}", state.formats ); @@ -258,7 +243,7 @@ impl WayshotConnection { ) }) .copied(); - tracing::debug!("Selected frame buffer format: {:#?}", frame_format); + tracing::trace!("Selected frame buffer format: {:#?}", frame_format); // Check if frame format exists. let frame_format = match frame_format { @@ -278,7 +263,7 @@ impl WayshotConnection { frame: ZwlrScreencopyFrameV1, frame_format: FrameFormat, fd: T, - ) -> Result { + ) -> Result { // Connecting to wayland environment. let qh = event_queue.handle(); @@ -310,9 +295,7 @@ impl WayshotConnection { return Err(Error::FramecopyFailed); } FrameState::Finished => { - buffer.destroy(); - shm_pool.destroy(); - return Ok(frame_format); + return Ok(FrameGuard { buffer, shm_pool }); } } } @@ -326,8 +309,8 @@ impl WayshotConnection { cursor_overlay: bool, output: &WlOutput, file: &File, - capture_region: Option, - ) -> Result { + capture_region: Option, + ) -> Result<(FrameFormat, FrameGuard)> { let (state, event_queue, frame, frame_format) = self.capture_output_frame_get_state(cursor_overlay as i32, output, capture_region)?; @@ -335,25 +318,28 @@ impl WayshotConnection { let frame_bytes = frame_format.stride * frame_format.height; file.set_len(frame_bytes as u64)?; - self.capture_output_frame_inner(state, event_queue, frame, frame_format, file) + let frame_guard = + self.capture_output_frame_inner(state, event_queue, frame, frame_format, file)?; + + Ok((frame_format, frame_guard)) } /// Get a FrameCopy instance with screenshot pixel data for any wl_output object. - fn capture_output_frame( + #[tracing::instrument(skip_all, fields(output = output_info.name, region = capture_region.map(|r| format!("{:}", r))))] + fn capture_frame_copy( &self, cursor_overlay: bool, - output: &WlOutput, - transform: Transform, - capture_region: Option, - ) -> Result { + output_info: &OutputInfo, + capture_region: Option, + ) -> Result<(FrameCopy, FrameGuard)> { // Create an in memory file and return it's file descriptor. let fd = create_shm_fd()?; // Create a writeable memory map backed by a mem_file. let mem_file = File::from(fd); - let frame_format = self.capture_output_frame_shm_from_file( + let (frame_format, frame_guard) = self.capture_output_frame_shm_from_file( cursor_overlay, - output, + &output_info.wl_output, &mem_file, capture_region, )?; @@ -367,64 +353,45 @@ impl WayshotConnection { tracing::error!("You can send a feature request for the above format to the mailing list for wayshot over at https://sr.ht/~shinyzenith/wayshot."); return Err(Error::NoSupportedBufferFormat); }; - Ok(FrameCopy { + let frame_copy = FrameCopy { frame_format, frame_color_type, frame_mmap, - transform, - }) + transform: output_info.transform, + position: capture_region + .map(|capture_region| { + let logical_region = capture_region.logical(); + (logical_region.inner.x as i64, logical_region.inner.y as i64) + }) + .unwrap_or_else(|| { + ( + output_info.dimensions.x as i64, + output_info.dimensions.y as i64, + ) + }), + }; + tracing::debug!("Created frame copy: {:#?}", frame_copy); + Ok((frame_copy, frame_guard)) } - fn create_frame_copy( + pub fn capture_frame_copies( &self, - capture_region: CaptureRegion, + output_capture_regions: &Vec<(OutputInfo, Option)>, cursor_overlay: bool, - ) -> Result { + ) -> Result> { let frame_copies = thread::scope(|scope| -> Result<_> { - let join_handles = self - .get_all_outputs() + let join_handles = output_capture_regions .into_iter() - .filter_map(|output| { - let x1: i32 = cmp::max(output.dimensions.x, capture_region.x_coordinate); - let y1: i32 = cmp::max(output.dimensions.y, capture_region.y_coordinate); - let x2: i32 = cmp::min( - output.dimensions.x + output.dimensions.width, - capture_region.x_coordinate + capture_region.width, - ); - let y2: i32 = cmp::min( - output.dimensions.y + output.dimensions.height, - capture_region.y_coordinate + capture_region.height, - ); - - let width = x2 - x1; - let height = y2 - y1; - - if width <= 0 || height <= 0 { - return None; - } - - let true_x = capture_region.x_coordinate - output.dimensions.x; - let true_y = capture_region.y_coordinate - output.dimensions.y; - let true_region = CaptureRegion { - x_coordinate: true_x, - y_coordinate: true_y, - width: capture_region.width, - height: capture_region.height, - }; - Some(IntersectingOutput { - output: output.wl_output.clone(), - region: true_region, - transform: output.transform, - }) - }) - .map(|intersecting_output| { + .map(|(output_info, capture_region)| { scope.spawn(move || { - self.capture_output_frame( + self.capture_frame_copy( cursor_overlay, - &intersecting_output.output, - intersecting_output.transform, - Some(intersecting_output.region), + &output_info, + capture_region.clone(), ) + .map(|(frame_copy, frame_guard)| { + (frame_copy, frame_guard, output_info.clone()) + }) }) }) .collect::>(); @@ -436,30 +403,71 @@ impl WayshotConnection { .collect::>() })?; - Ok((frame_copies, (capture_region.width, capture_region.height))) + Ok(frame_copies) } /// Take a screenshot from the specified region. - pub fn screenshot( + fn screenshot_region_capturer( &self, - capture_region: CaptureRegion, + region_capturer: RegionCapturer, cursor_overlay: bool, ) -> Result { - let (frame_copies, (width, height)) = - self.create_frame_copy(capture_region, cursor_overlay)?; + let outputs_capture_regions: &Vec<(OutputInfo, Option)> = + &match region_capturer { + RegionCapturer::Outputs(ref outputs) => outputs + .into_iter() + .map(|output_info| (output_info.clone(), None)) + .collect(), + RegionCapturer::Region(capture_region) => self + .get_all_outputs() + .into_iter() + .filter_map(|output_info| { + tracing::span!( + tracing::Level::DEBUG, + "filter_map", + output = format!( + "{name} at {region}", + name = output_info.name, + region = LogicalRegion::from(output_info), + ), + capture_region = format!("{}", capture_region), + ) + .in_scope(|| { + if let Some(relative_region) = + EmbeddedRegion::new(capture_region, output_info.into()) + { + tracing::debug!("Intersection found: {}", relative_region); + Some((output_info.clone(), Some(relative_region))) + } else { + tracing::debug!("No intersection found"); + None + } + }) + }) + .collect(), + }; + + let frames = self.capture_frame_copies(outputs_capture_regions, cursor_overlay)?; + + let capture_region: LogicalRegion = match region_capturer { + RegionCapturer::Outputs(ref outputs) => outputs.try_into()?, + RegionCapturer::Region(region) => region, + }; thread::scope(|scope| { - let rotate_join_handles = frame_copies + let rotate_join_handles = frames .into_iter() - .map(|frame_copy| { + .map(|(frame_copy, _, _)| { scope.spawn(move || { - let transform = frame_copy.transform; - let image = frame_copy.try_into()?; - Ok(image_util::rotate_image_buffer( - image, - transform, - width as u32, - height as u32, + let image = (&frame_copy).try_into()?; + Ok(( + image_util::rotate_image_buffer( + image, + frame_copy.transform, + frame_copy.frame_format.width, + frame_copy.frame_format.height, + ), + frame_copy, )) }) }) @@ -471,21 +479,38 @@ impl WayshotConnection { .flatten() .fold( None, - |possible_overlayed_image_or_error: Option>, image: Result<_>| { - if let Some(overlayed_image_or_error) = possible_overlayed_image_or_error { - if let Ok(mut overlayed_image) = overlayed_image_or_error { - if let Ok(image) = image { - overlay(&mut overlayed_image, &image, 0, 0); - Some(Ok(overlayed_image)) - } else { - Some(image) - } - } else { - Some(image) - } - } else { - Some(image) - } + |composite_image: Option>, image: Result<_>| { + // Default to a transparent image. + let composite_image = composite_image.unwrap_or_else(|| { + Ok(DynamicImage::new_rgba8( + capture_region.inner.width as u32, + capture_region.inner.height as u32, + )) + }); + + Some(|| -> Result<_> { + let mut composite_image = composite_image?; + let (image, frame_copy) = image?; + let frame_copy_region = LogicalRegion::from(&frame_copy); + let (x, y) = ( + frame_copy_region.inner.x as i64 - capture_region.inner.x as i64, + frame_copy_region.inner.y as i64 - capture_region.inner.y as i64, + ); + tracing::span!( + tracing::Level::DEBUG, + "replace", + frame_copy_region = format!("{}", frame_copy_region), + capture_region = format!("{}", capture_region), + x = x, + y = y, + ) + .in_scope(|| { + tracing::debug!("Replacing parts of the final image"); + replace(&mut composite_image, &image, x, y); + }); + + Ok(composite_image) + }()) }, ) .ok_or_else(|| { @@ -495,19 +520,23 @@ impl WayshotConnection { }) } + /// Take a screenshot from the specified region. + pub fn screenshot( + &self, + capture_region: LogicalRegion, + cursor_overlay: bool, + ) -> Result { + self.screenshot_region_capturer(RegionCapturer::Region(capture_region), cursor_overlay) + } + /// shot one ouput pub fn screenshot_single_output( &self, output_info: &OutputInfo, cursor_overlay: bool, ) -> Result { - let frame_copy = self.capture_output_frame( - cursor_overlay, - &output_info.wl_output, - output_info.transform, - None, - )?; - frame_copy.try_into() + let (frame_copy, _) = self.capture_frame_copy(cursor_overlay, output_info, None)?; + (&frame_copy).try_into() } /// Take a screenshot from all of the specified outputs. @@ -520,33 +549,7 @@ impl WayshotConnection { return Err(Error::NoOutputs); } - let x1 = outputs - .iter() - .map(|output| output.dimensions.x) - .min() - .unwrap(); - let y1 = outputs - .iter() - .map(|output| output.dimensions.y) - .min() - .unwrap(); - let x2 = outputs - .iter() - .map(|output| output.dimensions.x + output.dimensions.width) - .max() - .unwrap(); - let y2 = outputs - .iter() - .map(|output| output.dimensions.y + output.dimensions.height) - .max() - .unwrap(); - let capture_region = CaptureRegion { - x_coordinate: x1, - y_coordinate: y1, - width: x2 - x1, - height: y2 - y1, - }; - self.screenshot(capture_region, cursor_overlay) + self.screenshot_region_capturer(RegionCapturer::Outputs(outputs.clone()), cursor_overlay) } /// Take a screenshot from all accessible outputs. diff --git a/libwayshot/src/region.rs b/libwayshot/src/region.rs new file mode 100644 index 00000000..a82e0cdc --- /dev/null +++ b/libwayshot/src/region.rs @@ -0,0 +1,219 @@ +use std::cmp; + +use wayland_client::protocol::wl_output::Transform; + +use crate::error::Error; +use crate::output::OutputInfo; +use crate::screencopy::FrameCopy; + +/// Ways to say how a region for a screenshot should be captured. +pub enum RegionCapturer { + /// Capture all of the given outputs. + Outputs(Vec), + /// Capture an already known `LogicalRegion`. + Region(LogicalRegion), +} + +/// `Region` where the coordinate system is the logical coordinate system used +/// in Wayland to position outputs. Top left is (0, 0) and any transforms and +/// scaling have been applied. +#[derive(Debug, Copy, Clone)] +pub struct LogicalRegion { + pub inner: Region, +} + +/// An embedded region is a region entirely inside of another (often an output). +/// +/// It can only be contained inside of another and cannot exceed its bounds. +/// +/// Example of what +/// +/// ┌─────────────┐ +/// │ │ +/// │ ┌──────────┼──────┐ +/// │ │ │ ├──► Viewport +/// │ │ │ │ +/// │ │ ├──────┼─────────────────┐ +/// │ │ │xxxxxx│ │ +/// │ │ │xxxxx◄├─── Inner region │ +/// │ └──────────┼──────┘ │ +/// │ │ │ +/// │ │ Screen 2 ├──► Relative to +/// │ ├────────────────────────┘ +/// │ │ +/// │ Screen 1 │ +/// └─────────────┘ +#[derive(Debug, Copy, Clone)] +pub struct EmbeddedRegion { + /// The coordinate sysd + pub relative_to: LogicalRegion, + pub inner: Region, +} + +/// Rectangle area in an unspecified coordinate system. +/// +/// Use `LogicalRegion` or `EmbeddedRegion` instead as they convey the +/// coordinate system used. +#[derive(Debug, Copy, Clone)] +pub struct Region { + /// X coordinate of the area to capture. + pub x: i32, + /// y coordinate of the area to capture. + pub y: i32, + /// Width of the capture area. + pub width: i32, + /// Height of the capture area. + pub height: i32, +} + +impl EmbeddedRegion { + /// Given two `LogicalRegion`s, one seen as the `viewport` and the other + /// `relative_to` (think the output we want to capture), create an + /// embedded region that is entirely inside of the `relative_to` region. + /// + /// See `EmbeddedRegion` for an example ASCII visualisation. + #[tracing::instrument(ret, level = "debug")] + pub fn new(viewport: LogicalRegion, relative_to: LogicalRegion) -> Option { + let x_relative: i32 = viewport.inner.x - relative_to.inner.x; + let y_relative = viewport.inner.y - relative_to.inner.y; + + let x1 = cmp::max(x_relative, 0); + let x2 = cmp::min(x_relative + viewport.inner.width, relative_to.inner.width); + let width = x2 - x1; + if width <= 0 { + return None; + } + + let y1 = cmp::max(y_relative, 0); + let y2 = cmp::min(y_relative + viewport.inner.height, relative_to.inner.height); + let height = y2 - y1; + if height <= 0 { + return None; + } + + Some(Self { + relative_to: relative_to, + inner: Region { + x: x1, + y: y1, + width, + height, + }, + }) + } + + /// Return the `LogicalRegion` of the embedded region. + /// + /// Note that this remains a region of the same size, it's not the inverse + /// of `EmbeddedRegion::new` which removes the parts that are outside of + /// the `relative_to` region. + pub fn logical(&self) -> LogicalRegion { + LogicalRegion { + inner: Region { + x: self.relative_to.inner.x as i32 + self.inner.x, + y: self.relative_to.inner.y as i32 + self.inner.y, + width: self.inner.width, + height: self.inner.height, + }, + } + } +} + +impl std::fmt::Display for EmbeddedRegion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{region} relative to {relative_to}", + region = self.inner, + relative_to = self.relative_to, + ) + } +} + +impl std::fmt::Display for Region { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "({x}, {y}) ({width}x{height})", + x = self.x, + y = self.y, + width = self.width, + height = self.height, + ) + } +} + +impl std::fmt::Display for LogicalRegion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{inner}", inner = self.inner) + } +} + +impl From<&OutputInfo> for LogicalRegion { + fn from(output_info: &OutputInfo) -> Self { + LogicalRegion { + inner: Region { + x: output_info.dimensions.x, + y: output_info.dimensions.y, + width: output_info.dimensions.width, + height: output_info.dimensions.height, + }, + } + } +} + +impl TryFrom<&Vec> for LogicalRegion { + type Error = Error; + + fn try_from(output_info: &Vec) -> std::result::Result { + let x1 = output_info + .iter() + .map(|output| output.dimensions.x) + .min() + .unwrap(); + let y1 = output_info + .iter() + .map(|output| output.dimensions.y) + .min() + .unwrap(); + let x2 = output_info + .iter() + .map(|output| output.dimensions.x + output.dimensions.width) + .max() + .unwrap(); + let y2 = output_info + .iter() + .map(|output| output.dimensions.y + output.dimensions.height) + .max() + .unwrap(); + Ok(LogicalRegion { + inner: Region { + x: x1, + y: y1, + width: x2 - x1, + height: y2 - y1, + }, + }) + } +} + +impl From<&FrameCopy> for LogicalRegion { + fn from(frame_copy: &FrameCopy) -> Self { + let (width, height) = ( + frame_copy.frame_format.width as i32, + frame_copy.frame_format.height as i32, + ); + let is_portait = match frame_copy.transform { + Transform::_90 | Transform::_270 | Transform::Flipped90 | Transform::Flipped270 => true, + _ => false, + }; + LogicalRegion { + inner: Region { + x: frame_copy.position.0 as i32, + y: frame_copy.position.1 as i32, + width: if is_portait { height } else { width }, + height: if is_portait { width } else { height }, + }, + } + } +} diff --git a/libwayshot/src/screencopy.rs b/libwayshot/src/screencopy.rs index e2713582..2df15601 100644 --- a/libwayshot/src/screencopy.rs +++ b/libwayshot/src/screencopy.rs @@ -11,20 +11,38 @@ use nix::{ sys::{memfd, mman, stat}, unistd, }; -use wayland_client::protocol::{wl_output, wl_shm::Format}; +use wayland_client::protocol::{ + wl_buffer::WlBuffer, wl_output, wl_shm::Format, wl_shm_pool::WlShmPool, +}; use crate::{Error, Result}; +pub struct FrameGuard { + pub buffer: WlBuffer, + pub shm_pool: WlShmPool, +} + +impl Drop for FrameGuard { + fn drop(&mut self) { + self.buffer.destroy(); + self.shm_pool.destroy(); + } +} + /// Type of frame supported by the compositor. For now we only support Argb8888, Xrgb8888, and /// Xbgr8888. +/// +/// See `zwlr_screencopy_frame_v1::Event::Buffer` as it's retrieved from there. #[derive(Debug, Copy, Clone, PartialEq)] pub struct FrameFormat { pub format: Format, pub width: u32, pub height: u32, + /// Stride is the number of bytes between the start of a row and the start of the next row. pub stride: u32, } +#[tracing::instrument(skip(frame_mmap))] fn create_image_buffer

( frame_format: &FrameFormat, frame_mmap: &MmapMut, @@ -32,6 +50,7 @@ fn create_image_buffer

( where P: Pixel, { + tracing::debug!("Creating image buffer"); ImageBuffer::from_vec(frame_format.width, frame_format.height, frame_mmap.to_vec()) .ok_or(Error::BufferTooSmall) } @@ -44,12 +63,13 @@ pub struct FrameCopy { pub frame_color_type: ColorType, pub frame_mmap: MmapMut, pub transform: wl_output::Transform, + pub position: (i64, i64), } -impl TryFrom for DynamicImage { +impl TryFrom<&FrameCopy> for DynamicImage { type Error = Error; - fn try_from(value: FrameCopy) -> Result { + fn try_from(value: &FrameCopy) -> Result { Ok(match value.frame_color_type { ColorType::Rgb8 => { Self::ImageRgb8(create_image_buffer(&value.frame_format, &value.frame_mmap)?) diff --git a/wayshot/src/utils.rs b/wayshot/src/utils.rs index e24caf74..c6cd60fa 100644 --- a/wayshot/src/utils.rs +++ b/wayshot/src/utils.rs @@ -3,9 +3,9 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -use libwayshot::CaptureRegion; +use libwayshot::region::{LogicalRegion, Region}; -pub fn parse_geometry(g: &str) -> Option { +pub fn parse_geometry(g: &str) -> Option { let tail = g.trim(); let x_coordinate: i32; let y_coordinate: i32; @@ -32,11 +32,13 @@ pub fn parse_geometry(g: &str) -> Option { height = tail.parse::().ok()?; } - Some(CaptureRegion { - x_coordinate, - y_coordinate, - width, - height, + Some(LogicalRegion { + inner: Region { + x: x_coordinate, + y: y_coordinate, + width, + height, + }, }) } From a023ff1ce2daeb389006489afb3b320edf3637b8 Mon Sep 17 00:00:00 2001 From: Andreas Backx Date: Fri, 2 Feb 2024 18:59:36 +0000 Subject: [PATCH 02/26] Add freeze functionality --- Cargo.lock | 17 ++++++ libwayshot/src/dispatch.rs | 69 +++++++++++++++++----- libwayshot/src/error.rs | 2 + libwayshot/src/lib.rs | 113 ++++++++++++++++++++++++++++++++++++- libwayshot/src/output.rs | 7 ++- libwayshot/src/region.rs | 6 +- wayshot/Cargo.toml | 1 + wayshot/src/clap.rs | 4 +- wayshot/src/utils.rs | 36 ++++++------ wayshot/src/wayshot.rs | 29 ++++++---- 10 files changed, 236 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d5bff5d..500045a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -207,6 +207,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fastrand" version = "2.0.1" @@ -257,6 +267,12 @@ dependencies = [ "qoi", ] +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "jpeg-decoder" version = "0.3.0" @@ -756,6 +772,7 @@ version = "1.3.2-dev" dependencies = [ "clap", "dialoguer", + "eyre", "flate2", "image", "libwayshot", diff --git a/libwayshot/src/dispatch.rs b/libwayshot/src/dispatch.rs index 170e1026..f4eedb06 100644 --- a/libwayshot/src/dispatch.rs +++ b/libwayshot/src/dispatch.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashSet, process::exit, sync::atomic::{AtomicBool, Ordering}, }; @@ -6,8 +7,9 @@ use wayland_client::{ delegate_noop, globals::GlobalListContents, protocol::{ - wl_buffer::WlBuffer, wl_output, wl_output::WlOutput, wl_registry, wl_registry::WlRegistry, - wl_shm::WlShm, wl_shm_pool::WlShmPool, + wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_output, wl_output::WlOutput, + wl_registry, wl_registry::WlRegistry, wl_shm::WlShm, wl_shm_pool::WlShmPool, + wl_surface::WlSurface, }, Connection, Dispatch, QueueHandle, WEnum, WEnum::Value, @@ -15,6 +17,10 @@ use wayland_client::{ use wayland_protocols::xdg::xdg_output::zv1::client::{ zxdg_output_manager_v1::ZxdgOutputManagerV1, zxdg_output_v1, zxdg_output_v1::ZxdgOutputV1, }; +use wayland_protocols_wlr::layer_shell::v1::client::{ + zwlr_layer_shell_v1::ZwlrLayerShellV1, + zwlr_layer_surface_v1::{self, ZwlrLayerSurfaceV1}, +}; use wayland_protocols_wlr::screencopy::v1::client::{ zwlr_screencopy_frame_v1, zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1, zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1, @@ -58,16 +64,9 @@ impl Dispatch for OutputCaptureState { name: "".to_string(), description: String::new(), transform: wl_output::Transform::Normal, - dimensions: OutputPositioning { - x: 0, - y: 0, - width: 0, - height: 0, - }, - mode: WlOutputMode { - width: 0, - height: 0, - }, + scale: 1, + dimensions: OutputPositioning::default(), + mode: WlOutputMode::default(), }); } else { tracing::error!("Ignoring a wl_output with version < 4."); @@ -109,7 +108,11 @@ impl Dispatch for OutputCaptureState { } => { output.transform = transform; } - _ => (), + wl_output::Event::Scale { factor } => { + output.scale = factor; + } + wl_output::Event::Done => {} + _ => {} } } } @@ -227,3 +230,43 @@ impl wayland_client::Dispatch for W ) { } } + +pub struct LayerShellState { + pub configured_outputs: HashSet, +} + +delegate_noop!(LayerShellState: ignore WlCompositor); +delegate_noop!(LayerShellState: ignore WlShm); +delegate_noop!(LayerShellState: ignore WlShmPool); +delegate_noop!(LayerShellState: ignore WlBuffer); +delegate_noop!(LayerShellState: ignore ZwlrLayerShellV1); +delegate_noop!(LayerShellState: ignore WlSurface); + +impl wayland_client::Dispatch for LayerShellState { + // No need to instrument here, span from lib.rs is automatically used. + fn event( + state: &mut Self, + proxy: &ZwlrLayerSurfaceV1, + event: ::Event, + data: &WlOutput, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + match event { + zwlr_layer_surface_v1::Event::Configure { + serial, + width: _, + height: _, + } => { + tracing::debug!("Acking configure"); + state.configured_outputs.insert(data.clone()); + proxy.ack_configure(serial); + tracing::trace!("Acked configure"); + } + zwlr_layer_surface_v1::Event::Closed => { + tracing::debug!("Closed") + } + _ => {} + } + } +} diff --git a/libwayshot/src/error.rs b/libwayshot/src/error.rs index ddeff581..ab0e1ec3 100644 --- a/libwayshot/src/error.rs +++ b/libwayshot/src/error.rs @@ -27,4 +27,6 @@ pub enum Error { NoSupportedBufferFormat, #[error("Cannot find required wayland protocol")] ProtocolNotFound(String), + #[error("error occurred in freeze callback")] + FreezeCallbackError, } diff --git a/libwayshot/src/lib.rs b/libwayshot/src/lib.rs index c93cb335..2128ed55 100644 --- a/libwayshot/src/lib.rs +++ b/libwayshot/src/lib.rs @@ -12,6 +12,7 @@ pub mod region; mod screencopy; use std::{ + collections::HashSet, fs::File, os::fd::AsFd, process::exit, @@ -19,6 +20,7 @@ use std::{ thread, }; +use dispatch::LayerShellState; use image::{imageops::replace, DynamicImage}; use memmap2::MmapMut; use region::{EmbeddedRegion, RegionCapturer}; @@ -27,6 +29,7 @@ use tracing::debug; use wayland_client::{ globals::{registry_queue_init, GlobalList}, protocol::{ + wl_compositor::WlCompositor, wl_output::WlOutput, wl_shm::{self, WlShm}, }, @@ -35,9 +38,15 @@ use wayland_client::{ use wayland_protocols::xdg::xdg_output::zv1::client::{ zxdg_output_manager_v1::ZxdgOutputManagerV1, zxdg_output_v1::ZxdgOutputV1, }; -use wayland_protocols_wlr::screencopy::v1::client::{ - zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1, - zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1, +use wayland_protocols_wlr::{ + layer_shell::v1::client::{ + zwlr_layer_shell_v1::{Layer, ZwlrLayerShellV1}, + zwlr_layer_surface_v1::Anchor, + }, + screencopy::v1::client::{ + zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1, + zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1, + }, }; use crate::{ @@ -406,6 +415,87 @@ impl WayshotConnection { Ok(frame_copies) } + fn overlay_frames(&self, frames: &Vec<(FrameCopy, FrameGuard, OutputInfo)>) -> Result<()> { + let mut state = LayerShellState { + configured_outputs: HashSet::new(), + }; + let mut event_queue: EventQueue = + self.conn.new_event_queue::(); + let qh = event_queue.handle(); + + let compositor = match self.globals.bind::(&qh, 3..=3, ()) { + Ok(x) => x, + Err(e) => { + tracing::error!( + "Failed to create compositor Does your compositor implement WlCompositor?" + ); + tracing::error!("err: {e}"); + return Err(Error::ProtocolNotFound( + "WlCompositor not found".to_string(), + )); + } + }; + let layer_shell = match self.globals.bind::(&qh, 1..=1, ()) { + Ok(x) => x, + Err(e) => { + tracing::error!( + "Failed to create layer shell. Does your compositor implement WlrLayerShellV1?" + ); + tracing::error!("err: {e}"); + return Err(Error::ProtocolNotFound( + "WlrLayerShellV1 not found".to_string(), + )); + } + }; + + for (frame_copy, frame_guard, output_info) in frames { + tracing::span!( + tracing::Level::DEBUG, + "overlay_frames::surface", + output = output_info.name.as_str() + ) + .in_scope(|| -> Result<()> { + let surface = compositor.create_surface(&qh, ()); + + let layer_surface = layer_shell.get_layer_surface( + &surface, + Some(&output_info.wl_output), + Layer::Top, + "wayshot".to_string(), + &qh, + output_info.wl_output.clone(), + ); + + layer_surface.set_exclusive_zone(-1); + layer_surface.set_anchor(Anchor::Top | Anchor::Left); + layer_surface.set_size( + frame_copy.frame_format.width, + frame_copy.frame_format.height, + ); + + debug!("Committing surface creation changes."); + surface.commit(); + + debug!("Waiting for layer surface to be configured."); + while !state.configured_outputs.contains(&output_info.wl_output) { + event_queue.blocking_dispatch(&mut state)?; + } + + surface.set_buffer_transform(output_info.transform); + surface.set_buffer_scale(output_info.scale); + surface.attach(Some(&frame_guard.buffer), 0, 0); + + debug!("Committing surface with attached buffer."); + surface.commit(); + + event_queue.blocking_dispatch(&mut state)?; + + Ok(()) + })?; + } + Ok(()) + } + /// Take a screenshot from the specified region. fn screenshot_region_capturer( &self, @@ -445,6 +535,11 @@ impl WayshotConnection { }) }) .collect(), + RegionCapturer::Freeze(_) => self + .get_all_outputs() + .into_iter() + .map(|output_info| (output_info.clone(), None)) + .collect(), }; let frames = self.capture_frame_copies(outputs_capture_regions, cursor_overlay)?; @@ -452,6 +547,9 @@ impl WayshotConnection { let capture_region: LogicalRegion = match region_capturer { RegionCapturer::Outputs(ref outputs) => outputs.try_into()?, RegionCapturer::Region(region) => region, + RegionCapturer::Freeze(callback) => { + self.overlay_frames(&frames).and_then(|_| callback())? + } }; thread::scope(|scope| { @@ -529,6 +627,15 @@ impl WayshotConnection { self.screenshot_region_capturer(RegionCapturer::Region(capture_region), cursor_overlay) } + /// Take a screenshot, overlay the screenshot, run the callback, and then + /// unfreeze the screenshot and return the selected region. + pub fn screenshot_freeze( + &self, + callback: Box Result>, + cursor_overlay: bool, + ) -> Result { + self.screenshot_region_capturer(RegionCapturer::Freeze(callback), cursor_overlay) + } /// shot one ouput pub fn screenshot_single_output( &self, diff --git a/libwayshot/src/output.rs b/libwayshot/src/output.rs index ccca1c80..986f74df 100644 --- a/libwayshot/src/output.rs +++ b/libwayshot/src/output.rs @@ -3,23 +3,24 @@ use wayland_client::protocol::{wl_output, wl_output::WlOutput}; /// Represents an accessible wayland output. /// /// Do not instantiate, instead use [`crate::WayshotConnection::get_all_outputs`]. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct OutputInfo { pub wl_output: WlOutput, pub name: String, pub description: String, pub transform: wl_output::Transform, + pub scale: i32, pub dimensions: OutputPositioning, pub mode: WlOutputMode, } -#[derive(Default, Debug, Clone, PartialEq, Eq)] +#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] pub struct WlOutputMode { pub width: i32, pub height: i32, } -#[derive(Default, Debug, Clone, PartialEq, Eq)] +#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] pub struct OutputPositioning { pub x: i32, pub y: i32, diff --git a/libwayshot/src/region.rs b/libwayshot/src/region.rs index a82e0cdc..490e6082 100644 --- a/libwayshot/src/region.rs +++ b/libwayshot/src/region.rs @@ -2,7 +2,7 @@ use std::cmp; use wayland_client::protocol::wl_output::Transform; -use crate::error::Error; +use crate::error::{Error, Result}; use crate::output::OutputInfo; use crate::screencopy::FrameCopy; @@ -12,6 +12,10 @@ pub enum RegionCapturer { Outputs(Vec), /// Capture an already known `LogicalRegion`. Region(LogicalRegion), + /// The outputs will be "frozen" to the user at which point the given + /// callback is called to get the region to capture. This callback is often + /// a user interaction to let the user select a region. + Freeze(Box Result>), } /// `Region` where the coordinate system is the logical coordinate system used diff --git a/wayshot/Cargo.toml b/wayshot/Cargo.toml index 8d4aa528..e4ac7af3 100644 --- a/wayshot/Cargo.toml +++ b/wayshot/Cargo.toml @@ -29,6 +29,7 @@ image = { version = "0.24", default-features = false, features = [ ] } dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } +eyre = "0.6.8" [[bin]] name = "wayshot" diff --git a/wayshot/src/clap.rs b/wayshot/src/clap.rs index 37319334..dfed67b4 100644 --- a/wayshot/src/clap.rs +++ b/wayshot/src/clap.rs @@ -12,10 +12,10 @@ pub fn set_flags() -> Command { .help("Enable debug mode"), ) .arg( - arg!(-s --slurp ) + arg!(-s --slurp ) .required(false) .action(ArgAction::Set) - .help("Choose a portion of your display to screenshot using slurp"), + .help("Arguments to call slurp with for selecting a region"), ) .arg( arg!(-f - -file ) diff --git a/wayshot/src/utils.rs b/wayshot/src/utils.rs index c6cd60fa..cea4217d 100644 --- a/wayshot/src/utils.rs +++ b/wayshot/src/utils.rs @@ -1,3 +1,4 @@ +use eyre::{ContextCompat, Result}; use std::{ process::exit, time::{SystemTime, UNIX_EPOCH}, @@ -5,34 +6,37 @@ use std::{ use libwayshot::region::{LogicalRegion, Region}; -pub fn parse_geometry(g: &str) -> Option { +pub fn parse_geometry(g: &str) -> Result { let tail = g.trim(); let x_coordinate: i32; let y_coordinate: i32; let width: i32; let height: i32; + let validation_error = + "Invalid geometry provided.\nValid geometries:\n1) %d,%d %dx%d\n2) %d %d %d %d"; + if tail.contains(',') { // this accepts: "%d,%d %dx%d" - let (head, tail) = tail.split_once(',')?; - x_coordinate = head.parse::().ok()?; - let (head, tail) = tail.split_once(' ')?; - y_coordinate = head.parse::().ok()?; - let (head, tail) = tail.split_once('x')?; - width = head.parse::().ok()?; - height = tail.parse::().ok()?; + let (head, tail) = tail.split_once(',').wrap_err(validation_error)?; + x_coordinate = head.parse::()?; + let (head, tail) = tail.split_once(' ').wrap_err(validation_error)?; + y_coordinate = head.parse::()?; + let (head, tail) = tail.split_once('x').wrap_err(validation_error)?; + width = head.parse::()?; + height = tail.parse::()?; } else { // this accepts: "%d %d %d %d" - let (head, tail) = tail.split_once(' ')?; - x_coordinate = head.parse::().ok()?; - let (head, tail) = tail.split_once(' ')?; - y_coordinate = head.parse::().ok()?; - let (head, tail) = tail.split_once(' ')?; - width = head.parse::().ok()?; - height = tail.parse::().ok()?; + let (head, tail) = tail.split_once(' ').wrap_err(validation_error)?; + x_coordinate = head.parse::()?; + let (head, tail) = tail.split_once(' ').wrap_err(validation_error)?; + y_coordinate = head.parse::()?; + let (head, tail) = tail.split_once(' ').wrap_err(validation_error)?; + width = head.parse::()?; + height = tail.parse::()?; } - Some(LogicalRegion { + Ok(LogicalRegion { inner: Region { x: x_coordinate, y: y_coordinate, diff --git a/wayshot/src/wayshot.rs b/wayshot/src/wayshot.rs index b4b3c01f..4353d579 100644 --- a/wayshot/src/wayshot.rs +++ b/wayshot/src/wayshot.rs @@ -1,10 +1,10 @@ use std::{ - error::Error, io::{stdout, BufWriter, Cursor, Write}, - process::exit, + process::{exit, Command}, }; -use libwayshot::WayshotConnection; +use eyre::Result; +use libwayshot::{region::LogicalRegion, WayshotConnection}; mod clap; mod utils; @@ -29,7 +29,7 @@ where Some(selection) } -fn main() -> Result<(), Box> { +fn main() -> Result<()> { let args = clap::set_flags().get_matches(); let level = if args.get_flag("debug") { Level::TRACE @@ -86,12 +86,21 @@ fn main() -> Result<(), Box> { } let image_buffer = if let Some(slurp_region) = args.get_one::("slurp") { - if let Some(region) = utils::parse_geometry(slurp_region) { - wayshot_conn.screenshot(region, cursor_overlay)? - } else { - tracing::error!("Invalid geometry specification"); - exit(1); - } + let slurp_region = slurp_region.clone(); + wayshot_conn.screenshot_freeze( + Box::new(move || { + || -> Result { + let slurp_output = Command::new("slurp") + .args(slurp_region.split(" ")) + .output()? + .stdout; + + utils::parse_geometry(&String::from_utf8(slurp_output)?) + }() + .map_err(|_| libwayshot::Error::FreezeCallbackError) + }), + cursor_overlay, + )? } else if let Some(output_name) = args.get_one::("output") { let outputs = wayshot_conn.get_all_outputs(); if let Some(output) = outputs.iter().find(|output| &output.name == output_name) { From 87be331d815ef3176ae131b45d3353604fd87601 Mon Sep 17 00:00:00 2001 From: Andreas Backx Date: Fri, 2 Feb 2024 19:01:37 +0000 Subject: [PATCH 03/26] Move to clap_derive so CLI arguments are typed * Renames `clap.rs` to `cli.rs` because otherwise there's confusion between the `clap` crate and local module. * `Cli` struct added that almost identically represents the current state of the CLI with no logical changes. --- ## `--help` Comparison Before: https://gist.github.com/AndreasBackx/5945b366e989159f4669e7ba30c13239 After: https://gist.github.com/AndreasBackx/8929c8bde080eac0cafd33128210b0cc Diff (ignoring whitespace changes due to table alignment): ```diff 1c1 < Screenshot tool for compositors implementing zwlr_screencopy_v1. --- > Screenshot tool for wlroots based compositors implementing the zwlr_screencopy_v1 protocol. 9d8 < -c, --cursor Enable cursor in screenshots 10a10 > -c, --cursor Enable cursor in screenshots 12c12 < -l, --listoutputs List all valid outputs --- > -l, --list-outputs List all valid outputs 14c14 < --chooseoutput Present a fuzzy selector for outputs --- > --choose-output Present a fuzzy selector for outputs 16a17 > ``` You can see that the only changes are: - About is longer, this is now using the value from Cargo.toml instead of a duplicate text that was shorter. - Some have a dash where the English words would have a space, e.g: "list-outputs" instead of "listoutputs". I've also made the old still work via an alias: https://gist.github.com/AndreasBackx/6025e91844e3d766d4264a01ae4d1a71 This seems like a tiny improvement? I plan to make further changes later, but I want to keep PRs separate. --- Cargo.lock | 31 +++++++++++++++---- wayshot/Cargo.toml | 2 +- wayshot/src/clap.rs | 67 ------------------------------------------ wayshot/src/cli.rs | 43 +++++++++++++++++++++++++++ wayshot/src/wayshot.rs | 40 ++++++++++--------------- 5 files changed, 85 insertions(+), 98 deletions(-) delete mode 100644 wayshot/src/clap.rs create mode 100644 wayshot/src/cli.rs diff --git a/Cargo.lock b/Cargo.lock index 500045a8..9244fa4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "anstream" -version = "0.6.5" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" dependencies = [ "anstyle", "anstyle-parse", @@ -103,18 +103,19 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.4.11" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] name = "clap_builder" -version = "4.4.11" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" dependencies = [ "anstream", "anstyle", @@ -122,6 +123,18 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clap_lex" version = "0.6.0" @@ -251,6 +264,12 @@ dependencies = [ "thread_local", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "image" version = "0.24.7" diff --git a/wayshot/Cargo.toml b/wayshot/Cargo.toml index e4ac7af3..344654f9 100644 --- a/wayshot/Cargo.toml +++ b/wayshot/Cargo.toml @@ -18,7 +18,7 @@ tracing.workspace = true libwayshot.workspace = true -clap = "4.4.6" +clap = { version = "4.4.18", features = ["derive"] } tracing-subscriber = "0.3.17" image = { version = "0.24", default-features = false, features = [ diff --git a/wayshot/src/clap.rs b/wayshot/src/clap.rs deleted file mode 100644 index dfed67b4..00000000 --- a/wayshot/src/clap.rs +++ /dev/null @@ -1,67 +0,0 @@ -use clap::{arg, ArgAction, Command}; - -pub fn set_flags() -> Command { - Command::new("wayshot") - .version(env!("CARGO_PKG_VERSION")) - .author(env!("CARGO_PKG_AUTHORS")) - .about("Screenshot tool for compositors implementing zwlr_screencopy_v1.") - .arg( - arg!(-d - -debug) - .required(false) - .action(ArgAction::SetTrue) - .help("Enable debug mode"), - ) - .arg( - arg!(-s --slurp ) - .required(false) - .action(ArgAction::Set) - .help("Arguments to call slurp with for selecting a region"), - ) - .arg( - arg!(-f - -file ) - .required(false) - .conflicts_with("stdout") - .action(ArgAction::Set) - .help("Mention a custom file path"), - ) - .arg( - arg!(-c - -cursor) - .required(false) - .action(ArgAction::SetTrue) - .help("Enable cursor in screenshots"), - ) - .arg( - arg!(--stdout) - .required(false) - .conflicts_with("file") - .action(ArgAction::SetTrue) - .help("Output the image data to standard out"), - ) - .arg( - arg!(-e --extension ) - .required(false) - .action(ArgAction::Set) - .help("Set image encoder (Png is default)"), - ) - .arg( - arg!(-l - -listoutputs) - .required(false) - .action(ArgAction::SetTrue) - .help("List all valid outputs"), - ) - .arg( - arg!(-o --output ) - .required(false) - .action(ArgAction::Set) - .conflicts_with("slurp") - .help("Choose a particular display to screenshot"), - ) - .arg( - arg!(--chooseoutput) - .required(false) - .action(ArgAction::SetTrue) - .conflicts_with("slurp") - .conflicts_with("output") - .help("Present a fuzzy selector for outputs"), - ) -} diff --git a/wayshot/src/cli.rs b/wayshot/src/cli.rs new file mode 100644 index 00000000..182737b2 --- /dev/null +++ b/wayshot/src/cli.rs @@ -0,0 +1,43 @@ +use clap::arg; + +use clap::Parser; + +#[derive(Parser)] +#[command(version, about)] +pub struct Cli { + /// Enable debug mode + #[arg(short, long, action = clap::ArgAction::SetTrue)] + pub debug: bool, + + /// Arguments to call slurp with for selecting a region + #[arg(short, long, value_name = "SLURP_ARGS")] + pub slurp: Option, + + /// Mention a custom file path + #[arg(short, long, conflicts_with = "stdout", value_name = "FILE_PATH")] + pub file: Option, + + /// Output the image data to standard out + #[arg(long, conflicts_with = "file", action = clap::ArgAction::SetTrue)] + pub stdout: bool, + + /// Enable cursor in screenshots + #[arg(short, long, action = clap::ArgAction::SetTrue)] + pub cursor: bool, + + /// Set image encoder (Png is default) + #[arg(short, long, value_name = "FILE_EXTENSION")] + pub extension: Option, + + /// List all valid outputs + #[arg(short, long, alias = "listoutputs", action = clap::ArgAction::SetTrue)] + pub list_outputs: bool, + + /// Choose a particular display to screenshot + #[arg(short, long, conflicts_with = "slurp")] + pub output: Option, + + /// Present a fuzzy selector for outputs + #[arg(long, alias = "chooseoutput", conflicts_with_all = ["slurp", "output"], action = clap::ArgAction::SetTrue)] + pub choose_output: bool, +} diff --git a/wayshot/src/wayshot.rs b/wayshot/src/wayshot.rs index 4353d579..8e3bc8af 100644 --- a/wayshot/src/wayshot.rs +++ b/wayshot/src/wayshot.rs @@ -3,10 +3,11 @@ use std::{ process::{exit, Command}, }; +use clap::Parser; use eyre::Result; use libwayshot::{region::LogicalRegion, WayshotConnection}; -mod clap; +mod cli; mod utils; use dialoguer::{theme::ColorfulTheme, FuzzySelect}; @@ -30,18 +31,14 @@ where } fn main() -> Result<()> { - let args = clap::set_flags().get_matches(); - let level = if args.get_flag("debug") { - Level::TRACE - } else { - Level::INFO - }; + let cli = cli::Cli::parse(); + let level = if cli.debug { Level::TRACE } else { Level::INFO }; tracing_subscriber::fmt() .with_max_level(level) .with_writer(std::io::stderr) .init(); - let extension = if let Some(extension) = args.get_one::("extension") { + let extension = if let Some(extension) = cli.extension { let ext = extension.trim().to_lowercase(); tracing::debug!("Using custom extension: {:#?}", ext); @@ -62,9 +59,9 @@ fn main() -> Result<()> { let mut file_is_stdout: bool = false; let mut file_path: Option = None; - if args.get_flag("stdout") { + if cli.stdout { file_is_stdout = true; - } else if let Some(filepath) = args.get_one::("file") { + } else if let Some(filepath) = cli.file { file_path = Some(filepath.trim().to_string()); } else { file_path = Some(utils::get_default_file_name(extension)); @@ -72,7 +69,7 @@ fn main() -> Result<()> { let wayshot_conn = WayshotConnection::new()?; - if args.get_flag("listoutputs") { + if cli.list_outputs { let valid_outputs = wayshot_conn.get_all_outputs(); for output in valid_outputs { tracing::info!("{:#?}", output.name); @@ -80,12 +77,7 @@ fn main() -> Result<()> { exit(1); } - let mut cursor_overlay = false; - if args.get_flag("cursor") { - cursor_overlay = true; - } - - let image_buffer = if let Some(slurp_region) = args.get_one::("slurp") { + let image_buffer = if let Some(slurp_region) = cli.slurp { let slurp_region = slurp_region.clone(); wayshot_conn.screenshot_freeze( Box::new(move || { @@ -99,30 +91,30 @@ fn main() -> Result<()> { }() .map_err(|_| libwayshot::Error::FreezeCallbackError) }), - cursor_overlay, + cli.cursor, )? - } else if let Some(output_name) = args.get_one::("output") { + } else if let Some(output_name) = cli.output { let outputs = wayshot_conn.get_all_outputs(); - if let Some(output) = outputs.iter().find(|output| &output.name == output_name) { - wayshot_conn.screenshot_single_output(output, cursor_overlay)? + if let Some(output) = outputs.iter().find(|output| output.name == output_name) { + wayshot_conn.screenshot_single_output(output, cli.cursor)? } else { tracing::error!("No output found!\n"); exit(1); } - } else if args.get_flag("chooseoutput") { + } else if cli.choose_output { let outputs = wayshot_conn.get_all_outputs(); let output_names: Vec = outputs .iter() .map(|display| display.name.to_string()) .collect(); if let Some(index) = select_ouput(&output_names) { - wayshot_conn.screenshot_single_output(&outputs[index], cursor_overlay)? + wayshot_conn.screenshot_single_output(&outputs[index], cli.cursor)? } else { tracing::error!("No output found!\n"); exit(1); } } else { - wayshot_conn.screenshot_all(cursor_overlay)? + wayshot_conn.screenshot_all(cli.cursor)? }; if file_is_stdout { From 736b6515c86f5443e09aa888629c439cff1d5618 Mon Sep 17 00:00:00 2001 From: Andreas Backx Date: Fri, 2 Feb 2024 19:01:37 +0000 Subject: [PATCH 04/26] Improve CLI design This "improves" (and that is subjective) the design of the CLI. I am aiming to get some feedback on what people think of the new design: ``` Screenshot tool for wlroots based compositors implementing the zwlr_screencopy_v1 protocol. Usage: wayshot [OPTIONS] [OUTPUT] Arguments: [OUTPUT] Where to save the screenshot, "-" for stdout. Defaults to "$UNIX_TIMESTAMP-wayshot.$EXTENSION" Options: --log-level Log level to be used for printing to stderr [default: info] [possible values: trace, debug, info, warn, error] -s, --slurp Arguments to call slurp with for selecting a region -c, --cursor Enable cursor in screenshots --encoding Set image encoder, if output file contains an extension, that will be used instead [default: png] [aliases: extension, format, output-format] Possible values: - jpg: JPG/JPEG encoder - png: PNG encoder - ppm: PPM encoder - qoi: Qut encoder -l, --list-outputs List all valid outputs -o, --output Choose a particular output/display to screenshot --choose-output Present a fuzzy selector for output/display selection -h, --help Print help (see a summary with '-h') -V, --version Print version ``` The main changes are: 1. `--debug` is now `--log-level` because this makes it easy to select more specifically what log level to use. I considered using `-v`, `-vv`... to increase verbosity but the `clap-verbosity-crate` uses `log` and not `tracing`. We could use it somewhat, but we'd pull in `log` (which seems fine) and we'd need to map from `log`'s Level to `tracing`'s Level enums (they use inverse ordering). 2. `--stdout` and `--file` has been made an optional positional argument. This because it's what other CLIs often do and I wasn't sure what to call the option otherwise because `--output` and `-O`/`-o` is often what others use but we use it here to refer to displays/monitors/Wayland outputs. This avoids that confusion hopefully and I've also clarified this in the documentation. * Additionally if a path is given, its extension will always be used. So you cannot save `jpg` to `foo.png`. Perhaps this behaviour can be changed, though I don't see a reason to support this weird edge case? When is someone saving `png` to `jpg`? 3. `--extension` is `--encoding` with aliases like `extension`. Again, let me know what you think. --- .gitignore | 5 ++++ wayshot/src/cli.rs | 40 +++++++++++++------------ wayshot/src/utils.rs | 68 +++++++++++++++++++++++++++++++++++++----- wayshot/src/wayshot.rs | 64 +++++++++++++++++++-------------------- 4 files changed, 116 insertions(+), 61 deletions(-) diff --git a/.gitignore b/.gitignore index 5a4f6354..2622eacf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,8 @@ target *.gz *.out .direnv +*.jpg +*.jpeg +*.png +*.ppm +*.qoi diff --git a/wayshot/src/cli.rs b/wayshot/src/cli.rs index 182737b2..a076af5c 100644 --- a/wayshot/src/cli.rs +++ b/wayshot/src/cli.rs @@ -1,43 +1,45 @@ +use std::path::PathBuf; + use clap::arg; use clap::Parser; +use crate::utils::EncodingFormat; +use clap::builder::TypedValueParser; + #[derive(Parser)] #[command(version, about)] pub struct Cli { - /// Enable debug mode - #[arg(short, long, action = clap::ArgAction::SetTrue)] - pub debug: bool, + /// Where to save the screenshot, "-" for stdout. Defaults to "$UNIX_TIMESTAMP-wayshot.$EXTENSION". + #[arg(value_name = "OUTPUT")] + pub file: Option, + + /// Log level to be used for printing to stderr + #[arg(long, default_value = "info", value_parser = clap::builder::PossibleValuesParser::new(["trace", "debug", "info", "warn", "error"]).map(|s| -> tracing::Level{ s.parse().unwrap()}))] + pub log_level: tracing::Level, /// Arguments to call slurp with for selecting a region #[arg(short, long, value_name = "SLURP_ARGS")] pub slurp: Option, - /// Mention a custom file path - #[arg(short, long, conflicts_with = "stdout", value_name = "FILE_PATH")] - pub file: Option, - - /// Output the image data to standard out - #[arg(long, conflicts_with = "file", action = clap::ArgAction::SetTrue)] - pub stdout: bool, - /// Enable cursor in screenshots - #[arg(short, long, action = clap::ArgAction::SetTrue)] + #[arg(short, long)] pub cursor: bool, - /// Set image encoder (Png is default) - #[arg(short, long, value_name = "FILE_EXTENSION")] - pub extension: Option, + /// Set image encoder, by default uses the file extension from the OUTPUT + /// positional argument. Otherwise defaults to png. + #[arg(long, visible_aliases = ["extension", "format", "output-format"], value_name = "FILE_EXTENSION")] + pub encoding: Option, /// List all valid outputs - #[arg(short, long, alias = "listoutputs", action = clap::ArgAction::SetTrue)] + #[arg(short, long, alias = "listoutputs")] pub list_outputs: bool, - /// Choose a particular display to screenshot + /// Choose a particular output/display to screenshot #[arg(short, long, conflicts_with = "slurp")] pub output: Option, - /// Present a fuzzy selector for outputs - #[arg(long, alias = "chooseoutput", conflicts_with_all = ["slurp", "output"], action = clap::ArgAction::SetTrue)] + /// Present a fuzzy selector for output/display selection + #[arg(long, alias = "chooseoutput", conflicts_with_all = ["slurp", "output"])] pub choose_output: bool, } diff --git a/wayshot/src/utils.rs b/wayshot/src/utils.rs index cea4217d..1e394452 100644 --- a/wayshot/src/utils.rs +++ b/wayshot/src/utils.rs @@ -1,6 +1,11 @@ -use eyre::{ContextCompat, Result}; +use clap::ValueEnum; +use eyre::{bail, ContextCompat, Error, Result}; + use std::{ + fmt::Display, + path::PathBuf, process::exit, + str::FromStr, time::{SystemTime, UNIX_EPOCH}, }; @@ -47,18 +52,24 @@ pub fn parse_geometry(g: &str) -> Result { } /// Supported image encoding formats. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum)] pub enum EncodingFormat { - /// Jpeg / jpg encoder. + /// JPG/JPEG encoder. Jpg, - /// Png encoder. + /// PNG encoder. Png, - /// Ppm encoder. + /// PPM encoder. Ppm, - /// Qoi encoder. + /// Qut encoder. Qoi, } +impl Default for EncodingFormat { + fn default() -> Self { + Self::Png + } +} + impl From for image::ImageOutputFormat { fn from(format: EncodingFormat) -> Self { match format { @@ -70,6 +81,33 @@ impl From for image::ImageOutputFormat { } } +impl TryFrom<&PathBuf> for EncodingFormat { + type Error = Error; + + fn try_from(value: &PathBuf) -> std::result::Result { + value + .extension() + .wrap_err_with(|| { + format!( + "no extension in {} to deduce encoding format", + value.display() + ) + }) + .and_then(|ext| { + ext.to_str().wrap_err_with(|| { + format!("extension in {} is not valid unicode", value.display()) + }) + }) + .and_then(|ext| ext.parse()) + } +} + +impl Display for EncodingFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", Into::<&str>::into(*self)) + } +} + impl From for &str { fn from(format: EncodingFormat) -> Self { match format { @@ -81,7 +119,21 @@ impl From for &str { } } -pub fn get_default_file_name(extension: EncodingFormat) -> String { +impl FromStr for EncodingFormat { + type Err = Error; + + fn from_str(s: &str) -> std::result::Result { + Ok(match s { + "jpg" | "jpeg" => Self::Jpg, + "png" => Self::Png, + "ppm" => Self::Ppm, + "qoi" => Self::Qoi, + _ => bail!("unsupported extension '{s}'"), + }) + } +} + +pub fn get_default_file_name(extension: EncodingFormat) -> PathBuf { let time = match SystemTime::now().duration_since(UNIX_EPOCH) { Ok(n) => n.as_secs().to_string(), Err(_) => { @@ -90,5 +142,5 @@ pub fn get_default_file_name(extension: EncodingFormat) -> String { } }; - time + "-wayshot." + extension.into() + (time + "-wayshot." + extension.into()).into() } diff --git a/wayshot/src/wayshot.rs b/wayshot/src/wayshot.rs index 8e3bc8af..b5632caf 100644 --- a/wayshot/src/wayshot.rs +++ b/wayshot/src/wayshot.rs @@ -11,9 +11,7 @@ mod cli; mod utils; use dialoguer::{theme::ColorfulTheme, FuzzySelect}; -use tracing::Level; - -use crate::utils::EncodingFormat; +use utils::EncodingFormat; fn select_ouput(ouputs: &[T]) -> Option where @@ -32,41 +30,39 @@ where fn main() -> Result<()> { let cli = cli::Cli::parse(); - let level = if cli.debug { Level::TRACE } else { Level::INFO }; tracing_subscriber::fmt() - .with_max_level(level) + .with_max_level(cli.log_level) .with_writer(std::io::stderr) .init(); - let extension = if let Some(extension) = cli.extension { - let ext = extension.trim().to_lowercase(); - tracing::debug!("Using custom extension: {:#?}", ext); - - match ext.as_str() { - "jpeg" | "jpg" => EncodingFormat::Jpg, - "png" => EncodingFormat::Png, - "ppm" => EncodingFormat::Ppm, - "qoi" => EncodingFormat::Qoi, - _ => { - tracing::error!("Invalid extension provided.\nValid extensions:\n1) jpeg\n2) jpg\n3) png\n4) ppm\n5) qoi"); - exit(1); + let input_encoding = cli + .file + .as_ref() + .and_then(|pathbuf| pathbuf.try_into().ok()); + let requested_encoding = cli + .encoding + .or(input_encoding) + .unwrap_or(EncodingFormat::default()); + + if let Some(input_encoding) = input_encoding { + if input_encoding != requested_encoding { + tracing::warn!( + "The encoding requested '{requested_encoding}' does not match the output file's encoding '{input_encoding}'. Still using the requested encoding however.", + ); + } + } + + let file = match cli.file { + Some(pathbuf) => { + if pathbuf.to_string_lossy() == "-" { + None + } else { + Some(pathbuf) } } - } else { - EncodingFormat::Png + None => Some(utils::get_default_file_name(requested_encoding)), }; - let mut file_is_stdout: bool = false; - let mut file_path: Option = None; - - if cli.stdout { - file_is_stdout = true; - } else if let Some(filepath) = cli.file { - file_path = Some(filepath.trim().to_string()); - } else { - file_path = Some(utils::get_default_file_name(extension)); - } - let wayshot_conn = WayshotConnection::new()?; if cli.list_outputs { @@ -117,16 +113,16 @@ fn main() -> Result<()> { wayshot_conn.screenshot_all(cli.cursor)? }; - if file_is_stdout { + if let Some(file) = file { + image_buffer.save(file)?; + } else { let stdout = stdout(); let mut buffer = Cursor::new(Vec::new()); let mut writer = BufWriter::new(stdout.lock()); - image_buffer.write_to(&mut buffer, extension)?; + image_buffer.write_to(&mut buffer, requested_encoding)?; writer.write_all(buffer.get_ref())?; - } else { - image_buffer.save(file_path.unwrap())?; } Ok(()) From 7d6e20bdb2e6726fd0ca50778097af1174c6e0b8 Mon Sep 17 00:00:00 2001 From: Andreas Backx Date: Fri, 2 Feb 2024 19:01:37 +0000 Subject: [PATCH 05/26] Remove explicit panics and exits Let's bubble up errors instead of panicking and also not use `exit(...)` because it does not call destructors which might give issues. --- libwayshot/README.md | 4 +-- libwayshot/src/dispatch.rs | 23 +++++++++++------ libwayshot/src/error.rs | 7 +++++- libwayshot/src/lib.rs | 9 +++---- libwayshot/src/region.rs | 8 +++--- libwayshot/src/screencopy.rs | 25 ++++++++++-------- wayshot/Cargo.toml | 2 ++ wayshot/build.rs | 49 ++++++++++++++++++++++-------------- wayshot/src/cli.rs | 3 ++- wayshot/src/utils.rs | 14 ++++------- wayshot/src/wayshot.rs | 12 ++++----- 11 files changed, 89 insertions(+), 67 deletions(-) diff --git a/libwayshot/README.md b/libwayshot/README.md index a18983c7..a3bf2ab2 100644 --- a/libwayshot/README.md +++ b/libwayshot/README.md @@ -17,6 +17,6 @@ ```rust use libwayshot::WayshotConnection; -let wayshot_connection = WayshotConnection::new().unwrap(); -let image_buffer = wayshot_connection.screenshot_all().unwrap(); +let wayshot_connection = WayshotConnection::new()?; +let image_buffer = wayshot_connection.screenshot_all()?; ``` diff --git a/libwayshot/src/dispatch.rs b/libwayshot/src/dispatch.rs index f4eedb06..5352deb4 100644 --- a/libwayshot/src/dispatch.rs +++ b/libwayshot/src/dispatch.rs @@ -1,6 +1,5 @@ use std::{ collections::HashSet, - process::exit, sync::atomic::{AtomicBool, Ordering}, }; use wayland_client::{ @@ -86,11 +85,13 @@ impl Dispatch for OutputCaptureState { _: &Connection, _: &QueueHandle, ) { - let output: &mut OutputInfo = state - .outputs - .iter_mut() - .find(|x| x.wl_output == *wl_output) - .unwrap(); + let output: &mut OutputInfo = + if let Some(output) = state.outputs.iter_mut().find(|x| x.wl_output == *wl_output) { + output + } else { + tracing::error!("Received event for an output that is not registered: {event:#?}"); + return; + }; match event { wl_output::Event::Name { name } => { @@ -129,7 +130,14 @@ impl Dispatch for OutputCaptureState { _: &Connection, _: &QueueHandle, ) { - let output_info = state.outputs.get_mut(*index).unwrap(); + let output_info = if let Some(output_info) = state.outputs.get_mut(*index) { + output_info + } else { + tracing::error!( + "Received event for output index {index} that is not registered: {event:#?}" + ); + return; + }; match event { zxdg_output_v1::Event::LogicalPosition { x, y } => { @@ -189,7 +197,6 @@ impl Dispatch for CaptureFrameState { }) } else { tracing::debug!("Received Buffer event with unidentified format"); - exit(1); } } zwlr_screencopy_frame_v1::Event::Ready { .. } => { diff --git a/libwayshot/src/error.rs b/libwayshot/src/error.rs index ab0e1ec3..f3ad8523 100644 --- a/libwayshot/src/error.rs +++ b/libwayshot/src/error.rs @@ -1,7 +1,10 @@ use std::{io, result}; use thiserror::Error; -use wayland_client::{globals::GlobalError, ConnectError, DispatchError}; +use wayland_client::{ + globals::{BindError, GlobalError}, + ConnectError, DispatchError, +}; pub type Result = result::Result; @@ -17,6 +20,8 @@ pub enum Error { Io(#[from] io::Error), #[error("dispatch error: {0}")] Dispatch(#[from] DispatchError), + #[error("bind error: {0}")] + Bind(#[from] BindError), #[error("global error: {0}")] Global(#[from] GlobalError), #[error("connect error: {0}")] diff --git a/libwayshot/src/lib.rs b/libwayshot/src/lib.rs index 2128ed55..8198818c 100644 --- a/libwayshot/src/lib.rs +++ b/libwayshot/src/lib.rs @@ -15,7 +15,6 @@ use std::{ collections::HashSet, fs::File, os::fd::AsFd, - process::exit, sync::atomic::{AtomicBool, Ordering}, thread, }; @@ -68,8 +67,8 @@ pub mod reexport { /// # Example usage /// /// ``` -/// let wayshot_connection = WayshotConnection::new().unwrap(); -/// let image_buffer = wayshot_connection.screenshot_all().unwrap(); +/// let wayshot_connection = WayshotConnection::new()?; +/// let image_buffer = wayshot_connection.screenshot_all()?; /// ``` #[derive(Debug)] pub struct WayshotConnection { @@ -150,7 +149,7 @@ impl WayshotConnection { if state.outputs.is_empty() { tracing::error!("Compositor did not advertise any wl_output devices!"); - exit(1); + return Err(Error::NoOutputs); } tracing::trace!("Outputs detected: {:#?}", state.outputs); self.output_infos = state.outputs; @@ -280,7 +279,7 @@ impl WayshotConnection { let frame_bytes = frame_format.stride * frame_format.height; // Instantiate shm global. - let shm = self.globals.bind::(&qh, 1..=1, ()).unwrap(); + let shm = self.globals.bind::(&qh, 1..=1, ())?; let shm_pool = shm.create_pool(fd.as_fd(), frame_bytes as i32, &qh, ()); let buffer = shm_pool.create_buffer( 0, diff --git a/libwayshot/src/region.rs b/libwayshot/src/region.rs index 490e6082..27a35693 100644 --- a/libwayshot/src/region.rs +++ b/libwayshot/src/region.rs @@ -174,22 +174,22 @@ impl TryFrom<&Vec> for LogicalRegion { .iter() .map(|output| output.dimensions.x) .min() - .unwrap(); + .ok_or(Error::NoOutputs)?; let y1 = output_info .iter() .map(|output| output.dimensions.y) .min() - .unwrap(); + .ok_or(Error::NoOutputs)?; let x2 = output_info .iter() .map(|output| output.dimensions.x + output.dimensions.width) .max() - .unwrap(); + .ok_or(Error::NoOutputs)?; let y2 = output_info .iter() .map(|output| output.dimensions.y + output.dimensions.height) .max() - .unwrap(); + .ok_or(Error::NoOutputs)?; Ok(LogicalRegion { inner: Region { x: x1, diff --git a/libwayshot/src/screencopy.rs b/libwayshot/src/screencopy.rs index 2df15601..8d51b2ce 100644 --- a/libwayshot/src/screencopy.rs +++ b/libwayshot/src/screencopy.rs @@ -1,5 +1,5 @@ use std::{ - ffi::CStr, + ffi::CString, os::fd::{AsRawFd, IntoRawFd, OwnedFd}, time::{SystemTime, UNIX_EPOCH}, }; @@ -82,6 +82,16 @@ impl TryFrom<&FrameCopy> for DynamicImage { } } +fn get_mem_file_handle() -> String { + format!( + "/libwayshot-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|time| time.subsec_nanos().to_string()) + .unwrap_or("unknown".into()) + ) +} + /// Return a RawFd to a shm file. We use memfd create on linux and shm_open for BSD support. /// You don't need to mess around with this function, it is only used by /// capture_output_frame. @@ -91,7 +101,7 @@ pub fn create_shm_fd() -> std::io::Result { loop { // Create a file that closes on succesful execution and seal it's operations. match memfd::memfd_create( - CStr::from_bytes_with_nul(b"libwayshot\0").unwrap(), + CString::new("libwayshot")?.as_c_str(), memfd::MemFdCreateFlag::MFD_CLOEXEC | memfd::MemFdCreateFlag::MFD_ALLOW_SEALING, ) { Ok(fd) => { @@ -113,11 +123,7 @@ pub fn create_shm_fd() -> std::io::Result { } // Fallback to using shm_open. - let sys_time = SystemTime::now(); - let mut mem_file_handle = format!( - "/libwayshot-{}", - sys_time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos() - ); + let mut mem_file_handle = get_mem_file_handle(); loop { match mman::shm_open( // O_CREAT = Create file if does not exist. @@ -142,10 +148,7 @@ pub fn create_shm_fd() -> std::io::Result { }, Err(nix::errno::Errno::EEXIST) => { // If a file with that handle exists then change the handle - mem_file_handle = format!( - "/libwayshot-{}", - sys_time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos() - ); + mem_file_handle = get_mem_file_handle(); continue; } Err(nix::errno::Errno::EINTR) => continue, diff --git a/wayshot/Cargo.toml b/wayshot/Cargo.toml index 344654f9..4594d0b4 100644 --- a/wayshot/Cargo.toml +++ b/wayshot/Cargo.toml @@ -12,6 +12,8 @@ repository.workspace = true [build-dependencies] flate2 = "1.0.27" +eyre = "0.6.8" + [dependencies] tracing.workspace = true diff --git a/wayshot/build.rs b/wayshot/build.rs index 14152acb..37104611 100644 --- a/wayshot/build.rs +++ b/wayshot/build.rs @@ -1,13 +1,14 @@ extern crate flate2; +use eyre::{ContextCompat, Result}; use flate2::{write::GzEncoder, Compression}; use std::{ fs::{read_dir, File, OpenOptions}, io::{copy, BufReader, ErrorKind}, path::Path, - process::{exit, Command, Stdio}, + process::{Command, Stdio}, }; -fn main() { +fn main() -> Result<()> { if let Err(e) = Command::new("scdoc") .stdin(Stdio::null()) .stdout(Stdio::null()) @@ -15,50 +16,60 @@ fn main() { .spawn() { if let ErrorKind::NotFound = e.kind() { - exit(0); + return Ok(()); } } // We just append "out" so it's easy to find all the scdoc output later in line 38. - let man_pages: Vec<(String, String)> = read_and_replace_by_ext("./docs", ".scd", ".out"); + let man_pages: Vec<(String, String)> = read_and_replace_by_ext("./docs", ".scd", ".out")?; for man_page in man_pages { let output = OpenOptions::new() .write(true) .create(true) - .open(Path::new(&man_page.1)) - .unwrap(); + .open(Path::new(&man_page.1))?; _ = Command::new("scdoc") - .stdin(Stdio::from(File::open(man_page.0).unwrap())) + .stdin(Stdio::from(File::open(man_page.0)?)) .stdout(output) .spawn(); } // Gzipping the man pages let scdoc_output_files: Vec<(String, String)> = - read_and_replace_by_ext("./docs", ".out", ".gz"); + read_and_replace_by_ext("./docs", ".out", ".gz")?; for scdoc_output in scdoc_output_files { - let mut input = BufReader::new(File::open(scdoc_output.0).unwrap()); + let mut input = BufReader::new(File::open(scdoc_output.0)?); let output = OpenOptions::new() .write(true) .create(true) - .open(Path::new(&scdoc_output.1)) - .unwrap(); + .open(Path::new(&scdoc_output.1))?; let mut encoder = GzEncoder::new(output, Compression::default()); - copy(&mut input, &mut encoder).unwrap(); - encoder.finish().unwrap(); + copy(&mut input, &mut encoder)?; + encoder.finish()?; } + + Ok(()) } -fn read_and_replace_by_ext(path: &str, search: &str, replace: &str) -> Vec<(String, String)> { +fn read_and_replace_by_ext( + path: &str, + search: &str, + replace: &str, +) -> Result> { let mut files: Vec<(String, String)> = Vec::new(); - for path in read_dir(path).unwrap() { - let path = path.unwrap(); - if path.file_type().unwrap().is_dir() { + for path in read_dir(path)? { + let path = path?; + if path.file_type()?.is_dir() { continue; } if let Some(file_name) = path.path().to_str() { - if *path.path().extension().unwrap().to_str().unwrap() != search[1..] { + if *path + .path() + .extension() + .wrap_err_with(|| format!("no extension found for {}", path.path().display()))? + .to_string_lossy() + != search[1..] + { continue; } @@ -66,5 +77,5 @@ fn read_and_replace_by_ext(path: &str, search: &str, replace: &str) -> Vec<(Stri files.push((file_name.to_string(), file)); } } - files + Ok(files) } diff --git a/wayshot/src/cli.rs b/wayshot/src/cli.rs index a076af5c..418ac8d8 100644 --- a/wayshot/src/cli.rs +++ b/wayshot/src/cli.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use clap::arg; use clap::Parser; +use eyre::WrapErr; use crate::utils::EncodingFormat; use clap::builder::TypedValueParser; @@ -15,7 +16,7 @@ pub struct Cli { pub file: Option, /// Log level to be used for printing to stderr - #[arg(long, default_value = "info", value_parser = clap::builder::PossibleValuesParser::new(["trace", "debug", "info", "warn", "error"]).map(|s| -> tracing::Level{ s.parse().unwrap()}))] + #[arg(long, default_value = "info", value_parser = clap::builder::PossibleValuesParser::new(["trace", "debug", "info", "warn", "error"]).map(|s| -> tracing::Level{ s.parse().wrap_err_with(|| format!("Failed to parse log level: {}", s)).unwrap()}))] pub log_level: tracing::Level, /// Arguments to call slurp with for selecting a region diff --git a/wayshot/src/utils.rs b/wayshot/src/utils.rs index 1e394452..bb9a7a96 100644 --- a/wayshot/src/utils.rs +++ b/wayshot/src/utils.rs @@ -4,7 +4,6 @@ use eyre::{bail, ContextCompat, Error, Result}; use std::{ fmt::Display, path::PathBuf, - process::exit, str::FromStr, time::{SystemTime, UNIX_EPOCH}, }; @@ -134,13 +133,10 @@ impl FromStr for EncodingFormat { } pub fn get_default_file_name(extension: EncodingFormat) -> PathBuf { - let time = match SystemTime::now().duration_since(UNIX_EPOCH) { - Ok(n) => n.as_secs().to_string(), - Err(_) => { - tracing::error!("SystemTime before UNIX EPOCH!"); - exit(1); - } - }; + let time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|time| time.as_secs().to_string()) + .unwrap_or("unknown".into()); - (time + "-wayshot." + extension.into()).into() + format!("{time}-wayshot.{extension}").into() } diff --git a/wayshot/src/wayshot.rs b/wayshot/src/wayshot.rs index b5632caf..37152a79 100644 --- a/wayshot/src/wayshot.rs +++ b/wayshot/src/wayshot.rs @@ -1,10 +1,10 @@ use std::{ io::{stdout, BufWriter, Cursor, Write}, - process::{exit, Command}, + process::Command, }; use clap::Parser; -use eyre::Result; +use eyre::{bail, Result}; use libwayshot::{region::LogicalRegion, WayshotConnection}; mod cli; @@ -70,7 +70,7 @@ fn main() -> Result<()> { for output in valid_outputs { tracing::info!("{:#?}", output.name); } - exit(1); + return Ok(()); } let image_buffer = if let Some(slurp_region) = cli.slurp { @@ -94,8 +94,7 @@ fn main() -> Result<()> { if let Some(output) = outputs.iter().find(|output| output.name == output_name) { wayshot_conn.screenshot_single_output(output, cli.cursor)? } else { - tracing::error!("No output found!\n"); - exit(1); + bail!("No output found!"); } } else if cli.choose_output { let outputs = wayshot_conn.get_all_outputs(); @@ -106,8 +105,7 @@ fn main() -> Result<()> { if let Some(index) = select_ouput(&output_names) { wayshot_conn.screenshot_single_output(&outputs[index], cli.cursor)? } else { - tracing::error!("No output found!\n"); - exit(1); + bail!("No output found!"); } } else { wayshot_conn.screenshot_all(cli.cursor)? From 044910d6667582a4f53cdccf40c4a15f2bdd916c Mon Sep 17 00:00:00 2001 From: Andreas Backx Date: Fri, 2 Feb 2024 19:19:00 +0000 Subject: [PATCH 06/26] Make region, sizing, and positioning data structs reusable We use different structs/fields to store the same three types of data everywhere: - Position (x,y) - Size (width,height) - Region(position, size) This makes it so they all reuse the same information. --- libwayshot/src/dispatch.rs | 22 +++--- libwayshot/src/lib.rs | 92 +++++++++++++---------- libwayshot/src/output.rs | 28 +++---- libwayshot/src/region.rs | 140 ++++++++++++++++++----------------- libwayshot/src/screencopy.rs | 28 +++++-- wayshot/src/utils.rs | 23 +++--- 6 files changed, 183 insertions(+), 150 deletions(-) diff --git a/libwayshot/src/dispatch.rs b/libwayshot/src/dispatch.rs index 5352deb4..574fc813 100644 --- a/libwayshot/src/dispatch.rs +++ b/libwayshot/src/dispatch.rs @@ -26,7 +26,8 @@ use wayland_protocols_wlr::screencopy::v1::client::{ }; use crate::{ - output::{OutputInfo, OutputPositioning, WlOutputMode}, + output::OutputInfo, + region::{Position, Region, Size}, screencopy::FrameFormat, }; @@ -64,8 +65,7 @@ impl Dispatch for OutputCaptureState { description: String::new(), transform: wl_output::Transform::Normal, scale: 1, - dimensions: OutputPositioning::default(), - mode: WlOutputMode::default(), + region: Region::default(), }); } else { tracing::error!("Ignoring a wl_output with version < 4."); @@ -100,9 +100,7 @@ impl Dispatch for OutputCaptureState { wl_output::Event::Description { description } => { output.description = description; } - wl_output::Event::Mode { width, height, .. } => { - output.mode = WlOutputMode { width, height }; - } + wl_output::Event::Mode { .. } => {} wl_output::Event::Geometry { transform: WEnum::Value(transform), .. @@ -141,12 +139,13 @@ impl Dispatch for OutputCaptureState { match event { zxdg_output_v1::Event::LogicalPosition { x, y } => { - output_info.dimensions.x = x; - output_info.dimensions.y = y; + output_info.region.position = Position { x, y }; } zxdg_output_v1::Event::LogicalSize { width, height } => { - output_info.dimensions.width = width; - output_info.dimensions.height = height; + output_info.region.size = Size { + width: width as u32, + height: height as u32, + }; } zxdg_output_v1::Event::Done => {} zxdg_output_v1::Event::Name { .. } => {} @@ -191,8 +190,7 @@ impl Dispatch for CaptureFrameState { if let Value(f) = format { frame.formats.push(FrameFormat { format: f, - width, - height, + size: Size { width, height }, stride, }) } else { diff --git a/libwayshot/src/lib.rs b/libwayshot/src/lib.rs index 8198818c..beda48fe 100644 --- a/libwayshot/src/lib.rs +++ b/libwayshot/src/lib.rs @@ -29,7 +29,7 @@ use wayland_client::{ globals::{registry_queue_init, GlobalList}, protocol::{ wl_compositor::WlCompositor, - wl_output::WlOutput, + wl_output::{Transform, WlOutput}, wl_shm::{self, WlShm}, }, Connection, EventQueue, @@ -52,7 +52,7 @@ use crate::{ convert::create_converter, dispatch::{CaptureFrameState, FrameState, OutputCaptureState, WayshotState}, output::OutputInfo, - region::LogicalRegion, + region::{LogicalRegion, Region, Size}, screencopy::{create_shm_fd, FrameCopy, FrameFormat}, }; @@ -214,10 +214,10 @@ impl WayshotConnection { screencopy_manager.capture_output_region( cursor_overlay, output, - embedded_region.inner.x, - embedded_region.inner.y, - embedded_region.inner.width, - embedded_region.inner.height, + embedded_region.inner.position.x, + embedded_region.inner.position.y, + embedded_region.inner.size.width as i32, + embedded_region.inner.size.height as i32, &qh, (), ) @@ -275,16 +275,21 @@ impl WayshotConnection { // Connecting to wayland environment. let qh = event_queue.handle(); - // Bytes of data in the frame = stride * height. - let frame_bytes = frame_format.stride * frame_format.height; - // Instantiate shm global. let shm = self.globals.bind::(&qh, 1..=1, ())?; - let shm_pool = shm.create_pool(fd.as_fd(), frame_bytes as i32, &qh, ()); + let shm_pool = shm.create_pool( + fd.as_fd(), + frame_format + .byte_size() + .try_into() + .map_err(|_| Error::BufferTooSmall)?, + &qh, + (), + ); let buffer = shm_pool.create_buffer( 0, - frame_format.width as i32, - frame_format.height as i32, + frame_format.size.width as i32, + frame_format.size.height as i32, frame_format.stride as i32, frame_format.format, &qh, @@ -322,9 +327,7 @@ impl WayshotConnection { let (state, event_queue, frame, frame_format) = self.capture_output_frame_get_state(cursor_overlay as i32, output, capture_region)?; - // Bytes of data in the frame = stride * height. - let frame_bytes = frame_format.stride * frame_format.height; - file.set_len(frame_bytes as u64)?; + file.set_len(frame_format.byte_size())?; let frame_guard = self.capture_output_frame_inner(state, event_queue, frame, frame_format, file)?; @@ -333,7 +336,7 @@ impl WayshotConnection { } /// Get a FrameCopy instance with screenshot pixel data for any wl_output object. - #[tracing::instrument(skip_all, fields(output = output_info.name, region = capture_region.map(|r| format!("{:}", r))))] + #[tracing::instrument(skip_all, fields(output = format!("{output_info}"), region = capture_region.map(|r| format!("{:}", r))))] fn capture_frame_copy( &self, cursor_overlay: bool, @@ -361,22 +364,30 @@ impl WayshotConnection { tracing::error!("You can send a feature request for the above format to the mailing list for wayshot over at https://sr.ht/~shinyzenith/wayshot."); return Err(Error::NoSupportedBufferFormat); }; + let rotated_size = match output_info.transform { + Transform::_90 | Transform::_270 | Transform::Flipped90 | Transform::Flipped270 => { + Size { + width: frame_format.size.height, + height: frame_format.size.width, + } + } + _ => frame_format.size, + }; let frame_copy = FrameCopy { frame_format, frame_color_type, frame_mmap, transform: output_info.transform, - position: capture_region - .map(|capture_region| { - let logical_region = capture_region.logical(); - (logical_region.inner.x as i64, logical_region.inner.y as i64) - }) - .unwrap_or_else(|| { - ( - output_info.dimensions.x as i64, - output_info.dimensions.y as i64, - ) - }), + region: LogicalRegion { + inner: Region { + position: capture_region + .map(|capture_region: EmbeddedRegion| { + capture_region.logical().inner.position + }) + .unwrap_or_else(|| output_info.region.position), + size: rotated_size, + }, + }, }; tracing::debug!("Created frame copy: {:#?}", frame_copy); Ok((frame_copy, frame_guard)) @@ -451,7 +462,7 @@ impl WayshotConnection { tracing::span!( tracing::Level::DEBUG, "overlay_frames::surface", - output = output_info.name.as_str() + output = format!("{output_info}") ) .in_scope(|| -> Result<()> { let surface = compositor.create_surface(&qh, ()); @@ -468,8 +479,8 @@ impl WayshotConnection { layer_surface.set_exclusive_zone(-1); layer_surface.set_anchor(Anchor::Top | Anchor::Left); layer_surface.set_size( - frame_copy.frame_format.width, - frame_copy.frame_format.height, + frame_copy.frame_format.size.width, + frame_copy.frame_format.size.height, ); debug!("Committing surface creation changes."); @@ -515,8 +526,8 @@ impl WayshotConnection { tracing::Level::DEBUG, "filter_map", output = format!( - "{name} at {region}", - name = output_info.name, + "{output_info} at {region}", + output_info = format!("{output_info}"), region = LogicalRegion::from(output_info), ), capture_region = format!("{}", capture_region), @@ -561,8 +572,8 @@ impl WayshotConnection { image_util::rotate_image_buffer( image, frame_copy.transform, - frame_copy.frame_format.width, - frame_copy.frame_format.height, + frame_copy.frame_format.size.width, + frame_copy.frame_format.size.height, ), frame_copy, )) @@ -580,23 +591,24 @@ impl WayshotConnection { // Default to a transparent image. let composite_image = composite_image.unwrap_or_else(|| { Ok(DynamicImage::new_rgba8( - capture_region.inner.width as u32, - capture_region.inner.height as u32, + capture_region.inner.size.width, + capture_region.inner.size.height, )) }); Some(|| -> Result<_> { let mut composite_image = composite_image?; let (image, frame_copy) = image?; - let frame_copy_region = LogicalRegion::from(&frame_copy); let (x, y) = ( - frame_copy_region.inner.x as i64 - capture_region.inner.x as i64, - frame_copy_region.inner.y as i64 - capture_region.inner.y as i64, + frame_copy.region.inner.position.x as i64 + - capture_region.inner.position.x as i64, + frame_copy.region.inner.position.y as i64 + - capture_region.inner.position.y as i64, ); tracing::span!( tracing::Level::DEBUG, "replace", - frame_copy_region = format!("{}", frame_copy_region), + frame_copy_region = format!("{}", frame_copy.region), capture_region = format!("{}", capture_region), x = x, y = y, diff --git a/libwayshot/src/output.rs b/libwayshot/src/output.rs index 986f74df..5449f4f7 100644 --- a/libwayshot/src/output.rs +++ b/libwayshot/src/output.rs @@ -1,5 +1,9 @@ +use std::fmt::Display; + use wayland_client::protocol::{wl_output, wl_output::WlOutput}; +use crate::region::Region; + /// Represents an accessible wayland output. /// /// Do not instantiate, instead use [`crate::WayshotConnection::get_all_outputs`]. @@ -10,20 +14,16 @@ pub struct OutputInfo { pub description: String, pub transform: wl_output::Transform, pub scale: i32, - pub dimensions: OutputPositioning, - pub mode: WlOutputMode, -} - -#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] -pub struct WlOutputMode { - pub width: i32, - pub height: i32, + pub region: Region, } -#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] -pub struct OutputPositioning { - pub x: i32, - pub y: i32, - pub width: i32, - pub height: i32, +impl Display for OutputInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{name} ({description})", + name = self.name, + description = self.description + ) + } } diff --git a/libwayshot/src/region.rs b/libwayshot/src/region.rs index 27a35693..bbd81d39 100644 --- a/libwayshot/src/region.rs +++ b/libwayshot/src/region.rs @@ -1,10 +1,7 @@ use std::cmp; -use wayland_client::protocol::wl_output::Transform; - use crate::error::{Error, Result}; use crate::output::OutputInfo; -use crate::screencopy::FrameCopy; /// Ways to say how a region for a screenshot should be captured. pub enum RegionCapturer { @@ -58,16 +55,28 @@ pub struct EmbeddedRegion { /// /// Use `LogicalRegion` or `EmbeddedRegion` instead as they convey the /// coordinate system used. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] pub struct Region { - /// X coordinate of the area to capture. + /// Position of the region. + pub position: Position, + /// Size of the region. + pub size: Size, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] +pub struct Position { + /// X coordinate. pub x: i32, - /// y coordinate of the area to capture. + /// Y coordinate. pub y: i32, - /// Width of the capture area. - pub width: i32, - /// Height of the capture area. - pub height: i32, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] +pub struct Size { + /// Width. + pub width: u32, + /// Height. + pub height: u32, } impl EmbeddedRegion { @@ -78,30 +87,36 @@ impl EmbeddedRegion { /// See `EmbeddedRegion` for an example ASCII visualisation. #[tracing::instrument(ret, level = "debug")] pub fn new(viewport: LogicalRegion, relative_to: LogicalRegion) -> Option { - let x_relative: i32 = viewport.inner.x - relative_to.inner.x; - let y_relative = viewport.inner.y - relative_to.inner.y; + let x_relative: i32 = viewport.inner.position.x - relative_to.inner.position.x; + let y_relative = viewport.inner.position.y - relative_to.inner.position.y; let x1 = cmp::max(x_relative, 0); - let x2 = cmp::min(x_relative + viewport.inner.width, relative_to.inner.width); - let width = x2 - x1; - if width <= 0 { + let x2 = cmp::min( + x_relative + viewport.inner.size.width as i32, + relative_to.inner.size.width as i32, + ); + let width = if let Ok(width) = (x2 - x1).try_into() { + width + } else { return None; - } + }; let y1 = cmp::max(y_relative, 0); - let y2 = cmp::min(y_relative + viewport.inner.height, relative_to.inner.height); - let height = y2 - y1; - if height <= 0 { + let y2 = cmp::min( + y_relative + viewport.inner.size.height as i32, + relative_to.inner.size.height as i32, + ); + let height = if let Ok(height) = (y2 - y1).try_into() { + height + } else { return None; - } + }; Some(Self { relative_to: relative_to, inner: Region { - x: x1, - y: y1, - width, - height, + position: Position { x: x1, y: y1 }, + size: Size { width, height }, }, }) } @@ -114,10 +129,11 @@ impl EmbeddedRegion { pub fn logical(&self) -> LogicalRegion { LogicalRegion { inner: Region { - x: self.relative_to.inner.x as i32 + self.inner.x, - y: self.relative_to.inner.y as i32 + self.inner.y, - width: self.inner.width, - height: self.inner.height, + position: Position { + x: self.relative_to.inner.position.x + self.inner.position.x, + y: self.relative_to.inner.position.y + self.inner.position.y, + }, + size: self.inner.size, }, } } @@ -134,19 +150,34 @@ impl std::fmt::Display for EmbeddedRegion { } } -impl std::fmt::Display for Region { +impl std::fmt::Display for Position { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "({x}, {y})", x = self.x, y = self.y,) + } +} + +impl std::fmt::Display for Size { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "({x}, {y}) ({width}x{height})", - x = self.x, - y = self.y, + "({width}x{height})", width = self.width, height = self.height, ) } } +impl std::fmt::Display for Region { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "({position}) ({size})", + position = self.position, + size = self.size, + ) + } +} + impl std::fmt::Display for LogicalRegion { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{inner}", inner = self.inner) @@ -156,12 +187,7 @@ impl std::fmt::Display for LogicalRegion { impl From<&OutputInfo> for LogicalRegion { fn from(output_info: &OutputInfo) -> Self { LogicalRegion { - inner: Region { - x: output_info.dimensions.x, - y: output_info.dimensions.y, - width: output_info.dimensions.width, - height: output_info.dimensions.height, - }, + inner: output_info.region, } } } @@ -172,52 +198,32 @@ impl TryFrom<&Vec> for LogicalRegion { fn try_from(output_info: &Vec) -> std::result::Result { let x1 = output_info .iter() - .map(|output| output.dimensions.x) + .map(|output| output.region.position.x) .min() .ok_or(Error::NoOutputs)?; let y1 = output_info .iter() - .map(|output| output.dimensions.y) + .map(|output| output.region.position.y) .min() .ok_or(Error::NoOutputs)?; let x2 = output_info .iter() - .map(|output| output.dimensions.x + output.dimensions.width) + .map(|output| output.region.position.x + output.region.size.width as i32) .max() .ok_or(Error::NoOutputs)?; let y2 = output_info .iter() - .map(|output| output.dimensions.y + output.dimensions.height) + .map(|output| output.region.position.y + output.region.size.height as i32) .max() .ok_or(Error::NoOutputs)?; Ok(LogicalRegion { inner: Region { - x: x1, - y: y1, - width: x2 - x1, - height: y2 - y1, + position: Position { x: x1, y: y1 }, + size: Size { + width: (x2 - x1) as u32, + height: (y2 - y1) as u32, + }, }, }) } } - -impl From<&FrameCopy> for LogicalRegion { - fn from(frame_copy: &FrameCopy) -> Self { - let (width, height) = ( - frame_copy.frame_format.width as i32, - frame_copy.frame_format.height as i32, - ); - let is_portait = match frame_copy.transform { - Transform::_90 | Transform::_270 | Transform::Flipped90 | Transform::Flipped270 => true, - _ => false, - }; - LogicalRegion { - inner: Region { - x: frame_copy.position.0 as i32, - y: frame_copy.position.1 as i32, - width: if is_portait { height } else { width }, - height: if is_portait { width } else { height }, - }, - } - } -} diff --git a/libwayshot/src/screencopy.rs b/libwayshot/src/screencopy.rs index 8d51b2ce..734b6bfd 100644 --- a/libwayshot/src/screencopy.rs +++ b/libwayshot/src/screencopy.rs @@ -15,7 +15,10 @@ use wayland_client::protocol::{ wl_buffer::WlBuffer, wl_output, wl_shm::Format, wl_shm_pool::WlShmPool, }; -use crate::{Error, Result}; +use crate::{ + region::{LogicalRegion, Size}, + Error, Result, +}; pub struct FrameGuard { pub buffer: WlBuffer, @@ -36,12 +39,20 @@ impl Drop for FrameGuard { #[derive(Debug, Copy, Clone, PartialEq)] pub struct FrameFormat { pub format: Format, - pub width: u32, - pub height: u32, + /// Size of the frame in pixels. This will always be in "landscape" so a + /// portrait 1080x1920 frame will be 1920x1080 and will need to be rotated! + pub size: Size, /// Stride is the number of bytes between the start of a row and the start of the next row. pub stride: u32, } +impl FrameFormat { + /// Returns the size of the frame in bytes, which is the stride * height. + pub fn byte_size(&self) -> u64 { + self.stride as u64 * self.size.height as u64 + } +} + #[tracing::instrument(skip(frame_mmap))] fn create_image_buffer

( frame_format: &FrameFormat, @@ -51,8 +62,12 @@ where P: Pixel, { tracing::debug!("Creating image buffer"); - ImageBuffer::from_vec(frame_format.width, frame_format.height, frame_mmap.to_vec()) - .ok_or(Error::BufferTooSmall) + ImageBuffer::from_vec( + frame_format.size.width, + frame_format.size.height, + frame_mmap.to_vec(), + ) + .ok_or(Error::BufferTooSmall) } /// The copied frame comprising of the FrameFormat, ColorType (Rgba8), and a memory backed shm @@ -63,7 +78,8 @@ pub struct FrameCopy { pub frame_color_type: ColorType, pub frame_mmap: MmapMut, pub transform: wl_output::Transform, - pub position: (i64, i64), + /// Logical region with the transform already applied. + pub region: LogicalRegion, } impl TryFrom<&FrameCopy> for DynamicImage { diff --git a/wayshot/src/utils.rs b/wayshot/src/utils.rs index bb9a7a96..117c1f0c 100644 --- a/wayshot/src/utils.rs +++ b/wayshot/src/utils.rs @@ -8,14 +8,14 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -use libwayshot::region::{LogicalRegion, Region}; +use libwayshot::region::{LogicalRegion, Position, Region, Size}; pub fn parse_geometry(g: &str) -> Result { let tail = g.trim(); let x_coordinate: i32; let y_coordinate: i32; - let width: i32; - let height: i32; + let width: u32; + let height: u32; let validation_error = "Invalid geometry provided.\nValid geometries:\n1) %d,%d %dx%d\n2) %d %d %d %d"; @@ -27,8 +27,8 @@ pub fn parse_geometry(g: &str) -> Result { let (head, tail) = tail.split_once(' ').wrap_err(validation_error)?; y_coordinate = head.parse::()?; let (head, tail) = tail.split_once('x').wrap_err(validation_error)?; - width = head.parse::()?; - height = tail.parse::()?; + width = head.parse::()?; + height = tail.parse::()?; } else { // this accepts: "%d %d %d %d" let (head, tail) = tail.split_once(' ').wrap_err(validation_error)?; @@ -36,16 +36,17 @@ pub fn parse_geometry(g: &str) -> Result { let (head, tail) = tail.split_once(' ').wrap_err(validation_error)?; y_coordinate = head.parse::()?; let (head, tail) = tail.split_once(' ').wrap_err(validation_error)?; - width = head.parse::()?; - height = tail.parse::()?; + width = head.parse::()?; + height = tail.parse::()?; } Ok(LogicalRegion { inner: Region { - x: x_coordinate, - y: y_coordinate, - width, - height, + position: Position { + x: x_coordinate, + y: y_coordinate, + }, + size: Size { width, height }, }, }) } From 8f22e6da9005f7495d38d89f2430a2649c4499ef Mon Sep 17 00:00:00 2001 From: Andreas Backx Date: Wed, 21 Feb 2024 01:46:03 +0000 Subject: [PATCH 07/26] First version of scaling fix. (#85) --- libwayshot/src/dispatch.rs | 21 ++++++++------- libwayshot/src/image_util.rs | 33 +++++++++++++++-------- libwayshot/src/lib.rs | 51 ++++++++++++++++++++---------------- libwayshot/src/output.rs | 12 ++++++--- libwayshot/src/region.rs | 27 ++++++++++++------- libwayshot/src/screencopy.rs | 3 ++- 6 files changed, 92 insertions(+), 55 deletions(-) diff --git a/libwayshot/src/dispatch.rs b/libwayshot/src/dispatch.rs index 574fc813..c8f21fcf 100644 --- a/libwayshot/src/dispatch.rs +++ b/libwayshot/src/dispatch.rs @@ -27,7 +27,7 @@ use wayland_protocols_wlr::screencopy::v1::client::{ use crate::{ output::OutputInfo, - region::{Position, Region, Size}, + region::{LogicalRegion, Position, Size}, screencopy::FrameFormat, }; @@ -64,8 +64,8 @@ impl Dispatch for OutputCaptureState { name: "".to_string(), description: String::new(), transform: wl_output::Transform::Normal, - scale: 1, - region: Region::default(), + physical_size: Size::default(), + logical_region: LogicalRegion::default(), }); } else { tracing::error!("Ignoring a wl_output with version < 4."); @@ -100,16 +100,19 @@ impl Dispatch for OutputCaptureState { wl_output::Event::Description { description } => { output.description = description; } - wl_output::Event::Mode { .. } => {} + wl_output::Event::Mode { width, height, .. } => { + output.physical_size = Size { + width: width as u32, + height: height as u32, + }; + } wl_output::Event::Geometry { transform: WEnum::Value(transform), .. } => { output.transform = transform; } - wl_output::Event::Scale { factor } => { - output.scale = factor; - } + wl_output::Event::Scale { .. } => {} wl_output::Event::Done => {} _ => {} } @@ -139,10 +142,10 @@ impl Dispatch for OutputCaptureState { match event { zxdg_output_v1::Event::LogicalPosition { x, y } => { - output_info.region.position = Position { x, y }; + output_info.logical_region.inner.position = Position { x, y }; } zxdg_output_v1::Event::LogicalSize { width, height } => { - output_info.region.size = Size { + output_info.logical_region.inner.size = Size { width: width as u32, height: height as u32, }; diff --git a/libwayshot/src/image_util.rs b/libwayshot/src/image_util.rs index 197e8f11..e06af24d 100644 --- a/libwayshot/src/image_util.rs +++ b/libwayshot/src/image_util.rs @@ -1,21 +1,24 @@ use image::{DynamicImage, GenericImageView}; use wayland_client::protocol::wl_output::Transform; +use crate::region::Size; + +#[tracing::instrument(skip(image))] pub(crate) fn rotate_image_buffer( image: DynamicImage, transform: Transform, - width: u32, - height: u32, + logical_size: Size, + max_scale: f64, ) -> DynamicImage { // TODO Better document whether width and height are before or after the transform. // Perhaps this should be part of a cleanup of the FrameCopy struct. - let (width, height) = match transform { + let (logical_width, logical_height) = match transform { Transform::_90 | Transform::_270 | Transform::Flipped90 | Transform::Flipped270 => { - (height, width) + (logical_size.height, logical_size.width) } - _ => (width, height), + _ => (logical_size.width, logical_size.height), }; - let final_image = match transform { + let rotated_image = match transform { Transform::_90 => image::imageops::rotate90(&image).into(), Transform::_180 => image::imageops::rotate180(&image).into(), Transform::_270 => image::imageops::rotate270(&image).into(), @@ -35,14 +38,22 @@ pub(crate) fn rotate_image_buffer( _ => image, }; - if final_image.dimensions() == (width, height) { - return final_image; + let scale = rotated_image.width() as f64 / logical_width as f64; + // The amount of scaling left to perform. + let scaling_left = max_scale / scale; + if scaling_left <= 1.0 { + tracing::debug!("No scaling left to do"); + return rotated_image; } + tracing::debug!("Scaling left to do: {scaling_left}"); + let new_width = (rotated_image.width() as f64 * scaling_left).round() as u32; + let new_height = (rotated_image.height() as f64 * scaling_left).round() as u32; + tracing::debug!("Resizing image to {new_width}x{new_height}"); image::imageops::resize( - &final_image, - width, - height, + &rotated_image, + new_width, + new_height, image::imageops::FilterType::Gaussian, ) .into() diff --git a/libwayshot/src/lib.rs b/libwayshot/src/lib.rs index beda48fe..3938112a 100644 --- a/libwayshot/src/lib.rs +++ b/libwayshot/src/lib.rs @@ -336,7 +336,7 @@ impl WayshotConnection { } /// Get a FrameCopy instance with screenshot pixel data for any wl_output object. - #[tracing::instrument(skip_all, fields(output = format!("{output_info}"), region = capture_region.map(|r| format!("{:}", r))))] + #[tracing::instrument(skip_all, fields(output = format!("{output_info}"), region = capture_region.map(|r| format!("{:}", r)).unwrap_or("fullscreen".to_string())))] fn capture_frame_copy( &self, cursor_overlay: bool, @@ -364,7 +364,7 @@ impl WayshotConnection { tracing::error!("You can send a feature request for the above format to the mailing list for wayshot over at https://sr.ht/~shinyzenith/wayshot."); return Err(Error::NoSupportedBufferFormat); }; - let rotated_size = match output_info.transform { + let rotated_physical_size = match output_info.transform { Transform::_90 | Transform::_270 | Transform::Flipped90 | Transform::Flipped270 => { Size { width: frame_format.size.height, @@ -378,16 +378,10 @@ impl WayshotConnection { frame_color_type, frame_mmap, transform: output_info.transform, - region: LogicalRegion { - inner: Region { - position: capture_region - .map(|capture_region: EmbeddedRegion| { - capture_region.logical().inner.position - }) - .unwrap_or_else(|| output_info.region.position), - size: rotated_size, - }, - }, + logical_region: capture_region + .map(|capture_region| capture_region.logical()) + .unwrap_or(output_info.logical_region), + physical_size: rotated_physical_size, }; tracing::debug!("Created frame copy: {:#?}", frame_copy); Ok((frame_copy, frame_guard)) @@ -492,7 +486,7 @@ impl WayshotConnection { } surface.set_buffer_transform(output_info.transform); - surface.set_buffer_scale(output_info.scale); + // surface.set_buffer_scale(output_info.scale()); surface.attach(Some(&frame_guard.buffer), 0, 0); debug!("Committing surface with attached buffer."); @@ -507,6 +501,7 @@ impl WayshotConnection { } /// Take a screenshot from the specified region. + #[tracing::instrument(skip_all, fields(max_scale = tracing::field::Empty))] fn screenshot_region_capturer( &self, region_capturer: RegionCapturer, @@ -562,7 +557,17 @@ impl WayshotConnection { } }; + // TODO When freeze was used, we can still further remove the outputs + // that don't intersect with the capture region. + thread::scope(|scope| { + let max_scale = outputs_capture_regions + .iter() + .map(|(output_info, _)| output_info.scale()) + .fold(1.0, f64::max); + + tracing::Span::current().record("max_scale", &max_scale); + let rotate_join_handles = frames .into_iter() .map(|(frame_copy, _, _)| { @@ -572,8 +577,8 @@ impl WayshotConnection { image_util::rotate_image_buffer( image, frame_copy.transform, - frame_copy.frame_format.size.width, - frame_copy.frame_format.size.height, + frame_copy.logical_region.inner.size, + max_scale, ), frame_copy, )) @@ -591,8 +596,8 @@ impl WayshotConnection { // Default to a transparent image. let composite_image = composite_image.unwrap_or_else(|| { Ok(DynamicImage::new_rgba8( - capture_region.inner.size.width, - capture_region.inner.size.height, + (capture_region.inner.size.width as f64 * max_scale) as u32, + (capture_region.inner.size.height as f64 * max_scale) as u32, )) }); @@ -600,15 +605,17 @@ impl WayshotConnection { let mut composite_image = composite_image?; let (image, frame_copy) = image?; let (x, y) = ( - frame_copy.region.inner.position.x as i64 - - capture_region.inner.position.x as i64, - frame_copy.region.inner.position.y as i64 - - capture_region.inner.position.y as i64, + ((frame_copy.logical_region.inner.position.x as f64 + - capture_region.inner.position.x as f64) + * max_scale) as i64, + ((frame_copy.logical_region.inner.position.y as f64 + - capture_region.inner.position.y as f64) + * max_scale) as i64, ); tracing::span!( tracing::Level::DEBUG, "replace", - frame_copy_region = format!("{}", frame_copy.region), + frame_copy_region = format!("{}", frame_copy.logical_region), capture_region = format!("{}", capture_region), x = x, y = y, diff --git a/libwayshot/src/output.rs b/libwayshot/src/output.rs index 5449f4f7..03d0a725 100644 --- a/libwayshot/src/output.rs +++ b/libwayshot/src/output.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use wayland_client::protocol::{wl_output, wl_output::WlOutput}; -use crate::region::Region; +use crate::region::{LogicalRegion, Size}; /// Represents an accessible wayland output. /// @@ -13,8 +13,8 @@ pub struct OutputInfo { pub name: String, pub description: String, pub transform: wl_output::Transform, - pub scale: i32, - pub region: Region, + pub physical_size: Size, + pub logical_region: LogicalRegion, } impl Display for OutputInfo { @@ -27,3 +27,9 @@ impl Display for OutputInfo { ) } } + +impl OutputInfo { + pub(crate) fn scale(&self) -> f64 { + self.physical_size.height as f64 / self.logical_region.inner.size.height as f64 + } +} diff --git a/libwayshot/src/region.rs b/libwayshot/src/region.rs index bbd81d39..394e1fb3 100644 --- a/libwayshot/src/region.rs +++ b/libwayshot/src/region.rs @@ -17,8 +17,9 @@ pub enum RegionCapturer { /// `Region` where the coordinate system is the logical coordinate system used /// in Wayland to position outputs. Top left is (0, 0) and any transforms and -/// scaling have been applied. -#[derive(Debug, Copy, Clone)] +/// scaling have been applied. A unit is a logical pixel, meaning that this is +/// after scaling has been applied. +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] pub struct LogicalRegion { pub inner: Region, } @@ -27,8 +28,9 @@ pub struct LogicalRegion { /// /// It can only be contained inside of another and cannot exceed its bounds. /// -/// Example of what +/// Example: /// +/// ```` /// ┌─────────────┐ /// │ │ /// │ ┌──────────┼──────┐ @@ -44,6 +46,7 @@ pub struct LogicalRegion { /// │ │ /// │ Screen 1 │ /// └─────────────┘ +/// ```` #[derive(Debug, Copy, Clone)] pub struct EmbeddedRegion { /// The coordinate sysd @@ -171,7 +174,7 @@ impl std::fmt::Display for Region { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "({position}) ({size})", + "{position} {size}", position = self.position, size = self.size, ) @@ -187,7 +190,7 @@ impl std::fmt::Display for LogicalRegion { impl From<&OutputInfo> for LogicalRegion { fn from(output_info: &OutputInfo) -> Self { LogicalRegion { - inner: output_info.region, + inner: output_info.logical_region.inner, } } } @@ -198,22 +201,28 @@ impl TryFrom<&Vec> for LogicalRegion { fn try_from(output_info: &Vec) -> std::result::Result { let x1 = output_info .iter() - .map(|output| output.region.position.x) + .map(|output| output.logical_region.inner.position.x) .min() .ok_or(Error::NoOutputs)?; let y1 = output_info .iter() - .map(|output| output.region.position.y) + .map(|output| output.logical_region.inner.position.y) .min() .ok_or(Error::NoOutputs)?; let x2 = output_info .iter() - .map(|output| output.region.position.x + output.region.size.width as i32) + .map(|output| { + output.logical_region.inner.position.x + + output.logical_region.inner.size.width as i32 + }) .max() .ok_or(Error::NoOutputs)?; let y2 = output_info .iter() - .map(|output| output.region.position.y + output.region.size.height as i32) + .map(|output| { + output.logical_region.inner.position.y + + output.logical_region.inner.size.height as i32 + }) .max() .ok_or(Error::NoOutputs)?; Ok(LogicalRegion { diff --git a/libwayshot/src/screencopy.rs b/libwayshot/src/screencopy.rs index 734b6bfd..8b01c7c6 100644 --- a/libwayshot/src/screencopy.rs +++ b/libwayshot/src/screencopy.rs @@ -79,7 +79,8 @@ pub struct FrameCopy { pub frame_mmap: MmapMut, pub transform: wl_output::Transform, /// Logical region with the transform already applied. - pub region: LogicalRegion, + pub logical_region: LogicalRegion, + pub physical_size: Size, } impl TryFrom<&FrameCopy> for DynamicImage { From 9e7919e56021c1ef53057e697f9e45ac80c6210b Mon Sep 17 00:00:00 2001 From: Shivang K Raghuvanshi <136506971+shivkr6@users.noreply.github.com> Date: Sat, 23 Mar 2024 17:07:02 +0530 Subject: [PATCH 08/26] chore: remove useless roundtrip (#105) --- libwayshot/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/libwayshot/src/lib.rs b/libwayshot/src/lib.rs index 3938112a..e2d430a3 100644 --- a/libwayshot/src/lib.rs +++ b/libwayshot/src/lib.rs @@ -129,7 +129,6 @@ impl WayshotConnection { // Fetch all outputs; when their names arrive, add them to the list let _ = self.conn.display().get_registry(&qh, ()); event_queue.roundtrip(&mut state)?; - event_queue.roundtrip(&mut state)?; // We loop over each output and request its position data. let xdg_outputs: Vec = state From b9219b14526fafb8cb05a93ea23050d6672a806f Mon Sep 17 00:00:00 2001 From: Sooraj S <94284954+CheerfulPianissimo@users.noreply.github.com> Date: Sat, 23 Mar 2024 20:48:44 +0530 Subject: [PATCH 09/26] feat(clipboard): implement clipboard integration (#91) * feat(clipboard): implement clipboard integration Add the --clipboard flag and implement functionality to make image available on the clipboard using wl-clipboard-rs. * style(format): apply code formatting to cli.rs * feat(clipboard): implement fork wait for clipboard Add functionality offering image on clipboard persistently in the background * feat(clipboard): inform user about wayshot persisting in background with --clipboard * feat(clipboard): use tracing::warn instead of print if fork fails * feat(clipboard): Switch from the fork crate to the nix crate for daemonization * style(format): code formatting to wayshot.rs * style(typo): corrected a typo in the comments --- Cargo.lock | 169 +++++++++++++++++++++++++++++++++++++++-- wayshot/Cargo.toml | 3 + wayshot/src/cli.rs | 6 +- wayshot/src/wayshot.rs | 68 +++++++++++++---- 4 files changed, 225 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9244fa4d..ac5fa153 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,6 +74,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "bytecount" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" + [[package]] name = "bytemuck" version = "1.14.0" @@ -101,6 +107,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "clap" version = "4.4.18" @@ -132,7 +144,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.41", ] [[package]] @@ -175,6 +187,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "derive-new" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "dialoguer" version = "0.11.0" @@ -210,6 +233,12 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.8" @@ -245,6 +274,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.28" @@ -255,6 +290,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "fuzzy-matcher" version = "0.3.7" @@ -264,6 +305,12 @@ dependencies = [ "thread_local", ] +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "heck" version = "0.4.1" @@ -292,6 +339,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +[[package]] +name = "indexmap" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "jpeg-decoder" version = "0.3.0" @@ -306,9 +363,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.151" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libloading" @@ -370,6 +427,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -390,6 +453,7 @@ dependencies = [ "cfg-if", "libc", "memoffset", + "pin-utils", ] [[package]] @@ -403,6 +467,28 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -449,18 +535,44 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "os_pipe" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "pin-project-lite" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.27" @@ -577,6 +689,17 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.41" @@ -618,7 +741,7 @@ checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.41", ] [[package]] @@ -650,7 +773,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.41", ] [[package]] @@ -688,6 +811,20 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tree_magic_mini" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91adfd0607cacf6e4babdb870e9bec4037c1c4b151cfd279ccefc5e0c7feaa6d" +dependencies = [ + "bytecount", + "fnv", + "lazy_static", + "nom", + "once_cell", + "petgraph", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -795,8 +932,10 @@ dependencies = [ "flate2", "image", "libwayshot", + "nix 0.28.0", "tracing", "tracing-subscriber", + "wl-clipboard-rs", ] [[package]] @@ -1019,6 +1158,26 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "wl-clipboard-rs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57af79e973eadf08627115c73847392e6b766856ab8e3844a59245354b23d2fa" +dependencies = [ + "derive-new", + "libc", + "log", + "nix 0.26.4", + "os_pipe", + "tempfile", + "thiserror", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "zeroize" version = "1.7.0" diff --git a/wayshot/Cargo.toml b/wayshot/Cargo.toml index 4594d0b4..9364ff88 100644 --- a/wayshot/Cargo.toml +++ b/wayshot/Cargo.toml @@ -33,6 +33,9 @@ image = { version = "0.24", default-features = false, features = [ dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } eyre = "0.6.8" +wl-clipboard-rs = "0.8.0" +nix = { version = "0.28.0", features = ["process"] } + [[bin]] name = "wayshot" path = "src/wayshot.rs" diff --git a/wayshot/src/cli.rs b/wayshot/src/cli.rs index 418ac8d8..71083fac 100644 --- a/wayshot/src/cli.rs +++ b/wayshot/src/cli.rs @@ -11,10 +11,14 @@ use clap::builder::TypedValueParser; #[derive(Parser)] #[command(version, about)] pub struct Cli { - /// Where to save the screenshot, "-" for stdout. Defaults to "$UNIX_TIMESTAMP-wayshot.$EXTENSION". + /// Where to save the screenshot, "-" for stdout. Defaults to "$UNIX_TIMESTAMP-wayshot.$EXTENSION" unless --clipboard is present. #[arg(value_name = "OUTPUT")] pub file: Option, + /// Copy image to clipboard along with [OUTPUT] or stdout. wayshot persists in the background to offer the image till the clipboard is overwritten. + #[arg(long)] + pub clipboard: bool, + /// Log level to be used for printing to stderr #[arg(long, default_value = "info", value_parser = clap::builder::PossibleValuesParser::new(["trace", "debug", "info", "warn", "error"]).map(|s| -> tracing::Level{ s.parse().wrap_err_with(|| format!("Failed to parse log level: {}", s)).unwrap()}))] pub log_level: tracing::Level, diff --git a/wayshot/src/wayshot.rs b/wayshot/src/wayshot.rs index 37152a79..a1c8ac0d 100644 --- a/wayshot/src/wayshot.rs +++ b/wayshot/src/wayshot.rs @@ -13,6 +13,10 @@ mod utils; use dialoguer::{theme::ColorfulTheme, FuzzySelect}; use utils::EncodingFormat; +use wl_clipboard_rs::copy::{MimeType, Options, Source}; + +use nix::unistd::{fork, ForkResult}; + fn select_ouput(ouputs: &[T]) -> Option where T: ToString, @@ -52,17 +56,6 @@ fn main() -> Result<()> { } } - let file = match cli.file { - Some(pathbuf) => { - if pathbuf.to_string_lossy() == "-" { - None - } else { - Some(pathbuf) - } - } - None => Some(utils::get_default_file_name(requested_encoding)), - }; - let wayshot_conn = WayshotConnection::new()?; if cli.list_outputs { @@ -111,16 +104,61 @@ fn main() -> Result<()> { wayshot_conn.screenshot_all(cli.cursor)? }; + let mut stdout_print = false; + let file = match cli.file { + Some(pathbuf) => { + if pathbuf.to_string_lossy() == "-" { + stdout_print = true; + None + } else { + Some(pathbuf) + } + } + None => { + if cli.clipboard { + None + } else { + Some(utils::get_default_file_name(requested_encoding)) + } + } + }; + if let Some(file) = file { image_buffer.save(file)?; } else { - let stdout = stdout(); let mut buffer = Cursor::new(Vec::new()); - - let mut writer = BufWriter::new(stdout.lock()); image_buffer.write_to(&mut buffer, requested_encoding)?; - writer.write_all(buffer.get_ref())?; + if stdout_print { + let stdout = stdout(); + let mut writer = BufWriter::new(stdout.lock()); + writer.write_all(buffer.get_ref())?; + } + if cli.clipboard { + let mut opts = Options::new(); + match unsafe { fork() } { + // Having the image persistently available on the clipboard requires a wayshot process to be alive. + // Fork the process with a child detached from the main process and have the parent exit + Ok(ForkResult::Parent { .. }) => { + return Ok(()); + } + Ok(ForkResult::Child) => { + opts.foreground(true); // Offer the image till something else is available on the clipboard + opts.copy( + Source::Bytes(buffer.into_inner().into()), + MimeType::Autodetect, + )?; + } + Err(e) => { + tracing::warn!("Fork failed with error: {e}, couldn't offer image on the clipboard persistently. + Use a clipboard manager to record screenshot."); + opts.copy( + Source::Bytes(buffer.into_inner().into()), + MimeType::Autodetect, + )?; + } + } + } } Ok(()) From e43ab9f3c35dfd278707305bec4e05c16719856f Mon Sep 17 00:00:00 2001 From: Shinyzenith Date: Sat, 23 Mar 2024 20:56:21 +0530 Subject: [PATCH 10/26] docs(ManPage): Fix capitalization issue Signed-off-by: Shinyzenith --- wayshot/src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wayshot/src/cli.rs b/wayshot/src/cli.rs index 71083fac..c0508e9e 100644 --- a/wayshot/src/cli.rs +++ b/wayshot/src/cli.rs @@ -15,7 +15,7 @@ pub struct Cli { #[arg(value_name = "OUTPUT")] pub file: Option, - /// Copy image to clipboard along with [OUTPUT] or stdout. wayshot persists in the background to offer the image till the clipboard is overwritten. + /// Copy image to clipboard along with [OUTPUT] or stdout. Wayshot persists in the background to offer the image till the clipboard is overwritten. #[arg(long)] pub clipboard: bool, From 440bd2687a09cd95fed9788c90b8ff181bd1d99e Mon Sep 17 00:00:00 2001 From: Shinyzenith Date: Sat, 23 Mar 2024 20:57:48 +0530 Subject: [PATCH 11/26] docs: Document clipboard interaction Signed-off-by: Shinyzenith --- docs/wayshot.1.scd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/wayshot.1.scd b/docs/wayshot.1.scd index a71f50aa..1ab5762f 100644 --- a/docs/wayshot.1.scd +++ b/docs/wayshot.1.scd @@ -22,6 +22,9 @@ Wayshot - Screenshot tool for compositors implementing zwlr_screencopy_v1 such a *-c*, *--cursor* Enable cursor visibility in screenshots. +*--clipboard* + Copy image contents to clipboard. + *-e*, *--extension* Set the image encoder. Valid arguments: From f7f134f8b39e65c41e71969ac74608d93e09825f Mon Sep 17 00:00:00 2001 From: Shinyzenith Date: Sat, 23 Mar 2024 21:23:51 +0530 Subject: [PATCH 12/26] chore: Update Cargo.lock Signed-off-by: Shinyzenith --- Cargo.lock | 491 ++++++++++++++++------------------------------------- 1 file changed, 144 insertions(+), 347 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac5fa153..c5186d5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "anstream" -version = "0.6.11" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" dependencies = [ "anstyle", "anstyle-parse", @@ -24,9 +24,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "anstyle-parse" @@ -43,7 +43,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -53,7 +53,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -70,21 +70,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" - -[[package]] -name = "bytecount" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "bytemuck" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" [[package]] name = "byteorder" @@ -94,12 +88,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.0.83" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" [[package]] name = "cfg-if" @@ -115,9 +106,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "clap" -version = "4.4.18" +version = "4.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" dependencies = [ "clap_builder", "clap_derive", @@ -125,9 +116,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.18" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", @@ -137,21 +128,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.4.7" +version = "4.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.41", + "syn", ] [[package]] name = "clap_lex" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "color_quant" @@ -167,35 +158,35 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "console" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] [[package]] name = "derive-new" -version = "0.5.9" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" +checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] @@ -246,14 +237,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] name = "eyre" -version = "0.6.8" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" dependencies = [ "indenter", "once_cell", @@ -267,9 +258,9 @@ checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fdeflate" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" dependencies = [ "simd-adler32", ] @@ -313,21 +304,29 @@ checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys", +] [[package]] name = "image" -version = "0.24.7" +version = "0.24.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" dependencies = [ "bytemuck", "byteorder", "color_quant", "jpeg-decoder", - "num-rational", "num-traits", "png", "qoi", @@ -341,9 +340,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "2.2.3" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", @@ -351,9 +350,9 @@ dependencies = [ [[package]] name = "jpeg-decoder" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" [[package]] name = "lazy_static" @@ -369,12 +368,12 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libloading" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-sys 0.48.0", + "windows-targets", ] [[package]] @@ -393,40 +392,31 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memmap2" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a69c7c189ae418f83003da62820aca28d15a07725ce51fb924999335d622ff" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" dependencies = [ "libc", ] -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -435,34 +425,21 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", "simd-adler32", ] -[[package]] -name = "nix" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset", - "pin-utils", -] - [[package]] name = "nix" version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "cfg-if", "libc", ] @@ -473,7 +450,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "cfg-if", "cfg_aliases", "libc", @@ -499,32 +476,11 @@ dependencies = [ "winapi", ] -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] @@ -542,7 +498,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -567,23 +523,17 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "png" -version = "0.17.10" +version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -594,9 +544,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -612,42 +562,33 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "rustix" -version = "0.38.28" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -679,32 +620,21 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "1.0.109" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "syn" -version = "2.0.41" +version = "2.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" dependencies = [ "proc-macro2", "quote", @@ -713,42 +643,41 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.8.1" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", "rustix", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "thiserror" -version = "1.0.51" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.51" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn", ] [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", @@ -773,7 +702,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn", ] [[package]] @@ -813,13 +742,13 @@ dependencies = [ [[package]] name = "tree_magic_mini" -version = "3.0.3" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91adfd0607cacf6e4babdb870e9bec4037c1c4b151cfd279ccefc5e0c7feaa6d" +checksum = "77ee137597cdb361b55a4746983e4ac1b35ab6024396a419944ad473bb915265" dependencies = [ - "bytecount", "fnv", - "lazy_static", + "home", + "memchr", "nom", "once_cell", "petgraph", @@ -851,13 +780,13 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "wayland-backend" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19152ddd73f45f024ed4534d9ca2594e0ef252c1847695255dae47f34df9fbe4" +checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" dependencies = [ "cc", "downcast-rs", - "nix 0.26.4", + "rustix", "scoped-tls", "smallvec", "wayland-sys", @@ -865,23 +794,23 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.1" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca7d52347346f5473bf2f56705f360e8440873052e575e55890c4fa57843ed3" +checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" dependencies = [ - "bitflags 2.4.1", - "nix 0.26.4", + "bitflags 2.5.0", + "rustix", "wayland-backend", "wayland-scanner", ] [[package]] name = "wayland-protocols" -version = "0.31.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e253d7107ba913923dc253967f35e8561a3c65f914543e46843c88ddd729e21c" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -893,7 +822,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -902,9 +831,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb8e28403665c9f9513202b7e1ed71ec56fde5c107816843fb14057910b2c09c" +checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" dependencies = [ "proc-macro2", "quick-xml", @@ -960,214 +889,82 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-targets", ] [[package]] name = "windows-targets" -version = "0.48.5" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" -dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.2" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" -version = "0.48.5" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" -version = "0.42.2" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.5" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" -version = "0.42.2" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "wl-clipboard-rs" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57af79e973eadf08627115c73847392e6b766856ab8e3844a59245354b23d2fa" +checksum = "12b41773911497b18ca8553c3daaf8ec9fe9819caf93d451d3055f69de028adb" dependencies = [ "derive-new", "libc", "log", - "nix 0.26.4", + "nix 0.28.0", "os_pipe", "tempfile", "thiserror", From 5370c0885dba9ecdca3a7df539554f13115cdf40 Mon Sep 17 00:00:00 2001 From: rachancheet <55895940+rachancheet@users.noreply.github.com> Date: Sat, 23 Mar 2024 21:27:43 +0530 Subject: [PATCH 13/26] feat: added time_stamp flag (#93) * feat: added time_stamp flag * Syntax Refactor and filename format fixed * file name format fixes * wayshot-{timestamp}.{extension} as default filename * chore: Update Cargo.lock Signed-off-by: Shinyzenith --------- Signed-off-by: Shinyzenith Authored-by: rachancheet Co-authored-by: Shinyzenith --- wayshot/Cargo.toml | 1 + wayshot/src/utils.rs | 16 +++++----------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/wayshot/Cargo.toml b/wayshot/Cargo.toml index 9364ff88..3a111f26 100644 --- a/wayshot/Cargo.toml +++ b/wayshot/Cargo.toml @@ -32,6 +32,7 @@ image = { version = "0.24", default-features = false, features = [ dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } eyre = "0.6.8" +chrono = "0.4.35" wl-clipboard-rs = "0.8.0" nix = { version = "0.28.0", features = ["process"] } diff --git a/wayshot/src/utils.rs b/wayshot/src/utils.rs index 117c1f0c..158f997f 100644 --- a/wayshot/src/utils.rs +++ b/wayshot/src/utils.rs @@ -1,13 +1,9 @@ use clap::ValueEnum; use eyre::{bail, ContextCompat, Error, Result}; -use std::{ - fmt::Display, - path::PathBuf, - str::FromStr, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::{fmt::Display, path::PathBuf, str::FromStr}; +use chrono::{DateTime, Local}; use libwayshot::region::{LogicalRegion, Position, Region, Size}; pub fn parse_geometry(g: &str) -> Result { @@ -134,10 +130,8 @@ impl FromStr for EncodingFormat { } pub fn get_default_file_name(extension: EncodingFormat) -> PathBuf { - let time = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|time| time.as_secs().to_string()) - .unwrap_or("unknown".into()); + let current_datetime: DateTime = Local::now(); + let formated_time = format!("{}", current_datetime.format("%Y_%m_%d-%H_%M_%S")); - format!("{time}-wayshot.{extension}").into() + format!("wayshot-{formated_time}.{extension}").into() } From d7200c7aae18a4a29530e2f64006f5e8bfe9942c Mon Sep 17 00:00:00 2001 From: Shinyzenith Date: Sat, 23 Mar 2024 21:28:40 +0530 Subject: [PATCH 14/26] chore: Update Cargo.lock Signed-off-by: Shinyzenith --- Cargo.lock | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index c5186d5e..bc317536 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,21 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.13" @@ -74,6 +89,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "bumpalo" +version = "3.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" + [[package]] name = "bytemuck" version = "1.15.0" @@ -104,6 +125,20 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "chrono" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "clap" version = "4.5.3" @@ -169,6 +204,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "crc32fast" version = "1.4.0" @@ -317,6 +358,29 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "image" version = "0.24.9" @@ -354,6 +418,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -778,6 +851,60 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + [[package]] name = "wayland-backend" version = "0.3.3" @@ -855,6 +982,7 @@ dependencies = [ name = "wayshot" version = "1.3.2-dev" dependencies = [ + "chrono", "clap", "dialoguer", "eyre", @@ -889,6 +1017,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" From 017be62f244f1705ad1c557df2d754b4009036d7 Mon Sep 17 00:00:00 2001 From: Gigas002 <24297712+Gigas002@users.noreply.github.com> Date: Sun, 24 Mar 2024 01:06:32 +0900 Subject: [PATCH 15/26] feat: Add support for webp (#98) Signed-off-by: Shinyzenith --------- Signed-off-by: Shinyzenith Co-authored-by: Shinyzenith --- docs/wayshot.1.scd | 1 + wayshot/Cargo.toml | 1 + wayshot/src/utils.rs | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/docs/wayshot.1.scd b/docs/wayshot.1.scd index 1ab5762f..47a9a8fe 100644 --- a/docs/wayshot.1.scd +++ b/docs/wayshot.1.scd @@ -33,6 +33,7 @@ Wayshot - Screenshot tool for compositors implementing zwlr_screencopy_v1 such a - png (Default encoder) - ppm - qoi + - webp *-f*, *--file* Set a custom file path. The default path is `./{current_unix_timestamp}-wayshot.{encoder}` diff --git a/wayshot/Cargo.toml b/wayshot/Cargo.toml index 3a111f26..2835b246 100644 --- a/wayshot/Cargo.toml +++ b/wayshot/Cargo.toml @@ -28,6 +28,7 @@ image = { version = "0.24", default-features = false, features = [ "png", "pnm", "qoi", + "webp-encoder", ] } dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } diff --git a/wayshot/src/utils.rs b/wayshot/src/utils.rs index 158f997f..1f8bf8b4 100644 --- a/wayshot/src/utils.rs +++ b/wayshot/src/utils.rs @@ -58,6 +58,8 @@ pub enum EncodingFormat { Ppm, /// Qut encoder. Qoi, + /// WebP encoder, + Webp, } impl Default for EncodingFormat { @@ -73,6 +75,7 @@ impl From for image::ImageOutputFormat { EncodingFormat::Png => image::ImageFormat::Png.into(), EncodingFormat::Ppm => image::ImageFormat::Pnm.into(), EncodingFormat::Qoi => image::ImageFormat::Qoi.into(), + EncodingFormat::Webp => image::ImageFormat::WebP.into(), } } } @@ -111,6 +114,7 @@ impl From for &str { EncodingFormat::Png => "png", EncodingFormat::Ppm => "ppm", EncodingFormat::Qoi => "qoi", + EncodingFormat::Webp => "webp", } } } From f53e650441b0d7cb2a0fbc176075940cf1f775ab Mon Sep 17 00:00:00 2001 From: Sergey A <7361274+murlakatamenka@users.noreply.github.com> Date: Sat, 23 Mar 2024 19:30:25 +0300 Subject: [PATCH 16/26] refactor(libwayshot): Reduce allocations (#99) * refactor: remove unnecessary to_string() * refactor: fix clippy warnings * refactor: remove 1 level of indirection via &Vec -> &[T] Typically `&Vec` isn't something you want when you need a read-only view over a Vec, because it adds another level of indirection: Vec already stores a pointer to heap-allocated buffer internally. Using slices `&[T]` removes such unnecessary level of indirection and is considered a cleaner design. It is cache friendlier and can be better optimized by the compiler (not that it should matter in this case). --- libwayshot/src/lib.rs | 48 ++++++++++++++++++---------------------- libwayshot/src/region.rs | 6 ++--- wayshot/src/wayshot.rs | 6 ++--- 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/libwayshot/src/lib.rs b/libwayshot/src/lib.rs index e2d430a3..3cd7d280 100644 --- a/libwayshot/src/lib.rs +++ b/libwayshot/src/lib.rs @@ -100,8 +100,8 @@ impl WayshotConnection { } /// Fetch all accessible wayland outputs. - pub fn get_all_outputs(&self) -> &Vec { - &self.output_infos + pub fn get_all_outputs(&self) -> &[OutputInfo] { + self.output_infos.as_slice() } /// refresh the outputs, to get new outputs @@ -388,37 +388,32 @@ impl WayshotConnection { pub fn capture_frame_copies( &self, - output_capture_regions: &Vec<(OutputInfo, Option)>, + output_capture_regions: &[(OutputInfo, Option)], cursor_overlay: bool, ) -> Result> { let frame_copies = thread::scope(|scope| -> Result<_> { let join_handles = output_capture_regions - .into_iter() + .iter() .map(|(output_info, capture_region)| { scope.spawn(move || { - self.capture_frame_copy( - cursor_overlay, - &output_info, - capture_region.clone(), - ) - .map(|(frame_copy, frame_guard)| { - (frame_copy, frame_guard, output_info.clone()) - }) + self.capture_frame_copy(cursor_overlay, output_info, *capture_region) + .map(|(frame_copy, frame_guard)| { + (frame_copy, frame_guard, output_info.clone()) + }) }) }) .collect::>(); join_handles .into_iter() - .map(|join_handle| join_handle.join()) - .flatten() + .flat_map(|join_handle| join_handle.join()) .collect::>() })?; Ok(frame_copies) } - fn overlay_frames(&self, frames: &Vec<(FrameCopy, FrameGuard, OutputInfo)>) -> Result<()> { + fn overlay_frames(&self, frames: &[(FrameCopy, FrameGuard, OutputInfo)]) -> Result<()> { let mut state = LayerShellState { configured_outputs: HashSet::new(), }; @@ -506,15 +501,15 @@ impl WayshotConnection { region_capturer: RegionCapturer, cursor_overlay: bool, ) -> Result { - let outputs_capture_regions: &Vec<(OutputInfo, Option)> = - &match region_capturer { + let outputs_capture_regions: Vec<(OutputInfo, Option)> = + match region_capturer { RegionCapturer::Outputs(ref outputs) => outputs - .into_iter() + .iter() .map(|output_info| (output_info.clone(), None)) .collect(), RegionCapturer::Region(capture_region) => self .get_all_outputs() - .into_iter() + .iter() .filter_map(|output_info| { tracing::span!( tracing::Level::DEBUG, @@ -541,15 +536,15 @@ impl WayshotConnection { .collect(), RegionCapturer::Freeze(_) => self .get_all_outputs() - .into_iter() + .iter() .map(|output_info| (output_info.clone(), None)) .collect(), }; - let frames = self.capture_frame_copies(outputs_capture_regions, cursor_overlay)?; + let frames = self.capture_frame_copies(&outputs_capture_regions, cursor_overlay)?; let capture_region: LogicalRegion = match region_capturer { - RegionCapturer::Outputs(ref outputs) => outputs.try_into()?, + RegionCapturer::Outputs(outputs) => outputs.as_slice().try_into()?, RegionCapturer::Region(region) => region, RegionCapturer::Freeze(callback) => { self.overlay_frames(&frames).and_then(|_| callback())? @@ -565,7 +560,7 @@ impl WayshotConnection { .map(|(output_info, _)| output_info.scale()) .fold(1.0, f64::max); - tracing::Span::current().record("max_scale", &max_scale); + tracing::Span::current().record("max_scale", max_scale); let rotate_join_handles = frames .into_iter() @@ -587,8 +582,7 @@ impl WayshotConnection { rotate_join_handles .into_iter() - .map(|join_handle| join_handle.join()) - .flatten() + .flat_map(|join_handle| join_handle.join()) .fold( None, |composite_image: Option>, image: Result<_>| { @@ -666,14 +660,14 @@ impl WayshotConnection { /// Take a screenshot from all of the specified outputs. pub fn screenshot_outputs( &self, - outputs: &Vec, + outputs: &[OutputInfo], cursor_overlay: bool, ) -> Result { if outputs.is_empty() { return Err(Error::NoOutputs); } - self.screenshot_region_capturer(RegionCapturer::Outputs(outputs.clone()), cursor_overlay) + self.screenshot_region_capturer(RegionCapturer::Outputs(outputs.to_owned()), cursor_overlay) } /// Take a screenshot from all accessible outputs. diff --git a/libwayshot/src/region.rs b/libwayshot/src/region.rs index 394e1fb3..6813ded2 100644 --- a/libwayshot/src/region.rs +++ b/libwayshot/src/region.rs @@ -116,7 +116,7 @@ impl EmbeddedRegion { }; Some(Self { - relative_to: relative_to, + relative_to, inner: Region { position: Position { x: x1, y: y1 }, size: Size { width, height }, @@ -195,10 +195,10 @@ impl From<&OutputInfo> for LogicalRegion { } } -impl TryFrom<&Vec> for LogicalRegion { +impl TryFrom<&[OutputInfo]> for LogicalRegion { type Error = Error; - fn try_from(output_info: &Vec) -> std::result::Result { + fn try_from(output_info: &[OutputInfo]) -> std::result::Result { let x1 = output_info .iter() .map(|output| output.logical_region.inner.position.x) diff --git a/wayshot/src/wayshot.rs b/wayshot/src/wayshot.rs index a1c8ac0d..1a2ca746 100644 --- a/wayshot/src/wayshot.rs +++ b/wayshot/src/wayshot.rs @@ -72,7 +72,7 @@ fn main() -> Result<()> { Box::new(move || { || -> Result { let slurp_output = Command::new("slurp") - .args(slurp_region.split(" ")) + .args(slurp_region.split(' ')) .output()? .stdout; @@ -91,9 +91,9 @@ fn main() -> Result<()> { } } else if cli.choose_output { let outputs = wayshot_conn.get_all_outputs(); - let output_names: Vec = outputs + let output_names: Vec<&str> = outputs .iter() - .map(|display| display.name.to_string()) + .map(|display| display.name.as_str()) .collect(); if let Some(index) = select_ouput(&output_names) { wayshot_conn.screenshot_single_output(&outputs[index], cli.cursor)? From 2afa5b0cbb7750527983c6699193b06401888f41 Mon Sep 17 00:00:00 2001 From: rachancheet <55895940+rachancheet@users.noreply.github.com> Date: Sat, 23 Mar 2024 23:00:26 +0530 Subject: [PATCH 17/26] feat: account for directories in file path (#96) Signed-off-by: Shinyzenith --------- Signed-off-by: Shinyzenith Authored-by: rachancheet Co-authored-by: Shinyzenith --- Cargo.lock | 39 +++++++++++++++++++++++++++++++++++++++ wayshot/src/cli.rs | 14 +++++++++----- wayshot/src/wayshot.rs | 5 ++++- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc317536..04fcbd3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,10 @@ name = "cc" version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +dependencies = [ + "jobserver", + "libc", +] [[package]] name = "cfg-if" @@ -337,6 +341,12 @@ dependencies = [ "thread_local", ] +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "hashbrown" version = "0.14.3" @@ -394,6 +404,7 @@ dependencies = [ "num-traits", "png", "qoi", + "webp", ] [[package]] @@ -412,6 +423,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "jobserver" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +dependencies = [ + "libc", +] + [[package]] name = "jpeg-decoder" version = "0.3.1" @@ -463,6 +483,16 @@ dependencies = [ "wayland-protocols-wlr", ] +[[package]] +name = "libwebp-sys" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829b6b604f31ed6d2bccbac841fe0788de93dbd87e4eb1ba2c4adfe8c012a838" +dependencies = [ + "cc", + "glob", +] + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -995,6 +1025,15 @@ dependencies = [ "wl-clipboard-rs", ] +[[package]] +name = "webp" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb5d8e7814e92297b0e1c773ce43d290bef6c17452dafd9fc49e5edb5beba71" +dependencies = [ + "libwebp-sys", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/wayshot/src/cli.rs b/wayshot/src/cli.rs index c0508e9e..7a5dc1f6 100644 --- a/wayshot/src/cli.rs +++ b/wayshot/src/cli.rs @@ -11,12 +11,16 @@ use clap::builder::TypedValueParser; #[derive(Parser)] #[command(version, about)] pub struct Cli { - /// Where to save the screenshot, "-" for stdout. Defaults to "$UNIX_TIMESTAMP-wayshot.$EXTENSION" unless --clipboard is present. - #[arg(value_name = "OUTPUT")] + /// Custom output path can be of the following types: + /// 1. Directory (Default naming scheme is used for the image output). + /// 2. Path (Encoding is automatically inferred from the extension). + /// 3. `-` (Indicates writing to terminal [stdout]). + #[arg(value_name = "OUTPUT", verbatim_doc_comment)] pub file: Option, - /// Copy image to clipboard along with [OUTPUT] or stdout. Wayshot persists in the background to offer the image till the clipboard is overwritten. - #[arg(long)] + /// Copy image to clipboard along with [OUTPUT] or stdout. + /// Wayshot persists in the background to offer the image till the clipboard is overwritten. + #[arg(long, verbatim_doc_comment)] pub clipboard: bool, /// Log level to be used for printing to stderr @@ -33,7 +37,7 @@ pub struct Cli { /// Set image encoder, by default uses the file extension from the OUTPUT /// positional argument. Otherwise defaults to png. - #[arg(long, visible_aliases = ["extension", "format", "output-format"], value_name = "FILE_EXTENSION")] + #[arg(long, verbatim_doc_comment, visible_aliases = ["extension", "format", "output-format"], value_name = "FILE_EXTENSION")] pub encoding: Option, /// List all valid outputs diff --git a/wayshot/src/wayshot.rs b/wayshot/src/wayshot.rs index 1a2ca746..4ba2955f 100644 --- a/wayshot/src/wayshot.rs +++ b/wayshot/src/wayshot.rs @@ -106,11 +106,14 @@ fn main() -> Result<()> { let mut stdout_print = false; let file = match cli.file { - Some(pathbuf) => { + Some(mut pathbuf) => { if pathbuf.to_string_lossy() == "-" { stdout_print = true; None } else { + if pathbuf.is_dir() { + pathbuf.push(utils::get_default_file_name(requested_encoding)); + } Some(pathbuf) } } From 7381159578470306a105a2a1698a77e40784809c Mon Sep 17 00:00:00 2001 From: Sooraj S <94284954+CheerfulPianissimo@users.noreply.github.com> Date: Tue, 26 Mar 2024 19:26:22 +0530 Subject: [PATCH 18/26] fix: Clipboard flag ignored if path is qualified to file (#108) * feat(clipboard): fix issue #106 clipboard flag ignored if path is qualified to file * feat(clipboard): clarify flag description of --clipboard * feat(clipboard): enable multiline comment description for --clipboard feature flag Signed-off-by: Shinyzenith * feat(clipboard): reduce buffer allocations for encoding image * feat(clipboard): improve code quality * feat(clipboard): code style change: perform match inside function call --------- Signed-off-by: Shinyzenith Co-authored-by: Shinyzenith --- wayshot/src/cli.rs | 4 +-- wayshot/src/wayshot.rs | 76 +++++++++++++++++++++++++----------------- 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/wayshot/src/cli.rs b/wayshot/src/cli.rs index 7a5dc1f6..0ad61c23 100644 --- a/wayshot/src/cli.rs +++ b/wayshot/src/cli.rs @@ -18,8 +18,8 @@ pub struct Cli { #[arg(value_name = "OUTPUT", verbatim_doc_comment)] pub file: Option, - /// Copy image to clipboard along with [OUTPUT] or stdout. - /// Wayshot persists in the background to offer the image till the clipboard is overwritten. + /// Copy image to clipboard. Can be used simultaneously with [OUTPUT] or stdout. + /// Wayshot persists in the background offering the image till the clipboard is overwritten. #[arg(long, verbatim_doc_comment)] pub clipboard: bool, diff --git a/wayshot/src/wayshot.rs b/wayshot/src/wayshot.rs index 4ba2955f..b8a9420b 100644 --- a/wayshot/src/wayshot.rs +++ b/wayshot/src/wayshot.rs @@ -1,4 +1,5 @@ use std::{ + fs::File, io::{stdout, BufWriter, Cursor, Write}, process::Command, }; @@ -126,43 +127,58 @@ fn main() -> Result<()> { } }; + let mut image_buf: Option>> = None; if let Some(file) = file { image_buffer.save(file)?; - } else { + } else if stdout_print { let mut buffer = Cursor::new(Vec::new()); image_buffer.write_to(&mut buffer, requested_encoding)?; + let stdout = stdout(); + let mut writer = BufWriter::new(stdout.lock()); + writer.write_all(buffer.get_ref())?; + image_buf = Some(buffer); + } - if stdout_print { - let stdout = stdout(); - let mut writer = BufWriter::new(stdout.lock()); - writer.write_all(buffer.get_ref())?; - } - if cli.clipboard { - let mut opts = Options::new(); - match unsafe { fork() } { - // Having the image persistently available on the clipboard requires a wayshot process to be alive. - // Fork the process with a child detached from the main process and have the parent exit - Ok(ForkResult::Parent { .. }) => { - return Ok(()); - } - Ok(ForkResult::Child) => { - opts.foreground(true); // Offer the image till something else is available on the clipboard - opts.copy( - Source::Bytes(buffer.into_inner().into()), - MimeType::Autodetect, - )?; - } - Err(e) => { - tracing::warn!("Fork failed with error: {e}, couldn't offer image on the clipboard persistently. - Use a clipboard manager to record screenshot."); - opts.copy( - Source::Bytes(buffer.into_inner().into()), - MimeType::Autodetect, - )?; - } + if cli.clipboard { + clipboard_daemonize(match image_buf { + Some(buf) => buf, + None => { + let mut buffer = Cursor::new(Vec::new()); + image_buffer.write_to(&mut buffer, requested_encoding)?; + buffer } - } + })?; } Ok(()) } + +/// Daemonize and copy the given buffer containing the encoded image to the clipboard +fn clipboard_daemonize(buffer: Cursor>) -> Result<()> { + let mut opts = Options::new(); + match unsafe { fork() } { + // Having the image persistently available on the clipboard requires a wayshot process to be alive. + // Fork the process with a child detached from the main process and have the parent exit + Ok(ForkResult::Parent { .. }) => { + return Ok(()); + } + Ok(ForkResult::Child) => { + opts.foreground(true); // Offer the image till something else is available on the clipboard + opts.copy( + Source::Bytes(buffer.into_inner().into()), + MimeType::Autodetect, + )?; + } + Err(e) => { + tracing::warn!( + "Fork failed with error: {e}, couldn't offer image on the clipboard persistently. + Use a clipboard manager to record screenshot." + ); + opts.copy( + Source::Bytes(buffer.into_inner().into()), + MimeType::Autodetect, + )?; + } + } + Ok(()) +} From 2cb242c3b16a19928a9bec2b02d09eb982299b27 Mon Sep 17 00:00:00 2001 From: Access Date: Thu, 28 Mar 2024 16:25:16 +0800 Subject: [PATCH 19/26] chore: make clippy happy (#111) --- libwayshot/src/image_util.rs | 4 ++-- libwayshot/src/lib.rs | 2 +- wayshot/build.rs | 2 ++ wayshot/src/wayshot.rs | 1 - 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/libwayshot/src/image_util.rs b/libwayshot/src/image_util.rs index e06af24d..fbb7068f 100644 --- a/libwayshot/src/image_util.rs +++ b/libwayshot/src/image_util.rs @@ -1,4 +1,4 @@ -use image::{DynamicImage, GenericImageView}; +use image::DynamicImage; use wayland_client::protocol::wl_output::Transform; use crate::region::Size; @@ -12,7 +12,7 @@ pub(crate) fn rotate_image_buffer( ) -> DynamicImage { // TODO Better document whether width and height are before or after the transform. // Perhaps this should be part of a cleanup of the FrameCopy struct. - let (logical_width, logical_height) = match transform { + let (logical_width, _logical_height) = match transform { Transform::_90 | Transform::_270 | Transform::Flipped90 | Transform::Flipped270 => { (logical_size.height, logical_size.width) } diff --git a/libwayshot/src/lib.rs b/libwayshot/src/lib.rs index 3cd7d280..4e8bc98b 100644 --- a/libwayshot/src/lib.rs +++ b/libwayshot/src/lib.rs @@ -52,7 +52,7 @@ use crate::{ convert::create_converter, dispatch::{CaptureFrameState, FrameState, OutputCaptureState, WayshotState}, output::OutputInfo, - region::{LogicalRegion, Region, Size}, + region::{LogicalRegion, Size}, screencopy::{create_shm_fd, FrameCopy, FrameFormat}, }; diff --git a/wayshot/build.rs b/wayshot/build.rs index 37104611..9c7c82e8 100644 --- a/wayshot/build.rs +++ b/wayshot/build.rs @@ -26,6 +26,7 @@ fn main() -> Result<()> { let output = OpenOptions::new() .write(true) .create(true) + .truncate(true) .open(Path::new(&man_page.1))?; _ = Command::new("scdoc") .stdin(Stdio::from(File::open(man_page.0)?)) @@ -41,6 +42,7 @@ fn main() -> Result<()> { let output = OpenOptions::new() .write(true) .create(true) + .truncate(true) .open(Path::new(&scdoc_output.1))?; let mut encoder = GzEncoder::new(output, Compression::default()); copy(&mut input, &mut encoder)?; diff --git a/wayshot/src/wayshot.rs b/wayshot/src/wayshot.rs index b8a9420b..2d25e098 100644 --- a/wayshot/src/wayshot.rs +++ b/wayshot/src/wayshot.rs @@ -1,5 +1,4 @@ use std::{ - fs::File, io::{stdout, BufWriter, Cursor, Write}, process::Command, }; From 10b748f555fea5a2a7ac7d49f8ecf6637819f12d Mon Sep 17 00:00:00 2001 From: Sooraj S <94284954+CheerfulPianissimo@users.noreply.github.com> Date: Sun, 7 Apr 2024 00:47:57 +0530 Subject: [PATCH 20/26] feat(clipboard): fix interaction with freeze and select feature: Issue #109 (#110) * feat(clipboard): fix interaction with freeze and select feature: Issue #109 * feat(clipboard): handle callback fails --- libwayshot/src/lib.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/libwayshot/src/lib.rs b/libwayshot/src/lib.rs index 4e8bc98b..d0ec88ad 100644 --- a/libwayshot/src/lib.rs +++ b/libwayshot/src/lib.rs @@ -413,7 +413,13 @@ impl WayshotConnection { Ok(frame_copies) } - fn overlay_frames(&self, frames: &[(FrameCopy, FrameGuard, OutputInfo)]) -> Result<()> { + /// Create a layer shell surface for each output, + /// render the screen captures on them and use the callback to select a region from them + fn overlay_frames_and_select_region( + &self, + frames: &[(FrameCopy, FrameGuard, OutputInfo)], + callback: Box Result>, + ) -> Result { let mut state = LayerShellState { configured_outputs: HashSet::new(), }; @@ -491,7 +497,10 @@ impl WayshotConnection { Ok(()) })?; } - Ok(()) + let callback_result = callback(); + layer_shell.destroy(); + event_queue.blocking_dispatch(&mut state)?; + callback_result } /// Take a screenshot from the specified region. @@ -547,7 +556,7 @@ impl WayshotConnection { RegionCapturer::Outputs(outputs) => outputs.as_slice().try_into()?, RegionCapturer::Region(region) => region, RegionCapturer::Freeze(callback) => { - self.overlay_frames(&frames).and_then(|_| callback())? + self.overlay_frames_and_select_region(&frames, callback)? } }; From 2dd8af66476606e363f9696c29a3f73e48606687 Mon Sep 17 00:00:00 2001 From: Sooraj S <94284954+CheerfulPianissimo@users.noreply.github.com> Date: Wed, 29 May 2024 03:14:58 +0530 Subject: [PATCH 21/26] Destroy layer shell surfaces * [fix] unmap layer shell surfaces before destroying them --- libwayshot/src/lib.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/libwayshot/src/lib.rs b/libwayshot/src/lib.rs index d0ec88ad..9c52a811 100644 --- a/libwayshot/src/lib.rs +++ b/libwayshot/src/lib.rs @@ -452,6 +452,8 @@ impl WayshotConnection { } }; + let mut layer_shell_surfaces = Vec::with_capacity(frames.len()); + for (frame_copy, frame_guard, output_info) in frames { tracing::span!( tracing::Level::DEBUG, @@ -491,15 +493,23 @@ impl WayshotConnection { debug!("Committing surface with attached buffer."); surface.commit(); - + layer_shell_surfaces.push((surface, layer_surface)); event_queue.blocking_dispatch(&mut state)?; Ok(()) })?; } + let callback_result = callback(); - layer_shell.destroy(); - event_queue.blocking_dispatch(&mut state)?; + + debug!("Unmapping and destroying layer shell surfaces."); + for (surface, layer_shell_surface) in layer_shell_surfaces.iter() { + surface.attach(None, 0, 0); + surface.commit(); //unmap surface by committing a null buffer + layer_shell_surface.destroy(); + } + event_queue.roundtrip(&mut state)?; + callback_result } From 7934fd2d8ae1ea33818e14b2009914f831879ca8 Mon Sep 17 00:00:00 2001 From: Gigas002 <24297712+Gigas002@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:35:16 +0900 Subject: [PATCH 22/26] Implement missing conversion from string to webp enum (#121) --- wayshot/src/utils.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/wayshot/src/utils.rs b/wayshot/src/utils.rs index 1f8bf8b4..feca42f8 100644 --- a/wayshot/src/utils.rs +++ b/wayshot/src/utils.rs @@ -128,6 +128,7 @@ impl FromStr for EncodingFormat { "png" => Self::Png, "ppm" => Self::Ppm, "qoi" => Self::Qoi, + "webp" => Self::Webp, _ => bail!("unsupported extension '{s}'"), }) } From 1c3c8029f00191248d61aebb9f601b4e628c7b7d Mon Sep 17 00:00:00 2001 From: Gigas002 <24297712+Gigas002@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:36:43 +0900 Subject: [PATCH 23/26] Replace nix with rustix (#120) --- Cargo.lock | 17 +++----------- libwayshot/Cargo.toml | 2 +- libwayshot/src/screencopy.rs | 44 +++++++++++++----------------------- wayshot/Cargo.toml | 2 +- wayshot/src/wayshot.rs | 6 ++--- 5 files changed, 24 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04fcbd3d..2c9f32ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -475,7 +475,7 @@ version = "0.3.2-dev" dependencies = [ "image", "memmap2", - "nix 0.27.1", + "rustix", "thiserror", "tracing", "wayland-client", @@ -536,17 +536,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "nix" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" -dependencies = [ - "bitflags 2.5.0", - "cfg-if", - "libc", -] - [[package]] name = "nix" version = "0.28.0" @@ -1019,7 +1008,7 @@ dependencies = [ "flate2", "image", "libwayshot", - "nix 0.28.0", + "rustix", "tracing", "tracing-subscriber", "wl-clipboard-rs", @@ -1140,7 +1129,7 @@ dependencies = [ "derive-new", "libc", "log", - "nix 0.28.0", + "nix", "os_pipe", "tempfile", "thiserror", diff --git a/libwayshot/Cargo.toml b/libwayshot/Cargo.toml index 8a9719b0..7ff54d5e 100644 --- a/libwayshot/Cargo.toml +++ b/libwayshot/Cargo.toml @@ -12,7 +12,7 @@ edition.workspace = true tracing.workspace = true image = { version = "0.24", default-features = false } memmap2 = "0.9.0" -nix = { version = "0.27.1", features = ["fs", "mman"] } +rustix = { version = "0.38", features = ["fs", "shm"] } thiserror = "1" wayland-client = "0.31.1" diff --git a/libwayshot/src/screencopy.rs b/libwayshot/src/screencopy.rs index 8b01c7c6..ee7ae337 100644 --- a/libwayshot/src/screencopy.rs +++ b/libwayshot/src/screencopy.rs @@ -1,15 +1,14 @@ use std::{ ffi::CString, - os::fd::{AsRawFd, IntoRawFd, OwnedFd}, + os::fd::OwnedFd, time::{SystemTime, UNIX_EPOCH}, }; use image::{ColorType, DynamicImage, ImageBuffer, Pixel}; use memmap2::MmapMut; -use nix::{ - fcntl, - sys::{memfd, mman, stat}, - unistd, +use rustix::{ + fs::{self, SealFlags}, + io, shm, }; use wayland_client::protocol::{ wl_buffer::WlBuffer, wl_output, wl_shm::Format, wl_shm_pool::WlShmPool, @@ -117,24 +116,19 @@ pub fn create_shm_fd() -> std::io::Result { #[cfg(any(target_os = "linux", target_os = "freebsd"))] loop { // Create a file that closes on succesful execution and seal it's operations. - match memfd::memfd_create( + match fs::memfd_create( CString::new("libwayshot")?.as_c_str(), - memfd::MemFdCreateFlag::MFD_CLOEXEC | memfd::MemFdCreateFlag::MFD_ALLOW_SEALING, + fs::MemfdFlags::CLOEXEC | fs::MemfdFlags::ALLOW_SEALING, ) { Ok(fd) => { // This is only an optimization, so ignore errors. // F_SEAL_SRHINK = File cannot be reduced in size. // F_SEAL_SEAL = Prevent further calls to fcntl(). - let _ = fcntl::fcntl( - fd.as_raw_fd(), - fcntl::F_ADD_SEALS( - fcntl::SealFlag::F_SEAL_SHRINK | fcntl::SealFlag::F_SEAL_SEAL, - ), - ); + let _ = fs::fcntl_add_seals(&fd, fs::SealFlags::SHRINK | SealFlags::SEAL); return Ok(fd); } - Err(nix::errno::Errno::EINTR) => continue, - Err(nix::errno::Errno::ENOSYS) => break, + Err(io::Errno::INTR) => continue, + Err(io::Errno::NOSYS) => break, Err(errno) => return Err(std::io::Error::from(errno)), } } @@ -142,7 +136,7 @@ pub fn create_shm_fd() -> std::io::Result { // Fallback to using shm_open. let mut mem_file_handle = get_mem_file_handle(); loop { - match mman::shm_open( + match shm::shm_open( // O_CREAT = Create file if does not exist. // O_EXCL = Error if create and file exists. // O_RDWR = Open for reading and writing. @@ -150,25 +144,19 @@ pub fn create_shm_fd() -> std::io::Result { // S_IRUSR = Set user read permission bit . // S_IWUSR = Set user write permission bit. mem_file_handle.as_str(), - fcntl::OFlag::O_CREAT - | fcntl::OFlag::O_EXCL - | fcntl::OFlag::O_RDWR - | fcntl::OFlag::O_CLOEXEC, - stat::Mode::S_IRUSR | stat::Mode::S_IWUSR, + shm::ShmOFlags::CREATE | shm::ShmOFlags::EXCL | shm::ShmOFlags::RDWR, + fs::Mode::RUSR | fs::Mode::WUSR, ) { - Ok(fd) => match mman::shm_unlink(mem_file_handle.as_str()) { + Ok(fd) => match shm::shm_unlink(mem_file_handle.as_str()) { Ok(_) => return Ok(fd), - Err(errno) => match unistd::close(fd.into_raw_fd()) { - Ok(_) => return Err(std::io::Error::from(errno)), - Err(errno) => return Err(std::io::Error::from(errno)), - }, + Err(errno) => return Err(std::io::Error::from(errno)), }, - Err(nix::errno::Errno::EEXIST) => { + Err(io::Errno::EXIST) => { // If a file with that handle exists then change the handle mem_file_handle = get_mem_file_handle(); continue; } - Err(nix::errno::Errno::EINTR) => continue, + Err(io::Errno::INTR) => continue, Err(errno) => return Err(std::io::Error::from(errno)), } } diff --git a/wayshot/Cargo.toml b/wayshot/Cargo.toml index 2835b246..44bc8652 100644 --- a/wayshot/Cargo.toml +++ b/wayshot/Cargo.toml @@ -36,7 +36,7 @@ eyre = "0.6.8" chrono = "0.4.35" wl-clipboard-rs = "0.8.0" -nix = { version = "0.28.0", features = ["process"] } +rustix = { version = "0.38", features = ["process", "runtime"] } [[bin]] name = "wayshot" diff --git a/wayshot/src/wayshot.rs b/wayshot/src/wayshot.rs index 2d25e098..0930b845 100644 --- a/wayshot/src/wayshot.rs +++ b/wayshot/src/wayshot.rs @@ -15,7 +15,7 @@ use utils::EncodingFormat; use wl_clipboard_rs::copy::{MimeType, Options, Source}; -use nix::unistd::{fork, ForkResult}; +use rustix::runtime::{fork, Fork}; fn select_ouput(ouputs: &[T]) -> Option where @@ -158,10 +158,10 @@ fn clipboard_daemonize(buffer: Cursor>) -> Result<()> { match unsafe { fork() } { // Having the image persistently available on the clipboard requires a wayshot process to be alive. // Fork the process with a child detached from the main process and have the parent exit - Ok(ForkResult::Parent { .. }) => { + Ok(Fork::Parent(_)) => { return Ok(()); } - Ok(ForkResult::Child) => { + Ok(Fork::Child(_)) => { opts.foreground(true); // Offer the image till something else is available on the clipboard opts.copy( Source::Bytes(buffer.into_inner().into()), From 5c3e220c319e9020fa71b1a2cac4df6dbf4dfcad Mon Sep 17 00:00:00 2001 From: Sooraj S <94284954+CheerfulPianissimo@users.noreply.github.com> Date: Sun, 25 Aug 2024 21:32:01 +0530 Subject: [PATCH 24/26] Update documentation with changes in freeze-feat (#116) * [docs] update manpage (1) with changes in freeze-feat * [fix] make arguments to slurp optional * [docs] update manpage(7) with cli changes * [docs] update README with new cli --- README.md | 8 +++--- docs/wayshot.1.scd | 59 ++++++++++++++++++++++++++---------------- docs/wayshot.7.scd | 16 ++++++------ wayshot/src/cli.rs | 2 +- wayshot/src/wayshot.rs | 4 +-- 5 files changed, 51 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index b0c60dbf..ced3676d 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ NOTE: Read `man 7 wayshot` for more examples. NOTE: Read `man wayshot` for flag information. -Region Selection: +Screenshot and Crop Region: ```bash -wayshot -s "$(slurp)" +wayshot -s ``` Fullscreen: @@ -38,13 +38,13 @@ wayshot Screenshot and copy to clipboard: ```bash -wayshot --stdout | wl-copy +wayshot --clipboard ``` Pick a hex color code, using ImageMagick: ```bash -wayshot -s "$(slurp -p)" --stdout | convert - -format '%[pixel:p{0,0}]' txt:-|grep -E "#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})" -o +wayshot -s - | convert - -format '%[pixel:p{0,0}]' txt:-|grep -E "#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})" -o ``` # Installation diff --git a/docs/wayshot.1.scd b/docs/wayshot.1.scd index 47a9a8fe..65dea812 100644 --- a/docs/wayshot.1.scd +++ b/docs/wayshot.1.scd @@ -6,57 +6,70 @@ Wayshot - Screenshot tool for compositors implementing zwlr_screencopy_v1 such a # SYNOPSIS -*wayshot* [_options_] +*wayshot* [_options_] [_output_] + +# ARGUMENTS + +*output*, + Location to send captured screenshot to, it can be of the following types: + 1. A directory, image outputs will be saved in the default format "wayshot-yyyy-mm-dd-hh-mm-ss.png" + + 2. A path (Encoding is automatically inferred from the extension). + + 3. '-' which sends the screenshot to stdout. + + The *--clipboard* option can also be used simultaneously with any of the above to copy the image to the clipboard too. # OPTIONS -*-h*, *--help* - Print help message and quit. +*-h*, + Print concise help messages and quit. + +*--help*, + Print full help message and quit. *-V*, *--version* Print version information. -*-d*, *--debug* - Enable debug mode. +*--log-level * + Log level to be used for printing to stderr + Possible values: trace, debug, info, warn, error + + Default value: info *-c*, *--cursor* Enable cursor visibility in screenshots. *--clipboard* - Copy image contents to clipboard. + Copy image contents to clipboard also. + Using this flag will cause the wayshot process to fork and persist in the background offering the image + on the wayland clipboard until some other program overwrites the clipboard. *-e*, *--extension* - Set the image encoder. + Set the image encoder. Without this option, encoding is either inferred from the *output* filename or defaults to png. Valid arguments: - - jpeg - jpg - png (Default encoder) - ppm - qoi - webp -*-f*, *--file* - Set a custom file path. The default path is `./{current_unix_timestamp}-wayshot.{encoder}` - eg: 1659034753-wayshot.png - -*-l*, *--listoutputs* +*-l*, *--list-outputs* List all valid output names. This flag is generally used in combination with *-o* flag. -*--chooseoutput* +*--choose-output* Present a fuzzy selector for display (wl_output) selection. *-o*, *--output* Choose a particular display (wl_output) to screenshot. -*-s*, *--slurp* - Choose a portion of your display to screenshot using the slurp program. - https://github.com/emersion/slurp . Valid arguments have the form - "%x %y %w %h" or "%x,%y %wx%h", where for example "%w" is an integer giving - the width of the region. - -*--stdout* - Emit image data to stdout. The following flag is helpful to pipe image data - to other programs. +*-s*, *--slurp*= + If this option is passed, wayshot takes a screenshot first and then uses the *slurp* program to select a portion of that screenshot + https://github.com/emersion/slurp . SLURP_ARGS are any arguments that need to be passed to *slurp*, see *slurp(1)* for more information. + + Example: + *wayshot -s="-b 11223377"* + - This takes a screenshot then calls slurp with "-b 11223377" as an argument for cropping the screenshot. # SEE ALSO - wayshot(7) diff --git a/docs/wayshot.7.scd b/docs/wayshot.7.scd index b4a59da2..459edcc0 100644 --- a/docs/wayshot.7.scd +++ b/docs/wayshot.7.scd @@ -6,22 +6,22 @@ Wayshot - Screenshot tool for compositors implementing zwlr_screencopy_v1 such a # SYNOPSIS -*wayshot* [_options_] +*wayshot* [_options_] [_output_] # REGION SELECTION -wayshot -s "$(slurp)" +wayshot -s -# FULLSCREEN +# CAPTURE FULLSCREEN wayshot # CUSTOM FILE PATH AND EXTENSION -wayshot -f ../screenshot.png --extension ppm +wayshot ../screenshot.png --extension ppm # SCREENSHOT AND COPY TO CLIPBOARD -wayshot --stdout -e jpeg | wl-copy +wayshot --clipboard # SCREENSHOT A PARTICULAR DISPLAY @@ -30,15 +30,15 @@ wayshot -o eDP-1 # PICK A HEX COLOR CODE, USING IMAGEMAGICk -wayshot -s "$(slurp)" --stdout | convert - -format '%[pixel:p{0,0}]' txt:-|grep -E "#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})" -o +wayshot -s - | convert - -format '%[pixel:p{0,0}]' txt:-|grep -E "#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})" -o # PICK A HEX COLOR CODE WITHOUT USING IMAGEMAGICK -wayshot -s "$(slurp -p)" --stdout -e ppm | tail -c 3 | od -An -tuC | xargs printf '#%02X%02X%02X\n' +wayshot -s - --encoding ppm | tail -c 3 | od -An -tuC | xargs printf '#%02X%02X%02X\n' # PICK A COLOR, USING IMAGEMAGICK -wayshot -s "$(slurp -p)" --stdout | convert - -format '%[pixel:p{0,0}]' txt:- +wayshot -s - | convert - -format '%[pixel:p{0,0}]' txt:- # AUTHORS diff --git a/wayshot/src/cli.rs b/wayshot/src/cli.rs index 0ad61c23..9882e09d 100644 --- a/wayshot/src/cli.rs +++ b/wayshot/src/cli.rs @@ -29,7 +29,7 @@ pub struct Cli { /// Arguments to call slurp with for selecting a region #[arg(short, long, value_name = "SLURP_ARGS")] - pub slurp: Option, + pub slurp: Option>, /// Enable cursor in screenshots #[arg(short, long)] diff --git a/wayshot/src/wayshot.rs b/wayshot/src/wayshot.rs index 0930b845..73f524d7 100644 --- a/wayshot/src/wayshot.rs +++ b/wayshot/src/wayshot.rs @@ -66,8 +66,8 @@ fn main() -> Result<()> { return Ok(()); } - let image_buffer = if let Some(slurp_region) = cli.slurp { - let slurp_region = slurp_region.clone(); + let image_buffer = if let Some(slurp_args) = cli.slurp { + let slurp_region = slurp_args.unwrap_or("".to_string()); wayshot_conn.screenshot_freeze( Box::new(move || { || -> Result { From 5d690007e992462c9e179a1984afdc2d463dcc39 Mon Sep 17 00:00:00 2001 From: Sooraj S <94284954+CheerfulPianissimo@users.noreply.github.com> Date: Wed, 28 Aug 2024 21:59:12 +0530 Subject: [PATCH 25/26] Add screencopy dmabuf backend (#122) * [feat] rough MVP for screencpy+dmabuf * [feat] refactor MVP into libwayshot - create new constructor * [refactor] first draft of dmabuf API * [refactor] - Add error handling - Add example/demo for wayshot dmabuf API * [feat] import wayland-egl-ctx for use as MVP for dmabuf import functionality * [fix] correct modifier_lo parameter in waymirror-egl MVP * [feat] get waymirror-egl dmabuf MVP demo working * [feat] refactored dmabuf->eglImage API into libwayshot * [feat] - Implemented clean dropping of EGLImage - Improved API docs for dmabuf functions * [fix] libwayshot build error fixed * [fix] remove hardcoded GPU path from libwayshot constructor * [fix]] remove reduntant dmabuf_to_texture call from waymirror-egl * [docs] document WayshotConnection dmabuf constructor * [feat] Added helper/wrapper function to convert screencapture EGLImages into GL textures * [fix] improved logging in dmabuf API code * [doc] update waymirror-egl Readme * [fix] change logging level in waymirror-egl to Debug * [fix] remove unnecessary .gitignore in waymirror-egl * [ci/cd] attempting to fix the build * [ci\cd] add libegl system deps to fix github CI * [fix] remove unused egl_image struct field in Waymirror demo --- .github/workflows/build.yml | 7 + Cargo.lock | 205 ++++++- Cargo.toml | 2 +- libwayshot/Cargo.toml | 7 + libwayshot/examples/waymirror-egl/Cargo.lock | 517 ++++++++++++++++++ libwayshot/examples/waymirror-egl/Cargo.toml | 18 + libwayshot/examples/waymirror-egl/README.md | 8 + .../examples/waymirror-egl/src/dispatch.rs | 131 +++++ .../examples/waymirror-egl/src/error.rs | 22 + libwayshot/examples/waymirror-egl/src/main.rs | 64 +++ .../waymirror-egl/src/shaders/frag.glsl | 11 + .../waymirror-egl/src/shaders/vert.glsl | 11 + .../examples/waymirror-egl/src/state.rs | 303 ++++++++++ .../examples/waymirror-egl/src/utils.rs | 31 ++ libwayshot/examples/waymirror.rs | 205 +++++++ libwayshot/src/dispatch.rs | 93 +++- libwayshot/src/error.rs | 14 + libwayshot/src/lib.rs | 477 ++++++++++++++-- libwayshot/src/screencopy.rs | 67 ++- 19 files changed, 2121 insertions(+), 72 deletions(-) create mode 100644 libwayshot/examples/waymirror-egl/Cargo.lock create mode 100644 libwayshot/examples/waymirror-egl/Cargo.toml create mode 100644 libwayshot/examples/waymirror-egl/README.md create mode 100644 libwayshot/examples/waymirror-egl/src/dispatch.rs create mode 100644 libwayshot/examples/waymirror-egl/src/error.rs create mode 100644 libwayshot/examples/waymirror-egl/src/main.rs create mode 100644 libwayshot/examples/waymirror-egl/src/shaders/frag.glsl create mode 100644 libwayshot/examples/waymirror-egl/src/shaders/vert.glsl create mode 100644 libwayshot/examples/waymirror-egl/src/state.rs create mode 100644 libwayshot/examples/waymirror-egl/src/utils.rs create mode 100644 libwayshot/examples/waymirror.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 49f07bab..a9505f7f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,6 +12,13 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 + + - name: Install wayland dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + libwayland-dev \ + libegl-dev \ - name: Build run: | diff --git a/Cargo.lock b/Cargo.lock index 2c9f32ff..96182bc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -100,6 +100,20 @@ name = "bytemuck" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "byteorder" @@ -263,6 +277,45 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +[[package]] +name = "drm" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1" +dependencies = [ + "bitflags 2.5.0", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "rustix", +] + +[[package]] +name = "drm-ffi" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c98727e48b7ccb4f4aea8cfe881e5b07f702d17b7875991881b41af7278d53" +dependencies = [ + "drm-sys", + "rustix", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd39dde40b6e196c2e8763f23d119ddb1a8714534bf7d77fa97a65b0feda3986" +dependencies = [ + "libc", + "linux-raw-sys 0.6.4", +] + [[package]] name = "encode_unicode" version = "0.3.6" @@ -341,6 +394,60 @@ dependencies = [ "thread_local", ] +[[package]] +name = "gbm" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bf55ba6dd53ad0ac115046ff999c5324c283444ee6e0be82454c4e8eb2f36a" +dependencies = [ + "bitflags 2.5.0", + "drm", + "drm-fourcc", + "gbm-sys", + "libc", + "wayland-backend", + "wayland-server", +] + +[[package]] +name = "gbm-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fd2d6bf7c0143b38beece05f9a5c4c851a49a8434f62bf58ff28da92b0ddc58" +dependencies = [ + "libc", +] + +[[package]] +name = "gl" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "gl_loader" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e32d96dd5f881490e537041d5532320812ba096097f07fccb4626578da0b99d3" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "glob" version = "0.3.1" @@ -423,6 +530,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "io-lifetimes" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c" + [[package]] name = "jobserver" version = "0.1.28" @@ -447,6 +560,22 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + [[package]] name = "lazy_static" version = "1.4.0" @@ -461,9 +590,9 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", "windows-targets", @@ -473,11 +602,16 @@ dependencies = [ name = "libwayshot" version = "0.3.2-dev" dependencies = [ + "drm", + "gbm", + "gl", "image", + "khronos-egl", "memmap2", "rustix", "thiserror", "tracing", + "wayland-backend", "wayland-client", "wayland-protocols", "wayland-protocols-wlr", @@ -499,6 +633,12 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "linux-raw-sys" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b5399f6804fbab912acbd8878ed3532d506b7c951b8f9f164ef90fef39e3f4" + [[package]] name = "log" version = "0.4.21" @@ -520,6 +660,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -679,7 +828,7 @@ dependencies = [ "bitflags 2.5.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.13", "windows-sys", ] @@ -950,6 +1099,16 @@ dependencies = [ "wayland-scanner", ] +[[package]] +name = "wayland-egl" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355f652e5a24ae02d2ad536c8fc2d3dcc6c2bd635027cd6103a193e7d75eeda2" +dependencies = [ + "wayland-backend", + "wayland-sys", +] + [[package]] name = "wayland-protocols" version = "0.31.2" @@ -986,6 +1145,20 @@ dependencies = [ "quote", ] +[[package]] +name = "wayland-server" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e6e4d5c285bc24ba4ed2d5a4bd4febd5fd904451f465973225c8e99772fdb7" +dependencies = [ + "bitflags 2.5.0", + "downcast-rs", + "io-lifetimes", + "rustix", + "wayland-backend", + "wayland-scanner", +] + [[package]] name = "wayland-sys" version = "0.31.1" @@ -993,10 +1166,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" dependencies = [ "dlib", + "libc", "log", + "memoffset", "pkg-config", ] +[[package]] +name = "waymirror-egl" +version = "0.1.0" +dependencies = [ + "gl", + "gl_loader", + "khronos-egl", + "libloading", + "libwayshot", + "thiserror", + "tracing", + "tracing-subscriber", + "wayland-backend", + "wayland-client", + "wayland-egl", + "wayland-protocols", +] + [[package]] name = "wayshot" version = "1.3.2-dev" @@ -1140,6 +1333,12 @@ dependencies = [ "wayland-protocols-wlr", ] +[[package]] +name = "xml-rs" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" + [[package]] name = "zeroize" version = "1.7.0" diff --git a/Cargo.toml b/Cargo.toml index 4979f4eb..b13e9351 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["wayshot", "libwayshot"] +members = ["wayshot", "libwayshot","libwayshot/examples/waymirror-egl"] [workspace.package] authors = ["Shinyzenith "] diff --git a/libwayshot/Cargo.toml b/libwayshot/Cargo.toml index 7ff54d5e..d811f78e 100644 --- a/libwayshot/Cargo.toml +++ b/libwayshot/Cargo.toml @@ -18,3 +18,10 @@ thiserror = "1" wayland-client = "0.31.1" wayland-protocols = { version = "0.31.0", features = ["client", "unstable"] } wayland-protocols-wlr = { version = "0.2.0", features = ["client"] } +wayland-backend = { version = "0.3.3", features = ["client_system"] } + +gbm = "0.15.0" +drm = "0.12.0" + +gl = "0.14.0" +khronos-egl = { version = "6.0.0",features = ["static"] } \ No newline at end of file diff --git a/libwayshot/examples/waymirror-egl/Cargo.lock b/libwayshot/examples/waymirror-egl/Cargo.lock new file mode 100644 index 00000000..310b1996 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/Cargo.lock @@ -0,0 +1,517 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "cc" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "gl" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "gl_loader" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e32d96dd5f881490e537041d5532320812ba096097f07fccb4626578da0b99d3" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustix" +version = "0.38.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "syn" +version = "2.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "wayland-backend" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" +dependencies = [ + "bitflags", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-egl" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355f652e5a24ae02d2ad536c8fc2d3dcc6c2bd635027cd6103a193e7d75eeda2" +dependencies = [ + "wayland-backend", + "wayland-sys", +] + +[[package]] +name = "wayland-egl-ctx" +version = "0.1.0" +dependencies = [ + "gl", + "gl_loader", + "khronos-egl", + "thiserror", + "tracing", + "tracing-subscriber", + "wayland-backend", + "wayland-client", + "wayland-egl", + "wayland-protocols", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +dependencies = [ + "bitflags", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + +[[package]] +name = "xml-rs" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" diff --git a/libwayshot/examples/waymirror-egl/Cargo.toml b/libwayshot/examples/waymirror-egl/Cargo.toml new file mode 100644 index 00000000..eb6d9dc8 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "waymirror-egl" +version = "0.1.0" +edition = "2021" + +[dependencies] +gl = "0.14.0" +gl_loader = "0.1.2" +khronos-egl = { version = "6.0.0",features = ["static"] } +thiserror = "1.0.58" +tracing = "0.1.40" +tracing-subscriber = "0.3.18" +wayland-backend = { version = "0.3.3", features = ["client_system"] } +wayland-client = { version = "0.31.2" } +wayland-egl = { version = "0.32.0" } +wayland-protocols = { version = "0.31.2", features = ["client"] } +libwayshot={path="../.."} +libloading="0.8.4" \ No newline at end of file diff --git a/libwayshot/examples/waymirror-egl/README.md b/libwayshot/examples/waymirror-egl/README.md new file mode 100644 index 00000000..69d4fe45 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/README.md @@ -0,0 +1,8 @@ +# waymirror-egl +Example code for using the libwayshot DMA-BUF GL screencapture pipeline. + +This example sets up an EGL+OpenGL context, sets up libwayshot and renders the main display onto a rectangle after converting the screencapture into a texture. + +Adapted from https://github.com/Shinyzenith/wayland-egl-ctx, all credits to @Shinyzenith + +Run using `cargo run` inside this directory. diff --git a/libwayshot/examples/waymirror-egl/src/dispatch.rs b/libwayshot/examples/waymirror-egl/src/dispatch.rs new file mode 100644 index 00000000..2e3f38f8 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/src/dispatch.rs @@ -0,0 +1,131 @@ +use crate::state::WaylandEGLState; +use wayland_client::{ + delegate_noop, + protocol::{wl_compositor, wl_registry, wl_surface}, + Connection, Dispatch, QueueHandle, +}; +use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}; + +impl Dispatch for WaylandEGLState { + #[tracing::instrument(skip(registry, queue_handle, state), ret, level = "trace")] + fn event( + state: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _: &(), + _: &Connection, + queue_handle: &QueueHandle, + ) { + if let wl_registry::Event::Global { + name, + interface, + version, + } = event + { + match interface.as_str() { + "xdg_wm_base" => { + state.xdg_wm_base = Some(registry.bind::( + name, + version, + queue_handle, + (), + )); + } + "wl_compositor" => { + state.wl_compositor = Some(registry.bind::( + name, + version, + queue_handle, + (), + )); + } + _ => {} + } + } + } +} + +impl Dispatch for WaylandEGLState { + #[tracing::instrument(skip(xdg_wm_base), ret, level = "trace")] + fn event( + _: &mut Self, + xdg_wm_base: &xdg_wm_base::XdgWmBase, + event: xdg_wm_base::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let xdg_wm_base::Event::Ping { serial } = event { + xdg_wm_base.pong(serial); + } + } +} + +impl Dispatch for WaylandEGLState { + #[tracing::instrument(skip(xdg_surface), ret, level = "trace")] + fn event( + _: &mut Self, + xdg_surface: &xdg_surface::XdgSurface, + event: xdg_surface::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let xdg_surface::Event::Configure { serial } = event { + xdg_surface.ack_configure(serial); + } + } +} + +impl Dispatch for WaylandEGLState { + #[tracing::instrument(skip(), ret, level = "trace")] + fn event( + state: &mut Self, + _: &xdg_toplevel::XdgToplevel, + event: xdg_toplevel::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + xdg_toplevel::Event::Configure { width, height, .. } => { + if width == 0 || height == 0 { + return; // We do not respect this configure + } + + if state.width != width || state.height != height { + state.width = width; + state.height = height; + + state + .egl_window + .clone() + .unwrap() + .resize(state.width, state.height, 0, 0); + + unsafe { + gl::Viewport(0, 0, state.width, state.height); + } + state.wl_surface.clone().unwrap().commit(); + } + } + xdg_toplevel::Event::Close {} => { + state.running = false; + } + _ => {} + } + } +} + +impl Dispatch for WaylandEGLState { + fn event( + _state: &mut Self, + _proxy: &wl_surface::WlSurface, + _event: ::Event, + _data: &(), + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} +delegate_noop!(WaylandEGLState: wl_compositor::WlCompositor); diff --git a/libwayshot/examples/waymirror-egl/src/error.rs b/libwayshot/examples/waymirror-egl/src/error.rs new file mode 100644 index 00000000..1860cc85 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/src/error.rs @@ -0,0 +1,22 @@ +use std::result; +use thiserror::Error; + +pub type Result = result::Result; + +#[derive(Error, Debug)] +pub enum WaylandEGLStateError { + #[error("xdg_wm_base global missing")] + XdgWmBaseMissing, + + #[error("wl_compositor global missing")] + WlCompositorMissing, + + #[error("Shader compilation failed")] + GLShaderCompileFailed, + + #[error("Failed to create gl program")] + GLCreateProgramFailed, + + #[error("Failed to link gl program")] + GLLinkProgramFailed, +} diff --git a/libwayshot/examples/waymirror-egl/src/main.rs b/libwayshot/examples/waymirror-egl/src/main.rs new file mode 100644 index 00000000..969851a5 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/src/main.rs @@ -0,0 +1,64 @@ +mod dispatch; +mod error; +mod state; +mod utils; + +use error::Result; +use state::WaylandEGLState; + +pub fn main() -> Result<(), Box> { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .with_writer(std::io::stderr) + .init(); + + let mut state = WaylandEGLState::new()?; + let mut event_queue = state.wl_connection.new_event_queue(); + + let queue_handle = event_queue.handle(); + let _registry = state.wl_display.get_registry(&queue_handle, ()); + + event_queue.roundtrip(&mut state)?; + state.validate_globals()?; + + state.wl_surface = Some( + state + .wl_compositor + .as_ref() + .unwrap() + .create_surface(&queue_handle, ()), + ); + + state.xdg_surface = Some(state.xdg_wm_base.clone().unwrap().get_xdg_surface( + &state.wl_surface.clone().unwrap(), + &queue_handle, + (), + )); + state.xdg_toplevel = Some( + state + .xdg_surface + .clone() + .unwrap() + .get_toplevel(&queue_handle, ()), + ); + state + .xdg_toplevel + .clone() + .unwrap() + .set_title(state.title.clone()); + state.wl_surface.clone().unwrap().commit(); + + state.init_egl()?; + while state.running { + event_queue.dispatch_pending(&mut state)?; + state.draw(); + state + .egl + .swap_buffers(state.egl_display.unwrap(), state.egl_surface.unwrap())?; + + tracing::trace!("eglSwapBuffers called"); + } + state.deinit()?; + + Ok(()) +} diff --git a/libwayshot/examples/waymirror-egl/src/shaders/frag.glsl b/libwayshot/examples/waymirror-egl/src/shaders/frag.glsl new file mode 100644 index 00000000..8212a377 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/src/shaders/frag.glsl @@ -0,0 +1,11 @@ +#version 300 es + +precision mediump float; +out vec4 FragColor; +uniform sampler2D uTexture; +in vec2 vTexCoord; + +void main() { + vec4 color = texture(uTexture, vTexCoord); + FragColor = vec4 ( 1.0-color.r,1.0-color.g,1.0-color.b, 1.0 ); +} diff --git a/libwayshot/examples/waymirror-egl/src/shaders/vert.glsl b/libwayshot/examples/waymirror-egl/src/shaders/vert.glsl new file mode 100644 index 00000000..9b27c568 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/src/shaders/vert.glsl @@ -0,0 +1,11 @@ +#version 300 es +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec2 aTexCoord; + +out vec2 vTexCoord; + +void main() +{ + gl_Position = vec4(aPos, 1.0); + vTexCoord = vec2(aTexCoord.x, aTexCoord.y); +} \ No newline at end of file diff --git a/libwayshot/examples/waymirror-egl/src/state.rs b/libwayshot/examples/waymirror-egl/src/state.rs new file mode 100644 index 00000000..6b4a8022 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/src/state.rs @@ -0,0 +1,303 @@ +use crate::error::{Result, WaylandEGLStateError}; +use crate::utils::load_shader; + +use libwayshot::WayshotConnection; + +use gl::types::GLuint; +use khronos_egl::{self as egl}; +use std::{ffi::c_void, rc::Rc}; +use wayland_client::{ + protocol::{wl_compositor, wl_display::WlDisplay, wl_surface::WlSurface}, + ConnectError, Connection, Proxy, +}; +use wayland_egl::WlEglSurface; +use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}; + +#[derive(Debug)] +pub struct WaylandEGLState { + pub width: i32, + pub height: i32, + pub running: bool, + pub title: String, + + pub wl_connection: Connection, + pub wl_display: WlDisplay, + pub wl_surface: Option, + + pub egl: egl::Instance, + pub egl_window: Option>, + pub egl_display: Option, + pub egl_surface: Option, + pub egl_context: Option, + + pub gl_program: GLuint, + pub gl_texture: GLuint, + + pub xdg_wm_base: Option, + pub xdg_surface: Option, + pub xdg_toplevel: Option, + pub wl_compositor: Option, + + wayshot: WayshotConnection, +} + +impl WaylandEGLState { + #[tracing::instrument] + pub fn new() -> Result { + let server_connection = Connection::connect_to_env()?; + + Ok(Self { + width: 1920, + height: 1080, + running: true, + title: "Waymirror-EGL".into(), + + wl_connection: server_connection.clone(), + wl_display: server_connection.display(), + wl_surface: None, + + egl: khronos_egl::Instance::new(egl::Static), + egl_window: None, + egl_display: None, + egl_surface: None, + egl_context: None, + gl_program: 0, + gl_texture: 0, + + xdg_wm_base: None, + xdg_surface: None, + xdg_toplevel: None, + wl_compositor: None, + wayshot: WayshotConnection::from_connection_with_dmabuf( + server_connection, + "/dev/dri/renderD128", + ) + .unwrap(), + }) + } + + pub fn deinit(&self) -> Result<(), Box> { + unsafe { + gl::DeleteProgram(self.gl_program); + } + + self.egl + .destroy_surface(self.egl_display.unwrap(), self.egl_surface.unwrap())?; + self.egl + .destroy_context(self.egl_display.unwrap(), self.egl_context.unwrap())?; + + self.xdg_surface.clone().unwrap().destroy(); + self.wl_surface.clone().unwrap().destroy(); + + Ok(()) + } + + pub fn init_egl(&mut self) -> Result<(), Box> { + // Init gl + gl_loader::init_gl(); + gl::load_with(|s| gl_loader::get_proc_address(s) as *const _); + + self.egl_window = Some(Rc::new(WlEglSurface::new( + self.wl_surface.clone().unwrap().id(), + self.width, + self.height, + )?)); + + self.egl_display = Some( + unsafe { + self.egl + .get_display(self.wl_display.id().as_ptr() as *mut c_void) + } + .unwrap(), + ); + + self.egl.initialize(self.egl_display.unwrap())?; + + let attributes = [ + egl::SURFACE_TYPE, + egl::WINDOW_BIT, + egl::RENDERABLE_TYPE, + egl::OPENGL_ES2_BIT, + egl::RED_SIZE, + 8, + egl::GREEN_SIZE, + 8, + egl::BLUE_SIZE, + 8, + egl::NONE, + ]; + + let config = self + .egl + .choose_first_config(self.egl_display.unwrap(), &attributes)? + .expect("unable to find an appropriate EGL configuration"); + self.egl_surface = Some(unsafe { + self.egl.create_window_surface( + self.egl_display.unwrap(), + config, + self.egl_window.clone().unwrap().ptr() as egl::NativeWindowType, + None, + )? + }); + + let context_attributes = [egl::CONTEXT_CLIENT_VERSION, 2, egl::NONE]; + self.egl_context = Some(self.egl.create_context( + self.egl_display.unwrap(), + config, + None, + &context_attributes, + )?); + + self.egl.make_current( + self.egl_display.unwrap(), + self.egl_surface, + self.egl_surface, + self.egl_context, + )?; + + self.init_program()?; + + Ok(()) + } + + fn init_program(&mut self) -> Result<()> { + let vert_shader = load_shader( + gl::VERTEX_SHADER, + include_str!("./shaders/vert.glsl").into(), + ) + .unwrap(); + + let frag_shader = load_shader( + gl::FRAGMENT_SHADER, + include_str!("./shaders/frag.glsl").into(), + ) + .unwrap(); + + unsafe { + self.gl_program = gl::CreateProgram(); + } + + if self.gl_program == 0 { + tracing::event!(tracing::Level::ERROR, "glCreateProgramFailed!"); + return Err(WaylandEGLStateError::GLCreateProgramFailed); + } + + unsafe { + gl::AttachShader(self.gl_program, vert_shader); + gl::AttachShader(self.gl_program, frag_shader); + + gl::LinkProgram(self.gl_program); + } + + let mut linked: gl::types::GLint = 1; + unsafe { gl::GetProgramiv(self.gl_program, gl::LINK_STATUS, &mut linked as *mut i32) } + + if linked > 0 { + tracing::event!(tracing::Level::INFO, "Successfully linked the program!"); + } else { + return Err(WaylandEGLStateError::GLLinkProgramFailed); + } + + let vertices: [gl::types::GLfloat; 20] = [ + // positions // texture coords + 1.0, 1.0, 0.0, 1.0, 0.0, // top right + 1.0, -1.0, 0.0, 1.0, 1.0, // bottom right + -1.0, -1.0, 0.0, 0.0, 1.0, // bottom left + -1.0, 1.0, 0.0, 0.0, 0.0, // top left + ]; + let indices: [gl::types::GLint; 6] = [ + 0, 1, 3, // first Triangle + 1, 2, 3, // second Triangle + ]; + let mut vbo: GLuint = 0; + let mut vao: GLuint = 0; + let mut ebo: GLuint = 0; + + unsafe { + gl::GenTextures(1, &mut self.gl_texture); + + self.dmabuf_to_texture(); + + gl::GenVertexArrays(1, &mut vao as *mut u32); + gl::GenBuffers(1, &mut vbo as *mut u32); + gl::GenBuffers(1, &mut ebo as *mut u32); + gl::BindVertexArray(vao); + + gl::BindBuffer(gl::ARRAY_BUFFER, vbo); + gl::BufferData( + gl::ARRAY_BUFFER, + (vertices.len() * std::mem::size_of::()) + as gl::types::GLsizeiptr, + &vertices[0] as *const f32 as *const c_void, + gl::STATIC_DRAW, + ); + + gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, ebo); + gl::BufferData( + gl::ELEMENT_ARRAY_BUFFER, + (indices.len() * std::mem::size_of::()) + as gl::types::GLsizeiptr, + &indices[0] as *const i32 as *const c_void, + gl::STATIC_DRAW, + ); + + gl::VertexAttribPointer( + 0, + 3, + gl::FLOAT, + gl::FALSE, + 5 * std::mem::size_of::() as gl::types::GLint, + 0 as *const c_void, + ); + gl::EnableVertexAttribArray(0); + + gl::VertexAttribPointer( + 1, + 2, + gl::FLOAT, + gl::FALSE, + 5 * std::mem::size_of::() as gl::types::GLint, + (3 * std::mem::size_of::()) as *const c_void, + ); + gl::EnableVertexAttribArray(1); + gl::BindBuffer(gl::ARRAY_BUFFER, 0); + } + Ok(()) + } + + pub fn draw(&mut self) { + unsafe { + gl::ClearColor(1.0, 1.0, 0.0, 1.0); + gl::Clear(gl::COLOR_BUFFER_BIT); + // gl::DeleteTextures(1, &mut self.gl_texture); + + gl::UseProgram(self.gl_program); + gl::DrawElements(gl::TRIANGLES, 6, gl::UNSIGNED_INT, 0 as *const c_void); + } + } + + pub fn dmabuf_to_texture(&self) { + unsafe { + gl::BindTexture(gl::TEXTURE_2D, self.gl_texture); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as i32); + + self.wayshot + .bind_output_frame_to_gl_texture( + true, + &self.wayshot.get_all_outputs()[0].wl_output, + None, + ) + .unwrap(); + } + } + + pub fn validate_globals(&self) -> Result<()> { + if self.xdg_wm_base.is_none() { + return Err(WaylandEGLStateError::XdgWmBaseMissing); + } else if self.wl_compositor.is_none() { + return Err(WaylandEGLStateError::WlCompositorMissing); + } + + Ok(()) + } +} diff --git a/libwayshot/examples/waymirror-egl/src/utils.rs b/libwayshot/examples/waymirror-egl/src/utils.rs new file mode 100644 index 00000000..e532e2d3 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/src/utils.rs @@ -0,0 +1,31 @@ +use crate::error::{Result, WaylandEGLStateError}; +use gl::types::{GLenum, GLint, GLuint}; +use std::{ffi::CString, ptr}; + +pub fn load_shader(shader_type: GLenum, src: String) -> Result { + unsafe { + let shader: GLuint = gl::CreateShader(shader_type); + + if shader == 0 { + return Err(WaylandEGLStateError::GLShaderCompileFailed); + } + + let src_c_str = CString::new(src.as_bytes()).unwrap(); + gl::ShaderSource(shader, 1, &src_c_str.as_ptr(), ptr::null()); + + //gl::ShaderSource(shader, 1, &src_c_str.as_ptr(), std::ptr::null()); + + gl::CompileShader(shader); + + let mut status: GLint = 1; + gl::GetShaderiv(shader, gl::COMPILE_STATUS, &mut status as *mut i32); + + if status > 0 { + tracing::event!(tracing::Level::INFO, "Shader compile successfull!",); + } else { + return Err(WaylandEGLStateError::GLShaderCompileFailed); + } + + Ok(shader) + } +} diff --git a/libwayshot/examples/waymirror.rs b/libwayshot/examples/waymirror.rs new file mode 100644 index 00000000..c350131a --- /dev/null +++ b/libwayshot/examples/waymirror.rs @@ -0,0 +1,205 @@ +use libwayshot::WayshotConnection; +use wayland_client::{ + delegate_noop, + protocol::{ + wl_buffer::{self}, + wl_compositor, wl_keyboard, wl_registry, wl_seat, wl_shm, wl_shm_pool, wl_surface, + }, + Connection, Dispatch, QueueHandle, WEnum, +}; + +use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}; + +fn main() { + let conn = Connection::connect_to_env().unwrap(); + + let mut event_queue = conn.new_event_queue(); + let qhandle = event_queue.handle(); + + let display = conn.display(); + display.get_registry(&qhandle, ()); + let wayshot = + WayshotConnection::from_connection_with_dmabuf(conn, "/dev/dri/renderD128").unwrap(); + + let mut state = State { + wayshot, + running: true, + base_surface: None, + wm_base: None, + xdg_surface: None, + configured: false, + }; + + println!("Starting the example wayshot dmabuf demo app, press to quit."); + + while state.running { + event_queue.blocking_dispatch(&mut state).unwrap(); + } +} + +struct State { + wayshot: WayshotConnection, + running: bool, + base_surface: Option, + wm_base: Option, + xdg_surface: Option<(xdg_surface::XdgSurface, xdg_toplevel::XdgToplevel)>, + configured: bool, +} + +impl Dispatch for State { + fn event( + state: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _: &(), + _: &Connection, + qh: &QueueHandle, + ) { + if let wl_registry::Event::Global { + name, interface, .. + } = event + { + match &interface[..] { + "wl_compositor" => { + let compositor = + registry.bind::(name, 1, qh, ()); + let surface = compositor.create_surface(qh, ()); + state.base_surface = Some(surface); + + if state.wm_base.is_some() && state.xdg_surface.is_none() { + state.init_xdg_surface(qh); + } + } + "wl_seat" => { + registry.bind::(name, 1, qh, ()); + } + "xdg_wm_base" => { + let wm_base = registry.bind::(name, 1, qh, ()); + state.wm_base = Some(wm_base); + + if state.base_surface.is_some() && state.xdg_surface.is_none() { + state.init_xdg_surface(qh); + } + } + _ => {} + } + } + } +} + +// Ignore events from these object types in this example. +delegate_noop!(State: ignore wl_compositor::WlCompositor); +delegate_noop!(State: ignore wl_surface::WlSurface); +delegate_noop!(State: ignore wl_shm::WlShm); +delegate_noop!(State: ignore wl_shm_pool::WlShmPool); +delegate_noop!(State: ignore wl_buffer::WlBuffer); + +impl State { + fn init_xdg_surface(&mut self, qh: &QueueHandle) { + let wm_base = self.wm_base.as_ref().unwrap(); + let base_surface = self.base_surface.as_ref().unwrap(); + + let xdg_surface = wm_base.get_xdg_surface(base_surface, qh, ()); + let toplevel = xdg_surface.get_toplevel(qh, ()); + toplevel.set_title("DMABuf+wlr-screencpy example!".into()); + + base_surface.commit(); + + self.xdg_surface = Some((xdg_surface, toplevel)); + } +} + +impl Dispatch for State { + fn event( + _: &mut Self, + wm_base: &xdg_wm_base::XdgWmBase, + event: xdg_wm_base::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let xdg_wm_base::Event::Ping { serial } = event { + wm_base.pong(serial); + } + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + xdg_surface: &xdg_surface::XdgSurface, + event: xdg_surface::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let xdg_surface::Event::Configure { serial, .. } = event { + xdg_surface.ack_configure(serial); + state.configured = true; + let surface = state.base_surface.as_ref().unwrap(); + let (_frame_format, guard, _bo) = state + .wayshot + .capture_output_frame_dmabuf( + true, + &state.wayshot.get_all_outputs()[0].wl_output, + None, + ) + .unwrap(); + surface.attach(Some(&guard.buffer), 0, 0); + surface.commit(); + } + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + _: &xdg_toplevel::XdgToplevel, + event: xdg_toplevel::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let xdg_toplevel::Event::Close {} = event { + state.running = false; + } + } +} + +impl Dispatch for State { + fn event( + _: &mut Self, + seat: &wl_seat::WlSeat, + event: wl_seat::Event, + _: &(), + _: &Connection, + qh: &QueueHandle, + ) { + if let wl_seat::Event::Capabilities { + capabilities: WEnum::Value(capabilities), + } = event + { + if capabilities.contains(wl_seat::Capability::Keyboard) { + seat.get_keyboard(qh, ()); + } + } + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + _: &wl_keyboard::WlKeyboard, + event: wl_keyboard::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let wl_keyboard::Event::Key { key, .. } = event { + if key == 1 { + // ESC key + state.running = false; + } + } + } +} diff --git a/libwayshot/src/dispatch.rs b/libwayshot/src/dispatch.rs index c8f21fcf..e171cd1b 100644 --- a/libwayshot/src/dispatch.rs +++ b/libwayshot/src/dispatch.rs @@ -1,20 +1,32 @@ use std::{ collections::HashSet, + os::fd::{AsFd, BorrowedFd}, sync::atomic::{AtomicBool, Ordering}, }; use wayland_client::{ delegate_noop, globals::GlobalListContents, protocol::{ - wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_output, wl_output::WlOutput, - wl_registry, wl_registry::WlRegistry, wl_shm::WlShm, wl_shm_pool::WlShmPool, + wl_buffer::WlBuffer, + wl_compositor::WlCompositor, + wl_output::{self, WlOutput}, + wl_registry::{self, WlRegistry}, + wl_shm::WlShm, + wl_shm_pool::WlShmPool, wl_surface::WlSurface, }, - Connection, Dispatch, QueueHandle, WEnum, - WEnum::Value, + Connection, Dispatch, QueueHandle, + WEnum::{self, Value}, }; -use wayland_protocols::xdg::xdg_output::zv1::client::{ - zxdg_output_manager_v1::ZxdgOutputManagerV1, zxdg_output_v1, zxdg_output_v1::ZxdgOutputV1, +use wayland_protocols::{ + wp::linux_dmabuf::zv1::client::{ + zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1}, + zwp_linux_dmabuf_v1::{self, ZwpLinuxDmabufV1}, + }, + xdg::xdg_output::zv1::client::{ + zxdg_output_manager_v1::ZxdgOutputManagerV1, + zxdg_output_v1::{self, ZxdgOutputV1}, + }, }; use wayland_protocols_wlr::layer_shell::v1::client::{ zwlr_layer_shell_v1::ZwlrLayerShellV1, @@ -28,7 +40,7 @@ use wayland_protocols_wlr::screencopy::v1::client::{ use crate::{ output::OutputInfo, region::{LogicalRegion, Position, Size}, - screencopy::FrameFormat, + screencopy::{DMAFrameFormat, FrameFormat}, }; #[derive(Debug)] @@ -169,10 +181,35 @@ pub enum FrameState { pub struct CaptureFrameState { pub formats: Vec, + pub dmabuf_formats: Vec, pub state: Option, pub buffer_done: AtomicBool, } +impl Dispatch for CaptureFrameState { + fn event( + _frame: &mut Self, + _proxy: &ZwpLinuxDmabufV1, + _event: zwp_linux_dmabuf_v1::Event, + _data: &(), + _conn: &Connection, + _qhandle: &wayland_client::QueueHandle, + ) { + } +} + +impl Dispatch for CaptureFrameState { + fn event( + _state: &mut Self, + _proxy: &ZwpLinuxBufferParamsV1, + _event: zwp_linux_buffer_params_v1::Event, + _data: &(), + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + impl Dispatch for CaptureFrameState { #[tracing::instrument(skip(frame), ret, level = "trace")] fn event( @@ -191,6 +228,7 @@ impl Dispatch for CaptureFrameState { stride, } => { if let Value(f) = format { + tracing::debug!("Received Buffer event with format: {f:?}"); frame.formats.push(FrameFormat { format: f, size: Size { width, height }, @@ -209,7 +247,19 @@ impl Dispatch for CaptureFrameState { frame.state.replace(FrameState::Failed); } zwlr_screencopy_frame_v1::Event::Damage { .. } => {} - zwlr_screencopy_frame_v1::Event::LinuxDmabuf { .. } => {} + zwlr_screencopy_frame_v1::Event::LinuxDmabuf { + format, + width, + height, + } => { + tracing::debug!( + "Received wlr-screencopy linux_dmabuf event with format: {format} and size {width}x{height}" + ); + frame.dmabuf_formats.push(DMAFrameFormat { + format, + size: Size { width, height }, + }); + } zwlr_screencopy_frame_v1::Event::BufferDone => { frame.buffer_done.store(true, Ordering::SeqCst); } @@ -226,7 +276,7 @@ delegate_noop!(CaptureFrameState: ignore ZwlrScreencopyManagerV1); // TODO: Create a xdg-shell surface, check for the enter event, grab the output from it. pub struct WayshotState {} - +delegate_noop!(WayshotState: ignore ZwpLinuxDmabufV1); impl wayland_client::Dispatch for WayshotState { fn event( _: &mut WayshotState, @@ -278,3 +328,28 @@ impl wayland_client::Dispatch for LayerShellState } } } +pub(crate) struct Card(std::fs::File); + +/// Implementing [`AsFd`] is a prerequisite to implementing the traits found +/// in this crate. Here, we are just calling [`File::as_fd()`] on the inner +/// [`File`]. +impl AsFd for Card { + fn as_fd(&self) -> BorrowedFd<'_> { + self.0.as_fd() + } +} +impl drm::Device for Card {} +/// Simple helper methods for opening a `Card`. +impl Card { + pub fn open(path: &str) -> Self { + let mut options = std::fs::OpenOptions::new(); + options.read(true); + options.write(true); + Card(options.open(path).unwrap()) + } +} +#[derive(Debug)] +pub(crate) struct DMABUFState { + pub linux_dmabuf: ZwpLinuxDmabufV1, + pub gbmdev: gbm::Device, +} diff --git a/libwayshot/src/error.rs b/libwayshot/src/error.rs index f3ad8523..10d87b4f 100644 --- a/libwayshot/src/error.rs +++ b/libwayshot/src/error.rs @@ -1,5 +1,7 @@ use std::{io, result}; +use drm::buffer::UnrecognizedFourcc; +use gbm::{DeviceDestroyedError, FdError}; use thiserror::Error; use wayland_client::{ globals::{BindError, GlobalError}, @@ -34,4 +36,16 @@ pub enum Error { ProtocolNotFound(String), #[error("error occurred in freeze callback")] FreezeCallbackError, + #[error("dmabuf configuration not initialized. Did you not use Wayshot::from_connection_with_dmabuf()?")] + NoDMAStateError, + #[error("dmabuf color format provided by compositor is invalid")] + UnrecognizedColorCode(#[from] UnrecognizedFourcc), + #[error("dmabuf device has been destroyed")] + DRMDeviceLost(#[from] DeviceDestroyedError), + #[error("obtaining gbm buffer object file descriptor failed {0}")] + GBMBoFdError(#[from] FdError), + #[error(" EGLImage import from dmabuf failed: {0}")] + EGLError(#[from] khronos_egl::Error), + #[error("No EGLImageTargetTexture2DOES function located, this extension may not be supported")] + EGLImageToTexProcNotFoundError, } diff --git a/libwayshot/src/lib.rs b/libwayshot/src/lib.rs index 9c52a811..04b3438d 100644 --- a/libwayshot/src/lib.rs +++ b/libwayshot/src/lib.rs @@ -13,17 +13,19 @@ mod screencopy; use std::{ collections::HashSet, + ffi::c_void, fs::File, - os::fd::AsFd, + os::fd::{AsFd, IntoRawFd, OwnedFd}, sync::atomic::{AtomicBool, Ordering}, thread, }; -use dispatch::LayerShellState; +use dispatch::{DMABUFState, LayerShellState}; use image::{imageops::replace, DynamicImage}; +use khronos_egl::{self as egl, Instance}; use memmap2::MmapMut; use region::{EmbeddedRegion, RegionCapturer}; -use screencopy::FrameGuard; +use screencopy::{DMAFrameFormat, DMAFrameGuard, EGLImageGuard, FrameData, FrameGuard}; use tracing::debug; use wayland_client::{ globals::{registry_queue_init, GlobalList}, @@ -32,10 +34,15 @@ use wayland_client::{ wl_output::{Transform, WlOutput}, wl_shm::{self, WlShm}, }, - Connection, EventQueue, + Connection, EventQueue, Proxy, }; -use wayland_protocols::xdg::xdg_output::zv1::client::{ - zxdg_output_manager_v1::ZxdgOutputManagerV1, zxdg_output_v1::ZxdgOutputV1, +use wayland_protocols::{ + wp::linux_dmabuf::zv1::client::{ + zwp_linux_buffer_params_v1, zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, + }, + xdg::xdg_output::zv1::client::{ + zxdg_output_manager_v1::ZxdgOutputManagerV1, zxdg_output_v1::ZxdgOutputV1, + }, }; use wayland_protocols_wlr::{ layer_shell::v1::client::{ @@ -62,6 +69,7 @@ pub mod reexport { use wayland_client::protocol::wl_output; pub use wl_output::{Transform, WlOutput}; } +use gbm::{BufferObject, BufferObjectFlags, Device as GBMDevice}; /// Struct to store wayland connection and globals list. /// # Example usage @@ -75,6 +83,7 @@ pub struct WayshotConnection { pub conn: Connection, pub globals: GlobalList, output_infos: Vec, + dmabuf_state: Option, } impl WayshotConnection { @@ -92,6 +101,34 @@ impl WayshotConnection { conn, globals, output_infos: Vec::new(), + dmabuf_state: None, + }; + + initial_state.refresh_outputs()?; + + Ok(initial_state) + } + + ///Create a WayshotConnection struct having DMA-BUF support + /// Using this connection is required to make use of the dmabuf functions + ///# Parameters + /// - conn: a Wayland connection + /// - device_path: string pointing to the DRI device that is to be used for creating the DMA-BUFs on. For example: "/dev/dri/renderD128" + pub fn from_connection_with_dmabuf(conn: Connection, device_path: &str) -> Result { + let (globals, evq) = registry_queue_init::(&conn)?; + let linux_dmabuf = + globals.bind(&evq.handle(), 4..=ZwpLinuxDmabufV1::interface().version, ())?; + let gpu = dispatch::Card::open(device_path); + // init a GBM device + let gbm = GBMDevice::new(gpu).unwrap(); + let mut initial_state = Self { + conn, + globals, + output_infos: Vec::new(), + dmabuf_state: Some(DMABUFState { + linux_dmabuf, + gbmdev: gbm, + }), }; initial_state.refresh_outputs()?; @@ -166,14 +203,249 @@ impl WayshotConnection { capture_region: Option, ) -> Result<(FrameFormat, FrameGuard)> { let (state, event_queue, frame, frame_format) = - self.capture_output_frame_get_state(cursor_overlay, output, capture_region)?; + self.capture_output_frame_get_state_shm(cursor_overlay, output, capture_region)?; let frame_guard = self.capture_output_frame_inner(state, event_queue, frame, frame_format, fd)?; Ok((frame_format, frame_guard)) } - fn capture_output_frame_get_state( + fn capture_output_frame_shm_from_file( + &self, + cursor_overlay: bool, + output: &WlOutput, + file: &File, + capture_region: Option, + ) -> Result<(FrameFormat, FrameGuard)> { + let (state, event_queue, frame, frame_format) = + self.capture_output_frame_get_state_shm(cursor_overlay as i32, output, capture_region)?; + + file.set_len(frame_format.byte_size())?; + + let frame_guard = + self.capture_output_frame_inner(state, event_queue, frame, frame_format, file)?; + + Ok((frame_format, frame_guard)) + } + /// Helper function/wrapper that uses the OpenGL extension OES_EGL_image to convert the EGLImage obtained from [`WayshotConnection::capture_output_frame_eglimage`] + /// into a OpenGL texture. + /// - The caller is supposed to setup everything required for the texture binding. An example call may look like: + /// ``` + /// gl::BindTexture(gl::TEXTURE_2D, self.gl_texture); + /// gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as i32); + /// wayshot_conn + /// .bind_output_frame_to_gl_texture( + /// true, + /// &wayshot_conn.get_all_outputs()[0].wl_output, + /// None) + ///``` + /// # Parameters + /// - `cursor_overlay`: A boolean flag indicating whether the cursor should be included in the capture. + /// - `output`: Reference to the `WlOutput` from which the frame is to be captured. + /// - `capture_region`: Optional region specifying a sub-area of the output to capture. If `None`, the entire output is captured. + /// # Returns + /// - If the function was found and called, an OK(()), note that this does not neccesarily mean that binding was successful, only that the function was called. + /// The caller may check for any OpenGL errors using the standard routes. + /// - If the function was not found, [`Error::EGLImageToTexProcNotFoundError`] is returned + pub unsafe fn bind_output_frame_to_gl_texture( + &self, + cursor_overlay: bool, + output: &WlOutput, + capture_region: Option, + ) -> Result<()> { + let egl = khronos_egl::Instance::new(egl::Static); + let eglimage_guard = + self.capture_output_frame_eglimage(&egl, cursor_overlay, output, capture_region)?; + unsafe { + let gl_egl_image_texture_target_2d_oes: unsafe extern "system" fn( + target: gl::types::GLenum, + image: gl::types::GLeglImageOES, + ) -> () = + std::mem::transmute(match egl.get_proc_address("glEGLImageTargetTexture2DOES") { + Some(f) => { + tracing::debug!("glEGLImageTargetTexture2DOES found at address {:#?}", f); + f + } + None => { + tracing::error!("glEGLImageTargetTexture2DOES not found"); + return Err(Error::EGLImageToTexProcNotFoundError); + } + }); + + gl_egl_image_texture_target_2d_oes(gl::TEXTURE_2D, eglimage_guard.image.as_ptr()); + tracing::trace!("glEGLImageTargetTexture2DOES called"); + Ok(()) + } + } + + /// Obtain a screencapture in the form of a EGLImage. + /// The display on which this image is created is obtained from the Wayland Connection. + /// Uses the dma-buf provisions of the wlr-screencopy copy protocol to avoid VRAM->RAM copies + /// It returns the captured frame as an `EGLImage`, wrapped in an `EGLImageGuard` + /// for safe handling and cleanup. + /// # Parameters + /// - `egl_instance`: Reference to an egl API instance obtained from the khronos_egl crate, which is used to create the `EGLImage`. + /// - `cursor_overlay`: A boolean flag indicating whether the cursor should be included in the capture. + /// - `output`: Reference to the `WlOutput` from which the frame is to be captured. + /// - `capture_region`: Optional region specifying a sub-area of the output to capture. If `None`, the entire output is captured. + /// + /// # Returns + /// If successful, an EGLImageGuard which contains a pointer 'image' to the created EGLImage + /// On error, the EGL [error code](https://registry.khronos.org/EGL/sdk/docs/man/html/eglGetError.xhtml) is returned via this crates Error type + pub fn capture_output_frame_eglimage<'a, T: khronos_egl::api::EGL1_5>( + &self, + egl_instance: &'a Instance, + cursor_overlay: bool, + output: &WlOutput, + capture_region: Option, + ) -> Result> { + let egl_display = unsafe { + match egl_instance.get_display(self.conn.display().id().as_ptr() as *mut c_void) { + Some(disp) => disp, + None => return Err(egl_instance.get_error().unwrap().into()), + } + }; + tracing::trace!("eglDisplay obtained from Wayland connection's display"); + + egl_instance.initialize(egl_display)?; + self.capture_output_frame_eglimage_on_display( + &egl_instance, + egl_display, + cursor_overlay, + output, + capture_region, + ) + } + + /// Obtain a screencapture in the form of a EGLImage on the given EGLDisplay. + /// + /// Uses the dma-buf provisions of the wlr-screencopy copy protocol to avoid VRAM->RAM copies + /// It returns the captured frame as an `EGLImage`, wrapped in an `EGLImageGuard` + /// for safe handling and cleanup. + /// # Parameters + /// - `egl_instance`: Reference to an `EGL1_5` instance, which is used to create the `EGLImage`. + /// - `egl_display`: The `EGLDisplay` on which the image should be created. + /// - `cursor_overlay`: A boolean flag indicating whether the cursor should be included in the capture. + /// - `output`: Reference to the `WlOutput` from which the frame is to be captured. + /// - `capture_region`: Optional region specifying a sub-area of the output to capture. If `None`, the entire output is captured. + /// + /// # Returns + /// If successful, an EGLImageGuard which contains a pointer 'image' to the created EGLImage + /// On error, the EGL [error code](https://registry.khronos.org/EGL/sdk/docs/man/html/eglGetError.xhtml) is returned via this crates Error type + pub fn capture_output_frame_eglimage_on_display<'a, T: khronos_egl::api::EGL1_5>( + &self, + egl_instance: &'a Instance, + egl_display: egl::Display, + cursor_overlay: bool, + output: &WlOutput, + capture_region: Option, + ) -> Result> { + type Attrib = egl::Attrib; + let (frame_format, _guard, bo) = + self.capture_output_frame_dmabuf(cursor_overlay, output, capture_region)?; + let modifier: u64 = bo.modifier()?.into(); + let image_attribs = [ + egl::WIDTH as Attrib, + frame_format.size.width as Attrib, + egl::HEIGHT as Attrib, + frame_format.size.height as Attrib, + 0x3271, //EGL_LINUX_DRM_FOURCC_EXT + bo.format().unwrap() as Attrib, + 0x3272, //EGL_DMA_BUF_PLANE0_FD_EXT + bo.fd_for_plane(0).unwrap().into_raw_fd() as Attrib, + 0x3273, //EGL_DMA_BUF_PLANE0_OFFSET_EXT + bo.offset(0).unwrap() as Attrib, + 0x3274, //EGL_DMA_BUF_PLANE0_PITCH_EXT + bo.stride_for_plane(0).unwrap() as Attrib, + 0x3443, //EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT + (modifier as u32) as Attrib, + 0x3444, //EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT + (modifier >> 32) as Attrib, + egl::ATTRIB_NONE as Attrib, + ]; + tracing::debug!( + "Calling eglCreateImage with attributes: {:#?}", + image_attribs + ); + unsafe { + match egl_instance.create_image( + egl_display, + khronos_egl::Context::from_ptr(egl::NO_CONTEXT), + 0x3270, // EGL_LINUX_DMA_BUF_EXT + khronos_egl::ClientBuffer::from_ptr(std::ptr::null_mut()), //NULL + &image_attribs, + ) { + Ok(image) => Ok(EGLImageGuard { + image, + egl_instance, + egl_display, + }), + Err(e) => { + tracing::error!("eglCreateImage call failed with error {e}"); + Err(e.into()) + } + } + } + } + + /// Obtain a screencapture in the form of a WlBuffer backed by a GBM Bufferobject on the GPU. + /// Uses the dma-buf provisions of the wlr-screencopy copy protocol to avoid VRAM->RAM copies + /// The captured frame is returned as a tuple containing the frame format, a guard to manage + /// the WlBuffer's cleanup on drop, and the underlying `BufferObject`. + /// - `cursor_overlay`: A boolean flag indicating whether the cursor should be included in the capture. + /// - `output`: Reference to the `WlOutput` from which the frame is to be captured. + /// - `capture_region`: Optional region specifying a sub-area of the output to capture. If `None`, the entire output is captured. + ///# Returns + /// On success, returns a tuple containing the frame format, + /// a guard to manage the frame's lifecycle, and the GPU-backed `BufferObject`. + /// # Errors + /// - Returns `NoDMAStateError` if the DMA-BUF state is not initialized a the time of initialization of this struct. + pub fn capture_output_frame_dmabuf( + &self, + cursor_overlay: bool, + output: &WlOutput, + capture_region: Option, + ) -> Result<(DMAFrameFormat, DMAFrameGuard, BufferObject<()>)> { + match &self.dmabuf_state { + Some(dmabuf_state) => { + let (state, event_queue, frame, frame_format) = self + .capture_output_frame_get_state_dmabuf( + cursor_overlay as i32, + output, + capture_region, + )?; + let gbm = &dmabuf_state.gbmdev; + let bo = gbm.create_buffer_object::<()>( + frame_format.size.width, + frame_format.size.height, + gbm::Format::try_from(frame_format.format)?, + BufferObjectFlags::RENDERING | BufferObjectFlags::LINEAR, + )?; + + let stride = bo.stride()?; + let modifier: u64 = bo.modifier()?.into(); + tracing::debug!( + "Created GBM Buffer object with input frame format {:#?}, stride {:#?} and modifier {:#?} ", + frame_format, + stride,modifier + ); + let frame_guard = self.capture_output_frame_inner_dmabuf( + state, + event_queue, + frame, + frame_format, + stride, + modifier, + bo.fd_for_plane(0)?, + )?; + + Ok((frame_format, frame_guard, bo)) + } + None => Err(Error::NoDMAStateError), + } + } + + fn capture_output_frame_get_state_shm( &self, cursor_overlay: i32, output: &WlOutput, @@ -186,6 +458,7 @@ impl WayshotConnection { )> { let mut state = CaptureFrameState { formats: Vec::new(), + dmabuf_formats: Vec::new(), state: None, buffer_done: AtomicBool::new(false), }; @@ -208,7 +481,7 @@ impl WayshotConnection { } }; - debug!("Capturing output..."); + tracing::debug!("Capturing output(shm buffer)..."); let frame = if let Some(embedded_region) = capture_region { screencopy_manager.capture_output_region( cursor_overlay, @@ -263,6 +536,144 @@ impl WayshotConnection { Ok((state, event_queue, frame, frame_format)) } + fn capture_output_frame_get_state_dmabuf( + &self, + cursor_overlay: i32, + output: &WlOutput, + capture_region: Option, + ) -> Result<( + CaptureFrameState, + EventQueue, + ZwlrScreencopyFrameV1, + DMAFrameFormat, + )> { + let mut state = CaptureFrameState { + formats: Vec::new(), + dmabuf_formats: Vec::new(), + state: None, + buffer_done: AtomicBool::new(false), + }; + let mut event_queue = self.conn.new_event_queue::(); + let qh = event_queue.handle(); + + // Instantiating screencopy manager. + let screencopy_manager = match self.globals.bind::( + &qh, + 3..=3, + (), + ) { + Ok(x) => x, + Err(e) => { + tracing::error!("Failed to create screencopy manager. Does your compositor implement ZwlrScreencopy?"); + tracing::error!("err: {e}"); + return Err(Error::ProtocolNotFound( + "ZwlrScreencopy Manager not found".to_string(), + )); + } + }; + + tracing::debug!("Capturing output for DMA-BUF API..."); + let frame = if let Some(embedded_region) = capture_region { + screencopy_manager.capture_output_region( + cursor_overlay, + output, + embedded_region.inner.position.x, + embedded_region.inner.position.y, + embedded_region.inner.size.width as i32, + embedded_region.inner.size.height as i32, + &qh, + (), + ) + } else { + screencopy_manager.capture_output(cursor_overlay, output, &qh, ()) + }; + + // Empty internal event buffer until buffer_done is set to true which is when the Buffer done + // event is fired, aka the capture from the compositor is succesful. + while !state.buffer_done.load(Ordering::SeqCst) { + event_queue.blocking_dispatch(&mut state)?; + } + + tracing::trace!( + "Received compositor frame buffer formats: {:#?}", + state.formats + ); + // TODO select appropriate format if there is more than one + let frame_format = state.dmabuf_formats[0]; + tracing::trace!("Selected frame buffer format: {:#?}", frame_format); + + Ok((state, event_queue, frame, frame_format)) + } + + fn capture_output_frame_inner_dmabuf( + &self, + mut state: CaptureFrameState, + mut event_queue: EventQueue, + frame: ZwlrScreencopyFrameV1, + frame_format: DMAFrameFormat, + stride: u32, + modifier: u64, + fd: OwnedFd, + ) -> Result { + match &self.dmabuf_state { + Some(dmabuf_state) => { + // Connecting to wayland environment. + let qh = event_queue.handle(); + + let linux_dmabuf = &dmabuf_state.linux_dmabuf; + let dma_width = frame_format.size.width; + let dma_height = frame_format.size.height; + + let dma_params = linux_dmabuf.create_params(&qh, ()); + + dma_params.add( + fd.as_fd(), + 0, + 0, + stride, + (modifier >> 32) as u32, + (modifier & 0xffffffff) as u32, + ); + tracing::trace!("Called ZwpLinuxBufferParamsV1::create_params "); + let dmabuf_wlbuf = dma_params.create_immed( + dma_width as i32, + dma_height as i32, + frame_format.format, + zwp_linux_buffer_params_v1::Flags::empty(), + &qh, + (), + ); + tracing::trace!("Called ZwpLinuxBufferParamsV1::create_immed to create WlBuffer "); + // Copy the pixel data advertised by the compositor into the buffer we just created. + frame.copy(&dmabuf_wlbuf); + tracing::debug!("wlr-screencopy copy() with dmabuf complete"); + + // On copy the Ready / Failed events are fired by the frame object, so here we check for them. + loop { + // Basically reads, if frame state is not None then... + if let Some(state) = state.state { + match state { + FrameState::Failed => { + tracing::error!("Frame copy failed"); + return Err(Error::FramecopyFailed); + } + FrameState::Finished => { + tracing::trace!("Frame copy finished"); + + return Ok(DMAFrameGuard { + buffer: dmabuf_wlbuf, + }); + } + } + } + + event_queue.blocking_dispatch(&mut state)?; + } + } + None => Err(Error::NoDMAStateError), + } + } + fn capture_output_frame_inner( &self, mut state: CaptureFrameState, @@ -307,6 +718,7 @@ impl WayshotConnection { return Err(Error::FramecopyFailed); } FrameState::Finished => { + tracing::trace!("Frame copy finished"); return Ok(FrameGuard { buffer, shm_pool }); } } @@ -316,24 +728,6 @@ impl WayshotConnection { } } - fn capture_output_frame_shm_from_file( - &self, - cursor_overlay: bool, - output: &WlOutput, - file: &File, - capture_region: Option, - ) -> Result<(FrameFormat, FrameGuard)> { - let (state, event_queue, frame, frame_format) = - self.capture_output_frame_get_state(cursor_overlay as i32, output, capture_region)?; - - file.set_len(frame_format.byte_size())?; - - let frame_guard = - self.capture_output_frame_inner(state, event_queue, frame, frame_format, file)?; - - Ok((frame_format, frame_guard)) - } - /// Get a FrameCopy instance with screenshot pixel data for any wl_output object. #[tracing::instrument(skip_all, fields(output = format!("{output_info}"), region = capture_region.map(|r| format!("{:}", r)).unwrap_or("fullscreen".to_string())))] fn capture_frame_copy( @@ -375,7 +769,7 @@ impl WayshotConnection { let frame_copy = FrameCopy { frame_format, frame_color_type, - frame_mmap, + frame_data: FrameData::Mmap(frame_mmap), transform: output_info.transform, logical_region: capture_region .map(|capture_region| capture_region.logical()) @@ -391,26 +785,13 @@ impl WayshotConnection { output_capture_regions: &[(OutputInfo, Option)], cursor_overlay: bool, ) -> Result> { - let frame_copies = thread::scope(|scope| -> Result<_> { - let join_handles = output_capture_regions - .iter() - .map(|(output_info, capture_region)| { - scope.spawn(move || { - self.capture_frame_copy(cursor_overlay, output_info, *capture_region) - .map(|(frame_copy, frame_guard)| { - (frame_copy, frame_guard, output_info.clone()) - }) - }) - }) - .collect::>(); - - join_handles - .into_iter() - .flat_map(|join_handle| join_handle.join()) - .collect::>() - })?; - - Ok(frame_copies) + output_capture_regions + .iter() + .map(|(output_info, capture_region)| { + self.capture_frame_copy(cursor_overlay, output_info, *capture_region) + .map(|(frame_copy, frame_guard)| (frame_copy, frame_guard, output_info.clone())) + }) + .collect() } /// Create a layer shell surface for each output, diff --git a/libwayshot/src/screencopy.rs b/libwayshot/src/screencopy.rs index ee7ae337..adeec7fa 100644 --- a/libwayshot/src/screencopy.rs +++ b/libwayshot/src/screencopy.rs @@ -4,6 +4,7 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; +use gbm::BufferObject; use image::{ColorType, DynamicImage, ImageBuffer, Pixel}; use memmap2::MmapMut; use rustix::{ @@ -31,6 +32,31 @@ impl Drop for FrameGuard { } } +pub struct DMAFrameGuard { + pub buffer: WlBuffer, +} +impl Drop for DMAFrameGuard { + fn drop(&mut self) { + self.buffer.destroy(); + } +} + +pub struct EGLImageGuard<'a, T: khronos_egl::api::EGL1_5> { + pub image: khronos_egl::Image, + pub(crate) egl_instance: &'a khronos_egl::Instance, + pub(crate) egl_display: khronos_egl::Display, +} + +impl<'a, T: khronos_egl::api::EGL1_5> Drop for EGLImageGuard<'a, T> { + fn drop(&mut self) { + self.egl_instance + .destroy_image(self.egl_display, self.image) + .unwrap_or_else(|e| { + tracing::error!("EGLimage destruction had error: {e}"); + }); + } +} + /// Type of frame supported by the compositor. For now we only support Argb8888, Xrgb8888, and /// Xbgr8888. /// @@ -45,6 +71,17 @@ pub struct FrameFormat { pub stride: u32, } +/// Type of DMABUF frame supported by the compositor +/// +/// See `zwlr_screencopy_frame_v1::Event::linux_dmabuf` as it's retrieved from there. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct DMAFrameFormat { + pub format: u32, + /// Size of the frame in pixels. This will always be in "landscape" so a + /// portrait 1080x1920 frame will be 1920x1080 and will need to be rotated! + pub size: Size, +} + impl FrameFormat { /// Returns the size of the frame in bytes, which is the stride * height. pub fn byte_size(&self) -> u64 { @@ -52,30 +89,38 @@ impl FrameFormat { } } -#[tracing::instrument(skip(frame_mmap))] +#[tracing::instrument(skip(frame_data))] fn create_image_buffer

( frame_format: &FrameFormat, - frame_mmap: &MmapMut, + frame_data: &FrameData, ) -> Result>> where P: Pixel, { tracing::debug!("Creating image buffer"); - ImageBuffer::from_vec( - frame_format.size.width, - frame_format.size.height, - frame_mmap.to_vec(), - ) - .ok_or(Error::BufferTooSmall) + match frame_data { + FrameData::Mmap(frame_mmap) => ImageBuffer::from_vec( + frame_format.size.width, + frame_format.size.height, + frame_mmap.to_vec(), + ) + .ok_or(Error::BufferTooSmall), + FrameData::GBMBo(_) => todo!(), + } } +#[derive(Debug)] +pub enum FrameData { + Mmap(MmapMut), + GBMBo(BufferObject<()>), +} /// The copied frame comprising of the FrameFormat, ColorType (Rgba8), and a memory backed shm /// file that holds the image data in it. #[derive(Debug)] pub struct FrameCopy { pub frame_format: FrameFormat, pub frame_color_type: ColorType, - pub frame_mmap: MmapMut, + pub frame_data: FrameData, pub transform: wl_output::Transform, /// Logical region with the transform already applied. pub logical_region: LogicalRegion, @@ -88,10 +133,10 @@ impl TryFrom<&FrameCopy> for DynamicImage { fn try_from(value: &FrameCopy) -> Result { Ok(match value.frame_color_type { ColorType::Rgb8 => { - Self::ImageRgb8(create_image_buffer(&value.frame_format, &value.frame_mmap)?) + Self::ImageRgb8(create_image_buffer(&value.frame_format, &value.frame_data)?) } ColorType::Rgba8 => { - Self::ImageRgba8(create_image_buffer(&value.frame_format, &value.frame_mmap)?) + Self::ImageRgba8(create_image_buffer(&value.frame_format, &value.frame_data)?) } _ => return Err(Error::InvalidColor), }) From b52532afb1890b4288fcc41714d248554fbe12fc Mon Sep 17 00:00:00 2001 From: Access Date: Thu, 10 Oct 2024 17:02:20 +0900 Subject: [PATCH 26/26] fix: I find out a smart way to fix scale problem (#128) * fix: I find out a smart way to fix scale problem * Update libwayshot/src/lib.rs Co-authored-by: Aakash Sen Sharma <60808802+Shinyzenith@users.noreply.github.com> --------- Co-authored-by: Aakash Sen Sharma <60808802+Shinyzenith@users.noreply.github.com> --- libwayshot/src/dispatch.rs | 6 ++++++ libwayshot/src/lib.rs | 21 +++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/libwayshot/src/dispatch.rs b/libwayshot/src/dispatch.rs index e171cd1b..9c35fbc8 100644 --- a/libwayshot/src/dispatch.rs +++ b/libwayshot/src/dispatch.rs @@ -37,6 +37,10 @@ use wayland_protocols_wlr::screencopy::v1::client::{ zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1, }; +use wayland_protocols::wp::viewporter::client::{ + wp_viewport::WpViewport, wp_viewporter::WpViewporter, +}; + use crate::{ output::OutputInfo, region::{LogicalRegion, Position, Size}, @@ -299,6 +303,8 @@ delegate_noop!(LayerShellState: ignore WlShmPool); delegate_noop!(LayerShellState: ignore WlBuffer); delegate_noop!(LayerShellState: ignore ZwlrLayerShellV1); delegate_noop!(LayerShellState: ignore WlSurface); +delegate_noop!(LayerShellState: ignore WpViewport); +delegate_noop!(LayerShellState: ignore WpViewporter); impl wayland_client::Dispatch for LayerShellState { // No need to instrument here, span from lib.rs is automatically used. diff --git a/libwayshot/src/lib.rs b/libwayshot/src/lib.rs index 04b3438d..dfe9bbdd 100644 --- a/libwayshot/src/lib.rs +++ b/libwayshot/src/lib.rs @@ -37,8 +37,11 @@ use wayland_client::{ Connection, EventQueue, Proxy, }; use wayland_protocols::{ - wp::linux_dmabuf::zv1::client::{ - zwp_linux_buffer_params_v1, zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, + wp::{ + linux_dmabuf::zv1::client::{ + zwp_linux_buffer_params_v1, zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, + }, + viewporter::client::wp_viewporter::WpViewporter, }, xdg::xdg_output::zv1::client::{ zxdg_output_manager_v1::ZxdgOutputManagerV1, zxdg_output_v1::ZxdgOutputV1, @@ -832,6 +835,12 @@ impl WayshotConnection { )); } }; + let viewporter = self.globals.bind::(&qh, 1..=1, ()).ok(); + if viewporter.is_none() { + tracing::info!( + "Compositor does not support wp_viewporter, display scaling may be inaccurate." + ); + } let mut layer_shell_surfaces = Vec::with_capacity(frames.len()); @@ -872,6 +881,14 @@ impl WayshotConnection { // surface.set_buffer_scale(output_info.scale()); surface.attach(Some(&frame_guard.buffer), 0, 0); + if let Some(viewporter) = viewporter.as_ref() { + let viewport = viewporter.get_viewport(&surface, &qh, ()); + viewport.set_destination( + output_info.logical_region.inner.size.width as i32, + output_info.logical_region.inner.size.height as i32, + ); + } + debug!("Committing surface with attached buffer."); surface.commit(); layer_shell_surfaces.push((surface, layer_surface));