#[cfg(test)]
use crate::binding::{Action, ButtonId};
use crate::config::{Config, DeviceConfig};
#[cfg(test)]
use crate::config::{LightSettings, Lighting, LinkConfig};
use crate::device::Capabilities;
use crate::device_order::{DeviceIdentity, DeviceStableId, PhysicalDeviceKey};
#[cfg(test)]
use crate::hid::Dpi;
impl DeviceIdentity {
#[must_use]
pub fn config_key(&self) -> Option<String> {
self.is_physical().then(|| self.key())
}
}
#[must_use]
pub fn canonical_device_key(
stable: &DeviceStableId,
identity: Option<&DeviceIdentity>,
) -> Option<PhysicalDeviceKey> {
identity
.and_then(DeviceIdentity::config_key)
.and_then(|key| PhysicalDeviceKey::parse(&key))
.or_else(|| stable.physical_key())
}
impl Config {
#[must_use]
pub fn resolve_device_key(
&self,
stable: &DeviceStableId,
identity: Option<&DeviceIdentity>,
) -> Option<PhysicalDeviceKey> {
if let Some(key) = identity
.and_then(DeviceIdentity::config_key)
.and_then(|key| PhysicalDeviceKey::parse(&key))
{
let holds_settings = |key: &str| {
self.devices
.get(key)
.is_some_and(DeviceConfig::holds_settings)
};
let legacy = stable
.physical_key()
.filter(|legacy| legacy.as_str() != key.as_str());
if let Some(legacy) = legacy
&& !holds_settings(key.as_str())
&& holds_settings(legacy.as_str())
{
return Some(legacy);
}
return Some(key);
}
let route = stable.route_key();
if let Some(key) = self
.devices
.iter()
.find(|(_, device)| device.links.contains_key(&route))
.map(|(key, _)| key.as_str())
.and_then(PhysicalDeviceKey::parse)
{
return Some(key);
}
stable.physical_key()
}
pub fn adopt_route(
&mut self,
canonical: &PhysicalDeviceKey,
route_key: &str,
capabilities: Option<Capabilities>,
) -> bool {
let mut changed = false;
for (key, device) in &mut self.devices {
if key != canonical.as_str() && device.links.remove(route_key).is_some() {
changed = true;
}
}
let legacy = (route_key != canonical.as_str())
.then(|| self.devices.remove(route_key))
.flatten();
let device = self
.devices
.entry(canonical.as_str().to_string())
.or_default();
if !device.links.contains_key(route_key) {
changed = true;
}
let link = device.links.entry(route_key.to_string()).or_default();
if capabilities.is_some() && link.capabilities != capabilities {
link.capabilities = capabilities;
changed = true;
}
let Some(legacy) = legacy else {
return changed;
};
fold(device, legacy, route_key);
self.repoint_references(route_key, canonical.as_str());
true
}
fn repoint_references(&mut self, old: &str, new: &str) {
if self.selected_device.as_deref() == Some(old) {
self.selected_device = Some(new.to_string());
}
for device in self.devices.values_mut() {
for target in &mut device.host_switch_targets {
if target == old {
*target = new.to_string();
}
}
}
}
}
pub(super) fn fold(device: &mut DeviceConfig, mut legacy: DeviceConfig, route_key: &str) {
for (key, value) in std::mem::take(&mut legacy.links) {
device.links.entry(key).or_insert(value);
}
fold_maps(device, &mut legacy, route_key);
let link = device.links.entry(route_key.to_string()).or_default();
if let Some(capabilities) = legacy
.identity
.as_ref()
.map(|identity| identity.capabilities)
{
link.capabilities.get_or_insert(capabilities);
}
macro_rules! fold_field {
($field:ident) => {
match (&device.$field, legacy.$field) {
(None, Some(value)) => device.$field = Some(value),
(Some(ours), Some(theirs)) if *ours != theirs => {
link.overrides.$field = Some(theirs);
}
_ => {}
}
};
}
fold_field!(dpi);
fold_field!(scroll_resolution);
fold_field!(lighting);
fold_field!(smartshift);
if device.invert_scroll != legacy.invert_scroll && legacy.invert_scroll {
link.overrides.invert_scroll = Some(legacy.invert_scroll);
}
macro_rules! fold_option_field {
($field:ident) => {
match (&device.$field, legacy.$field) {
(None, Some(value)) => device.$field = Some(value),
(Some(ours), Some(theirs)) if *ours != theirs => {
tracing::warn!(
%route_key,
field = stringify!($field),
"value differs between merged entries; keeping the canonical one"
);
}
_ => {}
}
};
}
fold_option_field!(light);
fold_option_field!(camera_controls);
fold_option_field!(camera_profile);
fold_option_field!(thumbwheel_sensitivity);
fold_option_field!(fn_lock);
fold_option_field!(custom_name);
if device.identity.is_none() {
device.identity = legacy.identity.take();
}
macro_rules! fold_if_empty {
($field:ident) => {
if device.$field.is_empty() {
device.$field = legacy.$field;
} else if !legacy.$field.is_empty() && device.$field != legacy.$field {
tracing::warn!(
%route_key,
field = stringify!($field),
"list differs between merged entries; keeping the canonical one"
);
}
};
}
fold_if_empty!(dpi_presets);
fold_if_empty!(host_switch_targets);
if device.action_ring.is_default() {
device.action_ring = legacy.action_ring.clone();
} else if !legacy.action_ring.is_default() && device.action_ring != legacy.action_ring {
tracing::warn!(
%route_key,
field = "action_ring",
"ring differs between merged entries; keeping the canonical one"
);
}
if device.enabled && !legacy.enabled {
device.enabled = false;
}
}
fn fold_maps(device: &mut DeviceConfig, legacy: &mut DeviceConfig, route_key: &str) {
macro_rules! fold_map_field {
($field:ident) => {
for (key, value) in std::mem::take(&mut legacy.$field) {
if let Some(existing) = device.$field.get(&key) {
if *existing != value {
tracing::warn!(
%route_key,
field = stringify!($field),
key = ?key,
"entry differs between merged entries; keeping the canonical one"
);
}
} else {
device.$field.insert(key, value);
}
}
};
}
fold_map_field!(bindings);
fold_map_field!(disabled_gestures);
fold_map_field!(per_app_bindings);
fold_map_field!(camera_profiles);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::device_order::{DeviceIdentity, DeviceStableId};
fn cabled() -> DeviceStableId {
DeviceStableId::Direct {
vendor_id: 0x046d,
product_id: 0xc08d,
identity: DeviceIdentity::Unit([0x6b, 0xe9, 0xd3, 0x00]),
}
}
fn on_receiver() -> DeviceStableId {
DeviceStableId::Bolt {
receiver_uid: "82839805".to_string(),
slot: 1,
}
}
#[test]
fn a_known_unit_keys_the_entry_directly() {
let config = Config::default();
let unit = DeviceIdentity::Unit([0x6b, 0xe9, 0xd3, 0x00]);
let key = config
.resolve_device_key(&cabled(), Some(&unit))
.expect("a non-zero unit is a physical identity");
assert_eq!(key.as_str(), "unit:6be9d300");
}
#[test]
fn the_same_unit_resolves_alike_on_either_route() {
let config = Config::default();
let unit = DeviceIdentity::Unit([0x6b, 0xe9, 0xd3, 0x00]);
assert_eq!(
config.resolve_device_key(&cabled(), Some(&unit)),
config.resolve_device_key(&on_receiver(), Some(&unit)),
);
}
fn litra() -> DeviceStableId {
DeviceStableId::RawHid {
vendor_id: 0x046d,
product_id: 0xc901,
usage_page: 0xff43,
usage_id: 0x0202,
identity: "serial:beam-1".to_string(),
}
}
#[test]
fn settings_still_under_the_pre_upgrade_key_are_read_from_it() {
let mut config = Config::default();
config.devices.insert(
"receiver:82839805:slot:1".to_string(),
DeviceConfig {
dpi: Some(Dpi::new(3200)),
..DeviceConfig::default()
},
);
let unit = DeviceIdentity::Unit([0x6b, 0xe9, 0xd3, 0x00]);
let key = config
.resolve_device_key(&on_receiver(), Some(&unit))
.expect("resolves");
assert_eq!(key.as_str(), "receiver:82839805:slot:1");
assert_eq!(
config.devices[key.as_str()].dpi,
Some(Dpi::new(3200)),
"the settings are where the key says they are"
);
}
#[test]
fn a_standalone_raw_entry_keeps_its_light_settings() {
let mut config = Config::default();
config.devices.insert(
"raw:046d:c901:ff43:0202:serial:beam-1".to_string(),
DeviceConfig {
light: Some(LightSettings::default()),
..DeviceConfig::default()
},
);
let serial = DeviceIdentity::Serial("beam-1".to_string());
let key = config
.resolve_device_key(&litra(), Some(&serial))
.expect("resolves");
assert_eq!(key.as_str(), "raw:046d:c901:ff43:0202:serial:beam-1");
}
#[test]
fn a_canonical_entry_holding_only_metadata_does_not_shadow_the_legacy_one() {
let mut config = Config::default();
config.devices.insert(
"receiver:82839805:slot:1".to_string(),
DeviceConfig {
dpi: Some(Dpi::new(3200)),
..DeviceConfig::default()
},
);
let mut bare = DeviceConfig::default();
bare.links.insert(
"receiver:82839805:slot:1".to_string(),
LinkConfig::default(),
);
config.devices.insert("unit:6be9d300".to_string(), bare);
let unit = DeviceIdentity::Unit([0x6b, 0xe9, 0xd3, 0x00]);
let key = config
.resolve_device_key(&on_receiver(), Some(&unit))
.expect("resolves");
assert_eq!(key.as_str(), "receiver:82839805:slot:1");
}
#[test]
fn a_canonical_entry_with_settings_wins_over_a_legacy_one() {
let mut config = Config::default();
config.devices.insert(
"receiver:82839805:slot:1".to_string(),
DeviceConfig {
dpi: Some(Dpi::new(3200)),
..DeviceConfig::default()
},
);
config.devices.insert(
"unit:6be9d300".to_string(),
DeviceConfig {
dpi: Some(Dpi::new(800)),
..DeviceConfig::default()
},
);
let unit = DeviceIdentity::Unit([0x6b, 0xe9, 0xd3, 0x00]);
let key = config
.resolve_device_key(&on_receiver(), Some(&unit))
.expect("resolves");
assert_eq!(key.as_str(), "unit:6be9d300");
}
#[test]
fn the_route_key_is_the_legacy_entry_key_for_every_variant_but_direct() {
for stable in [on_receiver(), litra()] {
let physical = stable.physical_key().expect("physical");
assert_eq!(
stable.route_key(),
physical.as_str(),
"{stable:?}: the route key must find the legacy entry"
);
}
let unknown = DeviceStableId::Unknown {
slot: 2,
identity: DeviceIdentity::Unit([0x6b, 0xe9, 0xd3, 0x00]),
};
assert_eq!(
unknown.route_key(),
unknown.physical_key().expect("physical").as_str()
);
let direct = cabled();
assert_ne!(
direct.route_key(),
direct.physical_key().expect("physical").as_str(),
"a direct route key drops the identity that keys the entry"
);
}
#[test]
fn adoption_repoints_the_selection_and_any_host_switch_target() {
let mut config = Config::default();
config.devices.insert(
"receiver:82839805:slot:1".to_string(),
DeviceConfig {
dpi: Some(Dpi::new(800)),
..DeviceConfig::default()
},
);
config.devices.insert(
"receiver:82839805:slot:2".to_string(),
DeviceConfig {
host_switch_targets: vec!["receiver:82839805:slot:1".to_string()],
..DeviceConfig::default()
},
);
config.selected_device = Some("receiver:82839805:slot:1".to_string());
let canonical = PhysicalDeviceKey::parse("unit:6be9d300").expect("valid");
assert!(config.adopt_route(&canonical, "receiver:82839805:slot:1", None));
assert_eq!(config.selected_device.as_deref(), Some("unit:6be9d300"));
assert_eq!(
config.devices["receiver:82839805:slot:2"].host_switch_targets,
vec!["unit:6be9d300".to_string()],
"the keyboard still switches the mouse it was linked to"
);
}
#[test]
fn an_asleep_device_resolves_through_its_indexed_route() {
let mut config = Config::default();
let mut device = DeviceConfig::default();
device.links.insert(
"receiver:82839805:slot:1".to_string(),
LinkConfig::default(),
);
config.devices.insert("unit:6be9d300".to_string(), device);
let key = config
.resolve_device_key(&on_receiver(), None)
.expect("the indexed route resolves");
assert_eq!(key.as_str(), "unit:6be9d300");
}
#[test]
fn an_unindexed_route_falls_back_to_todays_key() {
let config = Config::default();
let key = config
.resolve_device_key(&on_receiver(), None)
.expect("receiver keys are physical");
assert_eq!(key.as_str(), "receiver:82839805:slot:1");
}
#[test]
fn an_all_zero_unit_over_a_receiver_keeps_its_route_key() {
let config = Config::default();
let zero = DeviceIdentity::Unit([0; 4]);
let key = config
.resolve_device_key(&on_receiver(), Some(&zero))
.expect("falls back rather than failing");
assert_eq!(key.as_str(), "receiver:82839805:slot:1");
}
#[test]
fn a_direct_device_with_no_identity_stays_non_persistent() {
let config = Config::default();
let anonymous = DeviceStableId::Direct {
vendor_id: 0x046d,
product_id: 0xc08d,
identity: DeviceIdentity::Unit([0; 4]),
};
assert_eq!(config.resolve_device_key(&anonymous, None), None);
}
#[test]
fn folding_takes_a_custom_name_the_canonical_entry_lacks() {
let mut config = Config::default();
let legacy = DeviceConfig {
custom_name: Some("Desk mouse".to_string()),
..DeviceConfig::default()
};
config
.devices
.insert("receiver:82839805:slot:1".to_string(), legacy);
config
.devices
.insert("unit:6be9d300".to_string(), DeviceConfig::default());
let canonical = PhysicalDeviceKey::parse("unit:6be9d300").expect("valid");
assert!(config.adopt_route(&canonical, "receiver:82839805:slot:1", None));
assert_eq!(
config.devices["unit:6be9d300"].custom_name.as_deref(),
Some("Desk mouse"),
"the user's alias survives the fold"
);
}
#[test]
fn folding_keeps_the_canonical_custom_name_when_both_are_named() {
let mut config = Config::default();
let legacy = DeviceConfig {
custom_name: Some("Old name".to_string()),
..DeviceConfig::default()
};
let canonical_entry = DeviceConfig {
custom_name: Some("Current name".to_string()),
..DeviceConfig::default()
};
config
.devices
.insert("receiver:82839805:slot:1".to_string(), legacy);
config
.devices
.insert("unit:6be9d300".to_string(), canonical_entry);
let canonical = PhysicalDeviceKey::parse("unit:6be9d300").expect("valid");
assert!(config.adopt_route(&canonical, "receiver:82839805:slot:1", None));
assert_eq!(
config.devices["unit:6be9d300"].custom_name.as_deref(),
Some("Current name"),
"the canonical alias wins a genuine conflict"
);
}
#[test]
fn adopting_a_route_folds_the_legacy_entry_in() {
let mut config = Config::default();
let legacy = DeviceConfig {
lighting: Some(Lighting::default()),
..DeviceConfig::default()
};
config
.devices
.insert("receiver:82839805:slot:1".to_string(), legacy);
config
.devices
.insert("unit:6be9d300".to_string(), DeviceConfig::default());
let canonical = PhysicalDeviceKey::parse("unit:6be9d300").expect("valid");
assert!(config.adopt_route(&canonical, "receiver:82839805:slot:1", None));
let device = &config.devices["unit:6be9d300"];
assert!(device.lighting.is_some(), "lighting moved to device level");
assert!(device.links.contains_key("receiver:82839805:slot:1"));
assert!(
!config.devices.contains_key("receiver:82839805:slot:1"),
"the legacy entry is consumed, not left behind"
);
}
#[test]
fn a_sighting_records_what_the_link_measured() {
let mut config = Config::default();
let canonical = PhysicalDeviceKey::parse("unit:6be9d300").expect("valid");
let wired = Capabilities {
hires_wheel: false,
..Capabilities::default()
};
let wireless = Capabilities {
hires_wheel: true,
..Capabilities::default()
};
assert!(config.adopt_route(&canonical, "direct:046d:c08d", Some(wired)));
assert!(config.adopt_route(&canonical, "receiver:82839805:slot:1", Some(wireless)));
let links = &config.devices["unit:6be9d300"].links;
assert_eq!(links["direct:046d:c08d"].capabilities, Some(wired));
assert_eq!(
links["receiver:82839805:slot:1"].capabilities,
Some(wireless)
);
assert!(!config.adopt_route(&canonical, "direct:046d:c08d", Some(wired)));
assert!(!config.adopt_route(&canonical, "direct:046d:c08d", None));
assert_eq!(
config.devices["unit:6be9d300"].links["direct:046d:c08d"].capabilities,
Some(wired)
);
}
#[test]
fn a_conflicting_value_becomes_a_per_link_override() {
let mut config = Config::default();
let legacy = DeviceConfig {
dpi: Some(Dpi::new(800)),
..DeviceConfig::default()
};
config
.devices
.insert("receiver:82839805:slot:1".to_string(), legacy);
let canonical_entry = DeviceConfig {
dpi: Some(Dpi::new(1600)),
..DeviceConfig::default()
};
config
.devices
.insert("unit:6be9d300".to_string(), canonical_entry);
let canonical = PhysicalDeviceKey::parse("unit:6be9d300").expect("valid");
config.adopt_route(&canonical, "receiver:82839805:slot:1", None);
let device = &config.devices["unit:6be9d300"];
assert_eq!(device.dpi, Some(Dpi::new(1600)), "canonical stays default");
assert_eq!(
device.links["receiver:82839805:slot:1"].overrides.dpi,
Some(Dpi::new(800)),
"the legacy value survives on its own link"
);
}
#[test]
fn adopting_an_unseen_route_registers_it_and_reports_a_change() {
let mut config = Config::default();
config
.devices
.insert("unit:6be9d300".to_string(), DeviceConfig::default());
let canonical = PhysicalDeviceKey::parse("unit:6be9d300").expect("valid");
assert!(config.adopt_route(&canonical, "direct:046d:c08d", None));
assert!(
config.devices["unit:6be9d300"]
.links
.contains_key("direct:046d:c08d")
);
}
#[test]
fn adopting_an_already_indexed_route_reports_no_change() {
let mut config = Config::default();
let mut device = DeviceConfig::default();
device
.links
.insert("direct:046d:c08d".to_string(), LinkConfig::default());
config.devices.insert("unit:6be9d300".to_string(), device);
let canonical = PhysicalDeviceKey::parse("unit:6be9d300").expect("valid");
assert!(!config.adopt_route(&canonical, "direct:046d:c08d", None));
}
#[test]
fn a_re_paired_slot_points_at_the_new_unit() {
let mut config = Config::default();
let mut first = DeviceConfig::default();
first.links.insert(
"receiver:82839805:slot:1".to_string(),
LinkConfig::default(),
);
config.devices.insert("unit:aaaaaaaa".to_string(), first);
config
.devices
.insert("unit:bbbbbbbb".to_string(), DeviceConfig::default());
let second = PhysicalDeviceKey::parse("unit:bbbbbbbb").expect("valid");
config.adopt_route(&second, "receiver:82839805:slot:1", None);
assert!(
!config.devices["unit:aaaaaaaa"]
.links
.contains_key("receiver:82839805:slot:1"),
"the route no longer points at the previous unit"
);
assert!(
config.devices["unit:bbbbbbbb"]
.links
.contains_key("receiver:82839805:slot:1")
);
}
#[test]
fn per_app_bindings_merge_key_by_key() {
let mut config = Config::default();
let legacy = DeviceConfig {
per_app_bindings: std::collections::BTreeMap::from([(
"com.example.App".to_string(),
std::collections::BTreeMap::from([(ButtonId::MiddleClick, Action::Copy)]),
)]),
..DeviceConfig::default()
};
config
.devices
.insert("receiver:82839805:slot:1".to_string(), legacy);
config
.devices
.insert("unit:6be9d300".to_string(), DeviceConfig::default());
let canonical = PhysicalDeviceKey::parse("unit:6be9d300").expect("valid");
config.adopt_route(&canonical, "receiver:82839805:slot:1", None);
let device = &config.devices["unit:6be9d300"];
assert_eq!(
device.per_app_bindings.get("com.example.App"),
Some(&std::collections::BTreeMap::from([(
ButtonId::MiddleClick,
Action::Copy
)])),
"the app overlay survives adoption"
);
}
#[test]
fn light_is_taken_when_the_canonical_side_has_none() {
let mut config = Config::default();
let legacy = DeviceConfig {
light: Some(LightSettings::default()),
..DeviceConfig::default()
};
config
.devices
.insert("receiver:82839805:slot:1".to_string(), legacy);
config
.devices
.insert("unit:6be9d300".to_string(), DeviceConfig::default());
let canonical = PhysicalDeviceKey::parse("unit:6be9d300").expect("valid");
config.adopt_route(&canonical, "receiver:82839805:slot:1", None);
assert_eq!(
config.devices["unit:6be9d300"].light,
Some(LightSettings::default()),
"the standalone-light setting is not silently discarded"
);
}
#[test]
fn a_legacy_disabled_flag_disables_the_canonical_entry() {
let mut config = Config::default();
let legacy = DeviceConfig {
enabled: false,
..DeviceConfig::default()
};
config
.devices
.insert("receiver:82839805:slot:1".to_string(), legacy);
config
.devices
.insert("unit:6be9d300".to_string(), DeviceConfig::default());
let canonical = PhysicalDeviceKey::parse("unit:6be9d300").expect("valid");
config.adopt_route(&canonical, "receiver:82839805:slot:1", None);
assert!(
!config.devices["unit:6be9d300"].enabled,
"the legacy opt-out survives adoption"
);
}
}