Skip to content

Commit

Permalink
Support per-monitor connect/disconnect configuration.
Browse files Browse the repository at this point in the history
Closes #3
  • Loading branch information
haimgel committed Sep 27, 2020
1 parent 3530030 commit 3a26e56
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 28 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 3 additions & 7 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -22,19 +22,15 @@ 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);
}
}

fn device_removed(&self, device_id: &str) {
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);
}
}
}
Expand Down
155 changes: 144 additions & 11 deletions src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<InputSource>,
pub on_usb_disconnect: Option<InputSource>,
}

#[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<InputSource>,
pub on_usb_disconnect: Option<InputSource>,
#[serde(flatten)]
input_sources: InputSources,
monitor1: Option<PerMonitorConfiguration>,
monitor2: Option<PerMonitorConfiguration>,
monitor3: Option<PerMonitorConfiguration>,
monitor4: Option<PerMonitorConfiguration>,
monitor5: Option<PerMonitorConfiguration>,
monitor6: Option<PerMonitorConfiguration>,
}

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<InputSource> {
match direction {
SwitchDirection::Connect => self.on_usb_connect,
SwitchDirection::Disconnect => self.on_usb_disconnect,
}
}
}

impl Configuration {
Expand Down Expand Up @@ -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)]
Expand All @@ -89,7 +169,7 @@ mod tests {
let config = load_test_config(
r#"
usb_device = "dead:BEEF"
monitor_input = "DisplayPort2"
on_usb_connect = "DisplayPort2"
"#,
)
.unwrap();
Expand All @@ -106,22 +186,22 @@ 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]
fn test_decimal_input_deserialization() {
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]
Expand All @@ -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
);
}
}
38 changes: 28 additions & 10 deletions src/display_control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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) => {
Expand All @@ -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
);
}
}
}

0 comments on commit 3a26e56

Please sign in to comment.