use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::settings::{
CameraControls, GestureOwner, LightSettings, Lighting, ScrollResolution, SmartShift,
deserialize_gesture_owner, deserialize_optional_thumbwheel_sensitivity,
};
use crate::binding::{Action, ActionRingConfig, Binding, ButtonId, GestureDirection};
use crate::device::{Capabilities, DeviceKind, DeviceModelInfo, LightCapabilities};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DeviceIdentity {
pub display_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_info: Option<DeviceModelInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub codename: Option<String>,
pub kind: DeviceKind,
pub capabilities: Capabilities,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub light_capabilities: Option<LightCapabilities>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub driver_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry_model_id: Option<String>,
}
impl DeviceIdentity {
#[must_use]
pub fn without_unit_identifiers(mut self) -> Self {
if let Some(model) = &mut self.model_info {
model.serial_number = None;
model.unit_id = [0; 4];
}
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(from = "RawDeviceConfig")]
pub struct DeviceConfig {
#[serde(default = "default_true", skip_serializing_if = "is_true")]
pub enabled: bool,
#[serde(skip_serializing)]
pub(super) gesture_owner: Option<GestureOwner>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identity: Option<DeviceIdentity>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub bindings: BTreeMap<ButtonId, Binding>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub disabled_gestures: BTreeMap<ButtonId, BTreeMap<GestureDirection, Action>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub per_app_bindings: BTreeMap<String, BTreeMap<ButtonId, Action>>,
#[serde(default, skip_serializing_if = "ActionRingConfig::is_default")]
pub action_ring: ActionRingConfig,
#[serde(
default,
deserialize_with = "deserialize_dpi_presets",
skip_serializing_if = "Vec::is_empty"
)]
pub dpi_presets: Vec<u32>,
#[serde(
default,
deserialize_with = "deserialize_optional_dpi",
skip_serializing_if = "Option::is_none"
)]
pub dpi: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lighting: Option<Lighting>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub light: Option<LightSettings>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub smartshift: Option<SmartShift>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub camera_controls: Option<CameraControls>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub camera_profiles: BTreeMap<String, CameraControls>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub camera_profile: Option<String>,
#[serde(
default,
deserialize_with = "deserialize_optional_thumbwheel_sensitivity",
skip_serializing_if = "Option::is_none"
)]
pub thumbwheel_sensitivity: Option<i32>,
#[serde(default, skip_serializing_if = "is_false")]
pub invert_scroll: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scroll_resolution: Option<ScrollResolution>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub host_switch_targets: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fn_lock: Option<bool>,
}
impl Default for DeviceConfig {
fn default() -> Self {
Self {
enabled: true,
gesture_owner: None,
identity: None,
bindings: BTreeMap::new(),
disabled_gestures: BTreeMap::new(),
per_app_bindings: BTreeMap::new(),
action_ring: ActionRingConfig::default(),
dpi_presets: Vec::new(),
dpi: None,
lighting: None,
light: None,
smartshift: None,
camera_controls: None,
camera_profiles: BTreeMap::new(),
camera_profile: None,
thumbwheel_sensitivity: None,
invert_scroll: false,
scroll_resolution: None,
host_switch_targets: Vec::new(),
fn_lock: None,
}
}
}
fn default_true() -> bool {
true
}
#[allow(
clippy::trivially_copy_pass_by_ref,
reason = "serde's skip_serializing_if requires a fn(&T) -> bool signature"
)]
fn is_true(b: &bool) -> bool {
*b
}
#[allow(
clippy::trivially_copy_pass_by_ref,
reason = "serde's skip_serializing_if requires a fn(&T) -> bool signature"
)]
fn is_false(b: &bool) -> bool {
!*b
}
fn deserialize_dpi_presets<'de, D>(deserializer: D) -> Result<Vec<u32>, D::Error>
where
D: serde::Deserializer<'de>,
{
let values = Vec::<u32>::deserialize(deserializer)?;
if let Some(value) = values.iter().find(|value| u16::try_from(**value).is_err()) {
return Err(serde::de::Error::custom(format_args!(
"DPI must fit the HID++ 16-bit range, got {value}"
)));
}
Ok(values)
}
fn deserialize_optional_dpi<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Option::<u32>::deserialize(deserializer)?;
if let Some(value) = value
&& u16::try_from(value).is_err()
{
return Err(serde::de::Error::custom(format_args!(
"DPI must fit the HID++ 16-bit range, got {value}"
)));
}
Ok(value)
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawDeviceConfig {
#[serde(default, deserialize_with = "deserialize_gesture_owner")]
gesture_owner: Option<GestureOwner>,
#[serde(default)]
identity: Option<DeviceIdentity>,
#[serde(default)]
bindings: BTreeMap<ButtonId, Binding>,
#[serde(default)]
disabled_gestures: BTreeMap<ButtonId, BTreeMap<GestureDirection, Action>>,
#[serde(default)]
button_bindings: BTreeMap<ButtonId, Action>,
#[serde(default)]
gesture_bindings: BTreeMap<GestureDirection, Action>,
#[serde(default)]
per_app_bindings: BTreeMap<String, BTreeMap<ButtonId, Action>>,
#[serde(default)]
action_ring: ActionRingConfig,
#[serde(default, deserialize_with = "deserialize_dpi_presets")]
dpi_presets: Vec<u32>,
#[serde(default, deserialize_with = "deserialize_optional_dpi")]
dpi: Option<u32>,
#[serde(default)]
lighting: Option<Lighting>,
#[serde(default)]
light: Option<LightSettings>,
#[serde(default)]
smartshift: Option<SmartShift>,
#[serde(default)]
camera_controls: Option<CameraControls>,
#[serde(default)]
camera_profiles: BTreeMap<String, CameraControls>,
#[serde(default)]
camera_profile: Option<String>,
#[serde(
default,
deserialize_with = "deserialize_optional_thumbwheel_sensitivity"
)]
thumbwheel_sensitivity: Option<i32>,
#[serde(default)]
invert_scroll: bool,
#[serde(default)]
scroll_resolution: Option<ScrollResolution>,
#[serde(default)]
host_switch_targets: Vec<String>,
#[serde(default)]
fn_lock: Option<bool>,
#[serde(default = "default_true")]
enabled: bool,
}
impl From<RawDeviceConfig> for DeviceConfig {
fn from(raw: RawDeviceConfig) -> Self {
let mut bindings = raw.bindings;
if !raw.gesture_bindings.is_empty() {
bindings
.entry(ButtonId::GestureButton)
.or_insert_with(|| Binding::Gesture(raw.gesture_bindings));
}
for (button, action) in raw.button_bindings {
if button == ButtonId::GestureButton {
continue;
}
bindings.entry(button).or_insert(Binding::Single(action));
}
DeviceConfig {
enabled: raw.enabled,
gesture_owner: raw.gesture_owner,
identity: raw.identity.map(DeviceIdentity::without_unit_identifiers),
bindings,
disabled_gestures: raw.disabled_gestures,
per_app_bindings: raw.per_app_bindings,
action_ring: raw.action_ring,
dpi_presets: raw.dpi_presets,
dpi: raw.dpi,
lighting: raw.lighting,
light: raw.light,
smartshift: raw.smartshift,
camera_controls: raw.camera_controls,
camera_profiles: raw.camera_profiles,
camera_profile: raw.camera_profile,
thumbwheel_sensitivity: raw.thumbwheel_sensitivity,
invert_scroll: raw.invert_scroll,
scroll_resolution: raw.scroll_resolution,
host_switch_targets: raw.host_switch_targets,
fn_lock: raw.fn_lock,
}
}
}
#[cfg(test)]
mod tests {
use super::DeviceConfig;
#[test]
fn host_switch_targets_round_trip_as_physical_keys() -> Result<(), Box<dyn std::error::Error>> {
let config: DeviceConfig = toml::from_str(
r#"host_switch_targets = [
"receiver:keyboard:slot:1",
"receiver:mouse:slot:2",
]"#,
)?;
assert_eq!(
config.host_switch_targets,
["receiver:keyboard:slot:1", "receiver:mouse:slot:2"]
);
let serialized = toml::to_string(&config)?;
assert!(serialized.contains("host_switch_targets"));
Ok(())
}
}