i3status_rs/blocks/
hueshift.rs

1//! Manage display temperature
2//!
3//! This block displays the current color temperature in Kelvin. When scrolling upon the block the color temperature is changed.
4//! A left click on the block sets the color temperature to `click_temp` that is by default to `6500K`.
5//! A right click completely resets the color temperature to its default value (`6500K`).
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. | `" $temperature "`
12//! `step`        | The step color temperature is in/decreased in Kelvin. | `100`
13//! `hue_shifter` | Program used to control screen color. | Detect automatically
14//! `max_temp`    | Max color temperature in Kelvin. | `10000`
15//! `min_temp`    | Min color temperature in Kelvin. | `1000`
16//! `click_temp`  | Left click color temperature in Kelvin. | `6500`
17//!
18//! Placeholder           | Value                        | Type   | Unit
19//! ----------------------|------------------------------|--------|---------------
20//! `temperature`         | Current temperature          | Number | -
21//!
22//! Action             | Default button
23//! -------------------|---------------
24//! `set_click_temp`   | Left
25//! `reset`            | Right
26//! `temperature_up`   | Wheel Up
27//! `temperature_down` | Wheel Down
28//!
29//! # Available Hue Shifters
30//!
31//! Name                 | Supports
32//! ---------------------|---------
33//! `"redshift"`         | X11
34//! `"sct"`              | X11
35//! `"gammastep"`        | X11 and Wayland
36//! `"wl_gammarelay"`    | Wayland
37//! `"wl_gammarelay_rs"` | Wayland
38//! `"wlsunset"`         | Wayland
39//!
40//! Note that at the moment, only [`wl_gammarelay`](https://github.com/jeremija/wl-gammarelay) and
41//! [`wl_gammarelay_rs`](https://github.com/MaxVerevkin/wl-gammarelay-rs)
42//! subscribe to the events and update the bar when the temperature is modified externally. Also,
43//! these are the only drivers at the moment that work under Wayland without flickering.
44//!
45//! # Example
46//!
47//! ```toml
48//! [[block]]
49//! block = "hueshift"
50//! hue_shifter = "redshift"
51//! step = 50
52//! click_temp = 3500
53//! ```
54//!
55//! A hard limit is set for the `max_temp` to `10000K` and the same for the `min_temp` which is `1000K`.
56//! The `step` has a hard limit as well, defined to `500K` to avoid too brutal changes.
57
58use super::prelude::*;
59use crate::subprocess::{spawn_process, spawn_shell};
60use crate::util::has_command;
61use futures::future::pending;
62
63#[derive(Deserialize, Debug, SmartDefault)]
64#[serde(deny_unknown_fields, default)]
65pub struct Config {
66    pub format: FormatConfig,
67    // TODO: Document once this option becomes useful
68    #[default(5.into())]
69    pub interval: Seconds,
70    #[default(10_000)]
71    pub max_temp: u16,
72    #[default(1_000)]
73    pub min_temp: u16,
74    // TODO: Remove (this option is undocumented)
75    #[default(6_500)]
76    pub current_temp: u16,
77    pub hue_shifter: Option<HueShifter>,
78    #[default(100)]
79    pub step: u16,
80    #[default(6_500)]
81    pub click_temp: u16,
82}
83
84pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
85    let mut actions = api.get_actions()?;
86    api.set_default_actions(&[
87        (MouseButton::Left, None, "set_click_temp"),
88        (MouseButton::Right, None, "reset"),
89        (MouseButton::WheelUp, None, "temperature_up"),
90        (MouseButton::WheelDown, None, "temperature_down"),
91    ])?;
92
93    let format = config.format.with_default(" $icon $temperature ")?;
94
95    // limit too big steps at 500K to avoid too brutal changes
96    let step = config.step.min(500);
97    let max_temp = config.max_temp.min(10_000);
98    let min_temp = config.min_temp.clamp(1_000, max_temp);
99
100    let hue_shifter = match config.hue_shifter {
101        Some(driver) => driver,
102        None => {
103            if has_command("wl-gammarelay-rs").await? {
104                HueShifter::WlGammarelayRs
105            } else if has_command("wl-gammarelay").await? {
106                HueShifter::WlGammarelay
107            } else if has_command("redshift").await? {
108                HueShifter::Redshift
109            } else if has_command("sct").await? {
110                HueShifter::Sct
111            } else if has_command("gammastep").await? {
112                HueShifter::Gammastep
113            } else if has_command("wlsunset").await? {
114                HueShifter::Wlsunset
115            } else {
116                return Err(Error::new("Could not detect driver program"));
117            }
118        }
119    };
120
121    let mut driver: Box<dyn HueShiftDriver> = match hue_shifter {
122        HueShifter::Redshift => Box::new(Redshift::new(config.interval)),
123        HueShifter::Sct => Box::new(Sct::new(config.interval)),
124        HueShifter::Gammastep => Box::new(Gammastep::new(config.interval)),
125        HueShifter::Wlsunset => Box::new(Wlsunset::new(config.interval)),
126        HueShifter::WlGammarelay => Box::new(WlGammarelayRs::new("wl-gammarelay").await?),
127        HueShifter::WlGammarelayRs => Box::new(WlGammarelayRs::new("wl-gammarelay-rs").await?),
128    };
129
130    let mut current_temp = driver.get().await?.unwrap_or(config.current_temp);
131
132    loop {
133        let mut widget = Widget::new().with_format(format.clone());
134        widget.set_values(map! {
135            "icon" => Value::icon("hueshift"),
136            "temperature" => Value::number(current_temp)
137        });
138        api.set_widget(widget)?;
139
140        select! {
141            update = driver.receive_update() => {
142                current_temp = update?;
143            }
144            _ = api.wait_for_update_request() => {
145                if let Some(val) = driver.get().await? {
146                    current_temp = val;
147                }
148            }
149            Some(action) = actions.recv() => match action.as_ref() {
150                "set_click_temp" => {
151                    current_temp = config.click_temp;
152                    driver.update(current_temp).await?;
153                }
154                "reset" => {
155                    if max_temp > 6500 {
156                        current_temp = 6500;
157                        driver.reset().await?;
158                    } else {
159                        current_temp = max_temp;
160                        driver.update(current_temp).await?;
161                    }
162                }
163                "temperature_up" => {
164                    current_temp = (current_temp + step).min(max_temp);
165                    driver.update(current_temp).await?;
166                }
167                "temperature_down" => {
168                    current_temp = current_temp.saturating_sub(step).max(min_temp);
169                    driver.update(current_temp).await?;
170                }
171                _ => (),
172            }
173        }
174    }
175}
176
177#[derive(Deserialize, Debug, Clone, Copy)]
178#[serde(rename_all = "snake_case")]
179pub enum HueShifter {
180    Redshift,
181    Sct,
182    Gammastep,
183    Wlsunset,
184    WlGammarelay,
185    WlGammarelayRs,
186}
187
188#[async_trait]
189trait HueShiftDriver {
190    async fn get(&mut self) -> Result<Option<u16>>;
191    async fn update(&mut self, temp: u16) -> Result<()>;
192    async fn reset(&mut self) -> Result<()>;
193    async fn receive_update(&mut self) -> Result<u16>;
194}
195
196struct Redshift {
197    interval: Seconds,
198}
199
200impl Redshift {
201    fn new(interval: Seconds) -> Self {
202        Self { interval }
203    }
204}
205
206#[async_trait]
207impl HueShiftDriver for Redshift {
208    async fn get(&mut self) -> Result<Option<u16>> {
209        // TODO
210        Ok(None)
211    }
212    async fn update(&mut self, temp: u16) -> Result<()> {
213        spawn_process("redshift", &["-O", &temp.to_string(), "-P"])
214            .error("Failed to set new color temperature using redshift.")
215    }
216    async fn reset(&mut self) -> Result<()> {
217        spawn_process("redshift", &["-x"])
218            .error("Failed to set new color temperature using redshift.")
219    }
220    async fn receive_update(&mut self) -> Result<u16> {
221        sleep(self.interval.0).await;
222        // self.get().await
223        pending().await
224    }
225}
226
227struct Sct {
228    interval: Seconds,
229}
230
231impl Sct {
232    fn new(interval: Seconds) -> Self {
233        Self { interval }
234    }
235}
236
237#[async_trait]
238impl HueShiftDriver for Sct {
239    async fn get(&mut self) -> Result<Option<u16>> {
240        // TODO
241        Ok(None)
242    }
243    async fn update(&mut self, temp: u16) -> Result<()> {
244        spawn_shell(&format!("sct {temp} >/dev/null 2>&1"))
245            .error("Failed to set new color temperature using sct.")
246    }
247    async fn reset(&mut self) -> Result<()> {
248        spawn_process("sct", &[]).error("Failed to set new color temperature using sct.")
249    }
250    async fn receive_update(&mut self) -> Result<u16> {
251        sleep(self.interval.0).await;
252        // self.get().await
253        pending().await
254    }
255}
256
257struct Gammastep {
258    interval: Seconds,
259}
260
261impl Gammastep {
262    fn new(interval: Seconds) -> Self {
263        Self { interval }
264    }
265}
266
267#[async_trait]
268impl HueShiftDriver for Gammastep {
269    async fn get(&mut self) -> Result<Option<u16>> {
270        // TODO
271        Ok(None)
272    }
273    async fn update(&mut self, temp: u16) -> Result<()> {
274        spawn_shell(&format!("pkill gammastep; gammastep -O {temp} -P &",))
275            .error("Failed to set new color temperature using gammastep.")
276    }
277    async fn reset(&mut self) -> Result<()> {
278        spawn_process("gammastep", &["-x"])
279            .error("Failed to set new color temperature using gammastep.")
280    }
281    async fn receive_update(&mut self) -> Result<u16> {
282        sleep(self.interval.0).await;
283        // self.get().await
284        pending().await
285    }
286}
287
288struct Wlsunset {
289    interval: Seconds,
290}
291
292impl Wlsunset {
293    fn new(interval: Seconds) -> Self {
294        Self { interval }
295    }
296}
297
298#[async_trait]
299impl HueShiftDriver for Wlsunset {
300    async fn get(&mut self) -> Result<Option<u16>> {
301        // TODO
302        Ok(None)
303    }
304    async fn update(&mut self, temp: u16) -> Result<()> {
305        // wlsunset does not have a oneshot option, so set both day and
306        // night temperature. wlsunset dose not allow for day and night
307        // temperatures to be the same, so increment the day temperature.
308        spawn_shell(&format!(
309            "pkill wlsunset; wlsunset -T {} -t {} &",
310            temp + 1,
311            temp
312        ))
313        .error("Failed to set new color temperature using wlsunset.")
314    }
315    async fn reset(&mut self) -> Result<()> {
316        // wlsunset does not have a reset option, so just kill the process.
317        // Trying to call wlsunset without any arguments uses the defaults:
318        // day temp: 6500K
319        // night temp: 4000K
320        // latitude/longitude: NaN
321        //     ^ results in sun_condition == POLAR_NIGHT at time of testing
322        // With these defaults, this results in the the color temperature
323        // getting set to 4000K.
324        spawn_process("pkill", &["wlsunset"])
325            .error("Failed to set new color temperature using wlsunset.")
326    }
327    async fn receive_update(&mut self) -> Result<u16> {
328        sleep(self.interval.0).await;
329        // self.get().await
330        pending().await
331    }
332}
333
334struct WlGammarelayRs {
335    proxy: WlGammarelayRsBusProxy<'static>,
336    updates: zbus::proxy::PropertyStream<'static, u16>,
337}
338
339impl WlGammarelayRs {
340    async fn new(cmd: &str) -> Result<Self> {
341        // Make sure the daemon is running
342        spawn_process(cmd, &[]).error("Failed to start wl-gammarelay daemon")?;
343        sleep(Duration::from_millis(100)).await;
344
345        let conn = crate::util::new_dbus_connection().await?;
346        let proxy = WlGammarelayRsBusProxy::new(&conn)
347            .await
348            .error("Failed to create wl-gammarelay-rs DBus proxy")?;
349        let updates = proxy.receive_temperature_changed().await;
350        Ok(Self { proxy, updates })
351    }
352}
353
354#[async_trait]
355impl HueShiftDriver for WlGammarelayRs {
356    async fn get(&mut self) -> Result<Option<u16>> {
357        let value = self
358            .proxy
359            .temperature()
360            .await
361            .error("Failed to get temperature")?;
362        Ok(Some(value))
363    }
364    async fn update(&mut self, temp: u16) -> Result<()> {
365        self.proxy
366            .set_temperature(temp)
367            .await
368            .error("Failed to set temperature")
369    }
370    async fn reset(&mut self) -> Result<()> {
371        self.update(6500).await
372    }
373    async fn receive_update(&mut self) -> Result<u16> {
374        let update = self.updates.next().await.error("No next update")?;
375        update.get().await.error("Failed to get temperature")
376    }
377}
378
379#[zbus::proxy(
380    interface = "rs.wl.gammarelay",
381    default_service = "rs.wl-gammarelay",
382    default_path = "/"
383)]
384trait WlGammarelayRsBus {
385    /// Brightness property
386    #[zbus(property)]
387    fn brightness(&self) -> zbus::Result<f64>;
388    #[zbus(property)]
389    fn set_brightness(&self, value: f64) -> zbus::Result<()>;
390
391    /// Temperature property
392    #[zbus(property)]
393    fn temperature(&self) -> zbus::Result<u16>;
394    #[zbus(property)]
395    fn set_temperature(&self, value: u16) -> zbus::Result<()>;
396}