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
44const 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 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)]
200pub struct Cli {
204 #[arg(skip)]
205 pub monitors: Vec<Monitor>,
206
207 #[arg(short, long)]
208 pub backend: Option<String>,
210
211 #[arg(id = "capabilities", short, long)]
212 pub needs_capabilities: bool,
214
215 #[arg(short = 'n', long)]
216 pub dry_run: bool,
218
219 #[arg(short, long)]
220 pub verbose: bool,
222
223 #[arg(skip)]
224 set_index: Option<usize>,
225
226 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 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 .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 assert_eq!(InputSource::from_str("hdmi1"), Ok(InputSource::Hdmi1));
381 assert_eq!(InputSource::from_str("HDMI1"), Ok(InputSource::Hdmi1));
382 assert_eq!(InputSource::from_str("DP1"), Ok(InputSource::DisplayPort1));
384 assert_eq!(InputSource::from_str("dp2"), Ok(InputSource::DisplayPort2));
385 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 assert_eq!(
394 InputSource::raw_from_str("Hdmi1").unwrap(),
395 InputSource::Hdmi1 as InputSourceRaw
396 );
397 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 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}