From 055ba54853dc9707d5cab70d8b08dc65c9d5b161 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 15 Mar 2026 13:59:06 +0100 Subject: [PATCH] Tray --- Cargo.lock | 369 +++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 +- config.toml | 1 + flake.nix | 14 +- src/eventloop.rs | 101 +++++++------ src/main.rs | 108 ++++++++++++-- 6 files changed, 539 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17bc8fb..bd20c1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,7 @@ version = "0.1.0" dependencies = [ "anyhow", "config", + "ctrlc", "grim-rs", "image", "image-compare", @@ -42,6 +43,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "tray-item", ] [[package]] @@ -53,6 +55,15 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -114,6 +125,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -221,6 +243,12 @@ dependencies = [ "core2", ] +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -230,6 +258,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "built" version = "0.8.0" @@ -303,6 +340,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + [[package]] name = "cmake" version = "0.1.57" @@ -312,6 +364,36 @@ dependencies = [ "cc", ] +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types", + "libc", + "objc", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -403,6 +485,30 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + [[package]] name = "core2" version = "0.4.0" @@ -471,6 +577,48 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys 0.61.2", +] + +[[package]] +name = "dbus" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "dbus-codegen" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a49da9fdfbe872d4841d56605dc42efa5e6ca3291299b87f44e1cde91a28617c" +dependencies = [ + "clap", + "dbus", + "xml-rs", +] + +[[package]] +name = "dbus-tree" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f456e698ae8e54575e19ddb1f9b7bce2298568524f215496b248eb9498b4f508" +dependencies = [ + "dbus", +] + [[package]] name = "digest" version = "0.10.7" @@ -481,6 +629,18 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "block2", + "libc", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -659,6 +819,33 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -845,6 +1032,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "http" version = "1.4.0" @@ -1236,6 +1432,18 @@ dependencies = [ "serde", ] +[[package]] +name = "ksni" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4934310bdd016e55725482b8d35ac0c16fd058c1b955d8959aa2d953b918c85b" +dependencies = [ + "dbus", + "dbus-codegen", + "dbus-tree", + "thiserror 1.0.69", +] + [[package]] name = "lebe" version = "0.5.3" @@ -1248,6 +1456,15 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + [[package]] name = "libfuzzer-sys" version = "0.4.12" @@ -1300,6 +1517,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "maybe-rayon" version = "0.1.1" @@ -1368,6 +1594,18 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "8.0.0" @@ -1433,6 +1671,50 @@ dependencies = [ "autocfg", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "oklab" version = "1.1.2" @@ -1479,6 +1761,12 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "padlock" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c10569378a1dacd9f30dbe7ae49e054d2c45dc2f8ee49899903e09c3924e8b6f" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2281,6 +2569,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "subtle" version = "2.6.1" @@ -2352,6 +2646,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2586,6 +2889,23 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tray-item" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59d4bd406170690dc30eabb3badc67a085beaf9b2c3b1923afcc9c26a2191353" +dependencies = [ + "cocoa", + "core-graphics", + "ksni", + "libc", + "objc", + "objc-foundation", + "objc_id", + "padlock", + "windows-sys 0.52.0", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -2622,6 +2942,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "untrusted" version = "0.9.0" @@ -2657,6 +2983,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version_check" version = "0.9.5" @@ -2861,6 +3193,22 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -2870,6 +3218,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" @@ -2958,6 +3312,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -3183,6 +3546,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + [[package]] name = "y4m" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index d783cd8..a8edd5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,9 @@ edition = "2021" [dependencies] anyhow = "1.0" config = "0.15.21" +ctrlc = "3.5.2" grim-rs = "0.1.6" -image = { version = "0.25.10", default-features = false } +image = "0.25.10" image-compare = "0.5.0" libc = "0.2.183" okmain = "0.2.0" @@ -16,3 +17,4 @@ rgb = "0.8" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" tokio = { version = "1.50.0", features = ["full"] } +tray-item = { version = "0.10.0", features = ["ksni"] } diff --git a/config.toml b/config.toml index 961604a..b73a284 100644 --- a/config.toml +++ b/config.toml @@ -27,4 +27,5 @@ restore_on_exit = true transition = 0.1 # Minimum percentage of difference required between frames to trigger an update (e.g. 1.0 = 1%) +# Helps with flickering on static images, from the non deterministic color algorithm. min_diff_percent = 5.0 diff --git a/flake.nix b/flake.nix index 1580844..1424d81 100644 --- a/flake.nix +++ b/flake.nix @@ -30,8 +30,20 @@ rustfmt clippy rustPlatform.bindgenHook - + dbus + gtk3 + glib + atk + pango + cairo + gdk-pixbuf + xdotool + libappindicator-gtk3 + libayatana-appindicator ]; + shellHook = '' + export LD_LIBRARY_PATH="${pkgs.libappindicator-gtk3}/lib:${pkgs.libayatana-appindicator}/lib:$LD_LIBRARY_PATH" + ''; }; } ); diff --git a/src/eventloop.rs b/src/eventloop.rs index 40dd29e..3208aa0 100644 --- a/src/eventloop.rs +++ b/src/eventloop.rs @@ -1,15 +1,23 @@ use crate::homeassistant::HaClient; use crate::screenshot; use crate::settings::Settings; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::time::{Duration, Instant}; +use tokio::sync::mpsc; use tokio::time::sleep; -pub async fn run_loop(settings: &Settings, ha_client: &HaClient, target_lights: &[String]) { +pub async fn run_loop( + settings: &Settings, + ha_client: &HaClient, + target_lights: &[String], + is_running: Arc, + mut exit_rx: mpsc::Receiver<()>, +) { 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); @@ -17,54 +25,57 @@ pub async fn run_loop(settings: &Settings, ha_client: &HaClient, target_lights: loop { let start_time = Instant::now(); - match screenshot::get_screenshot(0) { - Ok(current_screenshot) => { - let mut should_update = true; + if is_running.load(Ordering::Relaxed) { + 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), - ) + if let Some(prev) = &last_screenshot { + let diff = current_screenshot.diff_percent(prev); + if diff < settings.min_diff_percent { + should_update = false; } - }; - - 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); + 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), } - Err(e) => eprintln!("Screenshot error: {}", e), } // Sleep to maintain target FPS, saturating_sub prevents negative duration panics if we run slow @@ -73,7 +84,7 @@ pub async fn run_loop(settings: &Settings, ha_client: &HaClient, target_lights: tokio::select! { _ = sleep(sleep_duration) => {} - _ = &mut exit_signal => { + _ = exit_rx.recv() => { println!("\nExit signal received, stopping loop..."); break; } diff --git a/src/main.rs b/src/main.rs index cf900e9..3ab5ae7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,8 +3,14 @@ mod homeassistant; mod screenshot; mod settings; -#[tokio::main] -async fn main() { +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use tray_item::{IconSource, TrayItem}; + +fn main() { + let rt = tokio::runtime::Runtime::new().unwrap(); + let rt_handle = rt.handle().clone(); + let settings = settings::Settings::new().unwrap_or_else(|err| { eprintln!("Error loading configuration: {}", err); std::process::exit(1); @@ -16,7 +22,7 @@ async fn main() { settings.lights.clone() } else { println!("Fetching color-supported lights from Home Assistant..."); - match ha_client.get_color_lights().await { + 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); @@ -32,24 +38,104 @@ async fn main() { if settings.restore_on_exit { println!("Creating scene snapshot for restore on exit..."); - if let Err(e) = ha_client - .create_scene_snapshot("ambilight_restore", &target_lights) - .await + if let Err(e) = + rt.block_on(ha_client.create_scene_snapshot("ambilight_restore", &target_lights)) { eprintln!("Failed to create scene snapshot: {}", e); } } - // Run the main application event loop - eventloop::run_loop(&settings, &ha_client, &target_lights).await; + let is_running = Arc::new(AtomicBool::new(true)); + let (exit_tx, exit_rx) = tokio::sync::mpsc::channel(1); - // The event loop returns when a shutdown signal (like Ctrl+C) is received - if settings.restore_on_exit { + // Leak variables to give them a 'static lifetime for the thread + let settings_ref = &*Box::leak(Box::new(settings)); + let ha_client_ref = &*Box::leak(Box::new(ha_client)); + let target_lights_ref = &*Box::leak(Box::new(target_lights)); + + let is_running_clone = is_running.clone(); + + // Spawn the main application event loop on a separate thread + let app_thread = std::thread::spawn(move || { + let rt2 = tokio::runtime::Runtime::new().unwrap(); + rt2.block_on(eventloop::run_loop( + settings_ref, + ha_client_ref, + target_lights_ref, + is_running_clone, + exit_rx, + )); + }); + + // Create the tray application + let mut tray = TrayItem::new("Ambiligth", IconSource::Resource("")).unwrap(); + + let is_running_toggle = is_running.clone(); + let restore_on_toggle = settings_ref.restore_on_exit; + let ha_client_toggle = ha_client_ref; + let target_lights_toggle = target_lights_ref; + 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(()); + }) + .expect("Error setting Ctrl-C handler"); + + // Block the main thread until a quit signal is received + let _ = sync_exit_rx.recv(); + println!("Shutting down..."); + + // Stop the event loop + let _ = exit_tx.try_send(()); + + if settings_ref.restore_on_exit { println!("Restoring lights to original state..."); - if let Err(e) = ha_client.turn_on_scene("scene.ambilight_restore").await { + if let Err(e) = rt.block_on(ha_client_ref.turn_on_scene("scene.ambilight_restore")) { eprintln!("Failed to restore scene: {}", e); } else { println!("Lights restored successfully."); } } + + let _ = app_thread.join(); }