1use 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(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
86 let format = config.format.with_default(" $icon $temperature ")?;
87 BlockPlan::new(vec![
88 OutputPlan::new("main", format).icon("icon", IconChoices::one(icons::HUESHIFT)),
89 ])
90}
91
92pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
93 let mut actions = api.get_actions()?;
94 api.set_default_actions(&[
95 (MouseButton::Left, None, "set_click_temp"),
96 (MouseButton::Right, None, "reset"),
97 (MouseButton::WheelUp, None, "temperature_up"),
98 (MouseButton::WheelDown, None, "temperature_down"),
99 ])?;
100
101 let output_main = plan.output("main")?;
102
103 let step = config.step.min(500);
105 let max_temp = config.max_temp.min(10_000);
106 let min_temp = config.min_temp.clamp(1_000, max_temp);
107
108 let hue_shifter = match config.hue_shifter {
109 Some(driver) => driver,
110 None => {
111 if has_command("wl-gammarelay-rs").await? {
112 HueShifter::WlGammarelayRs
113 } else if has_command("wl-gammarelay").await? {
114 HueShifter::WlGammarelay
115 } else if has_command("redshift").await? {
116 HueShifter::Redshift
117 } else if has_command("sct").await? {
118 HueShifter::Sct
119 } else if has_command("xsct").await? {
120 HueShifter::Xsct
121 } else if has_command("gammastep").await? {
122 HueShifter::Gammastep
123 } else if has_command("wlsunset").await? {
124 HueShifter::Wlsunset
125 } else {
126 return Err(Error::new("Could not detect driver program"));
127 }
128 }
129 };
130
131 let mut driver: Box<dyn HueShiftDriver> = match hue_shifter {
132 HueShifter::Redshift => Box::new(Redshift::new(config.interval)),
133 HueShifter::Sct => Box::new(Sct::new("sct", config.interval)),
134 HueShifter::Xsct => Box::new(Sct::new("xsct", config.interval)),
135 HueShifter::Gammastep => Box::new(Gammastep::new(config.interval)),
136 HueShifter::Wlsunset => Box::new(Wlsunset::new(config.interval)),
137 HueShifter::WlGammarelay => Box::new(WlGammarelayRs::new("wl-gammarelay").await?),
138 HueShifter::WlGammarelayRs => Box::new(WlGammarelayRs::new("wl-gammarelay-rs").await?),
139 };
140
141 let mut current_temp = driver.get().await?.unwrap_or(config.current_temp);
142
143 loop {
144 let mut widget = output_main.new_widget();
145 widget.set_values(map! {
146 "icon" => Value::icon(icons::HUESHIFT),
147 "temperature" => Value::number(current_temp)
148 });
149 api.set_widget(widget)?;
150
151 select! {
152 update = driver.receive_update() => {
153 current_temp = update?;
154 }
155 _ = api.wait_for_update_request() => {
156 if let Some(val) = driver.get().await? {
157 current_temp = val;
158 }
159 }
160 Some(action) = actions.recv() => match action.as_ref() {
161 "set_click_temp" => {
162 current_temp = config.click_temp;
163 driver.update(current_temp).await?;
164 }
165 "reset" => {
166 if max_temp > 6500 {
167 current_temp = 6500;
168 driver.reset().await?;
169 } else {
170 current_temp = max_temp;
171 driver.update(current_temp).await?;
172 }
173 }
174 "temperature_up" => {
175 current_temp = (current_temp + step).min(max_temp);
176 driver.update(current_temp).await?;
177 }
178 "temperature_down" => {
179 current_temp = current_temp.saturating_sub(step).max(min_temp);
180 driver.update(current_temp).await?;
181 }
182 _ => (),
183 }
184 }
185 }
186}
187
188#[derive(Deserialize, Debug, Clone, Copy)]
189#[serde(rename_all = "snake_case")]
190pub enum HueShifter {
191 Redshift,
192 Sct,
193 Xsct,
194 Gammastep,
195 Wlsunset,
196 WlGammarelay,
197 WlGammarelayRs,
198}
199
200#[async_trait]
201trait HueShiftDriver {
202 async fn get(&mut self) -> Result<Option<u16>>;
203 async fn update(&mut self, temp: u16) -> Result<()>;
204 async fn reset(&mut self) -> Result<()>;
205 async fn receive_update(&mut self) -> Result<u16>;
206}
207
208struct Redshift {
209 interval: Seconds,
210}
211
212impl Redshift {
213 fn new(interval: Seconds) -> Self {
214 Self { interval }
215 }
216}
217
218#[async_trait]
219impl HueShiftDriver for Redshift {
220 async fn get(&mut self) -> Result<Option<u16>> {
221 Ok(None)
223 }
224 async fn update(&mut self, temp: u16) -> Result<()> {
225 spawn_process("redshift", &["-O", &temp.to_string(), "-P"])
226 .error("Failed to set new color temperature using redshift.")
227 }
228 async fn reset(&mut self) -> Result<()> {
229 spawn_process("redshift", &["-x"])
230 .error("Failed to set new color temperature using redshift.")
231 }
232 async fn receive_update(&mut self) -> Result<u16> {
233 sleep(self.interval.0).await;
234 pending().await
236 }
237}
238
239struct Sct {
240 cmd: &'static str,
241 interval: Seconds,
242}
243
244impl Sct {
245 fn new(cmd: &'static str, interval: Seconds) -> Self {
246 Self { cmd, interval }
247 }
248}
249
250#[async_trait]
251impl HueShiftDriver for Sct {
252 async fn get(&mut self) -> Result<Option<u16>> {
253 Ok(None)
255 }
256 async fn update(&mut self, temp: u16) -> Result<()> {
257 spawn_shell(&format!("{0} {temp} >/dev/null 2>&1", self.cmd))
258 .error("Failed to set new color temperature using sct.")
259 }
260 async fn reset(&mut self) -> Result<()> {
261 spawn_process(self.cmd, &["0"]).error("Failed to set new color temperature using sct.")
262 }
263 async fn receive_update(&mut self) -> Result<u16> {
264 sleep(self.interval.0).await;
265 pending().await
267 }
268}
269
270struct Gammastep {
271 interval: Seconds,
272}
273
274impl Gammastep {
275 fn new(interval: Seconds) -> Self {
276 Self { interval }
277 }
278}
279
280#[async_trait]
281impl HueShiftDriver for Gammastep {
282 async fn get(&mut self) -> Result<Option<u16>> {
283 Ok(None)
285 }
286 async fn update(&mut self, temp: u16) -> Result<()> {
287 spawn_shell(&format!("pkill gammastep; gammastep -O {temp} -P &",))
288 .error("Failed to set new color temperature using gammastep.")
289 }
290 async fn reset(&mut self) -> Result<()> {
291 spawn_process("gammastep", &["-x"])
292 .error("Failed to set new color temperature using gammastep.")
293 }
294 async fn receive_update(&mut self) -> Result<u16> {
295 sleep(self.interval.0).await;
296 pending().await
298 }
299}
300
301struct Wlsunset {
302 interval: Seconds,
303}
304
305impl Wlsunset {
306 fn new(interval: Seconds) -> Self {
307 Self { interval }
308 }
309}
310
311#[async_trait]
312impl HueShiftDriver for Wlsunset {
313 async fn get(&mut self) -> Result<Option<u16>> {
314 Ok(None)
316 }
317 async fn update(&mut self, temp: u16) -> Result<()> {
318 spawn_shell(&format!(
322 "pkill wlsunset; wlsunset -T {} -t {} &",
323 temp + 1,
324 temp
325 ))
326 .error("Failed to set new color temperature using wlsunset.")
327 }
328 async fn reset(&mut self) -> Result<()> {
329 spawn_process("pkill", &["wlsunset"])
338 .error("Failed to set new color temperature using wlsunset.")
339 }
340 async fn receive_update(&mut self) -> Result<u16> {
341 sleep(self.interval.0).await;
342 pending().await
344 }
345}
346
347struct WlGammarelayRs {
348 proxy: WlGammarelayRsBusProxy<'static>,
349 updates: zbus::proxy::PropertyStream<'static, u16>,
350}
351
352impl WlGammarelayRs {
353 async fn new(cmd: &str) -> Result<Self> {
354 spawn_process(cmd, &[]).error("Failed to start wl-gammarelay daemon")?;
356 sleep(Duration::from_millis(100)).await;
357
358 let conn = crate::util::new_dbus_connection().await?;
359 let proxy = WlGammarelayRsBusProxy::new(&conn)
360 .await
361 .error("Failed to create wl-gammarelay-rs DBus proxy")?;
362 let updates = proxy.receive_temperature_changed().await;
363 Ok(Self { proxy, updates })
364 }
365}
366
367#[async_trait]
368impl HueShiftDriver for WlGammarelayRs {
369 async fn get(&mut self) -> Result<Option<u16>> {
370 let value = self
371 .proxy
372 .temperature()
373 .await
374 .error("Failed to get temperature")?;
375 Ok(Some(value))
376 }
377 async fn update(&mut self, temp: u16) -> Result<()> {
378 self.proxy
379 .set_temperature(temp)
380 .await
381 .error("Failed to set temperature")
382 }
383 async fn reset(&mut self) -> Result<()> {
384 self.update(6500).await
385 }
386 async fn receive_update(&mut self) -> Result<u16> {
387 let update = self.updates.next().await.error("No next update")?;
388 update.get().await.error("Failed to get temperature")
389 }
390}
391
392#[zbus::proxy(
393 interface = "rs.wl.gammarelay",
394 default_service = "rs.wl-gammarelay",
395 default_path = "/"
396)]
397trait WlGammarelayRsBus {
398 #[zbus(property)]
400 fn brightness(&self) -> zbus::Result<f64>;
401 #[zbus(property)]
402 fn set_brightness(&self, value: f64) -> zbus::Result<()>;
403
404 #[zbus(property)]
406 fn temperature(&self) -> zbus::Result<u16>;
407 #[zbus(property)]
408 fn set_temperature(&self, value: u16) -> zbus::Result<()>;
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn plan_declares_a_single_output_with_its_icon() {
417 let plan = prepare(&Config::default()).unwrap();
418 let ids: Vec<_> = plan.outputs().map(|o| o.id()).collect();
419 assert_eq!(ids, ["main"]);
420 let main = plan.output("main").unwrap();
421 assert_eq!(main.single_icon("icon").unwrap(), "hueshift");
422 assert!(main.format().contains_key("temperature"));
423 }
424
425 #[test]
426 fn configured_format_is_installed() {
427 let config = Config {
428 format: " $temperature ".parse().unwrap(),
429 ..Config::default()
430 };
431 let plan = prepare(&config).unwrap();
432 let main = plan.output("main").unwrap();
433 assert!(!main.format().contains_key("icon"));
434 assert!(main.format().contains_key("temperature"));
435 }
436}