1mod calc_colorramp;
35mod calc_solar;
36mod cli;
37mod config;
38mod coproduct;
39mod error;
40
41#[cfg(unix_without_macos)]
42mod gamma_drm;
43#[cfg(unix_without_macos)]
44mod gamma_randr;
45#[cfg(unix_without_macos)]
46mod gamma_vidmode;
47
48#[cfg(windows)]
49mod gamma_win32gdi;
50
51mod gamma_dummy;
52mod location_manual;
53mod types;
54mod types_display;
55mod types_parse;
56mod utils;
57
58#[cfg(windows)]
59use crate::gamma_win32gdi::Win32Gdi;
60#[cfg(unix_without_macos)]
61use crate::{gamma_drm::Drm, gamma_randr::Randr, gamma_vidmode::Vidmode};
62pub use cli::cli_args_command;
63use error::ReddishError;
64use gamma_dummy::Dummy;
65use itertools::Itertools;
66use location_manual::Manual;
67use types::Location;
68
69use crate::{
70 cli::ClapColorChoiceExt,
71 config::{Config, ConfigBuilder, FADE_STEPS},
72 error::{AdjusterError, ProviderError},
73 types::{ColorSettings, Elevation, Mode, Period, PeriodInfo},
74 types_display::{BODY, HEADER},
75};
76use anstream::AutoStream;
77use chrono::{DateTime, SubsecRound, TimeDelta};
78use std::{
79 fmt::Debug,
80 io,
81 sync::mpsc::{self, Receiver, RecvTimeoutError},
82};
83use tracing::{error, info, Level};
84use tracing_subscriber::fmt::writer::MakeWriterExt;
85
86pub fn main() {
87 (|| -> Result<(), ReddishError> {
88 let c = ConfigBuilder::new(|verbosity, color| {
89 let choice = color.to_choice();
90 let stdout = move || AutoStream::new(io::stdout(), choice).lock();
91 let stderr = move || AutoStream::new(io::stderr(), choice).lock();
92 let stdio = stderr.with_max_level(Level::WARN).or_else(stdout);
93
94 tracing_subscriber::fmt()
95 .with_writer(stdio)
96 .with_max_level(verbosity.level_filter())
97 .without_time()
98 .with_level(false)
99 .with_target(false)
100 .init();
101 })?
102 .build()?;
103
104 let (tx, rx) = mpsc::channel();
105 ctrlc::set_handler(move || {
106 #[allow(clippy::expect_used)]
107 tx.send(()).expect("Could not send signal on channel")
108 })
109 .or_else(|e| match c.mode {
110 Mode::Oneshot | Mode::Set | Mode::Reset | Mode::Print => Ok(()),
111 Mode::Daemon => Err(e),
112 })?;
113
114 run(&c, &rx)
115 })()
116 .unwrap_or_else(|e| error!("{e}"))
117}
118
119fn run(c: &Config, sig: &Receiver<()>) -> Result<(), ReddishError> {
120 match c.mode {
121 Mode::Daemon => {
122 info!("{c}\n{HEADER}Current{HEADER:#}:");
123 DaemonMode::new(c, sig).run_loop()?;
124 c.method.restore()?;
125 }
126 Mode::Oneshot => {
127 let (p, i) = Period::from(&c.scheme, &c.location, c.time)?;
129 let interp = c.night.interpolate_with(&c.day, p.into());
130 info!("{c}\n{HEADER}Current{HEADER:#}:\n{p}\n{i}\n{interp}");
131 c.method.set(c.reset_ramps, &interp)?;
132 }
133 Mode::Set => {
134 c.method.set(c.reset_ramps, &c.day)?;
136 }
137 Mode::Reset => {
138 c.method.set(true, &ColorSettings::default())?;
139 }
140 Mode::Print => run_print_mode(c)?,
141 }
142
143 Ok(())
144}
145
146fn run_print_mode(c: &Config) -> Result<(), ReddishError> {
147 let now = (c.time)();
148 let delta = now.to_utc() - DateTime::UNIX_EPOCH;
149 let loc = c.location.get()?;
150 let mut buf = (0..24).map(|h| {
151 let d = TimeDelta::hours(h);
152 let time = (now + d).time().trunc_subsecs(0);
153 let elev = Elevation::new((delta + d).num_seconds() as f64, loc);
154 format!("{BODY}{time}{BODY:#}: {:6.2}°", *elev)
155 });
156 Ok(info!("{}", buf.join("\n")))
157}
158
159#[derive(Debug)]
160struct DaemonMode<'a, 'b> {
161 cfg: &'a Config,
162 sig: &'b Receiver<()>,
163
164 signal: Signal,
165 fade: FadeStatus,
166
167 period: Period,
168 info: PeriodInfo,
169 interp: ColorSettings,
170
171 prev_period: Option<Period>,
174 prev_info: Option<PeriodInfo>,
175 prev_interp: Option<ColorSettings>,
176}
177
178impl<'a, 'b> DaemonMode<'a, 'b> {
179 fn new(cfg: &'a Config, sig: &'b Receiver<()>) -> Self {
180 Self {
181 cfg,
182 sig,
183 signal: Default::default(),
184 fade: Default::default(),
185 period: Default::default(),
186 info: Default::default(),
187 interp: Default::default(),
188 prev_period: Default::default(),
189 prev_info: Default::default(),
190 prev_interp: Default::default(),
191 }
192 }
193
194 fn run_loop(&mut self) -> Result<(), ReddishError> {
198 let c = self.cfg;
199 loop {
200 (self.period, self.info) =
201 Period::from(&c.scheme, &c.location, c.time)?;
202
203 let target = match self.signal {
204 Signal::None => {
205 c.night.interpolate_with(&c.day, self.period.into())
206 }
207 Signal::Interrupt => ColorSettings::default(),
208 };
209
210 (self.interp, self.fade) = self.next_interpolate(target);
211
212 self.log();
213
214 c.method.set(c.reset_ramps, &self.interp)?;
220
221 self.prev_period = Some(self.period);
222 self.prev_info = Some(self.info.clone());
223 self.prev_interp = Some(self.interp.clone());
224
225 let sleep_duration = match (self.signal, self.fade) {
229 (Signal::None, FadeStatus::Completed) => c.sleep_duration,
230 (_, FadeStatus::Ungoing { .. }) => c.sleep_duration_short,
231 (Signal::Interrupt, FadeStatus::Completed) => break Ok(()),
232 };
233
234 match self.sig.recv_timeout(sleep_duration) {
235 Err(RecvTimeoutError::Timeout) => {}
236 Err(e) => Err(e)?,
237 Ok(()) => match self.signal {
238 Signal::None => self.signal = Signal::Interrupt,
239 Signal::Interrupt => break Ok(()),
240 },
241 }
242 }
243 }
244
245 fn next_interpolate(
246 &self,
247 target: ColorSettings,
248 ) -> (ColorSettings, FadeStatus) {
249 use FadeStatus::*;
250 let target_is_very_different = self.interp.is_very_diff_from(&target);
251 match (&self.fade, target_is_very_different, self.cfg.disable_fade) {
252 (_, _, true) | (Completed | Ungoing { .. }, false, false) => {
253 (target, Completed)
254 }
255
256 (Completed, true, false) => {
257 let next = Self::interpolate(&self.interp, &target, 0);
258 (next, Ungoing { step: 0 })
259 }
260
261 (Ungoing { step }, true, false) => {
262 if *step < FADE_STEPS {
263 let step = *step + 1;
264 let next = Self::interpolate(&self.interp, &target, step);
265 (next, Ungoing { step })
266 } else {
267 (target, Completed)
268 }
269 }
270 }
271 }
272
273 fn interpolate(
274 start: &ColorSettings,
275 end: &ColorSettings,
276 step: u8,
277 ) -> ColorSettings {
278 let frac = step as f64 / FADE_STEPS as f64;
279 let alpha = Self::ease_fade(frac)
280 .clamp(0.0, 1.0)
281 .try_into()
282 .unwrap_or_else(|_| unreachable!());
283 start.interpolate_with(end, alpha)
284 }
285
286 fn ease_fade(t: f64) -> f64 {
289 if t <= 0.0 {
290 0.0
291 } else if t >= 1.0 {
292 1.0
293 } else {
294 1.0042954579734844
295 * (-6.404173895841566 * (-7.290824133098134 * t).exp()).exp()
296 }
297 }
298}
299
300trait Provider {
301 fn get(&self) -> Result<Location, ProviderError>;
302}
303
304trait Adjuster {
305 fn restore(&self) -> Result<(), AdjusterError>;
307 fn set(
309 &self,
310 reset_ramps: bool,
311 cs: &ColorSettings,
312 ) -> Result<(), AdjusterError>;
313}
314
315#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
316enum Signal {
317 #[default]
318 None,
319 Interrupt,
320}
321
322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323enum FadeStatus {
324 Completed,
325 Ungoing { step: u8 },
326}
327
328impl Default for FadeStatus {
329 fn default() -> Self {
330 Self::Completed
331 }
332}
333
334#[derive(Debug, PartialEq)]
337pub enum LocationProvider {
338 Manual(Manual),
339 Geoclue2(Geoclue2),
340}
341
342#[derive(Debug)]
343pub enum AdjustmentMethod {
344 Dummy(Dummy),
345 #[cfg(unix_without_macos)]
346 Randr(Randr),
347 #[cfg(unix_without_macos)]
348 Drm(Drm),
349 #[cfg(unix_without_macos)]
350 Vidmode(Vidmode),
351 #[cfg(windows)]
352 Win32Gdi(Win32Gdi),
353}
354
355#[derive(Debug, Clone, Default, PartialEq, Eq)]
356pub struct Geoclue2;
357
358impl Provider for Geoclue2 {
359 fn get(&self) -> Result<Location, ProviderError> {
363 Err(ProviderError)
365 }
366}
367
368impl Provider for LocationProvider {
369 fn get(&self) -> Result<Location, ProviderError> {
370 match self {
371 Self::Manual(t) => t.get(),
372 Self::Geoclue2(t) => t.get(),
373 }
374 }
375}
376
377impl Adjuster for AdjustmentMethod {
378 fn restore(&self) -> Result<(), AdjusterError> {
379 match self {
380 Self::Dummy(t) => t.restore(),
381 #[cfg(unix_without_macos)]
382 Self::Randr(t) => t.restore(),
383 #[cfg(unix_without_macos)]
384 Self::Drm(t) => t.restore(),
385 #[cfg(unix_without_macos)]
386 Self::Vidmode(t) => t.restore(),
387 #[cfg(windows)]
388 Self::Win32Gdi(t) => t.restore(),
389 }
390 }
391
392 fn set(
393 &self,
394 reset_ramps: bool,
395 cs: &ColorSettings,
396 ) -> Result<(), AdjusterError> {
397 match self {
398 Self::Dummy(t) => t.set(reset_ramps, cs),
399 #[cfg(unix_without_macos)]
400 Self::Randr(t) => t.set(reset_ramps, cs),
401 #[cfg(unix_without_macos)]
402 Self::Drm(t) => t.set(reset_ramps, cs),
403 #[cfg(unix_without_macos)]
404 Self::Vidmode(t) => t.set(reset_ramps, cs),
405 #[cfg(windows)]
406 Self::Win32Gdi(t) => t.set(reset_ramps, cs),
407 }
418 }
419}