diff --git a/src/brightness.rs b/src/brightness.rs index acd2114..e2a6d63 100644 --- a/src/brightness.rs +++ b/src/brightness.rs @@ -3,19 +3,21 @@ //! Screen brightness monitor. //! -//! Reads from `/sys/class/backlight//brightness` and +//! Reads from `/sys/class/backlight//actual_brightness` and //! `/sys/class/backlight//max_brightness`, polling every 2 seconds //! and sending `CMD_BRIGHTNESS` packets to all keyboards when the value //! changes. //! -//! On a KDE6 / Wayland desktop the backlight sysfs path is typically one of: -//! - `/sys/class/backlight/intel_backlight` -//! - `/sys/class/backlight/amdgpu_bl0` -//! - `/sys/class/backlight/acpi_video0` +//! `actual_brightness` is preferred over `brightness` because it reflects the +//! real hardware state; on AMD GPU panels these can diverge. +//! +//! When multiple backlight devices are present (e.g. `amdgpu_bl2` and +//! `nvidia_0`), the device most likely to represent the internal laptop panel +//! is selected automatically (amdgpu/intel > acpi/platform > other > nvidia). //! //! # Future improvement -//! Replace polling with an inotify watch on the `brightness` file so changes -//! are reflected immediately. +//! Replace polling with an inotify watch on the `actual_brightness` file so +//! changes are reflected immediately. use std::fs; use std::path::{Path, PathBuf}; @@ -65,23 +67,54 @@ pub fn monitor(keyboards: Vec) { // Helpers // --------------------------------------------------------------------------- -/// Return the sysfs path to the first available backlight device. +/// Preference score for a backlight device name (higher = more preferred). +/// +/// Internal GPU panels (amdgpu, intel) are preferred over discrete GPU +/// outputs (nvidia), which often report a fixed ceiling value rather than the +/// actual adjustable panel brightness. +fn backlight_score(name: &str) -> u8 { + if name.starts_with("amdgpu_") || name.starts_with("intel_") { + 3 + } else if name.starts_with("acpi_") || name.starts_with("platform_") { + 2 + } else if name.starts_with("nvidia_") { + 0 + } else { + 1 + } +} + +/// Return the sysfs path to the best available backlight device. +/// +/// Scores each candidate by name prefix and returns the highest-scoring one. +/// Requires both `actual_brightness` and `max_brightness` to be present. fn find_backlight() -> Option { let read_dir = fs::read_dir("/sys/class/backlight").ok()?; + + let mut best: Option<(u8, PathBuf)> = None; + for entry in read_dir.flatten() { let path = entry.path(); - // Verify the required files exist before committing to this device. - if path.join("brightness").exists() && path.join("max_brightness").exists() { - return Some(path); + if !path.join("actual_brightness").exists() || !path.join("max_brightness").exists() { + continue; + } + let name = path.file_name()?.to_string_lossy().into_owned(); + let score = backlight_score(&name); + if best.as_ref().map_or(true, |(best_score, _)| score > *best_score) { + best = Some((score, path)); } } - None + + best.map(|(_, path)| path) } /// Read the current brightness as a percentage (0-100) from a sysfs backlight -/// directory, or `None` if the files cannot be read / parsed. +/// directory, or `None` if the files cannot be read or parsed. +/// +/// Uses `actual_brightness` (hardware-reported) rather than `brightness` +/// (software-requested) for accuracy on AMD GPU panels. fn read_brightness(backlight: &Path) -> Option { - let current: u64 = fs::read_to_string(backlight.join("brightness")) + let current: u64 = fs::read_to_string(backlight.join("actual_brightness")) .ok()? .trim() .parse()