i3status_rs/blocks/
hueshift.rs1use 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 #[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 #[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 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 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 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 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 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 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 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 Ok(None)
309 }
310 async fn update(&mut self, temp: u16) -> Result<()> {
311 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 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 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 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 #[zbus(property)]
393 fn brightness(&self) -> zbus::Result<f64>;
394 #[zbus(property)]
395 fn set_brightness(&self, value: f64) -> zbus::Result<()>;
396
397 #[zbus(property)]
399 fn temperature(&self) -> zbus::Result<u16>;
400 #[zbus(property)]
401 fn set_temperature(&self, value: u16) -> zbus::Result<()>;
402}