i3status_rs/blocks/
hueshift.rs1use 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 #[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 #[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 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 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 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 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 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 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 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 Ok(None)
303 }
304 async fn update(&mut self, temp: u16) -> Result<()> {
305 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 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 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 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 #[zbus(property)]
387 fn brightness(&self) -> zbus::Result<f64>;
388 #[zbus(property)]
389 fn set_brightness(&self, value: f64) -> zbus::Result<()>;
390
391 #[zbus(property)]
393 fn temperature(&self) -> zbus::Result<u16>;
394 #[zbus(property)]
395 fn set_temperature(&self, value: u16) -> zbus::Result<()>;
396}