reddish_shift/
cli.rs

1/*  config.rs -- Command line interface
2    This file is part of <https://github.com/mahor1221/reddish-shift>.
3    Copyright (C) 2024 Mahor Foruzesh <mahor1221@gmail.com>
4
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU General Public License for more details.
14
15    You should have received a copy of the GNU General Public License
16    along with this program.  If not, see <https://www.gnu.org/licenses/>.
17*/
18
19use crate::{
20    config::{DEFAULT_SLEEP_DURATION, DEFAULT_SLEEP_DURATION_SHORT},
21    types::{
22        AdjustmentMethodType, Brightness, BrightnessRange, Gamma, GammaRange,
23        LocationProviderType, Temperature, TemperatureRange, TransitionScheme,
24        MAX_TEMPERATURE, MIN_TEMPERATURE,
25    },
26};
27use anstream::ColorChoice;
28use clap::{
29    ArgAction, Args, ColorChoice as ClapColorChoice, Command, CommandFactory,
30    Parser, Subcommand,
31};
32use const_format::formatcp;
33use std::{cmp::Ordering, marker::PhantomData, path::PathBuf, str::FromStr};
34use tracing::{level_filters::LevelFilter, Level};
35
36const VERSION: &str = {
37    const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
38    const GIT_DESCRIBE: &str = env!("VERGEN_GIT_DESCRIBE");
39    const GIT_COMMIT_DATE: &str = env!("VERGEN_GIT_COMMIT_DATE");
40
41    #[allow(clippy::const_is_empty)]
42    if GIT_DESCRIBE.is_empty() {
43        formatcp!("{PKG_VERSION}")
44    } else {
45        formatcp!("{PKG_VERSION} ({GIT_DESCRIBE} {GIT_COMMIT_DATE})")
46    }
47};
48
49const LONG_VERSION: &str = {
50    const RUSTC_SEMVER: &str = env!("VERGEN_RUSTC_SEMVER");
51    const RUSTC_HOST_TRIPLE: &str = env!("VERGEN_RUSTC_HOST_TRIPLE");
52    const CARGO_FEATURES: &str = env!("VERGEN_CARGO_FEATURES");
53    const CARGO_TARGET_TRIPLE: &str = env!("VERGEN_CARGO_TARGET_TRIPLE");
54
55    formatcp!(
56        "{VERSION}
57
58rustc version:       {RUSTC_SEMVER}
59rustc host triple:   {RUSTC_HOST_TRIPLE}
60cargo features:      {CARGO_FEATURES}
61cargo target triple: {CARGO_TARGET_TRIPLE}"
62    )
63};
64
65#[derive(Debug, Parser)]
66#[command(about, version = VERSION, long_version = LONG_VERSION)]
67#[command(propagate_version = true, next_line_help(false))]
68pub struct CliArgs {
69    #[command(subcommand)]
70    pub mode: ModeArgs,
71
72    /// When to use color: auto, always, never [default: auto]
73    #[arg(long, value_name = "WHEN", value_parser = ClapColorChoice::from_str)]
74    #[arg(global = true, display_order(100))]
75    pub color: Option<ClapColorChoice>,
76
77    #[command(flatten)]
78    pub verbosity: Verbosity<InfoLevel>,
79}
80
81#[derive(Debug, Subcommand)]
82pub enum ModeArgs {
83    /// Apply screen color settings according to time of day continuously
84    #[command(next_line_help(true))]
85    Daemon {
86        #[command(flatten)]
87        c: CmdArgs,
88
89        /// Disable fading between color temperatures
90        ///
91        /// It will cause an immediate change between screen temperatures. by default,
92        /// the new screen temperature are gradually applied over a couple of seconds
93        #[arg(verbatim_doc_comment)]
94        #[arg(long, action = ArgAction::SetTrue)]
95        disable_fade: Option<bool>,
96
97        #[arg(help = formatcp!("Duration of sleep between screen updates [default: {DEFAULT_SLEEP_DURATION}]"))]
98        #[arg(long, value_name = "MILLISECONDS")]
99        sleep_duration: Option<u16>,
100
101        #[arg(help = formatcp!("Duration of sleep between screen updates for fade [default: {DEFAULT_SLEEP_DURATION_SHORT}]"))]
102        #[arg(long, value_name = "MILLISECONDS")]
103        sleep_duration_short: Option<u16>,
104    },
105
106    /// Like daemon mode, but do not run continuously
107    #[command(next_line_help(true))]
108    Oneshot {
109        #[command(flatten)]
110        c: CmdArgs,
111    },
112
113    /// Apply a specific screen color settings
114    #[command(next_line_help(true))]
115    Set {
116        #[command(flatten)]
117        cs: ColorSettingsArgs,
118        #[command(flatten)]
119        i: CmdInnerArgs,
120    },
121
122    /// Remove adjustment from screen
123    #[command(next_line_help(true))]
124    Reset {
125        #[command(flatten)]
126        i: CmdInnerArgs,
127    },
128
129    /// Print all solar elevation angles for the next 24 hours
130    #[command(next_line_help(true))]
131    Print {
132        /// Location [default: 0:0]
133        ///
134        /// Either set latitude and longitude manually or select a location provider.
135        /// Negative values represent west and south, respectively. e.g.:
136        ///     51.48:0.0 (Greenwich)
137        ///     geoclue2  (Currently not available)
138        #[arg(verbatim_doc_comment)]
139        #[arg(long, short, value_parser = LocationProviderType::from_str)]
140        #[arg(value_name = "LATITUDE:LONGITUDE | PROVIDER")]
141        #[arg(allow_hyphen_values = true)]
142        location: LocationProviderType,
143    },
144}
145
146#[derive(Debug, Args)]
147pub struct ColorSettingsArgs {
148    /// Color temperature to apply [default: 6500]
149    ///
150    /// The neutral temperature is 6500K. Using this value will not change the color
151    /// temperature of the display. Setting the color temperature to a value higher
152    /// than this results in more blue light, and setting a lower value will result
153    /// in more red light.
154    #[arg(verbatim_doc_comment)]
155    #[arg(long, short, value_parser = Temperature::from_str)]
156    #[arg(value_name = formatcp!("FROM {MIN_TEMPERATURE} TO {MAX_TEMPERATURE}"))]
157    #[arg(default_value_t)]
158    pub temperature: Temperature,
159
160    /// Additional gamma correction to apply [default: 1.0]
161    ///
162    /// Either set it for all colors, or each color channel individually. e.g.:
163    ///     0.9         (R=G=B=0.9)
164    ///     0.8:0.9:0.9 (R=0.8, G=0.9, B=0.9)
165    #[arg(verbatim_doc_comment)]
166    #[arg(long, short, value_parser = Gamma::from_str)]
167    #[arg(value_name = "FROM 0.1 TO 10")]
168    #[arg(default_value_t)]
169    pub gamma: Gamma,
170
171    /// Screen brightness to apply [default: 1.0]
172    #[arg(verbatim_doc_comment)]
173    #[arg(long, short, value_parser = Brightness::from_str)]
174    #[arg(value_name = "FROM 0.1 TO 1.0")]
175    #[arg(default_value_t)]
176    pub brightness: Brightness,
177}
178
179#[derive(Debug, Args)]
180pub struct CmdArgs {
181    /// Color temperature to set for day and night [default: 6500-4500]
182    ///
183    /// The neutral temperature is 6500K. Using this value will not change the color
184    /// temperature of the display. Setting the color temperature to a value higher
185    /// than this results in more blue light, and setting a lower value will result
186    /// in more red light. e.g.:
187    ///     5000      (day=night=5000)
188    ///     6500-4500 (day=6500, night=4500)
189    #[arg(verbatim_doc_comment)]
190    #[arg(long, short, value_parser = TemperatureRange::from_str)]
191    #[arg(value_name = formatcp!("FROM {MIN_TEMPERATURE} TO {MAX_TEMPERATURE}"))]
192    pub temperature: Option<TemperatureRange>,
193
194    /// Additional gamma correction to apply for day and night [default: 1.0]
195    ///
196    /// Either set it for all colors, or each color channel individually. e.g.:
197    ///   - 0.9               (day=night=0.9)
198    ///   - 1.0 - 0.8:0.9:0.9 (day=1.0, night=(R=0.8, G=0.9, B=0.9))
199    #[arg(verbatim_doc_comment)]
200    #[arg(long, short, value_parser = GammaRange::from_str)]
201    #[arg(value_name = "FROM 0.1 TO 10")]
202    pub gamma: Option<GammaRange>,
203
204    /// Screen brightness to apply for day and night [default: 1.0]
205    ///
206    /// It is a fake brightness adjustment obtained by manipulating the gamma ramps
207    /// which means that it does not reduce the backlight of the screen. e.g.:
208    ///     0.8     (day=night=0.8)
209    ///     1.0-0.8 (day=1.0, night=0.8)
210    #[arg(verbatim_doc_comment)]
211    #[arg(long, short, value_parser = BrightnessRange::from_str)]
212    #[arg(value_name = "FROM 0.1 TO 1.0")]
213    pub brightness: Option<BrightnessRange>,
214
215    /// Transition scheme [default: 3:-6]
216    ///
217    /// Either time ranges or elevation angles. By default, Reddish Shift will use
218    /// the current elevation of the sun to determine whether it is daytime, night
219    /// or in transition (dawn/dusk). You can also use the print command to see
220    /// solar elevation angles for the next 24 hours. e.g.:
221    ///     6:00-7:45 - 18:35-20:15 (dawn=6:00-7:45, dusk=18:35-20:15)
222    ///     7:45 - 18:35            (day starts at 7:45, night starts at 20:15)
223    ///     3:-6                    (above 3° is day, bellow -6° is night)
224    #[arg(verbatim_doc_comment)]
225    #[arg(long, short, value_parser = TransitionScheme::from_str)]
226    #[arg(value_name = "TIME-TIME - TIME-TIME | TIME-TIME | DEGREE:DEGREE")]
227    #[arg(allow_hyphen_values = true)]
228    pub scheme: Option<TransitionScheme>,
229
230    /// Location, used for computation of current solar elevation [default: 0:0]
231    ///
232    /// It is not needed when using manual time ranges for transition scheme Either
233    /// set latitude and longitude manually or select a location provider. Negative
234    /// values represent west and south, respectively. e.g.:
235    ///     51.48:0.0 (Greenwich)
236    ///     geoclue2 (Currently not available)
237    #[arg(verbatim_doc_comment)]
238    #[arg(long, short, value_parser = LocationProviderType::from_str)]
239    #[arg(value_name = "LATITUDE:LONGITUDE | PROVIDER")]
240    #[arg(allow_hyphen_values = true)]
241    pub location: Option<LocationProviderType>,
242
243    #[command(flatten)]
244    pub i: CmdInnerArgs,
245}
246
247#[derive(Debug, Args)]
248pub struct CmdInnerArgs {
249    /// Adjustment method to use to apply color settings
250    ///
251    /// If not set, the first available method will be used. e.g.:
252    ///     dummy               (does not affect the display)
253    ///   XVidMode extension:
254    ///     vidmode             (apply to $DISPLAY)
255    ///     vidmode:0           (apply to screen 0)
256    ///   XRANDR extension:
257    ///     randr               (apply to $DISPLAY)
258    ///     randr:0             (apply to screen 0)
259    ///     randr$DISPLAY:62,63 (apply to $DISPLAY with crtcs 62 and 63)
260    ///   Direct Rendering Manager:
261    ///     drm                 (apply to /dev/dri/card0)
262    ///     drm:1               (apply to /dev/dri/card1)
263    ///     drm:0:80            (apply to /dev/dri/card0 with crtc 80)
264    ///   Windows graphics device interface:
265    ///     win32gdi            (apply to current display)
266    #[arg(verbatim_doc_comment)]
267    #[arg(long, short, value_parser = AdjustmentMethodType::from_str)]
268    #[arg(
269        value_name = "METHOD [:(DISPLAY_NUM | CARD_NUM) [:CRTC1,CRTC2,...]]"
270    )]
271    pub method: Option<AdjustmentMethodType>,
272
273    /// Reset existing gamma ramps before applying new color settings
274    #[arg(long, action = ArgAction::SetTrue)]
275    pub reset_ramps: Option<bool>,
276
277    /// Path of the config file
278    ///
279    /// A template for the config file should have been installed alongside
280    /// the program.
281    #[arg(long, short, value_name = "FILE", display_order(99))]
282    pub config: Option<PathBuf>,
283}
284
285//
286
287pub trait DefaultLevel {
288    fn default() -> Option<Level>;
289}
290
291#[derive(Debug, Clone, Copy, Default)]
292pub struct InfoLevel;
293impl DefaultLevel for InfoLevel {
294    fn default() -> Option<Level> {
295        Some(Level::INFO)
296    }
297}
298
299#[derive(Args, Debug, Clone, Copy, Default)]
300pub struct Verbosity<L: DefaultLevel = InfoLevel> {
301    /// Increase verbosity
302    // #[arg(short, long, action = clap::ArgAction::Count)]
303    // #[arg(global = true, display_order(100), conflicts_with = "quite")]
304    #[arg(skip)]
305    verbose: u8,
306
307    /// Decrease verbosity
308    #[arg(short, long, action = clap::ArgAction::Count, global = true)]
309    #[arg(global = true, display_order(100))]
310    quiet: u8,
311
312    #[arg(skip)]
313    phantom: PhantomData<L>,
314}
315
316impl<L: DefaultLevel> Verbosity<L> {
317    pub fn level_filter(&self) -> LevelFilter {
318        self.level().into()
319    }
320
321    /// [None] means all output is disabled.
322    pub fn level(&self) -> Option<Level> {
323        match self.verbosity() {
324            i8::MIN..=-1 => None,
325            0 => Some(Level::ERROR),
326            1 => Some(Level::WARN),
327            2 => Some(Level::INFO),
328            3 => Some(Level::DEBUG),
329            4..=i8::MAX => Some(Level::TRACE),
330        }
331    }
332
333    fn verbosity(&self) -> i8 {
334        Self::level_i8(L::default()) - (self.quiet as i8)
335            + (self.verbose as i8)
336    }
337
338    fn level_i8(level: Option<Level>) -> i8 {
339        match level {
340            None => -1,
341            Some(Level::ERROR) => 0,
342            Some(Level::WARN) => 1,
343            Some(Level::INFO) => 2,
344            Some(Level::DEBUG) => 3,
345            Some(Level::TRACE) => 4,
346        }
347    }
348}
349
350impl<L: DefaultLevel> Eq for Verbosity<L> {}
351impl<L: DefaultLevel> PartialEq for Verbosity<L> {
352    fn eq(&self, other: &Self) -> bool {
353        self.level() == other.level()
354    }
355}
356
357impl<L: DefaultLevel> Ord for Verbosity<L> {
358    fn cmp(&self, other: &Self) -> Ordering {
359        self.level().cmp(&other.level())
360    }
361}
362impl<L: DefaultLevel> PartialOrd for Verbosity<L> {
363    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
364        Some(self.cmp(other))
365    }
366}
367
368pub trait ClapColorChoiceExt {
369    fn to_choice(&self) -> ColorChoice;
370}
371
372impl ClapColorChoiceExt for ClapColorChoice {
373    fn to_choice(&self) -> ColorChoice {
374        match self {
375            ClapColorChoice::Auto => ColorChoice::Auto,
376            ClapColorChoice::Always => ColorChoice::Always,
377            ClapColorChoice::Never => ColorChoice::Never,
378        }
379    }
380}
381
382// used for generation of shell completion scripts and man pages
383
384pub fn cli_args_command() -> Command {
385    CliArgs::command()
386}