cross plattform

This commit is contained in:
Your Name
2026-03-15 13:21:34 +01:00
parent acffe73504
commit 8772abadec
8 changed files with 584 additions and 222 deletions

15
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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

82
src/eventloop.rs Normal file
View File

@@ -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<screenshot::Screenshot> = 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;
}
}
}
}

View File

@@ -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<String>,
// 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<String>,
}
#[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<Vec<HaState>, Error> {
let url = format!("{}/api/states", self.base_url);
let states: Vec<HaState> = 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<Vec<HaState>, 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(())
}

View File

@@ -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::Screenshot> =
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 {

View File

@@ -1,5 +1,4 @@
use anyhow;
use grim_rs::Grim;
pub type ScreenResult<T> = Result<T, anyhow::Error>;
@@ -12,9 +11,9 @@ pub struct Pixel {
}
pub struct Screenshot {
data: Vec<Pixel>,
height: usize,
width: usize,
pub data: Vec<Pixel>,
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<Screenshot> {
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<Screenshot> {
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<Pixel> = 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<Pixel> = 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<Screenshot> {
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<Screenshot> {
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<CGDisplayCount> = 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<Pixel> = 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<Screenshot> {
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::<BITMAPINFOHEADER>() 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<u8> = 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<Pixel> = 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<Screenshot> {
ffi::get_screenshot_inner(screen)
}

View File

@@ -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 {