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 write!(f, "{:?}", self.ddc_hi_display.info)
114 }
115}
116
117impl Monitor {
118 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 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 pub fn set_dry_run(value: bool) {
144 unsafe { DRY_RUN = value }
145 }
146
147 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 if let Some(feature) = self.feature_descriptor(feature_code) {
182 return feature.code;
183 }
184 feature_code
185 }
186
187 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 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 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 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 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)]
270pub struct Cli {
295 #[arg(skip)]
296 pub monitors: Vec<Monitor>,
299
300 #[arg(short, long)]
301 pub backend: Option<String>,
303
304 #[arg(id = "capabilities", short, long)]
305 pub needs_capabilities: bool,
307
308 #[arg(short = 'n', long)]
309 pub dry_run: bool,
311
312 #[arg(short, long)]
313 pub verbose: bool,
315
316 #[arg(skip)]
317 set_index: Option<usize>,
318
319 pub args: Vec<String>,
323}
324
325impl Cli {
326 pub fn new() -> Self {
328 Cli {
329 monitors: Monitor::enumerate(),
330 ..Default::default()
331 }
332 }
333
334 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 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 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 .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 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 assert_eq!(InputSource::from_str("hdmi1"), Ok(InputSource::Hdmi1));
493 assert_eq!(InputSource::from_str("HDMI1"), Ok(InputSource::Hdmi1));
494 assert_eq!(InputSource::from_str("DP1"), Ok(InputSource::DisplayPort1));
496 assert_eq!(InputSource::from_str("dp2"), Ok(InputSource::DisplayPort2));
497 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 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}