From 8772abadec86091cc51d63692983f2b8a8468331 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 15 Mar 2026 13:21:34 +0100 Subject: [PATCH] cross plattform --- Cargo.lock | 15 ++ Cargo.toml | 3 + config.toml | 8 +- src/eventloop.rs | 82 +++++++++ src/homeassistant.rs | 169 +++++++++--------- src/main.rs | 108 +----------- src/screenshot.rs | 407 +++++++++++++++++++++++++++++++++++++++---- src/settings.rs | 14 ++ 8 files changed, 584 insertions(+), 222 deletions(-) create mode 100644 src/eventloop.rs diff --git a/Cargo.lock b/Cargo.lock index a9238c8..17bc8fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,9 @@ dependencies = [ "anyhow", "config", "grim-rs", + "image", + "image-compare", + "libc", "okmain", "reqwest", "rgb", @@ -1094,6 +1097,18 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "image-compare" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176623a137baf75908084aff53578d6336cbd932bffaf12fabe26b343eb13338" +dependencies = [ + "image", + "itertools", + "rayon", + "thiserror 2.0.18", +] + [[package]] name = "image-webp" version = "0.2.4" diff --git a/Cargo.toml b/Cargo.toml index a225f2c..d783cd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,9 @@ edition = "2021" anyhow = "1.0" config = "0.15.21" grim-rs = "0.1.6" +image = { version = "0.25.10", default-features = false } +image-compare = "0.5.0" +libc = "0.2.183" okmain = "0.2.0" reqwest = { version = "0.13.2", features = ["json"] } rgb = "0.8" diff --git a/config.toml b/config.toml index 3ffa0a6..961604a 100644 --- a/config.toml +++ b/config.toml @@ -6,7 +6,7 @@ ha_url = "http://192.168.1.238:8123" # A Long-Lived Access Token from Home Assistant. # You can generate this in Home Assistant by going to your user profile. -ha_token = "" +ha_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI0YjNjMmYxNGE3NWM0YmJjYmNhYzA0YWFmYTlhOTVkNCIsImlhdCI6MTc3MzUyOTcwNCwiZXhwIjoyMDg4ODg5NzA0fQ.I923kml1zWqYYZk-0JSunexbo5NKcgehhwmG-T8jBcg" # Target updates per second (FPS) target_fps = 3 @@ -22,3 +22,9 @@ lights = [] # Restore lights to their original state/color when the program exits. restore_on_exit = true + +# Time (in seconds) it takes to fade to the new color. 0.0 means instant. +transition = 0.1 + +# Minimum percentage of difference required between frames to trigger an update (e.g. 1.0 = 1%) +min_diff_percent = 5.0 diff --git a/src/eventloop.rs b/src/eventloop.rs new file mode 100644 index 0000000..40dd29e --- /dev/null +++ b/src/eventloop.rs @@ -0,0 +1,82 @@ +use crate::homeassistant::HaClient; +use crate::screenshot; +use crate::settings::Settings; +use std::time::{Duration, Instant}; +use tokio::time::sleep; + +pub async fn run_loop(settings: &Settings, ha_client: &HaClient, target_lights: &[String]) { + let fps = settings.target_fps.max(1); + let target_duration = Duration::from_millis((1000 / fps) as u64); + + let mut current_color: Option<(f32, f32, f32)> = None; + let mut exit_signal = std::pin::pin!(tokio::signal::ctrl_c()); + let mut last_screenshot: Option = None; + + println!("Starting Ambilight loop at {} FPS...", fps); + + loop { + let start_time = Instant::now(); + + match screenshot::get_screenshot(0) { + Ok(current_screenshot) => { + let mut should_update = true; + + if let Some(prev) = &last_screenshot { + let diff = current_screenshot.diff_percent(prev); + if diff < settings.min_diff_percent { + should_update = false; + } + } + + if should_update { + let color = current_screenshot.dominant_color(); + let (new_r, new_g, new_b) = (color.r as f32, color.g as f32, color.b as f32); + + let (r, g, b) = match current_color { + None => (new_r, new_g, new_b), + Some((cr, cg, cb)) => { + let s_factor = settings.smoothing.clamp(0.0, 1.0); + ( + cr * s_factor + new_r * (1.0 - s_factor), + cg * s_factor + new_g * (1.0 - s_factor), + cb * s_factor + new_b * (1.0 - s_factor), + ) + } + }; + + current_color = Some((r, g, b)); + let (final_r, final_g, final_b) = + (r.round() as u8, g.round() as u8, b.round() as u8); + + if let Err(e) = ha_client + .set_lights_color( + target_lights, + final_r, + final_g, + final_b, + settings.transition, + ) + .await + { + eprintln!("Failed to update lights: {}", e); + } + + last_screenshot = Some(current_screenshot); + } + } + Err(e) => eprintln!("Screenshot error: {}", e), + } + + // Sleep to maintain target FPS, saturating_sub prevents negative duration panics if we run slow + let elapsed = start_time.elapsed(); + let sleep_duration = target_duration.saturating_sub(elapsed); + + tokio::select! { + _ = sleep(sleep_duration) => {} + _ = &mut exit_signal => { + println!("\nExit signal received, stopping loop..."); + break; + } + } + } +} diff --git a/src/homeassistant.rs b/src/homeassistant.rs index 0d65999..3f5c2d2 100644 --- a/src/homeassistant.rs +++ b/src/homeassistant.rs @@ -1,5 +1,6 @@ use reqwest::{Client, Error}; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; +use serde_json::json; #[derive(Debug, Clone)] pub struct HaClient { @@ -11,35 +12,10 @@ pub struct HaClient { #[derive(Debug, Deserialize)] pub struct HaState { pub entity_id: String, - pub state: String, - pub last_changed: String, - pub last_updated: String, - // We can use a generic JSON value for attributes as they vary wildly between entities pub attributes: serde_json::Value, } -#[derive(Debug, Serialize)] -struct LightColorPayload { - entity_id: Vec, - // Sending rgb_color explicitly only updates the RGB parts - // without automatically altering the White channel on most RGBW/RGBWW devices. - rgb_color: [u8; 3], -} - -#[derive(Debug, Serialize)] -struct SceneCreatePayload { - scene_id: String, - snapshot_entities: Vec, -} - -#[derive(Debug, Serialize)] -struct EntityIdPayload { - entity_id: String, -} - impl HaClient { - /// Create a new Home Assistant REST API client. - /// `base_url` should be the address of your Home Assistant instance, e.g., "http://192.168.1.100:8123" pub fn new(base_url: &str, token: &str) -> Self { Self { base_url: base_url.trim_end_matches('/').to_string(), @@ -48,10 +24,8 @@ impl HaClient { } } - /// Fetches all currently loaded states and filters them to return only light entities. pub async fn get_lights(&self) -> Result, Error> { let url = format!("{}/api/states", self.base_url); - let states: Vec = self .client .get(&url) @@ -61,112 +35,133 @@ impl HaClient { .json() .await?; - let lights = states + Ok(states .into_iter() .filter(|s| s.entity_id.starts_with("light.")) - .collect(); - - Ok(lights) + .collect()) } - /// Fetches all currently loaded states and filters them to return only light entities that support color. pub async fn get_color_lights(&self) -> Result, Error> { - let lights = self.get_lights().await?; let color_modes = ["hs", "xy", "rgb", "rgbw", "rgbww"]; + let lights = self.get_lights().await?; - let color_lights = lights + Ok(lights .into_iter() .filter(|l| { - if let Some(modes) = l - .attributes + l.attributes .get("supported_color_modes") .and_then(|m| m.as_array()) - { - modes.iter().any(|m| { - if let Some(s) = m.as_str() { - color_modes.contains(&s) - } else { - false - } + .map(|modes| { + modes + .iter() + .any(|m| m.as_str().map_or(false, |s| color_modes.contains(&s))) }) - } else { - false - } + .unwrap_or(false) }) - .collect(); - - Ok(color_lights) + .collect()) } - /// Creates a scene snapshot of the specified entities. pub async fn create_scene_snapshot( &self, scene_id: &str, entities: &[String], ) -> Result<(), Error> { - let url = format!("{}/api/services/scene/create", self.base_url); - let payload = SceneCreatePayload { - scene_id: scene_id.to_string(), - snapshot_entities: entities.to_vec(), - }; - self.client - .post(&url) + .post(format!("{}/api/services/scene/create", self.base_url)) .bearer_auth(&self.token) - .json(&payload) + .json(&json!({ + "scene_id": scene_id, + "snapshot_entities": entities, + })) .send() .await? .error_for_status()?; - Ok(()) } - /// Turns on a specific entity, typically used for restoring a scene (e.g., "scene.my_snapshot"). pub async fn turn_on_scene(&self, scene_entity_id: &str) -> Result<(), Error> { - let url = format!("{}/api/services/scene/turn_on", self.base_url); - let payload = EntityIdPayload { - entity_id: scene_entity_id.to_string(), - }; - self.client - .post(&url) + .post(format!("{}/api/services/scene/turn_on", self.base_url)) .bearer_auth(&self.token) - .json(&payload) + .json(&json!({ "entity_id": scene_entity_id })) .send() .await? .error_for_status()?; - Ok(()) } - /// Set the color for a single light entity. - pub async fn set_light_color(&self, entity_id: &str, r: u8, g: u8, b: u8) -> Result<(), Error> { - self.set_lights_color(&[entity_id.to_string()], r, g, b) - .await - } - - /// Set the color for multiple light entities at once. pub async fn set_lights_color( &self, entity_ids: &[String], r: u8, g: u8, b: u8, + transition: f32, ) -> Result<(), Error> { + let states = self.get_lights().await.unwrap_or_default(); let url = format!("{}/api/services/light/turn_on", self.base_url); - let payload = LightColorPayload { - entity_id: entity_ids.to_vec(), - rgb_color: [r, g, b], - }; + for entity_id in entity_ids { + let mut payload = json!({ + "entity_id": entity_id, + "transition": transition, + }); - self.client - .post(&url) - .bearer_auth(&self.token) - .json(&payload) - .send() - .await? - .error_for_status()?; // Ensure we return an error if the request failed (e.g. 401 or 400) + if let Some(state) = states.iter().find(|s| s.entity_id == *entity_id) { + if let Some(brightness) = state.attributes.get("brightness") { + payload["brightness"] = brightness.clone(); + } + + let color_mode = state + .attributes + .get("color_mode") + .and_then(|m| m.as_str()) + .unwrap_or(""); + + match color_mode { + "rgbw" => { + let w = state + .attributes + .get("rgbw_color") + .and_then(|v| v.as_array()) + .and_then(|a| a.get(3)) + .cloned() + .unwrap_or(json!(0)); + payload["rgbw_color"] = json!([r, g, b, w]); + } + "rgbww" => { + let cw = state + .attributes + .get("rgbww_color") + .and_then(|v| v.as_array()) + .and_then(|a| a.get(3)) + .cloned() + .unwrap_or(json!(0)); + let ww = state + .attributes + .get("rgbww_color") + .and_then(|v| v.as_array()) + .and_then(|a| a.get(4)) + .cloned() + .unwrap_or(json!(0)); + payload["rgbww_color"] = json!([r, g, b, cw, ww]); + } + _ => { + payload["rgb_color"] = json!([r, g, b]); + } + } + } else { + payload["rgb_color"] = json!([r, g, b]); + } + + let _ = self + .client + .post(&url) + .bearer_auth(&self.token) + .json(&payload) + .send() + .await; + } Ok(()) } diff --git a/src/main.rs b/src/main.rs index 4ab1256..cf900e9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,8 @@ +mod eventloop; mod homeassistant; mod screenshot; mod settings; -use std::time::{Duration, Instant}; -use tokio::time::sleep; - #[tokio::main] async fn main() { let settings = settings::Settings::new().unwrap_or_else(|err| { @@ -14,13 +12,6 @@ async fn main() { let ha_client = homeassistant::HaClient::new(&settings.ha_url, &settings.ha_token); - let fps = if settings.target_fps > 0 { - settings.target_fps - } else { - 1 - }; - let target_duration = Duration::from_millis((1000 / fps) as u64); - let target_lights = if !settings.lights.is_empty() { settings.lights.clone() } else { @@ -49,101 +40,10 @@ async fn main() { } } - let mut current_r = 0.0_f32; - let mut current_g = 0.0_f32; - let mut current_b = 0.0_f32; - let mut first_frame = true; - - let mut exit_signal = std::pin::pin!(tokio::signal::ctrl_c()); - - loop { - // Check for exit signal before capturing - tokio::select! { - biased; - _ = &mut exit_signal => { - println!("Exit signal received."); - break; - } - _ = async {} => {} - } - - println!("Capturing screenshot..."); - let start_time = Instant::now(); - let result: screenshot::ScreenResult = - screenshot::get_screenshot(0); - - match result { - Ok(s) => { - let capture_time = start_time.elapsed(); - println!("Screenshot captured in {:?}", capture_time); - - let color_start_time = Instant::now(); - let color = s.dominant_color(); - let color_time = color_start_time.elapsed(); - - let new_r = color.r as f32; - let new_g = color.g as f32; - let new_b = color.b as f32; - - if first_frame { - current_r = new_r; - current_g = new_g; - current_b = new_b; - first_frame = false; - } else { - let s = settings.smoothing.clamp(0.0, 1.0); - current_r = current_r * s + new_r * (1.0 - s); - current_g = current_g * s + new_g * (1.0 - s); - current_b = current_b * s + new_b * (1.0 - s); - } - - let final_r = current_r.round() as u8; - let final_g = current_g.round() as u8; - let final_b = current_b.round() as u8; - - println!( - "Dominant color: R={}, G={}, B={} (calculated in {:?}) | Smoothed: R={}, G={}, B={}", - color.r, color.g, color.b, color_time, final_r, final_g, final_b - ); - - if let Err(e) = ha_client - .set_lights_color(&target_lights, final_r, final_g, final_b) - .await - { - eprintln!("Failed to set light colors: {}", e); - } - - println!("Total time: {:?}", start_time.elapsed()); - } - Err(e) => { - eprintln!( - "Error capturing screenshot: {} (after {:?})", - e, - start_time.elapsed() - ); - } - } - - // If there is time left to hit the target framerate, we wait. - // For example, if target is 1 fps (1000ms) and we took 600ms, we wait 400ms. - // If we took longer than the target duration, we don't wait. - let elapsed = start_time.elapsed(); - if elapsed < target_duration { - tokio::select! { - _ = sleep(target_duration - elapsed) => {} - _ = &mut exit_signal => { - println!("Exit signal received during sleep."); - break; - } - } - } else { - println!( - "Warning: Loop iteration took longer than target duration ({:?} > {:?}). Try running with `cargo run --release` for much faster performance!", - elapsed, target_duration - ); - } - } + // Run the main application event loop + eventloop::run_loop(&settings, &ha_client, &target_lights).await; + // The event loop returns when a shutdown signal (like Ctrl+C) is received if settings.restore_on_exit { println!("Restoring lights to original state..."); if let Err(e) = ha_client.turn_on_scene("scene.ambilight_restore").await { diff --git a/src/screenshot.rs b/src/screenshot.rs index 0b68246..6b9013d 100644 --- a/src/screenshot.rs +++ b/src/screenshot.rs @@ -1,5 +1,4 @@ use anyhow; -use grim_rs::Grim; pub type ScreenResult = Result; @@ -12,9 +11,9 @@ pub struct Pixel { } pub struct Screenshot { - data: Vec, - height: usize, - width: usize, + pub data: Vec, + pub height: usize, + pub width: usize, } impl Screenshot { @@ -42,37 +41,385 @@ impl Screenshot { }) .unwrap_or(rgb::Rgb { r: 0, g: 0, b: 0 }) } + + /// Compares this screenshot against another and returns the percentage of difference (0.0 to 100.0) + pub fn diff_percent(&self, other: &Screenshot) -> f32 { + if self.width != other.width || self.height != other.height { + return 100.0; + } + + let mut rgb_data1 = vec![0u8; self.data.len() * 3]; + let mut rgb_data2 = vec![0u8; other.data.len() * 3]; + + for (i, (p1, p2)) in self.data.iter().zip(other.data.iter()).enumerate() { + let offset = i * 3; + rgb_data1[offset] = p1.r; + rgb_data1[offset + 1] = p1.g; + rgb_data1[offset + 2] = p1.b; + + rgb_data2[offset] = p2.r; + rgb_data2[offset + 1] = p2.g; + rgb_data2[offset + 2] = p2.b; + } + + let img1 = image::RgbImage::from_vec(self.width as u32, self.height as u32, rgb_data1) + .expect("Failed to create image1 buffer"); + let img2 = image::RgbImage::from_vec(other.width as u32, other.height as u32, rgb_data2) + .expect("Failed to create image2 buffer"); + + let algo = image_compare::Algorithm::MSSIMSimple; + if let Ok(res) = image_compare::rgb_similarity_structure(&algo, &img1, &img2) { + ((1.0 - res.score) * 100.0) as f32 + } else { + 100.0 + } + } } -fn get_screenshot_inner() -> ScreenResult { - let mut grim = Grim::new().map_err(|e| anyhow::anyhow!("Failed to initialize grim: {}", e))?; - let result = grim - .capture_all() - .map_err(|e| anyhow::anyhow!("Failed to capture screenshot: {}", e))?; +#[cfg(any( + target_os = "linux", + not(any(target_os = "macos", target_os = "windows")) +))] +mod ffi { + use super::{Pixel, ScreenResult, Screenshot}; + use grim_rs::Grim; - let raw_data = result.data(); - let width = result.width() as usize; - let height = result.height() as usize; + pub fn get_screenshot_inner(_screen: usize) -> ScreenResult { + let mut grim = + Grim::new().map_err(|e| anyhow::anyhow!("Failed to initialize grim: {}", e))?; + let result = grim + .capture_all() + .map_err(|e| anyhow::anyhow!("Failed to capture screenshot: {}", e))?; - // Using chunks_exact allows the compiler to drop the bounds checking and - // remainder branching, which heavily promotes auto-vectorization (SIMD). - let pixels: Vec = raw_data - .chunks_exact(4) - .map(|chunk| Pixel { - r: chunk[0], - g: chunk[1], - b: chunk[2], - a: chunk[3], + let raw_data = result.data(); + let width = result.width() as usize; + let height = result.height() as usize; + + let pixels: Vec = raw_data + .chunks_exact(4) + .map(|chunk| Pixel { + r: chunk[0], + g: chunk[1], + b: chunk[2], + a: chunk[3], + }) + .collect(); + + Ok(Screenshot { + data: pixels, + height, + width, }) - .collect(); - - Ok(Screenshot { - data: pixels, - height, - width, - }) + } } -pub fn get_screenshot(_screen: usize) -> ScreenResult { - get_screenshot_inner() +#[cfg(target_os = "macos")] +mod ffi { + #![allow(non_upper_case_globals, dead_code)] + use super::{Pixel, ScreenResult, Screenshot}; + use libc; + use std::slice; + + type CFIndex = libc::c_long; + type CFDataRef = *const u8; + type CGError = libc::int32_t; + type CGDirectDisplayID = libc::uint32_t; + type CGDisplayCount = libc::uint32_t; + type CGImageRef = *mut u8; + type CGDataProviderRef = *mut u8; + const CGDisplayNoErr: CGError = 0; + + #[link(name = "CoreGraphics", kind = "framework")] + extern "C" { + fn CGGetActiveDisplayList( + max_displays: libc::uint32_t, + active_displays: *mut CGDirectDisplayID, + display_count: *mut CGDisplayCount, + ) -> CGError; + fn CGDisplayCreateImage(displayID: CGDirectDisplayID) -> CGImageRef; + fn CGImageRelease(image: CGImageRef); + fn CGImageGetBitsPerPixel(image: CGImageRef) -> libc::size_t; + fn CGImageGetDataProvider(image: CGImageRef) -> CGDataProviderRef; + fn CGImageGetHeight(image: CGImageRef) -> libc::size_t; + fn CGImageGetWidth(image: CGImageRef) -> libc::size_t; + fn CGDataProviderCopyData(provider: CGDataProviderRef) -> CFDataRef; + } + + #[link(name = "CoreFoundation", kind = "framework")] + extern "C" { + fn CFDataGetLength(theData: CFDataRef) -> CFIndex; + fn CFDataGetBytePtr(theData: CFDataRef) -> *const u8; + fn CFRelease(cf: *const libc::c_void); + } + + pub fn get_screenshot_inner(screen: usize) -> ScreenResult { + unsafe { + let mut count: CGDisplayCount = 0; + let mut err = CGGetActiveDisplayList(0, std::ptr::null_mut(), &mut count); + if err != CGDisplayNoErr { + return Err(anyhow::anyhow!("Error getting number of displays.")); + } + + let mut disps: Vec = Vec::with_capacity(count as usize); + disps.set_len(count as usize); + err = CGGetActiveDisplayList( + disps.len() as libc::uint32_t, + disps.as_mut_ptr(), + &mut count, + ); + if err != CGDisplayNoErr { + return Err(anyhow::anyhow!("Error getting list of displays.")); + } + + let disp_id = disps[screen]; + let cg_img = CGDisplayCreateImage(disp_id); + let width = CGImageGetWidth(cg_img) as usize; + let height = CGImageGetHeight(cg_img) as usize; + let pixel_bits = CGImageGetBitsPerPixel(cg_img) as usize; + + if pixel_bits % 8 != 0 { + return Err(anyhow::anyhow!("Pixels aren't integral bytes.")); + } + + let cf_data = CGDataProviderCopyData(CGImageGetDataProvider(cg_img)); + let raw_len = CFDataGetLength(cf_data) as usize; + + let data = slice::from_raw_parts(CFDataGetBytePtr(cf_data), raw_len); + let pixels: Vec = data + .chunks_exact(4) + .map(|chunk| Pixel { + b: chunk[0], + g: chunk[1], + r: chunk[2], + a: chunk[3], + }) + .collect(); + + CGImageRelease(cg_img); + CFRelease(cf_data as *const libc::c_void); + + Ok(Screenshot { + data: pixels, + height, + width, + }) + } + } +} + +#[cfg(target_os = "windows")] +mod ffi { + #![allow(non_snake_case, dead_code)] + use super::{Pixel, ScreenResult, Screenshot}; + use libc::{c_int, c_long, c_uint, c_void}; + use std::mem::size_of; + + type PVOID = *mut c_void; + type LPVOID = *mut c_void; + type WORD = u16; + type DWORD = u32; + type BOOL = c_int; + type BYTE = u8; + type UINT = c_uint; + type LONG = c_long; + type LPARAM = c_long; + + #[repr(C)] + struct RECT { + left: LONG, + top: LONG, + right: LONG, + bottom: LONG, + } + type LPCRECT = *const RECT; + type LPRECT = *mut RECT; + + type HANDLE = PVOID; + type HMONITOR = HANDLE; + type HWND = HANDLE; + type HDC = HANDLE; + + type HBITMAP = HANDLE; + type HGDIOBJ = HANDLE; + type LPBITMAPINFO = PVOID; + + const NULL: *mut c_void = 0usize as *mut c_void; + const HGDI_ERROR: *mut c_void = -1isize as *mut c_void; + const SM_CXSCREEN: c_int = 0; + const SM_CYSCREEN: c_int = 1; + + const SRCCOPY: u32 = 0x00CC0020; + const CAPTUREBLT: u32 = 0x40000000; + const DIB_RGB_COLORS: UINT = 0; + const BI_RGB: DWORD = 0; + + #[repr(C)] + struct BITMAPINFOHEADER { + biSize: DWORD, + biWidth: LONG, + biHeight: LONG, + biPlanes: WORD, + biBitCount: WORD, + biCompression: DWORD, + biSizeImage: DWORD, + biXPelsPerMeter: LONG, + biYPelsPerMeter: LONG, + biClrUsed: DWORD, + biClrImportant: DWORD, + } + + #[repr(C)] + struct RGBQUAD { + rgbBlue: BYTE, + rgbGreen: BYTE, + rgbRed: BYTE, + rgbReserved: BYTE, + } + + #[repr(C)] + struct BITMAPINFO { + bmiHeader: BITMAPINFOHEADER, + bmiColors: [RGBQUAD; 1], + } + + #[link(name = "user32")] + extern "system" { + fn GetSystemMetrics(m: c_int) -> c_int; + fn GetDesktopWindow() -> HWND; + fn GetDC(hWnd: HWND) -> HDC; + } + + #[link(name = "gdi32")] + extern "system" { + fn CreateCompatibleDC(hdc: HDC) -> HDC; + fn CreateCompatibleBitmap(hdc: HDC, nWidth: c_int, nHeight: c_int) -> HBITMAP; + fn SelectObject(hdc: HDC, hgdiobj: HGDIOBJ) -> HGDIOBJ; + fn BitBlt( + hdcDest: HDC, + nXDest: c_int, + nYDest: c_int, + nWidth: c_int, + nHeight: c_int, + hdcSrc: HDC, + nXSrc: c_int, + nYSrc: c_int, + dwRop: DWORD, + ) -> BOOL; + fn GetDIBits( + hdc: HDC, + hbmp: HBITMAP, + uStartScan: UINT, + cScanLines: UINT, + lpvBits: LPVOID, + lpbi: LPBITMAPINFO, + uUsage: UINT, + ) -> c_int; + fn DeleteObject(hObject: HGDIOBJ) -> BOOL; + fn ReleaseDC(hWnd: HWND, hDC: HDC) -> c_int; + fn DeleteDC(hdc: HDC) -> BOOL; + } + + pub fn get_screenshot_inner(_screen: usize) -> ScreenResult { + unsafe { + let h_wnd_screen = GetDesktopWindow(); + let h_dc_screen = GetDC(h_wnd_screen); + let width = GetSystemMetrics(SM_CXSCREEN); + let height = GetSystemMetrics(SM_CYSCREEN); + + let h_dc = CreateCompatibleDC(h_dc_screen); + if h_dc == NULL { + return Err(anyhow::anyhow!("Can't get a Windows display.")); + } + + let h_bmp = CreateCompatibleBitmap(h_dc_screen, width, height); + if h_bmp == NULL { + return Err(anyhow::anyhow!("Can't create a Windows buffer")); + } + + let res = SelectObject(h_dc, h_bmp); + if res == NULL || res == HGDI_ERROR { + return Err(anyhow::anyhow!("Can't select Windows buffer.")); + } + + let res = BitBlt( + h_dc, + 0, + 0, + width, + height, + h_dc_screen, + 0, + 0, + SRCCOPY | CAPTUREBLT, + ); + if res == 0 { + return Err(anyhow::anyhow!("Failed to copy screen to Windows buffer")); + } + + let pixel_width: usize = 4; + let mut bmi = BITMAPINFO { + bmiHeader: BITMAPINFOHEADER { + biSize: size_of::() as DWORD, + biWidth: width as LONG, + biHeight: height as LONG, + biPlanes: 1, + biBitCount: 8 * pixel_width as WORD, + biCompression: BI_RGB, + biSizeImage: (width * height * pixel_width as c_int) as DWORD, + biXPelsPerMeter: 0, + biYPelsPerMeter: 0, + biClrUsed: 0, + biClrImportant: 0, + }, + bmiColors: [RGBQUAD { + rgbBlue: 0, + rgbGreen: 0, + rgbRed: 0, + rgbReserved: 0, + }], + }; + + let size: usize = (width * height) as usize * pixel_width; + let mut data: Vec = vec![0; size]; + + GetDIBits( + h_dc, + h_bmp, + 0, + height as DWORD, + data.as_mut_ptr() as *mut c_void, + &mut bmi as *mut BITMAPINFO as *mut c_void, + DIB_RGB_COLORS, + ); + + ReleaseDC(h_wnd_screen, h_dc_screen); + DeleteDC(h_dc); + DeleteObject(h_bmp); + + let row_len = width as usize * pixel_width; + let mut pixels: Vec = Vec::with_capacity((width * height) as usize); + + for row_i in (0..height as usize).rev() { + for byte_i in (0..width as usize) { + let idx = row_i * row_len + byte_i * pixel_width; + pixels.push(Pixel { + b: data[idx], + g: data[idx + 1], + r: data[idx + 2], + a: data[idx + 3], + }); + } + } + + Ok(Screenshot { + data: pixels, + height: height as usize, + width: width as usize, + }) + } + } +} + +pub fn get_screenshot(screen: usize) -> ScreenResult { + ffi::get_screenshot_inner(screen) } diff --git a/src/settings.rs b/src/settings.rs index c1331d5..5319285 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -20,6 +20,20 @@ pub struct Settings { #[serde(default = "default_restore_on_exit")] pub restore_on_exit: bool, + + #[serde(default = "default_transition")] + pub transition: f32, + + #[serde(default = "default_min_diff_percent")] + pub min_diff_percent: f32, +} + +fn default_min_diff_percent() -> f32 { + 1.0 +} + +fn default_transition() -> f32 { + 0.5 } fn default_restore_on_exit() -> bool {