Ambiligth
Ambiligth is a highly optimized, cross-platform Rust application that captures your screen's dominant color in real-time and synchronizes it with your smart lights via Home Assistant. It acts as a software-based "Ambilight" for your monitor, creating an immersive viewing experience.
Features
- Cross-Platform Screen Capture:
- Linux (via
grim-rs) - macOS (via
CoreGraphics) - Windows (via
GDI/BitBlt)
- Linux (via
- Advanced Color Processing: Uses
okmain(Oklab color space) to accurately and efficiently extract the perceptual dominant color of the screen. - Smart Image Diffing: Uses
image-compare(MSSIM simple algorithm) to perceptually compare screen frames. It only sends updates to Home Assistant if the screen content has changed significantly, preventing API spam and light flickering. - RGBW/RGBWW Support: Intelligently preserves brightness and white channels for advanced smart lights, updating only the color values.
- Smooth Transitions & Filtering: Built-in exponential smoothing and configurable Home Assistant transition times.
- Auto-Restore: Optionally takes a snapshot of your lights' state on startup and restores them to their original colors when you exit the program.
Prerequisites
- Rust (cargo)
- A running Home Assistant instance with a Long-Lived Access Token.
- Color-capable smart lights integrated into Home Assistant.
- Optional:
nix(if you are using the provided Nix flake environment).
Configuration
Ambiligth is configured via a config.toml file in the root of the project, or via environment variables (e.g., HA_URL, HA_TOKEN).
Create a config.toml file with the following options:
# Your Home Assistant URL (default: "http://localhost:8123")
ha_url = "http://192.168.1.100:8123"
# Your Long-Lived Access Token (Required)
ha_token = "ey..."
# Target Frames Per Second for light updates (default: 15)
# This is the MAXIMUM rate - lights only update when color actually changes
target_fps = 15
# Color smoothing factor from 0.0 to 1.0 (default: 0.0)
# Higher values mean slower, smoother color transitions.
smoothing = 0.5
# Time in seconds for the lights to transition to the new color (default: 0.5)
transition = 0.5
# Minimum perceptual difference percentage to trigger an update (default: 1.0)
# Increase this if lights are flickering on mostly static screens.
min_diff_percent = 2.0
# Restore lights to their original state on exit (default: true)
restore_on_exit = true
# List of specific light entity IDs to control.
# If left empty, it will auto-detect all color-capable lights.
lights = [
"light.monitor_backlight",
"light.desk_strip"
]
Running the Application
Because screen capture and image processing are highly CPU-intensive, you must run this application in release mode to achieve real-time performance and avoid high CPU usage.
# Standard cargo run
cargo run --release
# If you are using the Nix dev shell
nix develop -c cargo run --release
How It Works
- Capture: Takes a fast, SIMD-optimized screenshot using platform-native APIs.
- Diff: Compares the new screenshot against the previous one using Structural Similarity (MSSIM). If the difference is below
min_diff_percent, it skips the update. - Analyze: Calculates the dominant color of the current frame using the Oklab color space.
- Smooth: Blends the new dominant color with the previous one based on the
smoothingfactor. - Update: Constructs a highly specific JSON payload for Home Assistant, ensuring white channels and brightness are preserved, and sends it asynchronously via
reqwest.
TODO: Parallel Event Loop Restructuring (COMPLETED)
This architecture has been implemented. See src/eventloop.rs for details.
Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ Shared State │
│ │
│ current_screenshot ← Latest captured (screenshot loop) │
│ active_screenshot ← Screenshot that produced target_color│
│ target_color ← Color calculated from active_screenshot│
│ active_color ← Smoothed color sent to lights │
└─────────────────────────────────────────────────────────────┘
Data Flow:
Screenshot Loop (screenshot_fps) Color Calc (on-demand)
│ │
▼ ▼
[current_screenshot] [target_color]
│ │
│ diff(current, active) │
│ > min_diff_percent? │
▼ │
[active_screenshot] ◄───────────────────────┘
│ │
│ Smoothing Loop (target_fps)│
│ │ │
│ ▼ │
│ [active_color] ◄─────────┘
│ │
▼ ▼
Light Update (parallel to all lights)
Implementation Tasks
Phase 1: Settings Update (settings.rs)
- Add
screenshot_fpsconfig option (default: 3) - Remove
transitionoption (smoothing handles transitions)
Phase 2: Create Shared State (state.rs)
pub struct AppState {
pub current_screenshot: RwLock<Option<Arc<Screenshot>>>,
pub active_screenshot: RwLock<Option<Arc<Screenshot>>>,
pub target_color: RwLock<Option<Rgb<f32>>>,
pub active_color: RwLock<Rgb<f32>>,
}
Phase 3: Parallel Event Loop (eventloop.rs)
Thread 1 - Screenshot Loop (std::thread, runs at screenshot_fps)
- Capture screenshot → store in
current_screenshot - Compare
current_screenshotvsactive_screenshot - If diff >
min_diff_percent:- Copy
current_screenshot→active_screenshot - Spawn color calc task
- Copy
On-demand - Color Calculation (tokio::spawn)
- Calculate dominant color from
active_screenshot - Update
target_color
Thread 2 - Smoothing & Light Update (tokio, runs at target_fps)
- Lerp
active_colortowardtarget_colorusingsmoothingfactor - Send
active_colorto all lights in parallel
Phase 4: Parallel Light Updates (homeassistant.rs)
- Refactor
set_lights_colorto update single light, return future - Create
set_lights_color_parallelusingfutures::future::join_all
Concurrency Strategy
| Resource | Lock Type | Writer | Readers |
|---|---|---|---|
| current_screenshot | RwLock | Screenshot loop | Diff check |
| active_screenshot | RwLock | Screenshot loop | Color calc |
| target_color | RwLock | Color calc task | Smoothing loop |
| active_color | RwLock | Smoothing loop | Light update |
Each field has a single writer - no complex synchronization needed.
File Changes Summary
| File | Changes |
|---|---|
settings.rs |
Add screenshot_fps, remove transition |
state.rs |
New - Shared state struct |
eventloop.rs |
Rewrite - Multi-threaded architecture |
homeassistant.rs |
Add parallel light update method |
main.rs |
Wire up new state, pass to eventloop |