openlogi-core 0.8.8

Core types, config, and paths for OpenLogi. No I/O specifics.
Documentation
//! One device's stored settings, a field at a time: the getters and setters
//! that address a [`DeviceConfig`](super::DeviceConfig) entry by its config key.
//!
//! A getter answers for a device with no entry the way a fresh entry would, and
//! a setter creates the entry it needs. Settings with rules of their own live
//! beside this file: gesture mode in [`gestures`](super::gestures),
//! per-application profiles in [`per_app`](super::per_app).

use std::collections::BTreeMap;

use super::{
    CameraControls, Config, DeviceIdentity, LightSettings, Lighting, ScrollResolution, SmartShift,
    ThumbwheelSensitivity,
};
use crate::binding::{
    ActionRingConfig, ActionRingIcon, ActionRingSlot, Binding, ButtonId, RingAction,
};
use crate::hid::Dpi;

impl Config {
    /// The bindings stored for `device_key` as they were committed, or an
    /// empty map when the device has none yet. The effective per-button map,
    /// with defaults and the per-app overlay applied, is
    /// [`crate::bindings::bindings_for`].
    #[must_use]
    pub fn stored_bindings(&self, device_key: &str) -> BTreeMap<ButtonId, Binding> {
        self.devices
            .get(device_key)
            .map(|d| d.bindings.clone())
            .unwrap_or_default()
    }

    /// Records `binding` for `button` on `device_key`, creating the device
    /// entry if needed. Replaces the whole binding (use
    /// [`Self::set_gesture_direction`] to edit one direction of a gesture
    /// binding in place).
    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);
    }

    /// Actions Ring settings for `device_key`, falling back to defaults when
    /// the device has no saved ring configuration.
    #[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()
    }

    /// Enable or disable `device_key`'s Actions Ring.
    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;
    }

    /// Enable or disable ring hover and activation haptics.
    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;
    }

    /// Replace or clear one slot in the default Actions Ring layout.
    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);
    }

    /// Set or restore the action-derived icon for one default ring slot.
    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);
    }

    /// The ordered DPI preset list for `device_key`, or an empty `Vec` if the
    /// device has none configured yet.
    #[must_use]
    pub fn dpi_presets(&self, device_key: &str) -> Vec<Dpi> {
        self.devices
            .get(device_key)
            .map(|d| d.dpi_presets.clone())
            .unwrap_or_default()
    }

    /// Replace the DPI preset list for `device_key`. Pass an empty `Vec` to
    /// clear (the device block is kept; the field is just omitted on save
    /// thanks to `skip_serializing_if`).
    pub fn set_dpi_presets(&mut self, device_key: &str, presets: Vec<Dpi>) {
        self.devices
            .entry(device_key.to_string())
            .or_default()
            .dpi_presets = presets;
    }

    /// The last-known [`DeviceIdentity`] for `device_key`, or `None` if the
    /// device has never been seen online (or was configured before identities
    /// were recorded).
    #[must_use]
    pub fn device_identity(&self, device_key: &str) -> Option<&DeviceIdentity> {
        self.devices
            .get(device_key)
            .and_then(|d| d.identity.as_ref())
    }

    /// Record (or refresh) the identity captured for `device_key` while it was
    /// online, creating the device entry if needed.
    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());
    }

    /// Drop everything recorded for `device_key` — identity, custom name, and
    /// per-device settings. Returns whether an entry existed.
    pub fn remove_device(&mut self, device_key: &str) -> bool {
        self.devices.remove(device_key).is_some()
    }

    /// The user-assigned name for `device_key`, if one is configured.
    #[must_use]
    pub fn device_custom_name(&self, device_key: &str) -> Option<&str> {
        self.devices
            .get(device_key)
            .and_then(|device| device.custom_name.as_deref())
    }

    /// Set the user-assigned name for `device_key`, or clear it to use the
    /// hardware model name again.
    pub fn set_device_custom_name(&mut self, device_key: &str, custom_name: Option<String>) {
        self.devices
            .entry(device_key.to_string())
            .or_default()
            .custom_name = custom_name;
    }

    /// Iterate every device we've recorded an identity for, as
    /// `(config_key, identity)`. Used to seed offline placeholder cards so a
    /// known device stays visible (with its panels) before any live probe.
    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)))
    }

    /// The lighting config for `device_key`, or `None` if unset.
    #[must_use]
    pub fn lighting(&self, device_key: &str) -> Option<Lighting> {
        self.devices
            .get(device_key)
            .and_then(|d| d.lighting.clone())
    }

    /// Replace the lighting config for `device_key`.
    pub fn set_lighting(&mut self, device_key: &str, lighting: Lighting) {
        self.devices
            .entry(device_key.to_string())
            .or_default()
            .lighting = Some(lighting);
    }

    /// The saved UVC image controls for `device_key`, or `None` if never set.
    #[must_use]
    pub fn camera_controls(&self, device_key: &str) -> Option<CameraControls> {
        self.devices
            .get(device_key)
            .and_then(|d| d.camera_controls.clone())
    }

    /// Replace the saved UVC image controls for `device_key`.
    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);
    }

    /// The saved custom camera profiles for `device_key` (name → snapshot).
    #[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()
    }

    /// Save (or overwrite) a custom camera profile for `device_key`.
    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);
    }

    /// Delete a custom camera profile, clearing the active selection if it
    /// named it. Unknown names are a no-op.
    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;
            }
        }
    }

    /// The last-applied camera profile name for `device_key`, if any.
    #[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())
    }

    /// Record which camera profile `device_key` last applied.
    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;
    }

    /// The standalone-light config for `device_key`, or `None` if unset.
    #[must_use]
    pub fn light(&self, device_key: &str) -> Option<LightSettings> {
        self.devices.get(device_key).and_then(|d| d.light)
    }

    /// Replace the standalone-light config for `device_key`.
    pub fn set_light(&mut self, device_key: &str, light: LightSettings) {
        self.devices
            .entry(device_key.to_string())
            .or_default()
            .light = Some(light);
    }

    /// The committed sensor DPI for `device_key`, or `None` if never set.
    #[must_use]
    pub fn dpi(&self, device_key: &str) -> Option<Dpi> {
        self.devices.get(device_key).and_then(|d| d.dpi)
    }

    /// Record the committed sensor DPI for `device_key`, so the agent can
    /// re-apply it when the device reconnects (#189).
    pub fn set_dpi(&mut self, device_key: &str, dpi: Dpi) {
        self.devices.entry(device_key.to_string()).or_default().dpi = Some(dpi);
    }

    /// The SmartShift wheel config for `device_key`, or `None` if never set.
    #[must_use]
    pub fn smartshift(&self, device_key: &str) -> Option<SmartShift> {
        self.devices.get(device_key).and_then(|d| d.smartshift)
    }

    /// The persisted keyboard Fn-lock state for `device_key`, or `None` when
    /// the user never set one (the keyboard keeps its own state).
    #[must_use]
    pub fn fn_lock(&self, device_key: &str) -> Option<bool> {
        self.devices.get(device_key).and_then(|d| d.fn_lock)
    }

    /// Record the SmartShift wheel config for `device_key`, so the agent can
    /// re-apply it when the device reconnects (#189).
    pub fn set_smartshift(&mut self, device_key: &str, smartshift: SmartShift) {
        self.devices
            .entry(device_key.to_string())
            .or_default()
            .smartshift = Some(smartshift);
    }

    /// Whether `device_key`'s scroll wheel is inverted (issue #126). `false`
    /// (the native direction) for an unconfigured or absent device.
    #[must_use]
    pub fn invert_scroll(&self, device_key: &str) -> bool {
        self.devices
            .get(device_key)
            .is_some_and(|d| d.invert_scroll)
    }

    /// Set whether `device_key`'s scroll wheel is inverted. The agent reads this
    /// on the next `ReloadConfig` and applies it in the OS hook.
    pub fn set_invert_scroll(&mut self, device_key: &str, invert: bool) {
        self.devices
            .entry(device_key.to_string())
            .or_default()
            .invert_scroll = invert;
    }

    /// The configured wheel resolution for `device_key`, or `None` when
    /// OpenLogi should leave the device's current resolution unchanged.
    #[must_use]
    pub fn scroll_resolution(&self, device_key: &str) -> Option<ScrollResolution> {
        self.devices
            .get(device_key)
            .and_then(|device| device.scroll_resolution)
    }

    /// Set the wheel resolution OpenLogi should restore for `device_key`.
    /// Passing `None` returns the device to its unmanaged default state.
    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;
    }

    /// Whether OpenLogi manages `device_key` at all (capture + volatile
    /// re-apply). Unconfigured devices are managed.
    #[must_use]
    pub fn device_enabled(&self, device_key: &str) -> bool {
        self.devices.get(device_key).is_none_or(|d| d.enabled)
    }

    /// Enable or disable OpenLogi's management of `device_key`.
    pub fn set_device_enabled(&mut self, device_key: &str, enabled: bool) {
        self.devices
            .entry(device_key.to_string())
            .or_default()
            .enabled = enabled;
    }

    /// The effective thumb-wheel sensitivity for `device_key`: the device's
    /// override when set, else the app-wide default.
    #[must_use]
    pub fn thumbwheel_sensitivity(&self, device_key: &str) -> ThumbwheelSensitivity {
        self.devices
            .get(device_key)
            .and_then(|d| d.thumbwheel_sensitivity)
            .unwrap_or(self.app_settings.thumbwheel_sensitivity)
    }

    /// Set (or clear, with `None`) `device_key`'s thumb-wheel sensitivity
    /// override.
    pub fn set_device_thumbwheel_sensitivity(
        &mut self,
        device_key: &str,
        sensitivity: Option<ThumbwheelSensitivity>,
    ) {
        self.devices
            .entry(device_key.to_string())
            .or_default()
            .thumbwheel_sensitivity = sensitivity;
    }
}