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;
13
14#[derive(Copy, Clone, Debug, PartialEq, AsRefStr, EnumString, FromRepr)]
15#[repr(u8)]
16#[strum(ascii_case_insensitive)]
17pub 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 pub fn as_raw(self) -> InputSourceRaw {
37 self as InputSourceRaw
38 }
39
40 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 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
87const INPUT_SELECT: FeatureCode = 0x60;
89
90static mut DRY_RUN: bool = false;
91
92pub 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 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 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 pub fn set_dry_run(value: bool) {
146 unsafe { DRY_RUN = value }
147 }
148
149 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 if let Some(feature) = self.feature_descriptor(feature_code) {
184 return feature.code;
185 }
186 feature_code
187 }
188
189 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 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 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 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 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)]
267pub struct Cli {
292 #[arg(skip)]
293 pub monitors: Vec<Monitor>,
296
297 #[arg(short, long)]
298 pub backend: Option<String>,
300
301 #[arg(id = "capabilities", short, long)]
302 pub needs_capabilities: bool,
304
305 #[arg(short = 'n', long)]
306 pub dry_run: bool,
308
309 #[arg(short, long)]
310 pub verbose: bool,
312
313 #[arg(skip)]
314 set_index: Option<usize>,
315
316 pub args: Vec<String>,
320}
321
322impl Cli {
323 pub fn new() -> Self {
325 Cli {
326 monitors: Monitor::enumerate(),
327 ..Default::default()
328 }
329 }
330
331 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 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 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 .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 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 assert_eq!(InputSource::from_str("hdmi1"), Ok(InputSource::Hdmi1));
490 assert_eq!(InputSource::from_str("HDMI1"), Ok(InputSource::Hdmi1));
491 assert_eq!(InputSource::from_str("DP1"), Ok(InputSource::DisplayPort1));
493 assert_eq!(InputSource::from_str("dp2"), Ok(InputSource::DisplayPort2));
494 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 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}