i3status_rs/blocks/backlight.rs
1//! The brightness of a backlight device
2//!
3//! This block reads brightness information directly from the filesystem, so it works under both
4//! X11 and Wayland. The block uses `inotify` to listen for changes in the device's brightness
5//! directly, so there is no need to set an update interval. This block uses DBus to set brightness
6//! level using the mouse wheel, but will [fallback to sysfs](#d-bus-fallback) if `systemd-logind` is not used.
7//!
8//! # Root scaling
9//!
10//! Some devices expose raw values that are best handled with nonlinear scaling. The human perception of lightness is close to the cube root of relative luminance, so settings for `root_scaling` between 2.4 and 3.0 are worth trying. For devices with few discrete steps this should be 1.0 (linear). More information: <https://en.wikipedia.org/wiki/Lightness>
11//!
12//! # Configuration
13//!
14//! Key | Values | Default
15//! ----|--------|--------
16//! `device` | A regex to match against `/sys/class/backlight` devices to read brightness information from (can match 1 or more devices). When there is no `device` specified, this block will display information for all devices found in the `/sys/class/backlight` directory. | Default device
17//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $brightness "`
18//! `missing_format` | A string to customise the output of this block. No placeholders available | `" no backlight devices "`
19//! `step_width` | The brightness increment to use when scrolling, in percent | `5`
20//! `minimum` | The minimum brightness that can be scrolled down to | `5`
21//! `maximum` | The maximum brightness that can be scrolled up to | `100`
22//! `cycle` | The brightnesses to cycle through on each click | `[minimum, maximum]`
23//! `root_scaling` | Scaling exponent reciprocal (ie. root) | `1.0`
24//! `invert_icons` | Invert icons' ordering, useful if you have colorful emoji | `false`
25//! `ddcci_sleep_multiplier` | [See ddcutil documentation](https://www.ddcutil.com/performance_options/#option-sleep-multiplier) | `1.0`
26//! `ddcci_max_tries_write_read` | The maximum number of times to attempt writing to or reading from a ddcci monitor | `10`
27//!
28//! Placeholder | Value | Type | Unit
29//! -------------|-------------------------------------------|--------|---------------
30//! `icon` | Icon based on backlight's state | Icon | -
31//! `brightness` | Current brightness | Number | %
32//!
33//! Action | Default button
34//! ------------------|---------------
35//! `cycle` | Left
36//! `brightness_up` | Wheel Up
37//! `brightness_down` | Wheel Down
38//!
39//! # Example
40//!
41//! ```toml
42//! [[block]]
43//! block = "backlight"
44//! device = "intel_backlight"
45//! ```
46//!
47//! Hide missing backlight:
48//!
49//! ```toml
50//! [[block]]
51//! block = "backlight"
52//! missing_format = ""
53//! ```
54//!
55//! # calibright
56//!
57//! Additional display brightness calibration can be set in `$XDG_CONFIG_HOME/calibright/config.toml`
58//! See <https://github.com/bim9262/calibright> for more details.
59//! This block will override any global config set in `$XDG_CONFIG_HOME/calibright/config.toml`
60//!
61//! # D-Bus Fallback
62//!
63//! If you don't use `systemd-logind` i3status-rust will attempt to set the brightness
64//! using sysfs. In order to do this you'll need to have write permission.
65//! You can do this by writing a `udev` rule for your system.
66//!
67//! First, check that your user is a member of the "video" group using the
68//! `groups` command. Then add a rule in the `/etc/udev/rules.d/` directory
69//! containing the following, for example in `backlight.rules`:
70//!
71//! ```text
72//! ACTION=="add", SUBSYSTEM=="backlight", GROUP="video", MODE="0664"
73//! ```
74//!
75//! This will allow the video group to modify all backlight devices. You will
76//! also need to restart for this rule to take effect.
77//!
78//! # Icons Used
79//! - `backlight` (`$icon`, as a progression)
80
81use std::sync::Arc;
82
83use calibright::{CalibrightBuilder, CalibrightConfig, CalibrightError, DeviceConfig};
84
85use super::prelude::*;
86
87#[derive(Deserialize, Debug, SmartDefault)]
88#[serde(deny_unknown_fields, default)]
89pub struct Config {
90 pub device: Option<String>,
91 pub format: FormatConfig,
92 pub missing_format: FormatConfig,
93 #[default(5.0)]
94 pub step_width: f64,
95 #[default(5.0)]
96 pub minimum: f64,
97 #[default(100.0)]
98 pub maximum: f64,
99 pub cycle: Option<Vec<f64>>,
100 pub invert_icons: bool,
101 //Calibright config settings
102 pub root_scaling: Option<f64>,
103 pub ddcci_sleep_multiplier: Option<f64>,
104 pub ddcci_max_tries_write_read: Option<u8>,
105}
106
107pub(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
108 BlockPlan::new(vec![
109 OutputPlan::new("main", config.format.with_default(" $icon $brightness ")?)
110 .icon("icon", IconChoices::one(icons::BACKLIGHT)),
111 // The "missing" output sets no values at all.
112 OutputPlan::new(
113 "missing",
114 config
115 .missing_format
116 .with_default(" no backlight devices ")?,
117 ),
118 ])
119}
120
121pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
122 let mut actions = api.get_actions()?;
123 api.set_default_actions(&[
124 (MouseButton::Left, None, "cycle"),
125 (MouseButton::WheelUp, None, "brightness_up"),
126 (MouseButton::WheelDown, None, "brightness_down"),
127 ])?;
128
129 let output_main = plan.output("main")?;
130 let output_missing = plan.output("missing")?;
131
132 let default_cycle = &[config.minimum, config.maximum];
133 let mut cycle = config
134 .cycle
135 .as_deref()
136 .unwrap_or(default_cycle)
137 .iter()
138 .map(|x| x / 100.0)
139 .cycle();
140
141 let step_width = config.step_width / 100.0;
142 let minimum = config.minimum / 100.0;
143 let maximum = config.maximum / 100.0;
144
145 let mut calibright_defaults = DeviceConfig::default();
146
147 if let Some(root_scaling) = config.root_scaling {
148 calibright_defaults.root_scaling = root_scaling;
149 }
150
151 if let Some(ddcci_sleep_multiplier) = config.ddcci_sleep_multiplier {
152 calibright_defaults.ddcci_sleep_multiplier = ddcci_sleep_multiplier;
153 }
154
155 if let Some(ddcci_max_tries_write_read) = config.ddcci_max_tries_write_read {
156 calibright_defaults.ddcci_max_tries_write_read = ddcci_max_tries_write_read;
157 }
158
159 let calibright_config = CalibrightConfig::new_with_defaults(&calibright_defaults)
160 .await
161 .error("calibright config error")?;
162
163 let mut calibright = CalibrightBuilder::new()
164 .with_device_regex(config.device.as_deref().unwrap_or("."))
165 .with_config(calibright_config)
166 .with_poll_interval(api.error_interval)
167 .build()
168 .await
169 .error("Failed to init calibright")?;
170
171 // This is used to display the error, if there is one
172 let mut block_error: Option<CalibrightError> = None;
173
174 let mut brightness = calibright
175 .get_brightness()
176 .await
177 .map_err(|e| block_error = Some(e))
178 .unwrap_or_default();
179
180 loop {
181 match block_error {
182 Some(CalibrightError::NoDevices) => {
183 let widget = output_missing.new_widget().with_state(State::Critical);
184 api.set_widget(widget)?;
185 }
186 Some(e) => {
187 api.set_error(Error {
188 message: None,
189 cause: Some(Arc::new(e)),
190 })?;
191 }
192 None => {
193 let mut widget = output_main.new_widget();
194 let mut icon_value = brightness;
195 if config.invert_icons {
196 icon_value = 1.0 - icon_value;
197 }
198 widget.set_values(map! {
199 "icon" => Value::icon_progression(icons::BACKLIGHT, icon_value),
200 "brightness" => Value::percents((brightness * 100.0).round())
201 });
202 api.set_widget(widget)?;
203 }
204 }
205
206 loop {
207 select! {
208 // Calibright can recover from errors, just keep reading the next event.
209 _ = calibright.next() => {
210 block_error = calibright
211 .get_brightness()
212 .await
213 .map(|new_brightness| {brightness = new_brightness;})
214 .err();
215
216 break;
217 },
218 Some(action) = actions.recv() => match action.as_ref() {
219 "cycle" => {
220 if let Some(cycle_brightness) = cycle.next() {
221 brightness = cycle_brightness;
222 block_error = calibright
223 .set_brightness(brightness)
224 .await
225 .err();
226 break;
227
228 }
229 }
230 "brightness_up" => {
231 brightness = (brightness + step_width).clamp(minimum, maximum);
232 block_error = calibright
233 .set_brightness(brightness)
234 .await
235 .err();
236 break;
237 }
238 "brightness_down" => {
239 brightness = (brightness - step_width).clamp(minimum, maximum);
240 block_error = calibright
241 .set_brightness(brightness)
242 .await
243 .err();
244 break;
245 }
246 _ => (),
247 }
248 }
249 }
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn plan_declares_main_and_missing() {
259 let plan = prepare(&Config::default()).unwrap();
260 let main = plan.output("main").unwrap();
261 assert_eq!(main.single_icon("icon").unwrap(), "backlight");
262 assert!(main.format().contains_key("brightness"));
263 let missing = plan.output("missing").unwrap();
264 assert_eq!(missing.output().icon_placeholders().count(), 0);
265 }
266
267 #[test]
268 fn custom_missing_format_is_used() {
269 let config = Config {
270 missing_format: " $brightness ".parse().unwrap(),
271 ..Config::default()
272 };
273 let plan = prepare(&config).unwrap();
274 let missing = plan.output("missing").unwrap();
275 assert!(missing.format().contains_key("brightness"));
276 }
277}