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