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