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(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 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 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
50const INPUT_SELECT: FeatureCode = 0x60;
52
53static mut DRY_RUN: bool = false;
54
55pub 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 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 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 pub fn set_dry_run(value: bool) {
100 unsafe { DRY_RUN = value }
101 }
102
103 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 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 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 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 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 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 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)]
222pub struct Cli {
226 #[arg(skip)]
227 pub monitors: Vec<Monitor>,
230
231 #[arg(short, long)]
232 pub backend: Option<String>,
234
235 #[arg(id = "capabilities", short, long)]
236 pub needs_capabilities: bool,
238
239 #[arg(short = 'n', long)]
240 pub dry_run: bool,
242
243 #[arg(short, long)]
244 pub verbose: bool,
246
247 #[arg(skip)]
248 set_index: Option<usize>,
249
250 pub args: Vec<String>,
254}
255
256impl Cli {
257 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 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 .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 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 assert_eq!(InputSource::from_str("hdmi1"), Ok(InputSource::Hdmi1));
411 assert_eq!(InputSource::from_str("HDMI1"), Ok(InputSource::Hdmi1));
412 assert_eq!(InputSource::from_str("DP1"), Ok(InputSource::DisplayPort1));
414 assert_eq!(InputSource::from_str("dp2"), Ok(InputSource::DisplayPort2));
415 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 assert_eq!(
424 InputSource::raw_from_str("Hdmi1").unwrap(),
425 InputSource::Hdmi1 as InputSourceRaw
426 );
427 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 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}