ui cleanup
This commit is contained in:
49
Cargo.lock
generated
49
Cargo.lock
generated
@@ -33,6 +33,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"config",
|
||||
"ctrlc",
|
||||
"directories",
|
||||
"embed-resource",
|
||||
"futures",
|
||||
"grim-rs",
|
||||
@@ -44,6 +45,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"toml 0.8.23",
|
||||
"tray-item",
|
||||
]
|
||||
|
||||
@@ -583,6 +585,27 @@ dependencies = [
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "directories"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
@@ -1456,6 +1479,15 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
@@ -1771,6 +1803,12 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-multimap"
|
||||
version = "0.7.3"
|
||||
@@ -2136,6 +2174,17 @@ dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"libredox",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.13.2"
|
||||
|
||||
@@ -8,6 +8,7 @@ build = "build.rs"
|
||||
anyhow = "1.0"
|
||||
config = "0.15.21"
|
||||
ctrlc = "3.5.2"
|
||||
directories = "6.0"
|
||||
futures = "0.3"
|
||||
oklab = "1.1.2"
|
||||
png = "0.17"
|
||||
@@ -16,6 +17,7 @@ rgb = "0.8"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
toml = "0.8"
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = "2.3"
|
||||
|
||||
110
src/eventloop.rs
110
src/eventloop.rs
@@ -4,7 +4,7 @@ use crate::settings::Settings;
|
||||
use crate::state::AppState;
|
||||
use rgb::RGB;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::sync::mpsc;
|
||||
@@ -12,7 +12,7 @@ use tokio::sync::mpsc;
|
||||
pub fn run_loop(
|
||||
settings: Arc<Settings>,
|
||||
ha_client: Arc<HaClient>,
|
||||
target_lights: Arc<Vec<String>>,
|
||||
target_lights: Arc<Mutex<Option<Vec<String>>>>,
|
||||
state: Arc<AppState>,
|
||||
is_running: Arc<AtomicBool>,
|
||||
mut exit_rx: mpsc::Receiver<()>,
|
||||
@@ -109,65 +109,69 @@ pub fn run_loop(
|
||||
}
|
||||
|
||||
if is_running_smoothing.load(Ordering::Relaxed) {
|
||||
if start.duration_since(last_calc_time) >= calc_interval {
|
||||
let active_screenshot = {
|
||||
let guard = state_smoothing.active_screenshot.read().unwrap();
|
||||
guard.clone()
|
||||
};
|
||||
let lights_opt = target_lights_smoothing.lock().unwrap().clone();
|
||||
|
||||
if let Some(lights) = lights_opt {
|
||||
if start.duration_since(last_calc_time) >= calc_interval {
|
||||
let active_screenshot = {
|
||||
let guard = state_smoothing.active_screenshot.read().unwrap();
|
||||
guard.clone()
|
||||
};
|
||||
|
||||
if let Some(screenshot) = active_screenshot {
|
||||
let color = screenshot.average_color(
|
||||
settings.brightness_boost,
|
||||
settings.saturation_boost,
|
||||
);
|
||||
let new_target = RGB::new(color.r as f32, color.g as f32, color.b as f32);
|
||||
if let Some(screenshot) = active_screenshot {
|
||||
let color = screenshot.average_color(
|
||||
settings.brightness_boost,
|
||||
settings.saturation_boost,
|
||||
);
|
||||
let new_target = RGB::new(color.r as f32, color.g as f32, color.b as f32);
|
||||
|
||||
{
|
||||
let mut target_guard = state_smoothing.target_color.write().unwrap();
|
||||
*target_guard = Some(new_target);
|
||||
{
|
||||
let mut target_guard = state_smoothing.target_color.write().unwrap();
|
||||
*target_guard = Some(new_target);
|
||||
}
|
||||
}
|
||||
last_calc_time = start;
|
||||
}
|
||||
|
||||
{
|
||||
let target_color = {
|
||||
let guard = state_smoothing.target_color.read().unwrap();
|
||||
*guard
|
||||
};
|
||||
|
||||
if let Some(target) = target_color {
|
||||
let mut active_guard = state_smoothing.active_color.write().unwrap();
|
||||
active_guard.r = active_guard.r * smoothing + target.r * (1.0 - smoothing);
|
||||
active_guard.g = active_guard.g * smoothing + target.g * (1.0 - smoothing);
|
||||
active_guard.b = active_guard.b * smoothing + target.b * (1.0 - smoothing);
|
||||
}
|
||||
}
|
||||
last_calc_time = start;
|
||||
}
|
||||
|
||||
{
|
||||
let target_color = {
|
||||
let guard = state_smoothing.target_color.read().unwrap();
|
||||
*guard
|
||||
let (r, g, b) = {
|
||||
let guard = state_smoothing.active_color.read().unwrap();
|
||||
(guard.r.round() as u8, guard.g.round() as u8, guard.b.round() as u8)
|
||||
};
|
||||
|
||||
if let Some(target) = target_color {
|
||||
let mut active_guard = state_smoothing.active_color.write().unwrap();
|
||||
active_guard.r = active_guard.r * smoothing + target.r * (1.0 - smoothing);
|
||||
active_guard.g = active_guard.g * smoothing + target.g * (1.0 - smoothing);
|
||||
active_guard.b = active_guard.b * smoothing + target.b * (1.0 - smoothing);
|
||||
}
|
||||
}
|
||||
let should_update = match last_sent_color {
|
||||
None => true,
|
||||
Some(last) => {
|
||||
let dr = (r as f32 - last.r as f32).abs();
|
||||
let dg = (g as f32 - last.g as f32).abs();
|
||||
let db = (b as f32 - last.b as f32).abs();
|
||||
dr > color_threshold || dg > color_threshold || db > color_threshold
|
||||
}
|
||||
};
|
||||
|
||||
let (r, g, b) = {
|
||||
let guard = state_smoothing.active_color.read().unwrap();
|
||||
(guard.r.round() as u8, guard.g.round() as u8, guard.b.round() as u8)
|
||||
};
|
||||
|
||||
let should_update = match last_sent_color {
|
||||
None => true,
|
||||
Some(last) => {
|
||||
let dr = (r as f32 - last.r as f32).abs();
|
||||
let dg = (g as f32 - last.g as f32).abs();
|
||||
let db = (b as f32 - last.b as f32).abs();
|
||||
dr > color_threshold || dg > color_threshold || db > color_threshold
|
||||
}
|
||||
};
|
||||
|
||||
if should_update && start.duration_since(last_update_time) >= min_update_interval_clone {
|
||||
if let Err(e) = ha_client_smoothing
|
||||
.set_lights_color_parallel(&target_lights_smoothing, r, g, b)
|
||||
.await
|
||||
{
|
||||
eprintln!("Failed to update lights: {}", e);
|
||||
} else {
|
||||
last_sent_color = Some(RGB::new(r, g, b));
|
||||
last_update_time = start;
|
||||
if should_update && start.duration_since(last_update_time) >= min_update_interval_clone {
|
||||
if let Err(e) = ha_client_smoothing
|
||||
.set_lights_color_parallel(&lights, r, g, b)
|
||||
.await
|
||||
{
|
||||
eprintln!("Failed to update lights: {}", e);
|
||||
} else {
|
||||
last_sent_color = Some(RGB::new(r, g, b));
|
||||
last_update_time = start;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
151
src/main.rs
151
src/main.rs
@@ -1,35 +1,13 @@
|
||||
mod eventloop;
|
||||
mod homeassistant;
|
||||
mod screenshot;
|
||||
mod settings;
|
||||
mod screenshot;
|
||||
mod state;
|
||||
mod ui;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use tray_item::{IconSource, TrayItem};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_tray_icon() -> IconSource {
|
||||
IconSource::Resource("app-icon")
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn get_tray_icon() -> IconSource {
|
||||
let icon_data = include_bytes!("../icon.png");
|
||||
let cursor = Cursor::new(&icon_data[..]);
|
||||
let mut decoder = png::Decoder::new(cursor);
|
||||
decoder.set_transformations(png::Transformations::normalize_to_color8());
|
||||
let mut reader = decoder.read_info().expect("Failed to read icon PNG");
|
||||
let mut buf: Vec<u8> = vec![0; reader.output_buffer_size()];
|
||||
let info = reader.next_frame(&mut buf).expect("Failed to decode icon PNG");
|
||||
|
||||
IconSource::Data {
|
||||
data: buf,
|
||||
width: info.width as i32,
|
||||
height: info.height as i32,
|
||||
}
|
||||
}
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
fn main() {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
@@ -40,110 +18,43 @@ fn main() {
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let ha_client = homeassistant::HaClient::new(&settings.ha_url, &settings.ha_token);
|
||||
|
||||
let target_lights = if !settings.lights.is_empty() {
|
||||
settings.lights.clone()
|
||||
} else {
|
||||
println!("Fetching color-supported lights from Home Assistant...");
|
||||
match rt.block_on(ha_client.get_color_lights()) {
|
||||
Ok(lights) => lights.into_iter().map(|l| l.entity_id).collect(),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to fetch color lights: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if target_lights.is_empty() {
|
||||
println!("No target lights found or configured. Exiting.");
|
||||
return;
|
||||
}
|
||||
|
||||
if settings.restore_on_exit {
|
||||
println!("Creating scene snapshot for restore on exit...");
|
||||
if let Err(e) =
|
||||
rt.block_on(ha_client.create_scene_snapshot("ambilight_restore", &target_lights))
|
||||
{
|
||||
eprintln!("Failed to create scene snapshot: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
let is_running = Arc::new(AtomicBool::new(true));
|
||||
let (exit_tx, exit_rx) = tokio::sync::mpsc::channel(1);
|
||||
|
||||
let ha_client = Arc::new(homeassistant::HaClient::new(&settings.ha_url, &settings.ha_token));
|
||||
let settings_arc = Arc::new(settings);
|
||||
let ha_client_arc = Arc::new(ha_client);
|
||||
let target_lights_arc = Arc::new(target_lights);
|
||||
|
||||
let is_running = Arc::new(AtomicBool::new(false));
|
||||
let (async_exit_tx, async_exit_rx) = tokio::sync::mpsc::channel(1);
|
||||
let (sync_exit_tx, sync_exit_rx) = channel();
|
||||
|
||||
let target_lights: Arc<Mutex<Option<Vec<String>>>> = Arc::new(Mutex::new(None));
|
||||
let state_arc = Arc::new(state::AppState::new());
|
||||
|
||||
let is_running_clone = is_running.clone();
|
||||
let settings_arc_clone = settings_arc.clone();
|
||||
let ha_client_arc_clone = ha_client_arc.clone();
|
||||
let target_lights_arc_clone = target_lights_arc.clone();
|
||||
let ha_client_arc_clone = ha_client.clone();
|
||||
let target_lights_clone = target_lights.clone();
|
||||
|
||||
let app_thread = std::thread::spawn(move || {
|
||||
eventloop::run_loop(
|
||||
settings_arc_clone,
|
||||
ha_client_arc_clone,
|
||||
target_lights_arc_clone,
|
||||
target_lights_clone,
|
||||
state_arc,
|
||||
is_running_clone,
|
||||
exit_rx,
|
||||
async_exit_rx,
|
||||
);
|
||||
});
|
||||
|
||||
let icon_source = get_tray_icon();
|
||||
let tray_ctx = ui::TrayContext {
|
||||
is_running: is_running.clone(),
|
||||
target_lights: target_lights.clone(),
|
||||
settings: settings_arc.clone(),
|
||||
ha_client: ha_client.clone(),
|
||||
rt_handle: rt_handle.clone(),
|
||||
exit_tx: sync_exit_tx.clone(),
|
||||
};
|
||||
|
||||
let mut tray = TrayItem::new("Ambiligth", icon_source)
|
||||
.expect("Failed to create tray icon");
|
||||
let _tray = ui::create_tray(tray_ctx);
|
||||
|
||||
let is_running_toggle = is_running.clone();
|
||||
let restore_on_toggle = settings_arc.restore_on_exit;
|
||||
let ha_client_toggle = ha_client_arc.clone();
|
||||
let target_lights_toggle = target_lights_arc.clone();
|
||||
let rt_handle_toggle = rt_handle.clone();
|
||||
|
||||
tray.add_menu_item("Toggle Ambilight", move || {
|
||||
let current = is_running_toggle.load(Ordering::Relaxed);
|
||||
is_running_toggle.store(!current, Ordering::Relaxed);
|
||||
|
||||
if current {
|
||||
println!("Ambilight disabled.");
|
||||
if restore_on_toggle {
|
||||
println!("Restoring lights to original state...");
|
||||
if let Err(e) = rt_handle_toggle
|
||||
.block_on(ha_client_toggle.turn_on_scene("scene.ambilight_restore"))
|
||||
{
|
||||
eprintln!("Failed to restore scene: {}", e);
|
||||
} else {
|
||||
println!("Lights restored successfully.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Ambilight enabled.");
|
||||
if restore_on_toggle {
|
||||
println!("Creating scene snapshot for restore on exit...");
|
||||
if let Err(e) = rt_handle_toggle.block_on(
|
||||
ha_client_toggle
|
||||
.create_scene_snapshot("ambilight_restore", &target_lights_toggle),
|
||||
) {
|
||||
eprintln!("Failed to create scene snapshot: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let (sync_exit_tx, sync_exit_rx) = std::sync::mpsc::channel();
|
||||
let sync_exit_tx_clone = sync_exit_tx.clone();
|
||||
|
||||
tray.add_menu_item("Quit", move || {
|
||||
let _ = sync_exit_tx_clone.send(());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Ctrl+C handling to cleanly exit
|
||||
ctrlc::set_handler(move || {
|
||||
let _ = sync_exit_tx.send(());
|
||||
})
|
||||
@@ -152,14 +63,16 @@ fn main() {
|
||||
let _ = sync_exit_rx.recv();
|
||||
println!("Shutting down...");
|
||||
|
||||
let _ = exit_tx.try_send(());
|
||||
let _ = async_exit_tx.try_send(());
|
||||
|
||||
if settings_arc.restore_on_exit {
|
||||
println!("Restoring lights to original state...");
|
||||
if let Err(e) = rt.block_on(ha_client_arc.turn_on_scene("scene.ambilight_restore")) {
|
||||
eprintln!("Failed to restore scene: {}", e);
|
||||
} else {
|
||||
println!("Lights restored successfully.");
|
||||
if let Some(ref lights) = *target_lights.lock().unwrap() {
|
||||
println!("Restoring lights to original state...");
|
||||
if let Err(e) = rt.block_on(ha_client.turn_on_scene("scene.ambilight_restore")) {
|
||||
eprintln!("Failed to restore scene: {}", e);
|
||||
} else {
|
||||
println!("Lights restored successfully.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use config::{Config, ConfigError, Environment, File};
|
||||
use serde::Deserialize;
|
||||
use directories::ProjectDirs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Settings {
|
||||
#[serde(default = "default_ha_url")]
|
||||
pub ha_url: String,
|
||||
@@ -70,19 +74,84 @@ fn default_ha_token() -> String {
|
||||
"YOUR_TOKEN".to_string()
|
||||
}
|
||||
|
||||
fn get_project_dirs() -> Option<ProjectDirs> {
|
||||
ProjectDirs::from("com", "ambiligth", "ambiligth")
|
||||
}
|
||||
|
||||
pub fn get_config_dir() -> PathBuf {
|
||||
get_project_dirs()
|
||||
.map(|dirs| dirs.config_dir().to_path_buf())
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
}
|
||||
|
||||
pub fn get_config_file_path() -> PathBuf {
|
||||
get_config_dir().join("config.toml")
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn new() -> Result<Self, ConfigError> {
|
||||
let builder = Config::builder()
|
||||
// Load settings from a configuration file named "config" (e.g. config.toml, config.json)
|
||||
// It's optional, so the program won't crash if the file doesn't exist
|
||||
.add_source(File::with_name("config").required(false))
|
||||
// Merge settings from environment variables
|
||||
// Variables like HA_URL and HA_TOKEN will automatically map to the struct fields
|
||||
.add_source(Environment::default());
|
||||
let config_dir = get_config_dir();
|
||||
let config_file = get_config_file_path();
|
||||
|
||||
let mut builder = Config::builder()
|
||||
.set_default("ha_url", default_ha_url())?
|
||||
.set_default("ha_token", default_ha_token())?
|
||||
.set_default("target_fps", default_target_fps() as i64)?
|
||||
.set_default("screenshot_fps", default_screenshot_fps() as i64)?
|
||||
.set_default("smoothing", default_smoothing() as f64)?
|
||||
.set_default("restore_on_exit", default_restore_on_exit())?
|
||||
.set_default("min_diff_percent", default_min_diff_percent() as f64)?
|
||||
.set_default("brightness_boost", default_brightness_boost() as f64)?
|
||||
.set_default("saturation_boost", default_saturation_boost() as f64)?;
|
||||
|
||||
if config_file.exists() {
|
||||
builder = builder.add_source(File::from(config_file));
|
||||
} else {
|
||||
builder = builder.add_source(File::with_name("config").required(false));
|
||||
}
|
||||
|
||||
builder = builder.add_source(Environment::default());
|
||||
|
||||
let config = builder.build()?;
|
||||
|
||||
// Deserialize the gathered configuration into the Settings struct
|
||||
config.try_deserialize()
|
||||
}
|
||||
|
||||
pub fn save(&self) -> io::Result<()> {
|
||||
let config_dir = get_config_dir();
|
||||
let config_file = get_config_file_path();
|
||||
|
||||
if !config_dir.exists() {
|
||||
fs::create_dir_all(&config_dir)?;
|
||||
}
|
||||
|
||||
let toml_string = toml::to_string_pretty(self)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
fs::write(&config_file, toml_string)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save_default_if_not_exists() -> io::Result<()> {
|
||||
let config_file = get_config_file_path();
|
||||
if !config_file.exists() {
|
||||
let default_settings = Settings::default_values();
|
||||
default_settings.save()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn default_values() -> Self {
|
||||
Settings {
|
||||
ha_url: default_ha_url(),
|
||||
ha_token: default_ha_token(),
|
||||
target_fps: default_target_fps(),
|
||||
screenshot_fps: default_screenshot_fps(),
|
||||
smoothing: default_smoothing(),
|
||||
lights: Vec::new(),
|
||||
restore_on_exit: default_restore_on_exit(),
|
||||
min_diff_percent: default_min_diff_percent(),
|
||||
brightness_boost: default_brightness_boost(),
|
||||
saturation_boost: default_saturation_boost(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
154
src/ui.rs
Normal file
154
src/ui.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
use crate::homeassistant;
|
||||
use crate::settings;
|
||||
use crate::state;
|
||||
use std::io::Cursor;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::runtime::Handle;
|
||||
use tray_item::{IconSource, TrayItem};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_tray_icon() -> IconSource {
|
||||
IconSource::Resource("app-icon")
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn get_tray_icon() -> IconSource {
|
||||
let icon_data = include_bytes!("../icon.png");
|
||||
let cursor = Cursor::new(&icon_data[..]);
|
||||
let mut decoder = png::Decoder::new(cursor);
|
||||
decoder.set_transformations(png::Transformations::normalize_to_color8());
|
||||
let mut reader = decoder.read_info().expect("Failed to read icon PNG");
|
||||
let mut buf: Vec<u8> = vec![0; reader.output_buffer_size()];
|
||||
let info = reader
|
||||
.next_frame(&mut buf)
|
||||
.expect("Failed to decode icon PNG");
|
||||
|
||||
IconSource::Data {
|
||||
data: buf,
|
||||
width: info.width as i32,
|
||||
height: info.height as i32,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn open_config_file() {
|
||||
let config_path = settings::get_config_file_path();
|
||||
std::process::Command::new("notepad")
|
||||
.arg(&config_path)
|
||||
.spawn()
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn open_config_file() {
|
||||
let config_path = settings::get_config_file_path();
|
||||
std::process::Command::new("xdg-open")
|
||||
.arg(&config_path)
|
||||
.spawn()
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub struct TrayContext {
|
||||
pub is_running: Arc<AtomicBool>,
|
||||
pub target_lights: Arc<Mutex<Option<Vec<String>>>>,
|
||||
pub settings: Arc<settings::Settings>,
|
||||
pub ha_client: Arc<homeassistant::HaClient>,
|
||||
pub rt_handle: Handle,
|
||||
pub exit_tx: Sender<()>,
|
||||
}
|
||||
|
||||
pub fn create_tray(ctx: TrayContext) -> TrayItem {
|
||||
let mut tray = TrayItem::new("Ambiligth", get_tray_icon()).expect("Failed to create tray icon");
|
||||
|
||||
let is_running_toggle = ctx.is_running.clone();
|
||||
let restore_on_toggle = ctx.settings.restore_on_exit;
|
||||
let ha_client_toggle = ctx.ha_client.clone();
|
||||
let target_lights_toggle = ctx.target_lights.clone();
|
||||
let rt_handle_toggle = ctx.rt_handle.clone();
|
||||
let settings_toggle = ctx.settings.clone();
|
||||
|
||||
tray.add_menu_item("Toggle Ambilight", move || {
|
||||
let current = is_running_toggle.load(Ordering::Relaxed);
|
||||
is_running_toggle.store(!current, Ordering::Relaxed);
|
||||
|
||||
if current {
|
||||
println!("Ambilight disabled.");
|
||||
if restore_on_toggle {
|
||||
println!("Restoring lights to original state...");
|
||||
if let Err(e) = rt_handle_toggle
|
||||
.block_on(ha_client_toggle.turn_on_scene("scene.ambilight_restore"))
|
||||
{
|
||||
eprintln!("Failed to restore scene: {}", e);
|
||||
} else {
|
||||
println!("Lights restored successfully.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Ambilight enabled.");
|
||||
|
||||
let needs_fetch_lights = {
|
||||
let lights_guard = target_lights_toggle.lock().unwrap();
|
||||
lights_guard.is_none()
|
||||
};
|
||||
|
||||
if needs_fetch_lights {
|
||||
let lights = if !settings_toggle.lights.is_empty() {
|
||||
settings_toggle.lights.clone()
|
||||
} else {
|
||||
println!("Fetching color-supported lights from Home Assistant...");
|
||||
match rt_handle_toggle.block_on(ha_client_toggle.get_color_lights()) {
|
||||
Ok(l) => l.into_iter().map(|l| l.entity_id).collect(),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to fetch color lights: {}", e);
|
||||
is_running_toggle.store(false, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if lights.is_empty() {
|
||||
eprintln!("No target lights found or configured.");
|
||||
is_running_toggle.store(false, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
let mut lights_guard = target_lights_toggle.lock().unwrap();
|
||||
*lights_guard = Some(lights.clone());
|
||||
}
|
||||
|
||||
if restore_on_toggle {
|
||||
println!("Creating scene snapshot for restore on exit...");
|
||||
if let Err(e) = rt_handle_toggle.block_on(
|
||||
ha_client_toggle.create_scene_snapshot("ambilight_restore", &lights),
|
||||
) {
|
||||
eprintln!("Failed to create scene snapshot: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
tray.add_menu_item("Open Config File", move || {
|
||||
let config_path = settings::get_config_file_path();
|
||||
if !config_path.exists() {
|
||||
if let Err(e) = settings::Settings::save_default_if_not_exists() {
|
||||
eprintln!("Failed to create default config: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
open_config_file();
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let exit_tx_clone = ctx.exit_tx.clone();
|
||||
tray.add_menu_item("Quit", move || {
|
||||
let _ = exit_tx_clone.send(());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
tray
|
||||
}
|
||||
Reference in New Issue
Block a user