use std::{collections::BTreeMap, path::Path};
use serde::{Deserialize, Serialize};
mod device;
mod file;
mod key_trigger;
mod settings;
#[cfg(test)]
mod tests;
pub use device::{DeviceConfig, DeviceIdentity};
pub use file::{ConfigError, ConfigFile};
#[cfg(test)]
use file::{backup_existing_config, config_backup_path};
pub use key_trigger::{KeyModifiers, KeyTrigger, KeyboardConfig, ParseTriggerError};
pub use settings::LightSettings;
pub use settings::{
AppSettings, Appearance, AssetSourcePreference, CameraControls, DEFAULT_THUMBWHEEL_SENSITIVITY,
Lighting, MAX_THUMBWHEEL_SENSITIVITY, MIN_THUMBWHEEL_SENSITIVITY,
SMARTSHIFT_AUTO_DISENGAGE_DEFAULT, SMARTSHIFT_MIN_AUTO_DISENGAGE, ScrollResolution, SmartShift,
WheelMode, clamp_thumbwheel_sensitivity,
};
use crate::binding::{
Action, ActionRingConfig, ActionRingIcon, ActionRingSlot, Binding, ButtonId, GestureDirection,
RingAction, default_binding, default_binding_for, default_gesture_binding,
};
use settings::GestureOwner;
pub const SCHEMA_VERSION: u32 = 4;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub schema_version: u32,
#[serde(default, skip_serializing_if = "AppSettings::is_default")]
pub app_settings: AppSettings,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub selected_device: Option<String>,
#[serde(skip)]
ephemeral: bool,
#[serde(default)]
pub devices: BTreeMap<String, DeviceConfig>,
#[serde(default)]
pub keyboard: KeyboardConfig,
}
impl Default for Config {
fn default() -> Self {
Self {
schema_version: SCHEMA_VERSION,
app_settings: AppSettings::default(),
selected_device: None,
devices: BTreeMap::new(),
ephemeral: false,
keyboard: KeyboardConfig::default(),
}
}
}
impl Config {
#[must_use]
pub fn ephemeral() -> Self {
Self {
ephemeral: true,
..Self::default()
}
}
#[must_use]
pub fn bindings_for(&self, device_key: &str) -> BTreeMap<ButtonId, Binding> {
self.devices
.get(device_key)
.map(|d| d.bindings.clone())
.unwrap_or_default()
}
pub fn set_binding(&mut self, device_key: &str, button: ButtonId, binding: Binding) {
self.devices
.entry(device_key.to_string())
.or_default()
.bindings
.insert(button, binding);
}
pub fn set_keyboard_binding(&mut self, trigger: KeyTrigger, action: Option<Action>) {
match action {
Some(a) => {
self.keyboard.bindings.insert(trigger, a);
}
None => {
self.keyboard.bindings.remove(&trigger);
}
}
}
#[must_use]
pub fn keyboard_bindings(&self) -> &BTreeMap<KeyTrigger, Action> {
&self.keyboard.bindings
}
pub fn set_gesture_direction(
&mut self,
device_key: &str,
button: ButtonId,
direction: GestureDirection,
action: Action,
) {
if let Binding::Gesture(map) = self.ensure_gesture_binding(device_key, button) {
map.insert(direction, action);
}
}
fn ensure_gesture_binding(&mut self, device_key: &str, button: ButtonId) -> &mut Binding {
let entry = self
.devices
.entry(device_key.to_string())
.or_default()
.bindings
.entry(button)
.or_insert_with(|| default_binding_for(button));
entry.upgrade_to_gesture();
entry
}
fn infer_gesture_owner(bindings: &BTreeMap<ButtonId, Binding>) -> Option<ButtonId> {
if let Some((id, _)) = bindings
.iter()
.find(|(id, b)| **id != ButtonId::GestureButton && b.is_gesture())
{
return Some(*id);
}
if matches!(
bindings.get(&ButtonId::GestureButton),
Some(Binding::Single(_))
) {
return None;
}
Some(ButtonId::GestureButton)
}
#[must_use]
pub fn is_gesture_mode(&self, device_key: &str, button: ButtonId) -> bool {
self.devices
.get(device_key)
.and_then(|d| d.bindings.get(&button))
.map_or_else(
|| default_binding_for(button).is_gesture(),
Binding::is_gesture,
)
}
#[must_use]
pub fn gesture_mode_buttons(&self, device_key: &str) -> Vec<ButtonId> {
ButtonId::ALL
.iter()
.copied()
.filter(|b| self.is_gesture_mode(device_key, *b))
.collect()
}
pub fn set_gesture_mode(&mut self, device_key: &str, button: ButtonId, enabled: bool) {
if enabled {
let device = self.devices.entry(device_key.to_string()).or_default();
if let Some(map) = device.disabled_gestures.remove(&button) {
device.bindings.insert(button, Binding::Gesture(map));
} else {
self.ensure_gesture_binding(device_key, button)
.fill_gesture_defaults();
}
return;
}
let device = self.devices.entry(device_key.to_string()).or_default();
match device.bindings.get_mut(&button) {
Some(binding) => {
if let Binding::Gesture(map) = binding {
device.disabled_gestures.insert(button, map.clone());
}
binding.demote_to_single(default_binding(button));
}
None => {
if default_binding_for(button).is_gesture() {
device.disabled_gestures.insert(
button,
GestureDirection::ALL
.iter()
.copied()
.map(|d| (d, default_gesture_binding(d)))
.collect(),
);
device
.bindings
.insert(button, Binding::Single(default_binding(button)));
}
}
}
}
fn migrate_owner_locked_gestures(&mut self) {
for device in self.devices.values_mut() {
let owner = match device.gesture_owner.take() {
Some(GestureOwner::Off) => None,
Some(GestureOwner::Button(id)) => Some(id),
None => Self::infer_gesture_owner(&device.bindings),
};
for (id, binding) in &mut device.bindings {
if Some(*id) != owner {
if let Binding::Gesture(map) = binding {
device.disabled_gestures.insert(*id, map.clone());
}
binding.demote_to_single(default_binding(*id));
}
}
if let Some(owner) = owner
&& owner.is_hidpp_gesture_source()
{
let seeded = || {
Binding::Gesture(
GestureDirection::ALL
.iter()
.copied()
.map(|d| (d, default_gesture_binding(d)))
.collect(),
)
};
match device.bindings.get_mut(&owner) {
Some(binding) if !binding.is_gesture() => *binding = seeded(),
Some(_) => {}
None => {
if !default_binding_for(owner).is_gesture() {
device.bindings.insert(owner, seeded());
}
}
}
}
if owner != Some(ButtonId::GestureButton) {
device
.bindings
.entry(ButtonId::GestureButton)
.or_insert_with(|| Binding::Single(default_binding(ButtonId::GestureButton)));
}
}
}
#[must_use]
pub fn effective_bindings(
&self,
device_key: &str,
bundle_id: Option<&str>,
) -> BTreeMap<ButtonId, Binding> {
let Some(device) = self.devices.get(device_key) else {
return BTreeMap::new();
};
let mut out = device.bindings.clone();
if let Some(bid) = bundle_id
&& let Some(overlay) = app_overlay(&device.per_app_bindings, bid)
{
for (k, v) in overlay {
out.insert(*k, Binding::Single(v.clone()));
}
}
out
}
pub fn set_per_app_binding(
&mut self,
device_key: &str,
bundle_id: &str,
button: ButtonId,
action: Option<Action>,
) {
let entry = self
.devices
.entry(device_key.to_string())
.or_default()
.per_app_bindings
.entry(bundle_id.to_string())
.or_default();
match action {
Some(a) => {
entry.insert(button, a);
}
None => {
entry.remove(&button);
}
}
if let Some(d) = self.devices.get_mut(device_key) {
d.per_app_bindings.retain(|_, m| !m.is_empty());
}
}
#[must_use]
pub fn action_ring(&self, device_key: &str) -> ActionRingConfig {
self.devices
.get(device_key)
.map(|device| device.action_ring.clone())
.unwrap_or_default()
}
pub fn set_action_ring_enabled(&mut self, device_key: &str, enabled: bool) {
self.devices
.entry(device_key.to_string())
.or_default()
.action_ring
.enabled = enabled;
}
pub fn set_action_ring_haptics(&mut self, device_key: &str, enabled: bool) {
self.devices
.entry(device_key.to_string())
.or_default()
.action_ring
.haptics = enabled;
}
pub fn set_action_ring_slot(
&mut self,
device_key: &str,
slot: ActionRingSlot,
action: Option<RingAction>,
) {
self.devices
.entry(device_key.to_string())
.or_default()
.action_ring
.default
.set_action(slot, action);
}
pub fn set_action_ring_icon(
&mut self,
device_key: &str,
slot: ActionRingSlot,
icon: Option<ActionRingIcon>,
) {
self.devices
.entry(device_key.to_string())
.or_default()
.action_ring
.default
.set_icon(slot, icon);
}
#[must_use]
pub fn selected_device(&self) -> Option<&str> {
self.selected_device.as_deref()
}
pub fn set_selected_device(&mut self, key: Option<String>) {
self.selected_device = key;
}
#[must_use]
pub fn dpi_presets(&self, device_key: &str) -> Vec<u32> {
self.devices
.get(device_key)
.map(|d| d.dpi_presets.clone())
.unwrap_or_default()
}
pub fn set_dpi_presets(&mut self, device_key: &str, presets: Vec<u32>) {
self.devices
.entry(device_key.to_string())
.or_default()
.dpi_presets = presets;
}
#[must_use]
pub fn device_identity(&self, device_key: &str) -> Option<&DeviceIdentity> {
self.devices
.get(device_key)
.and_then(|d| d.identity.as_ref())
}
pub fn set_device_identity(&mut self, device_key: &str, identity: DeviceIdentity) {
self.devices
.entry(device_key.to_string())
.or_default()
.identity = Some(identity.without_unit_identifiers());
}
#[must_use]
pub fn has_app_override(&self, device_key: &str, app: &str) -> bool {
self.devices.get(device_key).is_some_and(|d| {
app_overlay(&d.per_app_bindings, app).is_some_and(|overlay| !overlay.is_empty())
})
}
pub fn known_identities(&self) -> impl Iterator<Item = (&str, &DeviceIdentity)> {
self.devices
.iter()
.filter_map(|(k, d)| d.identity.as_ref().map(|i| (k.as_str(), i)))
}
#[must_use]
pub fn lighting(&self, device_key: &str) -> Option<Lighting> {
self.devices
.get(device_key)
.and_then(|d| d.lighting.clone())
}
pub fn set_lighting(&mut self, device_key: &str, lighting: Lighting) {
self.devices
.entry(device_key.to_string())
.or_default()
.lighting = Some(lighting);
}
#[must_use]
pub fn camera_controls(&self, device_key: &str) -> Option<CameraControls> {
self.devices
.get(device_key)
.and_then(|d| d.camera_controls.clone())
}
pub fn set_camera_controls(&mut self, device_key: &str, controls: CameraControls) {
self.devices
.entry(device_key.to_string())
.or_default()
.camera_controls = Some(controls);
}
#[must_use]
pub fn camera_profiles(&self, device_key: &str) -> BTreeMap<String, CameraControls> {
self.devices
.get(device_key)
.map(|d| d.camera_profiles.clone())
.unwrap_or_default()
}
pub fn save_camera_profile(&mut self, device_key: &str, name: &str, snap: CameraControls) {
self.devices
.entry(device_key.to_string())
.or_default()
.camera_profiles
.insert(name.to_string(), snap);
}
pub fn delete_camera_profile(&mut self, device_key: &str, name: &str) {
if let Some(device) = self.devices.get_mut(device_key) {
device.camera_profiles.remove(name);
if device.camera_profile.as_deref() == Some(name) {
device.camera_profile = None;
}
}
}
#[must_use]
pub fn camera_active_profile(&self, device_key: &str) -> Option<String> {
self.devices
.get(device_key)
.and_then(|d| d.camera_profile.clone())
}
pub fn set_camera_active_profile(&mut self, device_key: &str, name: Option<String>) {
self.devices
.entry(device_key.to_string())
.or_default()
.camera_profile = name;
}
#[must_use]
pub fn light(&self, device_key: &str) -> Option<LightSettings> {
self.devices.get(device_key).and_then(|d| d.light)
}
pub fn set_light(&mut self, device_key: &str, light: LightSettings) {
self.devices
.entry(device_key.to_string())
.or_default()
.light = Some(light);
}
#[must_use]
pub fn dpi(&self, device_key: &str) -> Option<u32> {
self.devices.get(device_key).and_then(|d| d.dpi)
}
pub fn set_dpi(&mut self, device_key: &str, dpi: u32) {
self.devices.entry(device_key.to_string()).or_default().dpi = Some(dpi);
}
#[must_use]
pub fn smartshift(&self, device_key: &str) -> Option<SmartShift> {
self.devices.get(device_key).and_then(|d| d.smartshift)
}
#[must_use]
pub fn fn_lock(&self, device_key: &str) -> Option<bool> {
self.devices.get(device_key).and_then(|d| d.fn_lock)
}
pub fn set_smartshift(&mut self, device_key: &str, smartshift: SmartShift) {
self.devices
.entry(device_key.to_string())
.or_default()
.smartshift = Some(smartshift);
}
#[must_use]
pub fn invert_scroll(&self, device_key: &str) -> bool {
self.devices
.get(device_key)
.is_some_and(|d| d.invert_scroll)
}
pub fn set_invert_scroll(&mut self, device_key: &str, invert: bool) {
self.devices
.entry(device_key.to_string())
.or_default()
.invert_scroll = invert;
}
#[must_use]
pub fn scroll_resolution(&self, device_key: &str) -> Option<ScrollResolution> {
self.devices
.get(device_key)
.and_then(|device| device.scroll_resolution)
}
pub fn set_scroll_resolution(
&mut self,
device_key: &str,
resolution: Option<ScrollResolution>,
) {
self.devices
.entry(device_key.to_string())
.or_default()
.scroll_resolution = resolution;
}
#[must_use]
pub fn device_enabled(&self, device_key: &str) -> bool {
self.devices.get(device_key).is_none_or(|d| d.enabled)
}
pub fn set_device_enabled(&mut self, device_key: &str, enabled: bool) {
self.devices
.entry(device_key.to_string())
.or_default()
.enabled = enabled;
}
#[must_use]
pub fn thumbwheel_sensitivity(&self, device_key: &str) -> i32 {
self.devices
.get(device_key)
.and_then(|d| d.thumbwheel_sensitivity)
.unwrap_or(self.app_settings.thumbwheel_sensitivity)
}
pub fn set_device_thumbwheel_sensitivity(
&mut self,
device_key: &str,
sensitivity: Option<i32>,
) {
self.devices
.entry(device_key.to_string())
.or_default()
.thumbwheel_sensitivity = sensitivity.map(clamp_thumbwheel_sensitivity);
}
}
fn app_overlay<'a, T>(overlays: &'a BTreeMap<String, T>, app: &str) -> Option<&'a T> {
overlays.get(app).or_else(|| {
let executable_name = app.rsplit(['\\', '/']).next()?;
if executable_name.is_empty()
|| !Path::new(executable_name)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("exe"))
{
return None;
}
overlays.get(&format!("exe:{}", executable_name.to_ascii_lowercase()))
})
}