├── .github └── workflows │ └── integrate.yaml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── list_displays_async.rs ├── set_brightness_async.rs └── set_brightness_blocking.rs └── src ├── blocking.rs ├── blocking ├── linux.rs └── windows.rs ├── lib.rs ├── linux.rs └── windows.rs /.github/workflows/integrate.yaml: -------------------------------------------------------------------------------- 1 | name: integrate 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | strategy: 6 | matrix: 7 | os: ["ubuntu-latest", "windows-latest"] 8 | runs-on: ${{ matrix.os }} 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Check formatting 12 | run: cargo fmt -- --check 13 | - name: Build 14 | run: cargo build 15 | - name: Test (No features) 16 | run: cargo test --no-default-features 17 | - name: Test (All features) 18 | run: cargo test --all-features 19 | - name: Check clippy 20 | run: cargo clippy --all-features --all-targets -- -D warnings -A clippy::enum_variant_names 21 | 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "brightness" 3 | version = "0.5.0" 4 | authors = ["Stephane Raux ", "Contributors"] 5 | edition = "2018" 6 | description = "Get and set display brightness" 7 | license = "0BSD" 8 | homepage = "https://github.com/stephaneyfx/brightness" 9 | repository = "https://github.com/stephaneyfx/brightness.git" 10 | documentation = "https://docs.rs/brightness" 11 | keywords = ["brightness", "backlight"] 12 | 13 | [features] 14 | async = ["async-trait", "futures", "blocking"] 15 | default = ["async"] 16 | 17 | [dependencies] 18 | async-trait = { version = "0.1.57", optional = true } 19 | blocking = { version = "1.2.0", optional = true } 20 | cfg-if = "1.0.0" 21 | futures = { version = "0.3.24", optional = true } 22 | itertools = "0.10.3" 23 | thiserror = "1.0.34" 24 | 25 | [target.'cfg(target_os = "linux")'.dependencies] 26 | zbus = "3.1.0" 27 | 28 | [target.'cfg(windows)'.dependencies.windows] 29 | version = "0.39.0" 30 | features = [ 31 | "Win32_Foundation", 32 | "Win32_Devices_Display", 33 | "Win32_Graphics_Gdi", 34 | "Win32_Storage_FileSystem", 35 | "Win32_Security", 36 | "Win32_System_IO", 37 | "Win32_UI_WindowsAndMessaging", 38 | "Win32_System_SystemServices", 39 | ] 40 | 41 | [[example]] 42 | name = "list_displays_async" 43 | path = "examples/list_displays_async.rs" 44 | required-features = ["async"] 45 | 46 | [[example]] 47 | name = "set_brightness_async" 48 | path = "examples/set_brightness_async.rs" 49 | required-features = ["async"] 50 | 51 | [package.metadata.docs.rs] 52 | all-features = true 53 | rustdoc-args = ["--cfg", "doc_cfg"] 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2021 Stephane Raux 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 7 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 8 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 9 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 10 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 11 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 12 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Overview 4 | - [📦 crates.io](https://crates.io/crates/brightness) 5 | - [📖 Documentation](https://docs.rs/brightness) 6 | - [⚖ 0BSD license](https://spdx.org/licenses/0BSD.html) 7 | 8 | This crate provides definitions to get and set display brightness. 9 | 10 | Linux and Windows are supported. 11 | 12 | # Example 13 | 14 | ```rust 15 | use brightness::Brightness; 16 | use futures::TryStreamExt; 17 | 18 | async fn show_brightness() -> Result<(), brightness::Error> { 19 | brightness::brightness_devices().try_for_each(|dev| async move { 20 | let name = dev.device_name().await?; 21 | let value = dev.get().await?; 22 | println!("Brightness of device {} is {}%", name, value); 23 | Ok(()) 24 | }).await 25 | } 26 | ``` 27 | 28 | # Linux 29 | 30 | This crate interacts with devices found at `/sys/class/backlight`. This means that the 31 | [ddcci-backlight](https://gitlab.com/ddcci-driver-linux/ddcci-driver-linux#ddcci-backlight-monitor-backlight-driver) 32 | kernel driver is required to control external displays (via DDC/CI). 33 | 34 | Setting brightness is attempted using D-Bus and logind, which requires 35 | [systemd 243 or newer](https://github.com/systemd/systemd/blob/877aa0bdcc2900712b02dac90856f181b93c4e40/NEWS#L262). 36 | If this fails because the method is not available, the desired brightness is written to 37 | `/sys/class/backlight/$DEVICE/brightness`, which requires permission (`udev` rules can help with 38 | that). 39 | 40 | # Contribute 41 | 42 | All contributions shall be licensed under the [0BSD license](https://spdx.org/licenses/0BSD.html). 43 | 44 | 45 | -------------------------------------------------------------------------------- /examples/list_displays_async.rs: -------------------------------------------------------------------------------- 1 | use brightness::{Brightness, BrightnessDevice}; 2 | use futures::{executor::block_on, TryStreamExt}; 3 | 4 | fn main() { 5 | block_on(run()); 6 | } 7 | 8 | async fn run() { 9 | let count = brightness::brightness_devices() 10 | .try_fold(0, |count, dev| async move { 11 | show_brightness(&dev).await?; 12 | Ok(count + 1) 13 | }) 14 | .await 15 | .unwrap(); 16 | println!("Found {} displays", count); 17 | } 18 | 19 | async fn show_brightness(dev: &BrightnessDevice) -> Result<(), brightness::Error> { 20 | println!("Display {}", dev.device_name().await?); 21 | println!("\tBrightness = {}%", dev.get().await?); 22 | show_platform_specific_info(dev).await?; 23 | Ok(()) 24 | } 25 | 26 | #[cfg(windows)] 27 | async fn show_platform_specific_info(dev: &BrightnessDevice) -> Result<(), brightness::Error> { 28 | use brightness::windows::BrightnessExt; 29 | println!("\tDevice description = {}", dev.device_description()?); 30 | println!("\tDevice registry key = {}", dev.device_registry_key()?); 31 | Ok(()) 32 | } 33 | 34 | #[cfg(not(windows))] 35 | async fn show_platform_specific_info(_: &BrightnessDevice) -> Result<(), brightness::Error> { 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /examples/set_brightness_async.rs: -------------------------------------------------------------------------------- 1 | use brightness::{Brightness, BrightnessDevice}; 2 | use futures::{executor::block_on, TryStreamExt}; 3 | use std::env; 4 | 5 | fn main() { 6 | let percentage = env::args() 7 | .nth(1) 8 | .and_then(|a| a.parse().ok()) 9 | .expect("Desired brightness percentage must be given as parameter"); 10 | block_on(run(percentage)); 11 | } 12 | 13 | async fn run(percentage: u32) { 14 | brightness::brightness_devices() 15 | .try_for_each(|mut dev| async move { 16 | show_brightness(&dev).await?; 17 | dev.set(percentage).await?; 18 | show_brightness(&dev).await 19 | }) 20 | .await 21 | .unwrap() 22 | } 23 | 24 | async fn show_brightness(dev: &BrightnessDevice) -> Result<(), brightness::Error> { 25 | println!( 26 | "Brightness of device {} is {}%", 27 | dev.device_name().await?, 28 | dev.get().await? 29 | ); 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /examples/set_brightness_blocking.rs: -------------------------------------------------------------------------------- 1 | use brightness::blocking::{Brightness, BrightnessDevice}; 2 | use std::env; 3 | 4 | fn main() { 5 | let percentage = env::args() 6 | .nth(1) 7 | .and_then(|a| a.parse().ok()) 8 | .expect("Desired brightness percentage must be given as parameter"); 9 | run(percentage); 10 | } 11 | 12 | fn run(percentage: u32) { 13 | brightness::blocking::brightness_devices() 14 | .try_for_each(|dev| { 15 | let dev = dev?; 16 | show_brightness(&dev)?; 17 | dev.set(percentage)?; 18 | show_brightness(&dev) 19 | }) 20 | .unwrap() 21 | } 22 | 23 | fn show_brightness(dev: &BrightnessDevice) -> Result<(), brightness::Error> { 24 | println!( 25 | "Brightness of device {} is {}%", 26 | dev.device_name()?, 27 | dev.get()? 28 | ); 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /src/blocking.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 The brightness project authors. Distributed under the 0BSD license. 2 | 3 | //! The blocking API. 4 | 5 | use crate::Error; 6 | 7 | cfg_if::cfg_if! { 8 | if #[cfg(target_os = "linux")] { 9 | pub(crate) mod linux; 10 | use self::linux as platform; 11 | } else if #[cfg(windows)] { 12 | pub mod windows; 13 | use self::windows as platform; 14 | } else { 15 | compile_error!("unsupported platform"); 16 | } 17 | } 18 | 19 | /// Blocking brightness device. 20 | #[derive(Debug)] 21 | pub struct BrightnessDevice(platform::BlockingDeviceImpl); 22 | 23 | /// Blocking interface to get and set brightness. 24 | pub trait Brightness { 25 | /// Returns the device name. 26 | fn device_name(&self) -> Result; 27 | 28 | /// Returns the current brightness as a percentage. 29 | fn get(&self) -> Result; 30 | 31 | /// Sets the brightness as a percentage. 32 | fn set(&self, percentage: u32) -> Result<(), Error>; 33 | } 34 | 35 | impl Brightness for BrightnessDevice { 36 | fn device_name(&self) -> Result { 37 | self.0.device_name() 38 | } 39 | 40 | fn get(&self) -> Result { 41 | self.0.get() 42 | } 43 | 44 | fn set(&self, percentage: u32) -> Result<(), Error> { 45 | self.0.set(percentage) 46 | } 47 | } 48 | 49 | /// Blocking function that returns all brightness devices on the running system. 50 | pub fn brightness_devices() -> impl Iterator> { 51 | platform::brightness_devices().map(|r| r.map(BrightnessDevice).map_err(Into::into)) 52 | } 53 | -------------------------------------------------------------------------------- /src/blocking/linux.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 The brightness project authors. Distributed under the 0BSD license. 2 | 3 | //! Platform-specific implementation for Linux. 4 | 5 | use crate::Error; 6 | use itertools::Either; 7 | use std::{fs, io, iter::once, path::PathBuf}; 8 | 9 | pub(crate) const BACKLIGHT_DIR: &str = "/sys/class/backlight"; 10 | pub(crate) const USER_DBUS_NAME: &str = "org.freedesktop.login1"; 11 | pub(crate) const SESSION_OBJECT_PATH: &str = "/org/freedesktop/login1/session/auto"; 12 | pub(crate) const SESSION_INTERFACE: &str = "org.freedesktop.login1.Session"; 13 | pub(crate) const SET_BRIGHTNESS_METHOD: &str = "SetBrightness"; 14 | 15 | #[derive(Debug)] 16 | pub(crate) struct BlockingDeviceImpl { 17 | device: String, 18 | } 19 | 20 | impl crate::blocking::Brightness for BlockingDeviceImpl { 21 | fn device_name(&self) -> Result { 22 | Ok(self.device.clone()) 23 | } 24 | 25 | fn get(&self) -> Result { 26 | let max = read_value(&self.device, Value::Max)?; 27 | let actual = read_value(&self.device, Value::Actual)?; 28 | let percentage = if max == 0 { 29 | 0 30 | } else { 31 | (actual * 100 / max) as u32 32 | }; 33 | Ok(percentage) 34 | } 35 | 36 | fn set(&self, percentage: u32) -> Result<(), Error> { 37 | let percentage = percentage.min(100); 38 | let max = read_value(&self.device, Value::Max)?; 39 | let desired_value = (u64::from(percentage) * u64::from(max) / 100) as u32; 40 | let desired = ("backlight", &self.device, desired_value); 41 | let bus = 42 | zbus::blocking::Connection::system().map_err(|e| Error::SettingBrightnessFailed { 43 | device: self.device.clone(), 44 | source: e.into(), 45 | })?; 46 | let response = bus.call_method( 47 | Some(USER_DBUS_NAME), 48 | SESSION_OBJECT_PATH, 49 | Some(SESSION_INTERFACE), 50 | SET_BRIGHTNESS_METHOD, 51 | &desired, 52 | ); 53 | match response { 54 | Ok(_) => Ok(()), 55 | Err(zbus::Error::MethodError(..)) => { 56 | // Setting brightness through dbus may not work on older systems that don't have 57 | // the `SetBrightness` method. Fall back to writing to the brightness file (which 58 | // requires permission). 59 | set_value(&self.device, desired_value)?; 60 | Ok(()) 61 | } 62 | Err(e) => Err(Error::SettingBrightnessFailed { 63 | device: self.device.clone(), 64 | source: e.into(), 65 | }), 66 | } 67 | } 68 | } 69 | 70 | pub(crate) fn brightness_devices() -> impl Iterator> { 71 | match fs::read_dir(BACKLIGHT_DIR) { 72 | Ok(devices) => Either::Left( 73 | devices 74 | .map(|device| { 75 | let device = device.map_err(SysError::ReadingBacklightDirFailed)?; 76 | let path = device.path(); 77 | let keep = path.join(Value::Actual.as_str()).exists() 78 | && path.join(Value::Max.as_str()).exists(); 79 | Ok(device 80 | .file_name() 81 | .into_string() 82 | .ok() 83 | .map(|device| BlockingDeviceImpl { device }) 84 | .filter(|_| keep)) 85 | }) 86 | .filter_map(Result::transpose), 87 | ), 88 | Err(e) => Either::Right(once(Err(SysError::ReadingBacklightDirFailed(e)))), 89 | } 90 | } 91 | 92 | #[derive(Clone, Copy, Debug)] 93 | pub(crate) enum Value { 94 | Actual, 95 | Max, 96 | } 97 | 98 | impl Value { 99 | pub(crate) fn as_str(&self) -> &str { 100 | match self { 101 | Value::Actual => "actual_brightness", 102 | Value::Max => "max_brightness", 103 | } 104 | } 105 | } 106 | 107 | #[derive(Debug, Error)] 108 | pub(crate) enum SysError { 109 | #[error("Failed to read {} directory", BACKLIGHT_DIR)] 110 | ReadingBacklightDirFailed(#[source] io::Error), 111 | #[error("Failed to read backlight device info {}", .path.display())] 112 | ReadingBacklightDeviceFailed { 113 | device: String, 114 | path: PathBuf, 115 | source: io::Error, 116 | }, 117 | #[error("Failed to parse backlight info in {}: {reason}", .path.display())] 118 | ParsingBacklightInfoFailed { 119 | device: String, 120 | path: PathBuf, 121 | reason: String, 122 | }, 123 | #[error("Failed to write brightness to {}", .path.display())] 124 | WritingBrightnessFailed { 125 | device: String, 126 | path: PathBuf, 127 | source: io::Error, 128 | }, 129 | } 130 | 131 | impl From for Error { 132 | fn from(e: SysError) -> Self { 133 | match &e { 134 | SysError::ReadingBacklightDirFailed(_) => Error::ListingDevicesFailed(e.into()), 135 | SysError::ReadingBacklightDeviceFailed { device, .. } 136 | | SysError::ParsingBacklightInfoFailed { device, .. } => { 137 | Error::GettingDeviceInfoFailed { 138 | device: device.clone(), 139 | source: e.into(), 140 | } 141 | } 142 | SysError::WritingBrightnessFailed { device, .. } => Error::SettingBrightnessFailed { 143 | device: device.clone(), 144 | source: e.into(), 145 | }, 146 | } 147 | } 148 | } 149 | 150 | /// Reads a backlight device brightness value from the filesystem. 151 | /// 152 | /// Note: Even though this makes a call to `std::fs`, we are communicating with a kernel pseudo file 153 | /// system so it is safe to call from an async context. 154 | pub(crate) fn read_value(device: &str, name: Value) -> Result { 155 | let path = [BACKLIGHT_DIR, device, name.as_str()] 156 | .iter() 157 | .collect::(); 158 | fs::read_to_string(&path) 159 | .map_err(|source| SysError::ReadingBacklightDeviceFailed { 160 | device: device.into(), 161 | path: path.clone(), 162 | source, 163 | })? 164 | .trim() 165 | .parse::() 166 | .map_err(|e| SysError::ParsingBacklightInfoFailed { 167 | device: device.into(), 168 | path, 169 | reason: e.to_string(), 170 | }) 171 | } 172 | 173 | /// Sets the brightness for a backlight device via the filesystem. 174 | /// 175 | /// This is a blocking operation that can take approximately 10-100ms depending on the device. 176 | pub(crate) fn set_value(device: &str, value: u32) -> Result<(), SysError> { 177 | let path = [BACKLIGHT_DIR, device, "brightness"] 178 | .iter() 179 | .collect::(); 180 | fs::write(&path, value.to_string()).map_err(|source| SysError::WritingBrightnessFailed { 181 | device: device.into(), 182 | path: path.clone(), 183 | source, 184 | })?; 185 | Ok(()) 186 | } 187 | -------------------------------------------------------------------------------- /src/blocking/windows.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 The brightness project authors. Distributed under the 0BSD license. 2 | 3 | //! Platform-specific implementation for Windows. 4 | 5 | use crate::{blocking::BrightnessDevice, Error}; 6 | use itertools::Either; 7 | use std::{ 8 | collections::HashMap, 9 | ffi::{c_void, OsString}, 10 | fmt, 11 | iter::once, 12 | mem::size_of, 13 | os::windows::ffi::OsStringExt, 14 | ptr, 15 | }; 16 | use windows::{ 17 | core::{Error as WinError, PCWSTR}, 18 | Win32::{ 19 | Devices::Display::{ 20 | DestroyPhysicalMonitor, DisplayConfigGetDeviceInfo, GetDisplayConfigBufferSizes, 21 | GetMonitorBrightness, GetNumberOfPhysicalMonitorsFromHMONITOR, 22 | GetPhysicalMonitorsFromHMONITOR, QueryDisplayConfig, SetMonitorBrightness, 23 | DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME, DISPLAYCONFIG_MODE_INFO, 24 | DISPLAYCONFIG_MODE_INFO_TYPE_TARGET, DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL, 25 | DISPLAYCONFIG_PATH_INFO, DISPLAYCONFIG_TARGET_DEVICE_NAME, 26 | DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY, DISPLAYPOLICY_AC, DISPLAYPOLICY_DC, 27 | DISPLAY_BRIGHTNESS, IOCTL_VIDEO_QUERY_DISPLAY_BRIGHTNESS, 28 | IOCTL_VIDEO_QUERY_SUPPORTED_BRIGHTNESS, IOCTL_VIDEO_SET_DISPLAY_BRIGHTNESS, 29 | PHYSICAL_MONITOR, 30 | }, 31 | Foundation::{ 32 | CloseHandle, BOOL, ERROR_ACCESS_DENIED, ERROR_SUCCESS, HANDLE, LPARAM, RECT, 33 | WIN32_ERROR, 34 | }, 35 | Graphics::Gdi::{ 36 | EnumDisplayDevicesW, EnumDisplayMonitors, GetMonitorInfoW, DISPLAY_DEVICEW, 37 | DISPLAY_DEVICE_ACTIVE, HDC, HMONITOR, MONITORINFO, MONITORINFOEXW, 38 | QDC_ONLY_ACTIVE_PATHS, 39 | }, 40 | Storage::FileSystem::{ 41 | CreateFileW, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_READ, FILE_SHARE_WRITE, 42 | OPEN_EXISTING, 43 | }, 44 | System::IO::DeviceIoControl, 45 | UI::WindowsAndMessaging::EDD_GET_DEVICE_INTERFACE_NAME, 46 | }, 47 | }; 48 | 49 | /// Windows-specific brightness functionality. 50 | pub trait BrightnessExt { 51 | /// Returns device description 52 | fn device_description(&self) -> Result; 53 | 54 | /// Returns the device registry key 55 | fn device_registry_key(&self) -> Result; 56 | 57 | /// Returns the device path 58 | fn device_path(&self) -> Result; 59 | } 60 | 61 | #[derive(Debug)] 62 | pub(crate) struct BlockingDeviceImpl { 63 | physical_monitor: WrappedPhysicalMonitor, 64 | file_handle: WrappedFileHandle, 65 | device_name: String, 66 | /// Note: PHYSICAL_MONITOR.szPhysicalMonitorDescription == DISPLAY_DEVICEW.DeviceString 67 | /// Description is **not** unique. 68 | pub(crate) device_description: String, 69 | pub(crate) device_key: String, 70 | /// Note: DISPLAYCONFIG_TARGET_DEVICE_NAME.monitorDevicePath == DISPLAY_DEVICEW.DeviceID (with EDD_GET_DEVICE_INTERFACE_NAME)\ 71 | /// These are in the "DOS Device Path" format. 72 | pub(crate) device_path: String, 73 | output_technology: DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY, 74 | } 75 | 76 | impl BlockingDeviceImpl { 77 | fn is_internal(&self) -> bool { 78 | self.output_technology == DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL 79 | } 80 | } 81 | 82 | /// A safe wrapper for a physical monitor handle that implements `Drop` to call `DestroyPhysicalMonitor` 83 | struct WrappedPhysicalMonitor(HANDLE); 84 | 85 | impl fmt::Debug for WrappedPhysicalMonitor { 86 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 87 | write!(f, "{}", self.0 .0) 88 | } 89 | } 90 | 91 | impl Drop for WrappedPhysicalMonitor { 92 | fn drop(&mut self) { 93 | unsafe { 94 | DestroyPhysicalMonitor(self.0); 95 | } 96 | } 97 | } 98 | 99 | /// A safe wrapper for a windows HANDLE that implements `Drop` to call `CloseHandle` 100 | struct WrappedFileHandle(HANDLE); 101 | 102 | impl fmt::Debug for WrappedFileHandle { 103 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 104 | write!(f, "{}", self.0 .0) 105 | } 106 | } 107 | 108 | impl Drop for WrappedFileHandle { 109 | fn drop(&mut self) { 110 | unsafe { 111 | CloseHandle(self.0); 112 | } 113 | } 114 | } 115 | 116 | #[inline] 117 | fn flag_set + std::cmp::PartialEq + Copy>(t: T, flag: T) -> bool { 118 | t & flag == flag 119 | } 120 | 121 | impl crate::blocking::Brightness for BlockingDeviceImpl { 122 | fn device_name(&self) -> Result { 123 | Ok(self.device_name.clone()) 124 | } 125 | 126 | fn get(&self) -> Result { 127 | Ok(if self.is_internal() { 128 | ioctl_query_display_brightness(self)? 129 | } else { 130 | ddcci_get_monitor_brightness(self)?.get_current_percentage() 131 | }) 132 | } 133 | 134 | fn set(&self, percentage: u32) -> Result<(), Error> { 135 | if self.is_internal() { 136 | let supported = ioctl_query_supported_brightness(self)?; 137 | let new_value = supported.get_nearest(percentage); 138 | ioctl_set_display_brightness(self, new_value)?; 139 | } else { 140 | let current = ddcci_get_monitor_brightness(self)?; 141 | let new_value = current.percentage_to_current(percentage); 142 | ddcci_set_monitor_brightness(self, new_value)?; 143 | } 144 | Ok(()) 145 | } 146 | } 147 | 148 | pub(crate) fn brightness_devices() -> impl Iterator> { 149 | unsafe { 150 | let device_info_map = match get_device_info_map() { 151 | Ok(info) => info, 152 | Err(e) => return Either::Right(once(Err(e))), 153 | }; 154 | let hmonitors = match enum_display_monitors() { 155 | Ok(monitors) => monitors, 156 | Err(e) => return Either::Right(once(Err(e))), 157 | }; 158 | Either::Left(hmonitors.into_iter().flat_map(move |hmonitor| { 159 | let physical_monitors = match get_physical_monitors_from_hmonitor(hmonitor) { 160 | Ok(p) => p, 161 | Err(e) => return vec![Err(e)], 162 | }; 163 | let display_devices = match get_display_devices_from_hmonitor(hmonitor) { 164 | Ok(p) => p, 165 | Err(e) => return vec![Err(e)], 166 | }; 167 | if display_devices.len() != physical_monitors.len() { 168 | // There doesn't seem to be any way to directly associate a physical monitor 169 | // handle with the equivalent display device, other than by array indexing 170 | // https://stackoverflow.com/questions/63095216/how-to-associate-physical-monitor-with-monitor-deviceid 171 | return vec![Err(SysError::EnumerationMismatch)]; 172 | } 173 | physical_monitors 174 | .into_iter() 175 | .zip(display_devices) 176 | .filter_map(|(physical_monitor, display_device)| { 177 | get_file_handle_for_display_device(&display_device) 178 | .transpose() 179 | .map(|file_handle| (physical_monitor, display_device, file_handle)) 180 | }) 181 | .map(|(physical_monitor, display_device, file_handle)| { 182 | let file_handle = file_handle?; 183 | let info = device_info_map 184 | .get(&display_device.DeviceID) 185 | .ok_or(SysError::DeviceInfoMissing)?; 186 | Ok(BlockingDeviceImpl { 187 | physical_monitor, 188 | file_handle, 189 | device_name: wchar_to_string(&display_device.DeviceName), 190 | device_description: wchar_to_string(&display_device.DeviceString), 191 | device_key: wchar_to_string(&display_device.DeviceKey), 192 | device_path: wchar_to_string(&display_device.DeviceID), 193 | output_technology: info.outputTechnology, 194 | }) 195 | }) 196 | .collect() 197 | })) 198 | } 199 | } 200 | 201 | /// Returns a `HashMap` of Device Path to `DISPLAYCONFIG_TARGET_DEVICE_NAME`.\ 202 | /// This can be used to find the `DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY` for a monitor.\ 203 | /// The output technology is used to determine if a device is internal or external. 204 | unsafe fn get_device_info_map( 205 | ) -> Result, SysError> { 206 | let mut path_count = 0; 207 | let mut mode_count = 0; 208 | check_status( 209 | GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &mut path_count, &mut mode_count), 210 | SysError::GetDisplayConfigBufferSizesFailed, 211 | )?; 212 | let mut display_paths = vec![DISPLAYCONFIG_PATH_INFO::default(); path_count as usize]; 213 | let mut display_modes = vec![DISPLAYCONFIG_MODE_INFO::default(); mode_count as usize]; 214 | check_status( 215 | QueryDisplayConfig( 216 | QDC_ONLY_ACTIVE_PATHS, 217 | &mut path_count, 218 | display_paths.as_mut_ptr(), 219 | &mut mode_count, 220 | display_modes.as_mut_ptr(), 221 | std::ptr::null_mut(), 222 | ), 223 | SysError::QueryDisplayConfigFailed, 224 | )?; 225 | display_modes 226 | .into_iter() 227 | .filter(|mode| mode.infoType == DISPLAYCONFIG_MODE_INFO_TYPE_TARGET) 228 | .flat_map(|mode| { 229 | let mut device_name = DISPLAYCONFIG_TARGET_DEVICE_NAME::default(); 230 | device_name.header.size = size_of::() as u32; 231 | device_name.header.adapterId = mode.adapterId; 232 | device_name.header.id = mode.id; 233 | device_name.header.r#type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME; 234 | let result = to_win32_error(DisplayConfigGetDeviceInfo(&mut device_name.header)); 235 | match result { 236 | ERROR_SUCCESS => Some(Ok((device_name.monitorDevicePath, device_name))), 237 | // This error occurs if the calling process does not have access to the current desktop or is running on a remote session. 238 | ERROR_ACCESS_DENIED => None, 239 | _ => Some(Err(SysError::DisplayConfigGetDeviceInfoFailed( 240 | result.into(), 241 | ))), 242 | } 243 | }) 244 | .collect() 245 | } 246 | 247 | /// Calls `EnumDisplayMonitors` and returns a list of `HMONITOR` handles.\ 248 | /// Note that a `HMONITOR` is a logical construct that may correspond to multiple physical monitors.\ 249 | /// e.g. when in "Duplicate" mode two physical monitors will belong to the same `HMONITOR` 250 | unsafe fn enum_display_monitors() -> Result, SysError> { 251 | unsafe extern "system" fn enum_monitors( 252 | handle: HMONITOR, 253 | _: HDC, 254 | _: *mut RECT, 255 | data: LPARAM, 256 | ) -> BOOL { 257 | let monitors = &mut *(data.0 as *mut Vec); 258 | monitors.push(handle); 259 | true.into() 260 | } 261 | let mut hmonitors = Vec::::new(); 262 | EnumDisplayMonitors( 263 | HDC::default(), 264 | ptr::null_mut(), 265 | Some(enum_monitors), 266 | LPARAM(&mut hmonitors as *mut _ as isize), 267 | ) 268 | .ok() 269 | .map_err(SysError::EnumDisplayMonitorsFailed)?; 270 | Ok(hmonitors) 271 | } 272 | 273 | /// Gets the list of `PHYSICAL_MONITOR` handles that belong to a `HMONITOR`.\ 274 | /// These handles are required for use with the DDC/CI functions, however a valid handle will still 275 | /// be returned for non DDC/CI monitors and also Remote Desktop Session displays.\ 276 | /// Also note that physically connected but disabled (inactive) monitors are not returned from this API. 277 | unsafe fn get_physical_monitors_from_hmonitor( 278 | hmonitor: HMONITOR, 279 | ) -> Result, SysError> { 280 | let mut physical_number: u32 = 0; 281 | BOOL(GetNumberOfPhysicalMonitorsFromHMONITOR( 282 | hmonitor, 283 | &mut physical_number, 284 | )) 285 | .ok() 286 | .map_err(SysError::GetPhysicalMonitorsFailed)?; 287 | let mut raw_physical_monitors = vec![PHYSICAL_MONITOR::default(); physical_number as usize]; 288 | // Allocate first so that pushing the wrapped handles always succeeds. 289 | let mut physical_monitors = Vec::with_capacity(raw_physical_monitors.len()); 290 | BOOL(GetPhysicalMonitorsFromHMONITOR( 291 | hmonitor, 292 | &mut raw_physical_monitors, 293 | )) 294 | .ok() 295 | .map_err(SysError::GetPhysicalMonitorsFailed)?; 296 | // Transform immediately into WrappedPhysicalMonitor so the handles don't leak 297 | raw_physical_monitors 298 | .into_iter() 299 | .for_each(|pm| physical_monitors.push(WrappedPhysicalMonitor(pm.hPhysicalMonitor))); 300 | Ok(physical_monitors) 301 | } 302 | 303 | /// Gets the list of display devices that belong to a `HMONITOR`.\ 304 | /// Due to the `EDD_GET_DEVICE_INTERFACE_NAME` flag, the `DISPLAY_DEVICEW` will contain the DOS 305 | /// device path for each monitor in the `DeviceID` field.\ 306 | /// Note: Connected but inactive displays have been filtered out. 307 | unsafe fn get_display_devices_from_hmonitor( 308 | hmonitor: HMONITOR, 309 | ) -> Result, SysError> { 310 | let mut info = MONITORINFOEXW::default(); 311 | info.monitorInfo.cbSize = size_of::() as u32; 312 | let info_ptr = &mut info as *mut _ as *mut MONITORINFO; 313 | GetMonitorInfoW(hmonitor, info_ptr) 314 | .ok() 315 | .map_err(SysError::GetMonitorInfoFailed)?; 316 | Ok((0..) 317 | .map_while(|device_number| { 318 | let mut device = DISPLAY_DEVICEW { 319 | cb: size_of::() as u32, 320 | ..Default::default() 321 | }; 322 | EnumDisplayDevicesW( 323 | PCWSTR(info.szDevice.as_ptr()), 324 | device_number, 325 | &mut device, 326 | EDD_GET_DEVICE_INTERFACE_NAME, 327 | ) 328 | .as_bool() 329 | .then(|| device) 330 | }) 331 | .filter(|device| flag_set(device.StateFlags, DISPLAY_DEVICE_ACTIVE)) 332 | .collect()) 333 | } 334 | 335 | /// Opens and returns a file handle for a display device using its DOS device path.\ 336 | /// These handles are only used for the `DeviceIoControl` API (for internal displays); a 337 | /// handle can still be returned for external displays, but it should not be used.\ 338 | /// A `None` value means that a handle could not be opened, but this was for an expected reason, 339 | /// indicating this display device should be skipped. 340 | unsafe fn get_file_handle_for_display_device( 341 | display_device: &DISPLAY_DEVICEW, 342 | ) -> Result, SysError> { 343 | CreateFileW( 344 | PCWSTR(display_device.DeviceID.as_ptr()), 345 | FILE_GENERIC_READ | FILE_GENERIC_WRITE, 346 | FILE_SHARE_READ | FILE_SHARE_WRITE, 347 | ptr::null_mut(), 348 | OPEN_EXISTING, 349 | Default::default(), 350 | HANDLE::default(), 351 | ) 352 | .map(|h| Some(WrappedFileHandle(h))) 353 | .or_else(|e| { 354 | // This error occurs for virtual devices e.g. Remote Desktop 355 | // sessions - they are not real monitors 356 | (e.code() == ERROR_ACCESS_DENIED.to_hresult()) 357 | .then_some(None) 358 | .ok_or_else(|| SysError::OpeningMonitorDeviceInterfaceHandleFailed { 359 | device_name: wchar_to_string(&display_device.DeviceName), 360 | source: e, 361 | }) 362 | }) 363 | } 364 | 365 | #[derive(Clone, Debug, Error)] 366 | pub(crate) enum SysError { 367 | #[error("Failed to enumerate device monitors")] 368 | EnumDisplayMonitorsFailed(#[source] WinError), 369 | #[error("Failed to get display config buffer sizes")] 370 | GetDisplayConfigBufferSizesFailed(#[source] WinError), 371 | #[error("Failed to query display config")] 372 | QueryDisplayConfigFailed(#[source] WinError), 373 | #[error("Failed to get display config device info")] 374 | DisplayConfigGetDeviceInfoFailed(#[source] WinError), 375 | #[error("Failed to get monitor info")] 376 | GetMonitorInfoFailed(#[source] WinError), 377 | #[error("Failed to get physical monitors from the HMONITOR")] 378 | GetPhysicalMonitorsFailed(#[source] WinError), 379 | #[error( 380 | "The length of GetPhysicalMonitorsFromHMONITOR() and EnumDisplayDevicesW() results did not \ 381 | match, this could be because monitors were connected/disconnected while loading devices" 382 | )] 383 | EnumerationMismatch, 384 | #[error( 385 | "Unable to find a matching device info for this display device, this could be because monitors \ 386 | were connected while loading devices" 387 | )] 388 | DeviceInfoMissing, 389 | #[error("Failed to open monitor interface handle (CreateFileW)")] 390 | OpeningMonitorDeviceInterfaceHandleFailed { 391 | device_name: String, 392 | source: WinError, 393 | }, 394 | #[error("Failed to query supported brightness (IOCTL)")] 395 | IoctlQuerySupportedBrightnessFailed { 396 | device_name: String, 397 | source: WinError, 398 | }, 399 | #[error("Failed to query display brightness (IOCTL)")] 400 | IoctlQueryDisplayBrightnessFailed { 401 | device_name: String, 402 | source: WinError, 403 | }, 404 | #[error("Unexpected response when querying display brightness (IOCTL)")] 405 | IoctlQueryDisplayBrightnessUnexpectedResponse { device_name: String }, 406 | #[error("Failed to get monitor brightness (DDCCI)")] 407 | GettingMonitorBrightnessFailed { 408 | device_name: String, 409 | source: WinError, 410 | }, 411 | #[error("Failed to set monitor brightness (IOCTL)")] 412 | IoctlSetBrightnessFailed { 413 | device_name: String, 414 | source: WinError, 415 | }, 416 | #[error("Failed to set monitor brightness (DDCCI)")] 417 | SettingBrightnessFailed { 418 | device_name: String, 419 | source: WinError, 420 | }, 421 | } 422 | 423 | impl From for Error { 424 | fn from(e: SysError) -> Self { 425 | match &e { 426 | SysError::EnumerationMismatch 427 | | SysError::DeviceInfoMissing 428 | | SysError::GetDisplayConfigBufferSizesFailed(..) 429 | | SysError::QueryDisplayConfigFailed(..) 430 | | SysError::DisplayConfigGetDeviceInfoFailed(..) 431 | | SysError::GetPhysicalMonitorsFailed(..) 432 | | SysError::EnumDisplayMonitorsFailed(..) 433 | | SysError::GetMonitorInfoFailed(..) 434 | | SysError::OpeningMonitorDeviceInterfaceHandleFailed { .. } => { 435 | Error::ListingDevicesFailed(Box::new(e)) 436 | } 437 | SysError::IoctlQuerySupportedBrightnessFailed { device_name, .. } 438 | | SysError::IoctlQueryDisplayBrightnessFailed { device_name, .. } 439 | | SysError::IoctlQueryDisplayBrightnessUnexpectedResponse { device_name } 440 | | SysError::GettingMonitorBrightnessFailed { device_name, .. } => { 441 | Error::GettingDeviceInfoFailed { 442 | device: device_name.clone(), 443 | source: Box::new(e), 444 | } 445 | } 446 | SysError::SettingBrightnessFailed { device_name, .. } 447 | | SysError::IoctlSetBrightnessFailed { device_name, .. } => { 448 | Error::SettingBrightnessFailed { 449 | device: device_name.clone(), 450 | source: Box::new(e), 451 | } 452 | } 453 | } 454 | } 455 | } 456 | 457 | fn wchar_to_string(s: &[u16]) -> String { 458 | let end = s.iter().position(|&x| x == 0).unwrap_or(s.len()); 459 | let truncated = &s[0..end]; 460 | OsString::from_wide(truncated).to_string_lossy().into() 461 | } 462 | 463 | fn to_win32_error(status: i32) -> WIN32_ERROR { 464 | WIN32_ERROR(status as u32) 465 | } 466 | 467 | fn check_status(status: i32, f: F) -> Result<(), E> 468 | where 469 | F: FnOnce(WinError) -> E, 470 | { 471 | to_win32_error(status).ok().map_err(f) 472 | } 473 | 474 | #[derive(Debug, Default)] 475 | struct DdcciBrightnessValues { 476 | min: u32, 477 | current: u32, 478 | max: u32, 479 | } 480 | 481 | impl DdcciBrightnessValues { 482 | fn get_current_percentage(&self) -> u32 { 483 | let normalised_max = (self.max - self.min) as f64; 484 | let normalised_current = (self.current - self.min) as f64; 485 | (normalised_current / normalised_max * 100.0).round() as u32 486 | } 487 | 488 | fn percentage_to_current(&self, percentage: u32) -> u32 { 489 | let normalised_max = (self.max - self.min) as f64; 490 | let fraction = percentage as f64 / 100.0; 491 | let normalised_current = fraction * normalised_max; 492 | normalised_current.round() as u32 + self.min 493 | } 494 | } 495 | 496 | fn ddcci_get_monitor_brightness( 497 | device: &BlockingDeviceImpl, 498 | ) -> Result { 499 | unsafe { 500 | let mut v = DdcciBrightnessValues::default(); 501 | BOOL(GetMonitorBrightness( 502 | device.physical_monitor.0, 503 | &mut v.min, 504 | &mut v.current, 505 | &mut v.max, 506 | )) 507 | .ok() 508 | .map(|_| v) 509 | .map_err(|e| SysError::GettingMonitorBrightnessFailed { 510 | device_name: device.device_name.clone(), 511 | source: e, 512 | }) 513 | } 514 | } 515 | 516 | fn ddcci_set_monitor_brightness(device: &BlockingDeviceImpl, value: u32) -> Result<(), SysError> { 517 | unsafe { 518 | BOOL(SetMonitorBrightness(device.physical_monitor.0, value)) 519 | .ok() 520 | .map_err(|e| SysError::SettingBrightnessFailed { 521 | device_name: device.device_name.clone(), 522 | source: e, 523 | }) 524 | } 525 | } 526 | 527 | /// Each level is a value from 0 to 100 528 | #[derive(Debug)] 529 | struct IoctlSupportedBrightnessLevels(Vec); 530 | 531 | impl IoctlSupportedBrightnessLevels { 532 | fn get_nearest(&self, percentage: u32) -> u8 { 533 | self.0 534 | .iter() 535 | .copied() 536 | .min_by_key(|&num| (num as i64 - percentage as i64).abs()) 537 | .unwrap_or(0) 538 | } 539 | } 540 | 541 | fn ioctl_query_supported_brightness( 542 | device: &BlockingDeviceImpl, 543 | ) -> Result { 544 | unsafe { 545 | let mut bytes_returned = 0; 546 | let mut out_buffer = Vec::::with_capacity(256); 547 | DeviceIoControl( 548 | device.file_handle.0, 549 | IOCTL_VIDEO_QUERY_SUPPORTED_BRIGHTNESS, 550 | ptr::null_mut(), 551 | 0, 552 | out_buffer.as_mut_ptr() as *mut c_void, 553 | out_buffer.capacity() as u32, 554 | &mut bytes_returned, 555 | ptr::null_mut(), 556 | ) 557 | .ok() 558 | .map(|_| { 559 | out_buffer.set_len(bytes_returned as usize); 560 | IoctlSupportedBrightnessLevels(out_buffer) 561 | }) 562 | .map_err(|e| SysError::IoctlQuerySupportedBrightnessFailed { 563 | device_name: device.device_name.clone(), 564 | source: e, 565 | }) 566 | } 567 | } 568 | 569 | fn ioctl_query_display_brightness(device: &BlockingDeviceImpl) -> Result { 570 | unsafe { 571 | let mut bytes_returned = 0; 572 | let mut display_brightness = DISPLAY_BRIGHTNESS::default(); 573 | DeviceIoControl( 574 | device.file_handle.0, 575 | IOCTL_VIDEO_QUERY_DISPLAY_BRIGHTNESS, 576 | ptr::null_mut(), 577 | 0, 578 | &mut display_brightness as *mut DISPLAY_BRIGHTNESS as *mut c_void, 579 | size_of::() as u32, 580 | &mut bytes_returned, 581 | ptr::null_mut(), 582 | ) 583 | .ok() 584 | .map_err(|e| SysError::IoctlQueryDisplayBrightnessFailed { 585 | device_name: device.device_name.clone(), 586 | source: e, 587 | }) 588 | .and_then(|_| match display_brightness.ucDisplayPolicy as u32 { 589 | DISPLAYPOLICY_AC => { 590 | // This is a value between 0 and 100. 591 | Ok(display_brightness.ucACBrightness as u32) 592 | } 593 | DISPLAYPOLICY_DC => { 594 | // This is a value between 0 and 100. 595 | Ok(display_brightness.ucDCBrightness as u32) 596 | } 597 | _ => Err(SysError::IoctlQueryDisplayBrightnessUnexpectedResponse { 598 | device_name: device.device_name.clone(), 599 | }), 600 | }) 601 | } 602 | } 603 | 604 | fn ioctl_set_display_brightness(device: &BlockingDeviceImpl, value: u8) -> Result<(), SysError> { 605 | // Seems to currently be missing from metadata 606 | const DISPLAYPOLICY_BOTH: u8 = 3; 607 | unsafe { 608 | let mut display_brightness = DISPLAY_BRIGHTNESS { 609 | ucACBrightness: value, 610 | ucDCBrightness: value, 611 | ucDisplayPolicy: DISPLAYPOLICY_BOTH, 612 | }; 613 | let mut bytes_returned = 0; 614 | DeviceIoControl( 615 | device.file_handle.0, 616 | IOCTL_VIDEO_SET_DISPLAY_BRIGHTNESS, 617 | &mut display_brightness as *mut DISPLAY_BRIGHTNESS as *mut c_void, 618 | size_of::() as u32, 619 | ptr::null_mut(), 620 | 0, 621 | &mut bytes_returned, 622 | ptr::null_mut(), 623 | ) 624 | .ok() 625 | .map(|_| { 626 | // There is a bug where if the IOCTL_VIDEO_QUERY_DISPLAY_BRIGHTNESS is 627 | // called immediately after then it won't show the newly updated values 628 | // Doing a very tiny sleep seems to mitigate this 629 | std::thread::sleep(std::time::Duration::from_nanos(1)); 630 | }) 631 | .map_err(|e| SysError::IoctlSetBrightnessFailed { 632 | device_name: device.device_name.clone(), 633 | source: e, 634 | }) 635 | } 636 | } 637 | 638 | impl BrightnessExt for BrightnessDevice { 639 | fn device_description(&self) -> Result { 640 | Ok(self.0.device_description.clone()) 641 | } 642 | 643 | fn device_registry_key(&self) -> Result { 644 | Ok(self.0.device_key.clone()) 645 | } 646 | 647 | fn device_path(&self) -> Result { 648 | Ok(self.0.device_path.clone()) 649 | } 650 | } 651 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 The brightness project authors. Distributed under the 0BSD license. 2 | 3 | //! # Overview 4 | //! - [📦 crates.io](https://crates.io/crates/brightness) 5 | //! - [📖 Documentation](https://docs.rs/brightness) 6 | //! - [⚖ 0BSD license](https://spdx.org/licenses/0BSD.html) 7 | //! 8 | //! This crate provides definitions to get and set display brightness. 9 | //! 10 | //! Linux and Windows are supported. 11 | //! 12 | //! # Example 13 | //! 14 | //! ```rust 15 | //! # #[cfg(feature = "async")] 16 | //! # mod doctest { 17 | //! use brightness::Brightness; 18 | //! use futures::TryStreamExt; 19 | //! 20 | //! async fn show_brightness() -> Result<(), brightness::Error> { 21 | //! brightness::brightness_devices().try_for_each(|dev| async move { 22 | //! let name = dev.device_name().await?; 23 | //! let value = dev.get().await?; 24 | //! println!("Brightness of device {} is {}%", name, value); 25 | //! Ok(()) 26 | //! }).await 27 | //! } 28 | //! # } 29 | //! ``` 30 | //! 31 | //! # Linux 32 | //! 33 | //! This crate interacts with devices found at `/sys/class/backlight`. This means that the 34 | //! [ddcci-backlight](https://gitlab.com/ddcci-driver-linux/ddcci-driver-linux#ddcci-backlight-monitor-backlight-driver) 35 | //! kernel driver is required to control external displays (via DDC/CI). 36 | //! 37 | //! Setting brightness is attempted using D-Bus and logind, which requires 38 | //! [systemd 243 or newer](https://github.com/systemd/systemd/blob/877aa0bdcc2900712b02dac90856f181b93c4e40/NEWS#L262). 39 | //! If this fails because the method is not available, the desired brightness is written to 40 | //! `/sys/class/backlight/$DEVICE/brightness`, which requires permission (`udev` rules can help with 41 | //! that). 42 | //! 43 | //! # Contribute 44 | //! 45 | //! All contributions shall be licensed under the [0BSD license](https://spdx.org/licenses/0BSD.html). 46 | 47 | #![deny(warnings)] 48 | #![deny(missing_docs)] 49 | #![cfg_attr(doc_cfg, feature(doc_cfg))] 50 | 51 | use std::error::Error as StdError; 52 | use thiserror::Error; 53 | 54 | pub mod blocking; 55 | 56 | #[cfg(feature = "async")] 57 | #[cfg_attr(doc_cfg, doc(cfg(feature = "async")))] 58 | cfg_if::cfg_if! { 59 | if #[cfg(target_os = "linux")] { 60 | mod linux; 61 | use self::linux as platform; 62 | } else if #[cfg(windows)] { 63 | pub mod windows; 64 | use self::windows as platform; 65 | } else { 66 | compile_error!("unsupported platform"); 67 | } 68 | } 69 | 70 | #[cfg(feature = "async")] 71 | #[cfg_attr(doc_cfg, doc(cfg(feature = "async")))] 72 | mod r#async { 73 | use super::{platform, Error}; 74 | use async_trait::async_trait; 75 | use futures::{Stream, StreamExt}; 76 | 77 | /// Async interface to get and set brightness. 78 | #[async_trait] 79 | pub trait Brightness { 80 | /// Returns the device name. 81 | async fn device_name(&self) -> Result; 82 | 83 | /// Returns the current brightness as a percentage. 84 | async fn get(&self) -> Result; 85 | 86 | /// Sets the brightness as a percentage. 87 | async fn set(&mut self, percentage: u32) -> Result<(), Error>; 88 | } 89 | 90 | /// Async brightness device. 91 | #[derive(Debug)] 92 | pub struct BrightnessDevice(pub(crate) platform::AsyncDeviceImpl); 93 | 94 | #[async_trait] 95 | impl Brightness for BrightnessDevice { 96 | async fn device_name(&self) -> Result { 97 | self.0.device_name().await 98 | } 99 | 100 | async fn get(&self) -> Result { 101 | self.0.get().await 102 | } 103 | 104 | async fn set(&mut self, percentage: u32) -> Result<(), Error> { 105 | self.0.set(percentage).await 106 | } 107 | } 108 | 109 | /// Returns all brightness devices on the running system. 110 | pub fn brightness_devices() -> impl Stream> { 111 | platform::brightness_devices().map(|r| r.map(BrightnessDevice).map_err(Into::into)) 112 | } 113 | } 114 | 115 | #[cfg(feature = "async")] 116 | pub use r#async::{brightness_devices, Brightness, BrightnessDevice}; 117 | 118 | /// Errors used in this API 119 | #[derive(Debug, Error)] 120 | #[non_exhaustive] 121 | pub enum Error { 122 | /// Getting a list of brightness devices failed 123 | #[error("Failed to list brightness devices")] 124 | ListingDevicesFailed(#[source] Box), 125 | 126 | /// Getting device information failed 127 | #[error("Failed to get brightness device {device} information")] 128 | GettingDeviceInfoFailed { 129 | /// Device name 130 | device: String, 131 | /// Cause 132 | source: Box, 133 | }, 134 | 135 | /// Setting brightness failed 136 | #[error("Setting brightness failed for device {device}")] 137 | SettingBrightnessFailed { 138 | /// Device name 139 | device: String, 140 | /// Cause 141 | source: Box, 142 | }, 143 | } 144 | -------------------------------------------------------------------------------- /src/linux.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 The brightness project authors. Distributed under the 0BSD license. 2 | 3 | //! Platform-specific implementation for Linux. 4 | 5 | use crate::{ 6 | blocking::linux::{ 7 | read_value, SysError, Value, BACKLIGHT_DIR, SESSION_INTERFACE, SESSION_OBJECT_PATH, 8 | SET_BRIGHTNESS_METHOD, USER_DBUS_NAME, 9 | }, 10 | Error, 11 | }; 12 | use async_trait::async_trait; 13 | use blocking::unblock; 14 | use futures::{future::ready, Stream, StreamExt}; 15 | 16 | #[derive(Debug)] 17 | pub(crate) struct AsyncDeviceImpl { 18 | device: String, 19 | } 20 | 21 | #[async_trait] 22 | impl crate::Brightness for AsyncDeviceImpl { 23 | async fn device_name(&self) -> Result { 24 | Ok(self.device.clone()) 25 | } 26 | 27 | async fn get(&self) -> Result { 28 | let max = read_value(&self.device, Value::Max)?; 29 | let actual = read_value(&self.device, Value::Actual)?; 30 | let percentage = if max == 0 { 31 | 0 32 | } else { 33 | (actual * 100 / max) as u32 34 | }; 35 | Ok(percentage) 36 | } 37 | 38 | async fn set(&mut self, percentage: u32) -> Result<(), Error> { 39 | let percentage = percentage.min(100); 40 | let max = read_value(&self.device, Value::Max)?; 41 | let desired_value = (u64::from(percentage) * u64::from(max) / 100) as u32; 42 | let desired = ("backlight", &self.device, desired_value); 43 | let bus = zbus::Connection::system() 44 | .await 45 | .map_err(|e| Error::SettingBrightnessFailed { 46 | device: self.device.clone(), 47 | source: e.into(), 48 | })?; 49 | let response = bus 50 | .call_method( 51 | Some(USER_DBUS_NAME), 52 | SESSION_OBJECT_PATH, 53 | Some(SESSION_INTERFACE), 54 | SET_BRIGHTNESS_METHOD, 55 | &desired, 56 | ) 57 | .await; 58 | match response { 59 | Ok(_) => Ok(()), 60 | Err(zbus::Error::MethodError(..)) => { 61 | // Setting brightness through dbus may not work on older systems that don't have 62 | // the `SetBrightness` method. Fall back to writing to the brightness file (which 63 | // requires permission). 64 | set_value(self.device.clone(), desired_value).await?; 65 | Ok(()) 66 | } 67 | Err(e) => Err(Error::SettingBrightnessFailed { 68 | device: self.device.clone(), 69 | source: e.into(), 70 | }), 71 | } 72 | } 73 | } 74 | 75 | pub(crate) fn brightness_devices() -> impl Stream> { 76 | match std::fs::read_dir(BACKLIGHT_DIR) { 77 | Ok(devices) => futures::stream::iter( 78 | devices 79 | .map(|device| { 80 | let device = device.map_err(SysError::ReadingBacklightDirFailed)?; 81 | let path = device.path(); 82 | let keep = path.join(Value::Actual.as_str()).exists() 83 | && path.join(Value::Max.as_str()).exists(); 84 | Ok(device 85 | .file_name() 86 | .into_string() 87 | .ok() 88 | .map(|device| AsyncDeviceImpl { device }) 89 | .filter(|_| keep)) 90 | }) 91 | .filter_map(Result::transpose), 92 | ) 93 | .right_stream(), 94 | Err(e) => { 95 | futures::stream::once(ready(Err(SysError::ReadingBacklightDirFailed(e)))).left_stream() 96 | } 97 | } 98 | } 99 | 100 | async fn set_value(device: String, value: u32) -> Result<(), SysError> { 101 | unblock(move || crate::blocking::linux::set_value(&device, value)).await 102 | } 103 | -------------------------------------------------------------------------------- /src/windows.rs: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 The brightness project authors. Distributed under the 0BSD license. 2 | 3 | //! Platform-specific implementation for Windows. 4 | 5 | pub use crate::blocking::windows::BrightnessExt; 6 | 7 | use crate::{ 8 | blocking::{ 9 | windows::{BlockingDeviceImpl, SysError}, 10 | Brightness, 11 | }, 12 | BrightnessDevice, Error, 13 | }; 14 | use async_trait::async_trait; 15 | use blocking::unblock; 16 | use futures::{stream, FutureExt, Stream, StreamExt}; 17 | use std::sync::Arc; 18 | 19 | #[derive(Debug)] 20 | pub(crate) struct AsyncDeviceImpl(Arc); 21 | 22 | // Windows doesn't have an async C API for monitors, so we will instead spawn the blocking tasks on 23 | // background threads. 24 | #[async_trait] 25 | impl crate::Brightness for AsyncDeviceImpl { 26 | async fn device_name(&self) -> Result { 27 | self.0.device_name() 28 | } 29 | 30 | async fn get(&self) -> Result { 31 | let cloned = Arc::clone(&self.0); 32 | unblock(move || cloned.get()).await 33 | } 34 | 35 | async fn set(&mut self, percentage: u32) -> Result<(), Error> { 36 | let cloned = Arc::clone(&self.0); 37 | unblock(move || cloned.set(percentage)).await 38 | } 39 | } 40 | 41 | pub(crate) fn brightness_devices() -> impl Stream> { 42 | unblock(crate::blocking::windows::brightness_devices) 43 | .into_stream() 44 | .map(stream::iter) 45 | .flatten() 46 | .map(|d| d.map(|d| AsyncDeviceImpl(Arc::new(d))).map_err(Into::into)) 47 | } 48 | 49 | impl BrightnessExt for BrightnessDevice { 50 | fn device_description(&self) -> Result { 51 | Ok(self.0 .0.device_description.clone()) 52 | } 53 | 54 | fn device_registry_key(&self) -> Result { 55 | Ok(self.0 .0.device_key.clone()) 56 | } 57 | 58 | fn device_path(&self) -> Result { 59 | Ok(self.0 .0.device_path.clone()) 60 | } 61 | } 62 | --------------------------------------------------------------------------------