From 3a26e56a595f45f64ed008996b820fb25d4e44fb Mon Sep 17 00:00:00 2001 From: Haim Gelfenbeyn Date: Sun, 27 Sep 2020 17:15:07 -0400 Subject: [PATCH] Support per-monitor connect/disconnect configuration. Closes #3 --- README.md | 21 ++++++ src/app.rs | 10 +-- src/configuration.rs | 155 ++++++++++++++++++++++++++++++++++++++--- src/display_control.rs | 38 +++++++--- 4 files changed, 196 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index bfcba1c..f67c194 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,27 @@ The optional `on_usb_disconnect` settings allows to switch in the other directio Note that the preferred way is to have this app installed on both computers. Switching "away" is problematic: if the other computer has put the monitors to sleep, they will switch immediately back to the original input. +### Different inputs on different monitors +`display-switch` supports per-monitor configuration: add one or more monitor-specific configuration sections to set +monitor-specific inputs. For example: + +```ini +on_usb_connect = "DisplayPort2" +on_usb_disconnect = "Hdmi1" + +[monitor1] +monitor_id = "len" +on_usb_connect = "DisplayPort1" + +[monitor2] +monitor_id = "dell" +on_usb_connect = "hdmi2" +``` + +`monitor_id` specifies a case-insensitive substring to match against the monitor ID. For example, 'len' would match +`LEN P27u-10 S/N 1144206897` monitor ID. If more than one section has a match, a first one will be used. +`on_usb_connect` and `on_usb_disconnect`, if defined, take precedence over global defaults. + ### USB Device IDs To locate the ID of your USB device ID on Windows: 1. Open Device Manager diff --git a/src/app.rs b/src/app.rs index f5497b7..a43acf6 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,7 +3,7 @@ // This code is licensed under MIT license (see LICENSE.txt for details) // -use crate::configuration::Configuration; +use crate::configuration::{Configuration, SwitchDirection}; use crate::display_control; use crate::logging; use crate::platform::{wake_displays, PnPDetect}; @@ -22,9 +22,7 @@ impl usb::UsbCallback for App { std::thread::spawn(|| { wake_displays().map_err(|err| error!("{:?}", err)); }); - if let Some(input) = self.config.on_usb_connect { - display_control::switch_to(input); - } + display_control::switch(&self.config, SwitchDirection::Connect); } } @@ -32,9 +30,7 @@ impl usb::UsbCallback for App { debug!("Detected device change. Removed device: {:?}", device_id); if device_id == self.config.usb_device { info!("Monitored device is ({:?}) is disconnected", &self.config.usb_device); - if let Some(input) = self.config.on_usb_disconnect { - display_control::switch_to(input); - } + display_control::switch(&self.config, SwitchDirection::Disconnect); } } } diff --git a/src/configuration.rs b/src/configuration.rs index b813451..4019b3d 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -6,14 +6,72 @@ use crate::input_source::InputSource; use anyhow::{anyhow, Result}; use serde::{Deserialize, Deserializer}; +use std::fmt; + +#[derive(Debug, Copy, Clone)] +pub enum SwitchDirection { + Connect, + Disconnect, +} + +#[derive(Debug, Deserialize, Copy, Clone)] +pub struct InputSources { + // Note: Serde alias won't work here, because of https://github.com/serde-rs/serde/issues/1504 + // So cannot alias "on_usb_connect" to "monitor_input" + pub on_usb_connect: Option, + pub on_usb_disconnect: Option, +} + +#[derive(Debug, Deserialize)] +struct PerMonitorConfiguration { + monitor_id: String, + #[serde(flatten)] + input_sources: InputSources, +} #[derive(Debug, Deserialize)] pub struct Configuration { #[serde(deserialize_with = "Configuration::deserialize_usb_device")] pub usb_device: String, - #[serde(alias = "monitor_input")] - pub on_usb_connect: Option, - pub on_usb_disconnect: Option, + #[serde(flatten)] + input_sources: InputSources, + monitor1: Option, + monitor2: Option, + monitor3: Option, + monitor4: Option, + monitor5: Option, + monitor6: Option, +} + +impl fmt::Display for SwitchDirection { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Connect => write!(f, "connect"), + Self::Disconnect => write!(f, "disconnect"), + } + } +} + +impl PerMonitorConfiguration { + fn matches(&self, monitor_id: &str) -> bool { + monitor_id.to_lowercase().contains(&self.monitor_id.to_lowercase()) + } +} + +impl InputSources { + fn merge(&self, default: &Self) -> Self { + Self { + on_usb_connect: self.on_usb_connect.or(default.on_usb_connect), + on_usb_disconnect: self.on_usb_disconnect.or(default.on_usb_disconnect), + } + } + + pub fn source(&self, direction: SwitchDirection) -> Option { + match direction { + SwitchDirection::Connect => self.on_usb_connect, + SwitchDirection::Disconnect => self.on_usb_disconnect, + } + } } impl Configuration { @@ -63,6 +121,28 @@ impl Configuration { std::fs::create_dir_all(&log_dir)?; Ok(log_dir.join("display-switch.log")) } + + pub fn configuration_for_monitor(&self, monitor_id: &str) -> InputSources { + // Find a matching per-monitor config, if there is any + let per_monitor_config = [ + &self.monitor1, + &self.monitor2, + &self.monitor3, + &self.monitor4, + &self.monitor5, + &self.monitor6, + ] + .iter() + .find_map(|config| { + config + .as_ref() + .and_then(|config| if config.matches(monitor_id) { Some(config) } else { None }) + }); + // Merge global config as needed + per_monitor_config.map_or(self.input_sources, |config| { + config.input_sources.merge(&self.input_sources) + }) + } } #[cfg(test)] @@ -89,7 +169,7 @@ mod tests { let config = load_test_config( r#" usb_device = "dead:BEEF" - monitor_input = "DisplayPort2" + on_usb_connect = "DisplayPort2" "#, ) .unwrap(); @@ -106,8 +186,8 @@ mod tests { "#, ) .unwrap(); - assert_eq!(config.on_usb_connect.unwrap().value(), 0x10); - assert_eq!(config.on_usb_disconnect.unwrap().value(), 0x0f); + assert_eq!(config.input_sources.on_usb_connect.unwrap().value(), 0x10); + assert_eq!(config.input_sources.on_usb_disconnect.unwrap().value(), 0x0f); } #[test] @@ -115,13 +195,13 @@ mod tests { let config = load_test_config( r#" usb_device = "dead:BEEF" - monitor_input = 22 + on_usb_connect = 22 on_usb_disconnect = 33 "#, ) .unwrap(); - assert_eq!(config.on_usb_connect.unwrap().value(), 22); - assert_eq!(config.on_usb_disconnect.unwrap().value(), 33); + assert_eq!(config.input_sources.on_usb_connect.unwrap().value(), 22); + assert_eq!(config.input_sources.on_usb_disconnect.unwrap().value(), 33); } #[test] @@ -134,7 +214,60 @@ mod tests { "#, ) .unwrap(); - assert_eq!(config.on_usb_connect.unwrap().value(), 0x10); - assert_eq!(config.on_usb_disconnect.unwrap().value(), 0x20); + assert_eq!(config.input_sources.on_usb_connect.unwrap().value(), 0x10); + assert_eq!(config.input_sources.on_usb_disconnect.unwrap().value(), 0x20); + } + + #[test] + fn test_per_monitor_config() { + let config = load_test_config( + r#" + usb_device = "dead:BEEF" + on_usb_connect = "0x10" + on_usb_disconnect = "0x20" + + [monitor1] + monitor_id = 123 + on_usb_connect = 0x11 + + [monitor2] + monitor_id = 45 + on_usb_connect = 0x12 + on_usb_disconnect = 0x13 + "#, + ) + .unwrap(); + + // When no specific monitor matches, use the global defaults + assert_eq!( + config.configuration_for_monitor("333").on_usb_connect.unwrap().value(), + 0x10 + ); + // Matches monitor #1, and it should use its "on-connect" and global "on-disconnect" + assert_eq!( + config.configuration_for_monitor("1234").on_usb_connect.unwrap().value(), + 0x11 + ); + assert_eq!( + config + .configuration_for_monitor("1234") + .on_usb_disconnect + .unwrap() + .value(), + 0x20 + ); + // Matches monitor #2, and it should use its "on-connect" and "on-disconnect" values + assert_eq!( + config.configuration_for_monitor("2345").on_usb_connect.unwrap().value(), + 0x12 + ); + assert_eq!( + config + .configuration_for_monitor("2345") + .on_usb_disconnect + .unwrap() + .value(), + 0x13 + ); } } diff --git a/src/display_control.rs b/src/display_control.rs index 520222f..4870c85 100644 --- a/src/display_control.rs +++ b/src/display_control.rs @@ -3,6 +3,7 @@ // This code is licensed under MIT license (see LICENSE.txt for details) // +use crate::configuration::{Configuration, SwitchDirection}; use crate::input_source::InputSource; use ddc_hi::{Ddc, Display}; @@ -14,7 +15,12 @@ fn display_name(display: &Display) -> String { } pub fn log_current_source() { - for mut display in Display::enumerate() { + let displays = Display::enumerate(); + if displays.is_empty() { + error!("Did not detect any DDC-compatible displays!"); + return; + } + for mut display in displays { let display_name = display_name(&display); match display.handle.get_vcp_feature(INPUT_SELECT) { Ok(raw_source) => { @@ -28,17 +34,29 @@ pub fn log_current_source() { } } -pub fn switch_to(source: InputSource) { - for mut display in Display::enumerate() { +pub fn switch(config: &Configuration, switch_direction: SwitchDirection) { + let displays = Display::enumerate(); + if displays.is_empty() { + error!("Did not detect any DDC-compatible displays!"); + return; + } + for mut display in displays { let display_name = display_name(&display); - debug!("Setting display '{}' to {}", display_name, source); - match display.handle.set_vcp_feature(INPUT_SELECT, source.value()) { - Ok(_) => { - info!("Display {} set to {}", display_name, source); - } - Err(err) => { - error!("Failed to set display {} to {} ({:?})", display_name, source, err); + if let Some(input) = config.configuration_for_monitor(&display_name).source(switch_direction) { + debug!("Setting display {} to {}", display_name, input); + match display.handle.set_vcp_feature(INPUT_SELECT, input.value()) { + Ok(_) => { + info!("Display {} set to {}", display_name, input); + } + Err(err) => { + error!("Failed to set display {} to {} ({:?})", display_name, input, err); + } } + } else { + info!( + "Display {} is not configured to switch on USB {}", + display_name, switch_direction + ); } } }