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