i3status_rs/blocks/
xrandr.rs

1//! X11 screen information
2//!
3//! X11 screen information (name, brightness, resolution). With a click you can toggle through your active screens and with wheel up and down you can adjust the selected screens brightness. Regarding brightness control, xrandr changes the brightness of the display using gamma rather than changing the brightness in hardware, so if that is not desirable then consider using the `backlight` block instead.
4//!
5//! NOTE: Some users report issues (e.g. [here](https://github.com/greshake/i3status-rust/issues/274) and [here](https://github.com/greshake/i3status-rust/issues/668) when using this block. The cause is currently unknown, however setting a higher update interval may help.
6//!
7//! # Configuration
8//!
9//! Key | Values | Default
10//! ----|--------|--------
11//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $display $brightness_icon $brightness "`
12//! `step_width` | The steps brightness is in/decreased for the selected screen (When greater than 50 it gets limited to 50). | `5`
13//! `interval` | Update interval in seconds. | `5`
14//! `invert_icons` | Invert icons' ordering, useful if you have colorful emoji | `false`
15//!
16//! Placeholder       | Value                        | Type   | Unit
17//! ------------------|------------------------------|--------|---------------
18//! `icon`            | A static icon                | Icon   | -
19//! `display`         | The name of a monitor        | Text   | -
20//! `brightness`      | The brightness of a monitor  | Number | %
21//! `brightness_icon` | A static icon                | Icon   | -
22//! `resolution`      | The resolution of a monitor  | Text   | -
23//! `res_icon`        | A static icon                | Icon   | -
24//!
25//! Action            | Default button
26//! ------------------|---------------
27//! `cycle_outputs`   | Left
28//! `brightness_up`   | Wheel Up
29//! `brightness_down` | Wheel Down
30//!
31//! # Example
32//!
33//! ```toml
34//! [[block]]
35//! block = "xrandr"
36//! format = " $icon $brightness $resolution "
37//! ```
38//!
39//! # Used Icons
40//! - `xrandr`
41//! - `backlight`
42//! - `resolution`
43
44use super::prelude::*;
45use crate::subprocess::spawn_shell;
46use regex::RegexSet;
47use tokio::process::Command;
48
49#[derive(Deserialize, Debug, SmartDefault)]
50#[serde(deny_unknown_fields, default)]
51pub struct Config {
52    #[default(5.into())]
53    pub interval: Seconds,
54    pub format: FormatConfig,
55    #[default(5)]
56    pub step_width: u32,
57    pub invert_icons: bool,
58}
59
60pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
61    let mut actions = api.get_actions()?;
62    api.set_default_actions(&[
63        (MouseButton::Left, None, "cycle_outputs"),
64        (MouseButton::WheelUp, None, "brightness_up"),
65        (MouseButton::WheelDown, None, "brightness_down"),
66    ])?;
67
68    let format = config
69        .format
70        .with_default(" $icon $display $brightness_icon $brightness ")?;
71
72    let mut cur_indx = 0;
73    let mut timer = config.interval.timer();
74
75    loop {
76        let mut monitors = get_monitors().await?;
77        if cur_indx > monitors.len() {
78            cur_indx = 0;
79        }
80
81        loop {
82            let mut widget = Widget::new().with_format(format.clone());
83
84            if let Some(mon) = monitors.get(cur_indx) {
85                let mut icon_value = (mon.brightness as f64) / 100.0;
86                if config.invert_icons {
87                    icon_value = 1.0 - icon_value;
88                }
89                widget.set_values(map! {
90                    "display" => Value::text(mon.name.clone()),
91                    "brightness" => Value::percents(mon.brightness),
92                    "brightness_icon" => Value::icon_progression("backlight", icon_value),
93                    "resolution" => Value::text(mon.resolution.clone()),
94                    "icon" => Value::icon("xrandr"),
95                    "res_icon" => Value::icon("resolution"),
96                });
97            }
98            api.set_widget(widget)?;
99
100            select! {
101                _ = timer.tick() => break,
102                _ = api.wait_for_update_request() => break,
103                Some(action) = actions.recv() => match action.as_ref() {
104                    "cycle_outputs" => {
105                        cur_indx = (cur_indx + 1) % monitors.len();
106                    }
107                    "brightness_up" => {
108                        if let Some(monitor) = monitors.get_mut(cur_indx) {
109                            let bright = (monitor.brightness + config.step_width).min(100);
110                            monitor.set_brightness(bright);
111                        }
112                    }
113                    "brightness_down" => {
114                        if let Some(monitor) = monitors.get_mut(cur_indx) {
115                            let bright = monitor.brightness.saturating_sub(config.step_width);
116                            monitor.set_brightness(bright);
117                        }
118                    }
119                    _ => (),
120                }
121            }
122        }
123    }
124}
125
126struct Monitor {
127    name: String,
128    brightness: u32,
129    resolution: String,
130}
131
132impl Monitor {
133    fn set_brightness(&mut self, brightness: u32) {
134        let _ = spawn_shell(&format!(
135            "xrandr --output {} --brightness  {}",
136            self.name,
137            brightness as f64 / 100.0
138        ));
139        self.brightness = brightness;
140    }
141}
142
143async fn get_monitors() -> Result<Vec<Monitor>> {
144    let mut monitors = Vec::new();
145
146    let active_monitors = Command::new("xrandr")
147        .arg("--listactivemonitors")
148        .output()
149        .await
150        .error("Failed to collect active xrandr monitors")?
151        .stdout;
152    let active_monitors =
153        String::from_utf8(active_monitors).error("xrandr produced non-UTF8 output")?;
154
155    let regex = active_monitors
156        .lines()
157        .filter_map(|line| line.split_ascii_whitespace().last())
158        .map(|name| format!("{name} connected"))
159        .chain(Some("Brightness:".into()));
160    let regex = RegexSet::new(regex).error("Failed to create RegexSet")?;
161
162    let monitors_info = Command::new("xrandr")
163        .arg("--verbose")
164        .output()
165        .await
166        .error("Failed to collect xrandr monitors info")?
167        .stdout;
168    let monitors_info =
169        String::from_utf8(monitors_info).error("xrandr produced non-UTF8 output")?;
170
171    let mut it = monitors_info.lines().filter(|line| regex.is_match(line));
172
173    while let (Some(line1), Some(line2)) = (it.next(), it.next()) {
174        let mut tokens = line1.split_ascii_whitespace().peekable();
175        let name = tokens.next().error("Failed to parse xrandr output")?.into();
176        let _ = tokens.next();
177
178        // The output may be "<name> connected <resolution>" or "<name> connected primary <resolution>"
179        let _ = tokens.next_if_eq(&"primary");
180
181        let resolution = tokens
182            .next()
183            .and_then(|x| x.split('+').next())
184            .error("Failed to parse xrandr output")?
185            .into();
186
187        let brightness = (line2
188            .split(':')
189            .nth(1)
190            .error("Failed to parse xrandr output")?
191            .trim()
192            .parse::<f64>()
193            .error("Failed to parse xrandr output")?
194            * 100.0) as u32;
195
196        monitors.push(Monitor {
197            name,
198            brightness,
199            resolution,
200        });
201    }
202
203    Ok(monitors)
204}