use serde::de::{self, Deserializer, IgnoredAny, MapAccess, Visitor};
use serde::{Deserialize, Serialize};
use std::fmt;
use crate::error::ComputerError;
const MAX_REGION_EDGE: u32 = 100_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct Region {
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
}
impl Region {
pub fn new(x: i32, y: i32, width: u32, height: u32) -> Result<Self, ComputerError> {
validate_region(x, y, width, height)?;
Ok(Self {
x,
y,
width,
height,
})
}
}
pub fn parse_region(text: &str) -> Result<Region, ComputerError> {
let parts: Vec<&str> = text.split(',').map(str::trim).collect();
if parts.len() != 4 {
return Err(ComputerError::InvalidArguments(format!(
"region must be x,y,width,height (got {text:?})"
)));
}
let x = parse_i32(parts[0], "x")?;
let y = parse_i32(parts[1], "y")?;
let width = parse_u32(parts[2], "width")?;
let height = parse_u32(parts[3], "height")?;
Region::new(x, y, width, height)
}
fn parse_i32(text: &str, name: &str) -> Result<i32, ComputerError> {
text.parse::<i32>().map_err(|_| {
ComputerError::InvalidArguments(format!("region {name} is not an integer: {text:?}"))
})
}
fn parse_u32(text: &str, name: &str) -> Result<u32, ComputerError> {
text.parse::<u32>().map_err(|_| {
ComputerError::InvalidArguments(format!(
"region {name} is not a non-negative integer: {text:?}"
))
})
}
fn validate_region(x: i32, y: i32, width: u32, height: u32) -> Result<(), ComputerError> {
if width == 0 || height == 0 {
return Err(ComputerError::InvalidArguments(
"region width and height must be greater than 0".into(),
));
}
if width > MAX_REGION_EDGE || height > MAX_REGION_EDGE {
return Err(ComputerError::InvalidArguments(format!(
"region width and height must be at most {MAX_REGION_EDGE}"
)));
}
let _ = (x, y);
Ok(())
}
impl<'de> Deserialize<'de> for Region {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct RegionVisitor;
impl<'de> Visitor<'de> for RegionVisitor {
type Value = Region;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a \"x,y,width,height\" string or {x,y,width,height} object")
}
fn visit_str<E: de::Error>(self, value: &str) -> Result<Region, E> {
parse_region(value).map_err(E::custom)
}
fn visit_string<E: de::Error>(self, value: String) -> Result<Region, E> {
self.visit_str(&value)
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Region, A::Error> {
let mut x = None;
let mut y = None;
let mut width = None;
let mut height = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"x" => x = Some(map.next_value()?),
"y" => y = Some(map.next_value()?),
"width" => width = Some(map.next_value()?),
"height" => height = Some(map.next_value()?),
_ => {
let _ = map.next_value::<IgnoredAny>()?;
}
}
}
let x = x.ok_or_else(|| de::Error::missing_field("x"))?;
let y = y.ok_or_else(|| de::Error::missing_field("y"))?;
let width = width.ok_or_else(|| de::Error::missing_field("width"))?;
let height = height.ok_or_else(|| de::Error::missing_field("height"))?;
Region::new(x, y, width, height).map_err(de::Error::custom)
}
}
deserializer.deserialize_any(RegionVisitor)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum MouseButton {
#[default]
Left,
Right,
Middle,
Back,
Forward,
}
impl MouseButton {
pub fn as_str(self) -> &'static str {
match self {
Self::Left => "left",
Self::Right => "right",
Self::Middle => "middle",
Self::Back => "back",
Self::Forward => "forward",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Point {
pub x: i32,
pub y: i32,
}
pub fn parse_path(text: &str) -> Result<Vec<Point>, ComputerError> {
let parts: Vec<&str> = text.split(',').map(str::trim).collect();
if parts.len() < 4 || !parts.len().is_multiple_of(2) || parts.iter().any(|part| part.is_empty())
{
return Err(ComputerError::InvalidArguments(format!(
"path must be at least two x,y points (got {text:?})"
)));
}
let mut points = Vec::with_capacity(parts.len() / 2);
for pair in parts.chunks(2) {
let x = pair[0].parse::<i32>().map_err(|_| {
ComputerError::InvalidArguments(format!("path x is not an integer: {:?}", pair[0]))
})?;
let y = pair[1].parse::<i32>().map_err(|_| {
ComputerError::InvalidArguments(format!("path y is not an integer: {:?}", pair[1]))
})?;
points.push(Point { x, y });
}
Ok(points)
}
pub fn region_inside(region: Region, width: u32, height: u32) -> Result<(), ComputerError> {
if region.x < 0 || region.y < 0 {
return Err(ComputerError::InvalidArguments(
"region is outside the capture".into(),
));
}
let right = i64::from(region.x) + i64::from(region.width);
let bottom = i64::from(region.y) + i64::from(region.height);
if right > i64::from(width) || bottom > i64::from(height) {
return Err(ComputerError::InvalidArguments(
"region is outside the capture".into(),
));
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Modifier {
#[serde(rename = "command")]
Command,
Control,
Shift,
Alt,
Meta,
}
impl Modifier {
pub fn as_str(self) -> &'static str {
match self {
Self::Command => "command",
Self::Control => "control",
Self::Shift => "shift",
Self::Alt => "alt",
Self::Meta => "meta",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ImageFormat {
#[default]
Png,
Jpeg,
}
impl ImageFormat {
pub fn as_str(self) -> &'static str {
match self {
Self::Png => "png",
Self::Jpeg => "jpeg",
}
}
pub fn extension(self) -> &'static str {
match self {
Self::Png => "png",
Self::Jpeg => "jpg",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ScrollDirection {
Up,
Down,
Left,
Right,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DisplayInfo {
pub id: u32,
pub name: String,
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
pub scale: f64,
pub is_primary: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct FrontmostInfo {
pub app: Option<String>,
pub window: Option<String>,
pub note: Option<String>,
}
#[derive(Clone)]
pub struct Frame {
width: u32,
height: u32,
scale: f64,
image: image::RgbaImage,
}
impl Frame {
pub fn new(width: u32, height: u32, scale: f64, rgba: Vec<u8>) -> Result<Self, ComputerError> {
if width == 0 || height == 0 {
return Err(ComputerError::InvalidArguments(
"frame width and height must be greater than 0".into(),
));
}
let image = image::RgbaImage::from_raw(width, height, rgba).ok_or_else(|| {
ComputerError::InvalidArguments(
"rgba buffer length does not match width*height*4".into(),
)
})?;
Ok(Self::from_image(image, scale))
}
pub(crate) fn from_image(image: image::RgbaImage, scale: f64) -> Self {
Self {
width: image.width(),
height: image.height(),
scale: normalize_scale(scale),
image,
}
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn scale(&self) -> f64 {
self.scale
}
pub(crate) fn image(&self) -> &image::RgbaImage {
&self.image
}
}
impl fmt::Debug for Frame {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Frame")
.field("width", &self.width)
.field("height", &self.height)
.field("scale", &self.scale)
.finish()
}
}
#[derive(Clone)]
pub struct RawCapture {
pub frame: Frame,
pub origin_x: i32,
pub origin_y: i32,
pub logical_width: u32,
pub logical_height: u32,
pub capture_source: String,
pub occluded: bool,
pub warnings: Vec<String>,
}
pub(crate) fn normalize_scale(scale: f64) -> f64 {
if scale.is_finite() && scale > 0.0 {
scale
} else {
1.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_region_accepts_whitespace_and_rejects_bad_shapes() {
let region = parse_region(" 10, 20, 30, 40 ").unwrap();
assert_eq!(
region,
Region {
x: 10,
y: 20,
width: 30,
height: 40
}
);
assert_eq!(parse_region("-5,-8,2,3").unwrap().x, -5);
assert!(parse_region("1,2,3").is_err());
assert!(parse_region("1,2,3,4,5").is_err());
assert!(parse_region("a,2,3,4").is_err());
assert!(parse_region("1,2,-3,4").is_err());
assert!(parse_region("0,0,0,10").is_err());
assert!(parse_region("0,0,10,0").is_err());
assert!(parse_region("").is_err());
}
#[test]
fn region_deserializes_from_string_and_object() {
let from_string: Region = serde_json::from_str("\"1,2,3,4\"").unwrap();
let from_object: Region =
serde_json::from_str(r#"{"x":1,"y":2,"width":3,"height":4,"ignored":true}"#).unwrap();
assert_eq!(from_string, from_object);
let json = serde_json::to_value(from_object).unwrap();
assert_eq!(json["width"], 3);
assert!(serde_json::from_str::<Region>(r#"{"x":1,"y":2,"width":0,"height":4}"#).is_err());
}
#[test]
fn path_needs_two_points_and_region_may_touch_the_far_edge() {
let path = parse_path("10, 20, 200, 40").unwrap();
assert_eq!(path, vec![Point { x: 10, y: 20 }, Point { x: 200, y: 40 }]);
assert!(parse_path("1,1").is_err());
assert!(parse_path("1,2,3").is_err());
let inside = Region::new(0, 0, 400, 300).unwrap();
assert!(region_inside(inside, 400, 300).is_ok());
let past = Region::new(0, 0, 401, 300).unwrap();
assert!(region_inside(past, 400, 300).is_err());
assert_eq!(MouseButton::Back.as_str(), "back");
assert_eq!(MouseButton::Forward.as_str(), "forward");
}
#[test]
fn frame_checks_buffer_length_and_normalizes_scale() {
let frame = Frame::new(1, 1, 0.0, vec![1, 2, 3, 4]).unwrap();
assert_eq!(frame.width(), 1);
assert_eq!(frame.scale(), 1.0);
assert!(Frame::new(1, 1, 1.0, vec![0, 0, 0]).is_err());
assert!(Frame::new(0, 1, 1.0, vec![]).is_err());
}
}