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    pub fn init_logger(&self) {
77        simplelog::CombinedLogger::init(vec![simplelog::TermLogger::new(
78            match self.verbose {
79                0 => LevelFilter::Info,
80                1 => LevelFilter::Debug,
81                _ => LevelFilter::Trace,
82            },
83            simplelog::ConfigBuilder::new()
84                .set_time_level(LevelFilter::Debug)
85                .build(),
86            simplelog::TerminalMode::Mixed,
87            simplelog::ColorChoice::Auto,
88        )])
89        .unwrap();
90    }
91
92    fn apply_filters(&mut self) -> anyhow::Result<()> {
93        if let Some(backend_str) = &self.backend {
94            self.monitors
95                .retain(|monitor| monitor.contains_backend(backend_str));
96        }
97        Ok(())
98    }
99
100    fn for_each<C>(&mut self, name: &str, mut callback: C) -> anyhow::Result<()>
101    where
102        C: FnMut(usize, &mut Monitor) -> anyhow::Result<()>,
103    {
104        if let Ok(index) = name.parse::<usize>() {
105            let monitor = &mut self.monitors[index];
106            if self.needs_capabilities {
107                // This may fail in some cases. Print warning but keep looking.
108                let _ = monitor.update_capabilities();
109            }
110            return callback(index, monitor);
111        }
112
113        let mut has_match = false;
114        for (index, monitor) in (&mut self.monitors).into_iter().enumerate() {
115            if self.needs_capabilities {
116                // This may fail in some cases. Print warning but keep looking.
117                let _ = monitor.update_capabilities();
118            }
119            if name.len() > 0 && !monitor.contains(name) {
120                continue;
121            }
122            has_match = true;
123            callback(index, monitor)?;
124        }
125        if has_match {
126            return Ok(());
127        }
128
129        anyhow::bail!("No display monitors found for \"{name}\".");
130    }
131
132    fn compute_toggle_set_index(
133        current_input_source: InputSourceRaw,
134        input_sources: &[InputSourceRaw],
135    ) -> usize {
136        input_sources
137            .iter()
138            .position(|v| *v == current_input_source)
139            // Toggle to the next index, or 0 if it's not in the list.
140            .map_or(0, |i| i + 1)
141    }
142
143    fn toggle(&mut self, name: &str, values: &[&str]) -> anyhow::Result<()> {
144        let mut input_sources: Vec<InputSourceRaw> = vec![];
145        for value in values {
146            input_sources.push(InputSource::raw_from_str(value)?);
147        }
148        let mut set_index = self.set_index;
149        let result = self.for_each(name, |_, monitor: &mut Monitor| {
150            if set_index.is_none() {
151                let current_input_source = monitor.input_source()?;
152                set_index = Some(Self::compute_toggle_set_index(
153                    current_input_source,
154                    &input_sources,
155                ));
156                debug!(
157                    "Set = {index} (because InputSource({monitor}) is {input_source})",
158                    index = set_index.unwrap(),
159                    input_source = InputSource::str_from_raw(current_input_source)
160                );
161            }
162            let used_index = set_index.unwrap().min(input_sources.len() - 1);
163            let input_source = input_sources[used_index];
164            monitor.set_input_source(input_source)
165        });
166        self.set_index = set_index;
167        result
168    }
169
170    fn set(&mut self, name: &str, value: &str) -> anyhow::Result<()> {
171        let toggle_values: Vec<&str> = value.split(',').collect();
172        if toggle_values.len() > 1 {
173            return self.toggle(name, &toggle_values);
174        }
175        let input_source = InputSource::raw_from_str(value)?;
176        self.for_each(name, |_, monitor: &mut Monitor| {
177            monitor.set_input_source(input_source)
178        })
179    }
180
181    fn print_list(&mut self, name: &str) -> anyhow::Result<()> {
182        self.for_each(name, |index, monitor| {
183            println!("{index}: {}", monitor.to_long_string());
184            trace!("{:?}", monitor);
185            Ok(())
186        })
187    }
188
189    fn sleep_all_if_needed(&mut self) {
190        let start_time = Instant::now();
191        for monitor in &mut self.monitors {
192            monitor.sleep_if_needed();
193        }
194        debug!("sleep_all() elapsed: {:?}", start_time.elapsed());
195    }
196
197    const RE_SET_PATTERN: &str = r"^([^=]+)=(.+)$";
198
199    /// Run the command line tool.
200    pub fn run(&mut self) -> anyhow::Result<()> {
201        let start_time = Instant::now();
202        Monitor::set_dry_run(self.dry_run);
203        self.apply_filters()?;
204
205        let re_set = Regex::new(Self::RE_SET_PATTERN).unwrap();
206        let mut has_valid_args = false;
207        let args = self.args.clone();
208        for arg in args {
209            if let Some(captures) = re_set.captures(&arg) {
210                self.set(&captures[1], &captures[2])?;
211                has_valid_args = true;
212                continue;
213            }
214
215            self.print_list(&arg)?;
216            has_valid_args = true;
217        }
218        if !has_valid_args {
219            self.print_list("")?;
220        }
221        self.sleep_all_if_needed();
222        debug!("Elapsed: {:?}", start_time.elapsed());
223        Ok(())
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use std::vec;
230
231    use super::*;
232
233    #[test]
234    fn cli_parse() {
235        let mut cli = Cli::parse_from([""]);
236        assert_eq!(cli.verbose, 0);
237        assert_eq!(cli.args.len(), 0);
238
239        cli = Cli::parse_from(["", "abc", "def"]);
240        assert_eq!(cli.verbose, 0);
241        assert_eq!(cli.args, ["abc", "def"]);
242
243        cli = Cli::parse_from(["", "-v", "abc", "def"]);
244        assert_eq!(cli.verbose, 1);
245        assert_eq!(cli.args, ["abc", "def"]);
246
247        cli = Cli::parse_from(["", "-vv", "abc", "def"]);
248        assert_eq!(cli.verbose, 2);
249        assert_eq!(cli.args, ["abc", "def"]);
250    }
251
252    #[test]
253    fn cli_parse_option_after_positional() {
254        let cli = Cli::parse_from(["", "abc", "def", "-v"]);
255        assert_eq!(cli.verbose, 1);
256        assert_eq!(cli.args, ["abc", "def"]);
257    }
258
259    #[test]
260    fn cli_parse_positional_with_hyphen() {
261        let cli = Cli::parse_from(["", "--", "-abc", "-def"]);
262        assert_eq!(cli.args, ["-abc", "-def"]);
263    }
264
265    fn matches<'a>(re: &'a Regex, input: &'a str) -> Vec<&'a str> {
266        re.captures(input)
267            .unwrap()
268            .iter()
269            .skip(1)
270            .map(|m| m.unwrap().as_str())
271            .collect()
272    }
273
274    #[test]
275    fn re_set() {
276        let re_set = Regex::new(Cli::RE_SET_PATTERN).unwrap();
277        assert_eq!(re_set.is_match("a"), false);
278        assert_eq!(re_set.is_match("a="), false);
279        assert_eq!(re_set.is_match("=a"), false);
280        assert_eq!(matches(&re_set, "a=b"), vec!["a", "b"]);
281        assert_eq!(matches(&re_set, "1=23"), vec!["1", "23"]);
282        assert_eq!(matches(&re_set, "12=34"), vec!["12", "34"]);
283        assert_eq!(matches(&re_set, "12=3,4"), vec!["12", "3,4"]);
284    }
285
286    #[test]
287    fn compute_toggle_set_index() {
288        assert_eq!(Cli::compute_toggle_set_index(1, &[1, 4, 9]), 1);
289        assert_eq!(Cli::compute_toggle_set_index(4, &[1, 4, 9]), 2);
290        assert_eq!(Cli::compute_toggle_set_index(9, &[1, 4, 9]), 3);
291        // The result should be 0 if the `value` isn't in the list.
292        assert_eq!(Cli::compute_toggle_set_index(0, &[1, 4, 9]), 0);
293        assert_eq!(Cli::compute_toggle_set_index(2, &[1, 4, 9]), 0);
294        assert_eq!(Cli::compute_toggle_set_index(10, &[1, 4, 9]), 0);
295    }
296}