monitor_input/
lib.rs

1use std::str::FromStr;
2
3use anyhow::Context;
4use clap::Parser;
5use ddc_hi::{Ddc, DdcHost, FeatureCode};
6use log::*;
7use regex::Regex;
8use strum_macros::{AsRefStr, EnumString, FromRepr};
9
10/// The raw representation of an input source value.
11/// See also [`InputSource`].
12pub type InputSourceRaw = u8;
13
14#[derive(Copy, Clone, Debug, PartialEq, AsRefStr, EnumString, FromRepr)]
15#[repr(u8)]
16#[strum(ascii_case_insensitive)]
17/// An input source value.
18/// See also [`InputSourceRaw`].
19pub enum InputSource {
20    #[strum(serialize = "DP1")]
21    DisplayPort1 = 0x0F,
22    #[strum(serialize = "DP2")]
23    DisplayPort2 = 0x10,
24    Hdmi1 = 0x11,
25    Hdmi2 = 0x12,
26    UsbC1 = 0x19,
27    UsbC2 = 0x1B,
28}
29
30impl InputSource {
31    /// Get [`InputSourceRaw`].
32    /// ```
33    /// # use monitor_input::InputSource;
34    /// assert_eq!(InputSource::Hdmi1.as_raw(), 17);
35    /// ```
36    pub fn as_raw(self) -> InputSourceRaw {
37        self as InputSourceRaw
38    }
39
40    /// Get [`InputSourceRaw`] from a string.
41    /// The string is either the name of an [`InputSource`] or a number.
42    /// # Examples
43    /// ```
44    /// # use monitor_input::InputSource;
45    /// // Input strings are either an [`InputSource`] or a number.
46    /// assert_eq!(
47    ///     InputSource::raw_from_str("Hdmi1").unwrap(),
48    ///     InputSource::Hdmi1.as_raw()
49    /// );
50    /// assert_eq!(InputSource::raw_from_str("27").unwrap(), 27);
51    ///
52    /// // Undefined string will be an error.
53    /// assert!(InputSource::raw_from_str("xyz").is_err());
54    /// // The error message should contain the original string.
55    /// assert!(
56    ///     InputSource::raw_from_str("xyz")
57    ///         .unwrap_err()
58    ///         .to_string()
59    ///         .contains("xyz")
60    /// );
61    /// ```
62    pub fn raw_from_str(input: &str) -> anyhow::Result<InputSourceRaw> {
63        if let Ok(value) = input.parse::<InputSourceRaw>() {
64            return Ok(value);
65        }
66        InputSource::from_str(input)
67            .map(|value| value.as_raw())
68            .with_context(|| format!("\"{input}\" is not a valid input source"))
69    }
70
71    /// Get a string from [`InputSourceRaw`].
72    /// # Examples
73    /// ```
74    /// # use monitor_input::InputSource;
75    /// assert_eq!(InputSource::str_from_raw(InputSource::Hdmi1.as_raw()), "Hdmi1");
76    /// assert_eq!(InputSource::str_from_raw(17), "Hdmi1");
77    /// assert_eq!(InputSource::str_from_raw(255), "255");
78    /// ```
79    pub fn str_from_raw(value: InputSourceRaw) -> String {
80        match InputSource::from_repr(value) {
81            Some(input_source) => input_source.as_ref().to_string(),
82            None => value.to_string(),
83        }
84    }
85}
86
87/// VCP feature code for input select
88const INPUT_SELECT: FeatureCode = 0x60;
89
90static mut DRY_RUN: bool = false;
91
92/// Represents a display monitor.
93/// # Examples
94/// ```no_run
95/// # use monitor_input::{InputSource,Monitor};
96/// let mut monitors = Monitor::enumerate();
97/// monitors[0].set_current_input_source(InputSource::UsbC1.as_raw());
98/// ```
99pub struct Monitor {
100    ddc_hi_display: ddc_hi::Display,
101    is_capabilities_updated: bool,
102    needs_sleep: bool,
103}
104
105impl std::fmt::Display for Monitor {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        write!(f, "{}", self.ddc_hi_display.info.id)
108    }
109}
110
111impl std::fmt::Debug for Monitor {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        write!(f, "{:?}", self.ddc_hi_display.info)
114    }
115}
116
117impl Monitor {
118    /// Create an instance from [`ddc_hi::Display`].
119    pub fn new(ddc_hi_display: ddc_hi::Display) -> Self {
120        Monitor {
121            ddc_hi_display: ddc_hi_display,
122            is_capabilities_updated: false,
123            needs_sleep: false,
124        }
125    }
126
127    /// Enumerate all display monitors.
128    /// See also [`ddc_hi::Display::enumerate()`].
129    pub fn enumerate() -> Vec<Self> {
130        ddc_hi::Display::enumerate()
131            .into_iter()
132            .map(|d| Monitor::new(d))
133            .collect()
134    }
135
136    fn is_dry_run() -> bool {
137        unsafe { return DRY_RUN }
138    }
139
140    /// Set the dry-run mode.
141    /// When in dry-run mode, functions that are supposed to make changes
142    /// don't actually make the changes.
143    pub fn set_dry_run(value: bool) {
144        unsafe { DRY_RUN = value }
145    }
146
147    /// Updates the display info with data retrieved from the device's
148    /// reported capabilities.
149    /// See also [`ddc_hi::Display::update_capabilities()`].
150    pub fn update_capabilities(&mut self) -> anyhow::Result<()> {
151        if self.is_capabilities_updated {
152            return Ok(());
153        }
154        self.is_capabilities_updated = true;
155        debug!("update_capabilities: {}", self);
156        self.ddc_hi_display
157            .update_capabilities()
158            .inspect_err(|e| warn!("{self}: Failed to update capabilities: {e}"))
159    }
160
161    fn contains_backend(&self, backend: &str) -> bool {
162        self.ddc_hi_display
163            .info
164            .backend
165            .to_string()
166            .contains(backend)
167    }
168
169    fn contains(&self, name: &str) -> bool {
170        self.ddc_hi_display.info.id.contains(name)
171    }
172
173    fn feature_descriptor(&self, feature_code: FeatureCode) -> Option<&mccs_db::Descriptor> {
174        self.ddc_hi_display.info.mccs_database.get(feature_code)
175    }
176
177    fn feature_code(&self, feature_code: FeatureCode) -> FeatureCode {
178        // TODO: `mccs_database` is initialized by `display.update_capabilities()`
179        // which is quite slow, and it seems to work without this.
180        // See also https://github.com/mjkoo/monitor-switch/blob/master/src/main.rs.
181        if let Some(feature) = self.feature_descriptor(feature_code) {
182            return feature.code;
183        }
184        feature_code
185    }
186
187    /// Get the current input source.
188    pub fn current_input_source(&mut self) -> anyhow::Result<InputSourceRaw> {
189        let feature_code: FeatureCode = self.feature_code(INPUT_SELECT);
190        Ok(self.ddc_hi_display.handle.get_vcp_feature(feature_code)?.sl)
191    }
192
193    /// Set the current input source.
194    pub fn set_current_input_source(&mut self, value: InputSourceRaw) -> anyhow::Result<()> {
195        if Self::is_dry_run() {
196            info!(
197                "{}.InputSource = {} (dry-run)",
198                self,
199                InputSource::str_from_raw(value)
200            );
201            return Ok(());
202        }
203        info!(
204            "{}.InputSource = {}",
205            self,
206            InputSource::str_from_raw(value)
207        );
208        let feature_code: FeatureCode = self.feature_code(INPUT_SELECT);
209        self.ddc_hi_display
210            .handle
211            .set_vcp_feature(feature_code, value as u16)
212            .inspect(|_| self.needs_sleep = true)
213    }
214
215    /// Get all input sources.
216    /// Requires to call [`Monitor::update_capabilities()`] beforehand.
217    pub fn input_sources(&mut self) -> Option<Vec<InputSourceRaw>> {
218        if let Some(feature) = self.feature_descriptor(INPUT_SELECT) {
219            debug!("{self}.INPUT_SELECT = {feature:?}");
220            if let mccs_db::ValueType::NonContinuous { values, .. } = &feature.ty {
221                return Some(values.keys().cloned().collect());
222            }
223        }
224        None
225    }
226
227    /// Sleep if any previous DDC commands need time to be executed.
228    /// See also [`ddc_hi::DdcHost::sleep()`].
229    pub fn sleep_if_needed(&mut self) {
230        if self.needs_sleep {
231            debug!("{}.sleep()", self);
232            self.needs_sleep = false;
233            self.ddc_hi_display.handle.sleep();
234            debug!("{}.sleep() done", self);
235        }
236    }
237
238    /// Get a multi-line descriptive string.
239    pub fn to_long_string(&mut self) -> String {
240        let mut lines = Vec::new();
241        lines.push(self.to_string());
242        let input_source = self.current_input_source();
243        lines.push(format!(
244            "Input Source: {}",
245            match input_source {
246                Ok(value) => InputSource::str_from_raw(value),
247                Err(e) => e.to_string(),
248            }
249        ));
250        if let Some(input_sources) = self.input_sources() {
251            lines.push(format!(
252                "Input Sources: {}",
253                input_sources
254                    .iter()
255                    .map(|value| InputSource::str_from_raw(*value))
256                    .collect::<Vec<_>>()
257                    .join(", ")
258            ));
259        }
260        if let Some(model) = &self.ddc_hi_display.info.model_name {
261            lines.push(format!("Model: {}", model));
262        }
263        lines.push(format!("Backend: {}", self.ddc_hi_display.info.backend));
264        return lines.join("\n    ");
265    }
266}
267
268#[derive(Debug, Default, Parser)]
269#[command(version, about)]
270/// A command line tool to change display monitors' input sources via DDC/CI.
271///
272/// # Examples
273/// ```
274/// # use monitor_input::Cli;
275/// fn run_cli(args: Vec<String>) -> anyhow::Result<()> {
276///     let mut cli = Cli::new();
277///     cli.args = args;
278///     cli.run()
279/// }
280/// ```
281/// To setup [`Cli`] from the command line arguments:
282/// ```no_run
283/// use clap::Parser;
284/// use monitor_input::{Cli,Monitor};
285///
286/// fn main() -> anyhow::Result<()> {
287///     let mut cli = Cli::parse();
288///     cli.init_logger();
289///     cli.monitors = Monitor::enumerate();
290///     cli.run()
291/// }
292/// ```
293/// See <https://github.com/kojiishi/monitor-input-rs> for more details.
294pub struct Cli {
295    #[arg(skip)]
296    /// The list of [`Monitor`]s to run the command line tool on.
297    /// This field is usually initialized to [`Monitor::enumerate()`].
298    pub monitors: Vec<Monitor>,
299
300    #[arg(short, long)]
301    /// Filter by the backend name.
302    pub backend: Option<String>,
303
304    #[arg(id = "capabilities", short, long)]
305    /// Get capabilities from the display monitors.
306    pub needs_capabilities: bool,
307
308    #[arg(short = 'n', long)]
309    /// Dry-run (prevent actual changes).
310    pub dry_run: bool,
311
312    #[arg(short, long)]
313    /// Show verbose information.
314    pub verbose: bool,
315
316    #[arg(skip)]
317    set_index: Option<usize>,
318
319    /// `name` to search,
320    /// `name=input` to change the input source,
321    /// or `name=input1,input2` to toggle.
322    pub args: Vec<String>,
323}
324
325impl Cli {
326    /// Construct an instance with display monitors from [`Monitor::enumerate()`].
327    pub fn new() -> Self {
328        Cli {
329            monitors: Monitor::enumerate(),
330            ..Default::default()
331        }
332    }
333
334    /// Initialize the logging.
335    /// The configurations depend on [`Cli::verbose`].
336    pub fn init_logger(&self) {
337        simplelog::CombinedLogger::init(vec![simplelog::TermLogger::new(
338            if self.verbose {
339                simplelog::LevelFilter::Debug
340            } else {
341                simplelog::LevelFilter::Info
342            },
343            simplelog::Config::default(),
344            simplelog::TerminalMode::Mixed,
345            simplelog::ColorChoice::Auto,
346        )])
347        .unwrap();
348    }
349
350    fn apply_filters(&mut self) -> anyhow::Result<()> {
351        if let Some(backend_str) = &self.backend {
352            self.monitors
353                .retain(|monitor| monitor.contains_backend(backend_str));
354        }
355        Ok(())
356    }
357
358    fn for_each<C>(&mut self, name: &str, mut callback: C) -> anyhow::Result<()>
359    where
360        C: FnMut(usize, &mut Monitor) -> anyhow::Result<()>,
361    {
362        if let Ok(index) = name.parse::<usize>() {
363            let monitor = &mut self.monitors[index];
364            if self.needs_capabilities {
365                // This may fail in some cases. Print warning but keep looking.
366                let _ = monitor.update_capabilities();
367            }
368            return callback(index, monitor);
369        }
370
371        let mut has_match = false;
372        for (index, monitor) in (&mut self.monitors).into_iter().enumerate() {
373            if self.needs_capabilities {
374                // This may fail in some cases. Print warning but keep looking.
375                let _ = monitor.update_capabilities();
376            }
377            if name.len() > 0 && !monitor.contains(name) {
378                continue;
379            }
380            has_match = true;
381            callback(index, monitor)?;
382        }
383        if has_match {
384            return Ok(());
385        }
386
387        anyhow::bail!("No display monitors found for \"{}\".", name);
388    }
389
390    fn compute_toggle_set_index(
391        current_input_source: InputSourceRaw,
392        input_sources: &[InputSourceRaw],
393    ) -> usize {
394        input_sources
395            .iter()
396            .position(|v| *v == current_input_source)
397            // Toggle to the next index, or 0 if it's not in the list.
398            .map_or(0, |i| i + 1)
399    }
400
401    fn toggle(&mut self, name: &str, values: &[&str]) -> anyhow::Result<()> {
402        let mut input_sources: Vec<InputSourceRaw> = vec![];
403        for value in values {
404            input_sources.push(InputSource::raw_from_str(value)?);
405        }
406        let mut set_index = self.set_index;
407        let result = self.for_each(name, |_, monitor: &mut Monitor| {
408            if set_index.is_none() {
409                let current_input_source = monitor.current_input_source()?;
410                set_index = Some(Self::compute_toggle_set_index(
411                    current_input_source,
412                    &input_sources,
413                ));
414                debug!(
415                    "Set = {} (because {monitor}.InputSource is {})",
416                    set_index.unwrap(),
417                    InputSource::str_from_raw(current_input_source)
418                );
419            }
420            let used_index = set_index.unwrap().min(input_sources.len() - 1);
421            let input_source = input_sources[used_index];
422            monitor.set_current_input_source(input_source)
423        });
424        self.set_index = set_index;
425        result
426    }
427
428    fn set(&mut self, name: &str, value: &str) -> anyhow::Result<()> {
429        let toggle_values: Vec<&str> = value.split(',').collect();
430        if toggle_values.len() > 1 {
431            return self.toggle(name, &toggle_values);
432        }
433        let input_source = InputSource::raw_from_str(value)?;
434        self.for_each(name, |_, monitor: &mut Monitor| {
435            monitor.set_current_input_source(input_source)
436        })
437    }
438
439    fn print_list(&mut self, name: &str) -> anyhow::Result<()> {
440        self.for_each(name, |index, monitor| {
441            println!("{index}: {}", monitor.to_long_string());
442            debug!("{:?}", monitor);
443            Ok(())
444        })
445    }
446
447    fn sleep_if_needed(&mut self) {
448        for monitor in &mut self.monitors {
449            monitor.sleep_if_needed();
450        }
451        debug!("All sleep() done");
452    }
453
454    const RE_SET_PATTERN: &str = r"^([^=]+)=(.+)$";
455
456    /// Run the command line tool.
457    pub fn run(&mut self) -> anyhow::Result<()> {
458        Monitor::set_dry_run(self.dry_run);
459        self.apply_filters()?;
460
461        let re_set = Regex::new(Self::RE_SET_PATTERN).unwrap();
462        let mut has_valid_args = false;
463        let args = self.args.clone();
464        for arg in args {
465            if let Some(captures) = re_set.captures(&arg) {
466                self.set(&captures[1], &captures[2])?;
467                has_valid_args = true;
468                continue;
469            }
470
471            self.print_list(&arg)?;
472            has_valid_args = true;
473        }
474        if !has_valid_args {
475            self.print_list("")?;
476        }
477        self.sleep_if_needed();
478        Ok(())
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use std::vec;
485
486    use super::*;
487
488    #[test]
489    fn input_source_from_str() {
490        assert_eq!(InputSource::from_str("Hdmi1"), Ok(InputSource::Hdmi1));
491        // Test `ascii_case_insensitive`.
492        assert_eq!(InputSource::from_str("hdmi1"), Ok(InputSource::Hdmi1));
493        assert_eq!(InputSource::from_str("HDMI1"), Ok(InputSource::Hdmi1));
494        // Test `serialize`.
495        assert_eq!(InputSource::from_str("DP1"), Ok(InputSource::DisplayPort1));
496        assert_eq!(InputSource::from_str("dp2"), Ok(InputSource::DisplayPort2));
497        // Test failures.
498        assert!(InputSource::from_str("xyz").is_err());
499    }
500
501    #[test]
502    fn cli_parse() {
503        let mut cli = Cli::parse_from([""]);
504        assert!(!cli.verbose);
505        assert_eq!(cli.args.len(), 0);
506
507        cli = Cli::parse_from(["", "abc", "def"]);
508        assert!(!cli.verbose);
509        assert_eq!(cli.args, ["abc", "def"]);
510
511        cli = Cli::parse_from(["", "-v", "abc", "def"]);
512        assert!(cli.verbose);
513        assert_eq!(cli.args, ["abc", "def"]);
514    }
515
516    #[test]
517    fn cli_parse_option_after_positional() {
518        let cli = Cli::parse_from(["", "abc", "def", "-v"]);
519        assert!(cli.verbose);
520        assert_eq!(cli.args, ["abc", "def"]);
521    }
522
523    #[test]
524    fn cli_parse_positional_with_hyphen() {
525        let cli = Cli::parse_from(["", "--", "-abc", "-def"]);
526        assert_eq!(cli.args, ["-abc", "-def"]);
527    }
528
529    fn matches<'a>(re: &'a Regex, input: &'a str) -> Vec<&'a str> {
530        re.captures(input)
531            .unwrap()
532            .iter()
533            .skip(1)
534            .map(|m| m.unwrap().as_str())
535            .collect()
536    }
537
538    #[test]
539    fn re_set() {
540        let re_set = Regex::new(Cli::RE_SET_PATTERN).unwrap();
541        assert_eq!(re_set.is_match("a"), false);
542        assert_eq!(re_set.is_match("a="), false);
543        assert_eq!(re_set.is_match("=a"), false);
544        assert_eq!(matches(&re_set, "a=b"), vec!["a", "b"]);
545        assert_eq!(matches(&re_set, "1=23"), vec!["1", "23"]);
546        assert_eq!(matches(&re_set, "12=34"), vec!["12", "34"]);
547        assert_eq!(matches(&re_set, "12=3,4"), vec!["12", "3,4"]);
548    }
549
550    #[test]
551    fn compute_toggle_set_index() {
552        assert_eq!(Cli::compute_toggle_set_index(1, &[1, 4, 9]), 1);
553        assert_eq!(Cli::compute_toggle_set_index(4, &[1, 4, 9]), 2);
554        assert_eq!(Cli::compute_toggle_set_index(9, &[1, 4, 9]), 3);
555        // The result should be 0 if the `value` isn't in the list.
556        assert_eq!(Cli::compute_toggle_set_index(0, &[1, 4, 9]), 0);
557        assert_eq!(Cli::compute_toggle_set_index(2, &[1, 4, 9]), 0);
558        assert_eq!(Cli::compute_toggle_set_index(10, &[1, 4, 9]), 0);
559    }
560}