monitor_input/
cli.rs

1use std::time::Instant;
2
3use super::*;
4use clap::{ArgAction, Parser};
5use log::*;
6use regex::Regex;
7
8#[derive(Debug, Default, Parser)]
9#[command(version, about)]
10/// A command line tool to change display monitors' input sources via DDC/CI.
11///
12/// # Examples
13/// ```
14/// # use monitor_input::Cli;
15/// fn run_cli(args: Vec<String>) -> anyhow::Result<()> {
16///     let mut cli = Cli::new();
17///     cli.args = args;
18///     cli.run()
19/// }
20/// ```
21/// To setup [`Cli`] from the command line arguments:
22/// ```no_run
23/// use clap::Parser;
24/// use monitor_input::{Cli,Monitor};
25///
26/// fn main() -> anyhow::Result<()> {
27///     let mut cli = Cli::parse();
28///     cli.init_logger();
29///     cli.monitors = Monitor::enumerate();
30///     cli.run()
31/// }
32/// ```
33/// See <https://github.com/kojiishi/monitor-input-rs> for more details.
34pub struct Cli {
35    #[arg(skip)]
36    /// The list of [`Monitor`]s to run the command line tool on.
37    /// This field is usually initialized to [`Monitor::enumerate()`].
38    pub monitors: Vec<Monitor>,
39
40    #[arg(short, long)]
41    /// Filter by the backend name.
42    pub backend: Option<String>,
43
44    #[arg(id = "capabilities", short, long)]
45    /// Get capabilities from the display monitors.
46    pub needs_capabilities: bool,
47
48    #[arg(short = 'n', long)]
49    /// Dry-run (prevent actual changes).
50    pub dry_run: bool,
51
52    #[arg(short, long, action = ArgAction::Count)]
53    /// Show verbose information.
54    pub verbose: u8,
55
56    #[arg(skip)]
57    set_index: Option<usize>,
58
59    /// `name` to search,
60    /// `name=input` to change the input source,
61    /// or `name=input1,input2` to toggle.
62    pub args: Vec<String>,
63}
64
65impl Cli {
66    /// Construct an instance with display monitors from [`Monitor::enumerate()`].
67    pub fn new() -> Self {
68        Cli {
69            monitors: Monitor::enumerate(),
70            ..Default::default()
71        }
72    }
73
74    /// Initialize the logging.
75    /// The configurations depend on [`Cli::verbose`].
76    #[deprecated(
77        since = "1.2.1",
78        note = "To remove loggers from the lib. Please setup your favorite logger."
79    )]
80    pub fn init_logger(&self) {
81        simplelog::CombinedLogger::init(vec![simplelog::TermLogger::new(
82            match self.verbose {
83                0 => LevelFilter::Info,
84                1 => LevelFilter::Debug,
85                _ => LevelFilter::Trace,
86            },
87            simplelog::ConfigBuilder::new()
88                .set_time_level(LevelFilter::Debug)
89                .build(),
90            simplelog::TerminalMode::Mixed,
91            simplelog::ColorChoice::Auto,
92        )])
93        .unwrap();
94    }
95
96    fn apply_filters(&mut self) -> anyhow::Result<()> {
97        if let Some(backend_str) = &self.backend {
98            self.monitors
99                .retain(|monitor| monitor.contains_backend(backend_str));
100        }
101        Ok(())
102    }
103
104    fn for_each<C>(&mut self, name: &str, mut callback: C) -> anyhow::Result<()>
105    where
106        C: FnMut(usize, &mut Monitor) -> anyhow::Result<()>,
107    {
108        if let Ok(index) = name.parse::<usize>() {
109            let monitor = &mut self.monitors[index];
110            if self.needs_capabilities {
111                // This may fail in some cases. Print warning but keep looking.
112                let _ = monitor.update_capabilities();
113            }
114            return callback(index, monitor);
115        }
116
117        let mut has_match = false;
118        for (index, monitor) in self.monitors.iter_mut().enumerate() {
119            if self.needs_capabilities {
120                // This may fail in some cases. Print warning but keep looking.
121                let _ = monitor.update_capabilities();
122            }
123            if !name.is_empty() && !monitor.contains(name) {
124                continue;
125            }
126            has_match = true;
127            callback(index, monitor)?;
128        }
129        if has_match {
130            return Ok(());
131        }
132
133        anyhow::bail!("No display monitors found for \"{name}\".");
134    }
135
136    fn compute_toggle_set_index(
137        current_input_source: InputSourceRaw,
138        input_sources: &[InputSourceRaw],
139    ) -> usize {
140        input_sources
141            .iter()
142            .position(|v| *v == current_input_source)
143            // Toggle to the next index, or 0 if it's not in the list.
144            .map_or(0, |i| i + 1)
145    }
146
147    fn toggle(&mut self, name: &str, values: &[&str]) -> anyhow::Result<()> {
148        let mut input_sources: Vec<InputSourceRaw> = vec![];
149        for value in values {
150            input_sources.push(InputSource::raw_from_str(value)?);
151        }
152        let mut set_index = self.set_index;
153        let result = self.for_each(name, |_, monitor: &mut Monitor| {
154            if set_index.is_none() {
155                let current_input_source = monitor.input_source()?;
156                set_index = Some(Self::compute_toggle_set_index(
157                    current_input_source,
158                    &input_sources,
159                ));
160                debug!(
161                    "Set = {index} (because InputSource({monitor}) is {input_source})",
162                    index = set_index.unwrap(),
163                    input_source = InputSource::str_from_raw(current_input_source)
164                );
165            }
166            let used_index = set_index.unwrap().min(input_sources.len() - 1);
167            let input_source = input_sources[used_index];
168            monitor.set_input_source(input_source)
169        });
170        self.set_index = set_index;
171        result
172    }
173
174    fn set(&mut self, name: &str, value: &str) -> anyhow::Result<()> {
175        let toggle_values: Vec<&str> = value.split(',').collect();
176        if toggle_values.len() > 1 {
177            return self.toggle(name, &toggle_values);
178        }
179        let input_source = InputSource::raw_from_str(value)?;
180        self.for_each(name, |_, monitor: &mut Monitor| {
181            monitor.set_input_source(input_source)
182        })
183    }
184
185    fn print_list(&mut self, name: &str) -> anyhow::Result<()> {
186        self.for_each(name, |index, monitor| {
187            println!("{index}: {}", monitor.to_long_string());
188            trace!("{monitor:?}");
189            Ok(())
190        })
191    }
192
193    fn sleep_all_if_needed(&mut self) {
194        let start_time = Instant::now();
195        for monitor in &mut self.monitors {
196            monitor.sleep_if_needed();
197        }
198        debug!("sleep_all() elapsed: {:?}", start_time.elapsed());
199    }
200
201    const RE_SET_PATTERN: &str = r"^([^=]+)=(.+)$";
202
203    /// Run the command line tool.
204    pub fn run(&mut self) -> anyhow::Result<()> {
205        let start_time = Instant::now();
206        Monitor::set_dry_run(self.dry_run);
207        self.apply_filters()?;
208
209        let re_set = Regex::new(Self::RE_SET_PATTERN).unwrap();
210        let mut has_valid_args = false;
211        let args = self.args.clone();
212        for arg in args {
213            if let Some(captures) = re_set.captures(&arg) {
214                self.set(&captures[1], &captures[2])?;
215                has_valid_args = true;
216                continue;
217            }
218
219            self.print_list(&arg)?;
220            has_valid_args = true;
221        }
222        if !has_valid_args {
223            self.print_list("")?;
224        }
225        self.sleep_all_if_needed();
226        debug!("Elapsed: {:?}", start_time.elapsed());
227        Ok(())
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use std::vec;
234
235    use super::*;
236
237    #[test]
238    fn cli_parse() {
239        let mut cli = Cli::parse_from([""]);
240        assert_eq!(cli.verbose, 0);
241        assert_eq!(cli.args.len(), 0);
242
243        cli = Cli::parse_from(["", "abc", "def"]);
244        assert_eq!(cli.verbose, 0);
245        assert_eq!(cli.args, ["abc", "def"]);
246
247        cli = Cli::parse_from(["", "-v", "abc", "def"]);
248        assert_eq!(cli.verbose, 1);
249        assert_eq!(cli.args, ["abc", "def"]);
250
251        cli = Cli::parse_from(["", "-vv", "abc", "def"]);
252        assert_eq!(cli.verbose, 2);
253        assert_eq!(cli.args, ["abc", "def"]);
254    }
255
256    #[test]
257    fn cli_parse_option_after_positional() {
258        let cli = Cli::parse_from(["", "abc", "def", "-v"]);
259        assert_eq!(cli.verbose, 1);
260        assert_eq!(cli.args, ["abc", "def"]);
261    }
262
263    #[test]
264    fn cli_parse_positional_with_hyphen() {
265        let cli = Cli::parse_from(["", "--", "-abc", "-def"]);
266        assert_eq!(cli.args, ["-abc", "-def"]);
267    }
268
269    fn matches<'a>(re: &'a Regex, input: &'a str) -> Vec<&'a str> {
270        re.captures(input)
271            .unwrap()
272            .iter()
273            .skip(1)
274            .map(|m| m.unwrap().as_str())
275            .collect()
276    }
277
278    #[test]
279    fn re_set() {
280        let re_set = Regex::new(Cli::RE_SET_PATTERN).unwrap();
281        assert!(!re_set.is_match("a"));
282        assert!(!re_set.is_match("a="));
283        assert!(!re_set.is_match("=a"));
284        assert_eq!(matches(&re_set, "a=b"), vec!["a", "b"]);
285        assert_eq!(matches(&re_set, "1=23"), vec!["1", "23"]);
286        assert_eq!(matches(&re_set, "12=34"), vec!["12", "34"]);
287        assert_eq!(matches(&re_set, "12=3,4"), vec!["12", "3,4"]);
288    }
289
290    #[test]
291    fn compute_toggle_set_index() {
292        assert_eq!(Cli::compute_toggle_set_index(1, &[1, 4, 9]), 1);
293        assert_eq!(Cli::compute_toggle_set_index(4, &[1, 4, 9]), 2);
294        assert_eq!(Cli::compute_toggle_set_index(9, &[1, 4, 9]), 3);
295        // The result should be 0 if the `value` isn't in the list.
296        assert_eq!(Cli::compute_toggle_set_index(0, &[1, 4, 9]), 0);
297        assert_eq!(Cli::compute_toggle_set_index(2, &[1, 4, 9]), 0);
298        assert_eq!(Cli::compute_toggle_set_index(10, &[1, 4, 9]), 0);
299    }
300}