Skip to main content

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}