cross plattform
This commit is contained in:
15
Cargo.lock
generated
15
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
82
src/eventloop.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
108
src/main.rs
108
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::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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user