Skip to main content

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