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